Clickable URL links

This commit is contained in:
Miguel Jacq 2025-11-02 11:00:00 +11:00
parent c4f99f9b2b
commit 327e7882b5
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
3 changed files with 153 additions and 3 deletions

View file

@ -1,23 +1,147 @@
from __future__ import annotations
from PySide6.QtGui import (
QBrush,
QColor,
QDesktopServices,
QFont,
QFontDatabase,
QTextCharFormat,
QTextCursor,
QTextListFormat,
QTextBlockFormat,
)
from PySide6.QtCore import Slot
from PySide6.QtCore import Qt, QUrl, Signal, Slot, QRegularExpression
from PySide6.QtWidgets import QTextEdit
from .url_highlighter import UrlHighlighter
class Editor(QTextEdit):
def __init__(self):
super().__init__()
linkActivated = Signal(str)
_URL_RX = QRegularExpression(r"(https?://[^\s<>\"]+|www\.[^\s<>\"]+)")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tab_w = 4 * self.fontMetrics().horizontalAdvance(" ")
self.setTabStopDistance(tab_w)
self.setTextInteractionFlags(
Qt.TextInteractionFlag.TextEditorInteraction
| Qt.TextInteractionFlag.LinksAccessibleByMouse
| Qt.TextInteractionFlag.LinksAccessibleByKeyboard
)
self.setAcceptRichText(True)
# Turn raw URLs into anchors
self._linkifying = False
self.textChanged.connect(self._linkify_document)
self.viewport().setMouseTracking(True)
def _linkify_document(self):
if self._linkifying:
return
self._linkifying = True
doc = self.document()
cur = QTextCursor(doc)
cur.beginEditBlock()
block = doc.begin()
while block.isValid():
text = block.text()
it = self._URL_RX.globalMatch(text)
while it.hasNext():
m = it.next()
start = block.position() + m.capturedStart()
end = start + m.capturedLength()
cur.setPosition(start)
cur.setPosition(end, QTextCursor.KeepAnchor)
fmt = cur.charFormat()
if fmt.isAnchor(): # already linkified; skip
continue
href = m.captured(0)
if href.startswith("www."):
href = "https://" + href
fmt.setAnchor(True)
# Qt 6: use setAnchorHref; for compatibility, also set names.
try:
fmt.setAnchorHref(href)
except AttributeError:
fmt.setAnchorNames([href])
fmt.setFontUnderline(True)
fmt.setForeground(Qt.blue)
cur.setCharFormat(fmt)
block = block.next()
cur.endEditBlock()
self._linkifying = False
def mouseReleaseEvent(self, e):
if e.button() == Qt.LeftButton and (e.modifiers() & Qt.ControlModifier):
href = self.anchorAt(e.pos())
if href:
QDesktopServices.openUrl(QUrl.fromUserInput(href))
self.linkActivated.emit(href)
return
super().mouseReleaseEvent(e)
def mouseMoveEvent(self, e):
if (e.modifiers() & Qt.ControlModifier) and self.anchorAt(e.pos()):
self.viewport().setCursor(Qt.PointingHandCursor)
else:
self.viewport().setCursor(Qt.IBeamCursor)
super().mouseMoveEvent(e)
def keyPressEvent(self, e):
key = e.key()
# Pre-insert: stop link/format bleed for “word boundary” keys
if key in (Qt.Key_Space, Qt.Key_Tab):
self._break_anchor_for_next_char()
return super().keyPressEvent(e)
# When pressing Enter/return key, insert first, then neutralise the empty blocks inline format
if key in (Qt.Key_Return, Qt.Key_Enter):
super().keyPressEvent(e) # create the new (possibly empty) paragraph
# If we're on an empty block, clear the insertion char format so the
# *next* Enter will create another new line (not consume the press to reset formatting).
c = self.textCursor()
block = c.block()
if block.length() == 1:
self._clear_insertion_char_format()
return
return super().keyPressEvent(e)
def _clear_insertion_char_format(self):
"""Reset inline typing format (keeps lists, alignment, margins, etc.)."""
nf = QTextCharFormat()
self.setCurrentCharFormat(nf)
def _break_anchor_for_next_char(self):
c = self.textCursor()
fmt = c.charFormat()
if fmt.isAnchor() or fmt.fontUnderline() or fmt.foreground().style() != 0:
# clone, then strip just the link-specific bits so the next char is plain text
nf = QTextCharFormat(fmt)
nf.setAnchor(False)
nf.setFontUnderline(False)
nf.clearForeground()
try:
nf.setAnchorHref("")
except AttributeError:
nf.setAnchorNames([])
self.setCurrentCharFormat(nf)
def merge_on_sel(self, fmt):
"""
Sets the styling on the selected characters.

View file

@ -19,6 +19,7 @@ class ToolBar(QToolBar):
def __init__(self, parent=None):
super().__init__("Format", parent)
self._build_actions()
self.setObjectName("Format")
def _build_actions(self):
# Bold

View file

@ -0,0 +1,25 @@
from __future__ import annotations
import re
from PySide6.QtGui import QSyntaxHighlighter, QTextCharFormat
from PySide6.QtCore import Qt, QRegularExpression
class UrlHighlighter(QSyntaxHighlighter):
def __init__(self, doc):
super().__init__(doc)
self.rx = QRegularExpression(r"(https?://[^\s<>\"]+|www\.[^\s<>\"]+)")
def highlightBlock(self, text: str):
it = self.rx.globalMatch(text)
while it.hasNext():
m = it.next()
href = m.captured(0)
if href.startswith("www."):
href = "https://" + href
fmt = QTextCharFormat()
fmt.setAnchor(True)
fmt.setAnchorHref(href)
fmt.setFontUnderline(True)
fmt.setForeground(Qt.blue)
self.setFormat(m.capturedStart(0), m.capturedLength(0), fmt)