Update various features in the doc editor using new helper code

This commit is contained in:
Veronica Berglyd Olsen
2024-10-30 19:13:13 +01:00
parent fa0bef5500
commit a2b09fdfec
6 changed files with 104 additions and 104 deletions
+56 -60
View File
@@ -38,13 +38,12 @@ from enum import Enum
from time import time
from PyQt5.QtCore import (
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, QUrl,
pyqtSignal, pyqtSlot
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
pyqtSlot
)
from PyQt5.QtGui import (
QColor, QCursor, QDesktopServices, QKeyEvent, QKeySequence, QMouseEvent,
QPalette, QPixmap, QResizeEvent, QTextBlock, QTextCursor, QTextDocument,
QTextOption
QColor, QCursor, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap,
QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption
)
from PyQt5.QtWidgets import (
QAction, QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit,
@@ -52,10 +51,13 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import minmax, transferCase
from novelwriter.common import minmax, qtLambda, transferCase
from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
from novelwriter.core.document import NWDocument
from novelwriter.enum import nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
from novelwriter.enum import (
nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwItemType,
nwTrinary
)
from novelwriter.extensions.configlayout import NColourLabel
from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton
@@ -392,9 +394,12 @@ class GuiDocEditor(QPlainTextEdit):
"""
self._nwDocument = SHARED.project.storage.getDocument(tHandle)
self._nwItem = self._nwDocument.nwItem
if not ((nwItem := self._nwItem) and nwItem.itemType == nwItemType.FILE):
logger.debug("Requested item '%s' is not a document", tHandle)
self.clearEditor()
return False
docText = self._nwDocument.readDocument()
if docText is None:
if (docText := self._nwDocument.readDocument()) is None:
# There was an I/O error
self.clearEditor()
return False
@@ -415,10 +420,10 @@ class GuiDocEditor(QPlainTextEdit):
self.setReadOnly(False)
self.updateDocMargins()
if tLine is None and self._nwItem is not None:
self.setCursorPosition(self._nwItem.cursorPos)
elif isinstance(tLine, int):
if isinstance(tLine, int):
self.setCursorLine(tLine)
else:
self.setCursorPosition(nwItem.cursorPos)
self.docHeader.setHandle(tHandle)
self.docFooter.setHandle(tHandle)
@@ -433,15 +438,16 @@ class GuiDocEditor(QPlainTextEdit):
self.setDocumentChanged(False)
self._qDocument.clearUndoRedoStacks()
self.docToolBar.setVisible(CONFIG.showEditToolBar)
# Process State Changes
SHARED.project.data.setLastHandle(tHandle, "editor")
self.itemHandleChanged.emit(tHandle)
# Finalise
QApplication.restoreOverrideCursor()
# Update the status bar
if self._nwItem is not None:
self.updateStatusMessage.emit(
self.tr("Opened Document: {0}").format(self._nwItem.itemName)
)
self.updateStatusMessage.emit(
self.tr("Opened Document: {0}").format(nwItem.itemName)
)
return True
@@ -995,7 +1001,7 @@ class GuiDocEditor(QPlainTextEdit):
cursor = self.cursorForPosition(event.pos())
mData, mType = self._qDocument.metaDataAtPos(cursor.position())
if mData and mType == "url":
self._openWebsite(mData)
SHARED.openWebsite(mData)
else:
self._processTag(cursor)
super().mouseReleaseEvent(event)
@@ -1126,48 +1132,48 @@ class GuiDocEditor(QPlainTextEdit):
ctxMenu.setObjectName("ContextMenu")
if pBlock.userState() == BLOCK_TITLE:
action = ctxMenu.addAction(self.tr("Set as Document Name"))
action.triggered.connect(lambda: self._emitRenameItem(pBlock))
action.triggered.connect(qtLambda(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))
action.triggered.connect(qtLambda(SHARED.openWebsite, mData))
ctxMenu.addSeparator()
# Follow
status = self._processTag(cursor=pCursor, follow=False)
if status == nwTrinary.POSITIVE:
action = ctxMenu.addAction(self.tr("Follow Tag"))
action.triggered.connect(lambda: self._processTag(cursor=pCursor, follow=True))
action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, follow=True))
ctxMenu.addSeparator()
elif status == nwTrinary.NEGATIVE:
action = ctxMenu.addAction(self.tr("Create Note for Tag"))
action.triggered.connect(lambda: self._processTag(cursor=pCursor, create=True))
action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, create=True))
ctxMenu.addSeparator()
# Cut, Copy and Paste
if uCursor.hasSelection():
action = ctxMenu.addAction(self.tr("Cut"))
action.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
action.triggered.connect(qtLambda(self.docAction, nwDocAction.CUT))
action = ctxMenu.addAction(self.tr("Copy"))
action.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
action.triggered.connect(qtLambda(self.docAction, nwDocAction.COPY))
action = ctxMenu.addAction(self.tr("Paste"))
action.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
action.triggered.connect(qtLambda(self.docAction, nwDocAction.PASTE))
ctxMenu.addSeparator()
# Selections
action = ctxMenu.addAction(self.tr("Select All"))
action.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
action.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL))
action = ctxMenu.addAction(self.tr("Select Word"))
action.triggered.connect(
lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, pos)
)
action.triggered.connect(qtLambda(
self._makePosSelection, QTextCursor.SelectionType.WordUnderCursor, pos,
))
action = ctxMenu.addAction(self.tr("Select Paragraph"))
action.triggered.connect(lambda: self._makePosSelection(
QTextCursor.SelectionType.BlockUnderCursor, pos)
)
action.triggered.connect(qtLambda(
self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, pos
))
# Spell Checking
if SHARED.project.data.spellCheck:
@@ -1183,18 +1189,16 @@ class GuiDocEditor(QPlainTextEdit):
ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
for option in suggest[:15]:
action = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
action.triggered.connect(
lambda _, option=option: self._correctWord(sCursor, option)
)
action.triggered.connect(qtLambda(self._correctWord, sCursor, option))
else:
trNone = self.tr("No Suggestions")
ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {trNone}")
ctxMenu.addSeparator()
action = ctxMenu.addAction(self.tr("Ignore Word"))
action.triggered.connect(lambda: self._addWord(word, block, False))
action.triggered.connect(qtLambda(self._addWord, word, block, False))
action = ctxMenu.addAction(self.tr("Add Word to Dictionary"))
action.triggered.connect(lambda: self._addWord(word, block, True))
action.triggered.connect(qtLambda(self._addWord, word, block, True))
# Execute the context menu
ctxMenu.exec(self.viewport().mapToGlobal(pos))
@@ -1202,12 +1206,6 @@ 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."""
@@ -2207,7 +2205,7 @@ class MetaCompleter(QMenu):
for value in sorted(options):
rep = value + suffix
action = self.addAction(value)
action.triggered.connect(lambda _, r=rep: self._emitComplete(offset, length, r))
action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
return True
@@ -2307,61 +2305,61 @@ class GuiDocToolBar(QWidget):
self.tbBoldMD = NIconToolButton(self, iSz)
self.tbBoldMD.setToolTip(self.tr("Markdown Bold"))
self.tbBoldMD.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.MD_BOLD)
qtLambda(self.requestDocAction.emit, nwDocAction.MD_BOLD)
)
self.tbItalicMD = NIconToolButton(self, iSz)
self.tbItalicMD.setToolTip(self.tr("Markdown Italic"))
self.tbItalicMD.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.MD_ITALIC)
qtLambda(self.requestDocAction.emit, nwDocAction.MD_ITALIC)
)
self.tbStrikeMD = NIconToolButton(self, iSz)
self.tbStrikeMD.setToolTip(self.tr("Markdown Strikethrough"))
self.tbStrikeMD.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.MD_STRIKE)
qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE)
)
self.tbBold = NIconToolButton(self, iSz)
self.tbBold.setToolTip(self.tr("Shortcode Bold"))
self.tbBold.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.SC_BOLD)
qtLambda(self.requestDocAction.emit, nwDocAction.SC_BOLD)
)
self.tbItalic = NIconToolButton(self, iSz)
self.tbItalic.setToolTip(self.tr("Shortcode Italic"))
self.tbItalic.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.SC_ITALIC)
qtLambda(self.requestDocAction.emit, nwDocAction.SC_ITALIC)
)
self.tbStrike = NIconToolButton(self, iSz)
self.tbStrike.setToolTip(self.tr("Shortcode Strikethrough"))
self.tbStrike.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.SC_STRIKE)
qtLambda(self.requestDocAction.emit, nwDocAction.SC_STRIKE)
)
self.tbUnderline = NIconToolButton(self, iSz)
self.tbUnderline.setToolTip(self.tr("Shortcode Underline"))
self.tbUnderline.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE)
qtLambda(self.requestDocAction.emit, nwDocAction.SC_ULINE)
)
self.tbMark = NIconToolButton(self, iSz)
self.tbMark.setToolTip(self.tr("Shortcode Highlight"))
self.tbMark.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.SC_MARK)
qtLambda(self.requestDocAction.emit, nwDocAction.SC_MARK)
)
self.tbSuperscript = NIconToolButton(self, iSz)
self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript"))
self.tbSuperscript.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.SC_SUP)
qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUP)
)
self.tbSubscript = NIconToolButton(self, iSz)
self.tbSubscript.setToolTip(self.tr("Shortcode Subscript"))
self.tbSubscript.clicked.connect(
lambda: self.requestDocAction.emit(nwDocAction.SC_SUB)
qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUB)
)
# Assemble
@@ -2825,7 +2823,7 @@ class GuiDocEditHeader(QWidget):
self.tbButton = NIconToolButton(self, iSz)
self.tbButton.setVisible(False)
self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
self.tbButton.clicked.connect(qtLambda(self.toggleToolBarRequest.emit))
self.outlineButton = NIconToolButton(self, iSz)
self.outlineButton.setVisible(False)
@@ -2840,7 +2838,7 @@ class GuiDocEditHeader(QWidget):
self.minmaxButton = NIconToolButton(self, iSz)
self.minmaxButton.setVisible(False)
self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit())
self.minmaxButton.clicked.connect(qtLambda(self.docEditor.toggleFocusModeRequest.emit))
self.closeButton = NIconToolButton(self, iSz)
self.closeButton.setVisible(False)
@@ -2903,9 +2901,7 @@ class GuiDocEditHeader(QWidget):
self.outlineMenu.clear()
for number, text in data.items():
action = self.outlineMenu.addAction(text)
action.triggered.connect(
lambda _, number=number: self._gotoBlock(number)
)
action.triggered.connect(qtLambda(self._gotoBlock, number))
self._docOutline = data
logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
return
+5 -12
View File
@@ -28,8 +28,7 @@ import logging
from pathlib import Path
from typing import TYPE_CHECKING
from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QAction, QMenuBar
from novelwriter import CONFIG, SHARED
@@ -107,12 +106,6 @@ class GuiMainMenu(QMenuBar):
self.mainGui.docEditor.toggleSpellCheck(None)
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 _openUserManualFile(self) -> None:
"""Open the documentation in PDF format."""
@@ -1033,7 +1026,7 @@ class GuiMainMenu(QMenuBar):
# Help > User Manual (Online)
self.aHelpDocs = self.helpMenu.addAction(self.tr("User Manual (Online)"))
self.aHelpDocs.setShortcut("F1")
self.aHelpDocs.triggered.connect(qtLambda(self._openWebsite, nwConst.URL_DOCS))
self.aHelpDocs.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_DOCS))
self.mainGui.addAction(self.aHelpDocs)
# Help > User Manual (PDF)
@@ -1048,14 +1041,14 @@ class GuiMainMenu(QMenuBar):
# Document > Report an Issue
self.aIssue = self.helpMenu.addAction(self.tr("Report an Issue (GitHub)"))
self.aIssue.triggered.connect(qtLambda(self._openWebsite, nwConst.URL_REPORT))
self.aIssue.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_REPORT))
# Document > Ask a Question
self.aQuestion = self.helpMenu.addAction(self.tr("Ask a Question (GitHub)"))
self.aQuestion.triggered.connect(qtLambda(self._openWebsite, nwConst.URL_HELP))
self.aQuestion.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_HELP))
# Document > Main Website
self.aWebsite = self.helpMenu.addAction(self.tr("The novelWriter Website"))
self.aWebsite.triggered.connect(qtLambda(self._openWebsite, nwConst.URL_WEB))
self.aWebsite.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_WEB))
return
+13 -21
View File
@@ -44,7 +44,7 @@ from novelwriter.dialogs.about import GuiAbout
from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.projectsettings import GuiProjectSettings
from novelwriter.dialogs.wordlist import GuiWordList
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwItemType, nwView
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwView
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.gui.docviewer import GuiDocViewer
from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
@@ -481,8 +481,7 @@ class GuiMain(QMainWindow):
QApplication.processEvents()
self.openDocument(lastEdited, doScroll=True)
lastViewed = SHARED.project.data.getLastHandle("viewer")
if lastViewed is not None:
if lastViewed := SHARED.project.data.getLastHandle("viewer"):
QApplication.processEvents()
self.viewDocument(lastViewed)
@@ -512,7 +511,7 @@ class GuiMain(QMainWindow):
# Document Actions
##
def closeDocument(self, beforeOpen: bool = False) -> None:
def closeDocument(self) -> None:
"""Close the document and clear the editor and title field."""
if SHARED.hasProject:
# Disable focus mode if it is active
@@ -531,12 +530,8 @@ class GuiMain(QMainWindow):
doScroll: bool = False
) -> bool:
"""Open a specific document, optionally at a given line."""
if not SHARED.hasProject:
logger.error("No project open")
return False
if not tHandle or not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
logger.debug("Requested item '%s' is not a document", tHandle)
if not (SHARED.hasProject and tHandle):
logger.error("Nothing to open open")
return False
if sTitle and tLine is None:
@@ -546,18 +541,15 @@ class GuiMain(QMainWindow):
self._changeView(nwView.EDITOR)
if tHandle == self.docEditor.docHandle:
self.docEditor.setCursorLine(tLine)
if changeFocus:
self.docEditor.setFocus()
return True
self.closeDocument(beforeOpen=True)
if self.docEditor.loadText(tHandle, tLine):
SHARED.project.data.setLastHandle(tHandle, "editor")
self.projView.setSelectedHandle(tHandle, doScroll=doScroll)
if changeFocus:
self.docEditor.setFocus()
else:
return False
self.closeDocument()
if self.docEditor.loadText(tHandle, tLine):
self.projView.setSelectedHandle(tHandle, doScroll=doScroll)
else:
return False
if changeFocus:
self.docEditor.setFocus()
return True
+12 -2
View File
@@ -30,8 +30,8 @@ from pathlib import Path
from time import time
from typing import TYPE_CHECKING, TypeVar
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal
from PyQt5.QtGui import QFont
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QDesktopServices, QFont
from PyQt5.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget
from novelwriter.common import formatFileFilter
@@ -292,6 +292,16 @@ class SharedData(QObject):
return widget
return None
##
# Public Slots
##
@pyqtSlot(str)
def openWebsite(self, url: str) -> None:
"""Open a URL in the system's default browser."""
QDesktopServices.openUrl(QUrl(url))
return
##
# Signal Proxy
##
+18
View File
@@ -20,8 +20,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget
from novelwriter.core.project import NWProject
@@ -63,6 +67,20 @@ def testBaseSharedData_Init():
assert shared.projectLock is None
@pytest.mark.base
def testBaseSharedData_Functions(monkeypatch):
"""Test SharedData class functions."""
shared = SharedData()
# Open URL
with monkeypatch.context() as mp:
openUrl = MagicMock()
mp.setattr(QDesktopServices, "openUrl", openUrl)
shared.openWebsite("http://www.example.com")
assert openUrl.called is True
assert openUrl.call_args[0][0] == QUrl("http://www.example.com")
@pytest.mark.base
def testBaseSharedData_Projects(monkeypatch, caplog, fncPath):
"""Test SharedData handling of projects."""
-9
View File
@@ -24,7 +24,6 @@ from unittest.mock import MagicMock
import pytest
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices, QTextBlock, QTextCursor
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
@@ -42,14 +41,6 @@ def testGuiMainMenu_Slots(qtbot, monkeypatch, nwGUI, projPath):
"""Test the main menu slots."""
buildTestProject(nwGUI, projPath)
# Open URL
with monkeypatch.context() as mp:
openUrl = MagicMock()
mp.setattr(QDesktopServices, "openUrl", openUrl)
nwGUI.mainMenu._openWebsite("http://www.example.com")
assert openUrl.called is True
assert openUrl.call_args[0][0] == QUrl("http://www.example.com")
# Open Manual
with monkeypatch.context() as mp:
openUrl = MagicMock()