From afa8067a20e40df6260d1b339624b638221da783 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 6 Oct 2024 12:54:24 +0200 Subject: [PATCH] Add transparent overlay window (#4746) --- CHANGELOG.md | 1 + docs/ChatterinoTheme.schema.json | 92 ++- resources/themes/Black.json | 16 + resources/themes/Dark.json | 16 + resources/themes/Light.json | 16 + resources/themes/White.json | 16 + src/CMakeLists.txt | 4 + src/controllers/hotkeys/ActionNames.hpp | 17 +- src/controllers/hotkeys/HotkeyController.cpp | 6 + src/messages/MessageColor.cpp | 10 +- src/messages/MessageColor.hpp | 5 +- src/messages/MessageElement.cpp | 77 +-- src/messages/MessageElement.hpp | 31 +- src/messages/layouts/MessageLayout.cpp | 49 +- src/messages/layouts/MessageLayout.hpp | 8 +- src/messages/layouts/MessageLayoutContext.cpp | 37 +- src/messages/layouts/MessageLayoutContext.hpp | 28 +- src/messages/layouts/MessageLayoutElement.cpp | 2 +- src/singletons/Settings.hpp | 13 + src/singletons/Theme.cpp | 61 +- src/singletons/Theme.hpp | 36 +- src/singletons/WindowManager.cpp | 32 ++ src/singletons/WindowManager.hpp | 3 + src/widgets/Notebook.cpp | 16 + src/widgets/Notebook.hpp | 3 + src/widgets/OverlayWindow.cpp | 530 ++++++++++++++++++ src/widgets/OverlayWindow.hpp | 87 +++ src/widgets/Scrollbar.cpp | 36 +- src/widgets/Scrollbar.hpp | 3 + src/widgets/helper/ChannelView.cpp | 74 ++- src/widgets/helper/ChannelView.hpp | 12 + src/widgets/helper/MessageView.cpp | 16 +- src/widgets/helper/OverlayInteraction.cpp | 123 ++++ src/widgets/helper/OverlayInteraction.hpp | 47 ++ src/widgets/settingspages/GeneralPage.cpp | 34 ++ src/widgets/splits/Split.cpp | 56 ++ src/widgets/splits/Split.hpp | 6 + src/widgets/splits/SplitHeader.cpp | 3 + tests/src/MessageLayout.cpp | 12 +- tests/src/MessageLayoutContainer.cpp | 22 +- 40 files changed, 1465 insertions(+), 191 deletions(-) create mode 100644 src/widgets/OverlayWindow.cpp create mode 100644 src/widgets/OverlayWindow.hpp create mode 100644 src/widgets/helper/OverlayInteraction.cpp create mode 100644 src/widgets/helper/OverlayInteraction.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed6d781ded..4976ddfaa1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Major: Add option to show pronouns in user card. (#5442, #5583) - Major: Release plugins alpha. (#5288) - Major: Improve high-DPI support on Windows. (#4868, #5391) +- Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746) - Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) - Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625) - Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530) diff --git a/docs/ChatterinoTheme.schema.json b/docs/ChatterinoTheme.schema.json index 972d7598af0..7e23e73a2a3 100644 --- a/docs/ChatterinoTheme.schema.json +++ b/docs/ChatterinoTheme.schema.json @@ -215,6 +215,47 @@ "text": { "$ref": "#/definitions/qt-color" } }, "required": ["backgrounds", "line", "text"] + }, + "text-colors": { + "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"] + }, + "message-backgrounds": { + "type": "object", + "additionalProperties": false, + "properties": { + "alternate": { "$ref": "#/definitions/qt-color" }, + "regular": { "$ref": "#/definitions/qt-color" } + }, + "required": ["alternate", "regular"] + }, + "message-colors": { + "type": "object", + "additionalProperties": false, + "properties": { + "backgrounds": { "$ref": "#/definitions/message-backgrounds" }, + "disabled": { "$ref": "#/definitions/qt-color" }, + "highlightAnimationEnd": { "$ref": "#/definitions/qt-color" }, + "highlightAnimationStart": { "$ref": "#/definitions/qt-color" }, + "selection": { "$ref": "#/definitions/qt-color" }, + "textColors": { "$ref": "#/definitions/text-colors" } + }, + "required": [ + "backgrounds", + "disabled", + "highlightAnimationEnd", + "highlightAnimationStart", + "selection", + "textColors" + ] } }, "type": "object", @@ -229,37 +270,12 @@ "type": "object", "additionalProperties": false, "properties": { - "backgrounds": { - "type": "object", - "additionalProperties": false, - "properties": { - "alternate": { "$ref": "#/definitions/qt-color" }, - "regular": { "$ref": "#/definitions/qt-color" } - }, - "required": ["alternate", "regular"] - }, + "backgrounds": { "$ref": "#/definitions/message-backgrounds" }, "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" - ] - } + "textColors": { "$ref": "#/definitions/text-colors" } }, "required": [ "backgrounds", @@ -270,6 +286,27 @@ "textColors" ] }, + "overlayMessages": { + "type": "object", + "additionalProperties": false, + "properties": { + "backgrounds": { "$ref": "#/definitions/message-backgrounds" }, + "disabled": { "$ref": "#/definitions/qt-color" }, + "selection": { "$ref": "#/definitions/qt-color" }, + "textColors": { "$ref": "#/definitions/text-colors" }, + "background": { + "$ref": "#/definitions/qt-color", + "description": "Note: The alpha value is ignored (set through the settings)" + } + }, + "required": [ + "backgrounds", + "disabled", + "selection", + "textColors", + "background" + ] + }, "scrollbars": { "type": "object", "additionalProperties": false, @@ -376,6 +413,7 @@ "required": [ "accent", "messages", + "overlayMessages", "scrollbars", "splits", "tabs", diff --git a/resources/themes/Black.json b/resources/themes/Black.json index 20014cbed0d..c194aa793ad 100644 --- a/resources/themes/Black.json +++ b/resources/themes/Black.json @@ -22,6 +22,22 @@ "system": "#8c7f7f" } }, + "overlayMessages": { + "backgrounds": { + "alternate": "#32000000", + "regular": "transparent" + }, + "disabled": "#64000000", + "selection": "#40ffffff", + "textColors": { + "caret": "#ffffff", + "chatPlaceholder": "#5d5555", + "link": "#4286f4", + "regular": "#ffffff", + "system": "#8c7f7f" + }, + "background": "#000" + }, "scrollbars": { "background": "#00000000", "thumb": "#4d4d4d", diff --git a/resources/themes/Dark.json b/resources/themes/Dark.json index cc0ff7f07fd..2b85545d766 100644 --- a/resources/themes/Dark.json +++ b/resources/themes/Dark.json @@ -22,6 +22,22 @@ "system": "#8c7f7f" } }, + "overlayMessages": { + "backgrounds": { + "alternate": "#32000000", + "regular": "transparent" + }, + "disabled": "#64000000", + "selection": "#40ffffff", + "textColors": { + "caret": "#ffffff", + "chatPlaceholder": "#5d5555", + "link": "#4286f4", + "regular": "#ffffff", + "system": "#8c7f7f" + }, + "background": "#000" + }, "scrollbars": { "background": "#00000000", "thumb": "#575757", diff --git a/resources/themes/Light.json b/resources/themes/Light.json index 2097ff81b95..ed610e313d9 100644 --- a/resources/themes/Light.json +++ b/resources/themes/Light.json @@ -22,6 +22,22 @@ "system": "#8c7f7f" } }, + "overlayMessages": { + "backgrounds": { + "alternate": "#32000000", + "regular": "transparent" + }, + "disabled": "#64000000", + "selection": "#40ffffff", + "textColors": { + "caret": "#ffffff", + "chatPlaceholder": "#5d5555", + "link": "#4286f4", + "regular": "#ffffff", + "system": "#8c7f7f" + }, + "background": "#333" + }, "scrollbars": { "background": "#00000000", "thumb": "#a8a8a8", diff --git a/resources/themes/White.json b/resources/themes/White.json index 950cfc6e039..89b317f3742 100644 --- a/resources/themes/White.json +++ b/resources/themes/White.json @@ -22,6 +22,22 @@ "system": "#8c7f7f" } }, + "overlayMessages": { + "backgrounds": { + "alternate": "#32000000", + "regular": "transparent" + }, + "disabled": "#64000000", + "selection": "#40ffffff", + "textColors": { + "caret": "#ffffff", + "chatPlaceholder": "#5d5555", + "link": "#4286f4", + "regular": "#ffffff", + "system": "#8c7f7f" + }, + "background": "#333" + }, "scrollbars": { "background": "#00000000", "thumb": "#b3b3b3", diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 68c9bd4e541..d24a0155e86 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -551,6 +551,8 @@ set(SOURCE_FILES widgets/Label.hpp widgets/Notebook.cpp widgets/Notebook.hpp + widgets/OverlayWindow.cpp + widgets/OverlayWindow.hpp widgets/Scrollbar.cpp widgets/Scrollbar.hpp widgets/TooltipEntryWidget.cpp @@ -638,6 +640,8 @@ set(SOURCE_FILES widgets/helper/NotebookButton.hpp widgets/helper/NotebookTab.cpp widgets/helper/NotebookTab.hpp + widgets/helper/OverlayInteraction.cpp + widgets/helper/OverlayInteraction.hpp widgets/helper/RegExpItemDelegate.cpp widgets/helper/RegExpItemDelegate.hpp widgets/helper/ResizingTextEdit.cpp diff --git a/src/controllers/hotkeys/ActionNames.hpp b/src/controllers/hotkeys/ActionNames.hpp index 3ebd7d051c0..f15d2e2026b 100644 --- a/src/controllers/hotkeys/ActionNames.hpp +++ b/src/controllers/hotkeys/ActionNames.hpp @@ -181,6 +181,20 @@ inline const std::map actionNames{ {"showSearch", ActionDefinition{"Search current channel"}}, {"showGlobalSearch", ActionDefinition{"Search all channels"}}, {"debug", ActionDefinition{"Show debug popup"}}, + {"popupOverlay", ActionDefinition{"New overlay popup"}}, + {"toggleOverlayInertia", + ActionDefinition{ + .displayName = "Toggle overlay click-through", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"This", {"this"}}, + {"All", {"all"}}, + {"This or all", {"thisOrAll"}}, + }, + .argumentsPrompt = "Target popup:", + }}, }}, {HotkeyCategory::SplitInput, { @@ -259,7 +273,8 @@ inline const std::map actionNames{ {"moveTab", ActionDefinition{ "Move tab", - "", + "", 1, }}, {"newSplit", ActionDefinition{"Create a new split"}}, diff --git a/src/controllers/hotkeys/HotkeyController.cpp b/src/controllers/hotkeys/HotkeyController.cpp index f435bf4afa2..1aa6e7c9664 100644 --- a/src/controllers/hotkeys/HotkeyController.cpp +++ b/src/controllers/hotkeys/HotkeyController.cpp @@ -403,6 +403,12 @@ void HotkeyController::addDefaults(std::set &addedHotkeys) this->tryAddDefault(addedHotkeys, HotkeyCategory::Split, QKeySequence("F10"), "debug", std::vector(), "open debug popup"); + this->tryAddDefault(addedHotkeys, HotkeyCategory::Split, + QKeySequence("Ctrl+Alt+N"), "popupOverlay", {}, + "open overlay"); + this->tryAddDefault( + addedHotkeys, HotkeyCategory::Split, QKeySequence("Ctrl+Shift+U"), + "toggleOverlayInertia", {"all"}, "toggle overlay click-through"); } // split input diff --git a/src/messages/MessageColor.cpp b/src/messages/MessageColor.cpp index 674f4a6867e..ff5f01aca6f 100644 --- a/src/messages/MessageColor.cpp +++ b/src/messages/MessageColor.cpp @@ -1,6 +1,6 @@ #include "messages/MessageColor.hpp" -#include "singletons/Theme.hpp" +#include "messages/layouts/MessageLayoutContext.hpp" namespace chatterino { @@ -15,18 +15,18 @@ MessageColor::MessageColor(Type type) { } -const QColor &MessageColor::getColor(Theme &themeManager) const +const QColor &MessageColor::getColor(const MessageColors &colors) const { switch (this->type_) { case Type::Custom: return this->customColor_; case Type::Text: - return themeManager.messages.textColors.regular; + return colors.regularText; case Type::System: - return themeManager.messages.textColors.system; + return colors.systemText; case Type::Link: - return themeManager.messages.textColors.link; + return colors.linkText; } static QColor _default; diff --git a/src/messages/MessageColor.hpp b/src/messages/MessageColor.hpp index 5592b973515..8eb07d3504e 100644 --- a/src/messages/MessageColor.hpp +++ b/src/messages/MessageColor.hpp @@ -4,7 +4,8 @@ #include namespace chatterino { -class Theme; + +struct MessageColors; struct MessageColor { enum Type { Custom, Text, Link, System }; @@ -12,7 +13,7 @@ struct MessageColor { MessageColor(const QColor &color); MessageColor(Type type_ = Text); - const QColor &getColor(Theme &themeManager) const; + const QColor &getColor(const MessageColors &colors) const; QString toString() const; diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index a4fd6fe846d..d61613b750f 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -7,6 +7,7 @@ #include "messages/Emote.hpp" #include "messages/Image.hpp" #include "messages/layouts/MessageLayoutContainer.hpp" +#include "messages/layouts/MessageLayoutContext.hpp" #include "messages/layouts/MessageLayoutElement.hpp" #include "providers/emoji/Emojis.hpp" #include "singletons/Emotes.hpp" @@ -119,9 +120,9 @@ ImageElement::ImageElement(ImagePtr image, MessageElementFlags flags) } void ImageElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { auto size = QSize(this->image_->width() * container.getScale(), this->image_->height() * container.getScale()); @@ -151,9 +152,9 @@ CircularImageElement::CircularImageElement(ImagePtr image, int padding, } void CircularImageElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { auto imgSize = QSize(this->image_->width(), this->image_->height()) * container.getScale(); @@ -192,11 +193,11 @@ EmotePtr EmoteElement::getEmote() const } void EmoteElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { - if (flags.has(MessageElementFlag::EmoteImages)) + if (ctx.flags.has(MessageElementFlag::EmoteImages)) { auto image = this->emote_->images.getImageOrLoaded( container.getImageScale()); @@ -217,8 +218,9 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, { if (this->textElement_) { - this->textElement_->addToContainer(container, - MessageElementFlag::Misc); + auto textCtx = ctx; + textCtx.flags = MessageElementFlag::Misc; + this->textElement_->addToContainer(container, textCtx); } } } @@ -260,11 +262,11 @@ void LayeredEmoteElement::addEmoteLayer(const LayeredEmoteElement::Emote &emote) } void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { - if (flags.has(MessageElementFlag::EmoteImages)) + if (ctx.flags.has(MessageElementFlag::EmoteImages)) { auto images = this->getLoadedImages(container.getImageScale()); if (images.empty()) @@ -291,8 +293,9 @@ void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container, { if (this->textElement_) { - this->textElement_->addToContainer(container, - MessageElementFlag::Misc); + auto textCtx = ctx; + textCtx.flags = MessageElementFlag::Misc; + this->textElement_->addToContainer(container, textCtx); } } } @@ -447,9 +450,9 @@ BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags) } void BadgeElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { auto image = this->emote_->images.getImageOrLoaded(container.getImageScale()); @@ -574,11 +577,11 @@ TextElement::TextElement(const QString &text, MessageElementFlags flags, } void TextElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { auto *app = getApp(); - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { QFontMetrics metrics = app->getFonts()->getFontMetrics(this->style_, container.getScale()); @@ -589,7 +592,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container, auto getTextLayoutElement = [&](QString text, int width, bool hasTrailingSpace) { - auto color = this->color_.getColor(*app->getThemes()); + auto color = this->color_.getColor(ctx.messageColors); app->getThemes()->normalizeColor(color); auto *e = new TextLayoutElement( @@ -697,18 +700,18 @@ SingleLineTextElement::SingleLineTextElement(const QString &text, } void SingleLineTextElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { auto *app = getApp(); - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { QFontMetrics metrics = app->getFonts()->getFontMetrics(this->style_, container.getScale()); auto getTextLayoutElement = [&](QString text, int width, bool hasTrailingSpace) { - auto color = this->color_.getColor(*app->getThemes()); + auto color = this->color_.getColor(ctx.messageColors); app->getThemes()->normalizeColor(color); auto *e = new TextLayoutElement( @@ -838,11 +841,11 @@ LinkElement::LinkElement(const Parsed &parsed, const QString &fullUrl, } void LinkElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { this->words_ = getSettings()->lowercaseDomains ? this->lowercase_ : this->original_; - TextElement::addToContainer(container, flags); + TextElement::addToContainer(container, ctx); } Link LinkElement::getLink() const @@ -873,7 +876,7 @@ MentionElement::MentionElement(const QString &displayName, QString loginName_, } void MentionElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { if (getSettings()->colorUsernames) { @@ -893,7 +896,7 @@ void MentionElement::addToContainer(MessageLayoutContainer &container, this->style_ = FontStyle::ChatMedium; } - TextElement::addToContainer(container, flags); + TextElement::addToContainer(container, ctx); } MessageElement *MentionElement::setLink(const Link &link) @@ -943,9 +946,9 @@ TimestampElement::TimestampElement(QTime time) } void TimestampElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { if (getSettings()->timestampFormat != this->format_) { @@ -953,7 +956,7 @@ void TimestampElement::addToContainer(MessageLayoutContainer &container, this->element_.reset(this->formatTime(this->time_)); } - this->element_->addToContainer(container, flags); + this->element_->addToContainer(container, ctx); } } @@ -985,9 +988,9 @@ TwitchModerationElement::TwitchModerationElement() } void TwitchModerationElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { - if (flags.has(MessageElementFlag::ModeratorTools)) + if (ctx.flags.has(MessageElementFlag::ModeratorTools)) { QSize size(int(container.getScale() * 16), int(container.getScale() * 16)); @@ -1026,9 +1029,9 @@ LinebreakElement::LinebreakElement(MessageElementFlags flags) } void LinebreakElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { container.breakLine(); } @@ -1050,9 +1053,9 @@ ScalingImageElement::ScalingImageElement(ImageSet images, } void ScalingImageElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { const auto &image = this->images_.getImageOrLoaded(container.getImageScale()); @@ -1083,14 +1086,14 @@ ReplyCurveElement::ReplyCurveElement() } void ReplyCurveElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) + const MessageLayoutContext &ctx) { static const int width = 18; // Overall width static const float thickness = 1.5; // Pen width static const int radius = 6; // Radius of the top left corner static const int margin = 2; // Top/Left/Bottom margin - if (flags.hasAny(this->getFlags())) + if (ctx.flags.hasAny(this->getFlags())) { float scale = container.getScale(); container.addElement( diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 041593cac20..0e6c07b93ba 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -23,6 +23,7 @@ namespace chatterino { class Channel; struct MessageLayoutContainer; class MessageLayoutElement; +struct MessageLayoutContext; class Image; using ImagePtr = std::shared_ptr; @@ -184,7 +185,7 @@ class MessageElement void addFlags(MessageElementFlags flags); virtual void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) = 0; + const MessageLayoutContext &ctx) = 0; virtual QJsonObject toJson() const; @@ -205,7 +206,7 @@ class ImageElement : public MessageElement ImageElement(ImagePtr image, MessageElementFlags flags); void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; QJsonObject toJson() const override; @@ -221,7 +222,7 @@ class CircularImageElement : public MessageElement MessageElementFlags flags); void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; QJsonObject toJson() const override; @@ -241,7 +242,7 @@ class TextElement : public MessageElement ~TextElement() override = default; void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; QJsonObject toJson() const override; @@ -262,7 +263,7 @@ class SingleLineTextElement : public MessageElement ~SingleLineTextElement() override = default; void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; QJsonObject toJson() const override; @@ -298,7 +299,7 @@ class LinkElement : public TextElement LinkElement &operator=(LinkElement &&) = delete; void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; Link getLink() const override; @@ -338,7 +339,7 @@ class MentionElement : public TextElement MentionElement &operator=(MentionElement &&) = delete; void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; MessageElement *setLink(const Link &link) override; Link getLink() const override; @@ -369,7 +370,7 @@ class EmoteElement : public MessageElement const MessageColor &textElementColor = MessageColor::Text); void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags_) override; + const MessageLayoutContext &ctx) override; EmotePtr getEmote() const; QJsonObject toJson() const override; @@ -401,7 +402,7 @@ class LayeredEmoteElement : public MessageElement void addEmoteLayer(const Emote &emote); void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; // Returns a concatenation of each emote layer's cleaned copy string QString getCleanCopyString() const; @@ -433,7 +434,7 @@ class BadgeElement : public MessageElement BadgeElement(const EmotePtr &data, MessageElementFlags flags_); void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags_) override; + const MessageLayoutContext &ctx) override; EmotePtr getEmote() const; @@ -494,7 +495,7 @@ class TimestampElement : public MessageElement ~TimestampElement() override = default; void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; TextElement *formatTime(const QTime &time); @@ -514,7 +515,7 @@ class TwitchModerationElement : public MessageElement TwitchModerationElement(); void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; QJsonObject toJson() const override; }; @@ -526,7 +527,7 @@ class LinebreakElement : public MessageElement LinebreakElement(MessageElementFlags flags); void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; QJsonObject toJson() const override; }; @@ -538,7 +539,7 @@ class ScalingImageElement : public MessageElement ScalingImageElement(ImageSet images, MessageElementFlags flags); void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; QJsonObject toJson() const override; @@ -552,7 +553,7 @@ class ReplyCurveElement : public MessageElement ReplyCurveElement(); void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; + const MessageLayoutContext &ctx) override; QJsonObject toJson() const override; }; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index e1a3d8a3c48..81c75c6b471 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -68,8 +68,7 @@ int MessageLayout::getWidth() const // Layout // return true if redraw is required -bool MessageLayout::layout(int width, float scale, float imageScale, - MessageElementFlags flags, +bool MessageLayout::layout(const MessageLayoutContext &ctx, bool shouldInvalidateBuffer) { // BenchmarkGuard benchmark("MessageLayout::layout()"); @@ -77,9 +76,9 @@ bool MessageLayout::layout(int width, float scale, float imageScale, bool layoutRequired = false; // check if width changed - bool widthChanged = width != this->currentLayoutWidth_; + bool widthChanged = ctx.width != this->currentLayoutWidth_; layoutRequired |= widthChanged; - this->currentLayoutWidth_ = width; + this->currentLayoutWidth_ = ctx.width; // check if layout state changed const auto layoutGeneration = getApp()->getWindows()->getGeneration(); @@ -91,18 +90,18 @@ bool MessageLayout::layout(int width, float scale, float imageScale, } // check if work mask changed - layoutRequired |= this->currentWordFlags_ != flags; - this->currentWordFlags_ = flags; // getSettings()->getWordTypeMask(); + layoutRequired |= this->currentWordFlags_ != ctx.flags; + this->currentWordFlags_ = ctx.flags; // getSettings()->getWordTypeMask(); // check if layout was requested manually layoutRequired |= this->flags.has(MessageLayoutFlag::RequiresLayout); this->flags.unset(MessageLayoutFlag::RequiresLayout); // check if dpi changed - layoutRequired |= this->scale_ != scale; - this->scale_ = scale; - layoutRequired |= this->imageScale_ != imageScale; - this->imageScale_ = imageScale; + layoutRequired |= this->scale_ != ctx.scale; + this->scale_ = ctx.scale; + layoutRequired |= this->imageScale_ != ctx.imageScale; + this->imageScale_ = ctx.imageScale; if (!layoutRequired) { @@ -115,7 +114,7 @@ bool MessageLayout::layout(int width, float scale, float imageScale, } int oldHeight = this->container_.getHeight(); - this->actuallyLayout(width, flags); + this->actuallyLayout(ctx); if (widthChanged || this->container_.getHeight() != oldHeight) { this->deleteBuffer(); @@ -125,7 +124,7 @@ bool MessageLayout::layout(int width, float scale, float imageScale, return true; } -void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) +void MessageLayout::actuallyLayout(const MessageLayoutContext &ctx) { #ifdef FOURTF this->layoutCount_++; @@ -134,7 +133,7 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) auto messageFlags = this->message_->flags; if (this->flags.has(MessageLayoutFlag::Expanded) || - (flags.has(MessageElementFlag::ModeratorTools) && + (ctx.flags.has(MessageElementFlag::ModeratorTools) && !this->message_->flags.has(MessageFlag::Disabled))) { messageFlags.unset(MessageFlag::Collapsed); @@ -143,9 +142,9 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) bool hideModerated = getSettings()->hideModerated; bool hideModerationActions = getSettings()->hideModerationActions; bool hideSimilar = getSettings()->hideSimilar; - bool hideReplies = !flags.has(MessageElementFlag::RepliedMessage); + bool hideReplies = !ctx.flags.has(MessageElementFlag::RepliedMessage); - this->container_.beginLayout(width, this->scale_, this->imageScale_, + this->container_.beginLayout(ctx.width, this->scale_, this->imageScale_, messageFlags); for (const auto &element : this->message_->elements) @@ -177,7 +176,7 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) continue; } - element->addToContainer(this->container_, flags); + element->addToContainer(this->container_, ctx); } if (this->height_ != this->container_.getHeight()) @@ -201,10 +200,15 @@ MessagePaintResult MessageLayout::paint(const MessagePaintContext &ctx) { MessagePaintResult result; - QPixmap *pixmap = this->ensureBuffer(ctx.painter, ctx.canvasWidth); + QPixmap *pixmap = this->ensureBuffer(ctx.painter, ctx.canvasWidth, + ctx.messageColors.hasTransparency); if (!this->bufferValid_) { + if (ctx.messageColors.hasTransparency) + { + pixmap->fill(Qt::transparent); + } this->updateBuffer(pixmap, ctx); } @@ -278,7 +282,7 @@ MessagePaintResult MessageLayout::paint(const MessagePaintContext &ctx) return result; } -QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width) +QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width, bool clear) { if (this->buffer_ != nullptr) { @@ -292,6 +296,11 @@ QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width) painter.device()->devicePixelRatioF())); this->buffer_->setDevicePixelRatio(painter.device()->devicePixelRatioF()); + if (clear) + { + this->buffer_->fill(Qt::transparent); + } + this->bufferValid_ = false; DebugCount::increase("message drawing buffers"); return this->buffer_.get(); @@ -313,10 +322,10 @@ void MessageLayout::updateBuffer(QPixmap *buffer, if (ctx.preferences.alternateMessages && this->flags.has(MessageLayoutFlag::AlternateBackground)) { - return ctx.messageColors.alternate; + return ctx.messageColors.alternateBg; } - return ctx.messageColors.regular; + return ctx.messageColors.regularBg; }(); if (this->message_->flags.has(MessageFlag::ElevatedMessage) && diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index 12a8b153a76..841fd328d9f 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -18,6 +18,7 @@ struct Selection; struct MessageLayoutContainer; class MessageLayoutElement; struct MessagePaintContext; +struct MessageLayoutContext; enum class MessageElementFlag : int64_t; using MessageElementFlags = FlagsEnum; @@ -56,8 +57,7 @@ class MessageLayout MessageLayoutFlags flags; - bool layout(int width, float scale_, float imageScale, - MessageElementFlags flags, bool shouldInvalidateBuffer); + bool layout(const MessageLayoutContext &ctx, bool shouldInvalidateBuffer); // Painting MessagePaintResult paint(const MessagePaintContext &ctx); @@ -112,11 +112,11 @@ class MessageLayout private: // methods - void actuallyLayout(int width, MessageElementFlags flags); + void actuallyLayout(const MessageLayoutContext &ctx); void updateBuffer(QPixmap *buffer, const MessagePaintContext &ctx); // Create new buffer if required, returning the buffer - QPixmap *ensureBuffer(QPainter &painter, int width); + QPixmap *ensureBuffer(QPainter &painter, int width, bool clear); // variables const MessagePtr message_; diff --git a/src/messages/layouts/MessageLayoutContext.cpp b/src/messages/layouts/MessageLayoutContext.cpp index 98c963919d5..f754ea408f3 100644 --- a/src/messages/layouts/MessageLayoutContext.cpp +++ b/src/messages/layouts/MessageLayoutContext.cpp @@ -3,21 +3,44 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" +#include + namespace chatterino { -void MessageColors::applyTheme(Theme *theme) +void MessageColors::applyTheme(Theme *theme, bool isOverlay, + int backgroundOpacity) { - this->regular = theme->messages.backgrounds.regular; - this->alternate = theme->messages.backgrounds.alternate; - - this->disabled = theme->messages.disabled; - this->selection = theme->messages.selection; - this->system = theme->messages.textColors.system; + auto applyColors = [this](const auto &src) { + this->regularBg = src.backgrounds.regular; + this->alternateBg = src.backgrounds.alternate; + + this->disabled = src.disabled; + this->selection = src.selection; + + this->regularText = src.textColors.regular; + this->linkText = src.textColors.link; + this->systemText = src.textColors.system; + }; + + if (isOverlay) + { + this->channelBackground = theme->overlayMessages.background; + this->channelBackground.setAlpha(std::clamp(backgroundOpacity, 0, 255)); + applyColors(theme->overlayMessages); + } + else + { + this->channelBackground = theme->splits.background; + applyColors(theme->messages); + } this->messageSeperator = theme->splits.messageSeperator; this->focusedLastMessageLine = theme->tabs.selected.backgrounds.regular; this->unfocusedLastMessageLine = theme->tabs.selected.backgrounds.unfocused; + + this->hasTransparency = + this->regularBg.alpha() != 255 || this->alternateBg.alpha() != 255; } void MessagePreferences::connectSettings(Settings *settings, diff --git a/src/messages/layouts/MessageLayoutContext.hpp b/src/messages/layouts/MessageLayoutContext.hpp index d8f08ab3abf..b7a9b6886cf 100644 --- a/src/messages/layouts/MessageLayoutContext.hpp +++ b/src/messages/layouts/MessageLayoutContext.hpp @@ -1,5 +1,7 @@ #pragma once +#include "messages/MessageElement.hpp" + #include #include @@ -16,18 +18,27 @@ struct Selection; // TODO: Figure out if this could be a subset of Theme instead (e.g. Theme::MessageColors) struct MessageColors { - QColor regular; - QColor alternate; + QColor channelBackground; + + // true if any of the background colors have transparency + bool hasTransparency = false; + + QColor regularBg; + QColor alternateBg; + QColor disabled; QColor selection; - QColor system; + + QColor regularText; + QColor linkText; + QColor systemText; QColor messageSeperator; QColor focusedLastMessageLine; QColor unfocusedLastMessageLine; - void applyTheme(Theme *theme); + void applyTheme(Theme *theme, bool isOverlay, int backgroundOpacity); }; // TODO: Explore if we can let settings own this @@ -72,4 +83,13 @@ struct MessagePaintContext { bool isLastReadMessage{}; }; +struct MessageLayoutContext { + const MessageColors &messageColors; + MessageElementFlags flags; + + int width = 1; + float scale = 1; + float imageScale = 1; +}; + } // namespace chatterino diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index 3bfe773304a..7b5e3fb5842 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -555,7 +555,7 @@ void TextIconLayoutElement::paint(QPainter &painter, QFont font = app->getFonts()->getFont(FontStyle::Tiny, this->scale); - painter.setPen(messageColors.system); + painter.setPen(messageColors.systemText); painter.setFont(font); QTextOption option; diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index e787dcb0fc2..3566815e87e 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -183,6 +183,18 @@ class Settings // BoolSetting useCustomWindowFrame = {"/appearance/useCustomWindowFrame", // false}; + IntSetting overlayBackgroundOpacity = { + "/appearance/overlay/backgroundOpacity", 50}; + BoolSetting enableOverlayShadow = {"/appearance/overlay/shadow", true}; + IntSetting overlayShadowOpacity = {"/appearance/overlay/shadowOpacity", + 255}; + QStringSetting overlayShadowColor = {"/appearance/overlay/shadowColor", + "#000"}; + // These should be floats, but there's no good input UI for them + IntSetting overlayShadowOffsetX = {"/appearance/overlay/shadowOffsetX", 2}; + IntSetting overlayShadowOffsetY = {"/appearance/overlay/shadowOffsetY", 2}; + IntSetting overlayShadowRadius = {"/appearance/overlay/shadowRadius", 8}; + // Badges BoolSetting showBadgesGlobalAuthority = { "/appearance/badges/GlobalAuthority", true}; @@ -523,6 +535,7 @@ class Settings IntSetting startUpNotification = {"/misc/startUpNotification", 0}; QStringSetting currentVersion = {"/misc/currentVersion", ""}; + IntSetting overlayKnowledgeLevel = {"/misc/overlayKnowledgeLevel", 0}; BoolSetting loadTwitchMessageHistoryOnConnect = { "/misc/twitch/loadMessageHistoryOnConnect", true}; diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 3680e9d949b..0c29d429ca2 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) @@ -118,33 +119,56 @@ void parseTabs(const QJsonObject &tabs, const QJsonObject &tabsFallback, tabsFallback["selected"_L1].toObject(), theme.tabs.selected); } +void parseTextColors(const QJsonObject &textColors, + const QJsonObject &textColorsFallback, auto &messages) +{ + parseColor(messages, textColors, regular); + parseColor(messages, textColors, caret); + parseColor(messages, textColors, link); + parseColor(messages, textColors, system); + parseColor(messages, textColors, chatPlaceholder); +} + +void parseMessageBackgrounds(const QJsonObject &backgrounds, + const QJsonObject &backgroundsFallback, + auto &messages) +{ + parseColor(messages, backgrounds, regular); + parseColor(messages, backgrounds, alternate); +} + void parseMessages(const QJsonObject &messages, const QJsonObject &messagesFallback, chatterino::Theme &theme) { - { - const auto textColors = messages["textColors"_L1].toObject(); - const auto textColorsFallback = - messagesFallback["textColors"_L1].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"_L1].toObject(); - const auto backgroundsFallback = - messagesFallback["backgrounds"_L1].toObject(); - parseColor(theme.messages, backgrounds, regular); - parseColor(theme.messages, backgrounds, alternate); - } + parseTextColors(messages["textColors"_L1].toObject(), + messagesFallback["textColors"_L1].toObject(), + theme.messages); + parseMessageBackgrounds(messages["backgrounds"_L1].toObject(), + messagesFallback["backgrounds"_L1].toObject(), + theme.messages); parseColor(theme, messages, disabled); parseColor(theme, messages, selection); parseColor(theme, messages, highlightAnimationStart); parseColor(theme, messages, highlightAnimationEnd); } +void parseOverlayMessages(const QJsonObject &overlayMessages, + const QJsonObject &overlayMessagesFallback, + chatterino::Theme &theme) +{ + parseTextColors(overlayMessages["textColors"_L1].toObject(), + overlayMessagesFallback["textColors"_L1].toObject(), + theme.overlayMessages); + parseMessageBackgrounds( + overlayMessages["backgrounds"_L1].toObject(), + overlayMessagesFallback["backgrounds"_L1].toObject(), + theme.overlayMessages); + parseColor(theme, overlayMessages, disabled); + parseColor(theme, overlayMessages, selection); + parseColor(theme, overlayMessages, background); +} + void parseScrollbars(const QJsonObject &scrollbars, const QJsonObject &scrollbarsFallback, chatterino::Theme &theme) @@ -198,6 +222,9 @@ void parseColors(const QJsonObject &root, const QJsonObject &fallbackTheme, fallbackColors["tabs"_L1].toObject(), theme); parseMessages(colors["messages"_L1].toObject(), fallbackColors["messages"_L1].toObject(), theme); + parseOverlayMessages(colors["overlayMessages"_L1].toObject(), + fallbackColors["overlayMessages"_L1].toObject(), + theme); parseScrollbars(colors["scrollbars"_L1].toObject(), fallbackColors["scrollbars"_L1].toObject(), theme); parseSplits(colors["splits"_L1].toObject(), diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index cb811e2db24..ac9fa070c1d 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -62,6 +62,19 @@ class Theme final } line; }; + struct TextColors { + QColor regular; + QColor caret; + QColor link; + QColor system; + QColor chatPlaceholder; + }; + + struct MessageBackgrounds { + QColor regular; + QColor alternate; + }; + QColor accent{"#00aeef"}; /// WINDOW @@ -84,18 +97,8 @@ class Theme final /// MESSAGES struct { - struct { - QColor regular; - QColor caret; - QColor link; - QColor system; - QColor chatPlaceholder; - } textColors; - - struct { - QColor regular; - QColor alternate; - } backgrounds; + TextColors textColors; + MessageBackgrounds backgrounds; QColor disabled; QColor selection; @@ -104,6 +107,15 @@ class Theme final QColor highlightAnimationEnd; } messages; + struct { + TextColors textColors; + MessageBackgrounds backgrounds; + + QColor disabled; + QColor selection; + QColor background; + } overlayMessages; + /// SCROLLBAR struct { QColor background; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index bf0192e73ac..f64eb47e77c 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -16,6 +16,7 @@ #include "widgets/FramelessEmbedWindow.hpp" #include "widgets/helper/NotebookTab.hpp" #include "widgets/Notebook.hpp" +#include "widgets/OverlayWindow.hpp" #include "widgets/splits/Split.hpp" #include "widgets/splits/SplitContainer.hpp" #include "widgets/Window.hpp" @@ -544,6 +545,37 @@ void WindowManager::queueSave() this->saveTimer->start(10s); } +void WindowManager::toggleAllOverlayInertia() +{ + // check if any window is not inert + bool anyNonInert = false; + for (auto *window : this->windows_) + { + if (anyNonInert) + { + break; + } + window->getNotebook().forEachSplit([&](auto *split) { + auto *overlay = split->overlayWindow(); + if (overlay) + { + anyNonInert = anyNonInert || !overlay->isInert(); + } + }); + } + + for (auto *window : this->windows_) + { + window->getNotebook().forEachSplit([&](auto *split) { + auto *overlay = split->overlayWindow(); + if (overlay) + { + overlay->setInert(anyNonInert); + } + }); + } +} + void WindowManager::encodeTab(SplitContainer *tab, bool isSelected, QJsonObject &obj) { diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index 7f0dcdee6b2..8e737147d27 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -128,6 +128,9 @@ class WindowManager final // again void queueSave(); + /// Toggles the inertia in all open overlay windows + void toggleAllOverlayInertia(); + /// Signals pajlada::Signals::NoArgSignal gifRepaintRequested; diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index db2bcac4948..2a01020fc8f 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -1633,4 +1633,20 @@ void SplitNotebook::select(QWidget *page, bool focusPage) this->Notebook::select(page, focusPage); } +void SplitNotebook::forEachSplit(const std::function &cb) +{ + for (const auto &item : this->items()) + { + auto *page = dynamic_cast(item.page); + if (!page) + { + continue; + } + for (auto *split : page->getSplits()) + { + cb(split); + } + } +} + } // namespace chatterino diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index e5cbd17d107..ac6c4dad79f 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -18,6 +18,7 @@ class UpdateDialog; class NotebookButton; class NotebookTab; class SplitContainer; +class Split; enum NotebookTabLocation { Top = 0, Left = 1, Right = 2, Bottom = 3 }; @@ -229,6 +230,8 @@ class SplitNotebook : public Notebook void addNotebookActionsToMenu(QMenu *menu) override; + void forEachSplit(const std::function &cb); + /** * Toggles between the "Show all tabs" and "Hide all tabs" tab visibility states */ diff --git a/src/widgets/OverlayWindow.cpp b/src/widgets/OverlayWindow.cpp new file mode 100644 index 00000000000..f81ebf0ca86 --- /dev/null +++ b/src/widgets/OverlayWindow.cpp @@ -0,0 +1,530 @@ +#include "widgets/OverlayWindow.hpp" + +#include "Application.hpp" +#include "common/FlagsEnum.hpp" +#include "common/Literals.hpp" +#include "controllers/hotkeys/HotkeyController.hpp" +#include "singletons/Settings.hpp" +#include "singletons/WindowManager.hpp" +#include "widgets/BaseWidget.hpp" +#include "widgets/helper/ChannelView.hpp" +#include "widgets/helper/InvisibleSizeGrip.hpp" +#include "widgets/Scrollbar.hpp" +#include "widgets/splits/Split.hpp" + +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +# include +# include + +// This definition can be used to test the move interaction for other platforms +// on Windows by commenting it out. In a final build, Windows must always use +// this, as it's much smoother. +# define OVERLAY_NATIVE_MOVE +#endif + +namespace { + +using namespace chatterino; +using namespace literals; + +/// Progress the user has made in exploring the overlay +enum class Knowledge : std::int32_t { // NOLINT(performance-enum-size) + None = 0, + // User opened the overlay at least once + Activation = 1 << 0, +}; + +bool hasKnowledge(Knowledge knowledge) +{ + FlagsEnum current(static_cast( + getSettings()->overlayKnowledgeLevel.getValue())); + return current.has(knowledge); +} + +void acquireKnowledge(Knowledge knowledge) +{ + FlagsEnum current(static_cast( + getSettings()->overlayKnowledgeLevel.getValue())); + current.set(knowledge); + getSettings()->overlayKnowledgeLevel = + static_cast>(current.value()); +} + +/// Returns [seq?, toggleAllOverlays] +std::pair toggleIntertiaShortcut() +{ + auto seq = getApp()->getHotkeys()->getDisplaySequence( + HotkeyCategory::Split, u"toggleOverlayInertia"_s, {{u"this"_s}}); + if (!seq.isEmpty()) + { + return {seq, false}; + } + seq = getApp()->getHotkeys()->getDisplaySequence( + HotkeyCategory::Split, u"toggleOverlayInertia"_s, {{u"thisOrAll"_s}}); + if (!seq.isEmpty()) + { + return {seq, false}; + } + return { + getApp()->getHotkeys()->getDisplaySequence(HotkeyCategory::Split, + u"toggleOverlayInertia"_s), + true, + }; +} + +} // namespace + +namespace chatterino { + +using namespace std::chrono_literals; + +OverlayWindow::OverlayWindow(IndirectChannel channel) + : QWidget(nullptr, + Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint) +#ifdef Q_OS_WIN + , sizeAllCursor_(::LoadCursor(nullptr, IDC_SIZEALL)) +#endif + , channel_(std::move(channel)) + , channelView_(nullptr) + , interaction_(this) +{ + this->setAttribute(Qt::WA_DeleteOnClose); + this->setWindowTitle(u"Chatterino - Overlay"_s); + + // QGridLayout is (ab)used to stack widgets and position them + auto *grid = new QGridLayout(this); + grid->addWidget(&this->channelView_, 0, 0); + this->interaction_.attach(grid); +#ifndef OVERLAY_NATIVE_MOVE + grid->addWidget(new InvisibleSizeGrip(this), 0, 0, + Qt::AlignBottom | Qt::AlignRight); +#endif + + // the interaction overlay currently captures all events + this->interaction_.installEventFilter(this); + + this->shortInteraction_.setInterval(750ms); + QObject::connect(&this->shortInteraction_, &QTimer::timeout, [this] { + this->endInteraction(); + }); + + this->channelView_.installEventFilter(this); + this->channelView_.setChannel(this->channel_.get()); + this->channelView_.setIsOverlay(true); // use overlay colors + this->channelView_.setAttribute(Qt::WA_TranslucentBackground); + this->holder_.managedConnect(this->channel_.getChannelChanged(), [this]() { + this->channelView_.setChannel(this->channel_.get()); + }); + this->channelView_.scrollbar()->setShowThumb(false); + + this->setAutoFillBackground(false); + this->resize(300, 500); + this->move(QCursor::pos() - this->rect().center()); + this->setContentsMargins(0, 0, 0, 0); + this->setAttribute(Qt::WA_TranslucentBackground); + + auto *settings = getSettings(); + settings->enableOverlayShadow.connect( + [this](bool value) { + if (value) + { + this->dropShadow_ = new QGraphicsDropShadowEffect; + this->channelView_.setGraphicsEffect(this->dropShadow_); + } + else + { + this->channelView_.setGraphicsEffect(nullptr); + this->dropShadow_ = nullptr; // deleted by setGraphicsEffect + } + this->applyTheme(); + }, + this->holder_); + settings->overlayBackgroundOpacity.connect( + [this] { + this->channelView_.updateColorTheme(); + this->update(); + }, + this->holder_, false); + + auto applyIt = [this](auto /*unused*/) { + this->applyTheme(); + }; + settings->overlayShadowOffsetX.connect(applyIt, this->holder_, false); + settings->overlayShadowOffsetY.connect(applyIt, this->holder_, false); + settings->overlayShadowOpacity.connect(applyIt, this->holder_, false); + settings->overlayShadowRadius.connect(applyIt, this->holder_, false); + settings->overlayShadowColor.connect(applyIt, this->holder_, false); + + this->addShortcuts(); + this->triggerFirstActivation(); +} + +OverlayWindow::~OverlayWindow() +{ +#ifdef Q_OS_WIN + ::DestroyCursor(this->sizeAllCursor_); +#endif +} + +void OverlayWindow::applyTheme() +{ + auto *settings = getSettings(); + + if (this->dropShadow_) + { + QColor shadowColor(settings->overlayShadowColor.getValue()); + shadowColor.setAlpha( + std::clamp(settings->overlayShadowOpacity.getValue(), 0, 255)); + this->dropShadow_->setColor(shadowColor); + this->dropShadow_->setOffset(settings->overlayShadowOffsetX, + settings->overlayShadowOffsetY); + this->dropShadow_->setBlurRadius(settings->overlayShadowRadius); + } + this->update(); +} + +bool OverlayWindow::eventFilter(QObject * /*object*/, QEvent *event) +{ +#ifndef OVERLAY_NATIVE_MOVE + switch (event->type()) + { + case QEvent::MouseButtonPress: { + auto *evt = dynamic_cast(event); + this->moving_ = true; + this->moveOrigin_ = evt->globalPos(); + return true; + } + break; + case QEvent::MouseButtonRelease: { + if (this->moving_) + { + this->moving_ = false; + return true; + } + return false; + } + break; + case QEvent::MouseMove: { + auto *evt = dynamic_cast(event); + if (this->moving_) + { + auto newPos = evt->globalPos() - this->moveOrigin_; + this->move(newPos + this->pos()); + this->moveOrigin_ = evt->globalPos(); + return true; + } + if (this->interaction_.isInteracting()) + { + this->setOverrideCursor(Qt::SizeAllCursor); + return true; + } + return false; + } + break; + default: + return false; + } +#else + (void)event; + return false; +#endif +} + +void OverlayWindow::setOverrideCursor(const QCursor &cursor) +{ + this->channelView_.setCursor(cursor); + this->setCursor(cursor); +} + +bool OverlayWindow::isInert() const +{ + return this->inert_; +} + +void OverlayWindow::toggleInertia() +{ + this->setInert(!this->inert_); +} + +void OverlayWindow::enterEvent(EnterEvent * /*event*/) +{ +#ifndef OVERLAY_NATIVE_MOVE + this->startInteraction(); +#endif +} + +void OverlayWindow::leaveEvent(QEvent * /*event*/) +{ +#ifndef OVERLAY_NATIVE_MOVE + this->endInteraction(); +#endif +} + +#ifdef Q_OS_WIN +bool OverlayWindow::nativeEvent(const QByteArray &eventType, void *message, + NativeResult *result) +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + MSG *msg = reinterpret_cast(message); + + bool returnValue = false; + + switch (msg->message) + { +# ifdef OVERLAY_NATIVE_MOVE + case WM_NCHITTEST: + this->handleNCHITTEST(msg, result); + returnValue = true; + break; + case WM_MOUSEMOVE: + case WM_NCMOUSEMOVE: + this->startShortInteraction(); + break; + case WM_ENTERSIZEMOVE: + this->startInteraction(); + break; + case WM_EXITSIZEMOVE: + // wait a few seconds before hiding + this->startShortInteraction(); + break; + case WM_SETCURSOR: { + // When the window can be moved, the size-all cursor should be + // shown. Qt doesn't provide an interface to do this, so this + // manually sets the cursor. + if (LOWORD(msg->lParam) == HTCAPTION) + { + ::SetCursor(this->sizeAllCursor_); + *result = TRUE; + returnValue = true; + } + } + break; +# endif + + default: + return QWidget::nativeEvent(eventType, message, result); + } + + QWidget::nativeEvent(eventType, message, result); + + return returnValue; +} + +void OverlayWindow::handleNCHITTEST(MSG *msg, NativeResult *result) +{ + // This implementation is similar to the one of BaseWindow, but has the + // following differences: + // - The window can always be resized (or: it can't be maximized) + // - The close button is advertised as HTCLIENT instead of HTCLOSE + // - There isn't any other client area (the entire window can be moved) + const LONG borderWidth = 8; // in device independent pixels + + auto rect = this->rect(); + + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint point(p.x, p.y); + point /= this->devicePixelRatio(); + + auto x = point.x(); + auto y = point.y(); + + *result = 0; + + // left border + if (x < rect.left() + borderWidth) + { + *result = HTLEFT; + } + // right border + if (x >= rect.right() - borderWidth) + { + *result = HTRIGHT; + } + + // bottom border + if (y >= rect.bottom() - borderWidth) + { + *result = HTBOTTOM; + } + // top border + if (y < rect.top() + borderWidth) + { + *result = HTTOP; + } + + // bottom left corner + if (x >= rect.left() && x < rect.left() + borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) + { + *result = HTBOTTOMLEFT; + } + // bottom right corner + if (x < rect.right() && x >= rect.right() - borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) + { + *result = HTBOTTOMRIGHT; + } + // top left corner + if (x >= rect.left() && x < rect.left() + borderWidth && y >= rect.top() && + y < rect.top() + borderWidth) + { + *result = HTTOPLEFT; + } + // top right corner + if (x < rect.right() && x >= rect.right() - borderWidth && + y >= rect.top() && y < rect.top() + borderWidth) + { + *result = HTTOPRIGHT; + } + + if (*result == 0) + { + auto *closeButton = this->interaction_.closeButton(); + if (closeButton->isVisible() && closeButton->geometry().contains(point)) + { + *result = HTCLIENT; + } + else + { + *result = HTCAPTION; + } + } +} +#endif + +void OverlayWindow::triggerFirstActivation() +{ + if (hasKnowledge(Knowledge::Activation)) + { + return; + } + acquireKnowledge(Knowledge::Activation); + + auto welcomeText = + u"Hey! It looks like this is the first time you're using the overlay. "_s + "You can move the overlay by dragging it with your mouse. " +#ifdef OVERLAY_NATIVE_MOVE + "To resize the window, drag on any edge." +#else + "To resize the window, drag on the bottom right corner." +#endif + "

" + "By default, the overlay is interactive. "; + + auto [actualShortcut, allOverlays] = toggleIntertiaShortcut(); + if (actualShortcut.isEmpty()) + { + welcomeText += + u"To toggle the click-through mode, " + "add a hotkey for \"Toggle overlay click-through\" in the split " + "category to press while any Chatterino window is focused."_s; + } + else + { + welcomeText += + u"To toggle the click-through mode, press %1 (customizable "_s + "in the settings) while any Chatterino window is focused.".arg( + actualShortcut.toString()); + } + + welcomeText += u"

