From 05aa656e7541735d4d33cc20b5d32f5cced2a4b3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Feb 2022 22:18:16 +0100 Subject: [PATCH 001/179] Remove checks for unique root --- novelwriter/core/project.py | 7 +------ novelwriter/core/tree.py | 15 --------------- novelwriter/gui/mainmenu.py | 18 ------------------ novelwriter/gui/projtree.py | 2 -- 4 files changed, 1 insertion(+), 41 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 26d2c098..284efbec 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -121,13 +121,8 @@ class NWProject(): ## def newRoot(self, rootName, rootClass): - """Add a new root item. These items are unique, except for item class - CUSTOM, and always have parent handle set to None. + """Add a new root item. """ - if not self.projTree.checkRootUnique(rootClass): - self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR) - return None - newItem = NWItem(self) newItem.setName(rootName) newItem.setType(nwItemType.ROOT) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 7dbef4c6..f429873b 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -253,21 +253,6 @@ class NWTree(): return tItem.itemHandle return None - def checkRootUnique(self, theClass): - """Checks if there already is a root entry of class 'theClass' - in the root of the project tree. CUSTOM class is skipped as it - is not required to be unique. - """ - if theClass == nwItemClass.CUSTOM: - return True - for aRoot in self._treeRoots: - tItem = self.__getitem__(aRoot) - if tItem is None: - continue - if theClass == tItem.itemClass: - return False - return True - 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 diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 5f91eccc..58578fc7 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -71,24 +71,6 @@ class GuiMainMenu(QMenuBar): return - ## - # Methods - ## - - def setAvailableRoot(self): - """Update the list of available root folders and set the ones - that are active. - """ - for itemClass in nwItemClass: - if itemClass == nwItemClass.NO_CLASS: - continue - if itemClass == nwItemClass.TRASH: - continue - self.rootItems[itemClass].setVisible( - self.theProject.projTree.checkRootUnique(itemClass) - ) - return - ## # Update Menu on Settings Changed ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 9f55cbe4..a1cd9081 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -581,7 +581,6 @@ class GuiProjectTree(QTreeWidget): if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) self._deleteTreeItem(tHandle) - self.theParent.mainMenu.setAvailableRoot() self._setTreeChanged(True) else: self.theParent.makeAlert(self.tr( @@ -973,7 +972,6 @@ class GuiProjectTree(QTreeWidget): if pHandle is None: if nwItem.itemType == nwItemType.ROOT: self.addTopLevelItem(newItem) - self.theParent.mainMenu.setAvailableRoot() elif nwItem.itemType == nwItemType.TRASH: self.addTopLevelItem(newItem) else: From 3fdea6e57a9689028cb91ceae2bec73cc9065bce Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Feb 2022 22:18:38 +0100 Subject: [PATCH 002/179] Fix tests --- .../coreProject_NewRoot_nwProject.nwx | 38 ++++++++++-- tests/test_core/test_core_project.py | 8 +-- tests/test_core/test_core_tree.py | 6 -- tests/test_gui/test_gui_projtree.py | 61 ++++++++++--------- 4 files changed, 68 insertions(+), 45 deletions(-) diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 58344c12..9d74dc3c 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -39,7 +39,7 @@ Main - + Novel ROOT @@ -112,27 +112,55 @@ 0 + Novel + ROOT + NOVEL + New + False + + + Plot + ROOT + PLOT + New + False + + + Character + ROOT + CHARACTER + New + False + + + World + ROOT + WORLD + New + False + + Timeline ROOT TIMELINE New False - + Object ROOT OBJECT New False - + Custom1 ROOT CUSTOM New False - + Custom2 ROOT CUSTOM diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 7ae461eb..2f9d40e3 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -275,10 +275,10 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI): assert theProject.closeProject() is True assert theProject.openProject(projFile) is True - assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None)) - assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None)) - assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None)) - assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None)) + assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), str) + assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), str) + assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), str) + assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), str) assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str) assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str) assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str) diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index dc49c41a..51cb6d2a 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -229,12 +229,6 @@ def testCoreTree_Methods(mockGUI, mockItems): assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" - # Check for root uniqueness - assert theTree.checkRootUnique(nwItemClass.CUSTOM) - assert theTree.checkRootUnique(nwItemClass.WORLD) - assert not theTree.checkRootUnique(nwItemClass.NOVEL) - assert not theTree.checkRootUnique(nwItemClass.CHARACTER) - # Find root item of child item assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001" assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001" diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 445bfb9a..9d43481c 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -51,21 +51,22 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): ## # Try to add and move item with no project - assert not nwTree.newTreeItem(nwItemType.FILE, None) - assert not nwTree.moveTreeItem(1) + assert nwTree.newTreeItem(nwItemType.FILE, None) is False + assert nwTree.moveTreeItem(1) is False # Open a project - assert nwGUI.openProject(nwMinimal) + assert nwGUI.openProject(nwMinimal) is True # 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) + assert nwTree.newTreeItem(nwItemType.FILE, None) is False + assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False + assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is True # No itemType set or ROOT, but no class - assert not nwTree.newTreeItem(None, None) - assert not nwTree.newTreeItem(nwItemType.ROOT, None) + nwTree.clearSelection() + assert nwTree.newTreeItem(None, None) is False + assert nwTree.newTreeItem(nwItemType.ROOT, None) is False # Select a location chItem = nwTree._getTreeItem("a6d311a93600a") @@ -73,23 +74,23 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): chItem.setExpanded(True) # Create new item with no class set (defaults to NOVEL) - assert nwTree.newTreeItem(nwItemType.FILE, None) - assert nwTree.newTreeItem(nwItemType.FOLDER, None) + assert nwTree.newTreeItem(nwItemType.FILE, None) is True + assert nwTree.newTreeItem(nwItemType.FOLDER, None) is True # 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, nwItemClass.WORLD) # Duplicate - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid + # Add more roots + assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True # Duplicate + assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) is True # Unique # Change max depth and try to add a subfolder that is too deep monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2) chItem = nwTree._getTreeItem("71ee45a3c0db9") nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) - assert not nwTree.newTreeItem(nwItemType.FOLDER, None) + assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False ## # Move Items @@ -99,7 +100,7 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): # Shift focus and try to move item monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - assert not nwTree.moveTreeItem(1) + assert nwTree.moveTreeItem(1) is False assert nwTree.getTreeFromHandle("a6d311a93600a") == [ "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" ] @@ -153,7 +154,7 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11 + assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 12 ## # Delete and Trash @@ -168,20 +169,20 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): # Delete the items we added earlier nwTree.clearSelection() - assert not nwTree.emptyTrash() # No folder yet - assert not nwTree.deleteItem(None) - assert not nwTree.deleteItem("1111111111111") - assert nwTree.deleteItem("73475cb40a568") # New File - assert nwTree.deleteItem("71ee45a3c0db9") # New Folder - assert nwTree.deleteItem("811786ad1ae74") # Custom Root + assert nwTree.emptyTrash() is False # No folder yet + assert nwTree.deleteItem(None) is False + assert nwTree.deleteItem("1111111111111") is False + assert nwTree.deleteItem("73475cb40a568") is True # New File + assert nwTree.deleteItem("71ee45a3c0db9") is True # New Folder + assert nwTree.deleteItem("811786ad1ae74") is True # Custom Root assert "73475cb40a568" 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")) - assert nwTree.emptyTrash() - assert not nwTree.emptyTrash() # Already empty + assert nwTree.emptyTrash() is True + assert nwTree.emptyTrash() is False # Already empty assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder @@ -189,8 +190,8 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): 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) + assert nwTree.newTreeItem(nwItemType.FILE, None) is False + assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False # Close the project nwGUI.closeProject() @@ -217,21 +218,21 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): ## # Add an item with an invalid type - assert not nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL) + assert nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL) is False 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"]._parent = None - assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) + assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False nwTree.clearSelection() # Add a file with no parent, and fail to find a suitable parent item monkeypatch.setattr("novelwriter.core.tree.NWTree.findRoot", lambda *a: None) - assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) - assert not nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL) + assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False + assert nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL) is False # qtbot.stopForInteraction() nwGUI.closeProject() From 045a595537a6390639a56a609a667d92271aa4cf Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 20 Feb 2022 22:05:31 +0100 Subject: [PATCH 003/179] Implement project file format 1.4 (#993) * Update the item class with more compact item storage format * Bump version and file version * Update tests * Add explicit test coverage for format conversions for item class * Update the documentation --- docs/source/usage_projectformat.rst | 25 +- novelwriter/__init__.py | 4 +- novelwriter/core/item.py | 63 ++-- novelwriter/core/project.py | 7 +- sample/nwProject.nwx | 316 +++++------------- tests/lipsum/nwProject.nwx | 270 ++++----------- tests/minimal/nwProject.nwx | 93 ++---- .../coreProject_NewCustomA_nwProject.nwx | 274 ++++----------- .../coreProject_NewCustomB_nwProject.nwx | 163 +++------ .../coreProject_NewFile_nwProject.nwx | 117 ++----- .../coreProject_NewMinimal_nwProject.nwx | 89 ++--- .../coreProject_NewRoot_nwProject.nwx | 125 ++----- .../guiEditor_Main_Final_nwProject.nwx | 142 +++----- .../guiEditor_Main_Initial_nwProject.nwx | 89 ++--- .../guiProjSettings_Dialog_nwProject.nwx | 89 ++--- tests/test_core/test_core_item.py | 136 ++++++-- tests/test_core/test_core_tree.py | 53 ++- 17 files changed, 666 insertions(+), 1389 deletions(-) diff --git a/docs/source/usage_projectformat.rst b/docs/source/usage_projectformat.rst index ea0b55b5..6a7c4c8b 100644 --- a/docs/source/usage_projectformat.rst +++ b/docs/source/usage_projectformat.rst @@ -11,13 +11,32 @@ changes require minor actions from the user. The key changes in the formats are listed below, as well as the user actions required where applicable. +.. caution:: + + When you update a project from one format version to the next, the project can no longer be + opened by a version of novelWriter prior to the version where the new file format was + introduced. You will get a notification about any updates to your project file format and will + have the option to decline the upgrade. + + +.. _a_prjfmt_1_4: + +Format 1.4 Changes +================== + +This project format was introduced in novelWriter version 1.7. + +This format changes the way project items (folders, documents and notes) are stored. It is a more +compact format that is simpler and faster to parse, and easier to extend. The conversion is done +automatically the first time a project is loaded. No user action is required. + .. _a_prjfmt_1_3: Format 1.3 Changes ================== -This project format vas introduces in novelWriter version 1.5. +This project format was introduced in novelWriter version 1.5. With this format, the number of document layouts was reduced from 8 to 2. The conversion of document layouts is performed automatically when the project is opened. @@ -55,7 +74,7 @@ should be used only a few places in any given project. These are as follows: Format 1.2 Changes ================== -This project format was introduces in novelWriter version 0.10. +This project format was introduced in novelWriter version 0.10. With this format, the way auto-replace entries were stored in the main project XML file changed. Opening an old project automatically converts the storage format up to and including version 1.1.1. @@ -69,7 +88,7 @@ auto-replace is not being used, can still be opened in novelWriter as of version Format 1.1 Changes ================== -This project format was introduces in novelWriter version 0.7. +This project format was introduced in novelWriter version 0.7. With this format, the ``content`` folder was introduced in the project storage. Previously, all novelWriter documents were saved in a series of folders numbered from ``data_0`` to ``data_f``. diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 98247c68..740ed327 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -60,8 +60,8 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "1.6" -__hexversion__ = "0x010600f0" +__version__ = "1.7-alpha0" +__hexversion__ = "0x010700a0" __date__ = "2022-02-20" __status__ = "Stable" __domain__ = "novelwriter.io" diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 8c7b2ef4..07db4a41 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -139,24 +139,32 @@ class NWItem(): def packXML(self, xParent): """Pack all the data in the class instance into an XML object. """ - xPack = etree.SubElement(xParent, "item", attrib={ - "handle": str(self._handle), - "order": str(self._order), - "parent": str(self._parent), - }) - self._subPack(xPack, "name", text=str(self._name)) - self._subPack(xPack, "type", text=str(self._type.name)) - self._subPack(xPack, "class", text=str(self._class.name)) - self._subPack(xPack, "status", text=str(self._status)) + itemAttrib = {} + itemAttrib["handle"] = str(self._handle) + itemAttrib["parent"] = str(self._parent) + itemAttrib["order"] = str(self._order) + itemAttrib["type"] = str(self._type.name) + itemAttrib["class"] = str(self._class.name) if self._type == nwItemType.FILE: - self._subPack(xPack, "exported", text=str(self._exported)) - self._subPack(xPack, "layout", text=str(self._layout.name)) - self._subPack(xPack, "charCount", text=str(self._charCount), none=False) - self._subPack(xPack, "wordCount", text=str(self._wordCount), none=False) - self._subPack(xPack, "paraCount", text=str(self._paraCount), none=False) - self._subPack(xPack, "cursorPos", text=str(self._cursorPos), none=False) + itemAttrib["layout"] = str(self._layout.name) + + metaAttrib = {} + if self._type == nwItemType.FILE: + metaAttrib["charCount"] = str(self._charCount) + metaAttrib["wordCount"] = str(self._wordCount) + metaAttrib["paraCount"] = str(self._paraCount) + metaAttrib["cursorPos"] = str(self._cursorPos) else: - self._subPack(xPack, "expanded", text=str(self._expanded)) + metaAttrib["expanded"] = str(self._expanded) + + nameAttrib = {} + nameAttrib["status"] = str(self._status) + if self._type == nwItemType.FILE: + nameAttrib["exported"] = str(self._exported) + + xPack = etree.SubElement(xParent, "item", attrib=itemAttrib) + self._subPack(xPack, "meta", attrib=metaAttrib) + self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib) return @@ -175,19 +183,31 @@ class NWItem(): self.setParent(xItem.attrib.get("parent", None)) self.setOrder(xItem.attrib.get("order", 0)) + self.setType(xItem.attrib.get("type", None)) + self.setClass(xItem.attrib.get("class", None)) + self.setLayout(xItem.attrib.get("layout", None)) - tmpStatus = "" for xValue in xItem: - if xValue.tag == "name": + if xValue.tag == "meta": + self.setExpanded(xValue.attrib.get("expanded", False)) + self.setCharCount(xValue.attrib.get("charCount", 0)) + self.setWordCount(xValue.attrib.get("wordCount", 0)) + self.setParaCount(xValue.attrib.get("paraCount", 0)) + self.setCursorPos(xValue.attrib.get("cursorPos", 0)) + elif xValue.tag == "name": self.setName(xValue.text) + self.setStatus(xValue.attrib.get("status", None)) + self.setExported(xValue.attrib.get("exported", True)) + + # Legacy Format (1.3 and earlier) + elif xValue.tag == "status": + self.setStatus(xValue.text) elif xValue.tag == "type": self.setType(xValue.text) elif xValue.tag == "class": self.setClass(xValue.text) elif xValue.tag == "layout": self.setLayout(xValue.text) - elif xValue.tag == "status": - tmpStatus = xValue.text elif xValue.tag == "expanded": self.setExpanded(xValue.text) elif xValue.tag == "exported": @@ -206,9 +226,6 @@ class NWItem(): # version of novelWriter that doesn't know the tag logger.error("Unknown tag '%s'", xValue.tag) - # Guarantees that is parsed after - self.setStatus(tmpStatus) - return True @staticmethod diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 26d2c098..556f6e18 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -53,7 +53,7 @@ logger = logging.getLogger(__name__) class NWProject(): - FILE_VERSION = "1.3" + FILE_VERSION = "1.4" def __init__(self, theParent): @@ -479,8 +479,11 @@ class NWProject(): # 1.3 : Reduces the number of layouts to only two. One for novel # documents and one for project notes. Introduced in # version 1.5. + # 1.4 : Introduces a more compact format for storing items. All + # settings aside from name are now attributes. Introduced + # in version 1.7. - if fileVersion not in ("1.0", "1.1", "1.2", "1.3"): + if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"): self.theParent.makeAlert(self.tr( "Unknown or unsupported novelWriter project file format. " "The project cannot be opened by this version of novelWriter. " diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 9e37d34a..bf40e1ee 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1299 + 1303 199 - 64923 + 64457 False @@ -49,265 +49,105 @@ - - Novel - ROOT - NOVEL - Started - True + + + Novel - - Title Page - FILE - NOVEL - Started - True - DOCUMENT - 93 - 19 - 2 - 2 + + + Title Page - - Page - FILE - NOVEL - New - True - DOCUMENT - 186 - 39 - 2 - 212 + + + Page - - Part One - FILE - NOVEL - New - True - DOCUMENT - 26 - 6 - 1 - 33 + + + Part One - - A Folder - FOLDER - NOVEL - 1st Draft - True + + + A Folder - - Chapter One - FILE - NOVEL - Notes - True - DOCUMENT - 75 - 14 - 1 - 279 + + + Chapter One - - Making a Scene - FILE - NOVEL - 1st Draft - True - DOCUMENT - 2429 - 432 - 14 - 219 + + + Making a Scene - - Another Scene - FILE - NOVEL - 1st Draft - True - DOCUMENT - 476 - 93 - 3 - 577 + + + Another Scene - - Interlude - FILE - NOVEL - New - True - DOCUMENT - 617 - 101 - 3 - 4 + + + Interlude - - A Note on Structure - FILE - NOVEL - 2nd Draft - False - NOTE - 1692 - 313 - 6 - 1110 + + + A Note on Structure - - Chapter Two - FILE - NOVEL - 1st Draft - True - DOCUMENT - 139 - 28 - 1 - 343 + + + Chapter Two - - We Found John! - FILE - NOVEL - 1st Draft - True - DOCUMENT - 189 - 37 - 1 - 224 + + + We Found John! - - Characters - ROOT - CHARACTER - None - True + + + Characters - - Main Characters - FOLDER - CHARACTER - None - True + + + Main Characters - - John Smith - FILE - CHARACTER - Minor - True - NOTE - 49 - 9 - 1 - 24 + + + John Smith - - Jane Smith - FILE - CHARACTER - Major - True - NOTE - 55 - 9 - 1 - 25 + + + Jane Smith - - Locations - ROOT - WORLD - None - True + + + Locations - - Earth - FILE - WORLD - Main - True - NOTE - 76 - 15 - 1 - 20 + + + Earth - - Space - FILE - WORLD - Minor - True - NOTE - 115 - 24 - 1 - 133 + + + Space - - Mars - FILE - WORLD - Major - True - NOTE - 28 - 6 - 1 - 45 + + + Mars - - Archive - ROOT - ARCHIVE - New - True + + + Archive - - Scenes - FOLDER - ARCHIVE - New - True + + + Scenes - - Old File - FILE - NOVEL - 1st Draft - True - DOCUMENT - 315 - 55 - 1 - 322 + + + Old File - - Trash - TRASH - TRASH - None - True + + + Trash - - Delete Me! - FILE - NOVEL - New - True - DOCUMENT - 30 - 6 - 1 - 36 + + + Delete Me! diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index 02803bbd..c6eab805 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,12 +1,12 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 17 + 21 24 - 1777 + 1847 False @@ -44,227 +44,89 @@ - - Novel - ROOT - NOVEL - New - True + + + Novel - - Lorem Ipsum - FILE - NOVEL - Finished - True - DOCUMENT - 230 - 40 - 3 - 148 + + + Lorem Ipsum - - Front Matter - FILE - NOVEL - Finished - True - DOCUMENT - 1058 - 176 - 2 - 43 + + + Front Matter - - Prologue - FILE - NOVEL - Draft - True - DOCUMENT - 584 - 92 - 1 - 4 + + + Prologue - - Act One - FILE - NOVEL - New - True - DOCUMENT - 35 - 6 - 1 - 42 + + + Act One - - Chapter One - FOLDER - NOVEL - Draft - True + + + Chapter One - - Chapter One - FILE - NOVEL - Draft - True - DOCUMENT - 419 - 67 - 1 - 56 + + + Chapter One - - Scene One - FILE - NOVEL - Finished - True - DOCUMENT - 2758 - 404 - 4 - 1528 + + + Scene One - - Scene Two - FILE - NOVEL - Finished - True - DOCUMENT - 4043 - 600 - 6 - 2335 + + + Scene Two - - Interlude - FILE - NOVEL - New - False - DOCUMENT - 631 - 109 - 3 - 376 + + + Interlude - - Chapter Two - FOLDER - NOVEL - Draft - True + + + Chapter Two - - Chapter Two - FILE - NOVEL - Draft - True - DOCUMENT - 477 - 70 - 1 - 56 + + + Chapter Two - - Scene Three - FILE - NOVEL - Finished - True - DOCUMENT - 3006 - 439 - 4 - 57 + + + Scene Three - - Scene Four - FILE - NOVEL - Finished - True - DOCUMENT - 3839 - 563 - 6 - 56 + + + Scene Four - - Scene Five - FILE - NOVEL - Finished - True - DOCUMENT - 3644 - 543 - 5 - 351 + + + Scene Five - - Characters - ROOT - CHARACTER - New - True + + + Characters - - Mr. Nobody - FILE - CHARACTER - Major - True - NOTE - 1864 - 284 - 3 - 1883 + + + Mr. Nobody - - Plot - ROOT - PLOT - New - True + + + Plot - - Main - FILE - PLOT - Main - True - NOTE - 1369 - 195 - 2 - 1387 + + + Main - - World - ROOT - WORLD - New - True + + + World - - Ancient Europe - FILE - WORLD - Minor - True - NOTE - 1770 - 259 - 3 - 1792 + + + Ancient Europe diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index c1f2a901..991d8b68 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,13 +1,13 @@ - + Test Minimal Minimal Jane Doe John Doh - 9 + 12 2 - 113 + 129 True @@ -42,76 +42,37 @@ - - Novel - ROOT - NOVEL - New - True + + + Novel - - Title Page - FILE - NOVEL - New - True - DOCUMENT - 28 - 6 - 1 - 33 + + + Title Page - - New Chapter - FOLDER - NOVEL - New - True + + + New Chapter - - New Chapter - FILE - NOVEL - New - True - DOCUMENT - 11 - 2 - 0 - 16 + + + New Chapter - - New Scene - FILE - NOVEL - New - True - DOCUMENT - 9 - 2 - 0 - 15 + + + New Scene - - Plot - ROOT - PLOT - New - False + + + Plot - - Characters - ROOT - CHARACTER - New - False + + + Characters - - World - ROOT - WORLD - New - False + + + World diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 3ba6a538..4b8a67bf 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -42,231 +42,97 @@ - - Novel - ROOT - NOVEL - New - False + + + Novel - - Plot - ROOT - PLOT - New - False + + + Plot - - Characters - ROOT - CHARACTER - New - False + + + Characters - - Locations - ROOT - WORLD - New - False + + + Locations - - Timeline - ROOT - TIMELINE - New - False + + + Timeline - - Objects - ROOT - OBJECT - New - False + + + Objects - - Entities - ROOT - ENTITY - New - False + + + Entities - - Title Page - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Title Page - - Chapter 1 - FOLDER - NOVEL - New - False + + + Chapter 1 - - Chapter 1 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Chapter 1 - - Scene 1.1 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 1.1 - - Scene 1.2 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 1.2 - - Scene 1.3 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 1.3 - - Chapter 2 - FOLDER - NOVEL - New - False + + + Chapter 2 - - Chapter 2 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Chapter 2 - - Scene 2.1 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 2.1 - - Scene 2.2 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 2.2 - - Scene 2.3 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 2.3 - - Chapter 3 - FOLDER - NOVEL - New - False + + + Chapter 3 - - Chapter 3 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Chapter 3 - - Scene 3.1 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 3.1 - - Scene 3.2 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 3.2 - - Scene 3.3 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 3.3 diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 2afad85a..3f55663e 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -42,138 +42,61 @@ - - Novel - ROOT - NOVEL - New - False + + + Novel - - Plot - ROOT - PLOT - New - False + + + Plot - - Characters - ROOT - CHARACTER - New - False + + + Characters - - Locations - ROOT - WORLD - New - False + + + Locations - - Timeline - ROOT - TIMELINE - New - False + + + Timeline - - Objects - ROOT - OBJECT - New - False + + + Objects - - Entities - ROOT - ENTITY - New - False + + + Entities - - Title Page - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Title Page - - Scene 1 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 1 - - Scene 2 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 2 - - Scene 3 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 3 - - Scene 4 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 4 - - Scene 5 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 5 - - Scene 6 - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Scene 6 diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index 88c9aafb..d623ee2d 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,100 +40,45 @@ - - Novel - ROOT - NOVEL - New - False + + + Novel - - Plot - ROOT - PLOT - New - False + + + Plot - - Characters - ROOT - CHARACTER - New - False + + + Characters - - World - ROOT - WORLD - New - False + + + World - - Title Page - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Title Page - - New Chapter - FOLDER - NOVEL - New - False + + + New Chapter - - New Chapter - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + New Chapter - - New Scene - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + New Scene - - Hello - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Hello - - Jane - FILE - CHARACTER - New - True - NOTE - 0 - 0 - 0 - 0 + + + Jane diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index d3702a07..6becb112 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,76 +40,37 @@ - - Novel - ROOT - NOVEL - New - False + + + Novel - - Plot - ROOT - PLOT - New - False + + + Plot - - Characters - ROOT - CHARACTER - New - False + + + Characters - - World - ROOT - WORLD - New - False + + + World - - Title Page - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Title Page - - New Chapter - FOLDER - NOVEL - New - False + + + New Chapter - - New Chapter - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + New Chapter - - New Scene - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + New Scene diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 58344c12..cf55336e 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,104 +40,53 @@ - - Novel - ROOT - NOVEL - New - False + + + Novel - - Plot - ROOT - PLOT - New - False + + + Plot - - Characters - ROOT - CHARACTER - New - False + + + Characters - - World - ROOT - WORLD - New - False + + + World - - Title Page - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + Title Page - - New Chapter - FOLDER - NOVEL - New - False + + + New Chapter - - New Chapter - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + New Chapter - - New Scene - FILE - NOVEL - New - True - DOCUMENT - 0 - 0 - 0 - 0 + + + New Scene - - Timeline - ROOT - TIMELINE - New - False + + + Timeline - - Object - ROOT - OBJECT - New - False + + + Object - - Custom1 - ROOT - CUSTOM - New - False + + + Custom1 - - Custom2 - ROOT - CUSTOM - New - False + + + Custom2 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 1111c579..4dd43b9a 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,11 +1,11 @@ - + New Project 5 2 - 4 + 3 True @@ -40,119 +40,53 @@ - - Novel - ROOT - NOVEL - New - True + + + Novel - - Title Page - FILE - NOVEL - New - True - DOCUMENT - 11 - 2 - 0 - 0 + + + Title Page - - New Chapter - FOLDER - NOVEL - New - True + + + New Chapter - - New Chapter - FILE - NOVEL - New - True - DOCUMENT - 11 - 2 - 0 - 0 + + + New Chapter - - New Scene - FILE - NOVEL - New - True - DOCUMENT - 612 - 95 - 10 - 768 + + + New Scene - - Plot - ROOT - PLOT - New - True + + + Plot - - New File - FILE - PLOT - New - True - NOTE - 48 - 10 - 1 - 69 + + + New File - - Characters - ROOT - CHARACTER - New - True + + + Characters - - New File - FILE - CHARACTER - New - True - NOTE - 34 - 8 - 1 - 51 + + + New File - - World - ROOT - WORLD - New - True + + + World - - New File - FILE - WORLD - New - True - NOTE - 51 - 9 - 1 - 68 + + + New File - - Trash - TRASH - TRASH - None - True + + + Trash diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 7d2cfe5f..f1b17740 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,76 +40,37 @@ - - Novel - ROOT - NOVEL - New - False + + + Novel - - Title Page - FILE - NOVEL - New - True - DOCUMENT - 11 - 2 - 0 - 0 + + + Title Page - - New Chapter - FOLDER - NOVEL - New - False + + + New Chapter - - New Chapter - FILE - NOVEL - New - True - DOCUMENT - 11 - 2 - 0 - 0 + + + New Chapter - - New Scene - FILE - NOVEL - New - True - DOCUMENT - 9 - 2 - 0 - 0 + + + New Scene - - Plot - ROOT - PLOT - New - False + + + Plot - - Characters - ROOT - CHARACTER - New - False + + + Characters - - World - ROOT - WORLD - New - False + + + World diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index 96334e16..b88e7f17 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -46,76 +46,37 @@ - - Novel - ROOT - NOVEL - New - False + + + Novel - - Title Page - FILE - NOVEL - New - True - DOCUMENT - 11 - 2 - 0 - 0 + + + Title Page - - New Chapter - FOLDER - NOVEL - New - False + + + New Chapter - - New Chapter - FILE - NOVEL - New - True - DOCUMENT - 11 - 2 - 0 - 0 + + + New Chapter - - New Scene - FILE - NOVEL - New - True - DOCUMENT - 9 - 2 - 0 - 0 + + + New Scene - - Plot - ROOT - PLOT - New - False + + + Plot - - Characters - ROOT - CHARACTER - New - False + + + Characters - - World - ROOT - WORLD - New - False + + + World diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index dea2b3c7..368592f1 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -306,22 +306,6 @@ def testCoreItem_LayoutSetter(mockGUI): theItem.setLayout("NOTE") assert theItem.itemLayout == nwItemLayout.NOTE - # Deprecated Layouts - theItem.setLayout("TITLE") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("PAGE") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("BOOK") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("PARTITION") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("UNNUMBERED") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("CHAPTER") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("SCENE") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - # Alternatives theItem.setLayout(nwItemLayout.NOTE) assert theItem.itemLayout == nwItemLayout.NOTE @@ -358,12 +342,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): xContent = etree.SubElement(nwXML, "content") theItem.packXML(xContent) assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b"" - b"" - b"A NameFILENOVELNew" - b"FalseNOTE7" - b"5311" - b"" + b'A Name' ) # Unpack @@ -404,11 +385,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): xContent = etree.SubElement(nwXML, "content") theItem.packXML(xContent) assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b"" - b"" - b"A NameFOLDERNOVELNew" - b"True" - b"" + b'A Name' ) # Unpack @@ -462,3 +440,107 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): ) # END Test testCoreItem_XMLPackUnpack + + +@pytest.mark.core +def testCoreItem_ConvertFromFmt12(mockGUI): + """Test the setter for all the nwItemLayout values for the NWItem + class using the class names that were present in file format 1.2. + """ + theProject = NWProject(mockGUI) + theItem = NWItem(theProject) + + # Deprecated Layouts + theItem.setLayout("TITLE") + assert theItem.itemLayout == nwItemLayout.DOCUMENT + theItem.setLayout("PAGE") + assert theItem.itemLayout == nwItemLayout.DOCUMENT + theItem.setLayout("BOOK") + assert theItem.itemLayout == nwItemLayout.DOCUMENT + theItem.setLayout("PARTITION") + assert theItem.itemLayout == nwItemLayout.DOCUMENT + theItem.setLayout("UNNUMBERED") + assert theItem.itemLayout == nwItemLayout.DOCUMENT + theItem.setLayout("CHAPTER") + assert theItem.itemLayout == nwItemLayout.DOCUMENT + theItem.setLayout("SCENE") + assert theItem.itemLayout == nwItemLayout.DOCUMENT + theItem.setLayout("MUMBOJUMBO") + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + +# END Test testCoreItem_ConvertFromFmt12 + + +@pytest.mark.core +def testCoreItem_ConvertFromFmt13(mockGUI): + """Test packing and unpacking XML objects for the NWItem class from + format version 1.3 + """ + theProject = NWProject(mockGUI) + + # Make Version 1.3 XML + nwXML = etree.Element("novelWriterXML") + xContent = etree.SubElement(nwXML, "content") + + # Folder + xPack = etree.SubElement(xContent, "item", attrib={ + "handle": "a000000000001", + "order": "1", + "parent": "b000000000001", + }) + NWItem._subPack(xPack, "name", text="Folder") + NWItem._subPack(xPack, "type", text="FOLDER") + NWItem._subPack(xPack, "class", text="NOVEL") + NWItem._subPack(xPack, "status", text="New") + NWItem._subPack(xPack, "expanded", text="True") + + # Unpack Folder + theItem = NWItem(theProject) + theItem.unpackXML(xContent[0]) + assert theItem.itemHandle == "a000000000001" + assert theItem.itemParent == "b000000000001" + assert theItem.itemOrder == 1 + assert theItem.isExpanded is True + assert theItem.isExported is True + assert theItem.charCount == 0 + assert theItem.wordCount == 0 + assert theItem.paraCount == 0 + assert theItem.cursorPos == 0 + assert theItem.itemClass == nwItemClass.NOVEL + assert theItem.itemType == nwItemType.FOLDER + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + + # File + xPack = etree.SubElement(xContent, "item", attrib={ + "handle": "c000000000001", + "order": "2", + "parent": "a000000000001", + }) + NWItem._subPack(xPack, "name", text="Scene") + NWItem._subPack(xPack, "type", text="FILE") + NWItem._subPack(xPack, "class", text="NOVEL") + NWItem._subPack(xPack, "status", text="New") + NWItem._subPack(xPack, "exported", text="True") + NWItem._subPack(xPack, "layout", text="DOCUMENT") + NWItem._subPack(xPack, "charCount", text="600") + NWItem._subPack(xPack, "wordCount", text="100") + NWItem._subPack(xPack, "paraCount", text="6") + NWItem._subPack(xPack, "cursorPos", text="50") + + # Unpack File + theItem = NWItem(theProject) + theItem.unpackXML(xContent[1]) + assert theItem.itemHandle == "c000000000001" + assert theItem.itemParent == "a000000000001" + assert theItem.itemOrder == 2 + assert theItem.isExpanded is False + assert theItem.isExported is True + assert theItem.charCount == 600 + assert theItem.wordCount == 100 + assert theItem.paraCount == 6 + assert theItem.cursorPos == 50 + assert theItem.itemClass == nwItemClass.NOVEL + assert theItem.itemType == nwItemType.FILE + assert theItem.itemLayout == nwItemLayout.DOCUMENT + +# END Test testCoreItem_ConvertFromFmt13 diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index dc49c41a..e0e82f39 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -385,36 +385,29 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): nwXML = etree.Element("novelWriterXML") theTree.packXML(nwXML) assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( - b"" - b"" - b"" - b"NovelROOTNOVELNone" - b"True" - b"" - b"Act OneFOLDERNOVELNone" - b"True" - b"" - b"Chapter OneFILENOVELNone" - b"TrueDOCUMENT300" - b"5020" - b"" - b"Scene OneFILENOVELNone" - b"TrueDOCUMENT3000" - b"500200" - b"" - b"OuttakesROOTARCHIVENone" - b"False" - b"" - b"TrashTRASHTRASHNone" - b"False" - b"" - b"CharactersROOTCHARACTERNone" - b"True" - b"" - b"Jane DoeFILECHARACTERNone" - b"TrueNOTE2000" - b"400160" - b"" + b'' + b'' + b'' + b'Novel' + b'Act One' + b'' + b'Chapter One' + b'' + b'Scene One' + b'' + b'Outtakes' + b'' + b'Trash' + b'' + b'Characters' + b'Jane Doe' + b'' + b'' ) theTree.clear() From 011291f225d7032f375b4473a43a0749e27cf92d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 20 Feb 2022 23:35:33 +0100 Subject: [PATCH 004/179] Drop support for Python 3.6 (#1004) * Drop support for Python 3.6 * Remove unused time parsing function * Use Python 3.7 datetime function for writing stats data conversion * Test against Python 3.10 on macOS and Windows --- .github/workflows/test_linux.yml | 2 +- .github/workflows/test_mac.yml | 2 +- .github/workflows/test_win.yml | 2 +- README.md | 6 +++--- docs/source/int_started.rst | 2 +- novelwriter/__init__.py | 4 ++-- novelwriter/common.py | 13 ------------- novelwriter/core/toodt.py | 2 +- novelwriter/tools/writingstats.py | 8 ++------ setup.cfg | 3 +-- setup/debian/control | 6 +++--- setup/description_pypi.md | 6 +++--- tests/test_base/test_base_common.py | 23 +++-------------------- 13 files changed, 22 insertions(+), 57 deletions(-) diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml index 5699d8a8..45de8226 100644 --- a/.github/workflows/test_linux.yml +++ b/.github/workflows/test_linux.yml @@ -12,7 +12,7 @@ jobs: testLinux: strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] runs-on: ubuntu-latest steps: - name: Python Setup diff --git a/.github/workflows/test_mac.yml b/.github/workflows/test_mac.yml index c9b4497c..b11a2a71 100644 --- a/.github/workflows/test_mac.yml +++ b/.github/workflows/test_mac.yml @@ -15,7 +15,7 @@ jobs: - name: Python Setup uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" architecture: x64 - name: Install Packages run: | diff --git a/.github/workflows/test_win.yml b/.github/workflows/test_win.yml index 85a0fda6..fd0ad402 100644 --- a/.github/workflows/test_win.yml +++ b/.github/workflows/test_win.yml @@ -15,7 +15,7 @@ jobs: - name: Python Setup uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" architecture: x64 - name: Checkout Source uses: actions/checkout@v2 diff --git a/README.md b/README.md index 36aa11d2..60ca008b 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ The full credits are listed in ## Implementation -The application is written in Python 3 (3.6+) using Qt5 and PyQt5 (5.3+). It is developed on Linux, -but should in principle work fine on other operating systems as well as long as dependencies are -met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS. +The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.3+). It is developed on +Linux, but should in principle work fine on other operating systems as well as long as dependencies +are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS. ## Installation diff --git a/docs/source/int_started.rst b/docs/source/int_started.rst index f398a8b6..5b5fff70 100644 --- a/docs/source/int_started.rst +++ b/docs/source/int_started.rst @@ -115,7 +115,7 @@ Windows ------- First, make sure you have Python installed on your system. If you don't, you can download it from -`python.org`_. Python 3.6 or higher is required, but it is recommended that you install the latest +`python.org`_. Python 3.7 or higher is required, but it is recommended that you install the latest version. Make sure you select the "Add Python to PATH" option during installation, otherwise the ``python`` diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 740ed327..164a05e7 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -209,9 +209,9 @@ def main(sysArgs=None): # Check Packages and Versions errorData = [] errorCode = 0 - if sys.hexversion < 0x030600f0: + if sys.hexversion < 0x030700f0: errorData.append( - "At least Python 3.6 is required, found %s" % CONFIG.verPyString + "At least Python 3.7 is required, found %s" % CONFIG.verPyString ) errorCode |= 0x04 if CONFIG.verQtValue < 50300: diff --git a/novelwriter/common.py b/novelwriter/common.py index 8d6f9a8c..8e17a7ee 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -241,19 +241,6 @@ def formatTime(tS): return "ERROR" -def parseTimeStamp(theStamp, default, allowNone=False): - """Parses a text representation of a timestamp and converts it into - a float. Note that negative timestamps cause an OSError on Windows. - See https://bugs.python.org/issue29097 - """ - if str(theStamp).lower() == "none" and allowNone: - return None - try: - return datetime.strptime(theStamp, nwConst.FMT_TSTAMP).timestamp() - except Exception: - return default - - # =============================================================================================== # # String Functions # =============================================================================================== # diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py index c68f8991..c0b1daee 100644 --- a/novelwriter/core/toodt.py +++ b/novelwriter/core/toodt.py @@ -330,7 +330,7 @@ class ToOdt(Tokenizer): # Meta Data xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date")) - xMeta.text = datetime.now().strftime(r"%Y-%m-%dT%H:%M:%S") + xMeta.text = datetime.now().isoformat(sep="T", timespec="seconds") xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator")) xMeta.text = f"novelWriter/{novelwriter.__version__}" diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index e0a338bb..4ce83b66 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -457,12 +457,8 @@ class GuiWritingStats(QDialog): if len(inData) < 6: continue - dStart = datetime.strptime( - "%s %s" % (inData[0], inData[1]), nwConst.FMT_TSTAMP - ) - dEnd = datetime.strptime( - "%s %s" % (inData[2], inData[3]), nwConst.FMT_TSTAMP - ) + dStart = datetime.fromisoformat(" ".join(inData[0:2])) + dEnd = datetime.fromisoformat(" ".join(inData[2:4])) sIdle = 0 if len(inData) > 6: diff --git a/setup.cfg b/setup.cfg index d3bbcf41..0e005bf5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,6 @@ license_file = LICENSE.md license = GNU General Public License v3 classifiers = Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -29,7 +28,7 @@ project_urls = Source Code = https://github.com/vkbo/novelWriter [options] -python_requires = >=3.6 +python_requires = >=3.7 include_package_data = True packages = find: install_requires = diff --git a/setup/debian/control b/setup/debian/control index d32c56ba..e2d40f90 100644 --- a/setup/debian/control +++ b/setup/debian/control @@ -2,14 +2,14 @@ Source: novelwriter Maintainer: Veronica Berglyd Olsen Section: text Priority: optional -Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), python3 (>=3.6), python3-pyqt5 (>= 5.3), python3-lxml (>= 4.0), python3-enchant (>= 2.0) +Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), python3 (>=3.7), python3-pyqt5 (>= 5.3), python3-lxml (>= 4.0), python3-enchant (>= 2.0) Standards-Version: 4.5.1 Homepage: https://novelwriter.io -X-Python3-Version: >= 3.6 +X-Python3-Version: >= 3.7 Package: novelwriter Architecture: all -Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.6), python3-pyqt5 (>= 5.3), python3-lxml (>= 4.0), python3-enchant (>= 2.0) +Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.7), python3-pyqt5 (>= 5.3), python3-lxml (>= 4.0), python3-enchant (>= 2.0) Description: A markdown-like text editor for planning and writing novels novelWriter is a plain text editor designed for writing novels assembled from many smaller text documents. It uses a minimal formatting syntax inspired by diff --git a/setup/description_pypi.md b/setup/description_pypi.md index 8c15b1be..ed98ffda 100644 --- a/setup/description_pypi.md +++ b/setup/description_pypi.md @@ -10,9 +10,9 @@ synchronisation tools. All text is saved as plain text files with a meta data he project structure is stored in a single project XML file, and other meta data is primarily saved as JSON files. -The application is written in Python 3 (3.6+) using Qt5 and PyQt5 (5.3+). It is developed on Linux, -but should in principle work fine on other operating systems as well as long as dependencies are -met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS. +The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.3+). It is developed on +Linux, but should in principle work fine on other operating systems as well as long as dependencies +are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS. novelWriter is developed and maintained by [Veronica Berglyd Olsen](https://github.com/vkbo). diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 5b9b9c13..f5eef8fe 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -24,8 +24,6 @@ import os import time import pytest -from datetime import datetime - from mock import causeOSError from tools import writeFile @@ -33,9 +31,9 @@ from novelwriter.guimain import GuiMain from novelwriter.common import ( checkString, checkInt, checkFloat, checkBool, checkHandle, isHandle, isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, checkIntRange, - checkIntTuple, formatInt, formatTimeStamp, formatTime, parseTimeStamp, - splitVersionNumber, transferCase, fuzzyTime, numberToRoman, jsonEncode, - readTextFile, makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser + checkIntTuple, formatInt, formatTimeStamp, formatTime, splitVersionNumber, + transferCase, fuzzyTime, numberToRoman, jsonEncode, readTextFile, + makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser ) @@ -272,21 +270,6 @@ def testBaseCommon_FormatTime(): # END Test testBaseCommon_FormatTime -@pytest.mark.base -def testBaseCommon_ParseTimeStamp(): - """Test the parseTimeStamp function. - """ - localEpoch = datetime(2000, 1, 1).timestamp() - assert parseTimeStamp(None, 0.0, allowNone=True) is None - assert parseTimeStamp("None", 0.0, allowNone=True) is None - assert parseTimeStamp("None", 0.0) == 0.0 - assert parseTimeStamp("2000-01-01 00:00:00", 123.0) == localEpoch - assert parseTimeStamp("2000-13-01 00:00:00", 123.0) == 123.0 - assert parseTimeStamp("2000-01-32 00:00:00", 123.0) == 123.0 - -# END Test testBaseCommon_ParseTimeStamp - - @pytest.mark.base def testBaseCommon_SplitVersionNumber(): """Test the splitVersionNumber function. From 7f43584ed535a421887916e79b8074bb725850cc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Feb 2022 21:19:12 +0100 Subject: [PATCH 005/179] Merge bugfixes (#1009) * Fix document margin recursion error (#1007) * Fix project file icon path in Windows installer (#1006) --- novelwriter/gui/doceditor.py | 2 +- novelwriter/gui/docviewer.py | 5 +++-- setup/win_setup_embed.iss | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 41442385..b1760be4 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1061,8 +1061,8 @@ class GuiDocEditor(QTextEdit): """If the text editor is resized, we must make sure the document has its margins adjusted according to user preferences. """ - QTextEdit.resizeEvent(self, theEvent) self.updateDocMargins() + QTextEdit.resizeEvent(self, theEvent) return ## diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 72c142a0..4ec59570 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -471,10 +471,11 @@ class GuiDocViewer(QTextBrowser): ## def resizeEvent(self, theEvent): - """Make sure the document title is the same width as the window. + """If the text editor is resized, we must make sure the document + has its margins adjusted according to user preferences. """ - QTextBrowser.resizeEvent(self, theEvent) self.updateDocMargins() + QTextBrowser.resizeEvent(self, theEvent) return def mouseReleaseEvent(self, theEvent): diff --git a/setup/win_setup_embed.iss b/setup/win_setup_embed.iss index 84911592..7505f6c1 100644 --- a/setup/win_setup_embed.iss +++ b/setup/win_setup_embed.iss @@ -51,6 +51,6 @@ Filename: "{app}\pythonw.exe"; Parameters: "{#nwAppExeName}"; Description: "{cm [Registry] Root: HKA; Subkey: "Software\Classes\.nwx\OpenWithProgids"; ValueType: string; ValueName: "novelWriterProject.nwx"; ValueData: ""; Flags: uninsdeletevalue Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx"; ValueType: string; ValueName: ""; ValueData: "novelWriter Project File"; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\assets\icons\x-novelwriter-project.ico" +Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\novelwriter\assets\icons\x-novelwriter-project.ico" Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\pythonw.exe"" ""{app}\{#nwAppExeName}"" ""%1""" Root: HKA; Subkey: "Software\Classes\Applications\{#nwAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: ""; Flags: uninsdeletekey From ce4d3c0b5cb5e3f4d7a29db3992c1f9d256637b1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Mar 2022 22:07:33 +0100 Subject: [PATCH 006/179] Update linting settings, and drop Ubuntu 18.04 releases (#1014) * Add E133 and W503 to ignored list for flake8 * Drop making releases for Ubuntu 18.04 --- .github/workflows/syntax.yml | 4 ++-- setup.cfg | 2 +- setup.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index 560dd7dc..5294afe0 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -27,5 +27,5 @@ jobs: flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics - name: Coding Style Violations run: | - flake8 novelwriter --count --max-line-length=99 --ignore E221,E226,E228,E241 --show-source --statistics - flake8 tests --count --max-line-length=99 --ignore E221,E226,E228,E241 --show-source --statistics + flake8 novelwriter --count --max-line-length=99 --ignore E133,E221,E226,E228,E241,W503 --show-source --statistics + flake8 tests --count --max-line-length=99 --ignore E133,E221,E226,E228,E241,W503 --show-source --statistics diff --git a/setup.cfg b/setup.cfg index 0e005bf5..b88b656b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,6 @@ gui_scripts = universal = 0 [flake8] -ignore = E221,E226,E228,E241 +ignore = E133,E221,E226,E228,E241,W503 max-line-length = 99 exclude = docs/* diff --git a/setup.py b/setup.py index 44d5cb1f..f72ce936 100755 --- a/setup.py +++ b/setup.py @@ -786,7 +786,6 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): bldNum = "0" distLoop = [ - ("18.04", "bionic"), ("20.04", "focal"), ("21.10", "impish"), ("22.04", "jammy"), From 6e3262f78e56de33ca286b7779f09b075f56196a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 20 Mar 2022 17:00:29 +0100 Subject: [PATCH 007/179] Minor fixes and updates (#1026) * Make the Tokenizer class an abstract class * Fix error message when opening non-file project items --- novelwriter/core/item.py | 6 +++--- novelwriter/core/tokenizer.py | 9 +++++++-- tests/test_core/test_core_tokenizer.py | 21 +++++++++++++-------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 07db4a41..67dd836c 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -183,9 +183,9 @@ class NWItem(): self.setParent(xItem.attrib.get("parent", None)) self.setOrder(xItem.attrib.get("order", 0)) - self.setType(xItem.attrib.get("type", None)) - self.setClass(xItem.attrib.get("class", None)) - self.setLayout(xItem.attrib.get("layout", None)) + self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE)) + self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS)) + self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT)) for xValue in xItem: if xValue.tag == "meta": diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index 6a3a9809..271ffd2c 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -1,7 +1,7 @@ """ novelWriter – Text Tokenizer ============================ -Splits a piece of novelWriter markdown text into its elements +Split novelWriter plain text into its elements File History: Created: 2019-05-05 [0.0.1] @@ -27,6 +27,7 @@ import re import logging import novelwriter +from abc import ABC, abstractmethod from operator import itemgetter from functools import partial @@ -40,7 +41,7 @@ from novelwriter.core.document import NWDoc logger = logging.getLogger(__name__) -class Tokenizer(): +class Tokenizer(ABC): # In-Text Format FMT_B_B = 1 # Begin bold @@ -267,6 +268,10 @@ class Tokenizer(): # Class Methods ## + @abstractmethod + def doConvert(self): + raise NotImplementedError + def addRootHeading(self, theHandle): """Add a heading at the start of a new root folder. """ diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index 0c57214b..d277a601 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -28,12 +28,17 @@ from novelwriter.core import NWProject, NWDoc from novelwriter.core.tokenizer import Tokenizer +class BareTokenizer(Tokenizer): + def doConvert(self): + pass + + @pytest.mark.core def testCoreToken_Setters(mockGUI): """Test all the setters for the Tokenizer class. """ theProject = NWProject(mockGUI) - theToken = Tokenizer(theProject) + theToken = BareTokenizer(theProject) # Verify defaults assert theToken._fmtTitle == "%title%" @@ -135,7 +140,7 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): theProject.projLang = "en" theProject._loadProjectLocalisation() - theToken = Tokenizer(theProject) + theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) assert theProject.openProject(nwMinimal) @@ -222,7 +227,7 @@ def testCoreToken_HeaderFormat(mockGUI): """Test the tokenization of header formats in the Tokenizer class. """ theProject = NWProject(mockGUI) - theToken = Tokenizer(theProject) + theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) # Title @@ -426,7 +431,7 @@ def testCoreToken_MetaFormat(mockGUI): """Test the tokenization of meta formats in the Tokenizer class. """ theProject = NWProject(mockGUI) - theToken = Tokenizer(theProject) + theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) # Comment @@ -495,7 +500,7 @@ def testCoreToken_MarginFormat(mockGUI): """Test the tokenization of margin formats in the Tokenizer class. """ theProject = NWProject(mockGUI) - theToken = Tokenizer(theProject) + theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) # Alignment and Indentation @@ -550,7 +555,7 @@ def testCoreToken_TextFormat(mockGUI): """Test the tokenization of text formats in the Tokenizer class. """ theProject = NWProject(mockGUI) - theToken = Tokenizer(theProject) + theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) # Text @@ -672,7 +677,7 @@ def testCoreToken_SpecialFormat(mockGUI): """Test the tokenization of special formats in the Tokenizer class. """ theProject = NWProject(mockGUI) - theToken = Tokenizer(theProject) + theToken = BareTokenizer(theProject) theToken._isNovel = True @@ -877,7 +882,7 @@ def testCoreToken_ProcessHeaders(mockGUI): theProject = NWProject(mockGUI) theProject.projLang = "en" theProject._loadProjectLocalisation() - theToken = Tokenizer(theProject) + theToken = BareTokenizer(theProject) # Nothing theToken._theText = "Some text ...\n" From 99cc3635f7b636793ea5103e972c36207d7a52be Mon Sep 17 00:00:00 2001 From: Martijn Date: Wed, 23 Mar 2022 19:22:44 +0100 Subject: [PATCH 008/179] Add Dutch translation (#1027) * Add @mvdkleijn as translator * Update about.py --- CREDITS.md | 1 + i18n/nw_nl_NL.ts | 4521 ++++++++++++++++++++++++++++++++++ novelwriter/dialogs/about.py | 1 + 3 files changed, 4523 insertions(+) create mode 100644 i18n/nw_nl_NL.ts diff --git a/CREDITS.md b/CREDITS.md index 994768ac..e770434f 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -16,6 +16,7 @@ * Portuguese: Bruno Meneguello (@bkmeneguello) * Simplified Chinese: Qianzhi Long (@longqzh) * Latin American Spanish: Tommy Marplatt (@tmarplatt) +* Dutch: Martijn van der Kleijn (@mvdkleijn) ## Libraries diff --git a/i18n/nw_nl_NL.ts b/i18n/nw_nl_NL.ts new file mode 100644 index 00000000..332ceac8 --- /dev/null +++ b/i18n/nw_nl_NL.ts @@ -0,0 +1,4521 @@ + + + + + Common + + + in the future + in de toekomst + + + + just now + zojuist + + + + a minute ago + een minuut geleden + + + + {0} minutes ago + {0} minuten geleden + + + + an hour ago + een uur geleden + + + + {0} hours ago + {0} uur geleden + + + + a day ago + een dag geleden + + + + {0} days ago + {0} dagen geleden + + + + a week ago + een week geleden + + + + {0} weeks ago + {0} weken geleden + + + + a month ago + een maand geleden + + + + {0} months ago + {0} maanden geleden + + + + a year ago + een jaar geleden + + + + {0} years ago + {0} jaren geleden + + + + Constant + + + + + None + Geen + + + + Novel + Roman + + + + + Plot + Plot + + + + + Characters + Personages + + + + + Locations + Locaties + + + + + Timeline + Tijdslijn + + + + + Objects + Objecten + + + + + Entities + Entiteiten + + + + + Custom + Custom + + + + Archive + Archief + + + + Trash + Prullenbak + + + + + Novel Document + Roman Document + + + + + Project Note + Project Notitie + + + + Root Folder + Hoofdmap + + + + Folder + Map + + + + Novel Title Page + Roman Titel Pagina + + + + Novel Chapter + Roman Hoofdstuk + + + + Novel Scene + Roman Scene + + + + Tag + Label + + + + Point of View + Perspectief + + + + + Focus + Focus + + + + Title + Titel + + + + Level + Niveau + + + + Document + Document + + + + Line + Regel + + + + Chars + Tekens + + + + Words + Woorden + + + + Pars + Par. + + + + POV + Perspectief + + + + Synopsis + Synopsis + + + + Straight single quotation mark + Recht enkel aanhalingsteken + + + + Straight double quotation mark + Recht dubbel aanhalingsteken + + + + Left single quotation mark + Linker enkel aanhalingsteken + + + + Right single quotation mark + Rechter enkel aanhalingsteken + + + + Single low-9 quotation mark + Enkel lage-9 aanhalingsteken + + + + Single high-reversed-9 quotation mark + Enkel hoog-omgekeerd-9 aanhalingsteken + + + + Left double quotation mark + Linker dubbel aanhalingsteken + + + + Right double quotation mark + Rechter dubbel aanhalingsteken + + + + Double low-9 quotation mark + Dubbel lage-9 aanhalingsteken + + + + Double high-reversed-9 quotation mark + Dubbel hoog-omgekeerd-9 aanhalingsteken + + + + Double low-reversed-9 quotation mark + Dubbel laag-omgekeerd-9 aanhalingsteken + + + + Single left-pointing angle quotation mark + Enkel links-wijzende hoek aanhalingsteken + + + + Single right-pointing angle quotation mark + Enkel rechts-wijzende hoek aanhalingsteken + + + + Double left-pointing angle quotation mark + Dubbel links-wijzende hoek aanhalingsteken + + + + Double right-pointing angle quotation mark + Dubbel rechts-wijzende hoek aanhalingsteken + + + + Left corner bracket + Linker hoekbeugel + + + + Right corner bracket + Rechter hoekbeugel + + + + Left white corner bracket + Linker holle hoekbeugel + + + + Right white corner bracket + Rechter holle hoekbeugel + + + + GuiAbout + + + + About novelWriter + Over novelWriter + + + + About + Over + + + + Release + Uitgave + + + + + + + Licence + Licentie + + + + Website: {0} + Website: {0} + + + + Credits + Bijdragen + + + + Developer + Ontwikkelaar + + + + Concept + Concept + + + + i18n + i18n + + + + novelWriter is a markdown-like text editor designed for organising and writing novels. It is written in Python 3 with a Qt5 GUI, using PyQt5. + novelWriter is een markdown-achtige tekstbewerker, ontworpen voor het organiseren en schrijven van romans. Het is geschreven in Python 3 met een Qt5 GUI met behulp van PyQt5. + + + + novelWriter is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + novelWriter is gratis software: u kunt het herdistribueren en/of aanpassen onder de voorwaarden van de GNU General Public License zoals gepubliceerd door de Free Software Foundation, óf versie 3 van de Licentie, óf (naar uw keuze) elke latere versie. + + + + novelWriter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + novelWriter wordt verspreid in de hoop dat het nuttig is, maar ZONDER ENIGE GARANTIE; zonder zelfs de geïmpliceerde garantie van VERKOOPBAARHEID of GESCHIKTHEID VOOR EEN BEPAALD DOEL. + + + + See the Licence tab for the full licence text, or visit the GNU website at {0} for more details. + Zie het tabblad Licentie voor de volledige licentietekst, of bezoek de GNU-website op {0} voor meer details. + + + + Translations + Vertalingen + + + + Theme: {0} + Thema: {0} + + + + + + Author + Auteur + + + + + + Credit + Dank aan + + + + Icons: {0} + Pictogrammen: {0} + + + + Syntax: {0} + Syntaxis: {0} + + + + GuiBuildNovel + + + Build Novel Project + Bouw Roman Project + + + + Title Formats for Novel Files + Titel Indelingen voor Roman Bestanden + + + + Formatting Codes: + Opmaak codes: + + + + {0} for the title as set in the document + {0} voor de titel zoals ingesteld in het document + + + + {0} for chapter number (1, 2, 3) + {0} voor hoofdstuk nummer (1, 2, 3) + + + + {0} for chapter number as a word (one, two) + {0} voor hoofdstuk nummer als een woord (één, twee) + + + + {0} for chapter number in upper case Roman + {0} voor hoofdstuknummer in Romeinse hoofdletters + + + + {0} for chapter number in lower case Roman + {0} voor hoofdstuknummer in Romeinse kleine letters + + + + {0} for scene number within chapter + {0} voor scène nummer binnen hoofdstuk + + + + {0} for scene number within novel + {0} voor scènenummer in de roman + + + + Leave blank to skip this heading, or set to a static text, like for instance '{0}', to make a separator. The separator will be centred automatically and only appear between sections of the same type. + Laat leeg om deze kop over te slaan, of stel een statische tekst in, zoals bijvoorbeeld '{0}', om een scheiding te maken. De scheiding wordt automatisch gecentreerd en verschijnt alleen tussen secties van hetzelfde type. + + + + Not Set + Niet ingesteld + + + + Title + Titel + + + + Chapter + Hoofdstuk + + + + Unnumbered + Ongenummerd + + + + Scene + Scène + + + + Section + Sectie + + + + Language + Taal + + + + Hide scene + Verberg scène + + + + Hide section + Verberg sectie + + + + Font Options + Lettertype Opties + + + + Font family + Lettertype familie + + + + Font size + Lettertypegrootte + + + + Line height + Regelhoogte + + + + Justify text + Tekst uitvullen + + + + Disable styling + Opmaak uitschakelen + + + + Styling Options + Opmaak Opties + + + + Include Options + Invoeg Opties + + + + Include synopsis + Inclusief synopsis + + + + Include comments + Inclusief opmerkingen + + + + Include keywords + Inclusief trefwoorden + + + + Include body text + Inclusief inhoudstekst + + + + File Filter Options + Bestand Filter Opties + + + + Include novel files + Inclusief roman bestanden + + + + Include note files + Inclusief notitie bestanden + + + + Ignore export flag + Negeer export vlag + + + + Export Options + Export Opties + + + + Replace tabs with spaces + Vervang tabs door spaties + + + + Replace Unicode in HTML + Unicode in HTML vervangen + + + + Build Preview + Bouw voorbeeld + + + + Print + Afdrukken + + + + Print Preview + Afdrukvoorbeeld + + + + Print to PDF + Afdrukken naar PDF + + + + Save As + Opslaan als + + + + Open Document (.odt) + Open Document (.odt) + + + + Flat Open Document (.fodt) + Flat Open Document (.fodt) + + + + novelWriter HTML (.htm) + novelWriter HTML (.htm) + + + + novelWriter Markdown (.nwd) + novelWriter Markdown (.nwd) + + + + Standard Markdown (.md) + Standaard Markdown (.md) + + + + GitHub Markdown (.md) + GitHub Markdown (.md) + + + + JSON + novelWriter HTML (.json) + JSON + novelWriter HTML (.json) + + + + JSON + novelWriter Markdown (.json) + JSON + novelWriter Markdown (.json) + + + + Close + Sluiten + + + + Failed to generate preview. The result is too big. + Genereren van voorbeeld mislukt. Het resultaat is te groot. + + + + There were problems when building the project: + Er waren problemen bij het bouwen van het project: + + + + Open Document + Open Document + + + + Flat Open Document + Flat Open Document + + + + Plain HTML + Plain HTML + + + + novelWriter Markdown + novelWriter Markdown + + + + Standard Markdown + Standaard Markdown + + + + GitHub Markdown + GitHub Markdown + + + + JSON + novelWriter HTML + JSON + novelWriter HTML + + + + JSON + novelWriter Markdown + JSON + novelWriter Markdown + + + + PDF + PDF + + + + Save Document As + Document opslaan als + + + + {0} file successfully written to: + {0} bestand succesvol weggeschreven naar: + + + + Failed to write {0} file. {1} + Wegschrijven van {0} bestand mislukt. {1} + + + + GuiBuildNovelDocView + + + This area will show the content of the document to be exported or printed. Press the "Build Preview" button to generate content. + In dit gebied wordt de inhoud van het document getoond die geëxporteerd of afgedrukt moet worden. Druk op de knop "Bouw voorbeeld" om inhoud te genereren. + + + + Unknown + Onbekend + + + + Build Time: + Bouw tijd: + + + + GuiDocEditFooter + + + Status + Status + + + + Line: {0} ({1}) + Regel: {0} ({1}) + + + + Words: {0} ({1}) + Woorden: {0} ({1}) + + + + Document size is {0} bytes + Document grootte is {0} bytes + + + + Words: {0} selected + Woorden: {0} geselecteerd + + + + Character count: {0} + Aantal tekens: {0} + + + + GuiDocEditHeader + + + Edit document meta + Document meta-gegevens bewerken + + + + Search document + Doorzoek document + + + + Toggle Focus Mode + Schakel focus modus in/uit + + + + Close the document + Sluit het document + + + + GuiDocEditSearch + + + + Search + Zoek + + + + Replace + Vervang + + + + Case Sensitive + Hoofdlettergevoelig + + + + Whole Words Only + Alleen Hele Woorden + + + + RegEx Mode + RegEx Modus + + + + Loop Search + Zoekopdracht Herhalen + + + + Search Next File + Doorzoek Volgend Bestand + + + + Preserve Case + Behoud Hoofd/Kleine letters + + + + Close Search + Zoekopdracht Afsluiten + + + + Find in current document + Zoeken in huidige document + + + + Find and replace in current document + Zoek en vervang in huidig document + + + + GuiDocEditor + + + The document you are trying to open is too big. The document size is {0} MB. The maximum size allowed is {1} MB. + Het document dat u probeert te openen is te groot. De documentgrootte is {0} MB. De maximaal toegestane grootte is {1} MB. + + + + Opened Document: {0} + Geopend Document: {0} + + + + The text you are trying to add is too big. The text size is {0} MB. The maximum size allowed is {1} MB. + De tekst die u probeert toe te voegen is te groot. De tekst is {0} MB. De maximaal toegestane grootte is {1} MB. + + + + File Changed on Disk + Bestand Gewijzigd op Schijf + + + + This document has been changed outside of novelWriter while it was open. Overwrite the file on disk? + Dit document is gewijzigd buiten de openstaande novelWriter. Het bestand op de schijf overschrijven? + + + + Could not save document. + Kon document niet opslaan. + + + + Saved Document: {0} + Document Opgeslagen: {0} + + + + Spell checking requires the package PyEnchant. It does not appear to be installed. + Spellingscontrole vereist het pakket PyEnchant. Het lijkt niet geïnstalleerd te zijn. + + + + Spell check complete + Spellingscontrole compleet + + + + File Location + Bestands Locatie + + + + The currently open file is saved in: + Het momenteel geopende bestand is opgeslagen in: + + + + The document has grown too big and you cannot add more text to it. The maximum size of a single novelWriter document is {0} MB. + Het document is te groot geworden en u kunt er niet meer tekst aan toevoegen. De maximale grootte van een enkel novelWriter document is {0} MB. + + + + Follow Tag + Volg Label + + + + Cut + Knippen + + + + Copy + Kopiëren + + + + Paste + Plakken + + + + Select All + Selecteer Alles + + + + Select Word + Selecteer Woord + + + + Select Paragraph + Selecteer Paragraaf + + + + Spelling Suggestion(s) + Spelling Suggestie(s) + + + + No Suggestions + Geen Suggesties + + + + Add Word to Dictionary + Woord Toevoegen aan Woordenboek + + + + Please select some text before calling replace quotes. + Selecteer a.u.b. een tekst voordat u vervang aanhalingstekens aanroept. + + + + GuiDocMerge + + + Merge Documents + Documenten Samenvoegen + + + + Documents to Merge + Documenten om samen te voegen + + + + Drag and drop items to change the order. + Versleep items om de volgorde te wijzigen. + + + + No source documents found. Nothing to do. + Geen brondocumenten gevonden. Niets te doen. + + + + Failed to open document file. + Documentbestand openen mislukt. + + + + No source folder selected. Nothing to do. + Geen bronmap geselecteerd. Niets te doen. + + + + Internal error. + Interne fout. + + + + Could not save document. + Kon document niet opslaan. + + + + Element selected in the project tree must be a folder. + Element geselecteerd in de projectboom moet een map zijn. + + + + GuiDocSplit + + + + Split Document + Splits document + + + + Document Headers + Document kopteksten + + + + Select the maximum level to split into files. + Selecteer het maximale niveau om in bestanden op te splitsen. + + + + Split on Header Level 1 (Title) + Splits op koptekst niveau 1 (Titel) + + + + Split up to Header Level 2 (Chapter) + Opsplitsen tot kop niveau 2 (Hoofdstuk) + + + + Split up to Header Level 3 (Scene) + Opsplitsen tot kop niveau 3 (Scène) + + + + Split up to Header Level 4 (Section) + Opsplitsen tot kop niveau 4 (Sectie) + + + + No source document selected. Nothing to do. + Geen brondocument geselecteerd. Niets te doen. + + + + Could not parse source document. + Brondocument kan niet ontleden worden. + + + + Failed to open document file. + Kon documentbestand niet openen. + + + + No headers found. Nothing to do. + Geen koppen gevonden. Niets te doen. + + + + Cannot add new folder for the document split. Maximum folder depth has been reached. Please move the file to another level in the project tree. + Kan geen nieuwe map toevoegen voor de splitsing van documenten. Maximale diepte van de map is bereikt. Verplaats het bestand naar een ander niveau in de projectstructuur. + + + + The document will be split into {0} file(s) in a new folder. The original document will remain intact. + Het document zal worden opgesplitst in {0} bestand(en) in een nieuwe map. Het oorspronkelijke document blijft intact. + + + + Continue with the splitting process? + Doorgaan met het splitsings-proces? + + + + Could not save document. + Kon document niet opslaan. + + + + Element selected in the project tree must be a file. + Het in de projectboom geselecteerde element moet een bestand zijn. + + + + GuiDocViewFooter + + + Show/hide the references panel + Toon/verberg het referentiespaneel + + + + Activate to freeze the content of the references panel when changing document + Activeer om de inhoud van het referentiespaneel te bevriezen bij het wijzigen van document + + + + Show comments + Opmerkingen weergeven + + + + Show synopsis comments + Synopsis opmerkingen weergeven + + + + References + Referenties + + + + Sticky + Vastpinnen + + + + Comments + Opmerkingen + + + + Synopsis + Synopsis + + + + GuiDocViewHeader + + + Go backward + Ga achterwaarts + + + + Go forward + Ga voorwaarts + + + + Reload the document + Herlaad het document + + + + Close the document + Sluit het document + + + + GuiDocViewer + + + An error occurred while generating the preview. + Er is een fout opgetreden tijdens het genereren van het voorbeeld. + + + + Could not find the reference for tag '{0}'. It either doesn't exist, or the index is out of date. The index can be updated from the Tools menu, or by pressing {1}. + Kon de verwijzing voor tag '{0}' niet vinden. Hij bestaat niet of de index is verouderd. De index kan worden bijgewerkt in het Hulpmiddelenmenu, of door op {1} te drukken. + + + + Copy + Kopiëren + + + + Select All + Selecteer alles + + + + Select Word + Selecteer woord + + + + Select Paragraph + Selecteer paragraaf + + + + GuiItemDetails + + + Label + Label + + + + Status + Status + + + + Class + Klasse + + + + Usage + Gebruik + + + + Characters + Tekens + + + + Words + Woorden + + + + Paragraphs + Paragrafen + + + + GuiItemEditor + + + Item Settings + Item Instellingen + + + + Include when building project + Opnemen bij bouwen van project + + + + Label + Label + + + + Status + Status + + + + Layout + Indeling + + + + GuiMain + + + Project + Project + + + + Novel + Roman + + + + Project Details + Project details + + + + Writing Statistics + Schrijf statistieken + + + + Project Settings + Project instellingen + + + + Editor + Tekstbewerker + + + + Outline + Contour + + + + You are running an untested development version of novelWriter. Please be careful when working on a live project and make sure you take regular backups. + Je gebruikt een ongeteste ontwikkelingsversie van novelWriter. Wees voorzichtig bij het werken aan een live project en zorg ervoor dat je regelmatige reservekopieën maakt. + + + + novelWriter is ready ... + novelWriter is klaar ... + + + + Cannot create a new project when another project is open. + Kan geen nieuw project maken als een ander project geopend is. + + + + A project already exists in that location. Please choose another folder. + Er bestaat al een project op die locatie. Kies een andere map. + + + + New project created ... + Nieuw project aangemaakt... + + + + Close Project + Sluit project + + + + Close the current project? + Sluit het huidige project? + + + + + Changes are saved automatically. + Wijzigingen worden automatisch opgeslagen. + + + + Backup Project + Project reservekopie maken + + + + Backup the current project? + Reservekopie maken van het huidige project? + + + + The project was locked by the computer '{0}' ({1} {2}), last active on {3}. + Het project is vergrendeld door de computer '{0}' ({1} {2}), voor het laatst actief op {3}. + + + + Project Locked + Project vergrendeld + + + + The project is already open by another instance of novelWriter, and is therefore locked. Override lock and continue anyway? + Het project is al geopend door een andere instantie van novelWriter, en is daarom vergrendeld. Vergrendeling negeren en toch verder gaan? + + + + Note: If the program or the computer previously crashed, the lock can safely be overridden. However, overriding it is not recommended if the project is open in another instance of novelWriter. Doing so may corrupt the project. + Opmerking: als het programma of de computer eerder is vastgelopen, kan de vergrendeling veilig worden genegeerd. Het wordt echter niet aanbevolen als het project open is in een andere instantie van novelWriter. Toch doen kan het project beschadigen. + + + + The project index is outdated or broken. Rebuilding index. + De projectindex is verouderd of gebroken. De index wordt herbouwd. + + + + Text files ({0}) + Tekst bestanden ({0}) + + + + Markdown files ({0}) + Markdown bestanden ({0}) + + + + novelWriter files ({0}) + novelWriter bestanden ({0}) + + + + All files ({0}) + Alle bestanden ({0}) + + + + Import File + Importeer bestand + + + + Could not read file. The file must be an existing text file. + Kon het bestand niet lezen. Het bestand moet een bestaand tekst bestand zijn. + + + + Please open a document to import the text file into. + Open a.u.b. een document om het tekst bestand in te importeren. + + + + Import Document + Importeer document + + + + Importing the file will overwrite the current content of the document. Do you want to proceed? + Het importeren van het bestand overschrijft de huidige inhoud van het document. Wilt u doorgaan? + + + + + Indexing: '{0}' + Indexeren: '{0}' + + + + Unknown item + Onbekend item + + + + Indexing completed in {0} ms + Indexeren voltooid in {0} ms + + + + The project index has been successfully rebuilt. + De projectindex is succesvol opnieuw opgebouwd. + + + + Information + Informatie + + + + Warning + Waarschuwing + + + + Error + Foutmelding + + + + This is a bug! + Dit is een bug! + + + + Internal Error + Interne Foutmelding + + + + Exit + Afsluiten + + + + Do you want to exit novelWriter? + Wil je novelWriter afsluiten? + + + + GuiMainMenu + + + &Project + &Project + + + + New Project + Nieuw project + + + + Open Project + Open project + + + + Save Project + Project opslaan + + + + Close Project + Sluit project + + + + Project Settings + Project instellingen + + + + Project Details + Project details + + + + Create Root Folder + Maak hoofdmap aan + + + + Novel Root + Roman hoofdmap + + + + Plot Root + Plot hoofdmap + + + + Character Root + Personage hoofdmap + + + + Location Root + Locatie hoofdmap + + + + Timeline Root + Tijdslijn hoofdmap + + + + Object Root + Object hoofdmap + + + + Entity Root + Entiteit hoofdmap + + + + Custom Root + Aangepaste hoofdmap + + + + Archive Root + Archief hoofdmap + + + + Create Folder + Map aanmaken + + + + Edit Item + Item bewerken + + + + Delete Item + Item verwijderen + + + + Move Item Up + Verplaats item omhoog + + + + Move Item Down + Verplaats item omlaag + + + + Undo Last Move + Ongedaan maken laatste verplaatsing + + + + Empty Trash + Leeg prullenbak + + + + Exit + Afsluiten + + + + &Document + &Document + + + + New Document + Nieuw document + + + + Open Document + Open document + + + + Save Document + Document opslaan + + + + Close Document + Sluit document + + + + View Document + Document weergeven + + + + Close Document View + Sluit document weergave + + + + Show File Details + Toon bestandsdetails + + + + Import Text from File + Tekst importeren uit bestand + + + + Merge Folder to Document + Samenvoegen van map naar document + + + + Split Document to Folder + Document naar map splitsen + + + + &Edit + &Bewerken + + + + Undo + Ongedaan maken + + + + Redo + Opnieuw uitvoeren + + + + Cut + Knippen + + + + Copy + Kopiëren + + + + Paste + Plakken + + + + Select All + Selecteer alles + + + + Select Paragraph + Selecteer paragraaf + + + + &View + &Weergave + + + + Go to Project Tree + Ga naar projectboom + + + + Go to Document Editor + Ga naar Documentbewerker + + + + Go to Document Viewer + Ga naar documentweergave + + + + Go to Outline + Ga naar omlijning + + + + Navigate Backward + Navigeer achteruit + + + + Navigate Forward + Navigeer vooruit + + + + Focus Mode + Focus modus + + + + Full Screen Mode + Volledig scherm modus + + + + &Insert + &Invoegen + + + + Dashes + Streepjes + + + + Short Dash + Korte streep + + + + Long Dash + Lange streep + + + + Horizontal Bar + Horizontale lijn + + + + Figure Dash + Figuur streep + + + + Quote Marks + Aanhalingstekens + + + + Left Single Quote + Enkel Aanhalingsteken Links + + + + Right Single Quote + Enkel Aanhalingsteken Rechts + + + + Left Double Quote + Dubbel Aanhalingsteken Links + + + + Right Double Quote + Dubbele Aanhalingstekens Rechts + + + + Alternative Apostrophe + Alternatieve Apostrof + + + + General Punctuation + Algemene Leestekens + + + + Ellipsis + Ellips + + + + Prime + Priem + + + + Double Prime + Dubbele priem + + + + White Spaces + Witruimtes + + + + Non-Breaking Space + Vaste spatie + + + + Thin Space + Dunne spatie + + + + Thin Non-Breaking Space + Dunne vaste spatie + + + + Other Symbols + Andere symbolen + + + + List Bullet + Lijst opsommingsteken + + + + Hyphen Bullet + Koppelteken opsommingsteken + + + + Flower Mark + Bloem markering + + + + Per Mille + Per mille + + + + Degree Symbol + Graden symbool + + + + Minus Sign + Minus teken + + + + Times Sign + Vermenigvuldigingsteken + + + + Division Sign + Deelteken + + + + Tags and References + Tags en Referenties + + + + Page Break and Space + Pagina-einde en Spatie + + + + Page Break + Nieuwe pagina + + + + Vertical Space (Single) + Verticale spatie (enkel) + + + + Vertical Space (Multi) + Verticale spatie (multi) + + + + &Format + Opmaak + + + + Emphasis + Nadruk + + + + Strong Emphasis + Sterke nadruk + + + + Strikethrough + Doorhalen + + + + Wrap Double Quotes + Dubbele aanhalingstekens omwikkelen + + + + Wrap Single Quotes + Enkel aanhalingsteken omwikkelen + + + + Header 1 (Partition) + Kop 1 (Partitie) + + + + Header 2 (Chapter) + Kop 2 (Hoofdstuk) + + + + Header 3 (Scene) + Kop 3 (Scène) + + + + Header 4 (Section) + Kop 4 (Sectie) + + + + Novel Title + Roman titel + + + + Unnumbered Chapter + Ongenummerd hoofdstuk + + + + Align Left + Links uitlijnen + + + + Align Centre + Centreren + + + + Align Right + Rechts uitlijnen + + + + Indent Left + Links inspringen + + + + Indent Right + Rechts inspringen + + + + Toggle Comment + Opmerking in-/uitschakelen + + + + Remove Block Format + Verwijder blokformaat + + + + Convert Single Quotes + Converteer enkele aanhalingstekens + + + + Convert Double Quotes + Converteer dubbele aanhalingstekens + + + + Remove In-Paragraph Breaks + Verwijder in-paragraaf onderbrekingen + + + + &Search + &Zoeken + + + + Find + Vinden + + + + Replace + Vervangen + + + + Find Next + Volgende zoeken + + + + Find Previous + Vorige zoeken + + + + Replace Next + Vervang volgende + + + + &Tools + &Hulpmiddelen + + + + Check Spelling + Spelling controleren + + + + Re-Run Spell Check + Spellingscontrole opnieuw uitvoeren + + + + Project Word List + Project woordenlijst + + + + Rebuild Index + Index opnieuw opbouwen + + + + Rebuild Outline + Herbouw omlijning + + + + Auto-Update Outline + Auto-update omlijning + + + + Backup Project + Project back-up maken + + + + Build Novel Project + Bouw Roman Project + + + + Writing Statistics + Schrijf Statistieken + + + + Preferences + Voorkeuren + + + + &Help + &Help + + + + About novelWriter + Over novelWriter + + + + About Qt5 + Over Qt5 + + + + User Manual (Online) + Gebruikershandleiding (Online) + + + + User Manual (PDF) + Gebruikershandleiding (PDF) + + + + Report an Issue (GitHub) + Meld een probleem (GitHub) + + + + Ask a Question (GitHub) + Stel een vraag (GitHub) + + + + The novelWriter Website + De novelWriter website + + + + Check for New Release + Controleer op nieuwe release + + + + GuiMainStatus + + + + None + Geen + + + + Editor + Tekstverwerker + + + + Project + Project + + + + Session Time + Sessieduur + + + + Words: {0} ({1}) + Woorden: {0} ({1}) + + + + Project word count (session change) + Aantal projectwoorden (verandering sessie) + + + + Novel word count (session change) + Roman woordtelling (sessie verandering) + + + + GuiNovelTree + + + Novel Outline + Roman omlijning + + + + Words + Woorden + + + + POV + Perspectief + + + + Section title + Sectietitel + + + + Word count + Aantal woorden + + + + Point-of-view character + Point-of-view karakter + + + + GuiOutlineDetails + + + + + + Title + Titel + + + + Chapter + Hoofdstuk + + + + Scene + Scène + + + + Section + Sectie + + + + Document + Document + + + + Status + Status + + + + Characters + Tekens + + + + Words + Woorden + + + + Paragraphs + Paragrafen + + + + Synopsis + Synopsis + + + + Title Details + Titel details + + + + Reference Tags + Referentie tags + + + + GuiOutlineHeaderMenu + + + Select Columns + Selecteer kolommen + + + + GuiPreferences + + + Preferences + Voorkeuren + + + + General + Algemeen + + + + Projects + Projecten + + + + Documents + Documenten + + + + Editor + Tekstverwerker + + + + Highlighting + Markeren + + + + Automation + Automatisering + + + + Quotes + Aanhalingstekens + + + + Some changes will not be applied until novelWriter has been restarted. + Sommige wijzigingen zullen niet worden toegepast totdat novelWriter opnieuw is gestart. + + + + GuiPreferencesAutomation + + + Automatic Features + Automatische Functies + + + + Auto-select word under cursor + Automatisch woord onder cursor selecteren + + + + Apply formatting to word under cursor if no selection is made. + Opmaak toepassen op woord onder de cursor als er geen selectie is gemaakt. + + + + Auto-replace text as you type + Automatisch tekst vervangen terwijl u typt + + + + Allow the editor to replace symbols as you type. + Sta de editor toe om symbolen te vervangen terwijl u typt. + + + + Replace as You Type + Vervang Terwijl U Typt + + + + Auto-replace single quotes + Automatisch enkele aanhalingstekens vervangen + + + + + Try to guess which is an opening or a closing quote. + Probeer te raden wat een openend of afsluitend aanhalingsteken is. + + + + Auto-replace double quotes + Automatisch dubbele aanhalingstekens vervangen + + + + Auto-replace dashes + Automatisch streepjes vervangen + + + + Double and triple hyphens become short and long dashes. + Dubbele en drievoudige koppeltekens worden korte en lange streepjes. + + + + Auto-replace dots + Automatisch stippen vervangen + + + + Three consecutive dots become ellipsis. + Drie opeenvolgende stippen worden ellips. + + + + Automatic Padding + Automatische Opvulling + + + + Insert non-breaking space before + Vaste spatie invoegen voor + + + + Automatically add space before any of these symbols. + Voeg automatisch een spatie toe voor één van deze symbolen. + + + + Insert non-breaking space after + Vaste spatie invoegen na + + + + Automatically add space after any of these symbols. + Voeg automatisch een spatie toe na één van deze symbolen. + + + + Use thin space instead + Gebruik dunne spatie in plaats van + + + + Inserts a thin space instead of a regular space. + Voegt een dunne spatie toe in plaats van een normale spatie. + + + + GuiPreferencesDocuments + + + Text Style + Tekst Stijl + + + + Font family + Lettertype familie + + + + + + + Applies to both document editor and viewer. + Van toepassing op zowel de documentbewerker als de kijker. + + + + Font size + Lettertypegrootte + + + + pt + pt + + + + Text Flow + Tekst Flow + + + + Maximum text width in "Normal Mode" + Maximale tekstbreedte in "Normale Modus" + + + + Set to 0 to disable this feature. + Stel in op 0 om deze functie uit te schakelen. + + + + + + + px + px + + + + Maximum text width in "Focus Mode" + Maximale tekstbreedte in "Focus Modus" + + + + The maximum width cannot be disabled. + De maximale breedte kan niet worden uitgeschakeld. + + + + Hide document footer in "Focus Mode" + Verberg document voettekst in "Focus Modus" + + + + Hide the information bar in the document editor. + Verberg de informatiebalk in de documentbewerker. + + + + Justify the text margins + De tekstmarges uitvullen + + + + Minimum text margin + Minimale tekstmarge + + + + Tab width + Tab breedte + + + + The width of a tab key press in the editor and viewer. + De breedte van een tab teken in de tekstbewerker en kijker. + + + + GuiPreferencesEditor + + + Spell Checking + Spellingscontrole + + + + None + Geen + + + + Not installed + Niet geïnstalleerd + + + + Spell check language + Taal voor spellingscontrole + + + + Available languages are determined by your system. + Beschikbare talen worden bepaald door uw systeem. + + + + Big document limit + Groot document limiet + + + + Full spell checking is disabled above this limit. + Volledige spellingcontrole is uitgeschakeld boven dit limiet. + + + + kB + kB + + + + Word Count + Woord Telling + + + + Word count interval + Woord tellings interval + + + + seconds + seconden + + + + Include project notes in status bar word count + Project notities opnemen in de statusbalk woord telling + + + + Writing Guides + Schrijf Hulpjes + + + + Show tabs and spaces + Tabs en spaties weergeven + + + + Show line endings + Regeleindes weergeven + + + + Scroll Behaviour + Scroll Gedrag + + + + Scroll past end of the document + Scroll voorbij het einde van het document + + + + Set to 0 to disable this feature. + Stel in op 0 om deze functie uit te schakelen. + + + + lines + regels + + + + Typewriter style scrolling when you type + Schrijfmachine stijl scrollen bij het typen + + + + Keeps the cursor at a fixed vertical position. + Houd de cursor op een vaste verticale positie. + + + + Minimum position for Typewriter scrolling + Minimumpositie voor Schrijfmachine scrollen + + + + Percentage of the editor height from the top. + Percentage van de tekstverwerker hoogte vanaf de bovenkant. + + + + GuiPreferencesGeneral + + + Look and Feel + Look and Feel + + + + Main GUI language + Hoofdtaal van GUI + + + + + + + + Requires restart. + Vereist herstart. + + + + Main GUI theme + Hoofd GUI thema + + + + Main icon theme + Hoofd pictogrammen thema + + + + Font family + Lettertype familie + + + + Font size + Lettertypegrootte + + + + pt + pt + + + + GUI Settings + GUI Instellingen + + + + Emphasise partition and chapter labels + Partitie en hoofdstuk labels benadrukken + + + + Makes them stand out in the project tree. + Laat ze opvallen in de projectboom. + + + + Show full path in document header + Volledig pad in document kop weergeven + + + + Add the parent folder names to the header. + Voeg de bovenliggende mapnamen toe aan de kop. + + + + Hide vertical scroll bars in main windows + Verticale schuifbalken in hoofdvensters verbergen + + + + + Scrolling available with mouse wheel and keys only. + Scrollen alleen beschikbaar met muiswiel en toetsen. + + + + Hide horizontal scroll bars in main windows + Verberg horizontale schuifbalken in hoofdvensters + + + + GuiPreferencesProjects + + + Automatic Save + Automatisch Opslaan + + + + Save document interval + Document opslag interval + + + + How often the document is automatically saved. + Hoe vaak het document automatisch wordt opgeslagen. + + + + + seconds + seconden + + + + Save project interval + Project opslag interval + + + + How often the project is automatically saved. + Hoe vaak het project automatisch wordt opgeslagen. + + + + Project Backup + Project Reservekopie + + + + Browse + Blader + + + + Backup storage location + Opslaglocatie voor reservekopie + + + + + Path: {0} + Pad: {0} + + + + Run backup when the project is closed + Reservekopie maken wanneer het project wordt gesloten + + + + Can be overridden for individual projects in Project Settings. + Kan voor individuele projecten overschreven worden in Projectinstellingen. + + + + Ask before running backup + Vraag voor het maken van een reservekopie + + + + If off, backups will run in the background. + Indien uit, worden reservekopieën op de achtergrond gemaakt. + + + + Session Timer + Sessie Timer + + + + Pause the session timer when not writing + De sessie timer pauzeren wanneer niet geschreven wordt + + + + Also pauses when the application window does not have focus. + Pauzeert ook wanneer het toepassingsvenster geen focus heeft. + + + + Editor inactive time before pausing timer + Inactieve tekstbewerker duur voordat timer wordt gepauzeerd + + + + User activity includes typing and changing the content. + Gebruikersactiviteit omvat typen en het wijzigen van de inhoud. + + + + minutes + minuten + + + + Backup Directory + Reservekopie map + + + + GuiPreferencesQuotes + + + Quotation Style + Citeer Stijl + + + + Single quote open style + Enkel aanhalingsteken open stijl + + + + The symbol to use for a leading single quote. + Het symbool om te gebruiken voor een leidend enkel aanhalingsteken. + + + + Single quote close style + Enkel aanhalingsteken sluit stijl + + + + The symbol to use for a trailing single quote. + Het symbool om te gebruiken voor een afsluitend enkel aanhalingsteken. + + + + Double quote open style + Dubbele aanhalingsteken open stijl + + + + The symbol to use for a leading double quote. + Het symbool om te gebruiken voor een leidend dubbel aanhalingsteken. + + + + Double quote close style + Dubbel aanhalingsteken sluit stijl + + + + The symbol to use for a trailing double quote. + Het symbool om te gebruiken voor een afsluitend dubbel aanhalingsteken. + + + + GuiPreferencesSyntax + + + Highlighting Theme + Markeer thema + + + + Highlighting theme + Markeer thema + + + + Colour theme for the editor and viewer. + Kleur thema voor de bewerker en kijker. + + + + Quotes & Dialogue + Aanhalingstekens & Dialoog + + + + Highlight text wrapped in quotes + Markeer tekst verpakt in aanhalingstekens + + + + + + Applies to the document editor only. + Alleen van toepassing op de documentbewerker. + + + + Allow open-ended single quotes + Toestaan van open einde enkele aanhalingstekens + + + + Highlight single-quoted line with no closing quote. + Markeer regel zonder afsluitend enkel aanhalingsteken. + + + + Allow open-ended double quotes + Toestaan van open einde dubbele aanhalingstekens + + + + Highlight double-quoted line with no closing quote. + Markeer regel zonder afsluitend dubbel aanhalingsteken. + + + + Text Emphasis + Tekst nadruk + + + + Add highlight colour to emphasised text + Voeg markeerkleur toe aan geaccentueerde tekst + + + + Text Errors + Tekst Foutmeldingen + + + + Highlight multiple spaces + Meerdere spaties markeren + + + + GuiProjectDetails + + + Project Details + Project Details + + + + Overview + Overzicht + + + + Contents + Inhoud + + + + GuiProjectDetailsContents + + + Title + Titel + + + + Words + Woorden + + + + Pages + Pagina's + + + + Page + Pagina + + + + Progress + Voortgang + + + + Typical word count for a 5 by 8 inch book page with 11 pt font is 350. + Typische woordtelling voor een 5 bij 8 inch boek pagina met 11 pt lettertype is 350. + + + + Start counting page numbers from this page. + Begin met het tellen van paginanummers vanaf deze pagina. + + + + Assume a new chapter or partition always start on an odd numbered page. + Neem aan dat een nieuw hoofdstuk of partitie altijd op een oneven genummerde pagina begint. + + + + Words per page + Woorden per pagina + + + + Count pages from + Pagina's tellen vanaf + + + + Clear double pages + Dubbele pagina's wissen + + + + Table of Contents + Inhoudsopgave + + + + END + EINDE + + + + Untitled + Naamloos + + + + GuiProjectDetailsMain + + + Working Title: {0} + Werktitel: {0} + + + + By {0} + Door {0} + + + + Words + Woorden + + + + Chapters + Hoofdstukken + + + + Scenes + Scènes + + + + Revisions + Revisies + + + + Editing Time + Bewerk tijd + + + + Path + Pad + + + + GuiProjectEditMain + + + Project Settings + Project Instellingen + + + + Working title + Werk titel + + + + Should be set only once. + Mag slechts één keer worden ingesteld. + + + + Novel title + Roman titel + + + + Change whenever you want! + Verander wanneer je maar wilt! + + + + Author(s) + Auteur(s) + + + + One name per line. + Eén naam per regel. + + + + Default + Standaard + + + + Spell check language + Taal voor spellingscontrole + + + + + Overrides main preferences. + Overschrijft de hoofd voorkeuren. + + + + No backup on close + Geen back-up bij sluiten + + + + GuiProjectEditReplace + + + Text Replace List for Preview and Export + Tekst Vervang Lijst voor Voorbeeld en Export + + + + Keyword + Sleutelwoord + + + + Replace With + Vervang door + + + + Select item to edit + Selecteer te bewerken item + + + + Save + Opslaan + + + + GuiProjectEditStatus + + + Novel File Status Levels + Roman Bestand Status Niveaus + + + + Note File Importance Levels + Notitie Bestand Import Niveaus + + + + Label + Label + + + + Usage + Gebruik + + + + Select item to edit + Selecteer te bewerken item + + + + Colour + Kleur + + + + Save + Opslaan + + + + Select Colour + Selecteer kleur + + + + New Item + Nieuw item + + + + Cannot delete a status item that is in use. + Kan status item dat in gebruik is niet verwijderen. + + + + Not in use + Niet in gebruik + + + + Used once + Eenmalig gebruikt + + + + Used by {0} items + Gebruikt door {0} items + + + + GuiProjectLoad + + + + Open Project + Open Project + + + + Working Title + Werk titel + + + + Words + Woorden + + + + Last Opened + Laatst geopend + + + + Recently Opened Projects + Recent Geopende Projecten + + + + Path + Pad + + + + New + Nieuw + + + + Remove + Verwijder + + + + novelWriter Project File ({0}) + novelWriter Projectbestand ({0}) + + + + All files ({0}) + Alle bestanden ({0}) + + + + Remove Entry + Vermelding verwijderen + + + + Remove '{0}' from the recent projects list? The project files will not be deleted. + '{0}' uit de lijst met recente projecten verwijderen? De project bestanden zullen niet worden verwijderd. + + + + GuiProjectSettings + + + Project Settings + Project Instellingen + + + + Settings + Instellingen + + + + Status + Status + + + + Importance + Belangrijkheid + + + + Auto-Replace + Auto-Vervang + + + + GuiProjectTree + + + Project Tree + Project Boom + + + + Words + Woorden + + + + Item label + Item label + + + + Word count + Aantal woorden + + + + Include in build + Opnemen in bouw + + + + Item status + Item status + + + + Please select a valid location in the tree to add the document. + Selecteer een geldige locatie in de boomstructuur om het document aan toe te voegen. + + + + Please select a valid location in the tree to add the folder. + Selecteer een geldige locatie in de boomstructuur om de map aan toe te voegen. + + + + + Did not find anywhere to add the file or folder! + Kon geen plek vinden om het bestand of de map aan toe te voegen! + + + + Cannot add new files or folders to the Trash folder. + Kan geen nieuwe bestanden of mappen toevoegen aan de Prullenbak map. + + + + New File + Nieuw bestand + + + + Cannot add new folder to this item. Maximum folder depth has been reached. + Kan geen nieuwe map toevoegen aan dit item. Maximum map diepte is bereikt. + + + + New Folder + Nieuwe map + + + + There is currently no Trash folder in this project. + Er is momenteel geen Prullenbak map in dit project. + + + + The Trash folder is already empty. + De Prullenbak is al leeg. + + + + Empty Trash + Prullenbak legen + + + + Permanently delete {0} file(s) from Trash? + {0} bestand(en) permanent verwijderen uit de prullenbak? + + + + + Delete File + Verwijder bestand + + + + Permanently delete file '{0}'? + Bestand '{0}' permanent verwijderen ? + + + + Could not delete document file. + Kon documentbestand niet verwijderen. + + + + Move file '{0}' to Trash? + Verplaats bestand '{0}' naar prullenbak? + + + + Cannot delete folder. It is not empty. Recursive deletion is not supported. Please delete the content first. + Kan de map niet verwijderen. Het is niet leeg. Recursief verwijderen wordt niet ondersteund. Verwijder eerst de inhoud. + + + + Cannot delete root folder. It is not empty. Recursive deletion is not supported. Please delete the content first. + Kan de hoofdmap niet verwijderen. Het is niet leeg. Recursieve verwijdering wordt niet ondersteund. Verwijder eerst de inhoud. + + + + + The item cannot be moved to that location. + Het item kan niet worden verplaatst naar die locatie. + + + + There is nowhere to add item with name '{0}'. + Er is geen plek om item toe te voegen met de naam '{0}'. + + + + GuiProjectTreeMenu + + + Edit Project Item + Bewerk projectitem + + + + Open Document + Open document + + + + View Document + Document weergeven + + + + Toggle Included Flag + Toggle inbegrepen vlag + + + + New File + Nieuw bestand + + + + New Folder + Nieuwe map + + + + Delete Item + Item verwijderen + + + + Empty Trash + Leeg prullenbak + + + + Move Item Up + Verplaats item omhoog + + + + Move Item Down + Verplaats item omlaag + + + + GuiUpdates + + + Check for Updates + Controleren op updates + + + + Current Release + Huidige versie + + + + + novelWriter {0} released on {1} + novelWriter {0} uitgebracht op {1} + + + + Latest Release + Nieuwste versie + + + + Checking ... + Wordt gecontroleerd... + + + + Download: {0} + Download: {0} + + + + GuiWordList + + + + Project Word List + Project woordenlijst + + + + Cannot add a blank word. + Kan geen blanco woord toevoegen. + + + + The word '{0}' is already in the word list. + Het woord '{0}' staat al op de woordenlijst. + + + + GuiWritingStats + + + Writing Statistics + Schrijf Statistieken + + + + Session Start + Sessie start + + + + Length + Lengte + + + + Idle + Inactief + + + + Words + Woorden + + + + Histogram + Histogram + + + + Sum Totals + Som totalen + + + + Total Time: + Totale tijd: + + + + Idle Time: + Inactief tijd: + + + + Filtered Time: + Gefilterde tijd: + + + + Novel Word Count: + Roman woord telling: + + + + Notes Word Count: + Notities woord telling: + + + + Total Word Count: + Totaal woord telling: + + + + Filters + Filters + + + + Count novel files + Roman bestanden meetellen + + + + Count note files + Notitiebestanden tellen + + + + Hide zero word count + Verberg nul woorden aantal + + + + Hide negative word count + Negatieve woordtelling verbergen + + + + Group entries by day + Vermeldingen groeperen per dag + + + + Show idle time + Inactieve tijd weergeven + + + + Word count cap for the histogram + Woorden tellingslimiet voor het histogram + + + + Save As + Opslaan als + + + + JSON Data File (.json) + JSON gegevensbestand (.json) + + + + CSV Data File (.csv) + CSV-gegevensbestand (.csv) + + + + JSON Data File + JSON gegevensbestand + + + + CSV Data File + CSV-gegevensbestand + + + + Save Data As + Gegevens opslaan als + + + + {0} file successfully written to: + {0} bestand succesvol geschreven naar: + + + + Failed to write {0} file. + Schrijven van {0} bestand mislukt. + + + + Failed to read session log file. + Kon sessie log bestand niet lezen. + + + + NWProject + + + Duplicate root item detected. + Duplicaat root item gedetecteerd. + + + + + New + Nieuw + + + + Note + Notitie + + + + Draft + Concept + + + + Finished + Voltooid + + + + Minor + Klein + + + + Major + Groot + + + + Main + Hoofd + + + + New Project + Nieuw Project + + + + By + Door + + + + + Novel + Roman + + + + Plot + Plot + + + + Characters + Personages + + + + World + Wereld + + + + + Title Page + Titel pagina + + + + + + New Chapter + Nieuw hoofdstuk + + + + + New Scene + Nieuwe scène + + + + Chapter {0} + Hoofdstuk {0} + + + + + Scene {0} + Scène {0} + + + + File not found: {0} + Bestand niet gevonden: {0} + + + + + Failed to parse project xml. + Parsen van project xml mislukt. + + + + Attempting to open backup project file instead. + Poging om in plaats daarvan het reservekopiebestand van het project te openen. + + + + + Unknown + Onbekend + + + + Project file does not appear to be a novelWriterXML file. + Projectbestand lijkt geen novelWriter XML-bestand te zijn. + + + + Unknown or unsupported novelWriter project file format. The project cannot be opened by this version of novelWriter. The file was saved with novelWriter version {0}. + Onbekend of niet ondersteund bestandsformaat van novelWriter. Het project kan niet worden geopend door deze versie van novelWriter. Het bestand was opgeslagen met versie {0} van novelWriter. + + + + File Version + Bestands versie + + + + The file format of your project is about to be updated. If you proceed, older versions of novelWriter will no longer be able to open this project. Continue? + De bestandsindeling van uw project zal worden bijgewerkt. Als u doorgaat, kunnen oudere versies van novelWriter dit project niet meer openen. Doorgaan? + + + + Version Conflict + Versie conflict + + + + This project was saved by a newer version of novelWriter, version {0}. This is version {1}. If you continue to open the project, some attributes and settings may not be preserved, but the overall project should be fine. Continue opening the project? + Dit project was aangemaakt door een nieuwere versie van novelWriter, versie {0}. Dit is versie {1}. Als je het project blijft openen, kunnen sommige kenmerken en instellingen niet worden behouden, maar over het algemeen moet het project goed zijn. Doorgaan met het openen van het project? + + + + Opened Project: {0} + Geopend project: {0} + + + + Project path not set, cannot save project. + Projectpad niet ingesteld, project kan niet worden opgeslagen. + + + + + Failed to save project. + Opslaan project mislukt. + + + + Saved Project: {0} + Project opgeslagen: {0} + + + + Backing up project ... + Project back-uppen... + + + + Cannot backup project because no valid backup path is set. Please set a valid backup location in Preferences. + Kan geen back-up maken van project omdat er geen geldig backup pad is ingesteld. Stel een geldige back-up locatie in in Voorkeuren. + + + + Cannot backup project because no project name is set. Please set a Working Title in Project Settings. + Kan geen back-up maken van een project omdat er geen projectnaam is ingesteld. Stel een werktitel in bij Projectinstellingen. + + + + Could not create backup folder. + Kan de back-up map niet maken. + + + + Cannot backup project because the backup path is within the project folder to be backed up. Please choose a different backup path in Preferences. + Kan geen back-up maken van het project omdat het back-up pad zich in de projectmap bevindt. Kies een ander backup pad in Voorkeuren. + + + + Backup from {0} + Reservekopie van {0} + + + + Backup archive file written to: {0} + Reservekopie archief bestand weggeschreven naar: {0} + + + + Could not write backup archive. + Kon reservekopie archief niet wegschrijven. + + + + Project backed up to '{0}' + Project reservekopie gemaakt naar '{0}' + + + + + Failed to create a new example project. + Aanmaken van een nieuw voorbeeldproject is mislukt. + + + + Failed to create a new example project. Could not find the necessary files. They seem to be missing from this installation. + Aanmaken van een nieuw voorbeeldproject is mislukt. Kon de benodigde bestanden niet vinden. Ze lijken te ontbreken in deze installatie. + + + + Could not create new project folder. + Kon geen nieuwe projectmap aanmaken. + + + + New project folder is not empty. Each project requires a dedicated project folder. + Nieuwe projectmap is niet leeg. Elk project vereist een eigen projectmap. + + + + You must set a valid backup path in Preferences to use the automatic project backup feature. + U moet een geldig reservekopie pad instellen in de Voorkeuren om de automatische project reservekopie functie te kunnen gebruiken. + + + + You must set a valid project name in Project Settings to use the automatic project backup feature. + U moet een geldige projectnaam instellen in Projectinstellingen om de automatische project reservekopie functie te kunnen gebruiken. + + + + and + en + + + + Could not create folder. + Kon de map niet aanmaken. + + + + Found {0} orphaned file(s) in project folder. + {0} weesbestand(en) gevonden in de projectmap. + + + + Recovered + Hersteld + + + + [{0}] {1} + [{0}] {1} + + + + Recovered File {0} + Hersteld bestand {0} + + + + One or more orphaned files could not be added back into the project. Make sure at least a Novel root folder exists. + Een of meer verweesde bestanden konden niet aan het project worden terug gevoegd. Zorg ervoor dat er tenminste een Roman hoofdmap bestaat. + + + + Not a folder: {0} + Is geen map: {0} + + + + Could not move: {0} + Kon niet verplaatsen: {0} + + + + + Could not delete: {0} + Kon niet verwijderen: {0} + + + + Could not make folder: {0} + Kon map niet maken: {0} + + + + Could not move item {0} to {1}. + Kon item {0} niet verplaatsen naar {1}. + + + + ProjWizardCustomPage + + + Custom Project Options + Aangepaste projectopties + + + + Select which additional root folders to make, and how to populate the Novel folder. If you don't want to add chapters or scenes, set the values to 0. You can add scenes without chapters. + Selecteer welke extra hoofdmappen aan te maken en hoe de Roman map gevuld moet worden. Als je geen hoofdstukken of scènes wilt toevoegen, stel de waarden in op 0. Je kunt scènes toevoegen zonder hoofdstukken. + + + + Additional Root Folders + Aanvullende hoofdmappen + + + + + + + + + {0} folder + {0} map + + + + Populate Novel Folder + Roman map vullen + + + + Add chapters + Hoofdstukken toevoegen + + + + Scenes (per chapter) + Scènes (per hoofdstuk) + + + + Add chapter folders + Hoofdstukmappen toevoegen + + + + ProjWizardFinalPage + + + Finished + Voltooid + + + + All done. + Alles is klaar. + + + + Press '{0}' to create the new project. + Druk op '{0}' om het nieuwe project aan te maken. + + + + Done + Voltooid + + + + Finish + Voltooien + + + + ProjWizardFolderPage + + + + Select Project Folder + Selecteer projectmap + + + + Select a location to store the project. A new project folder will be created in the selected location. + Selecteer een locatie om het project op te slaan. Een nieuwe projectmap zal worden gemaakt op de geselecteerde locatie. + + + + Required + Vereist + + + + Project Path + Project pad + + + + ProjWizardIntroPage + + + Create New Project + Nieuw project maken + + + + Provide at least a working title. The working title should not be change beyond this point as it is used by the application for generating file names for for instance backups. The other fields are optional and can be changed at any time in Project Settings. + Geef ten minste een werk titel op. De werktitel mag niet worden gewijzigd na dit punt, omdat het wordt gebruikt door de applicatie voor het genereren van bestandsnamen voor, bijvoorbeeld, reservekopieën. De andere velden zijn optioneel en kunnen op elk gewenst moment worden gewijzigd in Projectinstellingen. + + + + Side image by {0}, {1} + Zijbeeld door {0}, {1} + + + + Required + Vereist + + + + Optional + Optioneel + + + + Optional. One name per line. + Optioneel. Eén naam per regel. + + + + Working Title + Werk titel + + + + Novel Title + Roman titel + + + + Author(s) + Auteur(s) + + + + ProjWizardPopulatePage + + + Populate Project + Project bevolken + + + + Choose how to pre-fill the project. Either with a minimal set of starter items, an example project explaining and showing many of the features, or show further custom options on the next page. + Kies hoe het project vooraf moet worden ingevuld. Ofwel met een minimale set van starter items, een voorbeeldproject dat veel van de functies uitlegt en laat zien, of toon verdere aanpasbare opties op de volgende pagina. + + + + Fill the project with a minimal set of items + Vul het project met een minimale set items + + + + Fill the project with example files + Vul het project met voorbeeldbestanden + + + + Show detailed options for filling the project + Toon gedetailleerde opties voor het vullen van het project + + + + QDialogButtonBox + + + OK + OK + + + + QGnomeTheme + + + &OK + &OK + + + + &Save + &Opslaan + + + + &Cancel + &Annuleren + + + + &Close + &Sluiten + + + + Close without Saving + Sluiten zonder opslaan + + + + QPlatformTheme + + + OK + OK + + + + Save + Opslaan + + + + Save All + Alles opslaan + + + + Open + Openen + + + + &Yes + &Ja + + + + Yes to &All + Ja voor &alles + + + + &No + &Nee + + + + N&o to All + N&ee op alles + + + + Abort + Afbreken + + + + Retry + Opnieuw proberen + + + + Ignore + Negeren + + + + Close + Sluiten + + + + Cancel + Annuleren + + + + Discard + Weggooien + + + + Help + Help + + + + Apply + Toepassen + + + + Reset + Beginwaarden + + + + Restore Defaults + Standaardwaarden herstellen + + + + QWizard + + + Go Back + Ga terug + + + + < &Back + < &Terug + + + + Continue + Doorgaan + + + + &Next + &Volgende + + + + &Next > + &Volgende > + + + + Commit + Vastleggen + + + + Done + Gereed + + + + &Finish + Vol&tooien + + + + + Cancel + Annuleren + + + + Help + Help + + + + &Help + &Help + + + + Tokenizer + + + Synopsis + Synopsis + + + + Document '{0}' is too big ({1} MB). Skipping. + Document '{0}' is te groot ({1} MB). Overgeslagen. + + + + ERROR + FOUT + + + diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index c8b9c898..574fba34 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -187,6 +187,7 @@ class GuiAbout(QDialog): ("Português", "Bruno Meneguello"), ("简体中文", "Qianzhi Long"), ("Español Latinoamericano", "Tommy Marplatt"), + ("Nederlands", "Martijn van der Kleijn"), ]) ) From a04b44d8905a86066a92ffde3fa53a6c48cf4301 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 2 Apr 2022 17:41:35 +0200 Subject: [PATCH 009/179] Add Lorem Ipsum tool (#1028) * Add a Lorem Ipsum dialog * Add the Lorem Ipsum text and insert action * Add test coverage --- novelwriter/assets/text/lipsum.txt | 100 ++++++++++++++++++ novelwriter/gui/mainmenu.py | 5 + novelwriter/guimain.py | 22 +++- novelwriter/tools/__init__.py | 2 + novelwriter/tools/lipsum.py | 142 ++++++++++++++++++++++++++ tests/test_tools/test_tools_lipsum.py | 76 ++++++++++++++ 6 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 novelwriter/assets/text/lipsum.txt create mode 100644 novelwriter/tools/lipsum.py create mode 100644 tests/test_tools/test_tools_lipsum.py diff --git a/novelwriter/assets/text/lipsum.txt b/novelwriter/assets/text/lipsum.txt new file mode 100644 index 00000000..f9743e26 --- /dev/null +++ b/novelwriter/assets/text/lipsum.txt @@ -0,0 +1,100 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus diam nibh, tincidunt et quam at, fringilla malesuada risus. Nullam eleifend, sem nec varius tincidunt, urna mi varius dolor, sit amet gravida risus eros at purus. Etiam vehicula hendrerit elit, sit amet pulvinar dolor viverra sed. Curabitur metus ex, gravida at sodales sed, tristique eget diam. Suspendisse ultricies lorem sed ullamcorper rutrum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis tempus magna mi, sit amet sagittis arcu viverra ut. +Fusce in pretium tellus. Donec finibus vitae arcu at consectetur. Suspendisse et ultricies nisl. Integer et nisi vel sem aliquam scelerisque. Curabitur placerat massa et mi feugiat vulputate. In ac orci vitae risus dictum porttitor quis ut dui. Vivamus egestas condimentum risus quis fermentum. Nam lorem ante, rhoncus vel porttitor in, sodales in purus. Mauris ultricies ex metus, molestie ullamcorper arcu suscipit id. +Cras dapibus porta eros, a tincidunt sapien mattis sit amet. Pellentesque ac metus in dui accumsan tristique eget eu tellus. Nam laoreet sapien vitae hendrerit ullamcorper. Mauris accumsan semper dui vitae pellentesque. Donec non velit cursus, eleifend nulla a, porttitor tellus. Donec elementum lobortis imperdiet. Etiam fermentum pretium arcu, sed consequat dui mattis nec. Mauris cursus fringilla magna, at pharetra justo. Sed tincidunt consequat urna, quis varius lacus dapibus id. Fusce sagittis lorem vitae sodales tempor. Ut vel feugiat lacus, in iaculis risus. Aliquam viverra tortor nec nibh tristique varius. Mauris interdum leo vitae massa sagittis venenatis. +Quisque cursus eu orci at viverra. Donec libero libero, sagittis a sagittis vitae, efficitur sed nibh. Donec imperdiet malesuada est. Etiam consequat quam arcu, quis egestas libero sagittis a. Donec purus urna, volutpat id ante nec, iaculis maximus velit. Vivamus malesuada lacus sed velit consequat fringilla. Pellentesque ornare accumsan aliquam. Suspendisse tempor vel nisi quis iaculis. Curabitur porta ligula ac libero molestie, nec viverra ex feugiat. Donec lacinia eget lorem eu lacinia. Maecenas vestibulum ornare dui a aliquet. +Proin id lobortis nunc, ut feugiat urna. Nam dictum odio tortor, bibendum sollicitudin erat suscipit eu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Suspendisse eu imperdiet lectus. Nullam faucibus elit leo, sit amet pulvinar diam iaculis ac. Pellentesque accumsan vulputate orci sed ornare. Fusce at tortor ac libero volutpat tempus nec a diam. Nam eu libero tristique, rutrum magna suscipit, malesuada ligula. +Praesent suscipit imperdiet arcu vitae faucibus. Phasellus massa mauris, pharetra at posuere vel, fringilla sed quam. Morbi nec congue ante, vel vulputate massa. Suspendisse imperdiet mollis dignissim. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis pulvinar odio tellus, non cursus turpis malesuada sit amet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque ex lorem, hendrerit ac enim et, tempor sodales ante. Curabitur quis tristique mauris, quis imperdiet metus. Mauris tristique interdum congue. Phasellus finibus condimentum pretium. Sed et tellus vel quam molestie aliquet. +Phasellus sagittis ligula nibh, nec hendrerit libero maximus et. Morbi vestibulum, tortor non efficitur semper, velit lacus molestie augue, ac volutpat elit ante ac augue. Pellentesque ut tristique odio, vel ornare eros. Nunc at ullamcorper arcu, in ornare risus. Vivamus sed libero auctor, vulputate lectus ut, luctus lectus. Maecenas finibus at nulla vel congue. Nullam finibus mauris leo, ac volutpat sem pulvinar eget. Maecenas in posuere tellus, in placerat est. Ut at est semper, ullamcorper ligula ac, volutpat nisi. Donec tempus tellus sed lorem posuere, a rutrum odio faucibus. Donec porta sem ac tortor vulputate, sed pharetra sapien convallis. Praesent ac feugiat odio. In risus eros, pharetra sit amet porta eu, lacinia vel nunc. +Praesent consectetur tincidunt mauris ut dignissim. Aenean sagittis ornare mi at sagittis. Proin id suscipit ipsum. Mauris lacinia commodo molestie. Fusce nec pharetra purus. Nulla eget velit varius, tincidunt erat tincidunt, cursus odio. Aliquam accumsan risus non nibh hendrerit fermentum. Aliquam erat volutpat. Phasellus non tincidunt lacus. Quisque sagittis augue eu egestas maximus. Integer sed eleifend neque, nec finibus sapien. Aliquam malesuada urna nec justo sodales dignissim. Cras volutpat maximus euismod. Pellentesque consequat lorem non augue placerat dignissim. Ut dolor nisl, malesuada sed nunc quis, malesuada dignissim nibh. Nam hendrerit hendrerit sapien quis auctor. +Aenean feugiat enim eros, interdum tincidunt eros aliquam sit amet. Fusce hendrerit mi vulputate nisi vestibulum tempor. Vestibulum at dui et nisl viverra condimentum id sit amet leo. Phasellus vitae leo commodo, blandit magna ut, commodo enim. Donec ex elit, mattis non tincidunt quis, posuere ut sapien. Aliquam lacus mauris, commodo sed libero vitae, rhoncus sodales massa. Phasellus commodo, leo sit amet sollicitudin luctus, massa purus aliquet lectus, at rutrum nulla ante eget felis. Nulla facilisi. Aenean cursus ut diam non volutpat. Aliquam nec erat non ipsum ultricies hendrerit. Phasellus blandit ac lacus non laoreet. Etiam ultricies risus mauris, a volutpat nibh cursus in. Maecenas viverra odio sit amet libero feugiat tempor. Nullam eu tristique magna. Praesent sed lacus ligula. +Sed iaculis viverra mollis. Phasellus sed eros sit amet lectus vulputate ornare sed non dui. Donec pretium dui quis felis feugiat elementum. Quisque eleifend eget nunc eu cursus. Etiam finibus lectus non ipsum ornare malesuada. Integer nec eros ullamcorper, pharetra neque at, suscipit ex. Nam faucibus sapien est, vel viverra ante viverra a. +Ut quis euismod lorem. Fusce auctor quam eu velit semper, sed pretium odio semper. Fusce vehicula porta dignissim. Integer suscipit ultrices ultricies. Aenean pharetra cursus sem. Suspendisse ut porttitor neque. In luctus purus sagittis risus gravida pulvinar. Integer eu ante convallis, pharetra velit vel, sodales urna. Curabitur pretium sapien arcu, eget dignissim neque ornare in. Vivamus eget metus et diam pulvinar aliquam vel eu turpis. Donec augue metus, dapibus eu porta a, faucibus in felis. Etiam sodales arcu a ex fermentum, sit amet rutrum est auctor. Integer gravida lorem in mauris tincidunt lobortis. +Etiam lobortis dictum dapibus. Nulla tincidunt placerat aliquam. Mauris non ornare nisi. Integer hendrerit ornare nibh, non vehicula sapien maximus eu. Proin purus erat, dapibus id dui commodo, blandit sodales mauris. Sed a vulputate lectus. Curabitur vel tincidunt orci. Vivamus cursus augue non nisl dictum, eget venenatis mi eleifend. Integer in ultricies ligula. Phasellus et sodales est, eget mollis magna. Sed lacinia, ligula eget feugiat ullamcorper, eros ipsum dignissim sapien, in suscipit mauris odio at metus. Suspendisse eget felis eu nulla porttitor dignissim at eu felis. Duis pulvinar est mauris, sit amet volutpat lacus hendrerit non. Aliquam finibus rhoncus mauris. Nunc dui nunc, lacinia aliquam finibus ut, maximus quis odio. Sed tempus dui vel orci vehicula, a tristique risus consectetur. +Ut egestas velit eu urna imperdiet cursus. Aenean blandit pretium turpis, quis sollicitudin lacus dignissim eu. In eu orci posuere, volutpat velit et, varius velit. Mauris rutrum sem nunc, nec sagittis lectus accumsan at. Morbi vestibulum est ut dolor maximus, non euismod magna consequat. Morbi pharetra gravida velit ac suscipit. Fusce vitae turpis eget ante tempus bibendum. Integer sollicitudin vulputate neque eu pulvinar. Etiam consequat pulvinar lorem eu dignissim. Fusce luctus id lacus ut fermentum. Vestibulum ligula neque, finibus et aliquam vel, consequat id lacus. Quisque pretium elit a tincidunt aliquam. +Maecenas varius magna in dictum dapibus. Vestibulum eget mattis velit, ac accumsan tellus. Donec luctus lorem in nisl convallis, eget egestas erat varius. Phasellus et lacinia dui. Praesent id nunc elementum, ultricies dolor a, pulvinar tellus. In a auctor mi, in fermentum justo. Proin sit amet vulputate leo, eu ultrices sem. Integer at sagittis ex, eu scelerisque velit. +Curabitur auctor mollis nunc quis venenatis. Cras lectus ligula, auctor in tellus eget, aliquet molestie odio. Vivamus vel venenatis sem. Sed ac purus suscipit, pulvinar sapien sed, commodo arcu. Donec eget metus ipsum. Quisque posuere congue hendrerit. Phasellus eu scelerisque ipsum, ut vulputate diam. Curabitur sollicitudin tortor sapien, a finibus arcu consequat non. Suspendisse tempus magna porttitor mauris laoreet, quis tincidunt lectus iaculis. Fusce nibh magna, finibus ac enim at, volutpat dapibus metus. Duis iaculis sapien non erat vestibulum auctor. +Maecenas nec porta leo. Fusce aliquam massa sit amet blandit interdum. Donec malesuada enim in eros laoreet iaculis. Integer fermentum faucibus nunc, et congue massa mattis eu. Pellentesque sed sodales orci. Praesent pulvinar lacus eget turpis condimentum imperdiet. Nullam molestie, ipsum a molestie accumsan, lectus quam sodales eros, malesuada faucibus ex leo malesuada velit. Fusce molestie ligula at sapien dictum semper non rutrum sapien. Ut facilisis gravida ante nec faucibus. Sed aliquet luctus auctor. In tempor libero at eleifend convallis. Phasellus in tristique ligula. Vestibulum eget quam dapibus, luctus enim vel, sagittis eros. Cras laoreet sapien diam, eu posuere nisl blandit quis. +Vestibulum bibendum a massa ac faucibus. Maecenas vestibulum arcu id diam mattis, non varius orci aliquet. Vivamus sit amet ultrices velit. Donec vitae lorem vel lectus dignissim porta. Sed ac dui dui. Suspendisse lectus ante, pellentesque non euismod vel, finibus ut erat. Vestibulum accumsan facilisis velit, at posuere velit malesuada at. Mauris aliquam tortor sed pretium viverra. Ut et porttitor ex. In quis tellus vitae neque dignissim finibus nec eu dolor. Phasellus viverra nulla a vestibulum auctor. Praesent in lorem gravida, mattis enim commodo, volutpat tellus. Quisque non urna ac eros sollicitudin blandit sit amet ut risus. Integer sed diam a metus tristique consectetur vitae non velit. Aliquam justo est, pharetra vel libero in, molestie varius enim. +Fusce vestibulum auctor varius. Maecenas malesuada, purus quis congue vehicula, arcu purus congue nunc, non convallis felis magna quis nisi. Curabitur nisi diam, imperdiet et sollicitudin quis, ultricies nec enim. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus rutrum, lectus at condimentum condimentum, felis justo lacinia orci, non venenatis nisl ex sed enim. Aenean semper ligula a diam cursus, ac sollicitudin ligula pretium. Etiam eleifend lorem nec massa tempor sollicitudin. Phasellus dignissim velit dignissim mauris vestibulum, sit amet sagittis sem blandit. Donec lobortis varius nunc sit amet posuere. Cras eu est lobortis, aliquet nisi in, varius mauris. Curabitur vitae leo laoreet sem condimentum vestibulum et eget enim. Nullam sed turpis ac nunc ornare tristique sit amet vel arcu. Cras efficitur ullamcorper lorem, et scelerisque lectus volutpat a. Maecenas ut urna in lacus rutrum volutpat. +Pellentesque pretium, neque quis cursus sagittis, justo mauris euismod turpis, non feugiat lacus augue a magna. Nam ornare dictum erat et consequat. Vivamus fringilla odio velit, vitae convallis arcu vulputate et. Maecenas tristique purus ac velit cursus fringilla. Suspendisse id blandit dui, eu facilisis metus. Proin a erat rutrum, tempus lectus ut, consequat nisi. In dui sem, bibendum nec nibh nec, sollicitudin varius ipsum. Nulla id lectus eu eros placerat faucibus eget nec nunc. Nullam urna lectus, tempus nec libero a, egestas malesuada enim. +Curabitur vestibulum a nibh eget varius. Sed lacus ipsum, porta sit amet egestas sit amet, pellentesque a dui. Etiam auctor mollis orci, eget pulvinar magna tristique quis. Proin condimentum ornare nibh, sed interdum orci congue at. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas quis lacinia tellus. Nullam ac nibh auctor, fermentum velit ac, rutrum ipsum. Nam egestas sodales hendrerit. +Suspendisse a feugiat mauris. Nunc eget nisi augue. Suspendisse pharetra justo sit amet sollicitudin tincidunt. Duis pretium, leo quis euismod sagittis, ex ante sodales purus, vel tempor odio nisi ut diam. Praesent odio erat, ultricies et eros ac, pulvinar aliquet justo. Pellentesque non vestibulum sem, a vulputate purus. Nunc rutrum nulla vitae mattis pharetra. Integer dui diam, pulvinar sit amet velit bibendum, aliquet dictum nulla. Nam ut magna et lorem ornare dignissim eget quis mauris. Vestibulum aliquet mi ac rutrum varius. Mauris a imperdiet eros. Curabitur lobortis lectus vitae leo aliquam auctor. Ut et tincidunt dui. +Mauris luctus risus eu tempor pulvinar. Mauris et mi nec ligula imperdiet vulputate a congue ex. Proin et venenatis mauris. Vivamus viverra accumsan lorem at gravida. Aliquam non pretium ante. Morbi aliquam risus sapien, quis consequat tellus maximus vitae. Sed consequat libero in molestie faucibus. Donec vestibulum arcu a sodales volutpat. Nullam ultricies ante quis quam accumsan rutrum. Quisque eu egestas velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu risus vestibulum enim iaculis pellentesque iaculis eu ligula. Nam rutrum, mauris sit amet efficitur eleifend, massa quam consequat metus, eget sodales leo sem eget risus. Aliquam dui libero, maximus at dui nec, consectetur maximus ligula. Sed gravida erat eget tristique lobortis. +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean turpis velit, vestibulum a consequat vitae, efficitur id urna. Cras dapibus dui et scelerisque luctus. Sed vel malesuada justo. Suspendisse molestie iaculis lacus eu tempor. Praesent laoreet lectus quis malesuada volutpat. Fusce consectetur mollis ligula, a lobortis justo suscipit id. Integer eget tellus in arcu viverra accumsan eu et odio. Praesent ut felis risus. Pellentesque convallis felis et quam condimentum, at pellentesque nulla eleifend. Sed eget efficitur dui. Vivamus congue, leo eu lacinia condimentum, erat mi dignissim purus, sit amet convallis lorem sem sed ligula. Duis viverra leo enim, in pharetra felis consectetur viverra. Integer euismod feugiat ipsum eget vestibulum. Morbi fermentum dui vitae tincidunt dictum. +Phasellus non eros ut ipsum pretium condimentum. Vestibulum tristique convallis aliquam. Phasellus tempor leo sit amet diam tristique, a auctor urna hendrerit. Mauris at velit euismod, sollicitudin mi at, scelerisque est. Mauris pulvinar consequat quam, eget varius augue tincidunt id. Curabitur turpis lectus, eleifend a egestas id, malesuada non diam. Proin aliquet tellus urna, venenatis consequat tortor finibus et. Quisque at tempor magna. +Donec nec congue erat. Donec mauris lorem, dignissim euismod massa non, egestas pulvinar est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean aliquam venenatis eleifend. Vivamus consectetur ante et fringilla sodales. Nam vestibulum felis non sapien suscipit consectetur et interdum velit. Sed at placerat massa. Proin quis leo lectus. Integer diam velit, gravida quis rhoncus eget, tempor at ante. Aliquam pharetra dictum mi ac venenatis. Nunc volutpat id magna a pellentesque. In rutrum leo et ligula finibus, ut pretium lorem tristique. Curabitur vel orci iaculis, pellentesque justo et, ullamcorper orci. Quisque erat ante, venenatis ac lacinia ut, maximus ac odio. Aliquam varius bibendum varius. +Duis ipsum orci, semper sit amet volutpat quis, scelerisque a nunc. Donec iaculis, erat vitae malesuada rutrum, nisl leo gravida nisi, et dignissim eros eros at mi. Cras volutpat est erat. Maecenas ex felis, porttitor vitae egestas sit amet, tincidunt efficitur tortor. Curabitur ac purus massa. Etiam porta maximus tristique. Morbi placerat pulvinar lectus sed tempus. Morbi fringilla odio posuere, volutpat mi sed, congue lorem. Cras convallis volutpat eros mattis vestibulum. Ut sit amet sodales eros, ac condimentum purus. Vivamus et elit augue. Pellentesque efficitur gravida ullamcorper. Sed non faucibus tellus, et consectetur urna. Pellentesque at turpis fringilla ipsum interdum hendrerit eu in dui. Nullam dictum eget metus non sollicitudin. Proin facilisis tincidunt euismod. +Nunc et dui porta, suscipit mi vitae, pretium ante. Mauris a accumsan magna. Fusce tincidunt, nunc ut ullamcorper laoreet, nibh risus dignissim eros, a maximus arcu nunc congue arcu. Vivamus imperdiet quam lorem, vulputate consequat ante aliquet id. Sed porttitor mollis ullamcorper. Praesent dui justo, hendrerit quis vulputate sit amet, vulputate id libero. Etiam quam odio, dictum sit amet nisi in, semper placerat erat. Vivamus lacinia augue a dolor tincidunt dignissim. Curabitur ut massa sit amet elit tincidunt maximus. +Integer sed lorem ac lacus tincidunt condimentum quis vel diam. Etiam vitae justo interdum, accumsan ex vel, pretium magna. Aenean id turpis malesuada, semper nisi vel, hendrerit leo. Vestibulum feugiat neque nec lacus auctor efficitur. Ut quis lobortis lacus, sit amet tempor orci. Donec a fringilla sem, nec sollicitudin ex. Sed vel faucibus libero. Sed pharetra leo sed porta ullamcorper. In auctor semper metus, id semper nunc rutrum a. Morbi rhoncus nulla quis ex condimentum, vel congue lectus tempor. Duis a venenatis est. Etiam dolor justo, rhoncus at lobortis quis, malesuada at quam. Donec facilisis convallis mi vitae rhoncus. Integer est ipsum, sollicitudin eu nulla in, accumsan egestas nisi. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nunc finibus sagittis nisl, et luctus ex. +In molestie est luctus magna hendrerit, non tristique felis ultricies. Curabitur non lectus placerat, tincidunt ipsum quis, mollis nunc. Nam lacinia ullamcorper hendrerit. Fusce porttitor nisi laoreet, elementum velit vitae, venenatis mi. Aliquam venenatis dui ac justo varius fermentum. Etiam nec ligula eu neque aliquam placerat mollis ac dolor. Pellentesque gravida, enim commodo tempus faucibus, lorem nibh vulputate arcu, sodales pellentesque metus ligula volutpat sem. In scelerisque nisi ac ante lacinia auctor. +Nunc at sodales libero, ut rutrum elit. Etiam vitae venenatis metus. Nullam faucibus turpis nisl. Maecenas varius sit amet lectus a ornare. Cras eget tortor mauris. Phasellus mattis nec tortor non sagittis. Integer eget placerat massa. Praesent eu porttitor leo. Vivamus sed bibendum sapien, sit amet maximus libero. Donec at lobortis lacus. +Maecenas convallis eros nibh, nec pellentesque magna lacinia vulputate. Donec posuere neque ut lacinia iaculis. Quisque varius dictum condimentum. Aliquam lacinia elit sed sem porttitor tempus a ac ipsum. Aliquam suscipit ex vel tellus mollis, nec suscipit nunc ullamcorper. Cras urna magna, feugiat a odio a, pharetra congue velit. Proin quis hendrerit odio. Proin non orci ut sem ultricies sodales. Morbi dictum orci non enim tempor, ut condimentum erat maximus. Nam sagittis ornare lacus, at sodales dolor porttitor nec. Mauris dignissim ipsum in massa rutrum blandit. Nulla sit amet ante vestibulum, pharetra tellus eu, elementum tortor. Donec nec fringilla mi. +Donec rhoncus odio a iaculis ullamcorper. Nam condimentum volutpat ante quis viverra. Vestibulum malesuada nibh tincidunt rutrum interdum. Sed dapibus et nisl vel ornare. Nunc ullamcorper nibh nec commodo eleifend. Nulla facilisi. Nullam ut est auctor ligula viverra dictum pellentesque id odio. Aenean eleifend ullamcorper mollis. Morbi quis rutrum dolor, vel dignissim purus. In vel metus scelerisque, auctor est quis, efficitur orci. Donec volutpat dui eu vulputate maximus. In hac habitasse platea dictumst. Quisque vel vulputate lorem. Phasellus lacinia tempus lectus, sed tristique magna hendrerit eu. Nullam non metus euismod dolor fringilla imperdiet et vitae purus. Sed varius ex tortor, vel rutrum turpis sagittis ac. +Sed nec tincidunt turpis, sit amet suscipit nisi. Curabitur ultrices orci ligula, nec cursus eros placerat non. Pellentesque faucibus venenatis nisi in condimentum. Morbi facilisis mauris odio, id facilisis ex dictum ut. Duis bibendum ultricies elit, sit amet dapibus risus ultricies quis. Nulla eget ullamcorper risus, ac imperdiet nulla. Proin ultrices nisi eu turpis elementum hendrerit. Maecenas euismod pretium quam accumsan finibus. Morbi augue enim, laoreet nec nunc id, lobortis hendrerit lorem. Nunc mattis cursus dui vel lacinia. Aenean massa tellus, laoreet sit amet turpis vel, pharetra facilisis dui. +Nunc sit amet tellus vel ligula vulputate dapibus. Donec non congue nisi. Mauris risus elit, vehicula a imperdiet a, tempor gravida ipsum. Sed erat magna, volutpat ac nunc sit amet, aliquam lobortis est. Mauris quis urna sem. Sed risus justo, vestibulum ac pharetra in, dignissim id nibh. Etiam nibh justo, auctor in tellus euismod, rhoncus lobortis dolor. Proin felis lectus, hendrerit nec dignissim nec, molestie a lorem. Phasellus commodo pellentesque ligula nec imperdiet. In quis sem a erat euismod elementum at sit amet sem. +Praesent tortor enim, iaculis sit amet placerat in, sodales non erat. Phasellus porttitor, massa vel posuere tincidunt, mauris magna ultrices mi, in aliquet sapien sapien ut libero. Sed accumsan odio ut mollis dignissim. Aliquam varius egestas condimentum. Etiam id massa condimentum, scelerisque velit sed, vulputate augue. Duis mollis augue eu felis venenatis dapibus. Vivamus non gravida nulla. Integer a mi mollis, pharetra ipsum eget, tincidunt sapien. +Phasellus libero arcu, aliquet rutrum commodo vel, efficitur quis libero. Morbi sed eros ante. Quisque efficitur leo eget nulla lobortis, eget imperdiet sem vehicula. Nunc commodo lorem sit amet felis aliquam, non facilisis velit tristique. Aliquam dapibus rutrum dignissim. Phasellus elementum nulla a enim venenatis, at ornare sapien feugiat. Proin ut sem eu nulla rutrum volutpat non non quam. Fusce vel pellentesque lectus, vitae pellentesque est. Praesent sit amet dolor non purus feugiat pharetra. Donec ullamcorper diam vitae sem varius, eget imperdiet nisi viverra. Fusce fermentum dui risus. Etiam sed diam ut libero sollicitudin faucibus at in sapien. Suspendisse bibendum purus at urna vehicula, sed maximus nisi pharetra. Proin posuere nisl ac consectetur sollicitudin. Nam auctor in ex scelerisque imperdiet. +Donec consequat arcu non lacus ullamcorper, a fermentum felis lobortis. Nunc tempor est quam, ut finibus odio rutrum sit amet. Proin hendrerit tincidunt nunc, sed tincidunt nisi tempor eget. Phasellus maximus a risus ut luctus. Duis libero enim, varius a tellus vitae, dignissim facilisis felis. Vestibulum iaculis tempor condimentum. Sed mollis velit justo, quis mollis lectus imperdiet sed. Quisque laoreet eget eros porta imperdiet. Curabitur pretium velit quis sapien placerat, in blandit metus suscipit. Ut pretium urna lectus, sed interdum arcu fermentum et. +Sed euismod nunc quam, nec tincidunt dui tempus at. Mauris molestie, quam vel lobortis dignissim, justo sem porta orci, nec commodo enim orci eget dolor. Pellentesque eros risus, fermentum non urna et, ultrices pharetra augue. Curabitur at tincidunt arcu. Etiam vel sollicitudin libero. Nulla dapibus orci odio, sit amet mattis quam sodales ac. Sed iaculis at eros volutpat egestas. In at fringilla massa. Proin at mattis turpis. Phasellus non metus et leo fermentum pretium non nec odio. Ut ante orci, interdum vel gravida eget, mollis in tortor. Sed rutrum ornare arcu in efficitur. Nulla facilisi. +Aenean vestibulum nisl sed eleifend ullamcorper. Suspendisse viverra imperdiet nisi, sed imperdiet tortor vulputate ut. Nunc consequat nisi eu laoreet mollis. Quisque metus ante, fringilla at rutrum id, consectetur vel enim. Pellentesque posuere tempor urna, sit amet vulputate urna. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; In hac habitasse platea dictumst. Nullam quis elementum purus. Cras eleifend sed justo vitae faucibus. Morbi interdum neque vitae elementum sollicitudin. Etiam eleifend, sapien at interdum consectetur, ex mauris dictum sem, in convallis dolor felis sollicitudin augue. Curabitur id tortor ex. Mauris finibus, lectus convallis euismod congue, est tortor suscipit velit, ac dictum augue risus ut lectus. Vestibulum ultricies luctus hendrerit. Integer elit nibh, condimentum id tempus nec, euismod nec nunc. +Sed porttitor hendrerit eros vitae suscipit. Vestibulum quis est elit. Sed neque leo, condimentum quis condimentum sed, posuere ut ex. Suspendisse mattis est nunc, in ullamcorper felis pellentesque sed. In hac habitasse platea dictumst. Maecenas tempus et turpis vestibulum porttitor. Quisque nulla odio, rhoncus et porta ut, hendrerit non sapien. Proin consequat, ex sed consequat iaculis, magna dolor consequat dui, eget rhoncus magna lorem in turpis. Cras urna sem, tincidunt ac aliquet id, imperdiet vitae neque. Fusce ac risus nisi. In erat urna, feugiat ac purus non, volutpat pulvinar ipsum. Morbi ultricies eros at nulla ullamcorper, nec lobortis arcu convallis. In maximus efficitur quam id maximus. +Etiam posuere urna non diam accumsan tempor. Donec facilisis blandit leo sed convallis. Nullam ut nisl vel dolor varius accumsan vitae a dui. Nunc ullamcorper nunc ac rhoncus pretium. Mauris ac lectus urna. Phasellus quis interdum nisi. Suspendisse mi ex, mollis a euismod vel, pretium et est. Mauris condimentum in ipsum quis lacinia. Mauris scelerisque molestie nibh, auctor lobortis felis convallis ac. Praesent vestibulum luctus urna non tincidunt. Quisque id nisl pretium, bibendum nisl nec, gravida quam. Curabitur eget cursus purus, non commodo felis. Vestibulum non lectus nec quam auctor dictum. Curabitur molestie elit mi, non tempor nisi aliquam in. Etiam fringilla lacinia est. +Pellentesque eleifend pulvinar eros, quis pulvinar arcu fermentum sed. Nam augue lectus, malesuada id mattis nec, ullamcorper sed nisi. Phasellus volutpat nisl eu commodo feugiat. Morbi eget sapien iaculis, consectetur est in, tempus metus. Donec hendrerit lectus aliquam ex faucibus pellentesque. Praesent lobortis libero sit amet metus commodo faucibus. Sed gravida eget mi at finibus. +Vivamus nec ligula vitae augue auctor viverra. Sed vulputate eget libero a porta. Aenean nisl turpis, tincidunt et placerat sit amet, bibendum quis libero. Nullam ultrices a ex sit amet dapibus. Suspendisse id congue lorem. In viverra convallis neque. In facilisis dui quis rhoncus semper. Ut id imperdiet libero. Fusce condimentum, sem id elementum aliquam, quam augue tincidunt felis, at suscipit lorem eros eget augue. Suspendisse eros dui, consectetur a arcu in, hendrerit dapibus risus. Proin purus erat, tincidunt eget ante sit amet, consequat convallis erat. Praesent et ornare nisl, id vehicula nulla. +Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean dignissim pellentesque mi, consectetur ornare leo egestas vitae. Suspendisse dignissim sapien orci, sed vestibulum neque maximus at. Sed lobortis urna consequat, sollicitudin lectus eget, finibus ante. Phasellus accumsan dolor non porttitor gravida. Duis eget eros ac lectus blandit posuere. Quisque sagittis nibh nunc, eu egestas nisi cursus at. Praesent accumsan fringilla neque eget iaculis. Nunc vulputate tempus tellus non porta. Sed faucibus vel dui in posuere. Sed quam arcu, accumsan id placerat sollicitudin, congue in diam. +Cras sed nunc in turpis dignissim vehicula non a libero. Sed iaculis nulla fermentum lacinia porta. Pellentesque sit amet efficitur est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis et ultrices justo. Etiam ut blandit nibh. In tempus lorem nec ultrices sagittis. Vivamus vel nulla faucibus, bibendum sapien ut, tristique diam. Donec pulvinar nisi nec semper blandit. Duis porttitor nibh enim, iaculis efficitur est sagittis a. Maecenas at eros ac quam pellentesque tempus nec ac magna. Donec pulvinar enim non congue facilisis. Donec eros orci, finibus vel lectus molestie, interdum scelerisque libero. Proin congue arcu lorem, in lobortis libero placerat vel. Nullam cursus est id lectus laoreet tempus et sed ex. Sed erat odio, molestie mattis egestas iaculis, sollicitudin eu ex. +Donec sodales metus ut mi suscipit, quis pretium lorem ultrices. Donec placerat ante at pellentesque vulputate. Etiam id augue vel eros facilisis interdum. Mauris imperdiet, arcu sit amet tincidunt posuere, risus quam luctus diam, eget tincidunt ipsum magna a mauris. In hac habitasse platea dictumst. Fusce lacinia sem ut ullamcorper tristique. Nunc mattis massa tellus, vitae lacinia sem tincidunt ut. Phasellus sodales justo ut ligula iaculis, sit amet ultricies arcu lacinia. Nam ac dignissim orci. Nullam nec orci nec est bibendum ultrices. Nam pulvinar consequat eros, ut laoreet massa blandit nec. Proin tristique venenatis pretium. Curabitur vitae dui est. Integer fermentum, ligula in faucibus porttitor, sem nunc rhoncus turpis, a faucibus lectus risus quis eros. Pellentesque et lorem eu dolor placerat interdum. Aliquam luctus scelerisque leo eu faucibus. +Vivamus id hendrerit odio, nec pellentesque magna. Proin augue eros, egestas nec eleifend ac, varius a sapien. Pellentesque viverra orci at condimentum euismod. Nam placerat lacus et augue porttitor blandit. Fusce scelerisque metus mollis nisl sollicitudin congue. Phasellus dictum velit arcu. Duis lacinia quam mauris, rhoncus porta enim dignissim posuere. Curabitur tempor urna eget ex cursus iaculis. Sed semper nisi nec nibh tempor pellentesque. Nulla vel turpis ac ipsum maximus porttitor eu sed nisl. Aliquam accumsan elit risus, eget sollicitudin ipsum lobortis at. Nulla sagittis faucibus sodales. Curabitur viverra pharetra quam quis efficitur. Integer eget justo maximus, dignissim magna vitae, consectetur metus. Suspendisse tincidunt, nunc eu vehicula faucibus, arcu felis accumsan velit, id rhoncus dui sapien vel purus. +Nullam lacinia urna commodo vehicula fringilla. Donec tellus est, bibendum a quam in, pellentesque auctor odio. Donec vehicula velit et leo consequat, sed eleifend mauris aliquam. Vivamus nibh ligula, blandit eu leo quis, pretium malesuada augue. Pellentesque at lacinia magna. Sed nec dolor porttitor, finibus erat ut, fringilla leo. Phasellus sed ex pellentesque, semper elit auctor, fermentum turpis. Vestibulum dictum sodales augue volutpat pretium. Integer pharetra volutpat nisl at dignissim. Mauris mattis, turpis nec volutpat maximus, sapien dolor elementum urna, ac iaculis neque dui non urna. Duis scelerisque magna nec nunc mattis maximus. In lobortis massa ante, non sagittis turpis viverra nec. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer viverra velit a fringilla sodales. +Suspendisse id arcu ut nisl ultrices convallis ornare in lectus. Nunc ultrices nulla vitae libero rutrum lobortis. Nullam hendrerit consectetur augue sit amet ornare. Cras dui est, pharetra at augue rhoncus, pharetra congue enim. Suspendisse maximus magna ut neque porta, in porttitor lorem pretium. Ut a dignissim magna, non tincidunt tortor. Vestibulum posuere tempus leo vitae blandit. Cras convallis ex a mi maximus fringilla a ac metus. Nunc sodales tincidunt semper. Praesent at risus eu neque tempus consectetur vel a odio. +Fusce bibendum ex a risus feugiat tincidunt. In ac volutpat quam, vestibulum molestie nibh. Maecenas pretium ultricies augue vitae luctus. Ut congue leo ante, consequat ullamcorper mi commodo sit amet. Duis et libero leo. Nam eu dictum sem, quis commodo tellus. Sed et dolor eu nisi varius sodales sed eu quam. Aenean eget auctor mauris, vitae faucibus nunc. Nam ultrices elit eu libero porta, vitae cursus odio aliquet. Vivamus sit amet interdum tellus. Suspendisse suscipit fringilla nisi, id scelerisque nisi aliquet vitae. Aliquam est nibh, commodo nec neque et, consequat ultrices nibh. Suspendisse efficitur id massa sed bibendum. Phasellus vitae posuere est. +Ut nec aliquet ex. Integer sapien nunc, tincidunt eu dolor ut, dictum pulvinar lorem. Donec nec augue nibh. Curabitur euismod, lorem ut mattis volutpat, nunc dui pharetra mauris, sed rutrum magna lacus non lacus. Nam ac rhoncus turpis, et dignissim erat. Fusce suscipit blandit mauris vel aliquam. Aenean id orci sed augue dapibus pellentesque ac ac sem. Nulla vel purus sapien. Integer vestibulum porttitor posuere. Mauris tincidunt elit nec risus tincidunt ornare. Duis bibendum, magna eu interdum bibendum, lectus nulla dictum dui, non condimentum sapien tortor sit amet orci. Integer ligula ligula, sodales vel nulla non, commodo semper eros. Quisque cursus tempus fringilla. Sed at nisl odio. Ut malesuada turpis ac libero consequat aliquet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Suspendisse tempus magna et massa euismod faucibus. Pellentesque sollicitudin nisl in pharetra aliquam. Vestibulum blandit massa ut turpis ornare, nec imperdiet metus interdum. Fusce molestie aliquet justo, ac condimentum justo. Praesent quis dolor vestibulum, posuere arcu eget, molestie nisi. Donec ac urna tempor, tempus lectus non, scelerisque est. Aliquam imperdiet dictum urna, non malesuada urna. Proin et tellus quis sapien dignissim vehicula. Vestibulum tortor metus, iaculis ac eros ut, laoreet vulputate nibh. Cras tempor vitae erat eget consequat. Sed ut nisl convallis, placerat quam quis, tincidunt turpis. Suspendisse sit amet rutrum augue. Duis sem justo, dictum eu dui at, eleifend commodo orci. Curabitur at erat odio. Phasellus porttitor interdum nibh, sit amet aliquet libero. Etiam convallis mattis massa, a rhoncus ligula condimentum et. +Duis ut diam ac lectus viverra volutpat et eget dolor. Nam tincidunt mauris vitae aliquet laoreet. Nam id ipsum eget ante euismod vulputate. Mauris elementum, tellus vitae rutrum vestibulum, libero orci finibus risus, in lacinia libero massa vitae elit. Nam et vestibulum justo, sed gravida orci. In ultrices mollis ultricies. Pellentesque odio leo, sagittis lacinia odio eu, accumsan auctor nibh. In non sem elementum, mollis justo sed, ornare nisi. Fusce interdum lobortis turpis ut dictum. Integer lacinia mollis nunc, nec condimentum quam hendrerit in. +Donec condimentum, dolor in aliquet facilisis, leo ex feugiat nisi, a commodo libero urna id enim. Donec faucibus urna eget pulvinar dapibus. Curabitur ultricies justo at ligula aliquam blandit. Mauris nisi urna, porttitor eu bibendum vitae, ultrices eu risus. In ac pulvinar nisl. Vestibulum viverra tellus purus, eget rutrum arcu venenatis et. Pellentesque nibh risus, sagittis eu hendrerit eget, sollicitudin sit amet enim. Quisque molestie ornare tellus sit amet placerat. +Ut finibus metus sit amet velit posuere, in egestas massa congue. Nulla sagittis, nisi eget pretium vestibulum, mauris libero elementum mauris, vel sollicitudin ligula metus vitae elit. Sed feugiat lectus sed ante maximus pharetra. Praesent felis eros, gravida sed varius in, faucibus ut metus. Nulla eget pretium nulla. Aliquam ultricies viverra magna, vel semper ligula accumsan ut. Pellentesque sollicitudin bibendum pretium. Vivamus volutpat commodo eleifend. Maecenas ornare ac nisl at tristique. Vivamus tellus lectus, euismod nec pellentesque non, lobortis ut nunc. Quisque hendrerit mi eget hendrerit ultrices. +Nunc rhoncus ligula ac libero consequat, semper commodo eros feugiat. Phasellus iaculis neque vitae luctus porttitor. Pellentesque suscipit mi ac ipsum bibendum mollis id at velit. Curabitur consectetur sollicitudin tincidunt. In a enim convallis, consequat justo nec, dignissim urna. Sed nisl ex, semper vitae lectus non, laoreet volutpat purus. Suspendisse ut mattis urna, quis hendrerit odio. Sed tincidunt libero pulvinar sem rhoncus, eu elementum enim ornare. Proin eu placerat ex. Cras euismod sem at ullamcorper scelerisque. +Nam fringilla, velit in ultrices pellentesque, nisl magna tempor felis, vel congue purus lectus sed metus. Cras consectetur quis massa id bibendum. Praesent sed purus odio. Fusce volutpat dolor ut magna congue auctor. In at magna lacinia, bibendum nisi in, pellentesque nisl. Pellentesque metus ligula, malesuada non turpis et, vulputate ultrices lectus. Sed erat leo, maximus id elementum sagittis, lacinia a metus. Proin mattis id lorem non malesuada. Aliquam sodales quam id gravida condimentum. +Phasellus quis bibendum risus, at porttitor sem. Nulla aliquet molestie eros quis sagittis. Ut lorem massa, ullamcorper vitae odio in, hendrerit vehicula urna. Quisque et viverra ex. Aliquam erat volutpat. Curabitur at interdum elit, ut tincidunt erat. Pellentesque quis augue pellentesque, finibus ante eget, feugiat quam. Cras nec efficitur ligula. Cras porta ac ipsum in ullamcorper. Nulla nunc erat, egestas egestas rhoncus viverra, mattis quis purus. Morbi convallis fringilla iaculis. Ut a sem mi. Praesent at velit ac dui euismod pulvinar. Donec mollis vehicula lorem vel tempus. Duis et scelerisque magna. +Suspendisse sollicitudin a libero ut convallis. Donec interdum faucibus dolor, id imperdiet nibh mollis eget. Sed sem augue, bibendum nec risus quis, mollis scelerisque magna. Cras molestie velit eget est pulvinar, a pharetra sapien tempor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan semper arcu id porttitor. Sed lacinia massa ut nibh gravida consectetur. Donec semper ante sed scelerisque varius. Vestibulum ullamcorper varius dui, sed aliquam elit aliquam nec. Sed auctor, dui sit amet mollis egestas, libero neque interdum est, efficitur elementum nibh lacus sed libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam blandit lacus in lacinia tristique. +Suspendisse scelerisque posuere nunc, non consectetur orci facilisis in. Donec aliquet fermentum mattis. Morbi fringilla id urna eget consectetur. Vivamus turpis est, efficitur a tempus ac, ornare vel lorem. Aenean bibendum metus non ante rutrum sollicitudin. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam erat volutpat. Suspendisse dui eros, sagittis nec ante in, gravida varius ante. Mauris sapien neque, lobortis et euismod vel, venenatis at risus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. Maecenas vestibulum metus arcu, sed faucibus lacus ultrices eget. Integer porta aliquam bibendum. +Etiam tempus pulvinar dictum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed sed nulla vehicula, iaculis nibh sit amet, blandit urna. Maecenas metus ante, posuere vitae bibendum quis, consequat sit amet magna. Cras venenatis nunc urna, vel posuere diam congue eu. Cras sit amet ipsum laoreet, tincidunt nunc ac, convallis elit. Praesent lacinia enim nibh, et efficitur leo convallis vel. Etiam varius diam arcu, sit amet convallis dui interdum vitae. In hac habitasse platea dictumst. Nullam commodo maximus turpis, quis congue mauris fermentum quis. Donec laoreet elit non lorem gravida pharetra. Ut aliquam purus a semper sodales. +Donec nec sollicitudin nisi. Proin ipsum est, rhoncus vel commodo ut, tristique eget justo. Praesent tristique massa sed odio sodales viverra. Aliquam at nisi vel turpis pulvinar volutpat. Phasellus rhoncus scelerisque mollis. Nunc et cursus nulla, in consectetur magna. Praesent et mollis nibh, non tristique dolor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas malesuada odio velit, eget finibus mauris congue eget. Praesent vestibulum mollis ex id ultricies. Integer sodales tempus vulputate. Phasellus blandit quam quis tortor blandit convallis. Donec tempor mi id urna venenatis fermentum. +Vestibulum ut sapien non tellus eleifend vestibulum eu vitae magna. Donec in lectus eget nisi ornare efficitur sed ut ex. Cras volutpat metus rhoncus hendrerit fringilla. Vivamus rutrum rutrum neque, eu imperdiet augue malesuada a. Aenean tristique massa vel diam vehicula pulvinar sit amet vel velit. Aenean faucibus luctus magna, non auctor nunc tempor ut. Etiam eleifend auctor metus, ac rhoncus massa. Aenean dignissim ornare tincidunt. Donec pulvinar sapien ante, vel dignissim sapien dictum sed. In eu ante in tortor fringilla viverra sit amet quis metus. Aenean in ex elementum, gravida nunc non, pretium nibh. Donec laoreet arcu sit amet dolor tempus pulvinar. Nullam at pharetra nisl. Donec sed justo sed arcu luctus ultrices. Fusce at venenatis eros. +Maecenas placerat quam ut massa suscipit lacinia. Maecenas et posuere risus. Proin interdum, libero sit amet consectetur pulvinar, dolor magna dignissim velit, sed hendrerit nulla ex sed nisi. Morbi dictum lobortis velit. Proin molestie mi at tortor finibus, quis ullamcorper enim maximus. Nullam varius, urna id volutpat volutpat, ligula lectus placerat eros, id sollicitudin dolor ligula eget sem. Curabitur quis augue vitae neque egestas tristique. Nam lacus libero, viverra faucibus risus et, placerat vestibulum neque. +In et convallis ante. Nulla hendrerit turpis eget consequat molestie. Fusce pharetra nunc ornare leo commodo, eu consequat odio dictum. Cras est diam, consequat et eleifend sed, faucibus quis neque. Morbi fermentum sem non ipsum mollis, id tempus risus blandit. Phasellus vulputate, ante finibus molestie finibus, velit enim mattis neque, et posuere felis risus a nisl. Cras a risus eu eros porttitor malesuada. Maecenas in mattis diam, sed tempor ex. In hac habitasse platea dictumst. Aliquam in magna quis arcu ultrices fringilla. Cras rhoncus tortor sed lacus blandit commodo. Nullam placerat augue vitae diam rutrum, ut eleifend ligula pellentesque. Aenean blandit lectus orci, egestas dapibus nisi finibus at. +Quisque id dapibus nisi, in placerat ante. Sed ut feugiat arcu. Etiam facilisis augue in nisi placerat facilisis. Donec vitae porttitor nibh. Nulla et nisl id purus egestas lobortis non id lorem. Nam viverra vulputate sapien, et posuere ex tincidunt a. Pellentesque venenatis turpis non purus rutrum faucibus. Nam non dapibus sem, sit amet rutrum neque. Vestibulum tempor, libero et faucibus feugiat, metus tellus condimentum nunc, sit amet pretium diam est a ipsum. +Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed nulla neque, vulputate in aliquet quis, mollis non tellus. Ut aliquet lacus tellus, sed vehicula lectus aliquet ut. Nullam mattis in felis in condimentum. Vivamus facilisis, justo ac volutpat posuere, turpis ligula hendrerit tortor, eu ullamcorper quam dui non ex. Praesent feugiat, ligula eu aliquam interdum, mauris magna sagittis enim, eget facilisis felis lectus eget velit. In accumsan pharetra tincidunt. Sed mi massa, sodales nec nisi sed, lobortis fermentum ipsum. Nulla facilisi. Sed at accumsan felis. Donec sed condimentum metus, mollis gravida velit. +Nulla at lobortis nisl. Sed eu laoreet felis. Maecenas velit erat, mattis a condimentum eu, fermentum vel elit. Aenean mattis rutrum risus, in interdum metus volutpat sed. Nulla at dignissim lorem, in convallis tellus. Suspendisse ac risus sit amet diam ullamcorper feugiat. Maecenas sed lectus id erat cursus aliquam vel eget nisi. Ut tempus, risus id mollis lobortis, sem metus suscipit nibh, ut dapibus lectus tellus quis ipsum. In nec hendrerit nibh. In vel consectetur ex, a ullamcorper sem. Donec mauris massa, dictum non luctus a, feugiat eu metus. Donec maximus ex sit amet sem vehicula finibus egestas eget ex. Etiam volutpat ut nulla quis euismod. +Nunc consequat ut augue quis cursus. Aliquam placerat, enim eget scelerisque rhoncus, massa erat placerat tortor, ut varius dui felis in neque. Suspendisse potenti. Aliquam et magna tristique, volutpat sem eget, elementum lectus. Vivamus nunc velit, sagittis sed massa vel, vestibulum molestie urna. Cras at erat pharetra, porttitor turpis mattis, tempus magna. Nulla commodo enim mi, eu porttitor sapien pellentesque id. Aenean quis ligula eu mauris scelerisque porttitor quis ut orci. Praesent tincidunt risus mi. Nunc dignissim, arcu quis dictum consectetur, est massa imperdiet felis, ut scelerisque magna mi vel nisi. Mauris imperdiet, diam sit amet ultricies porttitor, odio sapien placerat ligula, in condimentum sem augue non justo. Duis a lorem augue. +Aenean vitae venenatis dolor, at ultricies purus. Vivamus velit urna, tempus vitae ornare id, semper in metus. In sed mi et odio pulvinar ultricies ut quis enim. Duis erat dolor, aliquam sed sodales id, porttitor vitae turpis. Fusce feugiat venenatis ex quis aliquet. Nulla pretium elit vel nisi suscipit condimentum. Nunc dapibus, mi id venenatis ultrices, libero purus rhoncus erat, a eleifend metus nisl at elit. +Aenean eget porttitor risus. Donec ac lacus feugiat, faucibus ante sed, convallis neque. Donec pharetra in sem eget congue. Quisque ac neque in urna varius interdum. Donec justo nisi, volutpat nec lobortis vitae, pulvinar sit amet lorem. Maecenas porttitor magna orci. In eleifend risus ut lectus facilisis aliquam. Nullam nibh nulla, sodales quis tempus sed, condimentum quis nulla. Proin vitae hendrerit ante. Morbi tincidunt pharetra metus, quis lobortis elit viverra sit amet. Vivamus mattis eros erat, ut semper velit pretium sed. Nulla ac vulputate leo. +Praesent at ex at lacus dictum viverra vel nec nisi. Duis maximus nisi et eleifend fringilla. Cras lacinia arcu id turpis dignissim posuere. Sed faucibus nisi dignissim, sagittis orci quis, dapibus metus. Nullam ultricies libero eu auctor finibus. Vestibulum auctor odio in tortor molestie interdum. Proin nec libero mi. Donec tempor dignissim velit a molestie. Duis feugiat cursus turpis, in dignissim justo imperdiet non. Suspendisse faucibus dui non viverra interdum. Nullam dignissim interdum egestas. Phasellus vitae eros quis lectus bibendum scelerisque. Donec dui mauris, iaculis non molestie a, elementum sed felis. Donec iaculis congue tempor. Vestibulum dolor nisi, maximus quis cursus ut, vulputate non orci. Maecenas nisi ex, maximus ut aliquam commodo, interdum sed lorem. +Cras sed vehicula risus, sed suscipit eros. Suspendisse scelerisque sapien sit amet volutpat porta. Sed sagittis orci eget feugiat eleifend. Nulla viverra ex vitae mauris lacinia imperdiet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum arcu augue, euismod non elit non, feugiat aliquet ligula. Nulla ut neque vehicula, malesuada ex sit amet, finibus sem. Maecenas eleifend molestie justo a lobortis. +Aliquam at erat vitae neque porttitor ullamcorper. Integer et rutrum leo. Vivamus dui erat, luctus at purus et, auctor tincidunt felis. Proin fermentum maximus dui quis tempor. Nullam pellentesque at ex eget mollis. In hac habitasse platea dictumst. Maecenas efficitur turpis malesuada vulputate dapibus. Integer id faucibus diam. Aliquam lectus turpis, mollis vitae dolor eget, mattis faucibus est. Vestibulum id libero at est congue facilisis. Pellentesque tellus mauris, bibendum eget dui id, sagittis maximus enim. Sed condimentum ac lectus vel vulputate. Praesent lectus orci, pharetra id metus nec, fermentum congue nulla. +Nunc vel sem sem. Integer varius tincidunt lorem, id porta massa venenatis porta. Maecenas dictum mollis pharetra. In ex justo, auctor ac tempus id, lacinia et erat. Fusce dignissim purus in metus facilisis, vitae congue nunc tempor. Nam a euismod elit. Vestibulum tincidunt sed orci et porta. Vestibulum eget dui id mi efficitur fermentum. +Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut quis lorem vitae dui ullamcorper iaculis eget sed purus. Maecenas at ex lectus. Donec ut ligula eros. Nulla consectetur sed ligula quis volutpat. Integer rhoncus, justo egestas molestie scelerisque, nibh nibh finibus neque, dapibus vulputate quam lectus interdum tortor. Donec ultrices, tellus et suscipit tempor, urna lectus congue lorem, porta laoreet nibh purus id nisl. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse malesuada hendrerit turpis vel feugiat. Suspendisse at mi cursus, dictum ipsum in, tristique mi. Cras diam nisi, aliquam vel lacinia sed, vehicula id diam. Sed consectetur tempus massa, a pellentesque dolor posuere vel. Curabitur et finibus quam. Fusce orci magna, faucibus ac tellus id, fermentum tempor neque. Ut libero augue, egestas eget fringilla eget, suscipit vitae orci. +Nunc posuere nisi purus, a bibendum elit interdum id. Donec accumsan metus non rutrum porttitor. Donec in sollicitudin lorem. Sed porta magna et lobortis consequat. Sed eget tempus nibh. Ut sed ante vel ex interdum commodo. Fusce eget nisi nec mauris tristique scelerisque. Ut eleifend nunc ultrices mattis ultricies. Vivamus id sodales lacus. Vivamus dignissim elementum ante, sit amet ornare ipsum lacinia sit amet. Proin maximus pulvinar nibh vitae luctus. Sed at gravida metus, at ultricies eros. Integer porta velit auctor, dictum elit non, dictum dolor. +Suspendisse potenti. Morbi lobortis orci nisi, ac sollicitudin libero cursus sed. Integer vestibulum sem ac ante rutrum, at pellentesque tortor bibendum. Etiam at arcu et sapien molestie sodales. Nam fringilla turpis eget mi convallis, quis iaculis metus varius. Vivamus finibus purus ut nunc sodales pulvinar. Praesent varius, odio sed luctus tempus, orci augue sodales orci, a posuere nisi ante et justo. Ut ultricies rhoncus feugiat. Ut arcu massa, scelerisque vitae metus rhoncus, aliquet ultricies urna. +Nulla facilisi. Aliquam at porttitor quam. Pellentesque eu faucibus nunc. Sed nec leo leo. Integer cursus ex a magna lacinia interdum. Vivamus luctus lacinia odio in sollicitudin. Curabitur sem felis, condimentum sed nisi at, posuere ullamcorper arcu. Vivamus convallis vel nisl id suscipit. Maecenas semper dictum nibh, nec malesuada dolor semper id. Maecenas turpis massa, vulputate ac euismod quis, viverra id lorem. Integer imperdiet tincidunt bibendum. Donec laoreet viverra volutpat. Aliquam arcu elit, euismod vitae felis quis, venenatis commodo lectus. +Fusce mattis mi est. Proin augue dui, ultricies quis facilisis tristique, vehicula quis ex. Nunc blandit aliquet aliquet. Aliquam nibh sem, tempus a justo sit amet, viverra ullamcorper sem. Suspendisse potenti. Pellentesque quis risus eget ipsum tincidunt ornare ultrices at sapien. Phasellus viverra nec urna auctor dapibus. Curabitur id porttitor justo. Vestibulum risus sapien, dignissim vel libero ac, scelerisque fringilla risus. Donec porta at ex quis volutpat. Suspendisse potenti. Phasellus leo metus, pellentesque et laoreet sed, tempus ut ligula. Sed convallis elit libero, sed ornare odio mattis et. +Ut lobortis libero vel nibh dignissim ullamcorper. Curabitur sed tortor id leo bibendum dignissim at sit amet dolor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed vitae urna justo. Quisque in fringilla velit. Nullam venenatis laoreet tellus, sagittis sodales dolor facilisis ut. Integer tincidunt at est aliquet commodo. Cras sed urna ut sapien varius rhoncus. Duis pharetra a magna ut tincidunt. +Sed mattis nunc ut pulvinar sodales. Nullam bibendum, ex vulputate volutpat porttitor, sapien mi gravida augue, at scelerisque mi ipsum sit amet risus. Phasellus non lacus molestie, faucibus nibh vitae, bibendum ex. Mauris et elit ut augue tincidunt pellentesque. Vestibulum vehicula pretium magna in faucibus. In fringilla tincidunt nisi. Integer porta vehicula risus eu commodo. Quisque iaculis laoreet vestibulum. Donec vitae aliquam metus, a gravida purus. Integer sit amet ligula vitae tortor lacinia mollis. Phasellus leo tellus, mattis ut maximus quis, ultricies in mauris. Praesent a scelerisque quam. +Nunc tempor feugiat accumsan. Aliquam erat volutpat. Cras ut mi odio. Aenean sed purus sed nunc luctus tempor sit amet id ligula. Morbi nec faucibus neque, a luctus nisl. Maecenas aliquam lorem sed blandit maximus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. +In hac habitasse platea dictumst. Cras blandit nibh lacus, nec venenatis nulla elementum nec. Nam tincidunt posuere augue rhoncus mattis. Cras interdum tincidunt massa, ac tristique libero porta in. Integer hendrerit gravida nisi, ut fringilla ligula ullamcorper nec. Fusce erat nulla, ullamcorper vel pulvinar eget, feugiat molestie velit. Integer viverra sollicitudin velit, vitae accumsan orci aliquet ac. Suspendisse aliquet augue ac mollis hendrerit. Proin euismod sagittis metus ac pretium. Nam augue nulla, posuere ac tortor nec, ornare interdum risus. Proin non massa eros. Morbi aliquet ante et purus placerat faucibus. +Quisque pharetra dolor vitae magna vehicula fermentum quis nec odio. In commodo tincidunt turpis non scelerisque. Nam tempor consequat justo suscipit convallis. Sed sit amet augue at lorem porta eleifend sit amet sit amet velit. Fusce eu eros quis risus mattis vehicula eu at quam. Nulla faucibus elementum tincidunt. Sed in diam accumsan, blandit felis a, cursus mauris. +Nunc in hendrerit augue. Phasellus vel purus ullamcorper, auctor ante sed, auctor nisi. Nunc rutrum est erat, ut consequat elit mattis ac. Suspendisse potenti. Maecenas purus libero, pharetra quis vestibulum at, egestas eget enim. Vestibulum ut condimentum tortor. Aliquam vestibulum mattis placerat. Morbi non leo eu nisl maximus lobortis eget eget sapien. Donec sollicitudin ipsum nulla, non dignissim massa pretium sed. Nulla et molestie nibh, nec aliquet arcu. +Cras euismod ligula justo, at dignissim nibh mattis vitae. Integer bibendum sit amet urna vitae dapibus. Fusce gravida ut enim eget molestie. Quisque finibus nisl ut odio mattis, ut viverra justo pretium. Nunc convallis cursus tincidunt. Integer vel hendrerit orci. Donec leo orci, elementum at cursus a, eleifend a nisl. Nunc massa nunc, blandit non ex pretium, volutpat feugiat tortor. Phasellus gravida nibh ipsum, ac consequat ante eleifend eu. Nulla venenatis auctor efficitur. Vestibulum eleifend eros id nibh interdum, id tempus tortor molestie. Nam id finibus quam. Aenean eu vulputate ante. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur hendrerit mi ac venenatis suscipit. +Vestibulum lobortis maximus metus, a commodo massa ultricies et. Etiam ut lectus quis lacus euismod consequat. Maecenas tincidunt, felis dapibus aliquam gravida, est sapien tempus nibh, et rutrum dolor orci in dui. Proin tellus nisl, imperdiet mollis elit ac, gravida lacinia sapien. Aenean quis lectus id ante gravida lobortis. In sollicitudin et ex et facilisis. Curabitur in velit at augue commodo placerat. Nulla facilisi. Fusce facilisis est eu dolor rhoncus, a bibendum felis tincidunt. Sed sit amet feugiat ante. Sed accumsan, ipsum nec bibendum semper, justo elit dignissim magna, eu lacinia sem lorem sed justo. Etiam posuere sollicitudin nisi ac placerat. Duis maximus nisl nec nisi accumsan posuere. +Aliquam erat volutpat. Suspendisse tincidunt ut neque eu tincidunt. Nulla porttitor eu mi sed gravida. Maecenas in cursus nisl, et fringilla quam. Nulla posuere, turpis vel vestibulum condimentum, magna dui eleifend orci, nec semper metus risus id augue. Proin pretium risus id elit consequat tristique. Phasellus eget tortor eleifend, euismod arcu at, ullamcorper nibh. Etiam id ultricies dui, non mattis purus. Morbi sit amet mi diam. Nullam dictum erat vitae tortor lobortis imperdiet. Etiam venenatis ante non laoreet gravida. +Vestibulum eget egestas mauris. Nulla facilisi. Vivamus ut dignissim turpis. Nulla quis tincidunt libero. Ut eget metus eleifend, volutpat enim ac, semper metus. Nunc vel ex nec neque maximus rhoncus vel et orci. Ut vitae congue sem, et porttitor diam. +Duis sit amet est nec nibh scelerisque pellentesque ac in quam. Donec eget erat nec diam mattis feugiat eget a ex. Morbi interdum est non tortor accumsan porta. In hendrerit libero sit amet ex blandit, ut vestibulum quam dapibus. Integer quis velit id nibh tempor volutpat. Suspendisse euismod, sapien nec tempor malesuada, augue nunc tincidunt diam, et viverra dui massa sit amet velit. Proin efficitur, nisl non vulputate maximus, sapien nulla pharetra odio, vel eleifend neque velit at est. Phasellus leo sem, pharetra placerat lorem faucibus, pellentesque euismod mi. Nam vel diam neque. Praesent mollis feugiat magna vitae pellentesque. Proin elementum rhoncus ante, eget eleifend arcu volutpat quis. Fusce id enim sed odio mollis pulvinar sed a nulla. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; +Aliquam vulputate justo ut arcu lacinia accumsan. Phasellus quis convallis arcu, vel viverra urna. Curabitur tincidunt nibh id iaculis facilisis. In tellus arcu, elementum vel luctus at, tempor quis magna. Morbi maximus, elit in pharetra lacinia, ex lorem imperdiet risus, eget vehicula sapien enim sed nunc. Aliquam faucibus sodales tortor, vel fermentum quam finibus eu. Duis non dolor accumsan, lobortis neque a, pharetra purus. Donec a tincidunt urna. Cras eleifend elit ac tortor dictum rutrum. Donec ut est et diam tristique pellentesque. In mattis diam justo, sit amet rutrum ante varius nec. Sed in orci sit amet neque finibus fermentum. Aenean risus lectus, laoreet vitae finibus non, semper non sem. Donec vel porttitor mi. Duis efficitur posuere odio, in facilisis magna fringilla et. Donec quis tristique erat. +Suspendisse mattis mattis mi at tristique. Ut ultricies sagittis iaculis. Aliquam sagittis, diam in commodo sagittis, odio nisl venenatis enim, suscipit venenatis felis diam vitae enim. Morbi a molestie urna, ac lacinia nunc. Nam quis metus augue. Fusce tristique orci ut euismod cursus. Pellentesque sed elit ac nisl placerat porttitor. Quisque molestie pharetra eros sed hendrerit. In tincidunt tellus in consequat ultricies. Nam condimentum sollicitudin aliquam. Nulla non odio urna. Aliquam gravida tincidunt erat at semper. Sed quis tempor turpis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. +Maecenas cursus massa tincidunt tortor placerat tempor. In justo nisl, posuere eu erat eget, vestibulum ultrices nulla. Vivamus vel est a erat gravida dapibus ut ac mauris. Integer eleifend gravida odio, aliquet tincidunt justo sollicitudin et. Ut ut ex in diam feugiat varius quis sit amet lectus. Aliquam sit amet nibh condimentum, sollicitudin neque id, vulputate orci. In vestibulum hendrerit libero, ut dapibus dui efficitur a. Aenean tristique at arcu id viverra. Maecenas cursus massa vel felis laoreet pretium. Praesent viverra tincidunt risus non sodales. Nulla id dignissim est, sed pharetra purus. Aliquam commodo, eros varius sagittis faucibus, diam lorem viverra erat, vel egestas ipsum dui sed turpis. Aenean placerat rhoncus nibh vel finibus. +Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut porttitor, nulla sed pharetra laoreet, lacus orci molestie mi, sed ullamcorper felis arcu id sapien. Curabitur et commodo velit. In pharetra arcu at augue pulvinar, commodo elementum augue condimentum. Sed mattis ipsum sed tempor faucibus. Donec non dolor sed purus ultrices condimentum sit amet vel nisi. Cras dignissim tellus et sapien porta elementum. Maecenas in eros vel orci congue dignissim. Donec et luctus libero. Phasellus vitae porttitor nisi. Donec a nibh nisi. Nulla in nibh nec tellus accumsan pretium. Aenean ultrices id est blandit malesuada. Integer blandit metus sed suscipit scelerisque. In ac urna cursus, vehicula justo nec, facilisis metus. Vivamus id scelerisque diam. +In egestas nulla non tortor viverra laoreet. Fusce porttitor sem urna, sed varius justo maximus a. Duis ligula libero, elementum quis viverra mattis, vehicula nec sem. Aenean ut libero eu est auctor placerat. Nam quam dui, accumsan eget ipsum in, ornare volutpat purus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nunc blandit, justo ut fermentum tincidunt, ex dolor suscipit eros, sed pretium lacus dolor vel tortor. Aliquam erat volutpat. +Phasellus sagittis risus nec accumsan congue. Maecenas enim elit, mattis sit amet enim tristique, vestibulum sodales leo. Curabitur vel vehicula velit. Proin elementum dui at purus pulvinar, et dictum nisl sagittis. Nulla aliquet quam ultrices dapibus rutrum. Integer ultrices dapibus mauris, a vehicula elit posuere ut. Nullam lacinia mattis nulla, dignissim aliquam massa consequat at. Mauris ut mi dictum, iaculis urna rhoncus, euismod massa. Phasellus elementum libero id turpis imperdiet dictum. Nunc ac ipsum vitae orci ullamcorper tempus. Quisque lacinia quam sed molestie sagittis. Aenean sed rutrum sapien, et scelerisque sem. +Suspendisse tincidunt, leo in vulputate rhoncus, neque augue feugiat purus, ac tempor libero nisi vitae ante. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean at augue quis ante porttitor blandit vitae non magna. Donec vel ligula hendrerit, sodales massa et, placerat libero. Etiam lectus felis, luctus et varius hendrerit, dignissim at massa. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Phasellus velit tortor, rutrum ut tristique non, feugiat at turpis. In iaculis leo metus, eu laoreet nunc viverra vel. Nam porttitor vitae diam vel pellentesque. In ullamcorper nulla id purus cursus accumsan. Etiam et mi turpis. Praesent volutpat sem odio, vitae dapibus libero posuere ac. Mauris dignissim ultricies felis, eu vestibulum turpis fringilla vitae. Sed porttitor ornare consectetur. Nulla id justo mollis, iaculis leo sit amet, bibendum eros. Donec ac interdum sapien. +Praesent dolor urna, tincidunt a neque eu, hendrerit luctus odio. Pellentesque ac odio in est maximus maximus. Donec et lectus aliquet massa egestas euismod cursus et magna. Nam id mi hendrerit, rutrum eros iaculis, sodales enim. Nullam porttitor urna ac malesuada tincidunt. Suspendisse a turpis sapien. Curabitur rhoncus vel libero ut volutpat. Nam a sapien sem. Duis ac convallis eros, ut auctor nulla. +Interdum et malesuada fames ac ante ipsum primis in faucibus. In sagittis lorem vitae est laoreet dictum. Integer posuere est ac tortor sagittis, quis mattis ligula laoreet. Curabitur urna sem, finibus id euismod at, varius ac sem. Donec tristique tristique est, a dictum lorem ornare eu. Praesent tempor diam ligula, at placerat leo placerat nec. Nam eget dui vulputate, vestibulum nisi ut, vehicula dui. Aenean elementum tincidunt lectus at vestibulum. Nam tristique mauris sit amet sapien iaculis facilisis. Nam accumsan, nibh ac aliquam venenatis, dolor lacus pretium dolor, accumsan feugiat lectus tellus facilisis sem. Nullam a dui sit amet tortor elementum rhoncus. Aliquam mattis eget quam et lobortis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse ultricies, magna eu imperdiet efficitur, tellus sapien suscipit massa, sit amet finibus dolor libero vitae ante. Maecenas vel aliquam massa. diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 5f91eccc..4bb567d5 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -669,6 +669,11 @@ class GuiMainMenu(QMenuBar): self.aInsVSpaceM.triggered.connect(lambda: self._docInsert(nwDocInsert.VSPACE_M)) self.mInsBreaks.addAction(self.aInsVSpaceM) + # Insert > Placeholder Text + self.aLipsumText = QAction(self.tr("Placeholder Text"), self) + self.aLipsumText.triggered.connect(lambda: self.theParent.showLoremIpsumDialog()) + self.insertMenu.addAction(self.aLipsumText) + return def _buildFormatMenu(self): diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8674dda1..2eaea912 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -47,7 +47,9 @@ from novelwriter.dialogs import ( GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList ) -from novelwriter.tools import GuiBuildNovel, GuiProjectWizard, GuiWritingStats +from novelwriter.tools import ( + GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats +) from novelwriter.core import NWProject, NWIndex from novelwriter.enum import ( nwItemType, nwItemClass, nwAlert, nwWidget, nwState @@ -1050,6 +1052,24 @@ class GuiMain(QMainWindow): return True + def showLoremIpsumDialog(self): + """Open the insert lorem ipsum text dialog. + """ + if not self.hasProject: + logger.error("No project open") + return False + + dlgLipsum = getGuiItem("GuiLipsum") + if dlgLipsum is None: + dlgLipsum = GuiLipsum(self) + + dlgLipsum.setModal(False) + dlgLipsum.show() + dlgLipsum.raise_() + qApp.processEvents() + + return True + def showProjectWordListDialog(self): """Open the project word list dialog. """ diff --git a/novelwriter/tools/__init__.py b/novelwriter/tools/__init__.py index b42ec956..024ae3fd 100644 --- a/novelwriter/tools/__init__.py +++ b/novelwriter/tools/__init__.py @@ -20,11 +20,13 @@ along with this program. If not, see . """ from novelwriter.tools.build import GuiBuildNovel +from novelwriter.tools.lipsum import GuiLipsum from novelwriter.tools.projwizard import GuiProjectWizard from novelwriter.tools.writingstats import GuiWritingStats __all__ = [ "GuiBuildNovel", + "GuiLipsum", "GuiProjectWizard", "GuiWritingStats", ] diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py new file mode 100644 index 00000000..a6ac572d --- /dev/null +++ b/novelwriter/tools/lipsum.py @@ -0,0 +1,142 @@ +""" +novelWriter – Lorem Ipsum Tool +============================== +Simple tool for inserting placeholder text in a document + +File History: +Created: 2022-04-02 [1.7a0] + +This file is a part of novelWriter +Copyright 2018–2022, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import os +import random +import logging +import novelwriter + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QDialog, QGridLayout, QHBoxLayout, QVBoxLayout, QLabel, QDialogButtonBox, + QSpinBox +) + +from novelwriter.gui.custom import QSwitch +from novelwriter.common import readTextFile + +logger = logging.getLogger(__name__) + + +class GuiLipsum(QDialog): + + def __init__(self, theParent): + QDialog.__init__(self, theParent) + + logger.debug("Initialising GuiLipsum ...") + self.setObjectName("GuiLipsum") + + self.mainConf = novelwriter.CONFIG + self.theParent = theParent + self.theTheme = theParent.theTheme + + self.setWindowTitle(self.tr("Insert Placeholder Text")) + + self.innerBox = QHBoxLayout() + self.innerBox.setSpacing(self.mainConf.pxInt(16)) + + # Icon + nPx = self.mainConf.pxInt(64) + vSp = self.mainConf.pxInt(4) + self.docIcon = QLabel() + self.docIcon.setPixmap(self.theParent.theTheme.getPixmap("proj_document", (nPx, nPx))) + + self.leftBox = QVBoxLayout() + self.leftBox.setSpacing(vSp) + self.leftBox.addWidget(self.docIcon) + self.leftBox.addStretch(1) + self.innerBox.addLayout(self.leftBox) + + # Form + self.headLabel = QLabel("{0}".format(self.tr("Insert Lorem Ipsum Text"))) + + self.paraLabel = QLabel(self.tr("Number of pragraphs")) + self.paraCount = QSpinBox() + self.paraCount.setMinimum(1) + self.paraCount.setMaximum(100) + self.paraCount.setValue(5) + + self.randLabel = QLabel(self.tr("Randomise order")) + self.randSwitch = QSwitch() + + self.formBox = QGridLayout() + self.formBox.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignLeft) + self.formBox.addWidget(self.paraLabel, 1, 0, 1, 1, Qt.AlignLeft) + self.formBox.addWidget(self.paraCount, 1, 1, 1, 1, Qt.AlignRight) + self.formBox.addWidget(self.randLabel, 2, 0, 1, 1, Qt.AlignLeft) + self.formBox.addWidget(self.randSwitch, 2, 1, 1, 1, Qt.AlignRight) + self.formBox.setVerticalSpacing(vSp) + self.formBox.setRowStretch(3, 1) + self.innerBox.addLayout(self.formBox) + + # Buttons + self.buttonBox = QDialogButtonBox() + self.buttonBox.rejected.connect(self._doClose) + + self.btnClose = self.buttonBox.addButton(QDialogButtonBox.Close) + self.btnClose.setAutoDefault(False) + + self.btnSave = self.buttonBox.addButton(self.tr("Insert"), QDialogButtonBox.ActionRole) + self.btnSave.clicked.connect(self._doInsert) + self.btnSave.setAutoDefault(False) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.addLayout(self.innerBox) + self.outerBox.addWidget(self.buttonBox) + self.outerBox.setSpacing(self.mainConf.pxInt(16)) + self.setLayout(self.outerBox) + + logger.debug("GuiLipsum initialisation complete") + + return + + ## + # Slots + ## + + def _doInsert(self): + """Load the text and insert it in the open document. + """ + lipsumFile = os.path.join(self.mainConf.assetPath, "text", "lipsum.txt") + lipsumText = readTextFile(lipsumFile).splitlines() + + if self.randSwitch.isChecked(): + random.shuffle(lipsumText) + + pCount = self.paraCount.value() + inText = "\n\n".join(lipsumText[0:pCount]) + "\n\n" + + self.theParent.docEditor.insertText(inText) + + return + + def _doClose(self): + """Close the dialog window without doing anything. + """ + self.close() + return + +# END Class GuiLipsum diff --git a/tests/test_tools/test_tools_lipsum.py b/tests/test_tools/test_tools_lipsum.py new file mode 100644 index 00000000..d4596546 --- /dev/null +++ b/tests/test_tools/test_tools_lipsum.py @@ -0,0 +1,76 @@ +""" +novelWriter – Lorem Ipsum Tool Tester +===================================== + +This file is a part of novelWriter +Copyright 2018–2022, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import pytest + +from tools import getGuiItem + +from PyQt5.QtWidgets import QAction, QMessageBox + +from novelwriter.tools import GuiLipsum + + +@pytest.mark.gui +def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj): + """Test the Lorem Ipsum tool. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + + # Check that we cannot open when there is no project + nwGUI.mainMenu.aLipsumText.activate(QAction.Trigger) + assert getGuiItem("GuiLipsum") is None + + # Create a new project + nwGUI.theProject.projTree.setSeed(42) + assert nwGUI.newProject({"projPath": fncProj}) is True + assert nwGUI.openDocument("0e17daca5f3e1") is True + assert len(nwGUI.docEditor.getText()) == 15 + + # Open the tool + nwGUI.mainMenu.aLipsumText.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiLipsum") is not None, timeout=1000) + + nwLipsum = getGuiItem("GuiLipsum") + assert isinstance(nwLipsum, GuiLipsum) + + # Insert paragraphs + nwGUI.docEditor.setCursorPosition(100) # End of document + nwLipsum.paraCount.setValue(2) + nwLipsum._doInsert() + theText = nwGUI.docEditor.getText() + assert "Lorem ipsum" in theText + assert len(theText) == 965 + + # Insert random paragraph + nwGUI.docEditor.setCursorPosition(1000) # End of document + nwLipsum.randSwitch.setChecked(True) + nwLipsum.paraCount.setValue(1) + nwLipsum._doInsert() + theText = nwGUI.docEditor.getText() + assert len(theText) > 965 + + # Close + nwLipsum._doClose() + + # qtbot.stopForInteraction() + +# END Test testToolLipsum_Main From f9ddcd2f8a6fa66759c84f0f4550242709f880d3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:13:54 +0200 Subject: [PATCH 010/179] Restructure the record of root items in the tree class --- novelwriter/core/index.py | 4 +- novelwriter/core/project.py | 12 +++++ novelwriter/core/tree.py | 44 ++++++++++++------- novelwriter/tools/build.py | 3 +- .../coreProject_NewRoot_nwProject.nwx | 28 +++++++++--- tests/test_core/test_core_index.py | 1 + tests/test_core/test_core_tree.py | 7 --- 7 files changed, 66 insertions(+), 33 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index b05fa952..b9b1e5f9 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -208,8 +208,6 @@ class NWIndex(): text. """ theItem = self.theProject.projTree[tHandle] - theRoot = self.theProject.projTree.getRootItem(tHandle) - if theItem is None: logger.info("Not indexing unknown item '%s'", tHandle) return False @@ -232,7 +230,7 @@ class NWIndex(): if self.theProject.projTree.isTrashRoot(theItem.itemParent): logger.debug("Not indexing trash item '%s'", tHandle) return False - if theRoot.itemClass == nwItemClass.ARCHIVE: + if self.theProject.projTree.getItemClass(tHandle) == nwItemClass.ARCHIVE: logger.debug("Not indexing archived item '%s'", tHandle) return False diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index a3c16de6..47397e5c 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -608,6 +608,7 @@ class NWProject(): self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) self._scanProjectFolder() + self._checkProjectTree() self._loadProjectLocalisation() self.updateWordCounts() @@ -1340,6 +1341,17 @@ class NWProject(): xEntry.text = aValue return + def _checkProjectTree(self): + """Check the project tree and make sure all items have sensible + values. + """ + for tItem in self.projTree: + tHandle = tItem.itemHandle + logger.verbose("Checking item '%s'", tHandle) + if tItem.itemRoot is None: + self.projTree.updateItemRoot(tHandle) + logger.warning("Corrected the root setting of item '%s'", tHandle) + def _scanProjectFolder(self): """Scan the project folder and check that the files in it are also in the project XML file. If they aren't, import them as diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index f429873b..3316f993 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -47,7 +47,7 @@ class NWTree(): self._projTree = {} # Holds all the items of the project self._treeOrder = [] # The order of the tree items on the tree view - self._treeRoots = [] # The root items of the tree + self._treeRoots = {} # The root items of the tree self._trashRoot = None # The handle of the trash root folder self._archRoot = None # The handle of the archive root folder self._theIndex = 0 # The current iterator index @@ -67,7 +67,7 @@ class NWTree(): """ self._projTree = {} self._treeOrder = [] - self._treeRoots = [] + self._treeRoots = {} self._trashRoot = None self._archRoot = None self._theIndex = 0 @@ -98,7 +98,7 @@ class NWTree(): if nwItem.itemType == nwItemType.ROOT: logger.verbose("Item '%s' is a root item", str(tHandle)) - self._treeRoots.append(tHandle) + self._treeRoots[tHandle] = nwItem if nwItem.itemClass == nwItemClass.ARCHIVE: logger.verbose("Item '%s' is the archive folder", str(tHandle)) self._archRoot = tHandle @@ -253,20 +253,34 @@ class NWTree(): return tItem.itemHandle return None - 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 - maximum depth to make infinite loops impossible. + def isRoot(self, tHandle): + """Check if a handle is a root item. + """ + return tHandle in self._treeRoots + + def updateItemRoot(self, tHandle): + """Update the root item handle of a given item. + """ + tItem = self.__getitem__(tHandle) + iItem = tItem + if iItem is not None: + for _ in range(nwConst.MAX_DEPTH + 1): + if iItem.itemParent is None: + tItem.setRoot(iItem.itemHandle) + return iItem.itemHandle + else: + tHandle = iItem.itemParent + iItem = self.__getitem__(tHandle) + return None + + def getItemClass(self, tHandle): + """Return the class of a given item. """ tItem = self.__getitem__(tHandle) if tItem is not None: - for i in range(nwConst.MAX_DEPTH + 1): - if tItem.itemParent is None: - return tItem - else: - tHandle = tItem.itemParent - tItem = self.__getitem__(tHandle) - return None + if tItem.itemRoot in self._treeRoots: + return self._treeRoots[tItem.itemRoot].itemClass + return nwItemClass.NO_CLASS def getItemPath(self, tHandle): """Iterate upwards in the tree until we find the item with @@ -405,7 +419,7 @@ class NWTree(): return if tHandle in self._treeRoots: - self._treeRoots.remove(tHandle) + del self._treeRoots[tHandle] if tHandle == self._trashRoot: self._trashRoot = None if tHandle == self._archRoot: diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 7ebed572..0bb593a6 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -799,8 +799,7 @@ class GuiBuildNovel(QDialog): if isNovel and not novelFiles: return False - rootItem = self.theProject.projTree.getRootItem(theItem.itemHandle) - if rootItem.itemClass == nwItemClass.ARCHIVE: + if self.theProject.projTree.getItemClass(theItem.itemHandle) == nwItemClass.ARCHIVE: return False return True diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index cf55336e..b69de2b3 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -39,7 +39,7 @@ Main - + Novel @@ -72,19 +72,35 @@ New Scene - + + + Novel + + + + Plot + + + + Character + + + + World + + Timeline - + Object - + Custom1 - + Custom2 diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 9a7f7b88..5e5d3eb9 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -284,6 +284,7 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE) assert theProject.projTree[aHandle] is not None xItem.setParent(aHandle) + xItem.setRoot(aHandle) assert theIndex.scanText(xHandle, "Hello World!") is False # Make some usable items diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 66d76459..a86993a3 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -224,17 +224,10 @@ def testCoreTree_Methods(mockGUI, mockItems): assert theTree.checkType("c000000000001", nwItemType.FILE) is True # Root item lookup - theTree._treeRoots.append("stuff") assert theTree.findRoot(nwItemClass.WORLD) is None assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" - # Find root item of child item - assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001" - assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001" - assert theTree.getRootItem("c000000000002").itemHandle == "a000000000001" - assert theTree.getRootItem("stuff") is None - # Get item path assert theTree.getItemPath("stuff") == [] assert theTree.getItemPath("c000000000001") == [ From 37970bd4461054a08216c8d9ca2151881972f23c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:43:26 +0200 Subject: [PATCH 011/179] Add root information to project file --- novelwriter/core/item.py | 18 +++++++ novelwriter/core/tree.py | 1 + tests/lipsum/nwProject.nwx | 48 +++++++++---------- tests/minimal/nwProject.nwx | 22 ++++----- .../coreProject_NewCustomA_nwProject.nwx | 48 +++++++++---------- .../coreProject_NewCustomB_nwProject.nwx | 30 ++++++------ .../coreProject_NewFile_nwProject.nwx | 22 ++++----- .../coreProject_NewMinimal_nwProject.nwx | 18 +++---- .../coreProject_NewRoot_nwProject.nwx | 34 ++++++------- .../guiEditor_Main_Final_nwProject.nwx | 26 +++++----- .../guiEditor_Main_Initial_nwProject.nwx | 18 +++---- .../guiProjSettings_Dialog_nwProject.nwx | 18 +++---- tests/test_core/test_core_item.py | 29 +++++++++-- tests/test_core/test_core_tree.py | 41 ++++++++-------- 14 files changed, 207 insertions(+), 166 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 67dd836c..73dfdd6b 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -45,6 +45,7 @@ class NWItem(): self._name = "" self._handle = None self._parent = None + self._root = None self._order = 0 self._type = nwItemType.NO_TYPE self._class = nwItemClass.NO_CLASS @@ -84,6 +85,10 @@ class NWItem(): def itemParent(self): return self._parent + @property + def itemRoot(self): + return self._root + @property def itemOrder(self): return self._order @@ -142,6 +147,7 @@ class NWItem(): itemAttrib = {} itemAttrib["handle"] = str(self._handle) itemAttrib["parent"] = str(self._parent) + itemAttrib["root"] = str(self._root) itemAttrib["order"] = str(self._order) itemAttrib["type"] = str(self._type.name) itemAttrib["class"] = str(self._class.name) @@ -182,6 +188,7 @@ class NWItem(): return False self.setParent(xItem.attrib.get("parent", None)) + self.setRoot(xItem.attrib.get("root", None)) self.setOrder(xItem.attrib.get("order", 0)) self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE)) self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS)) @@ -301,6 +308,17 @@ class NWItem(): self._parent = None return + def setRoot(self, theRoot): + """Set the root handle, and ensure it is valid. + """ + if theRoot is None: + self._root = None + elif isHandle(theRoot): + self._root = theRoot + else: + self._root = None + return + def setOrder(self, theOrder): """Set the item order, and ensure that it is valid. This value is purely a meta value, and not actually used by novelWriter at diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 3316f993..b146c5a1 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -113,6 +113,7 @@ class NWTree(): self._projTree[tHandle] = nwItem self._treeOrder.append(tHandle) + self.updateItemRoot(tHandle) self._setTreeChanged(True) return True diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index c6eab805..457d8672 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,12 +1,12 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 21 + 23 24 - 1847 + 1857 False @@ -44,87 +44,87 @@ - + Novel - + Lorem Ipsum - + Front Matter - + Prologue - + Act One - + Chapter One - + Chapter One - + Scene One - + Scene Two - + Interlude - + Chapter Two - + Chapter Two - + Scene Three - + Scene Four - + Scene Five - + Characters - + Mr. Nobody - + Plot - + Main - + World - + Ancient Europe diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index 991d8b68..708d449f 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,13 +1,13 @@ - + Test Minimal Minimal Jane Doe John Doh - 12 + 13 2 - 129 + 134 True @@ -42,35 +42,35 @@ - + Novel - + Title Page - + New Chapter - + New Chapter - + New Scene - + Plot - + Characters - + World diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 4b8a67bf..92517d59 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -42,95 +42,95 @@ - + Novel - + Plot - + Characters - + Locations - + Timeline - + Objects - + Entities - + Title Page - + Chapter 1 - + Chapter 1 - + Scene 1.1 - + Scene 1.2 - + Scene 1.3 - + Chapter 2 - + Chapter 2 - + Scene 2.1 - + Scene 2.2 - + Scene 2.3 - + Chapter 3 - + Chapter 3 - + Scene 3.1 - + Scene 3.2 - + Scene 3.3 diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 3f55663e..2614990c 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -42,59 +42,59 @@ - + Novel - + Plot - + Characters - + Locations - + Timeline - + Objects - + Entities - + Title Page - + Scene 1 - + Scene 2 - + Scene 3 - + Scene 4 - + Scene 5 - + Scene 6 diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index d623ee2d..a0c16221 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,43 +40,43 @@ - + Novel - + Plot - + Characters - + World - + Title Page - + New Chapter - + New Chapter - + New Scene - + Hello - + Jane diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 6becb112..4581e521 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,35 +40,35 @@ - + Novel - + Plot - + Characters - + World - + Title Page - + New Chapter - + New Chapter - + New Scene diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index b69de2b3..25b47036 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,67 +40,67 @@ - + Novel - + Plot - + Characters - + World - + Title Page - + New Chapter - + New Chapter - + New Scene - + Novel - + Plot - + Character - + World - + Timeline - + Object - + Custom1 - + Custom2 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 4dd43b9a..a33a6e6a 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,51 +40,51 @@ - + Novel - + Title Page - + New Chapter - + New Chapter - + New Scene - + Plot - + New File - + Characters - + New File - + World - + New File - + Trash diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index f1b17740..5fbd17bb 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,35 +40,35 @@ - + Novel - + Title Page - + New Chapter - + New Chapter - + New Scene - + Plot - + Characters - + World diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index b88e7f17..e34a844b 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -46,35 +46,35 @@ - + Novel - + Title Page - + New Chapter - + New Chapter - + New Scene - + Plot - + Characters - + World diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 368592f1..05c1d077 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -65,6 +65,18 @@ def testCoreItem_Setters(mockGUI): theItem.setParent("0123456789abc") assert theItem.itemParent == "0123456789abc" + # Root + theItem.setRoot(None) + assert theItem.itemRoot is None + theItem.setRoot(123) + assert theItem.itemRoot is None + theItem.setRoot("0123456789abcdef") + assert theItem.itemRoot is None + theItem.setRoot("0123456789abg") + assert theItem.itemRoot is None + theItem.setRoot("0123456789abc") + assert theItem.itemRoot == "0123456789abc" + # Order theItem.setOrder(None) assert theItem.itemOrder == 0 @@ -326,6 +338,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): theItem = NWItem(theProject) theItem.setHandle("0123456789abc") theItem.setParent("0123456789abc") + theItem.setRoot("0123456789abc") theItem.setOrder(1) theItem.setName("A Name") theItem.setClass("NOVEL") @@ -342,9 +355,11 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): xContent = etree.SubElement(nwXML, "content") theItem.packXML(xContent) assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b'A Name' + b'' + b'A Name' + b'' ) # Unpack @@ -368,6 +383,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): theItem = NWItem(theProject) theItem.setHandle("0123456789abc") theItem.setParent("0123456789abc") + theItem.setRoot("0123456789abc") theItem.setOrder(1) theItem.setName("A Name") theItem.setClass("NOVEL") @@ -385,8 +401,11 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): xContent = etree.SubElement(nwXML, "content") theItem.packXML(xContent) assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b'A Name' + b'' + b'A Name' + b'' + b'' ) # Unpack diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index a86993a3..6216a1b1 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -374,25 +374,28 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( b'' b'' - b'' - b'Novel' - b'Act One' - b'' - b'Chapter One' - b'' - b'Scene One' - b'' - b'Outtakes' - b'' - b'Trash' - b'' - b'Characters' - b'Jane Doe' + b'Novel' + b'' + b'Act One' + b'Chapter One' + b'' + b'Scene One' + b'' + b'Outtakes' + b'Trash' + b'Characters' + b'Jane Doe' b'' b'' ) From bdde6a20802d5a1b0298a0fc6597beba390b75af Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Apr 2022 17:45:17 +0200 Subject: [PATCH 012/179] Clean up handling of root and class info for items --- novelwriter/core/index.py | 4 +- novelwriter/core/project.py | 28 +++--- novelwriter/core/tree.py | 131 ++++++++++++++--------------- novelwriter/gui/projtree.py | 2 +- novelwriter/tools/build.py | 6 +- tests/test_core/test_core_index.py | 3 +- tests/test_core/test_core_tree.py | 47 ++++++++--- 7 files changed, 121 insertions(+), 100 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index b9b1e5f9..b7332478 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -227,10 +227,10 @@ class NWIndex(): if theItem.itemParent is None: logger.info("Not indexing orphaned item '%s'", tHandle) return False - if self.theProject.projTree.isTrashRoot(theItem.itemParent): + if theItem.itemClass == nwItemClass.TRASH: logger.debug("Not indexing trash item '%s'", tHandle) return False - if self.theProject.projTree.getItemClass(tHandle) == nwItemClass.ARCHIVE: + if theItem.itemClass == nwItemClass.ARCHIVE: logger.debug("Not indexing archived item '%s'", tHandle) return False diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 47397e5c..913e159a 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -53,7 +53,7 @@ logger = logging.getLogger(__name__) class NWProject(): - FILE_VERSION = "1.4" + FILE_VERSION = "1.4" # The current project file format version def __init__(self, theParent): @@ -129,6 +129,7 @@ class NWProject(): newItem.setClass(rootClass) newItem.setStatus(0) self.projTree.append(None, None, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def newFolder(self, folderName, folderClass, pHandle): @@ -140,6 +141,7 @@ class NWProject(): newItem.setClass(folderClass) newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def newFile(self, fileName, fileClass, pHandle): @@ -156,6 +158,7 @@ class NWProject(): newItem.setClass(fileClass) newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def trashFolder(self): @@ -168,6 +171,7 @@ class NWProject(): newItem.setType(nwItemType.TRASH) newItem.setClass(nwItemClass.TRASH) self.projTree.append(None, None, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle return trashHandle @@ -605,10 +609,15 @@ class NWProject(): self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time()) self.mainConf.saveRecentCache() - self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) + # Check the project tree consistency + for tItem in self.projTree: + tHandle = tItem.itemHandle + logger.verbose("Checking item '%s'", tHandle) + if not self.projTree.updateItemData(tHandle): + logger.error("There was a problem item '%s', and it has been removed", tHandle) + del self.projTree[tHandle] # The file will be re-added as orphaned self._scanProjectFolder() - self._checkProjectTree() self._loadProjectLocalisation() self.updateWordCounts() @@ -617,6 +626,7 @@ class NWProject(): self._writeLockFile() self.setProjectChanged(False) + self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) return True @@ -1341,17 +1351,6 @@ class NWProject(): xEntry.text = aValue return - def _checkProjectTree(self): - """Check the project tree and make sure all items have sensible - values. - """ - for tItem in self.projTree: - tHandle = tItem.itemHandle - logger.verbose("Checking item '%s'", tHandle) - if tItem.itemRoot is None: - self.projTree.updateItemRoot(tHandle) - logger.warning("Corrected the root setting of item '%s'", tHandle) - def _scanProjectFolder(self): """Scan the project folder and check that the files in it are also in the project XML file. If they aren't, import them as @@ -1441,6 +1440,7 @@ class NWProject(): orphItem.setClass(oClass) orphItem.setLayout(oLayout) self.projTree.append(oHandle, oParent, orphItem) + self.projTree.updateItemData(orphItem.itemHandle) if noWhere: self.theParent.makeAlert(self.tr( diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index b146c5a1..ddef88b7 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -113,7 +113,6 @@ class NWTree(): self._projTree[tHandle] = nwItem self._treeOrder.append(tHandle) - self.updateItemRoot(tHandle) self._setTreeChanged(True) return True @@ -208,9 +207,30 @@ class NWTree(): return novelWords, noteWords ## - # Tree Structure Methods + # Tree Item Methods ## + def updateItemData(self, tHandle): + """Update the root item handle of a given item. Returns True if + a root was found and data updated, otherwise False. + """ + tItem = self.__getitem__(tHandle) + if tItem is None: + return False + + iItem = tItem + for _ in range(nwConst.MAX_DEPTH + 1): + if iItem.itemParent is None: + tItem.setRoot(iItem.itemHandle) + tItem.setClass(iItem.itemClass) + return True + else: + iItem = self.__getitem__(iItem.itemParent) + if iItem is None: + return False + + return False + def checkType(self, tHandle, itemType): """Return true of item exists and is of the specified item type. """ @@ -219,70 +239,6 @@ class NWTree(): return False return tItem.itemType == itemType - def trashRoot(self): - """Returns the handle of the trash folder, or None if there - isn't one. - """ - if self._trashRoot: - return self._trashRoot - return None - - def isTrashRoot(self, tHandle): - """Check if a handle is the trash folder. - """ - if self._trashRoot is None: - return False - return tHandle == self._trashRoot - - def archiveRoot(self): - """Returns the handle of the archive folder, or None if there - isn't one. - """ - if self._archRoot: - return self._archRoot - return None - - def findRoot(self, theClass): - """Find the root item for a given class. - Note: This returns the first item for class CUSTOM. - """ - for aRoot in self._treeRoots: - tItem = self.__getitem__(aRoot) - if tItem is None: - continue - if theClass == tItem.itemClass: - return tItem.itemHandle - return None - - def isRoot(self, tHandle): - """Check if a handle is a root item. - """ - return tHandle in self._treeRoots - - def updateItemRoot(self, tHandle): - """Update the root item handle of a given item. - """ - tItem = self.__getitem__(tHandle) - iItem = tItem - if iItem is not None: - for _ in range(nwConst.MAX_DEPTH + 1): - if iItem.itemParent is None: - tItem.setRoot(iItem.itemHandle) - return iItem.itemHandle - else: - tHandle = iItem.itemParent - iItem = self.__getitem__(tHandle) - return None - - def getItemClass(self, tHandle): - """Return the class of a given item. - """ - tItem = self.__getitem__(tHandle) - if tItem is not None: - if tItem.itemRoot in self._treeRoots: - return self._treeRoots[tItem.itemRoot].itemClass - return nwItemClass.NO_CLASS - def getItemPath(self, tHandle): """Iterate upwards in the tree until we find the item with parent None, the root item, and return the list of handles. @@ -305,6 +261,49 @@ class NWTree(): tTree.append(tHandle) return tTree + ## + # Tree Root Methods + ## + + def isRoot(self, tHandle): + """Check if a handle is a root item. + """ + return tHandle in self._treeRoots + + def isTrashRoot(self, tHandle): + """Check if a handle is the trash folder. + """ + if self._trashRoot is None: + return False + return tHandle == self._trashRoot + + def trashRoot(self): + """Returns the handle of the trash folder, or None if there + isn't one. + """ + if self._trashRoot: + return self._trashRoot + return None + + def archiveRoot(self): + """Returns the handle of the archive folder, or None if there + isn't one. + """ + if self._archRoot: + return self._archRoot + return None + + def findRoot(self, theClass): + """Find the first root item for a given class. + """ + for aRoot in self._treeRoots: + tItem = self.__getitem__(aRoot) + if tItem is None: + continue + if theClass == tItem.itemClass: + return tItem.itemHandle + return None + ## # Setters ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index a1cd9081..18b2b9f5 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -280,7 +280,7 @@ class GuiProjectTree(QTreeWidget): if nwItem.itemType != nwItemType.FILE: return True - # This is a new files, so let's add some content + # This is a new file, so let's add some content newDoc = NWDoc(self.theProject, tHandle) curTxt = newDoc.readDocument() if curTxt is None: diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 0bb593a6..feb1662d 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -780,12 +780,13 @@ class GuiBuildNovel(QDialog): if theItem is None: return False - if not theItem.isExported and not ignoreFlag: + if not (theItem.isExported or ignoreFlag): return False isNone = theItem.itemType != nwItemType.FILE isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemClass == nwItemClass.NO_CLASS + isNone |= theItem.itemClass == nwItemClass.ARCHIVE isNone |= theItem.itemClass == nwItemClass.TRASH isNone |= theItem.itemParent == self.theProject.projTree.trashRoot() isNone |= theItem.itemParent is None @@ -799,9 +800,6 @@ class GuiBuildNovel(QDialog): if isNovel and not novelFiles: return False - if self.theProject.projTree.getItemClass(theItem.itemHandle) == nwItemClass.ARCHIVE: - return False - return True def _saveDocument(self, theFmt): diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 5e5d3eb9..8070cbf2 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -278,13 +278,14 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): tHandle = theProject.trashFolder() assert theProject.projTree[tHandle] is not None xItem.setParent(tHandle) + theProject.projTree.updateItemData(xItem.itemHandle) assert theIndex.scanText(xHandle, "Hello World!") is False # Create the archive root aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE) assert theProject.projTree[aHandle] is not None xItem.setParent(aHandle) - xItem.setRoot(aHandle) + theProject.projTree.updateItemData(xItem.itemHandle) assert theIndex.scanText(xHandle, "Hello World!") is False # Make some usable items diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 6216a1b1..1e3e7d94 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -103,7 +103,7 @@ def mockItems(mockGUI): ("a000000000002", None, itemE), ("a000000000003", None, itemF), ("a000000000004", None, itemG), - ("b000000000002", "a000000000002", itemH), + ("b000000000002", "a000000000004", itemH), ] return theItems @@ -120,22 +120,23 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert theTree._handleSeed == 42 # Check that tree is empty (calls NWTree.__bool__) - assert not theTree + assert bool(theTree) is False # Check for archive and trash folders assert theTree.trashRoot() is None assert theTree.archiveRoot() is None - assert not theTree.isTrashRoot("a000000000003") + assert theTree.isTrashRoot("a000000000003") is False aHandles = [] for tHandle, pHandle, nwItem in mockItems: aHandles.append(tHandle) - assert theTree.append(tHandle, pHandle, nwItem) + assert theTree.append(tHandle, pHandle, nwItem) is True + assert theTree.updateItemData(tHandle) is True - assert theTree._treeChanged + assert theTree._treeChanged is True # Check that tree is not empty (calls __bool__) - assert theTree + assert bool(theTree) is True # Check the number of elements (calls __len__) assert len(theTree) == len(mockItems) @@ -151,6 +152,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert theTree.trashRoot() == "a000000000003" assert theTree.archiveRoot() == "a000000000002" assert theTree.isTrashRoot("a000000000003") + assert theTree.isRoot("a000000000002") # Try to add another trash folder itemT = NWItem(theProject) @@ -169,14 +171,15 @@ def testCoreTree_BuildTree(mockGUI, mockItems): itemT._class = nwItemClass.NOVEL itemT._layout = nwItemLayout.DOCUMENT - assert theTree.append(None, None, itemT) + assert theTree.append(None, None, itemT) is True + assert theTree.updateItemData(itemT.itemHandle) is True assert len(theTree) == len(mockItems) + 1 theList = theTree.handles() assert theList[-1] == "73475cb40a568" # Try to add existing handle - assert not theTree.append("73475cb40a568", None, itemT) + assert theTree.append("73475cb40a568", None, itemT) is False assert len(theTree) == len(mockItems) + 1 # Delete a non-existing item @@ -207,7 +210,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): @pytest.mark.core -def testCoreTree_Methods(mockGUI, mockItems): +def testCoreTree_Methods(monkeypatch, mockGUI, mockItems): """Test various class methods. """ theProject = NWProject(mockGUI) @@ -215,9 +218,27 @@ def testCoreTree_Methods(mockGUI, mockItems): for tHandle, pHandle, nwItem in mockItems: theTree.append(tHandle, pHandle, nwItem) + theTree.updateItemData(tHandle) assert len(theTree) == len(mockItems) + # Update item data, nonsense handle + assert theTree.updateItemData("stuff") is False + + # Update item data, invalid item parent + corrParent = theTree["b000000000001"].itemParent + theTree["b000000000001"].setParent("0000000000000") + assert theTree.updateItemData("b000000000001") is False + + # Update item data, valid item parent + theTree["b000000000001"].setParent(corrParent) + assert theTree.updateItemData("b000000000001") is True + + # Update item data, root is unreachable + with monkeypatch.context() as mp: + mp.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 0) + assert theTree.updateItemData("b000000000001") is False + # Chech type assert theTree.checkType("blabla", nwItemType.FILE) is False assert theTree.checkType("b000000000001", nwItemType.FILE) is False @@ -366,6 +387,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): for tHandle, pHandle, nwItem in mockItems: theTree.append(tHandle, pHandle, nwItem) + theTree.updateItemData(tHandle) assert len(theTree) == len(mockItems) @@ -377,8 +399,8 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'Novel' b'' - b'Act One' + b'type="FOLDER" class="NOVEL">Act One' + b'' b'Chapter One' @@ -393,7 +415,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'class="TRASH">Trash' b'Characters' - b'Jane Doe' b'' @@ -418,6 +440,7 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir): for tHandle, pHandle, nwItem in mockItems: theTree.append(tHandle, pHandle, nwItem) + theTree.updateItemData(tHandle) assert len(theTree) == len(mockItems) theTree._treeOrder.append("stuff") From 23fd6b815fc5db82791ab311afded877fad15a79 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Apr 2022 22:43:11 +0200 Subject: [PATCH 013/179] Save both status and importance flags (#1030) * Put the status icons in the status object itself * Allow saving both status and importance values * Update current tests * Improve test coverage * Update sample project file --- novelwriter/core/item.py | 47 ++++++++++-- novelwriter/core/project.py | 14 ++-- novelwriter/core/status.py | 76 +++++++++++-------- novelwriter/dialogs/docmerge.py | 1 + novelwriter/dialogs/docsplit.py | 1 + novelwriter/dialogs/itemeditor.py | 17 ++--- novelwriter/dialogs/projsettings.py | 2 +- novelwriter/gui/doceditor.py | 13 +--- novelwriter/gui/itemdetails.py | 15 +--- novelwriter/gui/projtree.py | 13 +--- novelwriter/guimain.py | 31 +------- sample/nwProject.nwx | 76 +++++++++---------- tests/conftest.py | 7 ++ tests/lipsum/nwProject.nwx | 64 ++++++++-------- tests/minimal/nwProject.nwx | 38 +++++----- .../coreProject_NewCustomA_nwProject.nwx | 64 ++++++++-------- .../coreProject_NewCustomB_nwProject.nwx | 46 +++++------ .../coreProject_NewFile_nwProject.nwx | 38 +++++----- .../coreProject_NewMinimal_nwProject.nwx | 34 ++++----- .../coreProject_NewRoot_nwProject.nwx | 42 +++++----- .../guiEditor_Main_Final_nwProject.nwx | 42 +++++----- .../guiEditor_Main_Initial_nwProject.nwx | 34 ++++----- .../guiProjSettings_Dialog_nwProject.nwx | 34 ++++----- tests/test_core/test_core_item.py | 75 ++++++++++++++---- tests/test_core/test_core_project.py | 8 +- tests/test_core/test_core_status.py | 33 +++++--- tests/test_core/test_core_tokenizer.py | 6 +- tests/test_core/test_core_tree.py | 23 +++--- tests/test_dialogs/test_dlg_itemeditor.py | 5 +- 29 files changed, 481 insertions(+), 418 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 67dd836c..993b183c 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -50,6 +50,7 @@ class NWItem(): self._class = nwItemClass.NO_CLASS self._layout = nwItemLayout.NO_LAYOUT self._status = None + self._import = None self._expanded = False self._exported = True @@ -104,6 +105,10 @@ class NWItem(): def itemStatus(self): return self._status + @property + def itemImport(self): + return self._import + @property def isExpanded(self): return self._expanded @@ -159,12 +164,13 @@ class NWItem(): nameAttrib = {} nameAttrib["status"] = str(self._status) + nameAttrib["import"] = str(self._import) if self._type == nwItemType.FILE: nameAttrib["exported"] = str(self._exported) xPack = etree.SubElement(xParent, "item", attrib=itemAttrib) - self._subPack(xPack, "meta", attrib=metaAttrib) - self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib) + self._subPack(xPack, "meta", attrib=metaAttrib) + self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib) return @@ -197,11 +203,12 @@ class NWItem(): elif xValue.tag == "name": self.setName(xValue.text) self.setStatus(xValue.attrib.get("status", None)) + self.setImport(xValue.attrib.get("import", None)) self.setExported(xValue.attrib.get("exported", True)) # Legacy Format (1.3 and earlier) elif xValue.tag == "status": - self.setStatus(xValue.text) + self.setImportStatus(xValue.text) elif xValue.tag == "type": self.setType(xValue.text) elif xValue.tag == "class": @@ -268,6 +275,28 @@ class NWItem(): return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, "")) + def getImportStatus(self): + """Return the relevant importance or status label and icon for + the current item based on its class. + """ + if self._class in nwLists.CLS_NOVEL: + stName = self.theProject.statusItems.checkEntry(self._status) + stIcon = self.theProject.statusItems.getIcon(stName) + else: + stName = self.theProject.importItems.checkEntry(self._import) + stIcon = self.theProject.importItems.getIcon(stName) + return stName, stIcon + + def setImportStatus(self, theLabel): + """Update the importance or status value based on class. This is + a wrapper setter for setStatus and setImport. + """ + if self._class in nwLists.CLS_NOVEL: + self.setStatus(theLabel) + else: + self.setImport(theLabel) + return + ## # Set Item Values ## @@ -354,10 +383,14 @@ class NWItem(): """Set the item status by looking it up in the valid status items of the current project. """ - if self._class in nwLists.CLS_NOVEL: - self._status = self.theProject.statusItems.checkEntry(theStatus) - else: - self._status = self.theProject.importItems.checkEntry(theStatus) + self._status = self.theProject.statusItems.checkEntry(theStatus) + return + + def setImport(self, theImport): + """Set the item importance by looking it up in the valid import + items of the current project. + """ + self._import = self.theProject.importItems.checkEntry(theImport) return def setExpanded(self, expState): diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 556f6e18..ff2c2774 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -46,7 +46,7 @@ from novelwriter.common import ( checkString, checkBool, checkInt, isHandle, formatTimeStamp, makeFileNameSafe, hexToInt ) -from novelwriter.constants import trConst, nwFiles, nwLabels +from novelwriter.constants import nwLists, trConst, nwFiles, nwLabels logger = logging.getLogger(__name__) @@ -1071,7 +1071,7 @@ class NWProject(): """ replaceMap = self.statusItems.setNewEntries(newCols) for nwItem in self.projTree: - if nwItem.itemClass == nwItemClass.NOVEL: + if nwItem.itemClass in nwLists.CLS_NOVEL: if nwItem.itemStatus in replaceMap: nwItem.setStatus(replaceMap[nwItem.itemStatus]) self.setProjectChanged(True) @@ -1083,9 +1083,9 @@ class NWProject(): """ replaceMap = self.importItems.setNewEntries(newCols) for nwItem in self.projTree: - if nwItem.itemClass != nwItemClass.NOVEL: - if nwItem.itemStatus in replaceMap: - nwItem.setStatus(replaceMap[nwItem.itemStatus]) + if nwItem.itemClass not in nwLists.CLS_NOVEL: + if nwItem.itemImport in replaceMap: + nwItem.setImport(replaceMap[nwItem.itemImport]) self.setProjectChanged(True) return True @@ -1206,10 +1206,10 @@ class NWProject(): self.statusItems.resetCounts() self.importItems.resetCounts() for nwItem in self.projTree: - if nwItem.itemClass == nwItemClass.NOVEL: + if nwItem.itemClass in nwLists.CLS_NOVEL: self.statusItems.countEntry(nwItem.itemStatus) else: - self.importItems.countEntry(nwItem.itemStatus) + self.importItems.countEntry(nwItem.itemImport) return def localLookup(self, theWord): diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 4318344f..2ca8ed53 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -24,9 +24,12 @@ along with this program. If not, see . """ import logging +import novelwriter from lxml import etree +from PyQt5.QtGui import QIcon, QPixmap, QColor + from novelwriter.common import checkInt logger = logging.getLogger(__name__) @@ -39,9 +42,11 @@ class NWStatus(): self._theLabels = [] self._theColours = [] self._theCounts = [] + self._theIcons = [] self._theMap = {} self._theLength = 0 self._theIndex = 0 + self._iconSize = novelwriter.CONFIG.pxInt(32) return @@ -50,38 +55,35 @@ class NWStatus(): a duplicate. """ theLabel = theLabel.strip() - if self.lookupEntry(theLabel) is None: + if self._getIndex(theLabel) is None: + theIcon = QPixmap(self._iconSize, self._iconSize) + theIcon.fill(QColor(*theColours)) + self._theIcons.append(QIcon(theIcon)) self._theLabels.append(theLabel) self._theColours.append(theColours) self._theCounts.append(0) self._theMap[theLabel] = self._theLength self._theLength += 1 - return True - def lookupEntry(self, theLabel): - """Look up a status entry in the object lists, and return it if - it exists. - """ - if theLabel is None: - return None - theLabel = theLabel.strip() - if theLabel in self._theMap.keys(): - return self._theMap[theLabel] - return None + return True def checkEntry(self, theStatus): """Check if a status value is valid, and returns the safe reference to be used internally. """ if isinstance(theStatus, str): - theStatus = theStatus.strip() - if self.lookupEntry(theStatus) is not None: - return theStatus - theStatus = checkInt(theStatus, 0, False) - if theStatus >= 0 and theStatus < self._theLength: - return self._theLabels[theStatus] + if self._getIndex(theStatus) is not None: + return theStatus.strip() return self._theLabels[0] + def getIcon(self, theLabel): + """Return the icon for the given status item. + """ + theIndex = self._getIndex(theLabel) + if theIndex is not None: + return self._theIcons[theIndex] + return QIcon() + def setNewEntries(self, newList): """Update the list of entries after they have been modified by the GUI tool. @@ -92,6 +94,7 @@ class NWStatus(): self._theLabels = [] self._theColours = [] self._theCounts = [] + self._theIcons = [] self._theMap = {} self._theLength = 0 self._theIndex = 0 @@ -113,7 +116,7 @@ class NWStatus(): """Increment the counter for a given label. This should be used together with resetCounts in a loop over project items. """ - theIndex = self.lookupEntry(theLabel) + theIndex = self._getIndex(theLabel) if theIndex is not None: self._theCounts[theIndex] += 1 return @@ -124,9 +127,9 @@ class NWStatus(): """ for n in range(self._theLength): xSub = etree.SubElement(xParent, "entry", attrib={ - "blue": str(self._theColours[n][2]), - "green": str(self._theColours[n][1]), "red": str(self._theColours[n][0]), + "green": str(self._theColours[n][1]), + "blue": str(self._theColours[n][2]), }) xSub.text = self._theLabels[n] return True @@ -145,18 +148,31 @@ class NWStatus(): theColours.append((cR, cG, cB)) if len(theLabels) > 0: - self._theLabels = [] + self._theLabels = [] self._theColours = [] - self._theCounts = [] - self._theMap = {} - self._theLength = 0 - self._theIndex = 0 + self._theCounts = [] + self._theIcons = [] + self._theMap = {} + self._theLength = 0 + self._theIndex = 0 for n in range(len(theLabels)): self.addEntry(theLabels[n], theColours[n]) return True + ## + # Internal Functions + ## + + def _getIndex(self, theLabel): + """Look up a status entry in the object lists, and return it if + it exists. + """ + if theLabel is None: + return None + return self._theMap.get(theLabel.strip(), None) + ## # Iterator Bits ## @@ -165,8 +181,8 @@ class NWStatus(): """Return an entry by its index. """ if n >= 0 and n < self._theLength: - return self._theLabels[n], self._theColours[n], self._theCounts[n] - return None, None, None + return self._theLabels[n], self._theColours[n], self._theCounts[n], self._theIcons[n] + return None, None, None, QIcon() def __iter__(self): """Initialise the iterator. @@ -178,9 +194,9 @@ class NWStatus(): """Return the next entry for the iterator. """ if self._theIndex < self._theLength: - theLabel, theColour, theCount = self.__getitem__(self._theIndex) + theLabel, theColour, theCount, theIcon = self.__getitem__(self._theIndex) self._theIndex += 1 - return theLabel, theColour, theCount + return theLabel, theColour, theCount, theIcon else: raise StopIteration diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index f9ee01af..f7695b9a 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -133,6 +133,7 @@ class GuiDocMerge(QDialog): nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent) newItem = self.theProject.projTree[nHandle] newItem.setStatus(srcItem.itemStatus) + newItem.setImport(srcItem.itemImport) outDoc = NWDoc(self.theProject, nHandle) if not outDoc.writeDocument(theText): diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 1138ec58..cdf0757a 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -203,6 +203,7 @@ class GuiDocSplit(QDialog): newItem = self.theProject.projTree[nHandle] newItem.setLayout(itemLayout) newItem.setStatus(srcItem.itemStatus) + newItem.setImport(srcItem.itemImport) logger.verbose( "Creating new document '%s' with text from line %d to %d", nHandle, iStart+1, iEnd diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py index 9ab1e4ac..a39d93f4 100644 --- a/novelwriter/dialogs/itemeditor.py +++ b/novelwriter/dialogs/itemeditor.py @@ -75,15 +75,11 @@ class GuiItemEditor(QDialog): self.editStatus = QComboBox() self.editStatus.setMinimumWidth(mVd) if self.theItem.itemClass in nwLists.CLS_NOVEL: - for sLabel, _, _ in self.theProject.statusItems: - self.editStatus.addItem( - self.theParent.statusIcons[sLabel], sLabel, sLabel - ) + for sLabel, _, _, sIcon in self.theProject.statusItems: + self.editStatus.addItem(sIcon, sLabel, sLabel) else: - for sLabel, _, _ in self.theProject.importItems: - self.editStatus.addItem( - self.theParent.importIcons[sLabel], sLabel, sLabel - ) + for sLabel, _, _, sIcon in self.theProject.importItems: + self.editStatus.addItem(sIcon, sLabel, sLabel) # Item Layout self.editLayout = QComboBox() @@ -120,7 +116,8 @@ class GuiItemEditor(QDialog): self.editName.setText(self.theItem.itemName) self.editName.selectAll() - statusIdx = self.editStatus.findData(self.theItem.itemStatus) + currStatus, _ = self.theItem.getImportStatus() + statusIdx = self.editStatus.findData(currStatus) if statusIdx != -1: self.editStatus.setCurrentIndex(statusIdx) @@ -180,7 +177,7 @@ class GuiItemEditor(QDialog): isExported = self.editExport.isChecked() self.theItem.setName(itemName) - self.theItem.setStatus(itemStatus) + self.theItem.setImportStatus(itemStatus) self.theItem.setLayout(itemLayout) self.theItem.setExported(isExported) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 3989f7eb..f6e4b611 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -285,7 +285,7 @@ class GuiProjectEditStatus(QWidget): self.listBox.setColumnWidth(self.COL_LABEL, wCol0) self.listBox.setIndentation(0) - for iName, iCol, nUse in self.theStatus: + for iName, iCol, nUse, _ in self.theStatus: self._addItem(iName, iCol, iName, nUse) # List Controls diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 4d35c978..64c2b73d 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -50,7 +50,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.core import NWDoc, NWSpellEnchant, countWords -from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwItemClass +from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert from novelwriter.common import transferCase from novelwriter.constants import nwConst, nwKeyWords, nwUnicode from novelwriter.gui.dochighlight import GuiDocHighlighter @@ -2940,17 +2940,10 @@ class GuiDocEditFooter(QWidget): sIcon = QPixmap() sText = "" else: - iStatus = self._theItem.itemStatus - if self._theItem.itemClass == nwItemClass.NOVEL: - iStatus = self.theProject.statusItems.checkEntry(iStatus) - theIcon = self.theParent.statusIcons[iStatus] - else: - iStatus = self.theProject.importItems.checkEntry(iStatus) - theIcon = self.theParent.importIcons[iStatus] - + theStatus, theIcon = self._theItem.getImportStatus() sIcon = theIcon.pixmap(self.sPx, self.sPx) hLevel = self.theParent.theIndex.getHandleHeaderLevel(self._docHandle) - sText = f"{self._theItem.itemStatus} / {self._theItem.describeMe(hLevel)}" + sText = f"{theStatus} / {self._theItem.describeMe(hLevel)}" self.statusIcon.setPixmap(sIcon) self.statusText.setText(sText) diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 0596f7a8..21df9b80 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -30,7 +30,7 @@ from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel -from novelwriter.enum import nwItemClass, nwItemType +from novelwriter.enum import nwItemType from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) @@ -249,16 +249,9 @@ class GuiItemDetails(QWidget): # Status # ====== - itStatus = nwItem.itemStatus - if nwItem.itemClass == nwItemClass.NOVEL: - itStatus = self.theProject.statusItems.checkEntry(itStatus) # Make sure it's valid - flagIcon = self.theParent.statusIcons[itStatus] - else: - itStatus = self.theProject.importItems.checkEntry(itStatus) # Make sure it's valid - flagIcon = self.theParent.importIcons[itStatus] - - self.statusIcon.setPixmap(flagIcon.pixmap(iPx, iPx)) - self.statusData.setText(nwItem.itemStatus) + theStatus, theIcon = nwItem.getImportStatus() + self.statusIcon.setPixmap(theIcon.pixmap(iPx, iPx)) + self.statusData.setText(theStatus) # Class # ===== diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 9f55cbe4..7c0ec69e 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -610,14 +610,7 @@ class GuiProjectTree(QTreeWidget): else: expIcon = self.theTheme.getIcon("cross") - iStatus = nwItem.itemStatus - if nwItem.itemClass == nwItemClass.NOVEL: - iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid - statIcon = self.theParent.statusIcons[iStatus] - else: - iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid - statIcon = self.theParent.importIcons[iStatus] - + itempStatus, statusIcon = nwItem.getImportStatus() hLevel = self.theIndex.getHandleHeaderLevel(tHandle) itemIcon = self.theTheme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel @@ -626,8 +619,8 @@ class GuiProjectTree(QTreeWidget): trItem.setIcon(self.C_NAME, itemIcon) trItem.setText(self.C_NAME, nwItem.itemName) trItem.setIcon(self.C_EXPORT, expIcon) - trItem.setIcon(self.C_STATUS, statIcon) - trItem.setToolTip(self.C_STATUS, nwItem.itemStatus) + trItem.setIcon(self.C_STATUS, statusIcon) + trItem.setToolTip(self.C_STATUS, itempStatus) if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT: trFont = trItem.font(self.C_NAME) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 2eaea912..9db27659 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -31,7 +31,7 @@ from time import time from datetime import datetime from PyQt5.QtCore import Qt, QTimer, QSize, QThreadPool, pyqtSlot -from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor +from PyQt5.QtGui import QIcon, QKeySequence, QCursor from PyQt5.QtWidgets import ( qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut, QMessageBox, QDialog, QTabWidget, QToolBar, QAction @@ -129,10 +129,6 @@ class GuiMain(QMainWindow): self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - # Minor GUI Elements - self.statusIcons = [] - self.importIcons = [] - # Project Tree Tabs self.projTabs = QTabWidget() self.projTabs.setTabPosition(QTabWidget.South) @@ -869,8 +865,6 @@ class GuiMain(QMainWindow): def rebuildTrees(self): """Rebuild the project tree. """ - self._makeStatusIcons() - self._makeImportIcons() self.treeView.buildTree() self.novelView.refreshTree() return @@ -1462,29 +1456,6 @@ class GuiMain(QMainWindow): self.saveDocument() return - def _makeStatusIcons(self): - """Generate all the item status icons based on project settings. - """ - self.statusIcons = {} - iPx = self.mainConf.pxInt(32) - for sLabel, sCol, _ in self.theProject.statusItems: - theIcon = QPixmap(iPx, iPx) - theIcon.fill(QColor(*sCol)) - self.statusIcons[sLabel] = QIcon(theIcon) - return - - def _makeImportIcons(self): - """Generate all the item importance icons based on project - settings. - """ - self.importIcons = {} - iPx = self.mainConf.pxInt(32) - for sLabel, sCol, _ in self.theProject.importItems: - theIcon = QPixmap(iPx, iPx) - theIcon.fill(QColor(*sCol)) - self.importIcons[sLabel] = QIcon(theIcon) - return - def _assembleProjectWizardData(self, newProj): """Extract the user choices from the New Project Wizard and store them in a dictionary. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index bf40e1ee..68265de5 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -7,7 +7,7 @@ Jay Doh 1303 199 - 64457 + 65005 False @@ -33,121 +33,121 @@
- New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Minor - Major - Main + None + Minor + Major + Main
- Novel + Novel - Title Page + Title Page - Page + Page - Part One + Part One - A Folder + A Folder - Chapter One + Chapter One - Making a Scene + Making a Scene - Another Scene + Another Scene - Interlude + Interlude - A Note on Structure + A Note on Structure - Chapter Two + Chapter Two - We Found John! + We Found John! - Characters + Characters - Main Characters + Main Characters - John Smith + John Smith - Jane Smith + Jane Smith - Locations + Locations - Earth + Earth - Space + Space - Mars + Mars - Archive + Archive - Scenes + Scenes - Old File + Old File - Trash + Trash - Delete Me! + Delete Me!
diff --git a/tests/conftest.py b/tests/conftest.py index cf348870..4d8d7183 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,13 @@ from PyQt5.QtWidgets import QMessageBox # noqa: E402 from novelwriter.config import Config # noqa: E402 +@pytest.fixture(autouse=True) +def initQt(qtbot): + """Ensures that the qt main thread is always available in all tests. + """ + return + + ## # Core Test Folders ## diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index c6eab805..55a582ad 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,12 +1,12 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 21 + 23 24 - 1847 + 1854 False @@ -31,102 +31,102 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main
- Novel + Novel - Lorem Ipsum + Lorem Ipsum - Front Matter + Front Matter - Prologue + Prologue - Act One + Act One - Chapter One + Chapter One - Chapter One + Chapter One - Scene One + Scene One - Scene Two + Scene Two - Interlude + Interlude - Chapter Two + Chapter Two - Chapter Two + Chapter Two - Scene Three + Scene Three - Scene Four + Scene Four - Scene Five + Scene Five - Characters + Characters - Mr. Nobody + Mr. Nobody - Plot + Plot - Main + Main - World + World - Ancient Europe + Ancient Europe
diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index 991d8b68..6f45815d 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,13 +1,13 @@ - + Test Minimal Minimal Jane Doe John Doh - 12 + 14 2 - 129 + 135 True @@ -29,50 +29,50 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main
- Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World
diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 4b8a67bf..10398c21 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -29,110 +29,110 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - Locations + Locations - Timeline + Timeline - Objects + Objects - Entities + Entities - Title Page + Title Page - Chapter 1 + Chapter 1 - Chapter 1 + Chapter 1 - Scene 1.1 + Scene 1.1 - Scene 1.2 + Scene 1.2 - Scene 1.3 + Scene 1.3 - Chapter 2 + Chapter 2 - Chapter 2 + Chapter 2 - Scene 2.1 + Scene 2.1 - Scene 2.2 + Scene 2.2 - Scene 2.3 + Scene 2.3 - Chapter 3 + Chapter 3 - Chapter 3 + Chapter 3 - Scene 3.1 + Scene 3.1 - Scene 3.2 + Scene 3.2 - Scene 3.3 + Scene 3.3
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 3f55663e..2bb2df31 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -29,74 +29,74 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - Locations + Locations - Timeline + Timeline - Objects + Objects - Entities + Entities - Title Page + Title Page - Scene 1 + Scene 1 - Scene 2 + Scene 2 - Scene 3 + Scene 3 - Scene 4 + Scene 4 - Scene 5 + Scene 5 - Scene 6 + Scene 6
diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index d623ee2d..d4ffb126 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,58 +27,58 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - World + World - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Hello + Hello - Jane + Jane
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 6becb112..20fa4d71 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,50 +27,50 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - World + World - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index cf55336e..22d51fd7 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,66 +27,66 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - World + World - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Timeline + Timeline - Object + Object - Custom1 + Custom1 - Custom2 + Custom2
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 4dd43b9a..685095bb 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,66 +27,66 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - New File + New File - Characters + Characters - New File + New File - World + World - New File + New File - Trash + Trash
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index f1b17740..a42552cf 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,50 +27,50 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World
diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index b88e7f17..c6652f72 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -33,50 +33,50 @@
- New - Note - Finished - Final + New + Note + Finished + Final - New - Minor - Major - Final + New + Minor + Major + Final - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World
diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 368592f1..61c67987 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -23,6 +23,8 @@ import pytest from lxml import etree +from PyQt5.QtGui import QIcon + from novelwriter.core import NWProject from novelwriter.core.item import NWItem from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout @@ -74,16 +76,17 @@ def testCoreItem_Setters(mockGUI): assert theItem.itemOrder == 1 # Importance - theItem.setStatus("Nonsense") - assert theItem.itemStatus == "New" - theItem.setStatus("New") - assert theItem.itemStatus == "New" - theItem.setStatus("Minor") - assert theItem.itemStatus == "Minor" - theItem.setStatus("Major") - assert theItem.itemStatus == "Major" - theItem.setStatus("Main") - assert theItem.itemStatus == "Main" + theItem._class = nwItemClass.CHARACTER + theItem.setImport("Nonsense") + assert theItem.itemImport == "New" + theItem.setImport("New") + assert theItem.itemImport == "New" + theItem.setImport("Minor") + assert theItem.itemImport == "Minor" + theItem.setImport("Major") + assert theItem.itemImport == "Major" + theItem.setImport("Main") + assert theItem.itemImport == "Main" # Status theItem._class = nwItemClass.NOVEL @@ -98,6 +101,27 @@ def testCoreItem_Setters(mockGUI): theItem.setStatus("Finished") assert theItem.itemStatus == "Finished" + # Status/Importance Wrapper + theItem._class = nwItemClass.CHARACTER + theItem.setImportStatus("New") + assert theItem.itemImport == "New" + theItem.setImportStatus("Minor") + assert theItem.itemImport == "Minor" + theItem.setImportStatus("Note") + assert theItem.itemImport == "New" + theItem.setImportStatus("Draft") + assert theItem.itemImport == "New" + + theItem._class = nwItemClass.NOVEL + theItem.setImportStatus("New") + assert theItem.itemStatus == "New" + theItem.setImportStatus("Minor") + assert theItem.itemStatus == "New" + theItem.setImportStatus("Note") + assert theItem.itemStatus == "Note" + theItem.setImportStatus("Draft") + assert theItem.itemStatus == "Draft" + # Expanded theItem.setExpanded(8) assert theItem.isExpanded is False @@ -196,6 +220,22 @@ def testCoreItem_Methods(mockGUI): theItem.setLayout("NOTE") assert theItem.describeMe() == "Project Note" + # Status + Icon + # ============= + theItem.setType("FILE") + theItem.setStatus("Note") + theItem.setImport("Minor") + + theItem.setClass("NOVEL") + stT, stI = theItem.getImportStatus() + assert stT == "Note" + assert isinstance(stI, QIcon) + + theItem.setClass("CHARACTER") + stT, stI = theItem.getImportStatus() + assert stT == "Minor" + assert isinstance(stI, QIcon) + # Representation # ============== @@ -342,9 +382,11 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): xContent = etree.SubElement(nwXML, "content") theItem.packXML(xContent) assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b'A Name' + b'' + b'' + b'A Name' + b'' ) # Unpack @@ -385,8 +427,11 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): xContent = etree.SubElement(nwXML, "content") theItem.packXML(xContent) assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b'A Name' + b'' + b'A Name' + b'' + b'' ) # Unpack diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 7ae461eb..311fca8c 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -61,10 +61,6 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI): # Creating the project once more should fail assert theProject.newProject({"projPath": fncDir}) is False - # Check the new project - copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) - # Open again assert theProject.openProject(projFile) is True @@ -857,7 +853,7 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): # Change importance fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3") - theProject.projTree[fHandle].setStatus("Main") + theProject.projTree[fHandle].setImport("Main") newList = [ ("New", 1, 1, 1, "New"), ("Minor", 2, 2, 2, "Minor"), @@ -872,7 +868,7 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): assert theProject.importItems._theColours == [ (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5) ] - assert theProject.projTree[fHandle].itemStatus == "Min" + assert theProject.projTree[fHandle].itemImport == "Min" # Check status counts assert theProject.statusItems._theCounts == [0, 0, 0, 0, 0] diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index ab5af333..a7a2d55e 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -23,6 +23,8 @@ import pytest from lxml import etree +from PyQt5.QtGui import QIcon + from novelwriter.core.status import NWStatus @@ -48,9 +50,9 @@ def testCoreStatus_Entries(): assert theStatus._theLength == 4 # Lookups - assert theStatus.lookupEntry(None) is None - assert theStatus.lookupEntry("stuff") is None - assert theStatus.lookupEntry("Main") == 3 + assert theStatus._getIndex(None) is None + assert theStatus._getIndex("stuff") is None + assert theStatus._getIndex("Main") == 3 # Checks assert theStatus.checkEntry(123) == "New" @@ -58,6 +60,10 @@ def testCoreStatus_Entries(): assert theStatus.checkEntry("New ") == "New" assert theStatus.checkEntry(" Main ") == "Main" + # Icons + assert isinstance(theStatus.getIcon("Stuff"), QIcon) + assert isinstance(theStatus.getIcon("New"), QIcon) + # Set new list newList = [ ("New", 1, 1, 1, "New"), @@ -87,12 +93,17 @@ def testCoreStatus_Entries(): assert theStatus._theCounts == countTo # Iterate - for i, (sA, sB, sC) in enumerate(theStatus): + for i, (sA, sB, sC, sD) in enumerate(theStatus): assert sA == theStatus._theLabels[i] assert sB == theStatus._theColours[i] assert sC == theStatus._theCounts[i] + assert sD == theStatus._theIcons[i] - assert theStatus[9] == (None, None, None) + sA, sB, sC, sD = theStatus[9] + assert sA is None + assert sB is None + assert sC is None + assert isinstance(sD, QIcon) # Clear counts theStatus.resetCounts() @@ -122,12 +133,12 @@ def testCoreStatus_XMLPackUnpack(): xStatus = etree.SubElement(nwXML, "status") theStatus.packXML(xStatus) assert etree.tostring(xStatus, pretty_print=False, encoding="utf-8") == ( - b"" - b"New" - b"Minor" - b"Major" - b"Main" - b"" + b'' + b'New' + b'Minor' + b'Major' + b'Main' + b'' ) # Unpack diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index d277a601..088de786 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -30,7 +30,7 @@ from novelwriter.core.tokenizer import Tokenizer class BareTokenizer(Tokenizer): def doConvert(self): - pass + super().doConvert() @pytest.mark.core @@ -219,6 +219,10 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): "# Notes: Plot\n\n" ) + # Ckeck abstract method + with pytest.raises(NotImplementedError): + theToken.doConvert() + # END Test testCoreToken_TextOps diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index e0e82f39..b9ad0473 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -387,25 +387,26 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( b'' b'' - b'' - b'Novel' + b'Novel' b'Act One' + b'class="NOVEL">Act One' + b'
' b'' - b'Chapter One' + b'Chapter One
' b'' - b'Scene One' - b'' - b'Outtakes' - b'' - b'Trash' + b'Scene One
' + b'Outtakes' + b'Trash' b'' - b'Characters' + b'Characters
' b'Jane Doe' + b'cursorPos="0"/>Jane Doe
' b'
' b'
' ) diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 4d183408..79038345 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -174,11 +174,12 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj): itemEdit.editName.setText("New Character") itemEdit.editStatus.setCurrentIndex(1) itemEdit.editExport.setChecked(False) + itemEdit._doSave() # Check New Settings - itemEdit._doSave() assert itemEdit.theItem.itemName == "New Character" - assert itemEdit.theItem.itemStatus == "Minor" + assert itemEdit.theItem.itemStatus == "New" + assert itemEdit.theItem.itemImport == "Minor" assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE assert itemEdit.theItem.isExported is False From 9c9408d78a868c94c2d599dbe5c118e4a4a19f54 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 11:22:41 +0200 Subject: [PATCH 014/179] Changing status or importance flags should still update hidden flags --- novelwriter/core/project.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index ff2c2774..04e5242e 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -1071,9 +1071,8 @@ class NWProject(): """ replaceMap = self.statusItems.setNewEntries(newCols) for nwItem in self.projTree: - if nwItem.itemClass in nwLists.CLS_NOVEL: - if nwItem.itemStatus in replaceMap: - nwItem.setStatus(replaceMap[nwItem.itemStatus]) + if nwItem.itemStatus in replaceMap: + nwItem.setStatus(replaceMap[nwItem.itemStatus]) self.setProjectChanged(True) return True @@ -1083,9 +1082,8 @@ class NWProject(): """ replaceMap = self.importItems.setNewEntries(newCols) for nwItem in self.projTree: - if nwItem.itemClass not in nwLists.CLS_NOVEL: - if nwItem.itemImport in replaceMap: - nwItem.setImport(replaceMap[nwItem.itemImport]) + if nwItem.itemImport in replaceMap: + nwItem.setImport(replaceMap[nwItem.itemImport]) self.setProjectChanged(True) return True From 7298e1f438b3110a81d2f31e4d92a6be827ab910 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 22:07:20 +0200 Subject: [PATCH 015/179] Add two new utility functions to the common module --- novelwriter/common.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/novelwriter/common.py b/novelwriter/common.py index 8e17a7ee..30ac0f2e 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -184,6 +184,12 @@ def checkIntRange(value, first, last, default): return default +def getMinMax(value, minVal, maxVal): + """Make sure an integer is between min and max value (inclusive). + """ + return min(maxVal, max(minVal, value)) + + def checkIntTuple(value, valid, default): """Check that an int is an element of a tuple. If it isn't, return the default value. @@ -245,6 +251,13 @@ def formatTime(tS): # String Functions # =============================================================================================== # +def simplified(string): + """Take a string an strip leading and trailing whitespaces, and + replace all occurences of (multiple) whitespaces with a 0x20 space. + """ + return " ".join(str(string).strip().split()) + + def splitVersionNumber(value): """Split a version string on the form aa.bb.cc into major, minor and patch, and computes an integer value aabbcc. From 40e7055f167c5f5f9442636e52a73c31be358807 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 22:08:14 +0200 Subject: [PATCH 016/179] Rewrite most of the NWStatus class --- novelwriter/core/item.py | 12 +- novelwriter/core/project.py | 24 ++-- novelwriter/core/status.py | 253 ++++++++++++++++++++---------------- 3 files changed, 162 insertions(+), 127 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 993b183c..d56d60f2 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -280,11 +280,11 @@ class NWItem(): the current item based on its class. """ if self._class in nwLists.CLS_NOVEL: - stName = self.theProject.statusItems.checkEntry(self._status) - stIcon = self.theProject.statusItems.getIcon(stName) + stName = self.theProject.statusItems.name(self._status) + stIcon = self.theProject.statusItems.icon(self._status) else: - stName = self.theProject.importItems.checkEntry(self._import) - stIcon = self.theProject.importItems.getIcon(stName) + stName = self.theProject.importItems.name(self._import) + stIcon = self.theProject.importItems.icon(self._import) return stName, stIcon def setImportStatus(self, theLabel): @@ -383,14 +383,14 @@ class NWItem(): """Set the item status by looking it up in the valid status items of the current project. """ - self._status = self.theProject.statusItems.checkEntry(theStatus) + self._status = self.theProject.statusItems.check(theStatus) return def setImport(self, theImport): """Set the item importance by looking it up in the valid import items of the current project. """ - self._import = self.theProject.importItems.checkEntry(theImport) + self._import = self.theProject.importItems.check(theImport) return def setExpanded(self, expState): diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 04e5242e..ee6f65ba 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -218,16 +218,16 @@ class NWProject(): } self.spellCheck = False self.autoOutline = True - self.statusItems = NWStatus() - self.statusItems.addEntry(self.tr("New"), (100, 100, 100)) - self.statusItems.addEntry(self.tr("Note"), (200, 50, 0)) - self.statusItems.addEntry(self.tr("Draft"), (200, 150, 0)) - self.statusItems.addEntry(self.tr("Finished"), (50, 200, 0)) - self.importItems = NWStatus() - self.importItems.addEntry(self.tr("New"), (100, 100, 100)) - self.importItems.addEntry(self.tr("Minor"), (200, 50, 0)) - self.importItems.addEntry(self.tr("Major"), (200, 150, 0)) - self.importItems.addEntry(self.tr("Main"), (50, 200, 0)) + self.statusItems = NWStatus("s") + self.statusItems.write(None, self.tr("New"), (100, 100, 100)) + self.statusItems.write(None, self.tr("Note"), (200, 50, 0)) + self.statusItems.write(None, self.tr("Draft"), (200, 150, 0)) + self.statusItems.write(None, self.tr("Finished"), (50, 200, 0)) + self.importItems = NWStatus("i") + self.importItems.write(None, self.tr("New"), (100, 100, 100)) + self.importItems.write(None, self.tr("Minor"), (200, 50, 0)) + self.importItems.write(None, self.tr("Major"), (200, 150, 0)) + self.importItems.write(None, self.tr("Main"), (50, 200, 0)) self.lastEdited = None self.lastViewed = None self.lastWCount = 0 @@ -1205,9 +1205,9 @@ class NWProject(): self.importItems.resetCounts() for nwItem in self.projTree: if nwItem.itemClass in nwLists.CLS_NOVEL: - self.statusItems.countEntry(nwItem.itemStatus) + self.statusItems.increment(nwItem.itemStatus) else: - self.importItems.countEntry(nwItem.itemImport) + self.importItems.increment(nwItem.itemImport) return def localLookup(self, theWord): diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 2ca8ed53..0f2f1aea 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -23,6 +23,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import random import logging import novelwriter @@ -30,134 +31,158 @@ from lxml import etree from PyQt5.QtGui import QIcon, QPixmap, QColor -from novelwriter.common import checkInt +from novelwriter.common import checkInt, getMinMax, simplified logger = logging.getLogger(__name__) class NWStatus(): - def __init__(self): + def __init__(self, type): + + self._type = str(type) + self._store = {} + self._reverse = {} + self._default = None - self._theLabels = [] - self._theColours = [] - self._theCounts = [] - self._theIcons = [] - self._theMap = {} - self._theLength = 0 - self._theIndex = 0 self._iconSize = novelwriter.CONFIG.pxInt(32) + pixmap = QPixmap(self._iconSize, self._iconSize) + pixmap.fill(QColor(100, 100, 100)) + self._defaultIcon = QIcon(pixmap) return - def addEntry(self, theLabel, theColours): - """Add a status entry to the status object, but ensure it isn't - a duplicate. + def write(self, key, name, cols): + """Add or update a status entry. If the key is invalid, a new + key is generated. """ - theLabel = theLabel.strip() - if self._getIndex(theLabel) is None: - theIcon = QPixmap(self._iconSize, self._iconSize) - theIcon.fill(QColor(*theColours)) - self._theIcons.append(QIcon(theIcon)) - self._theLabels.append(theLabel) - self._theColours.append(theColours) - self._theCounts.append(0) - self._theMap[theLabel] = self._theLength - self._theLength += 1 + if not self._isKey(key): + key = self._newKey() + if not isinstance(cols, tuple): + cols = (100, 100, 100) + if len(cols) != 3: + cols = (100, 100, 100) - return True + pixmap = QPixmap(self._iconSize, self._iconSize) + pixmap.fill(QColor(*cols)) - def checkEntry(self, theStatus): - """Check if a status value is valid, and returns the safe - reference to be used internally. + name = simplified(name) + count = self._store[key]["count"] if key in self._store else 0 + + self._store[key] = { + "name": name, + "icon": QIcon(pixmap), + "cols": cols, + "count": count, + } + self._reverse[name] = key + + if self._default is None: + self._default = key + + return key + + def check(self, value): + """Check the key against the stored status names. """ - if isinstance(theStatus, str): - if self._getIndex(theStatus) is not None: - return theStatus.strip() - return self._theLabels[0] + if self._isKey(value) and value in self._store: + return value + elif value in self._reverse: + return self._reverse[value] + elif self._default is not None: + return self._default + else: + return "" - def getIcon(self, theLabel): - """Return the icon for the given status item. + def name(self, key): + """Return the name associated with a given key. """ - theIndex = self._getIndex(theLabel) - if theIndex is not None: - return self._theIcons[theIndex] - return QIcon() + if key in self._store: + return self._store[key]["name"] + elif self._default is not None: + return self._store[self._default]["name"] + else: + return "" + + def cols(self, key): + """Return the colours associated with a given key. + """ + if key in self._store: + return self._store[key]["cols"] + elif self._default is not None: + return self._store[self._default]["cols"] + else: + return (100, 100, 100) + + def count(self, key): + """Return the count associated with a given key. + """ + if key in self._store: + return self._store[key]["count"] + elif self._default is not None: + return self._store[self._default]["count"] + else: + return 0 + + def icon(self, key): + """Return the icon associated with a given key. + """ + if key in self._store: + return self._store[key]["icon"] + elif self._default is not None: + return self._store[self._default]["icon"] + else: + return self._defaultIcon def setNewEntries(self, newList): """Update the list of entries after they have been modified by the GUI tool. """ - replaceMap = {} - - if newList is not None: - self._theLabels = [] - self._theColours = [] - self._theCounts = [] - self._theIcons = [] - self._theMap = {} - self._theLength = 0 - self._theIndex = 0 - - for nName, nR, nG, nB, oName in newList: - self.addEntry(nName, (nR, nG, nB)) - if nName != oName and oName is not None: - replaceMap[oName] = nName - - return replaceMap + return {} def resetCounts(self): """Clear the counts of references to the status entries. """ - self._theCounts = [0]*self._theLength + for key in self._store: + self._store[key]["count"] = 0 return - def countEntry(self, theLabel): - """Increment the counter for a given label. This should be used - together with resetCounts in a loop over project items. + def increment(self, key): + """Increment the counter for a given entry. """ - theIndex = self._getIndex(theLabel) - if theIndex is not None: - self._theCounts[theIndex] += 1 + if key in self._store: + self._store[key]["count"] += 1 return def packXML(self, xParent): """Pack the status entries into an XML object for saving to the main project file. """ - for n in range(self._theLength): + for key, data in self._store.items(): xSub = etree.SubElement(xParent, "entry", attrib={ - "red": str(self._theColours[n][0]), - "green": str(self._theColours[n][1]), - "blue": str(self._theColours[n][2]), + "key": key, + "red": str(data["cols"][0]), + "green": str(data["cols"][1]), + "blue": str(data["cols"][2]), }) - xSub.text = self._theLabels[n] + xSub.text = data["name"] + return True def unpackXML(self, xParent): """Unpack an XML tree and set the class values. """ - theLabels = [] - theColours = [] + self._store = {} + self._reverse = {} + self._default = None for xChild in xParent: - theLabels.append(xChild.text) - cR = checkInt(xChild.attrib.get("red", 0), 0, False) - cG = checkInt(xChild.attrib.get("green", 0), 0, False) - cB = checkInt(xChild.attrib.get("blue", 0), 0, False) - theColours.append((cR, cG, cB)) - - if len(theLabels) > 0: - self._theLabels = [] - self._theColours = [] - self._theCounts = [] - self._theIcons = [] - self._theMap = {} - self._theLength = 0 - self._theIndex = 0 - - for n in range(len(theLabels)): - self.addEntry(theLabels[n], theColours[n]) + name = xChild.text.strip() + key = xChild.attrib.get("key", None) + cR = getMinMax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255) + cG = getMinMax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255) + cB = getMinMax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255) + self.write(key, name, (cR, cG, cB)) return True @@ -165,39 +190,49 @@ class NWStatus(): # Internal Functions ## - def _getIndex(self, theLabel): - """Look up a status entry in the object lists, and return it if - it exists. + def _newKey(self): + """Generate a new key for a status flag. This method is + recursive, but should only fail if there is an issue with the + random number generator or the user has added a lot of status + flags. The Python recursion limit is given the job to handle + the extreme case and will cause an app crash. """ - if theLabel is None: - return None - return self._theMap.get(theLabel.strip(), None) + key = f"{self._type}{random.randint(0, 0xffffff):06x}" + if key in self._store: + key = self._newKey() + return key + + def _isKey(self, key): + """Check if a string is a key or not. + """ + if not isinstance(key, str): + return False + if len(key) != 7: + return False + if key[0] != self._type: + return False + for c in key[1:]: + if c not in "0123456789abcdef": + return False + return True ## # Iterator Bits ## - def __getitem__(self, n): - """Return an entry by its index. - """ - if n >= 0 and n < self._theLength: - return self._theLabels[n], self._theColours[n], self._theCounts[n], self._theIcons[n] - return None, None, None, QIcon() + def __getitem__(self, key): + return self._store[key] def __iter__(self): - """Initialise the iterator. - """ - self._theIndex = 0 - return self + return iter(self._store) - def __next__(self): - """Return the next entry for the iterator. - """ - if self._theIndex < self._theLength: - theLabel, theColour, theCount, theIcon = self.__getitem__(self._theIndex) - self._theIndex += 1 - return theLabel, theColour, theCount, theIcon - else: - raise StopIteration + def keys(self): + return self._store.keys() + + def items(self): + return self._store.items() + + def values(self): + return self._store.values() # END Class NWStatus From ddd8b6081647d3ca29776b29710127cbede18ef0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 23:13:39 +0200 Subject: [PATCH 017/179] Complete the needed GUI class changes for the new NWStatus class --- novelwriter/common.py | 2 +- novelwriter/core/project.py | 45 ++++++++--- novelwriter/core/status.py | 44 +++++++---- novelwriter/dialogs/projsettings.py | 118 +++++++++++++++------------- novelwriter/gui/itemdetails.py | 5 ++ novelwriter/guimain.py | 4 +- 6 files changed, 135 insertions(+), 83 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 30ac0f2e..69f6bc05 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -184,7 +184,7 @@ def checkIntRange(value, first, last, default): return default -def getMinMax(value, minVal, maxVal): +def minmax(value, minVal, maxVal): """Make sure an integer is between min and max value (inclusive). """ return min(maxVal, max(minVal, value)) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index ee6f65ba..d17cffc3 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -687,6 +687,8 @@ class NWProject(): if len(aKey) > 0: self._packProjectValue(xTitleFmt, aKey, aValue) + # Save Status/Importance + self.countStatus() xStatus = etree.SubElement(xSettings, "status") self.statusItems.packXML(xStatus) xStatus = etree.SubElement(xSettings, "importance") @@ -1018,7 +1020,8 @@ class NWProject(): if self.projSpell != theLang: self.projSpell = theLang self.setProjectChanged(True) - return True + return True + return False def setProjectLang(self, theLang): """Set the project-specific language. @@ -1065,26 +1068,46 @@ class NWProject(): self.setProjectChanged(True) return True - def setStatusColours(self, newCols): + def setStatusColours(self, newCols, delCols): """Update the list of novel file status flags. Also iterate through the project and replace keys that have been renamed. """ - replaceMap = self.statusItems.setNewEntries(newCols) - for nwItem in self.projTree: - if nwItem.itemStatus in replaceMap: - nwItem.setStatus(replaceMap[nwItem.itemStatus]) + if not (newCols or delCols): + return False + + for entry in newCols: + key = entry.get("key", None) + name = entry.get("name", "") + cols = entry.get("cols", (100, 100, 100)) + if name: + self.statusItems.write(key, name, cols) + + for key in delCols: + self.statusItems.remove(key) + self.setProjectChanged(True) + return True - def setImportColours(self, newCols): + def setImportColours(self, newCols, delCols): """Update the list of note file importance flags. Also iterate through the project and replace keys that have been renamed. """ - replaceMap = self.importItems.setNewEntries(newCols) - for nwItem in self.projTree: - if nwItem.itemImport in replaceMap: - nwItem.setImport(replaceMap[nwItem.itemImport]) + if not (newCols or delCols): + return False + + for entry in newCols: + key = entry.get("key", None) + name = entry.get("name", "") + cols = entry.get("cols", (100, 100, 100)) + if name: + self.importItems.write(key, name, cols) + + for key in delCols: + self.importItems.remove(key) + self.setProjectChanged(True) + return True def setAutoReplace(self, autoReplace): diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 0f2f1aea..4aee93b2 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -4,7 +4,8 @@ novelWriter – Project Item Status Class Data class for the status/importance settings of a project item File History: -Created: 2019-05-19 [0.1.3] +Created: 2019-05-19 [0.1.3] +Rewritten: 2022-04-05 [1.7a0] This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -31,7 +32,7 @@ from lxml import etree from PyQt5.QtGui import QIcon, QPixmap, QColor -from novelwriter.common import checkInt, getMinMax, simplified +from novelwriter.common import checkInt, minmax, simplified logger = logging.getLogger(__name__) @@ -52,7 +53,7 @@ class NWStatus(): return - def write(self, key, name, cols): + def write(self, key, name, cols, count=None): """Add or update a status entry. If the key is invalid, a new key is generated. """ @@ -67,7 +68,8 @@ class NWStatus(): pixmap.fill(QColor(*cols)) name = simplified(name) - count = self._store[key]["count"] if key in self._store else 0 + if count is None: + count = self._store[key]["count"] if key in self._store else 0 self._store[key] = { "name": name, @@ -82,6 +84,20 @@ class NWStatus(): return key + def remove(self, key): + """Remove an entry in the list, but not if the count is larger + than 0. + """ + if key not in self._store: + return False + if self._store[key]["count"] > 0: + return False + + del self._reverse[self._store[key]["name"]] + del self._store[key] + + return True + def check(self, value): """Check the key against the stored status names. """ @@ -134,12 +150,6 @@ class NWStatus(): else: return self._defaultIcon - def setNewEntries(self, newList): - """Update the list of entries after they have been modified by - the GUI tool. - """ - return {} - def resetCounts(self): """Clear the counts of references to the status entries. """ @@ -161,6 +171,7 @@ class NWStatus(): for key, data in self._store.items(): xSub = etree.SubElement(xParent, "entry", attrib={ "key": key, + "count": str(data["count"]), "red": str(data["cols"][0]), "green": str(data["cols"][1]), "blue": str(data["cols"][2]), @@ -177,12 +188,13 @@ class NWStatus(): self._default = None for xChild in xParent: - name = xChild.text.strip() - key = xChild.attrib.get("key", None) - cR = getMinMax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255) - cG = getMinMax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255) - cB = getMinMax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255) - self.write(key, name, (cR, cG, cB)) + key = xChild.attrib.get("key", None) + name = xChild.text.strip() + count = max(checkInt(xChild.attrib.get("count", 0), 0), 0) + red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255) + green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255) + blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255) + self.write(key, name, (red, green, blue), count) return True diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index f6e4b611..260f8b10 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -35,6 +35,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.enum import nwAlert +from novelwriter.common import simplified from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout logger = logging.getLogger(__name__) @@ -81,6 +82,9 @@ class GuiProjectSettings(PagedDialog): self.buttonBox.rejected.connect(self._doClose) self.addControls(self.buttonBox) + # Flags + self.spellChanged = False + logger.debug("GuiProjectSettings initialisation complete") return @@ -103,16 +107,18 @@ class GuiProjectSettings(PagedDialog): self.theProject.setProjectName(projName) self.theProject.setBookTitle(bookTitle) self.theProject.setBookAuthors(bookAuthors) - self.theProject.setSpellLang(spellLang) self.theProject.setProjBackup(doBackup) + # Remember this as updating spell dictionary can be expensive + self.spellChanged = self.theProject.setSpellLang(spellLang) + if self.tabStatus.colChanged: - statusCol = self.tabStatus.getNewList() - self.theProject.setStatusColours(statusCol) + newList, delList = self.tabStatus.getNewList() + self.theProject.setStatusColours(newList, delList) if self.tabImport.colChanged: - importCol = self.tabImport.getNewList() - self.theProject.setImportColours(importCol) + newList, delList = self.tabImport.getNewList() + self.theProject.setImportColours(newList, delList) if self.tabStatus.colChanged or self.tabImport.colChanged: self.theParent.rebuildTrees() @@ -245,6 +251,10 @@ class GuiProjectEditStatus(QWidget): COL_LABEL = 0 COL_USAGE = 1 + KEY_ROLE = Qt.UserRole + COL_ROLE = Qt.UserRole + 1 + NUM_ROLE = Qt.UserRole + 2 + def __init__(self, theParent, theProject, isStatus): QWidget.__init__(self, theParent) @@ -267,10 +277,9 @@ class GuiProjectEditStatus(QWidget): self.optState.getInt("GuiProjectSettings", colSetting, 130) ) - self.colData = [] - self.colCounts = [] + self.colDeleted = [] self.colChanged = False - self.selColour = None + self.selColour = QColor(100, 100, 100) self.iPx = self.theTheme.baseIconSize @@ -285,8 +294,8 @@ class GuiProjectEditStatus(QWidget): self.listBox.setColumnWidth(self.COL_LABEL, wCol0) self.listBox.setIndentation(0) - for iName, iCol, nUse, _ in self.theStatus: - self._addItem(iName, iCol, iName, nUse) + for key, data in self.theStatus.items(): + self._addItem(key, data["name"], data["cols"], data["count"]) # List Controls # ============= @@ -349,12 +358,15 @@ class GuiProjectEditStatus(QWidget): if self.colChanged: newList = [] for n in range(self.listBox.topLevelItemCount()): - nItem = self.listBox.topLevelItem(n) - nIdx = nItem.data(self.COL_LABEL, Qt.UserRole) - newList.append(self.colData[nIdx]) - return newList + item = self.listBox.topLevelItem(n) + newList.append({ + "key": item.data(self.COL_LABEL, self.KEY_ROLE), + "name": item.text(self.COL_LABEL), + "cols": item.data(self.COL_LABEL, self.COL_ROLE), + }) + return newList, self.colDeleted - return None + return [], [] ## # User Actions @@ -369,16 +381,16 @@ class GuiProjectEditStatus(QWidget): ) if newCol.isValid(): self.selColour = newCol - colPixmap = QPixmap(self.iPx, self.iPx) - colPixmap.fill(newCol) - self.colButton.setIcon(QIcon(colPixmap)) - self.colButton.setIconSize(colPixmap.rect().size()) + pixmap = QPixmap(self.iPx, self.iPx) + pixmap.fill(newCol) + self.colButton.setIcon(QIcon(pixmap)) + self.colButton.setIconSize(pixmap.rect().size()) return def _newItem(self): """Create a new status item. """ - newItem = self._addItem(self.tr("New Item"), (0, 0, 0), None, 0) + newItem = self._addItem(None, self.tr("New Item"), (0, 0, 0), 0) newItem.setBackground(self.COL_LABEL, QBrush(QColor(0, 255, 0, 70))) newItem.setBackground(self.COL_USAGE, QBrush(QColor(0, 255, 0, 70))) self.colChanged = True @@ -390,14 +402,14 @@ class GuiProjectEditStatus(QWidget): selItem = self._getSelectedItem() if selItem is not None: iRow = self.listBox.indexOfTopLevelItem(selItem) - selIdx = selItem.data(self.COL_LABEL, Qt.UserRole) - if self.colCounts[selIdx] == 0: - self.listBox.takeTopLevelItem(iRow) - self.colChanged = True - else: + if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: self.theParent.makeAlert(self.tr( "Cannot delete a status item that is in use." ), nwAlert.ERROR) + else: + self.listBox.takeTopLevelItem(iRow) + self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) + self.colChanged = True return def _saveItem(self): @@ -405,36 +417,33 @@ class GuiProjectEditStatus(QWidget): """ selItem = self._getSelectedItem() if selItem is not None: - selIdx = selItem.data(self.COL_LABEL, Qt.UserRole) - self.colData[selIdx] = ( - self.editName.text().strip(), - self.selColour.red(), - self.selColour.green(), - self.selColour.blue(), - self.colData[selIdx][4] - ) - selItem.setText(self.COL_LABEL, self.colData[selIdx][0]) - selItem.setText(self.COL_USAGE, self._usageString(self.colCounts[selIdx])) + selItem.setText(self.COL_LABEL, simplified(self.editName.text())) selItem.setIcon(self.COL_LABEL, self.colButton.icon()) + selItem.setData(self.COL_LABEL, self.COL_ROLE, ( + self.selColour.red(), self.selColour.green(), self.selColour.blue() + )) self.editName.setEnabled(False) self.colChanged = True return - def _addItem(self, iName, iCol, oName, nUse): + def _addItem(self, key, name, cols, count): """Add a status item to the list. """ - newIcon = QPixmap(self.iPx, self.iPx) - newIcon.fill(QColor(*iCol)) - newItem = QTreeWidgetItem() - newItem.setText(self.COL_LABEL, iName) - newItem.setText(self.COL_USAGE, self._usageString(nUse)) - newItem.setIcon(self.COL_LABEL, QIcon(newIcon)) - newItem.setData(self.COL_LABEL, Qt.UserRole, len(self.colData)) - self.listBox.addTopLevelItem(newItem) - self.colData.append((iName, iCol[0], iCol[1], iCol[2], oName)) - self.colCounts.append(nUse) - return newItem + pixmap = QPixmap(self.iPx, self.iPx) + pixmap.fill(QColor(*cols)) + + item = QTreeWidgetItem() + item.setText(self.COL_LABEL, name) + item.setIcon(self.COL_LABEL, QIcon(pixmap)) + item.setData(self.COL_LABEL, self.KEY_ROLE, key) + item.setData(self.COL_LABEL, self.COL_ROLE, cols) + item.setData(self.COL_LABEL, self.NUM_ROLE, count) + item.setText(self.COL_USAGE, self._usageString(count)) + + self.listBox.addTopLevelItem(item) + + return item def _selectedItem(self): """Extract the info of a selected item and populate the settings @@ -442,13 +451,14 @@ class GuiProjectEditStatus(QWidget): """ selItem = self._getSelectedItem() if selItem is not None: - selIdx = selItem.data(self.COL_LABEL, Qt.UserRole) - selVal = self.colData[selIdx] - self.selColour = QColor(selVal[1], selVal[2], selVal[3]) - newIcon = QPixmap(self.iPx, self.iPx) - newIcon.fill(self.selColour) - self.editName.setText(selVal[0]) - self.colButton.setIcon(QIcon(newIcon)) + cols = selItem.data(self.COL_LABEL, self.COL_ROLE) + name = selItem.text(self.COL_LABEL) + + pixmap = QPixmap(self.iPx, self.iPx) + pixmap.fill(QColor(*cols)) + self.selColour = QColor(*cols) + self.editName.setText(name) + self.colButton.setIcon(QIcon(pixmap)) self.editName.setEnabled(True) self.editName.selectAll() self.editName.setFocus() diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 21df9b80..73082fa5 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -214,6 +214,11 @@ class GuiItemDetails(QWidget): return + def refreshDetails(self): + """Reload the content of the details panel. + """ + self.updateViewBox(self._itemHandle) + def updateViewBox(self, tHandle): """Populate the details box from a given handle. """ diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 9db27659..2ffffe45 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1002,7 +1002,9 @@ class GuiMain(QMainWindow): if dlgProj.result() == QDialog.Accepted: logger.debug("Applying new project settings") - self.docEditor.setDictionaries() + if dlgProj.spellChanged: + self.docEditor.setDictionaries() + self.treeMeta.refreshDetails() self._updateWindowTitle(self.theProject.projName) return True From ac904ee9fc36935578ed3ec1f58235d5e04061e8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 23:15:18 +0200 Subject: [PATCH 018/179] Update sample project file --- sample/nwProject.nwx | 76 ++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 68265de5..002cea43 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -7,7 +7,7 @@ Jay Doh 1303 199 - 65005 + 65049 False @@ -33,121 +33,121 @@
- New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Minor - Major - Main + None + Minor + Major + Main
- Novel + Novel - Title Page + Title Page - Page + Page - Part One + Part One - A Folder + A Folder - Chapter One + Chapter One - Making a Scene + Making a Scene - Another Scene + Another Scene - Interlude + Interlude - A Note on Structure + A Note on Structure - Chapter Two + Chapter Two - We Found John! + We Found John! - Characters + Characters - Main Characters + Main Characters - John Smith + John Smith - Jane Smith + Jane Smith - Locations + Locations - Earth + Earth - Space + Space - Mars + Mars - Archive + Archive - Scenes + Scenes - Old File + Old File - Trash + Trash - Delete Me! + Delete Me!
From e98e942f198b957b27121371ba8d7eba0856be4f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 14:59:25 +0200 Subject: [PATCH 019/179] Replace key generator in the NWStatus class --- novelwriter/core/project.py | 5 +++-- novelwriter/core/status.py | 26 ++++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index d17cffc3..b00b73cf 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -218,12 +218,12 @@ class NWProject(): } self.spellCheck = False self.autoOutline = True - self.statusItems = NWStatus("s") + self.statusItems = NWStatus(NWStatus.STATUS) self.statusItems.write(None, self.tr("New"), (100, 100, 100)) self.statusItems.write(None, self.tr("Note"), (200, 50, 0)) self.statusItems.write(None, self.tr("Draft"), (200, 150, 0)) self.statusItems.write(None, self.tr("Finished"), (50, 200, 0)) - self.importItems = NWStatus("i") + self.importItems = NWStatus(NWStatus.IMPORT) self.importItems.write(None, self.tr("New"), (100, 100, 100)) self.importItems.write(None, self.tr("Minor"), (200, 50, 0)) self.importItems.write(None, self.tr("Major"), (200, 150, 0)) @@ -267,6 +267,7 @@ class NWProject(): logger.error("No project path set for the new project") return False + self.clearProject() if not self.setProjectPath(projPath, newProject=True): return False diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 4aee93b2..a3792764 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -39,9 +39,12 @@ logger = logging.getLogger(__name__) class NWStatus(): + STATUS = 1 + IMPORT = 2 + def __init__(self, type): - self._type = str(type) + self._type = type self._store = {} self._reverse = {} self._default = None @@ -51,6 +54,13 @@ class NWStatus(): pixmap.fill(QColor(100, 100, 100)) self._defaultIcon = QIcon(pixmap) + if self._type == self.STATUS: + self._prefix = "s" + elif self._type == self.IMPORT: + self._prefix = "i" + else: + raise Exception("This is a bug!") + return def write(self, key, name, cols, count=None): @@ -209,21 +219,21 @@ class NWStatus(): flags. The Python recursion limit is given the job to handle the extreme case and will cause an app crash. """ - key = f"{self._type}{random.randint(0, 0xffffff):06x}" + key = f"{self._prefix}{random.getrandbits(24):06x}" if key in self._store: key = self._newKey() return key - def _isKey(self, key): - """Check if a string is a key or not. + def _isKey(self, value): + """Check if a value is a key or not. """ - if not isinstance(key, str): + if not isinstance(value, str): return False - if len(key) != 7: + if len(value) != 7: return False - if key[0] != self._type: + if value[0] != self._prefix: return False - for c in key[1:]: + for c in value[1:]: if c not in "0123456789abcdef": return False return True From cf7e1da96aad29d52ccafc7eb6e5f19bda57a22a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 14:59:59 +0200 Subject: [PATCH 020/179] Fix display text issues in varuious GUI elements --- novelwriter/dialogs/itemeditor.py | 30 ++++++++++++++++------------- novelwriter/dialogs/projsettings.py | 4 ++-- novelwriter/gui/outlinedetails.py | 4 +++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py index a39d93f4..84a0db14 100644 --- a/novelwriter/dialogs/itemeditor.py +++ b/novelwriter/dialogs/itemeditor.py @@ -75,11 +75,20 @@ class GuiItemEditor(QDialog): self.editStatus = QComboBox() self.editStatus.setMinimumWidth(mVd) if self.theItem.itemClass in nwLists.CLS_NOVEL: - for sLabel, _, _, sIcon in self.theProject.statusItems: - self.editStatus.addItem(sIcon, sLabel, sLabel) + for key, entry in self.theProject.statusItems.items(): + self.editStatus.addItem(entry["icon"], entry["name"], key) + + index = self.editStatus.findData(self.theItem.itemStatus) + if index != -1: + self.editStatus.setCurrentIndex(index) + else: - for sLabel, _, _, sIcon in self.theProject.importItems: - self.editStatus.addItem(sIcon, sLabel, sLabel) + for key, entry in self.theProject.importItems.items(): + self.editStatus.addItem(entry["icon"], entry["name"], key) + + index = self.editStatus.findData(self.theItem.itemImport) + if index != -1: + self.editStatus.setCurrentIndex(index) # Item Layout self.editLayout = QComboBox() @@ -97,6 +106,10 @@ class GuiItemEditor(QDialog): if itemLayout in validLayouts: self.editLayout.addItem(trConst(nwLabels.LAYOUT_NAME[itemLayout]), itemLayout) + index = self.editLayout.findData(self.theItem.itemLayout) + if index != -1: + self.editLayout.setCurrentIndex(index) + # Export Switch self.textExport = QLabel(self.tr("Include when building project")) self.editExport = QSwitch() @@ -116,15 +129,6 @@ class GuiItemEditor(QDialog): self.editName.setText(self.theItem.itemName) self.editName.selectAll() - currStatus, _ = self.theItem.getImportStatus() - statusIdx = self.editStatus.findData(currStatus) - if statusIdx != -1: - self.editStatus.setCurrentIndex(statusIdx) - - layoutIdx = self.editLayout.findData(self.theItem.itemLayout) - if layoutIdx != -1: - self.editLayout.setCurrentIndex(layoutIdx) - ## # Assemble ## diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 260f8b10..9599cc92 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -294,8 +294,8 @@ class GuiProjectEditStatus(QWidget): self.listBox.setColumnWidth(self.COL_LABEL, wCol0) self.listBox.setIndentation(0) - for key, data in self.theStatus.items(): - self._addItem(key, data["name"], data["cols"], data["count"]) + for key, entry in self.theStatus.items(): + self._addItem(key, entry["name"], entry["cols"], entry["count"]) # List Controls # ============= diff --git a/novelwriter/gui/outlinedetails.py b/novelwriter/gui/outlinedetails.py index cc97cdf5..b45c298f 100644 --- a/novelwriter/gui/outlinedetails.py +++ b/novelwriter/gui/outlinedetails.py @@ -291,8 +291,10 @@ class GuiOutlineDetails(QScrollArea): self.titleLabel.setText("%s" % self.tr("Title")) self.titleValue.setText(novIdx["title"]) + itemStatus, _ = nwItem.getImportStatus() + self.fileValue.setText(nwItem.itemName) - self.itemValue.setText(nwItem.itemStatus) + self.itemValue.setText(itemStatus) cC = checkInt(novIdx["cCount"], 0) wC = checkInt(novIdx["wCount"], 0) From fedb064b1f6b2ec62414aca9da7b7eda10c03805 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 15:01:55 +0200 Subject: [PATCH 021/179] Fix other tests --- tests/conftest.py | 19 +++- tests/lipsum/nwProject.nwx | 64 +++++------ tests/minimal/nwProject.nwx | 38 +++---- .../coreProject_NewCustomA_nwProject.nwx | 64 +++++------ .../coreProject_NewCustomB_nwProject.nwx | 46 ++++---- .../coreProject_NewFile_nwProject.nwx | 38 +++---- .../coreProject_NewMinimal_nwProject.nwx | 34 +++--- .../coreProject_NewRoot_nwProject.nwx | 42 +++---- .../guiEditor_Main_Final_nwProject.nwx | 40 +++---- .../guiEditor_Main_Initial_nwProject.nwx | 34 +++--- .../guiProjSettings_Dialog_nwProject.nwx | 34 +++--- tests/test_base/test_base_common.py | 27 ++++- tests/test_core/test_core_item.py | 77 ++++++------- tests/test_core/test_core_project.py | 107 ++++++++++-------- tests/test_dialogs/test_dlg_itemeditor.py | 36 ++++-- tests/test_dialogs/test_dlg_projload.py | 3 +- tests/test_dialogs/test_dlg_projsettings.py | 39 +++++-- tests/test_dialogs/test_dlg_wordlist.py | 1 + tests/test_gui/test_gui_doceditor.py | 1 + tests/test_gui/test_gui_guimain.py | 4 +- tests/test_gui/test_gui_theme.py | 1 + 21 files changed, 412 insertions(+), 337 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4d8d7183..8551724b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,8 @@ import sys import pytest import shutil +from dataclasses import dataclass + from mock import MockGuiMain from tools import cleanProject @@ -249,9 +251,24 @@ def nwOldProj(tmpDir): ## -# Useful Fixtures +# Data Fixtures ## +@dataclass +class TestConst: + + statusKeys = ["sa3b179", "s1c8031", "s06671a", "sbdd640"] + importKeys = ["i466852", "i3eb13b", "i392456", "i23b8c1"] + + +@pytest.fixture(scope="session") +def constData(): + """A named tuple of known contstant values. For those that depend on + the random number generator, they assume the seed is 42. + """ + return TestConst() + + @pytest.fixture(scope="session") def ipsumText(): """Return five paragraphs of Lorem Ipsum text. diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index 55a582ad..8ffb64fc 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,12 +1,12 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 23 + 24 24 - 1854 + 1856 False @@ -31,102 +31,102 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main
- Novel + Novel - Lorem Ipsum + Lorem Ipsum - Front Matter + Front Matter - Prologue + Prologue - Act One + Act One - Chapter One + Chapter One - Chapter One + Chapter One - Scene One + Scene One - Scene Two + Scene Two - Interlude + Interlude - Chapter Two + Chapter Two - Chapter Two + Chapter Two - Scene Three + Scene Three - Scene Four + Scene Four - Scene Five + Scene Five - Characters + Characters - Mr. Nobody + Mr. Nobody - Plot + Plot - Main + Main - World + World - Ancient Europe + Ancient Europe
diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index 6f45815d..9824e2bb 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,13 +1,13 @@ - + Test Minimal Minimal Jane Doe John Doh - 14 + 15 2 - 135 + 146 True @@ -29,50 +29,50 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main
- Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World
diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 10398c21..65d56307 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -29,110 +29,110 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - Locations + Locations - Timeline + Timeline - Objects + Objects - Entities + Entities - Title Page + Title Page - Chapter 1 + Chapter 1 - Chapter 1 + Chapter 1 - Scene 1.1 + Scene 1.1 - Scene 1.2 + Scene 1.2 - Scene 1.3 + Scene 1.3 - Chapter 2 + Chapter 2 - Chapter 2 + Chapter 2 - Scene 2.1 + Scene 2.1 - Scene 2.2 + Scene 2.2 - Scene 2.3 + Scene 2.3 - Chapter 3 + Chapter 3 - Chapter 3 + Chapter 3 - Scene 3.1 + Scene 3.1 - Scene 3.2 + Scene 3.2 - Scene 3.3 + Scene 3.3
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 2bb2df31..baab7a17 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -29,74 +29,74 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - Locations + Locations - Timeline + Timeline - Objects + Objects - Entities + Entities - Title Page + Title Page - Scene 1 + Scene 1 - Scene 2 + Scene 2 - Scene 3 + Scene 3 - Scene 4 + Scene 4 - Scene 5 + Scene 5 - Scene 6 + Scene 6
diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index d4ffb126..978f5855 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,58 +27,58 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - World + World - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Hello + Hello - Jane + Jane
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 20fa4d71..fc2ad735 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,50 +27,50 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - World + World - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 22d51fd7..94fdbeea 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,66 +27,66 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Plot + Plot - Characters + Characters - World + World - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Timeline + Timeline - Object + Object - Custom1 + Custom1 - Custom2 + Custom2
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 685095bb..3432d06b 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,62 +27,62 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - New File + New File - Characters + Characters - New File + New File - World + World - New File + New File diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index a42552cf..e6e80636 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,50 +27,50 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World
diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index c6652f72..f41ac5c8 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -33,50 +33,50 @@
- New - Note - Finished - Final + New + Note + Finished + Final - New - Minor - Major - Final + New + Minor + Major + Final - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index f5eef8fe..d7ad4119 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -31,9 +31,9 @@ from novelwriter.guimain import GuiMain from novelwriter.common import ( checkString, checkInt, checkFloat, checkBool, checkHandle, isHandle, isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, checkIntRange, - checkIntTuple, formatInt, formatTimeStamp, formatTime, splitVersionNumber, - transferCase, fuzzyTime, numberToRoman, jsonEncode, readTextFile, - makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser + minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, simplified, + splitVersionNumber, transferCase, fuzzyTime, numberToRoman, jsonEncode, + readTextFile, makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser ) @@ -226,6 +226,16 @@ def testBaseCommon_CheckIntRange(): # END Test testBaseCommon_CheckIntRange +@pytest.mark.base +def testBaseCommon_MinMax(): + """Test the minmax function. + """ + for i in range(-5, 15): + assert 0 <= minmax(i, 0, 10) <= 10 + +# END Test testBaseCommon_MinMax + + @pytest.mark.base def testBaseCommon_CheckIntTuple(): """Test the checkIntTuple function. @@ -270,6 +280,17 @@ def testBaseCommon_FormatTime(): # END Test testBaseCommon_FormatTime +@pytest.mark.base +def testBaseCommon_Simplified(): + """Test the simplified function. + """ + assert simplified("Hello World") == "Hello World" + assert simplified(" Hello World ") == "Hello World" + assert simplified("\tHello\n\r\tWorld") == "Hello World" + +# END Test testBaseCommon_Simplified + + @pytest.mark.base def testBaseCommon_SplitVersionNumber(): """Test the splitVersionNumber function. diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 61c67987..eb1425b3 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -20,6 +20,7 @@ along with this program. If not, see . """ import pytest +import random from lxml import etree @@ -31,9 +32,10 @@ from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout @pytest.mark.core -def testCoreItem_Setters(mockGUI): +def testCoreItem_Setters(mockGUI, constData): """Test all the simple setters for the NWItem class. """ + random.seed(42) theProject = NWProject(mockGUI) theItem = NWItem(theProject) @@ -77,50 +79,32 @@ def testCoreItem_Setters(mockGUI): # Importance theItem._class = nwItemClass.CHARACTER - theItem.setImport("Nonsense") - assert theItem.itemImport == "New" - theItem.setImport("New") - assert theItem.itemImport == "New" - theItem.setImport("Minor") - assert theItem.itemImport == "Minor" - theItem.setImport("Major") - assert theItem.itemImport == "Major" - theItem.setImport("Main") - assert theItem.itemImport == "Main" + theItem.setImport("Word") + assert theItem.itemImport == constData.importKeys[0] # Default + for key in constData.importKeys: + theItem.setImport(key) + assert theItem.itemImport == key # Status theItem._class = nwItemClass.NOVEL - theItem.setStatus("Nonsense") - assert theItem.itemStatus == "New" - theItem.setStatus("New") - assert theItem.itemStatus == "New" - theItem.setStatus("Note") - assert theItem.itemStatus == "Note" - theItem.setStatus("Draft") - assert theItem.itemStatus == "Draft" - theItem.setStatus("Finished") - assert theItem.itemStatus == "Finished" + theItem.setStatus("Word") + assert theItem.itemStatus == constData.statusKeys[0] # Default + for key in constData.statusKeys: + theItem.setStatus(key) + assert theItem.itemStatus == key # Status/Importance Wrapper theItem._class = nwItemClass.CHARACTER - theItem.setImportStatus("New") - assert theItem.itemImport == "New" - theItem.setImportStatus("Minor") - assert theItem.itemImport == "Minor" - theItem.setImportStatus("Note") - assert theItem.itemImport == "New" - theItem.setImportStatus("Draft") - assert theItem.itemImport == "New" + for key in constData.importKeys: + theItem.setImport(key) + assert theItem.itemImport == key + assert theItem.itemStatus == constData.statusKeys[3] # Should not change theItem._class = nwItemClass.NOVEL - theItem.setImportStatus("New") - assert theItem.itemStatus == "New" - theItem.setImportStatus("Minor") - assert theItem.itemStatus == "New" - theItem.setImportStatus("Note") - assert theItem.itemStatus == "Note" - theItem.setImportStatus("Draft") - assert theItem.itemStatus == "Draft" + for key in constData.statusKeys: + theItem.setStatus(key) + assert theItem.itemImport == constData.importKeys[3] # Should not change + assert theItem.itemStatus == key # Expanded theItem.setExpanded(8) @@ -354,9 +338,10 @@ def testCoreItem_LayoutSetter(mockGUI): @pytest.mark.core -def testCoreItem_XMLPackUnpack(mockGUI, caplog): +def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): """Test packing and unpacking XML objects for the NWItem class. """ + random.seed(42) theProject = NWProject(mockGUI) nwXML = etree.Element("novelWriterXML") @@ -370,7 +355,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): theItem.setName("A Name") theItem.setClass("NOVEL") theItem.setType("FILE") - theItem.setStatus("Main") + theItem.setImport(constData.importKeys[3]) theItem.setLayout("NOTE") theItem.setExported(False) theItem.setParaCount(3) @@ -385,9 +370,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): b'' b'' - b'A Name' + b'A Name
' b'
' - ) + ) % bytes(constData.importKeys[3], encoding="utf8") # Unpack theItem = NWItem(theProject) @@ -403,6 +388,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): assert theItem.itemClass == nwItemClass.NOVEL assert theItem.itemType == nwItemType.FILE assert theItem.itemLayout == nwItemLayout.NOTE + assert theItem.itemStatus == constData.statusKeys[0] # Was None, should now be default + assert theItem.itemImport == constData.importKeys[3] # Folder # ====== @@ -414,7 +401,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): theItem.setName("A Name") theItem.setClass("NOVEL") theItem.setType("FOLDER") - theItem.setStatus("Main") + theItem.setStatus(constData.statusKeys[1]) theItem.setLayout("NOTE") theItem.setExpanded(True) theItem.setExported(False) @@ -429,10 +416,10 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( b'' b'A Name' + b'class="NOVEL">A Name' b'' b'' - ) + ) % bytes(constData.statusKeys[1], encoding="utf8") # Unpack theItem = NWItem(theProject) @@ -449,6 +436,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): assert theItem.itemClass == nwItemClass.NOVEL assert theItem.itemType == nwItemType.FOLDER assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + assert theItem.itemStatus == constData.statusKeys[1] + assert theItem.itemImport == constData.importKeys[0] # Was None, should now be default # Errors # ====== diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 311fca8c..b98b05f4 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -19,8 +19,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import os +import pytest +import random from shutil import copyfile from zipfile import ZipFile @@ -44,6 +45,7 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI): testFile = os.path.join(outDir, "coreProject_NewMinimal_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_NewMinimal_nwProject.nwx") + random.seed(42) theProject = NWProject(mockGUI) theProject.projTree.setSeed(42) @@ -112,6 +114,7 @@ def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI): "numScenes": 3, "chFolders": True, } + random.seed(42) theProject = NWProject(mockGUI) theProject.projTree.setSeed(42) @@ -154,6 +157,7 @@ def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI): "numScenes": 6, "chFolders": True, } + random.seed(42) theProject = NWProject(mockGUI) theProject.projTree.setSeed(42) @@ -262,6 +266,7 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI): testFile = os.path.join(outDir, "coreProject_NewRoot_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx") + random.seed(42) theProject = NWProject(mockGUI) theProject.projTree.setSeed(42) @@ -299,6 +304,7 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI): testFile = os.path.join(outDir, "coreProject_NewFile_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx") + random.seed(42) theProject = NWProject(mockGUI) theProject.projTree.setSeed(42) @@ -770,9 +776,10 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): # Spell language theProject.projChanged = False - assert theProject.setSpellLang(None) assert theProject.projSpell is None - assert theProject.setSpellLang("None") + assert theProject.setSpellLang(None) is False + assert theProject.projSpell is None + assert theProject.setSpellLang("None") is False # Should be interpreded as None assert theProject.projSpell is None assert theProject.setSpellLang("en_GB") assert theProject.projSpell == "en_GB" @@ -827,55 +834,55 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): assert theProject.setTreeOrder(oldOrder) assert theProject.projTree.handles() == oldOrder - # Change status - theProject.projTree["a35baf2e93843"].setStatus("Finished") - theProject.projTree["a6d311a93600a"].setStatus("Draft") - theProject.projTree["f5ab3e30151e1"].setStatus("Note") - theProject.projTree["8c659a11cd429"].setStatus("Finished") - newList = [ - ("New", 1, 1, 1, "New"), - ("Draft", 2, 2, 2, "Note"), # These are swapped - ("Note", 3, 3, 3, "Draft"), # These are swapped - ("Edited", 4, 4, 4, "Finished"), # Renamed - ("Finished", 5, 5, 5, None), # New, with reused name - ] - assert theProject.setStatusColours(newList) - assert theProject.statusItems._theLabels == [ - "New", "Draft", "Note", "Edited", "Finished" - ] - assert theProject.statusItems._theColours == [ - (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5) - ] - assert theProject.projTree["a35baf2e93843"].itemStatus == "Edited" # Renamed - assert theProject.projTree["a6d311a93600a"].itemStatus == "Note" # Swapped - assert theProject.projTree["f5ab3e30151e1"].itemStatus == "Draft" # Swapped - assert theProject.projTree["8c659a11cd429"].itemStatus == "Edited" # Renamed + # # Change status + # theProject.projTree["a35baf2e93843"].setStatus("Finished") + # theProject.projTree["a6d311a93600a"].setStatus("Draft") + # theProject.projTree["f5ab3e30151e1"].setStatus("Note") + # theProject.projTree["8c659a11cd429"].setStatus("Finished") + # newList = [ + # ("New", 1, 1, 1, "New"), + # ("Draft", 2, 2, 2, "Note"), # These are swapped + # ("Note", 3, 3, 3, "Draft"), # These are swapped + # ("Edited", 4, 4, 4, "Finished"), # Renamed + # ("Finished", 5, 5, 5, None), # New, with reused name + # ] + # assert theProject.setStatusColours(newList, []) + # assert theProject.statusItems._theLabels == [ + # "New", "Draft", "Note", "Edited", "Finished" + # ] + # assert theProject.statusItems._theColours == [ + # (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5) + # ] + # assert theProject.projTree["a35baf2e93843"].itemStatus == "Edited" # Renamed + # assert theProject.projTree["a6d311a93600a"].itemStatus == "Note" # Swapped + # assert theProject.projTree["f5ab3e30151e1"].itemStatus == "Draft" # Swapped + # assert theProject.projTree["8c659a11cd429"].itemStatus == "Edited" # Renamed - # Change importance - fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3") - theProject.projTree[fHandle].setImport("Main") - newList = [ - ("New", 1, 1, 1, "New"), - ("Minor", 2, 2, 2, "Minor"), - ("Major", 3, 3, 3, "Major"), - ("Min", 4, 4, 4, "Main"), - ("Max", 5, 5, 5, None), - ] - assert theProject.setImportColours(newList) - assert theProject.importItems._theLabels == [ - "New", "Minor", "Major", "Min", "Max" - ] - assert theProject.importItems._theColours == [ - (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5) - ] - assert theProject.projTree[fHandle].itemImport == "Min" + # # Change importance + # fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3") + # theProject.projTree[fHandle].setImport("Main") + # newList = [ + # ("New", 1, 1, 1, "New"), + # ("Minor", 2, 2, 2, "Minor"), + # ("Major", 3, 3, 3, "Major"), + # ("Min", 4, 4, 4, "Main"), + # ("Max", 5, 5, 5, None), + # ] + # assert theProject.setImportColours(newList) + # assert theProject.importItems._theLabels == [ + # "New", "Minor", "Major", "Min", "Max" + # ] + # assert theProject.importItems._theColours == [ + # (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5) + # ] + # assert theProject.projTree[fHandle].itemImport == "Min" - # Check status counts - assert theProject.statusItems._theCounts == [0, 0, 0, 0, 0] - assert theProject.importItems._theCounts == [0, 0, 0, 0, 0] - theProject.countStatus() - assert theProject.statusItems._theCounts == [1, 1, 1, 2, 0] - assert theProject.importItems._theCounts == [3, 0, 0, 1, 0] + # # Check status counts + # assert theProject.statusItems._theCounts == [0, 0, 0, 0, 0] + # assert theProject.importItems._theCounts == [0, 0, 0, 0, 0] + # theProject.countStatus() + # assert theProject.statusItems._theCounts == [1, 1, 1, 2, 0] + # assert theProject.importItems._theCounts == [3, 0, 0, 1, 0] # Session stats theProject.currWCount = 200 diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 79038345..10d72549 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -20,6 +20,7 @@ along with this program. If not, see . """ import pytest +import random from tools import getGuiItem @@ -88,7 +89,7 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj): @pytest.mark.gui -def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj): +def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, constData): """Test the item editor dialog for a novel document. """ # Block message box @@ -96,9 +97,13 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Create Project and Open Document + random.seed(42) nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) - assert nwGUI.openDocument("0e17daca5f3e1") + assert nwGUI.theProject.statusItems.name(constData.statusKeys[0]) == "New" + assert nwGUI.theProject.statusItems.name(constData.statusKeys[1]) == "Note" + + assert nwGUI.openDocument("0e17daca5f3e1") is True # Check that an invalid handle is managed itemEdit = GuiItemEditor(nwGUI, "whatever") @@ -111,7 +116,7 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj): # Check Existing Settings assert itemEdit.editName.text() == "New Scene" - assert itemEdit.editStatus.currentData() == "New" + assert itemEdit.editStatus.currentData() == constData.statusKeys[0] assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT assert itemEdit.editExport.isChecked() is True @@ -125,7 +130,7 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj): # Check New Settings itemEdit._doSave() assert itemEdit.theItem.itemName == "Great Scene" - assert itemEdit.theItem.itemStatus == "Note" + assert itemEdit.theItem.itemStatus == constData.statusKeys[1] assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE assert itemEdit.theItem.isExported is False @@ -141,7 +146,7 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj): @pytest.mark.gui -def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj): +def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData): """Test the item editor dialog for a project note. """ # Block message box @@ -149,8 +154,13 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Create Project and Open Document + random.seed(42) nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) + assert nwGUI.theProject.statusItems.name(constData.statusKeys[0]) == "New" + assert nwGUI.theProject.statusItems.name(constData.statusKeys[1]) == "Note" + assert nwGUI.theProject.importItems.name(constData.importKeys[0]) == "New" + assert nwGUI.theProject.importItems.name(constData.importKeys[1]) == "Minor" # Create Note nwGUI.treeView.clearSelection() @@ -166,7 +176,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj): # Check Existing Settings assert itemEdit.editName.text() == "New File" - assert itemEdit.editStatus.currentData() == "New" + assert itemEdit.editStatus.currentData() == constData.importKeys[0] assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE assert itemEdit.editExport.isChecked() is True @@ -178,8 +188,8 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj): # Check New Settings assert itemEdit.theItem.itemName == "New Character" - assert itemEdit.theItem.itemStatus == "New" - assert itemEdit.theItem.itemImport == "Minor" + assert itemEdit.theItem.itemStatus == constData.statusKeys[0] + assert itemEdit.theItem.itemImport == constData.importKeys[1] assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE assert itemEdit.theItem.isExported is False @@ -191,7 +201,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj): @pytest.mark.gui -def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj): +def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj, constData): """Test the item editor dialog for a folder. """ # Block message box @@ -199,6 +209,7 @@ def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Create Project and Open Document + random.seed(42) nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) @@ -206,9 +217,12 @@ def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj): itemEdit = GuiItemEditor(nwGUI, "31489056e0916") itemEdit.show() + assert nwGUI.theProject.statusItems.name(constData.statusKeys[0]) == "New" + assert nwGUI.theProject.statusItems.name(constData.statusKeys[1]) == "Note" + # Check Existing Settings assert itemEdit.editName.text() == "New Chapter" - assert itemEdit.editStatus.currentData() == "New" + assert itemEdit.editStatus.currentData() == constData.statusKeys[0] assert itemEdit.editLayout.currentData() == nwItemLayout.NO_LAYOUT assert itemEdit.editExport.isChecked() is False @@ -222,7 +236,7 @@ def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj): # Check New Settings itemEdit._doSave() assert itemEdit.theItem.itemName == "Chapter One" - assert itemEdit.theItem.itemStatus == "Note" + assert itemEdit.theItem.itemStatus == constData.statusKeys[1] assert itemEdit.theItem.itemLayout == nwItemLayout.NO_LAYOUT assert itemEdit.theItem.isExported is False diff --git a/tests/test_dialogs/test_dlg_projload.py b/tests/test_dialogs/test_dlg_projload.py index 195ca26b..6edb7a0d 100644 --- a/tests/test_dialogs/test_dlg_projload.py +++ b/tests/test_dialogs/test_dlg_projload.py @@ -42,7 +42,8 @@ def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, nwMinimal): """Test the load project wizard. """ # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) assert nwGUI.openProject(nwMinimal) assert nwGUI.closeProject() diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index ec316bea..f2aa8319 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -19,8 +19,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import os +import pytest +import random from shutil import copyfile from tools import cmpFiles, getGuiItem @@ -39,7 +40,9 @@ stepDelay = 20 @pytest.mark.gui -def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir): +def testDlgProjSettings_Dialog( + qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir, constData +): """Test the full project settings dialog. """ projFile = os.path.join(fncProj, "nwProject.nwx") @@ -55,6 +58,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi assert getGuiItem("GuiProjectSettings") is None # Create new project + random.seed(42) nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) nwGUI.mainConf.backupPath = fncDir @@ -111,7 +115,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi projEdit._tabBox.setCurrentWidget(projEdit.tabStatus) assert projEdit.tabStatus.colChanged is False - assert projEdit.tabStatus.getNewList() is None + assert projEdit.tabStatus.getNewList() == ([], []) assert projEdit.tabStatus.listBox.topLevelItemCount() == 4 # Fake drag'n'drop should change changed status @@ -150,12 +154,29 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi qtbot.wait(stepDelay) assert projEdit.tabStatus.colChanged is True - assert projEdit.tabStatus.getNewList() == [ - ("New", 100, 100, 100, "New"), - ("Note", 200, 50, 0, "Note"), - ("Finished", 50, 200, 0, "Finished"), - ("Final", 20, 30, 40, None) - ] + assert projEdit.tabStatus.getNewList() == ( + [ + { + "key": constData.statusKeys[0], + "name": "New", + "cols": (100, 100, 100) + }, { + "key": constData.statusKeys[1], + "name": "Note", + "cols": (200, 50, 0) + }, { + "key": constData.statusKeys[3], + "name": "Finished", + "cols": (50, 200, 0) + }, { + "key": None, + "name": "Final", + "cols": (20, 30, 40) + } + ], [ + constData.statusKeys[2] # Deleted item + ] + ) # Importance Tab # ============== diff --git a/tests/test_dialogs/test_dlg_wordlist.py b/tests/test_dialogs/test_dlg_wordlist.py index 3ca5a0ee..d9993d4d 100644 --- a/tests/test_dialogs/test_dlg_wordlist.py +++ b/tests/test_dialogs/test_dlg_wordlist.py @@ -40,6 +40,7 @@ stepDelay = 20 def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, nwMinimal): """test the word list editor. """ + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(GuiWordList, "exec_", lambda *a: None) diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 8057ca41..eaa1d2ba 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -43,6 +43,7 @@ def testGuiEditor_Init(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) # Open project assert nwGUI.openProject(nwMinimal) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 34cfaffa..fb36b3e2 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -19,8 +19,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import os +import random +import pytest from shutil import copyfile from tools import cmpFiles @@ -139,6 +140,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) # Create new, save, close project + random.seed(42) nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) assert nwGUI.saveProject() diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 6adac08e..da7fc414 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -35,6 +35,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir): """Test the theme and icon classes. """ # Block message box + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) From d7a8cb1537f70929b360a1b2b06355b19001b4d1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 15:38:13 +0200 Subject: [PATCH 022/179] Rewrite NWStatus class tests --- novelwriter/core/status.py | 10 + tests/test_core/test_core_status.py | 344 +++++++++++++++++++++------- 2 files changed, 268 insertions(+), 86 deletions(-) diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index a3792764..4e9317f2 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -106,6 +106,13 @@ class NWStatus(): del self._reverse[self._store[key]["name"]] del self._store[key] + keys = list(self._store.keys()) + if key == self._default: + if len(keys) > 0: + self._default = keys[0] + else: + self._default = None + return True def check(self, value): @@ -242,6 +249,9 @@ class NWStatus(): # Iterator Bits ## + def __len__(self): + return len(self._store) + def __getitem__(self, key): return self._store[key] diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index a7a2d55e..611f8327 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -20,6 +20,7 @@ along with this program. If not, see . """ import pytest +import random from lxml import etree @@ -29,103 +30,268 @@ from novelwriter.core.status import NWStatus @pytest.mark.core -def testCoreStatus_Entries(): - """Test all the simple setters for the NWItem class. +def testCoreStatus_Internal(constData): + """Test all the internal functions of the NWStatus class. """ - theStatus = NWStatus() + random.seed(42) + theStatus = NWStatus(NWStatus.STATUS) + theImport = NWStatus(NWStatus.IMPORT) - # Add entries - theStatus.addEntry("New", (100, 100, 100)) - theStatus.addEntry("Minor", (200, 50, 0)) - theStatus.addEntry("Major", (200, 150, 0)) - theStatus.addEntry("Main", (50, 200, 0)) + with pytest.raises(Exception): + NWStatus(999) - assert theStatus._theLabels == ["New", "Minor", "Major", "Main"] - assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)] - assert theStatus._theCounts == [0, 0, 0, 0] - assert theStatus._theMap["New"] == 0 - assert theStatus._theMap["Minor"] == 1 - assert theStatus._theMap["Major"] == 2 - assert theStatus._theMap["Main"] == 3 - assert theStatus._theLength == 4 + # Generate Key + # ============ - # Lookups - assert theStatus._getIndex(None) is None - assert theStatus._getIndex("stuff") is None - assert theStatus._getIndex("Main") == 3 + assert theStatus._newKey() == constData.statusKeys[0] + assert theStatus._newKey() == constData.statusKeys[1] - # Checks - assert theStatus.checkEntry(123) == "New" - assert theStatus.checkEntry("Stuff") == "New" - assert theStatus.checkEntry("New ") == "New" - assert theStatus.checkEntry(" Main ") == "Main" + # Key collision, should move to key 3 + theStatus.write(constData.statusKeys[2], "Crash", (0, 0, 0)) + assert theStatus._newKey() == constData.statusKeys[3] - # Icons - assert isinstance(theStatus.getIcon("Stuff"), QIcon) - assert isinstance(theStatus.getIcon("New"), QIcon) + assert theImport._newKey() == constData.importKeys[0] + assert theImport._newKey() == constData.importKeys[1] - # Set new list - newList = [ - ("New", 1, 1, 1, "New"), - ("Minor", 2, 2, 2, "Minor"), - ("Major", 3, 3, 3, "Major"), - ("Min", 4, 4, 4, "Main"), - ("Max", 5, 5, 5, None), - ] - assert theStatus.setNewEntries(None) == {} - assert theStatus.setNewEntries(newList) == {"Main": "Min"} + # Key collision, should move to key 3 + theImport.write(constData.importKeys[2], "Crash", (0, 0, 0)) + assert theImport._newKey() == constData.importKeys[3] - assert theStatus._theLabels == ["New", "Minor", "Major", "Min", "Max"] - assert theStatus._theColours == [(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)] - assert theStatus._theCounts == [0, 0, 0, 0, 0] - assert theStatus._theMap["New"] == 0 - assert theStatus._theMap["Minor"] == 1 - assert theStatus._theMap["Major"] == 2 - assert theStatus._theMap["Min"] == 3 - assert theStatus._theMap["Max"] == 4 - assert theStatus._theLength == 5 + # Check Key + # ========= - # Add counts - countTo = [3, 5, 7, 9, 11] - for i, n in enumerate(countTo): - for _ in range(n): - theStatus.countEntry(theStatus._theLabels[i]) - assert theStatus._theCounts == countTo + assert theStatus._isKey(None) is False # Not a string + assert theStatus._isKey("s00000") is False # Too short + assert theStatus._isKey("s000000") is True # Correct length + assert theStatus._isKey("s0000000") is False # Too long + assert theStatus._isKey("i000000") is False # Wrong type + assert theStatus._isKey("q000000") is False # Wrong type + assert theStatus._isKey("s12345H") is False # Not a hex value + assert theStatus._isKey("s12345F") is False # Not a lower case hex value + assert theStatus._isKey("s12345f") is True # Valid hex value + + assert theImport._isKey(None) is False # Not a string + assert theImport._isKey("i00000") is False # Too short + assert theImport._isKey("i000000") is True # Correct length + assert theImport._isKey("i0000000") is False # Too long + assert theImport._isKey("s000000") is False # Wrong type + assert theImport._isKey("q000000") is False # Wrong type + assert theImport._isKey("i12345H") is False # Not a hex value + assert theImport._isKey("i12345F") is False # Not a lower case hex value + assert theImport._isKey("i12345f") is True # Valid hex value + +# END Test testCoreStatus_Internal + + +@pytest.mark.core +def testCoreStatus_Iterator(constData): + """Test the iterator functions of the NWStatus class. + """ + random.seed(42) + theStatus = NWStatus(NWStatus.STATUS) + theStatus.write(None, "New", (100, 100, 100)) + theStatus.write(None, "Note", (200, 50, 0)) + theStatus.write(None, "Draft", (200, 150, 0)) + theStatus.write(None, "Finished", (50, 200, 0)) + + # Direct access + entry = theStatus[constData.statusKeys[0]] + assert entry["cols"] == (100, 100, 100) + assert entry["name"] == "New" + assert entry["count"] == 0 + assert isinstance(entry["icon"], QIcon) # Iterate - for i, (sA, sB, sC, sD) in enumerate(theStatus): - assert sA == theStatus._theLabels[i] - assert sB == theStatus._theColours[i] - assert sC == theStatus._theCounts[i] - assert sD == theStatus._theIcons[i] + entries = list(theStatus) + assert len(entries) == 4 + assert len(theStatus) == 4 - sA, sB, sC, sD = theStatus[9] - assert sA is None - assert sB is None - assert sC is None - assert isinstance(sD, QIcon) + # Keys + assert list(theStatus.keys()) == constData.statusKeys + + # Items + for index, (key, entry) in enumerate(theStatus.items()): + assert key == constData.statusKeys[index] + assert "cols" in entry + assert "name" in entry + assert "count" in entry + assert "icon" in entry + + # Valuse + for entry in theStatus.values(): + assert "cols" in entry + assert "name" in entry + assert "count" in entry + assert "icon" in entry + +# END Test testCoreStatus_Iterator + + +@pytest.mark.core +def testCoreStatus_Entries(constData): + """Test all the simple setters for the NWStatus class. + """ + random.seed(42) + theStatus = NWStatus(NWStatus.STATUS) + + # Write + # ===== + + # Have a key + theStatus.write(constData.statusKeys[0], "Entry 1", (200, 100, 50)) + assert theStatus[constData.statusKeys[0]]["name"] == "Entry 1" + assert theStatus[constData.statusKeys[0]]["cols"] == (200, 100, 50) + + # Don't have a key + theStatus.write(None, "Entry 2", (210, 110, 60)) + assert theStatus[constData.statusKeys[1]]["name"] == "Entry 2" + assert theStatus[constData.statusKeys[1]]["cols"] == (210, 110, 60) + + # Wrong colour spec + theStatus.write(None, "Entry 3", "what?") + assert theStatus[constData.statusKeys[2]]["name"] == "Entry 3" + assert theStatus[constData.statusKeys[2]]["cols"] == (100, 100, 100) + + # Wrong colour count + theStatus.write(None, "Entry 4", (10, 20)) + assert theStatus[constData.statusKeys[3]]["name"] == "Entry 4" + assert theStatus[constData.statusKeys[3]]["cols"] == (100, 100, 100) + + # Check reverse map + assert theStatus._reverse == { + "Entry 1": constData.statusKeys[0], + "Entry 2": constData.statusKeys[1], + "Entry 3": constData.statusKeys[2], + "Entry 4": constData.statusKeys[3], + } + + # Check + # ===== + + # Normal lookup + for key in constData.statusKeys: + assert theStatus.check(key) == key + + # Reverse map lookup + assert theStatus.check("Entry 1") == constData.statusKeys[0] + assert theStatus.check("Entry 2") == constData.statusKeys[1] + assert theStatus.check("Entry 3") == constData.statusKeys[2] + assert theStatus.check("Entry 4") == constData.statusKeys[3] + + # Non-existing name + assert theStatus.check("Entry 5") == constData.statusKeys[0] + + # Name Access + # =========== + + assert theStatus.name(constData.statusKeys[0]) == "Entry 1" + assert theStatus.name(constData.statusKeys[1]) == "Entry 2" + assert theStatus.name(constData.statusKeys[2]) == "Entry 3" + assert theStatus.name(constData.statusKeys[3]) == "Entry 4" + assert theStatus.name("blablabla") == "Entry 1" + + # Colour Access + # ============= + + assert theStatus.cols(constData.statusKeys[0]) == (200, 100, 50) + assert theStatus.cols(constData.statusKeys[1]) == (210, 110, 60) + assert theStatus.cols(constData.statusKeys[2]) == (100, 100, 100) + assert theStatus.cols(constData.statusKeys[3]) == (100, 100, 100) + assert theStatus.cols("blablabla") == (200, 100, 50) + + # Icon Access + # =========== + + assert isinstance(theStatus.icon(constData.statusKeys[0]), QIcon) + assert isinstance(theStatus.icon(constData.statusKeys[1]), QIcon) + assert isinstance(theStatus.icon(constData.statusKeys[2]), QIcon) + assert isinstance(theStatus.icon(constData.statusKeys[3]), QIcon) + assert isinstance(theStatus.icon("blablabla"), QIcon) + + # Increment and Count Access + # ========================== + + countTo = [3, 5, 7, 9] + for i, n in enumerate(countTo): + for _ in range(n): + theStatus.increment(constData.statusKeys[i]) + + assert theStatus.count(constData.statusKeys[0]) == countTo[0] + assert theStatus.count(constData.statusKeys[1]) == countTo[1] + assert theStatus.count(constData.statusKeys[2]) == countTo[2] + assert theStatus.count(constData.statusKeys[3]) == countTo[3] + assert theStatus.count("blablabla") == countTo[0] - # Clear counts theStatus.resetCounts() - assert theStatus._theCounts == [0, 0, 0, 0, 0] + + assert theStatus.count(constData.statusKeys[0]) == 0 + assert theStatus.count(constData.statusKeys[1]) == 0 + assert theStatus.count(constData.statusKeys[2]) == 0 + assert theStatus.count(constData.statusKeys[3]) == 0 + + # Default + # ======= + + default = theStatus._default + theStatus._default = None + + assert theStatus.check("Entry 5") == "" + assert theStatus.name("blablabla") == "" + assert theStatus.cols("blablabla") == (100, 100, 100) + assert theStatus.count("blablabla") == 0 + assert isinstance(theStatus.icon("blablabla"), QIcon) + + theStatus._default = default + + # Remove + # ====== + + # Non-existing entry + assert theStatus.remove("blablabla") is False + + # Non-zero entry + theStatus.increment(constData.statusKeys[3]) + assert theStatus.remove(constData.statusKeys[3]) is False + + # Delete last entry + theStatus.resetCounts() + lastName = theStatus.name(constData.statusKeys[3]) + assert lastName == "Entry 4" + assert theStatus.remove(constData.statusKeys[3]) is True + assert theStatus.check(constData.statusKeys[3]) == theStatus._default + assert theStatus.check(lastName) == theStatus._default + + # Delete default entry, Entry 2 is new default + firstName = theStatus.name(theStatus._default) + assert firstName == "Entry 1" + assert theStatus.remove(theStatus._default) is True + assert theStatus.name(firstName) == "Entry 2" + + # Remove remaining entries + assert theStatus.remove(constData.statusKeys[1]) is True + assert theStatus.remove(constData.statusKeys[2]) is True + + assert len(theStatus) == 0 + assert theStatus._default is None # END Test testCoreStatus_Entries @pytest.mark.core -def testCoreStatus_XMLPackUnpack(): - """Test all the simple setters for the NWItem class. +def testCoreStatus_XMLPackUnpack(constData): + """Test all the XML pack/unpack of the NWStatus class. """ - theStatus = NWStatus() - theStatus.addEntry("New", (100, 100, 100)) - theStatus.addEntry("Minor", (200, 50, 0)) - theStatus.addEntry("Major", (200, 150, 0)) - theStatus.addEntry("Main", (50, 200, 0)) + random.seed(42) + theStatus = NWStatus(NWStatus.STATUS) + theStatus.write(None, "New", (100, 100, 100)) + theStatus.write(None, "Note", (200, 50, 0)) + theStatus.write(None, "Draft", (200, 150, 0)) + theStatus.write(None, "Finished", (50, 200, 0)) countTo = [3, 5, 7, 9] for i, n in enumerate(countTo): for _ in range(n): - theStatus.countEntry(theStatus._theLabels[i]) + theStatus.increment(constData.statusKeys[i]) nwXML = etree.Element("novelWriterXML") @@ -134,23 +300,29 @@ def testCoreStatus_XMLPackUnpack(): theStatus.packXML(xStatus) assert etree.tostring(xStatus, pretty_print=False, encoding="utf-8") == ( b'' - b'New' - b'Minor' - b'Major' - b'Main' + b'New' + b'Note' + b'Draft' + b'Finished' b'' ) # Unpack - theStatus = NWStatus() + theStatus = NWStatus(NWStatus.STATUS) assert theStatus.unpackXML(xStatus) - assert theStatus._theLabels == ["New", "Minor", "Major", "Main"] - assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)] - assert theStatus._theCounts == [0, 0, 0, 0] - assert theStatus._theMap["New"] == 0 - assert theStatus._theMap["Minor"] == 1 - assert theStatus._theMap["Major"] == 2 - assert theStatus._theMap["Main"] == 3 - assert theStatus._theLength == 4 + assert len(theStatus._store) == 4 + assert list(theStatus._store.keys()) == constData.statusKeys + assert theStatus._store[constData.statusKeys[0]]["name"] == "New" + assert theStatus._store[constData.statusKeys[1]]["name"] == "Note" + assert theStatus._store[constData.statusKeys[2]]["name"] == "Draft" + assert theStatus._store[constData.statusKeys[3]]["name"] == "Finished" + assert theStatus._store[constData.statusKeys[0]]["cols"] == (100, 100, 100) + assert theStatus._store[constData.statusKeys[1]]["cols"] == (200, 50, 0) + assert theStatus._store[constData.statusKeys[2]]["cols"] == (200, 150, 0) + assert theStatus._store[constData.statusKeys[3]]["cols"] == (50, 200, 0) + assert theStatus._store[constData.statusKeys[0]]["count"] == countTo[0] + assert theStatus._store[constData.statusKeys[1]]["count"] == countTo[1] + assert theStatus._store[constData.statusKeys[2]]["count"] == countTo[2] + assert theStatus._store[constData.statusKeys[3]]["count"] == countTo[3] # END Test testCoreStatus_XMLPackUnpack From c5f92edaa845d547b7b121fd1fd017a9eee8aceb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 16:27:46 +0200 Subject: [PATCH 023/179] Update the NWProject class test for status and import labels --- tests/test_core/test_core_project.py | 197 ++++++++++++++++++--------- 1 file changed, 131 insertions(+), 66 deletions(-) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index b98b05f4..d4213442 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -686,13 +686,127 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): @pytest.mark.core -def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): +def testCoreProject_StatusImport(mockGUI, fncDir, constData): + """Test the status and importance flag handling. + """ + theProject = NWProject(mockGUI) + random.seed(42) + theProject.projTree.setSeed(42) + assert theProject.newProject({"projPath": fncDir}) is True + + # Change Status + # ============= + + theProject.projTree["44cb730c42048"].setStatus("Finished") + theProject.projTree["71ee45a3c0db9"].setStatus("Draft") + theProject.projTree["811786ad1ae74"].setStatus("Note") + theProject.projTree["25fc0e7096fc6"].setStatus("Finished") + + assert theProject.projTree["44cb730c42048"].itemStatus == constData.statusKeys[3] + assert theProject.projTree["71ee45a3c0db9"].itemStatus == constData.statusKeys[2] + assert theProject.projTree["811786ad1ae74"].itemStatus == constData.statusKeys[1] + assert theProject.projTree["25fc0e7096fc6"].itemStatus == constData.statusKeys[3] + + newList = [ + {"key": constData.statusKeys[0], "name": "New", "cols": (1, 1, 1)}, + {"key": constData.statusKeys[1], "name": "Draft", "cols": (2, 2, 2)}, # These are swapped + {"key": constData.statusKeys[2], "name": "Note", "cols": (3, 3, 3)}, # These are swapped + {"key": constData.statusKeys[3], "name": "Edited", "cols": (4, 4, 4)}, # Renamed + {"key": None, "name": "Finished", "cols": (5, 5, 5)}, # New, reused name + ] + assert theProject.setStatusColours(None, None) is False + assert theProject.setStatusColours([], []) is False + assert theProject.setStatusColours(newList, []) is True + + assert theProject.statusItems.name(constData.statusKeys[0]) == "New" + assert theProject.statusItems.name(constData.statusKeys[1]) == "Draft" + assert theProject.statusItems.name(constData.statusKeys[2]) == "Note" + assert theProject.statusItems.name(constData.statusKeys[3]) == "Edited" + assert theProject.statusItems.cols(constData.statusKeys[0]) == (1, 1, 1) + assert theProject.statusItems.cols(constData.statusKeys[1]) == (2, 2, 2) + assert theProject.statusItems.cols(constData.statusKeys[2]) == (3, 3, 3) + assert theProject.statusItems.cols(constData.statusKeys[3]) == (4, 4, 4) + + # Check the new entry + lastKey = theProject.statusItems.check("Finished") + assert lastKey == "sbc8960" + assert theProject.statusItems.name(lastKey) == "Finished" + assert theProject.statusItems.cols(lastKey) == (5, 5, 5) + + # Delete last entry + assert theProject.setStatusColours([], [lastKey]) is True + assert theProject.statusItems.name(lastKey) == "New" + + # Change Importance + # ================= + + fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "73475cb40a568") + theProject.projTree[fHandle].setImport("Main") + + assert theProject.projTree[fHandle].itemImport == constData.importKeys[3] + newList = [ + {"key": constData.importKeys[0], "name": "New", "cols": (1, 1, 1)}, + {"key": constData.importKeys[1], "name": "Minor", "cols": (2, 2, 2)}, + {"key": constData.importKeys[2], "name": "Major", "cols": (3, 3, 3)}, + {"key": constData.importKeys[3], "name": "Min", "cols": (4, 4, 4)}, + {"key": None, "name": "Max", "cols": (5, 5, 5)}, + ] + assert theProject.setImportColours(None, None) is False + assert theProject.setImportColours([], []) is False + assert theProject.setImportColours(newList, []) is True + + assert theProject.importItems.name(constData.importKeys[0]) == "New" + assert theProject.importItems.name(constData.importKeys[1]) == "Minor" + assert theProject.importItems.name(constData.importKeys[2]) == "Major" + assert theProject.importItems.name(constData.importKeys[3]) == "Min" + assert theProject.importItems.cols(constData.importKeys[0]) == (1, 1, 1) + assert theProject.importItems.cols(constData.importKeys[1]) == (2, 2, 2) + assert theProject.importItems.cols(constData.importKeys[2]) == (3, 3, 3) + assert theProject.importItems.cols(constData.importKeys[3]) == (4, 4, 4) + + # Check the new entry + lastKey = theProject.importItems.check("Max") + assert lastKey == "i1a3d1f" + assert theProject.importItems.name(lastKey) == "Max" + assert theProject.importItems.cols(lastKey) == (5, 5, 5) + + # Delete last entry + assert theProject.setImportColours([], [lastKey]) is True + assert theProject.importItems.name(lastKey) == "New" + + # Delete Status/Import + # ==================== + + theProject.statusItems.resetCounts() + for key in list(theProject.statusItems.keys()): + assert theProject.statusItems.remove(key) is True + + theProject.importItems.resetCounts() + for key in list(theProject.importItems.keys()): + assert theProject.importItems.remove(key) is True + + assert len(theProject.statusItems) == 0 + assert len(theProject.importItems) == 0 + assert theProject.saveProject() is True + assert theProject.closeProject() is True + + # This should restore the default status/import labels + random.seed(42) + assert theProject.openProject(fncDir) is True + assert theProject.saveProject() is True + assert list(theProject.statusItems.keys()) == constData.statusKeys + assert list(theProject.importItems.keys()) == constData.importKeys + +# END Test testCoreProject_StatusImport + + +@pytest.mark.core +def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir): """Test other project class methods and functions. """ theProject = NWProject(mockGUI) theProject.projTree.setSeed(42) - assert theProject.openProject(nwMinimal) - assert theProject.projPath == nwMinimal + assert theProject.newProject({"projPath": fncDir}) is True # Setting project path assert theProject.setProjectPath(None) @@ -703,16 +817,16 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): assert theProject.projPath == os.path.expanduser("~") # Create a new folder and populate it - projPath = os.path.join(nwMinimal, "mock1") + projPath = os.path.join(fncDir, "mock1") assert theProject.setProjectPath(projPath, newProject=True) # Make os.mkdir fail monkeypatch.setattr("os.mkdir", causeOSError) - projPath = os.path.join(nwMinimal, "mock2") + projPath = os.path.join(fncDir, "mock2") assert not theProject.setProjectPath(projPath, newProject=True) # Set back - assert theProject.setProjectPath(nwMinimal) + assert theProject.setProjectPath(fncDir) # Project Name assert theProject.setProjectName(" A Name ") @@ -750,9 +864,10 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): # Trash folder # Should create on first call, and just returned on later calls - assert theProject.projTree["73475cb40a568"] is None - assert theProject.trashFolder() == "73475cb40a568" - assert theProject.trashFolder() == "73475cb40a568" + hTrash = "1a6562590ef19" + assert theProject.projTree[hTrash] is None + assert theProject.trashFolder() == hTrash + assert theProject.trashFolder() == hTrash # Project backup assert theProject.doBackup is True @@ -819,14 +934,14 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): # Change project tree order oldOrder = [ - "a508bb932959c", "a35baf2e93843", "a6d311a93600a", - "f5ab3e30151e1", "8c659a11cd429", "7695ce551d265", - "afb3043c7b2b3", "9d5247ab588e0", "73475cb40a568", + "73475cb40a568", "44cb730c42048", "71ee45a3c0db9", + "811786ad1ae74", "25fc0e7096fc6", "31489056e0916", + "98010bd9270f9", "0e17daca5f3e1", "1a6562590ef19", ] newOrder = [ - "f5ab3e30151e1", "8c659a11cd429", "7695ce551d265", - "a508bb932959c", "a35baf2e93843", "a6d311a93600a", - "afb3043c7b2b3", "9d5247ab588e0", + "811786ad1ae74", "25fc0e7096fc6", "31489056e0916", + "73475cb40a568", "44cb730c42048", "71ee45a3c0db9", + "98010bd9270f9", "0e17daca5f3e1", ] assert theProject.projTree.handles() == oldOrder assert theProject.setTreeOrder(newOrder) @@ -834,56 +949,6 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): assert theProject.setTreeOrder(oldOrder) assert theProject.projTree.handles() == oldOrder - # # Change status - # theProject.projTree["a35baf2e93843"].setStatus("Finished") - # theProject.projTree["a6d311a93600a"].setStatus("Draft") - # theProject.projTree["f5ab3e30151e1"].setStatus("Note") - # theProject.projTree["8c659a11cd429"].setStatus("Finished") - # newList = [ - # ("New", 1, 1, 1, "New"), - # ("Draft", 2, 2, 2, "Note"), # These are swapped - # ("Note", 3, 3, 3, "Draft"), # These are swapped - # ("Edited", 4, 4, 4, "Finished"), # Renamed - # ("Finished", 5, 5, 5, None), # New, with reused name - # ] - # assert theProject.setStatusColours(newList, []) - # assert theProject.statusItems._theLabels == [ - # "New", "Draft", "Note", "Edited", "Finished" - # ] - # assert theProject.statusItems._theColours == [ - # (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5) - # ] - # assert theProject.projTree["a35baf2e93843"].itemStatus == "Edited" # Renamed - # assert theProject.projTree["a6d311a93600a"].itemStatus == "Note" # Swapped - # assert theProject.projTree["f5ab3e30151e1"].itemStatus == "Draft" # Swapped - # assert theProject.projTree["8c659a11cd429"].itemStatus == "Edited" # Renamed - - # # Change importance - # fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3") - # theProject.projTree[fHandle].setImport("Main") - # newList = [ - # ("New", 1, 1, 1, "New"), - # ("Minor", 2, 2, 2, "Minor"), - # ("Major", 3, 3, 3, "Major"), - # ("Min", 4, 4, 4, "Main"), - # ("Max", 5, 5, 5, None), - # ] - # assert theProject.setImportColours(newList) - # assert theProject.importItems._theLabels == [ - # "New", "Minor", "Major", "Min", "Max" - # ] - # assert theProject.importItems._theColours == [ - # (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5) - # ] - # assert theProject.projTree[fHandle].itemImport == "Min" - - # # Check status counts - # assert theProject.statusItems._theCounts == [0, 0, 0, 0, 0] - # assert theProject.importItems._theCounts == [0, 0, 0, 0, 0] - # theProject.countStatus() - # assert theProject.statusItems._theCounts == [1, 1, 1, 2, 0] - # assert theProject.importItems._theCounts == [3, 0, 0, 1, 0] - # Session stats theProject.currWCount = 200 theProject.lastWCount = 100 @@ -897,7 +962,7 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir): assert not theProject._appendSessionStats(idleTime=0) # Write entry - assert theProject.projMeta == os.path.join(nwMinimal, "meta") + assert theProject.projMeta == os.path.join(fncDir, "meta") statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS) theProject.projOpened = 1600002000 From 048e5faa1ff6c44be7d85ad57bbd5b807c9de1e4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 16:44:39 +0200 Subject: [PATCH 024/179] Also simplify item labels, due to XML limitations --- novelwriter/core/item.py | 4 ++-- tests/test_core/test_core_item.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index d56d60f2..2672a196 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -29,7 +29,7 @@ from lxml import etree from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.common import ( - checkInt, isHandle, isItemClass, isItemLayout, isItemType + checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified ) from novelwriter.constants import nwLabels, nwLists, trConst @@ -305,7 +305,7 @@ class NWItem(): """Set the item name. """ if isinstance(theName, str): - self._name = theName.strip() + self._name = simplified(theName) else: self._name = "" return diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index eb1425b3..3adda4de 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -44,6 +44,8 @@ def testCoreItem_Setters(mockGUI, constData): assert theItem.itemName == "A Name" theItem.setName("\t A Name ") assert theItem.itemName == "A Name" + theItem.setName("\t A\t\u2009\u202f\u2002\u2003\u2028\u2029Name ") + assert theItem.itemName == "A Name" theItem.setName(123) assert theItem.itemName == "" From 72c5a9ee92fe866ffafa39893f28d45e7ae508ea Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 16:47:57 +0200 Subject: [PATCH 025/179] Rename some internal variables in the NWItem class --- novelwriter/core/item.py | 102 +++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 2672a196..f9c1921d 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -301,142 +301,142 @@ class NWItem(): # Set Item Values ## - def setName(self, theName): + def setName(self, name): """Set the item name. """ - if isinstance(theName, str): - self._name = simplified(theName) + if isinstance(name, str): + self._name = simplified(name) else: self._name = "" return - def setHandle(self, theHandle): + def setHandle(self, tHandle): """Set the item handle, and ensure it is valid. """ - if isHandle(theHandle): - self._handle = theHandle + if isHandle(tHandle): + self._handle = tHandle else: self._handle = None return - def setParent(self, theParent): + def setParent(self, pHandle): """Set the parent handle, and ensure it is valid. """ - if theParent is None: + if pHandle is None: self._parent = None - elif isHandle(theParent): - self._parent = theParent + elif isHandle(pHandle): + self._parent = pHandle else: self._parent = None return - def setOrder(self, theOrder): + def setOrder(self, order): """Set the item order, and ensure that it is valid. This value is purely a meta value, and not actually used by novelWriter at the moment. """ - self._order = checkInt(theOrder, 0) + self._order = checkInt(order, 0) return - def setType(self, theType): + def setType(self, itemType): """Set the item type from either a proper nwItemType, or set it from a string representing an nwItemType. """ - if isinstance(theType, nwItemType): - self._type = theType - elif isItemType(theType): - self._type = nwItemType[theType] + if isinstance(itemType, nwItemType): + self._type = itemType + elif isItemType(itemType): + self._type = nwItemType[itemType] else: - logger.error("Unrecognised item type '%s'", theType) + logger.error("Unrecognised item type '%s'", itemType) self._type = nwItemType.NO_TYPE return - def setClass(self, theClass): + def setClass(self, itemClass): """Set the item class from either a proper nwItemClass, or set it from a string representing an nwItemClass. """ - if isinstance(theClass, nwItemClass): - self._class = theClass - elif isItemClass(theClass): - self._class = nwItemClass[theClass] + if isinstance(itemClass, nwItemClass): + self._class = itemClass + elif isItemClass(itemClass): + self._class = nwItemClass[itemClass] else: - logger.error("Unrecognised item class '%s'", theClass) + logger.error("Unrecognised item class '%s'", itemClass) self._class = nwItemClass.NO_CLASS return - def setLayout(self, theLayout): + def setLayout(self, itemLayout): """Set the item layout from either a proper nwItemLayout, or set it from a string representing an nwItemLayout. """ - if isinstance(theLayout, nwItemLayout): - self._layout = theLayout - elif isItemLayout(theLayout): - self._layout = nwItemLayout[theLayout] - elif theLayout in nwLists.DEP_LAYOUT: + if isinstance(itemLayout, nwItemLayout): + self._layout = itemLayout + elif isItemLayout(itemLayout): + self._layout = nwItemLayout[itemLayout] + elif itemLayout in nwLists.DEP_LAYOUT: self._layout = nwItemLayout.DOCUMENT else: - logger.error("Unrecognised item layout '%s'", theLayout) + logger.error("Unrecognised item layout '%s'", itemLayout) self._layout = nwItemLayout.NO_LAYOUT return - def setStatus(self, theStatus): + def setStatus(self, itemStatus): """Set the item status by looking it up in the valid status items of the current project. """ - self._status = self.theProject.statusItems.check(theStatus) + self._status = self.theProject.statusItems.check(itemStatus) return - def setImport(self, theImport): + def setImport(self, itemImport): """Set the item importance by looking it up in the valid import items of the current project. """ - self._import = self.theProject.importItems.check(theImport) + self._import = self.theProject.importItems.check(itemImport) return - def setExpanded(self, expState): + def setExpanded(self, state): """Set the expanded status of an item in the project tree. """ - if isinstance(expState, str): - self._expanded = (expState == str(True)) + if isinstance(state, str): + self._expanded = (state == str(True)) else: - self._expanded = (expState is True) + self._expanded = (state is True) return - def setExported(self, expState): + def setExported(self, state): """Set the export flag. """ - if isinstance(expState, str): - self._exported = (expState == str(True)) + if isinstance(state, str): + self._exported = (state == str(True)) else: - self._exported = (expState is True) + self._exported = (state is True) return ## # Set Document Meta Data ## - def setCharCount(self, theCount): + def setCharCount(self, count): """Set the character count, and ensure that it is an integer. """ - self._charCount = max(0, checkInt(theCount, 0)) + self._charCount = max(0, checkInt(count, 0)) return - def setWordCount(self, theCount): + def setWordCount(self, count): """Set the word count, and ensure that it is an integer. """ - self._wordCount = max(0, checkInt(theCount, 0)) + self._wordCount = max(0, checkInt(count, 0)) return - def setParaCount(self, theCount): + def setParaCount(self, count): """Set the paragraph count, and ensure that it is an integer. """ - self._paraCount = max(0, checkInt(theCount, 0)) + self._paraCount = max(0, checkInt(count, 0)) return - def setCursorPos(self, thePosition): + def setCursorPos(self, position): """Set the cursor position, and ensure that it is an integer. """ - self._cursorPos = max(0, checkInt(thePosition, 0)) + self._cursorPos = max(0, checkInt(position, 0)) return def saveInitialCount(self): From 2f547033cd9e7f61fa6bf67deb506610fb8a0ea6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 17:00:08 +0200 Subject: [PATCH 026/179] Simplify all other strings in main project class --- novelwriter/core/project.py | 30 +++++++++++-------- .../guiProjSettings_Dialog_nwProject.nwx | 4 +-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index b00b73cf..a8a9cce3 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -44,7 +44,7 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.error import logException from novelwriter.common import ( checkString, checkBool, checkInt, isHandle, formatTimeStamp, - makeFileNameSafe, hexToInt + makeFileNameSafe, hexToInt, simplified ) from novelwriter.constants import nwLists, trConst, nwFiles, nwLabels @@ -534,14 +534,16 @@ class NWProject(): if xItem.text is None: continue if xItem.tag == "name": - logger.verbose("Working Title: '%s'", xItem.text) - self.projName = xItem.text + self.projName = checkString(simplified(xItem.text), "") + logger.verbose("Working Title: '%s'", self.projName) elif xItem.tag == "title": - logger.verbose("Title is '%s'", xItem.text) - self.bookTitle = xItem.text + self.bookTitle = checkString(simplified(xItem.text), "") + logger.verbose("Title is '%s'", self.bookTitle) elif xItem.tag == "author": - logger.verbose("Author: '%s'", xItem.text) - self.bookAuthors.append(xItem.text) + author = checkString(simplified(xItem.text), "") + if author: + self.bookAuthors.append(author) + logger.verbose("Author: '%s'", author) elif xItem.tag == "saveCount": self.saveCount = checkInt(xItem.text, 0) elif xItem.tag == "autoCount": @@ -956,14 +958,14 @@ class NWProject(): """Set the project name (working title), This is the the title used for backup files etc. """ - self.projName = projName.strip() + self.projName = simplified(projName) self.setProjectChanged(True) return True def setBookTitle(self, bookTitle): """Set the book title, that is, the title to include in exports. """ - self.bookTitle = bookTitle.strip() + self.bookTitle = simplified(bookTitle) self.setProjectChanged(True) return True @@ -975,7 +977,7 @@ class NWProject(): self.bookAuthors = [] for bookAuthor in bookAuthors.splitlines(): - bookAuthor = bookAuthor.strip() + bookAuthor = simplified(bookAuthor) if bookAuthor == "": continue self.bookAuthors.append(bookAuthor) @@ -1114,7 +1116,9 @@ class NWProject(): def setAutoReplace(self, autoReplace): """Update the auto-replace dictionary. """ - self.autoReplace = autoReplace + self.autoReplace = {} + for key, entry in autoReplace.items(): + self.autoReplace[key] = simplified(entry) self.setProjectChanged(True) return True @@ -1123,7 +1127,9 @@ class NWProject(): """ for valKey, valEntry in titleFormat.items(): if valKey in self.titleFormat: - self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey]) + self.titleFormat[valKey] = checkString( + simplified(valEntry), self.titleFormat[valKey] + ) return True def setProjectChanged(self, bValue): diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index f41ac5c8..e894d5bf 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -23,7 +23,7 @@ B D - With This Stuff + With This Stuff %title% From 17c56440f22881fa3beaca80c60db7ea39ec6159 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 17:28:28 +0200 Subject: [PATCH 027/179] Automatic indexing of un-indexed files (#1039) * Downgrade rebuild index dialog from warning to info * Don't flag index as broken if file is missing, just add it again * Fix log warning * Drop the return value in the index checker --- novelwriter/core/index.py | 18 ++++++++++-------- novelwriter/guimain.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index b05fa952..39bdf0fb 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -681,16 +681,18 @@ class NWIndex(): logException() self._indexBroken = True - # Check that project files are indexed - for fHandle in self.theProject.projFiles: - if fHandle not in self._fileMeta: - self._indexBroken = True - break - - logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000) - if self._indexBroken: self.clearIndex() + logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000) + return + + # If the index was ok, we check that project files are indexed + for fHandle in self.theProject.projFiles: + if fHandle not in self._fileMeta: + logger.warning("Item '%s' is not in the index", fHandle) + self.reIndexHandle(fHandle) + + logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 2ffffe45..c21fc091 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -545,7 +545,7 @@ class GuiMain(QMainWindow): if self.theIndex.indexBroken: self.makeAlert(self.tr( "The project index is outdated or broken. Rebuilding index." - ), nwAlert.WARN) + ), nwAlert.INFO) self.rebuildIndex() # Make sure the changed status is set to false on things opened From fd2248de171532cc5c84233135ca1497806600ec Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 17:44:54 +0200 Subject: [PATCH 028/179] Add up and down button icons --- .../assets/icons/typicons_dark/icons.conf | 2 ++ .../icons/typicons_dark/typ_chevron-down.svg | 31 +++++++++++++++++++ .../icons/typicons_dark/typ_chevron-up.svg | 31 +++++++++++++++++++ .../assets/icons/typicons_light/icons.conf | 2 ++ .../icons/typicons_light/typ_chevron-down.svg | 31 +++++++++++++++++++ .../icons/typicons_light/typ_chevron-up.svg | 31 +++++++++++++++++++ novelwriter/gui/theme.py | 2 +- 7 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg create mode 100644 novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_chevron-down.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_chevron-up.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index e06aba68..bcaef71a 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -42,6 +42,7 @@ doc_h2 = mixed_heading2.svg doc_h3 = mixed_heading3.svg doc_h4 = mixed_heading4.svg done = typ_input-checked.svg +down = typ_chevron-down.svg edit = typ_pencil.svg forward = typ_chevron-right.svg hash = typ_hash.svg @@ -74,3 +75,4 @@ status_stats = typ_chart-bar-grey.svg status_time = typ_stopwatch-grey.svg sticky-off = typ_pin-outline.svg sticky-on = typ_pin.svg +up = typ_chevron-up.svg diff --git a/novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg b/novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg new file mode 100644 index 00000000..53389084 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg b/novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg new file mode 100644 index 00000000..9ac7e927 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 56e18b2a..8639908f 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -42,6 +42,7 @@ doc_h2 = mixed_heading2.svg doc_h3 = mixed_heading3.svg doc_h4 = mixed_heading4.svg done = typ_input-checked.svg +down = typ_chevron-down.svg edit = typ_pencil.svg forward = typ_chevron-right.svg hash = typ_hash.svg @@ -74,3 +75,4 @@ status_stats = typ_chart-bar-grey.svg status_time = typ_stopwatch-grey.svg sticky-off = typ_pin-outline.svg sticky-on = typ_pin.svg +up = typ_chevron-up.svg diff --git a/novelwriter/assets/icons/typicons_light/typ_chevron-down.svg b/novelwriter/assets/icons/typicons_light/typ_chevron-down.svg new file mode 100644 index 00000000..6ba80643 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_chevron-down.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_chevron-up.svg b/novelwriter/assets/icons/typicons_light/typ_chevron-up.svg new file mode 100644 index 00000000..1b9eb901 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_chevron-up.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 6cdf72ef..3c180b50 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -469,7 +469,7 @@ class GuiIcons: "delete", "close", "done", "clear", "save", "add", "remove", "search", "search_replace", "edit", "check", "cross", "hash", "maximise", "minimise", "refresh", "reference", "backward", - "forward", "settings", + "forward", "settings", "up", "down", # Switches "sticky-on", "sticky-off", From 6aa13e318bc82af5e62a942b6fcc7468210557b6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 18:17:01 +0200 Subject: [PATCH 029/179] Add sorting capability to status and importance labels --- novelwriter/core/project.py | 62 ++++++++++++---------------- novelwriter/core/status.py | 22 ++++++++++ novelwriter/dialogs/projsettings.py | 64 ++++++++++++++++++++--------- 3 files changed, 93 insertions(+), 55 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index a8a9cce3..9623520e 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -1072,46 +1072,14 @@ class NWProject(): return True def setStatusColours(self, newCols, delCols): - """Update the list of novel file status flags. Also iterate - through the project and replace keys that have been renamed. + """Update the list of novel file status flags. """ - if not (newCols or delCols): - return False - - for entry in newCols: - key = entry.get("key", None) - name = entry.get("name", "") - cols = entry.get("cols", (100, 100, 100)) - if name: - self.statusItems.write(key, name, cols) - - for key in delCols: - self.statusItems.remove(key) - - self.setProjectChanged(True) - - return True + return self._setStatusImport(newCols, delCols, self.statusItems) def setImportColours(self, newCols, delCols): - """Update the list of note file importance flags. Also iterate - through the project and replace keys that have been renamed. + """Update the list of note file importance flags. """ - if not (newCols or delCols): - return False - - for entry in newCols: - key = entry.get("key", None) - name = entry.get("name", "") - cols = entry.get("cols", (100, 100, 100)) - if name: - self.importItems.write(key, name, cols) - - for key in delCols: - self.importItems.remove(key) - - self.setProjectChanged(True) - - return True + return self._setStatusImport(newCols, delCols, self.importItems) def setAutoReplace(self, autoReplace): """Update the auto-replace dictionary. @@ -1251,6 +1219,28 @@ class NWProject(): # Internal Functions ## + def _setStatusImport(self, new, delete, target): + """Update the list of novel file status or importance flags, and + delete those that have been requested deleted. + """ + if not (new or delete): + return False + + order = [] + for entry in new: + key = entry.get("key", None) + name = entry.get("name", "") + cols = entry.get("cols", (100, 100, 100)) + if name: + order.append(target.write(key, name, cols)) + + for key in delete: + target.remove(key) + + target.reorder(order) + + return True + def _loadProjectLocalisation(self): """Load the language data for the current project language. """ diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 4e9317f2..9fcfbb88 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -167,6 +167,28 @@ class NWStatus(): else: return self._defaultIcon + def reorder(self, order): + """Reorder the items according to list. + """ + if len(order) != len(self._store): + logger.error("Length mismatch between new and old order") + return False + + if order == list(self._store.keys()): + return True + + store = {} + for key in order: + if key in self._store: + store[key] = self._store[key] + else: + logger.error("Unknown key '%s' in order", key) + return False + + self._store = store + + return True + def resetCounts(self): """Clear the counts of references to the status entries. """ diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 9599cc92..a0987418 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -306,6 +306,12 @@ class GuiProjectEditStatus(QWidget): self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") self.delButton.clicked.connect(self._delItem) + self.upButton = QPushButton(self.theTheme.getIcon("up"), "") + self.upButton.clicked.connect(lambda: self._moveItem(-1)) + + self.dnButton = QPushButton(self.theTheme.getIcon("down"), "") + self.dnButton.clicked.connect(lambda: self._moveItem(1)) + # Edit Form # ========= @@ -315,7 +321,7 @@ class GuiProjectEditStatus(QWidget): self.editName.setPlaceholderText(self.tr("Select item to edit")) self.colPixmap = QPixmap(self.iPx, self.iPx) - self.colPixmap.fill(QColor(120, 120, 120)) + self.colPixmap.fill(QColor(100, 100, 100)) self.colButton = QPushButton(QIcon(self.colPixmap), self.tr("Colour")) self.colButton.setIconSize(self.colPixmap.rect().size()) self.colButton.clicked.connect(self._selectColour) @@ -329,6 +335,8 @@ class GuiProjectEditStatus(QWidget): self.listControls = QVBoxLayout() self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.delButton) + self.listControls.addWidget(self.upButton) + self.listControls.addWidget(self.dnButton) self.listControls.addStretch(1) self.editBox = QHBoxLayout() @@ -390,7 +398,7 @@ class GuiProjectEditStatus(QWidget): def _newItem(self): """Create a new status item. """ - newItem = self._addItem(None, self.tr("New Item"), (0, 0, 0), 0) + newItem = self._addItem(None, self.tr("New Item"), (100, 100, 100), 0) newItem.setBackground(self.COL_LABEL, QBrush(QColor(0, 255, 0, 70))) newItem.setBackground(self.COL_USAGE, QBrush(QColor(0, 255, 0, 70))) self.colChanged = True @@ -445,23 +453,47 @@ class GuiProjectEditStatus(QWidget): return item + def _moveItem(self, step): + """Move and item up or down step. + """ + selItem = self._getSelectedItem() + if selItem is None: + return + + tIndex = self.listBox.indexOfTopLevelItem(selItem) + nChild = self.listBox.topLevelItemCount() + nIndex = tIndex + step + if nIndex < 0 or nIndex >= nChild: + return False + + cItem = self.listBox.takeTopLevelItem(tIndex) + self.listBox.insertTopLevelItem(nIndex, cItem) + self.listBox.clearSelection() + + cItem.setSelected(True) + self.colChanged = True + + return + def _selectedItem(self): """Extract the info of a selected item and populate the settings boxes and button. """ selItem = self._getSelectedItem() - if selItem is not None: - cols = selItem.data(self.COL_LABEL, self.COL_ROLE) - name = selItem.text(self.COL_LABEL) + if selItem is None: + return - pixmap = QPixmap(self.iPx, self.iPx) - pixmap.fill(QColor(*cols)) - self.selColour = QColor(*cols) - self.editName.setText(name) - self.colButton.setIcon(QIcon(pixmap)) - self.editName.setEnabled(True) - self.editName.selectAll() - self.editName.setFocus() + cols = selItem.data(self.COL_LABEL, self.COL_ROLE) + name = selItem.text(self.COL_LABEL) + + pixmap = QPixmap(self.iPx, self.iPx) + pixmap.fill(QColor(*cols)) + self.selColour = QColor(*cols) + self.editName.setText(name) + self.colButton.setIcon(QIcon(pixmap)) + self.editName.setEnabled(True) + self.editName.selectAll() + self.editName.setFocus() return @@ -477,12 +509,6 @@ class GuiProjectEditStatus(QWidget): return selItem[0] return None - def _rowsMoved(self): - """A row has been moved, so set the changed flag. - """ - self.colChanged = True - return - def _usageString(self, nUse): """Generate usage string. """ From 3ed74f0b02b94aee34846a881ad39974fca0af70 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 18:35:26 +0200 Subject: [PATCH 030/179] Add test coverage of status and importance reordering --- novelwriter/core/status.py | 2 +- novelwriter/dialogs/projsettings.py | 2 +- .../guiProjSettings_Dialog_nwProject.nwx | 4 +- tests/test_core/test_core_status.py | 32 +++++++++++++++ tests/test_dialogs/test_dlg_projsettings.py | 39 ++++++++++++------- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 9fcfbb88..4bade5e7 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -175,7 +175,7 @@ class NWStatus(): return False if order == list(self._store.keys()): - return True + return False store = {} for key in order: diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index a0987418..2a2bb969 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -464,7 +464,7 @@ class GuiProjectEditStatus(QWidget): nChild = self.listBox.topLevelItemCount() nIndex = tIndex + step if nIndex < 0 or nIndex >= nChild: - return False + return cItem = self.listBox.takeTopLevelItem(tIndex) self.listBox.insertTopLevelItem(nIndex, cItem) diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index e894d5bf..1b3c818a 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -42,7 +42,7 @@ New Minor Major - Final + Final diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index 611f8327..e1d224d8 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -229,6 +229,38 @@ def testCoreStatus_Entries(constData): assert theStatus.count(constData.statusKeys[2]) == 0 assert theStatus.count(constData.statusKeys[3]) == 0 + # Reorder + # ======= + + cOrder = list(theStatus.keys()) + assert cOrder == constData.statusKeys + + # Wrong length + assert theStatus.reorder([]) is False + + # No change + assert theStatus.reorder(cOrder) is False + + # Actual reaorder + nOrder = [ + constData.statusKeys[0], + constData.statusKeys[2], + constData.statusKeys[1], + constData.statusKeys[3], + ] + assert theStatus.reorder(nOrder) is True + assert list(theStatus.keys()) == nOrder + + # Add an unknown key + wOrder = nOrder.copy() + wOrder[3] = theStatus._newKey() + assert theStatus.reorder(wOrder) is False + assert list(theStatus.keys()) == nOrder + + # Put it back + assert theStatus.reorder(cOrder) is True + assert list(theStatus.keys()) == cOrder + # Default # ======= diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index f2aa8319..5a047522 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -28,9 +28,7 @@ from tools import cmpFiles, getGuiItem from PyQt5.QtGui import QColor from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import ( - QDialog, QAction, QMessageBox, QColorDialog, QTreeWidgetItem -) +from PyQt5.QtWidgets import QDialog, QAction, QMessageBox, QColorDialog from novelwriter.dialogs import GuiProjectSettings @@ -118,16 +116,6 @@ def testDlgProjSettings_Dialog( assert projEdit.tabStatus.getNewList() == ([], []) assert projEdit.tabStatus.listBox.topLevelItemCount() == 4 - # Fake drag'n'drop should change changed status - projEdit.tabStatus._rowsMoved() - assert projEdit.tabStatus.colChanged is True - projEdit.tabStatus.colChanged = False - - projEdit.tabStatus.listBox.clearSelection() - assert projEdit.tabStatus._getSelectedItem() is None - projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True) - assert isinstance(projEdit.tabStatus._getSelectedItem(), QTreeWidgetItem) - # Can't delete the first item (it's in use) projEdit.tabStatus.listBox.clearSelection() projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True) @@ -178,6 +166,31 @@ def testDlgProjSettings_Dialog( ] ) + # Move items + projEdit.tabStatus.listBox.clearSelection() + projEdit.tabStatus._moveItem(1) + assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ + constData.statusKeys[0], constData.statusKeys[1], constData.statusKeys[3], None + ] + + projEdit.tabStatus.listBox.clearSelection() + projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True) + projEdit.tabStatus._moveItem(-1) + assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ + constData.statusKeys[0], constData.statusKeys[1], constData.statusKeys[3], None + ] + + projEdit.tabStatus.listBox.clearSelection() + projEdit.tabStatus.listBox.topLevelItem(3).setSelected(True) + projEdit.tabStatus._moveItem(-1) + assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ + constData.statusKeys[0], constData.statusKeys[1], None, constData.statusKeys[3] + ] + projEdit.tabStatus._moveItem(1) + assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ + constData.statusKeys[0], constData.statusKeys[1], constData.statusKeys[3], None + ] + # Importance Tab # ============== From 61f5425662ccf5b7c3ac80c76127fa0f49d7e236 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 20:55:54 +0200 Subject: [PATCH 031/179] Update new item functions in project class --- novelwriter/core/item.py | 25 ++++++++++++++++--- novelwriter/core/project.py | 43 ++++++++++++--------------------- novelwriter/core/tree.py | 2 +- novelwriter/dialogs/docmerge.py | 2 +- novelwriter/dialogs/docsplit.py | 12 +++------ novelwriter/gui/projtree.py | 4 +-- 6 files changed, 45 insertions(+), 43 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 1edd93b8..af189597 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -294,14 +294,33 @@ class NWItem(): stIcon = self.theProject.importItems.icon(self._import) return stName, stIcon - def setImportStatus(self, theLabel): + def setImportStatus(self, value): """Update the importance or status value based on class. This is a wrapper setter for setStatus and setImport. """ if self._class in nwLists.CLS_NOVEL: - self.setStatus(theLabel) + self.setStatus(value) else: - self.setImport(theLabel) + self.setImport(value) + return + + def setClassDefaults(self, itemClass): + """Set the default values based on the item's class and the + project settings. + """ + self.setClass(itemClass) + + if self._class in nwLists.CLS_NOVEL: + self._layout = nwItemLayout.DOCUMENT + else: + self._layout = nwItemLayout.NOTE + + if self._status is None: + self.setStatus("New") # This forces a default value lookup + + if self._import is None: + self.setImport("New") # This forces a default value lookup + return ## diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 0d71b81f..28aba590 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -127,36 +127,26 @@ class NWProject(): newItem.setName(rootName) newItem.setType(nwItemType.ROOT) newItem.setClass(rootClass) - newItem.setStatus(0) self.projTree.append(None, None, newItem) self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle - def newFolder(self, folderName, folderClass, pHandle): - """Add a new folder with a given name and class and parent item. + def newFolder(self, folderName, pHandle): + """Add a new folder with a given name and parent item. """ newItem = NWItem(self) newItem.setName(folderName) newItem.setType(nwItemType.FOLDER) - newItem.setClass(folderClass) - newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle - def newFile(self, fileName, fileClass, pHandle): - """Add a new file with a given name and class, and set a layout - based on the class. DOCUMENT for NOVEL, otherwise NOTE. + def newFile(self, fileName, pHandle): + """Add a new file with a given name and parent item. """ newItem = NWItem(self) newItem.setName(fileName) newItem.setType(nwItemType.FILE) - if fileClass == nwItemClass.NOVEL: - newItem.setLayout(nwItemLayout.DOCUMENT) - else: - newItem.setLayout(nwItemLayout.NOTE) - newItem.setClass(fileClass) - newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle @@ -286,10 +276,10 @@ class NWProject(): xHandle[2] = self.newRoot(self.tr("Plot"), nwItemClass.PLOT) xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER) xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD) - xHandle[5] = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, xHandle[1]) - xHandle[6] = self.newFolder(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[1]) - xHandle[7] = self.newFile(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[6]) - xHandle[8] = self.newFile(self.tr("New Scene"), nwItemClass.NOVEL, xHandle[6]) + xHandle[5] = self.newFile(self.tr("Title Page"), xHandle[1]) + xHandle[6] = self.newFolder(self.tr("New Chapter"), xHandle[1]) + xHandle[7] = self.newFile(self.tr("New Chapter"), xHandle[6]) + xHandle[8] = self.newFile(self.tr("New Scene"), xHandle[6]) aDoc = NWDoc(self, xHandle[5]) aDoc.writeDocument(titlePage) @@ -312,8 +302,7 @@ class NWProject(): self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot) # Create a title page - tHandle = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, nHandle) - self.projTree.setFileItemLayout(tHandle, nwItemLayout.DOCUMENT) + tHandle = self.newFile(self.tr("Title Page"), nHandle) aDoc = NWDoc(self, tHandle) aDoc.writeDocument(titlePage) @@ -329,10 +318,9 @@ class NWProject(): chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}") pHandle = nHandle if chFolders: - pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle) + pHandle = self.newFolder(chTitle, nHandle) - cHandle = self.newFile(chTitle, nwItemClass.NOVEL, pHandle) - self.projTree.setFileItemLayout(cHandle, nwItemLayout.DOCUMENT) + cHandle = self.newFile(chTitle, pHandle) aDoc = NWDoc(self, cHandle) aDoc.writeDocument("## %s\n\n" % chTitle) @@ -341,7 +329,7 @@ class NWProject(): if numScenes > 0: for sc in range(numScenes): scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}") - sHandle = self.newFile(scTitle, nwItemClass.NOVEL, pHandle) + sHandle = self.newFile(scTitle, pHandle) aDoc = NWDoc(self, sHandle) aDoc.writeDocument("### %s\n\n" % scTitle) @@ -350,7 +338,7 @@ class NWProject(): elif numScenes > 0: for sc in range(numScenes): scTitle = self.tr("Scene {0}").format(f"{sc+1:d}") - sHandle = self.newFile(scTitle, nwItemClass.NOVEL, nHandle) + sHandle = self.newFile(scTitle, nHandle) aDoc = NWDoc(self, sHandle) aDoc.writeDocument("### %s\n\n" % scTitle) @@ -480,8 +468,9 @@ class NWProject(): # documents and one for project notes. Introduced in # version 1.5. # 1.4 : Introduces a more compact format for storing items. All - # settings aside from name are now attributes. Introduced - # in version 1.7. + # settings aside from name are now attributes. This format + # also changes the way satus and importance labels are + # stored and handled. Introduced in version 1.7. if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"): self.theParent.makeAlert(self.tr( diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index ddef88b7..e1b68220 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -222,7 +222,7 @@ class NWTree(): for _ in range(nwConst.MAX_DEPTH + 1): if iItem.itemParent is None: tItem.setRoot(iItem.itemHandle) - tItem.setClass(iItem.itemClass) + tItem.setClassDefaults(iItem.itemClass) return True else: iItem = self.__getitem__(iItem.itemParent) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index f7695b9a..c082dd3b 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -130,7 +130,7 @@ class GuiDocMerge(QDialog): self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR) return False - nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent) + nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent) newItem = self.theProject.projTree[nHandle] newItem.setStatus(srcItem.itemStatus) newItem.setImport(srcItem.itemImport) diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index cdf0757a..b650671d 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.core import NWDoc -from novelwriter.enum import nwAlert, nwItemType, nwItemClass, nwItemLayout +from novelwriter.enum import nwAlert, nwItemType from novelwriter.constants import nwConst from novelwriter.gui.custom import QHelpLabel @@ -186,22 +186,16 @@ class GuiDocSplit(QDialog): return False # Create the folder - fHandle = self.theProject.newFolder( - srcItem.itemName, srcItem.itemClass, srcItem.itemParent - ) + fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent) self.theParent.treeView.revealNewTreeItem(fHandle) logger.verbose("Creating folder '%s'", fHandle) # Loop through, and create the files for wTitle, iStart, iEnd in finalOrder: - isNovel = srcItem.itemClass == nwItemClass.NOVEL - itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE - wTitle = wTitle.lstrip("#").strip() - nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle) + nHandle = self.theProject.newFile(wTitle, fHandle) newItem = self.theProject.projTree[nHandle] - newItem.setLayout(itemLayout) newItem.setStatus(srcItem.itemStatus) newItem.setImport(srcItem.itemImport) logger.verbose( diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 01a8f707..584a281c 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -250,7 +250,7 @@ class GuiProjectTree(QTreeWidget): # If we're still here, add the file or folder if itemType == nwItemType.FILE: - tHandle = self.theProject.newFile(self.tr("New File"), itemClass, pHandle) + tHandle = self.theProject.newFile(self.tr("New File"), pHandle) elif itemType == nwItemType.FOLDER: if len(parTree) >= nwConst.MAX_DEPTH - 1: @@ -261,7 +261,7 @@ class GuiProjectTree(QTreeWidget): "Maximum folder depth has been reached." ), nwAlert.ERROR) return False - tHandle = self.theProject.newFolder(self.tr("New Folder"), itemClass, pHandle) + tHandle = self.theProject.newFolder(self.tr("New Folder"), pHandle) else: logger.error("Failed to add new item") From ebee9ff791b5bc385ebace92176f8b32c5b17ca2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 20:56:52 +0200 Subject: [PATCH 032/179] Update tests --- .../coreProject_NewCustomA_nwProject.nwx | 50 +++++++++---------- .../coreProject_NewCustomB_nwProject.nwx | 32 ++++++------ .../coreProject_NewFile_nwProject.nwx | 8 +-- .../coreProject_NewRoot_nwProject.nwx | 20 ++++---- .../guiEditor_Main_Final_nwProject.nwx | 12 ++--- .../guiEditor_Main_Initial_nwProject.nwx | 18 +++---- .../guiProjSettings_Dialog_nwProject.nwx | 20 ++++---- tests/test_core/test_core_document.py | 2 +- tests/test_core/test_core_index.py | 30 +++++------ tests/test_core/test_core_project.py | 10 ++-- tests/test_core/test_core_tree.py | 26 +++++----- tests/test_gui/test_gui_doceditor.py | 4 +- tests/test_gui/test_gui_statusbar.py | 4 +- 13 files changed, 119 insertions(+), 117 deletions(-) diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 2651909c..9152e3b6 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -35,7 +35,7 @@ Finished
- New + New Minor Major Main @@ -44,95 +44,95 @@ - Novel + Novel - Plot + Plot - Characters + Characters - Locations + Locations - Timeline + Timeline - Objects + Objects - Entities + Entities - Title Page + Title Page - Chapter 1 + Chapter 1 - Chapter 1 + Chapter 1 - Scene 1.1 + Scene 1.1 - Scene 1.2 + Scene 1.2 - Scene 1.3 + Scene 1.3 - Chapter 2 + Chapter 2 - Chapter 2 + Chapter 2 - Scene 2.1 + Scene 2.1 - Scene 2.2 + Scene 2.2 - Scene 2.3 + Scene 2.3 - Chapter 3 + Chapter 3 - Chapter 3 + Chapter 3 - Scene 3.1 + Scene 3.1 - Scene 3.2 + Scene 3.2 - Scene 3.3 + Scene 3.3
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 7ca8751d..ef29585b 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -35,7 +35,7 @@ Finished - New + New Minor Major Main @@ -44,59 +44,59 @@ - Novel + Novel - Plot + Plot - Characters + Characters - Locations + Locations - Timeline + Timeline - Objects + Objects - Entities + Entities - Title Page + Title Page - Scene 1 + Scene 1 - Scene 2 + Scene 2 - Scene 3 + Scene 3 - Scene 4 + Scene 4 - Scene 5 + Scene 5 - Scene 6 + Scene 6 diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index a1c33b58..13fd7966 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -33,7 +33,7 @@ Finished - New + New Minor Major Main @@ -74,11 +74,11 @@ - Hello + Hello - Jane + Jane diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 4274814c..264cfa0f 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -33,7 +33,7 @@ Finished - New + New Minor Major Main @@ -74,35 +74,35 @@ - Novel + Novel - Plot + Plot - Character + Character - World + World - Timeline + Timeline - Object + Object - Custom1 + Custom1 - Custom2 + Custom2 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 84515134..64ed4b4f 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -33,7 +33,7 @@ Finished - New + New Minor Major Main @@ -66,7 +66,7 @@ - New File + New File @@ -74,7 +74,7 @@ - New File + New File @@ -82,11 +82,11 @@ - New File + New File - Trash + Trash diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 8ed179b3..d6c8b904 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -33,7 +33,7 @@ Finished - New + New Minor Major Main @@ -42,35 +42,35 @@ - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index b74533d4..6c4cefdb 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -39,7 +39,7 @@ Final - New + New Minor Major Final @@ -48,35 +48,35 @@ - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py index eb7794b5..881290d6 100644 --- a/tests/test_core/test_core_document.py +++ b/tests/test_core/test_core_document.py @@ -66,7 +66,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal): # Try to open a new (non-existent) file nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL) assert nHandle is not None - xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle) + xHandle = theProject.newFile("New File", nHandle) theDoc = NWDoc(theProject, xHandle) assert bool(theDoc) is True assert repr(theDoc) == f"" diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 8070cbf2..cf7b1687 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -181,8 +181,8 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI): assert theProject.openProject(nwMinimal) is True theIndex = NWIndex(theProject) - nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") - cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") + nHandle = theProject.newFile("Hello", "a508bb932959c") + cHandle = theProject.newFile("Jane", "afb3043c7b2b3") nItem = theProject.projTree[nHandle] cItem = theProject.projTree[cHandle] @@ -260,8 +260,8 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): theIndex = NWIndex(theProject) # Some items for fail to scan tests - dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c") - xHandle = theProject.newFile("No Layout", nwItemClass.NOVEL, "a508bb932959c") + dHandle = theProject.newFolder("Folder", "a508bb932959c") + xHandle = theProject.newFile("No Layout", "a508bb932959c") xItem = theProject.projTree[xHandle] xItem.setLayout(nwItemLayout.NO_LAYOUT) @@ -279,6 +279,8 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theProject.projTree[tHandle] is not None xItem.setParent(tHandle) theProject.projTree.updateItemData(xItem.itemHandle) + assert xItem.itemRoot == tHandle + assert xItem.itemClass == nwItemClass.TRASH assert theIndex.scanText(xHandle, "Hello World!") is False # Create the archive root @@ -289,11 +291,11 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theIndex.scanText(xHandle, "Hello World!") is False # Make some usable items - tHandle = theProject.newFile("Title", nwItemClass.NOVEL, "a508bb932959c") - pHandle = theProject.newFile("Page", nwItemClass.NOVEL, "a508bb932959c") - nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") - cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") - sHandle = theProject.newFile("Scene", nwItemClass.NOVEL, "a508bb932959c") + tHandle = theProject.newFile("Title", "a508bb932959c") + pHandle = theProject.newFile("Page", "a508bb932959c") + nHandle = theProject.newFile("Hello", "a508bb932959c") + cHandle = theProject.newFile("Jane", "afb3043c7b2b3") + sHandle = theProject.newFile("Scene", "a508bb932959c") # Text Indexing # ============= @@ -475,8 +477,8 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): assert theProject.openProject(nwMinimal) is True theIndex = NWIndex(theProject) - nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") - cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") + nHandle = theProject.newFile("Hello", "a508bb932959c") + cHandle = theProject.newFile("Jane", "afb3043c7b2b3") assert theIndex.getNovelData("", "") is None assert theIndex.getNovelData("a508bb932959c", "") is None @@ -630,9 +632,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # Novel Stats # =========== - hHandle = theProject.newFile("Chapter", nwItemClass.NOVEL, "a508bb932959c") - sHandle = theProject.newFile("Scene One", nwItemClass.NOVEL, "a508bb932959c") - tHandle = theProject.newFile("Scene Two", nwItemClass.NOVEL, "a508bb932959c") + hHandle = theProject.newFile("Chapter", "a508bb932959c") + sHandle = theProject.newFile("Scene One", "a508bb932959c") + tHandle = theProject.newFile("Scene Two", "a508bb932959c") theProject.projTree[hHandle].itemLayout == nwItemLayout.DOCUMENT theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 30cb7f33..3d5b37f4 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -314,8 +314,8 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI): assert theProject.closeProject() is True assert theProject.openProject(projFile) is True - assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str) - assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str) + assert isinstance(theProject.newFile("Hello", "31489056e0916"), str) + assert isinstance(theProject.newFile("Jane", "71ee45a3c0db9"), str) assert theProject.projChanged assert theProject.saveProject() is True assert theProject.closeProject() is True @@ -661,7 +661,7 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): theProject.projTree._treeOrder.append("01234567789abc") # Add an item with a non-existent parent - nHandle = theProject.newFile("Test File", nwItemClass.NOVEL, "a6d311a93600a") + nHandle = theProject.newFile("Test File", "a6d311a93600a") theProject.projTree[nHandle].setParent("cba9876543210") assert theProject.projTree[nHandle].itemParent == "cba9876543210" @@ -740,7 +740,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, constData): # Change Importance # ================= - fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "73475cb40a568") + fHandle = theProject.newFile("Jane Doe", "73475cb40a568") theProject.projTree[fHandle].setImport("Main") assert theProject.projTree[fHandle].itemImport == constData.importKeys[3] @@ -1069,7 +1069,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): assert oItem.itemParent == "b3643d0f92e32" assert oItem.itemClass == nwItemClass.NOVEL assert oItem.itemType == nwItemType.FILE - assert oItem.itemLayout == nwItemLayout.NOTE + assert oItem.itemLayout == nwItemLayout.DOCUMENT assert theProject.saveProject(nwLipsum) assert theProject.closeProject() diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 3fd07a43..343c4609 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -397,31 +397,31 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'' b'' b'Novel' - b'' + b'class="NOVEL">Novel' b'Act One' + b'type="FOLDER" class="NOVEL">Act One' b'Chapter One' b'Scene One' b'Outtakes' + b'class="ARCHIVE">Outtakes' b'Trash' - b'' + b'class="TRASH">Trash' b'Characters' + b'class="CHARACTER">Characters' b'Jane Doe' b'' b'' diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index eaa1d2ba..cf5c3cea 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QAction, QMessageBox, qApp from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.core import countWords -from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout +from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout from novelwriter.constants import nwKeyWords, nwUnicode keyDelay = 2 @@ -1143,7 +1143,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): # Create Character theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n" - cHandle = nwGUI.theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3") + cHandle = nwGUI.theProject.newFile("Jane Doe", "afb3043c7b2b3") assert nwGUI.openDocument(cHandle) is True assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.saveDocument() is True diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 9d7c4efc..66329820 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -25,7 +25,7 @@ import pytest from PyQt5.QtWidgets import QMessageBox from novelwriter.core import NWDoc -from novelwriter.enum import nwItemClass, nwState +from novelwriter.enum import nwState @pytest.mark.gui @@ -36,7 +36,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj): nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) is True - cHandle = nwGUI.theProject.newFile("A Note", nwItemClass.CHARACTER, "71ee45a3c0db9") + cHandle = nwGUI.theProject.newFile("A Note", "71ee45a3c0db9") newDoc = NWDoc(nwGUI.theProject, cHandle) newDoc.writeDocument("# A Note\n\n") nwGUI.treeView.revealNewTreeItem(cHandle) From 4cfbbf4c680afd205a308182791ae7564b567b9e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 14:44:11 +0200 Subject: [PATCH 033/179] Remove max folder depth restriction, and simplify adding folders and files in the tree --- novelwriter/constants.py | 1 - novelwriter/core/item.py | 4 +- novelwriter/core/tree.py | 32 ++++- novelwriter/dialogs/docsplit.py | 11 -- novelwriter/gui/mainmenu.py | 4 +- novelwriter/gui/projtree.py | 127 +++++------------- .../guiEditor_Main_Final_031b4af5197ec.nwd | 2 +- .../guiEditor_Main_Final_1a6562590ef19.nwd | 2 +- .../guiEditor_Main_Final_41cfc0d1f2d12.nwd | 2 +- .../guiEditor_Main_Final_nwProject.nwx | 8 +- tests/test_core/test_core_tree.py | 10 +- tests/test_dialogs/test_dlg_docmerge.py | 6 +- tests/test_dialogs/test_dlg_docsplit.py | 8 +- tests/test_dialogs/test_dlg_itemeditor.py | 2 +- 14 files changed, 87 insertions(+), 132 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index f71453f3..796d0145 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -42,7 +42,6 @@ class nwConst(): FMT_DSTAMP = "%Y-%m-%d" # Date only format # Various Hard Limits - MAX_DEPTH = 30 # Maximum folder depth of a project MAX_DOCSIZE = 5000000 # Maxium size of a single document MAX_BUILDSIZE = 10000000 # Maxium size of a project build diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index af189597..893b3965 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -308,7 +308,9 @@ class NWItem(): """Set the default values based on the item's class and the project settings. """ - self.setClass(itemClass) + if self._parent is not None: + # Only update for child items + self.setClass(itemClass) if self._class in nwLists.CLS_NOVEL: self._layout = nwItemLayout.DOCUMENT diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index e1b68220..2983f88a 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -33,7 +33,7 @@ from hashlib import sha256 from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.error import logException from novelwriter.common import checkHandle -from novelwriter.constants import nwConst, nwFiles +from novelwriter.constants import nwFiles from novelwriter.core.item import NWItem logger = logging.getLogger(__name__) @@ -41,6 +41,8 @@ logger = logging.getLogger(__name__) class NWTree(): + MAX_DEPTH = 1000 # Cap of tree traversing for loops + def __init__(self, theProject): self.theProject = theProject @@ -219,7 +221,7 @@ class NWTree(): return False iItem = tItem - for _ in range(nwConst.MAX_DEPTH + 1): + for _ in range(self.MAX_DEPTH): if iItem.itemParent is None: tItem.setRoot(iItem.itemHandle) tItem.setClassDefaults(iItem.itemClass) @@ -228,8 +230,8 @@ class NWTree(): iItem = self.__getitem__(iItem.itemParent) if iItem is None: return False - - return False + else: + raise RecursionError("Critical internal error") def checkType(self, tHandle, itemType): """Return true of item exists and is of the specified item type. @@ -249,7 +251,7 @@ class NWTree(): tItem = self.__getitem__(tHandle) if tItem is not None: tTree.append(tHandle) - for _ in range(nwConst.MAX_DEPTH + 1): + for _ in range(self.MAX_DEPTH): if tItem.itemParent is None: return tTree else: @@ -259,6 +261,9 @@ class NWTree(): return tTree else: tTree.append(tHandle) + else: + raise RecursionError("Critical internal error") + return tTree ## @@ -270,6 +275,23 @@ class NWTree(): """ return tHandle in self._treeRoots + def isTrash(self, tHandle): + """Check if an item is in or is the trash folder. + """ + tItem = self.__getitem__(tHandle) + if tItem is None: + return True + if tItem.itemClass == nwItemClass.TRASH: + return True + if self._trashRoot is not None: + if tHandle == self._trashRoot: + return True + elif tItem.itemParent == self._trashRoot: + return True + elif tItem.itemRoot == self._trashRoot: + return True + return False + def isTrashRoot(self, tHandle): """Check if a handle is the trash folder. """ diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index b650671d..ff2cb849 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -34,7 +34,6 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwAlert, nwItemType -from novelwriter.constants import nwConst from novelwriter.gui.custom import QHelpLabel logger = logging.getLogger(__name__) @@ -160,16 +159,6 @@ class GuiDocSplit(QDialog): ), nwAlert.ERROR) return False - # Check that another folder can be created - parTree = self.theProject.projTree.getItemPath(srcItem.itemParent) - if len(parTree) >= nwConst.MAX_DEPTH - 1: - self.theParent.makeAlert(self.tr( - "Cannot add new folder for the document split. " - "Maximum folder depth has been reached. " - "Please move the file to another level in the project tree." - ), nwAlert.ERROR) - return False - msgYes = self.theParent.askQuestion( self.tr("Split Document"), "{0}

{1}".format( diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 0b7efa60..f69d5a56 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -197,7 +197,7 @@ class GuiMainMenu(QMenuBar): # Project > New Folder self.aCreateFolder = QAction(self.tr("Create Folder"), self) self.aCreateFolder.setShortcut("Ctrl+Shift+N") - self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER, None)) + self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER)) self.projMenu.addAction(self.aCreateFolder) # Project > Separator @@ -259,7 +259,7 @@ class GuiMainMenu(QMenuBar): # Document > New self.aNewDoc = QAction(self.tr("New Document"), self) self.aNewDoc.setShortcut("Ctrl+N") - self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE, None)) + self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE)) self.docuMenu.addAction(self.aNewDoc) # Document > Open diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 584a281c..0f25f68c 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.constants import nwConst, trConst, nwLists, nwLabels +from novelwriter.constants import trConst, nwLists, nwLabels logger = logging.getLogger(__name__) @@ -161,114 +161,64 @@ class GuiProjectTree(QTreeWidget): self._timeChanged = 0 return - def newTreeItem(self, itemType, itemClass): - """Add new item to the tree, with a given itemType and - itemClass, and attach it to the selected handle. Also make sure - the item is added in a place it can be added, and that other + def newTreeItem(self, itemType, itemClass=None): + """Add new item to the tree, with a given itemType (and + itemClass if Root), and attach it to the selected handle. Also make + sure the item is added in a place it can be added, and that other meta data is set correctly to ensure a valid project tree. """ - pHandle = self.getSelectedHandle() - nHandle = None - if not self.theParent.hasProject: 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 + nHandle = None + tHandle = None - # 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. - if itemClass is None and pHandle is not None: - pItem = self.theProject.projTree[pHandle] - if pItem is not None: - itemClass = pItem.itemClass + if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass): - # If class is still not set, alert the user and exit - if itemClass is None: - if itemType == nwItemType.FILE: - self.theParent.makeAlert(self.tr( - "Please select a valid location in the tree to add the document." - ), nwAlert.ERROR) - else: - self.theParent.makeAlert(self.tr( - "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 - logger.verbose( - "Adding new item of type '%s' and class '%s' to handle '%s'", - itemType.name, itemClass.name, str(pHandle) - ) - - if itemType == nwItemType.ROOT: tHandle = self.theProject.newRoot( trConst(nwLabels.CLASS_NAME[itemClass]), itemClass ) - if tHandle is None: - logger.error("No root item added") - return False - else: - # If no parent has been selected, make the new file under - # the root NOVEL item. - if pHandle is None: - pHandle = self.theProject.projTree.findRoot(nwItemClass.NOVEL) + elif itemType in (nwItemType.FILE, nwItemType.FOLDER): - # If still nothing, give up - if pHandle is None: + sHandle = self.getSelectedHandle() + if sHandle is None or sHandle not in self.theProject.projTree: self.theParent.makeAlert(self.tr( "Did not find anywhere to add the file or folder!" ), nwAlert.ERROR) return False - # Now check if the selected item is a file, in which case - # the new file will be a sibling - pItem = self.theProject.projTree[pHandle] + # If the selected item is a file, the new item will be a sibling + pItem = self.theProject.projTree[sHandle] if pItem.itemType == nwItemType.FILE: - nHandle = pHandle - pHandle = pItem.itemParent + nHandle = sHandle + sHandle = pItem.itemParent + if sHandle is None: + logger.error("Internal error") # Bug + return False - # If we again have no home, give up - if pHandle is None: - self.theParent.makeAlert(self.tr( - "Did not find anywhere to add the file or folder!" - ), nwAlert.ERROR) - return False - - if self.theProject.projTree.isTrashRoot(pHandle): + if self.theProject.projTree.isTrash(sHandle): self.theParent.makeAlert(self.tr( "Cannot add new files or folders to the Trash folder." ), nwAlert.ERROR) return False - parTree = self.theProject.projTree.getItemPath(pHandle) - - # If we're still here, add the file or folder + # Add the file or folder if itemType == nwItemType.FILE: - tHandle = self.theProject.newFile(self.tr("New File"), pHandle) - + if pItem.itemClass in nwLists.CLS_NOVEL: + tHandle = self.theProject.newFile(self.tr("New Document"), sHandle) + else: + tHandle = self.theProject.newFile(self.tr("New Note"), sHandle) elif itemType == nwItemType.FOLDER: - if len(parTree) >= nwConst.MAX_DEPTH - 1: - # Folders cannot be deeper than MAX_DEPTH - 1, leaving room - # for one more level of files. - self.theParent.makeAlert(self.tr( - "Cannot add new folder to this item. " - "Maximum folder depth has been reached." - ), nwAlert.ERROR) - return False - tHandle = self.theProject.newFolder(self.tr("New Folder"), pHandle) + tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle) - else: - logger.error("Failed to add new item") - return False + else: + logger.error("Failed to add new item") + return False - # If there is no handle set, return here - if tHandle is None: + # If there is no handle set, return here. This is a bug + if tHandle is None: # pragma: no cover return True # Add the new item to the tree @@ -282,11 +232,7 @@ class GuiProjectTree(QTreeWidget): # This is a new file, so let's add some content newDoc = NWDoc(self.theProject, tHandle) - curTxt = newDoc.readDocument() - if curTxt is None: - curTxt = "" - - if curTxt == "": + if not newDoc.readDocument(): if nwItem.itemLayout == nwItemLayout.DOCUMENT: newText = f"### {nwItem.itemName}\n\n" else: @@ -633,7 +579,7 @@ class GuiProjectTree(QTreeWidget): return - def propagateCount(self, tHandle, theCount, nDepth=0): + def propagateCount(self, tHandle, theCount): """Recursive function setting the word count for a given item, and propagating that count upwards in the tree until reaching a root item. This function is more efficient than recalculating @@ -653,12 +599,13 @@ class GuiProjectTree(QTreeWidget): return pCount = 0 + pHandle = None for i in range(pItem.childCount()): pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole)) pHandle = pItem.data(self.C_NAME, Qt.UserRole) - if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "": - self.propagateCount(pHandle, pCount, nDepth+1) + if pHandle: + self.propagateCount(pHandle, pCount) return @@ -1180,7 +1127,7 @@ class GuiProjectTreeMenu(QMenu): """Forward the new file call to the project tree. """ if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FILE, None) + self.theTree.newTreeItem(nwItemType.FILE) return @pyqtSlot() @@ -1188,7 +1135,7 @@ class GuiProjectTreeMenu(QMenu): """Forward the new folder call to the project tree. """ if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FOLDER, None) + self.theTree.newTreeItem(nwItemType.FOLDER) return @pyqtSlot() diff --git a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd b/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd index acb36501..6492390b 100644 --- a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd +++ b/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd @@ -1,4 +1,4 @@ -%%~name: New File +%%~name: New Note %%~path: 44cb730c42048/031b4af5197ec %%~kind: PLOT/NOTE # Main Plot diff --git a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd b/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd index 9a3ca0a9..1da5a713 100644 --- a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd +++ b/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd @@ -1,4 +1,4 @@ -%%~name: New File +%%~name: New Note %%~path: 71ee45a3c0db9/1a6562590ef19 %%~kind: CHARACTER/NOTE # Jane Doe diff --git a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd b/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd index 8e8cb037..14a58a49 100644 --- a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd +++ b/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd @@ -1,4 +1,4 @@ -%%~name: New File +%%~name: New Note %%~path: 811786ad1ae74/41cfc0d1f2d12 %%~kind: WORLD/NOTE # Main Location diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 64ed4b4f..34f5a0ed 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -66,7 +66,7 @@ - New File + New Note @@ -74,7 +74,7 @@ - New File + New Note @@ -82,7 +82,7 @@ - New File + New Note diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 343c4609..6cfa9cf0 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -210,7 +210,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): @pytest.mark.core -def testCoreTree_Methods(monkeypatch, mockGUI, mockItems): +def testCoreTree_Methods(mockGUI, mockItems): """Test various class methods. """ theProject = NWProject(mockGUI) @@ -235,9 +235,11 @@ def testCoreTree_Methods(monkeypatch, mockGUI, mockItems): assert theTree.updateItemData("b000000000001") is True # Update item data, root is unreachable - with monkeypatch.context() as mp: - mp.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 0) - assert theTree.updateItemData("b000000000001") is False + maxDepth = theTree.MAX_DEPTH + theTree.MAX_DEPTH = 0 + with pytest.raises(RecursionError): + theTree.updateItemData("b000000000001") + theTree.MAX_DEPTH = maxDepth # Chech type assert theTree.checkType("blabla", nwItemType.FILE) is False diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index 6a3825b8..a4b21f45 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -58,9 +58,9 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj): nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.clearSelection() nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.treeView.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index d8d4bbac..66f8868a 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -62,7 +62,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj): nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.clearSelection() nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -230,12 +230,6 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj): mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) assert nwSplit._doSplit() is False - # Block folder creation by returning that the folder has a depth - # of 50 items in the tree - with monkeypatch.context() as mp: - mp.setattr(NWTree, "getItemPath", lambda *a: [""]*50) - assert nwSplit._doSplit() is False - # Clear the list nwSplit.listBox.clear() assert nwSplit._doSplit() is False diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 10d72549..127d2e74 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -175,7 +175,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData): itemEdit.show() # Check Existing Settings - assert itemEdit.editName.text() == "New File" + assert itemEdit.editName.text() == "New Note" assert itemEdit.editStatus.currentData() == constData.importKeys[0] assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE assert itemEdit.editExport.isChecked() is True From d158de5d8265c0de1f6bc8e88d0faefdb957bcf6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 17:00:43 +0200 Subject: [PATCH 034/179] Update test coverage --- novelwriter/gui/projtree.py | 25 +- tests/conftest.py | 4 +- tests/test_core/test_core_project.py | 11 +- tests/test_core/test_core_tree.py | 110 ++++-- tests/test_gui/test_gui_projtree.py | 557 ++++++++++++++++++--------- 5 files changed, 474 insertions(+), 233 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 0f25f68c..5fdf7fcc 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -256,6 +256,9 @@ class GuiProjectTree(QTreeWidget): """Reveal a newly added project item in the project tree. """ nwItem = self.theProject.projTree[tHandle] + if nwItem is None: + return False + trItem = self._addTreeItem(nwItem, nHandle) if trItem is None: return False @@ -459,13 +462,6 @@ class GuiProjectTree(QTreeWidget): if doPermanent: logger.debug("Permanently deleting file with handle '%s'", tHandle) - self.propagateCount(tHandle, 0) - tIndex = trItemP.indexOfChild(trItemS) - trItemC = trItemP.takeChild(tIndex) - - if self.theParent.docEditor.docHandle() == tHandle: - self.theParent.closeDocument() - delDoc = NWDoc(self.theProject, tHandle) if not delDoc.deleteDocument(): self.theParent.makeAlert([ @@ -473,6 +469,13 @@ class GuiProjectTree(QTreeWidget): ], nwAlert.ERROR) return False + self.propagateCount(tHandle, 0) + tIndex = trItemP.indexOfChild(trItemS) + trItemC = trItemP.takeChild(tIndex) + + if self.theParent.docEditor.docHandle() == tHandle: + self.theParent.closeDocument() + self.theIndex.deleteHandle(tHandle) self._deleteTreeItem(tHandle) self._setTreeChanged(True) @@ -486,13 +489,10 @@ class GuiProjectTree(QTreeWidget): self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName), ) if msgYes: - if pHandle is None: - logger.warning("File has no parent item") - logger.debug("Moving file '%s' to trash", tHandle) self.propagateCount(tHandle, 0) - tIndex = trItemP.indexOfChild(trItemS) + tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) self._updateItemParent(tHandle) @@ -508,6 +508,7 @@ class GuiProjectTree(QTreeWidget): if trItemP is None: logger.error("Could not delete folder") return False + tIndex = trItemP.indexOfChild(trItemS) if trItemS.childCount() == 0: trItemP.takeChild(tIndex) @@ -993,7 +994,7 @@ class GuiProjectTree(QTreeWidget): """ if self.theProject.projTree.checkType(tHandle, nwItemType.FILE): nwItem = self.theProject.projTree[tHandle] - if nwItem.itemClass == nwItemClass.NOVEL: + if nwItem.itemClass in nwLists.CLS_NOVEL: self.novelItemChanged.emit() else: self.noteItemChanged.emit() diff --git a/tests/conftest.py b/tests/conftest.py index 8551724b..a11db7e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -166,7 +166,9 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf): """ monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) monkeypatch.setattr("novelwriter.CONFIG", fncConf) - nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir]) + nwGUI = novelwriter.main( + ["--testmode", "--info", "--config=%s" % fncDir, "--data=%s" % fncDir] + ) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.wait(20) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 3d5b37f4..b584679d 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -1016,9 +1016,16 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): """ theProject = NWProject(mockGUI) - assert theProject.openProject(nwLipsum) + assert theProject.openProject(nwLipsum) is True assert theProject.projTree["636b6aa9b697b"] is None - assert theProject.closeProject() + + # Add a file with non-existent parent + # This file will be renoved from the project on open + assert theProject.newFile("Oops", "0000000000000") + + # Save and close + assert theProject.saveProject() is True + assert theProject.closeProject() is True # First Item with Meta Data orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd") diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 6cfa9cf0..3e8aeeca 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -21,6 +21,7 @@ along with this program. If not, see . import os import pytest +import random from lxml import etree from hashlib import sha256 @@ -36,6 +37,7 @@ from novelwriter.constants import nwFiles def mockItems(mockGUI): """Create a list of mock items. """ + random.seed(42) theProject = NWProject(mockGUI) itemA = NWItem(theProject) @@ -151,8 +153,28 @@ def testCoreTree_BuildTree(mockGUI, mockItems): # Check that we have the correct archive and trash folders assert theTree.trashRoot() == "a000000000003" assert theTree.archiveRoot() == "a000000000002" - assert theTree.isTrashRoot("a000000000003") - assert theTree.isRoot("a000000000002") + assert theTree.isTrashRoot("a000000000003") is True + assert theTree.isRoot("a000000000002") is True + + # Check the isTrash function + assert theTree.isTrash("0000000000000") is True # Doesn't exist + assert theTree.isTrash("a000000000003") is True # This the trash folder + + theTree["a000000000003"].setClass(nwItemClass.NO_CLASS) + assert theTree.isTrash("a000000000003") is True # This is still trash + theTree["a000000000003"].setClass(nwItemClass.TRASH) + + assert theTree.isTrash("b000000000002") is False # This is not trash + + value = theTree["b000000000002"].itemParent + theTree["b000000000002"].setParent("a000000000003") + assert theTree.isTrash("b000000000002") is True # This is in trash + theTree["b000000000002"].setParent(value) + + value = theTree["b000000000002"].itemRoot + theTree["b000000000002"].setRoot("a000000000003") + assert theTree.isTrash("b000000000002") is True # This is in trash + theTree["b000000000002"].setRoot(value) # Try to add another trash folder itemT = NWItem(theProject) @@ -161,7 +183,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): itemT._class = nwItemClass.TRASH itemT._expanded = False - assert not theTree.append("1234567890abc", None, itemT) + assert theTree.append("1234567890abc", None, itemT) is False assert len(theTree) == len(mockItems) # Generate handle automatically @@ -251,12 +273,24 @@ def testCoreTree_Methods(mockGUI, mockItems): assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" + # Add a fake item to root and check that it can handle it + theTree._treeRoots["0000000000000"] = NWItem(theProject) + assert theTree.findRoot(nwItemClass.WORLD) is None + del theTree._treeRoots["0000000000000"] + # Get item path assert theTree.getItemPath("stuff") == [] assert theTree.getItemPath("c000000000001") == [ "c000000000001", "b000000000001", "a000000000001" ] + # Cause recursion error + maxDepth = theTree.MAX_DEPTH + theTree.MAX_DEPTH = 0 + with pytest.raises(RecursionError): + theTree.getItemPath("c000000000001") + theTree.MAX_DEPTH = maxDepth + # Break the folder parent handle theTree["b000000000001"]._parent = "stuff" assert theTree.getItemPath("c000000000001") == [ @@ -381,7 +415,7 @@ def testCoreTree_Reorder(mockGUI, mockItems): @pytest.mark.core -def testCoreTree_XMLPackUnpack(mockGUI, mockItems): +def testCoreTree_XMLPackUnpack(mockGUI, mockItems, constData): """Test packing and unpacking the tree to and from XML. """ theProject = NWProject(mockGUI) @@ -395,39 +429,41 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): nwXML = etree.Element("novelWriterXML") theTree.packXML(nwXML) - assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( - b'' - b'' - b'Novel' - b'Act One' - b'Chapter One' - b'Scene One' - b'Outtakes' - b'Trash' - b'Characters' - b'Jane Doe' - b'' - b'' - ) + assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == bytes(( + '' + '' + 'Novel' + 'Act One' + 'Chapter One' + 'Scene One' + 'Outtakes' + 'Trash' + 'Characters' + 'Jane Doe' + '' + '' + ).format( + s0=constData.statusKeys[0], i0=constData.importKeys[0] + ), encoding="utf8") theTree.clear() assert len(theTree) == 0 diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 9d43481c..1f1c8c95 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -22,9 +22,6 @@ along with this program. If not, see . import pytest import os -from tools import writeFile - -from PyQt5.QtCore import QItemSelectionModel from PyQt5.QtWidgets import QAction, QMessageBox from novelwriter.guimain import GuiMain @@ -33,208 +30,406 @@ from novelwriter.enum import nwItemType, nwItemClass @pytest.mark.gui -def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): +def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): """Test adding and removing items from the project tree. """ # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) - nwGUI.theProject.projTree.setSeed(42) nwTree = nwGUI.treeView - ## - # Add New Items - ## + # Try to add item with no project + assert nwTree.newTreeItem(nwItemType.FILE) is False - # Try to add and move item with no project - assert nwTree.newTreeItem(nwItemType.FILE, None) is False - assert nwTree.moveTreeItem(1) is False + # Create a project + nwGUI.theProject.projTree.setSeed(42) + prjDir = os.path.join(fncDir, "project") + assert nwGUI.newProject({"projPath": prjDir}) is True - # Open a project - assert nwGUI.openProject(nwMinimal) is True + # No itemType set + nwTree.clearSelection() + assert nwTree.newTreeItem(None) is False + + # Root Items + # ========== + + # No class set + assert nwTree.newTreeItem(nwItemType.ROOT) is False + + # Create root item + assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True + assert "1a6562590ef19" in nwGUI.theProject.projTree + + # File/Folder Items + # ================= # No location selected for new item nwTree.clearSelection() - assert nwTree.newTreeItem(nwItemType.FILE, None) is False - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False - assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is True + caplog.clear() + assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.newTreeItem(nwItemType.FOLDER) is False + assert "Did not find anywhere" in caplog.text - # No itemType set or ROOT, but no class - nwTree.clearSelection() - assert nwTree.newTreeItem(None, None) is False - assert nwTree.newTreeItem(nwItemType.ROOT, None) is False + # Create new folder as child of Novel folder + nwTree.setSelectedHandle("73475cb40a568") + assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwGUI.theProject.projTree["031b4af5197ec"].itemParent == "73475cb40a568" + assert nwGUI.theProject.projTree["031b4af5197ec"].itemRoot == "73475cb40a568" + assert nwGUI.theProject.projTree["031b4af5197ec"].itemClass == nwItemClass.NOVEL - # Select a location - chItem = nwTree._getTreeItem("a6d311a93600a") - nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) - chItem.setExpanded(True) + # Add a new file in the new folder + nwTree.setSelectedHandle("031b4af5197ec") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemParent == "031b4af5197ec" + assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemRoot == "73475cb40a568" + assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemClass == nwItemClass.NOVEL - # Create new item with no class set (defaults to NOVEL) - assert nwTree.newTreeItem(nwItemType.FILE, None) is True - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is True + # Add a new file next to the other new file + nwTree.setSelectedHandle("41cfc0d1f2d12") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwGUI.theProject.projTree["2858dcd1057d3"].itemParent == "031b4af5197ec" + assert nwGUI.theProject.projTree["2858dcd1057d3"].itemRoot == "73475cb40a568" + assert nwGUI.theProject.projTree["2858dcd1057d3"].itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument("2858dcd1057d3") + assert nwGUI.docEditor.getText() == "### New Document\n\n" - # Check that we have the correct tree order - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" - ] + # Add a new file to the characters folder + nwTree.setSelectedHandle("71ee45a3c0db9") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwGUI.theProject.projTree["2fca346db6561"].itemParent == "71ee45a3c0db9" + assert nwGUI.theProject.projTree["2fca346db6561"].itemRoot == "71ee45a3c0db9" + assert nwGUI.theProject.projTree["2fca346db6561"].itemClass == nwItemClass.CHARACTER + assert nwGUI.openDocument("2fca346db6561") + assert nwGUI.docEditor.getText() == "# New Note\n\n" - # Add more roots - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True # Duplicate - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) is True # Unique + # Make sure the sibling folder bug trap works + nwTree.setSelectedHandle("2858dcd1057d3") + nwGUI.theProject.projTree["2858dcd1057d3"].setParent(None) # This should not happen + caplog.clear() + assert nwTree.newTreeItem(nwItemType.FILE) is False + assert "Internal error" in caplog.text + nwGUI.theProject.projTree["2858dcd1057d3"].setParent("031b4af5197ec") - # Change max depth and try to add a subfolder that is too deep - monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2) - chItem = nwTree._getTreeItem("71ee45a3c0db9") - nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False + # Get the trash folder + nwTree._addTrashRoot() + trashHandle = nwGUI.theProject.trashFolder() + nwTree.setSelectedHandle(trashHandle) + assert nwTree.newTreeItem(nwItemType.FILE) is False + assert "Cannot add new files or folders to the Trash folder" in caplog.text - ## - # Move Items - ## + # Other Checks + # ============ - nwTree.setSelectedHandle("8c659a11cd429") + # Also check error handling in reveal function + assert nwTree.revealNewTreeItem("abc") is False - # Shift focus and try to move item - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - assert nwTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" - ] - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - - # Move second item up twice (should give same result) - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9" - ] - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9" - ] - - # 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", "44cb730c42048", "71ee45a3c0db9" - ] - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9" - ] - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" - ] - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" - ] - - # Move up twice, and undo - nwTree._lastMove = {} - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) - nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" - ] - - # Move a root item (top level items are different) twice - nwTree.flushTreeOrder() - 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") == 11 - - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 12 - - ## - # 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 - nwTree.clearSelection() - assert nwTree.emptyTrash() is False # No folder yet - assert nwTree.deleteItem(None) is False - assert nwTree.deleteItem("1111111111111") is False - assert nwTree.deleteItem("73475cb40a568") is True # New File - assert nwTree.deleteItem("71ee45a3c0db9") is True # New Folder - assert nwTree.deleteItem("811786ad1ae74") is True # Custom Root - assert "73475cb40a568" 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")) - assert nwTree.emptyTrash() is True - assert nwTree.emptyTrash() is False # Already empty - 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 nwTree.newTreeItem(nwItemType.FILE, None) is False - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False - - # Close the project - nwGUI.closeProject() - - ## - # Orphaned Files - ## - - # Add an orphaned file - orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd") - writeFile(orphFile, "# Hello World\n") - - # Open the project again - nwGUI.openProject(nwMinimal) - - # Check that the orphaned file was found and added to the tree - nwTree.flushTreeOrder() - assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder - orItem = nwTree._getTreeItem("1234567890abc") - assert orItem.text(nwTree.C_NAME) == "Recovered File 1" - - ## - # Unexpected Error Handling - ## - - # Add an item with an invalid type - assert nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL) is False - 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"]._parent = None - assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False - nwTree.clearSelection() - - # Add a file with no parent, and fail to find a suitable parent item - monkeypatch.setattr("novelwriter.core.tree.NWTree.findRoot", lambda *a: None) - - assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False - assert nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL) is False + # Add an item that cannot be displayed in the tree + nHandle = nwGUI.theProject.newFile("Test", None) + assert nwTree.revealNewTreeItem(nHandle) is False + # Clean up # qtbot.stopForInteraction() nwGUI.closeProject() -# END Test testGuiProjTree_TreeItems +# END Test testGuiProjTree_NewItems + + +@pytest.mark.gui +def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir): + """Test adding and removing items from the project tree. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + + nwTree = nwGUI.treeView + + # Try to move item with no project + assert nwTree.moveTreeItem(1) is False + + # Create a project + nwGUI.theProject.projTree.setSeed(42) + prjDir = os.path.join(fncDir, "project") + assert nwGUI.newProject({"projPath": prjDir}) is True + + # Move Documents + # ============== + + # Add some files + nwTree.setSelectedHandle("31489056e0916") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Move item without focus + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) + assert nwTree.moveTreeItem(1) is False + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) + + # Move with no selections + nwTree.clearSelection() + assert nwTree.moveTreeItem(1) is False + + # Move second item up twice (should give same result) + nwTree.setSelectedHandle("0e17daca5f3e1") + assert nwTree.moveTreeItem(-1) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "0e17daca5f3e1", "98010bd9270f9", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + assert nwTree.moveTreeItem(-1) is False + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "0e17daca5f3e1", "98010bd9270f9", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Restore via menu entry + nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Move fifth item down twice (should give same result) + nwTree.setSelectedHandle("031b4af5197ec") + assert nwTree.moveTreeItem(1) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec", + ] + assert nwTree.moveTreeItem(1) is False + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec", + ] + + # Restore via menu entry + nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Move down again, and restore via undo + nwTree.setSelectedHandle("031b4af5197ec") + assert nwTree.moveTreeItem(1) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec", + ] + nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Root Folder + # =========== + + nwTree.setSelectedHandle("73475cb40a568") + assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0 + + # Move novel folder up + assert nwTree.moveTreeItem(-1) is False + nwTree.flushTreeOrder() + assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0 + + # Move novel folder down + assert nwTree.moveTreeItem(1) is True + nwTree.flushTreeOrder() + assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 1 + + # Move novel folder up again + assert nwTree.moveTreeItem(-1) is True + nwTree.flushTreeOrder() + assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0 + + # Clean up + # qtbot.stopForInteraction() + nwGUI.closeProject() + +# END Test testGuiProjTree_MoveItems + + +@pytest.mark.gui +def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): + """Test adding and removing items from the project tree. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + + nwTree = nwGUI.treeView + + # Try to run with no project + assert nwTree.emptyTrash() is False + assert nwTree.deleteItem() is False + + # Create a project + nwGUI.theProject.projTree.setSeed(42) + prjDir = os.path.join(fncDir, "project") + assert nwGUI.newProject({"projPath": prjDir}) is True + + # Try emptying the trash already now, when there is no trash folder + assert nwTree.emptyTrash() is False + + # Add some files + nwTree.setSelectedHandle("31489056e0916") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Delete File + # =========== + + # Delete item without focus -> blocked + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) + nwTree.setSelectedHandle("41cfc0d1f2d12") + caplog.clear() + assert nwTree.deleteItem() is False + assert "blocked" in caplog.text + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) + + # No selection made + nwTree.clearSelection() + caplog.clear() + assert nwTree.deleteItem() is False + assert "no item to delete" in caplog.text + + # Not a valid handle + nwTree.clearSelection() + caplog.clear() + assert nwTree.deleteItem("0000000000000") is False + assert "Could not find tree item" in caplog.text + + # Block adding trash folder + funcPointer = nwTree._addTrashRoot + nwTree._addTrashRoot = lambda *a: None + assert nwTree.deleteItem("41cfc0d1f2d12") is False + nwTree._addTrashRoot = funcPointer + + # Delete last two documents, which also adds the trash folder + assert nwTree.deleteItem("41cfc0d1f2d12") is True + assert nwTree.deleteItem("031b4af5197ec") is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19" + ] + trashHandle = nwGUI.theProject.projTree.trashRoot() + assert nwTree.getTreeFromHandle(trashHandle) == [ + trashHandle, "41cfc0d1f2d12", "031b4af5197ec" + ] + + # Delete the first file again (permanent), and ask for permission + # Also open the document in the editor, which should trigger a close + assert os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd")) + assert "41cfc0d1f2d12" in nwGUI.theProject.projTree + assert nwGUI.docEditor.docHandle() is None + assert nwGUI.openDocument("41cfc0d1f2d12") is True + assert nwGUI.docEditor.docHandle() == "41cfc0d1f2d12" + assert nwTree.deleteItem("41cfc0d1f2d12") is True + assert nwGUI.docEditor.docHandle() is None + assert not os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd")) + assert "41cfc0d1f2d12" not in nwGUI.theProject.projTree + assert nwTree.getTreeFromHandle(trashHandle) == [ + trashHandle, "031b4af5197ec" + ] + + # Try to delete the second document, but block the deletion + with monkeypatch.context() as mp: + mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False) + assert nwTree.deleteItem("031b4af5197ec") is False + + # Delete proper, and skip asking for permission + assert os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd")) + assert "031b4af5197ec" in nwGUI.theProject.projTree + assert nwTree.deleteItem("031b4af5197ec", alreadyAsked=True) is True + assert not os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd")) + assert "031b4af5197ec" not in nwGUI.theProject.projTree + assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] + + # Delete Folder/Root + # ================== + + # Deleting non-empty folders is blocked + assert nwTree.deleteItem("31489056e0916") is False # Folder + assert nwTree.deleteItem("73475cb40a568") is False # Root + + # Add a folder we can delete + nwTree.setSelectedHandle("71ee45a3c0db9") # Character Root + assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert "2fca346db6561" in nwGUI.theProject.projTree + + # Try to delete, but block parent item lookup + with monkeypatch.context() as mp: + mp.setattr("PyQt5.QtWidgets.QTreeWidgetItem.parent", lambda *a: None) + caplog.clear() + assert nwTree.deleteItem("2fca346db6561") is False + assert "Could not delete folder" in caplog.text + assert "2fca346db6561" in nwGUI.theProject.projTree + + # Delete folder properly + assert nwTree.deleteItem("2fca346db6561") is True + assert "2fca346db6561" not in nwGUI.theProject.projTree + + # Delete the Character root + assert nwTree.deleteItem("71ee45a3c0db9") is True + assert "71ee45a3c0db9" not in nwGUI.theProject.projTree + + # Empty Trash + # =========== + + # Try to empty trash that is already empty + caplog.clear() + assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] + assert nwTree.emptyTrash() is False + assert "already empty" in caplog.text + + # Move the two remaining scene documents to trash + assert nwTree.deleteItem("0e17daca5f3e1") is True + assert nwTree.deleteItem("1a6562590ef19") is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9" + ] + assert nwTree.getTreeFromHandle(trashHandle) == [ + trashHandle, "0e17daca5f3e1", "1a6562590ef19" + ] + + # Empty trash, but select no on question + with monkeypatch.context() as mp: + mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + assert nwTree.emptyTrash() is False + + # Empty the trash proper + nwTree._setTreeChanged(False) + assert nwTree.emptyTrash() is True + assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] + assert nwTree._treeChanged is True + + # Clean up + # qtbot.stopForInteraction() + nwGUI.closeProject() + +# END Test testGuiProjTree_DeleteItems From 50db7fcddbc49f36930fea4afa7a8a3794778f7f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 17:49:47 +0200 Subject: [PATCH 035/179] Simplify updating an item after it has been moved --- novelwriter/constants.py | 11 +---- novelwriter/core/item.py | 38 ++++++++++++--- novelwriter/core/project.py | 4 +- novelwriter/dialogs/itemeditor.py | 6 +-- novelwriter/gui/projtree.py | 73 +++++++++------------------- novelwriter/guimain.py | 3 +- tests/test_core/test_core_project.py | 2 +- 7 files changed, 62 insertions(+), 75 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 796d0145..dbdc3664 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -25,7 +25,7 @@ along with this program. If not, see . from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP -from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwOutline +from novelwriter.enum import nwItemClass, nwItemLayout, nwOutline def trConst(tString): @@ -51,18 +51,9 @@ class nwConst(): class nwLists(): """Lists used for grouping various other constants. """ - # Regular user-accessible item types - REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE} - - # Item classes where the full list of novel layouts are allowed - CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE} - # Item classes which do not require items to have same class FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH} - # Deprecated nwItemLayout entries - DEP_LAYOUT = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") - # END Class nwLists diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 893b3965..3a741f62 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -31,10 +31,13 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.common import ( checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified ) -from novelwriter.constants import nwLabels, nwLists, trConst +from novelwriter.constants import nwLabels, trConst logger = logging.getLogger(__name__) +# Deprecated layout labels +DEP_LAYOUTS = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") + class NWItem(): @@ -282,11 +285,27 @@ class NWItem(): return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, "")) + def isNovelLike(self): + """Returns true if the item is of a novel-like class. + """ + return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE) + + def documentAllowed(self): + """Returns true if the item is allowed to be of document layout. + """ + return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH) + + def isInactive(self): + """Returns true if the item is in the inactive parts of the + project. + """ + return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH) + def getImportStatus(self): """Return the relevant importance or status label and icon for the current item based on its class. """ - if self._class in nwLists.CLS_NOVEL: + if self.isNovelLike(): stName = self.theProject.statusItems.name(self._status) stIcon = self.theProject.statusItems.icon(self._status) else: @@ -298,7 +317,7 @@ class NWItem(): """Update the importance or status value based on class. This is a wrapper setter for setStatus and setImport. """ - if self._class in nwLists.CLS_NOVEL: + if self.isNovelLike(): self.setStatus(value) else: self.setImport(value) @@ -312,9 +331,14 @@ class NWItem(): # Only update for child items self.setClass(itemClass) - if self._class in nwLists.CLS_NOVEL: - self._layout = nwItemLayout.DOCUMENT - else: + if self._layout == nwItemLayout.NO_LAYOUT: + # If no layout is set, pick one + if self.isNovelLike(): + self._layout = nwItemLayout.DOCUMENT + else: + self._layout = nwItemLayout.NOTE + elif not self.documentAllowed(): + # Change layout to note if it is not in an allowed folder self._layout = nwItemLayout.NOTE if self._status is None: @@ -411,7 +435,7 @@ class NWItem(): self._layout = itemLayout elif isItemLayout(itemLayout): self._layout = nwItemLayout[itemLayout] - elif itemLayout in nwLists.DEP_LAYOUT: + elif itemLayout in DEP_LAYOUTS: self._layout = nwItemLayout.DOCUMENT else: logger.error("Unrecognised item layout '%s'", itemLayout) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 28aba590..0d24d74c 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -46,7 +46,7 @@ from novelwriter.common import ( checkString, checkBool, checkInt, isHandle, formatTimeStamp, makeFileNameSafe, hexToInt, simplified ) -from novelwriter.constants import nwLists, trConst, nwFiles, nwLabels +from novelwriter.constants import trConst, nwFiles, nwLabels logger = logging.getLogger(__name__) @@ -1197,7 +1197,7 @@ class NWProject(): self.statusItems.resetCounts() self.importItems.resetCounts() for nwItem in self.projTree: - if nwItem.itemClass in nwLists.CLS_NOVEL: + if nwItem.isNovelLike(): self.statusItems.increment(nwItem.itemStatus) else: self.importItems.increment(nwItem.itemImport) diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py index 84a0db14..b5faec0d 100644 --- a/novelwriter/dialogs/itemeditor.py +++ b/novelwriter/dialogs/itemeditor.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.enum import nwItemLayout, nwItemType -from novelwriter.constants import trConst, nwLists, nwLabels +from novelwriter.constants import trConst, nwLabels from novelwriter.gui.custom import QSwitch logger = logging.getLogger(__name__) @@ -74,7 +74,7 @@ class GuiItemEditor(QDialog): # Item Status self.editStatus = QComboBox() self.editStatus.setMinimumWidth(mVd) - if self.theItem.itemClass in nwLists.CLS_NOVEL: + if self.theItem.isNovelLike(): for key, entry in self.theProject.statusItems.items(): self.editStatus.addItem(entry["icon"], entry["name"], key) @@ -95,7 +95,7 @@ class GuiItemEditor(QDialog): self.editLayout.setMinimumWidth(mVd) validLayouts = [] if self.theItem.itemType == nwItemType.FILE: - if self.theItem.itemClass in nwLists.CLS_NOVEL: + if self.theItem.documentAllowed(): validLayouts.append(nwItemLayout.DOCUMENT) validLayouts.append(nwItemLayout.NOTE) else: diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 5fdf7fcc..c75a0f58 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -206,7 +206,7 @@ class GuiProjectTree(QTreeWidget): # Add the file or folder if itemType == nwItemType.FILE: - if pItem.itemClass in nwLists.CLS_NOVEL: + if pItem.isNovelLike(): tHandle = self.theProject.newFile(self.tr("New Document"), sHandle) else: tHandle = self.theProject.newFile(self.tr("New Note"), sHandle) @@ -495,10 +495,7 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) - self._updateItemParent(tHandle) - self.propagateCount(tHandle, wCount) - - self.theIndex.deleteHandle(tHandle) + self._postItemMove(tHandle, wCount) self._recordLastMove(trItemS, trItemP, tIndex) self._setTreeChanged(True) @@ -661,9 +658,7 @@ class GuiProjectTree(QTreeWidget): movItem = parItem.takeChild(srcIndex) dstItem.insertChild(dstIndex, movItem) - snItem = self.theProject.projTree[sHandle] - dnItem = self.theProject.projTree[dHandle] - self._postItemMove(sHandle, snItem, dnItem, wCount) + self._postItemMove(sHandle, wCount) self.clearSelection() movItem.setSelected(True) @@ -812,7 +807,7 @@ class GuiProjectTree(QTreeWidget): logger.debug("Drag'n'drop of item '%s' accepted", sHandle) self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) - self._postItemMove(sHandle, snItem, dnItem, wCount) + self._postItemMove(sHandle, wCount) self._recordLastMove(sItem, pItem, pIndex) else: @@ -828,40 +823,37 @@ class GuiProjectTree(QTreeWidget): # Internal Functions ## - def _postItemMove(self, sHandle, snItem, dnItem, wCount): + def _postItemMove(self, tHandle, wCount): """Run various maintenance tasks for a moved item. """ - isFile = snItem.itemType == nwItemType.FILE - isSame = snItem.itemClass == dnItem.itemClass - onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile + trItemS = self._getTreeItem(tHandle) + nwItemS = self.theProject.projTree[tHandle] + trItemP = trItemS.parent() + if trItemP is None: + logger.error("Failed to find new parent item of '%s'", tHandle) + return False - self._updateItemParent(sHandle) + pHandle = trItemP.data(self.C_NAME, Qt.UserRole) + nwItemS.setParent(pHandle) + self.theProject.projTree.updateItemData(tHandle) + self.setTreeItemValues(tHandle) + self.propagateCount(tHandle, wCount) - # 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 or onFree): - logger.debug( - "Item '%s' class has been changed from '%s' to '%s'", - sHandle, snItem.itemClass.name, dnItem.itemClass.name - ) - snItem.setClass(dnItem.itemClass) - self.setTreeItemValues(sHandle) - - self.propagateCount(sHandle, wCount) + logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle) # The items dropped into archive or trash should be removed # from the project index, for all other items, we rescan the # file to ensure the index is up to date. - if onFree: - self.theIndex.deleteHandle(sHandle) + if nwItemS.isInactive(): + self.theIndex.deleteHandle(tHandle) else: - self.theIndex.reIndexHandle(sHandle) + self.theIndex.reIndexHandle(tHandle) # Trigger dependent updates self._setTreeChanged(True) - self._emitItemChange(sHandle) + self._emitItemChange(tHandle) - return + return True def _getTreeItem(self, tHandle): """Returns the QTreeWidgetItem of a given item handle. @@ -961,25 +953,6 @@ class GuiProjectTree(QTreeWidget): return trItem - def _updateItemParent(self, tHandle): - """Update the parent handle of an item so that the information - in the project is consistent with the treeView. - """ - trItemS = self._getTreeItem(tHandle) - nwItemS = self.theProject.projTree[tHandle] - trItemP = trItemS.parent() - 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) - - logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle) - - return True - def _setTreeChanged(self, theState): """Set the tree change flag, and propagate to the project. """ @@ -994,7 +967,7 @@ class GuiProjectTree(QTreeWidget): """ if self.theProject.projTree.checkType(tHandle, nwItemType.FILE): nwItem = self.theProject.projTree[tHandle] - if nwItem.itemClass in nwLists.CLS_NOVEL: + if nwItem.isNovelLike(): self.novelItemChanged.emit() else: self.noteItemChanged.emit() diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index c21fc091..fabd3867 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -55,7 +55,6 @@ from novelwriter.enum import ( nwItemType, nwItemClass, nwAlert, nwWidget, nwState ) from novelwriter.common import getGuiItem, hexToInt -from novelwriter.constants import nwLists logger = logging.getLogger(__name__) @@ -848,7 +847,7 @@ class GuiMain(QMainWindow): tItem = self.theProject.projTree[tHandle] if tItem is None: return False - if tItem.itemType not in nwLists.REG_TYPES: + if tItem.itemType == nwItemType.NO_TYPE: return False logger.verbose("Requesting change to item '%s'", tHandle) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index b584679d..8a962268 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -1076,7 +1076,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): assert oItem.itemParent == "b3643d0f92e32" assert oItem.itemClass == nwItemClass.NOVEL assert oItem.itemType == nwItemType.FILE - assert oItem.itemLayout == nwItemLayout.DOCUMENT + assert oItem.itemLayout == nwItemLayout.NOTE assert theProject.saveProject(nwLipsum) assert theProject.closeProject() From 3f4356467ef9f9ec580091eaaff9d4430e07b736 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 18:32:22 +0200 Subject: [PATCH 036/179] Relax moving restrictions in project tree, and improve test coverage of NWItem class --- novelwriter/core/item.py | 9 ++- novelwriter/gui/projtree.py | 18 ++--- tests/test_core/test_core_item.py | 115 +++++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 12 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 3a741f62..0178c512 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -259,7 +259,7 @@ class NWItem(): return ## - # Methods + # Lookup Methods ## def describeMe(self, hLevel=None): @@ -296,8 +296,7 @@ class NWItem(): return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH) def isInactive(self): - """Returns true if the item is in the inactive parts of the - project. + """Returns true if the item is in an inactive class. """ return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH) @@ -313,6 +312,10 @@ class NWItem(): stIcon = self.theProject.importItems.icon(self._import) return stName, stIcon + ## + # Special Setters + ## + def setImportStatus(self, value): """Update the importance or status value based on class. This is a wrapper setter for setStatus and setImport. diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index c75a0f58..baefcfea 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.constants import trConst, nwLists, nwLabels +from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) @@ -790,21 +790,23 @@ class GuiProjectTree(QTreeWidget): if pItem is not None: pIndex = pItem.indexOfChild(sItem) - wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) + # Determine if the drag and drop is allowed: + # - Files can be moved anywhere + # - Folders can only be moved within the same root folder + # - Root folders cannot be moved at all + # - Items cannot be dropped on top of a file (moved inside) + isFile = snItem.itemType == nwItemType.FILE isRoot = snItem.itemType == nwItemType.ROOT onFile = dnItem.itemType == nwItemType.FILE + inSame = snItem.itemRoot == dnItem.itemRoot - isSame = snItem.itemClass == dnItem.itemClass - isNone = snItem.itemClass == nwItemClass.NO_CLASS - isNote = snItem.itemLayout == nwItemLayout.NOTE - onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile - - allowDrop = isSame or isNone or isNote or onFree + allowDrop = inSame or isFile allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile) if allowDrop and not isRoot: logger.debug("Drag'n'drop of item '%s' accepted", sHandle) + wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) self._postItemMove(sHandle, wCount) diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index b86fe968..8f65bb05 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -220,6 +220,7 @@ def testCoreItem_Methods(mockGUI): # Status + Icon # ============= + theItem.setType("FILE") theItem.setStatus("Note") theItem.setImport("Minor") @@ -229,11 +230,19 @@ def testCoreItem_Methods(mockGUI): assert stT == "Note" assert isinstance(stI, QIcon) + theItem.setImportStatus("Draft") + stT, stI = theItem.getImportStatus() + assert stT == "Draft" + theItem.setClass("CHARACTER") stT, stI = theItem.getImportStatus() assert stT == "Minor" assert isinstance(stI, QIcon) + theItem.setImportStatus("Major") + stT, stI = theItem.getImportStatus() + assert stT == "Major" + # Representation # ============== @@ -275,6 +284,8 @@ def testCoreItem_TypeSetter(mockGUI): assert theItem.itemType == nwItemType.FILE theItem.setType("TRASH") assert theItem.itemType == nwItemType.TRASH + + # Alternative theItem.setType(nwItemType.ROOT) assert theItem.itemType == nwItemType.ROOT @@ -294,28 +305,74 @@ def testCoreItem_ClassSetter(mockGUI): assert theItem.itemClass == nwItemClass.NO_CLASS theItem.setClass("NONSENSE") assert theItem.itemClass == nwItemClass.NO_CLASS + theItem.setClass("NO_CLASS") assert theItem.itemClass == nwItemClass.NO_CLASS + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is True + theItem.setClass("NOVEL") assert theItem.itemClass == nwItemClass.NOVEL + assert theItem.isNovelLike() is True + assert theItem.documentAllowed() is True + assert theItem.isInactive() is False + theItem.setClass("PLOT") assert theItem.itemClass == nwItemClass.PLOT + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("CHARACTER") assert theItem.itemClass == nwItemClass.CHARACTER + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("WORLD") assert theItem.itemClass == nwItemClass.WORLD + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("TIMELINE") assert theItem.itemClass == nwItemClass.TIMELINE + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("OBJECT") assert theItem.itemClass == nwItemClass.OBJECT + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("ENTITY") assert theItem.itemClass == nwItemClass.ENTITY + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("CUSTOM") assert theItem.itemClass == nwItemClass.CUSTOM + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("ARCHIVE") assert theItem.itemClass == nwItemClass.ARCHIVE + assert theItem.isNovelLike() is True + assert theItem.documentAllowed() is True + assert theItem.isInactive() is True + theItem.setClass("TRASH") assert theItem.itemClass == nwItemClass.TRASH + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is True + assert theItem.isInactive() is True + + # Alternative theItem.setClass(nwItemClass.NOVEL) assert theItem.itemClass == nwItemClass.NOVEL @@ -344,13 +401,69 @@ def testCoreItem_LayoutSetter(mockGUI): theItem.setLayout("NOTE") assert theItem.itemLayout == nwItemLayout.NOTE - # Alternatives + # Alternative theItem.setLayout(nwItemLayout.NOTE) assert theItem.itemLayout == nwItemLayout.NOTE # END Test testCoreItem_LayoutSetter +@pytest.mark.core +def testCoreItem_ClassDefaults(mockGUI): + """Test the setter for the default values. + """ + theProject = NWProject(mockGUI) + theItem = NWItem(theProject) + + # Root items should not have their class updated + theItem.setParent(None) + theItem.setClass(nwItemClass.NO_CLASS) + assert theItem.itemClass == nwItemClass.NO_CLASS + + theItem.setClassDefaults(nwItemClass.NOVEL) + assert theItem.itemClass == nwItemClass.NO_CLASS + + # Non-root items should have their class updated + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + assert theItem.itemClass == nwItemClass.NO_CLASS + + theItem.setClassDefaults(nwItemClass.NOVEL) + assert theItem.itemClass == nwItemClass.NOVEL + + # Non-layout items should have their layout set based on class + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + theItem.setLayout(nwItemLayout.NO_LAYOUT) + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + + theItem.setClassDefaults(nwItemClass.NOVEL) + assert theItem.itemLayout == nwItemLayout.DOCUMENT + + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + theItem.setLayout(nwItemLayout.NO_LAYOUT) + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + + theItem.setClassDefaults(nwItemClass.PLOT) + assert theItem.itemLayout == nwItemLayout.NOTE + + # If documents are not allowed in that class, the layout should be changed + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + theItem.setLayout(nwItemLayout.DOCUMENT) + assert theItem.itemLayout == nwItemLayout.DOCUMENT + + theItem.setClassDefaults(nwItemClass.PLOT) + assert theItem.itemLayout == nwItemLayout.NOTE + + # In all cases, status and importance should no longer be None + assert theItem.itemStatus is not None + assert theItem.itemImport is not None + +# END Test testCoreItem_ClassDefaults + + @pytest.mark.core def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): """Test packing and unpacking XML objects for the NWItem class. From 4eec2926cb6d8acc49657ab436b6e6430c77a2ad Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 18:38:25 +0200 Subject: [PATCH 037/179] Clean up unused code and add more comments --- novelwriter/constants.py | 9 --------- novelwriter/gui/projtree.py | 7 ++++++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index dbdc3664..30071606 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -48,15 +48,6 @@ class nwConst(): # END Class nwConst -class nwLists(): - """Lists used for grouping various other constants. - """ - # Item classes which do not require items to have same class - FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH} - -# END Class nwLists - - class nwRegEx(): FMT_EI = r"(? Date: Sun, 17 Apr 2022 19:15:00 +0200 Subject: [PATCH 038/179] Make better use of the new NWItem functions and do some cleanup --- novelwriter/core/index.py | 9 ++-- novelwriter/core/item.py | 71 +++++++++++++++---------------- novelwriter/core/tree.py | 15 ------- novelwriter/gui/projtree.py | 3 +- novelwriter/tools/build.py | 5 +-- sample/nwProject.nwx | 60 +++++++++++++------------- tests/test_core/test_core_tree.py | 7 +-- 7 files changed, 71 insertions(+), 99 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 45ae9acc..9b8dd47c 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -30,7 +30,7 @@ import logging from time import time -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout +from novelwriter.enum import nwItemType, nwItemLayout from novelwriter.error import logException from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode from novelwriter.core.document import NWDoc @@ -227,11 +227,8 @@ class NWIndex(): if theItem.itemParent is None: logger.info("Not indexing orphaned item '%s'", tHandle) return False - if theItem.itemClass == nwItemClass.TRASH: - logger.debug("Not indexing trash item '%s'", tHandle) - return False - if theItem.itemClass == nwItemClass.ARCHIVE: - logger.debug("Not indexing archived item '%s'", tHandle) + if theItem.isInactive(): + logger.debug("Not indexing inactive item '%s'", tHandle) return False itemClass = theItem.itemClass diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 0178c512..f6d93b36 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -35,9 +35,6 @@ from novelwriter.constants import nwLabels, trConst logger = logging.getLogger(__name__) -# Deprecated layout labels -DEP_LAYOUTS = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") - class NWItem(): @@ -365,33 +362,33 @@ class NWItem(): self._name = "" return - def setHandle(self, tHandle): + def setHandle(self, handle): """Set the item handle, and ensure it is valid. """ - if isHandle(tHandle): - self._handle = tHandle + if isHandle(handle): + self._handle = handle else: self._handle = None return - def setParent(self, pHandle): + def setParent(self, handle): """Set the parent handle, and ensure it is valid. """ - if pHandle is None: + if handle is None: self._parent = None - elif isHandle(pHandle): - self._parent = pHandle + elif isHandle(handle): + self._parent = handle else: self._parent = None return - def setRoot(self, rHandle): + def setRoot(self, handle): """Set the root handle, and ensure it is valid. """ - if rHandle is None: + if handle is None: self._root = None - elif isHandle(rHandle): - self._root = rHandle + elif isHandle(handle): + self._root = handle else: self._root = None return @@ -404,59 +401,59 @@ class NWItem(): self._order = checkInt(order, 0) return - def setType(self, itemType): + def setType(self, value): """Set the item type from either a proper nwItemType, or set it from a string representing an nwItemType. """ - if isinstance(itemType, nwItemType): - self._type = itemType - elif isItemType(itemType): - self._type = nwItemType[itemType] + if isinstance(value, nwItemType): + self._type = value + elif isItemType(value): + self._type = nwItemType[value] else: - logger.error("Unrecognised item type '%s'", itemType) + logger.error("Unrecognised item type '%s'", value) self._type = nwItemType.NO_TYPE return - def setClass(self, itemClass): + def setClass(self, value): """Set the item class from either a proper nwItemClass, or set it from a string representing an nwItemClass. """ - if isinstance(itemClass, nwItemClass): - self._class = itemClass - elif isItemClass(itemClass): - self._class = nwItemClass[itemClass] + if isinstance(value, nwItemClass): + self._class = value + elif isItemClass(value): + self._class = nwItemClass[value] else: - logger.error("Unrecognised item class '%s'", itemClass) + logger.error("Unrecognised item class '%s'", value) self._class = nwItemClass.NO_CLASS return - def setLayout(self, itemLayout): + def setLayout(self, value): """Set the item layout from either a proper nwItemLayout, or set it from a string representing an nwItemLayout. """ - if isinstance(itemLayout, nwItemLayout): - self._layout = itemLayout - elif isItemLayout(itemLayout): - self._layout = nwItemLayout[itemLayout] - elif itemLayout in DEP_LAYOUTS: + if isinstance(value, nwItemLayout): + self._layout = value + elif isItemLayout(value): + self._layout = nwItemLayout[value] + elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"): self._layout = nwItemLayout.DOCUMENT else: - logger.error("Unrecognised item layout '%s'", itemLayout) + logger.error("Unrecognised item layout '%s'", value) self._layout = nwItemLayout.NO_LAYOUT return - def setStatus(self, itemStatus): + def setStatus(self, value): """Set the item status by looking it up in the valid status items of the current project. """ - self._status = self.theProject.statusItems.check(itemStatus) + self._status = self.theProject.statusItems.check(value) return - def setImport(self, itemImport): + def setImport(self, value): """Set the item importance by looking it up in the valid import items of the current project. """ - self._import = self.theProject.importItems.check(itemImport) + self._import = self.theProject.importItems.check(value) return def setExpanded(self, state): diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 2983f88a..29532c7d 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -292,13 +292,6 @@ class NWTree(): return True return False - def isTrashRoot(self, tHandle): - """Check if a handle is the trash folder. - """ - if self._trashRoot is None: - return False - return tHandle == self._trashRoot - def trashRoot(self): """Returns the handle of the trash folder, or None if there isn't one. @@ -307,14 +300,6 @@ class NWTree(): return self._trashRoot return None - def archiveRoot(self): - """Returns the handle of the archive folder, or None if there - isn't one. - """ - if self._archRoot: - return self._archRoot - return None - def findRoot(self, theClass): """Find the first root item for a given class. """ diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index cce1c91b..23238b84 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -444,8 +444,7 @@ class GuiProjectTree(QTreeWidget): logger.error("Could not delete item") return False - pHandle = nwItemS.itemParent - if self.theProject.projTree.isTrashRoot(pHandle): + if self.theProject.projTree.isTrash(tHandle): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. doPermanent = False diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index feb1662d..d84a0d4c 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -785,10 +785,7 @@ class GuiBuildNovel(QDialog): isNone = theItem.itemType != nwItemType.FILE isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT - isNone |= theItem.itemClass == nwItemClass.NO_CLASS - isNone |= theItem.itemClass == nwItemClass.ARCHIVE - isNone |= theItem.itemClass == nwItemClass.TRASH - isNone |= theItem.itemParent == self.theProject.projTree.trashRoot() + isNone |= theItem.isInactive() isNone |= theItem.itemParent is None isNote = theItem.itemLayout == nwItemLayout.NOTE isNovel = not isNone and not isNote diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 002cea43..23660870 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1303 + 1306 199 - 65049 + 65149 False @@ -33,7 +33,7 @@
- New + New Notes Started 1st Draft @@ -42,110 +42,110 @@ Finished - None + None Minor Major Main
- + Novel - + Title Page - + Page - + Part One - + A Folder - + Chapter One - + Making a Scene - + Another Scene - + Interlude - + A Note on Structure - + Chapter Two - + We Found John! - + Characters - + Main Characters - + John Smith - + Jane Smith - + Locations - + Earth - + Space - + Mars - + Archive - + Scenes - + Old File - + Trash - + Delete Me! diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 3e8aeeca..3b35abd5 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -126,8 +126,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems): # Check for archive and trash folders assert theTree.trashRoot() is None - assert theTree.archiveRoot() is None - assert theTree.isTrashRoot("a000000000003") is False aHandles = [] for tHandle, pHandle, nwItem in mockItems: @@ -152,8 +150,8 @@ def testCoreTree_BuildTree(mockGUI, mockItems): # Check that we have the correct archive and trash folders assert theTree.trashRoot() == "a000000000003" - assert theTree.archiveRoot() == "a000000000002" - assert theTree.isTrashRoot("a000000000003") is True + assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002" + assert theTree.isTrash("a000000000003") is True assert theTree.isRoot("a000000000002") is True # Check the isTrash function @@ -221,7 +219,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems): del theTree["a000000000002"] assert len(theTree) == len(mockItems) - 2 assert "a000000000002" not in theTree - assert theTree.archiveRoot() is None del theTree["a000000000003"] assert len(theTree) == len(mockItems) - 3 From ca76cb2af3f0ef62039b8efb6d35c547c560c6f9 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 20 Apr 2022 17:15:55 +0200 Subject: [PATCH 039/179] Make a couple of random, minor fixes --- novelwriter/config.py | 8 ++++---- novelwriter/error.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index c5159493..7fb334c2 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -978,12 +978,12 @@ class Config: """ try: import enchant # noqa: F401 - self.hasEnchant = True - logger.debug("Checking package 'pyenchant': OK") - except Exception: + except ImportError: self.hasEnchant = False logger.debug("Checking package 'pyenchant': Missing") - + else: + self.hasEnchant = True + logger.debug("Checking package 'pyenchant': OK") return # END Class Config diff --git a/novelwriter/error.py b/novelwriter/error.py index 0dec56e8..2ceae1e3 100644 --- a/novelwriter/error.py +++ b/novelwriter/error.py @@ -87,6 +87,8 @@ class NWErrorMessage(QDialog): self.mainBox.addWidget(self.btnBox, 2, 0, 1, 2) self.mainBox.setSpacing(16) + # Pick a random window title from a set of error messages by + # Hex, the computer, from Discworld self.setWindowTitle([ "+++ Out of Cheese Error +++", "+++ Divide by Cucumber Error +++", From 8320aab1bf534e2d9c669dd8a61af2994a56a9c3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:44:45 +0200 Subject: [PATCH 040/179] Use the same method to generate item handles as for status keys --- novelwriter/core/tree.py | 41 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 29532c7d..a158c37a 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -24,11 +24,10 @@ along with this program. If not, see . """ import os +import random import logging -from time import time from lxml import etree -from hashlib import sha256 from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.error import logException @@ -55,9 +54,6 @@ class NWTree(): self._theIndex = 0 # The current iterator index self._treeChanged = False # True if tree structure has changed - self._handleSeed = None # Used for generating handles for testing - self._handleCount = 0 # A counter that is added to the handle generator - return ## @@ -340,14 +336,6 @@ class NWTree(): return - def setSeed(self, theSeed): - """Used for debugging! - Sets a seed for generating handles so that they always come out - in a predictable order. - """ - self._handleSeed = theSeed - return - def setFileItemLayout(self, tHandle, itemLayout): """Set the nwItemLayout for a specific file. """ @@ -474,29 +462,16 @@ class NWTree(): self.theProject.setProjectChanged(True) return - def _makeHandle(self, addSeed=""): + def _makeHandle(self): """Generate a unique item handle. In the event that the key - already exists, salt the seed and generate a new handle. - A key collision is very unlikely to be caused by the truncation - of the sha256 hash to 13 characters. Assuming it is near-random, - it will on average happen every 4.5^15 times. However, the clock - seed is likely to occasionally generate a collision if the - handle requests come faster than the clock resolution. + already exists, generate a new one. """ - if self._handleSeed is None: - newSeed = "%s_%d_%s" % (str(time()), self._handleCount, addSeed) - self._handleCount += 1 - else: - # This is used for debugging - newSeed = str(self._handleSeed) - self._handleSeed += 1 - - logger.verbose("Generating handle with seed '%s'", newSeed) - itemHandle = sha256(newSeed.encode()).hexdigest()[0:13] - if itemHandle in self._projTree: + logger.verbose("Generating new handle") + handle = f"{random.getrandbits(52):013x}" + if handle in self._projTree: logger.warning("Duplicate handle encountered! Retrying ...") - itemHandle = self._makeHandle(addSeed+"!") + handle = self._makeHandle() - return itemHandle + return handle # END Class NWTree From e32eb21fbf492c5f6f5950e5d95f9b379decdc03 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:46:53 +0200 Subject: [PATCH 041/179] Add a mock random number generator for testing --- tests/conftest.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a11db7e2..c6634f7c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,8 +24,6 @@ import sys import pytest import shutil -from dataclasses import dataclass - from mock import MockGuiMain from tools import cleanProject @@ -184,6 +182,26 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf): return +## +# Python Objects +## + +@pytest.fixture(scope="function") +def mockRnd(monkeypatch): + """Create a mock random number generator that just counts upwards + from 0. This one will generate status/importance flags and handles + in a predictable sequence. + """ + def rnd(n): + for x in range(n): + yield x + + gen = rnd(1000) + monkeypatch.setattr("random.getrandbits", lambda *a: next(gen)) + + return + + ## # Temp Project Folders ## @@ -252,25 +270,6 @@ def nwOldProj(tmpDir): return -## -# Data Fixtures -## - -@dataclass -class TestConst: - - statusKeys = ["sa3b179", "s1c8031", "s06671a", "sbdd640"] - importKeys = ["i466852", "i3eb13b", "i392456", "i23b8c1"] - - -@pytest.fixture(scope="session") -def constData(): - """A named tuple of known contstant values. For those that depend on - the random number generator, they assume the seed is 42. - """ - return TestConst() - - @pytest.fixture(scope="session") def ipsumText(): """Return five paragraphs of Lorem Ipsum text. From 607f9409d29eb818443e05b6a0d76775b33e65ce Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:50:07 +0200 Subject: [PATCH 042/179] Update tests --- .../coreProject_NewCustomA_nwProject.nwx | 110 ++++----- .../coreProject_NewCustomB_nwProject.nwx | 74 +++--- .../coreProject_NewFile_nwProject.nwx | 58 ++--- .../coreProject_NewMinimal_nwProject.nwx | 52 ++--- .../coreProject_NewRoot_nwProject.nwx | 82 +++---- ...=> guiEditor_Main_Final_000000000000f.nwd} | 2 +- ...=> guiEditor_Main_Final_0000000000020.nwd} | 2 +- ...=> guiEditor_Main_Final_0000000000021.nwd} | 2 +- ...=> guiEditor_Main_Final_0000000000022.nwd} | 2 +- .../guiEditor_Main_Final_nwProject.nwx | 70 +++--- .../guiEditor_Main_Initial_nwProject.nwx | 50 ++-- .../guiProjSettings_Dialog_nwProject.nwx | 50 ++-- tests/test_core/test_core_index.py | 5 - tests/test_core/test_core_item.py | 45 ++-- tests/test_core/test_core_project.py | 132 +++++------ tests/test_core/test_core_status.py | 166 +++++++------- tests/test_core/test_core_tokenizer.py | 1 - tests/test_core/test_core_tree.py | 125 +++++----- tests/test_dialogs/test_dlg_docmerge.py | 28 +-- tests/test_dialogs/test_dlg_docsplit.py | 60 ++--- tests/test_dialogs/test_dlg_itemeditor.py | 72 +++--- tests/test_dialogs/test_dlg_projsettings.py | 23 +- tests/test_gui/test_gui_doceditor.py | 1 - tests/test_gui/test_gui_docviewer.py | 1 - tests/test_gui/test_gui_guimain.py | 71 +++--- tests/test_gui/test_gui_mainmenu.py | 13 +- tests/test_gui/test_gui_noveltree.py | 1 - tests/test_gui/test_gui_projtree.py | 213 +++++++++--------- tests/test_gui/test_gui_statusbar.py | 5 +- tests/test_tools/test_tools_lipsum.py | 5 +- 30 files changed, 740 insertions(+), 781 deletions(-) rename tests/reference/{guiEditor_Main_Final_0e17daca5f3e1.nwd => guiEditor_Main_Final_000000000000f.nwd} (95%) rename tests/reference/{guiEditor_Main_Final_1a6562590ef19.nwd => guiEditor_Main_Final_0000000000020.nwd} (71%) rename tests/reference/{guiEditor_Main_Final_031b4af5197ec.nwd => guiEditor_Main_Final_0000000000021.nwd} (74%) rename tests/reference/{guiEditor_Main_Final_41cfc0d1f2d12.nwd => guiEditor_Main_Final_0000000000022.nwd} (74%) diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 9152e3b6..8747363b 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -29,110 +29,110 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - + - Novel + Novel - + - Plot + Plot - + - Characters + Characters - + - Locations + Locations - + - Timeline + Timeline - + - Objects + Objects - + - Entities + Entities - + - Title Page + Title Page - + - Chapter 1 + Chapter 1 - + - Chapter 1 + Chapter 1 - + - Scene 1.1 + Scene 1.1 - + - Scene 1.2 + Scene 1.2 - + - Scene 1.3 + Scene 1.3 - + - Chapter 2 + Chapter 2 - + - Chapter 2 + Chapter 2 - + - Scene 2.1 + Scene 2.1 - + - Scene 2.2 + Scene 2.2 - + - Scene 2.3 + Scene 2.3 - + - Chapter 3 + Chapter 3 - + - Chapter 3 + Chapter 3 - + - Scene 3.1 + Scene 3.1 - + - Scene 3.2 + Scene 3.2 - + - Scene 3.3 + Scene 3.3
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index ef29585b..7f397e85 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -29,74 +29,74 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - + - Novel + Novel - + - Plot + Plot - + - Characters + Characters - + - Locations + Locations - + - Timeline + Timeline - + - Objects + Objects - + - Entities + Entities - + - Title Page + Title Page - + - Scene 1 + Scene 1 - + - Scene 2 + Scene 2 - + - Scene 3 + Scene 3 - + - Scene 4 + Scene 4 - + - Scene 5 + Scene 5 - + - Scene 6 + Scene 6
diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index 13fd7966..86f9cf11 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,58 +27,58 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - + - Novel + Novel - + - Plot + Plot - + - Characters + Characters - + - World + World - + - Title Page + Title Page - + - New Chapter + New Chapter - + - New Chapter + New Chapter - + - New Scene + New Scene - + - Hello + Hello - + - Jane + Jane
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 9c58c831..37a5428b 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,9 +1,9 @@ - + New Project - 1 + 2 1 0 @@ -27,50 +27,50 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - + - Novel + Novel - + - Plot + Plot - + - Characters + Characters - + - World + World - + - Title Page + Title Page - + - New Chapter + New Chapter - + - New Chapter + New Chapter - + - New Scene + New Scene
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 264cfa0f..67c49f05 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,82 +27,82 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - + - Novel + Novel - + - Plot + Plot - + - Characters + Characters - + - World + World - + - Title Page + Title Page - + - New Chapter + New Chapter - + - New Chapter + New Chapter - + - New Scene + New Scene - + - Novel + Novel - + - Plot + Plot - + - Character + Character - + - World + World - + - Timeline + Timeline - + - Object + Object - + - Custom1 + Custom1 - + - Custom2 + Custom2
diff --git a/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd b/tests/reference/guiEditor_Main_Final_000000000000f.nwd similarity index 95% rename from tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd rename to tests/reference/guiEditor_Main_Final_000000000000f.nwd index 67886bc1..fcab1110 100644 --- a/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd +++ b/tests/reference/guiEditor_Main_Final_000000000000f.nwd @@ -1,5 +1,5 @@ %%~name: New Scene -%%~path: 31489056e0916/0e17daca5f3e1 +%%~path: 000000000000d/000000000000f %%~kind: NOVEL/DOCUMENT # Novel diff --git a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd b/tests/reference/guiEditor_Main_Final_0000000000020.nwd similarity index 71% rename from tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd rename to tests/reference/guiEditor_Main_Final_0000000000020.nwd index 1da5a713..c0316819 100644 --- a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd +++ b/tests/reference/guiEditor_Main_Final_0000000000020.nwd @@ -1,5 +1,5 @@ %%~name: New Note -%%~path: 71ee45a3c0db9/1a6562590ef19 +%%~path: 000000000000a/0000000000020 %%~kind: CHARACTER/NOTE # Jane Doe diff --git a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd b/tests/reference/guiEditor_Main_Final_0000000000021.nwd similarity index 74% rename from tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd rename to tests/reference/guiEditor_Main_Final_0000000000021.nwd index 6492390b..5dddd23b 100644 --- a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd +++ b/tests/reference/guiEditor_Main_Final_0000000000021.nwd @@ -1,5 +1,5 @@ %%~name: New Note -%%~path: 44cb730c42048/031b4af5197ec +%%~path: 0000000000009/0000000000021 %%~kind: PLOT/NOTE # Main Plot diff --git a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd b/tests/reference/guiEditor_Main_Final_0000000000022.nwd similarity index 74% rename from tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd rename to tests/reference/guiEditor_Main_Final_0000000000022.nwd index 14a58a49..092f832a 100644 --- a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd +++ b/tests/reference/guiEditor_Main_Final_0000000000022.nwd @@ -1,5 +1,5 @@ %%~name: New Note -%%~path: 811786ad1ae74/41cfc0d1f2d12 +%%~path: 000000000000b/0000000000022 %%~kind: WORLD/NOTE # Main Location diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 34f5a0ed..3900c6d2 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,11 +1,11 @@ - + New Project 5 2 - 3 + 4 True @@ -13,7 +13,7 @@ True None True - 0e17daca5f3e1 + 000000000000f None 126 99 @@ -27,66 +27,66 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main
- + - Novel + Novel - + - Title Page + Title Page - + - New Chapter + New Chapter - + - New Chapter + New Chapter - + - New Scene + New Scene - + - Plot + Plot - + - New Note + New Note - + - Characters + Characters - + - New Note + New Note - + - World + World - + - New Note + New Note - + - Trash + Trash
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index d6c8b904..66a4c94e 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,50 +27,50 @@
- New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - + - Novel + Novel - + - Title Page + Title Page - + - New Chapter + New Chapter - + - New Chapter + New Chapter - + - New Scene + New Scene - + - Plot + Plot - + - Characters + Characters - + - World + World
diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index 6c4cefdb..77d01d45 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -33,50 +33,50 @@
- New - Note - Finished - Final + New + Note + Finished + Final - New - Minor - Major - Final + New + Minor + Major + Final - + - Novel + Novel - + - Title Page + Title Page - + - New Chapter + New Chapter - + - New Chapter + New Chapter - + - New Scene + New Scene - + - Plot + Plot - + - Characters + Characters - + - World + World
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index cf7b1687..c3e6ef47 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -43,7 +43,6 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): compFile = os.path.join(refDir, "coreIndex_LoadSave_tagsIndex.json") theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.openProject(nwLipsum) theIndex = NWIndex(theProject) @@ -125,7 +124,6 @@ def testCoreIndex_ScanThis(nwMinimal, mockGUI): """Test the tag scanner function scanThis. """ theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) is True theIndex = NWIndex(theProject) @@ -177,7 +175,6 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI): """Test the tag checker function checkThese. """ theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) is True theIndex = NWIndex(theProject) @@ -254,7 +251,6 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): """Check the index text scanner. """ theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) is True theIndex = NWIndex(theProject) @@ -473,7 +469,6 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): """Check the index data extraction functions. """ theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) is True theIndex = NWIndex(theProject) diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 8f65bb05..572b31d7 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -20,7 +20,6 @@ along with this program. If not, see . """ import pytest -import random from lxml import etree @@ -32,13 +31,15 @@ from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout @pytest.mark.core -def testCoreItem_Setters(mockGUI, constData): +def testCoreItem_Setters(mockGUI, mockRnd): """Test all the simple setters for the NWItem class. """ - random.seed(42) theProject = NWProject(mockGUI) theItem = NWItem(theProject) + statusKeys = ["s000000", "s000001", "s000002", "s000003"] + importKeys = ["i000004", "i000005", "i000006", "i000007"] + # Name theItem.setName("A Name") assert theItem.itemName == "A Name" @@ -94,30 +95,30 @@ def testCoreItem_Setters(mockGUI, constData): # Importance theItem._class = nwItemClass.CHARACTER theItem.setImport("Word") - assert theItem.itemImport == constData.importKeys[0] # Default - for key in constData.importKeys: + assert theItem.itemImport == importKeys[0] # Default + for key in importKeys: theItem.setImport(key) assert theItem.itemImport == key # Status theItem._class = nwItemClass.NOVEL theItem.setStatus("Word") - assert theItem.itemStatus == constData.statusKeys[0] # Default - for key in constData.statusKeys: + assert theItem.itemStatus == statusKeys[0] # Default + for key in statusKeys: theItem.setStatus(key) assert theItem.itemStatus == key # Status/Importance Wrapper theItem._class = nwItemClass.CHARACTER - for key in constData.importKeys: + for key in importKeys: theItem.setImport(key) assert theItem.itemImport == key - assert theItem.itemStatus == constData.statusKeys[3] # Should not change + assert theItem.itemStatus == statusKeys[3] # Should not change theItem._class = nwItemClass.NOVEL - for key in constData.statusKeys: + for key in statusKeys: theItem.setStatus(key) - assert theItem.itemImport == constData.importKeys[3] # Should not change + assert theItem.itemImport == importKeys[3] # Should not change assert theItem.itemStatus == key # Expanded @@ -465,13 +466,15 @@ def testCoreItem_ClassDefaults(mockGUI): @pytest.mark.core -def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): +def testCoreItem_XMLPackUnpack(mockGUI, caplog, mockRnd): """Test packing and unpacking XML objects for the NWItem class. """ - random.seed(42) theProject = NWProject(mockGUI) nwXML = etree.Element("novelWriterXML") + statusKeys = ["s000000", "s000001", "s000002", "s000003"] + importKeys = ["i000004", "i000005", "i000006", "i000007"] + # File # ==== @@ -483,7 +486,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): theItem.setName("A Name") theItem.setClass("NOVEL") theItem.setType("FILE") - theItem.setImport(constData.importKeys[3]) + theItem.setImport(importKeys[3]) theItem.setLayout("NOTE") theItem.setExported(False) theItem.setParaCount(3) @@ -500,7 +503,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): b'type="FILE" class="NOVEL" layout="NOTE">A Name
' b'
' - ) % bytes(constData.importKeys[3], encoding="utf8") + ) % bytes(importKeys[3], encoding="utf8") # Unpack theItem = NWItem(theProject) @@ -517,8 +520,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): assert theItem.itemClass == nwItemClass.NOVEL assert theItem.itemType == nwItemType.FILE assert theItem.itemLayout == nwItemLayout.NOTE - assert theItem.itemStatus == constData.statusKeys[0] # Was None, should now be default - assert theItem.itemImport == constData.importKeys[3] + assert theItem.itemStatus == statusKeys[0] # Was None, should now be default + assert theItem.itemImport == importKeys[3] # Folder # ====== @@ -531,7 +534,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): theItem.setName("A Name") theItem.setClass("NOVEL") theItem.setType("FOLDER") - theItem.setStatus(constData.statusKeys[1]) + theItem.setStatus(statusKeys[1]) theItem.setLayout("NOTE") theItem.setExpanded(True) theItem.setExported(False) @@ -549,7 +552,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): b'type="FOLDER" class="NOVEL">A Name
' b'' - ) % bytes(constData.statusKeys[1], encoding="utf8") + ) % bytes(statusKeys[1], encoding="utf8") # Unpack theItem = NWItem(theProject) @@ -567,8 +570,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): assert theItem.itemClass == nwItemClass.NOVEL assert theItem.itemType == nwItemType.FOLDER assert theItem.itemLayout == nwItemLayout.NO_LAYOUT - assert theItem.itemStatus == constData.statusKeys[1] - assert theItem.itemImport == constData.importKeys[0] # Was None, should now be default + assert theItem.itemStatus == statusKeys[1] + assert theItem.itemImport == importKeys[0] # Was None, should now be default # Errors # ====== diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 8a962268..55c18e65 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -21,7 +21,6 @@ along with this program. If not, see . import os import pytest -import random from shutil import copyfile from zipfile import ZipFile @@ -37,7 +36,7 @@ from novelwriter.constants import nwFiles @pytest.mark.core -def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI): +def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd): """Create a new project from a project wizard dictionary. With default setting, creating a Minimal project. """ @@ -45,9 +44,7 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI): testFile = os.path.join(outDir, "coreProject_NewMinimal_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_NewMinimal_nwProject.nwx") - random.seed(42) theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) # Setting no data should fail assert theProject.newProject({}) is False @@ -86,7 +83,7 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI): @pytest.mark.core -def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI): +def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd): """Create a new project from a project wizard dictionary. Custom type with chapters and scenes. """ @@ -114,9 +111,7 @@ def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI): "numScenes": 3, "chFolders": True, } - random.seed(42) theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.newProject(projData) is True assert theProject.saveProject() is True @@ -129,7 +124,7 @@ def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI): @pytest.mark.core -def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI): +def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI, mockRnd): """Create a new project from a project wizard dictionary. Custom type without chapters, but with scenes. """ @@ -157,9 +152,7 @@ def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI): "numScenes": 6, "chFolders": True, } - random.seed(42) theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.newProject(projData) is True assert theProject.saveProject() is True @@ -186,7 +179,6 @@ def testCoreProject_NewSampleA(fncDir, tmpConf, mockGUI, tmpDir): "popCustom": False, } theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) # Sample set, but no path assert not theProject.newProject({"popSample": True}) @@ -235,7 +227,6 @@ def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir): "popCustom": False, } theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) # Make sure we do not pick up the novelwriter/assets/sample.zip file tmpConf.assetPath = tmpDir @@ -259,16 +250,14 @@ def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir): @pytest.mark.core -def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI): +def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd): """Check that new root folders can be added to the project. """ projFile = os.path.join(fncDir, "nwProject.nwx") testFile = os.path.join(outDir, "coreProject_NewRoot_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx") - random.seed(42) theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.newProject({"projPath": fncDir}) is True assert theProject.setProjectPath(fncDir) is True @@ -297,16 +286,14 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI): @pytest.mark.core -def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI): +def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): """Check that new files can be added to the project. """ projFile = os.path.join(fncDir, "nwProject.nwx") testFile = os.path.join(outDir, "coreProject_NewFile_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx") - random.seed(42) theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.newProject({"projPath": fncDir}) is True assert theProject.setProjectPath(fncDir) is True @@ -686,50 +673,51 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): @pytest.mark.core -def testCoreProject_StatusImport(mockGUI, fncDir, constData): +def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): """Test the status and importance flag handling. """ theProject = NWProject(mockGUI) - random.seed(42) - theProject.projTree.setSeed(42) assert theProject.newProject({"projPath": fncDir}) is True + statusKeys = ["s000008", "s000009", "s00000a", "s00000b"] + importKeys = ["i00000c", "i00000d", "i00000e", "i00000f"] + # Change Status # ============= - theProject.projTree["44cb730c42048"].setStatus("Finished") - theProject.projTree["71ee45a3c0db9"].setStatus("Draft") - theProject.projTree["811786ad1ae74"].setStatus("Note") - theProject.projTree["25fc0e7096fc6"].setStatus("Finished") + theProject.projTree["0000000000014"].setStatus("Finished") + theProject.projTree["0000000000015"].setStatus("Draft") + theProject.projTree["0000000000016"].setStatus("Note") + theProject.projTree["0000000000017"].setStatus("Finished") - assert theProject.projTree["44cb730c42048"].itemStatus == constData.statusKeys[3] - assert theProject.projTree["71ee45a3c0db9"].itemStatus == constData.statusKeys[2] - assert theProject.projTree["811786ad1ae74"].itemStatus == constData.statusKeys[1] - assert theProject.projTree["25fc0e7096fc6"].itemStatus == constData.statusKeys[3] + assert theProject.projTree["0000000000014"].itemStatus == statusKeys[3] + assert theProject.projTree["0000000000015"].itemStatus == statusKeys[2] + assert theProject.projTree["0000000000016"].itemStatus == statusKeys[1] + assert theProject.projTree["0000000000017"].itemStatus == statusKeys[3] newList = [ - {"key": constData.statusKeys[0], "name": "New", "cols": (1, 1, 1)}, - {"key": constData.statusKeys[1], "name": "Draft", "cols": (2, 2, 2)}, # These are swapped - {"key": constData.statusKeys[2], "name": "Note", "cols": (3, 3, 3)}, # These are swapped - {"key": constData.statusKeys[3], "name": "Edited", "cols": (4, 4, 4)}, # Renamed - {"key": None, "name": "Finished", "cols": (5, 5, 5)}, # New, reused name + {"key": statusKeys[0], "name": "New", "cols": (1, 1, 1)}, + {"key": statusKeys[1], "name": "Draft", "cols": (2, 2, 2)}, # These are swapped + {"key": statusKeys[2], "name": "Note", "cols": (3, 3, 3)}, # These are swapped + {"key": statusKeys[3], "name": "Edited", "cols": (4, 4, 4)}, # Renamed + {"key": None, "name": "Finished", "cols": (5, 5, 5)}, # New, reused name ] assert theProject.setStatusColours(None, None) is False assert theProject.setStatusColours([], []) is False assert theProject.setStatusColours(newList, []) is True - assert theProject.statusItems.name(constData.statusKeys[0]) == "New" - assert theProject.statusItems.name(constData.statusKeys[1]) == "Draft" - assert theProject.statusItems.name(constData.statusKeys[2]) == "Note" - assert theProject.statusItems.name(constData.statusKeys[3]) == "Edited" - assert theProject.statusItems.cols(constData.statusKeys[0]) == (1, 1, 1) - assert theProject.statusItems.cols(constData.statusKeys[1]) == (2, 2, 2) - assert theProject.statusItems.cols(constData.statusKeys[2]) == (3, 3, 3) - assert theProject.statusItems.cols(constData.statusKeys[3]) == (4, 4, 4) + assert theProject.statusItems.name(statusKeys[0]) == "New" + assert theProject.statusItems.name(statusKeys[1]) == "Draft" + assert theProject.statusItems.name(statusKeys[2]) == "Note" + assert theProject.statusItems.name(statusKeys[3]) == "Edited" + assert theProject.statusItems.cols(statusKeys[0]) == (1, 1, 1) + assert theProject.statusItems.cols(statusKeys[1]) == (2, 2, 2) + assert theProject.statusItems.cols(statusKeys[2]) == (3, 3, 3) + assert theProject.statusItems.cols(statusKeys[3]) == (4, 4, 4) # Check the new entry lastKey = theProject.statusItems.check("Finished") - assert lastKey == "sbc8960" + assert lastKey == "s000018" assert theProject.statusItems.name(lastKey) == "Finished" assert theProject.statusItems.cols(lastKey) == (5, 5, 5) @@ -740,33 +728,33 @@ def testCoreProject_StatusImport(mockGUI, fncDir, constData): # Change Importance # ================= - fHandle = theProject.newFile("Jane Doe", "73475cb40a568") + fHandle = theProject.newFile("Jane Doe", "8b9d2e465e150") theProject.projTree[fHandle].setImport("Main") - assert theProject.projTree[fHandle].itemImport == constData.importKeys[3] + assert theProject.projTree[fHandle].itemImport == importKeys[3] newList = [ - {"key": constData.importKeys[0], "name": "New", "cols": (1, 1, 1)}, - {"key": constData.importKeys[1], "name": "Minor", "cols": (2, 2, 2)}, - {"key": constData.importKeys[2], "name": "Major", "cols": (3, 3, 3)}, - {"key": constData.importKeys[3], "name": "Min", "cols": (4, 4, 4)}, + {"key": importKeys[0], "name": "New", "cols": (1, 1, 1)}, + {"key": importKeys[1], "name": "Minor", "cols": (2, 2, 2)}, + {"key": importKeys[2], "name": "Major", "cols": (3, 3, 3)}, + {"key": importKeys[3], "name": "Min", "cols": (4, 4, 4)}, {"key": None, "name": "Max", "cols": (5, 5, 5)}, ] assert theProject.setImportColours(None, None) is False assert theProject.setImportColours([], []) is False assert theProject.setImportColours(newList, []) is True - assert theProject.importItems.name(constData.importKeys[0]) == "New" - assert theProject.importItems.name(constData.importKeys[1]) == "Minor" - assert theProject.importItems.name(constData.importKeys[2]) == "Major" - assert theProject.importItems.name(constData.importKeys[3]) == "Min" - assert theProject.importItems.cols(constData.importKeys[0]) == (1, 1, 1) - assert theProject.importItems.cols(constData.importKeys[1]) == (2, 2, 2) - assert theProject.importItems.cols(constData.importKeys[2]) == (3, 3, 3) - assert theProject.importItems.cols(constData.importKeys[3]) == (4, 4, 4) + assert theProject.importItems.name(importKeys[0]) == "New" + assert theProject.importItems.name(importKeys[1]) == "Minor" + assert theProject.importItems.name(importKeys[2]) == "Major" + assert theProject.importItems.name(importKeys[3]) == "Min" + assert theProject.importItems.cols(importKeys[0]) == (1, 1, 1) + assert theProject.importItems.cols(importKeys[1]) == (2, 2, 2) + assert theProject.importItems.cols(importKeys[2]) == (3, 3, 3) + assert theProject.importItems.cols(importKeys[3]) == (4, 4, 4) # Check the new entry lastKey = theProject.importItems.check("Max") - assert lastKey == "i1a3d1f" + assert lastKey == "i00001a" assert theProject.importItems.name(lastKey) == "Max" assert theProject.importItems.cols(lastKey) == (5, 5, 5) @@ -791,21 +779,25 @@ def testCoreProject_StatusImport(mockGUI, fncDir, constData): assert theProject.closeProject() is True # This should restore the default status/import labels - random.seed(42) assert theProject.openProject(fncDir) is True assert theProject.saveProject() is True - assert list(theProject.statusItems.keys()) == constData.statusKeys - assert list(theProject.importItems.keys()) == constData.importKeys + assert theProject.statusItems.name("s000023") == "New" + assert theProject.statusItems.name("s000024") == "Note" + assert theProject.statusItems.name("s000025") == "Draft" + assert theProject.statusItems.name("s000026") == "Finished" + assert theProject.importItems.name("i000027") == "New" + assert theProject.importItems.name("i000028") == "Minor" + assert theProject.importItems.name("i000029") == "Major" + assert theProject.importItems.name("i00002a") == "Main" # END Test testCoreProject_StatusImport @pytest.mark.core -def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir): +def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): """Test other project class methods and functions. """ theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) assert theProject.newProject({"projPath": fncDir}) is True # Setting project path @@ -864,7 +856,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir): # Trash folder # Should create on first call, and just returned on later calls - hTrash = "1a6562590ef19" + hTrash = "0000000000018" assert theProject.projTree[hTrash] is None assert theProject.trashFolder() == hTrash assert theProject.trashFolder() == hTrash @@ -934,14 +926,14 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir): # Change project tree order oldOrder = [ - "73475cb40a568", "44cb730c42048", "71ee45a3c0db9", - "811786ad1ae74", "25fc0e7096fc6", "31489056e0916", - "98010bd9270f9", "0e17daca5f3e1", "1a6562590ef19", + "0000000000010", "0000000000011", "0000000000012", + "0000000000013", "0000000000014", "0000000000015", + "0000000000016", "0000000000017", "0000000000018", ] newOrder = [ - "811786ad1ae74", "25fc0e7096fc6", "31489056e0916", - "73475cb40a568", "44cb730c42048", "71ee45a3c0db9", - "98010bd9270f9", "0e17daca5f3e1", + "0000000000013", "0000000000014", "0000000000015", + "0000000000010", "0000000000011", "0000000000012", + "0000000000016", "0000000000017", ] assert theProject.projTree.handles() == oldOrder assert theProject.setTreeOrder(newOrder) diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index e1d224d8..868a3034 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -28,9 +28,12 @@ from PyQt5.QtGui import QIcon from novelwriter.core.status import NWStatus +statusKeys = ["sa3b179", "s1c8031", "s06671a", "sbdd640"] +importKeys = ["i466852", "i3eb13b", "i392456", "i23b8c1"] + @pytest.mark.core -def testCoreStatus_Internal(constData): +def testCoreStatus_Internal(): """Test all the internal functions of the NWStatus class. """ random.seed(42) @@ -43,19 +46,19 @@ def testCoreStatus_Internal(constData): # Generate Key # ============ - assert theStatus._newKey() == constData.statusKeys[0] - assert theStatus._newKey() == constData.statusKeys[1] + assert theStatus._newKey() == statusKeys[0] + assert theStatus._newKey() == statusKeys[1] # Key collision, should move to key 3 - theStatus.write(constData.statusKeys[2], "Crash", (0, 0, 0)) - assert theStatus._newKey() == constData.statusKeys[3] + theStatus.write(statusKeys[2], "Crash", (0, 0, 0)) + assert theStatus._newKey() == statusKeys[3] - assert theImport._newKey() == constData.importKeys[0] - assert theImport._newKey() == constData.importKeys[1] + assert theImport._newKey() == importKeys[0] + assert theImport._newKey() == importKeys[1] # Key collision, should move to key 3 - theImport.write(constData.importKeys[2], "Crash", (0, 0, 0)) - assert theImport._newKey() == constData.importKeys[3] + theImport.write(importKeys[2], "Crash", (0, 0, 0)) + assert theImport._newKey() == importKeys[3] # Check Key # ========= @@ -84,18 +87,19 @@ def testCoreStatus_Internal(constData): @pytest.mark.core -def testCoreStatus_Iterator(constData): +def testCoreStatus_Iterator(): """Test the iterator functions of the NWStatus class. """ random.seed(42) theStatus = NWStatus(NWStatus.STATUS) + theStatus.write(None, "New", (100, 100, 100)) theStatus.write(None, "Note", (200, 50, 0)) theStatus.write(None, "Draft", (200, 150, 0)) theStatus.write(None, "Finished", (50, 200, 0)) # Direct access - entry = theStatus[constData.statusKeys[0]] + entry = theStatus[statusKeys[0]] assert entry["cols"] == (100, 100, 100) assert entry["name"] == "New" assert entry["count"] == 0 @@ -107,11 +111,11 @@ def testCoreStatus_Iterator(constData): assert len(theStatus) == 4 # Keys - assert list(theStatus.keys()) == constData.statusKeys + assert list(theStatus.keys()) == statusKeys # Items for index, (key, entry) in enumerate(theStatus.items()): - assert key == constData.statusKeys[index] + assert key == statusKeys[index] assert "cols" in entry assert "name" in entry assert "count" in entry @@ -128,7 +132,7 @@ def testCoreStatus_Iterator(constData): @pytest.mark.core -def testCoreStatus_Entries(constData): +def testCoreStatus_Entries(): """Test all the simple setters for the NWStatus class. """ random.seed(42) @@ -138,74 +142,74 @@ def testCoreStatus_Entries(constData): # ===== # Have a key - theStatus.write(constData.statusKeys[0], "Entry 1", (200, 100, 50)) - assert theStatus[constData.statusKeys[0]]["name"] == "Entry 1" - assert theStatus[constData.statusKeys[0]]["cols"] == (200, 100, 50) + theStatus.write(statusKeys[0], "Entry 1", (200, 100, 50)) + assert theStatus[statusKeys[0]]["name"] == "Entry 1" + assert theStatus[statusKeys[0]]["cols"] == (200, 100, 50) # Don't have a key theStatus.write(None, "Entry 2", (210, 110, 60)) - assert theStatus[constData.statusKeys[1]]["name"] == "Entry 2" - assert theStatus[constData.statusKeys[1]]["cols"] == (210, 110, 60) + assert theStatus[statusKeys[1]]["name"] == "Entry 2" + assert theStatus[statusKeys[1]]["cols"] == (210, 110, 60) # Wrong colour spec theStatus.write(None, "Entry 3", "what?") - assert theStatus[constData.statusKeys[2]]["name"] == "Entry 3" - assert theStatus[constData.statusKeys[2]]["cols"] == (100, 100, 100) + assert theStatus[statusKeys[2]]["name"] == "Entry 3" + assert theStatus[statusKeys[2]]["cols"] == (100, 100, 100) # Wrong colour count theStatus.write(None, "Entry 4", (10, 20)) - assert theStatus[constData.statusKeys[3]]["name"] == "Entry 4" - assert theStatus[constData.statusKeys[3]]["cols"] == (100, 100, 100) + assert theStatus[statusKeys[3]]["name"] == "Entry 4" + assert theStatus[statusKeys[3]]["cols"] == (100, 100, 100) # Check reverse map assert theStatus._reverse == { - "Entry 1": constData.statusKeys[0], - "Entry 2": constData.statusKeys[1], - "Entry 3": constData.statusKeys[2], - "Entry 4": constData.statusKeys[3], + "Entry 1": statusKeys[0], + "Entry 2": statusKeys[1], + "Entry 3": statusKeys[2], + "Entry 4": statusKeys[3], } # Check # ===== # Normal lookup - for key in constData.statusKeys: + for key in statusKeys: assert theStatus.check(key) == key # Reverse map lookup - assert theStatus.check("Entry 1") == constData.statusKeys[0] - assert theStatus.check("Entry 2") == constData.statusKeys[1] - assert theStatus.check("Entry 3") == constData.statusKeys[2] - assert theStatus.check("Entry 4") == constData.statusKeys[3] + assert theStatus.check("Entry 1") == statusKeys[0] + assert theStatus.check("Entry 2") == statusKeys[1] + assert theStatus.check("Entry 3") == statusKeys[2] + assert theStatus.check("Entry 4") == statusKeys[3] # Non-existing name - assert theStatus.check("Entry 5") == constData.statusKeys[0] + assert theStatus.check("Entry 5") == statusKeys[0] # Name Access # =========== - assert theStatus.name(constData.statusKeys[0]) == "Entry 1" - assert theStatus.name(constData.statusKeys[1]) == "Entry 2" - assert theStatus.name(constData.statusKeys[2]) == "Entry 3" - assert theStatus.name(constData.statusKeys[3]) == "Entry 4" + assert theStatus.name(statusKeys[0]) == "Entry 1" + assert theStatus.name(statusKeys[1]) == "Entry 2" + assert theStatus.name(statusKeys[2]) == "Entry 3" + assert theStatus.name(statusKeys[3]) == "Entry 4" assert theStatus.name("blablabla") == "Entry 1" # Colour Access # ============= - assert theStatus.cols(constData.statusKeys[0]) == (200, 100, 50) - assert theStatus.cols(constData.statusKeys[1]) == (210, 110, 60) - assert theStatus.cols(constData.statusKeys[2]) == (100, 100, 100) - assert theStatus.cols(constData.statusKeys[3]) == (100, 100, 100) + assert theStatus.cols(statusKeys[0]) == (200, 100, 50) + assert theStatus.cols(statusKeys[1]) == (210, 110, 60) + assert theStatus.cols(statusKeys[2]) == (100, 100, 100) + assert theStatus.cols(statusKeys[3]) == (100, 100, 100) assert theStatus.cols("blablabla") == (200, 100, 50) # Icon Access # =========== - assert isinstance(theStatus.icon(constData.statusKeys[0]), QIcon) - assert isinstance(theStatus.icon(constData.statusKeys[1]), QIcon) - assert isinstance(theStatus.icon(constData.statusKeys[2]), QIcon) - assert isinstance(theStatus.icon(constData.statusKeys[3]), QIcon) + assert isinstance(theStatus.icon(statusKeys[0]), QIcon) + assert isinstance(theStatus.icon(statusKeys[1]), QIcon) + assert isinstance(theStatus.icon(statusKeys[2]), QIcon) + assert isinstance(theStatus.icon(statusKeys[3]), QIcon) assert isinstance(theStatus.icon("blablabla"), QIcon) # Increment and Count Access @@ -214,26 +218,26 @@ def testCoreStatus_Entries(constData): countTo = [3, 5, 7, 9] for i, n in enumerate(countTo): for _ in range(n): - theStatus.increment(constData.statusKeys[i]) + theStatus.increment(statusKeys[i]) - assert theStatus.count(constData.statusKeys[0]) == countTo[0] - assert theStatus.count(constData.statusKeys[1]) == countTo[1] - assert theStatus.count(constData.statusKeys[2]) == countTo[2] - assert theStatus.count(constData.statusKeys[3]) == countTo[3] + assert theStatus.count(statusKeys[0]) == countTo[0] + assert theStatus.count(statusKeys[1]) == countTo[1] + assert theStatus.count(statusKeys[2]) == countTo[2] + assert theStatus.count(statusKeys[3]) == countTo[3] assert theStatus.count("blablabla") == countTo[0] theStatus.resetCounts() - assert theStatus.count(constData.statusKeys[0]) == 0 - assert theStatus.count(constData.statusKeys[1]) == 0 - assert theStatus.count(constData.statusKeys[2]) == 0 - assert theStatus.count(constData.statusKeys[3]) == 0 + assert theStatus.count(statusKeys[0]) == 0 + assert theStatus.count(statusKeys[1]) == 0 + assert theStatus.count(statusKeys[2]) == 0 + assert theStatus.count(statusKeys[3]) == 0 # Reorder # ======= cOrder = list(theStatus.keys()) - assert cOrder == constData.statusKeys + assert cOrder == statusKeys # Wrong length assert theStatus.reorder([]) is False @@ -243,10 +247,10 @@ def testCoreStatus_Entries(constData): # Actual reaorder nOrder = [ - constData.statusKeys[0], - constData.statusKeys[2], - constData.statusKeys[1], - constData.statusKeys[3], + statusKeys[0], + statusKeys[2], + statusKeys[1], + statusKeys[3], ] assert theStatus.reorder(nOrder) is True assert list(theStatus.keys()) == nOrder @@ -282,15 +286,15 @@ def testCoreStatus_Entries(constData): assert theStatus.remove("blablabla") is False # Non-zero entry - theStatus.increment(constData.statusKeys[3]) - assert theStatus.remove(constData.statusKeys[3]) is False + theStatus.increment(statusKeys[3]) + assert theStatus.remove(statusKeys[3]) is False # Delete last entry theStatus.resetCounts() - lastName = theStatus.name(constData.statusKeys[3]) + lastName = theStatus.name(statusKeys[3]) assert lastName == "Entry 4" - assert theStatus.remove(constData.statusKeys[3]) is True - assert theStatus.check(constData.statusKeys[3]) == theStatus._default + assert theStatus.remove(statusKeys[3]) is True + assert theStatus.check(statusKeys[3]) == theStatus._default assert theStatus.check(lastName) == theStatus._default # Delete default entry, Entry 2 is new default @@ -300,8 +304,8 @@ def testCoreStatus_Entries(constData): assert theStatus.name(firstName) == "Entry 2" # Remove remaining entries - assert theStatus.remove(constData.statusKeys[1]) is True - assert theStatus.remove(constData.statusKeys[2]) is True + assert theStatus.remove(statusKeys[1]) is True + assert theStatus.remove(statusKeys[2]) is True assert len(theStatus) == 0 assert theStatus._default is None @@ -310,7 +314,7 @@ def testCoreStatus_Entries(constData): @pytest.mark.core -def testCoreStatus_XMLPackUnpack(constData): +def testCoreStatus_XMLPackUnpack(): """Test all the XML pack/unpack of the NWStatus class. """ random.seed(42) @@ -323,7 +327,7 @@ def testCoreStatus_XMLPackUnpack(constData): countTo = [3, 5, 7, 9] for i, n in enumerate(countTo): for _ in range(n): - theStatus.increment(constData.statusKeys[i]) + theStatus.increment(statusKeys[i]) nwXML = etree.Element("novelWriterXML") @@ -343,18 +347,18 @@ def testCoreStatus_XMLPackUnpack(constData): theStatus = NWStatus(NWStatus.STATUS) assert theStatus.unpackXML(xStatus) assert len(theStatus._store) == 4 - assert list(theStatus._store.keys()) == constData.statusKeys - assert theStatus._store[constData.statusKeys[0]]["name"] == "New" - assert theStatus._store[constData.statusKeys[1]]["name"] == "Note" - assert theStatus._store[constData.statusKeys[2]]["name"] == "Draft" - assert theStatus._store[constData.statusKeys[3]]["name"] == "Finished" - assert theStatus._store[constData.statusKeys[0]]["cols"] == (100, 100, 100) - assert theStatus._store[constData.statusKeys[1]]["cols"] == (200, 50, 0) - assert theStatus._store[constData.statusKeys[2]]["cols"] == (200, 150, 0) - assert theStatus._store[constData.statusKeys[3]]["cols"] == (50, 200, 0) - assert theStatus._store[constData.statusKeys[0]]["count"] == countTo[0] - assert theStatus._store[constData.statusKeys[1]]["count"] == countTo[1] - assert theStatus._store[constData.statusKeys[2]]["count"] == countTo[2] - assert theStatus._store[constData.statusKeys[3]]["count"] == countTo[3] + assert list(theStatus._store.keys()) == statusKeys + assert theStatus._store[statusKeys[0]]["name"] == "New" + assert theStatus._store[statusKeys[1]]["name"] == "Note" + assert theStatus._store[statusKeys[2]]["name"] == "Draft" + assert theStatus._store[statusKeys[3]]["name"] == "Finished" + assert theStatus._store[statusKeys[0]]["cols"] == (100, 100, 100) + assert theStatus._store[statusKeys[1]]["cols"] == (200, 50, 0) + assert theStatus._store[statusKeys[2]]["cols"] == (200, 150, 0) + assert theStatus._store[statusKeys[3]]["cols"] == (50, 200, 0) + assert theStatus._store[statusKeys[0]]["count"] == countTo[0] + assert theStatus._store[statusKeys[1]]["count"] == countTo[1] + assert theStatus._store[statusKeys[2]]["count"] == countTo[2] + assert theStatus._store[statusKeys[3]]["count"] == countTo[3] # END Test testCoreStatus_XMLPackUnpack diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index 088de786..97023201 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -136,7 +136,6 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): """Test handling files and text in the Tokenizer class. """ theProject = NWProject(mockGUI) - theProject.projTree.setSeed(42) theProject.projLang = "en" theProject._loadProjectLocalisation() diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 3b35abd5..c70cc4bb 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -24,7 +24,6 @@ import pytest import random from lxml import etree -from hashlib import sha256 from tools import readFile @@ -34,10 +33,9 @@ from novelwriter.constants import nwFiles @pytest.fixture(scope="function") -def mockItems(mockGUI): +def mockItems(mockGUI, mockRnd): """Create a list of mock items. """ - random.seed(42) theProject = NWProject(mockGUI) itemA = NWItem(theProject) @@ -118,9 +116,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems): theProject = NWProject(mockGUI) theTree = NWTree(theProject) - theTree.setSeed(42) - assert theTree._handleSeed == 42 - # Check that tree is empty (calls NWTree.__bool__) assert bool(theTree) is False @@ -196,10 +191,11 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert len(theTree) == len(mockItems) + 1 theList = theTree.handles() - assert theList[-1] == "73475cb40a568" + nHandle = "0000000000010" + assert theList[-1] == nHandle # Try to add existing handle - assert theTree.append("73475cb40a568", None, itemT) is False + assert theTree.append(nHandle, None, itemT) is False assert len(theTree) == len(mockItems) + 1 # Delete a non-existing item @@ -207,9 +203,9 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert len(theTree) == len(mockItems) + 1 # Delete the last item - del theTree["73475cb40a568"] + del theTree[nHandle] assert len(theTree) == len(mockItems) - assert "73475cb40a568" not in theTree + assert nHandle not in theTree # Delete the Novel, Archive and Trash folders del theTree["a000000000001"] @@ -310,44 +306,31 @@ def testCoreTree_Methods(mockGUI, mockItems): @pytest.mark.core -def testCoreTree_MakeHandles(monkeypatch, mockGUI): +def testCoreTree_MakeHandles(mockGUI): """Test generating item handles. """ + random.seed(42) theProject = NWProject(mockGUI) theTree = NWTree(theProject) - theTree.setSeed(42) + handles = ["1c803a3b1799d", "bdd6406671ad1", "3eb1346685257", "23b8c392456de"] + random.seed(42) tHandle = theTree._makeHandle() - assert tHandle == "73475cb40a568" + assert tHandle == handles[0] + theTree._projTree[handles[0]] = None # Add the next in line to the project to force duplicate - theTree._projTree["44cb730c42048"] = None + theTree._projTree[handles[1]] = None tHandle = theTree._makeHandle() - assert tHandle == "71ee45a3c0db9" - - # Fix the time() function and force a handle collission - theTree.setSeed(None) - theTree._handleCount = 0 - monkeypatch.setattr("novelwriter.core.tree.time", lambda: 123.4) + assert tHandle == handles[2] + theTree._projTree[handles[2]] = None + # Reset the seed to force collissions, which should still end up + # returning the next handle in the sequence + random.seed(42) tHandle = theTree._makeHandle() - theTree._projTree[tHandle] = None - newSeed = "123.4_0_" - assert tHandle == sha256(newSeed.encode()).hexdigest()[0:13] - - tHandle = theTree._makeHandle() - theTree._projTree[tHandle] = None - newSeed = "123.4_1_" - assert tHandle == sha256(newSeed.encode()).hexdigest()[0:13] - - # Reset the count and the handle for 0 and 1 should be duplicates - # which forces the function to add the '!' - theTree._handleCount = 0 - tHandle = theTree._makeHandle() - theTree._projTree[tHandle] = None - newSeed = "123.4_1_!" - assert tHandle == sha256(newSeed.encode()).hexdigest()[0:13] + assert tHandle == handles[3] # END Test testCoreTree_MakeHandles @@ -412,7 +395,7 @@ def testCoreTree_Reorder(mockGUI, mockItems): @pytest.mark.core -def testCoreTree_XMLPackUnpack(mockGUI, mockItems, constData): +def testCoreTree_XMLPackUnpack(mockGUI, mockItems): """Test packing and unpacking the tree to and from XML. """ theProject = NWProject(mockGUI) @@ -426,41 +409,39 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems, constData): nwXML = etree.Element("novelWriterXML") theTree.packXML(nwXML) - assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == bytes(( - '' - '' - 'Novel' - 'Act One' - 'Chapter One' - 'Scene One' - 'Outtakes' - 'Trash' - 'Characters' - 'Jane Doe' - '' - '' - ).format( - s0=constData.statusKeys[0], i0=constData.importKeys[0] - ), encoding="utf8") + assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( + b'' + b'' + b'Novel' + b'Act One' + b'Chapter One' + b'Scene One' + b'Outtakes' + b'Trash' + b'Characters' + b'Jane Doe' + b'' + b'' + ) theTree.clear() assert len(theTree) == 0 diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index a4b21f45..f635be4a 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -19,21 +19,21 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import os +import pytest -from tools import getGuiItem, readFile, writeFile from mock import causeOSError +from tools import getGuiItem, readFile, writeFile from PyQt5.QtWidgets import QAction, QMessageBox, QDialog -from novelwriter.dialogs import GuiDocMerge, GuiItemEditor from novelwriter.enum import nwItemType, nwWidget +from novelwriter.dialogs import GuiDocMerge, GuiItemEditor from novelwriter.core.tree import NWTree @pytest.mark.gui -def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj): +def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test the merge documents tool. """ # Block message box @@ -41,17 +41,17 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj): monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) # Create a new project - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) # Handles for new objects - hChapterDir = "31489056e0916" - hChapterOne = "98010bd9270f9" - hSceneOne = "0e17daca5f3e1" - hSceneTwo = "1a6562590ef19" - hSceneThree = "031b4af5197ec" - hSceneFour = "41cfc0d1f2d12" - hMergedDoc = "2858dcd1057d3" + hNovelRoot = "0000000000008" + hChapterDir = "000000000000d" + hChapterOne = "000000000000e" + hSceneOne = "000000000000f" + hSceneTwo = "0000000000010" + hSceneThree = "0000000000011" + hSceneFour = "0000000000012" + hMergedDoc = "0000000000023" # Add Project Content monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) @@ -137,7 +137,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj): assert os.path.isfile(mergedFile) assert readFile(mergedFile) == ( "%%%%~name: New Chapter\n" - "%%%%~path: 73475cb40a568/2858dcd1057d3\n" + "%%%%~path: %s/%s\n" "%%%%~kind: NOVEL/DOCUMENT\n" "%s\n\n" "%s\n\n" @@ -145,6 +145,8 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj): "%s\n\n" "%s\n\n" ) % ( + hNovelRoot, + hMergedDoc, tChapterOne.strip(), tSceneOne.strip(), tSceneTwo.strip(), diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 66f8868a..19988c0b 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -19,22 +19,22 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import os +import pytest -from tools import getGuiItem, readFile, writeFile from mock import causeOSError +from tools import getGuiItem, readFile, writeFile from PyQt5.QtWidgets import QAction, QMessageBox, QDialog -from novelwriter.dialogs import GuiDocSplit, GuiItemEditor from novelwriter.enum import nwItemType, nwWidget -from novelwriter.core.document import NWDoc +from novelwriter.dialogs import GuiDocSplit, GuiItemEditor from novelwriter.core.tree import NWTree +from novelwriter.core.document import NWDoc @pytest.mark.gui -def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj): +def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test the split document tool. """ # Block message box @@ -42,20 +42,20 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj): monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) # Create a new project - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) is True # Handles for new objects - hNovelRoot = "73475cb40a568" - hChapterDir = "31489056e0916" - hToSplit = "1a6562590ef19" - hPartition = "41cfc0d1f2d12" - hChapterOne = "2858dcd1057d3" - hSceneOne = "2fca346db6561" - hSceneTwo = "02d20bbd7e394" - hSceneThree = "7688b6ef52555" - hSceneFour = "c837649cce43f" - hSceneFive = "6208ef0f7750c" + hNovelRoot = "0000000000008" + hChapterDir = "000000000000d" + hToSplit = "0000000000010" + hNewFolder = "0000000000021" + hPartition = "0000000000022" + hChapterOne = "0000000000023" + hSceneOne = "0000000000024" + hSceneTwo = "0000000000025" + hSceneThree = "0000000000026" + hSceneFour = "0000000000027" + hSceneFive = "0000000000028" # Add Project Content monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) @@ -173,52 +173,52 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj): assert readFile(os.path.join(contentDir, hPartition+".nwd")) == ( "%%%%~name: Nantucket\n" - "%%%%~path: 031b4af5197ec/%s\n" + "%%%%~path: %s/%s\n" "%%%%~kind: NOVEL/DOCUMENT\n" "%s\n\n" - ) % (hPartition, tPartition) + ) % (hNewFolder, hPartition, tPartition) assert readFile(os.path.join(contentDir, hChapterOne+".nwd")) == ( "%%%%~name: Chapter One\n" - "%%%%~path: 031b4af5197ec/%s\n" + "%%%%~path: %s/%s\n" "%%%%~kind: NOVEL/DOCUMENT\n" "%s\n\n" - ) % (hChapterOne, tChapterOne) + ) % (hNewFolder, hChapterOne, tChapterOne) assert readFile(os.path.join(contentDir, hSceneOne+".nwd")) == ( "%%%%~name: Scene One\n" - "%%%%~path: 031b4af5197ec/%s\n" + "%%%%~path: %s/%s\n" "%%%%~kind: NOVEL/DOCUMENT\n" "%s\n\n" - ) % (hSceneOne, tSceneOne) + ) % (hNewFolder, hSceneOne, tSceneOne) assert readFile(os.path.join(contentDir, hSceneTwo+".nwd")) == ( "%%%%~name: Scene Two\n" - "%%%%~path: 031b4af5197ec/%s\n" + "%%%%~path: %s/%s\n" "%%%%~kind: NOVEL/DOCUMENT\n" "%s\n\n" - ) % (hSceneTwo, tSceneTwo) + ) % (hNewFolder, hSceneTwo, tSceneTwo) assert readFile(os.path.join(contentDir, hSceneThree+".nwd")) == ( "%%%%~name: Scene Three\n" - "%%%%~path: 031b4af5197ec/%s\n" + "%%%%~path: %s/%s\n" "%%%%~kind: NOVEL/DOCUMENT\n" "%s\n\n" - ) % (hSceneThree, tSceneThree) + ) % (hNewFolder, hSceneThree, tSceneThree) assert readFile(os.path.join(contentDir, hSceneFour+".nwd")) == ( "%%%%~name: Scene Four\n" - "%%%%~path: 031b4af5197ec/%s\n" + "%%%%~path: %s/%s\n" "%%%%~kind: NOVEL/DOCUMENT\n" "%s\n\n" - ) % (hSceneFour, tSceneFour) + ) % (hNewFolder, hSceneFour, tSceneFour) assert readFile(os.path.join(contentDir, hSceneFive+".nwd")) == ( "%%%%~name: The End\n" - "%%%%~path: 031b4af5197ec/%s\n" + "%%%%~path: %s/%s\n" "%%%%~kind: NOVEL/DOCUMENT\n" "%s\n\n" - ) % (hSceneFive, tSceneFive) + ) % (hNewFolder, hSceneFive, tSceneFive) # OS error with monkeypatch.context() as mp: diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 127d2e74..ebd71d6c 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -20,7 +20,6 @@ along with this program. If not, see . """ import pytest -import random from tools import getGuiItem @@ -31,9 +30,12 @@ from novelwriter.enum import nwItemLayout, nwItemType from novelwriter.dialogs import GuiItemEditor from novelwriter.core.tree import NWTree +statusKeys = ["s000000", "s000001", "s000002", "s000003"] +importKeys = ["i000004", "i000005", "i000006", "i000007"] + @pytest.mark.gui -def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj): +def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test launching the item editor dialog from GuiMain. """ # Block message box @@ -46,15 +48,15 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj): assert nwGUI.editItem() is False # Create and Open Project - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) + tHandle = "000000000000f" # No Selection nwGUI.treeView.clearSelection() assert nwGUI.editItem() is False # Force opening from editor - assert nwGUI.openDocument("0e17daca5f3e1") + assert nwGUI.openDocument(tHandle) nwGUI.isFocusMode = True # Block Tree Lookup @@ -63,9 +65,9 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj): assert nwGUI.editItem() is False # Invalid Type - nwGUI.theProject.projTree["0e17daca5f3e1"]._type = nwItemType.NO_TYPE + nwGUI.theProject.projTree[tHandle]._type = nwItemType.NO_TYPE assert nwGUI.editItem() is False - nwGUI.theProject.projTree["0e17daca5f3e1"]._type = nwItemType.FILE + nwGUI.theProject.projTree[tHandle]._type = nwItemType.FILE # Open Properly assert nwGUI.editItem() is True @@ -89,7 +91,7 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj): @pytest.mark.gui -def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, constData): +def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test the item editor dialog for a novel document. """ # Block message box @@ -97,13 +99,13 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, constData): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Create Project and Open Document - random.seed(42) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) - assert nwGUI.theProject.statusItems.name(constData.statusKeys[0]) == "New" - assert nwGUI.theProject.statusItems.name(constData.statusKeys[1]) == "Note" + tHandle = "000000000000f" - assert nwGUI.openDocument("0e17daca5f3e1") is True + assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" + assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" + + assert nwGUI.openDocument(tHandle) is True # Check that an invalid handle is managed itemEdit = GuiItemEditor(nwGUI, "whatever") @@ -111,12 +113,12 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, constData): itemEdit._doClose() # Edit a Document - itemEdit = GuiItemEditor(nwGUI, "0e17daca5f3e1") + itemEdit = GuiItemEditor(nwGUI, tHandle) itemEdit.show() # Check Existing Settings assert itemEdit.editName.text() == "New Scene" - assert itemEdit.editStatus.currentData() == constData.statusKeys[0] + assert itemEdit.editStatus.currentData() == statusKeys[0] assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT assert itemEdit.editExport.isChecked() is True @@ -130,12 +132,12 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, constData): # Check New Settings itemEdit._doSave() assert itemEdit.theItem.itemName == "Great Scene" - assert itemEdit.theItem.itemStatus == constData.statusKeys[1] + assert itemEdit.theItem.itemStatus == statusKeys[1] assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE assert itemEdit.theItem.isExported is False # Check that the editor header is updated - nwGUI.docEditor.updateDocInfo("0e17daca5f3e1") + nwGUI.docEditor.updateDocInfo(tHandle) assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel › New Chapter › Great Scene" itemEdit.close() @@ -146,7 +148,7 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, constData): @pytest.mark.gui -def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData): +def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test the item editor dialog for a project note. """ # Block message box @@ -154,29 +156,27 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Create Project and Open Document - random.seed(42) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) - assert nwGUI.theProject.statusItems.name(constData.statusKeys[0]) == "New" - assert nwGUI.theProject.statusItems.name(constData.statusKeys[1]) == "Note" - assert nwGUI.theProject.importItems.name(constData.importKeys[0]) == "New" - assert nwGUI.theProject.importItems.name(constData.importKeys[1]) == "Minor" + assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" + assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" + assert nwGUI.theProject.importItems.name(importKeys[0]) == "New" + assert nwGUI.theProject.importItems.name(importKeys[1]) == "Minor" # Create Note nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True) + nwGUI.treeView._getTreeItem("000000000000a").setSelected(True) nwGUI.treeView.newTreeItem(nwItemType.FILE, None) # Open Note - assert nwGUI.openDocument("1a6562590ef19") + assert nwGUI.openDocument("0000000000010") # Edit a Document - itemEdit = GuiItemEditor(nwGUI, "1a6562590ef19") + itemEdit = GuiItemEditor(nwGUI, "0000000000010") itemEdit.show() # Check Existing Settings assert itemEdit.editName.text() == "New Note" - assert itemEdit.editStatus.currentData() == constData.importKeys[0] + assert itemEdit.editStatus.currentData() == importKeys[0] assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE assert itemEdit.editExport.isChecked() is True @@ -188,8 +188,8 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData): # Check New Settings assert itemEdit.theItem.itemName == "New Character" - assert itemEdit.theItem.itemStatus == constData.statusKeys[0] - assert itemEdit.theItem.itemImport == constData.importKeys[1] + assert itemEdit.theItem.itemStatus == statusKeys[0] + assert itemEdit.theItem.itemImport == importKeys[1] assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE assert itemEdit.theItem.isExported is False @@ -201,7 +201,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData): @pytest.mark.gui -def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj, constData): +def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test the item editor dialog for a folder. """ # Block message box @@ -209,20 +209,18 @@ def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj, constData): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Create Project and Open Document - random.seed(42) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) # Edit a Folder - itemEdit = GuiItemEditor(nwGUI, "31489056e0916") + itemEdit = GuiItemEditor(nwGUI, "000000000000d") itemEdit.show() - assert nwGUI.theProject.statusItems.name(constData.statusKeys[0]) == "New" - assert nwGUI.theProject.statusItems.name(constData.statusKeys[1]) == "Note" + assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" + assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" # Check Existing Settings assert itemEdit.editName.text() == "New Chapter" - assert itemEdit.editStatus.currentData() == constData.statusKeys[0] + assert itemEdit.editStatus.currentData() == statusKeys[0] assert itemEdit.editLayout.currentData() == nwItemLayout.NO_LAYOUT assert itemEdit.editExport.isChecked() is False @@ -236,7 +234,7 @@ def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj, constData): # Check New Settings itemEdit._doSave() assert itemEdit.theItem.itemName == "Chapter One" - assert itemEdit.theItem.itemStatus == constData.statusKeys[1] + assert itemEdit.theItem.itemStatus == statusKeys[1] assert itemEdit.theItem.itemLayout == nwItemLayout.NO_LAYOUT assert itemEdit.theItem.isExported is False diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index 5a047522..54827cfd 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -21,7 +21,6 @@ along with this program. If not, see . import os import pytest -import random from shutil import copyfile from tools import cmpFiles, getGuiItem @@ -35,11 +34,13 @@ from novelwriter.dialogs import GuiProjectSettings keyDelay = 2 typeDelay = 1 stepDelay = 20 +statusKeys = ["s000000", "s000001", "s000002", "s000003"] +importKeys = ["i000004", "i000005", "i000006", "i000007"] @pytest.mark.gui def testDlgProjSettings_Dialog( - qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir, constData + qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir, mockRnd ): """Test the full project settings dialog. """ @@ -56,8 +57,6 @@ def testDlgProjSettings_Dialog( assert getGuiItem("GuiProjectSettings") is None # Create new project - random.seed(42) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) nwGUI.mainConf.backupPath = fncDir @@ -145,15 +144,15 @@ def testDlgProjSettings_Dialog( assert projEdit.tabStatus.getNewList() == ( [ { - "key": constData.statusKeys[0], + "key": statusKeys[0], "name": "New", "cols": (100, 100, 100) }, { - "key": constData.statusKeys[1], + "key": statusKeys[1], "name": "Note", "cols": (200, 50, 0) }, { - "key": constData.statusKeys[3], + "key": statusKeys[3], "name": "Finished", "cols": (50, 200, 0) }, { @@ -162,7 +161,7 @@ def testDlgProjSettings_Dialog( "cols": (20, 30, 40) } ], [ - constData.statusKeys[2] # Deleted item + statusKeys[2] # Deleted item ] ) @@ -170,25 +169,25 @@ def testDlgProjSettings_Dialog( projEdit.tabStatus.listBox.clearSelection() projEdit.tabStatus._moveItem(1) assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ - constData.statusKeys[0], constData.statusKeys[1], constData.statusKeys[3], None + statusKeys[0], statusKeys[1], statusKeys[3], None ] projEdit.tabStatus.listBox.clearSelection() projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True) projEdit.tabStatus._moveItem(-1) assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ - constData.statusKeys[0], constData.statusKeys[1], constData.statusKeys[3], None + statusKeys[0], statusKeys[1], statusKeys[3], None ] projEdit.tabStatus.listBox.clearSelection() projEdit.tabStatus.listBox.topLevelItem(3).setSelected(True) projEdit.tabStatus._moveItem(-1) assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ - constData.statusKeys[0], constData.statusKeys[1], None, constData.statusKeys[3] + statusKeys[0], statusKeys[1], None, statusKeys[3] ] projEdit.tabStatus._moveItem(1) assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ - constData.statusKeys[0], constData.statusKeys[1], constData.statusKeys[3], None + statusKeys[0], statusKeys[1], statusKeys[3], None ] # Importance Tab diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index cf5c3cea..7f3aa30c 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -1286,7 +1286,6 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum): monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.openProject(nwLipsum) is True assert nwGUI.openDocument("4c4f28287af27") is True origText = nwGUI.docEditor.getText() diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 4d22b62d..0a11bc3d 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -43,7 +43,6 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) # Open project - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.openProject(nwLipsum) # Rebuild the index diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index fb36b3e2..65648862 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -20,7 +20,6 @@ along with this program. If not, see . """ import os -import random import pytest from shutil import copyfile @@ -72,16 +71,16 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI): @pytest.mark.gui -def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj): +def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test handling of project tree items based on GUI focus states. """ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) is True assert nwGUI.saveProject() is True + # assert False - sHandle = "0e17daca5f3e1" + sHandle = "000000000000f" assert nwGUI.openSelectedItem() is False # Project Tree has focus @@ -128,7 +127,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj): @pytest.mark.gui -def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): +def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mockRnd): """Test the document editor. """ # Block message box @@ -140,8 +139,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) # Create new, save, close project - random.seed(42) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) assert nwGUI.saveProject() assert nwGUI.closeProject() @@ -186,14 +183,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): assert not nwGUI.theProject.spellCheck # Check that tree items have been created - assert nwGUI.treeView._getTreeItem("73475cb40a568") is not None - assert nwGUI.treeView._getTreeItem("25fc0e7096fc6") is not None - assert nwGUI.treeView._getTreeItem("31489056e0916") is not None - assert nwGUI.treeView._getTreeItem("98010bd9270f9") is not None - assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None - assert nwGUI.treeView._getTreeItem("44cb730c42048") is not None - assert nwGUI.treeView._getTreeItem("71ee45a3c0db9") is not None - assert nwGUI.treeView._getTreeItem("811786ad1ae74") is not None + assert nwGUI.treeView._getTreeItem("0000000000008") is not None + assert nwGUI.treeView._getTreeItem("0000000000009") is not None + assert nwGUI.treeView._getTreeItem("000000000000a") is not None + assert nwGUI.treeView._getTreeItem("000000000000b") is not None + assert nwGUI.treeView._getTreeItem("000000000000c") is not None + assert nwGUI.treeView._getTreeItem("000000000000d") is not None + assert nwGUI.treeView._getTreeItem("000000000000e") is not None + assert nwGUI.treeView._getTreeItem("000000000000f") is not None nwGUI.mainMenu.aSpellCheck.setChecked(True) assert nwGUI.mainMenu._toggleSpellCheck() @@ -207,7 +204,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): # Add a Character File nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True) + nwGUI.treeView._getTreeItem("000000000000a").setSelected(True) nwGUI.treeView.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() @@ -229,7 +226,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): # Add a Plot File nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("44cb730c42048").setSelected(True) + nwGUI.treeView._getTreeItem("0000000000009").setSelected(True) nwGUI.treeView.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() @@ -251,7 +248,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): # Add a World File nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("811786ad1ae74").setSelected(True) + nwGUI.treeView._getTreeItem("000000000000b").setSelected(True) nwGUI.treeView.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() @@ -282,9 +279,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): # Select the 'New Scene' file nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("73475cb40a568").setExpanded(True) - nwGUI.treeView._getTreeItem("31489056e0916").setExpanded(True) - nwGUI.treeView._getTreeItem("0e17daca5f3e1").setSelected(True) + nwGUI.treeView._getTreeItem("0000000000008").setExpanded(True) + nwGUI.treeView._getTreeItem("000000000000d").setExpanded(True) + nwGUI.treeView._getTreeItem("000000000000f").setSelected(True) assert nwGUI.openSelectedItem() # Type something into the document @@ -419,8 +416,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): # Open and view the edited document nwGUI.switchFocus(nwWidget.VIEWER) - assert nwGUI.openDocument("0e17daca5f3e1") - assert nwGUI.viewDocument("0e17daca5f3e1") + assert nwGUI.openDocument("000000000000f") + assert nwGUI.viewDocument("000000000000f") qtbot.wait(stepDelay) assert nwGUI.saveProject() assert nwGUI.closeDocViewer() @@ -429,11 +426,11 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): # Check a Quick Create and Delete assert nwGUI.treeView.newTreeItem(nwItemType.FILE, None) newHandle = nwGUI.treeView.getSelectedHandle() - assert nwGUI.theProject.projTree["2858dcd1057d3"] is not None + assert nwGUI.theProject.projTree["0000000000020"] is not None assert nwGUI.treeView.deleteItem() assert nwGUI.treeView.setSelectedHandle(newHandle) assert nwGUI.treeView.deleteItem() - assert nwGUI.theProject.projTree["2fca346db6561"] is not None # Trash + assert nwGUI.theProject.projTree["0000000000024"] is not None # Trash assert nwGUI.saveProject() # Check the files @@ -443,27 +440,27 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) - projFile = os.path.join(fncProj, "content", "031b4af5197ec.nwd") - testFile = os.path.join(outDir, "guiEditor_Main_Final_031b4af5197ec.nwd") - compFile = os.path.join(refDir, "guiEditor_Main_Final_031b4af5197ec.nwd") + projFile = os.path.join(fncProj, "content", "000000000000f.nwd") + testFile = os.path.join(outDir, "guiEditor_Main_Final_000000000000f.nwd") + compFile = os.path.join(refDir, "guiEditor_Main_Final_000000000000f.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) - projFile = os.path.join(fncProj, "content", "1a6562590ef19.nwd") - testFile = os.path.join(outDir, "guiEditor_Main_Final_1a6562590ef19.nwd") - compFile = os.path.join(refDir, "guiEditor_Main_Final_1a6562590ef19.nwd") + projFile = os.path.join(fncProj, "content", "0000000000020.nwd") + testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000020.nwd") + compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000020.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) - projFile = os.path.join(fncProj, "content", "0e17daca5f3e1.nwd") - testFile = os.path.join(outDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd") - compFile = os.path.join(refDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd") + projFile = os.path.join(fncProj, "content", "0000000000021.nwd") + testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000021.nwd") + compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000021.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) - projFile = os.path.join(fncProj, "content", "41cfc0d1f2d12.nwd") - testFile = os.path.join(outDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd") - compFile = os.path.join(refDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd") + projFile = os.path.join(fncProj, "content", "0000000000022.nwd") + testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000022.nwd") + compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000022.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 15282fda..815c8566 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -48,7 +48,6 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, nwLipsum): # Test Document Action with No Project assert nwGUI.docEditor.docAction(nwDocAction.COPY) is False - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.openProject(nwLipsum) is True qtbot.wait(stepDelay) @@ -381,7 +380,6 @@ def testGuiMenu_ContextMenus(qtbot, monkeypatch, nwGUI, nwLipsum): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.openProject(nwLipsum) assert nwGUI.openDocument("4c4f28287af27") qtbot.wait(stepDelay) @@ -460,18 +458,17 @@ def testGuiMenu_ContextMenus(qtbot, monkeypatch, nwGUI, nwLipsum): @pytest.mark.gui -def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj): +def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): """Test the Insert menu. """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) - assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None - assert nwGUI.openDocument("0e17daca5f3e1") is True + assert nwGUI.treeView._getTreeItem("000000000000f") is not None + assert nwGUI.openDocument("000000000000f") is True nwGUI.docEditor.clear() # Test Faulty Inserts @@ -680,7 +677,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert not nwGUI.importDocument() # Open the document from before, and add some text to it - nwGUI.openDocument("0e17daca5f3e1") + nwGUI.openDocument("000000000000f") nwGUI.docEditor.setText("Bar") assert nwGUI.docEditor.getText() == "Bar" @@ -712,7 +709,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj): theBits = theMessage.split("
") assert len(theBits) == 2 assert theBits[0] == "The currently open file is saved in:" - assert theBits[1] == os.path.join(fncProj, "content", "0e17daca5f3e1.nwd") + assert theBits[1] == os.path.join(fncProj, "content", "000000000000f.nwd") # qtbot.stopForInteraction() diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 57e6acec..f2418a87 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -37,7 +37,6 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) nwGUI.openProject(nwMinimal) - nwGUI.theProject.projTree.setSeed(42) nwTree = nwGUI.novelView ## diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 1f1c8c95..d7d44a78 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -30,7 +30,7 @@ from novelwriter.enum import nwItemType, nwItemClass @pytest.mark.gui -def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): +def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): """Test adding and removing items from the project tree. """ # Block message box @@ -46,7 +46,6 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): assert nwTree.newTreeItem(nwItemType.FILE) is False # Create a project - nwGUI.theProject.projTree.setSeed(42) prjDir = os.path.join(fncDir, "project") assert nwGUI.newProject({"projPath": prjDir}) is True @@ -62,7 +61,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): # Create root item assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True - assert "1a6562590ef19" in nwGUI.theProject.projTree + assert "0000000000010" in nwGUI.theProject.projTree # File/Folder Items # ================= @@ -75,44 +74,44 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): assert "Did not find anywhere" in caplog.text # Create new folder as child of Novel folder - nwTree.setSelectedHandle("73475cb40a568") + nwTree.setSelectedHandle("0000000000008") assert nwTree.newTreeItem(nwItemType.FOLDER) is True - assert nwGUI.theProject.projTree["031b4af5197ec"].itemParent == "73475cb40a568" - assert nwGUI.theProject.projTree["031b4af5197ec"].itemRoot == "73475cb40a568" - assert nwGUI.theProject.projTree["031b4af5197ec"].itemClass == nwItemClass.NOVEL + assert nwGUI.theProject.projTree["0000000000011"].itemParent == "0000000000008" + assert nwGUI.theProject.projTree["0000000000011"].itemRoot == "0000000000008" + assert nwGUI.theProject.projTree["0000000000011"].itemClass == nwItemClass.NOVEL # Add a new file in the new folder - nwTree.setSelectedHandle("031b4af5197ec") + nwTree.setSelectedHandle("0000000000011") assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemParent == "031b4af5197ec" - assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemRoot == "73475cb40a568" - assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemClass == nwItemClass.NOVEL + assert nwGUI.theProject.projTree["0000000000012"].itemParent == "0000000000011" + assert nwGUI.theProject.projTree["0000000000012"].itemRoot == "0000000000008" + assert nwGUI.theProject.projTree["0000000000012"].itemClass == nwItemClass.NOVEL # Add a new file next to the other new file - nwTree.setSelectedHandle("41cfc0d1f2d12") + nwTree.setSelectedHandle("0000000000012") assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwGUI.theProject.projTree["2858dcd1057d3"].itemParent == "031b4af5197ec" - assert nwGUI.theProject.projTree["2858dcd1057d3"].itemRoot == "73475cb40a568" - assert nwGUI.theProject.projTree["2858dcd1057d3"].itemClass == nwItemClass.NOVEL - assert nwGUI.openDocument("2858dcd1057d3") + assert nwGUI.theProject.projTree["0000000000013"].itemParent == "0000000000011" + assert nwGUI.theProject.projTree["0000000000013"].itemRoot == "0000000000008" + assert nwGUI.theProject.projTree["0000000000013"].itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument("0000000000013") assert nwGUI.docEditor.getText() == "### New Document\n\n" # Add a new file to the characters folder - nwTree.setSelectedHandle("71ee45a3c0db9") + nwTree.setSelectedHandle("000000000000a") assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwGUI.theProject.projTree["2fca346db6561"].itemParent == "71ee45a3c0db9" - assert nwGUI.theProject.projTree["2fca346db6561"].itemRoot == "71ee45a3c0db9" - assert nwGUI.theProject.projTree["2fca346db6561"].itemClass == nwItemClass.CHARACTER - assert nwGUI.openDocument("2fca346db6561") + assert nwGUI.theProject.projTree["0000000000014"].itemParent == "000000000000a" + assert nwGUI.theProject.projTree["0000000000014"].itemRoot == "000000000000a" + assert nwGUI.theProject.projTree["0000000000014"].itemClass == nwItemClass.CHARACTER + assert nwGUI.openDocument("0000000000014") assert nwGUI.docEditor.getText() == "# New Note\n\n" # Make sure the sibling folder bug trap works - nwTree.setSelectedHandle("2858dcd1057d3") - nwGUI.theProject.projTree["2858dcd1057d3"].setParent(None) # This should not happen + nwTree.setSelectedHandle("0000000000013") + nwGUI.theProject.projTree["0000000000013"].setParent(None) # This should not happen caplog.clear() assert nwTree.newTreeItem(nwItemType.FILE) is False assert "Internal error" in caplog.text - nwGUI.theProject.projTree["2858dcd1057d3"].setParent("031b4af5197ec") + nwGUI.theProject.projTree["0000000000013"].setParent("0000000000011") # Get the trash folder nwTree._addTrashRoot() @@ -139,7 +138,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): @pytest.mark.gui -def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir): +def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): """Test adding and removing items from the project tree. """ # Block message box @@ -155,7 +154,6 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir): assert nwTree.moveTreeItem(1) is False # Create a project - nwGUI.theProject.projTree.setSeed(42) prjDir = os.path.join(fncDir, "project") assert nwGUI.newProject({"projPath": prjDir}) is True @@ -163,21 +161,21 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir): # ============== # Add some files - nwTree.setSelectedHandle("31489056e0916") + nwTree.setSelectedHandle("000000000000d") assert nwTree.newTreeItem(nwItemType.FILE) is True assert nwTree.newTreeItem(nwItemType.FILE) is True assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000011", "0000000000012", ] # Move item without focus monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) assert nwTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000011", "0000000000012", ] monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) @@ -186,78 +184,78 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir): assert nwTree.moveTreeItem(1) is False # Move second item up twice (should give same result) - nwTree.setSelectedHandle("0e17daca5f3e1") + nwTree.setSelectedHandle("000000000000f") assert nwTree.moveTreeItem(-1) is True - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "0e17daca5f3e1", "98010bd9270f9", - "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000f", "000000000000e", + "0000000000010", "0000000000011", "0000000000012", ] assert nwTree.moveTreeItem(-1) is False - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "0e17daca5f3e1", "98010bd9270f9", - "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000f", "000000000000e", + "0000000000010", "0000000000011", "0000000000012", ] # Restore via menu entry nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000011", "0000000000012", ] # Move fifth item down twice (should give same result) - nwTree.setSelectedHandle("031b4af5197ec") + nwTree.setSelectedHandle("0000000000011") assert nwTree.moveTreeItem(1) is True - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000012", "0000000000011", ] assert nwTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000012", "0000000000011", ] # Restore via menu entry nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000011", "0000000000012", ] # Move down again, and restore via undo - nwTree.setSelectedHandle("031b4af5197ec") + nwTree.setSelectedHandle("0000000000011") assert nwTree.moveTreeItem(1) is True - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000012", "0000000000011", ] nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000011", "0000000000012", ] # Root Folder # =========== - nwTree.setSelectedHandle("73475cb40a568") - assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0 + nwTree.setSelectedHandle("0000000000008") + assert nwGUI.theProject.projTree._treeOrder.index("0000000000008") == 0 # Move novel folder up assert nwTree.moveTreeItem(-1) is False nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0 + assert nwGUI.theProject.projTree._treeOrder.index("0000000000008") == 0 # Move novel folder down assert nwTree.moveTreeItem(1) is True nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 1 + assert nwGUI.theProject.projTree._treeOrder.index("0000000000008") == 1 # Move novel folder up again assert nwTree.moveTreeItem(-1) is True nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0 + assert nwGUI.theProject.projTree._treeOrder.index("0000000000008") == 0 # Clean up # qtbot.stopForInteraction() @@ -267,7 +265,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir): @pytest.mark.gui -def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): +def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): """Test adding and removing items from the project tree. """ # Block message box @@ -284,7 +282,6 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): assert nwTree.deleteItem() is False # Create a project - nwGUI.theProject.projTree.setSeed(42) prjDir = os.path.join(fncDir, "project") assert nwGUI.newProject({"projPath": prjDir}) is True @@ -292,13 +289,13 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): assert nwTree.emptyTrash() is False # Add some files - nwTree.setSelectedHandle("31489056e0916") + nwTree.setSelectedHandle("000000000000d") assert nwTree.newTreeItem(nwItemType.FILE) is True assert nwTree.newTreeItem(nwItemType.FILE) is True assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000011", "0000000000012", ] # Delete File @@ -306,7 +303,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): # Delete item without focus -> blocked monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - nwTree.setSelectedHandle("41cfc0d1f2d12") + nwTree.setSelectedHandle("0000000000012") caplog.clear() assert nwTree.deleteItem() is False assert "blocked" in caplog.text @@ -327,76 +324,76 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): # Block adding trash folder funcPointer = nwTree._addTrashRoot nwTree._addTrashRoot = lambda *a: None - assert nwTree.deleteItem("41cfc0d1f2d12") is False + assert nwTree.deleteItem("0000000000012") is False nwTree._addTrashRoot = funcPointer # Delete last two documents, which also adds the trash folder - assert nwTree.deleteItem("41cfc0d1f2d12") is True - assert nwTree.deleteItem("031b4af5197ec") is True - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", - "1a6562590ef19" + assert nwTree.deleteItem("0000000000012") is True + assert nwTree.deleteItem("0000000000011") is True + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010" ] trashHandle = nwGUI.theProject.projTree.trashRoot() assert nwTree.getTreeFromHandle(trashHandle) == [ - trashHandle, "41cfc0d1f2d12", "031b4af5197ec" + trashHandle, "0000000000012", "0000000000011" ] # Delete the first file again (permanent), and ask for permission # Also open the document in the editor, which should trigger a close - assert os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd")) - assert "41cfc0d1f2d12" in nwGUI.theProject.projTree + assert os.path.isfile(os.path.join(prjDir, "content", "0000000000012.nwd")) + assert "0000000000012" in nwGUI.theProject.projTree assert nwGUI.docEditor.docHandle() is None - assert nwGUI.openDocument("41cfc0d1f2d12") is True - assert nwGUI.docEditor.docHandle() == "41cfc0d1f2d12" - assert nwTree.deleteItem("41cfc0d1f2d12") is True + assert nwGUI.openDocument("0000000000012") is True + assert nwGUI.docEditor.docHandle() == "0000000000012" + assert nwTree.deleteItem("0000000000012") is True assert nwGUI.docEditor.docHandle() is None - assert not os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd")) - assert "41cfc0d1f2d12" not in nwGUI.theProject.projTree + assert not os.path.isfile(os.path.join(prjDir, "content", "0000000000012.nwd")) + assert "0000000000012" not in nwGUI.theProject.projTree assert nwTree.getTreeFromHandle(trashHandle) == [ - trashHandle, "031b4af5197ec" + trashHandle, "0000000000011" ] # Try to delete the second document, but block the deletion with monkeypatch.context() as mp: mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False) - assert nwTree.deleteItem("031b4af5197ec") is False + assert nwTree.deleteItem("0000000000011") is False # Delete proper, and skip asking for permission - assert os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd")) - assert "031b4af5197ec" in nwGUI.theProject.projTree - assert nwTree.deleteItem("031b4af5197ec", alreadyAsked=True) is True - assert not os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd")) - assert "031b4af5197ec" not in nwGUI.theProject.projTree + assert os.path.isfile(os.path.join(prjDir, "content", "0000000000011.nwd")) + assert "0000000000011" in nwGUI.theProject.projTree + assert nwTree.deleteItem("0000000000011", alreadyAsked=True) is True + assert not os.path.isfile(os.path.join(prjDir, "content", "0000000000011.nwd")) + assert "0000000000011" not in nwGUI.theProject.projTree assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] # Delete Folder/Root # ================== # Deleting non-empty folders is blocked - assert nwTree.deleteItem("31489056e0916") is False # Folder - assert nwTree.deleteItem("73475cb40a568") is False # Root + assert nwTree.deleteItem("000000000000d") is False # Folder + assert nwTree.deleteItem("0000000000008") is False # Root # Add a folder we can delete - nwTree.setSelectedHandle("71ee45a3c0db9") # Character Root + nwTree.setSelectedHandle("000000000000a") # Character Root assert nwTree.newTreeItem(nwItemType.FOLDER) is True - assert "2fca346db6561" in nwGUI.theProject.projTree + assert "0000000000014" in nwGUI.theProject.projTree # Try to delete, but block parent item lookup with monkeypatch.context() as mp: mp.setattr("PyQt5.QtWidgets.QTreeWidgetItem.parent", lambda *a: None) caplog.clear() - assert nwTree.deleteItem("2fca346db6561") is False + assert nwTree.deleteItem("0000000000014") is False assert "Could not delete folder" in caplog.text - assert "2fca346db6561" in nwGUI.theProject.projTree + assert "0000000000014" in nwGUI.theProject.projTree # Delete folder properly - assert nwTree.deleteItem("2fca346db6561") is True - assert "2fca346db6561" not in nwGUI.theProject.projTree + assert nwTree.deleteItem("0000000000014") is True + assert "0000000000014" not in nwGUI.theProject.projTree # Delete the Character root - assert nwTree.deleteItem("71ee45a3c0db9") is True - assert "71ee45a3c0db9" not in nwGUI.theProject.projTree + assert nwTree.deleteItem("000000000000a") is True + assert "000000000000a" not in nwGUI.theProject.projTree # Empty Trash # =========== @@ -408,13 +405,13 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): assert "already empty" in caplog.text # Move the two remaining scene documents to trash - assert nwTree.deleteItem("0e17daca5f3e1") is True - assert nwTree.deleteItem("1a6562590ef19") is True - assert nwTree.getTreeFromHandle("31489056e0916") == [ - "31489056e0916", "98010bd9270f9" + assert nwTree.deleteItem("000000000000f") is True + assert nwTree.deleteItem("0000000000010") is True + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e" ] assert nwTree.getTreeFromHandle(trashHandle) == [ - trashHandle, "0e17daca5f3e1", "1a6562590ef19" + trashHandle, "000000000000f", "0000000000010" ] # Empty trash, but select no on question diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 66329820..8993a2e8 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -29,14 +29,13 @@ from novelwriter.enum import nwState @pytest.mark.gui -def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj): +def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test the the various features of the status bar. """ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) is True - cHandle = nwGUI.theProject.newFile("A Note", "71ee45a3c0db9") + cHandle = nwGUI.theProject.newFile("A Note", "000000000000a") newDoc = NWDoc(nwGUI.theProject, cHandle) newDoc.writeDocument("# A Note\n\n") nwGUI.treeView.revealNewTreeItem(cHandle) diff --git a/tests/test_tools/test_tools_lipsum.py b/tests/test_tools/test_tools_lipsum.py index d4596546..7d6c08f9 100644 --- a/tests/test_tools/test_tools_lipsum.py +++ b/tests/test_tools/test_tools_lipsum.py @@ -29,7 +29,7 @@ from novelwriter.tools import GuiLipsum @pytest.mark.gui -def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj): +def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test the Lorem Ipsum tool. """ # Block message box @@ -40,9 +40,8 @@ def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj): assert getGuiItem("GuiLipsum") is None # Create a new project - nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) is True - assert nwGUI.openDocument("0e17daca5f3e1") is True + assert nwGUI.openDocument("000000000000f") is True assert len(nwGUI.docEditor.getText()) == 15 # Open the tool From 147b9a4501076f421c7bd406d1f4d77b49e6d3d2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 15:17:20 +0200 Subject: [PATCH 043/179] Fix the redundant spaces highlight option text in Preferences: issue #1043 --- novelwriter/dialogs/preferences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 81bef52a..c38a0c5f 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -902,7 +902,7 @@ class GuiPreferencesSyntax(QWidget): self.showMultiSpaces = QSwitch() self.showMultiSpaces.setChecked(self.mainConf.showMultiSpaces) self.mainForm.addRow( - self.tr("Highlight multiple spaces"), + self.tr("Highlight multiple or trailing spaces"), self.showMultiSpaces, self.tr("Applies to the document editor only.") ) From 31fac7bf4513ec70a0d9ac4ebbec334ae1cc609c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 15:28:26 +0200 Subject: [PATCH 044/179] Make the test suite less spammy again --- tests/conftest.py | 4 +--- tests/test_gui/test_gui_projtree.py | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c6634f7c..769f855c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -164,9 +164,7 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf): """ monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) monkeypatch.setattr("novelwriter.CONFIG", fncConf) - nwGUI = novelwriter.main( - ["--testmode", "--info", "--config=%s" % fncDir, "--data=%s" % fncDir] - ) + nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.wait(20) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index d7d44a78..84563086 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -304,9 +304,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Delete item without focus -> blocked monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) nwTree.setSelectedHandle("0000000000012") - caplog.clear() assert nwTree.deleteItem() is False - assert "blocked" in caplog.text monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # No selection made @@ -399,10 +397,8 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # =========== # Try to empty trash that is already empty - caplog.clear() assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] assert nwTree.emptyTrash() is False - assert "already empty" in caplog.text # Move the two remaining scene documents to trash assert nwTree.deleteItem("000000000000f") is True From 64da2793ac061e278319b3f6f17f7f0049909b96 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 15:38:53 +0200 Subject: [PATCH 045/179] Allow file items to save expanded status --- novelwriter/core/item.py | 3 +- tests/lipsum/nwProject.nwx | 36 +++++++++---------- tests/minimal/nwProject.nwx | 12 +++---- .../coreProject_NewCustomA_nwProject.nwx | 28 +++++++-------- .../coreProject_NewCustomB_nwProject.nwx | 16 ++++----- .../coreProject_NewFile_nwProject.nwx | 12 +++---- .../coreProject_NewMinimal_nwProject.nwx | 8 ++--- .../coreProject_NewRoot_nwProject.nwx | 8 ++--- .../guiEditor_Main_Final_nwProject.nwx | 16 ++++----- .../guiEditor_Main_Initial_nwProject.nwx | 8 ++--- .../guiProjSettings_Dialog_nwProject.nwx | 8 ++--- tests/test_core/test_core_item.py | 5 +-- tests/test_core/test_core_tree.py | 12 +++---- 13 files changed, 86 insertions(+), 86 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index f6d93b36..0aed8991 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -160,13 +160,12 @@ class NWItem(): itemAttrib["layout"] = str(self._layout.name) metaAttrib = {} + metaAttrib["expanded"] = str(self._expanded) if self._type == nwItemType.FILE: metaAttrib["charCount"] = str(self._charCount) metaAttrib["wordCount"] = str(self._wordCount) metaAttrib["paraCount"] = str(self._paraCount) metaAttrib["cursorPos"] = str(self._cursorPos) - else: - metaAttrib["expanded"] = str(self._expanded) nameAttrib = {} nameAttrib["status"] = str(self._status) diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index d8d7a49e..02e3df49 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,12 +1,12 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 24 + 26 24 - 1856 + 1863 False @@ -49,19 +49,19 @@ Novel - + Lorem Ipsum - + Front Matter - + Prologue - + Act One @@ -69,19 +69,19 @@ Chapter One - + Chapter One - + Scene One - + Scene Two - + Interlude @@ -89,19 +89,19 @@ Chapter Two - + Chapter Two - + Scene Three - + Scene Four - + Scene Five @@ -109,7 +109,7 @@ Characters - + Mr. Nobody @@ -117,7 +117,7 @@ Plot - + Main @@ -125,7 +125,7 @@ World - + Ancient Europe diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index 6945dc08..af7595a4 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,13 +1,13 @@ - + Test Minimal Minimal Jane Doe John Doh - 15 + 17 2 - 146 + 150 True @@ -47,7 +47,7 @@ Novel - + Title Page @@ -55,11 +55,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 8747363b..14abf9e2 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -71,7 +71,7 @@ Entities - + Title Page @@ -79,19 +79,19 @@ Chapter 1 - + Chapter 1 - + Scene 1.1 - + Scene 1.2 - + Scene 1.3 @@ -99,19 +99,19 @@ Chapter 2 - + Chapter 2 - + Scene 2.1 - + Scene 2.2 - + Scene 2.3 @@ -119,19 +119,19 @@ Chapter 3 - + Chapter 3 - + Scene 3.1 - + Scene 3.2 - + Scene 3.3 diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 7f397e85..bca6ea80 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -71,31 +71,31 @@ Entities - + Title Page - + Scene 1 - + Scene 2 - + Scene 3 - + Scene 4 - + Scene 5 - + Scene 6 diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index 86f9cf11..d253ebcc 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -57,7 +57,7 @@ World - + Title Page @@ -65,19 +65,19 @@ New Chapter - + New Chapter - + New Scene - + Hello - + Jane diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 37a5428b..a6711a84 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -57,7 +57,7 @@ World - + Title Page @@ -65,11 +65,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 67c49f05..2ab62301 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -57,7 +57,7 @@ World - + Title Page @@ -65,11 +65,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 3900c6d2..61abe4f3 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,11 +1,11 @@ - + New Project 5 2 - 4 + 3 True @@ -45,7 +45,7 @@ Novel - + Title Page @@ -53,11 +53,11 @@ New Chapter - + New Chapter - + New Scene @@ -65,7 +65,7 @@ Plot - + New Note @@ -73,7 +73,7 @@ Characters - + New Note @@ -81,7 +81,7 @@ World - + New Note diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 66a4c94e..1a5fd5be 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -45,7 +45,7 @@ Novel - + Title Page @@ -53,11 +53,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index 77d01d45..326c63e5 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -51,7 +51,7 @@ Novel - + Title Page @@ -59,11 +59,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 572b31d7..dec95cd7 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -500,8 +500,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, mockRnd): assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( b'' b'A Name' + b'type="FILE" class="NOVEL" layout="NOTE">A Name' b'' ) % bytes(importKeys[3], encoding="utf8") diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index c70cc4bb..e8734936 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -419,12 +419,12 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'type="FOLDER" class="NOVEL">Act One' b'Chapter One' b'Scene One' b'Characters' b'Jane Doe' b'' b'' From 1a47bbaf8c1f1bd95979e3cc109842c1ccc69fe0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 15:53:14 +0200 Subject: [PATCH 046/179] Allow files to have child items --- novelwriter/gui/projtree.py | 9 ++--- sample/nwProject.nwx | 68 ++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 23238b84..c912bb18 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -793,17 +793,12 @@ class GuiProjectTree(QTreeWidget): # - Files can be moved anywhere # - Folders can only be moved within the same root folder # - Root folders cannot be moved at all - # - Items cannot be dropped on top of a file (moved inside) isFile = snItem.itemType == nwItemType.FILE isRoot = snItem.itemType == nwItemType.ROOT - onFile = dnItem.itemType == nwItemType.FILE inSame = snItem.itemRoot == dnItem.itemRoot - allowDrop = inSame or isFile - allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile) - - if allowDrop and not isRoot: + if (inSame or isFile) and not isRoot: logger.debug("Drag'n'drop of item '%s' accepted", sHandle) wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) @@ -1057,7 +1052,7 @@ class GuiProjectTreeMenu(QMenu): trashHandle = self.theTree.theProject.projTree.trashRoot() - inTrash = theItem.itemParent == trashHandle and trashHandle is not None + inTrash = self.theTree.theProject.projTree.isTrash(theItem.itemHandle) isTrash = theItem.itemHandle == trashHandle and trashHandle is not None isFile = theItem.itemType == nwItemType.FILE diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 23660870..6bb6fdd4 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1306 - 199 - 65149 + 1308 + 201 + 65350 False @@ -54,47 +54,47 @@ Novel - + Title Page - + Page - + Part One A Folder - - - Chapter One - - - - Making a Scene - - - - Another Scene - - - + + Interlude - - + + + Chapter One + + + + Making a Scene + + + + Another Scene + + + A Note on Structure - - + + Chapter Two - - + + We Found John! @@ -106,11 +106,11 @@ Main Characters - + John Smith - + Jane Smith @@ -118,15 +118,15 @@ Locations - + Earth - + Space - + Mars @@ -138,7 +138,7 @@ Scenes - + Old File @@ -146,7 +146,7 @@ Trash - + Delete Me! From 5cdcda66a5b98fadb5b78f5fd568dd37eada8dc3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 16:09:50 +0200 Subject: [PATCH 047/179] Update data recursively when moving an item with child items --- novelwriter/gui/projtree.py | 48 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index c912bb18..5358b082 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -435,7 +435,6 @@ class GuiProjectTree(QTreeWidget): logger.error("Could not find tree item for deletion") return False - wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole)) if nwItemS.itemType == nwItemType.FILE: logger.debug("User requested file '%s' deleted", tHandle) trItemP = trItemS.parent() @@ -494,7 +493,7 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) - self._postItemMove(tHandle, wCount) + self._postItemMove(tHandle) self._recordLastMove(trItemS, trItemP, tIndex) self._setTreeChanged(True) @@ -646,7 +645,6 @@ class GuiProjectTree(QTreeWidget): return False dstIndex = min(max(0, dstIndex), dstItem.childCount()) - wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole)) sHandle = srcItem.data(self.C_NAME, Qt.UserRole) dHandle = dstItem.data(self.C_NAME, Qt.UserRole) logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex) @@ -657,7 +655,7 @@ class GuiProjectTree(QTreeWidget): movItem = parItem.takeChild(srcIndex) dstItem.insertChild(dstIndex, movItem) - self._postItemMove(sHandle, wCount) + self._postItemMove(sHandle) self.clearSelection() movItem.setSelected(True) @@ -801,11 +799,9 @@ class GuiProjectTree(QTreeWidget): if (inSame or isFile) and not isRoot: logger.debug("Drag'n'drop of item '%s' accepted", sHandle) - wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) self.propagateCount(sHandle, 0) - QTreeWidget.dropEvent(self, theEvent) - self._postItemMove(sHandle, wCount) + self._postItemMove(sHandle) self._recordLastMove(sItem, pItem, pIndex) else: @@ -822,7 +818,7 @@ class GuiProjectTree(QTreeWidget): # Internal Functions ## - def _postItemMove(self, tHandle, wCount): + def _postItemMove(self, tHandle): """Run various maintenance tasks for a moved item. """ trItemS = self._getTreeItem(tHandle) @@ -836,19 +832,23 @@ class GuiProjectTree(QTreeWidget): # is updated accordingly, and update word count pHandle = trItemP.data(self.C_NAME, Qt.UserRole) nwItemS.setParent(pHandle) - self.theProject.projTree.updateItemData(tHandle) - self.setTreeItemValues(tHandle) - self.propagateCount(tHandle, wCount) - logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle) - # The items dropped into archive or trash should be removed - # from the project index, for all other items, we rescan the - # file to ensure the index is up to date. - if nwItemS.isInactive(): - self.theIndex.deleteHandle(tHandle) - else: - self.theIndex.reIndexHandle(tHandle) + mHandles = self.getTreeFromHandle(tHandle) + logger.debug("A total of %d item(s) were moved", len(mHandles)) + for mHandle in mHandles: + logger.debug("Updating item '%s'", mHandle) + wCount = self._getItemWordCount(mHandle) + self.theProject.projTree.updateItemData(mHandle) + + # Update the index + if nwItemS.isInactive(): + self.theIndex.deleteHandle(tHandle) + else: + self.theIndex.reIndexHandle(tHandle) + + self.setTreeItemValues(mHandle) + self.propagateCount(mHandle, wCount) # Trigger dependent updates self._setTreeChanged(True) @@ -856,8 +856,16 @@ class GuiProjectTree(QTreeWidget): return True + def _getItemWordCount(self, tHandle): + """Retrun the word count of a given item handle. + """ + tItem = self._getTreeItem(tHandle) + if tItem is None: + return 0 + return int(tItem.data(self.C_COUNT, Qt.UserRole)) + def _getTreeItem(self, tHandle): - """Returns the QTreeWidgetItem of a given item handle. + """Return the QTreeWidgetItem of a given item handle. """ return self._treeMap.get(tHandle, None) From d96c8ddf8509d60c14f236abde9560694a6b6a3b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 16:21:20 +0200 Subject: [PATCH 048/179] Remove all restrictions on drag and drop except in ROOT items --- novelwriter/gui/projtree.py | 44 +++++++++++-------------------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 5358b082..39445dee 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -762,13 +762,9 @@ class GuiProjectTree(QTreeWidget): drop is allowed or not. Disallowed drops are cancelled. """ sHandle = self.getSelectedHandle() - if sHandle is None: - logger.error("No handle selected") - return - dIndex = self.indexAt(theEvent.pos()) - if not dIndex.isValid(): - logger.error("Invalid drop index") + if sHandle is None or not dIndex.isValid(): + logger.error("Invalid drag and drop event") return sItem = self._getTreeItem(sHandle) @@ -776,41 +772,27 @@ class GuiProjectTree(QTreeWidget): dHandle = dItem.data(self.C_NAME, Qt.UserRole) snItem = self.theProject.projTree[sHandle] dnItem = self.theProject.projTree[dHandle] - if dnItem is None: + + if snItem.itemType == nwItemType.ROOT or dnItem is None: + logger.debug("Drag'n'drop of item '%s' not accepted", sHandle) + theEvent.ignore() self.theParent.makeAlert(self.tr( "The item cannot be moved to that location." ), nwAlert.ERROR) return + logger.debug("Drag'n'drop of item '%s' accepted", sHandle) + + self.propagateCount(sHandle, 0) + QTreeWidget.dropEvent(self, theEvent) + self._postItemMove(sHandle) + pItem = sItem.parent() pIndex = 0 if pItem is not None: pIndex = pItem.indexOfChild(sItem) - # Determine if the drag and drop is allowed: - # - Files can be moved anywhere - # - Folders can only be moved within the same root folder - # - Root folders cannot be moved at all - - isFile = snItem.itemType == nwItemType.FILE - isRoot = snItem.itemType == nwItemType.ROOT - inSame = snItem.itemRoot == dnItem.itemRoot - - if (inSame or isFile) and not isRoot: - logger.debug("Drag'n'drop of item '%s' accepted", sHandle) - - self.propagateCount(sHandle, 0) - QTreeWidget.dropEvent(self, theEvent) - self._postItemMove(sHandle) - self._recordLastMove(sItem, pItem, pIndex) - - else: - logger.debug("Drag'n'drop of item '%s' not accepted", sHandle) - - theEvent.ignore() - self.theParent.makeAlert(self.tr( - "The item cannot be moved to that location." - ), nwAlert.ERROR) + self._recordLastMove(sItem, pItem, pIndex) return From bd5a14b18ecca3c21f7e20d18b08fc25535bf251 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 16:57:13 +0200 Subject: [PATCH 049/179] Block drag and drop for ROOT items on the widget level, and drop the TRASH item type --- novelwriter/core/item.py | 2 ++ novelwriter/core/project.py | 2 +- novelwriter/core/tree.py | 15 +++++++-------- novelwriter/enum.py | 1 - novelwriter/gui/projtree.py | 22 ++++------------------ novelwriter/gui/theme.py | 3 --- sample/nwProject.nwx | 8 ++++---- 7 files changed, 18 insertions(+), 35 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 0aed8991..e23b4fb9 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -408,6 +408,8 @@ class NWItem(): self._type = value elif isItemType(value): self._type = nwItemType[value] + elif value == "TRASH": + self._type = nwItemType.ROOT else: logger.error("Unrecognised item type '%s'", value) self._type = nwItemType.NO_TYPE diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 0d24d74c..26efb323 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -158,7 +158,7 @@ class NWProject(): if trashHandle is None: newItem = NWItem(self) newItem.setName(trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])) - newItem.setType(nwItemType.TRASH) + newItem.setType(nwItemType.ROOT) newItem.setClass(nwItemClass.TRASH) self.projTree.append(None, None, newItem) self.projTree.updateItemData(newItem.itemHandle) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index a158c37a..eb349bf8 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -100,14 +100,13 @@ class NWTree(): if nwItem.itemClass == nwItemClass.ARCHIVE: logger.verbose("Item '%s' is the archive folder", str(tHandle)) self._archRoot = tHandle - - if nwItem.itemType == nwItemType.TRASH: - if self._trashRoot is None: - logger.verbose("Item '%s' is the trash folder", str(tHandle)) - self._trashRoot = tHandle - else: - logger.error("Only one trash folder allowed") - return False + elif nwItem.itemClass == nwItemClass.TRASH: + if self._trashRoot is None: + logger.verbose("Item '%s' is the trash folder", str(tHandle)) + self._trashRoot = tHandle + else: + logger.error("Only one trash folder allowed") + return False self._projTree[tHandle] = nwItem self._treeOrder.append(tHandle) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 3360d8c5..56340541 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -32,7 +32,6 @@ class nwItemType(Enum): ROOT = 1 FOLDER = 2 FILE = 3 - TRASH = 4 # END Enum nwItemType diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 39445dee..7f5dc6f3 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -762,31 +762,18 @@ class GuiProjectTree(QTreeWidget): drop is allowed or not. Disallowed drops are cancelled. """ sHandle = self.getSelectedHandle() - dIndex = self.indexAt(theEvent.pos()) - if sHandle is None or not dIndex.isValid(): + if sHandle is None: logger.error("Invalid drag and drop event") return - sItem = self._getTreeItem(sHandle) - dItem = self.itemFromIndex(dIndex) - dHandle = dItem.data(self.C_NAME, Qt.UserRole) - snItem = self.theProject.projTree[sHandle] - dnItem = self.theProject.projTree[dHandle] - - if snItem.itemType == nwItemType.ROOT or dnItem is None: - logger.debug("Drag'n'drop of item '%s' not accepted", sHandle) - theEvent.ignore() - self.theParent.makeAlert(self.tr( - "The item cannot be moved to that location." - ), nwAlert.ERROR) - return - logger.debug("Drag'n'drop of item '%s' accepted", sHandle) self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) self._postItemMove(sHandle) + # Record undo information + sItem = self._getTreeItem(sHandle) pItem = sItem.parent() pIndex = 0 if pItem is not None: @@ -895,8 +882,7 @@ class GuiProjectTree(QTreeWidget): self._treeMap[tHandle] = newItem if pHandle is None: if nwItem.itemType == nwItemType.ROOT: - self.addTopLevelItem(newItem) - elif nwItem.itemType == nwItemType.TRASH: + newItem.setFlags(newItem.flags() ^ Qt.ItemIsDragEnabled) self.addTopLevelItem(newItem) else: self.theParent.makeAlert(self.tr( diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 3c180b50..19fe823a 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -639,9 +639,6 @@ class GuiIcons: iconName = "proj_scene" elif tLayout == nwItemLayout.NOTE: iconName = "proj_note" - elif tType == nwItemType.TRASH: - iconName = nwLabels.CLASS_ICON[tClass] - if iconName is None: return QIcon() diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 6bb6fdd4..b2ac1c0e 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1308 + 1309 201 - 65350 + 65353 False @@ -141,7 +141,7 @@ Old File - + Trash From 821833df8ec17c4d638cf6dbfdfe26f76ee21e5b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 17:05:29 +0200 Subject: [PATCH 050/179] Fix tests and drop unused function from NWTree class --- novelwriter/core/tree.py | 24 ------------------- .../guiEditor_Main_Final_nwProject.nwx | 4 ++-- tests/test_base/test_base_common.py | 16 ++++++++++++- tests/test_core/test_core_item.py | 6 +++-- tests/test_core/test_core_tree.py | 12 +++------- 5 files changed, 24 insertions(+), 38 deletions(-) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index eb349bf8..ee57e0db 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -351,30 +351,6 @@ class NWTree(): return True - ## - # Getters - ## - - def countTypes(self): - """Count the number of files, folders and roots in the project. - """ - nRoot = 0 - nFolder = 0 - nFile = 0 - - for tHandle in self._treeOrder: - tItem = self.__getitem__(tHandle) - if tItem is None: - continue - elif tItem.itemType == nwItemType.ROOT: - nRoot += 1 - elif tItem.itemType == nwItemType.FOLDER: - nFolder += 1 - elif tItem.itemType == nwItemType.FILE: - nFile += 1 - - return nRoot, nFolder, nFile - ## # Meta Methods ## diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 61abe4f3..e29290ca 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -84,7 +84,7 @@ New Note - + Trash diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index d7ad4119..0ce153b4 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -161,6 +161,7 @@ def testBaseCommon_IsItemClass(): assert isItemClass("ARCHIVE") is True assert isItemClass("TRASH") is True + # Invalid assert isItemClass("None") is False assert isItemClass(None) is False assert isItemClass("STUFF") is False @@ -176,8 +177,11 @@ def testBaseCommon_IsItemType(): assert isItemType("ROOT") is True assert isItemType("FOLDER") is True assert isItemType("FILE") is True - assert isItemType("TRASH") is True + # Deprecated Type + assert isItemType("TRASH") is False + + # Invalid assert isItemType("None") is False assert isItemType(None) is False assert isItemType("STUFF") is False @@ -193,6 +197,16 @@ def testBaseCommon_IsItemLayout(): assert isItemLayout("DOCUMENT") is True assert isItemLayout("NOTE") is True + # Deprecated Layouts + assert isItemLayout("TITLE") is False + assert isItemLayout("PAGE") is False + assert isItemLayout("BOOK") is False + assert isItemLayout("PARTITION") is False + assert isItemLayout("UNNUMBERED") is False + assert isItemLayout("CHAPTER") is False + assert isItemLayout("SCENE") is False + + # Invalid assert isItemLayout("None") is False assert isItemLayout(None) is False assert isItemLayout("STUFF") is False diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index dec95cd7..fbc0ded3 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -283,8 +283,6 @@ def testCoreItem_TypeSetter(mockGUI): assert theItem.itemType == nwItemType.FOLDER theItem.setType("FILE") assert theItem.itemType == nwItemType.FILE - theItem.setType("TRASH") - assert theItem.itemType == nwItemType.TRASH # Alternative theItem.setType(nwItemType.ROOT) @@ -712,4 +710,8 @@ def testCoreItem_ConvertFromFmt13(mockGUI): assert theItem.itemType == nwItemType.FILE assert theItem.itemLayout == nwItemLayout.DOCUMENT + # Deprecated Type + theItem.setType("TRASH") + assert theItem.itemType == nwItemType.ROOT + # END Test testCoreItem_ConvertFromFmt13 diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index e8734936..5c05f679 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -76,7 +76,7 @@ def mockItems(mockGUI, mockRnd): itemF = NWItem(theProject) itemF._name = "Trash" - itemF._type = nwItemType.TRASH + itemF._type = nwItemType.ROOT itemF._class = nwItemClass.TRASH itemF._expanded = False @@ -172,7 +172,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): # Try to add another trash folder itemT = NWItem(theProject) itemT._name = "Trash" - itemT._type = nwItemType.TRASH + itemT._type = nwItemType.ROOT itemT._class = nwItemClass.TRASH itemT._expanded = False @@ -353,12 +353,6 @@ def testCoreTree_Stats(mockGUI, mockItems): assert novelWords == 550 assert noteWords == 400 - # Count types - nRoot, nFolder, nFile = theTree.countTypes() - assert nRoot == 3 - assert nFolder == 1 - assert nFile == 3 - # END Test testCoreTree_Stats @@ -429,7 +423,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'Outtakes' - b'Trash' b' Date: Sat, 23 Apr 2022 18:03:03 +0200 Subject: [PATCH 051/179] Fix drag and drop word propagation --- novelwriter/gui/projtree.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 7f5dc6f3..44672a89 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -435,6 +435,7 @@ class GuiProjectTree(QTreeWidget): logger.error("Could not find tree item for deletion") return False + wCount = self._getItemWordCount(tHandle) if nwItemS.itemType == nwItemType.FILE: logger.debug("User requested file '%s' deleted", tHandle) trItemP = trItemS.parent() @@ -493,7 +494,7 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) - self._postItemMove(tHandle) + self._postItemMove(tHandle, wCount) self._recordLastMove(trItemS, trItemP, tIndex) self._setTreeChanged(True) @@ -649,13 +650,14 @@ class GuiProjectTree(QTreeWidget): dHandle = dstItem.data(self.C_NAME, Qt.UserRole) logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex) + wCount = self._getItemWordCount(sHandle) self.propagateCount(sHandle, 0) parItem = srcItem.parent() srcIndex = parItem.indexOfChild(srcItem) movItem = parItem.takeChild(srcIndex) dstItem.insertChild(dstIndex, movItem) - self._postItemMove(sHandle) + self._postItemMove(sHandle, wCount) self.clearSelection() movItem.setSelected(True) @@ -768,18 +770,23 @@ class GuiProjectTree(QTreeWidget): logger.debug("Drag'n'drop of item '%s' accepted", sHandle) - self.propagateCount(sHandle, 0) - QTreeWidget.dropEvent(self, theEvent) - self._postItemMove(sHandle) - - # Record undo information sItem = self._getTreeItem(sHandle) + isExpanded = False + if sItem is not None: + isExpanded = sItem.isExpanded() + pItem = sItem.parent() pIndex = 0 if pItem is not None: pIndex = pItem.indexOfChild(sItem) + wCount = self._getItemWordCount(sHandle) + self.propagateCount(sHandle, 0) + + QTreeWidget.dropEvent(self, theEvent) + self._postItemMove(sHandle, wCount) self._recordLastMove(sItem, pItem, pIndex) + sItem.setExpanded(isExpanded) return @@ -787,7 +794,7 @@ class GuiProjectTree(QTreeWidget): # Internal Functions ## - def _postItemMove(self, tHandle): + def _postItemMove(self, tHandle, wCount): """Run various maintenance tasks for a moved item. """ trItemS = self._getTreeItem(tHandle) @@ -807,19 +814,18 @@ class GuiProjectTree(QTreeWidget): logger.debug("A total of %d item(s) were moved", len(mHandles)) for mHandle in mHandles: logger.debug("Updating item '%s'", mHandle) - wCount = self._getItemWordCount(mHandle) self.theProject.projTree.updateItemData(mHandle) # Update the index if nwItemS.isInactive(): - self.theIndex.deleteHandle(tHandle) + self.theIndex.deleteHandle(mHandle) else: - self.theIndex.reIndexHandle(tHandle) + self.theIndex.reIndexHandle(mHandle) self.setTreeItemValues(mHandle) - self.propagateCount(mHandle, wCount) # Trigger dependent updates + self.propagateCount(tHandle, wCount) self._setTreeChanged(True) self._emitItemChange(tHandle) From 732f20a0f13bbef5ff12f7bcc81f33883f6ee5b0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 19:00:11 +0200 Subject: [PATCH 052/179] Fix word count accounting and double click tree expansion --- novelwriter/gui/projtree.py | 31 ++++++++++++++++++++++++------- novelwriter/guimain.py | 12 +++++++++--- sample/content/636b6aa9b697b.nwd | 2 +- sample/content/ae7339df26ded.nwd | 2 +- sample/content/bc0cbd2a407f3.nwd | 2 +- sample/nwProject.nwx | 22 +++++++++++----------- 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 44672a89..eadbe5b8 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -82,7 +82,7 @@ class GuiProjectTree(QTreeWidget): # Tree Settings iPx = self.theTheme.baseIconSize self.setIconSize(QSize(iPx, iPx)) - self.setExpandsOnDoubleClick(True) + self.setExpandsOnDoubleClick(False) self.setIndentation(iPx) self.setColumnCount(4) self.setHeaderLabels([ @@ -349,6 +349,14 @@ class GuiProjectTree(QTreeWidget): theList = self._scanChildren(theList, theItem, 0) return theList + def toggleExpanded(self, tHandle): + """Expand an item based on its handle. + """ + trItem = self._getTreeItem(tHandle) + if trItem is not None: + trItem.setExpanded(not trItem.isExpanded()) + return + def getColumnSizes(self): """Return the column widths for the tree columns. """ @@ -576,7 +584,7 @@ class GuiProjectTree(QTreeWidget): return - def propagateCount(self, tHandle, theCount): + def propagateCount(self, tHandle, newCount, countChildren=False): """Recursive function setting the word count for a given item, and propagating that count upwards in the tree until reaching a root item. This function is more efficient than recalculating @@ -588,8 +596,12 @@ class GuiProjectTree(QTreeWidget): if tItem is None: return - tItem.setText(self.C_COUNT, f"{theCount:n}") - tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount)) + if countChildren: + for i in range(tItem.childCount()): + newCount += int(tItem.child(i).data(self.C_COUNT, Qt.UserRole)) + + tItem.setText(self.C_COUNT, f"{newCount:n}") + tItem.setData(self.C_COUNT, Qt.UserRole, int(newCount)) pItem = tItem.parent() if pItem is None: @@ -602,7 +614,12 @@ class GuiProjectTree(QTreeWidget): pHandle = pItem.data(self.C_NAME, Qt.UserRole) if pHandle: - self.propagateCount(pHandle, pCount) + if self.theProject.projTree.checkType(pHandle, nwItemType.FILE): + # A file has an internal word count we need to account + # for, but a folder always has 0 words on its own. + pCount += self.theIndex.getCounts(pHandle)[1] + + self.propagateCount(pHandle, pCount, countChildren=False) return @@ -724,7 +741,7 @@ class GuiProjectTree(QTreeWidget): def doUpdateCounts(self, tHandle, cCount, wCount, pCount): """Slot for updating the word count of a specific item. """ - self.propagateCount(tHandle, wCount) + self.propagateCount(tHandle, wCount, countChildren=True) self.wordCountsChanged.emit() return @@ -908,7 +925,7 @@ class GuiProjectTree(QTreeWidget): self._treeMap[pHandle].insertChild(byIndex+1, newItem) else: self._treeMap[pHandle].addChild(newItem) - self.propagateCount(tHandle, nwItem.wordCount) + self.propagateCount(tHandle, nwItem.wordCount, countChildren=True) self.setTreeItemValues(tHandle) newItem.setExpanded(nwItem.isExpanded) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index fabd3867..60fe0d23 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -906,7 +906,7 @@ class GuiMain(QMainWindow): tItem.setCharCount(cC) tItem.setWordCount(wC) tItem.setParaCount(pC) - self.treeView.propagateCount(tItem.itemHandle, wC) + self.treeView.propagateCount(tItem.itemHandle, wC, countChildren=True) self.treeView.setTreeItemValues(tItem.itemHandle) tEnd = time() @@ -1568,11 +1568,17 @@ class GuiMain(QMainWindow): @pyqtSlot("QTreeWidgetItem*", int) def _treeDoubleClick(self, tItem, colNo): """The user double-clicked an item in the tree. If it is a file, - we open it. Otherwise, we do nothing. + we open it. Otherwise, we toggle the expanded status. """ tHandle = self.treeView.getSelectedHandle() if tHandle is not None: - self.openDocument(tHandle, changeFocus=False, doScroll=False) + tItem = self.theProject.projTree[tHandle] + if tItem is None: + return + if tItem.itemType == nwItemType.FILE: + self.openDocument(tHandle, changeFocus=False, doScroll=False) + else: + self.treeView.toggleExpanded(tHandle) return @pyqtSlot() diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 8fe96042..2927b7d0 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,5 +1,5 @@ %%~name: Making a Scene -%%~path: e7ded148d6e4a/636b6aa9b697b +%%~path: 6a2d6d5f4f401/636b6aa9b697b %%~kind: NOVEL/DOCUMENT ### Making a Scene diff --git a/sample/content/ae7339df26ded.nwd b/sample/content/ae7339df26ded.nwd index 9b135713..1eb7a65d 100644 --- a/sample/content/ae7339df26ded.nwd +++ b/sample/content/ae7339df26ded.nwd @@ -1,5 +1,5 @@ %%~name: We Found John! -%%~path: e7ded148d6e4a/ae7339df26ded +%%~path: 88706ddc78b1b/ae7339df26ded %%~kind: NOVEL/DOCUMENT ### We Found John! diff --git a/sample/content/bc0cbd2a407f3.nwd b/sample/content/bc0cbd2a407f3.nwd index 3c95e8ff..4ebbca1d 100644 --- a/sample/content/bc0cbd2a407f3.nwd +++ b/sample/content/bc0cbd2a407f3.nwd @@ -1,5 +1,5 @@ %%~name: Another Scene -%%~path: e7ded148d6e4a/bc0cbd2a407f3 +%%~path: 6a2d6d5f4f401/bc0cbd2a407f3 %%~kind: NOVEL/DOCUMENT ### Another Scene diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index b2ac1c0e..3a012406 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1309 - 201 - 65353 + 1323 + 207 + 66237 False @@ -69,22 +69,22 @@ A Folder - - - Interlude - - + Chapter One - + Making a Scene Another Scene + + + Interlude + A Note on Structure @@ -138,7 +138,7 @@ Scenes - + Old File From d9e3fd7a01441c7ac083fe0f0a8f1cc35057ab88 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 19:34:40 +0200 Subject: [PATCH 053/179] Make some minor improvements to how expanded status is saved for items --- novelwriter/gui/projtree.py | 10 ++++++++-- sample/nwProject.nwx | 8 ++++---- tests/reference/guiEditor_Main_Final_nwProject.nwx | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index eadbe5b8..cb59c9ee 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -825,6 +825,7 @@ class GuiProjectTree(QTreeWidget): # is updated accordingly, and update word count pHandle = trItemP.data(self.C_NAME, Qt.UserRole) nwItemS.setParent(pHandle) + trItemP.setExpanded(True) logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle) mHandles = self.getTreeFromHandle(tHandle) @@ -873,12 +874,17 @@ class GuiProjectTree(QTreeWidget): starting at a given QTreeWidgetItem. """ tHandle = tItem.data(self.C_NAME, Qt.UserRole) + cCount = tItem.childCount() + + # Update tree-related meta data nwItem = self.theProject.projTree[tHandle] - nwItem.setExpanded(tItem.isExpanded()) + nwItem.setExpanded(tItem.isExpanded() and cCount > 0) nwItem.setOrder(tIndex) + theList.append(tHandle) - for i in range(tItem.childCount()): + for i in range(cCount): self._scanChildren(theList, tItem.child(i), i) + return theList def _addTreeItem(self, nwItem, nHandle=None): diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 3a012406..406fa423 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1323 + 1327 207 - 66237 + 66285 False @@ -74,7 +74,7 @@ Chapter One - + Making a Scene diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index e29290ca..c4ba847c 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -85,7 +85,7 @@ New Note - + Trash From 9bc7250532aa038dca8877ff774ab1ada5c91504 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 21:33:11 +0200 Subject: [PATCH 054/179] Allow deleting non-empty folders --- novelwriter/gui/projtree.py | 154 ++++++++++++---------------- tests/test_gui/test_gui_projtree.py | 77 ++++++++------ 2 files changed, 115 insertions(+), 116 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index cb59c9ee..f7a11db4 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -444,90 +444,8 @@ class GuiProjectTree(QTreeWidget): return False wCount = self._getItemWordCount(tHandle) - if nwItemS.itemType == nwItemType.FILE: - logger.debug("User requested file '%s' deleted", tHandle) - trItemP = trItemS.parent() - trItemT = self._addTrashRoot() - if trItemP is None or trItemT is None: - logger.error("Could not delete item") - return False - - if self.theProject.projTree.isTrash(tHandle): - # If the file is in the trash folder already, as the - # user if they want to permanently delete the file. - doPermanent = False - if not alreadyAsked: - msgYes = self.theParent.askQuestion( - self.tr("Delete File"), - self.tr("Permanently delete file '{0}'?").format(nwItemS.itemName) - ) - if msgYes: - doPermanent = True - else: - doPermanent = True - - if doPermanent: - logger.debug("Permanently deleting file with handle '%s'", tHandle) - - delDoc = NWDoc(self.theProject, tHandle) - if not delDoc.deleteDocument(): - self.theParent.makeAlert([ - self.tr("Could not delete document file."), delDoc.getError() - ], nwAlert.ERROR) - return False - - self.propagateCount(tHandle, 0) - tIndex = trItemP.indexOfChild(trItemS) - trItemC = trItemP.takeChild(tIndex) - - if self.theParent.docEditor.docHandle() == tHandle: - self.theParent.closeDocument() - - self.theIndex.deleteHandle(tHandle) - self._deleteTreeItem(tHandle) - self._setTreeChanged(True) - self.wordCountsChanged.emit() - - else: - # The file is not already in the trash folder, so we - # move it there. - msgYes = self.theParent.askQuestion( - self.tr("Delete File"), - self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName), - ) - if msgYes: - logger.debug("Moving file '%s' to trash", tHandle) - - self.propagateCount(tHandle, 0) - tIndex = trItemP.indexOfChild(trItemS) - trItemC = trItemP.takeChild(tIndex) - trItemT.addChild(trItemC) - self._postItemMove(tHandle, wCount) - self._recordLastMove(trItemS, trItemP, tIndex) - self._setTreeChanged(True) - - elif nwItemS.itemType == nwItemType.FOLDER: - logger.debug("User requested folder '%s' deleted", tHandle) - trItemP = trItemS.parent() - if trItemP is None: - logger.error("Could not delete folder") - return False - - tIndex = trItemP.indexOfChild(trItemS) - if trItemS.childCount() == 0: - trItemP.takeChild(tIndex) - self._deleteTreeItem(tHandle) - self._setTreeChanged(True) - else: - self.theParent.makeAlert(self.tr( - "Cannot delete folder. It is not empty. " - "Recursive deletion is not supported. " - "Please delete the content first." - ), nwAlert.ERROR) - return False - - elif nwItemS.itemType == nwItemType.ROOT: - logger.debug("User requested root folder '%s' deleted", tHandle) + if nwItemS.itemType == nwItemType.ROOT: + logger.debug("User requested a root folder '%s' deleted", tHandle) tIndex = self.indexOfTopLevelItem(trItemS) if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) @@ -541,6 +459,60 @@ class GuiProjectTree(QTreeWidget): ), nwAlert.ERROR) return False + else: + logger.debug("User requested a file or folder '%s' deleted", tHandle) + trItemP = trItemS.parent() + trItemT = self._addTrashRoot() + if trItemP is None or trItemT is None: + logger.error("Could not delete item") + return False + + if self.theProject.projTree.isTrash(tHandle): + # If the file is in the trash folder already, as the + # user if they want to permanently delete the file. + doPermanent = False + if not alreadyAsked: + msgYes = self.theParent.askQuestion( + self.tr("Delete"), + self.tr("Permanently delete '{0}'?").format(nwItemS.itemName) + ) + if msgYes: + doPermanent = True + else: + doPermanent = True + + if doPermanent: + logger.debug("Permanently deleting item with handle '%s'", tHandle) + + self.propagateCount(tHandle, 0) + tIndex = trItemP.indexOfChild(trItemS) + trItemC = trItemP.takeChild(tIndex) + for dHandle in reversed(self.getTreeFromHandle(tHandle)): + if self.theParent.docEditor.docHandle() == dHandle: + self.theParent.closeDocument() + self._deleteTreeItem(dHandle) + + self._setTreeChanged(True) + self.wordCountsChanged.emit() + + else: + # The item is not already in the trash folder, so we + # move it there. + msgYes = self.theParent.askQuestion( + self.tr("Delete"), + self.tr("Move '{0}' to Trash?").format(nwItemS.itemName), + ) + if msgYes: + logger.debug("Moving item '%s' to trash", tHandle) + + self.propagateCount(tHandle, 0) + tIndex = trItemP.indexOfChild(trItemS) + trItemC = trItemP.takeChild(tIndex) + trItemT.addChild(trItemC) + self._postItemMove(tHandle, wCount) + self._recordLastMove(trItemS, trItemP, tIndex) + self._setTreeChanged(True) + return True def setTreeItemValues(self, tHandle): @@ -863,11 +835,21 @@ class GuiProjectTree(QTreeWidget): return self._treeMap.get(tHandle, None) def _deleteTreeItem(self, tHandle): - """Delete a tree item from the project and the map. + """Permanently delete a tree item from the project and the map. """ + if self.theProject.projTree.checkType(tHandle, nwItemType.FILE): + delDoc = NWDoc(self.theProject, tHandle) + if not delDoc.deleteDocument(): + self.theParent.makeAlert([ + self.tr("Could not delete document file."), delDoc.getError() + ], nwAlert.ERROR) + return False + + self.theIndex.deleteHandle(tHandle) del self.theProject.projTree[tHandle] self._treeMap.pop(tHandle, None) - return + + return True def _scanChildren(self, theList, tItem, tIndex): """This is a recursive function returning all items in a tree diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 84563086..59d02c2d 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -298,9 +298,6 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR "0000000000010", "0000000000011", "0000000000012", ] - # Delete File - # =========== - # Delete item without focus -> blocked monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) nwTree.setSelectedHandle("0000000000012") @@ -319,6 +316,16 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert nwTree.deleteItem("0000000000000") is False assert "Could not find tree item" in caplog.text + # Delete Folder/Root + # ================== + + # Deleting non-empty folders is blocked + assert nwTree.deleteItem("0000000000008") is False # Novel Root + assert nwTree.deleteItem("000000000000a") is True # Character Root + + # Delete File + # =========== + # Block adding trash folder funcPointer = nwTree._addTrashRoot nwTree._addTrashRoot = lambda *a: None @@ -352,12 +359,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR trashHandle, "0000000000011" ] - # Try to delete the second document, but block the deletion - with monkeypatch.context() as mp: - mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False) - assert nwTree.deleteItem("0000000000011") is False - - # Delete proper, and skip asking for permission + # Delete the second file, and skip asking for permission assert os.path.isfile(os.path.join(prjDir, "content", "0000000000011.nwd")) assert "0000000000011" in nwGUI.theProject.projTree assert nwTree.deleteItem("0000000000011", alreadyAsked=True) is True @@ -365,33 +367,36 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert "0000000000011" not in nwGUI.theProject.projTree assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] - # Delete Folder/Root - # ================== + # Delete Folder + # ============= - # Deleting non-empty folders is blocked - assert nwTree.deleteItem("000000000000d") is False # Folder - assert nwTree.deleteItem("0000000000008") is False # Root + trashHandle = nwGUI.theProject.projTree.trashRoot() - # Add a folder we can delete - nwTree.setSelectedHandle("000000000000a") # Character Root + # Add a folder with two files + nwTree.setSelectedHandle("0000000000009") assert nwTree.newTreeItem(nwItemType.FOLDER) is True - assert "0000000000014" in nwGUI.theProject.projTree + nwTree.setSelectedHandle("0000000000014") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd")) + assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd")) - # Try to delete, but block parent item lookup - with monkeypatch.context() as mp: - mp.setattr("PyQt5.QtWidgets.QTreeWidgetItem.parent", lambda *a: None) - caplog.clear() - assert nwTree.deleteItem("0000000000014") is False - assert "Could not delete folder" in caplog.text - assert "0000000000014" in nwGUI.theProject.projTree - - # Delete folder properly + # Delete the folder, which moves everything to Trash + assert nwTree.getTreeFromHandle("0000000000014") == [ + "0000000000014", "0000000000015", "0000000000016" + ] assert nwTree.deleteItem("0000000000014") is True - assert "0000000000014" not in nwGUI.theProject.projTree + assert nwTree.getTreeFromHandle(trashHandle) == [ + trashHandle, "0000000000014", "0000000000015", "0000000000016" + ] + assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd")) + assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd")) - # Delete the Character root - assert nwTree.deleteItem("000000000000a") is True - assert "000000000000a" not in nwGUI.theProject.projTree + # Delete again, which should delete folder and all files + assert nwTree.deleteItem("0000000000014") is True + assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] + assert not os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd")) + assert not os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd")) # Empty Trash # =========== @@ -421,6 +426,18 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] assert nwTree._treeChanged is True + # Try to delete a file, but block the underlying deletion of the file on disk + assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) + with monkeypatch.context() as mp: + mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False) + assert nwTree.deleteItem("000000000000e") is True + assert nwTree.deleteItem("000000000000e") is True + assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) + + # Delete proper + assert nwTree._deleteTreeItem("000000000000e") is True + assert not os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) + # Clean up # qtbot.stopForInteraction() nwGUI.closeProject() From bb53b2530cb9f1e68bf4aaba79b8cf65b56d1aaf Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Apr 2022 21:41:14 +0200 Subject: [PATCH 055/179] Fix error messages when emptying trash --- novelwriter/gui/projtree.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index f7a11db4..4b4d5e2d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -404,7 +404,7 @@ class GuiProjectTree(QTreeWidget): return False logger.verbose("Deleting %d file(s) from Trash", nTrash) - for tHandle in self.getTreeFromHandle(trashHandle): + for tHandle in reversed(self.getTreeFromHandle(trashHandle)): if tHandle == trashHandle: continue self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True) @@ -418,8 +418,8 @@ class GuiProjectTree(QTreeWidget): """Delete an item from the project tree. As a first step, files are moved to the Trash folder. Permanent deletion is a second step. This second step also deletes the item from the project object as well as - delete the files on disk. Folders are deleted if they're empty only, - and the deletion is always permanent. + delete the files on disk. Root folders are deleted if they're empty + only, and the deletion is always permanent. """ if not self.theParent.hasProject: logger.error("No project open") From 1e7d329962804b75a81cd63d91a89178d6fe9a39 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 Apr 2022 19:04:21 +0200 Subject: [PATCH 056/179] Force conistency of ROOT item settings --- novelwriter/core/item.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index e23b4fb9..54913b4a 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -239,6 +239,11 @@ class NWItem(): # version of novelWriter that doesn't know the tag logger.error("Unknown tag '%s'", xValue.tag) + # Make some checks to ensure consistency + if self._type == nwItemType.ROOT: + self._root = self._handle # Root items are their own ancestor + self._parent = None # Root items cannot have a parent + return True @staticmethod From b68280655f5020fca504f1586444bf05a045d57a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 Apr 2022 19:17:16 +0200 Subject: [PATCH 057/179] Add Mastodon link to main readme and docs --- README.md | 2 ++ docs/source/index.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 60ca008b..7b0a9965 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ The full documentation is available at The full credits are listed in [CREDITS.md](https://github.com/vkbo/novelWriter/blob/main/CREDITS.md). +You can also follow novelWriter on Mastodon at [fosstodon.org/@novelwriter](https://fosstodon.org/@novelwriter). + ## Implementation The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.3+). It is developed on diff --git a/docs/source/index.rst b/docs/source/index.rst index 0191425b..20c39895 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,11 +39,13 @@ too. novelWriter can be run directly from the Python source, installed from the * Website: https://novelwriter.io * Documentation: https://novelwriter.readthedocs.io +* Internationalisation: https://crowdin.com/project/novelwriter * Source Code: https://github.com/vkbo/novelWriter * Source Releases: https://github.com/vkbo/novelWriter/releases * Issue Tracker: https://github.com/vkbo/novelWriter/issues * Feature Discussions: https://github.com/vkbo/novelWriter/discussions * PyPi Project: https://pypi.org/project/novelWriter +* Social Media: https://fosstodon.org/@novelwriter .. toctree:: :maxdepth: 1 From 9ee4d1b2cdcd6398dfe3860f7253f86b01072157 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 8 May 2022 17:52:36 +0200 Subject: [PATCH 058/179] Reset meta data for folders --- novelwriter/core/item.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 54913b4a..88650475 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -171,7 +171,7 @@ class NWItem(): nameAttrib["status"] = str(self._status) nameAttrib["import"] = str(self._import) if self._type == nwItemType.FILE: - nameAttrib["exported"] = str(self._exported) + nameAttrib["exported"] = str(self._exported) xPack = etree.SubElement(xParent, "item", attrib=itemAttrib) self._subPack(xPack, "meta", attrib=metaAttrib) @@ -244,6 +244,12 @@ class NWItem(): self._root = self._handle # Root items are their own ancestor self._parent = None # Root items cannot have a parent + if self._type != nwItemType.FILE: + self._charCount = 0 # Only set for files + self._wordCount = 0 # Only set for files + self._paraCount = 0 # Only set for files + self._cursorPos = 0 # Only set for files + return True @staticmethod From 0a5c51ad4b7d8648c2f8d63b35e5389aa786f71e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 8 May 2022 18:07:21 +0200 Subject: [PATCH 059/179] Make deleting empty folders bypass Trash --- novelwriter/gui/projtree.py | 11 +++++++++++ tests/test_gui/test_gui_projtree.py | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 4b4d5e2d..75429563 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -445,6 +445,7 @@ class GuiProjectTree(QTreeWidget): wCount = self._getItemWordCount(tHandle) if nwItemS.itemType == nwItemType.ROOT: + # Only an empty ROOT folder can be deleted logger.debug("User requested a root folder '%s' deleted", tHandle) tIndex = self.indexOfTopLevelItem(trItemS) if trItemS.childCount() == 0: @@ -459,7 +460,17 @@ class GuiProjectTree(QTreeWidget): ), nwAlert.ERROR) return False + elif nwItemS.itemType == nwItemType.FOLDER and trItemS.childCount() == 0: + # An empty FOLDER is just deleted without any further checks + logger.debug("User requested an empty folder '%s' deleted", tHandle) + trItemP = trItemS.parent() + tIndex = trItemP.indexOfChild(trItemS) + trItemP.takeChild(tIndex) + self._deleteTreeItem(tHandle) + self._setTreeChanged(True) + else: + # A populated FOLDER or a FILE requires confirmtation logger.debug("User requested a file or folder '%s' deleted", tHandle) trItemP = trItemS.parent() trItemT = self._addTrashRoot() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 59d02c2d..af851138 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -398,6 +398,15 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert not os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd")) assert not os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd")) + # Add an empty folder, which can be deleted with no further restrictions + nwTree.setSelectedHandle("0000000000009") + assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.getTreeFromHandle("0000000000009") == ["0000000000009", "0000000000017"] + + nwTree.setSelectedHandle("0000000000017") + assert nwTree.deleteItem("0000000000017") is True + assert nwTree.getTreeFromHandle("0000000000009") == ["0000000000009"] + # Empty Trash # =========== From 9e9beea83c973cfeb71ef3338e8f2c903954d4ba Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 8 May 2022 18:21:17 +0200 Subject: [PATCH 060/179] Fix outdated docstring in project tree class --- novelwriter/gui/projtree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 75429563..0339ec5f 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -760,8 +760,8 @@ class GuiProjectTree(QTreeWidget): return def dropEvent(self, theEvent): - """Overload the drop of dragged item event to check whether the - drop is allowed or not. Disallowed drops are cancelled. + """Overload the drop item event to ensure relevant data has been + updated. """ sHandle = self.getSelectedHandle() if sHandle is None: From ea09a534962c9cd75dbf64584cc970e83b0730a7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 10 May 2022 20:56:52 +0200 Subject: [PATCH 061/179] Add the GuiViewsBar class --- novelwriter/gui/__init__.py | 2 + novelwriter/gui/viewsbar.py | 81 +++++++++++++++++++++++++++++++++++++ novelwriter/guimain.py | 5 ++- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 novelwriter/gui/viewsbar.py diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index 405549cb..8364e342 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -29,6 +29,7 @@ from novelwriter.gui.outlinedetails import GuiOutlineDetails from novelwriter.gui.projtree import GuiProjectTree from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.theme import GuiTheme +from novelwriter.gui.viewsbar import GuiViewsBar __all__ = [ "GuiDocEditor", @@ -42,4 +43,5 @@ __all__ = [ "GuiOutlineDetails", "GuiProjectTree", "GuiTheme", + "GuiViewsBar", ] diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py new file mode 100644 index 00000000..e8eb97fc --- /dev/null +++ b/novelwriter/gui/viewsbar.py @@ -0,0 +1,81 @@ +""" +novelWriter – GUI Main Window Views ToolBar +=========================================== +GUI class for the main window "Views" toolbar + +File History: +Created: 2022-05-10 [1.7b1] + +This file is a part of novelWriter +Copyright 2018–2022, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import logging +import novelwriter + +from PyQt5.QtCore import Qt, QSize +from PyQt5.QtWidgets import QToolBar, QWidget, QSizePolicy, QAction + +logger = logging.getLogger(__name__) + + +class GuiViewsBar(QToolBar): + + def __init__(self, theParent): + QToolBar.__init__(self, theParent) + + logger.debug("Initialising GuiViewsBar ...") + + self.mainConf = novelwriter.CONFIG + self.theParent = theParent + self.theTheme = theParent.theTheme + + # Style + iPx = self.mainConf.pxInt(22) + + lblFont = self.theTheme.guiFont + lblFont.setPointSizeF(0.65*self.theTheme.fontPointSize) + self.setFont(lblFont) + + self.setMovable(False) + self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + self.setIconSize(QSize(iPx, iPx)) + self.setContentsMargins(0, 0, 0, 0) + + stretch = QWidget(self) + stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Actions + self.aProject = QAction(self.tr("Project")) + self.aProject.setIcon(self.theTheme.getIcon("status_lines")) + + self.aStats = QAction(self.tr("Stats")) + self.aStats.setIcon(self.theTheme.getIcon("status_stats")) + + self.aSettings = QAction(self.tr("Settings")) + self.aSettings.setIcon(self.theTheme.getIcon("settings")) + + # Assemble + self.addWidget(stretch) + self.addAction(self.aProject) + self.addAction(self.aStats) + self.addAction(self.aSettings) + + logger.debug("GuiViewsBar initialisation complete") + + return + +# END Class GuiViewsBar diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 60fe0d23..84607fc0 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails, GuiProjectTree, - GuiTheme + GuiTheme, GuiViewsBar ) from novelwriter.dialogs import ( GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences, @@ -116,6 +116,7 @@ class GuiMain(QMainWindow): self.projView = GuiOutline(self) self.projMeta = GuiOutlineDetails(self) self.mainMenu = GuiMainMenu(self) + self.viewsBar = GuiViewsBar(self) # Connect Signals Between Main Elements self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) @@ -238,6 +239,7 @@ class GuiMain(QMainWindow): self.setMenuBar(self.mainMenu) self.setCentralWidget(self.splitMain) self.setStatusBar(self.statusBar) + self.addToolBar(Qt.LeftToolBarArea, self.viewsBar) # Finalise Initialisation # ======================= @@ -1299,6 +1301,7 @@ class GuiMain(QMainWindow): self.treePane.setVisible(isVisible) self.statusBar.setVisible(isVisible) self.mainMenu.setVisible(isVisible) + self.viewsBar.setVisible(isVisible) self.mainTabs.tabBar().setVisible(isVisible) hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter From 864b06a0e716dfd5d15c3dad4ceceed1f1218950 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 10 May 2022 22:14:35 +0200 Subject: [PATCH 062/179] Shift all view changing functionality to the new Views bar --- novelwriter/enum.py | 13 +++ novelwriter/gui/viewsbar.py | 28 +++++- novelwriter/guimain.py | 127 +++++++++++++-------------- tests/test_gui/test_gui_guimain.py | 4 +- tests/test_gui/test_gui_noveltree.py | 2 +- tests/test_gui/test_gui_outline.py | 2 +- 6 files changed, 102 insertions(+), 74 deletions(-) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 56340541..2d0cc56f 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -130,6 +130,19 @@ class nwState(Enum): # END Enum nwState +class nwView(Enum): + + PROJECT = 0 + NOVEL = 1 + OUTLINE = 2 + DETAILS = 3 + STATS = 4 + SET_PROJ = 5 + SET_MAIN = 6 + +# END Enum nwView + + class nwWidget(Enum): TREE = 1 diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py index e8eb97fc..71974357 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/viewsbar.py @@ -26,14 +26,18 @@ along with this program. If not, see . import logging import novelwriter -from PyQt5.QtCore import Qt, QSize +from PyQt5.QtCore import Qt, QSize, pyqtSignal from PyQt5.QtWidgets import QToolBar, QWidget, QSizePolicy, QAction +from novelwriter.enum import nwView + logger = logging.getLogger(__name__) class GuiViewsBar(QToolBar): + viewChangeRequested = pyqtSignal(nwView) + def __init__(self, theParent): QToolBar.__init__(self, theParent) @@ -60,17 +64,35 @@ class GuiViewsBar(QToolBar): # Actions self.aProject = QAction(self.tr("Project")) - self.aProject.setIcon(self.theTheme.getIcon("status_lines")) + self.aProject.setIcon(self.theTheme.getIcon("proj_folder")) + self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) + + self.aNovel = QAction(self.tr("Novel")) + self.aNovel.setIcon(self.theTheme.getIcon("cls_novel")) + self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) + + self.aOutline = QAction(self.tr("Outline")) + self.aOutline.setIcon(self.theTheme.getIcon("cls_plot")) + self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) + + self.aDetails = QAction(self.tr("Details")) + self.aDetails.setIcon(self.theTheme.getIcon("status_lines")) + self.aDetails.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.DETAILS)) self.aStats = QAction(self.tr("Stats")) self.aStats.setIcon(self.theTheme.getIcon("status_stats")) + self.aStats.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.STATS)) self.aSettings = QAction(self.tr("Settings")) self.aSettings.setIcon(self.theTheme.getIcon("settings")) + self.aSettings.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.SET_PROJ)) # Assemble - self.addWidget(stretch) self.addAction(self.aProject) + self.addAction(self.aNovel) + self.addAction(self.aOutline) + self.addWidget(stretch) + self.addAction(self.aDetails) self.addAction(self.aStats) self.addAction(self.aSettings) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 84607fc0..077a22d3 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -30,11 +30,11 @@ import novelwriter from time import time from datetime import datetime -from PyQt5.QtCore import Qt, QTimer, QSize, QThreadPool, pyqtSlot +from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot from PyQt5.QtGui import QIcon, QKeySequence, QCursor from PyQt5.QtWidgets import ( qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut, - QMessageBox, QDialog, QTabWidget, QToolBar, QAction + QMessageBox, QDialog, QStackedWidget ) from novelwriter.gui import ( @@ -52,7 +52,7 @@ from novelwriter.tools import ( ) from novelwriter.core import NWProject, NWIndex from novelwriter.enum import ( - nwItemType, nwItemClass, nwAlert, nwWidget, nwState + nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView ) from novelwriter.common import getGuiItem, hexToInt @@ -102,8 +102,6 @@ class GuiMain(QMainWindow): # Sizes mPx = self.mainConf.pxInt(4) - fPx = self.theTheme.fontPixelSize - fPt = self.theTheme.fontPointSize # Main GUI Elements self.statusBar = GuiMainStatus(self) @@ -129,48 +127,20 @@ class GuiMain(QMainWindow): self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - # Project Tree Tabs - self.projTabs = QTabWidget() - self.projTabs.setTabPosition(QTabWidget.South) - self.projTabs.setStyleSheet("QTabWidget::pane {border: 0;};") - self.projTabs.addTab(self.treeView, self.tr("Project")) - self.projTabs.addTab(self.novelView, self.tr("Novel")) - self.projTabs.currentChanged.connect(self._projTabsChanged) + self.viewsBar.viewChangeRequested.connect(self._changeView) - tabFont = self.projTabs.tabBar().font() - tabFont.setPointSizeF(0.9*fPt) - self.projTabs.tabBar().setFont(tabFont) - - # Project Tree Action Buttons - btnSize = int(round(0.7*fPx)) - self.treeButtons = QToolBar() - self.treeButtons.setToolButtonStyle(Qt.ToolButtonIconOnly) - self.treeButtons.setIconSize(QSize(btnSize, btnSize)) - self.treeButtons.setContentsMargins(0, 0, 0, 0) - self.treeButtons.setStyleSheet("QToolBar {padding: 0;}") - self.projTabs.setCornerWidget(self.treeButtons, Qt.BottomRightCorner) - - self.projDetailsBtn = QAction(self.tr("Project Details")) - self.projDetailsBtn.setIcon(self.theTheme.getIcon("status_lines")) - self.projDetailsBtn.triggered.connect(lambda: self.showProjectDetailsDialog()) - self.treeButtons.addAction(self.projDetailsBtn) - - self.projStatsBtn = QAction(self.tr("Writing Statistics")) - self.projStatsBtn.setIcon(self.theTheme.getIcon("status_stats")) - self.projStatsBtn.triggered.connect(lambda: self.showWritingStatsDialog()) - self.treeButtons.addAction(self.projStatsBtn) - - self.projSettingsBtn = QAction(self.tr("Project Settings")) - self.projSettingsBtn.setIcon(self.theTheme.getIcon("settings")) - self.projSettingsBtn.triggered.connect(lambda: self.showProjectSettingsDialog()) - self.treeButtons.addAction(self.projSettingsBtn) + # Project Tree Stack + self.projStack = QStackedWidget() + self.projStack.addWidget(self.treeView) + self.projStack.addWidget(self.novelView) + self.projStack.currentChanged.connect(self._projStackChanged) # Project Tree View self.treePane = QWidget() self.treeBox = QVBoxLayout() self.treeBox.setContentsMargins(0, 0, 0, 0) self.treeBox.setSpacing(mPx) - self.treeBox.addWidget(self.projTabs) + self.treeBox.addWidget(self.projStack) self.treeBox.addWidget(self.treeMeta) self.treePane.setLayout(self.treeBox) @@ -192,33 +162,31 @@ class GuiMain(QMainWindow): self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) # Main Tabs : Editor / Outline - self.mainTabs = QTabWidget() - self.mainTabs.setTabPosition(QTabWidget.East) - self.mainTabs.setStyleSheet("QTabWidget::pane {border: 0;}") - self.mainTabs.addTab(self.splitDocs, self.tr("Editor")) - self.mainTabs.addTab(self.splitOutline, self.tr("Outline")) - self.mainTabs.currentChanged.connect(self._mainTabChanged) + self.mainStack = QStackedWidget() + self.mainStack.addWidget(self.splitDocs) + self.mainStack.addWidget(self.splitOutline) + self.mainStack.currentChanged.connect(self._mainStackChanged) # Splitter : Project Tree / Main Tabs self.splitMain = QSplitter(Qt.Horizontal) self.splitMain.setContentsMargins(mPx, mPx, mPx, mPx) self.splitMain.addWidget(self.treePane) - self.splitMain.addWidget(self.mainTabs) + self.splitMain.addWidget(self.mainStack) self.splitMain.setSizes(self.mainConf.getMainPanePos()) # Indices of Splitter Widgets self.idxTree = self.splitMain.indexOf(self.treePane) - self.idxMain = self.splitMain.indexOf(self.mainTabs) + self.idxMain = self.splitMain.indexOf(self.mainStack) self.idxEditor = self.splitDocs.indexOf(self.docEditor) self.idxViewer = self.splitDocs.indexOf(self.splitView) self.idxViewDoc = self.splitView.indexOf(self.docViewer) self.idxViewMeta = self.splitView.indexOf(self.viewMeta) # Indices of Tab Widgets - self.idxTabEdit = self.mainTabs.indexOf(self.splitDocs) - self.idxTabProj = self.mainTabs.indexOf(self.splitOutline) - self.idxTreeView = self.projTabs.indexOf(self.treeView) - self.idxNovelView = self.projTabs.indexOf(self.novelView) + self.idxTabEdit = self.mainStack.indexOf(self.splitDocs) + self.idxTabProj = self.mainStack.indexOf(self.splitOutline) + self.idxTreeView = self.projStack.indexOf(self.treeView) + self.idxNovelView = self.projStack.indexOf(self.novelView) # Splitter Behaviour self.splitMain.setCollapsible(self.idxTree, False) @@ -450,7 +418,7 @@ class GuiMain(QMainWindow): self.theIndex.clearIndex() self.clearGUI() self.hasProject = False - self.mainTabs.setCurrentWidget(self.splitDocs) + self.mainStack.setCurrentWidget(self.splitDocs) return saveOK @@ -466,7 +434,7 @@ class GuiMain(QMainWindow): return False # Switch main tab to editor view - self.mainTabs.setCurrentWidget(self.splitDocs) + self.mainStack.setCurrentWidget(self.splitDocs) # Try to open the project if not self.theProject.openProject(projFile): @@ -605,7 +573,7 @@ class GuiMain(QMainWindow): return False self.closeDocument() - self.mainTabs.setCurrentWidget(self.splitDocs) + self.mainStack.setCurrentWidget(self.splitDocs) if self.docEditor.loadText(tHandle, tLine): if changeFocus: self.docEditor.setFocus() @@ -688,7 +656,7 @@ class GuiMain(QMainWindow): return False # Make sure main tab is in Editor view - self.mainTabs.setCurrentWidget(self.splitDocs) + self.mainStack.setCurrentWidget(self.splitDocs) logger.debug("Viewing document with handle '%s'", tHandle) if self.docViewer.loadText(tHandle): @@ -873,7 +841,7 @@ class GuiMain(QMainWindow): def requestNovelTreeRefresh(self): """Update the novel tree, but only if it is visible. """ - if self.projTabs.currentIndex() == self.idxNovelView and self.hasProject: + if self.projStack.currentIndex() == self.idxNovelView and self.hasProject: self.novelView.refreshTree() return True return False @@ -934,7 +902,7 @@ class GuiMain(QMainWindow): return False logger.verbose("Forcing a rebuild of the Project Outline") - self.mainTabs.setCurrentWidget(self.splitOutline) + self.mainStack.setCurrentWidget(self.splitOutline) self.projView.refreshTree(overRide=True) return True @@ -1247,19 +1215,19 @@ class GuiMain(QMainWindow): """Switch focus between main GUI views. """ if paneNo == nwWidget.TREE: - tabIdx = self.projTabs.currentIndex() + tabIdx = self.projStack.currentIndex() if tabIdx == self.idxTreeView: self.treeView.setFocus() elif tabIdx == self.idxNovelView: self.novelView.setFocus() elif paneNo == nwWidget.EDITOR: - self.mainTabs.setCurrentWidget(self.splitDocs) + self.mainStack.setCurrentWidget(self.splitDocs) self.docEditor.setFocus() elif paneNo == nwWidget.VIEWER: - self.mainTabs.setCurrentWidget(self.splitDocs) + self.mainStack.setCurrentWidget(self.splitDocs) self.docViewer.setFocus() elif paneNo == nwWidget.OUTLINE: - self.mainTabs.setCurrentWidget(self.splitOutline) + self.mainStack.setCurrentWidget(self.splitOutline) self.projView.setFocus() return @@ -1292,7 +1260,7 @@ class GuiMain(QMainWindow): self.mainMenu.setFocusMode(self.isFocusMode) if self.isFocusMode: logger.debug("Activating Focus Mode") - self.mainTabs.setCurrentWidget(self.splitDocs) + self.mainStack.setCurrentWidget(self.splitDocs) self.switchFocus(nwWidget.EDITOR) else: logger.debug("Deactivating Focus Mode") @@ -1302,7 +1270,6 @@ class GuiMain(QMainWindow): self.statusBar.setVisible(isVisible) self.mainMenu.setVisible(isVisible) self.viewsBar.setVisible(isVisible) - self.mainTabs.tabBar().setVisible(isVisible) hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter self.docEditor.docFooter.setVisible(not hideDocFooter) @@ -1516,6 +1483,32 @@ class GuiMain(QMainWindow): # Slots ## + @pyqtSlot(nwView) + def _changeView(self, view): + """Handle the requested change of view from the GuiViewBar. + """ + if view == nwView.PROJECT: + self.mainStack.setCurrentWidget(self.splitDocs) + self.projStack.setCurrentWidget(self.treeView) + + elif view == nwView.NOVEL: + self.mainStack.setCurrentWidget(self.splitDocs) + self.projStack.setCurrentWidget(self.novelView) + + elif view == nwView.OUTLINE: + self.mainStack.setCurrentWidget(self.splitOutline) + + elif view == nwView.DETAILS: + self.showProjectDetailsDialog() + + elif view == nwView.STATS: + self.showWritingStatsDialog() + + elif view == nwView.SET_PROJ: + self.showProjectSettingsDialog() + + return + @pyqtSlot() def _timeTick(self): """Triggered on every tick of the main timer. @@ -1589,7 +1582,7 @@ class GuiMain(QMainWindow): """Triggered when there is a change to a novel item in the project tree. """ - if self.mainTabs.currentIndex() == self.idxTabProj: + if self.mainStack.currentIndex() == self.idxTabProj: logger.verbose("Novel tree changed while Outline tab active") if self.hasProject: self.treeView.flushTreeOrder() @@ -1618,7 +1611,7 @@ class GuiMain(QMainWindow): return @pyqtSlot(int) - def _mainTabChanged(self, tabIndex): + def _mainStackChanged(self, tabIndex): """Activated when the main window tab is changed. """ if tabIndex == self.idxTabEdit: @@ -1631,7 +1624,7 @@ class GuiMain(QMainWindow): return @pyqtSlot(int) - def _projTabsChanged(self, tabIndex): + def _projStackChanged(self, tabIndex): """Activated when the project view tab is changed. """ sHandle = None diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 65648862..ddb4f09b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -85,7 +85,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Project Tree has focus nwGUI.switchFocus(nwWidget.TREE) - nwGUI.projTabs.setCurrentIndex(0) + nwGUI.projStack.setCurrentIndex(0) with monkeypatch.context() as mp: mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None @@ -95,7 +95,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.closeDocument() is True # Novel Tree has focus - nwGUI.projTabs.setCurrentIndex(1) + nwGUI.projStack.setCurrentIndex(1) nwGUI.novelView.refreshTree(True) with monkeypatch.context() as mp: mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True) diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index f2418a87..2bf2df8a 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -59,7 +59,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): # Populate Tree ## - nwGUI.projTabs.setCurrentIndex(nwGUI.idxNovelView) + nwGUI.projStack.setCurrentIndex(nwGUI.idxNovelView) nwGUI.rebuildIndex() nwTree._populateTree() assert nwTree.topLevelItemCount() == 1 diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 972e7456..901a2bd2 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -43,7 +43,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.mainConf.lastPath = nwLipsum nwGUI.rebuildIndex() - nwGUI.mainTabs.setCurrentIndex(nwGUI.idxTabProj) + nwGUI.mainStack.setCurrentIndex(nwGUI.idxTabProj) assert nwGUI.projView.topLevelItemCount() > 0 From e8a0a6d5f97e031ebe094b61a490caf86b39109e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 14 May 2022 18:27:18 +0200 Subject: [PATCH 063/179] Connect the change of view methods to all relevant actions --- novelwriter/enum.py | 15 ++++---- novelwriter/gui/itemdetails.py | 1 + novelwriter/gui/outlinedetails.py | 6 ++- novelwriter/guimain.py | 64 ++++++++++++++++--------------- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 2d0cc56f..20332b2f 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -132,13 +132,14 @@ class nwState(Enum): class nwView(Enum): - PROJECT = 0 - NOVEL = 1 - OUTLINE = 2 - DETAILS = 3 - STATS = 4 - SET_PROJ = 5 - SET_MAIN = 6 + EDITOR = 0 + PROJECT = 1 + NOVEL = 2 + OUTLINE = 3 + DETAILS = 4 + STATS = 5 + SET_PROJ = 6 + SET_MAIN = 7 # END Enum nwView diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 73082fa5..8b42b752 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -115,6 +115,7 @@ class GuiItemDetails(QWidget): self.usageData = QLabel("") self.usageData.setFont(fntValue) self.usageData.setAlignment(Qt.AlignLeft) + self.usageData.setWordWrap(True) # Character Count self.cCountName = QLabel(" "+self.tr("Characters")) diff --git a/novelwriter/gui/outlinedetails.py b/novelwriter/gui/outlinedetails.py index b45c298f..358a6531 100644 --- a/novelwriter/gui/outlinedetails.py +++ b/novelwriter/gui/outlinedetails.py @@ -26,11 +26,12 @@ along with this program. If not, see . import logging import novelwriter -from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP +from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP, pyqtSignal from PyQt5.QtWidgets import ( QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel ) +from novelwriter.enum import nwView from novelwriter.common import checkInt from novelwriter.constants import trConst, nwKeyWords, nwLabels @@ -46,6 +47,8 @@ class GuiOutlineDetails(QScrollArea): "H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"), } + viewChangeRequested = pyqtSignal(nwView) + def __init__(self, theParent): QScrollArea.__init__(self, theParent) @@ -329,6 +332,7 @@ class GuiOutlineDetails(QScrollArea): if len(theLink) > 0: theBits = theLink.split("=") if len(theBits) == 2: + self.viewChangeRequested.emit(nwView.PROJECT) self.theParent.docViewer.loadFromTag(theBits[1]) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 077a22d3..ad06cc59 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -128,6 +128,7 @@ class GuiMain(QMainWindow): self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) self.viewsBar.viewChangeRequested.connect(self._changeView) + self.projMeta.viewChangeRequested.connect(self._changeView) # Project Tree Stack self.projStack = QStackedWidget() @@ -161,32 +162,32 @@ class GuiMain(QMainWindow): self.splitOutline.addWidget(self.projMeta) self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) - # Main Tabs : Editor / Outline + # Splitter : Project Tree / Main Tabs + self.splitMain = QSplitter(Qt.Horizontal) + self.splitMain.setContentsMargins(0, 0, mPx, 0) + self.splitMain.addWidget(self.treePane) + self.splitMain.addWidget(self.splitDocs) + self.splitMain.setSizes(self.mainConf.getMainPanePos()) + + # Main Stack : Editor / Outline self.mainStack = QStackedWidget() - self.mainStack.addWidget(self.splitDocs) + self.mainStack.addWidget(self.splitMain) self.mainStack.addWidget(self.splitOutline) self.mainStack.currentChanged.connect(self._mainStackChanged) - # Splitter : Project Tree / Main Tabs - self.splitMain = QSplitter(Qt.Horizontal) - self.splitMain.setContentsMargins(mPx, mPx, mPx, mPx) - self.splitMain.addWidget(self.treePane) - self.splitMain.addWidget(self.mainStack) - self.splitMain.setSizes(self.mainConf.getMainPanePos()) - # Indices of Splitter Widgets self.idxTree = self.splitMain.indexOf(self.treePane) - self.idxMain = self.splitMain.indexOf(self.mainStack) + self.idxMain = self.splitMain.indexOf(self.splitDocs) self.idxEditor = self.splitDocs.indexOf(self.docEditor) self.idxViewer = self.splitDocs.indexOf(self.splitView) self.idxViewDoc = self.splitView.indexOf(self.docViewer) self.idxViewMeta = self.splitView.indexOf(self.viewMeta) # Indices of Tab Widgets - self.idxTabEdit = self.mainStack.indexOf(self.splitDocs) - self.idxTabProj = self.mainStack.indexOf(self.splitOutline) - self.idxTreeView = self.projStack.indexOf(self.treeView) - self.idxNovelView = self.projStack.indexOf(self.novelView) + self.idxEditorView = self.mainStack.indexOf(self.splitMain) + self.idxOutlineView = self.mainStack.indexOf(self.splitOutline) + self.idxTreeView = self.projStack.indexOf(self.treeView) + self.idxNovelView = self.projStack.indexOf(self.novelView) # Splitter Behaviour self.splitMain.setCollapsible(self.idxTree, False) @@ -205,7 +206,7 @@ class GuiMain(QMainWindow): # Set Main Window Elements self.setMenuBar(self.mainMenu) - self.setCentralWidget(self.splitMain) + self.setCentralWidget(self.mainStack) self.setStatusBar(self.statusBar) self.addToolBar(Qt.LeftToolBarArea, self.viewsBar) @@ -418,7 +419,7 @@ class GuiMain(QMainWindow): self.theIndex.clearIndex() self.clearGUI() self.hasProject = False - self.mainStack.setCurrentWidget(self.splitDocs) + self._changeView(nwView.PROJECT) return saveOK @@ -434,7 +435,7 @@ class GuiMain(QMainWindow): return False # Switch main tab to editor view - self.mainStack.setCurrentWidget(self.splitDocs) + self._changeView(nwView.PROJECT) # Try to open the project if not self.theProject.openProject(projFile): @@ -573,7 +574,7 @@ class GuiMain(QMainWindow): return False self.closeDocument() - self.mainStack.setCurrentWidget(self.splitDocs) + self._changeView(nwView.EDITOR) if self.docEditor.loadText(tHandle, tLine): if changeFocus: self.docEditor.setFocus() @@ -656,7 +657,7 @@ class GuiMain(QMainWindow): return False # Make sure main tab is in Editor view - self.mainStack.setCurrentWidget(self.splitDocs) + self._changeView(nwView.EDITOR) logger.debug("Viewing document with handle '%s'", tHandle) if self.docViewer.loadText(tHandle): @@ -902,7 +903,7 @@ class GuiMain(QMainWindow): return False logger.verbose("Forcing a rebuild of the Project Outline") - self.mainStack.setCurrentWidget(self.splitOutline) + self._changeView(nwView.OUTLINE) self.projView.refreshTree(overRide=True) return True @@ -1221,13 +1222,13 @@ class GuiMain(QMainWindow): elif tabIdx == self.idxNovelView: self.novelView.setFocus() elif paneNo == nwWidget.EDITOR: - self.mainStack.setCurrentWidget(self.splitDocs) + self._changeView(nwView.EDITOR) self.docEditor.setFocus() elif paneNo == nwWidget.VIEWER: - self.mainStack.setCurrentWidget(self.splitDocs) + self._changeView(nwView.EDITOR) self.docViewer.setFocus() elif paneNo == nwWidget.OUTLINE: - self.mainStack.setCurrentWidget(self.splitOutline) + self._changeView(nwView.OUTLINE) self.projView.setFocus() return @@ -1260,7 +1261,6 @@ class GuiMain(QMainWindow): self.mainMenu.setFocusMode(self.isFocusMode) if self.isFocusMode: logger.debug("Activating Focus Mode") - self.mainStack.setCurrentWidget(self.splitDocs) self.switchFocus(nwWidget.EDITOR) else: logger.debug("Deactivating Focus Mode") @@ -1487,12 +1487,16 @@ class GuiMain(QMainWindow): def _changeView(self, view): """Handle the requested change of view from the GuiViewBar. """ - if view == nwView.PROJECT: - self.mainStack.setCurrentWidget(self.splitDocs) + if view == nwView.EDITOR: + # Only change the main stack, but not the project stack + self.mainStack.setCurrentWidget(self.splitMain) + + elif view == nwView.PROJECT: + self.mainStack.setCurrentWidget(self.splitMain) self.projStack.setCurrentWidget(self.treeView) elif view == nwView.NOVEL: - self.mainStack.setCurrentWidget(self.splitDocs) + self.mainStack.setCurrentWidget(self.splitMain) self.projStack.setCurrentWidget(self.novelView) elif view == nwView.OUTLINE: @@ -1582,7 +1586,7 @@ class GuiMain(QMainWindow): """Triggered when there is a change to a novel item in the project tree. """ - if self.mainStack.currentIndex() == self.idxTabProj: + if self.mainStack.currentIndex() == self.idxOutlineView: logger.verbose("Novel tree changed while Outline tab active") if self.hasProject: self.treeView.flushTreeOrder() @@ -1614,9 +1618,9 @@ class GuiMain(QMainWindow): def _mainStackChanged(self, tabIndex): """Activated when the main window tab is changed. """ - if tabIndex == self.idxTabEdit: + if tabIndex == self.idxEditorView: logger.verbose("Editor tab activated") - elif tabIndex == self.idxTabProj: + elif tabIndex == self.idxOutlineView: logger.verbose("Project outline tab activated") if self.hasProject: self.projView.refreshTree() From 88cc4bd5a4f26afe57a98558a9617ccf46e5d44a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 14 May 2022 19:00:47 +0200 Subject: [PATCH 064/179] Add new icons for the views bar --- .../assets/icons/typicons_dark/icons.conf | 5 +++ .../icons/typicons_dark/typ_book-grey.svg | 31 +++++++++++++++++++ .../assets/icons/typicons_dark/typ_edit.svg | 31 +++++++++++++++++++ .../typicons_dark/typ_puzzle-outline.svg | 31 +++++++++++++++++++ .../assets/icons/typicons_light/icons.conf | 5 +++ .../icons/typicons_light/typ_book-grey.svg | 31 +++++++++++++++++++ .../assets/icons/typicons_light/typ_edit.svg | 31 +++++++++++++++++++ .../typicons_light/typ_puzzle-outline.svg | 31 +++++++++++++++++++ novelwriter/gui/theme.py | 22 ++++++------- novelwriter/gui/viewsbar.py | 10 +++--- 10 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/typ_book-grey.svg create mode 100644 novelwriter/assets/icons/typicons_dark/typ_edit.svg create mode 100644 novelwriter/assets/icons/typicons_dark/typ_puzzle-outline.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_book-grey.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_edit.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_puzzle-outline.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index bcaef71a..ccc55222 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -49,10 +49,12 @@ hash = typ_hash.svg maximise = typ_arrow-maximise.svg minimise = typ_arrow-minimise.svg proj_chapter = mixed_document-chapter.svg +proj_details = typ_th-list-grey.svg proj_document = typ_document-text.svg proj_folder = typ_folder.svg proj_note = mixed_document-note.svg proj_scene = mixed_document-scene.svg +proj_stats = typ_chart-bar-grey.svg proj_title = mixed_document-title.svg reference = typ_at.svg refresh = typ_refresh.svg @@ -76,3 +78,6 @@ status_time = typ_stopwatch-grey.svg sticky-off = typ_pin-outline.svg sticky-on = typ_pin.svg up = typ_chevron-up.svg +view_editor = typ_edit.svg +view_novel = typ_book-grey.svg +view_outline = typ_puzzle-outline.svg diff --git a/novelwriter/assets/icons/typicons_dark/typ_book-grey.svg b/novelwriter/assets/icons/typicons_dark/typ_book-grey.svg new file mode 100644 index 00000000..a0f49771 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_book-grey.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_edit.svg b/novelwriter/assets/icons/typicons_dark/typ_edit.svg new file mode 100644 index 00000000..652dda33 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_edit.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_puzzle-outline.svg b/novelwriter/assets/icons/typicons_dark/typ_puzzle-outline.svg new file mode 100644 index 00000000..526feec8 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_puzzle-outline.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 8639908f..a26d18b4 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -49,10 +49,12 @@ hash = typ_hash.svg maximise = typ_arrow-maximise.svg minimise = typ_arrow-minimise.svg proj_chapter = mixed_document-chapter.svg +proj_details = typ_th-list-grey.svg proj_document = typ_document-text.svg proj_folder = typ_folder.svg proj_note = mixed_document-note.svg proj_scene = mixed_document-scene.svg +proj_stats = typ_chart-bar-grey.svg proj_title = mixed_document-title.svg reference = typ_at.svg refresh = typ_refresh.svg @@ -76,3 +78,6 @@ status_time = typ_stopwatch-grey.svg sticky-off = typ_pin-outline.svg sticky-on = typ_pin.svg up = typ_chevron-up.svg +view_editor = typ_edit.svg +view_novel = typ_book-grey.svg +view_outline = typ_puzzle-outline.svg diff --git a/novelwriter/assets/icons/typicons_light/typ_book-grey.svg b/novelwriter/assets/icons/typicons_light/typ_book-grey.svg new file mode 100644 index 00000000..06f58ae1 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_book-grey.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_edit.svg b/novelwriter/assets/icons/typicons_light/typ_edit.svg new file mode 100644 index 00000000..512c1592 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_edit.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_puzzle-outline.svg b/novelwriter/assets/icons/typicons_light/typ_puzzle-outline.svg new file mode 100644 index 00000000..e2792d41 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_puzzle-outline.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 19fe823a..e3151ea8 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -456,20 +456,18 @@ class GuiIcons: ICON_KEYS = { # Project and GUI icons - "novelwriter", "proj_nwx", - "cls_none", "cls_novel", "cls_plot", "cls_character", "cls_world", - "cls_timeline", "cls_object", "cls_entity", "cls_custom", "cls_archive", "cls_trash", - "proj_document", "proj_title", "proj_chapter", "proj_scene", "proj_note", "proj_folder", - "status_lang", "status_time", "status_idle", "status_stats", "status_lines", - "doc_h0", "doc_h1", "doc_h2", "doc_h3", "doc_h4", - "search_case", "search_regex", "search_word", "search_loop", "search_project", - "search_cancel", "search_preserve", + "novelwriter", "cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", + "cls_novel", "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world", "doc_h0", + "doc_h1", "doc_h2", "doc_h3", "doc_h4", "proj_chapter", "proj_details", "proj_document", + "proj_folder", "proj_note", "proj_nwx", "proj_scene", "proj_stats", "proj_title", + "search_cancel", "search_case", "search_loop", "search_preserve", "search_project", + "search_regex", "search_word", "status_idle", "status_lang", "status_lines", + "status_stats", "status_time", "view_editor", "view_novel", "view_outline", # General Button Icons - "delete", "close", "done", "clear", "save", "add", "remove", - "search", "search_replace", "edit", "check", "cross", "hash", - "maximise", "minimise", "refresh", "reference", "backward", - "forward", "settings", "up", "down", + "add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit", + "forward", "hash", "maximise", "minimise", "reference", "refresh", "remove", "save", + "search_replace", "search", "settings", "up", # Switches "sticky-on", "sticky-off", diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py index 71974357..92244781 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/viewsbar.py @@ -64,23 +64,23 @@ class GuiViewsBar(QToolBar): # Actions self.aProject = QAction(self.tr("Project")) - self.aProject.setIcon(self.theTheme.getIcon("proj_folder")) + self.aProject.setIcon(self.theTheme.getIcon("view_editor")) self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) self.aNovel = QAction(self.tr("Novel")) - self.aNovel.setIcon(self.theTheme.getIcon("cls_novel")) + self.aNovel.setIcon(self.theTheme.getIcon("view_novel")) self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) self.aOutline = QAction(self.tr("Outline")) - self.aOutline.setIcon(self.theTheme.getIcon("cls_plot")) + self.aOutline.setIcon(self.theTheme.getIcon("view_outline")) self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) self.aDetails = QAction(self.tr("Details")) - self.aDetails.setIcon(self.theTheme.getIcon("status_lines")) + self.aDetails.setIcon(self.theTheme.getIcon("proj_details")) self.aDetails.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.DETAILS)) self.aStats = QAction(self.tr("Stats")) - self.aStats.setIcon(self.theTheme.getIcon("status_stats")) + self.aStats.setIcon(self.theTheme.getIcon("proj_stats")) self.aStats.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.STATS)) self.aSettings = QAction(self.tr("Settings")) From 096c2dbc835188c42b2513fc3b043d9feea1ffae Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 14 May 2022 19:03:51 +0200 Subject: [PATCH 065/179] Add Typicons link to credits --- CREDITS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CREDITS.md b/CREDITS.md index e770434f..74089153 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -32,7 +32,7 @@ The following libraries are dependencies of novelWriter: Some of the assets bundled with novelWriter were adapted from the following sources: -* Typicons icons by Stephen Hutchings (CC BY-SA 4.0) +* [Typicons](https://github.com/stephenhutchings/typicons.font) icons by Stephen Hutchings (CC BY-SA 4.0) * Tomorrow syntax themes by Chris Kempson (MIT License) * Owl syntax themes by Sarah Drasner (MIT License) * Solarized themes by Ethan Schoonover, added by @nullbasis (MIT License) From d965320b57c99e1cd0e3e28eb022e761ace70f47 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 14 May 2022 19:07:34 +0200 Subject: [PATCH 066/179] Fix broken test --- tests/test_gui/test_gui_outline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 901a2bd2..40c58388 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -43,7 +43,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.mainConf.lastPath = nwLipsum nwGUI.rebuildIndex() - nwGUI.mainStack.setCurrentIndex(nwGUI.idxTabProj) + nwGUI.mainStack.setCurrentIndex(nwGUI.idxOutlineView) assert nwGUI.projView.topLevelItemCount() > 0 From 1d5c8566d105b4ed16ce72fbf630443f7f77f3c6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 15 May 2022 15:11:17 +0200 Subject: [PATCH 067/179] Check the sanity of paths during project wizard run (issue #1058) --- novelwriter/tools/projwizard.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index 8f746ca6..e4cc6862 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -185,6 +185,9 @@ class ProjWizardFolderPage(QWizardPage): self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) self.browseButton.clicked.connect(self._doBrowse) + self.errLabel = QLabel("") + self.errLabel.setWordWrap(True) + self.mainForm = QHBoxLayout() self.mainForm.addWidget(QLabel(self.tr("Project Path")), 0) self.mainForm.addWidget(self.projPath, 1) @@ -198,11 +201,36 @@ class ProjWizardFolderPage(QWizardPage): self.outerBox.setSpacing(vS) self.outerBox.addWidget(self.theText) self.outerBox.addLayout(self.mainForm) + self.outerBox.addWidget(self.errLabel) self.outerBox.addStretch(1) self.setLayout(self.outerBox) return + def isComplete(self): + """Check that the selected path isn't already being used. + """ + self.errLabel.setText("") + if not QWizardPage.isComplete(self): + return False + + setPath = os.path.abspath(os.path.expanduser(self.projPath.text())) + parPath = os.path.dirname(setPath) + logger.verbose("Path is: %s", setPath) + if parPath and not os.path.isdir(parPath): + self.errLabel.setText(self.tr( + "Error: A project folder cannot be created using this path." + )) + return False + + if os.path.exists(setPath): + self.errLabel.setText(self.tr( + "Error: The selected path already exists." + )) + return False + + return True + ## # Slots ## From c91f19f9f707f367481b0aafd09954a04dd376cb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 15 May 2022 16:31:51 +0200 Subject: [PATCH 068/179] Update wizard test coverage --- tests/test_tools/test_tools_projwizard.py | 328 ++++++++++++---------- 1 file changed, 183 insertions(+), 145 deletions(-) diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py index 53f1cc79..f82e45a6 100644 --- a/tests/test_tools/test_tools_projwizard.py +++ b/tests/test_tools/test_tools_projwizard.py @@ -21,12 +21,11 @@ along with this program. If not, see . import pytest import os -import sys from tools import getGuiItem from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QFileDialog, QWizard, QMessageBox +from PyQt5.QtWidgets import QFileDialog, QWizard, QMessageBox, QDialog from novelwriter.enum import nwItemClass from novelwriter.tools.projwizard import ( @@ -40,9 +39,9 @@ stepDelay = 20 @pytest.mark.gui -@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") -def testToolProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal): - """Test the new project wizard. +# @pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") +def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal): + """Test the launch of the project wizard. Disabled for macOS because the test segfaults on QWizard.show() """ # Block message box @@ -55,173 +54,212 @@ def testToolProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal): # New with a project open should cause an error assert nwGUI.openProject(nwMinimal) - assert not nwGUI.newProject() + with monkeypatch.context() as mp: + mp.setattr(nwGUI, "closeProject", lambda *a: False) + assert nwGUI.newProject() is False # Close project, but call with invalid path assert nwGUI.closeProject() with monkeypatch.context() as mp: mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: None) - assert not nwGUI.newProject() + assert nwGUI.newProject() is False # Now, with an empty dictionary mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {}) - assert not nwGUI.newProject() + assert nwGUI.newProject() is False # Now, with a non-empty folder mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {"projPath": nwMinimal}) - assert not nwGUI.newProject() + assert nwGUI.newProject() is False ## - # Test the Wizard + # Test the Wizard Launching ## - monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None) nwGUI.mainConf.lastPath = " " + monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None) - nwGUI.closeProject() - nwGUI.showNewProjectDialog() + result = nwGUI.showNewProjectDialog() qtbot.waitUntil(lambda: getGuiItem("GuiProjectWizard") is not None, timeout=1000) nwWiz = getGuiItem("GuiProjectWizard") assert isinstance(nwWiz, GuiProjectWizard) nwWiz.show() + qtbot.wait(stepDelay) + qtbot.mouseClick(nwWiz.button(QWizard.CancelButton), Qt.LeftButton) + assert result is None - for wStep in range(4): - # This does not actually create the project, it just generates the - # dictionary that defines it. - - # Intro Page - introPage = nwWiz.currentPage() - assert isinstance(introPage, ProjWizardIntroPage) - assert not nwWiz.button(QWizard.NextButton).isEnabled() - - qtbot.wait(stepDelay) - for c in ("Test Minimal %d" % wStep): - qtbot.keyClick(introPage.projName, c, delay=typeDelay) - - qtbot.wait(stepDelay) - for c in "Minimal Novel": - qtbot.keyClick(introPage.projTitle, c, delay=typeDelay) - - qtbot.wait(stepDelay) - for c in "Jane Doe": - qtbot.keyClick(introPage.projAuthors, c, delay=typeDelay) - - # Setting projName should activate the button - assert nwWiz.button(QWizard.NextButton).isEnabled() - - qtbot.wait(stepDelay) - qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) - - # Folder Page - storagePage = nwWiz.currentPage() - assert isinstance(storagePage, ProjWizardFolderPage) - assert not nwWiz.button(QWizard.NextButton).isEnabled() - - if wStep == 0: - # Check invalid path first, the first time we reach here - monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: "") - qtbot.wait(stepDelay) - qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100) - assert storagePage.projPath.text() == "" - - # Then, we always return nwMinimal as path - monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: nwMinimal) - - qtbot.wait(stepDelay) - qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100) - projPath = os.path.join(nwMinimal, "Test Minimal %d" % wStep) - assert storagePage.projPath.text() == projPath - - # Setting projPath should activate the button - assert nwWiz.button(QWizard.NextButton).isEnabled() - - qtbot.wait(stepDelay) - qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) - - # Populate Page - popPage = nwWiz.currentPage() - assert isinstance(popPage, ProjWizardPopulatePage) - assert nwWiz.button(QWizard.NextButton).isEnabled() - - qtbot.wait(stepDelay) - if wStep == 0: - popPage.popMinimal.setChecked(True) - elif wStep == 1: - popPage.popCustom.setChecked(True) - elif wStep == 2: - popPage.popCustom.setChecked(True) - elif wStep == 3: - popPage.popSample.setChecked(True) - - qtbot.wait(stepDelay) - qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) - - # Custom Page - if wStep == 1 or wStep == 2: - customPage = nwWiz.currentPage() - assert isinstance(customPage, ProjWizardCustomPage) - assert nwWiz.button(QWizard.NextButton).isEnabled() - - customPage.addPlot.setChecked(True) - customPage.addChar.setChecked(True) - customPage.addWorld.setChecked(True) - customPage.addTime.setChecked(True) - customPage.addObject.setChecked(True) - customPage.addEntity.setChecked(True) - - if wStep == 2: - customPage.numChapters.setValue(0) - customPage.numScenes.setValue(10) - customPage.chFolders.setChecked(False) - - qtbot.wait(stepDelay) - qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) - - # Final Page - finalPage = nwWiz.currentPage() - assert isinstance(finalPage, ProjWizardFinalPage) - assert nwWiz.button(QWizard.FinishButton).isEnabled() # But we don't click it - - # Check Data - projData = nwGUI._assembleProjectWizardData(nwWiz) - assert projData["projName"] == "Test Minimal %d" % wStep - assert projData["projTitle"] == "Minimal Novel" - assert projData["projAuthors"] == "Jane Doe" - assert projData["projPath"] == projPath - assert projData["popMinimal"] == (wStep == 0) - assert projData["popCustom"] == (wStep == 1 or wStep == 2) - assert projData["popSample"] == (wStep == 3) - if wStep == 1 or wStep == 2: - assert projData["addRoots"] == [ - nwItemClass.PLOT, - nwItemClass.CHARACTER, - nwItemClass.WORLD, - nwItemClass.TIMELINE, - nwItemClass.OBJECT, - nwItemClass.ENTITY, - ] - if wStep == 1: - assert projData["numChapters"] == 5 - assert projData["numScenes"] == 5 - assert projData["chFolders"] - else: - assert projData["numChapters"] == 0 - assert projData["numScenes"] == 10 - assert not projData["chFolders"] - else: - assert projData["addRoots"] == [] - assert projData["numChapters"] == 0 - assert projData["numScenes"] == 0 - assert not projData["chFolders"] - - # Restart the wizard for next iteration - nwWiz.restart() + with monkeypatch.context() as mp: + mp.setattr(GuiProjectWizard, "result", lambda *a: QDialog.Accepted) + result = nwGUI.showNewProjectDialog() + nwWiz.button(QWizard.CancelButton).click() + assert isinstance(result, dict) nwWiz.reject() nwWiz.close() # qtbot.stopForInteraction() -# END Test testToolProjectWizard_Main +# END Test testToolProjectWizard_Handling + + +@pytest.mark.gui +@pytest.mark.parametrize("prjType", ["minimal", "custom1", "custom2", "sample"]) +def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType): + """Test the new project wizard with a set of selection scenarios. + """ + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None) + + nwGUI.mainConf.lastPath = " " + nwWiz = GuiProjectWizard(nwGUI) + nwWiz.show() + qtbot.wait(stepDelay) + + # Intro Page + # ========== + + introPage = nwWiz.currentPage() + assert isinstance(introPage, ProjWizardIntroPage) + assert not nwWiz.button(QWizard.NextButton).isEnabled() + + introPage.projName.setText("Test Wizard") + introPage.projTitle.setText("My Novel") + introPage.projAuthors.setPlainText("Jane Doe") + + # Setting projName should activate the button + assert nwWiz.button(QWizard.NextButton).isEnabled() + + qtbot.wait(stepDelay) + qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) + + # Folder Page + # =========== + + storagePage = nwWiz.currentPage() + assert isinstance(storagePage, ProjWizardFolderPage) + assert not nwWiz.button(QWizard.NextButton).isEnabled() + assert storagePage.errLabel.text() == "" + + # Set an invalid path + storagePage.projPath.setText(os.path.join(fncDir, "not", "a", "path")) + assert not nwWiz.button(QWizard.NextButton).isEnabled() + assert storagePage.errLabel.text().startswith("Error") + + # Set an existing path + storagePage.projPath.setText(fncDir) + assert not nwWiz.button(QWizard.NextButton).isEnabled() + assert storagePage.errLabel.text().startswith("Error") + + # Return a non-result from browse + with monkeypatch.context() as mp: + mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: "") + qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100) + assert storagePage.errLabel.text() == "" + + # Let the browse feature handle it + projPath = os.path.join(fncDir, "Test Wizard") + with monkeypatch.context() as mp: + mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: fncDir) + qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100) + + assert storagePage.projPath.text() == projPath + assert storagePage.errLabel.text() == "" + + # Setting projPath should activate the button + assert nwWiz.button(QWizard.NextButton).isEnabled() + + qtbot.wait(stepDelay) + qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) + + # Populate Page + # ============= + + popPage = nwWiz.currentPage() + assert isinstance(popPage, ProjWizardPopulatePage) + assert nwWiz.button(QWizard.NextButton).isEnabled() + + qtbot.wait(stepDelay) + if prjType.startswith("minimal"): + popPage.popMinimal.setChecked(True) + elif prjType.startswith("custom"): + popPage.popCustom.setChecked(True) + elif prjType.startswith("sample"): + popPage.popSample.setChecked(True) + + qtbot.wait(stepDelay) + qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) + + # Custom Page + # =========== + if prjType.startswith("custom"): + + customPage = nwWiz.currentPage() + assert isinstance(customPage, ProjWizardCustomPage) + assert nwWiz.button(QWizard.NextButton).isEnabled() + + customPage.addPlot.setChecked(True) + customPage.addChar.setChecked(True) + customPage.addWorld.setChecked(True) + customPage.addTime.setChecked(True) + customPage.addObject.setChecked(True) + customPage.addEntity.setChecked(True) + + if prjType == "custom2": + customPage.numChapters.setValue(0) + customPage.numScenes.setValue(10) + customPage.chFolders.setChecked(False) + + qtbot.wait(stepDelay) + qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) + + # Final Page + # ========== + + finalPage = nwWiz.currentPage() + assert isinstance(finalPage, ProjWizardFinalPage) + assert nwWiz.button(QWizard.FinishButton).isEnabled() # But we don't click it + + # Check Data + # ========== + + projData = nwGUI._assembleProjectWizardData(nwWiz) + assert projData["projName"] == "Test Wizard" + assert projData["projTitle"] == "My Novel" + assert projData["projAuthors"] == "Jane Doe" + assert projData["projPath"] == projPath + assert projData["popMinimal"] == prjType.startswith("minimal") + assert projData["popCustom"] == prjType.startswith("custom") + assert projData["popSample"] == prjType.startswith("sample") + if prjType.startswith("custom"): + assert projData["addRoots"] == [ + nwItemClass.PLOT, + nwItemClass.CHARACTER, + nwItemClass.WORLD, + nwItemClass.TIMELINE, + nwItemClass.OBJECT, + nwItemClass.ENTITY, + ] + if prjType == "custom1": + assert projData["numChapters"] == 5 + assert projData["numScenes"] == 5 + assert projData["chFolders"] + else: + assert projData["numChapters"] == 0 + assert projData["numScenes"] == 10 + assert not projData["chFolders"] + else: + assert projData["addRoots"] == [] + assert projData["numChapters"] == 0 + assert projData["numScenes"] == 0 + assert not projData["chFolders"] + + # Cleanup + nwWiz.reject() + nwWiz.close() + + # qtbot.stopForInteraction() + +# END Test testToolProjectWizard_Run From e2c10f52d2ba259690fbd9f1ef2cf0c1cd558138 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 15 May 2022 16:37:09 +0200 Subject: [PATCH 069/179] Disabled wizard test on macOS as it still segfaults --- tests/test_tools/test_tools_projwizard.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py index f82e45a6..62fae5f0 100644 --- a/tests/test_tools/test_tools_projwizard.py +++ b/tests/test_tools/test_tools_projwizard.py @@ -19,8 +19,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import os +import sys +import pytest from tools import getGuiItem @@ -39,7 +40,7 @@ stepDelay = 20 @pytest.mark.gui -# @pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") +@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal): """Test the launch of the project wizard. Disabled for macOS because the test segfaults on QWizard.show() From 7c7e4c2297f7bbf61f7a12a8bbeec055ef16c1d4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 15 May 2022 16:39:41 +0200 Subject: [PATCH 070/179] Disabled second wizard test on macOS as it too segfaults --- tests/test_tools/test_tools_projwizard.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py index 62fae5f0..ef4c31cd 100644 --- a/tests/test_tools/test_tools_projwizard.py +++ b/tests/test_tools/test_tools_projwizard.py @@ -107,6 +107,7 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal): @pytest.mark.gui @pytest.mark.parametrize("prjType", ["minimal", "custom1", "custom2", "sample"]) +@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType): """Test the new project wizard with a set of selection scenarios. """ From ca48e3716309f28844e885ab54ff7a8004f75c7b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 15 May 2022 17:45:51 +0200 Subject: [PATCH 071/179] Move the outline view into a wrapper widget --- novelwriter/gui/__init__.py | 2 - novelwriter/gui/outline.py | 407 +++++++++++++++++++++++++++++- novelwriter/gui/outlinedetails.py | 355 -------------------------- novelwriter/guimain.py | 30 +-- 4 files changed, 404 insertions(+), 390 deletions(-) delete mode 100644 novelwriter/gui/outlinedetails.py diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index 8364e342..e3560a99 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -25,7 +25,6 @@ from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.mainmenu import GuiMainMenu from novelwriter.gui.noveltree import GuiNovelTree from novelwriter.gui.outline import GuiOutline -from novelwriter.gui.outlinedetails import GuiOutlineDetails from novelwriter.gui.projtree import GuiProjectTree from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.theme import GuiTheme @@ -40,7 +39,6 @@ __all__ = [ "GuiMainStatus", "GuiNovelTree", "GuiOutline", - "GuiOutlineDetails", "GuiProjectTree", "GuiTheme", "GuiViewsBar", diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 1ac83ff1..3c31133a 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -4,7 +4,9 @@ novelWriter – GUI Project Outline GUI class for the project outline view File History: -Created: 2019-11-16 [0.4.1] +Created: 2019-11-16 [0.4.1] GuiOutlineView, GuiOutlineHeaderMenu +Created: 2020-06-02 [0.7.0] GuiOutlineDetails +Created: 2022-05-15 [1.7b1] GuiOutline This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -28,19 +30,79 @@ import novelwriter from time import time -from PyQt5.QtCore import Qt, QSize, pyqtSlot +from PyQt5.QtCore import ( + Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP +) from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView + QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel, + QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget ) -from novelwriter.enum import nwItemLayout, nwItemType, nwOutline +from novelwriter.enum import nwItemLayout, nwItemType, nwOutline, nwView from novelwriter.common import checkInt from novelwriter.constants import trConst, nwKeyWords, nwLabels + logger = logging.getLogger(__name__) -class GuiOutline(QTreeWidget): +class GuiOutline(QWidget): + + viewChangeRequested = pyqtSignal(nwView) + + def __init__(self, theParent): + QWidget.__init__(self, theParent) + + self.mainConf = novelwriter.CONFIG + self.theParent = theParent + self.theProject = theParent.theProject + + self.outlineView = GuiOutlineView(self) + self.outlineData = GuiOutlineDetails(self) + + self.splitOutline = QSplitter(Qt.Vertical) + self.splitOutline.addWidget(self.outlineView) + self.splitOutline.addWidget(self.outlineData) + self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.addWidget(self.splitOutline) + + self.setLayout(self.outerBox) + + return + + ## + # Methods + ## + + def splitSizes(self): + return self.splitOutline.sizes() + + def clearOutline(self): + self.outlineData.clearDetails() + return + + def initOutline(self): + self.outlineView.initOutline() + self.outlineData.initDetails() + return + + def closeOutline(self): + self.outlineView.closeOutline() + return + + def refreshView(self, overRide=False, novelChanged=False): + self.outlineView.refreshTree(overRide=overRide, novelChanged=novelChanged) + return + +# END Class GuiOutline + + +class GuiOutlineView(QTreeWidget): DEF_WIDTH = { nwOutline.TITLE: 200, @@ -82,17 +144,18 @@ class GuiOutline(QTreeWidget): nwOutline.SYNOP: False, } - def __init__(self, theParent): - QTreeWidget.__init__(self, theParent) + def __init__(self, theOutline): + QTreeWidget.__init__(self, theOutline) logger.debug("Initialising GuiOutline ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject - self.theTheme = theParent.theTheme - self.theIndex = theParent.theIndex - self.optState = theParent.theProject.optState + self.theOutline = theOutline + self.theParent = theOutline.theParent + self.theProject = theOutline.theParent.theProject + self.theTheme = theOutline.theParent.theTheme + self.theIndex = theOutline.theParent.theIndex + self.optState = theOutline.theParent.theProject.optState self.headerMenu = GuiOutlineHeaderMenu(self) self.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -233,7 +296,7 @@ class GuiOutline(QTreeWidget): if selItems: tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole) - self.theParent.projMeta.showItem(tHandle, sTitle) + self.theOutline.outlineData.showItem(tHandle, sTitle) self.theParent.treeView.setSelectedHandle(tHandle) return @@ -477,7 +540,7 @@ class GuiOutline(QTreeWidget): return newItem -# END Class GuiOutline +# END Class GuiOutlineView class GuiOutlineHeaderMenu(QMenu): @@ -533,3 +596,319 @@ class GuiOutlineHeaderMenu(QMenu): return # END Class GuiOutlineHeaderMenu + + +class GuiOutlineDetails(QScrollArea): + + LVL_MAP = { + "H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"), + "H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"), + "H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"), + "H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"), + } + + def __init__(self, theOutline): + QScrollArea.__init__(self, theOutline) + + logger.debug("Initialising GuiOutlineDetails ...") + + self.mainConf = novelwriter.CONFIG + self.theOutline = theOutline + self.theParent = theOutline.theParent + self.theProject = theOutline.theParent.theProject + self.theTheme = theOutline.theParent.theTheme + self.theIndex = theOutline.theParent.theIndex + self.optState = theOutline.theParent.theProject.optState + + # Sizes + minTitle = 30*self.theTheme.textNWidth + maxTitle = 40*self.theTheme.textNWidth + wCount = self.theTheme.getTextWidth("999,999") + hSpace = int(self.mainConf.pxInt(10)) + vSpace = int(self.mainConf.pxInt(4)) + + # Details Area + self.titleLabel = QLabel("%s" % self.tr("Title")) + self.fileLabel = QLabel("%s" % self.tr("Document")) + self.itemLabel = QLabel("%s" % self.tr("Status")) + self.titleValue = QLabel("") + self.fileValue = QLabel("") + self.itemValue = QLabel("") + + self.titleValue.setMinimumWidth(minTitle) + self.titleValue.setMaximumWidth(maxTitle) + self.fileValue.setMinimumWidth(minTitle) + self.fileValue.setMaximumWidth(maxTitle) + self.itemValue.setMinimumWidth(minTitle) + self.itemValue.setMaximumWidth(maxTitle) + + # Stats Area + self.cCLabel = QLabel("%s" % self.tr("Characters")) + self.wCLabel = QLabel("%s" % self.tr("Words")) + self.pCLabel = QLabel("%s" % self.tr("Paragraphs")) + self.cCValue = QLabel("") + self.wCValue = QLabel("") + self.pCValue = QLabel("") + + self.cCValue.setMinimumWidth(wCount) + self.wCValue.setMinimumWidth(wCount) + self.pCValue.setMinimumWidth(wCount) + self.cCValue.setAlignment(Qt.AlignRight) + self.wCValue.setAlignment(Qt.AlignRight) + self.pCValue.setAlignment(Qt.AlignRight) + + # Synopsis + self.synopLabel = QLabel("%s" % self.tr("Synopsis")) + self.synopValue = QLabel("") + self.synopLWrap = QHBoxLayout() + self.synopValue.setWordWrap(True) + self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft) + self.synopLWrap.addWidget(self.synopValue, 1) + + # Tags + self.povKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])) + self.focKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])) + self.chrKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY])) + self.pltKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])) + self.timKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY])) + self.wldKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY])) + self.objKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY])) + self.entKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY])) + self.cstKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY])) + + self.povKeyLWrap = QHBoxLayout() + self.focKeyLWrap = QHBoxLayout() + self.chrKeyLWrap = QHBoxLayout() + self.pltKeyLWrap = QHBoxLayout() + self.timKeyLWrap = QHBoxLayout() + self.wldKeyLWrap = QHBoxLayout() + self.objKeyLWrap = QHBoxLayout() + self.entKeyLWrap = QHBoxLayout() + self.cstKeyLWrap = QHBoxLayout() + + self.povKeyValue = QLabel("") + self.focKeyValue = QLabel("") + self.chrKeyValue = QLabel("") + self.pltKeyValue = QLabel("") + self.timKeyValue = QLabel("") + self.wldKeyValue = QLabel("") + self.objKeyValue = QLabel("") + self.entKeyValue = QLabel("") + self.cstKeyValue = QLabel("") + + self.povKeyValue.setWordWrap(True) + self.focKeyValue.setWordWrap(True) + self.chrKeyValue.setWordWrap(True) + self.pltKeyValue.setWordWrap(True) + self.timKeyValue.setWordWrap(True) + self.wldKeyValue.setWordWrap(True) + self.objKeyValue.setWordWrap(True) + self.entKeyValue.setWordWrap(True) + self.cstKeyValue.setWordWrap(True) + + self.povKeyValue.linkActivated.connect(self._tagClicked) + self.focKeyValue.linkActivated.connect(self._tagClicked) + self.chrKeyValue.linkActivated.connect(self._tagClicked) + self.pltKeyValue.linkActivated.connect(self._tagClicked) + self.timKeyValue.linkActivated.connect(self._tagClicked) + self.wldKeyValue.linkActivated.connect(self._tagClicked) + self.objKeyValue.linkActivated.connect(self._tagClicked) + self.entKeyValue.linkActivated.connect(self._tagClicked) + self.cstKeyValue.linkActivated.connect(self._tagClicked) + + self.povKeyLWrap.addWidget(self.povKeyValue, 1) + self.focKeyLWrap.addWidget(self.focKeyValue, 1) + self.chrKeyLWrap.addWidget(self.chrKeyValue, 1) + self.pltKeyLWrap.addWidget(self.pltKeyValue, 1) + self.timKeyLWrap.addWidget(self.timKeyValue, 1) + self.wldKeyLWrap.addWidget(self.wldKeyValue, 1) + self.objKeyLWrap.addWidget(self.objKeyValue, 1) + self.entKeyLWrap.addWidget(self.entKeyValue, 1) + self.cstKeyLWrap.addWidget(self.cstKeyValue, 1) + + # Selected Item Details + self.mainGroup = QGroupBox(self.tr("Title Details"), self) + self.mainForm = QGridLayout() + self.mainGroup.setLayout(self.mainForm) + + self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight) + self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight) + self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight) + self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft) + + self.mainForm.setColumnStretch(1, 1) + self.mainForm.setRowStretch(4, 1) + self.mainForm.setHorizontalSpacing(hSpace) + self.mainForm.setVerticalSpacing(vSpace) + + # Selected Item Tags + self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self) + self.tagsForm = QGridLayout() + self.tagsGroup.setLayout(self.tagsForm) + + self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + + self.tagsForm.setColumnStretch(1, 1) + self.tagsForm.setRowStretch(8, 1) + self.tagsForm.setHorizontalSpacing(hSpace) + self.tagsForm.setVerticalSpacing(vSpace) + + # Assemble + self.outerWidget = QWidget() + self.outerBox = QHBoxLayout() + self.outerBox.addWidget(self.mainGroup, 0) + self.outerBox.addWidget(self.tagsGroup, 1) + + self.outerWidget.setLayout(self.outerBox) + self.setWidget(self.outerWidget) + + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.setWidgetResizable(True) + + self.initDetails() + + logger.debug("GuiOutlineDetails initialisation complete") + + return + + def initDetails(self): + """Set or update outline settings. + """ + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + return + + def clearDetails(self): + """Clear all the data labels. + """ + self.titleLabel.setText("%s" % self.tr("Title")) + self.titleValue.setText("") + self.fileValue.setText("") + self.itemValue.setText("") + self.cCValue.setText("") + self.wCValue.setText("") + self.pCValue.setText("") + self.synopValue.setText("") + self.povKeyValue.setText("") + self.focKeyValue.setText("") + self.chrKeyValue.setText("") + self.pltKeyValue.setText("") + self.timKeyValue.setText("") + self.wldKeyValue.setText("") + self.objKeyValue.setText("") + self.entKeyValue.setText("") + self.cstKeyValue.setText("") + return + + def showItem(self, tHandle, sTitle): + """Update the content of the tree with the given handle and line + number pointing to a header. + """ + nwItem = self.theProject.projTree[tHandle] + novIdx = self.theIndex.getNovelData(tHandle, sTitle) + theRefs = self.theIndex.getReferences(tHandle, sTitle) + if nwItem is None or novIdx is None: + return False + + if novIdx["level"] in self.LVL_MAP: + self.titleLabel.setText("%s" % self.tr(self.LVL_MAP[novIdx["level"]])) + else: + self.titleLabel.setText("%s" % self.tr("Title")) + self.titleValue.setText(novIdx["title"]) + + itemStatus, _ = nwItem.getImportStatus() + + self.fileValue.setText(nwItem.itemName) + self.itemValue.setText(itemStatus) + + cC = checkInt(novIdx["cCount"], 0) + wC = checkInt(novIdx["wCount"], 0) + pC = checkInt(novIdx["pCount"], 0) + + self.cCValue.setText(f"{cC:n}") + self.wCValue.setText(f"{wC:n}") + self.pCValue.setText(f"{pC:n}") + + self.synopValue.setText(novIdx["synopsis"]) + + self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY)) + self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY)) + self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY)) + self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY)) + self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY)) + self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY)) + self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY)) + self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY)) + self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY)) + + return True + + ## + # Slots + ## + + def _tagClicked(self, theLink): + """Capture the click of a tag in the right-most column. + """ + logger.verbose("Clicked link: '%s'", theLink) + if len(theLink) > 0: + theBits = theLink.split("=") + if len(theBits) == 2: + self.theOutline.viewChangeRequested.emit(nwView.PROJECT) + self.theParent.docViewer.loadFromTag(theBits[1]) + return + + ## + # Internal Functions + ## + + def _formatTags(self, theRefs, theKey): + """Format the tags as clickable links. + """ + if theKey not in theRefs: + return "" + refTags = [] + for tTag in theRefs[theKey]: + refTags.append("%s" % ( + theKey[1:], tTag, tTag + )) + return ", ".join(refTags) + +# END Class GuiOutlineDetails diff --git a/novelwriter/gui/outlinedetails.py b/novelwriter/gui/outlinedetails.py deleted file mode 100644 index 358a6531..00000000 --- a/novelwriter/gui/outlinedetails.py +++ /dev/null @@ -1,355 +0,0 @@ -""" -novelWriter – GUI Project Outline Details -========================================= -GUI class for the project outline details panel - -File History: -Created: 2020-06-02 [0.7.0] - -This file is a part of novelWriter -Copyright 2018–2022, Veronica Berglyd Olsen - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, but -WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -import logging -import novelwriter - -from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP, pyqtSignal -from PyQt5.QtWidgets import ( - QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel -) - -from novelwriter.enum import nwView -from novelwriter.common import checkInt -from novelwriter.constants import trConst, nwKeyWords, nwLabels - -logger = logging.getLogger(__name__) - - -class GuiOutlineDetails(QScrollArea): - - LVL_MAP = { - "H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"), - "H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"), - "H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"), - "H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"), - } - - viewChangeRequested = pyqtSignal(nwView) - - def __init__(self, theParent): - QScrollArea.__init__(self, theParent) - - logger.debug("Initialising GuiOutlineDetails ...") - - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject - self.theTheme = theParent.theTheme - self.theIndex = theParent.theIndex - self.optState = theParent.theProject.optState - - # Sizes - minTitle = 30*self.theTheme.textNWidth - maxTitle = 40*self.theTheme.textNWidth - wCount = self.theTheme.getTextWidth("999,999") - hSpace = int(self.mainConf.pxInt(10)) - vSpace = int(self.mainConf.pxInt(4)) - - # Details Area - self.titleLabel = QLabel("%s" % self.tr("Title")) - self.fileLabel = QLabel("%s" % self.tr("Document")) - self.itemLabel = QLabel("%s" % self.tr("Status")) - self.titleValue = QLabel("") - self.fileValue = QLabel("") - self.itemValue = QLabel("") - - self.titleValue.setMinimumWidth(minTitle) - self.titleValue.setMaximumWidth(maxTitle) - self.fileValue.setMinimumWidth(minTitle) - self.fileValue.setMaximumWidth(maxTitle) - self.itemValue.setMinimumWidth(minTitle) - self.itemValue.setMaximumWidth(maxTitle) - - # Stats Area - self.cCLabel = QLabel("%s" % self.tr("Characters")) - self.wCLabel = QLabel("%s" % self.tr("Words")) - self.pCLabel = QLabel("%s" % self.tr("Paragraphs")) - self.cCValue = QLabel("") - self.wCValue = QLabel("") - self.pCValue = QLabel("") - - self.cCValue.setMinimumWidth(wCount) - self.wCValue.setMinimumWidth(wCount) - self.pCValue.setMinimumWidth(wCount) - self.cCValue.setAlignment(Qt.AlignRight) - self.wCValue.setAlignment(Qt.AlignRight) - self.pCValue.setAlignment(Qt.AlignRight) - - # Synopsis - self.synopLabel = QLabel("%s" % self.tr("Synopsis")) - self.synopValue = QLabel("") - self.synopLWrap = QHBoxLayout() - self.synopValue.setWordWrap(True) - self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft) - self.synopLWrap.addWidget(self.synopValue, 1) - - # Tags - self.povKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])) - self.focKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])) - self.chrKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY])) - self.pltKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])) - self.timKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY])) - self.wldKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY])) - self.objKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY])) - self.entKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY])) - self.cstKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY])) - - self.povKeyLWrap = QHBoxLayout() - self.focKeyLWrap = QHBoxLayout() - self.chrKeyLWrap = QHBoxLayout() - self.pltKeyLWrap = QHBoxLayout() - self.timKeyLWrap = QHBoxLayout() - self.wldKeyLWrap = QHBoxLayout() - self.objKeyLWrap = QHBoxLayout() - self.entKeyLWrap = QHBoxLayout() - self.cstKeyLWrap = QHBoxLayout() - - self.povKeyValue = QLabel("") - self.focKeyValue = QLabel("") - self.chrKeyValue = QLabel("") - self.pltKeyValue = QLabel("") - self.timKeyValue = QLabel("") - self.wldKeyValue = QLabel("") - self.objKeyValue = QLabel("") - self.entKeyValue = QLabel("") - self.cstKeyValue = QLabel("") - - self.povKeyValue.setWordWrap(True) - self.focKeyValue.setWordWrap(True) - self.chrKeyValue.setWordWrap(True) - self.pltKeyValue.setWordWrap(True) - self.timKeyValue.setWordWrap(True) - self.wldKeyValue.setWordWrap(True) - self.objKeyValue.setWordWrap(True) - self.entKeyValue.setWordWrap(True) - self.cstKeyValue.setWordWrap(True) - - self.povKeyValue.linkActivated.connect(self._tagClicked) - self.focKeyValue.linkActivated.connect(self._tagClicked) - self.chrKeyValue.linkActivated.connect(self._tagClicked) - self.pltKeyValue.linkActivated.connect(self._tagClicked) - self.timKeyValue.linkActivated.connect(self._tagClicked) - self.wldKeyValue.linkActivated.connect(self._tagClicked) - self.objKeyValue.linkActivated.connect(self._tagClicked) - self.entKeyValue.linkActivated.connect(self._tagClicked) - self.cstKeyValue.linkActivated.connect(self._tagClicked) - - self.povKeyLWrap.addWidget(self.povKeyValue, 1) - self.focKeyLWrap.addWidget(self.focKeyValue, 1) - self.chrKeyLWrap.addWidget(self.chrKeyValue, 1) - self.pltKeyLWrap.addWidget(self.pltKeyValue, 1) - self.timKeyLWrap.addWidget(self.timKeyValue, 1) - self.wldKeyLWrap.addWidget(self.wldKeyValue, 1) - self.objKeyLWrap.addWidget(self.objKeyValue, 1) - self.entKeyLWrap.addWidget(self.entKeyValue, 1) - self.cstKeyLWrap.addWidget(self.cstKeyValue, 1) - - # Selected Item Details - self.mainGroup = QGroupBox(self.tr("Title Details"), self) - self.mainForm = QGridLayout() - self.mainGroup.setLayout(self.mainForm) - - self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight) - self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight) - self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight) - self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft) - self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft) - - self.mainForm.setColumnStretch(1, 1) - self.mainForm.setRowStretch(4, 1) - self.mainForm.setHorizontalSpacing(hSpace) - self.mainForm.setVerticalSpacing(vSpace) - - # Selected Item Tags - self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self) - self.tagsForm = QGridLayout() - self.tagsGroup.setLayout(self.tagsForm) - - self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) - self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) - - self.tagsForm.setColumnStretch(1, 1) - self.tagsForm.setRowStretch(8, 1) - self.tagsForm.setHorizontalSpacing(hSpace) - self.tagsForm.setVerticalSpacing(vSpace) - - # Assemble - self.outerWidget = QWidget() - self.outerBox = QHBoxLayout() - self.outerBox.addWidget(self.mainGroup, 0) - self.outerBox.addWidget(self.tagsGroup, 1) - - self.outerWidget.setLayout(self.outerBox) - self.setWidget(self.outerWidget) - - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.setWidgetResizable(True) - - self.initDetails() - - logger.debug("GuiOutlineDetails initialisation complete") - - return - - def initDetails(self): - """Set or update outline settings. - """ - # Scroll bars - if self.mainConf.hideVScroll: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - else: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) - - if self.mainConf.hideHScroll: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - else: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - - return - - def clearDetails(self): - """Clear all the data labels. - """ - self.titleLabel.setText("%s" % self.tr("Title")) - self.titleValue.setText("") - self.fileValue.setText("") - self.itemValue.setText("") - self.cCValue.setText("") - self.wCValue.setText("") - self.pCValue.setText("") - self.synopValue.setText("") - self.povKeyValue.setText("") - self.focKeyValue.setText("") - self.chrKeyValue.setText("") - self.pltKeyValue.setText("") - self.timKeyValue.setText("") - self.wldKeyValue.setText("") - self.objKeyValue.setText("") - self.entKeyValue.setText("") - self.cstKeyValue.setText("") - return - - def showItem(self, tHandle, sTitle): - """Update the content of the tree with the given handle and line - number pointing to a header. - """ - nwItem = self.theProject.projTree[tHandle] - novIdx = self.theIndex.getNovelData(tHandle, sTitle) - theRefs = self.theIndex.getReferences(tHandle, sTitle) - if nwItem is None or novIdx is None: - return False - - if novIdx["level"] in self.LVL_MAP: - self.titleLabel.setText("%s" % self.tr(self.LVL_MAP[novIdx["level"]])) - else: - self.titleLabel.setText("%s" % self.tr("Title")) - self.titleValue.setText(novIdx["title"]) - - itemStatus, _ = nwItem.getImportStatus() - - self.fileValue.setText(nwItem.itemName) - self.itemValue.setText(itemStatus) - - cC = checkInt(novIdx["cCount"], 0) - wC = checkInt(novIdx["wCount"], 0) - pC = checkInt(novIdx["pCount"], 0) - - self.cCValue.setText(f"{cC:n}") - self.wCValue.setText(f"{wC:n}") - self.pCValue.setText(f"{pC:n}") - - self.synopValue.setText(novIdx["synopsis"]) - - self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY)) - self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY)) - self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY)) - self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY)) - self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY)) - self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY)) - self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY)) - self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY)) - self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY)) - - return True - - ## - # Slots - ## - - def _tagClicked(self, theLink): - """Capture the click of a tag in the right-most column. - """ - logger.verbose("Clicked link: '%s'", theLink) - if len(theLink) > 0: - theBits = theLink.split("=") - if len(theBits) == 2: - self.viewChangeRequested.emit(nwView.PROJECT) - self.theParent.docViewer.loadFromTag(theBits[1]) - return - - ## - # Internal Functions - ## - - def _formatTags(self, theRefs, theKey): - """Format the tags as clickable links. - """ - if theKey not in theRefs: - return "" - refTags = [] - for tTag in theRefs[theKey]: - refTags.append("%s" % ( - theKey[1:], tTag, tTag - )) - return ", ".join(refTags) - -# END Class GuiOutlineDetails diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ad06cc59..645990e9 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -39,8 +39,8 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, - GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails, GuiProjectTree, - GuiTheme, GuiViewsBar + GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectTree, GuiTheme, + GuiViewsBar ) from novelwriter.dialogs import ( GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences, @@ -112,7 +112,6 @@ class GuiMain(QMainWindow): self.docViewer = GuiDocViewer(self) self.treeMeta = GuiItemDetails(self) self.projView = GuiOutline(self) - self.projMeta = GuiOutlineDetails(self) self.mainMenu = GuiMainMenu(self) self.viewsBar = GuiViewsBar(self) @@ -128,7 +127,7 @@ class GuiMain(QMainWindow): self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) self.viewsBar.viewChangeRequested.connect(self._changeView) - self.projMeta.viewChangeRequested.connect(self._changeView) + self.projView.viewChangeRequested.connect(self._changeView) # Project Tree Stack self.projStack = QStackedWidget() @@ -156,12 +155,6 @@ class GuiMain(QMainWindow): self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.splitView) - # Splitter : Project Outlie / Outline Details - self.splitOutline = QSplitter(Qt.Vertical) - self.splitOutline.addWidget(self.projView) - self.splitOutline.addWidget(self.projMeta) - self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) - # Splitter : Project Tree / Main Tabs self.splitMain = QSplitter(Qt.Horizontal) self.splitMain.setContentsMargins(0, 0, mPx, 0) @@ -172,7 +165,7 @@ class GuiMain(QMainWindow): # Main Stack : Editor / Outline self.mainStack = QStackedWidget() self.mainStack.addWidget(self.splitMain) - self.mainStack.addWidget(self.splitOutline) + self.mainStack.addWidget(self.projView) self.mainStack.currentChanged.connect(self._mainStackChanged) # Indices of Splitter Widgets @@ -185,7 +178,7 @@ class GuiMain(QMainWindow): # Indices of Tab Widgets self.idxEditorView = self.mainStack.indexOf(self.splitMain) - self.idxOutlineView = self.mainStack.indexOf(self.splitOutline) + self.idxOutlineView = self.mainStack.indexOf(self.projView) self.idxTreeView = self.projStack.indexOf(self.treeView) self.idxNovelView = self.projStack.indexOf(self.novelView) @@ -293,7 +286,7 @@ class GuiMain(QMainWindow): self.docEditor.clearEditor() self.docEditor.setDictionaries() self.closeDocViewer() - self.projMeta.clearDetails() + self.projView.clearOutline() # General self.statusBar.clearStatus() @@ -904,7 +897,7 @@ class GuiMain(QMainWindow): logger.verbose("Forcing a rebuild of the Project Outline") self._changeView(nwView.OUTLINE) - self.projView.refreshTree(overRide=True) + self.projView.refreshView(overRide=True) return True @@ -955,7 +948,6 @@ class GuiMain(QMainWindow): self.treeView.initTree() self.novelView.initTree() self.projView.initOutline() - self.projMeta.initDetails() self._updateStatusWordCount() return @@ -1192,7 +1184,7 @@ class GuiMain(QMainWindow): if not self.isFocusMode: self.mainConf.setMainPanePos(self.splitMain.sizes()) self.mainConf.setDocPanePos(self.splitDocs.sizes()) - self.mainConf.setOutlinePanePos(self.splitOutline.sizes()) + self.mainConf.setOutlinePanePos(self.projView.splitSizes()) if self.viewMeta.isVisible(): self.mainConf.setViewPanePos(self.splitView.sizes()) @@ -1500,7 +1492,7 @@ class GuiMain(QMainWindow): self.projStack.setCurrentWidget(self.novelView) elif view == nwView.OUTLINE: - self.mainStack.setCurrentWidget(self.splitOutline) + self.mainStack.setCurrentWidget(self.projView) elif view == nwView.DETAILS: self.showProjectDetailsDialog() @@ -1590,7 +1582,7 @@ class GuiMain(QMainWindow): logger.verbose("Novel tree changed while Outline tab active") if self.hasProject: self.treeView.flushTreeOrder() - self.projView.refreshTree(novelChanged=True) + self.projView.refreshView(novelChanged=True) return @@ -1623,7 +1615,7 @@ class GuiMain(QMainWindow): elif tabIndex == self.idxOutlineView: logger.verbose("Project outline tab activated") if self.hasProject: - self.projView.refreshTree() + self.projView.refreshView() return From a5b2a06f84ad9ca714fd2f85c1387a330459c529 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 15 May 2022 17:56:36 +0200 Subject: [PATCH 072/179] Fix tests and add missing methods --- novelwriter/gui/outline.py | 9 ++++ novelwriter/guimain.py | 4 +- tests/test_gui/test_gui_guimain.py | 6 +-- tests/test_gui/test_gui_outline.py | 69 ++++++++++++++++-------------- 4 files changed, 50 insertions(+), 38 deletions(-) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 3c31133a..652af9d1 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -73,6 +73,9 @@ class GuiOutline(QWidget): self.setLayout(self.outerBox) + # Function Mappings + self.getSelectedHandle = self.outlineView.getSelectedHandle + return ## @@ -99,6 +102,12 @@ class GuiOutline(QWidget): self.outlineView.refreshTree(overRide=overRide, novelChanged=novelChanged) return + def treeFocus(self): + return self.outlineView.hasFocus() + + def setTreeFocus(self): + return self.outlineView.setFocus() + # END Class GuiOutline diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 645990e9..e30654ee 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -780,7 +780,7 @@ class GuiMain(QMainWindow): tHandle = self.treeView.getSelectedHandle() elif self.novelView.hasFocus(): tHandle, tLine = self.novelView.getSelectedHandle() - elif self.projView.hasFocus(): + elif self.projView.treeFocus(): tHandle, tLine = self.projView.getSelectedHandle() else: logger.warning("No item selected") @@ -1221,7 +1221,7 @@ class GuiMain(QMainWindow): self.docViewer.setFocus() elif paneNo == nwWidget.OUTLINE: self._changeView(nwView.OUTLINE) - self.projView.setFocus() + self.projView.setTreeFocus() return def closeDocEditor(self): diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index ddb4f09b..cac62ddf 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -111,12 +111,12 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Project Outline has focus nwGUI.switchFocus(nwWidget.OUTLINE) with monkeypatch.context() as mp: - mp.setattr(GuiOutline, "hasFocus", lambda *a: True) + mp.setattr(GuiOutline, "treeFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - actItem = nwGUI.projView.topLevelItem(0) + actItem = nwGUI.projView.outlineView.topLevelItem(0) chpItem = actItem.child(0) selItem = chpItem.child(0) - nwGUI.projView.setCurrentItem(selItem) + nwGUI.projView.outlineView.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 40c58388..02dd1041 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -45,69 +45,72 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.rebuildIndex() nwGUI.mainStack.setCurrentIndex(nwGUI.idxOutlineView) - assert nwGUI.projView.topLevelItemCount() > 0 + outlineView = nwGUI.projView.outlineView + outlineData = nwGUI.projView.outlineData + + assert outlineView.topLevelItemCount() > 0 # Context Menu - nwGUI.projView._headerRightClick(QPoint(1, 1)) - nwGUI.projView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger) - nwGUI.projView.headerMenu.close() - qtbot.mouseClick(nwGUI.projView, Qt.LeftButton) + outlineView._headerRightClick(QPoint(1, 1)) + outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger) + outlineView.headerMenu.close() + qtbot.mouseClick(outlineView, Qt.LeftButton) - nwGUI.projView._loadHeaderState() - assert not nwGUI.projView._colHidden[nwOutline.CCOUNT] + outlineView._loadHeaderState() + assert not outlineView._colHidden[nwOutline.CCOUNT] # First Item nwGUI.rebuildOutline() - selItem = nwGUI.projView.topLevelItem(0) + selItem = outlineView.topLevelItem(0) assert isinstance(selItem, QTreeWidgetItem) - nwGUI.projView.setCurrentItem(selItem) - assert nwGUI.projMeta.titleLabel.text() == "Title" - assert nwGUI.projMeta.titleValue.text() == "Lorem Ipsum" - assert nwGUI.projMeta.fileValue.text() == "Lorem Ipsum" - assert nwGUI.projMeta.itemValue.text() == "Finished" + outlineView.setCurrentItem(selItem) + assert outlineData.titleLabel.text() == "Title" + assert outlineData.titleValue.text() == "Lorem Ipsum" + assert outlineData.fileValue.text() == "Lorem Ipsum" + assert outlineData.itemValue.text() == "Finished" - assert nwGUI.projMeta.cCValue.text() == "230" - assert nwGUI.projMeta.wCValue.text() == "40" - assert nwGUI.projMeta.pCValue.text() == "3" + assert outlineData.cCValue.text() == "230" + assert outlineData.wCValue.text() == "40" + assert outlineData.pCValue.text() == "3" # Scene One - actItem = nwGUI.projView.topLevelItem(1) + actItem = outlineView.topLevelItem(1) chpItem = actItem.child(0) selItem = chpItem.child(0) - nwGUI.projView.setCurrentItem(selItem) - tHandle, tLine = nwGUI.projView.getSelectedHandle() + outlineView.setCurrentItem(selItem) + tHandle, tLine = outlineView.getSelectedHandle() assert tHandle == "88243afbe5ed8" assert tLine == 0 - assert nwGUI.projMeta.titleLabel.text() == "Scene" - assert nwGUI.projMeta.titleValue.text() == "Scene One" - assert nwGUI.projMeta.fileValue.text() == "Scene One" - assert nwGUI.projMeta.itemValue.text() == "Finished" + assert outlineData.titleLabel.text() == "Scene" + assert outlineData.titleValue.text() == "Scene One" + assert outlineData.fileValue.text() == "Scene One" + assert outlineData.itemValue.text() == "Finished" # Click POV Link - assert nwGUI.projMeta.povKeyValue.text() == "Bod" - nwGUI.projMeta._tagClicked("#pov=Bod") + assert outlineData.povKeyValue.text() == "Bod" + outlineData._tagClicked("#pov=Bod") assert nwGUI.docViewer.docHandle() == "4c4f28287af27" # Scene One, Section Two - actItem = nwGUI.projView.topLevelItem(1) + actItem = outlineView.topLevelItem(1) chpItem = actItem.child(0) scnItem = chpItem.child(0) selItem = scnItem.child(0) - nwGUI.projView.setCurrentItem(selItem) - tHandle, tLine = nwGUI.projView.getSelectedHandle() + outlineView.setCurrentItem(selItem) + tHandle, tLine = outlineView.getSelectedHandle() assert tHandle == "88243afbe5ed8" assert tLine == 12 - assert nwGUI.projMeta.titleLabel.text() == "Section" - assert nwGUI.projMeta.titleValue.text() == "Scene One, Section Two" - assert nwGUI.projMeta.fileValue.text() == "Scene One" - assert nwGUI.projMeta.itemValue.text() == "Finished" + assert outlineData.titleLabel.text() == "Section" + assert outlineData.titleValue.text() == "Scene One, Section Two" + assert outlineData.fileValue.text() == "Scene One" + assert outlineData.itemValue.text() == "Finished" - nwGUI.projView._treeDoubleClick(selItem, 0) + outlineView._treeDoubleClick(selItem, 0) assert nwGUI.docEditor.docHandle() == "88243afbe5ed8" # qtbot.stopForInteraction() From 9add2aaa87492c80187f7e232dcd5c97e125125a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 17 May 2022 17:51:40 +0200 Subject: [PATCH 073/179] Update the sample project to reflect new functionality --- sample/content/636b6aa9b697b.nwd | 10 +++---- sample/content/6a2d6d5f4f401.nwd | 6 ++--- sample/content/8a5deb88c0e97.nwd | 4 +-- sample/content/96b68994dfa3d.nwd | 8 +++--- sample/content/974e400180a99.nwd | 2 +- sample/content/ba8a28a246524.nwd | 4 +-- sample/content/bc0cbd2a407f3.nwd | 6 ++--- sample/nwProject.nwx | 46 +++++++++++++++----------------- 8 files changed, 41 insertions(+), 45 deletions(-) diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 2927b7d0..20a66690 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -7,13 +7,13 @@ @char: John @location: Earth -A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. +A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. The scene document can be sorted after the chapter document, or as a child of the chapter. Both result in the same output in the end, so it is a matter of preference. Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesn’t show it correctly, the export tool will not either. -In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.” +In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.” -If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with , and this C with . While is just . Press Ctrl+R to see what this looks like in the view pane. +If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with , and this C with . While is just . Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is sett in Project Settings. The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens. @@ -25,7 +25,7 @@ If you need to split a scene file up into further pieces, you can do so with the Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the Build Novel Project dialog. You can also have them replaced with scene separators like “* * *”. -#### Text Alignment +#### Text Alignment and Indentation The text by default will have the left or justified alignment in the main text files in your project. You can also specify alignment for a specific paragraph by “pushing” it away from an edge with a set of ‘>>’ or ‘<<’ symbols, like so: @@ -35,8 +35,6 @@ This text is left-aligned. << >> This text is centred. << -#### Text Indent - You can indent a paragraph from both the left and right margin with ‘>’ and ‘<’ symbols. > This paragraph is indented from both the left margin and the right margin. This is useful for when you want to quote a large chunk of text for instance. < diff --git a/sample/content/6a2d6d5f4f401.nwd b/sample/content/6a2d6d5f4f401.nwd index 60d8d2a0..2a536e9f 100644 --- a/sample/content/6a2d6d5f4f401.nwd +++ b/sample/content/6a2d6d5f4f401.nwd @@ -1,11 +1,11 @@ %%~name: Chapter One -%%~path: e7ded148d6e4a/6a2d6d5f4f401 +%%~path: 7031beac91f75/6a2d6d5f4f401 %%~kind: NOVEL/DOCUMENT ## So it Begins @pov: Jane @location: Earth -% Synopsis: We can add a chapter file, but keep the scene files separate. In the chapter file we can set the meta data that applies to the whole chapter if we wish to. +% Synopsis: We can add a chapter document, but keep the scene files separate. In the chapter document we can set the meta data that applies to the whole chapter if we wish to. You can add the scenes as child documents directly under the chapter. -A chapter can also contain leading text before the first scene. +A chapter can contain leading text before the first scene, like this piece of text. diff --git a/sample/content/8a5deb88c0e97.nwd b/sample/content/8a5deb88c0e97.nwd index c5b61a9f..31a99aeb 100644 --- a/sample/content/8a5deb88c0e97.nwd +++ b/sample/content/8a5deb88c0e97.nwd @@ -1,6 +1,6 @@ %%~name: Old File %%~path: ae9bf3c3ea159/8a5deb88c0e97 -%%~kind: NOVEL/DOCUMENT +%%~kind: ARCHIVE/DOCUMENT ### Discarded Scene -If you have files you no longer want in your main project, you can move them to the “Archive” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away, although the switch can be ignored when building the project, this folder cannot. +If you have files you no longer want in your main project, you can move them to the “Archive” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away. diff --git a/sample/content/96b68994dfa3d.nwd b/sample/content/96b68994dfa3d.nwd index 849cd41d..05ae7770 100644 --- a/sample/content/96b68994dfa3d.nwd +++ b/sample/content/96b68994dfa3d.nwd @@ -1,11 +1,11 @@ %%~name: A Note on Structure -%%~path: e7ded148d6e4a/96b68994dfa3d +%%~path: 7031beac91f75/96b68994dfa3d %%~kind: NOVEL/NOTE # A Note on Structure This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project. -In root folders that isn’t the Novel root folder, you can _only_ add notes. In the Novel root folder, you can choose between a number of layouts. Some of them are just to let yourself know what each file is for. +In root folders that aren’t the Novel or Archive root folders, you can _only_ add notes. In the Novel and Archive folder you can also add Project Documents, which are the documents that make up your actual story. ## Headers in Notes @@ -15,8 +15,10 @@ Unlike in novel files, headers in notes have no particular meaning other than vi The folders in the tree view have no structural meaning other than they’re a way for you to organise your files into groups in whatever way suits you. They are not intended to represent chapters, but you can of course use them for that. If you do, know that you still need to define chapter headers in your structure so novelWriter knows where you want them. +If you do have separate chapter documents, you can always add scene documents as child document of the chapter instead of using folders. + ## Linking Files and Notes -You can link files and notes together by assigning tags to them, and then reference them from other files. The file class of a file determines which reference keywords apply to each file. For instance a file in the Characters root folder can be referenced using either the @char keyword or the @pov keyword. +You can link project documents and notes together by assigning tags to the notes with the @tag keyword, and then reference them from other files using one of the many reference keywords. The file class of a file determines which reference keywords apply to each file. For instance a file in the Characters root folder can be referenced using either the @char keyword or the @pov keyword. If you want to see the content of the file the reference points to, you can click Ctrl+Enter with the cursor on top of the reference, and the view pane will show you the file. In the view pane, all references are clickable, so you can navigate further. At the bottom of the view pane, a list of files referencing the one your viewing will appear. This panel updates when you navigate, unless you make it sticky by clicking the sticky checkbox. \ No newline at end of file diff --git a/sample/content/974e400180a99.nwd b/sample/content/974e400180a99.nwd index ff9b71d5..99919687 100644 --- a/sample/content/974e400180a99.nwd +++ b/sample/content/974e400180a99.nwd @@ -6,4 +6,4 @@ This is a plain page with some text on it. -If you want the text to start on a fresh page, add the [NEW PAGE] code above the text. You can also add empty paragraphs with the [VSPACE] code. +If you want the text to start on a fresh page, add the [NEW PAGE] code above the text. You can also add empty paragraphs with the [VSPACE] code. The above code adds two empty paragraphs before the text starts. diff --git a/sample/content/ba8a28a246524.nwd b/sample/content/ba8a28a246524.nwd index cd684e19..c2acfb26 100644 --- a/sample/content/ba8a28a246524.nwd +++ b/sample/content/ba8a28a246524.nwd @@ -1,9 +1,9 @@ %%~name: Interlude -%%~path: e7ded148d6e4a/ba8a28a246524 +%%~path: 7031beac91f75/ba8a28a246524 %%~kind: NOVEL/DOCUMENT ##! Interlude -% Notice that this is a file with the flag ‘N.Un’. The ‘N’ means it’s a novel file, and the ‘Un’ means it’s an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue. +% Notice that this document has a title with a ‘!’ in it in addition to the two hash symbols. This means it is an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue. I am the very model of a modern Major-General I've information vegetable, animal, and mineral diff --git a/sample/content/bc0cbd2a407f3.nwd b/sample/content/bc0cbd2a407f3.nwd index 4ebbca1d..fd920d0d 100644 --- a/sample/content/bc0cbd2a407f3.nwd +++ b/sample/content/bc0cbd2a407f3.nwd @@ -7,9 +7,9 @@ @focus: Jane @location: Earth -Adding more scenes to a chapter is as easy as adding more scene files, with a level three heading, or just adding another level three heading in the same file if that works for the way you want to structure your files. +Adding more scenes to a chapter is as easy as adding more scene files with a level three heading. You can of course also just add another level three heading in the same file if that works for the way you want to structure your files. -In fact, if you wish, you can add all the scenes in the chapter file too. All novelWriter cares about is the level of the headings. +In fact, if you wish, you can add all the scenes in the chapter file too. All novelWriter cares about is the level of the headings and the order in which they appear. ### More Scenes @@ -17,4 +17,4 @@ In fact, if you wish, you can add all the scenes in the chapter file too. All no @focus: John @location: Earth -This is a second scene in the same file as the previous scene. You can always split the files up later. +This is a second scene in the same file as the previous scene. You can always split the files up later using the split tool. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 406fa423..21bd7776 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1327 - 207 - 66285 + 1328 + 220 + 67092 False @@ -17,9 +17,9 @@ True 636b6aa9b697b 636b6aa9b697b - 1206 - 830 - 376 + 1303 + 894 + 409 B E @@ -36,7 +36,7 @@ New Notes Started - 1st Draft + 1st Draft 2nd Draft 3rd Draft Finished @@ -48,7 +48,7 @@ Main - + Novel @@ -58,38 +58,34 @@ Title Page - + Page - + Part One - - - A Folder - - - + + Chapter One - + Making a Scene - + Another Scene - - + + Interlude - - + + A Note on Structure - + Chapter Two @@ -138,7 +134,7 @@ Scenes - + Old File From a71fa02b0c5857f6ddd0994558547fb22e6c6507 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 17 May 2022 18:40:10 +0200 Subject: [PATCH 074/179] Sort i18n credits on about and credits doc --- CREDITS.md | 8 +++++--- novelwriter/dialogs/about.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 74089153..469e4c40 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -11,12 +11,14 @@ ## Translations +The default language is English (UK) with English (US) as an option. + +* Dutch: Martijn van der Kleijn (@mvdkleijn) * French: Jan Lüdke (@jyhelle) +* Latin American Spanish: Tommy Marplatt (@tmarplatt) * Norwegian: Veronica Berglyd Olsen (@vkbo) * Portuguese: Bruno Meneguello (@bkmeneguello) * Simplified Chinese: Qianzhi Long (@longqzh) -* Latin American Spanish: Tommy Marplatt (@tmarplatt) -* Dutch: Martijn van der Kleijn (@mvdkleijn) ## Libraries @@ -32,7 +34,7 @@ The following libraries are dependencies of novelWriter: Some of the assets bundled with novelWriter were adapted from the following sources: -* [Typicons](https://github.com/stephenhutchings/typicons.font) icons by Stephen Hutchings (CC BY-SA 4.0) +* Typicons icons by [Stephen Hutchings](https://github.com/stephenhutchings/typicons.font) (CC BY-SA 4.0) * Tomorrow syntax themes by Chris Kempson (MIT License) * Owl syntax themes by Sarah Drasner (MIT License) * Solarized themes by Ethan Schoonover, added by @nullbasis (MIT License) diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 574fba34..f832343a 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -182,12 +182,12 @@ class GuiAbout(QDialog): self.tr("Translations"), self._wrapTable([ ("English", "Veronica Berglyd Olsen"), + ("Español Latinoamericano", "Tommy Marplatt"), ("Français", "Jan Lüdke (jyhelle)"), + ("Nederlands", "Martijn van der Kleijn"), ("Norsk Bokmål", "Veronica Berglyd Olsen"), ("Português", "Bruno Meneguello"), ("简体中文", "Qianzhi Long"), - ("Español Latinoamericano", "Tommy Marplatt"), - ("Nederlands", "Martijn van der Kleijn"), ]) ) From 9eaf240674f40d44269b30670014b534b8e83416 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 17 May 2022 18:41:01 +0200 Subject: [PATCH 075/179] Release 1.7-beta1 --- CHANGELOG.md | 89 +++++++++++++++++++++++ novelwriter/__init__.py | 6 +- novelwriter/assets/text/release_notes.htm | 54 ++------------ sample/nwProject.nwx | 6 +- 4 files changed, 102 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00509e4e..b1b08033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,94 @@ # novelWriter Changelog +## Version 1.7 Beta 1 [2022-05-17] + +### Release Notes + +This is a beta release of the next release version, and is intended for testing purposes. Please be +careful when using this version on live writing projects, and make sure you take frequent backups. + +Please check the changelog for an overview of changes. The full release notes will be added to the +final release. + +### Detailed Changelog + +**Features** + +* A simple tool to add Lorem Ipsum placeholder text has been added to the Insert menu. PR #1028. +* Status and Importance flags can now be reorganised in Project Settings. Issue #1035. PR #1040. +* It is now possible to create multiple Root Folders of the same kind. This makes it possible to + add multiple Novel root folders in a project, for instance. Issue #967. PR #1031. +* All documents can now be dragged and dropped anywhere in the project tree. The document layout + may be converted in the process. PR #1031. +* Documents in the project tree can now have other documents as child documents. Issue #1002. + PR #1047. +* Folders in the project tree that are not empty, can now be moved to trash. PR #1048. +* Empty folders are deleted on request, and not moved to trash. Issue #1052. PR #1055. + +**User Interface** + +* The tabs under the project tree and to the right of the main window have been replaced with a + toolbar on the left hand side. The toolbar has a set of buttons to change view between Project, + Novel and Outline. The three buttons that were available under the project tree have been moved + to the bottom of the new toolbar. Issue #1056. PR #1057. +* When a document changes from a project document to a note, and back again, the Status flag + setting is preserved. Previously, the Importance setting would overwrite it during the + conversion. PR #1030. +* Item labels, Status labels, and other labels on the GUI are now run through a "simplify" function + before being accepted. This functions strips out all whitespaces and consecutive whitespaces and + replace them with single plain whitespaces. This is a safer format to store in XML, and also + makes sure there aren't invisible characters floating around in the labels. PR #1038. +* Due to the changes to how drag and drop works, there are no longer any restrictions on folders + and documents. Only root folders remain restricted in terms of moving. Root folders can only be + reordered with the Move Up and Move Down commands. PR #1047. +* The label for the highlighting of redundant spaces in the Preferences dialog has been updated to + better reflect what it does. Issue #1043. PR #1046. +* The New Project Wizard will now try to check if the path selected for the new project can + actually be used before letting the user proceed to the next page. Issue #1058. PR #1062. + +**Internationalisation** + +* Dutch translations have been added by Martijn van der Kleijn (@mvdkleijn). PR #1027. + +**Functionality** + +* Documents that are missing in the project index when a project is opened are automatically + re-indexed. This also handles cases where the cached index is missing. PR #1039. + +**Installation and Packaging** + +* Python 3.6 is no longer supported. PR #1004. +* Ubuntu 18.04 packages will no longer be released, due to dropping Python 3.6. Issue #1005. + PR #1014. + +**Project File Format** + +* The item nodes in the content section of the main project XML file have been compacted. It now + consists of a main item node and meta and a name node. All settings have been made attributes of + one of these three nodes, except the item label which is the text value of the name node. The + file format version has been bumped to 1.4. Issue #995. PR #993. +* Both Importance and Status flag values are now saved to the project file. This means if a + document changes layout, the value is no longer lost. PR #1030. + +**Code Improvements** + +* The linting settings have been updated to select between mutually exclusive options in + pycodestyle. PR #1014. +* The Tokenizer class has been converted to an abstract base class. PR #1026. +* The class handling Status and Importance flags has been completely rewritten. The flags are now + handled using a unique random key as reference rather than relying on the text of the label + itself. This makes it a lot easier to rename them as there is no need to update project items. + PR #1034. +* Many of the decisions regarding where items are allowed to belong has been delegated to the + NWItem class that holds the item. Some is also handled by the NWTree class that holds the project + tree. A new maintenance function in the NWTree class will also ensure that the meta data of an + item is correct and up to date. This is especially important after an item has been moved, but is + also checked when items are initially loaded. PRs #1031 and #1054. +* Item handles are now generated using the standard library random number generator. The new + handles have the same format as the old algorithm, so they are compatible. PR #1044. + +---- + ## Version 1.6.2 [2022-03-20] ### Release Notes diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 164a05e7..0ae10566 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -60,9 +60,9 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "1.7-alpha0" -__hexversion__ = "0x010700a0" -__date__ = "2022-02-20" +__version__ = "1.7-beta1" +__hexversion__ = "0x010700b1" +__date__ = "2022-05-17" __status__ = "Stable" __domain__ = "novelwriter.io" __url__ = "https://novelwriter.io" diff --git a/novelwriter/assets/text/release_notes.htm b/novelwriter/assets/text/release_notes.htm index 0850e30c..aeb5fbc7 100644 --- a/novelwriter/assets/text/release_notes.htm +++ b/novelwriter/assets/text/release_notes.htm @@ -2,56 +2,16 @@ -

Release Notes for 1.6

-

Released on 20 February 2022

+

Release Notes for 1.7 Beta 1

+

Released on 17 May 2022

-

This release does not introduce any major new features, but is instead a collection of minor -improvements and tweaks based on user requests. There are also a number of changes under the hood -to improve the structure and performance of novelWriter.

-

Some key improvements to the user interface are:

-

✓ The max text width setting in Preferences now also applies to the document viewer, and -the setting itself on the Preference dialog has been simplified a bit.

-

✓ When text is selected in the document editor, the number of words selected is displayed -in the editor's footer area.

-

✓ The search tool in the document editor now shows the number of results in the -document.

-

✓ The Enter and Ctrl+O keyboard shortcuts should now work the same way in all tree -views.

-

✓ It is now possible to set a blank section title format on the Build Novel Project tool -and get empty paragraphs in the output. Previously, a blank format would just remove the section -break entirely. This change allows the user to define hard and soft scene breaks using level three -and four headings. The scene and section titles can be hidden completely with two new switches -added to the user interface.

-

Other feature changes include:

-

✓ The project index is now automatically rebuilt in the event it is empty or incomplete -when the project is opened.

-

✓ The user can now add their own syntax and GUI theme files in the app folder in their -user area on the host operating system. Where the custom files must be added is described in the -documentation.

-

✓ A Windows installer is yet again provided for novelWriter. If you have novelWriter -installed using another method, make sure you uninstall it properly first as the two methods are -not compatible.

-

✓ Release versions for Ubuntu 21.04 have been dropped, and added for the upcoming Ubuntu -22.04.

-

✓ Most translations have been updated. A Dutch translation is in the works.

+

This is a beta release of the next release version, and is intended for testing purposes. Please +be careful when using this version on live writing projects, and make sure you take frequent +backups.

+

Please check the changelog for an overview of changes. The full release notes will be added to +the final release.

See also the Releases page.

-

Patch Notes

- -

Patch 1.6.1 – 16 March 2022

- -

This is a bugfix and patch release that fixes two recursion/loop issues. One would potentially -cause a crash if the window was resized rapidly, and one would cause a hang with certain search -parameters in the editor's search box. The Latin American Spanish translation has also been -updated.

- -

Patch 1.6.2 – 20 March 2022

- -

This is a bugfix release that fixes a couple of minor issues. Projects containing one or more -empty documents would trigger a rebuild of the index each time the project was opened. This has now -been fixed. Another fix resolves an error message being written to the console logging output when -a new document was created. Both errors were harmless.

- diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 21bd7776..e53d6569 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1328 + 1329 220 - 67092 + 67104 False From 301d81f9877506ff7fa6deba6f307f851c8257ed Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 17 May 2022 20:22:16 +0200 Subject: [PATCH 076/179] Update various translation files --- i18n/nw_base.ts | 1213 +++++++++---------- i18n/nw_en_US.ts | 1221 ++++++++++---------- i18n/nw_nb_NO.ts | 1221 ++++++++++---------- novelwriter/assets/i18n/project_nl_NL.json | 200 ++-- 4 files changed, 1941 insertions(+), 1914 deletions(-) diff --git a/i18n/nw_base.ts b/i18n/nw_base.ts index 4eaa8252..8d7e2940 100644 --- a/i18n/nw_base.ts +++ b/i18n/nw_base.ts @@ -77,259 +77,259 @@ Constant - - - + + + None - + Novel - - + + Plot - - + + Characters - - + + Locations - - + + Timeline - - + + Objects - - + + Entities - - + + Custom - + Archive - + Trash - - + + Novel Document - - + + Project Note - + Root Folder - + Folder - + Novel Title Page - + Novel Chapter - + Novel Scene - + Tag - + Point of View - - + + Focus - + Title - + Level - + Document - + Line - + Chars - + Words - + Pars - + POV - + Synopsis - + Straight single quotation mark - + Straight double quotation mark - + Left single quotation mark - + Right single quotation mark - + Single low-9 quotation mark - + Single high-reversed-9 quotation mark - + Left double quotation mark - + Right double quotation mark - + Double low-9 quotation mark - + Double high-reversed-9 quotation mark - + Double low-reversed-9 quotation mark - + Single left-pointing angle quotation mark - + Single right-pointing angle quotation mark - + Double left-pointing angle quotation mark - + Double right-pointing angle quotation mark - + Left corner bracket - + Right corner bracket - + Left white corner bracket - + Right white corner bracket @@ -353,9 +353,9 @@ - - - + + + Licence @@ -411,31 +411,31 @@ - + Theme: {0} - - - - - Author - - + Author + + + + + + Credit - + Icons: {0} - + Syntax: {0} @@ -718,62 +718,62 @@ - + Open Document - + Flat Open Document - + Plain HTML - + novelWriter Markdown - + Standard Markdown - + GitHub Markdown - + JSON + novelWriter HTML - + JSON + novelWriter Markdown - + PDF - + Save Document As - + {0} file successfully written to: - + Failed to write {0} file. {1} @@ -781,17 +781,17 @@ GuiBuildNovelDocView - + This area will show the content of the document to be exported or printed. Press the "Build Preview" button to generate content. - + Unknown - + Build Time: @@ -799,32 +799,32 @@ GuiDocEditFooter - + Status - + Line: {0} ({1}) - + Words: {0} ({1}) - + Document size is {0} bytes - + Words: {0} selected - + Character count: {0} @@ -832,22 +832,22 @@ GuiDocEditHeader - + Edit document meta - + Search document - + Toggle Focus Mode - + Close the document @@ -855,58 +855,58 @@ GuiDocEditSearch - - + + Search - + Replace - + Case Sensitive - + Whole Words Only - + RegEx Mode - + Loop Search - + Search Next File - + Preserve Case - + Close Search - + Find in current document - + Find and replace in current document @@ -914,117 +914,117 @@ GuiDocEditor - + The document you are trying to open is too big. The document size is {0} MB. The maximum size allowed is {1} MB. - + Opened Document: {0} - + The text you are trying to add is too big. The text size is {0} MB. The maximum size allowed is {1} MB. - + File Changed on Disk - + This document has been changed outside of novelWriter while it was open. Overwrite the file on disk? - + Could not save document. - + Saved Document: {0} - + Spell checking requires the package PyEnchant. It does not appear to be installed. - + Spell check complete - + File Location - + The currently open file is saved in: - + The document has grown too big and you cannot add more text to it. The maximum size of a single novelWriter document is {0} MB. - + Follow Tag - + Cut - + Copy - + Paste - + Select All - + Select Word - + Select Paragraph - + Spelling Suggestion(s) - + No Suggestions - + Add Word to Dictionary - + Please select some text before calling replace quotes. @@ -1067,12 +1067,12 @@ - + Could not save document. - + Element selected in the project tree must be a folder. @@ -1080,83 +1080,78 @@ GuiDocSplit - - + + Split Document - + Document Headers - + Select the maximum level to split into files. - + Split on Header Level 1 (Title) - + Split up to Header Level 2 (Chapter) - + Split up to Header Level 3 (Scene) - + Split up to Header Level 4 (Section) - + No source document selected. Nothing to do. - + Could not parse source document. - + Failed to open document file. - + No headers found. Nothing to do. - - Cannot add new folder for the document split. Maximum folder depth has been reached. Please move the file to another level in the project tree. - - - - + The document will be split into {0} file(s) in a new folder. The original document will remain intact. - + Continue with the splitting process? - + Could not save document. - + Element selected in the project tree must be a file. @@ -1164,42 +1159,42 @@ GuiDocViewFooter - + Show/hide the references panel - + Activate to freeze the content of the references panel when changing document - + Show comments - + Show synopsis comments - + References - + Sticky - + Comments - + Synopsis @@ -1207,22 +1202,22 @@ GuiDocViewHeader - + Go backward - + Go forward - + Reload the document - + Close the document @@ -1283,17 +1278,17 @@ - + Characters - + Words - + Paragraphs @@ -1306,237 +1301,230 @@ - + Include when building project - + Label - + Status - + Layout + + GuiLipsum + + + Insert Placeholder Text + + + + + Insert Lorem Ipsum Text + + + + + Number of paragraphs + + + + + Randomise order + + + + + Insert + + + GuiMain - - Project - - - - - Novel - - - - - Project Details - - - - - Writing Statistics - - - - - Project Settings - - - - - Editor - - - - - Outline - - - - + You are running an untested development version of novelWriter. Please be careful when working on a live project and make sure you take regular backups. - + novelWriter is ready ... - + Cannot create a new project when another project is open. - + A project already exists in that location. Please choose another folder. - + New project created ... - + Close Project - + Close the current project? - - + + Changes are saved automatically. - + Backup Project - + Backup the current project? - + The project was locked by the computer '{0}' ({1} {2}), last active on {3}. - + Project Locked - + The project is already open by another instance of novelWriter, and is therefore locked. Override lock and continue anyway? - + Note: If the program or the computer previously crashed, the lock can safely be overridden. However, overriding it is not recommended if the project is open in another instance of novelWriter. Doing so may corrupt the project. - + The project index is outdated or broken. Rebuilding index. - + Text files ({0}) - + Markdown files ({0}) - + novelWriter files ({0}) - + All files ({0}) - + Import File - + Could not read file. The file must be an existing text file. - + Please open a document to import the text file into. - + Import Document - + Importing the file will overwrite the current content of the document. Do you want to proceed? - - + + Indexing: '{0}' - + Unknown item - + Indexing completed in {0} ms - + The project index has been successfully rebuilt. - + Information - + Warning - + Error - + This is a bug! - + Internal Error - + Exit - + Do you want to exit novelWriter? @@ -1544,677 +1532,682 @@ GuiMainMenu - + &Project - + New Project - + Open Project - + Save Project - + Close Project - + Project Settings - + Project Details - + Create Root Folder - + Novel Root - + Plot Root - + Character Root - + Location Root - + Timeline Root - + Object Root - + Entity Root - + Custom Root - + Archive Root - + Create Folder - + Edit Item - + Delete Item - + Move Item Up - + Move Item Down - + Undo Last Move - + Empty Trash - + Exit - + &Document - + New Document - + Open Document - + Save Document - + Close Document - + View Document - + Close Document View - + Show File Details - + Import Text from File - + Merge Folder to Document - + Split Document to Folder - + &Edit - + Undo - + Redo - + Cut - + Copy - + Paste - + Select All - + Select Paragraph - + &View - + Go to Project Tree - + Go to Document Editor - + Go to Document Viewer - + Go to Outline - + Navigate Backward - + Navigate Forward - + Focus Mode - + Full Screen Mode - + &Insert - + Dashes - + Short Dash - + Long Dash - + Horizontal Bar - + Figure Dash - + Quote Marks - + Left Single Quote - + Right Single Quote - + Left Double Quote - + Right Double Quote - + Alternative Apostrophe - + General Punctuation - + Ellipsis - + Prime - + Double Prime - + White Spaces - + Non-Breaking Space - + Thin Space - + Thin Non-Breaking Space - + Other Symbols - + List Bullet - + Hyphen Bullet - + Flower Mark - + Per Mille - + Degree Symbol - + Minus Sign - + Times Sign - + Division Sign - + Tags and References - + Page Break and Space - + Page Break - + Vertical Space (Single) - + Vertical Space (Multi) - + + Placeholder Text + + + + &Format - + Emphasis - + Strong Emphasis - + Strikethrough - + Wrap Double Quotes - + Wrap Single Quotes - + Header 1 (Partition) - + Header 2 (Chapter) - + Header 3 (Scene) - + Header 4 (Section) - + Novel Title - + Unnumbered Chapter - + Align Left - + Align Centre - + Align Right - + Indent Left - + Indent Right - + Toggle Comment - + Remove Block Format - + Convert Single Quotes - + Convert Double Quotes - + Remove In-Paragraph Breaks - + &Search - + Find - + Replace - + Find Next - + Find Previous - + Replace Next - + &Tools - + Check Spelling - + Re-Run Spell Check - + Project Word List - + Rebuild Index - + Rebuild Outline - + Auto-Update Outline - + Backup Project - + Build Novel Project - + Writing Statistics - + Preferences - + &Help - + About novelWriter - + About Qt5 - + User Manual (Online) - + User Manual (PDF) - + Report an Issue (GitHub) - + Ask a Question (GitHub) - + The novelWriter Website - + Check for New Release @@ -2294,65 +2287,65 @@ GuiOutlineDetails - - - - + + + + Title - + Chapter - + Scene - + Section - + Document - + Status - + Characters - + Words - + Paragraphs - + Synopsis - + Title Details - + Reference Tags @@ -3046,7 +3039,7 @@ - Highlight multiple spaces + Highlight multiple or trailing spaces @@ -3187,58 +3180,58 @@ GuiProjectEditMain - + Project Settings - + Working title - + Should be set only once. - + Novel title - + Change whenever you want! - + Author(s) - + One name per line. - + Default - + Spell check language - - + + Overrides main preferences. - + No backup on close @@ -3246,27 +3239,27 @@ GuiProjectEditReplace - + Text Replace List for Preview and Export - + Keyword - + Replace With - + Select item to edit - + Save @@ -3274,67 +3267,67 @@ GuiProjectEditStatus - + Novel File Status Levels - + Note File Importance Levels - + Label - + Usage - + Select item to edit - + Colour - + Save - + Select Colour - + New Item - + Cannot delete a status item that is in use. - + Not in use - + Used once - + Used by {0} items @@ -3406,27 +3399,27 @@ GuiProjectSettings - + Project Settings - + Settings - + Status - + Importance - + Auto-Replace @@ -3464,100 +3457,78 @@ - - Please select a valid location in the tree to add the document. - - - - - Please select a valid location in the tree to add the folder. - - - - - + Did not find anywhere to add the file or folder! - + Cannot add new files or folders to the Trash folder. - - New File + + New Document - - Cannot add new folder to this item. Maximum folder depth has been reached. + + New Note - + New Folder - + There is currently no Trash folder in this project. - + The Trash folder is already empty. - + Empty Trash - + Permanently delete {0} file(s) from Trash? - - - Delete File - - - - - Permanently delete file '{0}'? - - - - - Could not delete document file. - - - - - Move file '{0}' to Trash? - - - - - Cannot delete folder. It is not empty. Recursive deletion is not supported. Please delete the content first. - - - - + Cannot delete root folder. It is not empty. Recursive deletion is not supported. Please delete the content first. - - - The item cannot be moved to that location. + + + Delete - + + Permanently delete '{0}'? + + + + + Move '{0}' to Trash? + + + + + Could not delete document file. + + + + There is nowhere to add item with name '{0}'. @@ -3565,52 +3536,52 @@ GuiProjectTreeMenu - + Edit Project Item - + Open Document - + View Document - + Toggle Included Flag - + New File - + New Folder - + Delete Item - + Empty Trash - + Move Item Up - + Move Item Down @@ -3649,6 +3620,39 @@ + + GuiViewsBar + + + Project + + + + + Novel + + + + + Outline + + + + + Details + + + + + Stats + + + + + Settings + + + GuiWordList @@ -3816,7 +3820,7 @@ - + Failed to read session log file. @@ -3824,314 +3828,309 @@ NWProject - - Duplicate root item detected. - - - - - + + New - + Note - + Draft - + Finished - + Minor - + Major - + Main - + New Project - + By - - + + Novel - + Plot - + Characters - + World - - + + Title Page - - - + + + New Chapter - - + + New Scene - + Chapter {0} - - + + Scene {0} - + File not found: {0} - - + + Failed to parse project xml. - + Attempting to open backup project file instead. - - + + Unknown - + Project file does not appear to be a novelWriterXML file. - + Unknown or unsupported novelWriter project file format. The project cannot be opened by this version of novelWriter. The file was saved with novelWriter version {0}. - + File Version - + The file format of your project is about to be updated. If you proceed, older versions of novelWriter will no longer be able to open this project. Continue? - + Version Conflict - + This project was saved by a newer version of novelWriter, version {0}. This is version {1}. If you continue to open the project, some attributes and settings may not be preserved, but the overall project should be fine. Continue opening the project? - + Opened Project: {0} - + Project path not set, cannot save project. - - + + Failed to save project. - + Saved Project: {0} - + Backing up project ... - + Cannot backup project because no valid backup path is set. Please set a valid backup location in Preferences. - + Cannot backup project because no project name is set. Please set a Working Title in Project Settings. - + Could not create backup folder. - + Cannot backup project because the backup path is within the project folder to be backed up. Please choose a different backup path in Preferences. - + Backup from {0} - + Backup archive file written to: {0} - + Could not write backup archive. - + Project backed up to '{0}' - - + + Failed to create a new example project. - + Failed to create a new example project. Could not find the necessary files. They seem to be missing from this installation. - + Could not create new project folder. - + New project folder is not empty. Each project requires a dedicated project folder. - + You must set a valid backup path in Preferences to use the automatic project backup feature. - + You must set a valid project name in Project Settings to use the automatic project backup feature. - + and - + Could not create folder. - + Found {0} orphaned file(s) in project folder. - + Recovered - + [{0}] {1} - + Recovered File {0} - + One or more orphaned files could not be added back into the project. Make sure at least a Novel root folder exists. - + Not a folder: {0} - + Could not move: {0} - - + + Could not delete: {0} - + Could not make folder: {0} - + Could not move item {0} to {1}. @@ -4139,47 +4138,47 @@ ProjWizardCustomPage - + Custom Project Options - + Select which additional root folders to make, and how to populate the Novel folder. If you don't want to add chapters or scenes, set the values to 0. You can add scenes without chapters. - + Additional Root Folders - - - - - - + + + + + + {0} folder - + Populate Novel Folder - + Add chapters - + Scenes (per chapter) - + Add chapter folders @@ -4187,27 +4186,27 @@ ProjWizardFinalPage - + Finished - + All done. - + Press '{0}' to create the new project. - + Done - + Finish @@ -4215,7 +4214,7 @@ ProjWizardFolderPage - + Select Project Folder @@ -4231,10 +4230,20 @@ - + Project Path + + + Error: A project folder cannot be created using this path. + + + + + Error: The selected path already exists. + + ProjWizardIntroPage @@ -4287,27 +4296,27 @@ ProjWizardPopulatePage - + Populate Project - + Choose how to pre-fill the project. Either with a minimal set of starter items, an example project explaining and showing many of the features, or show further custom options on the next page. - + Fill the project with a minimal set of items - + Fill the project with example files - + Show detailed options for filling the project @@ -4503,17 +4512,17 @@ Tokenizer - + Synopsis - + Document '{0}' is too big ({1} MB). Skipping. - + ERROR diff --git a/i18n/nw_en_US.ts b/i18n/nw_en_US.ts index 3fb60626..75aee812 100644 --- a/i18n/nw_en_US.ts +++ b/i18n/nw_en_US.ts @@ -77,259 +77,259 @@ Constant - - - + + + None None - + Novel Novel - - + + Plot Plot - - + + Characters Characters - - + + Locations Locations - - + + Timeline Timeline - - + + Objects Objects - - + + Entities Entities - - + + Custom Custom - + Archive Archive - + Trash Trash - - + + Novel Document Novel Document - - + + Project Note Project Note - + Root Folder Root Folder - + Folder Folder - + Novel Title Page Novel Title Page - + Novel Chapter Novel Chapter - + Novel Scene Novel Scene - + Tag Tag - + Point of View Point of View - - + + Focus Focus - + Title Title - + Level Level - + Document Document - + Line Line - + Chars Chars - + Words Words - + Pars Pars - + POV POV - + Synopsis Synopsis - + Straight single quotation mark Straight single quotation mark - + Straight double quotation mark Straight double quotation mark - + Left single quotation mark Left single quotation mark - + Right single quotation mark Right single quotation mark - + Single low-9 quotation mark Single low-9 quotation mark - + Single high-reversed-9 quotation mark Single high-reversed-9 quotation mark - + Left double quotation mark Left double quotation mark - + Right double quotation mark Right double quotation mark - + Double low-9 quotation mark Double low-9 quotation mark - + Double high-reversed-9 quotation mark Double high-reversed-9 quotation mark - + Double low-reversed-9 quotation mark Double low-reversed-9 quotation mark - + Single left-pointing angle quotation mark Single left-pointing angle quotation mark - + Single right-pointing angle quotation mark Single right-pointing angle quotation mark - + Double left-pointing angle quotation mark Double left-pointing angle quotation mark - + Double right-pointing angle quotation mark Double right-pointing angle quotation mark - + Left corner bracket Left corner bracket - + Right corner bracket Right corner bracket - + Left white corner bracket Left white corner bracket - + Right white corner bracket Right white corner bracket @@ -353,9 +353,9 @@ Release - - - + + + Licence License @@ -411,31 +411,31 @@ Translations - + Theme: {0} Theme: {0} - - - - - Author - Author - + Author + Author + + + + + Credit Credit - + Icons: {0} Icons: {0} - + Syntax: {0} Syntax: {0} @@ -718,62 +718,62 @@ There were problems when building the project: - + Open Document Open Document - + Flat Open Document Flat Open Document - + Plain HTML Plain HTML - + novelWriter Markdown novelWriter Markdown - + Standard Markdown Standard Markdown - + GitHub Markdown GitHub Markdown - + JSON + novelWriter HTML JSON + novelWriter HTML - + JSON + novelWriter Markdown JSON + novelWriter Markdown - + PDF PDF - + Save Document As Save Document As - + {0} file successfully written to: {0} file successfully written to: - + Failed to write {0} file. {1} Failed to write {0} file. {1} @@ -781,17 +781,17 @@ GuiBuildNovelDocView - + This area will show the content of the document to be exported or printed. Press the "Build Preview" button to generate content. This area will show the content of the document to be exported or printed. Press the "Build Preview" button to generate content. - + Unknown Unknown - + Build Time: Build Time: @@ -799,32 +799,32 @@ GuiDocEditFooter - + Status Status - + Line: {0} ({1}) Line: {0} ({1}) - + Words: {0} ({1}) Words: {0} ({1}) - + Document size is {0} bytes Document size is {0} bytes - + Words: {0} selected Words: {0} selected - + Character count: {0} Character count: {0} @@ -832,22 +832,22 @@ GuiDocEditHeader - + Edit document meta Edit document meta - + Search document Search document - + Toggle Focus Mode Toggle Focus Mode - + Close the document Close the document @@ -855,58 +855,58 @@ GuiDocEditSearch - - + + Search Search - + Replace Replace - + Case Sensitive Case Sensitive - + Whole Words Only Whole Words Only - + RegEx Mode RegEx Mode - + Loop Search Loop Search - + Search Next File Search Next File - + Preserve Case Preserve Case - + Close Search Close Search - + Find in current document Find in current document - + Find and replace in current document Find and replace in current document @@ -914,117 +914,117 @@ GuiDocEditor - + The document you are trying to open is too big. The document size is {0} MB. The maximum size allowed is {1} MB. The document you are trying to open is too big. The document size is {0} MB. The maximum size allowed is {1} MB. - + Opened Document: {0} Opened Document: {0} - + The text you are trying to add is too big. The text size is {0} MB. The maximum size allowed is {1} MB. The text you are trying to add is too big. The text size is {0} MB. The maximum size allowed is {1} MB. - + File Changed on Disk File Changed on Disk - + This document has been changed outside of novelWriter while it was open. Overwrite the file on disk? This document has been changed outside of novelWriter while it was open. Overwrite the file on disk? - + Could not save document. Could not save document. - + Saved Document: {0} Saved Document: {0} - + Spell checking requires the package PyEnchant. It does not appear to be installed. Spell checking requires the package PyEnchant. It does not appear to be installed. - + Spell check complete Spell check complete - + File Location File Location - + The currently open file is saved in: The currently open file is saved in: - + The document has grown too big and you cannot add more text to it. The maximum size of a single novelWriter document is {0} MB. The document has grown too big and you cannot add more text to it. The maximum size of a single novelWriter document is {0} MB. - + Follow Tag Follow Tag - + Cut Cut - + Copy Copy - + Paste Paste - + Select All Select All - + Select Word Select Word - + Select Paragraph Select Paragraph - + Spelling Suggestion(s) Spelling Suggestion(s) - + No Suggestions No Suggestions - + Add Word to Dictionary Add Word to Dictionary - + Please select some text before calling replace quotes. Please select some text before calling replace quotes. @@ -1067,12 +1067,12 @@ Internal error. - + Could not save document. Could not save document. - + Element selected in the project tree must be a folder. Element selected in the project tree must be a folder. @@ -1080,83 +1080,78 @@ GuiDocSplit - - + + Split Document Split Document - + Document Headers Document Headers - + Select the maximum level to split into files. Select the maximum level to split into files. - + Split on Header Level 1 (Title) Split on Header Level 1 (Title) - + Split up to Header Level 2 (Chapter) Split up to Header Level 2 (Chapter) - + Split up to Header Level 3 (Scene) Split up to Header Level 3 (Scene) - + Split up to Header Level 4 (Section) Split up to Header Level 4 (Section) - + No source document selected. Nothing to do. No source document selected. Nothing to do. - + Could not parse source document. Could not parse source document. - + Failed to open document file. Failed to open document file. - + No headers found. Nothing to do. No headers found. Nothing to do. - - Cannot add new folder for the document split. Maximum folder depth has been reached. Please move the file to another level in the project tree. - Cannot add new folder for the document split. Maximum folder depth has been reached. Please move the file to another level in the project tree. - - - + The document will be split into {0} file(s) in a new folder. The original document will remain intact. The document will be split into {0} file(s) in a new folder. The original document will remain intact. - + Continue with the splitting process? Continue with the splitting process? - + Could not save document. Could not save document. - + Element selected in the project tree must be a file. Element selected in the project tree must be a file. @@ -1164,42 +1159,42 @@ GuiDocViewFooter - + Show/hide the references panel Show/hide the references panel - + Activate to freeze the content of the references panel when changing document Activate to freeze the content of the references panel when changing document - + Show comments Show comments - + Show synopsis comments Show synopsis comments - + References References - + Sticky Sticky - + Comments Comments - + Synopsis Synopsis @@ -1207,22 +1202,22 @@ GuiDocViewHeader - + Go backward Go backward - + Go forward Go forward - + Reload the document Reload the document - + Close the document Close the document @@ -1283,17 +1278,17 @@ Usage - + Characters Characters - + Words Words - + Paragraphs Paragraphs @@ -1306,237 +1301,230 @@ Item Settings - + Include when building project Include when building project - + Label Label - + Status Status - + Layout Layout + + GuiLipsum + + + Insert Placeholder Text + Insert Placeholder Text + + + + Insert Lorem Ipsum Text + Insert Lorem Ipsum Text + + + + Number of paragraphs + Number of paragraphs + + + + Randomise order + Randomize order + + + + Insert + Insert + + GuiMain - - Project - Project - - - - Novel - Novel - - - - Project Details - Project Details - - - - Writing Statistics - Writing Statistics - - - - Project Settings - Project Settings - - - - Editor - Editor - - - - Outline - Outline - - - + You are running an untested development version of novelWriter. Please be careful when working on a live project and make sure you take regular backups. You are running an untested development version of novelWriter. Please be careful when working on a live project and make sure you take regular backups. - + novelWriter is ready ... novelWriter is ready ... - + Cannot create a new project when another project is open. Cannot create a new project when another project is open. - + A project already exists in that location. Please choose another folder. A project already exists in that location. Please choose another folder. - + New project created ... New project created ... - + Close Project Close Project - + Close the current project? Close the current project? - - + + Changes are saved automatically. Changes are saved automatically. - + Backup Project Backup Project - + Backup the current project? Backup the current project? - + The project was locked by the computer '{0}' ({1} {2}), last active on {3}. The project was locked by the computer '{0}' ({1} {2}), last active on {3}. - + Project Locked Project Locked - + The project is already open by another instance of novelWriter, and is therefore locked. Override lock and continue anyway? The project is already open by another instance of novelWriter, and is therefore locked. Override lock and continue anyway? - + Note: If the program or the computer previously crashed, the lock can safely be overridden. However, overriding it is not recommended if the project is open in another instance of novelWriter. Doing so may corrupt the project. Note: If the program or the computer previously crashed, the lock can safely be overridden. However, overriding it is not recommended if the project is open in another instance of novelWriter. Doing so may corrupt the project. - + The project index is outdated or broken. Rebuilding index. The project index is outdated or broken. Rebuilding index. - + Text files ({0}) Text files ({0}) - + Markdown files ({0}) Markdown files ({0}) - + novelWriter files ({0}) novelWriter files ({0}) - + All files ({0}) All files ({0}) - + Import File Import File - + Could not read file. The file must be an existing text file. Could not read file. The file must be an existing text file. - + Please open a document to import the text file into. Please open a document to import the text file into. - + Import Document Import Document - + Importing the file will overwrite the current content of the document. Do you want to proceed? Importing the file will overwrite the current content of the document. Do you want to proceed? - - + + Indexing: '{0}' Indexing: '{0}' - + Unknown item Unknown item - + Indexing completed in {0} ms Indexing completed in {0} ms - + The project index has been successfully rebuilt. The project index has been successfully rebuilt. - + Information Information - + Warning Warning - + Error Error - + This is a bug! This is a bug! - + Internal Error Internal Error - + Exit Exit - + Do you want to exit novelWriter? Do you want to exit novelWriter? @@ -1544,677 +1532,682 @@ GuiMainMenu - + &Project &Project - + New Project New Project - + Open Project Open Project - + Save Project Save Project - + Close Project Close Project - + Project Settings Project Settings - + Project Details Project Details - + Create Root Folder Create Root Folder - + Novel Root Novel Root - + Plot Root Plot Root - + Character Root Character Root - + Location Root Location Root - + Timeline Root Timeline Root - + Object Root Object Root - + Entity Root Entity Root - + Custom Root Custom Root - + Archive Root Archive Root - + Create Folder Create Folder - + Edit Item Edit Item - + Delete Item Delete Item - + Move Item Up Move Item Up - + Move Item Down Move Item Down - + Undo Last Move Undo Last Move - + Empty Trash Empty Trash - + Exit Exit - + &Document &Document - + New Document New Document - + Open Document Open Document - + Save Document Save Document - + Close Document Close Document - + View Document View Document - + Close Document View Close Document View - + Show File Details Show File Details - + Import Text from File Import Text from File - + Merge Folder to Document Merge Folder to Document - + Split Document to Folder Split Document to Folder - + &Edit &Edit - + Undo Undo - + Redo Redo - + Cut Cut - + Copy Copy - + Paste Paste - + Select All Select All - + Select Paragraph Select Paragraph - + &View &View - + Go to Project Tree Go to Project Tree - + Go to Document Editor Go to Document Editor - + Go to Document Viewer Go to Document Viewer - + Go to Outline Go to Outline - + Navigate Backward Navigate Backward - + Navigate Forward Navigate Forward - + Focus Mode Focus Mode - + Full Screen Mode Full Screen Mode - + &Insert &Insert - + Dashes Dashes - + Short Dash Short Dash - + Long Dash Long Dash - + Horizontal Bar Horizontal Bar - + Figure Dash Figure Dash - + Quote Marks Quote Marks - + Left Single Quote Left Single Quote - + Right Single Quote Right Single Quote - + Left Double Quote Left Double Quote - + Right Double Quote Right Double Quote - + Alternative Apostrophe Alternative Apostrophe - + General Punctuation General Punctuation - + Ellipsis Ellipsis - + Prime Prime - + Double Prime Double Prime - + White Spaces White Spaces - + Non-Breaking Space Non-Breaking Space - + Thin Space Thin Space - + Thin Non-Breaking Space Thin Non-Breaking Space - + Other Symbols Other Symbols - + List Bullet List Bullet - + Hyphen Bullet Hyphen Bullet - + Flower Mark Flower Mark - + Per Mille Per Mille - + Degree Symbol Degree Symbol - + Minus Sign Minus Sign - + Times Sign Times Sign - + Division Sign Division Sign - + Tags and References Tags and References - + Page Break and Space Page Break and Space - + Page Break Page Break - + Vertical Space (Single) Vertical Space (Single) - + Vertical Space (Multi) Vertical Space (Multi) - + + Placeholder Text + Placeholder Text + + + &Format &Format - + Emphasis Emphasis - + Strong Emphasis Strong Emphasis - + Strikethrough Strikethrough - + Wrap Double Quotes Wrap Double Quotes - + Wrap Single Quotes Wrap Single Quotes - + Header 1 (Partition) Header 1 (Partition) - + Header 2 (Chapter) Header 2 (Chapter) - + Header 3 (Scene) Header 3 (Scene) - + Header 4 (Section) Header 4 (Section) - + Novel Title Novel Title - + Unnumbered Chapter Unnumbered Chapter - + Align Left Align Left - + Align Centre Align Center - + Align Right Align Right - + Indent Left Indent Left - + Indent Right Indent Right - + Toggle Comment Toggle Comment - + Remove Block Format Remove Block Format - + Convert Single Quotes Convert Single Quotes - + Convert Double Quotes Convert Double Quotes - + Remove In-Paragraph Breaks Remove In-Paragraph Breaks - + &Search &Search - + Find Find - + Replace Replace - + Find Next Find Next - + Find Previous Find Previous - + Replace Next Replace Next - + &Tools &Tools - + Check Spelling Check Spelling - + Re-Run Spell Check Re-Run Spell Check - + Project Word List Project Word List - + Rebuild Index Rebuild Index - + Rebuild Outline Rebuild Outline - + Auto-Update Outline Auto-Update Outline - + Backup Project Backup Project - + Build Novel Project Build Novel Project - + Writing Statistics Writing Statistics - + Preferences Preferences - + &Help &Help - + About novelWriter About novelWriter - + About Qt5 About Qt5 - + User Manual (Online) User Manual (Online) - + User Manual (PDF) User Manual (PDF) - + Report an Issue (GitHub) Report an Issue (GitHub) - + Ask a Question (GitHub) Ask a Question (GitHub) - + The novelWriter Website The novelWriter Website - + Check for New Release Check for New Release @@ -2294,65 +2287,65 @@ GuiOutlineDetails - - - - + + + + Title Title - + Chapter Chapter - + Scene Scene - + Section Section - + Document Document - + Status Status - + Characters Characters - + Words Words - + Paragraphs Paragraphs - + Synopsis Synopsis - + Title Details Title Details - + Reference Tags Reference Tags @@ -3046,8 +3039,8 @@ - Highlight multiple spaces - Highlight multiple spaces + Highlight multiple or trailing spaces + Highlight multiple or trailing spaces @@ -3187,58 +3180,58 @@ GuiProjectEditMain - + Project Settings Project Settings - + Working title Working title - + Should be set only once. Should be set only once. - + Novel title Novel title - + Change whenever you want! Change whenever you want! - + Author(s) Author(s) - + One name per line. One name per line. - + Default Default - + Spell check language Spell check language - - + + Overrides main preferences. Overrides main preferences. - + No backup on close No backup on close @@ -3246,27 +3239,27 @@ GuiProjectEditReplace - + Text Replace List for Preview and Export Text Replace List for Preview and Export - + Keyword Keyword - + Replace With Replace With - + Select item to edit Select item to edit - + Save Save @@ -3274,67 +3267,67 @@ GuiProjectEditStatus - + Novel File Status Levels Novel File Status Levels - + Note File Importance Levels Note File Importance Levels - + Label Label - + Usage Usage - + Select item to edit Select item to edit - + Colour Color - + Save Save - + Select Colour Select Color - + New Item New Item - + Cannot delete a status item that is in use. Cannot delete a status item that is in use. - + Not in use Not in use - + Used once Used once - + Used by {0} items Used by {0} items @@ -3406,27 +3399,27 @@ GuiProjectSettings - + Project Settings Project Settings - + Settings Settings - + Status Status - + Importance Importance - + Auto-Replace Auto-Replace @@ -3464,100 +3457,78 @@ Item status - - Please select a valid location in the tree to add the document. - Please select a valid location in the tree to add the document. - - - - Please select a valid location in the tree to add the folder. - Please select a valid location in the tree to add the folder. - - - - + Did not find anywhere to add the file or folder! Did not find anywhere to add the file or folder! - + Cannot add new files or folders to the Trash folder. Cannot add new files or folders to the Trash folder. - - New File - New File + + New Document + New Document - - Cannot add new folder to this item. Maximum folder depth has been reached. - Cannot add new folder to this item. Maximum folder depth has been reached. + + New Note + New Note - + New Folder New Folder - + There is currently no Trash folder in this project. There is currently no Trash folder in this project. - + The Trash folder is already empty. The Trash folder is already empty. - + Empty Trash Empty Trash - + Permanently delete {0} file(s) from Trash? Permanently delete {0} file(s) from Trash? - - - Delete File - Delete File - - - - Permanently delete file '{0}'? - Permanently delete file '{0}'? - - - - Could not delete document file. - Could not delete document file. - - - - Move file '{0}' to Trash? - Move file '{0}' to Trash? - - - - Cannot delete folder. It is not empty. Recursive deletion is not supported. Please delete the content first. - Cannot delete folder. It is not empty. Recursive deletion is not supported. Please delete the content first. - - - + Cannot delete root folder. It is not empty. Recursive deletion is not supported. Please delete the content first. Cannot delete root folder. It is not empty. Recursive deletion is not supported. Please delete the content first. - - - The item cannot be moved to that location. - The item cannot be moved to that location. + + + Delete + Delete - + + Permanently delete '{0}'? + Permanently delete '{0}'? + + + + Move '{0}' to Trash? + Move '{0}' to Trash? + + + + Could not delete document file. + Could not delete document file. + + + There is nowhere to add item with name '{0}'. There is nowhere to add item with name '{0}'. @@ -3565,52 +3536,52 @@ GuiProjectTreeMenu - + Edit Project Item Edit Project Item - + Open Document Open Document - + View Document View Document - + Toggle Included Flag Toggle Included Flag - + New File New File - + New Folder New Folder - + Delete Item Delete Item - + Empty Trash Empty Trash - + Move Item Up Move Item Up - + Move Item Down Move Item Down @@ -3649,6 +3620,39 @@ Download: {0} + + GuiViewsBar + + + Project + Project + + + + Novel + Novel + + + + Outline + Outline + + + + Details + Details + + + + Stats + Stats + + + + Settings + Settings + + GuiWordList @@ -3816,7 +3820,7 @@ Failed to write {0} file. - + Failed to read session log file. Failed to read session log file. @@ -3824,314 +3828,309 @@ NWProject - - Duplicate root item detected. - Duplicate root item detected. - - - - + + New New - + Note Note - + Draft Draft - + Finished Finished - + Minor Minor - + Major Major - + Main Main - + New Project New Project - + By By - - + + Novel Novel - + Plot Plot - + Characters Characters - + World World - - + + Title Page Title Page - - - + + + New Chapter New Chapter - - + + New Scene New Scene - + Chapter {0} Chapter {0} - - + + Scene {0} Scene {0} - + File not found: {0} File not found: {0} - - + + Failed to parse project xml. Failed to parse project xml. - + Attempting to open backup project file instead. Attempting to open backup project file instead. - - + + Unknown Unknown - + Project file does not appear to be a novelWriterXML file. Project file does not appear to be a novelWriterXML file. - + Unknown or unsupported novelWriter project file format. The project cannot be opened by this version of novelWriter. The file was saved with novelWriter version {0}. Unknown or unsupported novelWriter project file format. The project cannot be opened by this version of novelWriter. The file was saved with novelWriter version {0}. - + File Version File Version - + The file format of your project is about to be updated. If you proceed, older versions of novelWriter will no longer be able to open this project. Continue? The file format of your project is about to be updated. If you proceed, older versions of novelWriter will no longer be able to open this project. Continue? - + Version Conflict Version Conflict - + This project was saved by a newer version of novelWriter, version {0}. This is version {1}. If you continue to open the project, some attributes and settings may not be preserved, but the overall project should be fine. Continue opening the project? This project was saved by a newer version of novelWriter, version {0}. This is version {1}. If you continue to open the project, some attributes and settings may not be preserved, but the overall project should be fine. Continue opening the project? - + Opened Project: {0} Opened Project: {0} - + Project path not set, cannot save project. Project path not set, cannot save project. - - + + Failed to save project. Failed to save project. - + Saved Project: {0} Saved Project: {0} - + Backing up project ... Backing up project ... - + Cannot backup project because no valid backup path is set. Please set a valid backup location in Preferences. Cannot backup project because no valid backup path is set. Please set a valid backup location in Preferences. - + Cannot backup project because no project name is set. Please set a Working Title in Project Settings. Cannot backup project because no project name is set. Please set a Working Title in Project Settings. - + Could not create backup folder. Could not create backup folder. - + Cannot backup project because the backup path is within the project folder to be backed up. Please choose a different backup path in Preferences. Cannot backup project because the backup path is within the project folder to be backed up. Please choose a different backup path in Preferences. - + Backup from {0} Backup from {0} - + Backup archive file written to: {0} Backup archive file written to: {0} - + Could not write backup archive. Could not write backup archive. - + Project backed up to '{0}' Project backed up to '{0}' - - + + Failed to create a new example project. Failed to create a new example project. - + Failed to create a new example project. Could not find the necessary files. They seem to be missing from this installation. Failed to create a new example project. Could not find the necessary files. They seem to be missing from this installation. - + Could not create new project folder. Could not create new project folder. - + New project folder is not empty. Each project requires a dedicated project folder. New project folder is not empty. Each project requires a dedicated project folder. - + You must set a valid backup path in Preferences to use the automatic project backup feature. You must set a valid backup path in Preferences to use the automatic project backup feature. - + You must set a valid project name in Project Settings to use the automatic project backup feature. You must set a valid project name in Project Settings to use the automatic project backup feature. - + and and - + Could not create folder. Could not create folder. - + Found {0} orphaned file(s) in project folder. Found {0} orphaned file(s) in project folder. - + Recovered Recovered - + [{0}] {1} [{0}] {1} - + Recovered File {0} Recovered File {0} - + One or more orphaned files could not be added back into the project. Make sure at least a Novel root folder exists. One or more orphaned files could not be added back into the project. Make sure at least a Novel root folder exists. - + Not a folder: {0} Not a folder: {0} - + Could not move: {0} Could not move: {0} - - + + Could not delete: {0} Could not delete: {0} - + Could not make folder: {0} Could not make folder: {0} - + Could not move item {0} to {1}. Could not move item {0} to {1}. @@ -4139,47 +4138,47 @@ ProjWizardCustomPage - + Custom Project Options Custom Project Options - + Select which additional root folders to make, and how to populate the Novel folder. If you don't want to add chapters or scenes, set the values to 0. You can add scenes without chapters. Select which additional root folders to make, and how to populate the Novel folder. If you don't want to add chapters or scenes, set the values to 0. You can add scenes without chapters. - + Additional Root Folders Additional Root Folders - - - - - - + + + + + + {0} folder {0} folder - + Populate Novel Folder Populate Novel Folder - + Add chapters Add chapters - + Scenes (per chapter) Scenes (per chapter) - + Add chapter folders Add chapter folders @@ -4187,27 +4186,27 @@ ProjWizardFinalPage - + Finished Finished - + All done. All done. - + Press '{0}' to create the new project. Press '{0}' to create the new project. - + Done Done - + Finish Finish @@ -4215,7 +4214,7 @@ ProjWizardFolderPage - + Select Project Folder Select Project Folder @@ -4231,10 +4230,20 @@ Required - + Project Path Project Path + + + Error: A project folder cannot be created using this path. + Error: A project folder cannot be created using this path. + + + + Error: The selected path already exists. + Error: The selected path already exists. + ProjWizardIntroPage @@ -4287,27 +4296,27 @@ ProjWizardPopulatePage - + Populate Project Populate Project - + Choose how to pre-fill the project. Either with a minimal set of starter items, an example project explaining and showing many of the features, or show further custom options on the next page. Choose how to pre-fill the project. Either with a minimal set of starter items, an example project explaining and showing many of the features, or show further custom options on the next page. - + Fill the project with a minimal set of items Fill the project with a minimal set of items - + Fill the project with example files Fill the project with example files - + Show detailed options for filling the project Show detailed options for filling the project @@ -4503,17 +4512,17 @@ Tokenizer - + Synopsis Synopsis - + Document '{0}' is too big ({1} MB). Skipping. Document '{0}' is too big ({1} MB). Skipping. - + ERROR ERROR diff --git a/i18n/nw_nb_NO.ts b/i18n/nw_nb_NO.ts index a4a6c915..17cb5ddb 100644 --- a/i18n/nw_nb_NO.ts +++ b/i18n/nw_nb_NO.ts @@ -77,259 +77,259 @@ Constant - - - + + + None Ingen - + Novel Roman - - + + Plot Plott - - + + Characters Karakterer - - + + Locations Lokasjoner - - + + Timeline Tidslinje - - + + Objects Objekter - - + + Entities Enheter - - + + Custom Annet - + Archive Arkiv - + Trash Søppel - - + + Novel Document Romandokument - - + + Project Note Prosjektnotat - + Root Folder Hovedmappe - + Folder Mappe - + Novel Title Page Tittelside - + Novel Chapter Kapittel - + Novel Scene Scene - + Tag Knagg - + Point of View Perspektiv - - + + Focus Fokus - + Title Tittel - + Level Nivå - + Document Dokument - + Line Linje - + Chars Tegn - + Words Ord - + Pars Avsnitt - + POV Persp. - + Synopsis Sammendrag - + Straight single quotation mark Rett, enkelt sitattegn - + Straight double quotation mark Rett, dobbelt sitattegn - + Left single quotation mark Venstre, enkelt sitattegn - + Right single quotation mark Høyre, enkelt sitattegn - + Single low-9 quotation mark Enkelt, lavt-9 sitattegn - + Single high-reversed-9 quotation mark Enkelt, høyt, reversert-9 sitattegn - + Left double quotation mark Venstre, dobbelt sitattegn - + Right double quotation mark Høyre, dobbelt sitattegn - + Double low-9 quotation mark Dobbelt, lavt-9 sitattegn - + Double high-reversed-9 quotation mark Dobbelt, høyt, reversert-9 sitattegn - + Double low-reversed-9 quotation mark Dobbelt, lavt, reversert-9 sitattegn - + Single left-pointing angle quotation mark Enkelt, venstre, angulært sitattegn - + Single right-pointing angle quotation mark Enkelt, høyre, angulært sitattegn - + Double left-pointing angle quotation mark Dobbelt, venstre, angulært sitattegn - + Double right-pointing angle quotation mark Dobbelt, høyre, angulært sitattegn - + Left corner bracket Venstre hjørnevinkel - + Right corner bracket Høyre hjørnevinkel - + Left white corner bracket Venstre, hvit hjørnevinkel - + Right white corner bracket Høyre, hvit hjørnevinkel @@ -353,9 +353,9 @@ Utgivelse - - - + + + Licence Lisens @@ -411,31 +411,31 @@ Oversettelser - + Theme: {0} Tema: {0} - - - - - Author - Ansvarlig - + Author + Ansvarlig + + + + + Credit Kreditert - + Icons: {0} Ikoner: {0} - + Syntax: {0} Syntaks: {0} @@ -718,62 +718,62 @@ Det har oppstått problemer under bygging av prosjektet: - + Open Document Open Document - + Flat Open Document Flat Open Document - + Plain HTML Enkel HTML - + novelWriter Markdown novelWriter Markdown - + Standard Markdown Standard Markdown - + GitHub Markdown GitHub Markdown - + JSON + novelWriter HTML JSON + novelWriter HTML - + JSON + novelWriter Markdown JSON + novelWriter Markdown - + PDF PDF - + Save Document As Lagre dokumentet som - + {0} file successfully written to: Lagring av {0} var vellykket, og filen ble skrevet til: - + Failed to write {0} file. {1} Misslykkes i å skrive {0} til. {1} @@ -781,17 +781,17 @@ GuiBuildNovelDocView - + This area will show the content of the document to be exported or printed. Press the "Build Preview" button to generate content. Dette området vil vise innholdet av dokumentet som skal eksporteres. Trykk på knappen merket med "Lag forhåndsvisning" for å oppdatere innholdet. - + Unknown Ukjent - + Build Time: Bygget: @@ -799,32 +799,32 @@ GuiDocEditFooter - + Status Status - + Line: {0} ({1}) Linje: {0} ({1}) - + Words: {0} ({1}) Ord: {0} ({1}) - + Document size is {0} bytes Dokumentet er {0} byte - + Words: {0} selected Ord: {0} valgt - + Character count: {0} Antall tegn: {0} @@ -832,22 +832,22 @@ GuiDocEditHeader - + Edit document meta Rediger dokumentinstillinger - + Search document Søk i dokumentet - + Toggle Focus Mode Slå av/på "Fokus-modus" - + Close the document Lukk dokumentet @@ -855,58 +855,58 @@ GuiDocEditSearch - - + + Search Søk - + Replace Erstatt - + Case Sensitive Skill store/små bokstaver - + Whole Words Only Kun hele ord - + RegEx Mode RegEx-modus - + Loop Search Søk rundt - + Search Next File Søk i neste file - + Preserve Case Behold store/små bokstaver - + Close Search Lukk søk - + Find in current document Søk i det åpne dokumentet - + Find and replace in current document Søk og erstatt i det åpne dokumentet @@ -914,117 +914,117 @@ GuiDocEditor - + The document you are trying to open is too big. The document size is {0} MB. The maximum size allowed is {1} MB. Dokumentet du prøver å åpne er for stort. Dokumenter er på {0} MB. Den maksimale størrelsen tillat er {1} MB. - + Opened Document: {0} Åpnet dokument: {0} - + The text you are trying to add is too big. The text size is {0} MB. The maximum size allowed is {1} MB. Teksten du forsøker å legge til er for stor. Teksten er {0} MB. Den maksimale tillatte størrelsen er {1} MB. - + File Changed on Disk Filen er endret på disk - + This document has been changed outside of novelWriter while it was open. Overwrite the file on disk? Dette dokumentet er endret utenfor novelWriter mens det var åpent. Overskrive filen på disken? - + Could not save document. Kunne ikke lagre dokumentet. - + Saved Document: {0} Lagret dokument: {0} - + Spell checking requires the package PyEnchant. It does not appear to be installed. Stavekontroll krever at pakken PyEnchant er installert. Det ser det ikke ut til at den er. - + Spell check complete Stavekontrollen er ferdig - + File Location Filens plassering - + The currently open file is saved in: Det åpne dokumentet er lagret på følgende sted: - + The document has grown too big and you cannot add more text to it. The maximum size of a single novelWriter document is {0} MB. Dokumentet har blitt for stort og du kan ikke legge til mer tekst. Den maksimale tillatte størrelsen for et novelWriter-dokument er {0} MB. - + Follow Tag Følg knagg - + Cut Klipp - + Copy Kopier - + Paste Lim inn - + Select All Velg hele teksten - + Select Word Velg hele ordet - + Select Paragraph Velg hele avsnittet - + Spelling Suggestion(s) Forslag fra stavekontrollen - + No Suggestions Ingen forslag - + Add Word to Dictionary Legg til ord i ordbok - + Please select some text before calling replace quotes. Venligst velg en del av teksten før du velger å erstatte sitattegn. @@ -1067,12 +1067,12 @@ Intern feil - + Could not save document. Kunne ikke lagre dokumentet. - + Element selected in the project tree must be a folder. Elementet som er valgt i prosjekttreet må være en mappe. @@ -1080,83 +1080,78 @@ GuiDocSplit - - + + Split Document Del opp dokument - + Document Headers Dokumentets overskrifter - + Select the maximum level to split into files. Velg hvilket nivå av overskrifter å dele opp til. - + Split on Header Level 1 (Title) Del på overskrifter på nivå 1 (titler) - + Split up to Header Level 2 (Chapter) Del på overskrifter opp til nivå 2 (kapitler) - + Split up to Header Level 3 (Scene) Del på overskrifter opp til nivå 3 (scener) - + Split up to Header Level 4 (Section) Del på overskrifter opp til nivå 4 (seksjoner) - + No source document selected. Nothing to do. Ingen kilde-dokument er valgt. Det er ingenting å gjøre. - + Could not parse source document. Klarte ikke å lese kilde-dokumentet. - + Failed to open document file. Kunne ikke åpne dokumentets fil. - + No headers found. Nothing to do. Ingen overskrifter ble funnet i dokumentet. Det er ikke noe å gjøre. - - Cannot add new folder for the document split. Maximum folder depth has been reached. Please move the file to another level in the project tree. - Kan ikke legge til ny mappe for å dele opp dokumentet. Dokumentet har allerede maksimal dybde i prosjekttreet. Flytt dokumentet til et annet nivå først. - - - + The document will be split into {0} file(s) in a new folder. The original document will remain intact. Dokumentet vil nå bli delt opp i {0} nye filer i en ny mappe. Det originale dokumentet vil ikke bli endret eller fjernet. - + Continue with the splitting process? Fortsette med oppdelingen? - + Could not save document. Kunne ikke lagre dokumentet. - + Element selected in the project tree must be a file. Elementet som er valgt i prosjekttreet må være et dokument. @@ -1164,42 +1159,42 @@ GuiDocViewFooter - + Show/hide the references panel Skjul eller vis referanse-panelet - + Activate to freeze the content of the references panel when changing document Aktiver for å fryse innholdet i referanse-panelet ved bytte av vist dokument - + Show comments Vis kommentarer - + Show synopsis comments Vis sammendrag - + References Referanser - + Sticky Hold igjen - + Comments Kommentarer - + Synopsis Sammendrag @@ -1207,22 +1202,22 @@ GuiDocViewHeader - + Go backward Gå bakover - + Go forward Gå fremover - + Reload the document Last dokumentet på nytt - + Close the document Lukk dokumentet @@ -1283,17 +1278,17 @@ Formål - + Characters Tegn - + Words Ord - + Paragraphs Avsnitt @@ -1306,237 +1301,230 @@ Enhetsinstillinger - + Include when building project Ta med ved eksport - + Label Navn - + Status Status - + Layout Format + + GuiLipsum + + + Insert Placeholder Text + Sett inn midlertidig tekst + + + + Insert Lorem Ipsum Text + Sett inn Lorem Ipsum-tekst + + + + Number of paragraphs + Antall avsnitt + + + + Randomise order + Tilfeldig rekkefølge + + + + Insert + Sett inn + + GuiMain - - Project - Prosjekt - - - - Novel - Roman - - - - Project Details - Prosjektdetaljer - - - - Writing Statistics - Statistikk - - - - Project Settings - Prosjektinnstillinger - - - - Editor - Editor - - - - Outline - Disposisjon - - - + You are running an untested development version of novelWriter. Please be careful when working on a live project and make sure you take regular backups. Du kjører nå en utestet versjon av novelWriter. Vær forsiktig om du jobber med et av dine faktiske prosjekter. Husk å ta backup! - + novelWriter is ready ... novelWriter er klar ... - + Cannot create a new project when another project is open. Kan ikke lage et nytt prosjekt mens et annet prosjekt er åpent. - + A project already exists in that location. Please choose another folder. Et prosjekt finnes allerede i den mappen. Vennligst velg et annet sted å lagre prosjektet. - + New project created ... Et nytt prosjekt har blitt opprettet ... - + Close Project Lukk prosjektet - + Close the current project? Ønsker du å lukke dette prosjektet? - - + + Changes are saved automatically. Endringer lagres automatisk. - + Backup Project Sikkerhetskopiering - + Backup the current project? Ønsker du å ta sikkerhetskopi av dette prosjektet? - + The project was locked by the computer '{0}' ({1} {2}), last active on {3}. Prosjektet er låst av datamaskinen {0} ({1} {2}), siste registrerte aktivitet var {3}. - + Project Locked Prosjektlås - + The project is already open by another instance of novelWriter, and is therefore locked. Override lock and continue anyway? Prosjektet er allerede åpent av en annen instans av novelWriter, og er derfor låst. Vil du overstyre denne låsen og fortsette likevel? - + Note: If the program or the computer previously crashed, the lock can safely be overridden. However, overriding it is not recommended if the project is open in another instance of novelWriter. Doing so may corrupt the project. Merk: Hvis programmet eller datamaskinen tidligere krasjet, kan fil-låsen trygt overstyres. Det anbefales imidlertid ikke å overstyre den hvis prosjektet er åpent i en annen instans av novelWriter. Å gjøre det kan skape konflikter i prosjektets filer. - + The project index is outdated or broken. Rebuilding index. Prosjektets indeks er utdatert eller skadet. Bygger indeksen på nytt. - + Text files ({0}) Tekstfiler ({0}) - + Markdown files ({0}) Markdown-filer ({0}) - + novelWriter files ({0}) novelWriter-filer ({0}) - + All files ({0}) Alle filer ({0}) - + Import File Importer fil - + Could not read file. The file must be an existing text file. Kunne ikke lese filen. Filen må eksistere fra før av. - + Please open a document to import the text file into. Vennligst åpne et dokument hvor teksten i filen kan importeres. - + Import Document Importer dokument - + Importing the file will overwrite the current content of the document. Do you want to proceed? Å importere filen vil overskrive all eksisterende tekst i dokumentet. Ønsker du å fortsette? - - + + Indexing: '{0}' Indekserer: '{0}' - + Unknown item Ukjent enhet - + Indexing completed in {0} ms Indekseringen tok {0} ms - + The project index has been successfully rebuilt. Prosjektets indeks har blitt bygget på nytt. - + Information Informasjon - + Warning Advarsel - + Error Feil - + This is a bug! Dette er en systemfeil! - + Internal Error Intern feil - + Exit Avslutt - + Do you want to exit novelWriter? Ønsker du å avslutte novelWriter? @@ -1544,677 +1532,682 @@ GuiMainMenu - + &Project &Prosjekt - + New Project Nytt prosjekt - + Open Project Åpne prosjekt - + Save Project Lagre prosjektet - + Close Project Lukk prosjektet - + Project Settings Prosjektinnstillinger - + Project Details Prosjektdetaljer - + Create Root Folder Lag ny hovedmappe - + Novel Root Mappe for "Roman" - + Plot Root Mappe for "Plott" - + Character Root Mappe for "Karakterer" - + Location Root Mappe for "Lokasjoner" - + Timeline Root Mappe for "Tidslinjer" - + Object Root Mappe for "Objekter" - + Entity Root Mappe for "Enheter" - + Custom Root Mappe for "Annet" - + Archive Root Mappe for Arkiv - + Create Folder Lag ny mappe - + Edit Item Endre enhet - + Delete Item Slett enhet - + Move Item Up Flytt enhet opp - + Move Item Down Flytt enhet ned - + Undo Last Move Angre siste flytting - + Empty Trash Tøm søppel - + Exit Avslutt - + &Document &Dokument - + New Document Nytt dokument - + Open Document Åpne dokument - + Save Document Lagre dokumentet - + Close Document Lukk dokumentet - + View Document Vis dokument - + Close Document View Lukk dokumentvisning - + Show File Details Vis filinformasjon - + Import Text from File Importer tekst fra fil - + Merge Folder to Document Slå sammen mappe - + Split Document to Folder Del opp dokument - + &Edit &Rediger - + Undo Angre - + Redo Gjenopprett - + Cut Klipp - + Copy Kopier - + Paste Lim inn - + Select All Velg hele teksten - + Select Paragraph Velg hele avsnittet - + &View &Vis - + Go to Project Tree Gå til prosjekt-tre - + Go to Document Editor Gå til dokument-editor - + Go to Document Viewer Gå til visningsvindu - + Go to Outline Gå til disposisjon - + Navigate Backward Navigere bakover - + Navigate Forward Navigere fremover - + Focus Mode Focus-modus - + Full Screen Mode Fullskjerm-modus - + &Insert Sett &inn - + Dashes Bindestreker - + Short Dash Kort bindestrek - + Long Dash Lang bindestrek - + Horizontal Bar Horisontal strek - + Figure Dash Tallstrek - + Quote Marks Sitattegn - + Left Single Quote Venstre, enkelt sitattegn - + Right Single Quote Høyre, enkelt sitattegn - + Left Double Quote Venstre, dobbelt sitattegn - + Right Double Quote Høyre, dobbelt sitattegn - + Alternative Apostrophe Alternativ apostrof - + General Punctuation Generell tegnsetting - + Ellipsis Ellipsis - + Prime Primtegn - + Double Prime Dobbelt primtegn - + White Spaces Mellomrom - + Non-Breaking Space Hardt mellomrom - + Thin Space Kort mellomrom - + Thin Non-Breaking Space Hardt, kort mellomrom - + Other Symbols Andre symboler - + List Bullet Kulepunkt - + Hyphen Bullet Bindestrekpunkt - + Flower Mark Blomsterpunkt - + Per Mille Promille - + Degree Symbol Gradertegn - + Minus Sign Minustegn - + Times Sign Gangetegn - + Division Sign Deletegn - + Tags and References Knagger og referanser - + Page Break and Space Sideskift og avstand - + Page Break Sideskift - + Vertical Space (Single) Vertikal avstand (enkel) - + Vertical Space (Multi) Vertikal avstand (flere) - + + Placeholder Text + Midlertidig tekst + + + &Format &Formattering - + Emphasis Kursiv - + Strong Emphasis Uthev - + Strikethrough Gjennomstrek - + Wrap Double Quotes Sett i doble sitattegn - + Wrap Single Quotes Sett i enkle sitattegn - + Header 1 (Partition) Overskrift 1 (inndeling) - + Header 2 (Chapter) Overskrift 2 (kapittel) - + Header 3 (Scene) Overskrift 3 (scene) - + Header 4 (Section) Overskrift 4 (seksjon) - + Novel Title Boktittel - + Unnumbered Chapter Unumrert kapittel - + Align Left Venstrejuster - + Align Centre Sentrer - + Align Right Høyrejuster - + Indent Left Innrykk fra venstre - + Indent Right Innrykk fra høyre - + Toggle Comment Veksle kommentar - + Remove Block Format Fjern formattering - + Convert Single Quotes Konverter enkle sitattegn - + Convert Double Quotes Konverter doble sitattegn - + Remove In-Paragraph Breaks Fjern linjeskift i avsnittet - + &Search &Søk - + Find Søk - + Replace Erstatt - + Find Next Finn neste - + Find Previous Finn forrige - + Replace Next Erstatt neste - + &Tools &Verktøy - + Check Spelling Stavekontroll - + Re-Run Spell Check Kjør stavekontroll - + Project Word List Prosjektets ordliste - + Rebuild Index Bygg indeks - + Rebuild Outline Bygg disposisjon - + Auto-Update Outline Auto-oppdater disposisjon - + Backup Project Lag sikkerhetskopi av prosjektets mappe - + Build Novel Project Bygg prosjektet - + Writing Statistics Statistikk - + Preferences Innstillinger - + &Help &Hjelp - + About novelWriter Om novelWriter - + About Qt5 Om Qt5 - + User Manual (Online) Brukermanual (på nett) - + User Manual (PDF) Brukermanual (PDF) - + Report an Issue (GitHub) Rapporter en feil (GitHub) - + Ask a Question (GitHub) Still et spørsmål (GitHub) - + The novelWriter Website novelWriters nettside - + Check for New Release Sjekk etter oppdateringer @@ -2294,65 +2287,65 @@ GuiOutlineDetails - - - - + + + + Title Tittel - + Chapter Kapittel - + Scene Scene - + Section Seksjon - + Document Dokument - + Status Status - + Characters Tegn - + Words Ord - + Paragraphs Avsnitt - + Synopsis Sammendrag - + Title Details Oversikt - + Reference Tags Referanser @@ -3046,8 +3039,8 @@ - Highlight multiple spaces - Fremheve repeterte mellomrom + Highlight multiple or trailing spaces + Fremhev flere eller etterfølgende mellomrom @@ -3187,58 +3180,58 @@ GuiProjectEditMain - + Project Settings Prosjektinnstillinger - + Working title Arbeidstittel - + Should be set only once. Bør bare settes én gang. - + Novel title Bokens tittel - + Change whenever you want! Kan endres når som helst! - + Author(s) Forfatter(e) - + One name per line. Ett navn per linje. - + Default Ingen valg - + Spell check language Språk for stavekontroll - - + + Overrides main preferences. Overstyrer valg i innstillinger. - + No backup on close Slå av sikkerhetskopi @@ -3246,27 +3239,27 @@ GuiProjectEditReplace - + Text Replace List for Preview and Export Erstatningsliste for forhåndsvisning og eksport - + Keyword Kodeord - + Replace With Erstatt med - + Select item to edit Velg enhet å redigere - + Save Lagre @@ -3274,67 +3267,67 @@ GuiProjectEditStatus - + Novel File Status Levels Statusnivåer i roman-filer - + Note File Importance Levels Viktighetsnivåer i notatfiler - + Label Navn - + Usage Bruk - + Select item to edit Velg enhet å redigere - + Colour Farge - + Save Lagre - + Select Colour Velg farge - + New Item Legg til - + Cannot delete a status item that is in use. Kan ikke slette status som er i bruk. - + Not in use Ikke i bruk - + Used once Brukt ett sted - + Used by {0} items Brukt {0} steder @@ -3406,27 +3399,27 @@ GuiProjectSettings - + Project Settings Prosjektinnstillinger - + Settings Innstillinger - + Status Status - + Importance Viktighet - + Auto-Replace Autoerstatt @@ -3464,100 +3457,78 @@ Filen eller dokumentets status - - Please select a valid location in the tree to add the document. - Du må velge et gyldig sted i prosjekttreet for å legge til dokumentet. - - - - Please select a valid location in the tree to add the folder. - Du må velge et gyldig sted i prosjekttreet for å legge til mappen. - - - - + Did not find anywhere to add the file or folder! Fant ikke noe sted å legge til filen eller mappen! - + Cannot add new files or folders to the Trash folder. Kan ikke legge til nye filer eller mapper til søppel-mappen. - - New File - Ny fil + + New Document + Nytt dokument - - Cannot add new folder to this item. Maximum folder depth has been reached. - Kan ikke legge til ny mappe på dette stedet da maksimum mappe-dypde er nådd. + + New Note + Nytt notat - + New Folder Ny mappe - + There is currently no Trash folder in this project. Det er for øyeblikket ingen søppel-mappe i dette prosjektet. - + The Trash folder is already empty. Søppel-mappen er allerede tom. - + Empty Trash Tøm søppel - + Permanently delete {0} file(s) from Trash? Vil du slette {0} filer i søppel-mappen for godt? - - - Delete File - Slett fil - - - - Permanently delete file '{0}'? - Slette filen {0} for godt'? - - - - Could not delete document file. - Kunne ikke slette dokumentets fil. - - - - Move file '{0}' to Trash? - Vil du flytte filen {0} til søpla? - - - - Cannot delete folder. It is not empty. Recursive deletion is not supported. Please delete the content first. - Kan ikke slette mappen da den ikke er tom. Rekursiv sletting er ikke støttet. Du må slette innholdet først. - - - + Cannot delete root folder. It is not empty. Recursive deletion is not supported. Please delete the content first. Kan ikke slette hovedmappen da den ikke er tom. Rekursiv sletting er ikke støttet. Du må slette innholdet først. - - - The item cannot be moved to that location. - Denne enheten kan ikke flyttes til denne lokasjonen. + + + Delete + Slett - + + Permanently delete '{0}'? + Slette filen "{0}" for godt? + + + + Move '{0}' to Trash? + Vil du flytte filen "{0}" til søpla? + + + + Could not delete document file. + Kunne ikke slette dokumentets fil. + + + There is nowhere to add item with name '{0}'. Fant ikke noe sted å legge til enheten med navn {0}'. @@ -3565,52 +3536,52 @@ GuiProjectTreeMenu - + Edit Project Item Endre enhet - + Open Document Åpne dokument - + View Document Vis dokument - + Toggle Included Flag Slå av/på inkludering - + New File Ny fil - + New Folder Ny mappe - + Delete Item Slett enhet - + Empty Trash Tøm søppel - + Move Item Up Flytt enhet opp - + Move Item Down Flytt enhet ned @@ -3649,6 +3620,39 @@ Last ned: {0} + + GuiViewsBar + + + Project + Prosjekt + + + + Novel + Roman + + + + Outline + Oversikt + + + + Details + Detaljer + + + + Stats + Statistikk + + + + Settings + Oppsett + + GuiWordList @@ -3816,7 +3820,7 @@ Kunne ikke skrive {0}-filen. - + Failed to read session log file. Kunne ikke lese loggfil med skrive-statistikk. @@ -3824,314 +3828,309 @@ NWProject - - Duplicate root item detected. - Duplikat hovedmappe. - - - - + + New Ny - + Note Notat - + Draft Utkast - + Finished Ferdig - + Minor Mindre - + Major Større - + Main Hoved - + New Project Nytt prosjekt - + By Av - - + + Novel Roman - + Plot Plott - + Characters Karakterer - + World Verden - - + + Title Page Tittelside - - - + + + New Chapter Nytt kapittel - - + + New Scene Ny scene - + Chapter {0} Kapittel {0} - - + + Scene {0} Scene {0} - + File not found: {0} Fant ikke filen: {0} - - + + Failed to parse project xml. Kunne ikke lese prosjektets xml-data. - + Attempting to open backup project file instead. Forsøker å åpne prosjektets sekundære prosjektfil istedet. - - + + Unknown Ukjent - + Project file does not appear to be a novelWriterXML file. Prosjektfilen later ikke til å være en novelWriterXML-fil. - + Unknown or unsupported novelWriter project file format. The project cannot be opened by this version of novelWriter. The file was saved with novelWriter version {0}. Prosjektfilen har et ukjent eller ikke støttet format, og kan ikke åpnes med denne versjonen av novelWriter. Prosjektet ble lagret av novelWriter versjon {0}. - + File Version Filversjon - + The file format of your project is about to be updated. If you proceed, older versions of novelWriter will no longer be able to open this project. Continue? Filformatet til prosjektet ditt er i ferd med å bli oppdatert. Hvis du fortsetter, vil ikke eldre versjoner av novelWriter lenger kunne åpne dette prosjektet. Fortsette? - + Version Conflict Versjonskonflikt - + This project was saved by a newer version of novelWriter, version {0}. This is version {1}. If you continue to open the project, some attributes and settings may not be preserved, but the overall project should be fine. Continue opening the project? Dette prosjektet ble lagret av en nyere versjon av novelWriter, versjon {0}. Dette er versjon {1}. Hvis du ønsker å fortsette med å åpne prosjektet, kan noen av innstillingene bli borte, men selve prosjektet vil være i orden. Vil du fortsatt åpne prosjektet? - + Opened Project: {0} Åpnet prosjekt: {0} - + Project path not set, cannot save project. Prosjektet mangler filbane, og kan ikke lagres. - - + + Failed to save project. Kunne ikke lagre prosjektet. - + Saved Project: {0} Lagret prosjekt: {0} - + Backing up project ... Lager sikkerhetskopi ... - + Cannot backup project because no valid backup path is set. Please set a valid backup location in Preferences. Kan ikke ta sikkerhetskopi av prosjektet da ingen filbane er satt. Du må først sette en gyldig filbane i Innstillinger. - + Cannot backup project because no project name is set. Please set a Working Title in Project Settings. Kan ikke ta sikkerhetskopi av prosjektet da ingen arbeidstittel er satt. Du må først sette en arbeidstittel i Prosjektinnstillinger. - + Could not create backup folder. Kunne ikke lage mappe til sikkerhetskopi. - + Cannot backup project because the backup path is within the project folder to be backed up. Please choose a different backup path in Preferences. Kan ikke ta sikkerhetskopi av prosjektet da filbanen er inne i prosjektmappen. Du må sette en ny filbane i Innstillinger. - + Backup from {0} Sikkerhetskopi fra {0} - + Backup archive file written to: {0} Sikkerhetskopi skrevet til: {0} - + Could not write backup archive. Kunne ikke lage sikkerhetskopi. - + Project backed up to '{0}' Sikkerhetskopi skrevet til '{0}' - - + + Failed to create a new example project. Kunne ikke lage nytt eksempel-prosjekt. - + Failed to create a new example project. Could not find the necessary files. They seem to be missing from this installation. Kunne ikke lage nytt eksempel-prosjekt. Kunne ikke finne de nødvendige filene. De ser ut til å mangle i denne installasjonen. - + Could not create new project folder. Kunne ikke lage ny prosjekt-mappe. - + New project folder is not empty. Each project requires a dedicated project folder. Ny prosjektmappe er ikke tom. Hvert prosjekt trenger sin egen mappe. - + You must set a valid backup path in Preferences to use the automatic project backup feature. Du må sette en gyldig filbane i innstillingene for å kunne bruke automatisk sikkerhetskopi. - + You must set a valid project name in Project Settings to use the automatic project backup feature. Du må sette en gyldig arbeidstittel i prosjektinnstillingene for å kunne bruke automatisk sikkerhetskopi. - + and og - + Could not create folder. Kunne ikke opprette mappe. - + Found {0} orphaned file(s) in project folder. Fant {0} tapte filer i prosjektmappen. - + Recovered Gjennopprettet - + [{0}] {1} [{0}] {1} - + Recovered File {0} Gjennopprettet fil {0} - + One or more orphaned files could not be added back into the project. Make sure at least a Novel root folder exists. Én eller flere gjennopprettede filer kunne ikke bli lagt til i posjektet. Pass på at "Roman"-mappen i det minste eksisterer. - + Not a folder: {0} Ikke en mappe: {0} - + Could not move: {0} Kunne ikke flytte: {0} - - + + Could not delete: {0} Kunne ikke slette: {0} - + Could not make folder: {0} Kunne ikke lage mappe: {0} - + Could not move item {0} to {1}. Kunne ikke flytte {0} til {1}. @@ -4139,47 +4138,47 @@ ProjWizardCustomPage - + Custom Project Options Flere alternativer - + Select which additional root folders to make, and how to populate the Novel folder. If you don't want to add chapters or scenes, set the values to 0. You can add scenes without chapters. Velg hvilke mapper du ønsker i prosjektet, og hvordan du ønsker å fylle hovedmappen for boken. Hvis du ikke ønsker å legge til kapitler og scener, sett verdiene til 0. Du kan også legge til scener uten å legge til kapitler. - + Additional Root Folders Hovedmapper - - - - - - + + + + + + {0} folder {0} - + Populate Novel Folder Fyll roman-mappen - + Add chapters Legg til kapitler - + Scenes (per chapter) Scener (per kapittel) - + Add chapter folders Lag kapittel-mapper @@ -4187,27 +4186,27 @@ ProjWizardFinalPage - + Finished Ferdig - + All done. Alt er klart. - + Press '{0}' to create the new project. Trykk '{0}' for å opprette det nye prosjektet. - + Done Ferdig - + Finish Fullfør @@ -4215,7 +4214,7 @@ ProjWizardFolderPage - + Select Project Folder Velg prosjektmappe @@ -4231,10 +4230,20 @@ Påkrevd - + Project Path Filbane + + + Error: A project folder cannot be created using this path. + Feil: En prosjektmappe kan ikke opprettes ved hjelp av denne banen. + + + + Error: The selected path already exists. + Feil: Den valgte banen finnes allerede. + ProjWizardIntroPage @@ -4287,27 +4296,27 @@ ProjWizardPopulatePage - + Populate Project Fyll prosjektet - + Choose how to pre-fill the project. Either with a minimal set of starter items, an example project explaining and showing many of the features, or show further custom options on the next page. Velg hvordan du vil forhåndsfylle prosjektet. Du kan velge mellom et minimalt sett med mapper og filer, et eksempel-prosjekt som forklarer og viser hvordan du bruker programmet, eller se flere valg på neste side. - + Fill the project with a minimal set of items Fyll prosjektet med et minimalt innhold - + Fill the project with example files Fyll prosjektet med eksempelfiler - + Show detailed options for filling the project Vis detaljerte valg for å fylle prosjektet @@ -4503,17 +4512,17 @@ Tokenizer - + Synopsis Sammendrag - + Document '{0}' is too big ({1} MB). Skipping. Dokumentet '{0}' er for stort ({1} MB). Hopper over. - + ERROR FEIL diff --git a/novelwriter/assets/i18n/project_nl_NL.json b/novelwriter/assets/i18n/project_nl_NL.json index 644da56a..65f4eaca 100644 --- a/novelwriter/assets/i18n/project_nl_NL.json +++ b/novelwriter/assets/i18n/project_nl_NL.json @@ -2,104 +2,104 @@ "Synopsis": "Synopsis", "Comment": "Opmerking", "Notes": "Notities", - "0": "Nul", - "1": "Één", - "2": "Twee", - "3": "Drie", - "4": "Vier", - "5": "Vijf", - "6": "Zes", - "7": "Zeven", - "8": "Acht", - "9": "Negen", - "10": "Tien", - "11": "Elf", - "12": "Twaalf", - "13": "Dertien", - "14": "Veertien", - "15": "Vijftien", - "16": "Zestien", - "17": "Zeventien", - "18": "Achttien", - "19": "Negentien", - "20": "Twintig", - "21": "Eenentwintig", - "22": "Tweeentwintig", - "23": "Drieentwintig", - "24": "Vierentwintig", - "25": "Vijfentwintig", - "26": "Zesentwintig", - "27": "Zevenentwintig", - "28": "Achtentwintig", - "29": "Negenentwintig", - "30": "Dertig", - "31": "Eenendertig", - "32": "Tweeendertig", - "33": "Drieendertig", - "34": "Vierendertig", - "35": "Vijfendertig", - "36": "Zesendertig", - "37": "Zevenendertig", - "38": "Achtendertig", - "39": "Negenendertig", - "40": "Veertig", - "41": "Eenenveertig", - "42": "Tweeënveertig", - "43": "Drieenveertig", - "44": "Vierenveertig", - "45": "Vijfenveertig", - "46": "Zesenveertig", - "47": "Zevenenveertig", - "48": "Achtenveertig", - "49": "Negenenveertig", - "50": "Vijftig", - "51": "Eenenvijftig", - "52": "Tweeenvijftig", - "53": "Drieenvijftig", - "54": "Vierenvijftig", - "55": "Vijfenvijftig", - "56": "Zesenvijftig", - "57": "Zevenenvijftig", - "58": "Achtenvijftig", - "59": "Negenenvijftig", - "60": "Zestig", - "61": "Eenenzestig", - "62": "Tweeenzestig", - "63": "Drieenzestig", - "64": "Vierenzestig", - "65": "Vijfenzestig", - "66": "Zesenzestig", - "67": "Zevenenzestig", - "68": "Achtenzestig", - "69": "Negenenzestig", - "70": "Zeventig", - "71": "Eenenzeventig", - "72": "Tweeenzeventig", - "73": "Drieenzeventig", - "74": "Vierenzeventig", - "75": "Vijfenzeventig", - "76": "Zesenzeventig", - "77": "Zevenenzeventig", - "78": "Achtenzeventig", - "79": "Negenenzeventig", - "80": "Tachtig", - "81": "Eenentachtig", - "82": "Tweeentachtig", - "83": "Drieentachtig", - "84": "Vierentachtig", - "85": "Vijfentachtig", - "86": "Zesentachtig", - "87": "Zevenentachtig", - "88": "Achtentachtig", - "89": "Negenentachtig", - "90": "Negentig", - "91": "Eenennegentig", - "92": "Tweeennegentig", - "93": "Drieennegentig", - "94": "Vierennegentig", - "95": "Vijfennegentig", - "96": "Zesennegentig", - "97": "Zevenennegentig", - "98": "Achtennegentig", - "99": "Negenennegentig" + "0": "nul", + "1": "één", + "2": "twee", + "3": "drie", + "4": "vier", + "5": "vijf", + "6": "zes", + "7": "zeven", + "8": "acht", + "9": "negen", + "10": "tien", + "11": "elf", + "12": "twaalf", + "13": "dertien", + "14": "veertien", + "15": "vijftien", + "16": "zestien", + "17": "zeventien", + "18": "achttien", + "19": "negentien", + "20": "twintig", + "21": "eenentwintig", + "22": "tweeëntwintig", + "23": "drieëntwintig", + "24": "vierentwintig", + "25": "vijfentwintig", + "26": "zesentwintig", + "27": "zevenentwintig", + "28": "achtentwintig", + "29": "negenentwintig", + "30": "dertig", + "31": "eenendertig", + "32": "tweeëndertig", + "33": "drieëndertig", + "34": "vierendertig", + "35": "vijfendertig", + "36": "zesendertig", + "37": "zevenendertig", + "38": "achtendertig", + "39": "negenendertig", + "40": "veertig", + "41": "eenenveertig", + "42": "tweeënveertig", + "43": "drieënveertig", + "44": "vierenveertig", + "45": "vijfenveertig", + "46": "zesenveertig", + "47": "zevenenveertig", + "48": "achtenveertig", + "49": "negenenveertig", + "50": "vijftig", + "51": "eenenvijftig", + "52": "tweeënvijftig", + "53": "drieënvijftig", + "54": "vierenvijftig", + "55": "vijfenvijftig", + "56": "zesenvijftig", + "57": "zevenenvijftig", + "58": "achtenvijftig", + "59": "negenenvijftig", + "60": "zestig", + "61": "eenenzestig", + "62": "tweeënzestig", + "63": "drieënzestig", + "64": "vierenzestig", + "65": "vijfenzestig", + "66": "zesenzestig", + "67": "zevenenzestig", + "68": "achtenzestig", + "69": "negenenzestig", + "70": "zeventig", + "71": "eenenzeventig", + "72": "tweeënzeventig", + "73": "drieënzeventig", + "74": "vierenzeventig", + "75": "vijfenzeventig", + "76": "zesenzeventig", + "77": "zevenenzeventig", + "78": "achtenzeventig", + "79": "negenenzeventig", + "80": "tachtig", + "81": "eenentachtig", + "82": "tweeëntachtig", + "83": "drieëntachtig", + "84": "vierentachtig", + "85": "vijfentachtig", + "86": "zesentachtig", + "87": "zevenentachtig", + "88": "achtentachtig", + "89": "negenentachtig", + "90": "negentig", + "91": "eenennegentig", + "92": "tweeënnegentig", + "93": "drieënnegentig", + "94": "vierennegentig", + "95": "vijfennegentig", + "96": "zesennegentig", + "97": "zevenennegentig", + "98": "achtennegentig", + "99": "negenennegentig" } From ae2b5d4403566b180a6dc17260a6b0f749c278de Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 17 May 2022 20:22:39 +0200 Subject: [PATCH 077/179] Fix typo and set max width on views bar --- novelwriter/gui/viewsbar.py | 2 ++ novelwriter/tools/lipsum.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py index 92244781..c4dfc476 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/viewsbar.py @@ -49,6 +49,7 @@ class GuiViewsBar(QToolBar): # Style iPx = self.mainConf.pxInt(22) + mPx = self.mainConf.pxInt(58) lblFont = self.theTheme.guiFont lblFont.setPointSizeF(0.65*self.theTheme.fontPointSize) @@ -57,6 +58,7 @@ class GuiViewsBar(QToolBar): self.setMovable(False) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.setIconSize(QSize(iPx, iPx)) + self.setMaximumWidth(mPx) self.setContentsMargins(0, 0, 0, 0) stretch = QWidget(self) diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index a6ac572d..4ce21643 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -72,7 +72,7 @@ class GuiLipsum(QDialog): # Form self.headLabel = QLabel("{0}".format(self.tr("Insert Lorem Ipsum Text"))) - self.paraLabel = QLabel(self.tr("Number of pragraphs")) + self.paraLabel = QLabel(self.tr("Number of paragraphs")) self.paraCount = QSpinBox() self.paraCount.setMinimum(1) self.paraCount.setMaximum(100) From a166d9a4ddfd5aa6e28d10d2b01f47a062d7ec61 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 19 May 2022 20:04:10 +0200 Subject: [PATCH 078/179] Add a Translation Guidelines section in i18n --- i18n/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/i18n/README.md b/i18n/README.md index 27486cf9..b2dd8cc9 100644 --- a/i18n/README.md +++ b/i18n/README.md @@ -2,6 +2,7 @@ The maintenance of translations has been moved to the Crowdin service. The translation strings can be edited there at the [novelWriter project page](https://crowdin.com/project/novelwriter). +However, please read the Translation Guidelines section below. You can still use the manual approach listed below, and then upload the file through the website's interface. The translation strings for that language will then be updated and queued for approval. @@ -11,6 +12,22 @@ To verify a language file translated through the Crowdin tool, download and extr [Generate an Updated Translation File](#generate-an-updated-translation-file) below. +# Translation Guidelines + +When contributing translations, keep the following things in mind. + +* For descriptive labels and dialog boxes, make sure you do _not_ change the meaning of the text + when you translate it from English. The user must receive the same instructions or information + regardless of language. This is improtant, otherwise the documentation will be inconsistent with + the user interface and it will become a lot more difficult to handle user issues and questions. +* If you think a label or description is misleading or incomplete, please file an issue report. The + correct way to handle such changes is to change the text in the code first, which will then be + forwarded to _all_ translators such that the GUI is consistent across all languages. +* For very short labels, like button labels. it may be fine to replace the word with a similar + word, but only as long as the user understands what it is supposed to do. Some buttons and tabs + have limited space. If necessary, it is OK to use abbreviations. + + # Direct Approach Using Qt Linguist Here you will find instructions for translating novelWriter to a new language directly using Qt From 5574d8d6c0826fc19822d0a73f97439a2241fede Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 15:11:33 +0200 Subject: [PATCH 079/179] Update and clarify the options on the Wizard --- novelwriter/tools/projwizard.py | 113 ++++++++++---------------------- 1 file changed, 34 insertions(+), 79 deletions(-) diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index e4cc6862..a5f96d64 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -31,12 +31,10 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QWizard, QWizardPage, QLabel, QVBoxLayout, QLineEdit, QPlainTextEdit, QPushButton, QFileDialog, QHBoxLayout, QRadioButton, QFormLayout, - QGroupBox, QGridLayout, QSpinBox + QGridLayout, QSpinBox ) -from novelwriter.enum import nwItemClass from novelwriter.common import makeFileNameSafe -from novelwriter.constants import trConst, nwLabels from novelwriter.gui.custom import QSwitch logger = logging.getLogger(__name__) @@ -98,10 +96,10 @@ class ProjWizardIntroPage(QWizardPage): self.setTitle(self.tr("Create New Project")) self.theText = QLabel(self.tr( - "Provide at least a working title. The working title should not " - "be change beyond this point as it is used by the application for " - "generating file names for for instance backups. The other fields " - "are optional and can be changed at any time in Project Settings." + "Provide at least a project name. The project name should not " + "be change beyond this point as it is used for generating file " + "names for for instance backups. The other fields are optional " + "and can be changed at any time in Project Settings." )) self.theText.setWordWrap(True) @@ -134,7 +132,7 @@ class ProjWizardIntroPage(QWizardPage): self.projAuthors.setPlaceholderText(self.tr("Optional. One name per line.")) self.mainForm = QFormLayout() - self.mainForm.addRow(self.tr("Working Title"), self.projName) + self.mainForm.addRow(self.tr("Project Name"), self.projName) self.mainForm.addRow(self.tr("Novel Title"), self.projTitle) self.mainForm.addRow(self.tr("Author(s)"), self.projAuthors) self.mainForm.setVerticalSpacing(fS) @@ -324,68 +322,26 @@ class ProjWizardCustomPage(QWizardPage): self.setTitle(self.tr("Custom Project Options")) self.theText = QLabel(self.tr( - "Select which additional root folders to make, and how to populate " - "the Novel folder. If you don't want to add chapters or scenes, set " - "the values to 0. You can add scenes without chapters." + "Select which additional elements to populate the project with. " + "You can skip making chapters and add only scenes by setting the " + "number of chapters to 0." )) self.theText.setWordWrap(True) vS = self.mainConf.pxInt(12) # Root Folders - self.rootGroup = QGroupBox(self.tr("Additional Root Folders")) - self.rootForm = QGridLayout() - self.rootGroup.setLayout(self.rootForm) - - self.lblPlot = QLabel(self.tr("{0} folder").format( - trConst(nwLabels.CLASS_NAME[nwItemClass.PLOT])) - ) - self.lblChar = QLabel(self.tr("{0} folder").format( - trConst(nwLabels.CLASS_NAME[nwItemClass.CHARACTER])) - ) - self.lblWorld = QLabel(self.tr("{0} folder").format( - trConst(nwLabels.CLASS_NAME[nwItemClass.WORLD])) - ) - self.lblTime = QLabel(self.tr("{0} folder").format( - trConst(nwLabels.CLASS_NAME[nwItemClass.TIMELINE])) - ) - self.lblObject = QLabel(self.tr("{0} folder").format( - trConst(nwLabels.CLASS_NAME[nwItemClass.OBJECT])) - ) - self.lblEntity = QLabel(self.tr("{0} folder").format( - trConst(nwLabels.CLASS_NAME[nwItemClass.ENTITY])) - ) - - self.addPlot = QSwitch() - self.addChar = QSwitch() - self.addWorld = QSwitch() - self.addTime = QSwitch() - self.addObject = QSwitch() - self.addEntity = QSwitch() + self.addPlot = QSwitch() + self.addChar = QSwitch() + self.addWorld = QSwitch() + self.addNotes = QSwitch() self.addPlot.setChecked(True) self.addChar.setChecked(True) - self.addWorld.setChecked(True) - - self.rootForm.addWidget(self.lblPlot, 0, 0) - self.rootForm.addWidget(self.lblChar, 1, 0) - self.rootForm.addWidget(self.lblWorld, 2, 0) - self.rootForm.addWidget(self.lblTime, 3, 0) - self.rootForm.addWidget(self.lblObject, 4, 0) - self.rootForm.addWidget(self.lblEntity, 5, 0) - self.rootForm.addWidget(self.addPlot, 0, 1, 1, 1, Qt.AlignRight) - self.rootForm.addWidget(self.addChar, 1, 1, 1, 1, Qt.AlignRight) - self.rootForm.addWidget(self.addWorld, 2, 1, 1, 1, Qt.AlignRight) - self.rootForm.addWidget(self.addTime, 3, 1, 1, 1, Qt.AlignRight) - self.rootForm.addWidget(self.addObject, 4, 1, 1, 1, Qt.AlignRight) - self.rootForm.addWidget(self.addEntity, 5, 1, 1, 1, Qt.AlignRight) - self.rootForm.setRowStretch(6, 1) - - # Novel Options - self.novelGroup = QGroupBox(self.tr("Populate Novel Folder")) - self.novelForm = QGridLayout() - self.novelGroup.setLayout(self.novelForm) + self.addWorld.setChecked(False) + self.addNotes.setChecked(False) + # Generate Content self.numChapters = QSpinBox() self.numChapters.setRange(0, 100) self.numChapters.setValue(5) @@ -394,37 +350,36 @@ class ProjWizardCustomPage(QWizardPage): self.numScenes.setRange(0, 200) self.numScenes.setValue(5) - self.chFolders = QSwitch() - self.chFolders.setChecked(True) - - self.novelForm.addWidget(QLabel(self.tr("Add chapters")), 0, 0) - self.novelForm.addWidget(QLabel(self.tr("Scenes (per chapter)")), 1, 0) - self.novelForm.addWidget(QLabel(self.tr("Add chapter folders")), 2, 0) - self.novelForm.addWidget(self.numChapters, 0, 1, 1, 1, Qt.AlignRight) - self.novelForm.addWidget(self.numScenes, 1, 1, 1, 1, Qt.AlignRight) - self.novelForm.addWidget(self.chFolders, 2, 1, 1, 1, Qt.AlignRight) - self.novelForm.setRowStretch(3, 1) + # Grid Form + self.addBox = QGridLayout() + self.addBox.addWidget(QLabel(self.tr("Add a folder for plot notes")), 0, 0) + self.addBox.addWidget(QLabel(self.tr("Add a folder for character notes")), 1, 0) + self.addBox.addWidget(QLabel(self.tr("Add a folder for location notes")), 2, 0) + self.addBox.addWidget(QLabel(self.tr("Add example notes to the above")), 3, 0) + self.addBox.addWidget(QLabel(self.tr("Add chapters to the novel folder")), 4, 0) + self.addBox.addWidget(QLabel(self.tr("Add scenes to each chapter")), 5, 0) + self.addBox.addWidget(self.addPlot, 0, 1, 1, 1, Qt.AlignRight) + self.addBox.addWidget(self.addChar, 1, 1, 1, 1, Qt.AlignRight) + self.addBox.addWidget(self.addWorld, 2, 1, 1, 1, Qt.AlignRight) + self.addBox.addWidget(self.addNotes, 3, 1, 1, 1, Qt.AlignRight) + self.addBox.addWidget(self.numChapters, 4, 1, 1, 1, Qt.AlignRight) + self.addBox.addWidget(self.numScenes, 5, 1, 1, 1, Qt.AlignRight) + self.addBox.setRowStretch(6, 1) + self.addBox.setColumnStretch(2, 1) # Wizard Fields self.registerField("addPlot", self.addPlot) self.registerField("addChar", self.addChar) self.registerField("addWorld", self.addWorld) - self.registerField("addTime", self.addTime) - self.registerField("addObject", self.addObject) - self.registerField("addEntity", self.addEntity) + self.registerField("addNotes", self.addNotes) self.registerField("numChapters", self.numChapters) self.registerField("numScenes", self.numScenes) - self.registerField("chFolders", self.chFolders) # Assemble - self.innerBox = QHBoxLayout() - self.innerBox.addWidget(self.rootGroup) - self.innerBox.addWidget(self.novelGroup) - self.outerBox = QVBoxLayout() self.outerBox.setSpacing(vS) self.outerBox.addWidget(self.theText) - self.outerBox.addLayout(self.innerBox) + self.outerBox.addLayout(self.addBox) self.outerBox.addStretch(1) self.setLayout(self.outerBox) From 7a8c97225bb0769fff0bc72e3530e6f58a7067b1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 15:47:21 +0200 Subject: [PATCH 080/179] Add a summary page to the wizard --- novelwriter/tools/projwizard.py | 68 +++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index a5f96d64..0fceb899 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -328,7 +328,9 @@ class ProjWizardCustomPage(QWizardPage): )) self.theText.setWordWrap(True) - vS = self.mainConf.pxInt(12) + cM = self.mainConf.pxInt(12) + mH = self.mainConf.pxInt(26) + fS = self.mainConf.pxInt(4) # Root Folders self.addPlot = QSwitch() @@ -364,8 +366,12 @@ class ProjWizardCustomPage(QWizardPage): self.addBox.addWidget(self.addNotes, 3, 1, 1, 1, Qt.AlignRight) self.addBox.addWidget(self.numChapters, 4, 1, 1, 1, Qt.AlignRight) self.addBox.addWidget(self.numScenes, 5, 1, 1, 1, Qt.AlignRight) - self.addBox.setRowStretch(6, 1) + self.addBox.setVerticalSpacing(fS) + self.addBox.setHorizontalSpacing(cM) + self.addBox.setContentsMargins(cM, 0, cM, 0) self.addBox.setColumnStretch(2, 1) + for i in range(6): + self.addBox.setRowMinimumHeight(i, mH) # Wizard Fields self.registerField("addPlot", self.addPlot) @@ -377,7 +383,7 @@ class ProjWizardCustomPage(QWizardPage): # Assemble self.outerBox = QVBoxLayout() - self.outerBox.setSpacing(vS) + self.outerBox.setSpacing(cM) self.outerBox.addWidget(self.theText) self.outerBox.addLayout(self.addBox) self.outerBox.addStretch(1) @@ -397,14 +403,7 @@ class ProjWizardFinalPage(QWizardPage): self.theWizard = theWizard self.setTitle(self.tr("Finished")) - self.theText = QLabel( - "

%s

%s

" % ( - self.tr("All done."), - self.tr("Press '{0}' to create the new project.").format( - self.tr("Done") if self.mainConf.osDarwin else self.tr("Finish") - ) - ) - ) + self.theText = QLabel("") self.theText.setWordWrap(True) # Assemble @@ -416,4 +415,51 @@ class ProjWizardFinalPage(QWizardPage): return + def initializePage(self): + """Update the summary information on the final page. + """ + QWizardPage.initializePage(self) + + sumList = [] + sumList.append(self.tr("Project Name: {0}").format(self.field("projName"))) + sumList.append(self.tr("Project Path: {0}").format(self.field("projPath"))) + + if self.field("popMinimal"): + sumList.append(self.tr("Fill the project with a minimal set of items")) + elif self.field("popSample"): + sumList.append(self.tr("Fill the project with example files")) + elif self.field("popCustom"): + if self.field("addPlot"): + sumList.append(self.tr("Add a folder for plot notes")) + if self.field("addChar"): + sumList.append(self.tr("Add a folder for character notes")) + if self.field("addWorld"): + sumList.append(self.tr("Add a folder for location notes")) + if self.field("addNotes"): + sumList.append(self.tr("Add example notes to the above")) + if self.field("numChapters") > 0: + sumList.append(self.tr("Add {0} chapters to the novel folder").format( + self.field("numChapters") + )) + if self.field("numScenes") > 0: + sumList.append(self.tr("Add {0} scenes to each chapter").format( + self.field("numScenes") + )) + else: + if self.field("numScenes") > 0: + sumList.append(self.tr("Add {0} scenes").format( + self.field("numScenes") + )) + + self.theText.setText( + "

%s

 • %s

%s

" % ( + self.tr("Summary"), + "
 • ".join(sumList), + self.tr("Press '{0}' to create the new project.").format( + self.tr("Done") if self.mainConf.osDarwin else self.tr("Finish") + ) + ) + ) + return + # END Class ProjWizardFinalPage From 46dce17dfe51bef274b1231fed1e27c66f094e64 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 16:29:41 +0200 Subject: [PATCH 081/179] Update new project generation to match wizard --- novelwriter/core/project.py | 35 +++++++++++++++++++++-------------- novelwriter/guimain.py | 10 ++-------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 26efb323..fa0bddc9 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -295,11 +295,23 @@ class NWProject(): # and a number of chapters and scenes selected in the # wizard's custom page. + noteTitles = { + nwItemClass.PLOT: self.tr("Main Plot"), + nwItemClass.CHARACTER: self.tr("Protagonist"), + nwItemClass.WORLD: self.tr("Main Location"), + } + # Create root folders nHandle = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL) + addNotes = projData.get("addNotes", False) for newRoot in projData.get("addRoots", []): if newRoot in nwItemClass: - self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot) + rHandle = self.newRoot(nwLabels.CLASS_NAME[newRoot], newRoot) + if addNotes: + aHandle = self.newFile(noteTitles[newRoot], rHandle) + ntTag = simplified(noteTitles[newRoot]).replace(" ", "") + aDoc = NWDoc(self, aHandle) + aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n") # Create a title page tHandle = self.newFile(self.tr("Title Page"), nHandle) @@ -310,38 +322,33 @@ class NWProject(): # Create chapters and scenes numChapters = projData.get("numChapters", 0) numScenes = projData.get("numScenes", 0) - chFolders = projData.get("chFolders", False) + + chSynop = self.tr("Summary of the chapter.") + scSynop = self.tr("Summary of the scene.") # Create chapters if numChapters > 0: for ch in range(numChapters): chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}") - pHandle = nHandle - if chFolders: - pHandle = self.newFolder(chTitle, nHandle) - - cHandle = self.newFile(chTitle, pHandle) - + cHandle = self.newFile(chTitle, nHandle) aDoc = NWDoc(self, cHandle) - aDoc.writeDocument("## %s\n\n" % chTitle) + aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n") # Create chapter scenes if numScenes > 0: for sc in range(numScenes): scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}") - sHandle = self.newFile(scTitle, pHandle) - + sHandle = self.newFile(scTitle, cHandle) aDoc = NWDoc(self, sHandle) - aDoc.writeDocument("### %s\n\n" % scTitle) + aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n") # Create scenes (no chapters) elif numScenes > 0: for sc in range(numScenes): scTitle = self.tr("Scene {0}").format(f"{sc+1:d}") sHandle = self.newFile(scTitle, nHandle) - aDoc = NWDoc(self, sHandle) - aDoc.writeDocument("### %s\n\n" % scTitle) + aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n") # Finalise if popCustom or popMinimal: diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ad06cc59..724989f9 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1440,9 +1440,9 @@ class GuiMain(QMainWindow): "popMinimal": newProj.field("popMinimal"), "popCustom": newProj.field("popCustom"), "addRoots": [], + "addNotes": False, "numChapters": 0, "numScenes": 0, - "chFolders": False, } if newProj.field("popCustom"): addRoots = [] @@ -1452,16 +1452,10 @@ class GuiMain(QMainWindow): addRoots.append(nwItemClass.CHARACTER) if newProj.field("addWorld"): addRoots.append(nwItemClass.WORLD) - if newProj.field("addTime"): - addRoots.append(nwItemClass.TIMELINE) - if newProj.field("addObject"): - addRoots.append(nwItemClass.OBJECT) - if newProj.field("addEntity"): - addRoots.append(nwItemClass.ENTITY) projData["addRoots"] = addRoots + projData["addNotes"] = newProj.field("addNotes") projData["numChapters"] = newProj.field("numChapters") projData["numScenes"] = newProj.field("numScenes") - projData["chFolders"] = newProj.field("chFolders") return projData From ad42a714277c578747072d246e5dbab04a76204c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 16:57:09 +0200 Subject: [PATCH 082/179] Fix tests --- novelwriter/core/project.py | 37 ++++---- .../coreProject_NewCustomA_nwProject.nwx | 92 ++++++++----------- .../coreProject_NewCustomB_nwProject.nwx | 64 ++++++------- tests/test_core/test_core_project.py | 14 +-- tests/test_tools/test_tools_projwizard.py | 14 +-- 5 files changed, 96 insertions(+), 125 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index fa0bddc9..d193b4eb 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -295,27 +295,9 @@ class NWProject(): # and a number of chapters and scenes selected in the # wizard's custom page. - noteTitles = { - nwItemClass.PLOT: self.tr("Main Plot"), - nwItemClass.CHARACTER: self.tr("Protagonist"), - nwItemClass.WORLD: self.tr("Main Location"), - } - - # Create root folders + # Create novel folders nHandle = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL) - addNotes = projData.get("addNotes", False) - for newRoot in projData.get("addRoots", []): - if newRoot in nwItemClass: - rHandle = self.newRoot(nwLabels.CLASS_NAME[newRoot], newRoot) - if addNotes: - aHandle = self.newFile(noteTitles[newRoot], rHandle) - ntTag = simplified(noteTitles[newRoot]).replace(" ", "") - aDoc = NWDoc(self, aHandle) - aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n") - - # Create a title page tHandle = self.newFile(self.tr("Title Page"), nHandle) - aDoc = NWDoc(self, tHandle) aDoc.writeDocument(titlePage) @@ -350,6 +332,23 @@ class NWProject(): aDoc = NWDoc(self, sHandle) aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n") + # Create notes folders + noteTitles = { + nwItemClass.PLOT: self.tr("Main Plot"), + nwItemClass.CHARACTER: self.tr("Protagonist"), + nwItemClass.WORLD: self.tr("Main Location"), + } + + addNotes = projData.get("addNotes", False) + for newRoot in projData.get("addRoots", []): + if newRoot in nwItemClass: + rHandle = self.newRoot(nwLabels.CLASS_NAME[newRoot], newRoot) + if addNotes: + aHandle = self.newFile(noteTitles[newRoot], rHandle) + ntTag = simplified(noteTitles[newRoot]).replace(" ", "") + aDoc = NWDoc(self, aHandle) + aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n") + # Finalise if popCustom or popMinimal: self.projOpened = time() diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 14abf9e2..4e201ccd 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -29,7 +29,7 @@
- New + New Note Draft Finished @@ -41,98 +41,86 @@ Main
- + Novel - - - Plot - - - - Characters - - - - Locations - - - - Timeline - - - - Objects - - - - Entities - - + Title Page - - - Chapter 1 - - + Chapter 1 - + Scene 1.1 - + Scene 1.2 - + Scene 1.3 - - - Chapter 2 - - + Chapter 2 - + Scene 2.1 - + Scene 2.2 - + Scene 2.3 - - - Chapter 3 - - + Chapter 3 - + Scene 3.1 - + Scene 3.2 - + Scene 3.3 + + + Plot + + + + Main Plot + + + + Characters + + + + Protagonist + + + + Locations + + + + Main Location +
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index bca6ea80..6b45cbbc 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -46,57 +46,57 @@ Novel
- - - Plot - - - - Characters - - - - Locations - - - - Timeline - - - - Objects - - - - Entities - - + Title Page - + Scene 1 - + Scene 2 - + Scene 3 - + Scene 4 - + Scene 5 - + Scene 6 + + + Plot + + + + Main Plot + + + + Characters + + + + Protagonist + + + + Locations + + + + Main Location +
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 55c18e65..5162e0b9 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -103,13 +103,10 @@ def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd): nwItemClass.PLOT, nwItemClass.CHARACTER, nwItemClass.WORLD, - nwItemClass.TIMELINE, - nwItemClass.OBJECT, - nwItemClass.ENTITY, ], + "addNotes": True, "numChapters": 3, "numScenes": 3, - "chFolders": True, } theProject = NWProject(mockGUI) @@ -144,13 +141,10 @@ def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI, mockRnd): nwItemClass.PLOT, nwItemClass.CHARACTER, nwItemClass.WORLD, - nwItemClass.TIMELINE, - nwItemClass.OBJECT, - nwItemClass.ENTITY, ], + "addNotes": True, "numChapters": 0, "numScenes": 6, - "chFolders": True, } theProject = NWProject(mockGUI) @@ -1178,10 +1172,6 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir): theProject = NWProject(mockGUI) theProject.setProjectPath(fncDir) - # assert theProject.newProject({"projPath": fncDir}) - # assert theProject.saveProject() - # assert theProject.closeProject() - # Check behaviour of deprecated files function on OSError tstFile = os.path.join(fncDir, "ToC.json") writeFile(tstFile, "stuff") diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py index ef4c31cd..8839964e 100644 --- a/tests/test_tools/test_tools_projwizard.py +++ b/tests/test_tools/test_tools_projwizard.py @@ -205,14 +205,11 @@ def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType): customPage.addPlot.setChecked(True) customPage.addChar.setChecked(True) customPage.addWorld.setChecked(True) - customPage.addTime.setChecked(True) - customPage.addObject.setChecked(True) - customPage.addEntity.setChecked(True) + customPage.addNotes.setChecked(True) if prjType == "custom2": customPage.numChapters.setValue(0) customPage.numScenes.setValue(10) - customPage.chFolders.setChecked(False) qtbot.wait(stepDelay) qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) @@ -240,23 +237,20 @@ def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType): nwItemClass.PLOT, nwItemClass.CHARACTER, nwItemClass.WORLD, - nwItemClass.TIMELINE, - nwItemClass.OBJECT, - nwItemClass.ENTITY, ] if prjType == "custom1": assert projData["numChapters"] == 5 assert projData["numScenes"] == 5 - assert projData["chFolders"] + assert projData["addNotes"] is True else: assert projData["numChapters"] == 0 assert projData["numScenes"] == 10 - assert not projData["chFolders"] + assert projData["addNotes"] is True else: assert projData["addRoots"] == [] assert projData["numChapters"] == 0 assert projData["numScenes"] == 0 - assert not projData["chFolders"] + assert projData["addNotes"] is False # Cleanup nwWiz.reject() From b4ea5bc8d05d93e59cac083b077aa36b4f09b091 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 17:33:36 +0200 Subject: [PATCH 083/179] Update minimal project content --- novelwriter/core/project.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index d193b4eb..9cabb53e 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -277,17 +277,16 @@ class NWProject(): xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER) xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD) xHandle[5] = self.newFile(self.tr("Title Page"), xHandle[1]) - xHandle[6] = self.newFolder(self.tr("New Chapter"), xHandle[1]) - xHandle[7] = self.newFile(self.tr("New Chapter"), xHandle[6]) - xHandle[8] = self.newFile(self.tr("New Scene"), xHandle[6]) + xHandle[6] = self.newFile(self.tr("New Chapter"), xHandle[1]) + xHandle[7] = self.newFile(self.tr("New Scene"), xHandle[6]) aDoc = NWDoc(self, xHandle[5]) aDoc.writeDocument(titlePage) - aDoc = NWDoc(self, xHandle[7]) + aDoc = NWDoc(self, xHandle[6]) aDoc.writeDocument("## %s\n\n" % self.tr("New Chapter")) - aDoc = NWDoc(self, xHandle[8]) + aDoc = NWDoc(self, xHandle[7]) aDoc.writeDocument("### %s\n\n" % self.tr("New Scene")) elif popCustom: From 06d39ea35269b60a7b31cadcc27fc7d1b79af657 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 17:33:58 +0200 Subject: [PATCH 084/179] Make tests not depend on minimal project settings --- .../coreProject_NewFile_nwProject.nwx | 5 +- .../coreProject_NewMinimal_nwProject.nwx | 14 ++--- .../coreProject_NewRoot_nwProject.nwx | 5 +- .../guiEditor_Main_Final_nwProject.nwx | 13 ++--- .../guiEditor_Main_Initial_nwProject.nwx | 13 ++--- .../guiProjSettings_Dialog_nwProject.nwx | 10 ++-- tests/test_core/test_core_project.py | 10 ++-- tests/test_dialogs/test_dlg_docmerge.py | 4 +- tests/test_dialogs/test_dlg_docsplit.py | 4 +- tests/test_dialogs/test_dlg_itemeditor.py | 10 ++-- tests/test_dialogs/test_dlg_projsettings.py | 7 +-- tests/test_gui/test_gui_guimain.py | 14 +++-- tests/test_gui/test_gui_mainmenu.py | 4 +- tests/test_gui/test_gui_projtree.py | 8 +-- tests/test_gui/test_gui_statusbar.py | 8 +-- tests/test_tools/test_tools_lipsum.py | 4 +- tests/test_tools/test_tools_writingstats.py | 4 +- tests/tools.py | 54 +++++++++++++++++++ 18 files changed, 124 insertions(+), 67 deletions(-) diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index d253ebcc..19a2f4bf 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,8 +1,9 @@ - + New Project - + New Novel + Jane Doe 2 1 0 diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index a6711a84..68e08580 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,7 +27,7 @@
- New + New Note Draft Finished @@ -39,7 +39,7 @@ Main
- + Novel @@ -60,15 +60,11 @@ Title Page - - - New Chapter - - + New Chapter - + New Scene diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 2ab62301..02930a4e 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,8 +1,9 @@ - + New Project - + New Novel + Jane Doe 2 1 0 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index c4ba847c..a47d454d 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,9 +1,10 @@ - + New Project - - 5 + New Novel + Jane Doe + 4 2 3 @@ -15,8 +16,8 @@ True 000000000000f None - 126 - 99 + 129 + 102 27 @@ -45,7 +46,7 @@ Novel - + Title Page diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 1a5fd5be..03313570 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,9 +1,10 @@ - + New Project - - 3 + New Novel + Jane Doe + 2 1 0 @@ -15,8 +16,8 @@ True None None - 6 - 6 + 9 + 9 0 @@ -45,7 +46,7 @@ Novel - + Title Page diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index 326c63e5..f12c1dc2 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,11 +1,11 @@ - + Project Name Project Title Jane Doe John Doh - 2 + 1 1 0 @@ -17,8 +17,8 @@ True None None - 6 - 6 + 9 + 9 0 B @@ -51,7 +51,7 @@ Novel - + Title Page diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 5162e0b9..df0638fd 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -26,7 +26,7 @@ from shutil import copyfile from zipfile import ZipFile from lxml import etree -from tools import cmpFiles, writeFile, readFile +from tools import cmpFiles, writeFile, readFile, buildTestProject from mock import causeOSError from novelwriter.core.project import NWProject @@ -252,8 +252,8 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd): compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx") theProject = NWProject(mockGUI) + buildTestProject(theProject, fncDir) - assert theProject.newProject({"projPath": fncDir}) is True assert theProject.setProjectPath(fncDir) is True assert theProject.saveProject() is True assert theProject.closeProject() is True @@ -288,8 +288,8 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx") theProject = NWProject(mockGUI) + buildTestProject(theProject, fncDir) - assert theProject.newProject({"projPath": fncDir}) is True assert theProject.setProjectPath(fncDir) is True assert theProject.saveProject() is True assert theProject.closeProject() is True @@ -671,7 +671,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): """Test the status and importance flag handling. """ theProject = NWProject(mockGUI) - assert theProject.newProject({"projPath": fncDir}) is True + buildTestProject(theProject, fncDir) statusKeys = ["s000008", "s000009", "s00000a", "s00000b"] importKeys = ["i00000c", "i00000d", "i00000e", "i00000f"] @@ -792,7 +792,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): """Test other project class methods and functions. """ theProject = NWProject(mockGUI) - assert theProject.newProject({"projPath": fncDir}) is True + buildTestProject(theProject, fncDir) # Setting project path assert theProject.setProjectPath(None) diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index f635be4a..a82c77bd 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -23,7 +23,7 @@ import os import pytest from mock import causeOSError -from tools import getGuiItem, readFile, writeFile +from tools import getGuiItem, readFile, writeFile, buildTestProject from PyQt5.QtWidgets import QAction, QMessageBox, QDialog @@ -41,7 +41,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) # Create a new project - assert nwGUI.newProject({"projPath": fncProj}) + buildTestProject(nwGUI, fncProj) # Handles for new objects hNovelRoot = "0000000000008" diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 19988c0b..90e3375d 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -23,7 +23,7 @@ import os import pytest from mock import causeOSError -from tools import getGuiItem, readFile, writeFile +from tools import getGuiItem, readFile, writeFile, buildTestProject from PyQt5.QtWidgets import QAction, QMessageBox, QDialog @@ -42,7 +42,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) # Create a new project - assert nwGUI.newProject({"projPath": fncProj}) is True + buildTestProject(nwGUI, fncProj) # Handles for new objects hNovelRoot = "0000000000008" diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index ebd71d6c..c221138e 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -21,7 +21,7 @@ along with this program. If not, see . import pytest -from tools import getGuiItem +from tools import getGuiItem, buildTestProject from PyQt5.QtWidgets import QAction, QDialog, QMessageBox @@ -48,7 +48,7 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.editItem() is False # Create and Open Project - assert nwGUI.newProject({"projPath": fncProj}) + buildTestProject(nwGUI, fncProj) tHandle = "000000000000f" # No Selection @@ -99,7 +99,7 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Create Project and Open Document - assert nwGUI.newProject({"projPath": fncProj}) + buildTestProject(nwGUI, fncProj) tHandle = "000000000000f" assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" @@ -156,7 +156,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Create Project and Open Document - assert nwGUI.newProject({"projPath": fncProj}) + buildTestProject(nwGUI, fncProj) assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" assert nwGUI.theProject.importItems.name(importKeys[0]) == "New" @@ -209,7 +209,7 @@ def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Create Project and Open Document - assert nwGUI.newProject({"projPath": fncProj}) + buildTestProject(nwGUI, fncProj) # Edit a Folder itemEdit = GuiItemEditor(nwGUI, "000000000000d") diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index 54827cfd..f193194a 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -23,7 +23,7 @@ import os import pytest from shutil import copyfile -from tools import cmpFiles, getGuiItem +from tools import cmpFiles, getGuiItem, buildTestProject from PyQt5.QtGui import QColor from PyQt5.QtCore import Qt @@ -57,7 +57,7 @@ def testDlgProjSettings_Dialog( assert getGuiItem("GuiProjectSettings") is None # Create new project - assert nwGUI.newProject({"projPath": fncProj}) + buildTestProject(nwGUI, fncProj) nwGUI.mainConf.backupPath = fncDir nwGUI.theProject.setSpellLang("en") @@ -81,7 +81,7 @@ def testDlgProjSettings_Dialog( # ============ assert projEdit.tabMain.editName.text() == "New Project" - assert projEdit.tabMain.editTitle.text() == "" + assert projEdit.tabMain.editTitle.text() == "New Novel" assert projEdit.tabMain.editAuthors.toPlainText() == "Jane Smith\nJohn Smith" assert projEdit.tabMain.spellLang.currentData() == "en" assert projEdit.tabMain.doBackup.isChecked() is False @@ -90,6 +90,7 @@ def testDlgProjSettings_Dialog( projEdit.tabMain.editName.setText("") for c in "Project Name": qtbot.keyClick(projEdit.tabMain.editName, c, delay=typeDelay) + projEdit.tabMain.editTitle.setText("") for c in "Project Title": qtbot.keyClick(projEdit.tabMain.editTitle, c, delay=typeDelay) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index ddb4f09b..04d43d7f 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -23,7 +23,7 @@ import os import pytest from shutil import copyfile -from tools import cmpFiles +from tools import cmpFiles, buildTestProject from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QDialog @@ -76,9 +76,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - assert nwGUI.newProject({"projPath": fncProj}) is True - assert nwGUI.saveProject() is True - # assert False + buildTestProject(nwGUI, fncProj) sHandle = "000000000000f" assert nwGUI.openSelectedItem() is False @@ -139,7 +137,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) # Create new, save, close project - assert nwGUI.newProject({"projPath": fncProj}) + buildTestProject(nwGUI, fncProj) assert nwGUI.saveProject() assert nwGUI.closeProject() @@ -178,9 +176,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta") assert nwGUI.theProject.projFile == "nwProject.nwx" assert nwGUI.theProject.projName == "New Project" - assert nwGUI.theProject.bookTitle == "" - assert len(nwGUI.theProject.bookAuthors) == 0 - assert not nwGUI.theProject.spellCheck + assert nwGUI.theProject.bookTitle == "New Novel" + assert len(nwGUI.theProject.bookAuthors) == 1 + assert nwGUI.theProject.spellCheck is False # Check that tree items have been created assert nwGUI.treeView._getTreeItem("0000000000008") is not None diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 815c8566..801abd68 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextCursor, QTextBlock from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox -from tools import writeFile +from tools import writeFile, buildTestProject from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.enum import nwDocAction, nwDocInsert @@ -465,7 +465,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) - assert nwGUI.newProject({"projPath": fncProj}) + buildTestProject(nwGUI, fncProj) assert nwGUI.treeView._getTreeItem("000000000000f") is not None assert nwGUI.openDocument("000000000000f") is True diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index af851138..5d3354f9 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -22,6 +22,8 @@ along with this program. If not, see . import pytest import os +from tools import buildTestProject + from PyQt5.QtWidgets import QAction, QMessageBox from novelwriter.guimain import GuiMain @@ -47,7 +49,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # Create a project prjDir = os.path.join(fncDir, "project") - assert nwGUI.newProject({"projPath": prjDir}) is True + buildTestProject(nwGUI, prjDir) # No itemType set nwTree.clearSelection() @@ -155,7 +157,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Create a project prjDir = os.path.join(fncDir, "project") - assert nwGUI.newProject({"projPath": prjDir}) is True + buildTestProject(nwGUI, prjDir) # Move Documents # ============== @@ -283,7 +285,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Create a project prjDir = os.path.join(fncDir, "project") - assert nwGUI.newProject({"projPath": prjDir}) is True + buildTestProject(nwGUI, prjDir) # Try emptying the trash already now, when there is no trash folder assert nwTree.emptyTrash() is False diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 8993a2e8..51e9c70b 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -22,6 +22,8 @@ along with this program. If not, see . import time import pytest +from tools import buildTestProject + from PyQt5.QtWidgets import QMessageBox from novelwriter.core import NWDoc @@ -34,7 +36,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - assert nwGUI.newProject({"projPath": fncProj}) is True + buildTestProject(nwGUI, fncProj) cHandle = nwGUI.theProject.newFile("A Note", "000000000000a") newDoc = NWDoc(nwGUI.theProject, cHandle) newDoc.writeDocument("# A Note\n\n") @@ -89,10 +91,10 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Project Stats nwGUI.statusBar.mainConf.incNotesWCount = False nwGUI._updateStatusWordCount() - assert nwGUI.statusBar.statsText.text() == "Words: 6 (+6)" + assert nwGUI.statusBar.statsText.text() == "Words: 9 (+9)" nwGUI.statusBar.mainConf.incNotesWCount = True nwGUI._updateStatusWordCount() - assert nwGUI.statusBar.statsText.text() == "Words: 8 (+8)" + assert nwGUI.statusBar.statsText.text() == "Words: 11 (+11)" # qtbot.stopForInteraction() diff --git a/tests/test_tools/test_tools_lipsum.py b/tests/test_tools/test_tools_lipsum.py index 7d6c08f9..629deb56 100644 --- a/tests/test_tools/test_tools_lipsum.py +++ b/tests/test_tools/test_tools_lipsum.py @@ -21,7 +21,7 @@ along with this program. If not, see . import pytest -from tools import getGuiItem +from tools import getGuiItem, buildTestProject from PyQt5.QtWidgets import QAction, QMessageBox @@ -40,7 +40,7 @@ def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert getGuiItem("GuiLipsum") is None # Create a new project - assert nwGUI.newProject({"projPath": fncProj}) is True + buildTestProject(nwGUI, fncProj) assert nwGUI.openDocument("000000000000f") is True assert len(nwGUI.docEditor.getText()) == 15 diff --git a/tests/test_tools/test_tools_writingstats.py b/tests/test_tools/test_tools_writingstats.py index cffef9e6..b7358b20 100644 --- a/tests/test_tools/test_tools_writingstats.py +++ b/tests/test_tools/test_tools_writingstats.py @@ -23,8 +23,8 @@ import pytest import json import os -from tools import getGuiItem, writeFile from mock import causeOSError +from tools import getGuiItem, writeFile, buildTestProject from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox @@ -48,7 +48,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) # Create a project to work on - assert nwGUI.newProject({"projPath": fncProj}) + buildTestProject(nwGUI, fncProj) qtbot.wait(100) assert nwGUI.saveProject() sessFile = os.path.join(fncProj, "meta", nwFiles.SESS_STATS) diff --git a/tests/tools.py b/tests/tools.py index bd417aae..a8e2c774 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -20,6 +20,7 @@ along with this program. If not, see . """ import os +import time import shutil from PyQt5.QtWidgets import qApp @@ -119,3 +120,56 @@ def cleanProject(projPath): os.unlink(tocFile) return + + +def buildTestProject(theObject, projPath): + """Build a standard test project in projPath using theProject + object as the parent. + """ + from novelwriter.enum import nwItemClass + from novelwriter.core import NWProject, NWDoc + + if isinstance(theObject, NWProject): + theGUI = None + theProject = theObject + else: + theGUI = theObject + theProject = theObject.theProject + + theProject.clearProject() + theProject.setProjectPath(projPath, newProject=True) + theProject.setProjectName("New Project") + theProject.setBookTitle("New Novel") + theProject.setBookAuthors("Jane Doe") + + # Creating a minimal project with a few root folders and a + # single chapter folder with a single file. + xHandle = {} + xHandle[1] = theProject.newRoot(theProject.tr("Novel"), nwItemClass.NOVEL) + xHandle[2] = theProject.newRoot(theProject.tr("Plot"), nwItemClass.PLOT) + xHandle[3] = theProject.newRoot(theProject.tr("Characters"), nwItemClass.CHARACTER) + xHandle[4] = theProject.newRoot(theProject.tr("World"), nwItemClass.WORLD) + xHandle[5] = theProject.newFile(theProject.tr("Title Page"), xHandle[1]) + xHandle[6] = theProject.newFolder(theProject.tr("New Chapter"), xHandle[1]) + xHandle[7] = theProject.newFile(theProject.tr("New Chapter"), xHandle[6]) + xHandle[8] = theProject.newFile(theProject.tr("New Scene"), xHandle[6]) + + aDoc = NWDoc(theProject, xHandle[5]) + aDoc.writeDocument("#! New Novel\n\n>> By Jane DOe <<\n") + + aDoc = NWDoc(theProject, xHandle[7]) + aDoc.writeDocument("## %s\n\n" % theProject.tr("New Chapter")) + + aDoc = NWDoc(theProject, xHandle[8]) + aDoc.writeDocument("### %s\n\n" % theProject.tr("New Scene")) + + theProject.projOpened = time.time() + theProject.setProjectChanged(True) + theProject.saveProject(autoSave=True) + + if theGUI is not None: + theGUI.hasProject = True + theGUI.rebuildTrees() + theGUI.rebuildIndex(beQuiet=True) + + return From cea22b6147ab5456d0ebd67475a62fe38785ec2b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 17:58:47 +0200 Subject: [PATCH 085/179] Clean up new project creation function --- novelwriter/core/project.py | 51 ++++++++++--------- .../coreProject_NewCustomA_nwProject.nwx | 16 ++++-- .../coreProject_NewCustomB_nwProject.nwx | 16 ++++-- .../coreProject_NewMinimal_nwProject.nwx | 40 ++++++++------- 4 files changed, 73 insertions(+), 50 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 9cabb53e..0557ce6e 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -264,42 +264,41 @@ class NWProject(): self.setBookTitle(projTitle) self.setBookAuthors(projAuthors) + hNovelRoot = self.newRoot( + trConst(nwLabels.CLASS_NAME[nwItemClass.NOVEL]), nwItemClass.NOVEL + ) + titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self.projName) if self.bookAuthors: titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors()) + hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot) + aDoc = NWDoc(self, hTitlePage) + aDoc.writeDocument(titlePage) + if popMinimal: # Creating a minimal project with a few root folders and a - # single chapter folder with a single file. - xHandle = {} - xHandle[1] = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL) - xHandle[2] = self.newRoot(self.tr("Plot"), nwItemClass.PLOT) - xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER) - xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD) - xHandle[5] = self.newFile(self.tr("Title Page"), xHandle[1]) - xHandle[6] = self.newFile(self.tr("New Chapter"), xHandle[1]) - xHandle[7] = self.newFile(self.tr("New Scene"), xHandle[6]) - - aDoc = NWDoc(self, xHandle[5]) - aDoc.writeDocument(titlePage) - - aDoc = NWDoc(self, xHandle[6]) + # single chapter with a single scene. + hChapter = self.newFile(self.tr("New Chapter"), hNovelRoot) + aDoc = NWDoc(self, hChapter) aDoc.writeDocument("## %s\n\n" % self.tr("New Chapter")) - aDoc = NWDoc(self, xHandle[7]) + hScene = self.newFile(self.tr("New Scene"), hChapter) + aDoc = NWDoc(self, hScene) aDoc.writeDocument("### %s\n\n" % self.tr("New Scene")) + minClasses = [ + nwItemClass.PLOT, nwItemClass.CHARACTER, + nwItemClass.WORLD, nwItemClass.ARCHIVE + ] + for minClass in minClasses: + self.newRoot(trConst(nwLabels.CLASS_NAME[minClass]), minClass) + elif popCustom: # Create a project structure based on selected root folders # and a number of chapters and scenes selected in the # wizard's custom page. - # Create novel folders - nHandle = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL) - tHandle = self.newFile(self.tr("Title Page"), nHandle) - aDoc = NWDoc(self, tHandle) - aDoc.writeDocument(titlePage) - # Create chapters and scenes numChapters = projData.get("numChapters", 0) numScenes = projData.get("numScenes", 0) @@ -311,7 +310,7 @@ class NWProject(): if numChapters > 0: for ch in range(numChapters): chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}") - cHandle = self.newFile(chTitle, nHandle) + cHandle = self.newFile(chTitle, hNovelRoot) aDoc = NWDoc(self, cHandle) aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n") @@ -327,7 +326,7 @@ class NWProject(): elif numScenes > 0: for sc in range(numScenes): scTitle = self.tr("Scene {0}").format(f"{sc+1:d}") - sHandle = self.newFile(scTitle, nHandle) + sHandle = self.newFile(scTitle, hNovelRoot) aDoc = NWDoc(self, sHandle) aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n") @@ -341,13 +340,17 @@ class NWProject(): addNotes = projData.get("addNotes", False) for newRoot in projData.get("addRoots", []): if newRoot in nwItemClass: - rHandle = self.newRoot(nwLabels.CLASS_NAME[newRoot], newRoot) + rHandle = self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot) if addNotes: aHandle = self.newFile(noteTitles[newRoot], rHandle) ntTag = simplified(noteTitles[newRoot]).replace(" ", "") aDoc = NWDoc(self, aHandle) aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n") + # Also add the archive and trash folders + self.newRoot(trConst(nwLabels.CLASS_NAME[nwItemClass.ARCHIVE]), nwItemClass.ARCHIVE) + self.trashFolder() + # Finalise if popCustom or popMinimal: self.projOpened = time() diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 4e201ccd..be4845bf 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -29,19 +29,19 @@
- New + New Note Draft Finished - New + New Minor Major Main
- + Novel @@ -122,5 +122,13 @@ Main Location + + + Archive + + + + Trash +
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 6b45cbbc..12f911cd 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -29,19 +29,19 @@
- New + New Note Draft Finished - New + New Minor Major Main - + Novel @@ -98,5 +98,13 @@ Main Location + + + Archive + + + + Trash +
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 68e08580..422bb6b2 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,7 +27,7 @@
- New + New Note Draft Finished @@ -39,34 +39,38 @@ Main - + Novel - - - Plot - - - - Characters - - - - World - - + Title Page - + New Chapter - + New Scene + + + Plot + + + + Characters + + + + Locations + + + + Archive +
From 63a3e56cdb2da35c95c5942bd731032ef5a350d8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 18:15:38 +0200 Subject: [PATCH 086/179] Fix ignored lines in XML comparison --- tests/test_core/test_core_project.py | 18 +++++++++--------- tests/test_gui/test_gui_guimain.py | 6 +++--- tests/tools.py | 2 ++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index df0638fd..4f71b787 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -26,7 +26,7 @@ from shutil import copyfile from zipfile import ZipFile from lxml import etree -from tools import cmpFiles, writeFile, readFile, buildTestProject +from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE from mock import causeOSError from novelwriter.core.project import NWProject @@ -67,7 +67,7 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.saveProject() is True assert theProject.closeProject() is True copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert theProject.projChanged is False # Open a second time @@ -77,7 +77,7 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.saveProject() is True assert theProject.closeProject() is True copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) # END Test testCoreProject_NewMinimal @@ -115,7 +115,7 @@ def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.closeProject() is True copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) # END Test testCoreProject_NewCustomA @@ -153,7 +153,7 @@ def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.closeProject() is True copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) # END Test testCoreProject_NewCustomB @@ -273,7 +273,7 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.closeProject() is True copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert theProject.projChanged is False # END Test testCoreProject_NewRoot @@ -302,7 +302,7 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.closeProject() is True copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert theProject.projChanged is False # END Test testCoreProject_NewFile @@ -483,7 +483,7 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir): assert theProject.saveProject() is True assert theProject.saveCount == saveCount + 1 assert theProject.autoCount == autoCount - assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) # Check that a second save creates a .bak file assert os.path.isfile(backFile) is True @@ -494,7 +494,7 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir): assert theProject.saveProject(autoSave=True) is True assert theProject.saveCount == saveCount assert theProject.autoCount == autoCount + 1 - assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) # Close test project assert theProject.closeProject() diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 04d43d7f..a52e486e 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -23,7 +23,7 @@ import os import pytest from shutil import copyfile -from tools import cmpFiles, buildTestProject +from tools import cmpFiles, buildTestProject, XML_IGNORE from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QDialog @@ -158,7 +158,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock testFile = os.path.join(outDir, "guiEditor_Main_Initial_nwProject.nwx") compFile = os.path.join(refDir, "guiEditor_Main_Initial_nwProject.nwx") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) qtbot.wait(stepDelay) # qtbot.stopForInteraction() @@ -436,7 +436,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx") compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) projFile = os.path.join(fncProj, "content", "000000000000f.nwd") testFile = os.path.join(outDir, "guiEditor_Main_Final_000000000000f.nwd") diff --git a/tests/tools.py b/tests/tools.py index a8e2c774..bfdb5140 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -25,6 +25,8 @@ import shutil from PyQt5.QtWidgets import qApp +XML_IGNORE = (" Date: Sat, 21 May 2022 21:55:19 +0200 Subject: [PATCH 087/179] Update test coverage --- tests/test_gui/test_gui_guimain.py | 91 +++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index a52e486e..e0d7e2ec 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -23,7 +23,7 @@ import os import pytest from shutil import copyfile -from tools import cmpFiles, buildTestProject, XML_IGNORE +from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QDialog @@ -32,6 +32,7 @@ from novelwriter.gui import ( GuiDocEditor, GuiProjectTree, GuiNovelTree, GuiOutline ) from novelwriter.enum import nwItemType, nwWidget +from novelwriter.tools import GuiProjectWizard from novelwriter.dialogs.itemeditor import GuiItemEditor keyDelay = 2 @@ -70,6 +71,45 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI): # END Test testGuiMain_NoProject +@pytest.mark.gui +def testGuiMain_NewProject(monkeypatch, nwGUI, fncProj): + """Test creating a new project. + """ + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + + # No data + with monkeypatch.context() as mp: + mp.setattr(GuiProjectWizard, "exec_", lambda *a: None) + assert nwGUI.newProject(projData=None) is False + + # Close project + with monkeypatch.context() as mp: + nwGUI.hasProject = True + mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + assert nwGUI.newProject(projData={"projPath": fncProj}) is False + + # No project path + assert nwGUI.newProject(projData={}) is False + + # Project file already exists + projFile = os.path.join(fncProj, nwGUI.theProject.projFile) + writeFile(projFile, "Stuff") + assert nwGUI.newProject(projData={"projPath": fncProj}) is False + os.unlink(projFile) + + # An unreachable path should also fail + projPath = os.path.join(fncProj, "stuff", "stuff", "stuff") + assert nwGUI.newProject(projData={"projPath": projPath}) is False + + # This one should work just fine + assert nwGUI.newProject(projData={"projPath": fncProj}) is True + assert os.path.isfile(os.path.join(fncProj, nwGUI.theProject.projFile)) + assert os.path.isdir(os.path.join(fncProj, "content")) + +# END Test testGuiMain_NewProject + + @pytest.mark.gui def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test handling of project tree items based on GUI focus states. @@ -465,3 +505,52 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # qtbot.stopForInteraction() # END Test testGuiMain_Editing + + +@pytest.mark.gui +def testGuiMain_FocusFullMode(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): + """Test toggling focus mode in main window. + """ + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + + buildTestProject(nwGUI, fncProj) + assert nwGUI.isFocusMode is False + + # Focus Mode + # ========== + + # No document open, so not allowing focus mode + assert nwGUI.toggleFocusMode() is False + + # Open a file in editor and viewer + assert nwGUI.openDocument("000000000000f") + assert nwGUI.viewDocument("000000000000f") + + # Enable focus mode + assert nwGUI.toggleFocusMode() is True + assert nwGUI.treePane.isVisible() is False + assert nwGUI.statusBar.isVisible() is False + assert nwGUI.mainMenu.isVisible() is False + assert nwGUI.viewsBar.isVisible() is False + assert nwGUI.splitView.isVisible() is False + + # Disable focus mode + assert nwGUI.toggleFocusMode() is True + assert nwGUI.treePane.isVisible() is True + assert nwGUI.statusBar.isVisible() is True + assert nwGUI.mainMenu.isVisible() is True + assert nwGUI.viewsBar.isVisible() is True + assert nwGUI.splitView.isVisible() is True + + # Full Screen Mode + # ================ + + assert nwGUI.mainConf.isFullScreen is False + nwGUI.toggleFullScreenMode() + assert nwGUI.mainConf.isFullScreen is True + nwGUI.toggleFullScreenMode() + assert nwGUI.mainConf.isFullScreen is False + + # qtbot.stopForInteraction() + +# END Test testGuiMain_FocusFullMode From 78b97ce8fb9213c80be379984c1930f3b3c42aff Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 22:10:25 +0200 Subject: [PATCH 088/179] Remove name as a required parameter for new root folders since no part of the GUI uses it --- novelwriter/core/project.py | 42 +++++++++---------- novelwriter/gui/projtree.py | 5 +-- .../coreProject_NewRoot_nwProject.nwx | 12 +++--- tests/test_core/test_core_index.py | 2 +- tests/test_core/test_core_project.py | 16 +++---- tests/tools.py | 16 +++---- 6 files changed, 44 insertions(+), 49 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 0557ce6e..3341e578 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -120,32 +120,34 @@ class NWProject(): # Item Methods ## - def newRoot(self, rootName, rootClass): - """Add a new root item. + def newRoot(self, itemClass, label=None): + """Add a new root item. If label is None, use the class label. """ + if label is None: + label = trConst(nwLabels.CLASS_NAME[itemClass]) newItem = NWItem(self) - newItem.setName(rootName) + newItem.setName(label) newItem.setType(nwItemType.ROOT) - newItem.setClass(rootClass) + newItem.setClass(itemClass) self.projTree.append(None, None, newItem) self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle - def newFolder(self, folderName, pHandle): - """Add a new folder with a given name and parent item. + def newFolder(self, label, pHandle): + """Add a new folder with a given label and parent item. """ newItem = NWItem(self) - newItem.setName(folderName) + newItem.setName(label) newItem.setType(nwItemType.FOLDER) self.projTree.append(None, pHandle, newItem) self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle - def newFile(self, fileName, pHandle): - """Add a new file with a given name and parent item. + def newFile(self, label, pHandle): + """Add a new file with a given label and parent item. """ newItem = NWItem(self) - newItem.setName(fileName) + newItem.setName(label) newItem.setType(nwItemType.FILE) self.projTree.append(None, pHandle, newItem) self.projTree.updateItemData(newItem.itemHandle) @@ -264,15 +266,13 @@ class NWProject(): self.setBookTitle(projTitle) self.setBookAuthors(projAuthors) - hNovelRoot = self.newRoot( - trConst(nwLabels.CLASS_NAME[nwItemClass.NOVEL]), nwItemClass.NOVEL - ) + hNovelRoot = self.newRoot(nwItemClass.NOVEL) + hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot) titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self.projName) if self.bookAuthors: titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors()) - hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot) aDoc = NWDoc(self, hTitlePage) aDoc.writeDocument(titlePage) @@ -287,12 +287,10 @@ class NWProject(): aDoc = NWDoc(self, hScene) aDoc.writeDocument("### %s\n\n" % self.tr("New Scene")) - minClasses = [ - nwItemClass.PLOT, nwItemClass.CHARACTER, - nwItemClass.WORLD, nwItemClass.ARCHIVE - ] - for minClass in minClasses: - self.newRoot(trConst(nwLabels.CLASS_NAME[minClass]), minClass) + self.newRoot(nwItemClass.PLOT) + self.newRoot(nwItemClass.CHARACTER) + self.newRoot(nwItemClass.WORLD) + self.newRoot(nwItemClass.ARCHIVE) elif popCustom: # Create a project structure based on selected root folders @@ -340,7 +338,7 @@ class NWProject(): addNotes = projData.get("addNotes", False) for newRoot in projData.get("addRoots", []): if newRoot in nwItemClass: - rHandle = self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot) + rHandle = self.newRoot(newRoot) if addNotes: aHandle = self.newFile(noteTitles[newRoot], rHandle) ntTag = simplified(noteTitles[newRoot]).replace(" ", "") @@ -348,7 +346,7 @@ class NWProject(): aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n") # Also add the archive and trash folders - self.newRoot(trConst(nwLabels.CLASS_NAME[nwItemClass.ARCHIVE]), nwItemClass.ARCHIVE) + self.newRoot(nwItemClass.ARCHIVE) self.trashFolder() # Finalise diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 0339ec5f..5d40aeae 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -37,7 +37,6 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) @@ -176,9 +175,7 @@ class GuiProjectTree(QTreeWidget): if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass): - tHandle = self.theProject.newRoot( - trConst(nwLabels.CLASS_NAME[itemClass]), itemClass - ) + tHandle = self.theProject.newRoot(itemClass) elif itemType in (nwItemType.FILE, nwItemType.FOLDER): diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 02930a4e..cd25c1cf 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -83,11 +83,11 @@ - Character + Characters - World + Locations @@ -95,15 +95,15 @@ - Object + Objects - Custom1 + Custom - Custom2 + Custom diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index c3e6ef47..93ec35c6 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -280,7 +280,7 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theIndex.scanText(xHandle, "Hello World!") is False # Create the archive root - aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE) + aHandle = theProject.newRoot(nwItemClass.ARCHIVE) assert theProject.projTree[aHandle] is not None xItem.setParent(aHandle) theProject.projTree.updateItemData(xItem.itemHandle) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 4f71b787..e6e5092d 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -259,14 +259,14 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.closeProject() is True assert theProject.openProject(projFile) is True - assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), str) - assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), str) - assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), str) - assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), str) - assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str) - assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str) - assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str) - assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str) + assert isinstance(theProject.newRoot(nwItemClass.NOVEL), str) + assert isinstance(theProject.newRoot(nwItemClass.PLOT), str) + assert isinstance(theProject.newRoot(nwItemClass.CHARACTER), str) + assert isinstance(theProject.newRoot(nwItemClass.WORLD), str) + assert isinstance(theProject.newRoot(nwItemClass.TIMELINE), str) + assert isinstance(theProject.newRoot(nwItemClass.OBJECT), str) + assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str) + assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str) assert theProject.projChanged is True assert theProject.saveProject() is True diff --git a/tests/tools.py b/tests/tools.py index bfdb5140..fcdd3817 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -147,14 +147,14 @@ def buildTestProject(theObject, projPath): # Creating a minimal project with a few root folders and a # single chapter folder with a single file. xHandle = {} - xHandle[1] = theProject.newRoot(theProject.tr("Novel"), nwItemClass.NOVEL) - xHandle[2] = theProject.newRoot(theProject.tr("Plot"), nwItemClass.PLOT) - xHandle[3] = theProject.newRoot(theProject.tr("Characters"), nwItemClass.CHARACTER) - xHandle[4] = theProject.newRoot(theProject.tr("World"), nwItemClass.WORLD) - xHandle[5] = theProject.newFile(theProject.tr("Title Page"), xHandle[1]) - xHandle[6] = theProject.newFolder(theProject.tr("New Chapter"), xHandle[1]) - xHandle[7] = theProject.newFile(theProject.tr("New Chapter"), xHandle[6]) - xHandle[8] = theProject.newFile(theProject.tr("New Scene"), xHandle[6]) + xHandle[1] = theProject.newRoot(nwItemClass.NOVEL, "Novel") + xHandle[2] = theProject.newRoot(nwItemClass.PLOT, "Plot") + xHandle[3] = theProject.newRoot(nwItemClass.CHARACTER, "Characters") + xHandle[4] = theProject.newRoot(nwItemClass.WORLD, "World") + xHandle[5] = theProject.newFile("Title Page", xHandle[1]) + xHandle[6] = theProject.newFolder("New Chapter", xHandle[1]) + xHandle[7] = theProject.newFile("New Chapter", xHandle[6]) + xHandle[8] = theProject.newFile("New Scene", xHandle[6]) aDoc = NWDoc(theProject, xHandle[5]) aDoc.writeDocument("#! New Novel\n\n>> By Jane DOe <<\n") From e14348a05bb6a69d6a1f873921f7d86b6df45605 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 21 May 2022 22:18:16 +0200 Subject: [PATCH 089/179] Fix a few text labels on the wizard --- novelwriter/tools/projwizard.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index 0fceb899..765445b9 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -97,7 +97,7 @@ class ProjWizardIntroPage(QWizardPage): self.setTitle(self.tr("Create New Project")) self.theText = QLabel(self.tr( "Provide at least a project name. The project name should not " - "be change beyond this point as it is used for generating file " + "be changed beyond this point as it is used for generating file " "names for for instance backups. The other fields are optional " "and can be changed at any time in Project Settings." )) @@ -402,7 +402,7 @@ class ProjWizardFinalPage(QWizardPage): self.mainConf = novelwriter.CONFIG self.theWizard = theWizard - self.setTitle(self.tr("Finished")) + self.setTitle(self.tr("Summary")) self.theText = QLabel("") self.theText.setWordWrap(True) @@ -452,8 +452,8 @@ class ProjWizardFinalPage(QWizardPage): )) self.theText.setText( - "

%s

 • %s

%s

" % ( - self.tr("Summary"), + "

%s

 • %s

%s

" % ( + self.tr("You have selected the following:"), "
 • ".join(sumList), self.tr("Press '{0}' to create the new project.").format( self.tr("Done") if self.mainConf.osDarwin else self.tr("Finish") From a541c906901ca1c638895063090a040090d1a98f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 15:58:09 +0200 Subject: [PATCH 090/179] Add icon name to xdg mime type just in case (#1068) --- setup/data/x-novelwriter-project.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/setup/data/x-novelwriter-project.xml b/setup/data/x-novelwriter-project.xml index 789c08fd..db045446 100644 --- a/setup/data/x-novelwriter-project.xml +++ b/setup/data/x-novelwriter-project.xml @@ -4,5 +4,6 @@ novelWriter Project + From f7e31bd4527bc4c93365c81085d3624b633b6063 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 16:23:49 +0200 Subject: [PATCH 091/179] Use "project name" consistently on the GUI --- novelwriter/core/project.py | 6 +++--- novelwriter/dialogs/projsettings.py | 2 +- novelwriter/guimain.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 3341e578..b791f545 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -87,7 +87,7 @@ class NWProject(): self.projFiles = [] # A list of all files in the content folder on load # Project Meta - self.projName = "" # Project name (working title) + self.projName = "" # Project name self.bookTitle = "" # The final title; should only be used for exports self.bookAuthors = [] # A list of book authors @@ -956,8 +956,8 @@ class NWProject(): return True def setProjectName(self, projName): - """Set the project name (working title), This is the the title - used for backup files etc. + """Set the project name, This is the the name used for backup + files etc. """ self.projName = simplified(projName) self.setProjectChanged(True) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 2a2bb969..927f4ddd 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -187,7 +187,7 @@ class GuiProjectEditMain(QWidget): self.editName.setMaximumWidth(xW) self.editName.setText(self.theProject.projName) self.mainForm.addRow( - self.tr("Working title"), + self.tr("Project name"), self.editName, self.tr("Should be set only once.") ) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 724989f9..a99375d7 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1397,7 +1397,7 @@ class GuiMain(QMainWindow): return True def _updateWindowTitle(self, projName=None): - """Set the window title and add the project's working title. + """Set the window title and add the project's name. """ winTitle = self.mainConf.appName if projName is not None: From 4a4ed32fe611d3107418745edfd514e039dbbac9 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 17:00:09 +0200 Subject: [PATCH 092/179] Add new view bar icons --- .../assets/icons/typicons_dark/icons.conf | 3 +- .../assets/icons/typicons_dark/mixed_edit.svg | 52 +++++++++++++++++++ .../assets/icons/typicons_dark/typ_edit.svg | 31 ----------- .../assets/icons/typicons_dark/typ_export.svg | 16 ++++++ .../assets/icons/typicons_light/icons.conf | 3 +- .../icons/typicons_light/mixed_edit.svg | 52 +++++++++++++++++++ .../assets/icons/typicons_light/typ_edit.svg | 31 ----------- .../icons/typicons_light/typ_export.svg | 38 ++++++++++++++ 8 files changed, 162 insertions(+), 64 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/mixed_edit.svg delete mode 100644 novelwriter/assets/icons/typicons_dark/typ_edit.svg create mode 100644 novelwriter/assets/icons/typicons_dark/typ_export.svg create mode 100644 novelwriter/assets/icons/typicons_light/mixed_edit.svg delete mode 100644 novelwriter/assets/icons/typicons_light/typ_edit.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_export.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index ccc55222..cde02134 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -78,6 +78,7 @@ status_time = typ_stopwatch-grey.svg sticky-off = typ_pin-outline.svg sticky-on = typ_pin.svg up = typ_chevron-up.svg -view_editor = typ_edit.svg +view_build = typ_export.svg +view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg diff --git a/novelwriter/assets/icons/typicons_dark/mixed_edit.svg b/novelwriter/assets/icons/typicons_dark/mixed_edit.svg new file mode 100644 index 00000000..fac03f3e --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/mixed_edit.svg @@ -0,0 +1,52 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_edit.svg b/novelwriter/assets/icons/typicons_dark/typ_edit.svg deleted file mode 100644 index 652dda33..00000000 --- a/novelwriter/assets/icons/typicons_dark/typ_edit.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - diff --git a/novelwriter/assets/icons/typicons_dark/typ_export.svg b/novelwriter/assets/icons/typicons_dark/typ_export.svg new file mode 100644 index 00000000..6e36ed53 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_export.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index a26d18b4..85941827 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -78,6 +78,7 @@ status_time = typ_stopwatch-grey.svg sticky-off = typ_pin-outline.svg sticky-on = typ_pin.svg up = typ_chevron-up.svg -view_editor = typ_edit.svg +view_build = typ_export.svg +view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg diff --git a/novelwriter/assets/icons/typicons_light/mixed_edit.svg b/novelwriter/assets/icons/typicons_light/mixed_edit.svg new file mode 100644 index 00000000..982206ed --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/mixed_edit.svg @@ -0,0 +1,52 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_edit.svg b/novelwriter/assets/icons/typicons_light/typ_edit.svg deleted file mode 100644 index 512c1592..00000000 --- a/novelwriter/assets/icons/typicons_light/typ_edit.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - diff --git a/novelwriter/assets/icons/typicons_light/typ_export.svg b/novelwriter/assets/icons/typicons_light/typ_export.svg new file mode 100644 index 00000000..537f71f9 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_export.svg @@ -0,0 +1,38 @@ + + + + + + From 2425af4bfeceff3ce23c0a252c720d0ad687de7f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 17:00:45 +0200 Subject: [PATCH 093/179] Add menu to view bar settings, and add build button --- novelwriter/enum.py | 12 ++++-------- novelwriter/gui/theme.py | 2 +- novelwriter/gui/viewsbar.py | 37 +++++++++++++++++++++++++++++-------- novelwriter/guimain.py | 9 --------- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 20332b2f..fc9604e0 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -132,14 +132,10 @@ class nwState(Enum): class nwView(Enum): - EDITOR = 0 - PROJECT = 1 - NOVEL = 2 - OUTLINE = 3 - DETAILS = 4 - STATS = 5 - SET_PROJ = 6 - SET_MAIN = 7 + EDITOR = 0 + PROJECT = 1 + NOVEL = 2 + OUTLINE = 3 # END Enum nwView diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index e3151ea8..7c0e3ce6 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -462,7 +462,7 @@ class GuiIcons: "proj_folder", "proj_note", "proj_nwx", "proj_scene", "proj_stats", "proj_title", "search_cancel", "search_case", "search_loop", "search_preserve", "search_project", "search_regex", "search_word", "status_idle", "status_lang", "status_lines", - "status_stats", "status_time", "view_editor", "view_novel", "view_outline", + "status_stats", "status_time", "view_build", "view_editor", "view_novel", "view_outline", # General Button Icons "add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit", diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py index c4dfc476..5a96d92e 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/viewsbar.py @@ -27,7 +27,9 @@ import logging import novelwriter from PyQt5.QtCore import Qt, QSize, pyqtSignal -from PyQt5.QtWidgets import QToolBar, QWidget, QSizePolicy, QAction +from PyQt5.QtWidgets import ( + QToolBar, QWidget, QSizePolicy, QAction, QMenu, QToolButton +) from novelwriter.enum import nwView @@ -49,7 +51,7 @@ class GuiViewsBar(QToolBar): # Style iPx = self.mainConf.pxInt(22) - mPx = self.mainConf.pxInt(58) + mPx = self.mainConf.pxInt(60) lblFont = self.theTheme.guiFont lblFont.setPointSizeF(0.65*self.theTheme.fontPointSize) @@ -77,26 +79,45 @@ class GuiViewsBar(QToolBar): self.aOutline.setIcon(self.theTheme.getIcon("view_outline")) self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) + self.aBuild = QAction(self.tr("Build")) + self.aBuild.setIcon(self.theTheme.getIcon("view_build")) + self.aBuild.triggered.connect(lambda: self.theParent.showBuildProjectDialog()) + self.aDetails = QAction(self.tr("Details")) self.aDetails.setIcon(self.theTheme.getIcon("proj_details")) - self.aDetails.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.DETAILS)) + self.aDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog()) self.aStats = QAction(self.tr("Stats")) self.aStats.setIcon(self.theTheme.getIcon("proj_stats")) - self.aStats.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.STATS)) + self.aStats.triggered.connect(lambda: self.theParent.showWritingStatsDialog()) - self.aSettings = QAction(self.tr("Settings")) - self.aSettings.setIcon(self.theTheme.getIcon("settings")) - self.aSettings.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.SET_PROJ)) + # Settings Menu + self.mSettings = QMenu() + + self.aPrjSettings = QAction(self.tr("Project Settings")) + self.aPrjSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog()) + self.mSettings.addAction(self.aPrjSettings) + + self.aPreferences = QAction(self.tr("Preferences")) + self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog()) + self.mSettings.addAction(self.aPreferences) + + self.tbSettings = QToolButton(self) + self.tbSettings.setIcon(self.theTheme.getIcon("settings")) + self.tbSettings.setText(self.tr("Settings")) + self.tbSettings.setMenu(self.mSettings) + self.tbSettings.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + self.tbSettings.setPopupMode(QToolButton.InstantPopup) # Assemble self.addAction(self.aProject) self.addAction(self.aNovel) self.addAction(self.aOutline) + self.addAction(self.aBuild) self.addWidget(stretch) self.addAction(self.aDetails) self.addAction(self.aStats) - self.addAction(self.aSettings) + self.addWidget(self.tbSettings) logger.debug("GuiViewsBar initialisation complete") diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index a99375d7..bf1475b7 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1496,15 +1496,6 @@ class GuiMain(QMainWindow): elif view == nwView.OUTLINE: self.mainStack.setCurrentWidget(self.splitOutline) - elif view == nwView.DETAILS: - self.showProjectDetailsDialog() - - elif view == nwView.STATS: - self.showWritingStatsDialog() - - elif view == nwView.SET_PROJ: - self.showProjectSettingsDialog() - return @pyqtSlot() From 25427e8521758f324f8386263ae804b383896d8a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 17:43:38 +0200 Subject: [PATCH 094/179] Add tooltips to views bar --- novelwriter/gui/viewsbar.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py index 5a96d92e..fd7293d3 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/viewsbar.py @@ -68,26 +68,32 @@ class GuiViewsBar(QToolBar): # Actions self.aProject = QAction(self.tr("Project")) + self.aProject.setToolTip(self.tr("Show project tree and editor")) self.aProject.setIcon(self.theTheme.getIcon("view_editor")) self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) self.aNovel = QAction(self.tr("Novel")) + self.aNovel.setToolTip(self.tr("Show novel tree and editor")) self.aNovel.setIcon(self.theTheme.getIcon("view_novel")) self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) self.aOutline = QAction(self.tr("Outline")) + self.aOutline.setToolTip(self.tr("Show novel outline")) self.aOutline.setIcon(self.theTheme.getIcon("view_outline")) self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) self.aBuild = QAction(self.tr("Build")) + self.aBuild.setToolTip(self.tr("Build novel project")) self.aBuild.setIcon(self.theTheme.getIcon("view_build")) self.aBuild.triggered.connect(lambda: self.theParent.showBuildProjectDialog()) self.aDetails = QAction(self.tr("Details")) + self.aDetails.setToolTip(self.tr("Show project details")) self.aDetails.setIcon(self.theTheme.getIcon("proj_details")) self.aDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog()) self.aStats = QAction(self.tr("Stats")) + self.aStats.setToolTip(self.tr("Show project statistics")) self.aStats.setIcon(self.theTheme.getIcon("proj_stats")) self.aStats.triggered.connect(lambda: self.theParent.showWritingStatsDialog()) From 3cfc5d9a1a6e612a1ef345a285e19ef29e511dde Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 17:45:54 +0200 Subject: [PATCH 095/179] Remove borders from all main GUI widgets --- novelwriter/gui/doceditor.py | 3 ++- novelwriter/gui/docviewer.py | 4 +++- novelwriter/gui/noveltree.py | 5 ++++- novelwriter/gui/outline.py | 3 ++- novelwriter/gui/outlinedetails.py | 3 ++- novelwriter/gui/projtree.py | 3 ++- novelwriter/gui/viewsbar.py | 11 +++++++++-- novelwriter/guimain.py | 6 +++++- 8 files changed, 29 insertions(+), 9 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 64c2b73d..98648ab6 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -131,8 +131,9 @@ class GuiDocEditor(QTextEdit): # Editor Settings self.setMinimumWidth(self.mainConf.pxInt(300)) - self.setAutoFillBackground(True) self.setAcceptRichText(False) + self.setAutoFillBackground(True) + self.setFrameStyle(QFrame.NoFrame) # Custom Shortcuts QShortcut( diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 4ec59570..4c293da5 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -36,7 +36,7 @@ from PyQt5.QtGui import ( ) from PyQt5.QtWidgets import ( qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton, - QAction, QMenu + QAction, QMenu, QFrame ) from novelwriter.core import ToHtml @@ -68,6 +68,7 @@ class GuiDocViewer(QTextBrowser): self.setAutoFillBackground(True) self.setOpenExternalLinks(False) self.setFocusPolicy(Qt.StrongFocus) + self.setFrameStyle(QFrame.NoFrame) # Document Header and Footer self.docHeader = GuiDocViewHeader(self) @@ -1185,6 +1186,7 @@ class GuiDocViewDetails(QScrollArea): self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setWidgetResizable(True) self.setMinimumHeight(self.mainConf.pxInt(50)) + self.setFrameStyle(QFrame.NoFrame) logger.debug("GuiDocViewDetails initialisation complete") diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 0f7ac02f..d0732abe 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -29,7 +29,9 @@ import novelwriter from time import time from PyQt5.QtCore import Qt, QSize -from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView +from PyQt5.QtWidgets import ( + QTreeWidget, QTreeWidgetItem, QAbstractItemView, QFrame +) from novelwriter.common import checkInt from novelwriter.constants import nwKeyWords @@ -60,6 +62,7 @@ class GuiNovelTree(QTreeWidget): # Build GUI iPx = self.theTheme.baseIconSize + self.setFrameStyle(QFrame.NoFrame) self.setIconSize(QSize(iPx, iPx)) self.setIndentation(iPx) self.setColumnCount(3) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 1ac83ff1..4028ccf6 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -30,7 +30,7 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSlot from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView + QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView, QFrame ) from novelwriter.enum import nwItemLayout, nwItemType, nwOutline @@ -95,6 +95,7 @@ class GuiOutline(QTreeWidget): self.optState = theParent.theProject.optState self.headerMenu = GuiOutlineHeaderMenu(self) + self.setFrameStyle(QFrame.NoFrame) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setExpandsOnDoubleClick(False) diff --git a/novelwriter/gui/outlinedetails.py b/novelwriter/gui/outlinedetails.py index 358a6531..00c20e44 100644 --- a/novelwriter/gui/outlinedetails.py +++ b/novelwriter/gui/outlinedetails.py @@ -28,7 +28,7 @@ import novelwriter from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP, pyqtSignal from PyQt5.QtWidgets import ( - QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel + QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel, QFrame ) from novelwriter.enum import nwView @@ -233,6 +233,7 @@ class GuiOutlineDetails(QScrollArea): self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setWidgetResizable(True) + self.setFrameStyle(QFrame.NoFrame) self.initDetails() diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 5d40aeae..b134b901 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -32,7 +32,7 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction + QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame ) from novelwriter.core import NWDoc @@ -81,6 +81,7 @@ class GuiProjectTree(QTreeWidget): # Tree Settings iPx = self.theTheme.baseIconSize self.setIconSize(QSize(iPx, iPx)) + self.setFrameStyle(QFrame.NoFrame) self.setExpandsOnDoubleClick(False) self.setIndentation(iPx) self.setColumnCount(4) diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py index fd7293d3..e5b627b2 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/viewsbar.py @@ -55,44 +55,50 @@ class GuiViewsBar(QToolBar): lblFont = self.theTheme.guiFont lblFont.setPointSizeF(0.65*self.theTheme.fontPointSize) - self.setFont(lblFont) self.setMovable(False) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.setIconSize(QSize(iPx, iPx)) self.setMaximumWidth(mPx) self.setContentsMargins(0, 0, 0, 0) + self.setStyleSheet("QToolBar {border: 0px;}") stretch = QWidget(self) stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Actions self.aProject = QAction(self.tr("Project")) + self.aProject.setFont(lblFont) self.aProject.setToolTip(self.tr("Show project tree and editor")) self.aProject.setIcon(self.theTheme.getIcon("view_editor")) self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) self.aNovel = QAction(self.tr("Novel")) + self.aNovel.setFont(lblFont) self.aNovel.setToolTip(self.tr("Show novel tree and editor")) self.aNovel.setIcon(self.theTheme.getIcon("view_novel")) self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) self.aOutline = QAction(self.tr("Outline")) + self.aOutline.setFont(lblFont) self.aOutline.setToolTip(self.tr("Show novel outline")) self.aOutline.setIcon(self.theTheme.getIcon("view_outline")) self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) self.aBuild = QAction(self.tr("Build")) + self.aBuild.setFont(lblFont) self.aBuild.setToolTip(self.tr("Build novel project")) self.aBuild.setIcon(self.theTheme.getIcon("view_build")) self.aBuild.triggered.connect(lambda: self.theParent.showBuildProjectDialog()) self.aDetails = QAction(self.tr("Details")) + self.aDetails.setFont(lblFont) self.aDetails.setToolTip(self.tr("Show project details")) self.aDetails.setIcon(self.theTheme.getIcon("proj_details")) self.aDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog()) self.aStats = QAction(self.tr("Stats")) + self.aStats.setFont(lblFont) self.aStats.setToolTip(self.tr("Show project statistics")) self.aStats.setIcon(self.theTheme.getIcon("proj_stats")) self.aStats.triggered.connect(lambda: self.theParent.showWritingStatsDialog()) @@ -109,8 +115,9 @@ class GuiViewsBar(QToolBar): self.mSettings.addAction(self.aPreferences) self.tbSettings = QToolButton(self) - self.tbSettings.setIcon(self.theTheme.getIcon("settings")) + self.tbSettings.setFont(lblFont) self.tbSettings.setText(self.tr("Settings")) + self.tbSettings.setIcon(self.theTheme.getIcon("settings")) self.tbSettings.setMenu(self.mSettings) self.tbSettings.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.tbSettings.setPopupMode(QToolButton.InstantPopup) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index bf1475b7..a759972a 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -102,6 +102,7 @@ class GuiMain(QMainWindow): # Sizes mPx = self.mainConf.pxInt(4) + hWd = self.mainConf.pxInt(4) # Main GUI Elements self.statusBar = GuiMainStatus(self) @@ -149,12 +150,14 @@ class GuiMain(QMainWindow): self.splitView = QSplitter(Qt.Vertical) self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.viewMeta) + self.splitView.setHandleWidth(hWd) self.splitView.setSizes(self.mainConf.getViewPanePos()) # Splitter : Document Editor / Document Viewer self.splitDocs = QSplitter(Qt.Horizontal) self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.splitView) + self.splitDocs.setHandleWidth(hWd) # Splitter : Project Outlie / Outline Details self.splitOutline = QSplitter(Qt.Vertical) @@ -164,9 +167,10 @@ class GuiMain(QMainWindow): # Splitter : Project Tree / Main Tabs self.splitMain = QSplitter(Qt.Horizontal) - self.splitMain.setContentsMargins(0, 0, mPx, 0) + self.splitMain.setContentsMargins(0, 0, 0, 0) self.splitMain.addWidget(self.treePane) self.splitMain.addWidget(self.splitDocs) + self.splitMain.setHandleWidth(hWd) self.splitMain.setSizes(self.mainConf.getMainPanePos()) # Main Stack : Editor / Outline From b9fcc43ca65c154141687b24c84011f1daac5d77 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 17:53:46 +0200 Subject: [PATCH 096/179] Move highlighting theme setting back to General tab in Preferences --- novelwriter/dialogs/preferences.py | 40 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index c38a0c5f..d38a4ab6 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -203,6 +203,22 @@ class GuiPreferencesGeneral(QWidget): self.tr("Requires restart.") ) + # Editor Theme + self.guiSyntax = QComboBox() + self.guiSyntax.setMinimumWidth(self.mainConf.pxInt(200)) + self.theSyntaxes = self.theTheme.listSyntax() + for syntaxFile, syntaxName in self.theSyntaxes: + self.guiSyntax.addItem(syntaxName, syntaxFile) + syntaxIdx = self.guiSyntax.findData(self.mainConf.guiSyntax) + if syntaxIdx != -1: + self.guiSyntax.setCurrentIndex(syntaxIdx) + + self.mainForm.addRow( + self.tr("Editor theme"), + self.guiSyntax, + self.tr("Colour theme for the editor and viewer.") + ) + # Font Family self.guiFont = QLineEdit() self.guiFont.setReadOnly(True) @@ -275,6 +291,7 @@ class GuiPreferencesGeneral(QWidget): guiLang = self.guiLang.currentData() guiTheme = self.guiTheme.currentData() guiIcons = self.guiIcons.currentData() + guiSyntax = self.guiSyntax.currentData() guiFont = self.guiFont.text() guiFontSize = self.guiFontSize.value() emphLabels = self.emphLabels.isChecked() @@ -294,6 +311,7 @@ class GuiPreferencesGeneral(QWidget): self.mainConf.guiLang = guiLang self.mainConf.guiTheme = guiTheme self.mainConf.guiIcons = guiIcons + self.mainConf.guiSyntax = guiSyntax self.mainConf.guiFont = guiFont self.mainConf.guiFontSize = guiFontSize self.mainConf.emphLabels = emphLabels @@ -834,25 +852,6 @@ class GuiPreferencesSyntax(QWidget): self.mainForm.setHelpTextStyle(self.theTheme.helpText) self.setLayout(self.mainForm) - # Highlighting Theme - # ================== - self.mainForm.addGroupLabel(self.tr("Highlighting Theme")) - - self.guiSyntax = QComboBox() - self.guiSyntax.setMinimumWidth(self.mainConf.pxInt(200)) - self.theSyntaxes = self.theTheme.listSyntax() - for syntaxFile, syntaxName in self.theSyntaxes: - self.guiSyntax.addItem(syntaxName, syntaxFile) - syntaxIdx = self.guiSyntax.findData(self.mainConf.guiSyntax) - if syntaxIdx != -1: - self.guiSyntax.setCurrentIndex(syntaxIdx) - - self.mainForm.addRow( - self.tr("Highlighting theme"), - self.guiSyntax, - self.tr("Colour theme for the editor and viewer.") - ) - # Quotes & Dialogue # ================= self.mainForm.addGroupLabel(self.tr("Quotes & Dialogue")) @@ -912,9 +911,6 @@ class GuiPreferencesSyntax(QWidget): def saveValues(self): """Save the values set for this tab. """ - # Highlighting Theme - self.mainConf.guiSyntax = self.guiSyntax.currentData() - # Quotes & Dialogue self.mainConf.highlightQuotes = self.highlightQuotes.isChecked() self.mainConf.allowOpenSQuote = self.allowOpenSQuote.isChecked() From 1f66ed3e3bd4d7260630279f23a390747943b259 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 21:00:49 +0200 Subject: [PATCH 097/179] Reduce the list of tag classes in outline view when they are not used --- novelwriter/core/tree.py | 8 +++++++ novelwriter/gui/outline.py | 39 +++++++++++++++++++++++++++++-- novelwriter/gui/projtree.py | 7 ++++-- novelwriter/guimain.py | 3 +++ tests/test_core/test_core_tree.py | 5 ++++ 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index ee57e0db..11842a5a 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -265,6 +265,14 @@ class NWTree(): # Tree Root Methods ## + def rootClasses(self): + """Return a set of all root classes in use by the project. + """ + rootClasses = set() + for nwItem in self._treeRoots.values(): + rootClasses.add(nwItem.itemClass) + return rootClasses + def isRoot(self, tHandle): """Check if a handle is a root item. """ diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index cdb4c7c2..c29b9ff9 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -39,7 +39,9 @@ from PyQt5.QtWidgets import ( QWidget, QFrame ) -from novelwriter.enum import nwItemLayout, nwItemType, nwOutline, nwView +from novelwriter.enum import ( + nwItemClass, nwItemLayout, nwItemType, nwOutline, nwView +) from novelwriter.common import checkInt from novelwriter.constants import trConst, nwKeyWords, nwLabels @@ -75,6 +77,7 @@ class GuiOutline(QWidget): # Function Mappings self.getSelectedHandle = self.outlineView.getSelectedHandle + self.updateClasses = self.outlineData.updateClasses return @@ -96,6 +99,7 @@ class GuiOutline(QWidget): def closeOutline(self): self.outlineView.closeOutline() + self.outlineData.updateClasses() return def refreshView(self, overRide=False, novelChanged=False): @@ -307,7 +311,6 @@ class GuiOutlineView(QTreeWidget): tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole) self.theOutline.outlineData.showItem(tHandle, sTitle) - self.theParent.treeView.setSelectedHandle(tHandle) return @@ -824,6 +827,8 @@ class GuiOutlineDetails(QScrollArea): else: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.updateClasses() + return def clearDetails(self): @@ -846,6 +851,7 @@ class GuiOutlineDetails(QScrollArea): self.objKeyValue.setText("") self.entKeyValue.setText("") self.cstKeyValue.setText("") + self.updateClasses() return def showItem(self, tHandle, sTitle): @@ -895,6 +901,7 @@ class GuiOutlineDetails(QScrollArea): # Slots ## + @pyqtSlot(str) def _tagClicked(self, theLink): """Capture the click of a tag in the right-most column. """ @@ -906,6 +913,34 @@ class GuiOutlineDetails(QScrollArea): self.theParent.docViewer.loadFromTag(theBits[1]) return + @pyqtSlot() + def updateClasses(self): + """Update the visibility status of class details. + """ + usedClasses = self.theProject.projTree.rootClasses() + + pltVisible = nwItemClass.PLOT in usedClasses + timVisible = nwItemClass.TIMELINE in usedClasses + wldVisible = nwItemClass.WORLD in usedClasses + objVisible = nwItemClass.OBJECT in usedClasses + entVisible = nwItemClass.ENTITY in usedClasses + cstVisible = nwItemClass.CUSTOM in usedClasses + + self.pltKeyLabel.setVisible(pltVisible) + self.pltKeyValue.setVisible(pltVisible) + self.timKeyLabel.setVisible(timVisible) + self.timKeyValue.setVisible(timVisible) + self.wldKeyLabel.setVisible(wldVisible) + self.wldKeyValue.setVisible(wldVisible) + self.objKeyLabel.setVisible(objVisible) + self.objKeyValue.setVisible(objVisible) + self.entKeyLabel.setVisible(entVisible) + self.entKeyValue.setVisible(entVisible) + self.cstKeyLabel.setVisible(cstVisible) + self.cstKeyValue.setVisible(cstVisible) + + return + ## # Internal Functions ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index b134b901..9cbd5d18 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -51,6 +51,7 @@ class GuiProjectTree(QTreeWidget): novelItemChanged = pyqtSignal() noteItemChanged = pyqtSignal() wordCountsChanged = pyqtSignal() + rootFoldersChanged = pyqtSignal() def __init__(self, theParent): QTreeWidget.__init__(self, theParent) @@ -177,6 +178,7 @@ class GuiProjectTree(QTreeWidget): if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass): tHandle = self.theProject.newRoot(itemClass) + self.rootFoldersChanged.emit() elif itemType in (nwItemType.FILE, nwItemType.FOLDER): @@ -450,6 +452,7 @@ class GuiProjectTree(QTreeWidget): self.takeTopLevelItem(tIndex) self._deleteTreeItem(tHandle) self._setTreeChanged(True) + self.rootFoldersChanged.emit() else: self.theParent.makeAlert(self.tr( "Cannot delete root folder. It is not empty. " @@ -541,7 +544,7 @@ class GuiProjectTree(QTreeWidget): else: expIcon = self.theTheme.getIcon("cross") - itempStatus, statusIcon = nwItem.getImportStatus() + itemStatus, statusIcon = nwItem.getImportStatus() hLevel = self.theIndex.getHandleHeaderLevel(tHandle) itemIcon = self.theTheme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel @@ -551,7 +554,7 @@ class GuiProjectTree(QTreeWidget): trItem.setText(self.C_NAME, nwItem.itemName) trItem.setIcon(self.C_EXPORT, expIcon) trItem.setIcon(self.C_STATUS, statusIcon) - trItem.setToolTip(self.C_STATUS, itempStatus) + trItem.setToolTip(self.C_STATUS, itemStatus) if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT: trFont = trItem.font(self.C_NAME) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ea92aacd..8f7814e0 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -126,6 +126,7 @@ class GuiMain(QMainWindow): self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) + self.treeView.rootFoldersChanged.connect(self.projView.updateClasses) self.viewsBar.viewChangeRequested.connect(self._changeView) self.projView.viewChangeRequested.connect(self._changeView) @@ -352,6 +353,7 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.saveProject() self.docEditor.setDictionaries() + self.projView.updateClasses() self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -499,6 +501,7 @@ class GuiMain(QMainWindow): self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.mainMenu.setAutoOutline(self.theProject.autoOutline) self.statusBar.setRefTime(self.theProject.projOpened) + self.projView.updateClasses() self._updateStatusWordCount() # Restore previously open documents, if any diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 5c05f679..23eb7f73 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -149,6 +149,11 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert theTree.isTrash("a000000000003") is True assert theTree.isRoot("a000000000002") is True + # Check that we have the root classes + assert theTree.rootClasses() == { + nwItemClass.NOVEL, nwItemClass.CHARACTER, nwItemClass.ARCHIVE, nwItemClass.TRASH + } + # Check the isTrash function assert theTree.isTrash("0000000000000") is True # Doesn't exist assert theTree.isTrash("a000000000003") is True # This the trash folder From 007f2fbff4bfdcfb735884d56474354111b8779a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 22:46:42 +0200 Subject: [PATCH 098/179] Add a menu icon --- .../assets/icons/typicons_dark/icons.conf | 1 + .../assets/icons/typicons_dark/typ_th-menu.svg | 16 ++++++++++++++++ .../assets/icons/typicons_light/icons.conf | 1 + .../assets/icons/typicons_light/typ_th-menu.svg | 16 ++++++++++++++++ novelwriter/gui/theme.py | 4 ++-- 5 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/typ_th-menu.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_th-menu.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index cde02134..ae157e2c 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -47,6 +47,7 @@ edit = typ_pencil.svg forward = typ_chevron-right.svg hash = typ_hash.svg maximise = typ_arrow-maximise.svg +menu = typ_th-menu.svg minimise = typ_arrow-minimise.svg proj_chapter = mixed_document-chapter.svg proj_details = typ_th-list-grey.svg diff --git a/novelwriter/assets/icons/typicons_dark/typ_th-menu.svg b/novelwriter/assets/icons/typicons_dark/typ_th-menu.svg new file mode 100644 index 00000000..89434cdf --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_th-menu.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 85941827..4683bb19 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -47,6 +47,7 @@ edit = typ_pencil.svg forward = typ_chevron-right.svg hash = typ_hash.svg maximise = typ_arrow-maximise.svg +menu = typ_th-menu.svg minimise = typ_arrow-minimise.svg proj_chapter = mixed_document-chapter.svg proj_details = typ_th-list-grey.svg diff --git a/novelwriter/assets/icons/typicons_light/typ_th-menu.svg b/novelwriter/assets/icons/typicons_light/typ_th-menu.svg new file mode 100644 index 00000000..cfc8a1d9 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_th-menu.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 7c0e3ce6..bb93e0b8 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -466,8 +466,8 @@ class GuiIcons: # General Button Icons "add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit", - "forward", "hash", "maximise", "minimise", "reference", "refresh", "remove", "save", - "search_replace", "search", "settings", "up", + "forward", "hash", "maximise", "menu", "minimise", "reference", "refresh", "remove", + "save", "search_replace", "search", "settings", "up", # Switches "sticky-on", "sticky-off", From ecc0585c87e6163758cb31aeed95f2b470e7e418 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 May 2022 23:03:30 +0200 Subject: [PATCH 099/179] Add a toolbar to the Outline view --- docs/source/usage_shortcuts.rst | 2 - novelwriter/core/tree.py | 9 ++ novelwriter/gui/mainmenu.py | 25 ---- novelwriter/gui/outline.py | 195 ++++++++++++++++++++++------- novelwriter/guimain.py | 20 +-- tests/test_gui/test_gui_guimain.py | 1 - tests/test_gui/test_gui_outline.py | 19 ++- 7 files changed, 170 insertions(+), 101 deletions(-) diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst index fea65c8c..69c2e773 100644 --- a/docs/source/usage_shortcuts.rst +++ b/docs/source/usage_shortcuts.rst @@ -59,7 +59,6 @@ The main shorcuts are as follows: ":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo." ":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes." ":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking." - ":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline." ":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree." ":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree." ":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor." @@ -88,7 +87,6 @@ The main shorcuts are as follows: ":kbd:`F7`", "Re-run spell checker." ":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer." ":kbd:`F9`", "Re-build the project index." - ":kbd:`F10`", "Re-build the project outline." ":kbd:`F11`", "Activate full screen mode." ":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available." ":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document." diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 11842a5a..d151f370 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -273,6 +273,15 @@ class NWTree(): rootClasses.add(nwItem.itemClass) return rootClasses + def novelRoots(self): + """Return a doctionary of all novel-like root items. + """ + novelItems = {} + for tHandle, nwItem in self._treeRoots.items(): + if nwItem.isNovelLike(): + novelItems[tHandle] = nwItem + return novelItems + def isRoot(self, tHandle): """Check if a handle is a root item. """ diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index f69d5a56..bd8d1edd 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -81,12 +81,6 @@ class GuiMainMenu(QMenuBar): self.aSpellCheck.setChecked(theMode) return - def setAutoOutline(self, theMode): - """Forward auto outline check state to its action. - """ - self.aAutoOutline.setChecked(theMode) - return - def setFocusMode(self, theMode): """Forward focus mode check state to its action. """ @@ -105,12 +99,6 @@ class GuiMainMenu(QMenuBar): self.theParent.docEditor.toggleSpellCheck(None) return True - def _toggleAutoOutline(self, theMode): - """Toggle auto outline when the menu entry is checked. - """ - self.theProject.setAutoOutline(theMode) - return True - def _openWebsite(self, theUrl): """Open a URL in the system's default browser. """ @@ -889,19 +877,6 @@ class GuiMainMenu(QMenuBar): self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex()) self.toolsMenu.addAction(self.aRebuildIndex) - # Tools > Rebuild Outline - self.aRebuildOutline = QAction(self.tr("Rebuild Outline"), self) - self.aRebuildOutline.setShortcut("F10") - self.aRebuildOutline.triggered.connect(lambda: self.theParent.rebuildOutline()) - self.toolsMenu.addAction(self.aRebuildOutline) - - # Tools > Toggle Auto Build Outline - self.aAutoOutline = QAction(self.tr("Auto-Update Outline"), self) - self.aAutoOutline.setCheckable(True) - self.aAutoOutline.toggled.connect(self._toggleAutoOutline) - self.aAutoOutline.setShortcut("Ctrl+F10") - self.toolsMenu.addAction(self.aAutoOutline) - # Tools > Separator self.toolsMenu.addSeparator() diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index c29b9ff9..6b50df5f 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -4,9 +4,11 @@ novelWriter – GUI Project Outline GUI class for the project outline view File History: -Created: 2019-11-16 [0.4.1] GuiOutlineView, GuiOutlineHeaderMenu -Created: 2020-06-02 [0.7.0] GuiOutlineDetails Created: 2022-05-15 [1.7b1] GuiOutline +Created: 2022-05-22 [1.7b1] GuiOutlineToolBar +Created: 2019-11-16 [0.4.1] GuiOutlineView +Created: 2019-11-16 [0.4.1] GuiOutlineHeaderMenu +Created: 2020-06-02 [0.7.0] GuiOutlineDetails This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -29,6 +31,7 @@ import logging import novelwriter from time import time +from enum import Enum from PyQt5.QtCore import ( Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP @@ -36,7 +39,7 @@ from PyQt5.QtCore import ( from PyQt5.QtWidgets import ( QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout, - QWidget, QFrame + QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton ) from novelwriter.enum import ( @@ -60,6 +63,7 @@ class GuiOutline(QWidget): self.theParent = theParent self.theProject = theParent.theProject + self.outlineBar = GuiOutlineToolBar(self) self.outlineView = GuiOutlineView(self) self.outlineData = GuiOutlineDetails(self) @@ -71,13 +75,20 @@ class GuiOutline(QWidget): # Assemble self.outerBox = QVBoxLayout() self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.addWidget(self.outlineBar) self.outerBox.addWidget(self.splitOutline) self.setLayout(self.outerBox) + # Connect Signals + self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns) + self.outlineBar.columnToggled.connect(self.outlineView.menuColumnToggled) + self.outlineBar.viewRefreshRequested.connect( + lambda: self.outlineView.refreshTree(overRide=True) + ) + # Function Mappings self.getSelectedHandle = self.outlineView.getSelectedHandle - self.updateClasses = self.outlineData.updateClasses return @@ -112,9 +123,112 @@ class GuiOutline(QWidget): def setTreeFocus(self): return self.outlineView.setFocus() + ## + # Slots + ## + + @pyqtSlot() + def projectUpdated(self): + """Should be called whenever the number of root folders change. + """ + self.outlineBar.populateNovelList() + self.outlineData.updateClasses() + return + + @pyqtSlot() + def _updateMenuColumns(self): + """Trigger an update of the toggled state of the column menu + checkboxes whenever a signal is received that the hidden state + of columns has changed. + """ + self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns) + return + # END Class GuiOutline +class GuiOutlineToolBar(QToolBar): + + columnToggled = pyqtSignal(bool, Enum) + viewRefreshRequested = pyqtSignal() + + def __init__(self, theOutline): + QTreeWidget.__init__(self, theOutline) + + logger.debug("Initialising GuiOutlineToolBar ...") + + self.mainConf = novelwriter.CONFIG + self.theOutline = theOutline + self.theParent = theOutline.theParent + self.theProject = theOutline.theParent.theProject + self.theTheme = theOutline.theParent.theTheme + + iPx = self.mainConf.pxInt(22) + mPx = self.mainConf.pxInt(12) + + self.setMovable(False) + self.setIconSize(QSize(iPx, iPx)) + self.setContentsMargins(0, 0, 0, 0) + self.setStyleSheet("QToolBar {border: 0px;}") + + stretch = QWidget(self) + stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Novel Selector + self.novelLabel = QLabel(self.tr("Outline of")) + self.novelLabel.setContentsMargins(0, 0, mPx, 0) + + self.novelValue = QComboBox(self) + self.novelValue.setMinimumWidth(self.mainConf.pxInt(200)) + + # Actions + self.aRefresh = QAction(self.tr("Refresh"), self) + self.aRefresh.setIcon(self.theTheme.getIcon("refresh")) + self.aRefresh.triggered.connect( + lambda: self.viewRefreshRequested.emit() + ) + + # Column Menu + self.mColumns = GuiOutlineHeaderMenu(self) + self.mColumns.columnToggled.connect( + lambda isChecked, tItem: self.columnToggled.emit(isChecked, tItem) + ) + + self.tbColumns = QToolButton(self) + self.tbColumns.setIcon(self.theTheme.getIcon("menu")) + self.tbColumns.setMenu(self.mColumns) + self.tbColumns.setPopupMode(QToolButton.InstantPopup) + + # Assemble + self.addWidget(self.novelLabel) + self.addWidget(self.novelValue) + self.addSeparator() + self.addAction(self.aRefresh) + self.addWidget(self.tbColumns) + self.addWidget(stretch) + + self.populateNovelList() + + logger.debug("GuiOutlineToolBar initialisation complete") + + def populateNovelList(self): + """Fill the novel combo box. + """ + self.novelValue.clear() + for tHandle, nwItem in self.theProject.projTree.novelRoots().items(): + self.novelValue.addItem( + self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]), + nwItem.itemName, tHandle + ) + return + + def setColumnHiddenState(self, hiddenState): + self.mColumns.setHiddenState(hiddenState) + return + +# END Class GuiOutlineToolBar + + class GuiOutlineView(QTreeWidget): DEF_WIDTH = { @@ -157,10 +271,12 @@ class GuiOutlineView(QTreeWidget): nwOutline.SYNOP: False, } + hiddenStateChanged = pyqtSignal() + def __init__(self, theOutline): QTreeWidget.__init__(self, theOutline) - logger.debug("Initialising GuiOutline ...") + logger.debug("Initialising GuiOutlineView ...") self.mainConf = novelwriter.CONFIG self.theOutline = theOutline @@ -169,7 +285,6 @@ class GuiOutlineView(QTreeWidget): self.theTheme = theOutline.theParent.theTheme self.theIndex = theOutline.theParent.theIndex self.optState = theOutline.theParent.theProject.optState - self.headerMenu = GuiOutlineHeaderMenu(self) self.setFrameStyle(QFrame.NoFrame) self.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -184,8 +299,6 @@ class GuiOutlineView(QTreeWidget): self.setIndentation(iPx) self.treeHead = self.header() - self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu) - self.treeHead.customContextMenuRequested.connect(self._headerRightClick) self.treeHead.sectionMoved.connect(self._columnMoved) # Internals @@ -199,12 +312,25 @@ class GuiOutlineView(QTreeWidget): self.initOutline() self.clearOutline() - self.headerMenu.setHiddenState(self._colHidden) - logger.debug("GuiOutline initialisation complete") + self.hiddenStateChanged.emit() + + logger.debug("GuiOutlineView initialisation complete") return + ## + # Properties + ## + + @property + def hiddenColumns(self): + return self._colHidden + + ## + # Methods + ## + def initOutline(self): """Set or update outline settings. """ @@ -314,13 +440,6 @@ class GuiOutlineView(QTreeWidget): return - @pyqtSlot("QPoint") - def _headerRightClick(self, clickPos): - """Show the header column menu. - """ - self.headerMenu.exec_(self.mapToGlobal(clickPos)) - return - @pyqtSlot(int, int, int) def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx): """Make sure the order array is up to date with the actual order @@ -330,9 +449,10 @@ class GuiOutlineView(QTreeWidget): self._saveHeaderState() return - def _menuColumnToggled(self, isChecked, theItem): + @pyqtSlot(bool, Enum) + def menuColumnToggled(self, isChecked, theItem): """Receive the changes to column visibility forwarded by the - header context menu. + column selection menu. """ logger.verbose("User toggled Outline column '%s'", theItem.name) if theItem in self._colIdx: @@ -389,7 +509,7 @@ class GuiOutlineView(QTreeWidget): except Exception: logger.warning("Ignored unknown outline column '%s'", str(hName)) - self.headerMenu.setHiddenState(self._colHidden) + self.hiddenStateChanged.emit() return @@ -558,10 +678,11 @@ class GuiOutlineView(QTreeWidget): class GuiOutlineHeaderMenu(QMenu): - def __init__(self, theParent): - QMenu.__init__(self, theParent) + columnToggled = pyqtSignal(bool, Enum) + + def __init__(self, theOutline): + QMenu.__init__(self, theOutline) - self.theParent = theParent self.acceptToggle = True mnuHead = QAction(self.tr("Select Columns"), self) @@ -575,7 +696,7 @@ class GuiOutlineHeaderMenu(QMenu): self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self) self.actionMap[hItem].setCheckable(True) self.actionMap[hItem].toggled.connect( - lambda isChecked, tItem=hItem: self._columnToggled(isChecked, tItem) + lambda isChecked, tItem=hItem: self.columnToggled.emit(isChecked, tItem) ) self.addAction(self.actionMap[hItem]) @@ -596,18 +717,6 @@ class GuiOutlineHeaderMenu(QMenu): return - ## - # Slots - ## - - def _columnToggled(self, isChecked, theItem): - """The user has toggled the visibility of a column. Forward the - event to the parent class only if we're accepting changes. - """ - if self.acceptToggle: - self.theParent._menuColumnToggled(isChecked, theItem) - return - # END Class GuiOutlineHeaderMenu @@ -945,16 +1054,12 @@ class GuiOutlineDetails(QScrollArea): # Internal Functions ## - def _formatTags(self, theRefs, theKey): + def _formatTags(self, refs, key): """Format the tags as clickable links. """ - if theKey not in theRefs: - return "" - refTags = [] - for tTag in theRefs[theKey]: - refTags.append("%s" % ( - theKey[1:], tTag, tTag - )) - return ", ".join(refTags) + mKey = key[1:] + return ", ".join( + [f"{tag}" for tag in refs.get(key, [])] + ) # END Class GuiOutlineDetails diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8f7814e0..80fd06e2 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -126,7 +126,7 @@ class GuiMain(QMainWindow): self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - self.treeView.rootFoldersChanged.connect(self.projView.updateClasses) + self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated) self.viewsBar.viewChangeRequested.connect(self._changeView) self.projView.viewChangeRequested.connect(self._changeView) @@ -353,7 +353,7 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.saveProject() self.docEditor.setDictionaries() - self.projView.updateClasses() + self.projView.projectUpdated() self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -499,9 +499,8 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) - self.mainMenu.setAutoOutline(self.theProject.autoOutline) self.statusBar.setRefTime(self.theProject.projOpened) - self.projView.updateClasses() + self.projView.projectUpdated() self._updateStatusWordCount() # Restore previously open documents, if any @@ -895,19 +894,6 @@ class GuiMain(QMainWindow): return True - def rebuildOutline(self): - """Force a rebuild of the Outline view. - """ - if not self.hasProject: - logger.error("No project open") - return False - - logger.verbose("Forcing a rebuild of the Project Outline") - self._changeView(nwView.OUTLINE) - self.projView.refreshView(overRide=True) - - return True - ## # Main Dialogs ## diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index b420e4f3..c6647dbd 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -61,7 +61,6 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI): assert nwGUI.editItem() is False assert nwGUI.requestNovelTreeRefresh() is False assert nwGUI.rebuildIndex() is False - assert nwGUI.rebuildOutline() is False assert nwGUI.showProjectSettingsDialog() is False assert nwGUI.showProjectDetailsDialog() is False assert nwGUI.showBuildProjectDialog() is False diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 02dd1041..c1c258e6 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -21,10 +21,7 @@ along with this program. If not, see . import pytest -from PyQt5.QtCore import Qt, QPoint -from PyQt5.QtWidgets import QAction, QTreeWidgetItem, QMessageBox - -from novelwriter.enum import nwOutline +from PyQt5.QtWidgets import QTreeWidgetItem, QMessageBox keyDelay = 2 typeDelay = 1 @@ -51,16 +48,16 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): assert outlineView.topLevelItemCount() > 0 # Context Menu - outlineView._headerRightClick(QPoint(1, 1)) - outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger) - outlineView.headerMenu.close() - qtbot.mouseClick(outlineView, Qt.LeftButton) + # outlineView._headerRightClick(QPoint(1, 1)) + # outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger) + # outlineView.headerMenu.close() + # qtbot.mouseClick(outlineView, Qt.LeftButton) - outlineView._loadHeaderState() - assert not outlineView._colHidden[nwOutline.CCOUNT] + # outlineView._loadHeaderState() + # assert not outlineView._colHidden[nwOutline.CCOUNT] # First Item - nwGUI.rebuildOutline() + outlineView.refreshTree() selItem = outlineView.topLevelItem(0) assert isinstance(selItem, QTreeWidgetItem) From 8ac30b036a147d90606cdb3bff8285285c429473 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 26 May 2022 12:11:22 +0200 Subject: [PATCH 100/179] Change how tag links are followed --- novelwriter/enum.py | 8 +++ novelwriter/gui/doceditor.py | 6 +- novelwriter/gui/docviewer.py | 36 +++--------- novelwriter/gui/outline.py | 108 ++++++++++++++++++++--------------- novelwriter/guimain.py | 48 +++++++++++++--- 5 files changed, 123 insertions(+), 83 deletions(-) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index fc9604e0..48b894ba 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -62,6 +62,14 @@ class nwItemLayout(Enum): # END Enum nwItemLayout +class nwDocMode(Enum): + + VIEW = 0 + EDIT = 1 + +# END Enum nwDocMode + + class nwDocAction(Enum): NO_ACTION = 0 diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 98648ab6..b73fe943 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -33,6 +33,7 @@ import bisect import logging import novelwriter +from enum import Enum from time import time from PyQt5.QtCore import ( @@ -50,7 +51,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.core import NWDoc, NWSpellEnchant, countWords -from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert +from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode from novelwriter.common import transferCase from novelwriter.constants import nwConst, nwKeyWords, nwUnicode from novelwriter.gui.dochighlight import GuiDocHighlighter @@ -69,6 +70,7 @@ class GuiDocEditor(QTextEdit): spellDictionaryChanged = pyqtSignal(str, str) docEditedStatusChanged = pyqtSignal(bool) docCountsChanged = pyqtSignal(str, int, int, int) + loadDocumentTagRequest = pyqtSignal(str, Enum) def __init__(self, theParent): QTextEdit.__init__(self, theParent) @@ -1895,7 +1897,7 @@ class GuiDocEditor(QTextEdit): if loadTag: logger.verbose("Attempting to follow tag '%s'", theWord) - self.theParent.docViewer.loadFromTag(theWord) + self.loadDocumentTagRequest.emit(theWord, nwDocMode.VIEW) else: logger.verbose("Potential tag '%s'", theWord) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 4c293da5..10456369 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -30,7 +30,9 @@ along with this program. If not, see . import logging import novelwriter -from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot +from enum import Enum + +from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot, pyqtSignal from PyQt5.QtGui import ( QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor ) @@ -40,7 +42,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.core import ToHtml -from novelwriter.enum import nwAlert, nwItemType, nwDocAction +from novelwriter.enum import nwItemType, nwDocAction, nwDocMode from novelwriter.error import logException from novelwriter.constants import nwUnicode @@ -49,6 +51,8 @@ logger = logging.getLogger(__name__) class GuiDocViewer(QTextBrowser): + loadDocumentTagRequest = pyqtSignal(str, Enum) + def __init__(self, theParent): QTextBrowser.__init__(self, theParent) @@ -239,30 +243,6 @@ class GuiDocViewer(QTextBrowser): self.updateDocMargins() return - def loadFromTag(self, theTag): - """Load text in the document from a reference given by a meta - tag rather than a known handle. This function depends on the - index being up to date. - """ - logger.debug("Loading document from tag '%s'", theTag) - tHandle, _, sTitle = self.theParent.theIndex.getTagSource(theTag) - if tHandle is None: - self.theParent.makeAlert(self.tr( - "Could not find the reference for tag '{0}'. It either doesn't " - "exist, or the index is out of date. The index can be updated " - "from the Tools menu, or by pressing {1}." - ).format( - theTag, "F9" - ), nwAlert.ERROR) - return False - else: - # Let the parent handle the opening as it also ensures that - # the doc view panel is visible in case this request comes - # from outside this class. - logger.verbose("Tag points to '%s#%s'", tHandle, sTitle) - self.theParent.viewDocument(tHandle, "#%s" % sTitle) - return True - def docAction(self, theAction): """Wrapper function for various document actions on the current document. @@ -414,14 +394,14 @@ class GuiDocViewer(QTextBrowser): @pyqtSlot("QUrl") def _linkClicked(self, theURL): - """Slot for a link in the document being clicked. + """Process a clicked link internally in the document. """ theLink = theURL.url() logger.verbose("Clicked link: '%s'", theLink) if len(theLink) > 0: theBits = theLink.split("=") if len(theBits) == 2: - self.loadFromTag(theBits[1]) + self.loadDocumentTagRequest.emit(theBits[1], nwDocMode.VIEW) return @pyqtSlot("QPoint") diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 6b50df5f..21af7e4d 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -43,7 +43,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.enum import ( - nwItemClass, nwItemLayout, nwItemType, nwOutline, nwView + nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline ) from novelwriter.common import checkInt from novelwriter.constants import trConst, nwKeyWords, nwLabels @@ -54,14 +54,13 @@ logger = logging.getLogger(__name__) class GuiOutline(QWidget): - viewChangeRequested = pyqtSignal(nwView) + loadDocumentTagRequest = pyqtSignal(str, Enum) def __init__(self, theParent): QWidget.__init__(self, theParent) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainConf = novelwriter.CONFIG + self.theParent = theParent self.outlineBar = GuiOutlineToolBar(self) self.outlineView = GuiOutlineView(self) @@ -82,7 +81,9 @@ class GuiOutline(QWidget): # Connect Signals self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns) - self.outlineBar.columnToggled.connect(self.outlineView.menuColumnToggled) + self.outlineView.activeItemChanged.connect(self.outlineData.showItem) + self.outlineData.itemTagClicked.connect(self._tagClicked) + self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled) self.outlineBar.viewRefreshRequested.connect( lambda: self.outlineView.refreshTree(overRide=True) ) @@ -144,13 +145,22 @@ class GuiOutline(QWidget): self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns) return + @pyqtSlot(str) + def _tagClicked(self, link): + """Capture the click of a tag in the details panel. + """ + if link: + self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW) + return + # END Class GuiOutline class GuiOutlineToolBar(QToolBar): - columnToggled = pyqtSignal(bool, Enum) + novelRootChanged = pyqtSignal(str) viewRefreshRequested = pyqtSignal() + viewColumnToggled = pyqtSignal(bool, Enum) def __init__(self, theOutline): QTreeWidget.__init__(self, theOutline) @@ -158,7 +168,6 @@ class GuiOutlineToolBar(QToolBar): logger.debug("Initialising GuiOutlineToolBar ...") self.mainConf = novelwriter.CONFIG - self.theOutline = theOutline self.theParent = theOutline.theParent self.theProject = theOutline.theParent.theProject self.theTheme = theOutline.theParent.theTheme @@ -180,6 +189,7 @@ class GuiOutlineToolBar(QToolBar): self.novelValue = QComboBox(self) self.novelValue.setMinimumWidth(self.mainConf.pxInt(200)) + self.novelValue.currentIndexChanged.connect(self._novelValueChanged) # Actions self.aRefresh = QAction(self.tr("Refresh"), self) @@ -191,7 +201,7 @@ class GuiOutlineToolBar(QToolBar): # Column Menu self.mColumns = GuiOutlineHeaderMenu(self) self.mColumns.columnToggled.connect( - lambda isChecked, tItem: self.columnToggled.emit(isChecked, tItem) + lambda isChecked, tItem: self.viewColumnToggled.emit(isChecked, tItem) ) self.tbColumns = QToolButton(self) @@ -207,10 +217,12 @@ class GuiOutlineToolBar(QToolBar): self.addWidget(self.tbColumns) self.addWidget(stretch) - self.populateNovelList() - logger.debug("GuiOutlineToolBar initialisation complete") + ## + # Methods + ## + def populateNovelList(self): """Fill the novel combo box. """ @@ -223,9 +235,23 @@ class GuiOutlineToolBar(QToolBar): return def setColumnHiddenState(self, hiddenState): + """Forward the change of column hidden states to the menu. + """ self.mColumns.setHiddenState(hiddenState) return + ## + # Slots + ## + + @pyqtSlot(int) + def _novelValueChanged(self, index): + """Emit a signal containing the handle of the selected item. + """ + if index >= 0: + self.novelRootChanged.emit(self.novelValue.currentData()) + return + # END Class GuiOutlineToolBar @@ -272,6 +298,7 @@ class GuiOutlineView(QTreeWidget): } hiddenStateChanged = pyqtSignal() + activeItemChanged = pyqtSignal(str, str) def __init__(self, theOutline): QTreeWidget.__init__(self, theOutline) @@ -279,7 +306,6 @@ class GuiOutlineView(QTreeWidget): logger.debug("Initialising GuiOutlineView ...") self.mainConf = novelwriter.CONFIG - self.theOutline = theOutline self.theParent = theOutline.theParent self.theProject = theOutline.theParent.theProject self.theTheme = theOutline.theParent.theTheme @@ -436,7 +462,7 @@ class GuiOutlineView(QTreeWidget): if selItems: tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole) - self.theOutline.outlineData.showItem(tHandle, sTitle) + self.activeItemChanged.emit(tHandle, sTitle) return @@ -729,6 +755,8 @@ class GuiOutlineDetails(QScrollArea): "H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"), } + itemTagClicked = pyqtSignal(str) + def __init__(self, theOutline): QScrollArea.__init__(self, theOutline) @@ -828,15 +856,18 @@ class GuiOutlineDetails(QScrollArea): self.entKeyValue.setWordWrap(True) self.cstKeyValue.setWordWrap(True) - self.povKeyValue.linkActivated.connect(self._tagClicked) - self.focKeyValue.linkActivated.connect(self._tagClicked) - self.chrKeyValue.linkActivated.connect(self._tagClicked) - self.pltKeyValue.linkActivated.connect(self._tagClicked) - self.timKeyValue.linkActivated.connect(self._tagClicked) - self.wldKeyValue.linkActivated.connect(self._tagClicked) - self.objKeyValue.linkActivated.connect(self._tagClicked) - self.entKeyValue.linkActivated.connect(self._tagClicked) - self.cstKeyValue.linkActivated.connect(self._tagClicked) + def tagClicked(link): + self.itemTagClicked.emit(link) + + self.povKeyValue.linkActivated.connect(tagClicked) + self.focKeyValue.linkActivated.connect(tagClicked) + self.chrKeyValue.linkActivated.connect(tagClicked) + self.pltKeyValue.linkActivated.connect(tagClicked) + self.timKeyValue.linkActivated.connect(tagClicked) + self.wldKeyValue.linkActivated.connect(tagClicked) + self.objKeyValue.linkActivated.connect(tagClicked) + self.entKeyValue.linkActivated.connect(tagClicked) + self.cstKeyValue.linkActivated.connect(tagClicked) self.povKeyLWrap.addWidget(self.povKeyValue, 1) self.focKeyLWrap.addWidget(self.focKeyValue, 1) @@ -963,6 +994,11 @@ class GuiOutlineDetails(QScrollArea): self.updateClasses() return + ## + # Slots + ## + + @pyqtSlot(str, str) def showItem(self, tHandle, sTitle): """Update the content of the tree with the given handle and line number pointing to a header. @@ -1006,22 +1042,6 @@ class GuiOutlineDetails(QScrollArea): return True - ## - # Slots - ## - - @pyqtSlot(str) - def _tagClicked(self, theLink): - """Capture the click of a tag in the right-most column. - """ - logger.verbose("Clicked link: '%s'", theLink) - if len(theLink) > 0: - theBits = theLink.split("=") - if len(theBits) == 2: - self.theOutline.viewChangeRequested.emit(nwView.PROJECT) - self.theParent.docViewer.loadFromTag(theBits[1]) - return - @pyqtSlot() def updateClasses(self): """Update the visibility status of class details. @@ -1050,16 +1070,12 @@ class GuiOutlineDetails(QScrollArea): return - ## - # Internal Functions - ## - - def _formatTags(self, refs, key): - """Format the tags as clickable links. + @staticmethod + def _formatTags(refs, key): + """Convert a list of tags into a list of clickable tag links. """ - mKey = key[1:] return ", ".join( - [f"{tag}" for tag in refs.get(key, [])] + [f"{tag}" for tag in refs.get(key, [])] ) # END Class GuiOutlineDetails diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 80fd06e2..67bf126b 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -27,6 +27,7 @@ import os import logging import novelwriter +from enum import Enum from time import time from datetime import datetime @@ -52,7 +53,7 @@ from novelwriter.tools import ( ) from novelwriter.core import NWProject, NWIndex from novelwriter.enum import ( - nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView + nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView ) from novelwriter.common import getGuiItem, hexToInt @@ -117,10 +118,7 @@ class GuiMain(QMainWindow): self.viewsBar = GuiViewsBar(self) # Connect Signals Between Main Elements - self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) - self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) - self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts) - self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts) + self.viewsBar.viewChangeRequested.connect(self._changeView) self.treeView.itemSelectionChanged.connect(self._treeSingleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) @@ -128,8 +126,15 @@ class GuiMain(QMainWindow): self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated) - self.viewsBar.viewChangeRequested.connect(self._changeView) - self.projView.viewChangeRequested.connect(self._changeView) + self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) + self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) + self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts) + self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts) + self.docEditor.loadDocumentTagRequest.connect(self._followTag) + + self.docViewer.loadDocumentTagRequest.connect(self._followTag) + + self.projView.loadDocumentTagRequest.connect(self._followTag) # Project Tree Stack self.projStack = QStackedWidget() @@ -1444,6 +1449,23 @@ class GuiMain(QMainWindow): return projData + def _getTagSource(self, tTag): + """A wrapper function for the index lookup of a tag that will + display an alert if the tag cannot be found. + """ + tHandle, _, sTitle = self.theIndex.getTagSource(tTag) + if tHandle is None: + self.makeAlert(self.tr( + "Could not find the reference for tag '{0}'. It either doesn't " + "exist, or the index is out of date. The index can be updated " + "from the Tools menu, or by pressing {1}." + ).format( + tTag, "F9" + ), nwAlert.ERROR) + return None, None + + return tHandle, sTitle + ## # Events ## @@ -1462,6 +1484,18 @@ class GuiMain(QMainWindow): # Slots ## + @pyqtSlot(str, Enum) + def _followTag(self, tTag, tMode): + """Follow a tag after user interaction with a link. + """ + tHandle, sTitle = self._getTagSource(tTag) + if tHandle is not None: + if tMode == nwDocMode.EDIT: + self.openDocument(tHandle) + elif tMode == nwDocMode.VIEW: + self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}") + return + @pyqtSlot(nwView) def _changeView(self, view): """Handle the requested change of view from the GuiViewBar. From 8bdd09400ad20290ac74ec3d273ff927c9db82b1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 28 May 2022 14:11:58 +0200 Subject: [PATCH 101/179] Move the project index into the project class --- novelwriter/core/project.py | 16 +++++++++++++--- novelwriter/core/tohtml.py | 2 +- novelwriter/core/tomd.py | 2 +- novelwriter/core/toodt.py | 2 +- novelwriter/dialogs/projdetails.py | 9 ++++----- novelwriter/gui/custom.py | 10 +++++----- novelwriter/gui/doceditor.py | 13 ++++++------- novelwriter/gui/dochighlight.py | 7 ++++--- novelwriter/gui/docviewer.py | 4 ++-- novelwriter/gui/itemdetails.py | 2 +- novelwriter/gui/noveltree.py | 11 ++++++----- novelwriter/gui/outline.py | 9 ++++----- novelwriter/gui/outlinedetails.py | 6 +++--- novelwriter/gui/projtree.py | 17 +++++++++-------- novelwriter/guimain.py | 17 ++++++++--------- tests/mock.py | 1 - tests/test_gui/test_gui_docviewer.py | 4 ++-- 17 files changed, 70 insertions(+), 62 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index b791f545..24a281b6 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -37,6 +37,7 @@ from PyQt5.QtCore import QCoreApplication from novelwriter.core.tree import NWTree from novelwriter.core.item import NWItem +from novelwriter.core.index import NWIndex from novelwriter.core.status import NWStatus from novelwriter.core.options import OptionState from novelwriter.core.document import NWDoc @@ -62,9 +63,10 @@ class NWProject(): self.mainConf = novelwriter.CONFIG # Core Elements - self.optState = OptionState(self) # Project-specific GUI options - self.projTree = NWTree(self) # The project tree - self.langData = {} # Localisation data + self.optState = OptionState(self) # Project-specific GUI options + self.projTree = NWTree(self) # The project tree + self._projIndex = NWIndex(self) # The projecty index + self.langData = {} # Localisation data # Project Status self.projOpened = 0 # The time stamp of when the project file was opened @@ -116,6 +118,14 @@ class NWProject(): return + ## + # Properties + ## + + @property + def index(self): + return self._projIndex + ## # Item Methods ## diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py index baa5165d..86a21786 100644 --- a/novelwriter/core/tohtml.py +++ b/novelwriter/core/tohtml.py @@ -451,7 +451,7 @@ class ToHtml(Tokenizer): def _formatKeywords(self, tText): """Apply HTML formatting to keywords. """ - isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText) + isValid, theBits, _ = self.theProject.index.scanThis("@"+tText) if not isValid or not theBits: return "" diff --git a/novelwriter/core/tomd.py b/novelwriter/core/tomd.py index bd468f55..48d23a35 100644 --- a/novelwriter/core/tomd.py +++ b/novelwriter/core/tomd.py @@ -193,7 +193,7 @@ class ToMarkdown(Tokenizer): def _formatKeywords(self, tText, tStyle): """Apply Markdown formatting to keywords. """ - isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText) + isValid, theBits, _ = self.theProject.index.scanThis("@"+tText) if not isValid or not theBits: return "" diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py index c0b1daee..59eaf30f 100644 --- a/novelwriter/core/toodt.py +++ b/novelwriter/core/toodt.py @@ -550,7 +550,7 @@ class ToOdt(Tokenizer): def _formatKeywords(self, tText): """Apply formatting to keywords. """ - isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText) + isValid, theBits, _ = self.theProject.index.scanThis("@"+tText) if not isValid or not theBits: return "" diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 54b6c995..491df296 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -145,7 +145,6 @@ class GuiProjectDetailsMain(QWidget): self.theParent = theParent self.theProject = theProject self.theTheme = theParent.theTheme - self.theIndex = theParent.theIndex fPx = self.theTheme.fontPixelSize fPt = self.theTheme.fontPointSize @@ -245,8 +244,9 @@ class GuiProjectDetailsMain(QWidget): def updateValues(self): """Set all the values. """ - hCounts = self.theIndex.getNovelTitleCounts() - nwCount = self.theIndex.getNovelWordCount() + pIndex = self.theProject.index + hCounts = pIndex.getNovelTitleCounts() + nwCount = pIndex.getNovelWordCount() edTime = self.theProject.getCurrentEditTime() self.wordCountVal.setText(f"{nwCount:n}") @@ -277,7 +277,6 @@ class GuiProjectDetailsContents(QWidget): self.theParent = theParent self.theProject = theProject self.theTheme = theParent.theTheme - self.theIndex = theParent.theIndex self.optState = theProject.optState # Internal @@ -424,7 +423,7 @@ class GuiProjectDetailsContents(QWidget): """Extract the data for the tree. """ self._theToC = [] - self._theToC = self.theIndex.getTableOfContents(2) + self._theToC = self.theProject.index.getTableOfContents(2) self._theToC.append(("", 0, self.tr("END"), 0)) return diff --git a/novelwriter/gui/custom.py b/novelwriter/gui/custom.py index 16d26b40..62dd3c91 100644 --- a/novelwriter/gui/custom.py +++ b/novelwriter/gui/custom.py @@ -409,10 +409,10 @@ class PagedDialog(QDialog): return - def addTab(self, tabWidget, tabLabel): + def addTab(self, widget, label): """Forwards the adding of tabs to the QTabWidget. """ - self._tabBox.addTab(tabWidget, tabLabel) + self._tabBox.addTab(widget, label) return def addControls(self, buttonBar): @@ -431,15 +431,15 @@ class VerticalTabBar(QTabBar): self._mW = novelwriter.CONFIG.pxInt(150) return - def tabSizeHint(self, theIndex): + def tabSizeHint(self, index): """Returns a transposed size hint for the rotated bar. """ - tSize = QTabBar.tabSizeHint(self, theIndex) + tSize = QTabBar.tabSizeHint(self, index) tSize.transpose() tSize.setWidth(min(tSize.width(), self._mW)) return tSize - def paintEvent(self, theEvent): + def paintEvent(self, event): """Custom implementation of the label painter that rotates the label 90 degrees. """ diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 98648ab6..9cd99497 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -79,7 +79,6 @@ class GuiDocEditor(QTextEdit): self.mainConf = novelwriter.CONFIG self.theParent = theParent self.theTheme = theParent.theTheme - self.theIndex = theParent.theIndex self.theProject = theParent.theProject self._nwDocument = None @@ -401,7 +400,7 @@ class GuiDocEditor(QTextEdit): self.document().rootFrame().setFrameFormat(docFrame) self.docFooter.updateLineCount() - self._docHeaders = self.theIndex.getHandleHeaders(self._docHandle) + self._docHeaders = self.theProject.index.getHandleHeaders(self._docHandle) qApp.processEvents() self.document().clearUndoRedoStacks() @@ -506,9 +505,9 @@ class GuiDocEditor(QTextEdit): self.setDocumentChanged(False) - oldHeader = self.theIndex.getHandleHeaderLevel(tHandle) - self.theIndex.scanText(tHandle, docText) - newHeader = self.theIndex.getHandleHeaderLevel(tHandle) + oldHeader = self.theProject.index.getHandleHeaderLevel(tHandle) + self.theProject.index.scanText(tHandle, docText) + newHeader = self.theProject.index.getHandleHeaderLevel(tHandle) if self._updateHeaders(checkLevel=True): self.theParent.requestNovelTreeRefresh() @@ -2003,7 +2002,7 @@ class GuiDocEditor(QTextEdit): if self._docHandle is None: return False - newHeaders = self.theIndex.getHandleHeaders(self._docHandle) + newHeaders = self.theProject.index.getHandleHeaders(self._docHandle) if checkPos: newPos = [x[0] for x in newHeaders] oldPos = [x[0] for x in self._docHeaders] @@ -2943,7 +2942,7 @@ class GuiDocEditFooter(QWidget): else: theStatus, theIcon = self._theItem.getImportStatus() sIcon = theIcon.pixmap(self.sPx, self.sPx) - hLevel = self.theParent.theIndex.getHandleHeaderLevel(self._docHandle) + hLevel = self.theProject.index.getHandleHeaderLevel(self._docHandle) sText = f"{theStatus} / {self._theItem.describeMe(hLevel)}" self.statusIcon.setPixmap(sIcon) diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index eddc27c3..0133f91c 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -55,7 +55,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.spEnchant = spEnchant self.theParent = theParent self.theTheme = theParent.theTheme - self.theIndex = theParent.theIndex + self.theProject = theParent.theProject self.theHandle = None self.spellCheck = False self.spellRx = None @@ -287,9 +287,10 @@ class GuiDocHighlighter(QSyntaxHighlighter): if theText.startswith("@"): # Keywords and commands self.setCurrentBlockState(self.BLOCK_META) + pIndex = self.theProject.index tItem = self.theParent.theProject.projTree[self.theHandle] - isValid, theBits, thePos = self.theIndex.scanThis(theText) - isGood = self.theIndex.checkThese(theBits, tItem) + isValid, theBits, thePos = pIndex.scanThis(theText) + isGood = pIndex.checkThese(theBits, tItem) if isValid: for n, theBit in enumerate(theBits): xPos = thePos[n] diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 4c293da5..615f121c 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -245,7 +245,7 @@ class GuiDocViewer(QTextBrowser): index being up to date. """ logger.debug("Loading document from tag '%s'", theTag) - tHandle, _, sTitle = self.theParent.theIndex.getTagSource(theTag) + tHandle, _, sTitle = self.theProject.index.getTagSource(theTag) if tHandle is None: self.theParent.makeAlert(self.tr( "Could not find the reference for tag '{0}'. It either doesn't " @@ -1199,7 +1199,7 @@ class GuiDocViewDetails(QScrollArea): if self.theParent.docViewer.stickyRef: return - theRefs = self.theParent.theIndex.getBackReferenceList(tHandle) + theRefs = self.theProject.index.getBackReferenceList(tHandle) theList = [] for tHandle in theRefs: tItem = self.theProject.projTree[tHandle] diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 8b42b752..88419395 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -269,7 +269,7 @@ class GuiItemDetails(QWidget): # Layout # ====== - hLevel = self.theParent.theIndex.getHandleHeaderLevel(tHandle) + hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) usageIcon = self.theTheme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel ) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index d0732abe..91895c7a 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -54,7 +54,6 @@ class GuiNovelTree(QTreeWidget): self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theParent.theProject - self.theIndex = theParent.theIndex # Internal Variables self._treeMap = {} @@ -137,7 +136,7 @@ class GuiNovelTree(QTreeWidget): """ logger.verbose("Requesting refresh of the novel tree") treeChanged = self.theParent.treeView.changedSince(self._lastBuild) - indexChanged = self.theIndex.novelChangedSince(self._lastBuild) + indexChanged = self.theProject.index.novelChangedSince(self._lastBuild) if not (treeChanged or indexChanged or overRide): logger.verbose("No changes have been made to the novel index") return @@ -158,7 +157,7 @@ class GuiNovelTree(QTreeWidget): def updateWordCounts(self, tHandle): """Update the word count for a given handle. """ - tHeaders = self.theIndex.getHandleWordCounts(tHandle) + tHeaders = self.theProject.index.getHandleWordCounts(tHandle) for titleKey, wCount in tHeaders: if titleKey in self._treeMap: self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}") @@ -252,7 +251,9 @@ class GuiNovelTree(QTreeWidget): currChapter = None currScene = None - for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True): + for tKey, tHandle, sTitle, novIdx in self.theProject.index.novelStructure( + skipExcluded=True + ): tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) self._treeMap[tKey] = tItem @@ -315,7 +316,7 @@ class GuiNovelTree(QTreeWidget): newItem.setText(self.C_WORDS, f"{wC:n}") newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - theRefs = self.theIndex.getReferences(tHandle, sTitle) + theRefs = self.theProject.index.getReferences(tHandle, sTitle) newItem.setText(self.C_POV, ", ".join(theRefs[nwKeyWords.POV_KEY])) return newItem diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 4028ccf6..2b7a654b 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -91,7 +91,6 @@ class GuiOutline(QTreeWidget): self.theParent = theParent self.theProject = theParent.theProject self.theTheme = theParent.theTheme - self.theIndex = theParent.theIndex self.optState = theParent.theProject.optState self.headerMenu = GuiOutlineHeaderMenu(self) @@ -182,7 +181,7 @@ class GuiOutline(QTreeWidget): # If the novel index or novel tree has changed since the tree # was last built, we rebuild the tree from the updated index. - indexChanged = self.theIndex.novelChangedSince(self._lastBuild) + indexChanged = self.theProject.index.novelChangedSince(self._lastBuild) doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline if doBuild or overRide: logger.debug("Rebuilding Project Outline") @@ -388,7 +387,7 @@ class GuiOutline(QTreeWidget): currChapter = None currScene = None - for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True): + for _, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcluded=True): tItem = self._createTreeItem(tHandle, sTitle, novIdx) @@ -442,7 +441,7 @@ class GuiOutline(QTreeWidget): newItem = QTreeWidgetItem() hIcon = "doc_%s" % novIdx["level"].lower() - hLevel = self.theIndex.getHandleHeaderLevel(tHandle) + hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) dIcon = self.theTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel) cC = int(novIdx["cCount"]) @@ -465,7 +464,7 @@ class GuiOutline(QTreeWidget): newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) - theRefs = self.theIndex.getReferences(tHandle, sTitle) + theRefs = self.theProject.index.getReferences(tHandle, sTitle) newItem.setText(self._colIdx[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY])) newItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY])) newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY])) diff --git a/novelwriter/gui/outlinedetails.py b/novelwriter/gui/outlinedetails.py index 00c20e44..92d41c3c 100644 --- a/novelwriter/gui/outlinedetails.py +++ b/novelwriter/gui/outlinedetails.py @@ -58,7 +58,6 @@ class GuiOutlineDetails(QScrollArea): self.theParent = theParent self.theProject = theParent.theProject self.theTheme = theParent.theTheme - self.theIndex = theParent.theIndex self.optState = theParent.theProject.optState # Sizes @@ -283,9 +282,10 @@ class GuiOutlineDetails(QScrollArea): """Update the content of the tree with the given handle and line number pointing to a header. """ + pIndex = self.theProject.index nwItem = self.theProject.projTree[tHandle] - novIdx = self.theIndex.getNovelData(tHandle, sTitle) - theRefs = self.theIndex.getReferences(tHandle, sTitle) + novIdx = pIndex.getNovelData(tHandle, sTitle) + theRefs = pIndex.getReferences(tHandle, sTitle) if nwItem is None or novIdx is None: return False diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index b134b901..fd9e21ce 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -61,7 +61,6 @@ class GuiProjectTree(QTreeWidget): self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theParent.theProject - self.theIndex = theParent.theIndex # Internal Variables self._treeMap = {} @@ -236,12 +235,14 @@ class GuiProjectTree(QTreeWidget): else: newText = f"# {nwItem.itemName}\n\n" + pIndex = self.theProject.index + # Save the text and index it newDoc.writeDocument(newText) - self.theIndex.scanText(tHandle, newText) + pIndex.scanText(tHandle, newText) # Get Word Counts - cC, wC, pC = self.theIndex.getCounts(tHandle) + cC, wC, pC = pIndex.getCounts(tHandle) nwItem.setCharCount(cC) nwItem.setWordCount(wC) nwItem.setParaCount(pC) @@ -542,7 +543,7 @@ class GuiProjectTree(QTreeWidget): expIcon = self.theTheme.getIcon("cross") itempStatus, statusIcon = nwItem.getImportStatus() - hLevel = self.theIndex.getHandleHeaderLevel(tHandle) + hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) itemIcon = self.theTheme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel ) @@ -598,7 +599,7 @@ class GuiProjectTree(QTreeWidget): if self.theProject.projTree.checkType(pHandle, nwItemType.FILE): # A file has an internal word count we need to account # for, but a folder always has 0 words on its own. - pCount += self.theIndex.getCounts(pHandle)[1] + pCount += self.theProject.index.getCounts(pHandle)[1] self.propagateCount(pHandle, pCount, countChildren=False) @@ -817,9 +818,9 @@ class GuiProjectTree(QTreeWidget): # Update the index if nwItemS.isInactive(): - self.theIndex.deleteHandle(mHandle) + self.theProject.index.deleteHandle(mHandle) else: - self.theIndex.reIndexHandle(mHandle) + self.theProject.index.reIndexHandle(mHandle) self.setTreeItemValues(mHandle) @@ -854,7 +855,7 @@ class GuiProjectTree(QTreeWidget): ], nwAlert.ERROR) return False - self.theIndex.deleteHandle(tHandle) + self.theProject.index.deleteHandle(tHandle) del self.theProject.projTree[tHandle] self._treeMap.pop(tHandle, None) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index a759972a..34f9abcd 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -50,7 +50,7 @@ from novelwriter.dialogs import ( from novelwriter.tools import ( GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats ) -from novelwriter.core import NWProject, NWIndex +from novelwriter.core import NWProject from novelwriter.enum import ( nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView ) @@ -86,7 +86,6 @@ class GuiMain(QMainWindow): # Core Classes and Settings self.theTheme = GuiTheme() self.theProject = NWProject(self) - self.theIndex = NWIndex(self.theProject) self.hasProject = False self.isFocusMode = False self.idleRefTime = time() @@ -420,7 +419,7 @@ class GuiMain(QMainWindow): self.idleRefTime = time() self.idleTime = 0.0 - self.theIndex.clearIndex() + self.theProject.index.clearIndex() self.clearGUI() self.hasProject = False self._changeView(nwView.PROJECT) @@ -497,7 +496,7 @@ class GuiMain(QMainWindow): self.idleTime = 0.0 # Load the tag index - self.theIndex.loadIndex() + self.theProject.index.loadIndex() # Update GUI self._updateWindowTitle(self.theProject.projName) @@ -516,7 +515,7 @@ class GuiMain(QMainWindow): self.viewDocument(self.theProject.lastViewed) # Check if we need to rebuild the index - if self.theIndex.indexBroken: + if self.theProject.index.indexBroken: self.makeAlert(self.tr( "The project index is outdated or broken. Rebuilding index." ), nwAlert.INFO) @@ -540,7 +539,7 @@ class GuiMain(QMainWindow): self.treeView.saveTreeOrder() if self.theProject.saveProject(autoSave=autoSave): - self.theIndex.saveIndex() + self.theProject.index.saveIndex() return True @@ -863,7 +862,7 @@ class GuiMain(QMainWindow): tStart = time() self.treeView.saveTreeOrder() - self.theIndex.clearIndex() + self.theProject.index.clearIndex() for tItem in self.theProject.projTree: @@ -874,10 +873,10 @@ class GuiMain(QMainWindow): if tItem is not None and tItem.itemType == nwItemType.FILE: logger.verbose("Scanning '%s'", tItem.itemName) - self.theIndex.reIndexHandle(tItem.itemHandle) + self.theProject.index.reIndexHandle(tItem.itemHandle) # Get Word Counts - cC, wC, pC = self.theIndex.getCounts(tItem.itemHandle) + cC, wC, pC = self.theProject.index.getCounts(tItem.itemHandle) tItem.setCharCount(cC) tItem.setWordCount(wC) tItem.setParaCount(pC) diff --git a/tests/mock.py b/tests/mock.py index 4b272a17..23938d41 100644 --- a/tests/mock.py +++ b/tests/mock.py @@ -29,7 +29,6 @@ class MockGuiMain(): def __init__(self): self.mainConf = None self.hasProject = True - self.theIndex = None self.theProject = None self.statusBar = MockStatusBar() diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 0a11bc3d..48f61eff 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -47,8 +47,8 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): # Rebuild the index nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger) - assert nwGUI.theIndex._tagIndex != {} - assert nwGUI.theIndex._refIndex != {} + assert nwGUI.theProject.index._tagIndex != {} + assert nwGUI.theProject.index._refIndex != {} # Select a document in the project tree nwGUI.treeView.setSelectedHandle("88243afbe5ed8") From 44926a4e9a7039f5fe4ac871d1d89372c0cc723c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 28 May 2022 14:33:25 +0200 Subject: [PATCH 102/179] Also make tree and options properties of the project --- novelwriter/core/document.py | 2 +- novelwriter/core/index.py | 6 +- novelwriter/core/project.py | 86 ++++++++++++---------- novelwriter/core/tokenizer.py | 6 +- novelwriter/dialogs/docmerge.py | 8 +- novelwriter/dialogs/docsplit.py | 13 ++-- novelwriter/dialogs/itemeditor.py | 2 +- novelwriter/dialogs/projdetails.py | 45 ++++++------ novelwriter/dialogs/projsettings.py | 23 +++--- novelwriter/dialogs/wordlist.py | 11 +-- novelwriter/gui/doceditor.py | 9 +-- novelwriter/gui/dochighlight.py | 2 +- novelwriter/gui/docviewer.py | 10 +-- novelwriter/gui/itemdetails.py | 2 +- novelwriter/gui/outline.py | 20 ++--- novelwriter/gui/outlinedetails.py | 3 +- novelwriter/gui/projtree.py | 44 +++++------ novelwriter/guimain.py | 12 +-- novelwriter/tools/build.py | 90 +++++++++++------------ novelwriter/tools/writingstats.py | 67 ++++++++--------- tests/test_core/test_core_document.py | 2 +- tests/test_core/test_core_index.py | 32 ++++---- tests/test_core/test_core_project.py | 50 ++++++------- tests/test_dialogs/test_dlg_itemeditor.py | 4 +- tests/test_gui/test_gui_doceditor.py | 18 ++--- tests/test_gui/test_gui_docviewer.py | 2 +- tests/test_gui/test_gui_guimain.py | 20 ++--- tests/test_gui/test_gui_projtree.py | 50 ++++++------- 28 files changed, 324 insertions(+), 315 deletions(-) diff --git a/novelwriter/core/document.py b/novelwriter/core/document.py index 2334c77c..5420d44e 100644 --- a/novelwriter/core/document.py +++ b/novelwriter/core/document.py @@ -52,7 +52,7 @@ class NWDoc(): self._docHandle = theHandle if self._docHandle is not None: - self._theItem = self.theProject.projTree[theHandle] + self._theItem = self.theProject.tree[theHandle] return diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 9b8dd47c..7647a9f6 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -107,7 +107,7 @@ class NWIndex(): project. """ logger.debug("Re-indexing item '%s'", tHandle) - if not self.theProject.projTree.checkType(tHandle, nwItemType.FILE): + if not self.theProject.tree.checkType(tHandle, nwItemType.FILE): return False theDoc = NWDoc(self.theProject, tHandle) @@ -207,7 +207,7 @@ class NWIndex(): files before we save them in which case we already have the text. """ - theItem = self.theProject.projTree[tHandle] + theItem = self.theProject.tree[tHandle] if theItem is None: logger.info("Not indexing unknown item '%s'", tHandle) return False @@ -639,7 +639,7 @@ class NWIndex(): """Return a list of all handles that exist in the novel index. """ theHandles = [] - for tItem in self.theProject.projTree: + for tItem in self.theProject.tree: if tItem is None: continue if not tItem.isExported and skipExcluded: diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 24a281b6..0a13cb16 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -63,10 +63,10 @@ class NWProject(): self.mainConf = novelwriter.CONFIG # Core Elements - self.optState = OptionState(self) # Project-specific GUI options - self.projTree = NWTree(self) # The project tree + self._optState = OptionState(self) # Project-specific GUI options + self._projTree = NWTree(self) # The project tree self._projIndex = NWIndex(self) # The projecty index - self.langData = {} # Localisation data + self._langData = {} # Localisation data # Project Status self.projOpened = 0 # The time stamp of when the project file was opened @@ -123,9 +123,17 @@ class NWProject(): ## @property - def index(self): + def index(self) -> NWIndex: return self._projIndex + @property + def tree(self) -> NWTree: + return self._projTree + + @property + def options(self) -> OptionState: + return self._optState + ## # Item Methods ## @@ -139,8 +147,8 @@ class NWProject(): newItem.setName(label) newItem.setType(nwItemType.ROOT) newItem.setClass(itemClass) - self.projTree.append(None, None, newItem) - self.projTree.updateItemData(newItem.itemHandle) + self._projTree.append(None, None, newItem) + self._projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def newFolder(self, label, pHandle): @@ -149,8 +157,8 @@ class NWProject(): newItem = NWItem(self) newItem.setName(label) newItem.setType(nwItemType.FOLDER) - self.projTree.append(None, pHandle, newItem) - self.projTree.updateItemData(newItem.itemHandle) + self._projTree.append(None, pHandle, newItem) + self._projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def newFile(self, label, pHandle): @@ -159,21 +167,21 @@ class NWProject(): newItem = NWItem(self) newItem.setName(label) newItem.setType(nwItemType.FILE) - self.projTree.append(None, pHandle, newItem) - self.projTree.updateItemData(newItem.itemHandle) + self._projTree.append(None, pHandle, newItem) + self._projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def trashFolder(self): """Add the special trash root folder to the project. """ - trashHandle = self.projTree.trashRoot() + trashHandle = self._projTree.trashRoot() if trashHandle is None: newItem = NWItem(self) newItem.setName(trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])) newItem.setType(nwItemType.ROOT) newItem.setClass(nwItemClass.TRASH) - self.projTree.append(None, None, newItem) - self.projTree.updateItemData(newItem.itemHandle) + self._projTree.append(None, None, newItem) + self._projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle return trashHandle @@ -194,7 +202,7 @@ class NWProject(): self.autoCount = 0 # Project Tree - self.projTree.clear() + self._projTree.clear() # Project Settings self.projPath = None @@ -598,9 +606,9 @@ class NWProject(): elif xChild.tag == "content": logger.debug("Found project content") - self.projTree.unpackXML(xChild) + self._projTree.unpackXML(xChild) - self.optState.loadSettings() + self._optState.loadSettings() # Sort out old file locations if legacyList: @@ -618,12 +626,12 @@ class NWProject(): self.mainConf.saveRecentCache() # Check the project tree consistency - for tItem in self.projTree: + for tItem in self._projTree: tHandle = tItem.itemHandle logger.verbose("Checking item '%s'", tHandle) - if not self.projTree.updateItemData(tHandle): + if not self._projTree.updateItemData(tHandle): logger.error("There was a problem item '%s', and it has been removed", tHandle) - del self.projTree[tHandle] # The file will be re-added as orphaned + del self._projTree[tHandle] # The file will be re-added as orphaned self._scanProjectFolder() self._loadProjectLocalisation() @@ -710,7 +718,7 @@ class NWProject(): # Save Tree Content logger.debug("Writing project content") - self.projTree.packXML(nwXML) + self._projTree.packXML(nwXML) # Write the xml tree to file tempFile = os.path.join(self.projPath, self.projFile+"~") @@ -743,7 +751,7 @@ class NWProject(): return False # Save project GUI options - self.optState.saveSettings() + self._optState.saveSettings() # Update recent projects self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime) @@ -759,8 +767,8 @@ class NWProject(): """Close the current project and clear all meta data. """ logger.info("Closing project: %s", self.projPath) - self.optState.saveSettings() - self.projTree.writeToCFile() + self._optState.saveSettings() + self._projTree.writeToCFile() self._appendSessionStats(idleTime) self._clearLockFile() self.clearProject() @@ -1060,9 +1068,9 @@ class NWProject(): items in the GUI project tree. The user can rearrange the order by drag-and-drop. Forwarded to the NWTree class. """ - if len(self.projTree) != len(newOrder): + if len(self._projTree) != len(newOrder): logger.warning("Sizes of new and old tree order do not match") - self.projTree.setOrder(newOrder) + self._projTree.setOrder(newOrder) self.setProjectChanged(True) return True @@ -1156,16 +1164,16 @@ class NWProject(): capable of handling it. """ sentItems = [] - iterItems = self.projTree.handles() + iterItems = self._projTree.handles() n = 0 nMax = min(len(iterItems), 10000) while n < nMax: tHandle = iterItems[n] - tItem = self.projTree[tHandle] + tItem = self._projTree[tHandle] n += 1 if tItem is None: # Technically a bug since treeOrder is built from the - # same data as projTree + # same data as _projTree continue elif tItem.itemParent is None: # Item is a root, or already been identified as an @@ -1196,7 +1204,7 @@ class NWProject(): def updateWordCounts(self): """Update the total word count values. """ - wcNovel, wcNotes = self.projTree.sumWords() + wcNovel, wcNotes = self._projTree.sumWords() wcTotal = wcNovel + wcNotes if wcTotal != self.currWCount: self.currNovelWC = wcNovel @@ -1212,7 +1220,7 @@ class NWProject(): """ self.statusItems.resetCounts() self.importItems.resetCounts() - for nwItem in self.projTree: + for nwItem in self._projTree: if nwItem.isNovelLike(): self.statusItems.increment(nwItem.itemStatus) else: @@ -1224,7 +1232,7 @@ class NWProject(): return it. The variable is cast to a string before lookup. If the word does not exist, it returns itself. """ - return self.langData.get(str(theWord), str(theWord)) + return self._langData.get(str(theWord), str(theWord)) ## # Internal Functions @@ -1256,7 +1264,7 @@ class NWProject(): """Load the language data for the current project language. """ if self.projLang is None: - self.langData = {} + self._langData = {} return False langFile = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self.projLang) @@ -1265,7 +1273,7 @@ class NWProject(): try: with open(langFile, mode="r", encoding="utf-8") as inFile: - self.langData = json.load(inFile) + self._langData = json.load(inFile) logger.debug("Loaded project language file: %s", os.path.basename(langFile)) except Exception: @@ -1400,7 +1408,7 @@ class NWProject(): logger.warning("Skipping file: %s", fileItem) continue - if fHandle in self.projTree: + if fHandle in self._projTree: self.projFiles.append(fHandle) logger.debug("Checking file %s, handle '%s': OK", fileItem, fHandle) else: @@ -1447,10 +1455,10 @@ class NWProject(): if oLayout is None: oLayout = nwItemLayout.NOTE - if oParent is None or oParent not in self.projTree: - oParent = self.projTree.findRoot(oClass) + if oParent is None or oParent not in self._projTree: + oParent = self._projTree.findRoot(oClass) if oParent is None: - oParent = self.projTree.findRoot(nwItemClass.NOVEL) + oParent = self._projTree.findRoot(nwItemClass.NOVEL) # If the file still has no parent item, skip it if oParent is None: @@ -1462,8 +1470,8 @@ class NWProject(): orphItem.setType(nwItemType.FILE) orphItem.setClass(oClass) orphItem.setLayout(oLayout) - self.projTree.append(oHandle, oParent, orphItem) - self.projTree.updateItemData(orphItem.itemHandle) + self._projTree.append(oHandle, oParent, orphItem) + self._projTree.updateItemData(orphItem.itemHandle) if noWhere: self.theParent.makeAlert(self.tr( diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index 271ffd2c..bb5f3d8e 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -275,7 +275,7 @@ class Tokenizer(ABC): def addRootHeading(self, theHandle): """Add a heading at the start of a new root folder. """ - if not self.theProject.projTree.checkType(theHandle, nwItemType.ROOT): + if not self.theProject.tree.checkType(theHandle, nwItemType.ROOT): return False if self._isFirst: @@ -284,7 +284,7 @@ class Tokenizer(ABC): else: textAlign = self.A_PBB | self.A_CENTRE - theItem = self.theProject.projTree[theHandle] + theItem = self.theProject.tree[theHandle] locNotes = self._localLookup("Notes") theTitle = f"{locNotes}: {theItem.itemName}" self._theTokens = [] @@ -301,7 +301,7 @@ class Tokenizer(ABC): not set, load it from the file. """ self._theHandle = theHandle - self._theItem = self.theProject.projTree[theHandle] + self._theItem = self.theProject.tree[theHandle] if self._theItem is None: return False diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index c082dd3b..033f3698 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -125,13 +125,13 @@ class GuiDocMerge(QDialog): ), nwAlert.ERROR) return False - srcItem = self.theProject.projTree[self.sourceItem] + srcItem = self.theProject.tree[self.sourceItem] if srcItem is None: self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR) return False nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent) - newItem = self.theProject.projTree[nHandle] + newItem = self.theProject.tree[nHandle] newItem.setStatus(srcItem.itemStatus) newItem.setImport(srcItem.itemImport) @@ -170,7 +170,7 @@ class GuiDocMerge(QDialog): if tHandle is None: return False - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] if nwItem is None: return False @@ -182,7 +182,7 @@ class GuiDocMerge(QDialog): for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle): newItem = QListWidgetItem() - nwItem = self.theProject.projTree[sHandle] + nwItem = self.theProject.tree[sHandle] if nwItem.itemType is not nwItemType.FILE: continue newItem.setText(nwItem.itemName) diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index ff2cb849..76e64d5f 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -50,7 +50,6 @@ class GuiDocSplit(QDialog): self.mainConf = novelwriter.CONFIG self.theParent = theParent self.theProject = theParent.theProject - self.optState = theParent.theProject.optState self.sourceItem = None self.sourceText = [] @@ -75,7 +74,7 @@ class GuiDocSplit(QDialog): self.splitLevel.addItem(self.tr("Split up to Header Level 3 (Scene)"), 3) self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4) spIndex = self.splitLevel.findData( - self.optState.getInt("GuiDocSplit", "spLevel", 3) + self.theProject.options.getInt("GuiDocSplit", "spLevel", 3) ) if spIndex != -1: self.splitLevel.setCurrentIndex(spIndex) @@ -121,7 +120,7 @@ class GuiDocSplit(QDialog): ), nwAlert.ERROR) return False - srcItem = self.theProject.projTree[self.sourceItem] + srcItem = self.theProject.tree[self.sourceItem] if srcItem is None: self.theParent.makeAlert(self.tr( "Could not parse source document." @@ -184,7 +183,7 @@ class GuiDocSplit(QDialog): wTitle = wTitle.lstrip("#").strip() nHandle = self.theProject.newFile(wTitle, fHandle) - newItem = self.theProject.projTree[nHandle] + newItem = self.theProject.tree[nHandle] newItem.setStatus(srcItem.itemStatus) newItem.setImport(srcItem.itemImport) logger.verbose( @@ -211,7 +210,7 @@ class GuiDocSplit(QDialog): def _doClose(self): """Close the dialog window without doing anything. """ - self.optState.saveSettings() + self.theProject.options.saveSettings() self.close() return @@ -232,7 +231,7 @@ class GuiDocSplit(QDialog): if self.sourceItem is None: return False - nwItem = self.theProject.projTree[self.sourceItem] + nwItem = self.theProject.tree[self.sourceItem] if nwItem is None: return False @@ -249,7 +248,7 @@ class GuiDocSplit(QDialog): return False spLevel = self.splitLevel.currentData() - self.optState.setValue("GuiDocSplit", "spLevel", spLevel) + self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel) logger.debug( "Scanning document '%s' for headings level <= %d", self.sourceItem, spLevel diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py index b5faec0d..acf07134 100644 --- a/novelwriter/dialogs/itemeditor.py +++ b/novelwriter/dialogs/itemeditor.py @@ -55,7 +55,7 @@ class GuiItemEditor(QDialog): # Build GUI ## - self.theItem = self.theProject.projTree[tHandle] + self.theItem = self.theProject.tree[tHandle] if self.theItem is None: self.close() return diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 491df296..258b82e4 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -52,18 +52,18 @@ class GuiProjectDetails(PagedDialog): self.mainConf = novelwriter.CONFIG self.theParent = theParent self.theProject = theParent.theProject - self.optState = theParent.theProject.optState self.setWindowTitle(self.tr("Project Details")) wW = self.mainConf.pxInt(600) wH = self.mainConf.pxInt(400) + pOptions = self.theProject.options self.setMinimumWidth(wW) self.setMinimumHeight(wH) self.resize( - self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winWidth", wW)), - self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winHeight", wH)) + self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "winWidth", wW)), + self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH)) ) self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject) @@ -120,16 +120,17 @@ class GuiProjectDetails(PagedDialog): countFrom = self.tabContents.poValue.value() clearDouble = self.tabContents.dblValue.isChecked() - self.optState.setValue("GuiProjectDetails", "winWidth", winWidth) - self.optState.setValue("GuiProjectDetails", "winHeight", winHeight) - self.optState.setValue("GuiProjectDetails", "widthCol0", widthCol0) - self.optState.setValue("GuiProjectDetails", "widthCol1", widthCol1) - self.optState.setValue("GuiProjectDetails", "widthCol2", widthCol2) - self.optState.setValue("GuiProjectDetails", "widthCol3", widthCol3) - self.optState.setValue("GuiProjectDetails", "widthCol4", widthCol4) - self.optState.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage) - self.optState.setValue("GuiProjectDetails", "countFrom", countFrom) - self.optState.setValue("GuiProjectDetails", "clearDouble", clearDouble) + pOptions = self.theProject.options + pOptions.setValue("GuiProjectDetails", "winWidth", winWidth) + pOptions.setValue("GuiProjectDetails", "winHeight", winHeight) + pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0) + pOptions.setValue("GuiProjectDetails", "widthCol1", widthCol1) + pOptions.setValue("GuiProjectDetails", "widthCol2", widthCol2) + pOptions.setValue("GuiProjectDetails", "widthCol3", widthCol3) + pOptions.setValue("GuiProjectDetails", "widthCol4", widthCol4) + pOptions.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage) + pOptions.setValue("GuiProjectDetails", "countFrom", countFrom) + pOptions.setValue("GuiProjectDetails", "clearDouble", clearDouble) return @@ -277,7 +278,6 @@ class GuiProjectDetailsContents(QWidget): self.theParent = theParent self.theProject = theProject self.theTheme = theParent.theTheme - self.optState = theProject.optState # Internal self._theToC = [] @@ -285,6 +285,7 @@ class GuiProjectDetailsContents(QWidget): iPx = self.theTheme.baseIconSize hPx = self.mainConf.pxInt(12) vPx = self.mainConf.pxInt(4) + pOptions = self.theProject.options # Contents Tree # ============= @@ -313,11 +314,11 @@ class GuiProjectDetailsContents(QWidget): treeHeader.setStretchLastSection(True) treeHeader.setMinimumSectionSize(hPx) - wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol0", 200)) - wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol1", 60)) - wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol2", 60)) - wCol3 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol3", 60)) - wCol4 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol4", 90)) + wCol0 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol0", 200)) + wCol1 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol1", 60)) + wCol2 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol2", 60)) + wCol3 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol3", 60)) + wCol4 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol4", 90)) self.tocTree.setColumnWidth(0, wCol0) self.tocTree.setColumnWidth(1, wCol1) @@ -329,9 +330,9 @@ class GuiProjectDetailsContents(QWidget): # Options # ======= - wordsPerPage = self.optState.getInt("GuiProjectDetails", "wordsPerPage", 350) - countFrom = self.optState.getInt("GuiProjectDetails", "countFrom", 1) - clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True) + wordsPerPage = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350) + countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1) + clearDouble = pOptions.getInt("GuiProjectDetails", "clearDouble", True) wordsHelp = ( self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.") diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 927f4ddd..8bbdcee3 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -52,19 +52,19 @@ class GuiProjectSettings(PagedDialog): self.mainConf = novelwriter.CONFIG self.theParent = theParent self.theProject = theParent.theProject - self.optState = theParent.theProject.optState self.theProject.countStatus() self.setWindowTitle(self.tr("Project Settings")) wW = self.mainConf.pxInt(570) wH = self.mainConf.pxInt(375) + pOptions = self.theProject.options self.setMinimumWidth(wW) self.setMinimumHeight(wH) self.resize( - self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winWidth", wW)), - self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winHeight", wH)) + self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winWidth", wW)), + self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH)) ) self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) @@ -152,11 +152,12 @@ class GuiProjectSettings(PagedDialog): statusColW = self.mainConf.rpxInt(self.tabStatus.listBox.columnWidth(0)) importColW = self.mainConf.rpxInt(self.tabImport.listBox.columnWidth(0)) - self.optState.setValue("GuiProjectSettings", "winWidth", winWidth) - self.optState.setValue("GuiProjectSettings", "winHeight", winHeight) - self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW) - self.optState.setValue("GuiProjectSettings", "statusColW", statusColW) - self.optState.setValue("GuiProjectSettings", "importColW", importColW) + pOptions = self.theProject.options + pOptions.setValue("GuiProjectSettings", "winWidth", winWidth) + pOptions.setValue("GuiProjectSettings", "winHeight", winHeight) + pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW) + pOptions.setValue("GuiProjectSettings", "statusColW", statusColW) + pOptions.setValue("GuiProjectSettings", "importColW", importColW) return @@ -261,7 +262,6 @@ class GuiProjectEditStatus(QWidget): self.mainConf = novelwriter.CONFIG self.theParent = theParent self.theProject = theProject - self.optState = theProject.optState self.theTheme = theParent.theTheme if isStatus: @@ -274,7 +274,7 @@ class GuiProjectEditStatus(QWidget): colSetting = "importColW" wCol0 = self.mainConf.pxInt( - self.optState.getInt("GuiProjectSettings", colSetting, 130) + self.theProject.options.getInt("GuiProjectSettings", colSetting, 130) ) self.colDeleted = [] @@ -534,11 +534,10 @@ class GuiProjectEditReplace(QWidget): self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theProject - self.optState = theProject.optState self.arChanged = False wCol0 = self.mainConf.pxInt( - self.optState.getInt("GuiProjectSettings", "replaceColW", 130) + self.theProject.options.getInt("GuiProjectSettings", "replaceColW", 130) ) pageLabel = self.tr("Text Replace List for Preview and Export") diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 7dadf258..77a4fb5a 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -52,19 +52,19 @@ class GuiWordList(QDialog): self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theParent.theProject - self.optState = theParent.theProject.optState self.setWindowTitle(self.tr("Project Word List")) mS = self.mainConf.pxInt(250) wW = self.mainConf.pxInt(320) wH = self.mainConf.pxInt(340) + pOptions = self.theProject.options self.setMinimumWidth(mS) self.setMinimumHeight(mS) self.resize( - self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winWidth", wW)), - self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winHeight", wH)) + self.mainConf.pxInt(pOptions.getInt("GuiWordList", "winWidth", wW)), + self.mainConf.pxInt(pOptions.getInt("GuiWordList", "winHeight", wH)) ) # Main Widgets @@ -207,8 +207,9 @@ class GuiWordList(QDialog): winWidth = self.mainConf.rpxInt(self.width()) winHeight = self.mainConf.rpxInt(self.height()) - self.optState.setValue("GuiWordList", "winWidth", winWidth) - self.optState.setValue("GuiWordList", "winHeight", winHeight) + pOptions = self.theProject.options + pOptions.setValue("GuiWordList", "winWidth", winWidth) + pOptions.setValue("GuiWordList", "winHeight", winHeight) return diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 9cd99497..64e72259 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2701,15 +2701,15 @@ class GuiDocEditHeader(QWidget): if self.mainConf.showFullPath: tTitle = [] - tTree = self.theProject.projTree.getItemPath(tHandle) + tTree = self.theProject.tree.getItemPath(tHandle) for aHandle in reversed(tTree): - nwItem = self.theProject.projTree[aHandle] + nwItem = self.theProject.tree[aHandle] if nwItem is not None: tTitle.append(nwItem.itemName) sSep = " %s " % nwUnicode.U_RSAQUO self.theTitle.setText(sSep.join(tTitle)) else: - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] if nwItem is None: return False self.theTitle.setText(nwItem.itemName) @@ -2795,7 +2795,6 @@ class GuiDocEditFooter(QWidget): self.theParent = docEditor.theParent self.theProject = docEditor.theProject self.theTheme = docEditor.theTheme - self.optState = docEditor.theProject.optState self._theItem = None self._docHandle = None @@ -2918,7 +2917,7 @@ class GuiDocEditFooter(QWidget): logger.verbose("No handle set, so clearing the editor footer") self._theItem = None else: - self._theItem = self.theProject.projTree[self._docHandle] + self._theItem = self.theProject.tree[self._docHandle] self.setHasSelection(False) self.updateInfo() diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 0133f91c..bd2d78eb 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -288,7 +288,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): if theText.startswith("@"): # Keywords and commands self.setCurrentBlockState(self.BLOCK_META) pIndex = self.theProject.index - tItem = self.theParent.theProject.projTree[self.theHandle] + tItem = self.theParent.theProject.tree[self.theHandle] isValid, theBits, thePos = pIndex.scanThis(theText) isGood = pIndex.checkThese(theBits, tItem) if isValid: diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 615f121c..2587f125 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -160,7 +160,7 @@ class GuiDocViewer(QTextBrowser): def loadText(self, tHandle, updateHistory=True): """Load text into the viewer from an item handle. """ - if not self.theProject.projTree.checkType(tHandle, nwItemType.FILE): + if not self.theProject.tree.checkType(tHandle, nwItemType.FILE): logger.warning("Item not found") return False @@ -863,15 +863,15 @@ class GuiDocViewHeader(QWidget): if self.mainConf.showFullPath: tTitle = [] - tTree = self.theProject.projTree.getItemPath(tHandle) + tTree = self.theProject.tree.getItemPath(tHandle) for aHandle in reversed(tTree): - nwItem = self.theProject.projTree[aHandle] + nwItem = self.theProject.tree[aHandle] if nwItem is not None: tTitle.append(nwItem.itemName) sSep = " %s " % nwUnicode.U_RSAQUO self.theTitle.setText(sSep.join(tTitle)) else: - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] if nwItem is None: return False self.theTitle.setText(nwItem.itemName) @@ -1202,7 +1202,7 @@ class GuiDocViewDetails(QScrollArea): theRefs = self.theProject.index.getBackReferenceList(tHandle) theList = [] for tHandle in theRefs: - tItem = self.theProject.projTree[tHandle] + tItem = self.theProject.tree[tHandle] if tItem is not None: theList.append("%s" % ( tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 88419395..89a38b76 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -227,7 +227,7 @@ class GuiItemDetails(QWidget): self.clearDetails() return - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] if nwItem is None: self.clearDetails() return diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 2b7a654b..26cb8029 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -91,7 +91,6 @@ class GuiOutline(QTreeWidget): self.theParent = theParent self.theProject = theParent.theProject self.theTheme = theParent.theTheme - self.optState = theParent.theProject.optState self.headerMenu = GuiOutlineHeaderMenu(self) self.setFrameStyle(QFrame.NoFrame) @@ -273,10 +272,12 @@ class GuiOutline(QTreeWidget): """Load the state of the main tree header, that is, column order and column width. """ + pOptions = self.theProject.options + # Load whatever we saved last time, regardless of wether it # contains the correct names or number of columns. The names # must be valid though. - tempOrder = self.optState.getValue("GuiOutline", "headerOrder", []) + tempOrder = pOptions.getValue("GuiOutline", "headerOrder", []) treeOrder = [] for hName in tempOrder: try: @@ -299,14 +300,14 @@ class GuiOutline(QTreeWidget): # We load whatever column widths and hidden states we find in # the file, and leave the rest in their default state. - tmpWidth = self.optState.getValue("GuiOutline", "columnWidth", {}) + tmpWidth = pOptions.getValue("GuiOutline", "columnWidth", {}) for hName in tmpWidth: try: self._colWidth[nwOutline[hName]] = self.mainConf.pxInt(tmpWidth[hName]) except Exception: logger.warning("Ignored unknown outline column '%s'", str(hName)) - tmpHidden = self.optState.getValue("GuiOutline", "columnHidden", {}) + tmpHidden = pOptions.getValue("GuiOutline", "columnHidden", {}) for hName in tmpHidden: try: self._colHidden[nwOutline[hName]] = tmpHidden[hName] @@ -347,10 +348,11 @@ class GuiOutline(QTreeWidget): if not logHidden and logWidth > 0: colWidth[hName] = logWidth - self.optState.setValue("GuiOutline", "headerOrder", treeOrder) - self.optState.setValue("GuiOutline", "columnWidth", colWidth) - self.optState.setValue("GuiOutline", "columnHidden", colHidden) - self.optState.saveSettings() + pOptions = self.theProject.options + pOptions.setValue("GuiOutline", "headerOrder", treeOrder) + pOptions.setValue("GuiOutline", "columnWidth", colWidth) + pOptions.setValue("GuiOutline", "columnHidden", colHidden) + pOptions.saveSettings() return @@ -437,7 +439,7 @@ class GuiOutline(QTreeWidget): def _createTreeItem(self, tHandle, sTitle, novIdx): """Populate a tree item with all the column values. """ - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] newItem = QTreeWidgetItem() hIcon = "doc_%s" % novIdx["level"].lower() diff --git a/novelwriter/gui/outlinedetails.py b/novelwriter/gui/outlinedetails.py index 92d41c3c..40a3d29e 100644 --- a/novelwriter/gui/outlinedetails.py +++ b/novelwriter/gui/outlinedetails.py @@ -58,7 +58,6 @@ class GuiOutlineDetails(QScrollArea): self.theParent = theParent self.theProject = theParent.theProject self.theTheme = theParent.theTheme - self.optState = theParent.theProject.optState # Sizes minTitle = 30*self.theTheme.textNWidth @@ -283,7 +282,7 @@ class GuiOutlineDetails(QScrollArea): number pointing to a header. """ pIndex = self.theProject.index - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] novIdx = pIndex.getNovelData(tHandle, sTitle) theRefs = pIndex.getReferences(tHandle, sTitle) if nwItem is None or novIdx is None: diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index fd9e21ce..e35662f1 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -180,14 +180,14 @@ class GuiProjectTree(QTreeWidget): elif itemType in (nwItemType.FILE, nwItemType.FOLDER): sHandle = self.getSelectedHandle() - if sHandle is None or sHandle not in self.theProject.projTree: + if sHandle is None or sHandle not in self.theProject.tree: self.theParent.makeAlert(self.tr( "Did not find anywhere to add the file or folder!" ), nwAlert.ERROR) return False # If the selected item is a file, the new item will be a sibling - pItem = self.theProject.projTree[sHandle] + pItem = self.theProject.tree[sHandle] if pItem.itemType == nwItemType.FILE: nHandle = sHandle sHandle = pItem.itemParent @@ -195,7 +195,7 @@ class GuiProjectTree(QTreeWidget): logger.error("Internal error") # Bug return False - if self.theProject.projTree.isTrash(sHandle): + if self.theProject.tree.isTrash(sHandle): self.theParent.makeAlert(self.tr( "Cannot add new files or folders to the Trash folder." ), nwAlert.ERROR) @@ -221,7 +221,7 @@ class GuiProjectTree(QTreeWidget): # Add the new item to the tree self.revealNewTreeItem(tHandle, nHandle) self.theParent.editItem(tHandle) - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] # If this is a folder, return here if nwItem.itemType != nwItemType.FILE: @@ -254,7 +254,7 @@ class GuiProjectTree(QTreeWidget): def revealNewTreeItem(self, tHandle, nHandle=None): """Reveal a newly added project item in the project tree. """ - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] if nwItem is None: return False @@ -375,7 +375,7 @@ class GuiProjectTree(QTreeWidget): logger.error("No project open") return False - trashHandle = self.theProject.projTree.trashRoot() + trashHandle = self.theProject.tree.trashRoot() logger.debug("Emptying Trash folder") if trashHandle is None: @@ -436,7 +436,7 @@ class GuiProjectTree(QTreeWidget): return False trItemS = self._getTreeItem(tHandle) - nwItemS = self.theProject.projTree[tHandle] + nwItemS = self.theProject.tree[tHandle] if trItemS is None or nwItemS is None: logger.error("Could not find tree item for deletion") @@ -477,7 +477,7 @@ class GuiProjectTree(QTreeWidget): logger.error("Could not delete item") return False - if self.theProject.projTree.isTrash(tHandle): + if self.theProject.tree.isTrash(tHandle): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. doPermanent = False @@ -531,7 +531,7 @@ class GuiProjectTree(QTreeWidget): already coming from the project tree. """ trItem = self._getTreeItem(tHandle) - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] if trItem is None or nwItem is None: return @@ -596,7 +596,7 @@ class GuiProjectTree(QTreeWidget): pHandle = pItem.data(self.C_NAME, Qt.UserRole) if pHandle: - if self.theProject.projTree.checkType(pHandle, nwItemType.FILE): + if self.theProject.tree.checkType(pHandle, nwItemType.FILE): # A file has an internal word count we need to account # for, but a folder always has 0 words on its own. pCount += self.theProject.index.getCounts(pHandle)[1] @@ -711,7 +711,7 @@ class GuiProjectTree(QTreeWidget): if isinstance(selItem, QTreeWidgetItem): tHandle = selItem.data(self.C_NAME, Qt.UserRole) self.setSelectedHandle(tHandle) # Just to be safe - tItem = self.theProject.projTree[tHandle] + tItem = self.theProject.tree[tHandle] if tItem is not None: if self.ctxMenu.filterActions(tItem): # Only open menu if any actions remain after filter @@ -749,7 +749,7 @@ class GuiProjectTree(QTreeWidget): return tHandle = selItem.data(self.C_NAME, Qt.UserRole) - tItem = self.theProject.projTree[tHandle] + tItem = self.theProject.tree[tHandle] if tItem is None: return @@ -797,7 +797,7 @@ class GuiProjectTree(QTreeWidget): """Run various maintenance tasks for a moved item. """ trItemS = self._getTreeItem(tHandle) - nwItemS = self.theProject.projTree[tHandle] + nwItemS = self.theProject.tree[tHandle] trItemP = trItemS.parent() if trItemP is None: logger.error("Failed to find new parent item of '%s'", tHandle) @@ -814,7 +814,7 @@ class GuiProjectTree(QTreeWidget): logger.debug("A total of %d item(s) were moved", len(mHandles)) for mHandle in mHandles: logger.debug("Updating item '%s'", mHandle) - self.theProject.projTree.updateItemData(mHandle) + self.theProject.tree.updateItemData(mHandle) # Update the index if nwItemS.isInactive(): @@ -847,7 +847,7 @@ class GuiProjectTree(QTreeWidget): def _deleteTreeItem(self, tHandle): """Permanently delete a tree item from the project and the map. """ - if self.theProject.projTree.checkType(tHandle, nwItemType.FILE): + if self.theProject.tree.checkType(tHandle, nwItemType.FILE): delDoc = NWDoc(self.theProject, tHandle) if not delDoc.deleteDocument(): self.theParent.makeAlert([ @@ -856,7 +856,7 @@ class GuiProjectTree(QTreeWidget): return False self.theProject.index.deleteHandle(tHandle) - del self.theProject.projTree[tHandle] + del self.theProject.tree[tHandle] self._treeMap.pop(tHandle, None) return True @@ -869,7 +869,7 @@ class GuiProjectTree(QTreeWidget): cCount = tItem.childCount() # Update tree-related meta data - nwItem = self.theProject.projTree[tHandle] + nwItem = self.theProject.tree[tHandle] nwItem.setExpanded(tItem.isExpanded() and cCount > 0) nwItem.setOrder(tIndex) @@ -943,7 +943,7 @@ class GuiProjectTree(QTreeWidget): trItem = self._getTreeItem(trashHandle) if trItem is None: trItem = self._addTreeItem( - self.theProject.projTree[trashHandle] + self.theProject.tree[trashHandle] ) if trItem is not None: trItem.setExpanded(True) @@ -963,8 +963,8 @@ class GuiProjectTree(QTreeWidget): def _emitItemChange(self, tHandle): """Emit an item change signal for a given handle. """ - if self.theProject.projTree.checkType(tHandle, nwItemType.FILE): - nwItem = self.theProject.projTree[tHandle] + if self.theProject.tree.checkType(tHandle, nwItemType.FILE): + nwItem = self.theProject.tree[tHandle] if nwItem.isNovelLike(): self.novelItemChanged.emit() else: @@ -1047,9 +1047,9 @@ class GuiProjectTreeMenu(QMenu): logger.error("Failed to extract information to build tree context menu") return False - trashHandle = self.theTree.theProject.projTree.trashRoot() + trashHandle = self.theTree.theProject.tree.trashRoot() - inTrash = self.theTree.theProject.projTree.isTrash(theItem.itemHandle) + inTrash = self.theTree.theProject.tree.isTrash(theItem.itemHandle) isTrash = theItem.itemHandle == trashHandle and trashHandle is not None isFile = theItem.itemType == nwItemType.FILE diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 34f9abcd..3a957577 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -572,7 +572,7 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - if not self.theProject.projTree.checkType(tHandle, nwItemType.FILE): + if not self.theProject.tree.checkType(tHandle, nwItemType.FILE): logger.debug("Requested item '%s' is not a document", tHandle) return False @@ -600,8 +600,8 @@ class GuiMain(QMainWindow): nHandle = None # The next handle after tHandle fHandle = None # The first file handle we encounter foundIt = False # We've found tHandle, pick the next we see - for tItem in self.theProject.projTree: - if not self.theProject.projTree.checkType(tItem.itemHandle, nwItemType.FILE): + for tItem in self.theProject.tree: + if not self.theProject.tree.checkType(tItem.itemHandle, nwItemType.FILE): continue if fHandle is None: fHandle = tItem.itemHandle @@ -818,7 +818,7 @@ class GuiMain(QMainWindow): logger.warning("No item selected") return False - tItem = self.theProject.projTree[tHandle] + tItem = self.theProject.tree[tHandle] if tItem is None: return False if tItem.itemType == nwItemType.NO_TYPE: @@ -864,7 +864,7 @@ class GuiMain(QMainWindow): self.treeView.saveTreeOrder() self.theProject.index.clearIndex() - for tItem in self.theProject.projTree: + for tItem in self.theProject.tree: if tItem is not None: self.setStatus(self.tr("Indexing: '{0}'").format(tItem.itemName)) @@ -1560,7 +1560,7 @@ class GuiMain(QMainWindow): """ tHandle = self.treeView.getSelectedHandle() if tHandle is not None: - tItem = self.theProject.projTree[tHandle] + tItem = self.theProject.tree[tHandle] if tItem is None: return if tItem.itemType == nwItemType.FILE: diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index d84a0d4c..06849ee9 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -75,7 +75,6 @@ class GuiBuildNovel(QDialog): self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theParent.theProject - self.optState = theParent.theProject.optState self.htmlText = [] # List of html documents self.htmlStyle = [] # List of html styles @@ -86,9 +85,10 @@ class GuiBuildNovel(QDialog): self.setMinimumWidth(self.mainConf.pxInt(700)) self.setMinimumHeight(self.mainConf.pxInt(600)) + pOptions = self.theProject.options self.resize( - self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winWidth", 900)), - self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winHeight", 800)) + self.mainConf.pxInt(pOptions.getInt("GuiBuildNovel", "winWidth", 900)), + self.mainConf.pxInt(pOptions.getInt("GuiBuildNovel", "winHeight", 800)) ) self.docView = GuiBuildNovelDocView(self, self.theProject) @@ -174,12 +174,12 @@ class GuiBuildNovel(QDialog): self.hideScene = QSwitch(width=wS, height=hS) self.hideScene.setChecked( - self.optState.getBool("GuiBuildNovel", "hideScene", False) + pOptions.getBool("GuiBuildNovel", "hideScene", False) ) self.hideSection = QSwitch(width=wS, height=hS) self.hideSection.setChecked( - self.optState.getBool("GuiBuildNovel", "hideSection", True) + pOptions.getBool("GuiBuildNovel", "hideSection", True) ) # Wrapper boxes due to QGridView and QLineEdit expand bug @@ -235,7 +235,7 @@ class GuiBuildNovel(QDialog): self.textFont.setReadOnly(True) self.textFont.setMinimumWidth(xFmt) self.textFont.setText( - self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont) + pOptions.getString("GuiBuildNovel", "textFont", self.mainConf.textFont) ) self.fontButton = QPushButton("...") self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) @@ -247,7 +247,7 @@ class GuiBuildNovel(QDialog): self.textSize.setMaximum(72) self.textSize.setSingleStep(1) self.textSize.setValue( - self.optState.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize) + pOptions.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize) ) self.lineHeight = QDoubleSpinBox(self) @@ -257,7 +257,7 @@ class GuiBuildNovel(QDialog): self.lineHeight.setSingleStep(0.05) self.lineHeight.setDecimals(2) self.lineHeight.setValue( - self.optState.getFloat("GuiBuildNovel", "lineHeight", 1.15) + pOptions.getFloat("GuiBuildNovel", "lineHeight", 1.15) ) # Wrapper box due to QGridView and QLineEdit expand bug @@ -291,12 +291,12 @@ class GuiBuildNovel(QDialog): self.justifyText = QSwitch(width=wS, height=hS) self.justifyText.setChecked( - self.optState.getBool("GuiBuildNovel", "justifyText", False) + pOptions.getBool("GuiBuildNovel", "justifyText", False) ) self.noStyling = QSwitch(width=wS, height=hS) self.noStyling.setChecked( - self.optState.getBool("GuiBuildNovel", "noStyling", False) + pOptions.getBool("GuiBuildNovel", "noStyling", False) ) self.styleForm.addWidget(justifyLabel, 1, 0, 1, 1, Qt.AlignLeft) @@ -316,22 +316,22 @@ class GuiBuildNovel(QDialog): self.includeSynopsis = QSwitch(width=wS, height=hS) self.includeSynopsis.setChecked( - self.optState.getBool("GuiBuildNovel", "incSynopsis", False) + pOptions.getBool("GuiBuildNovel", "incSynopsis", False) ) self.includeComments = QSwitch(width=wS, height=hS) self.includeComments.setChecked( - self.optState.getBool("GuiBuildNovel", "incComments", False) + pOptions.getBool("GuiBuildNovel", "incComments", False) ) self.includeKeywords = QSwitch(width=wS, height=hS) self.includeKeywords.setChecked( - self.optState.getBool("GuiBuildNovel", "incKeywords", False) + pOptions.getBool("GuiBuildNovel", "incKeywords", False) ) self.includeBody = QSwitch(width=wS, height=hS) self.includeBody.setChecked( - self.optState.getBool("GuiBuildNovel", "incBodyText", True) + pOptions.getBool("GuiBuildNovel", "incBodyText", True) ) synopsisLabel = QLabel(self.tr("Include synopsis")) @@ -360,17 +360,17 @@ class GuiBuildNovel(QDialog): self.novelFiles = QSwitch(width=wS, height=hS) self.novelFiles.setChecked( - self.optState.getBool("GuiBuildNovel", "addNovel", True) + pOptions.getBool("GuiBuildNovel", "addNovel", True) ) self.noteFiles = QSwitch(width=wS, height=hS) self.noteFiles.setChecked( - self.optState.getBool("GuiBuildNovel", "addNotes", False) + pOptions.getBool("GuiBuildNovel", "addNotes", False) ) self.ignoreFlag = QSwitch(width=wS, height=hS) self.ignoreFlag.setChecked( - self.optState.getBool("GuiBuildNovel", "ignoreFlag", False) + pOptions.getBool("GuiBuildNovel", "ignoreFlag", False) ) novelLabel = QLabel(self.tr("Include novel files")) @@ -396,12 +396,12 @@ class GuiBuildNovel(QDialog): self.replaceTabs = QSwitch(width=wS, height=hS) self.replaceTabs.setChecked( - self.optState.getBool("GuiBuildNovel", "replaceTabs", False) + pOptions.getBool("GuiBuildNovel", "replaceTabs", False) ) self.replaceUCode = QSwitch(width=wS, height=hS) self.replaceUCode.setChecked( - self.optState.getBool("GuiBuildNovel", "replaceUCode", False) + pOptions.getBool("GuiBuildNovel", "replaceUCode", False) ) tabsLabel = QLabel(self.tr("Replace tabs with spaces")) @@ -493,9 +493,9 @@ class GuiBuildNovel(QDialog): # Splitter Position boxWidth = self.mainConf.pxInt(350) - boxWidth = self.optState.getInt("GuiBuildNovel", "boxWidth", boxWidth) + boxWidth = pOptions.getInt("GuiBuildNovel", "boxWidth", boxWidth) docWidth = max(self.width() - boxWidth, 100) - docWidth = self.optState.getInt("GuiBuildNovel", "docWidth", docWidth) + docWidth = pOptions.getInt("GuiBuildNovel", "docWidth", docWidth) # The Tool Box self.toolsBox = QVBoxLayout() @@ -712,10 +712,10 @@ class GuiBuildNovel(QDialog): self.theParent.treeView.flushTreeOrder() self.theParent.saveDocument() - self.buildProgress.setMaximum(len(self.theProject.projTree)) + self.buildProgress.setMaximum(len(self.theProject.tree)) self.buildProgress.setValue(0) - for nItt, tItem in enumerate(self.theProject.projTree): + for nItt, tItem in enumerate(self.theProject.tree): noteRoot = noteFiles noteRoot &= tItem.itemType == nwItemType.ROOT @@ -1153,28 +1153,28 @@ class GuiBuildNovel(QDialog): self.theProject.setProjectLang(buildLang) # GUI Settings - self.optState.setValue("GuiBuildNovel", "hideScene", hideScene) - self.optState.setValue("GuiBuildNovel", "hideSection", hideSection) - self.optState.setValue("GuiBuildNovel", "winWidth", winWidth) - self.optState.setValue("GuiBuildNovel", "winHeight", winHeight) - self.optState.setValue("GuiBuildNovel", "boxWidth", boxWidth) - self.optState.setValue("GuiBuildNovel", "docWidth", docWidth) - self.optState.setValue("GuiBuildNovel", "justifyText", justifyText) - self.optState.setValue("GuiBuildNovel", "noStyling", noStyling) - self.optState.setValue("GuiBuildNovel", "textFont", textFont) - self.optState.setValue("GuiBuildNovel", "textSize", textSize) - self.optState.setValue("GuiBuildNovel", "lineHeight", lineHeight) - self.optState.setValue("GuiBuildNovel", "addNovel", novelFiles) - self.optState.setValue("GuiBuildNovel", "addNotes", noteFiles) - self.optState.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag) - self.optState.setValue("GuiBuildNovel", "incSynopsis", incSynopsis) - self.optState.setValue("GuiBuildNovel", "incComments", incComments) - self.optState.setValue("GuiBuildNovel", "incKeywords", incKeywords) - self.optState.setValue("GuiBuildNovel", "incBodyText", incBodyText) - self.optState.setValue("GuiBuildNovel", "replaceTabs", replaceTabs) - self.optState.setValue("GuiBuildNovel", "replaceUCode", replaceUCode) - - self.optState.saveSettings() + pOptions = self.theProject.options + pOptions.setValue("GuiBuildNovel", "hideScene", hideScene) + pOptions.setValue("GuiBuildNovel", "hideSection", hideSection) + pOptions.setValue("GuiBuildNovel", "winWidth", winWidth) + pOptions.setValue("GuiBuildNovel", "winHeight", winHeight) + pOptions.setValue("GuiBuildNovel", "boxWidth", boxWidth) + pOptions.setValue("GuiBuildNovel", "docWidth", docWidth) + pOptions.setValue("GuiBuildNovel", "justifyText", justifyText) + pOptions.setValue("GuiBuildNovel", "noStyling", noStyling) + pOptions.setValue("GuiBuildNovel", "textFont", textFont) + pOptions.setValue("GuiBuildNovel", "textSize", textSize) + pOptions.setValue("GuiBuildNovel", "lineHeight", lineHeight) + pOptions.setValue("GuiBuildNovel", "addNovel", novelFiles) + pOptions.setValue("GuiBuildNovel", "addNotes", noteFiles) + pOptions.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag) + pOptions.setValue("GuiBuildNovel", "incSynopsis", incSynopsis) + pOptions.setValue("GuiBuildNovel", "incComments", incComments) + pOptions.setValue("GuiBuildNovel", "incKeywords", incKeywords) + pOptions.setValue("GuiBuildNovel", "incBodyText", incBodyText) + pOptions.setValue("GuiBuildNovel", "replaceTabs", replaceTabs) + pOptions.setValue("GuiBuildNovel", "replaceUCode", replaceUCode) + pOptions.saveSettings() return diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index 4ce83b66..aff7bc3d 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -67,33 +67,34 @@ class GuiWritingStats(QDialog): self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theParent.theProject - self.optState = theParent.theProject.optState self.logData = [] self.filterData = [] self.timeFilter = 0.0 self.wordOffset = 0 + pOptions = self.theProject.options + self.setWindowTitle(self.tr("Writing Statistics")) self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumHeight(self.mainConf.pxInt(400)) self.resize( - self.mainConf.pxInt(self.optState.getInt("GuiWritingStats", "winWidth", 550)), - self.mainConf.pxInt(self.optState.getInt("GuiWritingStats", "winHeight", 500)) + self.mainConf.pxInt(pOptions.getInt("GuiWritingStats", "winWidth", 550)), + self.mainConf.pxInt(pOptions.getInt("GuiWritingStats", "winHeight", 500)) ) # List Box wCol0 = self.mainConf.pxInt( - self.optState.getInt("GuiWritingStats", "widthCol0", 180) + pOptions.getInt("GuiWritingStats", "widthCol0", 180) ) wCol1 = self.mainConf.pxInt( - self.optState.getInt("GuiWritingStats", "widthCol1", 80) + pOptions.getInt("GuiWritingStats", "widthCol1", 80) ) wCol2 = self.mainConf.pxInt( - self.optState.getInt("GuiWritingStats", "widthCol2", 80) + pOptions.getInt("GuiWritingStats", "widthCol2", 80) ) wCol3 = self.mainConf.pxInt( - self.optState.getInt("GuiWritingStats", "widthCol3", 80) + pOptions.getInt("GuiWritingStats", "widthCol3", 80) ) self.listBox = QTreeWidget() @@ -115,9 +116,9 @@ class GuiWritingStats(QDialog): hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight) hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight) - sortCol = checkIntRange(self.optState.getInt("GuiWritingStats", "sortCol", 0), 0, 2, 0) + sortCol = checkIntRange(pOptions.getInt("GuiWritingStats", "sortCol", 0), 0, 2, 0) sortOrder = checkIntTuple( - self.optState.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder), + pOptions.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder), (Qt.AscendingOrder, Qt.DescendingOrder), Qt.DescendingOrder ) self.listBox.sortByColumn(sortCol, sortOrder) @@ -190,37 +191,37 @@ class GuiWritingStats(QDialog): self.incNovel = QSwitch(width=2*sPx, height=sPx) self.incNovel.setChecked( - self.optState.getBool("GuiWritingStats", "incNovel", True) + pOptions.getBool("GuiWritingStats", "incNovel", True) ) self.incNovel.clicked.connect(self._updateListBox) self.incNotes = QSwitch(width=2*sPx, height=sPx) self.incNotes.setChecked( - self.optState.getBool("GuiWritingStats", "incNotes", True) + pOptions.getBool("GuiWritingStats", "incNotes", True) ) self.incNotes.clicked.connect(self._updateListBox) self.hideZeros = QSwitch(width=2*sPx, height=sPx) self.hideZeros.setChecked( - self.optState.getBool("GuiWritingStats", "hideZeros", True) + pOptions.getBool("GuiWritingStats", "hideZeros", True) ) self.hideZeros.clicked.connect(self._updateListBox) self.hideNegative = QSwitch(width=2*sPx, height=sPx) self.hideNegative.setChecked( - self.optState.getBool("GuiWritingStats", "hideNegative", False) + pOptions.getBool("GuiWritingStats", "hideNegative", False) ) self.hideNegative.clicked.connect(self._updateListBox) self.groupByDay = QSwitch(width=2*sPx, height=sPx) self.groupByDay.setChecked( - self.optState.getBool("GuiWritingStats", "groupByDay", False) + pOptions.getBool("GuiWritingStats", "groupByDay", False) ) self.groupByDay.clicked.connect(self._updateListBox) self.showIdleTime = QSwitch(width=2*sPx, height=sPx) self.showIdleTime.setChecked( - self.optState.getBool("GuiWritingStats", "showIdleTime", False) + pOptions.getBool("GuiWritingStats", "showIdleTime", False) ) self.showIdleTime.clicked.connect(self._updateListBox) @@ -244,7 +245,7 @@ class GuiWritingStats(QDialog): self.histMax.setMaximum(100000) self.histMax.setSingleStep(100) self.histMax.setValue( - self.optState.getInt("GuiWritingStats", "histMax", 2000) + pOptions.getInt("GuiWritingStats", "histMax", 2000) ) self.histMax.valueChanged.connect(self._updateListBox) @@ -323,23 +324,23 @@ class GuiWritingStats(QDialog): showIdleTime = self.showIdleTime.isChecked() histMax = self.histMax.value() - self.optState.setValue("GuiWritingStats", "winWidth", winWidth) - self.optState.setValue("GuiWritingStats", "winHeight", winHeight) - self.optState.setValue("GuiWritingStats", "widthCol0", widthCol0) - self.optState.setValue("GuiWritingStats", "widthCol1", widthCol1) - self.optState.setValue("GuiWritingStats", "widthCol2", widthCol2) - self.optState.setValue("GuiWritingStats", "widthCol3", widthCol3) - self.optState.setValue("GuiWritingStats", "sortCol", sortCol) - self.optState.setValue("GuiWritingStats", "sortOrder", sortOrder) - self.optState.setValue("GuiWritingStats", "incNovel", incNovel) - self.optState.setValue("GuiWritingStats", "incNotes", incNotes) - self.optState.setValue("GuiWritingStats", "hideZeros", hideZeros) - self.optState.setValue("GuiWritingStats", "hideNegative", hideNegative) - self.optState.setValue("GuiWritingStats", "groupByDay", groupByDay) - self.optState.setValue("GuiWritingStats", "showIdleTime", showIdleTime) - self.optState.setValue("GuiWritingStats", "histMax", histMax) - - self.optState.saveSettings() + pOptions = self.theProject.options + pOptions.setValue("GuiWritingStats", "winWidth", winWidth) + pOptions.setValue("GuiWritingStats", "winHeight", winHeight) + pOptions.setValue("GuiWritingStats", "widthCol0", widthCol0) + pOptions.setValue("GuiWritingStats", "widthCol1", widthCol1) + pOptions.setValue("GuiWritingStats", "widthCol2", widthCol2) + pOptions.setValue("GuiWritingStats", "widthCol3", widthCol3) + pOptions.setValue("GuiWritingStats", "sortCol", sortCol) + pOptions.setValue("GuiWritingStats", "sortOrder", sortOrder) + pOptions.setValue("GuiWritingStats", "incNovel", incNovel) + pOptions.setValue("GuiWritingStats", "incNotes", incNotes) + pOptions.setValue("GuiWritingStats", "hideZeros", hideZeros) + pOptions.setValue("GuiWritingStats", "hideNegative", hideNegative) + pOptions.setValue("GuiWritingStats", "groupByDay", groupByDay) + pOptions.setValue("GuiWritingStats", "showIdleTime", showIdleTime) + pOptions.setValue("GuiWritingStats", "histMax", histMax) + pOptions.saveSettings() self.close() return diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py index 881290d6..2f80e45a 100644 --- a/tests/test_core/test_core_document.py +++ b/tests/test_core/test_core_document.py @@ -64,7 +64,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal): assert theDoc.readDocument() == "### New Scene\n\n" # Try to open a new (non-existent) file - nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL) + nHandle = theProject.tree.findRoot(nwItemClass.NOVEL) assert nHandle is not None xHandle = theProject.newFile("New File", nHandle) theDoc = NWDoc(theProject, xHandle) diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 93ec35c6..0d070eba 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -54,7 +54,7 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): "6c6afb1247750": False, # Plot ROOT "60bdf227455cc": False, # World ROOT } - for tItem in theProject.projTree: + for tItem in theProject.tree: assert theIndex.reIndexHandle(tItem.itemHandle) is notIndexable.get(tItem.itemHandle, True) assert theIndex.reIndexHandle(None) is False @@ -180,8 +180,8 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI): theIndex = NWIndex(theProject) nHandle = theProject.newFile("Hello", "a508bb932959c") cHandle = theProject.newFile("Jane", "afb3043c7b2b3") - nItem = theProject.projTree[nHandle] - cItem = theProject.projTree[cHandle] + nItem = theProject.tree[nHandle] + cItem = theProject.tree[cHandle] assert theIndex.novelChangedSince(0) is False assert theIndex.notesChangedSince(0) is False @@ -258,7 +258,7 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): # Some items for fail to scan tests dHandle = theProject.newFolder("Folder", "a508bb932959c") xHandle = theProject.newFile("No Layout", "a508bb932959c") - xItem = theProject.projTree[xHandle] + xItem = theProject.tree[xHandle] xItem.setLayout(nwItemLayout.NO_LAYOUT) # Check invalid data @@ -272,18 +272,18 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): # Create the trash folder tHandle = theProject.trashFolder() - assert theProject.projTree[tHandle] is not None + assert theProject.tree[tHandle] is not None xItem.setParent(tHandle) - theProject.projTree.updateItemData(xItem.itemHandle) + theProject.tree.updateItemData(xItem.itemHandle) assert xItem.itemRoot == tHandle assert xItem.itemClass == nwItemClass.TRASH assert theIndex.scanText(xHandle, "Hello World!") is False # Create the archive root aHandle = theProject.newRoot(nwItemClass.ARCHIVE) - assert theProject.projTree[aHandle] is not None + assert theProject.tree[aHandle] is not None xItem.setParent(aHandle) - theProject.projTree.updateItemData(xItem.itemHandle) + theProject.tree.updateItemData(xItem.itemHandle) assert theIndex.scanText(xHandle, "Hello World!") is False # Make some usable items @@ -433,7 +433,7 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): # Page wo/Title # ============= - theProject.projTree[pHandle]._layout = nwItemLayout.DOCUMENT + theProject.tree[pHandle]._layout = nwItemLayout.DOCUMENT assert theIndex.scanText(pHandle, ( "This is a page with some text on it.\n\n" )) @@ -446,7 +446,7 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theIndex._fileIndex[pHandle]["T000000"]["pCount"] == 1 assert theIndex._fileIndex[pHandle]["T000000"]["synopsis"] == "" - theProject.projTree[pHandle]._layout = nwItemLayout.NOTE + theProject.tree[pHandle]._layout = nwItemLayout.NOTE assert theIndex.scanText(pHandle, ( "This is a page with some text on it.\n\n" )) @@ -499,7 +499,7 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): assert theKeys == ["%s:T000001" % nHandle] # Check that excluded files can be skipped - theProject.projTree[nHandle].setExported(False) + theProject.tree[nHandle].setExported(False) theKeys = [] for aKey, _, _, _ in theIndex.novelStructure(skipExcluded=False): @@ -631,9 +631,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): sHandle = theProject.newFile("Scene One", "a508bb932959c") tHandle = theProject.newFile("Scene Two", "a508bb932959c") - theProject.projTree[hHandle].itemLayout == nwItemLayout.DOCUMENT - theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT - theProject.projTree[tHandle].itemLayout == nwItemLayout.DOCUMENT + theProject.tree[hHandle].itemLayout == nwItemLayout.DOCUMENT + theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT + theProject.tree[tHandle].itemLayout == nwItemLayout.DOCUMENT assert theIndex.scanText(hHandle, "## Chapter One\n\n") assert theIndex.scanText(sHandle, "### Scene One\n\n") @@ -643,9 +643,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): assert theIndex._listNovelHandles(True) == [hHandle, sHandle, tHandle] # Add a fake handle to the tree and check that it's ignored - theProject.projTree._treeOrder.append("0000000000000") + theProject.tree._treeOrder.append("0000000000000") assert theIndex._listNovelHandles(False) == [nHandle, hHandle, sHandle, tHandle] - theProject.projTree._treeOrder.remove("0000000000000") + theProject.tree._treeOrder.remove("0000000000000") # Extract stats assert theIndex.getNovelWordCount(False) == 34 diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index e6e5092d..005277c6 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -634,17 +634,17 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): "afb3043c7b2b3", # ROOT: Characters "9d5247ab588e0", # ROOT: World ] - assert theProject.projTree.handles() == oldOrder + assert theProject.tree.handles() == oldOrder assert theProject.setTreeOrder(newOrder) - assert theProject.projTree.handles() == newOrder + assert theProject.tree.handles() == newOrder # Add a non-existing item - theProject.projTree._treeOrder.append("01234567789abc") + theProject.tree._treeOrder.append("01234567789abc") # Add an item with a non-existent parent nHandle = theProject.newFile("Test File", "a6d311a93600a") - theProject.projTree[nHandle].setParent("cba9876543210") - assert theProject.projTree[nHandle].itemParent == "cba9876543210" + theProject.tree[nHandle].setParent("cba9876543210") + assert theProject.tree[nHandle].itemParent == "cba9876543210" retOrder = [] for tItem in theProject.getProjectItems(): @@ -661,7 +661,7 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): "f5ab3e30151e1", # FILE: New Chapter "8c659a11cd429", # FILE: New Scene ] - assert theProject.projTree[nHandle].itemParent is None + assert theProject.tree[nHandle].itemParent is None # END Test testCoreProject_AccessItems @@ -679,15 +679,15 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): # Change Status # ============= - theProject.projTree["0000000000014"].setStatus("Finished") - theProject.projTree["0000000000015"].setStatus("Draft") - theProject.projTree["0000000000016"].setStatus("Note") - theProject.projTree["0000000000017"].setStatus("Finished") + theProject.tree["0000000000014"].setStatus("Finished") + theProject.tree["0000000000015"].setStatus("Draft") + theProject.tree["0000000000016"].setStatus("Note") + theProject.tree["0000000000017"].setStatus("Finished") - assert theProject.projTree["0000000000014"].itemStatus == statusKeys[3] - assert theProject.projTree["0000000000015"].itemStatus == statusKeys[2] - assert theProject.projTree["0000000000016"].itemStatus == statusKeys[1] - assert theProject.projTree["0000000000017"].itemStatus == statusKeys[3] + assert theProject.tree["0000000000014"].itemStatus == statusKeys[3] + assert theProject.tree["0000000000015"].itemStatus == statusKeys[2] + assert theProject.tree["0000000000016"].itemStatus == statusKeys[1] + assert theProject.tree["0000000000017"].itemStatus == statusKeys[3] newList = [ {"key": statusKeys[0], "name": "New", "cols": (1, 1, 1)}, @@ -723,9 +723,9 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): # ================= fHandle = theProject.newFile("Jane Doe", "8b9d2e465e150") - theProject.projTree[fHandle].setImport("Main") + theProject.tree[fHandle].setImport("Main") - assert theProject.projTree[fHandle].itemImport == importKeys[3] + assert theProject.tree[fHandle].itemImport == importKeys[3] newList = [ {"key": importKeys[0], "name": "New", "cols": (1, 1, 1)}, {"key": importKeys[1], "name": "Minor", "cols": (2, 2, 2)}, @@ -851,7 +851,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): # Trash folder # Should create on first call, and just returned on later calls hTrash = "0000000000018" - assert theProject.projTree[hTrash] is None + assert theProject.tree[hTrash] is None assert theProject.trashFolder() == hTrash assert theProject.trashFolder() == hTrash @@ -929,11 +929,11 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): "0000000000010", "0000000000011", "0000000000012", "0000000000016", "0000000000017", ] - assert theProject.projTree.handles() == oldOrder + assert theProject.tree.handles() == oldOrder assert theProject.setTreeOrder(newOrder) - assert theProject.projTree.handles() == newOrder + assert theProject.tree.handles() == newOrder assert theProject.setTreeOrder(oldOrder) - assert theProject.projTree.handles() == oldOrder + assert theProject.tree.handles() == oldOrder # Session stats theProject.currWCount = 200 @@ -1003,7 +1003,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): theProject = NWProject(mockGUI) assert theProject.openProject(nwLipsum) is True - assert theProject.projTree["636b6aa9b697b"] is None + assert theProject.tree["636b6aa9b697b"] is None # Add a file with non-existent parent # This file will be renoved from the project on open @@ -1041,11 +1041,11 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): assert theProject.openProject(nwLipsum) assert theProject.projPath is not None - assert theProject.projTree["636b6aa9b697bb"] is None - assert theProject.projTree["abcdefghijklm"] is None + assert theProject.tree["636b6aa9b697bb"] is None + assert theProject.tree["abcdefghijklm"] is None # First Item with Meta Data - oItem = theProject.projTree["636b6aa9b697b"] + oItem = theProject.tree["636b6aa9b697b"] assert oItem is not None assert oItem.itemName == "[Recovered] Mars" assert oItem.itemHandle == "636b6aa9b697b" @@ -1055,7 +1055,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): assert oItem.itemLayout == nwItemLayout.NOTE # Second Item without Meta Data - oItem = theProject.projTree["736b6aa9b697b"] + oItem = theProject.tree["736b6aa9b697b"] assert oItem is not None assert oItem.itemName == "Recovered File 1" assert oItem.itemHandle == "736b6aa9b697b" diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index c221138e..95b1ee71 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -65,9 +65,9 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.editItem() is False # Invalid Type - nwGUI.theProject.projTree[tHandle]._type = nwItemType.NO_TYPE + nwGUI.theProject.tree[tHandle]._type = nwItemType.NO_TYPE assert nwGUI.editItem() is False - nwGUI.theProject.projTree[tHandle]._type = nwItemType.FILE + nwGUI.theProject.tree[tHandle]._type = nwItemType.FILE # Open Properly assert nwGUI.editItem() is True diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 7f3aa30c..1727b69f 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -185,10 +185,10 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumTe assert "Could not save document." in caplog.text # Change header level - assert nwGUI.theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT + assert nwGUI.theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT nwGUI.docEditor.replaceText(longText[1:]) assert nwGUI.docEditor.saveText() is True - assert nwGUI.theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT + assert nwGUI.theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT # Regular save assert nwGUI.docEditor.saveText() is True @@ -236,9 +236,9 @@ def testGuiEditor_MetaData(qtbot, monkeypatch, nwGUI, nwMinimal): assert nwGUI.docEditor.setCursorPosition(None) is False assert nwGUI.docEditor.setCursorPosition(10) is True assert nwGUI.docEditor.getCursorPosition() == 10 - assert nwGUI.theProject.projTree[sHandle].cursorPos != 10 + assert nwGUI.theProject.tree[sHandle].cursorPos != 10 nwGUI.docEditor.saveCursorPosition() - assert nwGUI.theProject.projTree[sHandle].cursorPos == 10 + assert nwGUI.theProject.tree[sHandle].cursorPos == 10 assert nwGUI.docEditor.setCursorLine(None) is False assert nwGUI.docEditor.setCursorLine(2) is True @@ -1226,8 +1226,8 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ips # Open a document and populate it sHandle = "8c659a11cd429" - nwGUI.theProject.projTree[sHandle]._initCount = 0 # Clear item's count - nwGUI.theProject.projTree[sHandle]._wordCount = 0 # Clear item's count + nwGUI.theProject.tree[sHandle]._initCount = 0 # Clear item's count + nwGUI.theProject.tree[sHandle]._wordCount = 0 # Clear item's count assert nwGUI.openDocument(sHandle) is True qtbot.wait(stepDelay) @@ -1253,9 +1253,9 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ips nwGUI.docEditor.wCounterDoc.run() # nwGUI.docEditor._updateDocCounts(cC, wC, pC) qtbot.wait(stepDelay) - assert nwGUI.theProject.projTree[sHandle]._charCount == cC - assert nwGUI.theProject.projTree[sHandle]._wordCount == wC - assert nwGUI.theProject.projTree[sHandle]._paraCount == pC + assert nwGUI.theProject.tree[sHandle]._charCount == cC + assert nwGUI.theProject.tree[sHandle]._wordCount == wC + assert nwGUI.theProject.tree[sHandle]._paraCount == pC assert nwGUI.docEditor.docFooter.wordsText.text() == f"Words: {wC} (+{wC})" # Select all text diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 48f61eff..d3f245d1 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -140,7 +140,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.docViewer.reloadText() # Change document title - nwItem = nwGUI.theProject.projTree["4c4f28287af27"] + nwItem = nwGUI.theProject.tree["4c4f28287af27"] nwItem.setName("Test Title") assert nwItem.itemName == "Test Title" nwGUI.docViewer.updateDocInfo("4c4f28287af27") diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index e0d7e2ec..5e00815d 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -181,10 +181,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.saveProject() assert nwGUI.closeProject() - assert len(nwGUI.theProject.projTree) == 0 - assert len(nwGUI.theProject.projTree._treeOrder) == 0 - assert len(nwGUI.theProject.projTree._treeRoots) == 0 - assert nwGUI.theProject.projTree.trashRoot() is None + assert len(nwGUI.theProject.tree) == 0 + assert len(nwGUI.theProject.tree._treeOrder) == 0 + assert len(nwGUI.theProject.tree._treeRoots) == 0 + assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.projPath is None assert nwGUI.theProject.projMeta is None assert nwGUI.theProject.projFile == "nwProject.nwx" @@ -208,10 +208,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock qtbot.wait(stepDelay) # Check that we loaded the data - assert len(nwGUI.theProject.projTree) == 8 - assert len(nwGUI.theProject.projTree._treeOrder) == 8 - assert len(nwGUI.theProject.projTree._treeRoots) == 4 - assert nwGUI.theProject.projTree.trashRoot() is None + assert len(nwGUI.theProject.tree) == 8 + assert len(nwGUI.theProject.tree._treeOrder) == 8 + assert len(nwGUI.theProject.tree._treeRoots) == 4 + assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.projPath == fncProj assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta") assert nwGUI.theProject.projFile == "nwProject.nwx" @@ -464,11 +464,11 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Check a Quick Create and Delete assert nwGUI.treeView.newTreeItem(nwItemType.FILE, None) newHandle = nwGUI.treeView.getSelectedHandle() - assert nwGUI.theProject.projTree["0000000000020"] is not None + assert nwGUI.theProject.tree["0000000000020"] is not None assert nwGUI.treeView.deleteItem() assert nwGUI.treeView.setSelectedHandle(newHandle) assert nwGUI.treeView.deleteItem() - assert nwGUI.theProject.projTree["0000000000024"] is not None # Trash + assert nwGUI.theProject.tree["0000000000024"] is not None # Trash assert nwGUI.saveProject() # Check the files diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 5d3354f9..39e0b4dc 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -63,7 +63,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # Create root item assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True - assert "0000000000010" in nwGUI.theProject.projTree + assert "0000000000010" in nwGUI.theProject.tree # File/Folder Items # ================= @@ -78,42 +78,42 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # Create new folder as child of Novel folder nwTree.setSelectedHandle("0000000000008") assert nwTree.newTreeItem(nwItemType.FOLDER) is True - assert nwGUI.theProject.projTree["0000000000011"].itemParent == "0000000000008" - assert nwGUI.theProject.projTree["0000000000011"].itemRoot == "0000000000008" - assert nwGUI.theProject.projTree["0000000000011"].itemClass == nwItemClass.NOVEL + assert nwGUI.theProject.tree["0000000000011"].itemParent == "0000000000008" + assert nwGUI.theProject.tree["0000000000011"].itemRoot == "0000000000008" + assert nwGUI.theProject.tree["0000000000011"].itemClass == nwItemClass.NOVEL # Add a new file in the new folder nwTree.setSelectedHandle("0000000000011") assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwGUI.theProject.projTree["0000000000012"].itemParent == "0000000000011" - assert nwGUI.theProject.projTree["0000000000012"].itemRoot == "0000000000008" - assert nwGUI.theProject.projTree["0000000000012"].itemClass == nwItemClass.NOVEL + assert nwGUI.theProject.tree["0000000000012"].itemParent == "0000000000011" + assert nwGUI.theProject.tree["0000000000012"].itemRoot == "0000000000008" + assert nwGUI.theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL # Add a new file next to the other new file nwTree.setSelectedHandle("0000000000012") assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwGUI.theProject.projTree["0000000000013"].itemParent == "0000000000011" - assert nwGUI.theProject.projTree["0000000000013"].itemRoot == "0000000000008" - assert nwGUI.theProject.projTree["0000000000013"].itemClass == nwItemClass.NOVEL + assert nwGUI.theProject.tree["0000000000013"].itemParent == "0000000000011" + assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008" + assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL assert nwGUI.openDocument("0000000000013") assert nwGUI.docEditor.getText() == "### New Document\n\n" # Add a new file to the characters folder nwTree.setSelectedHandle("000000000000a") assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwGUI.theProject.projTree["0000000000014"].itemParent == "000000000000a" - assert nwGUI.theProject.projTree["0000000000014"].itemRoot == "000000000000a" - assert nwGUI.theProject.projTree["0000000000014"].itemClass == nwItemClass.CHARACTER + assert nwGUI.theProject.tree["0000000000014"].itemParent == "000000000000a" + assert nwGUI.theProject.tree["0000000000014"].itemRoot == "000000000000a" + assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.CHARACTER assert nwGUI.openDocument("0000000000014") assert nwGUI.docEditor.getText() == "# New Note\n\n" # Make sure the sibling folder bug trap works nwTree.setSelectedHandle("0000000000013") - nwGUI.theProject.projTree["0000000000013"].setParent(None) # This should not happen + nwGUI.theProject.tree["0000000000013"].setParent(None) # This should not happen caplog.clear() assert nwTree.newTreeItem(nwItemType.FILE) is False assert "Internal error" in caplog.text - nwGUI.theProject.projTree["0000000000013"].setParent("0000000000011") + nwGUI.theProject.tree["0000000000013"].setParent("0000000000011") # Get the trash folder nwTree._addTrashRoot() @@ -242,22 +242,22 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # =========== nwTree.setSelectedHandle("0000000000008") - assert nwGUI.theProject.projTree._treeOrder.index("0000000000008") == 0 + assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder up assert nwTree.moveTreeItem(-1) is False nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("0000000000008") == 0 + assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder down assert nwTree.moveTreeItem(1) is True nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("0000000000008") == 1 + assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1 # Move novel folder up again assert nwTree.moveTreeItem(-1) is True nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("0000000000008") == 0 + assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Clean up # qtbot.stopForInteraction() @@ -341,7 +341,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR "000000000000d", "000000000000e", "000000000000f", "0000000000010" ] - trashHandle = nwGUI.theProject.projTree.trashRoot() + trashHandle = nwGUI.theProject.tree.trashRoot() assert nwTree.getTreeFromHandle(trashHandle) == [ trashHandle, "0000000000012", "0000000000011" ] @@ -349,30 +349,30 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Delete the first file again (permanent), and ask for permission # Also open the document in the editor, which should trigger a close assert os.path.isfile(os.path.join(prjDir, "content", "0000000000012.nwd")) - assert "0000000000012" in nwGUI.theProject.projTree + assert "0000000000012" in nwGUI.theProject.tree assert nwGUI.docEditor.docHandle() is None assert nwGUI.openDocument("0000000000012") is True assert nwGUI.docEditor.docHandle() == "0000000000012" assert nwTree.deleteItem("0000000000012") is True assert nwGUI.docEditor.docHandle() is None assert not os.path.isfile(os.path.join(prjDir, "content", "0000000000012.nwd")) - assert "0000000000012" not in nwGUI.theProject.projTree + assert "0000000000012" not in nwGUI.theProject.tree assert nwTree.getTreeFromHandle(trashHandle) == [ trashHandle, "0000000000011" ] # Delete the second file, and skip asking for permission assert os.path.isfile(os.path.join(prjDir, "content", "0000000000011.nwd")) - assert "0000000000011" in nwGUI.theProject.projTree + assert "0000000000011" in nwGUI.theProject.tree assert nwTree.deleteItem("0000000000011", alreadyAsked=True) is True assert not os.path.isfile(os.path.join(prjDir, "content", "0000000000011.nwd")) - assert "0000000000011" not in nwGUI.theProject.projTree + assert "0000000000011" not in nwGUI.theProject.tree assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] # Delete Folder # ============= - trashHandle = nwGUI.theProject.projTree.trashRoot() + trashHandle = nwGUI.theProject.tree.trashRoot() # Add a folder with two files nwTree.setSelectedHandle("0000000000009") From b5b00744117a74d2c797ae644e964588c457ec5c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 28 May 2022 18:48:31 +0200 Subject: [PATCH 103/179] Rewritten index storage using classes instead --- novelwriter/core/index.py | 226 +++++++++++++++++++++++++++++++++++++- 1 file changed, 225 insertions(+), 1 deletion(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 7647a9f6..6745a637 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -35,7 +35,7 @@ from novelwriter.error import logException from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode from novelwriter.core.document import NWDoc from novelwriter.common import ( - isHandle, isTitleTag, isItemClass, isItemLayout, jsonEncode + checkInt, isHandle, isTitleTag, isItemClass, isItemLayout, jsonEncode ) logger = logging.getLogger(__name__) @@ -59,6 +59,9 @@ class NWIndex(): self._fileIndex = {} self._fileMeta = {} + self._tags = {} + self._items = {} + # TimeStamps self._timeNovel = 0 self._timeNotes = 0 @@ -84,6 +87,10 @@ class NWIndex(): self._timeNovel = 0 self._timeNotes = 0 self._timeIndex = 0 + + self._tags = {} + self._items = {} + return def deleteHandle(self, tHandle): @@ -194,6 +201,18 @@ class NWIndex(): logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000) + indexFile = os.path.join(self.theProject.projMeta, "tagsIndex2.json") + tStart = time() + + itemsIndex = {} + for item in self._items.values(): + item.packData(itemsIndex) + + with open(indexFile, mode="w+", encoding="utf-8") as outFile: + outFile.write(jsonEncode(itemsIndex, nmax=3)) + + logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000) + return True ## @@ -219,6 +238,11 @@ class NWIndex(): cC, wC, pC = countWords(theText) self._fileMeta[tHandle] = ["H0", cC, wC, pC] + self._items[tHandle] = IndexItem(tHandle, theItem) + theItem.setCharCount(cC) + theItem.setWordCount(wC) + theItem.setParaCount(pC) + # If the file's meta data is missing, or the file is out of the # main project, we don't index the content if theItem.itemLayout == nwItemLayout.NO_LAYOUT: @@ -338,6 +362,10 @@ class NWIndex(): # first header level is recorded in the file meta index self._fileMeta[tHandle][0] = hDepth + tItem = self._items[tHandle] + tItem.updateLevel(hDepth) + tItem.addHeading(IndexHeading(sTitle, hDepth, hText)) + return True def _indexPage(self, tHandle, itemLayout): @@ -364,6 +392,8 @@ class NWIndex(): self._fileIndex[tHandle][sTitle]["cCount"] = cC self._fileIndex[tHandle][sTitle]["wCount"] = wC self._fileIndex[tHandle][sTitle]["pCount"] = pC + if tHandle in self._items: + self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC) return def _indexSynopsis(self, tHandle, theText, nTitle): @@ -373,6 +403,8 @@ class NWIndex(): if tHandle in self._fileIndex: if sTitle in self._fileIndex[tHandle]: self._fileIndex[tHandle][sTitle]["synopsis"] = theText + if tHandle in self._items: + self._items[tHandle].setHeadingSynopsis(sTitle, theText) return def _indexKeyword(self, tHandle, aLine, nLine, nTitle, itemClass): @@ -391,6 +423,8 @@ class NWIndex(): sTitle = f"T{nTitle:06d}" if theBits[0] == nwKeyWords.TAG_KEY: self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle] + if tHandle in self._items: + self._items[tHandle].setHeadingTag(sTitle, theBits[1]) else: if tHandle not in self._refIndex: @@ -399,6 +433,8 @@ class NWIndex(): self._refIndex[tHandle][sTitle] = [] for aVal in theBits[1:]: self._refIndex[tHandle][sTitle].append([nLine, theBits[0], aVal]) + if tHandle in self._items: + self._items[tHandle].addHeadingReferences(sTitle, theBits[1:], theBits[0]) return @@ -892,3 +928,191 @@ def countWords(theText): prevEmpty = not countPara return charCount, wordCount, paraCount + + +class IndexItem: + + DEF_HKEY = "T000000" + + def __init__(self, tHandle, tItem): + self._handle = tHandle + self._item = tItem + + self._level = "H0" + self._headings = {} + + # Add a placeholder heading + self._headings[self.DEF_HKEY] = IndexHeading(self.DEF_HKEY) + + return + + ## + # Properties + ## + + @property + def level(self): + return self._level + + ## + # Setters + ## + + def setLevel(self, level): + if level in H_VALID: + self._level = level + else: + self._level = "H0" + return + + def updateLevel(self, level): + """Set the level only if it is H0. + """ + if level in H_VALID and self._level == "H0": + self._level = level + else: + self._level = "H0" + return + + def addHeading(self, tHeading): + if "T000000" in self._headings: + self._headings.pop("T000000") + self._headings[tHeading.key] = tHeading + return + + def setHeadingCounts(self, sTitle, charCount, wordCount, paraCount): + if sTitle in self._headings: + self._headings[sTitle].setCounts(charCount, wordCount, paraCount) + return + + def setHeadingSynopsis(self, sTitle, synopText): + if sTitle in self._headings: + self._headings[sTitle].setSynopsis(synopText) + return + + def setHeadingTag(self, sTitle, tagKey): + if sTitle in self._headings: + self._headings[sTitle].setTag(tagKey) + return + + def addHeadingReferences(self, sTitle, tagKeys, refType): + if sTitle in self._headings: + for tagKey in tagKeys: + self._headings[sTitle].addReference(tagKey, refType) + return + + ## + # Data Methods + ## + + def packData(self, container): + """Pack the indexed item's data into an existing dictionary. + """ + container[self._handle] = { + "firstLevel": self._level, + } + container[self._handle]["headings"] = { + key: value.packData() for key, value in self._headings.items() + } + container[self._handle]["references"] = { + key: value.packReferences() for key, value in self._headings.items() + } + return + +# END Class IndexItem + + +class IndexHeading: + + def __init__(self, key, level="H0", title=""): + self._key = key + self._level = level + self._title = title + + self._charCount = 0 + self._wordCount = 0 + self._paraCount = 0 + self._synopsis = "" + + self._tag = "" + self._refs = {} + + return + + ## + # Properties + ## + + @property + def key(self): + return self._key + + ## + # Setters + ## + + def setLevel(self, level): + if level in H_VALID: + self._level = level + else: + self._level = "H0" + return + + def setCounts(self, charCount, wordCount, paraCount): + self._charCount = max(0, checkInt(charCount, 0)) + self._wordCount = max(0, checkInt(wordCount, 0)) + self._paraCount = max(0, checkInt(paraCount, 0)) + return + + def setSynopsis(self, synopText): + self._synopsis = str(synopText) + return + + def setTag(self, tagKey): + self._tag = str(tagKey) + return + + def addReference(self, tagKey, refType): + """Add a record of a reference tag, and what keyword types it is + associated with. + """ + if tagKey not in self._refs: + self._refs[tagKey] = set() + self._refs[tagKey].add(refType) + return + + ## + # Data Methods + ## + + def packData(self): + """Pack the values into a dictionary for saving to cache. + """ + return { + "level": self._level, + "title": self._title, + "tag": self._tag, + "cCount": self._charCount, + "wCount": self._wordCount, + "pCount": self._paraCount, + "synopsis": self._synopsis, + } + + def packReferences(self): + return {key: list(value) for key, value in self._refs.items()} + + def unpackData(self, data): + """Unpack a title entry + """ + self._setLevel(data.get("level", "H0")) + self._title = str(data.get("title", "")) + self._tag = str(data.get("tag", "")) + self.setCounts( + data.get("cCount", 0), + data.get("wCount", 0), + data.get("pCount", 0), + ) + self._synopsis = str(data.get("synopsis", "")) + return + +# END Class IndexHeading From d584f57a3b916a0a8f081e187b9d3a4fc2b0871f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 28 May 2022 19:45:33 +0200 Subject: [PATCH 104/179] Loading of new index now works --- novelwriter/core/index.py | 114 ++++++++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 17 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 6745a637..bd82df0c 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -173,6 +173,33 @@ class NWIndex(): logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) + indexFile = os.path.join(self.theProject.projMeta, "tagsIndex2.json") + tStart = time() + + if os.path.isfile(indexFile): + logger.debug("Loading index file") + try: + with open(indexFile, mode="r", encoding="utf-8") as inFile: + theData = json.load(inFile) + + except Exception: + logger.error("Failed to load index file") + logException() + self._indexBroken = True + return False + + for tHandle, tData in theData.items(): + nwItem = self.theProject.tree[tHandle] + if nwItem is not None: + tItem = IndexItem(tHandle, nwItem) + tItem.unpackData(tData) + self._items[tHandle] = tItem + + self._generateTagsIndex() + # print(json.dumps(self._tags, indent=2, default=str)) + + logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) + self._checkIndex() return True @@ -204,10 +231,7 @@ class NWIndex(): indexFile = os.path.join(self.theProject.projMeta, "tagsIndex2.json") tStart = time() - itemsIndex = {} - for item in self._items.values(): - item.packData(itemsIndex) - + itemsIndex = {handle: item.packData() for handle, item in self._items.items()} with open(indexFile, mode="w+", encoding="utf-8") as outFile: outFile.write(jsonEncode(itemsIndex, nmax=3)) @@ -423,6 +447,7 @@ class NWIndex(): sTitle = f"T{nTitle:06d}" if theBits[0] == nwKeyWords.TAG_KEY: self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle] + self._tags[theBits[1]] = [tHandle, itemClass.name, sTitle] if tHandle in self._items: self._items[tHandle].setHeadingTag(sTitle, theBits[1]) @@ -687,6 +712,17 @@ class NWIndex(): return theHandles + def _generateTagsIndex(self): + """Generate the reverse tags index from the loaded index data. + The tags index must be updated during runtime with new changes. + """ + self._tags = {} + for tHandle, tItem in self._items.items(): + for sTitle, tHead in tItem.items(): + if tHead.tag: + self._tags[tHead.tag] = (tHandle, tItem.itemClass.name, sTitle) + return + ## # Index Checkers ## @@ -940,6 +976,7 @@ class IndexItem: self._level = "H0" self._headings = {} + self._index = 0 # Add a placeholder heading self._headings[self.DEF_HKEY] = IndexHeading(self.DEF_HKEY) @@ -954,6 +991,10 @@ class IndexItem: def level(self): return self._level + @property + def itemClass(self): + return self._item.itemClass + ## # Setters ## @@ -1005,18 +1046,44 @@ class IndexItem: # Data Methods ## - def packData(self, container): - """Pack the indexed item's data into an existing dictionary. + def __getitem__(self, sTitle): + return self._headings.get(sTitle, None) + + def items(self): + return self._headings.items() + + ## + # Pack/Unpack + ## + + def packData(self): + """Pack the indexed item's data into a dictionary. """ - container[self._handle] = { - "firstLevel": self._level, - } - container[self._handle]["headings"] = { - key: value.packData() for key, value in self._headings.items() - } - container[self._handle]["references"] = { - key: value.packReferences() for key, value in self._headings.items() - } + heads = {} + refs = {} + for sTitle, hItem in self._headings.items(): + heads[sTitle] = hItem.packData() + hRefs = hItem.packReferences() + if hRefs: + refs[sTitle] = hRefs + + data = {"level": self._level} + data["headings"] = heads + if refs: + data["references"] = refs + + return data + + def unpackData(self, data): + """Unpack an item entry from the data. + """ + self._level = data.get("level", "H0") + references = data.get("references", {}) + for sTitle, hData in data.get("headings", {}).items(): + tHeading = IndexHeading(sTitle) + tHeading.unpackData(hData) + tHeading.unpackReferences(references.get(sTitle, {})) + self.addHeading(tHeading) return # END Class IndexItem @@ -1047,6 +1114,10 @@ class IndexHeading: def key(self): return self._key + @property + def tag(self): + return self._tag + ## # Setters ## @@ -1099,12 +1170,14 @@ class IndexHeading: } def packReferences(self): + """Pack references into a dictionary for saving to cache. + """ return {key: list(value) for key, value in self._refs.items()} def unpackData(self, data): - """Unpack a title entry + """Unpack a heading entry from a dictionary. """ - self._setLevel(data.get("level", "H0")) + self.setLevel(data.get("level", "H0")) self._title = str(data.get("title", "")) self._tag = str(data.get("tag", "")) self.setCounts( @@ -1115,4 +1188,11 @@ class IndexHeading: self._synopsis = str(data.get("synopsis", "")) return + def unpackReferences(self, data): + """Unpack a set of references from a dictionary. + """ + for tagKey, refTypes in data.items(): + self._refs[tagKey] = set(refTypes) + return + # END Class IndexHeading From 01be8be44f036106dfc95c4761b416e2966bfa48 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 28 May 2022 20:16:27 +0200 Subject: [PATCH 105/179] Remove old tagsIndex --- novelwriter/core/index.py | 93 ++++++------------- novelwriter/gui/docviewer.py | 2 +- tests/test_core/test_core_index.py | 142 +++++++++++++++-------------- 3 files changed, 102 insertions(+), 135 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index bd82df0c..bcdc6f3c 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -35,13 +35,14 @@ from novelwriter.error import logException from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode from novelwriter.core.document import NWDoc from novelwriter.common import ( - checkInt, isHandle, isTitleTag, isItemClass, isItemLayout, jsonEncode + checkInt, isHandle, isTitleTag, isItemLayout, jsonEncode ) logger = logging.getLogger(__name__) H_VALID = ("H0", "H1", "H2", "H3", "H4") H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} +H_NONE = "T000000" class NWIndex(): @@ -54,7 +55,6 @@ class NWIndex(): self._indexBroken = False # Indices - self._tagIndex = {} self._refIndex = {} self._fileIndex = {} self._fileMeta = {} @@ -80,7 +80,6 @@ class NWIndex(): def clearIndex(self): """Clear the index dictionaries and time stamps. """ - self._tagIndex = {} self._refIndex = {} self._fileIndex = {} self._fileMeta = {} @@ -98,9 +97,9 @@ class NWIndex(): """ logger.debug("Removing item '%s' from the index", tHandle) - delTags = list(filter(lambda x: self._tagIndex[x][1] == tHandle, self._tagIndex)) + delTags = list(filter(lambda x: self._tags[x].get("handle") == tHandle, self._tags)) for tTag in delTags: - self._tagIndex.pop(tTag, None) + self._tags.pop(tTag, None) self._refIndex.pop(tHandle, None) self._fileIndex.pop(tHandle, None) @@ -161,7 +160,6 @@ class NWIndex(): self._indexBroken = True return False - self._tagIndex = theData.get("tagIndex", {}) self._refIndex = theData.get("refIndex", {}) self._fileIndex = theData.get("fileIndex", {}) self._fileMeta = theData.get("fileMeta", {}) @@ -188,16 +186,14 @@ class NWIndex(): self._indexBroken = True return False - for tHandle, tData in theData.items(): + self._tags = theData.get("tagsIndex", {}) + for tHandle, tData in theData.get("itemIndex", {}).items(): nwItem = self.theProject.tree[tHandle] if nwItem is not None: tItem = IndexItem(tHandle, nwItem) tItem.unpackData(tData) self._items[tHandle] = tItem - self._generateTagsIndex() - # print(json.dumps(self._tags, indent=2, default=str)) - logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) self._checkIndex() @@ -215,7 +211,6 @@ class NWIndex(): try: with open(indexFile, mode="w+", encoding="utf-8") as outFile: outFile.write("{\n") - outFile.write(f' "tagIndex": {jsonEncode(self._tagIndex, n=1, nmax=2)},\n') outFile.write(f' "refIndex": {jsonEncode(self._refIndex, n=1, nmax=3)},\n') outFile.write(f' "fileIndex": {jsonEncode(self._fileIndex, n=1, nmax=3)},\n') outFile.write(f' "fileMeta": {jsonEncode(self._fileMeta, n=1, nmax=2)}\n') @@ -233,7 +228,10 @@ class NWIndex(): itemsIndex = {handle: item.packData() for handle, item in self._items.items()} with open(indexFile, mode="w+", encoding="utf-8") as outFile: - outFile.write(jsonEncode(itemsIndex, nmax=3)) + outFile.write("{\n") + outFile.write(f' "tagsIndex": {jsonEncode(self._tags, n=1, nmax=2)},\n') + outFile.write(f' "itemIndex": {jsonEncode(itemsIndex, n=1, nmax=4)}\n') + outFile.write("}\n") logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000) @@ -289,9 +287,9 @@ class NWIndex(): self._fileIndex[tHandle] = {} # Also clear references to the file in the tags index - clearTags = list(filter(lambda x: self._tagIndex[x][1] == tHandle, self._tagIndex)) + clearTags = list(filter(lambda x: self._tags[x].get("handle") == tHandle, self._tags)) for aTag in clearTags: - self._tagIndex.pop(aTag) + self._tags.pop(aTag) # Scan the text content nTitle = 0 @@ -395,7 +393,7 @@ class NWIndex(): def _indexPage(self, tHandle, itemLayout): """Index a page with no title. """ - self._fileIndex[tHandle]["T000000"] = { + self._fileIndex[tHandle][H_NONE] = { "level": "H0", "title": "", "layout": itemLayout.name, @@ -446,8 +444,11 @@ class NWIndex(): sTitle = f"T{nTitle:06d}" if theBits[0] == nwKeyWords.TAG_KEY: - self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle] - self._tags[theBits[1]] = [tHandle, itemClass.name, sTitle] + self._tags[theBits[1]] = { + "handle": tHandle, + "heading": sTitle, + "class": itemClass.name, + } if tHandle in self._items: self._items[tHandle].setHeadingTag(sTitle, theBits[1]) @@ -521,8 +522,8 @@ class NWIndex(): # For a tag, only the first value is accepted, the rest are ignored if theBits[0] == nwKeyWords.TAG_KEY and nBits > 1: - if theBits[1] in self._tagIndex: - isGood[1] = self._tagIndex[theBits[1]][1] == tItem.itemHandle + if theBits[1] in self._tags: + isGood[1] = self._tags[theBits[1]].get("handle") == tItem.itemHandle else: isGood[1] = True return isGood @@ -530,8 +531,8 @@ class NWIndex(): # If we're still here, we check that the references exist theKey = nwKeyWords.KEY_CLASS[theBits[0]].name for n in range(1, nBits): - if theBits[n] in self._tagIndex: - isGood[n] = theKey == self._tagIndex[theBits[n]][2] + if theBits[n] in self._tags: + isGood[n] = theKey == self._tags[theBits[n]].get("class") return isGood @@ -674,7 +675,7 @@ class NWIndex(): return {} theRefs = {} - theTags = set(filter(lambda x: self._tagIndex[x][1] == tHandle, self._tagIndex)) + theTags = set(filter(lambda x: self._tags[x].get("handle") == tHandle, self._tags)) if theTags: for tHandle in self._refIndex: for sTitle in self._refIndex[tHandle]: @@ -687,10 +688,8 @@ class NWIndex(): def getTagSource(self, theTag): """Return the source location of a given tag. """ - theRef = self._tagIndex.get(theTag, []) - if len(theRef) == 4: - return theRef[1], theRef[0], theRef[3] - return None, 0, "T000000" + ref = self._tags.get(theTag, {}) + return ref.get("handle"), ref.get("heading", H_NONE) ## # Internal Functions @@ -712,17 +711,6 @@ class NWIndex(): return theHandles - def _generateTagsIndex(self): - """Generate the reverse tags index from the loaded index data. - The tags index must be updated during runtime with new changes. - """ - self._tags = {} - for tHandle, tItem in self._items.items(): - for sTitle, tHead in tItem.items(): - if tHead.tag: - self._tags[tHead.tag] = (tHandle, tItem.itemClass.name, sTitle) - return - ## # Index Checkers ## @@ -737,7 +725,6 @@ class NWIndex(): tStart = time() try: - self._checkTagIndex() self._checkRefIndex() self._checkFileIndex() self._checkFileMeta() @@ -763,28 +750,6 @@ class NWIndex(): return - def _checkTagIndex(self): - """Scan the tag index for errors. - Warning: This function raises exceptions. - """ - for tTag in self._tagIndex: - if not isinstance(tTag, str): - raise KeyError("tagIndex key is not a string") - - tEntry = self._tagIndex[tTag] - if len(tEntry) != 4: - raise IndexError("tagIndex[a] expected 4 values") - if not isinstance(tEntry[0], int): - raise ValueError("tagIndex[a][0] is not an integer") - if not isHandle(tEntry[1]): - raise ValueError("tagIndex[a][1] is not a handle") - if not isItemClass(tEntry[2]): - raise ValueError("tagIndex[a][2] is not an nwItemClass") - if not isTitleTag(tEntry[3]): - raise ValueError("tagIndex[a][3] is not a title tag") - - return - def _checkRefIndex(self): """Scan the reference index for errors. Warning: This function raises exceptions. @@ -968,8 +933,6 @@ def countWords(theText): class IndexItem: - DEF_HKEY = "T000000" - def __init__(self, tHandle, tItem): self._handle = tHandle self._item = tItem @@ -979,7 +942,7 @@ class IndexItem: self._index = 0 # Add a placeholder heading - self._headings[self.DEF_HKEY] = IndexHeading(self.DEF_HKEY) + self._headings[H_NONE] = IndexHeading(H_NONE) return @@ -1016,8 +979,8 @@ class IndexItem: return def addHeading(self, tHeading): - if "T000000" in self._headings: - self._headings.pop("T000000") + if H_NONE in self._headings: + self._headings.pop(H_NONE) self._headings[tHeading.key] = tHeading return diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 2587f125..2b59cf8e 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -245,7 +245,7 @@ class GuiDocViewer(QTextBrowser): index being up to date. """ logger.debug("Loading document from tag '%s'", theTag) - tHandle, _, sTitle = self.theProject.index.getTagSource(theTag) + tHandle, sTitle = self.theProject.index.getTagSource(theTag) if tHandle is None: self.theParent.makeAlert(self.tr( "Could not find the reference for tag '{0}'. It either doesn't " diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 0d070eba..d2e76643 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -68,25 +68,25 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): assert theIndex.saveIndex() is True # Take a copy of the index - tagIndex = str(theIndex._tagIndex) + tagIndex = str(theIndex._tags) refIndex = str(theIndex._refIndex) fileIndex = str(theIndex._fileIndex) textCounts = str(theIndex._fileMeta) # Delete a handle - assert theIndex._tagIndex.get("Bod", None) is not None + assert theIndex._tags.get("Bod", None) is not None assert theIndex._refIndex.get("4c4f28287af27", None) is not None assert theIndex._fileIndex.get("4c4f28287af27", None) is not None assert theIndex._fileMeta.get("4c4f28287af27", None) is not None theIndex.deleteHandle("4c4f28287af27") - assert theIndex._tagIndex.get("Bod", None) is None + assert theIndex._tags.get("Bod", None) is None assert theIndex._refIndex.get("4c4f28287af27", None) is None assert theIndex._fileIndex.get("4c4f28287af27", None) is None assert theIndex._fileMeta.get("4c4f28287af27", None) is None # Clear the index theIndex.clearIndex() - assert theIndex._tagIndex == {} + assert theIndex._tags == {} assert theIndex._refIndex == {} assert theIndex._fileIndex == {} assert theIndex._fileMeta == {} @@ -99,16 +99,16 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): # Make the load pass assert theIndex.loadIndex() is True - assert str(theIndex._tagIndex) == tagIndex + assert str(theIndex._tags) == tagIndex assert str(theIndex._refIndex) == refIndex assert str(theIndex._fileIndex) == fileIndex assert str(theIndex._fileMeta) == textCounts # Break the index and check that we notice - assert theIndex.indexBroken is False - theIndex._tagIndex["Bod"].append("Stuff") - theIndex._checkIndex() - assert theIndex.indexBroken is True + # assert theIndex.indexBroken is False + # theIndex._tagIndex["Bod"].append("Stuff") + # theIndex._checkIndex() + # assert theIndex.indexBroken is True # Finalise assert theProject.closeProject() is True @@ -198,7 +198,9 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI): "@pov: Jane\n" "@invalid: John\n" # Checks for issue #688 )) - assert theIndex._tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]} + assert theIndex._tags == { + "Jane": {"handle": cHandle, "heading": "T000001", "class": "CHARACTER"} + } assert theIndex.getNovelData(nHandle, "T000001")["title"] == "Hello World!" assert theIndex.getReferences(nHandle, "T000001") == { "@char": [], @@ -309,7 +311,9 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "This is a story about Jane Smith.\n\n" "Well, not really.\n" )) - assert theIndex._tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]} + assert theIndex._tags == { + "Jane": {"handle": cHandle, "heading": "T000001", "class": "CHARACTER"} + } assert theIndex.getNovelData(nHandle, "T000001")["title"] == "Hello World!" # Title Indexing @@ -551,8 +555,8 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # getTagSource # ============ - assert theIndex.getTagSource("Jane") == (cHandle, 2, "T000001") - assert theIndex.getTagSource("John") == (None, 0, "T000000") + assert theIndex.getTagSource("Jane") == (cHandle, "T000001") + assert theIndex.getTagSource("John") == (None, "T000000") # getCounts # ========= @@ -696,69 +700,69 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # END Test testCoreIndex_ExtractData -@pytest.mark.core -def testCoreIndex_CheckTagIndex(mockGUI): - """Test the tag index checker. - """ - theProject = NWProject(mockGUI) - theIndex = NWIndex(theProject) +# @pytest.mark.core +# def testCoreIndex_CheckTagIndex(mockGUI): +# """Test the tag index checker. +# """ +# theProject = NWProject(mockGUI) +# theIndex = NWIndex(theProject) - # Valid Index - theIndex._tagIndex = { - "John": [3, "14298de4d9524", "CHARACTER", "T000001"], - "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001"], - } - assert theIndex._checkTagIndex() is None +# # Valid Index +# theIndex._tagIndex = { +# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], +# "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001"], +# } +# assert theIndex._checkTagIndex() is None - # Wrong Key Type - theIndex._tagIndex = { - "John": [3, "14298de4d9524", "CHARACTER", "T000001"], - 123456: [3, "bb2c23b3c42cc", "CHARACTER", "T000001"], - } - with pytest.raises(KeyError): - theIndex._checkTagIndex() +# # Wrong Key Type +# theIndex._tagIndex = { +# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], +# 123456: [3, "bb2c23b3c42cc", "CHARACTER", "T000001"], +# } +# with pytest.raises(KeyError): +# theIndex._checkTagIndex() - # Wrong Length - theIndex._tagIndex = { - "John": [3, "14298de4d9524", "CHARACTER", "T000001"], - "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001", "Stuff"], - } - with pytest.raises(IndexError): - theIndex._checkTagIndex() +# # Wrong Length +# theIndex._tagIndex = { +# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], +# "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001", "Stuff"], +# } +# with pytest.raises(IndexError): +# theIndex._checkTagIndex() - # Wrong Type of Entry 0 - theIndex._tagIndex = { - "John": [3, "14298de4d9524", "CHARACTER", "T000001"], - "Jane": ["3", "bb2c23b3c42cc", "CHARACTER", "T000001"], - } - with pytest.raises(ValueError): - theIndex._checkTagIndex() +# # Wrong Type of Entry 0 +# theIndex._tagIndex = { +# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], +# "Jane": ["3", "bb2c23b3c42cc", "CHARACTER", "T000001"], +# } +# with pytest.raises(ValueError): +# theIndex._checkTagIndex() - # Wrong Type of Entry 1 - theIndex._tagIndex = { - "John": [3, "14298de4d9524", "CHARACTER", "T000001"], - "Jane": [3, 0xbb2c23b3c42cc, "CHARACTER", "T000001"], - } - with pytest.raises(ValueError): - theIndex._checkTagIndex() +# # Wrong Type of Entry 1 +# theIndex._tagIndex = { +# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], +# "Jane": [3, 0xbb2c23b3c42cc, "CHARACTER", "T000001"], +# } +# with pytest.raises(ValueError): +# theIndex._checkTagIndex() - # Wrong Type of Entry 2 - theIndex._tagIndex = { - "John": [3, "14298de4d9524", "CHARACTER", "T000001"], - "Jane": [3, "bb2c23b3c42cc", "INVALID_CLASS", "T000001"], - } - with pytest.raises(ValueError): - theIndex._checkTagIndex() +# # Wrong Type of Entry 2 +# theIndex._tagIndex = { +# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], +# "Jane": [3, "bb2c23b3c42cc", "INVALID_CLASS", "T000001"], +# } +# with pytest.raises(ValueError): +# theIndex._checkTagIndex() - # Wrong Type of Entry 3 - theIndex._tagIndex = { - "John": [3, "14298de4d9524", "CHARACTER", "T000001"], - "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "INVALID"], - } - with pytest.raises(ValueError): - theIndex._checkTagIndex() +# # Wrong Type of Entry 3 +# theIndex._tagIndex = { +# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], +# "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "INVALID"], +# } +# with pytest.raises(ValueError): +# theIndex._checkTagIndex() -# END Test testCoreIndex_CheckTagIndex +# # END Test testCoreIndex_CheckTagIndex @pytest.mark.core @@ -1225,7 +1229,7 @@ def testCoreIndex_CheckFileMeta(mockGUI): with pytest.raises(ValueError): theIndex._checkFileMeta() -# END Test testCoreIndex_CheckTextCounts +# END Test testCoreIndex_CheckFileMeta @pytest.mark.core From 94ba7a2f94041dfaf20de2eeabdbc04a198c0fff Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 28 May 2022 21:59:52 +0200 Subject: [PATCH 106/179] Use the new index instead of the old --- novelwriter/core/index.py | 400 ++++------ novelwriter/gui/noveltree.py | 8 +- novelwriter/gui/outline.py | 16 +- novelwriter/gui/outlinedetails.py | 14 +- .../coreIndex_LoadSave_tagsIndex.json | 216 +++--- tests/test_core/test_core_index.py | 700 ++---------------- tests/test_gui/test_gui_docviewer.py | 4 +- 7 files changed, 343 insertions(+), 1015 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index bcdc6f3c..95485bf9 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -34,9 +34,7 @@ from novelwriter.enum import nwItemType, nwItemLayout from novelwriter.error import logException from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode from novelwriter.core.document import NWDoc -from novelwriter.common import ( - checkInt, isHandle, isTitleTag, isItemLayout, jsonEncode -) +from novelwriter.common import checkInt, jsonEncode logger = logging.getLogger(__name__) @@ -55,10 +53,6 @@ class NWIndex(): self._indexBroken = False # Indices - self._refIndex = {} - self._fileIndex = {} - self._fileMeta = {} - self._tags = {} self._items = {} @@ -80,9 +74,6 @@ class NWIndex(): def clearIndex(self): """Clear the index dictionaries and time stamps. """ - self._refIndex = {} - self._fileIndex = {} - self._fileMeta = {} self._timeNovel = 0 self._timeNotes = 0 self._timeIndex = 0 @@ -95,15 +86,15 @@ class NWIndex(): def deleteHandle(self, tHandle): """Delete all entries of a given document handle. """ + if tHandle not in self._items: + return + logger.debug("Removing item '%s' from the index", tHandle) - delTags = list(filter(lambda x: self._tags[x].get("handle") == tHandle, self._tags)) - for tTag in delTags: + for tTag in self._items[tHandle].allTags(): self._tags.pop(tTag, None) - self._refIndex.pop(tHandle, None) - self._fileIndex.pop(tHandle, None) - self._fileMeta.pop(tHandle, None) + self._items.pop(tHandle, None) return @@ -148,32 +139,6 @@ class NWIndex(): indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) tStart = time() - if os.path.isfile(indexFile): - logger.debug("Loading index file") - try: - with open(indexFile, mode="r", encoding="utf-8") as inFile: - theData = json.load(inFile) - - except Exception: - logger.error("Failed to load index file") - logException() - self._indexBroken = True - return False - - self._refIndex = theData.get("refIndex", {}) - self._fileIndex = theData.get("fileIndex", {}) - self._fileMeta = theData.get("fileMeta", {}) - - nowTime = round(time()) - self._timeNovel = nowTime - self._timeNotes = nowTime - self._timeIndex = nowTime - - logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) - - indexFile = os.path.join(self.theProject.projMeta, "tagsIndex2.json") - tStart = time() - if os.path.isfile(indexFile): logger.debug("Loading index file") try: @@ -194,6 +159,11 @@ class NWIndex(): tItem.unpackData(tData) self._items[tHandle] = tItem + nowTime = round(time()) + self._timeNovel = nowTime + self._timeNotes = nowTime + self._timeIndex = nowTime + logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) self._checkIndex() @@ -209,11 +179,11 @@ class NWIndex(): tStart = time() try: + itemsIndex = {handle: item.packData() for handle, item in self._items.items()} with open(indexFile, mode="w+", encoding="utf-8") as outFile: outFile.write("{\n") - outFile.write(f' "refIndex": {jsonEncode(self._refIndex, n=1, nmax=3)},\n') - outFile.write(f' "fileIndex": {jsonEncode(self._fileIndex, n=1, nmax=3)},\n') - outFile.write(f' "fileMeta": {jsonEncode(self._fileMeta, n=1, nmax=2)}\n') + outFile.write(f' "tagsIndex": {jsonEncode(self._tags, n=1, nmax=2)},\n') + outFile.write(f' "itemIndex": {jsonEncode(itemsIndex, n=1, nmax=4)}\n') outFile.write("}\n") except Exception: @@ -223,18 +193,6 @@ class NWIndex(): logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000) - indexFile = os.path.join(self.theProject.projMeta, "tagsIndex2.json") - tStart = time() - - itemsIndex = {handle: item.packData() for handle, item in self._items.items()} - with open(indexFile, mode="w+", encoding="utf-8") as outFile: - outFile.write("{\n") - outFile.write(f' "tagsIndex": {jsonEncode(self._tags, n=1, nmax=2)},\n') - outFile.write(f' "itemIndex": {jsonEncode(itemsIndex, n=1, nmax=4)}\n') - outFile.write("}\n") - - logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000) - return True ## @@ -256,11 +214,12 @@ class NWIndex(): logger.info("Not indexing non-file item '%s'", tHandle) return False - # Run word counter for the whole text - cC, wC, pC = countWords(theText) - self._fileMeta[tHandle] = ["H0", cC, wC, pC] + self.deleteHandle(tHandle) + # Run word counter for the whole text self._items[tHandle] = IndexItem(tHandle, theItem) + + cC, wC, pC = countWords(theText) theItem.setCharCount(cC) theItem.setWordCount(wC) theItem.setParaCount(pC) @@ -282,15 +241,6 @@ class NWIndex(): logger.debug("Indexing item with handle '%s'", tHandle) - # Delete or reset old entries for the file - self._refIndex.pop(tHandle, None) - self._fileIndex[tHandle] = {} - - # Also clear references to the file in the tags index - clearTags = list(filter(lambda x: self._tags[x].get("handle") == tHandle, self._tags)) - for aTag in clearTags: - self._tags.pop(aTag) - # Scan the text content nTitle = 0 theLines = theText.splitlines() @@ -326,7 +276,6 @@ class NWIndex(): # Index page with no titles and references if nTitle == 0: - self._indexPage(tHandle, itemLayout) self._indexWordCounts(tHandle, theText, nTitle) # Update timestamps for index changes @@ -369,51 +318,17 @@ class NWIndex(): return False sTitle = f"T{nLine:06d}" - self._fileIndex[tHandle][sTitle] = { - "level": hDepth, - "title": hText, - "layout": itemLayout.name, - "cCount": 0, - "wCount": 0, - "pCount": 0, - "synopsis": "", - } - - if self._fileMeta[tHandle][0] == "H0": - # Since this initialises to H0, this ensures that only the - # first header level is recorded in the file meta index - self._fileMeta[tHandle][0] = hDepth - tItem = self._items[tHandle] tItem.updateLevel(hDepth) tItem.addHeading(IndexHeading(sTitle, hDepth, hText)) return True - def _indexPage(self, tHandle, itemLayout): - """Index a page with no title. - """ - self._fileIndex[tHandle][H_NONE] = { - "level": "H0", - "title": "", - "layout": itemLayout.name, - "cCount": 0, - "wCount": 0, - "pCount": 0, - "synopsis": "", - } - return - def _indexWordCounts(self, tHandle, theText, nTitle): """Count text stats and save the counts to the index. """ cC, wC, pC = countWords(theText) sTitle = f"T{nTitle:06d}" - if tHandle in self._fileIndex: - if sTitle in self._fileIndex[tHandle]: - self._fileIndex[tHandle][sTitle]["cCount"] = cC - self._fileIndex[tHandle][sTitle]["wCount"] = wC - self._fileIndex[tHandle][sTitle]["pCount"] = pC if tHandle in self._items: self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC) return @@ -422,9 +337,6 @@ class NWIndex(): """Save the synopsis to the index. """ sTitle = f"T{nTitle:06d}" - if tHandle in self._fileIndex: - if sTitle in self._fileIndex[tHandle]: - self._fileIndex[tHandle][sTitle]["synopsis"] = theText if tHandle in self._items: self._items[tHandle].setHeadingSynopsis(sTitle, theText) return @@ -451,14 +363,7 @@ class NWIndex(): } if tHandle in self._items: self._items[tHandle].setHeadingTag(sTitle, theBits[1]) - else: - if tHandle not in self._refIndex: - self._refIndex[tHandle] = {} - if sTitle not in self._refIndex[tHandle]: - self._refIndex[tHandle][sTitle] = [] - for aVal in theBits[1:]: - self._refIndex[tHandle][sTitle].append([nLine, theBits[0], aVal]) if tHandle in self._items: self._items[tHandle].addHeadingReferences(sTitle, theBits[1:], theBits[0]) @@ -546,17 +451,17 @@ class NWIndex(): files, but skipping all note files. """ for tHandle in self._listNovelHandles(skipExcluded): - for sTitle in sorted(self._fileIndex[tHandle]): + for sTitle in self._items[tHandle].headings: tKey = f"{tHandle}:{sTitle}" - yield tKey, tHandle, sTitle, self._fileIndex[tHandle][sTitle] + yield tKey, tHandle, sTitle, self._items[tHandle][sTitle] def getNovelWordCount(self, skipExcluded=True): """Count the number of words in the novel project. """ wCount = 0 for tHandle in self._listNovelHandles(skipExcluded): - for sTitle in self._fileIndex[tHandle]: - wCount += self._fileIndex[tHandle][sTitle]["wCount"] + for hItem in self._items[tHandle].entries: + wCount += hItem.wordCount return wCount @@ -565,8 +470,8 @@ class NWIndex(): """ hCount = [0, 0, 0, 0, 0] for tHandle in self._listNovelHandles(skipExcluded): - for sTitle in self._fileIndex[tHandle]: - iLevel = H_LEVEL.get(self._fileIndex[tHandle][sTitle]["level"], 0) + for hItem in self._items[tHandle].entries: + iLevel = H_LEVEL.get(hItem.level, 0) hCount[iLevel] += 1 return hCount @@ -574,19 +479,26 @@ class NWIndex(): def getHandleWordCounts(self, tHandle): """Get all header word counts for a specific handle. """ - hRecord = self._fileIndex.get(tHandle, {}) - return [(f"{tHandle}:{sTitle}", sData["wCount"]) for sTitle, sData in hRecord.items()] + return [ + (f"{tHandle}:{sTitle}", hItem.wordCount) + for sTitle, hItem in self._items.get(tHandle, {}).items() + ] def getHandleHeaders(self, tHandle): """Get all headers for a specific handle. """ - hRecord = self._fileIndex.get(tHandle, {}) - return [(sTitle, sData["level"], sData["title"]) for sTitle, sData in hRecord.items()] + return [ + (sTitle, hItem.level, hItem.title) + for sTitle, hItem in self._items.get(tHandle, {}).items() + ] def getHandleHeaderLevel(self, tHandle): """Get the header level of the first header of a handle. """ - return self._fileMeta.get(tHandle, ["H0"])[0] + if tHandle in self._items: + return self._items[tHandle].level + else: + return "H0" def getTableOfContents(self, maxDepth, skipExcluded=True): """Generate a table of contents up to a maximum depth. @@ -595,21 +507,20 @@ class NWIndex(): tData = {} pKey = None for tHandle in self._listNovelHandles(skipExcluded): - for sTitle in sorted(self._fileIndex[tHandle]): + for sTitle in self._items[tHandle].headings: tKey = f"{tHandle}:{sTitle}" - theData = self._fileIndex[tHandle][sTitle] - iLevel = H_LEVEL.get(theData["level"], 0) + hItem = self._items[tHandle][sTitle] + iLevel = H_LEVEL.get(hItem.level, 0) if iLevel > maxDepth: if pKey in tData: - theData["wCount"] - tData[pKey]["words"] += theData["wCount"] + tData[pKey]["words"] += hItem.wordCount else: pKey = tKey tOrder.append(tKey) tData[tKey] = { "level": iLevel, - "title": theData["title"], - "words": theData["wCount"], + "title": hItem.title, + "words": hItem.wordCount, } theToC = [( @@ -630,16 +541,18 @@ class NWIndex(): pC = 0 if sTitle is None: - if tHandle in self._fileMeta: - cC = self._fileMeta[tHandle][1] - wC = self._fileMeta[tHandle][2] - pC = self._fileMeta[tHandle][3] + if tHandle in self._items: + tItem = self._items[tHandle].item + cC = tItem.charCount + wC = tItem.wordCount + pC = tItem.paraCount else: - if tHandle in self._fileIndex: - if sTitle in self._fileIndex[tHandle]: - cC = self._fileIndex[tHandle][sTitle]["cCount"] - wC = self._fileIndex[tHandle][sTitle]["wCount"] - pC = self._fileIndex[tHandle][sTitle]["pCount"] + if tHandle in self._items: + if sTitle in self._items[tHandle]: + hItem = self._items[tHandle][sTitle] + cC = hItem.charCount + wC = hItem.wordCount + pC = hItem.paraCount return cC, wC, pC @@ -648,40 +561,43 @@ class NWIndex(): section. """ theRefs = {x: [] for x in nwKeyWords.KEY_CLASS} - if tHandle not in self._refIndex: + if tHandle not in self._items: return theRefs - for refTitle in self._refIndex[tHandle]: - for aTag in self._refIndex[tHandle][refTitle]: - if len(aTag) == 3 and (sTitle is None or sTitle == refTitle): - if aTag[1] in theRefs: - theRefs[aTag[1]].append(aTag[2]) + for rTitle, hItem in self._items[tHandle].items(): + if sTitle is None or sTitle == rTitle: + for aTag, refTypes in hItem.references.items(): + for refType in refTypes: + if refType in theRefs: + theRefs[refType].append(aTag) return theRefs def getNovelData(self, tHandle, sTitle): """Return the novel data of a given handle and title. """ - if tHandle in self._fileIndex: - if sTitle in self._fileIndex[tHandle]: - return self._fileIndex[tHandle][sTitle] + if tHandle in self._items: + if sTitle in self._items[tHandle]: + return self._items[tHandle][sTitle] return None def getBackReferenceList(self, tHandle): """Build a list of files referring back to our file, specified by tHandle. """ - if tHandle is None: + if tHandle is None or tHandle not in self._items: return {} theRefs = {} - theTags = set(filter(lambda x: self._tags[x].get("handle") == tHandle, self._tags)) - if theTags: - for tHandle in self._refIndex: - for sTitle in self._refIndex[tHandle]: - for _, _, tTag in self._refIndex[tHandle][sTitle]: - if tTag in theTags and tHandle not in theRefs: - theRefs[tHandle] = sTitle + theTags = self._items[tHandle].allTags() + if not theTags: + return theRefs + + for aHandle, tItem in self._items.items(): + for sTitle, hItem in tItem.items(): + for aTag in hItem.references: + if aTag in theTags and aHandle not in theRefs: + theRefs[aHandle] = sTitle return theRefs @@ -706,7 +622,7 @@ class NWIndex(): continue if tItem.itemLayout == nwItemLayout.NOTE: continue - if tItem.itemHandle in self._fileIndex: + if tItem.itemHandle in self._items: theHandles.append(tItem.itemHandle) return theHandles @@ -724,25 +640,9 @@ class NWIndex(): logger.debug("Checking index") tStart = time() - try: - self._checkRefIndex() - self._checkFileIndex() - self._checkFileMeta() - self._indexBroken = False - - except Exception: - logger.error("Error while checking index") - logException() - self._indexBroken = True - - if self._indexBroken: - self.clearIndex() - logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000) - return - # If the index was ok, we check that project files are indexed for fHandle in self.theProject.projFiles: - if fHandle not in self._fileMeta: + if fHandle not in self._items: logger.warning("Item '%s' is not in the index", fHandle) self.reIndexHandle(fHandle) @@ -750,103 +650,6 @@ class NWIndex(): return - def _checkRefIndex(self): - """Scan the reference index for errors. - Warning: This function raises exceptions. - """ - for tHandle in self._refIndex: - if not isHandle(tHandle): - raise KeyError("refIndex key is not a handle") - - hEntry = self._refIndex[tHandle] - for sTitle in hEntry: - if not isTitleTag(sTitle): - raise KeyError("refIndex[a] key is not a title tag") - - sEntry = hEntry[sTitle] - for tEntry in sEntry: - if len(tEntry) != 3: - raise IndexError("refIndex[a][b][i] expected 3 values") - if not isinstance(tEntry[0], int): - raise ValueError("refIndex[a][b][i][0] is not an integer") - if not tEntry[1] in nwKeyWords.VALID_KEYS: - raise ValueError("refIndex[a][b][i][1] is not a keyword") - if not isinstance(tEntry[2], str): - raise ValueError("refIndex[a][b][i][2] is not a string") - - return - - def _checkFileIndex(self): - """Scan the file index for errors. - Warning: This function raises exceptions. - """ - for tHandle in self._fileIndex: - if not isHandle(tHandle): - raise KeyError("fileIndex key is not a handle") - - hEntry = self._fileIndex[tHandle] - for sTitle in self._fileIndex[tHandle]: - if not isTitleTag(sTitle): - raise KeyError("fileIndex[a] key is not a title tag") - - sEntry = hEntry[sTitle] - if len(sEntry) != 7: - raise IndexError("fileIndex[a][b] expected 7 values") - - if "level" not in sEntry: - raise KeyError("fileIndex[a][b] has no 'level' key") - if "title" not in sEntry: - raise KeyError("fileIndex[a][b] has no 'title' key") - if "layout" not in sEntry: - raise KeyError("fileIndex[a][b] has no 'layout' key") - if "cCount" not in sEntry: - raise KeyError("fileIndex[a][b] has no 'cCount' key") - if "wCount" not in sEntry: - raise KeyError("fileIndex[a][b] has no 'wCount' key") - if "pCount" not in sEntry: - raise KeyError("fileIndex[a][b] has no 'pCount' key") - if "synopsis" not in sEntry: - raise KeyError("fileIndex[a][b] has no 'synopsis' key") - - if not sEntry["level"] in H_VALID: - raise ValueError("fileIndex[a][b][level] is not a header level") - if not isinstance(sEntry["title"], str): - raise ValueError("fileIndex[a][b][title] is not a string") - if not isItemLayout(sEntry["layout"]): - raise ValueError("fileIndex[a][b][layout] is not an nwItemLayout") - if not isinstance(sEntry["cCount"], int): - raise ValueError("fileIndex[a][b][cCount] is not an integer") - if not isinstance(sEntry["wCount"], int): - raise ValueError("fileIndex[a][b][wCount] is not an integer") - if not isinstance(sEntry["pCount"], int): - raise ValueError("fileIndex[a][b][pCount] is not an integer") - if not isinstance(sEntry["synopsis"], str): - raise ValueError("fileIndex[a][b][synopsis] is not a string") - - return - - def _checkFileMeta(self): - """Scan the text counts index for errors. - Warning: This function raises exceptions. - """ - for tHandle in self._fileMeta: - if not isHandle(tHandle): - raise KeyError("fileMeta key is not a handle") - - tEntry = self._fileMeta[tHandle] - if len(tEntry) != 4: - raise IndexError("fileMeta[a] expected 4 values") - if not tEntry[0] in H_VALID: - raise ValueError("fileMeta[a][0] is not a header level") - if not isinstance(tEntry[1], int): - raise ValueError("fileMeta[a][1] is not an integer") - if not isinstance(tEntry[2], int): - raise ValueError("fileMeta[a][2] is not an integer") - if not isinstance(tEntry[3], int): - raise ValueError("fileMeta[a][3] is not an integer") - - return - # END Class NWIndex @@ -950,13 +753,21 @@ class IndexItem: # Properties ## + @property + def item(self): + return self._item + @property def level(self): return self._level @property - def itemClass(self): - return self._item.itemClass + def headings(self): + return sorted(self._headings.keys()) + + @property + def entries(self): + return self._headings.values() ## # Setters @@ -1012,9 +823,22 @@ class IndexItem: def __getitem__(self, sTitle): return self._headings.get(sTitle, None) + def __contains__(self, sTitle): + return sTitle in self._headings + def items(self): return self._headings.items() + def allTags(self): + """Return a list of all tags in the current item. + """ + tags = [] + for hItem in self._headings.values(): + tag = hItem.tag + if tag: + tags.append(tag) + return tags + ## # Pack/Unpack ## @@ -1077,10 +901,38 @@ class IndexHeading: def key(self): return self._key + @property + def level(self): + return self._level + + @property + def title(self): + return self._title + + @property + def charCount(self): + return self._charCount + + @property + def wordCount(self): + return self._wordCount + + @property + def paraCount(self): + return self._paraCount + + @property + def synopsis(self): + return self._synopsis + @property def tag(self): return self._tag + @property + def references(self): + return self._refs + ## # Setters ## diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 91895c7a..28edb2d9 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -258,7 +258,7 @@ class GuiNovelTree(QTreeWidget): tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) self._treeMap[tKey] = tItem - tLevel = novIdx["level"] + tLevel = novIdx.level if tLevel == "H1": self.addTopLevelItem(tItem) currTitle = tItem @@ -305,12 +305,12 @@ class GuiNovelTree(QTreeWidget): """Populate a tree item with all the column values. """ newItem = QTreeWidgetItem() - hIcon = "doc_%s" % novIdx["level"].lower() + hIcon = "doc_%s" % novIdx.level.lower() theData = (tHandle, sTitle[1:].lstrip("0"), titleKey) - wC = int(novIdx["wCount"]) + wC = int(novIdx.wordCount) - newItem.setText(self.C_TITLE, novIdx["title"]) + newItem.setText(self.C_TITLE, novIdx.title) newItem.setData(self.C_TITLE, Qt.UserRole, theData) newItem.setIcon(self.C_TITLE, self.theTheme.getIcon(hIcon)) newItem.setText(self.C_WORDS, f"{wC:n}") diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 26cb8029..e3886b87 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -393,7 +393,7 @@ class GuiOutline(QTreeWidget): tItem = self._createTreeItem(tHandle, sTitle, novIdx) - tLevel = novIdx["level"] + tLevel = novIdx.level if tLevel == "H1": self.addTopLevelItem(tItem) currTitle = tItem @@ -441,24 +441,24 @@ class GuiOutline(QTreeWidget): """ nwItem = self.theProject.tree[tHandle] newItem = QTreeWidgetItem() - hIcon = "doc_%s" % novIdx["level"].lower() + hIcon = "doc_%s" % novIdx.level.lower() hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) dIcon = self.theTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel) - cC = int(novIdx["cCount"]) - wC = int(novIdx["wCount"]) - pC = int(novIdx["pCount"]) + cC = int(novIdx.charCount) + wC = int(novIdx.wordCount) + pC = int(novIdx.paraCount) - newItem.setText(self._colIdx[nwOutline.TITLE], novIdx["title"]) + newItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title) newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle) newItem.setIcon(self._colIdx[nwOutline.TITLE], self.theTheme.getIcon(hIcon)) - newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx["level"]) + newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level) newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName) newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon) newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0")) newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle) - newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx["synopsis"]) + newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis) newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}") newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}") newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}") diff --git a/novelwriter/gui/outlinedetails.py b/novelwriter/gui/outlinedetails.py index 40a3d29e..f6f86e67 100644 --- a/novelwriter/gui/outlinedetails.py +++ b/novelwriter/gui/outlinedetails.py @@ -288,26 +288,26 @@ class GuiOutlineDetails(QScrollArea): if nwItem is None or novIdx is None: return False - if novIdx["level"] in self.LVL_MAP: - self.titleLabel.setText("%s" % self.tr(self.LVL_MAP[novIdx["level"]])) + if novIdx.level in self.LVL_MAP: + self.titleLabel.setText("%s" % self.tr(self.LVL_MAP[novIdx.level])) else: self.titleLabel.setText("%s" % self.tr("Title")) - self.titleValue.setText(novIdx["title"]) + self.titleValue.setText(novIdx.title) itemStatus, _ = nwItem.getImportStatus() self.fileValue.setText(nwItem.itemName) self.itemValue.setText(itemStatus) - cC = checkInt(novIdx["cCount"], 0) - wC = checkInt(novIdx["wCount"], 0) - pC = checkInt(novIdx["pCount"], 0) + cC = checkInt(novIdx.charCount, 0) + wC = checkInt(novIdx.wordCount, 0) + pC = checkInt(novIdx.paraCount, 0) self.cCValue.setText(f"{cC:n}") self.wCValue.setText(f"{wC:n}") self.pCValue.setText(f"{pC:n}") - self.synopValue.setText(novIdx["synopsis"]) + self.synopValue.setText(novIdx.synopsis) self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY)) self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY)) diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index fb4d9acd..44adc7ad 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -1,99 +1,125 @@ { -"tagIndex": { - "Bod": [3, "4c4f28287af27", "CHARACTER", "T000001"], - "Main": [3, "2426c6f0ca922", "PLOT", "T000001"], - "Europe": [3, "04468803b92e1", "WORLD", "T000001"] -}, -"refIndex": { - "fb609cd8319dc": { - "T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]] + "tagsIndex": { + "Bod": {"handle": "4c4f28287af27", "heading": "T000001", "class": "CHARACTER"}, + "Main": {"handle": "2426c6f0ca922", "heading": "T000001", "class": "PLOT"}, + "Europe": {"handle": "04468803b92e1", "heading": "T000001", "class": "WORLD"} }, - "88243afbe5ed8": { - "T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]] - }, - "f96ec11c6a3da": { - "T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]] - }, - "441420a886d82": { - "T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]] - }, - "eb103bc70c90c": { - "T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]] - }, - "f8c0562e50f1b": { - "T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]] - }, - "47666c91c7ccf": { - "T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]] - }, - "4c4f28287af27": { - "T000001": [[4, "@plot", "Main"]] + "itemIndex": { + "7a992350f3eb6": { + "level": "H1", + "headings": { + "T000001": {"level": "H1", "title": "Lorem Ipsum", "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""} + } + }, + "8c58a65414c23": { + "level": "H0", + "headings": { + "T000000": {"level": "H0", "title": "", "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""} + } + }, + "88d59a277361b": { + "level": "H2", + "headings": { + "T000001": {"level": "H2", "title": "Prologue", "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."} + } + }, + "db7e733775d4d": { + "level": "H1", + "headings": { + "T000001": {"level": "H1", "title": "Act One", "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""} + } + }, + "fb609cd8319dc": { + "level": "H2", + "headings": { + "T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."} + }, + "references": { + "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + } + }, + "88243afbe5ed8": { + "level": "H0", + "headings": { + "T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."}, + "T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} + }, + "references": { + "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + } + }, + "f96ec11c6a3da": { + "level": "H0", + "headings": { + "T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."}, + "T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} + }, + "references": { + "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + } + }, + "846352075de7d": { + "level": "H2", + "headings": { + "T000001": {"level": "H2", "title": "Why do we use it?", "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""} + } + }, + "441420a886d82": { + "level": "H2", + "headings": { + "T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."} + }, + "references": { + "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + } + }, + "eb103bc70c90c": { + "level": "H3", + "headings": { + "T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."} + }, + "references": { + "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + } + }, + "f8c0562e50f1b": { + "level": "H3", + "headings": { + "T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."} + }, + "references": { + "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + } + }, + "47666c91c7ccf": { + "level": "H3", + "headings": { + "T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."} + }, + "references": { + "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + } + }, + "4c4f28287af27": { + "level": "H1", + "headings": { + "T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""} + }, + "references": { + "T000001": {"Main": ["@plot"]} + } + }, + "2426c6f0ca922": { + "level": "H1", + "headings": { + "T000001": {"level": "H1", "title": "Main Plot", "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""} + } + }, + "04468803b92e1": { + "level": "H1", + "headings": { + "T000001": {"level": "H1", "title": "Ancient Europe", "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""} + } + } } -}, -"fileIndex": { - "7a992350f3eb6": { - "T000001": {"level": "H1", "title": "Lorem Ipsum", "layout": "DOCUMENT", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""} - }, - "8c58a65414c23": { - "T000000": {"level": "H0", "title": "", "layout": "DOCUMENT", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""} - }, - "88d59a277361b": { - "T000001": {"level": "H2", "title": "Prologue", "layout": "DOCUMENT", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."} - }, - "db7e733775d4d": { - "T000001": {"level": "H1", "title": "Act One", "layout": "DOCUMENT", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""} - }, - "fb609cd8319dc": { - "T000001": {"level": "H2", "title": "Chapter One", "layout": "DOCUMENT", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."} - }, - "88243afbe5ed8": { - "T000001": {"level": "H3", "title": "Scene One", "layout": "DOCUMENT", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."}, - "T000013": {"level": "H4", "title": "Scene One, Section Two", "layout": "DOCUMENT", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} - }, - "f96ec11c6a3da": { - "T000001": {"level": "H3", "title": "Scene Two", "layout": "DOCUMENT", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."}, - "T000015": {"level": "H4", "title": "Scene Two, Section Two", "layout": "DOCUMENT", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} - }, - "846352075de7d": { - "T000001": {"level": "H2", "title": "Why do we use it?", "layout": "DOCUMENT", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""} - }, - "441420a886d82": { - "T000001": {"level": "H2", "title": "Chapter Two", "layout": "DOCUMENT", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."} - }, - "eb103bc70c90c": { - "T000001": {"level": "H3", "title": "Scene Three", "layout": "DOCUMENT", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."} - }, - "f8c0562e50f1b": { - "T000001": {"level": "H3", "title": "Scene Four", "layout": "DOCUMENT", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."} - }, - "47666c91c7ccf": { - "T000001": {"level": "H3", "title": "Scene Five", "layout": "DOCUMENT", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."} - }, - "4c4f28287af27": { - "T000001": {"level": "H1", "title": "Nobody Owens", "layout": "NOTE", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""} - }, - "2426c6f0ca922": { - "T000001": {"level": "H1", "title": "Main Plot", "layout": "NOTE", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""} - }, - "04468803b92e1": { - "T000001": {"level": "H1", "title": "Ancient Europe", "layout": "NOTE", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""} - } -}, -"fileMeta": { - "7a992350f3eb6": ["H1", 230, 40, 3], - "8c58a65414c23": ["H0", 1058, 176, 2], - "88d59a277361b": ["H2", 584, 92, 1], - "db7e733775d4d": ["H1", 35, 6, 1], - "fb609cd8319dc": ["H2", 419, 67, 1], - "88243afbe5ed8": ["H3", 2758, 404, 4], - "f96ec11c6a3da": ["H3", 4043, 600, 6], - "846352075de7d": ["H2", 631, 109, 3], - "441420a886d82": ["H2", 477, 70, 1], - "eb103bc70c90c": ["H3", 3006, 439, 4], - "f8c0562e50f1b": ["H3", 3839, 563, 6], - "47666c91c7ccf": ["H3", 3644, 543, 5], - "4c4f28287af27": ["H1", 1864, 284, 3], - "2426c6f0ca922": ["H1", 1369, 195, 2], - "04468803b92e1": ["H1", 1770, 259, 3] -} } diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index d2e76643..eff85fec 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -69,27 +69,19 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): # Take a copy of the index tagIndex = str(theIndex._tags) - refIndex = str(theIndex._refIndex) - fileIndex = str(theIndex._fileIndex) - textCounts = str(theIndex._fileMeta) + itemsIndex = str({handle: item.packData() for handle, item in theIndex._items.items()}) # Delete a handle assert theIndex._tags.get("Bod", None) is not None - assert theIndex._refIndex.get("4c4f28287af27", None) is not None - assert theIndex._fileIndex.get("4c4f28287af27", None) is not None - assert theIndex._fileMeta.get("4c4f28287af27", None) is not None + assert theIndex._items.get("4c4f28287af27", None) is not None theIndex.deleteHandle("4c4f28287af27") assert theIndex._tags.get("Bod", None) is None - assert theIndex._refIndex.get("4c4f28287af27", None) is None - assert theIndex._fileIndex.get("4c4f28287af27", None) is None - assert theIndex._fileMeta.get("4c4f28287af27", None) is None + assert theIndex._items.get("4c4f28287af27", None) is None # Clear the index theIndex.clearIndex() assert theIndex._tags == {} - assert theIndex._refIndex == {} - assert theIndex._fileIndex == {} - assert theIndex._fileMeta == {} + assert theIndex._items == {} # Make the load fail with monkeypatch.context() as mp: @@ -100,9 +92,9 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): assert theIndex.loadIndex() is True assert str(theIndex._tags) == tagIndex - assert str(theIndex._refIndex) == refIndex - assert str(theIndex._fileIndex) == fileIndex - assert str(theIndex._fileMeta) == textCounts + assert str( + {handle: item.packData() for handle, item in theIndex._items.items()} + ) == itemsIndex # Break the index and check that we notice # assert theIndex.indexBroken is False @@ -201,7 +193,7 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI): assert theIndex._tags == { "Jane": {"handle": cHandle, "heading": "T000001", "class": "CHARACTER"} } - assert theIndex.getNovelData(nHandle, "T000001")["title"] == "Hello World!" + assert theIndex.getNovelData(nHandle, "T000001").title == "Hello World!" assert theIndex.getReferences(nHandle, "T000001") == { "@char": [], "@custom": [], @@ -314,7 +306,7 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theIndex._tags == { "Jane": {"handle": cHandle, "heading": "T000001", "class": "CHARACTER"} } - assert theIndex.getNovelData(nHandle, "T000001")["title"] == "Hello World!" + assert theIndex.getNovelData(nHandle, "T000001").title == "Hello World!" # Title Indexing # ============== @@ -336,42 +328,40 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "##### Title Five\n\n" # Not interpreted as a title, the hashes are counted as a word "Paragraph Five.\n\n" )) - assert nHandle not in theIndex._refIndex + assert theIndex._items[nHandle]["T000001"].references == {} + assert theIndex._items[nHandle]["T000007"].references == {} + assert theIndex._items[nHandle]["T000013"].references == {} + assert theIndex._items[nHandle]["T000019"].references == {} - assert theIndex._fileIndex[nHandle]["T000001"]["level"] == "H1" - assert theIndex._fileIndex[nHandle]["T000007"]["level"] == "H2" - assert theIndex._fileIndex[nHandle]["T000013"]["level"] == "H3" - assert theIndex._fileIndex[nHandle]["T000019"]["level"] == "H4" + assert theIndex._items[nHandle]["T000001"].level == "H1" + assert theIndex._items[nHandle]["T000007"].level == "H2" + assert theIndex._items[nHandle]["T000013"].level == "H3" + assert theIndex._items[nHandle]["T000019"].level == "H4" - assert theIndex._fileIndex[nHandle]["T000001"]["title"] == "Title One" - assert theIndex._fileIndex[nHandle]["T000007"]["title"] == "Title Two" - assert theIndex._fileIndex[nHandle]["T000013"]["title"] == "Title Three" - assert theIndex._fileIndex[nHandle]["T000019"]["title"] == "Title Four" + assert theIndex._items[nHandle]["T000001"].title == "Title One" + assert theIndex._items[nHandle]["T000007"].title == "Title Two" + assert theIndex._items[nHandle]["T000013"].title == "Title Three" + assert theIndex._items[nHandle]["T000019"].title == "Title Four" - assert theIndex._fileIndex[nHandle]["T000001"]["layout"] == "DOCUMENT" - assert theIndex._fileIndex[nHandle]["T000007"]["layout"] == "DOCUMENT" - assert theIndex._fileIndex[nHandle]["T000013"]["layout"] == "DOCUMENT" - assert theIndex._fileIndex[nHandle]["T000019"]["layout"] == "DOCUMENT" + assert theIndex._items[nHandle]["T000001"].charCount == 23 + assert theIndex._items[nHandle]["T000007"].charCount == 23 + assert theIndex._items[nHandle]["T000013"].charCount == 27 + assert theIndex._items[nHandle]["T000019"].charCount == 56 - assert theIndex._fileIndex[nHandle]["T000001"]["cCount"] == 23 - assert theIndex._fileIndex[nHandle]["T000007"]["cCount"] == 23 - assert theIndex._fileIndex[nHandle]["T000013"]["cCount"] == 27 - assert theIndex._fileIndex[nHandle]["T000019"]["cCount"] == 56 + assert theIndex._items[nHandle]["T000001"].wordCount == 4 + assert theIndex._items[nHandle]["T000007"].wordCount == 4 + assert theIndex._items[nHandle]["T000013"].wordCount == 4 + assert theIndex._items[nHandle]["T000019"].wordCount == 9 - assert theIndex._fileIndex[nHandle]["T000001"]["wCount"] == 4 - assert theIndex._fileIndex[nHandle]["T000007"]["wCount"] == 4 - assert theIndex._fileIndex[nHandle]["T000013"]["wCount"] == 4 - assert theIndex._fileIndex[nHandle]["T000019"]["wCount"] == 9 + assert theIndex._items[nHandle]["T000001"].paraCount == 1 + assert theIndex._items[nHandle]["T000007"].paraCount == 1 + assert theIndex._items[nHandle]["T000013"].paraCount == 1 + assert theIndex._items[nHandle]["T000019"].paraCount == 3 - assert theIndex._fileIndex[nHandle]["T000001"]["pCount"] == 1 - assert theIndex._fileIndex[nHandle]["T000007"]["pCount"] == 1 - assert theIndex._fileIndex[nHandle]["T000013"]["pCount"] == 1 - assert theIndex._fileIndex[nHandle]["T000019"]["pCount"] == 3 - - assert theIndex._fileIndex[nHandle]["T000001"]["synopsis"] == "Synopsis One." - assert theIndex._fileIndex[nHandle]["T000007"]["synopsis"] == "Synopsis Two." - assert theIndex._fileIndex[nHandle]["T000013"]["synopsis"] == "Synopsis Three." - assert theIndex._fileIndex[nHandle]["T000019"]["synopsis"] == "Synopsis Four." + assert theIndex._items[nHandle]["T000001"].synopsis == "Synopsis One." + assert theIndex._items[nHandle]["T000007"].synopsis == "Synopsis Two." + assert theIndex._items[nHandle]["T000013"].synopsis == "Synopsis Three." + assert theIndex._items[nHandle]["T000019"].synopsis == "Synopsis Four." # Note File assert theIndex.scanText(cHandle, ( @@ -380,15 +370,13 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "% synopsis: Synopsis One.\n\n" "Paragraph One.\n\n" )) - assert cHandle not in theIndex._refIndex - - assert theIndex._fileIndex[cHandle]["T000001"]["level"] == "H1" - assert theIndex._fileIndex[cHandle]["T000001"]["title"] == "Title One" - assert theIndex._fileIndex[cHandle]["T000001"]["layout"] == "NOTE" - assert theIndex._fileIndex[cHandle]["T000001"]["cCount"] == 23 - assert theIndex._fileIndex[cHandle]["T000001"]["wCount"] == 4 - assert theIndex._fileIndex[cHandle]["T000001"]["pCount"] == 1 - assert theIndex._fileIndex[cHandle]["T000001"]["synopsis"] == "Synopsis One." + assert theIndex._items[cHandle]["T000001"].references == {} + assert theIndex._items[cHandle]["T000001"].level == "H1" + assert theIndex._items[cHandle]["T000001"].title == "Title One" + assert theIndex._items[cHandle]["T000001"].charCount == 23 + assert theIndex._items[cHandle]["T000001"].wordCount == 4 + assert theIndex._items[cHandle]["T000001"].paraCount == 1 + assert theIndex._items[cHandle]["T000001"].synopsis == "Synopsis One." # Valid and Invalid References assert theIndex.scanText(sHandle, ( @@ -399,9 +387,9 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "% synopsis: Synopsis One.\n\n" "Paragraph One.\n\n" )) - assert theIndex._refIndex[sHandle]["T000001"] == ( - [[3, "@pov", "One"], [5, "@char", "Two"]] - ) + assert theIndex._items[sHandle]["T000001"].references == { + "One": {"@pov"}, "Two": {"@char"} + } # Special Titles # ============== @@ -410,29 +398,25 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "#! My Project\n\n" ">> By Jane Doe <<\n\n" )) - assert tHandle not in theIndex._refIndex - - assert theIndex._fileIndex[tHandle]["T000001"]["level"] == "H1" - assert theIndex._fileIndex[tHandle]["T000001"]["title"] == "My Project" - assert theIndex._fileIndex[tHandle]["T000001"]["layout"] == "DOCUMENT" - assert theIndex._fileIndex[tHandle]["T000001"]["cCount"] == 21 - assert theIndex._fileIndex[tHandle]["T000001"]["wCount"] == 5 - assert theIndex._fileIndex[tHandle]["T000001"]["pCount"] == 1 - assert theIndex._fileIndex[tHandle]["T000001"]["synopsis"] == "" + assert theIndex._items[cHandle]["T000001"].references == {} + assert theIndex._items[tHandle]["T000001"].level == "H1" + assert theIndex._items[tHandle]["T000001"].title == "My Project" + assert theIndex._items[tHandle]["T000001"].charCount == 21 + assert theIndex._items[tHandle]["T000001"].wordCount == 5 + assert theIndex._items[tHandle]["T000001"].paraCount == 1 + assert theIndex._items[tHandle]["T000001"].synopsis == "" assert theIndex.scanText(tHandle, ( "##! Prologue\n\n" "In the beginning there was time ...\n\n" )) - assert tHandle not in theIndex._refIndex - - assert theIndex._fileIndex[tHandle]["T000001"]["level"] == "H2" - assert theIndex._fileIndex[tHandle]["T000001"]["title"] == "Prologue" - assert theIndex._fileIndex[tHandle]["T000001"]["layout"] == "DOCUMENT" - assert theIndex._fileIndex[tHandle]["T000001"]["cCount"] == 43 - assert theIndex._fileIndex[tHandle]["T000001"]["wCount"] == 8 - assert theIndex._fileIndex[tHandle]["T000001"]["pCount"] == 1 - assert theIndex._fileIndex[tHandle]["T000001"]["synopsis"] == "" + assert theIndex._items[cHandle]["T000001"].references == {} + assert theIndex._items[tHandle]["T000001"].level == "H2" + assert theIndex._items[tHandle]["T000001"].title == "Prologue" + assert theIndex._items[tHandle]["T000001"].charCount == 43 + assert theIndex._items[tHandle]["T000001"].wordCount == 8 + assert theIndex._items[tHandle]["T000001"].paraCount == 1 + assert theIndex._items[tHandle]["T000001"].synopsis == "" # Page wo/Title # ============= @@ -441,27 +425,25 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theIndex.scanText(pHandle, ( "This is a page with some text on it.\n\n" )) - assert pHandle in theIndex._fileIndex - assert theIndex._fileIndex[pHandle]["T000000"]["level"] == "H0" - assert theIndex._fileIndex[pHandle]["T000000"]["title"] == "" - assert theIndex._fileIndex[pHandle]["T000000"]["layout"] == "DOCUMENT" - assert theIndex._fileIndex[pHandle]["T000000"]["cCount"] == 36 - assert theIndex._fileIndex[pHandle]["T000000"]["wCount"] == 9 - assert theIndex._fileIndex[pHandle]["T000000"]["pCount"] == 1 - assert theIndex._fileIndex[pHandle]["T000000"]["synopsis"] == "" + assert theIndex._items[pHandle]["T000000"].references == {} + assert theIndex._items[pHandle]["T000000"].level == "H0" + assert theIndex._items[pHandle]["T000000"].title == "" + assert theIndex._items[pHandle]["T000000"].charCount == 36 + assert theIndex._items[pHandle]["T000000"].wordCount == 9 + assert theIndex._items[pHandle]["T000000"].paraCount == 1 + assert theIndex._items[pHandle]["T000000"].synopsis == "" theProject.tree[pHandle]._layout = nwItemLayout.NOTE assert theIndex.scanText(pHandle, ( "This is a page with some text on it.\n\n" )) - assert pHandle in theIndex._fileIndex - assert theIndex._fileIndex[pHandle]["T000000"]["level"] == "H0" - assert theIndex._fileIndex[pHandle]["T000000"]["title"] == "" - assert theIndex._fileIndex[pHandle]["T000000"]["layout"] == "NOTE" - assert theIndex._fileIndex[pHandle]["T000000"]["cCount"] == 36 - assert theIndex._fileIndex[pHandle]["T000000"]["wCount"] == 9 - assert theIndex._fileIndex[pHandle]["T000000"]["pCount"] == 1 - assert theIndex._fileIndex[pHandle]["T000000"]["synopsis"] == "" + assert theIndex._items[pHandle]["T000000"].references == {} + assert theIndex._items[pHandle]["T000000"].level == "H0" + assert theIndex._items[pHandle]["T000000"].title == "" + assert theIndex._items[pHandle]["T000000"].charCount == 36 + assert theIndex._items[pHandle]["T000000"].wordCount == 9 + assert theIndex._items[pHandle]["T000000"].paraCount == 1 + assert theIndex._items[pHandle]["T000000"].synopsis == "" assert theProject.closeProject() is True @@ -700,538 +682,6 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # END Test testCoreIndex_ExtractData -# @pytest.mark.core -# def testCoreIndex_CheckTagIndex(mockGUI): -# """Test the tag index checker. -# """ -# theProject = NWProject(mockGUI) -# theIndex = NWIndex(theProject) - -# # Valid Index -# theIndex._tagIndex = { -# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], -# "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001"], -# } -# assert theIndex._checkTagIndex() is None - -# # Wrong Key Type -# theIndex._tagIndex = { -# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], -# 123456: [3, "bb2c23b3c42cc", "CHARACTER", "T000001"], -# } -# with pytest.raises(KeyError): -# theIndex._checkTagIndex() - -# # Wrong Length -# theIndex._tagIndex = { -# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], -# "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001", "Stuff"], -# } -# with pytest.raises(IndexError): -# theIndex._checkTagIndex() - -# # Wrong Type of Entry 0 -# theIndex._tagIndex = { -# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], -# "Jane": ["3", "bb2c23b3c42cc", "CHARACTER", "T000001"], -# } -# with pytest.raises(ValueError): -# theIndex._checkTagIndex() - -# # Wrong Type of Entry 1 -# theIndex._tagIndex = { -# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], -# "Jane": [3, 0xbb2c23b3c42cc, "CHARACTER", "T000001"], -# } -# with pytest.raises(ValueError): -# theIndex._checkTagIndex() - -# # Wrong Type of Entry 2 -# theIndex._tagIndex = { -# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], -# "Jane": [3, "bb2c23b3c42cc", "INVALID_CLASS", "T000001"], -# } -# with pytest.raises(ValueError): -# theIndex._checkTagIndex() - -# # Wrong Type of Entry 3 -# theIndex._tagIndex = { -# "John": [3, "14298de4d9524", "CHARACTER", "T000001"], -# "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "INVALID"], -# } -# with pytest.raises(ValueError): -# theIndex._checkTagIndex() - -# # END Test testCoreIndex_CheckTagIndex - - -@pytest.mark.core -def testCoreIndex_CheckRefIndex(mockGUI): - """Test the reference index checker. - """ - theProject = NWProject(mockGUI) - theIndex = NWIndex(theProject) - - # Valid Index - theIndex._refIndex = { - "6a2d6d5f4f401": { - "T000000": [], - "T000001": [[3, "@pov", "Jane"], [4, "@location", "Earth"]], - } - } - assert theIndex._checkRefIndex() is None - - # Invalid Handle - theIndex._refIndex = { - "Ha2d6d5f4f401": { - "T000000": [], - "T000001": [[3, "@pov", "Jane"], [4, "@location", "Earth"]], - } - } - with pytest.raises(KeyError): - theIndex._checkRefIndex() - - # Invalid Title - theIndex._refIndex = { - "6a2d6d5f4f401": { - "T000000": [], - "INVALID": [[3, "@pov", "Jane"], [4, "@location", "Earth"]], - } - } - with pytest.raises(KeyError): - theIndex._checkRefIndex() - - # Wrong Length - theIndex._refIndex = { - "6a2d6d5f4f401": { - "T000000": [], - "T000001": [[3, "@pov", "Jane"], [4, "@location", "Earth", "Stuff"]], - } - } - with pytest.raises(IndexError): - theIndex._checkRefIndex() - - # Wrong Type of Entry 0 - theIndex._refIndex = { - "6a2d6d5f4f401": { - "T000000": [], - "T000001": [[3, "@pov", "Jane"], ["4", "@location", "Earth"]], - } - } - with pytest.raises(ValueError): - theIndex._checkRefIndex() - - # Wrong Type of Entry 1 - theIndex._refIndex = { - "6a2d6d5f4f401": { - "T000000": [], - "T000001": [[3, "@pov", "Jane"], [4, "@stuff", "Earth"]], - } - } - with pytest.raises(ValueError): - theIndex._checkRefIndex() - - # Wrong Type of Entry 2 - theIndex._refIndex = { - "6a2d6d5f4f401": { - "T000000": [], - "T000001": [[3, "@pov", "Jane"], [4, "@location", 123456]], - } - } - with pytest.raises(ValueError): - theIndex._checkRefIndex() - -# END Test testCoreIndex_CheckRefIndex - - -@pytest.mark.core -def testCoreIndex_CheckFileIndex(mockGUI): - """Test the file index checker. - """ - theProject = NWProject(mockGUI) - theIndex = NWIndex(theProject) - - # Valid Index - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - theIndex._fileIndex = theIndex._fileIndex.copy() - assert theIndex._checkFileIndex() is None - - # Invalid Handle - theIndex._fileIndex = { - "H3b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(KeyError): - theIndex._checkFileIndex() - - # Invalid Title - theIndex._fileIndex = { - "53b69b83cdafc": { - "INVALID": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(KeyError): - theIndex._checkFileIndex() - - # Wrong Length - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - "stuff": None - } - } - } - with pytest.raises(IndexError): - theIndex._checkFileIndex() - - # Missing Keys - # ============ - - # Missing 'level' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "stuff": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(KeyError): - theIndex._checkFileIndex() - - # Missing 'title' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "stuff": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(KeyError): - theIndex._checkFileIndex() - - # Missing 'layout' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "stuff": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(KeyError): - theIndex._checkFileIndex() - - # Missing 'cCount' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "stuff": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(KeyError): - theIndex._checkFileIndex() - - # Missing 'wCount' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "stuff": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(KeyError): - theIndex._checkFileIndex() - - # Missing 'pCount' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "stuff": 2, - "synopsis": "text", - } - } - } - with pytest.raises(KeyError): - theIndex._checkFileIndex() - - # Missing 'synopsis' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "stuff": "text", - } - } - } - with pytest.raises(KeyError): - theIndex._checkFileIndex() - - # Wrong Types - # =========== - - # Wrong Type for 'level' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "XX", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(ValueError): - theIndex._checkFileIndex() - - # Wrong Type for 'title' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": 12345678, - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(ValueError): - theIndex._checkFileIndex() - - # Wrong Type for 'layout' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "INVALID", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(ValueError): - theIndex._checkFileIndex() - - # Wrong Type for 'cCount' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": "72", - "wCount": 15, - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(ValueError): - theIndex._checkFileIndex() - - # Wrong Type for 'wCount' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": "15", - "pCount": 2, - "synopsis": "text", - } - } - } - with pytest.raises(ValueError): - theIndex._checkFileIndex() - - # Wrong Type for 'pCount' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": "2", - "synopsis": "text", - } - } - } - with pytest.raises(ValueError): - theIndex._checkFileIndex() - - # Wrong Type for 'synopsis' - theIndex._fileIndex = { - "53b69b83cdafc": { - "T000001": { - "level": "H1", - "title": "My Novel", - "layout": "DOCUMENT", - "cCount": 72, - "wCount": 15, - "pCount": 2, - "synopsis": 123456, - } - } - } - with pytest.raises(ValueError): - theIndex._checkFileIndex() - -# END Test testCoreIndex_CheckFileIndex - - -@pytest.mark.core -def testCoreIndex_CheckFileMeta(mockGUI): - """Test the file meta checker. - """ - theProject = NWProject(mockGUI) - theIndex = NWIndex(theProject) - - # Valid Index - theIndex._fileMeta = { - "53b69b83cdafc": ["H0", 72, 15, 2], - "974e400180a99": ["H0", 210, 40, 2], - } - assert theIndex._checkFileMeta() is None - - # Invalid Handle - theIndex._fileMeta = { - "53b69b83cdafc": ["H0", 72, 15, 2], - "h74e400180a99": ["H0", 210, 40, 2], - } - with pytest.raises(KeyError): - theIndex._checkFileMeta() - - # Wrong Length - theIndex._fileMeta = { - "53b69b83cdafc": ["H0", 72, 15, 2], - "974e400180a99": ["H0", 210, 40, 2, 8], - } - with pytest.raises(IndexError): - theIndex._checkFileMeta() - - # Content of Entry 0 - theIndex._fileMeta = { - "53b69b83cdafc": ["H0", 72, 15, 2], - "974e400180a99": ["XXX", 210, 40, 2], - } - with pytest.raises(ValueError): - theIndex._checkFileMeta() - - # Type of Entry 1 - theIndex._fileMeta = { - "53b69b83cdafc": ["H0", 72, 15, 2], - "974e400180a99": ["H0", "210", 40, 2], - } - with pytest.raises(ValueError): - theIndex._checkFileMeta() - - # Type of Entry 2 - theIndex._fileMeta = { - "53b69b83cdafc": ["H0", 72, 15, 2], - "974e400180a99": ["H0", 210, "40", 2], - } - with pytest.raises(ValueError): - theIndex._checkFileMeta() - - # Type of Entry 3 - theIndex._fileMeta = { - "53b69b83cdafc": ["H0", 72, 15, 2], - "974e400180a99": ["H0", 210, 40, "2"], - } - with pytest.raises(ValueError): - theIndex._checkFileMeta() - -# END Test testCoreIndex_CheckFileMeta - - @pytest.mark.core def testCoreIndex_CountWords(): """Test the word counter and the exclusion filers. diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index d3f245d1..0ed15742 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -47,8 +47,8 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): # Rebuild the index nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger) - assert nwGUI.theProject.index._tagIndex != {} - assert nwGUI.theProject.index._refIndex != {} + assert nwGUI.theProject.index._tags != {} + assert nwGUI.theProject.index._items != {} # Select a document in the project tree nwGUI.treeView.setSelectedHandle("88243afbe5ed8") From 347d34bf8342a2ffb57f2cb063cfab68bd4acdb3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 29 May 2022 00:18:48 +0200 Subject: [PATCH 107/179] Clean up the index code a bit and add index validation --- novelwriter/core/index.py | 154 +++++++++++------- novelwriter/core/project.py | 6 +- .../coreIndex_LoadSave_tagsIndex.json | 4 +- 3 files changed, 97 insertions(+), 67 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 95485bf9..d350f9fd 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -34,7 +34,9 @@ from novelwriter.enum import nwItemType, nwItemLayout from novelwriter.error import logException from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode from novelwriter.core.document import NWDoc -from novelwriter.common import checkInt, jsonEncode +from novelwriter.common import ( + checkInt, isHandle, isItemClass, isTitleTag, jsonEncode +) logger = logging.getLogger(__name__) @@ -74,13 +76,11 @@ class NWIndex(): def clearIndex(self): """Clear the index dictionaries and time stamps. """ + self._tags = {} + self._items = {} self._timeNovel = 0 self._timeNotes = 0 self._timeIndex = 0 - - self._tags = {} - self._items = {} - return def deleteHandle(self, tHandle): @@ -90,10 +90,8 @@ class NWIndex(): return logger.debug("Removing item '%s' from the index", tHandle) - for tTag in self._items[tHandle].allTags(): self._tags.pop(tTag, None) - self._items.pop(tHandle, None) return @@ -144,30 +142,36 @@ class NWIndex(): try: with open(indexFile, mode="r", encoding="utf-8") as inFile: theData = json.load(inFile) - except Exception: logger.error("Failed to load index file") logException() self._indexBroken = True return False - self._tags = theData.get("tagsIndex", {}) - for tHandle, tData in theData.get("itemIndex", {}).items(): - nwItem = self.theProject.tree[tHandle] - if nwItem is not None: - tItem = IndexItem(tHandle, nwItem) - tItem.unpackData(tData) - self._items[tHandle] = tItem + try: + self._validateTagsIndex(theData["tagsIndex"]) + self._validateItemIndex(theData["itemIndex"]) + except Exception: + logger.error("The index content is invalid") + logException() + self._indexBroken = True + return False - nowTime = round(time()) - self._timeNovel = nowTime - self._timeNotes = nowTime - self._timeIndex = nowTime + logger.debug("Checking index") + + # Check that all files are indexed + for fHandle in self.theProject.projFiles: + if fHandle not in self._items: + logger.warning("Item '%s' is not in the index", fHandle) + self.reIndexHandle(fHandle) + + nowTime = round(time()) + self._timeNovel = nowTime + self._timeNotes = nowTime + self._timeIndex = nowTime logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) - self._checkIndex() - return True def saveIndex(self): @@ -214,11 +218,11 @@ class NWIndex(): logger.info("Not indexing non-file item '%s'", tHandle) return False + # Delete the old entry and create a new self.deleteHandle(tHandle) - - # Run word counter for the whole text self._items[tHandle] = IndexItem(tHandle, theItem) + # Run word counter for the whole text cC, wC, pC = countWords(theText) theItem.setCharCount(cC) theItem.setWordCount(wC) @@ -249,7 +253,7 @@ class NWIndex(): continue if aLine.startswith("#"): - isTitle = self._indexTitle(tHandle, aLine, nLine, itemLayout) + isTitle = self._indexTitle(tHandle, aLine, nLine) if isTitle and nLine > 0: if nTitle > 0: lastText = "\n".join(theLines[nTitle-1:nLine-1]) @@ -257,7 +261,7 @@ class NWIndex(): nTitle = nLine elif aLine.startswith("@"): - self._indexKeyword(tHandle, aLine, nLine, nTitle, itemClass) + self._indexKeyword(tHandle, aLine, nTitle, itemClass) elif aLine.startswith("%"): if nTitle > 0: @@ -274,7 +278,7 @@ class NWIndex(): lastText = "\n".join(theLines[nTitle-1:]) self._indexWordCounts(tHandle, lastText, nTitle) - # Index page with no titles and references + # Also count words on a page with no titles if nTitle == 0: self._indexWordCounts(tHandle, theText, nTitle) @@ -292,7 +296,7 @@ class NWIndex(): # Internal Indexers ## - def _indexTitle(self, tHandle, aLine, nLine, itemLayout): + def _indexTitle(self, tHandle, aLine, nLine): """Save information about the title and its location in the file to the index. """ @@ -341,7 +345,7 @@ class NWIndex(): self._items[tHandle].setHeadingSynopsis(sTitle, theText) return - def _indexKeyword(self, tHandle, aLine, nLine, nTitle, itemClass): + def _indexKeyword(self, tHandle, aLine, nTitle, itemClass): """Validate and save the information about a reference to a tag in another file. """ @@ -361,11 +365,9 @@ class NWIndex(): "heading": sTitle, "class": itemClass.name, } - if tHandle in self._items: - self._items[tHandle].setHeadingTag(sTitle, theBits[1]) + self._items[tHandle].setHeadingTag(sTitle, theBits[1]) else: - if tHandle in self._items: - self._items[tHandle].addHeadingReferences(sTitle, theBits[1:], theBits[0]) + self._items[tHandle].addHeadingReferences(sTitle, theBits[1:], theBits[0]) return @@ -627,26 +629,51 @@ class NWIndex(): return theHandles - ## - # Index Checkers - ## - - def _checkIndex(self): - """Check that the entries in the index are valid and contain the - elements it should. Also check that each file present in the - contents folder when the project was loaded are also present in - the fileMeta index. + def _validateTagsIndex(self, tagsIndex): + """Iterate through the tagsIndex loaded from cache and check + that it's valid. """ - logger.debug("Checking index") - tStart = time() + self._tags = {} + if not isinstance(tagsIndex, dict): + raise ValueError("tagsIndex is not a dict") - # If the index was ok, we check that project files are indexed - for fHandle in self.theProject.projFiles: - if fHandle not in self._items: - logger.warning("Item '%s' is not in the index", fHandle) - self.reIndexHandle(fHandle) + for tagKey, tagData in tagsIndex.items(): + if not isinstance(tagKey, str): + raise ValueError("tagsIndex keys must be a strings") + if "handle" not in tagData: + raise KeyError("A tagIndex item is missing a handle entry") + if "heading" not in tagData: + raise KeyError("A tagIndex item is missing a heading entry") + if "class" not in tagData: + raise KeyError("A tagIndex item is missing a class entry") + if not isHandle(tagData["handle"]): + raise ValueError("tagsIndex handle must be a handle") + if not isTitleTag(tagData["heading"]): + raise ValueError("tagsIndex heading must be a title tag") + if not isItemClass(tagData["class"]): + raise ValueError("tagsIndex handle must be an nwItemClass") - logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000) + self._tags = tagsIndex + + return + + def _validateItemIndex(self, itemIndex): + """Iterate through the itemIndex loaded from cache and check + that it's valid. + """ + self._items = {} + if not isinstance(itemIndex, dict): + raise ValueError("itemIndex is not a dict") + + for tHandle, tData in itemIndex.items(): + if not isHandle(tHandle): + raise ValueError("itemIndex keys must be handles") + + nwItem = self.theProject.tree[tHandle] + if nwItem is not None: + tItem = IndexItem(tHandle, nwItem) + tItem.unpackData(tData) + self._items[tHandle] = tItem return @@ -734,6 +761,10 @@ def countWords(theText): return charCount, wordCount, paraCount +# =============================================================================================== # +# Indexer Objects +# =============================================================================================== # + class IndexItem: def __init__(self, tHandle, tItem): @@ -773,20 +804,11 @@ class IndexItem: # Setters ## - def setLevel(self, level): - if level in H_VALID: - self._level = level - else: - self._level = "H0" - return - def updateLevel(self, level): """Set the level only if it is H0. """ - if level in H_VALID and self._level == "H0": + if self._level == "H0": self._level = level - else: - self._level = "H0" return def addHeading(self, tHeading): @@ -867,6 +889,8 @@ class IndexItem: self._level = data.get("level", "H0") references = data.get("references", {}) for sTitle, hData in data.get("headings", {}).items(): + if not isTitleTag(sTitle): + raise ValueError("The itemIndex contains an invalid title key") tHeading = IndexHeading(sTitle) tHeading.unpackData(hData) tHeading.unpackReferences(references.get(sTitle, {})) @@ -940,8 +964,6 @@ class IndexHeading: def setLevel(self, level): if level in H_VALID: self._level = level - else: - self._level = "H0" return def setCounts(self, charCount, wordCount, paraCount): @@ -1007,7 +1029,15 @@ class IndexHeading: """Unpack a set of references from a dictionary. """ for tagKey, refTypes in data.items(): - self._refs[tagKey] = set(refTypes) + if not isinstance(tagKey, str): + raise ValueError("itemIndex reference key must be a string") + if not isinstance(refTypes, list): + raise ValueError("itemIndex reference types must be a list") + for refType in refTypes: + if refType in nwKeyWords.VALID_KEYS: + self.addReference(tagKey, refType) + else: + raise ValueError("The itemIndex contains an invalid reference type") return # END Class IndexHeading diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 0a13cb16..460fb91f 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -123,15 +123,15 @@ class NWProject(): ## @property - def index(self) -> NWIndex: + def index(self): return self._projIndex @property - def tree(self) -> NWTree: + def tree(self): return self._projTree @property - def options(self) -> OptionState: + def options(self): return self._optState ## diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index 44adc7ad..fafdeb68 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -39,7 +39,7 @@ } }, "88243afbe5ed8": { - "level": "H0", + "level": "H3", "headings": { "T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."}, "T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} @@ -49,7 +49,7 @@ } }, "f96ec11c6a3da": { - "level": "H0", + "level": "H3", "headings": { "T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."}, "T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} From 77e50a6cee6216de4d528db5e777dc0cf5b74f4a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 29 May 2022 18:58:32 +0200 Subject: [PATCH 108/179] Move the item index into a wrapper class and combine the access functions --- novelwriter/core/index.py | 562 +++++++++++++++++---------- novelwriter/gui/noveltree.py | 4 +- novelwriter/gui/outline.py | 2 +- tests/test_core/test_core_index.py | 167 ++++---- tests/test_gui/test_gui_docviewer.py | 2 +- 5 files changed, 440 insertions(+), 297 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index d350f9fd..dcd38a1d 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -4,8 +4,9 @@ novelWriter – Project Index Data class for the project index of tags, headers and references File History: -Created: 2019-04-22 [0.0.1] countWords -Created: 2019-05-27 [0.1.4] NWIndex +Created: 2019-04-22 [0.0.1] countWords +Created: 2019-05-27 [0.1.4] NWIndex +Created: 2022-05-28 [1.7rc1] IndexItem, IndexHeading This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -56,7 +57,7 @@ class NWIndex(): # Indices self._tags = {} - self._items = {} + self._itemIndex = ItemIndex(theProject) # TimeStamps self._timeNovel = 0 @@ -65,6 +66,10 @@ class NWIndex(): return + ## + # Properties + ## + @property def indexBroken(self): return self._indexBroken @@ -77,7 +82,7 @@ class NWIndex(): """Clear the index dictionaries and time stamps. """ self._tags = {} - self._items = {} + self._itemIndex.clear() self._timeNovel = 0 self._timeNotes = 0 self._timeIndex = 0 @@ -86,13 +91,11 @@ class NWIndex(): def deleteHandle(self, tHandle): """Delete all entries of a given document handle. """ - if tHandle not in self._items: - return - logger.debug("Removing item '%s' from the index", tHandle) - for tTag in self._items[tHandle].allTags(): + for tTag in self._itemIndex.allItemTags(tHandle): self._tags.pop(tTag, None) - self._items.pop(tHandle, None) + + del self._itemIndex[tHandle] return @@ -150,7 +153,7 @@ class NWIndex(): try: self._validateTagsIndex(theData["tagsIndex"]) - self._validateItemIndex(theData["itemIndex"]) + self._itemIndex.unpackData(theData["itemIndex"]) except Exception: logger.error("The index content is invalid") logException() @@ -161,7 +164,7 @@ class NWIndex(): # Check that all files are indexed for fHandle in self.theProject.projFiles: - if fHandle not in self._items: + if fHandle not in self._itemIndex: logger.warning("Item '%s' is not in the index", fHandle) self.reIndexHandle(fHandle) @@ -183,11 +186,11 @@ class NWIndex(): tStart = time() try: - itemsIndex = {handle: item.packData() for handle, item in self._items.items()} + itemIndex = self._itemIndex.packData() with open(indexFile, mode="w+", encoding="utf-8") as outFile: outFile.write("{\n") outFile.write(f' "tagsIndex": {jsonEncode(self._tags, n=1, nmax=2)},\n') - outFile.write(f' "itemIndex": {jsonEncode(itemsIndex, n=1, nmax=4)}\n') + outFile.write(f' "itemIndex": {jsonEncode(itemIndex, n=1, nmax=4)}\n') outFile.write("}\n") except Exception: @@ -220,7 +223,7 @@ class NWIndex(): # Delete the old entry and create a new self.deleteHandle(tHandle) - self._items[tHandle] = IndexItem(tHandle, theItem) + self._itemIndex.add(tHandle, theItem) # Run word counter for the whole text cC, wC, pC = countWords(theText) @@ -240,9 +243,6 @@ class NWIndex(): logger.debug("Not indexing inactive item '%s'", tHandle) return False - itemClass = theItem.itemClass - itemLayout = theItem.itemLayout - logger.debug("Indexing item with handle '%s'", tHandle) # Scan the text content @@ -261,7 +261,7 @@ class NWIndex(): nTitle = nLine elif aLine.startswith("@"): - self._indexKeyword(tHandle, aLine, nTitle, itemClass) + self._indexKeyword(tHandle, aLine, nTitle, theItem.itemClass) elif aLine.startswith("%"): if nTitle > 0: @@ -285,7 +285,7 @@ class NWIndex(): # Update timestamps for index changes nowTime = round(time()) self._timeIndex = nowTime - if itemLayout == nwItemLayout.NOTE: + if theItem.itemLayout == nwItemLayout.NOTE: self._timeNotes = nowTime else: self._timeNovel = nowTime @@ -296,7 +296,7 @@ class NWIndex(): # Internal Indexers ## - def _indexTitle(self, tHandle, aLine, nLine): + def _indexTitle(self, tHandle, aLine, nTitle): """Save information about the title and its location in the file to the index. """ @@ -321,28 +321,24 @@ class NWIndex(): else: return False - sTitle = f"T{nLine:06d}" - tItem = self._items[tHandle] - tItem.updateLevel(hDepth) - tItem.addHeading(IndexHeading(sTitle, hDepth, hText)) + sTitle = f"T{nTitle:06d}" + self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText) return True def _indexWordCounts(self, tHandle, theText, nTitle): """Count text stats and save the counts to the index. """ - cC, wC, pC = countWords(theText) sTitle = f"T{nTitle:06d}" - if tHandle in self._items: - self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC) + cC, wC, pC = countWords(theText) + self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC) return def _indexSynopsis(self, tHandle, theText, nTitle): """Save the synopsis to the index. """ sTitle = f"T{nTitle:06d}" - if tHandle in self._items: - self._items[tHandle].setHeadingSynopsis(sTitle, theText) + self._itemIndex.setHeadingSynopsis(tHandle, sTitle, theText) return def _indexKeyword(self, tHandle, aLine, nTitle, itemClass): @@ -365,9 +361,9 @@ class NWIndex(): "heading": sTitle, "class": itemClass.name, } - self._items[tHandle].setHeadingTag(sTitle, theBits[1]) + self._itemIndex.setHeadingTag(tHandle, sTitle, theBits[1]) else: - self._items[tHandle].addHeadingReferences(sTitle, theBits[1:], theBits[0]) + self._itemIndex.addHeadingReferences(tHandle, sTitle, theBits[1:], theBits[0]) return @@ -447,35 +443,31 @@ class NWIndex(): # Extract Data ## - def novelStructure(self, skipExcluded=True): + def novelStructure(self, skipExcl=True): """Iterate over all titles in the novel, in the correct order as they appear in the tree view and in the respective document files, but skipping all note files. """ - for tHandle in self._listNovelHandles(skipExcluded): - for sTitle in self._items[tHandle].headings: - tKey = f"{tHandle}:{sTitle}" - yield tKey, tHandle, sTitle, self._items[tHandle][sTitle] + for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): + tKey = f"{tHandle}:{sTitle}" + yield tKey, tHandle, sTitle, hItem + return - def getNovelWordCount(self, skipExcluded=True): + def getNovelWordCount(self, skipExcl=True): """Count the number of words in the novel project. """ wCount = 0 - for tHandle in self._listNovelHandles(skipExcluded): - for hItem in self._items[tHandle].entries: - wCount += hItem.wordCount - + for _, _, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): + wCount += hItem.wordCount return wCount - def getNovelTitleCounts(self, skipExcluded=True): + def getNovelTitleCounts(self, skipExcl=True): """Count the number of titles in the novel project. """ hCount = [0, 0, 0, 0, 0] - for tHandle in self._listNovelHandles(skipExcluded): - for hItem in self._items[tHandle].entries: - iLevel = H_LEVEL.get(hItem.level, 0) - hCount[iLevel] += 1 - + for _, _, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): + iLevel = H_LEVEL.get(hItem.level, 0) + hCount[iLevel] += 1 return hCount def getHandleWordCounts(self, tHandle): @@ -483,7 +475,7 @@ class NWIndex(): """ return [ (f"{tHandle}:{sTitle}", hItem.wordCount) - for sTitle, hItem in self._items.get(tHandle, {}).items() + for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle) ] def getHandleHeaders(self, tHandle): @@ -491,39 +483,34 @@ class NWIndex(): """ return [ (sTitle, hItem.level, hItem.title) - for sTitle, hItem in self._items.get(tHandle, {}).items() + for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle) ] def getHandleHeaderLevel(self, tHandle): """Get the header level of the first header of a handle. """ - if tHandle in self._items: - return self._items[tHandle].level - else: - return "H0" + return self._itemIndex.mainItemHeader(tHandle) - def getTableOfContents(self, maxDepth, skipExcluded=True): + def getTableOfContents(self, maxDepth, skipExcl=True): """Generate a table of contents up to a maximum depth. """ tOrder = [] tData = {} pKey = None - for tHandle in self._listNovelHandles(skipExcluded): - for sTitle in self._items[tHandle].headings: - tKey = f"{tHandle}:{sTitle}" - hItem = self._items[tHandle][sTitle] - iLevel = H_LEVEL.get(hItem.level, 0) - if iLevel > maxDepth: - if pKey in tData: - tData[pKey]["words"] += hItem.wordCount - else: - pKey = tKey - tOrder.append(tKey) - tData[tKey] = { - "level": iLevel, - "title": hItem.title, - "words": hItem.wordCount, - } + for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): + tKey = f"{tHandle}:{sTitle}" + iLevel = H_LEVEL.get(hItem.level, 0) + if iLevel > maxDepth: + if pKey in tData: + tData[pKey]["words"] += hItem.wordCount + else: + pKey = tKey + tOrder.append(tKey) + tData[tKey] = { + "level": iLevel, + "title": hItem.title, + "words": hItem.wordCount, + } theToC = [( tKey, @@ -538,35 +525,26 @@ class NWIndex(): """Return the counts for a file, or a section of a file, starting at title sTitle if it is provided. """ - cC = 0 - wC = 0 - pC = 0 + tItem = self._itemIndex[tHandle] + if tItem is None: + return 0, 0, 0 if sTitle is None: - if tHandle in self._items: - tItem = self._items[tHandle].item - cC = tItem.charCount - wC = tItem.wordCount - pC = tItem.paraCount + cItem = tItem.item else: - if tHandle in self._items: - if sTitle in self._items[tHandle]: - hItem = self._items[tHandle][sTitle] - cC = hItem.charCount - wC = hItem.wordCount - pC = hItem.paraCount + cItem = tItem[sTitle] - return cC, wC, pC + if cItem is not None: + return cItem.charCount, cItem.wordCount, cItem.paraCount + + return 0, 0, 0 def getReferences(self, tHandle, sTitle=None): """Extract all references made in a file, and optionally title section. """ theRefs = {x: [] for x in nwKeyWords.KEY_CLASS} - if tHandle not in self._items: - return theRefs - - for rTitle, hItem in self._items[tHandle].items(): + for rTitle, hItem in self._itemIndex.iterItemHeaders(tHandle): if sTitle is None or sTitle == rTitle: for aTag, refTypes in hItem.references.items(): for refType in refTypes: @@ -578,28 +556,26 @@ class NWIndex(): def getNovelData(self, tHandle, sTitle): """Return the novel data of a given handle and title. """ - if tHandle in self._items: - if sTitle in self._items[tHandle]: - return self._items[tHandle][sTitle] + if tHandle in self._itemIndex: + return self._itemIndex[tHandle][sTitle] return None def getBackReferenceList(self, tHandle): """Build a list of files referring back to our file, specified by tHandle. """ - if tHandle is None or tHandle not in self._items: + if tHandle is None or tHandle not in self._itemIndex: return {} theRefs = {} - theTags = self._items[tHandle].allTags() + theTags = self._itemIndex.allItemTags(tHandle) if not theTags: return theRefs - for aHandle, tItem in self._items.items(): - for sTitle, hItem in tItem.items(): - for aTag in hItem.references: - if aTag in theTags and aHandle not in theRefs: - theRefs[aHandle] = sTitle + for aHandle, sTitle, hItem in self._itemIndex.iterAllHeaders(): + for aTag in hItem.references: + if aTag in theTags and aHandle not in theRefs: + theRefs[aHandle] = sTitle return theRefs @@ -613,22 +589,6 @@ class NWIndex(): # Internal Functions ## - def _listNovelHandles(self, skipExcluded): - """Return a list of all handles that exist in the novel index. - """ - theHandles = [] - for tItem in self.theProject.tree: - if tItem is None: - continue - if not tItem.isExported and skipExcluded: - continue - if tItem.itemLayout == nwItemLayout.NOTE: - continue - if tItem.itemHandle in self._items: - theHandles.append(tItem.itemHandle) - - return theHandles - def _validateTagsIndex(self, tagsIndex): """Iterate through the tagsIndex loaded from cache and check that it's valid. @@ -657,15 +617,172 @@ class NWIndex(): return - def _validateItemIndex(self, itemIndex): - """Iterate through the itemIndex loaded from cache and check - that it's valid. +# END Class NWIndex + + +# =============================================================================================== # +# Indexer Objects +# =============================================================================================== # + +class ItemIndex: + """A wrapper object holding the indexed items. + """ + + def __init__(self, theProject): + self.theProject = theProject + self._items = {} + return + + ## + # Methods + ## + + def clear(self): + """Clear the index. """ self._items = {} - if not isinstance(itemIndex, dict): + return + + def __contains__(self, tHandle): + """Check if an item exists in the index, + """ + return tHandle in self._items + + def __delitem__(self, tHandle): + """Delete an entry in the index. + """ + self._items.pop(tHandle, None) + return + + def __getitem__(self, tHandle): + """Return an item, or return None if it isn't found. + """ + return self._items.get(tHandle, None) + + def add(self, tHandle, tItem): + """Add a new item to the index. This will overwrite the item if + it already exists. + """ + self._items[tHandle] = IndexItem(tHandle, tItem) + return + + def mainItemHeader(self, tHandle): + """Return the primary item header for an item. + """ + if tHandle in self._items: + return self._items[tHandle].level + return "H0" + + def allItemTags(self, tHandle): + """Get all tags set for headings of an item. + """ + if tHandle in self._items: + return self._items[tHandle].allTags() + return [] + + def iterItemHeaders(self, tHandle): + """Iterate over all item headers of an item. + """ + if tHandle in self._items: + for sTitle, hItem in self._items[tHandle].items(): + yield sTitle, hItem + return + + def iterAllHeaders(self): + """Iterate through all items and headings in the index. + """ + for tHandle, tItem in self._items.items(): + for sTitle, hItem in tItem.items(): + yield tHandle, sTitle, hItem + return + + def iterNovelStructure(self, rootHandle=None, skipExcl=False): + """Iterate over all items and headers in the novel structure for + a given root handle, or for all if root handle is None. + """ + for tItem in self.theProject.tree: + if tItem is None: + continue + if tItem.itemLayout == nwItemLayout.NOTE: + continue + if skipExcl and not tItem.isExported: + continue + + tHandle = tItem.itemHandle + if tHandle not in self._items: + continue + + if rootHandle is None: + for sTitle, hItem in self._items[tHandle].items(): + yield tHandle, sTitle, hItem + elif tItem.rootHandle == rootHandle: + for sTitle, hItem in self._items[tHandle].items(): + yield tHandle, sTitle, hItem + else: + continue + + return + + ## + # Setters + ## + + def addItemHeading(self, tHandle, sTitle, hDepth, hText): + """Set the main heading level of an item. + """ + if tHandle in self._items: + tItem = self._items[tHandle] + tItem.updateLevel(hDepth) + tItem.addHeading(IndexHeading(sTitle, hDepth, hText)) + return + + def setHeadingCounts(self, tHandle, sTitle, cC, wC, pC): + """Set the character, word and paragraph counts of a heading + on a given item. + """ + if tHandle in self._items: + self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC) + return + + def setHeadingSynopsis(self, tHandle, sTitle, sText): + """Set the synopsis text for a heading on a given item. + """ + if tHandle in self._items: + self._items[tHandle].setHeadingSynopsis(sTitle, sText) + return + + def setHeadingTag(self, tHandle, sTitle, tagKey): + """Set the main tag for a heading on a given item. + """ + if tHandle in self._items: + self._items[tHandle].setHeadingTag(sTitle, tagKey) + return + + def addHeadingReferences(self, tHandle, sTitle, tagKeys, refType): + """Set the reference tags for a heading on a given item. + """ + if tHandle in self._items: + self._items[tHandle].addHeadingReferences(sTitle, tagKeys, refType) + return + + ## + # Pack/Unpack + ## + + def packData(self): + """Pack all the data of the index into a single dictionary. + """ + return {handle: item.packData() for handle, item in self._items.items()} + + def unpackData(self, data): + """Iterate through the itemIndex loaded from cache and check + that it's valid. This will raise errors if there is a problem. + """ + self._items = {} + if not isinstance(data, dict): raise ValueError("itemIndex is not a dict") - for tHandle, tData in itemIndex.items(): + for tHandle, tData in data.items(): if not isHandle(tHandle): raise ValueError("itemIndex keys must be handles") @@ -677,100 +794,14 @@ class NWIndex(): return -# END Class NWIndex +# END Class ItemIndex -# =============================================================================================== # -# Simple Word Counter -# =============================================================================================== # - -def countWords(theText): - """Count words in a piece of text, skipping special syntax and - comments. - """ - charCount = 0 - wordCount = 0 - paraCount = 0 - prevEmpty = True - - if not isinstance(theText, str): - return charCount, wordCount, paraCount - - # We need to treat dashes as word separators for counting words. - # The check+replace approach is much faster than direct replace for - # large texts, and a bit slower for small texts, but in the latter - # case it doesn't really matter. - if nwUnicode.U_ENDASH in theText: - theText = theText.replace(nwUnicode.U_ENDASH, " ") - if nwUnicode.U_EMDASH in theText: - theText = theText.replace(nwUnicode.U_EMDASH, " ") - - for aLine in theText.splitlines(): - - countPara = True - - if not aLine: - prevEmpty = True - continue - if aLine[0] == "@" or aLine[0] == "%": - continue - - if aLine[0] == "[": - if aLine.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")): - continue - elif aLine.startswith("[VSPACE:") and aLine.endswith("]"): - continue - - elif aLine[0] == "#": - if aLine[:5] == "#### ": - aLine = aLine[5:] - countPara = False - elif aLine[:4] == "### ": - aLine = aLine[4:] - countPara = False - elif aLine[:3] == "## ": - aLine = aLine[3:] - countPara = False - elif aLine[:2] == "# ": - aLine = aLine[2:] - countPara = False - elif aLine[:3] == "#! ": - aLine = aLine[3:] - countPara = False - elif aLine[:4] == "##! ": - aLine = aLine[4:] - countPara = False - - elif aLine[0] == ">" or aLine[-1] == "<": - if aLine[:2] == ">>": - aLine = aLine[2:].lstrip(" ") - elif aLine[:1] == ">": - aLine = aLine[1:].lstrip(" ") - if aLine[-2:] == "<<": - aLine = aLine[:-2].rstrip(" ") - elif aLine[-1:] == "<": - aLine = aLine[:-1].rstrip(" ") - - wordCount += len(aLine.split()) - charCount += len(aLine) - if countPara and prevEmpty: - paraCount += 1 - - prevEmpty = not countPara - - return charCount, wordCount, paraCount - - -# =============================================================================================== # -# Indexer Objects -# =============================================================================================== # - class IndexItem: def __init__(self, tHandle, tItem): self._handle = tHandle self._item = tItem - self._level = "H0" self._headings = {} self._index = 0 @@ -780,6 +811,9 @@ class IndexItem: return + def __repr__(self): + return f"" + ## # Properties ## @@ -792,47 +826,50 @@ class IndexItem: def level(self): return self._level - @property - def headings(self): - return sorted(self._headings.keys()) - - @property - def entries(self): - return self._headings.values() - ## # Setters ## def updateLevel(self, level): - """Set the level only if it is H0. + """Set the level only if it has not already been set. """ if self._level == "H0": self._level = level return def addHeading(self, tHeading): + """Add a heading to the item. Also remove the placeholder entry + if it exists. + """ if H_NONE in self._headings: self._headings.pop(H_NONE) self._headings[tHeading.key] = tHeading return def setHeadingCounts(self, sTitle, charCount, wordCount, paraCount): + """Set the character, word and paragraph count of a heading. + """ if sTitle in self._headings: self._headings[sTitle].setCounts(charCount, wordCount, paraCount) return def setHeadingSynopsis(self, sTitle, synopText): + """Set the synopsis text of a heading. + """ if sTitle in self._headings: self._headings[sTitle].setSynopsis(synopText) return def setHeadingTag(self, sTitle, tagKey): + """Set the tag of a heading. + """ if sTitle in self._headings: self._headings[sTitle].setTag(tagKey) return def addHeadingReferences(self, sTitle, tagKeys, refType): + """Add a reference key and all its types to a heading. + """ if sTitle in self._headings: for tagKey in tagKeys: self._headings[sTitle].addReference(tagKey, refType) @@ -917,6 +954,9 @@ class IndexHeading: return + def __repr__(self): + return f"" + ## # Properties ## @@ -962,21 +1002,30 @@ class IndexHeading: ## def setLevel(self, level): + """Set the level of the header if it's a valid value. + """ if level in H_VALID: self._level = level return def setCounts(self, charCount, wordCount, paraCount): + """Set the character, word and paragraph count. Make sure the + value is an integer and is not smaller than 0. + """ self._charCount = max(0, checkInt(charCount, 0)) self._wordCount = max(0, checkInt(wordCount, 0)) self._paraCount = max(0, checkInt(paraCount, 0)) return def setSynopsis(self, synopText): + """Set the synopsis text and make sure it is a string. + """ self._synopsis = str(synopText) return def setTag(self, tagKey): + """Set the tag for references, and make sure it is a string. + """ self._tag = str(tagKey) return @@ -1041,3 +1090,84 @@ class IndexHeading: return # END Class IndexHeading + + +# =============================================================================================== # +# Simple Word Counter +# =============================================================================================== # + +def countWords(theText): + """Count words in a piece of text, skipping special syntax and + comments. + """ + charCount = 0 + wordCount = 0 + paraCount = 0 + prevEmpty = True + + if not isinstance(theText, str): + return charCount, wordCount, paraCount + + # We need to treat dashes as word separators for counting words. + # The check+replace approach is much faster than direct replace for + # large texts, and a bit slower for small texts, but in the latter + # case it doesn't really matter. + if nwUnicode.U_ENDASH in theText: + theText = theText.replace(nwUnicode.U_ENDASH, " ") + if nwUnicode.U_EMDASH in theText: + theText = theText.replace(nwUnicode.U_EMDASH, " ") + + for aLine in theText.splitlines(): + + countPara = True + + if not aLine: + prevEmpty = True + continue + if aLine[0] == "@" or aLine[0] == "%": + continue + + if aLine[0] == "[": + if aLine.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")): + continue + elif aLine.startswith("[VSPACE:") and aLine.endswith("]"): + continue + + elif aLine[0] == "#": + if aLine[:5] == "#### ": + aLine = aLine[5:] + countPara = False + elif aLine[:4] == "### ": + aLine = aLine[4:] + countPara = False + elif aLine[:3] == "## ": + aLine = aLine[3:] + countPara = False + elif aLine[:2] == "# ": + aLine = aLine[2:] + countPara = False + elif aLine[:3] == "#! ": + aLine = aLine[3:] + countPara = False + elif aLine[:4] == "##! ": + aLine = aLine[4:] + countPara = False + + elif aLine[0] == ">" or aLine[-1] == "<": + if aLine[:2] == ">>": + aLine = aLine[2:].lstrip(" ") + elif aLine[:1] == ">": + aLine = aLine[1:].lstrip(" ") + if aLine[-2:] == "<<": + aLine = aLine[:-2].rstrip(" ") + elif aLine[-1:] == "<": + aLine = aLine[:-1].rstrip(" ") + + wordCount += len(aLine.split()) + charCount += len(aLine) + if countPara and prevEmpty: + paraCount += 1 + + prevEmpty = not countPara + + return charCount, wordCount, paraCount diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 28edb2d9..a1cd11d3 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -251,9 +251,7 @@ class GuiNovelTree(QTreeWidget): currChapter = None currScene = None - for tKey, tHandle, sTitle, novIdx in self.theProject.index.novelStructure( - skipExcluded=True - ): + for tKey, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True): tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) self._treeMap[tKey] = tItem diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index e3886b87..83b04f47 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -389,7 +389,7 @@ class GuiOutline(QTreeWidget): currChapter = None currScene = None - for _, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcluded=True): + for _, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True): tItem = self._createTreeItem(tHandle, sTitle, novIdx) diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index eff85fec..e39981f6 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -69,19 +69,19 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): # Take a copy of the index tagIndex = str(theIndex._tags) - itemsIndex = str({handle: item.packData() for handle, item in theIndex._items.items()}) + itemsIndex = str(theIndex._itemIndex.packData()) # Delete a handle assert theIndex._tags.get("Bod", None) is not None - assert theIndex._items.get("4c4f28287af27", None) is not None + assert theIndex._itemIndex["4c4f28287af27"] is not None theIndex.deleteHandle("4c4f28287af27") assert theIndex._tags.get("Bod", None) is None - assert theIndex._items.get("4c4f28287af27", None) is None + assert theIndex._itemIndex["4c4f28287af27"] is None # Clear the index theIndex.clearIndex() assert theIndex._tags == {} - assert theIndex._items == {} + assert theIndex._itemIndex._items == {} # Make the load fail with monkeypatch.context() as mp: @@ -92,9 +92,7 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): assert theIndex.loadIndex() is True assert str(theIndex._tags) == tagIndex - assert str( - {handle: item.packData() for handle, item in theIndex._items.items()} - ) == itemsIndex + assert str(theIndex._itemIndex.packData()) == itemsIndex # Break the index and check that we notice # assert theIndex.indexBroken is False @@ -328,40 +326,40 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "##### Title Five\n\n" # Not interpreted as a title, the hashes are counted as a word "Paragraph Five.\n\n" )) - assert theIndex._items[nHandle]["T000001"].references == {} - assert theIndex._items[nHandle]["T000007"].references == {} - assert theIndex._items[nHandle]["T000013"].references == {} - assert theIndex._items[nHandle]["T000019"].references == {} + assert theIndex._itemIndex[nHandle]["T000001"].references == {} + assert theIndex._itemIndex[nHandle]["T000007"].references == {} + assert theIndex._itemIndex[nHandle]["T000013"].references == {} + assert theIndex._itemIndex[nHandle]["T000019"].references == {} - assert theIndex._items[nHandle]["T000001"].level == "H1" - assert theIndex._items[nHandle]["T000007"].level == "H2" - assert theIndex._items[nHandle]["T000013"].level == "H3" - assert theIndex._items[nHandle]["T000019"].level == "H4" + assert theIndex._itemIndex[nHandle]["T000001"].level == "H1" + assert theIndex._itemIndex[nHandle]["T000007"].level == "H2" + assert theIndex._itemIndex[nHandle]["T000013"].level == "H3" + assert theIndex._itemIndex[nHandle]["T000019"].level == "H4" - assert theIndex._items[nHandle]["T000001"].title == "Title One" - assert theIndex._items[nHandle]["T000007"].title == "Title Two" - assert theIndex._items[nHandle]["T000013"].title == "Title Three" - assert theIndex._items[nHandle]["T000019"].title == "Title Four" + assert theIndex._itemIndex[nHandle]["T000001"].title == "Title One" + assert theIndex._itemIndex[nHandle]["T000007"].title == "Title Two" + assert theIndex._itemIndex[nHandle]["T000013"].title == "Title Three" + assert theIndex._itemIndex[nHandle]["T000019"].title == "Title Four" - assert theIndex._items[nHandle]["T000001"].charCount == 23 - assert theIndex._items[nHandle]["T000007"].charCount == 23 - assert theIndex._items[nHandle]["T000013"].charCount == 27 - assert theIndex._items[nHandle]["T000019"].charCount == 56 + assert theIndex._itemIndex[nHandle]["T000001"].charCount == 23 + assert theIndex._itemIndex[nHandle]["T000007"].charCount == 23 + assert theIndex._itemIndex[nHandle]["T000013"].charCount == 27 + assert theIndex._itemIndex[nHandle]["T000019"].charCount == 56 - assert theIndex._items[nHandle]["T000001"].wordCount == 4 - assert theIndex._items[nHandle]["T000007"].wordCount == 4 - assert theIndex._items[nHandle]["T000013"].wordCount == 4 - assert theIndex._items[nHandle]["T000019"].wordCount == 9 + assert theIndex._itemIndex[nHandle]["T000001"].wordCount == 4 + assert theIndex._itemIndex[nHandle]["T000007"].wordCount == 4 + assert theIndex._itemIndex[nHandle]["T000013"].wordCount == 4 + assert theIndex._itemIndex[nHandle]["T000019"].wordCount == 9 - assert theIndex._items[nHandle]["T000001"].paraCount == 1 - assert theIndex._items[nHandle]["T000007"].paraCount == 1 - assert theIndex._items[nHandle]["T000013"].paraCount == 1 - assert theIndex._items[nHandle]["T000019"].paraCount == 3 + assert theIndex._itemIndex[nHandle]["T000001"].paraCount == 1 + assert theIndex._itemIndex[nHandle]["T000007"].paraCount == 1 + assert theIndex._itemIndex[nHandle]["T000013"].paraCount == 1 + assert theIndex._itemIndex[nHandle]["T000019"].paraCount == 3 - assert theIndex._items[nHandle]["T000001"].synopsis == "Synopsis One." - assert theIndex._items[nHandle]["T000007"].synopsis == "Synopsis Two." - assert theIndex._items[nHandle]["T000013"].synopsis == "Synopsis Three." - assert theIndex._items[nHandle]["T000019"].synopsis == "Synopsis Four." + assert theIndex._itemIndex[nHandle]["T000001"].synopsis == "Synopsis One." + assert theIndex._itemIndex[nHandle]["T000007"].synopsis == "Synopsis Two." + assert theIndex._itemIndex[nHandle]["T000013"].synopsis == "Synopsis Three." + assert theIndex._itemIndex[nHandle]["T000019"].synopsis == "Synopsis Four." # Note File assert theIndex.scanText(cHandle, ( @@ -370,13 +368,13 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "% synopsis: Synopsis One.\n\n" "Paragraph One.\n\n" )) - assert theIndex._items[cHandle]["T000001"].references == {} - assert theIndex._items[cHandle]["T000001"].level == "H1" - assert theIndex._items[cHandle]["T000001"].title == "Title One" - assert theIndex._items[cHandle]["T000001"].charCount == 23 - assert theIndex._items[cHandle]["T000001"].wordCount == 4 - assert theIndex._items[cHandle]["T000001"].paraCount == 1 - assert theIndex._items[cHandle]["T000001"].synopsis == "Synopsis One." + assert theIndex._itemIndex[cHandle]["T000001"].references == {} + assert theIndex._itemIndex[cHandle]["T000001"].level == "H1" + assert theIndex._itemIndex[cHandle]["T000001"].title == "Title One" + assert theIndex._itemIndex[cHandle]["T000001"].charCount == 23 + assert theIndex._itemIndex[cHandle]["T000001"].wordCount == 4 + assert theIndex._itemIndex[cHandle]["T000001"].paraCount == 1 + assert theIndex._itemIndex[cHandle]["T000001"].synopsis == "Synopsis One." # Valid and Invalid References assert theIndex.scanText(sHandle, ( @@ -387,7 +385,7 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "% synopsis: Synopsis One.\n\n" "Paragraph One.\n\n" )) - assert theIndex._items[sHandle]["T000001"].references == { + assert theIndex._itemIndex[sHandle]["T000001"].references == { "One": {"@pov"}, "Two": {"@char"} } @@ -398,25 +396,25 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "#! My Project\n\n" ">> By Jane Doe <<\n\n" )) - assert theIndex._items[cHandle]["T000001"].references == {} - assert theIndex._items[tHandle]["T000001"].level == "H1" - assert theIndex._items[tHandle]["T000001"].title == "My Project" - assert theIndex._items[tHandle]["T000001"].charCount == 21 - assert theIndex._items[tHandle]["T000001"].wordCount == 5 - assert theIndex._items[tHandle]["T000001"].paraCount == 1 - assert theIndex._items[tHandle]["T000001"].synopsis == "" + assert theIndex._itemIndex[cHandle]["T000001"].references == {} + assert theIndex._itemIndex[tHandle]["T000001"].level == "H1" + assert theIndex._itemIndex[tHandle]["T000001"].title == "My Project" + assert theIndex._itemIndex[tHandle]["T000001"].charCount == 21 + assert theIndex._itemIndex[tHandle]["T000001"].wordCount == 5 + assert theIndex._itemIndex[tHandle]["T000001"].paraCount == 1 + assert theIndex._itemIndex[tHandle]["T000001"].synopsis == "" assert theIndex.scanText(tHandle, ( "##! Prologue\n\n" "In the beginning there was time ...\n\n" )) - assert theIndex._items[cHandle]["T000001"].references == {} - assert theIndex._items[tHandle]["T000001"].level == "H2" - assert theIndex._items[tHandle]["T000001"].title == "Prologue" - assert theIndex._items[tHandle]["T000001"].charCount == 43 - assert theIndex._items[tHandle]["T000001"].wordCount == 8 - assert theIndex._items[tHandle]["T000001"].paraCount == 1 - assert theIndex._items[tHandle]["T000001"].synopsis == "" + assert theIndex._itemIndex[cHandle]["T000001"].references == {} + assert theIndex._itemIndex[tHandle]["T000001"].level == "H2" + assert theIndex._itemIndex[tHandle]["T000001"].title == "Prologue" + assert theIndex._itemIndex[tHandle]["T000001"].charCount == 43 + assert theIndex._itemIndex[tHandle]["T000001"].wordCount == 8 + assert theIndex._itemIndex[tHandle]["T000001"].paraCount == 1 + assert theIndex._itemIndex[tHandle]["T000001"].synopsis == "" # Page wo/Title # ============= @@ -425,25 +423,25 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theIndex.scanText(pHandle, ( "This is a page with some text on it.\n\n" )) - assert theIndex._items[pHandle]["T000000"].references == {} - assert theIndex._items[pHandle]["T000000"].level == "H0" - assert theIndex._items[pHandle]["T000000"].title == "" - assert theIndex._items[pHandle]["T000000"].charCount == 36 - assert theIndex._items[pHandle]["T000000"].wordCount == 9 - assert theIndex._items[pHandle]["T000000"].paraCount == 1 - assert theIndex._items[pHandle]["T000000"].synopsis == "" + assert theIndex._itemIndex[pHandle]["T000000"].references == {} + assert theIndex._itemIndex[pHandle]["T000000"].level == "H0" + assert theIndex._itemIndex[pHandle]["T000000"].title == "" + assert theIndex._itemIndex[pHandle]["T000000"].charCount == 36 + assert theIndex._itemIndex[pHandle]["T000000"].wordCount == 9 + assert theIndex._itemIndex[pHandle]["T000000"].paraCount == 1 + assert theIndex._itemIndex[pHandle]["T000000"].synopsis == "" theProject.tree[pHandle]._layout = nwItemLayout.NOTE assert theIndex.scanText(pHandle, ( "This is a page with some text on it.\n\n" )) - assert theIndex._items[pHandle]["T000000"].references == {} - assert theIndex._items[pHandle]["T000000"].level == "H0" - assert theIndex._items[pHandle]["T000000"].title == "" - assert theIndex._items[pHandle]["T000000"].charCount == 36 - assert theIndex._items[pHandle]["T000000"].wordCount == 9 - assert theIndex._items[pHandle]["T000000"].paraCount == 1 - assert theIndex._items[pHandle]["T000000"].synopsis == "" + assert theIndex._itemIndex[pHandle]["T000000"].references == {} + assert theIndex._itemIndex[pHandle]["T000000"].level == "H0" + assert theIndex._itemIndex[pHandle]["T000000"].title == "" + assert theIndex._itemIndex[pHandle]["T000000"].charCount == 36 + assert theIndex._itemIndex[pHandle]["T000000"].wordCount == 9 + assert theIndex._itemIndex[pHandle]["T000000"].paraCount == 1 + assert theIndex._itemIndex[pHandle]["T000000"].synopsis == "" assert theProject.closeProject() is True @@ -488,13 +486,13 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): theProject.tree[nHandle].setExported(False) theKeys = [] - for aKey, _, _, _ in theIndex.novelStructure(skipExcluded=False): + for aKey, _, _, _ in theIndex.novelStructure(skipExcl=False): theKeys.append(aKey) assert theKeys == ["%s:T000001" % nHandle] theKeys = [] - for aKey, _, _, _ in theIndex.novelStructure(skipExcluded=True): + for aKey, _, _, _ in theIndex.novelStructure(skipExcl=True): theKeys.append(aKey) assert theKeys == [] @@ -625,12 +623,29 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): assert theIndex.scanText(sHandle, "### Scene One\n\n") assert theIndex.scanText(tHandle, "### Scene Two\n\n") - assert theIndex._listNovelHandles(False) == [nHandle, hHandle, sHandle, tHandle] - assert theIndex._listNovelHandles(True) == [hHandle, sHandle, tHandle] + assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ + (nHandle, "T000001"), + (nHandle, "T000011"), + (hHandle, "T000001"), + (sHandle, "T000001"), + (tHandle, "T000001"), + ] + + assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=True)] == [ + (hHandle, "T000001"), + (sHandle, "T000001"), + (tHandle, "T000001"), + ] # Add a fake handle to the tree and check that it's ignored theProject.tree._treeOrder.append("0000000000000") - assert theIndex._listNovelHandles(False) == [nHandle, hHandle, sHandle, tHandle] + assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ + (nHandle, "T000001"), + (nHandle, "T000011"), + (hHandle, "T000001"), + (sHandle, "T000001"), + (tHandle, "T000001"), + ] theProject.tree._treeOrder.remove("0000000000000") # Extract stats diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 0ed15742..999504bf 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -48,7 +48,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): # Rebuild the index nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger) assert nwGUI.theProject.index._tags != {} - assert nwGUI.theProject.index._items != {} + assert nwGUI.theProject.index._itemIndex._items != {} # Select a document in the project tree nwGUI.treeView.setSelectedHandle("88243afbe5ed8") From d1b32f1ba178f17390eb62ea7d1d2a3df6a71b58 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 29 May 2022 19:24:41 +0200 Subject: [PATCH 109/179] Move the tags index into a wrapper class --- novelwriter/core/index.py | 121 +++++++++++++++++++++------ tests/test_core/test_core_index.py | 22 ++--- tests/test_gui/test_gui_docviewer.py | 2 +- 3 files changed, 107 insertions(+), 38 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index dcd38a1d..ad4b863c 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -56,7 +56,7 @@ class NWIndex(): self._indexBroken = False # Indices - self._tags = {} + self._tagsIndex = TagsIndex() self._itemIndex = ItemIndex(theProject) # TimeStamps @@ -81,7 +81,7 @@ class NWIndex(): def clearIndex(self): """Clear the index dictionaries and time stamps. """ - self._tags = {} + self._tagsIndex.clear() self._itemIndex.clear() self._timeNovel = 0 self._timeNotes = 0 @@ -93,7 +93,7 @@ class NWIndex(): """ logger.debug("Removing item '%s' from the index", tHandle) for tTag in self._itemIndex.allItemTags(tHandle): - self._tags.pop(tTag, None) + del self._tagsIndex[tTag] del self._itemIndex[tHandle] @@ -152,7 +152,7 @@ class NWIndex(): return False try: - self._validateTagsIndex(theData["tagsIndex"]) + self._tagsIndex.unpackData(theData["tagsIndex"]) self._itemIndex.unpackData(theData["itemIndex"]) except Exception: logger.error("The index content is invalid") @@ -186,10 +186,11 @@ class NWIndex(): tStart = time() try: + tagsIndex = self._tagsIndex.packData() itemIndex = self._itemIndex.packData() with open(indexFile, mode="w+", encoding="utf-8") as outFile: outFile.write("{\n") - outFile.write(f' "tagsIndex": {jsonEncode(self._tags, n=1, nmax=2)},\n') + outFile.write(f' "tagsIndex": {jsonEncode(tagsIndex, n=1, nmax=2)},\n') outFile.write(f' "itemIndex": {jsonEncode(itemIndex, n=1, nmax=4)}\n') outFile.write("}\n") @@ -356,11 +357,7 @@ class NWIndex(): sTitle = f"T{nTitle:06d}" if theBits[0] == nwKeyWords.TAG_KEY: - self._tags[theBits[1]] = { - "handle": tHandle, - "heading": sTitle, - "class": itemClass.name, - } + self._tagsIndex.add(theBits[1], tHandle, sTitle, itemClass) self._itemIndex.setHeadingTag(tHandle, sTitle, theBits[1]) else: self._itemIndex.addHeadingReferences(tHandle, sTitle, theBits[1:], theBits[0]) @@ -425,8 +422,8 @@ class NWIndex(): # For a tag, only the first value is accepted, the rest are ignored if theBits[0] == nwKeyWords.TAG_KEY and nBits > 1: - if theBits[1] in self._tags: - isGood[1] = self._tags[theBits[1]].get("handle") == tItem.itemHandle + if theBits[1] in self._tagsIndex: + isGood[1] = self._tagsIndex.tagHandle(theBits[1]) == tItem.itemHandle else: isGood[1] = True return isGood @@ -434,8 +431,8 @@ class NWIndex(): # If we're still here, we check that the references exist theKey = nwKeyWords.KEY_CLASS[theBits[0]].name for n in range(1, nBits): - if theBits[n] in self._tags: - isGood[n] = theKey == self._tags[theBits[n]].get("class") + if theBits[n] in self._tagsIndex: + isGood[n] = self._tagsIndex.tagClass(theBits[n]) == theKey return isGood @@ -582,22 +579,98 @@ class NWIndex(): def getTagSource(self, theTag): """Return the source location of a given tag. """ - ref = self._tags.get(theTag, {}) - return ref.get("handle"), ref.get("heading", H_NONE) + tHandle = self._tagsIndex.tagHandle(theTag) + sTitle = self._tagsIndex.tagHeading(theTag) + return tHandle, sTitle + +# END Class NWIndex + + +# =============================================================================================== # +# Indexer Objects +# =============================================================================================== # + +class TagsIndex: + """A wrapper class that holds the reverse lookup tags index. + """ + + def __init__(self): + self._tags = {} + return ## - # Internal Functions + # Methods ## - def _validateTagsIndex(self, tagsIndex): + def clear(self): + """Clear the index. + """ + self._tags = {} + return + + def __contains__(self, tagKey): + """Check if a tag exists in the index, + """ + return tagKey in self._tags + + def __delitem__(self, tagKey): + """Delete an entry in the index. + """ + self._tags.pop(tagKey, None) + return + + def __getitem__(self, tagKey): + """Return a tag, or return None if it isn't found. + """ + return self._tags.get(tagKey, None) + + def add(self, tagKey, tHandle, sTitle, itemClass): + """Add a key to the index and set all values. + """ + self._tags[tagKey] = { + "handle": tHandle, "heading": sTitle, "class": itemClass.name + } + return + + def tagHandle(self, tagKey): + """Get the handle of a given tag. + """ + if tagKey in self._tags: + return self._tags.get(tagKey).get("handle") + return None + + def tagHeading(self, tagKey): + """Get the heading of a given tag. + """ + if tagKey in self._tags: + return self._tags.get(tagKey).get("heading") + return H_NONE + + def tagClass(self, tagKey): + """Get the class of a given tag. + """ + if tagKey in self._tags: + return self._tags.get(tagKey).get("class") + return None + + ## + # Pack/Unpack + ## + + def packData(self): + """Pack all the data of the tags into a single dictionary. + """ + return self._tags + + def unpackData(self, data): """Iterate through the tagsIndex loaded from cache and check that it's valid. """ self._tags = {} - if not isinstance(tagsIndex, dict): + if not isinstance(data, dict): raise ValueError("tagsIndex is not a dict") - for tagKey, tagData in tagsIndex.items(): + for tagKey, tagData in data.items(): if not isinstance(tagKey, str): raise ValueError("tagsIndex keys must be a strings") if "handle" not in tagData: @@ -613,17 +686,13 @@ class NWIndex(): if not isItemClass(tagData["class"]): raise ValueError("tagsIndex handle must be an nwItemClass") - self._tags = tagsIndex + self._tags = data return -# END Class NWIndex +# END Class TagsIndex -# =============================================================================================== # -# Indexer Objects -# =============================================================================================== # - class ItemIndex: """A wrapper object holding the indexed items. """ diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index e39981f6..76ffc70a 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -68,19 +68,19 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): assert theIndex.saveIndex() is True # Take a copy of the index - tagIndex = str(theIndex._tags) + tagIndex = str(theIndex._tagsIndex.packData()) itemsIndex = str(theIndex._itemIndex.packData()) # Delete a handle - assert theIndex._tags.get("Bod", None) is not None + assert theIndex._tagsIndex["Bod"] is not None assert theIndex._itemIndex["4c4f28287af27"] is not None theIndex.deleteHandle("4c4f28287af27") - assert theIndex._tags.get("Bod", None) is None + assert theIndex._tagsIndex["Bod"] is None assert theIndex._itemIndex["4c4f28287af27"] is None # Clear the index theIndex.clearIndex() - assert theIndex._tags == {} + assert theIndex._tagsIndex._tags == {} assert theIndex._itemIndex._items == {} # Make the load fail @@ -91,7 +91,7 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): # Make the load pass assert theIndex.loadIndex() is True - assert str(theIndex._tags) == tagIndex + assert str(theIndex._tagsIndex.packData()) == tagIndex assert str(theIndex._itemIndex.packData()) == itemsIndex # Break the index and check that we notice @@ -188,9 +188,9 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI): "@pov: Jane\n" "@invalid: John\n" # Checks for issue #688 )) - assert theIndex._tags == { - "Jane": {"handle": cHandle, "heading": "T000001", "class": "CHARACTER"} - } + assert theIndex._tagsIndex.tagHandle("Jane") == cHandle + assert theIndex._tagsIndex.tagHeading("Jane") == "T000001" + assert theIndex._tagsIndex.tagClass("Jane") == "CHARACTER" assert theIndex.getNovelData(nHandle, "T000001").title == "Hello World!" assert theIndex.getReferences(nHandle, "T000001") == { "@char": [], @@ -301,9 +301,9 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): "This is a story about Jane Smith.\n\n" "Well, not really.\n" )) - assert theIndex._tags == { - "Jane": {"handle": cHandle, "heading": "T000001", "class": "CHARACTER"} - } + assert theIndex._tagsIndex.tagHandle("Jane") == cHandle + assert theIndex._tagsIndex.tagHeading("Jane") == "T000001" + assert theIndex._tagsIndex.tagClass("Jane") == "CHARACTER" assert theIndex.getNovelData(nHandle, "T000001").title == "Hello World!" # Title Indexing diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 999504bf..1fa6b2c7 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -47,7 +47,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): # Rebuild the index nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger) - assert nwGUI.theProject.index._tags != {} + assert nwGUI.theProject.index._tagsIndex._tags != {} assert nwGUI.theProject.index._itemIndex._items != {} # Select a document in the project tree From 865acfd7e92e362da479f2ff18f17462746e1989 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 29 May 2022 19:40:14 +0200 Subject: [PATCH 110/179] Clean up imports --- novelwriter/core/__init__.py | 3 +-- novelwriter/core/index.py | 2 +- tests/test_core/test_core_tohtml.py | 3 ++- tests/test_core/test_core_tomd.py | 3 ++- tests/test_core/test_core_toodt.py | 3 ++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/novelwriter/core/__init__.py b/novelwriter/core/__init__.py index 6e69917f..c91ca941 100644 --- a/novelwriter/core/__init__.py +++ b/novelwriter/core/__init__.py @@ -20,7 +20,7 @@ along with this program. If not, see . """ from novelwriter.core.document import NWDoc -from novelwriter.core.index import NWIndex, countWords +from novelwriter.core.index import countWords from novelwriter.core.project import NWProject from novelwriter.core.spellcheck import NWSpellEnchant from novelwriter.core.tohtml import ToHtml @@ -30,7 +30,6 @@ from novelwriter.core.tomd import ToMarkdown __all__ = [ "countWords", "NWDoc", - "NWIndex", "NWProject", "NWSpellEnchant", "ToHtml", diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index ad4b863c..c1cb1e01 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -46,7 +46,7 @@ H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} H_NONE = "T000000" -class NWIndex(): +class NWIndex: def __init__(self, theProject): diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py index 11d89572..12072e09 100644 --- a/tests/test_core/test_core_tohtml.py +++ b/tests/test_core/test_core_tohtml.py @@ -24,7 +24,8 @@ import pytest from tools import readFile -from novelwriter.core import NWProject, NWIndex, ToHtml +from novelwriter.core import NWProject, ToHtml +from novelwriter.core.index import NWIndex @pytest.mark.core diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py index 51eea72b..c2235ff8 100644 --- a/tests/test_core/test_core_tomd.py +++ b/tests/test_core/test_core_tomd.py @@ -24,7 +24,8 @@ import pytest from tools import readFile -from novelwriter.core import NWProject, NWIndex, ToMarkdown +from novelwriter.core import NWProject, ToMarkdown +from novelwriter.core.index import NWIndex @pytest.mark.core diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index e2ccb4a5..febbc94f 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -28,7 +28,8 @@ from shutil import copyfile from tools import cmpFiles -from novelwriter.core import NWProject, NWIndex, ToOdt +from novelwriter.core import NWProject, ToOdt +from novelwriter.core.index import NWIndex from novelwriter.core.toodt import ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag XML_NS = [ From eed59a96be863ea14bd933c5562d4afa40eebb6f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 30 May 2022 00:28:43 +0200 Subject: [PATCH 111/179] Make some fixes to the index class, and improve test coverage --- novelwriter/core/index.py | 53 +++-- novelwriter/guimain.py | 21 +- sample/content/5eaea4e8cdee8.nwd | 1 + sample/content/88706ddc78b1b.nwd | 2 +- sample/content/b3e74dbc1f584.nwd | 1 + sample/content/b8136a5a774a0.nwd | 2 +- tests/test_core/test_core_index.py | 332 ++++++++++++++++++++++++----- 7 files changed, 320 insertions(+), 92 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index c1cb1e01..cdd78032 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -7,6 +7,7 @@ File History: Created: 2019-04-22 [0.0.1] countWords Created: 2019-05-27 [0.1.4] NWIndex Created: 2022-05-28 [1.7rc1] IndexItem, IndexHeading +Created: 2022-05-29 [1.7rc1] TagsIndex, ItemIndex This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -43,10 +44,24 @@ logger = logging.getLogger(__name__) H_VALID = ("H0", "H1", "H2", "H3", "H4") H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} -H_NONE = "T000000" +TT_NONE = "T000000" class NWIndex: + """This class holds the entire index for a given project. The index + contains the data that isn't stored in the project items themselves. + The content of the index is updated every time a file item is saved. + + The primary index data is contained in the ItemIndex class, which + contains an IndexItem representing each NWItem. Each IndexItem holds + an IndexHeading object for each heading of the item's text. + + A reverse index of all tags is contained in the TagsIndex class. + This is duplicate information used for quicker lookups from the tags + and back to items where they are defined. + + The index data is cached in a JSON file between writing sessions. + """ def __init__(self, theProject): @@ -104,10 +119,10 @@ class NWIndex: moved from the archive or trash folders back into the active project. """ - logger.debug("Re-indexing item '%s'", tHandle) if not self.theProject.tree.checkType(tHandle, nwItemType.FILE): return False + logger.debug("Re-indexing item '%s'", tHandle) theDoc = NWDoc(self.theProject, tHandle) theText = theDoc.readDocument() self.scanText(tHandle, theText if theText is not None else "") @@ -140,6 +155,7 @@ class NWIndex: indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) tStart = time() + self._indexBroken = False if os.path.isfile(indexFile): logger.debug("Loading index file") try: @@ -222,8 +238,9 @@ class NWIndex: logger.info("Not indexing non-file item '%s'", tHandle) return False - # Delete the old entry and create a new - self.deleteHandle(tHandle) + # Delete tags and create new item entry + for tTag in self._itemIndex.allItemTags(tHandle): + del self._tagsIndex[tTag] self._itemIndex.add(tHandle, theItem) # Run word counter for the whole text @@ -644,7 +661,7 @@ class TagsIndex: """ if tagKey in self._tags: return self._tags.get(tagKey).get("heading") - return H_NONE + return TT_NONE def tagClass(self, tagKey): """Get the class of a given tag. @@ -782,11 +799,11 @@ class ItemIndex: continue if rootHandle is None: - for sTitle, hItem in self._items[tHandle].items(): - yield tHandle, sTitle, hItem - elif tItem.rootHandle == rootHandle: - for sTitle, hItem in self._items[tHandle].items(): - yield tHandle, sTitle, hItem + for sTitle in self._items[tHandle].headings(): + yield tHandle, sTitle, self._items[tHandle][sTitle] + elif tItem.itemRoot == rootHandle: + for sTitle in self._items[tHandle].headings(): + yield tHandle, sTitle, self._items[tHandle][sTitle] else: continue @@ -876,7 +893,7 @@ class IndexItem: self._index = 0 # Add a placeholder heading - self._headings[H_NONE] = IndexHeading(H_NONE) + self._headings[TT_NONE] = IndexHeading(TT_NONE) return @@ -910,8 +927,8 @@ class IndexItem: """Add a heading to the item. Also remove the placeholder entry if it exists. """ - if H_NONE in self._headings: - self._headings.pop(H_NONE) + if TT_NONE in self._headings: + self._headings.pop(TT_NONE) self._headings[tHeading.key] = tHeading return @@ -957,6 +974,9 @@ class IndexItem: def items(self): return self._headings.items() + def headings(self): + return sorted(self._headings.keys()) + def allTags(self): """Return a list of all tags in the current item. """ @@ -1102,9 +1122,10 @@ class IndexHeading: """Add a record of a reference tag, and what keyword types it is associated with. """ - if tagKey not in self._refs: - self._refs[tagKey] = set() - self._refs[tagKey].add(refType) + if refType in nwKeyWords.VALID_KEYS: + if tagKey not in self._refs: + self._refs[tagKey] = set() + self._refs[tagKey].add(refType) return ## diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 3a957577..3d61a65e 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -865,22 +865,13 @@ class GuiMain(QMainWindow): self.theProject.index.clearIndex() for tItem in self.theProject.tree: + if tItem is None: # pragma: no cover + continue # This is a bug trap - if tItem is not None: - self.setStatus(self.tr("Indexing: '{0}'").format(tItem.itemName)) - else: - self.setStatus(self.tr("Indexing: '{0}'").format(self.tr("Unknown item"))) - - if tItem is not None and tItem.itemType == nwItemType.FILE: - logger.verbose("Scanning '%s'", tItem.itemName) - self.theProject.index.reIndexHandle(tItem.itemHandle) - - # Get Word Counts - cC, wC, pC = self.theProject.index.getCounts(tItem.itemHandle) - tItem.setCharCount(cC) - tItem.setWordCount(wC) - tItem.setParaCount(pC) - self.treeView.propagateCount(tItem.itemHandle, wC, countChildren=True) + logger.verbose("Indexing '%s'", tItem.itemName) + if self.theProject.index.reIndexHandle(tItem.itemHandle): + # Update Word Counts + self.treeView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True) self.treeView.setTreeItemValues(tItem.itemHandle) tEnd = time() diff --git a/sample/content/5eaea4e8cdee8.nwd b/sample/content/5eaea4e8cdee8.nwd index 1a7f3c79..0f8ecc26 100644 --- a/sample/content/5eaea4e8cdee8.nwd +++ b/sample/content/5eaea4e8cdee8.nwd @@ -4,5 +4,6 @@ # Mars @tag: Mars +@location: Space It’s red. Dusty and red. diff --git a/sample/content/88706ddc78b1b.nwd b/sample/content/88706ddc78b1b.nwd index ce4d2123..0f140538 100644 --- a/sample/content/88706ddc78b1b.nwd +++ b/sample/content/88706ddc78b1b.nwd @@ -1,5 +1,5 @@ %%~name: Chapter Two -%%~path: e7ded148d6e4a/88706ddc78b1b +%%~path: 7031beac91f75/88706ddc78b1b %%~kind: NOVEL/DOCUMENT ## Where has John Gone? diff --git a/sample/content/b3e74dbc1f584.nwd b/sample/content/b3e74dbc1f584.nwd index 6931a299..c980865e 100644 --- a/sample/content/b3e74dbc1f584.nwd +++ b/sample/content/b3e74dbc1f584.nwd @@ -4,5 +4,6 @@ # Earth @tag: Earth +@location: Space Third planet from the sun, fairly dense, and with lots of people on it. \ No newline at end of file diff --git a/sample/content/b8136a5a774a0.nwd b/sample/content/b8136a5a774a0.nwd index 3c2c1854..636c7227 100644 --- a/sample/content/b8136a5a774a0.nwd +++ b/sample/content/b8136a5a774a0.nwd @@ -1,6 +1,6 @@ %%~name: Delete Me! %%~path: 98acd8c76c93a/b8136a5a774a0 -%%~kind: NOVEL/DOCUMENT +%%~kind: TRASH/DOCUMENT ### Delete Me! This scene is trash. \ No newline at end of file diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 76ffc70a..b1233275 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -19,14 +19,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import os import json +import pytest from shutil import copyfile from mock import causeException -from tools import cmpFiles +from tools import buildTestProject, cmpFiles, writeFile from novelwriter.core.project import NWProject from novelwriter.core.index import NWIndex, countWords @@ -87,36 +87,58 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): with monkeypatch.context() as mp: mp.setattr(json, "load", causeException) assert theIndex.loadIndex() is False + assert theIndex.indexBroken is True # Make the load pass assert theIndex.loadIndex() is True + assert theIndex.indexBroken is False assert str(theIndex._tagsIndex.packData()) == tagIndex assert str(theIndex._itemIndex.packData()) == itemsIndex - # Break the index and check that we notice - # assert theIndex.indexBroken is False - # theIndex._tagIndex["Bod"].append("Stuff") - # theIndex._checkIndex() - # assert theIndex.indexBroken is True + # Check File + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile) + + # Write an emtpy index file and load it + writeFile(projFile, "{}") + assert theIndex.loadIndex() is False + assert theIndex.indexBroken is True + + # Write an index file that passes loading, but is still empty + writeFile(projFile, '{"tagsIndex": {}, "itemIndex": {}}') + assert theIndex.loadIndex() is True + assert theIndex.indexBroken is False + + # Check that the index is re-populated + assert "04468803b92e1" in theIndex._itemIndex + assert "2426c6f0ca922" in theIndex._itemIndex + assert "441420a886d82" in theIndex._itemIndex + assert "47666c91c7ccf" in theIndex._itemIndex + assert "4c4f28287af27" in theIndex._itemIndex + assert "846352075de7d" in theIndex._itemIndex + assert "88243afbe5ed8" in theIndex._itemIndex + assert "88d59a277361b" in theIndex._itemIndex + assert "8c58a65414c23" in theIndex._itemIndex + assert "db7e733775d4d" in theIndex._itemIndex + assert "eb103bc70c90c" in theIndex._itemIndex + assert "f8c0562e50f1b" in theIndex._itemIndex + assert "f96ec11c6a3da" in theIndex._itemIndex + assert "fb609cd8319dc" in theIndex._itemIndex + assert "7a992350f3eb6" in theIndex._itemIndex # Finalise assert theProject.closeProject() is True - copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) - # END Test testCoreIndex_LoadSave @pytest.mark.core -def testCoreIndex_ScanThis(nwMinimal, mockGUI): +def testCoreIndex_ScanThis(mockGUI): """Test the tag scanner function scanThis. """ theProject = NWProject(mockGUI) - assert theProject.openProject(nwMinimal) is True - - theIndex = NWIndex(theProject) + theIndex = theProject.index isValid, theBits, thePos = theIndex.scanThis("tag: this, and this") assert isValid is False @@ -161,15 +183,15 @@ def testCoreIndex_ScanThis(nwMinimal, mockGUI): @pytest.mark.core -def testCoreIndex_CheckThese(nwMinimal, mockGUI): +def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): """Test the tag checker function checkThese. """ theProject = NWProject(mockGUI) - assert theProject.openProject(nwMinimal) is True + buildTestProject(theProject, fncDir) + theIndex = theProject.index - theIndex = NWIndex(theProject) - nHandle = theProject.newFile("Hello", "a508bb932959c") - cHandle = theProject.newFile("Jane", "afb3043c7b2b3") + nHandle = theProject.newFile("Hello", "0000000000010") + cHandle = theProject.newFile("Jane", "0000000000012") nItem = theProject.tree[nHandle] cItem = theProject.tree[cHandle] @@ -239,17 +261,16 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI): @pytest.mark.core -def testCoreIndex_ScanText(nwMinimal, mockGUI): +def testCoreIndex_ScanText(mockGUI, fncDir, mockRnd): """Check the index text scanner. """ theProject = NWProject(mockGUI) - assert theProject.openProject(nwMinimal) is True - - theIndex = NWIndex(theProject) + buildTestProject(theProject, fncDir) + theIndex = theProject.index # Some items for fail to scan tests - dHandle = theProject.newFolder("Folder", "a508bb932959c") - xHandle = theProject.newFile("No Layout", "a508bb932959c") + dHandle = theProject.newFolder("Folder", "0000000000010") + xHandle = theProject.newFile("No Layout", "0000000000010") xItem = theProject.tree[xHandle] xItem.setLayout(nwItemLayout.NO_LAYOUT) @@ -279,11 +300,11 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theIndex.scanText(xHandle, "Hello World!") is False # Make some usable items - tHandle = theProject.newFile("Title", "a508bb932959c") - pHandle = theProject.newFile("Page", "a508bb932959c") - nHandle = theProject.newFile("Hello", "a508bb932959c") - cHandle = theProject.newFile("Jane", "afb3043c7b2b3") - sHandle = theProject.newFile("Scene", "a508bb932959c") + tHandle = theProject.newFile("Title", "0000000000010") + pHandle = theProject.newFile("Page", "0000000000010") + nHandle = theProject.newFile("Hello", "0000000000010") + cHandle = theProject.newFile("Jane", "0000000000012") + sHandle = theProject.newFile("Scene", "0000000000010") # Text Indexing # ============= @@ -449,18 +470,27 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): @pytest.mark.core -def testCoreIndex_ExtractData(nwMinimal, mockGUI): +def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd): """Check the index data extraction functions. """ theProject = NWProject(mockGUI) - assert theProject.openProject(nwMinimal) is True + buildTestProject(theProject, fncDir) - theIndex = NWIndex(theProject) - nHandle = theProject.newFile("Hello", "a508bb932959c") - cHandle = theProject.newFile("Jane", "afb3043c7b2b3") + theIndex = theProject.index + theIndex.reIndexHandle("0000000000010") + theIndex.reIndexHandle("0000000000011") + theIndex.reIndexHandle("0000000000012") + theIndex.reIndexHandle("0000000000013") + theIndex.reIndexHandle("0000000000014") + theIndex.reIndexHandle("0000000000015") + theIndex.reIndexHandle("0000000000016") + theIndex.reIndexHandle("0000000000017") + + nHandle = theProject.newFile("Hello", "0000000000010") + cHandle = theProject.newFile("Jane", "0000000000012") assert theIndex.getNovelData("", "") is None - assert theIndex.getNovelData("a508bb932959c", "") is None + assert theIndex.getNovelData("0000000000010", "") is None assert theIndex.scanText(cHandle, ( "# Jane Smith\n" @@ -480,7 +510,12 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): for aKey, _, _, _ in theIndex.novelStructure(): theKeys.append(aKey) - assert theKeys == ["%s:T000001" % nHandle] + assert theKeys == [ + "0000000000014:T000001", + "0000000000016:T000001", + "0000000000017:T000001", + "%s:T000001" % nHandle, + ] # Check that excluded files can be skipped theProject.tree[nHandle].setExported(False) @@ -489,19 +524,22 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): for aKey, _, _, _ in theIndex.novelStructure(skipExcl=False): theKeys.append(aKey) - assert theKeys == ["%s:T000001" % nHandle] + assert theKeys == [ + "0000000000014:T000001", + "0000000000016:T000001", + "0000000000017:T000001", + "%s:T000001" % nHandle, + ] theKeys = [] for aKey, _, _, _ in theIndex.novelStructure(skipExcl=True): theKeys.append(aKey) - assert theKeys == [] - - theKeys = [] - for aKey, _, _, _ in theIndex.novelStructure(): - theKeys.append(aKey) - - assert theKeys == [] + assert theKeys == [ + "0000000000014:T000001", + "0000000000016:T000001", + "0000000000017:T000001", + ] # The novel file should have the correct counts cC, wC, pC = theIndex.getCounts(nHandle) @@ -528,6 +566,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # None handle should return an empty dict assert theIndex.getBackReferenceList(None) == {} + # The Title Page file should have no references as it has no tag + assert theIndex.getBackReferenceList("0000000000014") == {} + # The character file should have a record of the reference from the novel file theRefs = theIndex.getBackReferenceList(cHandle) assert theRefs == {nHandle: "T000001"} @@ -542,6 +583,10 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # ========= # For whole text and sections + # Invalid handle or title should return 0s + assert theIndex.getCounts("stuff") == (0, 0, 0) + assert theIndex.getCounts(nHandle, "stuff") == (0, 0, 0) + # Get section counts for a novel file assert theIndex.scanText(nHandle, ( "# Hello World!\n" @@ -611,9 +656,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # Novel Stats # =========== - hHandle = theProject.newFile("Chapter", "a508bb932959c") - sHandle = theProject.newFile("Scene One", "a508bb932959c") - tHandle = theProject.newFile("Scene Two", "a508bb932959c") + hHandle = theProject.newFile("Chapter", "0000000000010") + sHandle = theProject.newFile("Scene One", "0000000000010") + tHandle = theProject.newFile("Scene Two", "0000000000010") theProject.tree[hHandle].itemLayout == nwItemLayout.DOCUMENT theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT @@ -624,6 +669,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): assert theIndex.scanText(tHandle, "### Scene Two\n\n") assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ + ("0000000000014", "T000001"), + ("0000000000016", "T000001"), + ("0000000000017", "T000001"), (nHandle, "T000001"), (nHandle, "T000011"), (hHandle, "T000001"), @@ -632,6 +680,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): ] assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=True)] == [ + ("0000000000014", "T000001"), + ("0000000000016", "T000001"), + ("0000000000017", "T000001"), (hHandle, "T000001"), (sHandle, "T000001"), (tHandle, "T000001"), @@ -640,6 +691,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # Add a fake handle to the tree and check that it's ignored theProject.tree._treeOrder.append("0000000000000") assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ + ("0000000000014", "T000001"), + ("0000000000016", "T000001"), + ("0000000000017", "T000001"), (nHandle, "T000001"), (nHandle, "T000011"), (hHandle, "T000001"), @@ -649,25 +703,33 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): theProject.tree._treeOrder.remove("0000000000000") # Extract stats - assert theIndex.getNovelWordCount(False) == 34 - assert theIndex.getNovelWordCount(True) == 6 - assert theIndex.getNovelTitleCounts(False) == [0, 2, 1, 2, 0] - assert theIndex.getNovelTitleCounts(True) == [0, 0, 1, 2, 0] + assert theIndex.getNovelWordCount(skipExcl=False) == 43 + assert theIndex.getNovelWordCount(skipExcl=True) == 15 + assert theIndex.getNovelTitleCounts(skipExcl=False) == [0, 3, 2, 3, 0] + assert theIndex.getNovelTitleCounts(skipExcl=True) == [0, 1, 2, 3, 0] # Table of Contents - assert theIndex.getTableOfContents(0, True) == [] - assert theIndex.getTableOfContents(1, True) == [] - assert theIndex.getTableOfContents(2, True) == [ + assert theIndex.getTableOfContents(0, skipExcl=True) == [] + assert theIndex.getTableOfContents(1, skipExcl=True) == [ + ("0000000000014:T000001", 1, "New Novel", 15), + ] + assert theIndex.getTableOfContents(2, skipExcl=True) == [ + ("0000000000014:T000001", 1, "New Novel", 5), + ("0000000000016:T000001", 2, "New Chapter", 4), ("%s:T000001" % hHandle, 2, "Chapter One", 6), ] - assert theIndex.getTableOfContents(3, True) == [ + assert theIndex.getTableOfContents(3, skipExcl=True) == [ + ("0000000000014:T000001", 1, "New Novel", 5), + ("0000000000016:T000001", 2, "New Chapter", 2), + ("0000000000017:T000001", 3, "New Scene", 2), ("%s:T000001" % hHandle, 2, "Chapter One", 2), ("%s:T000001" % sHandle, 3, "Scene One", 2), ("%s:T000001" % tHandle, 3, "Scene Two", 2), ] - assert theIndex.getTableOfContents(0, False) == [] - assert theIndex.getTableOfContents(1, False) == [ + assert theIndex.getTableOfContents(0, skipExcl=False) == [] + assert theIndex.getTableOfContents(1, skipExcl=False) == [ + ("0000000000014:T000001", 1, "New Novel", 9), ("%s:T000001" % nHandle, 1, "Hello World!", 12), ("%s:T000011" % nHandle, 1, "Hello World!", 22), ] @@ -682,7 +744,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): ("%s:T000001" % nHandle, 12), ("%s:T000011" % nHandle, 16) ] - assert theProject.closeProject() + assert theIndex.saveIndex() is True + assert theProject.saveProject() is True + assert theProject.closeProject() is True # Header Record bHandle = "0000000000000" @@ -697,6 +761,156 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # END Test testCoreIndex_ExtractData +@pytest.mark.core +def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd): + """Check the ItemIndex class. + """ + theProject = NWProject(mockGUI) + buildTestProject(theProject, fncDir) + + nHandle = "0000000000014" + cHandle = "0000000000016" + sHandle = "0000000000017" + + assert theProject.index.saveIndex() is True + itemIndex = theProject.index._itemIndex + + # The index should be empty + assert nHandle not in itemIndex + assert cHandle not in itemIndex + assert sHandle not in itemIndex + + # Unpack Data + # =========== + + # Data must be dictionary + with pytest.raises(ValueError): + itemIndex.unpackData("stuff") + + # Keys must be valid handles + with pytest.raises(ValueError): + itemIndex.unpackData({"stuff": "more stuff"}) + + # Unknown keys should be skipped + itemIndex.unpackData({"0000000000000": {}}) + assert itemIndex._items == {} + + # Known keys can be added, even witout data + itemIndex.unpackData({nHandle: {}}) + assert nHandle in itemIndex + itemIndex.clear() + + # Add Items + # ========= + assert cHandle not in itemIndex + + # Add the novel chapter file + itemIndex.add(cHandle, theProject.tree[cHandle]) + assert cHandle in itemIndex + assert itemIndex[cHandle].item == theProject.tree[cHandle] + assert itemIndex.mainItemHeader(cHandle) == "H0" + assert itemIndex.allItemTags(cHandle) == [] + assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T000000" + + # Add a heading to the item, which should replace the T000000 heading + itemIndex.addItemHeading(cHandle, "T000001", "H2", "Chapter One") + assert itemIndex.mainItemHeader(cHandle) == "H2" + assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T000001" + + # Set the remainig data values + itemIndex.setHeadingCounts(cHandle, "T000001", 60, 10, 2) + itemIndex.setHeadingSynopsis(cHandle, "T000001", "In the beginning ...") + itemIndex.setHeadingTag(cHandle, "T000001", "One") # Although it isn't allowed to have a tag + itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@pov") + itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@focus") + itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane", "John"], "@char") + idxData = itemIndex.packData() + + assert idxData[cHandle]["level"] == "H2" + assert idxData[cHandle]["headings"]["T000001"] == { + "level": "H2", "title": "Chapter One", "tag": "One", + "cCount": 60, "wCount": 10, "pCount": 2, "synopsis": "In the beginning ...", + } + assert "@pov" in idxData[cHandle]["references"]["T000001"]["Jane"] + assert "@focus" in idxData[cHandle]["references"]["T000001"]["Jane"] + assert "@char" in idxData[cHandle]["references"]["T000001"]["Jane"] + assert "@char" in idxData[cHandle]["references"]["T000001"]["John"] + + # Add the other two files + itemIndex.add(nHandle, theProject.tree[nHandle]) + itemIndex.add(sHandle, theProject.tree[sHandle]) + itemIndex.addItemHeading(nHandle, "T000001", "H1", "Novel") + itemIndex.addItemHeading(sHandle, "T000001", "H3", "Scene One") + + # Data Extraction + # =============== + + # Get headers + allHeads = list(itemIndex.iterAllHeaders()) + assert allHeads[0][0] == cHandle + assert allHeads[1][0] == nHandle + assert allHeads[2][0] == sHandle + assert allHeads[0][1] == "T000001" + assert allHeads[1][1] == "T000001" + assert allHeads[2][1] == "T000001" + + # Ask for stuff that doesn't exist + assert itemIndex.mainItemHeader("blablabla") == "H0" + assert itemIndex.allItemTags("blablabla") == [] + + # Novel Structure + # =============== + + # Add a second novel + mHandle = theProject.newRoot(nwItemClass.NOVEL) + uHandle = theProject.newFile("Title Page", mHandle) + itemIndex.add(uHandle, theProject.tree[uHandle]) + itemIndex.addItemHeading(uHandle, "T000001", "H1", "Novel 2") + assert uHandle in itemIndex + + # Structure of all novels + nStruct = list(itemIndex.iterNovelStructure()) + assert len(nStruct) == 4 + assert nStruct[0][0] == nHandle + assert nStruct[1][0] == cHandle + assert nStruct[2][0] == sHandle + assert nStruct[3][0] == uHandle + + # Novel structure with root handle set + nStruct = list(itemIndex.iterNovelStructure(rootHandle="0000000000010")) + assert len(nStruct) == 3 + assert nStruct[0][0] == nHandle + assert nStruct[1][0] == cHandle + assert nStruct[2][0] == sHandle + + nStruct = list(itemIndex.iterNovelStructure(rootHandle=mHandle)) + assert len(nStruct) == 1 + assert nStruct[0][0] == uHandle + + # Inject garbage into tree + theProject.tree._treeOrder.append("stuff") + nStruct = list(itemIndex.iterNovelStructure()) + assert len(nStruct) == 4 + assert nStruct[0][0] == nHandle + assert nStruct[1][0] == cHandle + assert nStruct[2][0] == sHandle + assert nStruct[3][0] == uHandle + + # Skip excluded + theProject.tree[sHandle].setExported(False) + nStruct = list(itemIndex.iterNovelStructure(skipExcl=True)) + assert len(nStruct) == 3 + assert nStruct[0][0] == nHandle + assert nStruct[1][0] == cHandle + assert nStruct[2][0] == uHandle + + # Delete new item + del itemIndex[uHandle] + assert uHandle not in itemIndex + +# END Test testCoreIndex_ItemIndex + + @pytest.mark.core def testCoreIndex_CountWords(): """Test the word counter and the exclusion filers. From 22099c1b56d7a7316e5d14c851cab094b866fd54 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 3 Jun 2022 11:07:40 +0200 Subject: [PATCH 112/179] Make some minor improvements to handling of tags and references --- novelwriter/core/index.py | 44 +++++++++++++------ sample/content/636b6aa9b697b.nwd | 2 +- sample/content/88706ddc78b1b.nwd | 1 + sample/content/ae7339df26ded.nwd | 1 + sample/content/b3e74dbc1f584.nwd | 2 +- .../coreIndex_LoadSave_tagsIndex.json | 16 +++---- 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index cdd78032..aba00ba5 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -227,7 +227,7 @@ class NWIndex: """Scan a piece of text associated with a handle. This will update the indices accordingly. This function takes the handle and text as separate inputs as we want to primarily scan the - files before we save them in which case we already have the + files before we save them, in which case we already have the text. """ theItem = self.theProject.tree[tHandle] @@ -238,9 +238,8 @@ class NWIndex: logger.info("Not indexing non-file item '%s'", tHandle) return False - # Delete tags and create new item entry - for tTag in self._itemIndex.allItemTags(tHandle): - del self._tagsIndex[tTag] + # Keep a record of existing tags, and create a new item entry + itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), False) self._itemIndex.add(tHandle, theItem) # Run word counter for the whole text @@ -279,7 +278,7 @@ class NWIndex: nTitle = nLine elif aLine.startswith("@"): - self._indexKeyword(tHandle, aLine, nTitle, theItem.itemClass) + self._indexKeyword(tHandle, aLine, nTitle, theItem.itemClass, itemTags) elif aLine.startswith("%"): if nTitle > 0: @@ -300,6 +299,12 @@ class NWIndex: if nTitle == 0: self._indexWordCounts(tHandle, theText, nTitle) + # Prune no longer used tags + for tTag, isActive in itemTags.items(): + if not isActive: + logger.verbose("Deleting removed tag '%s'", tTag) + del self._tagsIndex[tTag] + # Update timestamps for index changes nowTime = round(time()) self._timeIndex = nowTime @@ -359,9 +364,11 @@ class NWIndex: self._itemIndex.setHeadingSynopsis(tHandle, sTitle, theText) return - def _indexKeyword(self, tHandle, aLine, nTitle, itemClass): + def _indexKeyword(self, tHandle, aLine, nTitle, itemClass, itemTags): """Validate and save the information about a reference to a tag - in another file. + in another file, or the setting of a tag in the file. A record + of active tags is updated so that no longer used tags can be + pruned later. """ isValid, theBits, _ = self.scanThis(aLine) if not isValid or len(theBits) < 2: @@ -374,8 +381,10 @@ class NWIndex: sTitle = f"T{nTitle:06d}" if theBits[0] == nwKeyWords.TAG_KEY: - self._tagsIndex.add(theBits[1], tHandle, sTitle, itemClass) - self._itemIndex.setHeadingTag(tHandle, sTitle, theBits[1]) + tagName = theBits[1] + self._tagsIndex.add(tagName, tHandle, sTitle, itemClass) + self._itemIndex.setHeadingTag(tHandle, sTitle, tagName) + itemTags[tagName] = True else: self._itemIndex.addHeadingReferences(tHandle, sTitle, theBits[1:], theBits[0]) @@ -884,6 +893,11 @@ class ItemIndex: class IndexItem: + """This object represents the index data of a project item (NWItem). + It holds a record of all the headings in the text, and the meta data + associated with each heading. It also holds a pointer to the project + item. + """ def __init__(self, tHandle, tItem): self._handle = tHandle @@ -1027,6 +1041,10 @@ class IndexItem: class IndexHeading: + """This object represents a section of text in a project item + associated with a single (valid) heading. It holds a separate record + of all references made under each heading. + """ def __init__(self, key, level="H0", title=""): self._key = key @@ -1148,7 +1166,7 @@ class IndexHeading: def packReferences(self): """Pack references into a dictionary for saving to cache. """ - return {key: list(value) for key, value in self._refs.items()} + return {key: ",".join(value) for key, value in self._refs.items()} def unpackData(self, data): """Unpack a heading entry from a dictionary. @@ -1170,9 +1188,9 @@ class IndexHeading: for tagKey, refTypes in data.items(): if not isinstance(tagKey, str): raise ValueError("itemIndex reference key must be a string") - if not isinstance(refTypes, list): - raise ValueError("itemIndex reference types must be a list") - for refType in refTypes: + if not isinstance(refTypes, str): + raise ValueError("itemIndex reference types must be a string") + for refType in refTypes.split(","): if refType in nwKeyWords.VALID_KEYS: self.addReference(tagKey, refType) else: diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 20a66690..a334e8ce 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -4,7 +4,7 @@ ### Making a Scene @pov: Jane -@char: John +@char: John, Jane @location: Earth A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. The scene document can be sorted after the chapter document, or as a child of the chapter. Both result in the same output in the end, so it is a matter of preference. diff --git a/sample/content/88706ddc78b1b.nwd b/sample/content/88706ddc78b1b.nwd index 0f140538..ceaddd29 100644 --- a/sample/content/88706ddc78b1b.nwd +++ b/sample/content/88706ddc78b1b.nwd @@ -11,6 +11,7 @@ ### Jane Cannot Find John @pov: Jane +@focus: John @location: Space Jane has been looking all over for John. He’s nowhere to be found on Earth, so Jane goes to space. diff --git a/sample/content/ae7339df26ded.nwd b/sample/content/ae7339df26ded.nwd index 1eb7a65d..8b53f816 100644 --- a/sample/content/ae7339df26ded.nwd +++ b/sample/content/ae7339df26ded.nwd @@ -4,6 +4,7 @@ ### We Found John! @pov: John +@focus: John @location: Mars Jane has been searching for a while, and she finally found John on Mars. He was indeed in space! What was he doing on Mars anyway? Well, it turns out, he was farming potatoes. diff --git a/sample/content/b3e74dbc1f584.nwd b/sample/content/b3e74dbc1f584.nwd index c980865e..bb88600b 100644 --- a/sample/content/b3e74dbc1f584.nwd +++ b/sample/content/b3e74dbc1f584.nwd @@ -6,4 +6,4 @@ @tag: Earth @location: Space -Third planet from the sun, fairly dense, and with lots of people on it. \ No newline at end of file +Third planet from the sun, fairly dense, and with lots of people on it. diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index fafdeb68..60c59d86 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -35,7 +35,7 @@ "T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "88243afbe5ed8": { @@ -45,7 +45,7 @@ "T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "f96ec11c6a3da": { @@ -55,7 +55,7 @@ "T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "846352075de7d": { @@ -70,7 +70,7 @@ "T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "eb103bc70c90c": { @@ -79,7 +79,7 @@ "T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "f8c0562e50f1b": { @@ -88,7 +88,7 @@ "T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "47666c91c7ccf": { @@ -97,7 +97,7 @@ "T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "4c4f28287af27": { @@ -106,7 +106,7 @@ "T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""} }, "references": { - "T000001": {"Main": ["@plot"]} + "T000001": {"Main": "@plot"} } }, "2426c6f0ca922": { From 63068f1018248b605d19c511b2fe1274c91186f5 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Jun 2022 13:17:24 +0200 Subject: [PATCH 113/179] Add full test coverage of index class --- novelwriter/constants.py | 10 +- novelwriter/core/index.py | 44 +++-- tests/test_core/test_core_index.py | 293 ++++++++++++++++++++++++++--- 3 files changed, 305 insertions(+), 42 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 30071606..dc62dbd3 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -34,7 +34,7 @@ def trConst(tString): return QCoreApplication.translate("Constant", tString) -class nwConst(): +class nwConst: # Date and Time Formats FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format @@ -48,7 +48,7 @@ class nwConst(): # END Class nwConst -class nwRegEx(): +class nwRegEx: FMT_EI = r"(?" + ## # Properties ## @@ -124,8 +125,7 @@ class NWIndex: logger.debug("Re-indexing item '%s'", tHandle) theDoc = NWDoc(self.theProject, tHandle) - theText = theDoc.readDocument() - self.scanText(tHandle, theText if theText is not None else "") + self.scanText(tHandle, theDoc.readDocument() or "") return True @@ -316,7 +316,7 @@ class NWIndex: return True ## - # Internal Indexers + # Internal Indexer Helpers ## def _indexTitle(self, tHandle, aLine, nTitle): @@ -613,11 +613,13 @@ class NWIndex: # =============================================================================================== # -# Indexer Objects +# The Tags Index Object # =============================================================================================== # class TagsIndex: - """A wrapper class that holds the reverse lookup tags index. + """A wrapper class that holds the reverse lookup tags index. This is + just a simple wrapper around a single dictionary to keep tighter + control of the keys. """ def __init__(self): @@ -719,8 +721,16 @@ class TagsIndex: # END Class TagsIndex +# =============================================================================================== # +# The Item Index Objects +# =============================================================================================== # + class ItemIndex: - """A wrapper object holding the indexed items. + """A wrapper object holding the indexed items. This is a warapper + class around a single storage dictionary with a set of utility + functions for setting and accessing the index data. Each indexed + item is stored in an IndexItem object, which again holds an + IndexHeading object for each header of the text. """ def __init__(self, theProject): @@ -896,7 +906,8 @@ class IndexItem: """This object represents the index data of a project item (NWItem). It holds a record of all the headings in the text, and the meta data associated with each heading. It also holds a pointer to the project - item. + item. The main heading level of the item is also held here since it + must be reset each time the item is re-indexed. """ def __init__(self, tHandle, tItem): @@ -912,7 +923,7 @@ class IndexItem: return def __repr__(self): - return f"" + return f"" ## # Properties @@ -1062,7 +1073,7 @@ class IndexHeading: return def __repr__(self): - return f"" + return f"" ## # Properties @@ -1165,8 +1176,11 @@ class IndexHeading: def packReferences(self): """Pack references into a dictionary for saving to cache. + Multiple types are packed into a sorted, comma separated string. + It is sorted to prevent creating unnecessary diffs as the order + of a set is not guaranteed. """ - return {key: ",".join(value) for key, value in self._refs.items()} + return {key: ",".join(sorted(list(value))) for key, value in self._refs.items()} def unpackData(self, data): """Unpack a heading entry from a dictionary. @@ -1189,7 +1203,7 @@ class IndexHeading: if not isinstance(tagKey, str): raise ValueError("itemIndex reference key must be a string") if not isinstance(refTypes, str): - raise ValueError("itemIndex reference types must be a string") + raise ValueError("itemIndex reference type must be a string") for refType in refTypes.split(","): if refType in nwKeyWords.VALID_KEYS: self.addReference(tagKey, refType) diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index b1233275..78361bb6 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -29,7 +29,7 @@ from mock import causeException from tools import buildTestProject, cmpFiles, writeFile from novelwriter.core.project import NWProject -from novelwriter.core.index import NWIndex, countWords +from novelwriter.core.index import NWIndex, countWords, TagsIndex from novelwriter.enum import nwItemClass, nwItemLayout @@ -46,6 +46,8 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir): assert theProject.openProject(nwLipsum) theIndex = NWIndex(theProject) + assert repr(theIndex) == "" + notIndexable = { "b3643d0f92e32": False, # Novel ROOT "45e6b01ca35c1": False, # Chapter One FOLDER @@ -761,6 +763,166 @@ def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd): # END Test testCoreIndex_ExtractData +@pytest.mark.core +def testCoreIndex_TagsIndex(): + """Check the TagsIndex class. + """ + tagsIndex = TagsIndex() + assert tagsIndex._tags == {} + + # Expected data + content = { + "Tag1": { + "handle": "0000000000001", + "heading": "T000001", + "class": nwItemClass.NOVEL.name, + }, + "Tag2": { + "handle": "0000000000002", + "heading": "T000002", + "class": nwItemClass.CHARACTER.name, + }, + "Tag3": { + "handle": "0000000000003", + "heading": "T000003", + "class": nwItemClass.PLOT.name, + }, + } + + # Add data + tagsIndex.add("Tag1", "0000000000001", "T000001", nwItemClass.NOVEL) + tagsIndex.add("Tag2", "0000000000002", "T000002", nwItemClass.CHARACTER) + tagsIndex.add("Tag3", "0000000000003", "T000003", nwItemClass.PLOT) + assert tagsIndex._tags == content + + # Get items + assert tagsIndex["Tag1"] == content["Tag1"] + assert tagsIndex["Tag2"] == content["Tag2"] + assert tagsIndex["Tag3"] == content["Tag3"] + assert tagsIndex["Tag4"] is None + + # Contains + assert "Tag1" in tagsIndex + assert "Tag2" in tagsIndex + assert "Tag3" in tagsIndex + assert "Tag4" not in tagsIndex + + # Read back handles + assert tagsIndex.tagHandle("Tag1") == "0000000000001" + assert tagsIndex.tagHandle("Tag2") == "0000000000002" + assert tagsIndex.tagHandle("Tag3") == "0000000000003" + assert tagsIndex.tagHandle("Tag4") is None + + # Read back headings + assert tagsIndex.tagHeading("Tag1") == "T000001" + assert tagsIndex.tagHeading("Tag2") == "T000002" + assert tagsIndex.tagHeading("Tag3") == "T000003" + assert tagsIndex.tagHeading("Tag4") == "T000000" + + # Read back classes + assert tagsIndex.tagClass("Tag1") == nwItemClass.NOVEL.name + assert tagsIndex.tagClass("Tag2") == nwItemClass.CHARACTER.name + assert tagsIndex.tagClass("Tag3") == nwItemClass.PLOT.name + assert tagsIndex.tagClass("Tag4") is None + + # Pack Data + assert tagsIndex.packData() == content + + # Delete the second key and a nomn-existant key + del tagsIndex["Tag2"] + del tagsIndex["Tag4"] + assert "Tag1" in tagsIndex + assert "Tag2" not in tagsIndex + assert "Tag3" in tagsIndex + assert "Tag4" not in tagsIndex + + # Clear and reload + tagsIndex.clear() + assert tagsIndex._tags == {} + assert tagsIndex.packData() == {} + + tagsIndex.unpackData(content) + assert tagsIndex._tags == content + assert tagsIndex.packData() == content + + # Unpack Errors + # ============= + tagsIndex.clear() + + # Invalid data type + with pytest.raises(ValueError): + tagsIndex.unpackData([]) + + # Invalid key + with pytest.raises(ValueError): + tagsIndex.unpackData({ + 1234: { + "handle": "0000000000001", + "heading": "T000001", + "class": "NOVEL", + } + }) + + # Missing handle + with pytest.raises(KeyError): + tagsIndex.unpackData({ + "Tag1": { + "heading": "T000001", + "class": "NOVEL", + } + }) + + # Missing heading + with pytest.raises(KeyError): + tagsIndex.unpackData({ + "Tag1": { + "handle": "0000000000001", + "class": "NOVEL", + } + }) + + # Missing class + with pytest.raises(KeyError): + tagsIndex.unpackData({ + "Tag1": { + "handle": "0000000000001", + "heading": "T000001", + } + }) + + # Invalid handle + with pytest.raises(ValueError): + tagsIndex.unpackData({ + "Tag1": { + "handle": "blablabla", + "heading": "T000001", + "class": "NOVEL", + } + }) + + # Invalid heading + with pytest.raises(ValueError): + tagsIndex.unpackData({ + "Tag1": { + "handle": "0000000000001", + "heading": "blabla", + "class": "NOVEL", + } + }) + + # Invalid class + with pytest.raises(ValueError): + tagsIndex.unpackData({ + "Tag1": { + "handle": "0000000000001", + "heading": "T000001", + "class": "blabla", + } + }) + +# END Test testCoreIndex_TagsIndex + + @pytest.mark.core def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd): """Check the ItemIndex class. @@ -780,26 +942,6 @@ def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd): assert cHandle not in itemIndex assert sHandle not in itemIndex - # Unpack Data - # =========== - - # Data must be dictionary - with pytest.raises(ValueError): - itemIndex.unpackData("stuff") - - # Keys must be valid handles - with pytest.raises(ValueError): - itemIndex.unpackData({"stuff": "more stuff"}) - - # Unknown keys should be skipped - itemIndex.unpackData({"0000000000000": {}}) - assert itemIndex._items == {} - - # Known keys can be added, even witout data - itemIndex.unpackData({nHandle: {}}) - assert nHandle in itemIndex - itemIndex.clear() - # Add Items # ========= assert cHandle not in itemIndex @@ -820,7 +962,7 @@ def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd): # Set the remainig data values itemIndex.setHeadingCounts(cHandle, "T000001", 60, 10, 2) itemIndex.setHeadingSynopsis(cHandle, "T000001", "In the beginning ...") - itemIndex.setHeadingTag(cHandle, "T000001", "One") # Although it isn't allowed to have a tag + itemIndex.setHeadingTag(cHandle, "T000001", "One") itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@pov") itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@focus") itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane", "John"], "@char") @@ -842,6 +984,37 @@ def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd): itemIndex.addItemHeading(nHandle, "T000001", "H1", "Novel") itemIndex.addItemHeading(sHandle, "T000001", "H3", "Scene One") + # Check Item and Heading Direct Access + # ==================================== + + # Check repr strings + assert repr(itemIndex[nHandle]) == f"" + assert repr(itemIndex[nHandle]["T000001"]) == "" + + # Check content of a single item + assert "T000001" in itemIndex[nHandle] + assert itemIndex[cHandle].allTags() == ["One"] + + # Check the content of a single heading + assert itemIndex[cHandle]["T000001"].key == "T000001" + assert itemIndex[cHandle]["T000001"].level == "H2" + assert itemIndex[cHandle]["T000001"].title == "Chapter One" + assert itemIndex[cHandle]["T000001"].tag == "One" + assert itemIndex[cHandle]["T000001"].charCount == 60 + assert itemIndex[cHandle]["T000001"].wordCount == 10 + assert itemIndex[cHandle]["T000001"].paraCount == 2 + assert itemIndex[cHandle]["T000001"].synopsis == "In the beginning ..." + assert "Jane" in itemIndex[cHandle]["T000001"].references + assert "John" in itemIndex[cHandle]["T000001"].references + + # Check heading level setter + itemIndex[cHandle]["T000001"].setLevel("H3") # Change it + assert itemIndex[cHandle]["T000001"].level == "H3" + itemIndex[cHandle]["T000001"].setLevel("H2") # Set it back + assert itemIndex[cHandle]["T000001"].level == "H2" + itemIndex[cHandle]["T000001"].setLevel("H5") # Invalid level + assert itemIndex[cHandle]["T000001"].level == "H2" + # Data Extraction # =============== @@ -908,6 +1081,82 @@ def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd): del itemIndex[uHandle] assert uHandle not in itemIndex + # Unpack Error Handling + # ===================== + + # Pack/unpack should restore state + content = itemIndex.packData() + itemIndex.clear() + itemIndex.unpackData(content) + assert itemIndex.packData() == content + itemIndex.clear() + + # Data must be dictionary + with pytest.raises(ValueError): + itemIndex.unpackData("stuff") + + # Keys must be valid handles + with pytest.raises(ValueError): + itemIndex.unpackData({"stuff": "more stuff"}) + + # Unknown keys should be skipped + itemIndex.unpackData({"0000000000000": {}}) + assert itemIndex._items == {} + + # Known keys can be added, even witout data + itemIndex.unpackData({nHandle: {}}) + assert nHandle in itemIndex + + # Title tags must be valid + with pytest.raises(ValueError): + itemIndex.unpackData({cHandle: {"headings": {"TTTTTTT": {}}}}) + + # Reference without a heading should be rejected + itemIndex.unpackData({ + cHandle: { + "headings": {"T000001": {}}, + "references": {"T000001": {}, "T000002": {}}, + } + }) + assert "T000001" in itemIndex[cHandle] + assert "T000002" not in itemIndex[cHandle] + itemIndex.clear() + + # Tag keys must be strings + with pytest.raises(ValueError): + itemIndex.unpackData({ + cHandle: { + "headings": {"T000001": {}}, + "references": {"T000001": {1234: "@pov"}}, + } + }) + + # Type must be strings + with pytest.raises(ValueError): + itemIndex.unpackData({ + cHandle: { + "headings": {"T000001": {}}, + "references": {"T000001": {"John": []}}, + } + }) + + # Types must be valid + with pytest.raises(ValueError): + itemIndex.unpackData({ + cHandle: { + "headings": {"T000001": {}}, + "references": {"T000001": {"John": "@pov,@char,@stuff"}}, + } + }) + + # This should pass + itemIndex.unpackData({ + cHandle: { + "headings": {"T000001": {}}, + "references": {"T000001": {"John": "@pov,@char"}}, + } + }) + # END Test testCoreIndex_ItemIndex From de07546e3f1a92459646d52dd108f142dbbaa51e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Jun 2022 13:41:22 +0200 Subject: [PATCH 114/179] Fix tests --- tests/test_core/test_core_tohtml.py | 4 ---- tests/test_core/test_core_tomd.py | 4 ---- tests/test_core/test_core_toodt.py | 8 -------- tests/test_gui/test_gui_outline.py | 2 +- 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py index 12072e09..f21d5d78 100644 --- a/tests/test_core/test_core_tohtml.py +++ b/tests/test_core/test_core_tohtml.py @@ -25,7 +25,6 @@ import pytest from tools import readFile from novelwriter.core import NWProject, ToHtml -from novelwriter.core.index import NWIndex @pytest.mark.core @@ -33,7 +32,6 @@ def testCoreToHtml_ConvertFormat(mockGUI): """Test the tokenizer and converter chain using the ToHtml class. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theHtml = ToHtml(theProject) # Novel Files Headers @@ -236,7 +234,6 @@ def testCoreToHtml_ConvertDirect(mockGUI): """Test the converter directly using the ToHtml class. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theHtml = ToHtml(theProject) theHtml._isNovel = True @@ -607,7 +604,6 @@ def testCoreToHtml_Format(mockGUI): """Test all the formatters for the ToHtml class. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theHtml = ToHtml(theProject) # Export Mode diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py index c2235ff8..2e49d16b 100644 --- a/tests/test_core/test_core_tomd.py +++ b/tests/test_core/test_core_tomd.py @@ -25,7 +25,6 @@ import pytest from tools import readFile from novelwriter.core import NWProject, ToMarkdown -from novelwriter.core.index import NWIndex @pytest.mark.core @@ -33,7 +32,6 @@ def testCoreToMarkdown_ConvertFormat(mockGUI): """Test the tokenizer and converter chain using the ToMarkdown class. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theMD = ToMarkdown(theProject) # Headers @@ -162,7 +160,6 @@ def testCoreToMarkdown_ConvertDirect(mockGUI): """Test the converter directly using the ToMarkdown class. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theMD = ToMarkdown(theProject) theMD._isNovel = True @@ -267,7 +264,6 @@ def testCoreToMarkdown_Format(mockGUI): """Test all the formatters for the ToMarkdown class. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theMD = ToMarkdown(theProject) assert theMD._formatKeywords("", theMD.A_NONE) == "" diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index febbc94f..c714b5f5 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -29,7 +29,6 @@ from shutil import copyfile from tools import cmpFiles from novelwriter.core import NWProject, ToOdt -from novelwriter.core.index import NWIndex from novelwriter.core.toodt import ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag XML_NS = [ @@ -56,7 +55,6 @@ def testCoreToOdt_Init(mockGUI): """Test initialisation of the ODT document. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) # Flat Doc # ======== @@ -112,7 +110,6 @@ def testCoreToOdt_TextFormatting(mockGUI): """Test formatting of paragraphs. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theDoc = ToOdt(theProject, isFlat=True) theDoc.initDocument() @@ -234,7 +231,6 @@ def testCoreToOdt_Convert(mockGUI): """Test the converter of the ToOdt class. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theDoc = ToOdt(theProject, isFlat=True) theDoc._isNovel = True @@ -566,7 +562,6 @@ def testCoreToOdt_ConvertDirect(mockGUI): otherwise hard to reach conditions. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theDoc = ToOdt(theProject, isFlat=True) theDoc._isNovel = True @@ -621,7 +616,6 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir): """Test the document save functions. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theDoc = ToOdt(theProject, isFlat=True) theDoc._isNovel = True @@ -658,7 +652,6 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir): """Test the document save functions. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theDoc = ToOdt(theProject, isFlat=False) theDoc._isNovel = True @@ -738,7 +731,6 @@ def testCoreToOdt_Format(mockGUI): """Test the formatters for the ToOdt class. """ theProject = NWProject(mockGUI) - mockGUI.theIndex = NWIndex(theProject) theDoc = ToOdt(theProject, isFlat=True) assert theDoc._formatSynopsis("synopsis text") == ( diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 1a4617ff..f089928e 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -88,7 +88,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): # Click POV Link assert outlineData.povKeyValue.text() == "Bod" - outlineData._tagClicked("#pov=Bod") + nwGUI.projView._tagClicked("Bod") assert nwGUI.docViewer.docHandle() == "4c4f28287af27" # Scene One, Section Two From 2873336dce4cddb67541807d455160d0b56f9c8a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Jun 2022 15:04:04 +0200 Subject: [PATCH 115/179] Allow switching between root novel folders --- novelwriter/core/index.py | 48 +++++++++++------------------- novelwriter/gui/noveltree.py | 2 +- novelwriter/gui/outline.py | 21 +++++++++---- sample/content/a520879ca0b45.nwd | 17 +++++++++++ sample/content/bacb7059e3083.nwd | 8 +++++ sample/nwProject.nwx | 42 ++++++++++++++++---------- tests/test_core/test_core_index.py | 6 ++-- 7 files changed, 88 insertions(+), 56 deletions(-) create mode 100644 sample/content/a520879ca0b45.nwd create mode 100644 sample/content/bacb7059e3083.nwd diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 76a7ec55..ee9ea089 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -73,9 +73,8 @@ class NWIndex: self._indexBroken = False # TimeStamps - self._timeNovel = 0 - self._timeNotes = 0 - self._timeIndex = 0 + self._indexChange = 0 + self._rootChange = {} return @@ -99,9 +98,8 @@ class NWIndex: """ self._tagsIndex.clear() self._itemIndex.clear() - self._timeNovel = 0 - self._timeNotes = 0 - self._timeIndex = 0 + self._indexChange = 0 + self._rootChange = {} return def deleteHandle(self, tHandle): @@ -129,20 +127,16 @@ class NWIndex: return True - def novelChangedSince(self, checkTime): - """Check if the novel index has changed since a given time. - """ - return self._timeNovel > checkTime - - def notesChangedSince(self, checkTime): - """Check if the notes index has changed since a given time. - """ - return self._timeNotes > checkTime - def indexChangedSince(self, checkTime): """Check if the index has changed since a given time. """ - return self._timeIndex > checkTime + return self._indexChange > checkTime + + def rootChangedSince(self, rootHandle, checkTime): + """Check if the index has changed since a given time for a + given root item. + """ + return self._rootChange.get(rootHandle, self._indexChange) > checkTime ## # Load and Save Index to/from File @@ -184,10 +178,7 @@ class NWIndex: logger.warning("Item '%s' is not in the index", fHandle) self.reIndexHandle(fHandle) - nowTime = round(time()) - self._timeNovel = nowTime - self._timeNotes = nowTime - self._timeIndex = nowTime + self._indexChange = round(time()) logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) @@ -307,11 +298,8 @@ class NWIndex: # Update timestamps for index changes nowTime = round(time()) - self._timeIndex = nowTime - if theItem.itemLayout == nwItemLayout.NOTE: - self._timeNotes = nowTime - else: - self._timeNovel = nowTime + self._indexChange = nowTime + self._rootChange[theItem.itemRoot] = nowTime return True @@ -466,14 +454,14 @@ class NWIndex: # Extract Data ## - def novelStructure(self, skipExcl=True): + def novelStructure(self, rootHandle=None, skipExcl=True): """Iterate over all titles in the novel, in the correct order as they appear in the tree view and in the respective document files, but skipping all note files. """ - for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): - tKey = f"{tHandle}:{sTitle}" - yield tKey, tHandle, sTitle, hItem + novStruct = self._itemIndex.iterNovelStructure(rootHandle=rootHandle, skipExcl=skipExcl) + for tHandle, sTitle, hItem in novStruct: + yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem return def getNovelWordCount(self, skipExcl=True): diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index a1cd11d3..e5ad816e 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -136,7 +136,7 @@ class GuiNovelTree(QTreeWidget): """ logger.verbose("Requesting refresh of the novel tree") treeChanged = self.theParent.treeView.changedSince(self._lastBuild) - indexChanged = self.theProject.index.novelChangedSince(self._lastBuild) + indexChanged = self.theProject.index.indexChangedSince(self._lastBuild) if not (treeChanged or indexChanged or overRide): logger.verbose("No changes have been made to the novel index") return diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 58710584..ea919615 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -83,6 +83,7 @@ class GuiOutline(QWidget): self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns) self.outlineView.activeItemChanged.connect(self.outlineData.showItem) self.outlineData.itemTagClicked.connect(self._tagClicked) + self.outlineBar.novelRootChanged.connect(self._rootItemChanged) self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled) self.outlineBar.viewRefreshRequested.connect( lambda: self.outlineView.refreshTree(overRide=True) @@ -153,6 +154,13 @@ class GuiOutline(QWidget): self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW) return + @pyqtSlot(str) + def _rootItemChanged(self, handle): + """The root novel handle has been changed. + """ + self.outlineView.refreshTree(rootHandle=handle, overRide=True) + return + # END Class GuiOutline @@ -394,7 +402,7 @@ class GuiOutlineView(QTreeWidget): return - def refreshTree(self, overRide=False, novelChanged=False): + def refreshTree(self, rootHandle=None, overRide=False, novelChanged=False): """Called whenever the Outline tab is activated and controls what data to load, and if necessary, force a rebuild of the tree. @@ -402,17 +410,17 @@ class GuiOutlineView(QTreeWidget): # If it's the first time, we always build if self._firstView or self._firstView and overRide: self._loadHeaderState() - self._populateTree() + self._populateTree(rootHandle) self._firstView = False return # If the novel index or novel tree has changed since the tree # was last built, we rebuild the tree from the updated index. - indexChanged = self.theProject.index.novelChangedSince(self._lastBuild) + indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild) doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline if doBuild or overRide: logger.debug("Rebuilding Project Outline") - self._populateTree() + self._populateTree(rootHandle) return @@ -577,7 +585,7 @@ class GuiOutlineView(QTreeWidget): return - def _populateTree(self): + def _populateTree(self, rootHandle): """Build the tree based on the project index, and the header based on the defined constants, default values and user selected width, order and hidden state. All columns are populated, even @@ -610,7 +618,8 @@ class GuiOutlineView(QTreeWidget): currChapter = None currScene = None - for _, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True): + novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) + for _, tHandle, sTitle, novIdx in novStruct: tItem = self._createTreeItem(tHandle, sTitle, novIdx) diff --git a/sample/content/a520879ca0b45.nwd b/sample/content/a520879ca0b45.nwd new file mode 100644 index 00000000..12d7a62a --- /dev/null +++ b/sample/content/a520879ca0b45.nwd @@ -0,0 +1,17 @@ +%%~name: Chapter One +%%~path: e5e47ebf63b1c/a520879ca0b45 +%%~kind: NOVEL/DOCUMENT +### Chapter One + +@pov: Jane + +% Synopsis: Remember Jane and John? + +### Scene One + +@pov: Jane +@focus: John + +A project can have multiple novel root folders for multiple novels. This is the first scene of a sequel to the first novel. + +In this way, the writer can keep the same notes for multiple novels. This can be especially useful if the writer is planning a multi-novel story in advance. diff --git a/sample/content/bacb7059e3083.nwd b/sample/content/bacb7059e3083.nwd new file mode 100644 index 00000000..b6be7a07 --- /dev/null +++ b/sample/content/bacb7059e3083.nwd @@ -0,0 +1,8 @@ +%%~name: Title Page +%%~path: e5e47ebf63b1c/bacb7059e3083 +%%~kind: NOVEL/DOCUMENT +#! Sequel Novel + +>> **By Jane Doh** << + +% Synopsis: Jane and John are back in a sequel to My Novel! diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index cdd3ae50..9f3a6587 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1331 - 220 - 67108 + 1334 + 225 + 67746 False @@ -15,10 +15,10 @@ True None True - 636b6aa9b697b + a520879ca0b45 636b6aa9b697b - 1303 - 894 + 1363 + 954 409 B @@ -33,10 +33,10 @@
- New + New Notes Started - 1st Draft + 1st Draft 2nd Draft 3rd Draft Finished @@ -48,13 +48,13 @@ Main
- + Novel - + Title Page @@ -93,7 +93,19 @@ We Found John! - + + + Sequel + + + + Title Page + + + + Chapter One + + Characters @@ -109,7 +121,7 @@ Jane Smith - + Locations @@ -125,7 +137,7 @@ Mars - + Archive @@ -137,7 +149,7 @@ Old File - + Trash diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 78361bb6..8f126e56 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -197,8 +197,7 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): nItem = theProject.tree[nHandle] cItem = theProject.tree[cHandle] - assert theIndex.novelChangedSince(0) is False - assert theIndex.notesChangedSince(0) is False + assert theIndex.rootChangedSince("0000000000010", 0) is False assert theIndex.indexChangedSince(0) is False assert theIndex.scanText(cHandle, ( @@ -228,8 +227,7 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): "@time": [] } - assert theIndex.novelChangedSince(0) is True - assert theIndex.notesChangedSince(0) is True + assert theIndex.rootChangedSince("0000000000010", 0) is True assert theIndex.indexChangedSince(0) is True assert theIndex.getHandleHeaderLevel(cHandle) == "H1" From 88976d6800e1de38dfc2402853dad33161a2f808 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Jun 2022 17:25:39 +0200 Subject: [PATCH 116/179] Streamline how project item changes are reported --- novelwriter/gui/doceditor.py | 27 +++--- novelwriter/gui/docviewer.py | 25 +++--- novelwriter/gui/itemdetails.py | 9 +- novelwriter/gui/noveltree.py | 1 - novelwriter/gui/outline.py | 12 ++- novelwriter/gui/projtree.py | 124 +++++++++++++++------------- novelwriter/guimain.py | 41 +++------ novelwriter/tools/build.py | 1 - tests/test_gui/test_gui_projtree.py | 5 -- 9 files changed, 122 insertions(+), 123 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 9c080c96..5ede17f1 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -569,16 +569,6 @@ class GuiDocEditor(QTextEdit): return - def updateDocInfo(self, tHandle): - """Called when an item label is changed to check if the document - title bar needs updating, - """ - if tHandle == self._docHandle: - self.docHeader.setTitleFromHandle(self._docHandle) - self.docFooter.updateInfo() - self.updateDocMargins() - return - ## # Properties ## @@ -1068,7 +1058,22 @@ class GuiDocEditor(QTextEdit): return ## - # Slots + # Public Slots + ## + + @pyqtSlot(str) + def updateDocInfo(self, tHandle): + """Called when an item label is changed to check if the document + title bar needs updating, + """ + if tHandle == self._docHandle: + self.docHeader.setTitleFromHandle(self._docHandle) + self.docFooter.updateInfo() + self.updateDocMargins() + return + + ## + # Private Slots ## @pyqtSlot(int, int, int) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 313cce0d..89ae8ea2 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -322,15 +322,6 @@ class GuiDocViewer(QTextBrowser): return - def updateDocInfo(self, tHandle): - """Called when an item label is changed to check if the document - title bar needs updating, - """ - if tHandle == self._docHandle: - self.docHeader.setTitleFromHandle(self._docHandle) - self.updateDocMargins() - return - ## # Properties ## @@ -389,7 +380,21 @@ class GuiDocViewer(QTextBrowser): return 0 ## - # Slots + # Public Slots + ## + + @pyqtSlot(str) + def updateDocInfo(self, tHandle): + """Called when an item label is changed to check if the document + title bar needs updating, + """ + if tHandle == self._docHandle: + self.docHeader.setTitleFromHandle(self._docHandle) + self.updateDocMargins() + return + + ## + # Private Slots ## @pyqtSlot("QUrl") diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 89a38b76..643479a4 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -220,6 +220,11 @@ class GuiItemDetails(QWidget): """ self.updateViewBox(self._itemHandle) + ## + # Public Slots + ## + + @pyqtSlot(str) def updateViewBox(self, tHandle): """Populate the details box from a given handle. """ @@ -290,10 +295,6 @@ class GuiItemDetails(QWidget): return - ## - # Slots - ## - @pyqtSlot(str, int, int, int) def doUpdateCounts(self, tHandle, cC, wC, pC): """Update the counts if the handle is the same as the one we're diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index e5ad816e..9ac2ea2b 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -146,7 +146,6 @@ class GuiNovelTree(QTreeWidget): if selItem: titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2] - self.theParent.treeView.flushTreeOrder() self._populateTree() if titleKey is not None and titleKey in self._treeMap: diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index ea919615..20464485 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -126,17 +126,21 @@ class GuiOutline(QWidget): return self.outlineView.setFocus() ## - # Slots + # Public Slots ## - @pyqtSlot() - def projectUpdated(self): - """Should be called whenever the number of root folders change. + @pyqtSlot(str) + def updateRootItem(self, tHandle): + """Should be called whenever a root folders changes. """ self.outlineBar.populateNovelList() self.outlineData.updateClasses() return + ## + # Private Slots + ## + @pyqtSlot() def _updateMenuColumns(self): """Trigger an update of the toggled state of the column menu diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 833b8b87..b4f99421 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -32,11 +32,13 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame + QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, + QDialog ) from novelwriter.core import NWDoc from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert +from novelwriter.dialogs.itemeditor import GuiItemEditor logger = logging.getLogger(__name__) @@ -48,10 +50,10 @@ class GuiProjectTree(QTreeWidget): C_EXPORT = 2 C_STATUS = 3 - novelItemChanged = pyqtSignal() - noteItemChanged = pyqtSignal() + treeItemChanged = pyqtSignal(str) + novelItemChanged = pyqtSignal(str) + rootFolderChanged = pyqtSignal(str) wordCountsChanged = pyqtSignal() - rootFoldersChanged = pyqtSignal() def __init__(self, theParent): QTreeWidget.__init__(self, theParent) @@ -64,10 +66,9 @@ class GuiProjectTree(QTreeWidget): self.theProject = theParent.theProject # Internal Variables - self._treeMap = {} - self._treeChanged = False + self._treeMap = {} + self._lastMove = {} self._timeChanged = 0 - self._lastMove = {} ## # Build GUI @@ -157,15 +158,15 @@ class GuiProjectTree(QTreeWidget): """ self.clear() self._treeMap = {} - self._treeChanged = False + self._lastMove = {} self._timeChanged = 0 return def newTreeItem(self, itemType, itemClass=None): """Add new item to the tree, with a given itemType (and - itemClass if Root), and attach it to the selected handle. Also make - sure the item is added in a place it can be added, and that other - meta data is set correctly to ensure a valid project tree. + itemClass if Root), and attach it to the selected handle. Also + make sure the item is added in a place it can be added, and that + other meta data is set correctly to ensure a valid project tree. """ if not self.theParent.hasProject: logger.error("No project open") @@ -177,7 +178,6 @@ class GuiProjectTree(QTreeWidget): if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass): tHandle = self.theProject.newRoot(itemClass) - self.rootFoldersChanged.emit() elif itemType in (nwItemType.FILE, nwItemType.FOLDER): @@ -223,9 +223,9 @@ class GuiProjectTree(QTreeWidget): # Add the new item to the tree self.revealNewTreeItem(tHandle, nHandle) self.theParent.editItem(tHandle) - nwItem = self.theProject.tree[tHandle] - # If this is a folder, return here + # Handle new file creation + nwItem = self.theProject.tree[tHandle] if nwItem.itemType != nwItemType.FILE: return True @@ -268,7 +268,7 @@ class GuiProjectTree(QTreeWidget): if pHandle is not None and pHandle in self._treeMap: self._treeMap[pHandle].setExpanded(True) - self._emitItemChange(tHandle) + self._alertTreeChange(tHandle=tHandle, flush=True) self.clearSelection() trItem.setSelected(True) @@ -310,10 +310,31 @@ class GuiProjectTree(QTreeWidget): pItem.insertChild(nIndex, cItem) self._recordLastMove(cItem, pItem, tIndex) + self._alertTreeChange(tHandle=tHandle, flush=True) self.clearSelection() cItem.setSelected(True) - self._setTreeChanged(True) - self._emitItemChange(tHandle) + + return True + + def editTreeItem(self, tHandle=None): + """Open the edit item dialog. + """ + if tHandle is None: + logger.warning("No item selected") + return False + + tItem = self.theProject.tree[tHandle] + if tItem is None: + return False + if tItem.itemType == nwItemType.NO_TYPE: + return False + + logger.verbose("Requesting change to item '%s'", tHandle) + dlgProj = GuiItemEditor(self, tHandle) + dlgProj.exec_() + if dlgProj.result() == QDialog.Accepted: + self.setTreeItemValues(tHandle) + self._alertTreeChange(tHandle=tHandle, flush=False) return True @@ -330,16 +351,6 @@ class GuiProjectTree(QTreeWidget): self.theProject.setTreeOrder(theList) return True - def flushTreeOrder(self): - """Calls saveTreeOrder if there are unsaved changes, otherwise - does nothing. - """ - if self._treeChanged: - logger.verbose("Flushing project tree to project class") - self.saveTreeOrder() - self._setTreeChanged(False) - return - def getTreeFromHandle(self, tHandle): """Recursively return all the children items starting from a given item handle. @@ -411,7 +422,7 @@ class GuiProjectTree(QTreeWidget): self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True) if nTrash > 0: - self._setTreeChanged(True) + self._alertTreeChange(tHandle=trashHandle, flush=True) return True @@ -445,6 +456,7 @@ class GuiProjectTree(QTreeWidget): return False wCount = self._getItemWordCount(tHandle) + autoFlush = not bulkAction if nwItemS.itemType == nwItemType.ROOT: # Only an empty ROOT folder can be deleted logger.debug("User requested a root folder '%s' deleted", tHandle) @@ -452,8 +464,7 @@ class GuiProjectTree(QTreeWidget): if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) self._deleteTreeItem(tHandle) - self._setTreeChanged(True) - self.rootFoldersChanged.emit() + self._alertTreeChange(tHandle=tHandle, flush=True) else: self.theParent.makeAlert(self.tr( "Cannot delete root folder. It is not empty. " @@ -469,7 +480,7 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemP.takeChild(tIndex) self._deleteTreeItem(tHandle) - self._setTreeChanged(True) + self._alertTreeChange(tHandle=tHandle, flush=autoFlush) else: # A populated FOLDER or a FILE requires confirmtation @@ -505,7 +516,7 @@ class GuiProjectTree(QTreeWidget): self.theParent.closeDocument() self._deleteTreeItem(dHandle) - self._setTreeChanged(True) + self._alertTreeChange(tHandle=tHandle, flush=autoFlush) self.wordCountsChanged.emit() else: @@ -524,7 +535,7 @@ class GuiProjectTree(QTreeWidget): trItemT.addChild(trItemC) self._postItemMove(tHandle, wCount) self._recordLastMove(trItemS, trItemP, tIndex) - self._setTreeChanged(True) + self._alertTreeChange(tHandle=tHandle, flush=autoFlush) return True @@ -660,6 +671,7 @@ class GuiProjectTree(QTreeWidget): dstItem.insertChild(dstIndex, movItem) self._postItemMove(sHandle, wCount) + self._alertTreeChange(tHandle=sHandle, flush=True) self.clearSelection() movItem.setSelected(True) @@ -788,6 +800,7 @@ class GuiProjectTree(QTreeWidget): QTreeWidget.dropEvent(self, theEvent) self._postItemMove(sHandle, wCount) self._recordLastMove(sItem, pItem, pIndex) + self._alertTreeChange(tHandle=sHandle, flush=True) sItem.setExpanded(isExpanded) return @@ -829,8 +842,6 @@ class GuiProjectTree(QTreeWidget): # Trigger dependent updates self.propagateCount(tHandle, wCount) - self._setTreeChanged(True) - self._emitItemChange(tHandle) return True @@ -931,8 +942,6 @@ class GuiProjectTree(QTreeWidget): self.setTreeItemValues(tHandle) newItem.setExpanded(nwItem.isExpanded) - self._setTreeChanged(True) - return newItem def _addTrashRoot(self): @@ -945,33 +954,34 @@ class GuiProjectTree(QTreeWidget): trItem = self._getTreeItem(trashHandle) if trItem is None: - trItem = self._addTreeItem( - self.theProject.tree[trashHandle] - ) + trItem = self._addTreeItem(self.theProject.tree[trashHandle]) if trItem is not None: trItem.setExpanded(True) - self._setTreeChanged(True) + self._alertTreeChange(tHandle=trashHandle, flush=True) return trItem - def _setTreeChanged(self, theState): - """Set the tree change flag, and propagate to the project. + def _alertTreeChange(self, tHandle=None, flush=True): + """Update information on tree change state, and emit necessary + signals. """ - self._treeChanged = theState - if theState: - self._timeChanged = time() - self.theProject.setProjectChanged(True) - return + self._timeChanged = time() + self.theProject.setProjectChanged(True) + if flush: + self.saveTreeOrder() + + tItem = self.theProject.tree[tHandle] + if tItem is None: + return + + itemType = tItem.itemType + if itemType == nwItemType.ROOT: + self.rootFolderChanged.emit(tHandle) + elif itemType == nwItemType.FILE and tItem.isNovelLike(): + self.novelItemChanged.emit(tHandle) + + self.treeItemChanged.emit(tHandle) - def _emitItemChange(self, tHandle): - """Emit an item change signal for a given handle. - """ - if self.theProject.tree.checkType(tHandle, nwItemType.FILE): - nwItem = self.theProject.tree[tHandle] - if nwItem.isNovelLike(): - self.novelItemChanged.emit() - else: - self.noteItemChanged.emit() return def _recordLastMove(self, srcItem, parItem, parIndex): diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ad6dd742..c2051c6e 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -44,9 +44,8 @@ from novelwriter.gui import ( GuiViewsBar ) from novelwriter.dialogs import ( - GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences, - GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, GuiUpdates, - GuiWordList + GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails, + GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList ) from novelwriter.tools import ( GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats @@ -123,7 +122,10 @@ class GuiMain(QMainWindow): self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated) + self.treeView.treeItemChanged.connect(self.docEditor.updateDocInfo) + self.treeView.treeItemChanged.connect(self.docViewer.updateDocInfo) + self.treeView.treeItemChanged.connect(self.treeMeta.updateViewBox) + self.treeView.rootFolderChanged.connect(self.projView.updateRootItem) self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) @@ -357,7 +359,7 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.saveProject() self.docEditor.setDictionaries() - self.projView.projectUpdated() + self.projView.updateRootItem(None) self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -504,7 +506,7 @@ class GuiMain(QMainWindow): self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.statusBar.setRefTime(self.theProject.projOpened) - self.projView.projectUpdated() + self.projView.updateRootItem(None) self._updateStatusWordCount() # Restore previously open documents, if any @@ -596,7 +598,6 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - self.treeView.flushTreeOrder() nHandle = None # The next handle after tHandle fHandle = None # The first file handle we encounter foundIt = False # We've found tHandle, pick the next we see @@ -813,27 +814,10 @@ class GuiMain(QMainWindow): tHandle = self.docEditor.docHandle() else: tHandle = self.treeView.getSelectedHandle() + if tHandle: + return self.treeView.editTreeItem(tHandle) - if tHandle is None: - logger.warning("No item selected") - return False - - tItem = self.theProject.tree[tHandle] - if tItem is None: - return False - if tItem.itemType == nwItemType.NO_TYPE: - return False - - logger.verbose("Requesting change to item '%s'", tHandle) - dlgProj = GuiItemEditor(self, tHandle) - dlgProj.exec_() - if dlgProj.result() == QDialog.Accepted: - self.treeView.setTreeItemValues(tHandle) - self.treeMeta.updateViewBox(tHandle) - self.docEditor.updateDocInfo(tHandle) - self.docViewer.updateDocInfo(tHandle) - - return True + return False def rebuildTrees(self): """Rebuild the project tree. @@ -966,8 +950,6 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - self.treeView.flushTreeOrder() - dlgDetails = getGuiItem("GuiProjectDetails") if dlgDetails is None: dlgDetails = GuiProjectDetails(self) @@ -1583,7 +1565,6 @@ class GuiMain(QMainWindow): if self.mainStack.currentIndex() == self.idxOutlineView: logger.verbose("Novel tree changed while Outline tab active") if self.hasProject: - self.treeView.flushTreeOrder() self.projView.refreshView(novelChanged=True) return diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 06849ee9..66f203bd 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -709,7 +709,6 @@ class GuiBuildNovel(QDialog): bldObj.initDocument() # Make sure the project and document is up to date - self.theParent.treeView.flushTreeOrder() self.theParent.saveDocument() self.buildProgress.setMaximum(len(self.theProject.tree)) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 39e0b4dc..96e04da4 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -246,17 +246,14 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Move novel folder up assert nwTree.moveTreeItem(-1) is False - nwTree.flushTreeOrder() assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder down assert nwTree.moveTreeItem(1) is True - nwTree.flushTreeOrder() assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1 # Move novel folder up again assert nwTree.moveTreeItem(-1) is True - nwTree.flushTreeOrder() assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Clean up @@ -432,10 +429,8 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert nwTree.emptyTrash() is False # Empty the trash proper - nwTree._setTreeChanged(False) assert nwTree.emptyTrash() is True assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] - assert nwTree._treeChanged is True # Try to delete a file, but block the underlying deletion of the file on disk assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) From 92924b3288e6c9abf404e197d86b3ad34edf458b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Jun 2022 17:41:29 +0200 Subject: [PATCH 117/179] Make the new item function better at guessing header level of new files --- novelwriter/core/index.py | 5 +++++ novelwriter/gui/projtree.py | 11 ++++++++--- tests/test_core/test_core_index.py | 3 +++ tests/test_gui/test_gui_projtree.py | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index ee9ea089..f86de8c2 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -502,6 +502,11 @@ class NWIndex: """ return self._itemIndex.mainItemHeader(tHandle) + def getHandleHeaderIntLevel(self, tHandle): + """Get the integer header level of the first header of a handle. + """ + return H_LEVEL.get(self._itemIndex.mainItemHeader(tHandle), 0) + def getTableOfContents(self, maxDepth, skipExcl=True): """Generate a table of contents up to a maximum depth. """ diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index b4f99421..57981904 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -38,6 +38,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert +from novelwriter.common import minmax from novelwriter.dialogs.itemeditor import GuiItemEditor logger = logging.getLogger(__name__) @@ -188,9 +189,11 @@ class GuiProjectTree(QTreeWidget): ), nwAlert.ERROR) return False - # If the selected item is a file, the new item will be a sibling + # If the selected item is a file, the new item will be a + # sibling if the file has no children, otherwise a child pItem = self.theProject.tree[sHandle] - if pItem.itemType == nwItemType.FILE: + qItem = self._getTreeItem(sHandle) + if pItem.itemType == nwItemType.FILE and qItem.childCount() == 0: nHandle = sHandle sHandle = pItem.itemParent if sHandle is None: @@ -233,7 +236,9 @@ class GuiProjectTree(QTreeWidget): newDoc = NWDoc(self.theProject, tHandle) if not newDoc.readDocument(): if nwItem.itemLayout == nwItemLayout.DOCUMENT: - newText = f"### {nwItem.itemName}\n\n" + iLvl = self.theProject.index.getHandleHeaderIntLevel(sHandle) + hLvl = "#"*minmax(iLvl + 1, 2, 4) + newText = f"{hLvl} {nwItem.itemName}\n\n" else: newText = f"# {nwItem.itemName}\n\n" diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 8f126e56..3d40a9df 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -232,6 +232,9 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): assert theIndex.getHandleHeaderLevel(cHandle) == "H1" assert theIndex.getHandleHeaderLevel(nHandle) == "H1" + assert theIndex.getHandleHeaderIntLevel(cHandle) == 1 + assert theIndex.getHandleHeaderIntLevel(nHandle) == 1 + assert theIndex.getHandleHeaderIntLevel("stuff") == 0 # Zero Items assert theIndex.checkThese([], cItem) == [] diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 96e04da4..258ad150 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -96,7 +96,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL assert nwGUI.openDocument("0000000000013") - assert nwGUI.docEditor.getText() == "### New Document\n\n" + assert nwGUI.docEditor.getText() == "## New Document\n\n" # Add a new file to the characters folder nwTree.setSelectedHandle("000000000000a") From 85f63721e04c350d91a4341a6110f7a864e4ac06 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Jun 2022 18:43:09 +0200 Subject: [PATCH 118/179] Allow showing all novel files in Outline --- novelwriter/core/tree.py | 11 +++++------ novelwriter/gui/outline.py | 39 +++++++++++++++++++------------------ novelwriter/gui/projtree.py | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index d151f370..10a7a78f 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -273,14 +273,13 @@ class NWTree(): rootClasses.add(nwItem.itemClass) return rootClasses - def novelRoots(self): - """Return a doctionary of all novel-like root items. + def iterRoots(self, itemClass): + """Iterate over all items of a given class. """ - novelItems = {} for tHandle, nwItem in self._treeRoots.items(): - if nwItem.isNovelLike(): - novelItems[tHandle] = nwItem - return novelItems + if nwItem.itemClass == itemClass: + yield tHandle, nwItem + return def isRoot(self, tHandle): """Check if a handle is a root item. diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 20464485..61b35410 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -83,11 +83,8 @@ class GuiOutline(QWidget): self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns) self.outlineView.activeItemChanged.connect(self.outlineData.showItem) self.outlineData.itemTagClicked.connect(self._tagClicked) - self.outlineBar.novelRootChanged.connect(self._rootItemChanged) + self.outlineBar.loadNovelRootRequest.connect(self._rootItemChanged) self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled) - self.outlineBar.viewRefreshRequested.connect( - lambda: self.outlineView.refreshTree(overRide=True) - ) # Function Mappings self.getSelectedHandle = self.outlineView.getSelectedHandle @@ -160,9 +157,9 @@ class GuiOutline(QWidget): @pyqtSlot(str) def _rootItemChanged(self, handle): - """The root novel handle has been changed. + """The root novel handle has changed or needs to be refreshed. """ - self.outlineView.refreshTree(rootHandle=handle, overRide=True) + self.outlineView.refreshTree(rootHandle=(handle or None), overRide=True) return # END Class GuiOutline @@ -170,8 +167,7 @@ class GuiOutline(QWidget): class GuiOutlineToolBar(QToolBar): - novelRootChanged = pyqtSignal(str) - viewRefreshRequested = pyqtSignal() + loadNovelRootRequest = pyqtSignal(str) viewColumnToggled = pyqtSignal(bool, Enum) def __init__(self, theOutline): @@ -206,9 +202,7 @@ class GuiOutlineToolBar(QToolBar): # Actions self.aRefresh = QAction(self.tr("Refresh"), self) self.aRefresh.setIcon(self.theTheme.getIcon("refresh")) - self.aRefresh.triggered.connect( - lambda: self.viewRefreshRequested.emit() - ) + self.aRefresh.triggered.connect(self._refreshRequested) # Column Menu self.mColumns = GuiOutlineHeaderMenu(self) @@ -236,14 +230,14 @@ class GuiOutlineToolBar(QToolBar): ## def populateNovelList(self): - """Fill the novel combo box. + """Fill the novel combo box with a list of all novel folders. """ self.novelValue.clear() - for tHandle, nwItem in self.theProject.tree.novelRoots().items(): - self.novelValue.addItem( - self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]), - nwItem.itemName, tHandle - ) + tIcon = self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) + for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL): + self.novelValue.addItem(tIcon, nwItem.itemName, tHandle) + self.novelValue.insertSeparator(self.novelValue.count()) + self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "") return def setColumnHiddenState(self, hiddenState): @@ -253,7 +247,7 @@ class GuiOutlineToolBar(QToolBar): return ## - # Slots + # Private Slots ## @pyqtSlot(int) @@ -261,7 +255,14 @@ class GuiOutlineToolBar(QToolBar): """Emit a signal containing the handle of the selected item. """ if index >= 0: - self.novelRootChanged.emit(self.novelValue.currentData()) + self.loadNovelRootRequest.emit(self.novelValue.currentData()) + return + + @pyqtSlot() + def _refreshRequested(self): + """Emit a signal containing the handle of the selected item. + """ + self.loadNovelRootRequest.emit(self.novelValue.currentData()) return # END Class GuiOutlineToolBar diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 57981904..e0a8ef62 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -939,7 +939,7 @@ class GuiProjectTree(QTreeWidget): except Exception: logger.error("Failed to get index of item with handle '%s'", nHandle) if byIndex >= 0: - self._treeMap[pHandle].insertChild(byIndex+1, newItem) + self._treeMap[pHandle].insertChild(byIndex + 1, newItem) else: self._treeMap[pHandle].addChild(newItem) self.propagateCount(tHandle, nwItem.wordCount, countChildren=True) From 4d9b94cfe68d1ac1a6e240f1d52f93cbafd5a127 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Jun 2022 22:31:10 +0200 Subject: [PATCH 119/179] Improve test coverage of Outline --- novelwriter/gui/outline.py | 2 +- novelwriter/guimain.py | 2 +- tests/test_gui/test_gui_outline.py | 198 ++++++++++++++++++++++++++--- 3 files changed, 181 insertions(+), 21 deletions(-) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 61b35410..34cb35da 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -461,7 +461,7 @@ class GuiOutlineView(QTreeWidget): document editor. """ tHandle, tLine = self.getSelectedHandle() - self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True) + self.theParent.openDocument(tHandle, tLine=tLine - 1, doScroll=True) return @pyqtSlot() diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index c2051c6e..6a056dca 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1453,7 +1453,7 @@ class GuiMain(QMainWindow): return ## - # Slots + # Private Slots ## @pyqtSlot(str, Enum) diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index f089928e..49a86b16 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -19,17 +19,144 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import os +import time import pytest -from PyQt5.QtWidgets import QTreeWidgetItem, QMessageBox +from tools import buildTestProject, writeFile -keyDelay = 2 -typeDelay = 1 -stepDelay = 20 +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QWidget, QMessageBox, QAction + +from novelwriter.enum import nwItemClass, nwOutline, nwView @pytest.mark.gui -def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): +def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): + """Test the outline view. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + + # Create a project + prjDir = os.path.join(fncDir, "project") + buildTestProject(nwGUI, prjDir) + + nwGUI.rebuildIndex() + nwGUI._changeView(nwView.OUTLINE) + + outlineMain = nwGUI.projView + outlineView = outlineMain.outlineView + outlineData = outlineMain.outlineData + outlineMenu = outlineMain.outlineBar.mColumns + + # Toggle scrollbars + nwGUI.mainConf.hideVScroll = True + nwGUI.mainConf.hideHScroll = True + nwGUI.projView.initOutline() + assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff + assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff + assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff + assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff + + nwGUI.mainConf.hideVScroll = False + nwGUI.mainConf.hideHScroll = False + nwGUI.projView.initOutline() + assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded + assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded + assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded + assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded + + # Check focus + with monkeypatch.context() as mp: + mp.setattr(QWidget, "hasFocus", lambda *a: True) + assert outlineMain.treeFocus() is True + + outlineMain.setTreeFocus() # Can't check. just ensures that it doesn't error + + # Option State + # ============ + pOptions = nwGUI.theProject.options + colNames = [h.name for h in nwOutline] + colItems = [h for h in nwOutline] + colWidth = {h: outlineView.DEF_WIDTH[h] for h in nwOutline} + colHidden = {h: outlineView.DEF_HIDDEN[h] for h in nwOutline} + + assert outlineView.topLevelItemCount() > 0 + + # Save header state not allowed + outlineView._lastBuild = 0 + outlineView._saveHeaderState() + assert pOptions.getValue("GuiOutline", "headerOrder", []) == [] + + # Allow saving header state + outlineView._lastBuild = time.time() + outlineView._saveHeaderState() + assert pOptions.getValue("GuiOutline", "headerOrder", []) == colNames + assert outlineView._treeOrder == colItems + assert outlineView._colWidth == colWidth + assert outlineView._colHidden == colHidden + + # Get default values + optItems = pOptions.getValue("GuiOutline", "headerOrder", []) + optWidth = pOptions.getValue("GuiOutline", "columnWidth", {}) + optHidden = pOptions.getValue("GuiOutline", "columnHidden", {}) + + # Add invalid column name + pOptions.setValue("GuiOutline", "headerOrder", optItems + ["blabla"]) + outlineView._loadHeaderState() + assert outlineView._treeOrder == colItems + assert outlineView._colHidden == colHidden + + # Add duplicate column name + pOptions.setValue("GuiOutline", "headerOrder", optItems + [optItems[-1]]) + outlineView._loadHeaderState() + assert outlineView._treeOrder == colItems + assert outlineView._colHidden == colHidden + + # Invalid column width data + pOptions.setValue("GuiOutline", "headerOrder", optItems) + pOptions.setValue("GuiOutline", "columnWidth", {"blabla": None}) + outlineView._loadHeaderState() + assert outlineView._treeOrder == colItems + assert outlineView._colHidden == colHidden + + # Invalid column width data + pOptions.setValue("GuiOutline", "headerOrder", optItems) + pOptions.setValue("GuiOutline", "columnWidth", optWidth) + pOptions.setValue("GuiOutline", "columnHidden", {"bloabla": None}) + outlineView._loadHeaderState() + assert outlineView._treeOrder == colItems + assert outlineView._colHidden == colHidden + + # Valid settings + pOptions.setValue("GuiOutline", "headerOrder", optItems) + pOptions.setValue("GuiOutline", "columnWidth", optWidth) + pOptions.setValue("GuiOutline", "columnHidden", optHidden) + outlineView._loadHeaderState() + assert outlineView._treeOrder == colItems + assert outlineView._colHidden == colHidden + + # Header Menu + # =========== + + # Trigger the menu entry for all hidden columns + for hItem in nwOutline: + if outlineView.DEF_HIDDEN[hItem]: + outlineMenu.actionMap[hItem].activate(QAction.Trigger) + + # Now no columns should be hidden + outlineView._saveHeaderState() + assert not any(pOptions.getValue("GuiOutline", "columnHidden", None).values()) + + # qtbot.stop() + +# END Test testGuiOutline_Main + + +@pytest.mark.gui +def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): """Test the outline view. """ # Block message box @@ -40,26 +167,59 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.mainConf.lastPath = nwLipsum nwGUI.rebuildIndex() - nwGUI.mainStack.setCurrentIndex(nwGUI.idxOutlineView) + nwGUI._changeView(nwView.OUTLINE) - outlineView = nwGUI.projView.outlineView - outlineData = nwGUI.projView.outlineData + outlineMain = nwGUI.projView + outlineBar = outlineMain.outlineBar + outlineView = outlineMain.outlineView + outlineData = outlineMain.outlineData - assert outlineView.topLevelItemCount() > 0 + lipHandle = "b3643d0f92e32" - # Context Menu - # outlineView._headerRightClick(QPoint(1, 1)) - # outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger) - # outlineView.headerMenu.close() - # qtbot.mouseClick(outlineView, Qt.LeftButton) + # Check defaults in dropdown list + assert outlineBar.novelValue.itemData(0) == lipHandle + assert outlineBar.novelValue.itemData(1) is None # Separator + assert outlineBar.novelValue.itemData(2) == "" # All novels - # outlineView._loadHeaderState() - # assert not outlineView._colHidden[nwOutline.CCOUNT] + # Add a second novel folder + newHandle = nwGUI.theProject.newRoot(nwItemClass.NOVEL) + nwGUI.treeView.revealNewTreeItem(newHandle) + + # Check new values in dropdown list + assert outlineBar.novelValue.itemData(0) == lipHandle + assert outlineBar.novelValue.itemData(1) == newHandle + assert outlineBar.novelValue.itemData(2) is None # Separator + assert outlineBar.novelValue.itemData(3) == "" # All novels + + # Add a bunch of files in a header order that hits all tree combos + docList = [ + ("Section 1", 4), ("Scene 1", 3), ("Chapter 1", 2), ("Part 1", 1), + ("Section 2", 4), ("Scene 2", 3), ("Chapter 2", 2), + ("Section 3", 4), ("Scene 3", 3), + ("Section 4", 4), + ] + for dTitle, hLevel in docList: + aHandle = nwGUI.theProject.newFile(dTitle, newHandle) + hHash = "#"*hLevel + writeFile(os.path.join(nwLipsum, "content", f"{aHandle}.nwd"), f"{hHash} {dTitle}\n\n") + nwGUI.treeView.revealNewTreeItem(aHandle) + + nwGUI.rebuildIndex() + + # Build the second novel + outlineBar.novelValue.setCurrentIndex(1) + outlineBar._refreshRequested() + + # Go back to Lipsum + outlineBar.novelValue.setCurrentIndex(0) + outlineBar._refreshRequested() + + # Check Details + # ============= # First Item outlineView.refreshTree() selItem = outlineView.topLevelItem(0) - assert isinstance(selItem, QTreeWidgetItem) outlineView.setCurrentItem(selItem) assert outlineData.titleLabel.text() == "Title" @@ -110,6 +270,6 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): outlineView._treeDoubleClick(selItem, 0) assert nwGUI.docEditor.docHandle() == "88243afbe5ed8" - # qtbot.stopForInteraction() + # qtbot.stop() -# END Test testGuiOutline_Main +# END Test testGuiOutline_Content From 2f81aef0a3608835fd6e67585362f627f3135681 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Jun 2022 22:42:25 +0200 Subject: [PATCH 120/179] Set build language for Open Document files --- novelwriter/tools/build.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 66f203bd..b27a98a4 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -659,6 +659,7 @@ class GuiBuildNovel(QDialog): fmtUnnumbered = self.fmtUnnumbered.text() fmtScene = self.fmtScene.text() fmtSection = self.fmtSection.text() + buildLang = self.buildLang.currentData() hideScene = self.hideScene.isChecked() hideSection = self.hideSection.isChecked() textFont = self.textFont.text() @@ -676,7 +677,7 @@ class GuiBuildNovel(QDialog): replaceUCode = self.replaceUCode.isChecked() # The language lookup dict is reloaded if needed - self.theProject.setProjectLang(self.buildLang.currentData()) + self.theProject.setProjectLang(buildLang) # Get font information fontInfo = QFontInfo(QFont(textFont, textSize)) @@ -706,6 +707,7 @@ class GuiBuildNovel(QDialog): if isOdt: bldObj.setColourHeaders(not noStyling) + bldObj.setLanguage(buildLang) bldObj.initDocument() # Make sure the project and document is up to date From 669eabc7e88696a31339d30ea5252a294d96823e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 13:46:54 +0200 Subject: [PATCH 121/179] Handle actions in the project tree internally in the class --- novelwriter/gui/projtree.py | 98 ++++++++++++++++++++++++------------- novelwriter/guimain.py | 83 +++++++++++++------------------ 2 files changed, 99 insertions(+), 82 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index e0a8ef62..4a069f42 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -27,6 +27,7 @@ along with this program. If not, see . import logging import novelwriter +from enum import Enum from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot @@ -37,7 +38,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.core import NWDoc -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert +from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.common import minmax from novelwriter.dialogs.itemeditor import GuiItemEditor @@ -51,11 +52,16 @@ class GuiProjectTree(QTreeWidget): C_EXPORT = 2 C_STATUS = 3 + # Signals triggered when the meta data values of items change treeItemChanged = pyqtSignal(str) novelItemChanged = pyqtSignal(str) rootFolderChanged = pyqtSignal(str) wordCountsChanged = pyqtSignal() + # Signals for user interaction with the project tree + selectedItemChanged = pyqtSignal(str) + openDocumentRequest = pyqtSignal(str, Enum) + def __init__(self, theParent): QTreeWidget.__init__(self, theParent) @@ -127,6 +133,10 @@ class GuiProjectTree(QTreeWidget): # The last column should just auto-scale self.resizeColumnToContents(self.C_STATUS) + # Connect signals + self.itemDoubleClicked.connect(self._treeDoubleClick) + self.itemSelectionChanged.connect(self._treeSelectionChange) + # Set custom settings self.initTree() @@ -223,9 +233,9 @@ class GuiProjectTree(QTreeWidget): if tHandle is None: # pragma: no cover return True - # Add the new item to the tree + # Add the new item to the tree and open the editor dialog self.revealNewTreeItem(tHandle, nHandle) - self.theParent.editItem(tHandle) + self.editTreeItem(tHandle) # Handle new file creation nwItem = self.theProject.tree[tHandle] @@ -249,10 +259,7 @@ class GuiProjectTree(QTreeWidget): pIndex.scanText(tHandle, newText) # Get Word Counts - cC, wC, pC = pIndex.getCounts(tHandle) - nwItem.setCharCount(cC) - nwItem.setWordCount(wC) - nwItem.setParaCount(pC) + _, wC, _ = pIndex.getCounts(tHandle) self.propagateCount(tHandle, wC) self.wordCountsChanged.emit() @@ -280,8 +287,8 @@ class GuiProjectTree(QTreeWidget): return True def moveTreeItem(self, nStep): - """Move an item up or down in the tree, but only if the treeView - has focus. This also applies when the menu is used. + """Move an item up or down in the tree, but only if the project + tree has focus. This also applies when the menu is used. """ if not self.theParent.hasProject: logger.error("No project open") @@ -321,13 +328,9 @@ class GuiProjectTree(QTreeWidget): return True - def editTreeItem(self, tHandle=None): + def editTreeItem(self, tHandle): """Open the edit item dialog. """ - if tHandle is None: - logger.warning("No item selected") - return False - tItem = self.theProject.tree[tHandle] if tItem is None: return False @@ -357,8 +360,8 @@ class GuiProjectTree(QTreeWidget): return True def getTreeFromHandle(self, tHandle): - """Recursively return all the children items starting from a - given item handle. + """Recursively return all the child items starting from a given + item handle. """ theList = [] theItem = self._getTreeItem(tHandle) @@ -366,14 +369,6 @@ class GuiProjectTree(QTreeWidget): theList = self._scanChildren(theList, theItem, 0) return theList - def toggleExpanded(self, tHandle): - """Expand an item based on its handle. - """ - trItem = self._getTreeItem(tHandle) - if trItem is not None: - trItem.setExpanded(not trItem.isExpanded()) - return - def getColumnSizes(self): """Return the column widths for the tree columns. """ @@ -719,9 +714,52 @@ class GuiProjectTree(QTreeWidget): return self._timeChanged > checkTime ## - # Slots + # Public Solts ## + @pyqtSlot(str, int, int, int) + def doUpdateCounts(self, tHandle, cCount, wCount, pCount): + """Slot for updating the word count of a specific item. + """ + self.propagateCount(tHandle, wCount, countChildren=True) + self.wordCountsChanged.emit() + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _treeSelectionChange(self): + """The user changed which item is selected. + """ + tHandle = self.getSelectedHandle() + if tHandle is not None: + self.selectedItemChanged.emit(tHandle) + return + + @pyqtSlot("QTreeWidgetItem*", int) + def _treeDoubleClick(self, tItem, colNo): + """Capture a double-click event and either request the document + for editing if it is a file, or expand/close the node it is not. + """ + tHandle = self.getSelectedHandle() + if tHandle is None: + return + + tItem = self.theProject.tree[tHandle] + if tItem is None: + return + + if tItem.itemType == nwItemType.FILE: + self.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + else: + trItem = self._getTreeItem(tHandle) + if trItem is not None: + trItem.setExpanded(not trItem.isExpanded()) + + return + @pyqtSlot("QPoint") def _rightClickMenu(self, clickPos): """The user right clicked an element in the project tree, so we @@ -739,14 +777,6 @@ class GuiProjectTree(QTreeWidget): return - @pyqtSlot(str, int, int, int) - def doUpdateCounts(self, tHandle, cCount, wCount, pCount): - """Slot for updating the word count of a specific item. - """ - self.propagateCount(tHandle, wCount, countChildren=True) - self.wordCountsChanged.emit() - return - ## # Events ## @@ -774,7 +804,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.theParent.viewDocument(tHandle) + self.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6a056dca..354edbf4 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -115,28 +115,6 @@ class GuiMain(QMainWindow): self.mainMenu = GuiMainMenu(self) self.viewsBar = GuiViewsBar(self) - # Connect Signals Between Main Elements - self.viewsBar.viewChangeRequested.connect(self._changeView) - - self.treeView.itemSelectionChanged.connect(self._treeSingleClick) - self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) - self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) - self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - self.treeView.treeItemChanged.connect(self.docEditor.updateDocInfo) - self.treeView.treeItemChanged.connect(self.docViewer.updateDocInfo) - self.treeView.treeItemChanged.connect(self.treeMeta.updateViewBox) - self.treeView.rootFolderChanged.connect(self.projView.updateRootItem) - - self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) - self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) - self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts) - self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts) - self.docEditor.loadDocumentTagRequest.connect(self._followTag) - - self.docViewer.loadDocumentTagRequest.connect(self._followTag) - - self.projView.loadDocumentTagRequest.connect(self._followTag) - # Project Tree Stack self.projStack = QStackedWidget() self.projStack.addWidget(self.treeView) @@ -214,6 +192,30 @@ class GuiMain(QMainWindow): self.setStatusBar(self.statusBar) self.addToolBar(Qt.LeftToolBarArea, self.viewsBar) + # Connect Signals + # =============== + + self.viewsBar.viewChangeRequested.connect(self._changeView) + + self.treeView.selectedItemChanged.connect(self.treeMeta.updateViewBox) + self.treeView.openDocumentRequest.connect(self._openDocument) + self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) + self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) + self.treeView.treeItemChanged.connect(self.docEditor.updateDocInfo) + self.treeView.treeItemChanged.connect(self.docViewer.updateDocInfo) + self.treeView.treeItemChanged.connect(self.treeMeta.updateViewBox) + self.treeView.rootFolderChanged.connect(self.projView.updateRootItem) + + self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) + self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) + self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts) + self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts) + self.docEditor.loadDocumentTagRequest.connect(self._followTag) + + self.docViewer.loadDocumentTagRequest.connect(self._followTag) + + self.projView.loadDocumentTagRequest.connect(self._followTag) + # Finalise Initialisation # ======================= @@ -1468,6 +1470,17 @@ class GuiMain(QMainWindow): self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}") return + @pyqtSlot(str, Enum) + def _openDocument(self, tHandle, tMode): + """Handle an open document request. + """ + if tHandle is not None: + if tMode == nwDocMode.EDIT: + self.openDocument(tHandle, changeFocus=False) + elif tMode == nwDocMode.VIEW: + self.viewDocument(tHandle=tHandle) + return + @pyqtSlot(nwView) def _changeView(self, view): """Handle the requested change of view from the GuiViewBar. @@ -1531,32 +1544,6 @@ class GuiMain(QMainWindow): return - @pyqtSlot() - def _treeSingleClick(self): - """Single click on a project tree item just updates the details - panel below the tree. - """ - tHandle = self.treeView.getSelectedHandle() - if tHandle is not None: - self.treeMeta.updateViewBox(tHandle) - return - - @pyqtSlot("QTreeWidgetItem*", int) - def _treeDoubleClick(self, tItem, colNo): - """The user double-clicked an item in the tree. If it is a file, - we open it. Otherwise, we toggle the expanded status. - """ - tHandle = self.treeView.getSelectedHandle() - if tHandle is not None: - tItem = self.theProject.tree[tHandle] - if tItem is None: - return - if tItem.itemType == nwItemType.FILE: - self.openDocument(tHandle, changeFocus=False, doScroll=False) - else: - self.treeView.toggleExpanded(tHandle) - return - @pyqtSlot() def _treeNovelItemChanged(self): """Triggered when there is a change to a novel item in the From b88cdb767727b6868e9b144920f79bbfc04e4069 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 14:11:59 +0200 Subject: [PATCH 122/179] Make the project tree columns fixed --- novelwriter/gui/projtree.py | 47 ++++++++++--------------------------- novelwriter/guimain.py | 1 - 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 4a069f42..c6d077b8 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -34,7 +34,7 @@ from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, - QDialog + QDialog, QHeaderView ) from novelwriter.core import NWDoc @@ -88,28 +88,24 @@ class GuiProjectTree(QTreeWidget): # Tree Settings iPx = self.theTheme.baseIconSize + cMg = self.mainConf.pxInt(6) self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) self.setExpandsOnDoubleClick(False) + self.setHeaderHidden(True) self.setIndentation(iPx) self.setColumnCount(4) - self.setHeaderLabels([ - self.tr("Project Tree"), self.tr("Words"), "", "" - ]) - treeHeadItem = self.headerItem() - treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) - treeHeadItem.setToolTip(self.C_NAME, self.tr("Item label")) - treeHeadItem.setToolTip(self.C_COUNT, self.tr("Word count")) - treeHeadItem.setToolTip(self.C_EXPORT, self.tr("Include in build")) - treeHeadItem.setToolTip(self.C_STATUS, self.tr("Item status")) - - # Let the last column stretch, and set the minimum size to the - # size of the icon as the default Qt font metrics approach fails - # for some fonts like the Ubuntu font. + # Lock the column sizes treeHeader = self.header() - treeHeader.setStretchLastSection(True) - treeHeader.setMinimumSectionSize(iPx + 6) + treeHeader.setStretchLastSection(False) + treeHeader.setMinimumSectionSize(iPx + cMg) + treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.Stretch) + treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_EXPORT, QHeaderView.Fixed) + treeHeader.setSectionResizeMode(self.C_STATUS, QHeaderView.Fixed) + treeHeader.resizeSection(self.C_EXPORT, iPx + cMg) + treeHeader.resizeSection(self.C_STATUS, iPx + cMg) # Allow Move by Drag & Drop self.setDragEnabled(True) @@ -124,15 +120,6 @@ class GuiProjectTree(QTreeWidget): # self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) - # Get user's column width preferences for NAME and COUNT - treeColWidth = self.mainConf.getTreeColWidths() - if len(treeColWidth) <= 4: - for colN, colW in enumerate(treeColWidth): - self.setColumnWidth(colN, colW) - - # The last column should just auto-scale - self.resizeColumnToContents(self.C_STATUS) - # Connect signals self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._treeSelectionChange) @@ -369,16 +356,6 @@ class GuiProjectTree(QTreeWidget): theList = self._scanChildren(theList, theItem, 0) return theList - def getColumnSizes(self): - """Return the column widths for the tree columns. - """ - retVals = [ - self.columnWidth(0), - self.columnWidth(1), - self.columnWidth(2), - ] - return retVals - def emptyTrash(self): """Permanently delete all documents in the Trash folder. This function only asks for confirmation once, and calls the regular diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 354edbf4..7afd1675 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1161,7 +1161,6 @@ class GuiMain(QMainWindow): self.mainConf.setViewPanePos(self.splitView.sizes()) self.mainConf.setShowRefPanel(self.viewMeta.isVisible()) - self.mainConf.setTreeColWidths(self.treeView.getColumnSizes()) self.mainConf.setNovelColWidths(self.novelView.getColumnSizes()) if not self.mainConf.isFullScreen: self.mainConf.setWinSize(self.width(), self.height()) From d07f5dc520162c6228de07f726f5041e47665d02 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 15:50:46 +0200 Subject: [PATCH 123/179] Wrap project tree in an outer widget with a toolbar --- novelwriter/dialogs/preferences.py | 2 +- novelwriter/gui/__init__.py | 4 +- novelwriter/gui/itemdetails.py | 2 +- novelwriter/gui/outline.py | 3 + novelwriter/gui/projtree.py | 185 ++++++++++++++++++---- novelwriter/guimain.py | 20 +-- tests/test_dialogs/test_dlg_docmerge.py | 28 ++-- tests/test_dialogs/test_dlg_docsplit.py | 20 +-- tests/test_dialogs/test_dlg_itemeditor.py | 10 +- tests/test_gui/test_gui_docviewer.py | 6 +- tests/test_gui/test_gui_guimain.py | 49 +++--- tests/test_gui/test_gui_mainmenu.py | 6 +- tests/test_gui/test_gui_projtree.py | 20 +-- 13 files changed, 238 insertions(+), 117 deletions(-) diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index d38a4ab6..755b9198 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -107,7 +107,7 @@ class GuiPreferences(PagedDialog): ), nwAlert.INFO) if refreshTree: - self.theParent.treeView.buildTree() + self.theParent.treeView.populateTree() self._saveWindowSize() self.accept() diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index e3560a99..0ae23024 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -25,7 +25,7 @@ from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.mainmenu import GuiMainMenu from novelwriter.gui.noveltree import GuiNovelTree from novelwriter.gui.outline import GuiOutline -from novelwriter.gui.projtree import GuiProjectTree +from novelwriter.gui.projtree import GuiProjectWiew from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.theme import GuiTheme from novelwriter.gui.viewsbar import GuiViewsBar @@ -39,7 +39,7 @@ __all__ = [ "GuiMainStatus", "GuiNovelTree", "GuiOutline", - "GuiProjectTree", + "GuiProjectWiew", "GuiTheme", "GuiViewsBar", ] diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 643479a4..cf9e8017 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -296,7 +296,7 @@ class GuiItemDetails(QWidget): return @pyqtSlot(str, int, int, int) - def doUpdateCounts(self, tHandle, cC, wC, pC): + def updateCounts(self, tHandle, cC, wC, pC): """Update the counts if the handle is the same as the one we're already showing. Otherwise, do nothing. """ diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 34cb35da..65bffdbd 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -62,6 +62,7 @@ class GuiOutline(QWidget): self.mainConf = novelwriter.CONFIG self.theParent = theParent + # Build GUI self.outlineBar = GuiOutlineToolBar(self) self.outlineView = GuiOutlineView(self) self.outlineData = GuiOutlineDetails(self) @@ -225,6 +226,8 @@ class GuiOutlineToolBar(QToolBar): logger.debug("GuiOutlineToolBar initialisation complete") + return + ## # Methods ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index c6d077b8..7286f87f 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -6,6 +6,8 @@ GUI classes for the main window project tree File History: Created: 2018-09-29 [0.0.1] GuiProjectTree Created: 2020-06-04 [0.7] GuiProjectTreeMenu +Created: 2022-06-06 [1.7b1] GuiProjectWiew +Created: 2022-06-06 [1.7b1] GuiProjectToolBar This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -34,7 +36,8 @@ from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, - QDialog, QHeaderView + QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, QToolButton, + QSizePolicy ) from novelwriter.core import NWDoc @@ -45,12 +48,11 @@ from novelwriter.dialogs.itemeditor import GuiItemEditor logger = logging.getLogger(__name__) -class GuiProjectTree(QTreeWidget): - - C_NAME = 0 - C_COUNT = 1 - C_EXPORT = 2 - C_STATUS = 3 +class GuiProjectWiew(QWidget): + """This is a wrapper class holding all the elements of the project + tree. The core object is the project tree itself. Most methods + available are mapped through to the project tree class. + """ # Signals triggered when the meta data values of items change treeItemChanged = pyqtSignal(str) @@ -63,14 +65,143 @@ class GuiProjectTree(QTreeWidget): openDocumentRequest = pyqtSignal(str, Enum) def __init__(self, theParent): - QTreeWidget.__init__(self, theParent) + QWidget.__init__(self, theParent) + + self.theParent = theParent + + # Build GUI + self.projBar = GuiProjectToolBar(self) + self.projTree = GuiProjectTree(self) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.projBar) + self.outerBox.addWidget(self.projTree) + self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + # Function Mappings + self.newTreeItem = self.projTree.newTreeItem + self.revealNewTreeItem = self.projTree.revealNewTreeItem + self.moveTreeItem = self.projTree.moveTreeItem + self.editTreeItem = self.projTree.editTreeItem + self.getTreeFromHandle = self.projTree.getTreeFromHandle + self.emptyTrash = self.projTree.emptyTrash + self.deleteItem = self.projTree.deleteItem + self.setTreeItemValues = self.projTree.setTreeItemValues + self.propagateCount = self.projTree.propagateCount + self.undoLastMove = self.projTree.undoLastMove + self.getSelectedHandle = self.projTree.getSelectedHandle + self.setSelectedHandle = self.projTree.setSelectedHandle + self.changedSince = self.projTree.changedSince + + return + + ## + # Methods + ## + + def initSettings(self): + self.projTree.initSettings() + return + + def clearProject(self): + self.projTree.clearTree() + return + + def saveProjectTree(self): + self.projTree.saveTreeOrder() + return + + def populateTree(self): + self.projTree.buildTree() + return + + def treeFocus(self): + return self.projTree.hasFocus() + + ## + # Public Solts + ## + + @pyqtSlot(str, int, int, int) + def updateCounts(self, tHandle, cCount, wCount, pCount): + """Slot for updating the word count of a specific item. + """ + self.projTree.propagateCount(tHandle, wCount, countChildren=True) + self.wordCountsChanged.emit() + return + +# END Class GuiProjectWiew + + +class GuiProjectToolBar(QToolBar): + + def __init__(self, theWidget): + QTreeWidget.__init__(self, theWidget) + + logger.debug("Initialising GuiProjectToolBar ...") + + self.mainConf = novelwriter.CONFIG + self.theParent = theWidget.theParent + self.theProject = theWidget.theParent.theProject + self.theTheme = theWidget.theParent.theTheme + + iPx = self.theTheme.baseIconSize + mPx = self.mainConf.pxInt(12) + + self.setMovable(False) + self.setIconSize(QSize(iPx, iPx)) + self.setContentsMargins(0, 0, 0, 0) + self.setStyleSheet("QToolBar {border: 0px;}") + + # Novel Selector + self.projLabel = QLabel(self.tr("Project")) + self.projLabel.setContentsMargins(0, 0, mPx, 0) + self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Itemss Menu + self.tbItems = QToolButton(self) + self.tbItems.setIcon(self.theTheme.getIcon("add")) + self.tbItems.setPopupMode(QToolButton.InstantPopup) + + # Settings Menu + self.tbSettings = QToolButton(self) + self.tbSettings.setIcon(self.theTheme.getIcon("menu")) + self.tbSettings.setPopupMode(QToolButton.InstantPopup) + + # Assemble + self.addWidget(self.projLabel) + self.addSeparator() + self.addWidget(self.tbItems) + self.addWidget(self.tbSettings) + + logger.debug("GuiProjectToolBar initialisation complete") + + return + +# END Class GuiProjectToolBar + + +class GuiProjectTree(QTreeWidget): + + C_NAME = 0 + C_COUNT = 1 + C_EXPORT = 2 + C_STATUS = 3 + + def __init__(self, theWidget): + QTreeWidget.__init__(self, theWidget) logger.debug("Initialising GuiProjectTree ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.theWidget = theWidget + self.theParent = theWidget.theParent + self.theTheme = theWidget.theParent.theTheme + self.theProject = theWidget.theParent.theProject # Internal Variables self._treeMap = {} @@ -125,13 +256,13 @@ class GuiProjectTree(QTreeWidget): self.itemSelectionChanged.connect(self._treeSelectionChange) # Set custom settings - self.initTree() + self.initSettings() logger.debug("GuiProjectTree initialisation complete") return - def initTree(self): + def initSettings(self): """Set or update tree widget settings. """ # Scroll bars @@ -248,7 +379,7 @@ class GuiProjectTree(QTreeWidget): # Get Word Counts _, wC, _ = pIndex.getCounts(tHandle) self.propagateCount(tHandle, wC) - self.wordCountsChanged.emit() + self.theWidget.wordCountsChanged.emit() return True @@ -494,7 +625,7 @@ class GuiProjectTree(QTreeWidget): self._deleteTreeItem(dHandle) self._alertTreeChange(tHandle=tHandle, flush=autoFlush) - self.wordCountsChanged.emit() + self.theWidget.wordCountsChanged.emit() else: # The item is not already in the trash folder, so we @@ -690,18 +821,6 @@ class GuiProjectTree(QTreeWidget): """ return self._timeChanged > checkTime - ## - # Public Solts - ## - - @pyqtSlot(str, int, int, int) - def doUpdateCounts(self, tHandle, cCount, wCount, pCount): - """Slot for updating the word count of a specific item. - """ - self.propagateCount(tHandle, wCount, countChildren=True) - self.wordCountsChanged.emit() - return - ## # Private Slots ## @@ -712,7 +831,7 @@ class GuiProjectTree(QTreeWidget): """ tHandle = self.getSelectedHandle() if tHandle is not None: - self.selectedItemChanged.emit(tHandle) + self.theWidget.selectedItemChanged.emit(tHandle) return @pyqtSlot("QTreeWidgetItem*", int) @@ -729,7 +848,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + self.theWidget.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) else: trItem = self._getTreeItem(tHandle) if trItem is not None: @@ -781,7 +900,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + self.theWidget.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) return @@ -988,11 +1107,11 @@ class GuiProjectTree(QTreeWidget): itemType = tItem.itemType if itemType == nwItemType.ROOT: - self.rootFolderChanged.emit(tHandle) + self.theWidget.rootFolderChanged.emit(tHandle) elif itemType == nwItemType.FILE and tItem.isNovelLike(): - self.novelItemChanged.emit(tHandle) + self.theWidget.novelItemChanged.emit(tHandle) - self.treeItemChanged.emit(tHandle) + self.theWidget.treeItemChanged.emit(tHandle) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 7afd1675..d18d4d17 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, - GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectTree, GuiTheme, + GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectWiew, GuiTheme, GuiViewsBar ) from novelwriter.dialogs import ( @@ -105,7 +105,7 @@ class GuiMain(QMainWindow): # Main GUI Elements self.statusBar = GuiMainStatus(self) - self.treeView = GuiProjectTree(self) + self.treeView = GuiProjectWiew(self) self.novelView = GuiNovelTree(self) self.docEditor = GuiDocEditor(self) self.viewMeta = GuiDocViewDetails(self) @@ -208,8 +208,8 @@ class GuiMain(QMainWindow): self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) - self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts) - self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts) + self.docEditor.docCountsChanged.connect(self.treeMeta.updateCounts) + self.docEditor.docCountsChanged.connect(self.treeView.updateCounts) self.docEditor.loadDocumentTagRequest.connect(self._followTag) self.docViewer.loadDocumentTagRequest.connect(self._followTag) @@ -291,7 +291,7 @@ class GuiMain(QMainWindow): """Wrapper function to clear all sub-elements of the main GUI. """ # Project Area - self.treeView.clearTree() + self.treeView.clearProject() self.novelView.clearTree() self.treeMeta.clearDetails() @@ -541,7 +541,7 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - self.treeView.saveTreeOrder() + self.treeView.saveProjectTree() if self.theProject.saveProject(autoSave=autoSave): self.theProject.index.saveIndex() @@ -789,7 +789,7 @@ class GuiMain(QMainWindow): tHandle = None tLine = None - if self.treeView.hasFocus(): + if self.treeView.treeFocus(): tHandle = self.treeView.getSelectedHandle() elif self.novelView.hasFocus(): tHandle, tLine = self.novelView.getSelectedHandle() @@ -824,7 +824,7 @@ class GuiMain(QMainWindow): def rebuildTrees(self): """Rebuild the project tree. """ - self.treeView.buildTree() + self.treeView.populateTree() self.novelView.refreshTree() return @@ -847,7 +847,7 @@ class GuiMain(QMainWindow): qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) tStart = time() - self.treeView.saveTreeOrder() + self.treeView.saveProjectTree() self.theProject.index.clearIndex() for tItem in self.theProject.tree: @@ -919,7 +919,7 @@ class GuiMain(QMainWindow): self.saveDocument() self.docEditor.initEditor() self.docViewer.initViewer() - self.treeView.initTree() + self.treeView.initSettings() self.novelView.initTree() self.projView.initOutline() self._updateStatusWordCount() diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index a82c77bd..ce5c68d4 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -56,11 +56,11 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Add Project Content monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE) - nwGUI.treeView.newTreeItem(nwItemType.FILE) - nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -82,8 +82,8 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Open the Merge tool nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None) nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger) @@ -101,27 +101,27 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwMerge.listBox.count() == 0 # No item selected - nwGUI.treeView.clearSelection() + nwGUI.treeView.projTree.clearSelection() assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Non-existing item with monkeypatch.context() as mp: mp.setattr(NWTree, "__getitem__", lambda *a: None) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Select a non-folder - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterOne).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterOne).setSelected(True) assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Select the chapter folder - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwMerge._populateList() is True assert nwMerge.listBox.count() == 5 diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 90e3375d..0e3174c7 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -60,9 +60,9 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Add Project Content monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hNovelRoot).setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -89,8 +89,8 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Open the Split tool nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hToSplit).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hToSplit).setSelected(True) monkeypatch.setattr(GuiDocSplit, "exec_", lambda *a: None) nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger) @@ -109,7 +109,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # No item selected nwSplit.sourceItem = None - nwGUI.treeView.clearSelection() + nwGUI.treeView.projTree.clearSelection() assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 @@ -117,15 +117,15 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): with monkeypatch.context() as mp: mp.setattr(NWTree, "__getitem__", lambda *a: None) nwSplit.sourceItem = None - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hToSplit).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hToSplit).setSelected(True) assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 # Select a non-file nwSplit.sourceItem = None - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 95b1ee71..501d1e3b 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -25,10 +25,10 @@ from tools import getGuiItem, buildTestProject from PyQt5.QtWidgets import QAction, QDialog, QMessageBox -from novelwriter.gui import GuiProjectTree from novelwriter.enum import nwItemLayout, nwItemType from novelwriter.dialogs import GuiItemEditor from novelwriter.core.tree import NWTree +from novelwriter.gui.projtree import GuiProjectTree statusKeys = ["s000000", "s000001", "s000002", "s000003"] importKeys = ["i000004", "i000005", "i000006", "i000007"] @@ -52,7 +52,7 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): tHandle = "000000000000f" # No Selection - nwGUI.treeView.clearSelection() + nwGUI.treeView.projTree.clearSelection() assert nwGUI.editItem() is False # Force opening from editor @@ -163,9 +163,9 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.theProject.importItems.name(importKeys[1]) == "Minor" # Create Note - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) # Open Note assert nwGUI.openDocument("0000000000010") diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 1fa6b2c7..d1d8797a 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -54,9 +54,9 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.treeView.setSelectedHandle("88243afbe5ed8") # Middle-click the selected item - theItem = nwGUI.treeView._getTreeItem("88243afbe5ed8") - theRect = nwGUI.treeView.visualItemRect(theItem) - qtbot.mouseClick(nwGUI.treeView.viewport(), Qt.MidButton, pos=theRect.center()) + theItem = nwGUI.treeView.projTree._getTreeItem("88243afbe5ed8") + theRect = nwGUI.treeView.projTree.visualItemRect(theItem) + qtbot.mouseClick(nwGUI.treeView.projTree.viewport(), Qt.MidButton, pos=theRect.center()) assert nwGUI.docViewer.docHandle() == "88243afbe5ed8" # Reload the text diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 5f49d51d..78c2af66 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -28,11 +28,10 @@ from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QDialog -from novelwriter.gui import ( - GuiDocEditor, GuiProjectTree, GuiNovelTree, GuiOutline -) +from novelwriter.gui import GuiDocEditor, GuiNovelTree, GuiOutline from novelwriter.enum import nwItemType, nwWidget from novelwriter.tools import GuiProjectWizard +from novelwriter.gui.projtree import GuiProjectTree from novelwriter.dialogs.itemeditor import GuiItemEditor keyDelay = 2 @@ -126,7 +125,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): with monkeypatch.context() as mp: mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - nwGUI.treeView._getTreeItem(sHandle).setSelected(True) + nwGUI.treeView.projTree._getTreeItem(sHandle).setSelected(True) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True @@ -220,14 +219,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.spellCheck is False # Check that tree items have been created - assert nwGUI.treeView._getTreeItem("0000000000008") is not None - assert nwGUI.treeView._getTreeItem("0000000000009") is not None - assert nwGUI.treeView._getTreeItem("000000000000a") is not None - assert nwGUI.treeView._getTreeItem("000000000000b") is not None - assert nwGUI.treeView._getTreeItem("000000000000c") is not None - assert nwGUI.treeView._getTreeItem("000000000000d") is not None - assert nwGUI.treeView._getTreeItem("000000000000e") is not None - assert nwGUI.treeView._getTreeItem("000000000000f") is not None + assert nwGUI.treeView.projTree._getTreeItem("0000000000008") is not None + assert nwGUI.treeView.projTree._getTreeItem("0000000000009") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000a") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000b") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000c") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000d") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000e") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000f") is not None nwGUI.mainMenu.aSpellCheck.setChecked(True) assert nwGUI.mainMenu._toggleSpellCheck() @@ -240,9 +239,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a Character File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() # Type something into the document @@ -262,9 +261,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a Plot File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("0000000000009").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("0000000000009").setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() # Type something into the document @@ -284,9 +283,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a World File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("000000000000b").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("000000000000b").setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() # Add Some Text @@ -315,10 +314,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Select the 'New Scene' file nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("0000000000008").setExpanded(True) - nwGUI.treeView._getTreeItem("000000000000d").setExpanded(True) - nwGUI.treeView._getTreeItem("000000000000f").setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("0000000000008").setExpanded(True) + nwGUI.treeView.projTree._getTreeItem("000000000000d").setExpanded(True) + nwGUI.treeView.projTree._getTreeItem("000000000000f").setSelected(True) assert nwGUI.openSelectedItem() # Type something into the document diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 801abd68..274746d7 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -467,7 +467,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): buildTestProject(nwGUI, fncProj) - assert nwGUI.treeView._getTreeItem("000000000000f") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000f") is not None assert nwGUI.openDocument("000000000000f") is True nwGUI.docEditor.clear() @@ -476,10 +476,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): assert nwGUI.docEditor.getText() == "hello world" nwGUI.docEditor.clear() - assert not nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) + assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False assert nwGUI.docEditor.isEmpty() - assert not nwGUI.docEditor.insertText(None) + assert nwGUI.docEditor.insertText(None) is False assert nwGUI.docEditor.isEmpty() # qtbot.stopForInteraction() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 258ad150..7b66f305 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -52,7 +52,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) buildTestProject(nwGUI, prjDir) # No itemType set - nwTree.clearSelection() + nwTree.projTree.clearSelection() assert nwTree.newTreeItem(None) is False # Root Items @@ -69,7 +69,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # ================= # No location selected for new item - nwTree.clearSelection() + nwTree.projTree.clearSelection() caplog.clear() assert nwTree.newTreeItem(nwItemType.FILE) is False assert nwTree.newTreeItem(nwItemType.FOLDER) is False @@ -116,7 +116,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) nwGUI.theProject.tree["0000000000013"].setParent("0000000000011") # Get the trash folder - nwTree._addTrashRoot() + nwTree.projTree._addTrashRoot() trashHandle = nwGUI.theProject.trashFolder() nwTree.setSelectedHandle(trashHandle) assert nwTree.newTreeItem(nwItemType.FILE) is False @@ -182,7 +182,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Move with no selections - nwTree.clearSelection() + nwTree.projTree.clearSelection() assert nwTree.moveTreeItem(1) is False # Move second item up twice (should give same result) @@ -304,13 +304,13 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # No selection made - nwTree.clearSelection() + nwTree.projTree.clearSelection() caplog.clear() assert nwTree.deleteItem() is False assert "no item to delete" in caplog.text # Not a valid handle - nwTree.clearSelection() + nwTree.projTree.clearSelection() caplog.clear() assert nwTree.deleteItem("0000000000000") is False assert "Could not find tree item" in caplog.text @@ -326,10 +326,10 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # =========== # Block adding trash folder - funcPointer = nwTree._addTrashRoot - nwTree._addTrashRoot = lambda *a: None + funcPointer = nwTree.projTree._addTrashRoot + nwTree.projTree._addTrashRoot = lambda *a: None assert nwTree.deleteItem("0000000000012") is False - nwTree._addTrashRoot = funcPointer + nwTree.projTree._addTrashRoot = funcPointer # Delete last two documents, which also adds the trash folder assert nwTree.deleteItem("0000000000012") is True @@ -441,7 +441,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) # Delete proper - assert nwTree._deleteTreeItem("000000000000e") is True + assert nwTree.projTree._deleteTreeItem("000000000000e") is True assert not os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) # Clean up From 12a7ec7fe388030c18884e4064edb1b9b14d267c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 22:02:44 +0200 Subject: [PATCH 124/179] Allow the project class to create new files --- novelwriter/core/project.py | 32 ++++++++++++++- tests/test_core/test_core_project.py | 58 +++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 460fb91f..cce4603d 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -45,7 +45,7 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.error import logException from novelwriter.common import ( checkString, checkBool, checkInt, isHandle, formatTimeStamp, - makeFileNameSafe, hexToInt, simplified + makeFileNameSafe, hexToInt, minmax, simplified ) from novelwriter.constants import trConst, nwFiles, nwLabels @@ -154,6 +154,8 @@ class NWProject(): def newFolder(self, label, pHandle): """Add a new folder with a given label and parent item. """ + if pHandle not in self._projTree: + return None newItem = NWItem(self) newItem.setName(label) newItem.setType(nwItemType.FOLDER) @@ -164,6 +166,8 @@ class NWProject(): def newFile(self, label, pHandle): """Add a new file with a given label and parent item. """ + if pHandle not in self._projTree: + return None newItem = NWItem(self) newItem.setName(label) newItem.setType(nwItemType.FILE) @@ -171,6 +175,32 @@ class NWProject(): self._projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle + def writeNewFile(self, tHandle, hLevel, isDocument): + """Write content to a new document after it is created. This + will not run if the file exists and is not empty. + """ + tItem = self._projTree[tHandle] + if tItem is None: + return False + if tItem.itemType != nwItemType.FILE: + return False + + newDoc = NWDoc(self, tHandle) + if newDoc.readDocument().strip(): + return False + + hshText = "#"*minmax(hLevel, 1, 4) + newText = f"{hshText} {tItem.itemName}\n\n" + if tItem.isNovelLike() and isDocument: + tItem.setLayout(nwItemLayout.DOCUMENT) + else: + tItem.setLayout(nwItemLayout.NOTE) + + newDoc.writeDocument(newText) + self._projIndex.scanText(tHandle, newText) + + return True + def trashFolder(self): """Add the special trash root folder to the project. """ diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 005277c6..9454b0fc 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -29,10 +29,14 @@ from lxml import etree from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE from mock import causeOSError -from novelwriter.core.project import NWProject from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.common import formatTimeStamp from novelwriter.constants import nwFiles +from novelwriter.core.tree import NWTree +from novelwriter.core.index import NWIndex +from novelwriter.core.project import NWProject +from novelwriter.core.options import OptionState +from novelwriter.core.document import NWDoc @pytest.mark.core @@ -280,12 +284,12 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd): @pytest.mark.core -def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): +def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd): """Check that new files can be added to the project. """ projFile = os.path.join(fncDir, "nwProject.nwx") - testFile = os.path.join(outDir, "coreProject_NewFile_nwProject.nwx") - compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx") + testFile = os.path.join(outDir, "coreProject_NewFileFolder_nwProject.nwx") + compFile = os.path.join(refDir, "coreProject_NewFileFolder_nwProject.nwx") theProject = NWProject(mockGUI) buildTestProject(theProject, fncDir) @@ -295,9 +299,33 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.closeProject() is True assert theProject.openProject(projFile) is True - assert isinstance(theProject.newFile("Hello", "31489056e0916"), str) - assert isinstance(theProject.newFile("Jane", "71ee45a3c0db9"), str) - assert theProject.projChanged + # Invalid call + assert theProject.newFolder("New Folder", "1234567890abc") is None + assert theProject.newFile("New File", "1234567890abc") is None + + # Add files properly + assert theProject.newFolder("Stuff", "0000000000015") == "0000000000028" + assert theProject.newFile("Hello", "0000000000015") == "0000000000029" + assert theProject.newFile("Jane", "0000000000012") == "000000000002a" + + assert "0000000000028" in theProject.tree + assert "0000000000029" in theProject.tree + assert "000000000002a" in theProject.tree + + # Write to file, failed + assert theProject.writeNewFile("blabla", 1, True) is False # Not a handle + assert theProject.writeNewFile("0000000000028", 1, True) is False # Not a file + assert theProject.writeNewFile("0000000000014", 1, True) is False # Already has content + + # Write to file, success + assert theProject.writeNewFile("0000000000029", 2, True) is True + assert NWDoc(theProject, "0000000000029").readDocument() == "## Hello\n\n" + + assert theProject.writeNewFile("000000000002a", 1, False) is True + assert NWDoc(theProject, "000000000002a").readDocument() == "# Jane\n\n" + + # Save, close and check + assert theProject.projChanged is True assert theProject.saveProject() is True assert theProject.closeProject() is True @@ -305,7 +333,7 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert theProject.projChanged is False -# END Test testCoreProject_NewFile +# END Test testCoreProject_NewFileFolder @pytest.mark.core @@ -613,6 +641,11 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): theProject = NWProject(mockGUI) theProject.openProject(nwMinimal) + # Storage Objects + assert isinstance(theProject.index, NWIndex) + assert isinstance(theProject.tree, NWTree) + assert isinstance(theProject.options, OptionState) + # Move Novel ROOT to after its files oldOrder = [ "a508bb932959c", # ROOT: Novel @@ -722,7 +755,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): # Change Importance # ================= - fHandle = theProject.newFile("Jane Doe", "8b9d2e465e150") + fHandle = theProject.newFile("Jane Doe", "0000000000012") theProject.tree[fHandle].setImport("Main") assert theProject.tree[fHandle].itemImport == importKeys[3] @@ -894,6 +927,10 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.setProjectLang("en_GB") is True assert theProject.projLang == "en_GB" + # Language Lookup + assert theProject.localLookup(1) == "One" + assert theProject.localLookup(10) == "Ten" + # Automatic outline update theProject.projChanged = False assert theProject.setAutoOutline(True) @@ -1007,7 +1044,8 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): # Add a file with non-existent parent # This file will be renoved from the project on open - assert theProject.newFile("Oops", "0000000000000") + oHandle = theProject.newFile("Oops", "b3643d0f92e32") + theProject.tree[oHandle].setParent("1234567890abc") # Save and close assert theProject.saveProject() is True From 46bb8e5bccf2704f3db3055d609a81b2b1f4827e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 22:03:42 +0200 Subject: [PATCH 125/179] Populate the add button menu in the project widget --- novelwriter/core/index.py | 5 - novelwriter/gui/projtree.py | 158 +++++++++++++----- sample/nwProject.nwx | 10 +- ...> coreProject_NewFileFolder_nwProject.nwx} | 30 ++-- tests/test_core/test_core_index.py | 3 - tests/test_dialogs/test_dlg_docmerge.py | 3 +- tests/test_dialogs/test_dlg_docsplit.py | 3 +- tests/test_dialogs/test_dlg_itemeditor.py | 5 +- tests/test_gui/test_gui_guimain.py | 9 +- tests/test_gui/test_gui_projtree.py | 40 +++-- 10 files changed, 180 insertions(+), 86 deletions(-) rename tests/reference/{coreProject_NewFile_nwProject.nwx => coreProject_NewFileFolder_nwProject.nwx} (76%) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index f86de8c2..ee9ea089 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -502,11 +502,6 @@ class NWIndex: """ return self._itemIndex.mainItemHeader(tHandle) - def getHandleHeaderIntLevel(self, tHandle): - """Get the integer header level of the first header of a handle. - """ - return H_LEVEL.get(self._itemIndex.mainItemHeader(tHandle), 0) - def getTableOfContents(self, maxDepth, skipExcl=True): """Generate a table of contents up to a maximum depth. """ diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 7286f87f..2e58a61d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -37,13 +37,13 @@ from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, QToolButton, - QSizePolicy + QSizePolicy, QInputDialog ) from novelwriter.core import NWDoc from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.common import minmax from novelwriter.dialogs.itemeditor import GuiItemEditor +from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) @@ -82,6 +82,8 @@ class GuiProjectWiew(QWidget): self.setLayout(self.outerBox) + # Connect Signals + # Function Mappings self.newTreeItem = self.projTree.newTreeItem self.revealNewTreeItem = self.projTree.revealNewTreeItem @@ -139,12 +141,19 @@ class GuiProjectWiew(QWidget): class GuiProjectToolBar(QToolBar): + ADD_PLAIN = 0 + ADD_CHAP = 1 + ADD_SCENE = 2 + ADD_NOTE = 3 + ADD_FOLDER = 4 + def __init__(self, theWidget): QTreeWidget.__init__(self, theWidget) logger.debug("Initialising GuiProjectToolBar ...") self.mainConf = novelwriter.CONFIG + self.theWidget = theWidget self.theParent = theWidget.theParent self.theProject = theWidget.theParent.theProject self.theTheme = theWidget.theParent.theTheme @@ -157,14 +166,49 @@ class GuiProjectToolBar(QToolBar): self.setContentsMargins(0, 0, 0, 0) self.setStyleSheet("QToolBar {border: 0px;}") - # Novel Selector + # Tree Label self.projLabel = QLabel(self.tr("Project")) self.projLabel.setContentsMargins(0, 0, mPx, 0) self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - # Itemss Menu + # Items Menu + self.mItems = QMenu() + + self.aAddEmpty = self.mItems.addAction(self.tr("Plain Document")) + self.aAddEmpty.setIcon(self.theTheme.getIcon("proj_document")) + self.aAddEmpty.triggered.connect(lambda: self._forwardNewItem(self.ADD_PLAIN)) + + self.aAddChap = self.mItems.addAction(self.tr("Chapter Document")) + self.aAddChap.setIcon(self.theTheme.getIcon("proj_chapter")) + self.aAddChap.triggered.connect(lambda: self._forwardNewItem(self.ADD_CHAP)) + + self.aAddScene = self.mItems.addAction(self.tr("Scene Document")) + self.aAddScene.setIcon(self.theTheme.getIcon("proj_scene")) + self.aAddScene.triggered.connect(lambda: self._forwardNewItem(self.ADD_SCENE)) + + self.aAddNote = self.mItems.addAction(self.tr("Project Note")) + self.aAddNote.setIcon(self.theTheme.getIcon("proj_note")) + self.aAddNote.triggered.connect(lambda: self._forwardNewItem(self.ADD_NOTE)) + + self.aAddFolder = self.mItems.addAction(self.tr("Folder")) + self.aAddFolder.setIcon(self.theTheme.getIcon("proj_folder")) + self.aAddFolder.triggered.connect(lambda: self._forwardNewItem(self.ADD_FOLDER)) + + self.mAddRoot = self.mItems.addMenu(self.tr("Root Folder")) + self._addRootFolderEntry(nwItemClass.NOVEL) + self._addRootFolderEntry(nwItemClass.ARCHIVE) + self.mAddRoot.addSeparator() + self._addRootFolderEntry(nwItemClass.PLOT) + self._addRootFolderEntry(nwItemClass.CHARACTER) + self._addRootFolderEntry(nwItemClass.WORLD) + self._addRootFolderEntry(nwItemClass.ARCHIVE) + self._addRootFolderEntry(nwItemClass.OBJECT) + self._addRootFolderEntry(nwItemClass.ENTITY) + self._addRootFolderEntry(nwItemClass.CUSTOM) + self.tbItems = QToolButton(self) self.tbItems.setIcon(self.theTheme.getIcon("add")) + self.tbItems.setMenu(self.mItems) self.tbItems.setPopupMode(QToolButton.InstantPopup) # Settings Menu @@ -182,6 +226,45 @@ class GuiProjectToolBar(QToolBar): return + ## + # Private Slots + ## + + @pyqtSlot(Enum) + def _forwardNewRootFolder(self, itemClass): + """Forward the request for a new root folder to the tree. + """ + self.theWidget.projTree.newTreeItem(nwItemType.ROOT, itemClass) + return + + @pyqtSlot(int) + def _forwardNewItem(self, type): + """Forward the request for a new item of a given type. + """ + if type == self.ADD_PLAIN: + self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) + elif type == self.ADD_CHAP: + self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) + elif type == self.ADD_SCENE: + self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) + elif type == self.ADD_NOTE: + self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + elif type == self.ADD_FOLDER: + self.theWidget.projTree.newTreeItem(nwItemType.FOLDER) + return + + ## + # Internal Functions + ## + + def _addRootFolderEntry(self, itemClass): + """Add a menu entry for a root folder of a given class. + """ + aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) + aNew.setIcon(self.theTheme.getIcon(nwLabels.CLASS_ICON[itemClass])) + aNew.triggered.connect(lambda: self._forwardNewRootFolder(itemClass)) + self.mAddRoot.addAction(aNew) + # END Class GuiProjectToolBar @@ -291,7 +374,7 @@ class GuiProjectTree(QTreeWidget): self._timeChanged = 0 return - def newTreeItem(self, itemType, itemClass=None): + def newTreeItem(self, itemType, itemClass=None, hLevel=1, isNote=False): """Add new item to the tree, with a given itemType (and itemClass if Root), and attach it to the selected handle. Also make sure the item is added in a place it can be added, and that @@ -334,52 +417,49 @@ class GuiProjectTree(QTreeWidget): ), nwAlert.ERROR) return False + # Ask for label + if itemType == nwItemType.FILE: + if isNote: + newLabel = self.tr("New Note") + elif hLevel == 2: + newLabel = self.tr("New Chapter") + elif hLevel == 3: + newLabel = self.tr("New Scene") + else: + newLabel = self.tr("New Document") + else: + newLabel = self.tr("New Folder") + + newLabel, dlgOk = QInputDialog.getText(self, "", self.tr("Label:"), text=newLabel) + if not dlgOk: + logger.info("New item creation cancelled by user") + return False + # Add the file or folder if itemType == nwItemType.FILE: - if pItem.isNovelLike(): - tHandle = self.theProject.newFile(self.tr("New Document"), sHandle) - else: - tHandle = self.theProject.newFile(self.tr("New Note"), sHandle) - elif itemType == nwItemType.FOLDER: - tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle) + tHandle = self.theProject.newFile(newLabel, sHandle) + else: + tHandle = self.theProject.newFolder(newLabel, sHandle) else: logger.error("Failed to add new item") return False - # If there is no handle set, return here. This is a bug + # If there is no handle set, return here. This is a bug. if tHandle is None: # pragma: no cover + logger.error("Internal error") return True - # Add the new item to the tree and open the editor dialog - self.revealNewTreeItem(tHandle, nHandle) - self.editTreeItem(tHandle) - # Handle new file creation - nwItem = self.theProject.tree[tHandle] - if nwItem.itemType != nwItemType.FILE: - return True + if itemType == nwItemType.FILE and hLevel > 0: + if self.theProject.writeNewFile(tHandle, hLevel, not isNote): + # If successful, update word count + wC = self.theProject.index.getCounts(tHandle)[1] + self.propagateCount(tHandle, wC) + self.theWidget.wordCountsChanged.emit() - # This is a new file, so let's add some content - newDoc = NWDoc(self.theProject, tHandle) - if not newDoc.readDocument(): - if nwItem.itemLayout == nwItemLayout.DOCUMENT: - iLvl = self.theProject.index.getHandleHeaderIntLevel(sHandle) - hLvl = "#"*minmax(iLvl + 1, 2, 4) - newText = f"{hLvl} {nwItem.itemName}\n\n" - else: - newText = f"# {nwItem.itemName}\n\n" - - pIndex = self.theProject.index - - # Save the text and index it - newDoc.writeDocument(newText) - pIndex.scanText(tHandle, newText) - - # Get Word Counts - _, wC, _ = pIndex.getCounts(tHandle) - self.propagateCount(tHandle, wC) - self.theWidget.wordCountsChanged.emit() + # Add the new item to the project tree + self.revealNewTreeItem(tHandle, nHandle) return True diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 9f3a6587..4dd6396a 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1334 - 225 - 67746 + 1342 + 227 + 68090 False @@ -15,7 +15,7 @@ True None True - a520879ca0b45 + 636b6aa9b697b 636b6aa9b697b 1363 954 diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx similarity index 76% rename from tests/reference/coreProject_NewFile_nwProject.nwx rename to tests/reference/coreProject_NewFileFolder_nwProject.nwx index 19a2f4bf..4235e9bb 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,9 +16,9 @@ True None None - 0 - 0 - 0 + 2 + 1 + 1 %title% @@ -28,19 +28,19 @@
- New + New Note Draft Finished - New + New Minor Major Main
- + Novel @@ -73,13 +73,17 @@ New Scene - - - Hello + + + Stuff - - - Jane + + + Hello + + + + Jane
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 3d40a9df..8f126e56 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -232,9 +232,6 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): assert theIndex.getHandleHeaderLevel(cHandle) == "H1" assert theIndex.getHandleHeaderLevel(nHandle) == "H1" - assert theIndex.getHandleHeaderIntLevel(cHandle) == 1 - assert theIndex.getHandleHeaderIntLevel(nHandle) == 1 - assert theIndex.getHandleHeaderIntLevel("stuff") == 0 # Zero Items assert theIndex.checkThese([], cItem) == [] diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index ce5c68d4..f796f864 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -25,7 +25,7 @@ import pytest from mock import causeOSError from tools import getGuiItem, readFile, writeFile, buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from PyQt5.QtWidgets import QAction, QMessageBox, QDialog, QInputDialog from novelwriter.enum import nwItemType, nwWidget from novelwriter.dialogs import GuiDocMerge, GuiItemEditor @@ -39,6 +39,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) # Create a new project buildTestProject(nwGUI, fncProj) diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 0e3174c7..080be8ab 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -25,7 +25,7 @@ import pytest from mock import causeOSError from tools import getGuiItem, readFile, writeFile, buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from PyQt5.QtWidgets import QAction, QMessageBox, QDialog, QInputDialog from novelwriter.enum import nwItemType, nwWidget from novelwriter.dialogs import GuiDocSplit, GuiItemEditor @@ -40,6 +40,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) # Create a new project buildTestProject(nwGUI, fncProj) diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 501d1e3b..53426df7 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -23,7 +23,7 @@ import pytest from tools import getGuiItem, buildTestProject -from PyQt5.QtWidgets import QAction, QDialog, QMessageBox +from PyQt5.QtWidgets import QAction, QDialog, QMessageBox, QInputDialog from novelwriter.enum import nwItemLayout, nwItemType from novelwriter.dialogs import GuiItemEditor @@ -154,6 +154,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) # Create Project and Open Document buildTestProject(nwGUI, fncProj) @@ -165,7 +166,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Create Note nwGUI.treeView.projTree.clearSelection() nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) # Open Note assert nwGUI.openDocument("0000000000010") diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 78c2af66..6085c59b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -26,7 +26,7 @@ from shutil import copyfile from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox, QDialog +from PyQt5.QtWidgets import QMessageBox, QDialog, QInputDialog from novelwriter.gui import GuiDocEditor, GuiNovelTree, GuiOutline from novelwriter.enum import nwItemType, nwWidget @@ -173,6 +173,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock monkeypatch.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) # Create new, save, close project buildTestProject(nwGUI, fncProj) @@ -241,7 +242,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.projTree.clearSelection() nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -263,7 +264,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.projTree.clearSelection() nwGUI.treeView.projTree._getTreeItem("0000000000009").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -285,7 +286,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.projTree.clearSelection() nwGUI.treeView.projTree._getTreeItem("000000000000b").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Add Some Text diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 7b66f305..81c9a801 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,9 +24,8 @@ import os from tools import buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox +from PyQt5.QtWidgets import QAction, QMessageBox, QInputDialog -from novelwriter.guimain import GuiMain from novelwriter.gui.projtree import GuiProjectTree from novelwriter.enum import nwItemType, nwItemClass @@ -40,7 +39,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) nwTree = nwGUI.treeView @@ -89,22 +88,31 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) assert nwGUI.theProject.tree["0000000000012"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL - # Add a new file next to the other new file + # Add a new chapter next to the other new file nwTree.setSelectedHandle("0000000000012") - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE, hLevel=2) is True assert nwGUI.theProject.tree["0000000000013"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL assert nwGUI.openDocument("0000000000013") - assert nwGUI.docEditor.getText() == "## New Document\n\n" + assert nwGUI.docEditor.getText() == "## New Chapter\n\n" + + # Add a new scene next to the other new file + nwTree.setSelectedHandle("0000000000012") + assert nwTree.newTreeItem(nwItemType.FILE, hLevel=3) is True + assert nwGUI.theProject.tree["0000000000014"].itemParent == "0000000000011" + assert nwGUI.theProject.tree["0000000000014"].itemRoot == "0000000000008" + assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument("0000000000014") + assert nwGUI.docEditor.getText() == "### New Scene\n\n" # Add a new file to the characters folder nwTree.setSelectedHandle("000000000000a") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwGUI.theProject.tree["0000000000014"].itemParent == "000000000000a" - assert nwGUI.theProject.tree["0000000000014"].itemRoot == "000000000000a" - assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.CHARACTER - assert nwGUI.openDocument("0000000000014") + assert nwTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True + assert nwGUI.theProject.tree["0000000000015"].itemParent == "000000000000a" + assert nwGUI.theProject.tree["0000000000015"].itemRoot == "000000000000a" + assert nwGUI.theProject.tree["0000000000015"].itemClass == nwItemClass.CHARACTER + assert nwGUI.openDocument("0000000000015") assert nwGUI.docEditor.getText() == "# New Note\n\n" # Make sure the sibling folder bug trap works @@ -115,6 +123,12 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) assert "Internal error" in caplog.text nwGUI.theProject.tree["0000000000013"].setParent("0000000000011") + # Cancel during creation + with monkeypatch.context() as mp: + mp.setattr(QInputDialog, "getText", lambda *a, **k: ("", False)) + nwTree.setSelectedHandle("0000000000013") + assert nwTree.newTreeItem(nwItemType.FILE) is False + # Get the trash folder nwTree.projTree._addTrashRoot() trashHandle = nwGUI.theProject.trashFolder() @@ -148,7 +162,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) nwTree = nwGUI.treeView @@ -272,7 +286,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) nwTree = nwGUI.treeView From 272e7f6e2461b849abd9bc662b32b4b45a9644de Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 22:17:24 +0200 Subject: [PATCH 126/179] Remove redundant menu entries --- novelwriter/gui/mainmenu.py | 29 +---------------------------- novelwriter/gui/projtree.py | 31 +------------------------------ 2 files changed, 2 insertions(+), 58 deletions(-) diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index bd8d1edd..1996f151 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction -from novelwriter.enum import nwItemType, nwItemClass, nwDocAction, nwDocInsert, nwWidget +from novelwriter.enum import nwItemType, nwDocAction, nwDocInsert, nwWidget from novelwriter.constants import trConst, nwKeyWords, nwLabels, nwUnicode logger = logging.getLogger(__name__) @@ -164,33 +164,6 @@ class GuiMainMenu(QMenuBar): # Project > Separator self.projMenu.addSeparator() - # Project > New Root - self.rootMenu = self.projMenu.addMenu(self.tr("Create Root Folder")) - self.rootItems = {} - self.rootItems[nwItemClass.NOVEL] = QAction(self.tr("Novel Root"), self.rootMenu) - self.rootItems[nwItemClass.PLOT] = QAction(self.tr("Plot Root"), self.rootMenu) - self.rootItems[nwItemClass.CHARACTER] = QAction(self.tr("Character Root"), self.rootMenu) - self.rootItems[nwItemClass.WORLD] = QAction(self.tr("Location Root"), self.rootMenu) - self.rootItems[nwItemClass.TIMELINE] = QAction(self.tr("Timeline Root"), self.rootMenu) - self.rootItems[nwItemClass.OBJECT] = QAction(self.tr("Object Root"), self.rootMenu) - self.rootItems[nwItemClass.ENTITY] = QAction(self.tr("Entity Root"), self.rootMenu) - self.rootItems[nwItemClass.CUSTOM] = QAction(self.tr("Custom Root"), self.rootMenu) - self.rootItems[nwItemClass.ARCHIVE] = QAction(self.tr("Archive Root"), self.rootMenu) - for n, itemClass in enumerate(self.rootItems.keys()): - self.rootItems[itemClass].triggered.connect( - lambda n, itemClass=itemClass: self._newTreeItem(nwItemType.ROOT, itemClass) - ) - self.rootMenu.addAction(self.rootItems[itemClass]) - - # Project > New Folder - self.aCreateFolder = QAction(self.tr("Create Folder"), self) - self.aCreateFolder.setShortcut("Ctrl+Shift+N") - self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER)) - self.projMenu.addAction(self.aCreateFolder) - - # Project > Separator - self.projMenu.addSeparator() - # Project > Edit self.aEditItem = QAction(self.tr("Edit Item"), self) self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 2e58a61d..f519fb96 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -167,7 +167,7 @@ class GuiProjectToolBar(QToolBar): self.setStyleSheet("QToolBar {border: 0px;}") # Tree Label - self.projLabel = QLabel(self.tr("Project")) + self.projLabel = QLabel("%s" % self.tr("Project Content")) self.projLabel.setContentsMargins(0, 0, mPx, 0) self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -1235,14 +1235,6 @@ class GuiProjectTreeMenu(QMenu): self.toggleExp.triggered.connect(self._doToggleExported) self.addAction(self.toggleExp) - self.newFile = QAction(self.tr("New File"), self) - self.newFile.triggered.connect(self._doMakeFile) - self.addAction(self.newFile) - - self.newFolder = QAction(self.tr("New Folder"), self) - self.newFolder.triggered.connect(self._doMakeFolder) - self.addAction(self.newFolder) - self.deleteItem = QAction(self.tr("Delete Item"), self) self.deleteItem.triggered.connect(self._doDeleteItem) self.addAction(self.deleteItem) @@ -1273,18 +1265,13 @@ class GuiProjectTreeMenu(QMenu): trashHandle = self.theTree.theProject.tree.trashRoot() - inTrash = self.theTree.theProject.tree.isTrash(theItem.itemHandle) isTrash = theItem.itemHandle == trashHandle and trashHandle is not None isFile = theItem.itemType == nwItemType.FILE - allowNew = not (isTrash or inTrash) - self.editItem.setVisible(not isTrash) self.openItem.setVisible(isFile) self.viewItem.setVisible(isFile) self.toggleExp.setVisible(isFile) - self.newFile.setVisible(allowNew) - self.newFolder.setVisible(allowNew) self.deleteItem.setVisible(not isTrash) self.emptyTrash.setVisible(isTrash) @@ -1318,22 +1305,6 @@ class GuiProjectTreeMenu(QMenu): self.theTree.theParent.editItem() return - @pyqtSlot() - def _doMakeFile(self): - """Forward the new file call to the project tree. - """ - if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FILE) - return - - @pyqtSlot() - def _doMakeFolder(self): - """Forward the new folder call to the project tree. - """ - if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FOLDER) - return - @pyqtSlot() def _doToggleExported(self): """Flip the isExported flag of the current item. From 5a6ef5efd3804a4778257d43c2afea3879dac836 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 7 Jun 2022 00:11:42 +0200 Subject: [PATCH 127/179] Add move buttons to project tree toolbar --- docs/source/usage_shortcuts.rst | 3 +- novelwriter/gui/__init__.py | 4 +- novelwriter/gui/mainmenu.py | 22 +---------- novelwriter/gui/projtree.py | 58 ++++++++++++++++++++++++----- novelwriter/guimain.py | 4 +- tests/test_gui/test_gui_projtree.py | 45 +++++++++++----------- 6 files changed, 78 insertions(+), 58 deletions(-) diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst index 69c2e773..fa761a3e 100644 --- a/docs/source/usage_shortcuts.rst +++ b/docs/source/usage_shortcuts.rst @@ -48,7 +48,7 @@ The main shorcuts are as follows: ":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)" ":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)." ":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`." - ":kbd:`Ctrl`:kbd:`N`", "Create new document." + ":kbd:`Ctrl`:kbd:`N`", "Create new project item." ":kbd:`Ctrl`:kbd:`O`", "Open selected document." ":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter." ":kbd:`Ctrl`:kbd:`R`", "If in the project tree, open a document for viewing. If the editor has focus, open current document for viewing." @@ -72,7 +72,6 @@ The main shorcuts are as follows: ":kbd:`Ctrl`:kbd:`Shift`:kbd:`A`", "Select all text in current paragraph." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`G`", "Find previous occurrence of search word in current document." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`I`", "Import text to the current document from a text file." - ":kbd:`Ctrl`:kbd:`Shift`:kbd:`N`", "Create new folder." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`O`", "Open a project." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project." diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index 0ae23024..9560df1b 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -25,7 +25,7 @@ from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.mainmenu import GuiMainMenu from novelwriter.gui.noveltree import GuiNovelTree from novelwriter.gui.outline import GuiOutline -from novelwriter.gui.projtree import GuiProjectWiew +from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.theme import GuiTheme from novelwriter.gui.viewsbar import GuiViewsBar @@ -39,7 +39,7 @@ __all__ = [ "GuiMainStatus", "GuiNovelTree", "GuiOutline", - "GuiProjectWiew", + "GuiProjectView", "GuiTheme", "GuiViewsBar", ] diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 1996f151..37f1f75c 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction -from novelwriter.enum import nwItemType, nwDocAction, nwDocInsert, nwWidget +from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget from novelwriter.constants import trConst, nwKeyWords, nwLabels, nwUnicode logger = logging.getLogger(__name__) @@ -62,8 +62,6 @@ class GuiMainMenu(QMenuBar): # Function Pointers self._docAction = self.theParent.passDocumentAction - self._moveTreeItem = self.theParent.treeView.moveTreeItem - self._newTreeItem = self.theParent.treeView.newTreeItem self._docInsert = self.theParent.docEditor.insertText self._insertKeyWord = self.theParent.docEditor.insertKeyWord @@ -176,18 +174,6 @@ class GuiMainMenu(QMenuBar): self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) - # Project > Move Up - self.aMoveUp = QAction(self.tr("Move Item Up"), self) - self.aMoveUp.setShortcut("Ctrl+Up") - self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1)) - self.projMenu.addAction(self.aMoveUp) - - # Project > Move Down - self.aMoveDown = QAction(self.tr("Move Item Down"), self) - self.aMoveDown.setShortcut("Ctrl+Down") - self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1)) - self.projMenu.addAction(self.aMoveDown) - # Project > Undo Last Action self.aMoveUndo = QAction(self.tr("Undo Last Move"), self) self.aMoveUndo.setShortcut("Ctrl+Shift+Z") @@ -217,12 +203,6 @@ class GuiMainMenu(QMenuBar): # Document self.docuMenu = self.addMenu(self.tr("&Document")) - # Document > New - self.aNewDoc = QAction(self.tr("New Document"), self) - self.aNewDoc.setShortcut("Ctrl+N") - self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE)) - self.docuMenu.addAction(self.aNewDoc) - # Document > Open self.aOpenDoc = QAction(self.tr("Open Document"), self) self.aOpenDoc.setShortcut("Ctrl+O") diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index f519fb96..fcd2cd83 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -6,7 +6,7 @@ GUI classes for the main window project tree File History: Created: 2018-09-29 [0.0.1] GuiProjectTree Created: 2020-06-04 [0.7] GuiProjectTreeMenu -Created: 2022-06-06 [1.7b1] GuiProjectWiew +Created: 2022-06-06 [1.7b1] GuiProjectView Created: 2022-06-06 [1.7b1] GuiProjectToolBar This file is a part of novelWriter @@ -35,9 +35,9 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, - QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, QToolButton, - QSizePolicy, QInputDialog + qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, + QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, + QToolButton, QSizePolicy, QInputDialog ) from novelwriter.core import NWDoc @@ -48,7 +48,7 @@ from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) -class GuiProjectWiew(QWidget): +class GuiProjectView(QWidget): """This is a wrapper class holding all the elements of the project tree. The core object is the project tree itself. Most methods available are mapped through to the project tree class. @@ -87,7 +87,6 @@ class GuiProjectWiew(QWidget): # Function Mappings self.newTreeItem = self.projTree.newTreeItem self.revealNewTreeItem = self.projTree.revealNewTreeItem - self.moveTreeItem = self.projTree.moveTreeItem self.editTreeItem = self.projTree.editTreeItem self.getTreeFromHandle = self.projTree.getTreeFromHandle self.emptyTrash = self.projTree.emptyTrash @@ -121,9 +120,26 @@ class GuiProjectWiew(QWidget): self.projTree.buildTree() return + def setFocus(self): + """Forward the set focus call to the tree widget. + """ + self.projTree.setFocus() + return + def treeFocus(self): + """Check if the project tree has focus. + """ return self.projTree.hasFocus() + def anyFocus(self): + """Check if any widget or child widget has focus. + """ + if self.hasFocus(): + return True + if self.isAncestorOf(qApp.focusWidget()): + return True + return False + ## # Public Solts ## @@ -136,7 +152,7 @@ class GuiProjectWiew(QWidget): self.wordCountsChanged.emit() return -# END Class GuiProjectWiew +# END Class GuiProjectView class GuiProjectToolBar(QToolBar): @@ -171,6 +187,19 @@ class GuiProjectToolBar(QToolBar): self.projLabel.setContentsMargins(0, 0, mPx, 0) self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # Move Buttons + self.tbMoveU = QToolButton(self) + self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up")) + self.tbMoveU.setShortcut("Ctrl+Up") + self.tbMoveU.setIcon(self.theTheme.getIcon("up")) + self.tbMoveU.clicked.connect(lambda: self._forwardMoveItem(-1)) + + self.tbMoveD = QToolButton(self) + self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) + self.tbMoveD.setShortcut("Ctrl+Down") + self.tbMoveD.setIcon(self.theTheme.getIcon("down")) + self.tbMoveD.clicked.connect(lambda: self._forwardMoveItem(1)) + # Items Menu self.mItems = QMenu() @@ -207,6 +236,8 @@ class GuiProjectToolBar(QToolBar): self._addRootFolderEntry(nwItemClass.CUSTOM) self.tbItems = QToolButton(self) + self.tbItems.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) + self.tbItems.setShortcut("Ctrl+N") self.tbItems.setIcon(self.theTheme.getIcon("add")) self.tbItems.setMenu(self.mItems) self.tbItems.setPopupMode(QToolButton.InstantPopup) @@ -219,6 +250,8 @@ class GuiProjectToolBar(QToolBar): # Assemble self.addWidget(self.projLabel) self.addSeparator() + self.addWidget(self.tbMoveU) + self.addWidget(self.tbMoveD) self.addWidget(self.tbItems) self.addWidget(self.tbSettings) @@ -253,6 +286,13 @@ class GuiProjectToolBar(QToolBar): self.theWidget.projTree.newTreeItem(nwItemType.FOLDER) return + @pyqtSlot(int) + def _forwardMoveItem(self, steps): + """Forward the request to move an item up or down. + """ + self.theWidget.projTree.moveTreeItem(steps) + return + ## # Internal Functions ## @@ -492,7 +532,7 @@ class GuiProjectTree(QTreeWidget): logger.error("No project open") return False - if not self.hasFocus(): + if not self.theWidget.anyFocus(): return False tHandle = self.getSelectedHandle() @@ -831,7 +871,7 @@ class GuiProjectTree(QTreeWidget): dstItem = self._lastMove.get("parent", None) dstIndex = self._lastMove.get("index", None) - if not self.hasFocus(): + if not self.theWidget.anyFocus(): return False if srcItem is None or dstItem is None or dstIndex is None: diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index d18d4d17..2716213e 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, - GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectWiew, GuiTheme, + GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectView, GuiTheme, GuiViewsBar ) from novelwriter.dialogs import ( @@ -105,7 +105,7 @@ class GuiMain(QMainWindow): # Main GUI Elements self.statusBar = GuiMainStatus(self) - self.treeView = GuiProjectWiew(self) + self.treeView = GuiProjectView(self) self.novelView = GuiNovelTree(self) self.docEditor = GuiDocEditor(self) self.viewMeta = GuiDocViewDetails(self) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 81c9a801..031e7680 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -26,7 +26,7 @@ from tools import buildTestProject from PyQt5.QtWidgets import QAction, QMessageBox, QInputDialog -from novelwriter.gui.projtree import GuiProjectTree +from novelwriter.gui.projtree import GuiProjectView, GuiProjectTree from novelwriter.enum import nwItemType, nwItemClass @@ -163,11 +163,12 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiProjectView, "anyFocus", lambda *a: True) nwTree = nwGUI.treeView # Try to move item with no project - assert nwTree.moveTreeItem(1) is False + assert nwTree.projTree.moveTreeItem(1) is False # Create a project prjDir = os.path.join(fncDir, "project") @@ -187,33 +188,33 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): ] # Move item without focus - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - assert nwTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle("000000000000d") == [ - "000000000000d", "000000000000e", "000000000000f", - "0000000000010", "0000000000011", "0000000000012", - ] - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) + with monkeypatch.context() as mp: + mp.setattr(GuiProjectView, "anyFocus", lambda *a: False) + assert nwTree.projTree.moveTreeItem(1) is False + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000011", "0000000000012", + ] # Move with no selections nwTree.projTree.clearSelection() - assert nwTree.moveTreeItem(1) is False + assert nwTree.projTree.moveTreeItem(1) is False # Move second item up twice (should give same result) nwTree.setSelectedHandle("000000000000f") - assert nwTree.moveTreeItem(-1) is True + assert nwTree.projTree.moveTreeItem(-1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000f", "000000000000e", "0000000000010", "0000000000011", "0000000000012", ] - assert nwTree.moveTreeItem(-1) is False + assert nwTree.projTree.moveTreeItem(-1) is False assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000f", "000000000000e", "0000000000010", "0000000000011", "0000000000012", ] - # Restore via menu entry - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + # Restore + assert nwTree.projTree.moveTreeItem(1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -221,19 +222,19 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Move fifth item down twice (should give same result) nwTree.setSelectedHandle("0000000000011") - assert nwTree.moveTreeItem(1) is True + assert nwTree.projTree.moveTreeItem(1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", ] - assert nwTree.moveTreeItem(1) is False + assert nwTree.projTree.moveTreeItem(1) is False assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", ] - # Restore via menu entry - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + # Restore + assert nwTree.projTree.moveTreeItem(-1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -241,7 +242,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Move down again, and restore via undo nwTree.setSelectedHandle("0000000000011") - assert nwTree.moveTreeItem(1) is True + assert nwTree.projTree.moveTreeItem(1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", @@ -259,15 +260,15 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder up - assert nwTree.moveTreeItem(-1) is False + assert nwTree.projTree.moveTreeItem(-1) is False assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder down - assert nwTree.moveTreeItem(1) is True + assert nwTree.projTree.moveTreeItem(1) is True assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1 # Move novel folder up again - assert nwTree.moveTreeItem(-1) is True + assert nwTree.projTree.moveTreeItem(-1) is True assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Clean up From 0909bf7c362da6aa34bfc0ff07239c20560b17f7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 7 Jun 2022 10:34:27 +0200 Subject: [PATCH 128/179] Update code of conduct to v2.1 --- CODE_OF_CONDUCT.md | 150 +++++++++++++++++++++++++++++++-------------- 1 file changed, 103 insertions(+), 47 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f64bc973..28de89d7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,75 +2,131 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project leader at issues (at) novelwriter.io. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at issues (at) +novelwriter.io. All complaints will be reviewed and investigated promptly and +fairly. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations From ef270bc0698f272df542321ff9ddf5549aef0319 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 7 Jun 2022 22:38:02 +0200 Subject: [PATCH 129/179] Tweak project toolbar look, and fix shortcuts --- novelwriter/gui/projtree.py | 165 +++++++++++++--------------- tests/test_gui/test_gui_projtree.py | 9 -- 2 files changed, 79 insertions(+), 95 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index fcd2cd83..bc5b78b6 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -33,11 +33,11 @@ from enum import Enum from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, - QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, - QToolButton, QSizePolicy, QInputDialog + QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QLabel, QToolButton, + QSizePolicy, QInputDialog, QHBoxLayout, QShortcut ) from novelwriter.core import NWDoc @@ -75,13 +75,24 @@ class GuiProjectView(QWidget): # Assemble self.outerBox = QVBoxLayout() - self.outerBox.addWidget(self.projBar) - self.outerBox.addWidget(self.projTree) + self.outerBox.addWidget(self.projBar, 0) + self.outerBox.addWidget(self.projTree, 1) self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setSpacing(0) self.setLayout(self.outerBox) + # Keyboard Shortcuts + self.keyCtrlUp = QShortcut(self.projTree) + self.keyCtrlUp.setKey("Ctrl+Up") + self.keyCtrlUp.setContext(Qt.WidgetShortcut) + self.keyCtrlUp.activated.connect(lambda: self.projTree.moveTreeItem(-1)) + + self.keyCtrlDown = QShortcut(self.projTree) + self.keyCtrlDown.setKey("Ctrl+Down") + self.keyCtrlDown.setContext(Qt.WidgetShortcut) + self.keyCtrlDown.activated.connect(lambda: self.projTree.moveTreeItem(1)) + # Connect Signals # Function Mappings @@ -155,7 +166,7 @@ class GuiProjectView(QWidget): # END Class GuiProjectView -class GuiProjectToolBar(QToolBar): +class GuiProjectToolBar(QWidget): ADD_PLAIN = 0 ADD_CHAP = 1 @@ -163,42 +174,52 @@ class GuiProjectToolBar(QToolBar): ADD_NOTE = 3 ADD_FOLDER = 4 - def __init__(self, theWidget): - QTreeWidget.__init__(self, theWidget) + def __init__(self, projView): + QTreeWidget.__init__(self, projView) logger.debug("Initialising GuiProjectToolBar ...") self.mainConf = novelwriter.CONFIG - self.theWidget = theWidget - self.theParent = theWidget.theParent - self.theProject = theWidget.theParent.theProject - self.theTheme = theWidget.theParent.theTheme + self.projView = projView + self.theParent = projView.theParent + self.theProject = projView.theParent.theProject + self.theTheme = projView.theParent.theTheme iPx = self.theTheme.baseIconSize - mPx = self.mainConf.pxInt(12) + mPx = self.mainConf.pxInt(4) - self.setMovable(False) - self.setIconSize(QSize(iPx, iPx)) self.setContentsMargins(0, 0, 0, 0) - self.setStyleSheet("QToolBar {border: 0px;}") + self.setAutoFillBackground(True) + + qPalette = self.palette() + qPalette.setBrush(QPalette.Window, qPalette.base()) + self.setPalette(qPalette) + + fadeCol = qPalette.text().color() + buttonStyle = ( + "QToolButton {{border: none; background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" + ).format(fadeCol.red(), fadeCol.green(), fadeCol.blue()) # Tree Label self.projLabel = QLabel("%s" % self.tr("Project Content")) - self.projLabel.setContentsMargins(0, 0, mPx, 0) + self.projLabel.setContentsMargins(0, 0, 0, 0) self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Move Buttons self.tbMoveU = QToolButton(self) self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up")) - self.tbMoveU.setShortcut("Ctrl+Up") self.tbMoveU.setIcon(self.theTheme.getIcon("up")) - self.tbMoveU.clicked.connect(lambda: self._forwardMoveItem(-1)) + self.tbMoveU.setIconSize(QSize(iPx, iPx)) + self.tbMoveU.setStyleSheet(buttonStyle) + self.tbMoveU.clicked.connect(lambda: self.projView.projTree.moveTreeItem(-1)) self.tbMoveD = QToolButton(self) self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) - self.tbMoveD.setShortcut("Ctrl+Down") self.tbMoveD.setIcon(self.theTheme.getIcon("down")) - self.tbMoveD.clicked.connect(lambda: self._forwardMoveItem(1)) + self.tbMoveD.setIconSize(QSize(iPx, iPx)) + self.tbMoveD.setStyleSheet(buttonStyle) + self.tbMoveD.clicked.connect(lambda: self.projView.projTree.moveTreeItem(1)) # Items Menu self.mItems = QMenu() @@ -239,21 +260,29 @@ class GuiProjectToolBar(QToolBar): self.tbItems.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) self.tbItems.setShortcut("Ctrl+N") self.tbItems.setIcon(self.theTheme.getIcon("add")) + self.tbItems.setIconSize(QSize(iPx, iPx)) + self.tbItems.setStyleSheet(buttonStyle) self.tbItems.setMenu(self.mItems) self.tbItems.setPopupMode(QToolButton.InstantPopup) # Settings Menu self.tbSettings = QToolButton(self) self.tbSettings.setIcon(self.theTheme.getIcon("menu")) + self.tbSettings.setIconSize(QSize(iPx, iPx)) + self.tbSettings.setStyleSheet(buttonStyle) self.tbSettings.setPopupMode(QToolButton.InstantPopup) # Assemble - self.addWidget(self.projLabel) - self.addSeparator() - self.addWidget(self.tbMoveU) - self.addWidget(self.tbMoveD) - self.addWidget(self.tbItems) - self.addWidget(self.tbSettings) + self.outerBox = QHBoxLayout() + self.outerBox.addWidget(self.projLabel) + self.outerBox.addWidget(self.tbMoveU) + self.outerBox.addWidget(self.tbMoveD) + self.outerBox.addWidget(self.tbItems) + self.outerBox.addWidget(self.tbSettings) + self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) + self.outerBox.setSpacing(mPx) + + self.setLayout(self.outerBox) logger.debug("GuiProjectToolBar initialisation complete") @@ -267,7 +296,7 @@ class GuiProjectToolBar(QToolBar): def _forwardNewRootFolder(self, itemClass): """Forward the request for a new root folder to the tree. """ - self.theWidget.projTree.newTreeItem(nwItemType.ROOT, itemClass) + self.projView.projTree.newTreeItem(nwItemType.ROOT, itemClass) return @pyqtSlot(int) @@ -275,22 +304,15 @@ class GuiProjectToolBar(QToolBar): """Forward the request for a new item of a given type. """ if type == self.ADD_PLAIN: - self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) + self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) elif type == self.ADD_CHAP: - self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) + self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) elif type == self.ADD_SCENE: - self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) + self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) elif type == self.ADD_NOTE: - self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) elif type == self.ADD_FOLDER: - self.theWidget.projTree.newTreeItem(nwItemType.FOLDER) - return - - @pyqtSlot(int) - def _forwardMoveItem(self, steps): - """Forward the request to move an item up or down. - """ - self.theWidget.projTree.moveTreeItem(steps) + self.projView.projTree.newTreeItem(nwItemType.FOLDER) return ## @@ -315,16 +337,16 @@ class GuiProjectTree(QTreeWidget): C_EXPORT = 2 C_STATUS = 3 - def __init__(self, theWidget): - QTreeWidget.__init__(self, theWidget) + def __init__(self, projView): + QTreeWidget.__init__(self, projView) logger.debug("Initialising GuiProjectTree ...") self.mainConf = novelwriter.CONFIG - self.theWidget = theWidget - self.theParent = theWidget.theParent - self.theTheme = theWidget.theParent.theTheme - self.theProject = theWidget.theParent.theProject + self.projView = projView + self.theParent = projView.theParent + self.theTheme = projView.theParent.theTheme + self.theProject = projView.theParent.theProject # Internal Variables self._treeMap = {} @@ -496,7 +518,7 @@ class GuiProjectTree(QTreeWidget): # If successful, update word count wC = self.theProject.index.getCounts(tHandle)[1] self.propagateCount(tHandle, wC) - self.theWidget.wordCountsChanged.emit() + self.projView.wordCountsChanged.emit() # Add the new item to the project tree self.revealNewTreeItem(tHandle, nHandle) @@ -525,19 +547,12 @@ class GuiProjectTree(QTreeWidget): return True def moveTreeItem(self, nStep): - """Move an item up or down in the tree, but only if the project - tree has focus. This also applies when the menu is used. + """Move an item up or down in the tree. """ - if not self.theParent.hasProject: - logger.error("No project open") - return False - - if not self.theWidget.anyFocus(): - return False - tHandle = self.getSelectedHandle() tItem = self._getTreeItem(tHandle) if tItem is None: + logger.verbose("No item selected") return False pItem = tItem.parent() @@ -745,7 +760,7 @@ class GuiProjectTree(QTreeWidget): self._deleteTreeItem(dHandle) self._alertTreeChange(tHandle=tHandle, flush=autoFlush) - self.theWidget.wordCountsChanged.emit() + self.projView.wordCountsChanged.emit() else: # The item is not already in the trash folder, so we @@ -871,7 +886,7 @@ class GuiProjectTree(QTreeWidget): dstItem = self._lastMove.get("parent", None) dstIndex = self._lastMove.get("index", None) - if not self.theWidget.anyFocus(): + if not self.projView.anyFocus(): return False if srcItem is None or dstItem is None or dstIndex is None: @@ -951,7 +966,7 @@ class GuiProjectTree(QTreeWidget): """ tHandle = self.getSelectedHandle() if tHandle is not None: - self.theWidget.selectedItemChanged.emit(tHandle) + self.projView.selectedItemChanged.emit(tHandle) return @pyqtSlot("QTreeWidgetItem*", int) @@ -968,7 +983,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.theWidget.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) else: trItem = self._getTreeItem(tHandle) if trItem is not None: @@ -1020,7 +1035,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.theWidget.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) return @@ -1227,11 +1242,11 @@ class GuiProjectTree(QTreeWidget): itemType = tItem.itemType if itemType == nwItemType.ROOT: - self.theWidget.rootFolderChanged.emit(tHandle) + self.projView.rootFolderChanged.emit(tHandle) elif itemType == nwItemType.FILE and tItem.isNovelLike(): - self.theWidget.novelItemChanged.emit(tHandle) + self.projView.novelItemChanged.emit(tHandle) - self.theWidget.treeItemChanged.emit(tHandle) + self.projView.treeItemChanged.emit(tHandle) return @@ -1283,14 +1298,6 @@ class GuiProjectTreeMenu(QMenu): self.emptyTrash.triggered.connect(self._doEmptyTrash) self.addAction(self.emptyTrash) - self.moveUp = QAction(self.tr("Move Item Up"), self) - self.moveUp.triggered.connect(self._doMoveUp) - self.addAction(self.moveUp) - - self.moveDown = QAction(self.tr("Move Item Down"), self) - self.moveDown.triggered.connect(self._doMoveDown) - self.addAction(self.moveDown) - return def filterActions(self, theItem): @@ -1369,18 +1376,4 @@ class GuiProjectTreeMenu(QMenu): self.theTree.emptyTrash() return - @pyqtSlot() - def _doMoveUp(self): - """Forward the move item call to the project tree. - """ - self.theTree.moveTreeItem(-1) - return - - @pyqtSlot() - def _doMoveDown(self): - """Forward the move item call to the project tree. - """ - self.theTree.moveTreeItem(1) - return - # END Class GuiProjectTreeMenu diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 031e7680..c0189a66 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -187,15 +187,6 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): "0000000000010", "0000000000011", "0000000000012", ] - # Move item without focus - with monkeypatch.context() as mp: - mp.setattr(GuiProjectView, "anyFocus", lambda *a: False) - assert nwTree.projTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle("000000000000d") == [ - "000000000000d", "000000000000e", "000000000000f", - "0000000000010", "0000000000011", "0000000000012", - ] - # Move with no selections nwTree.projTree.clearSelection() assert nwTree.projTree.moveTreeItem(1) is False From af39fb98a13f0a3a8e09dcc0d3e5bc731f5263ad Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 7 Jun 2022 23:38:17 +0200 Subject: [PATCH 130/179] Clean up project tree code and add undo action to the menu --- novelwriter/gui/mainmenu.py | 6 -- novelwriter/gui/projtree.py | 153 +++++++++++++--------------- tests/test_gui/test_gui_guimain.py | 2 +- tests/test_gui/test_gui_projtree.py | 52 +++++----- 4 files changed, 95 insertions(+), 118 deletions(-) diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 37f1f75c..33b43cfb 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -174,12 +174,6 @@ class GuiMainMenu(QMenuBar): self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) - # Project > Undo Last Action - self.aMoveUndo = QAction(self.tr("Undo Last Move"), self) - self.aMoveUndo.setShortcut("Ctrl+Shift+Z") - self.aMoveUndo.triggered.connect(lambda: self.theParent.treeView.undoLastMove()) - self.projMenu.addAction(self.aMoveUndo) - # Project > Empty Trash self.aEmptyTrash = QAction(self.tr("Empty Trash"), self) self.aEmptyTrash.triggered.connect(lambda: self.theParent.treeView.emptyTrash()) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index bc5b78b6..6ad4d501 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -70,8 +70,8 @@ class GuiProjectView(QWidget): self.theParent = theParent # Build GUI - self.projBar = GuiProjectToolBar(self) self.projTree = GuiProjectTree(self) + self.projBar = GuiProjectToolBar(self) # Assemble self.outerBox = QVBoxLayout() @@ -83,20 +83,22 @@ class GuiProjectView(QWidget): self.setLayout(self.outerBox) # Keyboard Shortcuts - self.keyCtrlUp = QShortcut(self.projTree) - self.keyCtrlUp.setKey("Ctrl+Up") - self.keyCtrlUp.setContext(Qt.WidgetShortcut) - self.keyCtrlUp.activated.connect(lambda: self.projTree.moveTreeItem(-1)) + self.keyMoveUp = QShortcut(self.projTree) + self.keyMoveUp.setKey("Ctrl+Up") + self.keyMoveUp.setContext(Qt.WidgetShortcut) + self.keyMoveUp.activated.connect(lambda: self.projTree.moveTreeItem(-1)) - self.keyCtrlDown = QShortcut(self.projTree) - self.keyCtrlDown.setKey("Ctrl+Down") - self.keyCtrlDown.setContext(Qt.WidgetShortcut) - self.keyCtrlDown.activated.connect(lambda: self.projTree.moveTreeItem(1)) + self.keyMoveDn = QShortcut(self.projTree) + self.keyMoveDn.setKey("Ctrl+Down") + self.keyMoveDn.setContext(Qt.WidgetShortcut) + self.keyMoveDn.activated.connect(lambda: self.projTree.moveTreeItem(1)) - # Connect Signals + self.keyUndoMv = QShortcut(self.projTree) + self.keyUndoMv.setKey("Ctrl+Shift+Z") + self.keyUndoMv.setContext(Qt.WidgetShortcut) + self.keyUndoMv.activated.connect(lambda: self.projTree.undoLastMove()) # Function Mappings - self.newTreeItem = self.projTree.newTreeItem self.revealNewTreeItem = self.projTree.revealNewTreeItem self.editTreeItem = self.projTree.editTreeItem self.getTreeFromHandle = self.projTree.getTreeFromHandle @@ -104,7 +106,6 @@ class GuiProjectView(QWidget): self.deleteItem = self.projTree.deleteItem self.setTreeItemValues = self.projTree.setTreeItemValues self.propagateCount = self.projTree.propagateCount - self.undoLastMove = self.projTree.undoLastMove self.getSelectedHandle = self.projTree.getSelectedHandle self.setSelectedHandle = self.projTree.setSelectedHandle self.changedSince = self.projTree.changedSince @@ -152,7 +153,7 @@ class GuiProjectView(QWidget): return False ## - # Public Solts + # Public Slots ## @pyqtSlot(str, int, int, int) @@ -168,12 +169,6 @@ class GuiProjectView(QWidget): class GuiProjectToolBar(QWidget): - ADD_PLAIN = 0 - ADD_CHAP = 1 - ADD_SCENE = 2 - ADD_NOTE = 3 - ADD_FOLDER = 4 - def __init__(self, projView): QTreeWidget.__init__(self, projView) @@ -181,6 +176,7 @@ class GuiProjectToolBar(QWidget): self.mainConf = novelwriter.CONFIG self.projView = projView + self.projTree = projView.projTree self.theParent = projView.theParent self.theProject = projView.theParent.theProject self.theTheme = projView.theParent.theTheme @@ -197,9 +193,9 @@ class GuiProjectToolBar(QWidget): fadeCol = qPalette.text().color() buttonStyle = ( - "QToolButton {{border: none; background: transparent;}} " - "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(fadeCol.red(), fadeCol.green(), fadeCol.blue()) + "QToolButton {{padding: {0}px; border: none; background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" + ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue()) # Tree Label self.projLabel = QLabel("%s" % self.tr("Project Content")) @@ -212,39 +208,49 @@ class GuiProjectToolBar(QWidget): self.tbMoveU.setIcon(self.theTheme.getIcon("up")) self.tbMoveU.setIconSize(QSize(iPx, iPx)) self.tbMoveU.setStyleSheet(buttonStyle) - self.tbMoveU.clicked.connect(lambda: self.projView.projTree.moveTreeItem(-1)) + self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1)) self.tbMoveD = QToolButton(self) self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) self.tbMoveD.setIcon(self.theTheme.getIcon("down")) self.tbMoveD.setIconSize(QSize(iPx, iPx)) self.tbMoveD.setStyleSheet(buttonStyle) - self.tbMoveD.clicked.connect(lambda: self.projView.projTree.moveTreeItem(1)) + self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1)) - # Items Menu - self.mItems = QMenu() + # Add Item Menu + self.mAdd = QMenu() - self.aAddEmpty = self.mItems.addAction(self.tr("Plain Document")) + self.aAddEmpty = self.mAdd.addAction(self.tr("Plain Document")) self.aAddEmpty.setIcon(self.theTheme.getIcon("proj_document")) - self.aAddEmpty.triggered.connect(lambda: self._forwardNewItem(self.ADD_PLAIN)) + self.aAddEmpty.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) + ) - self.aAddChap = self.mItems.addAction(self.tr("Chapter Document")) + self.aAddChap = self.mAdd.addAction(self.tr("Chapter Document")) self.aAddChap.setIcon(self.theTheme.getIcon("proj_chapter")) - self.aAddChap.triggered.connect(lambda: self._forwardNewItem(self.ADD_CHAP)) + self.aAddChap.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) + ) - self.aAddScene = self.mItems.addAction(self.tr("Scene Document")) + self.aAddScene = self.mAdd.addAction(self.tr("Scene Document")) self.aAddScene.setIcon(self.theTheme.getIcon("proj_scene")) - self.aAddScene.triggered.connect(lambda: self._forwardNewItem(self.ADD_SCENE)) + self.aAddScene.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) + ) - self.aAddNote = self.mItems.addAction(self.tr("Project Note")) + self.aAddNote = self.mAdd.addAction(self.tr("Project Note")) self.aAddNote.setIcon(self.theTheme.getIcon("proj_note")) - self.aAddNote.triggered.connect(lambda: self._forwardNewItem(self.ADD_NOTE)) + self.aAddNote.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + ) - self.aAddFolder = self.mItems.addAction(self.tr("Folder")) + self.aAddFolder = self.mAdd.addAction(self.tr("Folder")) self.aAddFolder.setIcon(self.theTheme.getIcon("proj_folder")) - self.aAddFolder.triggered.connect(lambda: self._forwardNewItem(self.ADD_FOLDER)) + self.aAddFolder.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FOLDER) + ) - self.mAddRoot = self.mItems.addMenu(self.tr("Root Folder")) + self.mAddRoot = self.mAdd.addMenu(self.tr("Root Folder")) self._addRootFolderEntry(nwItemClass.NOVEL) self._addRootFolderEntry(nwItemClass.ARCHIVE) self.mAddRoot.addSeparator() @@ -256,31 +262,38 @@ class GuiProjectToolBar(QWidget): self._addRootFolderEntry(nwItemClass.ENTITY) self._addRootFolderEntry(nwItemClass.CUSTOM) - self.tbItems = QToolButton(self) - self.tbItems.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) - self.tbItems.setShortcut("Ctrl+N") - self.tbItems.setIcon(self.theTheme.getIcon("add")) - self.tbItems.setIconSize(QSize(iPx, iPx)) - self.tbItems.setStyleSheet(buttonStyle) - self.tbItems.setMenu(self.mItems) - self.tbItems.setPopupMode(QToolButton.InstantPopup) + self.tbAdd = QToolButton(self) + self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) + self.tbAdd.setShortcut("Ctrl+N") + self.tbAdd.setIcon(self.theTheme.getIcon("add")) + self.tbAdd.setIconSize(QSize(iPx, iPx)) + self.tbAdd.setStyleSheet(buttonStyle) + self.tbAdd.setMenu(self.mAdd) + self.tbAdd.setPopupMode(QToolButton.InstantPopup) - # Settings Menu - self.tbSettings = QToolButton(self) - self.tbSettings.setIcon(self.theTheme.getIcon("menu")) - self.tbSettings.setIconSize(QSize(iPx, iPx)) - self.tbSettings.setStyleSheet(buttonStyle) - self.tbSettings.setPopupMode(QToolButton.InstantPopup) + # More Options Menu + self.mMore = QMenu() + + self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move")) + self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove()) + + self.tbMore = QToolButton(self) + self.tbMore.setToolTip(self.tr("More Options")) + self.tbMore.setIcon(self.theTheme.getIcon("menu")) + self.tbMore.setIconSize(QSize(iPx, iPx)) + self.tbMore.setStyleSheet(buttonStyle) + self.tbMore.setMenu(self.mMore) + self.tbMore.setPopupMode(QToolButton.InstantPopup) # Assemble self.outerBox = QHBoxLayout() self.outerBox.addWidget(self.projLabel) self.outerBox.addWidget(self.tbMoveU) self.outerBox.addWidget(self.tbMoveD) - self.outerBox.addWidget(self.tbItems) - self.outerBox.addWidget(self.tbSettings) + self.outerBox.addWidget(self.tbAdd) + self.outerBox.addWidget(self.tbMore) self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) - self.outerBox.setSpacing(mPx) + self.outerBox.setSpacing(0) self.setLayout(self.outerBox) @@ -288,33 +301,6 @@ class GuiProjectToolBar(QWidget): return - ## - # Private Slots - ## - - @pyqtSlot(Enum) - def _forwardNewRootFolder(self, itemClass): - """Forward the request for a new root folder to the tree. - """ - self.projView.projTree.newTreeItem(nwItemType.ROOT, itemClass) - return - - @pyqtSlot(int) - def _forwardNewItem(self, type): - """Forward the request for a new item of a given type. - """ - if type == self.ADD_PLAIN: - self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) - elif type == self.ADD_CHAP: - self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) - elif type == self.ADD_SCENE: - self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) - elif type == self.ADD_NOTE: - self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) - elif type == self.ADD_FOLDER: - self.projView.projTree.newTreeItem(nwItemType.FOLDER) - return - ## # Internal Functions ## @@ -324,7 +310,7 @@ class GuiProjectToolBar(QWidget): """ aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) aNew.setIcon(self.theTheme.getIcon(nwLabels.CLASS_ICON[itemClass])) - aNew.triggered.connect(lambda: self._forwardNewRootFolder(itemClass)) + aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass)) self.mAddRoot.addAction(aNew) # END Class GuiProjectToolBar @@ -886,9 +872,6 @@ class GuiProjectTree(QTreeWidget): dstItem = self._lastMove.get("parent", None) dstIndex = self._lastMove.get("index", None) - if not self.projView.anyFocus(): - return False - if srcItem is None or dstItem is None or dstIndex is None: logger.verbose("No tree move to undo") return False diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 6085c59b..ab3a1141 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -461,7 +461,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock qtbot.wait(stepDelay) # Check a Quick Create and Delete - assert nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + assert nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) newHandle = nwGUI.treeView.getSelectedHandle() assert nwGUI.theProject.tree["0000000000020"] is not None assert nwGUI.treeView.deleteItem() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index c0189a66..fa904d34 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,7 +24,7 @@ import os from tools import buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QInputDialog +from PyQt5.QtWidgets import QMessageBox, QInputDialog from novelwriter.gui.projtree import GuiProjectView, GuiProjectTree from novelwriter.enum import nwItemType, nwItemClass @@ -44,7 +44,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) nwTree = nwGUI.treeView # Try to add item with no project - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False # Create a project prjDir = os.path.join(fncDir, "project") @@ -52,16 +52,16 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # No itemType set nwTree.projTree.clearSelection() - assert nwTree.newTreeItem(None) is False + assert nwTree.projTree.newTreeItem(None) is False # Root Items # ========== # No class set - assert nwTree.newTreeItem(nwItemType.ROOT) is False + assert nwTree.projTree.newTreeItem(nwItemType.ROOT) is False # Create root item - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True + assert nwTree.projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True assert "0000000000010" in nwGUI.theProject.tree # File/Folder Items @@ -70,27 +70,27 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # No location selected for new item nwTree.projTree.clearSelection() caplog.clear() - assert nwTree.newTreeItem(nwItemType.FILE) is False - assert nwTree.newTreeItem(nwItemType.FOLDER) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is False assert "Did not find anywhere" in caplog.text # Create new folder as child of Novel folder nwTree.setSelectedHandle("0000000000008") - assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True assert nwGUI.theProject.tree["0000000000011"].itemParent == "0000000000008" assert nwGUI.theProject.tree["0000000000011"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000011"].itemClass == nwItemClass.NOVEL # Add a new file in the new folder nwTree.setSelectedHandle("0000000000011") - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert nwGUI.theProject.tree["0000000000012"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000012"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL # Add a new chapter next to the other new file nwTree.setSelectedHandle("0000000000012") - assert nwTree.newTreeItem(nwItemType.FILE, hLevel=2) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True assert nwGUI.theProject.tree["0000000000013"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL @@ -99,7 +99,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # Add a new scene next to the other new file nwTree.setSelectedHandle("0000000000012") - assert nwTree.newTreeItem(nwItemType.FILE, hLevel=3) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True assert nwGUI.theProject.tree["0000000000014"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000014"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.NOVEL @@ -108,7 +108,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # Add a new file to the characters folder nwTree.setSelectedHandle("000000000000a") - assert nwTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True assert nwGUI.theProject.tree["0000000000015"].itemParent == "000000000000a" assert nwGUI.theProject.tree["0000000000015"].itemRoot == "000000000000a" assert nwGUI.theProject.tree["0000000000015"].itemClass == nwItemClass.CHARACTER @@ -119,7 +119,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) nwTree.setSelectedHandle("0000000000013") nwGUI.theProject.tree["0000000000013"].setParent(None) # This should not happen caplog.clear() - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False assert "Internal error" in caplog.text nwGUI.theProject.tree["0000000000013"].setParent("0000000000011") @@ -127,13 +127,13 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) with monkeypatch.context() as mp: mp.setattr(QInputDialog, "getText", lambda *a, **k: ("", False)) nwTree.setSelectedHandle("0000000000013") - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False # Get the trash folder nwTree.projTree._addTrashRoot() trashHandle = nwGUI.theProject.trashFolder() nwTree.setSelectedHandle(trashHandle) - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False assert "Cannot add new files or folders to the Trash folder" in caplog.text # Other Checks @@ -179,9 +179,9 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Add some files nwTree.setSelectedHandle("000000000000d") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -238,7 +238,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", ] - nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) + assert nwTree.projTree.undoLastMove() is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -295,9 +295,9 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Add some files nwTree.setSelectedHandle("000000000000d") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -379,10 +379,10 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Add a folder with two files nwTree.setSelectedHandle("0000000000009") - assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True nwTree.setSelectedHandle("0000000000014") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd")) assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd")) @@ -405,7 +405,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Add an empty folder, which can be deleted with no further restrictions nwTree.setSelectedHandle("0000000000009") - assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True assert nwTree.getTreeFromHandle("0000000000009") == ["0000000000009", "0000000000017"] nwTree.setSelectedHandle("0000000000017") From 77746f712f4dc35623b572c05062823de6d67dd7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 12:33:37 +0200 Subject: [PATCH 131/179] Fix inconsistend expand/collapse when tree items are moved --- novelwriter/gui/projtree.py | 33 +++++++++++++---------------- tests/test_gui/test_gui_projtree.py | 3 +-- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 6ad4d501..dc52ffc2 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -35,9 +35,9 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( - qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, - QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QLabel, QToolButton, - QSizePolicy, QInputDialog, QHBoxLayout, QShortcut + QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView, + QInputDialog, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, + QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter.core import NWDoc @@ -143,15 +143,6 @@ class GuiProjectView(QWidget): """ return self.projTree.hasFocus() - def anyFocus(self): - """Check if any widget or child widget has focus. - """ - if self.hasFocus(): - return True - if self.isAncestorOf(qApp.focusWidget()): - return True - return False - ## # Public Slots ## @@ -536,34 +527,40 @@ class GuiProjectTree(QTreeWidget): """Move an item up or down in the tree. """ tHandle = self.getSelectedHandle() - tItem = self._getTreeItem(tHandle) - if tItem is None: + trItem = self._getTreeItem(tHandle) + if trItem is None: logger.verbose("No item selected") return False - pItem = tItem.parent() + pItem = trItem.parent() + isExp = trItem.isExpanded() if pItem is None: - tIndex = self.indexOfTopLevelItem(tItem) + tIndex = self.indexOfTopLevelItem(trItem) nChild = self.topLevelItemCount() + nIndex = tIndex + nStep if nIndex < 0 or nIndex >= nChild: return False + cItem = self.takeTopLevelItem(tIndex) self.insertTopLevelItem(nIndex, cItem) else: - tIndex = pItem.indexOfChild(tItem) + tIndex = pItem.indexOfChild(trItem) nChild = pItem.childCount() + nIndex = tIndex + nStep if nIndex < 0 or nIndex >= nChild: return False + cItem = pItem.takeChild(tIndex) pItem.insertChild(nIndex, cItem) self._recordLastMove(cItem, pItem, tIndex) self._alertTreeChange(tHandle=tHandle, flush=True) self.clearSelection() - cItem.setSelected(True) + trItem.setSelected(True) + trItem.setExpanded(isExp) return True diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index fa904d34..ee1a1105 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -26,7 +26,7 @@ from tools import buildTestProject from PyQt5.QtWidgets import QMessageBox, QInputDialog -from novelwriter.gui.projtree import GuiProjectView, GuiProjectTree +from novelwriter.gui.projtree import GuiProjectTree from novelwriter.enum import nwItemType, nwItemClass @@ -163,7 +163,6 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) - monkeypatch.setattr(GuiProjectView, "anyFocus", lambda *a: True) nwTree = nwGUI.treeView From f0bef18b63911c6139165aaf1586e9be2ac78b43 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 13:44:45 +0200 Subject: [PATCH 132/179] Re-implement the tree context menu --- novelwriter/gui/projtree.py | 277 +++++++++++++++++++----------------- sample/nwProject.nwx | 24 ++-- 2 files changed, 158 insertions(+), 143 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index dc52ffc2..65614e05 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -5,7 +5,6 @@ GUI classes for the main window project tree File History: Created: 2018-09-29 [0.0.1] GuiProjectTree -Created: 2020-06-04 [0.7] GuiProjectTreeMenu Created: 2022-06-06 [1.7b1] GuiProjectView Created: 2022-06-06 [1.7b1] GuiProjectToolBar @@ -35,9 +34,9 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( - QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView, - QInputDialog, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, - QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QInputDialog, + QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, + QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter.core import NWDoc @@ -211,37 +210,37 @@ class GuiProjectToolBar(QWidget): # Add Item Menu self.mAdd = QMenu() - self.aAddEmpty = self.mAdd.addAction(self.tr("Plain Document")) + self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"])) self.aAddEmpty.setIcon(self.theTheme.getIcon("proj_document")) self.aAddEmpty.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) ) - self.aAddChap = self.mAdd.addAction(self.tr("Chapter Document")) + self.aAddChap = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"])) self.aAddChap.setIcon(self.theTheme.getIcon("proj_chapter")) self.aAddChap.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) ) - self.aAddScene = self.mAdd.addAction(self.tr("Scene Document")) + self.aAddScene = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"])) self.aAddScene.setIcon(self.theTheme.getIcon("proj_scene")) self.aAddScene.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) ) - self.aAddNote = self.mAdd.addAction(self.tr("Project Note")) + self.aAddNote = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["note"])) self.aAddNote.setIcon(self.theTheme.getIcon("proj_note")) self.aAddNote.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) ) - self.aAddFolder = self.mAdd.addAction(self.tr("Folder")) + self.aAddFolder = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["folder"])) self.aAddFolder.setIcon(self.theTheme.getIcon("proj_folder")) self.aAddFolder.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FOLDER) ) - self.mAddRoot = self.mAdd.addMenu(self.tr("Root Folder")) + self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"])) self._addRootFolderEntry(nwItemClass.NOVEL) self._addRootFolderEntry(nwItemClass.ARCHIVE) self.mAddRoot.addSeparator() @@ -268,6 +267,9 @@ class GuiProjectToolBar(QWidget): self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move")) self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove()) + self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash")) + self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash()) + self.tbMore = QToolButton(self) self.tbMore.setToolTip(self.tr("More Options")) self.tbMore.setIcon(self.theTheme.getIcon("menu")) @@ -335,9 +337,8 @@ class GuiProjectTree(QTreeWidget): ## # Context Menu - self.ctxMenu = GuiProjectTreeMenu(self) self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self._rightClickMenu) + self.customContextMenuRequested.connect(self._openContextMenu) # Tree Settings iPx = self.theTheme.baseIconSize @@ -972,19 +973,106 @@ class GuiProjectTree(QTreeWidget): return @pyqtSlot("QPoint") - def _rightClickMenu(self, clickPos): + def _openContextMenu(self, clickPos): """The user right clicked an element in the project tree, so we open a context menu in-place. """ + tItem = None selItem = self.itemAt(clickPos) if isinstance(selItem, QTreeWidgetItem): tHandle = selItem.data(self.C_NAME, Qt.UserRole) - self.setSelectedHandle(tHandle) # Just to be safe tItem = self.theProject.tree[tHandle] - 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)) + + if tItem is None: + logger.debug("No item found") + + ctxMenu = QMenu() + + # Trash Folder + # ============ + + trashHandle = self.theProject.tree.trashRoot() + if tItem.itemHandle == trashHandle and trashHandle is not None: + # The trash folder only has one option + ctxMenu.addAction( + self.tr("Empty Trash"), lambda: self.emptyTrash() + ) + ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) + return + + # Document Actions + # ================ + + isFile = tItem.itemType == nwItemType.FILE + if isFile: + ctxMenu.addAction( + self.tr("Open Document"), + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + ) + ctxMenu.addAction( + self.tr("View Document"), + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + ) + ctxMenu.addSeparator() + + # Edit Item Settings + # ================== + + if isFile: + ctxMenu.addAction( + self.tr("Toggle Exported"), lambda: self._toggleItemExported(tHandle) + ) + + if tItem.isNovelLike(): + mStatus = ctxMenu.addMenu(self.tr("Change Status")) + for n, (key, entry) in enumerate(self.theProject.statusItems.items()): + aStatus = mStatus.addAction(entry["icon"], entry["name"]) + aStatus.triggered.connect( + lambda n, key=key: self._changeItemStatus(tHandle, key) + ) + else: + mImport = ctxMenu.addMenu(self.tr("Change Importance")) + for n, (key, entry) in enumerate(self.theProject.importItems.items()): + aImport = mImport.addAction(entry["icon"], entry["name"]) + aImport.triggered.connect( + lambda n, key=key: self._changeItemImport(tHandle, key) + ) + + if isFile and tItem.documentAllowed(): + if tItem.itemLayout == nwItemLayout.NOTE: + ctxMenu.addAction( + self.tr("Change to {0}").format( + trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT]) + ), + lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT) + ) + else: + ctxMenu.addAction( + self.tr("Change to {0}").format( + trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE]) + ), + lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE) + ) + + ctxMenu.addSeparator() + + # Major Item Actions + # ================== + + ctxMenu.addAction( + self.tr("Edit Item Settings"), lambda: self.editTreeItem(tHandle) + ) + + if tItem.itemClass == nwItemClass.TRASH or tItem.itemType == nwItemType.ROOT: + ctxMenu.addAction( + self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle) + ) + else: + ctxMenu.addAction( + self.tr("Move to Trash"), lambda: self.deleteItem(tHandle) + ) + + ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) return @@ -1121,6 +1209,46 @@ class GuiProjectTree(QTreeWidget): return True + def _toggleItemExported(self, tHandle): + """Toggle the exported status of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + tItem.setExported(not tItem.isExported) + self.setTreeItemValues(tItem.itemHandle) + return + + def _changeItemStatus(self, tHandle, tStatus): + """Set a new status value of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + tItem.setStatus(tStatus) + self.setTreeItemValues(tItem.itemHandle) + return + + def _changeItemImport(self, tHandle, tImport): + """Set a new importance value of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + tItem.setImport(tImport) + self.setTreeItemValues(tItem.itemHandle) + return + + def _changeItemLayout(self, tHandle, itemLayout): + """Set a new item layout value of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): + tItem.setLayout(nwItemLayout.DOCUMENT) + self.setTreeItemValues(tItem.itemHandle) + elif itemLayout == nwItemLayout.NOTE: + tItem.setLayout(nwItemLayout.NOTE) + self.setTreeItemValues(tItem.itemHandle) + return + def _scanChildren(self, theList, tItem, tIndex): """This is a recursive function returning all items in a tree starting at a given QTreeWidgetItem. @@ -1244,116 +1372,3 @@ class GuiProjectTree(QTreeWidget): return # END Class GuiProjectTree - - -class GuiProjectTreeMenu(QMenu): - - def __init__(self, theTree): - QMenu.__init__(self, theTree) - - self.theTree = theTree - self.theItem = None - - self.editItem = QAction(self.tr("Edit Project Item"), self) - self.editItem.triggered.connect(self._doEditItem) - self.addAction(self.editItem) - - self.openItem = QAction(self.tr("Open Document"), self) - self.openItem.triggered.connect(self._doOpenItem) - self.addAction(self.openItem) - - self.viewItem = QAction(self.tr("View Document"), self) - self.viewItem.triggered.connect(self._doViewItem) - self.addAction(self.viewItem) - - self.toggleExp = QAction(self.tr("Toggle Included Flag"), self) - self.toggleExp.triggered.connect(self._doToggleExported) - self.addAction(self.toggleExp) - - self.deleteItem = QAction(self.tr("Delete Item"), self) - self.deleteItem.triggered.connect(self._doDeleteItem) - self.addAction(self.deleteItem) - - self.emptyTrash = QAction(self.tr("Empty Trash"), self) - self.emptyTrash.triggered.connect(self._doEmptyTrash) - self.addAction(self.emptyTrash) - - return - - def filterActions(self, theItem): - """Filter the menu entries available based on the properties of - the item the menu was activated on. - """ - self.theItem = theItem - - if theItem is None: - logger.error("Failed to extract information to build tree context menu") - return False - - trashHandle = self.theTree.theProject.tree.trashRoot() - - isTrash = theItem.itemHandle == trashHandle and trashHandle is not None - isFile = theItem.itemType == nwItemType.FILE - - self.editItem.setVisible(not isTrash) - self.openItem.setVisible(isFile) - self.viewItem.setVisible(isFile) - self.toggleExp.setVisible(isFile) - self.deleteItem.setVisible(not isTrash) - self.emptyTrash.setVisible(isTrash) - - return True - - ## - # Slots - ## - - @pyqtSlot() - def _doOpenItem(self): - """Forward the open document call to the main GUI window. - """ - if self.theItem is not None: - self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False) - return - - @pyqtSlot() - def _doViewItem(self): - """Forward the view document call to the main GUI window. - """ - if self.theItem is not None: - self.theTree.theParent.viewDocument(self.theItem.itemHandle) - return - - @pyqtSlot() - def _doEditItem(self): - """Forward the edit item call to the main GUI window. - """ - if self.theItem is not None: - self.theTree.theParent.editItem() - return - - @pyqtSlot() - def _doToggleExported(self): - """Flip the isExported flag of the current item. - """ - if self.theItem is not None: - self.theItem.setExported(not self.theItem.isExported) - self.theTree.setTreeItemValues(self.theItem.itemHandle) - return - - @pyqtSlot() - def _doDeleteItem(self): - """Forward the delete item call to the project tree. - """ - if self.theItem is not None: - self.theTree.deleteItem() - return - - @pyqtSlot() - def _doEmptyTrash(self): - """Forward the empty trash call to the project tree. - """ - self.theTree.emptyTrash() - return - -# END Class GuiProjectTreeMenu diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 4dd6396a..e8ee5c59 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1342 - 227 - 68090 + 1345 + 229 + 68286 False @@ -33,13 +33,13 @@
- New - Notes + New + Notes Started - 1st Draft - 2nd Draft + 1st Draft + 2nd Draft 3rd Draft - Finished + Finished None @@ -63,7 +63,7 @@
- Part One + Part One @@ -79,11 +79,11 @@ - Interlude + Interlude - A Note on Structure + A Note on Structure From 025463a28e0b65404002a9244d0c52f3526e19a3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 14:40:03 +0200 Subject: [PATCH 133/179] Improve project tree test coverage --- novelwriter/gui/projtree.py | 5 +- tests/test_gui/test_gui_projtree.py | 90 ++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 65614e05..372a2d84 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -985,6 +985,7 @@ class GuiProjectTree(QTreeWidget): if tItem is None: logger.debug("No item found") + return False ctxMenu = QMenu() @@ -998,7 +999,7 @@ class GuiProjectTree(QTreeWidget): self.tr("Empty Trash"), lambda: self.emptyTrash() ) ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) - return + return True # Document Actions # ================ @@ -1074,7 +1075,7 @@ class GuiProjectTree(QTreeWidget): ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) - return + return True ## # Events diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index ee1a1105..7b6d2f42 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,10 +24,10 @@ import os from tools import buildTestProject -from PyQt5.QtWidgets import QMessageBox, QInputDialog +from PyQt5.QtWidgets import QMessageBox, QInputDialog, QMenu from novelwriter.gui.projtree import GuiProjectTree -from novelwriter.enum import nwItemType, nwItemClass +from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass @pytest.mark.gui @@ -454,3 +454,89 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR nwGUI.closeProject() # END Test testGuiProjTree_DeleteItems + + +@pytest.mark.gui +def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): + """Test the building of the project tree context menu. All this does + is test that the menu builds. It doesn't open the actual menu, + """ + # Block message box + monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(QMenu, "exec_", lambda *a: None) + + # Create a project + prjDir = os.path.join(fncDir, "project") + buildTestProject(nwGUI, prjDir) + + # Handles for new objects + hNovelRoot = "0000000000008" + hTitlePage = "000000000000c" + hChapterDir = "000000000000d" + hChapterFile = "000000000000e" + hCharRoot = "000000000000a" + hCharNote = "0000000000011" + hNovelNote = "0000000000012" + + projTree = nwGUI.treeView.projTree + projTree._getTreeItem(hNovelRoot).setExpanded(True) + projTree._getTreeItem(hChapterDir).setExpanded(True) + + projTree._addTrashRoot() + hTrashRoot = projTree.theProject.tree.trashRoot() + + projTree.setSelectedHandle(hCharRoot) + projTree.newTreeItem(nwItemType.FILE) + projTree.setSelectedHandle(hNovelRoot) + projTree.newTreeItem(nwItemType.FILE, isNote=True) + + def itemPos(tHandle): + return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center() + + # No item under menu + assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False + + # Generate the possible menu combinarions + assert projTree._openContextMenu(itemPos(hTrashRoot)) is True + assert projTree._openContextMenu(itemPos(hNovelRoot)) is True + assert projTree._openContextMenu(itemPos(hNovelNote)) is True + assert projTree._openContextMenu(itemPos(hTitlePage)) is True + assert projTree._openContextMenu(itemPos(hChapterDir)) is True + assert projTree._openContextMenu(itemPos(hChapterFile)) is True + assert projTree._openContextMenu(itemPos(hCharRoot)) is True + assert projTree._openContextMenu(itemPos(hCharNote)) is True + + # Direct Edit Functions + # ===================== + # Trigger the dedicated functions the menu entries connect to + nwItem = projTree.theProject.tree[hNovelNote] + + # Toggle exported flag + assert nwItem.isExported is True + projTree._toggleItemExported(hNovelNote) + assert nwItem.isExported is False + + # Change item status + assert nwItem.itemStatus == "s000000" + projTree._changeItemStatus(hNovelNote, "s000001") + assert nwItem.itemStatus == "s000001" + + # Change item importance + assert nwItem.itemImport == "i000004" + projTree._changeItemImport(hNovelNote, "i000005") + assert nwItem.itemImport == "i000005" + + # Change item layout + assert nwItem.itemLayout == nwItemLayout.NOTE + projTree._changeItemLayout(hNovelNote, nwItemLayout.DOCUMENT) + assert nwItem.itemLayout == nwItemLayout.DOCUMENT + projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE) + assert nwItem.itemLayout == nwItemLayout.NOTE + + # qtbot.stop() + +# END Test testGuiProjTree_ContextMenu From 0181bebd7085905392184984c8b54f196cfd43c8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 15:33:32 +0200 Subject: [PATCH 134/179] Rename ambiguous theParent variable to mainGui --- novelwriter/core/project.py | 78 ++++++++++----------- novelwriter/core/tokenizer.py | 1 - novelwriter/dialogs/about.py | 22 +++--- novelwriter/dialogs/docmerge.py | 30 ++++---- novelwriter/dialogs/docsplit.py | 30 ++++---- novelwriter/dialogs/itemeditor.py | 8 +-- novelwriter/dialogs/preferences.py | 98 +++++++++++++------------- novelwriter/dialogs/projdetails.py | 28 ++++---- novelwriter/dialogs/projload.py | 14 ++-- novelwriter/dialogs/projsettings.py | 48 ++++++------- novelwriter/dialogs/quotes.py | 4 +- novelwriter/dialogs/updates.py | 10 +-- novelwriter/dialogs/wordlist.py | 14 ++-- novelwriter/gui/custom.py | 8 +-- novelwriter/gui/doceditor.py | 70 +++++++++---------- novelwriter/gui/dochighlight.py | 10 +-- novelwriter/gui/docviewer.py | 40 +++++------ novelwriter/gui/itemdetails.py | 10 +-- novelwriter/gui/mainmenu.py | 102 ++++++++++++++-------------- novelwriter/gui/noveltree.py | 18 ++--- novelwriter/gui/outline.py | 28 ++++---- novelwriter/gui/projtree.py | 48 ++++++------- novelwriter/gui/statusbar.py | 8 +-- novelwriter/gui/viewsbar.py | 20 +++--- novelwriter/tools/build.py | 28 ++++---- novelwriter/tools/lipsum.py | 14 ++-- novelwriter/tools/projwizard.py | 10 +-- novelwriter/tools/writingstats.py | 16 ++--- 28 files changed, 407 insertions(+), 408 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index cce4603d..dfb577b1 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -56,11 +56,11 @@ class NWProject(): FILE_VERSION = "1.4" # The current project file format version - def __init__(self, theParent): + def __init__(self, mainGui): # Internal - self.theParent = theParent - self.mainConf = novelwriter.CONFIG + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui # Core Elements self._optState = OptionState(self) # Project-specific GUI options @@ -414,7 +414,7 @@ class NWProject(): if not os.path.isfile(fileName): fileName = os.path.join(fileName, nwFiles.PROJ_FILE) if not os.path.isfile(fileName): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "File not found: {0}" ).format(fileName), nwAlert.ERROR) return False @@ -465,20 +465,20 @@ class NWProject(): try: nwXML = etree.parse(fileName) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to parse project xml." ), nwAlert.ERROR, exception=exc) # Trying to open backup file instead backFile = fileName[:-3]+"bak" if os.path.isfile(backFile): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Attempting to open backup project file instead." ), nwAlert.INFO) try: nwXML = etree.parse(backFile) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to parse project xml." ), nwAlert.ERROR, exception=exc) self.clearProject() @@ -501,7 +501,7 @@ class NWProject(): # =============== if not nwxRoot == "novelWriterXML": - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Project file does not appear to be a novelWriterXML file." ), nwAlert.ERROR) self.clearProject() @@ -527,7 +527,7 @@ class NWProject(): # stored and handled. Introduced in version 1.7. if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Unknown or unsupported novelWriter project file format. " "The project cannot be opened by this version of novelWriter. " "The file was saved with novelWriter version {0}." @@ -536,7 +536,7 @@ class NWProject(): return False if fileVersion != self.FILE_VERSION: - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("File Version"), self.tr( "The file format of your project is about to be updated. " @@ -552,7 +552,7 @@ class NWProject(): # ========================= if hexToInt(hexVersion) > hexToInt(novelwriter.__hexversion__): - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Version Conflict"), self.tr( "This project was saved by a newer version of " @@ -646,7 +646,7 @@ class NWProject(): for projItem in legacyList: errList = self._legacyDataFolder(projItem, errList) if errList: - self.theParent.makeAlert(errList, nwAlert.ERROR) + self.mainGui.makeAlert(errList, nwAlert.ERROR) # Clean up no longer used files self._deprecatedFiles() @@ -672,7 +672,7 @@ class NWProject(): self._writeLockFile() self.setProjectChanged(False) - self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) + self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self.projName)) return True @@ -683,7 +683,7 @@ class NWProject(): file. """ if self.projPath is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Project path not set, cannot save project." ), nwAlert.ERROR) return False @@ -763,7 +763,7 @@ class NWProject(): xml_declaration=True )) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to save project." ), nwAlert.ERROR, exception=exc) return False @@ -775,7 +775,7 @@ class NWProject(): os.replace(saveFile, backFile) os.replace(tempFile, saveFile) except OSError as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to save project." ), nwAlert.ERROR, exception=exc) return False @@ -788,7 +788,7 @@ class NWProject(): self.mainConf.saveRecentCache() self._writeLockFile() - self.theParent.setStatus(self.tr("Saved Project: {0}").format(self.projName)) + self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self.projName)) self.setProjectChanged(False) return True @@ -836,22 +836,22 @@ class NWProject(): def zipIt(self, doNotify): """Create a zip file of the entire project. """ - if not self.theParent.hasProject: + if not self.mainGui.hasProject: logger.error("No project open") return False logger.info("Backing up project") - self.theParent.setStatus(self.tr("Backing up project ...")) + self.mainGui.setStatus(self.tr("Backing up project ...")) if not (self.mainConf.backupPath and os.path.isdir(self.mainConf.backupPath)): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot backup project because no valid backup path is set. " "Please set a valid backup location in Preferences." ), nwAlert.ERROR) return False if not self.projName: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot backup project because no project name is set. " "Please set a Working Title in Project Settings." ), nwAlert.ERROR) @@ -864,13 +864,13 @@ class NWProject(): os.mkdir(baseDir) logger.debug("Created folder: %s", baseDir) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not create backup folder." ), nwAlert.ERROR, exception=exc) return False if baseDir and baseDir.startswith(self.projPath): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot backup project because the backup path is within the " "project folder to be backed up. Please choose a different " "backup path in Preferences." @@ -886,17 +886,17 @@ class NWProject(): self._writeLockFile() logger.info("Backup written to: %s", archName) if doNotify: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Backup archive file written to: {0}" ).format(f"{os.path.join(cleanName, archName)}.zip"), nwAlert.INFO) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not write backup archive." ), nwAlert.ERROR, exception=exc) return False - self.theParent.setStatus(self.tr( + self.mainGui.setStatus(self.tr( "Project backed up to '{0}'" ).format(f"{baseName}.zip")) @@ -924,7 +924,7 @@ class NWProject(): shutil.unpack_archive(pkgSample, projPath) isSuccess = True except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to create a new example project." ), nwAlert.ERROR, exception=exc) @@ -946,12 +946,12 @@ class NWProject(): isSuccess = True except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to create a new example project." ), nwAlert.ERROR, exception=exc) else: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to create a new example project. " "Could not find the necessary files. " "They seem to be missing from this installation." @@ -959,8 +959,8 @@ class NWProject(): if isSuccess: self.clearProject() - self.theParent.openProject(projPath) - self.theParent.rebuildIndex() + self.mainGui.openProject(projPath) + self.mainGui.rebuildIndex() return isSuccess @@ -985,14 +985,14 @@ class NWProject(): os.mkdir(projPath) logger.debug("Created folder: %s", projPath) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not create new project folder." ), nwAlert.ERROR, exception=exc) return False if os.path.isdir(projPath): if os.listdir(self.projPath): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "New project folder is not empty. " "Each project requires a dedicated project folder." ), nwAlert.ERROR) @@ -1042,14 +1042,14 @@ class NWProject(): self.doBackup = doBackup if doBackup: if not os.path.isdir(self.mainConf.backupPath): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "You must set a valid backup path in Preferences to use " "the automatic project backup feature." ), nwAlert.WARN) return False if self.projName == "": - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "You must set a valid project name in Project Settings to " "use the automatic project backup feature." ), nwAlert.WARN) @@ -1154,7 +1154,7 @@ class NWProject(): information to the GUI statusbar. """ self.projChanged = bValue - self.theParent.statusBar.doUpdateProjectStatus(bValue) + self.mainGui.statusBar.doUpdateProjectStatus(bValue) if bValue: # If we've changed the project at all, this should be True self.projAltered = True @@ -1384,7 +1384,7 @@ class NWProject(): os.mkdir(thePath) logger.debug("Created folder: %s", thePath) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not create folder." ), nwAlert.ERROR, exception=exc) return False @@ -1447,7 +1447,7 @@ class NWProject(): # Report status if len(orphanFiles) > 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Found {0} orphaned file(s) in project folder." ).format(len(orphanFiles)), nwAlert.WARN) else: @@ -1504,7 +1504,7 @@ class NWProject(): self._projTree.updateItemData(orphItem.itemHandle) if noWhere: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "One or more orphaned files could not be added back into the project. " "Make sure at least a Novel root folder exists." ), nwAlert.WARN) diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index bb5f3d8e..3a3b52f1 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -82,7 +82,6 @@ class Tokenizer(ABC): def __init__(self, theProject): self.theProject = theProject - self.theParent = theProject.theParent self.mainConf = novelwriter.CONFIG # Data Variables diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index f832343a..83e3c347 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -43,15 +43,15 @@ logger = logging.getLogger(__name__) class GuiAbout(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiAbout ...") self.setObjectName("GuiAbout") - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme self.outerBox = QVBoxLayout() self.innerBox = QHBoxLayout() @@ -63,7 +63,7 @@ class GuiAbout(QDialog): nPx = self.mainConf.pxInt(96) self.nwIcon = QLabel() - self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(self.mainGui.theTheme.getPixmap("novelwriter", (nPx, nPx))) self.lblName = QLabel("novelWriter") self.lblVers = QLabel(f"v{novelwriter.__version__}") self.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x")) @@ -191,8 +191,8 @@ class GuiAbout(QDialog): ]) ) - theTheme = self.theParent.theTheme - theIcons = self.theParent.theTheme.theIcons + theTheme = self.mainGui.theTheme + theIcons = self.mainGui.theTheme.theIcons if theTheme.themeName and theTheme.themeAuthor != "N/A": licURL = f"{theTheme.themeLicense}" aboutMsg += "

{0}

{1}

".format( @@ -279,9 +279,9 @@ class GuiAbout(QDialog): " padding-right: 0.8em;" "}}\n" ).format( - hColR=self.theParent.theTheme.colHead[0], - hColG=self.theParent.theTheme.colHead[1], - hColB=self.theParent.theTheme.colHead[2], + hColR=self.mainGui.theTheme.colHead[0], + hColG=self.mainGui.theTheme.colHead[1], + hColB=self.mainGui.theTheme.colHead[2], kColR=self.theTheme.colKey[0], kColG=self.theTheme.colKey[1], kColB=self.theTheme.colKey[2], diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 033f3698..23657fb5 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -41,15 +41,15 @@ logger = logging.getLogger(__name__) class GuiDocMerge(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiDocMerge ...") self.setObjectName("GuiDocMerge") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.sourceItem = None self.outerBox = QVBoxLayout() @@ -57,7 +57,7 @@ class GuiDocMerge(QDialog): self.headLabel = QLabel("{0}".format(self.tr("Documents to Merge"))) self.helpLabel = QHelpLabel( - self.tr("Drag and drop items to change the order."), self.theParent.theTheme.helpText + self.tr("Drag and drop items to change the order."), self.mainGui.theTheme.helpText ) self.listBox = QListWidget() @@ -102,7 +102,7 @@ class GuiDocMerge(QDialog): finalOrder.append(self.listBox.item(i).data(Qt.UserRole)) if len(finalOrder) == 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "No source documents found. Nothing to do." ), nwAlert.ERROR) return False @@ -113,21 +113,21 @@ class GuiDocMerge(QDialog): docText = inDoc.readDocument() docErr = inDoc.getError() if docText is None and docErr: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Failed to open document file."), docErr ], nwAlert.ERROR) if docText: theText += docText.rstrip("\n")+"\n\n" if self.sourceItem is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "No source folder selected. Nothing to do." ), nwAlert.ERROR) return False srcItem = self.theProject.tree[self.sourceItem] if srcItem is None: - self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR) + self.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR) return False nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent) @@ -137,13 +137,13 @@ class GuiDocMerge(QDialog): outDoc = NWDoc(self.theProject, nHandle) if not outDoc.writeDocument(theText): - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Could not save document."), outDoc.getError() ], nwAlert.ERROR) return False - self.theParent.treeView.revealNewTreeItem(nHandle) - self.theParent.openDocument(nHandle, doScroll=True) + self.mainGui.treeView.revealNewTreeItem(nHandle) + self.mainGui.openDocument(nHandle, doScroll=True) self._doClose() @@ -165,7 +165,7 @@ class GuiDocMerge(QDialog): are then added to the list view in order. The list itself can be reordered by the user. """ - tHandle = self.theParent.treeView.getSelectedHandle() + tHandle = self.mainGui.treeView.getSelectedHandle() self.sourceItem = tHandle if tHandle is None: return False @@ -175,12 +175,12 @@ class GuiDocMerge(QDialog): return False if nwItem.itemType is not nwItemType.FOLDER: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Element selected in the project tree must be a folder." ), nwAlert.ERROR) return False - for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle): + for sHandle in self.mainGui.treeView.getTreeFromHandle(tHandle): newItem = QListWidgetItem() nwItem = self.theProject.tree[sHandle] if nwItem.itemType is not nwItemType.FILE: diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 76e64d5f..4d2ecc4a 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -41,15 +41,15 @@ logger = logging.getLogger(__name__) class GuiDocSplit(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiDocSplit ...") self.setObjectName("GuiDocSplit") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.sourceItem = None self.sourceText = [] @@ -60,7 +60,7 @@ class GuiDocSplit(QDialog): self.headLabel = QLabel("{0}".format(self.tr("Document Headers"))) self.helpLabel = QHelpLabel( self.tr("Select the maximum level to split into files."), - self.theParent.theTheme.helpText + self.mainGui.theTheme.helpText ) self.listBox = QListWidget() @@ -115,14 +115,14 @@ class GuiDocSplit(QDialog): logger.verbose("GuiDocSplit split button clicked") if self.sourceItem is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "No source document selected. Nothing to do." ), nwAlert.ERROR) return False srcItem = self.theProject.tree[self.sourceItem] if srcItem is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not parse source document." ), nwAlert.ERROR) return False @@ -132,7 +132,7 @@ class GuiDocSplit(QDialog): docErr = inDoc.getError() if theText is None and docErr: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Failed to open document file."), docErr ], nwAlert.ERROR) @@ -153,12 +153,12 @@ class GuiDocSplit(QDialog): nFiles = len(finalOrder) if nFiles == 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "No headers found. Nothing to do." ), nwAlert.ERROR) return False - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Split Document"), "{0}

{1}".format( self.tr( @@ -175,7 +175,7 @@ class GuiDocSplit(QDialog): # Create the folder fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent) - self.theParent.treeView.revealNewTreeItem(fHandle) + self.mainGui.treeView.revealNewTreeItem(fHandle) logger.verbose("Creating folder '%s'", fHandle) # Loop through, and create the files @@ -196,12 +196,12 @@ class GuiDocSplit(QDialog): outDoc = NWDoc(self.theProject, nHandle) if not outDoc.writeDocument(theText): - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Could not save document."), outDoc.getError() ], nwAlert.ERROR) return False - self.theParent.treeView.revealNewTreeItem(nHandle) + self.mainGui.treeView.revealNewTreeItem(nHandle) self._doClose() @@ -226,7 +226,7 @@ class GuiDocSplit(QDialog): """ self.listBox.clear() if self.sourceItem is None: - self.sourceItem = self.theParent.treeView.getSelectedHandle() + self.sourceItem = self.mainGui.treeView.getSelectedHandle() if self.sourceItem is None: return False @@ -236,7 +236,7 @@ class GuiDocSplit(QDialog): return False if nwItem.itemType is not nwItemType.FILE: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Element selected in the project tree must be a file." ), nwAlert.ERROR) return False diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py index acf07134..7ad4eed0 100644 --- a/novelwriter/dialogs/itemeditor.py +++ b/novelwriter/dialogs/itemeditor.py @@ -41,15 +41,15 @@ logger = logging.getLogger(__name__) class GuiItemEditor(QDialog): - def __init__(self, theParent, tHandle): - QDialog.__init__(self, theParent) + def __init__(self, mainGui, tHandle): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiItemEditor ...") self.setObjectName("GuiItemEditor") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject ## # Build GUI diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 755b9198..4b74655f 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -43,25 +43,25 @@ logger = logging.getLogger(__name__) class GuiPreferences(PagedDialog): - def __init__(self, theParent): - PagedDialog.__init__(self, theParent) + def __init__(self, mainGui): + PagedDialog.__init__(self, mainGui) logger.debug("Initialising GuiPreferences ...") self.setObjectName("GuiPreferences") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.setWindowTitle(self.tr("Preferences")) - self.tabGeneral = GuiPreferencesGeneral(self.theParent) - self.tabProjects = GuiPreferencesProjects(self.theParent) - self.tabDocs = GuiPreferencesDocuments(self.theParent) - self.tabEditor = GuiPreferencesEditor(self.theParent) - self.tabSyntax = GuiPreferencesSyntax(self.theParent) - self.tabAuto = GuiPreferencesAutomation(self.theParent) - self.tabQuote = GuiPreferencesQuotes(self.theParent) + self.tabGeneral = GuiPreferencesGeneral(self.mainGui) + self.tabProjects = GuiPreferencesProjects(self.mainGui) + self.tabDocs = GuiPreferencesDocuments(self.mainGui) + self.tabEditor = GuiPreferencesEditor(self.mainGui) + self.tabSyntax = GuiPreferencesSyntax(self.mainGui) + self.tabAuto = GuiPreferencesAutomation(self.mainGui) + self.tabQuote = GuiPreferencesQuotes(self.mainGui) self.addTab(self.tabGeneral, self.tr("General")) self.addTab(self.tabProjects, self.tr("Projects")) @@ -102,12 +102,12 @@ class GuiPreferences(PagedDialog): self.tabQuote.saveValues() if needsRestart: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Some changes will not be applied until novelWriter has been restarted." ), nwAlert.INFO) if refreshTree: - self.theParent.treeView.populateTree() + self.mainGui.treeView.populateTree() self._saveWindowSize() self.accept() @@ -138,12 +138,12 @@ class GuiPreferences(PagedDialog): class GuiPreferencesGeneral(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme # The Form self.mainForm = QConfigLayout() @@ -344,12 +344,12 @@ class GuiPreferencesGeneral(QWidget): class GuiPreferencesProjects(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme # The Form self.mainForm = QConfigLayout() @@ -505,12 +505,12 @@ class GuiPreferencesProjects(QWidget): class GuiPreferencesDocuments(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme # The Form self.mainForm = QConfigLayout() @@ -666,12 +666,12 @@ class GuiPreferencesDocuments(QWidget): class GuiPreferencesEditor(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme # The Form self.mainForm = QConfigLayout() @@ -688,7 +688,7 @@ class GuiPreferencesEditor(QWidget): self.spellLanguage = QComboBox(self) self.spellLanguage.setMaximumWidth(mW) - langAvail = self.theParent.docEditor.spEnchant.listDictionaries() + langAvail = self.mainGui.docEditor.spEnchant.listDictionaries() if self.mainConf.hasEnchant: if langAvail: for spTag, spProv in langAvail: @@ -840,12 +840,12 @@ class GuiPreferencesEditor(QWidget): class GuiPreferencesSyntax(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme # The Form self.mainForm = QConfigLayout() @@ -943,12 +943,12 @@ class GuiPreferencesSyntax(QWidget): class GuiPreferencesAutomation(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme # The Form self.mainForm = QConfigLayout() @@ -1100,12 +1100,12 @@ class GuiPreferencesAutomation(QWidget): class GuiPreferencesQuotes(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme # The Form self.mainForm = QConfigLayout() diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 258b82e4..8ab1943f 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -43,15 +43,15 @@ logger = logging.getLogger(__name__) class GuiProjectDetails(PagedDialog): - def __init__(self, theParent): - PagedDialog.__init__(self, theParent) + def __init__(self, mainGui): + PagedDialog.__init__(self, mainGui) logger.debug("Initialising GuiProjectDetails ...") self.setObjectName("GuiProjectDetails") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.setWindowTitle(self.tr("Project Details")) @@ -66,8 +66,8 @@ class GuiProjectDetails(PagedDialog): self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH)) ) - self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject) - self.tabContents = GuiProjectDetailsContents(self.theParent, self.theProject) + self.tabMain = GuiProjectDetailsMain(self.mainGui, self.theProject) + self.tabContents = GuiProjectDetailsContents(self.mainGui, self.theProject) self.addTab(self.tabMain, self.tr("Overview")) self.addTab(self.tabContents, self.tr("Contents")) @@ -139,13 +139,13 @@ class GuiProjectDetails(PagedDialog): class GuiProjectDetailsMain(QWidget): - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent self.theProject = theProject - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theTheme = mainGui.theTheme fPx = self.theTheme.fontPixelSize fPt = self.theTheme.fontPointSize @@ -271,13 +271,13 @@ class GuiProjectDetailsContents(QWidget): C_PAGE = 3 C_PROG = 4 - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent self.theProject = theProject - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theTheme = mainGui.theTheme # Internal self._theToC = [] diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index bbae4920..6d296980 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -53,15 +53,15 @@ class GuiProjectLoad(QDialog): C_COUNT = 1 C_TIME = 2 - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiProjectLoad ...") self.setObjectName("GuiProjectLoad") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theTheme = mainGui.theTheme self.openState = self.NONE_STATE self.openPath = None @@ -80,7 +80,7 @@ class GuiProjectLoad(QDialog): self.setModal(True) self.nwIcon = QLabel() - self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(self.mainGui.theTheme.getPixmap("novelwriter", (nPx, nPx))) self.innerBox.addWidget(self.nwIcon, 0, Qt.AlignTop) self.projectForm = QGridLayout() @@ -225,7 +225,7 @@ class GuiProjectLoad(QDialog): selList = self.listBox.selectedItems() if selList: projName = selList[0].text(self.C_NAME) - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Remove Entry"), self.tr( "Remove '{0}' from the recent projects list? " @@ -280,7 +280,7 @@ class GuiProjectLoad(QDialog): sortList = sorted(dataList, key=lambda x: x[1], reverse=True) for theTitle, theTime, theWords, projPath in sortList: newItem = QTreeWidgetItem([""]*4) - newItem.setIcon(self.C_NAME, self.theParent.theTheme.getIcon("proj_nwx")) + newItem.setIcon(self.C_NAME, self.mainGui.theTheme.getIcon("proj_nwx")) newItem.setText(self.C_NAME, theTitle) newItem.setData(self.C_NAME, Qt.UserRole, projPath) newItem.setText(self.C_COUNT, formatInt(theWords)) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 8bbdcee3..c7531866 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -43,15 +43,15 @@ logger = logging.getLogger(__name__) class GuiProjectSettings(PagedDialog): - def __init__(self, theParent): - PagedDialog.__init__(self, theParent) + def __init__(self, mainGui): + PagedDialog.__init__(self, mainGui) logger.debug("Initialising GuiProjectSettings ...") self.setObjectName("GuiProjectSettings") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.theProject.countStatus() self.setWindowTitle(self.tr("Project Settings")) @@ -67,10 +67,10 @@ class GuiProjectSettings(PagedDialog): self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH)) ) - self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) - self.tabStatus = GuiProjectEditStatus(self.theParent, self.theProject, True) - self.tabImport = GuiProjectEditStatus(self.theParent, self.theProject, False) - self.tabReplace = GuiProjectEditReplace(self.theParent, self.theProject) + self.tabMain = GuiProjectEditMain(self.mainGui, self.theProject) + self.tabStatus = GuiProjectEditStatus(self.mainGui, self.theProject, True) + self.tabImport = GuiProjectEditStatus(self.mainGui, self.theProject, False) + self.tabReplace = GuiProjectEditReplace(self.mainGui, self.theProject) self.addTab(self.tabMain, self.tr("Settings")) self.addTab(self.tabStatus, self.tr("Status")) @@ -121,7 +121,7 @@ class GuiProjectSettings(PagedDialog): self.theProject.setImportColours(newList, delList) if self.tabStatus.colChanged or self.tabImport.colChanged: - self.theParent.rebuildTrees() + self.mainGui.rebuildTrees() if self.tabReplace.arChanged: newList = self.tabReplace.getNewList() @@ -166,22 +166,22 @@ class GuiProjectSettings(PagedDialog): class GuiProjectEditMain(QWidget): - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent + self.mainGui = mainGui self.theProject = theProject # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theParent.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainGui.theTheme.helpText) self.setLayout(self.mainForm) self.mainForm.addGroupLabel(self.tr("Project Settings")) xW = self.mainConf.pxInt(250) - xH = round(4.8*self.theParent.theTheme.fontPixelSize) + xH = round(4.8*self.mainGui.theTheme.fontPixelSize) self.editName = QLineEdit() self.editName.setMaxLength(200) @@ -216,7 +216,7 @@ class GuiProjectEditMain(QWidget): self.spellLang = QComboBox(self) self.spellLang.setMaximumWidth(xW) self.spellLang.addItem(self.tr("Default"), "None") - langAvail = self.theParent.docEditor.spEnchant.listDictionaries() + langAvail = self.mainGui.docEditor.spEnchant.listDictionaries() for spTag, spProv in langAvail: qLocal = QLocale(spTag) spLang = qLocal.nativeLanguageName().title() @@ -256,13 +256,13 @@ class GuiProjectEditStatus(QWidget): COL_ROLE = Qt.UserRole + 1 NUM_ROLE = Qt.UserRole + 2 - def __init__(self, theParent, theProject, isStatus): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject, isStatus): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent + self.mainGui = mainGui self.theProject = theProject - self.theTheme = theParent.theTheme + self.theTheme = mainGui.theTheme if isStatus: self.theStatus = self.theProject.statusItems @@ -411,7 +411,7 @@ class GuiProjectEditStatus(QWidget): if selItem is not None: iRow = self.listBox.indexOfTopLevelItem(selItem) if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot delete a status item that is in use." ), nwAlert.ERROR) else: @@ -527,12 +527,12 @@ class GuiProjectEditReplace(QWidget): COL_KEY = 0 COL_REPL = 1 - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theTheme = mainGui.theTheme self.theProject = theProject self.arChanged = False diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index e8426adf..299386d8 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -42,8 +42,8 @@ class GuiQuoteSelect(QDialog): selectedQuote = "" - def __init__(self, theParent=None, currentQuote='"'): - QDialog.__init__(self, parent=theParent) + def __init__(self, parent=None, currentQuote='"'): + QDialog.__init__(self, parent=parent) self.mainConf = novelwriter.CONFIG diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py index fccc49c2..a8f3c8a9 100644 --- a/novelwriter/dialogs/updates.py +++ b/novelwriter/dialogs/updates.py @@ -43,14 +43,14 @@ logger = logging.getLogger(__name__) class GuiUpdates(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiUpdates ...") self.setObjectName("GuiUpdates") - self.mainConf = novelwriter.CONFIG - self.theParent = theParent + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui self.setWindowTitle(self.tr("Check for Updates")) @@ -61,7 +61,7 @@ class GuiUpdates(QDialog): # Left Box self.nwIcon = QLabel() - self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(self.mainGui.theTheme.getPixmap("novelwriter", (nPx, nPx))) self.leftBox = QVBoxLayout() self.leftBox.addWidget(self.nwIcon) diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 77a4fb5a..172ae846 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -42,16 +42,16 @@ logger = logging.getLogger(__name__) class GuiWordList(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiWordList ...") self.setObjectName("GuiWordList") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theTheme = mainGui.theTheme + self.theProject = mainGui.theProject self.setWindowTitle(self.tr("Project Word List")) @@ -121,13 +121,13 @@ class GuiWordList(QDialog): """ newWord = self.newEntry.text().strip() if newWord == "": - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot add a blank word." ), nwAlert.ERROR) return False if self.listBox.findItems(newWord, Qt.MatchExactly): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The word '{0}' is already in the word list." ).format(newWord), nwAlert.ERROR) return False diff --git a/novelwriter/gui/custom.py b/novelwriter/gui/custom.py index 62dd3c91..e571a5a1 100644 --- a/novelwriter/gui/custom.py +++ b/novelwriter/gui/custom.py @@ -376,8 +376,8 @@ class QSwitch(QAbstractButton): class PagedDialog(QDialog): - def __init__(self, theParent=None): - QDialog.__init__(self, parent=theParent) + def __init__(self, parent=None): + QDialog.__init__(self, parent=parent) self._tabBar = VerticalTabBar(self) self._tabBar.setExpanding(False) @@ -426,8 +426,8 @@ class PagedDialog(QDialog): class VerticalTabBar(QTabBar): - def __init__(self, theParent=None): - QTabBar.__init__(self, parent=theParent) + def __init__(self, parent=None): + QTabBar.__init__(self, parent=parent) self._mW = novelwriter.CONFIG.pxInt(150) return diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 5ede17f1..e1b1d948 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -72,16 +72,16 @@ class GuiDocEditor(QTextEdit): docCountsChanged = pyqtSignal(str, int, int, int) loadDocumentTagRequest = pyqtSignal(str, Enum) - def __init__(self, theParent): - QTextEdit.__init__(self, theParent) + def __init__(self, mainGui): + QTextEdit.__init__(self, mainGui) logger.debug("Initialising GuiDocEditor ...") # Class Variables self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theTheme = mainGui.theTheme + self.theProject = mainGui.theProject self._nwDocument = None self._nwItem = None @@ -124,7 +124,7 @@ class GuiDocEditor(QTextEdit): # Syntax self.spEnchant = NWSpellEnchant() - self.highLight = GuiDocHighlighter(qDoc, self.theParent, self.spEnchant) + self.highLight = GuiDocHighlighter(qDoc, self.mainGui, self.spEnchant) # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -341,7 +341,7 @@ class GuiDocEditor(QTextEdit): docSize = len(theDoc) if docSize > nwConst.MAX_DOCSIZE: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The document you are trying to open is too big. " "The document size is {0} MB. " "The maximum size allowed is {1} MB." @@ -417,7 +417,7 @@ class GuiDocEditor(QTextEdit): # Update the status bar if self._nwItem is not None: - self.theParent.setStatus( + self.mainGui.setStatus( self.tr("Opened Document: {0}").format(self._nwItem.itemName) ) @@ -442,7 +442,7 @@ class GuiDocEditor(QTextEdit): """ docSize = len(theText) if docSize > nwConst.MAX_DOCSIZE: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The text you are trying to add is too big. " "The text size is {0} MB. " "The maximum size allowed is {1} MB." @@ -488,7 +488,7 @@ class GuiDocEditor(QTextEdit): if not self._nwDocument.writeDocument(docText): saveOk = False if self._nwDocument._currHash != self._nwDocument._prevHash: - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("File Changed on Disk"), self.tr( "This document has been changed outside of novelWriter " @@ -499,7 +499,7 @@ class GuiDocEditor(QTextEdit): saveOk = self._nwDocument.writeDocument(docText, forceWrite=True) if not saveOk: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Could not save document."), self._nwDocument.getError() ], nwAlert.ERROR) @@ -512,17 +512,17 @@ class GuiDocEditor(QTextEdit): newHeader = self.theProject.index.getHandleHeaderLevel(tHandle) if self._updateHeaders(checkLevel=True): - self.theParent.requestNovelTreeRefresh() + self.mainGui.requestNovelTreeRefresh() else: - self.theParent.novelView.updateWordCounts(tHandle) + self.mainGui.novelView.updateWordCounts(tHandle) if oldHeader != newHeader: - self.theParent.treeView.setTreeItemValues(tHandle) - self.theParent.treeMeta.updateViewBox(tHandle) + self.mainGui.treeView.setTreeItemValues(tHandle) + self.mainGui.treeMeta.updateViewBox(tHandle) self.docFooter.updateInfo() # Update the status bar - self.theParent.setStatus( + self.mainGui.setStatus( self.tr("Saved Document: {0}").format(self._nwItem.itemName) ) @@ -544,8 +544,8 @@ class GuiDocEditor(QTextEdit): sH = hBar.height() if hBar.isVisible() else 0 tM = cM - if self.mainConf.textWidth > 0 or self.theParent.isFocusMode: - tW = self.mainConf.getTextWidth(self.theParent.isFocusMode) + if self.mainConf.textWidth > 0 or self.mainGui.isFocusMode: + tW = self.mainConf.getTextWidth(self.mainGui.isFocusMode) tM = max((wW - sW - tW)//2, cM) tB = self.frameWidth() @@ -704,7 +704,7 @@ class GuiDocEditor(QTextEdit): if not self.mainConf.hasEnchant: if theMode: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Spell checking requires the package PyEnchant. " "It does not appear to be installed." ), nwAlert.INFO) @@ -714,7 +714,7 @@ class GuiDocEditor(QTextEdit): theMode = False self._spellCheck = theMode - self.theParent.mainMenu.setSpellCheck(theMode) + self.mainGui.mainMenu.setSpellCheck(theMode) self.theProject.setSpellCheck(theMode) self.highLight.setSpellCheck(theMode) if not self._bigDoc: @@ -742,7 +742,7 @@ class GuiDocEditor(QTextEdit): qApp.restoreOverrideCursor() afTime = time() logger.debug("Document highlighted in %.3f ms", 1000*(afTime-bfTime)) - self.theParent.statusBar.setStatus(self.tr("Spell check complete")) + self.mainGui.statusBar.setStatus(self.tr("Spell check complete")) return True @@ -1085,7 +1085,7 @@ class GuiDocEditor(QTextEdit): self._lastFind = None if self.document().characterCount() > nwConst.MAX_DOCSIZE: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The document has grown too big and you cannot add more text to it. " "The maximum size of a single novelWriter document is {0} MB." ).format( @@ -1245,7 +1245,7 @@ class GuiDocEditor(QTextEdit): if time() - self._lastEdit < 5 * self.wcInterval: logger.verbose("Running word counter") - self.theParent.threadPool.start(self.wCounterDoc) + self.mainGui.threadPool.start(self.wCounterDoc) return @@ -1302,7 +1302,7 @@ class GuiDocEditor(QTextEdit): logger.verbose("Selection word counter is busy") return - self.theParent.threadPool.start(self.wCounterSel) + self.mainGui.threadPool.start(self.wCounterSel) return @@ -1381,7 +1381,7 @@ class GuiDocEditor(QTextEdit): self.docSearch.setResultCount(0, 0) self._lastFind = None if self.docSearch.doNextFile and not goBack: - self.theParent.openNextDocument( + self.mainGui.openNextDocument( self._docHandle, wrapAround=self.docSearch.doLoop ) self.beginSearch() @@ -1401,7 +1401,7 @@ class GuiDocEditor(QTextEdit): if resIdx > maxIdx: if self.docSearch.doNextFile and not goBack: - self.theParent.openNextDocument( + self.mainGui.openNextDocument( self._docHandle, wrapAround=self.docSearch.doLoop ) self.beginSearch() @@ -1645,7 +1645,7 @@ class GuiDocEditor(QTextEdit): """ theCursor = self.textCursor() if not theCursor.hasSelection(): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Please select some text before calling replace quotes." ), nwAlert.ERROR) return False @@ -2187,7 +2187,7 @@ class GuiDocEditSearch(QFrame): self.mainConf = novelwriter.CONFIG self.docEditor = docEditor - self.theParent = docEditor.theParent + self.mainGui = docEditor.mainGui self.theProject = docEditor.theProject self.theTheme = docEditor.theTheme @@ -2575,7 +2575,7 @@ class GuiDocEditHeader(QWidget): self.mainConf = novelwriter.CONFIG self.docEditor = docEditor - self.theParent = docEditor.theParent + self.mainGui = docEditor.mainGui self.theProject = docEditor.theProject self.theTheme = docEditor.theTheme @@ -2733,7 +2733,7 @@ class GuiDocEditHeader(QWidget): This function is called by the GuiMain class via the toggleFocusMode function and should not be activated directly. """ - if self.theParent.isFocusMode: + if self.mainGui.isFocusMode: self.minmaxButton.setIcon(self.theTheme.getIcon("minimise")) else: self.minmaxButton.setIcon(self.theTheme.getIcon("maximise")) @@ -2746,7 +2746,7 @@ class GuiDocEditHeader(QWidget): def _editDocument(self): """Open the edit item dialog from the main GUI. """ - self.theParent.editItem(self._docHandle) + self.mainGui.editItem(self._docHandle) return def _searchDocument(self): @@ -2758,7 +2758,7 @@ class GuiDocEditHeader(QWidget): def _closeDocument(self): """Trigger the close editor on the main window. """ - self.theParent.closeDocEditor() + self.mainGui.closeDocEditor() self.editButton.setVisible(False) self.searchButton.setVisible(False) self.closeButton.setVisible(False) @@ -2768,7 +2768,7 @@ class GuiDocEditHeader(QWidget): def _minmaxDocument(self): """Switch on or off Focus Mode. """ - self.theParent.toggleFocusMode() + self.mainGui.toggleFocusMode() return ## @@ -2779,7 +2779,7 @@ class GuiDocEditHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.theParent.treeView.setSelectedHandle(self._docHandle, doScroll=True) + self.mainGui.treeView.setSelectedHandle(self._docHandle, doScroll=True) return # END Class GuiDocEditHeader @@ -2799,7 +2799,7 @@ class GuiDocEditFooter(QWidget): self.mainConf = novelwriter.CONFIG self.docEditor = docEditor - self.theParent = docEditor.theParent + self.mainGui = docEditor.mainGui self.theProject = docEditor.theProject self.theTheme = docEditor.theTheme diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index bd2d78eb..d811332c 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -46,16 +46,16 @@ class GuiDocHighlighter(QSyntaxHighlighter): BLOCK_META = 2 BLOCK_TITLE = 4 - def __init__(self, theDoc, theParent, spEnchant): + def __init__(self, theDoc, mainGui, spEnchant): QSyntaxHighlighter.__init__(self, theDoc) logger.debug("Initialising GuiDocHighlighter ...") self.mainConf = novelwriter.CONFIG self.theDoc = theDoc self.spEnchant = spEnchant - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theTheme = mainGui.theTheme + self.theProject = mainGui.theProject self.theHandle = None self.spellCheck = False self.spellRx = None @@ -288,7 +288,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): if theText.startswith("@"): # Keywords and commands self.setCurrentBlockState(self.BLOCK_META) pIndex = self.theProject.index - tItem = self.theParent.theProject.tree[self.theHandle] + tItem = self.mainGui.theProject.tree[self.theHandle] isValid, theBits, thePos = pIndex.scanThis(theText) isGood = pIndex.checkThese(theBits, tItem) if isValid: diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 89ae8ea2..5a9c61cd 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -53,16 +53,16 @@ class GuiDocViewer(QTextBrowser): loadDocumentTagRequest = pyqtSignal(str, Enum) - def __init__(self, theParent): - QTextBrowser.__init__(self, theParent) + def __init__(self, mainGui): + QTextBrowser.__init__(self, mainGui) logger.debug("Initialising GuiDocViewer ...") # Class Variables self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theTheme = mainGui.theTheme + self.theProject = mainGui.theProject # Internal Variables self._docHandle = None @@ -221,7 +221,7 @@ class GuiDocViewer(QTextBrowser): self.updateDocMargins() # Make sure the main GUI knows we changed the content - self.theParent.viewMeta.refreshReferences(tHandle) + self.mainGui.viewMeta.refreshReferences(tHandle) # Since we change the content while it may still be rendering, we mark # the document dirty again to make sure it's re-rendered properly. @@ -714,7 +714,7 @@ class GuiDocViewHeader(QWidget): self.mainConf = novelwriter.CONFIG self.docViewer = docViewer - self.theParent = docViewer.theParent + self.mainGui = docViewer.mainGui self.theProject = docViewer.theProject self.theTheme = docViewer.theTheme @@ -882,14 +882,14 @@ class GuiDocViewHeader(QWidget): def _closeDocument(self): """Trigger the close editor/viewer on the main window. """ - self.theParent.closeDocViewer() + self.mainGui.closeDocViewer() return def _refreshDocument(self): """Reload the content of the document. """ - if self.docViewer.docHandle() == self.theParent.docEditor.docHandle(): - self.theParent.saveDocument() + if self.docViewer.docHandle() == self.mainGui.docEditor.docHandle(): + self.mainGui.saveDocument() self.docViewer.reloadText() return @@ -901,7 +901,7 @@ class GuiDocViewHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.theParent.treeView.setSelectedHandle(self._docHandle, doScroll=True) + self.mainGui.treeView.setSelectedHandle(self._docHandle, doScroll=True) return # END Class GuiDocViewHeader @@ -921,9 +921,9 @@ class GuiDocViewFooter(QWidget): self.mainConf = novelwriter.CONFIG self.docViewer = docViewer - self.theParent = docViewer.theParent + self.mainGui = docViewer.mainGui self.theTheme = docViewer.theTheme - self.viewMeta = docViewer.theParent.viewMeta + self.viewMeta = docViewer.mainGui.viewMeta # Internal Variables self._docHandle = None @@ -1140,14 +1140,14 @@ class GuiDocViewFooter(QWidget): class GuiDocViewDetails(QScrollArea): - def __init__(self, theParent): - QScrollArea.__init__(self, theParent) + def __init__(self, mainGui): + QScrollArea.__init__(self, mainGui) logger.debug("Initialising GuiDocViewDetails ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theProject = mainGui.theProject + self.theTheme = mainGui.theTheme self.refList = QLabel("") self.refList.setWordWrap(True) @@ -1181,7 +1181,7 @@ class GuiDocViewDetails(QScrollArea): """Update the current list of document references from the project index. """ - if self.theParent.docViewer.stickyRef: + if self.mainGui.docViewer.stickyRef: return theRefs = self.theProject.index.getBackReferenceList(tHandle) @@ -1209,7 +1209,7 @@ class GuiDocViewDetails(QScrollArea): if len(theLink) == 21: tHandle = theLink[:13] tAnchor = theLink[13:] - self.theParent.viewDocument(tHandle, tAnchor) + self.mainGui.viewDocument(tHandle, tAnchor) return # END Class GuiDocViewDetails diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index cf9e8017..97d4ba0e 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -38,14 +38,14 @@ logger = logging.getLogger(__name__) class GuiItemDetails(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) logger.debug("Initialising GuiItemDetails ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theProject = mainGui.theProject + self.theTheme = mainGui.theTheme # Internal Variables self._itemHandle = None diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 33b43cfb..f482544f 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -41,13 +41,13 @@ logger = logging.getLogger(__name__) class GuiMainMenu(QMenuBar): - def __init__(self, theParent): - QMenuBar.__init__(self, theParent) + def __init__(self, mainGui): + QMenuBar.__init__(self, mainGui) logger.debug("Initialising GuiMainMenu ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject # Build Menu self._buildProjectMenu() @@ -61,9 +61,9 @@ class GuiMainMenu(QMenuBar): self._buildHelpMenu() # Function Pointers - self._docAction = self.theParent.passDocumentAction - self._docInsert = self.theParent.docEditor.insertText - self._insertKeyWord = self.theParent.docEditor.insertKeyWord + self._docAction = self.mainGui.passDocumentAction + self._docInsert = self.mainGui.docEditor.insertText + self._insertKeyWord = self.mainGui.docEditor.insertKeyWord logger.debug("GuiMainMenu initialisation complete") @@ -94,7 +94,7 @@ class GuiMainMenu(QMenuBar): flag is handled by the document editor class, so we make no decision, just pass a None to the function and let it decide. """ - self.theParent.docEditor.toggleSpellCheck(None) + self.mainGui.docEditor.toggleSpellCheck(None) return True def _openWebsite(self, theUrl): @@ -123,25 +123,25 @@ class GuiMainMenu(QMenuBar): # Project > New Project self.aNewProject = QAction(self.tr("New Project"), self) - self.aNewProject.triggered.connect(lambda: self.theParent.newProject(None)) + self.aNewProject.triggered.connect(lambda: self.mainGui.newProject(None)) self.projMenu.addAction(self.aNewProject) # Project > Open Project self.aOpenProject = QAction(self.tr("Open Project"), self) self.aOpenProject.setShortcut("Ctrl+Shift+O") - self.aOpenProject.triggered.connect(lambda: self.theParent.showProjectLoadDialog()) + self.aOpenProject.triggered.connect(lambda: self.mainGui.showProjectLoadDialog()) self.projMenu.addAction(self.aOpenProject) # Project > Save Project self.aSaveProject = QAction(self.tr("Save Project"), self) self.aSaveProject.setShortcut("Ctrl+Shift+S") - self.aSaveProject.triggered.connect(lambda: self.theParent.saveProject()) + self.aSaveProject.triggered.connect(lambda: self.mainGui.saveProject()) self.projMenu.addAction(self.aSaveProject) # Project > Close Project self.aCloseProject = QAction(self.tr("Close Project"), self) self.aCloseProject.setShortcut("Ctrl+Shift+W") - self.aCloseProject.triggered.connect(lambda: self.theParent.closeProject(False)) + self.aCloseProject.triggered.connect(lambda: self.mainGui.closeProject(False)) self.projMenu.addAction(self.aCloseProject) # Project > Separator @@ -150,13 +150,13 @@ class GuiMainMenu(QMenuBar): # Project > Project Settings self.aProjectSettings = QAction(self.tr("Project Settings"), self) self.aProjectSettings.setShortcut("Ctrl+Shift+,") - self.aProjectSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog()) + self.aProjectSettings.triggered.connect(lambda: self.mainGui.showProjectSettingsDialog()) self.projMenu.addAction(self.aProjectSettings) # Project > Project Details self.aProjectDetails = QAction(self.tr("Project Details"), self) self.aProjectDetails.setShortcut("Shift+F6") - self.aProjectDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog()) + self.aProjectDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog()) self.projMenu.addAction(self.aProjectDetails) # Project > Separator @@ -165,18 +165,18 @@ class GuiMainMenu(QMenuBar): # Project > Edit self.aEditItem = QAction(self.tr("Edit Item"), self) self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) - self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None)) + self.aEditItem.triggered.connect(lambda: self.mainGui.editItem(None)) self.projMenu.addAction(self.aEditItem) # Project > Delete self.aDeleteItem = QAction(self.tr("Delete Item"), self) self.aDeleteItem.setShortcut("Ctrl+Shift+Del") - self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None)) + self.aDeleteItem.triggered.connect(lambda: self.mainGui.treeView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) # Project > Empty Trash self.aEmptyTrash = QAction(self.tr("Empty Trash"), self) - self.aEmptyTrash.triggered.connect(lambda: self.theParent.treeView.emptyTrash()) + self.aEmptyTrash.triggered.connect(lambda: self.mainGui.treeView.emptyTrash()) self.projMenu.addAction(self.aEmptyTrash) # Project > Separator @@ -186,7 +186,7 @@ class GuiMainMenu(QMenuBar): self.aExitNW = QAction(self.tr("Exit"), self) self.aExitNW.setShortcut("Ctrl+Q") self.aExitNW.setMenuRole(QAction.QuitRole) - self.aExitNW.triggered.connect(lambda: self.theParent.closeMain()) + self.aExitNW.triggered.connect(lambda: self.mainGui.closeMain()) self.projMenu.addAction(self.aExitNW) return @@ -200,19 +200,19 @@ class GuiMainMenu(QMenuBar): # Document > Open self.aOpenDoc = QAction(self.tr("Open Document"), self) self.aOpenDoc.setShortcut("Ctrl+O") - self.aOpenDoc.triggered.connect(lambda: self.theParent.openSelectedItem()) + self.aOpenDoc.triggered.connect(lambda: self.mainGui.openSelectedItem()) self.docuMenu.addAction(self.aOpenDoc) # Document > Save self.aSaveDoc = QAction(self.tr("Save Document"), self) self.aSaveDoc.setShortcut("Ctrl+S") - self.aSaveDoc.triggered.connect(lambda: self.theParent.saveDocument()) + self.aSaveDoc.triggered.connect(lambda: self.mainGui.saveDocument()) self.docuMenu.addAction(self.aSaveDoc) # Document > Close self.aCloseDoc = QAction(self.tr("Close Document"), self) self.aCloseDoc.setShortcut("Ctrl+W") - self.aCloseDoc.triggered.connect(lambda: self.theParent.closeDocEditor()) + self.aCloseDoc.triggered.connect(lambda: self.mainGui.closeDocEditor()) self.docuMenu.addAction(self.aCloseDoc) # Document > Separator @@ -221,13 +221,13 @@ class GuiMainMenu(QMenuBar): # Document > Preview self.aViewDoc = QAction(self.tr("View Document"), self) self.aViewDoc.setShortcut("Ctrl+R") - self.aViewDoc.triggered.connect(lambda: self.theParent.viewDocument(None)) + self.aViewDoc.triggered.connect(lambda: self.mainGui.viewDocument(None)) self.docuMenu.addAction(self.aViewDoc) # Document > Close Preview self.aCloseView = QAction(self.tr("Close Document View"), self) self.aCloseView.setShortcut("Ctrl+Shift+R") - self.aCloseView.triggered.connect(lambda: self.theParent.closeDocViewer()) + self.aCloseView.triggered.connect(lambda: self.mainGui.closeDocViewer()) self.docuMenu.addAction(self.aCloseView) # Document > Separator @@ -235,23 +235,23 @@ class GuiMainMenu(QMenuBar): # Document > Show File Details self.aFileDetails = QAction(self.tr("Show File Details"), self) - self.aFileDetails.triggered.connect(lambda: self.theParent.docEditor.revealLocation()) + self.aFileDetails.triggered.connect(lambda: self.mainGui.docEditor.revealLocation()) self.docuMenu.addAction(self.aFileDetails) # Document > Import From File self.aImportFile = QAction(self.tr("Import Text from File"), self) self.aImportFile.setShortcut("Ctrl+Shift+I") - self.aImportFile.triggered.connect(lambda: self.theParent.importDocument()) + self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument()) self.docuMenu.addAction(self.aImportFile) # Document > Merge Documents self.aMergeDocs = QAction(self.tr("Merge Folder to Document"), self) - self.aMergeDocs.triggered.connect(lambda: self.theParent.mergeDocuments()) + self.aMergeDocs.triggered.connect(lambda: self.mainGui.mergeDocuments()) self.docuMenu.addAction(self.aMergeDocs) # Document > Split Document self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self) - self.aSplitDoc.triggered.connect(lambda: self.theParent.splitDocument()) + self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument()) self.docuMenu.addAction(self.aSplitDoc) return @@ -324,7 +324,7 @@ class GuiMainMenu(QMenuBar): self.aFocusTree.setShortcut("Ctrl+Alt+1") else: self.aFocusTree.setShortcut("Alt+1") - self.aFocusTree.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.TREE)) + self.aFocusTree.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.TREE)) self.viewMenu.addAction(self.aFocusTree) # View > Document Pane 1 @@ -333,7 +333,7 @@ class GuiMainMenu(QMenuBar): self.aFocusEditor.setShortcut("Ctrl+Alt+2") else: self.aFocusEditor.setShortcut("Alt+2") - self.aFocusEditor.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.EDITOR)) + self.aFocusEditor.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.EDITOR)) self.viewMenu.addAction(self.aFocusEditor) # View > Document Pane 2 @@ -342,7 +342,7 @@ class GuiMainMenu(QMenuBar): self.aFocusView.setShortcut("Ctrl+Alt+3") else: self.aFocusView.setShortcut("Alt+3") - self.aFocusView.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.VIEWER)) + self.aFocusView.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.VIEWER)) self.viewMenu.addAction(self.aFocusView) # View > Outline @@ -351,7 +351,7 @@ class GuiMainMenu(QMenuBar): self.aFocusOutline.setShortcut("Ctrl+Alt+4") else: self.aFocusOutline.setShortcut("Alt+4") - self.aFocusOutline.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.OUTLINE)) + self.aFocusOutline.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.OUTLINE)) self.viewMenu.addAction(self.aFocusOutline) # View > Separator @@ -360,13 +360,13 @@ class GuiMainMenu(QMenuBar): # View > Go Backward self.aViewPrev = QAction(self.tr("Navigate Backward"), self) self.aViewPrev.setShortcut("Alt+Left") - self.aViewPrev.triggered.connect(lambda: self.theParent.docViewer.navBackward()) + self.aViewPrev.triggered.connect(lambda: self.mainGui.docViewer.navBackward()) self.viewMenu.addAction(self.aViewPrev) # View > Go Forward self.aViewNext = QAction(self.tr("Navigate Forward"), self) self.aViewNext.setShortcut("Alt+Right") - self.aViewNext.triggered.connect(lambda: self.theParent.docViewer.navForward()) + self.aViewNext.triggered.connect(lambda: self.mainGui.docViewer.navForward()) self.viewMenu.addAction(self.aViewNext) # View > Separator @@ -376,14 +376,14 @@ class GuiMainMenu(QMenuBar): self.aFocusMode = QAction(self.tr("Focus Mode"), self) self.aFocusMode.setShortcut("F8") self.aFocusMode.setCheckable(True) - self.aFocusMode.setChecked(self.theParent.isFocusMode) - self.aFocusMode.triggered.connect(lambda: self.theParent.toggleFocusMode()) + self.aFocusMode.setChecked(self.mainGui.isFocusMode) + self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode()) self.viewMenu.addAction(self.aFocusMode) # View > Toggle Full Screen self.aFullScreen = QAction(self.tr("Full Screen Mode"), self) self.aFullScreen.setShortcut("F11") - self.aFullScreen.triggered.connect(lambda: self.theParent.toggleFullScreenMode()) + self.aFullScreen.triggered.connect(lambda: self.mainGui.toggleFullScreenMode()) self.viewMenu.addAction(self.aFullScreen) return @@ -588,7 +588,7 @@ class GuiMainMenu(QMenuBar): # Insert > Placeholder Text self.aLipsumText = QAction(self.tr("Placeholder Text"), self) - self.aLipsumText.triggered.connect(lambda: self.theParent.showLoremIpsumDialog()) + self.aLipsumText.triggered.connect(lambda: self.mainGui.showLoremIpsumDialog()) self.insertMenu.addAction(self.aLipsumText) return @@ -752,7 +752,7 @@ class GuiMainMenu(QMenuBar): # Search > Find self.aFind = QAction(self.tr("Find"), self) self.aFind.setShortcut("Ctrl+F") - self.aFind.triggered.connect(lambda: self.theParent.docEditor.beginSearch()) + self.aFind.triggered.connect(lambda: self.mainGui.docEditor.beginSearch()) self.srcMenu.addAction(self.aFind) # Search > Replace @@ -761,7 +761,7 @@ class GuiMainMenu(QMenuBar): self.aReplace.setShortcut("Ctrl+=") else: self.aReplace.setShortcut("Ctrl+H") - self.aReplace.triggered.connect(lambda: self.theParent.docEditor.beginReplace()) + self.aReplace.triggered.connect(lambda: self.mainGui.docEditor.beginReplace()) self.srcMenu.addAction(self.aReplace) # Search > Find Next @@ -770,7 +770,7 @@ class GuiMainMenu(QMenuBar): self.aFindNext.setShortcuts(["Ctrl+G", "F3"]) else: self.aFindNext.setShortcuts(["F3", "Ctrl+G"]) - self.aFindNext.triggered.connect(lambda: self.theParent.docEditor.findNext()) + self.aFindNext.triggered.connect(lambda: self.mainGui.docEditor.findNext()) self.srcMenu.addAction(self.aFindNext) # Search > Find Prev @@ -779,13 +779,13 @@ class GuiMainMenu(QMenuBar): self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"]) else: self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"]) - self.aFindPrev.triggered.connect(lambda: self.theParent.docEditor.findNext(goBack=True)) + self.aFindPrev.triggered.connect(lambda: self.mainGui.docEditor.findNext(goBack=True)) self.srcMenu.addAction(self.aFindPrev) # Search > Replace Next self.aReplaceNext = QAction(self.tr("Replace Next"), self) self.aReplaceNext.setShortcut("Ctrl+Shift+1") - self.aReplaceNext.triggered.connect(lambda: self.theParent.docEditor.replaceNext()) + self.aReplaceNext.triggered.connect(lambda: self.mainGui.docEditor.replaceNext()) self.srcMenu.addAction(self.aReplaceNext) return @@ -807,12 +807,12 @@ class GuiMainMenu(QMenuBar): # Tools > Re-Run Spell Check self.aReRunSpell = QAction(self.tr("Re-Run Spell Check"), self) self.aReRunSpell.setShortcut("F7") - self.aReRunSpell.triggered.connect(lambda: self.theParent.docEditor.spellCheckDocument()) + self.aReRunSpell.triggered.connect(lambda: self.mainGui.docEditor.spellCheckDocument()) self.toolsMenu.addAction(self.aReRunSpell) # Tools > Project Word List self.aEditWordList = QAction(self.tr("Project Word List"), self) - self.aEditWordList.triggered.connect(lambda: self.theParent.showProjectWordListDialog()) + self.aEditWordList.triggered.connect(lambda: self.mainGui.showProjectWordListDialog()) self.toolsMenu.addAction(self.aEditWordList) # Tools > Separator @@ -821,7 +821,7 @@ class GuiMainMenu(QMenuBar): # Tools > Rebuild Indices self.aRebuildIndex = QAction(self.tr("Rebuild Index"), self) self.aRebuildIndex.setShortcut("F9") - self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex()) + self.aRebuildIndex.triggered.connect(lambda: self.mainGui.rebuildIndex()) self.toolsMenu.addAction(self.aRebuildIndex) # Tools > Separator @@ -835,20 +835,20 @@ class GuiMainMenu(QMenuBar): # Tools > Export Project self.aBuildProject = QAction(self.tr("Build Novel Project"), self) self.aBuildProject.setShortcut("F5") - self.aBuildProject.triggered.connect(lambda: self.theParent.showBuildProjectDialog()) + self.aBuildProject.triggered.connect(lambda: self.mainGui.showBuildProjectDialog()) self.toolsMenu.addAction(self.aBuildProject) # Tools > Writing Stats self.aWritingStats = QAction(self.tr("Writing Statistics"), self) self.aWritingStats.setShortcut("F6") - self.aWritingStats.triggered.connect(lambda: self.theParent.showWritingStatsDialog()) + self.aWritingStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog()) self.toolsMenu.addAction(self.aWritingStats) # Tools > Settings self.aPreferences = QAction(self.tr("Preferences"), self) self.aPreferences.setShortcut("Ctrl+,") self.aPreferences.setMenuRole(QAction.PreferencesRole) - self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog()) + self.aPreferences.triggered.connect(lambda: self.mainGui.showPreferencesDialog()) self.toolsMenu.addAction(self.aPreferences) return @@ -862,13 +862,13 @@ class GuiMainMenu(QMenuBar): # Help > About self.aAboutNW = QAction(self.tr("About novelWriter"), self) self.aAboutNW.setMenuRole(QAction.AboutRole) - self.aAboutNW.triggered.connect(lambda: self.theParent.showAboutNWDialog()) + self.aAboutNW.triggered.connect(lambda: self.mainGui.showAboutNWDialog()) self.helpMenu.addAction(self.aAboutNW) # Help > About Qt5 self.aAboutQt = QAction(self.tr("About Qt5"), self) self.aAboutQt.setMenuRole(QAction.AboutQtRole) - self.aAboutQt.triggered.connect(lambda: self.theParent.showAboutQtDialog()) + self.aAboutQt.triggered.connect(lambda: self.mainGui.showAboutQtDialog()) self.helpMenu.addAction(self.aAboutQt) # Help > Separator @@ -915,7 +915,7 @@ class GuiMainMenu(QMenuBar): # Document > Check for Updates self.aUpdates = QAction(self.tr("Check for New Release"), self) - self.aUpdates.triggered.connect(lambda: self.theParent.showUpdatesDialog()) + self.aUpdates.triggered.connect(lambda: self.mainGui.showUpdatesDialog()) self.helpMenu.addAction(self.aUpdates) return diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 9ac2ea2b..af79fad5 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -45,15 +45,15 @@ class GuiNovelTree(QTreeWidget): C_WORDS = 1 C_POV = 2 - def __init__(self, theParent): - QTreeWidget.__init__(self, theParent) + def __init__(self, mainGui): + QTreeWidget.__init__(self, mainGui) logger.debug("Initialising GuiNovelTree ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theTheme = mainGui.theTheme + self.theProject = mainGui.theProject # Internal Variables self._treeMap = {} @@ -135,7 +135,7 @@ class GuiNovelTree(QTreeWidget): """Called whenever the Novel tab is activated. """ logger.verbose("Requesting refresh of the novel tree") - treeChanged = self.theParent.treeView.changedSince(self._lastBuild) + treeChanged = self.mainGui.treeView.changedSince(self._lastBuild) indexChanged = self.theProject.index.indexChangedSince(self._lastBuild) if not (treeChanged or indexChanged or overRide): logger.verbose("No changes have been made to the novel index") @@ -209,7 +209,7 @@ class GuiNovelTree(QTreeWidget): if tHandle is None: return - self.theParent.viewDocument(tHandle) + self.mainGui.viewDocument(tHandle) return @@ -223,7 +223,7 @@ class GuiNovelTree(QTreeWidget): document editor. """ tHandle, tLine = self.getSelectedHandle() - self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True) + self.mainGui.openDocument(tHandle, tLine=tLine-1, doScroll=True) return def _itemSelected(self): @@ -233,7 +233,7 @@ class GuiNovelTree(QTreeWidget): selItems = self.selectedItems() if selItems: tHandle = selItems[0].data(self.C_TITLE, Qt.UserRole)[0] - self.theParent.treeMeta.updateViewBox(tHandle) + self.mainGui.treeMeta.updateViewBox(tHandle) return diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 65bffdbd..d2b2eb04 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -56,11 +56,11 @@ class GuiOutline(QWidget): loadDocumentTagRequest = pyqtSignal(str, Enum) - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui # Build GUI self.outlineBar = GuiOutlineToolBar(self) @@ -177,9 +177,9 @@ class GuiOutlineToolBar(QToolBar): logger.debug("Initialising GuiOutlineToolBar ...") self.mainConf = novelwriter.CONFIG - self.theParent = theOutline.theParent - self.theProject = theOutline.theParent.theProject - self.theTheme = theOutline.theParent.theTheme + self.mainGui = theOutline.mainGui + self.theProject = theOutline.mainGui.theProject + self.theTheme = theOutline.mainGui.theTheme iPx = self.mainConf.pxInt(22) mPx = self.mainConf.pxInt(12) @@ -322,9 +322,9 @@ class GuiOutlineView(QTreeWidget): logger.debug("Initialising GuiOutlineView ...") self.mainConf = novelwriter.CONFIG - self.theParent = theOutline.theParent - self.theProject = theOutline.theParent.theProject - self.theTheme = theOutline.theParent.theTheme + self.mainGui = theOutline.mainGui + self.theProject = theOutline.mainGui.theProject + self.theTheme = theOutline.mainGui.theTheme self.setFrameStyle(QFrame.NoFrame) self.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -464,7 +464,7 @@ class GuiOutlineView(QTreeWidget): document editor. """ tHandle, tLine = self.getSelectedHandle() - self.theParent.openDocument(tHandle, tLine=tLine - 1, doScroll=True) + self.mainGui.openDocument(tHandle, tLine=tLine - 1, doScroll=True) return @pyqtSlot() @@ -782,9 +782,9 @@ class GuiOutlineDetails(QScrollArea): self.mainConf = novelwriter.CONFIG self.theOutline = theOutline - self.theParent = theOutline.theParent - self.theProject = theOutline.theParent.theProject - self.theTheme = theOutline.theParent.theTheme + self.mainGui = theOutline.mainGui + self.theProject = theOutline.mainGui.theProject + self.theTheme = theOutline.mainGui.theTheme # Sizes minTitle = 30*self.theTheme.textNWidth diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 372a2d84..f2a7b2b3 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -63,10 +63,10 @@ class GuiProjectView(QWidget): selectedItemChanged = pyqtSignal(str) openDocumentRequest = pyqtSignal(str, Enum) - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.theParent = theParent + self.mainGui = mainGui # Build GUI self.projTree = GuiProjectTree(self) @@ -167,9 +167,9 @@ class GuiProjectToolBar(QWidget): self.mainConf = novelwriter.CONFIG self.projView = projView self.projTree = projView.projTree - self.theParent = projView.theParent - self.theProject = projView.theParent.theProject - self.theTheme = projView.theParent.theTheme + self.mainGui = projView.mainGui + self.theProject = projView.mainGui.theProject + self.theTheme = projView.mainGui.theTheme iPx = self.theTheme.baseIconSize mPx = self.mainConf.pxInt(4) @@ -323,9 +323,9 @@ class GuiProjectTree(QTreeWidget): self.mainConf = novelwriter.CONFIG self.projView = projView - self.theParent = projView.theParent - self.theTheme = projView.theParent.theTheme - self.theProject = projView.theParent.theProject + self.mainGui = projView.mainGui + self.theTheme = projView.mainGui.theTheme + self.theProject = projView.mainGui.theProject # Internal Variables self._treeMap = {} @@ -420,7 +420,7 @@ class GuiProjectTree(QTreeWidget): make sure the item is added in a place it can be added, and that other meta data is set correctly to ensure a valid project tree. """ - if not self.theParent.hasProject: + if not self.mainGui.hasProject: logger.error("No project open") return False @@ -435,7 +435,7 @@ class GuiProjectTree(QTreeWidget): sHandle = self.getSelectedHandle() if sHandle is None or sHandle not in self.theProject.tree: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Did not find anywhere to add the file or folder!" ), nwAlert.ERROR) return False @@ -452,7 +452,7 @@ class GuiProjectTree(QTreeWidget): return False if self.theProject.tree.isTrash(sHandle): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot add new files or folders to the Trash folder." ), nwAlert.ERROR) return False @@ -611,7 +611,7 @@ class GuiProjectTree(QTreeWidget): function only asks for confirmation once, and calls the regular deleteItem function for each document in the Trash folder. """ - if not self.theParent.hasProject: + if not self.mainGui.hasProject: logger.error("No project open") return False @@ -619,7 +619,7 @@ class GuiProjectTree(QTreeWidget): logger.debug("Emptying Trash folder") if trashHandle is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "There is currently no Trash folder in this project." ), nwAlert.INFO) return False @@ -630,12 +630,12 @@ class GuiProjectTree(QTreeWidget): nTrash = len(theTrash) if nTrash == 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The Trash folder is already empty." ), nwAlert.INFO) return False - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Empty Trash"), self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash) ) @@ -660,7 +660,7 @@ class GuiProjectTree(QTreeWidget): delete the files on disk. Root folders are deleted if they're empty only, and the deletion is always permanent. """ - if not self.theParent.hasProject: + if not self.mainGui.hasProject: logger.error("No project open") return False @@ -693,7 +693,7 @@ class GuiProjectTree(QTreeWidget): self._deleteTreeItem(tHandle) self._alertTreeChange(tHandle=tHandle, flush=True) else: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot delete root folder. It is not empty. " "Recursive deletion is not supported. " "Please delete the content first." @@ -723,7 +723,7 @@ class GuiProjectTree(QTreeWidget): # user if they want to permanently delete the file. doPermanent = False if not alreadyAsked: - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Delete"), self.tr("Permanently delete '{0}'?").format(nwItemS.itemName) ) @@ -739,8 +739,8 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) for dHandle in reversed(self.getTreeFromHandle(tHandle)): - if self.theParent.docEditor.docHandle() == dHandle: - self.theParent.closeDocument() + if self.mainGui.docEditor.docHandle() == dHandle: + self.mainGui.closeDocument() self._deleteTreeItem(dHandle) self._alertTreeChange(tHandle=tHandle, flush=autoFlush) @@ -749,7 +749,7 @@ class GuiProjectTree(QTreeWidget): else: # The item is not already in the trash folder, so we # move it there. - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Delete"), self.tr("Move '{0}' to Trash?").format(nwItemS.itemName), ) @@ -1199,7 +1199,7 @@ class GuiProjectTree(QTreeWidget): if self.theProject.tree.checkType(tHandle, nwItemType.FILE): delDoc = NWDoc(self.theProject, tHandle) if not delDoc.deleteDocument(): - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Could not delete document file."), delDoc.getError() ], nwAlert.ERROR) return False @@ -1295,7 +1295,7 @@ class GuiProjectTree(QTreeWidget): newItem.setFlags(newItem.flags() ^ Qt.ItemIsDragEnabled) self.addTopLevelItem(newItem) else: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "There is nowhere to add item with name '{0}'." ).format(nwItem.itemName), nwAlert.ERROR) del self._treeMap[tHandle] diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index e3891e7e..4863f9bc 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -41,14 +41,14 @@ logger = logging.getLogger(__name__) class GuiMainStatus(QStatusBar): - def __init__(self, theParent): - QStatusBar.__init__(self, theParent) + def __init__(self, mainGui): + QStatusBar.__init__(self, mainGui) logger.debug("Initialising GuiMainStatus ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theTheme = mainGui.theTheme self.refTime = None self.userIdle = False diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py index e5b627b2..d62b5543 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/viewsbar.py @@ -40,14 +40,14 @@ class GuiViewsBar(QToolBar): viewChangeRequested = pyqtSignal(nwView) - def __init__(self, theParent): - QToolBar.__init__(self, theParent) + def __init__(self, mainGui): + QToolBar.__init__(self, mainGui) logger.debug("Initialising GuiViewsBar ...") - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme # Style iPx = self.mainConf.pxInt(22) @@ -89,29 +89,29 @@ class GuiViewsBar(QToolBar): self.aBuild.setFont(lblFont) self.aBuild.setToolTip(self.tr("Build novel project")) self.aBuild.setIcon(self.theTheme.getIcon("view_build")) - self.aBuild.triggered.connect(lambda: self.theParent.showBuildProjectDialog()) + self.aBuild.triggered.connect(lambda: self.mainGui.showBuildProjectDialog()) self.aDetails = QAction(self.tr("Details")) self.aDetails.setFont(lblFont) self.aDetails.setToolTip(self.tr("Show project details")) self.aDetails.setIcon(self.theTheme.getIcon("proj_details")) - self.aDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog()) + self.aDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog()) self.aStats = QAction(self.tr("Stats")) self.aStats.setFont(lblFont) self.aStats.setToolTip(self.tr("Show project statistics")) self.aStats.setIcon(self.theTheme.getIcon("proj_stats")) - self.aStats.triggered.connect(lambda: self.theParent.showWritingStatsDialog()) + self.aStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog()) # Settings Menu self.mSettings = QMenu() self.aPrjSettings = QAction(self.tr("Project Settings")) - self.aPrjSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog()) + self.aPrjSettings.triggered.connect(lambda: self.mainGui.showProjectSettingsDialog()) self.mSettings.addAction(self.aPrjSettings) self.aPreferences = QAction(self.tr("Preferences")) - self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog()) + self.aPreferences.triggered.connect(lambda: self.mainGui.showPreferencesDialog()) self.mSettings.addAction(self.aPreferences) self.tbSettings = QToolButton(self) diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index b27a98a4..4ce9dea1 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -65,16 +65,16 @@ class GuiBuildNovel(QDialog): FMT_JSON_H = 8 # HTML5 wrapped in JSON FMT_JSON_M = 9 # nW Markdown wrapped in JSON - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiBuildNovel ...") self.setObjectName("GuiBuildNovel") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theTheme = mainGui.theTheme + self.theProject = mainGui.theProject self.htmlText = [] # List of html documents self.htmlStyle = [] # List of html styles @@ -711,7 +711,7 @@ class GuiBuildNovel(QDialog): bldObj.initDocument() # Make sure the project and document is up to date - self.theParent.saveDocument() + self.mainGui.saveDocument() self.buildProgress.setMaximum(len(self.theProject.tree)) self.buildProgress.setValue(0) @@ -760,7 +760,7 @@ class GuiBuildNovel(QDialog): logger.debug("Built project in %.3f ms", 1000*(tEnd - tStart)) if bldObj.errData: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("There were problems when building the project:") ] + bldObj.errData, nwAlert.ERROR) @@ -1003,11 +1003,11 @@ class GuiBuildNovel(QDialog): # ============== if wSuccess: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("{0} file successfully written to:").format(textFmt), savePath ], nwAlert.INFO) else: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to write {0} file. {1}" ).format(textFmt, errMsg), nwAlert.ERROR) @@ -1193,18 +1193,18 @@ class GuiBuildNovel(QDialog): class GuiBuildNovelDocView(QTextBrowser): - def __init__(self, theParent, theProject): - QTextBrowser.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QTextBrowser.__init__(self, mainGui) logger.debug("Initialising GuiBuildNovelDocView ...") self.mainConf = novelwriter.CONFIG self.theProject = theProject - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theTheme = mainGui.theTheme self.buildTime = 0 - self.setMinimumWidth(40*self.theParent.theTheme.textNWidth) + self.setMinimumWidth(40*self.mainGui.theTheme.textNWidth) self.setOpenExternalLinks(False) self.document().setDocumentMargin(self.mainConf.getTextMargin()) diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index 4ce21643..d741058c 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -42,15 +42,15 @@ logger = logging.getLogger(__name__) class GuiLipsum(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiLipsum ...") self.setObjectName("GuiLipsum") - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme self.setWindowTitle(self.tr("Insert Placeholder Text")) @@ -61,7 +61,7 @@ class GuiLipsum(QDialog): nPx = self.mainConf.pxInt(64) vSp = self.mainConf.pxInt(4) self.docIcon = QLabel() - self.docIcon.setPixmap(self.theParent.theTheme.getPixmap("proj_document", (nPx, nPx))) + self.docIcon.setPixmap(self.mainGui.theTheme.getPixmap("proj_document", (nPx, nPx))) self.leftBox = QVBoxLayout() self.leftBox.setSpacing(vSp) @@ -129,7 +129,7 @@ class GuiLipsum(QDialog): pCount = self.paraCount.value() inText = "\n\n".join(lipsumText[0:pCount]) + "\n\n" - self.theParent.docEditor.insertText(inText) + self.mainGui.docEditor.insertText(inText) return diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index 765445b9..3dac9ed8 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -48,15 +48,15 @@ PAGE_FINAL = 4 class GuiProjectWizard(QWizard): - def __init__(self, theParent): - QWizard.__init__(self, theParent) + def __init__(self, mainGui): + QWizard.__init__(self, mainGui) logger.debug("Initialising GuiProjectWizard ...") self.setObjectName("GuiProjectWizard") - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theTheme = mainGui.theTheme self.sideImage = self.theTheme.loadDecoration( "wiz-back", None, self.mainConf.pxInt(370) diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index aff7bc3d..d4527612 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -57,16 +57,16 @@ class GuiWritingStats(QDialog): FMT_JSON = 0 FMT_CSV = 1 - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiWritingStats ...") self.setObjectName("GuiWritingStats") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theTheme = mainGui.theTheme + self.theProject = mainGui.theProject self.logData = [] self.filterData = [] @@ -411,11 +411,11 @@ class GuiWritingStats(QDialog): # Report to user if wSuccess: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("{0} file successfully written to:").format(textFmt), savePath ], nwAlert.INFO) else: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Failed to write {0} file.").format(textFmt), errMsg ], nwAlert.ERROR) @@ -478,7 +478,7 @@ class GuiWritingStats(QDialog): self.logData.append((dStart, sDiff, wcNovel, wcNotes, sIdle)) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to read session log file." ), nwAlert.ERROR, exception=exc) return False From 94068e777aebc47fbae41a20b979b30350bf2de7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 15:49:45 +0200 Subject: [PATCH 135/179] Give main GUI elements more identifiable names --- novelwriter/dialogs/docmerge.py | 6 +- novelwriter/dialogs/docsplit.py | 6 +- novelwriter/dialogs/preferences.py | 2 +- novelwriter/gui/doceditor.py | 6 +- novelwriter/gui/docviewer.py | 2 +- novelwriter/gui/mainmenu.py | 4 +- novelwriter/gui/noveltree.py | 4 +- novelwriter/guimain.py | 118 +++++++++++----------- tests/test_dialogs/test_dlg_docmerge.py | 28 ++--- tests/test_dialogs/test_dlg_docsplit.py | 20 ++-- tests/test_dialogs/test_dlg_itemeditor.py | 8 +- tests/test_gui/test_gui_doceditor.py | 2 +- tests/test_gui/test_gui_docviewer.py | 10 +- tests/test_gui/test_gui_guimain.py | 58 +++++------ tests/test_gui/test_gui_mainmenu.py | 2 +- tests/test_gui/test_gui_outline.py | 16 +-- tests/test_gui/test_gui_projtree.py | 8 +- tests/test_gui/test_gui_statusbar.py | 2 +- 18 files changed, 151 insertions(+), 151 deletions(-) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 23657fb5..96a357f4 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -142,7 +142,7 @@ class GuiDocMerge(QDialog): ], nwAlert.ERROR) return False - self.mainGui.treeView.revealNewTreeItem(nHandle) + self.mainGui.projView.revealNewTreeItem(nHandle) self.mainGui.openDocument(nHandle, doScroll=True) self._doClose() @@ -165,7 +165,7 @@ class GuiDocMerge(QDialog): are then added to the list view in order. The list itself can be reordered by the user. """ - tHandle = self.mainGui.treeView.getSelectedHandle() + tHandle = self.mainGui.projView.getSelectedHandle() self.sourceItem = tHandle if tHandle is None: return False @@ -180,7 +180,7 @@ class GuiDocMerge(QDialog): ), nwAlert.ERROR) return False - for sHandle in self.mainGui.treeView.getTreeFromHandle(tHandle): + for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle): newItem = QListWidgetItem() nwItem = self.theProject.tree[sHandle] if nwItem.itemType is not nwItemType.FILE: diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 4d2ecc4a..c2d51c8e 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -175,7 +175,7 @@ class GuiDocSplit(QDialog): # Create the folder fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent) - self.mainGui.treeView.revealNewTreeItem(fHandle) + self.mainGui.projView.revealNewTreeItem(fHandle) logger.verbose("Creating folder '%s'", fHandle) # Loop through, and create the files @@ -201,7 +201,7 @@ class GuiDocSplit(QDialog): ], nwAlert.ERROR) return False - self.mainGui.treeView.revealNewTreeItem(nHandle) + self.mainGui.projView.revealNewTreeItem(nHandle) self._doClose() @@ -226,7 +226,7 @@ class GuiDocSplit(QDialog): """ self.listBox.clear() if self.sourceItem is None: - self.sourceItem = self.mainGui.treeView.getSelectedHandle() + self.sourceItem = self.mainGui.projView.getSelectedHandle() if self.sourceItem is None: return False diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 4b74655f..e0fd41e8 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -107,7 +107,7 @@ class GuiPreferences(PagedDialog): ), nwAlert.INFO) if refreshTree: - self.mainGui.treeView.populateTree() + self.mainGui.projView.populateTree() self._saveWindowSize() self.accept() diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index e1b1d948..a060f151 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -517,8 +517,8 @@ class GuiDocEditor(QTextEdit): self.mainGui.novelView.updateWordCounts(tHandle) if oldHeader != newHeader: - self.mainGui.treeView.setTreeItemValues(tHandle) - self.mainGui.treeMeta.updateViewBox(tHandle) + self.mainGui.projView.setTreeItemValues(tHandle) + self.mainGui.itemDetails.updateViewBox(tHandle) self.docFooter.updateInfo() # Update the status bar @@ -2779,7 +2779,7 @@ class GuiDocEditHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.mainGui.treeView.setSelectedHandle(self._docHandle, doScroll=True) + self.mainGui.projView.setSelectedHandle(self._docHandle, doScroll=True) return # END Class GuiDocEditHeader diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 5a9c61cd..f77e02ec 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -901,7 +901,7 @@ class GuiDocViewHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.mainGui.treeView.setSelectedHandle(self._docHandle, doScroll=True) + self.mainGui.projView.setSelectedHandle(self._docHandle, doScroll=True) return # END Class GuiDocViewHeader diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index f482544f..e7203bb9 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -171,12 +171,12 @@ class GuiMainMenu(QMenuBar): # Project > Delete self.aDeleteItem = QAction(self.tr("Delete Item"), self) self.aDeleteItem.setShortcut("Ctrl+Shift+Del") - self.aDeleteItem.triggered.connect(lambda: self.mainGui.treeView.deleteItem(None)) + self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) # Project > Empty Trash self.aEmptyTrash = QAction(self.tr("Empty Trash"), self) - self.aEmptyTrash.triggered.connect(lambda: self.mainGui.treeView.emptyTrash()) + self.aEmptyTrash.triggered.connect(lambda: self.mainGui.projView.emptyTrash()) self.projMenu.addAction(self.aEmptyTrash) # Project > Separator diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index af79fad5..dbdd02c0 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -135,7 +135,7 @@ class GuiNovelTree(QTreeWidget): """Called whenever the Novel tab is activated. """ logger.verbose("Requesting refresh of the novel tree") - treeChanged = self.mainGui.treeView.changedSince(self._lastBuild) + treeChanged = self.mainGui.projView.changedSince(self._lastBuild) indexChanged = self.theProject.index.indexChangedSince(self._lastBuild) if not (treeChanged or indexChanged or overRide): logger.verbose("No changes have been made to the novel index") @@ -233,7 +233,7 @@ class GuiNovelTree(QTreeWidget): selItems = self.selectedItems() if selItems: tHandle = selItems[0].data(self.C_TITLE, Qt.UserRole)[0] - self.mainGui.treeMeta.updateViewBox(tHandle) + self.mainGui.itemDetails.updateViewBox(tHandle) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 2716213e..24c2180d 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -104,20 +104,20 @@ class GuiMain(QMainWindow): hWd = self.mainConf.pxInt(4) # Main GUI Elements - self.statusBar = GuiMainStatus(self) - self.treeView = GuiProjectView(self) - self.novelView = GuiNovelTree(self) - self.docEditor = GuiDocEditor(self) - self.viewMeta = GuiDocViewDetails(self) - self.docViewer = GuiDocViewer(self) - self.treeMeta = GuiItemDetails(self) - self.projView = GuiOutline(self) - self.mainMenu = GuiMainMenu(self) - self.viewsBar = GuiViewsBar(self) + self.statusBar = GuiMainStatus(self) + self.projView = GuiProjectView(self) + self.novelView = GuiNovelTree(self) + self.docEditor = GuiDocEditor(self) + self.viewMeta = GuiDocViewDetails(self) + self.docViewer = GuiDocViewer(self) + self.itemDetails = GuiItemDetails(self) + self.outlineView = GuiOutline(self) + self.mainMenu = GuiMainMenu(self) + self.viewsBar = GuiViewsBar(self) # Project Tree Stack self.projStack = QStackedWidget() - self.projStack.addWidget(self.treeView) + self.projStack.addWidget(self.projView) self.projStack.addWidget(self.novelView) self.projStack.currentChanged.connect(self._projStackChanged) @@ -127,7 +127,7 @@ class GuiMain(QMainWindow): self.treeBox.setContentsMargins(0, 0, 0, 0) self.treeBox.setSpacing(mPx) self.treeBox.addWidget(self.projStack) - self.treeBox.addWidget(self.treeMeta) + self.treeBox.addWidget(self.itemDetails) self.treePane.setLayout(self.treeBox) # Splitter : Document Viewer / Document Meta @@ -154,7 +154,7 @@ class GuiMain(QMainWindow): # Main Stack : Editor / Outline self.mainStack = QStackedWidget() self.mainStack.addWidget(self.splitMain) - self.mainStack.addWidget(self.projView) + self.mainStack.addWidget(self.outlineView) self.mainStack.currentChanged.connect(self._mainStackChanged) # Indices of Splitter Widgets @@ -167,8 +167,8 @@ class GuiMain(QMainWindow): # Indices of Tab Widgets self.idxEditorView = self.mainStack.indexOf(self.splitMain) - self.idxOutlineView = self.mainStack.indexOf(self.projView) - self.idxTreeView = self.projStack.indexOf(self.treeView) + self.idxOutlineView = self.mainStack.indexOf(self.outlineView) + self.idxProjView = self.projStack.indexOf(self.projView) self.idxNovelView = self.projStack.indexOf(self.novelView) # Splitter Behaviour @@ -197,24 +197,24 @@ class GuiMain(QMainWindow): self.viewsBar.viewChangeRequested.connect(self._changeView) - self.treeView.selectedItemChanged.connect(self.treeMeta.updateViewBox) - self.treeView.openDocumentRequest.connect(self._openDocument) - self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) - self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - self.treeView.treeItemChanged.connect(self.docEditor.updateDocInfo) - self.treeView.treeItemChanged.connect(self.docViewer.updateDocInfo) - self.treeView.treeItemChanged.connect(self.treeMeta.updateViewBox) - self.treeView.rootFolderChanged.connect(self.projView.updateRootItem) + self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox) + self.projView.openDocumentRequest.connect(self._openDocument) + self.projView.novelItemChanged.connect(self._treeNovelItemChanged) + self.projView.wordCountsChanged.connect(self._updateStatusWordCount) + self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo) + self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo) + self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox) + self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem) self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) - self.docEditor.docCountsChanged.connect(self.treeMeta.updateCounts) - self.docEditor.docCountsChanged.connect(self.treeView.updateCounts) + self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) + self.docEditor.docCountsChanged.connect(self.projView.updateCounts) self.docEditor.loadDocumentTagRequest.connect(self._followTag) self.docViewer.loadDocumentTagRequest.connect(self._followTag) - self.projView.loadDocumentTagRequest.connect(self._followTag) + self.outlineView.loadDocumentTagRequest.connect(self._followTag) # Finalise Initialisation # ======================= @@ -291,15 +291,15 @@ class GuiMain(QMainWindow): """Wrapper function to clear all sub-elements of the main GUI. """ # Project Area - self.treeView.clearProject() + self.projView.clearProject() self.novelView.clearTree() - self.treeMeta.clearDetails() + self.itemDetails.clearDetails() # Work Area self.docEditor.clearEditor() self.docEditor.setDictionaries() self.closeDocViewer() - self.projView.clearOutline() + self.outlineView.clearOutline() # General self.statusBar.clearStatus() @@ -361,7 +361,7 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.saveProject() self.docEditor.setDictionaries() - self.projView.updateRootItem(None) + self.outlineView.updateRootItem(None) self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -417,7 +417,7 @@ class GuiMain(QMainWindow): if saveOK: self.closeDocument() self.docViewer.clearNavHistory() - self.projView.closeOutline() + self.outlineView.closeOutline() self.theProject.closeProject(self.idleTime) self.idleRefTime = time() @@ -508,7 +508,7 @@ class GuiMain(QMainWindow): self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.statusBar.setRefTime(self.theProject.projOpened) - self.projView.updateRootItem(None) + self.outlineView.updateRootItem(None) self._updateStatusWordCount() # Restore previously open documents, if any @@ -541,7 +541,7 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - self.treeView.saveProjectTree() + self.projView.saveProjectTree() if self.theProject.saveProject(autoSave=autoSave): self.theProject.index.saveIndex() @@ -586,7 +586,7 @@ class GuiMain(QMainWindow): if changeFocus: self.docEditor.setFocus() self.theProject.setLastEdited(tHandle) - self.treeView.setSelectedHandle(tHandle, doScroll=doScroll) + self.projView.setSelectedHandle(tHandle, doScroll=doScroll) else: return False @@ -652,7 +652,7 @@ class GuiMain(QMainWindow): self.saveDocument() else: logger.verbose("Trying selected document") - tHandle = self.treeView.getSelectedHandle() + tHandle = self.projView.getSelectedHandle() if tHandle is None: logger.verbose("Trying last viewed document") @@ -789,12 +789,12 @@ class GuiMain(QMainWindow): tHandle = None tLine = None - if self.treeView.treeFocus(): - tHandle = self.treeView.getSelectedHandle() + if self.projView.treeFocus(): + tHandle = self.projView.getSelectedHandle() elif self.novelView.hasFocus(): tHandle, tLine = self.novelView.getSelectedHandle() - elif self.projView.treeFocus(): - tHandle, tLine = self.projView.getSelectedHandle() + elif self.outlineView.treeFocus(): + tHandle, tLine = self.outlineView.getSelectedHandle() else: logger.warning("No item selected") return False @@ -815,16 +815,16 @@ class GuiMain(QMainWindow): if self.docEditor.anyFocus() or self.isFocusMode: tHandle = self.docEditor.docHandle() else: - tHandle = self.treeView.getSelectedHandle() + tHandle = self.projView.getSelectedHandle() if tHandle: - return self.treeView.editTreeItem(tHandle) + return self.projView.editTreeItem(tHandle) return False def rebuildTrees(self): """Rebuild the project tree. """ - self.treeView.populateTree() + self.projView.populateTree() self.novelView.refreshTree() return @@ -847,7 +847,7 @@ class GuiMain(QMainWindow): qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) tStart = time() - self.treeView.saveProjectTree() + self.projView.saveProjectTree() self.theProject.index.clearIndex() for tItem in self.theProject.tree: @@ -857,8 +857,8 @@ class GuiMain(QMainWindow): logger.verbose("Indexing '%s'", tItem.itemName) if self.theProject.index.reIndexHandle(tItem.itemHandle): # Update Word Counts - self.treeView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True) - self.treeView.setTreeItemValues(tItem.itemHandle) + self.projView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True) + self.projView.setTreeItemValues(tItem.itemHandle) tEnd = time() self.setStatus( @@ -919,9 +919,9 @@ class GuiMain(QMainWindow): self.saveDocument() self.docEditor.initEditor() self.docViewer.initViewer() - self.treeView.initSettings() + self.projView.initSettings() self.novelView.initTree() - self.projView.initOutline() + self.outlineView.initOutline() self._updateStatusWordCount() return @@ -940,7 +940,7 @@ class GuiMain(QMainWindow): logger.debug("Applying new project settings") if dlgProj.spellChanged: self.docEditor.setDictionaries() - self.treeMeta.refreshDetails() + self.itemDetails.refreshDetails() self._updateWindowTitle(self.theProject.projName) return True @@ -1156,7 +1156,7 @@ class GuiMain(QMainWindow): if not self.isFocusMode: self.mainConf.setMainPanePos(self.splitMain.sizes()) self.mainConf.setDocPanePos(self.splitDocs.sizes()) - self.mainConf.setOutlinePanePos(self.projView.splitSizes()) + self.mainConf.setOutlinePanePos(self.outlineView.splitSizes()) if self.viewMeta.isVisible(): self.mainConf.setViewPanePos(self.splitView.sizes()) @@ -1180,8 +1180,8 @@ class GuiMain(QMainWindow): """ if paneNo == nwWidget.TREE: tabIdx = self.projStack.currentIndex() - if tabIdx == self.idxTreeView: - self.treeView.setFocus() + if tabIdx == self.idxProjView: + self.projView.setFocus() elif tabIdx == self.idxNovelView: self.novelView.setFocus() elif paneNo == nwWidget.EDITOR: @@ -1192,7 +1192,7 @@ class GuiMain(QMainWindow): self.docViewer.setFocus() elif paneNo == nwWidget.OUTLINE: self._changeView(nwView.OUTLINE) - self.projView.setTreeFocus() + self.outlineView.setTreeFocus() return def closeDocEditor(self): @@ -1490,14 +1490,14 @@ class GuiMain(QMainWindow): elif view == nwView.PROJECT: self.mainStack.setCurrentWidget(self.splitMain) - self.projStack.setCurrentWidget(self.treeView) + self.projStack.setCurrentWidget(self.projView) elif view == nwView.NOVEL: self.mainStack.setCurrentWidget(self.splitMain) self.projStack.setCurrentWidget(self.novelView) elif view == nwView.OUTLINE: - self.mainStack.setCurrentWidget(self.projView) + self.mainStack.setCurrentWidget(self.outlineView) return @@ -1551,7 +1551,7 @@ class GuiMain(QMainWindow): if self.mainStack.currentIndex() == self.idxOutlineView: logger.verbose("Novel tree changed while Outline tab active") if self.hasProject: - self.projView.refreshView(novelChanged=True) + self.outlineView.refreshView(novelChanged=True) return @@ -1584,7 +1584,7 @@ class GuiMain(QMainWindow): elif tabIndex == self.idxOutlineView: logger.verbose("Project outline tab activated") if self.hasProject: - self.projView.refreshView() + self.outlineView.refreshView() return @@ -1594,9 +1594,9 @@ class GuiMain(QMainWindow): """ sHandle = None - if tabIndex == self.idxTreeView: + if tabIndex == self.idxProjView: logger.verbose("Project tree tab activated") - sHandle = self.treeView.getSelectedHandle() + sHandle = self.projView.getSelectedHandle() elif tabIndex == self.idxNovelView: logger.verbose("Novel tree tab activated") @@ -1604,7 +1604,7 @@ class GuiMain(QMainWindow): self.novelView.refreshTree() sHandle, _ = self.novelView.getSelectedHandle() - self.treeMeta.updateViewBox(sHandle) + self.itemDetails.updateViewBox(sHandle) return diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index f796f864..9d9c9082 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -57,11 +57,11 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Add Project Content monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -83,8 +83,8 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Open the Merge tool nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None) nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger) @@ -102,27 +102,27 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwMerge.listBox.count() == 0 # No item selected - nwGUI.treeView.projTree.clearSelection() + nwGUI.projView.projTree.clearSelection() assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Non-existing item with monkeypatch.context() as mp: mp.setattr(NWTree, "__getitem__", lambda *a: None) - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Select a non-folder - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem(hChapterOne).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterOne).setSelected(True) assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Select the chapter folder - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwMerge._populateList() is True assert nwMerge.listBox.count() == 5 diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 080be8ab..5b042570 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -61,9 +61,9 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Add Project Content monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem(hNovelRoot).setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hNovelRoot).setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -90,8 +90,8 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Open the Split tool nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem(hToSplit).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True) monkeypatch.setattr(GuiDocSplit, "exec_", lambda *a: None) nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger) @@ -110,7 +110,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # No item selected nwSplit.sourceItem = None - nwGUI.treeView.projTree.clearSelection() + nwGUI.projView.projTree.clearSelection() assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 @@ -118,15 +118,15 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): with monkeypatch.context() as mp: mp.setattr(NWTree, "__getitem__", lambda *a: None) nwSplit.sourceItem = None - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem(hToSplit).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True) assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 # Select a non-file nwSplit.sourceItem = None - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 53426df7..360f7641 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -52,7 +52,7 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): tHandle = "000000000000f" # No Selection - nwGUI.treeView.projTree.clearSelection() + nwGUI.projView.projTree.clearSelection() assert nwGUI.editItem() is False # Force opening from editor @@ -164,9 +164,9 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.theProject.importItems.name(importKeys[1]) == "Minor" # Create Note - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) # Open Note assert nwGUI.openDocument("0000000000010") diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 1727b69f..d4a3d749 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -1147,7 +1147,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): assert nwGUI.openDocument(cHandle) is True assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.saveDocument() is True - assert nwGUI.treeView.revealNewTreeItem(cHandle) + assert nwGUI.projView.revealNewTreeItem(cHandle) nwGUI.docEditor.updateTagHighLighting() # Follow Tag diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index d1d8797a..2226a4c3 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -51,12 +51,12 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): assert nwGUI.theProject.index._itemIndex._items != {} # Select a document in the project tree - nwGUI.treeView.setSelectedHandle("88243afbe5ed8") + nwGUI.projView.setSelectedHandle("88243afbe5ed8") # Middle-click the selected item - theItem = nwGUI.treeView.projTree._getTreeItem("88243afbe5ed8") - theRect = nwGUI.treeView.projTree.visualItemRect(theItem) - qtbot.mouseClick(nwGUI.treeView.projTree.viewport(), Qt.MidButton, pos=theRect.center()) + theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8") + theRect = nwGUI.projView.projTree.visualItemRect(theItem) + qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center()) assert nwGUI.docViewer.docHandle() == "88243afbe5ed8" # Reload the text @@ -117,7 +117,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False # Open again via menu - assert nwGUI.treeView.setSelectedHandle("88243afbe5ed8") + assert nwGUI.projView.setSelectedHandle("88243afbe5ed8") nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger) # Select "Bod" link diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index ab3a1141..02c97617 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -125,7 +125,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): with monkeypatch.context() as mp: mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - nwGUI.treeView.projTree._getTreeItem(sHandle).setSelected(True) + nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True @@ -149,10 +149,10 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): with monkeypatch.context() as mp: mp.setattr(GuiOutline, "treeFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - actItem = nwGUI.projView.outlineView.topLevelItem(0) + actItem = nwGUI.outlineView.outlineView.topLevelItem(0) chpItem = actItem.child(0) selItem = chpItem.child(0) - nwGUI.projView.outlineView.setCurrentItem(selItem) + nwGUI.outlineView.outlineView.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True @@ -220,14 +220,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.spellCheck is False # Check that tree items have been created - assert nwGUI.treeView.projTree._getTreeItem("0000000000008") is not None - assert nwGUI.treeView.projTree._getTreeItem("0000000000009") is not None - assert nwGUI.treeView.projTree._getTreeItem("000000000000a") is not None - assert nwGUI.treeView.projTree._getTreeItem("000000000000b") is not None - assert nwGUI.treeView.projTree._getTreeItem("000000000000c") is not None - assert nwGUI.treeView.projTree._getTreeItem("000000000000d") is not None - assert nwGUI.treeView.projTree._getTreeItem("000000000000e") is not None - assert nwGUI.treeView.projTree._getTreeItem("000000000000f") is not None + assert nwGUI.projView.projTree._getTreeItem("0000000000008") is not None + assert nwGUI.projView.projTree._getTreeItem("0000000000009") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000a") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000b") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000c") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000d") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000e") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None nwGUI.mainMenu.aSpellCheck.setChecked(True) assert nwGUI.mainMenu._toggleSpellCheck() @@ -240,9 +240,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a Character File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -262,9 +262,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a Plot File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem("0000000000009").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("0000000000009").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -284,9 +284,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a World File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem("000000000000b").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("000000000000b").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Add Some Text @@ -315,10 +315,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Select the 'New Scene' file nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.projTree.clearSelection() - nwGUI.treeView.projTree._getTreeItem("0000000000008").setExpanded(True) - nwGUI.treeView.projTree._getTreeItem("000000000000d").setExpanded(True) - nwGUI.treeView.projTree._getTreeItem("000000000000f").setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("0000000000008").setExpanded(True) + nwGUI.projView.projTree._getTreeItem("000000000000d").setExpanded(True) + nwGUI.projView.projTree._getTreeItem("000000000000f").setSelected(True) assert nwGUI.openSelectedItem() # Type something into the document @@ -461,12 +461,12 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock qtbot.wait(stepDelay) # Check a Quick Create and Delete - assert nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) - newHandle = nwGUI.treeView.getSelectedHandle() + assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None) + newHandle = nwGUI.projView.getSelectedHandle() assert nwGUI.theProject.tree["0000000000020"] is not None - assert nwGUI.treeView.deleteItem() - assert nwGUI.treeView.setSelectedHandle(newHandle) - assert nwGUI.treeView.deleteItem() + assert nwGUI.projView.deleteItem() + assert nwGUI.projView.setSelectedHandle(newHandle) + assert nwGUI.projView.deleteItem() assert nwGUI.theProject.tree["0000000000024"] is not None # Trash assert nwGUI.saveProject() diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 274746d7..f579a1a0 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -467,7 +467,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): buildTestProject(nwGUI, fncProj) - assert nwGUI.treeView.projTree._getTreeItem("000000000000f") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None assert nwGUI.openDocument("000000000000f") is True nwGUI.docEditor.clear() diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 49a86b16..c8aa893d 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -46,7 +46,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): nwGUI.rebuildIndex() nwGUI._changeView(nwView.OUTLINE) - outlineMain = nwGUI.projView + outlineMain = nwGUI.outlineView outlineView = outlineMain.outlineView outlineData = outlineMain.outlineData outlineMenu = outlineMain.outlineBar.mColumns @@ -54,7 +54,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): # Toggle scrollbars nwGUI.mainConf.hideVScroll = True nwGUI.mainConf.hideHScroll = True - nwGUI.projView.initOutline() + outlineMain.initOutline() assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff @@ -62,7 +62,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): nwGUI.mainConf.hideVScroll = False nwGUI.mainConf.hideHScroll = False - nwGUI.projView.initOutline() + outlineMain.initOutline() assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded @@ -169,8 +169,8 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.rebuildIndex() nwGUI._changeView(nwView.OUTLINE) - outlineMain = nwGUI.projView - outlineBar = outlineMain.outlineBar + outlineMain = nwGUI.outlineView + outlineBar = outlineMain.outlineBar outlineView = outlineMain.outlineView outlineData = outlineMain.outlineData @@ -183,7 +183,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): # Add a second novel folder newHandle = nwGUI.theProject.newRoot(nwItemClass.NOVEL) - nwGUI.treeView.revealNewTreeItem(newHandle) + nwGUI.projView.revealNewTreeItem(newHandle) # Check new values in dropdown list assert outlineBar.novelValue.itemData(0) == lipHandle @@ -202,7 +202,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): aHandle = nwGUI.theProject.newFile(dTitle, newHandle) hHash = "#"*hLevel writeFile(os.path.join(nwLipsum, "content", f"{aHandle}.nwd"), f"{hHash} {dTitle}\n\n") - nwGUI.treeView.revealNewTreeItem(aHandle) + nwGUI.projView.revealNewTreeItem(aHandle) nwGUI.rebuildIndex() @@ -248,7 +248,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): # Click POV Link assert outlineData.povKeyValue.text() == "Bod" - nwGUI.projView._tagClicked("Bod") + outlineMain._tagClicked("Bod") assert nwGUI.docViewer.docHandle() == "4c4f28287af27" # Scene One, Section Two diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 7b6d2f42..b3206aa5 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -41,7 +41,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) - nwTree = nwGUI.treeView + nwTree = nwGUI.projView # Try to add item with no project assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False @@ -164,7 +164,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) - nwTree = nwGUI.treeView + nwTree = nwGUI.projView # Try to move item with no project assert nwTree.projTree.moveTreeItem(1) is False @@ -279,7 +279,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) - nwTree = nwGUI.treeView + nwTree = nwGUI.projView # Try to run with no project assert nwTree.emptyTrash() is False @@ -482,7 +482,7 @@ def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR hCharNote = "0000000000011" hNovelNote = "0000000000012" - projTree = nwGUI.treeView.projTree + projTree = nwGUI.projView.projTree projTree._getTreeItem(hNovelRoot).setExpanded(True) projTree._getTreeItem(hChapterDir).setExpanded(True) diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 51e9c70b..0c6b1e81 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -40,7 +40,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): cHandle = nwGUI.theProject.newFile("A Note", "000000000000a") newDoc = NWDoc(nwGUI.theProject, cHandle) newDoc.writeDocument("# A Note\n\n") - nwGUI.treeView.revealNewTreeItem(cHandle) + nwGUI.projView.revealNewTreeItem(cHandle) nwGUI.rebuildIndex(beQuiet=True) # Reference Time From db01d85f2e9d0f39f38468344e20cf3808206a53 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 15:59:08 +0200 Subject: [PATCH 136/179] Rename outline components --- novelwriter/gui/__init__.py | 4 +- novelwriter/gui/outline.py | 42 +++++------ novelwriter/guimain.py | 4 +- tests/test_gui/test_gui_guimain.py | 8 +-- tests/test_gui/test_gui_outline.py | 108 ++++++++++++++--------------- 5 files changed, 83 insertions(+), 83 deletions(-) diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index 9560df1b..1892e83d 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -24,7 +24,7 @@ from novelwriter.gui.docviewer import GuiDocViewer, GuiDocViewDetails from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.mainmenu import GuiMainMenu from novelwriter.gui.noveltree import GuiNovelTree -from novelwriter.gui.outline import GuiOutline +from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.theme import GuiTheme @@ -38,7 +38,7 @@ __all__ = [ "GuiMainMenu", "GuiMainStatus", "GuiNovelTree", - "GuiOutline", + "GuiOutlineView", "GuiProjectView", "GuiTheme", "GuiViewsBar", diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index d2b2eb04..e3283ead 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -4,9 +4,9 @@ novelWriter – GUI Project Outline GUI class for the project outline view File History: -Created: 2022-05-15 [1.7b1] GuiOutline +Created: 2022-05-15 [1.7b1] GuiOutlineView Created: 2022-05-22 [1.7b1] GuiOutlineToolBar -Created: 2019-11-16 [0.4.1] GuiOutlineView +Created: 2019-11-16 [0.4.1] GuiOutlineTree Created: 2019-11-16 [0.4.1] GuiOutlineHeaderMenu Created: 2020-06-02 [0.7.0] GuiOutlineDetails @@ -52,7 +52,7 @@ from novelwriter.constants import trConst, nwKeyWords, nwLabels logger = logging.getLogger(__name__) -class GuiOutline(QWidget): +class GuiOutlineView(QWidget): loadDocumentTagRequest = pyqtSignal(str, Enum) @@ -64,11 +64,11 @@ class GuiOutline(QWidget): # Build GUI self.outlineBar = GuiOutlineToolBar(self) - self.outlineView = GuiOutlineView(self) + self.outlineTree = GuiOutlineTree(self) self.outlineData = GuiOutlineDetails(self) self.splitOutline = QSplitter(Qt.Vertical) - self.splitOutline.addWidget(self.outlineView) + self.splitOutline.addWidget(self.outlineTree) self.splitOutline.addWidget(self.outlineData) self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) @@ -81,14 +81,14 @@ class GuiOutline(QWidget): self.setLayout(self.outerBox) # Connect Signals - self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns) - self.outlineView.activeItemChanged.connect(self.outlineData.showItem) + self.outlineTree.hiddenStateChanged.connect(self._updateMenuColumns) + self.outlineTree.activeItemChanged.connect(self.outlineData.showItem) self.outlineData.itemTagClicked.connect(self._tagClicked) self.outlineBar.loadNovelRootRequest.connect(self._rootItemChanged) - self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled) + self.outlineBar.viewColumnToggled.connect(self.outlineTree.menuColumnToggled) # Function Mappings - self.getSelectedHandle = self.outlineView.getSelectedHandle + self.getSelectedHandle = self.outlineTree.getSelectedHandle return @@ -104,24 +104,24 @@ class GuiOutline(QWidget): return def initOutline(self): - self.outlineView.initOutline() + self.outlineTree.initOutline() self.outlineData.initDetails() return def closeOutline(self): - self.outlineView.closeOutline() + self.outlineTree.closeOutline() self.outlineData.updateClasses() return def refreshView(self, overRide=False, novelChanged=False): - self.outlineView.refreshTree(overRide=overRide, novelChanged=novelChanged) + self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged) return def treeFocus(self): - return self.outlineView.hasFocus() + return self.outlineTree.hasFocus() def setTreeFocus(self): - return self.outlineView.setFocus() + return self.outlineTree.setFocus() ## # Public Slots @@ -145,7 +145,7 @@ class GuiOutline(QWidget): checkboxes whenever a signal is received that the hidden state of columns has changed. """ - self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns) + self.outlineBar.setColumnHiddenState(self.outlineTree.hiddenColumns) return @pyqtSlot(str) @@ -160,10 +160,10 @@ class GuiOutline(QWidget): def _rootItemChanged(self, handle): """The root novel handle has changed or needs to be refreshed. """ - self.outlineView.refreshTree(rootHandle=(handle or None), overRide=True) + self.outlineTree.refreshTree(rootHandle=(handle or None), overRide=True) return -# END Class GuiOutline +# END Class GuiOutlineView class GuiOutlineToolBar(QToolBar): @@ -271,7 +271,7 @@ class GuiOutlineToolBar(QToolBar): # END Class GuiOutlineToolBar -class GuiOutlineView(QTreeWidget): +class GuiOutlineTree(QTreeWidget): DEF_WIDTH = { nwOutline.TITLE: 200, @@ -319,7 +319,7 @@ class GuiOutlineView(QTreeWidget): def __init__(self, theOutline): QTreeWidget.__init__(self, theOutline) - logger.debug("Initialising GuiOutlineView ...") + logger.debug("Initialising GuiOutlineTree ...") self.mainConf = novelwriter.CONFIG self.mainGui = theOutline.mainGui @@ -355,7 +355,7 @@ class GuiOutlineView(QTreeWidget): self.hiddenStateChanged.emit() - logger.debug("GuiOutlineView initialisation complete") + logger.debug("GuiOutlineTree initialisation complete") return @@ -717,7 +717,7 @@ class GuiOutlineView(QTreeWidget): return newItem -# END Class GuiOutlineView +# END Class GuiOutlineTree class GuiOutlineHeaderMenu(QMenu): diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 24c2180d..8c0e0e51 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, - GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectView, GuiTheme, + GuiMainStatus, GuiNovelTree, GuiOutlineView, GuiProjectView, GuiTheme, GuiViewsBar ) from novelwriter.dialogs import ( @@ -111,7 +111,7 @@ class GuiMain(QMainWindow): self.viewMeta = GuiDocViewDetails(self) self.docViewer = GuiDocViewer(self) self.itemDetails = GuiItemDetails(self) - self.outlineView = GuiOutline(self) + self.outlineView = GuiOutlineView(self) self.mainMenu = GuiMainMenu(self) self.viewsBar = GuiViewsBar(self) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 02c97617..cf38c7ea 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -28,7 +28,7 @@ from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QDialog, QInputDialog -from novelwriter.gui import GuiDocEditor, GuiNovelTree, GuiOutline +from novelwriter.gui import GuiDocEditor, GuiNovelTree, GuiOutlineView from novelwriter.enum import nwItemType, nwWidget from novelwriter.tools import GuiProjectWizard from novelwriter.gui.projtree import GuiProjectTree @@ -147,12 +147,12 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Project Outline has focus nwGUI.switchFocus(nwWidget.OUTLINE) with monkeypatch.context() as mp: - mp.setattr(GuiOutline, "treeFocus", lambda *a: True) + mp.setattr(GuiOutlineView, "treeFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - actItem = nwGUI.outlineView.outlineView.topLevelItem(0) + actItem = nwGUI.outlineView.outlineTree.topLevelItem(0) chpItem = actItem.child(0) selItem = chpItem.child(0) - nwGUI.outlineView.outlineView.setCurrentItem(selItem) + nwGUI.outlineView.outlineTree.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index c8aa893d..1038404a 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -46,57 +46,57 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): nwGUI.rebuildIndex() nwGUI._changeView(nwView.OUTLINE) - outlineMain = nwGUI.outlineView - outlineView = outlineMain.outlineView - outlineData = outlineMain.outlineData - outlineMenu = outlineMain.outlineBar.mColumns + outlineView = nwGUI.outlineView + outlineTree = outlineView.outlineTree + outlineData = outlineView.outlineData + outlineMenu = outlineView.outlineBar.mColumns # Toggle scrollbars nwGUI.mainConf.hideVScroll = True nwGUI.mainConf.hideHScroll = True - outlineMain.initOutline() - assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff - assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff + outlineView.initOutline() + assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff + assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff nwGUI.mainConf.hideVScroll = False nwGUI.mainConf.hideHScroll = False - outlineMain.initOutline() - assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded - assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded + outlineView.initOutline() + assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded + assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded # Check focus with monkeypatch.context() as mp: mp.setattr(QWidget, "hasFocus", lambda *a: True) - assert outlineMain.treeFocus() is True + assert outlineView.treeFocus() is True - outlineMain.setTreeFocus() # Can't check. just ensures that it doesn't error + outlineView.setTreeFocus() # Can't check. just ensures that it doesn't error # Option State # ============ pOptions = nwGUI.theProject.options colNames = [h.name for h in nwOutline] colItems = [h for h in nwOutline] - colWidth = {h: outlineView.DEF_WIDTH[h] for h in nwOutline} - colHidden = {h: outlineView.DEF_HIDDEN[h] for h in nwOutline} + colWidth = {h: outlineTree.DEF_WIDTH[h] for h in nwOutline} + colHidden = {h: outlineTree.DEF_HIDDEN[h] for h in nwOutline} - assert outlineView.topLevelItemCount() > 0 + assert outlineTree.topLevelItemCount() > 0 # Save header state not allowed - outlineView._lastBuild = 0 - outlineView._saveHeaderState() + outlineTree._lastBuild = 0 + outlineTree._saveHeaderState() assert pOptions.getValue("GuiOutline", "headerOrder", []) == [] # Allow saving header state - outlineView._lastBuild = time.time() - outlineView._saveHeaderState() + outlineTree._lastBuild = time.time() + outlineTree._saveHeaderState() assert pOptions.getValue("GuiOutline", "headerOrder", []) == colNames - assert outlineView._treeOrder == colItems - assert outlineView._colWidth == colWidth - assert outlineView._colHidden == colHidden + assert outlineTree._treeOrder == colItems + assert outlineTree._colWidth == colWidth + assert outlineTree._colHidden == colHidden # Get default values optItems = pOptions.getValue("GuiOutline", "headerOrder", []) @@ -105,49 +105,49 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): # Add invalid column name pOptions.setValue("GuiOutline", "headerOrder", optItems + ["blabla"]) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Add duplicate column name pOptions.setValue("GuiOutline", "headerOrder", optItems + [optItems[-1]]) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Invalid column width data pOptions.setValue("GuiOutline", "headerOrder", optItems) pOptions.setValue("GuiOutline", "columnWidth", {"blabla": None}) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Invalid column width data pOptions.setValue("GuiOutline", "headerOrder", optItems) pOptions.setValue("GuiOutline", "columnWidth", optWidth) pOptions.setValue("GuiOutline", "columnHidden", {"bloabla": None}) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Valid settings pOptions.setValue("GuiOutline", "headerOrder", optItems) pOptions.setValue("GuiOutline", "columnWidth", optWidth) pOptions.setValue("GuiOutline", "columnHidden", optHidden) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Header Menu # =========== # Trigger the menu entry for all hidden columns for hItem in nwOutline: - if outlineView.DEF_HIDDEN[hItem]: + if outlineTree.DEF_HIDDEN[hItem]: outlineMenu.actionMap[hItem].activate(QAction.Trigger) # Now no columns should be hidden - outlineView._saveHeaderState() + outlineTree._saveHeaderState() assert not any(pOptions.getValue("GuiOutline", "columnHidden", None).values()) # qtbot.stop() @@ -169,10 +169,10 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.rebuildIndex() nwGUI._changeView(nwView.OUTLINE) - outlineMain = nwGUI.outlineView - outlineBar = outlineMain.outlineBar - outlineView = outlineMain.outlineView - outlineData = outlineMain.outlineData + outlineView = nwGUI.outlineView + outlineBar = outlineView.outlineBar + outlineTree = outlineView.outlineTree + outlineData = outlineView.outlineData lipHandle = "b3643d0f92e32" @@ -218,10 +218,10 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): # ============= # First Item - outlineView.refreshTree() - selItem = outlineView.topLevelItem(0) + outlineTree.refreshTree() + selItem = outlineTree.topLevelItem(0) - outlineView.setCurrentItem(selItem) + outlineTree.setCurrentItem(selItem) assert outlineData.titleLabel.text() == "Title" assert outlineData.titleValue.text() == "Lorem Ipsum" assert outlineData.fileValue.text() == "Lorem Ipsum" @@ -232,12 +232,12 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): assert outlineData.pCValue.text() == "3" # Scene One - actItem = outlineView.topLevelItem(1) + actItem = outlineTree.topLevelItem(1) chpItem = actItem.child(0) selItem = chpItem.child(0) - outlineView.setCurrentItem(selItem) - tHandle, tLine = outlineView.getSelectedHandle() + outlineTree.setCurrentItem(selItem) + tHandle, tLine = outlineTree.getSelectedHandle() assert tHandle == "88243afbe5ed8" assert tLine == 0 @@ -248,17 +248,17 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): # Click POV Link assert outlineData.povKeyValue.text() == "Bod" - outlineMain._tagClicked("Bod") + outlineView._tagClicked("Bod") assert nwGUI.docViewer.docHandle() == "4c4f28287af27" # Scene One, Section Two - actItem = outlineView.topLevelItem(1) + actItem = outlineTree.topLevelItem(1) chpItem = actItem.child(0) scnItem = chpItem.child(0) selItem = scnItem.child(0) - outlineView.setCurrentItem(selItem) - tHandle, tLine = outlineView.getSelectedHandle() + outlineTree.setCurrentItem(selItem) + tHandle, tLine = outlineTree.getSelectedHandle() assert tHandle == "88243afbe5ed8" assert tLine == 12 @@ -267,7 +267,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): assert outlineData.fileValue.text() == "Scene One" assert outlineData.itemValue.text() == "Finished" - outlineView._treeDoubleClick(selItem, 0) + outlineTree._treeDoubleClick(selItem, 0) assert nwGUI.docEditor.docHandle() == "88243afbe5ed8" # qtbot.stop() From 985179bbb9c63a7863a77255a2d98bf0e241862f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 16:29:49 +0200 Subject: [PATCH 137/179] Rename main theme object --- novelwriter/dialogs/about.py | 54 +++++++------- novelwriter/dialogs/docmerge.py | 2 +- novelwriter/dialogs/docsplit.py | 2 +- novelwriter/dialogs/preferences.py | 70 +++++++++--------- novelwriter/dialogs/projdetails.py | 12 ++-- novelwriter/dialogs/projload.py | 12 ++-- novelwriter/dialogs/projsettings.py | 22 +++--- novelwriter/dialogs/updates.py | 2 +- novelwriter/dialogs/wordlist.py | 6 +- novelwriter/gui/doceditor.py | 90 +++++++++++------------ novelwriter/gui/dochighlight.py | 30 ++++---- novelwriter/gui/docviewer.py | 104 +++++++++++++-------------- novelwriter/gui/itemdetails.py | 20 +++--- novelwriter/gui/noveltree.py | 6 +- novelwriter/gui/outline.py | 24 +++---- novelwriter/gui/projtree.py | 34 ++++----- novelwriter/gui/statusbar.py | 20 +++--- novelwriter/gui/theme.py | 16 ++--- novelwriter/gui/viewsbar.py | 24 +++---- novelwriter/guimain.py | 4 +- novelwriter/tools/build.py | 18 ++--- novelwriter/tools/lipsum.py | 8 +-- novelwriter/tools/projwizard.py | 16 ++--- novelwriter/tools/writingstats.py | 28 ++++---- tests/test_dialogs/test_dlg_about.py | 4 +- tests/test_gui/test_gui_theme.py | 64 ++++++++--------- 26 files changed, 346 insertions(+), 346 deletions(-) diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 83e3c347..9cf43daf 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -49,9 +49,9 @@ class GuiAbout(QDialog): logger.debug("Initialising GuiAbout ...") self.setObjectName("GuiAbout") - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme self.outerBox = QVBoxLayout() self.innerBox = QHBoxLayout() @@ -63,7 +63,7 @@ class GuiAbout(QDialog): nPx = self.mainConf.pxInt(96) self.nwIcon = QLabel() - self.nwIcon.setPixmap(self.mainGui.theTheme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx))) self.lblName = QLabel("novelWriter") self.lblVers = QLabel(f"v{novelwriter.__version__}") self.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x")) @@ -191,37 +191,37 @@ class GuiAbout(QDialog): ]) ) - theTheme = self.mainGui.theTheme - theIcons = self.mainGui.theTheme.theIcons - if theTheme.themeName and theTheme.themeAuthor != "N/A": - licURL = f"{theTheme.themeLicense}" + mainTheme = self.mainGui.mainTheme + iconCache = self.mainGui.mainTheme.iconCache + if mainTheme.themeName and mainTheme.themeAuthor != "N/A": + licURL = f"{mainTheme.themeLicense}" aboutMsg += "

{0}

{1}

".format( - self.tr("Theme: {0}").format(theTheme.themeName), + self.tr("Theme: {0}").format(mainTheme.themeName), self._wrapTable([ - (self.tr("Author"), theTheme.themeAuthor), - (self.tr("Credit"), theTheme.themeCredit), + (self.tr("Author"), mainTheme.themeAuthor), + (self.tr("Credit"), mainTheme.themeCredit), (self.tr("Licence"), licURL), ]) ) - if theIcons.themeName: - licURL = f"{theIcons.themeLicense}" + if iconCache.themeName: + licURL = f"{iconCache.themeLicense}" aboutMsg += "

{0}

{1}

".format( - self.tr("Icons: {0}").format(theIcons.themeName), + self.tr("Icons: {0}").format(iconCache.themeName), self._wrapTable([ - (self.tr("Author"), theIcons.themeAuthor), - (self.tr("Credit"), theIcons.themeCredit), + (self.tr("Author"), iconCache.themeAuthor), + (self.tr("Credit"), iconCache.themeCredit), (self.tr("Licence"), licURL), ]) ) - if theTheme.syntaxName: - licURL = f"{theTheme.syntaxLicense}" + if mainTheme.syntaxName: + licURL = f"{mainTheme.syntaxLicense}" aboutMsg += "

{0}

{1}

".format( - self.tr("Syntax: {0}").format(theTheme.syntaxName), + self.tr("Syntax: {0}").format(mainTheme.syntaxName), self._wrapTable([ - (self.tr("Author"), theTheme.syntaxAuthor), - (self.tr("Credit"), theTheme.syntaxCredit), + (self.tr("Author"), mainTheme.syntaxAuthor), + (self.tr("Credit"), mainTheme.syntaxCredit), (self.tr("Licence"), licURL), ]) ) @@ -279,12 +279,12 @@ class GuiAbout(QDialog): " padding-right: 0.8em;" "}}\n" ).format( - hColR=self.mainGui.theTheme.colHead[0], - hColG=self.mainGui.theTheme.colHead[1], - hColB=self.mainGui.theTheme.colHead[2], - kColR=self.theTheme.colKey[0], - kColG=self.theTheme.colKey[1], - kColB=self.theTheme.colKey[2], + hColR=self.mainGui.mainTheme.colHead[0], + hColG=self.mainGui.mainTheme.colHead[1], + hColB=self.mainGui.mainTheme.colHead[2], + kColR=self.mainTheme.colKey[0], + kColG=self.mainTheme.colKey[1], + kColB=self.mainTheme.colKey[2], ) self.pageAbout.document().setDefaultStyleSheet(styleSheet) self.pageNotes.document().setDefaultStyleSheet(styleSheet) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 96a357f4..30eb602f 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -57,7 +57,7 @@ class GuiDocMerge(QDialog): self.headLabel = QLabel("{0}".format(self.tr("Documents to Merge"))) self.helpLabel = QHelpLabel( - self.tr("Drag and drop items to change the order."), self.mainGui.theTheme.helpText + self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText ) self.listBox = QListWidget() diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index c2d51c8e..b507479e 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -60,7 +60,7 @@ class GuiDocSplit(QDialog): self.headLabel = QLabel("{0}".format(self.tr("Document Headers"))) self.helpLabel = QHelpLabel( self.tr("Select the maximum level to split into files."), - self.mainGui.theTheme.helpText + self.mainGui.mainTheme.helpText ) self.listBox = QListWidget() diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index e0fd41e8..f807fde1 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -141,13 +141,13 @@ class GuiPreferencesGeneral(QWidget): def __init__(self, mainGui): QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Look and Feel @@ -174,7 +174,7 @@ class GuiPreferencesGeneral(QWidget): # Select Theme self.guiTheme = QComboBox() self.guiTheme.setMinimumWidth(minWidth) - self.theThemes = self.theTheme.listThemes() + self.theThemes = self.mainTheme.listThemes() for themeDir, themeName in self.theThemes: self.guiTheme.addItem(themeName, themeDir) themeIdx = self.guiTheme.findData(self.mainConf.guiTheme) @@ -190,8 +190,8 @@ class GuiPreferencesGeneral(QWidget): # Select Icon Theme self.guiIcons = QComboBox() self.guiIcons.setMinimumWidth(minWidth) - self.theIcons = self.theTheme.theIcons.listThemes() - for iconDir, iconName in self.theIcons: + self.iconCache = self.mainTheme.iconCache.listThemes() + for iconDir, iconName in self.iconCache: self.guiIcons.addItem(iconName, iconDir) iconIdx = self.guiIcons.findData(self.mainConf.guiIcons) if iconIdx != -1: @@ -206,7 +206,7 @@ class GuiPreferencesGeneral(QWidget): # Editor Theme self.guiSyntax = QComboBox() self.guiSyntax.setMinimumWidth(self.mainConf.pxInt(200)) - self.theSyntaxes = self.theTheme.listSyntax() + self.theSyntaxes = self.mainTheme.listSyntax() for syntaxFile, syntaxName in self.theSyntaxes: self.guiSyntax.addItem(syntaxName, syntaxFile) syntaxIdx = self.guiSyntax.findData(self.mainConf.guiSyntax) @@ -225,7 +225,7 @@ class GuiPreferencesGeneral(QWidget): self.guiFont.setFixedWidth(self.mainConf.pxInt(162)) self.guiFont.setText(self.mainConf.guiFont) self.fontButton = QPushButton("...") - self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.fontButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.mainForm.addRow( self.tr("Font family"), @@ -347,13 +347,13 @@ class GuiPreferencesProjects(QWidget): def __init__(self, mainGui): QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Automatic Save @@ -508,13 +508,13 @@ class GuiPreferencesDocuments(QWidget): def __init__(self, mainGui): QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Text Style @@ -527,7 +527,7 @@ class GuiPreferencesDocuments(QWidget): self.textFont.setFixedWidth(self.mainConf.pxInt(162)) self.textFont.setText(self.mainConf.textFont) self.fontButton = QPushButton("...") - self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.fontButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.mainForm.addRow( self.tr("Font family"), @@ -669,13 +669,13 @@ class GuiPreferencesEditor(QWidget): def __init__(self, mainGui): QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) mW = self.mainConf.pxInt(250) @@ -843,13 +843,13 @@ class GuiPreferencesSyntax(QWidget): def __init__(self, mainGui): QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Quotes & Dialogue @@ -946,13 +946,13 @@ class GuiPreferencesAutomation(QWidget): def __init__(self, mainGui): QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Automatic Features @@ -1103,13 +1103,13 @@ class GuiPreferencesQuotes(QWidget): def __init__(self, mainGui): QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Quotation Style @@ -1117,7 +1117,7 @@ class GuiPreferencesQuotes(QWidget): self.mainForm.addGroupLabel(self.tr("Quotation Style")) qWidth = self.mainConf.pxInt(40) - bWidth = int(2.5*self.theTheme.getTextWidth("...")) + bWidth = int(2.5*self.mainTheme.getTextWidth("...")) self.quoteSym = {} # Single Quote Style diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 8ab1943f..e83fc0a4 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -145,10 +145,10 @@ class GuiProjectDetailsMain(QWidget): self.mainConf = novelwriter.CONFIG self.theProject = theProject self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme - fPx = self.theTheme.fontPixelSize - fPt = self.theTheme.fontPointSize + fPx = self.mainTheme.fontPixelSize + fPt = self.mainTheme.fontPointSize vPx = self.mainConf.pxInt(4) hPx = self.mainConf.pxInt(12) @@ -277,12 +277,12 @@ class GuiProjectDetailsContents(QWidget): self.mainConf = novelwriter.CONFIG self.theProject = theProject self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme # Internal self._theToC = [] - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize hPx = self.mainConf.pxInt(12) vPx = self.mainConf.pxInt(4) pOptions = self.theProject.options @@ -469,7 +469,7 @@ class GuiProjectDetailsContents(QWidget): if tTitle.strip() == "": tTitle = self.tr("Untitled") - newItem.setIcon(self.C_TITLE, self.theTheme.getIcon("doc_h%d" % tLevel)) + newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon("doc_h%d" % tLevel)) newItem.setText(self.C_TITLE, tTitle) newItem.setText(self.C_WORDS, f"{wCount:n}") newItem.setText(self.C_PAGES, f"{pCount:n}") diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index 6d296980..c7229f44 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -61,13 +61,13 @@ class GuiProjectLoad(QDialog): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.openState = self.NONE_STATE self.openPath = None sPx = self.mainConf.pxInt(16) nPx = self.mainConf.pxInt(96) - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize self.outerBox = QVBoxLayout() self.innerBox = QHBoxLayout() @@ -80,7 +80,7 @@ class GuiProjectLoad(QDialog): self.setModal(True) self.nwIcon = QLabel() - self.nwIcon.setPixmap(self.mainGui.theTheme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx))) self.innerBox.addWidget(self.nwIcon, 0, Qt.AlignTop) self.projectForm = QGridLayout() @@ -110,7 +110,7 @@ class GuiProjectLoad(QDialog): self.selPath.setReadOnly(True) self.browseButton = QPushButton("...") - self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.browseButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.browseButton.clicked.connect(self._doBrowse) self.projectForm.addWidget(self.lblRecent, 0, 0, 1, 3) @@ -280,7 +280,7 @@ class GuiProjectLoad(QDialog): sortList = sorted(dataList, key=lambda x: x[1], reverse=True) for theTitle, theTime, theWords, projPath in sortList: newItem = QTreeWidgetItem([""]*4) - newItem.setIcon(self.C_NAME, self.mainGui.theTheme.getIcon("proj_nwx")) + newItem.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx")) newItem.setText(self.C_NAME, theTitle) newItem.setData(self.C_NAME, Qt.UserRole, projPath) newItem.setText(self.C_COUNT, formatInt(theWords)) @@ -288,7 +288,7 @@ class GuiProjectLoad(QDialog): newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter) - newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed) + newItem.setFont(self.C_TIME, self.mainTheme.guiFontFixed) self.listBox.addTopLevelItem(newItem) if self.listBox.topLevelItemCount() > 0: diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index c7531866..11a640b1 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -175,13 +175,13 @@ class GuiProjectEditMain(QWidget): # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.mainGui.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainGui.mainTheme.helpText) self.setLayout(self.mainForm) self.mainForm.addGroupLabel(self.tr("Project Settings")) xW = self.mainConf.pxInt(250) - xH = round(4.8*self.mainGui.theTheme.fontPixelSize) + xH = round(4.8*self.mainGui.mainTheme.fontPixelSize) self.editName = QLineEdit() self.editName.setMaxLength(200) @@ -262,7 +262,7 @@ class GuiProjectEditStatus(QWidget): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui self.theProject = theProject - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme if isStatus: self.theStatus = self.theProject.statusItems @@ -281,7 +281,7 @@ class GuiProjectEditStatus(QWidget): self.colChanged = False self.selColour = QColor(100, 100, 100) - self.iPx = self.theTheme.baseIconSize + self.iPx = self.mainTheme.baseIconSize # The List # ======== @@ -300,16 +300,16 @@ class GuiProjectEditStatus(QWidget): # List Controls # ============= - self.addButton = QPushButton(self.theTheme.getIcon("add"), "") + self.addButton = QPushButton(self.mainTheme.getIcon("add"), "") self.addButton.clicked.connect(self._newItem) - self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") + self.delButton = QPushButton(self.mainTheme.getIcon("remove"), "") self.delButton.clicked.connect(self._delItem) - self.upButton = QPushButton(self.theTheme.getIcon("up"), "") + self.upButton = QPushButton(self.mainTheme.getIcon("up"), "") self.upButton.clicked.connect(lambda: self._moveItem(-1)) - self.dnButton = QPushButton(self.theTheme.getIcon("down"), "") + self.dnButton = QPushButton(self.mainTheme.getIcon("down"), "") self.dnButton.clicked.connect(lambda: self._moveItem(1)) # Edit Form @@ -532,7 +532,7 @@ class GuiProjectEditReplace(QWidget): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.theProject = theProject self.arChanged = False @@ -563,10 +563,10 @@ class GuiProjectEditReplace(QWidget): # List Controls # ============= - self.addButton = QPushButton(self.theTheme.getIcon("add"), "") + self.addButton = QPushButton(self.mainTheme.getIcon("add"), "") self.addButton.clicked.connect(self._addEntry) - self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") + self.delButton = QPushButton(self.mainTheme.getIcon("remove"), "") self.delButton.clicked.connect(self._delEntry) # Edit Form diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py index a8f3c8a9..c4782e6d 100644 --- a/novelwriter/dialogs/updates.py +++ b/novelwriter/dialogs/updates.py @@ -61,7 +61,7 @@ class GuiUpdates(QDialog): # Left Box self.nwIcon = QLabel() - self.nwIcon.setPixmap(self.mainGui.theTheme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx))) self.leftBox = QVBoxLayout() self.leftBox.addWidget(self.nwIcon) diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 172ae846..9a17eed8 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -50,7 +50,7 @@ class GuiWordList(QDialog): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.theProject = mainGui.theProject self.setWindowTitle(self.tr("Project Word List")) @@ -78,10 +78,10 @@ class GuiWordList(QDialog): self.newEntry = QLineEdit() - self.addButton = QPushButton(self.theTheme.getIcon("add"), "") + self.addButton = QPushButton(self.mainTheme.getIcon("add"), "") self.addButton.clicked.connect(self._doAdd) - self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") + self.delButton = QPushButton(self.mainTheme.getIcon("remove"), "") self.delButton.clicked.connect(self._doDelete) self.editBox = QHBoxLayout() diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index a060f151..ab2c7f2f 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -80,7 +80,7 @@ class GuiDocEditor(QTextEdit): # Class Variables self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.theProject = mainGui.theProject self._nwDocument = None @@ -239,10 +239,10 @@ class GuiDocEditor(QTextEdit): if self.mainConf.textFont is None: # If none is defined, set a default font theFont = QFont() - if self.mainConf.osWindows and "Arial" in self.theTheme.guiFontDB.families(): + if self.mainConf.osWindows and "Arial" in self.mainTheme.guiFontDB.families(): theFont.setFamily("Arial") theFont.setPointSize(12) - elif self.mainConf.osDarwin and "Courier" in self.theTheme.guiFontDB.families(): + elif self.mainConf.osDarwin and "Courier" in self.mainTheme.guiFontDB.families(): theFont.setFamily("Courier") theFont.setPointSize(12) else: @@ -257,14 +257,14 @@ class GuiDocEditor(QTextEdit): # Set the widget colours to match syntax theme mainPalette = self.palette() - mainPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - mainPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) - mainPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + mainPalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + mainPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack)) + mainPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) - docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + docPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack)) + docPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -2189,7 +2189,7 @@ class GuiDocEditSearch(QFrame): self.docEditor = docEditor self.mainGui = docEditor.mainGui self.theProject = docEditor.theProject - self.theTheme = docEditor.theTheme + self.mainTheme = docEditor.mainTheme self.repVisible = False self.isCaseSense = self.mainConf.searchCase @@ -2200,9 +2200,9 @@ class GuiDocEditSearch(QFrame): self.doMatchCap = self.mainConf.searchMatchCap mPx = self.mainConf.pxInt(6) - tPx = int(0.8*self.theTheme.fontPixelSize) - self.boxFont = self.theTheme.guiFont - self.boxFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + tPx = int(0.8*self.mainTheme.fontPixelSize) + self.boxFont = self.mainTheme.guiFont + self.boxFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) @@ -2236,38 +2236,38 @@ class GuiDocEditSearch(QFrame): self.resultLabel = QLabel("?/?") self.resultLabel.setFont(self.boxFont) - self.resultLabel.setMinimumWidth(self.theTheme.getTextWidth("?/?", self.boxFont)) + self.resultLabel.setMinimumWidth(self.mainTheme.getTextWidth("?/?", self.boxFont)) self.toggleCase = QAction(self.tr("Case Sensitive"), self) - self.toggleCase.setIcon(self.theTheme.getIcon("search_case")) + self.toggleCase.setIcon(self.mainTheme.getIcon("search_case")) self.toggleCase.setCheckable(True) self.toggleCase.setChecked(self.isCaseSense) self.toggleCase.toggled.connect(self._doToggleCase) self.searchOpt.addAction(self.toggleCase) self.toggleWord = QAction(self.tr("Whole Words Only"), self) - self.toggleWord.setIcon(self.theTheme.getIcon("search_word")) + self.toggleWord.setIcon(self.mainTheme.getIcon("search_word")) self.toggleWord.setCheckable(True) self.toggleWord.setChecked(self.isWholeWord) self.toggleWord.toggled.connect(self._doToggleWord) self.searchOpt.addAction(self.toggleWord) self.toggleRegEx = QAction(self.tr("RegEx Mode"), self) - self.toggleRegEx.setIcon(self.theTheme.getIcon("search_regex")) + self.toggleRegEx.setIcon(self.mainTheme.getIcon("search_regex")) self.toggleRegEx.setCheckable(True) self.toggleRegEx.setChecked(self.isRegEx) self.toggleRegEx.toggled.connect(self._doToggleRegEx) self.searchOpt.addAction(self.toggleRegEx) self.toggleLoop = QAction(self.tr("Loop Search"), self) - self.toggleLoop.setIcon(self.theTheme.getIcon("search_loop")) + self.toggleLoop.setIcon(self.mainTheme.getIcon("search_loop")) self.toggleLoop.setCheckable(True) self.toggleLoop.setChecked(self.doLoop) self.toggleLoop.toggled.connect(self._doToggleLoop) self.searchOpt.addAction(self.toggleLoop) self.toggleProject = QAction(self.tr("Search Next File"), self) - self.toggleProject.setIcon(self.theTheme.getIcon("search_project")) + self.toggleProject.setIcon(self.mainTheme.getIcon("search_project")) self.toggleProject.setCheckable(True) self.toggleProject.setChecked(self.doNextFile) self.toggleProject.toggled.connect(self._doToggleProject) @@ -2276,7 +2276,7 @@ class GuiDocEditSearch(QFrame): self.searchOpt.addSeparator() self.toggleMatchCap = QAction(self.tr("Preserve Case"), self) - self.toggleMatchCap.setIcon(self.theTheme.getIcon("search_preserve")) + self.toggleMatchCap.setIcon(self.mainTheme.getIcon("search_preserve")) self.toggleMatchCap.setCheckable(True) self.toggleMatchCap.setChecked(self.doMatchCap) self.toggleMatchCap.toggled.connect(self._doToggleMatchCap) @@ -2285,7 +2285,7 @@ class GuiDocEditSearch(QFrame): self.searchOpt.addSeparator() self.cancelSearch = QAction(self.tr("Close Search"), self) - self.cancelSearch.setIcon(self.theTheme.getIcon("search_cancel")) + self.cancelSearch.setIcon(self.mainTheme.getIcon("search_cancel")) self.cancelSearch.triggered.connect(self._doClose) self.searchOpt.addAction(self.cancelSearch) @@ -2300,12 +2300,12 @@ class GuiDocEditSearch(QFrame): self.showReplace.setStyleSheet("QToolButton {border: none; background: transparent;}") self.showReplace.toggled.connect(self._doToggleReplace) - self.searchButton = QPushButton(self.theTheme.getIcon("search"), "") + self.searchButton = QPushButton(self.mainTheme.getIcon("search"), "") self.searchButton.setFixedSize(QSize(bPx, bPx)) self.searchButton.setToolTip(self.tr("Find in current document")) self.searchButton.clicked.connect(self._doSearch) - self.replaceButton = QPushButton(self.theTheme.getIcon("search_replace"), "") + self.replaceButton = QPushButton(self.mainTheme.getIcon("search_replace"), "") self.replaceButton.setFixedSize(QSize(bPx, bPx)) self.replaceButton.setToolTip(self.tr("Find and replace in current document")) self.replaceButton.clicked.connect(self._doReplace) @@ -2424,7 +2424,7 @@ class GuiDocEditSearch(QFrame): """ currRes = "?" if currRes is None else currRes resCount = "?" if resCount is None else "1000+" if resCount > 1000 else resCount - minWidth = self.theTheme.getTextWidth(f"{resCount}//{resCount}", self.boxFont) + minWidth = self.mainTheme.getTextWidth(f"{resCount}//{resCount}", self.boxFont) self.resultLabel.setText(f"{currRes}/{resCount}") self.resultLabel.setMinimumWidth(minWidth) self.adjustSize() @@ -2577,11 +2577,11 @@ class GuiDocEditHeader(QWidget): self.docEditor = docEditor self.mainGui = docEditor.mainGui self.theProject = docEditor.theProject - self.theTheme = docEditor.theTheme + self.mainTheme = docEditor.mainTheme self._docHandle = None - fPx = int(0.9*self.theTheme.fontPixelSize) + fPx = int(0.9*self.mainTheme.fontPixelSize) hSp = self.mainConf.pxInt(6) # Main Widget Settings @@ -2598,17 +2598,17 @@ class GuiDocEditHeader(QWidget): self.theTitle.setFixedHeight(fPx) lblFont = self.theTitle.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) self.theTitle.setFont(lblFont) buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*self.theTheme.colText) + ).format(*self.mainTheme.colText) # Buttons self.editButton = QToolButton(self) - self.editButton.setIcon(self.theTheme.getIcon("edit")) + self.editButton.setIcon(self.mainTheme.getIcon("edit")) self.editButton.setContentsMargins(0, 0, 0, 0) self.editButton.setIconSize(QSize(fPx, fPx)) self.editButton.setFixedSize(fPx, fPx) @@ -2619,7 +2619,7 @@ class GuiDocEditHeader(QWidget): self.editButton.clicked.connect(self._editDocument) self.searchButton = QToolButton(self) - self.searchButton.setIcon(self.theTheme.getIcon("search")) + self.searchButton.setIcon(self.mainTheme.getIcon("search")) self.searchButton.setContentsMargins(0, 0, 0, 0) self.searchButton.setIconSize(QSize(fPx, fPx)) self.searchButton.setFixedSize(fPx, fPx) @@ -2630,7 +2630,7 @@ class GuiDocEditHeader(QWidget): self.searchButton.clicked.connect(self._searchDocument) self.minmaxButton = QToolButton(self) - self.minmaxButton.setIcon(self.theTheme.getIcon("maximise")) + self.minmaxButton.setIcon(self.mainTheme.getIcon("maximise")) self.minmaxButton.setContentsMargins(0, 0, 0, 0) self.minmaxButton.setIconSize(QSize(fPx, fPx)) self.minmaxButton.setFixedSize(fPx, fPx) @@ -2641,7 +2641,7 @@ class GuiDocEditHeader(QWidget): self.minmaxButton.clicked.connect(self._minmaxDocument) self.closeButton = QToolButton(self) - self.closeButton.setIcon(self.theTheme.getIcon("close")) + self.closeButton.setIcon(self.mainTheme.getIcon("close")) self.closeButton.setContentsMargins(0, 0, 0, 0) self.closeButton.setIconSize(QSize(fPx, fPx)) self.closeButton.setFixedSize(fPx, fPx) @@ -2684,9 +2684,9 @@ class GuiDocEditHeader(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText)) - thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText)) + thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(thePalette) self.theTitle.setPalette(thePalette) @@ -2734,9 +2734,9 @@ class GuiDocEditHeader(QWidget): toggleFocusMode function and should not be activated directly. """ if self.mainGui.isFocusMode: - self.minmaxButton.setIcon(self.theTheme.getIcon("minimise")) + self.minmaxButton.setIcon(self.mainTheme.getIcon("minimise")) else: - self.minmaxButton.setIcon(self.theTheme.getIcon("maximise")) + self.minmaxButton.setIcon(self.mainTheme.getIcon("maximise")) return ## @@ -2801,20 +2801,20 @@ class GuiDocEditFooter(QWidget): self.docEditor = docEditor self.mainGui = docEditor.mainGui self.theProject = docEditor.theProject - self.theTheme = docEditor.theTheme + self.mainTheme = docEditor.mainTheme self._theItem = None self._docHandle = None self._docSelection = False - self.sPx = int(round(0.9*self.theTheme.baseIconSize)) - fPx = int(0.9*self.theTheme.fontPixelSize) + self.sPx = int(round(0.9*self.mainTheme.baseIconSize)) + fPx = int(0.9*self.mainTheme.fontPixelSize) bSp = self.mainConf.pxInt(4) hSp = self.mainConf.pxInt(6) lblFont = self.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) # Main Widget Settings self.setContentsMargins(0, 0, 0, 0) @@ -2837,7 +2837,7 @@ class GuiDocEditFooter(QWidget): # Lines self.linesIcon = QLabel("") - self.linesIcon.setPixmap(self.theTheme.getPixmap("status_lines", (self.sPx, self.sPx))) + self.linesIcon.setPixmap(self.mainTheme.getPixmap("status_lines", (self.sPx, self.sPx))) self.linesIcon.setContentsMargins(0, 0, 0, 0) self.linesIcon.setFixedHeight(self.sPx) self.linesIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop) @@ -2853,7 +2853,7 @@ class GuiDocEditFooter(QWidget): # Words self.wordsIcon = QLabel("") - self.wordsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (self.sPx, self.sPx))) + self.wordsIcon.setPixmap(self.mainTheme.getPixmap("status_stats", (self.sPx, self.sPx))) self.wordsIcon.setContentsMargins(0, 0, 0, 0) self.wordsIcon.setFixedHeight(self.sPx) self.wordsIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop) @@ -2905,9 +2905,9 @@ class GuiDocEditFooter(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText)) - thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText)) + thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(thePalette) self.statusText.setPalette(thePalette) diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index d811332c..8b7ec1a5 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -54,7 +54,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.theDoc = theDoc self.spEnchant = spEnchant self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.theProject = mainGui.theProject self.theHandle = None self.spellCheck = False @@ -87,24 +87,24 @@ class GuiDocHighlighter(QSyntaxHighlighter): """ logger.debug("Setting up highlighting rules") - self.colHead = QColor(*self.theTheme.colHead) - self.colHeadH = QColor(*self.theTheme.colHeadH) - self.colDialN = QColor(*self.theTheme.colDialN) - self.colDialD = QColor(*self.theTheme.colDialD) - self.colDialS = QColor(*self.theTheme.colDialS) - self.colHidden = QColor(*self.theTheme.colHidden) - self.colKey = QColor(*self.theTheme.colKey) - self.colVal = QColor(*self.theTheme.colVal) - self.colSpell = QColor(*self.theTheme.colSpell) - self.colError = QColor(*self.theTheme.colError) - self.colRepTag = QColor(*self.theTheme.colRepTag) - self.colMod = QColor(*self.theTheme.colMod) - self.colBreak = QColor(*self.theTheme.colEmph) + self.colHead = QColor(*self.mainTheme.colHead) + self.colHeadH = QColor(*self.mainTheme.colHeadH) + self.colDialN = QColor(*self.mainTheme.colDialN) + self.colDialD = QColor(*self.mainTheme.colDialD) + self.colDialS = QColor(*self.mainTheme.colDialS) + self.colHidden = QColor(*self.mainTheme.colHidden) + self.colKey = QColor(*self.mainTheme.colKey) + self.colVal = QColor(*self.mainTheme.colVal) + self.colSpell = QColor(*self.mainTheme.colSpell) + self.colError = QColor(*self.mainTheme.colError) + self.colRepTag = QColor(*self.mainTheme.colRepTag) + self.colMod = QColor(*self.mainTheme.colMod) + self.colBreak = QColor(*self.mainTheme.colEmph) self.colBreak.setAlpha(64) self.colEmph = None if self.mainConf.highlightEmph: - self.colEmph = QColor(*self.theTheme.colEmph) + self.colEmph = QColor(*self.mainTheme.colEmph) self.hStyles = { "header1": self._makeFormat(self.colHead, "bold", 1.8), diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index f77e02ec..6ce275a3 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -61,7 +61,7 @@ class GuiDocViewer(QTextBrowser): # Class Variables self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.theProject = mainGui.theProject # Internal Variables @@ -118,14 +118,14 @@ class GuiDocViewer(QTextBrowser): # Set the widget colours to match syntax theme mainPalette = self.palette() - mainPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - mainPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) - mainPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + mainPalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + mainPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack)) + mainPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) - docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + docPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack)) + docPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -539,27 +539,27 @@ class GuiDocViewer(QTextBrowser): " text-align: center;" "}}\n" ).format( - tColR=self.theTheme.colText[0], - tColG=self.theTheme.colText[1], - tColB=self.theTheme.colText[2], - hColR=self.theTheme.colHead[0], - hColG=self.theTheme.colHead[1], - hColB=self.theTheme.colHead[2], - aColR=self.theTheme.colVal[0], - aColG=self.theTheme.colVal[1], - aColB=self.theTheme.colVal[2], - eColR=self.theTheme.colEmph[0], - eColG=self.theTheme.colEmph[1], - eColB=self.theTheme.colEmph[2], - kColR=self.theTheme.colKey[0], - kColG=self.theTheme.colKey[1], - kColB=self.theTheme.colKey[2], - cColR=self.theTheme.colHidden[0], - cColG=self.theTheme.colHidden[1], - cColB=self.theTheme.colHidden[2], - mColR=self.theTheme.colMod[0], - mColG=self.theTheme.colMod[1], - mColB=self.theTheme.colMod[2], + tColR=self.mainTheme.colText[0], + tColG=self.mainTheme.colText[1], + tColB=self.mainTheme.colText[2], + hColR=self.mainTheme.colHead[0], + hColG=self.mainTheme.colHead[1], + hColB=self.mainTheme.colHead[2], + aColR=self.mainTheme.colVal[0], + aColG=self.mainTheme.colVal[1], + aColB=self.mainTheme.colVal[2], + eColR=self.mainTheme.colEmph[0], + eColG=self.mainTheme.colEmph[1], + eColB=self.mainTheme.colEmph[2], + kColR=self.mainTheme.colKey[0], + kColG=self.mainTheme.colKey[1], + kColB=self.mainTheme.colKey[2], + cColR=self.mainTheme.colHidden[0], + cColG=self.mainTheme.colHidden[1], + cColB=self.mainTheme.colHidden[2], + mColR=self.mainTheme.colMod[0], + mColG=self.mainTheme.colMod[1], + mColB=self.mainTheme.colMod[2], ) self.document().setDefaultStyleSheet(styleSheet) @@ -716,12 +716,12 @@ class GuiDocViewHeader(QWidget): self.docViewer = docViewer self.mainGui = docViewer.mainGui self.theProject = docViewer.theProject - self.theTheme = docViewer.theTheme + self.mainTheme = docViewer.mainTheme # Internal Variables self._docHandle = None - fPx = int(0.9*self.theTheme.fontPixelSize) + fPx = int(0.9*self.mainTheme.fontPixelSize) hSp = self.mainConf.pxInt(6) # Main Widget Settings @@ -738,17 +738,17 @@ class GuiDocViewHeader(QWidget): self.theTitle.setFixedHeight(fPx) lblFont = self.theTitle.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) self.theTitle.setFont(lblFont) buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*self.theTheme.colText) + ).format(*self.mainTheme.colText) # Buttons self.backButton = QToolButton(self) - self.backButton.setIcon(self.theTheme.getIcon("backward")) + self.backButton.setIcon(self.mainTheme.getIcon("backward")) self.backButton.setContentsMargins(0, 0, 0, 0) self.backButton.setIconSize(QSize(fPx, fPx)) self.backButton.setFixedSize(fPx, fPx) @@ -759,7 +759,7 @@ class GuiDocViewHeader(QWidget): self.backButton.clicked.connect(self.docViewer.navBackward) self.forwardButton = QToolButton(self) - self.forwardButton.setIcon(self.theTheme.getIcon("forward")) + self.forwardButton.setIcon(self.mainTheme.getIcon("forward")) self.forwardButton.setContentsMargins(0, 0, 0, 0) self.forwardButton.setIconSize(QSize(fPx, fPx)) self.forwardButton.setFixedSize(fPx, fPx) @@ -770,7 +770,7 @@ class GuiDocViewHeader(QWidget): self.forwardButton.clicked.connect(self.docViewer.navForward) self.refreshButton = QToolButton(self) - self.refreshButton.setIcon(self.theTheme.getIcon("refresh")) + self.refreshButton.setIcon(self.mainTheme.getIcon("refresh")) self.refreshButton.setContentsMargins(0, 0, 0, 0) self.refreshButton.setIconSize(QSize(fPx, fPx)) self.refreshButton.setFixedSize(fPx, fPx) @@ -781,7 +781,7 @@ class GuiDocViewHeader(QWidget): self.refreshButton.clicked.connect(self._refreshDocument) self.closeButton = QToolButton(self) - self.closeButton.setIcon(self.theTheme.getIcon("close")) + self.closeButton.setIcon(self.mainTheme.getIcon("close")) self.closeButton.setContentsMargins(0, 0, 0, 0) self.closeButton.setIconSize(QSize(fPx, fPx)) self.closeButton.setFixedSize(fPx, fPx) @@ -824,9 +824,9 @@ class GuiDocViewHeader(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText)) - thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText)) + thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(thePalette) self.theTitle.setPalette(thePalette) @@ -922,25 +922,25 @@ class GuiDocViewFooter(QWidget): self.mainConf = novelwriter.CONFIG self.docViewer = docViewer self.mainGui = docViewer.mainGui - self.theTheme = docViewer.theTheme + self.mainTheme = docViewer.mainTheme self.viewMeta = docViewer.mainGui.viewMeta # Internal Variables self._docHandle = None - fPx = int(0.9*self.theTheme.fontPixelSize) + fPx = int(0.9*self.mainTheme.fontPixelSize) bSp = self.mainConf.pxInt(2) hSp = self.mainConf.pxInt(8) # Icons - stickyOn = self.theTheme.getPixmap("sticky-on", (fPx, fPx)) - stickyOff = self.theTheme.getPixmap("sticky-off", (fPx, fPx)) + stickyOn = self.mainTheme.getPixmap("sticky-on", (fPx, fPx)) + stickyOff = self.mainTheme.getPixmap("sticky-off", (fPx, fPx)) stickyIcon = QIcon() stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On) stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off) - bulletOn = self.theTheme.getPixmap("bullet-on", (fPx, fPx)) - bulletOff = self.theTheme.getPixmap("bullet-off", (fPx, fPx)) + bulletOn = self.mainTheme.getPixmap("bullet-on", (fPx, fPx)) + bulletOff = self.mainTheme.getPixmap("bullet-off", (fPx, fPx)) bulletIcon = QIcon() bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On) bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off) @@ -952,13 +952,13 @@ class GuiDocViewFooter(QWidget): buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*self.theTheme.colText) + ).format(*self.mainTheme.colText) # Show/Hide Details self.showHide = QToolButton(self) self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly) self.showHide.setStyleSheet(buttonStyle) - self.showHide.setIcon(self.theTheme.getIcon("reference")) + self.showHide.setIcon(self.mainTheme.getIcon("reference")) self.showHide.setIconSize(QSize(fPx, fPx)) self.showHide.setFixedSize(QSize(fPx, fPx)) self.showHide.clicked.connect(self._doShowHide) @@ -1039,7 +1039,7 @@ class GuiDocViewFooter(QWidget): self.lblSynopsis.setAlignment(Qt.AlignLeft | Qt.AlignTop) lblFont = self.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) self.lblRefs.setFont(lblFont) self.lblSticky.setFont(lblFont) self.lblComments.setFont(lblFont) @@ -1084,9 +1084,9 @@ class GuiDocViewFooter(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText)) - thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText)) + thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(thePalette) self.lblRefs.setPalette(thePalette) @@ -1147,7 +1147,7 @@ class GuiDocViewDetails(QScrollArea): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui self.theProject = mainGui.theProject - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.refList = QLabel("") self.refList.setWordWrap(True) @@ -1156,7 +1156,7 @@ class GuiDocViewDetails(QScrollArea): self.refList.linkActivated.connect(self._linkClicked) self.linkStyle = "style='color: rgb({0},{1},{2})'".format( - *self.theTheme.colLink + *self.mainTheme.colLink ) # Assemble diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 97d4ba0e..859e58f2 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -45,7 +45,7 @@ class GuiItemDetails(QWidget): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui self.theProject = mainGui.theProject - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme # Internal Variables self._itemHandle = None @@ -54,11 +54,11 @@ class GuiItemDetails(QWidget): hSp = self.mainConf.pxInt(6) vSp = self.mainConf.pxInt(1) mPx = self.mainConf.pxInt(6) - iPx = self.theTheme.baseIconSize - fPt = self.theTheme.fontPointSize + iPx = self.mainTheme.baseIconSize + fPt = self.mainTheme.fontPointSize - self._expCheck = self.theTheme.getPixmap("check", (iPx, iPx)) - self._expCross = self.theTheme.getPixmap("cross", (iPx, iPx)) + self._expCheck = self.mainTheme.getPixmap("check", (iPx, iPx)) + self._expCross = self.mainTheme.getPixmap("cross", (iPx, iPx)) fntLabel = QFont() fntLabel.setBold(True) @@ -181,8 +181,8 @@ class GuiItemDetails(QWidget): self.setLayout(self.mainBox) # Make sure the columns for flags and counts don't resize too often - flagWidth = self.theTheme.getTextWidth("Mm", fntValue) - countWidth = self.theTheme.getTextWidth("99,999", fntValue) + flagWidth = self.mainTheme.getTextWidth("Mm", fntValue) + countWidth = self.mainTheme.getTextWidth("99,999", fntValue) self.mainBox.setColumnMinimumWidth(1, flagWidth) self.mainBox.setColumnMinimumWidth(4, countWidth) @@ -238,7 +238,7 @@ class GuiItemDetails(QWidget): return self._itemHandle = tHandle - iPx = int(round(0.8*self.theTheme.baseIconSize)) + iPx = int(round(0.8*self.mainTheme.baseIconSize)) # Label # ===== @@ -267,7 +267,7 @@ class GuiItemDetails(QWidget): # Class # ===== - classIcon = self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]) + classIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]) self.classIcon.setPixmap(classIcon.pixmap(iPx, iPx)) self.classData.setText(trConst(nwLabels.CLASS_NAME[nwItem.itemClass])) @@ -275,7 +275,7 @@ class GuiItemDetails(QWidget): # ====== hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) - usageIcon = self.theTheme.getItemIcon( + usageIcon = self.mainTheme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel ) self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx)) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index dbdd02c0..f634ce23 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -52,7 +52,7 @@ class GuiNovelTree(QTreeWidget): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.theProject = mainGui.theProject # Internal Variables @@ -60,7 +60,7 @@ class GuiNovelTree(QTreeWidget): self._lastBuild = 0 # Build GUI - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize self.setFrameStyle(QFrame.NoFrame) self.setIconSize(QSize(iPx, iPx)) self.setIndentation(iPx) @@ -309,7 +309,7 @@ class GuiNovelTree(QTreeWidget): newItem.setText(self.C_TITLE, novIdx.title) newItem.setData(self.C_TITLE, Qt.UserRole, theData) - newItem.setIcon(self.C_TITLE, self.theTheme.getIcon(hIcon)) + newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon(hIcon)) newItem.setText(self.C_WORDS, f"{wC:n}") newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index e3283ead..c858c480 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -179,7 +179,7 @@ class GuiOutlineToolBar(QToolBar): self.mainConf = novelwriter.CONFIG self.mainGui = theOutline.mainGui self.theProject = theOutline.mainGui.theProject - self.theTheme = theOutline.mainGui.theTheme + self.mainTheme = theOutline.mainGui.mainTheme iPx = self.mainConf.pxInt(22) mPx = self.mainConf.pxInt(12) @@ -202,7 +202,7 @@ class GuiOutlineToolBar(QToolBar): # Actions self.aRefresh = QAction(self.tr("Refresh"), self) - self.aRefresh.setIcon(self.theTheme.getIcon("refresh")) + self.aRefresh.setIcon(self.mainTheme.getIcon("refresh")) self.aRefresh.triggered.connect(self._refreshRequested) # Column Menu @@ -212,7 +212,7 @@ class GuiOutlineToolBar(QToolBar): ) self.tbColumns = QToolButton(self) - self.tbColumns.setIcon(self.theTheme.getIcon("menu")) + self.tbColumns.setIcon(self.mainTheme.getIcon("menu")) self.tbColumns.setMenu(self.mColumns) self.tbColumns.setPopupMode(QToolButton.InstantPopup) @@ -236,7 +236,7 @@ class GuiOutlineToolBar(QToolBar): """Fill the novel combo box with a list of all novel folders. """ self.novelValue.clear() - tIcon = self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) + tIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL): self.novelValue.addItem(tIcon, nwItem.itemName, tHandle) self.novelValue.insertSeparator(self.novelValue.count()) @@ -324,7 +324,7 @@ class GuiOutlineTree(QTreeWidget): self.mainConf = novelwriter.CONFIG self.mainGui = theOutline.mainGui self.theProject = theOutline.mainGui.theProject - self.theTheme = theOutline.mainGui.theTheme + self.mainTheme = theOutline.mainGui.mainTheme self.setFrameStyle(QFrame.NoFrame) self.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -334,7 +334,7 @@ class GuiOutlineTree(QTreeWidget): self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._itemSelected) - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize self.setIconSize(QSize(iPx, iPx)) self.setIndentation(iPx) @@ -682,7 +682,7 @@ class GuiOutlineTree(QTreeWidget): hIcon = "doc_%s" % novIdx.level.lower() hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) - dIcon = self.theTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel) + dIcon = self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel) cC = int(novIdx.charCount) wC = int(novIdx.wordCount) @@ -690,7 +690,7 @@ class GuiOutlineTree(QTreeWidget): newItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title) newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle) - newItem.setIcon(self._colIdx[nwOutline.TITLE], self.theTheme.getIcon(hIcon)) + newItem.setIcon(self._colIdx[nwOutline.TITLE], self.mainTheme.getIcon(hIcon)) newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level) newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName) newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon) @@ -784,12 +784,12 @@ class GuiOutlineDetails(QScrollArea): self.theOutline = theOutline self.mainGui = theOutline.mainGui self.theProject = theOutline.mainGui.theProject - self.theTheme = theOutline.mainGui.theTheme + self.mainTheme = theOutline.mainGui.mainTheme # Sizes - minTitle = 30*self.theTheme.textNWidth - maxTitle = 40*self.theTheme.textNWidth - wCount = self.theTheme.getTextWidth("999,999") + minTitle = 30*self.mainTheme.textNWidth + maxTitle = 40*self.mainTheme.textNWidth + wCount = self.mainTheme.getTextWidth("999,999") hSpace = int(self.mainConf.pxInt(10)) vSpace = int(self.mainConf.pxInt(4)) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index f2a7b2b3..4c0ea59b 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -169,9 +169,9 @@ class GuiProjectToolBar(QWidget): self.projTree = projView.projTree self.mainGui = projView.mainGui self.theProject = projView.mainGui.theProject - self.theTheme = projView.mainGui.theTheme + self.mainTheme = projView.mainGui.mainTheme - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize mPx = self.mainConf.pxInt(4) self.setContentsMargins(0, 0, 0, 0) @@ -195,14 +195,14 @@ class GuiProjectToolBar(QWidget): # Move Buttons self.tbMoveU = QToolButton(self) self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up")) - self.tbMoveU.setIcon(self.theTheme.getIcon("up")) + self.tbMoveU.setIcon(self.mainTheme.getIcon("up")) self.tbMoveU.setIconSize(QSize(iPx, iPx)) self.tbMoveU.setStyleSheet(buttonStyle) self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1)) self.tbMoveD = QToolButton(self) self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) - self.tbMoveD.setIcon(self.theTheme.getIcon("down")) + self.tbMoveD.setIcon(self.mainTheme.getIcon("down")) self.tbMoveD.setIconSize(QSize(iPx, iPx)) self.tbMoveD.setStyleSheet(buttonStyle) self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1)) @@ -211,31 +211,31 @@ class GuiProjectToolBar(QWidget): self.mAdd = QMenu() self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"])) - self.aAddEmpty.setIcon(self.theTheme.getIcon("proj_document")) + self.aAddEmpty.setIcon(self.mainTheme.getIcon("proj_document")) self.aAddEmpty.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) ) self.aAddChap = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"])) - self.aAddChap.setIcon(self.theTheme.getIcon("proj_chapter")) + self.aAddChap.setIcon(self.mainTheme.getIcon("proj_chapter")) self.aAddChap.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) ) self.aAddScene = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"])) - self.aAddScene.setIcon(self.theTheme.getIcon("proj_scene")) + self.aAddScene.setIcon(self.mainTheme.getIcon("proj_scene")) self.aAddScene.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) ) self.aAddNote = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["note"])) - self.aAddNote.setIcon(self.theTheme.getIcon("proj_note")) + self.aAddNote.setIcon(self.mainTheme.getIcon("proj_note")) self.aAddNote.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) ) self.aAddFolder = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["folder"])) - self.aAddFolder.setIcon(self.theTheme.getIcon("proj_folder")) + self.aAddFolder.setIcon(self.mainTheme.getIcon("proj_folder")) self.aAddFolder.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FOLDER) ) @@ -255,7 +255,7 @@ class GuiProjectToolBar(QWidget): self.tbAdd = QToolButton(self) self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) self.tbAdd.setShortcut("Ctrl+N") - self.tbAdd.setIcon(self.theTheme.getIcon("add")) + self.tbAdd.setIcon(self.mainTheme.getIcon("add")) self.tbAdd.setIconSize(QSize(iPx, iPx)) self.tbAdd.setStyleSheet(buttonStyle) self.tbAdd.setMenu(self.mAdd) @@ -272,7 +272,7 @@ class GuiProjectToolBar(QWidget): self.tbMore = QToolButton(self) self.tbMore.setToolTip(self.tr("More Options")) - self.tbMore.setIcon(self.theTheme.getIcon("menu")) + self.tbMore.setIcon(self.mainTheme.getIcon("menu")) self.tbMore.setIconSize(QSize(iPx, iPx)) self.tbMore.setStyleSheet(buttonStyle) self.tbMore.setMenu(self.mMore) @@ -302,7 +302,7 @@ class GuiProjectToolBar(QWidget): """Add a menu entry for a root folder of a given class. """ aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) - aNew.setIcon(self.theTheme.getIcon(nwLabels.CLASS_ICON[itemClass])) + aNew.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[itemClass])) aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass)) self.mAddRoot.addAction(aNew) @@ -324,7 +324,7 @@ class GuiProjectTree(QTreeWidget): self.mainConf = novelwriter.CONFIG self.projView = projView self.mainGui = projView.mainGui - self.theTheme = projView.mainGui.theTheme + self.mainTheme = projView.mainGui.mainTheme self.theProject = projView.mainGui.theProject # Internal Variables @@ -341,7 +341,7 @@ class GuiProjectTree(QTreeWidget): self.customContextMenuRequested.connect(self._openContextMenu) # Tree Settings - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize cMg = self.mainConf.pxInt(6) self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) @@ -779,13 +779,13 @@ class GuiProjectTree(QTreeWidget): expIcon = QIcon() if nwItem.itemType == nwItemType.FILE: if nwItem.isExported: - expIcon = self.theTheme.getIcon("check") + expIcon = self.mainTheme.getIcon("check") else: - expIcon = self.theTheme.getIcon("cross") + expIcon = self.mainTheme.getIcon("cross") itemStatus, statusIcon = nwItem.getImportStatus() hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) - itemIcon = self.theTheme.getItemIcon( + itemIcon = self.mainTheme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel ) diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 4863f9bc..a0cf9416 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -48,15 +48,15 @@ class GuiMainStatus(QStatusBar): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.refTime = None self.userIdle = False - colNone = QColor(*self.theTheme.statNone) - colTrue = QColor(*self.theTheme.statUnsaved) - colFalse = QColor(*self.theTheme.statSaved) + colNone = QColor(*self.mainTheme.statNone) + colTrue = QColor(*self.mainTheme.statUnsaved) + colFalse = QColor(*self.mainTheme.statSaved) - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize # Permanent Widgets # ================= @@ -66,7 +66,7 @@ class GuiMainStatus(QStatusBar): # The Spell Checker Language self.langIcon = QLabel("") self.langText = QLabel(self.tr("None")) - self.langIcon.setPixmap(self.theTheme.getPixmap("status_lang", (iPx, iPx))) + self.langIcon.setPixmap(self.mainTheme.getPixmap("status_lang", (iPx, iPx))) self.langIcon.setContentsMargins(0, 0, 0, 0) self.langText.setContentsMargins(0, 0, xM, 0) self.addPermanentWidget(self.langIcon) @@ -91,7 +91,7 @@ class GuiMainStatus(QStatusBar): # The Project and Session Stats self.statsIcon = QLabel() self.statsText = QLabel("") - self.statsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (iPx, iPx))) + self.statsIcon.setPixmap(self.mainTheme.getPixmap("status_stats", (iPx, iPx))) self.statsIcon.setContentsMargins(0, 0, 0, 0) self.statsText.setContentsMargins(0, 0, xM, 0) self.addPermanentWidget(self.statsIcon) @@ -99,14 +99,14 @@ class GuiMainStatus(QStatusBar): # The Session Clock # Set the mimimum width so the label doesn't rescale every second - self.timePixmap = self.theTheme.getPixmap("status_time", (iPx, iPx)) - self.idlePixmap = self.theTheme.getPixmap("status_idle", (iPx, iPx)) + self.timePixmap = self.mainTheme.getPixmap("status_time", (iPx, iPx)) + self.idlePixmap = self.mainTheme.getPixmap("status_idle", (iPx, iPx)) self.timeIcon = QLabel() self.timeText = QLabel("") self.timeIcon.setPixmap(self.timePixmap) self.timeText.setToolTip(self.tr("Session Time")) - self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:")) + self.timeText.setMinimumWidth(self.mainTheme.getTextWidth("00:00:00:")) self.timeIcon.setContentsMargins(0, 0, 0, 0) self.timeText.setContentsMargins(0, 0, 0, 0) self.addPermanentWidget(self.timeIcon) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index bb93e0b8..3a59e7e2 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -54,7 +54,7 @@ class GuiTheme: def __init__(self): self.mainConf = novelwriter.CONFIG - self.theIcons = GuiIcons(self) + self.iconCache = GuiIcons(self) # Loaded Theme Settings # ===================== @@ -127,13 +127,13 @@ class GuiTheme: self.updateFont() self.updateTheme() - self.theIcons.updateTheme() + self.iconCache.updateTheme() # Icon Functions - self.getIcon = self.theIcons.getIcon - self.getPixmap = self.theIcons.getPixmap - self.getItemIcon = self.theIcons.getItemIcon - self.loadDecoration = self.theIcons.loadDecoration + self.getIcon = self.iconCache.getIcon + self.getPixmap = self.iconCache.getPixmap + self.getItemIcon = self.iconCache.getItemIcon + self.loadDecoration = self.iconCache.loadDecoration # Extract Other Info self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX() @@ -478,10 +478,10 @@ class GuiIcons: "wiz-back": "wizard-back.jpg", } - def __init__(self, theTheme): + def __init__(self, mainTheme): self.mainConf = novelwriter.CONFIG - self.theTheme = theTheme + self.mainTheme = mainTheme # Storage self._qIcons = {} diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py index d62b5543..031f4316 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/viewsbar.py @@ -45,16 +45,16 @@ class GuiViewsBar(QToolBar): logger.debug("Initialising GuiViewsBar ...") - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # Style iPx = self.mainConf.pxInt(22) mPx = self.mainConf.pxInt(60) - lblFont = self.theTheme.guiFont - lblFont.setPointSizeF(0.65*self.theTheme.fontPointSize) + lblFont = self.mainTheme.guiFont + lblFont.setPointSizeF(0.65*self.mainTheme.fontPointSize) self.setMovable(False) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) @@ -70,37 +70,37 @@ class GuiViewsBar(QToolBar): self.aProject = QAction(self.tr("Project")) self.aProject.setFont(lblFont) self.aProject.setToolTip(self.tr("Show project tree and editor")) - self.aProject.setIcon(self.theTheme.getIcon("view_editor")) + self.aProject.setIcon(self.mainTheme.getIcon("view_editor")) self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) self.aNovel = QAction(self.tr("Novel")) self.aNovel.setFont(lblFont) self.aNovel.setToolTip(self.tr("Show novel tree and editor")) - self.aNovel.setIcon(self.theTheme.getIcon("view_novel")) + self.aNovel.setIcon(self.mainTheme.getIcon("view_novel")) self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) self.aOutline = QAction(self.tr("Outline")) self.aOutline.setFont(lblFont) self.aOutline.setToolTip(self.tr("Show novel outline")) - self.aOutline.setIcon(self.theTheme.getIcon("view_outline")) + self.aOutline.setIcon(self.mainTheme.getIcon("view_outline")) self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) self.aBuild = QAction(self.tr("Build")) self.aBuild.setFont(lblFont) self.aBuild.setToolTip(self.tr("Build novel project")) - self.aBuild.setIcon(self.theTheme.getIcon("view_build")) + self.aBuild.setIcon(self.mainTheme.getIcon("view_build")) self.aBuild.triggered.connect(lambda: self.mainGui.showBuildProjectDialog()) self.aDetails = QAction(self.tr("Details")) self.aDetails.setFont(lblFont) self.aDetails.setToolTip(self.tr("Show project details")) - self.aDetails.setIcon(self.theTheme.getIcon("proj_details")) + self.aDetails.setIcon(self.mainTheme.getIcon("proj_details")) self.aDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog()) self.aStats = QAction(self.tr("Stats")) self.aStats.setFont(lblFont) self.aStats.setToolTip(self.tr("Show project statistics")) - self.aStats.setIcon(self.theTheme.getIcon("proj_stats")) + self.aStats.setIcon(self.mainTheme.getIcon("proj_stats")) self.aStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog()) # Settings Menu @@ -117,7 +117,7 @@ class GuiViewsBar(QToolBar): self.tbSettings = QToolButton(self) self.tbSettings.setFont(lblFont) self.tbSettings.setText(self.tr("Settings")) - self.tbSettings.setIcon(self.theTheme.getIcon("settings")) + self.tbSettings.setIcon(self.mainTheme.getIcon("settings")) self.tbSettings.setMenu(self.mSettings) self.tbSettings.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.tbSettings.setPopupMode(QToolButton.InstantPopup) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8c0e0e51..6b315866 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -84,7 +84,7 @@ class GuiMain(QMainWindow): # ============ # Core Classes and Settings - self.theTheme = GuiTheme() + self.mainTheme = GuiTheme() self.theProject = NWProject(self) self.hasProject = False self.isFocusMode = False @@ -915,7 +915,7 @@ class GuiMain(QMainWindow): if dlgConf.result() == QDialog.Accepted: logger.debug("Applying new preferences") self.initMain() - self.theTheme.updateTheme() + self.mainTheme.updateTheme() self.saveDocument() self.docEditor.initEditor() self.docViewer.initViewer() diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 4ce9dea1..efeff928 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -73,7 +73,7 @@ class GuiBuildNovel(QDialog): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.theProject = mainGui.theProject self.htmlText = [] # List of html documents @@ -93,7 +93,7 @@ class GuiBuildNovel(QDialog): self.docView = GuiBuildNovelDocView(self, self.theProject) - hS = self.theTheme.fontPixelSize + hS = self.mainTheme.fontPixelSize wS = 2*hS # Title Formats @@ -238,11 +238,11 @@ class GuiBuildNovel(QDialog): pOptions.getString("GuiBuildNovel", "textFont", self.mainConf.textFont) ) self.fontButton = QPushButton("...") - self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.fontButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.textSize = QSpinBox(self) - self.textSize.setFixedWidth(6*self.theTheme.textNWidth) + self.textSize.setFixedWidth(6*self.mainTheme.textNWidth) self.textSize.setMinimum(6) self.textSize.setMaximum(72) self.textSize.setSingleStep(1) @@ -251,7 +251,7 @@ class GuiBuildNovel(QDialog): ) self.lineHeight = QDoubleSpinBox(self) - self.lineHeight.setFixedWidth(6*self.theTheme.textNWidth) + self.lineHeight.setFixedWidth(6*self.mainTheme.textNWidth) self.lineHeight.setMinimum(0.8) self.lineHeight.setMaximum(3.0) self.lineHeight.setSingleStep(0.05) @@ -1201,10 +1201,10 @@ class GuiBuildNovelDocView(QTextBrowser): self.mainConf = novelwriter.CONFIG self.theProject = theProject self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.buildTime = 0 - self.setMinimumWidth(40*self.mainGui.theTheme.textNWidth) + self.setMinimumWidth(40*self.mainGui.mainTheme.textNWidth) self.setOpenExternalLinks(False) self.document().setDocumentMargin(self.mainConf.getTextMargin()) @@ -1238,9 +1238,9 @@ class GuiBuildNovelDocView(QTextBrowser): lblPalette.setColor(QPalette.Foreground, lblPalette.toolTipText().color()) lblFont = self.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) - fPx = int(1.1*self.theTheme.fontPixelSize) + fPx = int(1.1*self.mainTheme.fontPixelSize) self.theTitle = QLabel("", self) self.theTitle.setIndent(0) diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index d741058c..2086d615 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -48,9 +48,9 @@ class GuiLipsum(QDialog): logger.debug("Initialising GuiLipsum ...") self.setObjectName("GuiLipsum") - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme self.setWindowTitle(self.tr("Insert Placeholder Text")) @@ -61,7 +61,7 @@ class GuiLipsum(QDialog): nPx = self.mainConf.pxInt(64) vSp = self.mainConf.pxInt(4) self.docIcon = QLabel() - self.docIcon.setPixmap(self.mainGui.theTheme.getPixmap("proj_document", (nPx, nPx))) + self.docIcon.setPixmap(self.mainTheme.getPixmap("proj_document", (nPx, nPx))) self.leftBox = QVBoxLayout() self.leftBox.setSpacing(vSp) diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index 3dac9ed8..5b21f69a 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -54,11 +54,11 @@ class GuiProjectWizard(QWizard): logger.debug("Initialising GuiProjectWizard ...") self.setObjectName("GuiProjectWizard") - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme - self.sideImage = self.theTheme.loadDecoration( + self.sideImage = self.mainTheme.loadDecoration( "wiz-back", None, self.mainConf.pxInt(370) ) self.setWizardStyle(QWizard.ModernStyle) @@ -92,7 +92,7 @@ class ProjWizardIntroPage(QWizardPage): self.mainConf = novelwriter.CONFIG self.theWizard = theWizard - self.theTheme = theWizard.theTheme + self.mainTheme = theWizard.mainTheme self.setTitle(self.tr("Create New Project")) self.theText = QLabel(self.tr( @@ -107,7 +107,7 @@ class ProjWizardIntroPage(QWizardPage): "Peter Mitterhofer", "CC BY-SA 4.0" )) lblFont = self.imgCredit.font() - lblFont.setPointSizeF(0.6*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.6*self.mainTheme.fontPointSize) self.imgCredit.setFont(lblFont) xW = self.mainConf.pxInt(300) @@ -162,7 +162,7 @@ class ProjWizardFolderPage(QWizardPage): self.mainConf = novelwriter.CONFIG self.theWizard = theWizard - self.theTheme = theWizard.theTheme + self.mainTheme = theWizard.mainTheme self.setTitle(self.tr("Select Project Folder")) self.theText = QLabel(self.tr( @@ -180,7 +180,7 @@ class ProjWizardFolderPage(QWizardPage): self.projPath.setPlaceholderText(self.tr("Required")) self.browseButton = QPushButton("...") - self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.browseButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.browseButton.clicked.connect(self._doBrowse) self.errLabel = QLabel("") diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index d4527612..add052ec 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -65,7 +65,7 @@ class GuiWritingStats(QDialog): self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self.theTheme = mainGui.theTheme + self.mainTheme = mainGui.mainTheme self.theProject = mainGui.theProject self.logData = [] @@ -125,7 +125,7 @@ class GuiWritingStats(QDialog): self.listBox.setSortingEnabled(True) # Word Bar - self.barHeight = int(round(0.5*self.theTheme.fontPixelSize)) + self.barHeight = int(round(0.5*self.mainTheme.fontPixelSize)) self.barWidth = self.mainConf.pxInt(200) self.barImage = QPixmap(self.barHeight, self.barHeight) self.barImage.fill(self.palette().highlight().color()) @@ -136,27 +136,27 @@ class GuiWritingStats(QDialog): self.infoBox.setLayout(self.infoForm) self.labelTotal = QLabel(formatTime(0)) - self.labelTotal.setFont(self.theTheme.guiFontFixed) + self.labelTotal.setFont(self.mainTheme.guiFontFixed) self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelIdleT = QLabel(formatTime(0)) - self.labelIdleT.setFont(self.theTheme.guiFontFixed) + self.labelIdleT.setFont(self.mainTheme.guiFontFixed) self.labelIdleT.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelFilter = QLabel(formatTime(0)) - self.labelFilter.setFont(self.theTheme.guiFontFixed) + self.labelFilter.setFont(self.mainTheme.guiFontFixed) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.novelWords = QLabel("0") - self.novelWords.setFont(self.theTheme.guiFontFixed) + self.novelWords.setFont(self.mainTheme.guiFontFixed) self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.notesWords = QLabel("0") - self.notesWords.setFont(self.theTheme.guiFontFixed) + self.notesWords.setFont(self.mainTheme.guiFontFixed) self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.totalWords = QLabel("0") - self.totalWords.setFont(self.theTheme.guiFontFixed) + self.totalWords.setFont(self.mainTheme.guiFontFixed) self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) lblTTime = QLabel(self.tr("Total Time:")) @@ -183,7 +183,7 @@ class GuiWritingStats(QDialog): self.infoForm.setRowStretch(6, 1) # Filter Options - sPx = self.theTheme.baseIconSize + sPx = self.mainTheme.baseIconSize self.filterBox = QGroupBox(self.tr("Filters"), self) self.filterForm = QGridLayout(self) @@ -605,13 +605,13 @@ class GuiWritingStats(QDialog): newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter) - newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed) - newItem.setFont(self.C_LENGTH, self.theTheme.guiFontFixed) - newItem.setFont(self.C_COUNT, self.theTheme.guiFontFixed) + newItem.setFont(self.C_TIME, self.mainTheme.guiFontFixed) + newItem.setFont(self.C_LENGTH, self.mainTheme.guiFontFixed) + newItem.setFont(self.C_COUNT, self.mainTheme.guiFontFixed) if showIdleTime: - newItem.setFont(self.C_IDLE, self.theTheme.guiFontFixed) + newItem.setFont(self.C_IDLE, self.mainTheme.guiFontFixed) else: - newItem.setFont(self.C_IDLE, self.theTheme.guiFont) + newItem.setFont(self.C_IDLE, self.mainTheme.guiFont) self.listBox.addTopLevelItem(newItem) self.timeFilter += sDiff diff --git a/tests/test_dialogs/test_dlg_about.py b/tests/test_dialogs/test_dlg_about.py index c5ccfe7f..a4623c1f 100644 --- a/tests/test_dialogs/test_dlg_about.py +++ b/tests/test_dialogs/test_dlg_about.py @@ -36,8 +36,8 @@ def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI): monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) # NW About - nwGUI.theTheme.themeName = "A Theme" - nwGUI.theTheme.themeAuthor = "An Author" + nwGUI.mainTheme.themeName = "A Theme" + nwGUI.mainTheme.themeAuthor = "An Author" assert nwGUI.showAboutNWDialog(showNotes=True) is True qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index da7fc414..756b2a81 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -93,82 +93,82 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir): assert thePalette.link().color() == QColor(44, 152, 247) assert thePalette.linkVisited().color() == QColor(44, 152, 247) - assert nwGUI.theTheme.statNone == [150, 152, 150] - assert nwGUI.theTheme.statSaved == [39, 135, 78] - assert nwGUI.theTheme.statUnsaved == [138, 32, 32] + assert nwGUI.mainTheme.statNone == [150, 152, 150] + assert nwGUI.mainTheme.statSaved == [39, 135, 78] + assert nwGUI.mainTheme.statUnsaved == [138, 32, 32] # Check Syntax Colours - assert nwGUI.theTheme.colBack == [45, 45, 45] - assert nwGUI.theTheme.colText == [204, 204, 204] - assert nwGUI.theTheme.colLink == [102, 153, 204] - assert nwGUI.theTheme.colHead == [102, 153, 204] - assert nwGUI.theTheme.colHeadH == [102, 153, 204] - assert nwGUI.theTheme.colEmph == [249, 145, 57] - assert nwGUI.theTheme.colDialN == [242, 119, 122] - assert nwGUI.theTheme.colDialD == [153, 204, 153] - assert nwGUI.theTheme.colDialS == [255, 204, 102] - assert nwGUI.theTheme.colHidden == [153, 153, 153] - assert nwGUI.theTheme.colKey == [242, 119, 122] - assert nwGUI.theTheme.colVal == [204, 153, 204] - assert nwGUI.theTheme.colSpell == [242, 119, 122] - assert nwGUI.theTheme.colError == [153, 204, 153] - assert nwGUI.theTheme.colRepTag == [102, 204, 204] - assert nwGUI.theTheme.colMod == [249, 145, 57] + assert nwGUI.mainTheme.colBack == [45, 45, 45] + assert nwGUI.mainTheme.colText == [204, 204, 204] + assert nwGUI.mainTheme.colLink == [102, 153, 204] + assert nwGUI.mainTheme.colHead == [102, 153, 204] + assert nwGUI.mainTheme.colHeadH == [102, 153, 204] + assert nwGUI.mainTheme.colEmph == [249, 145, 57] + assert nwGUI.mainTheme.colDialN == [242, 119, 122] + assert nwGUI.mainTheme.colDialD == [153, 204, 153] + assert nwGUI.mainTheme.colDialS == [255, 204, 102] + assert nwGUI.mainTheme.colHidden == [153, 153, 153] + assert nwGUI.mainTheme.colKey == [242, 119, 122] + assert nwGUI.mainTheme.colVal == [204, 153, 204] + assert nwGUI.mainTheme.colSpell == [242, 119, 122] + assert nwGUI.mainTheme.colError == [153, 204, 153] + assert nwGUI.mainTheme.colRepTag == [102, 204, 204] + assert nwGUI.mainTheme.colMod == [249, 145, 57] # Test Icon class - theIcons = nwGUI.theTheme.theIcons + iconCache = nwGUI.mainTheme.iconCache novelwriter.CONFIG.guiIcons = "invalid" - assert theIcons.updateTheme() is True + assert iconCache.updateTheme() is True assert novelwriter.CONFIG.guiIcons == "typicons_light" # Ask for a non-existent key - anImg = theIcons.loadDecoration("nonsense", 20, 20) + anImg = iconCache.loadDecoration("nonsense", 20, 20) assert isinstance(anImg, QPixmap) assert anImg.isNull() # Add a non-existent file and request it - theIcons.DECO_MAP["nonsense"] = "nofile.jpg" - anImg = theIcons.loadDecoration("nonsense", 20, 20) + iconCache.DECO_MAP["nonsense"] = "nofile.jpg" + anImg = iconCache.loadDecoration("nonsense", 20, 20) assert isinstance(anImg, QPixmap) assert anImg.isNull() # Get a real image, with different size parameters - anImg = theIcons.loadDecoration("wiz-back", 20, None) + anImg = iconCache.loadDecoration("wiz-back", 20, None) assert isinstance(anImg, QPixmap) assert not anImg.isNull() assert anImg.width() == 20 assert anImg.height() >= 56 - anImg = theIcons.loadDecoration("wiz-back", None, 70) + anImg = iconCache.loadDecoration("wiz-back", None, 70) assert isinstance(anImg, QPixmap) assert not anImg.isNull() assert anImg.height() == 70 assert anImg.width() >= 24 - anImg = theIcons.loadDecoration("wiz-back", 30, 70) + anImg = iconCache.loadDecoration("wiz-back", 30, 70) assert isinstance(anImg, QPixmap) assert not anImg.isNull() assert anImg.height() == 70 assert anImg.width() == 30 - anImg = theIcons.loadDecoration("wiz-back", None, None) + anImg = iconCache.loadDecoration("wiz-back", None, None) assert isinstance(anImg, QPixmap) assert not anImg.isNull() assert anImg.height() >= 1500 assert anImg.width() >= 500 # Load icons - anIcon = theIcons.getIcon("nonsense") + anIcon = iconCache.getIcon("nonsense") assert isinstance(anIcon, QIcon) assert anIcon.isNull() - anIcon = theIcons.getIcon("novelwriter") + anIcon = iconCache.getIcon("novelwriter") assert isinstance(anIcon, QIcon) assert not anIcon.isNull() # Check return empty icon if file not found - theIcons.ICON_KEYS.add("testicon3") - anIcon = theIcons.getIcon("testicon3") + iconCache.ICON_KEYS.add("testicon3") + anIcon = iconCache.getIcon("testicon3") assert isinstance(anIcon, QIcon) assert anIcon.isNull() From 60584239701997ef268e6dae0ce6e1d78b33ce5d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 18:17:57 +0200 Subject: [PATCH 138/179] Add a new edit label dialog --- novelwriter/dialogs/editlabel.py | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 novelwriter/dialogs/editlabel.py diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py new file mode 100644 index 00000000..03bf9d08 --- /dev/null +++ b/novelwriter/dialogs/editlabel.py @@ -0,0 +1,84 @@ +""" +novelWriter – Edit Label Dialog +=============================== +A simple dialog for editing a label + +File History: +Created: 2022-06-11 [1.7b1] + +This file is a part of novelWriter +Copyright 2018–2022, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import logging +import novelwriter + +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QLineEdit, QLabel, QDialogButtonBox, QHBoxLayout +) + +logger = logging.getLogger(__name__) + + +class GuiEditLabel(QDialog): + + def __init__(self, parent, text=""): + QDialog.__init__(self, parent=parent) + + self.setObjectName("GuiEditLabel") + self.setWindowTitle(self.tr("Item Label")) + + mVd = novelwriter.CONFIG.pxInt(220) + mSp = novelwriter.CONFIG.pxInt(12) + + # Item Label + self.labelValue = QLineEdit() + self.labelValue.setMinimumWidth(mVd) + self.labelValue.setMaxLength(200) + self.labelValue.setText(text) + self.labelValue.selectAll() + + # Buttons + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + # Assemble + self.innerBox = QHBoxLayout() + self.innerBox.addWidget(QLabel(self.tr("Label")), 0) + self.innerBox.addWidget(self.labelValue, 1) + self.innerBox.setSpacing(mSp) + + self.outerBox = QVBoxLayout() + self.outerBox.setSpacing(mSp) + self.outerBox.addLayout(self.innerBox, 1) + self.outerBox.addWidget(self.buttonBox, 0) + + self.setLayout(self.outerBox) + + return + + @property + def itemLabel(self): + return self.labelValue.text() + + @classmethod + def getLabel(cls, parent, text): + cls = GuiEditLabel(parent, text=text) + cls.exec_() + return cls.itemLabel, cls.result() == QDialog.Accepted + +# END Class GuiEditLabel From 37c3bb8cd229ffcea076ee3e9df17bdda5737cc7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 18:19:21 +0200 Subject: [PATCH 139/179] Replace the item editor with the label editor dialog --- novelwriter/dialogs/__init__.py | 4 +- novelwriter/dialogs/itemeditor.py | 203 ------------------ novelwriter/gui/mainmenu.py | 4 +- novelwriter/gui/projtree.py | 39 ++-- novelwriter/guimain.py | 2 +- tests/test_dialogs/test_dlg_docmerge.py | 7 +- tests/test_dialogs/test_dlg_docsplit.py | 7 +- tests/test_dialogs/test_dlg_itemeditor.py | 246 ---------------------- tests/test_gui/test_gui_guimain.py | 7 +- tests/test_gui/test_gui_projtree.py | 15 +- 10 files changed, 40 insertions(+), 494 deletions(-) delete mode 100644 novelwriter/dialogs/itemeditor.py delete mode 100644 tests/test_dialogs/test_dlg_itemeditor.py diff --git a/novelwriter/dialogs/__init__.py b/novelwriter/dialogs/__init__.py index 30bd5d82..03efef5c 100644 --- a/novelwriter/dialogs/__init__.py +++ b/novelwriter/dialogs/__init__.py @@ -22,7 +22,7 @@ along with this program. If not, see . from novelwriter.dialogs.about import GuiAbout from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docsplit import GuiDocSplit -from novelwriter.dialogs.itemeditor import GuiItemEditor +from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.projdetails import GuiProjectDetails from novelwriter.dialogs.projload import GuiProjectLoad @@ -35,7 +35,7 @@ __all__ = [ "GuiAbout", "GuiDocMerge", "GuiDocSplit", - "GuiItemEditor", + "GuiEditLabel", "GuiPreferences", "GuiProjectDetails", "GuiProjectLoad", diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py deleted file mode 100644 index 7ad4eed0..00000000 --- a/novelwriter/dialogs/itemeditor.py +++ /dev/null @@ -1,203 +0,0 @@ -""" -novelWriter – GUI Item Editor -============================= -GUI class for the item editor dialog - -File History: -Created: 2019-04-27 [0.0.1] - -This file is a part of novelWriter -Copyright 2018–2022, Veronica Berglyd Olsen - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, but -WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -import logging -import novelwriter - -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QGridLayout, QLineEdit, QComboBox, QLabel, - QDialogButtonBox -) - -from novelwriter.enum import nwItemLayout, nwItemType -from novelwriter.constants import trConst, nwLabels -from novelwriter.gui.custom import QSwitch - -logger = logging.getLogger(__name__) - - -class GuiItemEditor(QDialog): - - def __init__(self, mainGui, tHandle): - QDialog.__init__(self, mainGui) - - logger.debug("Initialising GuiItemEditor ...") - self.setObjectName("GuiItemEditor") - - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.theProject = mainGui.theProject - - ## - # Build GUI - ## - - self.theItem = self.theProject.tree[tHandle] - if self.theItem is None: - self.close() - return - - self.setWindowTitle(self.tr("Item Settings")) - - mVd = self.mainConf.pxInt(220) - mSp = self.mainConf.pxInt(16) - vSp = self.mainConf.pxInt(4) - - # Item Label - self.editName = QLineEdit() - self.editName.setMinimumWidth(mVd) - self.editName.setMaxLength(200) - - # Item Status - self.editStatus = QComboBox() - self.editStatus.setMinimumWidth(mVd) - if self.theItem.isNovelLike(): - for key, entry in self.theProject.statusItems.items(): - self.editStatus.addItem(entry["icon"], entry["name"], key) - - index = self.editStatus.findData(self.theItem.itemStatus) - if index != -1: - self.editStatus.setCurrentIndex(index) - - else: - for key, entry in self.theProject.importItems.items(): - self.editStatus.addItem(entry["icon"], entry["name"], key) - - index = self.editStatus.findData(self.theItem.itemImport) - if index != -1: - self.editStatus.setCurrentIndex(index) - - # Item Layout - self.editLayout = QComboBox() - self.editLayout.setMinimumWidth(mVd) - validLayouts = [] - if self.theItem.itemType == nwItemType.FILE: - if self.theItem.documentAllowed(): - validLayouts.append(nwItemLayout.DOCUMENT) - validLayouts.append(nwItemLayout.NOTE) - else: - validLayouts.append(nwItemLayout.NO_LAYOUT) - self.editLayout.setEnabled(False) - - for itemLayout in nwItemLayout: - if itemLayout in validLayouts: - self.editLayout.addItem(trConst(nwLabels.LAYOUT_NAME[itemLayout]), itemLayout) - - index = self.editLayout.findData(self.theItem.itemLayout) - if index != -1: - self.editLayout.setCurrentIndex(index) - - # Export Switch - self.textExport = QLabel(self.tr("Include when building project")) - self.editExport = QSwitch() - if self.theItem.itemType == nwItemType.FILE: - self.editExport.setEnabled(True) - self.editExport.setChecked(self.theItem.isExported) - else: - self.editExport.setEnabled(False) - self.editExport.setChecked(False) - - # Buttons - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self._doClose) - - # Set Current Values - self.editName.setText(self.theItem.itemName) - self.editName.selectAll() - - ## - # Assemble - ## - - nameLabel = QLabel(self.tr("Label")) - statusLabel = QLabel(self.tr("Status")) - layoutLabel = QLabel(self.tr("Layout")) - - self.mainForm = QGridLayout() - self.mainForm.setVerticalSpacing(vSp) - self.mainForm.setHorizontalSpacing(mSp) - self.mainForm.addWidget(nameLabel, 0, 0, 1, 1) - self.mainForm.addWidget(self.editName, 0, 1, 1, 2) - self.mainForm.addWidget(statusLabel, 1, 0, 1, 1) - self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2) - self.mainForm.addWidget(layoutLabel, 2, 0, 1, 1) - self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2) - self.mainForm.addWidget(self.textExport, 3, 0, 1, 2) - self.mainForm.addWidget(self.editExport, 3, 2, 1, 1) - self.mainForm.setColumnStretch(0, 0) - self.mainForm.setColumnStretch(1, 1) - self.mainForm.setColumnStretch(2, 0) - - self.outerBox = QVBoxLayout() - self.outerBox.setSpacing(mSp) - self.outerBox.addLayout(self.mainForm) - self.outerBox.addStretch(1) - self.outerBox.addWidget(self.buttonBox) - self.setLayout(self.outerBox) - - self.rejected.connect(self._doClose) - - logger.debug("GuiItemEditor initialisation complete") - - return - - ## - # Slots - ## - - @pyqtSlot() - def _doSave(self): - """Save the setting to the item. - """ - logger.verbose("ItemEditor save button clicked") - - itemName = self.editName.text() - itemStatus = self.editStatus.currentData() - itemLayout = self.editLayout.currentData() - isExported = self.editExport.isChecked() - - self.theItem.setName(itemName) - self.theItem.setImportStatus(itemStatus) - self.theItem.setLayout(itemLayout) - self.theItem.setExported(isExported) - - self.theProject.setProjectChanged(True) - - self.accept() - self.close() - - return - - @pyqtSlot() - def _doClose(self): - """Close the dialog without saving the settings. - """ - logger.verbose("ItemEditor cancel button clicked") - self.close() - return - -# END Class GuiItemEditor diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index e7203bb9..85920749 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -163,8 +163,8 @@ class GuiMainMenu(QMenuBar): self.projMenu.addSeparator() # Project > Edit - self.aEditItem = QAction(self.tr("Edit Item"), self) - self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) + self.aEditItem = QAction(self.tr("Rename Item"), self) + self.aEditItem.setShortcuts(["F2"]) self.aEditItem.triggered.connect(lambda: self.mainGui.editItem(None)) self.projMenu.addAction(self.aEditItem) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 4c0ea59b..aa1ff9ce 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -34,15 +34,15 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( - QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QInputDialog, - QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, - QTreeWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel, + QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, + QVBoxLayout, QWidget ) from novelwriter.core import NWDoc from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.dialogs.itemeditor import GuiItemEditor from novelwriter.constants import trConst, nwLabels +from novelwriter.dialogs.editlabel import GuiEditLabel logger = logging.getLogger(__name__) @@ -99,7 +99,7 @@ class GuiProjectView(QWidget): # Function Mappings self.revealNewTreeItem = self.projTree.revealNewTreeItem - self.editTreeItem = self.projTree.editTreeItem + self.renameTreeItem = self.projTree.renameTreeItem self.getTreeFromHandle = self.projTree.getTreeFromHandle self.emptyTrash = self.projTree.emptyTrash self.deleteItem = self.projTree.deleteItem @@ -470,7 +470,7 @@ class GuiProjectTree(QTreeWidget): else: newLabel = self.tr("New Folder") - newLabel, dlgOk = QInputDialog.getText(self, "", self.tr("Label:"), text=newLabel) + newLabel, dlgOk = GuiEditLabel.getLabel(self, text=newLabel) if not dlgOk: logger.info("New item creation cancelled by user") return False @@ -565,23 +565,20 @@ class GuiProjectTree(QTreeWidget): return True - def editTreeItem(self, tHandle): - """Open the edit item dialog. + def renameTreeItem(self, tHandle): + """Open a dialog to edit the label of an item. """ tItem = self.theProject.tree[tHandle] if tItem is None: return False - if tItem.itemType == nwItemType.NO_TYPE: - return False - logger.verbose("Requesting change to item '%s'", tHandle) - dlgProj = GuiItemEditor(self, tHandle) - dlgProj.exec_() - if dlgProj.result() == QDialog.Accepted: + newLabel, dlgOk = GuiEditLabel.getLabel(self, text=tItem.itemName) + if dlgOk: + tItem.setName(newLabel) self.setTreeItemValues(tHandle) self._alertTreeChange(tHandle=tHandle, flush=False) - return True + return def saveTreeOrder(self): """Build a list of the items in the project tree and send them @@ -1019,6 +1016,10 @@ class GuiProjectTree(QTreeWidget): # Edit Item Settings # ================== + ctxMenu.addAction( + self.tr("Change Label"), lambda: self.renameTreeItem(tHandle) + ) + if isFile: ctxMenu.addAction( self.tr("Toggle Exported"), lambda: self._toggleItemExported(tHandle) @@ -1057,12 +1058,8 @@ class GuiProjectTree(QTreeWidget): ctxMenu.addSeparator() - # Major Item Actions - # ================== - - ctxMenu.addAction( - self.tr("Edit Item Settings"), lambda: self.editTreeItem(tHandle) - ) + # Delete Item + # =========== if tItem.itemClass == nwItemClass.TRASH or tItem.itemType == nwItemType.ROOT: ctxMenu.addAction( diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6b315866..de80c59c 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -817,7 +817,7 @@ class GuiMain(QMainWindow): else: tHandle = self.projView.getSelectedHandle() if tHandle: - return self.projView.editTreeItem(tHandle) + return self.projView.renameTreeItem(tHandle) return False diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index 9d9c9082..4e3138f6 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -25,10 +25,10 @@ import pytest from mock import causeOSError from tools import getGuiItem, readFile, writeFile, buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QDialog, QInputDialog +from PyQt5.QtWidgets import QAction, QMessageBox from novelwriter.enum import nwItemType, nwWidget -from novelwriter.dialogs import GuiDocMerge, GuiItemEditor +from novelwriter.dialogs import GuiDocMerge, GuiEditLabel from novelwriter.core.tree import NWTree @@ -39,7 +39,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) - monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create a new project buildTestProject(nwGUI, fncProj) @@ -55,7 +55,6 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): hMergedDoc = "0000000000023" # Add Project Content - monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 5b042570..d0201d6a 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -25,10 +25,10 @@ import pytest from mock import causeOSError from tools import getGuiItem, readFile, writeFile, buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QDialog, QInputDialog +from PyQt5.QtWidgets import QAction, QMessageBox from novelwriter.enum import nwItemType, nwWidget -from novelwriter.dialogs import GuiDocSplit, GuiItemEditor +from novelwriter.dialogs import GuiDocSplit, GuiEditLabel from novelwriter.core.tree import NWTree from novelwriter.core.document import NWDoc @@ -40,7 +40,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) - monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create a new project buildTestProject(nwGUI, fncProj) @@ -59,7 +59,6 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): hSceneFive = "0000000000028" # Add Project Content - monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree._getTreeItem(hNovelRoot).setSelected(True) diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py deleted file mode 100644 index 360f7641..00000000 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ /dev/null @@ -1,246 +0,0 @@ -""" -novelWriter – Item Editor Dialog Class Tester -============================================= - -This file is a part of novelWriter -Copyright 2018–2022, Veronica Berglyd Olsen - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, but -WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -import pytest - -from tools import getGuiItem, buildTestProject - -from PyQt5.QtWidgets import QAction, QDialog, QMessageBox, QInputDialog - -from novelwriter.enum import nwItemLayout, nwItemType -from novelwriter.dialogs import GuiItemEditor -from novelwriter.core.tree import NWTree -from novelwriter.gui.projtree import GuiProjectTree - -statusKeys = ["s000000", "s000001", "s000002", "s000003"] -importKeys = ["i000004", "i000005", "i000006", "i000007"] - - -@pytest.mark.gui -def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): - """Test launching the item editor dialog from GuiMain. - """ - # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - - # Block Dialog exec_ - monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None) - - # Open Editor wo/Project - assert nwGUI.editItem() is False - - # Create and Open Project - buildTestProject(nwGUI, fncProj) - tHandle = "000000000000f" - - # No Selection - nwGUI.projView.projTree.clearSelection() - assert nwGUI.editItem() is False - - # Force opening from editor - assert nwGUI.openDocument(tHandle) - nwGUI.isFocusMode = True - - # Block Tree Lookup - with monkeypatch.context() as mp: - mp.setattr(NWTree, "__getitem__", lambda *a: None) - assert nwGUI.editItem() is False - - # Invalid Type - nwGUI.theProject.tree[tHandle]._type = nwItemType.NO_TYPE - assert nwGUI.editItem() is False - nwGUI.theProject.tree[tHandle]._type = nwItemType.FILE - - # Open Properly - assert nwGUI.editItem() is True - qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000) - itemEdit = getGuiItem("GuiItemEditor") - assert itemEdit is not None - itemEdit.close() - - # Open Via Menu - with monkeypatch.context() as mp: - mp.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted) - nwGUI.mainMenu.aEditItem.activate(QAction.Trigger) - qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000) - itemEdit = getGuiItem("GuiItemEditor") - assert itemEdit is not None - itemEdit.close() - - nwGUI.isFocusMode = False - -# END Test testDlgItemEditor_Dialog - - -@pytest.mark.gui -def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): - """Test the item editor dialog for a novel document. - """ - # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - - # Create Project and Open Document - buildTestProject(nwGUI, fncProj) - tHandle = "000000000000f" - - assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" - assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" - - assert nwGUI.openDocument(tHandle) is True - - # Check that an invalid handle is managed - itemEdit = GuiItemEditor(nwGUI, "whatever") - itemEdit.show() - itemEdit._doClose() - - # Edit a Document - itemEdit = GuiItemEditor(nwGUI, tHandle) - itemEdit.show() - - # Check Existing Settings - assert itemEdit.editName.text() == "New Scene" - assert itemEdit.editStatus.currentData() == statusKeys[0] - assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT - assert itemEdit.editExport.isChecked() is True - - # Change Settings - layoutIdx = itemEdit.editLayout.findData(nwItemLayout.NOTE) - itemEdit.editName.setText("Great Scene") - itemEdit.editStatus.setCurrentIndex(1) - itemEdit.editLayout.setCurrentIndex(layoutIdx) - itemEdit.editExport.setChecked(False) - - # Check New Settings - itemEdit._doSave() - assert itemEdit.theItem.itemName == "Great Scene" - assert itemEdit.theItem.itemStatus == statusKeys[1] - assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE - assert itemEdit.theItem.isExported is False - - # Check that the editor header is updated - nwGUI.docEditor.updateDocInfo(tHandle) - assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel › New Chapter › Great Scene" - - itemEdit.close() - del itemEdit - # qtbot.stopForInteraction() - -# END Test testDlgItemEditor_Dialog - - -@pytest.mark.gui -def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): - """Test the item editor dialog for a project note. - """ - # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) - - # Create Project and Open Document - buildTestProject(nwGUI, fncProj) - assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" - assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" - assert nwGUI.theProject.importItems.name(importKeys[0]) == "New" - assert nwGUI.theProject.importItems.name(importKeys[1]) == "Minor" - - # Create Note - nwGUI.projView.projTree.clearSelection() - nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True) - nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) - - # Open Note - assert nwGUI.openDocument("0000000000010") - - # Edit a Document - itemEdit = GuiItemEditor(nwGUI, "0000000000010") - itemEdit.show() - - # Check Existing Settings - assert itemEdit.editName.text() == "New Note" - assert itemEdit.editStatus.currentData() == importKeys[0] - assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE - assert itemEdit.editExport.isChecked() is True - - # Change Settings - itemEdit.editName.setText("New Character") - itemEdit.editStatus.setCurrentIndex(1) - itemEdit.editExport.setChecked(False) - itemEdit._doSave() - - # Check New Settings - assert itemEdit.theItem.itemName == "New Character" - assert itemEdit.theItem.itemStatus == statusKeys[0] - assert itemEdit.theItem.itemImport == importKeys[1] - assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE - assert itemEdit.theItem.isExported is False - - itemEdit.close() - del itemEdit - # qtbot.stopForInteraction() - -# END Test testDlgItemEditor_Note - - -@pytest.mark.gui -def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): - """Test the item editor dialog for a folder. - """ - # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - - # Create Project and Open Document - buildTestProject(nwGUI, fncProj) - - # Edit a Folder - itemEdit = GuiItemEditor(nwGUI, "000000000000d") - itemEdit.show() - - assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" - assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" - - # Check Existing Settings - assert itemEdit.editName.text() == "New Chapter" - assert itemEdit.editStatus.currentData() == statusKeys[0] - assert itemEdit.editLayout.currentData() == nwItemLayout.NO_LAYOUT - assert itemEdit.editExport.isChecked() is False - - assert itemEdit.editLayout.isEnabled() is False - assert itemEdit.editExport.isEnabled() is False - - # Change Settings - itemEdit.editName.setText("Chapter One") - itemEdit.editStatus.setCurrentIndex(1) - - # Check New Settings - itemEdit._doSave() - assert itemEdit.theItem.itemName == "Chapter One" - assert itemEdit.theItem.itemStatus == statusKeys[1] - assert itemEdit.theItem.itemLayout == nwItemLayout.NO_LAYOUT - assert itemEdit.theItem.isExported is False - - itemEdit.close() - del itemEdit - # qtbot.stopForInteraction() - -# END Test testDlgItemEditor_Folder diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index cf38c7ea..039901e8 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -26,13 +26,13 @@ from shutil import copyfile from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox, QDialog, QInputDialog +from PyQt5.QtWidgets import QMessageBox, QInputDialog from novelwriter.gui import GuiDocEditor, GuiNovelTree, GuiOutlineView from novelwriter.enum import nwItemType, nwWidget from novelwriter.tools import GuiProjectWizard from novelwriter.gui.projtree import GuiProjectTree -from novelwriter.dialogs.itemeditor import GuiItemEditor +from novelwriter.dialogs import GuiEditLabel keyDelay = 2 typeDelay = 1 @@ -169,11 +169,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None) - monkeypatch.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create new, save, close project buildTestProject(nwGUI, fncProj) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index b3206aa5..d7ed41d0 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,10 +24,11 @@ import os from tools import buildTestProject -from PyQt5.QtWidgets import QMessageBox, QInputDialog, QMenu +from PyQt5.QtWidgets import QMessageBox, QMenu -from novelwriter.gui.projtree import GuiProjectTree from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass +from novelwriter.dialogs import GuiEditLabel +from novelwriter.gui.projtree import GuiProjectTree @pytest.mark.gui @@ -39,7 +40,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) nwTree = nwGUI.projView @@ -125,7 +126,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # Cancel during creation with monkeypatch.context() as mp: - mp.setattr(QInputDialog, "getText", lambda *a, **k: ("", False)) + mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False)) nwTree.setSelectedHandle("0000000000013") assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False @@ -162,7 +163,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) nwTree = nwGUI.projView @@ -277,7 +278,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) nwTree = nwGUI.projView @@ -466,7 +467,7 @@ def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(QMenu, "exec_", lambda *a: None) # Create a project From 946656f8c5bd65fda8a1a0fb63e1303949557965 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 18:26:41 +0200 Subject: [PATCH 140/179] Add test for edit label dialog --- tests/test_dialogs/test_dlg_dialogs.py | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/tests/test_dialogs/test_dlg_dialogs.py b/tests/test_dialogs/test_dlg_dialogs.py index 7ae56d94..3423beb5 100644 --- a/tests/test_dialogs/test_dlg_dialogs.py +++ b/tests/test_dialogs/test_dlg_dialogs.py @@ -24,11 +24,7 @@ import pytest from PyQt5.QtCore import QItemSelectionModel from PyQt5.QtWidgets import QAction, QListWidgetItem, QDialog, QMessageBox -from novelwriter.dialogs import GuiQuoteSelect, GuiUpdates - -keyDelay = 2 -typeDelay = 1 -stepDelay = 20 +from novelwriter.dialogs import GuiQuoteSelect, GuiUpdates, GuiEditLabel @pytest.mark.gui @@ -101,3 +97,24 @@ def testDlgOther_Updates(qtbot, monkeypatch, nwGUI): nwUpdate._doClose() # END Test testDlgOther_Updates + + +@pytest.mark.gui +def testDlgOther_EditLabel(qtbot, monkeypatch): + """Test the label editor dialog. + """ + monkeypatch.setattr(GuiEditLabel, "exec_", lambda *a: None) + + with monkeypatch.context() as mp: + mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Accepted) + newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World") + assert dlgOk is True + assert newLabel == "Hello World" + + with monkeypatch.context() as mp: + mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Rejected) + newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World") + assert dlgOk is False + assert newLabel == "Hello World" + +# END Test testDlgOther_EditLabel From 8d427f40c659bb71e591cb91cd8a0959aee98094 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 18:40:21 +0200 Subject: [PATCH 141/179] Add a shortcut to open project tree context menu --- docs/source/usage_shortcuts.rst | 3 +-- novelwriter/gui/projtree.py | 17 ++++++++++++++--- tests/test_gui/test_gui_projtree.py | 6 ++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst index fa761a3e..e9e7bb36 100644 --- a/docs/source/usage_shortcuts.rst +++ b/docs/source/usage_shortcuts.rst @@ -25,7 +25,7 @@ The main shorcuts are as follows: ":kbd:`Alt`:kbd:`4`", "Switch focus to outline view. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`4`." ":kbd:`Alt`:kbd:`Left`", "Move backward in the view history of the document viewer." ":kbd:`Alt`:kbd:`Right`", "Move forward in the view history of the document viewer." - ":kbd:`Ctrl`:kbd:`.`", "Open menu to correct word under cursor." + ":kbd:`Ctrl`:kbd:`.`", "Open the context menu in the project tree or the document editor." ":kbd:`Ctrl`:kbd:`,`", "Open the :guilabel:`Preferences` dialog." ":kbd:`Ctrl`:kbd:`/`", "Toggle block format as comment." ":kbd:`Ctrl`:kbd:`0`", "Remove block formatting for block under cursor." @@ -42,7 +42,6 @@ The main shorcuts are as follows: ":kbd:`Ctrl`:kbd:`B`", "Format selected text, or word under cursor, with strong emphasis (bold)." ":kbd:`Ctrl`:kbd:`C`", "Copy selected text to clipboard." ":kbd:`Ctrl`:kbd:`D`", "Strikethrough selected text, or word under cursor." - ":kbd:`Ctrl`:kbd:`E`", "If in the project tree, edit a document or folder settings." ":kbd:`Ctrl`:kbd:`F`", "Open the search bar and search for the selected word, if any is selected." ":kbd:`Ctrl`:kbd:`G`", "Find next occurrence of search word in current document." ":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)" diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index aa1ff9ce..b9bfda0d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -97,6 +97,11 @@ class GuiProjectView(QWidget): self.keyUndoMv.setContext(Qt.WidgetShortcut) self.keyUndoMv.activated.connect(lambda: self.projTree.undoLastMove()) + self.keyContext = QShortcut(self.projTree) + self.keyContext.setKey("Ctrl+.") + self.keyContext.setContext(Qt.WidgetShortcut) + self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected()) + # Function Mappings self.revealNewTreeItem = self.projTree.revealNewTreeItem self.renameTreeItem = self.projTree.renameTreeItem @@ -913,9 +918,6 @@ class GuiProjectTree(QTreeWidget): def setSelectedHandle(self, tHandle, doScroll=False): """Set a specific handle as the selected item. """ - if tHandle not in self._treeMap: - return False - tItem = self._getTreeItem(tHandle) if tItem is None: return False @@ -929,6 +931,15 @@ class GuiProjectTree(QTreeWidget): return True + def openContextOnSelected(self): + """Open the context menu on the current selected item. + """ + selItem = self.selectedItems() + if selItem: + pos = self.visualItemRect(selItem[0]).center() + return self._openContextMenu(pos) + return False + def changedSince(self, checkTime): """Check if the tree has changed since a given time. """ diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index d7ed41d0..feef859d 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -511,6 +511,12 @@ def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert projTree._openContextMenu(itemPos(hCharRoot)) is True assert projTree._openContextMenu(itemPos(hCharNote)) is True + # Check the keyboard shortcut handler as well + projTree.setSelectedHandle(hNovelRoot) + assert projTree.openContextOnSelected() is True + projTree.clearSelection() + assert projTree.openContextOnSelected() is False + # Direct Edit Functions # ===================== # Trigger the dedicated functions the menu entries connect to From 2cc8be577612ed35314d7d0fe0eb05592aa4ca4a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 18:48:22 +0200 Subject: [PATCH 142/179] Rename the main GUI function for editing an item --- novelwriter/gui/doceditor.py | 2 +- novelwriter/gui/mainmenu.py | 2 +- novelwriter/guimain.py | 2 +- tests/test_gui/test_gui_guimain.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index ab2c7f2f..25552037 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2746,7 +2746,7 @@ class GuiDocEditHeader(QWidget): def _editDocument(self): """Open the edit item dialog from the main GUI. """ - self.mainGui.editItem(self._docHandle) + self.mainGui.editItemLabel(self._docHandle) return def _searchDocument(self): diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 85920749..59af7691 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -165,7 +165,7 @@ class GuiMainMenu(QMenuBar): # Project > Edit self.aEditItem = QAction(self.tr("Rename Item"), self) self.aEditItem.setShortcuts(["F2"]) - self.aEditItem.triggered.connect(lambda: self.mainGui.editItem(None)) + self.aEditItem.triggered.connect(lambda: self.mainGui.editItemLabel(None)) self.projMenu.addAction(self.aEditItem) # Project > Delete diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index de80c59c..e15a82b0 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -804,7 +804,7 @@ class GuiMain(QMainWindow): return True - def editItem(self, tHandle=None): + def editItemLabel(self, tHandle=None): """Open the edit item dialog. """ if not self.hasProject: diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 039901e8..5fa6951b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -57,7 +57,7 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI): assert nwGUI.mergeDocuments() is False assert nwGUI.splitDocument() is False assert nwGUI.openSelectedItem() is False - assert nwGUI.editItem() is False + assert nwGUI.editItemLabel() is False assert nwGUI.requestNovelTreeRefresh() is False assert nwGUI.rebuildIndex() is False assert nwGUI.showProjectSettingsDialog() is False From c58868ed3952ac06f3ff3870b0e5591ba5ba03b8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 23:54:20 +0200 Subject: [PATCH 143/179] Simplify the conversion of very old project data --- novelwriter/core/project.py | 98 ++++++++----------------- tests/test_core/test_core_project.py | 104 +++++++++------------------ 2 files changed, 61 insertions(+), 141 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index dfb577b1..c2683cc7 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -438,7 +438,7 @@ class NWProject(): legacyList = [] # Cleanup is done later for projItem in os.listdir(self.projPath): logger.verbose("Project contains: %s", projItem) - if projItem.startswith("data_"): + if projItem.startswith("data_") and len(projItem) == 6: legacyList.append(projItem) # Project Lock @@ -500,7 +500,7 @@ class NWProject(): # Check File Type # =============== - if not nwxRoot == "novelWriterXML": + if nwxRoot != "novelWriterXML": self.mainGui.makeAlert(self.tr( "Project file does not appear to be a novelWriterXML file." ), nwAlert.ERROR) @@ -642,11 +642,14 @@ class NWProject(): # Sort out old file locations if legacyList: - errList = [] - for projItem in legacyList: - errList = self._legacyDataFolder(projItem, errList) - if errList: - self.mainGui.makeAlert(errList, nwAlert.ERROR) + try: + for projItem in legacyList: + self._legacyDataFolder(projItem) + except Exception: + self.mainGui.makeAlert(self.tr( + "There was an error while converting project version 1.0. " + "Some data may not have been preserved." + ), nwAlert.ERROR) # Clean up no longer used files self._deprecatedFiles() @@ -1558,82 +1561,37 @@ class NWProject(): # Legacy Data Structure Handlers ## - def _legacyDataFolder(self, theFolder, errList): + def _legacyDataFolder(self, dataDir): """Clean up legacy data folders. """ - theData = os.path.join(self.projPath, theFolder) - if not os.path.isdir(theData): - errList.append(self.tr("Not a folder: {0}").format(theData)) - return errList + dataPath = os.path.join(self.projPath, dataDir) + if not os.path.isdir(dataPath): + return False - logger.info("Old data folder %s found", theFolder) + logger.info("Old data folder found: %s", dataDir) # Move Documents to Content - for dataItem in os.listdir(theData): - theFile = os.path.join(theData, dataItem) - if not os.path.isfile(theFile): - theErr = self._moveUnknownItem(theData, dataItem) - if theErr: - errList.append(theErr) + for dataItem in os.listdir(dataPath): + dataFile = os.path.join(dataPath, dataItem) + if not os.path.isfile(dataFile): continue if len(dataItem) == 21 and dataItem.endswith("_main.nwd"): - tHandle = theFolder[-1]+dataItem[:12] - newPath = os.path.join(self.projContent, tHandle+".nwd") - try: - os.rename(theFile, newPath) - logger.info("Moved file: %s", theFile) - logger.info("New location: %s", newPath) - except Exception: - errList.append(self.tr("Could not move: {0}").format(theFile)) - logger.error("Could not move: %s", theFile) - logException() + tHandle = dataDir[-1] + dataItem[:12] + newPath = os.path.join(self.projContent, f"{tHandle}.nwd") + os.rename(dataFile, newPath) + logger.info("Moved file: %s", dataFile) elif len(dataItem) == 21 and dataItem.endswith("_main.bak"): - try: - os.unlink(theFile) - logger.info("Deleted file: %s", theFile) - except Exception: - errList.append(self.tr("Could not delete: {0}").format(theFile)) - logger.error("Could not delete: %s", theFile) - logException() - - else: - theErr = self._moveUnknownItem(theData, dataItem) - if theErr: - errList.append(theErr) + os.unlink(dataFile) + logger.info("Deleted file: %s", dataFile) # Remove Data Folder - try: - os.rmdir(theData) - logger.info("Deleted folder: %s", theFolder) - except Exception: - errList.append(self.tr("Could not delete: {0}").format(theFolder)) - logger.error("Could not delete: %s", theFolder) - logException() + if not os.listdir(dataPath): + os.rmdir(dataPath) + logger.info("Deleted folder: %s", dataDir) - return errList - - def _moveUnknownItem(self, theDir, theItem): - """Move an item that doesn't belong in the project folder to - a junk folder. - """ - theJunk = os.path.join(self.projPath, "junk") - if not self._checkFolder(theJunk): - return self.tr("Could not make folder: {0}").format(theJunk) - - theSrc = os.path.join(theDir, theItem) - theDst = os.path.join(theJunk, theItem) - - try: - os.rename(theSrc, theDst) - logger.info("Moved to junk: %s", theSrc) - except Exception: - logger.error("Could not move item %s to junk", theSrc) - logException() - return self.tr("Could not move item {0} to {1}.").format(theSrc, theJunk) - - return "" + return True def _deprecatedFiles(self): """Delete files that are no longer used by novelWriter. diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 9454b0fc..bdc27771 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -463,12 +463,15 @@ def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI): os.rename(oName, rName) # Add some legacy stuff that cannot be removed - writeFile(os.path.join(nwMinimal, "junk"), "stuff") - os.mkdir(os.path.join(nwMinimal, "data_0")) - writeFile(os.path.join(nwMinimal, "data_0", "junk"), "stuff") - mockGUI.clear() - assert theProject.openProject(nwMinimal) is True - assert "data_0" in mockGUI.lastAlert + with monkeypatch.context() as mp: + mp.setattr(theProject, "_legacyDataFolder", causeOSError) + os.mkdir(os.path.join(nwMinimal, "data_0")) + writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.nwd"), "stuff") + writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.bak"), "stuff") + mockGUI.clear() + assert theProject.openProject(nwMinimal) is True + assert "version 1.0" in mockGUI.lastAlert + assert theProject.closeProject() # END Test testCoreProject_Open @@ -1141,14 +1144,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj): os.path.join(nwOldProj, "meta", "sessionLogOptions.json"), ] - # Add some files that shouldn't be there - deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.nwd")) - deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.txt")) - - # Add some folders that shouldn't be there - os.mkdir(os.path.join(nwOldProj, "stuff")) - os.mkdir(os.path.join(nwOldProj, "data_1", "stuff")) - # Create mock files os.mkdir(os.path.join(nwOldProj, "cache")) for aFile in deleteFiles: @@ -1162,7 +1157,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj): for aFile in deleteFiles: assert not os.path.isfile(aFile) - assert not os.path.isdir(os.path.join(nwOldProj, "data_1", "stuff")) assert not os.path.isdir(os.path.join(nwOldProj, "data_1")) assert not os.path.isdir(os.path.join(nwOldProj, "data_7")) assert not os.path.isdir(os.path.join(nwOldProj, "data_8")) @@ -1170,12 +1164,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj): assert not os.path.isdir(os.path.join(nwOldProj, "data_a")) assert not os.path.isdir(os.path.join(nwOldProj, "data_f")) - # Check stuff that has been moved - assert os.path.isdir(os.path.join(nwOldProj, "junk")) - assert os.path.isdir(os.path.join(nwOldProj, "junk", "stuff")) - assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.nwd")) - assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.txt")) - # Check that files we want to keep are in the right place assert os.path.isdir(os.path.join(nwOldProj, "cache")) assert os.path.isdir(os.path.join(nwOldProj, "content")) @@ -1217,7 +1205,7 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir): with monkeypatch.context() as mp: mp.setattr("os.unlink", causeOSError) - assert not theProject._deprecatedFiles() + assert theProject._deprecatedFiles() is False assert theProject._deprecatedFiles() assert not os.path.isfile(tstFile) @@ -1226,63 +1214,36 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir): tstFile = os.path.join(fncDir, "data_0") writeFile(tstFile, "stuff") assert os.path.isfile(tstFile) - - errList = [] - errList = theProject._legacyDataFolder(tstFile, errList) - assert len(errList) > 0 - - # Move folder in data folder, shouldn't be there - tstData = os.path.join(fncDir, "data_1") - errItem = os.path.join(fncDir, "data_1", "stuff") - os.mkdir(tstData) - os.mkdir(errItem) - assert os.path.isdir(tstData) - assert os.path.isdir(errItem) - - # This causes a failure to create the 'junk' folder - with monkeypatch.context() as mp: - mp.setattr("os.mkdir", causeOSError) - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) > 0 - - # This causes a failure to move 'stuff' to 'junk' - with monkeypatch.context() as mp: - mp.setattr("os.rename", causeOSError) - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) > 0 - - # This should be successful - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) == 0 - assert os.path.isdir(os.path.join(fncDir, "junk", "stuff")) + assert theProject._legacyDataFolder(tstFile) is False # Check renaming/deleting of old document files - tstData = os.path.join(fncDir, "data_2") - tstDoc1m = os.path.join(tstData, "000000000001_main.nwd") - tstDoc1b = os.path.join(tstData, "000000000001_main.bak") - tstDoc2m = os.path.join(tstData, "000000000002_main.nwd") - tstDoc2b = os.path.join(tstData, "000000000002_main.bak") - tstDoc3m = os.path.join(tstData, "tooshort003_main.nwd") - tstDoc3b = os.path.join(tstData, "tooshort003_main.bak") + tstData2 = os.path.join(fncDir, "data_2") + tstData3 = os.path.join(fncDir, "data_3") + tstDoc1m = os.path.join(tstData2, "000000000001_main.nwd") + tstDoc1b = os.path.join(tstData2, "000000000001_main.bak") + tstDoc2m = os.path.join(tstData2, "000000000002_main.nwd") + tstDoc2b = os.path.join(tstData2, "000000000002_main.bak") + tstDoc3m = os.path.join(tstData3, "tooshort003_main.nwd") + tstDoc3b = os.path.join(tstData3, "tooshort003_main.bak") + tstDir4a = os.path.join(tstData3, "stuff") - os.mkdir(tstData) + os.mkdir(tstData2) + os.mkdir(tstData3) writeFile(tstDoc1m, "stuff") writeFile(tstDoc1b, "stuff") writeFile(tstDoc2m, "stuff") writeFile(tstDoc2b, "stuff") writeFile(tstDoc3m, "stuff") writeFile(tstDoc3b, "stuff") + os.mkdir(tstDir4a) # Make the above fail with monkeypatch.context() as mp: mp.setattr("os.rename", causeOSError) mp.setattr("os.unlink", causeOSError) - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) > 0 + with pytest.raises(OSError): + theProject._legacyDataFolder(tstData2) + theProject._legacyDataFolder(tstData3) assert os.path.isfile(tstDoc1m) assert os.path.isfile(tstDoc1b) assert os.path.isfile(tstDoc2m) @@ -1291,15 +1252,16 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir): assert os.path.isfile(tstDoc3b) # And succeed ... - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) == 0 + assert theProject._legacyDataFolder(tstData2) is True + assert theProject._legacyDataFolder(tstData3) is True - assert not os.path.isdir(tstData) + assert not os.path.isdir(tstData2) + assert os.path.isdir(tstData3) assert os.path.isfile(os.path.join(fncDir, "content", "2000000000001.nwd")) assert os.path.isfile(os.path.join(fncDir, "content", "2000000000002.nwd")) - assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.nwd")) - assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.bak")) + assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.nwd")) + assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.bak")) + assert os.path.isdir(tstDir4a) # END Test testCoreProject_LegacyData From b3eb276437dcee8bbac9ec868d42a10283e64ad3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jun 2022 15:45:39 +0200 Subject: [PATCH 144/179] Wrap the novel tree in an outer widget --- novelwriter/gui/__init__.py | 4 +- novelwriter/gui/noveltree.py | 93 +++++++++++++++++++++++----- novelwriter/guimain.py | 11 ++-- tests/test_gui/test_gui_guimain.py | 8 +-- tests/test_gui/test_gui_noveltree.py | 77 +++++++++++------------ 5 files changed, 126 insertions(+), 67 deletions(-) diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index 1892e83d..17699942 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -23,7 +23,7 @@ from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.docviewer import GuiDocViewer, GuiDocViewDetails from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.mainmenu import GuiMainMenu -from novelwriter.gui.noveltree import GuiNovelTree +from novelwriter.gui.noveltree import GuiNovelView from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.statusbar import GuiMainStatus @@ -37,7 +37,7 @@ __all__ = [ "GuiItemDetails", "GuiMainMenu", "GuiMainStatus", - "GuiNovelTree", + "GuiNovelView", "GuiOutlineView", "GuiProjectView", "GuiTheme", diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index f634ce23..85773478 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -30,7 +30,8 @@ from time import time from PyQt5.QtCore import Qt, QSize from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QAbstractItemView, QFrame + QAbstractItemView, QFrame, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget ) from novelwriter.common import checkInt @@ -39,21 +40,88 @@ from novelwriter.constants import nwKeyWords logger = logging.getLogger(__name__) +class GuiNovelView(QWidget): + + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) + + self.mainGui = mainGui + + # Build GUI + self.novelTree = GuiNovelTree(self) + self.novelBar = GuiNovelToolBar(self) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.novelBar, 0) + self.outerBox.addWidget(self.novelTree, 1) + self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + # Function Mappings + self.refreshTree = self.novelTree.refreshTree + self.updateWordCounts = self.novelTree.updateWordCounts + self.getSelectedHandle = self.novelTree.getSelectedHandle + + return + + ## + # Methods + ## + + def initSettings(self): + self.novelTree.initSettings() + return + + def clearProject(self): + self.novelTree.clearTree() + return + + def setFocus(self): + """Forward the set focus call to the tree widget. + """ + self.novelTree.setFocus() + return + + def treeFocus(self): + """Check if the novel tree has focus. + """ + return self.novelTree.hasFocus() + +# END Class GuiNovelView + + +class GuiNovelToolBar(QWidget): + + def __init__(self, novelView): + QTreeWidget.__init__(self, novelView) + + self.mainConf = novelwriter.CONFIG + self.novelView = novelView + + return + +# END Class GuiNovelToolBar + + class GuiNovelTree(QTreeWidget): C_TITLE = 0 C_WORDS = 1 C_POV = 2 - def __init__(self, mainGui): - QTreeWidget.__init__(self, mainGui) + def __init__(self, novelView): + QTreeWidget.__init__(self, novelView) logger.debug("Initialising GuiNovelTree ...") self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.mainTheme = mainGui.mainTheme - self.theProject = mainGui.theProject + self.novelView = novelView + self.mainGui = novelView.mainGui + self.mainTheme = novelView.mainGui.mainTheme + self.theProject = novelView.mainGui.theProject # Internal Variables self._treeMap = {} @@ -97,13 +165,13 @@ class GuiNovelTree(QTreeWidget): self.resizeColumnToContents(self.C_POV) # Set custom settings - self.initTree() + self.initSettings() logger.debug("GuiNovelTree initialisation complete") return - def initTree(self): + def initSettings(self): """Set or update tree widget settings. """ # Scroll bars @@ -162,15 +230,6 @@ class GuiNovelTree(QTreeWidget): self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}") return - def getColumnSizes(self): - """Return the column widths for the tree columns. - """ - retVals = [ - self.columnWidth(0), - self.columnWidth(1), - ] - return retVals - def getSelectedHandle(self): """Get the currently selected handle. If multiple items are selected, return the first. diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index e15a82b0..6fa52eb6 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, - GuiMainStatus, GuiNovelTree, GuiOutlineView, GuiProjectView, GuiTheme, + GuiMainStatus, GuiNovelView, GuiOutlineView, GuiProjectView, GuiTheme, GuiViewsBar ) from novelwriter.dialogs import ( @@ -106,7 +106,7 @@ class GuiMain(QMainWindow): # Main GUI Elements self.statusBar = GuiMainStatus(self) self.projView = GuiProjectView(self) - self.novelView = GuiNovelTree(self) + self.novelView = GuiNovelView(self) self.docEditor = GuiDocEditor(self) self.viewMeta = GuiDocViewDetails(self) self.docViewer = GuiDocViewer(self) @@ -292,7 +292,7 @@ class GuiMain(QMainWindow): """ # Project Area self.projView.clearProject() - self.novelView.clearTree() + self.novelView.clearProject() self.itemDetails.clearDetails() # Work Area @@ -791,7 +791,7 @@ class GuiMain(QMainWindow): tLine = None if self.projView.treeFocus(): tHandle = self.projView.getSelectedHandle() - elif self.novelView.hasFocus(): + elif self.novelView.treeFocus(): tHandle, tLine = self.novelView.getSelectedHandle() elif self.outlineView.treeFocus(): tHandle, tLine = self.outlineView.getSelectedHandle() @@ -920,7 +920,7 @@ class GuiMain(QMainWindow): self.docEditor.initEditor() self.docViewer.initViewer() self.projView.initSettings() - self.novelView.initTree() + self.novelView.initSettings() self.outlineView.initOutline() self._updateStatusWordCount() @@ -1161,7 +1161,6 @@ class GuiMain(QMainWindow): self.mainConf.setViewPanePos(self.splitView.sizes()) self.mainConf.setShowRefPanel(self.viewMeta.isVisible()) - self.mainConf.setNovelColWidths(self.novelView.getColumnSizes()) if not self.mainConf.isFullScreen: self.mainConf.setWinSize(self.width(), self.height()) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 5fa6951b..9a56af3d 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -28,7 +28,7 @@ from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QInputDialog -from novelwriter.gui import GuiDocEditor, GuiNovelTree, GuiOutlineView +from novelwriter.gui import GuiDocEditor, GuiNovelView, GuiOutlineView from novelwriter.enum import nwItemType, nwWidget from novelwriter.tools import GuiProjectWizard from novelwriter.gui.projtree import GuiProjectTree @@ -134,12 +134,12 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): nwGUI.projStack.setCurrentIndex(1) nwGUI.novelView.refreshTree(True) with monkeypatch.context() as mp: - mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True) + mp.setattr(GuiNovelView, "treeFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - actItem = nwGUI.novelView.topLevelItem(0) + actItem = nwGUI.novelView.novelTree.topLevelItem(0) chpItem = actItem.child(0) selItem = chpItem.child(0) - nwGUI.novelView.setCurrentItem(selItem) + nwGUI.novelView.novelTree.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 2bf2df8a..644839d3 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -37,23 +37,24 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) nwGUI.openProject(nwMinimal) - nwTree = nwGUI.novelView + novelView = nwGUI.novelView + novelTree = novelView.novelTree ## # Show/Hide Scrollbars ## - nwTree.mainConf.hideVScroll = True - nwTree.mainConf.hideHScroll = True - nwTree.initTree() - assert not nwTree.verticalScrollBar().isVisible() - assert not nwTree.horizontalScrollBar().isVisible() + nwGUI.mainConf.hideVScroll = True + nwGUI.mainConf.hideHScroll = True + novelView.initSettings() + assert not novelTree.verticalScrollBar().isVisible() + assert not novelTree.horizontalScrollBar().isVisible() - nwTree.mainConf.hideVScroll = False - nwTree.mainConf.hideHScroll = False - nwTree.initTree() - assert nwTree.verticalScrollBar().isEnabled() - assert nwTree.horizontalScrollBar().isEnabled() + nwGUI.mainConf.hideVScroll = False + nwGUI.mainConf.hideHScroll = False + novelView.initSettings() + assert novelTree.verticalScrollBar().isEnabled() + assert novelTree.horizontalScrollBar().isEnabled() ## # Populate Tree @@ -61,31 +62,31 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): nwGUI.projStack.setCurrentIndex(nwGUI.idxNovelView) nwGUI.rebuildIndex() - nwTree._populateTree() - assert nwTree.topLevelItemCount() == 1 + novelTree._populateTree() + assert novelTree.topLevelItemCount() == 1 # Rebuild should preserve selection - topItem = nwTree.topLevelItem(0) + topItem = novelTree.topLevelItem(0) assert not topItem.isSelected() topItem.setSelected(True) - assert nwTree.selectedItems()[0] == topItem - assert nwTree.getSelectedHandle() == ("a35baf2e93843", 0) + assert novelTree.selectedItems()[0] == topItem + assert novelView.getSelectedHandle() == ("a35baf2e93843", 0) - nwTree.refreshTree() - assert nwTree.topLevelItem(0).isSelected() + novelView.refreshTree() + assert novelTree.topLevelItem(0).isSelected() ## # Open Items ## # Clear selection - nwTree.clearSelection() - scItem = nwTree.topLevelItem(0).child(0).child(0) + novelTree.clearSelection() + scItem = novelTree.topLevelItem(0).child(0).child(0) scItem.setSelected(True) assert scItem.isSelected() # Clear selection with mouse - vPort = nwTree.viewport() + vPort = novelTree.viewport() qtbot.mouseClick(vPort, Qt.LeftButton, pos=vPort.rect().center(), delay=10) assert not scItem.isSelected() @@ -93,7 +94,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): scItem.setSelected(True) assert scItem.isSelected() assert nwGUI.docEditor.docHandle() is None - nwTree._treeDoubleClick(scItem, 0) + novelTree._treeDoubleClick(scItem, 0) assert nwGUI.docEditor.docHandle() == "8c659a11cd429" # Open item with middle mouse button @@ -103,13 +104,13 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): qtbot.mouseClick(vPort, Qt.MiddleButton, pos=vPort.rect().center(), delay=10) assert nwGUI.docViewer.docHandle() is None - scRect = nwTree.visualItemRect(scItem) - oldData = scItem.data(nwTree.C_TITLE, Qt.UserRole) - scItem.setData(nwTree.C_TITLE, Qt.UserRole, (None, "", "")) + scRect = novelTree.visualItemRect(scItem) + oldData = scItem.data(novelTree.C_TITLE, Qt.UserRole) + scItem.setData(novelTree.C_TITLE, Qt.UserRole, (None, "", "")) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) assert nwGUI.docViewer.docHandle() is None - scItem.setData(nwTree.C_TITLE, Qt.UserRole, oldData) + scItem.setData(novelTree.C_TITLE, Qt.UserRole, oldData) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) assert nwGUI.docViewer.docHandle() == "8c659a11cd429" @@ -132,23 +133,23 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): "#### Section\n\n" )) nwGUI.rebuildIndex() - nwTree._populateTree() - assert nwTree.topLevelItem(0).text(nwTree.C_TITLE) == "Section wo/Scene" - assert nwTree.topLevelItem(1).text(nwTree.C_TITLE) == "Scene wo/Chapter" - assert nwTree.topLevelItem(2).text(nwTree.C_TITLE) == "Chapter wo/Title" - assert nwTree.topLevelItem(3).text(nwTree.C_TITLE) == "Title" + novelTree._populateTree() + assert novelTree.topLevelItem(0).text(novelTree.C_TITLE) == "Section wo/Scene" + assert novelTree.topLevelItem(1).text(novelTree.C_TITLE) == "Scene wo/Chapter" + assert novelTree.topLevelItem(2).text(novelTree.C_TITLE) == "Chapter wo/Title" + assert novelTree.topLevelItem(3).text(novelTree.C_TITLE) == "Title" - tTitle = nwTree.topLevelItem(3) - assert tTitle.child(0).text(nwTree.C_TITLE) == "Section w/Title, wo/Scene" - assert tTitle.child(1).text(nwTree.C_TITLE) == "Scene w/Title, wo/Chapter" - assert tTitle.child(2).text(nwTree.C_TITLE) == "Chapter" + tTitle = novelTree.topLevelItem(3) + assert tTitle.child(0).text(novelTree.C_TITLE) == "Section w/Title, wo/Scene" + assert tTitle.child(1).text(novelTree.C_TITLE) == "Scene w/Title, wo/Chapter" + assert tTitle.child(2).text(novelTree.C_TITLE) == "Chapter" tChap = tTitle.child(2) - assert tChap.child(0).text(nwTree.C_TITLE) == "Section w/Chapter, wo/Scene" - assert tChap.child(1).text(nwTree.C_TITLE) == "Scene" + assert tChap.child(0).text(novelTree.C_TITLE) == "Section w/Chapter, wo/Scene" + assert tChap.child(1).text(novelTree.C_TITLE) == "Scene" tScene = tChap.child(1) - assert tScene.child(0).text(nwTree.C_TITLE) == "Section" + assert tScene.child(0).text(novelTree.C_TITLE) == "Section" ## # Close From 52683d5a21202a6e85a15b029262027afa747889 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jun 2022 17:47:32 +0200 Subject: [PATCH 145/179] Add novel tree toolbar and connect actions --- novelwriter/core/project.py | 16 +++ novelwriter/gui/noveltree.py | 224 +++++++++++++++++++++++++++-------- novelwriter/gui/projtree.py | 26 ++-- novelwriter/guimain.py | 18 ++- sample/nwProject.nwx | 16 +-- 5 files changed, 224 insertions(+), 76 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index c2683cc7..43f3297e 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -102,6 +102,8 @@ class NWProject(): self.importItems = None # Note file importance values self.lastEdited = None # The handle of the last file to be edited self.lastViewed = None # The handle of the last file to be viewed + self.lastNovel = None # The handle of the last novel root viewed + self.lastOutline = None # The handle of the last outline root viewed self.lastWCount = 0 # The project word count from last session self.lastNovelWC = 0 # The novel files word count from last session self.lastNotesWC = 0 # The note files word count from last session @@ -612,6 +614,10 @@ class NWProject(): self.lastEdited = checkString(xItem.text, None, True) elif xItem.tag == "lastViewed": self.lastViewed = checkString(xItem.text, None, True) + elif xItem.tag == "lastNovel": + self.lastNovel = checkString(xItem.text, None, True) + elif xItem.tag == "lastOutline": + self.lastOutline = checkString(xItem.text, None, True) elif xItem.tag == "lastWordCount": self.lastWCount = checkInt(xItem.text, 0, False) elif xItem.tag == "novelWordCount": @@ -732,6 +738,8 @@ class NWProject(): self._packProjectValue(xSettings, "autoOutline", self.autoOutline) self._packProjectValue(xSettings, "lastEdited", self.lastEdited) self._packProjectValue(xSettings, "lastViewed", self.lastViewed) + self._packProjectValue(xSettings, "lastNovel", self.lastNovel) + self._packProjectValue(xSettings, "lastOutline", self.lastOutline) self._packProjectValue(xSettings, "lastWordCount", self.currWCount) self._packProjectValue(xSettings, "novelWordCount", self.currNovelWC) self._packProjectValue(xSettings, "notesWordCount", self.currNotesWC) @@ -1123,6 +1131,14 @@ class NWProject(): self.setProjectChanged(True) return True + def setLastNovelViewed(self, tHandle): + """Set last viewed novel root in the novel tree. + """ + if self.lastNovel != tHandle: + self.lastNovel = tHandle + self.setProjectChanged(True) + return True + def setStatusColours(self, newCols, delCols): """Update the list of novel file status flags. """ diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 85773478..8cec9fdb 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -27,25 +27,34 @@ import logging import novelwriter from time import time +from enum import Enum -from PyQt5.QtCore import Qt, QSize +from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal +from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import ( - QAbstractItemView, QFrame, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, + QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) +from novelwriter.enum import nwDocMode, nwItemClass from novelwriter.common import checkInt -from novelwriter.constants import nwKeyWords +from novelwriter.constants import nwKeyWords, nwLabels logger = logging.getLogger(__name__) class GuiNovelView(QWidget): + # Signals for user interaction with the novel tree + selectedItemChanged = pyqtSignal(str) + openDocumentRequest = pyqtSignal(str, Enum, int, str) + def __init__(self, mainGui): QWidget.__init__(self, mainGui) - self.mainGui = mainGui + self.mainGui = mainGui + self.theProject = mainGui.theProject # Build GUI self.novelTree = GuiNovelTree(self) @@ -60,6 +69,11 @@ class GuiNovelView(QWidget): self.setLayout(self.outerBox) + # Connect Signals + self.novelBar.rootFolderSelectionChanged.connect( + lambda tHandle: self.novelTree.refreshTree(rootHandle=tHandle, overRide=True) + ) + # Function Mappings self.refreshTree = self.novelTree.refreshTree self.updateWordCounts = self.novelTree.updateWordCounts @@ -79,6 +93,21 @@ class GuiNovelView(QWidget): self.novelTree.clearTree() return + def openProjectTasks(self): + """Run tasks related to opening a project. + """ + lastNovel = self.theProject.lastNovel + if lastNovel is None: + lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL) + + logger.debug("Setting novel tree to root item '%s'", lastNovel) + + self.clearProject() + self.novelBar.rebuildNovelRootMenu(selHandle=lastNovel) + self.novelTree.refreshTree(rootHandle=lastNovel, overRide=True) + + return + def setFocus(self): """Forward the set focus call to the tree widget. """ @@ -90,16 +119,114 @@ class GuiNovelView(QWidget): """ return self.novelTree.hasFocus() + ## + # Public Slots + ## + + @pyqtSlot(str) + def updateRootItem(self, tHandle): + """Should be called whenever a root folders changes. + """ + self.novelBar.rebuildNovelRootMenu() + return + # END Class GuiNovelView class GuiNovelToolBar(QWidget): + rootFolderSelectionChanged = pyqtSignal(str) + def __init__(self, novelView): QTreeWidget.__init__(self, novelView) - self.mainConf = novelwriter.CONFIG - self.novelView = novelView + logger.debug("Initialising GuiNovelToolBar ...") + + self.mainConf = novelwriter.CONFIG + self.novelView = novelView + self.theProject = novelView.mainGui.theProject + self.mainTheme = novelView.mainGui.mainTheme + + iPx = self.mainTheme.baseIconSize + mPx = self.mainConf.pxInt(4) + + self.setContentsMargins(0, 0, 0, 0) + self.setAutoFillBackground(True) + + qPalette = self.palette() + qPalette.setBrush(QPalette.Window, qPalette.base()) + self.setPalette(qPalette) + + fadeCol = qPalette.text().color() + buttonStyle = ( + "QToolButton {{padding: {0}px; border: none; background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" + ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue()) + + # Widget Label + self.viewLabel = QLabel("%s" % self.tr("Novel Outline")) + self.viewLabel.setContentsMargins(0, 0, 0, 0) + self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Novel Root Menu + self.mRoot = QMenu() + + self.tbRoot = QToolButton(self) + self.tbRoot.setToolTip(self.tr("Novel Root")) + self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])) + self.tbRoot.setIconSize(QSize(iPx, iPx)) + self.tbRoot.setStyleSheet(buttonStyle) + self.tbRoot.setMenu(self.mRoot) + self.tbRoot.setPopupMode(QToolButton.InstantPopup) + + # More Options Menu + self.mMore = QMenu() + + self.tbMore = QToolButton(self) + self.tbMore.setToolTip(self.tr("More Options")) + self.tbMore.setIcon(self.mainTheme.getIcon("menu")) + self.tbMore.setIconSize(QSize(iPx, iPx)) + self.tbMore.setStyleSheet(buttonStyle) + self.tbMore.setMenu(self.mMore) + self.tbMore.setPopupMode(QToolButton.InstantPopup) + + # Assemble + self.outerBox = QHBoxLayout() + self.outerBox.addWidget(self.viewLabel) + self.outerBox.addWidget(self.tbRoot) + self.outerBox.addWidget(self.tbMore) + self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + logger.debug("GuiNovelToolBar initialisation complete") + + return + + ## + # Methods + ## + + def rebuildNovelRootMenu(self, selHandle=None): + """Build the novel root menu. + """ + self.mRoot.clear() + + agRoot = QActionGroup(self.mRoot) + for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(nwItemClass.NOVEL)): + aRoot = self.mRoot.addAction(nwItem.itemName) + aRoot.setData(tHandle) + aRoot.setCheckable(True) + aRoot.triggered.connect( + lambda n, tHandle=tHandle: self.rootFolderSelectionChanged.emit(tHandle) + ) + agRoot.addAction(aRoot) + + if n == 0: + aRoot.setChecked(True) + if selHandle == tHandle: + aRoot.setChecked(True) return @@ -128,41 +255,32 @@ class GuiNovelTree(QTreeWidget): self._lastBuild = 0 # Build GUI + # ========= + iPx = self.mainTheme.baseIconSize - self.setFrameStyle(QFrame.NoFrame) + cMg = self.mainConf.pxInt(6) + self.setIconSize(QSize(iPx, iPx)) + self.setFrameStyle(QFrame.NoFrame) + self.setHeaderHidden(True) self.setIndentation(iPx) self.setColumnCount(3) - self.setHeaderLabels([ - self.tr("Novel Outline"), - self.tr("Words"), - self.tr("POV") - ]) - self.itemDoubleClicked.connect(self._treeDoubleClick) - self.itemSelectionChanged.connect(self._itemSelected) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setExpandsOnDoubleClick(False) self.setDragEnabled(False) - treeHeadItem = self.headerItem() - treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - treeHeadItem.setToolTip(self.C_TITLE, self.tr("Section title")) - treeHeadItem.setToolTip(self.C_WORDS, self.tr("Word count")) - treeHeadItem.setToolTip(self.C_POV, self.tr("Point-of-view character")) - + # Lock the column sizes treeHeader = self.header() - treeHeader.setStretchLastSection(True) - treeHeader.setMinimumSectionSize(iPx + 6) + treeHeader.setStretchLastSection(False) + treeHeader.setMinimumSectionSize(iPx + cMg) + treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch) + treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_POV, QHeaderView.ResizeToContents) - # Get user's column width preferences for NAME and COUNT - treeColWidth = self.mainConf.getNovelColWidths() - if len(treeColWidth) <= 3: - for colN, colW in enumerate(treeColWidth): - self.setColumnWidth(colN, colW) - - # The last column should just auto-scale - self.resizeColumnToContents(self.C_POV) + # Connect signals + self.itemDoubleClicked.connect(self._treeDoubleClick) + self.itemSelectionChanged.connect(self._treeSelectionChange) # Set custom settings self.initSettings() @@ -199,12 +317,15 @@ class GuiNovelTree(QTreeWidget): self._lastBuild = 0 return - def refreshTree(self, overRide=False): + def refreshTree(self, rootHandle=None, overRide=False): """Called whenever the Novel tab is activated. """ logger.verbose("Requesting refresh of the novel tree") + if rootHandle is None: + rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL) + treeChanged = self.mainGui.projView.changedSince(self._lastBuild) - indexChanged = self.theProject.index.indexChangedSince(self._lastBuild) + indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild) if not (treeChanged or indexChanged or overRide): logger.verbose("No changes have been made to the novel index") return @@ -214,7 +335,8 @@ class GuiNovelTree(QTreeWidget): if selItem: titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2] - self._populateTree() + self._populateTree(rootHandle) + self.theProject.setLastNovelViewed(rootHandle) if titleKey is not None and titleKey in self._treeMap: self._treeMap[titleKey].setSelected(True) @@ -268,39 +390,39 @@ class GuiNovelTree(QTreeWidget): if tHandle is None: return - self.mainGui.viewDocument(tHandle) + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") return ## - # Slots + # Private Slots ## - def _treeDoubleClick(self, tItem, tCol): + @pyqtSlot() + def _treeSelectionChange(self): + """Extract the handle and line number of the currently selected + title, and send it to the tree meta panel. + """ + tHandle, _ = self.getSelectedHandle() + if tHandle is not None: + self.novelView.selectedItemChanged.emit(tHandle) + return + + @pyqtSlot("QTreeWidgetItem*", int) + def _treeDoubleClick(self, tItem, colNo): """Extract the handle and line number of the title double- clicked, and send it to the main gui class for opening in the document editor. """ tHandle, tLine = self.getSelectedHandle() - self.mainGui.openDocument(tHandle, tLine=tLine-1, doScroll=True) - return - - def _itemSelected(self): - """Extract the handle and line number of the currently selected - title, and send it to the tree meta panel. - """ - selItems = self.selectedItems() - if selItems: - tHandle = selItems[0].data(self.C_TITLE, Qt.UserRole)[0] - self.mainGui.itemDetails.updateViewBox(tHandle) - + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, tLine, "") return ## # Internal Functions ## - def _populateTree(self): + def _populateTree(self, rootHandle): """Build the tree based on the project index. """ self.clearTree() @@ -309,7 +431,9 @@ class GuiNovelTree(QTreeWidget): currChapter = None currScene = None - for tKey, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True): + logger.verbose("Building novel tree for root item '%s'", rootHandle) + novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) + for tKey, tHandle, sTitle, novIdx in novStruct: tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) self._treeMap[tKey] = tItem diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index b9bfda0d..4f320bfc 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -61,7 +61,7 @@ class GuiProjectView(QWidget): # Signals for user interaction with the project tree selectedItemChanged = pyqtSignal(str) - openDocumentRequest = pyqtSignal(str, Enum) + openDocumentRequest = pyqtSignal(str, Enum, int, str) def __init__(self, mainGui): QWidget.__init__(self, mainGui) @@ -192,10 +192,10 @@ class GuiProjectToolBar(QWidget): "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue()) - # Tree Label - self.projLabel = QLabel("%s" % self.tr("Project Content")) - self.projLabel.setContentsMargins(0, 0, 0, 0) - self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # Widget Label + self.viewLabel = QLabel("%s" % self.tr("Project Content")) + self.viewLabel.setContentsMargins(0, 0, 0, 0) + self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Move Buttons self.tbMoveU = QToolButton(self) @@ -285,7 +285,7 @@ class GuiProjectToolBar(QWidget): # Assemble self.outerBox = QHBoxLayout() - self.outerBox.addWidget(self.projLabel) + self.outerBox.addWidget(self.viewLabel) self.outerBox.addWidget(self.tbMoveU) self.outerBox.addWidget(self.tbMoveD) self.outerBox.addWidget(self.tbAdd) @@ -337,9 +337,8 @@ class GuiProjectTree(QTreeWidget): self._lastMove = {} self._timeChanged = 0 - ## - # Build GUI - ## + # Build GUI + # ========= # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -348,6 +347,7 @@ class GuiProjectTree(QTreeWidget): # Tree Settings iPx = self.mainTheme.baseIconSize cMg = self.mainConf.pxInt(6) + self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) self.setExpandsOnDoubleClick(False) @@ -972,7 +972,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") else: trItem = self._getTreeItem(tHandle) if trItem is not None: @@ -1016,11 +1016,11 @@ class GuiProjectTree(QTreeWidget): if isFile: ctxMenu.addAction( self.tr("Open Document"), - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") ) ctxMenu.addAction( self.tr("View Document"), - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") ) ctxMenu.addSeparator() @@ -1112,7 +1112,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6fa52eb6..e06b9edd 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -205,6 +205,10 @@ class GuiMain(QMainWindow): self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo) self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox) self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem) + self.projView.rootFolderChanged.connect(self.novelView.updateRootItem) + + self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox) + self.novelView.openDocumentRequest.connect(self._openDocument) self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) @@ -362,6 +366,7 @@ class GuiMain(QMainWindow): self.saveProject() self.docEditor.setDictionaries() self.outlineView.updateRootItem(None) + self.novelView.openProjectTasks() self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -509,6 +514,7 @@ class GuiMain(QMainWindow): self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.statusBar.setRefTime(self.theProject.projOpened) self.outlineView.updateRootItem(None) + self.novelView.openProjectTasks() self._updateStatusWordCount() # Restore previously open documents, if any @@ -825,7 +831,7 @@ class GuiMain(QMainWindow): """Rebuild the project tree. """ self.projView.populateTree() - self.novelView.refreshTree() + # self.novelView.refreshTree() return def requestNovelTreeRefresh(self): @@ -1468,15 +1474,15 @@ class GuiMain(QMainWindow): self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}") return - @pyqtSlot(str, Enum) - def _openDocument(self, tHandle, tMode): - """Handle an open document request. + @pyqtSlot(str, Enum, int, str) + def _openDocument(self, tHandle, tMode, tLine, tAnchor): + """Handle an open document request from one of the tree views. """ if tHandle is not None: if tMode == nwDocMode.EDIT: - self.openDocument(tHandle, changeFocus=False) + self.openDocument(tHandle, tLine=tLine, changeFocus=False) elif tMode == nwDocMode.VIEW: - self.viewDocument(tHandle=tHandle) + self.viewDocument(tHandle=tHandle, tAnchor=(tAnchor or None)) return @pyqtSlot(nwView) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index e8ee5c59..af509400 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1345 - 229 - 68286 + 1367 + 231 + 68784 False @@ -17,6 +17,8 @@ True 636b6aa9b697b 636b6aa9b697b + 7031beac91f75 + None 1363 954 409 @@ -70,11 +72,11 @@ Chapter One
- + Making a Scene - + Another Scene @@ -86,7 +88,7 @@ A Note on Structure - + Chapter Two From 88b2d61cec978044e77b310f50e95f921e502bfb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jun 2022 17:54:37 +0200 Subject: [PATCH 146/179] Update tests --- tests/lipsum/nwProject.nwx | 2 ++ tests/minimal/nwProject.nwx | 2 ++ tests/reference/coreProject_NewCustomA_nwProject.nwx | 4 +++- tests/reference/coreProject_NewCustomB_nwProject.nwx | 4 +++- tests/reference/coreProject_NewFileFolder_nwProject.nwx | 4 +++- tests/reference/coreProject_NewMinimal_nwProject.nwx | 4 +++- tests/reference/coreProject_NewRoot_nwProject.nwx | 4 +++- tests/reference/guiEditor_Main_Final_nwProject.nwx | 4 +++- tests/reference/guiEditor_Main_Initial_nwProject.nwx | 4 +++- tests/reference/guiProjSettings_Dialog_nwProject.nwx | 4 +++- tests/test_gui/test_gui_guimain.py | 2 +- tests/test_gui/test_gui_noveltree.py | 4 ++-- 12 files changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index 02e3df49..a8cf6393 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -16,6 +16,8 @@ True 7a992350f3eb6 None + None + None 3847 3109 738 diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index af7595a4..430ecf17 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -17,6 +17,8 @@ True None None + None + None 10 10 0 diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index be4845bf..f046fceb 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -17,6 +17,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 12f911cd..5d02172c 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -17,6 +17,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 4235e9bb..20aeb027 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True None None + None + None 2 1 1 diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 422bb6b2..ba08600f 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -15,6 +15,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index cd25c1cf..0a606137 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index a47d454d..ecc96604 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True 000000000000f None + 0000000000008 + None 129 102 27 diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 03313570..1a79d2c2 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True None None + None + None 9 9 0 diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index f12c1dc2..883cb26d 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -17,6 +17,8 @@ True None None + None + None 9 9 0 diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 9a56af3d..b197885e 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -132,7 +132,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Novel Tree has focus nwGUI.projStack.setCurrentIndex(1) - nwGUI.novelView.refreshTree(True) + nwGUI.novelView.refreshTree(rootHandle=None, overRide=True) with monkeypatch.context() as mp: mp.setattr(GuiNovelView, "treeFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 644839d3..b1e223cf 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -62,7 +62,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): nwGUI.projStack.setCurrentIndex(nwGUI.idxNovelView) nwGUI.rebuildIndex() - novelTree._populateTree() + novelTree._populateTree(rootHandle=None) assert novelTree.topLevelItemCount() == 1 # Rebuild should preserve selection @@ -133,7 +133,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): "#### Section\n\n" )) nwGUI.rebuildIndex() - novelTree._populateTree() + novelTree._populateTree(None) assert novelTree.topLevelItem(0).text(novelTree.C_TITLE) == "Section wo/Scene" assert novelTree.topLevelItem(1).text(novelTree.C_TITLE) == "Scene wo/Chapter" assert novelTree.topLevelItem(2).text(novelTree.C_TITLE) == "Chapter wo/Title" From 80404417574c63dbbf2e74f12163be51394028dc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jun 2022 19:59:18 +0200 Subject: [PATCH 147/179] Allow changing content of last column of the novel tree --- novelwriter/core/options.py | 3 +- novelwriter/gui/noveltree.py | 117 +++++++++++++++++++++++++++++++++-- novelwriter/guimain.py | 1 + 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 144ba4ac..b3724047 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -56,7 +56,8 @@ VALID_MAP = { "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble" }, - "GuiWordList": {"winWidth", "winHeight"} + "GuiWordList": {"winWidth", "winHeight"}, + "GuiNovelView": {"lastCol"}, } diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 8cec9fdb..a23e1e9e 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -39,11 +39,20 @@ from PyQt5.QtWidgets import ( from novelwriter.enum import nwDocMode, nwItemClass from novelwriter.common import checkInt -from novelwriter.constants import nwKeyWords, nwLabels +from novelwriter.constants import nwKeyWords, nwLabels, trConst logger = logging.getLogger(__name__) +class NovelColumnType(Enum): + + HIDDEN = 0 + POV = 1 + FOCUS = 2 + +# END Enum NovelColumnType + + class GuiNovelView(QWidget): # Signals for user interaction with the novel tree @@ -94,7 +103,7 @@ class GuiNovelView(QWidget): return def openProjectTasks(self): - """Run tasks related to opening a project. + """Run tasks when opening a project. """ lastNovel = self.theProject.lastNovel if lastNovel is None: @@ -104,10 +113,17 @@ class GuiNovelView(QWidget): self.clearProject() self.novelBar.rebuildNovelRootMenu(selHandle=lastNovel) + self.novelTree.loadOptions() self.novelTree.refreshTree(rootHandle=lastNovel, overRide=True) return + def closeProjectTasks(self): + """Run tasks when closing a project. + """ + self.novelTree.saveOptions() + return + def setFocus(self): """Forward the set focus call to the tree widget. """ @@ -168,6 +184,14 @@ class GuiNovelToolBar(QWidget): self.viewLabel.setContentsMargins(0, 0, 0, 0) self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # Refresh Button + self.tbRefresh = QToolButton(self) + self.tbRefresh.setToolTip(self.tr("Refresh")) + self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh")) + self.tbRefresh.setIconSize(QSize(iPx, iPx)) + self.tbRefresh.setStyleSheet(buttonStyle) + self.tbRefresh.clicked.connect(self._refreshNovelTree) + # Novel Root Menu self.mRoot = QMenu() @@ -182,6 +206,17 @@ class GuiNovelToolBar(QWidget): # More Options Menu self.mMore = QMenu() + self.mCol3 = self.mMore.addMenu(self.tr("Third Column")) + self.mCol3.addAction(self.tr("Hide Column")).triggered.connect( + lambda: self.novelView.novelTree.setLastColType(NovelColumnType.HIDDEN) + ) + self.mCol3.addAction(self.tr("Point of View Character")).triggered.connect( + lambda: self.novelView.novelTree.setLastColType(NovelColumnType.POV) + ) + self.mCol3.addAction(self.tr("Focus Character")).triggered.connect( + lambda: self.novelView.novelTree.setLastColType(NovelColumnType.FOCUS) + ) + self.tbMore = QToolButton(self) self.tbMore.setToolTip(self.tr("More Options")) self.tbMore.setIcon(self.mainTheme.getIcon("menu")) @@ -193,6 +228,7 @@ class GuiNovelToolBar(QWidget): # Assemble self.outerBox = QHBoxLayout() self.outerBox.addWidget(self.viewLabel) + self.outerBox.addWidget(self.tbRefresh) self.outerBox.addWidget(self.tbRoot) self.outerBox.addWidget(self.tbMore) self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) @@ -230,6 +266,18 @@ class GuiNovelToolBar(QWidget): return + ## + # Private Slots + ## + + @pyqtSlot() + def _refreshNovelTree(self): + """Rebuild the current tree. + """ + rootHandle = self.theProject.lastNovel + self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) + return + # END Class GuiNovelToolBar @@ -237,7 +285,7 @@ class GuiNovelTree(QTreeWidget): C_TITLE = 0 C_WORDS = 1 - C_POV = 2 + C_LAST = 2 def __init__(self, novelView): QTreeWidget.__init__(self, novelView) @@ -253,6 +301,11 @@ class GuiNovelTree(QTreeWidget): # Internal Variables self._treeMap = {} self._lastBuild = 0 + self._lastCol = NovelColumnType.POV + + # Cached i18n Strings + self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) + self._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]) # Build GUI # ========= @@ -276,7 +329,7 @@ class GuiNovelTree(QTreeWidget): treeHeader.setMinimumSectionSize(iPx + cMg) treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch) treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents) - treeHeader.setSectionResizeMode(self.C_POV, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents) # Connect signals self.itemDoubleClicked.connect(self._treeDoubleClick) @@ -317,6 +370,32 @@ class GuiNovelTree(QTreeWidget): self._lastBuild = 0 return + def loadOptions(self): + """Load user options. + """ + try: + lastCol = NovelColumnType[self.theProject.options.getString( + "GuiNovelView", "lastCol", NovelColumnType.POV.name + )] + except Exception: + logger.error("Failed to load last column type from options") + return False + + self._lastCol = lastCol + self.setColumnHidden(self.C_LAST, lastCol == NovelColumnType.HIDDEN) + + return True + + def saveOptions(self): + """Save user options. + """ + try: + self.theProject.options.setValue("GuiNovelView", "lastCol", self._lastCol.name) + except Exception: + logger.error("Failed to save last column type to options") + return False + return True + def refreshTree(self, rootHandle=None, overRide=False): """Called whenever the Novel tab is activated. """ @@ -365,6 +444,16 @@ class GuiNovelTree(QTreeWidget): return tHandle, tLine + def setLastColType(self, colType): + """Change the content type of the last column and rebuild. + """ + if self._lastCol != colType: + logger.debug("Changing last column to %s", colType.name) + self._lastCol = colType + self.setColumnHidden(self.C_LAST, colType == NovelColumnType.HIDDEN) + self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) + return + ## # Events ## @@ -431,6 +520,8 @@ class GuiNovelTree(QTreeWidget): currChapter = None currScene = None + tStart = time() + logger.verbose("Building novel tree for root item '%s'", rootHandle) novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) for tKey, tHandle, sTitle, novIdx in novStruct: @@ -477,6 +568,8 @@ class GuiNovelTree(QTreeWidget): tItem.setExpanded(True) + logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000) + self._lastBuild = time() return @@ -496,8 +589,20 @@ class GuiNovelTree(QTreeWidget): newItem.setText(self.C_WORDS, f"{wC:n}") newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - theRefs = self.theProject.index.getReferences(tHandle, sTitle) - newItem.setText(self.C_POV, ", ".join(theRefs[nwKeyWords.POV_KEY])) + if self._lastCol == NovelColumnType.HIDDEN: + newItem.setText(self.C_LAST, "") + else: + theRefs = self.theProject.index.getReferences(tHandle, sTitle) + if self._lastCol == NovelColumnType.POV: + newText = ", ".join(theRefs[nwKeyWords.POV_KEY]) + newItem.setText(self.C_LAST, newText) + if newText: + newItem.setToolTip(self.C_LAST, f"{self._povLabel}: {newText}") + elif self._lastCol == NovelColumnType.FOCUS: + newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY]) + newItem.setText(self.C_LAST, newText) + if newText: + newItem.setToolTip(self.C_LAST, f"{self._focLabel}: {newText}") return newItem diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index e06b9edd..d3e36e90 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -423,6 +423,7 @@ class GuiMain(QMainWindow): self.closeDocument() self.docViewer.clearNavHistory() self.outlineView.closeOutline() + self.novelView.closeProjectTasks() self.theProject.closeProject(self.idleTime) self.idleRefTime = time() From 49ace054bdb1982c63dfa32a430eff525f771b9f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jun 2022 20:18:24 +0200 Subject: [PATCH 148/179] Make options class handle enums, and add plot column to novel tree --- novelwriter/core/options.py | 19 ++++++++++++++++++- novelwriter/gui/noveltree.py | 33 ++++++++++++++++----------------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index b3724047..d64970ae 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -28,6 +28,8 @@ import os import json import logging +from enum import Enum + from novelwriter.error import logException from novelwriter.common import checkBool, checkFloat, checkInt, checkString from novelwriter.constants import nwFiles @@ -138,7 +140,10 @@ class OptionState(): if group not in self._theState: self._theState[group] = {} - self._theState[group][name] = value + if isinstance(value, Enum): + self._theState[group][name] = value.name + else: + self._theState[group][name] = value return True @@ -187,4 +192,16 @@ class OptionState(): return checkBool(self._theState[group].get(name, default), default) return default + def getEnum(self, group, name, lookup, default): + """Return the value mapped to an enum. Otherwise return the + default value + """ + if issubclass(lookup, Enum): + if group in self._theState: + if name in self._theState[group]: + value = self._theState[group][name] + if value in lookup.__members__: + return lookup[value] + return default + # END Class OptionState diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index a23e1e9e..8bfd207a 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -49,6 +49,7 @@ class NovelColumnType(Enum): HIDDEN = 0 POV = 1 FOCUS = 2 + PLOT = 3 # END Enum NovelColumnType @@ -216,6 +217,9 @@ class GuiNovelToolBar(QWidget): self.mCol3.addAction(self.tr("Focus Character")).triggered.connect( lambda: self.novelView.novelTree.setLastColType(NovelColumnType.FOCUS) ) + self.mCol3.addAction(self.tr("Novel Plot")).triggered.connect( + lambda: self.novelView.novelTree.setLastColType(NovelColumnType.PLOT) + ) self.tbMore = QToolButton(self) self.tbMore.setToolTip(self.tr("More Options")) @@ -306,6 +310,7 @@ class GuiNovelTree(QTreeWidget): # Cached i18n Strings self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) self._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]) + self._pltLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]) # Build GUI # ========= @@ -373,28 +378,17 @@ class GuiNovelTree(QTreeWidget): def loadOptions(self): """Load user options. """ - try: - lastCol = NovelColumnType[self.theProject.options.getString( - "GuiNovelView", "lastCol", NovelColumnType.POV.name - )] - except Exception: - logger.error("Failed to load last column type from options") - return False - - self._lastCol = lastCol - self.setColumnHidden(self.C_LAST, lastCol == NovelColumnType.HIDDEN) - + self._lastCol = self.theProject.options.getEnum( + "GuiNovelView", "lastCol", NovelColumnType, NovelColumnType.POV + ) + self.setColumnHidden(self.C_LAST, self._lastCol == NovelColumnType.HIDDEN) return True def saveOptions(self): """Save user options. """ - try: - self.theProject.options.setValue("GuiNovelView", "lastCol", self._lastCol.name) - except Exception: - logger.error("Failed to save last column type to options") - return False - return True + self.theProject.options.setValue("GuiNovelView", "lastCol", self._lastCol) + return def refreshTree(self, rootHandle=None, overRide=False): """Called whenever the Novel tab is activated. @@ -603,6 +597,11 @@ class GuiNovelTree(QTreeWidget): newItem.setText(self.C_LAST, newText) if newText: newItem.setToolTip(self.C_LAST, f"{self._focLabel}: {newText}") + elif self._lastCol == NovelColumnType.PLOT: + newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY]) + newItem.setText(self.C_LAST, newText) + if newText: + newItem.setToolTip(self.C_LAST, f"{self._pltLabel}: {newText}") return newItem From ea61db9591316db21774840411e7fd4a021ec608 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 14 Jun 2022 00:14:51 +0200 Subject: [PATCH 149/179] Simplify the novel outline tree --- novelwriter/constants.py | 9 ++ novelwriter/core/index.py | 20 ++--- novelwriter/core/project.py | 2 +- novelwriter/gui/noveltree.py | 164 ++++++++++++++++------------------- novelwriter/guimain.py | 20 ++--- 5 files changed, 101 insertions(+), 114 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index dc62dbd3..3d1b5c13 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -57,6 +57,15 @@ class nwRegEx: # END Class nwRegEx +class nwHeaders: + + H_VALID = ("H0", "H1", "H2", "H3", "H4") + H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} + TT_NONE = "T000000" + +# END Class nwHeaders + + class nwFiles: PROJ_FILE = "nwProject.nwx" diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index ee9ea089..0b3afdc5 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -34,7 +34,7 @@ from time import time from novelwriter.enum import nwItemType, nwItemLayout from novelwriter.error import logException -from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode +from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders from novelwriter.core.document import NWDoc from novelwriter.common import ( checkInt, isHandle, isItemClass, isTitleTag, jsonEncode @@ -42,10 +42,6 @@ from novelwriter.common import ( logger = logging.getLogger(__name__) -H_VALID = ("H0", "H1", "H2", "H3", "H4") -H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} -TT_NONE = "T000000" - class NWIndex: """This class holds the entire index for a given project. The index @@ -477,7 +473,7 @@ class NWIndex: """ hCount = [0, 0, 0, 0, 0] for _, _, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): - iLevel = H_LEVEL.get(hItem.level, 0) + iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) hCount[iLevel] += 1 return hCount @@ -510,7 +506,7 @@ class NWIndex: pKey = None for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): tKey = f"{tHandle}:{sTitle}" - iLevel = H_LEVEL.get(hItem.level, 0) + iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if iLevel > maxDepth: if pKey in tData: tData[pKey]["words"] += hItem.wordCount @@ -660,7 +656,7 @@ class TagsIndex: """ if tagKey in self._tags: return self._tags.get(tagKey).get("heading") - return TT_NONE + return nwHeaders.TT_NONE def tagClass(self, tagKey): """Get the class of a given tag. @@ -906,7 +902,7 @@ class IndexItem: self._index = 0 # Add a placeholder heading - self._headings[TT_NONE] = IndexHeading(TT_NONE) + self._headings[nwHeaders.TT_NONE] = IndexHeading(nwHeaders.TT_NONE) return @@ -940,8 +936,8 @@ class IndexItem: """Add a heading to the item. Also remove the placeholder entry if it exists. """ - if TT_NONE in self._headings: - self._headings.pop(TT_NONE) + if nwHeaders.TT_NONE in self._headings: + self._headings.pop(nwHeaders.TT_NONE) self._headings[tHeading.key] = tHeading return @@ -1110,7 +1106,7 @@ class IndexHeading: def setLevel(self, level): """Set the level of the header if it's a valid value. """ - if level in H_VALID: + if level in nwHeaders.H_VALID: self._level = level return diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 43f3297e..fb81f033 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -653,7 +653,7 @@ class NWProject(): self._legacyDataFolder(projItem) except Exception: self.mainGui.makeAlert(self.tr( - "There was an error while converting project version 1.0. " + "There was an error updating the project. " "Some data may not have been preserved." ), nwAlert.ERROR) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 8bfd207a..33981243 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -4,7 +4,9 @@ novelWriter – GUI Novel Tree GUI classe for the main window novel tree File History: -Created: 2020-12-20 [1.1a0] +Created: 2020-12-20 [1.1a0] GuiNovelTree +Created: 2022-06-12 [1.7b1] GuiNovelView +Created: 2022-06-12 [1.7b1] GuiNovelToolBar This file is a part of novelWriter Copyright 2018–2020, Veronica Berglyd Olsen @@ -30,7 +32,7 @@ from time import time from enum import Enum from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal -from PyQt5.QtGui import QPalette +from PyQt5.QtGui import QPalette, QPixmap, QColor from PyQt5.QtWidgets import ( QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, @@ -39,19 +41,19 @@ from PyQt5.QtWidgets import ( from novelwriter.enum import nwDocMode, nwItemClass from novelwriter.common import checkInt -from novelwriter.constants import nwKeyWords, nwLabels, trConst +from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst logger = logging.getLogger(__name__) -class NovelColumnType(Enum): +class NovelTreeColumn(Enum): HIDDEN = 0 POV = 1 FOCUS = 2 PLOT = 3 -# END Enum NovelColumnType +# END Enum NovelTreeColumn class GuiNovelView(QWidget): @@ -209,16 +211,16 @@ class GuiNovelToolBar(QWidget): self.mCol3 = self.mMore.addMenu(self.tr("Third Column")) self.mCol3.addAction(self.tr("Hide Column")).triggered.connect( - lambda: self.novelView.novelTree.setLastColType(NovelColumnType.HIDDEN) + lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.HIDDEN) ) self.mCol3.addAction(self.tr("Point of View Character")).triggered.connect( - lambda: self.novelView.novelTree.setLastColType(NovelColumnType.POV) + lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.POV) ) self.mCol3.addAction(self.tr("Focus Character")).triggered.connect( - lambda: self.novelView.novelTree.setLastColType(NovelColumnType.FOCUS) + lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.FOCUS) ) self.mCol3.addAction(self.tr("Novel Plot")).triggered.connect( - lambda: self.novelView.novelTree.setLastColType(NovelColumnType.PLOT) + lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.PLOT) ) self.tbMore = QToolButton(self) @@ -305,9 +307,9 @@ class GuiNovelTree(QTreeWidget): # Internal Variables self._treeMap = {} self._lastBuild = 0 - self._lastCol = NovelColumnType.POV + self._lastCol = NovelTreeColumn.POV - # Cached i18n Strings + # Cached Strings self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) self._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]) self._pltLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]) @@ -316,12 +318,15 @@ class GuiNovelTree(QTreeWidget): # ========= iPx = self.mainTheme.baseIconSize + nPx = self.mainTheme.textNWidth cMg = self.mainConf.pxInt(6) + mPx = self.mainConf.pxInt(4) + nMg = self.mainConf.pxInt(6) - self.setIconSize(QSize(iPx, iPx)) + # self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) self.setHeaderHidden(True) - self.setIndentation(iPx) + self.setIndentation(mPx) self.setColumnCount(3) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) @@ -336,6 +341,25 @@ class GuiNovelTree(QTreeWidget): treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents) treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents) + # Pre-Generate Tree Formatting + fH1 = self.font() + fH1.setBold(True) + fH1.setUnderline(True) + + fH2 = self.font() + fH2.setBold(True) + + self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()] + self._hIndent = ["", "", "", "\u2022\u00a0", "\u00bb\u00a0"] + self._pIndent = [QPixmap(), QPixmap()] + + hPix = QPixmap(QSize(iPx, iPx)) + hPix.fill(QColor(0, 0, 0, 0)) + for m in range(1, 4): + self._pIndent.append(hPix.scaled( + max(nPx*m - nMg, nMg), 2, Qt.IgnoreAspectRatio, Qt.FastTransformation + )) + # Connect signals self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._treeSelectionChange) @@ -379,9 +403,9 @@ class GuiNovelTree(QTreeWidget): """Load user options. """ self._lastCol = self.theProject.options.getEnum( - "GuiNovelView", "lastCol", NovelColumnType, NovelColumnType.POV + "GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.POV ) - self.setColumnHidden(self.C_LAST, self._lastCol == NovelColumnType.HIDDEN) + self.setColumnHidden(self.C_LAST, self._lastCol == NovelTreeColumn.HIDDEN) return True def saveOptions(self): @@ -444,7 +468,7 @@ class GuiNovelTree(QTreeWidget): if self._lastCol != colType: logger.debug("Changing last column to %s", colType.name) self._lastCol = colType - self.setColumnHidden(self.C_LAST, colType == NovelColumnType.HIDDEN) + self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN) self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) return @@ -509,100 +533,58 @@ class GuiNovelTree(QTreeWidget): """Build the tree based on the project index. """ self.clearTree() - - currTitle = None - currChapter = None - currScene = None - tStart = time() - logger.verbose("Building novel tree for root item '%s'", rootHandle) + novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) for tKey, tHandle, sTitle, novIdx in novStruct: - tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) - self._treeMap[tKey] = tItem + iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0) + if iLevel == 0: + continue - tLevel = novIdx.level - if tLevel == "H1": - self.addTopLevelItem(tItem) - currTitle = tItem - currChapter = None - currScene = None + newItem = QTreeWidgetItem() + theData = (tHandle, sTitle[1:].lstrip("0"), tKey) - elif tLevel == "H2": - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - currChapter = tItem - currScene = None + newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel]) + newItem.setText(self.C_TITLE, self._hIndent[iLevel] + novIdx.title) + newItem.setData(self.C_TITLE, Qt.UserRole, theData) + newItem.setFont(self.C_TITLE, self._hFonts[iLevel]) + newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}") + newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - elif tLevel == "H3": - if currChapter is None: - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - else: - currChapter.addChild(tItem) - currScene = tItem + lastText, toolTip = self._getLastColumnText(tHandle, sTitle) + newItem.setText(self.C_LAST, lastText) + if lastText: + newItem.setToolTip(self.C_LAST, toolTip) - elif tLevel == "H4": - if currScene is None: - if currChapter is None: - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - else: - currChapter.addChild(tItem) - else: - currScene.addChild(tItem) - - tItem.setExpanded(True) + self._treeMap[tKey] = newItem + self.addTopLevelItem(newItem) logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000) - self._lastBuild = time() return - def _createTreeItem(self, tHandle, sTitle, titleKey, novIdx): - """Populate a tree item with all the column values. + def _getLastColumnText(self, tHandle, sTitle): + """Generate the text for the last column based on user settings. """ - newItem = QTreeWidgetItem() - hIcon = "doc_%s" % novIdx.level.lower() - theData = (tHandle, sTitle[1:].lstrip("0"), titleKey) + if self._lastCol == NovelTreeColumn.HIDDEN: + return "", "" - wC = int(novIdx.wordCount) + theRefs = self.theProject.index.getReferences(tHandle, sTitle) + if self._lastCol == NovelTreeColumn.POV: + newText = ", ".join(theRefs[nwKeyWords.POV_KEY]) + return newText, f"{self._povLabel}: {newText}" - newItem.setText(self.C_TITLE, novIdx.title) - newItem.setData(self.C_TITLE, Qt.UserRole, theData) - newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon(hIcon)) - newItem.setText(self.C_WORDS, f"{wC:n}") - newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) + elif self._lastCol == NovelTreeColumn.FOCUS: + newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY]) + return newText, f"{self._focLabel}: {newText}" - if self._lastCol == NovelColumnType.HIDDEN: - newItem.setText(self.C_LAST, "") - else: - theRefs = self.theProject.index.getReferences(tHandle, sTitle) - if self._lastCol == NovelColumnType.POV: - newText = ", ".join(theRefs[nwKeyWords.POV_KEY]) - newItem.setText(self.C_LAST, newText) - if newText: - newItem.setToolTip(self.C_LAST, f"{self._povLabel}: {newText}") - elif self._lastCol == NovelColumnType.FOCUS: - newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY]) - newItem.setText(self.C_LAST, newText) - if newText: - newItem.setToolTip(self.C_LAST, f"{self._focLabel}: {newText}") - elif self._lastCol == NovelColumnType.PLOT: - newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY]) - newItem.setText(self.C_LAST, newText) - if newText: - newItem.setToolTip(self.C_LAST, f"{self._pltLabel}: {newText}") + elif self._lastCol == NovelTreeColumn.PLOT: + newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY]) + return newText, f"{self._pltLabel}: {newText}" - return newItem + return "", "" # END Class GuiNovelTree diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index d3e36e90..71d99e33 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1582,30 +1582,30 @@ class GuiMain(QMainWindow): return @pyqtSlot(int) - def _mainStackChanged(self, tabIndex): + def _mainStackChanged(self, stIndex): """Activated when the main window tab is changed. """ - if tabIndex == self.idxEditorView: - logger.verbose("Editor tab activated") - elif tabIndex == self.idxOutlineView: - logger.verbose("Project outline tab activated") + if stIndex == self.idxEditorView: + logger.verbose("Editor View activated") + elif stIndex == self.idxOutlineView: + logger.verbose("Outline View activated") if self.hasProject: self.outlineView.refreshView() return @pyqtSlot(int) - def _projStackChanged(self, tabIndex): + def _projStackChanged(self, stIndex): """Activated when the project view tab is changed. """ sHandle = None - if tabIndex == self.idxProjView: - logger.verbose("Project tree tab activated") + if stIndex == self.idxProjView: + logger.verbose("Project Tree View activated") sHandle = self.projView.getSelectedHandle() - elif tabIndex == self.idxNovelView: - logger.verbose("Novel tree tab activated") + elif stIndex == self.idxNovelView: + logger.verbose("Novel Tree View activated") if self.hasProject: self.novelView.refreshTree() sHandle, _ = self.novelView.getSelectedHandle() From 538b722bdf7750921df9668d2e2ab40ba2c98b09 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 14 Jun 2022 19:33:12 +0200 Subject: [PATCH 150/179] Fix tests and a few other bits --- novelwriter/gui/doceditor.py | 2 + novelwriter/gui/noveltree.py | 1 - sample/content/a520879ca0b45.nwd | 2 +- sample/content/edca4be2fcaf8.nwd | 2 +- tests/test_core/test_core_project.py | 2 +- tests/test_gui/test_gui_guimain.py | 12 ++--- tests/test_gui/test_gui_noveltree.py | 66 +++++----------------------- 7 files changed, 22 insertions(+), 65 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 25552037..0d32689e 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -511,11 +511,13 @@ class GuiDocEditor(QTextEdit): self.theProject.index.scanText(tHandle, docText) newHeader = self.theProject.index.getHandleHeaderLevel(tHandle) + # ToDo: This should be a signal if self._updateHeaders(checkLevel=True): self.mainGui.requestNovelTreeRefresh() else: self.mainGui.novelView.updateWordCounts(tHandle) + # ToDo: This should be a signal if oldHeader != newHeader: self.mainGui.projView.setTreeItemValues(tHandle) self.mainGui.itemDetails.updateViewBox(tHandle) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 33981243..336058a5 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -254,7 +254,6 @@ class GuiNovelToolBar(QWidget): """Build the novel root menu. """ self.mRoot.clear() - agRoot = QActionGroup(self.mRoot) for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(nwItemClass.NOVEL)): aRoot = self.mRoot.addAction(nwItem.itemName) diff --git a/sample/content/a520879ca0b45.nwd b/sample/content/a520879ca0b45.nwd index 12d7a62a..75ba39c8 100644 --- a/sample/content/a520879ca0b45.nwd +++ b/sample/content/a520879ca0b45.nwd @@ -1,7 +1,7 @@ %%~name: Chapter One %%~path: e5e47ebf63b1c/a520879ca0b45 %%~kind: NOVEL/DOCUMENT -### Chapter One +## Chapter One @pov: Jane diff --git a/sample/content/edca4be2fcaf8.nwd b/sample/content/edca4be2fcaf8.nwd index 3028b3fd..8b3efc78 100644 --- a/sample/content/edca4be2fcaf8.nwd +++ b/sample/content/edca4be2fcaf8.nwd @@ -3,4 +3,4 @@ %%~kind: NOVEL/DOCUMENT # Part One ->> In the beginning … << \ No newline at end of file +>> In the beginning … << diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index bdc27771..a6409b98 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -470,7 +470,7 @@ def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI): writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.bak"), "stuff") mockGUI.clear() assert theProject.openProject(nwMinimal) is True - assert "version 1.0" in mockGUI.lastAlert + assert "There was an error updating the project." in mockGUI.lastAlert assert theProject.closeProject() diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index b197885e..c308451b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -29,7 +29,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QInputDialog from novelwriter.gui import GuiDocEditor, GuiNovelView, GuiOutlineView -from novelwriter.enum import nwItemType, nwWidget +from novelwriter.enum import nwItemType, nwView, nwWidget from novelwriter.tools import GuiProjectWizard from novelwriter.gui.projtree import GuiProjectTree from novelwriter.dialogs import GuiEditLabel @@ -120,6 +120,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.openSelectedItem() is False # Project Tree has focus + nwGUI._changeView(nwView.PROJECT) nwGUI.switchFocus(nwWidget.TREE) nwGUI.projStack.setCurrentIndex(0) with monkeypatch.context() as mp: @@ -131,20 +132,19 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.closeDocument() is True # Novel Tree has focus - nwGUI.projStack.setCurrentIndex(1) + nwGUI._changeView(nwView.NOVEL) nwGUI.novelView.refreshTree(rootHandle=None, overRide=True) with monkeypatch.context() as mp: mp.setattr(GuiNovelView, "treeFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - actItem = nwGUI.novelView.novelTree.topLevelItem(0) - chpItem = actItem.child(0) - selItem = chpItem.child(0) + selItem = nwGUI.novelView.novelTree.topLevelItem(2) nwGUI.novelView.novelTree.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True # Project Outline has focus + nwGUI._changeView(nwView.OUTLINE) nwGUI.switchFocus(nwWidget.OUTLINE) with monkeypatch.context() as mp: mp.setattr(GuiOutlineView, "treeFocus", lambda *a: True) @@ -157,7 +157,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True - # qtbot.stopForInteraction() + # qtbot.stop() # END Test testGuiMain_ProjectTreeItems diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index b1e223cf..ea8b49bc 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -20,9 +20,6 @@ along with this program. If not, see . """ import pytest -import os - -from tools import writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox @@ -40,9 +37,8 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): novelView = nwGUI.novelView novelTree = novelView.novelTree - ## - # Show/Hide Scrollbars - ## + # Show/Hide Scrollbars + # ==================== nwGUI.mainConf.hideVScroll = True nwGUI.mainConf.hideHScroll = True @@ -56,14 +52,13 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): assert novelTree.verticalScrollBar().isEnabled() assert novelTree.horizontalScrollBar().isEnabled() - ## - # Populate Tree - ## + # Populate Tree + # ============= nwGUI.projStack.setCurrentIndex(nwGUI.idxNovelView) nwGUI.rebuildIndex() novelTree._populateTree(rootHandle=None) - assert novelTree.topLevelItemCount() == 1 + assert novelTree.topLevelItemCount() == 3 # Rebuild should preserve selection topItem = novelTree.topLevelItem(0) @@ -75,13 +70,12 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): novelView.refreshTree() assert novelTree.topLevelItem(0).isSelected() - ## - # Open Items - ## + # Open Items + # ========== # Clear selection novelTree.clearSelection() - scItem = novelTree.topLevelItem(0).child(0).child(0) + scItem = novelTree.topLevelItem(2) scItem.setSelected(True) assert scItem.isSelected() @@ -114,48 +108,10 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) assert nwGUI.docViewer.docHandle() == "8c659a11cd429" - ## - # Populate Tree - ## + # Close + # ===== - # Add weird titles to first file to check hnadling of non-standard - # order of title levels. - writeFile(os.path.join(nwMinimal, "content", "a35baf2e93843.nwd"), ( - "#### Section wo/Scene\n\n" - "### Scene wo/Chapter\n\n" - "## Chapter wo/Title\n\n" - "# Title\n\n" - "#### Section w/Title, wo/Scene\n\n" - "### Scene w/Title, wo/Chapter\n\n" - "## Chapter\n\n" - "#### Section w/Chapter, wo/Scene\n\n" - "### Scene\n\n" - "#### Section\n\n" - )) - nwGUI.rebuildIndex() - novelTree._populateTree(None) - assert novelTree.topLevelItem(0).text(novelTree.C_TITLE) == "Section wo/Scene" - assert novelTree.topLevelItem(1).text(novelTree.C_TITLE) == "Scene wo/Chapter" - assert novelTree.topLevelItem(2).text(novelTree.C_TITLE) == "Chapter wo/Title" - assert novelTree.topLevelItem(3).text(novelTree.C_TITLE) == "Title" - - tTitle = novelTree.topLevelItem(3) - assert tTitle.child(0).text(novelTree.C_TITLE) == "Section w/Title, wo/Scene" - assert tTitle.child(1).text(novelTree.C_TITLE) == "Scene w/Title, wo/Chapter" - assert tTitle.child(2).text(novelTree.C_TITLE) == "Chapter" - - tChap = tTitle.child(2) - assert tChap.child(0).text(novelTree.C_TITLE) == "Section w/Chapter, wo/Scene" - assert tChap.child(1).text(novelTree.C_TITLE) == "Scene" - - tScene = tChap.child(1) - assert tScene.child(0).text(novelTree.C_TITLE) == "Section" - - ## - # Close - ## - - # qtbot.stopForInteraction() + # qtbot.stop() nwGUI.closeProject() # END Test testGuiNovelTree_TreeItems From 3a061eec3f43f8c253faf2864c1f365efa65045e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 16:14:28 +0200 Subject: [PATCH 151/179] Update novel tree menu processing and fix issue with reaload of document --- novelwriter/gui/noveltree.py | 159 +++++++++++++++++------------ novelwriter/gui/outline.py | 2 +- novelwriter/gui/projtree.py | 29 ++---- novelwriter/guimain.py | 6 +- tests/test_gui/test_gui_guimain.py | 4 +- tests/test_gui/test_gui_outline.py | 2 +- 6 files changed, 110 insertions(+), 92 deletions(-) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 336058a5..2332bc7c 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -28,11 +28,11 @@ along with this program. If not, see . import logging import novelwriter -from time import time from enum import Enum +from time import time -from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtGui import QPalette, QPixmap, QColor +from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtWidgets import ( QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, @@ -81,13 +81,7 @@ class GuiNovelView(QWidget): self.setLayout(self.outerBox) - # Connect Signals - self.novelBar.rootFolderSelectionChanged.connect( - lambda tHandle: self.novelTree.refreshTree(rootHandle=tHandle, overRide=True) - ) - # Function Mappings - self.refreshTree = self.novelTree.refreshTree self.updateWordCounts = self.novelTree.updateWordCounts self.getSelectedHandle = self.novelTree.getSelectedHandle @@ -101,30 +95,44 @@ class GuiNovelView(QWidget): self.novelTree.initSettings() return + def refreshTree(self): + """Refresh the current tree. + """ + self.novelTree.refreshTree(rootHandle=self.theProject.lastNovel) + return + def clearProject(self): - self.novelTree.clearTree() + """Clear project-related GUI content. + """ + self.novelTree.clearContent() + self.novelBar.clearContent() return def openProjectTasks(self): - """Run tasks when opening a project. + """Run opening project tasks. """ lastNovel = self.theProject.lastNovel - if lastNovel is None: + if lastNovel not in self.theProject.tree: lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL) logger.debug("Setting novel tree to root item '%s'", lastNovel) + lastCol = self.theProject.options.getEnum( + "GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.HIDDEN + ) + self.clearProject() - self.novelBar.rebuildNovelRootMenu(selHandle=lastNovel) - self.novelTree.loadOptions() - self.novelTree.refreshTree(rootHandle=lastNovel, overRide=True) + self.novelBar.buildNovelRootMenu() + self.novelBar.setLastColType(lastCol, doRefresh=False) + self.novelBar.setCurrentRoot(lastNovel) return def closeProjectTasks(self): - """Run tasks when closing a project. + """Run closing project tasks. """ - self.novelTree.saveOptions() + lastColType = self.novelTree.lastColType + self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType) return def setFocus(self): @@ -133,7 +141,7 @@ class GuiNovelView(QWidget): self.novelTree.setFocus() return - def treeFocus(self): + def treeHasFocus(self): """Check if the novel tree has focus. """ return self.novelTree.hasFocus() @@ -144,9 +152,9 @@ class GuiNovelView(QWidget): @pyqtSlot(str) def updateRootItem(self, tHandle): - """Should be called whenever a root folders changes. + """If any root item changes, rebuild the novel root menu. """ - self.novelBar.rebuildNovelRootMenu() + self.novelBar.buildNovelRootMenu() return # END Class GuiNovelView @@ -154,8 +162,6 @@ class GuiNovelView(QWidget): class GuiNovelToolBar(QWidget): - rootFolderSelectionChanged = pyqtSignal(str) - def __init__(self, novelView): QTreeWidget.__init__(self, novelView) @@ -167,7 +173,7 @@ class GuiNovelToolBar(QWidget): self.mainTheme = novelView.mainGui.mainTheme iPx = self.mainTheme.baseIconSize - mPx = self.mainConf.pxInt(4) + mPx = self.mainConf.pxInt(3) self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) @@ -197,6 +203,8 @@ class GuiNovelToolBar(QWidget): # Novel Root Menu self.mRoot = QMenu() + self.gRoot = QActionGroup(self.mRoot) + self.aRoot = {} self.tbRoot = QToolButton(self) self.tbRoot.setToolTip(self.tr("Novel Root")) @@ -209,19 +217,13 @@ class GuiNovelToolBar(QWidget): # More Options Menu self.mMore = QMenu() - self.mCol3 = self.mMore.addMenu(self.tr("Third Column")) - self.mCol3.addAction(self.tr("Hide Column")).triggered.connect( - lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.HIDDEN) - ) - self.mCol3.addAction(self.tr("Point of View Character")).triggered.connect( - lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.POV) - ) - self.mCol3.addAction(self.tr("Focus Character")).triggered.connect( - lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.FOCUS) - ) - self.mCol3.addAction(self.tr("Novel Plot")).triggered.connect( - lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.PLOT) - ) + self.mLastCol = self.mMore.addMenu(self.tr("Last Column")) + self.gLastCol = QActionGroup(self.mMore) + self.aLastCol = {} + self._addLastColAction(NovelTreeColumn.HIDDEN, self.tr("Hidden")) + self._addLastColAction(NovelTreeColumn.POV, self.tr("Point of View Character")) + self._addLastColAction(NovelTreeColumn.FOCUS, self.tr("Focus Character")) + self._addLastColAction(NovelTreeColumn.PLOT, self.tr("Novel Plot")) self.tbMore = QToolButton(self) self.tbMore.setToolTip(self.tr("More Options")) @@ -250,25 +252,41 @@ class GuiNovelToolBar(QWidget): # Methods ## - def rebuildNovelRootMenu(self, selHandle=None): + def clearContent(self): + """Run clearing project tasks. + """ + self.mRoot.clear() + self.aRoot = {} + return + + def buildNovelRootMenu(self): """Build the novel root menu. """ self.mRoot.clear() - agRoot = QActionGroup(self.mRoot) + self.aRoot = {} for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(nwItemClass.NOVEL)): aRoot = self.mRoot.addAction(nwItem.itemName) aRoot.setData(tHandle) aRoot.setCheckable(True) - aRoot.triggered.connect( - lambda n, tHandle=tHandle: self.rootFolderSelectionChanged.emit(tHandle) - ) - agRoot.addAction(aRoot) + aRoot.triggered.connect(lambda n, tHandle=tHandle: self.setCurrentRoot(tHandle)) + self.gRoot.addAction(aRoot) + self.aRoot[tHandle] = aRoot - if n == 0: - aRoot.setChecked(True) - if selHandle == tHandle: - aRoot.setChecked(True) + return + def setCurrentRoot(self, rootHandle): + """Set the current active root handle. + """ + if rootHandle in self.aRoot: + self.aRoot[rootHandle].setChecked(True) + self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) + return + + def setLastColType(self, colType, doRefresh=True): + """Set the last column type. + """ + self.aLastCol[colType].setChecked(True) + self.novelView.novelTree.setLastColType(colType, doRefresh=doRefresh) return ## @@ -283,6 +301,20 @@ class GuiNovelToolBar(QWidget): self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return + ## + # Internal Functions + ## + + def _addLastColAction(self, colType, actionLabel): + """Add a column selection entry to the last column menu. + """ + aLast = self.mLastCol.addAction(actionLabel) + aLast.setCheckable(True) + aLast.setActionGroup(self.gLastCol) + aLast.triggered.connect(lambda: self.setLastColType(colType)) + self.aLastCol[colType] = aLast + return + # END Class GuiNovelToolBar @@ -319,13 +351,12 @@ class GuiNovelTree(QTreeWidget): iPx = self.mainTheme.baseIconSize nPx = self.mainTheme.textNWidth cMg = self.mainConf.pxInt(6) - mPx = self.mainConf.pxInt(4) nMg = self.mainConf.pxInt(6) # self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) self.setHeaderHidden(True) - self.setIndentation(mPx) + self.setIndentation(0) self.setColumnCount(3) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) @@ -349,7 +380,7 @@ class GuiNovelTree(QTreeWidget): fH2.setBold(True) self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()] - self._hIndent = ["", "", "", "\u2022\u00a0", "\u00bb\u00a0"] + self._hIndent = ["", "", "", "\u203a\u00a0", "\u00bb\u00a0"] self._pIndent = [QPixmap(), QPixmap()] hPix = QPixmap(QSize(iPx, iPx)) @@ -386,11 +417,19 @@ class GuiNovelTree(QTreeWidget): return + ## + # Properties + ## + + @property + def lastColType(self): + return self._lastCol + ## # Class Methods ## - def clearTree(self): + def clearContent(self): """Clear the GUI content and the related maps. """ self.clear() @@ -398,21 +437,6 @@ class GuiNovelTree(QTreeWidget): self._lastBuild = 0 return - def loadOptions(self): - """Load user options. - """ - self._lastCol = self.theProject.options.getEnum( - "GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.POV - ) - self.setColumnHidden(self.C_LAST, self._lastCol == NovelTreeColumn.HIDDEN) - return True - - def saveOptions(self): - """Save user options. - """ - self.theProject.options.setValue("GuiNovelView", "lastCol", self._lastCol) - return - def refreshTree(self, rootHandle=None, overRide=False): """Called whenever the Novel tab is activated. """ @@ -461,14 +485,15 @@ class GuiNovelTree(QTreeWidget): return tHandle, tLine - def setLastColType(self, colType): + def setLastColType(self, colType, doRefresh=True): """Change the content type of the last column and rebuild. """ if self._lastCol != colType: logger.debug("Changing last column to %s", colType.name) self._lastCol = colType self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN) - self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) + if doRefresh: + self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) return ## @@ -531,7 +556,7 @@ class GuiNovelTree(QTreeWidget): def _populateTree(self, rootHandle): """Build the tree based on the project index. """ - self.clearTree() + self.clearContent() tStart = time() logger.verbose("Building novel tree for root item '%s'", rootHandle) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index c858c480..4b2077f6 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -117,7 +117,7 @@ class GuiOutlineView(QWidget): self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged) return - def treeFocus(self): + def treeHasFocus(self): return self.outlineTree.hasFocus() def setTreeFocus(self): diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 4f320bfc..08b24232 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -31,8 +31,8 @@ import novelwriter from enum import Enum from time import time +from PyQt5.QtGui import QPalette from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, @@ -142,7 +142,7 @@ class GuiProjectView(QWidget): self.projTree.setFocus() return - def treeFocus(self): + def treeHasFocus(self): """Check if the project tree has focus. """ return self.projTree.hasFocus() @@ -177,7 +177,7 @@ class GuiProjectToolBar(QWidget): self.mainTheme = projView.mainGui.mainTheme iPx = self.mainTheme.baseIconSize - mPx = self.mainConf.pxInt(4) + mPx = self.mainConf.pxInt(3) self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) @@ -252,7 +252,7 @@ class GuiProjectToolBar(QWidget): self._addRootFolderEntry(nwItemClass.PLOT) self._addRootFolderEntry(nwItemClass.CHARACTER) self._addRootFolderEntry(nwItemClass.WORLD) - self._addRootFolderEntry(nwItemClass.ARCHIVE) + self._addRootFolderEntry(nwItemClass.TIMELINE) self._addRootFolderEntry(nwItemClass.OBJECT) self._addRootFolderEntry(nwItemClass.ENTITY) self._addRootFolderEntry(nwItemClass.CUSTOM) @@ -778,13 +778,6 @@ class GuiProjectTree(QTreeWidget): if trItem is None or nwItem is None: return - expIcon = QIcon() - if nwItem.itemType == nwItemType.FILE: - if nwItem.isExported: - expIcon = self.mainTheme.getIcon("check") - else: - expIcon = self.mainTheme.getIcon("cross") - itemStatus, statusIcon = nwItem.getImportStatus() hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) itemIcon = self.mainTheme.getItemIcon( @@ -793,18 +786,18 @@ class GuiProjectTree(QTreeWidget): trItem.setIcon(self.C_NAME, itemIcon) trItem.setText(self.C_NAME, nwItem.itemName) - trItem.setIcon(self.C_EXPORT, expIcon) trItem.setIcon(self.C_STATUS, statusIcon) trItem.setToolTip(self.C_STATUS, itemStatus) + if nwItem.itemType == nwItemType.FILE: + trItem.setIcon( + self.C_EXPORT, self.mainTheme.getIcon("check" if nwItem.isExported else "cross") + ) + if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT: trFont = trItem.font(self.C_NAME) - if hLevel in ("H1", "H2"): - trFont.setBold(True) - trFont.setUnderline(True) - else: - trFont.setBold(False) - trFont.setUnderline(False) + trFont.setBold(hLevel == "H1" or hLevel == "H2") + trFont.setUnderline(hLevel == "H1") trItem.setFont(self.C_NAME, trFont) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 71d99e33..5030347b 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -796,11 +796,11 @@ class GuiMain(QMainWindow): tHandle = None tLine = None - if self.projView.treeFocus(): + if self.projView.treeHasFocus(): tHandle = self.projView.getSelectedHandle() - elif self.novelView.treeFocus(): + elif self.novelView.treeHasFocus(): tHandle, tLine = self.novelView.getSelectedHandle() - elif self.outlineView.treeFocus(): + elif self.outlineView.treeHasFocus(): tHandle, tLine = self.outlineView.getSelectedHandle() else: logger.warning("No item selected") diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index c308451b..5637f5c1 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -135,7 +135,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): nwGUI._changeView(nwView.NOVEL) nwGUI.novelView.refreshTree(rootHandle=None, overRide=True) with monkeypatch.context() as mp: - mp.setattr(GuiNovelView, "treeFocus", lambda *a: True) + mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None selItem = nwGUI.novelView.novelTree.topLevelItem(2) nwGUI.novelView.novelTree.setCurrentItem(selItem) @@ -147,7 +147,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): nwGUI._changeView(nwView.OUTLINE) nwGUI.switchFocus(nwWidget.OUTLINE) with monkeypatch.context() as mp: - mp.setattr(GuiOutlineView, "treeFocus", lambda *a: True) + mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None actItem = nwGUI.outlineView.outlineTree.topLevelItem(0) chpItem = actItem.child(0) diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 1038404a..26ca9803 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -71,7 +71,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): # Check focus with monkeypatch.context() as mp: mp.setattr(QWidget, "hasFocus", lambda *a: True) - assert outlineView.treeFocus() is True + assert outlineView.treeHasFocus() is True outlineView.setTreeFocus() # Can't check. just ensures that it doesn't error From cff464392ae5656eb9fd1d117c161ad781517b25 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 16:15:29 +0200 Subject: [PATCH 152/179] Use the indenta decorators of the novel tree to indicate type --- .../assets/icons/typicons_dark/icons.conf | 5 +++ .../assets/icons/typicons_dark/nw_deco-h0.svg | 35 +++++++++++++++++++ .../assets/icons/typicons_dark/nw_deco-h1.svg | 35 +++++++++++++++++++ .../assets/icons/typicons_dark/nw_deco-h2.svg | 35 +++++++++++++++++++ .../assets/icons/typicons_dark/nw_deco-h3.svg | 35 +++++++++++++++++++ .../assets/icons/typicons_dark/nw_deco-h4.svg | 35 +++++++++++++++++++ .../assets/icons/typicons_light/icons.conf | 5 +++ .../icons/typicons_light/nw_deco-h0.svg | 35 +++++++++++++++++++ .../icons/typicons_light/nw_deco-h1.svg | 35 +++++++++++++++++++ .../icons/typicons_light/nw_deco-h2.svg | 35 +++++++++++++++++++ .../icons/typicons_light/nw_deco-h3.svg | 35 +++++++++++++++++++ .../icons/typicons_light/nw_deco-h4.svg | 35 +++++++++++++++++++ novelwriter/gui/noveltree.py | 24 ++++++------- novelwriter/gui/theme.py | 22 +++++++----- sample/nwProject.nwx | 16 ++++----- tests/test_gui/test_gui_theme.py | 2 +- 16 files changed, 393 insertions(+), 31 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-h0.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-h1.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-h2.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-h3.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-h4.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index ae157e2c..d4106803 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -83,3 +83,8 @@ view_build = typ_export.svg view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg + +deco_doc_h1 = nw_deco-h1.svg +deco_doc_h2 = nw_deco-h2.svg +deco_doc_h3 = nw_deco-h3.svg +deco_doc_h4 = nw_deco-h4.svg diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg new file mode 100644 index 00000000..3c1618c9 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg new file mode 100644 index 00000000..1c0dec9b --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg new file mode 100644 index 00000000..0f86e5bb --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg new file mode 100644 index 00000000..f05e46e6 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg new file mode 100644 index 00000000..aa74e6f3 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 4683bb19..8d267a47 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -83,3 +83,8 @@ view_build = typ_export.svg view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg + +deco_doc_h1 = nw_deco-h1.svg +deco_doc_h2 = nw_deco-h2.svg +deco_doc_h3 = nw_deco-h3.svg +deco_doc_h4 = nw_deco-h4.svg diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h0.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h0.svg new file mode 100644 index 00000000..3c1618c9 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h0.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h1.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h1.svg new file mode 100644 index 00000000..e6c8efdc --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h1.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h2.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h2.svg new file mode 100644 index 00000000..7caa4203 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h2.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h3.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h3.svg new file mode 100644 index 00000000..61feca1b --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h3.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h4.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h4.svg new file mode 100644 index 00000000..b76fd7da --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h4.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 2332bc7c..8711dc8d 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -31,7 +31,7 @@ import novelwriter from enum import Enum from time import time -from PyQt5.QtGui import QPalette, QPixmap, QColor +from PyQt5.QtGui import QPalette from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtWidgets import ( QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, @@ -349,11 +349,9 @@ class GuiNovelTree(QTreeWidget): # ========= iPx = self.mainTheme.baseIconSize - nPx = self.mainTheme.textNWidth cMg = self.mainConf.pxInt(6) - nMg = self.mainConf.pxInt(6) - # self.setIconSize(QSize(iPx, iPx)) + self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) self.setHeaderHidden(True) self.setIndentation(0) @@ -380,15 +378,13 @@ class GuiNovelTree(QTreeWidget): fH2.setBold(True) self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()] - self._hIndent = ["", "", "", "\u203a\u00a0", "\u00bb\u00a0"] - self._pIndent = [QPixmap(), QPixmap()] - - hPix = QPixmap(QSize(iPx, iPx)) - hPix.fill(QColor(0, 0, 0, 0)) - for m in range(1, 4): - self._pIndent.append(hPix.scaled( - max(nPx*m - nMg, nMg), 2, Qt.IgnoreAspectRatio, Qt.FastTransformation - )) + self._pIndent = [ + self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx), + ] # Connect signals self.itemDoubleClicked.connect(self._treeDoubleClick) @@ -571,7 +567,7 @@ class GuiNovelTree(QTreeWidget): theData = (tHandle, sTitle[1:].lstrip("0"), tKey) newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel]) - newItem.setText(self.C_TITLE, self._hIndent[iLevel] + novIdx.title) + newItem.setText(self.C_TITLE, novIdx.title) newItem.setData(self.C_TITLE, Qt.UserRole, theData) newItem.setFont(self.C_TITLE, self._hFonts[iLevel]) newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}") diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 3a59e7e2..6c4ff50e 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -472,9 +472,12 @@ class GuiIcons: # Switches "sticky-on", "sticky-off", "bullet-on", "bullet-off", + + # Decorations + "deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", } - DECO_MAP = { + IMAGE_MAP = { "wiz-back": "wizard-back.jpg", } @@ -573,19 +576,22 @@ class GuiIcons: # Access Functions ## - def loadDecoration(self, decoKey, pxW, pxH): + def loadDecoration(self, decoKey, pxW=None, pxH=None): """Load graphical decoration element based on the decoration - map. This function always returns a QSwgWidget. + map or the icon map. This function always returns a QPixmap. """ - if decoKey not in self.DECO_MAP: + if decoKey in self._themeMap: + imgPath = self._themeMap[decoKey] + elif decoKey in self.IMAGE_MAP: + imgPath = os.path.join( + self.mainConf.assetPath, "images", self.IMAGE_MAP[decoKey] + ) + else: logger.error("Decoration with name '%s' does not exist", decoKey) return QPixmap() - imgPath = os.path.join( - self.mainConf.assetPath, "images", self.DECO_MAP[decoKey] - ) if not os.path.isfile(imgPath): - logger.error("Decoration file '%s' not in assets folder", self.DECO_MAP[decoKey]) + logger.error("Asset '%s' not found", self.IMAGE_MAP[decoKey]) return QPixmap() theDeco = QPixmap(imgPath) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index af509400..6ba4b5f1 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1367 - 231 - 68784 + 1371 + 236 + 69222 False @@ -35,9 +35,9 @@
- New + New Notes - Started + Started 1st Draft 2nd Draft 3rd Draft @@ -92,7 +92,7 @@ Chapter Two
- + We Found John! @@ -101,7 +101,7 @@ - Title Page + Title Page diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 756b2a81..8c7b934a 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -127,7 +127,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir): assert anImg.isNull() # Add a non-existent file and request it - iconCache.DECO_MAP["nonsense"] = "nofile.jpg" + iconCache.IMAGE_MAP["nonsense"] = "nofile.jpg" anImg = iconCache.loadDecoration("nonsense", 20, 20) assert isinstance(anImg, QPixmap) assert anImg.isNull() From 6ccbb358b916aedff1c63b6bec92001e218ad8fc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 16:46:44 +0200 Subject: [PATCH 153/179] Update tests --- tests/test_gui/test_gui_guimain.py | 2 +- tests/test_gui/test_gui_noveltree.py | 69 +++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 5637f5c1..6407b84b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -133,7 +133,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Novel Tree has focus nwGUI._changeView(nwView.NOVEL) - nwGUI.novelView.refreshTree(rootHandle=None, overRide=True) + nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True) with monkeypatch.context() as mp: mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index ea8b49bc..102b03d2 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -19,23 +19,47 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import os import pytest +from tools import buildTestProject, writeFile + from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox +from novelwriter.enum import nwWidget, nwItemType +from novelwriter.dialogs import GuiEditLabel +from novelwriter.gui.noveltree import NovelTreeColumn + @pytest.mark.gui -def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): +def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test navigating the novel tree. """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) + + buildTestProject(nwGUI, fncProj) + + nwGUI.switchFocus(nwWidget.TREE) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) + + writeFile( + os.path.join(nwGUI.theProject.projContent, "0000000000010.nwd"), + "# Jane Doe\n\n@tag: Jane\n\n" + ) + writeFile( + os.path.join(nwGUI.theProject.projContent, "000000000000f.nwd"), + "### Scene One\n\n@pov: Jane\n@focus: Jane\n\n" + ) - nwGUI.openProject(nwMinimal) novelView = nwGUI.novelView novelTree = novelView.novelTree + novelBar = novelView.novelBar # Show/Hide Scrollbars # ==================== @@ -65,9 +89,10 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): assert not topItem.isSelected() topItem.setSelected(True) assert novelTree.selectedItems()[0] == topItem - assert novelView.getSelectedHandle() == ("a35baf2e93843", 0) + assert novelView.getSelectedHandle() == ("000000000000c", 0) - novelView.refreshTree() + # Refresh using the slot for the butoom + novelBar._refreshNovelTree() assert novelTree.topLevelItem(0).isSelected() # Open Items @@ -89,7 +114,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): assert scItem.isSelected() assert nwGUI.docEditor.docHandle() is None novelTree._treeDoubleClick(scItem, 0) - assert nwGUI.docEditor.docHandle() == "8c659a11cd429" + assert nwGUI.docEditor.docHandle() == "000000000000f" # Open item with middle mouse button scItem.setSelected(True) @@ -106,7 +131,39 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): scItem.setData(novelTree.C_TITLE, Qt.UserRole, oldData) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) - assert nwGUI.docViewer.docHandle() == "8c659a11cd429" + assert nwGUI.docViewer.docHandle() == "000000000000f" + + # Last Column + # =========== + + novelBar.setLastColType(NovelTreeColumn.HIDDEN) + assert novelTree.isColumnHidden(novelTree.C_LAST) is True + assert novelTree.lastColType == NovelTreeColumn.HIDDEN + assert novelTree._getLastColumnText("000000000000f", "T000001") == ("", "") + + novelBar.setLastColType(NovelTreeColumn.POV) + assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.lastColType == NovelTreeColumn.POV + assert novelTree._getLastColumnText("000000000000f", "T000001") == ( + "Jane", "Point of View: Jane" + ) + + novelBar.setLastColType(NovelTreeColumn.FOCUS) + assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.lastColType == NovelTreeColumn.FOCUS + assert novelTree._getLastColumnText("000000000000f", "T000001") == ( + "Jane", "Focus: Jane" + ) + + novelBar.setLastColType(NovelTreeColumn.PLOT) + assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.lastColType == NovelTreeColumn.PLOT + assert novelTree._getLastColumnText("000000000000f", "T000001") == ( + "", "Plot: " + ) + + novelTree._lastCol = None + assert novelTree._getLastColumnText("0000000000000", "T000000") == ("", "") # Close # ===== From 50f299171a1d9c1b4ce8f2e64bbfea53a994a10b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 17:33:47 +0200 Subject: [PATCH 154/179] Add some convenient tree widget settings to the different trees --- novelwriter/gui/noveltree.py | 2 ++ novelwriter/gui/outline.py | 1 + novelwriter/gui/projtree.py | 3 +++ 3 files changed, 6 insertions(+) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 8711dc8d..1c4dd1ee 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -353,6 +353,8 @@ class GuiNovelTree(QTreeWidget): self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) + self.setUniformRowHeights(True) + self.setAllColumnsShowFocus(True) self.setHeaderHidden(True) self.setIndentation(0) self.setColumnCount(3) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 4b2077f6..0ae253f9 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -326,6 +326,7 @@ class GuiOutlineTree(QTreeWidget): self.theProject = theOutline.mainGui.theProject self.mainTheme = theOutline.mainGui.mainTheme + self.setUniformRowHeights(True) self.setFrameStyle(QFrame.NoFrame) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 08b24232..13b0b619 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -350,7 +350,10 @@ class GuiProjectTree(QTreeWidget): self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) + self.setUniformRowHeights(True) + self.setAllColumnsShowFocus(True) self.setExpandsOnDoubleClick(False) + self.setAutoExpandDelay(1000) self.setHeaderHidden(True) self.setIndentation(iPx) self.setColumnCount(4) From ba0ce33cfcff6eb7338b18b11f02b7cb904d23a3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 19:17:24 +0200 Subject: [PATCH 155/179] Add a tool tip with meta data behind a button on each novel tree item --- .../assets/icons/typicons_dark/icons.conf | 1 + .../typicons_dark/nw_deco-noveltree-more.svg | 30 ++++++ .../assets/icons/typicons_light/icons.conf | 1 + .../typicons_light/nw_deco-noveltree-more.svg | 30 ++++++ novelwriter/gui/noveltree.py | 99 ++++++++++++++++--- novelwriter/gui/theme.py | 2 +- tests/test_gui/test_gui_noveltree.py | 8 +- 7 files changed, 151 insertions(+), 20 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-noveltree-more.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-noveltree-more.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index d4106803..f372c60c 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -88,3 +88,4 @@ deco_doc_h1 = nw_deco-h1.svg deco_doc_h2 = nw_deco-h2.svg deco_doc_h3 = nw_deco-h3.svg deco_doc_h4 = nw_deco-h4.svg +deco_doc_more = nw_deco-noveltree-more.svg diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-noveltree-more.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-noveltree-more.svg new file mode 100644 index 00000000..f42d3306 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-noveltree-more.svg @@ -0,0 +1,30 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 8d267a47..4514eab9 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -88,3 +88,4 @@ deco_doc_h1 = nw_deco-h1.svg deco_doc_h2 = nw_deco-h2.svg deco_doc_h3 = nw_deco-h3.svg deco_doc_h4 = nw_deco-h4.svg +deco_doc_more = nw_deco-noveltree-more.svg diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-noveltree-more.svg b/novelwriter/assets/icons/typicons_light/nw_deco-noveltree-more.svg new file mode 100644 index 00000000..f42d3306 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-noveltree-more.svg @@ -0,0 +1,30 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 1c4dd1ee..711a71e3 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -35,11 +35,11 @@ from PyQt5.QtGui import QPalette from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtWidgets import ( QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, - QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, - QWidget + QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget, QTreeWidgetItem, + QVBoxLayout, QWidget ) -from novelwriter.enum import nwDocMode, nwItemClass +from novelwriter.enum import nwDocMode, nwItemClass, nwOutline from novelwriter.common import checkInt from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst @@ -322,7 +322,12 @@ class GuiNovelTree(QTreeWidget): C_TITLE = 0 C_WORDS = 1 - C_LAST = 2 + C_EXTRA = 2 + C_MORE = 3 + + D_HANDLE = Qt.UserRole + D_TITLE = Qt.UserRole + 1 + D_KEY = Qt.UserRole + 2 def __init__(self, novelView): QTreeWidget.__init__(self, novelView) @@ -357,7 +362,7 @@ class GuiNovelTree(QTreeWidget): self.setAllColumnsShowFocus(True) self.setHeaderHidden(True) self.setIndentation(0) - self.setColumnCount(3) + self.setColumnCount(4) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setExpandsOnDoubleClick(False) @@ -369,7 +374,8 @@ class GuiNovelTree(QTreeWidget): treeHeader.setMinimumSectionSize(iPx + cMg) treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch) treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents) - treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_EXTRA, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_MORE, QHeaderView.ResizeToContents) # Pre-Generate Tree Formatting fH1 = self.font() @@ -387,8 +393,12 @@ class GuiNovelTree(QTreeWidget): self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx), self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx), ] + self._pMore = self.mainTheme.loadDecoration("deco_doc_more", pxH=iPx) + self._pActive = self.mainTheme.loadDecoration("deco_more_on", pxH=iPx) + self._pInactive = self.mainTheme.loadDecoration("deco_more_off", pxH=iPx) # Connect signals + self.clicked.connect(self._treeItemClicked) self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._treeSelectionChange) @@ -451,7 +461,7 @@ class GuiNovelTree(QTreeWidget): selItem = self.selectedItems() titleKey = None if selItem: - titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2] + titleKey = selItem[0].data(self.C_TITLE, self.D_KEY) self._populateTree(rootHandle) self.theProject.setLastNovelViewed(rootHandle) @@ -478,8 +488,9 @@ class GuiNovelTree(QTreeWidget): tHandle = None tLine = 0 if selItem: - tHandle = selItem[0].data(self.C_TITLE, Qt.UserRole)[0] - tLine = checkInt(selItem[0].data(self.C_TITLE, Qt.UserRole)[1], 1) - 1 + tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE) + sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE) + tLine = checkInt(sTitle[1:], 1) - 1 return tHandle, tLine @@ -489,7 +500,7 @@ class GuiNovelTree(QTreeWidget): if self._lastCol != colType: logger.debug("Changing last column to %s", colType.name) self._lastCol = colType - self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN) + self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN) if doRefresh: self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) return @@ -527,6 +538,17 @@ class GuiNovelTree(QTreeWidget): # Private Slots ## + @pyqtSlot("QModelIndex") + def _treeItemClicked(self, mIndex): + """The user clicked on an item in the tree. + """ + if mIndex.column() == self.C_MORE: + tHandle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_HANDLE) + sTitle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_TITLE) + tipPos = self.mapToGlobal(self.visualRect(mIndex).topRight()) + self._popMetaBox(tipPos, tHandle, sTitle) + return + @pyqtSlot() def _treeSelectionChange(self): """Extract the handle and line number of the currently selected @@ -566,19 +588,21 @@ class GuiNovelTree(QTreeWidget): continue newItem = QTreeWidgetItem() - theData = (tHandle, sTitle[1:].lstrip("0"), tKey) - newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel]) newItem.setText(self.C_TITLE, novIdx.title) - newItem.setData(self.C_TITLE, Qt.UserRole, theData) + newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle) + newItem.setData(self.C_TITLE, self.D_TITLE, sTitle) + newItem.setData(self.C_TITLE, self.D_KEY, tKey) newItem.setFont(self.C_TITLE, self._hFonts[iLevel]) newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}") newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) lastText, toolTip = self._getLastColumnText(tHandle, sTitle) - newItem.setText(self.C_LAST, lastText) + newItem.setText(self.C_EXTRA, lastText) if lastText: - newItem.setToolTip(self.C_LAST, toolTip) + newItem.setToolTip(self.C_EXTRA, toolTip) + + newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore) self._treeMap[tKey] = newItem self.addTopLevelItem(newItem) @@ -609,4 +633,49 @@ class GuiNovelTree(QTreeWidget): return "", "" + def _popMetaBox(self, qPos, tHandle, sTitle): + """Show the novel meta data box. + """ + logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle) + + pIndex = self.theProject.index + novIdx = pIndex.getNovelData(tHandle, sTitle) + refTags = pIndex.getReferences(tHandle, sTitle) + + synopText = novIdx.synopsis + if synopText: + synopLabel = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP]) + synopText = f"

{synopLabel}: {synopText}

" + + refLines = [] + refLines = self._appendMetaTag(refTags, nwKeyWords.POV_KEY, refLines) + refLines = self._appendMetaTag(refTags, nwKeyWords.FOCUS_KEY, refLines) + refLines = self._appendMetaTag(refTags, nwKeyWords.CHAR_KEY, refLines) + refLines = self._appendMetaTag(refTags, nwKeyWords.PLOT_KEY, refLines) + refLines = self._appendMetaTag(refTags, nwKeyWords.TIME_KEY, refLines) + refLines = self._appendMetaTag(refTags, nwKeyWords.WORLD_KEY, refLines) + refLines = self._appendMetaTag(refTags, nwKeyWords.OBJECT_KEY, refLines) + refLines = self._appendMetaTag(refTags, nwKeyWords.ENTITY_KEY, refLines) + refLines = self._appendMetaTag(refTags, nwKeyWords.CUSTOM_KEY, refLines) + + refText = "" + if refLines: + refList = "
".join(refLines) + refText = f"

{refList}

" + + ttText = refText + synopText or self.tr("No meta data") + if ttText: + QToolTip.showText(qPos, ttText) + + return + + @staticmethod + def _appendMetaTag(refs, key, lines): + """Generate a reference list for a given reference key. + """ + tags = ", ".join(refs.get(key, [])) + if tags: + lines.append(f"{trConst(nwLabels.KEY_NAME[key])}: {tags}") + return lines + # END Class GuiNovelTree diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 6c4ff50e..e72a8c97 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -474,7 +474,7 @@ class GuiIcons: "bullet-on", "bullet-off", # Decorations - "deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", + "deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", "deco_doc_more", } IMAGE_MAP = { diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 102b03d2..53275b12 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -137,26 +137,26 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # =========== novelBar.setLastColType(NovelTreeColumn.HIDDEN) - assert novelTree.isColumnHidden(novelTree.C_LAST) is True + assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True assert novelTree.lastColType == NovelTreeColumn.HIDDEN assert novelTree._getLastColumnText("000000000000f", "T000001") == ("", "") novelBar.setLastColType(NovelTreeColumn.POV) - assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False assert novelTree.lastColType == NovelTreeColumn.POV assert novelTree._getLastColumnText("000000000000f", "T000001") == ( "Jane", "Point of View: Jane" ) novelBar.setLastColType(NovelTreeColumn.FOCUS) - assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False assert novelTree.lastColType == NovelTreeColumn.FOCUS assert novelTree._getLastColumnText("000000000000f", "T000001") == ( "Jane", "Focus: Jane" ) novelBar.setLastColType(NovelTreeColumn.PLOT) - assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False assert novelTree.lastColType == NovelTreeColumn.PLOT assert novelTree._getLastColumnText("000000000000f", "T000001") == ( "", "Plot: " From 469275eea5a6f938a3793a60a01d48ed85621d4c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 19:39:07 +0200 Subject: [PATCH 156/179] Add novel tree highlight of rows of current document --- novelwriter/assets/themes/default_dark.conf | 2 +- novelwriter/assets/themes/solarized_dark.conf | 2 +- novelwriter/gui/noveltree.py | 29 +++++++++++++++++-- novelwriter/guimain.py | 2 ++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index 14d0ad51..44317ac1 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -10,7 +10,7 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ window = 54, 54, 54 windowtext = 174, 174, 174 base = 62, 62, 62 -alternatebase = 67, 67, 67 +alternatebase = 78, 78, 78 text = 174, 174, 174 tooltipbase = 255, 255, 192 tooltiptext = 21, 21, 13 diff --git a/novelwriter/assets/themes/solarized_dark.conf b/novelwriter/assets/themes/solarized_dark.conf index 81a85a16..812d968d 100644 --- a/novelwriter/assets/themes/solarized_dark.conf +++ b/novelwriter/assets/themes/solarized_dark.conf @@ -10,7 +10,7 @@ licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE window = 0, 43, 54 windowtext = 253, 246, 227 base = 7, 54, 66 -alternatebase = 67, 67, 67 +alternatebase = 0, 43, 54 text = 253, 246, 227 tooltipbase = 133, 153, 0 tooltiptext = 0, 43, 54 diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 711a71e3..9dd0f78c 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -84,6 +84,7 @@ class GuiNovelView(QWidget): # Function Mappings self.updateWordCounts = self.novelTree.updateWordCounts self.getSelectedHandle = self.novelTree.getSelectedHandle + self.setActiveHandle = self.novelTree.setActiveHandle return @@ -505,6 +506,23 @@ class GuiNovelTree(QTreeWidget): self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) return + def setActiveHandle(self, tHandle): + """Highlight the rows associated with a given handle. + """ + for i in range(self.topLevelItemCount()): + tItem = self.topLevelItem(i) + if tItem.data(self.C_TITLE, self.D_HANDLE) == tHandle: + tItem.setBackground(self.C_TITLE, self.palette().alternateBase()) + tItem.setBackground(self.C_WORDS, self.palette().alternateBase()) + tItem.setBackground(self.C_EXTRA, self.palette().alternateBase()) + tItem.setBackground(self.C_MORE, self.palette().alternateBase()) + else: + tItem.setBackground(self.C_TITLE, self.palette().base()) + tItem.setBackground(self.C_WORDS, self.palette().base()) + tItem.setBackground(self.C_EXTRA, self.palette().base()) + tItem.setBackground(self.C_MORE, self.palette().base()) + return + ## # Events ## @@ -534,6 +552,13 @@ class GuiNovelTree(QTreeWidget): return + def focusOutEvent(self, theEvent): + """Clear the selection when the tree no longer has focus. + """ + QTreeWidget.focusOutEvent(self, theEvent) + self.clearSelection() + return + ## # Private Slots ## @@ -596,14 +621,14 @@ class GuiNovelTree(QTreeWidget): newItem.setFont(self.C_TITLE, self._hFonts[iLevel]) newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}") newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) + newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore) + # Custom column lastText, toolTip = self._getLastColumnText(tHandle, sTitle) newItem.setText(self.C_EXTRA, lastText) if lastText: newItem.setToolTip(self.C_EXTRA, toolTip) - newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore) - self._treeMap[tKey] = newItem self.addTopLevelItem(newItem) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index e9535747..5cbecf7a 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -573,6 +573,7 @@ class GuiMain(QMainWindow): if self.docEditor.docChanged(): self.saveDocument() self.docEditor.clearEditor() + self.novelView.setActiveHandle(None) return True @@ -594,6 +595,7 @@ class GuiMain(QMainWindow): self.docEditor.setFocus() self.theProject.setLastEdited(tHandle) self.projView.setSelectedHandle(tHandle, doScroll=doScroll) + self.novelView.setActiveHandle(tHandle) else: return False From bb85232ea7e16a3156fa88790249640c3c3bd7fc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 19:47:04 +0200 Subject: [PATCH 157/179] Add some fixes to keep novel tree active handle up to date --- novelwriter/gui/noveltree.py | 9 +++++++++ novelwriter/guimain.py | 7 ++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 9dd0f78c..b96f5331 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -345,6 +345,7 @@ class GuiNovelTree(QTreeWidget): self._treeMap = {} self._lastBuild = 0 self._lastCol = NovelTreeColumn.POV + self._actHandle = None # Cached Strings self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) @@ -509,6 +510,9 @@ class GuiNovelTree(QTreeWidget): def setActiveHandle(self, tHandle): """Highlight the rows associated with a given handle. """ + tStart = time() + + self._actHandle = tHandle for i in range(self.topLevelItemCount()): tItem = self.topLevelItem(i) if tItem.data(self.C_TITLE, self.D_HANDLE) == tHandle: @@ -521,6 +525,9 @@ class GuiNovelTree(QTreeWidget): tItem.setBackground(self.C_WORDS, self.palette().base()) tItem.setBackground(self.C_EXTRA, self.palette().base()) tItem.setBackground(self.C_MORE, self.palette().base()) + + logger.verbose("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000) + return ## @@ -632,6 +639,8 @@ class GuiNovelTree(QTreeWidget): self._treeMap[tKey] = newItem self.addTopLevelItem(newItem) + self.setActiveHandle(self._actHandle) + logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000) self._lastBuild = time() diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 5cbecf7a..e34b873e 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -558,7 +558,7 @@ class GuiMain(QMainWindow): # Document Actions ## - def closeDocument(self): + def closeDocument(self, beforeOpen=False): """Close the document and clear the editor and title field. """ if not self.hasProject: @@ -573,7 +573,8 @@ class GuiMain(QMainWindow): if self.docEditor.docChanged(): self.saveDocument() self.docEditor.clearEditor() - self.novelView.setActiveHandle(None) + if not beforeOpen: + self.novelView.setActiveHandle(None) return True @@ -588,7 +589,7 @@ class GuiMain(QMainWindow): logger.debug("Requested item '%s' is not a document", tHandle) return False - self.closeDocument() + self.closeDocument(beforeOpen=True) self._changeView(nwView.EDITOR) if self.docEditor.loadText(tHandle, tLine): if changeFocus: From 4fa814a2fa8139e5f6efc0d1aa854c8b56b8fb86 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 19:51:13 +0200 Subject: [PATCH 158/179] Remove no longer needed config settings, and change default text width --- novelwriter/config.py | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index 7fb334c2..c3766553 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -96,8 +96,6 @@ class Config: # Sizes self.winGeometry = [1200, 650] self.prefGeometry = [700, 615] - self.treeColWidth = [200, 50, 30] - self.novelColWidth = [200, 50] self.projColWidth = [200, 60, 140] self.mainPanePos = [300, 800] self.docPanePos = [400, 400] @@ -117,7 +115,7 @@ class Config: # Text Editor self.textFont = None # Editor font self.textSize = 12 # Editor font size - self.textWidth = 600 # Editor text width + self.textWidth = 700 # Editor text width self.textMargin = 40 # Editor/viewer text margin self.tabWidth = 40 # Editor tabulator width @@ -460,8 +458,6 @@ class Config: cnfSec = "Sizes" self.winGeometry = theConf.rdIntList(cnfSec, "geometry", self.winGeometry) self.prefGeometry = theConf.rdIntList(cnfSec, "preferences", self.prefGeometry) - self.treeColWidth = theConf.rdIntList(cnfSec, "treecols", self.treeColWidth) - self.novelColWidth = theConf.rdIntList(cnfSec, "novelcols", self.novelColWidth) self.projColWidth = theConf.rdIntList(cnfSec, "projcols", self.projColWidth) self.mainPanePos = theConf.rdIntList(cnfSec, "mainpane", self.mainPanePos) self.docPanePos = theConf.rdIntList(cnfSec, "docpane", self.docPanePos) @@ -581,8 +577,6 @@ class Config: theConf["Sizes"] = { "geometry": self._packList(self.winGeometry), "preferences": self._packList(self.prefGeometry), - "treecols": self._packList(self.treeColWidth), - "novelcols": self._packList(self.novelColWidth), "projcols": self._packList(self.projColWidth), "mainpane": self._packList(self.mainPanePos), "docpane": self._packList(self.docPanePos), @@ -811,20 +805,6 @@ class Config: self.confChanged = True return True - def setTreeColWidths(self, colWidths): - """Set the column widths of the main project tree. - """ - self.treeColWidth = [int(x/self.guiScale) for x in colWidths] - self.confChanged = True - return True - - def setNovelColWidths(self, colWidths): - """Set the column widths of the novel tree. - """ - self.novelColWidth = [int(x/self.guiScale) for x in colWidths] - self.confChanged = True - return True - def setProjColWidths(self, colWidths): """Set the column widths of the Load Project dialog. """ @@ -910,12 +890,6 @@ class Config: def getPreferencesSize(self): return [int(x*self.guiScale) for x in self.prefGeometry] - def getTreeColWidths(self): - return [int(x*self.guiScale) for x in self.treeColWidth] - - def getNovelColWidths(self): - return [int(x*self.guiScale) for x in self.novelColWidth] - def getProjColWidths(self): return [int(x*self.guiScale) for x in self.projColWidth] From 8c4ecaeee255c524dcb7e62aa2604c617a3232c9 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 20:01:45 +0200 Subject: [PATCH 159/179] Fix tests --- tests/reference/baseConfig_novelwriter.conf | 4 +-- .../reference/guiPreferences_novelwriter.conf | 2 -- tests/test_base/test_base_config.py | 30 ++----------------- tests/test_dialogs/test_dlg_preferences.py | 4 +-- tests/test_gui/test_gui_noveltree.py | 6 ++-- tests/test_gui/test_gui_theme.py | 2 +- 6 files changed, 9 insertions(+), 39 deletions(-) diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index 8018b089..56b5a807 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -13,8 +13,6 @@ hidehscroll = False [Sizes] geometry = 1200, 650 preferences = 700, 615 -treecols = 200, 50, 30 -novelcols = 200, 50 projcols = 200, 60, 140 mainpane = 300, 800 docpane = 400, 400 @@ -30,7 +28,7 @@ emphlabels = True [Editor] textfont = None textsize = 12 -width = 600 +width = 700 margin = 40 tabwidth = 40 focuswidth = 800 diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf index 71c08fb1..9d21830d 100644 --- a/tests/reference/guiPreferences_novelwriter.conf +++ b/tests/reference/guiPreferences_novelwriter.conf @@ -13,8 +13,6 @@ hidehscroll = True [Sizes] geometry = 1200, 650 preferences = 670, 589 -treecols = 200, 50, 30 -novelcols = 200, 50 projcols = 200, 60, 140 mainpane = 300, 800 docpane = 400, 400 diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 2e055884..83ff3160 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -410,32 +410,6 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): assert tmpConf.setPreferencesSize(700, 615) - # Project Tree Columns - tmpConf.guiScale = 2.0 - assert tmpConf.setTreeColWidths([10, 20, 25]) - assert tmpConf.getTreeColWidths() == [10, 20, 24] - assert tmpConf.treeColWidth == [5, 10, 12] - - tmpConf.guiScale = 1.0 - assert tmpConf.setTreeColWidths([10, 20, 25]) - assert tmpConf.getTreeColWidths() == [10, 20, 25] - assert tmpConf.treeColWidth == [10, 20, 25] - - assert tmpConf.setTreeColWidths([200, 50, 30]) - - # Novel Tree Columns - tmpConf.guiScale = 2.0 - assert tmpConf.setNovelColWidths([10, 20]) - assert tmpConf.getNovelColWidths() == [10, 20] - assert tmpConf.novelColWidth == [5, 10] - - tmpConf.guiScale = 1.0 - assert tmpConf.setNovelColWidths([10, 20]) - assert tmpConf.getNovelColWidths() == [10, 20] - assert tmpConf.novelColWidth == [10, 20] - - assert tmpConf.setNovelColWidths([200, 50]) - # Project Settings Tree Columns tmpConf.guiScale = 2.0 assert tmpConf.setProjColWidths([10, 20, 30]) @@ -505,13 +479,13 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): # ============ tmpConf.guiScale = 1.0 - assert tmpConf.getTextWidth(False) == 600 + assert tmpConf.getTextWidth(False) == 700 assert tmpConf.getTextWidth(True) == 800 assert tmpConf.getTextMargin() == 40 assert tmpConf.getTabWidth() == 40 tmpConf.guiScale = 2.0 - assert tmpConf.getTextWidth(False) == 1200 + assert tmpConf.getTextWidth(False) == 1400 assert tmpConf.getTextWidth(True) == 1600 assert tmpConf.getTextMargin() == 80 assert tmpConf.getTabWidth() == 80 diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index aa33ba82..1507d351 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -239,8 +239,8 @@ def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): copyfile(projFile, testFile) ignTuple = ( "timestamp", "guifont", "lastnotes", "guilang", "geometry", - "preferences", "treecols", "novelcols", "projcols", "mainpane", - "docpane", "viewpane", "outlinepane", "textfont", "textsize" + "preferences", "projcols", "mainpane", "docpane", "viewpane", + "outlinepane", "textfont", "textsize" ) assert cmpFiles(testFile, compFile, ignoreStart=ignTuple) diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 53275b12..93a10b92 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -124,12 +124,12 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.docViewer.docHandle() is None scRect = novelTree.visualItemRect(scItem) - oldData = scItem.data(novelTree.C_TITLE, Qt.UserRole) - scItem.setData(novelTree.C_TITLE, Qt.UserRole, (None, "", "")) + oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE) + scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) assert nwGUI.docViewer.docHandle() is None - scItem.setData(novelTree.C_TITLE, Qt.UserRole, oldData) + scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) assert nwGUI.docViewer.docHandle() == "000000000000f" diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 8c7b934a..0014a4da 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -81,7 +81,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir): assert thePalette.window().color() == QColor(54, 54, 54) assert thePalette.windowText().color() == QColor(174, 174, 174) assert thePalette.base().color() == QColor(62, 62, 62) - assert thePalette.alternateBase().color() == QColor(67, 67, 67) + assert thePalette.alternateBase().color() == QColor(78, 78, 78) assert thePalette.text().color() == QColor(174, 174, 174) assert thePalette.toolTipBase().color() == QColor(255, 255, 192) assert thePalette.toolTipText().color() == QColor(21, 21, 13) From 0d01e108f1de0432e4490dc589e551ca1b9a189f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 15 Jun 2022 20:37:19 +0200 Subject: [PATCH 160/179] Improve test coverage --- tests/test_gui/test_gui_noveltree.py | 40 +++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 93a10b92..f8f18875 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -24,8 +24,9 @@ import pytest from tools import buildTestProject, writeFile -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtGui import QFocusEvent +from PyQt5.QtCore import Qt, QEvent +from PyQt5.QtWidgets import QMessageBox, QToolTip from novelwriter.enum import nwWidget, nwItemType from novelwriter.dialogs import GuiEditLabel @@ -53,8 +54,12 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): "# Jane Doe\n\n@tag: Jane\n\n" ) writeFile( - os.path.join(nwGUI.theProject.projContent, "000000000000f.nwd"), - "### Scene One\n\n@pov: Jane\n@focus: Jane\n\n" + os.path.join(nwGUI.theProject.projContent, "000000000000f.nwd"), ( + "### Scene One\n\n" + "@pov: Jane\n" + "@focus: Jane\n\n" + "% Synopsis: This is a scene." + ) ) novelView = nwGUI.novelView @@ -165,6 +170,33 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): novelTree._lastCol = None assert novelTree._getLastColumnText("0000000000000", "T000000") == ("", "") + # Item Meta + # ========= + + ttText = "" + + def showText(pos, text): + nonlocal ttText + ttText = text + + mIndex = novelTree.model().index(2, novelTree.C_MORE) + with monkeypatch.context() as mp: + mp.setattr(QToolTip, "showText", showText) + novelTree._treeItemClicked(mIndex) + assert ttText == ( + "

Point of View: Jane
Focus: Jane

" + "

Synopsis: This is a scene.

" + ) + + # Other Checks + # ============ + + scItem = novelTree.topLevelItem(2) + scItem.setSelected(True) + assert scItem.isSelected() + novelTree.focusOutEvent(QFocusEvent(QEvent.None_, Qt.MouseFocusReason)) + assert not scItem.isSelected() + # Close # ===== From 211ab290d098b0b1feeba0c917ea7799e89a6fdf Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 8 Jul 2022 14:06:56 -0700 Subject: [PATCH 161/179] bring .desktop into compliance with Desktop Entry specification 1.1 validated with `desktop-file-validate` --- setup/data/novelwriter.desktop | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup/data/novelwriter.desktop b/setup/data/novelwriter.desktop index 2666fa13..8b140ffc 100644 --- a/setup/data/novelwriter.desktop +++ b/setup/data/novelwriter.desktop @@ -1,10 +1,9 @@ [Desktop Entry] Type=Application -Encoding=UTF-8 Name=novelWriter Comment=A markdown-like text editor for planning and writing novels Exec=novelwriter %f Icon=novelwriter Categories=Qt;Office;WordProcessor; Terminal=false -MimeType=application/x-novelwriter-project +MimeType=application/x-novelwriter-project; From b363259a723a24377b5a35b8d2d22825ebe5737a Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 8 Jul 2022 14:08:13 -0700 Subject: [PATCH 162/179] fix spelling --- setup/description_short.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/description_short.txt b/setup/description_short.txt index ac106560..d3d30690 100644 --- a/setup/description_short.txt +++ b/setup/description_short.txt @@ -2,5 +2,5 @@ novelWriter is a plain text editor designed for writing novels assembled from many smaller text documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data syntax for comments, synopsis, and cross-referencing. It's designed to be a simple text editor that allows for -easy organisation of text and notes, using human readable text files as +easy organization of text and notes, using human readable text files as storage for robustness. From 073a9acfb3b7e3f4c9378cc39570b347e4103e47 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 8 Jul 2022 15:26:03 -0700 Subject: [PATCH 163/179] add a method to automate the building of an Appimage package --- setup.py | 223 ++++++++++++++++++++++++++++++++++ setup/novelwriter.appdata.xml | 21 ++++ 2 files changed, 244 insertions(+) create mode 100644 setup/novelwriter.appdata.xml diff --git a/setup.py b/setup.py index f72ce936..b7ce8318 100755 --- a/setup.py +++ b/setup.py @@ -835,10 +835,225 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): return +## +# Make Appimage (build-appimage) +## + +def makeAppimage(sysArgs): + """Build an Appimage + """ + + import argparse + import platform + import glob + + try: + import python_appimage + except ImportError: + print( + "ERROR: Package 'python-appimage' is missing on this system.\n" + " Please run 'pip install --user python-appimage' to install it.\n" + ) + sys.exit(1) + + print("") + print("Build Appimage") + print("==============") + print("") + + plat = platform.machine() + + parser = argparse.ArgumentParser(prog='build_appimage', + description='Build an Appimage', + epilog='see https://appimage.org/ for more details') + parser.add_argument('-l', '--linux-tag', nargs='?', default=f"manylinux2014_{plat}", + help=( + 'linux compatibility tag (e.g. manylinux1_x86_64) \n' + 'see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n' + 'and https://github.com/pypa/manylinux for a list of valid tags' + )) + parser.add_argument('-p', '--python-version', nargs='?', default='3.11', + help='python version (e.g. 3.11)') + + args, unknown = parser.parse_known_args(sysArgs) + + linuxTag = args.linux_tag + pythonVer = args.python_version + + # Version Info + # ============ + + numVers, hexVers, relDate = extractVersion() + pkgVers = compactVersion(numVers) + relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") + print("") + + # Set Up Folder + # ============= + + bldDir = "dist_appimage" + bldPkg = f"novelwriter_{pkgVers}" + outDir = f"{bldDir}/{bldPkg}" + imageDir = f"{bldDir}/appimage" + + # Set Up Folders + # ============== + + if not os.path.isdir(bldDir): + os.mkdir(bldDir) + + if os.path.isdir(outDir): + print("Removing old build files ...") + print("") + shutil.rmtree(outDir) + + os.mkdir(outDir) + + if os.path.isdir(imageDir): + print("Removing old build metadata files ...") + print("") + shutil.rmtree(imageDir) + + os.mkdir(imageDir) + + # Remove old Appimages + outFiles = glob.glob(f"{bldDir}/*.AppImage") + + if outFiles: + print("Removing old Appimages") + print("") + for image in outFiles: + try: + os.remove(image) + except OSError: + print("Error while deleting file : ", image) + + # Build Additional Assets + # ======================= + + buildQtI18n() + buildSampleZip() + buildPdfManual() + + # Copy novelWriter Source + # ======================= + + print("Copying novelWriter source ...") + print("") + + for nPath, _, nFiles in os.walk("novelwriter"): + if nPath.endswith("__pycache__"): + print("Skipped: %s" % nPath) + continue + + pPath = f"{outDir}/{nPath}" + if not os.path.isdir(pPath): + os.mkdir(pPath) + + fCount = 0 + for fFile in nFiles: + nFile = f"{nPath}/{fFile}" + pFile = f"{pPath}/{fFile}" + + if fFile.endswith(".pyc"): + print("Skipped: %s" % nFile) + continue + + shutil.copyfile(nFile, pFile) + fCount += 1 + + print("Copied: %s/* [Files: %d]" % (nPath, fCount)) + + print("") + print("Copying or generating additional files ...") + print("") + + # Copy/Write Root Files + # ===================== + + copyFiles = ["LICENSE.md", "CREDITS.md", "CHANGELOG.md", "pyproject.toml"] + for copyFile in copyFiles: + shutil.copyfile(copyFile, f"{outDir}/{copyFile}") + print("Copied: %s" % copyFile) + + writeFile(f"{outDir}/MANIFEST.in", ( + "include LICENSE.md\n" + "include CREDITS.md\n" + "include CHANGELOG.md\n" + "include data/*\n" + "recursive-include novelwriter/assets *\n" + )) + print("Wrote: MANIFEST.in") + + writeFile(f"{outDir}/setup.py", ( + "import setuptools\n" + "setuptools.setup()\n" + )) + print("Wrote: setup.py") + + setupCfg = readFile("setup.cfg").replace( + "file: setup/description_pypi.md", "file: data/description_short.txt" + ) + writeFile(f"{outDir}/setup.cfg", setupCfg) + print("Wrote: setup.cfg") + + # Write Metadata + # ============== + + appDescription = readFile("setup/description_short.txt") + appdataXML = readFile("setup/novelwriter.appdata.xml").format(description=appDescription) + writeFile(f"{imageDir}/novelwriter.appdata.xml", appdataXML) + print("Wrote: novelwriter.appdata.xml") + + writeFile(f"{imageDir}/entrypoint.sh", ( + '#! /bin/bash \n' + '{{ python-executable }} -sE ${APPDIR}/opt/python{{ python-version }}/bin/novelwriter "$@"' + )) + print("Wrote: entrypoint.sh") + + writeFile(f"{imageDir}/requirements.txt", os.path.abspath(outDir)) + print("Wrote: requirements.txt") + + shutil.copyfile("setup/data/novelwriter.desktop", f"{imageDir}/novelwriter.desktop") + print("Copied: setup/data/novelwriter.desktop") + + shutil.copyfile("setup/icons/novelwriter.svg", f"{imageDir}/novelwriter.svg") + print("Copied: setup/icons/novelwriter.svg") + + shutil.copyfile("setup/data/hicolor/256x256/apps/novelwriter.png", + f"{imageDir}/novelwriter.png") + print("Copied: setup/data/hicolor/256x256/apps/novelwriter.png") + + # Build Appimage + # ============== + + try: + subprocess.call( + ["python", "-m", "python_appimage", "build", "app", + "-l", linuxTag, "-p", pythonVer, "appimage"], cwd=bldDir) + except Exception as exc: + print("Appimage build: FAILED") + print("") + print(str(exc)) + print("") + print("Dependencies:") + print(" * pip install python-appimage") + print("") + sys.exit(1) + + outFile = glob.glob(f"{bldDir}/*.AppImage")[0] + shaFile = makeCheckSum(os.path.basename(outFile), cwd=bldDir) + + toUpload(outFile) + toUpload(shaFile) + + return + ## # Make Windows Setup EXE (build-win-exe) ## + def makeWindowsEmbedded(sysArgs): """Set up a package with embedded Python and dependencies for Windows installation. @@ -1679,6 +1894,14 @@ if __name__ == "__main__": makeWindowsEmbedded(sys.argv) sys.exit(0) # Don't continue execution + if "build-appimage" in sys.argv: + sys.argv.remove("build-appimage") + if hostOS == OS_LINUX: + makeAppimage(sys.argv) + else: + print("ERROR: Command 'build-ubuntu' can only be used on Linux") + sys.exit(1) + # General Installers # ================== diff --git a/setup/novelwriter.appdata.xml b/setup/novelwriter.appdata.xml new file mode 100644 index 00000000..98a6e33b --- /dev/null +++ b/setup/novelwriter.appdata.xml @@ -0,0 +1,21 @@ + + + novelwriter + GPL-3.0 + GPL-3.0 + novelWriter + A markdown-like text editor for planning and writing novels + +

{description}

+
+ novelwriter.desktop + https://novelwriter.io/ + + + https://novelwriter.io/images/screenshot-multi.png + + + + novelwriter.desktop + +
\ No newline at end of file From 3dc4d1443fc58ec902962873b333b00d4c2654a0 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 8 Jul 2022 18:40:54 -0700 Subject: [PATCH 164/179] ensure prefix matching of `build-appimage` args do not shadow normal setup move defaults to pyhton 3.10 and manylinux2010 for better compatability cleanup --- setup.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index b7ce8318..15144a99 100755 --- a/setup.py +++ b/setup.py @@ -866,16 +866,16 @@ def makeAppimage(sysArgs): parser = argparse.ArgumentParser(prog='build_appimage', description='Build an Appimage', epilog='see https://appimage.org/ for more details') - parser.add_argument('-l', '--linux-tag', nargs='?', default=f"manylinux2014_{plat}", + parser.add_argument('--linux-tag', nargs='?', default=f"manylinux2010_{plat}", help=( 'linux compatibility tag (e.g. manylinux1_x86_64) \n' 'see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n' 'and https://github.com/pypa/manylinux for a list of valid tags' )) - parser.add_argument('-p', '--python-version', nargs='?', default='3.11', - help='python version (e.g. 3.11)') + parser.add_argument('--python-version', nargs='?', default='3.10', + help='python version (e.g. 3.10)') - args, unknown = parser.parse_known_args(sysArgs) + args, unparsedArgs = parser.parse_known_args(sysArgs) linuxTag = args.linux_tag pythonVer = args.python_version @@ -1047,7 +1047,7 @@ def makeAppimage(sysArgs): toUpload(outFile) toUpload(shaFile) - return + return unparsedArgs ## # Make Windows Setup EXE (build-win-exe) @@ -1897,9 +1897,9 @@ if __name__ == "__main__": if "build-appimage" in sys.argv: sys.argv.remove("build-appimage") if hostOS == OS_LINUX: - makeAppimage(sys.argv) + sys.argv = makeAppimage(sys.argv) # Build appimage and prune it's args else: - print("ERROR: Command 'build-ubuntu' can only be used on Linux") + print("ERROR: Command 'build-appimage' can only be used on Linux") sys.exit(1) # General Installers From 82a5da391fa0392e6b9c594002638e267ee2c3ea Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 8 Jul 2022 18:53:01 -0700 Subject: [PATCH 165/179] - ensure `dist_appimage` removed during cleanup - cleanup --- setup.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 15144a99..fdf8e072 100755 --- a/setup.py +++ b/setup.py @@ -197,6 +197,7 @@ def cleanBuildDirs(): removeFolder("dist") removeFolder("dist_deb") removeFolder("dist_minimal") + removeFolder("dist_appimage") removeFolder("novelWriter.egg-info") print("") @@ -863,17 +864,24 @@ def makeAppimage(sysArgs): plat = platform.machine() - parser = argparse.ArgumentParser(prog='build_appimage', - description='Build an Appimage', - epilog='see https://appimage.org/ for more details') - parser.add_argument('--linux-tag', nargs='?', default=f"manylinux2010_{plat}", - help=( - 'linux compatibility tag (e.g. manylinux1_x86_64) \n' - 'see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n' - 'and https://github.com/pypa/manylinux for a list of valid tags' - )) - parser.add_argument('--python-version', nargs='?', default='3.10', - help='python version (e.g. 3.10)') + parser = argparse.ArgumentParser( + prog="build_appimage", + description="Build an Appimage", + epilog="see https://appimage.org/ for more details", + ) + parser.add_argument( + "--linux-tag", + nargs="?", + default=f"manylinux2010_{plat}", + help=( + "linux compatibility tag (e.g. manylinux1_x86_64) \n" + "see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n" + "and https://github.com/pypa/manylinux for a list of valid tags" + ), + ) + parser.add_argument( + "--python-version", nargs="?", default="3.10", help="python version (e.g. 3.10)" + ) args, unparsedArgs = parser.parse_known_args(sysArgs) From 67aff2550bf827543111e1d508f685f3806415db Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 15 Jul 2022 14:38:37 +0200 Subject: [PATCH 166/179] Change look of outline tree --- novelwriter/gui/noveltree.py | 4 +- novelwriter/gui/outline.py | 162 +++++++++++++++-------------------- novelwriter/gui/projtree.py | 2 +- 3 files changed, 71 insertions(+), 97 deletions(-) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index b96f5331..0dec2551 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -174,7 +174,7 @@ class GuiNovelToolBar(QWidget): self.mainTheme = novelView.mainGui.mainTheme iPx = self.mainTheme.baseIconSize - mPx = self.mainConf.pxInt(3) + mPx = self.mainConf.pxInt(2) self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) @@ -396,8 +396,6 @@ class GuiNovelTree(QTreeWidget): self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx), ] self._pMore = self.mainTheme.loadDecoration("deco_doc_more", pxH=iPx) - self._pActive = self.mainTheme.loadDecoration("deco_more_on", pxH=iPx) - self._pInactive = self.mainTheme.loadDecoration("deco_more_off", pxH=iPx) # Connect signals self.clicked.connect(self._treeItemClicked) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 0ae253f9..49efe5f0 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -37,16 +37,16 @@ from PyQt5.QtCore import ( Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP ) from PyQt5.QtWidgets import ( - QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel, - QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout, - QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton + QAbstractItemView, QAction, QComboBox, QFrame, QGridLayout, QGroupBox, + QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar, + QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter.enum import ( nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline ) from novelwriter.common import checkInt -from novelwriter.constants import trConst, nwKeyWords, nwLabels +from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels logger = logging.getLogger(__name__) @@ -313,6 +313,9 @@ class GuiOutlineTree(QTreeWidget): nwOutline.SYNOP: False, } + D_HANDLE = Qt.UserRole + D_TITLE = Qt.UserRole + 1 + hiddenStateChanged = pyqtSignal() activeItemChanged = pyqtSignal(str, str) @@ -337,11 +340,35 @@ class GuiOutlineTree(QTreeWidget): iPx = self.mainTheme.baseIconSize self.setIconSize(QSize(iPx, iPx)) - self.setIndentation(iPx) + self.setIndentation(0) self.treeHead = self.header() self.treeHead.sectionMoved.connect(self._columnMoved) + # Pre-Generate Tree Formatting + fH1 = self.font() + fH1.setBold(True) + fH1.setUnderline(True) + + fH2 = self.font() + fH2.setBold(True) + + self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()] + self._pIndent = [ + self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx), + ] + self._dIcon = { + "H0": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"), + "H1": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"), + "H2": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"), + "H3": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"), + "H4": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"), + } + # Internals self._treeOrder = [] self._colWidth = {} @@ -449,7 +476,7 @@ class GuiOutlineTree(QTreeWidget): tHandle = None tLine = 0 if selItem: - tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) + tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1 return tHandle, tLine @@ -475,8 +502,8 @@ class GuiOutlineTree(QTreeWidget): """ selItems = self.selectedItems() if selItems: - tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) - sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole) + tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) + sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE) self.activeItemChanged.emit(tHandle, sTitle) return @@ -614,8 +641,7 @@ class GuiOutlineTree(QTreeWidget): self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem]) self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem]) - # Make sure title column is always visible, - # and handle column always hidden + # Make sure title column is always visible self.setColumnHidden(self._colIdx[nwOutline.TITLE], False) headItem = self.headerItem() @@ -623,101 +649,51 @@ class GuiOutlineTree(QTreeWidget): headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) - currTitle = None - currChapter = None - currScene = None - novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) for _, tHandle, sTitle, novIdx in novStruct: - tItem = self._createTreeItem(tHandle, sTitle, novIdx) + iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0) + dLevel = self.theProject.index.getHandleHeaderLevel(tHandle) + if iLevel == 0: + continue - tLevel = novIdx.level - if tLevel == "H1": - self.addTopLevelItem(tItem) - currTitle = tItem - currChapter = None - currScene = None + trItem = QTreeWidgetItem() + nwItem = self.theProject.tree[tHandle] - elif tLevel == "H2": - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - currChapter = tItem - currScene = None + trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, self._pIndent[iLevel]) + trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title) + trItem.setData(self._colIdx[nwOutline.TITLE], self.D_HANDLE, tHandle) + trItem.setData(self._colIdx[nwOutline.TITLE], self.D_TITLE, sTitle) + trItem.setFont(self._colIdx[nwOutline.TITLE], self._hFonts[iLevel]) + trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level) + trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[dLevel]) + trItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName) + trItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0")) + trItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis) + trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}") + trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}") + trItem.setText(self._colIdx[nwOutline.PCOUNT], f"{novIdx.paraCount:n}") + trItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight) + trItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) + trItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) - elif tLevel == "H3": - if currChapter is None: - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - else: - currChapter.addChild(tItem) - currScene = tItem + refs = self.theProject.index.getReferences(tHandle, sTitle) + trItem.setText(self._colIdx[nwOutline.POV], ", ".join(refs[nwKeyWords.POV_KEY])) + trItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY])) + trItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY])) + trItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(refs[nwKeyWords.PLOT_KEY])) + trItem.setText(self._colIdx[nwOutline.TIME], ", ".join(refs[nwKeyWords.TIME_KEY])) + trItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(refs[nwKeyWords.WORLD_KEY])) + trItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(refs[nwKeyWords.OBJECT_KEY])) + trItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(refs[nwKeyWords.ENTITY_KEY])) + trItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(refs[nwKeyWords.CUSTOM_KEY])) - elif tLevel == "H4": - if currScene is None: - if currChapter is None: - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - else: - currChapter.addChild(tItem) - else: - currScene.addChild(tItem) - - tItem.setExpanded(True) + self.addTopLevelItem(trItem) self._lastBuild = time() return - def _createTreeItem(self, tHandle, sTitle, novIdx): - """Populate a tree item with all the column values. - """ - nwItem = self.theProject.tree[tHandle] - newItem = QTreeWidgetItem() - hIcon = "doc_%s" % novIdx.level.lower() - - hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) - dIcon = self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel) - - cC = int(novIdx.charCount) - wC = int(novIdx.wordCount) - pC = int(novIdx.paraCount) - - newItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title) - newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle) - newItem.setIcon(self._colIdx[nwOutline.TITLE], self.mainTheme.getIcon(hIcon)) - newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level) - newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName) - newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon) - newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0")) - newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle) - newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis) - newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}") - newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}") - newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}") - newItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight) - newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) - newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) - - theRefs = self.theProject.index.getReferences(tHandle, sTitle) - newItem.setText(self._colIdx[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY])) - newItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY])) - newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY])) - newItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY])) - newItem.setText(self._colIdx[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY])) - newItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY])) - newItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY])) - newItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY])) - newItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY])) - - return newItem - # END Class GuiOutlineTree diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 13b0b619..acb5e8b4 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -177,7 +177,7 @@ class GuiProjectToolBar(QWidget): self.mainTheme = projView.mainGui.mainTheme iPx = self.mainTheme.baseIconSize - mPx = self.mainConf.pxInt(3) + mPx = self.mainConf.pxInt(2) self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) From 895beb6aa10457c4f360755141c8b7544d173730 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 15 Jul 2022 16:36:45 +0200 Subject: [PATCH 167/179] Revert change of spelling from UK to US --- setup/description_short.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/description_short.txt b/setup/description_short.txt index d3d30690..ac106560 100644 --- a/setup/description_short.txt +++ b/setup/description_short.txt @@ -2,5 +2,5 @@ novelWriter is a plain text editor designed for writing novels assembled from many smaller text documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data syntax for comments, synopsis, and cross-referencing. It's designed to be a simple text editor that allows for -easy organization of text and notes, using human readable text files as +easy organisation of text and notes, using human readable text files as storage for robustness. From 338d158baedaedede1207c988442ba75d5dba401 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 15 Jul 2022 16:39:12 +0200 Subject: [PATCH 168/179] Make some minor changes to the setup script --- setup.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/setup.py b/setup.py index fdf8e072..3bbefb9f 100755 --- a/setup.py +++ b/setup.py @@ -837,19 +837,19 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): ## -# Make Appimage (build-appimage) +# Make AppImage (build-appimage) ## -def makeAppimage(sysArgs): +def makeAppImage(sysArgs): """Build an Appimage """ + import glob import argparse import platform - import glob try: - import python_appimage + import python_appimage # noqa F401 except ImportError: print( "ERROR: Package 'python-appimage' is missing on this system.\n" @@ -858,21 +858,19 @@ def makeAppimage(sysArgs): sys.exit(1) print("") - print("Build Appimage") + print("Build AppImage") print("==============") print("") - plat = platform.machine() - parser = argparse.ArgumentParser( prog="build_appimage", - description="Build an Appimage", + description="Build an AppImage", epilog="see https://appimage.org/ for more details", ) parser.add_argument( "--linux-tag", nargs="?", - default=f"manylinux2010_{plat}", + default=f"manylinux2010_{platform.machine()}", help=( "linux compatibility tag (e.g. manylinux1_x86_64) \n" "see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n" @@ -891,7 +889,7 @@ def makeAppimage(sysArgs): # Version Info # ============ - numVers, hexVers, relDate = extractVersion() + numVers, _, relDate = extractVersion() pkgVers = compactVersion(numVers) relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") print("") @@ -928,7 +926,7 @@ def makeAppimage(sysArgs): outFiles = glob.glob(f"{bldDir}/*.AppImage") if outFiles: - print("Removing old Appimages") + print("Removing old AppImages") print("") for image in outFiles: try: @@ -1036,11 +1034,12 @@ def makeAppimage(sysArgs): # ============== try: - subprocess.call( - ["python", "-m", "python_appimage", "build", "app", - "-l", linuxTag, "-p", pythonVer, "appimage"], cwd=bldDir) + subprocess.call([ + sys.executable, "-m", "python_appimage", "build", "app", + "-l", linuxTag, "-p", pythonVer, "appimage" + ], cwd=bldDir) except Exception as exc: - print("Appimage build: FAILED") + print("AppImage build: FAILED") print("") print(str(exc)) print("") @@ -1804,6 +1803,8 @@ if __name__ == "__main__": " Add --snapshot to make a snapshot package.", " build-win-exe Build a setup.exe file with Python embedded for Windows.", " The package must be built from a minimal windows zip file.", + " build-appimage Build an AppImage. Argument --linux-tag defaults to", + " manylinux1_x86_64 / i386, and --python-version to 3.10.", "", "System Install:", "", @@ -1905,7 +1906,7 @@ if __name__ == "__main__": if "build-appimage" in sys.argv: sys.argv.remove("build-appimage") if hostOS == OS_LINUX: - sys.argv = makeAppimage(sys.argv) # Build appimage and prune it's args + sys.argv = makeAppImage(sys.argv) # Build appimage and prune its args else: print("ERROR: Command 'build-appimage' can only be used on Linux") sys.exit(1) From 069e3baf67814bdc45869c3f3ffb077264857b30 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jul 2022 18:40:16 +0200 Subject: [PATCH 169/179] Add functionality to load and save last selected outline novel, and fix a few inconsistencies --- .../assets/icons/typicons_dark/icons.conf | 1 + .../assets/icons/typicons_light/icons.conf | 1 + novelwriter/core/project.py | 21 +++--- novelwriter/gui/outline.py | 69 ++++++++++++++----- novelwriter/gui/projtree.py | 3 - novelwriter/guimain.py | 21 ++---- sample/nwProject.nwx | 9 ++- 7 files changed, 68 insertions(+), 57 deletions(-) diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index f372c60c..568c5b24 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -84,6 +84,7 @@ view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg +deco_doc_h0 = nw_deco-h0.svg deco_doc_h1 = nw_deco-h1.svg deco_doc_h2 = nw_deco-h2.svg deco_doc_h3 = nw_deco-h3.svg diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 4514eab9..1d6a6c0c 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -84,6 +84,7 @@ view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg +deco_doc_h0 = nw_deco-h0.svg deco_doc_h1 = nw_deco-h1.svg deco_doc_h2 = nw_deco-h2.svg deco_doc_h3 = nw_deco-h3.svg diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index fb81f033..4b470d69 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -97,7 +97,6 @@ class NWProject(): self.autoReplace = {} # Text to auto-replace on exports self.titleFormat = {} # The formatting of titles for exports self.spellCheck = False # Controls the spellcheck-as-you-type feature - self.autoOutline = True # If true, the Project Outline is updated automatically self.statusItems = None # Novel file progress status values self.importItems = None # Note file importance values self.lastEdited = None # The handle of the last file to be edited @@ -258,7 +257,6 @@ class NWProject(): "section": "", } self.spellCheck = False - self.autoOutline = True self.statusItems = NWStatus(NWStatus.STATUS) self.statusItems.write(None, self.tr("New"), (100, 100, 100)) self.statusItems.write(None, self.tr("Note"), (200, 50, 0)) @@ -608,8 +606,6 @@ class NWProject(): self.spellCheck = checkBool(xItem.text, False) elif xItem.tag == "spellLang": self.projSpell = checkString(xItem.text, None, True) - elif xItem.tag == "autoOutline": - self.autoOutline = checkBool(xItem.text, True) elif xItem.tag == "lastEdited": self.lastEdited = checkString(xItem.text, None, True) elif xItem.tag == "lastViewed": @@ -735,7 +731,6 @@ class NWProject(): self._packProjectValue(xSettings, "language", self.projLang) self._packProjectValue(xSettings, "spellCheck", self.spellCheck) self._packProjectValue(xSettings, "spellLang", self.projSpell) - self._packProjectValue(xSettings, "autoOutline", self.autoOutline) self._packProjectValue(xSettings, "lastEdited", self.lastEdited) self._packProjectValue(xSettings, "lastViewed", self.lastViewed) self._packProjectValue(xSettings, "lastNovel", self.lastNovel) @@ -1096,14 +1091,6 @@ class NWProject(): self.setProjectChanged(True) return True - def setAutoOutline(self, theMode): - """Enable/disable automatic update of project outline. - """ - if self.autoOutline != theMode: - self.autoOutline = theMode - self.setProjectChanged(True) - return self.autoOutline - def setTreeOrder(self, newOrder): """A list representing the linear/flattened order of project items in the GUI project tree. The user can rearrange the order @@ -1139,6 +1126,14 @@ class NWProject(): self.setProjectChanged(True) return True + def setLastOutlineViewed(self, tHandle): + """Set last viewed novel root in the outline view. + """ + if self.lastOutline != tHandle: + self.lastOutline = tHandle + self.setProjectChanged(True) + return True + def setStatusColours(self, newCols, delCols): """Update the list of novel file status flags. """ diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 49efe5f0..7c35ebb7 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -59,8 +59,9 @@ class GuiOutlineView(QWidget): def __init__(self, mainGui): QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + self.theProject = mainGui.theProject # Build GUI self.outlineBar = GuiOutlineToolBar(self) @@ -96,26 +97,43 @@ class GuiOutlineView(QWidget): # Methods ## - def splitSizes(self): - return self.splitOutline.sizes() - - def clearOutline(self): - self.outlineData.clearDetails() - return - def initOutline(self): self.outlineTree.initOutline() self.outlineData.initDetails() return + def refreshTree(self): + """Refresh the current tree. + """ + self.outlineTree.refreshTree(rootHandle=self.theProject.lastOutline) + return + + def clearProject(self): + self.outlineData.clearDetails() + return + + def openProjectTasks(self): + """Run opening project tasks. + """ + lastOutline = self.theProject.lastOutline + if not (lastOutline in self.theProject.tree or lastOutline is None): + lastOutline = self.theProject.tree.findRoot(nwItemClass.NOVEL) + + logger.debug("Setting outline tree to root item '%s'", lastOutline) + + self.clearProject() + self.outlineBar.populateNovelList() + self.outlineBar.setCurrentRoot(lastOutline) + + return + def closeOutline(self): self.outlineTree.closeOutline() self.outlineData.updateClasses() return - def refreshView(self, overRide=False, novelChanged=False): - self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged) - return + def splitSizes(self): + return self.splitOutline.sizes() def treeHasFocus(self): return self.outlineTree.hasFocus() @@ -243,6 +261,17 @@ class GuiOutlineToolBar(QToolBar): self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "") return + def setCurrentRoot(self, rootHandle): + """Set the current active root handle. + """ + if rootHandle is None: + rootIdx = self.novelValue.count() - 1 + else: + rootIdx = self.novelValue.findData(rootHandle) + if rootIdx >= 0: + self.novelValue.setCurrentIndex(rootIdx) + return + def setColumnHiddenState(self, hiddenState): """Forward the change of column hidden states to the menu. """ @@ -379,7 +408,7 @@ class GuiOutlineTree(QTreeWidget): self._lastBuild = 0 self.initOutline() - self.clearOutline() + self.clearContent() self.hiddenStateChanged.emit() @@ -415,7 +444,7 @@ class GuiOutlineTree(QTreeWidget): return - def clearOutline(self): + def clearContent(self): """Clear the tree and header and set the default values for the columns arrays. """ @@ -453,10 +482,12 @@ class GuiOutlineTree(QTreeWidget): # If the novel index or novel tree has changed since the tree # was last built, we rebuild the tree from the updated index. indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild) - doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline - if doBuild or overRide: - logger.debug("Rebuilding Project Outline") - self._populateTree(rootHandle) + if not (novelChanged or indexChanged or overRide): + logger.verbose("No changes have been made to the novel index") + return + + self._populateTree(rootHandle) + self.theProject.setLastOutlineViewed(rootHandle or None) return @@ -464,7 +495,7 @@ class GuiOutlineTree(QTreeWidget): """Called before a project is closed. """ self._saveHeaderState() - self.clearOutline() + self.clearContent() self._firstView = True return diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index acb5e8b4..a1b5bf8e 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -55,7 +55,6 @@ class GuiProjectView(QWidget): # Signals triggered when the meta data values of items change treeItemChanged = pyqtSignal(str) - novelItemChanged = pyqtSignal(str) rootFolderChanged = pyqtSignal(str) wordCountsChanged = pyqtSignal() @@ -1356,8 +1355,6 @@ class GuiProjectTree(QTreeWidget): itemType = tItem.itemType if itemType == nwItemType.ROOT: self.projView.rootFolderChanged.emit(tHandle) - elif itemType == nwItemType.FILE and tItem.isNovelLike(): - self.projView.novelItemChanged.emit(tHandle) self.projView.treeItemChanged.emit(tHandle) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index e34b873e..6374e768 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -199,7 +199,6 @@ class GuiMain(QMainWindow): self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox) self.projView.openDocumentRequest.connect(self._openDocument) - self.projView.novelItemChanged.connect(self._treeNovelItemChanged) self.projView.wordCountsChanged.connect(self._updateStatusWordCount) self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo) self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo) @@ -303,7 +302,7 @@ class GuiMain(QMainWindow): self.docEditor.clearEditor() self.docEditor.setDictionaries() self.closeDocViewer() - self.outlineView.clearOutline() + self.outlineView.clearProject() # General self.statusBar.clearStatus() @@ -365,8 +364,8 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.saveProject() self.docEditor.setDictionaries() - self.outlineView.updateRootItem(None) self.novelView.openProjectTasks() + self.outlineView.openProjectTasks() self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -514,8 +513,8 @@ class GuiMain(QMainWindow): self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.statusBar.setRefTime(self.theProject.projOpened) - self.outlineView.updateRootItem(None) self.novelView.openProjectTasks() + self.outlineView.openProjectTasks() self._updateStatusWordCount() # Restore previously open documents, if any @@ -1552,18 +1551,6 @@ class GuiMain(QMainWindow): return - @pyqtSlot() - def _treeNovelItemChanged(self): - """Triggered when there is a change to a novel item in the - project tree. - """ - if self.mainStack.currentIndex() == self.idxOutlineView: - logger.verbose("Novel tree changed while Outline tab active") - if self.hasProject: - self.outlineView.refreshView(novelChanged=True) - - return - @pyqtSlot() def _keyPressReturn(self): """Forward the return/enter keypress to the function that opens @@ -1593,7 +1580,7 @@ class GuiMain(QMainWindow): elif stIndex == self.idxOutlineView: logger.verbose("Outline View activated") if self.hasProject: - self.outlineView.refreshView() + self.outlineView.refreshTree() return diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 6ba4b5f1..b5bf2647 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,24 +1,23 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1371 + 1378 236 - 69222 + 69273 False en_GB True None - True 636b6aa9b697b 636b6aa9b697b 7031beac91f75 - None + 7031beac91f75 1363 954 409 From bb2c78c0f6c9eee90d2d20c615d0a9e0c763a4b8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jul 2022 18:50:43 +0200 Subject: [PATCH 170/179] Update tests --- tests/lipsum/nwProject.nwx | 1 - tests/minimal/nwProject.nwx | 1 - tests/reference/coreProject_NewCustomA_nwProject.nwx | 3 +-- tests/reference/coreProject_NewCustomB_nwProject.nwx | 1 - tests/reference/coreProject_NewFileFolder_nwProject.nwx | 3 +-- tests/reference/coreProject_NewMinimal_nwProject.nwx | 3 +-- tests/reference/coreProject_NewRoot_nwProject.nwx | 3 +-- tests/reference/guiEditor_Main_Final_nwProject.nwx | 5 ++--- tests/reference/guiEditor_Main_Initial_nwProject.nwx | 3 +-- tests/reference/guiProjSettings_Dialog_nwProject.nwx | 3 +-- tests/test_core/test_core_project.py | 6 ------ tests/test_gui/test_gui_guimain.py | 4 +--- tests/test_gui/test_gui_outline.py | 9 ++------- 13 files changed, 11 insertions(+), 34 deletions(-) diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index a8cf6393..4eac7bc6 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -13,7 +13,6 @@ en_GB False None - True 7a992350f3eb6 None None diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index 430ecf17..5862d48c 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -14,7 +14,6 @@ en_GB False None - True None None None diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index f046fceb..48ae6363 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -14,7 +14,6 @@ None False None - True None None None diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 5d02172c..9399dd3f 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -14,7 +14,6 @@ None False None - True None None None diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 20aeb027..b8df1c93 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,7 +13,6 @@ None False None - True None None None diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index ba08600f..633f9f4c 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -12,7 +12,6 @@ None False None - True None None None diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 0a606137..9102601d 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,7 +13,6 @@ None False None - True None None None diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index ecc96604..884ab272 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,11 +13,10 @@ None True None - True 000000000000f None 0000000000008 - None + 0000000000008 129 102 27 diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 1a79d2c2..0563c440 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,7 +13,6 @@ None False None - True None None None diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index 883cb26d..1db9d48c 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -14,7 +14,6 @@ None False en - True None None None diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index a6409b98..3be812e1 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -934,12 +934,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.localLookup(1) == "One" assert theProject.localLookup(10) == "Ten" - # Automatic outline update - theProject.projChanged = False - assert theProject.setAutoOutline(True) - assert not theProject.setAutoOutline(False) - assert theProject.projChanged - # Last edited theProject.projChanged = False assert theProject.setLastEdited("0123456789abc") diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 6407b84b..a27fc816 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -149,9 +149,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): with monkeypatch.context() as mp: mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - actItem = nwGUI.outlineView.outlineTree.topLevelItem(0) - chpItem = actItem.child(0) - selItem = chpItem.child(0) + selItem = nwGUI.outlineView.outlineTree.topLevelItem(2) nwGUI.outlineView.outlineTree.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 26ca9803..f530b418 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -232,9 +232,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): assert outlineData.pCValue.text() == "3" # Scene One - actItem = outlineTree.topLevelItem(1) - chpItem = actItem.child(0) - selItem = chpItem.child(0) + selItem = outlineTree.topLevelItem(4) outlineTree.setCurrentItem(selItem) tHandle, tLine = outlineTree.getSelectedHandle() @@ -252,10 +250,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): assert nwGUI.docViewer.docHandle() == "4c4f28287af27" # Scene One, Section Two - actItem = outlineTree.topLevelItem(1) - chpItem = actItem.child(0) - scnItem = chpItem.child(0) - selItem = scnItem.child(0) + selItem = outlineTree.topLevelItem(5) outlineTree.setCurrentItem(selItem) tHandle, tLine = outlineTree.getSelectedHandle() From cc7ae316ad730ce5bf42aeaa49e37db252fd97ca Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:08:25 +0200 Subject: [PATCH 171/179] Make some minor changes for consistency between similar classes --- novelwriter/gui/noveltree.py | 10 +++++---- novelwriter/gui/outline.py | 36 ++++++++++++++++++------------ novelwriter/guimain.py | 6 ++--- tests/test_gui/test_gui_outline.py | 4 ++-- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 0dec2551..c8d88b88 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -69,8 +69,8 @@ class GuiNovelView(QWidget): self.theProject = mainGui.theProject # Build GUI - self.novelTree = GuiNovelTree(self) self.novelBar = GuiNovelToolBar(self) + self.novelTree = GuiNovelTree(self) # Assemble self.outerBox = QVBoxLayout() @@ -93,6 +93,8 @@ class GuiNovelView(QWidget): ## def initSettings(self): + """Initialise GUI elements that depend on specific settings. + """ self.novelTree.initSettings() return @@ -110,7 +112,7 @@ class GuiNovelView(QWidget): return def openProjectTasks(self): - """Run opening project tasks. + """Run open project tasks. """ lastNovel = self.theProject.lastNovel if lastNovel not in self.theProject.tree: @@ -136,8 +138,8 @@ class GuiNovelView(QWidget): self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType) return - def setFocus(self): - """Forward the set focus call to the tree widget. + def setTreeFocus(self): + """Set the focus to the tree widget. """ self.novelTree.setFocus() return diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 7c35ebb7..e8514c15 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -97,9 +97,11 @@ class GuiOutlineView(QWidget): # Methods ## - def initOutline(self): - self.outlineTree.initOutline() - self.outlineData.initDetails() + def initSettings(self): + """Initialise GUI elements that depend on specific settings. + """ + self.outlineTree.initSettings() + self.outlineData.initSettings() return def refreshTree(self): @@ -109,11 +111,13 @@ class GuiOutlineView(QWidget): return def clearProject(self): + """Clear project-related GUI content. + """ self.outlineData.clearDetails() return def openProjectTasks(self): - """Run opening project tasks. + """Run open project tasks. """ lastOutline = self.theProject.lastOutline if not (lastOutline in self.theProject.tree or lastOutline is None): @@ -127,20 +131,24 @@ class GuiOutlineView(QWidget): return - def closeOutline(self): - self.outlineTree.closeOutline() + def closeProjectTasks(self): + self.outlineTree.closeProjectTasks() self.outlineData.updateClasses() return def splitSizes(self): return self.splitOutline.sizes() - def treeHasFocus(self): - return self.outlineTree.hasFocus() - def setTreeFocus(self): + """Set the focus to the tree widget. + """ return self.outlineTree.setFocus() + def treeHasFocus(self): + """Check if the outline tree has focus. + """ + return self.outlineTree.hasFocus() + ## # Public Slots ## @@ -407,7 +415,7 @@ class GuiOutlineTree(QTreeWidget): self._firstView = True self._lastBuild = 0 - self.initOutline() + self.initSettings() self.clearContent() self.hiddenStateChanged.emit() @@ -428,7 +436,7 @@ class GuiOutlineTree(QTreeWidget): # Methods ## - def initOutline(self): + def initSettings(self): """Set or update outline settings. """ # Scroll bars @@ -491,7 +499,7 @@ class GuiOutlineTree(QTreeWidget): return - def closeOutline(self): + def closeProjectTasks(self): """Called before a project is closed. """ self._saveHeaderState() @@ -971,13 +979,13 @@ class GuiOutlineDetails(QScrollArea): self.setWidgetResizable(True) self.setFrameStyle(QFrame.NoFrame) - self.initDetails() + self.initSettings() logger.debug("GuiOutlineDetails initialisation complete") return - def initDetails(self): + def initSettings(self): """Set or update outline settings. """ # Scroll bars diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6374e768..bad8c904 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -421,7 +421,7 @@ class GuiMain(QMainWindow): if saveOK: self.closeDocument() self.docViewer.clearNavHistory() - self.outlineView.closeOutline() + self.outlineView.closeProjectTasks() self.novelView.closeProjectTasks() self.theProject.closeProject(self.idleTime) @@ -930,7 +930,7 @@ class GuiMain(QMainWindow): self.docViewer.initViewer() self.projView.initSettings() self.novelView.initSettings() - self.outlineView.initOutline() + self.outlineView.initSettings() self._updateStatusWordCount() return @@ -1191,7 +1191,7 @@ class GuiMain(QMainWindow): if tabIdx == self.idxProjView: self.projView.setFocus() elif tabIdx == self.idxNovelView: - self.novelView.setFocus() + self.novelView.setTreeFocus() elif paneNo == nwWidget.EDITOR: self._changeView(nwView.EDITOR) self.docEditor.setFocus() diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index f530b418..2f33cf7a 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -54,7 +54,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): # Toggle scrollbars nwGUI.mainConf.hideVScroll = True nwGUI.mainConf.hideHScroll = True - outlineView.initOutline() + outlineView.initSettings() assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff @@ -62,7 +62,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): nwGUI.mainConf.hideVScroll = False nwGUI.mainConf.hideHScroll = False - outlineView.initOutline() + outlineView.initSettings() assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded From 2407b61830ece297b8667fd275ea0040a3f8c8d5 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Aug 2022 23:24:18 +0200 Subject: [PATCH 172/179] Fix typo in docstring and add reference to issue --- novelwriter/gui/doceditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index c26995ca..b127090f 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2010,7 +2010,7 @@ class GuiDocEditor(QTextEdit): def _allowSpaceBeforeColon(text, char): """Special checker function only used by the insert space feature for French, Spanish, etc, so it doesn't insert a - sapce before colons in meta data lines. + space before colons in meta data lines. See issue #1090. """ if char == ":" and len(text) > 1: if text[0] == "@": From 01b66bc62e4c961f058e353cf8bdc1cf9d5bf463 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Sep 2022 17:04:21 +0200 Subject: [PATCH 173/179] Add some type and layout checker functions to the item class --- novelwriter/core/item.py | 41 ++++++++++++++++++++++--------- tests/test_core/test_core_item.py | 5 ++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 88650475..95418079 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -292,6 +292,22 @@ class NWItem(): return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, "")) + def getImportStatus(self): + """Return the relevant importance or status label and icon for + the current item based on its class. + """ + if self.isNovelLike(): + stName = self.theProject.statusItems.name(self._status) + stIcon = self.theProject.statusItems.icon(self._status) + else: + stName = self.theProject.importItems.name(self._import) + stIcon = self.theProject.importItems.icon(self._import) + return stName, stIcon + + ## + # Checker Methods + ## + def isNovelLike(self): """Returns true if the item is of a novel-like class. """ @@ -307,17 +323,20 @@ class NWItem(): """ return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH) - def getImportStatus(self): - """Return the relevant importance or status label and icon for - the current item based on its class. - """ - if self.isNovelLike(): - stName = self.theProject.statusItems.name(self._status) - stIcon = self.theProject.statusItems.icon(self._status) - else: - stName = self.theProject.importItems.name(self._import) - stIcon = self.theProject.importItems.icon(self._import) - return stName, stIcon + def isRootType(self): + return self._type == nwItemType.ROOT + + def isFolderType(self): + return self._type == nwItemType.FOLDER + + def isFileType(self): + return self._type == nwItemType.FILE + + def isNoteLayout(self): + return self._layout == nwItemLayout.NOTE + + def isDocumentLayout(self): + return self._layout == nwItemLayout.DOCUMENT ## # Special Setters diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index fbc0ded3..e2fa75e0 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -203,12 +203,16 @@ def testCoreItem_Methods(mockGUI): theItem.setType("ROOT") assert theItem.describeMe() == "Root Folder" + assert theItem.isRootType() is True theItem.setType("FOLDER") assert theItem.describeMe() == "Folder" + assert theItem.isFolderType() is True theItem.setType("FILE") theItem.setLayout("DOCUMENT") + assert theItem.isFileType() is True + assert theItem.isDocumentLayout() is True assert theItem.describeMe() == "Novel Document" assert theItem.describeMe("H0") == "Novel Document" assert theItem.describeMe("H1") == "Novel Title Page" @@ -217,6 +221,7 @@ def testCoreItem_Methods(mockGUI): assert theItem.describeMe("H4") == "Novel Document" theItem.setLayout("NOTE") + assert theItem.isNoteLayout() is True assert theItem.describeMe() == "Project Note" # Status + Icon From 5cba10151a561dfa8c2e972f708a711768257249 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Sep 2022 17:06:16 +0200 Subject: [PATCH 174/179] Make the add new item feature a little smarter --- novelwriter/gui/projtree.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index a1b5bf8e..699938ac 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -41,7 +41,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.constants import trConst, nwLabels +from novelwriter.constants import nwHeaders, trConst, nwLabels from novelwriter.dialogs.editlabel import GuiEditLabel logger = logging.getLogger(__name__) @@ -447,16 +447,11 @@ class GuiProjectTree(QTreeWidget): ), nwAlert.ERROR) return False - # If the selected item is a file, the new item will be a - # sibling if the file has no children, otherwise a child + # Collect some information about the selected item that pItem = self.theProject.tree[sHandle] qItem = self._getTreeItem(sHandle) - if pItem.itemType == nwItemType.FILE and qItem.childCount() == 0: - nHandle = sHandle - sHandle = pItem.itemParent - if sHandle is None: - logger.error("Internal error") # Bug - return False + sLevel = nwHeaders.H_LEVEL.get(self.theProject.index.getHandleHeaderLevel(sHandle), 0) + sIsParent = False if qItem is None else qItem.childCount() > 0 if self.theProject.tree.isTrash(sHandle): self.mainGui.makeAlert(self.tr( @@ -464,19 +459,36 @@ class GuiProjectTree(QTreeWidget): ), nwAlert.ERROR) return False - # Ask for label + # Set default label and determine if new item is to be added + # as child or sibling to the selected item if itemType == nwItemType.FILE: if isNote: newLabel = self.tr("New Note") + asChild = sIsParent elif hLevel == 2: newLabel = self.tr("New Chapter") + asChild = sIsParent and pItem.isDocumentLayout() and sLevel < 2 elif hLevel == 3: newLabel = self.tr("New Scene") + asChild = sIsParent and pItem.isDocumentLayout() and sLevel < 3 else: newLabel = self.tr("New Document") + asChild = sIsParent and pItem.isDocumentLayout() else: newLabel = self.tr("New Folder") + asChild = False + if not (asChild or pItem.isFolderType() or pItem.isRootType()): + # Move to the parent item so that the new item is added + # as a sibling instead + nHandle = sHandle + sHandle = pItem.itemParent + if sHandle is None: + # Bug: We have a condition that is unhandled + logger.error("Internal error") + return False + + # Ask for label newLabel, dlgOk = GuiEditLabel.getLabel(self, text=newLabel) if not dlgOk: logger.info("New item creation cancelled by user") From 73b8e3e0fb0e07b34b42b2e18ab6ab6a9ed9bf1e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Sep 2022 17:22:46 +0200 Subject: [PATCH 175/179] Use the new item checker functions instead of equal operator --- novelwriter/core/index.py | 4 ++-- novelwriter/core/project.py | 2 +- novelwriter/core/tree.py | 6 +++--- novelwriter/dialogs/docmerge.py | 2 +- novelwriter/dialogs/docsplit.py | 4 ++-- novelwriter/gui/itemdetails.py | 5 ++--- novelwriter/gui/projtree.py | 20 ++++++++++---------- novelwriter/tools/build.py | 4 ++-- 8 files changed, 23 insertions(+), 24 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 0b3afdc5..7ea713b6 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -221,7 +221,7 @@ class NWIndex: if theItem is None: logger.info("Not indexing unknown item '%s'", tHandle) return False - if theItem.itemType != nwItemType.FILE: + if not theItem.isFileType(): logger.info("Not indexing non-file item '%s'", tHandle) return False @@ -792,7 +792,7 @@ class ItemIndex: for tItem in self.theProject.tree: if tItem is None: continue - if tItem.itemLayout == nwItemLayout.NOTE: + if tItem.isNoteLayout(): continue if skipExcl and not tItem.isExported: continue diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 4b470d69..1dffd1aa 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -183,7 +183,7 @@ class NWProject(): tItem = self._projTree[tHandle] if tItem is None: return False - if tItem.itemType != nwItemType.FILE: + if not tItem.isFileType(): return False newDoc = NWDoc(self, tHandle) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 10a7a78f..6bb24959 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -29,7 +29,7 @@ import logging from lxml import etree -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout +from novelwriter.enum import nwItemClass, nwItemLayout from novelwriter.error import logException from novelwriter.common import checkHandle from novelwriter.constants import nwFiles @@ -94,7 +94,7 @@ class NWTree(): nwItem.setHandle(tHandle) nwItem.setParent(pHandle) - if nwItem.itemType == nwItemType.ROOT: + if nwItem.isRootType(): logger.verbose("Item '%s' is a root item", str(tHandle)) self._treeRoots[tHandle] = nwItem if nwItem.itemClass == nwItemClass.ARCHIVE: @@ -357,7 +357,7 @@ class NWTree(): tItem = self.__getitem__(tHandle) if tItem is None: return False - if tItem.itemType != nwItemType.FILE: + if not tItem.isFileType(): logger.error("Item '%s' is not a file", tHandle) return False if not isinstance(itemLayout, nwItemLayout): diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 30eb602f..22a6eb6b 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -183,7 +183,7 @@ class GuiDocMerge(QDialog): for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle): newItem = QListWidgetItem() nwItem = self.theProject.tree[sHandle] - if nwItem.itemType is not nwItemType.FILE: + if not nwItem.isFileType(): continue newItem.setText(nwItem.itemName) newItem.setData(Qt.UserRole, sHandle) diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index b507479e..e4c9b13a 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.core import NWDoc -from novelwriter.enum import nwAlert, nwItemType +from novelwriter.enum import nwAlert from novelwriter.gui.custom import QHelpLabel logger = logging.getLogger(__name__) @@ -235,7 +235,7 @@ class GuiDocSplit(QDialog): if nwItem is None: return False - if nwItem.itemType is not nwItemType.FILE: + if not nwItem.isFileType(): self.mainGui.makeAlert(self.tr( "Element selected in the project tree must be a file." ), nwAlert.ERROR) diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 859e58f2..a467ed04 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -30,7 +30,6 @@ from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel -from novelwriter.enum import nwItemType from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) @@ -247,7 +246,7 @@ class GuiItemDetails(QWidget): if len(theLabel) > 100: theLabel = theLabel[:96].rstrip()+" ..." - if nwItem.itemType == nwItemType.FILE: + if nwItem.isFileType(): if nwItem.isExported: self.labelIcon.setPixmap(self._expCheck) else: @@ -284,7 +283,7 @@ class GuiItemDetails(QWidget): # Counts # ====== - if nwItem.itemType == nwItemType.FILE: + if nwItem.isFileType(): self.cCountData.setText(f"{nwItem.charCount:n}") self.wCountData.setText(f"{nwItem.wordCount:n}") self.pCountData.setText(f"{nwItem.paraCount:n}") diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 699938ac..b8e4dfbf 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -700,7 +700,7 @@ class GuiProjectTree(QTreeWidget): wCount = self._getItemWordCount(tHandle) autoFlush = not bulkAction - if nwItemS.itemType == nwItemType.ROOT: + if nwItemS.isRootType(): # Only an empty ROOT folder can be deleted logger.debug("User requested a root folder '%s' deleted", tHandle) tIndex = self.indexOfTopLevelItem(trItemS) @@ -716,7 +716,7 @@ class GuiProjectTree(QTreeWidget): ), nwAlert.ERROR) return False - elif nwItemS.itemType == nwItemType.FOLDER and trItemS.childCount() == 0: + elif nwItemS.isFolderType() and trItemS.childCount() == 0: # An empty FOLDER is just deleted without any further checks logger.debug("User requested an empty folder '%s' deleted", tHandle) trItemP = trItemS.parent() @@ -803,12 +803,12 @@ class GuiProjectTree(QTreeWidget): trItem.setIcon(self.C_STATUS, statusIcon) trItem.setToolTip(self.C_STATUS, itemStatus) - if nwItem.itemType == nwItemType.FILE: + if nwItem.isFileType(): trItem.setIcon( self.C_EXPORT, self.mainTheme.getIcon("check" if nwItem.isExported else "cross") ) - if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT: + if self.mainConf.emphLabels and nwItem.isDocumentLayout(): trFont = trItem.font(self.C_NAME) trFont.setBold(hLevel == "H1" or hLevel == "H2") trFont.setUnderline(hLevel == "H1") @@ -978,7 +978,7 @@ class GuiProjectTree(QTreeWidget): if tItem is None: return - if tItem.itemType == nwItemType.FILE: + if tItem.isFileType(): self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") else: trItem = self._getTreeItem(tHandle) @@ -1019,7 +1019,7 @@ class GuiProjectTree(QTreeWidget): # Document Actions # ================ - isFile = tItem.itemType == nwItemType.FILE + isFile = tItem.isFileType() if isFile: ctxMenu.addAction( self.tr("Open Document"), @@ -1059,7 +1059,7 @@ class GuiProjectTree(QTreeWidget): ) if isFile and tItem.documentAllowed(): - if tItem.itemLayout == nwItemLayout.NOTE: + if tItem.isNoteLayout(): ctxMenu.addAction( self.tr("Change to {0}").format( trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT]) @@ -1079,7 +1079,7 @@ class GuiProjectTree(QTreeWidget): # Delete Item # =========== - if tItem.itemClass == nwItemClass.TRASH or tItem.itemType == nwItemType.ROOT: + if tItem.itemClass == nwItemClass.TRASH or tItem.isRootType(): ctxMenu.addAction( self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle) ) @@ -1118,7 +1118,7 @@ class GuiProjectTree(QTreeWidget): if tItem is None: return - if tItem.itemType == nwItemType.FILE: + if tItem.isFileType(): self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") return @@ -1306,7 +1306,7 @@ class GuiProjectTree(QTreeWidget): self._treeMap[tHandle] = newItem if pHandle is None: - if nwItem.itemType == nwItemType.ROOT: + if nwItem.isRootType(): newItem.setFlags(newItem.flags() ^ Qt.ItemIsDragEnabled) self.addTopLevelItem(newItem) else: diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index efeff928..7a0eba6c 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -784,11 +784,11 @@ class GuiBuildNovel(QDialog): if not (theItem.isExported or ignoreFlag): return False - isNone = theItem.itemType != nwItemType.FILE + isNone = not theItem.isFileType() isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.isInactive() isNone |= theItem.itemParent is None - isNote = theItem.itemLayout == nwItemLayout.NOTE + isNote = theItem.isNoteLayout() isNovel = not isNone and not isNote if isNone: From 19c583f615ce732c274b1dda41158285bd4f60a2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Sep 2022 18:48:15 +0200 Subject: [PATCH 176/179] Fix misleading deletion label in context menu for empty folders --- novelwriter/gui/projtree.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index b8e4dfbf..9c2bf9fc 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1019,7 +1019,11 @@ class GuiProjectTree(QTreeWidget): # Document Actions # ================ + isRoot = tItem.isRootType() + isFolder = tItem.isFolderType() isFile = tItem.isFileType() + isEmpty = selItem.childCount() == 0 + if isFile: ctxMenu.addAction( self.tr("Open Document"), @@ -1079,7 +1083,7 @@ class GuiProjectTree(QTreeWidget): # Delete Item # =========== - if tItem.itemClass == nwItemClass.TRASH or tItem.isRootType(): + if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and isEmpty): ctxMenu.addAction( self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle) ) From 874f4f7198507438456aa67ebd0402409202c02d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Sep 2022 18:51:18 +0200 Subject: [PATCH 177/179] Change a few other project tree context menu labels --- novelwriter/gui/projtree.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 9c2bf9fc..e6acd81d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1048,14 +1048,14 @@ class GuiProjectTree(QTreeWidget): ) if tItem.isNovelLike(): - mStatus = ctxMenu.addMenu(self.tr("Change Status")) + mStatus = ctxMenu.addMenu(self.tr("Set Status to ...")) for n, (key, entry) in enumerate(self.theProject.statusItems.items()): aStatus = mStatus.addAction(entry["icon"], entry["name"]) aStatus.triggered.connect( lambda n, key=key: self._changeItemStatus(tHandle, key) ) else: - mImport = ctxMenu.addMenu(self.tr("Change Importance")) + mImport = ctxMenu.addMenu(self.tr("Set Importance to ...")) for n, (key, entry) in enumerate(self.theProject.importItems.items()): aImport = mImport.addAction(entry["icon"], entry["name"]) aImport.triggered.connect( @@ -1065,14 +1065,14 @@ class GuiProjectTree(QTreeWidget): if isFile and tItem.documentAllowed(): if tItem.isNoteLayout(): ctxMenu.addAction( - self.tr("Change to {0}").format( + self.tr("Convert to {0}").format( trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT]) ), lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT) ) else: ctxMenu.addAction( - self.tr("Change to {0}").format( + self.tr("Convert to {0}").format( trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE]) ), lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE) From 90fbfc30adf04021162991ac7738f38c29951ad0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:33:09 +0200 Subject: [PATCH 178/179] Add Ubuntu 22.10 to setup builds --- setup.py | 1 + setup/make_snapshot.sh | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100755 setup/make_snapshot.sh diff --git a/setup.py b/setup.py index 9121cc1e..a0bb9b03 100755 --- a/setup.py +++ b/setup.py @@ -851,6 +851,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): distLoop = [ ("20.04", "focal"), ("22.04", "jammy"), + ("22.10", "kinetic"), ] tStamp = datetime.datetime.now().strftime("%Y%m%d~%H%M%S") diff --git a/setup/make_snapshot.sh b/setup/make_snapshot.sh new file mode 100755 index 00000000..371996a0 --- /dev/null +++ b/setup/make_snapshot.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +if [ ! -f setup.py ]; then + echo "Must be called from the root folder of the source" + exit 1 +fi + +echo "" +echo " Building Dependencies" +echo "================================================================================" +echo "" +python3 setup.py clean-assets +python3 setup.py qtlrelease manual sample + +echo "" +echo " Building Linux Snapshots" +echo "================================================================================" +echo "" +python3 setup.py build-ubuntu --sign --snapshot From af55e57ba3bc10a5080fc613b0af6a3aa73356f0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 14 Sep 2022 16:48:46 +0200 Subject: [PATCH 179/179] Fix clipping of cursor due to 0 doc margin, issue #1112 --- novelwriter/gui/doceditor.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index b127090f..b1274f34 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -92,6 +92,7 @@ class GuiDocEditor(QTextEdit): self._spellCheck = False # Flag for spell checking enabled self._nonWord = "\"'" # Characters to not include in spell checking + self._vpMargin = 0 # The editor viewport margin, set during init # Document Variables self._charCount = 0 # Character count @@ -271,9 +272,12 @@ class GuiDocEditor(QTextEdit): self.docFooter.matchColours() # Set default text margins - cM = self.mainConf.getTextMargin() - qDoc.setDocumentMargin(0) - self.setViewportMargins(cM, cM, cM, cM) + # Due to cursor visibility, a part of the margin must be + # allocated to the document itself. See issue #1112. + cW = self.cursorWidth() + qDoc.setDocumentMargin(cW) + self._vpMargin = max(self.mainConf.getTextMargin() - cW, 0) + self.setViewportMargins(self._vpMargin, self._vpMargin, self._vpMargin, self._vpMargin) # Also set the document text options for the document text flow theOpt = QTextOption() @@ -537,7 +541,6 @@ class GuiDocEditor(QTextEdit): """ wW = self.width() wH = self.height() - cM = self.mainConf.getTextMargin() vBar = self.verticalScrollBar() sW = vBar.width() if vBar.isVisible() else 0 @@ -545,10 +548,10 @@ class GuiDocEditor(QTextEdit): hBar = self.horizontalScrollBar() sH = hBar.height() if hBar.isVisible() else 0 - tM = cM + tM = self._vpMargin if self.mainConf.textWidth > 0 or self.mainGui.isFocusMode: tW = self.mainConf.getTextWidth(self.mainGui.isFocusMode) - tM = max((wW - sW - tW)//2, cM) + tM = max((wW - sW - tW)//2, self._vpMargin) tB = self.frameWidth() tW = wW - 2*tB - sW @@ -565,8 +568,8 @@ class GuiDocEditor(QTextEdit): rL = wW - sW - rW - 2*tB self.docSearch.move(rL, 2*tB) - uM = max(cM, tH, rH) - lM = max(cM, fH) + uM = max(self._vpMargin, tH, rH) + lM = max(self._vpMargin, fH) self.setViewportMargins(tM, uM, tM, lM) return