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
4253TLabel::~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+
50132void TLabel::setClick (const int func)
51133{
52134 releaseFunc (mClickFunction , func);
@@ -91,6 +173,15 @@ void TLabel::setLeave(const int func)
91173
92174void 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
116207void 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)
189289void 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