diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 01be6af83..8e7de38cd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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" diff --git a/.prettierignore b/.prettierignore index 2c684666b..c08429e13 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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/*/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ca71d500c..17e2dc964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/cmake/resources/generate_resources.cmake b/cmake/resources/generate_resources.cmake index 048d81f91..d9ceccad2 100644 --- a/cmake/resources/generate_resources.cmake +++ b/cmake/resources/generate_resources.cmake @@ -7,6 +7,7 @@ set( resources.qrc resources_autogenerated.qrc windows.rc + themes/ChatterinoTheme.schema.json ) set(RES_IMAGE_EXCLUDE_FILTER ^linuxinstall/) diff --git a/docs/ChatterinoTheme.schema.json b/docs/ChatterinoTheme.schema.json new file mode 100644 index 000000000..a91de0129 --- /dev/null +++ b/docs/ChatterinoTheme.schema.json @@ -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 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"] +} diff --git a/resources/themes/Black.json b/resources/themes/Black.json new file mode 100644 index 000000000..970f3c378 --- /dev/null +++ b/resources/themes/Black.json @@ -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" + } + } +} diff --git a/resources/themes/Dark.json b/resources/themes/Dark.json new file mode 100644 index 000000000..036ce18a3 --- /dev/null +++ b/resources/themes/Dark.json @@ -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" + } + } +} diff --git a/resources/themes/Light.json b/resources/themes/Light.json new file mode 100644 index 000000000..338c642e2 --- /dev/null +++ b/resources/themes/Light.json @@ -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" + } + } +} diff --git a/resources/themes/White.json b/resources/themes/White.json new file mode 100644 index 000000000..7676ac629 --- /dev/null +++ b/resources/themes/White.json @@ -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" + } + } +} diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index fb9afa263..a3168a993 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -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); diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index d3585f18c..c2d0ae2ca 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -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); diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 19f44bf27..1a4263cc8 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -2,34 +2,153 @@ #include "singletons/Theme.hpp" #include "Application.hpp" -#include "singletons/Resources.hpp" +#include "common/QLogging.hpp" #include +#include +#include +#include +#include #include 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 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 diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index fb6690ee6..034bb799e 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -120,7 +120,9 @@ public: private: bool isLight_ = false; - void actuallyUpdate(double multiplier); + + void parse(); + void parseFrom(const QJsonObject &root); pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_;