mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
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:
committed by
GitHub
parent
5dbc0ebce9
commit
e909fad85a
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
src/Resources/images/sidebar/extra-2.png
Normal file
BIN
src/Resources/images/sidebar/extra-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 294 B |
@@ -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
|
||||
|
||||
@@ -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
32
src/Train/MenuProvider.h
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
535
src/Train/WorkoutMenuProvider.cpp
Normal file
535
src/Train/WorkoutMenuProvider.cpp
Normal 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();
|
||||
}
|
||||
108
src/Train/WorkoutMenuProvider.h
Normal file
108
src/Train/WorkoutMenuProvider.h
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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* {
|
||||
|
||||
Reference in New Issue
Block a user