mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 08:08:42 +00:00
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:
committed by
GitHub
parent
7a8f329a84
commit
a00e27638f
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
365
src/Gui/FilterSimilarDialog.cpp
Normal file
365
src/Gui/FilterSimilarDialog.cpp
Normal 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("™")) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
55
src/Gui/FilterSimilarDialog.h
Normal file
55
src/Gui/FilterSimilarDialog.h
Normal 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
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 \
|
||||
|
||||
23
unittests/Core/utils/testUtils.cpp
Normal file
23
unittests/Core/utils/testUtils.cpp
Normal 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"
|
||||
6
unittests/Core/utils/utils.pro
Normal file
6
unittests/Core/utils/utils.pro
Normal file
@@ -0,0 +1,6 @@
|
||||
QT += testlib core widgets
|
||||
|
||||
SOURCES = testUtils.cpp
|
||||
GC_OBJS = Utils
|
||||
|
||||
include(../../unittests.pri)
|
||||
@@ -9,6 +9,7 @@ equals(GC_UNITTESTS, active) {
|
||||
Core/season \
|
||||
Core/seasonParser \
|
||||
Core/units \
|
||||
Core/utils \
|
||||
Core/signalSafety \
|
||||
Core/splineCrash \
|
||||
Gui/calendarData
|
||||
|
||||
Reference in New Issue
Block a user