qBittorrent
uithememanager.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2019, 2021 Prince Gupta <[email protected]>
4  *
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.
9  *
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.
14  *
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,
18  * USA.
19  *
20  * In addition, as a special exception, the copyright holders give permission to
21  * link this program with the OpenSSL project's "OpenSSL" library (or with
22  * modified versions of it that use the same license as the "OpenSSL" library),
23  * and distribute the linked executables. You must obey the GNU General Public
24  * License in all respects for all of the code used other than "OpenSSL". If
25  * you modify file(s), you may extend this exception to your version of the
26  * file(s), but you are not obligated to do so. If you do not wish to do so,
27  * delete this exception statement from your version.
28  */
29 
30 #include "uithememanager.h"
31 
32 #include <QApplication>
33 #include <QDir>
34 #include <QFile>
35 #include <QJsonDocument>
36 #include <QJsonObject>
37 #include <QPalette>
38 #include <QResource>
39 
40 #include "base/logger.h"
41 #include "base/preferences.h"
42 #include "base/utils/fs.h"
43 
44 namespace
45 {
46  const QString CONFIG_FILE_NAME = QStringLiteral("config.json");
47  const QString DEFAULT_ICONS_DIR = QStringLiteral(":icons/");
48  const QString STYLESHEET_FILE_NAME = QStringLiteral("stylesheet.qss");
49 
50  // Directory used by stylesheet to reference internal resources
51  // for example `icon: url(:/uitheme/file.svg)` will be expected to
52  // point to a file `file.svg` in root directory of CONFIG_FILE_NAME
53  const QString STYLESHEET_RESOURCES_DIR = QStringLiteral(":/uitheme/");
54 
55  const QString THEME_ICONS_DIR = QStringLiteral("icons/");
56 
57  QString findIcon(const QString &iconId, const QString &dir)
58  {
59  const QString pathSvg = dir + iconId + QLatin1String(".svg");
60  if (QFile::exists(pathSvg))
61  return pathSvg;
62 
63  const QString pathPng = dir + iconId + QLatin1String(".png");
64  if (QFile::exists(pathPng))
65  return pathPng;
66 
67  return {};
68  }
69 
70  QByteArray readFile(const QString &fileName)
71  {
72  QFile file {fileName};
73  if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
74  {
75  LogMsg(UIThemeManager::tr("UITheme - Failed to open \"%1\". Reason: %2")
76  .arg(QFileInfo(fileName).fileName(), file.errorString())
77  , Log::WARNING);
78  return {};
79  }
80 
81  return file.readAll();
82  }
83 
84  class QRCThemeSource final : public UIThemeSource
85  {
86  public:
87  QByteArray readStyleSheet() override
88  {
89  return readFile(m_qrcThemeDir + STYLESHEET_FILE_NAME);
90  }
91 
92  QByteArray readConfig() override
93  {
94  return readFile(m_qrcThemeDir + CONFIG_FILE_NAME);
95  }
96 
97  QString iconPath(const QString &iconId) const override
98  {
99  return findIcon(iconId, m_qrcIconsDir);
100  }
101 
102  private:
103  const QString m_qrcThemeDir {":/uitheme/"};
104  const QString m_qrcIconsDir = m_qrcThemeDir + THEME_ICONS_DIR;
105  };
106 
107  class FolderThemeSource final : public UIThemeSource
108  {
109  public:
110  explicit FolderThemeSource(const QDir &dir)
111  : m_folder {dir}
112  , m_iconsDir {m_folder.absolutePath() + '/' + THEME_ICONS_DIR}
113  {
114  }
115 
116  QByteArray readStyleSheet() override
117  {
118  QByteArray styleSheetData = readFile(m_folder.absoluteFilePath(STYLESHEET_FILE_NAME));
119  return styleSheetData.replace(STYLESHEET_RESOURCES_DIR.toUtf8(), (m_folder.absolutePath() + '/').toUtf8());
120  }
121 
122  QByteArray readConfig() override
123  {
124  return readFile(m_folder.absoluteFilePath(CONFIG_FILE_NAME));
125  }
126 
127  QString iconPath(const QString &iconId) const override
128  {
129  return findIcon(iconId, m_iconsDir);
130  }
131 
132  private:
133  const QDir m_folder;
134  const QString m_iconsDir;
135  };
136 
137 
138  std::unique_ptr<UIThemeSource> createUIThemeSource(const QString &themePath)
139  {
140  const QFileInfo themeInfo {themePath};
141 
142  if (themeInfo.fileName() == CONFIG_FILE_NAME)
143  return std::make_unique<FolderThemeSource>(themeInfo.dir());
144 
145  if ((themeInfo.suffix() == QLatin1String {"qbtheme"})
146  && QResource::registerResource(themePath, QLatin1String {"/uitheme"}))
147  {
148  return std::make_unique<QRCThemeSource>();
149  }
150 
151  return nullptr;
152  }
153 }
154 
156 
158 {
159  delete m_instance;
160  m_instance = nullptr;
161 }
162 
164 {
165  if (!m_instance)
167 }
168 
170  : m_useCustomTheme(Preferences::instance()->useCustomUITheme())
171 #if (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
172  , m_useSystemTheme(Preferences::instance()->useSystemIconTheme())
173 #endif
174 {
175  if (m_useCustomTheme)
176  {
177  const QString themePath = Preferences::instance()->customUIThemePath();
178  m_themeSource = createUIThemeSource(themePath);
179  if (!m_themeSource)
180  {
181  LogMsg(tr("Failed to load UI theme from file: \"%1\"").arg(themePath), Log::WARNING);
182  }
183  else
184  {
186  applyPalette();
187  applyStyleSheet();
188  }
189  }
190 }
191 
193 {
194  return m_instance;
195 }
196 
198 {
199  qApp->setStyleSheet(m_themeSource->readStyleSheet());
200 }
201 
202 QIcon UIThemeManager::getIcon(const QString &iconId, const QString &fallback) const
203 {
204 #if (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
205  if (m_useSystemTheme)
206  {
207  QIcon icon = QIcon::fromTheme(iconId);
208  if (icon.name() != iconId)
209  icon = QIcon::fromTheme(fallback, QIcon(getIconPathFromResources(iconId, fallback)));
210  return icon;
211  }
212 #endif
213 
214  // Cache to avoid rescaling svg icons
215  // And don't cache system icons because users might change them at run time
216  const auto iter = m_iconCache.find(iconId);
217  if (iter != m_iconCache.end())
218  return *iter;
219 
220  const QIcon icon {getIconPathFromResources(iconId, fallback)};
221  m_iconCache[iconId] = icon;
222  return icon;
223 }
224 
225 QIcon UIThemeManager::getFlagIcon(const QString &countryIsoCode) const
226 {
227  if (countryIsoCode.isEmpty()) return {};
228 
229  const QString key = countryIsoCode.toLower();
230  const auto iter = m_flagCache.find(key);
231  if (iter != m_flagCache.end())
232  return *iter;
233 
234  const QIcon icon {QLatin1String(":/icons/flags/") + key + QLatin1String(".svg")};
235  m_flagCache[key] = icon;
236  return icon;
237 }
238 
239 QColor UIThemeManager::getColor(const QString &id, const QColor &defaultColor) const
240 {
241  return m_colors.value(id, defaultColor);
242 }
243 
244 #ifndef Q_OS_MACOS
246 {
248  switch (style)
249  {
250 #if defined(Q_OS_UNIX)
252  return QIcon::fromTheme(QLatin1String("qbittorrent-tray"));
254  return QIcon::fromTheme(QLatin1String("qbittorrent-tray-dark"));
256  return QIcon::fromTheme(QLatin1String("qbittorrent-tray-light"));
257 #else
259  return getIcon(QLatin1String("qbittorrent-tray"));
261  return getIcon(QLatin1String("qbittorrent-tray-dark"));
263  return getIcon(QLatin1String("qbittorrent-tray-light"));
264 #endif
265  default:
266  break;
267  }
268 
269  // As a failsafe in case the enum is invalid
270  return getIcon(QLatin1String("qbittorrent-tray"));
271 }
272 #endif
273 
274 QString UIThemeManager::getIconPath(const QString &iconId) const
275 {
276 #if (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
277  if (m_useSystemTheme)
278  {
279  QString path = Utils::Fs::tempPath() + iconId + QLatin1String(".png");
280  if (!QFile::exists(path))
281  {
282  const QIcon icon = QIcon::fromTheme(iconId);
283  if (!icon.isNull())
284  icon.pixmap(32).save(path);
285  else
286  path = getIconPathFromResources(iconId);
287  }
288 
289  return path;
290  }
291 #endif
292  return getIconPathFromResources(iconId, {});
293 }
294 
295 QString UIThemeManager::getIconPathFromResources(const QString &iconId, const QString &fallback) const
296 {
298  {
299  const QString customIcon = m_themeSource->iconPath(iconId);
300  if (!customIcon.isEmpty())
301  return customIcon;
302 
303  if (!fallback.isEmpty())
304  {
305  const QString fallbackIcon = m_themeSource->iconPath(fallback);
306  if (!fallbackIcon.isEmpty())
307  return fallbackIcon;
308  }
309  }
310 
311  return findIcon(iconId, DEFAULT_ICONS_DIR);
312 }
313 
315 {
316  const QByteArray config = m_themeSource->readConfig();
317  if (config.isEmpty())
318  return;
319 
320  QJsonParseError jsonError;
321  const QJsonDocument configJsonDoc = QJsonDocument::fromJson(config, &jsonError);
322  if (jsonError.error != QJsonParseError::NoError)
323  {
324  LogMsg(tr("\"%1\" has invalid format. Reason: %2").arg(CONFIG_FILE_NAME, jsonError.errorString()), Log::WARNING);
325  return;
326  }
327  if (!configJsonDoc.isObject())
328  {
329  LogMsg(tr("\"%1\" has invalid format. Reason: %2").arg(CONFIG_FILE_NAME, tr("Root JSON value is not an object")), Log::WARNING);
330  return;
331  }
332 
333  const QJsonObject colors = configJsonDoc.object().value("colors").toObject();
334  for (auto color = colors.constBegin(); color != colors.constEnd(); ++color)
335  {
336  const QColor providedColor(color.value().toString());
337  if (!providedColor.isValid())
338  {
339  LogMsg(tr("Invalid color for ID \"%1\" is provided by theme").arg(color.key()), Log::WARNING);
340  continue;
341  }
342  m_colors.insert(color.key(), providedColor);
343  }
344 }
345 
347 {
348  struct ColorDescriptor
349  {
350  QString id;
351  QPalette::ColorRole colorRole;
352  QPalette::ColorGroup colorGroup;
353  };
354 
355  const ColorDescriptor paletteColorDescriptors[] =
356  {
357  {QLatin1String("Palette.Window"), QPalette::Window, QPalette::Normal},
358  {QLatin1String("Palette.WindowText"), QPalette::WindowText, QPalette::Normal},
359  {QLatin1String("Palette.Base"), QPalette::Base, QPalette::Normal},
360  {QLatin1String("Palette.AlternateBase"), QPalette::AlternateBase, QPalette::Normal},
361  {QLatin1String("Palette.Text"), QPalette::Text, QPalette::Normal},
362  {QLatin1String("Palette.ToolTipBase"), QPalette::ToolTipBase, QPalette::Normal},
363  {QLatin1String("Palette.ToolTipText"), QPalette::ToolTipText, QPalette::Normal},
364  {QLatin1String("Palette.BrightText"), QPalette::BrightText, QPalette::Normal},
365  {QLatin1String("Palette.Highlight"), QPalette::Highlight, QPalette::Normal},
366  {QLatin1String("Palette.HighlightedText"), QPalette::HighlightedText, QPalette::Normal},
367  {QLatin1String("Palette.Button"), QPalette::Button, QPalette::Normal},
368  {QLatin1String("Palette.ButtonText"), QPalette::ButtonText, QPalette::Normal},
369  {QLatin1String("Palette.Link"), QPalette::Link, QPalette::Normal},
370  {QLatin1String("Palette.LinkVisited"), QPalette::LinkVisited, QPalette::Normal},
371  {QLatin1String("Palette.Light"), QPalette::Light, QPalette::Normal},
372  {QLatin1String("Palette.Midlight"), QPalette::Midlight, QPalette::Normal},
373  {QLatin1String("Palette.Mid"), QPalette::Mid, QPalette::Normal},
374  {QLatin1String("Palette.Dark"), QPalette::Dark, QPalette::Normal},
375  {QLatin1String("Palette.Shadow"), QPalette::Shadow, QPalette::Normal},
376  {QLatin1String("Palette.WindowTextDisabled"), QPalette::WindowText, QPalette::Disabled},
377  {QLatin1String("Palette.TextDisabled"), QPalette::Text, QPalette::Disabled},
378  {QLatin1String("Palette.ToolTipTextDisabled"), QPalette::ToolTipText, QPalette::Disabled},
379  {QLatin1String("Palette.BrightTextDisabled"), QPalette::BrightText, QPalette::Disabled},
380  {QLatin1String("Palette.HighlightedTextDisabled"), QPalette::HighlightedText, QPalette::Disabled},
381  {QLatin1String("Palette.ButtonTextDisabled"), QPalette::ButtonText, QPalette::Disabled}
382  };
383 
384  QPalette palette = qApp->palette();
385  for (const ColorDescriptor &colorDescriptor : paletteColorDescriptors)
386  {
387  const QColor defaultColor = palette.color(colorDescriptor.colorGroup, colorDescriptor.colorRole);
388  const QColor newColor = getColor(colorDescriptor.id, defaultColor);
389  palette.setColor(colorDescriptor.colorGroup, colorDescriptor.colorRole, newColor);
390  }
391  qApp->setPalette(palette);
392 }
QBITTORRENT_HAS_EXECINFO_H if(NOT QBITTORRENT_HAS_EXECINFO_H) message(FATAL_ERROR "execinfo.h header file not found.\n" "Please either disable the STACKTRACE feature or use a libc that has this header file
static Preferences * instance()
QString customUIThemePath() const
TrayIcon::Style trayIconStyle() const
void applyPalette() const
QHash< QString, QIcon > m_flagCache
QString getIconPath(const QString &iconId) const
std::unique_ptr< UIThemeSource > m_themeSource
QString getIconPathFromResources(const QString &iconId, const QString &fallback={}) const
QIcon getSystrayIcon() const
static UIThemeManager * m_instance
static UIThemeManager * instance()
static void freeInstance()
QColor getColor(const QString &id, const QColor &defaultColor) const
QHash< QString, QColor > m_colors
void applyStyleSheet() const
static void initInstance()
QIcon getFlagIcon(const QString &countryIsoCode) const
QIcon getIcon(const QString &iconId, const QString &fallback={}) const
const bool m_useCustomTheme
void loadColorsFromJSONConfig()
QHash< QString, QIcon > m_iconCache
QString iconPath(const QString &iconId) const override
QString iconPath(const QString &iconId) const override
void LogMsg(const QString &message, const Log::MsgType &type)
Definition: logger.cpp:125
@ WARNING
Definition: logger.h:47
QString fileName(const QString &filePath)
Definition: fs.cpp:87
QString tempPath()
Definition: fs.cpp:314
QString findIcon(const QString &iconId, const QString &dir)
std::unique_ptr< UIThemeSource > createUIThemeSource(const QString &themePath)
QByteArray readFile(const QString &fileName)
file(GLOB QBT_TS_FILES "${qBittorrent_SOURCE_DIR}/src/lang/*.ts") set_source_files_properties($
Definition: CMakeLists.txt:5