Skip to content

Commit d406511

Browse files
Merge branch 'development' into sentry-crash-reporting
2 parents 83036a2 + 8e2882e commit d406511

File tree

8 files changed

+385
-2
lines changed

8 files changed

+385
-2
lines changed

src/Host.cpp

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3347,6 +3347,66 @@ bool Host::setClickthrough(const QString& name, bool clickthrough)
33473347
return false;
33483348
}
33493349

3350+
bool Host::setLabelStyleSheet(const QString& name, const QString& styleSheet)
3351+
{
3352+
if (!mpConsole) {
3353+
return false;
3354+
}
3355+
3356+
auto pL = mpConsole->mLabelMap.value(name);
3357+
if (pL) {
3358+
pL->setStyleSheet(styleSheet);
3359+
return true;
3360+
}
3361+
3362+
return false;
3363+
}
3364+
3365+
bool Host::setLinkStyle(const QString& name, const QString& linkColor, const QString& linkVisitedColor, bool underline)
3366+
{
3367+
if (!mpConsole) {
3368+
return false;
3369+
}
3370+
3371+
auto pL = mpConsole->mLabelMap.value(name);
3372+
if (pL) {
3373+
pL->setLinkStyle(linkColor, linkVisitedColor, underline);
3374+
return true;
3375+
}
3376+
3377+
return false;
3378+
}
3379+
3380+
bool Host::resetLinkStyle(const QString& name)
3381+
{
3382+
if (!mpConsole) {
3383+
return false;
3384+
}
3385+
3386+
auto pL = mpConsole->mLabelMap.value(name);
3387+
if (pL) {
3388+
pL->resetLinkStyle();
3389+
return true;
3390+
}
3391+
3392+
return false;
3393+
}
3394+
3395+
bool Host::clearVisitedLinks(const QString& name)
3396+
{
3397+
if (!mpConsole) {
3398+
return false;
3399+
}
3400+
3401+
auto pL = mpConsole->mLabelMap.value(name);
3402+
if (pL) {
3403+
pL->clearVisitedLinks();
3404+
return true;
3405+
}
3406+
3407+
return false;
3408+
}
3409+
33503410
void Host::hideMudletsVariables()
33513411
{
33523412
auto varUnit = getLuaInterface()->getVarUnit();
@@ -4004,9 +4064,12 @@ bool Host::setBackgroundColor(const QString& name, int r, int g, int b, int alph
40044064
QString styleSheet = pL->styleSheet();
40054065
QString newColor = QString("background-color: rgba(%1, %2, %3, %4);").arg(r).arg(g).arg(b).arg(alpha);
40064066
if (styleSheet.contains(qsl("background-color"))) {
4007-
QRegularExpression re("background-color: .*;");
4067+
QRegularExpression re("background-color:[^;]*;");
40084068
styleSheet.replace(re, newColor);
40094069
} else {
4070+
if (!styleSheet.isEmpty() && !styleSheet.endsWith('\n')) {
4071+
styleSheet.append('\n');
4072+
}
40104073
styleSheet.append(newColor);
40114074
}
40124075

@@ -4414,6 +4477,9 @@ void Host::setFocusOnHostActiveCommandLine()
44144477

44154478
void Host::recordActiveCommandLine(TCommandLine* pCommandLine)
44164479
{
4480+
if (!pCommandLine) {
4481+
return;
4482+
}
44174483
mpLastCommandLineUsed.removeAll(QPointer<TCommandLine>(pCommandLine));
44184484
mpLastCommandLineUsed.push(QPointer<TCommandLine>(pCommandLine));
44194485
}

src/Host.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ class Host : public QObject
379379
std::pair<bool, QString> createScrollBox(const QString& windowname, const QString& name, int x, int y, int width, int height) const;
380380
std::pair<bool, QString> createLabel(const QString& windowname, const QString& name, int x, int y, int width, int height, bool fillBg, bool clickthrough);
381381
bool setClickthrough(const QString& name, bool clickthrough);
382+
bool setLabelStyleSheet(const QString& name, const QString& styleSheet);
383+
bool setLinkStyle(const QString& name, const QString& linkColor, const QString& linkVisitedColor, bool underline);
384+
bool resetLinkStyle(const QString& name);
385+
bool clearVisitedLinks(const QString& name);
382386
void hideMudletsVariables();
383387
bool createBuffer(const QString& name);
384388
QSize calcFontSize(const QString& windowName);

src/TLabel.cpp

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
#include "TDockWidget.h"
2828
#include "mudlet.h"
2929

30+
#include <QDesktopServices>
31+
#include <QRegularExpression>
32+
#include <QTextCursor>
33+
#include <QTimer>
34+
#include <QUrl>
3035
#include <QtEvents>
3136

3237

@@ -37,6 +42,12 @@ TLabel::TLabel(Host* pH, const QString& name, QWidget* pW)
3742
{
3843
setMouseTracking(true);
3944
setObjectName(qsl("label_%1_%2").arg(pH->getName(), mName));
45+
46+
setTextFormat(Qt::RichText);
47+
setTextInteractionFlags(Qt::TextBrowserInteraction);
48+
setOpenExternalLinks(false);
49+
50+
connect(this, &QLabel::linkActivated, this, &TLabel::slot_linkActivated);
4051
}
4152

4253
TLabel::~TLabel()
@@ -47,6 +58,77 @@ TLabel::~TLabel()
4758
}
4859
}
4960

