From 43449c25082a947875ac19ea07b1e6c579b374f4 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 24 Nov 2019 17:45:52 +0100
Subject: [PATCH 01/60] Bumped version and updated changelog
---
CHANGELOG.md | 14 ++++++++++++++
docs/source/conf.py | 4 ++--
nw/__init__.py | 4 ++--
setup.py | 2 +-
4 files changed, 19 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 236d217a..98e21175 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# novelWriter ChangeLog
+## Version 0.4.3 [2019-11-24]
+
+**User Interface**
+
+* Added keyboard shortcuts and menu entries for formatting headers, comments, and removing block formats. PR #155.
+* Disable re-highlighting of open file when resizing window. This is potentially a slow process if the spell checker is on and the file is large. There is no need to do this just for reflowing text, so it is now disabled on resize events. PR #153 fixing issue #150.
+* Improved the speed of the syntax highlighter by about 40% by not using regular expressions for highlighting block formats and by skipping empty lines entirely. PR #154.
+
+**Bug Fixes**
+
+* Fixed an issue when closing the import file dialog without selecting a file, the import would procede, but fail on file not found. The import is now cancelled when there is no file selected. PR #149.
+* Fixed an issue with markdown export did not take into account hard line breaks. PR #152 fixing issue #151.
+* Fixed a crash when running file status check when the project contains orphaned files. PR #152.
+
## Version 0.4.2 [2019-11-17]
**User Interface**
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 8cdb4fad..62a72b99 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -24,9 +24,9 @@ copyright = "2018-2019, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen"
# The short X.Y version
-version = "0.4.2"
+version = "0.4.3"
# The full version, including alpha/beta/rc tags
-release = "0.4.2"
+release = "0.4.3"
# -- General configuration ---------------------------------------------------
diff --git a/nw/__init__.py b/nw/__init__.py
index 7f1e2e6d..245b9043 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -25,8 +25,8 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 2018–2019, Veronica Berglyd Olsen"
__license__ = "GPLv3"
-__version__ = "0.4.2"
-__date__ = "2019-11-17"
+__version__ = "0.4.3"
+__date__ = "2019-11-24"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__status__ = "Development"
diff --git a/setup.py b/setup.py
index 38e3f3c8..75f2fbd7 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
setuptools.setup(
name = "novelWriter",
- version = "0.4.2",
+ version = "0.4.3",
author = "Veronica Berglyd Olsen",
author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels",
From 7bc4116df86c2bd5bc750dfa57cbf5a9edf1f095 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 24 Nov 2019 17:59:51 +0100
Subject: [PATCH 02/60] New block format shortcuts were not working in
distraction free mode
---
nw/guimain.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/nw/guimain.py b/nw/guimain.py
index 54a928e2..63a31795 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -797,6 +797,12 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aFmtULine)
self.addAction(self.mainMenu.aFmtDQuote)
self.addAction(self.mainMenu.aFmtSQuote)
+ self.addAction(self.mainMenu.aFmtHead1)
+ self.addAction(self.mainMenu.aFmtHead2)
+ self.addAction(self.mainMenu.aFmtHead3)
+ self.addAction(self.mainMenu.aFmtHead4)
+ self.addAction(self.mainMenu.aFmtComment)
+ self.addAction(self.mainMenu.aFmtNoFormat)
self.addAction(self.mainMenu.aSpellCheck)
self.addAction(self.mainMenu.aReRunSpell)
self.addAction(self.mainMenu.aPreferences)
From 9ab64627f33c683914a132523909ab03ab52904b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Tue, 26 Nov 2019 23:07:18 +0100
Subject: [PATCH 03/60] Added a spinning cursor for spell checker and open
document
---
nw/gui/elements/doceditor.py | 25 ++++++++++++++++---------
nw/gui/mainmenu.py | 2 +-
2 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py
index 681f1082..d2e6d62b 100644
--- a/nw/gui/elements/doceditor.py
+++ b/nw/gui/elements/doceditor.py
@@ -21,7 +21,7 @@ from PyQt5.QtWidgets import (
)
from PyQt5.QtGui import (
QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette,
- QTextDocument
+ QTextDocument, QCursor
)
from nw.project import NWDoc
@@ -209,7 +209,7 @@ class GuiDocEditor(QTextEdit):
risk overwriting the file if it exists. This can for instance
happen of the file contains binary elements or an encoding that
novelWriter does not support. If load is successful, or the
- document is new (empty string) we set up the editor for editing
+ document is new (empty string), we set up the editor for editing
the file.
"""
@@ -219,6 +219,7 @@ class GuiDocEditor(QTextEdit):
self.clearEditor()
return False
+ qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self.hLight.setHandle(tHandle)
# Check that the document is not too big for full, initial spell
@@ -246,6 +247,7 @@ class GuiDocEditor(QTextEdit):
self.theParent.noticeBar.showNote("This document is read only.")
self.hLight.spellCheck = spTemp
+ qApp.restoreOverrideCursor()
return True
@@ -277,8 +279,9 @@ class GuiDocEditor(QTextEdit):
return True
def updateDocMargins(self):
- """Automatically adjust the margins so the text is centred, but
- only if Config.textFixedW is enabled or we're in Zen mode.
+ """Automatically adjust the margins so the text is centred if
+ Config.textFixedW is enabled or we're in Zen mode. Otherwise,
+ just ensure the margins are set correctly.
"""
if self.mainConf.textFixedW or self.theParent.isZenMode:
@@ -335,7 +338,7 @@ class GuiDocEditor(QTextEdit):
return theText
def setCursorPosition(self, thePosition):
- if thePosition > 0:
+ if thePosition >= 0:
theCursor = self.textCursor()
theCursor.setPosition(thePosition)
self.setTextCursor(theCursor)
@@ -361,8 +364,8 @@ class GuiDocEditor(QTextEdit):
def setSpellCheck(self, theMode):
"""This is the master spell check setting function, and this one
should call all other setSpellCheck functions in other classes.
- If the spell check mode is not defined, then toggle the current
- status saved in the class.
+ If the spell check mode (theMode) is not defined (None), then
+ toggle the current status saved in this class.
"""
if theMode is None:
@@ -375,23 +378,27 @@ class GuiDocEditor(QTextEdit):
self.theParent.mainMenu.setSpellCheck(theMode)
self.theProject.setSpellCheck(theMode)
self.hLight.setSpellCheck(theMode)
- self.reHighlightDocument()
+ if not self.bigDoc:
+ self.spellCheckDocument()
logger.verbose("Spell check is set to %s" % str(theMode))
return True
- def reHighlightDocument(self):
+ def spellCheckDocument(self):
"""Rerun the highlighter to update spell checking status of the
currently loaded text. The fastest way to do this, at least as
of Qt 5.13, is to clear the text and put it back.
"""
+ logger.verbose("Running spell checker")
if self.spellCheck:
theText = self.getText()
self.clear()
bfTime = time()
+ qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self.setPlainText(theText)
+ qApp.restoreOverrideCursor()
afTime = time()
logger.debug("Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)))
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index 679dbacc..ae5df846 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -663,7 +663,7 @@ class GuiMainMenu(QMenuBar):
self.aReRunSpell = QAction("Re-Run Spell Check", self)
self.aReRunSpell.setStatusTip("Run the spell checker on current document")
self.aReRunSpell.setShortcut("F7")
- self.aReRunSpell.triggered.connect(self.theParent.docEditor.reHighlightDocument)
+ self.aReRunSpell.triggered.connect(self.theParent.docEditor.spellCheckDocument)
self.toolsMenu.addAction(self.aReRunSpell)
# Tools > Separator
From a2defebe94873ab518da4b77020430b25a878d94 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 27 Nov 2019 00:58:52 +0100
Subject: [PATCH 04/60] The replace text method should only be used for large
documents
---
nw/gui/elements/doceditor.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py
index d2e6d62b..28a63dce 100644
--- a/nw/gui/elements/doceditor.py
+++ b/nw/gui/elements/doceditor.py
@@ -393,11 +393,13 @@ class GuiDocEditor(QTextEdit):
logger.verbose("Running spell checker")
if self.spellCheck:
- theText = self.getText()
- self.clear()
bfTime = time()
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
- self.setPlainText(theText)
+ if self.bigDoc:
+ theText = self.getText()
+ self.setPlainText(theText)
+ else:
+ self.hLight.rehighlight()
qApp.restoreOverrideCursor()
afTime = time()
logger.debug("Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)))
From 06031ab7d52467f9107b5cba3a6caf6b4cd1d483 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 1 Dec 2019 16:26:37 +0100
Subject: [PATCH 05/60] Started adding split amd merge functions
---
nw/project/project.py | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/nw/project/project.py b/nw/project/project.py
index 041029a1..2a88333b 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -369,6 +369,39 @@ class NWProject():
self.clearProject()
return True
+ ##
+ # Document Methods
+ ##
+
+ def splitDocument(self, tHandle, headerLevel, folderLevel):
+ """Split a document into multiple documents under the same
+ header. Header level threshold determines at what level the
+ splitting should occur, and folders can also be created at a
+ certain structure level.
+ """
+
+ return True
+
+ def mergeDocuments(self, handleList):
+ """Merge a list of document handles into a single document.
+ """
+
+ fileList = []
+ for tHandle in handleList:
+ tItem = self.getItem(tHandle)
+ if tItem is None:
+ continue
+ if tItem.itemType == nwItemType.FILE:
+ fileList.append(tHandle)
+ else:
+ pass
+
+ mergeText = ""
+ for tHandle in fileList:
+ pass
+
+ return True
+
##
# Set Functions
##
From a111247a3c3a73572916c5b95e9744e4673fc09b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Tue, 21 Jan 2020 11:51:49 +0100
Subject: [PATCH 06/60] Added split and merge icons
---
.../graphics/{export.txt => license.txt} | 5 +++
nw/assets/graphics/merge.svg | 31 +++++++++++++++++++
nw/assets/graphics/split.svg | 31 +++++++++++++++++++
3 files changed, 67 insertions(+)
rename nw/assets/graphics/{export.txt => license.txt} (75%)
create mode 100644 nw/assets/graphics/merge.svg
create mode 100644 nw/assets/graphics/split.svg
diff --git a/nw/assets/graphics/export.txt b/nw/assets/graphics/license.txt
similarity index 75%
rename from nw/assets/graphics/export.txt
rename to nw/assets/graphics/license.txt
index 1a65e8da..8a822fd2 100644
--- a/nw/assets/graphics/export.txt
+++ b/nw/assets/graphics/license.txt
@@ -1,3 +1,8 @@
FROM ICON SET: Typicons
LICENSE: Creative Commons (Attribution-Share Alike 3.0 Unported)
https://creativecommons.org/licenses/by-sa/3.0/
+
+Apllies to:
+export.svg
+merge.svg
+split.svg
diff --git a/nw/assets/graphics/merge.svg b/nw/assets/graphics/merge.svg
new file mode 100644
index 00000000..1b4c6ae9
--- /dev/null
+++ b/nw/assets/graphics/merge.svg
@@ -0,0 +1,31 @@
+
+
diff --git a/nw/assets/graphics/split.svg b/nw/assets/graphics/split.svg
new file mode 100644
index 00000000..c427eea6
--- /dev/null
+++ b/nw/assets/graphics/split.svg
@@ -0,0 +1,31 @@
+
+
From 6cac679458ecdab38873cd379589bdf5f1b77457 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 23 Jan 2020 15:52:27 +0100
Subject: [PATCH 07/60] Added merge documents dialog and related code
---
nw/constants/constants.py | 1 +
nw/gui/__init__.py | 2 +
nw/gui/dialogs/__init__.py | 2 +
nw/gui/dialogs/docmerge.py | 108 +++++++++++++++++++++++++++++++++++++
nw/gui/icons.py | 2 +
nw/gui/mainmenu.py | 7 +++
nw/guimain.py | 13 ++++-
7 files changed, 134 insertions(+), 1 deletion(-)
create mode 100644 nw/gui/dialogs/docmerge.py
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index a0325482..d3ccd1c7 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -29,6 +29,7 @@ class nwFiles():
EXPORT_OPT = "exportOptions.json"
TLINE_OPT = "timelineOptions.json"
SLOG_OPT = "sessionLogOptions.json"
+ MERGE_OPT = "docMergeOptions.json"
# END Class nwFiles
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index b8196cc2..fad7b148 100644
--- a/nw/gui/__init__.py
+++ b/nw/gui/__init__.py
@@ -8,6 +8,7 @@ from nw.gui.theme import GuiTheme
# Dialogs
from nw.gui.dialogs.configeditor import GuiConfigEditor
+from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
@@ -33,6 +34,7 @@ __all__ = [
"GuiMainStatus",
"GuiTheme",
"GuiConfigEditor",
+ "GuiDocMerge",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py
index fac299ba..0c6b842e 100644
--- a/nw/gui/dialogs/__init__.py
+++ b/nw/gui/dialogs/__init__.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from nw.gui.dialogs.configeditor import GuiConfigEditor
+from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
@@ -9,6 +10,7 @@ from nw.gui.dialogs.timelineview import GuiTimeLineView
__all__ = [
"GuiConfigEditor",
+ "GuiDocMerge",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
diff --git a/nw/gui/dialogs/docmerge.py b/nw/gui/dialogs/docmerge.py
new file mode 100644
index 00000000..264fc703
--- /dev/null
+++ b/nw/gui/dialogs/docmerge.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+"""novelWriter GUI Doc Merge
+
+ novelWriter – GUI Doc Merge
+=============================
+ Tool for merging multiple documents to one
+
+ File History:
+ Created: 2020-01-23 [0.4.3]
+
+"""
+
+import logging
+import nw
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import (
+ QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QLabel,
+ QProgressBar
+)
+
+from nw.tools import OptLastState
+from nw.constants import nwFiles
+
+logger = logging.getLogger(__name__)
+
+class GuiDocMerge(QDialog):
+
+ def __init__(self, theParent, theProject):
+ QDialog.__init__(self, theParent)
+
+ logger.debug("Initialising GuiDocMerge ...")
+
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+ self.theProject = theProject
+ self.optState = DocMergeLastState(self.theProject,nwFiles.MERGE_OPT)
+ self.optState.loadSettings()
+
+ self.outerBox = QHBoxLayout()
+ self.innerBox = QVBoxLayout()
+ self.setWindowTitle("Merge Documents")
+ self.setLayout(self.outerBox)
+
+ self.guiDeco = self.theParent.theTheme.loadDecoration("merge",(64,64))
+
+ self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
+ self.outerBox.addLayout(self.innerBox)
+
+ self.doMergeForm = QGridLayout()
+ self.doMergeForm.setContentsMargins(10,5,0,10)
+
+ self.mergeButton = QPushButton("Merge")
+ self.mergeButton.clicked.connect(self._doMerge)
+
+ self.closeButton = QPushButton("Close")
+ self.closeButton.clicked.connect(self._doClose)
+
+ self.mergeStatus = QLabel("Ready ...")
+ self.mergeProgress = QProgressBar(self)
+
+ self.doMergeForm.addWidget(self.mergeStatus, 0, 0, 1, 3)
+ self.doMergeForm.addWidget(self.mergeProgress, 1, 0)
+ self.doMergeForm.addWidget(self.mergeButton, 1, 1)
+ self.doMergeForm.addWidget(self.closeButton, 1, 2)
+
+ self.innerBox.addLayout(self.doMergeForm)
+
+ self.rejected.connect(self._doClose)
+ self.show()
+
+ logger.debug("GuiDocMerge initialisation complete")
+
+ return
+
+ ##
+ # Buttons
+ ##
+
+ def _doMerge(self):
+
+ logger.verbose("GuiDocMerge merge button clicked")
+
+ return
+
+ def _doClose(self):
+
+ logger.verbose("GuiDocMerge close button clicked")
+
+ self.optState.saveSettings()
+ self.close()
+
+ return
+
+# END Class GuiDocMerge
+
+class DocMergeLastState(OptLastState):
+
+ def __init__(self, theProject, theFile):
+ OptLastState.__init__(self, theProject, theFile)
+ self.theState = {
+ }
+ self.stringOpt = ()
+ self.boolOpt = ()
+ self.intOpt = ()
+ return
+
+# END Class DocMergeLastState
diff --git a/nw/gui/icons.py b/nw/gui/icons.py
index 88149b6e..facb84d7 100644
--- a/nw/gui/icons.py
+++ b/nw/gui/icons.py
@@ -46,7 +46,9 @@ class GuiIcons:
DECO_MAP = {
"export" : "export.svg",
+ "merge" : "merge.svg",
"settings" : "gear.svg",
+ "split" : "split.svg",
}
def __init__(self, theParent):
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index ae5df846..061b5e2d 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -369,6 +369,13 @@ class GuiMainMenu(QMenuBar):
self.aImportFile.triggered.connect(self.theParent.importDocument)
self.docuMenu.addAction(self.aImportFile)
+ # Document > Merge Documents
+ self.aMergeDocs = QAction("Merge Documents", self)
+ self.aMergeDocs.setStatusTip("Merge multiple documents")
+ # self.aMergeDocs.setShortcut("Ctrl+Shift+I")
+ self.aMergeDocs.triggered.connect(self.theParent.mergeDocuments)
+ self.docuMenu.addAction(self.aMergeDocs)
+
return
def _buildViewMenu(self):
diff --git a/nw/guimain.py b/nw/guimain.py
index 63a31795..4572ee01 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -27,7 +27,7 @@ from nw.gui import (
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor,
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar,
GuiDocViewDetails, GuiConfigEditor, GuiProjectEditor, GuiExport,
- GuiItemEditor, GuiTimeLineView, GuiSessionLogView
+ GuiItemEditor, GuiTimeLineView, GuiSessionLogView, GuiDocMerge
)
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
from nw.tools import countWords
@@ -458,6 +458,17 @@ class GuiMain(QMainWindow):
return True
+ def mergeDocuments(self):
+ """Merge multiple documents to one single new document.
+ """
+
+ if self.mainConf.showGUI:
+ dlgMerge = GuiDocMerge(self, self.theProject)
+ if dlgMerge.exec_():
+ pass
+
+ return True
+
def passDocumentAction(self, theAction):
"""Pass on document action theAction to whatever document has
the focus. If no document has focus, the action is discarded.
From 6b79b7a42395c53e465e15cdf629665bc834317f Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Fri, 31 Jan 2020 17:17:07 +0100
Subject: [PATCH 08/60] Merging a folder of documents into one file now works
---
nw/gui/dialogs/docmerge.py | 78 +++++++++++++++++++++++++++++++++-----
nw/gui/elements/doctree.py | 25 ++++++++++--
2 files changed, 89 insertions(+), 14 deletions(-)
diff --git a/nw/gui/dialogs/docmerge.py b/nw/gui/dialogs/docmerge.py
index 264fc703..503eba44 100644
--- a/nw/gui/dialogs/docmerge.py
+++ b/nw/gui/dialogs/docmerge.py
@@ -16,11 +16,11 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QLabel,
- QProgressBar
+ QProgressBar, QListWidget, QAbstractItemView, QListWidgetItem
)
-
+from nw.constants import nwFiles, nwAlert, nwItemClass, nwItemType
+from nw.project import NWDoc
from nw.tools import OptLastState
-from nw.constants import nwFiles
logger = logging.getLogger(__name__)
@@ -34,6 +34,7 @@ class GuiDocMerge(QDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
+ self.sourceItem = None
self.optState = DocMergeLastState(self.theProject,nwFiles.MERGE_OPT)
self.optState.loadSettings()
@@ -50,25 +51,26 @@ class GuiDocMerge(QDialog):
self.doMergeForm = QGridLayout()
self.doMergeForm.setContentsMargins(10,5,0,10)
+ self.listBox = QListWidget()
+ self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
+
self.mergeButton = QPushButton("Merge")
self.mergeButton.clicked.connect(self._doMerge)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
- self.mergeStatus = QLabel("Ready ...")
- self.mergeProgress = QProgressBar(self)
-
- self.doMergeForm.addWidget(self.mergeStatus, 0, 0, 1, 3)
- self.doMergeForm.addWidget(self.mergeProgress, 1, 0)
- self.doMergeForm.addWidget(self.mergeButton, 1, 1)
- self.doMergeForm.addWidget(self.closeButton, 1, 2)
+ self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
+ self.doMergeForm.addWidget(self.mergeButton, 1, 1)
+ self.doMergeForm.addWidget(self.closeButton, 1, 2)
self.innerBox.addLayout(self.doMergeForm)
self.rejected.connect(self._doClose)
self.show()
+ self._populateList()
+
logger.debug("GuiDocMerge initialisation complete")
return
@@ -81,6 +83,31 @@ class GuiDocMerge(QDialog):
logger.verbose("GuiDocMerge merge button clicked")
+ finalOrder = []
+ for i in range(self.listBox.count()):
+ finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
+
+ theDoc = NWDoc(self.theProject, self.theParent)
+ theText = ""
+ for tHandle in finalOrder:
+ theText += theDoc.openDocument(tHandle, False).rstrip()
+ theText += "\n\n"
+
+ if self.sourceItem is None:
+ self.theParent.makeAlert((
+ "Cannot parse source item."
+ ), nwAlert.ERROR)
+ return
+
+ srcItem = self.theProject.getItem(self.sourceItem)
+ nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
+ self.theParent.treeView.revealTreeItem(nHandle)
+ theDoc.openDocument(nHandle, False)
+ theDoc.saveDocument(theText)
+ self.theParent.openDocument(nHandle)
+
+ self.close()
+
return
def _doClose(self):
@@ -92,6 +119,37 @@ class GuiDocMerge(QDialog):
return
+ ##
+ # Internal Functions
+ ##
+
+ def _populateList(self):
+
+ tHandle = self.theParent.treeView.getSelectedHandle()
+ self.sourceItem = tHandle
+ if tHandle is None:
+ return
+
+ nwItem = self.theProject.getItem(tHandle)
+ if nwItem is None:
+ return
+ if nwItem.itemType is not nwItemType.FOLDER:
+ self.theParent.makeAlert((
+ "Element selected in the project tree must be a folder."
+ ), nwAlert.ERROR)
+ return
+
+ for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
+ newItem = QListWidgetItem()
+ nwItem = self.theProject.getItem(sHandle)
+ if nwItem.itemType is not nwItemType.FILE:
+ continue
+ newItem.setText(nwItem.itemName)
+ newItem.setData(Qt.UserRole, sHandle)
+ self.listBox.addItem(newItem)
+
+ return
+
# END Class GuiDocMerge
class DocMergeLastState(OptLastState):
diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py
index d5b9fb4f..63a998dd 100644
--- a/nw/gui/elements/doctree.py
+++ b/nw/gui/elements/doctree.py
@@ -171,14 +171,21 @@ class GuiDocTree(QTreeWidget):
return False
# Add the new item to the tree
- nwItem = self.theProject.getItem(tHandle)
- trItem = self._addTreeItem(nwItem)
+ self.revealTreeItem(tHandle)
+ self.theParent.editItem()
+
+ return True
+
+ def revealTreeItem(self, tHandle):
+ """Reveal a newly added project item in the project tree.
+ """
+ nwItem = self.theProject.getItem(tHandle)
+ trItem = self._addTreeItem(nwItem)
+ pHandle = nwItem.parHandle
if pHandle is not None and pHandle in self.theMap.keys():
self.theMap[pHandle].setExpanded(True)
self.clearSelection()
trItem.setSelected(True)
- self.theParent.editItem()
-
return True
def moveTreeItem(self, nStep):
@@ -221,6 +228,16 @@ class GuiDocTree(QTreeWidget):
self.theProject.setTreeOrder(theList)
return True
+ def getTreeFromHandle(self, tHandle):
+ """Recursively return all the children items starting from a
+ given item handle.
+ """
+ theList = []
+ theItem = self._getTreeItem(tHandle)
+ if theItem is not None:
+ theList = self._scanChildren(theList, theItem, 0)
+ return theList
+
def getColumnSizes(self):
retVals = [
self.columnWidth(0),
From 605541c87dc440cfbc0fa0885f5653ff10472e3f Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Feb 2020 12:49:45 +0100
Subject: [PATCH 09/60] Some cleanup of the merge document dialog
---
nw/gui/dialogs/docmerge.py | 37 ++++++++++++++-----------------------
1 file changed, 14 insertions(+), 23 deletions(-)
diff --git a/nw/gui/dialogs/docmerge.py b/nw/gui/dialogs/docmerge.py
index 503eba44..2b46eba5 100644
--- a/nw/gui/dialogs/docmerge.py
+++ b/nw/gui/dialogs/docmerge.py
@@ -15,12 +15,11 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
- QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QLabel,
- QProgressBar, QListWidget, QAbstractItemView, QListWidgetItem
+ QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton,
+ QListWidget, QAbstractItemView, QListWidgetItem
)
-from nw.constants import nwFiles, nwAlert, nwItemClass, nwItemType
+from nw.constants import nwAlert, nwItemType
from nw.project import NWDoc
-from nw.tools import OptLastState
logger = logging.getLogger(__name__)
@@ -35,8 +34,6 @@ class GuiDocMerge(QDialog):
self.theParent = theParent
self.theProject = theProject
self.sourceItem = None
- self.optState = DocMergeLastState(self.theProject,nwFiles.MERGE_OPT)
- self.optState.loadSettings()
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
@@ -80,6 +77,10 @@ class GuiDocMerge(QDialog):
##
def _doMerge(self):
+ """Perform the merge of the files in the selected folder, and
+ create a new file in the same parent folder. The old files are
+ not removed in the merge process, and must be deleted manually.
+ """
logger.verbose("GuiDocMerge merge button clicked")
@@ -111,12 +112,10 @@ class GuiDocMerge(QDialog):
return
def _doClose(self):
-
+ """Close the dialog window without doing anything.
+ """
logger.verbose("GuiDocMerge close button clicked")
-
- self.optState.saveSettings()
self.close()
-
return
##
@@ -124,6 +123,11 @@ class GuiDocMerge(QDialog):
##
def _populateList(self):
+ """Get the item selected in the tree, check that it is a folder,
+ and try to find all files associated with it. The valid files
+ are then added to the list view in order. The list itself can be
+ reordered by the user.
+ """
tHandle = self.theParent.treeView.getSelectedHandle()
self.sourceItem = tHandle
@@ -151,16 +155,3 @@ class GuiDocMerge(QDialog):
return
# END Class GuiDocMerge
-
-class DocMergeLastState(OptLastState):
-
- def __init__(self, theProject, theFile):
- OptLastState.__init__(self, theProject, theFile)
- self.theState = {
- }
- self.stringOpt = ()
- self.boolOpt = ()
- self.intOpt = ()
- return
-
-# END Class DocMergeLastState
From f937ebc396e956a3b251e2d7bf6f9f42599cbccb Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Feb 2020 14:10:48 +0100
Subject: [PATCH 10/60] Added code to permanently delete project documents
---
nw/gui/elements/doctree.py | 69 ++++++++++++++++++++++++++++++++------
nw/gui/mainmenu.py | 4 +--
nw/project/document.py | 35 ++++++++++++++-----
nw/project/project.py | 50 ++++++---------------------
4 files changed, 96 insertions(+), 62 deletions(-)
diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py
index 63a998dd..30a369e1 100644
--- a/nw/gui/elements/doctree.py
+++ b/nw/gui/elements/doctree.py
@@ -16,10 +16,10 @@ import nw
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QFont, QColor
from PyQt5.QtWidgets import (
- QTreeWidget, QTreeWidgetItem, QAbstractItemView, QApplication
+ QTreeWidget, QTreeWidgetItem, QAbstractItemView, QApplication, QMessageBox
)
-from nw.project import NWItem
+from nw.project import NWItem, NWDoc
from nw.constants import (
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert
)
@@ -263,21 +263,65 @@ class GuiDocTree(QTreeWidget):
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.getItem(tHandle)
+ if nwItemS is None:
+ return False
+
if nwItemS.itemType == nwItemType.FILE:
logger.debug("User requested file %s moved to trash" % tHandle)
trItemP = trItemS.parent()
trItemT = self._addTrashRoot()
if trItemP is None or trItemT is None:
- logger.error("Could not move item to trash")
+ logger.error("Could not delete item")
return False
- tIndex = trItemP.indexOfChild(trItemS)
- trItemC = trItemP.takeChild(tIndex)
- trItemT.addChild(trItemC)
- nwItemS.setParent(self.theProject.trashRoot)
- self.clearSelection()
- trItemP.setSelected(True)
- self.theProject.setProjectChanged(True)
- self.theParent.theIndex.deleteHandle(tHandle)
+
+ pHandle = nwItemS.parHandle
+ if pHandle is not None and pHandle == self.theProject.trashRoot:
+ # If the file is in the trash folder already, as the
+ # user if they want to permanently delete the file.
+
+ doPermanent = False
+ if self.mainConf.showGUI:
+ msgBox = QMessageBox()
+ msgRes = msgBox.question(
+ self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
+ )
+ if msgRes == QMessageBox.Yes:
+ doPermanent = True
+ else:
+ doPermanent = True
+
+ if doPermanent:
+ logger.debug("Permanently deleting file with handle %s" % tHandle)
+
+ tIndex = trItemP.indexOfChild(trItemS)
+ trItemC = trItemP.takeChild(tIndex)
+ self.clearSelection()
+ trItemP.setSelected(True)
+
+ if self.theParent.docEditor.theHandle == tHandle:
+ self.theParent.closeDocument()
+
+ theDoc = NWDoc(self.theProject, self.theParent)
+ theDoc.deleteDocument(tHandle)
+ self.theProject.deleteItem(tHandle)
+ self.theParent.theIndex.deleteHandle(tHandle)
+
+ else:
+ # The file is not already in the trash folder, so we
+ # move it there.
+
+ if pHandle is None:
+ logger.warning("File has no parent item")
+
+ tIndex = trItemP.indexOfChild(trItemS)
+ trItemC = trItemP.takeChild(tIndex)
+ trItemT.addChild(trItemC)
+ nwItemS.setParent(self.theProject.trashRoot)
+ self.clearSelection()
+ trItemP.setSelected(True)
+
+ self.theProject.setProjectChanged(True)
+ self.theParent.theIndex.deleteHandle(tHandle)
elif nwItemS.itemType == nwItemType.FOLDER:
logger.debug("User requested folder %s deleted" % tHandle)
@@ -445,6 +489,9 @@ class GuiDocTree(QTreeWidget):
return newItem
def _addTrashRoot(self):
+ """Adds the trash root folder if it doesn't already exist in the
+ project tree.
+ """
if self.theProject.trashRoot is None:
self.theProject.addTrash()
trItem = self._addTreeItem(
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index 061b5e2d..580aacaf 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -370,8 +370,8 @@ class GuiMainMenu(QMenuBar):
self.docuMenu.addAction(self.aImportFile)
# Document > Merge Documents
- self.aMergeDocs = QAction("Merge Documents", self)
- self.aMergeDocs.setStatusTip("Merge multiple documents")
+ self.aMergeDocs = QAction("Merge Folder to Document", self)
+ self.aMergeDocs.setStatusTip("Merge a folder of documents to a single document")
# self.aMergeDocs.setShortcut("Ctrl+Shift+I")
self.aMergeDocs.triggered.connect(self.theParent.mergeDocuments)
self.docuMenu.addAction(self.aMergeDocs)
diff --git a/nw/project/document.py b/nw/project/document.py
index 0cc0fa66..e2a46d65 100644
--- a/nw/project/document.py
+++ b/nw/project/document.py
@@ -59,7 +59,7 @@ class NWDoc():
if self.theItem.parHandle == self.theProject.trashRoot:
self.docEditable = False
- docDir, docFile = self._assemblePath(self.FILE_MN)
+ docDir, docFile = self.assemblePath(self.docHandle, self.FILE_MN)
self.fileLoc = path.join(docDir,docFile)
logger.debug("Opening document %s" % self.fileLoc)
dataDir = path.join(self.theProject.projPath, docDir)
@@ -92,7 +92,7 @@ class NWDoc():
if self.docHandle is None or not self.docEditable:
return False
- docDir, docFile = self._assemblePath(self.FILE_MN)
+ docDir, docFile = self.assemblePath(self.docHandle, self.FILE_MN)
logger.debug("Saving document %s" % path.join(docDir,docFile))
dataPath = path.join(self.theProject.projPath, docDir)
docPath = path.join(dataPath, docFile)
@@ -124,15 +124,32 @@ class NWDoc():
return True
- ##
- # Internal Functions
- ##
+ def deleteDocument(self, tHandle):
+ """Permanently delete a document source file and its backups
+ from the project data folder.
+ """
+ docDir, docFile = self.assemblePath(tHandle, self.FILE_MN)
+ dataPath = path.join(self.theProject.projPath, docDir)
+ chkList = []
+ chkList.append(path.join(dataPath, docFile))
+ chkList.append(path.join(dataPath,docFile[:-3]+"tmp"))
+ chkList.append(path.join(dataPath,docFile[:-3]+"bak"))
+ for chkFile in chkList:
+ if path.isfile(chkFile):
+ try:
+ unlink(chkFile)
+ logger.debug("Deleted: %s" % chkFile)
+ except Exception as e:
+ self.makeAlert(["Could not delete document file.",str(e)], nwAlert.ERROR)
+ return False
+ return True
- def _assemblePath(self, docExt):
- if self.docHandle is None:
+ @staticmethod
+ def assemblePath(tHandle, docExt):
+ if tHandle is None:
return None
- docDir = "data_"+self.docHandle[0]
- docFile = self.docHandle[1:13]+"_"+docExt
+ docDir = "data_"+tHandle[0]
+ docFile = tHandle[1:13]+"_"+docExt
return docDir, docFile
# END Class NWDoc
diff --git a/nw/project/project.py b/nw/project/project.py
index 2a88333b..a52a83f3 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -13,7 +13,7 @@
import logging
import nw
-from os import path, mkdir, listdir
+from os import path, mkdir, listdir, unlink
from shutil import copyfile
from lxml import etree
from hashlib import sha256
@@ -369,39 +369,6 @@ class NWProject():
self.clearProject()
return True
- ##
- # Document Methods
- ##
-
- def splitDocument(self, tHandle, headerLevel, folderLevel):
- """Split a document into multiple documents under the same
- header. Header level threshold determines at what level the
- splitting should occur, and folders can also be created at a
- certain structure level.
- """
-
- return True
-
- def mergeDocuments(self, handleList):
- """Merge a list of document handles into a single document.
- """
-
- fileList = []
- for tHandle in handleList:
- tItem = self.getItem(tHandle)
- if tItem is None:
- continue
- if tItem.itemType == nwItemType.FILE:
- fileList.append(tHandle)
- else:
- pass
-
- mergeText = ""
- for tHandle in fileList:
- pass
-
- return True
-
##
# Set Functions
##
@@ -485,9 +452,6 @@ class NWProject():
self.setProjectChanged(True)
return True
- def getSessionWordCount(self):
- return self.currWCount - self.lastWCount
-
def setStatusColours(self, newCols):
replaceMap = self.statusItems.setNewEntries(newCols)
if self.projTree is not None:
@@ -530,6 +494,9 @@ class NWProject():
logger.error("No tree item with handle %s" % str(tHandle))
return None
+ def getSessionWordCount(self):
+ return self.currWCount - self.lastWCount
+
def getRootItem(self, tHandle):
"""Iterate upwards in the tree until we find the item with
parent None, the root item. We do this with a for loop with a
@@ -538,10 +505,13 @@ class NWProject():
tItem = self.getItem(tHandle)
if tItem is not None:
for i in range(200):
- tHandle = tItem.parHandle
- tItem = self.getItem(tHandle)
- if tItem is None:
+ if tItem.parHandle is None:
return tHandle
+ else:
+ tHandle = tItem.parHandle
+ tItem = self.getItem(tHandle)
+ if tItem is None:
+ return tHandle
return None
def getProjectItems(self):
From 068674e63dc05cfe19b093d67095f04e348b9c78 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Tue, 4 Feb 2020 23:57:29 +0100
Subject: [PATCH 11/60] Remove the change of selected item when deleting, and
let the tree widget handle it
---
nw/gui/elements/doctree.py | 6 ------
1 file changed, 6 deletions(-)
diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py
index 30a369e1..5ade6c5b 100644
--- a/nw/gui/elements/doctree.py
+++ b/nw/gui/elements/doctree.py
@@ -295,8 +295,6 @@ class GuiDocTree(QTreeWidget):
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
- self.clearSelection()
- trItemP.setSelected(True)
if self.theParent.docEditor.theHandle == tHandle:
self.theParent.closeDocument()
@@ -317,8 +315,6 @@ class GuiDocTree(QTreeWidget):
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
nwItemS.setParent(self.theProject.trashRoot)
- self.clearSelection()
- trItemP.setSelected(True)
self.theProject.setProjectChanged(True)
self.theParent.theIndex.deleteHandle(tHandle)
@@ -332,8 +328,6 @@ class GuiDocTree(QTreeWidget):
tIndex = trItemP.indexOfChild(trItemS)
if trItemS.childCount() == 0:
trItemP.takeChild(tIndex)
- self.clearSelection()
- trItemP.setSelected(True)
self.theProject.deleteItem(tHandle)
else:
self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR)
From c43e121389a23931f7cb80c55edcd12ab809012a Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Tue, 4 Feb 2020 23:59:00 +0100
Subject: [PATCH 12/60] Added document split dialog and associated code
---
nw/gui/__init__.py | 2 +
nw/gui/dialogs/__init__.py | 2 +
nw/gui/dialogs/docsplit.py | 247 +++++++++++++++++++++++++++++++++++++
nw/gui/mainmenu.py | 7 +-
nw/guimain.py | 21 ++--
nw/project/project.py | 5 +
6 files changed, 275 insertions(+), 9 deletions(-)
create mode 100644 nw/gui/dialogs/docsplit.py
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index fad7b148..d8842bcc 100644
--- a/nw/gui/__init__.py
+++ b/nw/gui/__init__.py
@@ -9,6 +9,7 @@ from nw.gui.theme import GuiTheme
# Dialogs
from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.docmerge import GuiDocMerge
+from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
@@ -35,6 +36,7 @@ __all__ = [
"GuiTheme",
"GuiConfigEditor",
"GuiDocMerge",
+ "GuiDocSplit",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py
index 0c6b842e..a58b4982 100644
--- a/nw/gui/dialogs/__init__.py
+++ b/nw/gui/dialogs/__init__.py
@@ -2,6 +2,7 @@
from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.docmerge import GuiDocMerge
+from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
@@ -11,6 +12,7 @@ from nw.gui.dialogs.timelineview import GuiTimeLineView
__all__ = [
"GuiConfigEditor",
"GuiDocMerge",
+ "GuiDocSplit",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py
new file mode 100644
index 00000000..3e8502b5
--- /dev/null
+++ b/nw/gui/dialogs/docsplit.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+"""novelWriter GUI Doc Split
+
+ novelWriter – GUI Doc Split
+=============================
+ Tool for splitting a single document into multiple documents
+
+ File History:
+ Created: 2020-02-01 [0.4.3]
+
+"""
+
+import logging
+import nw
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import (
+ QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QComboBox,
+ QListWidget, QAbstractItemView, QListWidgetItem
+)
+from nw.constants import nwAlert, nwItemType, nwItemClass, nwItemLayout
+from nw.project import NWDoc
+
+logger = logging.getLogger(__name__)
+
+class GuiDocSplit(QDialog):
+
+ def __init__(self, theParent, theProject):
+ QDialog.__init__(self, theParent)
+
+ logger.debug("Initialising GuiDocSplit ...")
+
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+ self.theProject = theProject
+ self.sourceItem = None
+
+ self.outerBox = QHBoxLayout()
+ self.innerBox = QVBoxLayout()
+ self.setWindowTitle("Split Document")
+ self.setLayout(self.outerBox)
+
+ self.guiDeco = self.theParent.theTheme.loadDecoration("split",(64,64))
+
+ self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
+ self.outerBox.addLayout(self.innerBox)
+
+ self.doMergeForm = QGridLayout()
+ self.doMergeForm.setContentsMargins(10,5,0,10)
+
+ self.listBox = QListWidget()
+ self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
+
+ self.splitLevel = QComboBox(self)
+ self.splitLevel.addItem("Split on Title (Level 1)", 1)
+ self.splitLevel.addItem("Split on Chapter (Level 2)", 2)
+ self.splitLevel.addItem("Split on Scene (Level 3)", 3)
+ self.splitLevel.addItem("Split on Section (Level 4)", 4)
+ self.splitLevel.setCurrentIndex(2)
+ self.splitLevel.currentIndexChanged.connect(self._populateList)
+
+ self.splitButton = QPushButton("Split")
+ self.splitButton.clicked.connect(self._doSplit)
+
+ self.closeButton = QPushButton("Close")
+ self.closeButton.clicked.connect(self._doClose)
+
+ self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
+ self.doMergeForm.addWidget(self.splitLevel, 1, 0, 1, 2)
+ self.doMergeForm.addWidget(self.splitButton, 2, 1)
+ self.doMergeForm.addWidget(self.closeButton, 2, 2)
+
+ self.innerBox.addLayout(self.doMergeForm)
+
+ self.rejected.connect(self._doClose)
+ self.show()
+
+ self._populateList()
+
+ logger.debug("GuiDocSplit initialisation complete")
+
+ return
+
+ ##
+ # Buttons
+ ##
+
+ def _doSplit(self):
+ """Perform the split of the file, create a new folder in the
+ same parent folder, and multiple files depending on split level
+ settings. The old file is not removed in the merge process, and
+ must be deleted manually.
+ """
+
+ logger.verbose("GuiDocSplit split button clicked")
+
+ if self.sourceItem is None:
+ self.theParent.makeAlert((
+ "No source document selected. Nothing to do."
+ ), nwAlert.ERROR)
+ return
+
+ srcItem = self.theProject.getItem(self.sourceItem)
+ if srcItem is None:
+ self.theParent.makeAlert((
+ "Could not parse source document."
+ ), nwAlert.ERROR)
+ return
+
+ theDoc = NWDoc(self.theProject, self.theParent)
+ theText = theDoc.openDocument(self.sourceItem, False)
+ theLines = theText.splitlines()
+ nLines = len(theLines)
+ theLines.insert(0, "%Split Doc")
+
+ finalOrder = []
+ for i in range(self.listBox.count()):
+ listItem = self.listBox.item(i)
+ wTitle = listItem.text()
+ lineNo = listItem.data(Qt.UserRole)
+ finalOrder.append([wTitle, lineNo, nLines])
+ if i > 0:
+ finalOrder[i-1][2] = lineNo
+
+ if len(finalOrder) == 0:
+ self.theParent.makeAlert((
+ "No headers found. Nothing to do."
+ ), nwAlert.ERROR)
+ return
+
+ fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
+ self.theParent.treeView.revealTreeItem(fHandle)
+
+ for wTitle, iStart, iEnd in finalOrder:
+
+ itemLayout = nwItemLayout.NOTE
+ if srcItem.itemClass == nwItemClass.NOVEL:
+ if wTitle.startswith("# "):
+ itemLayout = nwItemLayout.PARTITION
+ elif wTitle.startswith("## "):
+ itemLayout = nwItemLayout.CHAPTER
+ elif wTitle.startswith("### "):
+ itemLayout = nwItemLayout.SCENE
+ elif wTitle.startswith("#### "):
+ itemLayout = nwItemLayout.PAGE
+
+ wTitle = wTitle.lstrip("#")
+ wTitle = wTitle.strip()
+ print(wTitle, iStart, iEnd)
+
+ nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle)
+ newItem = self.theProject.getItem(nHandle)
+ newItem.setLayout(itemLayout)
+
+ theText = "\n".join(theLines[iStart:iEnd])
+ theDoc.openDocument(nHandle, False)
+ theDoc.saveDocument(theText)
+ theDoc.clearDocument()
+ self.theParent.treeView.revealTreeItem(nHandle)
+
+ # theDoc = NWDoc(self.theProject, self.theParent)
+ # theText = ""
+ # for tHandle in finalOrder:
+ # theText += theDoc.openDocument(tHandle, False).rstrip()
+ # theText += "\n\n"
+
+ # if self.sourceItem is None:
+ # self.theParent.makeAlert((
+ # "Cannot parse source item."
+ # ), nwAlert.ERROR)
+ # return
+
+ # srcItem = self.theProject.getItem(self.sourceItem)
+ # nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
+ # self.theParent.treeView.revealTreeItem(nHandle)
+ # theDoc.openDocument(nHandle, False)
+ # theDoc.saveDocument(theText)
+ # self.theParent.openDocument(nHandle)
+
+ self.close()
+
+ return
+
+ def _doClose(self):
+ """Close the dialog window without doing anything.
+ """
+ logger.verbose("GuiDocSplit close button clicked")
+ self.close()
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _populateList(self):
+ """Get the item selected in the tree, check that it is a folder,
+ and try to find all files associated with it. The valid files
+ are then added to the list view in order. The list itself can be
+ reordered by the user.
+ """
+
+ if self.sourceItem is None:
+ self.sourceItem = self.theParent.treeView.getSelectedHandle()
+
+ if self.sourceItem is None:
+ return
+
+ nwItem = self.theProject.getItem(self.sourceItem)
+ if nwItem is None:
+ return
+ if nwItem.itemType is not nwItemType.FILE:
+ self.theParent.makeAlert((
+ "Element selected in the project tree must be a file."
+ ), nwAlert.ERROR)
+ return
+
+ self.listBox.clear()
+ theDoc = NWDoc(self.theProject, self.theParent)
+ theText = theDoc.openDocument(self.sourceItem, False)
+
+ spLevel = self.splitLevel.currentData()
+ logger.debug("Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel))
+
+ lineNo = 0
+ for aLine in theText.splitlines():
+
+ lineNo += 1
+ onLine = 0
+
+ if aLine.startswith("# ") and spLevel >= 1:
+ onLine = lineNo
+ elif aLine.startswith("## ") and spLevel >= 2:
+ onLine = lineNo
+ elif aLine.startswith("### ") and spLevel >= 3:
+ onLine = lineNo
+ elif aLine.startswith("#### ") and spLevel >= 4:
+ onLine = lineNo
+
+ if onLine > 0:
+ newItem = QListWidgetItem()
+ newItem.setText(aLine.strip())
+ newItem.setData(Qt.UserRole, onLine)
+ self.listBox.addItem(newItem)
+
+ return
+
+# END Class GuiDocSplit
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index 580aacaf..ba75e779 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -372,10 +372,15 @@ class GuiMainMenu(QMenuBar):
# Document > Merge Documents
self.aMergeDocs = QAction("Merge Folder to Document", self)
self.aMergeDocs.setStatusTip("Merge a folder of documents to a single document")
- # self.aMergeDocs.setShortcut("Ctrl+Shift+I")
self.aMergeDocs.triggered.connect(self.theParent.mergeDocuments)
self.docuMenu.addAction(self.aMergeDocs)
+ # Document > Split Document
+ self.aSplitDoc = QAction("Split Document to Folder", self)
+ self.aSplitDoc.setStatusTip("Split a document into a folder of multiple documents")
+ self.aSplitDoc.triggered.connect(self.theParent.splitDocument)
+ self.docuMenu.addAction(self.aSplitDoc)
+
return
def _buildViewMenu(self):
diff --git a/nw/guimain.py b/nw/guimain.py
index 4572ee01..bfc67115 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -24,10 +24,10 @@ from PyQt5.QtWidgets import (
)
from nw.gui import (
- GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor,
- GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar,
- GuiDocViewDetails, GuiConfigEditor, GuiProjectEditor, GuiExport,
- GuiItemEditor, GuiTimeLineView, GuiSessionLogView, GuiDocMerge
+ GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport,
+ GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
+ GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiTimeLineView,
+ GuiSessionLogView, GuiDocMerge, GuiDocSplit
)
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
from nw.tools import countWords
@@ -83,7 +83,7 @@ class GuiMain(QMainWindow):
# Assemble Main Window
self.treePane = QFrame()
- self.treeBox = QVBoxLayout()
+ self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0,0,0,0)
self.treeBox.addWidget(self.treeView)
self.treeBox.addWidget(self.treeMeta)
@@ -461,12 +461,17 @@ class GuiMain(QMainWindow):
def mergeDocuments(self):
"""Merge multiple documents to one single new document.
"""
-
if self.mainConf.showGUI:
dlgMerge = GuiDocMerge(self, self.theProject)
- if dlgMerge.exec_():
- pass
+ dlgMerge.exec_()
+ return True
+ def splitDocument(self):
+ """Split a single document into multiple documents.
+ """
+ if self.mainConf.showGUI:
+ dlgSplit = GuiDocSplit(self, self.theProject)
+ dlgSplit.exec_()
return True
def passDocumentAction(self, theAction):
diff --git a/nw/project/project.py b/nw/project/project.py
index a52a83f3..e6167eb9 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -563,6 +563,11 @@ class NWProject():
"""This only removes the item from the order list, but not from
the project tree.
"""
+ if tHandle not in self.treeOrder:
+ logger.warning(
+ "Could not remove item %s from treeOrder as it does not exist" % tHandle
+ )
+ return False
self.treeOrder.remove(tHandle)
self.setProjectChanged(True)
return True
From 643a301e2d1b82c74ad125699a30fa8df2cdb7fc Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 5 Feb 2020 00:10:29 +0100
Subject: [PATCH 13/60] Some cleanup in the new doc split dialog
---
nw/gui/dialogs/docsplit.py | 29 ++++++++---------------------
1 file changed, 8 insertions(+), 21 deletions(-)
diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py
index 3e8502b5..739bb14e 100644
--- a/nw/gui/dialogs/docsplit.py
+++ b/nw/gui/dialogs/docsplit.py
@@ -66,7 +66,7 @@ class GuiDocSplit(QDialog):
self.closeButton.clicked.connect(self._doClose)
self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
- self.doMergeForm.addWidget(self.splitLevel, 1, 0, 1, 2)
+ self.doMergeForm.addWidget(self.splitLevel, 1, 0, 1, 3)
self.doMergeForm.addWidget(self.splitButton, 2, 1)
self.doMergeForm.addWidget(self.closeButton, 2, 2)
@@ -112,6 +112,9 @@ class GuiDocSplit(QDialog):
theLines = theText.splitlines()
nLines = len(theLines)
theLines.insert(0, "%Split Doc")
+ logger.debug(
+ "Splitting document %s with %d lines" % (self.sourceItem,nLines)
+ )
finalOrder = []
for i in range(self.listBox.count()):
@@ -130,6 +133,7 @@ class GuiDocSplit(QDialog):
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
self.theParent.treeView.revealTreeItem(fHandle)
+ logger.verbose("Creating folder %s" % fHandle)
for wTitle, iStart, iEnd in finalOrder:
@@ -146,11 +150,13 @@ class GuiDocSplit(QDialog):
wTitle = wTitle.lstrip("#")
wTitle = wTitle.strip()
- print(wTitle, iStart, iEnd)
nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle)
newItem = self.theProject.getItem(nHandle)
newItem.setLayout(itemLayout)
+ logger.verbose(
+ "Creating new document %s with text from line %d to %d" % (nHandle, iStart, iEnd-1)
+ )
theText = "\n".join(theLines[iStart:iEnd])
theDoc.openDocument(nHandle, False)
@@ -158,25 +164,6 @@ class GuiDocSplit(QDialog):
theDoc.clearDocument()
self.theParent.treeView.revealTreeItem(nHandle)
- # theDoc = NWDoc(self.theProject, self.theParent)
- # theText = ""
- # for tHandle in finalOrder:
- # theText += theDoc.openDocument(tHandle, False).rstrip()
- # theText += "\n\n"
-
- # if self.sourceItem is None:
- # self.theParent.makeAlert((
- # "Cannot parse source item."
- # ), nwAlert.ERROR)
- # return
-
- # srcItem = self.theProject.getItem(self.sourceItem)
- # nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
- # self.theParent.treeView.revealTreeItem(nHandle)
- # theDoc.openDocument(nHandle, False)
- # theDoc.saveDocument(theText)
- # self.theParent.openDocument(nHandle)
-
self.close()
return
From 6cfa2853fe1d71c424612c2d81408dc3d5e3ae34 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 5 Feb 2020 01:02:18 +0100
Subject: [PATCH 14/60] trying to make travis run again
---
.travis.yml | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index d40b1c68..bba7e64e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,12 +11,17 @@ addons:
- libenchant-dev
- python3-pyqt5
- python3-pyqt5.qtsvg
+ - python3-appdirs
+ - python3-lxml
+ - python3-enchant
python:
- "3.5"
- "3.7"
install:
- - pip install -r requirements.txt
+# - pip install -r requirements.txt
# - pip install pytest-faulthandler
+ - pip install latexcodec
+ - pip install pypandoc
- pip install pytest-xvfb
- pip install pytest-cov
- pip install pytest-qt
From f58b7afd43431a7672d135ef261cb94b221a7c9b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 5 Feb 2020 01:10:01 +0100
Subject: [PATCH 15/60] trying to make travis run again
---
.travis.yml | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index bba7e64e..82955733 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,19 +9,19 @@ addons:
apt:
packages:
- libenchant-dev
- - python3-pyqt5
- - python3-pyqt5.qtsvg
- - python3-appdirs
- - python3-lxml
- - python3-enchant
+# - python3-pyqt5
+# - python3-pyqt5.qtsvg
+# - python3-appdirs
+# - python3-lxml
+# - python3-enchant
python:
- "3.5"
- "3.7"
install:
-# - pip install -r requirements.txt
+ - pip install -r requirements.txt
# - pip install pytest-faulthandler
- - pip install latexcodec
- - pip install pypandoc
+# - pip install latexcodec
+# - pip install pypandoc
- pip install pytest-xvfb
- pip install pytest-cov
- pip install pytest-qt
From 5d971508426e973361430b5b186bc0f748a830bd Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 5 Feb 2020 01:13:07 +0100
Subject: [PATCH 16/60] trying to make travis run again
---
.travis.yml | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 82955733..8097a051 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,19 +9,15 @@ addons:
apt:
packages:
- libenchant-dev
-# - python3-pyqt5
-# - python3-pyqt5.qtsvg
-# - python3-appdirs
-# - python3-lxml
-# - python3-enchant
+ - python3-pyqt5
+ - python3-pyqt5.qtsvg
python:
- "3.5"
- "3.7"
install:
+ - pip install --upgrade pip
- pip install -r requirements.txt
# - pip install pytest-faulthandler
-# - pip install latexcodec
-# - pip install pypandoc
- pip install pytest-xvfb
- pip install pytest-cov
- pip install pytest-qt
From cac75abcafdeed688a2e69f5faa930137b139e99 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 5 Feb 2020 01:22:19 +0100
Subject: [PATCH 17/60] Switch ubutu distro on travis builds
---
.travis.yml | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 8097a051..5958aeec 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,9 @@
os: linux
-dist: xenial
+dist: bionic
services:
- xvfb
language: python
cache: bundler
-sudo: required
addons:
apt:
packages:
@@ -13,7 +12,7 @@ addons:
- python3-pyqt5.qtsvg
python:
- "3.5"
- - "3.7"
+ - "3.8"
install:
- pip install --upgrade pip
- pip install -r requirements.txt
From 123d64bba93a2da5f49bd02e3317120fe1bd5a4e Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 5 Feb 2020 01:27:17 +0100
Subject: [PATCH 18/60] Dropping Python 3.5 support
---
.travis.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index 5958aeec..de4a683a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,7 +11,8 @@ addons:
- python3-pyqt5
- python3-pyqt5.qtsvg
python:
- - "3.5"
+ - "3.6"
+ - "3.7"
- "3.8"
install:
- pip install --upgrade pip
From 833bc5ffd241fdf82ce5e4913a15a84c9f7f09a1 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 5 Feb 2020 01:33:43 +0100
Subject: [PATCH 19/60] Set minimum Python version to 3.6
---
novelWriter.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/novelWriter.py b/novelWriter.py
index 0723076f..d7c3d314 100755
--- a/novelWriter.py
+++ b/novelWriter.py
@@ -3,8 +3,8 @@
import sys
-if sys.hexversion < 0x030500F0:
- print("ERROR: At least Python 3.5 is required")
+if sys.hexversion < 0x030600F0:
+ print("ERROR: At least Python 3.6 is required")
sys.exit(1)
try:
From b03b80961b17491b0afb7e79f0ac200e70bf41d6 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Feb 2020 20:45:36 +0100
Subject: [PATCH 20/60] Clarified dropdown options on split document dialog
---
nw/gui/dialogs/docmerge.py | 2 +-
nw/gui/dialogs/docsplit.py | 10 +++++-----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/nw/gui/dialogs/docmerge.py b/nw/gui/dialogs/docmerge.py
index 2b46eba5..ba3f8d61 100644
--- a/nw/gui/dialogs/docmerge.py
+++ b/nw/gui/dialogs/docmerge.py
@@ -46,7 +46,7 @@ class GuiDocMerge(QDialog):
self.outerBox.addLayout(self.innerBox)
self.doMergeForm = QGridLayout()
- self.doMergeForm.setContentsMargins(10,5,0,10)
+ self.doMergeForm.setContentsMargins(0,0,0,0)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py
index 739bb14e..817a3284 100644
--- a/nw/gui/dialogs/docsplit.py
+++ b/nw/gui/dialogs/docsplit.py
@@ -46,16 +46,16 @@ class GuiDocSplit(QDialog):
self.outerBox.addLayout(self.innerBox)
self.doMergeForm = QGridLayout()
- self.doMergeForm.setContentsMargins(10,5,0,10)
+ self.doMergeForm.setContentsMargins(0,0,0,0)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.splitLevel = QComboBox(self)
- self.splitLevel.addItem("Split on Title (Level 1)", 1)
- self.splitLevel.addItem("Split on Chapter (Level 2)", 2)
- self.splitLevel.addItem("Split on Scene (Level 3)", 3)
- self.splitLevel.addItem("Split on Section (Level 4)", 4)
+ self.splitLevel.addItem("Split on Header Level 1 (Title)", 1)
+ self.splitLevel.addItem("Split up to Header Level 2 (Chapter)", 2)
+ self.splitLevel.addItem("Split up to Header Level 3 (Scene)", 3)
+ self.splitLevel.addItem("Split up to Header Level 4 (Section)", 4)
self.splitLevel.setCurrentIndex(2)
self.splitLevel.currentIndexChanged.connect(self._populateList)
From f2eedff6d212ca5fc3246990436496db5bc6757a Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Feb 2020 21:17:57 +0100
Subject: [PATCH 21/60] Added empty trash option
---
nw/gui/elements/doctree.py | 40 ++++++++++++++++++++++++++++++++++++--
nw/gui/mainmenu.py | 6 ++++++
2 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py
index 5ade6c5b..a388a7a4 100644
--- a/nw/gui/elements/doctree.py
+++ b/nw/gui/elements/doctree.py
@@ -246,7 +246,43 @@ class GuiDocTree(QTreeWidget):
]
return retVals
- def deleteItem(self, tHandle=None):
+ def emptyTrash(self):
+ """Permanently delete all documents in the Trash folder. This
+ function only asks for confirmation once, and calls the regular
+ deleteItem function for each document in the Trash folder.
+ """
+
+ if self.theProject.trashRoot is None:
+ self.makeAlert("There is no Trash folder.", nwAlert.INFO)
+ return False
+
+ theTrash = self.getTreeFromHandle(self.theProject.trashRoot)
+ if self.theProject.trashRoot in theTrash:
+ theTrash.remove(self.theProject.trashRoot)
+
+ nTrash = len(theTrash)
+ print(theTrash)
+ if nTrash == 0:
+ self.makeAlert("The Trash folder is empty.", nwAlert.INFO)
+ return False
+
+ msgBox = QMessageBox()
+ msgRes = msgBox.question(
+ self, "Empty Trash", "Permanently delete %d file%s from Trash?" % (
+ nTrash, "s"*int(nTrash > 1)
+ )
+ )
+ if msgRes != QMessageBox.Yes:
+ return False
+
+ for tHandle in self.getTreeFromHandle(self.theProject.trashRoot):
+ if tHandle == self.theProject.trashRoot:
+ continue
+ self.deleteItem(tHandle, True)
+
+ return True
+
+ def deleteItem(self, tHandle=None, alreadyAsked=False):
"""Delete items from the tree. Note that this does not delete
the item from the item tree in the project object. However,
since this is only meta data, there isn't really a need to do
@@ -280,7 +316,7 @@ class GuiDocTree(QTreeWidget):
# user if they want to permanently delete the file.
doPermanent = False
- if self.mainConf.showGUI:
+ if self.mainConf.showGUI and not alreadyAsked:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index ba75e779..b515576a 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -281,6 +281,12 @@ class GuiMainMenu(QMenuBar):
self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None))
self.projMenu.addAction(self.aDeleteItem)
+ # Project > Empty Trash
+ self.aEmptyTrash = QAction("Empty Trash", self)
+ self.aEmptyTrash.setStatusTip("Permanently delete all files in the Trash folder")
+ self.aEmptyTrash.triggered.connect(self.theParent.treeView.emptyTrash)
+ self.projMenu.addAction(self.aEmptyTrash)
+
# Project > Separator
self.projMenu.addSeparator()
From 963c819f859a05d70a3f89d7d8c5c97e68756160 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Feb 2020 21:20:34 +0100
Subject: [PATCH 22/60] Removed debug print and added logging stuff
---
nw/gui/elements/doctree.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py
index a388a7a4..28222af9 100644
--- a/nw/gui/elements/doctree.py
+++ b/nw/gui/elements/doctree.py
@@ -252,6 +252,7 @@ class GuiDocTree(QTreeWidget):
deleteItem function for each document in the Trash folder.
"""
+ logger.debug("Emptying Trash folder")
if self.theProject.trashRoot is None:
self.makeAlert("There is no Trash folder.", nwAlert.INFO)
return False
@@ -261,7 +262,6 @@ class GuiDocTree(QTreeWidget):
theTrash.remove(self.theProject.trashRoot)
nTrash = len(theTrash)
- print(theTrash)
if nTrash == 0:
self.makeAlert("The Trash folder is empty.", nwAlert.INFO)
return False
@@ -275,6 +275,7 @@ class GuiDocTree(QTreeWidget):
if msgRes != QMessageBox.Yes:
return False
+ logger.verbose("Deleting %d files from Trash" % nTrash)
for tHandle in self.getTreeFromHandle(self.theProject.trashRoot):
if tHandle == self.theProject.trashRoot:
continue
From 7858412419b621b87c9df5cfd135a63d469dc498 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Feb 2020 21:25:20 +0100
Subject: [PATCH 23/60] Added check to make sure items are not dropped on the
oprhaned folder
---
nw/gui/elements/doctree.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py
index 28222af9..ae7aef17 100644
--- a/nw/gui/elements/doctree.py
+++ b/nw/gui/elements/doctree.py
@@ -613,16 +613,22 @@ class GuiDocTree(QTreeWidget):
"""
sHandle = self.getSelectedHandle()
if sHandle is None:
+ logger.error("No handle selected")
return
- dIndex = self.indexAt(theEvent.pos())
+ dIndex = self.indexAt(theEvent.pos())
if not dIndex.isValid():
+ logger.error("Invalid drop index")
return
dItem = self.itemFromIndex(dIndex)
dHandle = dItem.text(self.C_HANDLE)
snItem = self.theProject.getItem(sHandle)
dnItem = self.theProject.getItem(dHandle)
+ if dnItem is None:
+ self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
+ return
+
isSame = snItem.itemClass == dnItem.itemClass
isNone = snItem.itemClass == nwItemClass.NO_CLASS
isNote = snItem.itemLayout == nwItemLayout.NOTE
From 6e6c9b98f773e0eb86d6c24e46e83d6a7209bec2 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Feb 2020 00:36:30 +0100
Subject: [PATCH 24/60] Additional unparsed command line options are assumed to
be a project path to open
---
nw/__init__.py | 7 ++++++-
nw/config.py | 3 ++-
nw/guimain.py | 4 ++++
3 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/nw/__init__.py b/nw/__init__.py
index 245b9043..02b62656 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -126,14 +126,18 @@ def main(sysArgs=None):
confPath = None
testMode = False
qtStyle = "Fusion"
+ cmdOpen = None
# Parse Options
try:
- inOpts, inArgs = getopt.getopt(sysArgs,shortOpt,longOpt)
+ inOpts, inRemain = getopt.getopt(sysArgs,shortOpt,longOpt)
except getopt.GetoptError:
print(helpMsg)
sys.exit(2)
+ if len(inRemain) > 0:
+ cmdOpen = inRemain[0]
+
for inOpt, inArg in inOpts:
if inOpt in ("-h","--help"):
print(helpMsg)
@@ -165,6 +169,7 @@ def main(sysArgs=None):
# Set Config Options
CONFIG.showGUI = not testMode
CONFIG.debugInfo = debugLevel < logging.INFO
+ CONFIG.cmdOpen = cmdOpen
# Set Logging
if showTime: debugStr = timeStr+debugStr
diff --git a/nw/config.py b/nw/config.py
index 281fe863..3aa6a254 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -41,6 +41,7 @@ class Config:
self.appHandle = nw.__package__.lower()
self.showGUI = True
self.debugInfo = False
+ self.cmdOpen = None
# Set Paths
self.confPath = None
@@ -57,7 +58,7 @@ class Config:
self.iconPath = None
# Set default values
- self.confChanged = False
+ self.confChanged = False
## General
self.guiTheme = "default"
diff --git a/nw/guimain.py b/nw/guimain.py
index bfc67115..a1b88781 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -182,6 +182,10 @@ class GuiMain(QMainWindow):
logger.debug("GUI initialisation complete")
+ if self.mainConf.cmdOpen is not None:
+ logger.debug("Opening project from additional command line option")
+ self.openProject(self.mainConf.cmdOpen)
+
return
def clearGUI(self):
From 3a25e7332bce6e223b39549f7f5a52668bd1a911 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Feb 2020 19:03:21 +0100
Subject: [PATCH 25/60] Change the way the project file is saved
---
nw/guimain.py | 6 ++--
nw/project/project.py | 65 +++++++++++++------------------------------
2 files changed, 22 insertions(+), 49 deletions(-)
diff --git a/nw/guimain.py b/nw/guimain.py
index a1b88781..b1fdf031 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -330,7 +330,7 @@ class GuiMain(QMainWindow):
return True
- def saveProject(self, isAuto=False):
+ def saveProject(self):
"""Save the current project.
"""
if not self.hasProject:
@@ -344,7 +344,7 @@ class GuiMain(QMainWindow):
return False
self.treeView.saveTreeOrder()
- self.theProject.saveProject(isAuto)
+ self.theProject.saveProject()
self.theIndex.saveIndex()
self.mainMenu.updateRecentProjects()
@@ -840,7 +840,7 @@ class GuiMain(QMainWindow):
if (self.hasProject and self.theProject.projChanged and
self.theProject.projPath is not None):
logger.debug("Autosaving project")
- self.saveProject(isAuto=True)
+ self.saveProject()
return
def _autoSaveDocument(self):
diff --git a/nw/project/project.py b/nw/project/project.py
index e6167eb9..a8960de2 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -13,7 +13,7 @@
import logging
import nw
-from os import path, mkdir, listdir, unlink
+from os import path, mkdir, listdir, unlink, rename
from shutil import copyfile
from lxml import etree
from hashlib import sha256
@@ -180,6 +180,11 @@ class NWProject():
return
def openProject(self, fileName):
+ """Open the project file provided, or if 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.
+ """
if not path.isfile(fileName):
fileName = path.join(fileName, nwFiles.PROJ_FILE)
@@ -288,7 +293,7 @@ class NWProject():
return True
- def saveProject(self, isAuto=False):
+ def saveProject(self):
if self.projPath is None:
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)
@@ -303,10 +308,6 @@ class NWProject():
logger.debug("Saving project: %s" % self.projPath)
- # Save a copy of the current file, just in case
- if not isAuto:
- self._maintainPrevious()
-
# Root element and project details
logger.debug("Writing project meta")
nwXML = etree.Element("novelWriterXML",attrib={
@@ -345,9 +346,11 @@ class NWProject():
self.projTree[tHandle].packXML(xContent)
# Write the xml tree to file
- saveFile = path.join(self.projPath,self.projFile)
+ tempFile = path.join(self.projPath, self.projFile+"~")
+ saveFile = path.join(self.projPath, self.projFile)
+ backFile = path.join(self.projPath, self.projFile[:-3]+"bak")
try:
- with open(saveFile,mode="wb") as outFile:
+ with open(tempFile, mode="wb") as outFile:
outFile.write(etree.tostring(
nwXML,
pretty_print = True,
@@ -358,6 +361,14 @@ class NWProject():
self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR)
return False
+ # If we're here, the file was successfully saved,
+ # so let's sort out the temps and backups
+ if path.isfile(backFile):
+ unlink(backFile)
+ if path.isfile(saveFile):
+ rename(saveFile, backFile)
+ rename(tempFile, saveFile)
+
self.mainConf.setRecent(self.projPath)
self.theParent.setStatus("Saved Project: %s" % self.projName)
self.setProjectChanged(False)
@@ -740,42 +751,4 @@ class NWProject():
itemHandle = self._makeHandle(addSeed+"!")
return itemHandle
- def _maintainPrevious(self):
- """This function will take the current project file and copy it
- into the project cache folder with an incremental file extension
- added. These serve as a backup in case the xml file gets
- corrupted.
- """
-
- countFile = path.join(self.projCache, nwFiles.PROJ_COUNT)
- projCount = 0
-
- if path.isfile(countFile):
- try:
- with open(countFile, mode="r") as inFile:
- projCount = int(inFile.read())+1
- except:
- projCount = 0
-
- if projCount > 9:
- projCount = 0
-
- projBackup = "%s.%d" % (nwFiles.PROJ_FILE, projCount)
-
- try:
- copyfile(
- path.join(self.projPath, self.projFile),
- path.join(self.projCache, projBackup)
- )
- except:
- logger.error("Failed to write to file %s" % projBackup)
-
- try:
- with open(countFile, mode="w") as outFile:
- outFile.write(str(projCount))
- except:
- logger.error("Failed to write to file %s" % countFile)
-
- return
-
# END Class NWProject
From ca7617ad075444778b74d279a4bdc081ca4cae79 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Feb 2020 19:07:51 +0100
Subject: [PATCH 26/60] Remove the project cache folder as it isn't, and was
never really, needed
---
nw/constants/constants.py | 1 -
nw/project/project.py | 17 +++++------------
tests/test_gui.py | 2 --
3 files changed, 5 insertions(+), 15 deletions(-)
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index d3ccd1c7..f85f6558 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -22,7 +22,6 @@ class nwFiles():
APP_ICON = "novelWriter.svg"
PROJ_FILE = "nwProject.nwx"
- PROJ_COUNT = "projCount.txt"
PROJ_DICT = "wordlist.txt"
SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json"
diff --git a/nw/project/project.py b/nw/project/project.py
index a8960de2..60cdfa72 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -50,7 +50,6 @@ class NWProject():
self.trashRoot = None # The handle of the trash root folder
self.projPath = None # The full path to where the currently open project is saved
self.projMeta = None # The full path to the project's meta data folder
- self.projCache = None # The full path to the project's cache folder
self.projDict = None # The spell check dictionary
self.projFile = None # The file name of the project main xml file
@@ -154,7 +153,6 @@ class NWProject():
self.trashRoot = None
self.projPath = None
self.projMeta = None
- self.projCache = None
self.projDict = None
self.projFile = nwFiles.PROJ_FILE
self.projName = ""
@@ -196,14 +194,11 @@ class NWProject():
self.projPath = path.dirname(fileName)
logger.debug("Opening project: %s" % self.projPath)
- self.projMeta = path.join(self.projPath,"meta")
- self.projCache = path.join(self.projPath,"cache")
- self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
+ self.projMeta = path.join(self.projPath,"meta")
+ self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
if not self._checkFolder(self.projMeta):
return
- if not self._checkFolder(self.projCache):
- return
try:
nwXML = etree.parse(fileName)
@@ -299,12 +294,10 @@ class NWProject():
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)
return False
- self.projMeta = path.join(self.projPath,"meta")
- self.projCache = path.join(self.projPath,"cache")
+ self.projMeta = path.join(self.projPath,"meta")
- if not self._checkFolder(self.projPath): return
- if not self._checkFolder(self.projMeta): return
- if not self._checkFolder(self.projCache): return
+ if not self._checkFolder(self.projPath): return
+ if not self._checkFolder(self.projMeta): return
logger.debug("Saving project: %s" % self.projPath)
diff --git a/tests/test_gui.py b/tests/test_gui.py
index 89121ace..dc279222 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -37,7 +37,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
assert nwGUI.theProject.trashRoot is None
assert nwGUI.theProject.projPath is None
assert nwGUI.theProject.projMeta is None
- assert nwGUI.theProject.projCache is None
assert nwGUI.theProject.projFile == "nwProject.nwx"
assert nwGUI.theProject.projName == ""
assert nwGUI.theProject.bookTitle == ""
@@ -62,7 +61,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
assert nwGUI.theProject.trashRoot is None
assert nwGUI.theProject.projPath == nwTempGUI
assert nwGUI.theProject.projMeta == path.join(nwTempGUI,"meta")
- assert nwGUI.theProject.projCache == path.join(nwTempGUI,"cache")
assert nwGUI.theProject.projFile == "nwProject.nwx"
assert nwGUI.theProject.projName == ""
assert nwGUI.theProject.bookTitle == ""
From c28a246d238402c9196013a2254ae2eb227526bd Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Feb 2020 19:16:29 +0100
Subject: [PATCH 27/60] Make document saving consistent with how project file
is saved
---
nw/project/document.py | 28 +++++++++++++---------------
1 file changed, 13 insertions(+), 15 deletions(-)
diff --git a/nw/project/document.py b/nw/project/document.py
index e2a46d65..9339a4cd 100644
--- a/nw/project/document.py
+++ b/nw/project/document.py
@@ -100,25 +100,23 @@ class NWDoc():
mkdir(dataPath)
logger.debug("Created folder %s" % dataPath)
- docTemp = path.join(dataPath,docFile[:-3]+"tmp")
- docBack = path.join(dataPath,docFile[:-3]+"bak")
-
- if path.isfile(docTemp):
- unlink(docTemp)
- if path.isfile(docBack):
- rename(docBack,docTemp)
- if path.isfile(docPath):
- rename(docPath,docBack)
+ docTemp = path.join(dataPath, docFile+"~")
+ docBack = path.join(dataPath, docFile[:-3]+"bak")
try:
- with open(docPath,mode="w",encoding="utf8") as outFile:
+ with open(docTemp,mode="w",encoding="utf8") as outFile:
outFile.write(docText)
except Exception as e:
self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR)
return False
- if path.isfile(docTemp):
- unlink(docTemp)
+ # If we're here, the file was successfully saved,
+ # so let's sort out the temps and backups
+ if path.isfile(docBack):
+ unlink(docBack)
+ if path.isfile(docPath):
+ rename(docPath, docBack)
+ rename(docTemp, docPath)
self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName)
@@ -132,8 +130,8 @@ class NWDoc():
dataPath = path.join(self.theProject.projPath, docDir)
chkList = []
chkList.append(path.join(dataPath, docFile))
- chkList.append(path.join(dataPath,docFile[:-3]+"tmp"))
- chkList.append(path.join(dataPath,docFile[:-3]+"bak"))
+ chkList.append(path.join(dataPath, docFile+"~"))
+ chkList.append(path.join(dataPath, docFile[:-3]+"bak"))
for chkFile in chkList:
if path.isfile(chkFile):
try:
@@ -147,7 +145,7 @@ class NWDoc():
@staticmethod
def assemblePath(tHandle, docExt):
if tHandle is None:
- return None
+ return None, None
docDir = "data_"+tHandle[0]
docFile = tHandle[1:13]+"_"+docExt
return docDir, docFile
From 760aee0c8f5d1be3a8063522f1decf9e390df2a9 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Feb 2020 21:12:28 +0100
Subject: [PATCH 28/60] Added a function to clean out old project files no
longer needed
---
nw/project/project.py | 6 ++++++
nw/tools/__init__.py | 2 ++
nw/tools/legacy.py | 47 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 55 insertions(+)
create mode 100644 nw/tools/legacy.py
diff --git a/nw/project/project.py b/nw/project/project.py
index 60cdfa72..03aacf42 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -22,6 +22,7 @@ from time import time
from nw.project.status import NWStatus
from nw.project.item import NWItem
+from nw.tools import projectMaintenance
from nw.common import checkString, checkBool, checkInt
from nw.constants import (
nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert
@@ -200,6 +201,11 @@ class NWProject():
if not self._checkFolder(self.projMeta):
return
+ try:
+ projectMaintenance(self)
+ except Exception as E:
+ logger.error(str(E))
+
try:
nwXML = etree.parse(fileName)
except Exception as e:
diff --git a/nw/tools/__init__.py b/nw/tools/__init__.py
index 582d18fb..71f6b141 100644
--- a/nw/tools/__init__.py
+++ b/nw/tools/__init__.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from nw.tools.analyse import TextAnalysis
+from nw.tools.legacy import projectMaintenance
from nw.tools.optlaststate import OptLastState
from nw.tools.spellcheck import NWSpellCheck
from nw.tools.spellenchant import NWSpellEnchant
@@ -10,6 +11,7 @@ from nw.tools.wordcount import countWords
__all__ = [
"TextAnalysis",
+ "projectMaintenance",
"OptLastState",
"NWSpellCheck",
"NWSpellEnchant",
diff --git a/nw/tools/legacy.py b/nw/tools/legacy.py
new file mode 100644
index 00000000..7b622cb6
--- /dev/null
+++ b/nw/tools/legacy.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Legacy Tools
+
+ novelWriter – Legacy Tools
+============================
+ Various functions to handle old projects
+
+ File History:
+ Created: 2020-02-13 [0.4.3]
+
+"""
+
+import logging
+import nw
+
+from os import path, unlink, rmdir
+
+logger = logging.getLogger(__name__)
+
+def projectMaintenance(theProject):
+ """Wrapper class for handling various tasks related to managing old
+ projects with content from older versions of novelWriter.
+ """
+
+ # Remove no longer used project cache folder
+ if path.isdir(theProject.projPath):
+ cacheDir = path.join(theProject.projPath, "cache")
+ if path.isdir(cacheDir):
+ logger.info("Deprecated cache folder found")
+ rmList = []
+ for i in range(10):
+ rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i))
+ rmList.append(path.join(cacheDir, "projCount.txt"))
+ for rmFile in rmList:
+ if path.isfile(rmFile):
+ logger.info("Deleting: %s" % rmFile)
+ try:
+ unlink(rmFile)
+ except Exception as e:
+ logger.error(str(e))
+ logger.info("Deleting: %s" % cacheDir)
+ try:
+ rmdir(cacheDir)
+ except Exception as e:
+ logger.error(str(e))
+
+ return
From beb656267cec976402295d2eed0dd44d645c3908 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Feb 2020 21:22:36 +0100
Subject: [PATCH 29/60] If opening a project file fails, try to open the backup
file instead
---
nw/project/project.py | 21 +++++++++++++++++++--
1 file changed, 19 insertions(+), 2 deletions(-)
diff --git a/nw/project/project.py b/nw/project/project.py
index 03aacf42..c564a1f6 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -210,8 +210,20 @@ class NWProject():
nwXML = etree.parse(fileName)
except Exception as e:
self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR)
- self.clearProject()
- return False
+
+ # Trying to open backup file instead
+ backFile = fileName[:-3]+"bak"
+ if path.isfile(backFile):
+ self.makeAlert("Attempting to open backup project file instead.", nwAlert.INFO)
+ try:
+ nwXML = etree.parse(backFile)
+ except Exception as e:
+ self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR)
+ self.clearProject()
+ return False
+ else:
+ self.clearProject()
+ return False
xRoot = nwXML.getroot()
nwxRoot = xRoot.tag
@@ -295,6 +307,11 @@ class NWProject():
return True
def saveProject(self):
+ """Save the project main XML file. The saving command itself
+ uses a temporary filename, and the file is renamed afterwards to
+ make sure if the save fails, we're not left with a truncated
+ file.
+ """
if self.projPath is None:
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)
From 0e045eeed6ffd5beef82823581b3cf632bd6529b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Feb 2020 22:54:40 +0100
Subject: [PATCH 30/60] Make sure we have absoulte path when opening from
command line input that may be relative path
---
nw/project/project.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/nw/project/project.py b/nw/project/project.py
index c564a1f6..26e385f7 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -192,7 +192,7 @@ class NWProject():
return False
self.clearProject()
- self.projPath = path.dirname(fileName)
+ self.projPath = path.abspath(path.dirname(fileName))
logger.debug("Opening project: %s" % self.projPath)
self.projMeta = path.join(self.projPath,"meta")
From ca18fe5461e18e81d8352b531b0ed7387223ce3f Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 15 Feb 2020 17:59:40 +0100
Subject: [PATCH 31/60] Added some fixes for bugs discovered when testing with
PySide2
---
nw/gui/elements/docdetails.py | 5 -----
nw/gui/elements/searchbar.py | 4 ++--
nw/gui/mainmenu.py | 4 ++--
nw/guimain.py | 19 +++++++------------
pytest.ini | 1 +
5 files changed, 12 insertions(+), 21 deletions(-)
diff --git a/nw/gui/elements/docdetails.py b/nw/gui/elements/docdetails.py
index 78fbf386..00b3aa89 100644
--- a/nw/gui/elements/docdetails.py
+++ b/nw/gui/elements/docdetails.py
@@ -23,11 +23,6 @@ logger = logging.getLogger(__name__)
class GuiDocDetails(QFrame):
- C_NAME = 0
- C_COUNT = 1
- C_FLAGS = 2
- C_HANDLE = 3
-
def __init__(self, theParent, theProject):
QFrame.__init__(self, theParent)
diff --git a/nw/gui/elements/searchbar.py b/nw/gui/elements/searchbar.py
index cb40878e..21be1cfb 100644
--- a/nw/gui/elements/searchbar.py
+++ b/nw/gui/elements/searchbar.py
@@ -86,13 +86,13 @@ class GuiSearchBar(QFrame):
if not self.isVisible():
self.setVisible(True)
self.searchBox.setText(theText)
- self.searchBox.setFocus(True)
+ self.searchBox.setFocus()
logger.verbose("Setting search text to '%s'" % theText)
return True
def setReplaceText(self, theText):
self._replaceVisible(True)
- self.replaceBox.setFocus(True)
+ self.replaceBox.setFocus()
self.replaceBox.setText(theText)
return True
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index b515576a..14ff2279 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -78,7 +78,7 @@ class GuiMainMenu(QMenuBar):
if recentProject == "": continue
menuItem = QAction("%s" % recentProject, self.projMenu)
menuItem.triggered.connect(
- lambda menuItem, n=n : self.openRecentProject(menuItem, n)
+ lambda a1=menuItem, a2=n : self.openRecentProject(a1, a2)
)
self.recentMenu.addAction(menuItem)
@@ -255,7 +255,7 @@ class GuiMainMenu(QMenuBar):
self.rootItems[itemClass].triggered.connect(
lambda nCount, itemClass=itemClass : self._newTreeItem(nwItemType.ROOT, itemClass)
)
- self.rootMenu.addActions(self.rootItems.values())
+ self.rootMenu.addAction(self.rootItems[itemClass])
# Project > New Folder
self.aCreateFolder = QAction("Create Folder", self)
diff --git a/nw/guimain.py b/nw/guimain.py
index b1fdf031..670b1534 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -17,7 +17,7 @@ import nw
from os import path
from PyQt5.QtCore import Qt, QTimer
-from PyQt5.QtGui import QIcon, QPixmap, QColor
+from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence
from PyQt5.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QShortcut,
QMessageBox, QProgressDialog, QDialog
@@ -152,17 +152,12 @@ class GuiMain(QMainWindow):
# Shortcuts and Actions
self._connectMenuActions()
- QShortcut(
- Qt.Key_Return,
- self.treeView,
- context=Qt.WidgetShortcut,
- activated=self._treeKeyPressReturn
- )
- QShortcut(
- Qt.Key_Escape,
- self,
- activated=self._keyPressEscape
- )
+ keyReturn = QShortcut(self.treeView)
+ keyReturn.setKey(QKeySequence(Qt.Key_Return))
+ keyReturn.activated.connect(self._treeKeyPressReturn)
+ keyEscape = QShortcut(self)
+ keyEscape.setKey(QKeySequence(Qt.Key_Escape))
+ keyEscape.activated.connect(self._keyPressEscape)
# Forward Functions
self.setStatus = self.statusBar.setStatus
diff --git a/pytest.ini b/pytest.ini
index 17f2ac93..f266d081 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -4,3 +4,4 @@ markers =
core: Core functionality tests
gui: Qt5 GUI tests
serial
+qt_api = pyqt5
From aab9805bc677af10bba6630589f1b12d426cc8dd Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 15 Feb 2020 21:39:21 +0100
Subject: [PATCH 32/60] Remove dependency on appdirs package for finding config
dir as this is provided by Qt
---
README.md | 2 --
docs/source/started.txt | 1 -
novelWriter.py | 6 ------
nw/config.py | 6 +++---
nw/constants/constants.py | 4 ----
requirements.txt | 1 -
setup.py | 1 -
7 files changed, 3 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 0325cb84..b16c3a91 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,6 @@ For the apt package manager on Debian systems, the following Python3 packages ar
* `python3-pyqt5` for the GUI
* `python3-pyqt5.qtsvg` may need to be installed separately
-* `python3-appdirs` for locating the system's config folder
* `python3-lxml` for writing project files
These are optional, but recommended:
@@ -88,7 +87,6 @@ in the application folder.
You can also do them one at a time, skipping the ones you don't need:
```
python3 -m pip install pyqt5
-python3 -m pip install appdirs
python3 -m pip install lxml
python3 -m pip install pyenchant
python3 -m pip install latexcodec
diff --git a/docs/source/started.txt b/docs/source/started.txt
index 747a8c27..aa6977a8 100644
--- a/docs/source/started.txt
+++ b/docs/source/started.txt
@@ -26,7 +26,6 @@ On some operating systems you need to use ``python3`` instead of ``python``.
The following Python packages are required to run novelWriter:
* ``pyqt5`` for the GUI
-* ``appdirs`` for locating the system's config folder
* ``lxml`` for writing project files
.. note::
diff --git a/novelWriter.py b/novelWriter.py
index d7c3d314..3aa88038 100755
--- a/novelWriter.py
+++ b/novelWriter.py
@@ -27,12 +27,6 @@ except:
print("ERROR: Failed to load dependency python3-lxml")
sys.exit(1)
-try:
- import appdirs
-except:
- print("ERROR: Failed to load dependency python3-appdirs")
- sys.exit(1)
-
if __name__ == "__main__":
import nw
nw.main(sys.argv[1:])
diff --git a/nw/config.py b/nw/config.py
index 3aa6a254..9aa7e6d8 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -16,11 +16,10 @@ import sys
import nw
from os import path, mkdir, makedirs
-from appdirs import user_config_dir
from datetime import datetime
from PyQt5.Qt import PYQT_VERSION_STR
-from PyQt5.QtCore import QT_VERSION_STR
+from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths
from nw.constants import nwFiles, nwUnicode
from nw.common import splitVersionNumber
@@ -167,7 +166,8 @@ class Config:
def initConfig(self, confPath=None):
if confPath is None:
- self.confPath = user_config_dir(self.appHandle)
+ confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
+ self.confPath = path.join(confRoot, self.appHandle)
else:
logger.info("Setting config from alternative path: %s" % confPath)
self.confPath = confPath
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index f85f6558..78e0c7b3 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -116,10 +116,6 @@ class nwDependencies():
"site" : "",
"docs" : "",
},
- "appdirs" : {
- "site" : "",
- "docs" : "",
- },
"lxml" : {
"site" : "",
"docs" : "",
diff --git a/requirements.txt b/requirements.txt
index b1d28992..648be38e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,4 @@
pyqt5
-appdirs
lxml
pyenchant
latexcodec
diff --git a/setup.py b/setup.py
index 75f2fbd7..081befd5 100755
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,6 @@ setuptools.setup(
python_requires = ">=3.5",
install_requires = [
"pyqt5",
- "appdirs",
"lxml",
"pyenchant",
"latexcodec",
From c991795f0d9193d2a9431619e7534a6e69aa6237 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 15 Feb 2020 21:57:05 +0100
Subject: [PATCH 33/60] Some additional tweaks for Windows
---
nw/config.py | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index 9aa7e6d8..a932553e 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -167,7 +167,7 @@ class Config:
if confPath is None:
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
- self.confPath = path.join(confRoot, self.appHandle)
+ self.confPath = path.join(path.abspath(confRoot), self.appHandle)
else:
logger.info("Setting config from alternative path: %s" % confPath)
self.confPath = confPath
@@ -186,12 +186,13 @@ class Config:
# If config folder does not exist, make it.
# This assumes that the os config folder itself exists.
- if self.osWindows:
- if not path.isdir(self.confPath):
- makedirs(self.confPath)
- else:
- if not path.isdir(self.confPath):
+ if not path.isdir(self.confPath):
+ try:
mkdir(self.confPath)
+ except Exception as e:
+ logger.error("Could not create folder: %s" % self.confPath)
+ logger.error(str(e))
+ return False
# Check if config file exists
if path.isfile(path.join(self.confPath,self.confFile)):
From 110d27d06781641c9c924a94f74638c871b73847 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 17 Feb 2020 13:30:26 +0100
Subject: [PATCH 34/60] Cleanup of imports
---
nw/config.py | 2 +-
nw/gui/dialogs/itemeditor.py | 2 --
nw/gui/dialogs/projecteditor.py | 2 --
nw/gui/dialogs/timelineview.py | 2 --
nw/gui/elements/doceditor.py | 4 ++--
nw/gui/elements/viewdetails.py | 2 --
nw/project/item.py | 3 ---
nw/project/project.py | 1 -
8 files changed, 3 insertions(+), 15 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index a932553e..eb0720a4 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -15,7 +15,7 @@ import configparser
import sys
import nw
-from os import path, mkdir, makedirs
+from os import path, mkdir
from datetime import datetime
from PyQt5.Qt import PYQT_VERSION_STR
diff --git a/nw/gui/dialogs/itemeditor.py b/nw/gui/dialogs/itemeditor.py
index 0dc219ff..5383aefa 100644
--- a/nw/gui/dialogs/itemeditor.py
+++ b/nw/gui/dialogs/itemeditor.py
@@ -13,8 +13,6 @@
import logging
import nw
-from os import path
-
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QFormLayout, QLineEdit,
diff --git a/nw/gui/dialogs/projecteditor.py b/nw/gui/dialogs/projecteditor.py
index 6a09fe7d..3b8c0114 100644
--- a/nw/gui/dialogs/projecteditor.py
+++ b/nw/gui/dialogs/projecteditor.py
@@ -13,8 +13,6 @@
import logging
import nw
-from os import path
-
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
from PyQt5.QtWidgets import (
diff --git a/nw/gui/dialogs/timelineview.py b/nw/gui/dialogs/timelineview.py
index dab545fc..5046d112 100644
--- a/nw/gui/dialogs/timelineview.py
+++ b/nw/gui/dialogs/timelineview.py
@@ -13,8 +13,6 @@
import logging
import nw
-from os import path
-
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QPixmap
from PyQt5.QtWidgets import (
diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py
index 28a63dce..d277ac75 100644
--- a/nw/gui/elements/doceditor.py
+++ b/nw/gui/elements/doceditor.py
@@ -26,8 +26,8 @@ from PyQt5.QtGui import (
from nw.project import NWDoc
from nw.gui.tools import GuiDocHighlighter, WordCounter
-from nw.tools import NWSpellCheck, NWSpellSimple
-from nw.constants import nwFiles, nwUnicode, nwDocAction, nwAlert
+from nw.tools import NWSpellSimple
+from nw.constants import nwUnicode, nwDocAction
logger = logging.getLogger(__name__)
diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/elements/viewdetails.py
index 2a0c9a29..929e8026 100644
--- a/nw/gui/elements/viewdetails.py
+++ b/nw/gui/elements/viewdetails.py
@@ -18,8 +18,6 @@ from PyQt5.QtWidgets import (
QWidget, QLabel, QScrollArea, QFrame, QToolButton, QCheckBox, QGridLayout
)
-from nw.constants import nwLabels
-
logger = logging.getLogger(__name__)
class GuiDocViewDetails(QWidget):
diff --git a/nw/project/item.py b/nw/project/item.py
index 5eb98f4b..7ed0e6a3 100644
--- a/nw/project/item.py
+++ b/nw/project/item.py
@@ -14,7 +14,6 @@ import logging
import nw
from lxml import etree
-from datetime import datetime
from nw.common import checkInt
from nw.constants import nwItemType, nwItemClass, nwItemLayout
@@ -23,8 +22,6 @@ logger = logging.getLogger(__name__)
class NWItem():
- MAX_DEPTH = 8
-
def __init__(self, theProject):
self.theProject = theProject
diff --git a/nw/project/project.py b/nw/project/project.py
index 26e385f7..bc934b21 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -14,7 +14,6 @@ import logging
import nw
from os import path, mkdir, listdir, unlink, rename
-from shutil import copyfile
from lxml import etree
from hashlib import sha256
from datetime import datetime
From 7c1f6130145817cdbfca27e809504acba40c00bd Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 17 Feb 2020 13:43:05 +0100
Subject: [PATCH 35/60] Bumped version and removed -t command line argument
---
nw/__init__.py | 17 ++++++-----------
setup.py | 4 ++--
2 files changed, 8 insertions(+), 13 deletions(-)
diff --git a/nw/__init__.py b/nw/__init__.py
index 02b62656..e5da6b5c 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -25,8 +25,8 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 2018–2019, Veronica Berglyd Olsen"
__license__ = "GPLv3"
-__version__ = "0.4.3"
-__date__ = "2019-11-24"
+__version__ = "0.4.4"
+__date__ = "2020-02-17"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__status__ = "Development"
@@ -77,14 +77,13 @@ def main(sysArgs=None):
sysArgs = sys.argv[1:]
# Valid Input Options
- shortOpt = "hdiqtl:v"
+ shortOpt = "hdiql:v"
longOpt = [
"help",
"debug",
"info",
"verbose",
"quiet",
- "time",
"logfile=",
"version",
"config=",
@@ -103,7 +102,6 @@ def main(sysArgs=None):
" -d, --debug Print debug output.\n"
" --verbose Increase verbosity of debug output.\n"
" -q, --quiet Disable output to command line. Does not affect log file.\n"
- " -t, --time Shows time stamp in logging output.\n"
" -l, --logfile= Specify log file.\n"
" --style= Set Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n"
@@ -118,11 +116,9 @@ def main(sysArgs=None):
# Defaults
debugLevel = logging.WARN
debugStr = "{levelname:8} {message:}"
- timeStr = "[{asctime:}] "
logFile = ""
toFile = False
toStd = True
- showTime = False
confPath = None
testMode = False
qtStyle = "Fusion"
@@ -149,7 +145,7 @@ def main(sysArgs=None):
debugLevel = logging.INFO
elif inOpt in ("-d", "--debug"):
debugLevel = logging.DEBUG
- debugStr = "{name:>30}:{lineno:<4d} {levelname:8} {message:}"
+ debugStr = "[{asctime:}] {name:>30}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt in ("-l","--logfile"):
logFile = inArg
toFile = True
@@ -157,8 +153,6 @@ def main(sysArgs=None):
toStd = False
elif inOpt in ("--verbose"):
debugLevel = VERBOSE
- elif inOpt in ("-t","--time"):
- showTime = True
elif inOpt in ("--style"):
qtStyle = inArg
elif inOpt in ("--config"):
@@ -172,7 +166,6 @@ def main(sysArgs=None):
CONFIG.cmdOpen = cmdOpen
# Set Logging
- if showTime: debugStr = timeStr+debugStr
logFmt = logging.Formatter(fmt=debugStr,datefmt="%Y-%m-%d %H:%M:%S",style="{")
if not logFile == "" and toFile:
@@ -201,6 +194,8 @@ def main(sysArgs=None):
return nwGUI
else:
nwApp = QApplication([__package__,("-style=%s" % qtStyle)])
+ nwApp.setApplicationName(__package__)
+ nwApp.setApplicationVersion(__version__)
nwGUI = GuiMain()
sys.exit(nwApp.exec_())
diff --git a/setup.py b/setup.py
index 081befd5..7d2436dd 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
setuptools.setup(
name = "novelWriter",
- version = "0.4.3",
+ version = "0.4.4",
author = "Veronica Berglyd Olsen",
author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels",
@@ -35,7 +35,7 @@ setuptools.setup(
"Natural Language :: English",
"Topic :: Text Editors",
],
- python_requires = ">=3.5",
+ python_requires = ">=3.6",
install_requires = [
"pyqt5",
"lxml",
From e325aa4bb045de02f847de7dac808ea3f46e4bd9 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 17 Feb 2020 13:48:23 +0100
Subject: [PATCH 36/60] A few more places to bump stuff
---
docs/source/conf.py | 6 +++---
nw/__init__.py | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 62a72b99..35673d92 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -20,13 +20,13 @@
# -- Project information -----------------------------------------------------
project = "novelWriter"
-copyright = "2018-2019, Veronica Berglyd Olsen"
+copyright = "2018-2020, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen"
# The short X.Y version
-version = "0.4.3"
+version = "0.4.4"
# The full version, including alpha/beta/rc tags
-release = "0.4.3"
+release = "0.4.4"
# -- General configuration ---------------------------------------------------
diff --git a/nw/__init__.py b/nw/__init__.py
index e5da6b5c..87f77974 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -23,7 +23,7 @@ from nw.config import Config
__package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
-__copyright__ = "Copyright 2018–2019, Veronica Berglyd Olsen"
+__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
__license__ = "GPLv3"
__version__ = "0.4.4"
__date__ = "2020-02-17"
From 2094d67c38090f2096876cc5959f6136c784dcd9 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 17 Feb 2020 14:08:45 +0100
Subject: [PATCH 37/60] Updated changelog
---
CHANGELOG.md | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98e21175..601bd6b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
# novelWriter ChangeLog
+## Version 0.4.4 [2020-02-17]
+
+**Features**
+
+* A project can now be opened from the command line by providing the project path to the launching script. PRs #164 and #166.
+
+**User Interface**
+
+* Added functionality to split a document into a folder of multiple documents, and also to merge a folder of documents into a single document. PRs #159 and #163.
+* It is now possible to permanently delete files from the Trash folder. This can be done file-by-file or by using the Empty Trash option in the menu. PRs #159 and #163.
+* When running the spell checker, a wait cursor is displayed. This will alert the user that novelWriter is working on something when, for instance, a very large document is opened and initial spell checking is running. PR #158.
+
+**Bug Fixes**
+
+* Fixed a few keyboard shortcuts that were not working in distraction free mode. PR #157.
+* Added a check to ensure the user does not drag and drop an item into the Orphaned Items folder. Since this folder is not an actual project item, novelWriter would crash when trying to change the dropped item's parent item to the Orphaned Items folder. Now, instead, the drop event is cancelled if the target folder is Orphaned Items. PR #163.
+
+**Code Improvements**
+
+* The way project files are saved has been altered slightly. When a project file or document file is saved, the data is first streamed to a temp file. Then the old storage file is renamed to .bak, and and the temp file is renamed to the correct storage file name. This ensures that the storage file is only replaced after a complete and successful write. PR #165.
+* The cache folder has been removed. It was used to store the 10 most recent versions of the project file. Instead, the previous project file is renamed to .bak, and can be restored if opening from the latest project file fails. Any additional restore capabilities should be ensured by backup solutions, either the internal simple zip backup, or other third party tools. PR #165.
+* The dependency on the Python package `appdirs` has been dropped. It was used only for extracting the path to the user's config folder, a feature which is also provided by Qt. PR #169.
+
## Version 0.4.3 [2019-11-24]
**User Interface**
From ef377f5952f576b018028fe58db46c5830f3134f Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 17 Feb 2020 14:50:43 +0100
Subject: [PATCH 38/60] Bumped version again to fix botched release
---
CHANGELOG.md | 6 +++++-
docs/source/conf.py | 4 ++--
nw/__init__.py | 2 +-
setup.py | 2 +-
4 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 601bd6b6..4ae258ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# novelWriter ChangeLog
-## Version 0.4.4 [2020-02-17]
+## Version 0.4.5 [2020-02-17]
**Features**
@@ -23,6 +23,10 @@
* The cache folder has been removed. It was used to store the 10 most recent versions of the project file. Instead, the previous project file is renamed to .bak, and can be restored if opening from the latest project file fails. Any additional restore capabilities should be ensured by backup solutions, either the internal simple zip backup, or other third party tools. PR #165.
* The dependency on the Python package `appdirs` has been dropped. It was used only for extracting the path to the user's config folder, a feature which is also provided by Qt. PR #169.
+## Version 0.4.4 [2020-02-17]
+
+* Botched release. Replaced with 0.4.5
+
## Version 0.4.3 [2019-11-24]
**User Interface**
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 35673d92..51540235 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen"
# The short X.Y version
-version = "0.4.4"
+version = "0.4.5"
# The full version, including alpha/beta/rc tags
-release = "0.4.4"
+release = "0.4.5"
# -- General configuration ---------------------------------------------------
diff --git a/nw/__init__.py b/nw/__init__.py
index 87f77974..d562d446 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -25,7 +25,7 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
__license__ = "GPLv3"
-__version__ = "0.4.4"
+__version__ = "0.4.5"
__date__ = "2020-02-17"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
diff --git a/setup.py b/setup.py
index 7d2436dd..91393346 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
setuptools.setup(
name = "novelWriter",
- version = "0.4.4",
+ version = "0.4.5",
author = "Veronica Berglyd Olsen",
author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels",
From 08abdeabb079b26a959b13e3c049c9d658725de5 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 17 Feb 2020 21:51:10 +0100
Subject: [PATCH 39/60] Minor fix in README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index b16c3a91..d0e772cf 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ These are optional, but recommended:
* `python3-enchant` for better spell checking
* `python3-latexcodec` for escaping unicode characters in LaTeX export
-* `python3-pandoc` for additional exports to Word, Open Office, eBooks, etc.
+* `python3-pypandoc` for additional exports to Word, Open Office, eBooks, etc.
Alternatively, the packages can be installed with `pip` by running
```
From 4f0f36aa95df8b168b8dd20c1133577c9b95f6d9 Mon Sep 17 00:00:00 2001
From: Count Jocular
Date: Tue, 18 Feb 2020 09:23:22 +0000
Subject: [PATCH 40/60] Update started.txt with correct download links
Update Getting Started to point to the correct source archives (.zip and .tar.gz) for 0.4.5 release.
---
docs/source/started.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/source/started.txt b/docs/source/started.txt
index aa6977a8..93e75940 100644
--- a/docs/source/started.txt
+++ b/docs/source/started.txt
@@ -6,8 +6,8 @@ You can download novelWriter from https://github.com/vkbo/novelWriter/releases
Latest version is |version|:
-* ZIP file: https://github.com/vkbo/novelWriter/archive/v0.3.2.zip
-* TAR file: https://github.com/vkbo/novelWriter/archive/v0.3.2.tar.gz
+* ZIP file: https://github.com/vkbo/novelWriter/archive/v0.4.5.zip
+* TAR file: https://github.com/vkbo/novelWriter/archive/v0.4.5.tar.gz
Extract the archive to a location of your choice.
From 4eaaf97af200a3f26b650eb02b6a7d6f9f925b44 Mon Sep 17 00:00:00 2001
From: Count Jocular
Date: Tue, 18 Feb 2020 09:53:22 +0000
Subject: [PATCH 41/60] Update install.py with correct paths
Update install.py with correct joined paths for the `themes` folder and the `graphics` folder. This bug prevented pyInstaller from building successfully under Windows.
---
install.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/install.py b/install.py
index 14356f23..2d05835d 100755
--- a/install.py
+++ b/install.py
@@ -63,8 +63,8 @@ else:
instOpt = [
"--name=novelWriter",
"--onefile",
- "--add-data=%s%s%s" % (os.path.join("nw", "themes"), dotDot,"themes"),
- "--add-data=%s%s%s" % (os.path.join("nw", "graphics"),dotDot,"graphics"),
+ "--add-data=%s%s%s" % (os.path.join("nw", "assets", "themes"), dotDot,"themes"),
+ "--add-data=%s%s%s" % (os.path.join("nw", "assets", "graphics"), dotDot,"graphics"),
"--icon=%s" % os.path.join("nw", "assets", "icons", "novelWriter.ico"),
]
if buildWindowed:
From 64b02947c09425a997e83b3445fb35795b0fb02a Mon Sep 17 00:00:00 2001
From: Count Jocular
Date: Tue, 18 Feb 2020 11:07:21 +0000
Subject: [PATCH 42/60] Correct spelling mistake in interface help
s/renderred/rendered/g
---
docs/source/interface.txt | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/source/interface.txt b/docs/source/interface.txt
index 3703bf22..994f5571 100644
--- a/docs/source/interface.txt
+++ b/docs/source/interface.txt
@@ -37,9 +37,9 @@ The editor also has a minimal set of keywords used for setting tags and referenc
"``## Title``", "Heading level two. The space after the # is mandatory."
"``### Title``", "Heading level three. The space after the # is mandatory."
"``#### Title``", "Heading level four. The space after the # is mandatory."
- "``**text**``", "The text is renderred as bold text."
- "``_text_``", "The text is renderred as italics text."
- "``__text__``", "The text is renderred as underlined text."
+ "``**text**``", "The text is rendered as bold text."
+ "``_text_``", "The text is rendered as italicized text."
+ "``__text__``", "The text is rendered as underlined text."
"``% text...``", "A comment. The text is not exported, seen in viewer, or counted towards word counts."
"``@keyword: value``", "A keyword argument followed by a value, or a comma separated list of values."
From 6d69370ce111b5ad6021be18549cc5b14e0e5a01 Mon Sep 17 00:00:00 2001
From: Count Jocular
Date: Tue, 18 Feb 2020 11:14:00 +0000
Subject: [PATCH 43/60] Fix typos in projects help
Just a couple of typos.
---
docs/source/projects.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/source/projects.txt b/docs/source/projects.txt
index be2a75ae..2ece4f4b 100644
--- a/docs/source/projects.txt
+++ b/docs/source/projects.txt
@@ -22,7 +22,7 @@ These other root folder types are intended for your notes on the various element
Using these is of course entirely optional.
A new project will not have all of the root folders present, but you can add the ones you want from :menuselection:`Project --> Create Root Folder`.
-The root folders are intended for the follwing use, but aside from the Novel folder, not restrictions apply.
+The root folders are intended for the following use, but aside from the Novel folder, no restrictions apply.
* **Novel:** The root folder of all text that goes into the final novel.
This class of files have other rules and features than other files in the project.
@@ -44,7 +44,7 @@ Orphaned Documents
------------------
In the event the editor crashes or otherwise exits without saving the project state, files that have been added to the project tree and are saved to disk will appear in a special "Orphaned Items" root folder next time the application is started.
-These orphaned files will not have any meta data associated with them, so the label and other information has to be set again, and the files moved back to the correct location in the project.
+These orphaned files will not have any meta data associated with them, so the label and other information will have to be set again, and the files moved back to the correct location in the project.
Using Folders in the Project Tree
---------------------------------
From a1f53980032106914c16c587238dee98d718c6b7 Mon Sep 17 00:00:00 2001
From: Count Jocular
Date: Tue, 18 Feb 2020 11:27:39 +0000
Subject: [PATCH 44/60] Fix typo in technical help
Fix a 1-character typo
---
docs/source/technical.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/source/technical.txt b/docs/source/technical.txt
index 9c81d7c9..afee5551 100644
--- a/docs/source/technical.txt
+++ b/docs/source/technical.txt
@@ -28,7 +28,7 @@ The project XML file is suitable for diff tools and version control, although a
Project Documents
-----------------
-The project documents are saved in folders staring with ``data_``.
+The project documents are saved in folders starting with ``data_``.
Each document has a file handle taken from the first 13 characters of a SHA256 hash of the system time when the file was first created.
The documents are saved with a folder and filename derived from this hash.
If you wish to find the physical location of a file in the project, you can either look it up in the project XML file, or select :menuselection:`Document --> Show File Details` in the menu when having the document open.
From ecb739425bd2adf40765a4ea842668dac81db8d4 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 19 Feb 2020 18:58:19 +0100
Subject: [PATCH 45/60] Added new class to save dialog optins
---
nw/constants/constants.py | 1 +
nw/tools/optlaststate.py | 119 ++++++++++++++++++++++++++++++++++++--
2 files changed, 116 insertions(+), 4 deletions(-)
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index 78e0c7b3..80887188 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -25,6 +25,7 @@ class nwFiles():
PROJ_DICT = "wordlist.txt"
SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json"
+ OPTS_FILE = "guiOptions.json"
EXPORT_OPT = "exportOptions.json"
TLINE_OPT = "timelineOptions.json"
SLOG_OPT = "sessionLogOptions.json"
diff --git a/nw/tools/optlaststate.py b/nw/tools/optlaststate.py
index 0df6ff68..44c6a402 100644
--- a/nw/tools/optlaststate.py
+++ b/nw/tools/optlaststate.py
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
-"""novelWriter Options Last State
+"""novelWriter Options State
- novelWriter – Options Last State
-==================================
+ novelWriter – Options State
+=============================
Class holding the last state of GUI options
File History:
- Created: 2019-10-21 [0.3.1]
+ Created: 2019-10-21 [0.3.1] - Original version meant to be sub classed
+ Created: 2020-02-19 [0.4.5] - Rewritten from superclass to single file tool
"""
@@ -17,9 +18,119 @@ import nw
from os import path
from nw.common import checkString, checkBool, checkInt
+from nw.constants import nwFiles
logger = logging.getLogger(__name__)
+class OptionState():
+
+ def __init__(self, theProject):
+
+ self.theProject = theProject
+ self.theState = {}
+ self.stringOpt = ()
+ self.boolOpt = ()
+ self.intOpt = ()
+
+ return
+
+ def loadSettings(self):
+ """Load the options dictionary from the project settings file.
+ """
+
+ stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
+ theState = {}
+
+ if path.isfile(stateFile):
+ logger.debug("Loading GUI options file")
+ try:
+ with open(stateFile,mode="r",encoding="utf8") as inFile:
+ theJson = inFile.read()
+ theState = json.loads(theJson)
+ except Exception as e:
+ logger.error("Failed to load GUI options file")
+ logger.error(str(e))
+ return False
+ for anOpt in theState:
+ self.theState[anOpt] = theState[anOpt]
+
+ return True
+
+ def saveSettings(self):
+ """Save the options dictionary to the project settings file.
+ """
+
+ stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
+ logger.debug("Saving GUI options file")
+
+ try:
+ with open(stateFile,mode="w+",encoding="utf8") as outFile:
+ outFile.write(json.dumps(self.theState, indent=2))
+ except Exception as e:
+ logger.error("Failed to save GUI options file")
+ logger.error(str(e))
+ return False
+
+ return True
+
+ def setValue(self, setGroup, setName, setValue):
+ """Saves a value, with a given group and name.
+ """
+ if not setGroup in self.theState:
+ self.theState[setGroup] = {}
+ self.theState[setGroup][setName] = setValue
+ return True
+
+ def getString(self, getGroup, getName, defaultValue):
+ """Return the value as a string, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return str(self.theState[getGroup][getName])
+ except:
+ return defaultValue
+ return defaultValue
+
+ def getInt(self, getGroup, getName, defaultValue):
+ """Return the value as an int, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return int(self.theState[getGroup][getName])
+ except:
+ return defaultValue
+ return defaultValue
+
+ def getFloat(self, getGroup, getName, defaultValue):
+ """Return the value as a float, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return float(self.theState[getGroup][getName])
+ except:
+ return defaultValue
+ return defaultValue
+
+ def getBool(self, getGroup, getName, defaultValue):
+ """Return the value as a bool, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return bool(self.theState[getGroup][getName])
+ except:
+ return defaultValue
+ return defaultValue
+
+# END Class OptionState
+
class OptLastState():
def __init__(self, theProject, theFile):
From 5f0b3316553c4597035542e4037c16d9fc7aa506 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 19 Feb 2020 19:43:44 +0100
Subject: [PATCH 46/60] Replace old option state class with new one everywhere
---
nw/gui/dialogs/export.py | 129 +++++++++----------
nw/gui/dialogs/sessionlog.py | 58 +++------
nw/gui/dialogs/timelineview.py | 77 +++++------
nw/project/project.py | 5 +-
nw/tools/__init__.py | 4 +-
nw/tools/{optlaststate.py => optionstate.py} | 66 +---------
6 files changed, 132 insertions(+), 207 deletions(-)
rename nw/tools/{optlaststate.py => optionstate.py} (69%)
diff --git a/nw/gui/dialogs/export.py b/nw/gui/dialogs/export.py
index 8d580f87..7dfaaadb 100644
--- a/nw/gui/dialogs/export.py
+++ b/nw/gui/dialogs/export.py
@@ -24,7 +24,7 @@ from PyQt5.QtWidgets import (
)
from nw.convert import TextFile, HtmlFile, MarkdownFile, LaTeXFile, ConcatFile
-from nw.tools import OptLastState
+from nw.tools import OptionState
from nw.common import packageRefURL
from nw.constants import nwFiles, nwItemType, nwAlert
@@ -40,8 +40,7 @@ class GuiExport(QDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
- self.optState = ExportLastState(self.theProject,nwFiles.EXPORT_OPT)
- self.optState.loadSettings()
+ self.optState = self.theProject.optState
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
@@ -50,8 +49,8 @@ class GuiExport(QDialog):
self.guiDeco = self.theParent.theTheme.loadDecoration("export",(64,64))
- self.tabMain = GuiExportMain(self.theParent, self.theProject, self.optState)
- self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject, self.optState)
+ self.tabMain = GuiExportMain(self.theParent, self.theProject)
+ self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject)
self.tabWidget = QTabWidget()
self.tabWidget.addTab(self.tabMain, "Settings")
@@ -290,24 +289,24 @@ class GuiExport(QDialog):
if saveTo.startswith("~"):
saveTo = path.expanduser(saveTo)
- self.optState.setSetting("wNovel", wNovel)
- self.optState.setSetting("wNotes", wNotes)
- self.optState.setSetting("eFormat", eFormat)
- self.optState.setSetting("fixWidth", fixWidth)
- self.optState.setSetting("wComments",wComments)
- self.optState.setSetting("wKeywords",wKeywords)
- self.optState.setSetting("chFormat", chFormat)
- self.optState.setSetting("unFormat", unFormat)
- self.optState.setSetting("scFormat", scFormat)
- self.optState.setSetting("seFormat", seFormat)
- self.optState.setSetting("saveTo", saveTo)
- self.optState.setSetting("hScene", hScene)
- self.optState.setSetting("hSection", hSection)
+ self.optState.setValue("GuiExport", "wNovel", wNovel)
+ self.optState.setValue("GuiExport", "wNotes", wNotes)
+ self.optState.setValue("GuiExport", "eFormat", eFormat)
+ self.optState.setValue("GuiExport", "fixWidth", fixWidth)
+ self.optState.setValue("GuiExport", "wComments", wComments)
+ self.optState.setValue("GuiExport", "wKeywords", wKeywords)
+ self.optState.setValue("GuiExport", "chFormat", chFormat)
+ self.optState.setValue("GuiExport", "unFormat", unFormat)
+ self.optState.setValue("GuiExport", "scFormat", scFormat)
+ self.optState.setValue("GuiExport", "seFormat", seFormat)
+ self.optState.setValue("GuiExport", "saveTo", saveTo)
+ self.optState.setValue("GuiExport", "hScene", hScene)
+ self.optState.setValue("GuiExport", "hSection", hSection)
# Pandoc Settings
pFormat = self.tabPandoc.outputFormat.currentData()
- self.optState.setSetting("pFormat", pFormat)
+ self.optState.setValue("GuiExport", "pFormat", pFormat)
self.optState.saveSettings()
self.close()
@@ -362,14 +361,14 @@ class GuiExportMain(QWidget):
),
}
- def __init__(self, theParent, theProject, optState):
+ def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
self.theParent = theParent
self.theProject = theProject
self.theTheme = theParent.theTheme
self.outerBox = QGridLayout()
- self.optState = optState
+ self.optState = self.theProject.optState
self.currFormat = self.FMT_TXT
# Select Files
@@ -378,19 +377,27 @@ class GuiExportMain(QWidget):
self.guiFiles.setLayout(self.guiFilesForm)
self.expNovel = QCheckBox("Novel files",self)
- self.expNovel.setChecked(self.optState.getSetting("wNovel"))
+ self.expNovel.setChecked(
+ self.optState.getBool("GuiExport", "wNovel", True)
+ )
self.expNovel.setToolTip("Include all novel files in the exported document")
self.expNotes = QCheckBox("Note files",self)
- self.expNotes.setChecked(self.optState.getSetting("wNotes"))
+ self.expNotes.setChecked(
+ self.optState.getBool("GuiExport", "wNotes", False)
+ )
self.expNotes.setToolTip("Include all note files in the exported document")
self.expComments = QCheckBox("Comments",self)
- self.expComments.setChecked(self.optState.getSetting("wComments"))
+ self.expComments.setChecked(
+ self.optState.getBool("GuiExport", "wComments", False)
+ )
self.expComments.setToolTip("Export comments from all files")
self.expKeywords = QCheckBox("Keywords",self)
- self.expKeywords.setChecked(self.optState.getSetting("wKeywords"))
+ self.expKeywords.setChecked(
+ self.optState.getBool("GuiExport", "wKeywords", False)
+ )
self.expKeywords.setToolTip("Export @keywords from all files")
self.guiFilesForm.addWidget(self.expNovel, 0, 1)
@@ -406,13 +413,17 @@ class GuiExportMain(QWidget):
self.chapterFormat = QLineEdit()
self.chapterFormat.setMaxLength(200)
- self.chapterFormat.setText(self.optState.getSetting("chFormat"))
+ self.chapterFormat.setText(
+ self.optState.getString("GuiExport", "chFormat", "Chapter %numword%")
+ )
self.chapterFormat.setToolTip("Available formats: %num%, %numword%, %title%")
self.chapterFormat.setMinimumWidth(250)
self.unnumFormat = QLineEdit()
self.unnumFormat.setMaxLength(200)
- self.unnumFormat.setText(self.optState.getSetting("unFormat"))
+ self.unnumFormat.setText(
+ self.optState.getString("GuiExport", "unFormat", "%title%")
+ )
self.unnumFormat.setToolTip("Available formats: %title%")
self.unnumFormat.setMinimumWidth(250)
@@ -428,22 +439,30 @@ class GuiExportMain(QWidget):
self.sceneFormat = QLineEdit()
self.sceneFormat.setMaxLength(200)
- self.sceneFormat.setText(self.optState.getSetting("scFormat"))
+ self.sceneFormat.setText(
+ self.optState.getString("GuiExport", "scFormat", "* * *")
+ )
self.sceneFormat.setToolTip("Available formats: %title%")
self.sceneFormat.setMinimumWidth(100)
self.sectionFormat = QLineEdit()
self.sectionFormat.setMaxLength(200)
- self.sectionFormat.setText(self.optState.getSetting("seFormat"))
+ self.sectionFormat.setText(
+ self.optState.getString("GuiExport", "seFormat", "")
+ )
self.sectionFormat.setToolTip("Available formats: %title%")
self.sectionFormat.setMinimumWidth(100)
self.hideScene = QCheckBox("Skip",self)
- self.hideScene.setChecked(self.optState.getSetting("hScene"))
+ self.hideScene.setChecked(
+ self.optState.getBool("GuiExport", "hScene", False)
+ )
self.hideScene.setToolTip("Skip scene titles in export")
self.hideSection = QCheckBox("Skip",self)
- self.hideSection.setChecked(self.optState.getSetting("hSection"))
+ self.hideSection.setChecked(
+ self.optState.getBool("GuiExport", "hSection", False)
+ )
self.hideSection.setToolTip("Skip section titles in export")
self.guiScenesForm.addWidget(QLabel("Scenes"), 0, 0)
@@ -458,8 +477,9 @@ class GuiExportMain(QWidget):
self.exportToForm = QGridLayout(self)
self.exportTo.setLayout(self.exportToForm)
- self.exportPath = QLineEdit(self.optState.getSetting("saveTo"))
-
+ self.exportPath = QLineEdit(
+ self.optState.getString("GuiExport", "saveTo", "")
+ )
self.exportGetPath = QPushButton(self.theTheme.getIcon("folder"),"")
self.exportGetPath.clicked.connect(self._exportFolder)
@@ -486,7 +506,9 @@ class GuiExportMain(QWidget):
self.outputFormat.addItem("Pandoc via Markdown or HTML", self.FMT_PDOC)
self.outputFormat.currentIndexChanged.connect(self._updateFormat)
- optIdx = self.outputFormat.findData(self.optState.getSetting("eFormat"))
+ optIdx = self.outputFormat.findData(
+ self.optState.getInt("GuiExport", "eFormat", 1)
+ )
if optIdx == -1:
self.outputFormat.setCurrentIndex(1)
self._updateFormat(1)
@@ -508,7 +530,9 @@ class GuiExportMain(QWidget):
self.fixedWidth.setMinimum(0)
self.fixedWidth.setMaximum(999)
self.fixedWidth.setSingleStep(1)
- self.fixedWidth.setValue(self.optState.getSetting("fixWidth"))
+ self.fixedWidth.setValue(
+ self.optState.getInt("GuiExport", "fixWidth", 80)
+ )
self.fixedWidth.setToolTip(
"Applies to .txt and .md files. A value of '0' disables the feature."
)
@@ -609,13 +633,13 @@ class GuiExportPandoc(QWidget):
FMT_ZIM : "markdown",
}
- def __init__(self, theParent, theProject, optState):
+ def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
self.theParent = theParent
self.theProject = theProject
self.outerBox = QGridLayout()
- self.optState = optState
+ self.optState = self.theProject.optState
try:
import pypandoc
@@ -659,7 +683,9 @@ class GuiExportPandoc(QWidget):
self.outputFormat.addItem("ePUB eBook v3 (.epub3)", self.FMT_EPUB3)
self.outputFormat.addItem("Zim Wiki (.txt)", self.FMT_ZIM)
- optIdx = self.outputFormat.findData(self.optState.getSetting("pFormat"))
+ optIdx = self.outputFormat.findData(
+ self.optState.getInt("GuiExport", "pFormat", 1)
+ )
if optIdx == -1:
self.outputFormat.setCurrentIndex(1)
else:
@@ -678,30 +704,3 @@ class GuiExportPandoc(QWidget):
return
# END Class GuiExportPandoc
-
-class ExportLastState(OptLastState):
-
- def __init__(self, theProject, theFile):
- OptLastState.__init__(self, theProject, theFile)
- self.theState = {
- "wNovel" : True,
- "wNotes" : False,
- "eFormat" : 1,
- "pFormat" : 1,
- "fixWidth" : 80,
- "wComments" : False,
- "wKeywords" : False,
- "chFormat" : "Chapter %numword%",
- "unFormat" : "%title%",
- "scFormat" : "* * *",
- "seFormat" : "",
- "saveTo" : "",
- "hScene" : False,
- "hSection" : False,
- }
- self.stringOpt = ("chFormat","unFormat","scFormat","seFormat","saveTo")
- self.boolOpt = ("wNovel","wNotes","wComments","wKeywords","hScene","hSection")
- self.intOpt = ("eFormat","pFormat","fixWidth")
- return
-
-# END Class ExportLastState
diff --git a/nw/gui/dialogs/sessionlog.py b/nw/gui/dialogs/sessionlog.py
index ccea5b1d..a047a988 100644
--- a/nw/gui/dialogs/sessionlog.py
+++ b/nw/gui/dialogs/sessionlog.py
@@ -24,7 +24,7 @@ from PyQt5.QtWidgets import (
)
from nw.constants import nwConst, nwFiles, nwAlert
-from nw.tools import OptLastState
+from nw.tools import OptionState
logger = logging.getLogger(__name__)
@@ -38,8 +38,7 @@ class GuiSessionLogView(QDialog):
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
- self.optState = SessionLogLastState(self.theProject,nwFiles.SLOG_OPT)
- self.optState.loadSettings()
+ self.optState = self.theProject.optState
self.timeFilter = 0.0
self.timeTotal = 0.0
@@ -52,13 +51,13 @@ class GuiSessionLogView(QDialog):
self.setMinimumHeight(400)
widthCol0 = self.optState.validIntRange(
- self.optState.getSetting("widthCol0"), 30, 999, 180
+ self.optState.getInt("GuiSession", "widthCol0", 180), 30, 999, 180
)
widthCol1 = self.optState.validIntRange(
- self.optState.getSetting("widthCol1"), 30, 999, 80
+ self.optState.getInt("GuiSession", "widthCol1", 80), 30, 999, 80
)
widthCol2 = self.optState.validIntRange(
- self.optState.getSetting("widthCol2"), 30, 999, 80
+ self.optState.getInt("GuiSession", "widthCol2", 80), 30, 999, 80
)
self.listBox = QTreeWidget()
@@ -77,10 +76,11 @@ class GuiSessionLogView(QDialog):
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
sortCol = self.optState.validIntRange(
- self.optState.getSetting("sortCol"), 0, 2, 0
+ self.optState.getInt("GuiSession", "sortCol", 0), 0, 2, 0
)
sortOrder = self.optState.validIntTuple(
- self.optState.getSetting("sortOrder"), sortValid, Qt.DescendingOrder
+ self.optState.getInt("GuiSession", "sortOrder", Qt.DescendingOrder),
+ sortValid, Qt.DescendingOrder
)
self.listBox.sortByColumn(sortCol, sortOrder)
@@ -110,11 +110,15 @@ class GuiSessionLogView(QDialog):
self.filterBox.setLayout(self.filterBoxForm)
self.hideZeros = QCheckBox("Hide zero word count", self)
- self.hideZeros.setChecked(self.optState.getSetting("hideZeros"))
+ self.hideZeros.setChecked(
+ self.optState.getBool("GuiSession", "hideZeros", True)
+ )
self.hideZeros.stateChanged.connect(self._doHideZeros)
self.hideNegative = QCheckBox("Hide negative word count", self)
- self.hideNegative.setChecked(self.optState.getSetting("hideNegative"))
+ self.hideNegative.setChecked(
+ self.optState.getBool("GuiSession", "hideNegative", False)
+ )
self.hideNegative.stateChanged.connect(self._doHideNegative)
self.filterBoxForm.addWidget(self.hideZeros, 0, 0)
@@ -208,13 +212,13 @@ class GuiSessionLogView(QDialog):
hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked()
- self.optState.setSetting("widthCol0", widthCol0)
- self.optState.setSetting("widthCol1", widthCol1)
- self.optState.setSetting("widthCol2", widthCol2)
- self.optState.setSetting("sortCol", sortCol)
- self.optState.setSetting("sortOrder", sortOrder)
- self.optState.setSetting("hideZeros", hideZeros)
- self.optState.setSetting("hideNegative",hideNegative)
+ self.optState.setValue("GuiSession", "widthCol0", widthCol0)
+ self.optState.setValue("GuiSession", "widthCol1", widthCol1)
+ self.optState.setValue("GuiSession", "widthCol2", widthCol2)
+ self.optState.setValue("GuiSession", "sortCol", sortCol)
+ self.optState.setValue("GuiSession", "sortOrder", sortOrder)
+ self.optState.setValue("GuiSession", "hideZeros", hideZeros)
+ self.optState.setValue("GuiSession", "hideNegative", hideNegative)
self.optState.saveSettings()
self.close()
@@ -237,23 +241,3 @@ class GuiSessionLogView(QDialog):
return "%02d:%02d:%02d" % (tH,tM,tS)
# END Class GuiSessionLogView
-
-class SessionLogLastState(OptLastState):
-
- def __init__(self, theProject, theFile):
- OptLastState.__init__(self, theProject, theFile)
- self.theState = {
- "widthCol0" : 180,
- "widthCol1" : 80,
- "widthCol2" : 80,
- "sortCol" : 0,
- "sortOrder" : Qt.DescendingOrder,
- "hideZeros" : True,
- "hideNegative" : False,
- }
- self.stringOpt = ()
- self.boolOpt = ("hideZeros","hideNegative")
- self.intOpt = ("widthCol0","widthCol1","widthCol2","sortCol","sortOrder")
- return
-
-# END Class SessionLogLastState
diff --git a/nw/gui/dialogs/timelineview.py b/nw/gui/dialogs/timelineview.py
index 5046d112..49b24fa7 100644
--- a/nw/gui/dialogs/timelineview.py
+++ b/nw/gui/dialogs/timelineview.py
@@ -22,7 +22,7 @@ from PyQt5.QtWidgets import (
)
from nw.constants import nwFiles, nwItemClass
-from nw.tools import OptLastState
+from nw.tools import OptionState
logger = logging.getLogger(__name__)
@@ -37,8 +37,7 @@ class GuiTimeLineView(QDialog):
self.theProject = theProject
self.theParent = theParent
self.theIndex = theIndex
- self.optState = TimeLineLastState(self.theProject,nwFiles.TLINE_OPT)
- self.optState.loadSettings()
+ self.optState = self.theProject.optState
self.theMatrix = {}
self.numRows = 0
@@ -54,10 +53,10 @@ class GuiTimeLineView(QDialog):
self.setMinimumHeight(400)
winWidth = self.optState.validIntRange(
- self.optState.getSetting("winWidth"), 700, 10000, 700
+ self.optState.getInt("GuiTimeLine", "winWidth", 700), 700, 10000, 700
)
winHeight = self.optState.validIntRange(
- self.optState.getSetting("winHeight"), 400, 10000, 400
+ self.optState.getInt("GuiTimeLine", "winHeight", 400), 400, 10000, 400
)
self.resize(winWidth,winHeight)
@@ -79,27 +78,39 @@ class GuiTimeLineView(QDialog):
self.optFilter.setLayout(self.optFilterGrid)
self.filterPlot = QCheckBox("Plot tags", self)
- self.filterPlot.setChecked(self.optState.getSetting("fPlot"))
+ self.filterPlot.setChecked(
+ self.optState.getBool("GuiTimeLine", "fPlot", True)
+ )
self.filterPlot.stateChanged.connect(self._filterChange)
self.filterChar = QCheckBox("Character tags", self)
- self.filterChar.setChecked(self.optState.getSetting("fChar"))
+ self.filterChar.setChecked(
+ self.optState.getBool("GuiTimeLine", "fChar", True)
+ )
self.filterChar.stateChanged.connect(self._filterChange)
self.filterWorld = QCheckBox("Location tags", self)
- self.filterWorld.setChecked(self.optState.getSetting("fWorld"))
+ self.filterWorld.setChecked(
+ self.optState.getBool("GuiTimeLine", "fWorld", True)
+ )
self.filterWorld.stateChanged.connect(self._filterChange)
self.filterTime = QCheckBox("Timeline tags", self)
- self.filterTime.setChecked(self.optState.getSetting("fTime"))
+ self.filterTime.setChecked(
+ self.optState.getBool("GuiTimeLine", "fTime", True)
+ )
self.filterTime.stateChanged.connect(self._filterChange)
self.filterObject = QCheckBox("Object tags", self)
- self.filterObject.setChecked(self.optState.getSetting("fObject"))
+ self.filterObject.setChecked(
+ self.optState.getBool("GuiTimeLine", "fObject", True)
+ )
self.filterObject.stateChanged.connect(self._filterChange)
self.filterCustom = QCheckBox("Custom tags", self)
- self.filterCustom.setChecked(self.optState.getSetting("fCustom"))
+ self.filterCustom.setChecked(
+ self.optState.getBool("GuiTimeLine", "fCustom", True)
+ )
self.filterCustom.stateChanged.connect(self._filterChange)
self.optFilterGrid.addWidget(self.filterPlot, 0, 1)
@@ -114,7 +125,9 @@ class GuiTimeLineView(QDialog):
self.optHide.setLayout(self.optHideGrid)
self.hideUnused = QCheckBox("Hide unused", self)
- self.hideUnused.setChecked(self.optState.getSetting("hUnused"))
+ self.hideUnused.setChecked(
+ self.optState.getBool("GuiTimeLine", "hUnused", True)
+ )
self.hideUnused.stateChanged.connect(self._filterChange)
self.optHideGrid.addWidget(self.hideUnused, 0, 1)
@@ -227,15 +240,15 @@ class GuiTimeLineView(QDialog):
fCustom = self.filterCustom.isChecked()
hUnused = self.hideUnused.isChecked()
- self.optState.setSetting("winWidth", winWidth)
- self.optState.setSetting("winHeight",winHeight)
- self.optState.setSetting("fPlot", fPlot)
- self.optState.setSetting("fChar", fChar)
- self.optState.setSetting("fWorld", fWorld)
- self.optState.setSetting("fTime", fTime)
- self.optState.setSetting("fObject", fObject)
- self.optState.setSetting("fCustom", fCustom)
- self.optState.setSetting("hUnused", hUnused)
+ self.optState.setValue("GuiTimeLine", "winWidth", winWidth)
+ self.optState.setValue("GuiTimeLine", "winHeight", winHeight)
+ self.optState.setValue("GuiTimeLine", "fPlot", fPlot)
+ self.optState.setValue("GuiTimeLine", "fChar", fChar)
+ self.optState.setValue("GuiTimeLine", "fWorld", fWorld)
+ self.optState.setValue("GuiTimeLine", "fTime", fTime)
+ self.optState.setValue("GuiTimeLine", "fObject", fObject)
+ self.optState.setValue("GuiTimeLine", "fCustom", fCustom)
+ self.optState.setValue("GuiTimeLine", "hUnused", hUnused)
self.optState.saveSettings()
self.close()
@@ -247,25 +260,3 @@ class GuiTimeLineView(QDialog):
return
# END Class GuiTimeLineView
-
-class TimeLineLastState(OptLastState):
-
- def __init__(self, theProject, theFile):
- OptLastState.__init__(self, theProject, theFile)
- self.theState = {
- "winWidth" : 700,
- "winHeight" : 400,
- "fPlot" : True,
- "fChar" : True,
- "fWorld" : True,
- "fTime" : True,
- "fObject" : True,
- "fCustom" : True,
- "hUnused" : True,
- }
- self.stringOpt = ()
- self.boolOpt = ("fPlot","fChar","fWorld","fTime","fObject","fCustom","hUnused")
- self.intOpt = ("winWidth","winHeight")
- return
-
-# END Class TimeLineLastState
diff --git a/nw/project/project.py b/nw/project/project.py
index bc934b21..aac52f73 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -21,7 +21,7 @@ from time import time
from nw.project.status import NWStatus
from nw.project.item import NWItem
-from nw.tools import projectMaintenance
+from nw.tools import projectMaintenance, OptionState
from nw.common import checkString, checkBool, checkInt
from nw.constants import (
nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert
@@ -36,6 +36,7 @@ class NWProject():
# Internal
self.theParent = theParent
self.mainConf = self.theParent.mainConf
+ self.optState = OptionState(self)
self.projOpened = None # The time stamp of when the project file was opened
self.projChanged = None # The project has unsaved changes
self.projAltered = None # The project has been altered this session
@@ -295,6 +296,7 @@ class NWProject():
nwItem.setFromTag(xValue.tag,xValue.text)
self._appendItem(tHandle,pHandle,nwItem)
+ self.optState.loadSettings()
self.mainConf.setRecent(self.projPath)
self.theParent.setStatus("Opened Project: %s" % self.projName)
@@ -384,6 +386,7 @@ class NWProject():
rename(saveFile, backFile)
rename(tempFile, saveFile)
+ self.optState.saveSettings()
self.mainConf.setRecent(self.projPath)
self.theParent.setStatus("Saved Project: %s" % self.projName)
self.setProjectChanged(False)
diff --git a/nw/tools/__init__.py b/nw/tools/__init__.py
index 71f6b141..d43736f9 100644
--- a/nw/tools/__init__.py
+++ b/nw/tools/__init__.py
@@ -2,7 +2,7 @@
from nw.tools.analyse import TextAnalysis
from nw.tools.legacy import projectMaintenance
-from nw.tools.optlaststate import OptLastState
+from nw.tools.optionstate import OptionState
from nw.tools.spellcheck import NWSpellCheck
from nw.tools.spellenchant import NWSpellEnchant
from nw.tools.spellsimple import NWSpellSimple
@@ -12,7 +12,7 @@ from nw.tools.wordcount import countWords
__all__ = [
"TextAnalysis",
"projectMaintenance",
- "OptLastState",
+ "OptionState",
"NWSpellCheck",
"NWSpellEnchant",
"NWSpellSimple",
diff --git a/nw/tools/optlaststate.py b/nw/tools/optionstate.py
similarity index 69%
rename from nw/tools/optlaststate.py
rename to nw/tools/optionstate.py
index 44c6a402..44a33087 100644
--- a/nw/tools/optlaststate.py
+++ b/nw/tools/optionstate.py
@@ -129,74 +129,22 @@ class OptionState():
return defaultValue
return defaultValue
-# END Class OptionState
-
-class OptLastState():
-
- def __init__(self, theProject, theFile):
- self.theProject = theProject
- self.theFile = theFile
- self.theState = {}
- self.stringOpt = ()
- self.boolOpt = ()
- self.intOpt = ()
- return
-
- def loadSettings(self):
- stateFile = path.join(self.theProject.projMeta,self.theFile)
- theState = {}
- if path.isfile(stateFile):
- logger.debug("Loading options file")
- try:
- with open(stateFile,mode="r",encoding="utf8") as inFile:
- theJson = inFile.read()
- theState = json.loads(theJson)
- except Exception as e:
- logger.error("Failed to load options file")
- logger.error(str(e))
- return False
- for anOpt in theState:
- self.theState[anOpt] = theState[anOpt]
- return True
-
- def saveSettings(self):
- stateFile = path.join(self.theProject.projMeta,self.theFile)
- logger.debug("Saving options file")
- try:
- with open(stateFile,mode="w+",encoding="utf8") as outFile:
- outFile.write(json.dumps(self.theState, indent=2))
- except Exception as e:
- logger.error("Failed to save options file")
- logger.error(str(e))
- return False
- return True
-
- def setSetting(self, setName, setValue):
- if setName in self.theState:
- self.theState[setName] = setValue
- else:
- return False
- return True
-
- def getSetting(self, setName):
- if setName in self.stringOpt:
- return checkString(self.theState[setName],self.theState[setName],False)
- elif setName in self.boolOpt:
- return checkBool(self.theState[setName],self.theState[setName],False)
- elif setName in self.intOpt:
- return checkInt(self.theState[setName],self.theState[setName],False)
- return None
-
def validIntRange(self, theValue, intA, intB, intDefault):
+ """Check that an int is in a given range. If it isn't, return
+ the default value.
+ """
if isinstance(theValue, int):
if theValue >= intA and theValue <= intB:
return theValue
return intDefault
def validIntTuple(self, theValue, theTuple, intDefault):
+ """Check that an int is an element of a tuple. If it isn't,
+ return the default value.
+ """
if isinstance(theValue, int):
if theValue in theTuple:
return theValue
return intDefault
-# END Class OptLastState
+# END Class OptionState
From 4977ffd843ff2c964f7df87a9ab5237498209b3f Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 19 Feb 2020 19:57:08 +0100
Subject: [PATCH 47/60] Clean up old json files
---
nw/constants/constants.py | 4 ----
nw/tools/legacy.py | 16 ++++++++++++++++
2 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index 80887188..3a24e4ff 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -26,10 +26,6 @@ class nwFiles():
SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
- EXPORT_OPT = "exportOptions.json"
- TLINE_OPT = "timelineOptions.json"
- SLOG_OPT = "sessionLogOptions.json"
- MERGE_OPT = "docMergeOptions.json"
# END Class nwFiles
diff --git a/nw/tools/legacy.py b/nw/tools/legacy.py
index 7b622cb6..0bc1e10a 100644
--- a/nw/tools/legacy.py
+++ b/nw/tools/legacy.py
@@ -44,4 +44,20 @@ def projectMaintenance(theProject):
except Exception as e:
logger.error(str(e))
+ # Remove no longer used meta files
+ rmList = []
+ rmList.append(path.join(theProject.projMeta, "mainOptions.json"))
+ rmList.append(path.join(theProject.projMeta, "exportOptions.json"))
+ rmList.append(path.join(theProject.projMeta, "outlineOptions.json"))
+ rmList.append(path.join(theProject.projMeta, "timelineOptions.json"))
+ rmList.append(path.join(theProject.projMeta, "docMergeOptions.json"))
+ rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json"))
+ for rmFile in rmList:
+ if path.isfile(rmFile):
+ logger.info("Deleting: %s" % rmFile)
+ try:
+ unlink(rmFile)
+ except Exception as e:
+ logger.error(str(e))
+
return
From b90f96643e1f94891775baccdc2d3acc37d2e9a7 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 19 Feb 2020 19:58:03 +0100
Subject: [PATCH 48/60] Updated sample project xml
---
sample/sampleNovel/nwProject.nwx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx
index fe093bba..4675a881 100644
--- a/sample/sampleNovel/nwProject.nwx
+++ b/sample/sampleNovel/nwProject.nwx
@@ -1,5 +1,5 @@
-
+Sample ProjectSample Project
@@ -82,7 +82,7 @@
11992167
- 949
+ 19Another Scene
From 10dcbe6ee5522c70f748468c67c626cf8628c296 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 19 Feb 2020 20:08:27 +0100
Subject: [PATCH 49/60] Also saving options for DocSplit dialog
---
nw/gui/dialogs/docsplit.py | 9 ++++++++-
nw/gui/dialogs/export.py | 1 -
nw/gui/dialogs/sessionlog.py | 1 -
nw/gui/dialogs/timelineview.py | 1 -
4 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py
index 817a3284..4296717c 100644
--- a/nw/gui/dialogs/docsplit.py
+++ b/nw/gui/dialogs/docsplit.py
@@ -33,6 +33,7 @@ class GuiDocSplit(QDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
+ self.optState = self.theProject.optState
self.sourceItem = None
self.outerBox = QHBoxLayout()
@@ -56,7 +57,11 @@ class GuiDocSplit(QDialog):
self.splitLevel.addItem("Split up to Header Level 2 (Chapter)", 2)
self.splitLevel.addItem("Split up to Header Level 3 (Scene)", 3)
self.splitLevel.addItem("Split up to Header Level 4 (Section)", 4)
- self.splitLevel.setCurrentIndex(2)
+ spIndex = self.splitLevel.findData(
+ self.optState.getInt("GuiDocSplit", "spLevel", 3)
+ )
+ if spIndex != -1:
+ self.splitLevel.setCurrentIndex(spIndex)
self.splitLevel.currentIndexChanged.connect(self._populateList)
self.splitButton = QPushButton("Split")
@@ -172,6 +177,7 @@ class GuiDocSplit(QDialog):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiDocSplit close button clicked")
+ self.optState.saveSettings()
self.close()
return
@@ -206,6 +212,7 @@ class GuiDocSplit(QDialog):
theText = theDoc.openDocument(self.sourceItem, False)
spLevel = self.splitLevel.currentData()
+ self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug("Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel))
lineNo = 0
diff --git a/nw/gui/dialogs/export.py b/nw/gui/dialogs/export.py
index 7dfaaadb..a626ade6 100644
--- a/nw/gui/dialogs/export.py
+++ b/nw/gui/dialogs/export.py
@@ -24,7 +24,6 @@ from PyQt5.QtWidgets import (
)
from nw.convert import TextFile, HtmlFile, MarkdownFile, LaTeXFile, ConcatFile
-from nw.tools import OptionState
from nw.common import packageRefURL
from nw.constants import nwFiles, nwItemType, nwAlert
diff --git a/nw/gui/dialogs/sessionlog.py b/nw/gui/dialogs/sessionlog.py
index a047a988..ccae4bb3 100644
--- a/nw/gui/dialogs/sessionlog.py
+++ b/nw/gui/dialogs/sessionlog.py
@@ -24,7 +24,6 @@ from PyQt5.QtWidgets import (
)
from nw.constants import nwConst, nwFiles, nwAlert
-from nw.tools import OptionState
logger = logging.getLogger(__name__)
diff --git a/nw/gui/dialogs/timelineview.py b/nw/gui/dialogs/timelineview.py
index 49b24fa7..225a451c 100644
--- a/nw/gui/dialogs/timelineview.py
+++ b/nw/gui/dialogs/timelineview.py
@@ -22,7 +22,6 @@ from PyQt5.QtWidgets import (
)
from nw.constants import nwFiles, nwItemClass
-from nw.tools import OptionState
logger = logging.getLogger(__name__)
From 39db69f199df4e48baed9f612710df82e285ceb3 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 23 Feb 2020 17:15:20 +0100
Subject: [PATCH 50/60] Add error handling and reporting for the Config class,
whichs is initialised before the GUI
---
nw/config.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++--
nw/guimain.py | 14 ++++++++++++++
2 files changed, 60 insertions(+), 2 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index eb0720a4..5d8b3ec9 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -42,9 +42,14 @@ class Config:
self.debugInfo = False
self.cmdOpen = None
+ # Config Error Handling
+ self.hasError = False
+ self.errData = []
+
# Set Paths
self.confPath = None
self.confFile = None
+ self.dataPath = None
self.homePath = None
self.lastPath = None
self.appPath = None
@@ -172,6 +177,15 @@ class Config:
logger.info("Setting config from alternative path: %s" % confPath)
self.confPath = confPath
+ if self.verQtValue >= 50400:
+ dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
+ else:
+ dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
+ self.dataPath = path.join(path.abspath(dataRoot), self.appHandle)
+
+ logger.verbose("Config path: %s" % self.confPath)
+ logger.verbose("Data path: %s" % self.dataPath)
+
self.confFile = self.appHandle+".conf"
self.homePath = path.expanduser("~")
self.lastPath = self.homePath
@@ -192,7 +206,10 @@ class Config:
except Exception as e:
logger.error("Could not create folder: %s" % self.confPath)
logger.error(str(e))
- return False
+ self.hasError = True
+ self.errData.append("Could not create folder: %s" % self.confPath)
+ self.errData.append(str(e))
+ self.confPath = None
# Check if config file exists
if path.isfile(path.join(self.confPath,self.confFile)):
@@ -202,6 +219,19 @@ class Config:
# If it does not exist, save a copy of the default values
self.saveConfig()
+ # If data folder does not exist, make it.
+ # This assumes that the os data folder itself exists.
+ if not path.isdir(self.dataPath):
+ try:
+ mkdir(self.dataPath)
+ except Exception as e:
+ logger.error("Could not create folder: %s" % self.dataPath)
+ logger.error(str(e))
+ self.hasError = True
+ self.errData.append("Could not create folder: %s" % self.dataPath)
+ self.errData.append(str(e))
+ self.dataPath = None
+
# Check the availability of optional packages
self._checkOptionalPackages()
@@ -222,6 +252,10 @@ class Config:
)
except Exception as e:
logger.error("Could not load config file")
+ logger.error(str(e))
+ self.hasError = True
+ self.errData.append("Could not load config file")
+ self.errData.append(str(e))
return False
## Main
@@ -446,12 +480,16 @@ class Config:
self.confChanged = False
except Exception as e:
logger.error("Could not save config file")
+ logger.error(str(e))
+ self.hasError = True
+ self.errData.append("Could not save config file")
+ self.errData.append(str(e))
return False
return True
##
- # Setters
+ # Setters and Getters
##
def setRecent(self, recentPath):
@@ -516,6 +554,12 @@ class Config:
self.confChanged = True
return
+ def getErrData(self):
+ errMessage = " ".join(self.errData)
+ self.hasError = False
+ self.errData = []
+ return errMessage
+
##
# Internal Functions
##
diff --git a/nw/guimain.py b/nw/guimain.py
index 670b1534..2c0d15e9 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -166,6 +166,9 @@ class GuiMain(QMainWindow):
if self.mainConf.showGUI:
self.show()
+ # Check that config loaded fine
+ self.reportConfErr()
+
self.initMain()
self.asProjTimer.start()
self.asDocTimer.start()
@@ -686,6 +689,16 @@ class GuiMain(QMainWindow):
return
+ def reportConfErr(self):
+ """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.
+ """
+ if self.mainConf.hasError:
+ self.makeAlert(self.mainConf.getErrData(), nwAlert.ERROR)
+ return True
+ return False
+
##
# Main Window Actions
##
@@ -710,6 +723,7 @@ class GuiMain(QMainWindow):
self.mainConf.setMainPanePos(self.splitMain.sizes())
self.mainConf.setDocPanePos(self.splitView.sizes())
self.mainConf.saveConfig()
+ self.reportConfErr()
qApp.quit()
From 6867f386371fb755ff770c2d9f297128bc4598c9 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 15:30:08 +0100
Subject: [PATCH 51/60] Added the main dialog class for project management, and
made some changes to the icon class.
---
nw/gui/__init__.py | 2 +
nw/gui/dialogs/__init__.py | 2 +
nw/gui/dialogs/projectload.py | 87 +++++++++++++++++++++++++++++++++++
nw/gui/icons.py | 32 +++++++++++--
nw/guimain.py | 17 ++++++-
5 files changed, 133 insertions(+), 7 deletions(-)
create mode 100644 nw/gui/dialogs/projectload.py
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index d8842bcc..48896ba1 100644
--- a/nw/gui/__init__.py
+++ b/nw/gui/__init__.py
@@ -13,6 +13,7 @@ from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
+from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView
from nw.gui.dialogs.timelineview import GuiTimeLineView
@@ -40,6 +41,7 @@ __all__ = [
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
+ "GuiProjectLoad",
"GuiSessionLogView",
"GuiTimeLineView",
"GuiDocDetails",
diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py
index a58b4982..49b10df3 100644
--- a/nw/gui/dialogs/__init__.py
+++ b/nw/gui/dialogs/__init__.py
@@ -6,6 +6,7 @@ from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
+from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView
from nw.gui.dialogs.timelineview import GuiTimeLineView
@@ -16,6 +17,7 @@ __all__ = [
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
+ "GuiProjectLoad",
"GuiSessionLogView",
"GuiTimeLineView",
]
diff --git a/nw/gui/dialogs/projectload.py b/nw/gui/dialogs/projectload.py
new file mode 100644
index 00000000..9b075fbc
--- /dev/null
+++ b/nw/gui/dialogs/projectload.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+"""novelWriter GUI Open Project
+
+ novelWriter – GUI Open Project
+================================
+ New and open project dialog
+
+ File History:
+ Created: 2020-02-26 [0.4.5]
+
+"""
+
+import logging
+import nw
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import (
+ QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QListWidget,
+ QAbstractItemView
+)
+
+logger = logging.getLogger(__name__)
+
+class GuiProjectLoad(QDialog):
+
+ def __init__(self, theParent):
+ QDialog.__init__(self, theParent)
+
+ logger.debug("Initialising GuiProjectLoad ...")
+
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+ self.sourceItem = None
+
+ self.outerBox = QHBoxLayout()
+ self.innerBox = QVBoxLayout()
+ self.setWindowTitle("Manage Projects")
+ self.setLayout(self.outerBox)
+
+ self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (128, 128))
+
+ self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
+ self.outerBox.addLayout(self.innerBox)
+
+ self.projectForm = QGridLayout()
+ self.projectForm.setContentsMargins(0, 0, 0, 0)
+
+ self.listBox = QListWidget()
+ self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
+
+ self.closeButton = QPushButton("Close")
+ self.closeButton.clicked.connect(self._doClose)
+
+ self.projectForm.addWidget(self.listBox, 0, 0, 1, 3)
+ self.projectForm.addWidget(self.closeButton, 1, 2)
+
+ self.innerBox.addLayout(self.projectForm)
+
+ self.rejected.connect(self._doClose)
+ self.setModal(True)
+ self.show()
+
+ self._populateList()
+
+ logger.debug("GuiProjectLoad initialisation complete")
+
+ return
+
+ ##
+ # Buttons
+ ##
+
+ def _doClose(self):
+ """Close the dialog window without doing anything.
+ """
+ logger.verbose("GuiProjectLoad close button clicked")
+ self.close()
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _populateList(self):
+ return
+
+# END Class GuiProjectLoad
diff --git a/nw/gui/icons.py b/nw/gui/icons.py
index facb84d7..a6a68db0 100644
--- a/nw/gui/icons.py
+++ b/nw/gui/icons.py
@@ -45,10 +45,11 @@ class GuiIcons:
}
DECO_MAP = {
- "export" : "export.svg",
- "merge" : "merge.svg",
- "settings" : "gear.svg",
- "split" : "split.svg",
+ "nwicon" : ["icons", "novelWriter.svg"],
+ "export" : ["graphics", "export.svg"],
+ "merge" : ["graphics", "merge.svg"],
+ "settings" : ["graphics", "gear.svg"],
+ "split" : ["graphics", "split.svg"],
}
def __init__(self, theParent):
@@ -67,6 +68,9 @@ class GuiIcons:
return
def initIcons(self, priPath):
+ """Load all icons listed in the icon map. Can be overridden by
+ the selected theme.
+ """
self.priPath = priPath
self.secPath = self.mainConf.iconPath
@@ -78,12 +82,19 @@ class GuiIcons:
return
def loadDecoration(self, decoKey, decoSize=None):
+ """Load graphical decoration element based on the decoration
+ map. This function always returns a QSwgWidget.
+ """
if decoKey not in self.DECO_MAP:
logger.error("Decoration with name '%s' does not exist" % decoKey)
return QSvgWidget()
- svgPath = path.join(self.mainConf.graphPath, self.DECO_MAP[decoKey])
+ svgPath = path.join(
+ self.mainConf.assetPath,
+ self.DECO_MAP[decoKey][0],
+ self.DECO_MAP[decoKey][1]
+ )
if not path.isfile(svgPath):
logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey])
return QSvgWidget()
@@ -95,11 +106,17 @@ class GuiIcons:
return svgDeco
def getIcon(self, iconKey, iconSize=None):
+ """Return an icon from the icon buffer. If it doesn't exist,
+ return an empty icon.
+ """
if iconKey in self.qIcons:
return self.qIcons[iconKey]
return QIcon()
def getPixmap(self, iconKey, iconSize):
+ """Return an icon from the icon buffer as a QPixmap. If it
+ doesn't exist, return an empty QPixmap.
+ """
if iconKey in self.qIcons:
return self.qIcons[iconKey].pixmap(iconSize[0], iconSize[1], QIcon.Normal)
return QPixmap()
@@ -109,6 +126,11 @@ class GuiIcons:
##
def _loadIcon(self, iconKey):
+ """Load an icon from the assets or theme folder, with a
+ preference for dark/light icons depending on theme type, if such
+ an icon exists. Prefer svg files over png files. Always returns
+ a QIcon.
+ """
if iconKey not in self.ICON_MAP:
logger.error("Icon with name '%s' does not exist" % iconKey)
diff --git a/nw/guimain.py b/nw/guimain.py
index 2c0d15e9..96fc99b0 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -27,7 +27,7 @@ from nw.gui import (
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport,
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiTimeLineView,
- GuiSessionLogView, GuiDocMerge, GuiDocSplit
+ GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad
)
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
from nw.tools import countWords
@@ -64,7 +64,7 @@ class GuiMain(QMainWindow):
self.resize(*self.mainConf.winGeometry)
self._setWindowTitle()
- self.setWindowIcon(QIcon(path.join(self.mainConf.appIcon)))
+ self.setWindowIcon(QIcon(self.mainConf.appIcon))
# Main GUI Elements
self.statusBar = GuiMainStatus(self)
@@ -183,6 +183,8 @@ class GuiMain(QMainWindow):
if self.mainConf.cmdOpen is not None:
logger.debug("Opening project from additional command line option")
self.openProject(self.mainConf.cmdOpen)
+ else:
+ self.manageProjects()
return
@@ -204,6 +206,17 @@ class GuiMain(QMainWindow):
# Project Actions
##
+ def manageProjects(self):
+ """
+ """
+
+ if self.mainConf.showGUI:
+ dlgProj = GuiProjectLoad(self)
+ dlgProj.exec_()
+
+
+ return True
+
def newProject(self, projPath=None, forceNew=False):
if self.hasProject:
From c688c66d5b2d585cb13bbe350c4908e76d268960 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 17:44:11 +0100
Subject: [PATCH 52/60] Project manager dialog now lists recent projects and
lets the user open one of these, or browse for a folder
---
nw/common.py | 19 ++++++++
nw/config.py | 77 +++++++++++++++++++++++++++++-
nw/constants/constants.py | 13 +++---
nw/gui/dialogs/projectload.py | 88 +++++++++++++++++++++++++++++++++--
nw/gui/mainmenu.py | 2 +-
nw/guimain.py | 18 ++++---
nw/project/project.py | 9 +++-
7 files changed, 205 insertions(+), 21 deletions(-)
diff --git a/nw/common.py b/nw/common.py
index 8aa0a0f0..6764d244 100644
--- a/nw/common.py
+++ b/nw/common.py
@@ -88,6 +88,25 @@ def colRange(rgbStart, rgbEnd, nStep):
return retCol
+def formatInt(theInt):
+ """Formats an integer with k, M, G etc.
+ """
+ postFix = ["k","M","G","T","P","E"]
+ theVal = float(theInt)
+
+ if theVal > 1000.0:
+ for pF in postFix:
+ theVal /= 1000.0
+ if theVal < 1000.0:
+ if theVal < 10.0:
+ return "%4.2f%s" % (theVal,pF)
+ elif theVal < 100.0:
+ return "%4.1f%s" % (theVal,pF)
+ else:
+ return "%3.0f%s" % (theVal,pF)
+
+ return "%d" % theInt
+
def splitVersionNumber(vString):
""" Splits a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc.
diff --git a/nw/config.py b/nw/config.py
index 5d8b3ec9..4cc9f500 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -12,10 +12,11 @@
import logging
import configparser
+import json
import sys
import nw
-from os import path, mkdir
+from os import path, mkdir, unlink, rename
from datetime import datetime
from PyQt5.Qt import PYQT_VERSION_STR
@@ -162,6 +163,9 @@ class Config:
self.hasEnchant = False
self.hasSymSpell = False
+ # Recent Cache
+ self.recentProj = {}
+
return
##
@@ -219,6 +223,9 @@ class Config:
# If it does not exist, save a copy of the default values
self.saveConfig()
+ # Load re3cent projects cache
+ self.loadRecentCache()
+
# If data folder does not exist, make it.
# This assumes that the os data folder itself exists.
if not path.isdir(self.dataPath):
@@ -488,6 +495,74 @@ class Config:
return True
+ def loadRecentCache(self):
+ """Load the cache file for recent projects.
+ """
+ cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
+ self.recentProj = {}
+
+ if path.isfile(cacheFile):
+ try:
+ with open(cacheFile, mode="r", encoding="utf8") as inFile:
+ theJson = inFile.read()
+ theData = json.loads(theJson)
+
+ for projPath in theData.keys():
+ theEntry = theData[projPath]
+ theTitle = ""
+ lastTime = 0
+ wordCount = 0
+ if "title" in theEntry.keys():
+ theTitle = theEntry["title"]
+ if "time" in theEntry.keys():
+ lastTime = int(theEntry["time"])
+ if "words" in theEntry.keys():
+ wordCount = int(theEntry["words"])
+ self.recentProj[projPath] = {
+ "title" : theTitle,
+ "time" : lastTime,
+ "words" : wordCount,
+ }
+
+ except Exception as e:
+ self.hasError = True
+ self.errData.append("Could not load recent project cache")
+ self.errData.append(str(e))
+ return False
+
+ return True
+
+ def saveRecentCache(self):
+ """Save the cache dictionary of recent projects.
+ """
+ cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
+ cacheTemp = path.join(self.dataPath, nwFiles.RECENT_FILE+"~")
+
+ try:
+ with open(cacheTemp, mode="w+", encoding="utf8") as outFile:
+ outFile.write(json.dumps(self.recentProj, indent=2))
+ except Exception as e:
+ self.hasError = True
+ self.errData.append("Could not save recent project cache")
+ self.errData.append(str(e))
+ return False
+
+ if path.isfile(cacheFile):
+ unlink(cacheFile)
+ rename(cacheTemp, cacheFile)
+
+ return True
+
+ def updateRecentCache(self, projPath, projTitle, wordCount, saveTime):
+ """Add or update recent cache information o9n a given project.
+ """
+ self.recentProj[path.abspath(projPath)] = {
+ "title" : projTitle,
+ "time" : int(saveTime),
+ "words" : int(wordCount),
+ }
+ return True
+
##
# Setters and Getters
##
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index 3a24e4ff..4cc7f040 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -20,12 +20,13 @@ class nwConst():
class nwFiles():
- APP_ICON = "novelWriter.svg"
- PROJ_FILE = "nwProject.nwx"
- PROJ_DICT = "wordlist.txt"
- SESS_INFO = "sessionInfo.log"
- INDEX_FILE = "tagsIndex.json"
- OPTS_FILE = "guiOptions.json"
+ APP_ICON = "novelWriter.svg"
+ PROJ_FILE = "nwProject.nwx"
+ PROJ_DICT = "wordlist.txt"
+ SESS_INFO = "sessionInfo.log"
+ INDEX_FILE = "tagsIndex.json"
+ OPTS_FILE = "guiOptions.json"
+ RECENT_FILE = "recentProjects.json"
# END Class nwFiles
diff --git a/nw/gui/dialogs/projectload.py b/nw/gui/dialogs/projectload.py
index 9b075fbc..6fe7f92c 100644
--- a/nw/gui/dialogs/projectload.py
+++ b/nw/gui/dialogs/projectload.py
@@ -13,12 +13,16 @@
import logging
import nw
+from datetime import datetime
+
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
- QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QListWidget,
- QAbstractItemView
+ QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget,
+ QAbstractItemView, QTreeWidgetItem
)
+from nw.common import formatInt
+
logger = logging.getLogger(__name__)
class GuiProjectLoad(QDialog):
@@ -31,6 +35,7 @@ class GuiProjectLoad(QDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.sourceItem = None
+ self.openPath = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
@@ -45,19 +50,35 @@ class GuiProjectLoad(QDialog):
self.projectForm = QGridLayout()
self.projectForm.setContentsMargins(0, 0, 0, 0)
- self.listBox = QListWidget()
+ self.listBox = QTreeWidget()
+ self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
+ self.listBox.setColumnCount(4)
+ self.listBox.setHeaderLabels(["Working Title","Words","Accessed","Path"])
+ self.listBox.setRootIsDecorated(False)
+ treeHead = self.listBox.headerItem()
+ treeHead.setTextAlignment(1, Qt.AlignRight)
+
+ self.recentButton = QPushButton("Open")
+ self.recentButton.clicked.connect(self._doOpenRecent)
+ self.browseButton = QPushButton("Browse")
+ self.browseButton.clicked.connect(self._doBrowse)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
- self.projectForm.addWidget(self.listBox, 0, 0, 1, 3)
- self.projectForm.addWidget(self.closeButton, 1, 2)
+ self.projectForm.addWidget(self.listBox, 0, 0, 1, 4)
+ self.projectForm.addWidget(self.recentButton, 1, 1)
+ self.projectForm.addWidget(self.browseButton, 1, 2)
+ self.projectForm.addWidget(self.closeButton, 1, 3)
+ self.projectForm.setColumnStretch(0, 1)
self.innerBox.addLayout(self.projectForm)
self.rejected.connect(self._doClose)
self.setModal(True)
+ self.setMinimumWidth(750)
+ self.setMinimumHeight(450)
self.show()
self._populateList()
@@ -70,6 +91,29 @@ class GuiProjectLoad(QDialog):
# Buttons
##
+ def _doOpenRecent(self):
+ """Close the dialog window with a recent project selected.
+ """
+ logger.verbose("GuiProjectLoad open button clicked")
+
+ selItems = self.listBox.selectedItems()
+ if selItems:
+ self.openPath = selItems[0].text(3)
+ self.accept()
+ else:
+ self.openPath = None
+
+ return
+
+ def _doBrowse(self):
+ """Close the dialog window with no selected path, triggering the
+ project browser dialog.
+ """
+ logger.verbose("GuiProjectLoad browse button clicked")
+ self.openPath = None
+ self.accept()
+ return
+
def _doClose(self):
"""Close the dialog window without doing anything.
"""
@@ -82,6 +126,40 @@ class GuiProjectLoad(QDialog):
##
def _populateList(self):
+ """Populate the list box with recent project data.
+ """
+
+ listOrder = []
+ listData = {}
+ for projPath in self.mainConf.recentProj.keys():
+ theEntry = self.mainConf.recentProj[projPath]
+ theTitle = ""
+ theTime = 0
+ theWords = 0
+ if "title" in theEntry.keys():
+ theTitle = theEntry["title"]
+ if "time" in theEntry.keys():
+ theTime = theEntry["time"]
+ if "words" in theEntry.keys():
+ theWords = theEntry["words"]
+ if theTime > 0:
+ listOrder.append(theTime)
+ listData[theTime] = [theTitle, theWords, projPath]
+
+ self.listBox.clear()
+ for timeStamp in sorted(listOrder, reverse=True):
+ newItem = QTreeWidgetItem([""]*4)
+ newItem.setText(0, listData[timeStamp][0])
+ newItem.setText(1, formatInt(listData[timeStamp][1]))
+ newItem.setText(2, datetime.fromtimestamp(timeStamp).strftime("%x %X"))
+ newItem.setText(3, listData[timeStamp][2])
+ newItem.setTextAlignment(1, Qt.AlignRight)
+ self.listBox.addTopLevelItem(newItem)
+
+ self.listBox.resizeColumnToContents(0)
+ self.listBox.resizeColumnToContents(1)
+ self.listBox.resizeColumnToContents(2)
+
return
# END Class GuiProjectLoad
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index 14ff2279..1548eaaf 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -194,7 +194,7 @@ class GuiMainMenu(QMenuBar):
self.aOpenProject = QAction("Open Project", self)
self.aOpenProject.setStatusTip("Open project")
self.aOpenProject.setShortcut("Ctrl+Shift+O")
- self.aOpenProject.triggered.connect(lambda : self.theParent.openProject(None))
+ self.aOpenProject.triggered.connect(self.theParent.manageProjects)
self.projMenu.addAction(self.aOpenProject)
# Project > Save Project
diff --git a/nw/guimain.py b/nw/guimain.py
index 96fc99b0..19042480 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -207,13 +207,17 @@ class GuiMain(QMainWindow):
##
def manageProjects(self):
+ """Opens 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.
"""
- """
-
- if self.mainConf.showGUI:
- dlgProj = GuiProjectLoad(self)
- dlgProj.exec_()
+ if not self.mainConf.showGUI:
+ return False
+ dlgProj = GuiProjectLoad(self)
+ dlgProj.exec_()
+ if dlgProj.result() == QDialog.Accepted:
+ self.openProject(dlgProj.openPath)
return True
@@ -223,7 +227,7 @@ class GuiMain(QMainWindow):
msgBox = QMessageBox()
msgRes = msgBox.warning(
self, "New Project",
- "Please close the current project before making a new one."
+ "Please close the current project before making a new one."
)
return False
@@ -236,7 +240,7 @@ class GuiMain(QMainWindow):
msgBox = QMessageBox()
msgRes = msgBox.critical(
self, "New Project",
- "A project already exists in that location. Please choose another folder."
+ "A project already exists in that location. Please choose another folder."
)
return False
diff --git a/nw/project/project.py b/nw/project/project.py
index aac52f73..fa5ee920 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -319,6 +319,7 @@ class NWProject():
return False
self.projMeta = path.join(self.projPath,"meta")
+ saveTime = time()
if not self._checkFolder(self.projPath): return
if not self._checkFolder(self.projMeta): return
@@ -330,7 +331,7 @@ class NWProject():
nwXML = etree.Element("novelWriterXML",attrib={
"appVersion" : str(nw.__version__),
"fileVersion" : "1.0",
- "timeStamp" : datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "timeStamp" : datetime.fromtimestamp(saveTime).strftime("%Y-%m-%d %H:%M:%S"),
})
# Save Project Meta
@@ -386,8 +387,14 @@ class NWProject():
rename(saveFile, backFile)
rename(tempFile, saveFile)
+ # Save project GUI options
self.optState.saveSettings()
+
+ # Update recent projects
self.mainConf.setRecent(self.projPath)
+ self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
+ self.mainConf.saveRecentCache()
+
self.theParent.setStatus("Saved Project: %s" % self.projName)
self.setProjectChanged(False)
From 6b3e652440757cf5b9c7d20b707e6f609e6ef6b0 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 17:57:36 +0100
Subject: [PATCH 53/60] Delete old recent project code
---
nw/config.py | 17 -----------------
nw/gui/mainmenu.py | 38 --------------------------------------
nw/guimain.py | 2 --
nw/project/project.py | 7 +++++--
4 files changed, 5 insertions(+), 59 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index 4cc9f500..be5589e7 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -396,10 +396,6 @@ class Config:
self.lastPath = self._parseLine(
cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath
)
- for i in range(10):
- self.recentList[i] = self._parseLine(
- cnfParse, cnfSec, "recent%d" % i,self.CNF_STR, self.recentList[i]
- )
# Check Certain Values for None
self.spellLanguage = self._checkNone(self.spellLanguage)
@@ -478,8 +474,6 @@ class Config:
cnfSec = "Path"
cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec,"lastpath", str(self.lastPath))
- for i in range(10):
- cnfParse.set(cnfSec,"recent%d" % i, str(self.recentList[i]))
# Write config file
try:
@@ -567,17 +561,6 @@ class Config:
# Setters and Getters
##
- def setRecent(self, recentPath):
- if recentPath == "": return
- if recentPath in self.recentList[0:10]:
- self.recentList.remove(recentPath)
- self.recentList.insert(0,recentPath)
- return
-
- def clearRecent(self):
- self.recentList = [""]*10
- return
-
def setConfPath(self, newPath):
if newPath is None:
return True
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index 1548eaaf..1c13d81a 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -48,11 +48,6 @@ class GuiMainMenu(QMenuBar):
return
- def openRecentProject(self, menuItem, recentItem):
- logger.verbose("User requested opening recent project #%d" % recentItem)
- self.theParent.openProject(self.mainConf.recentList[recentItem])
- return True
-
def setAvailableRoot(self):
for itemClass in nwItemClass:
if itemClass == nwItemClass.NO_CLASS: continue
@@ -66,30 +61,6 @@ class GuiMainMenu(QMenuBar):
# Update Menu on Settings Changed
##
- def updateMenu(self):
- self.updateRecentProjects()
- return
-
- def updateRecentProjects(self):
-
- self.recentMenu.clear()
- for n in range(len(self.mainConf.recentList)):
- recentProject = self.mainConf.recentList[n]
- if recentProject == "": continue
- menuItem = QAction("%s" % recentProject, self.projMenu)
- menuItem.triggered.connect(
- lambda a1=menuItem, a2=n : self.openRecentProject(a1, a2)
- )
- self.recentMenu.addAction(menuItem)
-
- self.recentMenu.addSeparator()
- menuItem = QAction("Clear Recent Projects", self)
- menuItem.setStatusTip("Clear the list of recent projects")
- menuItem.triggered.connect(self._clearRecentProjects)
- self.recentMenu.addAction(menuItem)
-
- return
-
def setSpellCheck(self, theMode):
"""Set the spell check check box to theMode. This is controlled
by the document editor class, which holds the master spell check
@@ -166,11 +137,6 @@ class GuiMainMenu(QMenuBar):
QDesktopServices.openUrl(QUrl(nw.__docurl__))
return True
- def _clearRecentProjects(self):
- self.mainConf.clearRecent()
- self.updateRecentProjects()
- return True
-
def _showDocumentLocation(self):
self.theParent.docEditor.revealLocation()
return True
@@ -211,10 +177,6 @@ class GuiMainMenu(QMenuBar):
self.aCloseProject.triggered.connect(lambda : self.theParent.closeProject(False))
self.projMenu.addAction(self.aCloseProject)
- # Project > Recent Projects
- self.recentMenu = self.projMenu.addMenu("Recent Projects")
- self.updateRecentProjects()
-
# Project > Project Settings
self.aProjectSettings = QAction("Project Settings", self)
self.aProjectSettings.setStatusTip("Project settings")
diff --git a/nw/guimain.py b/nw/guimain.py
index 19042480..99ad02f5 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -331,7 +331,6 @@ class GuiMain(QMainWindow):
self.docEditor.setDictionaries()
self.docEditor.setSpellCheck(self.theProject.spellCheck)
self.statusBar.setRefTime(self.theProject.projOpened)
- self.mainMenu.updateMenu()
# Restore previously open documents, if any
if self.theProject.lastEdited is not None:
@@ -361,7 +360,6 @@ class GuiMain(QMainWindow):
self.treeView.saveTreeOrder()
self.theProject.saveProject()
self.theIndex.saveIndex()
- self.mainMenu.updateRecentProjects()
return True
diff --git a/nw/project/project.py b/nw/project/project.py
index fa5ee920..887fe38c 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -297,7 +297,11 @@ class NWProject():
self._appendItem(tHandle,pHandle,nwItem)
self.optState.loadSettings()
- self.mainConf.setRecent(self.projPath)
+
+ # Update recent projects
+ self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
+ self.mainConf.saveRecentCache()
+
self.theParent.setStatus("Opened Project: %s" % self.projName)
self._scanProjectFolder()
@@ -391,7 +395,6 @@ class NWProject():
self.optState.saveSettings()
# Update recent projects
- self.mainConf.setRecent(self.projPath)
self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
self.mainConf.saveRecentCache()
From 0587099d368e710feaed3e55f4058fe9c0aebb2c Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 18:02:37 +0100
Subject: [PATCH 54/60] A few tweaks to the dialog and code
---
nw/gui/dialogs/projectload.py | 6 +++++-
nw/guimain.py | 8 +++++---
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/nw/gui/dialogs/projectload.py b/nw/gui/dialogs/projectload.py
index 6fe7f92c..3e773a9d 100644
--- a/nw/gui/dialogs/projectload.py
+++ b/nw/gui/dialogs/projectload.py
@@ -39,7 +39,7 @@ class GuiProjectLoad(QDialog):
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
- self.setWindowTitle("Manage Projects")
+ self.setWindowTitle("Open Project")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (128, 128))
@@ -147,6 +147,7 @@ class GuiProjectLoad(QDialog):
listData[theTime] = [theTitle, theWords, projPath]
self.listBox.clear()
+ hasSelection = False
for timeStamp in sorted(listOrder, reverse=True):
newItem = QTreeWidgetItem([""]*4)
newItem.setText(0, listData[timeStamp][0])
@@ -155,6 +156,9 @@ class GuiProjectLoad(QDialog):
newItem.setText(3, listData[timeStamp][2])
newItem.setTextAlignment(1, Qt.AlignRight)
self.listBox.addTopLevelItem(newItem)
+ if not hasSelection:
+ newItem.setSelected(True)
+ hasSelection = True
self.listBox.resizeColumnToContents(0)
self.listBox.resizeColumnToContents(1)
diff --git a/nw/guimain.py b/nw/guimain.py
index 99ad02f5..a9cd7edb 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -222,6 +222,8 @@ class GuiMain(QMainWindow):
return True
def newProject(self, projPath=None, forceNew=False):
+ """Create new project with a few default files and folders.
+ """
if self.hasProject:
msgBox = QMessageBox()
@@ -255,9 +257,9 @@ class GuiMain(QMainWindow):
return True
def closeProject(self, isYes=False):
- """Closes the project if one is open.
- isYes is passed on from the close application event so the user
- doesn't get prompted twice.
+ """Closes the project if one is open. isYes is passed on from
+ the close application event so the user doesn't get prompted
+ twice.
"""
if not self.hasProject:
# There is no project loaded, everything OK
From 0696ef79e43eaf7c040c1a43e833c1b889ecbdd5 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 18:27:07 +0100
Subject: [PATCH 55/60] Fix tests and added --data option to command line
---
nw/__init__.py | 7 ++++++-
nw/config.py | 29 +++++++++++++++++++++--------
tests/reference/novelwriter.conf | 12 +-----------
tests/test_config.py | 2 +-
tests/test_gui.py | 16 ++++++++--------
tests/test_project.py | 4 ++--
6 files changed, 39 insertions(+), 31 deletions(-)
diff --git a/nw/__init__.py b/nw/__init__.py
index d562d446..f3766e6c 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -87,6 +87,7 @@ def main(sysArgs=None):
"logfile=",
"version",
"config=",
+ "data=",
"testmode",
"style=",
]
@@ -105,6 +106,7 @@ def main(sysArgs=None):
" -l, --logfile= Specify log file.\n"
" --style= Set Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n"
+ " --data= Alternative user data path.\n"
" --headless Do not display GUI. Useful for testing scripts.\n"
).format(
appname = __package__,
@@ -120,6 +122,7 @@ def main(sysArgs=None):
toFile = False
toStd = True
confPath = None
+ dataPath = None
testMode = False
qtStyle = "Fusion"
cmdOpen = None
@@ -157,6 +160,8 @@ def main(sysArgs=None):
qtStyle = inArg
elif inOpt in ("--config"):
confPath = inArg
+ elif inOpt in ("--data"):
+ dataPath = inArg
elif inOpt in ("--testmode"):
testMode = True
@@ -187,7 +192,7 @@ def main(sysArgs=None):
logger.setLevel(debugLevel)
- CONFIG.initConfig(confPath)
+ CONFIG.initConfig(confPath, dataPath)
if testMode:
nwGUI = GuiMain()
diff --git a/nw/config.py b/nw/config.py
index be5589e7..584e05c4 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -117,9 +117,6 @@ class Config:
self.showRefPanel = True
self.viewComments = True
- ## Path
- self.recentList = [""]*10
-
# Check Qt5 Versions
verQt = splitVersionNumber(QT_VERSION_STR)
self.verQtString = QT_VERSION_STR
@@ -172,7 +169,10 @@ class Config:
# Actions
##
- def initConfig(self, confPath=None):
+ def initConfig(self, confPath=None, dataPath=None):
+ """Initialise the config class. The manual setting of confPath
+ and dataPath is mainly intended for the test suite.
+ """
if confPath is None:
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
@@ -181,11 +181,15 @@ class Config:
logger.info("Setting config from alternative path: %s" % confPath)
self.confPath = confPath
- if self.verQtValue >= 50400:
- dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
+ if dataPath is None:
+ if self.verQtValue >= 50400:
+ dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
+ else:
+ dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
+ self.dataPath = path.join(path.abspath(dataRoot), self.appHandle)
else:
- dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
- self.dataPath = path.join(path.abspath(dataRoot), self.appHandle)
+ logger.info("Setting data path from alternative path: %s" % dataPath)
+ self.dataPath = dataPath
logger.verbose("Config path: %s" % self.confPath)
logger.verbose("Data path: %s" % self.dataPath)
@@ -571,6 +575,15 @@ class Config:
self.confFile = path.basename(newPath)
return True
+ def setDataPath(self, newPath):
+ if newPath is None:
+ return True
+ if not path.isdir(newPath):
+ logger.error("Config: Path not found. Using default data path instead.")
+ return False
+ self.dataPath = path.dirname(newPath)
+ return True
+
def setLastPath(self, lastPath):
if lastPath is None or lastPath == "":
self.lastPath = ""
diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf
index 79fb70a7..bac82e44 100644
--- a/tests/reference/novelwriter.conf
+++ b/tests/reference/novelwriter.conf
@@ -1,5 +1,5 @@
[Main]
-timestamp = 2019-11-19 21:49:29
+timestamp = 2020-02-26 18:10:35
theme = default
syntax = default_light
guidark = False
@@ -49,14 +49,4 @@ viewcomments = True
[Path]
lastpath =
-recent0 =
-recent1 =
-recent2 =
-recent3 =
-recent4 =
-recent5 =
-recent6 =
-recent7 =
-recent8 =
-recent9 =
diff --git a/tests/test_config.py b/tests/test_config.py
index 4c4c2523..81d7db7d 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -13,7 +13,7 @@ theConf = Config()
def testConfigInit(nwTemp,nwRef):
tmpConf = path.join(nwTemp,"novelwriter.conf")
refConf = path.join(nwRef, "novelwriter.conf")
- assert theConf.initConfig(nwTemp)
+ assert theConf.initConfig(nwTemp, nwTemp)
assert theConf.setLastPath("")
assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2])
diff --git a/tests/test_gui.py b/tests/test_gui.py
index dc279222..b039a9d8 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -18,8 +18,8 @@ keyDelay = 10
stepDelay = 50
@pytest.mark.gui
-def testMainWindows(qtbot, nwTempGUI, nwRef):
- nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
+def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
+ nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -254,8 +254,8 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
# qtbot.stopForInteraction()
@pytest.mark.gui
-def testTimeLineView(qtbot, nwTempGUI, nwRef):
- nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
+def testTimeLineView(qtbot, nwTempGUI, nwRef, nwTemp):
+ nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -276,8 +276,8 @@ def testTimeLineView(qtbot, nwTempGUI, nwRef):
nwGUI.closeMain()
@pytest.mark.gui
-def testProjectEditor(qtbot, nwTempGUI, nwRef):
- nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
+def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
+ nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -363,8 +363,8 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef):
# qtbot.stopForInteraction()
@pytest.mark.gui
-def testItemEditor(qtbot, nwTempGUI, nwRef):
- nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
+def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
+ nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
diff --git a/tests/test_project.py b/tests/test_project.py
index 907d0e9f..c2f4078d 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -22,10 +22,10 @@ theProject = NWProject(theMain)
theProject.handleSeed = 42
@pytest.mark.project
-def testProjectNew(nwTempProj,nwRef):
+def testProjectNew(nwTempProj,nwRef,nwTemp):
projFile = path.join(nwTempProj,"nwProject.nwx")
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
- assert theConf.initConfig(nwRef)
+ assert theConf.initConfig(nwRef, nwTemp)
assert theProject.newProject()
assert theProject.setProjectPath(nwTempProj)
assert theProject.saveProject()
From 6a330afc222fbd4767ca627a6af402ee39ff6453 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 20:26:01 +0100
Subject: [PATCH 56/60] Increase test coverage of Config class and fix a couple
of bugs in the process
---
nw/config.py | 56 ++++++++++++++++++++++++++------------------
tests/test_config.py | 38 +++++++++++++++++++++++++++++-
2 files changed, 70 insertions(+), 24 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index 584e05c4..3149edef 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -220,28 +220,30 @@ class Config:
self.confPath = None
# Check if config file exists
- if path.isfile(path.join(self.confPath,self.confFile)):
- # If it exists, load it
- self.loadConfig()
- else:
- # If it does not exist, save a copy of the default values
- self.saveConfig()
-
- # Load re3cent projects cache
- self.loadRecentCache()
+ if self.confPath is not None:
+ if path.isfile(path.join(self.confPath,self.confFile)):
+ # If it exists, load it
+ self.loadConfig()
+ else:
+ # If it does not exist, save a copy of the default values
+ self.saveConfig()
# If data folder does not exist, make it.
# This assumes that the os data folder itself exists.
- if not path.isdir(self.dataPath):
- try:
- mkdir(self.dataPath)
- except Exception as e:
- logger.error("Could not create folder: %s" % self.dataPath)
- logger.error(str(e))
- self.hasError = True
- self.errData.append("Could not create folder: %s" % self.dataPath)
- self.errData.append(str(e))
- self.dataPath = None
+ if self.dataPath is not None:
+ if not path.isdir(self.dataPath):
+ try:
+ mkdir(self.dataPath)
+ except Exception as e:
+ logger.error("Could not create folder: %s" % self.dataPath)
+ logger.error(str(e))
+ self.hasError = True
+ self.errData.append("Could not create folder: %s" % self.dataPath)
+ self.errData.append(str(e))
+ self.dataPath = None
+
+ # Load recent projects cache
+ self.loadRecentCache()
# Check the availability of optional packages
self._checkOptionalPackages()
@@ -496,6 +498,10 @@ class Config:
def loadRecentCache(self):
"""Load the cache file for recent projects.
"""
+
+ if self.dataPath is None:
+ return False
+
cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
self.recentProj = {}
@@ -533,6 +539,10 @@ class Config:
def saveRecentCache(self):
"""Save the cache dictionary of recent projects.
"""
+
+ if self.dataPath is None:
+ return False
+
cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
cacheTemp = path.join(self.dataPath, nwFiles.RECENT_FILE+"~")
@@ -581,7 +591,7 @@ class Config:
if not path.isdir(newPath):
logger.error("Config: Path not found. Using default data path instead.")
return False
- self.dataPath = path.dirname(newPath)
+ self.dataPath = path.abspath(newPath)
return True
def setLastPath(self, lastPath):
@@ -618,12 +628,12 @@ class Config:
def setShowRefPanel(self, checkState):
self.showRefPanel = checkState
self.confChanged = True
- return
+ return self.showRefPanel
def setViewComments(self, checkState):
self.viewComments = checkState
self.confChanged = True
- return
+ return self.viewComments
def getErrData(self):
errMessage = " ".join(self.errData)
@@ -667,7 +677,7 @@ class Config:
if checkVal is None:
return None
if isinstance(checkVal, str):
- if checkVal.lower == "none":
+ if checkVal.lower() == "none":
return None
return checkVal
diff --git a/tests/test_config.py b/tests/test_config.py
index 81d7db7d..a8562976 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -37,6 +37,14 @@ def testConfigSetConfPath(nwTemp):
assert theConf.confFile == "novelwriter.conf"
assert not theConf.confChanged
+@pytest.mark.core
+def testConfigSetDataPath(nwTemp):
+ assert theConf.setDataPath(None)
+ assert not theConf.setDataPath(path.join("somewhere","over","the","rainbow"))
+ assert theConf.setDataPath(nwTemp)
+ assert theConf.dataPath == nwTemp
+ assert not theConf.confChanged
+
@pytest.mark.core
def testConfigLoad():
assert theConf.loadConfig()
@@ -67,7 +75,7 @@ def testConfigSetTreeColWidths(nwTemp,nwRef):
assert not theConf.confChanged
@pytest.mark.core
-def testConfigSetMainPanePos(nwTemp,nwRef):
+def testConfigSetPanePos(nwTemp,nwRef):
tmpConf = path.join(nwTemp,"novelwriter.conf")
refConf = path.join(nwRef, "novelwriter.conf")
assert theConf.setMainPanePos([0, 0])
@@ -77,3 +85,31 @@ def testConfigSetMainPanePos(nwTemp,nwRef):
assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged
+
+@pytest.mark.core
+def testConfigFlags(nwTemp,nwRef):
+ tmpConf = path.join(nwTemp,"novelwriter.conf")
+ refConf = path.join(nwRef, "novelwriter.conf")
+ assert not theConf.setShowRefPanel(False)
+ assert theConf.setShowRefPanel(True)
+ assert not theConf.setViewComments(False)
+ assert theConf.setViewComments(True)
+ assert theConf.confChanged
+ assert theConf.saveConfig()
+ assert cmpFiles(tmpConf, refConf, [2])
+ assert not theConf.confChanged
+
+@pytest.mark.core
+def testConfigErrors(nwTemp):
+ nonPath = path.join("somewhere","over","the","rainbow")
+ assert theConf.initConfig(nonPath, nonPath)
+ assert theConf.hasError
+ assert not theConf.loadConfig()
+ assert not theConf.saveConfig()
+ assert not theConf.loadRecentCache()
+ assert len(theConf.getErrData()) > 0
+
+@pytest.mark.core
+def testConfigInternals():
+ assert theConf._checkNone(None) is None
+ assert theConf._checkNone("None") is None
From af410e0b15168c2399822f05008303413e0d8ed5 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 22:09:37 +0100
Subject: [PATCH 57/60] Added functions to read and write project lockfile
---
nw/config.py | 6 ++-
nw/constants/constants.py | 1 +
nw/project/project.py | 91 ++++++++++++++++++++++++++++++++++++++-
3 files changed, 96 insertions(+), 2 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index 3149edef..94c30f71 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -20,7 +20,7 @@ from os import path, mkdir, unlink, rename
from datetime import datetime
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.common import splitVersionNumber
@@ -156,6 +156,10 @@ class Config:
else:
self.osUnknown = True
+ # Other System Info
+ self.hostName = QSysInfo.machineHostName()
+ self.kernelVer = QSysInfo.kernelVersion()
+
# Packages
self.hasEnchant = False
self.hasSymSpell = False
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index 4cc7f040..b39830bc 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -23,6 +23,7 @@ class nwFiles():
APP_ICON = "novelWriter.svg"
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
+ PROJ_LOCK = "nwProject.lock"
SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
diff --git a/nw/project/project.py b/nw/project/project.py
index 887fe38c..afde1e02 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -40,6 +40,7 @@ class NWProject():
self.projOpened = None # The time stamp of when the project file was opened
self.projChanged = None # The project has unsaved changes
self.projAltered = None # The project has been altered this session
+ self.lockedBy = None # Data on which computer has the project open
# Debug
self.handleSeed = None
@@ -178,7 +179,7 @@ class NWProject():
return
- def openProject(self, fileName):
+ def openProject(self, fileName, overrideLock=False):
"""Open the project file provided, or if 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
@@ -201,6 +202,21 @@ class NWProject():
if not self._checkFolder(self.projMeta):
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:
projectMaintenance(self)
except Exception as E:
@@ -308,6 +324,7 @@ class NWProject():
self.setProjectChanged(False)
self.projOpened = time()
self.projAltered = False
+ self._writeLockFile()
return True
@@ -398,14 +415,19 @@ class NWProject():
self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
self.mainConf.saveRecentCache()
+ self._writeLockFile()
self.theParent.setStatus("Saved Project: %s" % self.projName)
self.setProjectChanged(False)
return True
def closeProject(self):
+ """Close the current project and clear all meta data.
+ """
self._appendSessionStats()
+ self._clearLockFile()
self.clearProject()
+ self.lockedBy = None
return True
##
@@ -645,6 +667,73 @@ class NWProject():
# 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):
if not path.isdir(thePath):
try:
From 99034619638546048445d763cfe42efdc718c2a6 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 22:09:59 +0100
Subject: [PATCH 58/60] Added lockfile check and dialog box to main GUI
---
nw/guimain.py | 44 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 40 insertions(+), 4 deletions(-)
diff --git a/nw/guimain.py b/nw/guimain.py
index a9cd7edb..a34f8f28 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -15,6 +15,7 @@ import time
import nw
from os import path
+from datetime import datetime
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence
@@ -49,9 +50,9 @@ class GuiMain(QMainWindow):
self.hasProject = False
self.isZenMode = False
- logger.info("OS: %s" % (
- self.mainConf.osType)
- )
+ logger.info("OS: %s" % self.mainConf.osType)
+ logger.info("Kernel: %s" % self.mainConf.kernelVer)
+ logger.info("Host: %s" % self.mainConf.hostName)
logger.info("Qt5 Version: %s (%d)" % (
self.mainConf.verQtString, self.mainConf.verQtValue)
)
@@ -319,7 +320,42 @@ class GuiMain(QMainWindow):
# Try to open the project
if not self.theProject.openProject(projFile):
- return False
+ if self.theProject.lockedBy is not None:
+ if self.mainConf.showGUI:
+ try:
+ lockDetails = (
+ "
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?
"
+ "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
self.hasProject = True
From 8f1c2daddf992c21d12b2de3aba08e9ef32ac325 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 22:10:09 +0100
Subject: [PATCH 59/60] Fixed tests
---
tests/reference/proj/2_nwProject.nwx | 10 +++++-----
tests/test_project.py | 17 +++++++++++++++++
2 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx
index b78c14a4..0a8e6b4d 100644
--- a/tests/reference/proj/2_nwProject.nwx
+++ b/tests/reference/proj/2_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
@@ -72,28 +72,28 @@
00
-
+ TimelineROOTTIMELINENewFalse
-
+ ObjectROOTOBJECTNewFalse
-
+ Custom1ROOTCUSTOMNewFalse
-
+ Custom2ROOTCUSTOM
diff --git a/tests/test_project.py b/tests/test_project.py
index c2f4078d..6723dcc7 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -29,6 +29,7 @@ def testProjectNew(nwTempProj,nwRef,nwTemp):
assert theProject.newProject()
assert theProject.setProjectPath(nwTempProj)
assert theProject.saveProject()
+ assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2])
@pytest.mark.project
@@ -41,9 +42,21 @@ def testProjectSave(nwTempProj,nwRef):
projFile = path.join(nwTempProj,"nwProject.nwx")
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
assert theProject.saveProject()
+ assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2])
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
def testProjectNewRoot(nwTempProj,nwRef):
projFile = path.join(nwTempProj,"nwProject.nwx")
@@ -59,6 +72,7 @@ def testProjectNewRoot(nwTempProj,nwRef):
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
assert theProject.projChanged
assert theProject.saveProject()
+ assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2])
assert not theProject.projChanged
@@ -96,6 +110,8 @@ def testIndexScanThis(nwTempProj):
assert str(theBits) == "['@tag', 'this', 'and this']"
assert str(thePos) == "[0, 6, 12]"
+ assert theProject.closeProject()
+
@pytest.mark.project
def testBuildIndex(nwTempProj):
projFile = path.join(nwTempProj,"nwProject.nwx")
@@ -117,3 +133,4 @@ def testBuildIndex(nwTempProj):
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.novelOrder) == "['31489056e0916:1', '31489056e0916:3', '31489056e0916:5', '31489056e0916:7']"
+ assert theProject.closeProject()
From 144ddef579582c9fd9ab04e6420e5ec374a9448f Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 22:18:34 +0100
Subject: [PATCH 60/60] Backup tool should not back up the lock file
---
nw/project/backup.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/nw/project/backup.py b/nw/project/backup.py
index bab5ffcc..1f2bd985 100644
--- a/nw/project/backup.py
+++ b/nw/project/backup.py
@@ -57,7 +57,9 @@ class NWBackup():
baseName = path.join(self.mainConf.backupPath, archName)
try:
+ self.theProject._clearLockFile()
make_archive(baseName, "zip", self.theProject.projPath, ".")
+ self.theProject._writeLockFile()
except Exception as e:
self.theParent.makeAlert(
["Could not write backup archive.",str(e)],