qTox  Version: nightly | Commit: bc751c8e1cac455f9690654fcfe0f560d2d7dfdd
style.cpp
Go to the documentation of this file.
1 /*
2  Copyright © 2014-2019 by The qTox Project Contributors
3 
4  This file is part of qTox, a Qt-based graphical interface for Tox.
5 
6  qTox is libre software: you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation, either version 3 of the License, or
9  (at your option) any later version.
10 
11  qTox is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with qTox. If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "style.h"
22 #include "src/widget/gui.h"
23 
24 #include <QDebug>
25 #include <QDir>
26 #include <QFile>
27 #include <QFontInfo>
28 #include <QMap>
29 #include <QPainter>
30 #include <QRegularExpression>
31 #include <QSettings>
32 #include <QStandardPaths>
33 #include <QStringBuilder>
34 #include <QStyle>
35 #include <QSvgRenderer>
36 #include <QWidget>
37 
66 namespace {
67  const QLatin1String ThemeSubFolder{"themes/"};
68  const QLatin1String BuiltinThemeDefaultPath{":themes/default/"};
69  const QLatin1String BuiltinThemeDarkPath{":themes/dark/"};
70 }
71 
72 // helper functions
73 QFont appFont(int pixelSize, int weight)
74 {
75  QFont font;
76  font.setPixelSize(pixelSize);
77  font.setWeight(weight);
78  return font;
79 }
80 
81 QString qssifyFont(QFont font)
82 {
83  return QString("%1 %2px \"%3\"").arg(font.weight() * 8).arg(font.pixelSize()).arg(font.family());
84 }
85 
86 static QMap<Style::ColorPalette, QColor> palette;
87 
88 static QMap<QString, QString> dictColor;
89 static QMap<QString, QString> dictFont;
90 static QMap<QString, QString> dictTheme;
91 
92 static const QList<Style::ThemeNameColor> themeNameColors = {
93  {Style::Light, QObject::tr("Default"), QColor()},
94  {Style::Light, QObject::tr("Blue"), QColor("#004aa4")},
95  {Style::Light, QObject::tr("Olive"), QColor("#97ba00")},
96  {Style::Light, QObject::tr("Red"), QColor("#c23716")},
97  {Style::Light, QObject::tr("Violet"), QColor("#4617b5")},
98  {Style::Dark, QObject::tr("Dark"), QColor()},
99  {Style::Dark, QObject::tr("Dark blue"), QColor("#00336d")},
100  {Style::Dark, QObject::tr("Dark olive"), QColor("#4d5f00")},
101  {Style::Dark, QObject::tr("Dark red"), QColor("#7a210d")},
102  {Style::Dark, QObject::tr("Dark violet"), QColor("#280d6c")}
103 };
104 
106 {
107  QStringList l;
108 
109  for (auto t : themeNameColors) {
110  l << t.name;
111  }
112 
113  return l;
114 }
115 
117 {
118  //TODO: return name of the current theme
119  const QString themeName = "default";
120  return QStringLiteral("default");
121 }
122 
124 {
125  const QString themeName = getThemeName();
126  const QString themeFolder = ThemeSubFolder % themeName;
127  const QString fullPath = QStandardPaths::locate(QStandardPaths::AppDataLocation,
128  themeFolder, QStandardPaths::LocateDirectory);
129 
130  // No themes available, fallback to builtin
131  if(fullPath.isEmpty()) {
132  return getThemePath();
133  }
134 
135  return fullPath % QDir::separator();
136 }
137 
138 
139 static const QMap<Style::ColorPalette, QString> aliasColors = {
140  {Style::TransferGood, "transferGood"},
141  {Style::TransferWait, "transferWait"},
142  {Style::TransferBad, "transferBad"},
143  {Style::TransferMiddle, "transferMiddle"},
144  {Style::MainText,"mainText"},
145  {Style::NameActive, "nameActive"},
146  {Style::StatusActive,"statusActive"},
147  {Style::GroundExtra, "groundExtra"},
148  {Style::GroundBase, "groundBase"},
149  {Style::Orange, "orange"},
150  {Style::Yellow, "yellow"},
151  {Style::ThemeDark, "themeDark"},
152  {Style::ThemeMediumDark, "themeMediumDark"},
153  {Style::ThemeMedium, "themeMedium"},
154  {Style::ThemeLight, "themeLight"},
155  {Style::Action, "action"},
156  {Style::Link, "link"},
157  {Style::SearchHighlighted, "searchHighlighted"},
158  {Style::SelectText, "selectText"},
159 };
160 
161 // stylesheet filename, font -> stylesheet
162 // QString implicit sharing deduplicates stylesheets rather than constructing a new one each time
163 static std::map<std::pair<const QString, const QFont>, const QString> stylesheetsCache;
164 
165 const QString Style::getStylesheet(const QString& filename, const QFont& baseFont)
166 {
167  const QString fullPath = getThemeFolder() + filename;
168  const std::pair<const QString, const QFont> cacheKey(fullPath, baseFont);
169  auto it = stylesheetsCache.find(cacheKey);
170  if (it != stylesheetsCache.end())
171  {
172  // cache hit
173  return it->second;
174  }
175  // cache miss, new styleSheet, read it from file and add to cache
176  const QString newStylesheet = resolve(filename, baseFont);
177  stylesheetsCache.insert(std::make_pair(cacheKey, newStylesheet));
178  return newStylesheet;
179 }
180 
181 static QStringList existingImagesCache;
182 const QString Style::getImagePath(const QString& filename)
183 {
184  QString fullPath = getThemeFolder() + filename;
185 
186  // search for image in cache
187  if (existingImagesCache.contains(fullPath)) {
188  return fullPath;
189  }
190 
191  // if not in cache
192  if (QFileInfo::exists(fullPath)) {
193  existingImagesCache << fullPath;
194  return fullPath;
195  } else {
196  qWarning() << "Failed to open file (using defaults):" << fullPath;
197 
198  fullPath = getThemePath() % filename;
199 
200  if (QFileInfo::exists(fullPath)) {
201  return fullPath;
202  } else {
203  qWarning() << "Failed to open default file:" << fullPath;
204  return {};
205  }
206  }
207 }
208 
210 {
211  return palette[entry];
212 }
213 
215 {
216  // fonts as defined in
217  // https://github.com/ItsDuke/Tox-UI/blob/master/UI%20GUIDELINES.md
218 
219  static int defSize = QFontInfo(QFont()).pixelSize();
220 
221  static QFont fonts[] = {
222  appFont(defSize + 3, QFont::Bold), // extra big
223  appFont(defSize + 1, QFont::Normal), // big
224  appFont(defSize + 1, QFont::Bold), // big bold
225  appFont(defSize, QFont::Normal), // medium
226  appFont(defSize, QFont::Bold), // medium bold
227  appFont(defSize - 1, QFont::Normal), // small
228  appFont(defSize - 1, QFont::Light), // small light
229  };
230 
231  return fonts[font];
232 }
233 
234 const QString Style::resolve(const QString& filename, const QFont& baseFont)
235 {
236  QString themePath = getThemeFolder();
237  QString fullPath = themePath + filename;
238  QString qss;
239 
240  QFile file{fullPath};
241  if (file.open(QFile::ReadOnly | QFile::Text)) {
242  qss = file.readAll();
243  } else {
244  qWarning() << "Failed to open file (using defaults):" << fullPath;
245 
246  fullPath = getThemePath();
247  QFile file{fullPath};
248 
249  if (file.open(QFile::ReadOnly | QFile::Text)) {
250  qss = file.readAll();
251  } else {
252  qWarning() << "Failed to open default file:" << fullPath;
253  return {};
254  }
255  }
256 
257  if (palette.isEmpty()) {
258  initPalette();
259  }
260 
261  if (dictColor.isEmpty()) {
262  initDictColor();
263  }
264 
265  if (dictFont.isEmpty()) {
266  dictFont = {
267  {"@baseFont",
268  QString::fromUtf8("'%1' %2px").arg(baseFont.family()).arg(QFontInfo(baseFont).pixelSize())},
269  {"@extraBig", qssifyFont(Style::getFont(Style::ExtraBig))},
271  {"@bigBold", qssifyFont(Style::getFont(Style::BigBold))},
272  {"@medium", qssifyFont(Style::getFont(Style::Medium))},
273  {"@mediumBold", qssifyFont(Style::getFont(Style::MediumBold))},
274  {"@small", qssifyFont(Style::getFont(Style::Small))},
275  {"@smallLight", qssifyFont(Style::getFont(Style::SmallLight))}};
276  }
277 
278  for (const QString& key : dictColor.keys()) {
279  qss.replace(QRegularExpression(key % QLatin1String{"\\b"}), dictColor[key]);
280  }
281 
282  for (const QString& key : dictFont.keys()) {
283  qss.replace(QRegularExpression(key % QLatin1String{"\\b"}), dictFont[key]);
284  }
285 
286  for (const QString& key : dictTheme.keys()) {
287  qss.replace(QRegularExpression(key % QLatin1String{"\\b"}), dictTheme[key]);
288  }
289 
290  // @getImagePath() function
291  const QRegularExpression re{QStringLiteral(R"(@getImagePath\([^)\s]*\))")};
292  QRegularExpressionMatchIterator i = re.globalMatch(qss);
293 
294  while (i.hasNext()) {
295  QRegularExpressionMatch match = i.next();
296  QString path = match.captured(0);
297  const QString phrase = path;
298 
299  path.remove(QStringLiteral("@getImagePath("));
300  path.chop(1);
301 
302  QString fullImagePath = getThemeFolder() + path;
303  // image not in cache
304  if (!existingImagesCache.contains(fullPath)) {
305  if (QFileInfo::exists(fullImagePath)) {
306  existingImagesCache << fullImagePath;
307  } else {
308  qWarning() << "Failed to open file (using defaults):" << fullImagePath;
309  fullImagePath = getThemePath() % path;
310  }
311  }
312 
313  qss.replace(phrase, fullImagePath);
314  }
315 
316  return qss;
317 }
318 
319 void Style::repolish(QWidget* w)
320 {
321  w->style()->unpolish(w);
322  w->style()->polish(w);
323 
324  for (QObject* o : w->children()) {
325  QWidget* c = qobject_cast<QWidget*>(o);
326  if (c) {
327  c->style()->unpolish(c);
328  c->style()->polish(c);
329  }
330  }
331 }
332 
333 void Style::setThemeColor(int color)
334 {
335  stylesheetsCache.clear(); // clear stylesheet cache which includes color info
336  palette.clear();
337  dictColor.clear();
338  initPalette();
339  initDictColor();
340  if (color < 0 || color >= themeNameColors.size())
341  setThemeColor(QColor());
342  else
343  setThemeColor(themeNameColors[color].color);
344 }
345 
352 void Style::setThemeColor(const QColor& color)
353 {
354  if (!color.isValid()) {
355  // Reset to default
356  palette[ThemeDark] = getColor(ThemeDark);
358  palette[ThemeMedium] = getColor(ThemeMedium);
359  palette[ThemeLight] = getColor(ThemeLight);
360  } else {
361  palette[ThemeDark] = color.darker(155);
362  palette[ThemeMediumDark] = color.darker(135);
363  palette[ThemeMedium] = color.darker(120);
364  palette[ThemeLight] = color.lighter(110);
365  }
366 
367  dictTheme["@themeDark"] = getColor(ThemeDark).name();
368  dictTheme["@themeMediumDark"] = getColor(ThemeMediumDark).name();
369  dictTheme["@themeMedium"] = getColor(ThemeMedium).name();
370  dictTheme["@themeLight"] = getColor(ThemeLight).name();
371 }
372 
377 {
379 }
380 
381 QPixmap Style::scaleSvgImage(const QString& path, uint32_t width, uint32_t height)
382 {
383  QSvgRenderer render(path);
384  QPixmap pixmap(width, height);
385  pixmap.fill(QColor(0, 0, 0, 0));
386  QPainter painter(&pixmap);
387  render.render(&painter, pixmap.rect());
388  return pixmap;
389 }
390 
392 {
393  QSettings settings(getThemePath() % "palette.ini", QSettings::IniFormat);
394 
395  auto keys = aliasColors.keys();
396 
397  settings.beginGroup("colors");
398  QMap<Style::ColorPalette, QString> c;
399  for (auto k : keys) {
400  c[k] = settings.value(aliasColors[k], "#000").toString();
401  palette[k] = QColor(settings.value(aliasColors[k], "#000").toString());
402  }
403  auto p = palette;
404  settings.endGroup();
405 
406 }
407 
409 {
410  dictColor = {
411  {"@transferGood", Style::getColor(Style::TransferGood).name()},
412  {"@transferWait", Style::getColor(Style::TransferWait).name()},
413  {"@transferBad", Style::getColor(Style::TransferBad).name()},
414  {"@transferMiddle", Style::getColor(Style::TransferMiddle).name()},
415  {"@mainText", Style::getColor(Style::MainText).name()},
416  {"@nameActive", Style::getColor(Style::NameActive).name()},
417  {"@statusActive", Style::getColor(Style::StatusActive).name()},
418  {"@groundExtra", Style::getColor(Style::GroundExtra).name()},
419  {"@groundBase", Style::getColor(Style::GroundBase).name()},
420  {"@orange", Style::getColor(Style::Orange).name()},
421  {"@yellow", Style::getColor(Style::Yellow).name()},
422  {"@action", Style::getColor(Style::Action).name()},
423  {"@link", Style::getColor(Style::Link).name()},
424  {"@searchHighlighted", Style::getColor(Style::SearchHighlighted).name()},
425  {"@selectText", Style::getColor(Style::SelectText).name()}};
426 }
427 
429 {
430  const int num = Settings::getInstance().getThemeColor();
431  if (themeNameColors[num].type == Dark) {
432  return BuiltinThemeDarkPath;
433  }
434 
435  return BuiltinThemeDefaultPath;
436 }
Style::getFont
static QFont getFont(Font font)
Definition: style.cpp:214
Style::MediumBold
@ MediumBold
Definition: style.h:60
Style::TransferGood
@ TransferGood
Definition: style.h:33
Style::Light
@ Light
Definition: style.h:67
style.h
appFont
QFont appFont(int pixelSize, int weight)
Definition: style.cpp:73
settings.h
Style::ThemeMediumDark
@ ThemeMediumDark
Definition: style.h:45
Style::ThemeLight
@ ThemeLight
Definition: style.h:47
Style::Link
@ Link
Definition: style.h:49
Style::Action
@ Action
Definition: style.h:48
Style::getThemePath
static QString getThemePath()
Definition: style.cpp:428
Style::ColorPalette
ColorPalette
Definition: style.h:31
HistMessageContentType::file
@ file
Style::GroundExtra
@ GroundExtra
Definition: style.h:40
Style::getThemeName
static QString getThemeName()
Definition: style.cpp:116
Style::TransferMiddle
@ TransferMiddle
Definition: style.h:36
QList
Definition: friendlist.h:25
Style::StatusActive
@ StatusActive
Definition: style.h:39
Style::getImagePath
static const QString getImagePath(const QString &filename)
Definition: style.cpp:182
Style::setThemeColor
static void setThemeColor(int color)
Definition: style.cpp:333
Style::Orange
@ Orange
Definition: style.h:42
Style::applyTheme
static void applyTheme()
Reloads some CCS.
Definition: style.cpp:376
Style::SmallLight
@ SmallLight
Definition: style.h:62
Style::getThemeFolder
static QString getThemeFolder()
Definition: style.cpp:123
Style::getThemeColorNames
static QStringList getThemeColorNames()
Definition: style.cpp:105
Style::SearchHighlighted
@ SearchHighlighted
Definition: style.h:50
Style::NameActive
@ NameActive
Definition: style.h:38
Style::ExtraBig
@ ExtraBig
Definition: style.h:56
Style::initPalette
static void initPalette()
Definition: style.cpp:391
Style::TransferWait
@ TransferWait
Definition: style.h:34
Style::Big
@ Big
Definition: style.h:57
qssifyFont
QString qssifyFont(QFont font)
Definition: style.cpp:81
Style::ThemeMedium
@ ThemeMedium
Definition: style.h:46
Style::ThemeDark
@ ThemeDark
Definition: style.h:44
Style::SelectText
@ SelectText
Definition: style.h:51
Style::repolish
static void repolish(QWidget *w)
Definition: style.cpp:319
Style::getColor
static QColor getColor(ColorPalette entry)
Definition: style.cpp:209
Settings::getInstance
static Settings & getInstance()
Returns the singleton instance.
Definition: settings.cpp:88
Style::GroundBase
@ GroundBase
Definition: style.h:41
Style::initDictColor
static void initDictColor()
Definition: style.cpp:408
GUI::reloadTheme
static void reloadTheme()
Reloads the application theme and redraw the window.
Definition: gui.cpp:101
Style::Font
Font
Definition: style.h:54
Settings::getThemeColor
int getThemeColor() const
Definition: settings.cpp:2140
Style::resolve
static const QString resolve(const QString &filename, const QFont &baseFont=QFont())
Definition: style.cpp:234
Style::getStylesheet
static const QString getStylesheet(const QString &filename, const QFont &baseFont=QFont())
Definition: style.cpp:165
Style::MainText
@ MainText
Definition: style.h:37
Style::BigBold
@ BigBold
Definition: style.h:58
gui.h
Style::Dark
@ Dark
Definition: style.h:68
Style::scaleSvgImage
static QPixmap scaleSvgImage(const QString &path, uint32_t width, uint32_t height)
Definition: style.cpp:381
Style::Small
@ Small
Definition: style.h:61
Style::TransferBad
@ TransferBad
Definition: style.h:35
Style::Medium
@ Medium
Definition: style.h:59
Style::Yellow
@ Yellow
Definition: style.h:43