From 3d3ac0d05da4f543a928e746551eb35db5230a7f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 19 Sep 2022 13:33:56 +0200
Subject: [PATCH 01/92] Allow converting folder to file in project tree (#1071)
---
novelwriter/gui/projtree.py | 46 +++++++++++++++++++++++++++++++++++--
1 file changed, 44 insertions(+), 2 deletions(-)
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index e6acd81d..a60c3dc3 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -1077,6 +1077,20 @@ class GuiProjectTree(QTreeWidget):
),
lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE)
)
+ elif isFolder:
+ if tItem.documentAllowed():
+ ctxMenu.addAction(
+ self.tr("Convert to {0}").format(
+ trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
+ ),
+ lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT)
+ )
+ ctxMenu.addAction(
+ self.tr("Convert to {0}").format(
+ trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
+ ),
+ lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE)
+ )
ctxMenu.addSeparator()
@@ -1269,6 +1283,32 @@ class GuiProjectTree(QTreeWidget):
self.setTreeItemValues(tItem.itemHandle)
return
+ def _covertFolderToFile(self, tHandle, itemLayout):
+ """Convert a folder to a note or document.
+ """
+ tItem = self.theProject.tree[tHandle]
+ if tItem is not None and tItem.isFolderType():
+ msgYes = self.mainGui.askQuestion(
+ self.tr("Convert Folder"),
+ self.tr(
+ "Do you want to convert the folder to a {0}? "
+ "This action cannot be reversed."
+ ).format(trConst(nwLabels.LAYOUT_NAME[itemLayout]))
+ )
+ if msgYes and itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
+ tItem.setType(nwItemType.FILE)
+ tItem.setLayout(nwItemLayout.DOCUMENT)
+ self.setTreeItemValues(tItem.itemHandle)
+ self._alertTreeChange(tHandle, flush=True)
+ elif msgYes and itemLayout == nwItemLayout.NOTE:
+ tItem.setType(nwItemType.FILE)
+ tItem.setLayout(nwItemLayout.NOTE)
+ self.setTreeItemValues(tItem.itemHandle)
+ self._alertTreeChange(tHandle, flush=True)
+ else:
+ logger.info("Folder conversion cancelled")
+ return
+
def _scanChildren(self, theList, tItem, tIndex):
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
@@ -1364,12 +1404,14 @@ class GuiProjectTree(QTreeWidget):
if flush:
self.saveTreeOrder()
+ if tHandle is None:
+ return
+
tItem = self.theProject.tree[tHandle]
if tItem is None:
return
- itemType = tItem.itemType
- if itemType == nwItemType.ROOT:
+ if tItem.isRootType():
self.projView.rootFolderChanged.emit(tHandle)
self.projView.treeItemChanged.emit(tHandle)
From 9c4b3dbc08e2d0e9943e4d095eb28c9331bca0b4 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 29 Sep 2022 14:09:03 +0200
Subject: [PATCH 02/92] Improve the tree change method
---
novelwriter/gui/projtree.py | 44 +++++++++++++++++++++----------------
1 file changed, 25 insertions(+), 19 deletions(-)
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index a60c3dc3..fd331c6a 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -537,7 +537,7 @@ class GuiProjectTree(QTreeWidget):
if pHandle is not None and pHandle in self._treeMap:
self._treeMap[pHandle].setExpanded(True)
- self._alertTreeChange(tHandle=tHandle, flush=True)
+ self._alertTreeChange(tHandle, flush=True)
self.clearSelection()
trItem.setSelected(True)
@@ -577,7 +577,7 @@ class GuiProjectTree(QTreeWidget):
pItem.insertChild(nIndex, cItem)
self._recordLastMove(cItem, pItem, tIndex)
- self._alertTreeChange(tHandle=tHandle, flush=True)
+ self._alertTreeChange(tHandle, flush=True)
self.clearSelection()
trItem.setSelected(True)
trItem.setExpanded(isExp)
@@ -595,7 +595,7 @@ class GuiProjectTree(QTreeWidget):
if dlgOk:
tItem.setName(newLabel)
self.setTreeItemValues(tHandle)
- self._alertTreeChange(tHandle=tHandle, flush=False)
+ self._alertTreeChange(tHandle, flush=False)
return
@@ -665,7 +665,7 @@ class GuiProjectTree(QTreeWidget):
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
if nTrash > 0:
- self._alertTreeChange(tHandle=trashHandle, flush=True)
+ self._alertTreeChange(trashHandle, flush=True)
return True
@@ -707,7 +707,7 @@ class GuiProjectTree(QTreeWidget):
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle)
- self._alertTreeChange(tHandle=tHandle, flush=True)
+ self._alertTreeChange(tHandle, flush=True)
else:
self.mainGui.makeAlert(self.tr(
"Cannot delete root folder. It is not empty. "
@@ -723,7 +723,7 @@ class GuiProjectTree(QTreeWidget):
tIndex = trItemP.indexOfChild(trItemS)
trItemP.takeChild(tIndex)
self._deleteTreeItem(tHandle)
- self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
+ self._alertTreeChange(tHandle, flush=autoFlush)
else:
# A populated FOLDER or a FILE requires confirmtation
@@ -759,7 +759,7 @@ class GuiProjectTree(QTreeWidget):
self.mainGui.closeDocument()
self._deleteTreeItem(dHandle)
- self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
+ self._alertTreeChange(tHandle, flush=autoFlush)
self.projView.wordCountsChanged.emit()
else:
@@ -778,7 +778,7 @@ class GuiProjectTree(QTreeWidget):
trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex)
- self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
+ self._alertTreeChange(tHandle, flush=autoFlush)
return True
@@ -904,7 +904,7 @@ class GuiProjectTree(QTreeWidget):
dstItem.insertChild(dstIndex, movItem)
self._postItemMove(sHandle, wCount)
- self._alertTreeChange(tHandle=sHandle, flush=True)
+ self._alertTreeChange(sHandle, flush=True)
self.clearSelection()
movItem.setSelected(True)
@@ -1168,7 +1168,7 @@ class GuiProjectTree(QTreeWidget):
QTreeWidget.dropEvent(self, theEvent)
self._postItemMove(sHandle, wCount)
self._recordLastMove(sItem, pItem, pIndex)
- self._alertTreeChange(tHandle=sHandle, flush=True)
+ self._alertTreeChange(sHandle, flush=True)
sItem.setExpanded(isExpanded)
return
@@ -1250,6 +1250,7 @@ class GuiProjectTree(QTreeWidget):
if tItem is not None:
tItem.setExported(not tItem.isExported)
self.setTreeItemValues(tItem.itemHandle)
+ self._alertTreeChange(tHandle, flush=False)
return
def _changeItemStatus(self, tHandle, tStatus):
@@ -1259,6 +1260,7 @@ class GuiProjectTree(QTreeWidget):
if tItem is not None:
tItem.setStatus(tStatus)
self.setTreeItemValues(tItem.itemHandle)
+ self._alertTreeChange(tHandle, flush=False)
return
def _changeItemImport(self, tHandle, tImport):
@@ -1268,6 +1270,7 @@ class GuiProjectTree(QTreeWidget):
if tItem is not None:
tItem.setImport(tImport)
self.setTreeItemValues(tItem.itemHandle)
+ self._alertTreeChange(tHandle, flush=False)
return
def _changeItemLayout(self, tHandle, itemLayout):
@@ -1277,10 +1280,12 @@ class GuiProjectTree(QTreeWidget):
if tItem is not None:
if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
tItem.setLayout(nwItemLayout.DOCUMENT)
- self.setTreeItemValues(tItem.itemHandle)
+ self.setTreeItemValues(tHandle)
+ self._alertTreeChange(tHandle, flush=False)
elif itemLayout == nwItemLayout.NOTE:
tItem.setLayout(nwItemLayout.NOTE)
- self.setTreeItemValues(tItem.itemHandle)
+ self.setTreeItemValues(tHandle)
+ self._alertTreeChange(tHandle, flush=False)
return
def _covertFolderToFile(self, tHandle, itemLayout):
@@ -1298,13 +1303,13 @@ class GuiProjectTree(QTreeWidget):
if msgYes and itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
tItem.setType(nwItemType.FILE)
tItem.setLayout(nwItemLayout.DOCUMENT)
- self.setTreeItemValues(tItem.itemHandle)
- self._alertTreeChange(tHandle, flush=True)
+ self.setTreeItemValues(tHandle)
+ self._alertTreeChange(tHandle, flush=False)
elif msgYes and itemLayout == nwItemLayout.NOTE:
tItem.setType(nwItemType.FILE)
tItem.setLayout(nwItemLayout.NOTE)
- self.setTreeItemValues(tItem.itemHandle)
- self._alertTreeChange(tHandle, flush=True)
+ self.setTreeItemValues(tHandle)
+ self._alertTreeChange(tHandle, flush=False)
else:
logger.info("Folder conversion cancelled")
return
@@ -1391,13 +1396,14 @@ class GuiProjectTree(QTreeWidget):
trItem = self._addTreeItem(self.theProject.tree[trashHandle])
if trItem is not None:
trItem.setExpanded(True)
- self._alertTreeChange(tHandle=trashHandle, flush=True)
+ self._alertTreeChange(trashHandle, flush=True)
return trItem
- def _alertTreeChange(self, tHandle=None, flush=True):
+ def _alertTreeChange(self, tHandle, flush=False):
"""Update information on tree change state, and emit necessary
- signals.
+ signals. A flush is only needed if an item is moved, created or
+ deleted.
"""
self._timeChanged = time()
self.theProject.setProjectChanged(True)
From 25dec2ace838472a35a19e184a13f36bc2ccfbe1 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Oct 2022 20:32:47 +0200
Subject: [PATCH 03/92] Add test coverage of convert folder feature
---
tests/test_gui/test_gui_projtree.py | 36 ++++++++++++++++++++++++++++-
1 file changed, 35 insertions(+), 1 deletion(-)
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index feef859d..4e381614 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -458,7 +458,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR
@pytest.mark.gui
-def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
+def testGuiProjTree_ContextMenu(qtbot, 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,
"""
@@ -470,6 +470,8 @@ def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
monkeypatch.setattr(QMenu, "exec_", lambda *a: None)
+ nwTree = nwGUI.projView
+
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
@@ -544,6 +546,38 @@ def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR
projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE)
assert nwItem.itemLayout == nwItemLayout.NOTE
+ # Convert Folders to Documents
+ # ============================
+
+ hNewFolderOne = "0000000000013"
+ hNewFolderTwo = "0000000000015"
+
+ nwTree.setSelectedHandle(hNovelNote)
+ assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
+ nwTree.setSelectedHandle(hNewFolderOne)
+ assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
+
+ nwTree.setSelectedHandle(hNovelNote)
+ assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
+ nwTree.setSelectedHandle(hNewFolderTwo)
+ assert nwTree.projTree.newTreeItem(nwItemType.FILE, isNote=True) is True
+
+ # Click no on the dialog
+ with monkeypatch.context() as mp:
+ mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
+ projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT)
+ assert nwGUI.theProject.tree[hNewFolderOne].isFolderType()
+
+ # Convert the first folder to a document
+ projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT)
+ assert nwGUI.theProject.tree[hNewFolderOne].isFileType()
+ assert nwGUI.theProject.tree[hNewFolderOne].isDocumentLayout()
+
+ # Convert the second folder to a note
+ projTree._covertFolderToFile(hNewFolderTwo, nwItemLayout.NOTE)
+ assert nwGUI.theProject.tree[hNewFolderTwo].isFileType()
+ assert nwGUI.theProject.tree[hNewFolderTwo].isNoteLayout()
+
# qtbot.stop()
# END Test testGuiProjTree_ContextMenu
From ba51fb4c462d99dc94ec0511ca5000e8bfec6cc4 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Oct 2022 21:17:47 +0200
Subject: [PATCH 04/92] Add Expand/Collapse All functionality to project tree
---
novelwriter/gui/projtree.py | 50 ++++++++++++++++++++++++++---
tests/test_gui/test_gui_projtree.py | 3 +-
2 files changed, 47 insertions(+), 6 deletions(-)
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index fd331c6a..e8fa541d 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -268,6 +268,12 @@ class GuiProjectToolBar(QWidget):
# More Options Menu
self.mMore = QMenu()
+ self.aExpand = self.mMore.addAction(self.tr("Expand All"))
+ self.aExpand.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, True))
+
+ self.aCollapse = self.mMore.addAction(self.tr("Collapse All"))
+ self.aCollapse.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, False))
+
self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move"))
self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove())
@@ -938,6 +944,15 @@ class GuiProjectTree(QTreeWidget):
return True
+ def setExpandedFromHandle(self, tHandle, isExpanded):
+ """Iterate through items below tHandle and change expanded
+ status for all child items. If tHandle is None, it affects the
+ entire tree.
+ """
+ trItem = self._getTreeItem(tHandle) or self.invisibleRootItem()
+ self._recursiveSetExpanded(trItem, isExpanded)
+ return
+
def openContextOnSelected(self):
"""Open the context menu on the current selected item.
"""
@@ -1022,7 +1037,7 @@ class GuiProjectTree(QTreeWidget):
isRoot = tItem.isRootType()
isFolder = tItem.isFolderType()
isFile = tItem.isFileType()
- isEmpty = selItem.childCount() == 0
+ hasChild = selItem.childCount() > 0
if isFile:
ctxMenu.addAction(
@@ -1094,10 +1109,24 @@ class GuiProjectTree(QTreeWidget):
ctxMenu.addSeparator()
+ # Expand/Collapse
+ # ===============
+
+ if hasChild:
+ ctxMenu.addSeparator()
+ ctxMenu.addAction(
+ self.tr("Expand All"),
+ lambda: self.setExpandedFromHandle(tHandle, True)
+ )
+ ctxMenu.addAction(
+ self.tr("Collapse All"),
+ lambda: self.setExpandedFromHandle(tHandle, False)
+ )
+
# Delete Item
# ===========
- if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and isEmpty):
+ if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
ctxMenu.addAction(
self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle)
)
@@ -1253,6 +1282,18 @@ class GuiProjectTree(QTreeWidget):
self._alertTreeChange(tHandle, flush=False)
return
+ def _recursiveSetExpanded(self, trItem, isExpanded):
+ """Recursive function to set expanded status starting from (and
+ not including) a given item.
+ """
+ if isinstance(trItem, QTreeWidgetItem):
+ chCount = trItem.childCount()
+ for i in range(chCount):
+ chItem = trItem.child(i)
+ chItem.setExpanded(isExpanded)
+ self._recursiveSetExpanded(chItem, isExpanded)
+ return
+
def _changeItemStatus(self, tHandle, tStatus):
"""Set a new status value of an item.
"""
@@ -1323,8 +1364,9 @@ class GuiProjectTree(QTreeWidget):
# Update tree-related meta data
nwItem = self.theProject.tree[tHandle]
- nwItem.setExpanded(tItem.isExpanded() and cCount > 0)
- nwItem.setOrder(tIndex)
+ if nwItem is not None:
+ nwItem.setExpanded(tItem.isExpanded() and cCount > 0)
+ nwItem.setOrder(tIndex)
theList.append(tHandle)
for i in range(cCount):
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index 4e381614..21ce22df 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -486,8 +486,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
hNovelNote = "0000000000012"
projTree = nwGUI.projView.projTree
- projTree._getTreeItem(hNovelRoot).setExpanded(True)
- projTree._getTreeItem(hChapterDir).setExpanded(True)
+ projTree.setExpandedFromHandle(None, True)
projTree._addTrashRoot()
hTrashRoot = projTree.theProject.tree.trashRoot()
From 479478490e3e022ed745c7866c7dd1b89530b823 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Oct 2022 22:02:00 +0200
Subject: [PATCH 05/92] Update the index to generate a ToC for a specific root
folder
---
novelwriter/core/index.py | 6 ++++--
tests/test_core/test_core_index.py | 12 ++++++------
2 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 7ea713b6..e9491027 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -498,13 +498,15 @@ class NWIndex:
"""
return self._itemIndex.mainItemHeader(tHandle)
- def getTableOfContents(self, maxDepth, skipExcl=True):
+ def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True):
"""Generate a table of contents up to a maximum depth.
"""
tOrder = []
tData = {}
pKey = None
- for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
+ for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(
+ rootHandle=rootHandle, skipExcl=skipExcl
+ ):
tKey = f"{tHandle}:{sTitle}"
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
if iLevel > maxDepth:
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 8f126e56..14aaa9be 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -709,16 +709,16 @@ def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd):
assert theIndex.getNovelTitleCounts(skipExcl=True) == [0, 1, 2, 3, 0]
# Table of Contents
- assert theIndex.getTableOfContents(0, skipExcl=True) == []
- assert theIndex.getTableOfContents(1, skipExcl=True) == [
+ assert theIndex.getTableOfContents("0000000000010", 0, skipExcl=True) == []
+ assert theIndex.getTableOfContents("0000000000010", 1, skipExcl=True) == [
("0000000000014:T000001", 1, "New Novel", 15),
]
- assert theIndex.getTableOfContents(2, skipExcl=True) == [
+ assert theIndex.getTableOfContents("0000000000010", 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, skipExcl=True) == [
+ assert theIndex.getTableOfContents("0000000000010", 3, skipExcl=True) == [
("0000000000014:T000001", 1, "New Novel", 5),
("0000000000016:T000001", 2, "New Chapter", 2),
("0000000000017:T000001", 3, "New Scene", 2),
@@ -727,8 +727,8 @@ def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd):
("%s:T000001" % tHandle, 3, "Scene Two", 2),
]
- assert theIndex.getTableOfContents(0, skipExcl=False) == []
- assert theIndex.getTableOfContents(1, skipExcl=False) == [
+ assert theIndex.getTableOfContents("0000000000010", 0, skipExcl=False) == []
+ assert theIndex.getTableOfContents("0000000000010", 1, skipExcl=False) == [
("0000000000014:T000001", 1, "New Novel", 9),
("%s:T000001" % nHandle, 1, "Hello World!", 12),
("%s:T000011" % nHandle, 1, "Hello World!", 22),
From 534614d0371c5fe6205ff29f92f800503316dcc9 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Oct 2022 22:02:24 +0200
Subject: [PATCH 06/92] Add a root folder combo box to the Project Details
dialog
---
novelwriter/dialogs/projdetails.py | 66 +++++++++++++++++++++++++-----
1 file changed, 56 insertions(+), 10 deletions(-)
diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py
index e83fc0a4..7b23498a 100644
--- a/novelwriter/dialogs/projdetails.py
+++ b/novelwriter/dialogs/projdetails.py
@@ -27,15 +27,17 @@ import math
import logging
import novelwriter
-from PyQt5.QtCore import Qt, QSize
+from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import (
- QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
- QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit, QAbstractItemView
+ QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QHBoxLayout,
+ QLabel, QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
+ QWidget
)
+from novelwriter.enum import nwItemClass
from novelwriter.common import numberToRoman
-from novelwriter.constants import nwUnicode
+from novelwriter.constants import nwLabels, nwUnicode
from novelwriter.gui.custom import PagedDialog, QSwitch
logger = logging.getLogger(__name__)
@@ -281,12 +283,26 @@ class GuiProjectDetailsContents(QWidget):
# Internal
self._theToC = []
+ self._currentRoot = None
iPx = self.mainTheme.baseIconSize
hPx = self.mainConf.pxInt(12)
vPx = self.mainConf.pxInt(4)
pOptions = self.theProject.options
+ # Header
+ # ======
+
+ self.tocLabel = QLabel("%s" % self.tr("Table of Contents"))
+
+ self.novelValue = QComboBox(self)
+ self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
+ self.novelValue.currentIndexChanged.connect(self._novelValueChanged)
+
+ self.headBox = QHBoxLayout()
+ self.headBox.addWidget(self.tocLabel)
+ self.headBox.addWidget(self.novelValue)
+
# Contents Tree
# =============
@@ -389,7 +405,7 @@ class GuiProjectDetailsContents(QWidget):
# ========
self.outerBox = QVBoxLayout()
- self.outerBox.addWidget(QLabel("%s" % self.tr("Table of Contents")))
+ self.outerBox.addLayout(self.headBox)
self.outerBox.addWidget(self.tocTree)
self.outerBox.addLayout(self.optionsBox)
@@ -412,19 +428,37 @@ class GuiProjectDetailsContents(QWidget):
def updateValues(self):
"""Populate the tree.
"""
- self._prepareData()
+ self._currentRoot = None
+ self._populateNovelList()
+
+ rootHandle = self.novelValue.currentData()
+ self._prepareData(rootHandle)
self._populateTree()
+
return
##
# Internal Functions
##
- def _prepareData(self):
- """Extract the data for the tree.
+ def _populateNovelList(self):
+ """Fill the novel combo box with a list of all novel folders.
"""
- self._theToC = []
- self._theToC = self.theProject.index.getTableOfContents(2)
+ self.novelValue.clear()
+
+ 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.setVisible(self.novelValue.count() > 1)
+
+ return
+
+ def _prepareData(self, rootHandle):
+ """Extract the information from the project index.
+ """
+ logger.debug("Populating ToC from handle '%s'", rootHandle)
+ self._theToC = self.theProject.index.getTableOfContents(rootHandle, 2)
self._theToC.append(("", 0, self.tr("END"), 0))
return
@@ -432,6 +466,18 @@ class GuiProjectDetailsContents(QWidget):
# Slots
##
+ @pyqtSlot()
+ def _novelValueChanged(self):
+ """Refresh the tree with another root item.
+ """
+ rootHandle = self.novelValue.currentData()
+ if rootHandle != self._currentRoot:
+ self._prepareData(rootHandle)
+ self._populateTree()
+ self._currentRoot = rootHandle
+ return
+
+ @pyqtSlot()
def _populateTree(self):
"""Set the content of the chapter/page tree.
"""
From 1aeb66d9af08691a57e3f1437e6e499fdb081740 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Oct 2022 22:35:00 +0200
Subject: [PATCH 07/92] Make sure existing space is stripped before inserting
non-breaking space (#1061)
---
novelwriter/gui/doceditor.py | 25 ++++++++++++++++---------
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 134b2e1c..e10f3fb4 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -111,6 +111,7 @@ class GuiDocEditor(QTextEdit):
self._typSQOpen = "'"
self._typSQClose = "'"
self._typPadChar = " "
+ self._doSpacePad = False
# Core Elements and Signals
qDoc = self.document()
@@ -221,6 +222,7 @@ class GuiDocEditor(QTextEdit):
self._nonWord += "".join(self.mainConf.fmtSingleQuotes)
# Typography
+ self._doSpacePad = bool(self.mainConf.fmtPadBefore) or bool(self.mainConf.fmtPadAfter)
if self.mainConf.fmtPadThin:
self._typPadChar = nwUnicode.U_THNBSP
else:
@@ -1992,16 +1994,21 @@ class GuiDocEditor(QTextEdit):
nDelete = 3
tInsert = nwUnicode.U_HELLIP
- tCheck = tInsert
- if tCheck in self.mainConf.fmtPadBefore:
- if self._allowSpaceBeforeColon(theText, tCheck):
- nDelete = max(nDelete, 1)
- tInsert = self._typPadChar + tInsert
+ if self._doSpacePad:
+ tCheck = tInsert
+ if tCheck in self.mainConf.fmtPadBefore:
+ if self._allowSpaceBeforeColon(theText, tCheck):
+ nDelete = max(nDelete, 1)
+ chkPos = thePos - nDelete - 1
+ if chkPos >= 0 and theText[chkPos].isspace():
+ # Strip existing space before inserting a new (#1061)
+ nDelete += 1
+ tInsert = self._typPadChar + tInsert
- if tCheck in self.mainConf.fmtPadAfter:
- if self._allowSpaceBeforeColon(theText, tCheck):
- nDelete = max(nDelete, 1)
- tInsert = tInsert + self._typPadChar
+ if tCheck in self.mainConf.fmtPadAfter:
+ if self._allowSpaceBeforeColon(theText, tCheck):
+ nDelete = max(nDelete, 1)
+ tInsert = tInsert + self._typPadChar
if nDelete > 0:
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
From 03db815723d0992b167cec63f1988cd667a53572 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Oct 2022 22:35:26 +0200
Subject: [PATCH 08/92] Update test coverage
---
tests/reference/guiEditor_Main_Final_000000000000f.nwd | 2 ++
tests/reference/guiEditor_Main_Final_nwProject.nwx | 8 ++++----
tests/test_gui/test_gui_guimain.py | 9 +++++++++
3 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/tests/reference/guiEditor_Main_Final_000000000000f.nwd b/tests/reference/guiEditor_Main_Final_000000000000f.nwd
index 1ac700fb..ab603231 100644
--- a/tests/reference/guiEditor_Main_Final_000000000000f.nwd
+++ b/tests/reference/guiEditor_Main_Final_000000000000f.nwd
@@ -35,6 +35,8 @@ Some “ double quoted text with spaces padded ”.
Add space before this colon : See?
+But don’t add a double space : See?
+
“Tab-indented text”
>“Paragraph-indented text”
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index 5496b5b3..f61d3493 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
@@ -17,8 +17,8 @@
None
0000000000008
0000000000008
- 145
- 118
+ 153
+ 126
27
@@ -59,7 +59,7 @@
New Chapter
-
-
+
New Scene
-
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index b2e446bc..1dd69314 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -412,6 +412,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Insert spaces before and after quotes
nwGUI.mainConf.fmtPadBefore = "\u201d"
nwGUI.mainConf.fmtPadAfter = "\u201c"
+ nwGUI.docEditor.initEditor()
for c in "Some \"double quoted text with spaces padded\".":
qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay)
@@ -420,9 +421,11 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
nwGUI.mainConf.fmtPadBefore = ""
nwGUI.mainConf.fmtPadAfter = ""
+ nwGUI.docEditor.initEditor()
# Insert spaces before colon, but ignore tags and synopsis
nwGUI.mainConf.fmtPadBefore = ":"
+ nwGUI.docEditor.initEditor()
for c in "@object: NoSpaceAdded":
qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay)
@@ -439,7 +442,13 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "But don't add a double space : See?":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+
nwGUI.mainConf.fmtPadBefore = ""
+ nwGUI.docEditor.initEditor()
# Indent and Align
# ================
From a24d37ffa4998a4b8c473d36040f5dd64e74070d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Oct 2022 22:41:18 +0200
Subject: [PATCH 09/92] Simplify the auto-insert space check code as this is
efficient enough
---
novelwriter/gui/doceditor.py | 29 +++++++++++++----------------
1 file changed, 13 insertions(+), 16 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index e10f3fb4..cfcb8409 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -111,7 +111,6 @@ class GuiDocEditor(QTextEdit):
self._typSQOpen = "'"
self._typSQClose = "'"
self._typPadChar = " "
- self._doSpacePad = False
# Core Elements and Signals
qDoc = self.document()
@@ -222,7 +221,6 @@ class GuiDocEditor(QTextEdit):
self._nonWord += "".join(self.mainConf.fmtSingleQuotes)
# Typography
- self._doSpacePad = bool(self.mainConf.fmtPadBefore) or bool(self.mainConf.fmtPadAfter)
if self.mainConf.fmtPadThin:
self._typPadChar = nwUnicode.U_THNBSP
else:
@@ -1994,21 +1992,20 @@ class GuiDocEditor(QTextEdit):
nDelete = 3
tInsert = nwUnicode.U_HELLIP
- if self._doSpacePad:
- tCheck = tInsert
- if tCheck in self.mainConf.fmtPadBefore:
- if self._allowSpaceBeforeColon(theText, tCheck):
- nDelete = max(nDelete, 1)
- chkPos = thePos - nDelete - 1
- if chkPos >= 0 and theText[chkPos].isspace():
- # Strip existing space before inserting a new (#1061)
- nDelete += 1
- tInsert = self._typPadChar + tInsert
+ tCheck = tInsert
+ if self.mainConf.fmtPadBefore and tCheck in self.mainConf.fmtPadBefore:
+ if self._allowSpaceBeforeColon(theText, tCheck):
+ nDelete = max(nDelete, 1)
+ chkPos = thePos - nDelete - 1
+ if chkPos >= 0 and theText[chkPos].isspace():
+ # Strip existing space before inserting a new (#1061)
+ nDelete += 1
+ tInsert = self._typPadChar + tInsert
- if tCheck in self.mainConf.fmtPadAfter:
- if self._allowSpaceBeforeColon(theText, tCheck):
- nDelete = max(nDelete, 1)
- tInsert = tInsert + self._typPadChar
+ if self.mainConf.fmtPadAfter and tCheck in self.mainConf.fmtPadAfter:
+ if self._allowSpaceBeforeColon(theText, tCheck):
+ nDelete = max(nDelete, 1)
+ tInsert = tInsert + self._typPadChar
if nDelete > 0:
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
From 47c39e68b7786e79bf1f9304380ef295548a52a6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Oct 2022 22:44:11 +0200
Subject: [PATCH 10/92] Remove the no longer needed changes to the test
---
tests/test_gui/test_gui_guimain.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 1dd69314..fd1218a0 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -412,7 +412,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Insert spaces before and after quotes
nwGUI.mainConf.fmtPadBefore = "\u201d"
nwGUI.mainConf.fmtPadAfter = "\u201c"
- nwGUI.docEditor.initEditor()
for c in "Some \"double quoted text with spaces padded\".":
qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay)
@@ -421,11 +420,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
nwGUI.mainConf.fmtPadBefore = ""
nwGUI.mainConf.fmtPadAfter = ""
- nwGUI.docEditor.initEditor()
# Insert spaces before colon, but ignore tags and synopsis
nwGUI.mainConf.fmtPadBefore = ":"
- nwGUI.docEditor.initEditor()
for c in "@object: NoSpaceAdded":
qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay)
@@ -448,7 +445,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
nwGUI.mainConf.fmtPadBefore = ""
- nwGUI.docEditor.initEditor()
# Indent and Align
# ================
From b80f5ba057f7b82997eead185427d1b2b2bafbd0 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 6 Oct 2022 09:10:58 +0200
Subject: [PATCH 11/92] Rename 1.7 to 2.0
---
CHANGELOG.md | 2 ++
novelwriter/__init__.py | 6 +++---
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16bb1b32..7171266f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## Version 1.7 Beta 1 [2022-05-17]
+**Note:** The 1.7 release was renamed to 2.0 on 2022-10-06. See #1144.
+
### Release Notes
This is a beta release of the next release version, and is intended for testing purposes. Please be
diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py
index 0ae10566..dc8f8c62 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-beta1"
-__hexversion__ = "0x010700b1"
-__date__ = "2022-05-17"
+__version__ = "2.0-beta1"
+__hexversion__ = "0x020000b1"
+__date__ = "2022-10-06"
__status__ = "Stable"
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
From e1c3a12d07a28053ac7222a0e5f0a8d6f960e354 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Oct 2022 15:30:02 +0200
Subject: [PATCH 12/92] Redesign the Document Merge dialog box
---
novelwriter/core/__init__.py | 2 +
novelwriter/core/doctools.py | 102 +++++++++++++++++
novelwriter/dialogs/docmerge.py | 189 +++++++++++++-------------------
novelwriter/gui/custom.py | 14 +--
novelwriter/gui/projtree.py | 62 +++++++++--
5 files changed, 238 insertions(+), 131 deletions(-)
create mode 100644 novelwriter/core/doctools.py
diff --git a/novelwriter/core/__init__.py b/novelwriter/core/__init__.py
index c91ca941..90b103f8 100644
--- a/novelwriter/core/__init__.py
+++ b/novelwriter/core/__init__.py
@@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+from novelwriter.core.doctools import DocMerger
from novelwriter.core.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
@@ -28,6 +29,7 @@ from novelwriter.core.toodt import ToOdt
from novelwriter.core.tomd import ToMarkdown
__all__ = [
+ "DocMerger",
"countWords",
"NWDoc",
"NWProject",
diff --git a/novelwriter/core/doctools.py b/novelwriter/core/doctools.py
new file mode 100644
index 00000000..b85d1c19
--- /dev/null
+++ b/novelwriter/core/doctools.py
@@ -0,0 +1,102 @@
+"""
+novelWriter – Project Document Tools
+====================================
+A collection of tools to create and manipulate documents
+
+File History:
+Created: 2022-10-02 [2.0b1]
+
+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
+
+logger = logging.getLogger(__name__)
+
+# logger.verbose("GuiDocMerge merge button clicked")
+
+# finalOrder = []
+# for i in range(self.listBox.count()):
+# finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
+
+# if len(finalOrder) == 0:
+# self.mainGui.makeAlert(self.tr(
+# "No source documents found. Nothing to do."
+# ), nwAlert.ERROR)
+# return False
+
+# theText = ""
+# for tHandle in finalOrder:
+# inDoc = NWDoc(self.theProject, tHandle)
+# docText = inDoc.readDocument()
+# docErr = inDoc.getError()
+# if docText is None and docErr:
+# 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.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.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
+# return False
+
+# nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
+# newItem = self.theProject.tree[nHandle]
+# newItem.setStatus(srcItem.itemStatus)
+# newItem.setImport(srcItem.itemImport)
+
+# outDoc = NWDoc(self.theProject, nHandle)
+# if not outDoc.writeDocument(theText):
+# self.mainGui.makeAlert([
+# self.tr("Could not save document."), outDoc.getError()
+# ], nwAlert.ERROR)
+# return False
+
+# self.mainGui.projView.revealNewTreeItem(nHandle)
+# self.mainGui.openDocument(nHandle, doScroll=True)
+
+# self._doClose()
+
+
+class DocMerger:
+
+ def __init__(self, theProject):
+
+ self.theProject = theProject
+
+ self._targetDoc = None
+
+ return
+
+ def setTargetDoc(self, tHandle):
+ return
+
+ def createNewDoc(self, docLabel, pHandle, itemLayout):
+ return
+
+ def appendDoc(self, tHandle):
+ return
+
+# END Class DocMerger
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index 22a6eb6b..2d248200 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -1,10 +1,11 @@
"""
-novelWriter – GUI Doc Merge Tool
-================================
-GUI class for merging multiple documents to one document
+novelWriter – GUI Doc Merge Dialog
+==================================
+Custom dialog class for merging documents.
File History:
-Created: 2020-01-23 [0.4.3]
+Created: 2020-01-23 [0.4.3]
+Rewritten: 2022-10-06 [2.0b1]
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -26,169 +27,131 @@ along with this program. If not, see .
import logging
import novelwriter
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
- QListWidgetItem, QDialogButtonBox
+ QListWidgetItem, QDialogButtonBox, QGridLayout
)
-from novelwriter.core import NWDoc
-from novelwriter.enum import nwAlert, nwItemType
-from novelwriter.gui.custom import QHelpLabel
+from novelwriter.gui.custom import QHelpLabel, QSwitch
logger = logging.getLogger(__name__)
class GuiDocMerge(QDialog):
- def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ def __init__(self, mainGui, sHandle, itemList):
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocMerge ...")
self.setObjectName("GuiDocMerge")
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
+ self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
- self.sourceItem = None
- self.outerBox = QVBoxLayout()
+ self._data = {}
+
self.setWindowTitle(self.tr("Merge Documents"))
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.mainTheme.helpText
- )
+ self.helpLabel = QHelpLabel(self.tr(
+ "Drag and drop items to change the order, or uncheck to exclude."
+ ), self.mainTheme.helpText)
+
+ iPx = self.mainTheme.baseIconSize
+ hSp = self.mainConf.pxInt(12)
+ vSp = self.mainConf.pxInt(8)
+ bSp = self.mainConf.pxInt(12)
self.listBox = QListWidget()
- self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
+ self.listBox.setIconSize(QSize(iPx, iPx))
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
+ self.listBox.setSelectionBehavior(QAbstractItemView.SelectRows)
+ self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
+ # Merge Options
+ self.trashLabel = QLabel(self.tr("Move merged items to Trash"))
+ self.trashSwitch = QSwitch()
+
+ self.optBox = QGridLayout()
+ self.optBox.addWidget(self.trashLabel, 0, 0)
+ self.optBox.addWidget(self.trashSwitch, 0, 1)
+ self.optBox.setHorizontalSpacing(hSp)
+ self.optBox.setColumnStretch(2, 1)
+
+ # Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
- self.buttonBox.accepted.connect(self._doMerge)
- self.buttonBox.rejected.connect(self._doClose)
+ self.buttonBox.accepted.connect(self.accept)
+ self.buttonBox.rejected.connect(self.reject)
+ # Assemble
+ self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel)
- self.outerBox.addSpacing(self.mainConf.pxInt(8))
+ self.outerBox.addSpacing(vSp)
self.outerBox.addWidget(self.listBox)
- self.outerBox.addSpacing(self.mainConf.pxInt(12))
+ self.outerBox.addSpacing(vSp)
+ self.outerBox.addLayout(self.optBox)
+ self.outerBox.addSpacing(bSp)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
- self.rejected.connect(self._doClose)
-
- self._populateList()
+ # Load Content
+ self._loadContent(sHandle, itemList)
logger.debug("GuiDocMerge initialisation complete")
return
- ##
- # Buttons
- ##
-
- def _doMerge(self):
- """Perform the merge of the files in the selected folder, and
- create a new file in the same parent folder. The old files are
- not removed in the merge process, and must be deleted manually.
+ def getData(self):
+ """Return the user's choices.
"""
- logger.verbose("GuiDocMerge merge button clicked")
-
- finalOrder = []
+ finalItems = []
for i in range(self.listBox.count()):
- finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
+ item = self.listBox.item(i)
+ if item.checkState() == Qt.Checked:
+ finalItems.append(item.data(Qt.UserRole))
- if len(finalOrder) == 0:
- self.mainGui.makeAlert(self.tr(
- "No source documents found. Nothing to do."
- ), nwAlert.ERROR)
- return False
+ self._data["moveToTrash"] = self.trashSwitch.isChecked()
+ self._data["finalItems"] = finalItems
- theText = ""
- for tHandle in finalOrder:
- inDoc = NWDoc(self.theProject, tHandle)
- docText = inDoc.readDocument()
- docErr = inDoc.getError()
- if docText is None and docErr:
- 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.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.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
- return False
-
- nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
- newItem = self.theProject.tree[nHandle]
- newItem.setStatus(srcItem.itemStatus)
- newItem.setImport(srcItem.itemImport)
-
- outDoc = NWDoc(self.theProject, nHandle)
- if not outDoc.writeDocument(theText):
- self.mainGui.makeAlert([
- self.tr("Could not save document."), outDoc.getError()
- ], nwAlert.ERROR)
- return False
-
- self.mainGui.projView.revealNewTreeItem(nHandle)
- self.mainGui.openDocument(nHandle, doScroll=True)
-
- self._doClose()
-
- return True
-
- def _doClose(self):
- """Close the dialog window without doing anything.
- """
- self.close()
- return
+ return self._data
##
# Internal Functions
##
- def _populateList(self):
- """Get the item selected in the tree, check that it is a folder,
- and try to find all files associated with it. The valid files
- are then added to the list view in order. The list itself can be
- reordered by the user.
+ def _loadContent(self, sHandle, itemList):
+ """Load content from a given list of items.
"""
- tHandle = self.mainGui.projView.getSelectedHandle()
- self.sourceItem = tHandle
- if tHandle is None:
- return False
+ self._data = {}
+ self._data["sHandle"] = sHandle
+ self._data["origItems"] = itemList
- nwItem = self.theProject.tree[tHandle]
- if nwItem is None:
- return False
-
- if nwItem.itemType is not nwItemType.FOLDER:
- self.mainGui.makeAlert(self.tr(
- "Element selected in the project tree must be a folder."
- ), nwAlert.ERROR)
- return False
-
- for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle):
- newItem = QListWidgetItem()
- nwItem = self.theProject.tree[sHandle]
- if not nwItem.isFileType():
+ self.listBox.clear()
+ for tHandle in itemList:
+ nwItem = self.theProject.tree[tHandle]
+ if nwItem is None or not nwItem.isFileType():
continue
+
+ hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
+ itemIcon = self.mainTheme.getItemIcon(
+ nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
+ )
+
+ newItem = QListWidgetItem()
+ newItem.setIcon(itemIcon)
newItem.setText(nwItem.itemName)
- newItem.setData(Qt.UserRole, sHandle)
+ newItem.setData(Qt.UserRole, tHandle)
+ newItem.setCheckState(Qt.Checked)
+
self.listBox.addItem(newItem)
- return True
+ return
# END Class GuiDocMerge
diff --git a/novelwriter/gui/custom.py b/novelwriter/gui/custom.py
index e571a5a1..2949ff6a 100644
--- a/novelwriter/gui/custom.py
+++ b/novelwriter/gui/custom.py
@@ -202,7 +202,7 @@ class QConfigLayout(QGridLayout):
class QHelpLabel(QLabel):
def __init__(self, theText, textCol, fontSize=0.9):
- QLabel.__init__(self, theText)
+ super().__init__(theText)
if isinstance(textCol, QColor):
qCol = textCol
@@ -377,7 +377,7 @@ class QSwitch(QAbstractButton):
class PagedDialog(QDialog):
def __init__(self, parent=None):
- QDialog.__init__(self, parent=parent)
+ super().__init__(parent=parent)
self._tabBar = VerticalTabBar(self)
self._tabBar.setExpanding(False)
@@ -410,13 +410,13 @@ class PagedDialog(QDialog):
return
def addTab(self, widget, label):
- """Forwards the adding of tabs to the QTabWidget.
+ """Forward the adding of tabs to the QTabWidget.
"""
self._tabBox.addTab(widget, label)
return
def addControls(self, buttonBar):
- """Adds a button bar to the dialog.
+ """Add a button bar to the dialog.
"""
self._buttonBox.addWidget(buttonBar)
return
@@ -427,14 +427,14 @@ class PagedDialog(QDialog):
class VerticalTabBar(QTabBar):
def __init__(self, parent=None):
- QTabBar.__init__(self, parent=parent)
+ super().__init__(parent=parent)
self._mW = novelwriter.CONFIG.pxInt(150)
return
def tabSizeHint(self, index):
- """Returns a transposed size hint for the rotated bar.
+ """Return a transposed size hint for the rotated bar.
"""
- tSize = QTabBar.tabSizeHint(self, index)
+ tSize = super().tabSizeHint(index)
tSize.transpose()
tSize.setWidth(min(tSize.width(), self._mW))
return tSize
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index e8fa541d..e841375a 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -34,15 +34,15 @@ from time import time
from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
- QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel,
+ QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel, QDialog,
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 import GuiDocMerge, GuiEditLabel
from novelwriter.constants import nwHeaders, trConst, nwLabels
-from novelwriter.dialogs.editlabel import GuiEditLabel
logger = logging.getLogger(__name__)
@@ -334,7 +334,7 @@ class GuiProjectTree(QTreeWidget):
self.mainConf = novelwriter.CONFIG
self.projView = projView
self.mainGui = projView.mainGui
- self.mainTheme = projView.mainGui.mainTheme
+ self.mainTheme = projView.mainGui.mainTheme
self.theProject = projView.mainGui.theProject
# Internal Variables
@@ -382,9 +382,8 @@ class GuiProjectTree(QTreeWidget):
trRoot = self.invisibleRootItem()
trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled)
- # Set Multiple Selection by CTRL
- # Disabled for now, until the merge files option has been added
- # self.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ # Set selection options
+ self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
# Connect signals
@@ -1077,16 +1076,21 @@ class GuiProjectTree(QTreeWidget):
lambda n, key=key: self._changeItemImport(tHandle, key)
)
+ # Transform Item
+ # ==============
+
+ mTrans = ctxMenu.addMenu(self.tr("Transform"))
+
if isFile and tItem.documentAllowed():
if tItem.isNoteLayout():
- ctxMenu.addAction(
+ mTrans.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
),
lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT)
)
else:
- ctxMenu.addAction(
+ mTrans.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
),
@@ -1094,19 +1098,37 @@ class GuiProjectTree(QTreeWidget):
)
elif isFolder:
if tItem.documentAllowed():
- ctxMenu.addAction(
+ mTrans.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
),
lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT)
)
- ctxMenu.addAction(
+ mTrans.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
),
lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE)
)
+ if hasChild:
+ if isFile:
+ mTrans.addAction(
+ self.tr("Merge Child Documents"),
+ lambda: self._mergeDocuments(tHandle, isFile)
+ )
+ else:
+ mTrans.addAction(
+ self.tr("Combine Documents in Folder"),
+ lambda: self._mergeDocuments(tHandle, isFile)
+ )
+
+ if isFile:
+ mTrans.addAction(
+ self.tr("Split Document by Header"),
+ lambda: self._splitDocument(tHandle)
+ )
+
ctxMenu.addSeparator()
# Expand/Collapse
@@ -1355,6 +1377,24 @@ class GuiProjectTree(QTreeWidget):
logger.info("Folder conversion cancelled")
return
+ def _mergeDocuments(self, tHandle, isFile):
+ """Merge an item's child documents into a single document.
+ """
+ logger.info("Request to merge items under handle '%s'", tHandle)
+ itemList = self.getTreeFromHandle(tHandle)
+ itemList.remove(tHandle)
+
+ dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
+ dlgMerge.exec_()
+
+ if dlgMerge.result() == QDialog.Accepted:
+ print(dlgMerge.getData())
+
+ return
+
+ def _splitDocument(self, tHandle):
+ return
+
def _scanChildren(self, theList, tItem, tIndex):
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
@@ -1380,7 +1420,7 @@ class GuiProjectTree(QTreeWidget):
"""
tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent
- newItem = QTreeWidgetItem([""]*4)
+ newItem = QTreeWidgetItem()
newItem.setText(self.C_NAME, "")
newItem.setText(self.C_COUNT, "0")
From 0f3d598f8d23d6003f496d9c57ca9060f8ee7670 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Oct 2022 17:19:01 +0200
Subject: [PATCH 13/92] Implement nre doc merge functionality in tree
---
novelwriter/core/doctools.py | 129 ++++++++++++++++++--------------
novelwriter/core/item.py | 6 +-
novelwriter/dialogs/docmerge.py | 16 ++++
novelwriter/gui/projtree.py | 75 ++++++++++++++++---
4 files changed, 156 insertions(+), 70 deletions(-)
diff --git a/novelwriter/core/doctools.py b/novelwriter/core/doctools.py
index b85d1c19..75a4c3b7 100644
--- a/novelwriter/core/doctools.py
+++ b/novelwriter/core/doctools.py
@@ -25,60 +25,10 @@ along with this program. If not, see .
import logging
+from novelwriter.core.document import NWDoc
+
logger = logging.getLogger(__name__)
-# logger.verbose("GuiDocMerge merge button clicked")
-
-# finalOrder = []
-# for i in range(self.listBox.count()):
-# finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
-
-# if len(finalOrder) == 0:
-# self.mainGui.makeAlert(self.tr(
-# "No source documents found. Nothing to do."
-# ), nwAlert.ERROR)
-# return False
-
-# theText = ""
-# for tHandle in finalOrder:
-# inDoc = NWDoc(self.theProject, tHandle)
-# docText = inDoc.readDocument()
-# docErr = inDoc.getError()
-# if docText is None and docErr:
-# 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.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.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
-# return False
-
-# nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
-# newItem = self.theProject.tree[nHandle]
-# newItem.setStatus(srcItem.itemStatus)
-# newItem.setImport(srcItem.itemImport)
-
-# outDoc = NWDoc(self.theProject, nHandle)
-# if not outDoc.writeDocument(theText):
-# self.mainGui.makeAlert([
-# self.tr("Could not save document."), outDoc.getError()
-# ], nwAlert.ERROR)
-# return False
-
-# self.mainGui.projView.revealNewTreeItem(nHandle)
-# self.mainGui.openDocument(nHandle, doScroll=True)
-
-# self._doClose()
-
class DocMerger:
@@ -86,17 +36,84 @@ class DocMerger:
self.theProject = theProject
+ self._error = ""
self._targetDoc = None
+ self._targetText = []
return
+ ##
+ # Methods
+ ##
+
+ def getError(self):
+ """Return any collected errors.
+ """
+ return self._error
+
def setTargetDoc(self, tHandle):
+ """Set the target document for the merging. Calling this
+ function resets the class.
+ """
+ self._targetDoc = tHandle
+ self._targetText = []
return
- def createNewDoc(self, docLabel, pHandle, itemLayout):
- return
+ def newTargetDoc(self, srcHandle, docLabel):
+ """Create a barnd new target document based on a source handle
+ and a new doc label. Calling this function resets the class.
+ """
+ srcItem = self.theProject.tree[srcHandle]
+ if srcItem is None:
+ return None
- def appendDoc(self, tHandle):
- return
+ newHandle = self.theProject.newFile(docLabel, srcItem.itemParent)
+ newItem = self.theProject.tree[newHandle]
+ newItem.setLayout(srcItem.itemLayout)
+ newItem.setStatus(srcItem.itemStatus)
+ newItem.setImport(srcItem.itemImport)
+
+ self._targetDoc = newHandle
+ self._targetText = []
+
+ return newHandle
+
+ def appendText(self, srcHandle, addComment, cmtPrefix):
+ """Append text from an existing document to the text buffer.
+ """
+ srcItem = self.theProject.tree[srcHandle]
+ if srcItem is None:
+ return False
+
+ inDoc = NWDoc(self.theProject, srcHandle)
+ docText = (inDoc.readDocument() or "").rstrip("\n")
+
+ if addComment:
+ docInfo = srcItem.describeMe("H0")
+ docSt, _ = srcItem.getImportStatus(incIcon=False)
+ cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n"
+ docText = cmtLine + docText
+
+ self._targetText.append(docText)
+
+ return True
+
+ def writeTargetDoc(self):
+ """Write the accumulated text into the designated target
+ document, appending any existing text.
+ """
+ if self._targetDoc is None:
+ return False
+
+ outDoc = NWDoc(self.theProject, self._targetDoc)
+ docText = (outDoc.readDocument() or "").rstrip("\n")
+ if docText:
+ self._targetText.insert(0, docText)
+
+ status = outDoc.writeDocument("\n\n".join(self._targetText))
+ if not status:
+ self._error = outDoc.getError()
+
+ return status
# END Class DocMerger
diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py
index 95418079..3f463a1b 100644
--- a/novelwriter/core/item.py
+++ b/novelwriter/core/item.py
@@ -292,16 +292,16 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
- def getImportStatus(self):
+ def getImportStatus(self, incIcon=True):
"""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)
+ stIcon = self.theProject.statusItems.icon(self._status) if incIcon else None
else:
stName = self.theProject.importItems.name(self._import)
- stIcon = self.theProject.importItems.icon(self._import)
+ stIcon = self.theProject.importItems.icon(self._import) if incIcon else None
return stName, stIcon
##
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index 2d248200..89149ab8 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -88,6 +88,9 @@ class GuiDocMerge(QDialog):
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
+ self.resetButton = self.buttonBox.addButton(QDialogButtonBox.Reset)
+ self.resetButton.clicked.connect(self._resetList)
+
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(0)
@@ -122,6 +125,19 @@ class GuiDocMerge(QDialog):
return self._data
+ ##
+ # Slots
+ ##
+
+ def _resetList(self):
+ """Reset the content of the list box to its original state.
+ """
+ logger.debug("Resetting list box content")
+ sHandle = self._data.get("sHandle", None)
+ itemList = self._data.get("origItems", [])
+ self._loadContent(sHandle, itemList)
+ return
+
##
# Internal Functions
##
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index e841375a..a32e0b48 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -39,7 +39,7 @@ from PyQt5.QtWidgets import (
QVBoxLayout, QWidget
)
-from novelwriter.core import NWDoc
+from novelwriter.core import NWDoc, DocMerger
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
from novelwriter.constants import nwHeaders, trConst, nwLabels
@@ -770,10 +770,14 @@ class GuiProjectTree(QTreeWidget):
else:
# The item is not already in the trash folder, so we
# move it there.
- msgYes = self.mainGui.askQuestion(
- self.tr("Delete"),
- self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
- )
+ if not alreadyAsked:
+ msgYes = self.mainGui.askQuestion(
+ self.tr("Delete"),
+ self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
+ )
+ else:
+ msgYes = True
+
if msgYes:
logger.debug("Moving item '%s' to trash", tHandle)
@@ -1114,13 +1118,17 @@ class GuiProjectTree(QTreeWidget):
if hasChild:
if isFile:
mTrans.addAction(
- self.tr("Merge Child Documents"),
- lambda: self._mergeDocuments(tHandle, isFile)
+ self.tr("Merge Child Items into Self"),
+ lambda: self._mergeDocuments(tHandle, False)
+ )
+ mTrans.addAction(
+ self.tr("Merge Child Items into New"),
+ lambda: self._mergeDocuments(tHandle, True)
)
else:
mTrans.addAction(
self.tr("Combine Documents in Folder"),
- lambda: self._mergeDocuments(tHandle, isFile)
+ lambda: self._mergeDocuments(tHandle, True)
)
if isFile:
@@ -1377,18 +1385,63 @@ class GuiProjectTree(QTreeWidget):
logger.info("Folder conversion cancelled")
return
- def _mergeDocuments(self, tHandle, isFile):
+ def _mergeDocuments(self, tHandle, newFile):
"""Merge an item's child documents into a single document.
"""
logger.info("Request to merge items under handle '%s'", tHandle)
itemList = self.getTreeFromHandle(tHandle)
- itemList.remove(tHandle)
+
+ tItem = self.theProject.tree[tHandle]
+ if tItem is None:
+ return
+
+ if not newFile:
+ itemList.remove(tHandle)
dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
dlgMerge.exec_()
if dlgMerge.result() == QDialog.Accepted:
- print(dlgMerge.getData())
+ # Save the open document first, in case it's part of this
+ self.mainGui.saveDocument()
+
+ # Set up the merge job
+ mrgData = dlgMerge.getData()
+ docMerger = DocMerger(self.theProject)
+ mLabel = self.tr("Merged")
+
+ if newFile:
+ docLabel = f"[{mLabel}] {tItem.itemName}"
+ mHandle = docMerger.newTargetDoc(tHandle, docLabel)
+ elif tItem.isFileType():
+ docMerger.setTargetDoc(tHandle)
+ mHandle = tHandle
+ else:
+ return
+
+ for sHandle in mrgData.get("finalItems", []):
+ docMerger.appendText(sHandle, True, mLabel)
+
+ if not docMerger.writeTargetDoc():
+ self.mainGui.makeAlert([
+ self.tr("Could not save document."), docMerger.getError()
+ ], nwAlert.ERROR)
+ return
+
+ if newFile:
+ self.mainGui.projView.revealNewTreeItem(mHandle, tHandle)
+
+ self.theProject.index.reIndexHandle(mHandle)
+ self.mainGui.openDocument(mHandle, doScroll=True)
+
+ if mrgData.get("moveToTrash", False):
+ for sHandle in reversed(mrgData.get("finalItems", [])):
+ trItem = self._getTreeItem(sHandle)
+ if isinstance(trItem, QTreeWidgetItem) and trItem.childCount() == 0:
+ self.deleteItem(sHandle, alreadyAsked=True, bulkAction=True)
+ self._alertTreeChange(sHandle, flush=False)
+
+ self._alertTreeChange(mHandle, flush=True)
return
From 2bda53e9713da6bf608a56df8210b99a0712c90f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Oct 2022 19:21:05 +0200
Subject: [PATCH 14/92] Split up the project tree deleteItem function
---
novelwriter/core/project.py | 17 ++
novelwriter/gui/mainmenu.py | 2 +-
novelwriter/gui/projtree.py | 249 +++++++++++++++--------------
tests/test_gui/test_gui_guimain.py | 4 +-
4 files changed, 149 insertions(+), 123 deletions(-)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 1dffd1aa..a6f88a7e 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -202,6 +202,23 @@ class NWProject():
return True
+ def removeItem(self, tHandle):
+ """Remove an item from the project. This will delete both the
+ project entry and a document file if it exists.
+ """
+ if self._projTree.checkType(tHandle, nwItemType.FILE):
+ delDoc = NWDoc(self, tHandle)
+ if not delDoc.deleteDocument():
+ self.mainGui.makeAlert([
+ self.tr("Could not delete document file."), delDoc.getError()
+ ], nwAlert.ERROR)
+ return False
+
+ self._projIndex.deleteHandle(tHandle)
+ del self._projTree[tHandle]
+
+ return True
+
def trashFolder(self):
"""Add the special trash root folder to the project.
"""
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 59af7691..b756327e 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -171,7 +171,7 @@ 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.projView.deleteItem(None))
+ self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
self.projMenu.addAction(self.aDeleteItem)
# Project > Empty Trash
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index a32e0b48..6ecf6699 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -39,7 +39,7 @@ from PyQt5.QtWidgets import (
QVBoxLayout, QWidget
)
-from novelwriter.core import NWDoc, DocMerger
+from novelwriter.core import DocMerger
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
from novelwriter.constants import nwHeaders, trConst, nwLabels
@@ -106,7 +106,7 @@ class GuiProjectView(QWidget):
self.renameTreeItem = self.projTree.renameTreeItem
self.getTreeFromHandle = self.projTree.getTreeFromHandle
self.emptyTrash = self.projTree.emptyTrash
- self.deleteItem = self.projTree.deleteItem
+ self.requestDeleteItem = self.projTree.requestDeleteItem
self.setTreeItemValues = self.projTree.setTreeItemValues
self.propagateCount = self.projTree.propagateCount
self.getSelectedHandle = self.projTree.getSelectedHandle
@@ -627,6 +627,42 @@ class GuiProjectTree(QTreeWidget):
theList = self._scanChildren(theList, theItem, 0)
return theList
+ def requestDeleteItem(self, tHandle=None):
+ """Request an item deleted from the project tree. This function
+ can be called on any item, and will check whether to attempt a
+ permanent deletion or moving the item to Trash.
+ """
+ if not self.mainGui.hasProject:
+ logger.error("No project open")
+ return False
+
+ if not self.hasFocus():
+ logger.info("Delete action blocked due to no widget focus")
+ return False
+
+ if tHandle is None:
+ tHandle = self.getSelectedHandle()
+
+ if tHandle is None:
+ logger.error("There is no item to delete")
+ return False
+
+ trashHandle = self.theProject.tree.trashRoot()
+ if tHandle == trashHandle:
+ logger.error("Cannot delete the Trash folder")
+ return False
+
+ nwItem = self.theProject.tree[tHandle]
+ if nwItem is None:
+ return False
+
+ if self.theProject.tree.isTrash(tHandle) or nwItem.isRootType():
+ self.permanentlyDeleteItem(tHandle)
+ else:
+ self.moveItemToTrash(tHandle)
+
+ return True
+
def emptyTrash(self):
"""Permanently delete all documents in the Trash folder. This
function only asks for confirmation once, and calls the regular
@@ -667,35 +703,17 @@ class GuiProjectTree(QTreeWidget):
for tHandle in reversed(self.getTreeFromHandle(trashHandle)):
if tHandle == trashHandle:
continue
- self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
+ self.permanentlyDeleteItem(tHandle, askFirst=False, flush=False)
if nTrash > 0:
self._alertTreeChange(trashHandle, flush=True)
return True
- def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False):
- """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. Root folders are deleted if they're empty
- only, and the deletion is always permanent.
+ def moveItemToTrash(self, tHandle, askFirst=True, flush=True):
+ """Move an item to Trash. Root folders cannot be moved to Trash,
+ so such a request is cancelled.
"""
- if not self.mainGui.hasProject:
- logger.error("No project open")
- return False
-
- if not self.hasFocus() and not bulkAction:
- logger.info("Delete action blocked due to no widget focus")
- return False
-
- if tHandle is None:
- tHandle = self.getSelectedHandle()
-
- if tHandle is None:
- logger.error("There is no item to delete")
- return False
-
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.tree[tHandle]
@@ -703,91 +721,99 @@ class GuiProjectTree(QTreeWidget):
logger.error("Could not find tree item for deletion")
return False
+ if self.theProject.tree.isTrash(tHandle):
+ logger.error("Item is already in the Trash folder")
+ return False
+
+ if nwItemS.isRootType():
+ logger.error("Root folders cannot be moved to Trash")
+ return False
+
+ logger.debug("User requested file or folder '%s' move to Trash", tHandle)
+
+ trItemP = trItemS.parent()
+ trItemT = self._addTrashRoot()
+ if trItemP is None or trItemT is None:
+ logger.error("Could not delete item")
+ return False
+
+ if askFirst:
+ msgYes = self.mainGui.askQuestion(
+ self.tr("Delete"),
+ self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
+ )
+ if not msgYes:
+ logger.info("Action cancelled by user")
+ return False
+
wCount = self._getItemWordCount(tHandle)
- autoFlush = not bulkAction
+ 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._alertTreeChange(tHandle, flush=flush)
+
+ logger.debug("Moved item '%s' to Trash", tHandle)
+
+ return True
+
+ def permanentlyDeleteItem(self, tHandle, askFirst=True, flush=False):
+ """Permanently delete a tree item from the project and the map.
+ Root items are handled a little different than other items.
+ """
+ trItemS = self._getTreeItem(tHandle)
+ nwItemS = self.theProject.tree[tHandle]
+ if trItemS is None or nwItemS is None:
+ logger.error("Could not find tree item for deletion")
+ return False
+
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)
- if trItemS.childCount() == 0:
- self.takeTopLevelItem(tIndex)
- self._deleteTreeItem(tHandle)
- self._alertTreeChange(tHandle, flush=True)
- else:
+ if trItemS.childCount() > 0:
self.mainGui.makeAlert(self.tr(
- "Cannot delete root folder. It is not empty. "
- "Recursive deletion is not supported. "
- "Please delete the content first."
+ "Root folders can only be deleted when they are empty."
), nwAlert.ERROR)
return False
- 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)
+ logger.debug("Permanently deleting root folder '%s'", tHandle)
+
+ tIndex = self.indexOfTopLevelItem(trItemS)
+ self.takeTopLevelItem(tIndex)
+ self.theProject.removeItem(tHandle)
+ self._treeMap.pop(tHandle, None)
+ self._alertTreeChange(tHandle, flush=True)
+
+ else:
+ if askFirst:
+ msgYes = self.mainGui.askQuestion(
+ self.tr("Delete"),
+ self.tr("Permanently delete '{0}'?").format(nwItemS.itemName)
+ )
+ if not msgYes:
+ logger.info("Action cancelled by user")
+ return False
+
+ logger.debug("Permanently deleting item '%s'", tHandle)
+
+ self.propagateCount(tHandle, 0)
+ itemList = self.getTreeFromHandle(tHandle)
+
trItemP = trItemS.parent()
tIndex = trItemP.indexOfChild(trItemS)
trItemP.takeChild(tIndex)
- self._deleteTreeItem(tHandle)
- self._alertTreeChange(tHandle, flush=autoFlush)
- 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()
- if trItemP is None or trItemT is None:
- logger.error("Could not delete item")
- return False
+ for dHandle in reversed(itemList):
+ if self.mainGui.docEditor.docHandle() == dHandle:
+ self.mainGui.closeDocument()
+ self.theProject.removeItem(tHandle)
+ self._treeMap.pop(tHandle, None)
- 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
- if not alreadyAsked:
- msgYes = self.mainGui.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.mainGui.docEditor.docHandle() == dHandle:
- self.mainGui.closeDocument()
- self._deleteTreeItem(dHandle)
-
- self._alertTreeChange(tHandle, flush=autoFlush)
- self.projView.wordCountsChanged.emit()
-
- else:
- # The item is not already in the trash folder, so we
- # move it there.
- if not alreadyAsked:
- msgYes = self.mainGui.askQuestion(
- self.tr("Delete"),
- self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
- )
- else:
- msgYes = True
-
- 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._alertTreeChange(tHandle, flush=autoFlush)
+ self._alertTreeChange(tHandle, flush=flush)
+ self.projView.wordCountsChanged.emit()
return True
@@ -1158,11 +1184,11 @@ class GuiProjectTree(QTreeWidget):
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
ctxMenu.addAction(
- self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle)
+ self.tr("Delete Permanently"), lambda: self.permanentlyDeleteItem(tHandle)
)
else:
ctxMenu.addAction(
- self.tr("Move to Trash"), lambda: self.deleteItem(tHandle)
+ self.tr("Move to Trash"), lambda: self.moveItemToTrash(tHandle)
)
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
@@ -1285,23 +1311,6 @@ class GuiProjectTree(QTreeWidget):
"""
return self._treeMap.get(tHandle, None)
- def _deleteTreeItem(self, tHandle):
- """Permanently delete a tree item from the project and the map.
- """
- if self.theProject.tree.checkType(tHandle, nwItemType.FILE):
- delDoc = NWDoc(self.theProject, tHandle)
- if not delDoc.deleteDocument():
- self.mainGui.makeAlert([
- self.tr("Could not delete document file."), delDoc.getError()
- ], nwAlert.ERROR)
- return False
-
- self.theProject.index.deleteHandle(tHandle)
- del self.theProject.tree[tHandle]
- self._treeMap.pop(tHandle, None)
-
- return True
-
def _toggleItemExported(self, tHandle):
"""Toggle the exported status of an item.
"""
@@ -1438,10 +1447,10 @@ class GuiProjectTree(QTreeWidget):
for sHandle in reversed(mrgData.get("finalItems", [])):
trItem = self._getTreeItem(sHandle)
if isinstance(trItem, QTreeWidgetItem) and trItem.childCount() == 0:
- self.deleteItem(sHandle, alreadyAsked=True, bulkAction=True)
- self._alertTreeChange(sHandle, flush=False)
+ self.moveItemToTrash(sHandle, askFirst=False, flush=False)
self._alertTreeChange(mHandle, flush=True)
+ self.projView.wordCountsChanged.emit()
return
@@ -1548,10 +1557,10 @@ class GuiProjectTree(QTreeWidget):
if tHandle is None:
return
- tItem = self.theProject.tree[tHandle]
- if tItem is None:
+ if tHandle not in self.theProject.tree:
return
+ tItem = self.theProject.tree[tHandle]
if tItem.isRootType():
self.projView.rootFolderChanged.emit(tHandle)
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index fd1218a0..f18e3050 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -504,9 +504,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None)
newHandle = nwGUI.projView.getSelectedHandle()
assert nwGUI.theProject.tree["0000000000020"] is not None
- assert nwGUI.projView.deleteItem()
+ assert nwGUI.projView.requestDeleteItem()
assert nwGUI.projView.setSelectedHandle(newHandle)
- assert nwGUI.projView.deleteItem()
+ assert nwGUI.projView.requestDeleteItem()
assert nwGUI.theProject.tree["0000000000024"] is not None # Trash
assert nwGUI.saveProject()
From 0859c145810cac056df8699bca7e370071d8a319 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Oct 2022 19:23:55 +0200
Subject: [PATCH 15/92] Remove the old merge document menu entry
---
novelwriter/gui/mainmenu.py | 5 -----
novelwriter/guimain.py | 16 ++--------------
tests/test_gui/test_gui_guimain.py | 1 -
3 files changed, 2 insertions(+), 20 deletions(-)
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index b756327e..7877d44a 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -244,11 +244,6 @@ class GuiMainMenu(QMenuBar):
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.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.mainGui.splitDocument())
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index bad8c904..7b165676 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -44,8 +44,8 @@ from novelwriter.gui import (
GuiViewsBar
)
from novelwriter.dialogs import (
- GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
- GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
+ GuiAbout, GuiDocSplit, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
+ GuiProjectSettings, GuiUpdates, GuiWordList
)
from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
@@ -745,18 +745,6 @@ class GuiMain(QMainWindow):
return True
- def mergeDocuments(self):
- """Merge multiple documents to one single new document.
- """
- if not self.hasProject:
- logger.error("No project open")
- return False
-
- dlgMerge = GuiDocMerge(self)
- dlgMerge.exec_()
-
- return True
-
def splitDocument(self):
"""Split a single document into multiple documents.
"""
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index f18e3050..5c75c406 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -54,7 +54,6 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
assert nwGUI.saveDocument() is False
assert nwGUI.viewDocument(None) is False
assert nwGUI.importDocument() is False
- assert nwGUI.mergeDocuments() is False
assert nwGUI.splitDocument() is False
assert nwGUI.openSelectedItem() is False
assert nwGUI.editItemLabel() is False
From 23b73dd936bd57ad2bfd1bb709e2f4d951b3cf03 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Oct 2022 20:49:25 +0200
Subject: [PATCH 16/92] Update tests for project tree deleting items
---
novelwriter/gui/projtree.py | 11 +-
tests/test_gui/test_gui_projtree.py | 475 +++++++++++++++++-----------
2 files changed, 297 insertions(+), 189 deletions(-)
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 6ecf6699..b47e6bc4 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -602,7 +602,7 @@ class GuiProjectTree(QTreeWidget):
self.setTreeItemValues(tHandle)
self._alertTreeChange(tHandle, flush=False)
- return
+ return True
def saveTreeOrder(self):
"""Build a list of the items in the project tree and send them
@@ -657,11 +657,11 @@ class GuiProjectTree(QTreeWidget):
return False
if self.theProject.tree.isTrash(tHandle) or nwItem.isRootType():
- self.permanentlyDeleteItem(tHandle)
+ status = self.permanentlyDeleteItem(tHandle)
else:
- self.moveItemToTrash(tHandle)
+ status = self.moveItemToTrash(tHandle)
- return True
+ return status
def emptyTrash(self):
"""Permanently delete all documents in the Trash folder. This
@@ -697,6 +697,7 @@ class GuiProjectTree(QTreeWidget):
self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
)
if not msgYes:
+ logger.info("Action cancelled by user")
return False
logger.verbose("Deleting %d file(s) from Trash", nTrash)
@@ -761,7 +762,7 @@ class GuiProjectTree(QTreeWidget):
return True
- def permanentlyDeleteItem(self, tHandle, askFirst=True, flush=False):
+ def permanentlyDeleteItem(self, tHandle, askFirst=True, flush=True):
"""Permanently delete a tree item from the project and the map.
Root items are handled a little different than other items.
"""
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index 21ce22df..e46418b9 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -19,8 +19,8 @@ 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 buildTestProject
@@ -42,113 +42,127 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
- nwTree = nwGUI.projView
+ projView = nwGUI.projView
+ projTree = nwGUI.projView.projTree
+ theProject = nwGUI.theProject
# Try to add item with no project
- assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
+ assert projView.projTree.newTreeItem(nwItemType.FILE) is False
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
# No itemType set
- nwTree.projTree.clearSelection()
- assert nwTree.projTree.newTreeItem(None) is False
+ projView.projTree.clearSelection()
+ assert projView.projTree.newTreeItem(None) is False
# Root Items
# ==========
# No class set
- assert nwTree.projTree.newTreeItem(nwItemType.ROOT) is False
+ assert projView.projTree.newTreeItem(nwItemType.ROOT) is False
# Create root item
- assert nwTree.projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
- assert "0000000000010" in nwGUI.theProject.tree
+ assert projView.projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
+ assert "0000000000010" in theProject.tree
# File/Folder Items
# =================
# No location selected for new item
- nwTree.projTree.clearSelection()
+ projView.projTree.clearSelection()
caplog.clear()
- assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
- assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is False
+ assert projView.projTree.newTreeItem(nwItemType.FILE) is False
+ assert projView.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.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
+ projView.setSelectedHandle("0000000000008")
+ assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True
+ assert theProject.tree["0000000000011"].itemParent == "0000000000008"
+ assert theProject.tree["0000000000011"].itemRoot == "0000000000008"
+ assert theProject.tree["0000000000011"].itemClass == nwItemClass.NOVEL
# Add a new file in the new folder
- nwTree.setSelectedHandle("0000000000011")
- 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
+ projView.setSelectedHandle("0000000000011")
+ assert projView.projTree.newTreeItem(nwItemType.FILE) is True
+ assert theProject.tree["0000000000012"].itemParent == "0000000000011"
+ assert theProject.tree["0000000000012"].itemRoot == "0000000000008"
+ assert theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL
# Add a new chapter next to the other new file
- nwTree.setSelectedHandle("0000000000012")
- 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
+ projView.setSelectedHandle("0000000000012")
+ assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True
+ assert theProject.tree["0000000000013"].itemParent == "0000000000011"
+ assert theProject.tree["0000000000013"].itemRoot == "0000000000008"
+ assert theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument("0000000000013")
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
# Add a new scene next to the other new file
- nwTree.setSelectedHandle("0000000000012")
- 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
+ projView.setSelectedHandle("0000000000012")
+ assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True
+ assert theProject.tree["0000000000014"].itemParent == "0000000000011"
+ assert theProject.tree["0000000000014"].itemRoot == "0000000000008"
+ assert 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.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
+ projView.setSelectedHandle("000000000000a")
+ assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True
+ assert theProject.tree["0000000000015"].itemParent == "000000000000a"
+ assert theProject.tree["0000000000015"].itemRoot == "000000000000a"
+ assert 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
- nwTree.setSelectedHandle("0000000000013")
- nwGUI.theProject.tree["0000000000013"].setParent(None) # This should not happen
+ projView.setSelectedHandle("0000000000013")
+ theProject.tree["0000000000013"].setParent(None) # This should not happen
caplog.clear()
- assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
+ assert projView.projTree.newTreeItem(nwItemType.FILE) is False
assert "Internal error" in caplog.text
- nwGUI.theProject.tree["0000000000013"].setParent("0000000000011")
+ theProject.tree["0000000000013"].setParent("0000000000011")
# Cancel during creation
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False))
- nwTree.setSelectedHandle("0000000000013")
- assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
+ projView.setSelectedHandle("0000000000013")
+ assert projView.projTree.newTreeItem(nwItemType.FILE) is False
# Get the trash folder
- nwTree.projTree._addTrashRoot()
- trashHandle = nwGUI.theProject.trashFolder()
- nwTree.setSelectedHandle(trashHandle)
- assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
+ projView.projTree._addTrashRoot()
+ trashHandle = theProject.trashFolder()
+ projView.setSelectedHandle(trashHandle)
+ assert projView.projTree.newTreeItem(nwItemType.FILE) is False
assert "Cannot add new files or folders to the Trash folder" in caplog.text
+ # Rename Item
+ # ===========
+
+ # Rename plot folder
+ with monkeypatch.context() as mp:
+ mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("Stuff", True))
+ projTree.renameTreeItem("0000000000009") is True
+ assert theProject.tree["0000000000009"].itemName == "Stuff"
+
+ # Rename invalid folder
+ projTree.renameTreeItem("0000000000000") is False
+
# Other Checks
# ============
# Also check error handling in reveal function
- assert nwTree.revealNewTreeItem("abc") is False
+ assert projView.revealNewTreeItem("abc") is False
# Add an item that cannot be displayed in the tree
- nHandle = nwGUI.theProject.newFile("Test", None)
- assert nwTree.revealNewTreeItem(nHandle) is False
+ nHandle = theProject.newFile("Test", None)
+ assert projView.revealNewTreeItem(nHandle) is False
# Clean up
- # qtbot.stopForInteraction()
+ # qtbot.stop()
nwGUI.closeProject()
# END Test testGuiProjTree_NewItems
@@ -270,8 +284,8 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
@pytest.mark.gui
-def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
- """Test adding and removing items from the project tree.
+def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
+ """Test external requests for removing items from project tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
@@ -280,181 +294,274 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
- nwTree = nwGUI.projView
+ nwView = nwGUI.projView
# Try to run with no project
- assert nwTree.emptyTrash() is False
- assert nwTree.deleteItem() is False
+ assert nwView.requestDeleteItem() is False
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
# Try emptying the trash already now, when there is no trash folder
- assert nwTree.emptyTrash() is False
+ assert nwView.emptyTrash() is False
# Add some files
- nwTree.setSelectedHandle("000000000000d")
- 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") == [
+ nwView.setSelectedHandle("000000000000d")
+ assert nwView.projTree.newTreeItem(nwItemType.FILE) is True
+ assert nwView.projTree.newTreeItem(nwItemType.FILE) is True
+ assert nwView.projTree.newTreeItem(nwItemType.FILE) is True
+ assert nwView.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010", "0000000000011", "0000000000012",
]
# Delete item without focus -> blocked
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
- nwTree.setSelectedHandle("0000000000012")
- assert nwTree.deleteItem() is False
+ nwView.setSelectedHandle("0000000000012")
+ assert nwView.requestDeleteItem() is False
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# No selection made
- nwTree.projTree.clearSelection()
+ nwView.projTree.clearSelection()
caplog.clear()
- assert nwTree.deleteItem() is False
+ assert nwView.requestDeleteItem() is False
assert "no item to delete" in caplog.text
# Not a valid handle
- nwTree.projTree.clearSelection()
+ nwView.projTree.clearSelection()
caplog.clear()
- assert nwTree.deleteItem("0000000000000") is False
- assert "Could not find tree item" in caplog.text
+ assert nwView.requestDeleteItem("0000000000000") is False
+ assert "No tree item with handle '0000000000000'" in caplog.text
- # Delete Folder/Root
- # ==================
+ # Delete Root Folders
+ # ===================
- # Deleting non-empty folders is blocked
- assert nwTree.deleteItem("0000000000008") is False # Novel Root
- assert nwTree.deleteItem("000000000000a") is True # Character Root
+ assert nwView.requestDeleteItem("0000000000008") is False # Novel Root is blocked
+ assert nwView.requestDeleteItem("000000000000a") is True # Character Root
# Delete File
# ===========
# Block adding trash folder
- funcPointer = nwTree.projTree._addTrashRoot
- nwTree.projTree._addTrashRoot = lambda *a: None
- assert nwTree.deleteItem("0000000000012") is False
- nwTree.projTree._addTrashRoot = funcPointer
+ funcPointer = nwView.projTree._addTrashRoot
+ nwView.projTree._addTrashRoot = lambda *a: None
+ assert nwView.requestDeleteItem("0000000000012") is False
+ nwView.projTree._addTrashRoot = funcPointer
# Delete last two documents, which also adds the trash folder
- assert nwTree.deleteItem("0000000000012") is True
- assert nwTree.deleteItem("0000000000011") is True
- assert nwTree.getTreeFromHandle("000000000000d") == [
+ assert nwView.requestDeleteItem("0000000000012") is True
+ assert nwView.requestDeleteItem("0000000000011") is True
+ assert nwView.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010"
]
trashHandle = nwGUI.theProject.tree.trashRoot()
- assert nwTree.getTreeFromHandle(trashHandle) == [
+ assert nwView.getTreeFromHandle(trashHandle) == [
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", "0000000000012.nwd"))
- 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.tree
- assert nwTree.getTreeFromHandle(trashHandle) == [
- trashHandle, "0000000000011"
- ]
+ # Try to delete the trash folder
+ caplog.clear()
+ assert nwView.requestDeleteItem("0000000000013") is False
+ assert "Cannot delete the Trash folder" in caplog.text
- # 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.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.tree
- assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
-
- # Delete Folder
- # =============
-
- trashHandle = nwGUI.theProject.tree.trashRoot()
-
- # Add a folder with two files
- nwTree.setSelectedHandle("0000000000009")
- assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
- nwTree.setSelectedHandle("0000000000014")
- 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"))
-
- # Delete the folder, which moves everything to Trash
- assert nwTree.getTreeFromHandle("0000000000014") == [
- "0000000000014", "0000000000015", "0000000000016"
- ]
- assert nwTree.deleteItem("0000000000014") is True
- 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 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"))
-
- # Add an empty folder, which can be deleted with no further restrictions
- nwTree.setSelectedHandle("0000000000009")
- assert nwTree.projTree.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
- # ===========
-
- # Try to empty trash that is already empty
- assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
- assert nwTree.emptyTrash() is False
-
- # Move the two remaining scene documents to trash
- assert nwTree.deleteItem("000000000000f") is True
- assert nwTree.deleteItem("0000000000010") is True
- assert nwTree.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000e"
- ]
- assert nwTree.getTreeFromHandle(trashHandle) == [
- trashHandle, "000000000000f", "0000000000010"
- ]
-
- # 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
- assert nwTree.emptyTrash() is True
- assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
-
- # 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.projTree._deleteTreeItem("000000000000e") is True
- assert not os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))
-
- # Clean up
- # qtbot.stopForInteraction()
nwGUI.closeProject()
-# END Test testGuiProjTree_DeleteItems
+# END Test testGuiProjTree_RequestDeleteItem
+
+
+@pytest.mark.gui
+def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
+ """Test moving items to Trash.
+ """
+ # 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(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
+
+ theProject = nwGUI.theProject
+ projTree = nwGUI.projView.projTree
+
+ # Create a project
+ prjDir = os.path.join(fncDir, "project")
+ buildTestProject(nwGUI, prjDir)
+
+ hInvalid = "0000000000000"
+ hNovelRoot = "0000000000008"
+ hTitlePage = "000000000000c"
+
+ # Invalid item
+ caplog.clear()
+ assert projTree.moveItemToTrash(hInvalid) is False
+ assert "Could not find tree item for deletion" in caplog.text
+
+ # Root folders cannot be moved to Trash
+ caplog.clear()
+ assert projTree.moveItemToTrash(hNovelRoot) is False
+ assert "Root folders cannot be moved to Trash" in caplog.text
+
+ # Block adding trash folder
+ funcPointer = projTree._addTrashRoot
+ projTree._addTrashRoot = lambda *a: None
+
+ caplog.clear()
+ assert projTree.moveItemToTrash(hTitlePage) is False
+ assert theProject.tree.isTrash(hTitlePage) is False
+ assert "Could not delete item" in caplog.text
+
+ projTree._addTrashRoot = funcPointer
+
+ # User cancels action
+ with monkeypatch.context() as mp:
+ mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
+ assert projTree.moveItemToTrash(hTitlePage) is False
+ assert theProject.tree.isTrash(hTitlePage) is False
+
+ # Move a document to Trash
+ assert projTree.moveItemToTrash(hTitlePage) is True
+ assert theProject.tree.isTrash(hTitlePage) is True
+
+ # Cannot be moved again
+ caplog.clear()
+ assert projTree.moveItemToTrash(hTitlePage) is False
+ assert "Item is already in the Trash folder" in caplog.text
+
+ nwGUI.closeProject()
+
+# END Test testGuiProjTree_MoveItemToTrash
+
+
+@pytest.mark.gui
+def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
+ """Test permanently deleting items.
+ """
+ # 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(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
+
+ theProject = nwGUI.theProject
+ projTree = nwGUI.projView.projTree
+
+ # Create a project
+ prjDir = os.path.join(fncDir, "project")
+ buildTestProject(nwGUI, prjDir)
+
+ hInvalid = "0000000000000"
+ hNovelRoot = "0000000000008"
+ hPlotRoot = "0000000000009"
+ hTitlePage = "000000000000c"
+ hChapterDir = "000000000000d"
+ hChapterDoc = "000000000000e"
+ hSceneDoc = "000000000000f"
+
+ # Invalid item
+ caplog.clear()
+ assert projTree.permanentlyDeleteItem(hInvalid) is False
+ assert "Could not find tree item for deletion" in caplog.text
+
+ # Not deleting root item in use
+ caplog.clear()
+ assert projTree.permanentlyDeleteItem(hNovelRoot) is False
+ assert "Root folders can only be deleted when they are empty" in caplog.text
+ assert hNovelRoot in theProject.tree
+
+ # Deleting unused root item is allowed
+ caplog.clear()
+ assert projTree.permanentlyDeleteItem(hPlotRoot) is True
+ assert hPlotRoot not in theProject.tree
+
+ # User cancels action
+ with monkeypatch.context() as mp:
+ mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
+ assert projTree.permanentlyDeleteItem(hTitlePage) is False
+ assert hTitlePage in theProject.tree
+
+ # Deleting file is OK, and if it is open, it should close
+ assert nwGUI.openDocument(hTitlePage) is True
+ assert nwGUI.docEditor.docHandle() == hTitlePage
+ assert projTree.permanentlyDeleteItem(hTitlePage) is True
+ assert hTitlePage not in theProject.tree
+ assert nwGUI.docEditor.docHandle() is None
+
+ # Deleting folder + files recursiely is ok
+ assert projTree.permanentlyDeleteItem(hChapterDir) is True
+ assert hChapterDir not in theProject.tree
+ assert hChapterDoc not in theProject.tree
+ assert hSceneDoc not in theProject.tree
+
+ nwGUI.closeProject()
+
+# END Test testGuiProjTree_PermanentlyDeleteItem
+
+
+@pytest.mark.gui
+def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
+ """Test emptying Trash.
+ """
+ # 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(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
+
+ theProject = nwGUI.theProject
+ projTree = nwGUI.projView.projTree
+
+ # No project open
+ caplog.clear()
+ assert projTree.emptyTrash() is False
+ assert "No project open" in caplog.text
+
+ # Create a project
+ prjDir = os.path.join(fncDir, "project")
+ buildTestProject(nwGUI, prjDir)
+
+ hTitlePage = "000000000000c"
+ hChapterDir = "000000000000d"
+ hChapterDoc = "000000000000e"
+ hSceneDoc = "000000000000f"
+
+ # No Trash folder
+ assert projTree.emptyTrash() is False
+
+ # Move some documents to Trash
+ assert projTree.moveItemToTrash(hTitlePage) is True
+ assert projTree.moveItemToTrash(hChapterDir) is True
+
+ assert theProject.tree.isTrash(hTitlePage) is True
+ assert theProject.tree.isTrash(hChapterDir) is True
+ assert theProject.tree.isTrash(hChapterDoc) is True
+ assert theProject.tree.isTrash(hSceneDoc) is True
+
+ # User cancels
+ with monkeypatch.context() as mp:
+ mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
+ assert projTree.emptyTrash() is False
+ assert hTitlePage in theProject.tree
+ assert hChapterDir in theProject.tree
+ assert hChapterDoc in theProject.tree
+ assert hSceneDoc in theProject.tree
+
+ # Run again to empty all items
+ assert projTree.emptyTrash() is True
+ assert hTitlePage not in theProject.tree
+ assert hChapterDir not in theProject.tree
+ assert hChapterDoc not in theProject.tree
+ assert hSceneDoc not in theProject.tree
+
+ # Running Emtpy Trash again is cancelled due to empty folder
+ assert projTree.emptyTrash() is False
+
+ nwGUI.closeProject()
+
+# END Test testGuiProjTree_EmptyTrash
@pytest.mark.gui
From d9a59bccdad92f06442deb68151108ff419c5618 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Oct 2022 21:30:21 +0200
Subject: [PATCH 17/92] Don't allow merging root folders
---
novelwriter/gui/projtree.py | 94 +++++++++++++++++--------------------
1 file changed, 42 insertions(+), 52 deletions(-)
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index b47e6bc4..93478c7a 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -1110,67 +1110,55 @@ class GuiProjectTree(QTreeWidget):
# Transform Item
# ==============
- mTrans = ctxMenu.addMenu(self.tr("Transform"))
+ if not isRoot:
+ mTrans = ctxMenu.addMenu(self.tr("Transform"))
- if isFile and tItem.documentAllowed():
- if tItem.isNoteLayout():
+ trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
+ trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
+
+ isDocFile = tItem.isDocumentLayout() and isFile
+ isNoteFile = tItem.isNoteLayout() and isFile
+
+ if (isNoteFile or isFolder) and tItem.documentAllowed():
mTrans.addAction(
- self.tr("Convert to {0}").format(
- trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
- ),
+ self.tr("Convert to {0}").format(trDoc),
lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT)
)
- else:
+
+ if isDocFile or isFolder:
mTrans.addAction(
- self.tr("Convert to {0}").format(
- trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
- ),
+ self.tr("Convert to {0}").format(trNote),
lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE)
)
- elif isFolder:
- if tItem.documentAllowed():
- mTrans.addAction(
- self.tr("Convert to {0}").format(
- trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
- ),
- lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT)
- )
- mTrans.addAction(
- self.tr("Convert to {0}").format(
- trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
- ),
- lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE)
- )
- if hasChild:
+ if hasChild:
+ if isFile:
+ mTrans.addAction(
+ self.tr("Merge Child Items into Self"),
+ lambda: self._mergeDocuments(tHandle, False)
+ )
+ mTrans.addAction(
+ self.tr("Merge Child Items into New"),
+ lambda: self._mergeDocuments(tHandle, True)
+ )
+ elif isFolder:
+ mTrans.addAction(
+ self.tr("Combine Documents in Folder"),
+ lambda: self._mergeDocuments(tHandle, True)
+ )
+
if isFile:
mTrans.addAction(
- self.tr("Merge Child Items into Self"),
- lambda: self._mergeDocuments(tHandle, False)
- )
- mTrans.addAction(
- self.tr("Merge Child Items into New"),
- lambda: self._mergeDocuments(tHandle, True)
- )
- else:
- mTrans.addAction(
- self.tr("Combine Documents in Folder"),
- lambda: self._mergeDocuments(tHandle, True)
+ self.tr("Split Document by Header"),
+ lambda: self._splitDocument(tHandle)
)
- if isFile:
- mTrans.addAction(
- self.tr("Split Document by Header"),
- lambda: self._splitDocument(tHandle)
- )
+ # Expand/Collapse/Delete
+ # ======================
ctxMenu.addSeparator()
- # Expand/Collapse
- # ===============
-
if hasChild:
- ctxMenu.addSeparator()
ctxMenu.addAction(
self.tr("Expand All"),
lambda: self.setExpandedFromHandle(tHandle, True)
@@ -1180,9 +1168,6 @@ class GuiProjectTree(QTreeWidget):
lambda: self.setExpandedFromHandle(tHandle, False)
)
- # Delete Item
- # ===========
-
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
ctxMenu.addAction(
self.tr("Delete Permanently"), lambda: self.permanentlyDeleteItem(tHandle)
@@ -1192,6 +1177,7 @@ class GuiProjectTree(QTreeWidget):
self.tr("Move to Trash"), lambda: self.moveItemToTrash(tHandle)
)
+ # Show Context Menu
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
return True
@@ -1403,7 +1389,11 @@ class GuiProjectTree(QTreeWidget):
tItem = self.theProject.tree[tHandle]
if tItem is None:
- return
+ return False
+
+ if tItem.isRootType():
+ logger.error("Cannot merge root item")
+ return False
if not newFile:
itemList.remove(tHandle)
@@ -1427,7 +1417,7 @@ class GuiProjectTree(QTreeWidget):
docMerger.setTargetDoc(tHandle)
mHandle = tHandle
else:
- return
+ return False
for sHandle in mrgData.get("finalItems", []):
docMerger.appendText(sHandle, True, mLabel)
@@ -1436,7 +1426,7 @@ class GuiProjectTree(QTreeWidget):
self.mainGui.makeAlert([
self.tr("Could not save document."), docMerger.getError()
], nwAlert.ERROR)
- return
+ return False
if newFile:
self.mainGui.projView.revealNewTreeItem(mHandle, tHandle)
@@ -1453,7 +1443,7 @@ class GuiProjectTree(QTreeWidget):
self._alertTreeChange(mHandle, flush=True)
self.projView.wordCountsChanged.emit()
- return
+ return True
def _splitDocument(self, tHandle):
return
From 02dc7f5fe0b7f0aac41988ab29a1fffc145b939f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Oct 2022 21:40:50 +0200
Subject: [PATCH 18/92] Make a few more improvements to doc merge
---
novelwriter/gui/projtree.py | 56 ++++++++++++++++++++++---------------
1 file changed, 34 insertions(+), 22 deletions(-)
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 93478c7a..ac257b93 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -1116,8 +1116,8 @@ class GuiProjectTree(QTreeWidget):
trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
- isDocFile = tItem.isDocumentLayout() and isFile
- isNoteFile = tItem.isNoteLayout() and isFile
+ isDocFile = isFile and tItem.isDocumentLayout()
+ isNoteFile = isFile and tItem.isNoteLayout()
if (isNoteFile or isFolder) and tItem.documentAllowed():
mTrans.addAction(
@@ -1131,21 +1131,21 @@ class GuiProjectTree(QTreeWidget):
lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE)
)
- if hasChild:
- if isFile:
- mTrans.addAction(
- self.tr("Merge Child Items into Self"),
- lambda: self._mergeDocuments(tHandle, False)
- )
- mTrans.addAction(
- self.tr("Merge Child Items into New"),
- lambda: self._mergeDocuments(tHandle, True)
- )
- elif isFolder:
- mTrans.addAction(
- self.tr("Combine Documents in Folder"),
- lambda: self._mergeDocuments(tHandle, True)
- )
+ if hasChild and isFile:
+ mTrans.addAction(
+ self.tr("Merge Child Items into Self"),
+ lambda: self._mergeDocuments(tHandle, False)
+ )
+ mTrans.addAction(
+ self.tr("Merge Child Items into New"),
+ lambda: self._mergeDocuments(tHandle, True)
+ )
+
+ if hasChild and isFolder:
+ mTrans.addAction(
+ self.tr("Combine Documents in Folder"),
+ lambda: self._mergeDocuments(tHandle, True)
+ )
if isFile:
mTrans.addAction(
@@ -1401,12 +1401,20 @@ class GuiProjectTree(QTreeWidget):
dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
dlgMerge.exec_()
- if dlgMerge.result() == QDialog.Accepted:
- # Save the open document first, in case it's part of this
+ if dlgMerge.result() != QDialog.Accepted:
+
+ mrgData = dlgMerge.getData()
+ mrgList = mrgData.get("finalItems", [])
+ if not mrgList:
+ self.mainGui.makeAlert([
+ self.tr("No documents selected for merging.")
+ ], nwAlert.INFO)
+ return False
+
+ # Save the open document first, in case it's part of merge
self.mainGui.saveDocument()
- # Set up the merge job
- mrgData = dlgMerge.getData()
+ # Create merge object, and append docs
docMerger = DocMerger(self.theProject)
mLabel = self.tr("Merged")
@@ -1419,7 +1427,7 @@ class GuiProjectTree(QTreeWidget):
else:
return False
- for sHandle in mrgData.get("finalItems", []):
+ for sHandle in mrgList:
docMerger.appendText(sHandle, True, mLabel)
if not docMerger.writeTargetDoc():
@@ -1443,6 +1451,10 @@ class GuiProjectTree(QTreeWidget):
self._alertTreeChange(mHandle, flush=True)
self.projView.wordCountsChanged.emit()
+ else:
+ logger.info("Action cancelled by user")
+ return False
+
return True
def _splitDocument(self, tHandle):
From 428dce3e6ed2649938be1b8bc48674a350f827a9 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Oct 2022 22:47:36 +0200
Subject: [PATCH 19/92] Fix doc merge dialog test
---
tests/test_dialogs/test_dlg_docmerge.py | 168 ++++++------------------
1 file changed, 43 insertions(+), 125 deletions(-)
diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py
index 4e3138f6..3291e23f 100644
--- a/tests/test_dialogs/test_dlg_docmerge.py
+++ b/tests/test_dialogs/test_dlg_docmerge.py
@@ -19,17 +19,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import pytest
-from mock import causeOSError
-from tools import getGuiItem, readFile, writeFile, buildTestProject
+from tools import buildTestProject
-from PyQt5.QtWidgets import QAction, QMessageBox
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import QMessageBox
-from novelwriter.enum import nwItemType, nwWidget
-from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
-from novelwriter.core.tree import NWTree
+from novelwriter.dialogs import GuiDocMerge
@pytest.mark.gui
@@ -39,142 +36,63 @@ 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(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
# Create a new project
buildTestProject(nwGUI, fncProj)
- # Handles for new objects
- hNovelRoot = "0000000000008"
+ hInvalid = "0000000000000"
hChapterDir = "000000000000d"
- hChapterOne = "000000000000e"
- hSceneOne = "000000000000f"
- hSceneTwo = "0000000000010"
- hSceneThree = "0000000000011"
- hSceneFour = "0000000000012"
- hMergedDoc = "0000000000023"
+ hChapterDoc = "000000000000e"
+ hSceneDoc = "000000000000f"
- # Add Project Content
- nwGUI.switchFocus(nwWidget.TREE)
- 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
-
- tChapterOne = "## Chapter One\n\n% Chapter one comment\n"
- tSceneOne = "### Scene One\n\nThere once was a man from Nantucket"
- tSceneTwo = "### Scene Two\n\nWho kept all his cash in a bucket."
- tSceneThree = "### Scene Three\n\n\tBut his daughter, named Nan, \n\tRan away with a man"
- tSceneFour = "### Scene Four\n\nAnd as for the bucket, Nantucket."
-
- contentDir = os.path.join(fncProj, "content")
- writeFile(os.path.join(contentDir, hChapterOne+".nwd"), tChapterOne)
- writeFile(os.path.join(contentDir, hSceneOne+".nwd"), tSceneOne)
- writeFile(os.path.join(contentDir, hSceneTwo+".nwd"), tSceneTwo)
- writeFile(os.path.join(contentDir, hSceneThree+".nwd"), tSceneThree)
- writeFile(os.path.join(contentDir, hSceneFour+".nwd"), tSceneFour)
-
- assert nwGUI.openProject(fncProj) is True
-
- # Open the Merge tool
- nwGUI.switchFocus(nwWidget.TREE)
- nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
-
- monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None)
- nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger)
- qtbot.waitUntil(lambda: getGuiItem("GuiDocMerge") is not None, timeout=1000)
-
- nwMerge = getGuiItem("GuiDocMerge")
- assert isinstance(nwMerge, GuiDocMerge)
+ # Check that the dialog kan handle invalid items
+ nwMerge = GuiDocMerge(nwGUI, hInvalid, [hInvalid])
+ qtbot.addWidget(nwMerge)
nwMerge.show()
- qtbot.wait(50)
-
- # Populate List
- # =============
-
- nwMerge.listBox.clear()
assert nwMerge.listBox.count() == 0
+ nwMerge.reject()
- # No item selected
- nwGUI.projView.projTree.clearSelection()
- assert nwMerge._populateList() is False
- assert nwMerge.listBox.count() == 0
+ # Load items from chapter dir
+ nwMerge = GuiDocMerge(nwGUI, hChapterDir, [hChapterDir, hChapterDoc, hSceneDoc])
+ qtbot.addWidget(nwMerge)
+ nwMerge.show()
- # Non-existing item
- with monkeypatch.context() as mp:
- mp.setattr(NWTree, "__getitem__", lambda *a: None)
- nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
- assert nwMerge._populateList() is False
- assert nwMerge.listBox.count() == 0
+ assert nwMerge.listBox.count() == 2
- # Select a non-folder
- nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem(hChapterOne).setSelected(True)
- assert nwMerge._populateList() is False
- assert nwMerge.listBox.count() == 0
+ itemOne = nwMerge.listBox.item(0)
+ itemTwo = nwMerge.listBox.item(1)
- # Select the chapter folder
- nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
- assert nwMerge._populateList() is True
- assert nwMerge.listBox.count() == 5
+ assert itemOne.data(Qt.UserRole) == hChapterDoc
+ assert itemTwo.data(Qt.UserRole) == hSceneDoc
- # Merge Documents
- # ===============
+ assert itemOne.checkState() == Qt.Checked
+ assert itemTwo.checkState() == Qt.Checked
- # First, a successful merge
- with monkeypatch.context() as mp:
- mp.setattr(GuiDocMerge, "_doClose", lambda *a: None)
- assert nwMerge._doMerge() is True
- assert nwGUI.saveProject() is True
- mergedFile = os.path.join(contentDir, hMergedDoc+".nwd")
- assert os.path.isfile(mergedFile)
- assert readFile(mergedFile) == (
- "%%%%~name: New Chapter\n"
- "%%%%~path: %s/%s\n"
- "%%%%~kind: NOVEL/DOCUMENT\n"
- "%s\n\n"
- "%s\n\n"
- "%s\n\n"
- "%s\n\n"
- "%s\n\n"
- ) % (
- hNovelRoot,
- hMergedDoc,
- tChapterOne.strip(),
- tSceneOne.strip(),
- tSceneTwo.strip(),
- tSceneThree.strip(),
- tSceneFour.strip(),
- )
+ data = nwMerge.getData()
+ assert data["sHandle"] == hChapterDir
+ assert data["origItems"] == [hChapterDir, hChapterDoc, hSceneDoc]
+ assert data["moveToTrash"] is False
+ assert data["finalItems"] == [hChapterDoc, hSceneDoc]
- # OS error
- with monkeypatch.context() as mp:
- mp.setattr("builtins.open", causeOSError)
- assert nwMerge._doMerge() is False
+ # Uncheck second item and toggle trash switch
+ itemTwo.setCheckState(Qt.Unchecked)
+ nwMerge.trashSwitch.setChecked(True)
- # Can't find the source item
- with monkeypatch.context() as mp:
- mp.setattr(NWTree, "__getitem__", lambda *a: None)
- assert nwMerge._doMerge() is False
+ data = nwMerge.getData()
+ assert data["sHandle"] == hChapterDir
+ assert data["origItems"] == [hChapterDir, hChapterDoc, hSceneDoc]
+ assert data["moveToTrash"] is True
+ assert data["finalItems"] == [hChapterDoc]
- # No source handle set
- nwMerge.sourceItem = None
- assert nwMerge._doMerge() is False
+ # Restore default values
+ nwMerge._resetList()
- # No documents to merge
- nwMerge.listBox.clear()
- assert nwMerge._doMerge() is False
+ data = nwMerge.getData()
+ assert data["sHandle"] == hChapterDir
+ assert data["origItems"] == [hChapterDir, hChapterDoc, hSceneDoc]
+ assert data["moveToTrash"] is True
+ assert data["finalItems"] == [hChapterDoc, hSceneDoc]
- # Close up
- nwMerge._doClose()
-
- # qtbot.stopForInteraction()
+ # qtbot.stop()
# END Test testDlgMerge_Main
From 747a1a212c00413e919a602f7bacb10827f38684 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 10 Oct 2022 15:48:45 +0200
Subject: [PATCH 20/92] Make some minor improvements to code and tests
---
novelwriter/dialogs/docmerge.py | 4 +-
novelwriter/gui/outline.py | 2 +-
novelwriter/gui/projtree.py | 2 +-
tests/test_dialogs/test_dlg_docmerge.py | 33 ++--
tests/test_gui/test_gui_guimain.py | 38 ++---
tests/test_gui/test_gui_mainmenu.py | 8 +-
tests/test_gui/test_gui_noveltree.py | 18 +--
tests/test_gui/test_gui_projtree.py | 206 +++++++++++-------------
tests/test_gui/test_gui_statusbar.py | 4 +-
tests/test_tools/test_tools_lipsum.py | 4 +-
tests/tools.py | 16 ++
11 files changed, 162 insertions(+), 173 deletions(-)
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index 89149ab8..7113fc0f 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -29,8 +29,8 @@ import novelwriter
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (
- QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
- QListWidgetItem, QDialogButtonBox, QGridLayout
+ QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel,
+ QListWidget, QListWidgetItem, QVBoxLayout,
)
from novelwriter.gui.custom import QHelpLabel, QSwitch
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index e8514c15..b5e67b2e 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -1048,7 +1048,7 @@ class GuiOutlineDetails(QScrollArea):
self.titleLabel.setText("%s" % self.tr("Title"))
self.titleValue.setText(novIdx.title)
- itemStatus, _ = nwItem.getImportStatus()
+ itemStatus, _ = nwItem.getImportStatus(incIcon=False)
self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(itemStatus)
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index ac257b93..15a2417c 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -34,7 +34,7 @@ from time import time
from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
- QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel, QDialog,
+ QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget
)
diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py
index 3291e23f..70acf961 100644
--- a/tests/test_dialogs/test_dlg_docmerge.py
+++ b/tests/test_dialogs/test_dlg_docmerge.py
@@ -21,7 +21,7 @@ along with this program. If not, see .
import pytest
-from tools import buildTestProject
+from tools import buildTestProject, C
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
@@ -40,20 +40,15 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
# Create a new project
buildTestProject(nwGUI, fncProj)
- hInvalid = "0000000000000"
- hChapterDir = "000000000000d"
- hChapterDoc = "000000000000e"
- hSceneDoc = "000000000000f"
-
# Check that the dialog kan handle invalid items
- nwMerge = GuiDocMerge(nwGUI, hInvalid, [hInvalid])
+ nwMerge = GuiDocMerge(nwGUI, C.hInvalid, [C.hInvalid])
qtbot.addWidget(nwMerge)
nwMerge.show()
assert nwMerge.listBox.count() == 0
nwMerge.reject()
# Load items from chapter dir
- nwMerge = GuiDocMerge(nwGUI, hChapterDir, [hChapterDir, hChapterDoc, hSceneDoc])
+ nwMerge = GuiDocMerge(nwGUI, C.hChapterDir, [C.hChapterDir, C.hChapterDoc, C.hSceneDoc])
qtbot.addWidget(nwMerge)
nwMerge.show()
@@ -62,36 +57,36 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
itemOne = nwMerge.listBox.item(0)
itemTwo = nwMerge.listBox.item(1)
- assert itemOne.data(Qt.UserRole) == hChapterDoc
- assert itemTwo.data(Qt.UserRole) == hSceneDoc
+ assert itemOne.data(Qt.UserRole) == C.hChapterDoc
+ assert itemTwo.data(Qt.UserRole) == C.hSceneDoc
assert itemOne.checkState() == Qt.Checked
assert itemTwo.checkState() == Qt.Checked
data = nwMerge.getData()
- assert data["sHandle"] == hChapterDir
- assert data["origItems"] == [hChapterDir, hChapterDoc, hSceneDoc]
+ assert data["sHandle"] == C.hChapterDir
+ assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc]
assert data["moveToTrash"] is False
- assert data["finalItems"] == [hChapterDoc, hSceneDoc]
+ assert data["finalItems"] == [C.hChapterDoc, C.hSceneDoc]
# Uncheck second item and toggle trash switch
itemTwo.setCheckState(Qt.Unchecked)
nwMerge.trashSwitch.setChecked(True)
data = nwMerge.getData()
- assert data["sHandle"] == hChapterDir
- assert data["origItems"] == [hChapterDir, hChapterDoc, hSceneDoc]
+ assert data["sHandle"] == C.hChapterDir
+ assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc]
assert data["moveToTrash"] is True
- assert data["finalItems"] == [hChapterDoc]
+ assert data["finalItems"] == [C.hChapterDoc]
# Restore default values
nwMerge._resetList()
data = nwMerge.getData()
- assert data["sHandle"] == hChapterDir
- assert data["origItems"] == [hChapterDir, hChapterDoc, hSceneDoc]
+ assert data["sHandle"] == C.hChapterDir
+ assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc]
assert data["moveToTrash"] is True
- assert data["finalItems"] == [hChapterDoc, hSceneDoc]
+ assert data["finalItems"] == [C.hChapterDoc, C.hSceneDoc]
# qtbot.stop()
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 5c75c406..76651260 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, writeFile
+from tools import C, cmpFiles, buildTestProject, XML_IGNORE, writeFile
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox, QInputDialog
@@ -216,14 +216,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.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
+ assert nwGUI.projView.projTree._getTreeItem(C.hNovelRoot) is not None
+ assert nwGUI.projView.projTree._getTreeItem(C.hPlotRoot) is not None
+ assert nwGUI.projView.projTree._getTreeItem(C.hCharRoot) is not None
+ assert nwGUI.projView.projTree._getTreeItem(C.hWorldRoot) is not None
+ assert nwGUI.projView.projTree._getTreeItem(C.hTitlePage) is not None
+ assert nwGUI.projView.projTree._getTreeItem(C.hChapterDir) is not None
+ assert nwGUI.projView.projTree._getTreeItem(C.hChapterDoc) is not None
+ assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None
nwGUI.mainMenu.aSpellCheck.setChecked(True)
assert nwGUI.mainMenu._toggleSpellCheck()
@@ -237,7 +237,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Add a Character File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True)
+ nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
@@ -259,7 +259,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Add a Plot File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem("0000000000009").setSelected(True)
+ nwGUI.projView.projTree._getTreeItem(C.hPlotRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
@@ -281,7 +281,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Add a World File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem("000000000000b").setSelected(True)
+ nwGUI.projView.projTree._getTreeItem(C.hWorldRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
@@ -312,9 +312,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Select the 'New Scene' file
nwGUI.switchFocus(nwWidget.TREE)
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)
+ nwGUI.projView.projTree._getTreeItem(C.hNovelRoot).setExpanded(True)
+ nwGUI.projView.projTree._getTreeItem(C.hChapterDir).setExpanded(True)
+ nwGUI.projView.projTree._getTreeItem(C.hSceneDoc).setSelected(True)
assert nwGUI.openSelectedItem()
# Type something into the document
@@ -492,8 +492,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Open and view the edited document
nwGUI.switchFocus(nwWidget.VIEWER)
- assert nwGUI.openDocument("000000000000f")
- assert nwGUI.viewDocument("000000000000f")
+ assert nwGUI.openDocument(C.hSceneDoc)
+ assert nwGUI.viewDocument(C.hSceneDoc)
qtbot.wait(stepDelay)
assert nwGUI.saveProject()
assert nwGUI.closeDocViewer()
@@ -561,8 +561,8 @@ def testGuiMain_FocusFullMode(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
assert nwGUI.toggleFocusMode() is False
# Open a file in editor and viewer
- assert nwGUI.openDocument("000000000000f")
- assert nwGUI.viewDocument("000000000000f")
+ assert nwGUI.openDocument(C.hSceneDoc)
+ assert nwGUI.viewDocument(C.hSceneDoc)
# Enable focus mode
assert nwGUI.toggleFocusMode() is True
diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py
index f579a1a0..0097b65b 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, buildTestProject
+from tools import C, writeFile, buildTestProject
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.enum import nwDocAction, nwDocInsert
@@ -467,8 +467,8 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd):
buildTestProject(nwGUI, fncProj)
- assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None
- assert nwGUI.openDocument("000000000000f") is True
+ assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None
+ assert nwGUI.openDocument(C.hSceneDoc) is True
nwGUI.docEditor.clear()
# Test Faulty Inserts
@@ -677,7 +677,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd):
assert not nwGUI.importDocument()
# Open the document from before, and add some text to it
- nwGUI.openDocument("000000000000f")
+ nwGUI.openDocument(C.hSceneDoc)
nwGUI.docEditor.setText("Bar")
assert nwGUI.docEditor.getText() == "Bar"
diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py
index f8f18875..fcbacdb2 100644
--- a/tests/test_gui/test_gui_noveltree.py
+++ b/tests/test_gui/test_gui_noveltree.py
@@ -22,7 +22,7 @@ along with this program. If not, see .
import os
import pytest
-from tools import buildTestProject, writeFile
+from tools import C, buildTestProject, writeFile
from PyQt5.QtGui import QFocusEvent
from PyQt5.QtCore import Qt, QEvent
@@ -46,7 +46,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True)
+ nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
writeFile(
@@ -94,7 +94,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
assert not topItem.isSelected()
topItem.setSelected(True)
assert novelTree.selectedItems()[0] == topItem
- assert novelView.getSelectedHandle() == ("000000000000c", 0)
+ assert novelView.getSelectedHandle() == (C.hTitlePage, 0)
# Refresh using the slot for the butoom
novelBar._refreshNovelTree()
@@ -119,7 +119,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
assert scItem.isSelected()
assert nwGUI.docEditor.docHandle() is None
novelTree._treeDoubleClick(scItem, 0)
- assert nwGUI.docEditor.docHandle() == "000000000000f"
+ assert nwGUI.docEditor.docHandle() == C.hSceneDoc
# Open item with middle mouse button
scItem.setSelected(True)
@@ -136,7 +136,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData)
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
- assert nwGUI.docViewer.docHandle() == "000000000000f"
+ assert nwGUI.docViewer.docHandle() == C.hSceneDoc
# Last Column
# ===========
@@ -144,26 +144,26 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
novelBar.setLastColType(NovelTreeColumn.HIDDEN)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
assert novelTree.lastColType == NovelTreeColumn.HIDDEN
- assert novelTree._getLastColumnText("000000000000f", "T000001") == ("", "")
+ assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ("", "")
novelBar.setLastColType(NovelTreeColumn.POV)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.POV
- assert novelTree._getLastColumnText("000000000000f", "T000001") == (
+ assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == (
"Jane", "Point of View: Jane"
)
novelBar.setLastColType(NovelTreeColumn.FOCUS)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.FOCUS
- assert novelTree._getLastColumnText("000000000000f", "T000001") == (
+ assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == (
"Jane", "Focus: Jane"
)
novelBar.setLastColType(NovelTreeColumn.PLOT)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.PLOT
- assert novelTree._getLastColumnText("000000000000f", "T000001") == (
+ assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == (
"", "Plot: "
)
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index e46418b9..f4bfa1c8 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -22,7 +22,7 @@ along with this program. If not, see .
import os
import pytest
-from tools import buildTestProject
+from tools import C, buildTestProject
from PyQt5.QtWidgets import QMessageBox, QMenu
@@ -78,24 +78,24 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd)
assert "Did not find anywhere" in caplog.text
# Create new folder as child of Novel folder
- projView.setSelectedHandle("0000000000008")
+ projView.setSelectedHandle(C.hNovelRoot)
assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True
- assert theProject.tree["0000000000011"].itemParent == "0000000000008"
- assert theProject.tree["0000000000011"].itemRoot == "0000000000008"
+ assert theProject.tree["0000000000011"].itemParent == C.hNovelRoot
+ assert theProject.tree["0000000000011"].itemRoot == C.hNovelRoot
assert theProject.tree["0000000000011"].itemClass == nwItemClass.NOVEL
# Add a new file in the new folder
projView.setSelectedHandle("0000000000011")
assert projView.projTree.newTreeItem(nwItemType.FILE) is True
assert theProject.tree["0000000000012"].itemParent == "0000000000011"
- assert theProject.tree["0000000000012"].itemRoot == "0000000000008"
+ assert theProject.tree["0000000000012"].itemRoot == C.hNovelRoot
assert theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL
# Add a new chapter next to the other new file
projView.setSelectedHandle("0000000000012")
assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True
assert theProject.tree["0000000000013"].itemParent == "0000000000011"
- assert theProject.tree["0000000000013"].itemRoot == "0000000000008"
+ assert theProject.tree["0000000000013"].itemRoot == C.hNovelRoot
assert theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument("0000000000013")
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
@@ -104,16 +104,16 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd)
projView.setSelectedHandle("0000000000012")
assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True
assert theProject.tree["0000000000014"].itemParent == "0000000000011"
- assert theProject.tree["0000000000014"].itemRoot == "0000000000008"
+ assert theProject.tree["0000000000014"].itemRoot == C.hNovelRoot
assert 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
- projView.setSelectedHandle("000000000000a")
+ projView.setSelectedHandle(C.hCharRoot)
assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True
- assert theProject.tree["0000000000015"].itemParent == "000000000000a"
- assert theProject.tree["0000000000015"].itemRoot == "000000000000a"
+ assert theProject.tree["0000000000015"].itemParent == C.hCharRoot
+ assert theProject.tree["0000000000015"].itemRoot == C.hCharRoot
assert theProject.tree["0000000000015"].itemClass == nwItemClass.CHARACTER
assert nwGUI.openDocument("0000000000015")
assert nwGUI.docEditor.getText() == "# New Note\n\n"
@@ -145,8 +145,8 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd)
# Rename plot folder
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("Stuff", True))
- projTree.renameTreeItem("0000000000009") is True
- assert theProject.tree["0000000000009"].itemName == "Stuff"
+ projTree.renameTreeItem(C.hPlotRoot) is True
+ assert theProject.tree[C.hPlotRoot].itemName == "Stuff"
# Rename invalid folder
projTree.renameTreeItem("0000000000000") is False
@@ -192,12 +192,12 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
# ==============
# Add some files
- nwTree.setSelectedHandle("000000000000d")
+ nwTree.setSelectedHandle(C.hChapterDir)
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",
+ assert nwTree.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000011", "0000000000012",
]
@@ -206,75 +206,75 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
assert nwTree.projTree.moveTreeItem(1) is False
# Move second item up twice (should give same result)
- nwTree.setSelectedHandle("000000000000f")
+ nwTree.setSelectedHandle(C.hSceneDoc)
assert nwTree.projTree.moveTreeItem(-1) is True
- assert nwTree.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000f", "000000000000e",
+ assert nwTree.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hSceneDoc, C.hChapterDoc,
"0000000000010", "0000000000011", "0000000000012",
]
assert nwTree.projTree.moveTreeItem(-1) is False
- assert nwTree.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000f", "000000000000e",
+ assert nwTree.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hSceneDoc, C.hChapterDoc,
"0000000000010", "0000000000011", "0000000000012",
]
# Restore
assert nwTree.projTree.moveTreeItem(1) is True
- assert nwTree.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000e", "000000000000f",
+ assert nwTree.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000011", "0000000000012",
]
# Move fifth item down twice (should give same result)
nwTree.setSelectedHandle("0000000000011")
assert nwTree.projTree.moveTreeItem(1) is True
- assert nwTree.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000e", "000000000000f",
+ assert nwTree.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000012", "0000000000011",
]
assert nwTree.projTree.moveTreeItem(1) is False
- assert nwTree.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000e", "000000000000f",
+ assert nwTree.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000012", "0000000000011",
]
# Restore
assert nwTree.projTree.moveTreeItem(-1) is True
- assert nwTree.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000e", "000000000000f",
+ assert nwTree.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000011", "0000000000012",
]
# Move down again, and restore via undo
nwTree.setSelectedHandle("0000000000011")
assert nwTree.projTree.moveTreeItem(1) is True
- assert nwTree.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000e", "000000000000f",
+ assert nwTree.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000012", "0000000000011",
]
assert nwTree.projTree.undoLastMove() is True
- assert nwTree.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000e", "000000000000f",
+ assert nwTree.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000011", "0000000000012",
]
# Root Folder
# ===========
- nwTree.setSelectedHandle("0000000000008")
- assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
+ nwTree.setSelectedHandle(C.hNovelRoot)
+ assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 0
# Move novel folder up
assert nwTree.projTree.moveTreeItem(-1) is False
- assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
+ assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 0
# Move novel folder down
assert nwTree.projTree.moveTreeItem(1) is True
- assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1
+ assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 1
# Move novel folder up again
assert nwTree.projTree.moveTreeItem(-1) is True
- assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
+ assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 0
# Clean up
# qtbot.stopForInteraction()
@@ -307,12 +307,12 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir,
assert nwView.emptyTrash() is False
# Add some files
- nwView.setSelectedHandle("000000000000d")
+ nwView.setSelectedHandle(C.hChapterDir)
assert nwView.projTree.newTreeItem(nwItemType.FILE) is True
assert nwView.projTree.newTreeItem(nwItemType.FILE) is True
assert nwView.projTree.newTreeItem(nwItemType.FILE) is True
- assert nwView.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000e", "000000000000f",
+ assert nwView.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000011", "0000000000012",
]
@@ -337,8 +337,8 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir,
# Delete Root Folders
# ===================
- assert nwView.requestDeleteItem("0000000000008") is False # Novel Root is blocked
- assert nwView.requestDeleteItem("000000000000a") is True # Character Root
+ assert nwView.requestDeleteItem(C.hNovelRoot) is False # Novel Root is blocked
+ assert nwView.requestDeleteItem(C.hCharRoot) is True # Character Root
# Delete File
# ===========
@@ -352,8 +352,8 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir,
# Delete last two documents, which also adds the trash folder
assert nwView.requestDeleteItem("0000000000012") is True
assert nwView.requestDeleteItem("0000000000011") is True
- assert nwView.getTreeFromHandle("000000000000d") == [
- "000000000000d", "000000000000e", "000000000000f",
+ assert nwView.getTreeFromHandle(C.hChapterDir) == [
+ C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010"
]
trashHandle = nwGUI.theProject.tree.trashRoot()
@@ -389,18 +389,14 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, m
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
- hInvalid = "0000000000000"
- hNovelRoot = "0000000000008"
- hTitlePage = "000000000000c"
-
# Invalid item
caplog.clear()
- assert projTree.moveItemToTrash(hInvalid) is False
+ assert projTree.moveItemToTrash(C.hInvalid) is False
assert "Could not find tree item for deletion" in caplog.text
# Root folders cannot be moved to Trash
caplog.clear()
- assert projTree.moveItemToTrash(hNovelRoot) is False
+ assert projTree.moveItemToTrash(C.hNovelRoot) is False
assert "Root folders cannot be moved to Trash" in caplog.text
# Block adding trash folder
@@ -408,8 +404,8 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, m
projTree._addTrashRoot = lambda *a: None
caplog.clear()
- assert projTree.moveItemToTrash(hTitlePage) is False
- assert theProject.tree.isTrash(hTitlePage) is False
+ assert projTree.moveItemToTrash(C.hTitlePage) is False
+ assert theProject.tree.isTrash(C.hTitlePage) is False
assert "Could not delete item" in caplog.text
projTree._addTrashRoot = funcPointer
@@ -417,16 +413,16 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, m
# User cancels action
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
- assert projTree.moveItemToTrash(hTitlePage) is False
- assert theProject.tree.isTrash(hTitlePage) is False
+ assert projTree.moveItemToTrash(C.hTitlePage) is False
+ assert theProject.tree.isTrash(C.hTitlePage) is False
# Move a document to Trash
- assert projTree.moveItemToTrash(hTitlePage) is True
- assert theProject.tree.isTrash(hTitlePage) is True
+ assert projTree.moveItemToTrash(C.hTitlePage) is True
+ assert theProject.tree.isTrash(C.hTitlePage) is True
# Cannot be moved again
caplog.clear()
- assert projTree.moveItemToTrash(hTitlePage) is False
+ assert projTree.moveItemToTrash(C.hTitlePage) is False
assert "Item is already in the Trash folder" in caplog.text
nwGUI.closeProject()
@@ -452,48 +448,40 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fnc
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
- hInvalid = "0000000000000"
- hNovelRoot = "0000000000008"
- hPlotRoot = "0000000000009"
- hTitlePage = "000000000000c"
- hChapterDir = "000000000000d"
- hChapterDoc = "000000000000e"
- hSceneDoc = "000000000000f"
-
# Invalid item
caplog.clear()
- assert projTree.permanentlyDeleteItem(hInvalid) is False
+ assert projTree.permanentlyDeleteItem(C.hInvalid) is False
assert "Could not find tree item for deletion" in caplog.text
# Not deleting root item in use
caplog.clear()
- assert projTree.permanentlyDeleteItem(hNovelRoot) is False
+ assert projTree.permanentlyDeleteItem(C.hNovelRoot) is False
assert "Root folders can only be deleted when they are empty" in caplog.text
- assert hNovelRoot in theProject.tree
+ assert C.hNovelRoot in theProject.tree
# Deleting unused root item is allowed
caplog.clear()
- assert projTree.permanentlyDeleteItem(hPlotRoot) is True
- assert hPlotRoot not in theProject.tree
+ assert projTree.permanentlyDeleteItem(C.hPlotRoot) is True
+ assert C.hPlotRoot not in theProject.tree
# User cancels action
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
- assert projTree.permanentlyDeleteItem(hTitlePage) is False
- assert hTitlePage in theProject.tree
+ assert projTree.permanentlyDeleteItem(C.hTitlePage) is False
+ assert C.hTitlePage in theProject.tree
# Deleting file is OK, and if it is open, it should close
- assert nwGUI.openDocument(hTitlePage) is True
- assert nwGUI.docEditor.docHandle() == hTitlePage
- assert projTree.permanentlyDeleteItem(hTitlePage) is True
- assert hTitlePage not in theProject.tree
+ assert nwGUI.openDocument(C.hTitlePage) is True
+ assert nwGUI.docEditor.docHandle() == C.hTitlePage
+ assert projTree.permanentlyDeleteItem(C.hTitlePage) is True
+ assert C.hTitlePage not in theProject.tree
assert nwGUI.docEditor.docHandle() is None
# Deleting folder + files recursiely is ok
- assert projTree.permanentlyDeleteItem(hChapterDir) is True
- assert hChapterDir not in theProject.tree
- assert hChapterDoc not in theProject.tree
- assert hSceneDoc not in theProject.tree
+ assert projTree.permanentlyDeleteItem(C.hChapterDir) is True
+ assert C.hChapterDir not in theProject.tree
+ assert C.hChapterDoc not in theProject.tree
+ assert C.hSceneDoc not in theProject.tree
nwGUI.closeProject()
@@ -523,38 +511,33 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRn
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
- hTitlePage = "000000000000c"
- hChapterDir = "000000000000d"
- hChapterDoc = "000000000000e"
- hSceneDoc = "000000000000f"
-
# No Trash folder
assert projTree.emptyTrash() is False
# Move some documents to Trash
- assert projTree.moveItemToTrash(hTitlePage) is True
- assert projTree.moveItemToTrash(hChapterDir) is True
+ assert projTree.moveItemToTrash(C.hTitlePage) is True
+ assert projTree.moveItemToTrash(C.hChapterDir) is True
- assert theProject.tree.isTrash(hTitlePage) is True
- assert theProject.tree.isTrash(hChapterDir) is True
- assert theProject.tree.isTrash(hChapterDoc) is True
- assert theProject.tree.isTrash(hSceneDoc) is True
+ assert theProject.tree.isTrash(C.hTitlePage) is True
+ assert theProject.tree.isTrash(C.hChapterDir) is True
+ assert theProject.tree.isTrash(C.hChapterDoc) is True
+ assert theProject.tree.isTrash(C.hSceneDoc) is True
# User cancels
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert projTree.emptyTrash() is False
- assert hTitlePage in theProject.tree
- assert hChapterDir in theProject.tree
- assert hChapterDoc in theProject.tree
- assert hSceneDoc in theProject.tree
+ assert C.hTitlePage in theProject.tree
+ assert C.hChapterDir in theProject.tree
+ assert C.hChapterDoc in theProject.tree
+ assert C.hSceneDoc in theProject.tree
# Run again to empty all items
assert projTree.emptyTrash() is True
- assert hTitlePage not in theProject.tree
- assert hChapterDir not in theProject.tree
- assert hChapterDoc not in theProject.tree
- assert hSceneDoc not in theProject.tree
+ assert C.hTitlePage not in theProject.tree
+ assert C.hChapterDir not in theProject.tree
+ assert C.hChapterDoc not in theProject.tree
+ assert C.hSceneDoc not in theProject.tree
# Running Emtpy Trash again is cancelled due to empty folder
assert projTree.emptyTrash() is False
@@ -584,13 +567,8 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
buildTestProject(nwGUI, prjDir)
# Handles for new objects
- hNovelRoot = "0000000000008"
- hTitlePage = "000000000000c"
- hChapterDir = "000000000000d"
- hChapterFile = "000000000000e"
- hCharRoot = "000000000000a"
- hCharNote = "0000000000011"
- hNovelNote = "0000000000012"
+ hCharNote = "0000000000011"
+ hNovelNote = "0000000000012"
projTree = nwGUI.projView.projTree
projTree.setExpandedFromHandle(None, True)
@@ -598,9 +576,9 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
projTree._addTrashRoot()
hTrashRoot = projTree.theProject.tree.trashRoot()
- projTree.setSelectedHandle(hCharRoot)
+ projTree.setSelectedHandle(C.hCharRoot)
projTree.newTreeItem(nwItemType.FILE)
- projTree.setSelectedHandle(hNovelRoot)
+ projTree.setSelectedHandle(C.hNovelRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
def itemPos(tHandle):
@@ -611,16 +589,16 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
# Generate the possible menu combinarions
assert projTree._openContextMenu(itemPos(hTrashRoot)) is True
- assert projTree._openContextMenu(itemPos(hNovelRoot)) is True
+ assert projTree._openContextMenu(itemPos(C.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(C.hTitlePage)) is True
+ assert projTree._openContextMenu(itemPos(C.hChapterDir)) is True
+ assert projTree._openContextMenu(itemPos(C.hChapterDoc)) is True
+ assert projTree._openContextMenu(itemPos(C.hCharRoot)) is True
assert projTree._openContextMenu(itemPos(hCharNote)) is True
# Check the keyboard shortcut handler as well
- projTree.setSelectedHandle(hNovelRoot)
+ projTree.setSelectedHandle(C.hNovelRoot)
assert projTree.openContextOnSelected() is True
projTree.clearSelection()
assert projTree.openContextOnSelected() is False
diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py
index 0c6b1e81..18f969f6 100644
--- a/tests/test_gui/test_gui_statusbar.py
+++ b/tests/test_gui/test_gui_statusbar.py
@@ -22,7 +22,7 @@ along with this program. If not, see .
import time
import pytest
-from tools import buildTestProject
+from tools import C, buildTestProject
from PyQt5.QtWidgets import QMessageBox
@@ -37,7 +37,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
buildTestProject(nwGUI, fncProj)
- cHandle = nwGUI.theProject.newFile("A Note", "000000000000a")
+ cHandle = nwGUI.theProject.newFile("A Note", C.hCharRoot)
newDoc = NWDoc(nwGUI.theProject, cHandle)
newDoc.writeDocument("# A Note\n\n")
nwGUI.projView.revealNewTreeItem(cHandle)
diff --git a/tests/test_tools/test_tools_lipsum.py b/tests/test_tools/test_tools_lipsum.py
index 629deb56..de70a428 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, buildTestProject
+from tools import C, getGuiItem, buildTestProject
from PyQt5.QtWidgets import QAction, QMessageBox
@@ -41,7 +41,7 @@ def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
# Create a new project
buildTestProject(nwGUI, fncProj)
- assert nwGUI.openDocument("000000000000f") is True
+ assert nwGUI.openDocument(C.hSceneDoc) is True
assert len(nwGUI.docEditor.getText()) == 15
# Open the tool
diff --git a/tests/tools.py b/tests/tools.py
index fcdd3817..5c126707 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -28,6 +28,22 @@ from PyQt5.QtWidgets import qApp
XML_IGNORE = ("
Date: Mon, 10 Oct 2022 16:35:13 +0200
Subject: [PATCH 21/92] Add test coverage of doc tools
---
novelwriter/core/doctools.py | 2 +-
novelwriter/core/project.py | 4 +-
tests/conftest.py | 18 ++-
.../coreDocTools_DocMerger_0000000000010.nwd | 33 +++++
.../coreDocTools_DocMerger_0000000000014.nwd | 35 +++++
tests/test_core/test_core_doctools.py | 120 ++++++++++++++++++
tests/test_core/test_core_project.py | 2 +-
7 files changed, 204 insertions(+), 10 deletions(-)
create mode 100644 tests/reference/coreDocTools_DocMerger_0000000000010.nwd
create mode 100644 tests/reference/coreDocTools_DocMerger_0000000000014.nwd
create mode 100644 tests/test_core/test_core_doctools.py
diff --git a/novelwriter/core/doctools.py b/novelwriter/core/doctools.py
index 75a4c3b7..f6716590 100644
--- a/novelwriter/core/doctools.py
+++ b/novelwriter/core/doctools.py
@@ -110,7 +110,7 @@ class DocMerger:
if docText:
self._targetText.insert(0, docText)
- status = outDoc.writeDocument("\n\n".join(self._targetText))
+ status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
if not status:
self._error = outDoc.getError()
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index a6f88a7e..475300fd 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -176,7 +176,7 @@ class NWProject():
self._projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
- def writeNewFile(self, tHandle, hLevel, isDocument):
+ def writeNewFile(self, tHandle, hLevel, isDocument, addText=""):
"""Write content to a new document after it is created. This
will not run if the file exists and is not empty.
"""
@@ -191,7 +191,7 @@ class NWProject():
return False
hshText = "#"*minmax(hLevel, 1, 4)
- newText = f"{hshText} {tItem.itemName}\n\n"
+ newText = f"{hshText} {tItem.itemName}\n\n{addText}"
if tItem.isNovelLike() and isDocument:
tItem.setLayout(nwItemLayout.DOCUMENT)
else:
diff --git a/tests/conftest.py b/tests/conftest.py
index 769f855c..40725923 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -190,14 +190,20 @@ def mockRnd(monkeypatch):
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
+ class MockRnd:
- gen = rnd(1000)
- monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
+ def __init__(self):
+ self.reset()
- return
+ def _rnd(self, n):
+ for x in range(n):
+ yield x
+
+ def reset(self):
+ gen = self._rnd(1000)
+ monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
+
+ return MockRnd()
##
diff --git a/tests/reference/coreDocTools_DocMerger_0000000000010.nwd b/tests/reference/coreDocTools_DocMerger_0000000000010.nwd
new file mode 100644
index 00000000..6dac570d
--- /dev/null
+++ b/tests/reference/coreDocTools_DocMerger_0000000000010.nwd
@@ -0,0 +1,33 @@
+%%~name: Chapter 1
+%%~path: 0000000000008/0000000000010
+%%~kind: NOVEL/DOCUMENT
+## Chapter 1
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.
+
+Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
+
+% Merge Novel Document: Scene 1.1 [New]
+
+### Scene 1.1
+
+Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
+
+Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
+
+% Merge Novel Document: Scene 1.2 [New]
+
+### Scene 1.2
+
+Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
+
+Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
+
+% Merge Novel Document: Scene 1.3 [New]
+
+### Scene 1.3
+
+Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
+
+Integer ac gravida quam. Quisque eleifend nisl nec pretium tincidunt. Quisque sollicitudin nisi in hendrerit scelerisque. Sed ornare nisl lacus, sit amet consectetur lectus egestas et. Vivamus nec arcu lorem. Donec rhoncus, purus a porta accumsan, nunc lectus iaculis libero, et fringilla tellus augue et velit. Integer varius felis scelerisque, vulputate tellus eu, laoreet justo. Suspendisse sit amet sem vehicula, auctor odio sed, aliquet enim. In ac tortor sed tortor fringilla elementum. Nulla non odio at magna vulputate scelerisque. Nam elementum diam eu rutrum scelerisque. Sed fermentum, felis quis vulputate fermentum, libero metus sollicitudin est, in faucibus purus nulla non dolor. Ut vitae felis porta, feugiat nunc et, bibendum neque. Nullam nec lorem nec metus ullamcorper malesuada ut a nisl. Etiam eget tristique dui. Nulla sed mi finibus, venenatis tellus non, maximus enim.
+
diff --git a/tests/reference/coreDocTools_DocMerger_0000000000014.nwd b/tests/reference/coreDocTools_DocMerger_0000000000014.nwd
new file mode 100644
index 00000000..6a0d3548
--- /dev/null
+++ b/tests/reference/coreDocTools_DocMerger_0000000000014.nwd
@@ -0,0 +1,35 @@
+%%~name: All of Chapter 1
+%%~path: 0000000000008/0000000000014
+%%~kind: NOVEL/DOCUMENT
+% Merge Novel Document: Chapter 1 [New]
+
+## Chapter 1
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.
+
+Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
+
+% Merge Novel Document: Scene 1.1 [New]
+
+### Scene 1.1
+
+Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
+
+Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
+
+% Merge Novel Document: Scene 1.2 [New]
+
+### Scene 1.2
+
+Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
+
+Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
+
+% Merge Novel Document: Scene 1.3 [New]
+
+### Scene 1.3
+
+Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
+
+Integer ac gravida quam. Quisque eleifend nisl nec pretium tincidunt. Quisque sollicitudin nisi in hendrerit scelerisque. Sed ornare nisl lacus, sit amet consectetur lectus egestas et. Vivamus nec arcu lorem. Donec rhoncus, purus a porta accumsan, nunc lectus iaculis libero, et fringilla tellus augue et velit. Integer varius felis scelerisque, vulputate tellus eu, laoreet justo. Suspendisse sit amet sem vehicula, auctor odio sed, aliquet enim. In ac tortor sed tortor fringilla elementum. Nulla non odio at magna vulputate scelerisque. Nam elementum diam eu rutrum scelerisque. Sed fermentum, felis quis vulputate fermentum, libero metus sollicitudin est, in faucibus purus nulla non dolor. Ut vitae felis porta, feugiat nunc et, bibendum neque. Nullam nec lorem nec metus ullamcorper malesuada ut a nisl. Etiam eget tristique dui. Nulla sed mi finibus, venenatis tellus non, maximus enim.
+
diff --git a/tests/test_core/test_core_doctools.py b/tests/test_core/test_core_doctools.py
new file mode 100644
index 00000000..0d93fcb9
--- /dev/null
+++ b/tests/test_core/test_core_doctools.py
@@ -0,0 +1,120 @@
+"""
+novelWriter – Project Document Tools 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 os
+import pytest
+
+from shutil import copyfile
+
+from mock import causeOSError
+from tools import C, buildTestProject, cmpFiles
+
+from novelwriter.core.project import NWProject
+from novelwriter.core.doctools import DocMerger
+
+
+@pytest.mark.core
+def testCoreDocTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRnd, ipsumText):
+ """Test the DocMerger utility.
+ """
+ theProject = NWProject(mockGUI)
+ mockRnd.reset()
+ buildTestProject(theProject, fncDir)
+
+ # Create File to Merge
+ # ====================
+
+ hChapter1 = theProject.newFile("Chapter 1", C.hNovelRoot)
+ hSceneOne11 = theProject.newFile("Scene 1.1", hChapter1)
+ hSceneOne12 = theProject.newFile("Scene 1.2", hChapter1)
+ hSceneOne13 = theProject.newFile("Scene 1.3", hChapter1)
+
+ docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
+ docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
+ docText3 = "\n\n".join(ipsumText[2:4]) + "\n\n"
+ docText4 = "\n\n".join(ipsumText[3:5]) + "\n\n"
+
+ theProject.writeNewFile(hChapter1, 2, True, docText1)
+ theProject.writeNewFile(hSceneOne11, 3, True, docText2)
+ theProject.writeNewFile(hSceneOne12, 3, True, docText3)
+ theProject.writeNewFile(hSceneOne13, 3, True, docText4)
+
+ # Basic Checks
+ # ============
+
+ docMerger = DocMerger(theProject)
+
+ # No writing without a target set
+ assert docMerger.writeTargetDoc() is False
+
+ # Cannot append invalid handle
+ assert docMerger.appendText(C.hInvalid, True, "Merge") is False
+
+ # Cannot create new target from invalid handle
+ assert docMerger.newTargetDoc(C.hInvalid, "Test") is None
+
+ # Merge to New
+ # ============
+
+ saveFile = os.path.join(fncDir, "content", "0000000000014.nwd")
+ testFile = os.path.join(outDir, "coreDocTools_DocMerger_0000000000014.nwd")
+ compFile = os.path.join(refDir, "coreDocTools_DocMerger_0000000000014.nwd")
+
+ assert docMerger.newTargetDoc(hChapter1, "All of Chapter 1") == "0000000000014"
+
+ assert docMerger.appendText(hChapter1, True, "Merge") is True
+ assert docMerger.appendText(hSceneOne11, True, "Merge") is True
+ assert docMerger.appendText(hSceneOne12, True, "Merge") is True
+ assert docMerger.appendText(hSceneOne13, True, "Merge") is True
+
+ # Block writing and check error handling
+ with monkeypatch.context() as mp:
+ mp.setattr("builtins.open", causeOSError)
+ assert docMerger.writeTargetDoc() is False
+ assert not os.path.isfile(saveFile)
+ assert docMerger.getError() != ""
+
+ # Write properly, and compare
+ assert docMerger.writeTargetDoc() is True
+ copyfile(saveFile, testFile)
+ assert cmpFiles(testFile, compFile)
+
+ # Merge into Existing
+ # ===================
+
+ saveFile = os.path.join(fncDir, "content", "0000000000010.nwd")
+ testFile = os.path.join(outDir, "coreDocTools_DocMerger_0000000000010.nwd")
+ compFile = os.path.join(refDir, "coreDocTools_DocMerger_0000000000010.nwd")
+
+ docMerger.setTargetDoc(hChapter1)
+
+ assert docMerger.appendText(hSceneOne11, True, "Merge") is True
+ assert docMerger.appendText(hSceneOne12, True, "Merge") is True
+ assert docMerger.appendText(hSceneOne13, True, "Merge") is True
+
+ assert docMerger.writeTargetDoc() is True
+ copyfile(saveFile, testFile)
+ assert cmpFiles(testFile, compFile)
+
+ # Just for debugging
+ docMerger.writeTargetDoc()
+
+# END Test testCoreDocTools_DocMerger
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 3be812e1..5ea25676 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -22,9 +22,9 @@ along with this program. If not, see .
import os
import pytest
+from lxml import etree
from shutil import copyfile
from zipfile import ZipFile
-from lxml import etree
from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE
from mock import causeOSError
From 07c93bcb7d8afb1178b271c573a7f00aab3948bb Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Oct 2022 21:20:03 +0200
Subject: [PATCH 22/92] Update test coverage
---
novelwriter/gui/projtree.py | 12 +-
tests/test_gui/test_gui_projtree.py | 243 ++++++++++++++++++++++++++--
2 files changed, 232 insertions(+), 23 deletions(-)
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 15a2417c..4195997c 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -915,7 +915,9 @@ class GuiProjectTree(QTreeWidget):
dstItem = self._lastMove.get("parent", None)
dstIndex = self._lastMove.get("index", None)
- if srcItem is None or dstItem is None or dstIndex is None:
+ srcOK = isinstance(srcItem, QTreeWidgetItem)
+ dstOk = isinstance(dstItem, QTreeWidgetItem)
+ if not srcOK or not dstOk or dstIndex is None:
logger.verbose("No tree move to undo")
return False
@@ -1011,7 +1013,7 @@ class GuiProjectTree(QTreeWidget):
return
@pyqtSlot("QTreeWidgetItem*", int)
- def _treeDoubleClick(self, tItem, colNo):
+ def _treeDoubleClick(self, trItem, 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.
"""
@@ -1026,9 +1028,7 @@ class GuiProjectTree(QTreeWidget):
if tItem.isFileType():
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "")
else:
- trItem = self._getTreeItem(tHandle)
- if trItem is not None:
- trItem.setExpanded(not trItem.isExpanded())
+ trItem.setExpanded(not trItem.isExpanded())
return
@@ -1401,7 +1401,7 @@ class GuiProjectTree(QTreeWidget):
dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
dlgMerge.exec_()
- if dlgMerge.result() != QDialog.Accepted:
+ if dlgMerge.result() == QDialog.Accepted:
mrgData = dlgMerge.getData()
mrgList = mrgData.get("finalItems", [])
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index f4bfa1c8..8e683742 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -22,12 +22,15 @@ along with this program. If not, see .
import os
import pytest
+from mock import causeOSError
from tools import C, buildTestProject
-from PyQt5.QtWidgets import QMessageBox, QMenu
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidgetItem, QDialog
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass
-from novelwriter.dialogs import GuiEditLabel
+from novelwriter.core import NWDoc
+from novelwriter.dialogs import GuiEditLabel, GuiDocMerge
from novelwriter.gui.projtree import GuiProjectTree
@@ -560,16 +563,18 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
monkeypatch.setattr(QMenu, "exec_", lambda *a: None)
- nwTree = nwGUI.projView
-
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
# Handles for new objects
- hCharNote = "0000000000011"
- hNovelNote = "0000000000012"
+ hCharNote = "0000000000011"
+ hNovelNote = "0000000000012"
+ hSubNote = "0000000000013"
+ hNewFolderOne = "0000000000014"
+ hNewFolderTwo = "0000000000016"
+ projView = nwGUI.projView
projTree = nwGUI.projView.projTree
projTree.setExpandedFromHandle(None, True)
@@ -581,6 +586,10 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
projTree.setSelectedHandle(C.hNovelRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
+ nwGUI.theProject.newFile("SubNote", hNovelNote)
+ projTree.revealNewTreeItem(hSubNote)
+ assert nwGUI.theProject.tree[hSubNote].itemParent == hNovelNote
+
def itemPos(tHandle):
return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center()
@@ -596,6 +605,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
assert projTree._openContextMenu(itemPos(C.hChapterDoc)) is True
assert projTree._openContextMenu(itemPos(C.hCharRoot)) is True
assert projTree._openContextMenu(itemPos(hCharNote)) is True
+ assert projTree._openContextMenu(itemPos(hNovelNote)) is True
# Check the keyboard shortcut handler as well
projTree.setSelectedHandle(C.hNovelRoot)
@@ -633,18 +643,15 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
# Convert Folders to Documents
# ============================
- hNewFolderOne = "0000000000013"
- hNewFolderTwo = "0000000000015"
+ projView.setSelectedHandle(hNovelNote)
+ assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True
+ projView.setSelectedHandle(hNewFolderOne)
+ assert projView.projTree.newTreeItem(nwItemType.FILE) is True
- nwTree.setSelectedHandle(hNovelNote)
- assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
- nwTree.setSelectedHandle(hNewFolderOne)
- assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
-
- nwTree.setSelectedHandle(hNovelNote)
- assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
- nwTree.setSelectedHandle(hNewFolderTwo)
- assert nwTree.projTree.newTreeItem(nwItemType.FILE, isNote=True) is True
+ projView.setSelectedHandle(hNovelNote)
+ assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True
+ projView.setSelectedHandle(hNewFolderTwo)
+ assert projView.projTree.newTreeItem(nwItemType.FILE, isNote=True) is True
# Click no on the dialog
with monkeypatch.context() as mp:
@@ -665,3 +672,205 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
# qtbot.stop()
# END Test testGuiProjTree_ContextMenu
+
+
+@pytest.mark.gui
+def testGuiProjTree_MergeDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ipsumText):
+ """Test the merge document function.
+ """
+ mergeData = {}
+
+ # 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(GuiDocMerge, "__init__", lambda *a: None)
+ monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None)
+ monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QDialog.Accepted)
+ monkeypatch.setattr(GuiDocMerge, "getData", lambda *a: mergeData)
+
+ # Create a project
+ prjDir = os.path.join(fncDir, "project")
+ buildTestProject(nwGUI, prjDir)
+
+ theProject = nwGUI.theProject
+ projTree = nwGUI.projView.projTree
+
+ mergedDoc1 = "0000000000014"
+
+ # Create File to Merge
+ hChapter1 = theProject.newFile("Chapter 1", C.hNovelRoot)
+ hSceneOne11 = theProject.newFile("Scene 1.1", hChapter1)
+ hSceneOne12 = theProject.newFile("Scene 1.2", hChapter1)
+ hSceneOne13 = theProject.newFile("Scene 1.3", hChapter1)
+
+ docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
+ docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
+ docText3 = "\n\n".join(ipsumText[2:4]) + "\n\n"
+ docText4 = "\n\n".join(ipsumText[3:5]) + "\n\n"
+
+ lenText1 = len(docText1)
+ lenText2 = len(docText2)
+ lenText3 = len(docText3)
+ lenText4 = len(docText4)
+ lenAll = lenText1 + lenText2 + lenText3 + lenText4
+
+ theProject.writeNewFile(hChapter1, 2, True, docText1)
+ theProject.writeNewFile(hSceneOne11, 3, True, docText2)
+ theProject.writeNewFile(hSceneOne12, 3, True, docText3)
+ theProject.writeNewFile(hSceneOne13, 3, True, docText4)
+
+ projTree.revealNewTreeItem(hChapter1)
+ projTree.revealNewTreeItem(hSceneOne11)
+ projTree.revealNewTreeItem(hSceneOne12)
+ projTree.revealNewTreeItem(hSceneOne13)
+
+ # Invalid file handle
+ assert projTree._mergeDocuments(C.hInvalid, False) is False
+
+ # Cannot merge root item
+ assert projTree._mergeDocuments(C.hNovelRoot, False) is False
+
+ # Merge to new file, but there is now merge data
+ mergeData.clear()
+ assert projTree._mergeDocuments(hChapter1, True) is False
+
+ # Merge to New Doc
+ # ================
+
+ # Set merge job for new documents
+ mergeData["finalItems"] = [hChapter1, hSceneOne11, hSceneOne12, hSceneOne13]
+ mergeData["moveToTrash"] = False
+
+ # User cancels merge
+ with monkeypatch.context() as mp:
+ mp.setattr(GuiDocMerge, "result", lambda *a: QDialog.Rejected)
+ assert projTree._mergeDocuments(hChapter1, True) is False
+
+ # The merge goes through
+ assert projTree._mergeDocuments(hChapter1, True) is True
+ assert len(NWDoc(theProject, mergedDoc1).readDocument()) > lenAll
+
+ # Merge to Existing Doc
+ # =====================
+
+ # Set merge job for parent document
+ mergeData["finalItems"] = [hSceneOne11, hSceneOne12, hSceneOne13]
+ mergeData["moveToTrash"] = False
+
+ # Merging to a folder is not allowed
+ assert projTree._mergeDocuments(C.hChapterDir, False) is False
+
+ # Block writing and check error handling
+ with monkeypatch.context() as mp:
+ mp.setattr("builtins.open", causeOSError)
+ assert projTree._mergeDocuments(hChapter1, False) is False
+
+ # Successful merge, and move to trash
+ mergeData["moveToTrash"] = True
+ assert len(NWDoc(theProject, hChapter1).readDocument()) < lenAll
+ assert projTree._mergeDocuments(hChapter1, False) is True
+ assert len(NWDoc(theProject, hChapter1).readDocument()) > lenAll
+
+ assert theProject.tree.isTrash(hSceneOne11)
+ assert theProject.tree.isTrash(hSceneOne12)
+ assert theProject.tree.isTrash(hSceneOne13)
+
+ # qtbot.stop()
+
+# END Test testGuiProjTree_MergeDocument
+
+
+@pytest.mark.gui
+def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
+ """Test various parts of the project tree class not covered by
+ other tests.
+ """
+ # 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)
+
+ # Create a project
+ prjDir = os.path.join(fncDir, "project")
+ buildTestProject(nwGUI, prjDir)
+
+ projView = nwGUI.projView
+ projTree = nwGUI.projView.projTree
+
+ # Method: initSettings
+ # ====================
+
+ # Test that the scrollbar setting works
+ nwGUI.mainConf.hideVScroll = True
+ nwGUI.mainConf.hideHScroll = True
+ projView.initSettings()
+ assert projTree.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
+ assert projTree.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
+
+ nwGUI.mainConf.hideVScroll = False
+ nwGUI.mainConf.hideHScroll = False
+ projView.initSettings()
+ assert projTree.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
+ assert projTree.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
+
+ # Method: revealNewTreeItem
+ # =========================
+
+ # Send invalid handle
+ assert projTree.revealNewTreeItem(C.hInvalid) is False
+
+ # Try to add an oprhaned file to the tree
+ nHandle = nwGUI.theProject.newFile("Test", C.hNovelRoot)
+ nwGUI.theProject.tree[nHandle].setParent(None)
+ assert projTree.revealNewTreeItem(nHandle) is False
+
+ # Method: undoLastMove
+ # ====================
+
+ # Nothing to move
+ assert projTree.undoLastMove() is False
+
+ projTree._lastMove["item"] = QTreeWidgetItem()
+ projTree._lastMove["parent"] = QTreeWidgetItem()
+ projTree._lastMove["index"] = 0
+ assert projTree.undoLastMove() is False
+
+ projTree._lastMove["item"] = projTree._treeMap[C.hTitlePage]
+ projTree._lastMove["parent"] = QTreeWidgetItem()
+ projTree._lastMove["index"] = 0
+ assert projTree.undoLastMove() is False
+
+ # Slot: _treeDoubleClick
+ # ======================
+
+ # Try to open a file with nothings selected
+ projTree.clearSelection()
+ projTree._treeDoubleClick(QTreeWidgetItem(), 0)
+ assert nwGUI.docEditor.docHandle() is None
+
+ # When the item cannot be found
+ projTree._getTreeItem(C.hTitlePage).setSelected(True)
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None)
+ projTree._treeDoubleClick(QTreeWidgetItem(), 0)
+ assert nwGUI.docEditor.docHandle() is None
+
+ # Successfully open a file
+ projTree._treeDoubleClick(projTree._getTreeItem(C.hTitlePage), 0)
+ assert nwGUI.docEditor.docHandle() == C.hTitlePage
+ projTree._getTreeItem(C.hTitlePage).setSelected(False)
+
+ # A non-file item should be expanded instead
+ projTree._getTreeItem(C.hNovelRoot).setExpanded(False)
+ projTree._getTreeItem(C.hNovelRoot).setSelected(True)
+ projTree._treeDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1)
+ assert nwGUI.docEditor.docHandle() == C.hTitlePage
+ assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True
+
+ # qtbot.stop()
+
+# END Test testGuiProjTree_Other
From 417758a29a9bffdef4a9d73cd53a1c7a75b75676 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Oct 2022 21:48:40 +0200
Subject: [PATCH 23/92] Add coverage of new function in project class
---
.../coreProject_NewFileFolder_nwProject.nwx | 68 +++++++-------
.../coreProject_NewRoot_nwProject.nwx | 82 ++++++++---------
tests/test_core/test_core_project.py | 90 ++++++++++++++-----
3 files changed, 142 insertions(+), 98 deletions(-)
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
index b8df1c93..c0ec400a 100644
--- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx
+++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
New Novel
@@ -17,9 +17,9 @@
None
None
None
- 2
+ 4
1
- 1
+ 3
%title%
@@ -29,62 +29,62 @@
- 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
- -
+
-
- Stuff
+ Stuff
- -
+
-
- Hello
+ Hello
- -
-
- Jane
+
-
+
+ Jane
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 9102601d..3e372adb 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
New Novel
@@ -29,82 +29,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
- -
+
-
- Characters
+ Characters
- -
+
-
- Locations
+ Locations
- -
+
-
- Timeline
+ Timeline
- -
+
-
- Objects
+ Objects
- -
+
-
- Custom
+ Custom
- -
+
-
- Custom
+ Custom
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 5ea25676..5a95295b 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -26,7 +26,7 @@ from lxml import etree
from shutil import copyfile
from zipfile import ZipFile
-from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE
+from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE, C
from mock import causeOSError
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@@ -256,6 +256,7 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx")
theProject = NWProject(mockGUI)
+ mockRnd.reset()
buildTestProject(theProject, fncDir)
assert theProject.setProjectPath(fncDir) is True
@@ -263,14 +264,14 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
- 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.newRoot(nwItemClass.NOVEL) == "0000000000020"
+ assert theProject.newRoot(nwItemClass.PLOT) == "0000000000021"
+ assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000022"
+ assert theProject.newRoot(nwItemClass.WORLD) == "0000000000023"
+ assert theProject.newRoot(nwItemClass.TIMELINE) == "0000000000024"
+ assert theProject.newRoot(nwItemClass.OBJECT) == "0000000000025"
+ assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000026"
+ assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000027"
assert theProject.projChanged is True
assert theProject.saveProject() is True
@@ -280,11 +281,30 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
assert theProject.projChanged is False
+ # Delete the new items
+ assert theProject.removeItem("0000000000020") is True
+ assert theProject.removeItem("0000000000021") is True
+ assert theProject.removeItem("0000000000022") is True
+ assert theProject.removeItem("0000000000023") is True
+ assert theProject.removeItem("0000000000024") is True
+ assert theProject.removeItem("0000000000025") is True
+ assert theProject.removeItem("0000000000026") is True
+ assert theProject.removeItem("0000000000027") is True
+
+ assert "0000000000020" not in theProject.tree
+ assert "0000000000021" not in theProject.tree
+ assert "0000000000022" not in theProject.tree
+ assert "0000000000023" not in theProject.tree
+ assert "0000000000024" not in theProject.tree
+ assert "0000000000025" not in theProject.tree
+ assert "0000000000026" not in theProject.tree
+ assert "0000000000027" not in theProject.tree
+
# END Test testCoreProject_NewRoot
@pytest.mark.core
-def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd):
+def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd):
"""Check that new files can be added to the project.
"""
projFile = os.path.join(fncDir, "nwProject.nwx")
@@ -292,6 +312,7 @@ def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd):
compFile = os.path.join(refDir, "coreProject_NewFileFolder_nwProject.nwx")
theProject = NWProject(mockGUI)
+ mockRnd.reset()
buildTestProject(theProject, fncDir)
assert theProject.setProjectPath(fncDir) is True
@@ -304,35 +325,58 @@ def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd):
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 theProject.newFolder("Stuff", C.hNovelRoot) == "0000000000020"
+ assert theProject.newFile("Hello", "0000000000020") == "0000000000021"
+ assert theProject.newFile("Jane", C.hCharRoot) == "0000000000022"
- assert "0000000000028" in theProject.tree
- assert "0000000000029" in theProject.tree
- assert "000000000002a" in theProject.tree
+ assert "0000000000020" in theProject.tree
+ assert "0000000000021" in theProject.tree
+ assert "0000000000022" 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
+ assert theProject.writeNewFile("0000000000020", 1, True) is False # Not a file
+ assert theProject.writeNewFile(C.hTitlePage, 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("0000000000021", 2, True) is True
+ assert NWDoc(theProject, "0000000000021").readDocument() == "## Hello\n\n"
- assert theProject.writeNewFile("000000000002a", 1, False) is True
- assert NWDoc(theProject, "000000000002a").readDocument() == "# Jane\n\n"
+ # Write to file with additional text, success
+ assert theProject.writeNewFile("0000000000022", 1, False, "Hi Jane\n\n") is True
+ assert NWDoc(theProject, "0000000000022").readDocument() == "# Jane\n\nHi Jane\n\n"
# Save, close and check
assert theProject.projChanged is True
assert theProject.saveProject() is True
- assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
assert theProject.projChanged is False
+ # Delete new file, but block access
+ with monkeypatch.context() as mp:
+ mp.setattr("os.unlink", causeOSError)
+ assert theProject.removeItem("0000000000021") is False
+ assert "0000000000021" in theProject.tree
+
+ # Delete new files and folders
+ assert os.path.isfile(os.path.join(fncDir, "content", "0000000000022.nwd"))
+ assert os.path.isfile(os.path.join(fncDir, "content", "0000000000021.nwd"))
+
+ assert theProject.removeItem("0000000000022") is True
+ assert theProject.removeItem("0000000000021") is True
+ assert theProject.removeItem("0000000000020") is True
+
+ assert not os.path.isfile(os.path.join(fncDir, "content", "0000000000022.nwd"))
+ assert not os.path.isfile(os.path.join(fncDir, "content", "0000000000021.nwd"))
+
+ assert "0000000000020" not in theProject.tree
+ assert "0000000000021" not in theProject.tree
+ assert "0000000000022" not in theProject.tree
+
+ assert theProject.closeProject() is True
+
# END Test testCoreProject_NewFileFolder
From 9b5d2b2c2bb70421acd89acd7c01504151ea2434 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Oct 2022 22:26:14 +0200
Subject: [PATCH 24/92] Resolve issue #1150
---
novelwriter/gui/doceditor.py | 5 +++++
sample/nwProject.nwx | 8 ++++----
.../guiEditor_Main_Final_000000000000f.nwd | 3 +++
.../guiEditor_Main_Final_nwProject.nwx | 10 +++++-----
tests/test_gui/test_gui_guimain.py | 17 ++++++++++++-----
5 files changed, 29 insertions(+), 14 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index cfcb8409..0942d823 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -1992,6 +1992,11 @@ class GuiDocEditor(QTextEdit):
nDelete = 3
tInsert = nwUnicode.U_HELLIP
+ elif theOne == nwUnicode.U_LSEP:
+ # This resolves issue #1150
+ nDelete = 1
+ tInsert = nwUnicode.U_PSEP
+
tCheck = tInsert
if self.mainConf.fmtPadBefore and tCheck in self.mainConf.fmtPadBefore:
if self._allowSpaceBeforeColon(theText, tCheck):
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index b5bf2647..0cd59b77 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,13 +1,13 @@
-
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 1378
+ 1380
236
- 69273
+ 69335
False
@@ -71,7 +71,7 @@
Chapter One
-
-
+
Making a Scene
-
diff --git a/tests/reference/guiEditor_Main_Final_000000000000f.nwd b/tests/reference/guiEditor_Main_Final_000000000000f.nwd
index ab603231..b45a9dca 100644
--- a/tests/reference/guiEditor_Main_Final_000000000000f.nwd
+++ b/tests/reference/guiEditor_Main_Final_000000000000f.nwd
@@ -21,6 +21,9 @@
This is a paragraph of nonsense text.
+This is another paragraph
+with a line separator in it.
+
This is another paragraph of much longer nonsense text. It is in fact 1 very very NONSENSICAL nonsense text! We can also try replacing “quotes”, even single ‘quotes’ are replaced. Isn’t that nice? We can hyphen-ate, make dashes – and even longer dashes — if we want. Ellipsis? Not a problem either … How about three hyphens — for long dash? It works too.
“Full line double quoted text.”
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index f61d3493..84e7e999 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,12 +1,12 @@
-
+
New Project
New Novel
Jane Doe
4
2
- 4
+ 5
True
@@ -17,8 +17,8 @@
None
0000000000008
0000000000008
- 153
- 126
+ 163
+ 136
27
@@ -59,7 +59,7 @@
New Chapter
-
-
+
New Scene
-
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 76651260..0a569e21 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -22,8 +22,8 @@ along with this program. If not, see .
import os
import pytest
-from shutil import copyfile
from tools import C, cmpFiles, buildTestProject, XML_IGNORE, writeFile
+from shutil import copyfile
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox, QInputDialog
@@ -31,8 +31,8 @@ from PyQt5.QtWidgets import QMessageBox, QInputDialog
from novelwriter.gui import GuiDocEditor, GuiNovelView, GuiOutlineView
from novelwriter.enum import nwItemType, nwView, nwWidget
from novelwriter.tools import GuiProjectWizard
-from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.dialogs import GuiEditLabel
+from novelwriter.gui.projtree import GuiProjectTree
keyDelay = 2
typeDelay = 1
@@ -196,8 +196,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
qtbot.wait(stepDelay)
- # qtbot.stopForInteraction()
-
# Re-open project
assert nwGUI.openProject(fncProj)
qtbot.wait(stepDelay)
@@ -372,6 +370,15 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ # Don't allow Shift+Enter to insert a line separator (issue #1150)
+ for c in "This is another paragraph":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Enter, modifier=Qt.ShiftModifier, delay=typeDelay)
+ for c in "with a line separator in it.":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+
# Auto-Replace
# ============
@@ -540,7 +547,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
- # qtbot.stopForInteraction()
+ # qtbot.stop()
# END Test testGuiMain_Editing
From 3a7c3cec3fd76e31cac082d013ffe79153cecc39 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Oct 2022 22:44:06 +0200
Subject: [PATCH 25/92] Reset idle time when creating a new project (#1149)
---
novelwriter/guimain.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 7b165676..1e670d2f 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -360,18 +360,26 @@ class GuiMain(QMainWindow):
logger.info("Creating new project")
if self.theProject.newProject(projData):
+
self.hasProject = True
+ self.idleRefTime = time()
+ self.idleTime = 0.0
+
self.rebuildTrees()
self.saveProject()
+
self.docEditor.setDictionaries()
self.novelView.openProjectTasks()
self.outlineView.openProjectTasks()
self.rebuildIndex(beQuiet=True)
+
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
self.statusBar.setDocumentStatus(nwState.NONE)
self.statusBar.setStatus(self.tr("New project created ..."))
+
self._updateWindowTitle(self.theProject.projName)
+
else:
self.theProject.clearProject()
return False
From 1f05e90266ba330111e9fbde4dd8b533420ce01a Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Oct 2022 22:49:36 +0200
Subject: [PATCH 26/92] Remove the context menu in the main window (#1147)
---
novelwriter/guimain.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 1e670d2f..67442f94 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -191,6 +191,7 @@ class GuiMain(QMainWindow):
self.setCentralWidget(self.mainStack)
self.setStatusBar(self.statusBar)
self.addToolBar(Qt.LeftToolBarArea, self.viewsBar)
+ self.setContextMenuPolicy(Qt.NoContextMenu) # Issue #1147
# Connect Signals
# ===============
From 5d6778be57a31d87ece251214eb2f62e2a8450ae Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Oct 2022 23:17:58 +0200
Subject: [PATCH 27/92] Clean up class initialisation
---
novelwriter/core/document.py | 2 +-
novelwriter/core/item.py | 2 +-
novelwriter/core/options.py | 2 +-
novelwriter/core/project.py | 2 +-
novelwriter/core/spellcheck.py | 2 +-
novelwriter/core/status.py | 2 +-
novelwriter/core/tohtml.py | 4 +--
novelwriter/core/tomd.py | 2 +-
novelwriter/core/toodt.py | 8 +++---
novelwriter/core/tree.py | 2 +-
novelwriter/dialogs/about.py | 2 +-
novelwriter/dialogs/docsplit.py | 2 +-
novelwriter/dialogs/editlabel.py | 2 +-
novelwriter/dialogs/preferences.py | 44 ++++++++++++++---------------
novelwriter/dialogs/projdetails.py | 6 ++--
novelwriter/dialogs/projload.py | 2 +-
novelwriter/dialogs/projsettings.py | 22 +++++++--------
novelwriter/dialogs/quotes.py | 2 +-
novelwriter/dialogs/updates.py | 2 +-
novelwriter/dialogs/wordlist.py | 2 +-
novelwriter/gui/doceditor.py | 18 ++++++------
novelwriter/gui/dochighlight.py | 2 +-
novelwriter/gui/docviewer.py | 14 ++++-----
novelwriter/gui/itemdetails.py | 2 +-
novelwriter/gui/mainmenu.py | 2 +-
novelwriter/gui/noveltree.py | 10 +++----
novelwriter/gui/outline.py | 10 +++----
novelwriter/gui/projtree.py | 10 +++----
novelwriter/gui/statusbar.py | 2 +-
novelwriter/gui/viewsbar.py | 2 +-
novelwriter/guimain.py | 2 +-
novelwriter/tools/build.py | 6 ++--
novelwriter/tools/lipsum.py | 2 +-
novelwriter/tools/projwizard.py | 16 +++++------
novelwriter/tools/writingstats.py | 2 +-
tests/mock.py | 4 +--
36 files changed, 109 insertions(+), 109 deletions(-)
diff --git a/novelwriter/core/document.py b/novelwriter/core/document.py
index 5420d44e..672fbee5 100644
--- a/novelwriter/core/document.py
+++ b/novelwriter/core/document.py
@@ -33,7 +33,7 @@ from novelwriter.common import isHandle, sha256sum
logger = logging.getLogger(__name__)
-class NWDoc():
+class NWDoc:
def __init__(self, theProject, theHandle):
diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py
index 3f463a1b..a8faea8b 100644
--- a/novelwriter/core/item.py
+++ b/novelwriter/core/item.py
@@ -36,7 +36,7 @@ from novelwriter.constants import nwLabels, trConst
logger = logging.getLogger(__name__)
-class NWItem():
+class NWItem:
def __init__(self, theProject):
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index d64970ae..d36e677b 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -63,7 +63,7 @@ VALID_MAP = {
}
-class OptionState():
+class OptionState:
def __init__(self, theProject):
self.theProject = theProject
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 475300fd..dcfa66d7 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -52,7 +52,7 @@ from novelwriter.constants import trConst, nwFiles, nwLabels
logger = logging.getLogger(__name__)
-class NWProject():
+class NWProject:
FILE_VERSION = "1.4" # The current project file format version
diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py
index d9c506cd..4a63bf39 100644
--- a/novelwriter/core/spellcheck.py
+++ b/novelwriter/core/spellcheck.py
@@ -33,7 +33,7 @@ from novelwriter.error import logException
logger = logging.getLogger(__name__)
-class NWSpellEnchant():
+class NWSpellEnchant:
def __init__(self):
diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py
index 4bade5e7..a379f911 100644
--- a/novelwriter/core/status.py
+++ b/novelwriter/core/status.py
@@ -37,7 +37,7 @@ from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__)
-class NWStatus():
+class NWStatus:
STATUS = 1
IMPORT = 2
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index 86a21786..2110300c 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -38,7 +38,7 @@ class ToHtml(Tokenizer):
M_EBOOK = 2 # Tweak output for converting to epub
def __init__(self, theProject):
- Tokenizer.__init__(self, theProject)
+ super().__init__(theProject)
self._genMode = self.M_EXPORT
self._cssStyles = True
@@ -107,7 +107,7 @@ class ToHtml(Tokenizer):
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
"""
- Tokenizer.doPreProcessing(self)
+ super().doPreProcessing()
self._theText = self._theText.translate(self._trMap)
return
diff --git a/novelwriter/core/tomd.py b/novelwriter/core/tomd.py
index 48d23a35..89ae9e4f 100644
--- a/novelwriter/core/tomd.py
+++ b/novelwriter/core/tomd.py
@@ -37,7 +37,7 @@ class ToMarkdown(Tokenizer):
M_GH = 1 # GitHub Markdown
def __init__(self, theProject):
- Tokenizer.__init__(self, theProject)
+ super().__init__(theProject)
self._genMode = self.M_STD
self._fullMD = []
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index 59eaf30f..022bda4c 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -89,7 +89,7 @@ M_DEL = ~X_DEL
class ToOdt(Tokenizer):
def __init__(self, theProject, isFlat):
- Tokenizer.__init__(self, theProject)
+ super().__init__(theProject)
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
@@ -994,7 +994,7 @@ class ToOdt(Tokenizer):
# Auto-Style Classes
# =============================================================================================== #
-class ODTParagraphStyle():
+class ODTParagraphStyle:
"""Wrapper class for the paragraph style setting used by the
exporter. Only the used settings are exposed here to keep the class
minimal and fast.
@@ -1208,7 +1208,7 @@ class ODTParagraphStyle():
# END Class ODTParagraphStyle
-class ODTTextStyle():
+class ODTTextStyle:
"""Wrapper class for the text style setting used by the exporter.
Only the used settings are exposed here to keep the class minimal
and fast.
@@ -1297,7 +1297,7 @@ X_SPAN_TEXT = 2
X_SPAN_SING = 3
-class XMLParagraph():
+class XMLParagraph:
"""This is a helper class to manage the text content of a single
XML element using mixed content tags.
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index 6bb24959..ec8073df 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -38,7 +38,7 @@ from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__)
-class NWTree():
+class NWTree:
MAX_DEPTH = 1000 # Cap of tree traversing for loops
diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py
index 9cf43daf..5b3a509e 100644
--- a/novelwriter/dialogs/about.py
+++ b/novelwriter/dialogs/about.py
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiAbout(QDialog):
def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiAbout ...")
self.setObjectName("GuiAbout")
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index e4c9b13a..f99166fb 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
class GuiDocSplit(QDialog):
def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocSplit ...")
self.setObjectName("GuiDocSplit")
diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py
index 03bf9d08..a1982ede 100644
--- a/novelwriter/dialogs/editlabel.py
+++ b/novelwriter/dialogs/editlabel.py
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
class GuiEditLabel(QDialog):
def __init__(self, parent, text=""):
- QDialog.__init__(self, parent=parent)
+ super().__init__(parent=parent)
self.setObjectName("GuiEditLabel")
self.setWindowTitle(self.tr("Item Label"))
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index f807fde1..964f92b0 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiPreferences(PagedDialog):
def __init__(self, mainGui):
- PagedDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiPreferences ...")
self.setObjectName("GuiPreferences")
@@ -55,13 +55,13 @@ class GuiPreferences(PagedDialog):
self.setWindowTitle(self.tr("Preferences"))
- 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.tabGeneral = GuiPreferencesGeneral(self, self.mainGui)
+ self.tabProjects = GuiPreferencesProjects(self, self.mainGui)
+ self.tabDocs = GuiPreferencesDocuments(self, self.mainGui)
+ self.tabEditor = GuiPreferencesEditor(self, self.mainGui)
+ self.tabSyntax = GuiPreferencesSyntax(self, self.mainGui)
+ self.tabAuto = GuiPreferencesAutomation(self, self.mainGui)
+ self.tabQuote = GuiPreferencesQuotes(self, self.mainGui)
self.addTab(self.tabGeneral, self.tr("General"))
self.addTab(self.tabProjects, self.tr("Projects"))
@@ -138,8 +138,8 @@ class GuiPreferences(PagedDialog):
class GuiPreferencesGeneral(QWidget):
- def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ def __init__(self, prefsGui, mainGui):
+ super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -344,8 +344,8 @@ class GuiPreferencesGeneral(QWidget):
class GuiPreferencesProjects(QWidget):
- def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ def __init__(self, prefsGui, mainGui):
+ super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -505,8 +505,8 @@ class GuiPreferencesProjects(QWidget):
class GuiPreferencesDocuments(QWidget):
- def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ def __init__(self, prefsGui, mainGui):
+ super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -666,8 +666,8 @@ class GuiPreferencesDocuments(QWidget):
class GuiPreferencesEditor(QWidget):
- def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ def __init__(self, prefsGui, mainGui):
+ super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -840,8 +840,8 @@ class GuiPreferencesEditor(QWidget):
class GuiPreferencesSyntax(QWidget):
- def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ def __init__(self, prefsGui, mainGui):
+ super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -943,8 +943,8 @@ class GuiPreferencesSyntax(QWidget):
class GuiPreferencesAutomation(QWidget):
- def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ def __init__(self, prefsGui, mainGui):
+ super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -1100,8 +1100,8 @@ class GuiPreferencesAutomation(QWidget):
class GuiPreferencesQuotes(QWidget):
- def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ def __init__(self, prefsGui, mainGui):
+ super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py
index 7b23498a..67ab6ebe 100644
--- a/novelwriter/dialogs/projdetails.py
+++ b/novelwriter/dialogs/projdetails.py
@@ -46,7 +46,7 @@ logger = logging.getLogger(__name__)
class GuiProjectDetails(PagedDialog):
def __init__(self, mainGui):
- PagedDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectDetails ...")
self.setObjectName("GuiProjectDetails")
@@ -142,7 +142,7 @@ class GuiProjectDetails(PagedDialog):
class GuiProjectDetailsMain(QWidget):
def __init__(self, mainGui, theProject):
- QWidget.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
self.mainConf = novelwriter.CONFIG
self.theProject = theProject
@@ -274,7 +274,7 @@ class GuiProjectDetailsContents(QWidget):
C_PROG = 4
def __init__(self, mainGui, theProject):
- QWidget.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
self.mainConf = novelwriter.CONFIG
self.theProject = theProject
diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py
index c7229f44..fd06fabf 100644
--- a/novelwriter/dialogs/projload.py
+++ b/novelwriter/dialogs/projload.py
@@ -54,7 +54,7 @@ class GuiProjectLoad(QDialog):
C_TIME = 2
def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectLoad ...")
self.setObjectName("GuiProjectLoad")
diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py
index 11a640b1..d2484a20 100644
--- a/novelwriter/dialogs/projsettings.py
+++ b/novelwriter/dialogs/projsettings.py
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiProjectSettings(PagedDialog):
def __init__(self, mainGui):
- PagedDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectSettings ...")
self.setObjectName("GuiProjectSettings")
@@ -67,10 +67,10 @@ class GuiProjectSettings(PagedDialog):
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
)
- 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.tabMain = GuiProjectEditMain(self, self.mainGui, self.theProject)
+ self.tabStatus = GuiProjectEditStatus(self, self.mainGui, self.theProject, True)
+ self.tabImport = GuiProjectEditStatus(self, self.mainGui, self.theProject, False)
+ self.tabReplace = GuiProjectEditReplace(self, self.mainGui, self.theProject)
self.addTab(self.tabMain, self.tr("Settings"))
self.addTab(self.tabStatus, self.tr("Status"))
@@ -166,8 +166,8 @@ class GuiProjectSettings(PagedDialog):
class GuiProjectEditMain(QWidget):
- def __init__(self, mainGui, theProject):
- QWidget.__init__(self, mainGui)
+ def __init__(self, projGui, mainGui, theProject):
+ super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -256,8 +256,8 @@ class GuiProjectEditStatus(QWidget):
COL_ROLE = Qt.UserRole + 1
NUM_ROLE = Qt.UserRole + 2
- def __init__(self, mainGui, theProject, isStatus):
- QWidget.__init__(self, mainGui)
+ def __init__(self, prefsGui, mainGui, theProject, isStatus):
+ super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -527,8 +527,8 @@ class GuiProjectEditReplace(QWidget):
COL_KEY = 0
COL_REPL = 1
- def __init__(self, mainGui, theProject):
- QWidget.__init__(self, mainGui)
+ def __init__(self, prefsGui, mainGui, theProject):
+ super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py
index 299386d8..685459d4 100644
--- a/novelwriter/dialogs/quotes.py
+++ b/novelwriter/dialogs/quotes.py
@@ -43,7 +43,7 @@ class GuiQuoteSelect(QDialog):
selectedQuote = ""
def __init__(self, parent=None, currentQuote='"'):
- QDialog.__init__(self, parent=parent)
+ super().__init__(parent=parent)
self.mainConf = novelwriter.CONFIG
diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py
index c4782e6d..8b8f1d27 100644
--- a/novelwriter/dialogs/updates.py
+++ b/novelwriter/dialogs/updates.py
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiUpdates(QDialog):
def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiUpdates ...")
self.setObjectName("GuiUpdates")
diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py
index 9a17eed8..f155c25e 100644
--- a/novelwriter/dialogs/wordlist.py
+++ b/novelwriter/dialogs/wordlist.py
@@ -43,7 +43,7 @@ logger = logging.getLogger(__name__)
class GuiWordList(QDialog):
def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiWordList ...")
self.setObjectName("GuiWordList")
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 0942d823..3f9fae10 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -73,7 +73,7 @@ class GuiDocEditor(QTextEdit):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui):
- QTextEdit.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocEditor ...")
@@ -1003,7 +1003,7 @@ class GuiDocEditor(QTextEdit):
if self.mainConf.autoScroll:
cOld = self.cursorRect().center().y()
- QTextEdit.keyPressEvent(self, keyEvent)
+ super().keyPressEvent(keyEvent)
kMod = keyEvent.modifiers()
okMod = kMod == Qt.NoModifier or kMod == Qt.ShiftModifier
@@ -1022,7 +1022,7 @@ class GuiDocEditor(QTextEdit):
doAnim.start()
else:
- QTextEdit.keyPressEvent(self, keyEvent)
+ super().keyPressEvent(keyEvent)
self.docFooter.updateLineCount()
@@ -1049,7 +1049,7 @@ class GuiDocEditor(QTextEdit):
theCursor = self.cursorForPosition(theEvent.pos())
self._followTag(theCursor)
- QTextEdit.mouseReleaseEvent(self, theEvent)
+ super().mouseReleaseEvent(theEvent)
self.docFooter.updateLineCount()
return
@@ -1059,7 +1059,7 @@ class GuiDocEditor(QTextEdit):
has its margins adjusted according to user preferences.
"""
self.updateDocMargins()
- QTextEdit.resizeEvent(self, theEvent)
+ super().resizeEvent(theEvent)
return
##
@@ -2161,7 +2161,7 @@ class GuiDocEditor(QTextEdit):
class BackgroundWordCounter(QRunnable):
def __init__(self, docEditor, forSelection=False):
- QRunnable.__init__(self)
+ super().__init__()
self._docEditor = docEditor
self._forSelection = forSelection
@@ -2211,7 +2211,7 @@ class BackgroundWordCounterSignals(QObject):
class GuiDocEditSearch(QFrame):
def __init__(self, docEditor):
- QFrame.__init__(self, docEditor)
+ super().__init__(parent=docEditor)
logger.debug("Initialising GuiDocEditSearch ...")
@@ -2599,7 +2599,7 @@ class GuiDocEditSearch(QFrame):
class GuiDocEditHeader(QWidget):
def __init__(self, docEditor):
- QWidget.__init__(self, docEditor)
+ super().__init__(parent=docEditor)
logger.debug("Initialising GuiDocEditHeader ...")
@@ -2823,7 +2823,7 @@ class GuiDocEditHeader(QWidget):
class GuiDocEditFooter(QWidget):
def __init__(self, docEditor):
- QWidget.__init__(self, docEditor)
+ super().__init__(parent=docEditor)
logger.debug("Initialising GuiDocEditFooter ...")
diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py
index 8b7ec1a5..91aa0416 100644
--- a/novelwriter/gui/dochighlight.py
+++ b/novelwriter/gui/dochighlight.py
@@ -47,7 +47,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
BLOCK_TITLE = 4
def __init__(self, theDoc, mainGui, spEnchant):
- QSyntaxHighlighter.__init__(self, theDoc)
+ super().__init__(theDoc)
logger.debug("Initialising GuiDocHighlighter ...")
self.mainConf = novelwriter.CONFIG
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 6ce275a3..f4413805 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -54,7 +54,7 @@ class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui):
- QTextBrowser.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocViewer ...")
@@ -461,7 +461,7 @@ class GuiDocViewer(QTextBrowser):
has its margins adjusted according to user preferences.
"""
self.updateDocMargins()
- QTextBrowser.resizeEvent(self, theEvent)
+ super().resizeEvent(theEvent)
return
def mouseReleaseEvent(self, theEvent):
@@ -472,7 +472,7 @@ class GuiDocViewer(QTextBrowser):
elif theEvent.button() == Qt.ForwardButton:
self.navForward()
else:
- QTextBrowser.mouseReleaseEvent(self, theEvent)
+ super().mouseReleaseEvent(theEvent)
return
##
@@ -568,7 +568,7 @@ class GuiDocViewer(QTextBrowser):
# END Class GuiDocViewer
-class GuiDocViewHistory():
+class GuiDocViewHistory:
def __init__(self, docViewer):
@@ -708,7 +708,7 @@ class GuiDocViewHistory():
class GuiDocViewHeader(QWidget):
def __init__(self, docViewer):
- QWidget.__init__(self, docViewer)
+ super().__init__(parent=docViewer)
logger.debug("Initialising GuiDocViewHeader ...")
@@ -915,7 +915,7 @@ class GuiDocViewHeader(QWidget):
class GuiDocViewFooter(QWidget):
def __init__(self, docViewer):
- QWidget.__init__(self, docViewer)
+ super().__init__(parent=docViewer)
logger.debug("Initialising GuiDocViewFooter ...")
@@ -1141,7 +1141,7 @@ class GuiDocViewFooter(QWidget):
class GuiDocViewDetails(QScrollArea):
def __init__(self, mainGui):
- QScrollArea.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocViewDetails ...")
self.mainConf = novelwriter.CONFIG
diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py
index a467ed04..dbd2838c 100644
--- a/novelwriter/gui/itemdetails.py
+++ b/novelwriter/gui/itemdetails.py
@@ -38,7 +38,7 @@ logger = logging.getLogger(__name__)
class GuiItemDetails(QWidget):
def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiItemDetails ...")
self.mainConf = novelwriter.CONFIG
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 7877d44a..f4e6e668 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
class GuiMainMenu(QMenuBar):
def __init__(self, mainGui):
- QMenuBar.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiMainMenu ...")
self.mainConf = novelwriter.CONFIG
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index c8d88b88..b68fb812 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -63,7 +63,7 @@ class GuiNovelView(QWidget):
openDocumentRequest = pyqtSignal(str, Enum, int, str)
def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
self.mainGui = mainGui
self.theProject = mainGui.theProject
@@ -166,7 +166,7 @@ class GuiNovelView(QWidget):
class GuiNovelToolBar(QWidget):
def __init__(self, novelView):
- QTreeWidget.__init__(self, novelView)
+ super().__init__(parent=novelView)
logger.debug("Initialising GuiNovelToolBar ...")
@@ -333,7 +333,7 @@ class GuiNovelTree(QTreeWidget):
D_KEY = Qt.UserRole + 2
def __init__(self, novelView):
- QTreeWidget.__init__(self, novelView)
+ super().__init__(parent=novelView)
logger.debug("Initialising GuiNovelTree ...")
@@ -539,7 +539,7 @@ class GuiNovelTree(QTreeWidget):
mouse in a blank area of the tree view, and to load a document
for viewing if the user middle-clicked.
"""
- QTreeWidget.mousePressEvent(self, theEvent)
+ super().mousePressEvent(theEvent)
if theEvent.button() == Qt.LeftButton:
selItem = self.indexAt(theEvent.pos())
@@ -562,7 +562,7 @@ class GuiNovelTree(QTreeWidget):
def focusOutEvent(self, theEvent):
"""Clear the selection when the tree no longer has focus.
"""
- QTreeWidget.focusOutEvent(self, theEvent)
+ super().focusOutEvent(theEvent)
self.clearSelection()
return
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index b5e67b2e..ea76676f 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -57,7 +57,7 @@ class GuiOutlineView(QWidget):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -198,7 +198,7 @@ class GuiOutlineToolBar(QToolBar):
viewColumnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
- QTreeWidget.__init__(self, theOutline)
+ super().__init__(parent=theOutline)
logger.debug("Initialising GuiOutlineToolBar ...")
@@ -357,7 +357,7 @@ class GuiOutlineTree(QTreeWidget):
activeItemChanged = pyqtSignal(str, str)
def __init__(self, theOutline):
- QTreeWidget.__init__(self, theOutline)
+ super().__init__(parent=theOutline)
logger.debug("Initialising GuiOutlineTree ...")
@@ -741,7 +741,7 @@ class GuiOutlineHeaderMenu(QMenu):
columnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
- QMenu.__init__(self, theOutline)
+ super().__init__(parent=theOutline)
self.acceptToggle = True
@@ -792,7 +792,7 @@ class GuiOutlineDetails(QScrollArea):
itemTagClicked = pyqtSignal(str)
def __init__(self, theOutline):
- QScrollArea.__init__(self, theOutline)
+ super().__init__(parent=theOutline)
logger.debug("Initialising GuiOutlineDetails ...")
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 4195997c..675cf65c 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -63,7 +63,7 @@ class GuiProjectView(QWidget):
openDocumentRequest = pyqtSignal(str, Enum, int, str)
def __init__(self, mainGui):
- QWidget.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
self.mainGui = mainGui
@@ -164,7 +164,7 @@ class GuiProjectView(QWidget):
class GuiProjectToolBar(QWidget):
def __init__(self, projView):
- QTreeWidget.__init__(self, projView)
+ super().__init__(parent=projView)
logger.debug("Initialising GuiProjectToolBar ...")
@@ -327,7 +327,7 @@ class GuiProjectTree(QTreeWidget):
C_STATUS = 3
def __init__(self, projView):
- QTreeWidget.__init__(self, projView)
+ super().__init__(parent=projView)
logger.debug("Initialising GuiProjectTree ...")
@@ -1191,7 +1191,7 @@ class GuiProjectTree(QTreeWidget):
mouse in a blank area of the tree view, and to load a document
for viewing if the user middle-clicked.
"""
- QTreeWidget.mousePressEvent(self, theEvent)
+ super().mousePressEvent(theEvent)
if theEvent.button() == Qt.LeftButton:
selItem = self.indexAt(theEvent.pos())
@@ -1237,7 +1237,7 @@ class GuiProjectTree(QTreeWidget):
wCount = self._getItemWordCount(sHandle)
self.propagateCount(sHandle, 0)
- QTreeWidget.dropEvent(self, theEvent)
+ super().dropEvent(theEvent)
self._postItemMove(sHandle, wCount)
self._recordLastMove(sItem, pItem, pIndex)
self._alertTreeChange(sHandle, flush=True)
diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py
index a0cf9416..c6191b8d 100644
--- a/novelwriter/gui/statusbar.py
+++ b/novelwriter/gui/statusbar.py
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
class GuiMainStatus(QStatusBar):
def __init__(self, mainGui):
- QStatusBar.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiMainStatus ...")
diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py
index 031f4316..4d19f818 100644
--- a/novelwriter/gui/viewsbar.py
+++ b/novelwriter/gui/viewsbar.py
@@ -41,7 +41,7 @@ class GuiViewsBar(QToolBar):
viewChangeRequested = pyqtSignal(nwView)
def __init__(self, mainGui):
- QToolBar.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiViewsBar ...")
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 67442f94..79f56b97 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -62,7 +62,7 @@ logger = logging.getLogger(__name__)
class GuiMain(QMainWindow):
def __init__(self):
- QMainWindow.__init__(self)
+ super().__init__()
logger.debug("Initialising GUI ...")
self.setObjectName("GuiMain")
diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py
index 7a0eba6c..2915818c 100644
--- a/novelwriter/tools/build.py
+++ b/novelwriter/tools/build.py
@@ -66,7 +66,7 @@ class GuiBuildNovel(QDialog):
FMT_JSON_M = 9 # nW Markdown wrapped in JSON
def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiBuildNovel ...")
self.setObjectName("GuiBuildNovel")
@@ -1194,7 +1194,7 @@ class GuiBuildNovel(QDialog):
class GuiBuildNovelDocView(QTextBrowser):
def __init__(self, mainGui, theProject):
- QTextBrowser.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiBuildNovelDocView ...")
@@ -1343,7 +1343,7 @@ class GuiBuildNovelDocView(QTextBrowser):
def resizeEvent(self, theEvent):
"""Make sure the document title is the same width as the window.
"""
- QTextBrowser.resizeEvent(self, theEvent)
+ super().resizeEvent(theEvent)
self._updateDocMargins()
return
diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py
index 2086d615..64069e6f 100644
--- a/novelwriter/tools/lipsum.py
+++ b/novelwriter/tools/lipsum.py
@@ -43,7 +43,7 @@ logger = logging.getLogger(__name__)
class GuiLipsum(QDialog):
def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiLipsum ...")
self.setObjectName("GuiLipsum")
diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py
index 5b21f69a..d2602a4c 100644
--- a/novelwriter/tools/projwizard.py
+++ b/novelwriter/tools/projwizard.py
@@ -49,7 +49,7 @@ PAGE_FINAL = 4
class GuiProjectWizard(QWizard):
def __init__(self, mainGui):
- QWizard.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectWizard ...")
self.setObjectName("GuiProjectWizard")
@@ -88,7 +88,7 @@ class GuiProjectWizard(QWizard):
class ProjWizardIntroPage(QWizardPage):
def __init__(self, theWizard):
- QWizardPage.__init__(self)
+ super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -158,7 +158,7 @@ class ProjWizardIntroPage(QWizardPage):
class ProjWizardFolderPage(QWizardPage):
def __init__(self, theWizard):
- QWizardPage.__init__(self)
+ super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -209,7 +209,7 @@ class ProjWizardFolderPage(QWizardPage):
"""Check that the selected path isn't already being used.
"""
self.errLabel.setText("")
- if not QWizardPage.isComplete(self):
+ if not super().isComplete():
return False
setPath = os.path.abspath(os.path.expanduser(self.projPath.text()))
@@ -259,7 +259,7 @@ class ProjWizardFolderPage(QWizardPage):
class ProjWizardPopulatePage(QWizardPage):
def __init__(self, theWizard):
- QWizardPage.__init__(self)
+ super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -315,7 +315,7 @@ class ProjWizardPopulatePage(QWizardPage):
class ProjWizardCustomPage(QWizardPage):
def __init__(self, theWizard):
- QWizardPage.__init__(self)
+ super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -397,7 +397,7 @@ class ProjWizardCustomPage(QWizardPage):
class ProjWizardFinalPage(QWizardPage):
def __init__(self, theWizard):
- QWizardPage.__init__(self)
+ super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -418,7 +418,7 @@ class ProjWizardFinalPage(QWizardPage):
def initializePage(self):
"""Update the summary information on the final page.
"""
- QWizardPage.initializePage(self)
+ super().initializePage()
sumList = []
sumList.append(self.tr("Project Name: {0}").format(self.field("projName")))
diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py
index add052ec..7033aadc 100644
--- a/novelwriter/tools/writingstats.py
+++ b/novelwriter/tools/writingstats.py
@@ -58,7 +58,7 @@ class GuiWritingStats(QDialog):
FMT_CSV = 1
def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiWritingStats ...")
self.setObjectName("GuiWritingStats")
diff --git a/tests/mock.py b/tests/mock.py
index 23938d41..6174a3cb 100644
--- a/tests/mock.py
+++ b/tests/mock.py
@@ -24,7 +24,7 @@ along with this program. If not, see .
# Mock GUI
# =========================================================================== #
-class MockGuiMain():
+class MockGuiMain:
def __init__(self):
self.mainConf = None
@@ -81,7 +81,7 @@ class MockGuiMain():
# END Class MockGuiMain
-class MockStatusBar():
+class MockStatusBar:
def __init__(self):
return
From 84104f22677697e3af9181a8bdbe640752a57d4f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Oct 2022 23:18:37 +0200
Subject: [PATCH 28/92] Include missing file from last commit
---
novelwriter/error.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/novelwriter/error.py b/novelwriter/error.py
index 2ceae1e3..ec5d034c 100644
--- a/novelwriter/error.py
+++ b/novelwriter/error.py
@@ -61,7 +61,7 @@ def formatException(exc):
class NWErrorMessage(QDialog):
def __init__(self, parent):
- QDialog.__init__(self, parent=parent)
+ super().__init__(parent=parent)
self.setObjectName("NWErrorMessage")
# Widgets
From 9a0bda9884216d7ca13765333a1ed3ad6b163da7 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Oct 2022 23:27:45 +0200
Subject: [PATCH 29/92] Fix tab parents on settings dialogs
---
novelwriter/dialogs/preferences.py | 56 ++++++++++++++---------------
novelwriter/dialogs/projsettings.py | 34 +++++++++---------
2 files changed, 45 insertions(+), 45 deletions(-)
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index 964f92b0..00c1fe6a 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -55,13 +55,13 @@ class GuiPreferences(PagedDialog):
self.setWindowTitle(self.tr("Preferences"))
- self.tabGeneral = GuiPreferencesGeneral(self, self.mainGui)
- self.tabProjects = GuiPreferencesProjects(self, self.mainGui)
- self.tabDocs = GuiPreferencesDocuments(self, self.mainGui)
- self.tabEditor = GuiPreferencesEditor(self, self.mainGui)
- self.tabSyntax = GuiPreferencesSyntax(self, self.mainGui)
- self.tabAuto = GuiPreferencesAutomation(self, self.mainGui)
- self.tabQuote = GuiPreferencesQuotes(self, self.mainGui)
+ self.tabGeneral = GuiPreferencesGeneral(self)
+ self.tabProjects = GuiPreferencesProjects(self)
+ self.tabDocs = GuiPreferencesDocuments(self)
+ self.tabEditor = GuiPreferencesEditor(self)
+ self.tabSyntax = GuiPreferencesSyntax(self)
+ self.tabAuto = GuiPreferencesAutomation(self)
+ self.tabQuote = GuiPreferencesQuotes(self)
self.addTab(self.tabGeneral, self.tr("General"))
self.addTab(self.tabProjects, self.tr("Projects"))
@@ -138,12 +138,12 @@ class GuiPreferences(PagedDialog):
class GuiPreferencesGeneral(QWidget):
- def __init__(self, prefsGui, mainGui):
+ def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.mainTheme = mainGui.mainTheme
+ self.mainGui = prefsGui.mainGui
+ self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -344,12 +344,12 @@ class GuiPreferencesGeneral(QWidget):
class GuiPreferencesProjects(QWidget):
- def __init__(self, prefsGui, mainGui):
+ def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.mainTheme = mainGui.mainTheme
+ self.mainGui = prefsGui.mainGui
+ self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -505,12 +505,12 @@ class GuiPreferencesProjects(QWidget):
class GuiPreferencesDocuments(QWidget):
- def __init__(self, prefsGui, mainGui):
+ def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.mainTheme = mainGui.mainTheme
+ self.mainGui = prefsGui.mainGui
+ self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -666,12 +666,12 @@ class GuiPreferencesDocuments(QWidget):
class GuiPreferencesEditor(QWidget):
- def __init__(self, prefsGui, mainGui):
+ def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.mainTheme = mainGui.mainTheme
+ self.mainGui = prefsGui.mainGui
+ self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -840,12 +840,12 @@ class GuiPreferencesEditor(QWidget):
class GuiPreferencesSyntax(QWidget):
- def __init__(self, prefsGui, mainGui):
+ def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.mainTheme = mainGui.mainTheme
+ self.mainGui = prefsGui.mainGui
+ self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -943,12 +943,12 @@ class GuiPreferencesSyntax(QWidget):
class GuiPreferencesAutomation(QWidget):
- def __init__(self, prefsGui, mainGui):
+ def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.mainTheme = mainGui.mainTheme
+ self.mainGui = prefsGui.mainGui
+ self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -1100,12 +1100,12 @@ class GuiPreferencesAutomation(QWidget):
class GuiPreferencesQuotes(QWidget):
- def __init__(self, prefsGui, mainGui):
+ def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.mainTheme = mainGui.mainTheme
+ self.mainGui = prefsGui.mainGui
+ self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py
index d2484a20..87954392 100644
--- a/novelwriter/dialogs/projsettings.py
+++ b/novelwriter/dialogs/projsettings.py
@@ -67,10 +67,10 @@ class GuiProjectSettings(PagedDialog):
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
)
- self.tabMain = GuiProjectEditMain(self, self.mainGui, self.theProject)
- self.tabStatus = GuiProjectEditStatus(self, self.mainGui, self.theProject, True)
- self.tabImport = GuiProjectEditStatus(self, self.mainGui, self.theProject, False)
- self.tabReplace = GuiProjectEditReplace(self, self.mainGui, self.theProject)
+ self.tabMain = GuiProjectEditMain(self)
+ self.tabStatus = GuiProjectEditStatus(self, True)
+ self.tabImport = GuiProjectEditStatus(self, False)
+ self.tabReplace = GuiProjectEditReplace(self)
self.addTab(self.tabMain, self.tr("Settings"))
self.addTab(self.tabStatus, self.tr("Status"))
@@ -166,12 +166,12 @@ class GuiProjectSettings(PagedDialog):
class GuiProjectEditMain(QWidget):
- def __init__(self, projGui, mainGui, theProject):
+ def __init__(self, projGui):
super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.theProject = theProject
+ self.mainGui = projGui.mainGui
+ self.theProject = projGui.theProject
# The Form
self.mainForm = QConfigLayout()
@@ -256,13 +256,13 @@ class GuiProjectEditStatus(QWidget):
COL_ROLE = Qt.UserRole + 1
NUM_ROLE = Qt.UserRole + 2
- def __init__(self, prefsGui, mainGui, theProject, isStatus):
- super().__init__(parent=prefsGui)
+ def __init__(self, projGui, isStatus):
+ super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.theProject = theProject
- self.mainTheme = mainGui.mainTheme
+ self.mainGui = projGui.mainGui
+ self.theProject = projGui.theProject
+ self.mainTheme = projGui.mainGui.mainTheme
if isStatus:
self.theStatus = self.theProject.statusItems
@@ -527,13 +527,13 @@ class GuiProjectEditReplace(QWidget):
COL_KEY = 0
COL_REPL = 1
- def __init__(self, prefsGui, mainGui, theProject):
- super().__init__(parent=prefsGui)
+ def __init__(self, projGui):
+ super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
- self.mainTheme = mainGui.mainTheme
- self.theProject = theProject
+ self.mainGui = projGui.mainGui
+ self.mainTheme = projGui.mainGui.mainTheme
+ self.theProject = projGui.theProject
self.arChanged = False
wCol0 = self.mainConf.pxInt(
From 4f44374f489d4badeb33d5d710adacce1f1a574a Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Oct 2022 23:41:39 +0200
Subject: [PATCH 30/92] Remove menu entry for split document
---
novelwriter/gui/mainmenu.py | 5 -----
tests/test_gui/test_gui_guimain.py | 1 -
2 files changed, 6 deletions(-)
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index f4e6e668..79361feb 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -244,11 +244,6 @@ class GuiMainMenu(QMenuBar):
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
self.docuMenu.addAction(self.aImportFile)
- # Document > Split Document
- self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self)
- self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument())
- self.docuMenu.addAction(self.aSplitDoc)
-
return
def _buildEditMenu(self):
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 0a569e21..52936986 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -54,7 +54,6 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
assert nwGUI.saveDocument() is False
assert nwGUI.viewDocument(None) is False
assert nwGUI.importDocument() is False
- assert nwGUI.splitDocument() is False
assert nwGUI.openSelectedItem() is False
assert nwGUI.editItemLabel() is False
assert nwGUI.requestNovelTreeRefresh() is False
From baafd5f83c3ee84ddf6a351d95261b11ae48e933 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 12 Oct 2022 22:36:06 +0200
Subject: [PATCH 31/92] Rewrite the GuiDocSplit dialog
---
novelwriter/core/options.py | 2 +-
novelwriter/dialogs/docmerge.py | 2 +-
novelwriter/dialogs/docsplit.py | 249 ++++++++++++--------------------
novelwriter/gui/projtree.py | 27 +++-
novelwriter/guimain.py | 14 +-
5 files changed, 120 insertions(+), 174 deletions(-)
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index d36e677b..7aa2b48a 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -42,7 +42,7 @@ VALID_MAP = {
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
},
- "GuiDocSplit": {"spLevel"},
+ "GuiDocSplit": {"spLevel", "intoFolder", "docHierarchy"},
"GuiBuildNovel": {
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index 7113fc0f..3dcf6d34 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -75,7 +75,7 @@ class GuiDocMerge(QDialog):
# Merge Options
self.trashLabel = QLabel(self.tr("Move merged items to Trash"))
- self.trashSwitch = QSwitch()
+ self.trashSwitch = QSwitch(width=2*iPx, height=iPx)
self.optBox = QGridLayout()
self.optBox.addWidget(self.trashLabel, 0, 0)
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index f99166fb..0cd567c9 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -1,10 +1,11 @@
"""
-novelWriter – GUI Doc Split Tool
-================================
-GUI class for splitting a single document into multiple documents
+novelWriter – GUI Doc Split Dialog
+==================================
+Custom dialog class for splitting documents.
File History:
-Created: 2020-02-01 [0.4.3]
+Created: 2020-02-01 [0.4.3]
+Rewritten: 2022-10-12 [2.0b1]
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -29,19 +30,18 @@ import novelwriter
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
- QListWidgetItem, QDialogButtonBox, QLabel
+ QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout
)
from novelwriter.core import NWDoc
-from novelwriter.enum import nwAlert
-from novelwriter.gui.custom import QHelpLabel
+from novelwriter.gui.custom import QHelpLabel, QSwitch
logger = logging.getLogger(__name__)
class GuiDocSplit(QDialog):
- def __init__(self, mainGui):
+ def __init__(self, mainGui, sHandle):
super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocSplit ...")
@@ -49,12 +49,12 @@ class GuiDocSplit(QDialog):
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
+ self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
- self.sourceItem = None
- self.sourceText = []
+ self._data = {}
+ self._text = []
- self.outerBox = QVBoxLayout()
self.setWindowTitle(self.tr("Split Document"))
self.headLabel = QLabel("{0}".format(self.tr("Document Headers")))
@@ -63,6 +63,18 @@ class GuiDocSplit(QDialog):
self.mainGui.mainTheme.helpText
)
+ # Values
+ iPx = self.mainTheme.baseIconSize
+ hSp = self.mainConf.pxInt(12)
+ vSp = self.mainConf.pxInt(8)
+ bSp = self.mainConf.pxInt(12)
+
+ pOptions = self.theProject.options
+ spLevel = pOptions.getInt("GuiDocSplit", "spLevel", 3)
+ intoFolder = pOptions.getBool("GuiDocSplit", "intoFolder", True)
+ docHierarchy = pOptions.getBool("GuiDocSplit", "docHierarchy", True)
+
+ # Header Selection
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
@@ -73,194 +85,117 @@ class GuiDocSplit(QDialog):
self.splitLevel.addItem(self.tr("Split up to Header Level 2 (Chapter)"), 2)
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.theProject.options.getInt("GuiDocSplit", "spLevel", 3)
- )
+ spIndex = self.splitLevel.findData(spLevel)
if spIndex != -1:
self.splitLevel.setCurrentIndex(spIndex)
- self.splitLevel.currentIndexChanged.connect(self._populateList)
+ self.splitLevel.currentIndexChanged.connect(self._reloadList)
+ # Split Options
+ self.folderLabel = QLabel(self.tr("Split into a new folder"))
+ self.folderSwitch = QSwitch(width=2*iPx, height=iPx)
+ self.folderSwitch.setChecked(intoFolder)
+
+ self.hierarchyLabel = QLabel(self.tr("Create document hierarchy"))
+ self.hierarchySwitch = QSwitch(width=2*iPx, height=iPx)
+ self.hierarchySwitch.setChecked(docHierarchy)
+
+ self.optBox = QGridLayout()
+ self.optBox.addWidget(self.folderLabel, 0, 0)
+ self.optBox.addWidget(self.folderSwitch, 0, 1)
+ self.optBox.addWidget(self.hierarchyLabel, 1, 0)
+ self.optBox.addWidget(self.hierarchySwitch, 1, 1)
+ self.optBox.setVerticalSpacing(vSp)
+ self.optBox.setHorizontalSpacing(hSp)
+ self.optBox.setColumnStretch(2, 1)
+
+ # Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
- self.buttonBox.accepted.connect(self._doSplit)
- self.buttonBox.rejected.connect(self._doClose)
+ self.buttonBox.accepted.connect(self.accept)
+ self.buttonBox.rejected.connect(self.reject)
+ # Assemble
+ self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel)
- self.outerBox.addSpacing(self.mainConf.pxInt(8))
+ self.outerBox.addSpacing(vSp)
self.outerBox.addWidget(self.listBox)
self.outerBox.addWidget(self.splitLevel)
- self.outerBox.addSpacing(self.mainConf.pxInt(12))
+ self.outerBox.addSpacing(vSp)
+ self.outerBox.addLayout(self.optBox)
+ self.outerBox.addSpacing(bSp)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
- self.rejected.connect(self._doClose)
-
- self._populateList()
+ # Load Content
+ self._loadContent(sHandle)
logger.debug("GuiDocSplit initialisation complete")
return
- ##
- # Buttons
- ##
-
- def _doSplit(self):
- """Perform the split of the file, create a new folder in the
- same parent folder, and multiple files depending on split level
- settings. The old file is not removed in the split process, and
- must be deleted manually.
+ def getData(self):
+ """Return the user's choices. Also save the users options for
+ the next time the dialog is used.
"""
- logger.verbose("GuiDocSplit split button clicked")
-
- if self.sourceItem is None:
- 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.mainGui.makeAlert(self.tr(
- "Could not parse source document."
- ), nwAlert.ERROR)
- return False
-
- inDoc = NWDoc(self.theProject, self.sourceItem)
- theText = inDoc.readDocument()
-
- docErr = inDoc.getError()
- if theText is None and docErr:
- self.mainGui.makeAlert([
- self.tr("Failed to open document file."), docErr
- ], nwAlert.ERROR)
-
- if theText is None:
- theText = ""
-
- nLines = len(self.sourceText)
- logger.debug("Splitting document %s with %d lines", self.sourceItem, nLines)
-
- finalOrder = []
+ headerList = []
for i in range(self.listBox.count()):
- listItem = self.listBox.item(i)
- wTitle = listItem.text()
- lineNo = listItem.data(Qt.UserRole)
- finalOrder.append([wTitle, lineNo, nLines])
- if i > 0:
- finalOrder[i-1][2] = lineNo
+ item = self.listBox.item(i)
+ headerList.append((item.text(), item.data(Qt.UserRole)))
- nFiles = len(finalOrder)
- if nFiles == 0:
- self.mainGui.makeAlert(self.tr(
- "No headers found. Nothing to do."
- ), nwAlert.ERROR)
- return False
+ spLevel = self.splitLevel.currentData()
+ intoFolder = self.folderSwitch.isChecked()
+ docHierarchy = self.hierarchySwitch.isChecked()
- msgYes = self.mainGui.askQuestion(
- self.tr("Split Document"),
- "{0}
{1}".format(
- self.tr(
- "The document will be split into {0} file(s) in a new folder. "
- "The original document will remain intact."
- ).format(nFiles),
- self.tr(
- "Continue with the splitting process?"
- )
- )
- )
- if not msgYes:
- return False
+ self._data["spLevel"] = spLevel
+ self._data["headerList"] = headerList
+ self._data["intoFolder"] = intoFolder
+ self._data["docHierarchy"] = docHierarchy
- # Create the folder
- fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
- self.mainGui.projView.revealNewTreeItem(fHandle)
- logger.verbose("Creating folder '%s'", fHandle)
+ pOptions = self.theProject.options
+ pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
+ pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
+ pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
- # Loop through, and create the files
- for wTitle, iStart, iEnd in finalOrder:
+ return self._data
- wTitle = wTitle.lstrip("#").strip()
- nHandle = self.theProject.newFile(wTitle, fHandle)
- newItem = self.theProject.tree[nHandle]
- 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
- )
+ ##
+ # Slots
+ ##
- theText = "\n".join(self.sourceText[iStart:iEnd])
- theText = theText.rstrip("\n") + "\n\n"
-
- outDoc = NWDoc(self.theProject, nHandle)
- if not outDoc.writeDocument(theText):
- self.mainGui.makeAlert([
- self.tr("Could not save document."), outDoc.getError()
- ], nwAlert.ERROR)
- return False
-
- self.mainGui.projView.revealNewTreeItem(nHandle)
-
- self._doClose()
-
- return True
-
- def _doClose(self):
- """Close the dialog window without doing anything.
+ def _reloadList(self):
+ """Reload the content of the list box.
"""
- self.theProject.options.saveSettings()
- self.close()
+ sHandle = self._data.get("sHandle", None)
+ self._loadContent(sHandle)
return
##
# Internal Functions
##
- def _populateList(self):
- """Get the item selected in the tree, check that it is a folder,
- and try to find all files associated with it. The valid files
- are then added to the list view in order. The list itself can be
- reordered by the user.
+ def _loadContent(self, sHandle):
+ """Load content from a given source item.
"""
- self.listBox.clear()
- if self.sourceItem is None:
- self.sourceItem = self.mainGui.projView.getSelectedHandle()
+ self._data = {}
+ self._data["sHandle"] = sHandle
- if self.sourceItem is None:
- return False
-
- nwItem = self.theProject.tree[self.sourceItem]
- if nwItem is None:
- return False
-
- if not nwItem.isFileType():
- self.mainGui.makeAlert(self.tr(
- "Element selected in the project tree must be a file."
- ), nwAlert.ERROR)
- return False
-
- inDoc = NWDoc(self.theProject, self.sourceItem)
- theText = inDoc.readDocument()
- if theText is None:
- theText = ""
+ nwItem = self.theProject.tree[sHandle]
+ if nwItem is None or not nwItem.isFileType():
return False
spLevel = self.splitLevel.currentData()
- self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel)
- logger.debug(
- "Scanning document '%s' for headings level <= %d",
- self.sourceItem, spLevel
- )
+ if not self._text:
+ inDoc = NWDoc(self.theProject, sHandle)
+ self._text = (inDoc.readDocument() or "").splitlines()
- self.sourceText = theText.splitlines()
- for lineNo, aLine in enumerate(self.sourceText):
+ self.listBox.clear()
+ for lineNo, aLine in enumerate(self._text):
onLine = -1
- if aLine.startswith("# ") and spLevel >= 1:
+ if aLine.startswith(("# ", "#! ")) and spLevel >= 1:
onLine = lineNo
- elif aLine.startswith("## ") and spLevel >= 2:
+ elif aLine.startswith(("## ", "##! ")) and spLevel >= 2:
onLine = lineNo
elif aLine.startswith("### ") and spLevel >= 3:
onLine = lineNo
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 675cf65c..540b3712 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -41,7 +41,7 @@ from PyQt5.QtWidgets import (
from novelwriter.core import DocMerger
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
-from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
+from novelwriter.dialogs import GuiDocMerge, GuiDocSplit, GuiEditLabel
from novelwriter.constants import nwHeaders, trConst, nwLabels
logger = logging.getLogger(__name__)
@@ -1458,7 +1458,30 @@ class GuiProjectTree(QTreeWidget):
return True
def _splitDocument(self, tHandle):
- return
+ """Split a document into multiple documents.
+ """
+ logger.info("Request to split items with handle '%s'", tHandle)
+
+ tItem = self.theProject.tree[tHandle]
+ if tItem is None:
+ return False
+
+ if not tItem.isFileType():
+ logger.error("Only documents can be split")
+ return False
+
+ dlgSplit = GuiDocSplit(self.mainGui, tHandle)
+ dlgSplit.exec_()
+
+ if dlgSplit.result() == QDialog.Accepted:
+
+ print(dlgSplit.getData())
+
+ else:
+ logger.info("Action cancelled by user")
+ return False
+
+ return True
def _scanChildren(self, theList, tItem, tIndex):
"""This is a recursive function returning all items in a tree
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 79f56b97..67071bf5 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -44,7 +44,7 @@ from novelwriter.gui import (
GuiViewsBar
)
from novelwriter.dialogs import (
- GuiAbout, GuiDocSplit, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
+ GuiAbout, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
GuiProjectSettings, GuiUpdates, GuiWordList
)
from novelwriter.tools import (
@@ -754,18 +754,6 @@ class GuiMain(QMainWindow):
return True
- def splitDocument(self):
- """Split a single document into multiple documents.
- """
- if not self.hasProject:
- logger.error("No project open")
- return False
-
- dlgSplit = GuiDocSplit(self)
- dlgSplit.exec_()
-
- return True
-
def passDocumentAction(self, theAction):
"""Pass on document action to the document viewer if it has
focus, or pass it to the document editor if it or any of
From 31a5761ba047538f57ac290207055c08c478c0c4 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 12 Oct 2022 23:39:56 +0200
Subject: [PATCH 32/92] Add the core code for the DocSplitter tool
---
novelwriter/core/__init__.py | 3 +-
novelwriter/core/doctools.py | 90 ++++++++++++++++++++++++++++++++-
novelwriter/dialogs/docsplit.py | 37 +++++++++++---
novelwriter/gui/projtree.py | 14 ++++-
4 files changed, 135 insertions(+), 9 deletions(-)
diff --git a/novelwriter/core/__init__.py b/novelwriter/core/__init__.py
index 90b103f8..f1b5dfd3 100644
--- a/novelwriter/core/__init__.py
+++ b/novelwriter/core/__init__.py
@@ -19,7 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-from novelwriter.core.doctools import DocMerger
+from novelwriter.core.doctools import DocMerger, DocSplitter
from novelwriter.core.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
@@ -30,6 +30,7 @@ from novelwriter.core.tomd import ToMarkdown
__all__ = [
"DocMerger",
+ "DocSplitter",
"countWords",
"NWDoc",
"NWProject",
diff --git a/novelwriter/core/doctools.py b/novelwriter/core/doctools.py
index f6716590..f4fc9cb5 100644
--- a/novelwriter/core/doctools.py
+++ b/novelwriter/core/doctools.py
@@ -4,7 +4,8 @@ novelWriter – Project Document Tools
A collection of tools to create and manipulate documents
File History:
-Created: 2022-10-02 [2.0b1]
+Created: 2022-10-02 [2.0b1] DocMerger
+Created: 2022-10-11 [2.0b1] DocSplitter
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -117,3 +118,90 @@ class DocMerger:
return status
# END Class DocMerger
+
+
+class DocSplitter:
+
+ def __init__(self, theProject, sHandle):
+
+ self.theProject = theProject
+
+ self._error = ""
+ self._parHandle = None
+ self._srcHandle = None
+ self._srcItem = None
+
+ self._rawData = []
+
+ srcItem = self.theProject.tree[sHandle]
+ if srcItem is not None and srcItem.isFileType():
+ self._srcHandle = sHandle
+ self._srcItem = srcItem
+
+ return
+
+ ##
+ # Methods
+ ##
+
+ def getError(self):
+ """Return any collected errors.
+ """
+ return self._error
+
+ def setParentItem(self, pHandle):
+ """Set the item that will be the top level parent item for the
+ new documents.
+ """
+ self._parHandle = pHandle
+ return
+
+ def newParentFolder(self, pHandle, folderLabel):
+ """Create a new folder that will be the top level parent item
+ for the new documents.
+ """
+ if self._srcItem is None:
+ return None
+
+ newHandle = self.theProject.newFolder(folderLabel, pHandle)
+ newItem = self.theProject.tree[self._parHandle]
+ newItem.setStatus(self._srcItem.itemStatus)
+ newItem.setImport(self._srcItem.itemImport)
+
+ self._parHandle = newHandle
+
+ return newHandle
+
+ def splitDocument(self, splitData, splitText):
+ """Loop through the split data record and perform the split job.
+ """
+ self._rawData = []
+ buffer = splitText.copy()
+ for lineNo, hLevel, hLabel in reversed(splitData):
+ chunk = buffer[lineNo:]
+ buffer = buffer[:lineNo]
+ self._rawData.insert(0, (chunk, hLevel, hLabel))
+
+ return True
+
+ def writeDocuments(self):
+ """An iterator that will write each document in the buffer, and
+ return its new handle, parent handle, and sibling handle.
+ """
+ nearHandle = self._parHandle
+ for docText, hLevel, docLabel in self._rawData:
+
+ newHandle = self.theProject.newFile(docLabel, self._parHandle)
+
+ outDoc = NWDoc(self.theProject, newHandle)
+ status = outDoc.writeDocument("\n".join(docText))
+ if not status:
+ self._error = outDoc.getError()
+
+ yield newHandle, self._parHandle, nearHandle
+
+ nearHandle = newHandle
+
+ return
+
+# END Class DocSplitter
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index 0cd567c9..7ff206ee 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -41,6 +41,10 @@ logger = logging.getLogger(__name__)
class GuiDocSplit(QDialog):
+ LINE_ROLE = Qt.UserRole
+ LEVEL_ROLE = Qt.UserRole + 1
+ LABEL_ROLE = Qt.UserRole + 2
+
def __init__(self, mainGui, sHandle):
super().__init__(parent=mainGui)
@@ -141,7 +145,9 @@ class GuiDocSplit(QDialog):
headerList = []
for i in range(self.listBox.count()):
item = self.listBox.item(i)
- headerList.append((item.text(), item.data(Qt.UserRole)))
+ headerList.append(
+ (item.data(self.LINE_ROLE), item.data(self.LEVEL_ROLE), item.data(self.LABEL_ROLE))
+ )
spLevel = self.splitLevel.currentData()
intoFolder = self.folderSwitch.isChecked()
@@ -157,7 +163,7 @@ class GuiDocSplit(QDialog):
pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
- return self._data
+ return self._data, self._text
##
# Slots
@@ -193,19 +199,38 @@ class GuiDocSplit(QDialog):
for lineNo, aLine in enumerate(self._text):
onLine = -1
- if aLine.startswith(("# ", "#! ")) and spLevel >= 1:
+ hLevel = 0
+ if aLine.startswith("# ") and spLevel >= 1:
onLine = lineNo
- elif aLine.startswith(("## ", "##! ")) and spLevel >= 2:
+ hLevel = 1
+ hLabel = aLine[2:].strip()
+ elif aLine.startswith("## ") and spLevel >= 2:
onLine = lineNo
+ hLevel = 2
+ hLabel = aLine[3:].strip()
elif aLine.startswith("### ") and spLevel >= 3:
onLine = lineNo
+ hLevel = 3
+ hLabel = aLine[4:].strip()
elif aLine.startswith("#### ") and spLevel >= 4:
onLine = lineNo
+ hLevel = 4
+ hLabel = aLine[5:].strip()
+ elif aLine.startswith("#! ") and spLevel >= 1:
+ onLine = lineNo
+ hLevel = 1
+ hLabel = aLine[3:].strip()
+ elif aLine.startswith("##! ") and spLevel >= 2:
+ onLine = lineNo
+ hLevel = 2
+ hLabel = aLine[4:].strip()
- if onLine >= 0:
+ if onLine >= 0 and hLevel > 0:
newItem = QListWidgetItem()
newItem.setText(aLine.strip())
- newItem.setData(Qt.UserRole, onLine)
+ newItem.setData(self.LINE_ROLE, onLine)
+ newItem.setData(self.LEVEL_ROLE, hLevel)
+ newItem.setData(self.LABEL_ROLE, hLabel)
self.listBox.addItem(newItem)
return True
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 540b3712..923410eb 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -40,6 +40,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.core import DocMerger
+from novelwriter.core.doctools import DocSplitter
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.dialogs import GuiDocMerge, GuiDocSplit, GuiEditLabel
from novelwriter.constants import nwHeaders, trConst, nwLabels
@@ -1475,7 +1476,18 @@ class GuiProjectTree(QTreeWidget):
if dlgSplit.result() == QDialog.Accepted:
- print(dlgSplit.getData())
+ splitData, splitText = dlgSplit.getData()
+ print(splitData)
+
+ headerList = splitData.get("headerList", [])
+
+ docSplit = DocSplitter(self.theProject, tHandle)
+ docSplit.setParentItem(tItem.itemParent)
+ docSplit.splitDocument(headerList, splitText)
+
+ for dHandle, _, nHandle in docSplit.writeDocuments():
+ self.mainGui.projView.revealNewTreeItem(dHandle, nHandle)
+ self._alertTreeChange(dHandle, flush=False)
else:
logger.info("Action cancelled by user")
From 1421ed3bf42238159afde46b177242eee97f2d85 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 12 Oct 2022 23:44:15 +0200
Subject: [PATCH 33/92] Turn off the old splitter tool test
---
novelwriter/core/doctools.py | 2 +-
novelwriter/gui/projtree.py | 2 +-
tests/test_dialogs/test_dlg_docsplit.py | 1 +
3 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/novelwriter/core/doctools.py b/novelwriter/core/doctools.py
index f4fc9cb5..a51f5901 100644
--- a/novelwriter/core/doctools.py
+++ b/novelwriter/core/doctools.py
@@ -198,7 +198,7 @@ class DocSplitter:
if not status:
self._error = outDoc.getError()
- yield newHandle, self._parHandle, nearHandle
+ yield status, newHandle, nearHandle
nearHandle = newHandle
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 923410eb..9ae801bd 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -1485,7 +1485,7 @@ class GuiProjectTree(QTreeWidget):
docSplit.setParentItem(tItem.itemParent)
docSplit.splitDocument(headerList, splitText)
- for dHandle, _, nHandle in docSplit.writeDocuments():
+ for writeOk, dHandle, nHandle in docSplit.writeDocuments():
self.mainGui.projView.revealNewTreeItem(dHandle, nHandle)
self._alertTreeChange(dHandle, flush=False)
diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py
index d0201d6a..818c3d9e 100644
--- a/tests/test_dialogs/test_dlg_docsplit.py
+++ b/tests/test_dialogs/test_dlg_docsplit.py
@@ -34,6 +34,7 @@ from novelwriter.core.document import NWDoc
@pytest.mark.gui
+@pytest.mark.skip
def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
"""Test the split document tool.
"""
From bb8b0555aeb1cb9a92164f8d3e868b3ba88376e7 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Oct 2022 10:22:31 +0200
Subject: [PATCH 34/92] Backport fix for #1149
---
novelwriter/guimain.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index f0aa7899..9399c9a7 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -383,16 +383,24 @@ class GuiMain(QMainWindow):
logger.info("Creating new project")
if self.theProject.newProject(projData):
+
self.hasProject = True
+ self.idleRefTime = time()
+ self.idleTime = 0.0
+
self.rebuildTrees()
self.saveProject()
+
self.docEditor.setDictionaries()
self.rebuildIndex(beQuiet=True)
+
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
self.statusBar.setDocumentStatus(nwState.NONE)
self.statusBar.setStatus(self.tr("New project created ..."))
+
self._updateWindowTitle(self.theProject.projName)
+
else:
self.theProject.clearProject()
return False
From 038c66258b05f831478e995f0cbc13ca2f6cf653 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Oct 2022 10:24:00 +0200
Subject: [PATCH 35/92] Backport fix for #1150
---
novelwriter/gui/doceditor.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index f2412242..83d971d5 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -1982,6 +1982,11 @@ class GuiDocEditor(QTextEdit):
nDelete = 3
tInsert = nwUnicode.U_HELLIP
+ elif theOne == nwUnicode.U_LSEP:
+ # This resolves issue #1150
+ nDelete = 1
+ tInsert = nwUnicode.U_PSEP
+
tCheck = tInsert
if tCheck in self.mainConf.fmtPadBefore:
if self._allowSpaceBeforeColon(theText, tCheck):
From 1001140eec69905420a7b9f80d2891d73b45ec97 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Oct 2022 10:25:38 +0200
Subject: [PATCH 36/92] Fix preferences dialog window size issue #989
---
novelwriter/dialogs/preferences.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index 81bef52a..3b7f688c 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -128,9 +128,7 @@ class GuiPreferences(PagedDialog):
def _saveWindowSize(self):
"""Save the dialog window size.
"""
- winWidth = self.mainConf.rpxInt(self.width())
- winHeight = self.mainConf.rpxInt(self.height())
- self.mainConf.setPreferencesSize(winWidth, winHeight)
+ self.mainConf.setPreferencesSize(self.width(), self.height())
return
# END Class GuiPreferences
From dbf672224f1ac5d91d720957897fd3c1e4c6e8c8 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Oct 2022 10:44:08 +0200
Subject: [PATCH 37/92] Bump version to 1.6.5 and update changelog
---
CHANGELOG.md | 33 +++++++++++++++++++++++
novelwriter/__init__.py | 6 ++---
novelwriter/assets/text/release_notes.htm | 10 +++++++
sample/nwProject.nwx | 8 +++---
4 files changed, 50 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c3bc08f..300f82fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,38 @@
# novelWriter Changelog
+## Version 1.6.5 [2022-10-13]
+
+### Release Notes
+
+This is a bugfix release that fixes a a few minor issues. The idle time for new projects would be
+artificially inflated as the clock was not reset when the project was first created. This only
+affects the first entry in the writing statistics. A scaling issue for the Preferences dialog has
+also been fixed. It only affected screens with UI scaling enabled. Lastly, typing `Shift+Enter` in
+the text editor now creates a regular line break instead of a special line separator. The line
+separator serves no purpose in plain text, and was producing inconsistencies in how text is
+processed and displayed.
+
+### Detailed Changelog
+
+**Bugfixes**
+
+* Fixed a bug where the idle time was not properly zeroed when a new project was generated after
+ the wizard was closed. The idle time would be calculated from the time the previous project
+ closed, thus inflating the value. Issue #1149. PR #1159.
+* Fixes an issue where the window size of the Preferences dialog would have the GUI scaling factor
+ applied twice when the dialog was closed, resulting in the dialog growing in size each time it is
+ opened. Issue #989. PR #1159.
+
+**Other Changes**
+
+* The text editor no longer creates a Unicode line separator (U+2028) when the user presses
+ `Shift+Enter`. The line separator serves no purpose in a plain text editor, and the code in
+ general treats them as regular line break. This caused the line separator to display differently
+ before and after saving. The line separator character is now automatically replaced by a
+ paragraph separator. Issue #1150. PR #1159.
+
+----
+
## Version 1.6.4 [2022-09-29]
### Release Notes
diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py
index 2ee812f4..2b40a55f 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.6.4"
-__hexversion__ = "0x010604f0"
-__date__ = "2022-09-29"
+__version__ = "1.6.5"
+__hexversion__ = "0x010605f0"
+__date__ = "2022-10-13"
__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 1cc14d48..34c423a8 100644
--- a/novelwriter/assets/text/release_notes.htm
+++ b/novelwriter/assets/text/release_notes.htm
@@ -70,5 +70,15 @@ disappears. It was not necessarily obvious how the viewer panel could be restore
basically no longer worked in the 1.6.3 release. This release also fixes a minor issue where the
text cursor sometimes disappears when reaching the right-hand edge of the text editor window.
+Patch 1.6.5 – 13 October 2022
+
+This is a bugfix release that fixes a a few minor issues. The idle time for new projects would
+be artificially inflated as the clock was not reset when the project was first created. This only
+affects the first entry in the writing statistics. A scaling issue for the Preferences dialog has
+also been fixed. It only affected screens with UI scaling enabled. Lastly, typing Shift+Enter in
+the text editor now creates a regular line break instead of a special line separator. The line
+separator serves no purpose in plain text, and was producing inconsistencies in how text is
+processed and displayed.
+