From fce48367cdf9b2adebf38f81b43df882541bfeef Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Sat, 19 Feb 2022 11:58:36 +0100 Subject: [PATCH] undo: more fine-grained undo of profile editing Place undo commands for every change of the profile, not only on "saving". Move the edit-mode from the mainwindow and the maintab to the profile widget. This is still very rough. For example, the only way to exit the edit mode is changing the current dive. The undo-commands are placed by the desktop-profile widget. We might think about moving that down to the profile-view so that this will be useable on mobile. Signed-off-by: Berthold Stoeger --- CHANGELOG.md | 1 + commands/command_base.cpp | 1 - desktop-widgets/mainwindow.cpp | 23 ------ desktop-widgets/mainwindow.h | 2 - desktop-widgets/profilewidget.cpp | 86 +++++++++++++++++++--- desktop-widgets/profilewidget.h | 17 ++++- desktop-widgets/tab-widgets/maintab.cpp | 15 ---- desktop-widgets/tab-widgets/maintab.h | 1 - profile-widget/profilewidget2.cpp | 95 ++++++++++++++----------- profile-widget/profilewidget2.h | 4 ++ qt-models/diveplannermodel.cpp | 12 ++-- qt-models/diveplannermodel.h | 5 +- 12 files changed, 162 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d049b8066..d9192817c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- profile: include profile editing in undo system - core: avoid crash with corrupted cloud storage - mobile: fix profile scaling issue on high DPR devices - mobile/Android: add logfiles as attachment to support emails diff --git a/commands/command_base.cpp b/commands/command_base.cpp index ad571089f..fe4cf7840 100644 --- a/commands/command_base.cpp +++ b/commands/command_base.cpp @@ -80,7 +80,6 @@ QString getListOfDives(QVector dives) return getListOfDives(std::vector(dives.begin(), dives.end())); } - // return a string that can be used for the commit message and should list the changes that // were made to the dive list QString changesMade() diff --git a/desktop-widgets/mainwindow.cpp b/desktop-widgets/mainwindow.cpp index e5497f44c..63564b71a 100644 --- a/desktop-widgets/mainwindow.cpp +++ b/desktop-widgets/mainwindow.cpp @@ -145,8 +145,6 @@ MainWindow::MainWindow() : QMainWindow(), registerApplicationState(ApplicationState::Default, { true, { mainTab.get(), FLAG_NONE }, { profile.get(), FLAG_NONE }, { diveList.get(), FLAG_NONE }, { mapWidget.get(), FLAG_NONE } }); - registerApplicationState(ApplicationState::EditDive, { false, { mainTab.get(), FLAG_NONE }, { profile.get(), FLAG_NONE }, - { diveList.get(), FLAG_NONE }, { mapWidget.get(), FLAG_NONE } }); registerApplicationState(ApplicationState::PlanDive, { false, { &plannerWidgets->plannerWidget, FLAG_NONE }, { profile.get(), FLAG_NONE }, { &plannerWidgets->plannerSettingsWidget, FLAG_NONE }, { &plannerWidgets->plannerDetails, FLAG_NONE } }); registerApplicationState(ApplicationState::EditPlannedDive, { true, { &plannerWidgets->plannerWidget, FLAG_NONE }, { profile.get(), FLAG_NONE }, @@ -246,8 +244,6 @@ MainWindow::MainWindow() : QMainWindow(), set_git_update_cb(&updateProgress); set_error_cb(&showErrorFromC); - connect(profile->view.get(), &ProfileWidget2::editCurrentDive, this, &MainWindow::editCurrentDive); - // full screen support is buggy on Windows and Ubuntu. // require the FULLSCREEN_SUPPORT macro to enable it! #ifndef FULLSCREEN_SUPPORT @@ -1408,25 +1404,6 @@ void MainWindow::on_actionImportDiveSites_triggered() 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") && current_dive->dc.samples) || !userMayChangeAppState()) - 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(false); - copy_dive(current_dive, &displayed_dive); // Work on a copy of the dive - DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); - DivePlannerPointsModel::instance()->loadFromDive(&displayed_dive); - profile->setEditState(&displayed_dive, 0); - setApplicationState(ApplicationState::EditDive); - mainTab->enableEdition(); -} - void MainWindow::on_actionExport_triggered() { DiveLogExportDialog diveLogExport; diff --git a/desktop-widgets/mainwindow.h b/desktop-widgets/mainwindow.h index 91673f814..7cfd4a7ac 100644 --- a/desktop-widgets/mainwindow.h +++ b/desktop-widgets/mainwindow.h @@ -54,7 +54,6 @@ public: enum class ApplicationState { Default, - EditDive, PlanDive, EditPlannedDive, EditDiveSite, @@ -155,7 +154,6 @@ slots: void refreshDisplay(); void showProfile(); void refreshProfile(); - void editCurrentDive(); void planCanceled(); void planCreated(); // Some shortcuts like "change DC" or "copy/paste dive components" diff --git a/desktop-widgets/profilewidget.cpp b/desktop-widgets/profilewidget.cpp index b701d00d0..5c916802d 100644 --- a/desktop-widgets/profilewidget.cpp +++ b/desktop-widgets/profilewidget.cpp @@ -2,9 +2,11 @@ #include "profilewidget.h" #include "profile-widget/profilewidget2.h" +#include "commands/command.h" #include "core/color.h" #include "core/settings/qPrefTechnicalDetails.h" #include "core/settings/qPrefPartialPressureGas.h" +#include "core/subsurface-string.h" #include "qt-models/diveplannermodel.h" #include @@ -49,7 +51,7 @@ void EmptyView::resizeEvent(QResizeEvent *) update(); } -ProfileWidget::ProfileWidget() +ProfileWidget::ProfileWidget() : originalDive(nullptr) { ui.setupUi(this); @@ -116,6 +118,10 @@ ProfileWidget::ProfileWidget() connect(ui.profPO2, &QAction::triggered, pp_gas, &qPrefPartialPressureGas::set_po2); connect(&diveListNotifier, &DiveListNotifier::settingsChanged, view.get(), &ProfileWidget2::settingsChanged); + connect(view.get(), &ProfileWidget2::editCurrentDive, this, &ProfileWidget::editDive); + connect(view.get(), &ProfileWidget2::stopAdded, this, &ProfileWidget::stopAdded); + connect(view.get(), &ProfileWidget2::stopRemoved, this, &ProfileWidget::stopRemoved); + connect(view.get(), &ProfileWidget2::stopMoved, this, &ProfileWidget::stopMoved); ui.profCalcAllTissues->setChecked(qPrefTechnicalDetails::calcalltissues()); ui.profCalcCeiling->setChecked(qPrefTechnicalDetails::calcceiling()); @@ -149,6 +155,7 @@ void ProfileWidget::setEnabledToolbar(bool enabled) void ProfileWidget::setDive(const struct dive *d) { + // If the user was currently editing a dive, exit edit mode. stack->setCurrentIndex(1); // show profile bool freeDiveMode = d->dc.divemode == FREEDIVE; @@ -176,8 +183,14 @@ void ProfileWidget::setDive(const struct dive *d) void ProfileWidget::plotCurrentDive() { + // Exit edit mode if the dive changed + if (editedDive && originalDive != current_dive) + exitEditMode(); + setEnabledToolbar(current_dive != nullptr); - if (current_dive) { + if (editedDive) { + view->plotDive(editedDive.get(), editedDc); + } else if (current_dive) { setDive(current_dive); view->setProfileState(current_dive, dc_number); view->resetZoom(); // when switching dive, reset the zoomLevel @@ -190,14 +203,9 @@ void ProfileWidget::plotCurrentDive() void ProfileWidget::setPlanState(const struct dive *d, int dc) { - setDive(d); // show subsurface logo - view->setPlanState(d, dc); -} - -void ProfileWidget::setEditState(const struct dive *d, int dc) -{ + exitEditMode(); setDive(d); - view->setEditState(d, dc); + view->setPlanState(d, dc); } void ProfileWidget::unsetProfHR() @@ -211,3 +219,63 @@ void ProfileWidget::unsetProfTissues() ui.profTissues->setChecked(false); qPrefTechnicalDetails::set_percentagegraph(false); } + +void ProfileWidget::editDive() +{ + // We only allow editing of the profile for manually added dives + // and when no other editing is in progress. + if (!current_dive || + (!same_string(current_dive->dc.model, "manually added dive") && current_dive->dc.samples) || + (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) || + editedDive) + return; + + editedDive.reset(alloc_dive()); + editedDc = dc_number; + copy_dive(current_dive, editedDive.get()); // Work on a copy of the dive + originalDive = current_dive; + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + DivePlannerPointsModel::instance()->loadFromDive(editedDive.get()); + view->setEditState(editedDive.get(), 0); +} + +void ProfileWidget::exitEditMode() +{ + if (!editedDive) + return; + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING); + view->setProfileState(current_dive, dc_number); // switch back to original dive before erasing the copy. + editedDive.reset(); + originalDive = nullptr; +} + +// Update depths of edited dive +static void calcDepth(dive &d, int dcNr) +{ + d.maxdepth.mm = get_dive_dc(&d, dcNr)->maxdepth.mm = 0; + fixup_dive(&d); +} + +void ProfileWidget::stopAdded() +{ + if (!editedDive) + return; + calcDepth(*editedDive, editedDc); + Command::editProfile(editedDive.get(), Command::EditProfileType::ADD, 0); +} + +void ProfileWidget::stopRemoved(int count) +{ + if (!editedDive) + return; + calcDepth(*editedDive, editedDc); + Command::editProfile(editedDive.get(), Command::EditProfileType::REMOVE, count); +} + +void ProfileWidget::stopMoved(int count) +{ + if (!editedDive) + return; + calcDepth(*editedDive, editedDc); + Command::editProfile(editedDive.get(), Command::EditProfileType::MOVE, count); +} diff --git a/desktop-widgets/profilewidget.h b/desktop-widgets/profilewidget.h index 467ade1a9..5f30cd99a 100644 --- a/desktop-widgets/profilewidget.h +++ b/desktop-widgets/profilewidget.h @@ -9,10 +9,13 @@ #include #include +struct dive; class ProfileWidget2; class EmptyView; class QStackedWidget; +extern "C" void free_dive(struct dive *); + class ProfileWidget : public QWidget { Q_OBJECT public: @@ -21,18 +24,30 @@ public: std::unique_ptr view; void plotCurrentDive(); void setPlanState(const struct dive *d, int dc); - void setEditState(const struct dive *d, int dc); void setEnabledToolbar(bool enabled); private slots: void unsetProfHR(); void unsetProfTissues(); + void stopAdded(); + void stopRemoved(int count); + void stopMoved(int count); private: + // The same code is in command/command_base.h. Should we make that a global feature? + struct DiveDeleter { + void operator()(dive *d) { free_dive(d); } + }; + std::unique_ptr emptyView; std::vector toolbarActions; Ui::ProfileWidget ui; QStackedWidget *stack; void setDive(const struct dive *d); + void editDive(); + void exitEditMode(); + std::unique_ptr editedDive; + int editedDc; + dive *originalDive; }; #endif // PROFILEWIDGET_H diff --git a/desktop-widgets/tab-widgets/maintab.cpp b/desktop-widgets/tab-widgets/maintab.cpp index bf9deae26..cd2a5500d 100644 --- a/desktop-widgets/tab-widgets/maintab.cpp +++ b/desktop-widgets/tab-widgets/maintab.cpp @@ -221,21 +221,6 @@ void MainTab::displayMessage(QString str) ui.diveNotesMessage->animatedShow(); } -void MainTab::enableEdition() -{ - if (current_dive == NULL || editMode) - return; - - ui.editDiveSiteButton->setEnabled(false); - MainWindow::instance()->diveList->setEnabled(false); - MainWindow::instance()->setEnabledToolbar(false); - - ui.dateEdit->setEnabled(true); - displayMessage(tr("This dive is being edited.")); - - editMode = true; -} - // This function gets called if a field gets updated by an undo command. // Refresh the corresponding UI field. void MainTab::divesChanged(const QVector &dives, DiveField field) diff --git a/desktop-widgets/tab-widgets/maintab.h b/desktop-widgets/tab-widgets/maintab.h index 7af843000..f5ef32760 100644 --- a/desktop-widgets/tab-widgets/maintab.h +++ b/desktop-widgets/tab-widgets/maintab.h @@ -57,7 +57,6 @@ slots: void closeMessage(); void closeWarning(); void displayMessage(QString str); - void enableEdition(); void escDetected(void); void updateDateTimeFields(); void colorsChanged(); diff --git a/profile-widget/profilewidget2.cpp b/profile-widget/profilewidget2.cpp index 33991a7d5..b84d8176f 100644 --- a/profile-widget/profilewidget2.cpp +++ b/profile-widget/profilewidget2.cpp @@ -283,6 +283,8 @@ void ProfileWidget2::divePlannerHandlerReleased() { if (zoomLevel) return; + if (currentState == EDIT) + emit stopMoved(1); shouldCalculateMax = true; replot(); } @@ -334,6 +336,8 @@ void ProfileWidget2::mouseDoubleClickEvent(QMouseEvent *event) int minutes = lrint(profileScene->timeAxis->valueAt(mappedPos) / 60); int milimeters = lrint(profileScene->profileYAxis->valueAt(mappedPos) / M_OR_FT(1, 1)) * M_OR_FT(1, 1); plannerModel->addStop(milimeters, minutes * 60); + if (currentState == EDIT) + emit stopAdded(); } } @@ -919,20 +923,31 @@ void ProfileWidget2::divePlannerHandlerMoved() plannerModel->editStop(index, data); } +std::vector ProfileWidget2::selectedDiveHandleIndices() const +{ + std::vector res; + res.reserve(scene()->selectedItems().size()); + for (QGraphicsItem *item: scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast(item)) + res.push_back(handleIndex(handler)); + } + return res; +} + void ProfileWidget2::keyDownAction() { if ((currentState != EDIT && currentState != PLAN) || !plannerModel) return; - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - int row = handleIndex(handler); - divedatapoint dp = plannerModel->at(row); + std::vector handleIndices = selectedDiveHandleIndices(); + for (int row: handleIndices) { + divedatapoint dp = plannerModel->at(row); - dp.depth.mm += M_OR_FT(1, 5); - plannerModel->editStop(row, dp); - } + dp.depth.mm += M_OR_FT(1, 5); + plannerModel->editStop(row, dp); } + if (currentState == EDIT && !handleIndices.empty()) + emit stopMoved(handleIndices.size()); // TODO: Accumulate key moves } void ProfileWidget2::keyUpAction() @@ -940,18 +955,18 @@ void ProfileWidget2::keyUpAction() if ((currentState != EDIT && currentState != PLAN) || !plannerModel) return; - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - int row = handleIndex(handler); - divedatapoint dp = plannerModel->at(row); + std::vector handleIndices = selectedDiveHandleIndices(); + for (int row: handleIndices) { + divedatapoint dp = plannerModel->at(row); - if (dp.depth.mm <= 0) - continue; + if (dp.depth.mm <= 0) + continue; - dp.depth.mm -= M_OR_FT(1, 5); - plannerModel->editStop(row, dp); - } + dp.depth.mm -= M_OR_FT(1, 5); + plannerModel->editStop(row, dp); } + if (currentState == EDIT && !handleIndices.empty()) + emit stopMoved(handleIndices.size()); // TODO: Accumulate key moves } void ProfileWidget2::keyLeftAction() @@ -959,18 +974,18 @@ void ProfileWidget2::keyLeftAction() if ((currentState != EDIT && currentState != PLAN) || !plannerModel) return; - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - int row = handleIndex(handler); - divedatapoint dp = plannerModel->at(row); + std::vector handleIndices = selectedDiveHandleIndices(); + for (int row: handleIndices) { + divedatapoint dp = plannerModel->at(row); - if (dp.time / 60 <= 0) - continue; + if (dp.time / 60 <= 0) + continue; - dp.time -= 60; - plannerModel->editStop(row, dp); - } + dp.time -= 60; + plannerModel->editStop(row, dp); } + if (currentState == EDIT && !handleIndices.empty()) + emit stopMoved(handleIndices.size()); // TODO: Accumulate key moves } void ProfileWidget2::keyRightAction() @@ -978,15 +993,15 @@ void ProfileWidget2::keyRightAction() if ((currentState != EDIT && currentState != PLAN) || !plannerModel) return; - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - int row = handleIndex(handler); - divedatapoint dp = plannerModel->at(row); + std::vector handleIndices = selectedDiveHandleIndices(); + for (int row: handleIndices) { + divedatapoint dp = plannerModel->at(row); - dp.time += 60; - plannerModel->editStop(row, dp); - } + dp.time += 60; + plannerModel->editStop(row, dp); } + if (currentState == EDIT && !handleIndices.empty()) + emit stopMoved(handleIndices.size()); // TODO: Accumulate key moves } void ProfileWidget2::keyDeleteAction() @@ -994,15 +1009,15 @@ void ProfileWidget2::keyDeleteAction() if ((currentState != EDIT && currentState != PLAN) || !plannerModel) return; - QVector selectedIndices; - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - selectedIndices.push_back(handleIndex(handler)); - handler->hide(); - } + std::vector handleIndices = selectedDiveHandleIndices(); + // For now, we have to convert to QVector. + for (int index: handleIndices) + handles[index]->hide(); + if (!handleIndices.empty()) { + plannerModel->removeSelectedPoints(handleIndices); + if (currentState == EDIT) + emit stopRemoved(handleIndices.size()); } - if (!selectedIndices.isEmpty()) - plannerModel->removeSelectedPoints(selectedIndices); } void ProfileWidget2::clearPictures() diff --git a/profile-widget/profilewidget2.h b/profile-widget/profilewidget2.h index 301976b0c..c613063ee 100644 --- a/profile-widget/profilewidget2.h +++ b/profile-widget/profilewidget2.h @@ -66,6 +66,9 @@ public: signals: void editCurrentDive(); + void stopAdded(); // only emitted in edit mode + void stopRemoved(int count); // only emitted in edit mode + void stopMoved(int count); // only emitted in edit mode public slots: // Necessary to call from QAction's signals. @@ -186,6 +189,7 @@ private: #endif friend class DiveHandler; bool shouldCalculateMax; // Calculate maximum time and depth (default). False when dragging handles. + std::vector selectedDiveHandleIndices() const; // We store a const pointer to the shown dive. However, the undo commands want // (understandably) a non-const pointer. Since the profile has a context-menu diff --git a/qt-models/diveplannermodel.cpp b/qt-models/diveplannermodel.cpp index 45beecdf3..5b2db708f 100644 --- a/qt-models/diveplannermodel.cpp +++ b/qt-models/diveplannermodel.cpp @@ -28,21 +28,21 @@ CylindersModel *DivePlannerPointsModel::cylindersModel() return &cylinders; } -void DivePlannerPointsModel::removePoints(const QVector &rows) +void DivePlannerPointsModel::removePoints(const std::vector &rows) { - if (!rows.count()) + if (rows.empty()) return; - QVector v2 = rows; + std::vector v2 = rows; std::sort(v2.begin(), v2.end()); - for (int i = v2.count() - 1; i >= 0; i--) { + for (int i = (int)v2.size() - 1; i >= 0; i--) { beginRemoveRows(QModelIndex(), v2[i], v2[i]); divepoints.erase(divepoints.begin() + v2[i]); endRemoveRows(); } } -void DivePlannerPointsModel::removeSelectedPoints(const QVector &rows) +void DivePlannerPointsModel::removeSelectedPoints(const std::vector &rows) { removePoints(rows); @@ -230,7 +230,7 @@ bool DivePlannerPointsModel::updateMaxDepth() void DivePlannerPointsModel::removeDeco() { - QVector computedPoints; + std::vector computedPoints; for (int i = 0; i < rowCount(); i++) { if (!at(i).entered) computedPoints.push_back(i); diff --git a/qt-models/diveplannermodel.h b/qt-models/diveplannermodel.h index 2a01c2d42..59f3c1583 100644 --- a/qt-models/diveplannermodel.h +++ b/qt-models/diveplannermodel.h @@ -4,6 +4,7 @@ #include #include +#include #include "core/deco.h" #include "core/planner.h" @@ -36,7 +37,7 @@ public: Qt::ItemFlags flags(const QModelIndex &index) const override; void gasChange(const QModelIndex &index, int newcylinderid); void cylinderRenumber(int mapping[]); - void removeSelectedPoints(const QVector &rows); + void removeSelectedPoints(const std::vector &rows); void setPlanMode(Mode mode); bool isPlanner() const; void createSimpleDive(struct dive *d); @@ -116,7 +117,7 @@ private: explicit DivePlannerPointsModel(QObject *parent = 0); void clear(); int addStop(int millimeters, int seconds, int cylinderid_in, int ccpoint, bool entered, enum divemode_t); - void removePoints(const QVector &rows); + void removePoints(const std::vector &rows); void setupStartTime(); void setupCylinders(); int lastEnteredPoint() const;