Store Themes as JSON files (#4471)

Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix 2023-04-08 11:05:55 +02:00 committed by GitHub
parent 7a286480d6
commit 4e3433e966
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1018 additions and 138 deletions

View file

@ -26,3 +26,7 @@ jobs:
- name: Show diff
run: git --no-pager diff --exit-code --color=never
shell: bash
- name: Check Theme files
run: |
npm i ajv-cli
npx -- ajv validate -s docs/ChatterinoTheme.schema.json -d "resources/themes/*.json"

View file

@ -1,5 +1,7 @@
# JSON resources should not be prettified
# JSON resources should not be prettified...
resources/*.json
# ...themes should be prettified for readability.
!resources/themes/*.json
# Ignore submodule files
lib/*/

View file

@ -18,6 +18,7 @@
- Bugfix: Fixed search popup ignoring setting for message scrollback limit. (#4496)
- Bugfix: Fixed a memory leak that occurred when loading message history. This was mostly noticeable with unstable internet connections where reconnections were frequent or long-running instances of Chatterino. (#4499)
- Dev: Disabling precompiled headers on Windows is now tested in CI. (#4472)
- Dev: Themes are now stored as JSON files in `resources/themes`. (#4471)
- Dev: Ignore unhandled BTTV user-events. (#4438)
- Dev: Only log debug messages when NDEBUG is not defined. (#4442)
- Dev: Cleaned up theme related code. (#4450)

View file

@ -7,6 +7,7 @@ set(
resources.qrc
resources_autogenerated.qrc
windows.rc
themes/ChatterinoTheme.schema.json
)
set(RES_IMAGE_EXCLUDE_FILTER ^linuxinstall/)

View file

@ -0,0 +1,398 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Chatterino Theme",
"description": "Colors and metadata for a Chatterino 2 theme",
"definitions": {
"qt-color": {
"type": "string",
"$comment": "https://doc.qt.io/qt-5/qcolor.html#setNamedColor",
"anyOf": [
{
"title": "#RGB",
"pattern": "^#[a-fA-F0-9]{3}$"
},
{
"title": "#RRGGBB",
"pattern": "^#[a-fA-F0-9]{6}$"
},
{
"title": "#AARRGGBB",
"$comment": "Note that this isn't identical to the CSS Color Moudle Level 4 <hex-color> where the alpha value is at the end.",
"pattern": "^#[a-fA-F0-9]{8}$"
},
{
"title": "#RRRGGGBBB",
"pattern": "^#[a-fA-F0-9]{9}$"
},
{
"title": "#RRRRGGGGBBBB",
"pattern": "^#[a-fA-F0-9]{12}$"
},
{
"title": "SVG Color",
"description": "This is stricter than Qt. You could theoretically put tabs an spaces between characters in a named color and capitalize the color.",
"$comment": "https://www.w3.org/TR/SVG11/types.html#ColorKeywords",
"enum": [
"aliceblue",
"antiquewhite",
"aqua",
"aquamarine",
"azure",
"beige",
"bisque",
"black",
"blanchedalmond",
"blue",
"blueviolet",
"brown",
"burlywood",
"cadetblue",
"chartreuse",
"chocolate",
"coral",
"cornflowerblue",
"cornsilk",
"crimson",
"cyan",
"darkblue",
"darkcyan",
"darkgoldenrod",
"darkgray",
"darkgreen",
"darkgrey",
"darkkhaki",
"darkmagenta",
"darkolivegreen",
"darkorange",
"darkorchid",
"darkred",
"darksalmon",
"darkseagreen",
"darkslateblue",
"darkslategray",
"darkslategrey",
"darkturquoise",
"darkviolet",
"deeppink",
"deepskyblue",
"dimgray",
"dimgrey",
"dodgerblue",
"firebrick",
"floralwhite",
"forestgreen",
"fuchsia",
"gainsboro",
"ghostwhite",
"gold",
"goldenrod",
"gray",
"grey",
"green",
"greenyellow",
"honeydew",
"hotpink",
"indianred",
"indigo",
"ivory",
"khaki",
"lavender",
"lavenderblush",
"lawngreen",
"lemonchiffon",
"lightblue",
"lightcoral",
"lightcyan",
"lightgoldenrodyellow",
"lightgray",
"lightgreen",
"lightgrey",
"lightpink",
"lightsalmon",
"lightseagreen",
"lightskyblue",
"lightslategray",
"lightslategrey",
"lightsteelblue",
"lightyellow",
"lime",
"limegreen",
"linen",
"magenta",
"maroon",
"mediumaquamarine",
"mediumblue",
"mediumorchid",
"mediumpurple",
"mediumseagreen",
"mediumslateblue",
"mediumspringgreen",
"mediumturquoise",
"mediumvioletred",
"midnightblue",
"mintcream",
"mistyrose",
"moccasin",
"navajowhite",
"navy",
"oldlace",
"olive",
"olivedrab",
"orange",
"orangered",
"orchid",
"palegoldenrod",
"palegreen",
"paleturquoise",
"palevioletred",
"papayawhip",
"peachpuff",
"peru",
"pink",
"plum",
"powderblue",
"purple",
"red",
"rosybrown",
"royalblue",
"saddlebrown",
"salmon",
"sandybrown",
"seagreen",
"seashell",
"sienna",
"silver",
"skyblue",
"slateblue",
"slategray",
"slategrey",
"snow",
"springgreen",
"steelblue",
"tan",
"teal",
"thistle",
"tomato",
"turquoise",
"violet",
"wheat",
"white",
"whitesmoke",
"yellow",
"yellowgreen"
]
},
{
"title": "transparent",
"enum": ["transparent"]
}
]
},
"tab-colors": {
"type": "object",
"additionalProperties": false,
"properties": {
"backgrounds": {
"type": "object",
"additionalProperties": false,
"properties": {
"hover": { "$ref": "#/definitions/qt-color" },
"regular": { "$ref": "#/definitions/qt-color" },
"unfocused": { "$ref": "#/definitions/qt-color" }
},
"required": ["hover", "regular", "unfocused"]
},
"line": {
"type": "object",
"additionalProperties": false,
"properties": {
"hover": { "$ref": "#/definitions/qt-color" },
"regular": { "$ref": "#/definitions/qt-color" },
"unfocused": { "$ref": "#/definitions/qt-color" }
},
"required": ["hover", "regular", "unfocused"]
},
"text": { "$ref": "#/definitions/qt-color" }
},
"required": ["backgrounds", "line", "text"]
}
},
"type": "object",
"additionalProperties": false,
"properties": {
"colors": {
"type": "object",
"additionalProperties": false,
"properties": {
"accent": { "$ref": "#/definitions/qt-color" },
"messages": {
"type": "object",
"additionalProperties": false,
"properties": {
"backgrounds": {
"type": "object",
"additionalProperties": false,
"properties": {
"alternate": { "$ref": "#/definitions/qt-color" },
"regular": { "$ref": "#/definitions/qt-color" }
},
"required": ["alternate", "regular"]
},
"disabled": { "$ref": "#/definitions/qt-color" },
"highlightAnimationEnd": { "$ref": "#/definitions/qt-color" },
"highlightAnimationStart": { "$ref": "#/definitions/qt-color" },
"selection": { "$ref": "#/definitions/qt-color" },
"textColors": {
"type": "object",
"additionalProperties": false,
"properties": {
"caret": { "$ref": "#/definitions/qt-color" },
"chatPlaceholder": { "$ref": "#/definitions/qt-color" },
"link": { "$ref": "#/definitions/qt-color" },
"regular": { "$ref": "#/definitions/qt-color" },
"system": { "$ref": "#/definitions/qt-color" }
},
"required": [
"caret",
"chatPlaceholder",
"link",
"regular",
"system"
]
}
},
"required": [
"backgrounds",
"disabled",
"highlightAnimationEnd",
"highlightAnimationStart",
"selection",
"textColors"
]
},
"scrollbars": {
"type": "object",
"additionalProperties": false,
"properties": {
"background": { "$ref": "#/definitions/qt-color" },
"thumb": { "$ref": "#/definitions/qt-color" },
"thumbSelected": { "$ref": "#/definitions/qt-color" }
},
"required": ["background", "thumb", "thumbSelected"]
},
"splits": {
"type": "object",
"additionalProperties": false,
"properties": {
"background": { "$ref": "#/definitions/qt-color" },
"dropPreview": { "$ref": "#/definitions/qt-color" },
"dropPreviewBorder": { "$ref": "#/definitions/qt-color" },
"dropTargetRect": { "$ref": "#/definitions/qt-color" },
"dropTargetRectBorder": { "$ref": "#/definitions/qt-color" },
"header": {
"type": "object",
"additionalProperties": false,
"properties": {
"background": { "$ref": "#/definitions/qt-color" },
"border": { "$ref": "#/definitions/qt-color" },
"focusedBackground": { "$ref": "#/definitions/qt-color" },
"focusedBorder": { "$ref": "#/definitions/qt-color" },
"focusedText": { "$ref": "#/definitions/qt-color" },
"text": { "$ref": "#/definitions/qt-color" }
},
"required": [
"background",
"border",
"focusedBackground",
"focusedBorder",
"focusedText",
"text"
]
},
"input": {
"type": "object",
"additionalProperties": false,
"properties": {
"background": { "$ref": "#/definitions/qt-color" },
"text": { "$ref": "#/definitions/qt-color" }
},
"required": ["background", "text"]
},
"messageSeperator": { "$ref": "#/definitions/qt-color" },
"resizeHandle": { "$ref": "#/definitions/qt-color" },
"resizeHandleBackground": { "$ref": "#/definitions/qt-color" }
},
"required": [
"background",
"dropPreview",
"dropPreviewBorder",
"dropTargetRect",
"dropTargetRectBorder",
"header",
"input",
"messageSeperator",
"resizeHandle",
"resizeHandleBackground"
]
},
"tabs": {
"type": "object",
"additionalProperties": false,
"properties": {
"dividerLine": { "$ref": "#/definitions/qt-color" },
"highlighted": {
"$ref": "#/definitions/tab-colors"
},
"newMessage": {
"$ref": "#/definitions/tab-colors"
},
"regular": {
"$ref": "#/definitions/tab-colors"
},
"selected": {
"$ref": "#/definitions/tab-colors"
}
},
"required": [
"dividerLine",
"highlighted",
"newMessage",
"regular",
"selected"
]
},
"window": {
"type": "object",
"additionalProperties": false,
"properties": {
"background": { "$ref": "#/definitions/qt-color" },
"text": { "$ref": "#/definitions/qt-color" }
},
"required": ["background", "text"]
}
},
"required": [
"accent",
"messages",
"scrollbars",
"splits",
"tabs",
"window"
]
},
"metadata": {
"type": "object",
"additionalProperties": false,
"properties": {
"iconTheme": {
"$comment": "Determines which icons to use. 'dark' will use dark icons (best for a light theme). 'light' will use light icons.",
"enum": ["light", "dark"],
"default": "light"
}
},
"required": ["iconTheme"]
},
"$schema": { "type": "string" }
},
"required": ["colors", "metadata"]
}

112
resources/themes/Black.json Normal file
View file

@ -0,0 +1,112 @@
{
"$schema": "../../docs/ChatterinoTheme.schema.json",
"metadata": {
"iconTheme": "light"
},
"colors": {
"accent": "#00aeef",
"messages": {
"backgrounds": {
"alternate": "#0a0a0a",
"regular": "#000000"
},
"disabled": "#99000000",
"highlightAnimationEnd": "#00e6e6e6",
"highlightAnimationStart": "#6ee6e6e6",
"selection": "#40ffffff",
"textColors": {
"caret": "#ffffff",
"chatPlaceholder": "#5d5555",
"link": "#4286f4",
"regular": "#ffffff",
"system": "#8c7f7f"
}
},
"scrollbars": {
"background": "#00000000",
"thumb": "#4d4d4d",
"thumbSelected": "#595959"
},
"splits": {
"background": "#000000",
"dropPreview": "#300094ff",
"dropPreviewBorder": "#0094ff",
"dropTargetRect": "#000094ff",
"dropTargetRectBorder": "#000094ff",
"header": {
"background": "#191919",
"border": "#262626",
"focusedBackground": "#363636",
"focusedBorder": "#383838",
"focusedText": "#84c1ff",
"text": "#ffffff"
},
"input": {
"background": "#0d0d0d",
"text": "#ffffff"
},
"messageSeperator": "#3c3c3c",
"resizeHandle": "#700094ff",
"resizeHandleBackground": "#200094ff"
},
"tabs": {
"dividerLine": "#555555",
"highlighted": {
"backgrounds": {
"hover": "#252525",
"regular": "#252525",
"unfocused": "#252525"
},
"line": {
"hover": "#ee6166",
"regular": "#ee6166",
"unfocused": "#ee6166"
},
"text": "#eeeeee"
},
"newMessage": {
"backgrounds": {
"hover": "#252525",
"regular": "#252525",
"unfocused": "#252525"
},
"line": {
"hover": "#888888",
"regular": "#888888",
"unfocused": "#888888"
},
"text": "#eeeeee"
},
"regular": {
"backgrounds": {
"hover": "#252525",
"regular": "#252525",
"unfocused": "#252525"
},
"line": {
"hover": "#444444",
"regular": "#444444",
"unfocused": "#444444"
},
"text": "#aaaaaa"
},
"selected": {
"backgrounds": {
"hover": "#555555",
"regular": "#555555",
"unfocused": "#555555"
},
"line": {
"hover": "#00aeef",
"regular": "#00aeef",
"unfocused": "#00aeef"
},
"text": "#ffffff"
}
},
"window": {
"background": "#111111",
"text": "#eeeeee"
}
}
}

112
resources/themes/Dark.json Normal file
View file

@ -0,0 +1,112 @@
{
"$schema": "../../docs/ChatterinoTheme.schema.json",
"metadata": {
"iconTheme": "light"
},
"colors": {
"accent": "#00aeef",
"messages": {
"backgrounds": {
"alternate": "#222222",
"regular": "#191919"
},
"disabled": "#99191919",
"highlightAnimationEnd": "#00e6e6e6",
"highlightAnimationStart": "#6ee6e6e6",
"selection": "#40ffffff",
"textColors": {
"caret": "#ffffff",
"chatPlaceholder": "#5d5555",
"link": "#4286f4",
"regular": "#ffffff",
"system": "#8c7f7f"
}
},
"scrollbars": {
"background": "#00000000",
"thumb": "#575757",
"thumbSelected": "#616161"
},
"splits": {
"background": "#191919",
"dropPreview": "#300094ff",
"dropPreviewBorder": "#0094ff",
"dropTargetRect": "#000094ff",
"dropTargetRectBorder": "#000094ff",
"header": {
"background": "#2e2e2e",
"border": "#383838",
"focusedBackground": "#444444",
"focusedBorder": "#464646",
"focusedText": "#84c1ff",
"text": "#ffffff"
},
"input": {
"background": "#242424",
"text": "#ffffff"
},
"messageSeperator": "#3c3c3c",
"resizeHandle": "#700094ff",
"resizeHandleBackground": "#200094ff"
},
"tabs": {
"dividerLine": "#555555",
"highlighted": {
"backgrounds": {
"hover": "#252525",
"regular": "#252525",
"unfocused": "#252525"
},
"line": {
"hover": "#ee6166",
"regular": "#ee6166",
"unfocused": "#ee6166"
},
"text": "#eeeeee"
},
"newMessage": {
"backgrounds": {
"hover": "#252525",
"regular": "#252525",
"unfocused": "#252525"
},
"line": {
"hover": "#888888",
"regular": "#888888",
"unfocused": "#888888"
},
"text": "#eeeeee"
},
"regular": {
"backgrounds": {
"hover": "#252525",
"regular": "#252525",
"unfocused": "#252525"
},
"line": {
"hover": "#444444",
"regular": "#444444",
"unfocused": "#444444"
},
"text": "#aaaaaa"
},
"selected": {
"backgrounds": {
"hover": "#555555",
"regular": "#555555",
"unfocused": "#555555"
},
"line": {
"hover": "#00aeef",
"regular": "#00aeef",
"unfocused": "#00aeef"
},
"text": "#ffffff"
}
},
"window": {
"background": "#111111",
"text": "#eeeeee"
}
}
}

112
resources/themes/Light.json Normal file
View file

@ -0,0 +1,112 @@
{
"$schema": "../../docs/ChatterinoTheme.schema.json",
"metadata": {
"iconTheme": "dark"
},
"colors": {
"accent": "#00aeef",
"messages": {
"backgrounds": {
"alternate": "#dddddd",
"regular": "#e6e6e6"
},
"disabled": "#99e6e6e6",
"highlightAnimationEnd": "#00141414",
"highlightAnimationStart": "#6e141414",
"selection": "#40000000",
"textColors": {
"caret": "#000000",
"chatPlaceholder": "#af9f9f",
"link": "#4286f4",
"regular": "#000000",
"system": "#8c7f7f"
}
},
"scrollbars": {
"background": "#00000000",
"thumb": "#a8a8a8",
"thumbSelected": "#9e9e9e"
},
"splits": {
"background": "#e6e6e6",
"dropPreview": "#300094ff",
"dropPreviewBorder": "#0094ff",
"dropTargetRect": "#00ffffff",
"dropTargetRectBorder": "#000094ff",
"header": {
"background": "#e6e6e6",
"border": "#e6e6e6",
"focusedBackground": "#dbdbdb",
"focusedBorder": "#d1d1d1",
"focusedText": "#0051a3",
"text": "#000000"
},
"input": {
"background": "#dbdbdb",
"text": "#000000"
},
"messageSeperator": "#7f7f7f",
"resizeHandle": "#0094ff",
"resizeHandleBackground": "#500094ff"
},
"tabs": {
"dividerLine": "#b4d7ff",
"highlighted": {
"backgrounds": {
"hover": "#eeeeee",
"regular": "#ffffff",
"unfocused": "#ffffff"
},
"line": {
"hover": "#ff0000",
"regular": "#ff0000",
"unfocused": "#ff0000"
},
"text": "#000000"
},
"newMessage": {
"backgrounds": {
"hover": "#eeeeee",
"regular": "#ffffff",
"unfocused": "#ffffff"
},
"line": {
"hover": "#bbbbbb",
"regular": "#bbbbbb",
"unfocused": "#bbbbbb"
},
"text": "#222222"
},
"regular": {
"backgrounds": {
"hover": "#eeeeee",
"regular": "#ffffff",
"unfocused": "#ffffff"
},
"line": {
"hover": "#ffffff",
"regular": "#ffffff",
"unfocused": "#ffffff"
},
"text": "#444444"
},
"selected": {
"backgrounds": {
"hover": "#b4d7ff",
"regular": "#b4d7ff",
"unfocused": "#b4d7ff"
},
"line": {
"hover": "#00aeef",
"regular": "#00aeef",
"unfocused": "#00aeef"
},
"text": "#000000"
}
},
"window": {
"background": "#ffffff",
"text": "#000000"
}
}
}

112
resources/themes/White.json Normal file
View file

@ -0,0 +1,112 @@
{
"$schema": "../../docs/ChatterinoTheme.schema.json",
"metadata": {
"iconTheme": "dark"
},
"colors": {
"accent": "#00aeef",
"messages": {
"backgrounds": {
"alternate": "#f5f5f5",
"regular": "#ffffff"
},
"disabled": "#99ffffff",
"highlightAnimationEnd": "#00141414",
"highlightAnimationStart": "#6e141414",
"selection": "#40000000",
"textColors": {
"caret": "#000000",
"chatPlaceholder": "#af9f9f",
"link": "#4286f4",
"regular": "#000000",
"system": "#8c7f7f"
}
},
"scrollbars": {
"background": "#00000000",
"thumb": "#b3b3b3",
"thumbSelected": "#a6a6a6"
},
"splits": {
"background": "#ffffff",
"dropPreview": "#300094ff",
"dropPreviewBorder": "#0094ff",
"dropTargetRect": "#00ffffff",
"dropTargetRectBorder": "#000094ff",
"header": {
"background": "#ffffff",
"border": "#ffffff",
"focusedBackground": "#f2f2f2",
"focusedBorder": "#e6e6e6",
"focusedText": "#0051a3",
"text": "#000000"
},
"input": {
"background": "#f2f2f2",
"text": "#000000"
},
"messageSeperator": "#7f7f7f",
"resizeHandle": "#0094ff",
"resizeHandleBackground": "#500094ff"
},
"tabs": {
"dividerLine": "#b4d7ff",
"highlighted": {
"backgrounds": {
"hover": "#eeeeee",
"regular": "#ffffff",
"unfocused": "#ffffff"
},
"line": {
"hover": "#ff0000",
"regular": "#ff0000",
"unfocused": "#ff0000"
},
"text": "#000000"
},
"newMessage": {
"backgrounds": {
"hover": "#eeeeee",
"regular": "#ffffff",
"unfocused": "#ffffff"
},
"line": {
"hover": "#bbbbbb",
"regular": "#bbbbbb",
"unfocused": "#bbbbbb"
},
"text": "#222222"
},
"regular": {
"backgrounds": {
"hover": "#eeeeee",
"regular": "#ffffff",
"unfocused": "#ffffff"
},
"line": {
"hover": "#ffffff",
"regular": "#ffffff",
"unfocused": "#ffffff"
},
"text": "#444444"
},
"selected": {
"backgrounds": {
"hover": "#b4d7ff",
"regular": "#b4d7ff",
"unfocused": "#b4d7ff"
},
"line": {
"hover": "#00aeef",
"regular": "#00aeef",
"unfocused": "#00aeef"
},
"text": "#000000"
}
},
"window": {
"background": "#ffffff",
"text": "#000000"
}
}
}

View file

@ -45,6 +45,7 @@ Q_LOGGING_CATEGORY(chatterinoSound, "chatterino.sound", logThreshold);
Q_LOGGING_CATEGORY(chatterinoStreamerMode, "chatterino.streamermode",
logThreshold);
Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTheme, "chatterino.theme", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTokenizer, "chatterino.tokenizer", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTwitch, "chatterino.twitch", logThreshold);
Q_LOGGING_CATEGORY(chatterinoUpdate, "chatterino.update", logThreshold);

View file

@ -34,6 +34,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventvEventAPI);
Q_DECLARE_LOGGING_CATEGORY(chatterinoSound);
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamerMode);
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTheme);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitch);
Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate);

View file

@ -2,34 +2,153 @@
#include "singletons/Theme.hpp"
#include "Application.hpp"
#include "singletons/Resources.hpp"
#include "common/QLogging.hpp"
#include <QColor>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSet>
#include <cmath>
namespace {
double getMultiplierByTheme(const QString &themeName)
void parseInto(const QJsonObject &obj, const QLatin1String &key, QColor &color)
{
if (themeName == "Light")
const auto &jsonValue = obj[key];
if (!jsonValue.isString()) [[unlikely]]
{
return 0.8;
qCWarning(chatterinoTheme) << key
<< "was expected but not found in the "
"current theme - using previous value.";
return;
}
if (themeName == "White")
QColor parsed = {jsonValue.toString()};
if (!parsed.isValid()) [[unlikely]]
{
return 1.0;
qCWarning(chatterinoTheme).nospace()
<< "While parsing " << key << ": '" << jsonValue.toString()
<< "' isn't a valid color.";
return;
}
if (themeName == "Black")
{
return -1.0;
}
if (themeName == "Dark")
{
return -0.8;
}
return -0.8; // default: Dark
color = parsed;
}
// NOLINTBEGIN(cppcoreguidelines-macro-usage)
#define parseColor(to, from, key) \
parseInto(from, QLatin1String(#key), (to).from.key)
// NOLINTEND(cppcoreguidelines-macro-usage)
void parseWindow(const QJsonObject &window, chatterino::Theme &theme)
{
parseColor(theme, window, background);
parseColor(theme, window, text);
}
void parseTabs(const QJsonObject &tabs, chatterino::Theme &theme)
{
const auto parseTabColors = [](auto json, auto &tab) {
parseInto(json, QLatin1String("text"), tab.text);
{
const auto backgrounds = json["backgrounds"].toObject();
parseColor(tab, backgrounds, regular);
parseColor(tab, backgrounds, hover);
parseColor(tab, backgrounds, unfocused);
}
{
const auto line = json["line"].toObject();
parseColor(tab, line, regular);
parseColor(tab, line, hover);
parseColor(tab, line, unfocused);
}
};
parseColor(theme, tabs, dividerLine);
parseTabColors(tabs["regular"].toObject(), theme.tabs.regular);
parseTabColors(tabs["newMessage"].toObject(), theme.tabs.newMessage);
parseTabColors(tabs["highlighted"].toObject(), theme.tabs.highlighted);
parseTabColors(tabs["selected"].toObject(), theme.tabs.selected);
}
void parseMessages(const QJsonObject &messages, chatterino::Theme &theme)
{
{
const auto textColors = messages["textColors"].toObject();
parseColor(theme.messages, textColors, regular);
parseColor(theme.messages, textColors, caret);
parseColor(theme.messages, textColors, link);
parseColor(theme.messages, textColors, system);
parseColor(theme.messages, textColors, chatPlaceholder);
}
{
const auto backgrounds = messages["backgrounds"].toObject();
parseColor(theme.messages, backgrounds, regular);
parseColor(theme.messages, backgrounds, alternate);
}
parseColor(theme, messages, disabled);
parseColor(theme, messages, selection);
parseColor(theme, messages, highlightAnimationStart);
parseColor(theme, messages, highlightAnimationEnd);
}
void parseScrollbars(const QJsonObject &scrollbars, chatterino::Theme &theme)
{
parseColor(theme, scrollbars, background);
parseColor(theme, scrollbars, thumb);
parseColor(theme, scrollbars, thumbSelected);
}
void parseSplits(const QJsonObject &splits, chatterino::Theme &theme)
{
parseColor(theme, splits, messageSeperator);
parseColor(theme, splits, background);
parseColor(theme, splits, dropPreview);
parseColor(theme, splits, dropPreviewBorder);
parseColor(theme, splits, dropTargetRect);
parseColor(theme, splits, dropTargetRectBorder);
parseColor(theme, splits, resizeHandle);
parseColor(theme, splits, resizeHandleBackground);
{
const auto header = splits["header"].toObject();
parseColor(theme.splits, header, border);
parseColor(theme.splits, header, focusedBorder);
parseColor(theme.splits, header, background);
parseColor(theme.splits, header, focusedBackground);
parseColor(theme.splits, header, text);
parseColor(theme.splits, header, focusedText);
}
{
const auto input = splits["input"].toObject();
parseColor(theme.splits, input, background);
parseColor(theme.splits, input, text);
}
}
void parseColors(const QJsonObject &root, chatterino::Theme &theme)
{
const auto colors = root["colors"].toObject();
parseInto(colors, QLatin1String("accent"), theme.accent);
parseWindow(colors["window"].toObject(), theme);
parseTabs(colors["tabs"].toObject(), theme);
parseMessages(colors["messages"].toObject(), theme);
parseScrollbars(colors["scrollbars"].toObject(), theme);
parseSplits(colors["splits"].toObject(), theme);
}
#undef parseColor
QString getThemePath(const QString &name)
{
static QSet<QString> knownThemes = {"White", "Light", "Dark", "Black"};
if (knownThemes.contains(name))
{
return QStringLiteral(":/themes/%1.json").arg(name);
}
return name;
}
} // namespace
namespace chatterino {
@ -52,142 +171,45 @@ Theme::Theme()
void Theme::update()
{
this->actuallyUpdate(getMultiplierByTheme(this->themeName.getValue()));
this->parse();
this->updated.invoke();
}
// multiplier: 1 = white, 0.8 = light, -0.8 dark, -1 black
void Theme::actuallyUpdate(double multiplier)
void Theme::parse()
{
this->isLight_ = multiplier > 0;
const auto isLight = this->isLightTheme();
auto getGray = [multiplier](double l, double a = 1.0) {
return QColor::fromHslF(0, 0, ((l - 0.5) * multiplier) + 0.5, a);
};
/// WINDOW
#ifdef Q_OS_LINUX
this->window.background = isLight ? "#fff" : QColor(61, 60, 56);
#else
this->window.background = isLight ? "#fff" : "#111";
#endif
this->window.text = isLight ? "#000" : "#eee";
/// TABSs
if (isLight)
QFile file(getThemePath(this->themeName));
if (!file.open(QFile::ReadOnly))
{
this->tabs.regular = {.text = "#444",
.backgrounds = {"#fff", "#eee", "#fff"},
.line = {"#fff", "#fff", "#fff"}};
this->tabs.newMessage = {.text = "#222",
.backgrounds = {"#fff", "#eee", "#fff"},
.line = {"#bbb", "#bbb", "#bbb"}};
this->tabs.highlighted = {.text = "#000",
.backgrounds = {"#fff", "#eee", "#fff"},
.line = {"#f00", "#f00", "#f00"}};
this->tabs.selected = {
.text = "#000",
.backgrounds = {"#b4d7ff", "#b4d7ff", "#b4d7ff"},
.line = {this->accent, this->accent, this->accent}};
}
else
{
this->tabs.regular = {.text = "#aaa",
.backgrounds{"#252525", "#252525", "#252525"},
.line = {"#444", "#444", "#444"}};
this->tabs.newMessage = {.text = "#eee",
.backgrounds{"#252525", "#252525", "#252525"},
.line = {"#888", "#888", "#888"}};
this->tabs.highlighted = {.text = "#eee",
.backgrounds{"#252525", "#252525", "#252525"},
.line = {"#ee6166", "#ee6166", "#ee6166"}};
this->tabs.selected = {
.text = "#fff",
.backgrounds{"#555", "#555", "#555"},
.line = {this->accent, this->accent, this->accent}};
qCWarning(chatterinoTheme) << "Failed to open" << file.fileName();
return;
}
this->tabs.dividerLine = this->tabs.selected.backgrounds.regular;
// Message
this->messages.textColors.caret = isLight ? "#000" : "#fff";
this->messages.textColors.regular = isLight ? "#000" : "#fff";
this->messages.textColors.link = QColor(66, 134, 244);
this->messages.textColors.system = QColor(140, 127, 127);
this->messages.textColors.chatPlaceholder =
isLight ? QColor(175, 159, 159) : QColor(93, 85, 85);
this->messages.backgrounds.regular = getGray(1);
this->messages.backgrounds.alternate = getGray(0.96);
this->messages.disabled = getGray(1, 0.6);
int complementaryGray = isLight ? 20 : 230;
this->messages.highlightAnimationStart =
QColor(complementaryGray, complementaryGray, complementaryGray, 110);
this->messages.highlightAnimationEnd =
QColor(complementaryGray, complementaryGray, complementaryGray, 0);
// Scrollbar
this->scrollbars.background = QColor(0, 0, 0, 0);
this->scrollbars.thumb = getGray(0.70);
this->scrollbars.thumbSelected = getGray(0.65);
// Selection
this->messages.selection =
isLight ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64);
// Splits
if (isLight)
QJsonParseError error{};
auto json = QJsonDocument::fromJson(file.readAll(), &error);
if (json.isNull())
{
this->splits.dropTargetRect = QColor(255, 255, 255, 0);
qCWarning(chatterinoTheme) << "Failed to parse" << file.fileName()
<< "error:" << error.errorString();
return;
}
else
{
this->splits.dropTargetRect = QColor(0, 148, 255, 0);
}
this->splits.dropTargetRectBorder = QColor(0, 148, 255, 0);
this->splits.dropPreview = QColor(0, 148, 255, 48);
this->splits.dropPreviewBorder = QColor(0, 148, 255);
this->splits.resizeHandle = QColor(0, 148, 255, isLight ? 255 : 112);
this->splits.resizeHandleBackground =
QColor(0, 148, 255, isLight ? 80 : 32);
this->splits.header.background = getGray(isLight ? 1 : 0.9);
this->splits.header.border = getGray(isLight ? 1 : 0.85);
this->splits.header.text = this->messages.textColors.regular;
this->splits.header.focusedBackground = getGray(isLight ? 0.95 : 0.79);
this->splits.header.focusedBorder = getGray(isLight ? 0.90 : 0.78);
this->splits.header.focusedText = QColor::fromHsvF(
0.58388, isLight ? 1.0 : 0.482, isLight ? 0.6375 : 1.0);
this->parseFrom(json.object());
}
void Theme::parseFrom(const QJsonObject &root)
{
parseColors(root, *this);
this->isLight_ =
root["metadata"]["iconTheme"].toString() == QStringLiteral("dark");
this->splits.input.background = getGray(0.95);
this->splits.input.text = this->messages.textColors.regular;
this->splits.input.styleSheet =
"background:" + this->splits.input.background.name() + ";" +
"border:" + this->tabs.selected.backgrounds.regular.name() + ";" +
"color:" + this->messages.textColors.regular.name() + ";" +
"selection-background-color:" +
(isLight ? "#68B1FF" : this->tabs.selected.backgrounds.regular.name());
this->splits.messageSeperator =
isLight ? QColor(127, 127, 127) : QColor(60, 60, 60);
this->splits.background = getGray(1);
// Copy button
if (isLight)
{
this->buttons.copy = getResources().buttons.copyDark;
this->buttons.pin = getResources().buttons.pinDisabledDark;
}
else
{
this->buttons.copy = getResources().buttons.copyLight;
this->buttons.pin = getResources().buttons.pinDisabledLight;
}
(this->isLightTheme() ? "#68B1FF"
: this->tabs.selected.backgrounds.regular.name());
}
void Theme::normalizeColor(QColor &color) const

View file

@ -120,7 +120,9 @@ public:
private:
bool isLight_ = false;
void actuallyUpdate(double multiplier);
void parse();
void parseFrom(const QJsonObject &root);
pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_;