move edit motifs to the edit menu option
[mussa.git] / qui / MussaWindow.cpp
1 #include <QAction>
2 #include <QApplication>
3 #include <QAssistantClient>
4 #include <QDir>
5 #include <QFileDialog>
6 #include <QHBoxLayout>
7 #include <QIcon>
8 #include <QMenuBar>
9 #include <QMessageBox>
10 #include <QScrollBar>
11 #include <QStatusBar>
12 #include <QString>
13 #include <QStringList>
14 #include <QWhatsThis>
15
16 #if defined(Q_WS_MAC)
17 #include <CoreFoundation/CoreFoundation.h>
18 #endif 
19
20 #include "qui/MussaWindow.hpp"
21 #include "mussa_exceptions.hpp"
22
23 #include <memory>
24 #include <iostream>
25
26 #include <boost/filesystem/path.hpp>
27 namespace fs = boost::filesystem;
28 #include <boost/bind.hpp>
29
30 using namespace std;
31
32 MussaWindow::MussaWindow(Mussa *analysis_, QWidget *parent) :
33   QMainWindow(parent),
34   analysis(analysis_),
35   motif_editor(0),
36   setup_analysis_dialog(this),
37   browser(this),
38   mussaViewTB("Path Views"),
39   zoom(),
40   threshold(),
41   progress_dialog(0),
42   aboutAction(0),
43   closeAction(0),
44   createNewAnalysisAction(0),
45   createSubAnalysisAction(0),
46   editMotifsAction(0),
47   loadMotifListAction(0),
48   loadMupaAction(0),
49   loadSavedAnalysisAction(0),
50   mussaManualAssistantAction(0),
51   newMussaWindowAction(0),
52   saveMotifListAction(0),
53   showMussaViewToolbarAction(0),
54   toggleMotifsAction(0),
55   saveBrowserPixmapAction(0),
56   whatsThisAction(0),
57   viewMussaAlignmentAction(0),
58   manualAssistant(0)
59 {
60   setupActions();
61   setupMainMenu();
62   setupAssistant();
63
64   //This next setWhatsThis function prevents
65   // a segfault when using WhatsThis feature with 
66   // opengl widget.
67   //scene->setWhatsThis(tr("Mussa in OpenGL!"));
68   setCentralWidget(&browser);
69   // well updatePosition isn't quite right as we really just need
70   // to call update()
71   connect(this, SIGNAL(changedAnnotations()), &browser, SLOT(update()));
72
73   mussaViewTB.addAction(toggleMotifsAction);
74   mussaViewTB.addWidget(&zoom);
75   
76   connect(&zoom, SIGNAL(valueChanged(double)), 
77           &browser, SLOT(setZoom(double)));
78   
79   // threshold range is set in updateAnalysis
80   
81   //scene->setClipPlane(20);
82   // FIXME: for when we get the paths drawn at the appropriate depth
83   //connect(&threshold, SIGNAL(thresholdChanged(int)),
84   //        this, SLOT(setClipPlane(int)));
85   connect(&threshold, SIGNAL(thresholdChanged(int)),
86           this, SLOT(setSoftThreshold(int)));
87   mussaViewTB.addWidget(&threshold);
88
89   addToolBar(&mussaViewTB);
90
91   statusBar()->showMessage("Welcome to mussa", 2000);
92   connect(analysis, SIGNAL(progress(const std::string&, int, int)),
93           this, SLOT(updateProgress(const std::string&, int, int)));
94   updateAnalysis();
95 }
96
97 MussaWindow::~MussaWindow()
98 {
99   if (analysis != 0) delete analysis;
100   aligned_windows.clear();
101   if (motif_editor != 0) delete motif_editor;
102   if (progress_dialog != 0) delete progress_dialog;
103
104   if (aboutAction != 0) delete aboutAction;
105   if (closeAction != 0) delete closeAction;
106   if (createNewAnalysisAction != 0) delete createNewAnalysisAction;
107   if (createSubAnalysisAction != 0) delete createSubAnalysisAction;
108   if (editMotifsAction != 0) delete editMotifsAction;
109   if (loadMotifListAction != 0) delete loadMotifListAction;
110   if (loadMupaAction != 0) delete loadMupaAction;
111   if (loadSavedAnalysisAction != 0) delete loadSavedAnalysisAction;
112   if (mussaManualAssistantAction != 0) delete mussaManualAssistantAction;
113   if (newMussaWindowAction != 0) delete newMussaWindowAction;
114   if (saveBrowserPixmapAction != 0) delete saveBrowserPixmapAction;
115   if (saveMotifListAction != 0) delete saveMotifListAction;
116   if (showMussaViewToolbarAction != 0) delete showMussaViewToolbarAction;
117   if (toggleMotifsAction != 0) delete toggleMotifsAction;
118   if (whatsThisAction != 0) delete whatsThisAction;
119   if (viewMussaAlignmentAction != 0) delete viewMussaAlignmentAction;
120   
121   if (manualAssistant != 0) delete manualAssistant;
122   
123 }
124
125 void MussaWindow::setAnalysis(Mussa *new_analysis)
126 {
127   if (new_analysis != 0) {
128     // only switch mussas if we loaded without error
129     clear();
130     delete analysis;
131     analysis = new_analysis;
132     setWindowTitle(analysis->get_name().c_str());
133     updateAnalysis();
134   }
135 }
136
137 void MussaWindow::setupActions()
138 {
139   // we really don't want to run this more than once.
140   assert (closeAction == 0);
141
142   // the ever popular about box
143   aboutAction = new QAction(tr("&About"), this);
144   connect(aboutAction, SIGNAL(triggered()), this, SLOT(about()));
145   aboutAction->setIcon(QIcon(":/icons/info.png"));
146
147   // add exit
148   closeAction = new QAction(tr("&Close"), this);
149   closeAction->setStatusTip(tr("Close this window"));
150   connect(closeAction, SIGNAL(triggered()), this, SLOT(close()));
151   closeAction->setIcon(QIcon(":/icons/exit.png"));
152   
153   createNewAnalysisAction = new QAction(tr("Create Analysis"), this);
154   connect(createNewAnalysisAction, SIGNAL(triggered()), 
155           this, SLOT(createNewAnalysis()));
156   createNewAnalysisAction->setIcon(QIcon(":/icons/filenew.png"));
157   
158   createSubAnalysisAction = new QAction(tr("Add to Subanalysis"), this);
159   connect(createSubAnalysisAction, SIGNAL(triggered()), 
160           this, SLOT(createSubAnalysis()));
161
162   editMotifsAction = new QAction(tr("Edit Motifs"), this);;
163   connect(editMotifsAction, SIGNAL(triggered()), this, SLOT(editMotifs()));
164   
165   loadMotifListAction = new QAction(tr("Load Motif List"), this);
166   connect(loadMotifListAction, SIGNAL(triggered()), 
167           this, SLOT(loadMotifList()));
168   loadMotifListAction->setIcon(QIcon(":/icons/fileopen.png"));
169   
170   loadMupaAction = new QAction(tr("Create Analysis from File"), this);
171   connect(loadMupaAction, SIGNAL(triggered()), 
172           this, SLOT(loadMupa()));
173   loadMupaAction->setIcon(QIcon(":/icons/fileopen.png"));
174
175   loadSavedAnalysisAction = new QAction(tr("Load Existing &Analysis"), this);
176   connect(loadSavedAnalysisAction, SIGNAL(triggered()), 
177           this, SLOT(loadSavedAnalysis()));
178   loadSavedAnalysisAction->setIcon(QIcon(":/icons/fileopen.png"));
179
180   mussaManualAssistantAction = new QAction(tr("Mussagl Manual..."), this);
181   mussaManualAssistantAction->setIcon(QIcon(":/icons/contents.png"));
182   connect(mussaManualAssistantAction, SIGNAL(triggered()),
183           this, SLOT(showManual()));
184
185   newMussaWindowAction = new QAction(tr("&New Mussa Window"), this);
186   newMussaWindowAction->setStatusTip("open another mussa window to allow comparing results");
187   connect(newMussaWindowAction, SIGNAL(triggered()), 
188           this, SLOT(newMussaWindow()));
189   newMussaWindowAction->setIcon(QIcon(":/icons/window_new.png"));
190
191   saveMotifListAction = new QAction(tr("Save Motifs"), this);
192   connect(saveMotifListAction, SIGNAL(triggered()), 
193           this, SLOT(saveMotifList()));
194   saveMotifListAction->setIcon(QIcon(":/icons/filesave.png"));
195
196   showMussaViewToolbarAction = new QAction(tr("Show Toolbar"), this);
197   connect(showMussaViewToolbarAction, SIGNAL(triggered()), 
198           this, SLOT(showMussaToolbar()));
199   showMussaViewToolbarAction->setCheckable(true);
200   showMussaViewToolbarAction->setChecked(true);
201
202   toggleMotifsAction = new QAction(tr("Toggle Motifs"), this);
203   connect(toggleMotifsAction, SIGNAL(triggered()), 
204           this, SLOT(toggleMotifs()));
205   toggleMotifsAction->setCheckable(true);
206   toggleMotifsAction->setIcon(QIcon(":/icons/motif_icon.png"));
207   toggleMotifsAction->setWhatsThis(tr("Toggle motif annotations on/off\n\n"
208                                    "You can load motif annotations via "
209                                    "'File->Load Motif List' menu option."));
210
211   //Save pixel map action
212   saveBrowserPixmapAction = new QAction(tr("Save to image..."), this);
213   connect(saveBrowserPixmapAction, (SIGNAL(triggered())),
214           &browser, SLOT(promptSaveBrowserPixmap()));
215   saveBrowserPixmapAction->setIcon(QIcon(":/icons/image2.png"));
216
217   viewMussaAlignmentAction = new QAction(tr("View sequence alignment"), this);
218   connect(viewMussaAlignmentAction, SIGNAL(triggered()),
219           this, SLOT(viewMussaAlignment() ));
220   viewMussaAlignmentAction->setWhatsThis(tr("Create a zoomed in window "
221                                             "showing alignment of the seqcomp "
222                                             "defined paths"));
223
224   whatsThisAction = QWhatsThis::createAction(this);
225   whatsThisAction->setIcon(QIcon(":/icons/help.png"));
226
227
228 }
229
230 void MussaWindow::setupMainMenu()
231 {
232   // we need to run setupActions first
233   assert (closeAction != 0);
234
235   QMenu *newMenu = menuBar()->addMenu(tr("&File"));
236   
237   newMenu->addAction(newMussaWindowAction);
238   newMenu->addAction(createNewAnalysisAction);
239   newMenu->addAction(loadMupaAction);
240   newMenu->addAction(loadSavedAnalysisAction);
241   newMenu->addSeparator();
242   newMenu->addAction(loadMotifListAction);
243   newMenu->addAction(saveMotifListAction);
244   newMenu->addSeparator();
245   newMenu->addAction(saveBrowserPixmapAction);
246   newMenu->addSeparator();
247   newMenu->addAction(closeAction);
248
249   newMenu = menuBar()->addMenu(tr("&Edit"));
250   newMenu->addAction(editMotifsAction);
251   newMenu->addAction(&browser.getCopySelectedSequenceAsFastaAction());
252   newMenu->addAction(createSubAnalysisAction);
253  
254   newMenu = menuBar()->addMenu(tr("&View"));
255   newMenu->addAction(viewMussaAlignmentAction);
256   newMenu->addAction(showMussaViewToolbarAction);
257
258   newMenu = menuBar()->addMenu(tr("&Help"));
259 #if defined(QT_ASSISTANT_LIB)
260   newMenu->addAction(mussaManualAssistantAction);
261 #endif
262   newMenu->addAction(whatsThisAction);
263   newMenu->addSeparator();
264   newMenu->addAction(aboutAction);
265
266   // add some extra features to the context menu
267   QMenu& popupMenu = browser.getPopupMenu();
268   popupMenu.addAction(viewMussaAlignmentAction);
269   popupMenu.addAction(createSubAnalysisAction);
270 }
271
272 void MussaWindow::setupAssistant()
273 {
274 #if 0 && defined(Q_WS_MAC)
275     CFURLRef pluginRef = CFBundleCopyBundleURL(CFBundleGetMainBundle());
276     CFStringRef macPath = CFURLCopyFileSystemPath(pluginRef,
277                                            kCFURLPOSIXPathStyle);
278     const char *pathPtr = CFStringGetCStringPtr(macPath,
279                                            CFStringGetSystemEncoding());
280     qDebug("Path = %s", pathPtr);
281     CFRelease(pluginRef);
282     CFRelease(macPath);
283 #endif
284 #if defined(QT_ASSISTANT_LIB)
285   QStringList manualAssistantArgs;
286   manualAssistantArgs = QStringList();
287   manualAssistantArgs << "-profile" << "./doc/manual/mussagl_manual.adp";
288   manualAssistant = new QAssistantClient("assistant", this);
289   manualAssistant->setArguments(manualAssistantArgs);
290   connect(manualAssistant, SIGNAL(error(QString)),
291           this, SLOT(assistantError(QString)));
292 #endif
293 }
294   
295 void MussaWindow::about()
296 {
297   QString msg("Welcome to Multiple Species Sequence Analysis\n"
298               "(c) 2005-2006 California Institute of Technology\n"
299               "Tristan De Buysscher, Diane Trout\n");
300    msg += "\n\r";
301    msg += "Path: ";
302    msg += QDir::currentPath();
303    msg += "\n";
304    msg += "OpenGL: ";
305    msg += (char *)glGetString(GL_VERSION);
306    msg += "\n";
307    QMessageBox::about(this, tr("About mussa"), msg);
308 }
309
310 void MussaWindow::clear()
311 {
312   aligned_windows.clear();
313   browser.clear();
314 }
315
316 void MussaWindow::createNewAnalysis()
317 {
318   try {
319     if (setup_analysis_dialog.exec()) {
320       Mussa *m = 0;
321       m = setup_analysis_dialog.getMussa();
322       setAnalysis(m);
323     } else {
324       std::cout << "New mussa exp. aborted!\n";
325     }
326   } catch(mussa_error e) {
327     QString msg(e.what());
328     QMessageBox::warning(this, tr("Create New Analysis"), msg);
329   }
330 }
331
332 void MussaWindow::createSubAnalysis()
333 {
334   if (not subanalysis_window.isVisible()) {
335     subanalysis_window.show();
336   }
337 }
338
339 void MussaWindow::editMotifs()
340 {
341   if (motif_editor != 0) {
342     motif_editor->hide();
343     delete motif_editor;
344   }
345   motif_editor = new MotifEditor(analysis);
346   connect(motif_editor, SIGNAL(changedMotifs()), 
347           this, SLOT(updateAnnotations()));
348   motif_editor->show();
349 }
350
351 void MussaWindow::loadMotifList()
352 {
353   QString caption("Load a motif list");
354   QString filter("Motif list(*.txt *.mtl)");
355   QString path = QFileDialog::getOpenFileName(this,
356                                                    caption, 
357                                                    QDir::currentPath(),
358                                                    filter);
359   // user hit cancel?
360   if (path.isNull()) 
361     return;
362   // try to load safely
363   try {
364     fs::path converted_path(path.toStdString(), fs::native);
365     analysis->load_motifs(converted_path);
366   } catch (runtime_error e) {
367     QString msg("Unable to load ");
368     msg += path;
369     msg += "\n";
370     msg += e.what();
371     QMessageBox::warning(this, "Load Motifs", msg);
372   }
373   assert (analysis != 0);
374 }
375
376 void MussaWindow::saveMotifList()
377 {
378   NotImplementedBox();
379 }
380
381 void MussaWindow::loadMupa()
382 {
383   QString caption("Load a mussa parameter file");
384   QString filter("Mussa Parameters (*.mupa)");
385   QString mupa_path = QFileDialog::getOpenFileName(this,
386                                                    caption, 
387                                                    QDir::currentPath(),
388                                                    filter);
389   // user hit cancel?
390   if (mupa_path.isNull()) 
391     return;
392   // try to load safely
393   try {
394     Mussa *m = new Mussa;
395     fs::path converted_path(mupa_path.toStdString(), fs::native);
396     connect(m, SIGNAL(progress(const std::string&, int, int)),
397             this, SLOT(updateProgress(const std::string&, int, int)));
398     m->load_mupa_file(converted_path);
399     m->analyze();
400     setAnalysis(m);
401     setWindowTitle(converted_path.native_file_string().c_str());
402   } catch (mussa_load_error e) {
403     QString msg("Unable to load ");
404     msg += mupa_path;
405     msg += "\n";
406     msg += e.what();
407     QMessageBox::warning(this, "Load Parameter", msg);
408   }
409   assert (analysis != 0);
410 }
411
412 void MussaWindow::loadSavedAnalysis()
413 {
414   QString caption("Load a previously run analysis");
415   QString muway_dir = QFileDialog::getExistingDirectory(this,
416                                                         caption, 
417                                                         QDir::currentPath());
418   // user hit cancel?
419   if (muway_dir.isNull()) 
420     return;
421   // try to safely load
422   try {
423     Mussa *m = new Mussa;
424     fs::path converted_path(muway_dir.toStdString(), fs::native);
425     connect(m, SIGNAL(progress(const std::string&, int, int)),
426             this, SLOT(updateProgress(const std::string&, int, int)));
427     m->load(converted_path);
428     // only switch mussas if we loaded without error
429     setAnalysis(m);
430     setWindowTitle(converted_path.native_file_string().c_str());
431   } catch (mussa_load_error e) {
432     QString msg("Unable to load ");
433     msg += muway_dir;
434     msg += "\n";
435     msg += e.what();
436     QMessageBox::warning(this, "Load Parameter", msg);
437   }
438   assert (analysis != 0);
439 }
440
441 void MussaWindow::newMussaWindow()
442 {
443   Mussa *a = new Mussa();
444   MussaWindow *win = new MussaWindow(a);
445   win->show();
446 }
447
448 void MussaWindow::setSoftThreshold(int threshold)
449 {
450   if (analysis->get_soft_threshold() != threshold) {
451     analysis->set_soft_threshold(threshold);
452     analysis->nway();
453     updateLinks();
454     update();
455   }
456 }
457
458 void MussaWindow::showMussaToolbar()
459 {
460   if (mussaViewTB.isVisible())
461     mussaViewTB.hide();
462   else
463     mussaViewTB.show();
464 }
465
466 void MussaWindow::toggleMotifs()
467 {
468   NotImplementedBox();
469 }
470
471 void MussaWindow::showManual()
472 {
473   manualAssistant->openAssistant();
474 }
475
476 void MussaWindow::assistantError(QString message)
477 {
478   //std::cout << "QAssistantError: " << message.toStdString() << "\n";
479   QMessageBox::warning ( this, "Warning: Mussagl Manual", message, 
480                          QMessageBox::Ok, 
481                          QMessageBox::NoButton);
482 }
483
484 void MussaWindow::NotImplementedBox()
485 {
486   QMessageBox::warning(this, QObject::tr("mussa"), QObject::tr("Not implemented yet"));
487 }      
488
489 void MussaWindow::viewMussaAlignment()
490 {
491   const set<int>& selected_paths = browser.selectedPaths();
492   if (selected_paths.size() == 0 ) {
493     QMessageBox::warning(this, 
494                          QObject::tr("mussa"),
495                          QObject::tr("you should probably select some paths "
496                                      "first"));
497   } else {
498     boost::shared_ptr<MussaAlignedWindow> ma_win( 
499       new MussaAlignedWindow(*analysis, selected_paths)
500     );
501
502     aligned_windows.push_back(ma_win);
503     connect(this, SIGNAL(changedAnnotations()), 
504             aligned_windows.back().get(), SLOT(update()));
505     aligned_windows.back()->show();
506   }
507 }
508                         
509 void MussaWindow::updateAnalysis()
510 {
511   threshold.setRange(analysis->get_threshold(),analysis->get_window());
512
513   const Mussa::vector_sequence_type& seqs = analysis->sequences();
514   browser.setSequences(seqs, analysis->colorMapper());
515   updateLinks();
516   browser.zoomOut();
517   zoom.setValue(browser.zoom());
518 }
519
520 void MussaWindow::updateAnnotations()
521 {
522   // motifs were changed in the sequences by 
523   // Mussa::update_sequences_motifs
524   emit changedAnnotations();
525   browser.update();
526 }
527
528 void MussaWindow::updateLinks()
529 {
530   browser.clear_links();
531   bool reversed = false;
532   const NwayPaths& nway = analysis->paths();
533
534   typedef list<ConservedPath> conserved_paths;
535   typedef conserved_paths::const_iterator const_conserved_paths_itor;
536   for(const_conserved_paths_itor path_itor = nway.refined_pathz.begin();
537       path_itor != nway.refined_pathz.end();
538       ++path_itor)
539   {
540     // since we were drawing to the start of a window, and opengl lines
541     // are centered around the two connecting points our lines were slightly
542     // offset. the idea of window_offset is to adjust them to the
543     // right for forward compliment or left for reverse compliment
544     // FIXME: figure out how to unit test these computations
545     //GLfloat window_offset = (path_itor->window_size)/2.0;
546
547     size_t track_len = path_itor->track_indexes.size();
548     vector<int> normalized_path;
549     normalized_path.reserve(track_len);
550     vector<bool> rc_flags(false, track_len);
551     for (size_t track_i=0; track_i != track_len; ++track_i)
552     {
553       int x = path_itor->track_indexes[track_i];
554       // at some point when we modify the pathz data structure to keep
555       // track of the score we can put grab the depth here.
556       //
557       // are we reverse complimented?
558       if ( x>=0) {
559         reversed = false;
560       } else {
561         reversed = true;
562         // make positive 
563         x = -x;
564       }
565       normalized_path.push_back(x);
566       rc_flags.push_back(reversed);
567     }
568     browser.link(normalized_path, rc_flags, path_itor->window_size);
569   }
570   browser.update();
571 }
572
573 void 
574 MussaWindow::updateProgress(const string& description, int current, int max)
575 {  
576   // if we're done  
577   if (current == max) {
578     if (progress_dialog != 0) {
579       progress_dialog->hide();
580       delete progress_dialog;
581       progress_dialog = 0;
582     }
583   } else {
584     // if we're starting, create the dialog
585     if (progress_dialog == 0) {
586       QString desc(description.c_str());
587       QString cancel("Cancel");
588       progress_dialog = new QProgressDialog(desc, cancel, current, max, this);
589       progress_dialog->show();
590     } else {
591       // just update the dialog
592       progress_dialog->setValue(current);
593     }
594   }
595   qApp->processEvents();
596 }