Undo: fix multi-level undo of delete-dive and remove-dive-from-trip

The original undo-code was fundamentally broken. Not only did it leak
resources (copied trips were never freed), it also kept references
to trips or dives that could be changed by other commands. Thus,
anything more than a single undo could lead to crashes.

Two ways of fixing this were considered
1) Don't store pointers, but unique dive-ids and trip-ids.
   Whereas such unique ids exist for dives, they would have to be
   implemented for trips.
2) Don't free objects in the backend.
   Instead, take ownership of deleted objects in the undo-object.
   Thus, all references in previous undo-objects are guaranteed to
   still exist (unless the objects are deleted elsewhere).

After some contemplation, the second method was chosen, because
it is significantly less intrusive. While touching the undo-objects,
clearly separate backend from ui-code, such that they can ultimately
be reused for mobile.

Note that if other parts of the code delete dives, crashes can still
be provoked. Notable examples are split/merge dives. These will have
to be fixed later. Nevertheless, the new code is a significant
improvement over the old state.

While touching the code, implement proper translation string based
on Qt's plural-feature (using %n).

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
Berthold Stoeger 2018-07-19 14:44:27 +02:00 committed by Dirk Hohndel
parent 8074f12b91
commit 403dd5a891
7 changed files with 329 additions and 153 deletions

View file

@ -416,12 +416,13 @@ extern bool autogroup;
extern void add_dive_to_trip(struct dive *, dive_trip_t *); 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 void delete_single_dive(int idx);
extern void add_single_dive(int idx, struct dive *dive); extern void add_single_dive(int idx, struct dive *dive);
extern void insert_trip(dive_trip_t **trip); extern void insert_trip(dive_trip_t **trip);
extern struct dive_trip *clone_empty_trip(struct dive_trip *trip); extern void unregister_trip(dive_trip_t *trip);
extern void free_trip(dive_trip_t *trip);
extern const struct units SI_units, IMPERIAL_units; extern const struct units SI_units, IMPERIAL_units;
extern struct units xml_parsing_units; extern struct units xml_parsing_units;

View file

