// SPDX-License-Identifier: GPL-2.0 #include "command_edit.h" #include "command_private.h" #include "core/divelist.h" #include "core/qthelper.h" // for copy_qstring #include "core/subsurface-string.h" #include "core/tag.h" #include "desktop-widgets/mapwidget.h" // TODO: Replace desktop-dependency by signal namespace Command { static std::vector getDives(bool currentDiveOnly) { if (currentDiveOnly) return current_dive ? std::vector { current_dive } : std::vector { }; std::vector res; struct dive *d; int i; for_each_dive (i, d) { if (d->selected) res.push_back(d); } return res; } EditDivesBase::EditDivesBase(bool currentDiveOnly) : dives(getDives(currentDiveOnly)), selectedDives(getDiveSelection()), current(current_dive) { } EditDivesBase::EditDivesBase(dive *d) : dives({ d }), selectedDives(getDiveSelection()), current(current_dive) { } int EditDivesBase::numDives() const { return dives.size(); } template EditBase::EditBase(T newValue, bool currentDiveOnly) : EditDivesBase(currentDiveOnly), value(std::move(newValue)) { } template EditBase::EditBase(T newValue, dive *d) : EditDivesBase(d), value(std::move(newValue)) { } // This is quite hackish: we can't use virtual functions in the constructor and // therefore can't initialize the list of dives [the values of the dives are // accessed by virtual functions]. Therefore, we (mis)use the fact that workToBeDone() // is called exactly once before adding the Command to the system and perform this here. // To be more explicit about this, we might think about renaming workToBeDone() to init(). template bool EditBase::workToBeDone() { // First, let's fetch the old value, i.e. the value of the current dive. // If no current dive exists, bail. if (!current) return false; old = data(current); // If there is no change - do nothing. if (old == value) return false; std::vector divesNew; divesNew.reserve(dives.size()); for (dive *d: dives) { if (data(d) == old) divesNew.push_back(d); } dives = std::move(divesNew); // Create a text for the menu entry. In the case of multiple dives add the number size_t num_dives = dives.size(); if (num_dives > 0) //: remove the part in parentheses for %n = 1 setText(tr("Edit %1 (%n dive(s))", "", num_dives).arg(fieldName())); return num_dives > 0; } template void EditBase::undo() { if (dives.empty()) { qWarning("Edit command called with empty dives list (shouldn't happen)"); return; } for (dive *d: dives) { set(d, value); invalidate_dive_cache(d); // Ensure that dive is written in git_save() } std::swap(old, value); // Send signals. DiveField id = fieldId(); emit diveListNotifier.divesChanged(QVector::fromStdVector(dives), id); setSelection(selectedDives, current); } // We have to manually instantiate the constructors of the EditBase class, // because the base class is never constructed and the derived classes // don't have their own constructor. They simply delegate to the base // class by virtue of a "using" declaration. template EditBase::EditBase(QString newValue, bool currentDiveOnly); template EditBase::EditBase(int newValue, bool currentDiveOnly); template EditBase::EditBase(struct dive_site *newValue, bool currentDiveOnly); // Undo and redo do the same as just the stored value is exchanged template void EditBase::redo() { // Note: here, we explicitly call the undo function of EditBase and don't do // virtual dispatch. Thus, derived classes can call this redo function without // having their own undo() function called. EditBase::undo(); } // Implementation of virtual functions // ***** Notes ***** void EditNotes::set(struct dive *d, QString s) const { free(d->notes); d->notes = strdup(qPrintable(s)); } QString EditNotes::data(struct dive *d) const { return QString(d->notes); } QString EditNotes::fieldName() const { return tr("notes"); } DiveField EditNotes::fieldId() const { return DiveField::NOTES; } // ***** Suit ***** void EditSuit::set(struct dive *d, QString s) const { free(d->suit); d->suit = strdup(qPrintable(s)); } QString EditSuit::data(struct dive *d) const { return QString(d->suit); } QString EditSuit::fieldName() const { return tr("suit"); } DiveField EditSuit::fieldId() const { return DiveField::SUIT; } // ***** Rating ***** void EditRating::set(struct dive *d, int value) const { d->rating = value; } int EditRating::data(struct dive *d) const { return d->rating; } QString EditRating::fieldName() const { return tr("rating"); } DiveField EditRating::fieldId() const { return DiveField::RATING; } // ***** Visibility ***** void EditVisibility::set(struct dive *d, int value) const { d->visibility = value; } int EditVisibility::data(struct dive *d) const { return d->visibility; } QString EditVisibility::fieldName() const { return tr("visibility"); } DiveField EditVisibility::fieldId() const { return DiveField::VISIBILITY; } // ***** Air Temperature ***** void EditAirTemp::set(struct dive *d, int value) const { d->airtemp.mkelvin = value > 0 ? (uint32_t)value : 0u; } int EditAirTemp::data(struct dive *d) const { return (int)d->airtemp.mkelvin; } QString EditAirTemp::fieldName() const { return tr("air temperature"); } DiveField EditAirTemp::fieldId() const { return DiveField::AIR_TEMP; } // ***** Water Temperature ***** void EditWaterTemp::set(struct dive *d, int value) const { d->watertemp.mkelvin = value > 0 ? (uint32_t)value : 0u; // re-populate the temperatures - easiest way to do this is by calling fixup_dive fixup_dive(d); } int EditWaterTemp::data(struct dive *d) const { return (int)d->watertemp.mkelvin; } QString EditWaterTemp::fieldName() const { return tr("water temperature"); } DiveField EditWaterTemp::fieldId() const { return DiveField::WATER_TEMP; } // ***** Atmospheric pressure ***** void EditAtmPress::set(struct dive *d, int value) const { d->surface_pressure.mbar = value > 0 ? (uint32_t)value : 0u; } int EditAtmPress::data(struct dive *d) const { return (int)d->surface_pressure.mbar; } QString EditAtmPress::fieldName() const { return tr("Atm. pressure"); } DiveField EditAtmPress::fieldId() const { return DiveField::ATM_PRESS; } // ***** Duration ***** void EditDuration::set(struct dive *d, int value) const { d->dc.duration.seconds = value; d->duration = d->dc.duration; d->dc.meandepth.mm = 0; d->dc.samples = 0; } int EditDuration::data(struct dive *d) const { return d->duration.seconds; } QString EditDuration::fieldName() const { return tr("duration"); } DiveField EditDuration::fieldId() const { return DiveField::DURATION; } // ***** Depth ***** void EditDepth::set(struct dive *d, int value) const { d->dc.maxdepth.mm = value; d->maxdepth = d->dc.maxdepth; d->dc.meandepth.mm = 0; d->dc.samples = 0; } int EditDepth::data(struct dive *d) const { return d->maxdepth.mm; } QString EditDepth::fieldName() const { return tr("depth"); } DiveField EditDepth::fieldId() const { return DiveField::DEPTH; } // ***** DiveSite ***** void EditDiveSite::set(struct dive *d, struct dive_site *dive_site) const { unregister_dive_from_dive_site(d); add_dive_to_dive_site(d, dive_site); } struct dive_site *EditDiveSite::data(struct dive *d) const { return d->dive_site; } QString EditDiveSite::fieldName() const { return tr("dive site"); } DiveField EditDiveSite::fieldId() const { return DiveField::DIVESITE; } void EditDiveSite::undo() { // Do the normal undo thing, then send dive site changed signals EditBase::undo(); if (value) emit diveListNotifier.diveSiteDivesChanged(value); if (old) emit diveListNotifier.diveSiteDivesChanged(old); } void EditDiveSite::redo() { EditDiveSite::undo(); // Undo and redo do the same } static struct dive_site *createDiveSite(const QString &name) { struct dive_site *ds = alloc_dive_site(); struct dive_site *old = current_dive ? current_dive->dive_site : nullptr; if (old) { copy_dive_site(old, ds); free(ds->name); // Free name, as we will overwrite it with our own version } // If the current dive has a location, use that as location for the new dive site if (current_dive) { location_t loc = dive_get_gps_location(current_dive); if (has_location(&loc)) ds->location = loc; } ds->name = copy_qstring(name); return ds; } EditDiveSiteNew::EditDiveSiteNew(const QString &newName, bool currentDiveOnly) : EditDiveSite(createDiveSite(newName), currentDiveOnly), diveSiteToAdd(value), diveSiteToRemove(nullptr) { } void EditDiveSiteNew::undo() { EditDiveSite::undo(); int idx = unregister_dive_site(diveSiteToRemove); diveSiteToAdd.reset(diveSiteToRemove); emit diveListNotifier.diveSiteDeleted(diveSiteToRemove, idx); // Inform frontend of removed dive site. diveSiteToRemove = nullptr; } void EditDiveSiteNew::redo() { diveSiteToRemove = diveSiteToAdd.get(); int idx = register_dive_site(diveSiteToAdd.release()); // Return ownership to backend. emit diveListNotifier.diveSiteAdded(diveSiteToRemove, idx); // Inform frontend of new dive site. EditDiveSite::redo(); } // ***** Mode ***** // Editing the dive mode has very peculiar semantics for historic reasons: // Since the dive-mode depends on the dive computer, the i-th dive computer // of each dive will be edited. If the dive has less than i dive computers, // the default dive computer will be edited. // The index "i" will be stored as an additional payload with the command. // Thus, we need an explicit constructor. Since the actual handling is done // by the base class, which knows nothing about this index, it will not be // sent via signals. Currently this is not needed. Should it turn out to // become necessary, then we might either // - Not derive EditMode from EditBase. // - Change the semantics of the mode-editing. // The future will tell. EditMode::EditMode(int indexIn, int newValue, bool currentDiveOnly) : EditBase(newValue, currentDiveOnly), index(indexIn) { } void EditMode::set(struct dive *d, int i) const { get_dive_dc(d, index)->divemode = (enum divemode_t)i; update_setpoint_events(d, get_dive_dc(d, index)); } int EditMode::data(struct dive *d) const { return get_dive_dc(d, index)->divemode; } QString EditMode::fieldName() const { return tr("dive mode"); } DiveField EditMode::fieldId() const { return DiveField::MODE; } // ***** Number ***** void EditNumber::set(struct dive *d, int number) const { d->number = number; } int EditNumber::data(struct dive *d) const { return d->number; } QString EditNumber::fieldName() const { return tr("number"); } DiveField EditNumber::fieldId() const { return DiveField::NR; } // ***** Tag based commands ***** EditTagsBase::EditTagsBase(const QStringList &newListIn, bool currentDiveOnly) : EditDivesBase(currentDiveOnly), newList(newListIn) { } // Two helper functions: returns true if first list contains any tag or // misses any tag of second list. static bool containsAny(const QStringList &tags1, const QStringList &tags2) { return std::any_of(tags2.begin(), tags2.end(), [&tags1](const QString &tag) { return tags1.contains(tag); }); } static bool missesAny(const QStringList &tags1, const QStringList &tags2) { return std::any_of(tags2.begin(), tags2.end(), [&tags1](const QString &tag) { return !tags1.contains(tag); }); } // This is quite hackish: we can't use virtual functions in the constructor and // therefore can't initialize the list of dives [the values of the dives are // accessed by virtual functions]. Therefore, we (mis)use the fact that workToBeDone() // is called exactly once before adding the Command to the system and perform this here. // To be more explicit about this, we might think about renaming workToBeDone() to init(). bool EditTagsBase::workToBeDone() { // changing the tags on multiple dives is semantically strange - what's the right thing to do? // here's what I think... add the tags that were added to the displayed dive and remove the tags // that were removed from it // If there is no current dive, bail. if (!current) return false; // Calculate tags to add and tags to remove QStringList oldList = data(current); for (const QString &s: newList) { if (!oldList.contains(s)) tagsToAdd.push_back(s); } for (const QString &s: oldList) { if (!newList.contains(s)) tagsToRemove.push_back(s); } // Now search for all dives that either // - miss a tag to be added // - have a tag to be removed std::vector divesNew; divesNew.reserve(dives.size()); for (dive *d: dives) { QStringList tags = data(d); if (missesAny(tags, tagsToAdd) || containsAny(tags, tagsToRemove)) divesNew.push_back(d); } dives = std::move(divesNew); // Create a text for the menu entry. In the case of multiple dives add the number size_t num_dives = dives.size(); if (num_dives > 0) //: remove the part in parentheses for %n = 1 setText(tr("Edit %1 (%n dive(s))", "", num_dives).arg(fieldName())); return num_dives != 0; } void EditTagsBase::undo() { if (dives.empty()) { qWarning("Edit command called with empty dives list (shouldn't happen)"); return; } for (dive *d: dives) { QStringList tags = data(d); for (const QString &tag: tagsToRemove) tags.removeAll(tag); for (const QString &tag: tagsToAdd) { if (!tags.contains(tag)) tags.push_back(tag); } invalidate_dive_cache(d); // Ensure that dive is written in git_save() set(d, tags); } std::swap(tagsToAdd, tagsToRemove); // Send signals. DiveField id = fieldId(); emit diveListNotifier.divesChanged(QVector::fromStdVector(dives), id); setSelection(selectedDives, current); } // Undo and redo do the same as just the stored value is exchanged void EditTagsBase::redo() { undo(); } // ***** Tags ***** QStringList EditTags::data(struct dive *d) const { QStringList res; for (const struct tag_entry *tag = d->tag_list; tag; tag = tag->next) res.push_back(tag->tag->name); return res; } void EditTags::set(struct dive *d, const QStringList &v) const { taglist_free(d->tag_list); d->tag_list = NULL; for (const QString &tag: v) taglist_add_tag(&d->tag_list, qPrintable(tag)); taglist_cleanup(&d->tag_list); } QString EditTags::fieldName() const { return tr("tags"); } DiveField EditTags::fieldId() const { return DiveField::TAGS; } // String list helper static QStringList stringToList(const QString &s) { QStringList res = s.split(",", QString::SkipEmptyParts); for (QString &str: res) str = str.trimmed(); return res; } // ***** Buddies ***** QStringList EditBuddies::data(struct dive *d) const { return stringToList(d->buddy); } void EditBuddies::set(struct dive *d, const QStringList &v) const { QString text = v.join(", "); free(d->buddy); d->buddy = copy_qstring(text); } QString EditBuddies::fieldName() const { return tr("buddies"); } DiveField EditBuddies::fieldId() const { return DiveField::BUDDY; } // ***** DiveMaster ***** QStringList EditDiveMaster::data(struct dive *d) const { return stringToList(d->divemaster); } void EditDiveMaster::set(struct dive *d, const QStringList &v) const { QString text = v.join(", "); free(d->divemaster); d->divemaster = copy_qstring(text); } QString EditDiveMaster::fieldName() const { return tr("dive master"); } DiveField EditDiveMaster::fieldId() const { return DiveField::DIVEMASTER; } // Helper function to copy cylinders. This supposes that the destination // cylinder is uninitialized. I.e. the old description is not freed! static void copy_cylinder(const cylinder_t &s, cylinder_t &d) { d.type.description = copy_string(s.type.description); d.type.size = s.type.size; d.type.workingpressure = s.type.workingpressure; d.gasmix = s.gasmix; d.cylinder_use = s.cylinder_use; d.depth = s.depth; } static void swapCandQString(QString &q, char *&c) { QString tmp(c); free(c); c = copy_qstring(q); q = std::move(tmp); } PasteState::PasteState(dive *dIn, const dive *data, dive_components what) : d(dIn), tags(nullptr) { memset(&cylinders[0], 0, sizeof(cylinders)); memset(&weightsystems, 0, sizeof(weightsystems)); if (what.notes) notes = data->notes; if (what.divemaster) divemaster = data->divemaster; if (what.buddy) buddy = data->buddy; if (what.suit) suit = data->suit; if (what.rating) rating = data->rating; if (what.visibility) visibility = data->visibility; if (what.divesite) divesite = data->dive_site; if (what.tags) tags = taglist_copy(data->tag_list); if (what.cylinders) { for (int i = 0; i < MAX_CYLINDERS; ++i) copy_cylinder(data->cylinder[i], cylinders[i]); } if (what.weights) copy_weights(&data->weightsystems, &weightsystems); } PasteState::~PasteState() { taglist_free(tags); for (cylinder_t &c: cylinders) free((void *)c.type.description); clear_weightsystem_table(&weightsystems); free(weightsystems.weightsystems); } void PasteState::swap(dive_components what) { if (what.notes) swapCandQString(notes, d->notes); if (what.divemaster) swapCandQString(divemaster, d->divemaster); if (what.buddy) swapCandQString(buddy, d->buddy); if (what.suit) swapCandQString(suit, d->suit); if (what.rating) std::swap(rating, d->rating); if (what.visibility) std::swap(visibility, d->visibility); if (what.divesite) std::swap(divesite, d->dive_site); if (what.tags) std::swap(tags, d->tag_list); if (what.cylinders) std::swap(cylinders, d->cylinder); if (what.weights) std::swap(weightsystems, d->weightsystems); } // ***** Paste ***** PasteDives::PasteDives(const dive *data, dive_components whatIn) : what(whatIn), current(current_dive) { std::vector selection = getDiveSelection(); dives.reserve(selection.size()); for (dive *d: selection) dives.emplace_back(d, data, what); setText(tr("Paste onto %n dive(s)", "", dives.size())); } bool PasteDives::workToBeDone() { return !dives.empty(); } void PasteDives::undo() { bool diveSiteListChanged = false; // If we had taken ownership of dive sites, readd them to the system for (OwningDiveSitePtr &ds: ownedDiveSites) { register_dive_site(ds.release()); diveSiteListChanged = true; } ownedDiveSites.clear(); QVector divesToNotify; // Remember dives so that we can send signals later divesToNotify.reserve(dives.size()); for (PasteState &state: dives) { divesToNotify.push_back(state.d); state.swap(what); invalidate_dive_cache(state.d); // Ensure that dive is written in git_save() } // If dive sites were pasted, collect all overwritten dive sites // and remove those which don't have users anymore from the core. // But keep an owning pointer. Thus if this undo command is freed, the // dive-site will be automatically deleted and on redo() it can be // readded to the system if (what.divesite) { std::vector divesites; for (const PasteState &d: dives) { if (std::find(divesites.begin(), divesites.end(), d.divesite) == divesites.end()) divesites.push_back(d.divesite); } for (dive_site *ds: divesites) { unregister_dive_site(ds); ownedDiveSites.emplace_back(ds); diveSiteListChanged = true; } } // Send signals. // TODO: We send one signal per changed field. This means that the dive list may // update the entry numerous times. Perhaps change the field-id into flags? // There seems to be a number of enums / flags describing dive fields. Perhaps unify them all? if (what.notes) emit diveListNotifier.divesChanged(divesToNotify, DiveField::NOTES); if (what.divemaster) emit diveListNotifier.divesChanged(divesToNotify, DiveField::DIVEMASTER); if (what.buddy) emit diveListNotifier.divesChanged(divesToNotify, DiveField::BUDDY); if (what.suit) emit diveListNotifier.divesChanged(divesToNotify, DiveField::SUIT); if (what.rating) emit diveListNotifier.divesChanged(divesToNotify, DiveField::RATING); if (what.visibility) emit diveListNotifier.divesChanged(divesToNotify, DiveField::VISIBILITY); if (what.divesite) emit diveListNotifier.divesChanged(divesToNotify, DiveField::DIVESITE); if (what.tags) emit diveListNotifier.divesChanged(divesToNotify, DiveField::TAGS); if (what.cylinders) emit diveListNotifier.cylindersReset(divesToNotify); if (what.weights) emit diveListNotifier.weightsystemsReset(divesToNotify); if (diveSiteListChanged) MapWidget::instance()->reload(); } // Redo and undo do the same void PasteDives::redo() { undo(); } // ***** Paste ***** ReplanDive::ReplanDive(dive *source) : d(current_dive), dc({ 0 }), notes(nullptr) { memset(&cylinders[0], 0, sizeof(cylinders)); if (!d) return; when = source->when; maxdepth = source->maxdepth; meandepth = source->meandepth; notes = copy_string(source->notes); duration = source->duration; salinity = source->salinity; surface_pressure = source->surface_pressure; // This resets the dive computers and cylinders of the source dive, avoiding deep copies. std::swap(source->cylinder, cylinders); std::swap(source->dc, dc); setText(tr("Replan dive")); } ReplanDive::~ReplanDive() { for (cylinder_t &c: cylinders) free((void *)c.type.description); free_dive_dcs(&dc); free(notes); } bool ReplanDive::workToBeDone() { return !!d; } void ReplanDive::undo() { std::swap(d->when, when); std::swap(d->maxdepth, maxdepth); std::swap(d->meandepth, meandepth); std::swap(d->cylinder, cylinders); std::swap(d->dc, dc); std::swap(d->notes, notes); std::swap(d->surface_pressure, surface_pressure); std::swap(d->duration, duration); std::swap(d->salinity, salinity); fixup_dive(d); QVector divesToNotify = { d }; // TODO: Turn field into flags to avoid multiple signals emit diveListNotifier.divesChanged(divesToNotify, DiveField::DATETIME); emit diveListNotifier.divesChanged(divesToNotify, DiveField::DURATION); emit diveListNotifier.divesChanged(divesToNotify, DiveField::DEPTH); emit diveListNotifier.divesChanged(divesToNotify, DiveField::MODE); emit diveListNotifier.divesChanged(divesToNotify, DiveField::NOTES); emit diveListNotifier.divesChanged(divesToNotify, DiveField::SALINITY); emit diveListNotifier.divesChanged(divesToNotify, DiveField::ATM_PRESS); emit diveListNotifier.cylindersReset(divesToNotify); } // Redo and undo do the same void ReplanDive::redo() { undo(); } } // namespace Command