// SPDX-License-Identifier: GPL-2.0 /* * mainwindow.cpp * * classes for the main UI window in Subsurface */ #include "mainwindow.h" #include #include #include #include #include #include #include #include #include #include "core/color.h" #include "core/device.h" #include "core/divesitehelpers.h" #include "core/errorhelper.h" #include "core/file.h" #include "core/gettextfromc.h" #include "core/git-access.h" #include "core/import-csv.h" #include "core/planner.h" #include "core/qthelper.h" #include "core/subsurface-string.h" #include "core/trip.h" #include "core/version.h" #include "core/windowtitleupdate.h" #include "core/settings/qPrefCloudStorage.h" #include "core/settings/qPrefDisplay.h" #include "core/settings/qPrefPartialPressureGas.h" #include "core/settings/qPrefTechnicalDetails.h" #include "desktop-widgets/about.h" #include "desktop-widgets/divecomputermanagementdialog.h" #include "desktop-widgets/divelistview.h" #include "desktop-widgets/divelogexportdialog.h" #include "desktop-widgets/divelogimportdialog.h" #include "desktop-widgets/divesiteimportdialog.h" #include "desktop-widgets/diveplanner.h" #include "desktop-widgets/downloadfromdivecomputer.h" #include "desktop-widgets/findmovedimagesdialog.h" #include "desktop-widgets/locationinformation.h" #include "desktop-widgets/mapwidget.h" #include "desktop-widgets/subsurfacewebservices.h" #include "desktop-widgets/tab-widgets/maintab.h" #include "desktop-widgets/updatemanager.h" #include "desktop-widgets/simplewidgets.h" #include "commands/command.h" #include "profile-widget/profilewidget2.h" #ifndef NO_PRINTING #include #include #include "desktop-widgets/printdialog.h" #endif #include "qt-models/cylindermodel.h" #include "qt-models/divepicturemodel.h" #include "qt-models/diveplannermodel.h" #include "qt-models/filtermodels.h" #include "qt-models/tankinfomodel.h" #include "qt-models/weightsysteminfomodel.h" #include "qt-models/yearlystatisticsmodel.h" #include "preferences/preferencesdialog.h" #ifndef NO_USERMANUAL #include "usermanual.h" #endif namespace { QProgressDialog *progressDialog = nullptr; bool progressDialogCanceled = false; int progressCounter = 0; int round_int (double value) { return static_cast(lrint(value)); }; } extern "C" int updateProgress(const char *text) { if (verbose) qDebug() << "git storage:" << text; if (progressDialog) { // apparently we don't always get enough space to show the full label // so let's manually make enough space (but don't shrink the existing size) #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) int width = QFontMetrics(qApp->font()).width(text) + 100; #else // QT 5.11 or newer int width = QFontMetrics(qApp->font()).horizontalAdvance(text) + 100; #endif if (width > progressDialog->width()) progressDialog->resize(width + 20, progressDialog->height()); progressDialog->setLabelText(text); progressDialog->setValue(++progressCounter); if (progressCounter == 100) progressCounter = 0; // yes this is silly, but we really don't know how long it will take } qApp->processEvents(); return progressDialogCanceled; } MainWindow *MainWindow::m_Instance = nullptr; extern "C" void showErrorFromC(char *buf) { QString error(buf); free(buf); emit MainWindow::instance()->showError(error); } MainWindow::MainWindow() : QMainWindow(), actionNextDive(nullptr), actionPreviousDive(nullptr), #ifndef NO_USERMANUAL helpView(0), #endif state(VIEWALL), findMovedImagesDialog(nullptr) { Q_ASSERT_X(m_Instance == NULL, "MainWindow", "MainWindow recreated!"); m_Instance = this; ui.setupUi(this); read_hashes(); Command::init(); // 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.reset(new MainTab); diveList = new DiveListView(this); graphics = new ProfileWidget2(this); MapWidget *mapWidget = MapWidget::instance(); divePlannerSettingsWidget = new PlannerSettingsWidget(this); divePlannerWidget = new DivePlannerWidget(this); plannerDetails = new PlannerDetails(this); // 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.profDeco, 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(graphics); profileContainer->setLayout(profLayout); diveSiteEdit = new LocationInformationWidget(this); registerApplicationState(ApplicationState::Default, { { mainTab.get(), FLAG_NONE }, { profileContainer, FLAG_NONE }, { diveList, FLAG_NONE }, { mapWidget, FLAG_NONE } }); registerApplicationState(ApplicationState::EditDive, { { mainTab.get(), FLAG_NONE }, { profileContainer, FLAG_NONE }, { diveList, FLAG_NONE }, { mapWidget, FLAG_NONE } }); registerApplicationState(ApplicationState::PlanDive, { { divePlannerWidget, FLAG_NONE }, { profileContainer, FLAG_NONE }, { divePlannerSettingsWidget, FLAG_NONE }, { plannerDetails, FLAG_NONE } }); registerApplicationState(ApplicationState::EditPlannedDive, { { divePlannerWidget, FLAG_NONE }, { profileContainer, FLAG_NONE }, { diveList, FLAG_NONE }, { mapWidget, FLAG_NONE } }); registerApplicationState(ApplicationState::EditDiveSite, { { diveSiteEdit, FLAG_NONE }, { profileContainer, FLAG_DISABLED }, { diveList, FLAG_DISABLED }, { mapWidget, FLAG_NONE } }); registerApplicationState(ApplicationState::FilterDive, { { mainTab.get(), FLAG_NONE }, { profileContainer, FLAG_NONE }, { diveList, FLAG_NONE }, { &filterWidget2, FLAG_NONE } }); setApplicationState(ApplicationState::Default); setWindowIcon(QIcon(":subsurface-icon")); if (!QIcon::hasThemeIcon("window-close")) { QIcon::setThemeName("subsurface"); } connect(diveList, &DiveListView::divesSelected, this, &MainWindow::selectionChanged); connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(readSettings())); connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveList, SLOT(update())); connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveList, SLOT(reloadHeaderActions())); connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), mainTab.get(), 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())); for (int i = 0; i < NUM_RECENT_FILES; i++) { actionsRecent[i] = new QAction(this); actionsRecent[i]->setData(i); ui.menuFile->insertAction(ui.actionQuit, actionsRecent[i]); connect(actionsRecent[i], SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); } ui.menuFile->insertSeparator(ui.actionQuit); 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, &MainWindow::showError, ui.mainErrorMessage, &NotificationWidget::showError, Qt::AutoConnection); connect(&windowTitleUpdate, &WindowTitleUpdate::updateTitle, this, &MainWindow::setAutomaticTitle); connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &MainWindow::setAutomaticTitle); // monitor when dives changed - but only in verbose mode // careful - changing verbose at runtime isn't enough (of course that could be added if we want it) if (verbose) connect(&diveListNotifier, &DiveListNotifier::divesChanged, this, &MainWindow::divesChanged); #ifdef NO_PRINTING plannerDetails->printPlan()->hide(); ui.menuFile->removeAction(ui.actionPrint); #endif enableDisableCloudActions(); ui.mainErrorMessage->hide(); graphics->setEmptyState(); initialUiSetup(); readSettings(); diveList->reload(); diveList->reloadHeaderActions(); diveList->setFocus(); MapWidget::instance()->reload(); diveList->expand(diveList->model()->index(0, 0)); diveList->scrollTo(diveList->model()->index(0, 0), QAbstractItemView::PositionAtCenter); divePlannerWidget->settingsChanged(); divePlannerSettingsWidget->settingsChanged(); #ifdef NO_USERMANUAL ui.menuHelp->removeAction(ui.actionUserManual); #endif memset(©PasteDive, 0, sizeof(copyPasteDive)); memset(&what, 0, sizeof(what)); updateManager = new UpdateManager(this); undoAction = Command::undoAction(this); redoAction = Command::redoAction(this); undoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Z)); redoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z)); ui.menu_Edit->addActions({ undoAction, redoAction }); #ifndef NO_PRINTING // copy the bundled print templates to the user path QStringList templateBackupList; QString templatePathUser(getPrintingTemplatePathUser()); copy_bundled_templates(getPrintingTemplatePathBundle(), templatePathUser, &templateBackupList); if (templateBackupList.length()) { QMessageBox msgBox(this); templatePathUser.replace("\\", "/"); templateBackupList.replaceInStrings(templatePathUser + "/", ""); msgBox.setWindowTitle(tr("Template backup created")); msgBox.setText(tr("The following backup printing templates were created:\n\n%1\n\n" "Location:\n%2\n\n" "Please note that as of this version of Subsurface the default templates\n" "are read-only and should not be edited directly, since the application\n" "can overwrite them on startup.").arg(templateBackupList.join("\n")).arg(templatePathUser)); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.exec(); } set_bundled_templates_as_read_only(); find_all_templates(); #endif setupSocialNetworkMenu(); set_git_update_cb(&updateProgress); set_error_cb(&showErrorFromC); // Toolbar Connections related to the Profile Update auto tec = qPrefTechnicalDetails::instance(); connect(ui.profCalcAllTissues, &QAction::triggered, tec, &qPrefTechnicalDetails::set_calcalltissues); connect(ui.profCalcCeiling, &QAction::triggered, tec, &qPrefTechnicalDetails::set_calcceiling); connect(ui.profDcCeiling, &QAction::triggered, tec, &qPrefTechnicalDetails::set_dcceiling); connect(ui.profEad, &QAction::triggered, tec, &qPrefTechnicalDetails::set_ead); connect(ui.profIncrement3m, &QAction::triggered, tec, &qPrefTechnicalDetails::set_calcceiling3m); connect(ui.profMod, &QAction::triggered, tec, &qPrefTechnicalDetails::set_mod); connect(ui.profNdl_tts, &QAction::triggered, tec, &qPrefTechnicalDetails::set_calcndltts); connect(ui.profDeco, &QAction::triggered, tec, &qPrefTechnicalDetails::set_decoinfo); connect(ui.profHR, &QAction::triggered, tec, &qPrefTechnicalDetails::set_hrgraph); connect(ui.profRuler, &QAction::triggered, tec, &qPrefTechnicalDetails::set_rulergraph); connect(ui.profSAC, &QAction::triggered, tec, &qPrefTechnicalDetails::set_show_sac); connect(ui.profScaled, &QAction::triggered, tec, &qPrefTechnicalDetails::set_zoomed_plot); connect(ui.profTogglePicture, &QAction::triggered, tec, &qPrefTechnicalDetails::set_show_pictures_in_profile); connect(ui.profTankbar, &QAction::triggered, tec, &qPrefTechnicalDetails::set_tankbar); connect(ui.profTissues, &QAction::triggered, tec, &qPrefTechnicalDetails::set_percentagegraph); connect(ui.profTissues, &QAction::triggered, this, &MainWindow::unsetProfHR); connect(ui.profHR, &QAction::triggered, this, &MainWindow::unsetProfTissues); auto pp_gas = qPrefPartialPressureGas::instance(); connect(ui.profPhe, &QAction::triggered, pp_gas, &qPrefPartialPressureGas::set_phe); connect(ui.profPn2, &QAction::triggered, pp_gas, &qPrefPartialPressureGas::set_pn2); connect(ui.profPO2, &QAction::triggered, pp_gas, &qPrefPartialPressureGas::set_po2); connect(tec, &qPrefTechnicalDetails::calcalltissuesChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::calcceilingChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::dcceilingChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::eadChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::calcceiling3mChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::modChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::calcndlttsChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::hrgraphChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::rulergraphChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::show_sacChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::zoomed_plotChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::show_pictures_in_profileChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::tankbarChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(tec, &qPrefTechnicalDetails::percentagegraphChanged , graphics, &ProfileWidget2::actionRequestedReplot); connect(pp_gas, &qPrefPartialPressureGas::pheChanged, graphics, &ProfileWidget2::actionRequestedReplot); connect(pp_gas, &qPrefPartialPressureGas::pn2Changed, graphics, &ProfileWidget2::actionRequestedReplot); connect(pp_gas, &qPrefPartialPressureGas::po2Changed, graphics, &ProfileWidget2::actionRequestedReplot); // now let's set up some connections connect(graphics, &ProfileWidget2::enableToolbar ,this, &MainWindow::setEnabledToolbar); connect(graphics, &ProfileWidget2::disableShortcuts, this, &MainWindow::disableShortcuts); connect(graphics, &ProfileWidget2::enableShortcuts, this, &MainWindow::enableShortcuts); connect(graphics, &ProfileWidget2::editCurrentDive, this, &MainWindow::editCurrentDive); connect(graphics, &ProfileWidget2::updateDiveInfo, mainTab.get(), &MainTab::updateDiveInfo); connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), graphics, SLOT(settingsChanged())); ui.profCalcAllTissues->setChecked(qPrefTechnicalDetails::calcalltissues()); ui.profCalcCeiling->setChecked(qPrefTechnicalDetails::calcceiling()); ui.profDcCeiling->setChecked(qPrefTechnicalDetails::dcceiling()); ui.profEad->setChecked(qPrefTechnicalDetails::ead()); ui.profIncrement3m->setChecked(qPrefTechnicalDetails::calcceiling3m()); ui.profMod->setChecked(qPrefTechnicalDetails::mod()); ui.profNdl_tts->setChecked(qPrefTechnicalDetails::calcndltts()); ui.profDeco->setChecked(qPrefTechnicalDetails::decoinfo()); ui.profPhe->setChecked(pp_gas->phe()); ui.profPn2->setChecked(pp_gas->pn2()); ui.profPO2->setChecked(pp_gas->po2()); ui.profHR->setChecked(qPrefTechnicalDetails::hrgraph()); ui.profRuler->setChecked(qPrefTechnicalDetails::rulergraph()); ui.profSAC->setChecked(qPrefTechnicalDetails::show_sac()); ui.profTogglePicture->setChecked(qPrefTechnicalDetails::show_pictures_in_profile()); ui.profTankbar->setChecked(qPrefTechnicalDetails::tankbar()); ui.profTissues->setChecked(qPrefTechnicalDetails::percentagegraph()); ui.profScaled->setChecked(qPrefTechnicalDetails::zoomed_plot()); // full screen support is buggy on Windows and Ubuntu. // require the FULLSCREEN_SUPPORT macro to enable it! #ifndef FULLSCREEN_SUPPORT ui.actionFullScreen->setEnabled(false); ui.actionFullScreen->setVisible(false); setWindowState(windowState() & ~Qt::WindowFullScreen); #endif } MainWindow::~MainWindow() { write_hashes(); m_Instance = nullptr; } void MainWindow::setupSocialNetworkMenu() { } void MainWindow::editDiveSite(dive_site *ds) { if (!ds) return; diveSiteEdit->initFields(ds); setApplicationState(ApplicationState::EditDiveSite); } void MainWindow::startDiveSiteEdit() { if (current_dive) editDiveSite(get_dive_site_for_dive(current_dive)); } void MainWindow::enableDisableCloudActions() { ui.actionCloudstorageopen->setEnabled(prefs.cloud_verification_status == qPrefCloudStorage::CS_VERIFIED); ui.actionCloudstoragesave->setEnabled(prefs.cloud_verification_status == qPrefCloudStorage::CS_VERIFIED); } void MainWindow::enableDisableOtherDCsActions() { bool nr = number_of_computers(current_dive) > 1; ui.actionNextDC->setEnabled(nr); ui.actionPreviousDC->setEnabled(nr); } void MainWindow::setDefaultState() { setApplicationState(ApplicationState::Default); if (mainTab->isEditing()) ui.bottomLeft->currentWidget()->setEnabled(false); } MainWindow *MainWindow::instance() { return m_Instance; } // This gets called after one or more dives were added, edited or downloaded for a dive computer void MainWindow::refreshDisplay() { mainTab->reload(); setApplicationState(ApplicationState::Default); diveList->setEnabled(true); diveList->setFocus(); ui.actionAutoGroup->setChecked(autogroup); } void MainWindow::configureToolbar() { if (current_dive) { bool freeDiveMode = current_dive->dc.divemode == FREEDIVE; ui.profCalcCeiling->setDisabled(freeDiveMode); ui.profCalcCeiling->setDisabled(freeDiveMode); ui.profCalcAllTissues ->setDisabled(freeDiveMode); ui.profIncrement3m->setDisabled(freeDiveMode); ui.profDcCeiling->setDisabled(freeDiveMode); ui.profPhe->setDisabled(freeDiveMode); ui.profPn2->setDisabled(freeDiveMode); //TODO is the same as scuba? ui.profPO2->setDisabled(freeDiveMode); //TODO is the same as scuba? ui.profTankbar->setDisabled(freeDiveMode); ui.profMod->setDisabled(freeDiveMode); ui.profNdl_tts->setDisabled(freeDiveMode); ui.profDeco->setDisabled(freeDiveMode); ui.profEad->setDisabled(freeDiveMode); ui.profSAC->setDisabled(freeDiveMode); ui.profTissues->setDisabled(freeDiveMode); ui.profRuler->setDisabled(false); ui.profScaled->setDisabled(false); // measuring and scaling ui.profTogglePicture->setDisabled(false); ui.profHR->setDisabled(false); } } void MainWindow::selectionChanged() { mainTab->updateDiveInfo(); if (current_dive) { configureToolbar(); enableDisableOtherDCsActions(); } graphics->plotDive(current_dive, false); MapWidget::instance()->selectionChanged(); } void MainWindow::on_actionNew_triggered() { on_actionClose_triggered(); } static QString lastUsedDir() { QString lastDir = QDir::homePath(); if (QDir(qPrefDisplay::lastDir()).exists()) lastDir = qPrefDisplay::lastDir(); return lastDir; } 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_open()); 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() { mainTab->stealFocus(); // Make sure that any currently edited field is updated before saving. file_save(); } void MainWindow::on_actionSaveAs_triggered() { mainTab->stealFocus(); // Make sure that any currently edited field is updated before saving. 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)) return; if (verbose) qDebug() << "Opening cloud storage from:" << filename; closeCurrentFile(); showProgressBar(); QByteArray fileNamePtr = QFile::encodeName(filename); if (!parse_file(fileNamePtr.data(), &dive_table, &trip_table, &dive_site_table)) setCurrentFile(fileNamePtr.data()); process_loaded_dives(); hideProgressBar(); refreshDisplay(); } // Return whether saving to cloud is OK. If it isn't, show an error return false. static bool saveToCloudOK() { if (!dive_table.nr) { report_error(qPrintable(gettextFromC::tr("Don't save an empty log to the cloud"))); return false; } return true; } void MainWindow::on_actionCloudstoragesave_triggered() { QString filename; if (!saveToCloudOK()) return; if (getCloudURL(filename)) return; if (verbose) qDebug() << "Saving cloud storage to:" << filename; if (mainTab->isEditing()) mainTab->acceptChanges(); mainTab->stealFocus(); // Make sure that any currently edited field is updated before saving. showProgressBar(); int error = save_dives(qPrintable(filename)); hideProgressBar(); if (error) return; setCurrentFile(qPrintable(filename)); setFileClean(); } // Currently we have two markers for unsaved changes: // 1) unsaved_changes() returns true for non-undoable changes. // 2) Command::isClean() returns false for undoable changes. static bool unsavedChanges() { return unsaved_changes() || !Command::isClean(); } void MainWindow::on_actionCloudOnline_triggered() { bool isOffline = !ui.actionCloudOnline->isChecked(); if (isOffline == git_local_only) return; // Refuse to go online if there is an edit in progress if (!isOffline && (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || mainTab->isEditing())) { QMessageBox::warning(this, tr("Warning"), tr("Please save or cancel the current dive edit before going online")); // We didn't switch to online, therefore uncheck the checkbox ui.actionCloudOnline->setChecked(false); return; } git_local_only = isOffline; if (!isOffline) { // User requests to go online. Try to sync cloud storage if (unsavedChanges()) { // If there are unsaved changes, ask the user if they want to save them. // If they don't, they have to sync manually. if (QMessageBox::warning(this, tr("Save changes?"), tr("You have unsaved changes. Do you want to commit them to the cloud storage?\n" "If answering no, the cloud will only be synced on next call to " "\"Open cloud storage\" or \"Save to cloud storage\"."), QMessageBox::Yes|QMessageBox::No) == QMessageBox::Yes) on_actionCloudstoragesave_triggered(); } else { // If there are no unsaved changes, let's just try to load the remote cloud on_actionCloudstorageopen_triggered(); } if (git_local_only) report_error(qPrintable(tr("Failure taking cloud storage online"))); } setTitle(); updateCloudOnlineStatus(); } bool MainWindow::okToClose(QString message) { if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || mainTab->isEditing() ) { QMessageBox::warning(this, tr("Warning"), message); return false; } if (unsavedChanges() && askSaveChanges() == false) return false; return true; } void MainWindow::setFileClean() { mark_divelist_changed(false); Command::setClean(); } void MainWindow::closeCurrentFile() { /* free the dives and trips */ clear_git_id(); clear_dive_file_data(); // this clears all the core data structures and resets the models setCurrentFile(nullptr); diveList->setSortOrder(DiveTripModelBase::NR, Qt::DescendingOrder); MapWidget::instance()->reload(); if (!existing_filename) setTitle(); disableShortcuts(); setFileClean(); } void MainWindow::updateCloudOnlineStatus() { bool is_cloud = existing_filename && prefs.cloud_git_url && prefs.cloud_verification_status == qPrefCloudStorage::CS_VERIFIED && strstr(existing_filename, prefs.cloud_git_url); ui.actionCloudOnline->setEnabled(is_cloud); ui.actionCloudOnline->setChecked(is_cloud && !git_local_only); } void MainWindow::setCurrentFile(const char *f) { set_filename(f); setTitle(); updateCloudOnlineStatus(); } void MainWindow::on_actionClose_triggered() { if (okToClose(tr("Please save or cancel the current dive edit before closing the file."))) { closeCurrentFile(); setApplicationState(ApplicationState::Default); } } void MainWindow::updateLastUsedDir(const QString &dir) { qPrefDisplay::set_lastDir(dir); } void MainWindow::on_actionPrint_triggered() { #ifndef NO_PRINTING PrintDialog dlg(this); dlg.exec(); #endif } void MainWindow::disableShortcuts(bool disablePaste) { undoAction->setEnabled(false); redoAction->setEnabled(false); ui.actionPreviousDC->setShortcut(QKeySequence()); ui.actionNextDC->setShortcut(QKeySequence()); ui.copy->setShortcut(QKeySequence()); if (disablePaste) ui.paste->setShortcut(QKeySequence()); } void MainWindow::enableShortcuts() { undoAction->setEnabled(true); redoAction->setEnabled(true); 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(ApplicationState::Default); } void MainWindow::on_actionPreferences_triggered() { // the refreshPages() is currently done for just one // reason. Allow the user to define a default cylinder that // is not hardcoded but coming from the logbook. PreferencesDialog::instance()->refreshPages(); PreferencesDialog::instance()->show(); PreferencesDialog::instance()->raise(); } void MainWindow::on_actionQuit_triggered() { if (!okToClose(tr("Please save or cancel the current dive edit before quiting the application."))) return; writeSettings(); QApplication::quit(); } void MainWindow::on_actionDownloadDC_triggered() { DownloadFromDCWidget dlg(this); dlg.exec(); } void MainWindow::on_actionDivelogs_de_triggered() { DivelogsDeWebServices::instance()->downloadDives(); } void MainWindow::on_actionEditDeviceNames_triggered() { DiveComputerManagementDialog::instance()->init(); DiveComputerManagementDialog::instance()->show(); } bool MainWindow::plannerStateClean() { if (progressDialog) // we are accessing the cloud, so let's not switch into Add or Plan mode return false; if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || mainTab->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->plotDive(current_dive, true); } void MainWindow::planCanceled() { // while planning we might have modified the displayed_dive // let's refresh what's shown on the profile refreshProfile(); refreshDisplay(); } void MainWindow::planCreated() { // make sure our UI is in a consistent state showProfile(); setApplicationState(ApplicationState::Default); diveList->setEnabled(true); diveList->setFocus(); } void MainWindow::setPlanNotes(QString plan) { plannerDetails->divePlanOutput()->setHtml(plan); } void MainWindow::printPlan() { #ifndef NO_PRINTING char *disclaimer = get_planner_disclaimer_formatted(); // Prepend a logo and a disclaimer to the plan. // Save the old plan so that it can be restored at the end of the function. QString origPlan = plannerDetails->divePlanOutput()->toHtml(); QString diveplan = QStringLiteral(" ") + QString(disclaimer) + origPlan; free(disclaimer); QPrinter printer; QPrintDialog *dialog = new QPrintDialog(&printer, this); dialog->setWindowTitle(tr("Print runtime table")); if (dialog->exec() != QDialog::Accepted) return; /* render the profile as a pixmap that is inserted as base64 data into a HTML tag * make it fit a page width defined by 2 cm margins via QTextDocument->print() (cannot be changed?) * the height of the profile is 40% of the page height. */ QSizeF renderSize = printer.pageRect(QPrinter::Inch).size(); const qreal marginsInch = 1.57480315; // = (2 x 2cm) / 2.45cm/inch renderSize.setWidth((renderSize.width() - marginsInch) * printer.resolution()); renderSize.setHeight(((renderSize.height() - marginsInch) * printer.resolution()) / 2.5); QPixmap pixmap(renderSize.toSize()); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::SmoothPixmapTransform); ProfileWidget2 *profile = graphics; QSize origSize = profile->size(); profile->resize(renderSize.toSize()); profile->setPrintMode(true); profile->render(&painter); profile->resize(origSize); profile->setPrintMode(false); QByteArray byteArray; QBuffer buffer(&byteArray); pixmap.save(&buffer, "PNG"); QString profileImage = QString("

