[Chat] Render cockatrice://joingame links in chat as clickable buttons

Words starting with cockatrice:// become button-style anchors labelled with
the game description, id and server (falling back to id + server for links
built without a description). The description is spliced via the multi-arg
arg() overloads so a title containing "%…" cannot corrupt the label.

Keyboard link access is enabled so the anchors are reachable without a mouse.
This commit is contained in:
Lukas Brübach 2026-08-16 20:50:58 +02:00
parent 7c134efd29
commit 2076ac734d
2 changed files with 48 additions and 1 deletions

View file

@ -14,6 +14,8 @@
#include <QMouseEvent>
#include <QScrollBar>
#include <QTimer>
#include <QUrl>
#include <QUrlQuery>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/settings/chat_settings.h>
@ -49,7 +51,7 @@ ChatView::ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _sho
viewport()->setCursor(Qt::IBeamCursor);
setReadOnly(true);
setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard);
setOpenLinks(false);
connect(this, &ChatView::anchorClicked, this, &ChatView::openLink);
@ -219,6 +221,44 @@ void ChatView::appendUrlTag(QTextCursor &cursor, QString url)
cursor.setCharFormat(oldFormat);
}
void ChatView::appendGameLinkTag(QTextCursor &cursor, const QString &url)
{
const QUrl gameUrl(url);
const QUrlQuery query(gameUrl);
const QString hostname = query.queryItemValue("hostname");
const QString description = query.queryItemValue("game");
const int gameId = query.queryItemValue("gameid").toInt();
QString label;
if (gameId > 0 && !hostname.isEmpty()) {
// Links built before the description was embedded stay readable: the
// id + server fallback below is identical to the old anchor text.
if (!description.isEmpty()) {
// Multi-arg .arg() replaces all placeholders in a single pass, so a
// description containing "%…" cannot corrupt later placeholders.
label = tr("Join game \"%1\" (#%2) on %3").arg(description, QString::number(gameId), hostname);
} else {
label = tr("Join game #%1 on %2").arg(QString::number(gameId), hostname);
}
} else {
label = tr("Join game");
}
QTextCharFormat oldFormat = cursor.charFormat();
QTextCharFormat gameLinkFormat = oldFormat;
gameLinkFormat.setForeground(linkColor);
gameLinkFormat.setFontWeight(QFont::Bold);
gameLinkFormat.setAnchor(true);
gameLinkFormat.setAnchorHref(url);
QColor background = palette().highlight().color();
background.setAlpha(40);
gameLinkFormat.setBackground(background);
cursor.setCharFormat(gameLinkFormat);
cursor.insertText(label);
cursor.setCharFormat(oldFormat);
}
void ChatView::appendMessage(QString message,
RoomMessageTypeFlags messageType,
const ServerInfo_User &userInfo,
@ -503,6 +543,12 @@ void ChatView::checkWord(QTextCursor &cursor, QString &message)
}
}
if (fullWordUpToSpaceOrEnd.startsWith("cockatrice://", Qt::CaseInsensitive)) {
appendGameLinkTag(cursor, fullWordUpToSpaceOrEnd);
cursor.insertText(rest, defaultFormat);
return;
}
// check word mentions
for (const QString &word : highlightedWords) {
if (fullWordUpToSpaceOrEnd.compare(word, Qt::CaseInsensitive) == 0) {

View file

@ -71,6 +71,7 @@ private:
void scrollToBottom();
void appendCardTag(QTextCursor &cursor, const QString &cardName);
void appendUrlTag(QTextCursor &cursor, QString url);
void appendGameLinkTag(QTextCursor &cursor, const QString &url);
static QColor getCustomMentionColor();
static QColor getCustomHighlightColor();
void showSystemPopup(const QString &userName);