Named workout filters (#4614)

* Added manager for named workout filters
* Including a tag-browser
* Changed behaviour of WorkoutFilterBox from editingFinished to
  returnPressed for consistency with SearchFilterBox and to prevent
  parallel execution of the update-slot (resulting in segv)
* WorkoutFilterBox: The clear-button updates the filtered list
* Repainting when changing the visibility of the error icon
This commit is contained in:
Joachim Kohlhammer
2025-02-06 00:39:01 +01:00
committed by GitHub
parent 5dbc0ebce9
commit e909fad85a
14 changed files with 822 additions and 23 deletions

View File

@@ -53,6 +53,29 @@ NoEditDelegate::createEditor
}
// NegativeListEditDelegate ///////////////////////////////////////////////////////
NegativeListEditDelegate::NegativeListEditDelegate
(const QStringList &negativeList, QObject *parent)
: QStyledItemDelegate(parent), negativeList(negativeList)
{
}
void
NegativeListEditDelegate::setModelData
(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QString newData = editor->property("text").toString().trimmed();
if (newData == index.data().toString() || negativeList.contains(newData)) {
return;
}
model->setData(index, newData, Qt::EditRole);
}
// UniqueLabelEditDelegate ////////////////////////////////////////////////////////
UniqueLabelEditDelegate::UniqueLabelEditDelegate
@@ -62,12 +85,20 @@ UniqueLabelEditDelegate::UniqueLabelEditDelegate
}
void
UniqueLabelEditDelegate::setNegativeList
(const QStringList &negativeList)
{
this->negativeList = negativeList;
}
void
UniqueLabelEditDelegate::setModelData
(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QString newData = editor->property("text").toString().trimmed();
if (newData.isEmpty() || newData == index.data().toString()) {
if (newData.isEmpty() || newData == index.data().toString() || negativeList.contains(newData)) {
return;
}
int rowCount = model->rowCount();

View File

@@ -42,6 +42,21 @@ public:
class NegativeListEditDelegate: public QStyledItemDelegate
{
Q_OBJECT
public:
NegativeListEditDelegate(const QStringList &negativeList, QObject *parent = nullptr);
virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
private:
QStringList negativeList;
};
class UniqueLabelEditDelegate: public QStyledItemDelegate
{
Q_OBJECT
@@ -49,7 +64,12 @@ class UniqueLabelEditDelegate: public QStyledItemDelegate
public:
UniqueLabelEditDelegate(QObject *parent = nullptr);
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
void setNegativeList(const QStringList &negativeList);
virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
private:
QStringList negativeList;
};

View File

@@ -197,6 +197,7 @@
<file>images/sidebar/minus.png</file>
<file>images/sidebar/plus.png</file>
<file>images/sidebar/extra.png</file>
<file>images/sidebar/extra-2.png</file>
<file>images/sidebar/folder.png</file>
<file>images/sidebar/movie.png</file>
<file>images/sidebar/power.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -22,24 +22,27 @@
#include <QAbstractItemView>
#include <QScrollBar>
#include "GcSideBarItem.h"
#include "Colors.h"
FilterEditor::FilterEditor
(QWidget *parent)
: QLineEdit(parent), _completer(nullptr), _completerModel(nullptr), _origCmds()
{
}
FilterEditor::FilterEditor
(const QString &contents, QWidget *parent)
: QLineEdit(contents, parent), _completer(nullptr), _completerModel(nullptr), _origCmds()
{
QIcon workoutFilterMenuIcon = iconFromPNG(":images/sidebar/extra-2.png");
_menuAction = addAction(workoutFilterMenuIcon, FilterEditor::TrailingPosition);
connect(_menuAction, &QAction::triggered, this, &FilterEditor::openMenu);
_menuAction->setVisible(false);
}
FilterEditor::~FilterEditor
()
{
if (_menuProvider != nullptr) {
delete _menuProvider;
}
}
@@ -89,6 +92,15 @@ FilterEditor::completer
}
void
FilterEditor::setMenuProvider
(MenuProvider *menuProvider)
{
_menuProvider = menuProvider;
_menuAction->setVisible(_menuProvider != nullptr);
}
void
FilterEditor::keyPressEvent
(QKeyEvent *e)
@@ -207,6 +219,20 @@ FilterEditor::updateCompleterModel
}
void
FilterEditor::openMenu
()
{
if (_menuProvider == nullptr) {
return;
}
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
_menuProvider->addActions(menu);
menu->exec(mapToGlobal(QPoint(width() - 28 * dpiXFactor, height())));
}
QString
FilterEditor::wordLeftOfCursor
() const

