add view alignment to the sequence browser context menu
[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("Define 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 mussa 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->addAction(createSubAnalysisAction);
242   newMenu->addSeparator();
243   newMenu->addAction(loadMotifListAction);
244   newMenu->addAction(saveMotifListAction);
245   newMenu->addSeparator();
246   newMenu->addAction(saveBrowserPixmapAction);
247   newMenu->addSeparator();
248   newMenu->addAction(closeAction);
249
250   newMenu = menuBar()->addMenu(tr("&Edit"));
251   newMenu->addAction(&browser.getCopySelectedSequenceAsFastaAction());
252  
253   newMenu = menuBar()->addMenu(tr("&View"));
254   newMenu->addAction(editMotifsAction);
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 }
270
271 void MussaWindow::setupAssistant()
272 {
273 #if 0 && defined(Q_WS_MAC)
274     CFURLRef pluginRef = CFBundleCopyBundleURL(CFBundleGetMainBundle());
275     CFStringRef macPath = CFURLCopyFileSystemPath(pluginRef,
276                                            kCFURLPOSIXPathStyle);
277     const char *pathPtr = CFStringGetCStringPtr(macPath,
278                                            CFStringGetSystemEncoding());
279     qDebug("Path = %s", pathPtr);
280     CFRelease(pluginRef);
281     CFRelease(macPath);
282 #endif
283 #if defined(QT_ASSISTANT_LIB)
284   QStringList manualAssistantArgs;
285   manualAssistantArgs = QStringList();
286   manualAssistantArgs << "-profile" << "./doc/manual/mussagl_manual.adp";
287   manualAssistant = new QAssistantClient("assistant", this);
288   manualAssistant->setArguments(manualAssistantArgs);
289   connect(manualAssistant, SIGNAL(error(QString)),
290           this, SLOT(assistantError(QString)));
291 #endif
292 }
293   
294 void MussaWindow::about()
295 {
296   QString msg("Welcome to Multiple Species Sequence Analysis\n"
297               "(c) 2005-2006 California Institute of Technology\n"
298               "Tristan De Buysscher, Diane Trout\n");
299    msg += "\n\r";
300    msg += "Path: ";
301    msg += QDir::currentPath();
302    msg += "\n";
303    msg += "OpenGL: ";
304    msg += (char *)glGetString(GL_VERSION);
305    msg += "\n";
306    QMessageBox::about(this, tr("About mussa"), msg);
307 }
308
309 void MussaWindow::clear()
310 {
311   aligned_windows.clear();
312   browser.clear();
313 }
314
315 void MussaWindow::createNewAnalysis()
316 {
317   try {
318     if (setup_analysis_dialog.exec()) {
319       Mussa *m = 0;
320       m = setup_analysis_dialog.getMussa();
321       setAnalysis(m);
322     } else {
323       std::cout << "New mussa exp. aborted!\n";
324     }
325   } catch(mussa_error e) {
326     QString msg(e.what());
327     QMessageBox::warning(this, tr("Create New Analysis"), msg);
328   }
329 }
330
331 void MussaWindow::createSubAnalysis()
332 {
333   NotImplementedBox();
334 }
335
336 void MussaWindow::editMotifs()
337 {
338   if (motif_editor != 0) {
339     motif_editor->hide();
340     delete motif_editor;
341   }
342   motif_editor = new MotifEditor(analysis);
343   connect(motif_editor, SIGNAL(changedMotifs()), 
344           this, SLOT(updateAnnotations()));
345   motif_editor->show();
346 }
347
348 void MussaWindow::loadMotifList()
349 {
350   QString caption("Load a motif list");
351   QString filter("Motif list(*.txt *.mtl)");
352   QString path = QFileDialog::getOpenFileName(this,
353                                                    caption, 
354                                                    QDir::currentPath(),
355                                                    filter);
356   // user hit cancel?
357   if (path.isNull()) 
358     return;
359   // try to load safely
360   try {
361     fs::path converted_path(path.toStdString(), fs::native);
362     analysis->load_motifs(converted_path);
363   } catch (runtime_error e) {
364     QString msg("Unable to load ");
365     msg += path;
366     msg += "\n";
367     msg += e.what();
368     QMessageBox::warning(this, "Load Motifs", msg);
369   }
370   assert (analysis != 0);
371 }
372
373 void MussaWindow::saveMotifList()
374 {
375   NotImplementedBox();
376 }
377
378 void MussaWindow::loadMupa()
379 {
380   QString caption("Load a mussa parameter file");
381   QString filter("Mussa Parameters (*.mupa)");
382   QString mupa_path = QFileDialog::getOpenFileName(this,
383                                                    caption, 
384                                                    QDir::currentPath(),
385                                                    filter);
386   // user hit cancel?
387   if (mupa_path.isNull()) 
388     return;
389   // try to load safely
390   try {
391     Mussa *m = new Mussa;
392     fs::path converted_path(mupa_path.toStdString(), fs::native);
393     connect(m, SIGNAL(progress(const std::string&, int, int)),
394             this, SLOT(updateProgress(const std::string&, int, int)));
395     m->load_mupa_file(converted_path);
396     m->analyze();
397     setAnalysis(m);
398     setWindowTitle(converted_path.native_file_string().c_str());
399   } catch (mussa_load_error e) {
400     QString msg("Unable to load ");
401     msg += mupa_path;
402     msg += "\n";
403     msg += e.what();
404     QMessageBox::warning(this, "Load Parameter", msg);
405   }
406   assert (analysis != 0);
407 }
408
409 void MussaWindow::loadSavedAnalysis()
410 {
411   QString caption("Load a previously run analysis");
412   QString muway_dir = QFileDialog::getExistingDirectory(this,
413                                                         caption, 
414                                                         QDir::currentPath());
415   // user hit cancel?
416   if (muway_dir.isNull()) 
417     return;
418   // try to safely load
419   try {
420     Mussa *m = new Mussa;
421     fs::path converted_path(muway_dir.toStdString(), fs::native);
422     connect(m, SIGNAL(progress(const std::string&, int, int)),
423             this, SLOT(updateProgress(const std::string&, int, int)));
424     m->load(converted_path);
425     // only switch mussas if we loaded without error
426     setAnalysis(m);
427     setWindowTitle(converted_path.native_file_string().c_str());
428   } catch (mussa_load_error e) {
429     QString msg("Unable to load ");
430     msg += muway_dir;
431     msg += "\n";
432     msg += e.what();
433     QMessageBox::warning(this, "Load Parameter", msg);
434   }
435   assert (analysis != 0);
436 }
437
438 void MussaWindow::newMussaWindow()
439 {
440   Mussa *a = new Mussa();
441   MussaWindow *win = new MussaWindow(a);
442   win->show();
443 }
444
445 void MussaWindow::setSoftThreshold(int threshold)
446 {
447   if (analysis->get_soft_threshold() != threshold) {
448     analysis->set_soft_threshold(threshold);
449     analysis->nway();
450     updateLinks();
451     update();
452   }
453 }
454
455 void MussaWindow::showMussaToolbar()
456 {
457   if (mussaViewTB.isVisible())
458     mussaViewTB.hide();
459   else
460     mussaViewTB.show();
461 }
462
463 void MussaWindow::toggleMotifs()
464 {
465   NotImplementedBox();
466 }
467
468 void MussaWindow::showManual()
469 {
470   manualAssistant->openAssistant();
471 }
472
473 void MussaWindow::assistantError(QString message)
474 {
475   //std::cout << "QAssistantError: " << message.toStdString() << "\n";
476   QMessageBox::warning ( this, "Warning: Mussagl Manual", message, 
477                          QMessageBox::Ok, 
478                          QMessageBox::NoButton);
479 }
480
481 void MussaWindow::NotImplementedBox()
482 {
483   QMessageBox::warning(this, QObject::tr("mussa"), QObject::tr("Not implemented yet"));
484 }      
485
486 void MussaWindow::viewMussaAlignment()
487 {
488   const set<int>& selected_paths = browser.selectedPaths();
489   if (selected_paths.size() == 0 ) {
490     QMessageBox::warning(this, 
491                          QObject::tr("mussa"),
492                          QObject::tr("you should probably select some paths "
493                                      "first"));
494   } else {
495     boost::shared_ptr<MussaAlignedWindow> ma_win( 
496       new MussaAlignedWindow(*analysis, selected_paths)
497     );
498
499     aligned_windows.push_back(ma_win);
500     connect(this, SIGNAL(changedAnnotations()), 
501             aligned_windows.back().get(), SLOT(update()));
502     aligned_windows.back()->show();
503   }
504 }
505                         
506 void MussaWindow::updateAnalysis()
507 {
508   threshold.setRange(analysis->get_threshold(),analysis->get_window());
509
510   const vector<Sequence>& seqs = analysis->sequences();
511   browser.setSequences(seqs, analysis->colorMapper());
512   updateLinks();
513   browser.zoomOut();
514   zoom.setValue(browser.zoom());
515 }
516
517 void MussaWindow::updateAnnotations()
518 {
519   // motifs were changed in the sequences by 
520   // Mussa::update_sequences_motifs
521   emit changedAnnotations();
522   browser.update();
523 }
524
525 void MussaWindow::updateLinks()
526 {
527   browser.clear_links();
528   bool reversed = false;
529   const NwayPaths& nway = analysis->paths();
530
531   typedef list<ConservedPath> conserved_paths;
532   typedef conserved_paths::const_iterator const_conserved_paths_itor;
533   for(const_conserved_paths_itor path_itor = nway.refined_pathz.begin();
534       path_itor != nway.refined_pathz.end();
535       ++path_itor)
536   {
537     // since we were drawing to the start of a window, and opengl lines
538     // are centered around the two connecting points our lines were slightly
539     // offset. the idea of window_offset is to adjust them to the
540     // right for forward compliment or left for reverse compliment
541     // FIXME: figure out how to unit test these computations
542     //GLfloat window_offset = (path_itor->window_size)/2.0;
543
544     size_t track_len = path_itor->track_indexes.size();
545     vector<int> normalized_path;
546     normalized_path.reserve(track_len);
547     vector<bool> rc_flags(false, track_len);
548     for (size_t track_i=0; track_i != track_len; ++track_i)
549     {
550       int x = path_itor->track_indexes[track_i];
551       // at some point when we modify the pathz data structure to keep
552       // track of the score we can put grab the depth here.
553       //
554       // are we reverse complimented?
555       if ( x>=0) {
556         reversed = false;
557       } else {
558         reversed = true;
559         // make positive 
560         x = -x;
561       }
562       normalized_path.push_back(x);
563       rc_flags.push_back(reversed);
564     }
565     browser.link(normalized_path, rc_flags, path_itor->window_size);
566   }
567   browser.update();
568 }
569
570 void 
571 MussaWindow::updateProgress(const string& description, int current, int max)
572 {  
573   // if we're done  
574   if (current == max) {
575     if (progress_dialog != 0) {
576       progress_dialog->hide();
577       delete progress_dialog;
578       progress_dialog = 0;
579     }
580   } else {
581     // if we're starting, create the dialog
582     if (progress_dialog == 0) {
583       QString desc(description.c_str());
584       QString cancel("Cancel");
585       progress_dialog = new QProgressDialog(desc, cancel, current, max, this);
586       progress_dialog->show();
587     } else {
588       // just update the dialog
589       progress_dialog->setValue(current);
590     }
591   }
592   qApp->processEvents();
593 }