diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index fad7b148..d8842bcc 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -9,6 +9,7 @@ from nw.gui.theme import GuiTheme # Dialogs from nw.gui.dialogs.configeditor import GuiConfigEditor from nw.gui.dialogs.docmerge import GuiDocMerge +from nw.gui.dialogs.docsplit import GuiDocSplit from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor @@ -35,6 +36,7 @@ __all__ = [ "GuiTheme", "GuiConfigEditor", "GuiDocMerge", + "GuiDocSplit", "GuiExport", "GuiItemEditor", "GuiProjectEditor", diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py index 0c6b842e..a58b4982 100644 --- a/nw/gui/dialogs/__init__.py +++ b/nw/gui/dialogs/__init__.py @@ -2,6 +2,7 @@ from nw.gui.dialogs.configeditor import GuiConfigEditor from nw.gui.dialogs.docmerge import GuiDocMerge +from nw.gui.dialogs.docsplit import GuiDocSplit from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor @@ -11,6 +12,7 @@ from nw.gui.dialogs.timelineview import GuiTimeLineView __all__ = [ "GuiConfigEditor", "GuiDocMerge", + "GuiDocSplit", "GuiExport", "GuiItemEditor", "GuiProjectEditor", diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py new file mode 100644 index 00000000..3e8502b5 --- /dev/null +++ b/nw/gui/dialogs/docsplit.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +"""novelWriter GUI Doc Split + + novelWriter – GUI Doc Split +============================= + Tool for splitting a single document into multiple documents + + File History: + Created: 2020-02-01 [0.4.3] + +""" + +import logging +import nw + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QComboBox, + QListWidget, QAbstractItemView, QListWidgetItem +) +from nw.constants import nwAlert, nwItemType, nwItemClass, nwItemLayout +from nw.project import NWDoc + +logger = logging.getLogger(__name__) + +class GuiDocSplit(QDialog): + + def __init__(self, theParent, theProject): + QDialog.__init__(self, theParent) + + logger.debug("Initialising GuiDocSplit ...") + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theProject = theProject + self.sourceItem = None + + self.outerBox = QHBoxLayout() + self.innerBox = QVBoxLayout() + self.setWindowTitle("Split Document") + self.setLayout(self.outerBox) + + self.guiDeco = self.theParent.theTheme.loadDecoration("split",(64,64)) + + self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) + self.outerBox.addLayout(self.innerBox) + + self.doMergeForm = QGridLayout() + self.doMergeForm.setContentsMargins(10,5,0,10) + + self.listBox = QListWidget() + self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop) + + self.splitLevel = QComboBox(self) + self.splitLevel.addItem("Split on Title (Level 1)", 1) + self.splitLevel.addItem("Split on Chapter (Level 2)", 2) + self.splitLevel.addItem("Split on Scene (Level 3)", 3) + self.splitLevel.addItem("Split on Section (Level 4)", 4) + self.splitLevel.setCurrentIndex(2) + self.splitLevel.currentIndexChanged.connect(self._populateList) + + self.splitButton = QPushButton("Split") + self.splitButton.clicked.connect(self._doSplit) + + self.closeButton = QPushButton("Close") + self.closeButton.clicked.connect(self._doClose) + + self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3) + self.doMergeForm.addWidget(self.splitLevel, 1, 0, 1, 2) + self.doMergeForm.addWidget(self.splitButton, 2, 1) + self.doMergeForm.addWidget(self.closeButton, 2, 2) + + self.innerBox.addLayout(self.doMergeForm) + + self.rejected.connect(self._doClose) + self.show() + + self._populateList() + + 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 merge process, and + must be deleted manually. + """ + + logger.verbose("GuiDocSplit split button clicked") + + if self.sourceItem is None: + self.theParent.makeAlert(( + "No source document selected. Nothing to do." + ), nwAlert.ERROR) + return + + srcItem = self.theProject.getItem(self.sourceItem) + if srcItem is None: + self.theParent.makeAlert(( + "Could not parse source document." + ), nwAlert.ERROR) + return + + theDoc = NWDoc(self.theProject, self.theParent) + theText = theDoc.openDocument(self.sourceItem, False) + theLines = theText.splitlines() + nLines = len(theLines) + theLines.insert(0, "%Split Doc") + + finalOrder = [] + 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 + + if len(finalOrder) == 0: + self.theParent.makeAlert(( + "No headers found. Nothing to do." + ), nwAlert.ERROR) + return + + fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemClass, srcItem.parHandle) + self.theParent.treeView.revealTreeItem(fHandle) + + for wTitle, iStart, iEnd in finalOrder: + + itemLayout = nwItemLayout.NOTE + if srcItem.itemClass == nwItemClass.NOVEL: + if wTitle.startswith("# "): + itemLayout = nwItemLayout.PARTITION + elif wTitle.startswith("## "): + itemLayout = nwItemLayout.CHAPTER + elif wTitle.startswith("### "): + itemLayout = nwItemLayout.SCENE + elif wTitle.startswith("#### "): + itemLayout = nwItemLayout.PAGE + + wTitle = wTitle.lstrip("#") + wTitle = wTitle.strip() + print(wTitle, iStart, iEnd) + + nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle) + newItem = self.theProject.getItem(nHandle) + newItem.setLayout(itemLayout) + + theText = "\n".join(theLines[iStart:iEnd]) + theDoc.openDocument(nHandle, False) + theDoc.saveDocument(theText) + theDoc.clearDocument() + self.theParent.treeView.revealTreeItem(nHandle) + + # theDoc = NWDoc(self.theProject, self.theParent) + # theText = "" + # for tHandle in finalOrder: + # theText += theDoc.openDocument(tHandle, False).rstrip() + # theText += "\n\n" + + # if self.sourceItem is None: + # self.theParent.makeAlert(( + # "Cannot parse source item." + # ), nwAlert.ERROR) + # return + + # srcItem = self.theProject.getItem(self.sourceItem) + # nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle) + # self.theParent.treeView.revealTreeItem(nHandle) + # theDoc.openDocument(nHandle, False) + # theDoc.saveDocument(theText) + # self.theParent.openDocument(nHandle) + + self.close() + + return + + def _doClose(self): + """Close the dialog window without doing anything. + """ + logger.verbose("GuiDocSplit close button clicked") + self.close() + 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. + """ + + if self.sourceItem is None: + self.sourceItem = self.theParent.treeView.getSelectedHandle() + + if self.sourceItem is None: + return + + nwItem = self.theProject.getItem(self.sourceItem) + if nwItem is None: + return + if nwItem.itemType is not nwItemType.FILE: + self.theParent.makeAlert(( + "Element selected in the project tree must be a file." + ), nwAlert.ERROR) + return + + self.listBox.clear() + theDoc = NWDoc(self.theProject, self.theParent) + theText = theDoc.openDocument(self.sourceItem, False) + + spLevel = self.splitLevel.currentData() + logger.debug("Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel)) + + lineNo = 0 + for aLine in theText.splitlines(): + + lineNo += 1 + onLine = 0 + + if aLine.startswith("# ") and spLevel >= 1: + onLine = lineNo + elif aLine.startswith("## ") and spLevel >= 2: + onLine = lineNo + elif aLine.startswith("### ") and spLevel >= 3: + onLine = lineNo + elif aLine.startswith("#### ") and spLevel >= 4: + onLine = lineNo + + if onLine > 0: + newItem = QListWidgetItem() + newItem.setText(aLine.strip()) + newItem.setData(Qt.UserRole, onLine) + self.listBox.addItem(newItem) + + return + +# END Class GuiDocSplit diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 580aacaf..ba75e779 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -372,10 +372,15 @@ class GuiMainMenu(QMenuBar): # Document > Merge Documents self.aMergeDocs = QAction("Merge Folder to Document", self) self.aMergeDocs.setStatusTip("Merge a folder of documents to a single document") - # self.aMergeDocs.setShortcut("Ctrl+Shift+I") self.aMergeDocs.triggered.connect(self.theParent.mergeDocuments) self.docuMenu.addAction(self.aMergeDocs) + # Document > Split Document + self.aSplitDoc = QAction("Split Document to Folder", self) + self.aSplitDoc.setStatusTip("Split a document into a folder of multiple documents") + self.aSplitDoc.triggered.connect(self.theParent.splitDocument) + self.docuMenu.addAction(self.aSplitDoc) + return def _buildViewMenu(self): diff --git a/nw/guimain.py b/nw/guimain.py index 4572ee01..bfc67115 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -24,10 +24,10 @@ from PyQt5.QtWidgets import ( ) from nw.gui import ( - GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, - GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, - GuiDocViewDetails, GuiConfigEditor, GuiProjectEditor, GuiExport, - GuiItemEditor, GuiTimeLineView, GuiSessionLogView, GuiDocMerge + GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport, + GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails, + GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiTimeLineView, + GuiSessionLogView, GuiDocMerge, GuiDocSplit ) from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup from nw.tools import countWords @@ -83,7 +83,7 @@ class GuiMain(QMainWindow): # Assemble Main Window self.treePane = QFrame() - self.treeBox = QVBoxLayout() + self.treeBox = QVBoxLayout() self.treeBox.setContentsMargins(0,0,0,0) self.treeBox.addWidget(self.treeView) self.treeBox.addWidget(self.treeMeta) @@ -461,12 +461,17 @@ class GuiMain(QMainWindow): def mergeDocuments(self): """Merge multiple documents to one single new document. """ - if self.mainConf.showGUI: dlgMerge = GuiDocMerge(self, self.theProject) - if dlgMerge.exec_(): - pass + dlgMerge.exec_() + return True + def splitDocument(self): + """Split a single document into multiple documents. + """ + if self.mainConf.showGUI: + dlgSplit = GuiDocSplit(self, self.theProject) + dlgSplit.exec_() return True def passDocumentAction(self, theAction): diff --git a/nw/project/project.py b/nw/project/project.py index a52a83f3..e6167eb9 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -563,6 +563,11 @@ class NWProject(): """This only removes the item from the order list, but not from the project tree. """ + if tHandle not in self.treeOrder: + logger.warning( + "Could not remove item %s from treeOrder as it does not exist" % tHandle + ) + return False self.treeOrder.remove(tHandle) self.setProjectChanged(True) return True