Skip to content

Commit 43ee75e

Browse files
committed
feat: open web browser about URL in REPL for Jupyter and MicroPython REPL panes
1 parent 8ed3a66 commit 43ee75e

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

mu/interface/panes.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
pyqtSignal,
3636
QTimer,
3737
QUrl,
38+
QEvent,
3839
)
3940
from collections import deque
4041
from PyQt6.QtWidgets import (
@@ -61,6 +62,8 @@
6162
QColor,
6263
QFont,
6364
QAction,
65+
QSyntaxHighlighter,
66+
QTextCharFormat,
6467
)
6568
from qtconsole.rich_jupyter_widget import RichJupyterWidget
6669
from ..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):
159193
VT100_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+
162216
class 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

Comments
 (0)