View File

@@ -29,6 +29,9 @@
#include <utility>
#include "MainWindow.h"
#include "MenuProvider.h"
class FilterEditorHelper
{
@@ -54,11 +57,11 @@ class FilterEditor: public QLineEdit
public:
FilterEditor(QWidget *parent = nullptr);
FilterEditor(const QString &contents, QWidget *parent = nullptr);
virtual ~FilterEditor();
void setFilterCommands(const QStringList &commands);
QCompleter *completer() const;
void setMenuProvider(MenuProvider *menuProvider);
protected:
void setCompleter(QCompleter *completer);
@@ -67,11 +70,14 @@ protected:
private slots:
void insertCompletion(const QString &completion);
void updateCompleterModel(const QString &text);
void openMenu();
private:
QCompleter *_completer;
QStringListModel *_completerModel;
QStringList _origCmds;
QAction *_menuAction;
MenuProvider *_menuProvider = nullptr;
FilterEditorHelper feh;
QString wordLeftOfCursor() const;

32
src/Train/MenuProvider.h Normal file
View File

@@ -0,0 +1,32 @@
/*
* 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 MENUPROVIDER_H
#define MENUPROVIDER_H
#include <QMenu>
class MenuProvider
{
public:
virtual ~MenuProvider() {}
virtual void addActions(QMenu *menu) const = 0;
};
#endif

View File

@@ -32,7 +32,7 @@ MultiFilterProxyModel::MultiFilterProxyModel
MultiFilterProxyModel::~MultiFilterProxyModel
()
{
removeFilters(false);
removeFilters(true);
}
@@ -72,7 +72,7 @@ bool
MultiFilterProxyModel::filterAcceptsRow
(int source_row, const QModelIndex &source_parent) const
{
for (auto filter : _filters) {
for (auto &filter : _filters) {
if (filter->modelColumn() < 0 || filter->modelColumn() > sourceModel()->columnCount()) {
return false;
}

View File

@@ -18,6 +18,7 @@
#include "WorkoutFilterBox.h"
#include "WorkoutFilter.h"
#include "WorkoutMenuProvider.h"
#include <QString>
#include <QDebug>
@@ -27,13 +28,23 @@
WorkoutFilterBox::WorkoutFilterBox(QWidget *parent, Context *context) : FilterEditor(parent), context(context)
{
QIcon workoutFilterClearIcon = QPixmap::fromImage(QImage(":images/toolbar/clear.png"));
QAction *workoutFilterClearAction = this->addAction(workoutFilterClearIcon, FilterEditor::TrailingPosition);
workoutFilterClearAction->setVisible(! text().isEmpty());
connect(this, &FilterEditor::textChanged, this, [=](const QString &text) {
workoutFilterClearAction->setVisible(! text.isEmpty());
});
connect(workoutFilterClearAction, &QAction::triggered, this, [=]() {
setText("");
});
QIcon workoutFilterErrorIcon = QPixmap::fromImage(QImage(":images/toolbar/popbutton.png"));
workoutFilterErrorAction = this->addAction(workoutFilterErrorIcon, FilterEditor::LeadingPosition);
workoutFilterErrorAction->setVisible(false);
this->setClearButtonEnabled(true);
this->setPlaceholderText(tr("Filter..."));
this->setFilterCommands(workoutFilterCommands());
connect(this, SIGNAL(editingFinished()), this, SLOT(editingFinished()));
this->setMenuProvider(new WorkoutMenuProvider(this, QDir(gcroot).canonicalPath() + "/workoutfilters.xml"));
connect(this, &FilterEditor::returnPressed, this, &WorkoutFilterBox::processInput);
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
// set appearance
@@ -44,9 +55,29 @@ WorkoutFilterBox::~WorkoutFilterBox()
{
}
void
WorkoutFilterBox::editingFinished()
WorkoutFilterBox::clear
()
{
FilterEditor::clear();
processInput();
}
void
WorkoutFilterBox::setText
(const QString &text)
{
FilterEditor::setText(text);
processInput();
}
void
WorkoutFilterBox::processInput()
{
bool errorActionWasVisible = workoutFilterErrorAction->isVisible();
workoutFilterErrorAction->setVisible(false);
bool ok = true;
QString msg;
@@ -68,6 +99,9 @@ WorkoutFilterBox::editingFinished()
context->clearWorkoutFilters();
}
}
if (errorActionWasVisible != workoutFilterErrorAction->isVisible()) {
repaint();
}
}
void

View File

@@ -35,8 +35,12 @@ public:
virtual ~WorkoutFilterBox();
void setContext(Context *ctx) { context = ctx; }
public slots:
void clear();
void setText(const QString &text);
private slots:
void editingFinished();
void processInput();
void configChanged(qint32 topic);
private:

View File

@@ -0,0 +1,535 @@
/*
* 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 "WorkoutMenuProvider.h"
#include <QDialog>
#include <QVBoxLayout>
#include <QCheckBox>
#include <QLineEdit>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QStandardItemModel>
#include "TrainDB.h"
#include "ActionButtonBox.h"
#include "Colors.h"
#include "StyledItemDelegates.h"
WorkoutFilterStore::WorkoutFilterStore
(const QString &fileName)
: fileName(fileName)
{
}
QList<WorkoutFilterStore::StorableWorkoutFilter>
WorkoutFilterStore::getWorkoutFilters
() const
{
return workoutFilters;
}
void
WorkoutFilterStore::append
(const QString &name, const QString &filter)
{
workoutFilters.append(StorableWorkoutFilter(name, filter));
}
void
WorkoutFilterStore::insert
(int index, const QString &name, const QString &filter)
{
workoutFilters.insert(index, StorableWorkoutFilter(name, filter));
}
void
WorkoutFilterStore::update
(int index, const QString &name, const QString &filter)
{
if (index >= 0 && index < workoutFilters.count()) {
workoutFilters[index].name = name;
workoutFilters[index].filter = filter;
}
}
void
WorkoutFilterStore::remove
(int index)
{
workoutFilters.removeAt(index);
}
void
WorkoutFilterStore::moveUp
(int index)
{
if (index > 0) {
workoutFilters.swapItemsAt(index, index - 1);
}
}
void
WorkoutFilterStore::moveDown
(int index)
{
if (index < workoutFilters.count() - 1) {
workoutFilters.swapItemsAt(index, index + 1);
}
}
void
WorkoutFilterStore::load
()
{
workoutFilters.clear();
QFile file(fileName);
if (! file.open(QFile::ReadOnly | QFile::Text)) {
qCritical() << "Cannot open file" << fileName << "for reading -" << file.errorString();
return;
}
QXmlStreamReader reader(&file);
while (! reader.atEnd()) {
reader.readNext();
if (reader.tokenType() == QXmlStreamReader::StartDocument) {
continue;
}
if (reader.tokenType() == QXmlStreamReader::StartElement) {
QString elemName = reader.name().toString();
if (elemName == "workoutFilters") {
parseWorkoutFilters(reader);
}
}
}
if (reader.hasError()) {
qCritical() << "Can't parse" << fileName << "-" << reader.errorString() << "in line" << reader.lineNumber() << "column" << reader.columnNumber();
}
file.close();
}
void
WorkoutFilterStore::save
()
{
QFile file(fileName);
if (! file.open(QFile::WriteOnly | QFile::Text)) {
qCritical() << "Cannot open file" << fileName << "for writing -" << file.errorString();
return;
}
QXmlStreamWriter stream(&file);
stream.setAutoFormatting(true);
stream.writeStartDocument();
stream.writeStartElement("workoutFilters");
for (const WorkoutFilterStore::StorableWorkoutFilter &filter : workoutFilters) {
stream.writeStartElement("workoutFilter");
stream.writeTextElement("name", filter.name);
stream.writeTextElement("filter", filter.filter);
stream.writeEndElement();
}
stream.writeEndElement();
stream.writeEndDocument();
file.close();
}
bool
WorkoutFilterStore::parseWorkoutFilters
(QXmlStreamReader &reader)
{
bool ok = true;
while (! reader.atEnd()) {
reader.readNext();
if (reader.tokenType() == QXmlStreamReader::StartElement) {
QString elemName = reader.name().toString();
if (elemName == "workoutFilter") {
ok &= parseWorkoutFilter(reader);
}
} else if (reader.tokenType() == QXmlStreamReader::EndElement) {
QString elemName = reader.name().toString();
if (elemName == "workoutFilters") {
break;
}
}
}
return ok;
}
bool
WorkoutFilterStore::parseWorkoutFilter
(QXmlStreamReader &reader)
{
QString text;
QString name;
QString filter;
while (! reader.atEnd()) {
reader.readNext();
if (reader.tokenType() == QXmlStreamReader::Characters) {
text = reader.text().toString().trimmed();
} else if (reader.tokenType() == QXmlStreamReader::EndElement) {
QString elemName = reader.name().toString();
if (elemName == "name") {
name = text;
} else if (elemName == "filter") {
filter = text;
} else if (elemName == "workoutFilter") {
break;
}
}
}
if (! name.isEmpty() && ! filter.isEmpty()) {
workoutFilters.append(StorableWorkoutFilter(name, filter));
return true;
}
return false;
}
TopLevelFilterProxyModel::TopLevelFilterProxyModel
(QObject *parent)
: QSortFilterProxyModel(parent)
{
}
void
TopLevelFilterProxyModel::setContains
(const QString &contains)
{
this->contains = contains;
invalidateFilter();
}
bool
TopLevelFilterProxyModel::filterAcceptsRow
(int sourceRow, const QModelIndex &sourceParent) const
{
if (! contains.isEmpty() && sourceParent == QModelIndex()) { // ignore children
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
return index.data(Qt::DisplayRole).toString().contains(contains, Qt::CaseInsensitive);
} else {
return true;
}
}
WorkoutMenuProvider::WorkoutMenuProvider
(WorkoutFilterBox *editor, const QString &filterFile)
: QObject(), editor(editor), store(filterFile)
{
store.load();
}
void
WorkoutMenuProvider::addActions
(QMenu *menu) const
{
bool editorDiffers = true;
QString editorText = editor->text().trimmed();
QList<WorkoutFilterStore::StorableWorkoutFilter> filters = store.getWorkoutFilters();
for (const WorkoutFilterStore::StorableWorkoutFilter &filter : filters) {
QAction *action = menu->addAction(filter.name);
connect(action, &QAction::triggered, this, [=]() {
editor->setText(filter.filter);
});
editorDiffers &= (filter.filter != editorText);
}
if (filters.count() > 0) {
menu->addSeparator();
}
QAction *tagAction = menu->addAction(tr("Select Tags") + "...");
tagAction->setEnabled(trainDB->getTags().count() > 0);
menu->addSeparator();
QAction *manageAction = menu->addAction(tr("Manage Filters") + "...");
QAction *addAction = menu->addAction(tr("Add Filter") + "...");
addAction->setEnabled(editorDiffers && ! editor->text().trimmed().isEmpty()); // Disable if editor is empty or filter already added
connect(tagAction, &QAction::triggered, this, &WorkoutMenuProvider::addTagDialog);
connect(manageAction, &QAction::triggered, this, &WorkoutMenuProvider::manageFiltersDialog);
connect(addAction, &QAction::triggered, this, &WorkoutMenuProvider::addFilterDialog);
}
void
WorkoutMenuProvider::addTagDialog
()
{
QDialog dialog;
dialog.setWindowTitle(tr("Add Tag to Filter"));
QLineEdit *filterEdit = new QLineEdit();
filterEdit->setPlaceholderText(tr("Filter Tags..."));
filterEdit->setClearButtonEnabled(true);
QStandardItemModel itemModel;
QList<TagStore::Tag> tags = trainDB->getTags();
for (const TagStore::Tag &tag : tags) {
itemModel.appendRow(new QStandardItem(tag.label));
}
TopLevelFilterProxyModel *filterModel = new TopLevelFilterProxyModel();
filterModel->setSourceModel(&itemModel);
QListView *tagList = new QListView();
tagList->setAlternatingRowColors(true);
tagList->setEditTriggers(QAbstractItemView::NoEditTriggers);
tagList->setSelectionMode(QAbstractItemView::MultiSelection);
tagList->setModel(filterModel);
QCheckBox *matchMode = new QCheckBox(tr("Match all selected tags"));
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
layout->addWidget(filterEdit);
layout->addWidget(tagList);
layout->addWidget(matchMode);
layout->addWidget(buttonBox);
connect(filterEdit, &WorkoutFilterBox::textChanged, this, [=](const QString &text) { filterModel->setContains(text.trimmed()); });
connect(tagList->selectionModel(), &QItemSelectionModel::selectionChanged, this, [=]() { buttonBox->button(QDialogButtonBox::Ok)->setEnabled(tagList->selectionModel()->hasSelection()); });
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() == QDialog::Accepted) {
QModelIndexList selection = tagList->selectionModel()->selectedIndexes();
if (selection.count() > 0) {
QStringList addTags;
for (const QModelIndex &index : selection) {
QString tagText(index.data(Qt::DisplayRole).toString().trimmed());
if (tagText.contains(" ")) {
tagText = "\"" + tagText + "\"";
}
addTags << tagText;
}
QString addText = addTags.join(matchMode->isChecked() ? ", " : " ");
if (editor->text().trimmed().size() > 0) {
editor->setText(QString("%1, %2").arg(editor->text()).arg(addText));
} else {
editor->setText(addText);
}
}
}
}
void
WorkoutMenuProvider::manageFiltersDialog
()
{
QDialog dialog;
dialog.setWindowTitle(tr("Manage Workout Filters"));
dialog.resize(800 * dpiXFactor, 600 * dpiYFactor);
NegativeListEditDelegate nameDelegate({ "" });
NegativeListEditDelegate filterDelegate({ "" });
QTreeWidget *filterTree = new QTreeWidget();
filterTree->headerItem()->setText(0, tr("Name"));
filterTree->headerItem()->setText(1, tr("Filter"));
filterTree->headerItem()->setText(2, tr("_index"));
filterTree->setColumnCount(3);
filterTree->setColumnHidden(2, true);
filterTree->setItemDelegateForColumn(0, &nameDelegate);
filterTree->setItemDelegateForColumn(1, &filterDelegate);
basicTreeWidgetStyle(filterTree);
ActionButtonBox *actionButtons = new ActionButtonBox(ActionButtonBox::UpDownGroup | ActionButtonBox::AddDeleteGroup);
QPushButton *execute = actionButtons->addButton(tr("Execute"));
QPushButton *update = actionButtons->addButton(tr("Update"));
actionButtons->defaultConnect(ActionButtonBox::UpDownGroup, filterTree);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
layout->addWidget(filterTree);
layout->addWidget(actionButtons);
layout->addWidget(buttonBox);
connect(filterTree, &QTreeWidget::itemChanged, this, &WorkoutMenuProvider::manageItemChanged);
QMetaObject::Connection conn = connect(this, &WorkoutMenuProvider::itemsRecreated, this, [=]() {
bool hasItem = filterTree->currentItem() != nullptr;
actionButtons->setButtonEnabled(ActionButtonBox::Delete, hasItem);
execute->setEnabled(hasItem);
update->setEnabled(hasItem && ! editor->text().trimmed().isEmpty());
});
connect(execute, &QPushButton::clicked, this, [=]() {
if (filterTree->currentItem() != nullptr) {
editor->setText(filterTree->currentItem()->data(1, Qt::DisplayRole).toString());
}
});
connect(update, &QPushButton::clicked, this, [=]() {
if (filterTree->currentItem() != nullptr && ! editor->text().trimmed().isEmpty()) {
int index = filterTree->currentItem()->data(2, Qt::DisplayRole).toInt();
QString name = filterTree->currentItem()->data(0, Qt::DisplayRole).toString();
store.update(index, name, editor->text().trimmed());
manageFillItems(filterTree, index);
}
});
connect(actionButtons, &ActionButtonBox::upRequested, this, [=]() { manageMoveUp(filterTree->currentItem()); });
connect(actionButtons, &ActionButtonBox::downRequested, this, [=]() { manageMoveDown(filterTree->currentItem()); });
connect(actionButtons, &ActionButtonBox::addRequested, this, [=]() { manageAdd(filterTree); });
connect(actionButtons, &ActionButtonBox::deleteRequested, this, [=]() { manageDelete(filterTree->currentItem()); });
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::accept);
QMetaObject::Connection updateEnabledConn = connect(editor, &WorkoutFilterBox::textChanged, this, [=](const QString &text) { update->setEnabled(! text.trimmed().isEmpty()); });
manageFillItems(filterTree);
if (dialog.exec() == QDialog::Accepted) {
store.save();
}
disconnect(conn);
disconnect(updateEnabledConn);
}
void
WorkoutMenuProvider::addFilterDialog
()
{
QDialog dialog;
dialog.setWindowTitle(tr("Add Workout Filter"));
QLineEdit *nameEdit = new QLineEdit();
nameEdit->setPlaceholderText(tr("Name of the Filter..."));
nameEdit->setClearButtonEnabled(true);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
layout->addWidget(nameEdit);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
connect(nameEdit, &QLineEdit::textChanged, this, [=](const QString &text) { buttonBox->button(QDialogButtonBox::Ok)->setEnabled(! text.trimmed().isEmpty()); });
nameEdit->setClearButtonEnabled(true);
if (dialog.exec() == QDialog::Accepted) {
store.append(nameEdit->text(), editor->text());
store.save();
}
}
void
WorkoutMenuProvider::manageItemChanged
(QTreeWidgetItem *item)
{
if (item != nullptr) {
store.update(item->data(2, Qt::DisplayRole).toInt(), item->data(0, Qt::DisplayRole).toString(), item->data(1, Qt::DisplayRole).toString());
manageFillItems(item->treeWidget(), item->data(2, Qt::DisplayRole).toInt());
}
}
void
WorkoutMenuProvider::manageAdd
(QTreeWidget *tree)
{
int index = store.getWorkoutFilters().count();
if (tree->currentItem() != nullptr) {
index = tree->currentItem()->data(2, Qt::DisplayRole).toInt();
}
QString token = tr("New");
for (int i = 0; tree->findItems(token, Qt::MatchExactly, 0).count() > 0 || tree->findItems(token, Qt::MatchExactly, 1).count() > 0; i++) {
token = tr("New (%1)").arg(i + 1);
}
store.insert(index, token, token);
manageFillItems(tree, index);
}
void
WorkoutMenuProvider::manageMoveUp
(QTreeWidgetItem *item)
{
if (item != nullptr) {
store.moveUp(item->data(2, Qt::DisplayRole).toInt());
manageFillItems(item->treeWidget(), item->data(2, Qt::DisplayRole).toInt() - 1);
}
}
void
WorkoutMenuProvider::manageMoveDown
(QTreeWidgetItem *item)
{
if (item != nullptr) {
store.moveDown(item->data(2, Qt::DisplayRole).toInt());
manageFillItems(item->treeWidget(), item->data(2, Qt::DisplayRole).toInt() + 1);
}
}
void
WorkoutMenuProvider::manageDelete
(QTreeWidgetItem *item)
{
if (item != nullptr) {
store.remove(item->data(2, Qt::DisplayRole).toInt());
manageFillItems(item->treeWidget(), item->data(2, Qt::DisplayRole).toInt());
}
}
void
WorkoutMenuProvider::manageFillItems
(QTreeWidget *tree, int selected)
{
tree->blockSignals(true);
tree->clear();
QList<WorkoutFilterStore::StorableWorkoutFilter> filters = store.getWorkoutFilters();
int select = std::min(selected, int(filters.count() - 1));
QTreeWidgetItem *selectedItem = nullptr;
int i = 0;
for (const WorkoutFilterStore::StorableWorkoutFilter &filter : filters) {
QTreeWidgetItem *add = new QTreeWidgetItem(tree->invisibleRootItem());
add->setFlags(add->flags() | Qt::ItemIsEditable);
add->setData(0, Qt::DisplayRole, filter.name);
add->setData(1, Qt::DisplayRole, filter.filter);
add->setData(2, Qt::DisplayRole, i);
if (select == i) {
selectedItem = add;
}
++i;
}
tree->blockSignals(false);
if (selectedItem != nullptr) {
tree->setCurrentItem(selectedItem);
}
emit itemsRecreated();
}

View File

@@ -0,0 +1,108 @@
/*
* 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 WORKOUTMENUPROVIDER_H
#define WORKOUTMENUPROVIDER_H
#include <QObject>
#include <QList>
#include <QXmlStreamReader>
#include <QTreeWidget>
#include <QSortFilterProxyModel>
#include "WorkoutFilterBox.h"
class WorkoutFilterStore
{
public:
struct StorableWorkoutFilter
{
StorableWorkoutFilter(const QString &name, const QString &filter) : name(name), filter(filter) {}
QString name;
QString filter;
};
WorkoutFilterStore(const QString &fileName);
QList<StorableWorkoutFilter> getWorkoutFilters() const;
void append(const QString &name, const QString &filter);
void insert(int index, const QString &name, const QString &filter);
void update(int index, const QString &name, const QString &filter);
void remove(int index);
void moveUp(int index);
void moveDown(int index);
void load();
void save();
private:
QList<StorableWorkoutFilter> workoutFilters;
QString fileName;
bool parseWorkoutFilters(QXmlStreamReader &reader);
bool parseWorkoutFilter(QXmlStreamReader &reader);
};
class TopLevelFilterProxyModel : public QSortFilterProxyModel
{
public:
TopLevelFilterProxyModel(QObject *parent = nullptr);
void setContains(const QString &contains);
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
private:
QString contains;
};
class WorkoutMenuProvider : public QObject, public MenuProvider
{
Q_OBJECT
public:
WorkoutMenuProvider(WorkoutFilterBox *editor, const QString &filterFile);
virtual ~WorkoutMenuProvider() {}
void addActions(QMenu *menu) const override;
signals:
void itemsRecreated();
private slots:
void addTagDialog();
void manageFiltersDialog();
void addFilterDialog();
void manageItemChanged(QTreeWidgetItem *item);
void manageAdd(QTreeWidget *tree);
void manageMoveUp(QTreeWidgetItem *item);
void manageMoveDown(QTreeWidgetItem *item);
void manageDelete(QTreeWidgetItem *item);
private:
WorkoutFilterBox *editor;
WorkoutFilterStore store;
void manageFillItems(QTreeWidget *tree, int selected = 0);
};
#endif

View File

@@ -14,11 +14,11 @@ public:
void setFilepath(const QString &filepath);
bool hasTag(int id) const;
void addTag(int id);
void removeTag(int id);
void clearTags();
QList<int> getTagIds() const;
bool hasTag(int id) const override;
void addTag(int id) override;
void removeTag(int id) override;
void clearTags() override;
QList<int> getTagIds() const override;
private:
QString filepath;

View File

@@ -712,7 +712,8 @@ HEADERS += Train/TrainBottom.h Train/TrainDB.h Train/TrainSidebar.h \
Train/LiveMapWebPageWindow.h Train/ScalingLabel.h \
Train/InfoWidget.h Train/PowerInfoWidget.h Train/PowerZonesWidget.h Train/RatingWidget.h \
Train/ErgOverview.h Train/Shy.h \
Train/WorkoutTagWrapper.h
Train/WorkoutTagWrapper.h \
Train/MenuProvider.h Train/WorkoutMenuProvider.h
###=============
@@ -824,7 +825,8 @@ SOURCES += Train/TrainBottom.cpp Train/TrainDB.cpp Train/TrainSidebar.cpp \
Train/LiveMapWebPageWindow.cpp Train/ScalingLabel.cpp \
Train/InfoWidget.cpp Train/PowerInfoWidget.cpp Train/PowerZonesWidget.cpp Train/RatingWidget.cpp \
Train/ErgOverview.cpp Train/Shy.cpp \
Train/WorkoutTagWrapper.cpp
Train/WorkoutTagWrapper.cpp \
Train/WorkoutMenuProvider.cpp
## Crash Handling
win32-msvc* {