always save
[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 <QCloseEvent>
9 #include <QDir>
10 #include <QFileDialog>
11 #include <QHBoxLayout>
12 #include <QIcon>
13 #include <QMenuBar>
14 #include <QMessageBox>
15 #include <QScrollBar>
16 #include <QStatusBar>
17 #include <QString>
18 #include <QStringList>
19 #include <QWhatsThis>
20
21 #include <memory>
22 #include <iterator>
23 #include <iostream>
24
25 #include <boost/filesystem/path.hpp>
26 #include <boost/filesystem/operations.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 void MussaWindow::setAnalysis(Mussa *new_analysis)
98 {
99   if (new_analysis != 0) {
100     // only switch mussas if we loaded without error
101     clear();
102     delete analysis;
103     analysis = new_analysis;
104     setWindowTitle(analysis->get_name().c_str());
105     updateAnalysis();
106   }
107 }
108
109 void MussaWindow::setupActions()
110 {
111   // we really don't want to run this more than once.
112   assert (closeAction == 0);
113
114   // the ever popular about box
115   aboutAction = new QAction(tr("&About"), this);
116   connect(aboutAction, SIGNAL(triggered()), this, SLOT(about()));
117   aboutAction->setIcon(QIcon(":/icons/info.png"));
118
119   // add exit
120   closeAction = new QAction(tr("&Close"), this);
121   closeAction->setStatusTip(tr("Close this window"));
122   connect(closeAction, SIGNAL(triggered()), this, SLOT(close()));
123   closeAction->setIcon(QIcon(":/icons/exit.png"));
124   
125   createNewAnalysisAction = new QAction(tr("Create Analysis"), this);
126   connect(createNewAnalysisAction, SIGNAL(triggered()), 
127           this, SLOT(createNewAnalysis()));
128   createNewAnalysisAction->setIcon(QIcon(":/icons/filenew.png"));
129   
130   createSubAnalysisAction = new QAction(tr("Add to Subanalysis"), this);
131   connect(createSubAnalysisAction, SIGNAL(triggered()), 
132           this, SLOT(createSubAnalysis()));
133
134   saveAnalysisAction = new QAction(tr("&Save Analysis"), this);
135   connect(saveAnalysisAction, SIGNAL(triggered()), 
136           this, SLOT(saveAnalysis()));
137   saveAnalysisAction->setIcon(QIcon(":/icons/filesave.png"));
138
139   saveAnalysisAsAction = new QAction(tr("Save Analysis &As"), this);
140   connect(saveAnalysisAsAction, SIGNAL(triggered()), 
141           this, SLOT(saveAnalysisAs()));
142   saveAnalysisAsAction->setIcon(QIcon(":/icons/filesave.png"));
143
144   editMotifsAction = new QAction(tr("Edit Motifs"), this);;
145   connect(editMotifsAction, SIGNAL(triggered()), this, SLOT(editMotifs()));
146   
147   loadMotifListAction = new QAction(tr("Load Motif List"), this);
148   connect(loadMotifListAction, SIGNAL(triggered()), 
149           this, SLOT(loadMotifList()));
150   loadMotifListAction->setIcon(QIcon(":/icons/fileopen.png"));
151   
152   loadMupaAction = new QAction(tr("Create Analysis from File"), this);
153   connect(loadMupaAction, SIGNAL(triggered()), 
154           this, SLOT(loadMupa()));
155   loadMupaAction->setIcon(QIcon(":/icons/fileopen.png"));
156
157   loadSavedAnalysisAction = new QAction(tr("Load Existing &Analysis"), this);
158   connect(loadSavedAnalysisAction, SIGNAL(triggered()), 
159           this, SLOT(loadSavedAnalysis()));
160   loadSavedAnalysisAction->setIcon(QIcon(":/icons/fileopen.png"));
161
162   mussaManualAssistantAction = new QAction(tr("Mussagl Manual..."), this);
163   mussaManualAssistantAction->setIcon(QIcon(":/icons/contents.png"));
164   connect(mussaManualAssistantAction, SIGNAL(triggered()),
165           this, SLOT(showManual()));
166
167   newMussaWindowAction = new QAction(tr("&New Mussa Window"), this);
168   newMussaWindowAction->setStatusTip("open another mussa window to allow comparing results");
169   connect(newMussaWindowAction, SIGNAL(triggered()), 
170           this, SLOT(newMussaWindow()));
171   newMussaWindowAction->setIcon(QIcon(":/icons/window_new.png"));
172
173   saveMotifListAction = new QAction(tr("Save Motifs"), this);
174   connect(saveMotifListAction, SIGNAL(triggered()), 
175           this, SLOT(saveMotifList()));
176   saveMotifListAction->setIcon(QIcon(":/icons/filesave.png"));
177
178   showMussaViewToolbarAction = new QAction(tr("Show Toolbar"), this);
179   connect(showMussaViewToolbarAction, SIGNAL(triggered()), 
180           this, SLOT(showMussaToolbar()));
181   showMussaViewToolbarAction->setCheckable(true);
182   showMussaViewToolbarAction->setChecked(true);
183
184   toggleMotifsAction = new QAction(tr("Toggle Motifs"), this);
185   connect(toggleMotifsAction, SIGNAL(triggered()), 
186           this, SLOT(toggleMotifs()));
187   toggleMotifsAction->setCheckable(true);
188   toggleMotifsAction->setIcon(QIcon(":/icons/motif_icon.png"));
189   toggleMotifsAction->setWhatsThis(tr("Toggle motif annotations on/off\n\n"
190                                    "You can load motif annotations via "
191                                    "'File->Load Motif List' menu option."));
192
193   //Save pixel map action
194   saveBrowserPixmapAction = new QAction(tr("Save to image..."), this);
195   connect(saveBrowserPixmapAction, (SIGNAL(triggered())),
196           &browser, SLOT(promptSaveBrowserPixmap()));
197   saveBrowserPixmapAction->setIcon(QIcon(":/icons/image2.png"));
198
199   viewMussaAlignmentAction = new QAction(tr("View sequence alignment"), this);
200   connect(viewMussaAlignmentAction, SIGNAL(triggered()),
201           this, SLOT(viewMussaAlignment() ));
202   viewMussaAlignmentAction->setWhatsThis(tr("Create a zoomed in window "
203                                             "showing alignment of the seqcomp "
204                                             "defined paths"));
205
206   whatsThisAction = QWhatsThis::createAction(this);
207   whatsThisAction->setIcon(QIcon(":/icons/help.png"));
208
209
210 }
211
212 void MussaWindow::closeEvent(QCloseEvent *event)
213 {
214   if(isClearingAnalysisSafe()) {
215     event->accept();
216   } else {
217     event->ignore();
218   }
219 }
220
221 void MussaWindow::setupMainMenu()
222 {
223   // we need to run setupActions first
224   assert (closeAction != 0);
225
226   QMenu *newMenu = menuBar()->addMenu(tr("&File"));
227   
228   newMenu->addAction(newMussaWindowAction);
229   newMenu->addAction(createNewAnalysisAction);
230   newMenu->addAction(loadMupaAction);
231   newMenu->addAction(loadSavedAnalysisAction);
232   newMenu->addAction(saveAnalysisAction);
233   newMenu->addAction(saveAnalysisAsAction);
234   newMenu->addSeparator();
235   newMenu->addAction(loadMotifListAction);
236   newMenu->addAction(saveMotifListAction);
237   newMenu->addSeparator();
238   newMenu->addAction(saveBrowserPixmapAction);
239   newMenu->addSeparator();
240   newMenu->addAction(closeAction);
241
242   newMenu = menuBar()->addMenu(tr("&Edit"));
243   newMenu->addAction(editMotifsAction);
244   newMenu->addAction(&browser.getCopySelectedSequenceAsFastaAction());
245   newMenu->addAction(createSubAnalysisAction);
246  
247   newMenu = menuBar()->addMenu(tr("&View"));
248   newMenu->addAction(viewMussaAlignmentAction);
249   newMenu->addAction(showMussaViewToolbarAction);
250
251   newMenu = menuBar()->addMenu(tr("&Help"));
252   newMenu->addAction(mussaManualAssistantAction);
253   newMenu->addAction(whatsThisAction);
254   newMenu->addSeparator();
255   newMenu->addAction(aboutAction);
256
257   // add some extra features to the context menu
258   QMenu& popupMenu = browser.getPopupMenu();
259   popupMenu.addAction(viewMussaAlignmentAction);
260   popupMenu.addAction(createSubAnalysisAction);
261 }
262
263 void MussaWindow::setupAssistant()
264 {
265 #if defined(QT_ASSISTANT_FOUND)
266   QStringList manualAssistantArgs;
267   manualAssistantArgs = QStringList();
268   manualAssistantArgs << "-profile" << "./doc/manual/mussagl_manual.adp";
269   manualAssistant = new QAssistantClient("assistant", this);
270   manualAssistant->setArguments(manualAssistantArgs);
271   connect(manualAssistant, SIGNAL(error(QString)),
272           this, SLOT(assistantError(QString)));
273 #endif
274 }
275   
276 void MussaWindow::about()
277 {
278   QString msg("Welcome to Multiple Species Sequence Analysis\n"
279               "(c) 2005-2006 California Institute of Technology\n"
280               "Tristan De Buysscher, Diane Trout\n");
281    msg += "\n\r";
282    msg += "Path: ";
283    msg += QDir::currentPath();
284    msg += "\n";
285    msg += "OpenGL: ";
286    msg += (char *)glGetString(GL_VERSION);
287    msg += "\n";
288    QMessageBox::about(this, tr("About mussa"), msg);
289 }
290
291 void MussaWindow::clear()
292 {
293   aligned_windows.clear();
294   browser.clear();
295 }
296
297 void MussaWindow::createNewAnalysis()
298 {
299   try {
300     // ideally we should open a new window if there's an analysis
301     // but this should work for the moment.
302     if (not isClearingAnalysisSafe()) return;
303
304     if (setup_analysis_dialog.exec()) {
305       Mussa *m = 0;
306       m = setup_analysis_dialog.getMussa();
307       setAnalysis(m);
308     }
309   } catch(mussa_error e) {
310     QString msg(e.what());
311     QMessageBox::warning(this, tr("Create New Analysis"), msg);
312   }
313 }
314
315 void MussaWindow::createSubAnalysis()
316 {
317   list<SequenceLocation> result;
318   SequenceLocationModel& model = subanalysis_window.getModel();
319   browser.copySelectedTracksAsSeqLocation(result);
320   for(list<SequenceLocation>::iterator result_itor = result.begin();
321       result_itor != result.end();
322       ++result_itor)
323   {
324     model.push_back(*result_itor);
325   }
326
327   if (not subanalysis_window.isVisible()) {
328     subanalysis_window.show();
329   }
330 }
331
332 void MussaWindow::saveAnalysis()
333 {
334   // if we've got an analysis
335   if (analysis and not analysis->empty()) {
336     // if it doesn't have a name we need to pick one
337     if (analysis->get_analysis_path().empty()) {
338       saveAnalysisAs();
339     }      
340     // if we've got a name, has it changed any?
341     // this doesn't work when a sequence changes something (like its
342     // name)
343     //else if (analysis->is_dirty())
344     analysis->save();
345   }
346 }
347
348 void MussaWindow::saveAnalysisAs()
349 {
350   std::auto_ptr<QFileDialog> dialog(new QFileDialog(this));
351   dialog->setAcceptMode(QFileDialog::AcceptSave);
352   dialog->setFileMode(QFileDialog::AnyFile);
353   dialog->setDirectory(QDir::home());
354
355   QStringList fileNames;
356   if (not dialog->exec()) {
357     return;
358   }
359   fileNames = dialog->selectedFiles();
360    
361   if (fileNames.size() != 1) {
362     return;
363   }
364
365   fs::path save_path(fileNames[0].toStdString());
366   // do you want to overwrite?
367   if (fs::exists(save_path) and 
368       QMessageBox::question(
369         this,
370         tr("Overwrite File? -- Mussa"),
371         tr("A file called %1 already exists"
372            "do you want to overwrite it?")
373            .arg(fileNames[0]),
374         tr("&Yes"), tr("&No"),
375         QString(), 0, 1)
376       ) {
377     return;
378   }
379   analysis->save(save_path);
380 }
381
382 bool MussaWindow::isClearingAnalysisSafe()
383 {
384   if (analysis and not analysis->empty() and analysis->is_dirty()) {
385     switch (QMessageBox::question(
386       this,
387       tr("Save Unsaved Changes -- Mussa"),
388       tr("There are unsaved changes,\ndo you want to save?"),
389       tr("&Yes"), tr("&No"), tr("&Cancel"),
390       0, 2)) {
391     case 0:
392       // save
393       saveAnalysis();
394       break;
395     case 1:
396       // don't save
397       break;
398     case 2:
399       // don't replace 
400       return false;
401       break;
402     default:
403       // error
404       throw runtime_error("isClearingAnalysis QMesageBox failure");
405     }
406   } 
407   // if we're here we've been saved and can replace
408   return true;
409 }
410
411 void MussaWindow::editMotifs()
412 {
413   if (motif_editor != 0) {
414     motif_editor->hide();
415     delete motif_editor;
416   }
417   motif_editor = new MotifEditor(analysis);
418   connect(motif_editor, SIGNAL(changedMotifs()), 
419           this, SLOT(updateAnnotations()));
420   motif_editor->show();
421 }
422
423 void MussaWindow::loadMotifList()
424 {
425   QString caption("Load a motif list");
426   QString filter("Motif list(*.txt *.mtl)");
427   QString path = QFileDialog::getOpenFileName(this,
428                                                    caption, 
429                                                    QDir::currentPath(),
430                                                    filter);
431   // user hit cancel?
432   if (path.isNull()) 
433     return;
434   // try to load safely
435   try {
436     fs::path converted_path(path.toStdString(), fs::native);
437     analysis->load_motifs(converted_path);
438   } catch (runtime_error e) {
439     QString msg("Unable to load ");
440     msg += path;
441     msg += "\n";
442     msg += e.what();
443     QMessageBox::warning(this, "Load Motifs", msg);
444   }
445   assert (analysis != 0);
446 }
447
448 void MussaWindow::saveMotifList()
449 {
450   NotImplementedBox();
451 }
452
453 void MussaWindow::loadMupa()
454 {
455   QString caption("Load a mussa parameter file");
456   QString filter("Mussa Parameters (*.mupa)");
457   QString mupa_path = QFileDialog::getOpenFileName(this,
458                                                    caption, 
459                                                    QDir::currentPath(),
460                                                    filter);
461   // user hit cancel?
462   if (mupa_path.isNull()) 
463     return;
464   // try to load safely
465   try {
466     // ideally we should open a new window if there's an analysis
467     // but this should work for the moment.
468     if (not isClearingAnalysisSafe()) return;
469
470     Mussa *m = new Mussa;
471     fs::path converted_path(mupa_path.toStdString(), fs::native);
472     connect(m, SIGNAL(progress(const std::string&, int, int)),
473             this, SLOT(updateProgress(const std::string&, int, int)));
474     m->load_mupa_file(converted_path);
475     m->analyze();
476     setAnalysis(m);
477     setWindowTitle(converted_path.native_file_string().c_str());
478   } catch (mussa_load_error e) {
479     QString msg("Unable to load ");
480     msg += mupa_path;
481     msg += "\n";
482     msg += e.what();
483     QMessageBox::warning(this, "Load Parameter", msg);
484   }
485   assert (analysis != 0);
486 }
487
488 void MussaWindow::loadSavedAnalysis()
489 {
490   QString caption("Load a previously run analysis");
491   QString muway_dir = QFileDialog::getExistingDirectory(this,
492                                                         caption, 
493                                                         QDir::currentPath());
494   // user hit cancel?
495   if (muway_dir.isNull()) 
496     return;
497   // try to safely load
498   try {
499     // ideally we should open a new window if there's an analysis
500     // but this should work for the moment.
501     if (not isClearingAnalysisSafe()) return;
502
503     Mussa *m = new Mussa;
504     fs::path converted_path(muway_dir.toStdString(), fs::native);
505     connect(m, SIGNAL(progress(const std::string&, int, int)),
506             this, SLOT(updateProgress(const std::string&, int, int)));
507     m->load(converted_path);
508     // only switch mussas if we loaded without error
509     setAnalysis(m);
510     setWindowTitle(converted_path.native_file_string().c_str());
511   } catch (mussa_load_error e) {
512     QString msg("Unable to load ");
513     msg += muway_dir;
514     msg += "\n";
515     msg += e.what();
516     QMessageBox::warning(this, "Load Parameter", msg);
517   }
518   assert (analysis != 0);
519 }
520
521 void MussaWindow::newMussaWindow()
522 {
523   Mussa *a = new Mussa();
524   MussaWindow *win = new MussaWindow(a);
525   win->show();
526 }
527
528 void MussaWindow::setSoftThreshold(int threshold)
529 {
530   if (analysis->get_soft_threshold() != threshold) {
531     analysis->set_soft_threshold(threshold);
532     analysis->nway();
533     updateLinks();
534     update();
535   }
536 }
537
538 void MussaWindow::showMussaToolbar()
539 {
540   if (mussaViewTB.isVisible())
541     mussaViewTB.hide();
542   else
543     mussaViewTB.show();
544 }
545
546 void MussaWindow::toggleMotifs()
547 {
548   NotImplementedBox();
549 }
550
551 void MussaWindow::showManual()
552 {
553 #if QT_QTASSISTANT_FOUND
554   manualAssistant->openAssistant();
555 #else
556   try {
557     boost::python::object webopen = get_py()["webbrowser.open"];
558     webopen("http://woldlab.caltech.edu/~king/mussagl_manual/");
559   } catch( boost::python::error_already_set ) {
560     PyErr_Print();
561   }
562 #endif //QT_QTASSISTANT_FOUND
563 }
564
565 void MussaWindow::assistantError(QString message)
566 {
567   //std::cout << "QAssistantError: " << message.toStdString() << "\n";
568   QMessageBox::warning ( this, "Warning: Mussagl Manual", message, 
569                          QMessageBox::Ok, 
570                          QMessageBox::NoButton);
571 }
572
573 void MussaWindow::NotImplementedBox()
574 {
575   QMessageBox::warning(this, QObject::tr("mussa"), QObject::tr("Not implemented yet"));
576 }      
577
578 void MussaWindow::viewMussaAlignment()
579 {
580   const set<int>& selected_paths = browser.selectedPaths();
581   if (selected_paths.size() == 0 ) {
582     QMessageBox::warning(this, 
583                          QObject::tr("mussa"),
584                          QObject::tr("you should probably select some paths "
585                                      "first"));
586   } else {
587     boost::shared_ptr<MussaAlignedWindow> ma_win( 
588       new MussaAlignedWindow(*analysis, selected_paths)
589     );
590
591     aligned_windows.push_back(ma_win);
592     connect(this, SIGNAL(changedAnnotations()), 
593             aligned_windows.back().get(), SLOT(update()));
594     aligned_windows.back()->show();
595   }
596 }
597                         
598 void MussaWindow::updateAnalysis()
599 {
600   threshold.setRange(analysis->get_threshold(),analysis->get_window());
601
602   const Mussa::vector_sequence_type& seqs = analysis->sequences();
603   browser.setSequences(seqs, analysis->colorMapper());
604   updateLinks();
605   browser.zoomOut();
606   zoom.setValue(browser.zoom());
607 }
608
609 void MussaWindow::updateAnnotations()
610 {
611   // motifs were changed in the sequences by 
612   // Mussa::update_sequences_motifs
613   emit changedAnnotations();
614   browser.update();
615 }
616
617 void MussaWindow::updateLinks()
618 {
619   browser.clear_links();
620   bool reversed = false;
621   const NwayPaths& nway = analysis->paths();
622
623   typedef list<ConservedPath> conserved_paths;
624   typedef conserved_paths::const_iterator const_conserved_paths_itor;
625   for(const_conserved_paths_itor path_itor = nway.refined_pathz.begin();
626       path_itor != nway.refined_pathz.end();
627       ++path_itor)
628   {
629     // since we were drawing to the start of a window, and opengl lines
630     // are centered around the two connecting points our lines were slightly
631     // offset. the idea of window_offset is to adjust them to the
632     // right for forward compliment or left for reverse compliment
633     // FIXME: figure out how to unit test these computations
634     //GLfloat window_offset = (path_itor->window_size)/2.0;
635
636     size_t track_len = path_itor->track_indexes.size();
637     vector<int> normalized_path;
638     normalized_path.reserve(track_len);
639     vector<bool> rc_flags(false, track_len);
640     for (size_t track_i=0; track_i != track_len; ++track_i)
641     {
642       int x = path_itor->track_indexes[track_i];
643       // at some point when we modify the pathz data structure to keep
644       // track of the score we can put grab the depth here.
645       //
646       // are we reverse complimented?
647       if ( x>=0) {
648         reversed = false;
649       } else {
650         reversed = true;
651         // make positive 
652         x = -x;
653       }
654       normalized_path.push_back(x);
655       rc_flags.push_back(reversed);
656     }
657     browser.link(normalized_path, rc_flags, path_itor->window_size);
658   }
659   browser.update();
660 }
661
662 void 
663 MussaWindow::updateProgress(const string& description, int current, int max)
664 {  
665   // if we're done  
666   if (current == max) {
667     if (progress_dialog != 0) {
668       progress_dialog->hide();
669       delete progress_dialog;
670       progress_dialog = 0;
671     }
672   } else {
673     // if we're starting, create the dialog
674     if (progress_dialog == 0) {
675       QString desc(description.c_str());
676       QString cancel("Cancel");
677       progress_dialog = new QProgressDialog(desc, cancel, current, max, this);
678       progress_dialog->show();
679     } else {
680       // just update the dialog
681       progress_dialog->setValue(current);
682     }
683   }
684   qApp->processEvents();
685 }