Show context menu anywhere in MessageLayout when applicable (#3566)

Co-authored-by: James Upjohn <jammehcow@jammehcow.co.nz>
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Infinitay 2022-02-12 19:46:39 -05:00 committed by GitHub
parent fb9c3ad42b
commit 5978ed8b1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 217 additions and 120 deletions

View file

@ -53,6 +53,7 @@
- Minor: Removed timestamp from AutoMod messages. (#3503) - Minor: Removed timestamp from AutoMod messages. (#3503)
- Minor: Added ability to copy message ID with `Shift + Right Click`. (#3481) - Minor: Added ability to copy message ID with `Shift + Right Click`. (#3481)
- Minor: Colorize the entire split header when focused. (#3379) - 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) - Minor: Make Tab Layout setting only accept predefined values (#3564)
- Bugfix: Fix Split Input hotkeys not being available when input is hidden (#3362) - Bugfix: Fix Split Input hotkeys not being available when input is hidden (#3362)
- Bugfix: Fixed colored usernames sometimes not working. (#3170) - Bugfix: Fixed colored usernames sometimes not working. (#3170)

View file

@ -1762,11 +1762,6 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event)
} }
} }
if (hoverLayoutElement == nullptr)
{
return;
}
// handle the click // handle the click
this->handleMouseClick(event, hoverLayoutElement, layout); this->handleMouseClick(event, hoverLayoutElement, layout);
@ -1790,7 +1785,12 @@ void ChannelView::handleMouseClick(QMouseEvent *event,
this->queueLayout(); this->queueLayout();
} }
auto &link = hoveredElement->getLink(); if (hoveredElement == nullptr)
{
return;
}
const auto &link = hoveredElement->getLink();
if (!getSettings()->linksDoubleClickOnly) if (!getSettings()->linksDoubleClickOnly)
{ {
this->handleLinkClick(event, link, layout.get()); this->handleLinkClick(event, link, layout.get());
@ -1812,28 +1812,39 @@ void ChannelView::handleMouseClick(QMouseEvent *event,
} }
}; };
auto &link = hoveredElement->getLink(); if (hoveredElement != nullptr)
if (link.type == Link::UserInfo)
{ {
const bool commaMention = getSettings()->mentionUsersWithComma; const auto &link = hoveredElement->getLink();
const bool isFirstWord =
split && split->getInput().isEditFirstWord(); if (link.type == Link::UserInfo)
auto userMention = {
formatUserMention(link.value, isFirstWord, commaMention); const bool commaMention =
insertText("@" + userMention + " "); getSettings()->mentionUsersWithComma;
} const bool isFirstWord =
else if (link.type == Link::UserWhisper) split && split->getInput().isEditFirstWord();
{ auto userMention = formatUserMention(
insertText("/w " + link.value + " "); link.value, isFirstWord, commaMention);
} insertText("@" + userMention + " ");
else return;
{ }
this->addContextMenuItems(hoveredElement, layout, event);
if (link.type == Link::UserWhisper)
{
insertText("/w " + link.value + " ");
return;
}
} }
this->addContextMenuItems(hoveredElement, layout, event);
} }
break; break;
case Qt::MiddleButton: { case Qt::MiddleButton: {
auto &link = hoveredElement->getLink(); if (hoveredElement == nullptr)
{
return;
}
const auto &link = hoveredElement->getLink();
if (!getSettings()->linksDoubleClickOnly) if (!getSettings()->linksDoubleClickOnly)
{ {
this->handleLinkClick(event, link, layout.get()); this->handleLinkClick(event, link, layout.get());
@ -1848,9 +1859,6 @@ void ChannelView::addContextMenuItems(
const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout, const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout,
QMouseEvent *event) QMouseEvent *event)
{ {
const auto &creator = hoveredElement->getCreator();
auto creatorFlags = creator.getFlags();
static QMenu *previousMenu = nullptr; static QMenu *previousMenu = nullptr;
if (previousMenu != nullptr) if (previousMenu != nullptr)
{ {
@ -1861,12 +1869,45 @@ void ChannelView::addContextMenuItems(
auto menu = new QMenu; auto menu = new QMenu;
previousMenu = menu; 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 // Badge actions
if (creatorFlags.hasAny({MessageElementFlag::Badges})) if (creatorFlags.hasAny({MessageElementFlag::Badges}))
{ {
if (auto badgeElement = dynamic_cast<const BadgeElement *>(&creator)) if (auto badgeElement = dynamic_cast<const BadgeElement *>(&creator))
{
addEmoteContextMenuItems(*badgeElement->getEmote(), creatorFlags, addEmoteContextMenuItems(*badgeElement->getEmote(), creatorFlags,
*menu); menu);
}
} }
// Emote actions // Emote actions
@ -1874,48 +1915,68 @@ void ChannelView::addContextMenuItems(
{MessageElementFlag::EmoteImages, MessageElementFlag::EmojiImage})) {MessageElementFlag::EmoteImages, MessageElementFlag::EmojiImage}))
{ {
if (auto emoteElement = dynamic_cast<const EmoteElement *>(&creator)) if (auto emoteElement = dynamic_cast<const EmoteElement *>(&creator))
{
addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags, addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags,
*menu); menu);
}
} }
// add seperator // 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 // 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; menu.addAction("Open link incognito", [url] {
openLinkIncognito(url);
// open link
menu->addAction("Open link", [url] {
QDesktopServices::openUrl(QUrl(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 // Copy actions
if (!this->selection_.isEmpty()) if (!this->selection_.isEmpty())
{ {
menu->addAction("Copy selection", [this] { menu.addAction("Copy selection", [this] {
crossPlatformCopy(this->getSelectedText()); crossPlatformCopy(this->getSelectedText());
}); });
} }
menu->addAction("Copy message", [layout] { menu.addAction("Copy message", [layout] {
QString copyString; QString copyString;
layout->addSelectionText(copyString, 0, INT_MAX, layout->addSelectionText(copyString, 0, INT_MAX,
CopyMode::OnlyTextAndEmotes); CopyMode::OnlyTextAndEmotes);
@ -1923,82 +1984,102 @@ void ChannelView::addContextMenuItems(
crossPlatformCopy(copyString); crossPlatformCopy(copyString);
}); });
menu->addAction("Copy full message", [layout] { menu.addAction("Copy full message", [layout] {
QString copyString; QString copyString;
layout->addSelectionText(copyString); layout->addSelectionText(copyString);
crossPlatformCopy(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\/)?(?<username>[a-z0-9_]{3,}))",
QRegularExpression::CaseInsensitiveOption);
static QSet<QString> 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\/)?(?<username>[a-z0-9_]{3,}))",
QRegularExpression::CaseInsensitiveOption);
static QSet<QString> 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) void ChannelView::mouseDoubleClickEvent(QMouseEvent *event)
{ {
std::shared_ptr<MessageLayout> layout; std::shared_ptr<MessageLayout> layout;

View file

@ -157,10 +157,25 @@ private:
const QPoint &relativePos, int &wordStart, int &wordEnd); const QPoint &relativePos, int &wordStart, int &wordEnd);
void handleMouseClick(QMouseEvent *event, void handleMouseClick(QMouseEvent *event,
const MessageLayoutElement *hoverLayoutElement, const MessageLayoutElement *hoveredElement,
MessageLayoutPtr layout); MessageLayoutPtr layout);
void addContextMenuItems(const MessageLayoutElement *hoveredElement, void addContextMenuItems(const MessageLayoutElement *hoveredElement,
MessageLayoutPtr layout, QMouseEvent *event); 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; int getLayoutWidth() const;
void updatePauses(); void updatePauses();
void unpaused(); void unpaused();