Annotate main gui

This commit is contained in:
Veronica Berglyd Olsen
2023-08-06 14:50:11 +02:00
parent 6f1aa5b812
commit 8778466d42
2 changed files with 138 additions and 181 deletions
+137 -180
View File
@@ -26,13 +26,12 @@ from __future__ import annotations
import sys
import logging
from enum import Enum
from time import time
from pathlib import Path
from datetime import datetime
from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot
from PyQt5.QtGui import QCursor, QIcon, QKeySequence
from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence
from PyQt5.QtWidgets import (
qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter,
QStackedWidget, QVBoxLayout, QWidget
@@ -64,7 +63,7 @@ from novelwriter.core.project import NWProject
from novelwriter.core.coretools import ProjectBuilder
from novelwriter.enum import (
nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView
nwDocAction, nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView
)
from novelwriter.common import getGuiItem, hexToInt
from novelwriter.constants import nwFiles
@@ -92,7 +91,7 @@ class GuiMain(QMainWindow):
shouldn't need).
"""
def __init__(self):
def __init__(self) -> None:
super().__init__()
logger.debug("Create: GUI")
@@ -328,9 +327,8 @@ class GuiMain(QMainWindow):
return
def clearGUI(self):
"""Wrapper function to clear all sub-elements of the main GUI.
"""
def clearGUI(self) -> None:
"""Clear all sub-elements of the main GUI."""
# Project Area
self.projView.clearProject()
self.novelView.clearProject()
@@ -346,16 +344,15 @@ class GuiMain(QMainWindow):
self.mainStatus.clearStatus()
self._updateWindowTitle()
return True
return
def initMain(self):
"""Initialise elements that depend on user settings.
"""
def initMain(self) -> None:
"""Initialise elements that depend on user settings."""
self.asProjTimer.setInterval(int(CONFIG.autoSaveProj*1000))
self.asDocTimer.setInterval(int(CONFIG.autoSaveDoc*1000))
return True
return
def postLaunchTasks(self, cmdOpen):
def postLaunchTasks(self, cmdOpen: str | None) -> None:
"""This function is called after the main window is created to
determine what to open or show after initialisation.
"""
@@ -377,9 +374,8 @@ class GuiMain(QMainWindow):
# Project Actions
##
def newProject(self, projData=None):
"""Create a new project via the new project wizard.
"""
def newProject(self, projData: dict | None = None) -> bool:
"""Create a new project via the new project wizard."""
if self.hasProject:
if not self.closeProject():
self.makeAlert(self.tr(
@@ -414,7 +410,7 @@ class GuiMain(QMainWindow):
return True
def closeProject(self, isYes=False):
def closeProject(self, isYes: bool = False) -> bool:
"""Close the project if one is open. isYes is passed on from the
close application event so the user doesn't get prompted twice
to confirm.
@@ -468,9 +464,8 @@ class GuiMain(QMainWindow):
return saveOK
def openProject(self, projFile):
"""Open a project from a projFile path.
"""
def openProject(self, projFile: str | Path | None) -> bool:
"""Open a project from a projFile path."""
if projFile is None:
return False
@@ -579,25 +574,21 @@ class GuiMain(QMainWindow):
return True
def saveProject(self, autoSave=False):
"""Save the current project.
"""
def saveProject(self, autoSave: bool = False) -> bool:
"""Save the current project."""
if not self.hasProject:
logger.error("No project open")
return False
self.projView.saveProjectTasks()
self.theProject.saveProject(autoSave=autoSave)
return True
##
# Document Actions
##
def closeDocument(self, beforeOpen=False):
"""Close the document and clear the editor and title field.
"""
def closeDocument(self, beforeOpen: bool = False) -> bool:
"""Close the document and clear the editor and title field."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -615,14 +606,14 @@ class GuiMain(QMainWindow):
return True
def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False):
"""Open a specific document, optionally at a given line.
"""
def openDocument(self, tHandle: str | None, tLine: int | None = None,
changeFocus: bool = True, doScroll: bool = False) -> bool:
"""Open a specific document, optionally at a given line."""
if not self.hasProject:
logger.error("No project open")
return False
if not self.theProject.tree.checkType(tHandle, nwItemType.FILE):
if not tHandle or not self.theProject.tree.checkType(tHandle, nwItemType.FILE):
logger.debug("Requested item '%s' is not a document", tHandle)
return False
@@ -646,7 +637,7 @@ class GuiMain(QMainWindow):
return True
def openNextDocument(self, tHandle, wrapAround=False):
def openNextDocument(self, tHandle: str, wrapAround: bool = False) -> bool:
"""Opens the next document in the project tree, following the
document with the given handle. Stops when reaching the end.
"""
@@ -677,20 +668,16 @@ class GuiMain(QMainWindow):
return False
def saveDocument(self):
"""Save the current documents.
"""
def saveDocument(self) -> bool:
"""Save the current documents."""
if not self.hasProject:
logger.error("No project open")
return False
self.docEditor.saveText()
return True
def viewDocument(self, tHandle=None, sTitle=None):
"""Load a document for viewing in the view panel.
"""
def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool:
"""Load a document for viewing in the view panel."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -732,7 +719,7 @@ class GuiMain(QMainWindow):
return True
def importDocument(self):
def importDocument(self) -> bool:
"""Import the text contained in an out-of-project text file, and
insert the text into the currently open document.
"""
@@ -788,16 +775,16 @@ class GuiMain(QMainWindow):
return True
def passDocumentAction(self, theAction):
def passDocumentAction(self, action: nwDocAction) -> None:
"""Pass on document action to the document viewer if it has
focus, or pass it to the document editor if it or any of
its child widgets have focus. If neither has focus, ignore the
action.
"""
if self.docViewer.hasFocus():
self.docViewer.docAction(theAction)
self.docViewer.docAction(action)
elif self.docEditor.hasFocus():
self.docEditor.docAction(theAction)
self.docEditor.docAction(action)
else:
logger.debug("Action cancelled as neither editor nor viewer has focus")
return
@@ -806,7 +793,7 @@ class GuiMain(QMainWindow):
# Tree Item Actions
##
def openSelectedItem(self):
def openSelectedItem(self) -> bool:
"""Open the selected item from the tree that is currently
active. It is not checked that the item is actually a document.
That should be handled by the openDocument function.
@@ -838,9 +825,8 @@ class GuiMain(QMainWindow):
return True
def editItemLabel(self, tHandle=None):
"""Open the edit item dialog.
"""
def editItemLabel(self, tHandle: str | None = None) -> bool:
"""Open the edit item dialog."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -851,15 +837,13 @@ class GuiMain(QMainWindow):
return True
def rebuildTrees(self):
"""Rebuild the project tree.
"""
def rebuildTrees(self) -> None:
"""Rebuild the project tree."""
self.projView.populateTree()
return
def rebuildIndex(self, beQuiet=False):
"""Rebuild the entire index.
"""
def rebuildIndex(self, beQuiet: bool = False) -> bool:
"""Rebuild the entire index."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -892,7 +876,7 @@ class GuiMain(QMainWindow):
# Main Dialogs
##
def showProjectLoadDialog(self):
def showProjectLoadDialog(self) -> None:
"""Open the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a
browse button for projects not yet cached. Selecting to create a
@@ -907,11 +891,10 @@ class GuiMain(QMainWindow):
elif dlgProj.openState == GuiProjectLoad.NEW_STATE:
self.newProject()
return True
return
def showNewProjectDialog(self):
"""Open the wizard and assemble a project options dict.
"""
def showNewProjectDialog(self) -> dict | None:
"""Open the wizard and assemble a project options dict."""
newProj = GuiProjectWizard(self)
newProj.exec_()
@@ -920,9 +903,8 @@ class GuiMain(QMainWindow):
return None
def showPreferencesDialog(self):
"""Open the preferences dialog.
"""
def showPreferencesDialog(self) -> None:
"""Open the preferences dialog."""
dlgConf = GuiPreferences(self)
dlgConf.exec_()
@@ -967,9 +949,8 @@ class GuiMain(QMainWindow):
return
@pyqtSlot(int)
def showProjectSettingsDialog(self, focusTab=GuiProjectSettings.TAB_MAIN):
"""Open the project settings dialog.
"""
def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> bool:
"""Open the project settings dialog."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -986,9 +967,8 @@ class GuiMain(QMainWindow):
return True
def showProjectDetailsDialog(self):
"""Open the project details dialog.
"""
def showProjectDetailsDialog(self) -> bool:
"""Open the project details dialog."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -1006,9 +986,8 @@ class GuiMain(QMainWindow):
return True
@pyqtSlot()
def showBuildManuscriptDialog(self):
"""Open the build manuscript dialog.
"""
def showBuildManuscriptDialog(self) -> bool:
"""Open the build manuscript dialog."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -1027,9 +1006,8 @@ class GuiMain(QMainWindow):
return True
def showLoremIpsumDialog(self):
"""Open the insert lorem ipsum text dialog.
"""
def showLoremIpsumDialog(self) -> bool:
"""Open the insert lorem ipsum text dialog."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -1046,9 +1024,8 @@ class GuiMain(QMainWindow):
return True
def showProjectWordListDialog(self):
"""Open the project word list dialog.
"""
def showProjectWordListDialog(self) -> bool:
"""Open the project word list dialog."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -1062,9 +1039,8 @@ class GuiMain(QMainWindow):
return True
def showWritingStatsDialog(self):
"""Open the session stats dialog.
"""
def showWritingStatsDialog(self) -> bool:
"""Open the session stats dialog."""
if not self.hasProject:
logger.error("No project open")
return False
@@ -1082,9 +1058,8 @@ class GuiMain(QMainWindow):
return True
def showAboutNWDialog(self, showNotes=False):
"""Show the about dialog for novelWriter.
"""
def showAboutNWDialog(self, showNotes: bool = False) -> bool:
"""Show the about dialog for novelWriter."""
dlgAbout = getGuiItem("GuiAbout")
if dlgAbout is None:
dlgAbout = GuiAbout(self)
@@ -1101,16 +1076,14 @@ class GuiMain(QMainWindow):
return True
def showAboutQtDialog(self):
"""Show the about dialog for Qt.
"""
def showAboutQtDialog(self) -> None:
"""Show the about dialog for Qt."""
msgBox = QMessageBox()
msgBox.aboutQt(self, "About Qt")
return True
return
def showUpdatesDialog(self):
"""Show the check for updates dialog.
"""
def showUpdatesDialog(self) -> None:
"""Show the check for updates dialog."""
dlgUpdate = getGuiItem("GuiUpdates")
if dlgUpdate is None:
dlgUpdate = GuiUpdates(self)
@@ -1124,7 +1097,8 @@ class GuiMain(QMainWindow):
return
def makeAlert(self, message, level=nwAlert.INFO, exception=None):
def makeAlert(self, message: list[str] | str, level: nwAlert = nwAlert.INFO,
exception: Exception | None = None) -> None:
"""Alert both the user and the logger at the same time. The
message can be either a string or a list of strings.
"""
@@ -1165,14 +1139,13 @@ class GuiMain(QMainWindow):
return
def askQuestion(self, title, question):
"""Ask the user a Yes/No question.
"""
def askQuestion(self, title: str, question: str) -> bool:
"""Ask the user a Yes/No question, and return the answer."""
msgBox = QMessageBox()
msgRes = msgBox.question(self, title, question, QMessageBox.Yes | QMessageBox.No)
return msgRes == QMessageBox.Yes
def reportConfErr(self):
def reportConfErr(self) -> bool:
"""Checks if the Config module has any errors to report, and let
the user know if this is the case. The Config module caches
errors since it is initialised before the GUI itself.
@@ -1222,9 +1195,8 @@ class GuiMain(QMainWindow):
return True
def switchFocus(self, paneNo):
"""Switch focus between main GUI views.
"""
def switchFocus(self, paneNo: nwWidget) -> None:
"""Switch focus between main GUI views."""
if paneNo == nwWidget.TREE:
tabIdx = self.projStack.currentIndex()
if tabIdx == self.idxProjView:
@@ -1242,16 +1214,14 @@ class GuiMain(QMainWindow):
self.outlineView.setTreeFocus()
return
def closeDocEditor(self):
"""Close the document edit panel. This does not hide the editor.
"""
def closeDocEditor(self) -> None:
"""Close the document editor. This does not hide the editor."""
self.closeDocument()
self.theProject.data.setLastHandle(None, "editor")
return
def closeDocViewer(self, byUser=True):
"""Close the document view panel.
"""
def closeDocViewer(self, byUser: bool = True) -> bool:
"""Close the document view panel."""
self.docViewer.clearViewer()
if byUser:
# Only reset the last handle if the user called this
@@ -1264,8 +1234,9 @@ class GuiMain(QMainWindow):
return not self.splitView.isVisible()
def toggleFocusMode(self):
"""Main GUI Focus Mode hides tree, view, statusbar and menu.
def toggleFocusMode(self) -> bool:
"""Handle toggle focus mode. The Main GUI Focus Mode hides tree,
view, statusbar and menu.
"""
if self.docEditor.docHandle() is None:
logger.error("No document open, so not activating Focus Mode")
@@ -1304,7 +1275,7 @@ class GuiMain(QMainWindow):
# Internal Functions
##
def _connectMenuActions(self):
def _connectMenuActions(self) -> None:
"""Connect to the main window all menu actions that need to be
available also when the main menu is hidden.
"""
@@ -1394,40 +1365,17 @@ class GuiMain(QMainWindow):
if isinstance(CONFIG.pdfDocs, Path):
self.addAction(self.mainMenu.aPdfDocs)
return True
return
def _updateWindowTitle(self, projName=None):
"""Set the window title and add the project's name.
"""
def _updateWindowTitle(self, projName: str | None = None) -> None:
"""Set the window title and add the project's name."""
winTitle = CONFIG.appName
if projName is not None:
winTitle += " - %s" % projName
self.setWindowTitle(winTitle)
return True
def _autoSaveProject(self):
"""Triggered by the autosave project timer to save the project.
"""
doSave = self.hasProject
doSave &= self.theProject.projChanged
doSave &= self.theProject.storage.isOpen()
if doSave:
logger.debug("Autosaving project")
self.saveProject(autoSave=True)
return
def _autoSaveDocument(self):
"""Triggered by the autosave document timer to save the
document.
"""
if self.hasProject and self.docEditor.docChanged():
logger.debug("Autosaving document")
self.saveDocument()
return
def _assembleProjectWizardData(self, newProj):
def _assembleProjectWizardData(self, newProj: GuiProjectWizard) -> dict:
"""Extract the user choices from the New Project Wizard and
store them in a dictionary.
"""
@@ -1459,72 +1407,68 @@ class GuiMain(QMainWindow):
return projData
def _getTagSource(self, tTag):
"""A wrapper function for the index lookup of a tag that will
display an alert if the tag cannot be found.
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 = self.theProject.index.getTagSource(tTag)
tHandle, sTitle = self.theProject.index.getTagSource(tag)
if tHandle is None:
self.makeAlert(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(
tTag, "F9"
tag, "F9"
), nwAlert.ERROR)
return None, None
return tHandle, sTitle
##
# Events
##
def closeEvent(self, theEvent):
def closeEvent(self, event: QCloseEvent):
"""Capture the closing event of the GUI and call the close
function to handle all the close process steps.
"""
if self.closeMain():
theEvent.accept()
event.accept()
else:
theEvent.ignore()
event.ignore()
return
##
# Private Slots
##
@pyqtSlot(str, Enum)
def _followTag(self, tTag, tMode):
"""Follow a tag after user interaction with a link.
"""
tHandle, sTitle = self._getTagSource(tTag)
@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:
if tMode == nwDocMode.EDIT:
if mode == nwDocMode.EDIT:
self.openDocument(tHandle)
elif tMode == nwDocMode.VIEW:
elif mode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return
@pyqtSlot(str, Enum, str, bool)
def _openDocument(self, tHandle, tMode, sTitle, setFocus):
"""Handle an open document request from one of the tree views.
"""
@pyqtSlot(str, nwDocMode, str, bool)
def _openDocument(self, tHandle: str, mode: nwDocMode, sTitle: str, setFocus: bool) -> None:
"""Handle an open document request."""
if tHandle is not None:
if tMode == nwDocMode.EDIT:
if mode == nwDocMode.EDIT:
tLine = None
hItem = self.theProject.index.getItemHeader(tHandle, sTitle)
if hItem is not None:
tLine = hItem.line
self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus)
elif tMode == nwDocMode.VIEW:
elif mode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return
@pyqtSlot(nwView)
def _changeView(self, view):
"""Handle the requested change of view from the GuiViewBar.
"""
def _changeView(self, view: nwView) -> None:
"""Handle the requested change of view from the GuiViewBar."""
if view == nwView.EDITOR:
# Only change the main stack, but not the project stack
self.mainStack.setCurrentWidget(self.splitMain)
@@ -1543,9 +1487,8 @@ class GuiMain(QMainWindow):
return
@pyqtSlot()
def _timeTick(self):
"""Triggered on every tick of the main timer.
"""
def _timeTick(self) -> None:
"""Process time tick of the main timer."""
if not self.hasProject:
return
@@ -1565,9 +1508,27 @@ class GuiMain(QMainWindow):
return
@pyqtSlot()
def _updateStatusWordCount(self):
"""Update the word count on the status bar.
"""
def _autoSaveProject(self) -> None:
"""Autosave of the project. This is a timer-activated slot."""
doSave = self.hasProject
doSave &= self.theProject.projChanged
doSave &= self.theProject.storage.isOpen()
if doSave:
logger.debug("Autosaving project")
self.saveProject(autoSave=True)
return
@pyqtSlot()
def _autoSaveDocument(self) -> None:
"""Autosave of the document. This is a timer-activated slot."""
if self.hasProject and self.docEditor.docChanged():
logger.debug("Autosaving document")
self.saveDocument()
return
@pyqtSlot()
def _updateStatusWordCount(self) -> None:
"""Update the word count on the status bar."""
if not self.hasProject:
self.mainStatus.setProjectStats(0, 0)
@@ -1584,7 +1545,7 @@ class GuiMain(QMainWindow):
return
@pyqtSlot()
def _keyPressReturn(self):
def _keyPressReturn(self) -> None:
"""Forward the return/enter keypress to the function that opens
the currently selected item.
"""
@@ -1592,10 +1553,8 @@ class GuiMain(QMainWindow):
return
@pyqtSlot()
def _keyPressEscape(self):
"""When the escape key is pressed somewhere in the main window,
do the following, in order:
"""
def _keyPressEscape(self) -> None:
"""Process escape keypress in the main window."""
if self.docEditor.docSearch.isVisible():
self.docEditor.closeSearch()
elif self.isFocusMode:
@@ -1603,22 +1562,20 @@ class GuiMain(QMainWindow):
return
@pyqtSlot(int)
def _mainStackChanged(self, stIndex):
"""Activated when the main window tab is changed.
"""
if stIndex == self.idxOutlineView:
def _mainStackChanged(self, index: int) -> None:
"""Process main window tab change."""
if index == self.idxOutlineView:
if self.hasProject:
self.outlineView.refreshTree()
return
@pyqtSlot(int)
def _projStackChanged(self, stIndex):
"""Activated when the project view tab is changed.
"""
def _projStackChanged(self, index: int) -> None:
"""Process project view tab change."""
sHandle = None
if stIndex == self.idxProjView:
if index == self.idxProjView:
sHandle = self.projView.getSelectedHandle()
elif stIndex == self.idxNovelView:
elif index == self.idxNovelView:
sHandle, _ = self.novelView.getSelectedHandle()
self.itemDetails.updateViewBox(sHandle)
return
+1 -1
View File
@@ -76,7 +76,7 @@ def testDlgAbout_QtDialog(monkeypatch, nwGUI):
# Open About
# All it can do is check against a crash
assert nwGUI.showAboutQtDialog() is True
nwGUI.showAboutQtDialog()
nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
# END Test testDlgAbout_QtDialog