61+
void TLabel::setText(const QString& text)
62+
{
63+
// If we have link styling configured and the text contains HTML links,
64+
// we need to inject inline styles because QTextDocument doesn't use
65+
// widget stylesheets or QPalette for link colors when a stylesheet exists
66+
if ((!mLinkColor.isEmpty() || !mLinkVisitedColor.isEmpty()) && text.contains(qsl("<a "))) {
67+
QString styledText = text;
68+
69+
// Replace all <a href="..."> tags with <a href="..." style="...">
70+
// Note: This regex is intentionally strict (lowercase, href first, no spacing around =)
71+
// because Mudlet's HTML generation (via echo(), setLabelText(), etc.) consistently
72+
// uses this format. User-provided HTML outside this pattern will still render as
73+
// clickable links (Qt handles that), but won't receive custom styling.
74+
QRegularExpression anchorRegex(qsl("<a\\s+href=([\"'][^\"']*[\"'])([^>]*)>"));
75+
QRegularExpressionMatchIterator it = anchorRegex.globalMatch(styledText);
76+
77+
// Process matches in reverse order to avoid offset issues
78+
QList<QRegularExpressionMatch> matches;
79+
while (it.hasNext()) {
80+
matches.prepend(it.next());
81+
}
82+
83+
for (const auto& match : matches) {
84+
QString fullMatch = match.captured(0);
85+
QString hrefPart = match.captured(1); // The href="..." part
86+
QString otherAttrs = match.captured(2); // Other attributes
87+
88+
// Extract the actual URL from hrefPart (remove quotes)
89+
QString url = hrefPart;
90+
url.remove(0, 1); // Remove opening quote
91+
url.chop(1); // Remove closing quote
92+
93+
bool isVisited = mVisitedLinks.contains(url);
94+
95+
QString linkStyle;
96+
if (isVisited && !mLinkVisitedColor.isEmpty()) {
97+
linkStyle += qsl("color: %1; ").arg(mLinkVisitedColor);
98+
} else if (!mLinkColor.isEmpty()) {
99+
linkStyle += qsl("color: %1; ").arg(mLinkColor);
100+
}
101+
102+
if (!mLinkUnderline) {
103+
linkStyle += qsl("text-decoration: none; ");
104+
}
105+
106+
if (!linkStyle.isEmpty()) {
107+
linkStyle = linkStyle.trimmed();
108+
109+
QString replacement;
110+
if (otherAttrs.contains(qsl("style="))) {
111+
// Already has a style attribute - merge our styles
112+
// This is complex, so for now just prepend our styles
113+
replacement = qsl("<a href=%1 style=\"%2\"").arg(hrefPart, linkStyle);
114+
// Intentionally overwrites any existing style attribute rather than merging
115+
// to keep implementation simple for the common case (labels without pre-existing inline styles)
116+
otherAttrs.remove(QRegularExpression(qsl("style=([\"'][^\"']*[\"'])")));
117+
replacement += otherAttrs + qsl(">");
118+
} else {
119+
replacement = qsl("<a href=%1 style=\"%2\"%3>").arg(hrefPart, linkStyle, otherAttrs);
120+
}
121+
122+
styledText.replace(match.capturedStart(), match.capturedLength(), replacement);
123+
}
124+
}
125+
126+
QLabel::setText(styledText);
127+
} else {
128+
QLabel::setText(text);
129+
}
130+
}
131+
50132
void TLabel::setClick(const int func)
51133
{
52134
releaseFunc(mClickFunction, func);
@@ -91,6 +173,15 @@ void TLabel::setLeave(const int func)
91173

92174
void TLabel::mousePressEvent(QMouseEvent* event)
93175
{
176+
// If the label has rich text with potential hyperlinks, let QLabel handle the event first
177+
// QLabel will emit linkActivated if a link was clicked
178+
if (!text().isEmpty() && textFormat() == Qt::RichText && text().contains(qsl("<a "))) {
179+
QLabel::mousePressEvent(event);
180+
// If QLabel didn't accept the event, then it wasn't a link click
181+
if (event->isAccepted()) {
182+
return;
183+
}
184+
}
94185

95186
if (mpHost && mClickFunction) {
96187
mpHost->getLuaInterpreter()->callLabelCallbackEvent(mClickFunction, event);
@@ -115,6 +206,15 @@ void TLabel::mouseDoubleClickEvent(QMouseEvent* event)
115206

116207
void TLabel::mouseReleaseEvent(QMouseEvent* event)
117208
{
209+
// If the label has rich text with potential hyperlinks, let QLabel handle the event first
210+
if (!text().isEmpty() && textFormat() == Qt::RichText && text().contains(qsl("<a "))) {
211+
QLabel::mouseReleaseEvent(event);
212+
// If QLabel accepted the event, it was handling a link click
213+
if (event->isAccepted()) {
214+
return;
215+
}
216+
}
217+
118218
auto labelParent = qobject_cast<TConsole*>(parent());
119219
if (labelParent && labelParent->mpDockWidget && labelParent->mpDockWidget->isFloating()) {
120220
// move focus back to the active console / command line:
@@ -189,4 +289,129 @@ void TLabel::releaseFunc(const int existingFunction, const int newFunction)
189289
void TLabel::setClickThrough(bool clickthrough)
190290
{
191291
setAttribute(Qt::WA_TransparentForMouseEvents, clickthrough);
292+
293+
// If clickthrough is enabled, text interaction (including hyperlinks) won't work
294+
// So we need to disable text interaction when clickthrough is on
295+
if (clickthrough) {
296+
setTextInteractionFlags(Qt::NoTextInteraction);
297+
} else {
298+
// Re-enable text interaction for clickable hyperlinks
299+
setTextInteractionFlags(Qt::TextBrowserInteraction);
300+
}
301+
}
302+
303+
void TLabel::setLinkStyle(const QString& linkColor, const QString& linkVisitedColor, bool underline)
304+
{
305+
mLinkColor = linkColor;
306+
mLinkVisitedColor = linkVisitedColor;
307+
mLinkUnderline = underline;
308+
309+
// Set QPalette as a fallback (works if no stylesheet is set on the widget)
310+
QPalette palette = this->palette();
311+
312+
if (!linkColor.isEmpty()) {
313+
QColor color(linkColor);
314+
palette.setColor(QPalette::Active, QPalette::Link, color);
315+
palette.setColor(QPalette::Inactive, QPalette::Link, color);
316+
}
317+
318+
if (!linkVisitedColor.isEmpty()) {
319+
QColor color(linkVisitedColor);
320+
palette.setColor(QPalette::Active, QPalette::LinkVisited, color);
321+
palette.setColor(QPalette::Inactive, QPalette::LinkVisited, color);
322+
}
323+
324+
setPalette(palette);
325+
326+
// Note: Widget stylesheets don't affect QTextDocument rendering
327+
// Link colors are applied via inline styles in setText()
328+
329+
// Force update to re-render with new styles
330+
update();
331+
}
332+
333+
void TLabel::resetLinkStyle()
334+
{
335+
setPalette(QPalette());
336+
337+
mLinkColor.clear();
338+
mLinkVisitedColor.clear();
339+
mLinkUnderline = true;
340+
341+
// Force update to re-render with new styles
342+
update();
343+
}
344+
345+
void TLabel::clearVisitedLinks()
346+
{
347+
mVisitedLinks.clear();
348+
349+
QString currentText = text();
350+
if (!currentText.isEmpty() && currentText.contains(qsl("<a "))) {
351+
setText(currentText);
352+
}
353+
}
354+
355+
void TLabel::slot_linkActivated(const QString& link)
356+
{
357+
if (!mpHost) {
358+
return;
359+
}
360+
361+
if (!mLinkVisitedColor.isEmpty()) {
362+
mVisitedLinks.insert(link);
363+
364+
// Refresh the label to update link colors
365+
// We need to re-apply the current text to trigger the styling update
366+
QString currentText = text();
367+
if (!currentText.isEmpty() && currentText.contains(qsl("<a "))) {
368+
setText(currentText);
369+
}
370+
}
371+
372+
// Check for custom schemes by looking for the colon separator
373+
const int colonPos = link.indexOf(':');
374+
375+
if (colonPos > 0) {
376+
const QString scheme = link.left(colonPos).toLower(); // RFC 3986: schemes are case-insensitive
377+
const QString payload = link.mid(colonPos + 1); // Everything after the colon
378+
379+
// Handle custom Mudlet URL schemes for Lua commands
380+
if (scheme == qsl("send")) {
381+
// send: scheme - send the command to the MUD immediately
382+
mpHost->send(payload);
383+
return;
384+
}
385+
386+
if (scheme == qsl("prompt")) {
387+
// prompt: scheme - put text in command line and wait for user to press enter
388+
if (mpHost->mpConsole && mpHost->mpConsole->mpCommandLine) {
389+
QPointer<TCommandLine> commandLine = mpHost->mpConsole->mpCommandLine;
390+
commandLine->setPlainText(payload);
391+
QTextCursor cursor = commandLine->textCursor();
392+
cursor.movePosition(QTextCursor::End);
393+
commandLine->setTextCursor(cursor);
394+
// Defer the focus operation to avoid issues with QPointer manipulation
395+
// during the signal handler execution
396+
QTimer::singleShot(0, commandLine.data(), [commandLine]() {
397+
if (commandLine) {
398+
commandLine->setFocus();
399+
}
400+
});
401+
}
402+
return;
403+
}
404+
405+
if (scheme == qsl("http") || scheme == qsl("https")) {
406+
QDesktopServices::openUrl(QUrl(link));
407+
return;
408+
}
409+
410+
// Unknown scheme - ignore safely to prevent unintended Lua execution
411+
// Only links without a scheme should be treated as Lua code
412+
return;
413+
}
414+
415+
// No scheme - treat as Lua code to execute
416+
mpHost->mLuaInterpreter.compileAndExecuteScript(link);
192417
}

0 commit comments

Comments
 (0)