// SPDX-License-Identifier: GPL-2.0 #include "filterconstraintwidget.h" #include "starwidget.h" #include "core/pref.h" #include "core/subsurface-qt/divelistnotifier.h" #include "qt-models/cleanertablemodel.h" // for trashIcon() #include "qt-models/filterconstraintmodel.h" #include #include #include #include #include #include #include #include #include // Helper function to get enums through Qt's variants template static T getEnum(const QModelIndex &idx, int role) { return static_cast(idx.data(role).value()); } // Helper function, which creates a combo box for a given model role // or returns null if the model doesn't return a proper list. static QComboBox *makeCombo(const QModelIndex &index, int role) { QStringList list = index.data(role).value(); if (list.isEmpty()) return nullptr; QComboBox *res = new QComboBox; res->addItems(list); res->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); return res; } // Helper function, which creates a multiple choice list for a given model role // or returns null if the model doesn't return a proper list. static QListWidget *makeMultipleChoice(const QModelIndex &index, int role) { QStringList list = index.data(role).value(); if (list.isEmpty()) return nullptr; QListWidget *res = new QListWidget; res->addItems(list); res->setSizePolicy(QSizePolicy::Maximum,QSizePolicy::Maximum); res->setSelectionMode(QAbstractItemView::ExtendedSelection); res->setFixedSize(res->sizeHintForColumn(0) + 2 * res->frameWidth(), res->sizeHintForRow(0) * res->count() + 2 * res->frameWidth()); return res; } // Helper function to create a floating point spin box. // Currently, this allows a large range of values. // The limits should be adapted to the constraint in question. static QDoubleSpinBox *makeSpinBox(int numDecimals) { QDoubleSpinBox *res = new QDoubleSpinBox; res->setRange(-10000.0, 10000.0); res->setDecimals(numDecimals); res->setSingleStep(pow(10.0, -numDecimals)); return res; } // Helper function to create a date edit widget static QDateEdit *makeDateEdit() { QDateEdit *res = new QDateEdit; res->setCalendarPopup(true); res->setTimeSpec(Qt::UTC); return res; } // Helper function to create a date edit widget static QTimeEdit *makeTimeEdit() { QTimeEdit *res = new QTimeEdit; res->setTimeSpec(Qt::UTC); return res; } // Helper function, which creates a label with a given string // or returns null if the string is empty. static QLabel *makeLabel(const QString &s) { if (s.isEmpty()) return nullptr; return new QLabel(s); } // Helper function, which sets the index of a combo box for a given // model role, if the combo box is not null. static void setIndex(QComboBox *w, const QModelIndex &index, int role) { if (!w) return; w->setCurrentIndex(index.data(role).value()); } // Helper function to add a widget to a layout if it is non-null static void addWidgetToLayout(QWidget *w, QHBoxLayout *l) { if (w) { l->addWidget(w); l->setAlignment(w, Qt::AlignLeft); } } // Helper functions that show / hide non-null widgets static void showWidget(QWidget *w) { if (w) w->show(); } // Helper functions that show / hide non-null widgets static void hideWidget(QWidget *w) { if (w) w->hide(); } // Helper function to create datatimes from either a single date widget, // or a combination of a date and a time widget. static QDateTime makeDateTime(const QDateEdit *d, const QTimeEdit *t) { if (!d) return QDateTime(); if (!t) return d->dateTime(); QDateTime res = d->dateTime(); res.setTime(t->time()); return res; } FilterConstraintWidget::FilterConstraintWidget(FilterConstraintModel *modelIn, const QModelIndex &index, QGridLayout *layoutIn) : QObject(layoutIn), layout(layoutIn), model(modelIn), row(index.row()), type(getEnum(index, FilterConstraintModel::TYPE_ROLE)) { rangeLayout.reset(new QHBoxLayout); rangeLayout->setAlignment(Qt::AlignLeft); trashButton.reset(new QPushButton(QIcon(trashIcon()), QString())); trashButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); trashButton->setToolTip(tr("Click to remove this constraint")); connect(trashButton.get(), &QPushButton::clicked, this, &FilterConstraintWidget::trash); typeLabel.reset(new QLabel(index.data(FilterConstraintModel::TYPE_DISPLAY_ROLE).value())); typeLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); QString unitString = index.data(FilterConstraintModel::UNIT_ROLE).value(); negate.reset(makeCombo(index, FilterConstraintModel::NEGATE_COMBO_ROLE)); connect(negate.get(), QOverload::of(&QComboBox::currentIndexChanged), this, &FilterConstraintWidget::negateEdited); stringMode.reset(makeCombo(index, FilterConstraintModel::STRING_MODE_COMBO_ROLE)); rangeMode.reset(makeCombo(index, FilterConstraintModel::RANGE_MODE_COMBO_ROLE)); multipleChoice.reset(makeMultipleChoice(index, FilterConstraintModel::MULTIPLE_CHOICE_LIST_ROLE)); bool isStarWidget = index.data(FilterConstraintModel::IS_STAR_WIDGET_ROLE).value(); bool hasDateWidget = index.data(FilterConstraintModel::HAS_DATE_WIDGET_ROLE).value(); bool hasTimeWidget = index.data(FilterConstraintModel::HAS_TIME_WIDGET_ROLE).value(); bool hasSpinBox = rangeMode && !stringMode && !hasDateWidget && !hasTimeWidget && !isStarWidget && !multipleChoice; unitFrom.reset(makeLabel(unitString)); unitTo.reset(makeLabel(unitString)); if (stringMode) { string.reset(new QLineEdit); string->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); } int numDecimals = hasSpinBox ? index.data(FilterConstraintModel::NUM_DECIMALS_ROLE).value() : 0; if (string) connect(string.get(), &QLineEdit::textEdited, this, &FilterConstraintWidget::stringEdited); if (multipleChoice) connect(multipleChoice.get(), &QListWidget::itemSelectionChanged, this, &FilterConstraintWidget::multipleChoiceEdited); if (stringMode) connect(stringMode.get(), QOverload::of(&QComboBox::currentIndexChanged), this, &FilterConstraintWidget::stringModeEdited); if (rangeMode) { toLabel.reset(new QLabel(tr("and"))); connect(rangeMode.get(), QOverload::of(&QComboBox::currentIndexChanged), this, &FilterConstraintWidget::rangeModeEdited); } if (hasSpinBox) { spinBoxFrom.reset(makeSpinBox(numDecimals)); spinBoxTo.reset(makeSpinBox(numDecimals)); connect(spinBoxFrom.get(), QOverload::of(&QDoubleSpinBox::valueChanged), this, &FilterConstraintWidget::fromEditedFloat); connect(spinBoxTo.get(), QOverload::of(&QDoubleSpinBox::valueChanged), this, &FilterConstraintWidget::toEditedFloat); } if (isStarWidget) { starFrom.reset(new StarWidget); starTo.reset(new StarWidget); connect(starFrom.get(), &StarWidget::valueChanged, this, &FilterConstraintWidget::fromEditedInt); connect(starTo.get(), &StarWidget::valueChanged, this, &FilterConstraintWidget::toEditedInt); } if (hasDateWidget) { dateFrom.reset(makeDateEdit()); dateTo.reset(makeDateEdit()); connect(dateFrom.get(), &QDateEdit::dateTimeChanged, this, &FilterConstraintWidget::fromEditedTimestamp); connect(dateTo.get(), &QDateEdit::dateTimeChanged, this, &FilterConstraintWidget::toEditedTimestamp); } if (hasTimeWidget) { timeFrom.reset(makeTimeEdit()); timeTo.reset(makeTimeEdit()); connect(timeFrom.get(), &QTimeEdit::dateTimeChanged, this, &FilterConstraintWidget::fromEditedTimestamp); connect(timeTo.get(), &QTimeEdit::dateTimeChanged, this, &FilterConstraintWidget::toEditedTimestamp); } addWidgetToLayout(string.get(), rangeLayout.get()); addWidgetToLayout(starFrom.get(), rangeLayout.get()); addWidgetToLayout(spinBoxFrom.get(), rangeLayout.get()); addWidgetToLayout(dateFrom.get(), rangeLayout.get()); addWidgetToLayout(timeFrom.get(), rangeLayout.get()); addWidgetToLayout(unitFrom.get(), rangeLayout.get()); addWidgetToLayout(toLabel.get(), rangeLayout.get()); addWidgetToLayout(starTo.get(), rangeLayout.get()); addWidgetToLayout(spinBoxTo.get(), rangeLayout.get()); addWidgetToLayout(dateTo.get(), rangeLayout.get()); addWidgetToLayout(timeTo.get(), rangeLayout.get()); addWidgetToLayout(unitTo.get(), rangeLayout.get()); rangeLayout->addStretch(); // Update the widget if the settings changed to reflect new units. connect(&diveListNotifier, &DiveListNotifier::settingsChanged, this, &FilterConstraintWidget::update); addToLayout(); update(); } FilterConstraintWidget::~FilterConstraintWidget() { } void FilterConstraintWidget::addToLayout() { // Careful: this must mirror the code in removeFromLayout() or weird things will happen. // Note: we add 2 to row, because the first and second row of the filter layout // are reserved for the title line and the fulltext widget! int layoutRow = row + 2; layout->addWidget(trashButton.get(), layoutRow, 0); if (!stringMode && !rangeMode && !multipleChoice) { // If there are no string or range modes, we rearrange to "negate / type" to get // a layout of the type "is not planned" or "is not logged", where the subject is // implicitly "dive". This presumes SVO grammar, but so does the rest of the layout. layout->addWidget(negate.get(), layoutRow, 1, Qt::AlignLeft); layout->addWidget(typeLabel.get(), layoutRow, 2, Qt::AlignLeft); } else { layout->addWidget(negate.get(), layoutRow, 2, Qt::AlignLeft); layout->addWidget(typeLabel.get(), layoutRow, 1, Qt::AlignLeft); } if (stringMode) layout->addWidget(stringMode.get(), layoutRow, 3, Qt::AlignLeft); else if (rangeMode) layout->addWidget(rangeMode.get(), layoutRow, 3, Qt::AlignLeft); else if (multipleChoice) layout->addWidget(multipleChoice.get(), layoutRow, 3, 1, 2, Qt::AlignLeft); // column span 2 if (!multipleChoice) layout->addLayout(rangeLayout.get(), layoutRow, 4, Qt::AlignLeft); } void FilterConstraintWidget::removeFromLayout() { // Careful: this must mirror the code in addToLayout() or weird things will happen. layout->removeWidget(trashButton.get()); layout->removeWidget(negate.get()); layout->removeWidget(typeLabel.get()); if (stringMode) layout->removeWidget(stringMode.get()); else if (rangeMode) layout->removeWidget(rangeMode.get()); else if (multipleChoice) layout->removeWidget(multipleChoice.get()); if (!multipleChoice) layout->removeItem(rangeLayout.get()); } void FilterConstraintWidget::update() { // The user might have changed the date and/or time format. Let's update the widgets. if (dateFrom) dateFrom->setDisplayFormat(prefs.date_format); if (dateTo) dateTo->setDisplayFormat(prefs.date_format); if (timeFrom) timeFrom->setDisplayFormat(prefs.time_format); if (timeTo) timeTo->setDisplayFormat(prefs.time_format); QModelIndex idx = model->index(row, 0); setIndex(negate.get(), idx, FilterConstraintModel::NEGATE_INDEX_ROLE); setIndex(stringMode.get(), idx, FilterConstraintModel::STRING_MODE_INDEX_ROLE); setIndex(rangeMode.get(), idx, FilterConstraintModel::RANGE_MODE_INDEX_ROLE); if (string) string->setText(idx.data(FilterConstraintModel::STRING_ROLE).value()); if (starFrom) starFrom->setCurrentStars(idx.data(FilterConstraintModel::INTEGER_FROM_ROLE).value()); if (starTo) starTo->setCurrentStars(idx.data(FilterConstraintModel::INTEGER_TO_ROLE).value()); if (spinBoxFrom) spinBoxFrom->setValue(idx.data(FilterConstraintModel::FLOAT_FROM_ROLE).value()); if (spinBoxTo) spinBoxTo->setValue(idx.data(FilterConstraintModel::FLOAT_TO_ROLE).value()); if (dateFrom) { QDateTime dateTime = idx.data(FilterConstraintModel::TIMESTAMP_FROM_ROLE).value(); dateFrom->setDateTime(dateTime); if (timeFrom) timeFrom->setDateTime(dateTime); } else if (timeFrom) { timeFrom->setTime(idx.data(FilterConstraintModel::TIME_FROM_ROLE).value()); } if (dateTo) { QDateTime dateTime = idx.data(FilterConstraintModel::TIMESTAMP_TO_ROLE).value(); dateTo->setDateTime(dateTime); if (timeTo) timeTo->setDateTime(dateTime); } else if (timeTo) { timeTo->setTime(idx.data(FilterConstraintModel::INTEGER_TO_ROLE).value()); } if (multipleChoice) { uint64_t bits = idx.data(FilterConstraintModel::MULTIPLE_CHOICE_ROLE).value(); for (int i = 0; i < multipleChoice->count(); ++i) multipleChoice->item(i)->setSelected(bits & (1ULL << i)); } // Update the unit strings in case the locale was changed if (unitFrom && unitTo) { QString unitString = idx.data(FilterConstraintModel::UNIT_ROLE).value(); unitFrom->setText(unitString); unitTo->setText(unitString); } if (rangeMode) { switch(getEnum(idx, FilterConstraintModel::RANGE_MODE_ROLE)) { case FILTER_CONSTRAINT_LESS: hideWidget(toLabel.get()); hideWidget(starFrom.get()); hideWidget(spinBoxFrom.get()); hideWidget(dateFrom.get()); hideWidget(timeFrom.get()); hideWidget(unitFrom.get()); showWidget(starTo.get()); showWidget(spinBoxTo.get()); showWidget(dateTo.get()); showWidget(timeTo.get()); showWidget(unitTo.get()); break; case FILTER_CONSTRAINT_EQUAL: case FILTER_CONSTRAINT_GREATER: hideWidget(toLabel.get()); showWidget(starFrom.get()); showWidget(spinBoxFrom.get()); showWidget(dateFrom.get()); showWidget(timeFrom.get()); showWidget(unitFrom.get()); hideWidget(starTo.get()); hideWidget(spinBoxTo.get()); hideWidget(dateTo.get()); hideWidget(timeTo.get()); hideWidget(unitTo.get()); break; case FILTER_CONSTRAINT_RANGE: showWidget(toLabel.get()); showWidget(starFrom.get()); showWidget(spinBoxFrom.get()); showWidget(dateFrom.get()); showWidget(timeFrom.get()); showWidget(unitFrom.get()); showWidget(starTo.get()); showWidget(spinBoxTo.get()); showWidget(dateTo.get()); showWidget(timeTo.get()); showWidget(unitTo.get()); break; } } } void FilterConstraintWidget::stringEdited(const QString &s) { QModelIndex idx = model->index(row, 0); model->setData(idx, s, FilterConstraintModel::STRING_ROLE); } void FilterConstraintWidget::multipleChoiceEdited() { // Turn selected items into bit-field uint64_t bits = 0; for (const QModelIndex &idx: multipleChoice->selectionModel()->selectedIndexes()) { int row = idx.row(); if (row >= 64) { qWarning("FilterConstraint: multiple-choice with more than 64 entries not supported"); continue; } bits |= 1ULL << row; } QModelIndex idx = model->index(row, 0); model->setData(idx, qulonglong(bits), FilterConstraintModel::MULTIPLE_CHOICE_ROLE); } void FilterConstraintWidget::fromEditedInt(int i) { QModelIndex idx = model->index(row, 0); model->setData(idx, i, FilterConstraintModel::INTEGER_FROM_ROLE); } void FilterConstraintWidget::toEditedInt(int i) { QModelIndex idx = model->index(row, 0); model->setData(idx, i, FilterConstraintModel::INTEGER_TO_ROLE); } void FilterConstraintWidget::fromEditedFloat(double f) { QModelIndex idx = model->index(row, 0); model->setData(idx, f, FilterConstraintModel::FLOAT_FROM_ROLE); } void FilterConstraintWidget::toEditedFloat(double f) { QModelIndex idx = model->index(row, 0); model->setData(idx, f, FilterConstraintModel::FLOAT_TO_ROLE); } void FilterConstraintWidget::fromEditedTimestamp(const QDateTime &datetime) { QModelIndex idx = model->index(row, 0); if (!dateFrom && timeFrom) model->setData(idx, timeFrom->time(), FilterConstraintModel::TIME_FROM_ROLE); else model->setData(idx, makeDateTime(dateFrom.get(), timeFrom.get()), FilterConstraintModel::TIMESTAMP_FROM_ROLE); } void FilterConstraintWidget::toEditedTimestamp(const QDateTime &datetime) { QModelIndex idx = model->index(row, 0); if (!dateTo && timeTo) model->setData(idx, timeTo->time(), FilterConstraintModel::TIME_TO_ROLE); else model->setData(idx, makeDateTime(dateTo.get(), timeTo.get()), FilterConstraintModel::TIMESTAMP_TO_ROLE); } void FilterConstraintWidget::negateEdited(int i) { QModelIndex idx = model->index(row, 0); model->setData(idx, i, FilterConstraintModel::NEGATE_INDEX_ROLE); } void FilterConstraintWidget::rangeModeEdited(int i) { QModelIndex idx = model->index(row, 0); model->setData(idx, i, FilterConstraintModel::RANGE_MODE_INDEX_ROLE); update(); // Range mode may change the shown widgets } void FilterConstraintWidget::stringModeEdited(int i) { QModelIndex idx = model->index(row, 0); model->setData(idx, i, FilterConstraintModel::STRING_MODE_INDEX_ROLE); } void FilterConstraintWidget::moveToRow(int rowIn) { removeFromLayout(); row = rowIn; addToLayout(); } void FilterConstraintWidget::trash() { model->deleteConstraint(row); }