Add url handling in editor and viewer

This commit is contained in:
Veronica Berglyd Olsen
2024-10-25 17:51:10 +02:00
parent 37268b873a
commit 759cfd77e9
4 changed files with 76 additions and 10 deletions
+24 -5
View File
@@ -38,12 +38,13 @@ from enum import Enum
from time import time
from PyQt5.QtCore import (
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
pyqtSlot
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, QUrl,
pyqtSignal, pyqtSlot
)
from PyQt5.QtGui import (
QColor, QCursor, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap,
QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption
QColor, QCursor, QDesktopServices, QKeyEvent, QKeySequence, QMouseEvent,
QPalette, QPixmap, QResizeEvent, QTextBlock, QTextCursor, QTextDocument,
QTextOption
)
from PyQt5.QtWidgets import (
QAction, QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit,
@@ -985,7 +986,12 @@ class GuiDocEditor(QPlainTextEdit):
follow tag function.
"""
if QApplication.keyboardModifiers() == QtModCtrl:
self._processTag(self.cursorForPosition(event.pos()))
cursor = self.cursorForPosition(event.pos())
mData, mType = self._qDocument.metaDataAtPos(cursor.position())
if mData and mType == "url":
self._openWebsite(mData)
else:
self._processTag(cursor)
super().mouseReleaseEvent(event)
return
@@ -1116,6 +1122,13 @@ class GuiDocEditor(QPlainTextEdit):
action = ctxMenu.addAction(self.tr("Set as Document Name"))
action.triggered.connect(lambda: self._emitRenameItem(pBlock))
# URL
(mData, mType) = self._qDocument.metaDataAtPos(pCursor.position())
if mData and mType == "url":
action = ctxMenu.addAction(self.tr("Open URL"))
action.triggered.connect(lambda: self._openWebsite(mData))
ctxMenu.addSeparator()
# Follow
status = self._processTag(cursor=pCursor, follow=False)
if status == nwTrinary.POSITIVE:
@@ -1183,6 +1196,12 @@ class GuiDocEditor(QPlainTextEdit):
return
@pyqtSlot(str)
def _openWebsite(self, url: str) -> None:
"""Open a URL in the system's default browser."""
QDesktopServices.openUrl(QUrl(url))
return
@pyqtSlot()
def _runDocumentTasks(self) -> None:
"""Run timer document tasks."""
+31 -3
View File
@@ -44,6 +44,7 @@ from novelwriter.text.patterns import REGEX_PATTERNS
logger = logging.getLogger(__name__)
RX_URL = REGEX_PATTERNS.url
RX_WORDS = REGEX_PATTERNS.wordSplit
RX_FMT_SC = REGEX_PATTERNS.shortcodePlain
RX_FMT_SV = REGEX_PATTERNS.shortcodeValue
@@ -113,10 +114,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._addCharFormat("replace", SHARED.theme.colRepTag)
self._addCharFormat("hidden", SHARED.theme.colHidden)
self._addCharFormat("markup", SHARED.theme.colHidden)
self._addCharFormat("link", SHARED.theme.colLink, "u")
self._addCharFormat("note", SHARED.theme.colNote)
self._addCharFormat("code", SHARED.theme.colCode)
self._addCharFormat("keyword", SHARED.theme.colKey)
self._addCharFormat("tag", SHARED.theme.colTag)
self._addCharFormat("tag", SHARED.theme.colTag, "u")
self._addCharFormat("modifier", SHARED.theme.colMod)
self._addCharFormat("value", SHARED.theme.colVal)
self._addCharFormat("optional", SHARED.theme.colOpt)
@@ -231,6 +233,15 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
# URLs
rxRule = REGEX_PATTERNS.url
hlRule = {
0: self._hStyles["link"],
}
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
# Alignment Tags
rxRule = re.compile(r"(^>{1,2}|<{1,2}$)", re.UNICODE)
hlRule = {
@@ -447,6 +458,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
charFormat.setFontWeight(QFont.Weight.Bold)
if "i" in styles:
charFormat.setFontItalic(True)
if "u" in styles:
charFormat.setFontUnderline(True)
if "s" in styles:
charFormat.setFontStrikeOut(True)
if "err" in styles:
@@ -469,9 +482,15 @@ class TextBlockData(QTextBlockUserData):
def __init__(self) -> None:
super().__init__()
self._spellErrors: list[tuple[int, int]] = []
self._metaData: list[tuple[int, int, str, str]] = []
self._spellErrors: list[tuple[int, int,]] = []
return
@property
def metaData(self) -> list[tuple[int, int, str, str]]:
"""Return meta data from last check."""
return self._metaData
@property
def spellErrors(self) -> list[tuple[int, int]]:
"""Return spell error data from last check."""
@@ -481,6 +500,8 @@ class TextBlockData(QTextBlockUserData):
"""Run the spell checker and cache the result, and return the
list of spell check errors.
"""
self._metaData = []
self._spellErrors = []
if "[" in text:
# Strip shortcodes
for regEx in [RX_FMT_SC, RX_FMT_SV]:
@@ -489,7 +510,14 @@ class TextBlockData(QTextBlockUserData):
pad = " "*(e - s)
text = f"{text[:s]}{pad}{text[e:]}"
self._spellErrors = []
if "http" in text:
# Strip URLs
for res in RX_URL.finditer(text, offset):
if (s := res.start(0)) >= 0 and (e := res.end(0)) >= 0:
pad = " "*(e - s)
text = f"{text[:s]}{pad}{text[e:]}"
self._metaData.append((s, e, res.group(0), "url"))
checker = SHARED.spelling
for res in RX_WORDS.finditer(text.replace("_", " "), offset):
if (
+6 -2
View File
@@ -31,7 +31,7 @@ import logging
from enum import Enum
from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor
from PyQt5.QtGui import QCursor, QDesktopServices, QMouseEvent, QPalette, QResizeEvent, QTextCursor
from PyQt5.QtWidgets import (
QAction, QApplication, QFrame, QHBoxLayout, QMenu, QTextBrowser,
QToolButton, QWidget
@@ -76,6 +76,7 @@ class GuiDocViewer(QTextBrowser):
# Settings
self.setMinimumWidth(CONFIG.pxInt(300))
self.setAutoFillBackground(True)
self.setOpenLinks(False)
self.setOpenExternalLinks(False)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.setFrameStyle(QFrame.Shape.NoFrame)
@@ -166,6 +167,7 @@ class GuiDocViewer(QTextBrowser):
self._docTheme.text = SHARED.theme.colText
self._docTheme.highlight = SHARED.theme.colMark
self._docTheme.head = SHARED.theme.colHead
self._docTheme.link = SHARED.theme.colLink
self._docTheme.comment = SHARED.theme.colHidden
self._docTheme.note = SHARED.theme.colNote
self._docTheme.code = SHARED.theme.colCode
@@ -378,8 +380,10 @@ class GuiDocViewer(QTextBrowser):
logger.debug("Clicked link: '%s'", link)
if (bits := link.partition("_")) and bits[0] == "#tag" and bits[2]:
self.loadDocumentTagRequest.emit(bits[2], nwDocMode.VIEW)
else:
elif link.startswith("#"):
self.navigateTo(link)
elif link.startswith("http"):
QDesktopServices.openUrl(QUrl(url))
return
@pyqtSlot("QPoint")
+15
View File
@@ -95,6 +95,21 @@ class GuiTextDocument(QTextDocument):
return
def metaDataAtPos(self, pos: int) -> tuple[str, str]:
"""Check if there is meta data available at a given position in
the document, and if so, return it.
"""
cursor = QTextCursor(self)
cursor.setPosition(pos)
block = cursor.block()
data = block.userData()
if block.isValid() and isinstance(data, TextBlockData):
if (check := pos - block.position()) >= 0:
for cPos, cEnd, cData, cType in data.metaData:
if cPos <= check <= cEnd:
return cData, cType
return "", ""
def spellErrorAtPos(self, pos: int) -> tuple[str, int, int, list[str]]:
"""Check if there is a misspelled word at a given position in
the document, and if so, return it.