From 5978ed8b1fb9b2bea6b82a6a366114b5e22a9d50 Mon Sep 17 00:00:00 2001 From: Infinitay Date: Sat, 12 Feb 2022 19:46:39 -0500 Subject: [PATCH] Show context menu anywhere in MessageLayout when applicable (#3566) Co-authored-by: James Upjohn Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/widgets/helper/ChannelView.cpp | 319 ++++++++++++++++++----------- src/widgets/helper/ChannelView.hpp | 17 +- 3 files changed, 217 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e1e0ac1..31293fdae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ - Minor: Removed timestamp from AutoMod messages. (#3503) - Minor: Added ability to copy message ID with `Shift + Right Click`. (#3481) - Minor: Colorize the entire split header when focused. (#3379) +- Minor: Show right click context menu anywhere within a message's line. (#3566) - Minor: Make Tab Layout setting only accept predefined values (#3564) - Bugfix: Fix Split Input hotkeys not being available when input is hidden (#3362) - Bugfix: Fixed colored usernames sometimes not working. (#3170) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 85f215a3c..0ff30e598 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1762,11 +1762,6 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) } } - if (hoverLayoutElement == nullptr) - { - return; - } - // handle the click this->handleMouseClick(event, hoverLayoutElement, layout); @@ -1790,7 +1785,12 @@ void ChannelView::handleMouseClick(QMouseEvent *event, this->queueLayout(); } - auto &link = hoveredElement->getLink(); + if (hoveredElement == nullptr) + { + return; + } + + const auto &link = hoveredElement->getLink(); if (!getSettings()->linksDoubleClickOnly) { this->handleLinkClick(event, link, layout.get()); @@ -1812,28 +1812,39 @@ void ChannelView::handleMouseClick(QMouseEvent *event, } }; - auto &link = hoveredElement->getLink(); - if (link.type == Link::UserInfo) + if (hoveredElement != nullptr) { - const bool commaMention = getSettings()->mentionUsersWithComma; - const bool isFirstWord = - split && split->getInput().isEditFirstWord(); - auto userMention = - formatUserMention(link.value, isFirstWord, commaMention); - insertText("@" + userMention + " "); - } - else if (link.type == Link::UserWhisper) - { - insertText("/w " + link.value + " "); - } - else - { - this->addContextMenuItems(hoveredElement, layout, event); + const auto &link = hoveredElement->getLink(); + + if (link.type == Link::UserInfo) + { + const bool commaMention = + getSettings()->mentionUsersWithComma; + const bool isFirstWord = + split && split->getInput().isEditFirstWord(); + auto userMention = formatUserMention( + link.value, isFirstWord, commaMention); + insertText("@" + userMention + " "); + return; + } + + if (link.type == Link::UserWhisper) + { + insertText("/w " + link.value + " "); + return; + } } + + this->addContextMenuItems(hoveredElement, layout, event); } break; case Qt::MiddleButton: { - auto &link = hoveredElement->getLink(); + if (hoveredElement == nullptr) + { + return; + } + + const auto &link = hoveredElement->getLink(); if (!getSettings()->linksDoubleClickOnly) { this->handleLinkClick(event, link, layout.get()); @@ -1848,9 +1859,6 @@ void ChannelView::addContextMenuItems( const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout, QMouseEvent *event) { - const auto &creator = hoveredElement->getCreator(); - auto creatorFlags = creator.getFlags(); - static QMenu *previousMenu = nullptr; if (previousMenu != nullptr) { @@ -1861,12 +1869,45 @@ void ChannelView::addContextMenuItems( auto menu = new QMenu; previousMenu = menu; + // Add image options if the element clicked contains an image (e.g. a badge or an emote) + this->addImageContextMenuItems(hoveredElement, layout, event, *menu); + + // Add link options if the element clicked contains a link + this->addLinkContextMenuItems(hoveredElement, layout, event, *menu); + + // Add message options + this->addMessageContextMenuItems(hoveredElement, layout, event, *menu); + + // Add Twitch-specific link options if the element clicked contains a link detected as a Twitch username + this->addTwitchLinkContextMenuItems(hoveredElement, layout, event, *menu); + + // Add hidden options (e.g. copy message ID) if the user held down Shift + this->addHiddenContextMenuItems(hoveredElement, layout, event, *menu); + + menu->popup(QCursor::pos()); + menu->raise(); +} + +void ChannelView::addImageContextMenuItems( + const MessageLayoutElement *hoveredElement, MessageLayoutPtr /*layout*/, + QMouseEvent * /*event*/, QMenu &menu) +{ + if (hoveredElement == nullptr) + { + return; + } + + const auto &creator = hoveredElement->getCreator(); + auto creatorFlags = creator.getFlags(); + // Badge actions if (creatorFlags.hasAny({MessageElementFlag::Badges})) { if (auto badgeElement = dynamic_cast(&creator)) + { addEmoteContextMenuItems(*badgeElement->getEmote(), creatorFlags, - *menu); + menu); + } } // Emote actions @@ -1874,48 +1915,68 @@ void ChannelView::addContextMenuItems( {MessageElementFlag::EmoteImages, MessageElementFlag::EmojiImage})) { if (auto emoteElement = dynamic_cast(&creator)) + { addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags, - *menu); + menu); + } } // add seperator - if (!menu->actions().empty()) + if (!menu.actions().empty()) { - menu->addSeparator(); + menu.addSeparator(); + } +} + +void ChannelView::addLinkContextMenuItems( + const MessageLayoutElement *hoveredElement, MessageLayoutPtr /*layout*/, + QMouseEvent * /*event*/, QMenu &menu) +{ + if (hoveredElement == nullptr) + { + return; + } + + const auto &link = hoveredElement->getLink(); + + if (link.type != Link::Url) + { + return; } // Link copy - if (hoveredElement->getLink().type == Link::Url) + QString url = link.value; + + // open link + menu.addAction("Open link", [url] { + QDesktopServices::openUrl(QUrl(url)); + }); + // open link default + if (supportsIncognitoLinks()) { - QString url = hoveredElement->getLink().value; - - // open link - menu->addAction("Open link", [url] { - QDesktopServices::openUrl(QUrl(url)); + menu.addAction("Open link incognito", [url] { + openLinkIncognito(url); }); - // open link default - if (supportsIncognitoLinks()) - { - menu->addAction("Open link incognito", [url] { - openLinkIncognito(url); - }); - } - menu->addAction("Copy link", [url] { - crossPlatformCopy(url); - }); - - menu->addSeparator(); } + menu.addAction("Copy link", [url] { + crossPlatformCopy(url); + }); + menu.addSeparator(); +} +void ChannelView::addMessageContextMenuItems( + const MessageLayoutElement * /*hoveredElement*/, MessageLayoutPtr layout, + QMouseEvent * /*event*/, QMenu &menu) +{ // Copy actions if (!this->selection_.isEmpty()) { - menu->addAction("Copy selection", [this] { + menu.addAction("Copy selection", [this] { crossPlatformCopy(this->getSelectedText()); }); } - menu->addAction("Copy message", [layout] { + menu.addAction("Copy message", [layout] { QString copyString; layout->addSelectionText(copyString, 0, INT_MAX, CopyMode::OnlyTextAndEmotes); @@ -1923,82 +1984,102 @@ void ChannelView::addContextMenuItems( crossPlatformCopy(copyString); }); - menu->addAction("Copy full message", [layout] { + menu.addAction("Copy full message", [layout] { QString copyString; layout->addSelectionText(copyString); crossPlatformCopy(copyString); }); - - // If is a link to a Twitch user/stream - if (hoveredElement->getLink().type == Link::Url) - { - static QRegularExpression twitchChannelRegex( - R"(^(?:https?:\/\/)?(?:www\.|go\.)?twitch\.tv\/(?:popout\/)?(?[a-z0-9_]{3,}))", - QRegularExpression::CaseInsensitiveOption); - static QSet ignoredUsernames{ - "directory", // - "downloads", // - "drops", // - "friends", // - "inventory", // - "jobs", // - "login", // - "messages", // - "payments", // - "profile", // - "security", // - "settings", // - "signup", // - "subscriptions", // - "turbo", // - "videos", // - "wallet", // - }; - - auto twitchMatch = - twitchChannelRegex.match(hoveredElement->getLink().value); - auto twitchUsername = twitchMatch.captured("username"); - if (!twitchUsername.isEmpty() && - !ignoredUsernames.contains(twitchUsername)) - { - menu->addSeparator(); - menu->addAction("Open in new split", [twitchUsername, this] { - this->openChannelIn.invoke(twitchUsername, - FromTwitchLinkOpenChannelIn::Split); - }); - menu->addAction("Open in new tab", [twitchUsername, this] { - this->openChannelIn.invoke(twitchUsername, - FromTwitchLinkOpenChannelIn::Tab); - }); - - menu->addSeparator(); - menu->addAction("Open player in browser", [twitchUsername, this] { - this->openChannelIn.invoke( - twitchUsername, FromTwitchLinkOpenChannelIn::BrowserPlayer); - }); - menu->addAction("Open in streamlink", [twitchUsername, this] { - this->openChannelIn.invoke( - twitchUsername, FromTwitchLinkOpenChannelIn::Streamlink); - }); - } - } - - if (event->modifiers() == Qt::ShiftModifier && - !layout->getMessage()->id.isEmpty()) - { - menu->addAction("Copy message ID", - [messageID = layout->getMessage()->id] { - crossPlatformCopy(messageID); - }); - } - - menu->popup(QCursor::pos()); - menu->raise(); - - return; } +void ChannelView::addTwitchLinkContextMenuItems( + const MessageLayoutElement *hoveredElement, MessageLayoutPtr /*layout*/, + QMouseEvent * /*event*/, QMenu &menu) +{ + if (hoveredElement == nullptr) + { + return; + } + + const auto &link = hoveredElement->getLink(); + + if (link.type != Link::Url) + { + return; + } + + static QRegularExpression twitchChannelRegex( + R"(^(?:https?:\/\/)?(?:www\.|go\.)?twitch\.tv\/(?:popout\/)?(?[a-z0-9_]{3,}))", + QRegularExpression::CaseInsensitiveOption); + static QSet ignoredUsernames{ + "directory", // + "downloads", // + "drops", // + "friends", // + "inventory", // + "jobs", // + "login", // + "messages", // + "payments", // + "profile", // + "security", // + "settings", // + "signup", // + "subscriptions", // + "turbo", // + "videos", // + "wallet", // + }; + + auto twitchMatch = twitchChannelRegex.match(link.value); + auto twitchUsername = twitchMatch.captured("username"); + if (!twitchUsername.isEmpty() && !ignoredUsernames.contains(twitchUsername)) + { + menu.addSeparator(); + menu.addAction("Open in new split", [twitchUsername, this] { + this->openChannelIn.invoke(twitchUsername, + FromTwitchLinkOpenChannelIn::Split); + }); + menu.addAction("Open in new tab", [twitchUsername, this] { + this->openChannelIn.invoke(twitchUsername, + FromTwitchLinkOpenChannelIn::Tab); + }); + + menu.addSeparator(); + menu.addAction("Open player in browser", [twitchUsername, this] { + this->openChannelIn.invoke( + twitchUsername, FromTwitchLinkOpenChannelIn::BrowserPlayer); + }); + menu.addAction("Open in streamlink", [twitchUsername, this] { + this->openChannelIn.invoke(twitchUsername, + FromTwitchLinkOpenChannelIn::Streamlink); + }); + } +} + +void ChannelView::addHiddenContextMenuItems( + const MessageLayoutElement * /*hoveredElement*/, MessageLayoutPtr layout, + QMouseEvent *event, QMenu &menu) +{ + if (!layout) + { + return; + } + + if (event->modifiers() != Qt::ShiftModifier) + { + // NOTE: We currently require the modifier to be ONLY shift - we might want to check if shift is among the modifiers instead + return; + } + + if (!layout->getMessage()->id.isEmpty()) + { + menu.addAction("Copy message ID", + [messageID = layout->getMessage()->id] { + crossPlatformCopy(messageID); + }); + } +} void ChannelView::mouseDoubleClickEvent(QMouseEvent *event) { std::shared_ptr layout; diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 4b4c7afe9..009b35b74 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -157,10 +157,25 @@ private: const QPoint &relativePos, int &wordStart, int &wordEnd); void handleMouseClick(QMouseEvent *event, - const MessageLayoutElement *hoverLayoutElement, + const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout); void addContextMenuItems(const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout, QMouseEvent *event); + void addImageContextMenuItems(const MessageLayoutElement *hoveredElement, + MessageLayoutPtr layout, QMouseEvent *event, + QMenu &menu); + void addLinkContextMenuItems(const MessageLayoutElement *hoveredElement, + MessageLayoutPtr layout, QMouseEvent *event, + QMenu &menu); + void addMessageContextMenuItems(const MessageLayoutElement *hoveredElement, + MessageLayoutPtr layout, QMouseEvent *event, + QMenu &menu); + void addTwitchLinkContextMenuItems( + const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout, + QMouseEvent *event, QMenu &menu); + void addHiddenContextMenuItems(const MessageLayoutElement *hoveredElement, + MessageLayoutPtr layout, QMouseEvent *event, + QMenu &menu); int getLayoutWidth() const; void updatePauses(); void unpaused();