@ -16,10 +16,14 @@
* void update_cylinder_related_info(struct dive *dive) * void update_cylinder_related_info(struct dive *dive)
* void dump_trip_list(void) * void dump_trip_list(void)
* void insert_trip(dive_trip_t **dive_trip_p) * void insert_trip(dive_trip_t **dive_trip_p)
* void unregister_trip(dive_trip_t *trip)
* void free_trip(dive_trip_t *trip)
* void remove_dive_from_trip(struct dive *dive)
* void remove_dive_from_trip(struct dive *dive, bool was_autogen) * void remove_dive_from_trip(struct dive *dive, bool was_autogen)
* void add_dive_to_trip(struct dive *dive, dive_trip_t *trip) * void add_dive_to_trip(struct dive *dive, dive_trip_t *trip)
* dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive) * dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive)
* void autogroup_dives(void) * void autogroup_dives(void)
* struct dive *unregister_dive(int idx)
* void delete_single_dive(int idx) * void delete_single_dive(int idx)
* void add_single_dive(int idx, struct dive *dive) * void add_single_dive(int idx, struct dive *dive)
* void merge_two_dives(struct dive *a, struct dive *b) * void merge_two_dives(struct dive *a, struct dive *b)
@ -732,20 +736,17 @@ void insert_trip(dive_trip_t **dive_trip_p)
#endif #endif
} }
/* create a copy of a dive trip, but don't add any dives. */ /* free resources associated with a trip structure */
dive_trip_t *clone_empty_trip(dive_trip_t *trip) void free_trip(dive_trip_t *trip)
{ {
dive_trip_t *copy = malloc(sizeof(struct dive_trip)); free(trip->location);
*copy = *trip; free(trip->notes);
copy->location = copy_string(trip->location); free(trip);
copy->notes = copy_string(trip->notes);
copy->nrdives = 0;
copy->next = NULL;
copy->dives = NULL;
return copy;
} }
static void delete_trip(dive_trip_t *trip) /* remove trip from the trip-list, but don't free its memory.
* caller takes ownership of the trip. */
void unregister_trip(dive_trip_t *trip)
{ {
dive_trip_t **p, *tmp; dive_trip_t **p, *tmp;
@ -760,11 +761,12 @@ static void delete_trip(dive_trip_t *trip)
} }
p = &tmp->next; p = &tmp->next;
} }
}
/* .. and free it */ static void delete_trip(dive_trip_t *trip)
free(trip->location); {
free(trip->notes); unregister_trip(trip);
free(trip); free_trip(trip);
} }
void find_new_trip_start_time(dive_trip_t *trip) void find_new_trip_start_time(dive_trip_t *trip)
@ -817,13 +819,17 @@ struct dive *last_selected_dive()
return ret; return ret;
} }
void remove_dive_from_trip(struct dive *dive, short was_autogen) /* remove a dive from the trip it's associated to, but don't delete the
* trip if this was the last dive in the trip. the caller is responsible
* for removing the trip, if the trip->nrdives went to 0.
*/
struct dive_trip *unregister_dive_from_trip(struct dive *dive, short was_autogen)
{ {
struct dive *next, **pprev; struct dive *next, **pprev;
dive_trip_t *trip = dive->divetrip; dive_trip_t *trip = dive->divetrip;
if (!trip) if (!trip)
return; return NULL;
/* Remove the dive from the trip's list of dives */ /* Remove the dive from the trip's list of dives */
next = dive->next; next = dive->next;
@ -838,10 +844,17 @@ void remove_dive_from_trip(struct dive *dive, short was_autogen)
else else
dive->tripflag = NO_TRIP; dive->tripflag = NO_TRIP;
assert(trip->nrdives > 0); assert(trip->nrdives > 0);
if (!--trip->nrdives) --trip->nrdives;
delete_trip(trip); if (trip->nrdives > 0 && trip->when == dive->when)
else if (trip->when == dive->when)
find_new_trip_start_time(trip); find_new_trip_start_time(trip);
return trip;
}
void remove_dive_from_trip(struct dive *dive, short was_autogen)
{
struct dive_trip *trip = unregister_dive_from_trip(dive, was_autogen);
if (trip && trip->nrdives == 0)
delete_trip(trip);
} }
void add_dive_to_trip(struct dive *dive, dive_trip_t *trip) void add_dive_to_trip(struct dive *dive, dive_trip_t *trip)
@ -918,29 +931,44 @@ void autogroup_dives(void)
#endif #endif
} }
static void unregister_dive_from_table(struct dive_table *table, int idx)
{
int i;
for (i = idx; i < table->nr - 1; i++)
table->dives[i] = table->dives[i + 1];
table->dives[--table->nr] = NULL;
}
/* Remove a dive from a dive table. This assumes that the /* Remove a dive from a dive table. This assumes that the
* dive was already removed from any trip and deselected. * dive was already removed from any trip and deselected.
* It simply shrinks the table and frees the trip */ * It simply shrinks the table and frees the trip */
void delete_dive_from_table(struct dive_table *table, int idx) void delete_dive_from_table(struct dive_table *table, int idx)
{ {
int i;
free_dive(table->dives[idx]); free_dive(table->dives[idx]);
for (i = idx; i < table->nr - 1; i++) unregister_dive_from_table(table, idx);
table->dives[i] = table->dives[i + 1]; }
table->dives[--table->nr] = NULL;
/* this removes a dive from the dive table and trip-list but doesn't
* free the resources associated with the dive. It returns a pointer
* to the unregistered dive. */
struct dive *unregister_dive(int idx)
{
struct dive *dive = get_dive(idx);
if (!dive)
return NULL; /* this should never happen */
remove_dive_from_trip(dive, false);
if (dive->selected)
deselect_dive(idx);
unregister_dive_from_table(&dive_table, idx);
return dive;
} }
/* this implements the mechanics of removing the dive from the table, /* this implements the mechanics of removing the dive from the table,
* but doesn't deal with updating dive trips, etc */ * but doesn't deal with updating dive trips, etc */
void delete_single_dive(int idx) void delete_single_dive(int idx)
{ {
struct dive *dive = get_dive(idx); struct dive *dive = unregister_dive(idx);
if (!dive) free_dive(dive);
return; /* this should never happen */
remove_dive_from_trip(dive, false);
if (dive->selected)
deselect_dive(idx);
delete_dive_from_table(&dive_table, idx);
} }
struct dive **grow_dive_table(struct dive_table *table) struct dive **grow_dive_table(struct dive_table *table)

