diff --git a/src/Charts/PlanningCalendarWindow.cpp b/src/Charts/PlanningCalendarWindow.cpp
index 89863592f..4380a5d13 100644
--- a/src/Charts/PlanningCalendarWindow.cpp
+++ b/src/Charts/PlanningCalendarWindow.cpp
@@ -30,6 +30,7 @@
#include "ManualActivityWizard.h"
#include "RepeatScheduleWizard.h"
#include "WorkoutFilter.h"
+#include "IconManager.h"
#define HLO "
"
#define HLC "
"
@@ -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 {
diff --git a/src/Gui/IconManager.cpp b/src/Gui/IconManager.cpp
new file mode 100644
index 000000000..a90b5115a
--- /dev/null
+++ b/src/Gui/IconManager.cpp
@@ -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
+
+#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 &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
+IconManager::readGroup
+(const QJsonObject &root, const QString &group)
+{
+ QHash 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;
+}
diff --git a/src/Gui/IconManager.h b/src/Gui/IconManager.h
new file mode 100644
index 000000000..292cce1fa
--- /dev/null
+++ b/src/Gui/IconManager.h
@@ -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
+#include
+#include
+
+#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> 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 &data) const;
+ QHash readGroup(const QJsonObject &rootObj, const QString &field);
+};
+
+#endif
diff --git a/src/Gui/ManualActivityWizard.cpp b/src/Gui/ManualActivityWizard.cpp
index 1371e34ba..66921493b 100644
--- a/src/Gui/ManualActivityWizard.cpp
+++ b/src/Gui/ManualActivityWizard.cpp
@@ -40,6 +40,7 @@
#include "RideMetadata.h"
#include "Units.h"
#include "HelpWhatsThis.h"
+#include "IconManager.h"
#define MANDATORY " *"
#define TRADEMARK "TM"
@@ -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));
}
diff --git a/src/Gui/Pages.cpp b/src/Gui/Pages.cpp
index bc2ac1e77..9f8438aef 100644
--- a/src/Gui/Pages.cpp
+++ b/src/Gui/Pages.cpp
@@ -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 &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 &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(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(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(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(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(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(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(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 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(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 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(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(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(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
//
diff --git a/src/Gui/Pages.h b/src/Gui/Pages.h
index 31925f05e..9ee7f8172 100644
--- a/src/Gui/Pages.h
+++ b/src/Gui/Pages.h
@@ -423,6 +423,40 @@ class KeywordsPage : public QWidget
MetadataPage *parent;
};
+class IconsPage : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ IconsPage(const QList &fieldDefinitions, QWidget *parent = nullptr);
+ qint32 saveClicked();
+
+ public slots:
+
+ protected:
+ bool eventFilter(QObject *watched, QEvent *event) override;
+
+ private:
+ QList 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;
diff --git a/src/Resources/application.qrc b/src/Resources/application.qrc
index 58127defd..5b90a33f0 100644
--- a/src/Resources/application.qrc
+++ b/src/Resources/application.qrc
@@ -124,6 +124,8 @@
xml/home-perspectives.xml
ini/measures.ini
html/ltm-summary.html
+ images/breeze/trash-empty.svg
+ images/breeze/edit-image-face-add.svg
images/breeze/media-playlist-repeat.svg
images/breeze/games-highscores.svg
images/breeze/network-mobile-0.svg
@@ -241,12 +243,6 @@
images/services/polarflow.png
images/services/sporttracks.png
images/services/nolio.png
- images/material/bike.svg
- images/material/run.svg
- images/material/swim.svg
- images/material/rowing.svg
- images/material/ski.svg
- images/material/weight-lifter.svg
python/library.py
images/devices/imagic.png
data/powerprofile.csv
diff --git a/src/Resources/images/breeze/edit-image-face-add.svg b/src/Resources/images/breeze/edit-image-face-add.svg
new file mode 100644
index 000000000..ba5869bc0
--- /dev/null
+++ b/src/Resources/images/breeze/edit-image-face-add.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/Resources/images/breeze/trash-empty.svg b/src/Resources/images/breeze/trash-empty.svg
new file mode 100644
index 000000000..0a5869dc0
--- /dev/null
+++ b/src/Resources/images/breeze/trash-empty.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/Resources/images/material/bike.svg b/src/Resources/images/material/bike.svg
deleted file mode 100644
index a425e223b..000000000
--- a/src/Resources/images/material/bike.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/Resources/images/material/rowing.svg b/src/Resources/images/material/rowing.svg
deleted file mode 100644
index 7a75c1f55..000000000
--- a/src/Resources/images/material/rowing.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/Resources/images/material/run.svg b/src/Resources/images/material/run.svg
deleted file mode 100644
index 7b69ebe4a..000000000
--- a/src/Resources/images/material/run.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/Resources/images/material/ski.svg b/src/Resources/images/material/ski.svg
deleted file mode 100644
index cb709a9d2..000000000
--- a/src/Resources/images/material/ski.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/Resources/images/material/swim.svg b/src/Resources/images/material/swim.svg
deleted file mode 100644
index d3632deb7..000000000
--- a/src/Resources/images/material/swim.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/Resources/images/material/weight-lifter.svg b/src/Resources/images/material/weight-lifter.svg
deleted file mode 100644
index 678428012..000000000
--- a/src/Resources/images/material/weight-lifter.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/src.pro b/src/src.pro
index 6127e87ae..12a9d5704 100644
--- a/src/src.pro
+++ b/src/src.pro
@@ -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 \