swift
stylesheetutility.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2013 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include <QAbstractScrollArea>
7 #include <QDir>
8 #include <QFile>
9 #include <QFileInfo>
10 #include <QFileInfoList>
11 #include <QFlags>
12 #include <QFont>
13 #include <QIODevice>
14 #include <QRegularExpression>
15 #include <QStringBuilder>
16 #include <QStyleOption>
17 #include <QStylePainter>
18 #include <QTextStream>
19 #include <QWidget>
20 #include <QtGlobal>
21 
22 #include "config/buildconfig.h"
23 #include "misc/fileutils.h"
24 #include "misc/logmessage.h"
25 #include "misc/swiftdirectories.h"
26 
27 using namespace swift::config;
28 using namespace swift::misc;
29 
30 namespace swift::gui
31 {
32  CStyleSheetUtility::CStyleSheetUtility(QObject *parent) : QObject(parent)
33  {
34  this->read();
35  connect(&m_fileWatcher, &QFileSystemWatcher::directoryChanged, this, &CStyleSheetUtility::qssDirectoryChanged);
36  connect(&m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &CStyleSheetUtility::qssDirectoryChanged);
37  }
38 
40  {
41  static const QStringList cats { CLogCategories::guiComponent() };
42  return cats;
43  }
44 
45  const QString &CStyleSheetUtility::fontStyleAsString(const QFont &font)
46  {
47  static const QString n("normal");
48  static const QString i("italic");
49  static const QString o("oblique");
50  static const QString e;
51 
52  switch (font.style())
53  {
54  case QFont::StyleNormal: return n;
55  case QFont::StyleItalic: return i;
56  case QFont::StyleOblique: return o;
57  default: return e;
58  }
59  }
60 
61  const QString &CStyleSheetUtility::fontWeightAsString(const QFont &font)
62  {
63  if (font.weight() < static_cast<int>(QFont::Normal))
64  {
65  static const QString l("light");
66  return l;
67  }
68  else if (font.weight() < static_cast<int>(QFont::DemiBold))
69  {
70  static const QString n("normal");
71  return n;
72  }
73  else if (font.weight() < static_cast<int>(QFont::Bold))
74  {
75  static const QString d("demibold");
76  return d;
77  }
78  else if (font.weight() < static_cast<int>(QFont::Black))
79  {
80  static const QString b("bold");
81  return b;
82  }
83  else
84  {
85  static const QString b("black");
86  return b;
87  }
88  }
89 
91  {
92  QString w = fontWeightAsString(font);
93  QString s = fontStyleAsString(font);
94  if (w == s) return w; // avoid "normal" "normal"
95  if (w.isEmpty() && s.isEmpty()) return "normal";
96  if (w.isEmpty()) return s;
97  if (s.isEmpty()) return w;
98  if (s == "normal") return w;
99  return w.append(" ").append(s);
100  }
101 
102  QString CStyleSheetUtility::asStylesheet(const QString &fontFamily, const QString &fontSize,
103  const QString &fontStyle, const QString &fontWeight,
104  const QString &fontColor)
105  {
106  static const QString indent(" ");
107  static const QString lf("\n");
108  static const QString fontStyleSheet(
109  "%1font-family: \"%3\";%2%1font-size: %4;%2%1font-style: %5;%2%1font-weight: %6;%2%1color: %7;%2");
110  static const QString fontStyleSheetNoColor(
111  "%1font-family: \"%3\";%2%1font-size: %4;%2%1font-style: %5;%2%1font-weight: %6;%2");
112 
113  return fontColor.isEmpty() ?
114  fontStyleSheetNoColor.arg(indent, lf, fontFamily, fontSize, fontStyle, fontWeight) :
115  fontStyleSheet.arg(indent, lf, fontFamily, fontSize, fontStyle, fontWeight, fontColor);
116  }
117 
118  QString CStyleSheetUtility::asStylesheet(const QWidget *widget, int pointSize)
119  {
120  Q_ASSERT_X(widget, Q_FUNC_INFO, "Missing widget");
121  const QFont f = widget->font();
123  f.family(), QStringLiteral("%1pt").arg(pointSize < 0 ? f.pointSize() : pointSize),
125  }
126 
128  {
129  const QString s = this->style(fileNameFonts()).toLower();
130  if (!s.contains("color:")) return "";
131  thread_local const QRegularExpression rx("color:\\s*(#*\\w+);");
132  const QString c = rx.match(s).captured(1);
133  return c.isEmpty() ? "" : c;
134  }
135 
137  {
138  QDir directory(CSwiftDirectories::stylesheetsDirectory());
139  if (!directory.exists()) { return false; }
140 
141  // qss/css files
142  const bool needsWatcher = m_fileWatcher.files().isEmpty();
143  if (needsWatcher)
144  {
145  m_fileWatcher.addPath(CSwiftDirectories::stylesheetsDirectory());
146  } // directory to deleted file watching
147  directory.setNameFilters({ "*.qss", "*.css" });
148  directory.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks);
149 
150  QMap<QString, QString> newStyleSheets;
151  const QFileInfoList fileInfoList = directory.entryInfoList();
152 
153  // here we generate the style sheets
154  for (const QFileInfo &fileInfo : fileInfoList)
155  {
156  const QString absolutePath = fileInfo.absoluteFilePath();
157  QFile file(absolutePath);
158  if (file.open(QFile::QIODevice::ReadOnly | QIODevice::Text))
159  {
160  if (needsWatcher) { m_fileWatcher.addPath(absolutePath); }
161  QTextStream in(&file);
162  const QString c = removeComments(in.readAll(), true, true);
163  const QString f = fileInfo.fileName().toLower();
164 
165  // save files for debugging
166  if (CBuildConfig::isLocalDeveloperDebugBuild())
167  {
168  const QString fn = CFileUtils::appendFilePaths(CSwiftDirectories::logDirectory(), f);
169  CFileUtils::writeStringToFile(c, fn);
170  }
171 
172  // keep even empty files as placeholders
173  newStyleSheets.insert(f, c); // set an empty string here to disable all stylesheet
174  }
175  file.close();
176  }
177 
178  // ignore redundant re-reads
179  if (newStyleSheets != m_styleSheets)
180  {
181  m_styleSheets = newStyleSheets;
182  emit this->styleSheetsChanged();
183  }
184  return true;
185  }
186 
187  QString CStyleSheetUtility::style(const QString &fileName) const
188  {
189  if (!this->containsStyle(fileName)) { return QString(); }
190  return m_styleSheets[fileName.toLower()].trimmed();
191  }
192 
193  QString CStyleSheetUtility::styles(const QStringList &fileNames) const
194  {
195  const bool hasModifiedFont = this->containsStyle(fileNameFontsModified());
196  bool fontAdded = false;
197 
198  QString style;
199  for (const QString &fileName : fileNames)
200  {
201  const QString key = fileName.toLower().trimmed();
202  if (!this->containsStyle(key)) { continue; }
203 
204  QString s;
205  if (fileName == fileNameFonts() || fileName == fileNameFontsModified())
206  {
207  if (fontAdded) { continue; }
208  fontAdded = true;
209  s = hasModifiedFont ? m_styleSheets[fileNameFontsModified().toLower()] : m_styleSheets[fileNameFonts()];
210  }
211  else { s = m_styleSheets[key]; }
212  if (s.isEmpty()) { continue; }
213 
214  style += (style.isEmpty() ? QString() : "\n\n") % u"/** file: " % fileName % " **/\n" % s;
215  }
216  return style;
217  }
218 
219  bool CStyleSheetUtility::containsStyle(const QString &fileName) const
220  {
221  if (fileName.isEmpty()) return false;
222  return m_styleSheets.contains(fileName.toLower().trimmed());
223  }
224 
225  bool CStyleSheetUtility::updateFont(const QFont &font)
226  {
227  QString fs;
228  if (font.pixelSize() >= 0) { fs.append(QString::number(font.pixelSize())).append("px"); }
229  else { fs.append(QString::number(font.pointSizeF())).append("pt"); }
230  return updateFont(font.family(), fs, fontStyleAsString(font), fontWeightAsString(font), "white");
231  }
232 
233  bool CStyleSheetUtility::updateFont(const QString &fontFamily, const QString &fontSize, const QString &fontStyle,
234  const QString &fontWeight, const QString &fontColor)
235  {
236  const QString qss = CStyleSheetUtility::asStylesheet(fontFamily, fontSize, fontStyle, fontWeight, fontColor);
237  return CStyleSheetUtility::updateFont(qss);
238  }
239 
240  bool CStyleSheetUtility::updateFont(const QString &qss)
241  {
242  const QString qssWidget(u"QWidget {\n" % qss % u"}\n");
243  const QString fn =
244  CFileUtils::appendFilePaths(CSwiftDirectories::stylesheetsDirectory(), fileNameFontsModified());
245  QFile fontFile(fn);
246  bool ok = fontFile.open(QFile::Text | QFile::WriteOnly);
247  if (ok)
248  {
249  QTextStream out(&fontFile);
250  out << qssWidget;
251  fontFile.close();
252  ok = this->read();
253  }
254  else
255  {
256  CLogMessage(static_cast<CStyleSheetUtility *>(nullptr)).warning(u"Cannot open file '%1' for writing") << fn;
257  }
258  return ok;
259  }
260 
262  {
263  QFile fontFile(CFileUtils::appendFilePaths(CSwiftDirectories::stylesheetsDirectory(), fileNameFontsModified()));
264  return fontFile.remove();
265  }
266 
267  QString CStyleSheetUtility::fontStyle(const QString &combinedStyleAndWeight)
268  {
269  static const QString n("normal");
270  const QString c = combinedStyleAndWeight.toLower();
271  for (const QString &s : fontStyles())
272  {
273  if (c.contains(s)) { return s; }
274  }
275  return n;
276  }
277 
278  QString CStyleSheetUtility::fontWeight(const QString &combinedStyleAndWeight)
279  {
280  static const QString n("normal");
281  const QString c = combinedStyleAndWeight.toLower();
282  for (const QString &w : fontWeights())
283  {
284  if (c.contains(w)) { return w; }
285  }
286  return n;
287  }
288 
290  {
291  static const QString f(getQssFileName("fonts"));
292  return f;
293  }
294 
296  {
297  static const QString f("fonts.modified.qss");
298  return f;
299  }
300 
302  {
303  const QString fn =
304  CFileUtils::appendFilePaths(CSwiftDirectories::stylesheetsDirectory(), fileNameFontsModified());
305  QFile file(fn);
306  if (!file.exists()) { return false; }
307  bool r = file.remove();
308  if (!r) { return false; }
309  this->read();
310  return true;
311  }
312 
314  {
315  static const QString f(getQssFileName("swiftstdgui"));
316  return f;
317  }
318 
320  {
321  static const QString fn = CFileUtils::appendFilePaths(CSwiftDirectories::stylesheetsDirectory(),
323  return fn;
324  }
325 
327  {
328  static const QString f(getQssFileName("infobar"));
329  return f;
330  }
331 
333  {
334  static const QString f(getQssFileName("navigator"));
335  return f;
336  }
337 
339  {
340  static const QString f(getQssFileName("dockwidgettab"));
341  return f;
342  }
343 
345  {
346  static const QString f(getQssFileName("stdwidget"));
347  return f;
348  }
349 
351  {
352  static const QString fn = CFileUtils::appendFilePaths(CSwiftDirectories::stylesheetsDirectory(),
354  return fn;
355  }
356 
358  {
359  static const QString f("textmessage.css");
360  return f;
361  }
362 
364  {
365  static const QString f(getQssFileName("filterdialog"));
366  return f;
367  }
368 
370  {
371  static const QString f(getQssFileName("swiftcore"));
372  return f;
373  }
374 
376  {
377  static const QString f(getQssFileName("swiftdata"));
378  return f;
379  }
380 
382  {
383  static const QString f(getQssFileName("swiftlauncher"));
384  return f;
385  }
386 
387  const QStringList &CStyleSheetUtility::fontWeights()
388  {
389  static const QStringList w({ "bold", "semibold", "light", "black", "normal" });
390  return w;
391  }
392 
393  const QStringList &CStyleSheetUtility::fontStyles()
394  {
395  static const QStringList s({ "italic", "oblique", "normal" });
396  return s;
397  }
398 
400  {
401  static const QString t = "background-color: transparent;";
402  return t;
403  }
404 
405  bool CStyleSheetUtility::useStyleSheetInDerivedWidget(QWidget *usedWidget, QStyle::PrimitiveElement element)
406  {
407  Q_ASSERT(usedWidget);
408  if (!usedWidget) { return false; }
409 
410  Q_ASSERT(usedWidget->style());
411  QStyle *style = usedWidget->style();
412  if (!style) { return false; }
413 
414  // 1) QStylePainter: modern version of
415  // usedWidget->style()->drawPrimitive(element, &opt, &p, usedWidget);
416  // 2) With viewport based widgets viewport has to be used
417  // see http://stackoverflow.com/questions/37952348/enable-own-widget-for-stylesheet
418  QAbstractScrollArea *sa = qobject_cast<QAbstractScrollArea *>(usedWidget);
419  QStylePainter p(sa ? sa->viewport() : usedWidget);
420  if (!p.isActive()) { return false; }
421 
422  QStyleOption opt;
423  opt.initFrom(usedWidget);
424  p.drawPrimitive(element, opt);
425  return true;
426  }
427 
428  QString CStyleSheetUtility::styleForIconCheckBox(const QString &checkedIcon, const QString &uncheckedIcon,
429  const QString &width, const QString &height)
430  {
431  Q_ASSERT(!checkedIcon.isEmpty());
432  Q_ASSERT(!uncheckedIcon.isEmpty());
433 
434  static const QString st = "QCheckBox::indicator { width: %1; height: %2; } QCheckBox::indicator:checked { "
435  "image: url(%3); } QCheckBox::indicator:unchecked { image: url(%4); }";
436  return st.arg(width, height, checkedIcon, uncheckedIcon);
437  }
438 
439  QString CStyleSheetUtility::concatStyles(const QString &style1, const QString &style2)
440  {
441  QString s1(style1.trimmed());
442  QString s2(style2.trimmed());
443  if (s1.isEmpty()) { return s2; }
444  if (s2.isEmpty()) { return s1; }
445  if (!s1.endsWith(";")) { s1 = s1.append("; "); }
446  s1.append(s2);
447  if (!s1.endsWith(";")) { s1 = s1.append(";"); }
448  return s1;
449  }
450 
451  void CStyleSheetUtility::setQSysInfoProperties(QWidget *widget, bool withChildWidgets)
452  {
453  Q_ASSERT_X(widget, Q_FUNC_INFO, "Missing widget");
454  if (!widget->property("qsysKernelType").isValid())
455  {
456  widget->setProperty("qsysKernelType", QSysInfo::kernelType());
457  widget->setProperty("qsysCurrentCpuArchitecture", QSysInfo::currentCpuArchitecture());
458  widget->setProperty("qsysBuildCpuArchitecture", QSysInfo::buildCpuArchitecture());
459  widget->setProperty("qsysProductType", QSysInfo::productType());
460  }
461 
462  if (withChildWidgets)
463  {
464  for (QWidget *w : widget->findChildren<QWidget *>(QString(), Qt::FindDirectChildrenOnly))
465  {
467  }
468  }
469  }
470 
471  void CStyleSheetUtility::qssDirectoryChanged(const QString &file)
472  {
473  Q_UNUSED(file);
474  this->read();
475  }
476 
477  QString CStyleSheetUtility::getQssFileName(const QString &fileName)
478  {
479  static const QString qss(".qss");
480  QString fn(fileName);
481  if (fn.endsWith(qss)) { fn.chop(qss.length()); }
482 
483  QString specific;
484  if (CBuildConfig::isRunningOnWindowsNtPlatform()) { specific = fn % u".win" % qss; }
485  else if (CBuildConfig::isRunningOnMacOSPlatform()) { specific = fn % u".mac" % qss; }
486  else if (CBuildConfig::isRunningOnLinuxPlatform()) { specific = fn % u".linux" % qss; }
487  return qssFileExists(specific) ? specific : fn + qss;
488  }
489 
490  bool CStyleSheetUtility::qssFileExists(const QString &filename)
491  {
492  if (filename.isEmpty()) { return false; }
493  const QFileInfo f(CFileUtils::appendFilePaths(CSwiftDirectories::stylesheetsDirectory(), filename));
494  return f.exists() && f.isReadable();
495  }
496 } // namespace swift::gui
Reads and provides style sheets.
bool containsStyle(const QString &fileName) const
Contains style for name.
static const QString & fileNameDockWidgetTab()
File name dockwidgettab.qss.
static const QString & fontWeightAsString(const QFont &font)
Font weight as string.
static QString asStylesheet(const QString &fontFamily, const QString &fontSize, const QString &fontStyle, const QString &fontWeight, const QString &fontColorString={})
Parameters as stylesheet.
static bool useStyleSheetInDerivedWidget(QWidget *derivedWidget, QStyle::PrimitiveElement element=QStyle::PE_Widget)
Use style sheets in derived widgets.
static const QString & fileNameSwiftData()
File name swiftcore.qss.
static const QString & fileNameSwiftStandardGui()
File name swift standard GUI.
static const QStringList & fontStyles()
Font styles.
static const QString & fontStyleAsString(const QFont &font)
Font style as string.
static const QString & fileNameSwiftCore()
File name swiftcore.qss.
static const QString & fileNameInfoBar()
File name infobar.qss.
static const QString & fileNameStandardWidget()
File name for standard widgets.
static const QString & fileNameTextMessage()
File name textmessage.qss.
static const QString & transparentBackgroundColor()
Transparent background color.
static QString fontStyle(const QString &combinedStyleAndWeight)
Get the font style.
static const QStringList & fontWeights()
Font weights.
static const QString & fileNameSwiftLauncher()
File name swiftlauncher.qss.
static QString styleForIconCheckBox(const QString &checkedIcon, const QString &uncheckedIcon, const QString &width="16px", const QString &height="16px")
Stylesheet string for a checkbox displayed as 2 icons.
bool updateFont(const QFont &font)
Update the fonts.
static const QStringList & getLogCategories()
Log cats.
static QString fontAsCombinedWeightStyle(const QFont &font)
Font as combined weight and style.
static QString concatStyles(const QString &style1, const QString &style2)
Concatenate 2 styles.
static const QString & fileNameFilterDialog()
File name maininfoarea.qss.
static void setQSysInfoProperties(QWidget *widget, bool withChildWidgets)
Set QSysInfo properties for given widget (which can be used in stylesheet)
QString fontColorString() const
Current font color from style sheet.
bool read()
Read the *.qss files.
QString style(const QString &fileName) const
Style for given file name.
static const QString & fileNameAndPathStandardWidget()
Full file path and name for standard widgets.
void styleSheetsChanged()
Sheets have been changed.
static const QString & fileNameFontsModified()
Name for user modified file.
QString styles(const QStringList &fileNames) const
Multiple styles concatenated.
static const QString & fileNameNavigator()
File name navigator.qss.
static QString fontWeight(const QString &combinedStyleAndWeight)
Get the font weight.
static const QString & fileNameFonts()
File name fonts.qss.
static const QString & fileNameAndPathSwiftStandardGui()
Full file path and name for swift standard GUI.
bool deleteModifiedFontFile()
Delete the modified file for fonts.
Class for emitting a log message.
Definition: logmessage.h:27
Derived & warning(const char16_t(&format)[N])
Set the severity to warning, providing a format string.
GUI related classes.
Free functions in swift::misc.
SWIFT_MISC_EXPORT QString removeComments(const QString &in, bool removeSlash, bool removeDoubleSlash)
Remove comments such as /‍** **‍/ or //.