Let the shared class handle project open/save/close and recreating the project instance
This commit is contained in:
+21
-55
@@ -129,8 +129,21 @@ class NWProject(QObject):
|
||||
|
||||
@property
|
||||
def isValid(self) -> bool:
|
||||
"""Return True if a project is loaded."""
|
||||
return self._valid
|
||||
|
||||
@property
|
||||
def lockStatus(self) -> list | None:
|
||||
"""Return the project lock information."""
|
||||
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
|
||||
return self._lockedBy
|
||||
return None
|
||||
|
||||
@property
|
||||
def currentEditTime(self) -> int:
|
||||
"""Return total edit time, including the current session."""
|
||||
return self._data.editTime + round(time() - self._session.start)
|
||||
|
||||
##
|
||||
# Item Methods
|
||||
##
|
||||
@@ -206,35 +219,12 @@ class NWProject(QObject):
|
||||
# Project Methods
|
||||
##
|
||||
|
||||
def clearProject(self) -> None:
|
||||
"""Clear the data for the current project, and set them to
|
||||
default values.
|
||||
|
||||
Note: Don't clear the lockedBy data here as it is needed after
|
||||
this function is called.
|
||||
"""
|
||||
# Core Elements
|
||||
self._options = OptionState(self)
|
||||
self._storage.clear()
|
||||
self._data = NWProjectData(self)
|
||||
self._tree.clear()
|
||||
self._index.clearIndex()
|
||||
self._session = NWSessionLog(self)
|
||||
|
||||
# Project Status
|
||||
self._langData = {}
|
||||
self._changed = False
|
||||
self._valid = False
|
||||
|
||||
return
|
||||
|
||||
def openProject(self, projPath: str | Path, overrideLock: bool = False) -> bool:
|
||||
def openProject(self, projPath: str | Path) -> bool:
|
||||
"""Open the project file provided. If it doesn't exist, assume
|
||||
it is a folder and look for the file within it. If successful,
|
||||
parse the XML of the file and populate the project variables and
|
||||
build the tree of project items.
|
||||
"""
|
||||
self.clearProject()
|
||||
logger.info("Opening project: %s", projPath)
|
||||
if not self._storage.openProjectInPlace(projPath):
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
@@ -245,9 +235,6 @@ class NWProject(QObject):
|
||||
# Project Lock
|
||||
# ============
|
||||
|
||||
if overrideLock:
|
||||
self._storage.clearLockFile()
|
||||
|
||||
lockStatus = self._storage.readLockFile()
|
||||
if len(lockStatus) > 0:
|
||||
if lockStatus[0] == "ERROR":
|
||||
@@ -255,7 +242,6 @@ class NWProject(QObject):
|
||||
else:
|
||||
logger.error("Project is locked, so not opening")
|
||||
self._lockedBy = lockStatus
|
||||
self.clearProject()
|
||||
return False
|
||||
else:
|
||||
logger.debug("Project is not locked")
|
||||
@@ -265,7 +251,6 @@ class NWProject(QObject):
|
||||
|
||||
xmlReader = self._storage.getXmlReader()
|
||||
if not isinstance(xmlReader, ProjectXMLReader):
|
||||
self.clearProject()
|
||||
return False
|
||||
|
||||
self._data = NWProjectData(self)
|
||||
@@ -289,8 +274,6 @@ class NWProject(QObject):
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Failed to parse project xml."
|
||||
), level=nwAlert.ERROR)
|
||||
|
||||
self.clearProject()
|
||||
return False
|
||||
|
||||
# Check Legacy Upgrade
|
||||
@@ -303,7 +286,6 @@ class NWProject(QObject):
|
||||
"longer be able to open this project. Continue?"
|
||||
))
|
||||
if not msgYes:
|
||||
self.clearProject()
|
||||
return False
|
||||
|
||||
# Check novelWriter Version
|
||||
@@ -318,7 +300,6 @@ class NWProject(QObject):
|
||||
"should be fine. Continue opening the project?"
|
||||
).format(appVersion, __version__))
|
||||
if not msgYes:
|
||||
self.clearProject()
|
||||
return False
|
||||
|
||||
# Extract Data
|
||||
@@ -329,9 +310,11 @@ class NWProject(QObject):
|
||||
self._loadProjectLocalisation()
|
||||
|
||||
# Update recent projects
|
||||
CONFIG.recentProjects.update(
|
||||
self._storage.storagePath, self._data.name, sum(self._data.initCounts), time()
|
||||
)
|
||||
storePath = self._storage.storagePath
|
||||
if storePath:
|
||||
CONFIG.recentProjects.update(
|
||||
storePath, self._data.name, sum(self._data.initCounts), time()
|
||||
)
|
||||
|
||||
# Check the project tree consistency
|
||||
# This also handles any orphaned files found
|
||||
@@ -416,7 +399,6 @@ class NWProject(QObject):
|
||||
self._tree.writeToCFile()
|
||||
self._session.appendSession(idleTime)
|
||||
self._storage.closeSession()
|
||||
self.clearProject()
|
||||
self._lockedBy = None
|
||||
return
|
||||
|
||||
@@ -522,22 +504,10 @@ class NWProject(QObject):
|
||||
return self._changed
|
||||
|
||||
##
|
||||
# Getters
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def getLockStatus(self) -> list | None:
|
||||
"""Return the project lock information for the project."""
|
||||
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
|
||||
return self._lockedBy
|
||||
return None
|
||||
|
||||
def getCurrentEditTime(self) -> int:
|
||||
"""Get the total project edit time, including the time spent in
|
||||
the current session.
|
||||
"""
|
||||
return self._data.editTime + round(time() - self._session.start)
|
||||
|
||||
def getProjectItems(self) -> Iterator[NWItem]:
|
||||
def iterProjectItems(self) -> Iterator[NWItem]:
|
||||
"""This function ensures that the item tree loaded is sent to
|
||||
the GUI tree view in such a way that the tree can be built. That
|
||||
is, the parent item must be sent before its child. In principle,
|
||||
@@ -580,10 +550,6 @@ class NWProject(QObject):
|
||||
yield tItem
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def updateWordCounts(self) -> None:
|
||||
"""Update the total word count values."""
|
||||
novel, notes = self._tree.sumWords()
|
||||
|
||||
@@ -237,14 +237,13 @@ class GuiProjectDetailsMain(QWidget):
|
||||
|
||||
return
|
||||
|
||||
def updateValues(self):
|
||||
"""Set all the values.
|
||||
"""
|
||||
def updateValues(self) -> None:
|
||||
"""Set all the values."""
|
||||
project = SHARED.project
|
||||
pIndex = project.index
|
||||
hCounts = pIndex.getNovelTitleCounts()
|
||||
nwCount = pIndex.getNovelWordCount()
|
||||
edTime = project.getCurrentEditTime()
|
||||
edTime = project.currentEditTime
|
||||
|
||||
self.bookTitle.setText(project.data.title or project.data.name)
|
||||
self.projName.setText(self.tr("Project: {0}").format(project.data.name))
|
||||
|
||||
@@ -1064,7 +1064,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
logger.debug("Building the project tree ...")
|
||||
self.clearTree()
|
||||
count = 0
|
||||
for nwItem in SHARED.project.getProjectItems():
|
||||
for nwItem in SHARED.project.iterProjectItems():
|
||||
count += 1
|
||||
self._addTreeItem(nwItem)
|
||||
if count > 0:
|
||||
|
||||
@@ -454,9 +454,7 @@ class GuiMain(QMainWindow):
|
||||
if SHARED.project.data.doBackup and CONFIG.backupOnClose:
|
||||
doBackup = True
|
||||
if CONFIG.askBeforeBackup:
|
||||
msgYes = self.askQuestion(self.tr("Backup the current project?"))
|
||||
if not msgYes:
|
||||
doBackup = False
|
||||
doBackup = self.askQuestion(self.tr("Backup the current project?"))
|
||||
|
||||
if doBackup:
|
||||
SHARED.project.backupProject(False)
|
||||
@@ -490,9 +488,9 @@ class GuiMain(QMainWindow):
|
||||
self._changeView(nwView.PROJECT)
|
||||
|
||||
# Try to open the project
|
||||
if not SHARED.project.openProject(projFile):
|
||||
if not SHARED.openProject(projFile):
|
||||
# The project open failed.
|
||||
lockStatus = SHARED.project.getLockStatus()
|
||||
lockStatus = SHARED.projectLock
|
||||
if lockStatus is None:
|
||||
# The project is not locked, so failed for some other
|
||||
# reason handled by the project class.
|
||||
@@ -522,7 +520,8 @@ class GuiMain(QMainWindow):
|
||||
lockDetails = ""
|
||||
|
||||
if self.askQuestion(lockText, info=lockInfo, details=lockDetails, level=nwAlert.WARN):
|
||||
if not SHARED.project.openProject(projFile, overrideLock=True):
|
||||
SHARED.unlockProject()
|
||||
if not SHARED.openProject(projFile):
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
@@ -578,8 +577,7 @@ class GuiMain(QMainWindow):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
self.projView.saveProjectTasks()
|
||||
SHARED.project.saveProject(autoSave=autoSave)
|
||||
return True
|
||||
return SHARED.saveProject(autoSave=autoSave)
|
||||
|
||||
##
|
||||
# Document Actions
|
||||
|
||||
+33
-4
@@ -42,6 +42,7 @@ class SharedData:
|
||||
self._gui = None
|
||||
self._theme = None
|
||||
self._project = None
|
||||
self._lockedBy = None
|
||||
return
|
||||
|
||||
@property
|
||||
@@ -69,6 +70,10 @@ class SharedData:
|
||||
def hasProject(self) -> bool:
|
||||
return self.project.isValid
|
||||
|
||||
@property
|
||||
def projectLock(self) -> list | None:
|
||||
return self._lockedBy
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -84,16 +89,38 @@ class SharedData:
|
||||
logger.debug("SharedData instance initialised")
|
||||
return
|
||||
|
||||
def openProject(self, path: str | Path) -> None:
|
||||
return
|
||||
def openProject(self, path: str | Path) -> bool:
|
||||
"""Open a project."""
|
||||
if self.project.isValid:
|
||||
logger.error("A project is already open")
|
||||
return False
|
||||
|
||||
def saveProject(self):
|
||||
return
|
||||
self._lockedBy = None
|
||||
status = self.project.openProject(path)
|
||||
if status is False:
|
||||
# We must cache the lock status before resetting the project
|
||||
self._lockedBy = self.project.lockStatus
|
||||
self._resetProject()
|
||||
|
||||
return status
|
||||
|
||||
def saveProject(self, autoSave: bool = False) -> bool:
|
||||
"""Save the current project."""
|
||||
if not self.project.isValid:
|
||||
logger.error("There is no project open")
|
||||
return False
|
||||
return self.project.saveProject(autoSave=autoSave)
|
||||
|
||||
def closeProject(self, idleTime: float) -> None:
|
||||
"""Close the current project."""
|
||||
self.project.closeProject(idleTime)
|
||||
self._resetProject()
|
||||
return
|
||||
|
||||
def unlockProject(self) -> bool:
|
||||
"""Remove the project lock."""
|
||||
return self.project.storage.clearLockFile()
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
@@ -101,6 +128,8 @@ class SharedData:
|
||||
def _resetProject(self) -> None:
|
||||
"""Create a new project instance."""
|
||||
from novelwriter.core.project import NWProject
|
||||
if isinstance(self._project, NWProject):
|
||||
self._project.deleteLater()
|
||||
self._project = NWProject(self.mainGui)
|
||||
return
|
||||
|
||||
|
||||
@@ -443,7 +443,7 @@ class _FilterTab(QWidget):
|
||||
logger.debug("Building project tree")
|
||||
self._treeMap = {}
|
||||
self.optTree.clear()
|
||||
for nwItem in SHARED.project.getProjectItems():
|
||||
for nwItem in SHARED.project.iterProjectItems():
|
||||
|
||||
tHandle = nwItem.itemHandle
|
||||
pHandle = nwItem.itemParent
|
||||
|
||||
@@ -176,7 +176,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
|
||||
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK
|
||||
assert theProject.storage.writeLockFile() is True
|
||||
assert theProject.openProject(fncPath) is False
|
||||
assert isinstance(theProject.getLockStatus(), list)
|
||||
assert isinstance(theProject.lockStatus, list)
|
||||
|
||||
# Fail to read lockfile (which still opens the project)
|
||||
with monkeypatch.context() as mp:
|
||||
@@ -189,9 +189,10 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
|
||||
# Force open with lockfile
|
||||
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK
|
||||
assert theProject.storage.writeLockFile() is True
|
||||
assert theProject.openProject(fncPath, overrideLock=True) is True
|
||||
theProject.storage.clearLockFile()
|
||||
assert theProject.openProject(fncPath) is True
|
||||
theProject.closeProject()
|
||||
assert theProject.getLockStatus() is None
|
||||
assert theProject.lockStatus is None
|
||||
|
||||
# Fail getting xml reader
|
||||
with monkeypatch.context() as mp:
|
||||
@@ -331,7 +332,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
|
||||
assert theProject.tree[nHandle].itemParent == "cba9876543210"
|
||||
|
||||
retOrder = []
|
||||
for tItem in theProject.getProjectItems():
|
||||
for tItem in theProject.iterProjectItems():
|
||||
retOrder.append(tItem.itemHandle)
|
||||
|
||||
assert retOrder == [
|
||||
@@ -482,7 +483,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
theProject._session._start = 1600000000
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("novelwriter.core.project.time", lambda: 1600005600)
|
||||
assert theProject.getCurrentEditTime() == 6834
|
||||
assert theProject.currentEditTime == 6834
|
||||
|
||||
# Trash folder
|
||||
# Should create on first call, and just returned on later calls
|
||||
|
||||
@@ -104,9 +104,8 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiMain_NewProject(monkeypatch, nwGUI, projPath):
|
||||
"""Test creating a new project.
|
||||
"""
|
||||
# No data
|
||||
"""Test creating a new project."""
|
||||
# Open wizard, but return no data
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiProjectWizard, "exec_", lambda *a: None)
|
||||
assert nwGUI.newProject(projData=None) is False
|
||||
@@ -116,6 +115,7 @@ def testGuiMain_NewProject(monkeypatch, nwGUI, projPath):
|
||||
SHARED.project._valid = True
|
||||
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
|
||||
assert nwGUI.newProject(projData={"projPath": projPath}) is False
|
||||
SHARED.project._valid = False
|
||||
|
||||
# No project path
|
||||
assert nwGUI.newProject(projData={}) is False
|
||||
|
||||
@@ -377,13 +377,14 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
|
||||
|
||||
# IOError
|
||||
# =======
|
||||
monkeypatch.setattr("builtins.open", causeOSError)
|
||||
assert not sessLog._loadLogFile()
|
||||
assert not sessLog._saveData(sessLog.FMT_CSV)
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert not sessLog._loadLogFile()
|
||||
assert not sessLog._saveData(sessLog.FMT_CSV)
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
sessLog._doClose()
|
||||
assert nwGUI.closeProject()
|
||||
assert nwGUI.closeProject() is True
|
||||
|
||||
# END Test testToolWritingStats_Main
|
||||
|
||||
@@ -168,7 +168,6 @@ def buildTestProject(obj, projPath):
|
||||
nwGUI = obj
|
||||
project = obj.project
|
||||
|
||||
project.clearProject()
|
||||
project.storage.openProjectInPlace(projPath)
|
||||
project.setDefaultStatusImport()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user