View file

@ -26,6 +26,7 @@ struct dive **grow_dive_table(struct dive_table *table);
extern void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2low_p); extern void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2low_p);
extern int get_divenr(const struct dive *dive); extern int get_divenr(const struct dive *dive);
extern int get_divesite_idx(const struct dive_site *ds); extern int get_divesite_idx(const struct dive_site *ds);
extern struct dive_trip *unregister_dive_from_trip(struct dive *dive, short was_autogen);
extern void remove_dive_from_trip(struct dive *dive, short was_autogen); extern void remove_dive_from_trip(struct dive *dive, short was_autogen);
extern dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive); extern dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive);
extern void autogroup_dives(void); extern void autogroup_dives(void);

View file

@ -679,10 +679,10 @@ void DiveListView::removeFromTrip()
//TODO: move this to C-code. //TODO: move this to C-code.
int i; int i;
struct dive *d; struct dive *d;
QMap<struct dive*, dive_trip*> divesToRemove; QVector<struct dive *> divesToRemove;
for_each_dive (i, d) { for_each_dive (i, d) {
if (d->selected && d->divetrip) if (d->selected && d->divetrip)
divesToRemove.insert(d, d->divetrip); divesToRemove.append(d);
} }
if (divesToRemove.isEmpty()) if (divesToRemove.isEmpty())
return; return;
@ -797,7 +797,7 @@ void DiveListView::deleteDive()
int i; int i;
int lastDiveNr = -1; int lastDiveNr = -1;
QList<struct dive*> deletedDives; //a list of all deleted dives to be stored in the undo command QVector<struct dive*> deletedDives; //a list of all deleted dives to be stored in the undo command
for_each_dive (i, d) { for_each_dive (i, d) {
if (!d->selected) if (!d->selected)
continue; continue;

View file

@ -160,15 +160,15 @@ void RenumberDialog::buttonClicked(QAbstractButton *button)
{ {
if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) {
MainWindow::instance()->dive_list()->rememberSelection(); MainWindow::instance()->dive_list()->rememberSelection();
// we remember a map from dive uuid to a pair of old number / new number // we remember a list from dive uuid to a new number
QMap<int, QPair<int, int>> renumberedDives; QVector<QPair<int, int>> renumberedDives;
int i; int i;
int newNr = ui.spinBox->value(); int newNr = ui.spinBox->value();
struct dive *dive = NULL; struct dive *dive = NULL;
for_each_dive (i, dive) { for_each_dive (i, dive) {
if (!selectedOnly || dive->selected) { if (!selectedOnly || dive->selected) {
invalidate_dive_cache(dive); invalidate_dive_cache(dive);
renumberedDives.insert(dive->id, QPair<int, int>(dive->number, newNr++)); renumberedDives.append(QPair<int, int>(dive->id, newNr++));
} }
} }
UndoRenumberDives *undoCommand = new UndoRenumberDives(renumberedDives); UndoRenumberDives *undoCommand = new UndoRenumberDives(renumberedDives);
@ -241,7 +241,7 @@ void ShiftTimesDialog::buttonClicked(QAbstractButton *button)
// DANGER, DANGER - this could get our dive_table unsorted... // DANGER, DANGER - this could get our dive_table unsorted...
int i; int i;
struct dive *dive; struct dive *dive;
QList<int> affectedDives; QVector<int> affectedDives;
for_each_dive (i, dive) { for_each_dive (i, dive) {
if (!dive->selected) if (!dive->selected)
continue; continue;
@ -249,8 +249,6 @@ void ShiftTimesDialog::buttonClicked(QAbstractButton *button)
affectedDives.append(dive->id); affectedDives.append(dive->id);
} }
MainWindow::instance()->undoStack->push(new UndoShiftTime(affectedDives, amount)); MainWindow::instance()->undoStack->push(new UndoShiftTime(affectedDives, amount));
sort_table(&dive_table);
mark_divelist_changed(true);
MainWindow::instance()->dive_list()->rememberSelection(); MainWindow::instance()->dive_list()->rememberSelection();
MainWindow::instance()->refreshDisplay(); MainWindow::instance()->refreshDisplay();
MainWindow::instance()->dive_list()->restoreSelection(); MainWindow::instance()->dive_list()->restoreSelection();

View file

@ -3,170 +3,156 @@
#include "desktop-widgets/mainwindow.h" #include "desktop-widgets/mainwindow.h"
#include "core/divelist.h" #include "core/divelist.h"
#include "core/subsurface-string.h" #include "core/subsurface-string.h"
#include "core/gettextfromc.h"
UndoDeleteDive::UndoDeleteDive(QList<dive *> deletedDives) : diveList(deletedDives) UndoDeleteDive::UndoDeleteDive(const QVector<struct dive*> &divesToDeleteIn) : divesToDelete(divesToDeleteIn)
{ {
setText("delete dive"); setText(tr("delete %n dive(s)", "", divesToDelete.size()));
if (diveList.count() > 1)
setText(QString("delete %1 dives").arg(QString::number(diveList.count())));
} }
void UndoDeleteDive::undo() void UndoDeleteDive::undo()
{ {
// first bring back the trip(s) // first bring back the trip(s)
Q_FOREACH(struct dive_trip *trip, tripList) for (auto &trip: tripsToAdd) {
insert_trip(&trip); dive_trip *t = trip.release(); // Give up ownership
insert_trip(&t); // Return ownership to backend
}
tripsToAdd.clear();
// now walk the list of deleted dives for (DiveToAdd &d: divesToAdd) {
for (int i = 0; i < diveList.count(); i++) { if (d.trip)
struct dive *d = diveList.at(i); add_dive_to_trip(d.dive.get(), d.trip);
// we adjusted the divetrip to point to the "new" divetrip divesToDelete.append(d.dive.get()); // Delete dive on redo
if (d->divetrip) { add_single_dive(d.idx, d.dive.release()); // Return ownership to backend
struct dive_trip *trip = d->divetrip;
tripflag_t tripflag = d->tripflag; // this gets overwritten in add_dive_to_trip()
d->divetrip = NULL;
d->next = NULL;
d->pprev = NULL;
add_dive_to_trip(d, trip);
d->tripflag = tripflag;
}
record_dive(diveList.at(i));
} }
mark_divelist_changed(true); mark_divelist_changed(true);
tripList.clear(); divesToAdd.clear();
// Finally, do the UI stuff:
MainWindow::instance()->refreshDisplay(); MainWindow::instance()->refreshDisplay();
} }
void UndoDeleteDive::redo() void UndoDeleteDive::redo()
{ {
QList<struct dive*> newList; for (dive *d: divesToDelete) {
for (int i = 0; i < diveList.count(); i++) { int idx = get_divenr(d);
// make a copy of the dive before deleting it if (idx < 0) {
struct dive* d = alloc_dive(); qWarning() << "Deletion of unknown dive!";
copy_dive(diveList.at(i), d); continue;
newList.append(d);
// check for trip - if this is the last dive in the trip
// the trip will get deleted, so we need to remember it as well
if (d->divetrip && d->divetrip->nrdives == 1) {
dive_trip *undo_trip = clone_empty_trip(d->divetrip);
// update all the dives who were in this trip to point to the copy of the
// trip that we are about to delete implicitly when deleting its last dive below
Q_FOREACH(struct dive *inner_dive, newList) {
if (inner_dive->divetrip == d->divetrip)
inner_dive->divetrip = undo_trip;
}
d->divetrip = undo_trip;
tripList.append(undo_trip);
} }
//delete the dive // remove dive from trip - if this is the last dive in the trip
int nr; // remove the whole trip.
if ((nr = get_divenr(diveList.at(i))) >= 0) dive_trip *trip = unregister_dive_from_trip(d, false);
delete_single_dive(nr); if (trip && trip->nrdives == 0) {
unregister_trip(trip); // Remove trip from backend
tripsToAdd.emplace_back(trip); // Take ownership of trip
}
unregister_dive(idx); // Remove dive from backend
divesToAdd.push_back({ OwningDivePtr(d), trip, idx });
// Take ownership for dive
} }
divesToDelete.clear();
mark_divelist_changed(true); mark_divelist_changed(true);
// Finally, do the UI stuff:
MainWindow::instance()->refreshDisplay(); MainWindow::instance()->refreshDisplay();
diveList.clear();
diveList = newList;
} }
UndoShiftTime::UndoShiftTime(QList<int> changedDives, int amount) UndoShiftTime::UndoShiftTime(QVector<int> changedDives, int amount)
: diveList(changedDives), timeChanged(amount) : diveList(changedDives), timeChanged(amount)
{ {
setText("shift time"); setText(tr("delete %n dive(s)", "", changedDives.size()));
} }
void UndoShiftTime::undo() void UndoShiftTime::undo()
{ {
for (int i = 0; i < diveList.count(); i++) { for (int i = 0; i < diveList.count(); i++) {
struct dive* d = get_dive_by_uniq_id(diveList.at(i)); dive *d = get_dive_by_uniq_id(diveList.at(i));
d->when -= timeChanged; d->when -= timeChanged;
} }
// Changing times may have unsorted the dive table
sort_table(&dive_table);
mark_divelist_changed(true); mark_divelist_changed(true);
// Negate the time-shift so that the next call does the reverse
timeChanged = -timeChanged;
// Finally, do the UI stuff:
MainWindow::instance()->refreshDisplay(); MainWindow::instance()->refreshDisplay();
} }
void UndoShiftTime::redo() void UndoShiftTime::redo()
{ {
for (int i = 0; i < diveList.count(); i++) { // Same as undo(), since after undo() we reversed the timeOffset
struct dive* d = get_dive_by_uniq_id(diveList.at(i)); undo();
d->when += timeChanged;
}
mark_divelist_changed(true);
MainWindow::instance()->refreshDisplay();
} }
UndoRenumberDives::UndoRenumberDives(QMap<int, QPair<int, int> > originalNumbers) UndoRenumberDives::UndoRenumberDives(const QVector<QPair<int, int>> &divesToRenumberIn) : divesToRenumber(divesToRenumberIn)
{ {
oldNumbers = originalNumbers; setText(tr("renumber %n dive(s)", "", divesToRenumber.count()));
if (oldNumbers.count() > 1)
setText(QString("renumber %1 dives").arg(QString::number(oldNumbers.count())));
else
setText("renumber dive");
} }
void UndoRenumberDives::undo() void UndoRenumberDives::undo()
{ {
foreach (int key, oldNumbers.keys()) { for (auto &pair: divesToRenumber) {
struct dive* d = get_dive_by_uniq_id(key); dive *d = get_dive_by_uniq_id(pair.first);
d->number = oldNumbers.value(key).first; if (!d)
continue;
std::swap(d->number, pair.second);
} }
mark_divelist_changed(true); mark_divelist_changed(true);
// Finally, do the UI stuff:
MainWindow::instance()->refreshDisplay(); MainWindow::instance()->refreshDisplay();
} }
void UndoRenumberDives::redo() void UndoRenumberDives::redo()
{ {
foreach (int key, oldNumbers.keys()) { // Redo and undo do the same thing!
struct dive* d = get_dive_by_uniq_id(key); undo();
d->number = oldNumbers.value(key).second;
}
mark_divelist_changed(true);
MainWindow::instance()->refreshDisplay();
} }
UndoRemoveDivesFromTrip::UndoRemoveDivesFromTrip(const QVector<dive *> &divesToRemoveIn) : divesToRemove(divesToRemoveIn)
UndoRemoveDivesFromTrip::UndoRemoveDivesFromTrip(QMap<dive *, dive_trip *> removedDives)
{ {
divesToUndo = removedDives; setText(tr("remove %n dive(s) from trip", "", divesToRemove.size()));
setText("remove dive(s) from trip");
} }
void UndoRemoveDivesFromTrip::undo() void UndoRemoveDivesFromTrip::undo()
{ {
// first bring back the trip(s) // first bring back the trip(s)
Q_FOREACH(struct dive_trip *trip, tripList) for (auto &trip: tripsToAdd) {
insert_trip(&trip); dive_trip *t = trip.release(); // Give up ownership
tripList.clear(); insert_trip(&t); // Return ownership to backend
QMapIterator<dive*, dive_trip*> i(divesToUndo);
while (i.hasNext()) {
i.next();
add_dive_to_trip(i.key(), i.value());
} }
mark_divelist_changed(true); tripsToAdd.clear();
for (auto &pair: divesToAdd)
add_dive_to_trip(pair.first, pair.second);
divesToAdd.clear();
// Finally, do the UI stuff:
MainWindow::instance()->refreshDisplay(); MainWindow::instance()->refreshDisplay();
} }
void UndoRemoveDivesFromTrip::redo() void UndoRemoveDivesFromTrip::redo()
{ {
QMapIterator<dive*, dive_trip*> i(divesToUndo); for (dive *d: divesToRemove) {
while (i.hasNext()) { // remove dive from trip - if this is the last dive in the trip
i.next(); // remove the whole trip.
// If the trip will be deleted, remember it so that we can restore it later. dive_trip *trip = unregister_dive_from_trip(d, false);
dive_trip *trip = i.value(); if (!trip)
if (trip->nrdives == 1) { continue; // This was not part of a trip
dive_trip *cloned_trip = clone_empty_trip(trip); if (trip->nrdives == 0) {
tripList.append(cloned_trip); unregister_trip(trip); // Remove trip from backend
// Rewrite the dive list, such that the dives will be added to the resurrected trip. tripsToAdd.emplace_back(trip); // Take ownership of trip
for (dive_trip *&old_trip: divesToUndo) {
if (old_trip == trip)
old_trip = cloned_trip;
}
} }
remove_dive_from_trip(i.key(), false); divesToAdd.emplace_back(d, trip);
} }
mark_divelist_changed(true); mark_divelist_changed(true);
// Finally, do the UI stuff:
MainWindow::instance()->refreshDisplay(); MainWindow::instance()->refreshDisplay();
} }

View file

@ -2,50 +2,212 @@
#ifndef UNDOCOMMANDS_H #ifndef UNDOCOMMANDS_H
#define UNDOCOMMANDS_H #define UNDOCOMMANDS_H
#include "core/dive.h"
#include <QUndoCommand> #include <QUndoCommand>
#include <QMap> #include <QCoreApplication> // For Q_DECLARE_TR_FUNCTIONS
#include <QVector>
#include <memory>
// The classes declared in this file represent units-of-work, which can be exectuted / undone
// repeatedly. The command objects are collected in a linear list implemented in the QUndoStack class.
// They contain the information that is necessary to either perform or undo the unit-of-work.
// The usage is:
// constructor: generate information that is needed for executing the unit-of-work
// redo(): performs the unit-of-work and generates the information that is needed for undo()
// undo(): undos the unit-of-work and regenerates the initial information needed in redo()
// The needed information is mostly kept in pointers to dives and/or trips, which have to be added
// or removed.
// For this to work it is crucial that
// 1) Pointers to dives and trips remain valid as long as referencing command-objects exist.
// 2) The dive-table is not resorted, because dives are inserted at given indices.
//
// Thus, if a command deletes a dive or a trip, the actual object must not be deleted. Instead,
// the command object removes pointers to the dive/trip object from the backend and takes ownership.
// To reverse such a deletion, the object is re-injected into the backend and ownership is given up.
// Once ownership of a dive is taken, any reference to it was removed from the backend. Thus,
// subsequent redo()/undo() actions cannot access this object and integrity of the data is ensured.
//
// As an example, consider the following course of events: Dive 1 is renumbered and deleted, dive 2
// is added and renumbered. The undo list looks like this (---> non-owning, ***> owning pointers,
// ===> next item in list)
//
// Undo-List
// +-----------------+ +---------------+ +------------+ +-----------------+
// | Renumber dive 1 |====>| Delete dive 1 |====>| Add dive 2 |====>| Renumber dive 2 |
// +------------------ +---------------+ +------------+ +-----------------+
// | * | |
// | +--------+ * | +--------+ |
// +----->| Dive 1 |<****** +--->| Dive 2 |<------+
// +--------+ +--------+
// ^
// +---------+ *
// | Backend |****************
// +---------+
// Two points of note:
// 1) Every dive is owned by either the backend or exactly one command object.
// 2) All references to dive 1 are *before* the owner "delete dive 2", thus the pointer is always valid.
// 3) References by the backend are *always* owning.
//
// The user undos the last two commands. The situation now looks like this:
//
//
// Undo-List Redo-List
// +-----------------+ +---------------+ +------------+ +-----------------+
// | Renumber dive 1 |====>| Delete dive 1 | | Add dive 2 |<====| Renumber dive 2 |
// +------------------ +---------------+ +------------+ +-----------------+
// | * * |
// | +--------+ * * +--------+ |
// +----->| Dive 1 |<****** ****>| Dive 2 |<------+
// +--------+ +--------+
//
// +---------+
// | Backend |
// +---------+
// Again:
// 1) Every dive is owned by either the backend (here none) or exactly one command object.
// 2) All references to dive 1 are *before* the owner "delete dive 1", thus the pointer is always valid.
// 3) All references to dive 2 are *after* the owner "add dive 2", thus the pointer is always valid.
//
// The user undos one more command:
//
// Undo-List Redo-List
// +-----------------+ +---------------+ +------------+ +-----------------+
// | Renumber dive 1 | | Delete dive 1 |<====| Add dive 2 |<====| Renumber dive 2 |
// +------------------ +---------------+ +------------+ +-----------------+
// | | * |
// | +--------+ | * +--------+ |
// +----->| Dive 1 |<-----+ ****>| Dive 2 |<------+
// +--------+ +--------+
// ^
// * +---------+
// ***************| Backend |
// +---------+
// Same points as above.
// The user now adds a dive 3. The redo list will be deleted:
//
// Undo-List
// +-----------------+ +------------+
// | Renumber dive 1 |=============================================>| Add dive 3 |
// +------------------ +------------+
// | |
// | +--------+ +--------+ |
// +----->| Dive 1 | | Dive 3 |<---+
// +--------+ +--------+
// ^ ^
// * +---------+ *
// ***************| Backend |****************
// +---------+
// Note:
// 1) Dive 2 was deleted with the "add dive 2" command, because that was the owner.
// 2) Dive 1 was not deleted, because it is owned by the backend.
//
// To take ownership of dives/trips, the OnwingDivePtr and OwningTripPtr types are used. These
// are simply derived from std::unique_ptr and therefore use well-established semantics.
// Expressed in C-terms: std::unique_ptr<T> is exactly the same as T* with the following
// twists:
// 1) default-initialized to NULL.
// 2) if it goes out of scope (local scope or containing object destroyed), it does:
// if (ptr) free_function(ptr);
// whereby free_function can be configured (defaults to delete ptr).
// 3) assignment between two std::unique_ptr<T> compiles only if the source is reset (to NULL).
// (hence the name - there's a *unique* owner).
// While this sounds trivial, experience shows that this distinctly simplifies memory-management
// (it's not necessary to manually delete all vector items in the destructur, etc).
// Note that Qt's own implementation (QScoperPointer) is not up to the job, because it doesn't implement
// move-semantics and Qt's containers are incompatible, owing to COW semantics.
//
// Usage:
// OwningDivePtr dPtr; // Initialize to null-state: not owning any dive.
// OwningDivePtr dPtr(dive); // Take ownership of dive (which is of type struct dive *).
// // If dPtr goes out of scope, the dive will be freed with free_dive().
// struct dive *d = dPtr.release(); // Give up ownership of dive. dPtr is reset to null.
// struct dive *d = d.get(); // Get pointer dive, but don't release ownership.
// dPtr.reset(dive2); // Delete currently owned dive with free_dive() and get ownership of dive2.
// dPtr.reset(); // Delete currently owned dive and reset to null.
// dPtr2 = dPtr1; // Fails to compile.
// dPtr2 = std::move(dPtr1); // dPtr2 takes ownership, dPtr1 is reset to null.
// OwningDivePtr fun();
// dPtr1 = fun(); // Compiles. Simply put: the compiler knows that the result of fun() will
// // be trashed and therefore can be moved-from.
// std::vector<OwningDivePtr> v: // Define an empty vector of owning pointers.
// v.emplace_back(dive); // Take ownership of dive and add at end of vector
// // If the vector goes out of scope, all dives will be freed with free_dive().
// v.clear(v); // Reset the vector to zero length. If the elements weren't release()d,
// // the pointed-to dives are freed with free_dive()
// Classes used to automatically call free_dive()/free_trip for owning pointers that go out of scope.
struct DiveDeleter {
void operator()(dive *d) { free_dive(d); }
};
struct TripDeleter {
void operator()(dive_trip *t) { free_trip(t); }
};
// Owning pointers to dive and dive_trip objects.
typedef std::unique_ptr<dive, DiveDeleter> OwningDivePtr;
typedef std::unique_ptr<dive_trip, TripDeleter> OwningTripPtr;
class UndoDeleteDive : public QUndoCommand { class UndoDeleteDive : public QUndoCommand {
Q_DECLARE_TR_FUNCTIONS(Command)
public: public:
UndoDeleteDive(QList<struct dive*> deletedDives); UndoDeleteDive(const QVector<dive *> &divesToDelete);
private:
void undo() override; void undo() override;
void redo() override; void redo() override;
private: // For redo
QList<struct dive*> diveList; QVector<struct dive*> divesToDelete;
QList<struct dive_trip*> tripList;
// 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;
}; };
class UndoShiftTime : public QUndoCommand { class UndoShiftTime : public QUndoCommand {
Q_DECLARE_TR_FUNCTIONS(Command)
public: public:
UndoShiftTime(QList<int> changedDives, int amount); UndoShiftTime(QVector<int> changedDives, int amount);
private:
void undo() override; void undo() override;
void redo() override; void redo() override;
private: // For redo and undo
QList<int> diveList; QVector<int> diveList;
int timeChanged; int timeChanged;
}; };
class UndoRenumberDives : public QUndoCommand { class UndoRenumberDives : public QUndoCommand {
Q_DECLARE_TR_FUNCTIONS(Command)
public: public:
UndoRenumberDives(QMap<int, QPair<int, int> > originalNumbers); UndoRenumberDives(const QVector<QPair<int, int>> &divesToRenumber);
private:
void undo() override; void undo() override;
void redo() override; void redo() override;
private: // For redo and undo: pairs of dive-id / new number
QMap<int,QPair<int, int> > oldNumbers; QVector<QPair<int, int>> divesToRenumber;
}; };
class UndoRemoveDivesFromTrip : public QUndoCommand { class UndoRemoveDivesFromTrip : public QUndoCommand {
Q_DECLARE_TR_FUNCTIONS(Command)
public: public:
UndoRemoveDivesFromTrip(QMap<struct dive*, dive_trip*> removedDives); UndoRemoveDivesFromTrip(const QVector<dive *> &divesToRemove);
private:
void undo() override; void undo() override;
void redo() override; void redo() override;
private: // For redo
QMap<struct dive*, dive_trip*> divesToUndo; QVector<dive *> divesToRemove;
QList<struct dive_trip*> tripList;
// For undo
std::vector<std::pair<dive *, dive_trip *>> divesToAdd;
std::vector<OwningTripPtr> tripsToAdd;
}; };
#endif // UNDOCOMMANDS_H #endif // UNDOCOMMANDS_H