Enable customizing the save statistics time interval
[qBittorrent.git] / src / gui / uithemedialog.cpp
blobff39805427994c633b6acf018b0e907d8d756f03
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2023-2024 Vladimir Golovnev <glassez@yandex.ru>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
29 #include "uithemedialog.h"
31 #include <QColor>
32 #include <QColorDialog>
33 #include <QFileDialog>
34 #include <QJsonDocument>
35 #include <QJsonObject>
36 #include <QLabel>
37 #include <QMenu>
38 #include <QMessageBox>
40 #include "base/3rdparty/expected.hpp"
41 #include "base/global.h"
42 #include "base/logger.h"
43 #include "base/path.h"
44 #include "base/profile.h"
45 #include "base/utils/fs.h"
46 #include "base/utils/io.h"
47 #include "uithemecommon.h"
48 #include "utils.h"
50 #include "ui_uithemedialog.h"
52 #define SETTINGS_KEY(name) u"GUI/UIThemeDialog/" name
54 namespace
56 Path userConfigPath()
58 return specialFolderLocation(SpecialFolder::Config) / Path(u"themes/default"_s);
61 Path defaultIconPath(const QString &iconID, [[maybe_unused]] const ColorMode colorMode)
63 return Path(u":icons"_s) / Path(iconID + u".svg");
67 class ColorWidget final : public QLabel
69 Q_DISABLE_COPY_MOVE(ColorWidget)
70 Q_DECLARE_TR_FUNCTIONS(ColorWidget)
72 public:
73 explicit ColorWidget(const QColor &currentColor, const QColor &defaultColor, QWidget *parent = nullptr)
74 : QLabel(parent)
75 , m_defaultColor {defaultColor}
76 , m_currentColor {currentColor}
78 setObjectName(u"colorWidget"_s);
79 setFrameShape(QFrame::Box);
80 setFrameShadow(QFrame::Plain);
81 setAlignment(Qt::AlignCenter);
83 applyColor(currentColor);
86 QColor currentColor() const
88 return m_currentColor;
91 private:
92 void mouseDoubleClickEvent([[maybe_unused]] QMouseEvent *event) override
94 showColorDialog();
97 void contextMenuEvent([[maybe_unused]] QContextMenuEvent *event) override
99 QMenu *menu = new QMenu(this);
100 menu->setAttribute(Qt::WA_DeleteOnClose);
102 menu->addAction(tr("Edit..."), this, &ColorWidget::showColorDialog);
103 menu->addAction(tr("Reset"), this, &ColorWidget::resetColor);
105 menu->popup(QCursor::pos());
108 void setCurrentColor(const QColor &color)
110 if (m_currentColor == color)
111 return;
113 m_currentColor = color;
114 applyColor(m_currentColor);
117 void resetColor()
119 setCurrentColor(m_defaultColor);
122 void applyColor(const QColor &color)
124 if (color.isValid())
126 setStyleSheet(u"#colorWidget { background-color: %1; }"_s.arg(color.name()));
127 setText({});
129 else
131 setStyleSheet({});
132 setText(tr("System"));
136 void showColorDialog()
138 auto *dialog = new QColorDialog(m_currentColor, this);
139 dialog->setAttribute(Qt::WA_DeleteOnClose);
140 connect(dialog, &QDialog::accepted, this, [this, dialog]
142 setCurrentColor(dialog->currentColor());
145 dialog->open();
148 const QColor m_defaultColor;
149 QColor m_currentColor;
152 class IconWidget final : public QLabel
154 Q_DISABLE_COPY_MOVE(IconWidget)
155 Q_DECLARE_TR_FUNCTIONS(IconWidget)
157 public:
158 explicit IconWidget(const Path &currentPath, const Path &defaultPath, QWidget *parent = nullptr)
159 : QLabel(parent)
160 , m_defaultPath {defaultPath}
162 setObjectName(u"iconWidget"_s);
163 setAlignment(Qt::AlignCenter);
165 setCurrentPath(currentPath);
168 Path currentPath() const
170 return m_currentPath;
173 private:
174 void mouseDoubleClickEvent([[maybe_unused]] QMouseEvent *event) override
176 showFileDialog();
179 void contextMenuEvent([[maybe_unused]] QContextMenuEvent *event) override
181 QMenu *menu = new QMenu(this);
182 menu->setAttribute(Qt::WA_DeleteOnClose);
184 menu->addAction(tr("Browse..."), this, &IconWidget::showFileDialog);
185 menu->addAction(tr("Reset"), this, &IconWidget::resetIcon);
187 menu->popup(QCursor::pos());
190 void setCurrentPath(const Path &path)
192 if (m_currentPath == path)
193 return;
195 m_currentPath = path;
196 showIcon(m_currentPath);
199 void resetIcon()
201 setCurrentPath(m_defaultPath);
204 void showIcon(const Path &iconPath)
206 const QIcon icon {iconPath.data()};
207 setPixmap(icon.pixmap(Utils::Gui::smallIconSize()));
210 void showFileDialog()
212 auto *dialog = new QFileDialog(this, tr("Select icon")
213 , QDir::homePath(), (tr("Supported image files") + u" (*.svg *.png)"));
214 dialog->setFileMode(QFileDialog::ExistingFile);
215 dialog->setAttribute(Qt::WA_DeleteOnClose);
216 connect(dialog, &QDialog::accepted, this, [this, dialog]
218 const Path iconPath {dialog->selectedFiles().value(0)};
219 setCurrentPath(iconPath);
222 dialog->open();
225 const Path m_defaultPath;
226 Path m_currentPath;
229 UIThemeDialog::UIThemeDialog(QWidget *parent)
230 : QDialog(parent)
231 , m_ui {new Ui::UIThemeDialog}
232 , m_storeDialogSize {SETTINGS_KEY(u"Size"_s)}
234 m_ui->setupUi(this);
236 connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
237 connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
239 loadColors();
240 loadIcons();
242 if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
243 resize(dialogSize);
246 UIThemeDialog::~UIThemeDialog()
248 m_storeDialogSize = size();
249 delete m_ui;
252 void UIThemeDialog::accept()
254 QDialog::accept();
256 bool hasError = false;
257 if (!storeColors())
258 hasError = true;
259 if (!storeIcons())
260 hasError = true;
262 if (hasError)
264 QMessageBox::critical(this, tr("UI Theme Configuration.")
265 , tr("The UI Theme changes could not be fully applied. The details can be found in the Log."));
269 void UIThemeDialog::loadColors()
271 const QHash<QString, UIThemeColor> defaultColors = defaultUIThemeColors();
272 const QList<QString> colorIDs = std::invoke([](auto &&list) { list.sort(); return list; }, defaultColors.keys());
273 int row = 2;
274 for (const QString &id : colorIDs)
276 if (id == u"Log.Normal")
277 qDebug() << "!!!!!!!";
278 m_ui->colorsLayout->addWidget(new QLabel(id), row, 0);
280 const UIThemeColor &defaultColor = defaultColors.value(id);
282 auto *lightColorWidget = new ColorWidget(m_defaultThemeSource.getColor(id, ColorMode::Light), defaultColor.light, this);
283 m_lightColorWidgets.insert(id, lightColorWidget);
284 m_ui->colorsLayout->addWidget(lightColorWidget, row, 2);
286 auto *darkColorWidget = new ColorWidget(m_defaultThemeSource.getColor(id, ColorMode::Dark), defaultColor.dark, this);
287 m_darkColorWidgets.insert(id, darkColorWidget);
288 m_ui->colorsLayout->addWidget(darkColorWidget, row, 4);
290 ++row;
294 void UIThemeDialog::loadIcons()
296 const QSet<QString> defaultIcons = defaultUIThemeIcons();
297 const QList<QString> iconIDs = std::invoke([](auto &&list) { list.sort(); return list; }
298 , QList<QString>(defaultIcons.cbegin(), defaultIcons.cend()));
299 int row = 2;
300 for (const QString &id : iconIDs)
302 m_ui->iconsLayout->addWidget(new QLabel(id), row, 0);
304 auto *lightIconWidget = new IconWidget(m_defaultThemeSource.getIconPath(id, ColorMode::Light)
305 , defaultIconPath(id, ColorMode::Light), this);
306 m_lightIconWidgets.insert(id, lightIconWidget);
307 m_ui->iconsLayout->addWidget(lightIconWidget, row, 2);
309 auto *darkIconWidget = new IconWidget(m_defaultThemeSource.getIconPath(id, ColorMode::Dark)
310 , defaultIconPath(id, ColorMode::Dark), this);
311 m_darkIconWidgets.insert(id, darkIconWidget);
312 m_ui->iconsLayout->addWidget(darkIconWidget, row, 4);
314 ++row;
318 bool UIThemeDialog::storeColors()
320 QJsonObject userConfig;
321 userConfig.insert(u"version", 2);
323 const QHash<QString, UIThemeColor> defaultColors = defaultUIThemeColors();
324 const auto addColorOverrides = [this, &defaultColors, &userConfig](const ColorMode colorMode)
326 const QHash<QString, ColorWidget *> &colorWidgets = (colorMode == ColorMode::Light)
327 ? m_lightColorWidgets : m_darkColorWidgets;
329 QJsonObject colors;
330 for (auto it = colorWidgets.cbegin(); it != colorWidgets.cend(); ++it)
332 const QString &colorID = it.key();
333 const QColor &defaultColor = (colorMode == ColorMode::Light)
334 ? defaultColors.value(colorID).light : defaultColors.value(colorID).dark;
335 const QColor &color = it.value()->currentColor();
336 if (color != defaultColor)
337 colors.insert(it.key(), color.name());
340 if (!colors.isEmpty())
341 userConfig.insert(((colorMode == ColorMode::Light) ? KEY_COLORS_LIGHT : KEY_COLORS_DARK), colors);
344 addColorOverrides(ColorMode::Light);
345 addColorOverrides(ColorMode::Dark);
347 const QByteArray configData = QJsonDocument(userConfig).toJson();
348 const nonstd::expected<void, QString> result = Utils::IO::saveToFile((userConfigPath() / Path(CONFIG_FILE_NAME)), configData);
349 if (!result)
351 const QString error = tr("Couldn't save UI Theme configuration. Reason: %1").arg(result.error());
352 LogMsg(error, Log::WARNING);
353 return false;
356 return true;
359 bool UIThemeDialog::storeIcons()
361 bool hasError = false;
363 const auto updateIcons = [this, &hasError](const ColorMode colorMode)
365 const QHash<QString, IconWidget *> &iconWidgets = (colorMode == ColorMode::Light)
366 ? m_lightIconWidgets : m_darkIconWidgets;
367 const Path subdirPath = (colorMode == ColorMode::Light)
368 ? Path(u"icons/light"_s) : Path(u"icons/dark"_s);
370 for (auto it = iconWidgets.cbegin(); it != iconWidgets.cend(); ++it)
372 const QString &id = it.key();
373 const Path &path = it.value()->currentPath();
374 if (path == m_defaultThemeSource.getIconPath(id, colorMode))
375 continue;
377 const Path &userIconPathBase = userConfigPath() / subdirPath / Path(id);
379 if (const Path oldIconPath = userIconPathBase + u".svg"
380 ; path.exists() && !Utils::Fs::removeFile(oldIconPath))
382 const QString error = tr("Couldn't remove icon file. File: %1.").arg(oldIconPath.toString());
383 LogMsg(error, Log::WARNING);
384 hasError = true;
385 continue;
388 if (const Path oldIconPath = userIconPathBase + u".png"
389 ; path.exists() && !Utils::Fs::removeFile(oldIconPath))
391 const QString error = tr("Couldn't remove icon file. File: %1.").arg(oldIconPath.toString());
392 LogMsg(error, Log::WARNING);
393 hasError = true;
394 continue;
397 if (const Path targetPath = userIconPathBase + path.extension()
398 ; !Utils::Fs::copyFile(path, targetPath))
400 const QString error = tr("Couldn't copy icon file. Source: %1. Destination: %2.")
401 .arg(path.toString(), targetPath.toString());
402 LogMsg(error, Log::WARNING);
403 hasError = true;
408 updateIcons(ColorMode::Light);
409 updateIcons(ColorMode::Dark);
411 return !hasError;