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"
32 #include <QColorDialog>
33 #include <QFileDialog>
34 #include <QJsonDocument>
35 #include <QJsonObject>
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"
50 #include "ui_uithemedialog.h"
52 #define SETTINGS_KEY(name) u"GUI/UIThemeDialog/" name
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
)
73 explicit ColorWidget(const QColor
¤tColor
, const QColor
&defaultColor
, QWidget
*parent
= nullptr)
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
;
92 void mouseDoubleClickEvent([[maybe_unused
]] QMouseEvent
*event
) override
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
)
113 m_currentColor
= color
;
114 applyColor(m_currentColor
);
119 setCurrentColor(m_defaultColor
);
122 void applyColor(const QColor
&color
)
126 setStyleSheet(u
"#colorWidget { background-color: %1; }"_s
.arg(color
.name()));
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());
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
)
158 explicit IconWidget(const Path
¤tPath
, const Path
&defaultPath
, QWidget
*parent
= nullptr)
160 , m_defaultPath
{defaultPath
}
162 setObjectName(u
"iconWidget"_s
);
163 setAlignment(Qt::AlignCenter
);
165 setCurrentPath(currentPath
);
168 Path
currentPath() const
170 return m_currentPath
;
174 void mouseDoubleClickEvent([[maybe_unused
]] QMouseEvent
*event
) override
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
)
195 m_currentPath
= path
;
196 showIcon(m_currentPath
);
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
);
225 const Path m_defaultPath
;
229 UIThemeDialog::UIThemeDialog(QWidget
*parent
)
231 , m_ui
{new Ui::UIThemeDialog
}
232 , m_storeDialogSize
{SETTINGS_KEY(u
"Size"_s
)}
236 connect(m_ui
->buttonBox
, &QDialogButtonBox::accepted
, this, &QDialog::accept
);
237 connect(m_ui
->buttonBox
, &QDialogButtonBox::rejected
, this, &QDialog::reject
);
242 if (const QSize dialogSize
= m_storeDialogSize
; dialogSize
.isValid())
246 UIThemeDialog::~UIThemeDialog()
248 m_storeDialogSize
= size();
252 void UIThemeDialog::accept()
256 bool hasError
= false;
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());
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);
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()));
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);
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
;
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
);
351 const QString error
= tr("Couldn't save UI Theme configuration. Reason: %1").arg(result
.error());
352 LogMsg(error
, Log::WARNING
);
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
))
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
);
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
);
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
);
408 updateIcons(ColorMode::Light
);
409 updateIcons(ColorMode::Dark
);