mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
* Reorder class data members and/or constructor initialisers to match, reducing confusion about when members will be initialised. * Remove (most) unused variables. Not removed: some global variables with `TODO(sudden6)` on them for using them in the future. I don't know how far into the future sudden6 wants to use them, so I left them there for now. * Distinguish different bootstrap nodes in the logs by index in the bootstrap node list. Originally, we used to log the address/port of the node we're bootstrapping to. This was removed out of privacy concerns (even though the bootstrap nodes are public). This made the logs much less useful when debugging why the client isn't connecting. Having indices makes it easier to see that different nodes are being selected, and makes it possible to determine which node was selected. * Explicitly cast unused results of Tox API functions to `void` when all we want is to know whether the function succeeds or not. * Don't try to `#include <unistd.h>` on Windows. It does not exist on MSVC. * Remove extra `;` after function definitions. * Remove reference indirection of QJsonValueRef, since a copy of that ref (small pointer-like object) has to be made anyway when iterating over QJsonArrays. * Make some file-scope global state `static`. * Use `nullptr` instead of `NULL`. * Add `#if DESKTOP_NOTIFICATIONS` around the code that implements desktop notifications, so it becomes a bit easier to compile everything with a single compiler command - useful for manually running static analysers. * Fix an error on MSVC where `disconnect` is looked up to be a non-static member function and the `this` capture is missing. * Consistently use `struct` and `class` tags for types. * Use references in ranged-for where it reduces copies. * Move private static data members out of the Style class and into file-local scope. There is no need for them to be in the class. Also marked them `const` where possible. * Removed unused lambda capture. * Ensure qTox can compile under NDEBUG with `-Wunused-variable` by inlining the unused variable into the `assert` that was its only target. * Minor reformatting in core_test.cpp.
435 lines
13 KiB
C++
435 lines
13 KiB
C++
/*
|
|
Copyright © 2014-2019 by The qTox Project Contributors
|
|
|
|
This file is part of qTox, a Qt-based graphical interface for Tox.
|
|
|
|
qTox is libre software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
qTox is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "style.h"
|
|
#include "src/persistence/settings.h"
|
|
#include "src/widget/gui.h"
|
|
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFontInfo>
|
|
#include <QMap>
|
|
#include <QPainter>
|
|
#include <QRegularExpression>
|
|
#include <QSettings>
|
|
#include <QStandardPaths>
|
|
#include <QStringBuilder>
|
|
#include <QStyle>
|
|
#include <QSvgRenderer>
|
|
#include <QWidget>
|
|
|
|
/**
|
|
* @enum Style::Font
|
|
*
|
|
* @var ExtraBig
|
|
* @brief [SystemDefault + 2]px, bold
|
|
*
|
|
* @var Big
|
|
* @brief [SystemDefault]px
|
|
*
|
|
* @var BigBold
|
|
* @brief [SystemDefault]px, bold
|
|
*
|
|
* @var Medium
|
|
* @brief [SystemDefault - 1]px
|
|
*
|
|
* @var MediumBold
|
|
* @brief [SystemDefault - 1]px, bold
|
|
*
|
|
* @var Small
|
|
* @brief [SystemDefault - 2]px
|
|
*
|
|
* @var SmallLight
|
|
* @brief [SystemDefault - 2]px, light
|
|
*
|
|
* @var BuiltinThemePath
|
|
* @brief Path to the theme built into the application binary
|
|
*/
|
|
|
|
namespace {
|
|
const QLatin1String ThemeSubFolder{"themes/"};
|
|
const QLatin1String BuiltinThemeDefaultPath{":themes/default/"};
|
|
const QLatin1String BuiltinThemeDarkPath{":themes/dark/"};
|
|
}
|
|
|
|
// helper functions
|
|
QFont appFont(int pixelSize, int weight)
|
|
{
|
|
QFont font;
|
|
font.setPixelSize(pixelSize);
|
|
font.setWeight(weight);
|
|
return font;
|
|
}
|
|
|
|
QString qssifyFont(QFont font)
|
|
{
|
|
return QString("%1 %2px \"%3\"").arg(font.weight() * 8).arg(font.pixelSize()).arg(font.family());
|
|
}
|
|
|
|
static QMap<Style::ColorPalette, QColor> palette;
|
|
|
|
static QMap<QString, QString> dictColor;
|
|
static QMap<QString, QString> dictFont;
|
|
static QMap<QString, QString> dictTheme;
|
|
|
|
static const QList<Style::ThemeNameColor> themeNameColors = {
|
|
{Style::Light, QObject::tr("Default"), QColor()},
|
|
{Style::Light, QObject::tr("Blue"), QColor("#004aa4")},
|
|
{Style::Light, QObject::tr("Olive"), QColor("#97ba00")},
|
|
{Style::Light, QObject::tr("Red"), QColor("#c23716")},
|
|
{Style::Light, QObject::tr("Violet"), QColor("#4617b5")},
|
|
{Style::Dark, QObject::tr("Dark"), QColor()},
|
|
{Style::Dark, QObject::tr("Dark blue"), QColor("#00336d")},
|
|
{Style::Dark, QObject::tr("Dark olive"), QColor("#4d5f00")},
|
|
{Style::Dark, QObject::tr("Dark red"), QColor("#7a210d")},
|
|
{Style::Dark, QObject::tr("Dark violet"), QColor("#280d6c")}
|
|
};
|
|
|
|
QStringList Style::getThemeColorNames()
|
|
{
|
|
QStringList l;
|
|
|
|
for (auto t : themeNameColors) {
|
|
l << t.name;
|
|
}
|
|
|
|
return l;
|
|
}
|
|
|
|
QString Style::getThemeName()
|
|
{
|
|
//TODO: return name of the current theme
|
|
const QString themeName = "default";
|
|
return QStringLiteral("default");
|
|
}
|
|
|
|
QString Style::getThemeFolder()
|
|
{
|
|
const QString themeName = getThemeName();
|
|
const QString themeFolder = ThemeSubFolder % themeName;
|
|
const QString fullPath = QStandardPaths::locate(QStandardPaths::AppDataLocation,
|
|
themeFolder, QStandardPaths::LocateDirectory);
|
|
|
|
// No themes available, fallback to builtin
|
|
if(fullPath.isEmpty()) {
|
|
return getThemePath();
|
|
}
|
|
|
|
return fullPath % QDir::separator();
|
|
}
|
|
|
|
|
|
static const QMap<Style::ColorPalette, QString> aliasColors = {
|
|
{Style::TransferGood, "transferGood"},
|
|
{Style::TransferWait, "transferWait"},
|
|
{Style::TransferBad, "transferBad"},
|
|
{Style::TransferMiddle, "transferMiddle"},
|
|
{Style::MainText,"mainText"},
|
|
{Style::NameActive, "nameActive"},
|
|
{Style::StatusActive,"statusActive"},
|
|
{Style::GroundExtra, "groundExtra"},
|
|
{Style::GroundBase, "groundBase"},
|
|
{Style::Orange, "orange"},
|
|
{Style::ThemeDark, "themeDark"},
|
|
{Style::ThemeMediumDark, "themeMediumDark"},
|
|
{Style::ThemeMedium, "themeMedium"},
|
|
{Style::ThemeLight, "themeLight"},
|
|
{Style::Action, "action"},
|
|
{Style::Link, "link"},
|
|
{Style::SearchHighlighted, "searchHighlighted"},
|
|
{Style::SelectText, "selectText"},
|
|
};
|
|
|
|
// stylesheet filename, font -> stylesheet
|
|
// QString implicit sharing deduplicates stylesheets rather than constructing a new one each time
|
|
static std::map<std::pair<const QString, const QFont>, const QString> stylesheetsCache;
|
|
|
|
const QString Style::getStylesheet(const QString& filename, const QFont& baseFont)
|
|
{
|
|
const QString fullPath = getThemeFolder() + filename;
|
|
const std::pair<const QString, const QFont> cacheKey(fullPath, baseFont);
|
|
auto it = stylesheetsCache.find(cacheKey);
|
|
if (it != stylesheetsCache.end())
|
|
{
|
|
// cache hit
|
|
return it->second;
|
|
}
|
|
// cache miss, new styleSheet, read it from file and add to cache
|
|
const QString newStylesheet = resolve(filename, baseFont);
|
|
stylesheetsCache.insert(std::make_pair(cacheKey, newStylesheet));
|
|
return newStylesheet;
|
|
}
|
|
|
|
static QStringList existingImagesCache;
|
|
const QString Style::getImagePath(const QString& filename)
|
|
{
|
|
QString fullPath = getThemeFolder() + filename;
|
|
|
|
// search for image in cache
|
|
if (existingImagesCache.contains(fullPath)) {
|
|
return fullPath;
|
|
}
|
|
|
|
// if not in cache
|
|
if (QFileInfo::exists(fullPath)) {
|
|
existingImagesCache << fullPath;
|
|
return fullPath;
|
|
} else {
|
|
qWarning() << "Failed to open file (using defaults):" << fullPath;
|
|
|
|
fullPath = getThemePath() % filename;
|
|
|
|
if (QFileInfo::exists(fullPath)) {
|
|
return fullPath;
|
|
} else {
|
|
qWarning() << "Failed to open default file:" << fullPath;
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
|
|
QColor Style::getColor(Style::ColorPalette entry)
|
|
{
|
|
return palette[entry];
|
|
}
|
|
|
|
QFont Style::getFont(Style::Font font)
|
|
{
|
|
// fonts as defined in
|
|
// https://github.com/ItsDuke/Tox-UI/blob/master/UI%20GUIDELINES.md
|
|
|
|
static int defSize = QFontInfo(QFont()).pixelSize();
|
|
|
|
static QFont fonts[] = {
|
|
appFont(defSize + 3, QFont::Bold), // extra big
|
|
appFont(defSize + 1, QFont::Normal), // big
|
|
appFont(defSize + 1, QFont::Bold), // big bold
|
|
appFont(defSize, QFont::Normal), // medium
|
|
appFont(defSize, QFont::Bold), // medium bold
|
|
appFont(defSize - 1, QFont::Normal), // small
|
|
appFont(defSize - 1, QFont::Light), // small light
|
|
};
|
|
|
|
return fonts[font];
|
|
}
|
|
|
|
const QString Style::resolve(const QString& filename, const QFont& baseFont)
|
|
{
|
|
QString themePath = getThemeFolder();
|
|
QString fullPath = themePath + filename;
|
|
QString qss;
|
|
|
|
QFile file{fullPath};
|
|
if (file.open(QFile::ReadOnly | QFile::Text)) {
|
|
qss = file.readAll();
|
|
} else {
|
|
qWarning() << "Failed to open file (using defaults):" << fullPath;
|
|
|
|
fullPath = getThemePath();
|
|
QFile file{fullPath};
|
|
|
|
if (file.open(QFile::ReadOnly | QFile::Text)) {
|
|
qss = file.readAll();
|
|
} else {
|
|
qWarning() << "Failed to open default file:" << fullPath;
|
|
return {};
|
|
}
|
|
}
|
|
|
|
if (palette.isEmpty()) {
|
|
initPalette();
|
|
}
|
|
|
|
if (dictColor.isEmpty()) {
|
|
initDictColor();
|
|
}
|
|
|
|
if (dictFont.isEmpty()) {
|
|
dictFont = {
|
|
{"@baseFont",
|
|
QString::fromUtf8("'%1' %2px").arg(baseFont.family()).arg(QFontInfo(baseFont).pixelSize())},
|
|
{"@extraBig", qssifyFont(Style::getFont(Style::ExtraBig))},
|
|
{"@big", qssifyFont(Style::getFont(Style::Big))},
|
|
{"@bigBold", qssifyFont(Style::getFont(Style::BigBold))},
|
|
{"@medium", qssifyFont(Style::getFont(Style::Medium))},
|
|
{"@mediumBold", qssifyFont(Style::getFont(Style::MediumBold))},
|
|
{"@small", qssifyFont(Style::getFont(Style::Small))},
|
|
{"@smallLight", qssifyFont(Style::getFont(Style::SmallLight))}};
|
|
}
|
|
|
|
for (const QString& key : dictColor.keys()) {
|
|
qss.replace(QRegularExpression(key % QLatin1String{"\\b"}), dictColor[key]);
|
|
}
|
|
|
|
for (const QString& key : dictFont.keys()) {
|
|
qss.replace(QRegularExpression(key % QLatin1String{"\\b"}), dictFont[key]);
|
|
}
|
|
|
|
for (const QString& key : dictTheme.keys()) {
|
|
qss.replace(QRegularExpression(key % QLatin1String{"\\b"}), dictTheme[key]);
|
|
}
|
|
|
|
// @getImagePath() function
|
|
const QRegularExpression re{QStringLiteral(R"(@getImagePath\([^)\s]*\))")};
|
|
QRegularExpressionMatchIterator i = re.globalMatch(qss);
|
|
|
|
while (i.hasNext()) {
|
|
QRegularExpressionMatch match = i.next();
|
|
QString path = match.captured(0);
|
|
const QString phrase = path;
|
|
|
|
path.remove(QStringLiteral("@getImagePath("));
|
|
path.chop(1);
|
|
|
|
QString fullImagePath = getThemeFolder() + path;
|
|
// image not in cache
|
|
if (!existingImagesCache.contains(fullPath)) {
|
|
if (QFileInfo::exists(fullImagePath)) {
|
|
existingImagesCache << fullImagePath;
|
|
} else {
|
|
qWarning() << "Failed to open file (using defaults):" << fullImagePath;
|
|
fullImagePath = getThemePath() % path;
|
|
}
|
|
}
|
|
|
|
qss.replace(phrase, fullImagePath);
|
|
}
|
|
|
|
return qss;
|
|
}
|
|
|
|
void Style::repolish(QWidget* w)
|
|
{
|
|
w->style()->unpolish(w);
|
|
w->style()->polish(w);
|
|
|
|
for (QObject* o : w->children()) {
|
|
QWidget* c = qobject_cast<QWidget*>(o);
|
|
if (c) {
|
|
c->style()->unpolish(c);
|
|
c->style()->polish(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Style::setThemeColor(int color)
|
|
{
|
|
stylesheetsCache.clear(); // clear stylesheet cache which includes color info
|
|
palette.clear();
|
|
dictColor.clear();
|
|
initPalette();
|
|
initDictColor();
|
|
if (color < 0 || color >= themeNameColors.size())
|
|
setThemeColor(QColor());
|
|
else
|
|
setThemeColor(themeNameColors[color].color);
|
|
}
|
|
|
|
/**
|
|
* @brief Set theme color.
|
|
* @param color Color to set.
|
|
*
|
|
* Pass an invalid QColor to reset to defaults.
|
|
*/
|
|
void Style::setThemeColor(const QColor& color)
|
|
{
|
|
if (!color.isValid()) {
|
|
// Reset to default
|
|
palette[ThemeDark] = getColor(ThemeDark);
|
|
palette[ThemeMediumDark] = getColor(ThemeMediumDark);
|
|
palette[ThemeMedium] = getColor(ThemeMedium);
|
|
palette[ThemeLight] = getColor(ThemeLight);
|
|
} else {
|
|
palette[ThemeDark] = color.darker(155);
|
|
palette[ThemeMediumDark] = color.darker(135);
|
|
palette[ThemeMedium] = color.darker(120);
|
|
palette[ThemeLight] = color.lighter(110);
|
|
}
|
|
|
|
dictTheme["@themeDark"] = getColor(ThemeDark).name();
|
|
dictTheme["@themeMediumDark"] = getColor(ThemeMediumDark).name();
|
|
dictTheme["@themeMedium"] = getColor(ThemeMedium).name();
|
|
dictTheme["@themeLight"] = getColor(ThemeLight).name();
|
|
}
|
|
|
|
/**
|
|
* @brief Reloads some CCS
|
|
*/
|
|
void Style::applyTheme()
|
|
{
|
|
GUI::reloadTheme();
|
|
}
|
|
|
|
QPixmap Style::scaleSvgImage(const QString& path, uint32_t width, uint32_t height)
|
|
{
|
|
QSvgRenderer render(path);
|
|
QPixmap pixmap(width, height);
|
|
pixmap.fill(QColor(0, 0, 0, 0));
|
|
QPainter painter(&pixmap);
|
|
render.render(&painter, pixmap.rect());
|
|
return pixmap;
|
|
}
|
|
|
|
void Style::initPalette()
|
|
{
|
|
QSettings settings(getThemePath() % "palette.ini", QSettings::IniFormat);
|
|
|
|
auto keys = aliasColors.keys();
|
|
|
|
settings.beginGroup("colors");
|
|
QMap<Style::ColorPalette, QString> c;
|
|
for (auto k : keys) {
|
|
c[k] = settings.value(aliasColors[k], "#000").toString();
|
|
palette[k] = QColor(settings.value(aliasColors[k], "#000").toString());
|
|
}
|
|
auto p = palette;
|
|
settings.endGroup();
|
|
|
|
}
|
|
|
|
void Style::initDictColor()
|
|
{
|
|
dictColor = {
|
|
{"@transferGood", Style::getColor(Style::TransferGood).name()},
|
|
{"@transferWait", Style::getColor(Style::TransferWait).name()},
|
|
{"@transferBad", Style::getColor(Style::TransferBad).name()},
|
|
{"@transferMiddle", Style::getColor(Style::TransferMiddle).name()},
|
|
{"@mainText", Style::getColor(Style::MainText).name()},
|
|
{"@nameActive", Style::getColor(Style::NameActive).name()},
|
|
{"@statusActive", Style::getColor(Style::StatusActive).name()},
|
|
{"@groundExtra", Style::getColor(Style::GroundExtra).name()},
|
|
{"@groundBase", Style::getColor(Style::GroundBase).name()},
|
|
{"@orange", Style::getColor(Style::Orange).name()},
|
|
{"@action", Style::getColor(Style::Action).name()},
|
|
{"@link", Style::getColor(Style::Link).name()},
|
|
{"@searchHighlighted", Style::getColor(Style::SearchHighlighted).name()},
|
|
{"@selectText", Style::getColor(Style::SelectText).name()}};
|
|
}
|
|
|
|
QString Style::getThemePath()
|
|
{
|
|
const int num = Settings::getInstance().getThemeColor();
|
|
if (themeNameColors[num].type == Dark) {
|
|
return BuiltinThemeDarkPath;
|
|
}
|
|
|
|
return BuiltinThemeDefaultPath;
|
|
}
|