Rewrite the GuiDocSplit dialog
This commit is contained in:
@@ -42,7 +42,7 @@ VALID_MAP = {
|
|||||||
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
|
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
|
||||||
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
|
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
|
||||||
},
|
},
|
||||||
"GuiDocSplit": {"spLevel"},
|
"GuiDocSplit": {"spLevel", "intoFolder", "docHierarchy"},
|
||||||
"GuiBuildNovel": {
|
"GuiBuildNovel": {
|
||||||
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
|
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
|
||||||
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
|
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class GuiDocMerge(QDialog):
|
|||||||
|
|
||||||
# Merge Options
|
# Merge Options
|
||||||
self.trashLabel = QLabel(self.tr("Move merged items to Trash"))
|
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 = QGridLayout()
|
||||||
self.optBox.addWidget(self.trashLabel, 0, 0)
|
self.optBox.addWidget(self.trashLabel, 0, 0)
|
||||||
|
|||||||
+92
-157
@@ -1,10 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
novelWriter – GUI Doc Split Tool
|
novelWriter – GUI Doc Split Dialog
|
||||||
================================
|
==================================
|
||||||
GUI class for splitting a single document into multiple documents
|
Custom dialog class for splitting documents.
|
||||||
|
|
||||||
File History:
|
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
|
This file is a part of novelWriter
|
||||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||||
@@ -29,19 +30,18 @@ import novelwriter
|
|||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
|
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
|
||||||
QListWidgetItem, QDialogButtonBox, QLabel
|
QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter.core import NWDoc
|
from novelwriter.core import NWDoc
|
||||||
from novelwriter.enum import nwAlert
|
from novelwriter.gui.custom import QHelpLabel, QSwitch
|
||||||
from novelwriter.gui.custom import QHelpLabel
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GuiDocSplit(QDialog):
|
class GuiDocSplit(QDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui, sHandle):
|
||||||
super().__init__(parent=mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiDocSplit ...")
|
logger.debug("Initialising GuiDocSplit ...")
|
||||||
@@ -49,12 +49,12 @@ class GuiDocSplit(QDialog):
|
|||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = mainGui
|
||||||
|
self.mainTheme = mainGui.mainTheme
|
||||||
self.theProject = mainGui.theProject
|
self.theProject = mainGui.theProject
|
||||||
|
|
||||||
self.sourceItem = None
|
self._data = {}
|
||||||
self.sourceText = []
|
self._text = []
|
||||||
|
|
||||||
self.outerBox = QVBoxLayout()
|
|
||||||
self.setWindowTitle(self.tr("Split Document"))
|
self.setWindowTitle(self.tr("Split Document"))
|
||||||
|
|
||||||
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
|
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
|
||||||
@@ -63,6 +63,18 @@ class GuiDocSplit(QDialog):
|
|||||||
self.mainGui.mainTheme.helpText
|
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 = QListWidget()
|
||||||
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
||||||
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
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 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 3 (Scene)"), 3)
|
||||||
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
|
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
|
||||||
spIndex = self.splitLevel.findData(
|
spIndex = self.splitLevel.findData(spLevel)
|
||||||
self.theProject.options.getInt("GuiDocSplit", "spLevel", 3)
|
|
||||||
)
|
|
||||||
if spIndex != -1:
|
if spIndex != -1:
|
||||||
self.splitLevel.setCurrentIndex(spIndex)
|
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 = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
self.buttonBox.accepted.connect(self._doSplit)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self._doClose)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
# Assemble
|
||||||
|
self.outerBox = QVBoxLayout()
|
||||||
self.outerBox.setSpacing(0)
|
self.outerBox.setSpacing(0)
|
||||||
self.outerBox.addWidget(self.headLabel)
|
self.outerBox.addWidget(self.headLabel)
|
||||||
self.outerBox.addWidget(self.helpLabel)
|
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.listBox)
|
||||||
self.outerBox.addWidget(self.splitLevel)
|
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.outerBox.addWidget(self.buttonBox)
|
||||||
self.setLayout(self.outerBox)
|
self.setLayout(self.outerBox)
|
||||||
|
|
||||||
self.rejected.connect(self._doClose)
|
# Load Content
|
||||||
|
self._loadContent(sHandle)
|
||||||
self._populateList()
|
|
||||||
|
|
||||||
logger.debug("GuiDocSplit initialisation complete")
|
logger.debug("GuiDocSplit initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
def getData(self):
|
||||||
# Buttons
|
"""Return the user's choices. Also save the users options for
|
||||||
##
|
the next time the dialog is used.
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
logger.verbose("GuiDocSplit split button clicked")
|
headerList = []
|
||||||
|
|
||||||
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 = []
|
|
||||||
for i in range(self.listBox.count()):
|
for i in range(self.listBox.count()):
|
||||||
listItem = self.listBox.item(i)
|
item = self.listBox.item(i)
|
||||||
wTitle = listItem.text()
|
headerList.append((item.text(), item.data(Qt.UserRole)))
|
||||||
lineNo = listItem.data(Qt.UserRole)
|
|
||||||
finalOrder.append([wTitle, lineNo, nLines])
|
|
||||||
if i > 0:
|
|
||||||
finalOrder[i-1][2] = lineNo
|
|
||||||
|
|
||||||
nFiles = len(finalOrder)
|
spLevel = self.splitLevel.currentData()
|
||||||
if nFiles == 0:
|
intoFolder = self.folderSwitch.isChecked()
|
||||||
self.mainGui.makeAlert(self.tr(
|
docHierarchy = self.hierarchySwitch.isChecked()
|
||||||
"No headers found. Nothing to do."
|
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
msgYes = self.mainGui.askQuestion(
|
self._data["spLevel"] = spLevel
|
||||||
self.tr("Split Document"),
|
self._data["headerList"] = headerList
|
||||||
"{0}<br><br>{1}".format(
|
self._data["intoFolder"] = intoFolder
|
||||||
self.tr(
|
self._data["docHierarchy"] = docHierarchy
|
||||||
"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
|
pOptions = self.theProject.options
|
||||||
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
|
pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
|
||||||
self.mainGui.projView.revealNewTreeItem(fHandle)
|
pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
|
||||||
logger.verbose("Creating folder '%s'", fHandle)
|
pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
|
||||||
|
|
||||||
# Loop through, and create the files
|
return self._data
|
||||||
for wTitle, iStart, iEnd in finalOrder:
|
|
||||||
|
|
||||||
wTitle = wTitle.lstrip("#").strip()
|
##
|
||||||
nHandle = self.theProject.newFile(wTitle, fHandle)
|
# Slots
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
theText = "\n".join(self.sourceText[iStart:iEnd])
|
def _reloadList(self):
|
||||||
theText = theText.rstrip("\n") + "\n\n"
|
"""Reload the content of the list box.
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
self.theProject.options.saveSettings()
|
sHandle = self._data.get("sHandle", None)
|
||||||
self.close()
|
self._loadContent(sHandle)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _populateList(self):
|
def _loadContent(self, sHandle):
|
||||||
"""Get the item selected in the tree, check that it is a folder,
|
"""Load content from a given source item.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
self.listBox.clear()
|
self._data = {}
|
||||||
if self.sourceItem is None:
|
self._data["sHandle"] = sHandle
|
||||||
self.sourceItem = self.mainGui.projView.getSelectedHandle()
|
|
||||||
|
|
||||||
if self.sourceItem is None:
|
nwItem = self.theProject.tree[sHandle]
|
||||||
return False
|
if nwItem is None or not nwItem.isFileType():
|
||||||
|
|
||||||
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
|
return False
|
||||||
|
|
||||||
spLevel = self.splitLevel.currentData()
|
spLevel = self.splitLevel.currentData()
|
||||||
self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel)
|
if not self._text:
|
||||||
logger.debug(
|
inDoc = NWDoc(self.theProject, sHandle)
|
||||||
"Scanning document '%s' for headings level <= %d",
|
self._text = (inDoc.readDocument() or "").splitlines()
|
||||||
self.sourceItem, spLevel
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sourceText = theText.splitlines()
|
self.listBox.clear()
|
||||||
for lineNo, aLine in enumerate(self.sourceText):
|
for lineNo, aLine in enumerate(self._text):
|
||||||
|
|
||||||
onLine = -1
|
onLine = -1
|
||||||
if aLine.startswith("# ") and spLevel >= 1:
|
if aLine.startswith(("# ", "#! ")) and spLevel >= 1:
|
||||||
onLine = lineNo
|
onLine = lineNo
|
||||||
elif aLine.startswith("## ") and spLevel >= 2:
|
elif aLine.startswith(("## ", "##! ")) and spLevel >= 2:
|
||||||
onLine = lineNo
|
onLine = lineNo
|
||||||
elif aLine.startswith("### ") and spLevel >= 3:
|
elif aLine.startswith("### ") and spLevel >= 3:
|
||||||
onLine = lineNo
|
onLine = lineNo
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ from PyQt5.QtWidgets import (
|
|||||||
|
|
||||||
from novelwriter.core import DocMerger
|
from novelwriter.core import DocMerger
|
||||||
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
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
|
from novelwriter.constants import nwHeaders, trConst, nwLabels
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1458,7 +1458,30 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _splitDocument(self, tHandle):
|
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):
|
def _scanChildren(self, theList, tItem, tIndex):
|
||||||
"""This is a recursive function returning all items in a tree
|
"""This is a recursive function returning all items in a tree
|
||||||
|
|||||||
+1
-13
@@ -44,7 +44,7 @@ from novelwriter.gui import (
|
|||||||
GuiViewsBar
|
GuiViewsBar
|
||||||
)
|
)
|
||||||
from novelwriter.dialogs import (
|
from novelwriter.dialogs import (
|
||||||
GuiAbout, GuiDocSplit, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
|
GuiAbout, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
|
||||||
GuiProjectSettings, GuiUpdates, GuiWordList
|
GuiProjectSettings, GuiUpdates, GuiWordList
|
||||||
)
|
)
|
||||||
from novelwriter.tools import (
|
from novelwriter.tools import (
|
||||||
@@ -754,18 +754,6 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
return True
|
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):
|
def passDocumentAction(self, theAction):
|
||||||
"""Pass on document action to the document viewer if it has
|
"""Pass on document action to the document viewer if it has
|
||||||
focus, or pass it to the document editor if it or any of
|
focus, or pass it to the document editor if it or any of
|
||||||
|
|||||||
Reference in New Issue
Block a user