mirror of
https://github.com/subsurface/subsurface.git
synced 2024-12-01 06:30:26 +00:00
29c6aea797
Invalidating the filter can cause numerous selection-change notifications. These cause a full UI reload. Therefore, go into "command" mode that was implemented for the undo commands. Then, all selection-changes are considered as "programmatical" and ignored. At the end of filter invalidation, a filter-finished signal causes a proper reload anyway. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
356 lines
9.9 KiB
C++
356 lines
9.9 KiB
C++
// SPDX-License-Identifier: GPL-2.0
|
|
#include "qt-models/filtermodels.h"
|
|
#include "qt-models/models.h"
|
|
#include "core/display.h"
|
|
#include "core/qthelper.h"
|
|
#include "core/divesite.h"
|
|
#include "core/subsurface-string.h"
|
|
#include "core/subsurface-qt/DiveListNotifier.h"
|
|
#include "qt-models/divetripmodel.h"
|
|
|
|
#if !defined(SUBSURFACE_MOBILE)
|
|
#include "desktop-widgets/divelistview.h"
|
|
#include "desktop-widgets/mainwindow.h"
|
|
#endif
|
|
|
|
#include <QDebug>
|
|
#include <algorithm>
|
|
|
|
namespace {
|
|
// Check if a string-list contains at least one string containing the second argument.
|
|
// Comparison is non case sensitive and removes white space.
|
|
bool listContainsSuperstring(const QStringList &list, const QString &s)
|
|
{
|
|
return std::any_of(list.begin(), list.end(), [&s](const QString &s2)
|
|
{ return s2.trimmed().contains(s.trimmed(), Qt::CaseInsensitive); } );
|
|
}
|
|
|
|
// Check whether either all, any or none of the items of the first list is
|
|
// in the second list as a super string.
|
|
// The mode is controlled by the second argument
|
|
bool check(const QStringList &items, const QStringList &list, FilterData::Mode mode)
|
|
{
|
|
bool negate = mode == FilterData::Mode::NONE_OF;
|
|
bool any_of = mode == FilterData::Mode::ANY_OF;
|
|
auto fun = [&list, negate](const QString &item)
|
|
{ return listContainsSuperstring(list, item) != negate; };
|
|
return any_of ? std::any_of(items.begin(), items.end(), fun)
|
|
: std::all_of(items.begin(), items.end(), fun);
|
|
}
|
|
|
|
bool hasTags(const QStringList &tags, const struct dive *d, FilterData::Mode mode)
|
|
{
|
|
if (tags.isEmpty())
|
|
return true;
|
|
QStringList dive_tags = get_taglist_string(d->tag_list).split(",");
|
|
dive_tags.append(gettextFromC::tr(divemode_text_ui[d->dc.divemode]));
|
|
return check(tags, dive_tags, mode);
|
|
}
|
|
|
|
bool hasPersons(const QStringList &people, const struct dive *d, FilterData::Mode mode)
|
|
{
|
|
if (people.isEmpty())
|
|
return true;
|
|
QStringList dive_people = QString(d->buddy).split(",", QString::SkipEmptyParts)
|
|
+ QString(d->divemaster).split(",", QString::SkipEmptyParts);
|
|
return check(people, dive_people, mode);
|
|
}
|
|
|
|
bool hasLocations(const QStringList &locations, const struct dive *d, FilterData::Mode mode)
|
|
{
|
|
if (locations.isEmpty())
|
|
return true;
|
|
QStringList diveLocations;
|
|
if (d->divetrip)
|
|
diveLocations.push_back(QString(d->divetrip->location));
|
|
|
|
if (d->dive_site)
|
|
diveLocations.push_back(QString(d->dive_site->name));
|
|
|
|
return check(locations, diveLocations, mode);
|
|
}
|
|
|
|
// TODO: Finish this implementation.
|
|
bool hasEquipment(const QStringList &, const struct dive *, FilterData::Mode)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool hasSuits(const QStringList &suits, const struct dive *d, FilterData::Mode mode)
|
|
{
|
|
if (suits.isEmpty())
|
|
return true;
|
|
QStringList diveSuits;
|
|
if (d->suit)
|
|
diveSuits.push_back(QString(d->suit));
|
|
return check(suits, diveSuits, mode);
|
|
}
|
|
|
|
bool hasNotes(const QStringList &dnotes, const struct dive *d, FilterData::Mode mode)
|
|
{
|
|
if (dnotes.isEmpty())
|
|
return true;
|
|
QStringList diveNotes;
|
|
if (d->notes)
|
|
diveNotes.push_back(QString(d->notes));
|
|
return check(dnotes, diveNotes, mode);
|
|
}
|
|
|
|
}
|
|
|
|
MultiFilterSortModel *MultiFilterSortModel::instance()
|
|
{
|
|
static MultiFilterSortModel self;
|
|
return &self;
|
|
}
|
|
|
|
MultiFilterSortModel::MultiFilterSortModel(QObject *parent) : QSortFilterProxyModel(parent),
|
|
divesDisplayed(0),
|
|
diveSiteRefCount(0)
|
|
{
|
|
setFilterKeyColumn(-1); // filter all columns
|
|
setFilterCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
connect(&diveListNotifier, &DiveListNotifier::divesAdded, this, &MultiFilterSortModel::divesAdded);
|
|
connect(&diveListNotifier, &DiveListNotifier::divesDeleted, this, &MultiFilterSortModel::divesDeleted);
|
|
}
|
|
|
|
void MultiFilterSortModel::resetModel(DiveTripModelBase::Layout layout)
|
|
{
|
|
DiveTripModelBase::resetModel(layout);
|
|
// DiveTripModelBase::resetModel() generates a new instance.
|
|
// Thus, the source model must be reset.
|
|
setSourceModel(DiveTripModelBase::instance());
|
|
}
|
|
|
|
bool MultiFilterSortModel::showDive(const struct dive *d) const
|
|
{
|
|
// If dive_sites is not empty, we are in a special dive-site filtering mode.
|
|
if (!dive_sites.isEmpty())
|
|
return dive_sites.contains(d->dive_site);
|
|
|
|
if (!filterData.validFilter)
|
|
return true;
|
|
|
|
if (d->visibility < filterData.minVisibility || d->visibility > filterData.maxVisibility)
|
|
return false;
|
|
|
|
if (d->rating < filterData.minRating || d->rating > filterData.maxRating)
|
|
return false;
|
|
|
|
auto temp_comp = prefs.units.temperature == units::CELSIUS ? C_to_mkelvin : F_to_mkelvin;
|
|
if (d->watertemp.mkelvin &&
|
|
(d->watertemp.mkelvin < (*temp_comp)(filterData.minWaterTemp) || d->watertemp.mkelvin > (*temp_comp)(filterData.maxWaterTemp)))
|
|
return false;
|
|
|
|
if (d->airtemp.mkelvin &&
|
|
(d->airtemp.mkelvin < (*temp_comp)(filterData.minAirTemp) || d->airtemp.mkelvin > (*temp_comp)(filterData.maxAirTemp)))
|
|
return false;
|
|
|
|
QDateTime t = filterData.fromDate;
|
|
t.setTime(filterData.fromTime);
|
|
if (filterData.fromDate.isValid() && filterData.fromTime.isValid() &&
|
|
d->when < t.toMSecsSinceEpoch()/1000 + t.offsetFromUtc())
|
|
return false;
|
|
|
|
t = filterData.toDate;
|
|
t.setTime(filterData.toTime);
|
|
if (filterData.toDate.isValid() && filterData.toTime.isValid() &&
|
|
d->when > t.toMSecsSinceEpoch()/1000 + t.offsetFromUtc())
|
|
return false;
|
|
|
|
// tags.
|
|
if (!hasTags(filterData.tags, d, filterData.tagsMode))
|
|
return false;
|
|
|
|
// people
|
|
if (!hasPersons(filterData.people, d, filterData.peopleMode))
|
|
return false;
|
|
|
|
// Location
|
|
if (!hasLocations(filterData.location, d, filterData.locationMode))
|
|
return false;
|
|
|
|
// Suit
|
|
if (!hasSuits(filterData.suit, d, filterData.suitMode))
|
|
return false;
|
|
|
|
// Notes
|
|
if (!hasNotes(filterData.dnotes, d, filterData.dnotesMode))
|
|
return false;
|
|
|
|
if (!hasEquipment(filterData.equipment, d, filterData.equipmentMode))
|
|
return false;
|
|
|
|
// Planned/Logged
|
|
if (!filterData.logged && !has_planned(d, true))
|
|
return false;
|
|
if (!filterData.planned && !has_planned(d, false))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MultiFilterSortModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
|
{
|
|
QModelIndex index0 = sourceModel()->index(source_row, 0, source_parent);
|
|
struct dive *d = sourceModel()->data(index0, DiveTripModelBase::DIVE_ROLE).value<struct dive *>();
|
|
|
|
// For dives, simply check the hidden_by_filter flag
|
|
if (d)
|
|
return !d->hidden_by_filter;
|
|
|
|
// Since this is not a dive, it must be a trip
|
|
dive_trip *trip = sourceModel()->data(index0, DiveTripModelBase::TRIP_ROLE).value<dive_trip *>();
|
|
|
|
if (!trip)
|
|
return false; // Oops. Neither dive nor trip, something is seriously wrong.
|
|
|
|
// Show the trip if any dive is visible
|
|
for (int i = 0; i < trip->dives.nr; ++i) {
|
|
if (!trip->dives.dives[i]->hidden_by_filter)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MultiFilterSortModel::filterChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles)
|
|
{
|
|
// Only redo the filter if a checkbox changed. If the count of an entry changed,
|
|
// we do *not* want to recalculate the filters.
|
|
if (roles.contains(Qt::CheckStateRole))
|
|
myInvalidate();
|
|
}
|
|
|
|
void MultiFilterSortModel::myInvalidate()
|
|
{
|
|
int i;
|
|
struct dive *d;
|
|
|
|
{
|
|
// This marker prevents the UI from getting notifications on selection changes.
|
|
// It is active until the end of the scope.
|
|
// This is actually meant for the undo-commands, so that they can do their work
|
|
// without having the UI updated.
|
|
// Here, it is used because invalidating the filter can generate numerous
|
|
// selection changes, which do full ui reloads. Instead, do that all at once
|
|
// as a consequence of the filterFinished signal right after the local scope.
|
|
auto marker = diveListNotifier.enterCommand();
|
|
|
|
divesDisplayed = 0;
|
|
|
|
// Apply filter for each dive
|
|
for_each_dive (i, d) {
|
|
bool show = showDive(d);
|
|
filter_dive(d, show);
|
|
if (show)
|
|
divesDisplayed++;
|
|
}
|
|
|
|
invalidateFilter();
|
|
|
|
// Tell the dive trip model to update the displayed-counts
|
|
DiveTripModelBase::instance()->filterFinished();
|
|
countsChanged();
|
|
}
|
|
|
|
emit filterFinished();
|
|
|
|
#if !defined(SUBSURFACE_MOBILE)
|
|
if (!dive_sites.isEmpty())
|
|
MainWindow::instance()->diveList->expandAll();
|
|
#endif
|
|
}
|
|
|
|
bool MultiFilterSortModel::updateDive(struct dive *d)
|
|
{
|
|
bool oldStatus = !d->hidden_by_filter;
|
|
bool newStatus = showDive(d);
|
|
bool changed = oldStatus != newStatus;
|
|
if (changed) {
|
|
filter_dive(d, newStatus);
|
|
divesDisplayed += newStatus - oldStatus;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
void MultiFilterSortModel::clearFilter()
|
|
{
|
|
myInvalidate();
|
|
}
|
|
|
|
void MultiFilterSortModel::startFilterDiveSites(QVector<dive_site *> ds)
|
|
{
|
|
dive_sites = ds;
|
|
if (++diveSiteRefCount > 1) {
|
|
setFilterDiveSite(ds);
|
|
} else {
|
|
std::sort(ds.begin(), ds.end());
|
|
dive_sites = ds;
|
|
myInvalidate();
|
|
}
|
|
}
|
|
|
|
void MultiFilterSortModel::stopFilterDiveSites()
|
|
{
|
|
if (--diveSiteRefCount > 0)
|
|
return;
|
|
dive_sites.clear();
|
|
myInvalidate();
|
|
}
|
|
|
|
void MultiFilterSortModel::setFilterDiveSite(QVector<dive_site *> ds)
|
|
{
|
|
// If the filter didn't change, return early to avoid a full
|
|
// map reload. For a well-defined comparison, sort the vector first.
|
|
std::sort(ds.begin(), ds.end());
|
|
if (ds == dive_sites)
|
|
return;
|
|
dive_sites = ds;
|
|
myInvalidate();
|
|
}
|
|
|
|
const QVector<dive_site *> &MultiFilterSortModel::filteredDiveSites() const
|
|
{
|
|
return dive_sites;
|
|
}
|
|
|
|
bool MultiFilterSortModel::diveSiteMode() const
|
|
{
|
|
return diveSiteRefCount > 0;
|
|
}
|
|
|
|
bool MultiFilterSortModel::lessThan(const QModelIndex &i1, const QModelIndex &i2) const
|
|
{
|
|
// Hand sorting down to the source model.
|
|
return DiveTripModelBase::instance()->lessThan(i1, i2);
|
|
}
|
|
|
|
void MultiFilterSortModel::filterDataChanged(const FilterData &data)
|
|
{
|
|
filterData = data;
|
|
myInvalidate();
|
|
}
|
|
|
|
void MultiFilterSortModel::divesAdded(dive_trip *, bool, const QVector<dive *> &dives)
|
|
{
|
|
for (dive *d: dives) {
|
|
if (!d->hidden_by_filter)
|
|
++divesDisplayed;
|
|
}
|
|
countsChanged();
|
|
}
|
|
|
|
void MultiFilterSortModel::divesDeleted(dive_trip *, bool, const QVector<dive *> &dives)
|
|
{
|
|
for (dive *d: dives) {
|
|
if (!d->hidden_by_filter)
|
|
--divesDisplayed;
|
|
}
|
|
countsChanged();
|
|
}
|
|
|
|
void MultiFilterSortModel::countsChanged()
|
|
{
|
|
updateWindowTitle();
|
|
}
|