From cf3350c6b69113c50aac58166f32f9bbdc4b0a87 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 20 Dec 2020 22:07:10 +0100
Subject: [PATCH 01/10] Trash and Orphaned folders shouldn't be editable
---
nw/constants/constants.py | 7 ++++++-
nw/gui/projtree.py | 21 +++++++++++++--------
nw/guimain.py | 12 +++++++++++-
3 files changed, 30 insertions(+), 10 deletions(-)
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index a768da9d..8a82f676 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -25,7 +25,9 @@
along with this program. If not, see .
"""
-from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline
+from nw.constants.enum import (
+ nwItemClass, nwItemLayout, nwItemType, nwOutline
+)
class nwConst():
@@ -43,6 +45,9 @@ class nwConst():
SP_INTERNAL = "internal"
SP_ENCHANT = "enchant"
+ # Check Lists
+ REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
+
# END Class nwConst
class nwRegEx():
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 2c52c4cb..51ef8005 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -687,9 +687,11 @@ class GuiProjectTree(QTreeWidget):
selItem = self.itemAt(clickPos)
if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
- tItem = self.theProject.projTree[tHandle]
- self.setSelectedHandle(tHandle) # Just to be safe
+ if tHandle is None:
+ return
+ self.setSelectedHandle(tHandle) # Just to be safe
+ tItem = self.theProject.projTree[tHandle]
if self.ctxMenu.filterActions(tItem):
# Only open menu if any actions remain after filter
self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
@@ -718,6 +720,9 @@ class GuiProjectTree(QTreeWidget):
return
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
+ if tHandle is None:
+ return
+
tItem = self.theProject.projTree[tHandle]
if tItem is None:
return
@@ -909,16 +914,16 @@ class GuiProjectTree(QTreeWidget):
"""
if self.orphRoot is None:
newItem = QTreeWidgetItem([""]*4)
- newItem.setText(self.C_NAME, "Orphaned Files")
- newItem.setText(self.C_COUNT, "")
+ newItem.setText(self.C_NAME, "Orphaned Files")
+ newItem.setData(self.C_NAME, Qt.UserRole, None)
+ newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_orphan"))
+ newItem.setText(self.C_COUNT, "")
+ newItem.setData(self.C_COUNT, Qt.UserRole, 0)
newItem.setText(self.C_EXPORT, "")
- newItem.setText(self.C_FLAGS, "")
+ newItem.setText(self.C_FLAGS, "")
self.addTopLevelItem(newItem)
self.orphRoot = newItem
newItem.setExpanded(True)
- newItem.setData(self.C_NAME, Qt.UserRole, "")
- newItem.setData(self.C_COUNT, Qt.UserRole, 0)
- newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_orphan"))
return
diff --git a/nw/guimain.py b/nw/guimain.py
index 4361ae8f..7e50cfef 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -47,7 +47,7 @@ from nw.gui import (
GuiTheme, GuiWritingStats
)
from nw.core import NWProject, NWDoc, NWIndex
-from nw.constants import nwItemType, nwItemClass, nwAlert
+from nw.constants import nwItemType, nwItemClass, nwAlert, nwConst
from nw.common import getGuiItem
logger = logging.getLogger(__name__)
@@ -722,6 +722,12 @@ class GuiMain(QMainWindow):
logger.warning("No item selected")
return
+ tItem = self.theProject.projTree[tHandle]
+ if tItem is None:
+ return
+ if tItem.itemType not in nwConst.REG_TYPES:
+ return
+
logger.verbose("Requesting change to item %s" % tHandle)
dlgProj = GuiItemEditor(self, self.theProject, tHandle)
dlgProj.exec_()
@@ -1312,6 +1318,9 @@ class GuiMain(QMainWindow):
we open it. Otherwise, we do nothing.
"""
tHandle = tItem.data(self.treeView.C_NAME, Qt.UserRole)
+ if tHandle is None:
+ return
+
logger.verbose("User double clicked tree item with handle %s" % tHandle)
nwItem = self.theProject.projTree[tHandle]
if nwItem is not None:
@@ -1320,6 +1329,7 @@ class GuiMain(QMainWindow):
self.openDocument(tHandle, changeFocus=False, doScroll=False)
else:
logger.verbose("Requested item %s is a folder" % tHandle)
+
return
def _treeKeyPressReturn(self):
From 1ea12fedb91e1c351e40624d4de39d3a9ff92f6c Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 20 Dec 2020 22:08:27 +0100
Subject: [PATCH 02/10] Don't force string format on file meta data (must be
able to handle NoneType)
---
nw/core/document.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/nw/core/document.py b/nw/core/document.py
index 7ae8f4de..67cd1253 100644
--- a/nw/core/document.py
+++ b/nw/core/document.py
@@ -149,9 +149,9 @@ class NWDoc():
docMeta = ""
else:
docMeta = (
- f"%%~name: {self._theItem.itemName:s}\n"
- f"%%~path: {self._theItem.itemParent:s}/{self._theItem.itemHandle:s}\n"
- f"%%~kind: {self._theItem.itemClass.name:s}/{self._theItem.itemLayout.name:s}\n"
+ f"%%~name: {self._theItem.itemName}\n"
+ f"%%~path: {self._theItem.itemParent}/{self._theItem.itemHandle}\n"
+ f"%%~kind: {self._theItem.itemClass.name}/{self._theItem.itemLayout.name}\n"
)
try:
From 6a188a5906caa393685a4ca5b5e92b87640bf4b8 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 21 Dec 2020 01:28:19 +0100
Subject: [PATCH 03/10] Orphaned files are now put back in the tree, or
rejected if nowhere to put them
---
nw/core/project.py | 19 +++++++++++++------
nw/core/tree.py | 5 +++++
nw/gui/projtree.py | 9 ++++++---
3 files changed, 24 insertions(+), 9 deletions(-)
diff --git a/nw/core/project.py b/nw/core/project.py
index 8bfbaa74..f1af6d5c 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -1324,23 +1324,30 @@ class NWProject():
oClass = None
oLayout = None
if aDoc.openDocument(oHandle, showStatus=False, isOrphan=True) is not None:
- oName, _, oClass, oLayout = aDoc.getMeta()
+ oName, oParent, oClass, oLayout = aDoc.getMeta()
- if oName == "":
+ if oName:
+ oName = "Recovered: %s" % oName.lstrip("Recovered: ")
+ else:
nOrph += 1
- oName = "Orphaned File %d" % nOrph
+ oName = "Recovered File %d" % nOrph
if oClass is None:
- oClass = nwItemClass.NO_CLASS
+ oClass = nwItemClass.NOVEL
if oLayout is None:
- oLayout = nwItemLayout.NO_LAYOUT
+ oLayout = nwItemLayout.NOTE
+
+ if oParent is None or not self.projTree.isValid(oParent):
+ oParent = self.projTree.findRoot(oClass)
+ if oParent is None:
+ oParent = self.projTree.findRoot(nwItemClass.NOVEL)
orphItem = NWItem(self)
orphItem.setName(oName)
orphItem.setType(nwItemType.FILE)
orphItem.setClass(oClass)
orphItem.setLayout(oLayout)
- self.projTree.append(oHandle, None, orphItem)
+ self.projTree.append(oHandle, oParent, orphItem)
return True
diff --git a/nw/core/tree.py b/nw/core/tree.py
index 43678b73..6e103ec1 100644
--- a/nw/core/tree.py
+++ b/nw/core/tree.py
@@ -294,6 +294,11 @@ class NWTree():
tTree.append(tHandle)
return tTree
+ def isValid(self, tHandle):
+ """Check if a handle exists in the project.
+ """
+ return tHandle in self._treeOrder
+
##
# Setters
##
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 51ef8005..748d9ed8 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -851,7 +851,6 @@ class GuiProjectTree(QTreeWidget):
newItem.setData(self.C_NAME, Qt.UserRole, tHandle)
newItem.setData(self.C_COUNT, Qt.UserRole, 0)
- self.theMap[tHandle] = newItem
if pHandle is None:
if nwItem.itemType == nwItemType.ROOT:
self.addTopLevelItem(newItem)
@@ -859,8 +858,11 @@ class GuiProjectTree(QTreeWidget):
elif nwItem.itemType == nwItemType.TRASH:
self.addTopLevelItem(newItem)
else:
- self._addOrphanedRoot()
- self.orphRoot.addChild(newItem)
+ self.makeAlert(
+ "There is nowhere to add file with name '%s'" % nwItem.itemName, nwAlert.ERROR
+ )
+ # self._addOrphanedRoot()
+ # self.orphRoot.addChild(newItem)
else:
byIndex = -1
if nHandle is not None and nHandle in self.theMap:
@@ -874,6 +876,7 @@ class GuiProjectTree(QTreeWidget):
self.theMap[pHandle].addChild(newItem)
self.propagateCount(tHandle, nwItem.wordCount)
+ self.theMap[tHandle] = newItem
self.setTreeItemValues(tHandle)
newItem.setExpanded(nwItem.isExpanded)
From 414f4d3a6e8a5d86042ce0be7a7158c5adbdb892 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 21 Dec 2020 01:39:18 +0100
Subject: [PATCH 04/10] Remove code related to orphaned files
---
.../icons/typicons_colour_dark/icons.conf | 1 -
.../icons/typicons_colour_dark/warning.svg | 31 -------
.../icons/typicons_colour_light/icons.conf | 1 -
.../icons/typicons_colour_light/warning.svg | 31 -------
nw/assets/icons/typicons_grey_dark/icons.conf | 1 -
.../icons/typicons_grey_dark/warning.svg | 31 -------
.../icons/typicons_grey_light/icons.conf | 1 -
.../icons/typicons_grey_light/warning.svg | 31 -------
nw/gui/projtree.py | 82 +++----------------
nw/gui/theme.py | 1 -
tests/test_gui_projtree.py | 5 +-
11 files changed, 13 insertions(+), 203 deletions(-)
delete mode 100644 nw/assets/icons/typicons_colour_dark/warning.svg
delete mode 100644 nw/assets/icons/typicons_colour_light/warning.svg
delete mode 100644 nw/assets/icons/typicons_grey_dark/warning.svg
delete mode 100644 nw/assets/icons/typicons_grey_light/warning.svg
diff --git a/nw/assets/icons/typicons_colour_dark/icons.conf b/nw/assets/icons/typicons_colour_dark/icons.conf
index 927da636..d4671820 100644
--- a/nw/assets/icons/typicons_colour_dark/icons.conf
+++ b/nw/assets/icons/typicons_colour_dark/icons.conf
@@ -29,7 +29,6 @@ cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
-proj_orphan = warning.svg
doc_h1 = heading1.svg
doc_h2 = heading2.svg
doc_h3 = heading3.svg
diff --git a/nw/assets/icons/typicons_colour_dark/warning.svg b/nw/assets/icons/typicons_colour_dark/warning.svg
deleted file mode 100644
index eef5a902..00000000
--- a/nw/assets/icons/typicons_colour_dark/warning.svg
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
diff --git a/nw/assets/icons/typicons_colour_light/icons.conf b/nw/assets/icons/typicons_colour_light/icons.conf
index 1a97029f..4f9ffb6c 100644
--- a/nw/assets/icons/typicons_colour_light/icons.conf
+++ b/nw/assets/icons/typicons_colour_light/icons.conf
@@ -29,7 +29,6 @@ cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
-proj_orphan = warning.svg
doc_h1 = heading1.svg
doc_h2 = heading2.svg
doc_h3 = heading3.svg
diff --git a/nw/assets/icons/typicons_colour_light/warning.svg b/nw/assets/icons/typicons_colour_light/warning.svg
deleted file mode 100644
index ad55f227..00000000
--- a/nw/assets/icons/typicons_colour_light/warning.svg
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
diff --git a/nw/assets/icons/typicons_grey_dark/icons.conf b/nw/assets/icons/typicons_grey_dark/icons.conf
index f9c8c49d..7dae46e9 100644
--- a/nw/assets/icons/typicons_grey_dark/icons.conf
+++ b/nw/assets/icons/typicons_grey_dark/icons.conf
@@ -29,7 +29,6 @@ cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
-proj_orphan = warning.svg
doc_h1 = heading1.svg
doc_h2 = heading2.svg
doc_h3 = heading3.svg
diff --git a/nw/assets/icons/typicons_grey_dark/warning.svg b/nw/assets/icons/typicons_grey_dark/warning.svg
deleted file mode 100644
index 3753c9f4..00000000
--- a/nw/assets/icons/typicons_grey_dark/warning.svg
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
diff --git a/nw/assets/icons/typicons_grey_light/icons.conf b/nw/assets/icons/typicons_grey_light/icons.conf
index 732486a5..f21000cb 100644
--- a/nw/assets/icons/typicons_grey_light/icons.conf
+++ b/nw/assets/icons/typicons_grey_light/icons.conf
@@ -29,7 +29,6 @@ cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
-proj_orphan = warning.svg
doc_h1 = heading1.svg
doc_h2 = heading2.svg
doc_h3 = heading3.svg
diff --git a/nw/assets/icons/typicons_grey_light/warning.svg b/nw/assets/icons/typicons_grey_light/warning.svg
deleted file mode 100644
index 86f67929..00000000
--- a/nw/assets/icons/typicons_grey_light/warning.svg
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 748d9ed8..28af1b0c 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -62,7 +62,6 @@ class GuiProjectTree(QTreeWidget):
# Tree Settings
self.theMap = None
- self.orphRoot = None
self.treeChanged = False
self.ctxMenu = GuiProjectTreeMenu(self)
@@ -149,7 +148,6 @@ class GuiProjectTree(QTreeWidget):
"""
self.clear()
self.theMap = {}
- self.orphRoot = None
return
def newTreeItem(self, itemType, itemClass):
@@ -270,6 +268,9 @@ class GuiProjectTree(QTreeWidget):
"""
nwItem = self.theProject.projTree[tHandle]
trItem = self._addTreeItem(nwItem, nHandle)
+ if trItem is None:
+ return False
+
pHandle = nwItem.itemParent
if pHandle is not None and pHandle in self.theMap:
self.theMap[pHandle].setExpanded(True)
@@ -326,8 +327,6 @@ class GuiProjectTree(QTreeWidget):
"""
theList = []
for i in range(self.topLevelItemCount()):
- if self.topLevelItem(i) == self.orphRoot:
- continue
theList = self._scanChildren(theList, self.topLevelItem(i), i)
logger.debug("Saving project tree item order")
self.theProject.setTreeOrder(theList)
@@ -603,13 +602,11 @@ class GuiProjectTree(QTreeWidget):
relevant values in the project and on the status bar. This call
is a fast way of getting this number, and depends on the
propagateCount function being called when it should to maintain
- the correct count. Orphan folder is not included in the total.
+ the correct count.
"""
nWords = 0
for n in range(self.topLevelItemCount()):
tItem = self.topLevelItem(n)
- if tItem == self.orphRoot:
- continue
nWords += int(tItem.data(self.C_COUNT, Qt.UserRole))
self.theProject.setProjectWordCount(nWords)
@@ -769,13 +766,7 @@ class GuiProjectTree(QTreeWidget):
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent)
-
- # Handle orphaned files differently than tracked files
- if isNone:
- self._moveOrphanedItem(sHandle, dHandle)
- self._cleanOrphanedRoot()
- else:
- self._updateItemParent(sHandle)
+ self._updateItemParent(sHandle)
# If the item does not have the same class as the target,
# and the target is not a free root folder, update its class
@@ -861,8 +852,7 @@ class GuiProjectTree(QTreeWidget):
self.makeAlert(
"There is nowhere to add file with name '%s'" % nwItem.itemName, nwAlert.ERROR
)
- # self._addOrphanedRoot()
- # self.orphRoot.addChild(newItem)
+ return None
else:
byIndex = -1
if nHandle is not None and nHandle in self.theMap:
@@ -906,39 +896,12 @@ class GuiProjectTree(QTreeWidget):
trItem = self._addTreeItem(
self.theProject.projTree[trashHandle]
)
- trItem.setExpanded(True)
- self._setTreeChanged(True)
+ if trItem is not None:
+ trItem.setExpanded(True)
+ self._setTreeChanged(True)
return trItem
- def _addOrphanedRoot(self):
- """Add the special Orphaned Files root item to hold non-root
- items with no parent set.
- """
- if self.orphRoot is None:
- newItem = QTreeWidgetItem([""]*4)
- newItem.setText(self.C_NAME, "Orphaned Files")
- newItem.setData(self.C_NAME, Qt.UserRole, None)
- newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_orphan"))
- newItem.setText(self.C_COUNT, "")
- newItem.setData(self.C_COUNT, Qt.UserRole, 0)
- newItem.setText(self.C_EXPORT, "")
- newItem.setText(self.C_FLAGS, "")
- self.addTopLevelItem(newItem)
- self.orphRoot = newItem
- newItem.setExpanded(True)
-
- return
-
- def _cleanOrphanedRoot(self):
- """Remove the special Orphaned Files root folder if it is empty.
- """
- if self.orphRoot is not None:
- if self.orphRoot.childCount() == 0:
- self.takeTopLevelItem(self.indexOfTopLevelItem(self.orphRoot))
- self.orphRoot = None
- return
-
def _updateItemParent(self, tHandle):
"""Update the parent handle of an item so that the information
in the project is consistent with the treeView.
@@ -959,27 +922,6 @@ class GuiProjectTree(QTreeWidget):
return True
- def _moveOrphanedItem(self, tHandle, dHandle):
- """Move an Orphaned Item to a new dHandle parent item. This
- function will set all the missing meta data based on the meta
- data of the destination item.
- """
- trItemS = self._getTreeItem(tHandle)
- nwItemS = self.theProject.projTree[tHandle]
- nwItemD = self.theProject.projTree[dHandle]
- trItemP = trItemS.parent()
- nwItemS.setClass(nwItemD.itemClass)
- if trItemP is None:
- logger.error("Failed to find new parent item of %s" % tHandle)
- return False
-
- pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
- nwItemS.setParent(pHandle)
- self.setTreeItemValues(tHandle)
- self._setTreeChanged(True)
-
- return True
-
def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project.
"""
@@ -1055,12 +997,10 @@ class GuiProjectTreeMenu(QMenu):
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE
- isOrph = isFile and theItem.itemParent is None
- allowEdit = not (isTrash or isOrph)
- allowNew = not (isTrash or inTrash or isOrph)
+ allowNew = not (isTrash or inTrash)
- self.editItem.setVisible(allowEdit)
+ self.editItem.setVisible(not isTrash)
self.openItem.setVisible(isFile)
self.viewItem.setVisible(isFile)
self.toggleExp.setVisible(isFile)
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index c2b4b9fd..cd691aa2 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -524,7 +524,6 @@ class GuiIcons:
"cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
"proj_document" : (QStyle.SP_FileIcon, "x-office-document"),
"proj_folder" : (QStyle.SP_DirIcon, "folder"),
- "proj_orphan" : (QStyle.SP_MessageBoxWarning, "dialog-warning"),
"proj_nwx" : (None, None),
"status_lang" : (None, None),
"status_time" : (None, None),
diff --git a/tests/test_gui_projtree.py b/tests/test_gui_projtree.py
index 94726348..a663b18a 100644
--- a/tests/test_gui_projtree.py
+++ b/tests/test_gui_projtree.py
@@ -127,11 +127,10 @@ def testGuiProjTree_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
nwGUI.openProject(nwMinimal)
# Check that the orphaned file was found and added to the tree
- assert nwTree.orphRoot is not None
nwTree.flushTreeOrder()
- assert "1234567890abc" not in nwGUI.theProject.projTree._treeOrder
+ assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder
orItem = nwTree._getTreeItem("1234567890abc")
- assert orItem.text(nwTree.C_NAME) == "Orphaned File 1"
+ assert orItem.text(nwTree.C_NAME) == "Recovered File 1"
# qtbot.stopForInteraction()
From dc160772054edc6874eaac8321284328b0371338 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 21 Dec 2020 01:42:57 +0100
Subject: [PATCH 05/10] Fix test
---
tests/test_core_project.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/tests/test_core_project.py b/tests/test_core_project.py
index 3abefbbc..57ee8f51 100644
--- a/tests/test_core_project.py
+++ b/tests/test_core_project.py
@@ -923,9 +923,9 @@ def testCoreProject_OrphanedFiles(dummyGUI, nwLipsum):
# First Item with Meta Data
oItem = theProject.projTree["636b6aa9b697b"]
assert oItem is not None
- assert oItem.itemName == "Mars"
+ assert oItem.itemName == "Recovered: Mars"
assert oItem.itemHandle == "636b6aa9b697b"
- assert oItem.itemParent is None
+ assert oItem.itemParent == "60bdf227455cc"
assert oItem.itemClass == nwItemClass.WORLD
assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NOTE
@@ -933,12 +933,12 @@ def testCoreProject_OrphanedFiles(dummyGUI, nwLipsum):
# Second Item without Meta Data
oItem = theProject.projTree["736b6aa9b697b"]
assert oItem is not None
- assert oItem.itemName == "Orphaned File 1"
+ assert oItem.itemName == "Recovered File 1"
assert oItem.itemHandle == "736b6aa9b697b"
- assert oItem.itemParent is None
- assert oItem.itemClass == nwItemClass.NO_CLASS
+ assert oItem.itemParent == "b3643d0f92e32"
+ assert oItem.itemClass == nwItemClass.NOVEL
assert oItem.itemType == nwItemType.FILE
- assert oItem.itemLayout == nwItemLayout.NO_LAYOUT
+ assert oItem.itemLayout == nwItemLayout.NOTE
assert theProject.saveProject(nwLipsum)
assert theProject.closeProject()
From 45095452142b15678e6a3cf40b9f45ad9281e2b7 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 21 Dec 2020 11:03:47 +0100
Subject: [PATCH 06/10] Some minor cleanup and fixes
---
nw/core/project.py | 17 ++++++++++-
nw/core/tree.py | 2 +-
nw/gui/projtree.py | 76 ++++++++++++++++++++++------------------------
3 files changed, 53 insertions(+), 42 deletions(-)
diff --git a/nw/core/project.py b/nw/core/project.py
index f1af6d5c..1af00534 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -1317,10 +1317,12 @@ class NWProject():
# Handle orphans
aDoc = NWDoc(self, self.theParent)
nOrph = 0
+ noWhere = False
for oHandle in orphanFiles:
# Look for meta data
oName = ""
+ oParent = None
oClass = None
oLayout = None
if aDoc.openDocument(oHandle, showStatus=False, isOrphan=True) is not None:
@@ -1332,16 +1334,23 @@ class NWProject():
nOrph += 1
oName = "Recovered File %d" % nOrph
+ # Recover file meta data
if oClass is None:
oClass = nwItemClass.NOVEL
+
if oLayout is None:
oLayout = nwItemLayout.NOTE
- if oParent is None or not self.projTree.isValid(oParent):
+ if oParent is None or not self.projTree.handleExists(oParent):
oParent = self.projTree.findRoot(oClass)
if oParent is None:
oParent = self.projTree.findRoot(nwItemClass.NOVEL)
+ # If the file still has no parent item, skip it
+ if oParent is None:
+ noWhere = True
+ continue
+
orphItem = NWItem(self)
orphItem.setName(oName)
orphItem.setType(nwItemType.FILE)
@@ -1349,6 +1358,12 @@ class NWProject():
orphItem.setLayout(oLayout)
self.projTree.append(oHandle, oParent, orphItem)
+ if noWhere:
+ self.makeAlert((
+ "One or more orphaned files could not be added back into the "
+ "project. Make sure at least a Novel root folder exists."
+ ), nwAlert.WARN)
+
return True
def _appendSessionStats(self):
diff --git a/nw/core/tree.py b/nw/core/tree.py
index 6e103ec1..0c59fd7f 100644
--- a/nw/core/tree.py
+++ b/nw/core/tree.py
@@ -294,7 +294,7 @@ class NWTree():
tTree.append(tHandle)
return tTree
- def isValid(self, tHandle):
+ def handleExists(self, tHandle):
"""Check if a handle exists in the project.
"""
return tHandle in self._treeOrder
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 28af1b0c..c3fe8c1d 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -61,7 +61,7 @@ class GuiProjectTree(QTreeWidget):
self.theIndex = theParent.theIndex
# Tree Settings
- self.theMap = None
+ self.theMap = {}
self.treeChanged = False
self.ctxMenu = GuiProjectTreeMenu(self)
@@ -144,10 +144,11 @@ class GuiProjectTree(QTreeWidget):
##
def clearTree(self):
- """Clear the GUI content and the related maps.
+ """Clear the GUI content and the related map.
"""
self.clear()
self.theMap = {}
+ self.treeChanged = False
return
def newTreeItem(self, itemType, itemClass):
@@ -390,14 +391,12 @@ class GuiProjectTree(QTreeWidget):
return False
msgYes = self.theParent.askQuestion(
- "Empty Trash", "Permanently delete %d file%s from Trash?" % (
- nTrash, "s" if nTrash > 1 else ""
- )
+ "Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash
)
if not msgYes:
return False
- logger.verbose("Deleting %d files from Trash" % nTrash)
+ logger.verbose("Deleting %d file(s) from Trash" % nTrash)
for tHandle in self.getTreeFromHandle(trashHandle):
if tHandle == trashHandle:
continue
@@ -637,11 +636,7 @@ class GuiProjectTree(QTreeWidget):
selected, return the first.
"""
selItem = self.selectedItems()
-
- if len(selItem) == 0:
- return None
-
- if isinstance(selItem[0], QTreeWidgetItem):
+ if selItem:
return selItem[0].data(self.C_NAME, Qt.UserRole)
return None
@@ -660,18 +655,21 @@ class GuiProjectTree(QTreeWidget):
def setSelectedHandle(self, tHandle, doScroll=False):
"""Set a specific handle as the selected item.
"""
- if tHandle in self.theMap:
- self.clearSelection()
- self.theMap[tHandle].setSelected(True)
+ if tHandle not in self.theMap:
+ return False
- selItems = self.selectedIndexes()
- if selItems and doScroll:
- self.scrollTo(
- selItems[0], QAbstractItemView.PositionAtCenter
- )
- return True
+ tItem = self._getTreeItem(tHandle)
+ if tItem is None:
+ return False
- return False
+ self.clearSelection()
+ self.theMap[tHandle].setSelected(True)
+
+ selItems = self.selectedIndexes()
+ if selItems and doScroll:
+ self.scrollTo(selItems[0], QAbstractItemView.PositionAtCenter)
+
+ return True
##
# Slots
@@ -684,14 +682,12 @@ class GuiProjectTree(QTreeWidget):
selItem = self.itemAt(clickPos)
if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
- if tHandle is None:
- return
-
self.setSelectedHandle(tHandle) # Just to be safe
tItem = self.theProject.projTree[tHandle]
- if self.ctxMenu.filterActions(tItem):
- # Only open menu if any actions remain after filter
- self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
+ if tItem is not None:
+ if self.ctxMenu.filterActions(tItem):
+ # Only open menu if any actions remain after filter
+ self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
return
@@ -770,7 +766,7 @@ class GuiProjectTree(QTreeWidget):
# If the item does not have the same class as the target,
# and the target is not a free root folder, update its class
- if not isSame and not onFree:
+ if not (isSame or onFree):
logger.debug("Item %s class has been changed from %s to %s" % (
sHandle,
snItem.itemClass.name,
@@ -803,9 +799,7 @@ class GuiProjectTree(QTreeWidget):
def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle.
"""
- if tHandle in self.theMap.keys():
- return self.theMap[tHandle]
- return None
+ return self.theMap.get(tHandle, None)
def _scanChildren(self, theList, theItem, theIndex):
"""This is a recursive function returning all items in a tree
@@ -829,19 +823,20 @@ class GuiProjectTree(QTreeWidget):
tClass = nwItem.itemClass
newItem = QTreeWidgetItem([""]*4)
- newItem.setText(self.C_NAME, "")
- newItem.setText(self.C_COUNT, "0")
+ newItem.setText(self.C_NAME, "")
+ newItem.setText(self.C_COUNT, "0")
newItem.setText(self.C_EXPORT, "")
- newItem.setText(self.C_FLAGS, "")
+ newItem.setText(self.C_FLAGS, "")
- newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
- newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
- newItem.setTextAlignment(self.C_EXPORT, Qt.AlignLeft | Qt.AlignVCenter)
- newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft | Qt.AlignVCenter)
+ newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft)
+ newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
+ newItem.setTextAlignment(self.C_EXPORT, Qt.AlignLeft)
+ newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft)
newItem.setData(self.C_NAME, Qt.UserRole, tHandle)
newItem.setData(self.C_COUNT, Qt.UserRole, 0)
+ self.theMap[tHandle] = newItem
if pHandle is None:
if nwItem.itemType == nwItemType.ROOT:
self.addTopLevelItem(newItem)
@@ -850,9 +845,11 @@ class GuiProjectTree(QTreeWidget):
self.addTopLevelItem(newItem)
else:
self.makeAlert(
- "There is nowhere to add file with name '%s'" % nwItem.itemName, nwAlert.ERROR
+ "There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
)
+ del self.theMap[tHandle]
return None
+
else:
byIndex = -1
if nHandle is not None and nHandle in self.theMap:
@@ -866,7 +863,6 @@ class GuiProjectTree(QTreeWidget):
self.theMap[pHandle].addChild(newItem)
self.propagateCount(tHandle, nwItem.wordCount)
- self.theMap[tHandle] = newItem
self.setTreeItemValues(tHandle)
newItem.setExpanded(nwItem.isExpanded)
From 6ad132e12da147869f1b8a87ede26a3bd14a8a5b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 21 Dec 2020 21:08:13 +0100
Subject: [PATCH 07/10] Clean up the logic of creating new project tree items,
and improve test coverage
---
nw/gui/projtree.py | 35 +++++-----
tests/test_gui_projtree.py | 128 ++++++++++++++++++++++++++++---------
2 files changed, 117 insertions(+), 46 deletions(-)
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index c3fe8c1d..938729a3 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -164,6 +164,11 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open")
return False
+ if not isinstance(itemType, nwItemType):
+ # This would indicate an internal bug
+ logger.error("No itemType provided")
+ return False
+
# The item needs to be assigned an item class, so one must be
# provided, or it must be possible to extract it from the parent
# item of the new item.
@@ -172,21 +177,18 @@ class GuiProjectTree(QTreeWidget):
if pItem is not None:
itemClass = pItem.itemClass
+ # If class is still not set, alert the user and exit
if itemClass is None:
- if itemType is not None:
- if itemType == nwItemType.FILE:
- self.makeAlert(
- "Please select a valid location in the tree to add a document.",
- nwAlert.ERROR
- )
- return False
- elif itemType == nwItemType.FOLDER:
- self.makeAlert(
- "Please select a valid location in the tree to add a folder.",
- nwAlert.ERROR
- )
- return False
- self.makeAlert("Failed to add new item.", nwAlert.BUG)
+ if itemType == nwItemType.FILE:
+ self.makeAlert(
+ "Please select a valid location in the tree to add the document.",
+ nwAlert.ERROR
+ )
+ else:
+ self.makeAlert(
+ "Please select a valid location in the tree to add the folder.",
+ nwAlert.ERROR
+ )
return False
# Everything is fine, we have what we need, so we proceed
@@ -259,8 +261,7 @@ class GuiProjectTree(QTreeWidget):
# Add the new item to the tree
if tHandle is not None:
self.revealNewTreeItem(tHandle, nHandle)
- if self.mainConf.showGUI:
- self.theParent.editItem(tHandle)
+ self.theParent.editItem(tHandle)
return True
@@ -287,7 +288,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open")
return False
- if qApp.focusWidget() != self and self.mainConf.showGUI:
+ if qApp.focusWidget() != self:
return False
tHandle = self.getSelectedHandle()
diff --git a/tests/test_gui_projtree.py b/tests/test_gui_projtree.py
index a663b18a..d49fa7a6 100644
--- a/tests/test_gui_projtree.py
+++ b/tests/test_gui_projtree.py
@@ -5,95 +5,133 @@
import pytest
import os
+from tools import writeFile
+
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QAction, QMessageBox
from nw.constants import nwItemType, nwItemClass
-keyDelay = 2
-typeDelay = 1
-stepDelay = 20
-
@pytest.mark.gui
-def testGuiProjTree_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
- """Test the project tree.
+def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
+ """Test adding and removing items from the project tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr("nw.guimain.GuiMain.editItem", lambda *args: None)
nwGUI.theProject.projTree.setSeed(42)
- assert nwGUI.openProject(nwMinimal)
nwTree = nwGUI.treeView
+ ##
+ # Add New Items
+ ##
+
+ # Try to add and move item with no project
+ assert not nwTree.newTreeItem(nwItemType.FILE, None)
+ assert not nwTree.moveTreeItem(1)
+
+ # Open a project
+ assert nwGUI.openProject(nwMinimal)
+
# No location selected for new item
+ nwTree.clearSelection()
assert not nwTree.newTreeItem(nwItemType.FILE, None)
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
+ assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
+
+ # No itemType set or ROOT, but no class
+ assert not nwTree.newTreeItem(None, None)
+ assert not nwTree.newTreeItem(nwItemType.ROOT, None)
# Select a location
chItem = nwTree._getTreeItem("a6d311a93600a")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
chItem.setExpanded(True)
- # Create new item with no class set
+ # Create new item with no class set (defaults to NOVEL)
assert nwTree.newTreeItem(nwItemType.FILE, None)
assert nwTree.newTreeItem(nwItemType.FOLDER, None)
+ # Check that we have the correct tree order
+ assert nwTree.getTreeFromHandle("a6d311a93600a") == [
+ "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
+ ]
+
# Add roots
- assert not nwTree.newTreeItem(nwItemType.ROOT, None) # Defaults to NOVEL
assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid
- # Check that we have the correct tree order
+ # Change max depth and try to add a subfolder that is too deep
+ monkeypatch.setattr("nw.constants.nwConst.MAX_DEPTH", 2)
+ chItem = nwTree._getTreeItem("71ee45a3c0db9")
+ nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
+ assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
+
+ ##
+ # Move Items
+ ##
+
+ nwTree.setSelectedHandle("8c659a11cd429")
+
+ # Shift focus and try to move item
+ monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: None)
+ assert not nwTree.moveTreeItem(1)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "73475cb40a568", "44cb730c42048"
+ "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
+ monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: nwTree)
# Move second item up twice (should give same result)
- nwTree.setSelectedHandle("8c659a11cd429")
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048"
+ "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048"
+ "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
]
- # Move it back down four times (last to should be the same)
+ # Move it back down four times (last two should be the same)
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "73475cb40a568", "44cb730c42048"
+ "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "8c659a11cd429", "44cb730c42048"
+ "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048", "8c659a11cd429"
+ "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048", "8c659a11cd429"
+ "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
# Move a root item (top level items are different) twice
nwTree.flushTreeOrder()
- assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 9
+ assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
nwTree.setSelectedHandle("9d5247ab588e0")
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
nwTree.flushTreeOrder()
- assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
+ assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
nwTree.flushTreeOrder()
- assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
+ assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
+
+ ##
+ # Delete and Trash
+ ##
# Add some content to the new file
nwGUI.openDocument("73475cb40a568")
nwGUI.docEditor.setText("# Hello World\n")
nwGUI.saveDocument()
+ nwGUI.saveProject()
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
# Delete the items we added earlier
@@ -102,11 +140,11 @@ def testGuiProjTree_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
assert not nwTree.deleteItem(None)
assert not nwTree.deleteItem("1111111111111")
assert nwTree.deleteItem("73475cb40a568") # New File
- assert nwTree.deleteItem("44cb730c42048") # New Folder
- assert nwTree.deleteItem("71ee45a3c0db9") # Custom Root
+ assert nwTree.deleteItem("71ee45a3c0db9") # New Folder
+ assert nwTree.deleteItem("811786ad1ae74") # Custom Root
assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder
- assert "44cb730c42048" not in nwGUI.theProject.projTree._treeOrder
assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder
+ assert "811786ad1ae74" not in nwGUI.theProject.projTree._treeOrder
# The file is in trash, empty it
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
@@ -115,13 +153,23 @@ def testGuiProjTree_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder
+ # Should not be allowed to add files and folders to Trash
+ trashHandle = nwGUI.theProject.projTree.trashRoot()
+ chItem = nwTree._getTreeItem(trashHandle)
+ nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
+ assert not nwTree.newTreeItem(nwItemType.FILE, None)
+ assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
+
# Close the project
nwGUI.closeProject()
+ ##
+ # Orphaned Files
+ ##
+
# Add an orphaned file
orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd")
- with open(orphFile, mode="w+", encoding="utf8") as outFile:
- outFile.write("# Hello World\n")
+ writeFile(orphFile, "# Hello World\n")
# Open the project again
nwGUI.openProject(nwMinimal)
@@ -132,6 +180,28 @@ def testGuiProjTree_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
orItem = nwTree._getTreeItem("1234567890abc")
assert orItem.text(nwTree.C_NAME) == "Recovered File 1"
- # qtbot.stopForInteraction()
+ ##
+ # Unexpected Error Handling
+ ##
-# END Test testGuiProjTree_Main
+ # Add an item with an invalid type
+ assert not nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL)
+ assert "Failed to add new item" in caplog.messages[-1]
+
+ # Add new file after one that has no parent handle
+ chItem = nwTree._getTreeItem("44cb730c42048")
+ nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
+ nwTree.theProject.projTree["44cb730c42048"].itemParent = None
+ assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
+ nwTree.clearSelection()
+
+ # Add a file with no parent, and fail to find a suitable parent item
+ monkeypatch.setattr("nw.core.tree.NWTree.findRoot", lambda *args: None)
+
+ assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
+ assert not nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL)
+
+ # qtbot.stopForInteraction()
+ nwGUI.closeProject()
+
+# END Test testGuiProjTree_TreeItems
From d008113eecb0a9335275d8348b47aab227f5c8a9 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 21 Dec 2020 21:08:49 +0100
Subject: [PATCH 08/10] Remove some more test flags in the main code
---
nw/gui/build.py | 40 +++++++++++++++++++---------------------
nw/guimain.py | 21 ++++++++++-----------
tests/test_base_init.py | 1 -
tests/test_gui_build.py | 4 +++-
4 files changed, 32 insertions(+), 34 deletions(-)
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 7902996d..c4a8fd8b 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -774,14 +774,13 @@ class GuiBuildNovel(QDialog):
if not os.path.isdir(saveDir):
saveDir = self.mainConf.homePath
- if self.mainConf.showGUI:
- dlgOpt = QFileDialog.Options()
- dlgOpt |= QFileDialog.DontUseNativeDialog
- savePath, _ = QFileDialog.getSaveFileName(
- self, "Save Document As", savePath, options=dlgOpt
- )
- if not savePath:
- return False
+ dlgOpt = QFileDialog.Options()
+ dlgOpt |= QFileDialog.DontUseNativeDialog
+ savePath, _ = QFileDialog.getSaveFileName(
+ self, "Save Document As", savePath, options=dlgOpt
+ )
+ if not savePath:
+ return False
self.mainConf.setLastPath(savePath)
@@ -887,19 +886,18 @@ class GuiBuildNovel(QDialog):
errMsg = "Unknown format"
# Report to user
- if self.mainConf.showGUI:
- if wSuccess:
- self.theParent.makeAlert(
- "%s file successfully written to: %s" % (
- textFmt, savePath
- ), nwAlert.INFO
- )
- else:
- self.theParent.makeAlert(
- "Failed to write %s file. %s" % (
- textFmt, errMsg
- ), nwAlert.ERROR
- )
+ if wSuccess:
+ self.theParent.makeAlert(
+ "%s file successfully written to: %s" % (
+ textFmt, savePath
+ ), nwAlert.INFO
+ )
+ else:
+ self.theParent.makeAlert(
+ "Failed to write %s file. %s" % (
+ textFmt, errMsg
+ ), nwAlert.ERROR
+ )
return wSuccess
diff --git a/nw/guimain.py b/nw/guimain.py
index 7e50cfef..37042ca5 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -971,17 +971,16 @@ class GuiMain(QMainWindow):
logger.error(msgLine)
# Popup
- if self.mainConf.showGUI:
- msgBox = QMessageBox()
- if theLevel == nwAlert.INFO:
- msgBox.information(self, "Information", popMsg)
- elif theLevel == nwAlert.WARN:
- msgBox.warning(self, "Warning", popMsg)
- elif theLevel == nwAlert.ERROR:
- msgBox.critical(self, "Error", popMsg)
- elif theLevel == nwAlert.BUG:
- popMsg += " This is a bug!"
- msgBox.critical(self, "Internal Error", popMsg)
+ msgBox = QMessageBox()
+ if theLevel == nwAlert.INFO:
+ msgBox.information(self, "Information", popMsg)
+ elif theLevel == nwAlert.WARN:
+ msgBox.warning(self, "Warning", popMsg)
+ elif theLevel == nwAlert.ERROR:
+ msgBox.critical(self, "Error", popMsg)
+ elif theLevel == nwAlert.BUG:
+ popMsg += " This is a bug!"
+ msgBox.critical(self, "Internal Error", popMsg)
return
diff --git a/tests/test_base_init.py b/tests/test_base_init.py
index b691bfa1..9a6daedf 100644
--- a/tests/test_base_init.py
+++ b/tests/test_base_init.py
@@ -61,7 +61,6 @@ def testBaseInit_Options(monkeypatch, tmpDir):
nwGUI = nw.main()
assert nw.logger.getEffectiveLevel() == logging.WARNING
assert nw.CONFIG.debugInfo is False
- assert nw.CONFIG.showGUI is False
assert nwGUI.closeMain() == "closeMain"
# Defaults
diff --git a/tests/test_gui_build.py b/tests/test_gui_build.py
index 701312f8..fbc887e3 100644
--- a/tests/test_gui_build.py
+++ b/tests/test_gui_build.py
@@ -9,7 +9,7 @@ from shutil import copyfile
from tools import cmpFiles, getGuiItem
from PyQt5.QtCore import Qt
-from PyQt5.QtWidgets import QAction, QMessageBox
+from PyQt5.QtWidgets import QAction, QMessageBox, QFileDialog
from nw.gui import GuiBuildNovel
@@ -23,6 +23,8 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda a, b, c, **kwargs: (c, None))
# Check that we cannot open when there is no project
nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger)
From fa319391c004bcd51ab07d15f7b30aa34eddd06b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 21 Dec 2020 21:25:04 +0100
Subject: [PATCH 09/10] Remove some now redundant checks
---
nw/gui/projtree.py | 3 ---
nw/guimain.py | 3 ---
2 files changed, 6 deletions(-)
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 938729a3..e1802a8b 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -714,9 +714,6 @@ class GuiProjectTree(QTreeWidget):
return
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
- if tHandle is None:
- return
-
tItem = self.theProject.projTree[tHandle]
if tItem is None:
return
diff --git a/nw/guimain.py b/nw/guimain.py
index 37042ca5..c03eceea 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -1317,9 +1317,6 @@ class GuiMain(QMainWindow):
we open it. Otherwise, we do nothing.
"""
tHandle = tItem.data(self.treeView.C_NAME, Qt.UserRole)
- if tHandle is None:
- return
-
logger.verbose("User double clicked tree item with handle %s" % tHandle)
nwItem = self.theProject.projTree[tHandle]
if nwItem is not None:
From a881fb8bd78e26d38e15d267f60708856a50e87d Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 21 Dec 2020 21:41:26 +0100
Subject: [PATCH 10/10] Update docs
---
docs/source/write_projects.rst | 90 ++++++++++++++++++----------------
1 file changed, 48 insertions(+), 42 deletions(-)
diff --git a/docs/source/write_projects.rst b/docs/source/write_projects.rst
index c25aa13b..a93589cf 100644
--- a/docs/source/write_projects.rst
+++ b/docs/source/write_projects.rst
@@ -7,9 +7,10 @@ Novel Projects
A novelWriter project requires a dedicated folder for storing its files on the local file system.
See the :ref:`a_tech` page for further details on how files are organised.
-A new project can be created from the :guilabel:`Project` menu by selecting :guilabel:`New Project`.
-A list of recently opened projects is maintained, and displayed in the :guilabel:`Open Project`
-dialog. A project can be removed from this list by selecting it and pressing the :kbd:`Del` key.
+A new project can be created from the :guilabel:`Project` menu by selecting
+:guilabel:`New Project`. A list of recently opened projects is maintained, and displayed in the
+:guilabel:`Open Project` dialog. A project can be removed from this list by selecting it and
+pressing the :kbd:`Del` key.
The project specific settings are available in :guilabel:`Project Settings` in the
:guilabel:`Project` menu. See further details below in the :ref:`a_proj_settings` section.
@@ -23,15 +24,15 @@ Project Roots
Projects are structured into a set of top level folders called *root folders*. They are visible in
the project tree at the left side of the main window.
-The core novel files go into a root folder of type :guilabel:`Novel`. Other supporting files go into
-the other root folders. These other root folder types are intended for your notes on the various
-elements of your story. Using these is of course entirely optional.
+The core novel files go into a root folder of type :guilabel:`Novel`. Other supporting files go
+into the other root folders. These other root folder types are intended for your notes on the
+various elements of your story. 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
:guilabel:`Create Root Folder` in the :guilabel:`Project` menu.
-The root folders are intended for the following use, but aside from the :guilabel:`Novel` folder, no
-restrictions are enforced by the application. You can use them however you want.
+The root folders are intended for the following use, but aside from the :guilabel:`Novel` folder,
+no restrictions are enforced by the application. You can use them however you want.
:guilabel:`Novel`
This is the root folder of all text that goes into the final novel. This class of files have
@@ -40,8 +41,8 @@ restrictions are enforced by the application. You can use them however you want.
:guilabel:`Plot`
This is the root folder where main plots can be outlined. It is optional, but adding at least
- dummy files can be useful in order to tag plot elements for the Outline view. Tags in this folder
- can be references using the ``@plot`` keyword.
+ dummy files can be useful in order to tag plot elements for the Outline view. Tags in this
+ folder can be references using the ``@plot`` keyword.
:guilabel:`Characters`
Character files go in this root folder. These are especially important if one wants to use the
@@ -86,8 +87,8 @@ Deleted Documents
-----------------
Deleted document files will be moved into a special :guilabel:`Trash` root folder. Files in the
-trash folder can then be deleted permanently, either individually, or by emptying the trash from the
-menu. Files in this folder are removed from the project index and cannot be referenced.
+trash folder can then be deleted permanently, either individually, or by emptying the trash from
+the menu. Files in this folder are removed from the project index and cannot be referenced.
Folders and root folders can only be deleted when they are empty. Recursive deletion is not
supported. A document file or a folder can be deleted from the :guilabel:`project` menu, or by
@@ -106,25 +107,30 @@ folder, only files. If you need folders in it to organise your files, you can of
ones there.
You can drag any file to this folder and preserve its settings. The file will always be excluded
-from the :guilabel:`Build Novel Project` builds. The file is also removed from the project index, so
-the tags and references defined in it will not show up anywhere else.
+from the :guilabel:`Build Novel Project` builds. The file is also removed from the project index,
+so the tags and references defined in it will not show up anywhere else.
.. _a_proj_roots_orph:
-Orphaned Documents
-------------------
+Recovered Documents
+-------------------
If novelWriter crashes or otherwise exits without saving the project state, or if you're using a
-file synchronisation tool that runs out of sync, there may be files in the project folder that isn't
-tracked in the core project file. These files, when discovered, are handled by the Orphaned
-Documents routine.
+file synchronisation tool that runs out of sync, there may be files in the project folder that
+aren't tracked in the core project file. These files, when discovered, are recovered and added back
+into the project if possible.
-Files that are discovered in the project folder, but not in the project, will be re-added to the
-project tree in a special :guilabel:`Orphaned Items` root folder next time the application is
-started. These orphaned files will not have most of the meta data preserved, although novelWriter
-will try to restore the file label it had in the project tree. Other information will have to be set
-again, and the files moved back to the correct location in the project tree.
+The discovered files are scanned for meta information that gives clues as to where the file may
+previously have been located in the project. The project loading routines will try to put them back
+as close as possible to this location if it still exists. Generally, it will be appended to the end
+of the folder where it previously was located. If that folder doesn't exist, it will try to add it
+to the correct root folder. If it cannot figure out which root folder is correct, the file will be
+added to the :guilabel:`Novel` root folder.
+
+If the title of the file can be recovered, the word "Recovered:" will be added as a prefix. If the
+title cannot be determined, the file will be named "Recovered File N" where N is a sequential
+number.
.. _a_proj_roots_lock:
@@ -193,16 +199,16 @@ Word Counts
A character, word and paragraph count is maintained for each file, as well as dor each section of a
file defined by a header. The word count, and change of words in the current session, is displayed
-in the footer of any document open in the editor, and all stats are shown in the details panel below
-the project tree for any file selected.
+in the footer of any document open in the editor, and all stats are shown in the details panel
+below the project tree for any file selected.
The word counts are not updated in real time, but runs in the background every five seconds for as
long as the document is being actively edited.
-A total project word count is displayed in the status bar. The total count depends on the sum of the
-values in the project tree, which again depend on an up to date index. If the counts seem wrong, a
-full project word recount can be initiated by rebuilding the project's index. Either form the
-:guilabel:`Tools` menu, or by pressing :kbd:`F9`.
+A total project word count is displayed in the status bar. The total count depends on the sum of
+the values in the project tree, which again depend on an up to date index. If the counts seem
+wrong, a full project word recount can be initiated by rebuilding the project's index. Either form
+the :guilabel:`Tools` menu, or by pressing :kbd:`F9`.
.. _a_proj_settings:
@@ -242,9 +248,9 @@ many words exist in the entire project.
Status and Importance Tabs
--------------------------
-Each file of type "Novel" can be given a status level, signified by a coloured icon and each file of
-the remaining types can be given an importance level. These are colour coded icons and labels that
-can be applied to each file.
+Each file of type "Novel" can be given a status level, signified by a coloured icon and each file
+of the remaining types can be given an importance level. These are colour coded icons and labels
+that can be applied to each file.
These are purely there for the user's convenience, and you are not required to use them for any
other feature to work. No other part of novelWriter accesses this information. The intention is to
@@ -266,8 +272,8 @@ also be applied to exports.
.. note::
A keyword cannot contain any spaces. The angle brackets are added by default, and when used in
- the text are a part of the keyword to be replaced. This is to ensure that parts of the text isn't
- unintentionally replaced by the content of the list.
+ the text are a part of the keyword to be replaced. This is to ensure that parts of the text
+ isn't unintentionally replaced by the content of the list.
.. _a_proj_backup:
@@ -275,22 +281,22 @@ also be applied to exports.
Backup
======
-An automatic backup system is built into novelWriter. In order to use it, a backup path to where the
-backup files are to be stored must to be provided in :guilabel:`Preferences`.
+An automatic backup system is built into novelWriter. In order to use it, a backup path to where
+the backup files are to be stored must to be provided in :guilabel:`Preferences`.
Backups can be run automatically when a project is closed, which also implies it is run when the
application is closed. Backups are date stamped zip files of the entire project folder, and are
-stored in a subfolder of the backup path with the same name as the project :guilabel:`Working Title`
-set in :ref:`a_proj_settings`.
+stored in a subfolder of the backup path with the same name as the project :guilabel:`Working
+Title` set in :ref:`a_proj_settings`.
The backup feature, when configured, can also be run manually from the :guilabel:`Tools` menu.
It is also possible to dissable automated backup for a given project in :guilabel:`Project
Settings`.
.. note::
- For the backup to be able to run, the :guilabel:`Working Title` must be set in :guilabel:`Project
- Settings`. This value is used to generate the folder name for the zip files. Without it, the
- backup will not run at all, but produce a warning message.
+ For the backup to be able to run, the :guilabel:`Working Title` must be set in
+ :guilabel:`Project Settings`. This value is used to generate the folder name for the zip files.
+ Without it, the backup will not run at all, but produce a warning message.
.. _a_proj_stats: