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