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 1/8] 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 2/8] 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 3/8] 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 4/8] 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 02976c5f12a24a1dc68740e0965099636ab546f1 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Oct 2022 17:11:43 +0200
Subject: [PATCH 5/8] Add remaining logic to document splitter, and fix bug in
recursive permanet delete
---
novelwriter/core/doctools.py | 44 +++++++++++++++++++++++++-----
novelwriter/gui/projtree.py | 53 ++++++++++++++++++++++--------------
2 files changed, 70 insertions(+), 27 deletions(-)
diff --git a/novelwriter/core/doctools.py b/novelwriter/core/doctools.py
index a51f5901..58394197 100644
--- a/novelwriter/core/doctools.py
+++ b/novelwriter/core/doctools.py
@@ -26,6 +26,7 @@ along with this program. If not, see .
import logging
+from novelwriter.common import minmax
from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
@@ -131,6 +132,7 @@ class DocSplitter:
self._srcHandle = None
self._srcItem = None
+ self._inFolder = False
self._rawData = []
srcItem = self.theProject.tree[sHandle]
@@ -154,6 +156,7 @@ class DocSplitter:
new documents.
"""
self._parHandle = pHandle
+ self._inFolder = False
return
def newParentFolder(self, pHandle, folderLabel):
@@ -164,11 +167,12 @@ class DocSplitter:
return None
newHandle = self.theProject.newFolder(folderLabel, pHandle)
- newItem = self.theProject.tree[self._parHandle]
+ newItem = self.theProject.tree[newHandle]
newItem.setStatus(self._srcItem.itemStatus)
newItem.setImport(self._srcItem.itemImport)
self._parHandle = newHandle
+ self._inFolder = True
return newHandle
@@ -184,23 +188,49 @@ class DocSplitter:
return True
- def writeDocuments(self):
+ def writeDocuments(self, docHierarchy):
"""An iterator that will write each document in the buffer, and
return its new handle, parent handle, and sibling handle.
"""
- nearHandle = self._parHandle
+ pHandle = self._parHandle
+ nHandle = self._parHandle if self._inFolder else self._srcHandle
+ hHandle = [self._parHandle, None, None, None, None]
+
+ pLevel = 0
for docText, hLevel, docLabel in self._rawData:
- newHandle = self.theProject.newFile(docLabel, self._parHandle)
+ hLevel = minmax(hLevel, 1, 4)
+ if pLevel == 0:
+ pLevel = hLevel
- outDoc = NWDoc(self.theProject, newHandle)
+ if docHierarchy:
+ if hLevel == 1:
+ pHandle = self._parHandle
+ elif hLevel == 2:
+ pHandle = hHandle[1] or hHandle[0]
+ elif hLevel == 3:
+ pHandle = hHandle[2] or hHandle[1] or hHandle[0]
+ elif hLevel == 4:
+ pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
+
+ if hLevel < pLevel:
+ nHandle = hHandle[hLevel] or hHandle[0]
+ elif hLevel > pLevel:
+ nHandle = pHandle
+
+ dHandle = self.theProject.newFile(docLabel, pHandle)
+ hHandle[hLevel] = dHandle
+
+ outDoc = NWDoc(self.theProject, dHandle)
status = outDoc.writeDocument("\n".join(docText))
if not status:
self._error = outDoc.getError()
- yield status, newHandle, nearHandle
+ yield status, dHandle, nHandle
- nearHandle = newHandle
+ hHandle[hLevel] = dHandle
+ nHandle = dHandle
+ pLevel = hLevel
return
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 9ae801bd..78609b45 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -517,18 +517,14 @@ class GuiProjectTree(QTreeWidget):
# Handle new file creation
if itemType == nwItemType.FILE and hLevel > 0:
- if self.theProject.writeNewFile(tHandle, hLevel, not isNote):
- # If successful, update word count
- wC = self.theProject.index.getCounts(tHandle)[1]
- self.propagateCount(tHandle, wC)
- self.projView.wordCountsChanged.emit()
+ self.theProject.writeNewFile(tHandle, hLevel, not isNote)
# Add the new item to the project tree
- self.revealNewTreeItem(tHandle, nHandle)
+ self.revealNewTreeItem(tHandle, nHandle=nHandle, wordCount=True)
return True
- def revealNewTreeItem(self, tHandle, nHandle=None):
+ def revealNewTreeItem(self, tHandle, nHandle=None, wordCount=False):
"""Reveal a newly added project item in the project tree.
"""
nwItem = self.theProject.tree[tHandle]
@@ -539,6 +535,11 @@ class GuiProjectTree(QTreeWidget):
if trItem is None:
return False
+ if nwItem.isFileType() and wordCount:
+ wC = self.theProject.index.getCounts(tHandle)[1]
+ self.propagateCount(tHandle, wC)
+ self.projView.wordCountsChanged.emit()
+
pHandle = nwItem.itemParent
if pHandle is not None and pHandle in self._treeMap:
self._treeMap[pHandle].setExpanded(True)
@@ -802,17 +803,16 @@ class GuiProjectTree(QTreeWidget):
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)
- for dHandle in reversed(itemList):
+ for dHandle in reversed(self.getTreeFromHandle(tHandle)):
if self.mainGui.docEditor.docHandle() == dHandle:
self.mainGui.closeDocument()
- self.theProject.removeItem(tHandle)
- self._treeMap.pop(tHandle, None)
+ self.theProject.removeItem(dHandle)
+ self._treeMap.pop(dHandle, None)
self._alertTreeChange(tHandle, flush=flush)
self.projView.wordCountsChanged.emit()
@@ -1433,14 +1433,14 @@ class GuiProjectTree(QTreeWidget):
if not docMerger.writeTargetDoc():
self.mainGui.makeAlert([
- self.tr("Could not save document."), docMerger.getError()
+ self.tr("Could not write document content."), docMerger.getError()
], nwAlert.ERROR)
return False
- if newFile:
- self.mainGui.projView.revealNewTreeItem(mHandle, tHandle)
-
self.theProject.index.reIndexHandle(mHandle)
+ if newFile:
+ self.revealNewTreeItem(mHandle, nHandle=tHandle, wordCount=True)
+
self.mainGui.openDocument(mHandle, doScroll=True)
if mrgData.get("moveToTrash", False):
@@ -1477,17 +1477,30 @@ class GuiProjectTree(QTreeWidget):
if dlgSplit.result() == QDialog.Accepted:
splitData, splitText = dlgSplit.getData()
- print(splitData)
headerList = splitData.get("headerList", [])
+ intoFolder = splitData.get("intoFolder", False)
+ docHierarchy = splitData.get("docHierarchy", False)
docSplit = DocSplitter(self.theProject, tHandle)
- docSplit.setParentItem(tItem.itemParent)
- docSplit.splitDocument(headerList, splitText)
+ if intoFolder:
+ fHandle = docSplit.newParentFolder(tItem.itemParent, tItem.itemName)
+ self.revealNewTreeItem(fHandle, nHandle=tHandle)
+ self._alertTreeChange(fHandle, flush=False)
+ else:
+ docSplit.setParentItem(tItem.itemParent)
- for writeOk, dHandle, nHandle in docSplit.writeDocuments():
- self.mainGui.projView.revealNewTreeItem(dHandle, nHandle)
+ docSplit.splitDocument(headerList, splitText)
+ for writeOk, dHandle, nHandle in docSplit.writeDocuments(docHierarchy):
+ self.theProject.index.reIndexHandle(dHandle)
+ self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True)
self._alertTreeChange(dHandle, flush=False)
+ if not writeOk:
+ self.mainGui.makeAlert([
+ self.tr("Could not write document content."), docSplit.getError()
+ ], nwAlert.ERROR)
+
+ self.saveTreeOrder()
else:
logger.info("Action cancelled by user")
From 3f4d85dcad50401788ba904eeaceda9fa1bd3918 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Oct 2022 18:18:08 +0200
Subject: [PATCH 6/8] Rewrite test for split dialog
---
novelwriter/dialogs/docsplit.py | 7 +-
tests/test_dialogs/test_dlg_docsplit.py | 254 ++++++------------------
2 files changed, 60 insertions(+), 201 deletions(-)
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index 7ff206ee..f744f32e 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -186,16 +186,17 @@ class GuiDocSplit(QDialog):
self._data = {}
self._data["sHandle"] = sHandle
+ self.listBox.clear()
+
nwItem = self.theProject.tree[sHandle]
if nwItem is None or not nwItem.isFileType():
- return False
+ return
spLevel = self.splitLevel.currentData()
if not self._text:
inDoc = NWDoc(self.theProject, sHandle)
self._text = (inDoc.readDocument() or "").splitlines()
- self.listBox.clear()
for lineNo, aLine in enumerate(self._text):
onLine = -1
@@ -233,6 +234,6 @@ class GuiDocSplit(QDialog):
newItem.setData(self.LABEL_ROLE, hLabel)
self.listBox.addItem(newItem)
- return True
+ return
# END Class GuiDocSplit
diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py
index 818c3d9e..b95d497c 100644
--- a/tests/test_dialogs/test_dlg_docsplit.py
+++ b/tests/test_dialogs/test_dlg_docsplit.py
@@ -19,22 +19,16 @@ 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 C, buildTestProject
-from PyQt5.QtWidgets import QAction, QMessageBox
+from PyQt5.QtWidgets import QMessageBox
-from novelwriter.enum import nwItemType, nwWidget
from novelwriter.dialogs import GuiDocSplit, GuiEditLabel
-from novelwriter.core.tree import NWTree
-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.
"""
@@ -46,207 +40,71 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
# Create a new project
buildTestProject(nwGUI, fncProj)
- # Handles for new objects
- hNovelRoot = "0000000000008"
- hChapterDir = "000000000000d"
- hToSplit = "0000000000010"
- hNewFolder = "0000000000021"
- hPartition = "0000000000022"
- hChapterOne = "0000000000023"
- hSceneOne = "0000000000024"
- hSceneTwo = "0000000000025"
- hSceneThree = "0000000000026"
- hSceneFour = "0000000000027"
- hSceneFive = "0000000000028"
+ theProject = nwGUI.theProject
+ projTree = nwGUI.projView.projTree
- # Add Project Content
- nwGUI.switchFocus(nwWidget.TREE)
- nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem(hNovelRoot).setSelected(True)
- nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
-
- assert nwGUI.saveProject() is True
- assert nwGUI.closeProject() is True
-
- tPartition = "# Nantucket"
- tChapterOne = "## Chapter One\n\n% Chapter one comment"
- 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."
- tSceneFive = "#### The End\n\nend"
-
- tToSplit = (
- f"{tPartition}\n\n{tChapterOne}\n\n"
- f"{tSceneOne}\n\n{tSceneTwo}\n\n"
- f"{tSceneThree}\n\n{tSceneFour}\n\n"
- f"{tSceneFive}\n\n"
+ docText = (
+ "Text\n\n"
+ "##! Prologue\n\n"
+ "Text\n\n"
+ "## Chapter One\n\n"
+ "### Scene One\n\n"
+ "Text\n\n"
+ "### Scene Two\n\n"
+ "Text\n\n"
+ "## Chapter Two\n\n"
+ "### Scene Three\n\n"
+ "Text\n\n"
+ "### Scene Four\n\n"
+ "Text\n\n"
+ "#! New Title\n\n"
+ "## New Chapter\n\n"
+ "### New Scene\n\n"
+ "#### New Section\n\n"
)
- contentDir = os.path.join(fncProj, "content")
- writeFile(os.path.join(contentDir, hToSplit+".nwd"), tToSplit)
+ hSplitDoc = theProject.newFile("Split Doc", C.hNovelRoot)
+ theProject.writeNewFile(hSplitDoc, 1, True, docText)
+ projTree.revealNewTreeItem(hSplitDoc, nHandle=C.hNovelRoot, wordCount=True)
- assert nwGUI.openProject(fncProj) is True
+ docText = f"# Split Doc\n\n{docText}"
- # Open the Split tool
- nwGUI.switchFocus(nwWidget.TREE)
- nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True)
-
- monkeypatch.setattr(GuiDocSplit, "exec_", lambda *a: None)
- nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger)
- qtbot.waitUntil(lambda: getGuiItem("GuiDocSplit") is not None, timeout=1000)
-
- nwSplit = getGuiItem("GuiDocSplit")
- assert isinstance(nwSplit, GuiDocSplit)
+ nwSplit = GuiDocSplit(nwGUI, hSplitDoc)
nwSplit.show()
- qtbot.wait(50)
+ qtbot.addWidget(nwSplit)
- # Populate List
- # =============
+ # By default, only up to level three headinsg should be listed
+ assert nwSplit.splitLevel.currentData() == 3
+ assert nwSplit.listBox.count() == 11
- nwSplit.listBox.clear()
- assert nwSplit.listBox.count() == 0
-
- # No item selected
- nwSplit.sourceItem = None
- nwGUI.projView.projTree.clearSelection()
- assert nwSplit._populateList() is False
- assert nwSplit.listBox.count() == 0
-
- # Non-existing item
- with monkeypatch.context() as mp:
- mp.setattr(NWTree, "__getitem__", lambda *a: None)
- nwSplit.sourceItem = None
- nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True)
- assert nwSplit._populateList() is False
- assert nwSplit.listBox.count() == 0
-
- # Select a non-file
- nwSplit.sourceItem = None
- nwGUI.projView.projTree.clearSelection()
- nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
- assert nwSplit._populateList() is False
- assert nwSplit.listBox.count() == 0
-
- # Error when reading documents
- with monkeypatch.context() as mp:
- mp.setattr(NWDoc, "readDocument", lambda *a: None)
- nwSplit.sourceItem = hToSplit
- assert nwSplit._populateList() is False
- assert nwSplit.listBox.count() == 0
-
- # Read properly, and check split levels
-
- # Level 1
- nwSplit.splitLevel.setCurrentIndex(0)
- nwSplit.sourceItem = hToSplit
- assert nwSplit._populateList() is True
- assert nwSplit.listBox.count() == 1
-
- # Level 2
- nwSplit.splitLevel.setCurrentIndex(1)
- nwSplit.sourceItem = hToSplit
- assert nwSplit._populateList() is True
- assert nwSplit.listBox.count() == 2
-
- # Level 3
- nwSplit.splitLevel.setCurrentIndex(2)
- nwSplit.sourceItem = hToSplit
- assert nwSplit._populateList() is True
- assert nwSplit.listBox.count() == 6
-
- # Level 4
+ # Changing to level 4, should reload and add the last section
nwSplit.splitLevel.setCurrentIndex(3)
- nwSplit.sourceItem = hToSplit
- assert nwSplit._populateList() is True
- assert nwSplit.listBox.count() == 7
+ assert nwSplit.listBox.count() == 12
- # Split Document
- # ==============
+ data, text = nwSplit.getData()
+ assert text == docText.splitlines()
+ assert data["sHandle"] == hSplitDoc
+ assert data["spLevel"] == 4
+ assert data["intoFolder"] is True
+ assert data["docHierarchy"] is True
+ assert data["headerList"][0] == (0, 1, "Split Doc")
+ assert data["headerList"][1] == (4, 2, "Prologue")
+ assert data["headerList"][2] == (8, 2, "Chapter One")
+ assert data["headerList"][3] == (10, 3, "Scene One")
+ assert data["headerList"][4] == (14, 3, "Scene Two")
+ assert data["headerList"][5] == (18, 2, "Chapter Two")
+ assert data["headerList"][6] == (20, 3, "Scene Three")
+ assert data["headerList"][7] == (24, 3, "Scene Four")
+ assert data["headerList"][8] == (28, 1, "New Title")
+ assert data["headerList"][9] == (30, 2, "New Chapter")
+ assert data["headerList"][10] == (32, 3, "New Scene")
+ assert data["headerList"][11] == (34, 4, "New Section")
- # Test a proper split first
- with monkeypatch.context() as mp:
- mp.setattr(GuiDocSplit, "_doClose", lambda *a: None)
- assert nwSplit._doSplit() is True
- assert nwGUI.saveProject()
+ # Loading the dialog on a non-file item produces an empty list
+ nwSplit._loadContent(C.hNovelRoot)
+ assert nwSplit.listBox.count() == 0
- assert readFile(os.path.join(contentDir, hPartition+".nwd")) == (
- "%%%%~name: Nantucket\n"
- "%%%%~path: %s/%s\n"
- "%%%%~kind: NOVEL/DOCUMENT\n"
- "%s\n\n"
- ) % (hNewFolder, hPartition, tPartition)
-
- assert readFile(os.path.join(contentDir, hChapterOne+".nwd")) == (
- "%%%%~name: Chapter One\n"
- "%%%%~path: %s/%s\n"
- "%%%%~kind: NOVEL/DOCUMENT\n"
- "%s\n\n"
- ) % (hNewFolder, hChapterOne, tChapterOne)
-
- assert readFile(os.path.join(contentDir, hSceneOne+".nwd")) == (
- "%%%%~name: Scene One\n"
- "%%%%~path: %s/%s\n"
- "%%%%~kind: NOVEL/DOCUMENT\n"
- "%s\n\n"
- ) % (hNewFolder, hSceneOne, tSceneOne)
-
- assert readFile(os.path.join(contentDir, hSceneTwo+".nwd")) == (
- "%%%%~name: Scene Two\n"
- "%%%%~path: %s/%s\n"
- "%%%%~kind: NOVEL/DOCUMENT\n"
- "%s\n\n"
- ) % (hNewFolder, hSceneTwo, tSceneTwo)
-
- assert readFile(os.path.join(contentDir, hSceneThree+".nwd")) == (
- "%%%%~name: Scene Three\n"
- "%%%%~path: %s/%s\n"
- "%%%%~kind: NOVEL/DOCUMENT\n"
- "%s\n\n"
- ) % (hNewFolder, hSceneThree, tSceneThree)
-
- assert readFile(os.path.join(contentDir, hSceneFour+".nwd")) == (
- "%%%%~name: Scene Four\n"
- "%%%%~path: %s/%s\n"
- "%%%%~kind: NOVEL/DOCUMENT\n"
- "%s\n\n"
- ) % (hNewFolder, hSceneFour, tSceneFour)
-
- assert readFile(os.path.join(contentDir, hSceneFive+".nwd")) == (
- "%%%%~name: The End\n"
- "%%%%~path: %s/%s\n"
- "%%%%~kind: NOVEL/DOCUMENT\n"
- "%s\n\n"
- ) % (hNewFolder, hSceneFive, tSceneFive)
-
- # OS error
- with monkeypatch.context() as mp:
- mp.setattr("builtins.open", causeOSError)
- assert nwSplit._doSplit() is False
-
- # Select to not split
- with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
- assert nwSplit._doSplit() is False
-
- # Clear the list
- nwSplit.listBox.clear()
- assert nwSplit._doSplit() is False
-
- # Can't find sourcv item
- with monkeypatch.context() as mp:
- mp.setattr(NWTree, "__getitem__", lambda *a: None)
- assert nwSplit._doSplit() is False
-
- # No source item set
- nwSplit.sourceItem = None
- assert nwSplit._doSplit() is False
-
- # Close
- nwSplit._doClose()
-
- # qtbot.stopForInteraction()
+ nwSplit.reject()
+ # qtbot.stop()
# END Test testDlgSplit_Main
From 84af87b15564494d642c05776a73ddf08e72de72 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Oct 2022 19:01:06 +0200
Subject: [PATCH 7/8] Add test for document splitter
---
novelwriter/core/doctools.py | 3 +
tests/test_core/test_core_doctools.py | 140 +++++++++++++++++++++++++-
2 files changed, 140 insertions(+), 3 deletions(-)
diff --git a/novelwriter/core/doctools.py b/novelwriter/core/doctools.py
index 58394197..e26f6681 100644
--- a/novelwriter/core/doctools.py
+++ b/novelwriter/core/doctools.py
@@ -192,6 +192,9 @@ class DocSplitter:
"""An iterator that will write each document in the buffer, and
return its new handle, parent handle, and sibling handle.
"""
+ if self._srcHandle is None:
+ return
+
pHandle = self._parHandle
nHandle = self._parHandle if self._inFolder else self._srcHandle
hHandle = [self._parHandle, None, None, None, None]
diff --git a/tests/test_core/test_core_doctools.py b/tests/test_core/test_core_doctools.py
index 0d93fcb9..bcff60ce 100644
--- a/tests/test_core/test_core_doctools.py
+++ b/tests/test_core/test_core_doctools.py
@@ -28,7 +28,8 @@ from mock import causeOSError
from tools import C, buildTestProject, cmpFiles
from novelwriter.core.project import NWProject
-from novelwriter.core.doctools import DocMerger
+from novelwriter.core.doctools import DocMerger, DocSplitter
+from novelwriter.core.document import NWDoc
@pytest.mark.core
@@ -39,8 +40,8 @@ def testCoreDocTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, moc
mockRnd.reset()
buildTestProject(theProject, fncDir)
- # Create File to Merge
- # ====================
+ # Create Files to Merge
+ # =====================
hChapter1 = theProject.newFile("Chapter 1", C.hNovelRoot)
hSceneOne11 = theProject.newFile("Scene 1.1", hChapter1)
@@ -118,3 +119,136 @@ def testCoreDocTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, moc
docMerger.writeTargetDoc()
# END Test testCoreDocTools_DocMerger
+
+
+@pytest.mark.core
+def testCoreDocTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRnd, ipsumText):
+ """Test the DocSplitter utility.
+ """
+ theProject = NWProject(mockGUI)
+ mockRnd.reset()
+ buildTestProject(theProject, fncDir)
+
+ # Create File to Split
+ # ====================
+
+ hSplitDoc = theProject.newFile("Split Doc", C.hNovelRoot)
+
+ docData = [
+ "# Part One", ipsumText[0],
+ "## Chapter One", ipsumText[1],
+ "### Scene One", ipsumText[2],
+ "#### Section One", ipsumText[3],
+ "#### Section Two", ipsumText[4],
+ "### Scene Two", ipsumText[0],
+ "## Chapter Two", ipsumText[1],
+ "### Scene Three", ipsumText[2],
+ "### Scene Four", ipsumText[3],
+ "### Scene Five", ipsumText[4],
+ ]
+ splitData = [
+ (0, 1, "Part One"),
+ (4, 2, "Chapter One"),
+ (8, 3, "Scene One"),
+ (12, 4, "Section One"),
+ (16, 4, "Section Two"),
+ (20, 3, "Scene Two"),
+ (24, 2, "Chapter Two"),
+ (28, 3, "Scene Three"),
+ (32, 3, "Scene Four"),
+ (36, 3, "Scene Five"),
+ ]
+
+ docText = "\n\n".join(docData)
+ docRaw = docText.splitlines()
+ assert NWDoc(theProject, hSplitDoc).writeDocument(docText) is True
+
+ docSplitter = DocSplitter(theProject, hSplitDoc)
+ assert docSplitter._srcItem.isFileType()
+ assert docSplitter.getError() == ""
+
+ # Run the split algorithm
+ docSplitter.splitDocument(splitData, docRaw)
+ for i, (lineNo, hLevel, hLabel) in enumerate(splitData):
+ assert docSplitter._rawData[i] == (docRaw[lineNo:lineNo+4], hLevel, hLabel)
+
+ # Test flat split into same parent
+ docSplitter.setParentItem(C.hNovelRoot)
+ assert docSplitter._inFolder is False
+
+ # Cause write error on all chunks
+ with monkeypatch.context() as mp:
+ mp.setattr("builtins.open", causeOSError)
+ resStatus = []
+ for status, _, _ in docSplitter.writeDocuments(False):
+ resStatus.append(status)
+ assert not any(resStatus)
+ assert docSplitter.getError() == "OSError: Mock OSError"
+
+ # Generate as flat structure in root folder
+ resStatus = []
+ resDocHandle = []
+ resNearHandle = []
+ for status, dHandle, nHandle in docSplitter.writeDocuments(False):
+ resStatus.append(status)
+ resDocHandle.append(dHandle)
+ resNearHandle.append(nHandle)
+
+ assert all(resStatus)
+ assert resDocHandle == [
+ "000000000001b", "000000000001c", "000000000001d", "000000000001e", "000000000001f",
+ "0000000000020", "0000000000021", "0000000000022", "0000000000023", "0000000000024",
+ ]
+ assert resNearHandle == [ # Each document should be next to the previous one
+ hSplitDoc, "000000000001b", "000000000001c", "000000000001d", "000000000001e",
+ "000000000001f", "0000000000020", "0000000000021", "0000000000022", "0000000000023",
+ ]
+
+ # Generate as hierarchy in new folder
+ hSplitFolder = docSplitter.newParentFolder(C.hNovelRoot, "Split Folder")
+ assert docSplitter._inFolder is True
+
+ resStatus = []
+ resDocHandle = []
+ resNearHandle = []
+ for status, dHandle, nHandle in docSplitter.writeDocuments(True):
+ resStatus.append(status)
+ resDocHandle.append(dHandle)
+ resNearHandle.append(nHandle)
+
+ assert all(resStatus)
+ assert resDocHandle == [
+ "0000000000026", # Part One
+ "0000000000027", # Chapter One
+ "0000000000028", # Scene One
+ "0000000000029", # Section One
+ "000000000002a", # Section Two
+ "000000000002b", # Scene Two
+ "000000000002c", # Chapter Two
+ "000000000002d", # Scene Three
+ "000000000002e", # Scene Four
+ "000000000002f", # Scene Five
+ ]
+ assert resNearHandle == [
+ hSplitFolder, # Part One is after Split Folder
+ "0000000000026", # Chapter One is after Part One
+ "0000000000027", # Scene One is after Chapter One
+ "0000000000028", # Section One is after Scene One
+ "0000000000029", # Section Two is after Section One
+ "0000000000028", # Scene Two is after Scene One
+ "0000000000027", # Chapter Two is after Chapter One
+ "000000000002c", # Scene Three is after Chapter Two
+ "000000000002d", # Scene Four is after Scene Three
+ "000000000002e", # Scene Five is after Scene Four
+ ]
+
+ # Check handling of improper initialisation
+ docSplitter = DocSplitter(theProject, C.hInvalid)
+ assert docSplitter._srcHandle is None
+ assert docSplitter._srcItem is None
+ assert docSplitter.newParentFolder(C.hNovelRoot, "Split Folder") is None
+ assert list(docSplitter.writeDocuments(False)) == []
+
+ theProject.saveProject()
+
+# END Test testCoreDocTools_DocSplitter
From d619f4a0129d1fdd97b2e00d556b0b3db06f73cb Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 13 Oct 2022 23:48:52 +0200
Subject: [PATCH 8/8] Add test coverage of splitter function in project tree
---
tests/test_core/test_core_doctools.py | 4 +-
tests/test_dialogs/test_dlg_docsplit.py | 45 ++++-----
tests/test_gui/test_gui_projtree.py | 121 +++++++++++++++++++++++-
3 files changed, 139 insertions(+), 31 deletions(-)
diff --git a/tests/test_core/test_core_doctools.py b/tests/test_core/test_core_doctools.py
index bcff60ce..ab55aef4 100644
--- a/tests/test_core/test_core_doctools.py
+++ b/tests/test_core/test_core_doctools.py
@@ -27,9 +27,7 @@ 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, DocSplitter
-from novelwriter.core.document import NWDoc
+from novelwriter.core import NWProject, DocMerger, DocSplitter, NWDoc
@pytest.mark.core
diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py
index b95d497c..84b65a51 100644
--- a/tests/test_dialogs/test_dlg_docsplit.py
+++ b/tests/test_dialogs/test_dlg_docsplit.py
@@ -45,22 +45,17 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
docText = (
"Text\n\n"
- "##! Prologue\n\n"
- "Text\n\n"
- "## Chapter One\n\n"
- "### Scene One\n\n"
- "Text\n\n"
- "### Scene Two\n\n"
- "Text\n\n"
- "## Chapter Two\n\n"
- "### Scene Three\n\n"
- "Text\n\n"
- "### Scene Four\n\n"
- "Text\n\n"
- "#! New Title\n\n"
- "## New Chapter\n\n"
- "### New Scene\n\n"
- "#### New Section\n\n"
+ "##! Prologue\n\nText\n\n"
+ "## Chapter One\n\nText\n\n"
+ "### Scene One\n\nText\n\n"
+ "### Scene Two\n\nText\n\n"
+ "## Chapter Two\n\nText\n\n"
+ "### Scene Three\n\nText\n\n"
+ "### Scene Four\n\nText\n\n"
+ "#! New Title\n\nText\n\n"
+ "## New Chapter\n\nText\n\n"
+ "### New Scene\n\nText\n\n"
+ "#### New Section\n\nText\n\n"
)
hSplitDoc = theProject.newFile("Split Doc", C.hNovelRoot)
@@ -90,15 +85,15 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
assert data["headerList"][0] == (0, 1, "Split Doc")
assert data["headerList"][1] == (4, 2, "Prologue")
assert data["headerList"][2] == (8, 2, "Chapter One")
- assert data["headerList"][3] == (10, 3, "Scene One")
- assert data["headerList"][4] == (14, 3, "Scene Two")
- assert data["headerList"][5] == (18, 2, "Chapter Two")
- assert data["headerList"][6] == (20, 3, "Scene Three")
- assert data["headerList"][7] == (24, 3, "Scene Four")
- assert data["headerList"][8] == (28, 1, "New Title")
- assert data["headerList"][9] == (30, 2, "New Chapter")
- assert data["headerList"][10] == (32, 3, "New Scene")
- assert data["headerList"][11] == (34, 4, "New Section")
+ assert data["headerList"][3] == (12, 3, "Scene One")
+ assert data["headerList"][4] == (16, 3, "Scene Two")
+ assert data["headerList"][5] == (20, 2, "Chapter Two")
+ assert data["headerList"][6] == (24, 3, "Scene Three")
+ assert data["headerList"][7] == (28, 3, "Scene Four")
+ assert data["headerList"][8] == (32, 1, "New Title")
+ assert data["headerList"][9] == (36, 2, "New Chapter")
+ assert data["headerList"][10] == (40, 3, "New Scene")
+ assert data["headerList"][11] == (44, 4, "New Section")
# Loading the dialog on a non-file item produces an empty list
nwSplit._loadContent(C.hNovelRoot)
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index 8e683742..82119f41 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidgetItem, QDialog
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass
from novelwriter.core import NWDoc
-from novelwriter.dialogs import GuiEditLabel, GuiDocMerge
+from novelwriter.dialogs import GuiEditLabel, GuiDocMerge, GuiDocSplit
from novelwriter.gui.projtree import GuiProjectTree
@@ -675,7 +675,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
@pytest.mark.gui
-def testGuiProjTree_MergeDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ipsumText):
+def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ipsumText):
"""Test the merge document function.
"""
mergeData = {}
@@ -780,7 +780,122 @@ def testGuiProjTree_MergeDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ip
# qtbot.stop()
-# END Test testGuiProjTree_MergeDocument
+# END Test testGuiProjTree_MergeDocuments
+
+
+@pytest.mark.gui
+def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ipsumText):
+ """Test the split document function.
+ """
+ splitData = {}
+ splitText = []
+
+ # 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(GuiDocSplit, "__init__", lambda *a: None)
+ monkeypatch.setattr(GuiDocSplit, "exec_", lambda *a: None)
+ monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QDialog.Accepted)
+ monkeypatch.setattr(GuiDocSplit, "getData", lambda *a: (splitData, splitText))
+
+ # Create a project
+ prjDir = os.path.join(fncDir, "project")
+ buildTestProject(nwGUI, prjDir)
+
+ theProject = nwGUI.theProject
+ projTree = nwGUI.projView.projTree
+
+ docText = (
+ "Text\n\n"
+ "##! Prologue\n\nText\n\n"
+ "## Chapter One\n\nText\n\n"
+ "### Scene One\n\nText\n\n"
+ "### Scene Two\n\nText\n\n"
+ "## Chapter Two\n\nText\n\n"
+ "### Scene Three\n\nText\n\n"
+ "### Scene Four\n\nText\n\n"
+ "#! New Title\n\nText\n\n"
+ "## New Chapter\n\nText\n\n"
+ "### New Scene\n\nText\n\n"
+ "#### New Section\n\nText\n\n"
+ )
+
+ hSplitDoc = theProject.newFile("Split Doc", C.hNovelRoot)
+ theProject.writeNewFile(hSplitDoc, 1, True, docText)
+ projTree.revealNewTreeItem(hSplitDoc, nHandle=C.hNovelRoot, wordCount=True)
+
+ docText = f"# Split Doc\n\n{docText}"
+ splitData["headerList"] = [
+ (0, 1, "Split Doc"),
+ (4, 2, "Prologue"),
+ (8, 2, "Chapter One"),
+ (12, 3, "Scene One"),
+ (16, 3, "Scene Two"),
+ (20, 2, "Chapter Two"),
+ (24, 3, "Scene Three"),
+ (28, 3, "Scene Four"),
+ (32, 1, "New Title"),
+ (36, 2, "New Chapter"),
+ (40, 3, "New Scene"),
+ (44, 4, "New Section"),
+ ]
+
+ fstSet = [
+ "0000000000011", "0000000000012", "0000000000013", "0000000000014",
+ "0000000000015", "0000000000016", "0000000000017", "0000000000018",
+ "0000000000019", "000000000001a", "000000000001b", "000000000001c",
+ ]
+ sndSet = [
+ "000000000001d", "000000000001e", "000000000001f", "0000000000020",
+ "0000000000021", "0000000000022", "0000000000023", "0000000000024",
+ "0000000000025", "0000000000026", "0000000000027", "0000000000028",
+ ]
+ trdSet = [
+ "000000000002a", "000000000002b", "000000000002c", "000000000002d",
+ "000000000002e", "000000000002f", "0000000000030", "0000000000031",
+ "0000000000032", "0000000000033", "0000000000034", "0000000000035",
+ ]
+
+ # Try to split an invalid document and a non-document
+ assert projTree._splitDocument(C.hInvalid) is False
+ assert projTree._splitDocument(C.hNovelRoot) is False
+
+ # Split into same root folder
+ splitData["intoFolder"] = False
+
+ # Writing fails
+ with monkeypatch.context() as mp:
+ mp.setattr("builtins.open", causeOSError)
+ assert projTree._splitDocument(hSplitDoc) is True
+ for tHandle in fstSet:
+ assert tHandle in theProject.tree
+ assert not os.path.isfile(os.path.join(prjDir, "content", f"{tHandle}.nwd"))
+
+ # Writing succeeds
+ assert projTree._splitDocument(hSplitDoc) is True
+ for tHandle in sndSet:
+ assert tHandle in theProject.tree
+ assert os.path.isfile(os.path.join(prjDir, "content", f"{tHandle}.nwd"))
+
+ # Add to a folder
+ splitData["intoFolder"] = True
+ assert projTree._splitDocument(hSplitDoc) is True
+ assert "0000000000029" in theProject.tree # The folder
+ for tHandle in trdSet:
+ assert tHandle in theProject.tree
+ assert os.path.isfile(os.path.join(prjDir, "content", f"{tHandle}.nwd"))
+
+ # Cancelled by user
+ with monkeypatch.context() as mp:
+ mp.setattr(GuiDocSplit, "result", lambda *a: QDialog.Rejected)
+ assert projTree._splitDocument(hSplitDoc) is False
+
+ # qtbot.stop()
+
+# END Test testGuiProjTree_SplitDocument
@pytest.mark.gui