"_s + "This is still an early version and some features are " + "missing. Please provide feedback on GitHub."; + + auto *box = + new QMessageBox(QMessageBox::Information, u"Chatterino - Overlay"_s, + welcomeText, QMessageBox::Ok, this); + box->open(); +} + +void OverlayWindow::addShortcuts() +{ + auto [seq, allOverlays] = toggleIntertiaShortcut(); + if (seq.isEmpty()) + { + return; + } + + auto *shortcut = new QShortcut(seq, this); + if (allOverlays) + { + QObject::connect(shortcut, &QShortcut::activated, this, [] { + getApp()->getWindows()->toggleAllOverlayInertia(); + }); + } + else + { + QObject::connect(shortcut, &QShortcut::activated, this, + &OverlayWindow::toggleInertia); + } +} + +void OverlayWindow::startInteraction() +{ + if (this->inert_) + { + return; + } + + this->interaction_.startInteraction(); + this->shortInteraction_.stop(); +} + +void OverlayWindow::startShortInteraction() +{ + if (this->inert_) + { + return; + } + + this->interaction_.startInteraction(); + this->shortInteraction_.start(); +} + +void OverlayWindow::endInteraction() +{ + this->interaction_.endInteraction(); +} + +void OverlayWindow::setInert(bool inert) +{ + if (this->inert_ == inert) + { + return; + } + + this->inert_ = inert; + + this->setWindowFlag(Qt::WindowTransparentForInput, inert); + if (this->isHidden()) + { + this->show(); + } + this->endInteraction(); + + if (inert) + { + if (this->channelView_.scrollbar()->isVisible()) + { + this->channelView_.scrollbar()->scrollToBottom(); + } + this->interaction_.hide(); + } + else + { + this->interaction_.show(); + } +} + +} // namespace chatterino diff --git a/src/widgets/OverlayWindow.hpp b/src/widgets/OverlayWindow.hpp new file mode 100644 index 00000000000..322dae468ab --- /dev/null +++ b/src/widgets/OverlayWindow.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include "common/Channel.hpp" +#include "widgets/helper/ChannelView.hpp" +#include "widgets/helper/OverlayInteraction.hpp" + +#include +#include +#include +#include + +#ifdef Q_OS_WIN +# include +#endif + +class QGraphicsDropShadowEffect; + +namespace chatterino { + +class OverlayWindow : public QWidget +{ + Q_OBJECT +public: + OverlayWindow(IndirectChannel channel); + ~OverlayWindow() override; + OverlayWindow(const OverlayWindow &) = delete; + OverlayWindow(OverlayWindow &&) = delete; + OverlayWindow &operator=(const OverlayWindow &) = delete; + OverlayWindow &operator=(OverlayWindow &&) = delete; + + void setOverrideCursor(const QCursor &cursor); + + bool isInert() const; + void setInert(bool inert); + void toggleInertia(); + +protected: +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + using NativeResult = qintptr; + using EnterEvent = QEnterEvent; +#else + using NativeResult = long; + using EnterEvent = QEvent; +#endif + + bool eventFilter(QObject *object, QEvent *event) override; + void enterEvent(EnterEvent *event) override; + void leaveEvent(QEvent *event) override; + +#ifdef Q_OS_WIN + bool nativeEvent(const QByteArray &eventType, void *message, + NativeResult *result) override; +#endif + +private: + void triggerFirstActivation(); + + void addShortcuts(); + + void startInteraction(); + void startShortInteraction(); + void endInteraction(); + + void applyTheme(); + +#ifdef Q_OS_WIN + void handleNCHITTEST(MSG *msg, NativeResult *result); + + HCURSOR sizeAllCursor_; +#endif + + IndirectChannel channel_; + pajlada::Signals::SignalHolder holder_; + + ChannelView channelView_; + QGraphicsDropShadowEffect *dropShadow_; + + bool inert_ = false; + + bool moving_ = false; + QPoint moveOrigin_; + + OverlayInteraction interaction_; + QTimer shortInteraction_; +}; + +} // namespace chatterino diff --git a/src/widgets/Scrollbar.cpp b/src/widgets/Scrollbar.cpp index 827ea645bec..4aef450363a 100644 --- a/src/widgets/Scrollbar.cpp +++ b/src/widgets/Scrollbar.cpp @@ -285,18 +285,21 @@ void Scrollbar::paintEvent(QPaintEvent * /*event*/) bool enableElevatedMessageHighlights = getSettings()->enableElevatedMessageHighlight; - this->thumbRect_.setX(xOffset); - - // mouse over thumb - if (this->mouseDownLocation_ == MouseLocation::InsideThumb) + if (this->showThumb_) { - painter.fillRect(this->thumbRect_, - this->theme->scrollbars.thumbSelected); - } - // mouse not over thumb - else - { - painter.fillRect(this->thumbRect_, this->theme->scrollbars.thumb); + this->thumbRect_.setX(xOffset); + + // mouse over thumb + if (this->mouseDownLocation_ == MouseLocation::InsideThumb) + { + painter.fillRect(this->thumbRect_, + this->theme->scrollbars.thumbSelected); + } + // mouse not over thumb + else + { + painter.fillRect(this->thumbRect_, this->theme->scrollbars.thumb); + } } // draw highlights @@ -449,6 +452,17 @@ void Scrollbar::updateScroll() this->update(); } +void Scrollbar::setShowThumb(bool showThumb) +{ + if (this->showThumb_ == showThumb) + { + return; + } + + this->showThumb_ = showThumb; + this->update(); +} + Scrollbar::MouseLocation Scrollbar::locationOfMouseEvent( QMouseEvent *event) const { diff --git a/src/widgets/Scrollbar.hpp b/src/widgets/Scrollbar.hpp index 08a843586fa..65fad101101 100644 --- a/src/widgets/Scrollbar.hpp +++ b/src/widgets/Scrollbar.hpp @@ -127,6 +127,8 @@ class Scrollbar : public BaseWidget /// unaffected by simultaneous shifts of minimum and maximum. qreal getRelativeCurrentValue() const; + void setShowThumb(bool showthumb); + // offset the desired value without breaking smooth scolling void offset(qreal value); pajlada::Signals::NoArgSignal &getCurrentValueChanged(); @@ -169,6 +171,7 @@ class Scrollbar : public BaseWidget boost::circular_buffer highlights_; bool atBottom_{false}; + bool showThumb_ = true; MouseLocation mouseOverLocation_ = MouseLocation::Outside; MouseLocation mouseDownLocation_ = MouseLocation::Outside; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 93be6ad91ba..066085030d0 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -362,7 +362,8 @@ ChannelView::ChannelView(InternalCtor /*tag*/, QWidget *parent, Split *split, this->queueUpdate(); }); - this->messageColors_.applyTheme(getTheme()); + this->messageColors_.applyTheme(getTheme(), this->isOverlay_, + getSettings()->overlayBackgroundOpacity); this->messagePreferences_.connectSettings(getSettings(), this->signalHolder_); } @@ -450,6 +451,11 @@ void ChannelView::initializeSignals() }); } +Scrollbar *ChannelView::scrollbar() +{ + return this->scrollBar_; +} + bool ChannelView::pausable() const { return pausable_; @@ -574,7 +580,19 @@ void ChannelView::themeChangedEvent() this->setupHighlightAnimationColors(); this->queueLayout(); - this->messageColors_.applyTheme(getTheme()); + this->messageColors_.applyTheme(getTheme(), this->isOverlay_, + getSettings()->overlayBackgroundOpacity); +} + +void ChannelView::updateColorTheme() +{ + this->themeChangedEvent(); +} + +void ChannelView::setIsOverlay(bool isOverlay) +{ + this->isOverlay_ = isOverlay; + this->themeChangedEvent(); } void ChannelView::setupHighlightAnimationColors() @@ -680,9 +698,15 @@ void ChannelView::layoutVisibleMessages( const auto &message = messages[i]; redrawRequired |= message->layout( - layoutWidth, this->scale(), - this->scale() * static_cast(this->devicePixelRatio()), - flags, this->bufferInvalidationQueued_); + { + .messageColors = this->messageColors_, + .flags = flags, + .width = layoutWidth, + .scale = this->scale(), + .imageScale = this->scale() * + static_cast(this->devicePixelRatio()), + }, + this->bufferInvalidationQueued_); y += message->getHeight(); } @@ -717,8 +741,14 @@ void ChannelView::updateScrollbar( auto *message = messages[i].get(); message->layout( - layoutWidth, this->scale(), - this->scale() * static_cast(this->devicePixelRatio()), flags, + { + .messageColors = this->messageColors_, + .flags = flags, + .width = layoutWidth, + .scale = this->scale(), + .imageScale = this->scale() * + static_cast(this->devicePixelRatio()), + }, false); h -= message->getHeight(); @@ -1486,7 +1516,7 @@ void ChannelView::paintEvent(QPaintEvent *event) QPainter painter(this); - painter.fillRect(rect(), this->theme->splits.background); + painter.fillRect(rect(), this->messageColors_.channelBackground); // draw messages this->drawMessages(painter, event->rect()); @@ -1705,10 +1735,16 @@ void ChannelView::wheelEvent(QWheelEvent *event) else { snapshot[i - 1]->layout( - this->getLayoutWidth(), this->scale(), - this->scale() * - static_cast(this->devicePixelRatio()), - this->getFlags(), false); + { + .messageColors = this->messageColors_, + .flags = this->getFlags(), + .width = this->getLayoutWidth(), + .scale = this->scale(), + .imageScale = + this->scale() * + static_cast(this->devicePixelRatio()), + }, + false); scrollFactor = 1; currentScrollLeft = snapshot[i - 1]->getHeight(); } @@ -1742,10 +1778,16 @@ void ChannelView::wheelEvent(QWheelEvent *event) else { snapshot[i + 1]->layout( - this->getLayoutWidth(), this->scale(), - this->scale() * - static_cast(this->devicePixelRatio()), - this->getFlags(), false); + { + .messageColors = this->messageColors_, + .flags = this->getFlags(), + .width = this->getLayoutWidth(), + .scale = this->scale(), + .imageScale = + this->scale() * + static_cast(this->devicePixelRatio()), + }, + false); scrollFactor = 1; currentScrollLeft = snapshot[i + 1]->getHeight(); diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 64683aca68c..7042107125a 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -202,6 +202,16 @@ class ChannelView final : public BaseWidget */ bool mayContainMessage(const MessagePtr &message); + void updateColorTheme(); + + /// @brief Adjusts the colors this view uses + /// + /// If @a isOverlay is true, the overlay colors (as specified in the theme) + /// will be used. Otherwise, regular message-colors will be used. + void setIsOverlay(bool isOverlay); + + Scrollbar *scrollbar(); + pajlada::Signals::Signal mouseDown; pajlada::Signals::NoArgSignal selectionChanged; pajlada::Signals::Signal tabHighlightRequested; @@ -377,6 +387,8 @@ class ChannelView final : public BaseWidget bool onlyUpdateEmotes_ = false; + bool isOverlay_ = false; + // Mouse event variables bool isLeftMouseDown_ = false; bool isRightMouseDown_ = false; diff --git a/src/widgets/helper/MessageView.cpp b/src/widgets/helper/MessageView.cpp index 6383ec5a6e5..ceb004c2795 100644 --- a/src/widgets/helper/MessageView.cpp +++ b/src/widgets/helper/MessageView.cpp @@ -97,8 +97,8 @@ void MessageView::paintEvent(QPaintEvent * /*event*/) void MessageView::themeChangedEvent() { - this->messageColors_.applyTheme(getTheme()); - this->messageColors_.regular = getTheme()->splits.input.background; + this->messageColors_.applyTheme(getTheme(), false, 255); + this->messageColors_.regularBg = getTheme()->splits.input.background; if (this->messageLayout_) { this->messageLayout_->invalidateBuffer(); @@ -120,9 +120,15 @@ void MessageView::layoutMessage() } bool updateRequired = this->messageLayout_->layout( - this->width_, this->scale(), - this->scale() * static_cast(this->devicePixelRatio()), - MESSAGE_FLAGS, false); + { + .messageColors = this->messageColors_, + .flags = MESSAGE_FLAGS, + .width = this->width_, + .scale = this->scale(), + .imageScale = + this->scale() * static_cast(this->devicePixelRatio()), + }, + false); if (updateRequired) { diff --git a/src/widgets/helper/OverlayInteraction.cpp b/src/widgets/helper/OverlayInteraction.cpp new file mode 100644 index 00000000000..d4a0d84feb7 --- /dev/null +++ b/src/widgets/helper/OverlayInteraction.cpp @@ -0,0 +1,123 @@ +#include "widgets/helper/OverlayInteraction.hpp" + +#include "common/Literals.hpp" +#include "widgets/OverlayWindow.hpp" + +#include + +namespace chatterino { + +using namespace literals; + +OverlayInteraction::OverlayInteraction(OverlayWindow *parent) + : QWidget(nullptr) + , interactAnimation_(this, "interactionProgress"_ba) + , window_(parent) +{ + this->interactAnimation_.setStartValue(0.0); + this->interactAnimation_.setEndValue(1.0); + + this->closeButton_.setButtonStyle(TitleBarButtonStyle::Close); + this->closeButton_.setScaleIndependantSize(46, 30); + this->closeButton_.hide(); + this->closeButton_.setCursor(Qt::PointingHandCursor); +} + +void OverlayInteraction::attach(QGridLayout *layout) +{ + layout->addWidget(this, 0, 0); + layout->addWidget(&this->closeButton_, 0, 0, Qt::AlignTop | Qt::AlignRight); + layout->setContentsMargins(0, 0, 0, 0); + + QObject::connect(&this->closeButton_, &TitleBarButton::leftClicked, + [this]() { + this->window_->close(); + }); +} + +QWidget *OverlayInteraction::closeButton() +{ + return &this->closeButton_; +} + +void OverlayInteraction::startInteraction() +{ + if (this->interacting_) + { + return; + } + + this->interacting_ = true; + if (this->interactAnimation_.state() != QPropertyAnimation::Stopped) + { + this->interactAnimation_.stop(); + } + this->interactAnimation_.setDirection(QPropertyAnimation::Forward); + this->interactAnimation_.setDuration(100); + this->interactAnimation_.start(); + this->window_->setOverrideCursor(Qt::SizeAllCursor); + this->closeButton_.show(); +} + +void OverlayInteraction::endInteraction() +{ + if (!this->interacting_) + { + return; + } + + this->interacting_ = false; + if (this->interactAnimation_.state() != QPropertyAnimation::Stopped) + { + this->interactAnimation_.stop(); + } + this->interactAnimation_.setDirection(QPropertyAnimation::Backward); + this->interactAnimation_.setDuration(200); + this->interactAnimation_.start(); + this->window_->setOverrideCursor(Qt::ArrowCursor); + this->closeButton_.hide(); +} + +bool OverlayInteraction::isInteracting() const +{ + return this->interacting_; +} + +void OverlayInteraction::paintEvent(QPaintEvent * /*event*/) +{ + QPainter painter(this); + QColor highlightColor( + 255, 255, 255, std::max(int(255.0 * this->interactionProgress()), 50)); + + painter.setPen({highlightColor, 2}); + // outline + auto bounds = this->rect(); + painter.drawRect(bounds); + + if (this->interactionProgress() <= 0.0) + { + return; + } + + highlightColor.setAlpha(highlightColor.alpha() / 4); + painter.setBrush(highlightColor); + painter.setPen(Qt::transparent); + + // close button + auto buttonSize = this->closeButton_.size(); + painter.drawRect( + QRect{bounds.topRight() - QPoint{buttonSize.width(), 0}, buttonSize}); +} + +double OverlayInteraction::interactionProgress() const +{ + return this->interactionProgress_; +} + +void OverlayInteraction::setInteractionProgress(double progress) +{ + this->interactionProgress_ = progress; + this->update(); +} + +} // namespace chatterino diff --git a/src/widgets/helper/OverlayInteraction.hpp b/src/widgets/helper/OverlayInteraction.hpp new file mode 100644 index 00000000000..25be810adfa --- /dev/null +++ b/src/widgets/helper/OverlayInteraction.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "widgets/helper/TitlebarButton.hpp" + +#include +#include + +class QGridLayout; + +namespace chatterino { + +class OverlayWindow; +class OverlayInteraction : public QWidget +{ + Q_OBJECT +public: + OverlayInteraction(OverlayWindow *parent); + + void attach(QGridLayout *layout); + + QWidget *closeButton(); + + void startInteraction(); + void endInteraction(); + + bool isInteracting() const; + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + Q_PROPERTY(double interactionProgress READ interactionProgress WRITE + setInteractionProgress) + + TitleBarButton closeButton_; + + double interactionProgress() const; + void setInteractionProgress(double progress); + + bool interacting_ = false; + double interactionProgress_ = 0.0; + QPropertyAnimation interactAnimation_; + + OverlayWindow *window_; +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 01ae9515bd5..7103fb52739 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -969,6 +969,40 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addCheckbox("Use custom FrankerFaceZ VIP badges", s.useCustomFfzVipBadges); + layout.addSubtitle("Overlay"); + layout.addIntInput( + "Background opacity (0-255)", s.overlayBackgroundOpacity, 0, 255, 1, + "Controls the opacity of the (possibly alternating) background behind " + "messages. The color is set through the current theme. 255 corresponds " + "to a fully opaque background."); + layout.addCheckbox("Enable Shadow", s.enableOverlayShadow, false, + "Enables a drop shadow on the overlay. This will use " + "more processing power."); + layout.addIntInput("Shadow opacity (0-255)", s.overlayShadowOpacity, 0, 255, + 1, + "Controls the opacity of the added drop shadow. 255 " + "corresponds to a fully opaque shadow."); + layout.addColorButton("Shadow color", + QColor(getSettings()->overlayShadowColor.getValue()), + getSettings()->overlayShadowColor); + layout + .addIntInput("Shadow radius", s.overlayShadowRadius, 0, 40, 1, + "Controls how far the shadow is spread (the blur " + "radius) in device-independent pixels.") + ->setSuffix("dp"); + layout + .addIntInput("Shadow offset x", s.overlayShadowOffsetX, -20, 20, 1, + "Controls how far the shadow is offset on the x axis in " + "device-independent pixels. A negative value offsets to " + "the left and a positive to the right.") + ->setSuffix("dp"); + layout + .addIntInput("Shadow offset y", s.overlayShadowOffsetY, -20, 20, 1, + "Controls how far the shadow is offset on the y axis in " + "device-independent pixels. A negative value offsets to " + "the top and a positive to the bottom.") + ->setSuffix("dp"); + layout.addSubtitle("Miscellaneous"); if (supportsIncognitoLinks()) diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 641a87a1018..2356653d628 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -33,6 +33,7 @@ #include "widgets/helper/ResizingTextEdit.hpp" #include "widgets/helper/SearchPopup.hpp" #include "widgets/Notebook.hpp" +#include "widgets/OverlayWindow.hpp" #include "widgets/Scrollbar.hpp" #include "widgets/splits/DraggedSplit.hpp" #include "widgets/splits/SplitContainer.hpp" @@ -765,6 +766,47 @@ void Split::addShortcuts() } return ""; }}, + {"popupOverlay", + [this](const auto &) -> QString { + this->showOverlayWindow(); + return {}; + }}, + {"toggleOverlayInertia", + [this](const auto &args) -> QString { + if (args.empty()) + { + return "No arguments provided to toggleOverlayInertia " + "(expected one)"; + } + const auto &arg = args.front(); + + if (arg == "this") + { + if (this->overlayWindow_) + { + this->overlayWindow_->toggleInertia(); + } + return {}; + } + if (arg == "thisOrAll") + { + if (this->overlayWindow_) + { + this->overlayWindow_->toggleInertia(); + } + else + { + getApp()->getWindows()->toggleAllOverlayInertia(); + } + return {}; + } + if (arg == "all") + { + getApp()->getWindows()->toggleAllOverlayInertia(); + return {}; + } + return {}; + }}, }; this->shortcuts_ = getApp()->getHotkeys()->shortcutsForCategory( @@ -1104,6 +1146,20 @@ void Split::popup() window.show(); } +OverlayWindow *Split::overlayWindow() +{ + return this->overlayWindow_.data(); +} + +void Split::showOverlayWindow() +{ + if (!this->overlayWindow_) + { + this->overlayWindow_ = new OverlayWindow(this->getIndirectChannel()); + } + this->overlayWindow_->show(); +} + void Split::clear() { this->view_->clearMessages(); diff --git a/src/widgets/splits/Split.hpp b/src/widgets/splits/Split.hpp index ab6322d5cdd..6d604bdf8d5 100644 --- a/src/widgets/splits/Split.hpp +++ b/src/widgets/splits/Split.hpp @@ -21,6 +21,7 @@ class SplitInput; class SplitContainer; class SplitOverlay; class SelectChannelDialog; +class OverlayWindow; // Each ChatWidget consists of three sub-elements that handle their own part of // the chat widget: ChatWidgetHeader @@ -80,6 +81,8 @@ class Split : public BaseWidget // This is called on window focus lost void unpause(); + OverlayWindow *overlayWindow(); + static pajlada::Signals::Signal modifierStatusChanged; static Qt::KeyboardModifiers modifierStatus; @@ -158,6 +161,8 @@ class Split : public BaseWidget SplitInput *const input_; SplitOverlay *const overlay_; + QPointer overlayWindow_; + QPointer selectChannelDialog_; pajlada::Signals::Connection channelIDChangedConnection_; @@ -179,6 +184,7 @@ public slots: void explainMoving(); void explainSplitting(); void popup(); + void showOverlayWindow(); void clear(); void openInBrowser(); void openModViewInBrowser(); diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index 09725879b23..b82f817be66 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -390,6 +390,9 @@ std::unique_ptr SplitHeader::createMainMenu() menu->addAction( "Popup", this->split_, &Split::popup, h->getDisplaySequence(HotkeyCategory::Window, "popup", {{"split"}})); + menu->addAction( + "Popup overlay", this->split_, &Split::showOverlayWindow, + h->getDisplaySequence(HotkeyCategory::Split, "popupOverlay")); menu->addAction( "Search", this->split_, [this] { diff --git a/tests/src/MessageLayout.cpp b/tests/src/MessageLayout.cpp index 06fb4db5932..72b50f872de 100644 --- a/tests/src/MessageLayout.cpp +++ b/tests/src/MessageLayout.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "controllers/accounts/AccountController.hpp" +#include "messages/layouts/MessageLayoutContext.hpp" #include "messages/MessageBuilder.hpp" #include "messages/MessageElement.hpp" #include "mocks/BaseApplication.hpp" @@ -55,7 +56,16 @@ class MessageLayoutTest builder.append( std::make_unique(text, MessageElementFlag::Text)); this->layout = std::make_unique(builder.release()); - this->layout->layout(WIDTH, 1, 1, MessageElementFlag::Text, false); + MessageColors colors; + this->layout->layout( + { + .messageColors = colors, + .flags = MessageElementFlag::Text, + .width = WIDTH, + .scale = 1, + .imageScale = 1, + }, + false); } MockApplication mockApplication; diff --git a/tests/src/MessageLayoutContainer.cpp b/tests/src/MessageLayoutContainer.cpp index b0aa4f30e4a..2f1b810fd02 100644 --- a/tests/src/MessageLayoutContainer.cpp +++ b/tests/src/MessageLayoutContainer.cpp @@ -2,6 +2,7 @@ #include "common/Literals.hpp" #include "messages/Emote.hpp" +#include "messages/layouts/MessageLayoutContext.hpp" #include "messages/layouts/MessageLayoutElement.hpp" #include "messages/Message.hpp" #include "messages/MessageElement.hpp" @@ -107,16 +108,25 @@ TEST_P(MessageLayoutContainerTest, RtlReordering) { auto [inputText, expected, expectedDirection] = GetParam(); MessageLayoutContainer container; - container.beginLayout(10000, 1.0F, 1.0F, {MessageFlag::Collapsed}); + MessageLayoutContext ctx{ + .messageColors = {}, + .flags = + { + MessageElementFlag::Text, + MessageElementFlag::Username, + MessageElementFlag::TwitchEmote, + }, + .width = 10000, + .scale = 1.0F, + .imageScale = 1.0F, + }; + container.beginLayout(ctx.width, ctx.scale, ctx.imageScale, + {MessageFlag::Collapsed}); auto elements = makeElements(inputText); for (const auto &element : elements) { - element->addToContainer(container, { - MessageElementFlag::Text, - MessageElementFlag::Username, - MessageElementFlag::TwitchEmote, - }); + element->addToContainer(container, ctx); } container.endLayout(); ASSERT_EQ(container.line_, 1) << "unexpected linebreak";