1 #include "qui/MussaWindow.hpp"
2 #include "mussa_exceptions.hpp"
6 #include <QApplication>
7 #include <QAssistantClient>
9 #include <QDesktopServices>
11 #include <QFileDialog>
12 #include <QHBoxLayout>
15 #include <QMessageBox>
19 #include <QStringList>
27 #include <boost/filesystem/path.hpp>
28 #include <boost/filesystem/operations.hpp>
29 #include <boost/filesystem/convenience.hpp>
30 namespace fs = boost::filesystem;
31 #include <boost/bind.hpp>
35 static void init_resources() {
36 static bool resources_loaded = false;
37 if (not resources_loaded) {
38 Q_INIT_RESOURCE(icons);
39 resources_loaded = true;
43 MussaWindow::MussaWindow(MussaRef analysis_, QWidget *parent) :
47 setup_analysis_dialog(0),
55 createNewAnalysisAction(0),
56 createSubAnalysisAction(0),
58 loadMotifListAction(0),
60 loadSavedAnalysisAction(0),
61 mussaManualAssistantAction(0),
62 newMussaWindowAction(0),
63 saveMotifListAction(0),
64 showMussaViewToolbarAction(0),
65 toggleMotifsAction(0),
66 saveBrowserPixmapAction(0),
68 viewMussaAlignmentAction(0),
73 default_dir.reset(new QDir(QDir::home().absolutePath()));
80 setWindowIcon(QIcon(":/icons/mussa.png"));
82 setCentralWidget(browser);
83 // well updatePosition isn't quite right as we really just need
85 connect(this, SIGNAL(changedAnnotations()), browser, SLOT(update()));
86 connect(this, SIGNAL(changedMotifs()), this, SLOT(updateAnnotations()));
87 connect(browser, SIGNAL(basepairsCopied(size_t)),
88 this, SLOT(showBasePairsCopied(size_t)));
89 connect(zoom, SIGNAL(valueChanged(double)),
90 browser, SLOT(setZoom(double)));
91 mussaViewTB->addWidget(zoom);
93 // Mouse Wheel triggered zooming
94 connect(browser, SIGNAL(mouseWheelZoom(double)),
95 zoom, SLOT(setValue(double)));
97 // threshold range is set in updateAnalysis
98 connect(threshold, SIGNAL(thresholdChanged(int)),
99 this, SLOT(setSoftThreshold(int)));
100 mussaViewTB->addWidget(threshold);
102 addToolBar(mussaViewTB);
104 statusBar()->showMessage("Welcome to mussa", 2000);
106 // FIXME: we should start refactoring the connect call to updateAnalysis or something
108 connect(analysis.get(), SIGNAL(progress(const QString&, int, int)),
109 this, SLOT(updateProgress(const QString&, int, int)));
110 connect(analysis.get(), SIGNAL(isModified(bool)),
111 this, SLOT(updateAnalysisModified(bool)));
117 void MussaWindow::setAnalysis(MussaRef new_analysis)
119 if (new_analysis != 0) {
120 // only switch mussas if we loaded without error
122 //std::cout << "analysis soft: " << analysis->get_soft_threshold()
123 // << " new analysis soft: " << new_analysis->get_soft_threshold()
125 analysis.swap(new_analysis);
126 //std::cout << "after swap soft thres: " << analysis->get_soft_threshold()
128 threshold->disconnect(this);
129 threshold->reset(analysis->get_threshold(),
130 analysis->get_window(),
131 analysis->get_soft_threshold());
132 connect(threshold, SIGNAL(thresholdChanged(int)),
133 this, SLOT(setSoftThreshold(int)));
139 void MussaWindow::setupActions()
141 // we really don't want to run this more than once.
142 assert (closeAction == 0);
144 // the ever popular about box
145 aboutAction = new QAction(tr("&About"), this);
146 connect(aboutAction, SIGNAL(triggered()), this, SLOT(about()));
147 aboutAction->setIcon(QIcon(":/icons/info.png"));
150 closeAction = new QAction(tr("&Close"), this);
151 closeAction->setStatusTip(tr("Close this window"));
152 connect(closeAction, SIGNAL(triggered()), this, SLOT(close()));
153 closeAction->setIcon(QIcon(":/icons/exit.png"));
155 createNewAnalysisAction = new QAction(tr("Create &New Analysis"), this);
156 connect(createNewAnalysisAction, SIGNAL(triggered()),
157 this, SLOT(createNewAnalysis()));
158 createNewAnalysisAction->setIcon(QIcon(":/icons/filenew.png"));
159 createNewAnalysisAction->setShortcut(Qt::CTRL | Qt::Key_N);
161 createSubAnalysisAction = new QAction(tr("Add to Subanalysis"), this);
162 connect(createSubAnalysisAction, SIGNAL(triggered()),
163 this, SLOT(createSubAnalysis()));
165 saveAnalysisAction = new QAction(tr("&Save Analysis"), this);
166 connect(saveAnalysisAction, SIGNAL(triggered()),
167 this, SLOT(saveAnalysis()));
168 saveAnalysisAction->setIcon(QIcon(":/icons/filesave.png"));
169 saveAnalysisAction->setShortcut(Qt::CTRL | Qt::Key_S);
171 saveAnalysisAsAction = new QAction(tr("Save Analysis &As"), this);
172 connect(saveAnalysisAsAction, SIGNAL(triggered()),
173 this, SLOT(saveAnalysisAs()));
174 saveAnalysisAsAction->setIcon(QIcon(":/icons/filesave.png"));
176 editMotifsAction = new QAction(tr("Edit Motifs"), this);;
177 connect(editMotifsAction, SIGNAL(triggered()), this, SLOT(editMotifs()));
179 loadMotifListAction = new QAction(tr("Open Motif List"), this);
180 connect(loadMotifListAction, SIGNAL(triggered()),
181 this, SLOT(loadMotifList()));
182 loadMotifListAction->setIcon(QIcon(":/icons/fileopen.png"));
184 loadMupaAction = new QAction(tr("Create Analysis from File"), this);
185 connect(loadMupaAction, SIGNAL(triggered()),
186 this, SLOT(loadMupa()));
187 loadMupaAction->setIcon(QIcon(":/icons/fileopen.png"));
189 loadSavedAnalysisAction = new QAction(tr("&Open Existing &Analysis"), this);
190 connect(loadSavedAnalysisAction, SIGNAL(triggered()),
191 this, SLOT(loadSavedAnalysis()));
192 loadSavedAnalysisAction->setIcon(QIcon(":/icons/fileopen.png"));
193 loadSavedAnalysisAction->setShortcut(Qt::CTRL | Qt::Key_O);
195 mussaManualAssistantAction = new QAction(tr("Mussagl Manual..."), this);
196 mussaManualAssistantAction->setIcon(QIcon(":/icons/contents.png"));
197 connect(mussaManualAssistantAction, SIGNAL(triggered()),
198 this, SLOT(showManual()));
200 newMussaWindowAction = new QAction(tr("&New Mussa Window"), this);
201 newMussaWindowAction->setStatusTip("open another mussa window to allow comparing results");
202 connect(newMussaWindowAction, SIGNAL(triggered()),
203 this, SLOT(newMussaWindow()));
204 newMussaWindowAction->setIcon(QIcon(":/icons/window_new.png"));
206 saveMotifListAction = new QAction(tr("Save Motifs"), this);
207 connect(saveMotifListAction, SIGNAL(triggered()),
208 this, SLOT(saveMotifList()));
209 saveMotifListAction->setIcon(QIcon(":/icons/filesave.png"));
211 showMussaViewToolbarAction = new QAction(tr("Show Toolbar"), this);
212 connect(showMussaViewToolbarAction, SIGNAL(triggered()),
213 this, SLOT(showMussaToolbar()));
214 showMussaViewToolbarAction->setCheckable(true);
215 showMussaViewToolbarAction->setChecked(true);
217 toggleMotifsAction = new QAction(tr("Toggle Motifs"), this);
218 connect(toggleMotifsAction, SIGNAL(triggered()),
219 this, SLOT(toggleMotifs()));
220 toggleMotifsAction->setCheckable(true);
221 toggleMotifsAction->setIcon(QIcon(":/icons/motif_icon.png"));
222 toggleMotifsAction->setWhatsThis(tr("Toggle motif annotations on/off\n\n"
223 "You can load motif annotations via "
224 "'File->Load Motif List' menu option."));
226 //Save pixel map action
227 saveBrowserPixmapAction = new QAction(tr("Save to image..."), this);
229 connect(saveBrowserPixmapAction, (SIGNAL(triggered())),
230 browser, SLOT(promptSaveBrowserPixmap()));
231 saveBrowserPixmapAction->setIcon(QIcon(":/icons/image2.png"));
234 viewMussaAlignmentAction = new QAction(tr("View sequence alignment"), this);
235 connect(viewMussaAlignmentAction, SIGNAL(triggered()),
236 this, SLOT(viewMussaAlignment() ));
237 viewMussaAlignmentAction->setWhatsThis(tr("Create a zoomed in window "
238 "showing alignment of the seqcomp "
241 whatsThisAction = QWhatsThis::createAction(this);
242 whatsThisAction->setIcon(QIcon(":/icons/help.png"));
247 void MussaWindow::closeEvent(QCloseEvent *event)
249 if(isClearingAnalysisSafe()) {
256 void MussaWindow::setupMainMenu()
258 // we need to run setupActions first
259 assert (closeAction != 0);
261 QMenu *newMenu = menuBar()->addMenu(tr("&File"));
263 newMenu->addAction(newMussaWindowAction);
264 newMenu->addAction(createNewAnalysisAction);
265 newMenu->addAction(loadMupaAction);
266 newMenu->addAction(loadSavedAnalysisAction);
267 newMenu->addAction(saveAnalysisAction);
268 newMenu->addAction(saveAnalysisAsAction);
269 newMenu->addSeparator();
270 newMenu->addAction(loadMotifListAction);
271 newMenu->addAction(saveMotifListAction);
272 newMenu->addSeparator();
273 newMenu->addAction(saveBrowserPixmapAction);
274 newMenu->addSeparator();
275 newMenu->addAction(closeAction);
277 newMenu = menuBar()->addMenu(tr("&Edit"));
278 newMenu->addAction(editMotifsAction);
279 if (browser) newMenu->addAction(browser->getCopySelectedSequenceAsStringAction());
280 if (browser) newMenu->addAction(browser->getCopySelectedSequenceAsFastaAction());
281 newMenu->addAction(createSubAnalysisAction);
282 if (browser) newMenu->addAction(browser->getEditSequencePropertiesAction());
284 newMenu = menuBar()->addMenu(tr("&View"));
285 newMenu->addAction(viewMussaAlignmentAction);
286 newMenu->addAction(showMussaViewToolbarAction);
288 newMenu = menuBar()->addMenu(tr("&Help"));
289 newMenu->addAction(mussaManualAssistantAction);
290 newMenu->addAction(whatsThisAction);
291 newMenu->addSeparator();
292 newMenu->addAction(aboutAction);
294 // add some extra features to the context menu
296 QMenu *popupMenu = browser->getPopupMenu();
298 popupMenu->addAction(viewMussaAlignmentAction);
299 popupMenu->addAction(createSubAnalysisAction);
304 void MussaWindow::setupWidgets()
306 setup_analysis_dialog = new MussaSetupDialog;
307 subanalysis_window.reset(new SubanalysisWindow(analysis));
308 browser = new SequenceBrowserWidget(default_dir);
309 mussaViewTB = new QToolBar("Path Views", this);
310 zoom = new ZoomWidget(mussaViewTB);
311 threshold = new ThresholdWidget(mussaViewTB);
314 void MussaWindow::setupAssistant()
316 #if defined(QT_QTASSISTANT_FOUND)
317 QStringList manualAssistantArgs;
318 manualAssistantArgs = QStringList();
319 manualAssistantArgs << "-profile" << "./doc/manual/mussagl_manual.adp";
320 manualAssistant = new QAssistantClient("assistant", this);
321 manualAssistant->setArguments(manualAssistantArgs);
322 connect(manualAssistant, SIGNAL(error(QString)),
323 this, SLOT(assistantError(QString)));
327 void MussaWindow::about()
330 msg += "Welcome to Multiple Species Sequence Analysis\n";
331 msg += "(c) 2005-2006 California Institute of Technology\n";
332 msg += "Diane Trout, Tristan De Buysscher, Brandon King\n";
334 msg += mussa_version;
340 msg += (char *)glGetString(GL_VERSION);
342 QMessageBox::about(this, tr("About mussa"), msg);
345 void MussaWindow::clear()
347 if (motif_editor != 0) {
348 motif_editor->hide();
352 aligned_windows.clear();
356 void MussaWindow::createNewAnalysis()
359 // ideally we should open a new window if there's an analysis
360 // but this should work for the moment.
361 if (not isClearingAnalysisSafe()) return;
363 if (setup_analysis_dialog->exec()) {
364 setAnalysis(setup_analysis_dialog->getMussa());
366 } catch(mussa_error e) {
367 QString msg(e.what());
368 QMessageBox::warning(this, tr("Create New Analysis"), msg);
372 void MussaWindow::createSubAnalysis()
374 list<SequenceLocation> result;
375 SequenceLocationModel& model = subanalysis_window->getModel();
376 browser->copySelectedTracksAsSeqLocation(result);
377 for(list<SequenceLocation>::iterator result_itor = result.begin();
378 result_itor != result.end();
381 model.push_back(*result_itor);
384 if (not subanalysis_window->isVisible()) {
385 subanalysis_window->show();
389 void MussaWindow::saveAnalysis()
391 // if we've got an analysis
392 if (analysis and not analysis->empty()) {
393 // if it doesn't have a name we need to pick one
394 if (analysis->get_analysis_path().empty()) {
397 // if we've got a name, has it changed any?
398 // this doesn't work when a sequence changes something (like its
400 //else if (analysis->is_dirty())
403 } catch (std::exception e) {
404 QMessageBox::critical(this,
405 tr("Mussa Save Error"),
407 QMessageBox::Ok, 0, 0);
414 void MussaWindow::saveAnalysisAs()
416 std::auto_ptr<QFileDialog> dialog(new QFileDialog(this));
417 dialog->setAcceptMode(QFileDialog::AcceptSave);
418 dialog->setFileMode(QFileDialog::AnyFile);
419 dialog->setDirectory(*default_dir);
421 QStringList fileNames;
422 if (not dialog->exec()) {
425 fileNames = dialog->selectedFiles();
427 if (fileNames.size() != 1) {
431 fs::path save_path(fileNames[0].toStdString(), fs::native);
432 // do you want to overwrite?
433 if (fs::exists(save_path) and
434 QMessageBox::question(
436 tr("Overwrite File? -- Mussa"),
437 tr("A file called %1 already exists"
438 "do you want to overwrite it?")
440 tr("&Yes"), tr("&No"),
445 analysis->save(save_path);
446 fs::path normalized_path = (save_path / "..").normalize();
447 default_dir->setPath(normalized_path.native_directory_string().c_str());
448 } catch (std::exception e) {
449 QMessageBox::critical(this,
450 tr("Mussa Save Error"),
452 QMessageBox::Ok, 0, 0);
456 bool MussaWindow::isClearingAnalysisSafe()
458 if (analysis and not analysis->empty() and analysis->is_dirty()) {
459 switch (QMessageBox::question(
461 tr("Save Unsaved Changes -- Mussa"),
462 tr("There are unsaved changes,\ndo you want to save?"),
463 tr("&Yes"), tr("&No"), tr("&Cancel"),
478 throw runtime_error("isClearingAnalysis QMesageBox failure");
481 // if we're here we've been saved and can replace
485 void MussaWindow::editMotifs()
487 if (not motif_editor) {
488 motif_editor = new MotifEditor(analysis);
489 connect(motif_editor, SIGNAL(changedMotifs()),
490 this, SLOT(updateAnnotations()));
492 motif_editor->show();
495 void MussaWindow::loadMotifList()
497 QString caption("Mussa Load Motifs");
498 QString filter("Motif list(*.txt *.mtl)");
499 QString path = QFileDialog::getOpenFileName(this,
501 default_dir->absolutePath(),
506 // try to load safely
508 fs::path converted_path(path.toStdString(), fs::native);
509 analysis->load_motifs(converted_path);
510 default_dir->setPath(converted_path.branch_path().native_directory_string().c_str());
511 emit changedMotifs();
512 } catch (std::exception e) {
513 QString msg("Unable to load ");
517 QMessageBox::warning(this, caption, msg);
521 void MussaWindow::saveMotifList()
523 QString caption("Mussa Save Motifs");
524 QString filter("Motif list(*.txt *.mtl)");
525 QString path = QFileDialog::getSaveFileName(this,
527 default_dir->absolutePath(),
532 // try to load safely
534 fs::path converted_path(path.toStdString(), fs::native);
535 if (fs::extension(converted_path).size() == 0) {
536 // no extension, so add one
537 fs::path base_path = converted_path.branch_path();
538 fs::path filename(converted_path.leaf() + ".mtl", fs::native);
539 converted_path = base_path / filename;
541 analysis->save_motifs(converted_path);
542 default_dir->setPath(converted_path.branch_path().native_directory_string().c_str());
543 } catch (std::exception e) {
544 QString msg("Unable to save ");
548 QMessageBox::warning(this, caption, msg);
551 void MussaWindow::loadMupa()
553 QString caption("Load a mussa parameter file");
554 QString filter("Mussa Parameters (*.mupa)");
555 QString mupa_path = QFileDialog::getOpenFileName(
558 default_dir->absolutePath(),
562 if (mupa_path.isNull())
564 // try to load safely
566 // ideally we should open a new window if there's an analysis
567 // but this should work for the moment.
568 if (not isClearingAnalysisSafe()) return;
570 MussaRef m = Mussa::init();
571 fs::path converted_path(mupa_path.toStdString(), fs::native);
572 connect(m.get(), SIGNAL(progress(const QString&, int, int)),
573 this, SLOT(updateProgress(const QString&, int, int)));
574 m->load_mupa_file(converted_path);
578 // grab the path ignoring the mupa file portion
579 default_dir->setPath(converted_path.branch_path().native_directory_string().c_str());
580 } catch (mussa_load_error e) {
581 QString msg("Unable to load ");
585 QMessageBox::warning(this, "Load Parameter", msg);
587 assert (analysis != 0);
590 void MussaWindow::loadSavedAnalysis()
592 QString caption("Load a previously run analysis");
593 QString muway_dir = QFileDialog::getExistingDirectory(
596 default_dir->absolutePath()
599 if (muway_dir.isNull())
601 // try to safely load
603 // ideally we should open a new window if there's an analysis
604 // but this should work for the moment.
605 if (not isClearingAnalysisSafe()) return;
607 MussaRef m = Mussa::init();
608 fs::path converted_path(muway_dir.toStdString(), fs::native);
609 connect(m.get(), SIGNAL(progress(const QString&, int, int)),
610 this, SLOT(updateProgress(const QString&, int, int)));
611 m->load(converted_path);
612 // only switch mussas if we loaded without error
613 if (analysis->empty()) {
614 // our current window is empty so load and replace.
617 default_dir->setPath(converted_path.branch_path().native_directory_string().c_str());
619 MussaWindow *win = new MussaWindow(m);
621 win->default_dir->setPath(converted_path.branch_path().native_directory_string().c_str());
624 } catch (boost::filesystem::filesystem_error e) {
625 QString msg("Unable to load ");
629 QMessageBox::warning(this, "Load Parameter", msg);
630 } catch (mussa_load_error e) {
631 QString msg("Unable to load ");
635 QMessageBox::warning(this, "Load Parameter", msg);
637 assert (analysis != 0);
640 void MussaWindow::newMussaWindow()
642 MussaWindow *win = new MussaWindow(Mussa::init());
643 win->default_dir = default_dir;
647 void MussaWindow::setSoftThreshold(int value)
649 //std::cout << "Soft: " << analysis->get_soft_threshold()
650 // << " Value: " << value << "\n";
651 if (analysis->get_soft_threshold() != value) {
652 threshold->setEnabled( false );
653 //std::cout << "Updating!!!!\n";
654 analysis->set_soft_threshold(value);
658 threshold->setEnabled( true );
662 // std::cout << "NOT Updating!!!!\n";
666 void MussaWindow::showMussaToolbar()
668 if (mussaViewTB->isVisible())
674 void MussaWindow::showBasePairsCopied(size_t bp_copied)
676 QString msg("Copied ");
678 num.setNum(bp_copied);
679 msg += num + " base pairs";
680 statusBar()->showMessage(msg, 5000);
684 void MussaWindow::toggleMotifs()
689 void MussaWindow::showManual()
691 #if defined(QT_QTASSISTANT_FOUND)
692 if (manualAssistant) {
693 manualAssistant->openAssistant();
695 QMessageBox::warning(this,
696 tr("Mussa Help Error"),
697 tr("QtAssistant not setup correctly"),
699 QMessageBox::NoButton,
700 QMessageBox::NoButton);
703 QUrl manual_url("http://woldlab.caltech.edu/~king/mussagl_manual/");
704 if (not QDesktopServices::openUrl(manual_url)) {
705 QMessageBox::warning(this,
706 tr("Mussa Help Error"),
707 tr("Unable to launch webbrowser"),
709 QMessageBox::NoButton,
710 QMessageBox::NoButton);
712 #endif //QT_QTASSISTANT_FOUND
715 void MussaWindow::assistantError(QString message)
717 //std::cout << "QAssistantError: " << message.toStdString() << "\n";
718 QMessageBox::warning ( this, "Warning: Mussagl Manual", message,
720 QMessageBox::NoButton);
723 void MussaWindow::NotImplementedBox()
725 QMessageBox::warning(this, QObject::tr("mussa"), QObject::tr("Not implemented yet"));
728 void MussaWindow::viewMussaAlignment()
730 const set<int>& selected_paths = browser->selectedPaths();
731 if (selected_paths.size() == 0 ) {
732 QMessageBox::warning(this,
733 QObject::tr("mussa"),
734 QObject::tr("you should probably select some paths "
737 MussaAlignedWindowRef ma_win(
738 new MussaAlignedWindow(analysis, default_dir, selected_paths, subanalysis_window)
741 aligned_windows.push_back(ma_win);
742 connect(this, SIGNAL(changedAnnotations()),
743 aligned_windows.back().get(), SLOT(update()));
744 aligned_windows.back()->show();
748 void MussaWindow::updateAnalysis()
750 const Mussa::vector_sequence_type& seqs = analysis->sequences();
751 browser->setSequences(seqs, analysis->colorMapper());
752 assert(browser->sequences().size() == analysis->size());
754 // setRange eventually emits something that causes updateLinks to be called
755 // but it's possible for us to not have had a chance to set out sequences
757 threshold->setRange(analysis->get_threshold(),analysis->get_window());
758 threshold->setBasepairThreshold(analysis->get_soft_threshold());
761 zoom->setValue(browser->zoom());
764 void MussaWindow::updateAnnotations()
766 // motifs were changed in the sequences by
767 // Mussa::update_sequences_motifs
768 emit changedAnnotations();
772 void MussaWindow::updateLinks()
774 if(browser->sequences().size() == 0) {
775 // we don't have any sequences load so we have no business setting links
779 browser->clear_links();
780 bool reversed = false;
781 const NwayPaths& nway = analysis->paths();
783 typedef list<ConservedPath> conserved_paths;
784 typedef conserved_paths::const_iterator const_conserved_paths_itor;
785 for(const_conserved_paths_itor path_itor = nway.refined_pathz.begin();
786 path_itor != nway.refined_pathz.end();
789 // since we were drawing to the start of a window, and opengl lines
790 // are centered around the two connecting points our lines were slightly
791 // offset. the idea of window_offset is to adjust them to the
792 // right for forward compliment or left for reverse compliment
793 // FIXME: figure out how to unit test these computations
794 //GLfloat window_offset = (path_itor->window_size)/2.0;
796 size_t track_len = path_itor->track_indexes.size();
797 vector<int> normalized_path;
798 normalized_path.reserve(track_len);
799 vector<bool> rc_flags(false, track_len);
800 for (size_t track_i=0; track_i != track_len; ++track_i)
802 int x = path_itor->track_indexes[track_i];
803 // at some point when we modify the pathz data structure to keep
804 // track of the score we can put grab the depth here.
806 // are we reverse complimented?
814 normalized_path.push_back(x);
815 rc_flags.push_back(reversed);
817 browser->link(normalized_path, rc_flags, path_itor->window_size);
823 MussaWindow::updateProgress(const QString& description, int current, int max)
826 if (current == max) {
827 if (progress_dialog != 0) {
828 progress_dialog->hide();
829 delete progress_dialog;
833 // if we're starting, create the dialog
834 if (progress_dialog == 0) {
835 QString cancel("Cancel");
836 progress_dialog = new QProgressDialog(description, cancel, current, max, this);
837 progress_dialog->show();
839 // just update the dialog
840 progress_dialog->setValue(current);
843 qApp->processEvents();
846 void MussaWindow::updateAnalysisModified(bool is_modified)
848 setWindowModified(is_modified);
851 void MussaWindow::updateTitle()
854 QString title(analysis->get_title().c_str());
856 setWindowTitle(title);