Skip to content

Commit

Permalink
Improve document editing and test coverage (#2077)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo authored Oct 30, 2024
2 parents 92d148b + f2e36d9 commit b529d5b
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 184 deletions.
138 changes: 70 additions & 68 deletions novelwriter/gui/doceditor.py

Large diffs are not rendered by default.

17 changes: 5 additions & 12 deletions novelwriter/gui/mainmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
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
Expand Down Expand Up @@ -107,12 +106,6 @@ def _toggleSpellCheck(self) -> None:
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."""
Expand Down Expand Up @@ -1033,7 +1026,7 @@ def _buildHelpMenu(self) -> None:
# 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)
Expand All @@ -1048,14 +1041,14 @@ def _buildHelpMenu(self) -> None:

# 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
41 changes: 23 additions & 18 deletions novelwriter/gui/noveltree.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ def __init__(self, parent: QWidget) -> None:

# Function Mappings
self.getSelectedHandle = self.novelTree.getSelectedHandle
self.setActiveHandle = self.novelTree.setActiveHandle

return

Expand Down Expand Up @@ -163,6 +162,12 @@ def treeHasFocus(self) -> bool:
# Public Slots
##

@pyqtSlot(str)
def setActiveHandle(self, tHandle: str) -> None:
"""Highlight the rows associated with a given handle."""
self.novelTree.setActiveHandle(tHandle)
return

@pyqtSlot()
def refreshTree(self) -> None:
"""Refresh the current tree."""
Expand Down Expand Up @@ -367,11 +372,11 @@ def __init__(self, novelView: GuiNovelView) -> None:
self.novelView = novelView

# Internal Variables
self._treeMap = {}
self._lastBuild = 0
self._lastCol = NovelTreeColumn.POV
self._lastColSize = 0.25
self._actHandle = None
self._treeMap: dict[str, QTreeWidgetItem] = {}

# Cached Strings
self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
Expand Down Expand Up @@ -540,25 +545,25 @@ def setLastColSize(self, colSize: int) -> None:
self._lastColSize = minmax(colSize, 15, 75)/100.0
return

def setActiveHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
def setActiveHandle(self, tHandle: str | None) -> None:
"""Highlight the rows associated with a given handle."""
didScroll = False
self._actHandle = tHandle
for i in range(self.topLevelItemCount()):
if tItem := self.topLevelItem(i):
if tItem.data(self.C_DATA, self.D_HANDLE) == tHandle:
tItem.setBackground(self.C_TITLE, self.palette().alternateBase())
tItem.setBackground(self.C_WORDS, self.palette().alternateBase())
tItem.setBackground(self.C_EXTRA, self.palette().alternateBase())
tItem.setBackground(self.C_MORE, self.palette().alternateBase())
if doScroll and not didScroll:
self.scrollToItem(tItem, QAbstractItemView.ScrollHint.PositionAtCenter)
brushOn = self.palette().alternateBase()
brushOff = self.palette().base()
if pHandle := self._actHandle:
for key, item in self._treeMap.items():
if key.startswith(pHandle):
for i in range(self.columnCount()):
item.setBackground(i, brushOff)
if tHandle:
for key, item in self._treeMap.items():
if key.startswith(tHandle):
for i in range(self.columnCount()):
item.setBackground(i, brushOn)
if not didScroll:
self.scrollToItem(item, QAbstractItemView.ScrollHint.PositionAtCenter)
didScroll = True
else:
tItem.setBackground(self.C_TITLE, self.palette().base())
tItem.setBackground(self.C_WORDS, self.palette().base())
tItem.setBackground(self.C_EXTRA, self.palette().base())
tItem.setBackground(self.C_MORE, self.palette().base())
self._actHandle = tHandle or None
return

##
Expand Down
20 changes: 20 additions & 0 deletions novelwriter/gui/projtree.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ def setSelectedHandle(self, tHandle: str, doScroll: bool = False) -> None:
self.projTree.setSelectedHandle(tHandle, doScroll=doScroll)
return

@pyqtSlot(str)
def setActiveHandle(self, tHandle: str | None) -> None:
"""Highlight the active handle."""
self.projTree.setActiveHandle(tHandle)
return

@pyqtSlot(str)
def updateItemValues(self, tHandle: str) -> None:
"""Update tree item."""
Expand Down Expand Up @@ -500,6 +506,7 @@ def __init__(self, projView: GuiProjectView) -> None:
self._treeMap: dict[str, QTreeWidgetItem] = {}
self._timeChanged = 0.0
self._popAlert = None
self._actHandle = None

# Cached Translations
self.trActive = self.tr("Active")
Expand Down Expand Up @@ -1144,6 +1151,19 @@ def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool

return True

def setActiveHandle(self, tHandle: str | None) -> None:
"""Highlight the rows associated with a given handle."""
brushOn = self.palette().alternateBase()
brushOff = self.palette().base()
if (pHandle := self._actHandle) and (item := self._treeMap.get(pHandle)):
for i in range(self.columnCount()):
item.setBackground(i, brushOff)
if tHandle and (item := self._treeMap.get(tHandle)):
for i in range(self.columnCount()):
item.setBackground(i, brushOn)
self._actHandle = tHandle or None
return

def setExpandedFromHandle(self, tHandle: str | None, isExpanded: bool) -> None:
"""Iterate through items below tHandle and change expanded
status for all child items. If tHandle is None, it affects the
Expand Down
119 changes: 50 additions & 69 deletions novelwriter/guimain.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
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
Expand Down Expand Up @@ -249,11 +249,13 @@ def __init__(self) -> None:
self.projSearch.openDocumentSelectRequest.connect(self._openDocumentSelection)
self.projSearch.selectedItemChanged.connect(self.itemDetails.updateViewBox)

self.docEditor.closeDocumentRequest.connect(self.closeDocEditor)
self.docEditor.closeEditorRequest.connect(self.closeDocEditor)
self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
self.docEditor.docTextChanged.connect(self.projSearch.textChanged)
self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus)
self.docEditor.itemHandleChanged.connect(self.novelView.setActiveHandle)
self.docEditor.itemHandleChanged.connect(self.projView.setActiveHandle)
self.docEditor.loadDocumentTagRequest.connect(self._followTag)
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
Expand All @@ -262,8 +264,8 @@ def __init__(self) -> None:
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState)
self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode)
self.docEditor.updateStatusMessage.connect(self.mainStatus.setStatusMessage)

