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