3535 pyqtSignal ,
3636 QTimer ,
3737 QUrl ,
38+ QEvent ,
3839)
3940from collections import deque
4041from PyQt6 .QtWidgets import (
6162 QColor ,
6263 QFont ,
6364 QAction ,
65+ QSyntaxHighlighter ,
66+ QTextCharFormat ,
6467)
6568from qtconsole .rich_jupyter_widget import RichJupyterWidget
6669from ..i18n import language_code
@@ -104,6 +107,37 @@ def __init__(self, theme="day", parent=None):
104107 super ().__init__ (parent )
105108 self .set_theme (theme )
106109 self .console_height = 10
110+ self .highlighter = URLHighlighter (self ._control .document ())
111+ self ._control .setMouseTracking (True )
112+ self ._control .installEventFilter (self )
113+
114+ def eventFilter (self , obj , event ):
115+ if obj == self ._control :
116+ if event .type () == QEvent .Type .MouseMove :
117+ if self .get_url_at_pos (event .pos ()):
118+ self ._control .viewport ().setCursor (Qt .PointingHandCursor )
119+ else :
120+ self ._control .viewport ().setCursor (Qt .IBeamCursor )
121+ elif event .type () == QEvent .Type .MouseButtonRelease :
122+ # Only handle left click
123+ if event .button () == Qt .MouseButton .LeftButton :
124+ url = self .get_url_at_pos (event .pos ())
125+ if url :
126+ QDesktopServices .openUrl (QUrl (url ))
127+ return True
128+ return super ().eventFilter (obj , event )
129+
130+ def get_url_at_pos (self , pos ):
131+ """
132+ Returns the URL at the given position, if any.
133+ """
134+ cursor = self ._control .cursorForPosition (pos )
135+ line = cursor .block ().text ()
136+ pos_in_line = cursor .positionInBlock ()
137+ for match in URLHighlighter .URL_REGEX .finditer (line ):
138+ if match .start () <= pos_in_line <= match .end ():
139+ return match .group (0 )
140+ return None
107141
108142 def _append_plain_text (self , text , * args , ** kwargs ):
109143 """
@@ -159,6 +193,26 @@ def setFocus(self):
159193VT100_END = b"\x1B [F"
160194
161195
196+ class URLHighlighter (QSyntaxHighlighter ):
197+ """
198+ Highlights URLs in the text.
199+ """
200+
201+ URL_REGEX = re .compile (r"https?://[^\s\"'()<>\[\]]+" )
202+
203+ def __init__ (self , parent = None ):
204+ super ().__init__ (parent )
205+ self ._format = QTextCharFormat ()
206+ self ._format .setForeground (QColor ("#0000FF" ))
207+ self ._format .setFontUnderline (True )
208+
209+ def highlightBlock (self , text ):
210+ for match in self .URL_REGEX .finditer (text ):
211+ self .setFormat (
212+ match .start (), match .end () - match .start (), self ._format
213+ )
214+
215+
162216class MicroPythonREPLPane (QTextEdit ):
163217 """
164218 REPL = Read, Evaluate, Print, Loop.
@@ -179,6 +233,8 @@ def __init__(self, connection, theme="day", parent=None):
179233 self .setUndoRedoEnabled (False )
180234 self .setContextMenuPolicy (Qt .CustomContextMenu )
181235 self .customContextMenuRequested .connect (self .context_menu )
236+ self .highlighter = URLHighlighter (self .document ())
237+ self .setMouseTracking (True )
182238 # The following variable maintains the position where we know
183239 # the device cursor is placed. It is initialized to the beginning
184240 # of the QTextEdit (i.e. equal to the Qt cursor position)
@@ -380,13 +436,41 @@ def mouseReleaseEvent(self, mouseEvent):
380436 the device (except if a selection is made, for selections we first
381437 move the cursor on deselection)
382438 """
439+ if mouseEvent .button () == Qt .MouseButton .LeftButton :
440+ url = self .get_url_at_pos (mouseEvent .pos ())
441+ if url :
442+ QDesktopServices .openUrl (QUrl (url ))
443+ return
444+
383445 super ().mouseReleaseEvent (mouseEvent )
384446
385447 # If when a user have clicked and not made a selection
386448 # move the device cursor to where the user clicked
387449 if not self .textCursor ().hasSelection ():
388450 self .set_devicecursor_to_qtcursor ()
389451
452+ def mouseMoveEvent (self , event ):
453+ """
454+ Change cursor to hand when hovering over a URL.
455+ """
456+ if self .get_url_at_pos (event .pos ()):
457+ self .viewport ().setCursor (Qt .PointingHandCursor )
458+ else :
459+ self .viewport ().setCursor (Qt .IBeamCursor )
460+ super ().mouseMoveEvent (event )
461+
462+ def get_url_at_pos (self , pos ):
463+ """
464+ Returns the URL at the given position, if any.
465+ """
466+ cursor = self .cursorForPosition (pos )
467+ line = cursor .block ().text ()
468+ pos_in_line = cursor .positionInBlock ()
469+ for match in URLHighlighter .URL_REGEX .finditer (line ):
470+ if match .start () <= pos_in_line <= match .end ():
471+ return match .group (0 )
472+ return None
473+
390474 def process_tty_data (self , data ):
391475 """
392476 Given some incoming bytes of data, work out how to handle / display
0 commit comments