Rewrite Split Tool (#1154)
This commit is contained in:
@@ -19,7 +19,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
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",
|
||||
|
||||
@@ -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
|
||||
@@ -25,6 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from novelwriter.common import minmax
|
||||
from novelwriter.core.document import NWDoc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -117,3 +119,122 @@ 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._inFolder = False
|
||||
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
|
||||
self._inFolder = False
|
||||
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[newHandle]
|
||||
newItem.setStatus(self._srcItem.itemStatus)
|
||||
newItem.setImport(self._srcItem.itemImport)
|
||||
|
||||
self._parHandle = newHandle
|
||||
self._inFolder = True
|
||||
|
||||
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, docHierarchy):
|
||||
"""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]
|
||||
|
||||
pLevel = 0
|
||||
for docText, hLevel, docLabel in self._rawData:
|
||||
|
||||
hLevel = minmax(hLevel, 1, 4)
|
||||
if pLevel == 0:
|
||||
pLevel = hLevel
|
||||
|
||||
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, dHandle, nHandle
|
||||
|
||||
hHandle[hLevel] = dHandle
|
||||
nHandle = dHandle
|
||||
pLevel = hLevel
|
||||
|
||||
return
|
||||
|
||||
# END Class DocSplitter
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
+118
-157
@@ -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,22 @@ 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):
|
||||
LINE_ROLE = Qt.UserRole
|
||||
LEVEL_ROLE = Qt.UserRole + 1
|
||||
LABEL_ROLE = Qt.UserRole + 2
|
||||
|
||||
def __init__(self, mainGui, sHandle):
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiDocSplit ...")
|
||||
@@ -49,12 +53,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("<b>{0}</b>".format(self.tr("Document Headers")))
|
||||
@@ -63,6 +67,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,206 +89,151 @@ 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
|
||||
|
||||
nFiles = len(finalOrder)
|
||||
if nFiles == 0:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"No headers found. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
msgYes = self.mainGui.askQuestion(
|
||||
self.tr("Split Document"),
|
||||
"{0}<br><br>{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
|
||||
|
||||
# Create the folder
|
||||
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
|
||||
self.mainGui.projView.revealNewTreeItem(fHandle)
|
||||
logger.verbose("Creating folder '%s'", fHandle)
|
||||
|
||||
# Loop through, and create the files
|
||||
for wTitle, iStart, iEnd in finalOrder:
|
||||
|
||||
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
|
||||
item = self.listBox.item(i)
|
||||
headerList.append(
|
||||
(item.data(self.LINE_ROLE), item.data(self.LEVEL_ROLE), item.data(self.LABEL_ROLE))
|
||||
)
|
||||
|
||||
theText = "\n".join(self.sourceText[iStart:iEnd])
|
||||
theText = theText.rstrip("\n") + "\n\n"
|
||||
spLevel = self.splitLevel.currentData()
|
||||
intoFolder = self.folderSwitch.isChecked()
|
||||
docHierarchy = self.hierarchySwitch.isChecked()
|
||||
|
||||
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._data["spLevel"] = spLevel
|
||||
self._data["headerList"] = headerList
|
||||
self._data["intoFolder"] = intoFolder
|
||||
self._data["docHierarchy"] = docHierarchy
|
||||
|
||||
self.mainGui.projView.revealNewTreeItem(nHandle)
|
||||
pOptions = self.theProject.options
|
||||
pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
|
||||
pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
|
||||
pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
|
||||
|
||||
self._doClose()
|
||||
return self._data, self._text
|
||||
|
||||
return True
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
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._data = {}
|
||||
self._data["sHandle"] = sHandle
|
||||
|
||||
self.listBox.clear()
|
||||
if self.sourceItem is None:
|
||||
self.sourceItem = self.mainGui.projView.getSelectedHandle()
|
||||
|
||||
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 = ""
|
||||
return False
|
||||
nwItem = self.theProject.tree[sHandle]
|
||||
if nwItem is None or not nwItem.isFileType():
|
||||
return
|
||||
|
||||
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):
|
||||
for lineNo, aLine in enumerate(self._text):
|
||||
|
||||
onLine = -1
|
||||
hLevel = 0
|
||||
if aLine.startswith("# ") and spLevel >= 1:
|
||||
onLine = lineNo
|
||||
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
|
||||
return
|
||||
|
||||
# END Class GuiDocSplit
|
||||
|
||||
@@ -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):
|
||||
|
||||
+65
-17
@@ -40,8 +40,9 @@ 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, GuiEditLabel
|
||||
from novelwriter.dialogs import GuiDocMerge, GuiDocSplit, GuiEditLabel
|
||||
from novelwriter.constants import nwHeaders, trConst, nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -516,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]
|
||||
@@ -538,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)
|
||||
@@ -801,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()
|
||||
@@ -1432,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):
|
||||
@@ -1458,7 +1459,54 @@ 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:
|
||||
|
||||
splitData, splitText = dlgSplit.getData()
|
||||
|
||||
headerList = splitData.get("headerList", [])
|
||||
intoFolder = splitData.get("intoFolder", False)
|
||||
docHierarchy = splitData.get("docHierarchy", False)
|
||||
|
||||
docSplit = DocSplitter(self.theProject, tHandle)
|
||||
if intoFolder:
|
||||
fHandle = docSplit.newParentFolder(tItem.itemParent, tItem.itemName)
|
||||
self.revealNewTreeItem(fHandle, nHandle=tHandle)
|
||||
self._alertTreeChange(fHandle, flush=False)
|
||||
else:
|
||||
docSplit.setParentItem(tItem.itemParent)
|
||||
|
||||
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")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _scanChildren(self, theList, tItem, tIndex):
|
||||
"""This is a recursive function returning all items in a tree
|
||||
|
||||
+1
-13
@@ -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
|
||||
|
||||
@@ -27,8 +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
|
||||
from novelwriter.core import NWProject, DocMerger, DocSplitter, NWDoc
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -39,8 +38,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 +117,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
|
||||
|
||||
@@ -19,18 +19,13 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -45,207 +40,66 @@ 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\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"
|
||||
)
|
||||
|
||||
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] == (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")
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user