+5
-1
@@ -20,7 +20,7 @@ from os import path, mkdir, unlink, rename
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from PyQt5.Qt import PYQT_VERSION_STR
|
from PyQt5.Qt import PYQT_VERSION_STR
|
||||||
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths
|
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
|
||||||
|
|
||||||
from nw.constants import nwFiles, nwUnicode
|
from nw.constants import nwFiles, nwUnicode
|
||||||
from nw.common import splitVersionNumber
|
from nw.common import splitVersionNumber
|
||||||
@@ -156,6 +156,10 @@ class Config:
|
|||||||
else:
|
else:
|
||||||
self.osUnknown = True
|
self.osUnknown = True
|
||||||
|
|
||||||
|
# Other System Info
|
||||||
|
self.hostName = QSysInfo.machineHostName()
|
||||||
|
self.kernelVer = QSysInfo.kernelVersion()
|
||||||
|
|
||||||
# Packages
|
# Packages
|
||||||
self.hasEnchant = False
|
self.hasEnchant = False
|
||||||
self.hasSymSpell = False
|
self.hasSymSpell = False
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class nwFiles():
|
|||||||
APP_ICON = "novelWriter.svg"
|
APP_ICON = "novelWriter.svg"
|
||||||
PROJ_FILE = "nwProject.nwx"
|
PROJ_FILE = "nwProject.nwx"
|
||||||
PROJ_DICT = "wordlist.txt"
|
PROJ_DICT = "wordlist.txt"
|
||||||
|
PROJ_LOCK = "nwProject.lock"
|
||||||
SESS_INFO = "sessionInfo.log"
|
SESS_INFO = "sessionInfo.log"
|
||||||
INDEX_FILE = "tagsIndex.json"
|
INDEX_FILE = "tagsIndex.json"
|
||||||
OPTS_FILE = "guiOptions.json"
|
OPTS_FILE = "guiOptions.json"
|
||||||
|
|||||||
+40
-4
@@ -15,6 +15,7 @@ import time
|
|||||||
import nw
|
import nw
|
||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence
|
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence
|
||||||
@@ -49,9 +50,9 @@ class GuiMain(QMainWindow):
|
|||||||
self.hasProject = False
|
self.hasProject = False
|
||||||
self.isZenMode = False
|
self.isZenMode = False
|
||||||
|
|
||||||
logger.info("OS: %s" % (
|
logger.info("OS: %s" % self.mainConf.osType)
|
||||||
self.mainConf.osType)
|
logger.info("Kernel: %s" % self.mainConf.kernelVer)
|
||||||
)
|
logger.info("Host: %s" % self.mainConf.hostName)
|
||||||
logger.info("Qt5 Version: %s (%d)" % (
|
logger.info("Qt5 Version: %s (%d)" % (
|
||||||
self.mainConf.verQtString, self.mainConf.verQtValue)
|
self.mainConf.verQtString, self.mainConf.verQtValue)
|
||||||
)
|
)
|
||||||
@@ -319,7 +320,42 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
# Try to open the project
|
# Try to open the project
|
||||||
if not self.theProject.openProject(projFile):
|
if not self.theProject.openProject(projFile):
|
||||||
return False
|
if self.theProject.lockedBy is not None:
|
||||||
|
if self.mainConf.showGUI:
|
||||||
|
try:
|
||||||
|
lockDetails = (
|
||||||
|
"<br><br>The project was locked by the computer "
|
||||||
|
"'%s' (%s %s), last active on %s"
|
||||||
|
) % (
|
||||||
|
self.theProject.lockedBy[0],
|
||||||
|
self.theProject.lockedBy[1],
|
||||||
|
self.theProject.lockedBy[2],
|
||||||
|
datetime.fromtimestamp(
|
||||||
|
int(self.theProject.lockedBy[3])
|
||||||
|
).strftime("%x %X")
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
lockDetails = ""
|
||||||
|
|
||||||
|
msgBox = QMessageBox()
|
||||||
|
msgRes = msgBox.warning(
|
||||||
|
self, "Project Locked", (
|
||||||
|
"The project is already open by another instance of %s, and is "
|
||||||
|
"therefore locked. Override lock and continue anyway?<br><br>"
|
||||||
|
"Note: If the program or the computer previously crashed, the lock "
|
||||||
|
"can safely be overridden. If, however, another instance of %s has "
|
||||||
|
"the project open, overriding the lock may corrupt the project, and "
|
||||||
|
"is not recommended.%s"
|
||||||
|
) % (nw.__package__, nw.__package__, lockDetails),
|
||||||
|
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
|
||||||
|
)
|
||||||
|
if msgRes == QMessageBox.Yes:
|
||||||
|
if not self.theProject.openProject(projFile, overrideLock=True):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
# project is loaded
|
# project is loaded
|
||||||
self.hasProject = True
|
self.hasProject = True
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ class NWBackup():
|
|||||||
baseName = path.join(self.mainConf.backupPath, archName)
|
baseName = path.join(self.mainConf.backupPath, archName)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self.theProject._clearLockFile()
|
||||||
make_archive(baseName, "zip", self.theProject.projPath, ".")
|
make_archive(baseName, "zip", self.theProject.projPath, ".")
|
||||||
|
self.theProject._writeLockFile()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.theParent.makeAlert(
|
self.theParent.makeAlert(
|
||||||
["Could not write backup archive.",str(e)],
|
["Could not write backup archive.",str(e)],
|
||||||
|
|||||||
+90
-1
@@ -40,6 +40,7 @@ class NWProject():
|
|||||||
self.projOpened = None # The time stamp of when the project file was opened
|
self.projOpened = None # The time stamp of when the project file was opened
|
||||||
self.projChanged = None # The project has unsaved changes
|
self.projChanged = None # The project has unsaved changes
|
||||||
self.projAltered = None # The project has been altered this session
|
self.projAltered = None # The project has been altered this session
|
||||||
|
self.lockedBy = None # Data on which computer has the project open
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
self.handleSeed = None
|
self.handleSeed = None
|
||||||
@@ -178,7 +179,7 @@ class NWProject():
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def openProject(self, fileName):
|
def openProject(self, fileName, overrideLock=False):
|
||||||
"""Open the project file provided, or if doesn't exist, assume
|
"""Open the project file provided, or if 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
|
||||||
@@ -201,6 +202,21 @@ class NWProject():
|
|||||||
if not self._checkFolder(self.projMeta):
|
if not self._checkFolder(self.projMeta):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if overrideLock:
|
||||||
|
self._clearLockFile()
|
||||||
|
|
||||||
|
lockStatus = self._readLockFile()
|
||||||
|
if len(lockStatus) > 0:
|
||||||
|
if lockStatus[0] == "ERROR":
|
||||||
|
logger.warning("Failed to check lock file")
|
||||||
|
else:
|
||||||
|
logger.error("Project is locked, so not opening")
|
||||||
|
self.lockedBy = lockStatus
|
||||||
|
self.clearProject()
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.verbose("Project is not locked")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
projectMaintenance(self)
|
projectMaintenance(self)
|
||||||
except Exception as E:
|
except Exception as E:
|
||||||
@@ -308,6 +324,7 @@ class NWProject():
|
|||||||
self.setProjectChanged(False)
|
self.setProjectChanged(False)
|
||||||
self.projOpened = time()
|
self.projOpened = time()
|
||||||
self.projAltered = False
|
self.projAltered = False
|
||||||
|
self._writeLockFile()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -398,14 +415,19 @@ class NWProject():
|
|||||||
self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
|
self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
|
||||||
self.mainConf.saveRecentCache()
|
self.mainConf.saveRecentCache()
|
||||||
|
|
||||||
|
self._writeLockFile()
|
||||||
self.theParent.setStatus("Saved Project: %s" % self.projName)
|
self.theParent.setStatus("Saved Project: %s" % self.projName)
|
||||||
self.setProjectChanged(False)
|
self.setProjectChanged(False)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def closeProject(self):
|
def closeProject(self):
|
||||||
|
"""Close the current project and clear all meta data.
|
||||||
|
"""
|
||||||
self._appendSessionStats()
|
self._appendSessionStats()
|
||||||
|
self._clearLockFile()
|
||||||
self.clearProject()
|
self.clearProject()
|
||||||
|
self.lockedBy = None
|
||||||
return True
|
return True
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -645,6 +667,73 @@ class NWProject():
|
|||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def _readLockFile(self):
|
||||||
|
"""Reads the lock file in the project folder.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.projPath is None:
|
||||||
|
return ["ERROR"]
|
||||||
|
|
||||||
|
lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
|
||||||
|
if not path.isfile(lockFile):
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(lockFile, mode="r", encoding="utf8") as inFile:
|
||||||
|
theData = inFile.read()
|
||||||
|
theLines = theData.splitlines()
|
||||||
|
if len(theLines) == 4:
|
||||||
|
return theLines
|
||||||
|
else:
|
||||||
|
return ["ERROR"]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to read project lockfile")
|
||||||
|
logger.error(str(e))
|
||||||
|
return ["ERROR"]
|
||||||
|
|
||||||
|
return ["ERROR"]
|
||||||
|
|
||||||
|
def _writeLockFile(self):
|
||||||
|
"""Writes a lock file to the project folder.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.projPath is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
|
||||||
|
try:
|
||||||
|
with open(lockFile, mode="w+", encoding="utf8") as outFile:
|
||||||
|
outFile.write("%s\n" % self.mainConf.hostName)
|
||||||
|
outFile.write("%s\n" % self.mainConf.osType)
|
||||||
|
outFile.write("%s\n" % self.mainConf.kernelVer)
|
||||||
|
outFile.write("%d\n" % time())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to write project lockfile")
|
||||||
|
logger.error(str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _clearLockFile(self):
|
||||||
|
"""Remove the lock file, if it exists.
|
||||||
|
"""
|
||||||
|
if self.projPath is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
|
||||||
|
if path.isfile(lockFile):
|
||||||
|
try:
|
||||||
|
unlink(lockFile)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to remove project lockfile")
|
||||||
|
logger.error(str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def _checkFolder(self, thePath):
|
def _checkFolder(self, thePath):
|
||||||
if not path.isdir(thePath):
|
if not path.isdir(thePath):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-06-08 20:53:03">
|
<novelWriterXML appVersion="0.4.5" fileVersion="1.0" timeStamp="2020-02-26 22:05:41">
|
||||||
<project>
|
<project>
|
||||||
<name></name>
|
<name></name>
|
||||||
<title></title>
|
<title></title>
|
||||||
@@ -72,28 +72,28 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="39fa9ec190eee" order="None" parent="None">
|
<item handle="8722616204217" order="None" parent="None">
|
||||||
<name>Timeline</name>
|
<name>Timeline</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>TIMELINE</class>
|
<class>TIMELINE</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="d029fa3a95e17" order="None" parent="None">
|
<item handle="96061e92f58e4" order="None" parent="None">
|
||||||
<name>Object</name>
|
<name>Object</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>OBJECT</class>
|
<class>OBJECT</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="81b8a03f97e87" order="None" parent="None">
|
<item handle="eb624dbe56eb6" order="None" parent="None">
|
||||||
<name>Custom1</name>
|
<name>Custom1</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>CUSTOM</class>
|
<class>CUSTOM</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="da4ea2a5506f2" order="None" parent="None">
|
<item handle="f369cb89fc627" order="None" parent="None">
|
||||||
<name>Custom2</name>
|
<name>Custom2</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>CUSTOM</class>
|
<class>CUSTOM</class>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ def testProjectNew(nwTempProj,nwRef,nwTemp):
|
|||||||
assert theProject.newProject()
|
assert theProject.newProject()
|
||||||
assert theProject.setProjectPath(nwTempProj)
|
assert theProject.setProjectPath(nwTempProj)
|
||||||
assert theProject.saveProject()
|
assert theProject.saveProject()
|
||||||
|
assert theProject.closeProject()
|
||||||
assert cmpFiles(projFile, refFile, [2])
|
assert cmpFiles(projFile, refFile, [2])
|
||||||
|
|
||||||
@pytest.mark.project
|
@pytest.mark.project
|
||||||
@@ -41,9 +42,21 @@ def testProjectSave(nwTempProj,nwRef):
|
|||||||
projFile = path.join(nwTempProj,"nwProject.nwx")
|
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||||
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
|
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
|
||||||
assert theProject.saveProject()
|
assert theProject.saveProject()
|
||||||
|
assert theProject.closeProject()
|
||||||
assert cmpFiles(projFile, refFile, [2])
|
assert cmpFiles(projFile, refFile, [2])
|
||||||
assert not theProject.projChanged
|
assert not theProject.projChanged
|
||||||
|
|
||||||
|
@pytest.mark.project
|
||||||
|
def testProjectOpenTwice(nwTempProj,nwRef):
|
||||||
|
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||||
|
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
|
||||||
|
assert theProject.openProject(projFile)
|
||||||
|
assert not theProject.openProject(projFile)
|
||||||
|
assert theProject.openProject(projFile, overrideLock=True)
|
||||||
|
assert theProject.saveProject()
|
||||||
|
assert theProject.closeProject()
|
||||||
|
assert cmpFiles(projFile, refFile, [2])
|
||||||
|
|
||||||
@pytest.mark.project
|
@pytest.mark.project
|
||||||
def testProjectNewRoot(nwTempProj,nwRef):
|
def testProjectNewRoot(nwTempProj,nwRef):
|
||||||
projFile = path.join(nwTempProj,"nwProject.nwx")
|
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||||
@@ -59,6 +72,7 @@ def testProjectNewRoot(nwTempProj,nwRef):
|
|||||||
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
|
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
|
||||||
assert theProject.projChanged
|
assert theProject.projChanged
|
||||||
assert theProject.saveProject()
|
assert theProject.saveProject()
|
||||||
|
assert theProject.closeProject()
|
||||||
assert cmpFiles(projFile, refFile, [2])
|
assert cmpFiles(projFile, refFile, [2])
|
||||||
assert not theProject.projChanged
|
assert not theProject.projChanged
|
||||||
|
|
||||||
@@ -96,6 +110,8 @@ def testIndexScanThis(nwTempProj):
|
|||||||
assert str(theBits) == "['@tag', 'this', 'and this']"
|
assert str(theBits) == "['@tag', 'this', 'and this']"
|
||||||
assert str(thePos) == "[0, 6, 12]"
|
assert str(thePos) == "[0, 6, 12]"
|
||||||
|
|
||||||
|
assert theProject.closeProject()
|
||||||
|
|
||||||
@pytest.mark.project
|
@pytest.mark.project
|
||||||
def testBuildIndex(nwTempProj):
|
def testBuildIndex(nwTempProj):
|
||||||
projFile = path.join(nwTempProj,"nwProject.nwx")
|
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||||
@@ -117,3 +133,4 @@ def testBuildIndex(nwTempProj):
|
|||||||
assert theIndex.buildNovelList()
|
assert theIndex.buildNovelList()
|
||||||
assert str(theIndex.novelList) == "[[1, 1, 'Novel', 'SCENE'], [3, 2, 'Chapter', 'SCENE'], [5, 3, 'Scene', 'SCENE'], [7, 4, 'Section', 'SCENE']]"
|
assert str(theIndex.novelList) == "[[1, 1, 'Novel', 'SCENE'], [3, 2, 'Chapter', 'SCENE'], [5, 3, 'Scene', 'SCENE'], [7, 4, 'Section', 'SCENE']]"
|
||||||
assert str(theIndex.novelOrder) == "['31489056e0916:1', '31489056e0916:3', '31489056e0916:5', '31489056e0916:7']"
|
assert str(theIndex.novelOrder) == "['31489056e0916:1', '31489056e0916:3', '31489056e0916:5', '31489056e0916:7']"
|
||||||
|
assert theProject.closeProject()
|
||||||
|
|||||||
Reference in New Issue
Block a user