Dialog to build filter queries for similar activities (#4805)

* list all (non-zero) fields and metrics
* filter list by field / metric name
* select an operator per field (ignore, equals, contains, larger than, ...)
* available in the context menus of activity- and calendar view
This commit is contained in:
Joachim Kohlhammer
2026-01-17 18:36:34 +01:00
committed by GitHub
parent 7a8f329a84
commit a00e27638f
16 changed files with 609 additions and 19 deletions

View File

@@ -31,6 +31,7 @@
#include "RepeatScheduleWizard.h"
#include "WorkoutFilter.h"
#include "IconManager.h"
#include "FilterSimilarDialog.h"
#include "SeasonDialogs.h"
#include "SaveDialogs.h"
@@ -333,6 +334,15 @@ CalendarWindow::CalendarWindow(Context *context)
}
}
});
connect(calendar, &Calendar::filterSimilar, this, [this](CalendarEntry activity) {
for (RideItem *rideItem : this->context->athlete->rideCache->rides()) {
if (rideItem != nullptr && rideItem->fileName == activity.reference) {
FilterSimilarDialog dlg(this->context, rideItem, this);
dlg.exec();
break;
}
}
});
connect(calendar, &Calendar::linkActivity, this, &CalendarWindow::linkActivities);
connect(calendar, &Calendar::unlinkActivity, this, &CalendarWindow::unlinkActivities);
connect(calendar, &Calendar::viewActivity, this, [this](CalendarEntry activity) {

View File

@@ -162,6 +162,31 @@ QString xmlprotect(const QString &string)
return s;
}
QString quoteEscape(const QString &string)
{
QString out;
out.reserve(string.size() * 2);
int backslashes = 0;
for (QChar c : string) {
if (c == '\\') {
backslashes++;
out += c;
} else if (c == '"') {
if (backslashes % 2 == 0) {
out += "\\\"";
} else {
out += '"';
}
backslashes = 0;
} else {
out += c;
backslashes = 0;
}
}
out.squeeze();
return out;
}
QString unescape(const QString &string)
{
// just unescape common \ characters

View File

@@ -40,6 +40,7 @@ namespace Utils
{
QString xmlprotect(const QString &string);
QString unprotect(const QString &buffer);
QString quoteEscape(const QString &string); // escape quotes (" -> \", \" -> \", \\" -> \\\", ...)
QString unescape(const QString &string); // simple string unescaping, used in datafilters
QString jsonprotect(const QString &buffer);
QString jsonunprotect(const QString &buffer);

View File

@@ -42,6 +42,9 @@
// Show in Train Mode
#include "WorkoutFilter.h"
// Filter for similar activities
#include "FilterSimilarDialog.h"
AnalysisSidebar::AnalysisSidebar(Context *context) : QWidget(context->mainWindow), context(context)
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
@@ -412,6 +415,16 @@ AnalysisSidebar::showActivityMenu(const QPoint &pos)
connect(actFindBest, SIGNAL(triggered(void)), this, SLOT(addIntervals(void)));
menu.addAction(actFindBest);
QAction *filterSimilar = new QAction(tr("Filter similar activities..."), rideNavigator);
connect(filterSimilar, &QAction::triggered, this, [this]() {
if (context->ride != nullptr) {
FilterSimilarDialog dlg(context, context->ride, this);
dlg.exec();
}
});
menu.addAction(filterSimilar);
if (rideItem->planned && rideItem->sport == "Bike") {
QString filter = buildWorkoutFilter(rideItem);
if (! filter.isEmpty()) {

View File

@@ -184,6 +184,8 @@ CalendarBaseTable::buildContextMenu
contextMenu->addAction(tr("Unlink from planned activity"), this, [this, entry]() { emit unlinkActivity(entry); });
}
contextMenu->addSeparator();
contextMenu->addAction(tr("Filter similar activities..."), this, [this, entry]() { emit filterSimilar(entry); });
contextMenu->addSeparator();
contextMenu->addAction(tr("Delete completed activity"), this, [this, entry]() { emit delActivity(entry); });
break;
case ENTRY_TYPE_PLANNED_ACTIVITY:
@@ -203,10 +205,11 @@ CalendarBaseTable::buildContextMenu
} else {
contextMenu->addAction(tr("Mark as incomplete"), this, [this, entry]() { emit unlinkActivity(entry); });
}
contextMenu->addSeparator();
if (entry.hasTrainMode) {
contextMenu->addSeparator();
contextMenu->addAction(tr("Show in train mode..."), this, [this, entry]() { emit showInTrainMode(entry); });
}
contextMenu->addAction(tr("Filter similar activities..."), this, [this, entry]() { emit filterSimilar(entry); });
contextMenu->addSeparator();
contextMenu->addAction(tr("Delete planned activity"), this, [this, entry]() { emit delActivity(entry); });
break;
@@ -1543,6 +1546,7 @@ CalendarDayView::CalendarDayView
connect(dayTable, &CalendarDayTable::viewLinkedActivity, this, &CalendarDayView::viewLinkedActivity);
connect(dayTable, &CalendarDayTable::addActivity, this, &CalendarDayView::addActivity);
connect(dayTable, &CalendarDayTable::showInTrainMode, this, &CalendarDayView::showInTrainMode);
connect(dayTable, &CalendarDayTable::filterSimilar, this, &CalendarDayView::filterSimilar);
connect(dayTable, &CalendarDayTable::delActivity, this, &CalendarDayView::delActivity);
connect(dayTable, &CalendarDayTable::saveChanges, this, &CalendarDayView::saveChanges);
connect(dayTable, &CalendarDayTable::discardChanges, this, &CalendarDayView::discardChanges);
@@ -1842,6 +1846,7 @@ CalendarWeekView::CalendarWeekView
connect(weekTable, &CalendarDayTable::viewLinkedActivity, this, &CalendarWeekView::viewLinkedActivity);
connect(weekTable, &CalendarDayTable::addActivity, this, &CalendarWeekView::addActivity);
connect(weekTable, &CalendarDayTable::showInTrainMode, this, &CalendarWeekView::showInTrainMode);
connect(weekTable, &CalendarDayTable::filterSimilar, this, &CalendarWeekView::filterSimilar);
connect(weekTable, &CalendarDayTable::delActivity, this, &CalendarWeekView::delActivity);
connect(weekTable, &CalendarDayTable::saveChanges, this, &CalendarWeekView::saveChanges);
connect(weekTable, &CalendarDayTable::discardChanges, this, &CalendarWeekView::discardChanges);
@@ -2039,6 +2044,7 @@ Calendar::Calendar
connect(dayView, &CalendarDayView::viewLinkedActivity, this, &Calendar::viewLinkedActivity);
connect(dayView, &CalendarDayView::addActivity, this, &Calendar::addActivity);
connect(dayView, &CalendarDayView::showInTrainMode, this, &Calendar::showInTrainMode);
connect(dayView, &CalendarDayView::filterSimilar, this, &Calendar::filterSimilar);
connect(dayView, &CalendarDayView::delActivity, this, &Calendar::delActivity);
connect(dayView, &CalendarDayView::saveChanges, this, &Calendar::saveChanges);
connect(dayView, &CalendarDayView::discardChanges, this, &Calendar::discardChanges);
@@ -2067,6 +2073,7 @@ Calendar::Calendar
connect(weekView, &CalendarWeekView::viewLinkedActivity, this, &Calendar::viewLinkedActivity);
connect(weekView, &CalendarWeekView::addActivity, this, &Calendar::addActivity);
connect(weekView, &CalendarWeekView::showInTrainMode, this, &Calendar::showInTrainMode);
connect(weekView, &CalendarWeekView::filterSimilar, this, &Calendar::filterSimilar);
connect(weekView, &CalendarWeekView::delActivity, this, &Calendar::delActivity);
connect(weekView, &CalendarWeekView::saveChanges, this, &Calendar::saveChanges);
connect(weekView, &CalendarWeekView::discardChanges, this, &Calendar::discardChanges);
@@ -2093,6 +2100,7 @@ Calendar::Calendar
setView(CalendarView::Day);
});
connect(monthView, &CalendarMonthTable::showInTrainMode, this, &Calendar::showInTrainMode);
connect(monthView, &CalendarMonthTable::filterSimilar, this, &Calendar::filterSimilar);
connect(monthView, &CalendarMonthTable::linkActivity, this, &Calendar::linkActivity);
connect(monthView, &CalendarMonthTable::unlinkActivity, this, &Calendar::unlinkActivity);
connect(monthView, &CalendarMonthTable::viewActivity, this, &Calendar::viewActivity);

View File

@@ -68,6 +68,7 @@ public:
signals:
void showInTrainMode(CalendarEntry ctivity);
void filterSimilar(CalendarEntry activity);
void linkActivity(CalendarEntry activity, bool autoLink);
void unlinkActivity(CalendarEntry activity);
void viewActivity(CalendarEntry activity);
@@ -262,6 +263,7 @@ signals:
void dayChanged(QDate date);
void showInTrainMode(CalendarEntry activity);
void filterSimilar(CalendarEntry activity);
void linkActivity(CalendarEntry activity, bool autoLink);
void unlinkActivity(CalendarEntry activity);
void viewActivity(CalendarEntry activity);
@@ -315,6 +317,7 @@ signals:
void dayChanged(QDate date);
void showInTrainMode(CalendarEntry activity);
void filterSimilar(CalendarEntry activity);
void linkActivity(CalendarEntry activity, bool autoLink);
void unlinkActivity(CalendarEntry activity);
void viewActivity(CalendarEntry activity);
@@ -381,6 +384,7 @@ signals:
void moveActivity(CalendarEntry activity, QDate srcDay, QDate destDay, QTime destTime);
void showInTrainMode(CalendarEntry activity);
void filterSimilar(CalendarEntry activity);
void linkActivity(CalendarEntry activity, bool autoLink);
void unlinkActivity(CalendarEntry activity);
void viewActivity(CalendarEntry activity);

View File

@@ -0,0 +1,365 @@
/*
* Copyright (c) 2025 Joachim Kohlhammer (joachim.kohlhammer@gmx.de)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "FilterSimilarDialog.h"
#include <QDialogButtonBox>
#include <QHeaderView>
#include "RideMetadata.h"
#include "Utils.h"
#include "MainWindow.h"
#include "Colors.h"
#include "StyledItemDelegates.h"
FilterSimilarDialog::FilterSimilarDialog
(Context *context, RideItem const * const rideItem, QWidget *parent)
: QDialog(parent), context(context), rideItem(rideItem)
{
setWindowTitle(tr("Filter for similar activities"));
setMinimumSize(800 * dpiXFactor, 800 * dpiYFactor);
resize(800 * dpiXFactor, 800 * dpiYFactor);
hideZeroed = new QCheckBox(tr("Hide zeroed fields and metrics"));
hideZeroed->setChecked(true);
filterTreeEdit = new QLineEdit();
filterTreeEdit->setClearButtonEnabled(true);
filterTreeEdit->setPlaceholderText(tr("Find fields and metrics by name..."));
tree = new QTreeWidget();
ComboBoxDelegate *operatorDelegate = new ComboBoxDelegate(tree);
operatorDelegate->setRoleForType(Qt::UserRole + 1);
operatorDelegate->addItemsForType(0, { "", tr("equals"), tr("contains") });
operatorDelegate->addItemsForType(1, { "", "", "", "", "", "", "" });
tree->setColumnCount(3);
tree->setHeaderHidden(true);
tree->setMouseTracking(true);
tree->viewport()->installEventFilter(this);
basicTreeWidgetStyle(tree, true);
tree->setItemDelegateForColumn(0, new NoEditDelegate(tree));
tree->setItemDelegateForColumn(1, operatorDelegate);
tree->setItemDelegateForColumn(2, new NoEditDelegate(tree));
tree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
addFields();
addMetrics();
preview = new QTextEdit();
preview->setReadOnly(true);
QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(tree, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem *item, int column) {
Q_UNUSED(column)
QFont font = tree->font();
if (item->data(1, Qt::DisplayRole).toInt() == 0) {
item->setData(0, Qt::FontRole, font);
item->setData(2, Qt::FontRole, font);
font.setWeight(QFont::Thin);
item->setData(1, Qt::FontRole, font);
} else {
font.setWeight(QFont::Bold);
item->setData(0, Qt::FontRole, font);
item->setData(1, Qt::FontRole, font);
item->setData(2, Qt::FontRole, font);
}
QString filter = buildFilter();
preview->setText(filter);
updateFilterTree();
});
connect(tree, &QTreeWidget::clicked, this, [this](const QModelIndex &index) {
if (index.column() == 1) {
tree->edit(index);
}
});
connect(hideZeroed, &QCheckBox::toggled, this, &FilterSimilarDialog::updateFilterTree);
connect(filterTreeEdit, &QLineEdit::textChanged, this, &FilterSimilarDialog::updateFilterTree);
connect(buttons, &QDialogButtonBox::accepted, this, [this, context]() {
QString filter = buildFilter();
if (! filter.isEmpty()) {
context->mainWindow->fillinFilter(filter);
}
accept();
});
connect(buttons, &QDialogButtonBox::rejected, this, [this]() {
reject();
});
QHBoxLayout *confLayout = new QHBoxLayout();
confLayout->addWidget(filterTreeEdit);
confLayout->addSpacing(20 * dpiXFactor);
confLayout->addWidget(hideZeroed);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(confLayout);
mainLayout->addSpacing(10 * dpiYFactor);
mainLayout->addWidget(tree, 10);
mainLayout->addSpacing(20 * dpiYFactor);
mainLayout->addWidget(new QLabel(tr("Generated Filter:")));
mainLayout->addWidget(preview, 2);
mainLayout->addSpacing(20 * dpiYFactor);
mainLayout->addWidget(buttons);
updateFilterTree();
}
bool
FilterSimilarDialog::eventFilter
(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::MouseMove) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
QModelIndex index = tree->indexAt(mouseEvent->pos());
if (index.isValid() && index.column() == 1) {
tree->viewport()->setCursor(Qt::PointingHandCursor);
} else {
tree->viewport()->setCursor(Qt::ArrowCursor);
}
}
return QDialog::eventFilter(obj, event);
}
QString
FilterSimilarDialog::buildFilter
() const
{
bool first = true;
QString filter;
for (int i = 0; i < tree->topLevelItemCount(); ++i) {
QTreeWidgetItem *section = tree->topLevelItem(i);
for (int j = 0; j < section->childCount(); ++j) {
QTreeWidgetItem *item = section->child(j);
int op = item->data(1, Qt::DisplayRole).toInt();
if (op != 0) {
if (! first) {
filter += " and ";
}
int type = item->data(1, Qt::UserRole + 1).toInt();
QString opStr = "=";
bool needQuotes = true;
bool similar = false;
QString similarLow;
QString similarHigh;
if (type == 0) {
if (op == 1) {
opStr = "=";
needQuotes = true;
} else if (op == 2) {
opStr = " contains ";
needQuotes = true;
}
} else if (type == 1) {
if (op == 1) {
opStr = "<";
needQuotes = false;
} else if (op == 2) {
opStr = "<=";
needQuotes = false;
} else if (op == 3) {
opStr = "=";
needQuotes = true;
} else if (op == 4) {
GcFieldType detailType = static_cast<GcFieldType>(item->data(1, Qt::UserRole + 2).value<int>());
double value = item->data(0, Qt::UserRole + 2).toString().toDouble();
double low = value * 0.95;
double high = value * 1.05;
if (value < 0) {
std::swap(low, high);
}
if ( detailType == GcFieldType::FIELD_INTEGER
|| detailType == GcFieldType::FIELD_DATE
|| detailType == GcFieldType::FIELD_TIME) {
similarLow = QString("%1").arg(static_cast<int>(std::floor(low)));
similarHigh = QString("%1").arg(static_cast<int>(std::ceil(high)));
} else {
similarLow = QString("%1").arg(low, 0, 'f', 2);
similarHigh = QString("%1").arg(high, 0, 'f', 2);
}
similar = true;
} else if (op == 5) {
opStr = ">=";
needQuotes = false;
} else if (op == 6) {
opStr = ">";
needQuotes = false;
}
}
if (similar) {
filter += QString("%1>=%2 and %1<=%3").arg(item->data(0, Qt::UserRole + 1).toString())
.arg(similarLow)
.arg(similarHigh);
} else {
filter += QString("%1%2%3%4%3").arg(item->data(0, Qt::UserRole + 1).toString())
.arg(opStr)
.arg(needQuotes ? "\"" : "")
.arg(Utils::quoteEscape(item->data(0, Qt::UserRole + 2).toString()));
}
first = false;
}
}
}
return filter;
}
void
FilterSimilarDialog::addFields
()
{
QFont bold = tree->font();
bold.setWeight(QFont::Bold);
bold.setPointSize(bold.pointSize() * 1.1);
QFont light = tree->font();
light.setWeight(QFont::Thin);
QTreeWidgetItem* sectionFields = new QTreeWidgetItem(tree);
sectionFields->setData(0, Qt::DisplayRole, tr("Fields"));
sectionFields->setData(0, Qt::FontRole, bold);
sectionFields->setFlags(sectionFields->flags() & ~Qt::ItemIsSelectable);
sectionFields->setExpanded(true);
sectionFields->setFirstColumnSpanned(true);
QList<FieldDefinition> fieldsDefs = GlobalContext::context()->rideMetadata->getFields();
for (const FieldDefinition &fieldDef : fieldsDefs) {
QString value = rideItem->getText(fieldDef.name, "");
if (rideItem->hasText(fieldDef.name) && ! value.isEmpty()) {
QString searchKey = fieldDef.name;
searchKey.replace(' ', '_');
QString displayValue = value;
if (fieldDef.isTextField()) {
displayValue.remove('\n').remove('\r');
} else if (fieldDef.type == GcFieldType::FIELD_DATE) {
QLocale locale;
QDate date(1900, 1, 1);
displayValue = locale.toString(date.addDays(value.toInt()), QLocale::ShortFormat);
} else if (fieldDef.type == GcFieldType::FIELD_TIME) {
QLocale locale;
QTime time(0, 0, 0);
displayValue = locale.toString(time.addSecs(value.toInt()), QLocale::ShortFormat);
}
QTreeWidgetItem* item = new QTreeWidgetItem(sectionFields);
item->setData(0, Qt::DisplayRole, fieldDef.name);
item->setData(0, Qt::UserRole + 1, searchKey);
item->setData(0, Qt::UserRole + 2, value);
item->setData(1, Qt::DisplayRole, 0);
item->setData(1, Qt::FontRole, light);
item->setData(1, Qt::UserRole + 1, fieldDef.isTextField() ? 0 : 1);
item->setData(1, Qt::UserRole + 2, QVariant::fromValue<int>(static_cast<int>(fieldDef.type)));
item->setData(2, Qt::DisplayRole, displayValue);
item->setFlags(item->flags() | Qt::ItemIsSelectable | Qt::ItemIsEditable);
}
}
}
void
FilterSimilarDialog::addMetrics
()
{
QFont bold = tree->font();
bold.setWeight(QFont::Bold);
bold.setPointSize(bold.pointSize() * 1.1);
QFont light = tree->font();
light.setWeight(QFont::Thin);
QTreeWidgetItem* sectionMetrics = new QTreeWidgetItem(tree);
sectionMetrics->setData(0, Qt::DisplayRole, tr("Metrics"));
sectionMetrics->setData(0, Qt::FontRole, bold);
sectionMetrics->setFlags(sectionMetrics->flags() & ~Qt::ItemIsSelectable);
sectionMetrics->setExpanded(true);
sectionMetrics->setFirstColumnSpanned(true);
bool useMetric = GlobalContext::context()->useMetricUnits;
const RideMetricFactory &factory = RideMetricFactory::instance();
for (const QString &metricSymbol : factory.allMetrics()) {
if (metricSymbol.startsWith("compatibility_")) {
continue;
}
double value = const_cast<RideItem*>(rideItem)->getForSymbol(metricSymbol, useMetric);
QString displayValue = const_cast<RideItem*>(rideItem)->getStringForSymbol(metricSymbol, useMetric);
RideMetric const *rm = factory.rideMetric(metricSymbol);
QString displayKey = rm->name();
QString searchKey = displayKey;
if (searchKey.endsWith("&#8482;")) {
searchKey.chop(7);
searchKey = searchKey.trimmed();
}
searchKey.replace(' ', '_');
QTreeWidgetItem* item = new QTreeWidgetItem(sectionMetrics);
item->setData(0, Qt::DisplayRole, Utils::unprotect(displayKey));
item->setData(0, Qt::UserRole + 1, searchKey);
item->setData(0, Qt::UserRole + 2, value);
item->setData(1, Qt::DisplayRole, 0);
item->setData(1, Qt::FontRole, light);
item->setData(1, Qt::UserRole + 1, 1);
if (rm->isTime()) {
item->setData(1, Qt::UserRole + 2, 6);
} else if (rm->isDate()) {
item->setData(1, Qt::UserRole + 2, 5);
} else {
item->setData(1, Qt::UserRole + 2, 4);
}
item->setData(2, Qt::DisplayRole, displayValue);
item->setFlags(item->flags() | Qt::ItemIsSelectable | Qt::ItemIsEditable);
}
}
void
FilterSimilarDialog::updateFilterTree
()
{
QString f = filterTreeEdit->text().trimmed();
bool empty = f.isEmpty();
bool hideZeroes = hideZeroed->isChecked();
bool isHiddenZero = false;
for (int i = 0; i < tree->topLevelItemCount(); ++i) {
QTreeWidgetItem *section = tree->topLevelItem(i);
for (int j = 0; j < section->childCount(); ++j) {
QTreeWidgetItem* item = section->child(j);
if (hideZeroes) {
int type = item->data(1, Qt::UserRole + 1).toInt(); // type: 0 - string, 1 - number
QVariant value = item->data(0, Qt::UserRole + 2); // raw value
if (type == 0) {
isHiddenZero = value.toString().isEmpty();
} else {
isHiddenZero = qFuzzyIsNull(value.toDouble());
}
}
if (isHiddenZero) {
item->setHidden(true);
} else if ( empty
|| item->data(1, Qt::DisplayRole).toInt() != 0
|| item->text(0).contains(f, Qt::CaseInsensitive)) {
item->setHidden(false);
} else {
item->setHidden(true);
}
}
section->setHidden(false);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2025 Joachim Kohlhammer (joachim.kohlhammer@gmx.de)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _GC_FilterSimilarDialog_h
#define _GC_FilterSimilarDialog_h 1
#include <QtGui>
#include <QTreeWidget>
#include "Context.h"
#include "RideItem.h"
class FilterSimilarDialog : public QDialog
{
Q_OBJECT
public:
FilterSimilarDialog(Context *context, RideItem const * const rideItem, QWidget *parent = nullptr);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
private:
Context *context;
RideItem const * const rideItem;
QCheckBox *hideZeroed;
QLineEdit *filterTreeEdit;
QTreeWidget *tree;
QTextEdit *preview;
QString buildFilter() const;
void addFields();
void addMetrics();
private slots:
void updateFilterTree();
};
#endif

View File

@@ -1133,6 +1133,20 @@ MainWindow::~MainWindow()
void MainWindow::setFilter(QStringList f) { currentAthleteTab->context->setFilter(f); }
void MainWindow::clearFilter() { currentAthleteTab->context->clearFilter(); }
void
MainWindow::fillinFilter(const QString &filterText)
{
searchBox->setMode(SearchBox::Filter);
searchBox->setText(filterText);
}
void
MainWindow::fillinSearch(const QString &filterText)
{
searchBox->setMode(SearchBox::Search);
searchBox->setText(filterText);
}
void
MainWindow::aboutDialog()
{

View File

@@ -197,6 +197,8 @@ class MainWindow : public QMainWindow
// Search / Filter
void setFilter(QStringList);
void clearFilter();
void fillinFilter(const QString &filterText);
void fillinSearch(const QString &searchText);
void selectAthlete();
void selectTrends();

View File

@@ -248,6 +248,32 @@ ComboBoxDelegate::addItems
}
void
ComboBoxDelegate::addItemsForType
(int type, const QStringList &texts)
{
textsForType[type] = texts;
fillSizeHint();
}
void
ComboBoxDelegate::setRoleForType
(int role)
{
_roleForType = role;
}
int
ComboBoxDelegate::roleForType
() const
{
return _roleForType;
}
void
ComboBoxDelegate::commitAndCloseEditor
()
@@ -266,7 +292,22 @@ ComboBoxDelegate::createEditor
Q_UNUSED(index)
QComboBox *combobox = new QComboBox(parent);
combobox->addItems(texts);
bool filled = false;
if (_roleForType != -1) {
QVariant typeVar = index.data(_roleForType);
if (typeVar.isValid() && ! typeVar.isNull() && typeVar.canConvert<int>()) {
int type = typeVar.toInt();
if (textsForType.contains(type)) {
combobox->addItems(textsForType[type]);
filled = true;
}
}
}
if (! filled) {
combobox->addItems(texts);
}
connect(combobox, SIGNAL(activated(int)), this, SLOT(commitAndCloseEditor()));
@@ -295,17 +336,28 @@ ComboBoxDelegate::setModelData
}
QString
ComboBoxDelegate::displayText
(const QVariant &value, const QLocale &locale) const
void
ComboBoxDelegate::initStyleOption
(QStyleOptionViewItem *option, const QModelIndex &index) const
{
Q_UNUSED(locale);
int index = value.toInt();
if (index >= 0 && index < texts.length()) {
return texts[index];
QStyledItemDelegate::initStyleOption(option, index);
int idx = index.data(Qt::DisplayRole).toInt();
if (_roleForType != -1) {
QVariant typeVar = index.data(_roleForType);
if (typeVar.isValid() && ! typeVar.isNull() && typeVar.canConvert<int>()) {
int type = typeVar.toInt();
if (textsForType.contains(type) && idx >= 0 && idx < textsForType[type].length()) {
option->text = textsForType[type][idx];
} else {
option->text = QString("INDEX OUT OF RANGE: %1 with %2 texts for type %3").arg(idx).arg(textsForType[type].length(), type);
}
} else {
option->text = "";
}
} else if (idx >= 0 && idx < texts.length()) {
option->text = texts[idx];
} else {
return QString("INDEX OUT OF RANGE: %1 with %2 texts").arg(index).arg(texts.length());
option->text = QString("INDEX OUT OF RANGE: %1 with %2 texts").arg(idx).arg(texts.length());
}
}
@@ -327,6 +379,9 @@ ComboBoxDelegate::fillSizeHint
{
QComboBox widget;
widget.addItems(texts);
for (const QStringList &list : textsForType) {
widget.addItems(list);
}
_sizeHint = widget.sizeHint();
}
@@ -815,8 +870,9 @@ QWidget*
ListEditDelegate::createEditor
([[maybe_unused]] QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_UNUSED(parent)
Q_UNUSED(option)
Q_UNUSED(index)
emit requestListEdit(index);
return nullptr;
}

View File

@@ -118,19 +118,26 @@ class ComboBoxDelegate: public QStyledItemDelegate
public:
ComboBoxDelegate(QObject *parent = nullptr);
virtual QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override;
virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
virtual QString displayText(const QVariant &value, const QLocale &locale) const override;
virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void addItems(const QStringList &texts);
void addItemsForType(int type, const QStringList &texts);
void setRoleForType(int role);
int roleForType() const;
protected:
void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override;
private slots:
void commitAndCloseEditor();
private:
QStringList texts;
QMap<int, QStringList> textsForType;
int _roleForType = -1;
QSize _sizeHint;
void fillSizeHint();

View File

@@ -650,7 +650,7 @@ HEADERS += Gui/AboutDialog.h Gui/AddIntervalDialog.h Gui/AnalysisSidebar.h Gui/C
Gui/PerspectiveDialog.h Gui/SplashScreen.h Gui/StyledItemDelegates.h Gui/MetadataDialog.h Gui/ActionButtonBox.h \
Gui/MetricOverrideDialog.h Gui/RepeatScheduleWizard.h \
Gui/Calendar.h Gui/Agenda.h Gui/CalendarData.h Gui/CalendarItemDelegates.h \
Gui/IconManager.h
Gui/IconManager.h Gui/FilterSimilarDialog.h
# metrics and models
HEADERS += Metrics/Banister.h Metrics/CPSolver.h Metrics/Estimator.h Metrics/ExtendedCriticalPower.h Metrics/HrZones.h Metrics/PaceZones.h \
@@ -763,7 +763,7 @@ SOURCES += Gui/AboutDialog.cpp Gui/AddIntervalDialog.cpp Gui/AnalysisSidebar.cpp
Gui/PerspectiveDialog.cpp Gui/SplashScreen.cpp Gui/StyledItemDelegates.cpp Gui/MetadataDialog.cpp Gui/ActionButtonBox.cpp \
Gui/MetricOverrideDialog.cpp Gui/RepeatScheduleWizard.cpp \
Gui/Calendar.cpp Gui/Agenda.cpp Gui/CalendarData.cpp Gui/CalendarItemDelegates.cpp \
Gui/IconManager.cpp
Gui/IconManager.cpp Gui/FilterSimilarDialog.cpp
## Models and Metrics
SOURCES += Metrics/aBikeScore.cpp Metrics/aCoggan.cpp Metrics/AerobicDecoupling.cpp Metrics/Banister.cpp Metrics/BasicRideMetrics.cpp \

View File

@@ -0,0 +1,23 @@
#include "Core/Utils.h"
#include <QTest>
class TestUtils: public QObject
{
Q_OBJECT
private slots:
void quoteEscapeTest() {
QCOMPARE(Utils::quoteEscape("abc"), QString("abc"));
QCOMPARE(Utils::quoteEscape("a\bc"), QString("a\bc"));
QCOMPARE(Utils::quoteEscape("a\\bc"), QString("a\\bc"));
QCOMPARE(Utils::quoteEscape("a\"bc"), QString("a\\\"bc"));
QCOMPARE(Utils::quoteEscape("a\\\"bc"), QString("a\\\"bc"));
QCOMPARE(Utils::quoteEscape("a\\\\\"bc"), QString("a\\\\\\\"bc"));
}
};
QTEST_MAIN(TestUtils)
#include "testUtils.moc"

View File

@@ -0,0 +1,6 @@
QT += testlib core widgets
SOURCES = testUtils.cpp
GC_OBJS = Utils
include(../../unittests.pri)

View File

@@ -9,6 +9,7 @@ equals(GC_UNITTESTS, active) {
Core/season \
Core/seasonParser \
Core/units \
Core/utils \
Core/signalSafety \
Core/splineCrash \
Gui/calendarData