"; diveplan = profileImage + diveplan; plannerDetails->divePlanOutput()->setHtml(diveplan); plannerDetails->divePlanOutput()->print(&printer); plannerDetails->divePlanOutput()->setHtml(origPlan); // restore original plan #endif } void MainWindow::on_actionReplanDive_triggered() { if (!plannerStateClean() || !current_dive) return; else if (!is_dc_planner(¤t_dive->dc)) { 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(ApplicationState::PlanDive); divePlannerWidget->setReplanButton(true); divePlannerWidget->setupStartTime(timestampToDateTime(current_dive->when)); if (current_dive->surface_pressure.mbar) divePlannerWidget->setSurfacePressure(current_dive->surface_pressure.mbar); if (current_dive->salinity) divePlannerWidget->setSalinity(current_dive->salinity); DivePlannerPointsModel::instance()->loadFromDive(current_dive); reset_cylinders(&displayed_dive, true); DivePlannerPointsModel::instance()->cylindersModel()->updateDive(&displayed_dive); } void MainWindow::on_actionDivePlanner_triggered() { if (!plannerStateClean()) return; // put us in PLAN mode DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); setApplicationState(ApplicationState::PlanDive); graphics->setPlanState(); dc_number = 1; // create a simple starting dive, using the first gas from the just copied cylinders DivePlannerPointsModel::instance()->createSimpleDive(); // plan the dive in the same mode as the currently selected one if (current_dive) { divePlannerSettingsWidget->setDiveMode(current_dive->dc.divemode); divePlannerSettingsWidget->setBailoutVisibility(current_dive->dc.divemode); if (current_dive->salinity) divePlannerWidget->setSalinity(current_dive->salinity); } divePlannerWidget->setReplanButton(false); } void MainWindow::on_actionAddDive_triggered() { if (!plannerStateClean()) return; // create a dive an hour from now with a default depth (15m/45ft) and duration (40 minutes) // as a starting point for the user to edit struct dive d = { 0 }; d.id = dive_getUniqID(); d.when = QDateTime::currentMSecsSinceEpoch() / 1000L + gettimezoneoffset() + 3600; d.dc.duration.seconds = 40 * 60; d.dc.maxdepth.mm = M_OR_FT(15, 45); d.dc.meandepth.mm = M_OR_FT(13, 39); // this creates a resonable looking safety stop d.dc.model = strdup("manually added dive"); // don't translate! this is stored in the XML file fake_dc(&d.dc); fixup_dive(&d); Command::addDive(&d, autogroup, true); } void MainWindow::on_actionRenumber_triggered() { RenumberDialog dialog(false, this); dialog.exec(); } void MainWindow::on_actionAutoGroup_triggered() { set_autogroup(ui.actionAutoGroup->isChecked()); if (autogroup) Command::autogroupDives(); else Command::removeAutogenTrips(); } 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(lrint(width() * .8), height() / 2); d.move(lrint(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 | Qt::WindowMaximizeButtonHint); d.setWindowTitle(tr("Yearly statistics")); d.setWindowIcon(QIcon(":subsurface-icon")); d.setSizeGripEnabled(true); d.exec(); } void MainWindow::toggleCollapsible(bool toggle) { ui.mainSplitter->setCollapsible(0, toggle); ui.mainSplitter->setCollapsible(1, toggle); ui.topSplitter->setCollapsible(0, toggle); ui.topSplitter->setCollapsible(1, toggle); ui.bottomSplitter->setCollapsible(0, toggle); ui.bottomSplitter->setCollapsible(1, toggle); } void MainWindow::on_actionViewList_triggered() { toggleCollapsible(true); beginChangeState(LIST_MAXIMIZED); ui.mainSplitter->setSizes({ COLLAPSED, EXPANDED }); showFilterIfEnabled(); } void MainWindow::on_actionViewProfile_triggered() { toggleCollapsible(true); beginChangeState(PROFILE_MAXIMIZED); ui.topSplitter->setSizes({ COLLAPSED, EXPANDED }); ui.mainSplitter->setSizes({ EXPANDED, COLLAPSED }); } void MainWindow::on_actionViewInfo_triggered() { toggleCollapsible(true); beginChangeState(INFO_MAXIMIZED); ui.topSplitter->setSizes({ EXPANDED, COLLAPSED }); ui.mainSplitter->setSizes({ EXPANDED, COLLAPSED }); } void MainWindow::on_actionViewMap_triggered() { toggleCollapsible(true); beginChangeState(MAP_MAXIMIZED); ui.mainSplitter->setSizes({ COLLAPSED, EXPANDED }); ui.bottomSplitter->setSizes({ COLLAPSED, EXPANDED }); } void MainWindow::on_actionViewAll_triggered() { toggleCollapsible(false); beginChangeState(VIEWALL); const int appH = qApp->desktop()->size().height(); const int appW = qApp->desktop()->size().width(); QList mainSizes = { round_int(appH * 0.7), round_int(appH * 0.3) }; QList infoProfileSizes = { round_int(appW * 0.3), round_int(appW * 0.7) }; QList listGlobeSizes = { round_int(appW * 0.7), round_int(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); } void MainWindow::enterState(CurrentState newState) { state = newState; switch (state) { case VIEWALL: on_actionViewAll_triggered(); break; case MAP_MAXIMIZED: on_actionViewMap_triggered(); break; case INFO_MAXIMIZED: on_actionViewInfo_triggered(); break; case LIST_MAXIMIZED: on_actionViewList_triggered(); break; case PROFILE_MAXIMIZED: on_actionViewProfile_triggered(); break; } } 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(current_dive, false); mainTab->updateDiveInfo(); } void MainWindow::on_actionNextDC_triggered() { unsigned nrdc = number_of_computers(current_dive); dc_number = (dc_number + 1) % nrdc; configureToolbar(); graphics->plotDive(current_dive, false); mainTab->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(this); helpView->show(); #endif } void MainWindow::on_actionHash_images_triggered() { if(!findMovedImagesDialog) findMovedImagesDialog = new FindMovedImagesDialog(this); findMovedImagesDialog->show(); } QString MainWindow::filter_open() { QString f = tr("Dive log files") + " (*.ssrf" " *.xml" " *.can" " *.db" " *.sql" " *.dld" " *.jlb" " *.lvd" " *.sde" " *.udcf" " *.uddf" " *.dlf" " *.log" " *.txt" " *.apd" " *.dive" " *.zxu *.zxl" ");;"; f += tr("Subsurface files") + " (*.ssrf *.xml);;"; f += tr("Cochran") + " (*.can);;"; f += tr("DiveLogs.de") + " (*.dld);;"; f += tr("JDiveLog") + " (*.jlb);;"; f += tr("Liquivision") + " (*.lvd);;"; f += tr("Suunto") + " (*.sde *.db);;"; f += tr("UDCF") + " (*.udcf);;"; f += tr("UDDF") + " (*.uddf);;"; f += tr("XML") + " (*.xml);;"; f += tr("Divesoft") + " (*.dlf);;"; f += tr("Datatrak/WLog") + " (*.log);;"; f += tr("MkVI files") + " (*.txt);;"; f += tr("APD log viewer") + " (*.apd);;"; f += tr("OSTCtools") + " (*.dive);;"; f += tr("DAN DL7") + " (*.zxu *.zxl)"; return f; } QString MainWindow::filter_import() { QString f = tr("Dive log files") + " (*.ssrf" " *.xml" " *.can" " *.csv" " *.db" " *.sql" " *.dld" " *.jlb" " *.lvd" " *.sde" " *.udcf" " *.uddf" " *.dlf" " *.log" " *.txt" " *.apd" " *.dive" " *.zxu *.zxl" ");;"; f += tr("Subsurface files") + " (*.ssrf *.xml);;"; f += tr("Cochran") + " (*.can);;"; f += tr("CSV") + " (*.csv *.CSV);;"; f += tr("DiveLogs.de") + " (*.dld);;"; f += tr("JDiveLog") + " (*.jlb);;"; f += tr("Liquivision") + " (*.lvd);;"; f += tr("Suunto") + " (*.sde *.db);;"; f += tr("UDCF") + " (*.udcf);;"; f += tr("UDDF") + " (*.uddf);;"; f += tr("XML") + " (*.xml);;"; f += tr("Divesoft") + " (*.dlf);;"; f += tr("Datatrak/WLog") + " (*.log);;"; f += tr("MkVI files") + " (*.txt);;"; f += tr("APD log viewer") + " (*.apd);;"; f += tr("OSTCtools") + " (*.dive);;"; f += tr("DAN DL7") + " (*.zxu *.zxl);;"; f += tr("All files") + " (*.*)"; return f; } QString MainWindow::filter_import_dive_sites() { QString f = tr("Dive site files") + " (*.ssrf" " *.xml" ");;"; f += tr("All files") + " (*.*)"; return f; } bool MainWindow::askSaveChanges() { QMessageBox response(this); QString message = existing_filename ? tr("Do you want to save the changes that you made in the file %1?").arg(displayedFilename(existing_filename)) : 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()) { showMaximized(); } else { restoreGeometry(settings.value("geometry").toByteArray()); restoreState(settings.value("windowState", 0).toByteArray()); } enterState((CurrentState)settings.value("lastState", 0).toInt()); settings.endGroup(); show(); } void MainWindow::readSettings() { init_proxy(); // now make sure that the cloud menu items are enabled IFF cloud account is verified enableDisableCloudActions(); loadRecentFiles(); } #undef TOOLBOX_PREF_BUTTON 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 || mainTab->isEditing()) { on_actionQuit_triggered(); event->ignore(); return; } if (unsavedChanges() && (askSaveChanges() == false)) { event->ignore(); return; } event->accept(); writeSettings(); QApplication::closeAllWindows(); } void MainWindow::loadRecentFiles() { recentFiles.clear(); QSettings s; s.beginGroup("Recent_Files"); foreach (const QString &key, s.childKeys()) { // TODO Sorting only correct up to 9 entries. Currently, only 4 used, so no problem. if (!key.startsWith("File_")) continue; QString file = s.value(key).toString(); // never add our cloud URL to the recent files if (!same_string(prefs.cloud_git_url, "") && file.startsWith(prefs.cloud_git_url)) continue; // but allow local git repos QRegularExpression gitrepo("(.*)\\[[^]]+]"); QRegularExpressionMatch match = gitrepo.match(file); if (match.hasMatch()) { const QFileInfo gitDirectory(match.captured(1) + "/.git"); if ((gitDirectory.exists()) && (gitDirectory.isDir())) recentFiles.append(file); } else if (QFile::exists(file)) { recentFiles.append(file); } if (recentFiles.count() > NUM_RECENT_FILES) break; } s.endGroup(); updateRecentFilesMenu(); } void MainWindow::updateRecentFilesMenu() { for (int c = 0; c < NUM_RECENT_FILES; c++) { QAction *action = actionsRecent[c]; if (recentFiles.count() > c) { QFileInfo fi(recentFiles.at(c)); action->setText(fi.fileName()); action->setToolTip(fi.absoluteFilePath()); action->setVisible(true); } else { action->setVisible(false); } } } void MainWindow::addRecentFile(const QString &file, bool update) { // never add Subsurface cloud file to the recent files - it has its own menu entry if (!same_string(prefs.cloud_git_url, "") && file.startsWith(prefs.cloud_git_url)) return; QString localFile = QDir::toNativeSeparators(file); int index = recentFiles.indexOf(localFile); if (index >= 0) recentFiles.removeAt(index); recentFiles.prepend(localFile); while (recentFiles.count() > NUM_RECENT_FILES) recentFiles.removeLast(); if (update) updateRecentFiles(); } void MainWindow::updateRecentFiles() { QSettings s; s.beginGroup("Recent_Files"); s.remove(""); // Remove all old entries for (int c = 1; c <= recentFiles.count(); c++) { QString key = QString("File_%1").arg(c); s.setValue(key, recentFiles.at(c - 1)); } s.endGroup(); s.sync(); updateRecentFilesMenu(); } void MainWindow::recentFileTriggered(bool) { if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) return; int filenr = ((QAction *)sender())->data().toInt(); if (filenr >= recentFiles.count()) return; const QString &filename = recentFiles[filenr]; 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 = copy_qstring(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 files") + " (*.ssrf *.xml)"); selection_dialog.setAcceptMode(QFileDialog::AcceptSave); selection_dialog.setFileMode(QFileDialog::AnyFile); selection_dialog.setDefaultSuffix(""); if (empty_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 /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 (mainTab->isEditing()) mainTab->acceptChanges(); if (save_dives(qPrintable(filename))) return -1; setCurrentFile(qPrintable(filename)); setFileClean(); addRecentFile(filename, true); 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 (is_cloud && !saveToCloudOK()) return -1; if (mainTab->isEditing()) mainTab->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)) { if (is_cloud) hideProgressBar(); return -1; } if (is_cloud) hideProgressBar(); setFileClean(); addRecentFile(QString(existing_filename), true); return 0; } NotificationWidget *MainWindow::getNotificationWidget() { return ui.mainErrorMessage; } QString MainWindow::displayedFilename(QString fullFilename) { QFile f(fullFilename); QFileInfo fileInfo(f); QString fileName(fileInfo.fileName()); if (fullFilename.contains(prefs.cloud_git_url)) { QString email = fileName.left(fileName.indexOf('[')); return git_local_only ? tr("[local cache for] %1").arg(email) : tr("[cloud storage for] %1").arg(email); } else { return fileName; } } void MainWindow::setAutomaticTitle() { setTitle(); } void MainWindow::setTitle() { if (empty_string(existing_filename)) { setWindowTitle("Subsurface"); return; } QString unsaved = (unsavedChanges() ? " *" : ""); QString shown = QString(" (%1)").arg(filterWidget2.shownText()); setWindowTitle("Subsurface: " + displayedFilename(existing_filename) + unsaved + shown); } void MainWindow::importFiles(const QStringList fileNames) { if (fileNames.isEmpty()) return; QByteArray fileNamePtr; struct dive_table table = empty_dive_table; struct trip_table trips = empty_trip_table; struct dive_site_table sites = empty_dive_site_table; for (int i = 0; i < fileNames.size(); ++i) { fileNamePtr = QFile::encodeName(fileNames.at(i)); parse_file(fileNamePtr.data(), &table, &trips, &sites); } QString source = fileNames.size() == 1 ? fileNames[0] : tr("multiple files"); Command::importDives(&table, &trips, &sites, IMPORT_MERGE_ALL_TRIPS, source); } void MainWindow::loadFiles(const QStringList fileNames) { if (fileNames.isEmpty()) { refreshDisplay(); return; } QByteArray fileNamePtr; showProgressBar(); for (int i = 0; i < fileNames.size(); ++i) { fileNamePtr = QFile::encodeName(fileNames.at(i)); if (!parse_file(fileNamePtr.data(), &dive_table, &trip_table, &dive_site_table)) { setCurrentFile(fileNamePtr.data()); addRecentFile(fileNamePtr, false); } } hideProgressBar(); updateRecentFiles(); process_loaded_dives(); refreshDisplay(); 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.")); } } static const char *csvExtensions[] = { ".csv", ".apd", ".zxu", ".zxl", ".txt" }; static bool isCsvFile(const QString &s) { for (const char *ext: csvExtensions) { if (s.endsWith(ext, Qt::CaseInsensitive)) return true; } return false; } void MainWindow::on_actionImportDiveLog_triggered() { QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open dive log file"), lastUsedDir(), filter_import()); if (fileNames.isEmpty()) return; updateLastUsedDir(QFileInfo(fileNames[0]).dir().path()); QStringList logFiles; QStringList csvFiles; for (const QString &fn: fileNames) { if (isCsvFile(fn)) csvFiles.append(fn); else logFiles.append(fn); } if (logFiles.size()) { importFiles(logFiles); } if (csvFiles.size()) { DiveLogImportDialog diveLogImport(csvFiles, this); diveLogImport.exec(); } } void MainWindow::on_actionImportDiveSites_triggered() { QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open dive site file"), lastUsedDir(), filter_import_dive_sites()); if (fileNames.isEmpty()) return; updateLastUsedDir(QFileInfo(fileNames[0]).dir().path()); struct dive_table table = empty_dive_table; struct trip_table trips = empty_trip_table; struct dive_site_table sites = empty_dive_site_table; for (const QString &s: fileNames) { QByteArray fileNamePtr = QFile::encodeName(s); parse_file(fileNamePtr.data(), &table, &trips, &sites); } // The imported dive sites still have pointers to imported dives - remove them for (int i = 0; i < sites.nr; ++i) sites.dive_sites[i]->dives.nr = 0; // Now we can clear the imported dives and trips. clear_dive_table(&table); clear_trip_table(&trips); QString source = fileNames.size() == 1 ? fileNames[0] : tr("multiple files"); // sites table will be cleared by DivesiteImportDialog constructor DivesiteImportDialog divesiteImport(sites, source, this); divesiteImport.exec(); } void MainWindow::editCurrentDive() { // We only allow editing of the profile for manually added dives. if (!current_dive || !same_string(current_dive->dc.model, "manually added dive")) return; // This shouldn't be possible, but let's make sure no weird "double editing" takes place. if (mainTab->isEditing() || DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) return; disableShortcuts(); DivePlannerPointsModel::instance()->clear(); DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); graphics->setAddState(); setApplicationState(ApplicationState::EditDive); DivePlannerPointsModel::instance()->loadFromDive(current_dive); mainTab->enableEdition(); } void MainWindow::turnOffNdlTts() { qPrefTechnicalDetails::set_calcndltts(false); } #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() { Command::pasteDives(©PasteDive, what); } void MainWindow::on_actionFilterTags_triggered() { setApplicationState(getAppState() == ApplicationState::FilterDive ? ApplicationState::Default : ApplicationState::FilterDive); if (state == LIST_MAXIMIZED) showFilterIfEnabled(); } void MainWindow::showFilterIfEnabled() { if (getAppState() == ApplicationState::FilterDive) { const int appW = qApp->desktop()->size().width(); QList profileFilterSizes = { round_int(appW * 0.7), round_int(appW * 0.3) }; ui.bottomSplitter->setSizes(profileFilterSizes); } else { ui.bottomSplitter->setSizes({ EXPANDED, COLLAPSED }); } } void MainWindow::addWidgets(const Quadrant &q, QStackedWidget *stack) { if (q.widget && stack->indexOf(q.widget) == -1) stack->addWidget(q.widget); } void MainWindow::registerApplicationState(ApplicationState state, Quadrants q) { applicationState[(int)state] = q; addWidgets(q.topLeft, ui.topLeft); addWidgets(q.topRight, ui.topRight); addWidgets(q.bottomLeft, ui.bottomLeft); addWidgets(q.bottomRight, ui.bottomRight); } void MainWindow::setQuadrant(const Quadrant &q, QStackedWidget *stack) { if (q.widget) { stack->setCurrentWidget(q.widget); stack->show(); q.widget->setEnabled(!(q.flags & FLAG_DISABLED)); } else { stack->hide(); } } void MainWindow::setApplicationState(ApplicationState state) { if (getAppState() == state) return; setAppState(state); const Quadrants &quadrants = applicationState[(int)state]; setQuadrant(quadrants.topLeft, ui.topLeft); setQuadrant(quadrants.topRight, ui.topRight); setQuadrant(quadrants.bottomLeft, ui.bottomLeft); setQuadrant(quadrants.bottomRight, ui.bottomRight); } void MainWindow::showProgressBar() { delete progressDialog; progressDialog = new QProgressDialog(tr("Contacting cloud service..."), tr("Cancel"), 0, 100, this); progressDialog->setWindowModality(Qt::WindowModal); progressDialog->setMinimumDuration(0); progressDialogCanceled = false; progressCounter = 0; connect(progressDialog, SIGNAL(canceled()), this, SLOT(cancelCloudStorageOperation())); } void MainWindow::cancelCloudStorageOperation() { progressDialogCanceled = true; } void MainWindow::hideProgressBar() { if (progressDialog) { progressDialog->setValue(100); delete progressDialog; progressDialog = nullptr; } } void MainWindow::unsetProfHR() { ui.profHR->setChecked(false); qPrefTechnicalDetails::set_hrgraph(false); } void MainWindow::unsetProfTissues() { ui.profTissues->setChecked(false); qPrefTechnicalDetails::set_percentagegraph(false); } void MainWindow::divesChanged(const QVector &dives, DiveField field) { Q_UNUSED(field) for (struct dive *d: dives) { qDebug() << "dive #" << d->number << "changed, cache is" << (dive_cache_is_valid(d) ? "valid" : "invalidated"); // a brute force way to deal with that would of course be to call // invalidate_dive_cache(d); } }