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 @@ + + + + + + image/svg+xml + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + 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