diff options
author | Berthold Stoeger <bstoeger@mail.tuwien.ac.at> | 2020-05-17 12:07:04 +0200 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2020-09-29 16:13:03 -0700 |
commit | c0af74ba88c0101a820da945b96258932c27bab5 (patch) | |
tree | bc2546400d325414dbd9fb6256677052d50426ed | |
parent | af9d379a4147b01ea599c86e0ff001cace926c1c (diff) | |
download | subsurface-c0af74ba88c0101a820da945b96258932c27bab5.tar.gz |
filter: add a filter constraint widget
This add a widget that represents a single filter constraint. Since
filter constraints are very general, the widget has to consider a
number of cases:
- numerical ranges
- star-widget ranges
- string lists
- multiple choice lists
Moreover, it supports units, which must be updated when the preferences
change.
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
-rw-r--r-- | desktop-widgets/CMakeLists.txt | 2 | ||||
-rw-r--r-- | desktop-widgets/filterconstraintwidget.cpp | 472 | ||||
-rw-r--r-- | desktop-widgets/filterconstraintwidget.h | 70 |
3 files changed, 544 insertions, 0 deletions
diff --git a/desktop-widgets/CMakeLists.txt b/desktop-widgets/CMakeLists.txt index 1de8914ee..a9a6a59a7 100644 --- a/desktop-widgets/CMakeLists.txt +++ b/desktop-widgets/CMakeLists.txt @@ -77,6 +77,8 @@ set(SUBSURFACE_INTERFACE divesiteimportdialog.h downloadfromdivecomputer.cpp downloadfromdivecomputer.h + filterconstraintwidget.cpp + filterconstraintwidget.h filterwidget2.cpp filterwidget2.h findmovedimagesdialog.cpp diff --git a/desktop-widgets/filterconstraintwidget.cpp b/desktop-widgets/filterconstraintwidget.cpp new file mode 100644 index 000000000..8cc8f130a --- /dev/null +++ b/desktop-widgets/filterconstraintwidget.cpp @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "filterconstraintwidget.h" +#include "starwidget.h" +#include "core/pref.h" +#include "desktop-widgets/preferences/preferencesdialog.h" +#include "qt-models/cleanertablemodel.h" // for trashIcon() +#include "qt-models/filterconstraintmodel.h" + +#include <QComboBox> +#include <QDateEdit> +#include <QDoubleSpinBox> +#include <QHBoxLayout> +#include <QLabel> +#include <QLineEdit> +#include <QListWidget> +#include <QPushButton> +#include <QTimeEdit> + +// Helper function to get enums through Qt's variants +template<typename T> +static T getEnum(const QModelIndex &idx, int role) +{ + return static_cast<T>(idx.data(role).value<int>()); +} + +// 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<QStringList>(); + 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<QStringList>(); + if (list.isEmpty()) + return nullptr; + QListWidget *res = new QListWidget; + res->addItems(list); + res->setSelectionMode(QAbstractItemView::ExtendedSelection); + 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<int>()); +} + +// 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<filter_constraint_type>(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<QString>())); + typeLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + QString unitString = index.data(FilterConstraintModel::UNIT_ROLE).value<QString>(); + negate.reset(makeCombo(index, FilterConstraintModel::NEGATE_COMBO_ROLE)); + connect(negate.get(), QOverload<int>::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>(); + bool hasDateWidget = index.data(FilterConstraintModel::HAS_DATE_WIDGET_ROLE).value<bool>(); + bool hasTimeWidget = index.data(FilterConstraintModel::HAS_TIME_WIDGET_ROLE).value<bool>(); + 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<int>() : 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<int>::of(&QComboBox::currentIndexChanged), this, &FilterConstraintWidget::stringModeEdited); + + if (rangeMode) { + toLabel.reset(new QLabel(tr("and"))); + connect(rangeMode.get(), QOverload<int>::of(&QComboBox::currentIndexChanged), this, &FilterConstraintWidget::rangeModeEdited); + } + + if (hasSpinBox) { + spinBoxFrom.reset(makeSpinBox(numDecimals)); + spinBoxTo.reset(makeSpinBox(numDecimals)); + connect(spinBoxFrom.get(), QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &FilterConstraintWidget::fromEditedFloat); + connect(spinBoxTo.get(), QOverload<double>::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(PreferencesDialog::instance(), &PreferencesDialog::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<QString>()); + if (starFrom) + starFrom->setCurrentStars(idx.data(FilterConstraintModel::INTEGER_FROM_ROLE).value<int>()); + if (starTo) + starTo->setCurrentStars(idx.data(FilterConstraintModel::INTEGER_TO_ROLE).value<int>()); + if (spinBoxFrom) + spinBoxFrom->setValue(idx.data(FilterConstraintModel::FLOAT_FROM_ROLE).value<double>()); + if (spinBoxTo) + spinBoxTo->setValue(idx.data(FilterConstraintModel::FLOAT_TO_ROLE).value<double>()); + if (dateFrom) { + QDateTime dateTime = idx.data(FilterConstraintModel::TIMESTAMP_FROM_ROLE).value<QDateTime>(); + dateFrom->setDateTime(dateTime); + if (timeFrom) + timeFrom->setDateTime(dateTime); + } else if (timeFrom) { + timeFrom->setTime(idx.data(FilterConstraintModel::TIME_FROM_ROLE).value<QTime>()); + } + if (dateTo) { + QDateTime dateTime = idx.data(FilterConstraintModel::TIMESTAMP_TO_ROLE).value<QDateTime>(); + dateTo->setDateTime(dateTime); + if (timeTo) + timeTo->setDateTime(dateTime); + } else if (timeTo) { + timeTo->setTime(idx.data(FilterConstraintModel::INTEGER_TO_ROLE).value<QTime>()); + } + if (multipleChoice) { + uint64_t bits = idx.data(FilterConstraintModel::MULTIPLE_CHOICE_ROLE).value<uint64_t>(); + for (int i = 0; i < multipleChoice->count(); ++i) + multipleChoice->item(i)->setSelected(bits & (1 << i)); + } + + // Update the unit strings in case the locale was changed + if (unitFrom || unitTo) { + QString unitString = idx.data(FilterConstraintModel::UNIT_ROLE).value<QString>(); + unitFrom->setText(unitString); + unitTo->setText(unitString); + } + + if (rangeMode) { + switch(getEnum<filter_constraint_range_mode>(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"); + bits |= 1 << 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); +} diff --git a/desktop-widgets/filterconstraintwidget.h b/desktop-widgets/filterconstraintwidget.h new file mode 100644 index 000000000..60e613780 --- /dev/null +++ b/desktop-widgets/filterconstraintwidget.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0 +#ifndef FILTERCONSTRAINTWIDGET_H +#define FILTERCONSTRAINTWIDGET_H + +#include "core/filterconstraint.h" +#include <QWidget> +#include <memory> + +class FilterConstraintModel; +class QComboBox; +class QDateEdit; +class QDoubleSpinBox; +class QGridLayout; +class QHBoxLayout; +class QLabel; +class QLineEdit; +class QListWidget; +class QPushButton; +class QTimeEdit; +class StarWidget; + +// Technically, this is not a single widget, but numerous widgets, +// which are added to the row of a QGridLayout +class FilterConstraintWidget : public QObject { + Q_OBJECT +public: + FilterConstraintWidget(FilterConstraintModel *model, const QModelIndex &index, QGridLayout *layout); + ~FilterConstraintWidget(); + void moveToRow(int row); // call if the row of the widget has changed. + // this will update the index used to access the model as well as the position in the layout + void update(); +private +slots: + void trash(); + void stringEdited(const QString &s); + void multipleChoiceEdited(); + void negateEdited(int index); + void rangeModeEdited(int index); + void stringModeEdited(int index); + void fromEditedInt(int i); + void toEditedInt(int i); + void fromEditedFloat(double f); + void toEditedFloat(double f); + void fromEditedTimestamp(const QDateTime &datetime); + void toEditedTimestamp(const QDateTime &datetime); +private: + QGridLayout *layout; + FilterConstraintModel *model; + int row; + const filter_constraint_type type; // we don't support changing the type + void addToLayout(); + void removeFromLayout(); + + std::unique_ptr<QHBoxLayout> rangeLayout; + std::unique_ptr<QPushButton> trashButton; + std::unique_ptr<QLabel> typeLabel; + std::unique_ptr<QComboBox> negate; + std::unique_ptr<QComboBox> stringMode; + std::unique_ptr<QComboBox> rangeMode; + std::unique_ptr<QListWidget> multipleChoice; + std::unique_ptr<QLineEdit> string; + std::unique_ptr<StarWidget> starFrom, starTo; + std::unique_ptr<QDoubleSpinBox> spinBoxFrom, spinBoxTo; + std::unique_ptr<QDateEdit> dateFrom, dateTo; + std::unique_ptr<QTimeEdit> timeFrom, timeTo; + std::unique_ptr<QLabel> unitFrom, unitTo; + std::unique_ptr<QLabel> toLabel; +}; + +#endif |