diff options
author | Berthold Stoeger <bstoeger@mail.tuwien.ac.at> | 2018-07-19 22:35:25 +0200 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2018-10-11 16:22:27 -0700 |
commit | 12df9faaa2037b5155ebb84a7f6f6102491a0091 (patch) | |
tree | 33dcf0e400f669d088f1b20652e9c0c6f69aae2b | |
parent | 61467ea0d59b04f141a68452ee16c70760421d72 (diff) | |
download | subsurface-12df9faaa2037b5155ebb84a7f6f6102491a0091.tar.gz |
Undo: implement undo of manual dive-creation
Play manual addition of dives via an UndoCommand. Since this does in
large parts the same thing as undo/redo of dive deletion (just the
other way round and only a single instead of multiple dive), factor
out the functions that add/delete dives and take care of trips.
The UI-interaction is just mindless copy&paste and will have to
be adapted.
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
-rw-r--r-- | core/dive.h | 1 | ||||
-rw-r--r-- | core/divelist.c | 44 | ||||
-rw-r--r-- | core/divelist.h | 1 | ||||
-rw-r--r-- | desktop-widgets/command_divelist.cpp | 904 | ||||
-rw-r--r-- | desktop-widgets/tab-widgets/maintab.cpp | 43 | ||||
-rw-r--r-- | desktop-widgets/undocommands.cpp | 106 | ||||
-rw-r--r-- | desktop-widgets/undocommands.h | 29 |
7 files changed, 1064 insertions, 64 deletions
diff --git a/core/dive.h b/core/dive.h index f80412a03..2d3edbc5c 100644 --- a/core/dive.h +++ b/core/dive.h @@ -418,6 +418,7 @@ extern void add_dive_to_trip(struct dive *, dive_trip_t *); struct dive *unregister_dive(int idx); extern void delete_single_dive(int idx); +extern int dive_get_insertion_index(struct dive *dive); extern void add_single_dive(int idx, struct dive *dive); extern void insert_trip(dive_trip_t **trip); diff --git a/core/divelist.c b/core/divelist.c index 07c9ea87d..a25927e90 100644 --- a/core/divelist.c +++ b/core/divelist.c @@ -987,25 +987,29 @@ struct dive **grow_dive_table(struct dive_table *table) return dives; } +/* get the index where we want to insert the dive so that everything stays + * ordered reverse-chronologically */ +int dive_get_insertion_index(struct dive *dive) +{ + for (int i = 0; i < dive_table.nr; i++) { + if (dive->when <= dive_table.dives[i]->when) + return i; + } + return dive_table.nr; +} + +/* add a dive at the given index. if the index is negative, the dive will + * be added according to reverse chronological order */ void add_single_dive(int idx, struct dive *dive) { int i; + if (idx < 0) + idx = dive_get_insertion_index(dive); grow_dive_table(&dive_table); dive_table.nr++; if (dive->selected) amount_selected++; - if (idx < 0) { - // convert an idx of -1 so we do insert-in-chronological-order - idx = dive_table.nr - 1; - for (int i = 0; i < dive_table.nr - 1; i++) { - if (dive->when <= dive_table.dives[i]->when) { - idx = i; - break; - } - } - } - for (i = idx; i < dive_table.nr; i++) { struct dive *tmp = dive_table.dives[i]; dive_table.dives[i] = dive; @@ -1453,6 +1457,24 @@ void process_imported_dives(struct dive_table *import_table, bool prefer_importe mark_divelist_changed(true); } +/* return the number a dive gets when inserted at the given index. + * this function is supposed to be called *before* a dive was added. + * this returns: + * - 1 for an empty log + * - last_nr+1 for addition at end of log (if last dive had a number) + * - 0 for all other cases + */ +int get_dive_nr_at_idx(int idx) +{ + if (dive_table.nr == 0) + return 1; + if (idx >= dive_table.nr) { + struct dive *last_dive = get_dive(dive_table.nr - 1); + return last_dive->number ? last_dive->number + 1 : 0; + } + return 0; +} + void set_dive_nr_for_current_dive() { if (dive_table.nr == 1) diff --git a/core/divelist.h b/core/divelist.h index 3687b0881..2b2ee07a5 100644 --- a/core/divelist.h +++ b/core/divelist.h @@ -42,6 +42,7 @@ extern void find_new_trip_start_time(dive_trip_t *trip); extern struct dive *first_selected_dive(); extern struct dive *last_selected_dive(); extern bool is_trip_before_after(const struct dive *dive, bool before); +extern int get_dive_nr_at_idx(int idx); extern void set_dive_nr_for_current_dive(); extern timestamp_t get_surface_interval(timestamp_t when); extern void delete_dive_from_table(struct dive_table *table, int idx); diff --git a/desktop-widgets/command_divelist.cpp b/desktop-widgets/command_divelist.cpp new file mode 100644 index 000000000..6f638b2a6 --- /dev/null +++ b/desktop-widgets/command_divelist.cpp @@ -0,0 +1,904 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include "command_divelist.h" +#include "desktop-widgets/mainwindow.h" +#include "desktop-widgets/divelistview.h" +#include "core/divelist.h" +#include "core/display.h" // for amount_selected +#include "core/subsurface-qt/DiveListNotifier.h" +#include "qt-models/filtermodels.h" + +namespace Command { + +// Generally, signals are sent in batches per trip. To avoid writing the same loop +// again and again, this template takes a vector of trip / dive pairs, sorts it +// by trip and then calls a function-object with trip and a QVector of dives in that trip. +// Input parameters: +// - dives: a vector of trip,dive pairs, which will be sorted and processed in batches by trip. +// - action: a function object, taking a trip-pointer and a QVector of dives, which will be called for each batch. +template<typename Function> +void processByTrip(std::vector<std::pair<dive_trip *, dive *>> &dives, Function action) +{ + // Use std::tie for lexicographical sorting of trip, then start-time + std::sort(dives.begin(), dives.end(), + [](const std::pair<dive_trip *, dive *> &e1, const std::pair<dive_trip *, dive *> &e2) + { return std::tie(e1.first, e1.second->when) < std::tie(e2.first, e2.second->when); }); + + // Then, process the dives in batches by trip + size_t i, j; // Begin and end of batch + for (i = 0; i < dives.size(); i = j) { + dive_trip *trip = dives[i].first; + for (j = i + 1; j < dives.size() && dives[j].first == trip; ++j) + ; // pass + // Copy dives into a QVector. Some sort of "range_view" would be ideal, but Qt doesn't work this way. + QVector<dive *> divesInTrip(j - i); + for (size_t k = i; k < j; ++k) + divesInTrip[k - i] = dives[k].second; + + // Finally, emit the signal + action(trip, divesInTrip); + } +} + + +// This helper function removes a dive, takes ownership of the dive and adds it to a DiveToAdd structure. +// It is crucial that dives are added in reverse order of deletion, so the the indices are correctly +// set and that the trips are added before they are used! +DiveToAdd DiveListBase::removeDive(struct dive *d) +{ + // If the dive to be removed is selected, we will inform the frontend + // later via a signal that the dive changed. + if (d->selected) + selectionChanged = true; + + // If the dive was the current dive, reset the current dive. The calling + // command is responsible of finding a new dive. + if (d == current_dive) { + selectionChanged = true; // Should have been set above, as current dive is always selected. + current_dive = nullptr; + } + + DiveToAdd res; + res.idx = get_divenr(d); + if (res.idx < 0) + qWarning() << "Deletion of unknown dive!"; + + // remove dive from trip - if this is the last dive in the trip + // remove the whole trip. + res.trip = unregister_dive_from_trip(d, false); + if (res.trip && res.trip->nrdives == 0) { + unregister_trip(res.trip); // Remove trip from backend + res.tripToAdd.reset(res.trip); // Take ownership of trip + } + + res.dive.reset(unregister_dive(res.idx)); // Remove dive from backend + + return res; +} + +// This helper function adds a dive and returns ownership to the backend. It may also add a dive trip. +// It is crucial that dives are added in reverse order of deletion (see comment above)! +// Returns pointer to added dive (which is owned by the backend!) +dive *DiveListBase::addDive(DiveToAdd &d) +{ + if (d.tripToAdd) + insert_trip_dont_merge(d.tripToAdd.release()); // Return ownership to backend + if (d.trip) + add_dive_to_trip(d.dive.get(), d.trip); + dive *res = d.dive.release(); // Give up ownership of dive + + // Set the filter flag according to current filter settings + bool show = MultiFilterSortModel::instance()->showDive(res); + res->hidden_by_filter = !show; + + add_single_dive(d.idx, res); // Return ownership to backend + + // If the dive to be removed is selected, we will inform the frontend + // later via a signal that the dive changed. + if (res->selected) + selectionChanged = true; + + return res; +} + +// This helper function calls removeDive() on a list of dives to be removed and +// returns a vector of corresponding DiveToAdd objects, which can later be readded. +// The passed in vector is cleared. +std::vector<DiveToAdd> DiveListBase::removeDives(std::vector<dive *> &divesToDelete) +{ + std::vector<DiveToAdd> res; + res.reserve(divesToDelete.size()); + + // First, tell the filters that dives are removed. This could + // be done later using the emitted signals, but we do this here + // for symmetry with addDives() + MultiFilterSortModel::instance()->divesDeleted(QVector<dive *>::fromStdVector(divesToDelete)); + + for (dive *d: divesToDelete) + res.push_back(removeDive(d)); + divesToDelete.clear(); + + // We send one dives-deleted signal per trip (see comments in DiveListNotifier.h). + // Therefore, collect all dives in an array and sort by trip. + std::vector<std::pair<dive_trip *, dive *>> dives; + dives.reserve(res.size()); + for (const DiveToAdd &entry: res) + dives.push_back({ entry.trip, entry.dive.get() }); + + // Send signals. + processByTrip(dives, [&](dive_trip *trip, const QVector<dive *> &divesInTrip) { + // Now, let's check if this trip is supposed to be deleted, by checking if it was marked + // as "add it". We could be smarter here, but let's just check the whole array for brevity. + bool deleteTrip = trip && + std::find_if(res.begin(), res.end(), [trip](const DiveToAdd &entry) + { return entry.tripToAdd.get() == trip; }) != res.end(); + emit diveListNotifier.divesDeleted(trip, deleteTrip, divesInTrip); + }); + return res; +} + +// This helper function is the counterpart fo removeDives(): it calls addDive() on a list +// of dives to be (re)added and returns a vector of the added dives. It does this in reverse +// order, so that trips are created appropriately and indexing is correct. +// The passed in vector is cleared. +std::vector<dive *> DiveListBase::addDives(std::vector<DiveToAdd> &divesToAdd) +{ + std::vector<dive *> res; + res.resize(divesToAdd.size()); + + // First, tell the filters that new dives are added. We do this here + // instead of later by signals, so that the filter can set the + // checkboxes of the new rows to its liking. The added dives will + // then appear in the correct shown/hidden state. + QVector<dive *> divesForFilter; + for (const DiveToAdd &entry: divesToAdd) + divesForFilter.push_back(entry.dive.get()); + MultiFilterSortModel::instance()->divesAdded(divesForFilter); + + // At the end of the function, to send the proper dives-added signals, + // we the the list of added trips. Create this list now. + std::vector<dive_trip *> addedTrips; + for (const DiveToAdd &entry: divesToAdd) { + if (entry.tripToAdd) + addedTrips.push_back(entry.tripToAdd.get()); + } + + // Now, add the dives + // Note: the idiomatic STL-way would be std::transform, but let's use a loop since + // that is closer to classical C-style. + auto it2 = res.rbegin(); + for (auto it = divesToAdd.rbegin(); it != divesToAdd.rend(); ++it, ++it2) + *it2 = addDive(*it); + divesToAdd.clear(); + + // We send one dives-deleted signal per trip (see comments in DiveListNotifier.h). + // Therefore, collect all dives in a array and sort by trip. + std::vector<std::pair<dive_trip *, dive *>> dives; + dives.reserve(res.size()); + for (dive *d: res) + dives.push_back({ d->divetrip, d }); + + // Send signals. + processByTrip(dives, [&](dive_trip *trip, const QVector<dive *> &divesInTrip) { + // Now, let's check if this trip is supposed to be created, by checking if it was marked + // as "add it". We could be smarter here, but let's just check the whole array for brevity. + bool createTrip = trip && std::find(addedTrips.begin(), addedTrips.end(), trip) != addedTrips.end(); + // Finally, emit the signal + emit diveListNotifier.divesAdded(trip, createTrip, divesInTrip); + }); + return res; +} + +// This helper function renumbers dives according to an array of id/number pairs. +// The old numbers are stored in the array, thus calling this function twice has no effect. +// TODO: switch from uniq-id to indexes once all divelist-actions are controlled by undo-able commands +static void renumberDives(QVector<QPair<dive *, int>> &divesToRenumber) +{ + for (auto &pair: divesToRenumber) { + dive *d = pair.first; + if (!d) + continue; + std::swap(d->number, pair.second); + } + + // Emit changed signals per trip. + // First, collect all dives and sort by trip + std::vector<std::pair<dive_trip *, dive *>> dives; + dives.reserve(divesToRenumber.size()); + for (const auto &pair: divesToRenumber) { + dive *d = pair.first; + dives.push_back({ d->divetrip, d }); + } + + // Send signals. + processByTrip(dives, [&](dive_trip *trip, const QVector<dive *> &divesInTrip) { + emit diveListNotifier.divesChanged(trip, divesInTrip); + }); +} + +// This helper function moves a dive to a trip. The old trip is recorded in the +// passed-in structure. This means that calling the function twice on the same +// object is a no-op concerning the dive. If the old trip was deleted from the +// core, an owning pointer to the removed trip is returned, otherwise a null pointer. +static OwningTripPtr moveDiveToTrip(DiveToTrip &diveToTrip) +{ + // Firstly, check if we move to the same trip and bail if this is a no-op. + if (diveToTrip.trip == diveToTrip.dive->divetrip) + return {}; + + // Remove from old trip + OwningTripPtr res; + + // Remove dive from trip - if this is the last dive in the trip, remove the whole trip. + dive_trip *trip = unregister_dive_from_trip(diveToTrip.dive, false); + if (trip && trip->nrdives == 0) { + unregister_trip(trip); // Remove trip from backend + res.reset(trip); + } + + // Store old trip and get new trip we should associate this dive with + std::swap(trip, diveToTrip.trip); + add_dive_to_trip(diveToTrip.dive, trip); + return res; +} + +// This helper function moves a set of dives between trips using the +// moveDiveToTrip function. Before doing so, it adds the necessary trips to +// the core. Trips that are removed from the core because they are empty +// are recorded in the passed in struct. The vectors of trips and dives +// are reversed. Thus, calling the function twice on the same object is +// a no-op. +static void moveDivesBetweenTrips(DivesToTrip &dives) +{ + // We collect an array of created trips so that we can instruct + // the model to create a new entry + std::vector<dive_trip *> createdTrips; + createdTrips.reserve(dives.tripsToAdd.size()); + + // First, bring back the trip(s) + for (OwningTripPtr &trip: dives.tripsToAdd) { + dive_trip *t = trip.release(); // Give up ownership + createdTrips.push_back(t); + insert_trip_dont_merge(t); // Return ownership to backend + } + dives.tripsToAdd.clear(); + + for (DiveToTrip &dive: dives.divesToMove) { + OwningTripPtr tripToAdd = moveDiveToTrip(dive); + // register trips that we'll have to readd + if (tripToAdd) + dives.tripsToAdd.push_back(std::move(tripToAdd)); + } + + // We send one signal per from-trip/to-trip pair. + // First, collect all dives in a struct and sort by from-trip/to-trip. + struct DiveMoved { + dive_trip *from; + dive_trip *to; + dive *d; + }; + std::vector<DiveMoved> divesMoved; + divesMoved.reserve(dives.divesToMove.size()); + for (const DiveToTrip &entry: dives.divesToMove) + divesMoved.push_back({ entry.trip, entry.dive->divetrip, entry.dive }); + + // Sort lexicographically by from-trip, to-trip and by start-time. + // Use std::tie() for lexicographical sorting. + std::sort(divesMoved.begin(), divesMoved.end(), [] ( const DiveMoved &d1, const DiveMoved &d2) + { return std::tie(d1.from, d1.to, d1.d->when) < std::tie(d2.from, d2.to, d2.d->when); }); + + // Now, process the dives in batches by trip + // TODO: this is a bit different from the cases above, so we don't use the processByTrip template, + // but repeat the loop here. We might think about generalizing the template, if more of such + // "special cases" appear. + size_t i, j; // Begin and end of batch + for (i = 0; i < divesMoved.size(); i = j) { + dive_trip *from = divesMoved[i].from; + dive_trip *to = divesMoved[i].to; + for (j = i + 1; j < divesMoved.size() && divesMoved[j].from == from && divesMoved[j].to == to; ++j) + ; // pass + // Copy dives into a QVector. Some sort of "range_view" would be ideal, but Qt doesn't work this way. + QVector<dive *> divesInTrip(j - i); + for (size_t k = i; k < j; ++k) + divesInTrip[k - i] = divesMoved[k].d; + + // Check if the from-trip was deleted: If yes, it was recorded in the tripsToAdd structure + bool deleteFrom = from && + std::find_if(dives.tripsToAdd.begin(), dives.tripsToAdd.end(), + [from](const OwningTripPtr &trip) { return trip.get() == from; }) != dives.tripsToAdd.end(); + // Check if the to-trip has to be created. For this purpose, we saved an array of trips to be created. + bool createTo = false; + if (to) { + // Check if the element is there... + auto it = std::find(createdTrips.begin(), createdTrips.end(), to); + + // ...if it is - remove it as we don't want the model to create the trip twice! + if (it != createdTrips.end()) { + createTo = true; + // erase/remove would be more performant, but this is irrelevant in the big scheme of things. + createdTrips.erase(it); + } + } + + // Finally, emit the signal + emit diveListNotifier.divesMovedBetweenTrips(from, to, deleteFrom, createTo, divesInTrip); + } + + // Reverse the tripsToAdd and the divesToAdd, so that on undo/redo the operations + // will be performed in reverse order. + std::reverse(dives.tripsToAdd.begin(), dives.tripsToAdd.end()); + std::reverse(dives.divesToMove.begin(), dives.divesToMove.end()); +} + +// When we initialize the command we don't have to roll-back any selection change +DiveListBase::DiveListBase() : firstExecution(true) +{ +} + +// Turn current selection into a vector. +// TODO: This could be made much more efficient if we kept a sorted list of selected dives! +static std::vector<dive *> getDiveSelection() +{ + std::vector<dive *> res; + res.reserve(amount_selected); + + int i; + dive *d; + for_each_dive(i, d) { + if (d->selected) + res.push_back(d); + } + return res; +} + +void DiveListBase::initWork() +{ + selectionChanged = false; +} + +void DiveListBase::finishWork() +{ + if (selectionChanged) // If the selection changed -> tell the frontend + emit diveListNotifier.selectionChanged(); +} + +// Set the current dive either from a list of selected dives, +// or a newly selected dive. In both cases, try to select the +// dive that is newer that is newer than the given date. +// This mimics the old behavior when the current dive changed. +static void setClosestCurrentDive(timestamp_t when, const std::vector<dive *> &selection) +{ + // Start from back until we get the first dive that is before + // the supposed-to-be selected dive. (Note: this mimics the + // old behavior when the current dive changed). + for (auto it = selection.rbegin(); it < selection.rend(); ++it) { + if ((*it)->when > when && !(*it)->hidden_by_filter) { + current_dive = *it; + return; + } + } + + // We didn't find a more recent selected dive -> try to + // find *any* visible selected dive. + for (dive *d: selection) { + if (!d->hidden_by_filter) { + current_dive = d; + return; + } + } + + // No selected dive is visible! Take the closest dive. Note, this might + // return null, but that just means unsetting the current dive (as no + // dive is visible anyway). + current_dive = find_next_visible_dive(when); +} + +// Rese the selection to the dives of the "selection" vector and send the appropriate signals. +// Set the current dive to "currentDive". "currentDive" must be an element of "selection" (or +// null if "seletion" is empty). +void DiveListBase::restoreSelection(const std::vector<dive *> &selection, dive *currentDive) +{ + // To do so, generate vectors of dives to be selected and deselected. + // We send signals batched by trip, so keep track of trip/dive pairs. + std::vector<std::pair<dive_trip *, dive *>> divesToSelect; + std::vector<std::pair<dive_trip *, dive *>> divesToDeselect; + + // TODO: We might want to keep track of selected dives in a more efficient way! + int i; + dive *d; + amount_selected = 0; // We recalculate amount_selected + for_each_dive(i, d) { + // We only modify dives that are currently visible. + if (d->hidden_by_filter) { + d->selected = false; // Note, not necessary, just to be sure + // that we get amount_selected right + continue; + } + + // Search the dive in the list of selected dives. + // TODO: By sorting the list in the same way as the backend, this could be made more efficient. + bool newState = std::find(selection.begin(), selection.end(), d) != selection.end(); + + // TODO: Instead of using select_dive() and deselect_dive(), we set selected directly. + // The reason is that deselect() automatically sets a new current dive, which we + // don't want, as we set it later anyway. + // There is other parts of the C++ code that touches the innards directly, but + // ultimately this should be pushed down to C. + if (newState && !d->selected) { + d->selected = true; + ++amount_selected; + divesToSelect.push_back({ d->divetrip, d }); + } else if (!newState && d->selected) { + d->selected = false; + divesToDeselect.push_back({ d->divetrip, d }); + } + } + + // Send the select and deselect signals + processByTrip(divesToSelect, [&](dive_trip *trip, const QVector<dive *> &divesInTrip) { + emit diveListNotifier.divesSelected(trip, divesInTrip); + }); + processByTrip(divesToDeselect, [&](dive_trip *trip, const QVector<dive *> &divesInTrip) { + emit diveListNotifier.divesDeselected(trip, divesInTrip); + }); + + bool currentDiveChanged = false; + if (current_dive != currentDive) { + currentDiveChanged = true; + + // We cannot simply change the currentd dive to the given dive. + // It might be hidden by a filter and thus not be selected. + if (currentDive->selected) + // Current dive is visible and selected. Excellent. + current_dive = currentDive; + else + // Current not visible -> find a different dive. + setClosestCurrentDive(currentDive->when, selection); + emit diveListNotifier.currentDiveChanged(); + } + + // If anything changed (selection or current dive), send a final signal. + if (!divesToSelect.empty() || !divesToDeselect.empty() || currentDiveChanged) + selectionChanged = true; +} + +void DiveListBase::undo() +{ + auto marker = diveListNotifier.enterCommand(); + initWork(); + undoit(); + finishWork(); +} + +void DiveListBase::redo() +{ + auto marker = diveListNotifier.enterCommand(); + initWork(); + redoit(); + finishWork(); +} + +AddDive::AddDive(dive *d, bool autogroup, bool newNumber) +{ + setText(tr("add dive")); + // By convention, d is "displayed dive" and can be overwritten. + d->maxdepth.mm = 0; + d->dc.maxdepth.mm = 0; + fixup_dive(d); + + // Get an owning pointer to a copied or moved dive + // Note: if move is true, this destroys the old dive! + OwningDivePtr divePtr(clone_dive(d)); + divePtr->selected = false; // If we clone a planned dive, it might have been selected. + // We have to clear the flag, as selections will be managed + // on dive-addition. + + // If we alloc a new-trip for autogrouping, get an owning pointer to it. + OwningTripPtr allocTrip; + dive_trip *trip = divePtr->divetrip; + // We have to delete the pointer-to-trip, because this would prevent the core from adding to the trip + // and we would get the count-of-dives in the trip wrong. Yes, that's all horribly subtle! + divePtr->divetrip = nullptr; + if (!trip && autogroup) { + bool alloc; + trip = get_trip_for_new_dive(divePtr.get(), &alloc); + if (alloc) + allocTrip.reset(trip); + } + + int idx = dive_get_insertion_index(divePtr.get()); + if (newNumber) + divePtr->number = get_dive_nr_at_idx(idx); + + divesToAdd.push_back({ std::move(divePtr), std::move(allocTrip), trip, idx }); +} + +bool AddDive::workToBeDone() +{ + return true; +} + +void AddDive::redoit() +{ + // Remember selection so that we can undo it + selection = getDiveSelection(); + currentDive = current_dive; + + divesToRemove = addDives(divesToAdd); + mark_divelist_changed(true); + + // Select the newly added dive + restoreSelection(divesToRemove, divesToRemove[0]); + + // Exit from edit mode, but don't recalculate dive list + // TODO: Remove edit mode + MainWindow::instance()->refreshDisplay(false); +} + +void AddDive::undoit() +{ + // Simply remove the dive that was previously added... + divesToAdd = removeDives(divesToRemove); + + // ...and restore the selection + restoreSelection(selection, currentDive); + + // Exit from edit mode, but don't recalculate dive list + // TODO: Remove edit mode + MainWindow::instance()->refreshDisplay(false); +} + +DeleteDive::DeleteDive(const QVector<struct dive*> &divesToDeleteIn) : divesToDelete(divesToDeleteIn.toStdVector()) +{ + setText(tr("delete %n dive(s)", "", divesToDelete.size())); +} + +bool DeleteDive::workToBeDone() +{ + return !divesToDelete.empty(); +} + +void DeleteDive::undoit() +{ + divesToDelete = addDives(divesToAdd); + mark_divelist_changed(true); + + // Select all re-added dives and make the first one current + dive *currentDive = !divesToDelete.empty() ? divesToDelete[0] : nullptr; + restoreSelection(divesToDelete, currentDive); +} + +void DeleteDive::redoit() +{ + divesToAdd = removeDives(divesToDelete); + mark_divelist_changed(true); + + // Deselect all dives and select dive that was close to the first deleted dive + dive *newCurrent = nullptr; + if (!divesToAdd.empty()) { + timestamp_t when = divesToAdd[0].dive->when; + newCurrent = find_next_visible_dive(when); + } + if (newCurrent) + restoreSelection(std::vector<dive *>{ newCurrent }, newCurrent); + else + restoreSelection(std::vector<dive *>(), nullptr); +} + + +ShiftTime::ShiftTime(const QVector<dive *> &changedDives, int amount) + : diveList(changedDives), timeChanged(amount) +{ + setText(tr("shift time of %n dives", "", changedDives.count())); +} + +void ShiftTime::redoit() +{ + for (dive *d: diveList) + d->when -= timeChanged; + + // Changing times may have unsorted the dive table + sort_table(&dive_table); + + // We send one dives-deleted signal per trip (see comments in DiveListNotifier.h). + // Therefore, collect all dives in a array and sort by trip. + std::vector<std::pair<dive_trip *, dive *>> dives; + dives.reserve(diveList.size()); + for (dive *d: diveList) + dives.push_back({ d->divetrip, d }); + + // Send signals. + processByTrip(dives, [&](dive_trip *trip, const QVector<dive *> &divesInTrip) { + emit diveListNotifier.divesTimeChanged(trip, timeChanged, divesInTrip); + }); + + // Negate the time-shift so that the next call does the reverse + timeChanged = -timeChanged; + + mark_divelist_changed(true); +} + +bool ShiftTime::workToBeDone() +{ + return !diveList.isEmpty(); +} + +void ShiftTime::undoit() +{ + // Same as redoit(), since after redoit() we reversed the timeOffset + redoit(); +} + + +RenumberDives::RenumberDives(const QVector<QPair<dive *, int>> &divesToRenumberIn) : divesToRenumber(divesToRenumberIn) +{ + setText(tr("renumber %n dive(s)", "", divesToRenumber.count())); +} + +void RenumberDives::undoit() +{ + renumberDives(divesToRenumber); + mark_divelist_changed(true); +} + +bool RenumberDives::workToBeDone() +{ + return !divesToRenumber.isEmpty(); +} + +void RenumberDives::redoit() +{ + // Redo and undo do the same thing! + undoit(); +} + +bool TripBase::workToBeDone() +{ + return !divesToMove.divesToMove.empty(); +} + +void TripBase::redoit() +{ + moveDivesBetweenTrips(divesToMove); + + mark_divelist_changed(true); +} + +void TripBase::undoit() +{ + // Redo and undo do the same thing! + redoit(); +} + +RemoveDivesFromTrip::RemoveDivesFromTrip(const QVector<dive *> &divesToRemove) +{ + setText(tr("remove %n dive(s) from trip", "", divesToRemove.size())); + divesToMove.divesToMove.reserve(divesToRemove.size()); + for (dive *d: divesToRemove) + divesToMove.divesToMove.push_back( {d, nullptr} ); +} + +RemoveAutogenTrips::RemoveAutogenTrips() +{ + setText(tr("remove autogenerated trips")); + // TODO: don't touch core-innards directly + int i; + struct dive *dive; + for_each_dive(i, dive) { + if (dive->divetrip && dive->divetrip->autogen) + divesToMove.divesToMove.push_back( {dive, nullptr} ); + } +} + +AddDivesToTrip::AddDivesToTrip(const QVector<dive *> &divesToAddIn, dive_trip *trip) +{ + setText(tr("add %n dives to trip", "", divesToAddIn.size())); + for (dive *d: divesToAddIn) + divesToMove.divesToMove.push_back( {d, trip} ); +} + +CreateTrip::CreateTrip(const QVector<dive *> &divesToAddIn) +{ + setText(tr("create trip")); + + if (divesToAddIn.isEmpty()) + return; + + dive_trip *trip = create_trip_from_dive(divesToAddIn[0]); + divesToMove.tripsToAdd.emplace_back(trip); + for (dive *d: divesToAddIn) + divesToMove.divesToMove.push_back( {d, trip} ); +} + +AutogroupDives::AutogroupDives() +{ + setText(tr("autogroup dives")); + + dive_trip *trip; + bool alloc; + int from, to; + for(int i = 0; (trip = get_dives_to_autogroup(i, &from, &to, &alloc)) != NULL; i = to) { + // If this is an allocated trip, take ownership + if (alloc) + divesToMove.tripsToAdd.emplace_back(trip); + for (int j = from; j < to; ++j) + divesToMove.divesToMove.push_back( { get_dive(j), trip } ); + } +} + +MergeTrips::MergeTrips(dive_trip *trip1, dive_trip *trip2) +{ + if (trip1 == trip2) + return; + dive_trip *newTrip = combine_trips_create(trip1, trip2); + divesToMove.tripsToAdd.emplace_back(newTrip); + for (dive *d = trip1->dives; d; d = d->next) + divesToMove.divesToMove.push_back( { d, newTrip } ); + for (dive *d = trip2->dives; d; d = d->next) + divesToMove.divesToMove.push_back( { d, newTrip } ); +} + +SplitDives::SplitDives(dive *d, duration_t time) +{ + setText(tr("split dive")); + + // Split the dive + dive *new1, *new2; + int idx = time.seconds < 0 ? + split_dive_dont_insert(d, &new1, &new2) : + split_dive_at_time_dont_insert(d, time, &new1, &new2); + + // If this didn't work, simply return. Empty arrays indicate that nothing is to be done. + if (idx < 0) + return; + + // Currently, the core code selects the dive -> this is not what we want, as + // we manually manage the selection post-command. + // TODO: Reset selection in core. + new1->selected = false; + new2->selected = false; + + diveToSplit.push_back(d); + splitDives.resize(2); + splitDives[0].dive.reset(new1); + splitDives[0].trip = d->divetrip; + splitDives[0].idx = idx; + splitDives[1].dive.reset(new2); + splitDives[1].trip = d->divetrip; + splitDives[1].idx = idx + 1; +} + +bool SplitDives::workToBeDone() +{ + return !diveToSplit.empty(); +} + +void SplitDives::redoit() +{ + divesToUnsplit = addDives(splitDives); + unsplitDive = removeDives(diveToSplit); + mark_divelist_changed(true); + + // Select split dives and make first dive current + restoreSelection(divesToUnsplit, divesToUnsplit[0]); +} + +void SplitDives::undoit() +{ + // Note: reverse order with respect to redoit() + diveToSplit = addDives(unsplitDive); + splitDives = removeDives(divesToUnsplit); + mark_divelist_changed(true); + + // Select unsplit dive and make it current + restoreSelection(diveToSplit, diveToSplit[0] ); +} + +MergeDives::MergeDives(const QVector <dive *> &dives) +{ + setText(tr("merge dive")); + + // Just a safety check - if there's not two or more dives - do nothing + // The caller should have made sure that this doesn't happen. + if (dives.count() < 2) { + qWarning() << "Merging less than two dives"; + return; + } + + dive_trip *preferred_trip; + OwningDivePtr d(merge_dives(dives[0], dives[1], dives[1]->when - dives[0]->when, false, &preferred_trip)); + + // Currently, the core code selects the dive -> this is not what we want, as + // we manually manage the selection post-command. + // TODO: Remove selection code from core. + d->selected = false; + + // Set the preferred dive trip, so that for subsequent merges the better trip can be selected + d->divetrip = preferred_trip; + for (int i = 2; i < dives.count(); ++i) { + d.reset(merge_dives(d.get(), dives[i], dives[i]->when - d->when, false, &preferred_trip)); + // Set the preferred dive trip, so that for subsequent merges the better trip can be selected + d->divetrip = preferred_trip; + } + + // We got our preferred trip, so now the reference can be deleted from the newly generated dive + d->divetrip = nullptr; + + // The merged dive gets the number of the first dive + d->number = dives[0]->number; + + // We will only renumber the remaining dives if the joined dives are consecutive. + // Otherwise all bets are off concerning what the user wanted and doing nothing seems + // like the best option. + int idx = get_divenr(dives[0]); + int num = dives.count(); + if (idx < 0 || idx + num > dive_table.nr) { + // It was the callers responsibility to pass only known dives. + // Something is seriously wrong - give up. + qWarning() << "Merging unknown dives"; + return; + } + // std::equal compares two ranges. The parameters are (begin_range1, end_range1, begin_range2). + // Here, we can compare C-arrays, because QVector guarantees contiguous storage. + if (std::equal(&dives[0], &dives[0] + num, &dive_table.dives[idx]) && + dives[0]->number && dives.last()->number && dives[0]->number < dives.last()->number) { + // We have a consecutive set of dives. Rename all following dives according to the + // number of erased dives. This considers that there might be missing numbers. + // Comment copied from core/divelist.c: + // So if you had a dive list 1 3 6 7 8, and you + // merge 1 and 3, the resulting numbered list will + // be 1 4 5 6, because we assume that there were + // some missing dives (originally dives 4 and 5), + // that now will still be missing (dives 2 and 3 + // in the renumbered world). + // + // Obviously the normal case is that everything is + // consecutive, and the difference will be 1, so the + // above example is not supposed to be normal. + int diff = dives.last()->number - dives[0]->number; + divesToRenumber.reserve(dive_table.nr - idx - num); + int previousnr = dives[0]->number; + for (int i = idx + num; i < dive_table.nr; ++i) { + int newnr = dive_table.dives[i]->number - diff; + + // Stop renumbering if stuff isn't in order (see also core/divelist.c) + if (newnr <= previousnr) + break; + divesToRenumber.append(QPair<dive *,int>(dive_table.dives[i], newnr)); + previousnr = newnr; + } + } + + mergedDive.resize(1); + mergedDive[0].dive = std::move(d); + mergedDive[0].idx = get_divenr(dives[0]); + mergedDive[0].trip = preferred_trip; + divesToMerge = dives.toStdVector(); +} + +bool MergeDives::workToBeDone() +{ + return !mergedDive.empty(); +} + +void MergeDives::redoit() +{ + renumberDives(divesToRenumber); + diveToUnmerge = addDives(mergedDive); + unmergedDives = removeDives(divesToMerge); + + // Select merged dive and make it current + restoreSelection(diveToUnmerge, diveToUnmerge[0]); +} + +void MergeDives::undoit() +{ + divesToMerge = addDives(unmergedDives); + mergedDive = removeDives(diveToUnmerge); + renumberDives(divesToRenumber); + + // Select unmerged dives and make first one current + restoreSelection(divesToMerge, divesToMerge[0]); +} + +} // namespace Command diff --git a/desktop-widgets/tab-widgets/maintab.cpp b/desktop-widgets/tab-widgets/maintab.cpp index 766d32414..e5974a834 100644 --- a/desktop-widgets/tab-widgets/maintab.cpp +++ b/desktop-widgets/tab-widgets/maintab.cpp @@ -26,6 +26,7 @@ #include "core/subsurface-string.h" #include "core/gettextfromc.h" #include "desktop-widgets/locationinformation.h" +#include "desktop-widgets/undocommands.h" #include "TabDiveExtraInfo.h" #include "TabDiveInformation.h" @@ -788,23 +789,30 @@ void MainTab::acceptChanges() hideMessage(); ui.equipmentTab->setEnabled(true); if (editMode == ADD) { - // We need to add the dive we just created to the dive list and select it. - // Easy, right? - struct dive *added_dive = clone_dive(&displayed_dive); - record_dive(added_dive); - addedId = added_dive->id; // make sure that the dive site is handled as well - updateDiveSite(ui.location->currDiveSiteUuid(), added_dive); + updateDiveSite(ui.location->currDiveSiteUuid(), &displayed_dive); - // unselect everything as far as the UI is concerned and select the new - // dive - we'll have to undo/redo this later after we resort the dive_table - // but we need the dive selected for the middle part of this function - this - // way we can reuse the code used for editing dives - MainWindow::instance()->dive_list()->unselectDives(); - selected_dive = get_divenr(added_dive); - amount_selected = 1; - // finally, make sure we get the tags - saveTags(); + UndoAddDive *undoCommand = new UndoAddDive(&displayed_dive); + MainWindow::instance()->undoStack->push(undoCommand); + + editMode = NONE; + MainWindow::instance()->exitEditState(); + cylindersModel->changed = false; + weightModel->changed = false; + MainWindow::instance()->setEnabledToolbar(true); + acceptingEdit = false; + ui.editDiveSiteButton->setEnabled(!ui.location->text().isEmpty()); + emit addDiveFinished(); + MainWindow::instance()->dive_list()->reload(DiveTripModel::CURRENT, true); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING); + int scrolledBy = MainWindow::instance()->dive_list()->verticalScrollBar()->sliderPosition(); + MainWindow::instance()->dive_list()->verticalScrollBar()->setSliderPosition(scrolledBy); + MainWindow::instance()->dive_list()->setFocus(); + resetPallete(); + saveTags(QVector<dive *>{ &displayed_dive }); + displayed_dive.divetrip = nullptr; // Should not be necessary, just in case! + Command::addDive(&displayed_dive, autogroup, true); + return; } else if (MainWindow::instance() && MainWindow::instance()->dive_list()->selectedTrips().count() == 1) { /* now figure out if things have changed */ if (displayedTrip.notes && !same_string(displayedTrip.notes, currentTrip->notes)) { @@ -957,7 +965,7 @@ void MainTab::acceptChanges() current_dive->divetrip->when = current_dive->when; find_new_trip_start_time(current_dive->divetrip); } - if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE) { + if (editMode == MANUALLY_ADDED_DIVE) { // we just added or edited the dive, let fixup_dive() make // sure we get the max. depth right current_dive->maxdepth.mm = current_dc->maxdepth.mm = 0; @@ -969,7 +977,7 @@ void MainTab::acceptChanges() } int scrolledBy = MainWindow::instance()->dive_list()->verticalScrollBar()->sliderPosition(); resetPallete(); - if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE) { + if (editMode == MANUALLY_ADDED_DIVE) { // since a newly added dive could be in the middle of the dive_table we need // to resort the dive list and make sure the newly added dive gets selected again sort_table(&dive_table); @@ -980,7 +988,6 @@ void MainTab::acceptChanges() editMode = NONE; MainWindow::instance()->refreshDisplay(); MainWindow::instance()->graphics()->replot(); - emit addDiveFinished(); } else { editMode = NONE; if (do_replot) diff --git a/desktop-widgets/undocommands.cpp b/desktop-widgets/undocommands.cpp index d58ac7a4b..668a84f9e 100644 --- a/desktop-widgets/undocommands.cpp +++ b/desktop-widgets/undocommands.cpp @@ -1,10 +1,83 @@ // SPDX-License-Identifier: GPL-2.0 #include "desktop-widgets/undocommands.h" #include "desktop-widgets/mainwindow.h" +#include "desktop-widgets/divelistview.h" #include "core/divelist.h" #include "core/subsurface-string.h" #include "core/gettextfromc.h" +// This helper function removes a dive, takes ownership of the dive and adds it to a DiveToAdd structure. +// It is crucial that dives are added in reverse order of deletion, so the the indices are correctly +// set and that the trips are added before they are used! +static DiveToAdd removeDive(struct dive *d) +{ + DiveToAdd res; + res.idx = get_divenr(d); + if (res.idx < 0) + qWarning() << "Deletion of unknown dive!"; + + // remove dive from trip - if this is the last dive in the trip + // remove the whole trip. + res.trip = unregister_dive_from_trip(d, false); + if (res.trip && res.trip->nrdives == 0) { + unregister_trip(res.trip); // Remove trip from backend + res.tripToAdd.reset(res.trip); // Take ownership of trip + } + + res.dive.reset(unregister_dive(res.idx)); // Remove dive from backend + return res; +} + +// This helper function adds a dive and returns ownership to the backend. It may also add a dive trip. +// It is crucial that dives are added in reverse order of deletion (see comment above)! +// Returns pointer to added dive (which is owned by the backend!) +static dive *addDive(DiveToAdd &d) +{ + if (d.tripToAdd) { + dive_trip *t = d.tripToAdd.release(); // Give up ownership of trip + insert_trip(&t); // Return ownership to backend + } + if (d.trip) + add_dive_to_trip(d.dive.get(), d.trip); + dive *res = d.dive.release(); // Give up ownership of dive + add_single_dive(d.idx, res); // Return ownership to backend + return res; +} + +UndoAddDive::UndoAddDive(dive *d) +{ + setText(gettextFromC::tr("add dive")); + // TODO: handle tags + //saveTags(); + d->maxdepth.mm = 0; + fixup_dive(d); + diveToAdd.trip = d->divetrip; + d->divetrip = nullptr; + diveToAdd.idx = dive_get_insertion_index(d); + d->number = get_dive_nr_at_idx(diveToAdd.idx); + diveToAdd.dive.reset(clone_dive(d)); +} + +void UndoAddDive::redo() +{ + diveToRemove = addDive(diveToAdd); + mark_divelist_changed(true); + + // Finally, do the UI stuff: + MainWindow::instance()->dive_list()->unselectDives(); + MainWindow::instance()->dive_list()->selectDive(diveToAdd.idx, true); + MainWindow::instance()->refreshDisplay(); +} + +void UndoAddDive::undo() +{ + // Simply remove the dive that was previously added + diveToAdd = removeDive(diveToRemove); + + // Finally, do the UI stuff: + MainWindow::instance()->refreshDisplay(); +} + UndoDeleteDive::UndoDeleteDive(const QVector<struct dive*> &divesToDeleteIn) : divesToDelete(divesToDeleteIn) { setText(tr("delete %n dive(s)", "", divesToDelete.size())); @@ -12,19 +85,9 @@ UndoDeleteDive::UndoDeleteDive(const QVector<struct dive*> &divesToDeleteIn) : d void UndoDeleteDive::undo() { - // first bring back the trip(s) - for (auto &trip: tripsToAdd) { - dive_trip *t = trip.release(); // Give up ownership - insert_trip(&t); // Return ownership to backend - } - tripsToAdd.clear(); + for (auto it = divesToAdd.rbegin(); it != divesToAdd.rend(); ++it) + divesToDelete.append(addDive(*it)); - for (DiveToAdd &d: divesToAdd) { - if (d.trip) - add_dive_to_trip(d.dive.get(), d.trip); - divesToDelete.append(d.dive.get()); // Delete dive on redo - add_single_dive(d.idx, d.dive.release()); // Return ownership to backend - } mark_divelist_changed(true); divesToAdd.clear(); @@ -34,24 +97,9 @@ void UndoDeleteDive::undo() void UndoDeleteDive::redo() { - for (dive *d: divesToDelete) { - int idx = get_divenr(d); - if (idx < 0) { - qWarning() << "Deletion of unknown dive!"; - continue; - } - // remove dive from trip - if this is the last dive in the trip - // remove the whole trip. - dive_trip *trip = unregister_dive_from_trip(d, false); - if (trip && trip->nrdives == 0) { - unregister_trip(trip); // Remove trip from backend - tripsToAdd.emplace_back(trip); // Take ownership of trip - } + for (dive *d: divesToDelete) + divesToAdd.push_back(removeDive(d)); - unregister_dive(idx); // Remove dive from backend - divesToAdd.push_back({ OwningDivePtr(d), trip, idx }); - // Take ownership for dive - } divesToDelete.clear(); mark_divelist_changed(true); diff --git a/desktop-widgets/undocommands.h b/desktop-widgets/undocommands.h index bbc1f05bd..4c4073454 100644 --- a/desktop-widgets/undocommands.h +++ b/desktop-widgets/undocommands.h @@ -148,6 +148,29 @@ struct TripDeleter { typedef std::unique_ptr<dive, DiveDeleter> OwningDivePtr; typedef std::unique_ptr<dive_trip, TripDeleter> OwningTripPtr; +// This helper structure describes a dive that we want to add. +// Potentially it also adds a trip (if deletion of the dive resulted in deletion of the trip) +struct DiveToAdd { + OwningDivePtr dive; // Dive to add + OwningTripPtr tripToAdd; // Not-null if we also have to add a dive + dive_trip *trip; // Trip the dive belongs to, may be null + int idx; // Position in divelist +}; + +class UndoAddDive : public QUndoCommand { +public: + UndoAddDive(dive *dive); // Warning: old dive will be erased (moved in C++-speak)! +private: + void undo() override; + void redo() override; + + // For redo + DiveToAdd diveToAdd; + + // For undo + dive *diveToRemove; +}; + class UndoDeleteDive : public QUndoCommand { Q_DECLARE_TR_FUNCTIONS(Command) public: @@ -159,12 +182,6 @@ private: // For redo QVector<struct dive*> divesToDelete; - // For undo - struct DiveToAdd { - OwningDivePtr dive; // Dive to add - dive_trip *trip; // Trip, may be null - int idx; // Position in divelist - }; std::vector<OwningTripPtr> tripsToAdd; std::vector<DiveToAdd> divesToAdd; }; |