Improve handling of project closing

This commit is contained in:
Veronica Berglyd Olsen
2023-08-13 23:32:10 +02:00
parent 150f6603cd
commit c4140d753c
18 changed files with 281 additions and 289 deletions
+6 -4
View File
@@ -382,9 +382,11 @@ class NWProject(QObject):
self._storage.runPostSaveTasks(autoSave=autoSave)
# Update recent projects
CONFIG.recentProjects.update(
self._storage.storagePath, self._data.name, sum(self._data.currCounts), saveTime
)
storePath = self._storage.storagePath
if storePath:
CONFIG.recentProjects.update(
storePath, self._data.name, sum(self._data.currCounts), saveTime
)
self._storage.writeLockFile()
self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name))
@@ -393,7 +395,7 @@ class NWProject(QObject):
return True
def closeProject(self, idleTime: float = 0.0) -> None:
"""Close the current project and clear all meta data."""
"""Close the project."""
logger.info("Closing project")
self._options.saveSettings()
self._tree.writeToCFile()
+6 -6
View File
@@ -129,7 +129,7 @@ class NWStatus:
def name(self, key: str | None) -> str:
"""Return the name associated with a given key."""
if key in self._store:
if key and key in self._store:
return self._store[key]["name"]
elif self._default is not None:
return self._store[self._default]["name"]
@@ -137,7 +137,7 @@ class NWStatus:
def cols(self, key: str | None) -> tuple[int, int, int]:
"""Return the colours associated with a given key."""
if key in self._store:
if key and key in self._store:
return self._store[key]["cols"]
elif self._default is not None:
return self._store[self._default]["cols"]
@@ -145,7 +145,7 @@ class NWStatus:
def count(self, key: str | None) -> int:
"""Return the count associated with a given key."""
if key in self._store:
if key and key in self._store:
return self._store[key]["count"]
elif self._default is not None:
return self._store[self._default]["count"]
@@ -153,7 +153,7 @@ class NWStatus:
def icon(self, key: str | None) -> QIcon:
"""Return the icon associated with a given key."""
if key in self._store:
if key and key in self._store:
return self._store[key]["icon"]
elif self._default is not None:
return self._store[self._default]["icon"]
@@ -186,9 +186,9 @@ class NWStatus:
self._store[key]["count"] = 0
return
def increment(self, key: str) -> None:
def increment(self, key: str | None) -> None:
"""Increment the counter for a given entry."""
if key in self._store:
if key and key in self._store:
self._store[key]["count"] += 1
return
+30 -27
View File
@@ -101,7 +101,7 @@ class GuiDocEditor(QTextEdit):
self._wordCount = 0 # Word count
self._paraCount = 0 # Paragraph count
self._lastEdit = 0 # Time stamp of last edit
self._lastActive = 0 # Time stamp of last activity
self._lastActive = 0.0 # Time stamp of last activity
self._lastFind = None # Position of the last found search word
self._bigDoc = False # Flag for very large document size
self._doReplace = False # Switch to temporarily disable auto-replace
@@ -188,6 +188,34 @@ class GuiDocEditor(QTextEdit):
return
##
# Properties
##
@property
def docChanged(self) -> bool:
"""Return the changed status of the document."""
return self._docChanged
@property
def docHandle(self) -> str | None:
"""Return the handle of the currently open document."""
return self._docHandle
@property
def lastActive(self) -> float:
"""Return the last active timestamp for the user."""
return self._lastActive
@property
def isEmpty(self) -> bool:
"""Check if the current document is empty."""
return self.document().isEmpty()
##
# Methods
##
def clearEditor(self):
"""Clear the current document and reset all document-related
flags and counters.
@@ -203,7 +231,7 @@ class GuiDocEditor(QTextEdit):
self._wordCount = 0
self._paraCount = 0
self._lastEdit = 0
self._lastActive = 0
self._lastActive = 0.0
self._lastFind = None
self._bigDoc = False
self._doReplace = False
@@ -580,31 +608,6 @@ class GuiDocEditor(QTextEdit):
return
##
# Properties
##
def docChanged(self):
"""Return the changed status of the document in the editor.
"""
return self._docChanged
def docHandle(self):
"""Return the handle of the currently open document. Return
None if no document is open.
"""
return self._docHandle
def lastActive(self):
"""Return the last active timestamp for the user.
"""
return self._lastActive
def isEmpty(self):
"""Wrapper function to check if the current document is empty.
"""
return self.document().isEmpty()
##
# Getters
##
+121 -134
View File
@@ -30,10 +30,11 @@ from __future__ import annotations
import logging
from enum import Enum
from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtCore import QPoint, Qt, QUrl, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtGui import (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
QMouseEvent, QResizeEvent, QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
)
from PyQt5.QtWidgets import (
qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton,
@@ -46,6 +47,9 @@ from novelwriter.error import logException
from novelwriter.constants import nwUnicode
from novelwriter.core.tohtml import ToHtml
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__)
@@ -53,7 +57,7 @@ class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui):
def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui)
logger.debug("Create: GuiDocViewer")
@@ -90,25 +94,43 @@ class GuiDocViewer(QTextBrowser):
return
def clearViewer(self):
"""Clear the content of the document and reset key variables.
"""
##
# Properties
##
@property
def docHandle(self) -> str | None:
"""Return the handle of the currently open document."""
return self._docHandle
@property
def scrollPosition(self) -> int:
"""Return the scrollbar position."""
vBar = self.verticalScrollBar()
if vBar.isVisible():
return vBar.value()
return 0
##
# Methods
##
def clearViewer(self) -> None:
"""Clear the content of the document and reset key variables."""
self.clear()
self.setSearchPaths([""])
self._docHandle = None
self.docHeader.setTitleFromHandle(self._docHandle)
return True
return
def updateTheme(self):
"""Update theme elements.
"""
def updateTheme(self) -> None:
"""Update theme elements."""
self.docHeader.updateTheme()
self.docFooter.updateTheme()
return
def initViewer(self):
"""Set editor settings from main config.
"""
def initViewer(self) -> None:
"""Set editor settings from main config."""
self._makeStyleSheet()
# Set Font
@@ -157,11 +179,10 @@ class GuiDocViewer(QTextBrowser):
if self._docHandle is not None:
self.reloadText()
return True
return
def loadText(self, tHandle, updateHistory=True):
"""Load text into the viewer from an item handle.
"""
def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
"""Load text into the viewer from an item handle."""
if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
logger.warning("Item not found")
return False
@@ -224,23 +245,20 @@ class GuiDocViewer(QTextBrowser):
return True
def reloadText(self):
"""Reload the text in the current document.
"""
self.loadText(self._docHandle, updateHistory=False)
def reloadText(self) -> None:
"""Reload the text in the current document."""
if self._docHandle:
self.loadText(self._docHandle, updateHistory=False)
return
def redrawText(self):
"""Redraw the text by marking the document content as "dirty".
"""
def redrawText(self) -> None:
"""Redraw the text by marking the content as "dirty"."""
self.document().markContentsDirty(0, self.document().characterCount())
self.updateDocMargins()
return
def docAction(self, theAction):
"""Wrapper function for various document actions on the current
document.
"""
def docAction(self, theAction: nwDocAction) -> bool:
"""Process document actions on the current document."""
logger.debug("Requesting action: '%s'", theAction.name)
if self._docHandle is None:
logger.error("No document open")
@@ -258,9 +276,8 @@ class GuiDocViewer(QTextBrowser):
return False
return True
def navigateTo(self, tAnchor):
"""Go to a specific #link in the document.
"""
def navigateTo(self, tAnchor: str) -> bool:
"""Go to a specific #link in the document."""
if not isinstance(tAnchor, str):
return False
if tAnchor.startswith("#"):
@@ -268,27 +285,13 @@ class GuiDocViewer(QTextBrowser):
self.setSource(QUrl(tAnchor))
return True
def navBackward(self):
"""Navigate backwards in the document view history.
"""
self.docHistory.backward()
return
def navForward(self):
"""Navigate forwards in the document view history.
"""
self.docHistory.forward()
return
def clearNavHistory(self):
"""Clear the navigation history.
"""
def clearNavHistory(self) -> None:
"""Clear the navigation history."""
self.docHistory.clear()
return
def updateDocMargins(self):
"""Automatically adjust the margins so the text is centred.
"""
def updateDocMargins(self) -> None:
"""Automatically adjust the margins so the text is centred."""
wW = self.width()
wH = self.height()
cM = CONFIG.getTextMargin()
@@ -320,41 +323,20 @@ class GuiDocViewer(QTextBrowser):
# Setters
##
def setScrollPosition(self, thePos):
"""Set the scrollbar position.
"""
def setScrollPosition(self, pos: int) -> None:
"""Set the scrollbar position."""
vBar = self.verticalScrollBar()
if vBar.isVisible():
vBar.setValue(thePos)
vBar.setValue(pos)
return
##
# Getters
##
def docHandle(self):
"""Return the handle of the currently open document. Returns
None if no document is open.
"""
return self._docHandle
def getScrollPosition(self):
"""Get the scrollbar position. Returns 0 if no scrollbar.
"""
vBar = self.verticalScrollBar()
if vBar.isVisible():
return vBar.value()
return 0
##
# Public Slots
##
@pyqtSlot(str)
def updateDocInfo(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
def updateDocInfo(self, tHandle: str) -> None:
"""Update the header titlebar if needed."""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins()
@@ -364,11 +346,22 @@ class GuiDocViewer(QTextBrowser):
# Private Slots
##
@pyqtSlot()
def navBackward(self) -> None:
"""Navigate backwards in the document view history."""
self.docHistory.backward()
return
@pyqtSlot()
def navForward(self) -> None:
"""Navigate forwards in the document view history."""
self.docHistory.forward()
return
@pyqtSlot("QUrl")
def _linkClicked(self, theURL):
"""Process a clicked link internally in the document.
"""
theLink = theURL.url()
def _linkClicked(self, url: QUrl) -> None:
"""Process a clicked link internally in the document."""
theLink = url.url()
logger.debug("Clicked link: '%s'", theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
@@ -377,9 +370,8 @@ class GuiDocViewer(QTextBrowser):
return
@pyqtSlot("QPoint")
def _openContextMenu(self, thePos):
"""Triggered by right click to open the context menu.
"""
def _openContextMenu(self, point: QPoint) -> None:
"""Open context menu at location."""
userCursor = self.textCursor()
userSelection = userCursor.hasSelection()
@@ -404,18 +396,18 @@ class GuiDocViewer(QTextBrowser):
mnuSelWord = QAction(self.tr("Select Word"), mnuContext)
mnuSelWord.triggered.connect(
lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos)
lambda: self._makePosSelection(QTextCursor.WordUnderCursor, point)
)
mnuContext.addAction(mnuSelWord)
mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext)
mnuSelPara.triggered.connect(
lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos)
lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, point)
)
mnuContext.addAction(mnuSelPara)
# Open the context menu
mnuContext.exec_(self.viewport().mapToGlobal(thePos))
mnuContext.exec_(self.viewport().mapToGlobal(point))
return
@@ -423,37 +415,33 @@ class GuiDocViewer(QTextBrowser):
# Events
##
def resizeEvent(self, theEvent):
"""If the text editor is resized, we must make sure the document
has its margins adjusted according to user preferences.
"""
def resizeEvent(self, event: QResizeEvent) -> None:
"""Update document margins when widget is resized."""
self.updateDocMargins()
super().resizeEvent(theEvent)
super().resizeEvent(event)
return
def mouseReleaseEvent(self, theEvent):
"""Capture mouse click events on the document.
"""
if theEvent.button() == Qt.BackButton:
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
"""Capture mouse click events on the document."""
if event.button() == Qt.BackButton:
self.navBackward()
elif theEvent.button() == Qt.ForwardButton:
elif event.button() == Qt.ForwardButton:
self.navForward()
else:
super().mouseReleaseEvent(theEvent)
super().mouseReleaseEvent(event)
return
##
# Internal Functions
##
def _makeSelection(self, selMode):
"""Wrapper function to select text based on a selection mode.
"""
def _makeSelection(self, selType: QTextCursor.SelectionType) -> None:
"""Handle select of text based on a selection mode."""
theCursor = self.textCursor()
theCursor.clearSelection()
theCursor.select(selMode)
theCursor.select(selType)
if selMode == QTextCursor.BlockUnderCursor:
if selType == QTextCursor.BlockUnderCursor:
# This selection mode also selects the preceding paragraph
# separator, which we want to avoid.
posS = theCursor.selectionStart()
@@ -467,19 +455,18 @@ class GuiDocViewer(QTextBrowser):
return
def _makePosSelection(self, selMode, thePos):
"""Wrapper function to select text based on selection mode, but
first move cursor to given position.
"""
theCursor = self.cursorForPosition(thePos)
def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None:
"""Handle text selection at a given location."""
theCursor = self.cursorForPosition(pos)
self.setTextCursor(theCursor)
self._makeSelection(selMode)
self._makeSelection(selType)
return
def _makeStyleSheet(self):
def _makeStyleSheet(self) -> None:
"""Generate an appropriate style sheet for the document viewer,
based on the current syntax highlighter theme,
"""
pTheme = SHARED.theme
styleSheet = (
"body {{"
" color: rgb({tColR}, {tColG}, {tColB});"
@@ -506,31 +493,31 @@ class GuiDocViewer(QTextBrowser):
" text-align: center;"
"}}\n"
).format(
tColR=SHARED.theme.colText[0],
tColG=SHARED.theme.colText[1],
tColB=SHARED.theme.colText[2],
hColR=SHARED.theme.colHead[0],
hColG=SHARED.theme.colHead[1],
hColB=SHARED.theme.colHead[2],
aColR=SHARED.theme.colVal[0],
aColG=SHARED.theme.colVal[1],
aColB=SHARED.theme.colVal[2],
eColR=SHARED.theme.colEmph[0],
eColG=SHARED.theme.colEmph[1],
eColB=SHARED.theme.colEmph[2],
kColR=SHARED.theme.colKey[0],
kColG=SHARED.theme.colKey[1],
kColB=SHARED.theme.colKey[2],
cColR=SHARED.theme.colHidden[0],
cColG=SHARED.theme.colHidden[1],
cColB=SHARED.theme.colHidden[2],
mColR=SHARED.theme.colMod[0],
mColG=SHARED.theme.colMod[1],
mColB=SHARED.theme.colMod[2],
tColR=pTheme.colText[0],
tColG=pTheme.colText[1],
tColB=pTheme.colText[2],
hColR=pTheme.colHead[0],
hColG=pTheme.colHead[1],
hColB=pTheme.colHead[2],
aColR=pTheme.colVal[0],
aColG=pTheme.colVal[1],
aColB=pTheme.colVal[2],
eColR=pTheme.colEmph[0],
eColG=pTheme.colEmph[1],
eColB=pTheme.colEmph[2],
kColR=pTheme.colKey[0],
kColG=pTheme.colKey[1],
kColB=pTheme.colKey[2],
cColR=pTheme.colHidden[0],
cColG=pTheme.colHidden[1],
cColB=pTheme.colHidden[2],
mColR=pTheme.colMod[0],
mColG=pTheme.colMod[1],
mColB=pTheme.colMod[2],
)
self.document().setDefaultStyleSheet(styleSheet)
return True
return
# END Class GuiDocViewer
@@ -628,7 +615,7 @@ class GuiDocViewHistory:
"""Update the scrollbar position of the previous entry.
"""
if self._prevPos >= 0 and self._prevPos < len(self._posHistory):
self._posHistory[self._prevPos] = self.docViewer.getScrollPosition()
self._posHistory[self._prevPos] = self.docViewer.scrollPosition
return
def _updateNavButtons(self):
@@ -864,7 +851,7 @@ class GuiDocViewHeader(QWidget):
def _refreshDocument(self):
"""Reload the content of the document.
"""
if self.docViewer.docHandle() == self.mainGui.docEditor.docHandle():
if self.docViewer.docHandle == self.mainGui.docEditor.docHandle:
self.mainGui.saveDocument()
self.docViewer.reloadText()
return
@@ -1102,8 +1089,8 @@ class GuiDocViewFooter(QWidget):
"""
logger.debug("Reference sticky is %s", str(theState))
self.docViewer.stickyRef = theState
if not theState and self.docViewer.docHandle() is not None:
self.viewMeta.refreshReferences(self.docViewer.docHandle())
if not theState and self.docViewer.docHandle is not None:
self.viewMeta.refreshReferences(self.docViewer.docHandle)
return
@pyqtSlot(bool)
+27 -31
View File
@@ -25,19 +25,24 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
from novelwriter import CONFIG, SHARED
from novelwriter.constants import trConst, nwLabels
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__)
class GuiItemDetails(QWidget):
def __init__(self, mainGui):
def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui)
logger.debug("Create: GuiItemDetails")
@@ -187,35 +192,28 @@ class GuiItemDetails(QWidget):
# Class Methods
##
def clearDetails(self):
"""Clear all the data values.
"""
def clearDetails(self) -> None:
"""Clear all the data values."""
self._itemHandle = None
self.labelIcon.setPixmap(QPixmap(1, 1))
self.statusIcon.setPixmap(QPixmap(1, 1))
self.classIcon.setText("")
self.usageIcon.setText("")
self.labelData.setText("")
self.statusData.setText("")
self.classData.setText("")
self.usageData.setText("")
self.cCountData.setText("")
self.wCountData.setText("")
self.pCountData.setText("")
self.labelIcon.clear()
self.labelData.clear()
self.statusIcon.clear()
self.statusData.clear()
self.classIcon.clear()
self.classData.clear()
self.usageIcon.clear()
self.usageData.clear()
self.cCountData.clear()
self.wCountData.clear()
self.pCountData.clear()
return
def refreshDetails(self):
"""Reload the content of the details panel.
"""
def refreshDetails(self) -> None:
"""Reload the content of the details panel."""
self.updateViewBox(self._itemHandle)
def updateTheme(self):
"""Update theme elements.
"""
def updateTheme(self) -> None:
"""Update theme elements."""
self.updateViewBox(self._itemHandle)
return
@@ -224,9 +222,8 @@ class GuiItemDetails(QWidget):
##
@pyqtSlot(str)
def updateViewBox(self, tHandle):
"""Populate the details box from a given handle.
"""
def updateViewBox(self, tHandle: str) -> None:
"""Populate the details box from a given handle."""
if tHandle is None:
self.clearDetails()
return
@@ -294,7 +291,7 @@ class GuiItemDetails(QWidget):
return
@pyqtSlot(str, int, int, int)
def updateCounts(self, tHandle, cC, wC, pC):
def updateCounts(self, tHandle: str, cC: int, wC: int, pC: int) -> None:
"""Update the counts if the handle is the same as the one we're
already showing. Otherwise, do nothing.
"""
@@ -302,7 +299,6 @@ class GuiItemDetails(QWidget):
self.cCountData.setText(f"{cC:n}")
self.wCountData.setText(f"{wC:n}")
self.pCountData.setText(f"{pC:n}")
return
# END Class GuiItemDetails
+2 -2
View File
@@ -349,13 +349,13 @@ class GuiMainMenu(QMenuBar):
# View > Go Backward
self.aViewPrev = QAction(self.tr("Navigate Backward"), self)
self.aViewPrev.setShortcut("Alt+Left")
self.aViewPrev.triggered.connect(lambda: self.mainGui.docViewer.navBackward())
self.aViewPrev.triggered.connect(self.mainGui.docViewer.navBackward)
self.viewMenu.addAction(self.aViewPrev)
# View > Go Forward
self.aViewNext = QAction(self.tr("Navigate Forward"), self)
self.aViewNext.setShortcut("Alt+Right")
self.aViewNext.triggered.connect(lambda: self.mainGui.docViewer.navForward())
self.aViewNext.triggered.connect(self.mainGui.docViewer.navForward)
self.viewMenu.addAction(self.aViewNext)
# View > Separator
+4 -5
View File
@@ -106,7 +106,7 @@ class GuiNovelView(QWidget):
self.novelTree.initSettings()
return
def clearProject(self):
def clearNovelView(self):
"""Clear project-related GUI content.
"""
self.novelTree.clearContent()
@@ -130,8 +130,7 @@ class GuiNovelView(QWidget):
"GuiNovelView", "lastColSize", 25
)
self.clearProject()
self.clearNovelView()
self.novelBar.buildNovelRootMenu()
self.novelBar.setLastColType(lastCol, doRefresh=False)
self.novelBar.setCurrentRoot(lastNovel)
@@ -142,13 +141,13 @@ class GuiNovelView(QWidget):
return
def closeProjectTasks(self):
"""Run closing project tasks.
"""
"""Run closing project tasks."""
lastColType = self.novelTree.lastColType
lastColSize = self.novelTree.lastColSize
pOptions = SHARED.project.options
pOptions.setValue("GuiNovelView", "lastCol", lastColType)
pOptions.setValue("GuiNovelView", "lastColSize", lastColSize)
self.clearNovelView()
return
def setTreeFocus(self):
+3 -2
View File
@@ -118,7 +118,7 @@ class GuiOutlineView(QWidget):
self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline"))
return
def clearProject(self):
def clearOutline(self):
"""Clear project-related GUI content.
"""
self.outlineData.clearDetails()
@@ -134,7 +134,7 @@ class GuiOutlineView(QWidget):
logger.debug("Setting outline tree to root item '%s'", lastOutline)
self.clearProject()
self.clearOutline()
self.outlineBar.populateNovelList()
self.outlineBar.setCurrentRoot(lastOutline)
self.outlineBar.setEnabled(True)
@@ -144,6 +144,7 @@ class GuiOutlineView(QWidget):
def closeProjectTasks(self):
self.outlineTree.closeProjectTasks()
self.outlineData.updateClasses()
self.clearOutline()
return
def splitSizes(self):
+2 -2
View File
@@ -165,7 +165,7 @@ class GuiProjectView(QWidget):
self.projTree.initSettings()
return
def clearProject(self) -> None:
def clearProjectView(self) -> None:
"""Clear project-related GUI content."""
self.projBar.clearContent()
self.projBar.setEnabled(False)
@@ -964,7 +964,7 @@ class GuiProjectTree(QTreeWidget):
trItemP.takeChild(tIndex)
for dHandle in reversed(self.getTreeFromHandle(tHandle)):
if self.mainGui.docEditor.docHandle() == dHandle:
if self.mainGui.docEditor.docHandle == dHandle:
self.mainGui.closeDocument()
SHARED.project.removeItem(dHandle)
self._treeMap.pop(dHandle, None)
+19 -32
View File
@@ -236,7 +236,7 @@ class GuiMain(QMainWindow):
# Connect Signals
# ===============
SHARED.project.projectStatusChanged.connect(self.mainStatus.doUpdateProjectStatus)
SHARED.projectStatusChanged.connect(self.mainStatus.doUpdateProjectStatus)
self.viewsBar.viewChangeRequested.connect(self._changeView)
@@ -337,25 +337,6 @@ class GuiMain(QMainWindow):
return
def clearGUI(self) -> None:
"""Clear all sub-elements of the main GUI."""
# Project Area
self.projView.clearProject()
self.novelView.clearProject()
self.itemDetails.clearDetails()
# Work Area
self.docEditor.clearEditor()
self.docEditor.setDictionaries()
self.closeDocViewer(byUser=False)
self.outlineView.clearProject()
# General
self.mainStatus.clearStatus()
self._updateWindowTitle()
return
def initMain(self) -> None:
"""Initialise elements that depend on user settings."""
self.asProjTimer.setInterval(int(CONFIG.autoSaveProj*1000))
@@ -446,7 +427,7 @@ class GuiMain(QMainWindow):
if not msgYes:
return False
if self.docEditor.docChanged():
if self.docEditor.docChanged:
self.saveDocument()
saveOK = self.saveProject()
@@ -462,14 +443,20 @@ class GuiMain(QMainWindow):
if saveOK:
self.closeDocument()
self.docViewer.clearNavHistory()
self.closeDocViewer(byUser=False)
self.outlineView.closeProjectTasks()
self.novelView.closeProjectTasks()
self.projView.clearProjectView()
self.itemDetails.clearDetails()
self.mainStatus.clearStatus()
SHARED.closeProject(self.idleTime)
self.idleRefTime = time()
self.idleTime = 0.0
self.clearGUI()
self.docEditor.setDictionaries()
self._updateWindowTitle()
self._changeView(nwView.PROJECT)
return saveOK
@@ -594,7 +581,7 @@ class GuiMain(QMainWindow):
self.toggleFocusMode()
self.docEditor.saveCursorPosition()
if self.docEditor.docChanged():
if self.docEditor.docChanged:
self.saveDocument()
self.docEditor.clearEditor()
if not beforeOpen:
@@ -614,7 +601,7 @@ class GuiMain(QMainWindow):
return False
self._changeView(nwView.EDITOR)
cHandle = self.docEditor.docHandle()
cHandle = self.docEditor.docHandle
if cHandle == tHandle:
self.docEditor.setCursorLine(tLine)
if changeFocus:
@@ -682,7 +669,7 @@ class GuiMain(QMainWindow):
logger.debug("Viewing document, but no handle provided")
if self.docEditor.hasFocus():
tHandle = self.docEditor.docHandle()
tHandle = self.docEditor.docHandle
if tHandle is not None:
self.saveDocument()
@@ -750,13 +737,13 @@ class GuiMain(QMainWindow):
), level=nwAlert.ERROR, exc=exc)
return False
if self.docEditor.docHandle() is None:
if self.docEditor.docHandle is None:
self.makeAlert(self.tr(
"Please open a document to import the text file into."
), level=nwAlert.ERROR)
return False
if not self.docEditor.isEmpty():
if not self.docEditor.isEmpty:
msgYes = self.askQuestion(self.tr(
"Importing the file will overwrite the current content of "
"the document. Do you want to proceed?"
@@ -825,7 +812,7 @@ class GuiMain(QMainWindow):
return False
if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode):
tHandle = self.docEditor.docHandle()
tHandle = self.docEditor.docHandle
self.projView.renameTreeItem(tHandle)
return True
@@ -1219,7 +1206,7 @@ class GuiMain(QMainWindow):
"""Handle toggle focus mode. The Main GUI Focus Mode hides tree,
view, statusbar and menu.
"""
if self.docEditor.docHandle() is None:
if self.docEditor.docHandle is None:
logger.error("No document open, so not activating Focus Mode")
return False
@@ -1242,7 +1229,7 @@ class GuiMain(QMainWindow):
if self.splitView.isVisible():
self.splitView.setVisible(False)
elif self.docViewer.docHandle() is not None:
elif self.docViewer.docHandle is not None:
self.splitView.setVisible(True)
return True
@@ -1474,7 +1461,7 @@ class GuiMain(QMainWindow):
return
currTime = time()
editIdle = currTime - self.docEditor.lastActive() > CONFIG.userIdleTime
editIdle = currTime - self.docEditor.lastActive > CONFIG.userIdleTime
userIdle = qApp.applicationState() != Qt.ApplicationActive
if editIdle or userIdle:
@@ -1502,7 +1489,7 @@ class GuiMain(QMainWindow):
@pyqtSlot()
def _autoSaveDocument(self) -> None:
"""Autosave of the document. This is a timer-activated slot."""
if SHARED.hasProject and self.docEditor.docChanged():
if SHARED.hasProject and self.docEditor.docChanged:
logger.debug("Autosaving document")
self.saveDocument()
return
+18 -1
View File
@@ -28,6 +28,8 @@ import logging
from typing import TYPE_CHECKING
from pathlib import Path
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
from novelwriter.gui.theme import GuiTheme
@@ -36,9 +38,12 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
class SharedData:
class SharedData(QObject):
projectStatusChanged = pyqtSignal(bool)
def __init__(self) -> None:
super().__init__()
self._gui = None
self._theme = None
self._project = None
@@ -121,6 +126,16 @@ class SharedData:
"""Remove the project lock."""
return self.project.storage.clearLockFile()
##
# Internal Slots
##
@pyqtSlot(bool)
def _processProjectStatusChange(self, state: bool) -> None:
"""Forward the project status slot."""
self.projectStatusChanged.emit(state)
return
##
# Internal Functions
##
@@ -129,8 +144,10 @@ class SharedData:
"""Create a new project instance."""
from novelwriter.core.project import NWProject
if isinstance(self._project, NWProject):
self._project.projectStatusChanged.disconnect()
self._project.deleteLater()
self._project = NWProject(self.mainGui)
self._project.projectStatusChanged.connect(self._processProjectStatusChange)
return
# END Class SharedData
+9 -9
View File
@@ -194,10 +194,10 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd):
assert nwGUI.docEditor.getText() == "### New Scene\n\nSome\ntext.\nMore\u00a0text.\n"
# Check Propertoes
assert nwGUI.docEditor.docChanged() is True
assert nwGUI.docEditor.docHandle() == C.hSceneDoc
assert nwGUI.docEditor.lastActive() > 0.0
assert nwGUI.docEditor.isEmpty() is False
assert nwGUI.docEditor.docChanged is True
assert nwGUI.docEditor.docHandle == C.hSceneDoc
assert nwGUI.docEditor.lastActive > 0.0
assert nwGUI.docEditor.isEmpty is False
# Cursor Position
assert nwGUI.docEditor.setCursorPosition(None) is False
@@ -1361,7 +1361,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Next match
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert nwGUI.docEditor.docHandle() == "2426c6f0ca922" # Next document
assert nwGUI.docEditor.docHandle == "2426c6f0ca922" # Next document
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
@@ -1371,11 +1371,11 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
assert nwGUI.docEditor.docSearch.doNextFile is True
assert nwGUI.docEditor.docSearch.setSearchText("abcdef")
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert nwGUI.docEditor.docHandle() != "2426c6f0ca922"
assert nwGUI.docEditor.docHandle() == "04468803b92e1"
assert nwGUI.docEditor.docHandle != "2426c6f0ca922"
assert nwGUI.docEditor.docHandle == "04468803b92e1"
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert nwGUI.docEditor.docHandle() != "04468803b92e1"
assert nwGUI.docEditor.docHandle() == "7a992350f3eb6"
assert nwGUI.docEditor.docHandle != "04468803b92e1"
assert nwGUI.docEditor.docHandle == "7a992350f3eb6"
# Toggle Replace
nwGUI.docEditor.beginReplace()
+6 -6
View File
@@ -50,7 +50,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8")
theRect = nwGUI.projView.projTree.visualItemRect(theItem)
qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center())
assert nwGUI.docViewer.docHandle() == "88243afbe5ed8"
assert nwGUI.docViewer.docHandle == "88243afbe5ed8"
# Reload the text
origText = nwGUI.docViewer.toPlainText()
@@ -97,7 +97,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
# Close document
nwGUI.docViewer.docHeader._closeDocument()
assert nwGUI.docViewer.docHandle() is None
assert nwGUI.docViewer.docHandle is None
# Action on no document
assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False
@@ -114,17 +114,17 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
theRect = nwGUI.docViewer.cursorRect()
# qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100)
nwGUI.docViewer._linkClicked(QUrl("#char=Bod"))
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
assert nwGUI.docViewer.docHandle == "4c4f28287af27"
# Click mouse nav buttons
qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.BackButton, pos=theRect.center(), delay=100)
assert nwGUI.docViewer.docHandle() == "88243afbe5ed8"
assert nwGUI.docViewer.docHandle == "88243afbe5ed8"
qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.ForwardButton, pos=theRect.center(), delay=100)
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
assert nwGUI.docViewer.docHandle == "4c4f28287af27"
# Scroll bar default on empty document
nwGUI.docViewer.clear()
assert nwGUI.docViewer.getScrollPosition() == 0
assert nwGUI.docViewer.scrollPosition == 0
nwGUI.docViewer.reloadText()
# Change document title
+8 -8
View File
@@ -153,10 +153,10 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI.projStack.setCurrentIndex(0)
with monkeypatch.context() as mp:
mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
assert nwGUI.docEditor.docHandle is None
nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.docEditor.docHandle == sHandle
assert nwGUI.closeDocument() is True
# Novel Tree has focus
@@ -164,11 +164,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True)
with monkeypatch.context() as mp:
mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
assert nwGUI.docEditor.docHandle is None
selItem = nwGUI.novelView.novelTree.topLevelItem(2)
nwGUI.novelView.novelTree.setCurrentItem(selItem)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.docEditor.docHandle == sHandle
assert nwGUI.closeDocument() is True
# Project Outline has focus
@@ -176,11 +176,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI.switchFocus(nwWidget.OUTLINE)
with monkeypatch.context() as mp:
mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
assert nwGUI.docEditor.docHandle is None
selItem = nwGUI.outlineView.outlineTree.topLevelItem(2)
nwGUI.outlineView.outlineTree.setCurrentItem(selItem)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.docEditor.docHandle == sHandle
assert nwGUI.closeDocument() is True
# qtbot.stop()
@@ -506,9 +506,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
nwGUI.docEditor.wCounterDoc.run()
# Save the document
assert nwGUI.docEditor.docChanged()
assert nwGUI.docEditor.docChanged
assert nwGUI.saveDocument()
assert not nwGUI.docEditor.docChanged()
assert not nwGUI.docEditor.docChanged
nwGUI.rebuildIndex()
# Open and view the edited document
+6 -6
View File
@@ -206,7 +206,7 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
# Clear the Text
nwGUI.docEditor.clear()
assert nwGUI.docEditor.isEmpty()
assert nwGUI.docEditor.isEmpty
# Alignment & Indent
# ==================
@@ -403,17 +403,17 @@ def testGuiMenu_ContextMenus(qtbot, nwGUI, prjLipsum):
# Navigation History
assert nwGUI.viewDocument("04468803b92e1")
assert nwGUI.docViewer.docHandle() == "04468803b92e1"
assert nwGUI.docViewer.docHandle == "04468803b92e1"
assert nwGUI.docViewer.docHeader.backButton.isEnabled()
assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled()
qtbot.mouseClick(nwGUI.docViewer.docHeader.backButton, Qt.LeftButton)
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
assert nwGUI.docViewer.docHandle == "4c4f28287af27"
assert not nwGUI.docViewer.docHeader.backButton.isEnabled()
assert nwGUI.docViewer.docHeader.forwardButton.isEnabled()
qtbot.mouseClick(nwGUI.docViewer.docHeader.forwardButton, Qt.LeftButton)
assert nwGUI.docViewer.docHandle() == "04468803b92e1"
assert nwGUI.docViewer.docHandle == "04468803b92e1"
assert nwGUI.docViewer.docHeader.backButton.isEnabled()
assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled()
@@ -438,10 +438,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
nwGUI.docEditor.clear()
assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False
assert nwGUI.docEditor.isEmpty()
assert nwGUI.docEditor.isEmpty
assert nwGUI.docEditor.insertText(None) is False
assert nwGUI.docEditor.isEmpty()
assert nwGUI.docEditor.isEmpty
# qtbot.stop()
+6 -6
View File
@@ -118,26 +118,26 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Double-click item
scItem.setSelected(True)
assert scItem.isSelected()
assert nwGUI.docEditor.docHandle() is None
assert nwGUI.docEditor.docHandle is None
novelTree._treeDoubleClick(scItem, 0)
assert nwGUI.docEditor.docHandle() == C.hSceneDoc
assert nwGUI.docEditor.docHandle == C.hSceneDoc
# Open item with middle mouse button
scItem.setSelected(True)
assert scItem.isSelected()
assert nwGUI.docViewer.docHandle() is None
assert nwGUI.docViewer.docHandle is None
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=vPort.rect().center(), delay=10)
assert nwGUI.docViewer.docHandle() is None
assert nwGUI.docViewer.docHandle is None
scRect = novelTree.visualItemRect(scItem)
oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE)
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None)
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
assert nwGUI.docViewer.docHandle() is None
assert nwGUI.docViewer.docHandle is None
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData)
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
assert nwGUI.docViewer.docHandle() == C.hSceneDoc
assert nwGUI.docViewer.docHandle == C.hSceneDoc
# Last Column
# ===========
+2 -2
View File
@@ -246,7 +246,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum):
# Click POV Link
assert outlineData.povKeyValue.text() == "<a href='Bod'>Bod</a>"
outlineView._tagClicked("Bod")
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
assert nwGUI.docViewer.docHandle == "4c4f28287af27"
# Scene One, Section Two
selItem = outlineTree.topLevelItem(5)
@@ -262,7 +262,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum):
assert outlineData.itemValue.text() == "Finished"
outlineTree._treeDoubleClick(selItem, 0)
assert nwGUI.docEditor.docHandle() == "88243afbe5ed8"
assert nwGUI.docEditor.docHandle == "88243afbe5ed8"
# qtbot.stop()
+6 -6
View File
@@ -450,10 +450,10 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, pro
# Deleting file is OK, and if it is open, it should close
assert nwGUI.openDocument(C.hTitlePage) is True
assert nwGUI.docEditor.docHandle() == C.hTitlePage
assert nwGUI.docEditor.docHandle == C.hTitlePage
assert projTree.permDeleteItem(C.hTitlePage) is True
assert C.hTitlePage not in theProject.tree
assert nwGUI.docEditor.docHandle() is None
assert nwGUI.docEditor.docHandle is None
# Deleting folder + files recursively is ok
assert projTree.permDeleteItem(C.hChapterDir) is True
@@ -969,25 +969,25 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
# Try to open a file with nothings selected
projTree.clearSelection()
projTree._treeDoubleClick(QTreeWidgetItem(), 0)
assert nwGUI.docEditor.docHandle() is None
assert nwGUI.docEditor.docHandle is None
# When the item cannot be found
projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None)
projTree._treeDoubleClick(QTreeWidgetItem(), 0)
assert nwGUI.docEditor.docHandle() is None
assert nwGUI.docEditor.docHandle is None
# Successfully open a file
projTree._treeDoubleClick(projTree._getTreeItem(C.hTitlePage), 0)
assert nwGUI.docEditor.docHandle() == C.hTitlePage
assert nwGUI.docEditor.docHandle == C.hTitlePage
projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore
# A non-file item should be expanded instead
projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore
projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore
projTree._treeDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1)
assert nwGUI.docEditor.docHandle() == C.hTitlePage
assert nwGUI.docEditor.docHandle == C.hTitlePage
assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore
# Navigate the Tree