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