diff options
Diffstat (limited to 'desktop-widgets/mainwindow.cpp')
-rw-r--r-- | desktop-widgets/mainwindow.cpp | 1923 |
1 files changed, 1923 insertions, 0 deletions
diff --git a/desktop-widgets/mainwindow.cpp b/desktop-widgets/mainwindow.cpp new file mode 100644 index 000000000..e1e0d81a2 --- /dev/null +++ b/desktop-widgets/mainwindow.cpp @@ -0,0 +1,1923 @@ +/* + * mainwindow.cpp + * + * classes for the main UI window in Subsurface + */ +#include "mainwindow.h" + +#include <QFileDialog> +#include <QMessageBox> +#include <QDesktopWidget> +#include <QSettings> +#include <QShortcut> +#include <QToolBar> +#include "version.h" +#include "divelistview.h" +#include "downloadfromdivecomputer.h" +#include "preferences.h" +#include "subsurfacewebservices.h" +#include "divecomputermanagementdialog.h" +#include "about.h" +#include "updatemanager.h" +#include "planner.h" +#include "filtermodels.h" +#include "profile/profilewidget2.h" +#include "globe.h" +#include "divecomputer.h" +#include "maintab.h" +#include "diveplanner.h" +#ifndef NO_PRINTING +#include <QPrintDialog> +#include "printdialog.h" +#endif +#include "tankinfomodel.h" +#include "weigthsysteminfomodel.h" +#include "yearlystatisticsmodel.h" +#include "diveplannermodel.h" +#include "divelogimportdialog.h" +#include "divelogexportdialog.h" +#include "usersurvey.h" +#include "divesitehelpers.h" +#include "windowtitleupdate.h" +#include "locationinformation.h" + +#ifndef NO_USERMANUAL +#include "usermanual.h" +#endif +#include "divepicturemodel.h" +#include "git-access.h" +#include <QNetworkProxy> +#include <QUndoStack> +#include <qthelper.h> +#include <QtConcurrentRun> +#include "subsurface-core/color.h" + +#if defined(FBSUPPORT) +#include "socialnetworks.h" +#endif + +QProgressDialog *progressDialog = NULL; +bool progressDialogCanceled = false; + +extern "C" int updateProgress(int percent) +{ + if (progressDialog) + progressDialog->setValue(percent); + return progressDialogCanceled; +} + +MainWindow *MainWindow::m_Instance = NULL; + +MainWindow::MainWindow() : QMainWindow(), + actionNextDive(0), + actionPreviousDive(0), + helpView(0), + state(VIEWALL), + survey(0) +{ + Q_ASSERT_X(m_Instance == NULL, "MainWindow", "MainWindow recreated!"); + m_Instance = this; + ui.setupUi(this); + read_hashes(); + // Define the States of the Application Here, Currently the states are situations where the different + // widgets will change on the mainwindow. + + // for the "default" mode + MainTab *mainTab = new MainTab(); + DiveListView *diveListView = new DiveListView(); + ProfileWidget2 *profileWidget = new ProfileWidget2(); + +#ifndef NO_MARBLE + GlobeGPS *globeGps = GlobeGPS::instance(); +#else + QWidget *globeGps = NULL; +#endif + + PlannerSettingsWidget *plannerSettings = new PlannerSettingsWidget(); + DivePlannerWidget *plannerWidget = new DivePlannerWidget(); + PlannerDetails *plannerDetails = new PlannerDetails(); + + // what is a sane order for those icons? we should have the ones the user is + // most likely to want towards the top so they are always visible + // and the ones that someone likely sets and then never touches again towards the bottom + profileToolbarActions << ui.profCalcCeiling << ui.profCalcAllTissues << // start with various ceilings + ui.profIncrement3m << ui.profDcCeiling << + ui.profPhe << ui.profPn2 << ui.profPO2 << // partial pressure graphs + ui.profRuler << ui.profScaled << // measuring and scaling + ui.profTogglePicture << ui.profTankbar << + ui.profMod << ui.profNdl_tts << // various values that a user is either interested in or not + ui.profEad << ui.profSAC << + ui.profHR << // very few dive computers support this + ui.profTissues; // maybe less frequently used + + QToolBar *toolBar = new QToolBar(); + Q_FOREACH (QAction *a, profileToolbarActions) + toolBar->addAction(a); + toolBar->setOrientation(Qt::Vertical); + toolBar->setIconSize(QSize(24,24)); + QWidget *profileContainer = new QWidget(); + QHBoxLayout *profLayout = new QHBoxLayout(); + profLayout->setSpacing(0); + profLayout->setMargin(0); + profLayout->setContentsMargins(0,0,0,0); + profLayout->addWidget(toolBar); + profLayout->addWidget(profileWidget); + profileContainer->setLayout(profLayout); + + LocationInformationWidget * diveSiteEdit = new LocationInformationWidget(); + connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, + this, &MainWindow::setDefaultState); + + connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, + mainTab, &MainTab::refreshDiveInfo); + + connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, + mainTab, &MainTab::refreshDisplayedDiveSite); + + QWidget *diveSitePictures = new QWidget(); // Placeholder + + std::pair<QByteArray, QVariant> enabled = std::make_pair("enabled", QVariant(true)); + std::pair<QByteArray, QVariant> disabled = std::make_pair("enabled", QVariant(false)); + PropertyList enabledList; + PropertyList disabledList; + enabledList.push_back(enabled); + disabledList.push_back(disabled); + + registerApplicationState("Default", mainTab, profileContainer, diveListView, globeGps ); + registerApplicationState("AddDive", mainTab, profileContainer, diveListView, globeGps ); + registerApplicationState("EditDive", mainTab, profileContainer, diveListView, globeGps ); + registerApplicationState("PlanDive", plannerWidget, profileContainer, plannerSettings, plannerDetails ); + registerApplicationState("EditPlannedDive", plannerWidget, profileContainer, diveListView, globeGps ); + registerApplicationState("EditDiveSite", diveSiteEdit, profileContainer, diveListView, globeGps); + + setStateProperties("Default", enabledList, enabledList, enabledList,enabledList); + setStateProperties("AddDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("EditDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("PlanDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("EditPlannedDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("EditDiveSite", enabledList, disabledList, disabledList, enabledList); + + setApplicationState("Default"); + + ui.multiFilter->hide(); + + setWindowIcon(QIcon(":subsurface-icon")); + if (!QIcon::hasThemeIcon("window-close")) { + QIcon::setThemeName("subsurface"); + } + connect(dive_list(), SIGNAL(currentDiveChanged(int)), this, SLOT(current_dive_changed(int))); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(readSettings())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveListView, SLOT(update())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveListView, SLOT(reloadHeaderActions())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), information(), SLOT(updateDiveInfo())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), divePlannerWidget(), SLOT(settingsChanged())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), divePlannerSettingsWidget(), SLOT(settingsChanged())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), TankInfoModel::instance(), SLOT(update())); + connect(ui.actionRecent1, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(ui.actionRecent2, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(ui.actionRecent3, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(ui.actionRecent4, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(information(), SIGNAL(addDiveFinished()), graphics(), SLOT(setProfileState())); + connect(DivePlannerPointsModel::instance(), SIGNAL(planCreated()), this, SLOT(planCreated())); + connect(DivePlannerPointsModel::instance(), SIGNAL(planCanceled()), this, SLOT(planCanceled())); + connect(plannerDetails->printPlan(), SIGNAL(pressed()), divePlannerWidget(), SLOT(printDecoPlan())); + connect(this, SIGNAL(startDiveSiteEdit()), this, SLOT(on_actionDiveSiteEdit_triggered())); + +#ifndef NO_MARBLE + connect(information(), SIGNAL(diveSiteChanged(struct dive_site *)), globeGps, SLOT(centerOnDiveSite(struct dive_site *))); +#endif + wtu = new WindowTitleUpdate(); + connect(WindowTitleUpdate::instance(), SIGNAL(updateTitle()), this, SLOT(setAutomaticTitle())); +#ifdef NO_PRINTING + plannerDetails->printPlan()->hide(); + ui.menuFile->removeAction(ui.actionPrint); +#endif +#ifndef USE_LIBGIT23_API + ui.menuFile->removeAction(ui.actionCloudstorageopen); + ui.menuFile->removeAction(ui.actionCloudstoragesave); + qDebug() << "disabled / made invisible the cloud storage stuff"; +#else + enableDisableCloudActions(); +#endif + + ui.mainErrorMessage->hide(); + graphics()->setEmptyState(); + initialUiSetup(); + readSettings(); + diveListView->reload(DiveTripModel::TREE); + diveListView->reloadHeaderActions(); + diveListView->setFocus(); + GlobeGPS::instance()->reload(); + diveListView->expand(dive_list()->model()->index(0, 0)); + diveListView->scrollTo(dive_list()->model()->index(0, 0), QAbstractItemView::PositionAtCenter); + divePlannerWidget()->settingsChanged(); + divePlannerSettingsWidget()->settingsChanged(); +#ifdef NO_MARBLE + ui.menuView->removeAction(ui.actionViewGlobe); +#endif +#ifdef NO_USERMANUAL + ui.menuHelp->removeAction(ui.actionUserManual); +#endif + memset(©PasteDive, 0, sizeof(copyPasteDive)); + memset(&what, 0, sizeof(what)); + + updateManager = new UpdateManager(this); + undoStack = new QUndoStack(this); + QAction *undoAction = undoStack->createUndoAction(this, tr("&Undo")); + QAction *redoAction = undoStack->createRedoAction(this, tr("&Redo")); + undoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Z)); + redoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z)); + QList<QAction*>undoRedoActions; + undoRedoActions.append(undoAction); + undoRedoActions.append(redoAction); + ui.menu_Edit->addActions(undoRedoActions); + + ReverseGeoLookupThread *geoLookup = ReverseGeoLookupThread::instance(); + connect(geoLookup, SIGNAL(started()),information(), SLOT(disableGeoLookupEdition())); + connect(geoLookup, SIGNAL(finished()), information(), SLOT(enableGeoLookupEdition())); +#ifndef NO_PRINTING + // copy the bundled print templates to the user path; no overwriting occurs! + copyPath(getPrintingTemplatePathBundle(), getPrintingTemplatePathUser()); + find_all_templates(); +#endif + +#if defined(FBSUPPORT) + FacebookManager *fb = FacebookManager::instance(); + connect(fb, SIGNAL(justLoggedIn(bool)), ui.actionFacebook, SLOT(setEnabled(bool))); + connect(fb, SIGNAL(justLoggedOut(bool)), ui.actionFacebook, SLOT(setEnabled(bool))); + connect(ui.actionFacebook, SIGNAL(triggered(bool)), fb, SLOT(sendDive())); + ui.actionFacebook->setEnabled(fb->loggedIn()); +#else + ui.actionFacebook->setEnabled(false); +#endif + + + ui.menubar->show(); + set_git_update_cb(&updateProgress); +} + +MainWindow::~MainWindow() +{ + write_hashes(); + m_Instance = NULL; +} + +void MainWindow::setStateProperties(const QByteArray& state, const PropertyList& tl, const PropertyList& tr, const PropertyList& bl, const PropertyList& br) +{ + stateProperties[state] = PropertiesForQuadrant(tl, tr, bl, br); +} + +void MainWindow::on_actionDiveSiteEdit_triggered() { + setApplicationState("EditDiveSite"); +} + +void MainWindow::enableDisableCloudActions() +{ +#ifdef USE_LIBGIT23_API + ui.actionCloudstorageopen->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); + ui.actionCloudstoragesave->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); +#endif +} + +PlannerDetails *MainWindow::plannerDetails() const { + return qobject_cast<PlannerDetails*>(applicationState["PlanDive"].bottomRight); +} + +PlannerSettingsWidget *MainWindow::divePlannerSettingsWidget() { + return qobject_cast<PlannerSettingsWidget*>(applicationState["PlanDive"].bottomLeft); +} + +void MainWindow::setDefaultState() { + setApplicationState("Default"); + if (information()->getEditMode() != MainTab::NONE) { + ui.bottomLeft->currentWidget()->setEnabled(false); + } +} + +void MainWindow::setLoadedWithFiles(bool f) +{ + filesAsArguments = f; +} + +bool MainWindow::filesFromCommandLine() const +{ + return filesAsArguments; +} + +MainWindow *MainWindow::instance() +{ + return m_Instance; +} + +// this gets called after we download dives from a divecomputer +void MainWindow::refreshDisplay(bool doRecreateDiveList) +{ + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + information()->reload(); + TankInfoModel::instance()->update(); + GlobeGPS::instance()->reload(); + if (doRecreateDiveList) + recreateDiveList(); + + setApplicationState("Default"); + dive_list()->setEnabled(true); + dive_list()->setFocus(); + WSInfoModel::instance()->updateInfo(); + if (amount_selected == 0) + cleanUpEmpty(); +} + +void MainWindow::recreateDiveList() +{ + dive_list()->reload(DiveTripModel::CURRENT); + TagFilterModel::instance()->repopulate(); + BuddyFilterModel::instance()->repopulate(); + LocationFilterModel::instance()->repopulate(); + SuitsFilterModel::instance()->repopulate(); +} + +void MainWindow::configureToolbar() { + if (selected_dive>0) { + if (current_dive->dc.divemode == FREEDIVE) { + ui.profCalcCeiling->setDisabled(true); + ui.profCalcAllTissues ->setDisabled(true); + ui.profIncrement3m->setDisabled(true); + ui.profDcCeiling->setDisabled(true); + ui.profPhe->setDisabled(true); + ui.profPn2->setDisabled(true); //TODO is the same as scuba? + ui.profPO2->setDisabled(true); //TODO is the same as scuba? + ui.profRuler->setDisabled(false); + ui.profScaled->setDisabled(false); // measuring and scaling + ui.profTogglePicture->setDisabled(false); + ui.profTankbar->setDisabled(true); + ui.profMod->setDisabled(true); + ui.profNdl_tts->setDisabled(true); + ui.profEad->setDisabled(true); + ui.profSAC->setDisabled(true); + ui.profHR->setDisabled(false); + ui.profTissues->setDisabled(true); + } else { + ui.profCalcCeiling->setDisabled(false); + ui.profCalcAllTissues ->setDisabled(false); + ui.profIncrement3m->setDisabled(false); + ui.profDcCeiling->setDisabled(false); + ui.profPhe->setDisabled(false); + ui.profPn2->setDisabled(false); + ui.profPO2->setDisabled(false); // partial pressure graphs + ui.profRuler->setDisabled(false); + ui.profScaled->setDisabled(false); // measuring and scaling + ui.profTogglePicture->setDisabled(false); + ui.profTankbar->setDisabled(false); + ui.profMod->setDisabled(false); + ui.profNdl_tts->setDisabled(false); // various values that a user is either interested in or not + ui.profEad->setDisabled(false); + ui.profSAC->setDisabled(false); + ui.profHR->setDisabled(false); // very few dive computers support this + ui.profTissues->setDisabled(false);; // maybe less frequently used + } + } +} + +void MainWindow::current_dive_changed(int divenr) +{ + if (divenr >= 0) { + select_dive(divenr); + } + graphics()->plotDive(); + information()->updateDiveInfo(); + configureToolbar(); + GlobeGPS::instance()->reload(); +} + +void MainWindow::on_actionNew_triggered() +{ + on_actionClose_triggered(); +} + +void MainWindow::on_actionOpen_triggered() +{ + if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) + return; + + // yes, this look wrong to use getSaveFileName() for the open dialog, but we need to be able + // to enter file names that don't exist in order to use our git syntax /path/to/dir[branch] + // with is a potentially valid input, but of course won't exist. So getOpenFileName() wouldn't work + QFileDialog dialog(this, tr("Open file"), lastUsedDir(), filter()); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setViewMode(QFileDialog::Detail); + dialog.setLabelText(QFileDialog::Accept, tr("Open")); + dialog.setLabelText(QFileDialog::Reject, tr("Cancel")); + dialog.setAcceptMode(QFileDialog::AcceptOpen); + QStringList filenames; + if (dialog.exec()) + filenames = dialog.selectedFiles(); + if (filenames.isEmpty()) + return; + updateLastUsedDir(QFileInfo(filenames.first()).dir().path()); + closeCurrentFile(); + // some file dialogs decide to add the default extension to a filename without extension + // so we would get dir[branch].ssrf when trying to select dir[branch]. + // let's detect that and remove the incorrect extension + QStringList cleanFilenames; + QRegularExpression reg(".*\\[[^]]+]\\.ssrf", QRegularExpression::CaseInsensitiveOption); + + Q_FOREACH (QString filename, filenames) { + if (reg.match(filename).hasMatch()) + filename.remove(QRegularExpression("\\.ssrf$", QRegularExpression::CaseInsensitiveOption)); + cleanFilenames << filename; + } + loadFiles(cleanFilenames); +} + +void MainWindow::on_actionSave_triggered() +{ + file_save(); +} + +void MainWindow::on_actionSaveAs_triggered() +{ + file_save_as(); +} + +void MainWindow::on_actionCloudstorageopen_triggered() +{ + if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) + return; + + QString filename; + if (getCloudURL(filename)) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return; + } + qDebug() << filename; + + closeCurrentFile(); + + int error; + + showProgressBar(); + QByteArray fileNamePtr = QFile::encodeName(filename); + error = parse_file(fileNamePtr.data()); + if (!error) { + set_filename(fileNamePtr.data(), true); + setTitle(MWTF_FILENAME); + } + getNotificationWidget()->hideNotification(); + process_dives(false, false); + hideProgressBar(); + refreshDisplay(); + ui.actionAutoGroup->setChecked(autogroup); +} + +void MainWindow::on_actionCloudstoragesave_triggered() +{ + QString filename; + if (getCloudURL(filename)) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return; + } + qDebug() << filename; + if (information()->isEditing()) + information()->acceptChanges(); + + showProgressBar(); + + if (save_dives(filename.toUtf8().data())) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return; + } + + hideProgressBar(); + + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + set_filename(filename.toUtf8().data(), true); + setTitle(MWTF_FILENAME); + mark_divelist_changed(false); +} + +void learnImageDirs(QStringList dirnames) +{ + QList<QFuture<void> > futures; + foreach (QString dir, dirnames) { + futures << QtConcurrent::run(learnImages, QDir(dir), 10, false); + } + DivePictureModel::instance()->updateDivePicturesWhenDone(futures); +} + +void MainWindow::on_actionHash_images_triggered() +{ + QFuture<void> future; + QFileDialog dialog(this, tr("Traverse image directories"), lastUsedDir(), filter()); + dialog.setFileMode(QFileDialog::Directory); + dialog.setViewMode(QFileDialog::Detail); + dialog.setLabelText(QFileDialog::Accept, tr("Scan")); + dialog.setLabelText(QFileDialog::Reject, tr("Cancel")); + QStringList dirnames; + if (dialog.exec()) + dirnames = dialog.selectedFiles(); + if (dirnames.isEmpty()) + return; + future = QtConcurrent::run(learnImageDirs,dirnames); + MainWindow::instance()->getNotificationWidget()->showNotification(tr("Scanning images...(this can take a while)"), KMessageWidget::Information); + MainWindow::instance()->getNotificationWidget()->setFuture(future); + +} + +ProfileWidget2 *MainWindow::graphics() const +{ + return qobject_cast<ProfileWidget2*>(applicationState["Default"].topRight->layout()->itemAt(1)->widget()); +} + +void MainWindow::cleanUpEmpty() +{ + information()->clearStats(); + information()->clearInfo(); + information()->clearEquipment(); + information()->updateDiveInfo(true); + graphics()->setEmptyState(); + dive_list()->reload(DiveTripModel::TREE); + GlobeGPS::instance()->reload(); + if (!existing_filename) + setTitle(MWTF_DEFAULT); + disableShortcuts(); +} + +bool MainWindow::okToClose(QString message) +{ + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || + information()->isEditing() ) { + QMessageBox::warning(this, tr("Warning"), message); + return false; + } + if (unsaved_changes() && askSaveChanges() == false) + return false; + + return true; +} + +void MainWindow::closeCurrentFile() +{ + graphics()->setEmptyState(); + /* free the dives and trips */ + clear_git_id(); + clear_dive_file_data(); + cleanUpEmpty(); + mark_divelist_changed(false); + + clear_events(); + + dcList.dcMap.clear(); +} + +void MainWindow::on_actionClose_triggered() +{ + if (okToClose(tr("Please save or cancel the current dive edit before closing the file."))) { + closeCurrentFile(); + // hide any pictures and the filter + DivePictureModel::instance()->updateDivePictures(); + ui.multiFilter->closeFilter(); + recreateDiveList(); + } +} + +QString MainWindow::lastUsedDir() +{ + QSettings settings; + QString lastDir = QDir::homePath(); + + settings.beginGroup("FileDialog"); + if (settings.contains("LastDir")) + if (QDir::setCurrent(settings.value("LastDir").toString())) + lastDir = settings.value("LastDir").toString(); + return lastDir; +} + +void MainWindow::updateLastUsedDir(const QString &dir) +{ + QSettings s; + s.beginGroup("FileDialog"); + s.setValue("LastDir", dir); +} + +void MainWindow::on_actionPrint_triggered() +{ +#ifndef NO_PRINTING + PrintDialog dlg(this); + + dlg.exec(); +#endif +} + +void MainWindow::disableShortcuts(bool disablePaste) +{ + ui.actionPreviousDC->setShortcut(QKeySequence()); + ui.actionNextDC->setShortcut(QKeySequence()); + ui.copy->setShortcut(QKeySequence()); + if (disablePaste) + ui.paste->setShortcut(QKeySequence()); +} + +void MainWindow::enableShortcuts() +{ + ui.actionPreviousDC->setShortcut(Qt::Key_Left); + ui.actionNextDC->setShortcut(Qt::Key_Right); + ui.copy->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C)); + ui.paste->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_V)); +} + +void MainWindow::showProfile() +{ + enableShortcuts(); + graphics()->setProfileState(); + setApplicationState("Default"); +} + +void MainWindow::on_actionPreferences_triggered() +{ + PreferencesDialog::instance()->show(); + PreferencesDialog::instance()->raise(); +} + +void MainWindow::on_actionQuit_triggered() +{ + if (information()->isEditing()) { + information()->rejectChanges(); + if (information()->isEditing()) + // didn't discard the edits + return; + } + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { + DivePlannerPointsModel::instance()->cancelPlan(); + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) + // The planned dive was not discarded + return; + } + + if (unsaved_changes() && (askSaveChanges() == false)) + return; + writeSettings(); + QApplication::quit(); +} + +void MainWindow::on_actionDownloadDC_triggered() +{ + DownloadFromDCWidget dlg(this); + + dlg.exec(); +} + +void MainWindow::on_actionDownloadWeb_triggered() +{ + SubsurfaceWebServices dlg(this); + + dlg.exec(); +} + +void MainWindow::on_actionDivelogs_de_triggered() +{ + DivelogsDeWebServices::instance()->downloadDives(); +} + +void MainWindow::on_actionEditDeviceNames_triggered() +{ + DiveComputerManagementDialog::instance()->init(); + DiveComputerManagementDialog::instance()->update(); + DiveComputerManagementDialog::instance()->show(); +} + +bool MainWindow::plannerStateClean() +{ + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || + information()->isEditing()) { + QMessageBox::warning(this, tr("Warning"), tr("Please save or cancel the current dive edit before trying to add a dive.")); + return false; + } + return true; +} + +void MainWindow::refreshProfile() +{ + showProfile(); + configureToolbar(); + graphics()->replot(get_dive(selected_dive)); + DivePictureModel::instance()->updateDivePictures(); +} + +void MainWindow::planCanceled() +{ + // while planning we might have modified the displayed_dive + // let's refresh what's shown on the profile + refreshProfile(); + refreshDisplay(false); +} + +void MainWindow::planCreated() +{ + // get the new dive selected and assign a number if reasonable + graphics()->setProfileState(); + if (displayed_dive.id == 0) { + // we might have added a new dive (so displayed_dive was cleared out by clone_dive() + dive_list()->unselectDives(); + select_dive(dive_table.nr - 1); + dive_list()->selectDive(selected_dive); + set_dive_nr_for_current_dive(); + } + // make sure our UI is in a consistent state + information()->updateDiveInfo(); + showProfile(); + refreshDisplay(); +} + +void MainWindow::setPlanNotes() +{ + plannerDetails()->divePlanOutput()->setHtml(displayed_dive.notes); +} + +void MainWindow::printPlan() +{ +#ifndef NO_PRINTING + QString diveplan = plannerDetails()->divePlanOutput()->toHtml(); + QString withDisclaimer = QString("<img height=50 src=\":subsurface-icon\"> ") + diveplan + QString(disclaimer); + + QPrinter printer; + QPrintDialog *dialog = new QPrintDialog(&printer, this); + dialog->setWindowTitle(tr("Print runtime table")); + if (dialog->exec() != QDialog::Accepted) + return; + + plannerDetails()->divePlanOutput()->setHtml(withDisclaimer); + plannerDetails()->divePlanOutput()->print(&printer); + plannerDetails()->divePlanOutput()->setHtml(diveplan); +#endif +} + +void MainWindow::setupForAddAndPlan(const char *model) +{ + // clean out the dive and give it an id and the correct dc model + clear_dive(&displayed_dive); + clear_dive_site(&displayed_dive_site); + displayed_dive.id = dive_getUniqID(&displayed_dive); + displayed_dive.when = QDateTime::currentMSecsSinceEpoch() / 1000L + gettimezoneoffset() + 3600; + displayed_dive.dc.model = model; // don't translate! this is stored in the XML file + // setup the dive cylinders + DivePlannerPointsModel::instance()->clear(); + DivePlannerPointsModel::instance()->setupCylinders(); +} + +void MainWindow::on_actionReplanDive_triggered() +{ + if (!plannerStateClean() || !current_dive || !current_dive->dc.model) + return; + else if (strcmp(current_dive->dc.model, "planned dive")) { + if (QMessageBox::warning(this, tr("Warning"), tr("Trying to replan a dive that's not a planned dive."), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) + return; + } + // put us in PLAN mode + DivePlannerPointsModel::instance()->clear(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); + + graphics()->setPlanState(); + graphics()->clearHandlers(); + setApplicationState("PlanDive"); + divePlannerWidget()->setReplanButton(true); + DivePlannerPointsModel::instance()->loadFromDive(current_dive); + reset_cylinders(&displayed_dive, true); +} + +void MainWindow::on_actionDivePlanner_triggered() +{ + if (!plannerStateClean()) + return; + + // put us in PLAN mode + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); + setApplicationState("PlanDive"); + + graphics()->setPlanState(); + + // create a simple starting dive, using the first gas from the just copied cylinders + setupForAddAndPlan("planned dive"); // don't translate, stored in XML file + DivePlannerPointsModel::instance()->setupStartTime(); + DivePlannerPointsModel::instance()->createSimpleDive(); + DivePictureModel::instance()->updateDivePictures(); + divePlannerWidget()->setReplanButton(false); +} + +DivePlannerWidget* MainWindow::divePlannerWidget() { + return qobject_cast<DivePlannerWidget*>(applicationState["PlanDive"].topLeft); +} + +void MainWindow::on_actionAddDive_triggered() +{ + if (!plannerStateClean()) + return; + + if (dive_list()->selectedTrips().count() >= 1) { + dive_list()->rememberSelection(); + dive_list()->clearSelection(); + } + + setApplicationState("AddDive"); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + + // setup things so we can later create our starting dive + setupForAddAndPlan("manually added dive"); // don't translate, stored in the XML file + + // now show the mostly empty main tab + information()->updateDiveInfo(); + + // show main tab + information()->setCurrentIndex(0); + + information()->addDiveStarted(); + + graphics()->setAddState(); + DivePlannerPointsModel::instance()->createSimpleDive(); + configureToolbar(); + graphics()->plotDive(); +} + +void MainWindow::on_actionEditDive_triggered() +{ + if (information()->isEditing() || DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { + QMessageBox::warning(this, tr("Warning"), tr("Please, first finish the current edition before trying to do another.")); + return; + } + + const bool isTripEdit = dive_list()->selectedTrips().count() >= 1; + if (!current_dive || isTripEdit || (current_dive->dc.model && strcmp(current_dive->dc.model, "manually added dive"))) { + QMessageBox::warning(this, tr("Warning"), tr("Trying to edit a dive that's not a manually added dive.")); + return; + } + + DivePlannerPointsModel::instance()->clear(); + disableShortcuts(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + graphics()->setAddState(); + GlobeGPS::instance()->endGetDiveCoordinates(); + setApplicationState("EditDive"); + DivePlannerPointsModel::instance()->loadFromDive(current_dive); + information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); +} + +void MainWindow::on_actionRenumber_triggered() +{ + RenumberDialog::instance()->renumberOnlySelected(false); + RenumberDialog::instance()->show(); +} + +void MainWindow::on_actionAutoGroup_triggered() +{ + autogroup = ui.actionAutoGroup->isChecked(); + if (autogroup) + autogroup_dives(); + else + remove_autogen_trips(); + refreshDisplay(); + mark_divelist_changed(true); +} + +void MainWindow::on_actionYearlyStatistics_triggered() +{ + QDialog d; + QVBoxLayout *l = new QVBoxLayout(&d); + YearlyStatisticsModel *m = new YearlyStatisticsModel(); + QTreeView *view = new QTreeView(); + view->setModel(m); + l->addWidget(view); + d.resize(width() * .8, height() / 2); + d.move(width() * .1, height() / 4); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), &d); + connect(close, SIGNAL(activated()), &d, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), &d); + connect(quit, SIGNAL(activated()), this, SLOT(close())); + d.setWindowFlags(Qt::Window | Qt::CustomizeWindowHint + | Qt::WindowCloseButtonHint | Qt::WindowTitleHint); + d.setWindowTitle(tr("Yearly statistics")); + d.setWindowIcon(QIcon(":/subsurface-icon")); + d.exec(); +} + +#define BEHAVIOR QList<int>() + +#define TOGGLE_COLLAPSABLE( X ) \ + ui.mainSplitter->setCollapsible(0, X); \ + ui.mainSplitter->setCollapsible(1, X); \ + ui.topSplitter->setCollapsible(0, X); \ + ui.topSplitter->setCollapsible(1, X); \ + ui.bottomSplitter->setCollapsible(0, X); \ + ui.bottomSplitter->setCollapsible(1, X); + +void MainWindow::on_actionViewList_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(LIST_MAXIMIZED); + ui.topSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); + ui.mainSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); +} + +void MainWindow::on_actionViewProfile_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(PROFILE_MAXIMIZED); + ui.topSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); + ui.mainSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); +} + +void MainWindow::on_actionViewInfo_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(INFO_MAXIMIZED); + ui.topSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); + ui.mainSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); +} + +void MainWindow::on_actionViewGlobe_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(GLOBE_MAXIMIZED); + ui.mainSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); + ui.bottomSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); +} +#undef BEHAVIOR + +void MainWindow::on_actionViewAll_triggered() +{ + TOGGLE_COLLAPSABLE( false ); + beginChangeState(VIEWALL); + static QList<int> mainSizes; + const int appH = qApp->desktop()->size().height(); + const int appW = qApp->desktop()->size().width(); + if (mainSizes.empty()) { + mainSizes.append(appH * 0.7); + mainSizes.append(appH * 0.3); + } + static QList<int> infoProfileSizes; + if (infoProfileSizes.empty()) { + infoProfileSizes.append(appW * 0.3); + infoProfileSizes.append(appW * 0.7); + } + + static QList<int> listGlobeSizes; + if (listGlobeSizes.empty()) { + listGlobeSizes.append(appW * 0.7); + listGlobeSizes.append(appW * 0.3); + } + + QSettings settings; + settings.beginGroup("MainWindow"); + if (settings.value("mainSplitter").isValid()) { + ui.mainSplitter->restoreState(settings.value("mainSplitter").toByteArray()); + ui.topSplitter->restoreState(settings.value("topSplitter").toByteArray()); + ui.bottomSplitter->restoreState(settings.value("bottomSplitter").toByteArray()); + if (ui.mainSplitter->sizes().first() == 0 || ui.mainSplitter->sizes().last() == 0) + ui.mainSplitter->setSizes(mainSizes); + if (ui.topSplitter->sizes().first() == 0 || ui.topSplitter->sizes().last() == 0) + ui.topSplitter->setSizes(infoProfileSizes); + if (ui.bottomSplitter->sizes().first() == 0 || ui.bottomSplitter->sizes().last() == 0) + ui.bottomSplitter->setSizes(listGlobeSizes); + + } else { + ui.mainSplitter->setSizes(mainSizes); + ui.topSplitter->setSizes(infoProfileSizes); + ui.bottomSplitter->setSizes(listGlobeSizes); + } + ui.mainSplitter->setCollapsible(0, false); + ui.mainSplitter->setCollapsible(1, false); + ui.topSplitter->setCollapsible(0, false); + ui.topSplitter->setCollapsible(1, false); + ui.bottomSplitter->setCollapsible(0,false); + ui.bottomSplitter->setCollapsible(1,false); +} + +#undef TOGGLE_COLLAPSABLE + +void MainWindow::beginChangeState(CurrentState s) +{ + if (state == VIEWALL && state != s) { + saveSplitterSizes(); + } + state = s; +} + +void MainWindow::saveSplitterSizes() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + settings.setValue("mainSplitter", ui.mainSplitter->saveState()); + settings.setValue("topSplitter", ui.topSplitter->saveState()); + settings.setValue("bottomSplitter", ui.bottomSplitter->saveState()); +} + +void MainWindow::on_actionPreviousDC_triggered() +{ + unsigned nrdc = number_of_computers(current_dive); + dc_number = (dc_number + nrdc - 1) % nrdc; + configureToolbar(); + graphics()->plotDive(); + information()->updateDiveInfo(); +} + +void MainWindow::on_actionNextDC_triggered() +{ + unsigned nrdc = number_of_computers(current_dive); + dc_number = (dc_number + 1) % nrdc; + configureToolbar(); + graphics()->plotDive(); + information()->updateDiveInfo(); +} + +void MainWindow::on_actionFullScreen_triggered(bool checked) +{ + if (checked) { + setWindowState(windowState() | Qt::WindowFullScreen); + } else { + setWindowState(windowState() & ~Qt::WindowFullScreen); + } +} + +void MainWindow::on_actionAboutSubsurface_triggered() +{ + SubsurfaceAbout dlg(this); + + dlg.exec(); +} + +void MainWindow::on_action_Check_for_Updates_triggered() +{ + if (!updateManager) + updateManager = new UpdateManager(this); + + updateManager->checkForUpdates(); +} + +void MainWindow::on_actionUserManual_triggered() +{ +#ifndef NO_USERMANUAL + if (!helpView) { + helpView = new UserManual(); + } + helpView->show(); +#endif +} + +void MainWindow::on_actionUserSurvey_triggered() +{ + if(!survey) { + survey = new UserSurvey(this); + } + survey->show(); +} + +QString MainWindow::filter() +{ + QString f; + f += "Dive log files ( *.ssrf "; + f += "*.can *.CAN "; + f += "*.db *.DB " ; + f += "*.sql *.SQL " ; + f += "*.dld *.DLD "; + f += "*.jlb *.JLB "; + f += "*.lvd *.LVD "; + f += "*.sde *.SDE "; + f += "*.udcf *.UDCF "; + f += "*.uddf *.UDDF "; + f += "*.xml *.XML "; + f += "*.dlf *.DLF "; + f += ");;"; + + f += "Subsurface (*.ssrf);;"; + f += "Cochran (*.can *.CAN);;"; + f += "DiveLogs.de (*.dld *.DLD);;"; + f += "JDiveLog (*.jlb *.JLB);;"; + f += "Liquivision (*.lvd *.LVD);;"; + f += "Suunto (*.sde *.SDE *.db *.DB);;"; + f += "UDCF (*.udcf *.UDCF);;"; + f += "UDDF (*.uddf *.UDDF);;"; + f += "XML (*.xml *.XML)"; + f += "Divesoft (*.dlf *.DLF)"; + f += "Datatrak/WLog Files (*.log *.LOG)"; + + return f; +} + +bool MainWindow::askSaveChanges() +{ + QString message; + QMessageBox response(this); + + if (existing_filename) + message = tr("Do you want to save the changes that you made in the file %1?") + .arg(displayedFilename(existing_filename)); + else + message = tr("Do you want to save the changes that you made in the data file?"); + + response.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + response.setDefaultButton(QMessageBox::Save); + response.setText(message); + response.setWindowTitle(tr("Save changes?")); // Not displayed on MacOSX as described in Qt API + response.setInformativeText(tr("Changes will be lost if you don't save them.")); + response.setIcon(QMessageBox::Warning); + response.setWindowModality(Qt::WindowModal); + int ret = response.exec(); + + switch (ret) { + case QMessageBox::Save: + file_save(); + return true; + case QMessageBox::Discard: + return true; + } + return false; +} + +void MainWindow::initialUiSetup() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + if (settings.value("maximized", isMaximized()).value<bool>()) { + showMaximized(); + } else { + restoreGeometry(settings.value("geometry").toByteArray()); + restoreState(settings.value("windowState", 0).toByteArray()); + } + + state = (CurrentState)settings.value("lastState", 0).toInt(); + switch (state) { + case VIEWALL: + on_actionViewAll_triggered(); + break; + case GLOBE_MAXIMIZED: + on_actionViewGlobe_triggered(); + break; + case INFO_MAXIMIZED: + on_actionViewInfo_triggered(); + break; + case LIST_MAXIMIZED: + on_actionViewList_triggered(); + break; + case PROFILE_MAXIMIZED: + on_actionViewProfile_triggered(); + break; + } + settings.endGroup(); + show(); +} + +const char *getSetting(QSettings &s, QString name) +{ + QVariant v; + v = s.value(name); + if (v.isValid()) { + return strdup(v.toString().toUtf8().data()); + } + return NULL; +} + +#define TOOLBOX_PREF_BUTTON(pref, setting, button) \ + prefs.pref = s.value(#setting).toBool(); \ + ui.button->setChecked(prefs.pref); + +void MainWindow::readSettings() +{ + static bool firstRun = true; + QSettings s; + // the static object for preferences already reads in the settings + // and sets up the font, so just get what we need for the toolbox and other widgets here + + s.beginGroup("TecDetails"); + TOOLBOX_PREF_BUTTON(calcalltissues, calcalltissues, profCalcAllTissues); + TOOLBOX_PREF_BUTTON(calcceiling, calcceiling, profCalcCeiling); + TOOLBOX_PREF_BUTTON(dcceiling, dcceiling, profDcCeiling); + TOOLBOX_PREF_BUTTON(ead, ead, profEad); + TOOLBOX_PREF_BUTTON(calcceiling3m, calcceiling3m, profIncrement3m); + TOOLBOX_PREF_BUTTON(mod, mod, profMod); + TOOLBOX_PREF_BUTTON(calcndltts, calcndltts, profNdl_tts); + TOOLBOX_PREF_BUTTON(pp_graphs.phe, phegraph, profPhe); + TOOLBOX_PREF_BUTTON(pp_graphs.pn2, pn2graph, profPn2); + TOOLBOX_PREF_BUTTON(pp_graphs.po2, po2graph, profPO2); + TOOLBOX_PREF_BUTTON(hrgraph, hrgraph, profHR); + TOOLBOX_PREF_BUTTON(rulergraph, rulergraph, profRuler); + TOOLBOX_PREF_BUTTON(show_sac, show_sac, profSAC); + TOOLBOX_PREF_BUTTON(show_pictures_in_profile, show_pictures_in_profile, profTogglePicture); + TOOLBOX_PREF_BUTTON(tankbar, tankbar, profTankbar); + TOOLBOX_PREF_BUTTON(percentagegraph, percentagegraph, profTissues); + TOOLBOX_PREF_BUTTON(zoomed_plot, zoomed_plot, profScaled); + s.endGroup(); // note: why doesn't the list of 17 buttons match the order in the gui? + s.beginGroup("DiveComputer"); + default_dive_computer_vendor = getSetting(s, "dive_computer_vendor"); + default_dive_computer_product = getSetting(s, "dive_computer_product"); + default_dive_computer_device = getSetting(s, "dive_computer_device"); + default_dive_computer_download_mode = s.value("dive_computer_download_mode").toInt(); + s.endGroup(); + QNetworkProxy proxy; + proxy.setType(QNetworkProxy::ProxyType(prefs.proxy_type)); + proxy.setHostName(prefs.proxy_host); + proxy.setPort(prefs.proxy_port); + if (prefs.proxy_auth) { + proxy.setUser(prefs.proxy_user); + proxy.setPassword(prefs.proxy_pass); + } + QNetworkProxy::setApplicationProxy(proxy); + +#if !defined(SUBSURFACE_MOBILE) + loadRecentFiles(&s); + if (firstRun) { + checkSurvey(&s); + firstRun = false; + } +#endif +} + +#undef TOOLBOX_PREF_BUTTON + +void MainWindow::checkSurvey(QSettings *s) +{ + s->beginGroup("UserSurvey"); + if (!s->contains("FirstUse42")) { + QVariant value = QDate().currentDate(); + s->setValue("FirstUse42", value); + } + // wait a week for production versions, but not at all for non-tagged builds + QString ver(subsurface_version()); + int waitTime = 7; + QDate firstUse42 = s->value("FirstUse42").toDate(); + if (run_survey || (firstUse42.daysTo(QDate().currentDate()) > waitTime && !s->contains("SurveyDone"))) { + if (!survey) + survey = new UserSurvey(this); + survey->show(); + } + s->endGroup(); +} + +void MainWindow::writeSettings() +{ + QSettings settings; + + settings.beginGroup("MainWindow"); + settings.setValue("geometry", saveGeometry()); + settings.setValue("windowState", saveState()); + settings.setValue("maximized", isMaximized()); + settings.setValue("lastState", (int)state); + if (state == VIEWALL) + saveSplitterSizes(); + settings.endGroup(); +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || + information()->isEditing()) { + on_actionQuit_triggered(); + event->ignore(); + return; + } + +#ifndef NO_USERMANUAL + if (helpView && helpView->isVisible()) { + helpView->close(); + helpView->deleteLater(); + } +#endif + + if (survey && survey->isVisible()) { + survey->close(); + survey->deleteLater(); + } + + if (unsaved_changes() && (askSaveChanges() == false)) { + event->ignore(); + return; + } + event->accept(); + writeSettings(); + QApplication::closeAllWindows(); +} + +DiveListView *MainWindow::dive_list() +{ + return qobject_cast<DiveListView*>(applicationState["Default"].bottomLeft); +} + +MainTab *MainWindow::information() +{ + return qobject_cast<MainTab*>(applicationState["Default"].topLeft); +} + +void MainWindow::loadRecentFiles(QSettings *s) +{ + QStringList files; + bool modified = false; + + s->beginGroup("Recent_Files"); + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + if (s->contains(key)) { + QString file = s->value(key).toString(); + + if (QFile::exists(file)) { + files.append(file); + } else { + modified = true; + } + } else { + break; + } + } + + if (modified) { + for (int c = 0; c < 4; c++) { + QString key = QString("File_%1").arg(c + 1); + + if (files.count() > c) { + s->setValue(key, files.at(c)); + } else { + if (s->contains(key)) { + s->remove(key); + } + } + } + + s->sync(); + } + s->endGroup(); + + for (int c = 0; c < 4; c++) { + QAction *action = this->findChild<QAction *>(QString("actionRecent%1").arg(c + 1)); + + if (files.count() > c) { + QFileInfo fi(files.at(c)); + action->setText(fi.fileName()); + action->setToolTip(fi.absoluteFilePath()); + action->setVisible(true); + } else { + action->setVisible(false); + } + } +} + +void MainWindow::addRecentFile(const QStringList &newFiles) +{ + QStringList files; + QSettings s; + + if (newFiles.isEmpty()) + return; + + s.beginGroup("Recent_Files"); + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + if (s.contains(key)) { + QString file = s.value(key).toString(); + + files.append(file); + } else { + break; + } + } + + foreach (const QString &file, newFiles) { + int index = files.indexOf(QDir::toNativeSeparators(file)); + + if (index >= 0) { + files.removeAt(index); + } + } + + foreach (const QString &file, newFiles) { + if (QFile::exists(file)) { + files.prepend(QDir::toNativeSeparators(file)); + } + } + + while (files.count() > 4) { + files.removeLast(); + } + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + + if (files.count() >= c) { + s.setValue(key, files.at(c - 1)); + } else { + if (s.contains(key)) { + s.remove(key); + } + } + } + s.endGroup(); + s.sync(); + + loadRecentFiles(&s); +} + +void MainWindow::removeRecentFile(QStringList failedFiles) +{ + QStringList files; + QSettings s; + + if (failedFiles.isEmpty()) + return; + + s.beginGroup("Recent_Files"); + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + + if (s.contains(key)) { + QString file = s.value(key).toString(); + files.append(file); + } else { + break; + } + } + + foreach (const QString &file, failedFiles) + files.removeAll(file); + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + + if (files.count() >= c) + s.setValue(key, files.at(c - 1)); + else if (s.contains(key)) + s.remove(key); + } + + s.endGroup(); + s.sync(); + + loadRecentFiles(&s); +} + +void MainWindow::recentFileTriggered(bool checked) +{ + Q_UNUSED(checked); + + if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) + return; + + QAction *actionRecent = (QAction *)sender(); + + const QString &filename = actionRecent->toolTip(); + + updateLastUsedDir(QFileInfo(filename).dir().path()); + closeCurrentFile(); + loadFiles(QStringList() << filename); +} + +int MainWindow::file_save_as(void) +{ + QString filename; + const char *default_filename = existing_filename; + + // if the default is to save to cloud storage, pick something that will work as local file: + // simply extract the branch name which should be the users email address + if (default_filename && strstr(default_filename, prefs.cloud_git_url)) { + QString filename(default_filename); + filename.remove(prefs.cloud_git_url); + filename.remove(0, filename.indexOf("[") + 1); + filename.replace("]", ".ssrf"); + default_filename = strdup(qPrintable(filename)); + } + // create a file dialog that allows us to save to a new file + QFileDialog selection_dialog(this, tr("Save file as"), default_filename, + tr("Subsurface XML files (*.ssrf *.xml *.XML)")); + selection_dialog.setAcceptMode(QFileDialog::AcceptSave); + selection_dialog.setFileMode(QFileDialog::AnyFile); + selection_dialog.setDefaultSuffix(""); + if (same_string(default_filename, "")) { + QFileInfo defaultFile(system_default_filename()); + selection_dialog.setDirectory(qPrintable(defaultFile.absolutePath())); + } + /* if the exit/cancel button is pressed return */ + if (!selection_dialog.exec()) + return 0; + + /* get the first selected file */ + filename = selection_dialog.selectedFiles().at(0); + + /* now for reasons I don't understand we appear to add a .ssrf to + * git style filenames <path>/directory[branch] + * so let's remove that */ + QRegularExpression reg(".*\\[[^]]+]\\.ssrf", QRegularExpression::CaseInsensitiveOption); + if (reg.match(filename).hasMatch()) + filename.remove(QRegularExpression("\\.ssrf$", QRegularExpression::CaseInsensitiveOption)); + if (filename.isNull() || filename.isEmpty()) + return report_error("No filename to save into"); + + if (information()->isEditing()) + information()->acceptChanges(); + + if (save_dives(filename.toUtf8().data())) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return -1; + } + + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + set_filename(filename.toUtf8().data(), true); + setTitle(MWTF_FILENAME); + mark_divelist_changed(false); + addRecentFile(QStringList() << filename); + return 0; +} + +int MainWindow::file_save(void) +{ + const char *current_default; + bool is_cloud = false; + + if (!existing_filename) + return file_save_as(); + + is_cloud = (strncmp(existing_filename, "http", 4) == 0); + + if (information()->isEditing()) + information()->acceptChanges(); + + current_default = prefs.default_filename; + if (strcmp(existing_filename, current_default) == 0) { + /* if we are using the default filename the directory + * that we are creating the file in may not exist */ + QDir current_def_dir = QFileInfo(current_default).absoluteDir(); + if (!current_def_dir.exists()) + current_def_dir.mkpath(current_def_dir.absolutePath()); + } + if (is_cloud) + showProgressBar(); + if (save_dives(existing_filename)) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + if (is_cloud) + hideProgressBar(); + return -1; + } + if (is_cloud) + hideProgressBar(); + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + mark_divelist_changed(false); + addRecentFile(QStringList() << QString(existing_filename)); + return 0; +} + +NotificationWidget *MainWindow::getNotificationWidget() +{ + return ui.mainErrorMessage; +} + +void MainWindow::showError() +{ + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); +} + +QString MainWindow::displayedFilename(QString fullFilename) +{ + QFile f(fullFilename); + QFileInfo fileInfo(f); + QString fileName(fileInfo.fileName()); + + if (fullFilename.contains(prefs.cloud_git_url)) + return tr("[cloud storage for] %1").arg(fileName.left(fileName.indexOf('['))); + else + return fileName; +} + +void MainWindow::setAutomaticTitle() +{ + setTitle(); +} + +void MainWindow::setTitle(enum MainWindowTitleFormat format) +{ + switch (format) { + case MWTF_DEFAULT: + setWindowTitle("Subsurface"); + break; + case MWTF_FILENAME: + if (!existing_filename) { + setTitle(MWTF_DEFAULT); + return; + } + QString unsaved = (unsaved_changes() ? " *" : ""); + setWindowTitle("Subsurface: " + displayedFilename(existing_filename) + unsaved); + break; + } +} + +void MainWindow::importFiles(const QStringList fileNames) +{ + if (fileNames.isEmpty()) + return; + + QByteArray fileNamePtr; + + for (int i = 0; i < fileNames.size(); ++i) { + fileNamePtr = QFile::encodeName(fileNames.at(i)); + parse_file(fileNamePtr.data()); + } + process_dives(true, false); + refreshDisplay(); +} + +void MainWindow::importTxtFiles(const QStringList fileNames) +{ + if (fileNames.isEmpty()) + return; + + QByteArray fileNamePtr, csv; + + for (int i = 0; i < fileNames.size(); ++i) { + fileNamePtr = QFile::encodeName(fileNames.at(i)); + csv = fileNamePtr.data(); + csv.replace(strlen(csv.data()) - 3, 3, "csv"); + parse_txt_file(fileNamePtr.data(), csv); + } + process_dives(true, false); + refreshDisplay(); +} + +void MainWindow::loadFiles(const QStringList fileNames) +{ + bool showWarning = false; + if (fileNames.isEmpty()) { + refreshDisplay(); + return; + } + QByteArray fileNamePtr; + QStringList failedParses; + + showProgressBar(); + for (int i = 0; i < fileNames.size(); ++i) { + int error; + + fileNamePtr = QFile::encodeName(fileNames.at(i)); + error = parse_file(fileNamePtr.data()); + if (!error) { + set_filename(fileNamePtr.data(), true); + setTitle(MWTF_FILENAME); + // if there were any messages, show them + QString warning = get_error_string(); + if (!warning.isEmpty()) { + showWarning = true; + getNotificationWidget()->showNotification(warning , KMessageWidget::Information); + } + } else { + failedParses.append(fileNames.at(i)); + } + } + hideProgressBar(); + if (!showWarning) + getNotificationWidget()->hideNotification(); + process_dives(false, false); + addRecentFile(fileNames); + removeRecentFile(failedParses); + + refreshDisplay(); + ui.actionAutoGroup->setChecked(autogroup); + + int min_datafile_version = get_min_datafile_version(); + if (min_datafile_version >0 && min_datafile_version < DATAFORMAT_VERSION) { + QMessageBox::warning(this, tr("Opening datafile from older version"), + tr("You opened a data file from an older version of Subsurface. We recommend " + "you read the manual to learn about the changes in the new version, especially " + "about dive site management which has changed significantly.\n" + "Subsurface has already tried to pre-populate the data but it might be worth " + "while taking a look at the new dive site management system and to make " + "sure that everything looks correct.")); + } +} + +void MainWindow::on_actionImportDiveLog_triggered() +{ + QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open dive log file"), lastUsedDir(), + tr("Dive log files (*.ssrf *.can *.csv *.db *.sql *.dld *.jlb *.lvd *.sde *.udcf *.uddf *.xml *.txt *.dlf *.apd" + "*.SSRF *.CAN *.CSV *.DB *.SQL *.DLD *.JLB *.LVD *.SDE *.UDCF *.UDDF *.xml *.TXT *.DLF *.APD);;" + "Cochran files (*.can *.CAN);;" + "CSV files (*.csv *.CSV);;" + "DiveLog.de files (*.dld *.DLD);;" + "JDiveLog files (*.jlb *.JLB);;" + "Liquivision files (*.lvd *.LVD);;" + "MkVI files (*.txt *.TXT);;" + "Suunto files (*.sde *.db *.SDE *.DB);;" + "Divesoft files (*.dlf *.DLF);;" + "UDDF/UDCF files (*.uddf *.udcf *.UDDF *.UDCF);;" + "XML files (*.xml *.XML);;" + "APD log viewer (*.apd *.APD);;" + "Datatrak/WLog Files (*.log *.LOG);;" + "OSTCtools Files (*.dive *.DIVE);;" + "All files (*)")); + + if (fileNames.isEmpty()) + return; + updateLastUsedDir(QFileInfo(fileNames[0]).dir().path()); + + QStringList logFiles = fileNames.filter(QRegExp("^.*\\.(?!(csv|txt|apd))", Qt::CaseInsensitive)); + QStringList csvFiles = fileNames.filter(".csv", Qt::CaseInsensitive); + csvFiles += fileNames.filter(".apd", Qt::CaseInsensitive); + QStringList txtFiles = fileNames.filter(".txt", Qt::CaseInsensitive); + + if (logFiles.size()) { + importFiles(logFiles); + } + + if (csvFiles.size()) { + DiveLogImportDialog *diveLogImport = new DiveLogImportDialog(csvFiles, this); + diveLogImport->show(); + process_dives(true, false); + refreshDisplay(); + } + + if (txtFiles.size()) { + importTxtFiles(txtFiles); + } +} + +void MainWindow::editCurrentDive() +{ + if (information()->isEditing() || DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { + QMessageBox::warning(this, tr("Warning"), tr("Please, first finish the current edition before trying to do another.")); + return; + } + + struct dive *d = current_dive; + QString defaultDC(d->dc.model); + DivePlannerPointsModel::instance()->clear(); + if (defaultDC == "manually added dive") { + disableShortcuts(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + graphics()->setAddState(); + setApplicationState("EditDive"); + DivePlannerPointsModel::instance()->loadFromDive(d); + information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); + } else if (defaultDC == "planned dive") { + disableShortcuts(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); + setApplicationState("EditPlannedDive"); + DivePlannerPointsModel::instance()->loadFromDive(d); + information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); + } +} + +#define PREF_PROFILE(QT_PREFS) \ + QSettings s; \ + s.beginGroup("TecDetails"); \ + s.setValue(#QT_PREFS, triggered); \ + PreferencesDialog::instance()->emitSettingsChanged(); + +#define TOOLBOX_PREF_PROFILE(METHOD, INTERNAL_PREFS, QT_PREFS) \ + void MainWindow::on_##METHOD##_triggered(bool triggered) \ + { \ + prefs.INTERNAL_PREFS = triggered; \ + PREF_PROFILE(QT_PREFS); \ + } + +// note: why doesn't the list of 17 buttons match the order in the gui? or the order above? (line 1136) +TOOLBOX_PREF_PROFILE(profCalcAllTissues, calcalltissues, calcalltissues); +TOOLBOX_PREF_PROFILE(profCalcCeiling, calcceiling, calcceiling); +TOOLBOX_PREF_PROFILE(profDcCeiling, dcceiling, dcceiling); +TOOLBOX_PREF_PROFILE(profEad, ead, ead); +TOOLBOX_PREF_PROFILE(profIncrement3m, calcceiling3m, calcceiling3m); +TOOLBOX_PREF_PROFILE(profMod, mod, mod); +TOOLBOX_PREF_PROFILE(profNdl_tts, calcndltts, calcndltts); +TOOLBOX_PREF_PROFILE(profPhe, pp_graphs.phe, phegraph); +TOOLBOX_PREF_PROFILE(profPn2, pp_graphs.pn2, pn2graph); +TOOLBOX_PREF_PROFILE(profPO2, pp_graphs.po2, po2graph); +TOOLBOX_PREF_PROFILE(profHR, hrgraph, hrgraph); +TOOLBOX_PREF_PROFILE(profRuler, rulergraph, rulergraph); +TOOLBOX_PREF_PROFILE(profSAC, show_sac, show_sac); +TOOLBOX_PREF_PROFILE(profScaled, zoomed_plot, zoomed_plot); +TOOLBOX_PREF_PROFILE(profTogglePicture, show_pictures_in_profile, show_pictures_in_profile); +TOOLBOX_PREF_PROFILE(profTankbar, tankbar, tankbar); +TOOLBOX_PREF_PROFILE(profTissues, percentagegraph, percentagegraph); +// couldn't the args to TOOLBOX_PREF_PROFILE be made to go in the same sequence as TOOLBOX_PREF_BUTTON? + +void MainWindow::turnOffNdlTts() +{ + const bool triggered = false; + prefs.calcndltts = triggered; + PREF_PROFILE(calcndltts); +} + +#undef TOOLBOX_PREF_PROFILE +#undef PERF_PROFILE + +void MainWindow::on_actionExport_triggered() +{ + DiveLogExportDialog diveLogExport; + diveLogExport.exec(); +} + +void MainWindow::on_actionConfigure_Dive_Computer_triggered() +{ + ConfigureDiveComputerDialog *dcConfig = new ConfigureDiveComputerDialog(this); + dcConfig->show(); +} + +void MainWindow::setEnabledToolbar(bool arg1) +{ + Q_FOREACH (QAction *b, profileToolbarActions) + b->setEnabled(arg1); +} + +void MainWindow::on_copy_triggered() +{ + // open dialog to select what gets copied + // copy the displayed dive + DiveComponentSelection dialog(this, ©PasteDive, &what); + dialog.exec(); +} + +void MainWindow::on_paste_triggered() +{ + // take the data in our copyPasteDive and apply it to selected dives + selective_copy_dive(©PasteDive, &displayed_dive, what, false); + information()->showAndTriggerEditSelective(what); +} + +void MainWindow::on_actionFilterTags_triggered() +{ + if (ui.multiFilter->isVisible()) + ui.multiFilter->closeFilter(); + else + ui.multiFilter->setVisible(true); +} + +void MainWindow::registerApplicationState(const QByteArray& state, QWidget *topLeft, QWidget *topRight, QWidget *bottomLeft, QWidget *bottomRight) +{ + applicationState[state] = WidgetForQuadrant(topLeft, topRight, bottomLeft, bottomRight); + if (ui.topLeft->indexOf(topLeft) == -1 && topLeft) { + ui.topLeft->addWidget(topLeft); + } + if (ui.topRight->indexOf(topRight) == -1 && topRight) { + ui.topRight->addWidget(topRight); + } + if (ui.bottomLeft->indexOf(bottomLeft) == -1 && bottomLeft) { + ui.bottomLeft->addWidget(bottomLeft); + } + if(ui.bottomRight->indexOf(bottomRight) == -1 && bottomRight) { + ui.bottomRight->addWidget(bottomRight); + } +} + +void MainWindow::setApplicationState(const QByteArray& state) { + if (!applicationState.keys().contains(state)) + return; + + if (getCurrentAppState() == state) + return; + + setCurrentAppState(state); + +#define SET_CURRENT_INDEX( X ) \ + if (applicationState[state].X) { \ + ui.X->setCurrentWidget( applicationState[state].X); \ + ui.X->show(); \ + } else { \ + ui.X->hide(); \ + } + + SET_CURRENT_INDEX( topLeft ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].topLeft) { + ui.topLeft->currentWidget()->setProperty( p.first.data(), p.second); + } + SET_CURRENT_INDEX( topRight ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].topRight) { + ui.topRight->currentWidget()->setProperty( p.first.data(), p.second); + } + SET_CURRENT_INDEX( bottomLeft ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].bottomLeft) { + ui.bottomLeft->currentWidget()->setProperty( p.first.data(), p.second); + } + SET_CURRENT_INDEX( bottomRight ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].bottomRight) { + ui.bottomRight->currentWidget()->setProperty( p.first.data(), p.second); + } +#undef SET_CURRENT_INDEX +} + +void MainWindow::showProgressBar() +{ + if (progressDialog) + delete progressDialog; + + progressDialog = new QProgressDialog(tr("Contacting cloud service..."), tr("Cancel"), 0, 100, this); + progressDialog->setWindowModality(Qt::WindowModal); + progressDialog->setMinimumDuration(200); + progressDialogCanceled = false; + connect(progressDialog, SIGNAL(canceled()), this, SLOT(cancelCloudStorageOperation())); +} + +void MainWindow::cancelCloudStorageOperation() +{ + progressDialogCanceled = true; +} + +void MainWindow::hideProgressBar() +{ + if (progressDialog) { + progressDialog->setValue(100); + progressDialog->deleteLater(); + progressDialog = NULL; + } +} |