diff --git a/.travis.yml b/.travis.yml
index d40b1c68..de4a683a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,9 @@
os: linux
-dist: xenial
+dist: bionic
services:
- xvfb
language: python
cache: bundler
-sudo: required
addons:
apt:
packages:
@@ -12,9 +11,11 @@ addons:
- python3-pyqt5
- python3-pyqt5.qtsvg
python:
- - "3.5"
+ - "3.6"
- "3.7"
+ - "3.8"
install:
+ - pip install --upgrade pip
- pip install -r requirements.txt
# - pip install pytest-faulthandler
- pip install pytest-xvfb
diff --git a/nw/assets/graphics/export.txt b/nw/assets/graphics/license.txt
similarity index 75%
rename from nw/assets/graphics/export.txt
rename to nw/assets/graphics/license.txt
index 1a65e8da..8a822fd2 100644
--- a/nw/assets/graphics/export.txt
+++ b/nw/assets/graphics/license.txt
@@ -1,3 +1,8 @@
FROM ICON SET: Typicons
LICENSE: Creative Commons (Attribution-Share Alike 3.0 Unported)
https://creativecommons.org/licenses/by-sa/3.0/
+
+Apllies to:
+export.svg
+merge.svg
+split.svg
diff --git a/nw/assets/graphics/merge.svg b/nw/assets/graphics/merge.svg
new file mode 100644
index 00000000..1b4c6ae9
--- /dev/null
+++ b/nw/assets/graphics/merge.svg
@@ -0,0 +1,31 @@
+
+
diff --git a/nw/assets/graphics/split.svg b/nw/assets/graphics/split.svg
new file mode 100644
index 00000000..c427eea6
--- /dev/null
+++ b/nw/assets/graphics/split.svg
@@ -0,0 +1,31 @@
+
+
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index a0325482..d3ccd1c7 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -29,6 +29,7 @@ class nwFiles():
EXPORT_OPT = "exportOptions.json"
TLINE_OPT = "timelineOptions.json"
SLOG_OPT = "sessionLogOptions.json"
+ MERGE_OPT = "docMergeOptions.json"
# END Class nwFiles
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index b8196cc2..d8842bcc 100644
--- a/nw/gui/__init__.py
+++ b/nw/gui/__init__.py
@@ -8,6 +8,8 @@ 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
@@ -33,6 +35,8 @@ __all__ = [
"GuiMainStatus",
"GuiTheme",
"GuiConfigEditor",
+ "GuiDocMerge",
+ "GuiDocSplit",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py
index fac299ba..a58b4982 100644
--- a/nw/gui/dialogs/__init__.py
+++ b/nw/gui/dialogs/__init__.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
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
@@ -9,6 +11,8 @@ from nw.gui.dialogs.timelineview import GuiTimeLineView
__all__ = [
"GuiConfigEditor",
+ "GuiDocMerge",
+ "GuiDocSplit",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
diff --git a/nw/gui/dialogs/docmerge.py b/nw/gui/dialogs/docmerge.py
new file mode 100644
index 00000000..2b46eba5
--- /dev/null
+++ b/nw/gui/dialogs/docmerge.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+"""novelWriter GUI Doc Merge
+
+ novelWriter – GUI Doc Merge
+=============================
+ Tool for merging multiple documents to one
+
+ File History:
+ Created: 2020-01-23 [0.4.3]
+
+"""
+
+import logging
+import nw
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import (
+ QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton,
+ QListWidget, QAbstractItemView, QListWidgetItem
+)
+from nw.constants import nwAlert, nwItemType
+from nw.project import NWDoc
+
+logger = logging.getLogger(__name__)
+
+class GuiDocMerge(QDialog):
+
+ def __init__(self, theParent, theProject):
+ QDialog.__init__(self, theParent)
+
+ logger.debug("Initialising GuiDocMerge ...")
+
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+ self.theProject = theProject
+ self.sourceItem = None
+
+ self.outerBox = QHBoxLayout()
+ self.innerBox = QVBoxLayout()
+ self.setWindowTitle("Merge Documents")
+ self.setLayout(self.outerBox)
+
+ self.guiDeco = self.theParent.theTheme.loadDecoration("merge",(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.InternalMove)
+
+ self.mergeButton = QPushButton("Merge")
+ self.mergeButton.clicked.connect(self._doMerge)
+
+ self.closeButton = QPushButton("Close")
+ self.closeButton.clicked.connect(self._doClose)
+
+ self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
+ self.doMergeForm.addWidget(self.mergeButton, 1, 1)
+ self.doMergeForm.addWidget(self.closeButton, 1, 2)
+
+ self.innerBox.addLayout(self.doMergeForm)
+
+ self.rejected.connect(self._doClose)
+ self.show()
+
+ self._populateList()
+
+ logger.debug("GuiDocMerge initialisation complete")
+
+ return
+
+ ##
+ # Buttons
+ ##
+
+ def _doMerge(self):
+ """Perform the merge of the files in the selected folder, and
+ create a new file in the same parent folder. The old files are
+ not removed in the merge process, and must be deleted manually.
+ """
+
+ logger.verbose("GuiDocMerge merge button clicked")
+
+ finalOrder = []
+ for i in range(self.listBox.count()):
+ finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
+
+ 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("GuiDocMerge 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.
+ """
+
+ tHandle = self.theParent.treeView.getSelectedHandle()
+ self.sourceItem = tHandle
+ if tHandle is None:
+ return
+
+ nwItem = self.theProject.getItem(tHandle)
+ if nwItem is None:
+ return
+ if nwItem.itemType is not nwItemType.FOLDER:
+ self.theParent.makeAlert((
+ "Element selected in the project tree must be a folder."
+ ), nwAlert.ERROR)
+ return
+
+ for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
+ newItem = QListWidgetItem()
+ nwItem = self.theProject.getItem(sHandle)
+ if nwItem.itemType is not nwItemType.FILE:
+ continue
+ newItem.setText(nwItem.itemName)
+ newItem.setData(Qt.UserRole, sHandle)
+ self.listBox.addItem(newItem)
+
+ return
+
+# END Class GuiDocMerge
diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py
new file mode 100644
index 00000000..739bb14e
--- /dev/null
+++ b/nw/gui/dialogs/docsplit.py
@@ -0,0 +1,234 @@
+# -*- 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, 3)
+ 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")
+ logger.debug(
+ "Splitting document %s with %d lines" % (self.sourceItem,nLines)
+ )
+
+ 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)
+ logger.verbose("Creating folder %s" % 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()
+
+ nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle)
+ newItem = self.theProject.getItem(nHandle)
+ newItem.setLayout(itemLayout)
+ logger.verbose(
+ "Creating new document %s with text from line %d to %d" % (nHandle, iStart, iEnd-1)
+ )
+
+ theText = "\n".join(theLines[iStart:iEnd])
+ theDoc.openDocument(nHandle, False)
+ theDoc.saveDocument(theText)
+ theDoc.clearDocument()
+ self.theParent.treeView.revealTreeItem(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/elements/doctree.py b/nw/gui/elements/doctree.py
index d5b9fb4f..5ade6c5b 100644
--- a/nw/gui/elements/doctree.py
+++ b/nw/gui/elements/doctree.py
@@ -16,10 +16,10 @@ import nw
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QFont, QColor
from PyQt5.QtWidgets import (
- QTreeWidget, QTreeWidgetItem, QAbstractItemView, QApplication
+ QTreeWidget, QTreeWidgetItem, QAbstractItemView, QApplication, QMessageBox
)
-from nw.project import NWItem
+from nw.project import NWItem, NWDoc
from nw.constants import (
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert
)
@@ -171,14 +171,21 @@ class GuiDocTree(QTreeWidget):
return False
# Add the new item to the tree
- nwItem = self.theProject.getItem(tHandle)
- trItem = self._addTreeItem(nwItem)
+ self.revealTreeItem(tHandle)
+ self.theParent.editItem()
+
+ return True
+
+ def revealTreeItem(self, tHandle):
+ """Reveal a newly added project item in the project tree.
+ """
+ nwItem = self.theProject.getItem(tHandle)
+ trItem = self._addTreeItem(nwItem)
+ pHandle = nwItem.parHandle
if pHandle is not None and pHandle in self.theMap.keys():
self.theMap[pHandle].setExpanded(True)
self.clearSelection()
trItem.setSelected(True)
- self.theParent.editItem()
-
return True
def moveTreeItem(self, nStep):
@@ -221,6 +228,16 @@ class GuiDocTree(QTreeWidget):
self.theProject.setTreeOrder(theList)
return True
+ def getTreeFromHandle(self, tHandle):
+ """Recursively return all the children items starting from a
+ given item handle.
+ """
+ theList = []
+ theItem = self._getTreeItem(tHandle)
+ if theItem is not None:
+ theList = self._scanChildren(theList, theItem, 0)
+ return theList
+
def getColumnSizes(self):
retVals = [
self.columnWidth(0),
@@ -246,21 +263,61 @@ class GuiDocTree(QTreeWidget):
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.getItem(tHandle)
+ if nwItemS is None:
+ return False
+
if nwItemS.itemType == nwItemType.FILE:
logger.debug("User requested file %s moved to trash" % tHandle)
trItemP = trItemS.parent()
trItemT = self._addTrashRoot()
if trItemP is None or trItemT is None:
- logger.error("Could not move item to trash")
+ logger.error("Could not delete item")
return False
- tIndex = trItemP.indexOfChild(trItemS)
- trItemC = trItemP.takeChild(tIndex)
- trItemT.addChild(trItemC)
- nwItemS.setParent(self.theProject.trashRoot)
- self.clearSelection()
- trItemP.setSelected(True)
- self.theProject.setProjectChanged(True)
- self.theParent.theIndex.deleteHandle(tHandle)
+
+ pHandle = nwItemS.parHandle
+ if pHandle is not None and pHandle == self.theProject.trashRoot:
+ # If the file is in the trash folder already, as the
+ # user if they want to permanently delete the file.
+
+ doPermanent = False
+ if self.mainConf.showGUI:
+ msgBox = QMessageBox()
+ msgRes = msgBox.question(
+ self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
+ )
+ if msgRes == QMessageBox.Yes:
+ doPermanent = True
+ else:
+ doPermanent = True
+
+ if doPermanent:
+ logger.debug("Permanently deleting file with handle %s" % tHandle)
+
+ tIndex = trItemP.indexOfChild(trItemS)
+ trItemC = trItemP.takeChild(tIndex)
+
+ if self.theParent.docEditor.theHandle == tHandle:
+ self.theParent.closeDocument()
+
+ theDoc = NWDoc(self.theProject, self.theParent)
+ theDoc.deleteDocument(tHandle)
+ self.theProject.deleteItem(tHandle)
+ self.theParent.theIndex.deleteHandle(tHandle)
+
+ else:
+ # The file is not already in the trash folder, so we
+ # move it there.
+
+ if pHandle is None:
+ logger.warning("File has no parent item")
+
+ tIndex = trItemP.indexOfChild(trItemS)
+ trItemC = trItemP.takeChild(tIndex)
+ trItemT.addChild(trItemC)
+ nwItemS.setParent(self.theProject.trashRoot)
+
+ self.theProject.setProjectChanged(True)
+ self.theParent.theIndex.deleteHandle(tHandle)
elif nwItemS.itemType == nwItemType.FOLDER:
logger.debug("User requested folder %s deleted" % tHandle)
@@ -271,8 +328,6 @@ class GuiDocTree(QTreeWidget):
tIndex = trItemP.indexOfChild(trItemS)
if trItemS.childCount() == 0:
trItemP.takeChild(tIndex)
- self.clearSelection()
- trItemP.setSelected(True)
self.theProject.deleteItem(tHandle)
else:
self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR)
@@ -428,6 +483,9 @@ class GuiDocTree(QTreeWidget):
return newItem
def _addTrashRoot(self):
+ """Adds the trash root folder if it doesn't already exist in the
+ project tree.
+ """
if self.theProject.trashRoot is None:
self.theProject.addTrash()
trItem = self._addTreeItem(
diff --git a/nw/gui/icons.py b/nw/gui/icons.py
index 88149b6e..facb84d7 100644
--- a/nw/gui/icons.py
+++ b/nw/gui/icons.py
@@ -46,7 +46,9 @@ class GuiIcons:
DECO_MAP = {
"export" : "export.svg",
+ "merge" : "merge.svg",
"settings" : "gear.svg",
+ "split" : "split.svg",
}
def __init__(self, theParent):
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index ae5df846..ba75e779 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -369,6 +369,18 @@ class GuiMainMenu(QMenuBar):
self.aImportFile.triggered.connect(self.theParent.importDocument)
self.docuMenu.addAction(self.aImportFile)
+ # 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.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 63a31795..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
+ 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)
@@ -458,6 +458,22 @@ class GuiMain(QMainWindow):
return True
+ def mergeDocuments(self):
+ """Merge multiple documents to one single new document.
+ """
+ if self.mainConf.showGUI:
+ dlgMerge = GuiDocMerge(self, self.theProject)
+ 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):
"""Pass on document action theAction to whatever document has
the focus. If no document has focus, the action is discarded.
diff --git a/nw/project/document.py b/nw/project/document.py
index 0cc0fa66..e2a46d65 100644
--- a/nw/project/document.py
+++ b/nw/project/document.py
@@ -59,7 +59,7 @@ class NWDoc():
if self.theItem.parHandle == self.theProject.trashRoot:
self.docEditable = False
- docDir, docFile = self._assemblePath(self.FILE_MN)
+ docDir, docFile = self.assemblePath(self.docHandle, self.FILE_MN)
self.fileLoc = path.join(docDir,docFile)
logger.debug("Opening document %s" % self.fileLoc)
dataDir = path.join(self.theProject.projPath, docDir)
@@ -92,7 +92,7 @@ class NWDoc():
if self.docHandle is None or not self.docEditable:
return False
- docDir, docFile = self._assemblePath(self.FILE_MN)
+ docDir, docFile = self.assemblePath(self.docHandle, self.FILE_MN)
logger.debug("Saving document %s" % path.join(docDir,docFile))
dataPath = path.join(self.theProject.projPath, docDir)
docPath = path.join(dataPath, docFile)
@@ -124,15 +124,32 @@ class NWDoc():
return True
- ##
- # Internal Functions
- ##
+ def deleteDocument(self, tHandle):
+ """Permanently delete a document source file and its backups
+ from the project data folder.
+ """
+ docDir, docFile = self.assemblePath(tHandle, self.FILE_MN)
+ dataPath = path.join(self.theProject.projPath, docDir)
+ chkList = []
+ chkList.append(path.join(dataPath, docFile))
+ chkList.append(path.join(dataPath,docFile[:-3]+"tmp"))
+ chkList.append(path.join(dataPath,docFile[:-3]+"bak"))
+ for chkFile in chkList:
+ if path.isfile(chkFile):
+ try:
+ unlink(chkFile)
+ logger.debug("Deleted: %s" % chkFile)
+ except Exception as e:
+ self.makeAlert(["Could not delete document file.",str(e)], nwAlert.ERROR)
+ return False
+ return True
- def _assemblePath(self, docExt):
- if self.docHandle is None:
+ @staticmethod
+ def assemblePath(tHandle, docExt):
+ if tHandle is None:
return None
- docDir = "data_"+self.docHandle[0]
- docFile = self.docHandle[1:13]+"_"+docExt
+ docDir = "data_"+tHandle[0]
+ docFile = tHandle[1:13]+"_"+docExt
return docDir, docFile
# END Class NWDoc
diff --git a/nw/project/project.py b/nw/project/project.py
index 041029a1..e6167eb9 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -13,7 +13,7 @@
import logging
import nw
-from os import path, mkdir, listdir
+from os import path, mkdir, listdir, unlink
from shutil import copyfile
from lxml import etree
from hashlib import sha256
@@ -452,9 +452,6 @@ class NWProject():
self.setProjectChanged(True)
return True
- def getSessionWordCount(self):
- return self.currWCount - self.lastWCount
-
def setStatusColours(self, newCols):
replaceMap = self.statusItems.setNewEntries(newCols)
if self.projTree is not None:
@@ -497,6 +494,9 @@ class NWProject():
logger.error("No tree item with handle %s" % str(tHandle))
return None
+ def getSessionWordCount(self):
+ return self.currWCount - self.lastWCount
+
def getRootItem(self, tHandle):
"""Iterate upwards in the tree until we find the item with
parent None, the root item. We do this with a for loop with a
@@ -505,10 +505,13 @@ class NWProject():
tItem = self.getItem(tHandle)
if tItem is not None:
for i in range(200):
- tHandle = tItem.parHandle
- tItem = self.getItem(tHandle)
- if tItem is None:
+ if tItem.parHandle is None:
return tHandle
+ else:
+ tHandle = tItem.parHandle
+ tItem = self.getItem(tHandle)
+ if tItem is None:
+ return tHandle
return None
def getProjectItems(self):
@@ -560,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