2019-01-25 17:27:31 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
|
|
|
|
|
|
|
#include "command_edit.h"
|
|
|
|
#include "core/divelist.h"
|
core: introduce divelog structure
The parser API was very annoying, as a number of tables
to-be-filled were passed in as pointers. The goal of this
commit is to collect all these tables in a single struct.
This should make it (more or less) clear what is actually
written into the divelog files.
Moreover, it should now be rather easy to search for
instances, where the global logfile is accessed (and it
turns out that there are many!).
The divelog struct does not contain the tables as substructs,
but only collects pointers. The idea is that the "divelog.h"
file can be included without all the other files describing
the numerous tables.
To make it easier to use from C++ parts of the code, the
struct implements a constructor and a destructor. Sadly,
we can't use smart pointers, since the pointers are accessed
from C code. Therfore the constructor and destructor are
quite complex.
The whole commit is large, but was mostly an automatic
conversion.
One oddity of note: the divelog structure also contains
the "autogroup" flag, since that is saved in the divelog.
This actually fixes a bug: Before, when importing dives
from a different log, the autogroup flag was overwritten.
This was probably not intended and does not happen anymore.
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2022-11-08 20:31:08 +00:00
|
|
|
#include "core/divelog.h"
|
2020-03-16 22:10:47 +00:00
|
|
|
#include "core/fulltext.h"
|
2019-03-20 20:46:58 +00:00
|
|
|
#include "core/qthelper.h" // for copy_qstring
|
2022-02-19 17:47:25 +00:00
|
|
|
#include "core/sample.h"
|
2019-11-24 12:26:29 +00:00
|
|
|
#include "core/selection.h"
|
2019-02-23 17:17:20 +00:00
|
|
|
#include "core/subsurface-string.h"
|
2019-05-30 16:29:36 +00:00
|
|
|
#include "core/tag.h"
|
2019-11-08 21:47:38 +00:00
|
|
|
#include "qt-models/weightsysteminfomodel.h"
|
2020-02-23 10:43:50 +00:00
|
|
|
#include "qt-models/tankinfomodel.h"
|
2020-01-10 00:25:37 +00:00
|
|
|
#ifdef SUBSURFACE_MOBILE
|
|
|
|
#include "qt-models/divelocationmodel.h"
|
|
|
|
#endif
|
2019-01-25 17:27:31 +00:00
|
|
|
|
|
|
|
namespace Command {
|
|
|
|
|
2020-03-21 16:36:58 +00:00
|
|
|
template <typename T, DiveField::Flags ID>
|
|
|
|
DiveField EditTemplate<T, ID>::fieldId() const
|
|
|
|
{
|
|
|
|
return ID;
|
|
|
|
}
|
|
|
|
|
|
|
|
template <DiveField::Flags ID>
|
|
|
|
DiveField EditTagsTemplate<ID>::fieldId() const
|
|
|
|
{
|
|
|
|
return ID;
|
|
|
|
}
|
|
|
|
|
2020-03-21 17:10:54 +00:00
|
|
|
template <typename T, DiveField::Flags ID, T dive::*PTR>
|
|
|
|
void EditDefaultSetter<T, ID, PTR>::set(struct dive *d, T v) const
|
|
|
|
{
|
|
|
|
d->*PTR = v;
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T, DiveField::Flags ID, T dive::*PTR>
|
|
|
|
T EditDefaultSetter<T, ID, PTR>::data(struct dive *d) const
|
|
|
|
{
|
|
|
|
return d->*PTR;
|
|
|
|
}
|
|
|
|
|
2020-03-21 17:30:49 +00:00
|
|
|
template <DiveField::Flags ID, char *dive::*PTR>
|
|
|
|
void EditStringSetter<ID, PTR>::set(struct dive *d, QString v) const
|
|
|
|
{
|
|
|
|
free(d->*PTR);
|
|
|
|
d->*PTR = copy_qstring(v);
|
|
|
|
}
|
|
|
|
|
|
|
|
template <DiveField::Flags ID, char *dive::*PTR>
|
|
|
|
QString EditStringSetter<ID, PTR>::data(struct dive *d) const
|
|
|
|
{
|
|
|
|
return QString(d->*PTR);
|
|
|
|
}
|
|
|
|
|
2019-02-18 20:30:11 +00:00
|
|
|
static std::vector<dive *> getDives(bool currentDiveOnly)
|
2019-02-14 22:07:12 +00:00
|
|
|
{
|
|
|
|
if (currentDiveOnly)
|
|
|
|
return current_dive ? std::vector<dive *> { current_dive }
|
|
|
|
: std::vector<dive *> { };
|
|
|
|
|
|
|
|
std::vector<dive *> res;
|
|
|
|
struct dive *d;
|
|
|
|
int i;
|
|
|
|
for_each_dive (i, d) {
|
|
|
|
if (d->selected)
|
|
|
|
res.push_back(d);
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2019-05-23 18:27:19 +00:00
|
|
|
EditDivesBase::EditDivesBase(bool currentDiveOnly) :
|
2019-02-18 20:30:11 +00:00
|
|
|
dives(getDives(currentDiveOnly)),
|
|
|
|
selectedDives(getDiveSelection()),
|
2019-02-14 22:07:12 +00:00
|
|
|
current(current_dive)
|
2019-01-25 17:27:31 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2019-07-17 13:49:45 +00:00
|
|
|
EditDivesBase::EditDivesBase(dive *d) :
|
|
|
|
dives({ d }),
|
|
|
|
selectedDives(getDiveSelection()),
|
|
|
|
current(current_dive)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2019-05-23 18:27:19 +00:00
|
|
|
int EditDivesBase::numDives() const
|
|
|
|
{
|
|
|
|
return dives.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
template<typename T>
|
|
|
|
EditBase<T>::EditBase(T newValue, bool currentDiveOnly) :
|
|
|
|
EditDivesBase(currentDiveOnly),
|
|
|
|
value(std::move(newValue))
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2019-07-17 13:49:45 +00:00
|
|
|
template<typename T>
|
|
|
|
EditBase<T>::EditBase(T newValue, dive *d) :
|
|
|
|
EditDivesBase(d),
|
|
|
|
value(std::move(newValue))
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2019-01-25 17:27:31 +00:00
|
|
|
// 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<typename T>
|
|
|
|
bool EditBase<T>::workToBeDone()
|
|
|
|
{
|
2019-02-14 22:07:12 +00:00
|
|
|
// 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;
|
|
|
|
|
2019-01-25 17:27:31 +00:00
|
|
|
std::vector<dive *> 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();
|
2019-12-04 14:30:59 +00:00
|
|
|
if (num_dives == 1)
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Edit %1").arg(fieldName())).arg(diveNumberOrDate(dives[0])));
|
2019-12-04 14:30:59 +00:00
|
|
|
else if (num_dives > 0)
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Edit %1 (%n dive(s))", "", num_dives).arg(fieldName())).arg(getListOfDives(dives)));
|
2019-01-25 17:27:31 +00:00
|
|
|
|
2019-02-14 22:07:12 +00:00
|
|
|
return num_dives > 0;
|
2019-01-25 17:27:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
template<typename T>
|
|
|
|
void EditBase<T>::undo()
|
|
|
|
{
|
|
|
|
if (dives.empty()) {
|
|
|
|
qWarning("Edit command called with empty dives list (shouldn't happen)");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (dive *d: dives) {
|
|
|
|
set(d, value);
|
2020-03-16 22:10:47 +00:00
|
|
|
fulltext_register(d); // Update the fulltext cache
|
2019-01-25 17:27:31 +00:00
|
|
|
invalidate_dive_cache(d); // Ensure that dive is written in git_save()
|
|
|
|
}
|
|
|
|
|
|
|
|
std::swap(old, value);
|
|
|
|
|
2019-02-11 14:34:43 +00:00
|
|
|
// Send signals.
|
|
|
|
DiveField id = fieldId();
|
2020-10-25 21:42:40 +00:00
|
|
|
emit diveListNotifier.divesChanged(stdToQt<dive *>(dives), id);
|
2022-02-14 20:59:49 +00:00
|
|
|
if (!placingCommand())
|
2022-04-04 16:57:28 +00:00
|
|
|
setSelection(selectedDives, current, -1);
|
2019-01-25 17:27:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2019-02-14 22:07:12 +00:00
|
|
|
EditBase<QString>::EditBase(QString newValue, bool currentDiveOnly);
|
2019-01-28 21:35:07 +00:00
|
|
|
template
|
2019-02-14 22:07:12 +00:00
|
|
|
EditBase<int>::EditBase(int newValue, bool currentDiveOnly);
|
2019-03-20 20:46:58 +00:00
|
|
|
template
|
2020-03-21 17:10:54 +00:00
|
|
|
EditBase<bool>::EditBase(bool newValue, bool currentDiveOnly);
|
|
|
|
template
|
2019-02-14 22:07:12 +00:00
|
|
|
EditBase<struct dive_site *>::EditBase(struct dive_site *newValue, bool currentDiveOnly);
|
2019-01-25 17:27:31 +00:00
|
|
|
|
|
|
|
// Undo and redo do the same as just the stored value is exchanged
|
|
|
|
template<typename T>
|
|
|
|
void EditBase<T>::redo()
|
|
|
|
{
|
2019-03-20 20:46:58 +00:00
|
|
|
// Note: here, we explicitly call the undo function of EditBase<T> and don't do
|
|
|
|
// virtual dispatch. Thus, derived classes can call this redo function without
|
|
|
|
// having their own undo() function called.
|
|
|
|
EditBase<T>::undo();
|
2019-01-25 17:27:31 +00:00
|
|
|
}
|
|
|
|
|
2019-01-28 17:35:27 +00:00
|
|
|
// Implementation of virtual functions
|
|
|
|
|
|
|
|
// ***** Notes *****
|
2019-01-25 17:27:31 +00:00
|
|
|
QString EditNotes::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("notes");
|
2019-01-25 17:27:31 +00:00
|
|
|
}
|
|
|
|
|
2019-01-28 20:42:59 +00:00
|
|
|
// ***** Suit *****
|
|
|
|
QString EditSuit::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("suit");
|
2019-01-28 20:42:59 +00:00
|
|
|
}
|
|
|
|
|
2019-01-28 21:35:07 +00:00
|
|
|
// ***** Rating *****
|
|
|
|
QString EditRating::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("rating");
|
2019-01-28 21:35:07 +00:00
|
|
|
}
|
|
|
|
|
2019-01-30 21:13:24 +00:00
|
|
|
// ***** Visibility *****
|
2019-01-28 21:35:07 +00:00
|
|
|
QString EditVisibility::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("visibility");
|
2019-01-28 21:35:07 +00:00
|
|
|
}
|
|
|
|
|
2019-11-28 19:04:52 +00:00
|
|
|
// ***** WaveSize *****
|
|
|
|
QString EditWaveSize::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("wavesize");
|
2019-11-28 19:04:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ***** Current *****
|
|
|
|
QString EditCurrent::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("current");
|
2019-11-28 19:04:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ***** Surge *****
|
|
|
|
QString EditSurge::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("surge");
|
2019-11-28 19:04:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ***** Chill *****
|
|
|
|
QString EditChill::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("chill");
|
2019-11-28 19:04:52 +00:00
|
|
|
}
|
|
|
|
|
2019-01-30 21:13:24 +00:00
|
|
|
// ***** 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
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("air temperature");
|
2019-01-30 21:13:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ***** Water Temperature *****
|
|
|
|
void EditWaterTemp::set(struct dive *d, int value) const
|
|
|
|
{
|
|
|
|
d->watertemp.mkelvin = value > 0 ? (uint32_t)value : 0u;
|
2019-02-24 20:46:11 +00:00
|
|
|
|
|
|
|
// re-populate the temperatures - easiest way to do this is by calling fixup_dive
|
|
|
|
fixup_dive(d);
|
2019-01-30 21:13:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
int EditWaterTemp::data(struct dive *d) const
|
|
|
|
{
|
|
|
|
return (int)d->watertemp.mkelvin;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString EditWaterTemp::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("water temperature");
|
2019-01-30 21:13:24 +00:00
|
|
|
}
|
|
|
|
|
2019-11-19 17:16:45 +00:00
|
|
|
// ***** Water Type *****
|
|
|
|
void EditWaterTypeUser::set(struct dive *d, int value) const
|
|
|
|
{
|
|
|
|
d->user_salinity = value > 0 ? value : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int EditWaterTypeUser::data(struct dive *d) const
|
|
|
|
{
|
|
|
|
return d->user_salinity;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString EditWaterTypeUser::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("salinity");
|
2019-11-19 17:16:45 +00:00
|
|
|
}
|
|
|
|
|
2019-04-30 10:42:33 +00:00
|
|
|
// ***** 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
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("Atm. pressure");
|
2019-04-30 10:42:33 +00:00
|
|
|
}
|
|
|
|
|
2019-02-10 16:37:06 +00:00
|
|
|
// ***** 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;
|
2021-12-04 15:21:33 +00:00
|
|
|
fake_dc(&d->dc);
|
2019-02-10 16:37:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
int EditDuration::data(struct dive *d) const
|
|
|
|
{
|
|
|
|
return d->duration.seconds;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString EditDuration::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("duration");
|
2019-02-10 16:37:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ***** 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;
|
2021-12-04 15:21:33 +00:00
|
|
|
fake_dc(&d->dc);
|
2019-02-10 16:37:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
int EditDepth::data(struct dive *d) const
|
|
|
|
{
|
|
|
|
return d->maxdepth.mm;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString EditDepth::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("depth");
|
2019-02-10 16:37:06 +00:00
|
|
|
}
|
|
|
|
|
2019-03-20 20:46:58 +00:00
|
|
|
// ***** 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
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("dive site");
|
2019-03-20 20:46:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void EditDiveSite::undo()
|
|
|
|
{
|
|
|
|
// Do the normal undo thing, then send dive site changed signals
|
|
|
|
EditBase<dive_site *>::undo();
|
|
|
|
if (value)
|
|
|
|
emit diveListNotifier.diveSiteDivesChanged(value);
|
|
|
|
if (old)
|
|
|
|
emit diveListNotifier.diveSiteDivesChanged(old);
|
|
|
|
}
|
|
|
|
|
|
|
|
void EditDiveSite::redo()
|
|
|
|
{
|
|
|
|
EditDiveSite::undo(); // Undo and redo do the same
|
|
|
|
}
|
|
|
|
|
2019-02-14 22:07:12 +00:00
|
|
|
static struct dive_site *createDiveSite(const QString &name)
|
2019-03-20 20:46:58 +00:00
|
|
|
{
|
|
|
|
struct dive_site *ds = alloc_dive_site();
|
2019-02-14 22:07:12 +00:00
|
|
|
struct dive_site *old = current_dive ? current_dive->dive_site : nullptr;
|
2019-03-20 20:46:58 +00:00
|
|
|
if (old) {
|
|
|
|
copy_dive_site(old, ds);
|
|
|
|
free(ds->name); // Free name, as we will overwrite it with our own version
|
|
|
|
}
|
2019-04-24 21:59:59 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2019-03-20 20:46:58 +00:00
|
|
|
ds->name = copy_qstring(name);
|
|
|
|
return ds;
|
|
|
|
}
|
|
|
|
|
2019-02-14 22:07:12 +00:00
|
|
|
EditDiveSiteNew::EditDiveSiteNew(const QString &newName, bool currentDiveOnly) :
|
|
|
|
EditDiveSite(createDiveSite(newName), currentDiveOnly),
|
2019-03-20 20:46:58 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2019-01-28 17:35:27 +00:00
|
|
|
// ***** 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.
|
2019-02-14 22:07:12 +00:00
|
|
|
EditMode::EditMode(int indexIn, int newValue, bool currentDiveOnly)
|
2020-03-21 16:36:58 +00:00
|
|
|
: EditTemplate(newValue, currentDiveOnly), index(indexIn)
|
2019-01-28 17:35:27 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("dive mode");
|
2019-01-28 17:35:27 +00:00
|
|
|
}
|
|
|
|
|
2019-12-12 22:07:17 +00:00
|
|
|
// ***** Invalid *****
|
|
|
|
QString EditInvalid::fieldName() const
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("invalid");
|
2019-12-12 22:07:17 +00:00
|
|
|
}
|
|
|
|
|
2019-02-07 18:59:34 +00:00
|
|
|
// ***** Tag based commands *****
|
2019-02-14 22:07:12 +00:00
|
|
|
EditTagsBase::EditTagsBase(const QStringList &newListIn, bool currentDiveOnly) :
|
2019-05-23 18:27:19 +00:00
|
|
|
EditDivesBase(currentDiveOnly),
|
2019-02-14 22:07:12 +00:00
|
|
|
newList(newListIn)
|
2019-02-07 18:59:34 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2019-02-14 22:07:12 +00:00
|
|
|
// If there is no current dive, bail.
|
|
|
|
if (!current)
|
|
|
|
return false;
|
|
|
|
|
2019-02-07 18:59:34 +00:00
|
|
|
// Calculate tags to add and tags to remove
|
2019-02-14 22:07:12 +00:00
|
|
|
QStringList oldList = data(current);
|
2019-02-07 18:59:34 +00:00
|
|
|
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<dive *> 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();
|
2019-12-04 14:30:59 +00:00
|
|
|
if (num_dives == 1)
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Edit %1").arg(fieldName())).arg(diveNumberOrDate(dives[0])));
|
2019-12-04 14:30:59 +00:00
|
|
|
else if (num_dives > 0)
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Edit %1 (%n dive(s))", "", num_dives).arg(fieldName())).arg(getListOfDives(dives)));
|
2019-02-07 18:59:34 +00:00
|
|
|
|
2019-02-14 22:07:12 +00:00
|
|
|
return num_dives != 0;
|
2019-02-07 18:59:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
set(d, tags);
|
2020-03-16 22:10:47 +00:00
|
|
|
fulltext_register(d); // Update the fulltext cache
|
|
|
|
invalidate_dive_cache(d); // Ensure that dive is written in git_save()
|
2019-02-07 18:59:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::swap(tagsToAdd, tagsToRemove);
|
|
|
|
|
2019-02-11 14:34:43 +00:00
|
|
|
// Send signals.
|
|
|
|
DiveField id = fieldId();
|
2020-10-25 21:42:40 +00:00
|
|
|
emit diveListNotifier.divesChanged(stdToQt<dive *>(dives), id);
|
2022-04-04 16:57:28 +00:00
|
|
|
setSelection(selectedDives, current, -1);
|
2019-02-07 18:59:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("tags");
|
2019-02-07 18:59:34 +00:00
|
|
|
}
|
|
|
|
|
2019-02-07 20:00:09 +00:00
|
|
|
// ***** 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
|
|
|
|
{
|
2020-03-21 23:46:36 +00:00
|
|
|
return Command::Base::tr("buddies");
|
2019-02-07 20:00:09 +00:00
|
|
|
}
|
|
|
|
|
2022-02-12 13:03:18 +00:00
|
|
|
// ***** DiveGuide *****
|
|
|
|
QStringList EditDiveGuide::data(struct dive *d) const
|
2019-02-07 20:23:00 +00:00
|
|
|
{
|
2022-02-12 13:03:18 +00:00
|
|
|
return stringToList(d->diveguide);
|
2019-02-07 20:23:00 +00:00
|
|
|
}
|
|
|
|
|
2022-02-12 13:03:18 +00:00
|
|
|
void EditDiveGuide::set(struct dive *d, const QStringList &v) const
|
2019-02-07 20:23:00 +00:00
|
|
|
{
|
|
|
|
QString text = v.join(", ");
|
2022-02-12 13:03:18 +00:00
|
|
|
free(d->diveguide);
|
|
|
|
d->diveguide = copy_qstring(text);
|
2019-02-07 20:23:00 +00:00
|
|
|
}
|
|
|
|
|
2022-02-12 13:03:18 +00:00
|
|
|
QString EditDiveGuide::fieldName() const
|
2019-02-07 20:23:00 +00:00
|
|
|
{
|
2022-02-12 13:03:18 +00:00
|
|
|
return Command::Base::tr("dive guide");
|
2019-02-07 20:23:00 +00:00
|
|
|
}
|
|
|
|
|
2019-02-23 17:17:20 +00:00
|
|
|
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),
|
2019-08-04 16:44:57 +00:00
|
|
|
tags(nullptr)
|
2019-02-23 17:17:20 +00:00
|
|
|
{
|
2019-08-04 16:44:57 +00:00
|
|
|
memset(&cylinders, 0, sizeof(cylinders));
|
2019-08-24 11:42:49 +00:00
|
|
|
memset(&weightsystems, 0, sizeof(weightsystems));
|
2019-02-23 17:17:20 +00:00
|
|
|
if (what.notes)
|
|
|
|
notes = data->notes;
|
2022-02-12 13:03:18 +00:00
|
|
|
if (what.diveguide)
|
|
|
|
diveguide = data->diveguide;
|
2019-02-23 17:17:20 +00:00
|
|
|
if (what.buddy)
|
|
|
|
buddy = data->buddy;
|
|
|
|
if (what.suit)
|
|
|
|
suit = data->suit;
|
|
|
|
if (what.rating)
|
|
|
|
rating = data->rating;
|
|
|
|
if (what.visibility)
|
|
|
|
visibility = data->visibility;
|
2019-11-28 19:04:52 +00:00
|
|
|
if (what.wavesize)
|
|
|
|
wavesize = data->wavesize;
|
|
|
|
if (what.current)
|
|
|
|
current = data->current;
|
|
|
|
if (what.surge)
|
|
|
|
surge = data->surge;
|
|
|
|
if (what.chill)
|
|
|
|
chill = data->chill;
|
2019-02-23 17:17:20 +00:00
|
|
|
if (what.divesite)
|
|
|
|
divesite = data->dive_site;
|
|
|
|
if (what.tags)
|
|
|
|
tags = taglist_copy(data->tag_list);
|
undo: refine pasting of cylinders
When pasting cylinders, the destination dive got a verbatim copy
of the cylinders of the source dive. This is not what users want:
for example, the start and stop pressures from the dive computer
as well as the gas mix may be overwritten.
There seems to be no perfect solution, since some times users may
want to paste the gas-mix.
Therefore, let's choose a heuristic approach for now (in the future
we might implement a UI-flag):
When copying over existing cylinders, only copy type and maximum
pressure.
When adding new cylinders (i.e. from-dive has more cylinders than
to-dive), copy everything, but reset start- and top-pressure to 0,
which represents unkown.
Moroever, in the latter case, set "manually added" to true, since
obviously this wasn't added by a dive computer.
Fixes #2676
Reported-by: Miika Turkia <miika.turkia@gmail.com>
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2020-04-08 19:36:43 +00:00
|
|
|
if (what.cylinders) {
|
2019-08-04 16:44:57 +00:00
|
|
|
copy_cylinders(&data->cylinders, &cylinders);
|
undo: refine pasting of cylinders
When pasting cylinders, the destination dive got a verbatim copy
of the cylinders of the source dive. This is not what users want:
for example, the start and stop pressures from the dive computer
as well as the gas mix may be overwritten.
There seems to be no perfect solution, since some times users may
want to paste the gas-mix.
Therefore, let's choose a heuristic approach for now (in the future
we might implement a UI-flag):
When copying over existing cylinders, only copy type and maximum
pressure.
When adding new cylinders (i.e. from-dive has more cylinders than
to-dive), copy everything, but reset start- and top-pressure to 0,
which represents unkown.
Moroever, in the latter case, set "manually added" to true, since
obviously this wasn't added by a dive computer.
Fixes #2676
Reported-by: Miika Turkia <miika.turkia@gmail.com>
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2020-04-08 19:36:43 +00:00
|
|
|
// Paste cylinders is "special":
|
|
|
|
// 1) For cylinders that exist in the destination dive we keep the gas-mix and pressures.
|
|
|
|
// 2) For cylinders that do not yet exist in the destination dive, we set the pressures to 0, i.e. unset.
|
|
|
|
// Moreover, for these we set the manually_added flag, because they weren't downloaded from a DC.
|
|
|
|
for (int i = 0; i < d->cylinders.nr && i < cylinders.nr; ++i) {
|
2020-08-20 05:31:04 +00:00
|
|
|
const cylinder_t &src = *get_cylinder(d, i);
|
undo: refine pasting of cylinders
When pasting cylinders, the destination dive got a verbatim copy
of the cylinders of the source dive. This is not what users want:
for example, the start and stop pressures from the dive computer
as well as the gas mix may be overwritten.
There seems to be no perfect solution, since some times users may
want to paste the gas-mix.
Therefore, let's choose a heuristic approach for now (in the future
we might implement a UI-flag):
When copying over existing cylinders, only copy type and maximum
pressure.
When adding new cylinders (i.e. from-dive has more cylinders than
to-dive), copy everything, but reset start- and top-pressure to 0,
which represents unkown.
Moroever, in the latter case, set "manually added" to true, since
obviously this wasn't added by a dive computer.
Fixes #2676
Reported-by: Miika Turkia <miika.turkia@gmail.com>
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2020-04-08 19:36:43 +00:00
|
|
|
cylinder_t &dst = cylinders.cylinders[i];
|
|
|
|
dst.gasmix = src.gasmix;
|
|
|
|
dst.start = src.start;
|
|
|
|
dst.end = src.end;
|
|
|
|
dst.sample_start = src.sample_start;
|
|
|
|
dst.sample_end = src.sample_end;
|
|
|
|
dst.depth = src.depth;
|
|
|
|
dst.manually_added = src.manually_added;
|
|
|
|
dst.gas_used = src.gas_used;
|
|
|
|
dst.deco_gas_used = src.deco_gas_used;
|
|
|
|
dst.cylinder_use = src.cylinder_use;
|
|
|
|
dst.bestmix_o2 = src.bestmix_o2;
|
|
|
|
dst.bestmix_he = src.bestmix_he;
|
|
|
|
}
|
|
|
|
for (int i = d->cylinders.nr; i < cylinders.nr; ++i) {
|
|
|
|
cylinder_t &cyl = cylinders.cylinders[i];
|
|
|
|
cyl.start.mbar = 0;
|
|
|
|
cyl.end.mbar = 0;
|
|
|
|
cyl.sample_start.mbar = 0;
|
|
|
|
cyl.sample_end.mbar = 0;
|
|
|
|
cyl.manually_added = true;
|
|
|
|
}
|
|
|
|
}
|
2019-06-26 15:21:03 +00:00
|
|
|
if (what.weights)
|
|
|
|
copy_weights(&data->weightsystems, &weightsystems);
|
2021-05-16 03:39:47 +00:00
|
|
|
if (what.number)
|
|
|
|
number = data->number;
|
|
|
|
if (what.when)
|
|
|
|
when = data->when;
|
2019-02-23 17:17:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
PasteState::~PasteState()
|
|
|
|
{
|
|
|
|
taglist_free(tags);
|
2019-08-04 16:44:57 +00:00
|
|
|
clear_cylinder_table(&cylinders);
|
2019-06-26 15:21:03 +00:00
|
|
|
clear_weightsystem_table(&weightsystems);
|
|
|
|
free(weightsystems.weightsystems);
|
2019-02-23 17:17:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void PasteState::swap(dive_components what)
|
|
|
|
{
|
|
|
|
if (what.notes)
|
|
|
|
swapCandQString(notes, d->notes);
|
2022-02-12 13:03:18 +00:00
|
|
|
if (what.diveguide)
|
|
|
|
swapCandQString(diveguide, d->diveguide);
|
2019-02-23 17:17:20 +00:00
|
|
|
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);
|
2019-11-28 19:04:52 +00:00
|
|
|
if (what.wavesize)
|
|
|
|
std::swap(wavesize, d->wavesize);
|
|
|
|
if (what.current)
|
|
|
|
std::swap(current, d->current);
|
|
|
|
if (what.surge)
|
|
|
|
std::swap(surge, d->surge);
|
|
|
|
if (what.chill)
|
|
|
|
std::swap(chill, d->chill);
|
2019-02-23 17:17:20 +00:00
|
|
|
if (what.divesite)
|
|
|
|
std::swap(divesite, d->dive_site);
|
|
|
|
if (what.tags)
|
|
|
|
std::swap(tags, d->tag_list);
|
2019-08-04 16:44:57 +00:00
|
|
|
if (what.cylinders)
|
|
|
|
std::swap(cylinders, d->cylinders);
|
2019-02-23 17:17:20 +00:00
|
|
|
if (what.weights)
|
2019-06-26 15:21:03 +00:00
|
|
|
std::swap(weightsystems, d->weightsystems);
|
2021-05-16 03:39:47 +00:00
|
|
|
if (what.number)
|
|
|
|
std::swap(number, d->number);
|
|
|
|
if (what.when)
|
|
|
|
std::swap(when, d->when);
|
2019-02-23 17:17:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ***** Paste *****
|
2020-01-06 20:24:53 +00:00
|
|
|
PasteDives::PasteDives(const dive *data, dive_components whatIn) : what(whatIn)
|
2019-02-23 17:17:20 +00:00
|
|
|
{
|
|
|
|
std::vector<dive *> selection = getDiveSelection();
|
|
|
|
dives.reserve(selection.size());
|
|
|
|
for (dive *d: selection)
|
|
|
|
dives.emplace_back(d, data, what);
|
|
|
|
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Paste onto %n dive(s)", "", dives.size())).arg(getListOfDives(selection)));
|
2019-02-23 17:17:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool PasteDives::workToBeDone()
|
|
|
|
{
|
|
|
|
return !dives.empty();
|
|
|
|
}
|
|
|
|
|
|
|
|
void PasteDives::undo()
|
|
|
|
{
|
2019-06-23 07:22:26 +00:00
|
|
|
QVector<dive *> divesToNotify; // Remember dives so that we can send signals later
|
2019-02-23 17:17:20 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send signals.
|
2019-10-13 10:44:39 +00:00
|
|
|
DiveField fields(DiveField::NONE);
|
|
|
|
fields.notes = what.notes;
|
2022-02-12 13:03:18 +00:00
|
|
|
fields.diveguide = what.diveguide;
|
2019-10-13 10:44:39 +00:00
|
|
|
fields.buddy = what.buddy;
|
|
|
|
fields.suit = what.suit;
|
|
|
|
fields.rating = what.rating;
|
|
|
|
fields.visibility = what.visibility;
|
2019-11-28 19:04:52 +00:00
|
|
|
fields.wavesize = what.wavesize;
|
|
|
|
fields.current = what.current;
|
|
|
|
fields.surge = what.surge;
|
|
|
|
fields.chill = what.chill;
|
2019-10-13 10:44:39 +00:00
|
|
|
fields.divesite = what.divesite;
|
|
|
|
fields.tags = what.tags;
|
2021-05-16 03:39:47 +00:00
|
|
|
fields.datetime = what.when;
|
|
|
|
fields.nr = what.number;
|
2019-10-13 10:44:39 +00:00
|
|
|
emit diveListNotifier.divesChanged(divesToNotify, fields);
|
2019-06-23 07:22:26 +00:00
|
|
|
if (what.cylinders)
|
|
|
|
emit diveListNotifier.cylindersReset(divesToNotify);
|
|
|
|
if (what.weights)
|
|
|
|
emit diveListNotifier.weightsystemsReset(divesToNotify);
|
2019-02-23 17:17:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Redo and undo do the same
|
|
|
|
void PasteDives::redo()
|
|
|
|
{
|
|
|
|
undo();
|
|
|
|
}
|
|
|
|
|
2020-01-06 20:11:04 +00:00
|
|
|
// ***** ReplanDive *****
|
2022-02-13 18:32:19 +00:00
|
|
|
ReplanDive::ReplanDive(dive *source) : d(current_dive),
|
2020-01-06 20:11:04 +00:00
|
|
|
when(0),
|
|
|
|
maxdepth({0}),
|
|
|
|
meandepth({0}),
|
2019-10-06 18:54:25 +00:00
|
|
|
dc({ 0 }),
|
2020-01-06 20:11:04 +00:00
|
|
|
notes(nullptr),
|
|
|
|
surface_pressure({0}),
|
|
|
|
duration({0}),
|
|
|
|
salinity(0)
|
2019-10-06 18:54:25 +00:00
|
|
|
{
|
2019-08-04 16:44:57 +00:00
|
|
|
memset(&cylinders, 0, sizeof(cylinders));
|
2019-10-06 18:54:25 +00:00
|
|
|
if (!d)
|
|
|
|
return;
|
|
|
|
|
2019-11-30 14:51:34 +00:00
|
|
|
// Fix source. Things might be inconsistent after modifying the profile.
|
|
|
|
source->maxdepth.mm = source->dc.maxdepth.mm = 0;
|
|
|
|
fixup_dive(source);
|
|
|
|
|
2019-10-06 18:54:25 +00:00
|
|
|
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.
|
2019-08-04 16:44:57 +00:00
|
|
|
std::swap(source->cylinders, cylinders);
|
2019-10-06 18:54:25 +00:00
|
|
|
std::swap(source->dc, dc);
|
|
|
|
|
2022-02-13 18:32:19 +00:00
|
|
|
setText(Command::Base::tr("Replan dive"));
|
2019-10-06 18:54:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ReplanDive::~ReplanDive()
|
|
|
|
{
|
2019-08-04 16:44:57 +00:00
|
|
|
clear_cylinder_table(&cylinders);
|
2019-10-06 18:54:25 +00:00
|
|
|
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);
|
2019-08-04 16:44:57 +00:00
|
|
|
std::swap(d->cylinders, cylinders);
|
2019-10-06 18:54:25 +00:00
|
|
|
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);
|
2021-01-11 10:14:41 +00:00
|
|
|
invalidate_dive_cache(d); // Ensure that dive is written in git_save()
|
2019-10-06 18:54:25 +00:00
|
|
|
|
|
|
|
QVector<dive *> divesToNotify = { d };
|
2019-08-04 16:44:57 +00:00
|
|
|
// Note that we have to emit cylindersReset before divesChanged, because the divesChanged
|
2021-12-03 18:09:39 +00:00
|
|
|
// updates the profile, which is out-of-sync and gets confused.
|
2019-08-04 16:44:57 +00:00
|
|
|
emit diveListNotifier.cylindersReset(divesToNotify);
|
2019-10-13 10:44:39 +00:00
|
|
|
emit diveListNotifier.divesChanged(divesToNotify, DiveField::DATETIME | DiveField::DURATION | DiveField::DEPTH | DiveField::MODE |
|
|
|
|
DiveField::NOTES | DiveField::SALINITY | DiveField::ATM_PRESS);
|
2022-02-19 10:42:19 +00:00
|
|
|
if (!placingCommand())
|
2022-04-04 16:57:28 +00:00
|
|
|
setSelection({ d }, d, -1);
|
2019-10-06 18:54:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Redo and undo do the same
|
|
|
|
void ReplanDive::redo()
|
|
|
|
{
|
|
|
|
undo();
|
|
|
|
}
|
|
|
|
|
2022-02-13 18:32:19 +00:00
|
|
|
// ***** EditProfile *****
|
|
|
|
QString editProfileTypeToString(EditProfileType type, int count)
|
|
|
|
{
|
|
|
|
switch (type) {
|
|
|
|
default:
|
|
|
|
case EditProfileType::ADD: return Command::Base::tr("Add stop");
|
|
|
|
case EditProfileType::REMOVE: return Command::Base::tr("Remove %n stop(s)", "", count);
|
|
|
|
case EditProfileType::MOVE: return Command::Base::tr("Move %n stop(s)", "", count);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-21 19:30:57 +00:00
|
|
|
EditProfile::EditProfile(const dive *source, int dcNr, EditProfileType type, int count) : d(current_dive),
|
|
|
|
dcNr(dcNr),
|
2022-02-13 18:32:19 +00:00
|
|
|
maxdepth({0}),
|
|
|
|
meandepth({0}),
|
|
|
|
dcmaxdepth({0}),
|
|
|
|
duration({0}),
|
|
|
|
dc({ 0 })
|
|
|
|
{
|
|
|
|
const struct divecomputer *sdc = get_dive_dc_const(source, dcNr);
|
|
|
|
if (!sdc)
|
|
|
|
d = nullptr; // Signal that we refuse to do anything.
|
|
|
|
if (!d)
|
|
|
|
return;
|
|
|
|
|
|
|
|
maxdepth = source->maxdepth;
|
|
|
|
dcmaxdepth = sdc->maxdepth;
|
|
|
|
meandepth = source->meandepth;
|
|
|
|
duration = source->duration;
|
|
|
|
|
|
|
|
copy_samples(sdc, &dc);
|
|
|
|
copy_events(sdc, &dc);
|
|
|
|
|
|
|
|
setText(editProfileTypeToString(type, count) + diveNumberOrDate(d));
|
|
|
|
}
|
|
|
|
|
|
|
|
EditProfile::~EditProfile()
|
|
|
|
{
|
|
|
|
free_dive_dcs(&dc);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EditProfile::workToBeDone()
|
|
|
|
{
|
|
|
|
return !!d;
|
|
|
|
}
|
|
|
|
|
|
|
|
void EditProfile::undo()
|
|
|
|
{
|
|
|
|
struct divecomputer *sdc = get_dive_dc(d, dcNr);
|
|
|
|
if (!sdc)
|
|
|
|
return;
|
|
|
|
std::swap(sdc->samples, dc.samples);
|
|
|
|
std::swap(sdc->alloc_samples, dc.alloc_samples);
|
|
|
|
std::swap(sdc->sample, dc.sample);
|
|
|
|
std::swap(sdc->maxdepth, dc.maxdepth);
|
|
|
|
std::swap(d->maxdepth, maxdepth);
|
|
|
|
std::swap(d->meandepth, meandepth);
|
|
|
|
std::swap(d->duration, duration);
|
|
|
|
fixup_dive(d);
|
|
|
|
invalidate_dive_cache(d); // Ensure that dive is written in git_save()
|
|
|
|
|
|
|
|
QVector<dive *> divesToNotify = { d };
|
|
|
|
emit diveListNotifier.divesChanged(divesToNotify, DiveField::DURATION | DiveField::DEPTH);
|
2022-02-19 10:42:19 +00:00
|
|
|
if (!placingCommand())
|
2022-04-04 16:57:28 +00:00
|
|
|
setSelection({ d }, d, dcNr);
|
2022-02-13 18:32:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Redo and undo do the same
|
|
|
|
void EditProfile::redo()
|
|
|
|
{
|
|
|
|
undo();
|
|
|
|
}
|
|
|
|
|
2019-11-02 20:19:29 +00:00
|
|
|
// ***** Add Weight *****
|
|
|
|
AddWeight::AddWeight(bool currentDiveOnly) :
|
|
|
|
EditDivesBase(currentDiveOnly)
|
|
|
|
{
|
2019-12-04 14:30:59 +00:00
|
|
|
if (dives.size() == 1)
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Add weight")).arg(diveNumberOrDate(dives[0])));
|
2019-12-04 14:30:59 +00:00
|
|
|
else
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Add weight (%n dive(s))", "", dives.size())).arg(getListOfDives(dives)));
|
2019-11-02 20:19:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool AddWeight::workToBeDone()
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void AddWeight::undo()
|
|
|
|
{
|
|
|
|
for (dive *d: dives) {
|
|
|
|
if (d->weightsystems.nr <= 0)
|
|
|
|
continue;
|
|
|
|
remove_weightsystem(d, d->weightsystems.nr - 1);
|
|
|
|
emit diveListNotifier.weightRemoved(d, d->weightsystems.nr);
|
2021-01-11 10:14:41 +00:00
|
|
|
invalidate_dive_cache(d); // Ensure that dive is written in git_save()
|
2019-11-02 20:19:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AddWeight::redo()
|
|
|
|
{
|
|
|
|
for (dive *d: dives) {
|
2019-11-03 22:08:32 +00:00
|
|
|
add_cloned_weightsystem(&d->weightsystems, empty_weightsystem);
|
2019-11-02 20:19:29 +00:00
|
|
|
emit diveListNotifier.weightAdded(d, d->weightsystems.nr - 1);
|
2021-01-11 10:14:41 +00:00
|
|
|
invalidate_dive_cache(d); // Ensure that dive is written in git_save()
|
2019-11-02 20:19:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-03 14:04:48 +00:00
|
|
|
static int find_weightsystem_index(const struct dive *d, weightsystem_t ws)
|
|
|
|
{
|
|
|
|
for (int idx = 0; idx < d->weightsystems.nr; ++idx) {
|
|
|
|
if (same_weightsystem(d->weightsystems.weightsystems[idx], ws))
|
|
|
|
return idx;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2019-11-08 21:47:38 +00:00
|
|
|
EditWeightBase::EditWeightBase(int index, bool currentDiveOnly) :
|
2019-11-03 14:04:48 +00:00
|
|
|
EditDivesBase(currentDiveOnly),
|
2019-11-03 22:08:32 +00:00
|
|
|
ws(empty_weightsystem)
|
2019-11-03 14:04:48 +00:00
|
|
|
{
|
|
|
|
// Get the old weightsystem, bail if index is invalid
|
|
|
|
if (!current || index < 0 || index >= current->weightsystems.nr) {
|
|
|
|
dives.clear();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
ws = clone_weightsystem(current->weightsystems.weightsystems[index]);
|
|
|
|
|
|
|
|
// Deleting a weightsystem from multiple dives is semantically ill-defined.
|
|
|
|
// What we will do is trying to delete the same weightsystem if it exists.
|
|
|
|
// For that purpose, we will determine the indices of the same weightsystem.
|
|
|
|
std::vector<dive *> divesNew;
|
|
|
|
divesNew.reserve(dives.size());
|
2020-03-11 10:30:51 +00:00
|
|
|
indices.reserve(dives.size());
|
2019-11-03 14:04:48 +00:00
|
|
|
|
|
|
|
for (dive *d: dives) {
|
|
|
|
if (d == current) {
|
|
|
|
divesNew.push_back(d);
|
2020-03-11 10:30:51 +00:00
|
|
|
indices.push_back(index);
|
2019-11-03 14:04:48 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
int idx = find_weightsystem_index(d, ws);
|
|
|
|
if (idx >= 0) {
|
|
|
|
divesNew.push_back(d);
|
2020-03-11 10:30:51 +00:00
|
|
|
indices.push_back(idx);
|
2019-11-03 14:04:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
dives = std::move(divesNew);
|
|
|
|
}
|
|
|
|
|
2019-11-08 21:47:38 +00:00
|
|
|
EditWeightBase::~EditWeightBase()
|
2019-11-03 14:04:48 +00:00
|
|
|
{
|
2019-11-03 22:14:35 +00:00
|
|
|
free_weightsystem(ws);
|
2019-11-03 14:04:48 +00:00
|
|
|
}
|
|
|
|
|
2019-11-08 21:47:38 +00:00
|
|
|
bool EditWeightBase::workToBeDone()
|
2019-11-03 14:04:48 +00:00
|
|
|
{
|
|
|
|
return !dives.empty();
|
|
|
|
}
|
|
|
|
|
2020-02-23 10:43:50 +00:00
|
|
|
// ***** Remove Weight *****
|
2019-11-08 21:47:38 +00:00
|
|
|
RemoveWeight::RemoveWeight(int index, bool currentDiveOnly) :
|
|
|
|
EditWeightBase(index, currentDiveOnly)
|
|
|
|
{
|
2020-03-05 17:00:00 +00:00
|
|
|
size_t num_dives = dives.size();
|
|
|
|
if (num_dives == 1)
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Remove weight")).arg(diveNumberOrDate(dives[0])));
|
2019-12-04 14:30:59 +00:00
|
|
|
else
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Remove weight (%n dive(s))", "", num_dives)).arg(getListOfDives(dives)));
|
2019-11-08 21:47:38 +00:00
|
|
|
}
|
|
|
|
|
2019-11-03 14:04:48 +00:00
|
|
|
void RemoveWeight::undo()
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < dives.size(); ++i) {
|
2020-03-11 10:30:51 +00:00
|
|
|
add_to_weightsystem_table(&dives[i]->weightsystems, indices[i], clone_weightsystem(ws));
|
|
|
|
emit diveListNotifier.weightAdded(dives[i], indices[i]);
|
2021-01-11 10:14:41 +00:00
|
|
|
invalidate_dive_cache(dives[i]); // Ensure that dive is written in git_save()
|
2019-11-03 14:04:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void RemoveWeight::redo()
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < dives.size(); ++i) {
|
2020-03-11 10:30:51 +00:00
|
|
|
remove_weightsystem(dives[i], indices[i]);
|
|
|
|
emit diveListNotifier.weightRemoved(dives[i], indices[i]);
|
2021-01-11 10:14:41 +00:00
|
|
|
invalidate_dive_cache(dives[i]); // Ensure that dive is written in git_save()
|
2019-11-03 14:04:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-23 10:43:50 +00:00
|
|
|
// ***** Edit Weight *****
|
2019-11-08 21:47:38 +00:00
|
|
|
EditWeight::EditWeight(int index, weightsystem_t wsIn, bool currentDiveOnly) :
|
|
|
|
EditWeightBase(index, currentDiveOnly),
|
|
|
|
new_ws(empty_weightsystem)
|
|
|
|
{
|
|
|
|
if (dives.empty())
|
|
|
|
return;
|
|
|
|
|
2020-03-05 17:00:00 +00:00
|
|
|
size_t num_dives = dives.size();
|
|
|
|
if (num_dives == 1)
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Edit weight")).arg(diveNumberOrDate(dives[0])));
|
2019-12-04 14:30:59 +00:00
|
|
|
else
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(QStringLiteral("%1 [%2]").arg(Command::Base::tr("Edit weight (%n dive(s))", "", num_dives)).arg(getListOfDives(dives)));
|
2019-11-08 21:47:38 +00:00
|
|
|
|
|
|
|
// Try to untranslate the weightsystem name
|
|
|
|
new_ws = clone_weightsystem(wsIn);
|
|
|
|
QString vString(new_ws.description);
|
|
|
|
for (int i = 0; i < MAX_WS_INFO && ws_info[i].name; ++i) {
|
|
|
|
if (gettextFromC::tr(ws_info[i].name) == vString) {
|
|
|
|
free_weightsystem(new_ws);
|
|
|
|
new_ws.description = copy_string(ws_info[i].name);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If that doesn't change anything, do nothing
|
|
|
|
if (same_weightsystem(ws, new_ws)) {
|
|
|
|
dives.clear();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
WSInfoModel *wsim = WSInfoModel::instance();
|
|
|
|
QModelIndexList matches = wsim->match(wsim->index(0, 0), Qt::DisplayRole, gettextFromC::tr(new_ws.description));
|
|
|
|
if (!matches.isEmpty())
|
|
|
|
wsim->setData(wsim->index(matches.first().row(), WSInfoModel::GR), new_ws.weight.grams);
|
|
|
|
}
|
|
|
|
|
|
|
|
EditWeight::~EditWeight()
|
|
|
|
{
|
|
|
|
free_weightsystem(new_ws);
|
|
|
|
}
|
|
|
|
|
|
|
|
void EditWeight::redo()
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < dives.size(); ++i) {
|
2020-03-11 10:30:51 +00:00
|
|
|
set_weightsystem(dives[i], indices[i], new_ws);
|
|
|
|
emit diveListNotifier.weightEdited(dives[i], indices[i]);
|
2021-01-11 10:14:41 +00:00
|
|
|
invalidate_dive_cache(dives[i]); // Ensure that dive is written in git_save()
|
2019-11-08 21:47:38 +00:00
|
|
|
}
|
|
|
|
std::swap(ws, new_ws);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Undo and redo do the same as just the stored value is exchanged
|
|
|
|
void EditWeight::undo()
|
|
|
|
{
|
|
|
|
redo();
|
|
|
|
}
|
|
|
|
|
2020-02-23 10:43:50 +00:00
|
|
|
// ***** Add Cylinder *****
|
|
|
|
AddCylinder::AddCylinder(bool currentDiveOnly) :
|
|
|
|
EditDivesBase(currentDiveOnly),
|
|
|
|
cyl(empty_cylinder)
|
|
|
|
{
|
|
|
|
if (dives.empty())
|
|
|
|
return;
|
|
|
|
else if (dives.size() == 1)
|
2020-11-08 22:38:59 +00:00
|
|
|
setText(Command::Base::tr("Add cylinder"));
|
2020-02-23 10:43:50 +00:00
|
|
|
else
|
2020-11-08 22:38:59 +00:00
|
|
|
setText(Command::Base::tr("Add cylinder (%n dive(s))", "", dives.size()));
|
2020-02-23 10:43:50 +00:00
|
|
|
cyl = create_new_cylinder(dives[0]);
|
2021-11-21 10:39:02 +00:00
|
|
|
indexes.reserve(dives.size());
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
AddCylinder::~AddCylinder()
|
|
|
|
{
|
|
|
|
free_cylinder(cyl);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool AddCylinder::workToBeDone()
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void AddCylinder::undo()
|
|
|
|
{
|
2021-11-21 10:39:02 +00:00
|
|
|
for (size_t i = 0; i < dives.size(); ++i) {
|
|
|
|
remove_cylinder(dives[i], indexes[i]);
|
|
|
|
update_cylinder_related_info(dives[i]);
|
|
|
|
emit diveListNotifier.cylinderRemoved(dives[i], indexes[i]);
|
|
|
|
invalidate_dive_cache(dives[i]); // Ensure that dive is written in git_save()
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AddCylinder::redo()
|
|
|
|
{
|
2021-11-21 10:39:02 +00:00
|
|
|
indexes.clear();
|
2020-02-23 10:43:50 +00:00
|
|
|
for (dive *d: dives) {
|
2021-11-21 10:39:02 +00:00
|
|
|
int index = first_hidden_cylinder(d);
|
|
|
|
indexes.push_back(index);
|
|
|
|
add_cylinder(&d->cylinders, index, clone_cylinder(cyl));
|
2020-05-05 09:34:16 +00:00
|
|
|
update_cylinder_related_info(d);
|
2021-11-21 10:39:02 +00:00
|
|
|
emit diveListNotifier.cylinderAdded(d, index);
|
2020-04-03 12:25:43 +00:00
|
|
|
invalidate_dive_cache(d); // Ensure that dive is written in git_save()
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-22 22:27:47 +00:00
|
|
|
static bool same_cylinder_type(const cylinder_t &cyl1, const cylinder_t &cyl2)
|
|
|
|
{
|
|
|
|
return same_string(cyl1.type.description, cyl2.type.description) &&
|
|
|
|
cyl1.cylinder_use == cyl2.cylinder_use;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool same_cylinder_size(const cylinder_t &cyl1, const cylinder_t &cyl2)
|
|
|
|
{
|
|
|
|
return cyl1.type.size.mliter == cyl2.type.size.mliter &&
|
|
|
|
cyl1.type.workingpressure.mbar == cyl2.type.workingpressure.mbar;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Flags for comparing cylinders
|
2021-12-16 21:17:47 +00:00
|
|
|
static constexpr int SAME_TYPE = 1 << 0;
|
|
|
|
static constexpr int SAME_SIZE = 1 << 1;
|
|
|
|
static constexpr int SAME_PRESS = 1 << 2;
|
|
|
|
static constexpr int SAME_GAS = 1 << 3;
|
2020-03-22 22:27:47 +00:00
|
|
|
|
|
|
|
EditCylinderBase::EditCylinderBase(int index, bool currentDiveOnly, bool nonProtectedOnly, int sameCylinderFlags) :
|
2020-03-27 08:22:11 +00:00
|
|
|
EditDivesBase(currentDiveOnly)
|
2020-02-23 10:43:50 +00:00
|
|
|
{
|
|
|
|
// Get the old cylinder, bail if index is invalid
|
|
|
|
if (!current || index < 0 || index >= current->cylinders.nr) {
|
|
|
|
dives.clear();
|
|
|
|
return;
|
|
|
|
}
|
2020-08-20 05:31:04 +00:00
|
|
|
const cylinder_t &orig = *get_cylinder(current, index);
|
2020-02-23 10:43:50 +00:00
|
|
|
|
|
|
|
std::vector<dive *> divesNew;
|
|
|
|
divesNew.reserve(dives.size());
|
|
|
|
indexes.reserve(dives.size());
|
2020-03-27 08:22:11 +00:00
|
|
|
cyl.reserve(dives.size());
|
2020-02-23 10:43:50 +00:00
|
|
|
|
|
|
|
for (dive *d: dives) {
|
2023-02-10 18:00:49 +00:00
|
|
|
if (index >= d->cylinders.nr)
|
|
|
|
continue;
|
2022-06-08 20:47:13 +00:00
|
|
|
if (nonProtectedOnly && is_cylinder_prot(d, index))
|
2020-02-24 09:57:36 +00:00
|
|
|
continue;
|
2023-02-10 18:00:49 +00:00
|
|
|
// We checked that the cylinder exists above.
|
|
|
|
const cylinder_t &cylinder = *get_cylinder(d, index);
|
2022-06-08 20:47:13 +00:00
|
|
|
if (d != current &&
|
2023-02-10 18:00:49 +00:00
|
|
|
(!same_cylinder_size(orig, cylinder) || !same_cylinder_type(orig, cylinder))) {
|
2022-06-08 20:47:13 +00:00
|
|
|
// when editing cylinders, we assume that the user wanted to edit the 'n-th' cylinder
|
|
|
|
// and we only do edit that cylinder, if it was the same type as the one in the current dive
|
|
|
|
continue;
|
2023-02-10 18:00:49 +00:00
|
|
|
}
|
2022-06-08 20:47:13 +00:00
|
|
|
|
2020-02-24 09:57:36 +00:00
|
|
|
divesNew.push_back(d);
|
2022-06-08 20:47:13 +00:00
|
|
|
// that's silly as it's always the same value - but we need this vector of indices in the case where we add
|
|
|
|
// a cylinder to several dives as the spot will potentially be different in different dives
|
|
|
|
indexes.push_back(index);
|
2023-02-10 18:00:49 +00:00
|
|
|
cyl.push_back(clone_cylinder(cylinder));
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
dives = std::move(divesNew);
|
|
|
|
}
|
|
|
|
|
|
|
|
EditCylinderBase::~EditCylinderBase()
|
|
|
|
{
|
2020-03-27 08:22:11 +00:00
|
|
|
for (cylinder_t c: cyl)
|
|
|
|
free_cylinder(c);
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool EditCylinderBase::workToBeDone()
|
|
|
|
{
|
|
|
|
return !dives.empty();
|
|
|
|
}
|
|
|
|
|
|
|
|
// ***** Remove Cylinder *****
|
|
|
|
RemoveCylinder::RemoveCylinder(int index, bool currentDiveOnly) :
|
2020-03-22 22:27:47 +00:00
|
|
|
EditCylinderBase(index, currentDiveOnly, true, SAME_TYPE | SAME_PRESS | SAME_GAS)
|
2020-02-23 10:43:50 +00:00
|
|
|
{
|
|
|
|
if (dives.size() == 1)
|
2020-11-08 22:38:59 +00:00
|
|
|
setText(Command::Base::tr("Remove cylinder"));
|
2020-02-23 10:43:50 +00:00
|
|
|
else
|
2020-11-08 22:38:59 +00:00
|
|
|
setText(Command::Base::tr("Remove cylinder (%n dive(s))", "", dives.size()));
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void RemoveCylinder::undo()
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < dives.size(); ++i) {
|
2020-02-23 18:24:06 +00:00
|
|
|
std::vector<int> mapping = get_cylinder_map_for_add(dives[i]->cylinders.nr, indexes[i]);
|
2020-04-28 12:50:40 +00:00
|
|
|
add_cylinder(&dives[i]->cylinders, indexes[i], clone_cylinder(cyl[i]));
|
2021-09-02 17:16:05 +00:00
|
|
|
cylinder_renumber(dives[i], &mapping[0]);
|
2020-05-05 09:34:16 +00:00
|
|
|
update_cylinder_related_info(dives[i]);
|
2020-02-23 10:43:50 +00:00
|
|
|
emit diveListNotifier.cylinderAdded(dives[i], indexes[i]);
|
2020-04-03 12:25:43 +00:00
|
|
|
invalidate_dive_cache(dives[i]); // Ensure that dive is written in git_save()
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void RemoveCylinder::redo()
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < dives.size(); ++i) {
|
2020-02-23 18:24:06 +00:00
|
|
|
std::vector<int> mapping = get_cylinder_map_for_remove(dives[i]->cylinders.nr, indexes[i]);
|
2020-02-23 10:43:50 +00:00
|
|
|
remove_cylinder(dives[i], indexes[i]);
|
2020-02-23 18:24:06 +00:00
|
|
|
cylinder_renumber(dives[i], &mapping[0]);
|
2020-05-05 09:34:16 +00:00
|
|
|
update_cylinder_related_info(dives[i]);
|
2020-02-23 10:43:50 +00:00
|
|
|
emit diveListNotifier.cylinderRemoved(dives[i], indexes[i]);
|
2020-04-03 12:25:43 +00:00
|
|
|
invalidate_dive_cache(dives[i]); // Ensure that dive is written in git_save()
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 20:09:59 +00:00
|
|
|
static int editCylinderTypeToFlags(EditCylinderType type)
|
|
|
|
{
|
|
|
|
switch (type) {
|
|
|
|
default:
|
|
|
|
case EditCylinderType::TYPE:
|
|
|
|
return SAME_TYPE | SAME_SIZE;
|
|
|
|
case EditCylinderType::PRESSURE:
|
|
|
|
return SAME_PRESS;
|
|
|
|
case EditCylinderType::GASMIX:
|
|
|
|
return SAME_GAS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-23 10:43:50 +00:00
|
|
|
// ***** Edit Cylinder *****
|
2020-03-27 20:09:59 +00:00
|
|
|
EditCylinder::EditCylinder(int index, cylinder_t cylIn, EditCylinderType typeIn, bool currentDiveOnly) :
|
|
|
|
EditCylinderBase(index, currentDiveOnly, false, editCylinderTypeToFlags(typeIn)),
|
|
|
|
type(typeIn)
|
2020-02-23 10:43:50 +00:00
|
|
|
{
|
|
|
|
if (dives.empty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (dives.size() == 1)
|
2020-11-08 22:38:59 +00:00
|
|
|
setText(Command::Base::tr("Edit cylinder"));
|
2020-02-23 10:43:50 +00:00
|
|
|
else
|
2020-11-08 22:38:59 +00:00
|
|
|
setText(Command::Base::tr("Edit cylinder (%n dive(s))", "", dives.size()));
|
2020-02-23 10:43:50 +00:00
|
|
|
|
|
|
|
// Try to untranslate the cylinder type
|
2020-03-27 08:22:11 +00:00
|
|
|
QString description = cylIn.type.description;
|
2020-12-11 21:34:35 +00:00
|
|
|
for (int i = 0; i < tank_info_table.nr; ++i) {
|
|
|
|
if (gettextFromC::tr(tank_info_table.infos[i].name) == description) {
|
|
|
|
description = tank_info_table.infos[i].name;
|
2020-02-23 10:43:50 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 08:22:11 +00:00
|
|
|
// Update the tank info model
|
2020-02-23 10:43:50 +00:00
|
|
|
TankInfoModel *tim = TankInfoModel::instance();
|
2020-03-27 08:22:11 +00:00
|
|
|
QModelIndexList matches = tim->match(tim->index(0, 0), Qt::DisplayRole, gettextFromC::tr(cylIn.type.description));
|
2020-02-23 10:43:50 +00:00
|
|
|
if (!matches.isEmpty()) {
|
2020-03-27 08:22:11 +00:00
|
|
|
if (cylIn.type.size.mliter != cyl[0].type.size.mliter)
|
|
|
|
tim->setData(tim->index(matches.first().row(), TankInfoModel::ML), cylIn.type.size.mliter);
|
|
|
|
if (cylIn.type.workingpressure.mbar != cyl[0].type.workingpressure.mbar)
|
|
|
|
tim->setData(tim->index(matches.first().row(), TankInfoModel::BAR), cylIn.type.workingpressure.mbar / 1000.0);
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
|
2020-03-27 08:22:11 +00:00
|
|
|
// The base class copied the cylinders for us, let's edit them
|
|
|
|
for (int i = 0; i < (int)indexes.size(); ++i) {
|
2020-03-27 20:09:59 +00:00
|
|
|
switch (type) {
|
|
|
|
case EditCylinderType::TYPE:
|
|
|
|
free((void *)cyl[i].type.description);
|
|
|
|
cyl[i].type = cylIn.type;
|
|
|
|
cyl[i].type.description = copy_qstring(description);
|
|
|
|
cyl[i].cylinder_use = cylIn.cylinder_use;
|
|
|
|
break;
|
|
|
|
case EditCylinderType::PRESSURE:
|
|
|
|
cyl[i].start.mbar = cylIn.start.mbar;
|
|
|
|
cyl[i].end.mbar = cylIn.end.mbar;
|
|
|
|
break;
|
|
|
|
case EditCylinderType::GASMIX:
|
|
|
|
cyl[i].gasmix = cylIn.gasmix;
|
|
|
|
cyl[i].bestmix_o2 = cylIn.bestmix_o2;
|
|
|
|
cyl[i].bestmix_he = cylIn.bestmix_he;
|
2021-01-10 20:17:14 +00:00
|
|
|
sanitize_gasmix(&cyl[i].gasmix);
|
2020-03-27 20:09:59 +00:00
|
|
|
break;
|
|
|
|
}
|
2020-03-27 08:22:11 +00:00
|
|
|
}
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void EditCylinder::redo()
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < dives.size(); ++i) {
|
2020-08-20 05:31:04 +00:00
|
|
|
std::swap(*get_cylinder(dives[i], indexes[i]), cyl[i]);
|
2020-05-05 09:34:16 +00:00
|
|
|
update_cylinder_related_info(dives[i]);
|
2020-02-23 10:43:50 +00:00
|
|
|
emit diveListNotifier.cylinderEdited(dives[i], indexes[i]);
|
2020-04-03 12:25:43 +00:00
|
|
|
invalidate_dive_cache(dives[i]); // Ensure that dive is written in git_save()
|
2020-02-23 10:43:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Undo and redo do the same as just the stored value is exchanged
|
|
|
|
void EditCylinder::undo()
|
|
|
|
{
|
|
|
|
redo();
|
|
|
|
}
|
|
|
|
|
2022-05-21 19:13:32 +00:00
|
|
|
EditSensors::EditSensors(int toCylinderIn, int fromCylinderIn, int dcNr)
|
|
|
|
: d(current_dive), dc(get_dive_dc(d, dcNr)), toCylinder(toCylinderIn), fromCylinder(fromCylinderIn)
|
2022-02-19 17:47:25 +00:00
|
|
|
{
|
|
|
|
if (!d || !dc)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setText(Command::Base::tr("Edit sensors"));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void EditSensors::mapSensors(int toCyl, int fromCyl)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < dc->samples; ++i) {
|
|
|
|
for (int s = 0; s < MAX_SENSORS; ++s) {
|
|
|
|
if (dc->sample[i].pressure[s].mbar && dc->sample[i].sensor[s] == fromCyl)
|
|
|
|
dc->sample[i].sensor[s] = toCyl;
|
2022-09-02 20:51:19 +00:00
|
|
|
// In case the cylinder we are moving to has a sensor attached, move it to the other cylinder
|
|
|
|
else if (dc->sample[i].pressure[s].mbar && dc->sample[i].sensor[s] == toCyl)
|
|
|
|
dc->sample[i].sensor[s] = fromCyl;
|
2022-02-19 17:47:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
emit diveListNotifier.diveComputerEdited(dc);
|
|
|
|
invalidate_dive_cache(d); // Ensure that dive is written in git_save()
|
|
|
|
}
|
|
|
|
|
|
|
|
void EditSensors::undo()
|
|
|
|
{
|
|
|
|
mapSensors(fromCylinder, toCylinder);
|
|
|
|
}
|
|
|
|
|
|
|
|
void EditSensors::redo()
|
|
|
|
{
|
|
|
|
mapSensors(toCylinder, fromCylinder);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EditSensors::workToBeDone()
|
|
|
|
{
|
|
|
|
return d && dc;
|
|
|
|
}
|
|
|
|
|
2020-01-10 00:25:37 +00:00
|
|
|
#ifdef SUBSURFACE_MOBILE
|
|
|
|
|
|
|
|
EditDive::EditDive(dive *oldDiveIn, dive *newDiveIn, dive_site *createDs, dive_site *editDs, location_t dsLocationIn)
|
|
|
|
: oldDive(oldDiveIn)
|
|
|
|
, newDive(newDiveIn)
|
2020-04-04 14:35:24 +00:00
|
|
|
, newDiveSite(newDiveIn->dive_site)
|
2020-01-10 00:25:37 +00:00
|
|
|
, changedFields(DiveField::NONE)
|
|
|
|
, siteToRemove(nullptr)
|
|
|
|
, siteToAdd(createDs)
|
|
|
|
, siteToEdit(editDs)
|
|
|
|
, dsLocation(dsLocationIn)
|
|
|
|
{
|
2020-04-04 14:35:24 +00:00
|
|
|
if (!oldDive || !newDive)
|
2020-01-10 00:25:37 +00:00
|
|
|
return;
|
|
|
|
|
2020-03-21 23:46:36 +00:00
|
|
|
setText(Command::Base::tr("Edit dive [%1]").arg(diveNumberOrDate(oldDive)));
|
2020-01-10 00:25:37 +00:00
|
|
|
|
|
|
|
// Calculate the fields that changed.
|
|
|
|
// Note: Probably not needed, as on mobile we don't have that granularity.
|
|
|
|
// However, for future-proofeness let's just do it.
|
|
|
|
changedFields = DiveField::NONE;
|
|
|
|
if (oldDive->number != newDive->number)
|
|
|
|
changedFields |= DiveField::NR;
|
|
|
|
if (oldDive->when != newDive->when)
|
|
|
|
changedFields |= DiveField::DATETIME;
|
|
|
|
if (oldDive->maxdepth.mm != newDive->maxdepth.mm)
|
|
|
|
changedFields |= DiveField::DEPTH;
|
|
|
|
if (oldDive->duration.seconds != newDive->duration.seconds)
|
|
|
|
changedFields |= DiveField::DURATION;
|
|
|
|
if (oldDive->airtemp.mkelvin != newDive->airtemp.mkelvin)
|
|
|
|
changedFields |= DiveField::AIR_TEMP;
|
|
|
|
if (oldDive->watertemp.mkelvin != newDive->watertemp.mkelvin)
|
|
|
|
changedFields |= DiveField::WATER_TEMP;
|
|
|
|
if (oldDive->surface_pressure.mbar != newDive->surface_pressure.mbar)
|
|
|
|
changedFields |= DiveField::ATM_PRESS;
|
|
|
|
if (oldDive->dive_site != newDive->dive_site)
|
|
|
|
changedFields |= DiveField::DIVESITE;
|
2022-02-12 13:03:18 +00:00
|
|
|
if (!same_string(oldDive->diveguide, newDive->diveguide))
|
|
|
|
changedFields |= DiveField::DIVEGUIDE;
|
2020-01-10 00:25:37 +00:00
|
|
|
if (!same_string(oldDive->buddy, newDive->buddy))
|
|
|
|
changedFields |= DiveField::BUDDY;
|
|
|
|
if (oldDive->rating != newDive->rating)
|
|
|
|
changedFields |= DiveField::RATING;
|
|
|
|
if (oldDive->visibility != newDive->visibility)
|
|
|
|
changedFields |= DiveField::VISIBILITY;
|
|
|
|
if (oldDive->wavesize != newDive->wavesize)
|
|
|
|
changedFields |= DiveField::WAVESIZE;
|
|
|
|
if (oldDive->current != newDive->current)
|
|
|
|
changedFields |= DiveField::CURRENT;
|
|
|
|
if (oldDive->surge != newDive->surge)
|
|
|
|
changedFields |= DiveField::SURGE;
|
|
|
|
if (oldDive->chill != newDive->chill)
|
|
|
|
changedFields |= DiveField::CHILL;
|
|
|
|
if (!same_string(oldDive->suit, newDive->suit))
|
|
|
|
changedFields |= DiveField::SUIT;
|
|
|
|
if (get_taglist_string(oldDive->tag_list) != get_taglist_string(newDive->tag_list)) // This is cheating. Do we have a taglist comparison function?
|
|
|
|
changedFields |= DiveField::TAGS;
|
|
|
|
if (oldDive->dc.divemode != newDive->dc.divemode)
|
|
|
|
changedFields |= DiveField::MODE;
|
|
|
|
if (!same_string(oldDive->notes, newDive->notes))
|
|
|
|
changedFields |= DiveField::NOTES;
|
|
|
|
if (oldDive->salinity != newDive->salinity)
|
|
|
|
changedFields |= DiveField::SALINITY;
|
2020-04-04 14:35:24 +00:00
|
|
|
|
|
|
|
newDive->dive_site = nullptr; // We will add the dive to the site manually and therefore saved the dive site.
|
2020-01-10 00:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void EditDive::undo()
|
|
|
|
{
|
|
|
|
if (siteToRemove) {
|
|
|
|
int idx = unregister_dive_site(siteToRemove);
|
|
|
|
siteToAdd.reset(siteToRemove);
|
|
|
|
emit diveListNotifier.diveSiteDeleted(siteToRemove, idx); // Inform frontend of removed dive site.
|
|
|
|
}
|
|
|
|
|
|
|
|
exchangeDives();
|
|
|
|
editDs();
|
|
|
|
}
|
|
|
|
|
|
|
|
void EditDive::redo()
|
|
|
|
{
|
|
|
|
if (siteToAdd) {
|
|
|
|
siteToRemove = siteToAdd.get();
|
|
|
|
int idx = register_dive_site(siteToAdd.release()); // Return ownership to backend.
|
|
|
|
emit diveListNotifier.diveSiteAdded(siteToRemove, idx); // Inform frontend of new dive site.
|
|
|
|
}
|
|
|
|
|
|
|
|
exchangeDives();
|
|
|
|
editDs();
|
|
|
|
}
|
|
|
|
|
|
|
|
void EditDive::exchangeDives()
|
|
|
|
{
|
2020-03-27 22:06:07 +00:00
|
|
|
// Set the filter flag of the new dive to the old dive.
|
|
|
|
// Reason: When we send the dive-changed signal, the model will
|
|
|
|
// track the *change* of the filter flag, so we must not overwrite
|
|
|
|
// it by swapping the dive data.
|
|
|
|
newDive->hidden_by_filter = oldDive->hidden_by_filter;
|
|
|
|
|
2020-03-27 22:21:37 +00:00
|
|
|
// Bluntly exchange dive data by shallow copy.
|
|
|
|
// Don't forget to unregister the old and register the new dive!
|
2020-04-04 14:35:24 +00:00
|
|
|
// Likewise take care to add/remove the dive from the dive site.
|
2020-03-27 22:21:37 +00:00
|
|
|
fulltext_unregister(oldDive);
|
2020-04-04 14:35:24 +00:00
|
|
|
dive_site *oldDiveSite = oldDive->dive_site;
|
|
|
|
if (oldDiveSite)
|
|
|
|
unregister_dive_from_dive_site(oldDive); // the dive-site pointer in the dive is now NULL
|
2020-01-10 00:25:37 +00:00
|
|
|
std::swap(*newDive, *oldDive);
|
2020-03-27 22:21:37 +00:00
|
|
|
fulltext_register(oldDive);
|
2020-04-04 14:35:24 +00:00
|
|
|
if (newDiveSite)
|
|
|
|
add_dive_to_dive_site(oldDive, newDiveSite);
|
|
|
|
newDiveSite = oldDiveSite; // remember the previous dive site
|
2020-01-10 00:25:37 +00:00
|
|
|
invalidate_dive_cache(oldDive);
|
|
|
|
|
|
|
|
// Changing times may have unsorted the dive and trip tables
|
|
|
|
QVector<dive *> dives = { oldDive };
|
|
|
|
timestamp_t delta = oldDive->when - newDive->when;
|
|
|
|
if (delta != 0) {
|
core: introduce divelog structure
The parser API was very annoying, as a number of tables
to-be-filled were passed in as pointers. The goal of this
commit is to collect all these tables in a single struct.
This should make it (more or less) clear what is actually
written into the divelog files.
Moreover, it should now be rather easy to search for
instances, where the global logfile is accessed (and it
turns out that there are many!).
The divelog struct does not contain the tables as substructs,
but only collects pointers. The idea is that the "divelog.h"
file can be included without all the other files describing
the numerous tables.
To make it easier to use from C++ parts of the code, the
struct implements a constructor and a destructor. Sadly,
we can't use smart pointers, since the pointers are accessed
from C code. Therfore the constructor and destructor are
quite complex.
The whole commit is large, but was mostly an automatic
conversion.
One oddity of note: the divelog structure also contains
the "autogroup" flag, since that is saved in the divelog.
This actually fixes a bug: Before, when importing dives
from a different log, the autogroup flag was overwritten.
This was probably not intended and does not happen anymore.
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2022-11-08 20:31:08 +00:00
|
|
|
sort_dive_table(divelog.dives);
|
|
|
|
sort_trip_table(divelog.trips);
|
2020-01-10 00:25:37 +00:00
|
|
|
if (newDive->divetrip != oldDive->divetrip)
|
|
|
|
qWarning("Command::EditDive::redo(): This command does not support moving between trips!");
|
|
|
|
if (oldDive->divetrip)
|
|
|
|
sort_dive_table(&newDive->divetrip->dives); // Keep the trip-table in order
|
|
|
|
emit diveListNotifier.divesTimeChanged(delta, dives);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send signals
|
|
|
|
emit diveListNotifier.divesChanged(dives, changedFields);
|
|
|
|
|
|
|
|
// Select the changed dives
|
2022-04-04 16:57:28 +00:00
|
|
|
setSelection( { oldDive }, oldDive, -1);
|
2020-01-10 00:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void EditDive::editDs()
|
|
|
|
{
|
|
|
|
if (siteToEdit) {
|
|
|
|
std::swap(siteToEdit->location, dsLocation);
|
|
|
|
emit diveListNotifier.diveSiteChanged(siteToEdit, LocationInformationModel::LOCATION); // Inform frontend of changed dive site.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EditDive::workToBeDone()
|
|
|
|
{
|
|
|
|
// We trust the frontend that an EditDive command is only created if there are changes.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif // SUBSURFACE_MOBILE
|
|
|
|
|
2019-01-25 17:27:31 +00:00
|
|
|
} // namespace Command
|