self.docViewer.closeDocumentRequest.connect(self.closeDocViewer)
self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
Expand Down Expand Up @@ -479,8 +481,7 @@ def openProject(self, projFile: str | Path | None) -> bool:
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)

Expand Down Expand Up @@ -510,16 +511,14 @@ def saveProject(self, autoSave: bool = False) -> bool:
# 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
if SHARED.focusMode:
SHARED.setFocusMode(False)
self.saveDocument()
self.docEditor.clearEditor()
if not beforeOpen:
self.novelView.setActiveHandle(None)
return

def openDocument(
Expand All @@ -531,12 +530,8 @@ def openDocument(
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:
Expand All @@ -546,19 +541,15 @@ def openDocument(
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)
self.novelView.setActiveHandle(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

Expand Down Expand Up @@ -853,35 +844,33 @@ def showDictionariesDialog(self) -> None:

def closeMain(self) -> bool:
"""Save everything, and close novelWriter."""
if SHARED.hasProject:
msgYes = SHARED.question("%s<br>%s" % (
self.tr("Do you want to exit novelWriter?"),
self.tr("Changes are saved automatically.")
))
if not msgYes:
return False

logger.info("Exiting novelWriter")
if SHARED.hasProject and SHARED.question("%s<br>%s" % (
self.tr("Do you want to exit novelWriter?"),
self.tr("Changes are saved automatically.")
)):
logger.info("Exiting novelWriter")

if not SHARED.focusMode:
CONFIG.setMainPanePos(self.splitMain.sizes())
CONFIG.setOutlinePanePos(self.outlineView.splitSizes())
if self.docViewerPanel.isVisible():
CONFIG.setViewPanePos(self.splitView.sizes())

CONFIG.showViewerPanel = self.docViewerPanel.isVisible()
wFull = Qt.WindowState.WindowFullScreen
if self.windowState() & wFull != wFull:
# Ignore window size if in full screen mode
CONFIG.setMainWinSize(self.width(), self.height())

if not SHARED.focusMode:
CONFIG.setMainPanePos(self.splitMain.sizes())
CONFIG.setOutlinePanePos(self.outlineView.splitSizes())
if self.docViewerPanel.isVisible():
CONFIG.setViewPanePos(self.splitView.sizes())

CONFIG.showViewerPanel = self.docViewerPanel.isVisible()
wFull = Qt.WindowState.WindowFullScreen
if self.windowState() & wFull != wFull:
# Ignore window size if in full screen mode
CONFIG.setMainWinSize(self.width(), self.height())
if SHARED.hasProject:
self.closeProject(True)
CONFIG.saveConfig()

if SHARED.hasProject:
self.closeProject(True)
CONFIG.saveConfig()
QApplication.quit()

QApplication.quit()
return True

return True
return False

def closeViewerPanel(self, byUser: bool = True) -> bool:
"""Close the document view panel."""
Expand Down Expand Up @@ -1110,8 +1099,16 @@ def _processWordListChanges(self) -> None:
@pyqtSlot(str, nwDocMode)
def _followTag(self, tag: str, mode: nwDocMode) -> None:
"""Follow a tag after user interaction with a link."""
tHandle, sTitle = self._getTagSource(tag)
if tHandle is not None:
tHandle, sTitle = SHARED.project.index.getTagSource(tag)
if tHandle is None:
SHARED.error(self.tr(
"Could not find the reference for tag '{0}'. It either doesn't "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing {1}."
).format(
tag, "F9"
))
else:
if mode == nwDocMode.EDIT:
self.openDocument(tHandle, sTitle=sTitle)
elif mode == nwDocMode.VIEW:
Expand Down Expand Up @@ -1302,19 +1299,3 @@ def _updateWindowTitle(self, projName: str | None = None) -> None:
"""Set the window title and add the project's name."""
self.setWindowTitle(" - ".join(filter(None, [projName, CONFIG.appName])))
return

def _getTagSource(self, tag: str) -> tuple[str | None, str | None]:
"""Handle the index lookup of a tag and display an alert if the
tag cannot be found.
"""
tHandle, sTitle = SHARED.project.index.getTagSource(tag)
if tHandle is None:
SHARED.error(self.tr(
"Could not find the reference for tag '{0}'. It either doesn't "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing {1}."
).format(
tag, "F9"
))
return None, None
return tHandle, sTitle
14 changes: 12 additions & 2 deletions novelwriter/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
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
Expand Down Expand Up @@ -292,6 +292,16 @@ def findTopLevelWidget(self, kind: type[NWWidget]) -> NWWidget | None:
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
##
Expand Down
Loading

0 comments on commit b529d5b

Please sign in to comment.