Global config dialog to assign icons to Sport / SubSport (#4695)

* New dialog Options > Data Fields > Icons
* New IconManager to centrally assign icons (svg only) to Sport / SubSport
* Removed all material symbols due to license incompatibility
* Using IconManager in PlanningCalendarWindow and ManualActivityWizard
This commit is contained in:
Joachim Kohlhammer
2025-09-06 14:49:40 +02:00
committed by GitHub
parent a1ddf9b8e0
commit be55156336
16 changed files with 974 additions and 44 deletions

View File

@@ -30,6 +30,7 @@
#include "ManualActivityWizard.h"
#include "RepeatScheduleWizard.h"
#include "WorkoutFilter.h"
#include "IconManager.h"
#define HLO "<h4>"
#define HLC "</h4>"
@@ -490,21 +491,7 @@ PlanningCalendarWindow::getActivities
activity.secondaryMetric = "";
}
if (sport == "Bike") {
activity.iconFile = ":images/material/bike.svg";
} else if (sport == "Run") {
activity.iconFile = ":images/material/run.svg";
} else if (sport == "Swim") {
activity.iconFile = ":images/material/swim.svg";
} else if (sport == "Row") {
activity.iconFile = ":images/material/rowing.svg";
} else if (sport == "Ski") {
activity.iconFile = ":images/material/ski.svg";
} else if (sport == "Gym") {
activity.iconFile = ":images/material/weight-lifter.svg";
} else {
activity.iconFile = ":images/breeze/games-highscores.svg";
}
activity.iconFile = IconManager::instance().getFilepath(rideItem);
if (rideItem->color.alpha() < 255 || rideItem->planned) {
activity.color = QColor("#F79130");
} else {

315
src/Gui/IconManager.cpp Normal file
View File

@@ -0,0 +1,315 @@
/*
* 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 "IconManager.h"
#include <QSvgRenderer>
#include "../qzip/zipwriter.h"
#include "../qzip/zipreader.h"
IconManager&
IconManager::instance
()
{
static IconManager instance;
return instance;
}
IconManager::IconManager
()
{
baseDir.mkpath(".");
loadMapping();
}
QString
IconManager::getFilepath
(const QString &sport, const QString &subSport) const
{
QString ret;
if ( ! sport.isEmpty()
&& icons.contains("Sport")
&& ! icons["Sport"].value(sport, "").isEmpty()) {
ret = baseDir.absoluteFilePath(icons["Sport"].value(sport, ""));
}
if ( ! subSport.isEmpty()
&& icons.contains("SubSport")
&& ! icons["SubSport"].value(subSport, "").isEmpty()) {
ret = baseDir.absoluteFilePath(icons["SubSport"].value(subSport, ""));
}
QFileInfo fileInfo(ret);
if (! fileInfo.isFile() || ! fileInfo.isReadable()) {
ret = defaultIcon;
}
return ret;
}
QString
IconManager::getFilepath
(RideItem const * const rideItem) const
{
if (rideItem != nullptr) {
return getFilepath(rideItem->sport, rideItem->getText("SubSport", ""));
}
return defaultIcon;
}
QString
IconManager::getDefault
() const
{
return defaultIcon;
}
QStringList
IconManager::listIconFiles
() const
{
QStringList nameFilter { "*.svg" };
return baseDir.entryList(nameFilter, QDir::Files | QDir::Readable);
}
QString
IconManager::toFilepath
(const QString &filename)
{
return baseDir.absoluteFilePath(filename);
}
QString
IconManager::assignedIcon
(const QString &field, const QString &value) const
{
if (icons.contains(field)) {
return icons[field].value(value, "");
}
return "";
}
void
IconManager::assignIcon
(const QString &field, const QString &value, const QString &filename)
{
if (! filename.isEmpty()) {
icons[field][value] = filename;
} else if (icons.contains(field)) {
icons[field].remove(value);
}
saveConfig();
}
bool
IconManager::addIconFile
(const QFile &sourceFile)
{
QFileInfo fileInfo(sourceFile);
if (fileInfo.suffix() != "svg") {
return false;
}
QSvgRenderer renderer(sourceFile.fileName());
if (! renderer.isValid()) {
return false;
}
return QFile::copy(fileInfo.absoluteFilePath(), baseDir.absoluteFilePath(fileInfo.fileName()));
}
bool
IconManager::deleteIconFile
(const QString &filename)
{
QString filepath = toFilepath(filename);
QFileInfo fileInfo(filepath);
if (fileInfo.suffix() != "svg") {
return false;
}
if (QFile::remove(filepath)) {
bool save = false;
for (QString &field : icons.keys()) {
for (QString &value : icons[field].keys()) {
if (icons[field].value(value, "") == filename) {
save = true;
icons[field].remove(value);
}
}
}
if (save) {
saveConfig();
}
return true;
} else {
return false;
}
}
bool
IconManager::exportBundle
(const QString &filename)
{
QFile zipFile(filename);
if (! zipFile.open(QIODevice::WriteOnly)) {
return false;
}
zipFile.close();
ZipWriter writer(zipFile.fileName());
QStringList files = listIconFiles();
files << "LICENSE"
<< "LICENSE.md"
<< "LICENSE.txt"
<< "README"
<< "README.md"
<< "README.txt"
<< "COPYING"
<< "mapping.json";
for (QString file : files) {
QFile sourceFile(toFilepath(file));
if (sourceFile.open(QIODevice::ReadOnly)) {
writer.addFile(file, sourceFile.readAll());
sourceFile.close();
}
}
writer.close();
return true;
}
bool
IconManager::importBundle
(const QString &filename)
{
QFile zipFile(filename);
if (! zipFile.open(QIODevice::ReadOnly)) {
return false;
}
zipFile.close();
ZipReader reader(zipFile.fileName());
for (ZipReader::FileInfo info : reader.fileInfoList()) {
if (info.isFile) {
QByteArray data = reader.fileData(info.filePath);
QFile file(baseDir.absoluteFilePath(info.filePath));
QFileInfo fileInfo(file);
if (! file.open(QIODevice::WriteOnly)) {
continue;
}
qint64 bytesWritten = file.write(data);
if (bytesWritten == -1) {
continue;
}
file.close();
}
}
reader.close();
return loadMapping();
}
bool
IconManager::saveConfig
() const
{
QJsonObject rootObj;
for (const QString &field : icons.keys()) {
writeGroup(rootObj, field, icons[field]);
}
QJsonDocument jsonDoc(rootObj);
QFile file(baseDir.absoluteFilePath(configFile));
if (! file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning() << "Could not open file for writing:" << file.errorString();
return false;
}
file.write(jsonDoc.toJson(QJsonDocument::Indented));
file.close();
return true;
}
bool
IconManager::loadMapping
()
{
QFile file(baseDir.absoluteFilePath(configFile));
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QByteArray jsonData = file.readAll();
file.close();
QJsonParseError parseError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
if ( ! jsonDoc.isNull()
&& jsonDoc.isObject()) {
QJsonObject root = jsonDoc.object();
icons["Sport"] = readGroup(root, "Sport");
icons["SubSport"] = readGroup(root, "SubSport");
}
} else {
qWarning().noquote().nospace()
<< "Cannot read icon mappings ("
<< file.fileName()
<< "): "
<< file.errorString();
return false;
}
return true;
}
void
IconManager::writeGroup
(QJsonObject &rootObj, const QString &group, const QHash<QString, QString> &data) const
{
QJsonObject groupObj;
if (data.size() > 0) {
for (auto it = data.constBegin(); it != data.constEnd(); ++it) {
groupObj.insert(it.key(), it.value());
}
rootObj.insert(group, groupObj);
}
}
QHash<QString, QString>
IconManager::readGroup
(const QJsonObject &root, const QString &group)
{
QHash<QString, QString> result;
if (root.contains(group)) {
QJsonObject groupObj = root.value(group).toObject();
for (auto it = groupObj.constBegin(); it != groupObj.constEnd(); ++it) {
QString filename = it.value().toString();
if (filename.endsWith(".svg") && QFile::exists(toFilepath(filename))) {
result.insert(it.key(), filename);
}
}
}
return result;
}

73
src/Gui/IconManager.h Normal file
View File

@@ -0,0 +1,73 @@
/*
* 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_IconManager_h
#define _GC_IconManager_h
#include <QString>
#include <QHash>
#include <QDir>
#include "MainWindow.h"
#include "RideItem.h"
class IconManager {
public:
static IconManager &instance();
IconManager(const IconManager&) = delete;
IconManager &operator=(const IconManager&) = delete;
// Full path
QString getFilepath(const QString &sport, const QString &subSport = "") const;
QString getFilepath(RideItem const * const rideItem) const;
QString getDefault() const;
// Filename only
QStringList listIconFiles() const;
QString toFilepath(const QString &filename);
QString assignedIcon(const QString &field, const QString &value) const;
void assignIcon(const QString &field, const QString &value, const QString &filename);
bool addIconFile(const QFile &sourceFile);
bool deleteIconFile(const QString &filename);
bool exportBundle(const QString &filepath);
bool importBundle(const QString &filepath);
bool saveConfig() const;
private:
IconManager();
const QString defaultIcon = ":images/breeze/games-highscores.svg";
QHash<QString, QHash<QString, QString>> icons; // Key 1: Fieldname (Sport, SubSport, ...)
// Key 2: Value (Bike, Run, ...)
// Value: Icon filename (no path)
const QDir baseDir = QDir(gcroot + "/.icons");
const QString configFile = "mapping.json";
bool loadMapping();
void writeGroup(QJsonObject &rootObj, const QString &field, const QHash<QString, QString> &data) const;
QHash<QString, QString> readGroup(const QJsonObject &rootObj, const QString &field);
};
#endif

View File

@@ -40,6 +40,7 @@
#include "RideMetadata.h"
#include "Units.h"
#include "HelpWhatsThis.h"
#include "IconManager.h"
#define MANDATORY " *"
#define TRADEMARK "<sup>TM</sup>"
@@ -81,7 +82,7 @@ ManualActivityWizard::ManualActivityWizard
#else
setWizardStyle(QWizard::ModernStyle);
#endif
setPixmap(ICON_TYPE, svgAsColoredPixmap(":images/breeze/games-highscores.svg", QSize(ICON_SIZE * dpiXFactor, ICON_SIZE * dpiYFactor), ICON_MARGIN * dpiXFactor, ICON_COLOR));
setPixmap(ICON_TYPE, svgAsColoredPixmap(IconManager::instance().getDefault(), QSize(ICON_SIZE * dpiXFactor, ICON_SIZE * dpiYFactor), ICON_MARGIN * dpiXFactor, ICON_COLOR));
setPage(PageBasics, new ManualActivityPageBasics(context, plan, when));
setPage(PageWorkout, new ManualActivityPageWorkout(context));
@@ -331,6 +332,7 @@ ManualActivityPageBasics::ManualActivityPageBasics
connect(timeEdit, &QTimeEdit::timeChanged, this, &ManualActivityPageBasics::checkDateTime);
connect(sportEdit, &QLineEdit::editingFinished, this, &ManualActivityPageBasics::sportsChanged);
connect(sportEdit, &QLineEdit::textChanged, this, [this]() { emit completeChanged(); });
connect(subSportEdit, &QLineEdit::editingFinished, this, &ManualActivityPageBasics::sportsChanged);
registerField("activityDate", dateEdit);
registerField("activityTime", timeEdit, "time", SIGNAL(timeChanged(QTime)));
@@ -419,21 +421,9 @@ void
ManualActivityPageBasics::sportsChanged
()
{
QString path(":images/breeze/games-highscores.svg");
QString sport = RideFile::sportTag(field("sport").toString().trimmed());
if (sport == "Bike") {
path = ":images/material/bike.svg";
} else if (sport == "Run") {
path = ":images/material/run.svg";
} else if (sport == "Swim") {
path = ":images/material/swim.svg";
} else if (sport == "Row") {
path = ":images/material/rowing.svg";
} else if (sport == "Ski") {
path = ":images/material/ski.svg";
} else if (sport == "Gym") {
path = ":images/material/weight-lifter.svg";
}
QString subSport = field("subSport").toString().trimmed();
QString path = IconManager::instance().getFilepath(sport, subSport);
wizard()->setPixmap(ICON_TYPE, svgAsColoredPixmap(path, QSize(ICON_SIZE * dpiXFactor, ICON_SIZE * dpiYFactor), ICON_MARGIN * dpiXFactor, ICON_COLOR));
}

View File

@@ -42,6 +42,7 @@
#include "LocalFileStore.h"
#include "Secrets.h"
#include "Utils.h"
#include "IconManager.h"
#ifdef GC_WANT_PYTHON
#include "PythonEmbed.h"
#include "FixPySettings.h"
@@ -2152,12 +2153,14 @@ MetadataPage::MetadataPage(Context *context) : context(context)
// setup maintenance pages using current config
fieldsPage = new FieldsPage(this, fieldDefinitions);
keywordsPage = new KeywordsPage(this, keywordDefinitions);
iconsPage = new IconsPage(fieldDefinitions, this);
defaultsPage = new DefaultsPage(this, defaultDefinitions);
processorPage = new ProcessorPage(context);
tabs = new QTabWidget(this);
tabs->addTab(fieldsPage, tr("Fields"));
tabs->addTab(keywordsPage, tr("Colour Keywords"));
tabs->addTab(iconsPage, tr("Icons"));
tabs->addTab(defaultsPage, tr("Defaults"));
tabs->addTab(processorPage, tr("Processors && Automation"));
@@ -2198,6 +2201,8 @@ MetadataPage::saveClicked()
if (b4.fieldFingerprint != FieldDefinition::fingerprint(fieldDefinitions))
state += CONFIG_FIELDS;
state |= iconsPage->saveClicked();
return state;
}
@@ -2402,6 +2407,513 @@ KeywordsPage::getDefinitions(QList<KeywordDefinition> &keywordList)
}
}
//
// Icons page
//
#define ICONSPAGE_L_W 64 * dpiXFactor
#define ICONSPAGE_L_H 64 * dpiXFactor
#define ICONSPAGE_L QSize(ICONSPAGE_L_W, ICONSPAGE_L_H)
#define ICONSPAGE_L_SPACE QSize(80 * dpiXFactor, 80 * dpiYFactor)
#define ICONSPAGE_S_W 48 * dpiXFactor
#define ICONSPAGE_S_H 48 * dpiXFactor
#define ICONSPAGE_S QSize(ICONSPAGE_S_W, ICONSPAGE_S_H)
#define ICONSPAGE_MARGIN 2 * dpiXFactor
IconsPage::IconsPage
(const QList<FieldDefinition> &fieldDefinitions, QWidget *parent)
: QWidget(parent), fieldDefinitions(fieldDefinitions)
{
QPalette palette;
sportTree = new QTreeWidget();
sportTree->setColumnCount(3);
basicTreeWidgetStyle(sportTree);
sportTree->setHeaderLabels({ tr("Field"), tr("Value"), tr("Icon") });
sportTree->setIconSize(ICONSPAGE_S);
sportTree->setAcceptDrops(true);
sportTree->installEventFilter(this);
sportTree->viewport()->installEventFilter(this);
initSportTree();
iconList = new QListWidget();
iconList->setViewMode(QListView::IconMode);
iconList->setIconSize(ICONSPAGE_L);
iconList->setGridSize(ICONSPAGE_L_SPACE);
iconList->setResizeMode(QListView::Adjust);
iconList->setWrapping(true);
iconList->setFlow(QListView::LeftToRight);
iconList->setSpacing(10 * dpiXFactor);
iconList->setMovement(QListView::Static);
iconList->setUniformItemSizes(true);
iconList->setSelectionMode(QAbstractItemView::SingleSelection);
iconList->setDragEnabled(false);
iconList->setAcceptDrops(true);
iconList->installEventFilter(this);
iconList->viewport()->installEventFilter(this);
updateIconList();
QPixmap trashPixmap = svgAsColoredPixmap(":images/breeze/trash-empty.svg", ICONSPAGE_L, ICONSPAGE_MARGIN, palette.color(QPalette::WindowText));
QPixmap trashPixmapActive = svgAsColoredPixmap(":images/breeze/trash-empty.svg", ICONSPAGE_L, ICONSPAGE_MARGIN, QColor("#F79130"));
trashIcon.addPixmap(trashPixmap, QIcon::Normal);
trashIcon.addPixmap(trashPixmapActive, QIcon::Active);
trash = new QLabel();
trash->setAcceptDrops(true);
trash->setPixmap(trashIcon.pixmap(ICONSPAGE_L, QIcon::Normal));
trash->installEventFilter(this);
QPushButton *importButton = new QPushButton(tr("Import"));
QPushButton *exportButton = new QPushButton(tr("Export"));
QHBoxLayout *contentLayout = new QHBoxLayout();
contentLayout->addWidget(sportTree);
contentLayout->addWidget(iconList);
QHBoxLayout *actionLayout = new QHBoxLayout();
actionLayout->addWidget(trash);
actionLayout->addStretch();
actionLayout->addWidget(importButton);
actionLayout->addWidget(exportButton);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(contentLayout);
mainLayout->addLayout(actionLayout);
connect(importButton, &QPushButton::clicked, [=]() {
QString zipFile = QFileDialog::getOpenFileName(this, tr("Import Icons"), "", tr("Zip Files (*.zip)"));
if (zipFile.isEmpty() || ! IconManager::instance().importBundle(zipFile)) {
QMessageBox::warning(nullptr, tr("Icons Bundle"), tr("Bundle file %1 cannot be imported.").arg(zipFile));
} else {
initSportTree();
updateIconList();
}
});
connect(exportButton, &QPushButton::clicked, [=]() {
QString zipFile = QFileDialog::getSaveFileName(this, tr("Export Icons"), "", tr("Zip Files (*.zip)"));
if (zipFile.isEmpty() || ! IconManager::instance().exportBundle(zipFile)) {
QMessageBox::warning(nullptr, tr("Icons Bundle"), tr("Bundle file %1 cannot be created.").arg(zipFile));
}
});
}
qint32
IconsPage::saveClicked
()
{
bool changed = false;
int rowCount = sportTree->topLevelItemCount();
for (int i = 0; i < rowCount; ++i) {
QTreeWidgetItem *item = sportTree->topLevelItem(i);
if (! item) {
continue;
}
QString originalIcon = item->data(0, Qt::UserRole + 1).toString();
QString newIcon = item->data(0, Qt::UserRole + 2).toString();
if (originalIcon != newIcon) {
QString type = item->data(0, Qt::UserRole).toString();
QString key = item->data(1, Qt::DisplayRole).toString();
IconManager::instance().assignIcon(type, key, newIcon);
}
}
if (changed) {
return CONFIG_APPEARANCE;
} else {
return 0;
}
}
bool
IconsPage::eventFilter
(QObject *watched, QEvent *event)
{
bool handled = false;
if (watched == trash) {
handled = eventFilterTrash(event);
} else if (watched == sportTree) {
handled = eventFilterSportTree(event);
} else if (watched == sportTree->viewport()) {
handled = eventFilterSportTreeViewport(event);
} else if (watched == iconList) {
handled = eventFilterIconList(event);
} else if (watched == iconList->viewport()) {
handled = eventFilterIconListViewport(event);
}
return handled ? true : QObject::eventFilter(watched, event);
}
bool
IconsPage::eventFilterTrash
(QEvent *event)
{
if ( event->type() == QEvent::DragEnter
|| event->type() == QEvent::Drop) {
QDropEvent *dropEvent = static_cast<QDropEvent*>(event);
if ( dropEvent->mimeData()->hasFormat("application/x-gc-icon")
&& (dropEvent->possibleActions() & Qt::MoveAction)
&& ( dropEvent->source() == sportTree
|| dropEvent->source() == iconList)) {
dropEvent->setDropAction(Qt::MoveAction);
dropEvent->accept();
trash->setPixmap(trashIcon.pixmap(ICONSPAGE_L, event->type() == QEvent::Drop ? QIcon::Normal : QIcon::Active));
return true;
}
} else if (event->type() == QEvent::DragLeave) {
trash->setPixmap(trashIcon.pixmap(ICONSPAGE_L, QIcon::Normal));
return true;
}
return false;
}
bool
IconsPage::eventFilterSportTree
(QEvent *event)
{
if (event->type() == QEvent::DragEnter) {
QDragEnterEvent *dragEnterEvent = static_cast<QDragEnterEvent*>(event);
if ( dragEnterEvent->mimeData()->hasFormat("application/x-gc-icon")
&& (dragEnterEvent->possibleActions() & Qt::LinkAction)
&& dragEnterEvent->source() == iconList) {
sportTree->setStyleSheet("QTreeWidget { border: 2px solid #F79130; border-radius: 8px; }");
dragEnterEvent->setDropAction(Qt::LinkAction);
dragEnterEvent->accept();
return true;
}
} else if (event->type() == QEvent::DragMove) {
QDragMoveEvent *dragMoveEvent = static_cast<QDragMoveEvent*>(event);
if ( dragMoveEvent->mimeData()->hasFormat("application/x-gc-icon")
&& (dragMoveEvent->possibleActions() & Qt::LinkAction)
&& dragMoveEvent->source() == iconList) {
QPoint globalCursor = QCursor::pos();
QPoint pos = sportTree->viewport()->mapFromGlobal(globalCursor);
QTreeWidgetItem* targetItem = sportTree->itemAt(pos);
if (targetItem) {
sportTree->setCurrentItem(targetItem);
dragMoveEvent->setDropAction(Qt::LinkAction);
dragMoveEvent->accept();
} else {
dragMoveEvent->ignore();
}
return true;
}
} else if (event->type() == QEvent::DragLeave) {
sportTree->setStyleSheet("");
return true;
} else if (event->type() == QEvent::Drop) {
QDropEvent *dropEvent = static_cast<QDropEvent*>(event);
if ( dropEvent->mimeData()->hasFormat("application/x-gc-icon")
&& (dropEvent->possibleActions() & Qt::LinkAction)
&& dropEvent->source() == iconList) {
QByteArray iconBytes = dropEvent->mimeData()->data("application/x-gc-icon");
QString iconFile = QString::fromUtf8(iconBytes);
QPoint globalCursor = QCursor::pos();
QPoint pos = sportTree->viewport()->mapFromGlobal(globalCursor);
QTreeWidgetItem* targetItem = sportTree->itemAt(pos);
if (targetItem) {
QPalette palette;
QElapsedTimer timer;
timer.start();
QPixmap pixmap = svgAsColoredPixmap(IconManager::instance().toFilepath(iconFile), QSize(1000, 1000), 0, palette.color(QPalette::Text));
qint64 elapsed = timer.elapsed();
if ( elapsed < 50
|| QMessageBox::question(this, tr("Complex Icon"), tr("The selected icon %1 appears to be complex and could impact performance. Are you sure you want to use this icon?").arg(iconFile)) == QMessageBox::Yes) {
QPixmap pixmap = svgAsColoredPixmap(IconManager::instance().toFilepath(iconFile), ICONSPAGE_S, ICONSPAGE_MARGIN, palette.color(QPalette::Text));
targetItem->setData(0, Qt::UserRole + 2, iconFile);
targetItem->setIcon(2, QIcon(pixmap));
dropEvent->setDropAction(Qt::LinkAction);
dropEvent->accept();
} else {
dropEvent->ignore();
}
}
}
sportTree->setStyleSheet("");
return true;
}
return false;
}
bool
IconsPage::eventFilterSportTreeViewport
(QEvent *event)
{
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
#if QT_VERSION >= 0x060000
QTreeWidgetItem *item = sportTree->itemAt(mouseEvent->position().toPoint());
#else
QTreeWidgetItem *item = sportTree->itemAt(mouseEvent->pos());
#endif
if (item && ! item->data(0, Qt::UserRole + 2).toString().isEmpty()) {
#if QT_VERSION >= 0x060000
sportTreeDragStartPos = mouseEvent->position().toPoint();
#else
sportTreeDragStartPos = mouseEvent->pos();
#endif
sportTreeDragWatch = true;
} else {
sportTreeDragWatch = false;
}
}
return true;
} else if (event->type() == QEvent::MouseMove) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
if ( ! (mouseEvent->buttons() & Qt::LeftButton)
|| ! sportTreeDragWatch
#if QT_VERSION >= 0x060000
|| (mouseEvent->position().toPoint() - sportTreeDragStartPos).manhattanLength() < QApplication::startDragDistance()) {
#else
|| (mouseEvent->pos() - sportTreeDragStartPos).manhattanLength() < QApplication::startDragDistance()) {
#endif
return true;
}
sportTreeDragWatch = false;
#if QT_VERSION >= 0x060000
QTreeWidgetItem *item = sportTree->itemAt(mouseEvent->position().toPoint());
#else
QTreeWidgetItem *item = sportTree->itemAt(mouseEvent->pos());
#endif
if (! item) {
return true;
}
QByteArray entryBytes = item->data(0, Qt::UserRole).toString().toUtf8();
QMimeData *mimeData = new QMimeData();
mimeData->setData("application/x-gc-icon", entryBytes);
QDrag *drag = new QDrag(sportTree);
drag->setMimeData(mimeData);
drag->setPixmap(item->icon(2).pixmap(ICONSPAGE_S));
drag->setHotSpot(QPoint(ICONSPAGE_S_W / 2, ICONSPAGE_S_H / 2));
Qt::DropAction dropAction = drag->exec(Qt::MoveAction);
if (dropAction == Qt::MoveAction) {
QPixmap pixmap(ICONSPAGE_S);
pixmap.fill(Qt::transparent);
QIcon icon(pixmap);
item->setData(0, Qt::UserRole + 2, "");
item->setIcon(2, icon);
}
return true;
}
return false;
}
bool
IconsPage::eventFilterIconList
(QEvent *event)
{
if (event->type() == QEvent::DragEnter) {
QDragEnterEvent *dragEnterEvent = static_cast<QDragEnterEvent*>(event);
if ( dragEnterEvent->mimeData()->hasFormat("application/x-gc-icon")
&& (dragEnterEvent->possibleActions() & Qt::MoveAction)
&& dragEnterEvent->source() == sportTree) {
dragEnterEvent->setDropAction(Qt::MoveAction);
dragEnterEvent->accept();
} else if ( dragEnterEvent->mimeData()->hasUrls()
&& (dragEnterEvent->possibleActions() & Qt::CopyAction)) {
const QList<QUrl> urls = dragEnterEvent->mimeData()->urls();
int svgs = 0;
for (const QUrl &url : urls) {
QString path = url.toLocalFile();
if (path.endsWith(".svg")) {
++svgs;
}
}
if (svgs > 0) {
dragEnterEvent->setDropAction(Qt::CopyAction);
dragEnterEvent->accept();
}
}
if (dragEnterEvent->isAccepted()) {
iconList->setStyleSheet("QListWidget { border: 2px solid #F79130; border-radius: 8px; }");
}
return true;
} else if (event->type() == QEvent::DragLeave) {
iconList->setStyleSheet("");
return true;
} else if (event->type() == QEvent::Drop) {
QDropEvent *dropEvent = static_cast<QDropEvent*>(event);
if ( dropEvent->mimeData()->hasFormat("application/x-gc-icon")
&& (dropEvent->possibleActions() & Qt::MoveAction)
&& dropEvent->source() == sportTree) {
dropEvent->setDropAction(Qt::MoveAction);
dropEvent->accept();
} else if ( dropEvent->mimeData()->hasUrls()
&& (dropEvent->possibleActions() & Qt::CopyAction)) {
const QList<QUrl> urls = dropEvent->mimeData()->urls();
int added = 0;
for (const QUrl &url : urls) {
QString path = url.toLocalFile();
if (IconManager::instance().addIconFile(path)) {
++added;
}
}
if (added > 0) {
dropEvent->setDropAction(Qt::CopyAction);
dropEvent->accept();
updateIconList();
}
}
iconList->setStyleSheet("");
return true;
}
return false;
}
bool
IconsPage::eventFilterIconListViewport
(QEvent *event)
{
if (event->type() == QEvent::Paint) {
if (iconList->count() == 0) {
QPaintEvent *paintEvent = static_cast<QPaintEvent*>(event);
QPainter painter(iconList->viewport());
QPalette palette;
QColor pixmapColor = palette.color(QPalette::Disabled, QPalette::Text);
if (palette.color(QPalette::Base).lightness() < 127) {
pixmapColor = pixmapColor.darker(135);
} else {
pixmapColor = pixmapColor.lighter(135);
}
QPixmap pixmap = svgAsColoredPixmap(":/images/breeze/edit-image-face-add.svg", QSize(256 * dpiXFactor, 256 * dpiYFactor), 0, pixmapColor);
painter.drawPixmap((paintEvent->rect().width() - pixmap.width()) / 2, (paintEvent->rect().height() - pixmap.height()) / 2, pixmap);
painter.drawText(paintEvent->rect(), Qt::AlignCenter | Qt::TextWordWrap, tr("No icons available.\nDrag and drop .svg files here to add icons."));
return true;
}
return false;
} else if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
#if QT_VERSION >= 0x060000
QListWidgetItem *item = iconList->itemAt(mouseEvent->position().toPoint());
#else
QListWidgetItem *item = iconList->itemAt(mouseEvent->pos());
#endif
if (item) {
#if QT_VERSION >= 0x060000
iconListDragStartPos = mouseEvent->position().toPoint();
#else
iconListDragStartPos = mouseEvent->pos();
#endif
iconListDragWatch = true;
} else {
iconListDragWatch = false;
}
}
return true;
} else if (event->type() == QEvent::MouseMove) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
if ( ! (mouseEvent->buttons() & Qt::LeftButton)
|| ! iconListDragWatch
#if QT_VERSION >= 0x060000
|| (mouseEvent->position().toPoint() - iconListDragStartPos).manhattanLength() < QApplication::startDragDistance()) {
#else
|| (mouseEvent->pos() - iconListDragStartPos).manhattanLength() < QApplication::startDragDistance()) {
#endif
return true;
}
iconListDragWatch = false;
#if QT_VERSION >= 0x060000
QListWidgetItem *item = iconList->itemAt(mouseEvent->position().toPoint());
#else
QListWidgetItem *item = iconList->itemAt(mouseEvent->pos());
#endif
if (! item) {
return true;
}
QString iconFile = item->data(Qt::UserRole).toString();
QMimeData *mimeData = new QMimeData();
mimeData->setData("application/x-gc-icon", iconFile.toUtf8());
QDrag *drag = new QDrag(iconList);
drag->setMimeData(mimeData);
drag->setPixmap(item->icon().pixmap(ICONSPAGE_L));
drag->setHotSpot(QPoint(ICONSPAGE_L_W / 2, ICONSPAGE_L_H / 2));
Qt::DropAction dropAction = drag->exec(Qt::MoveAction | Qt::LinkAction);
if (dropAction == Qt::MoveAction) {
if (IconManager::instance().deleteIconFile(iconFile)) {
updateIconList();
int rowCount = sportTree->topLevelItemCount();
for (int i = 0; i < rowCount; ++i) {
QTreeWidgetItem *item = sportTree->topLevelItem(i);
if (item && item->data(0, Qt::UserRole + 2).toString() == iconFile) {
QPixmap pixmap(ICONSPAGE_S);
pixmap.fill(Qt::transparent);
item->setData(0, Qt::UserRole + 2, "");
item->setIcon(2, QIcon(pixmap));
}
}
}
}
return true;
}
return false;
}
void
IconsPage::initSportTree
()
{
sportTree->clear();
QPalette palette;
SpecialFields &specials = SpecialFields::getInstance();
for (QString field : { "Sport", "SubSport" }) {
for (const FieldDefinition &fieldDefinition : fieldDefinitions) {
if (fieldDefinition.name == field) {
for (const QString &fieldValue : fieldDefinition.values) {
QIcon icon;
QString assignedFile = IconManager::instance().assignedIcon(fieldDefinition.name, fieldValue);
if (! assignedFile.isEmpty()) {
QPixmap pixmap = svgAsColoredPixmap(IconManager::instance().toFilepath(assignedFile), ICONSPAGE_S, ICONSPAGE_MARGIN, palette.color(QPalette::Text));
icon.addPixmap(pixmap);
} else {
QPixmap pixmap(ICONSPAGE_S);
pixmap.fill(Qt::transparent);
icon.addPixmap(pixmap);
}
QTreeWidgetItem *item = new QTreeWidgetItem();
item->setData(0, Qt::DisplayRole, specials.displayName(fieldDefinition.name));
item->setData(0, Qt::UserRole, fieldDefinition.name);
item->setData(0, Qt::UserRole + 1, assignedFile);
item->setData(0, Qt::UserRole + 2, assignedFile);
item->setData(1, Qt::DisplayRole, fieldValue);
item->setIcon(2, icon);
sportTree->addTopLevelItem(item);
}
}
}
}
}
void
IconsPage::updateIconList
()
{
iconList->clear();
QPalette palette;
QStringList icons = IconManager::instance().listIconFiles();
for (QString icon : icons) {
QPixmap pixmap = svgAsColoredPixmap(IconManager::instance().toFilepath(icon), ICONSPAGE_L, ICONSPAGE_MARGIN, palette.color(QPalette::Text));
QListWidgetItem *item = new QListWidgetItem(QIcon(pixmap), "");
item->setData(Qt::UserRole, icon);
iconList->addItem(item);
}
}
//
// Ride metadata page
//

View File

@@ -423,6 +423,40 @@ class KeywordsPage : public QWidget
MetadataPage *parent;
};
class IconsPage : public QWidget
{
Q_OBJECT
public:
IconsPage(const QList<FieldDefinition> &fieldDefinitions, QWidget *parent = nullptr);
qint32 saveClicked();
public slots:
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
QList<FieldDefinition> fieldDefinitions;
QTreeWidget *sportTree;
QListWidget *iconList;
QLabel *trash;
QIcon trashIcon;
QPoint sportTreeDragStartPos;
bool sportTreeDragWatch = false;
QPoint iconListDragStartPos;
bool iconListDragWatch = false;
bool eventFilterTrash(QEvent *event);
bool eventFilterSportTree(QEvent *event);
bool eventFilterSportTreeViewport(QEvent *event);
bool eventFilterIconList(QEvent *event);
bool eventFilterIconListViewport(QEvent *event);
void initSportTree();
void updateIconList();
};
class ColorsPage : public QWidget
{
Q_OBJECT
@@ -594,6 +628,7 @@ class MetadataPage : public QWidget
QTabWidget *tabs;
KeywordsPage *keywordsPage;
IconsPage *iconsPage;
FieldsPage *fieldsPage;
DefaultsPage *defaultsPage;
ProcessorPage *processorPage;

View File

@@ -124,6 +124,8 @@
<file>xml/home-perspectives.xml</file>
<file>ini/measures.ini</file>
<file>html/ltm-summary.html</file>
<file>images/breeze/trash-empty.svg</file>
<file>images/breeze/edit-image-face-add.svg</file>
<file>images/breeze/media-playlist-repeat.svg</file>
<file>images/breeze/games-highscores.svg</file>
<file>images/breeze/network-mobile-0.svg</file>
@@ -241,12 +243,6 @@
<file>images/services/polarflow.png</file>
<file>images/services/sporttracks.png</file>
<file>images/services/nolio.png</file>
<file>images/material/bike.svg</file>
<file>images/material/run.svg</file>
<file>images/material/swim.svg</file>
<file>images/material/rowing.svg</file>
<file>images/material/ski.svg</file>
<file>images/material/weight-lifter.svg</file>
<file>python/library.py</file>
<file>images/devices/imagic.png</file>
<file>data/powerprofile.csv</file>

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 2 2 L 2 5 L 3 5 L 3 3 L 5 3 L 5 2 L 2 2 z M 6 2 L 6 3 L 10 3 L 10 2 L 6 2 z M 11 2 L 11 3 L 13 3 L 13 5 L 14 5 L 14 2 L 11 2 z M 8 4 A 1.5 1.5 0 0 0 6.5 5.5 A 1.5 1.5 0 0 0 8 7 A 1.5 1.5 0 0 0 9.5 5.5 A 1.5 1.5 0 0 0 8 4 z M 2 6 L 2 10 L 3 10 L 3 6 L 2 6 z M 13 6 L 13 9 L 14 9 L 14 6 L 13 6 z M 8 8 A 3 3 0 0 0 5 11 L 5 12 L 8 12 L 8 11 L 6 11 A 2 2 0 0 1 8 9 L 10.232422 9 A 3 3 0 0 0 8 8 z M 11 9 L 11 11 L 9 11 L 9 12 L 11 12 L 11 14 L 12 14 L 12 12 L 14 12 L 14 11 L 12 11 L 12 9 L 11 9 z M 2 11 L 2 14 L 5 14 L 5 13 L 3 13 L 3 11 L 2 11 z M 6 13 L 6 14 L 9 14 L 9 13 L 6 13 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 920 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="m5 2v2h1v-1h4v1h1v-2h-5zm-3 3v1h2v8h8v-8h2v-1zm3 1h6v7h-6z"
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 394 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-bike" viewBox="0 0 24 24"><path d="M5,20.5A3.5,3.5 0 0,1 1.5,17A3.5,3.5 0 0,1 5,13.5A3.5,3.5 0 0,1 8.5,17A3.5,3.5 0 0,1 5,20.5M5,12A5,5 0 0,0 0,17A5,5 0 0,0 5,22A5,5 0 0,0 10,17A5,5 0 0,0 5,12M14.8,10H19V8.2H15.8L13.86,4.93C13.57,4.43 13,4.1 12.4,4.1C11.93,4.1 11.5,4.29 11.2,4.6L7.5,8.29C7.19,8.6 7,9 7,9.5C7,10.13 7.33,10.66 7.85,10.97L11.2,13V18H13V11.5L10.75,9.85L13.07,7.5M19,20.5A3.5,3.5 0 0,1 15.5,17A3.5,3.5 0 0,1 19,13.5A3.5,3.5 0 0,1 22.5,17A3.5,3.5 0 0,1 19,20.5M19,12A5,5 0 0,0 14,17A5,5 0 0,0 19,22A5,5 0 0,0 24,17A5,5 0 0,0 19,12M16,4.8C17,4.8 17.8,4 17.8,3C17.8,2 17,1.2 16,1.2C15,1.2 14.2,2 14.2,3C14.2,4 15,4.8 16,4.8Z" /></svg>

Before

Width:  |  Height:  |  Size: 693 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-rowing" viewBox="0 0 24 24"><path d="M8.5,14.5L4,19L5.5,20.5L9,17H11L8.5,14.5M15,1A2,2 0 0,0 13,3A2,2 0 0,0 15,5A2,2 0 0,0 17,3A2,2 0 0,0 15,1M21,21L18,24L15,21V19.5L7.91,12.41C7.6,12.46 7.3,12.5 7,12.5V10.32C8.66,10.35 10.61,9.45 11.67,8.28L13.07,6.73C13.26,6.5 13.5,6.35 13.76,6.23C14.05,6.09 14.38,6 14.72,6H14.75C16,6 17,7 17,8.26V14C17,14.85 16.65,15.62 16.08,16.17L12.5,12.59V10.32C11.87,10.84 11.07,11.34 10.21,11.71L16.5,18H18L21,21Z" /></svg>

Before

Width:  |  Height:  |  Size: 499 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-run" viewBox="0 0 24 24"><path d="M13.5,5.5C14.59,5.5 15.5,4.58 15.5,3.5C15.5,2.38 14.59,1.5 13.5,1.5C12.39,1.5 11.5,2.38 11.5,3.5C11.5,4.58 12.39,5.5 13.5,5.5M9.89,19.38L10.89,15L13,17V23H15V15.5L12.89,13.5L13.5,10.5C14.79,12 16.79,13 19,13V11C17.09,11 15.5,10 14.69,8.58L13.69,7C13.29,6.38 12.69,6 12,6C11.69,6 11.5,6.08 11.19,6.08L6,8.28V13H8V9.58L9.79,8.88L8.19,17L3.29,16L2.89,18L9.89,19.38Z" /></svg>

Before

Width:  |  Height:  |  Size: 454 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-ski" viewBox="0 0 24 24"><path d="M17.92 13.32C17.67 13.28 16.71 13 16.46 12.89L14.39 19.37L11.3 18.24L13.5 12.47L10.45 9L13 7.54C13.45 8.67 14.17 9.62 15.12 10.4S17.16 11.67 18.38 11.86L19.5 8.43L18.06 7.96L17.54 9.56C16.88 9.28 16.3 8.86 15.8 8.32C15.3 7.77 14.94 7.13 14.72 6.41L14.39 5.33C14.27 4.93 14.04 4.61 13.71 4.37C13.38 4.14 13 4 12.63 3.97C12.24 3.94 11.86 4 11.5 4.21L8 6.23C7.63 6.44 7.36 6.74 7.19 7.12C7 7.5 6.96 7.88 7 8.29S7.26 9.06 7.54 9.37L11.11 13.08L9.42 17.54L2.47 15.05L2 16.46L16.04 21.58C16.82 21.86 17.65 22 18.53 22C19.15 22 19.76 21.92 20.36 21.77C20.95 21.61 21.5 21.39 22 21.11L20.87 20C20.12 20.33 19.34 20.5 18.53 20.5C17.87 20.5 17.21 20.39 16.55 20.17L15.8 19.89L17.92 13.32M19 3C19 4.11 18.11 5 17 5S15 4.11 15 3 15.9 1 17 1 19 1.9 19 3Z" /></svg>

Before

Width:  |  Height:  |  Size: 833 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-swim" viewBox="0 0 24 24"><path d="M2,18C4.22,17 6.44,16 8.67,16C10.89,16 13.11,18 15.33,18C17.56,18 19.78,16 22,16V19C19.78,19 17.56,21 15.33,21C13.11,21 10.89,19 8.67,19C6.44,19 4.22,20 2,21V18M8.67,13C7.89,13 7.12,13.12 6.35,13.32L11.27,9.88L10.23,8.64C10.09,8.47 10,8.24 10,8C10,7.66 10.17,7.35 10.44,7.17L16.16,3.17L17.31,4.8L12.47,8.19L17.7,14.42C16.91,14.75 16.12,15 15.33,15C13.11,15 10.89,13 8.67,13M18,7A2,2 0 0,1 20,9A2,2 0 0,1 18,11A2,2 0 0,1 16,9A2,2 0 0,1 18,7Z" /></svg>

Before

Width:  |  Height:  |  Size: 533 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-weight-lifter" viewBox="0 0 24 24"><path d="M12 5C10.89 5 10 5.89 10 7S10.89 9 12 9 14 8.11 14 7 13.11 5 12 5M22 1V6H20V4H4V6H2V1H4V3H20V1H22M15 11.26V23H13V18H11V23H9V11.26C6.93 10.17 5.5 8 5.5 5.5L5.5 5H7.5L7.5 5.5C7.5 8 9.5 10 12 10S16.5 8 16.5 5.5L16.5 5H18.5L18.5 5.5C18.5 8 17.07 10.17 15 11.26Z" /></svg>

Before

Width:  |  Height:  |  Size: 359 B

View File

@@ -651,7 +651,8 @@ HEADERS += Gui/AboutDialog.h Gui/AddIntervalDialog.h Gui/AnalysisSidebar.h Gui/C
Gui/AddTileWizard.h Gui/NavigationModel.h Gui/AthleteView.h Gui/AthleteConfigDialog.h Gui/AthletePages.h Gui/Perspective.h \
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/CalendarData.h Gui/CalendarItemDelegates.h
Gui/Calendar.h Gui/CalendarData.h Gui/CalendarItemDelegates.h \
Gui/IconManager.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 +764,8 @@ SOURCES += Gui/AboutDialog.cpp Gui/AddIntervalDialog.cpp Gui/AnalysisSidebar.cpp
Gui/AddTileWizard.cpp Gui/NavigationModel.cpp Gui/AthleteView.cpp Gui/AthleteConfigDialog.cpp Gui/AthletePages.cpp Gui/Perspective.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/CalendarItemDelegates.cpp
Gui/Calendar.cpp Gui/CalendarItemDelegates.cpp \
Gui/IconManager.cpp
## Models and Metrics
SOURCES += Metrics/aBikeScore.cpp Metrics/aCoggan.cpp Metrics/AerobicDecoupling.cpp Metrics/Banister.cpp Metrics/BasicRideMetrics.cpp \