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: 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)

View file

@ -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<const BadgeElement *>(&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<const EmoteElement *>(&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\/)?(?<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)
{
std::shared_ptr<MessageLayout> layout;

View file

@ -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();