Rewrite Merge Tool (#1148)

This commit is contained in:
Veronica Berglyd Olsen
2022-10-11 22:02:26 +02:00
committed by GitHub
25 changed files with 1479 additions and 814 deletions
+2
View File
@@ -19,6 +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.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
@@ -28,6 +29,7 @@ from novelwriter.core.toodt import ToOdt
from novelwriter.core.tomd import ToMarkdown
__all__ = [
"DocMerger",
"countWords",
"NWDoc",
"NWProject",
+119
View File
@@ -0,0 +1,119 @@
"""
novelWriter Project Document Tools
====================================
A collection of tools to create and manipulate documents
File History:
Created: 2022-10-02 [2.0b1]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
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 logging
from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
class DocMerger:
def __init__(self, theProject):
self.theProject = theProject
self._error = ""
self._targetDoc = None
self._targetText = []
return
##
# Methods
##
def getError(self):
"""Return any collected errors.
"""
return self._error
def setTargetDoc(self, tHandle):
"""Set the target document for the merging. Calling this
function resets the class.
"""
self._targetDoc = tHandle
self._targetText = []
return
def newTargetDoc(self, srcHandle, docLabel):
"""Create a barnd new target document based on a source handle
and a new doc label. Calling this function resets the class.
"""
srcItem = self.theProject.tree[srcHandle]
if srcItem is None:
return None
newHandle = self.theProject.newFile(docLabel, srcItem.itemParent)
newItem = self.theProject.tree[newHandle]
newItem.setLayout(srcItem.itemLayout)
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
self._targetDoc = newHandle
self._targetText = []
return newHandle
def appendText(self, srcHandle, addComment, cmtPrefix):
"""Append text from an existing document to the text buffer.
"""
srcItem = self.theProject.tree[srcHandle]
if srcItem is None:
return False
inDoc = NWDoc(self.theProject, srcHandle)
docText = (inDoc.readDocument() or "").rstrip("\n")
if addComment:
docInfo = srcItem.describeMe("H0")
docSt, _ = srcItem.getImportStatus(incIcon=False)
cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n"
docText = cmtLine + docText
self._targetText.append(docText)
return True
def writeTargetDoc(self):
"""Write the accumulated text into the designated target
document, appending any existing text.
"""
if self._targetDoc is None:
return False
outDoc = NWDoc(self.theProject, self._targetDoc)
docText = (outDoc.readDocument() or "").rstrip("\n")
if docText:
self._targetText.insert(0, docText)
status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
if not status:
self._error = outDoc.getError()
return status
# END Class DocMerger
+3 -3
View File
@@ -292,16 +292,16 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
def getImportStatus(self):
def getImportStatus(self, incIcon=True):
"""Return the relevant importance or status label and icon for
the current item based on its class.
"""
if self.isNovelLike():
stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.icon(self._status)
stIcon = self.theProject.statusItems.icon(self._status) if incIcon else None
else:
stName = self.theProject.importItems.name(self._import)
stIcon = self.theProject.importItems.icon(self._import)
stIcon = self.theProject.importItems.icon(self._import) if incIcon else None
return stName, stIcon
##
+19 -2
View File
@@ -176,7 +176,7 @@ class NWProject():
self._projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def writeNewFile(self, tHandle, hLevel, isDocument):
def writeNewFile(self, tHandle, hLevel, isDocument, addText=""):
"""Write content to a new document after it is created. This
will not run if the file exists and is not empty.
"""
@@ -191,7 +191,7 @@ class NWProject():
return False
hshText = "#"*minmax(hLevel, 1, 4)
newText = f"{hshText} {tItem.itemName}\n\n"
newText = f"{hshText} {tItem.itemName}\n\n{addText}"
if tItem.isNovelLike() and isDocument:
tItem.setLayout(nwItemLayout.DOCUMENT)
else:
@@ -202,6 +202,23 @@ class NWProject():
return True
def removeItem(self, tHandle):
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
"""
if self._projTree.checkType(tHandle, nwItemType.FILE):
delDoc = NWDoc(self, tHandle)
if not delDoc.deleteDocument():
self.mainGui.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
], nwAlert.ERROR)
return False
self._projIndex.deleteHandle(tHandle)
del self._projTree[tHandle]
return True
def trashFolder(self):
"""Add the special trash root folder to the project.
"""
+89 -110
View File
@@ -1,10 +1,11 @@
"""
novelWriter GUI Doc Merge Tool
================================
GUI class for merging multiple documents to one document
novelWriter GUI Doc Merge Dialog
==================================
Custom dialog class for merging documents.
File History:
Created: 2020-01-23 [0.4.3]
Created: 2020-01-23 [0.4.3]
Rewritten: 2022-10-06 [2.0b1]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
@@ -26,169 +27,147 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import novelwriter
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
QListWidgetItem, QDialogButtonBox
QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel,
QListWidget, QListWidgetItem, QVBoxLayout,
)
from novelwriter.core import NWDoc
from novelwriter.enum import nwAlert, nwItemType
from novelwriter.gui.custom import QHelpLabel
from novelwriter.gui.custom import QHelpLabel, QSwitch
logger = logging.getLogger(__name__)
class GuiDocMerge(QDialog):
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
def __init__(self, mainGui, sHandle, itemList):
super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocMerge ...")
self.setObjectName("GuiDocMerge")
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
self.sourceItem = None
self.outerBox = QVBoxLayout()
self._data = {}
self.setWindowTitle(self.tr("Merge Documents"))
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
self.helpLabel = QHelpLabel(
self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText
)
self.helpLabel = QHelpLabel(self.tr(
"Drag and drop items to change the order, or uncheck to exclude."
), self.mainTheme.helpText)
iPx = self.mainTheme.baseIconSize
hSp = self.mainConf.pxInt(12)
vSp = self.mainConf.pxInt(8)
bSp = self.mainConf.pxInt(12)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
self.listBox.setIconSize(QSize(iPx, iPx))
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
self.listBox.setSelectionBehavior(QAbstractItemView.SelectRows)
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
# Merge Options
self.trashLabel = QLabel(self.tr("Move merged items to Trash"))
self.trashSwitch = QSwitch()
self.optBox = QGridLayout()
self.optBox.addWidget(self.trashLabel, 0, 0)
self.optBox.addWidget(self.trashSwitch, 0, 1)
self.optBox.setHorizontalSpacing(hSp)
self.optBox.setColumnStretch(2, 1)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doMerge)
self.buttonBox.rejected.connect(self._doClose)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.resetButton = self.buttonBox.addButton(QDialogButtonBox.Reset)
self.resetButton.clicked.connect(self._resetList)
# 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.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, itemList)
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.
def getData(self):
"""Return the user's choices.
"""
logger.verbose("GuiDocMerge merge button clicked")
finalOrder = []
finalItems = []
for i in range(self.listBox.count()):
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
item = self.listBox.item(i)
if item.checkState() == Qt.Checked:
finalItems.append(item.data(Qt.UserRole))
if len(finalOrder) == 0:
self.mainGui.makeAlert(self.tr(
"No source documents found. Nothing to do."
), nwAlert.ERROR)
return False
self._data["moveToTrash"] = self.trashSwitch.isChecked()
self._data["finalItems"] = finalItems
theText = ""
for tHandle in finalOrder:
inDoc = NWDoc(self.theProject, tHandle)
docText = inDoc.readDocument()
docErr = inDoc.getError()
if docText is None and docErr:
self.mainGui.makeAlert([
self.tr("Failed to open document file."), docErr
], nwAlert.ERROR)
if docText:
theText += docText.rstrip("\n")+"\n\n"
return self._data
if self.sourceItem is None:
self.mainGui.makeAlert(self.tr(
"No source folder selected. Nothing to do."
), nwAlert.ERROR)
return False
##
# Slots
##
srcItem = self.theProject.tree[self.sourceItem]
if srcItem is None:
self.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
return False
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
newItem = self.theProject.tree[nHandle]
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
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.mainGui.openDocument(nHandle, doScroll=True)
self._doClose()
return True
def _doClose(self):
"""Close the dialog window without doing anything.
def _resetList(self):
"""Reset the content of the list box to its original state.
"""
self.close()
logger.debug("Resetting list box content")
sHandle = self._data.get("sHandle", None)
itemList = self._data.get("origItems", [])
self._loadContent(sHandle, itemList)
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, itemList):
"""Load content from a given list of items.
"""
tHandle = self.mainGui.projView.getSelectedHandle()
self.sourceItem = tHandle
if tHandle is None:
return False
self._data = {}
self._data["sHandle"] = sHandle
self._data["origItems"] = itemList
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
return False
if nwItem.itemType is not nwItemType.FOLDER:
self.mainGui.makeAlert(self.tr(
"Element selected in the project tree must be a folder."
), nwAlert.ERROR)
return False
for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle):
newItem = QListWidgetItem()
nwItem = self.theProject.tree[sHandle]
if not nwItem.isFileType():
self.listBox.clear()
for tHandle in itemList:
nwItem = self.theProject.tree[tHandle]
if nwItem is None or not nwItem.isFileType():
continue
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
itemIcon = self.mainTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
)
newItem = QListWidgetItem()
newItem.setIcon(itemIcon)
newItem.setText(nwItem.itemName)
newItem.setData(Qt.UserRole, sHandle)
newItem.setData(Qt.UserRole, tHandle)
newItem.setCheckState(Qt.Checked)
self.listBox.addItem(newItem)
return True
return
# END Class GuiDocMerge
+7 -7
View File
@@ -202,7 +202,7 @@ class QConfigLayout(QGridLayout):
class QHelpLabel(QLabel):
def __init__(self, theText, textCol, fontSize=0.9):
QLabel.__init__(self, theText)
super().__init__(theText)
if isinstance(textCol, QColor):
qCol = textCol
@@ -377,7 +377,7 @@ class QSwitch(QAbstractButton):
class PagedDialog(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent=parent)
super().__init__(parent=parent)
self._tabBar = VerticalTabBar(self)
self._tabBar.setExpanding(False)
@@ -410,13 +410,13 @@ class PagedDialog(QDialog):
return
def addTab(self, widget, label):
"""Forwards the adding of tabs to the QTabWidget.
"""Forward the adding of tabs to the QTabWidget.
"""
self._tabBox.addTab(widget, label)
return
def addControls(self, buttonBar):
"""Adds a button bar to the dialog.
"""Add a button bar to the dialog.
"""
self._buttonBox.addWidget(buttonBar)
return
@@ -427,14 +427,14 @@ class PagedDialog(QDialog):
class VerticalTabBar(QTabBar):
def __init__(self, parent=None):
QTabBar.__init__(self, parent=parent)
super().__init__(parent=parent)
self._mW = novelwriter.CONFIG.pxInt(150)
return
def tabSizeHint(self, index):
"""Returns a transposed size hint for the rotated bar.
"""Return a transposed size hint for the rotated bar.
"""
tSize = QTabBar.tabSizeHint(self, index)
tSize = super().tabSizeHint(index)
tSize.transpose()
tSize.setWidth(min(tSize.width(), self._mW))
return tSize
+1 -6
View File
@@ -171,7 +171,7 @@ class GuiMainMenu(QMenuBar):
# Project > Delete
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.deleteItem(None))
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
self.projMenu.addAction(self.aDeleteItem)
# Project > Empty Trash
@@ -244,11 +244,6 @@ class GuiMainMenu(QMenuBar):
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
self.docuMenu.addAction(self.aImportFile)
# Document > Merge Documents
self.aMergeDocs = QAction(self.tr("Merge Folder to Document"), self)
self.aMergeDocs.triggered.connect(lambda: self.mainGui.mergeDocuments())
self.docuMenu.addAction(self.aMergeDocs)
# Document > Split Document
self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self)
self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument())
+1 -1
View File
@@ -1048,7 +1048,7 @@ class GuiOutlineDetails(QScrollArea):
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText(novIdx.title)
itemStatus, _ = nwItem.getImportStatus()
itemStatus, _ = nwItem.getImportStatus(incIcon=False)
self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(itemStatus)
+263 -158
View File
@@ -34,15 +34,15 @@ from time import time
from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel,
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget
)
from novelwriter.core import NWDoc
from novelwriter.core import DocMerger
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
from novelwriter.constants import nwHeaders, trConst, nwLabels
from novelwriter.dialogs.editlabel import GuiEditLabel
logger = logging.getLogger(__name__)
@@ -106,7 +106,7 @@ class GuiProjectView(QWidget):
self.renameTreeItem = self.projTree.renameTreeItem
self.getTreeFromHandle = self.projTree.getTreeFromHandle
self.emptyTrash = self.projTree.emptyTrash
self.deleteItem = self.projTree.deleteItem
self.requestDeleteItem = self.projTree.requestDeleteItem
self.setTreeItemValues = self.projTree.setTreeItemValues
self.propagateCount = self.projTree.propagateCount
self.getSelectedHandle = self.projTree.getSelectedHandle
@@ -334,7 +334,7 @@ class GuiProjectTree(QTreeWidget):
self.mainConf = novelwriter.CONFIG
self.projView = projView
self.mainGui = projView.mainGui
self.mainTheme = projView.mainGui.mainTheme
self.mainTheme = projView.mainGui.mainTheme
self.theProject = projView.mainGui.theProject
# Internal Variables
@@ -382,9 +382,8 @@ class GuiProjectTree(QTreeWidget):
trRoot = self.invisibleRootItem()
trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled)
# Set Multiple Selection by CTRL
# Disabled for now, until the merge files option has been added
# self.setSelectionMode(QAbstractItemView.ExtendedSelection)
# Set selection options
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
# Connect signals
@@ -603,7 +602,7 @@ class GuiProjectTree(QTreeWidget):
self.setTreeItemValues(tHandle)
self._alertTreeChange(tHandle, flush=False)
return
return True
def saveTreeOrder(self):
"""Build a list of the items in the project tree and send them
@@ -628,6 +627,42 @@ class GuiProjectTree(QTreeWidget):
theList = self._scanChildren(theList, theItem, 0)
return theList
def requestDeleteItem(self, tHandle=None):
"""Request an item deleted from the project tree. This function
can be called on any item, and will check whether to attempt a
permanent deletion or moving the item to Trash.
"""
if not self.mainGui.hasProject:
logger.error("No project open")
return False
if not self.hasFocus():
logger.info("Delete action blocked due to no widget focus")
return False
if tHandle is None:
tHandle = self.getSelectedHandle()
if tHandle is None:
logger.error("There is no item to delete")
return False
trashHandle = self.theProject.tree.trashRoot()
if tHandle == trashHandle:
logger.error("Cannot delete the Trash folder")
return False
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
return False
if self.theProject.tree.isTrash(tHandle) or nwItem.isRootType():
status = self.permanentlyDeleteItem(tHandle)
else:
status = self.moveItemToTrash(tHandle)
return status
def emptyTrash(self):
"""Permanently delete all documents in the Trash folder. This
function only asks for confirmation once, and calls the regular
@@ -662,41 +697,24 @@ class GuiProjectTree(QTreeWidget):
self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
)
if not msgYes:
logger.info("Action cancelled by user")
return False
logger.verbose("Deleting %d file(s) from Trash", nTrash)
for tHandle in reversed(self.getTreeFromHandle(trashHandle)):
if tHandle == trashHandle:
continue
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
self.permanentlyDeleteItem(tHandle, askFirst=False, flush=False)
if nTrash > 0:
self._alertTreeChange(trashHandle, flush=True)
return True
def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False):
"""Delete an item from the project tree. As a first step, files are
moved to the Trash folder. Permanent deletion is a second step. This
second step also deletes the item from the project object as well as
delete the files on disk. Root folders are deleted if they're empty
only, and the deletion is always permanent.
def moveItemToTrash(self, tHandle, askFirst=True, flush=True):
"""Move an item to Trash. Root folders cannot be moved to Trash,
so such a request is cancelled.
"""
if not self.mainGui.hasProject:
logger.error("No project open")
return False
if not self.hasFocus() and not bulkAction:
logger.info("Delete action blocked due to no widget focus")
return False
if tHandle is None:
tHandle = self.getSelectedHandle()
if tHandle is None:
logger.error("There is no item to delete")
return False
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.tree[tHandle]
@@ -704,87 +722,99 @@ class GuiProjectTree(QTreeWidget):
logger.error("Could not find tree item for deletion")
return False
if self.theProject.tree.isTrash(tHandle):
logger.error("Item is already in the Trash folder")
return False
if nwItemS.isRootType():
logger.error("Root folders cannot be moved to Trash")
return False
logger.debug("User requested file or folder '%s' move to Trash", tHandle)
trItemP = trItemS.parent()
trItemT = self._addTrashRoot()
if trItemP is None or trItemT is None:
logger.error("Could not delete item")
return False
if askFirst:
msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
)
if not msgYes:
logger.info("Action cancelled by user")
return False
wCount = self._getItemWordCount(tHandle)
autoFlush = not bulkAction
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex)
self._alertTreeChange(tHandle, flush=flush)
logger.debug("Moved item '%s' to Trash", tHandle)
return True
def permanentlyDeleteItem(self, tHandle, askFirst=True, flush=True):
"""Permanently delete a tree item from the project and the map.
Root items are handled a little different than other items.
"""
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.tree[tHandle]
if trItemS is None or nwItemS is None:
logger.error("Could not find tree item for deletion")
return False
if nwItemS.isRootType():
# Only an empty ROOT folder can be deleted
logger.debug("User requested a root folder '%s' deleted", tHandle)
tIndex = self.indexOfTopLevelItem(trItemS)
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle)
self._alertTreeChange(tHandle, flush=True)
else:
if trItemS.childCount() > 0:
self.mainGui.makeAlert(self.tr(
"Cannot delete root folder. It is not empty. "
"Recursive deletion is not supported. "
"Please delete the content first."
"Root folders can only be deleted when they are empty."
), nwAlert.ERROR)
return False
elif nwItemS.isFolderType() and trItemS.childCount() == 0:
# An empty FOLDER is just deleted without any further checks
logger.debug("User requested an empty folder '%s' deleted", tHandle)
logger.debug("Permanently deleting root folder '%s'", tHandle)
tIndex = self.indexOfTopLevelItem(trItemS)
self.takeTopLevelItem(tIndex)
self.theProject.removeItem(tHandle)
self._treeMap.pop(tHandle, None)
self._alertTreeChange(tHandle, flush=True)
else:
if askFirst:
msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Permanently delete '{0}'?").format(nwItemS.itemName)
)
if not msgYes:
logger.info("Action cancelled by user")
return False
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)
self._deleteTreeItem(tHandle)
self._alertTreeChange(tHandle, flush=autoFlush)
else:
# A populated FOLDER or a FILE requires confirmtation
logger.debug("User requested a file or folder '%s' deleted", tHandle)
trItemP = trItemS.parent()
trItemT = self._addTrashRoot()
if trItemP is None or trItemT is None:
logger.error("Could not delete item")
return False
for dHandle in reversed(itemList):
if self.mainGui.docEditor.docHandle() == dHandle:
self.mainGui.closeDocument()
self.theProject.removeItem(tHandle)
self._treeMap.pop(tHandle, None)
if self.theProject.tree.isTrash(tHandle):
# If the file is in the trash folder already, as the
# user if they want to permanently delete the file.
doPermanent = False
if not alreadyAsked:
msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Permanently delete '{0}'?").format(nwItemS.itemName)
)
if msgYes:
doPermanent = True
else:
doPermanent = True
if doPermanent:
logger.debug("Permanently deleting item with handle '%s'", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
for dHandle in reversed(self.getTreeFromHandle(tHandle)):
if self.mainGui.docEditor.docHandle() == dHandle:
self.mainGui.closeDocument()
self._deleteTreeItem(dHandle)
self._alertTreeChange(tHandle, flush=autoFlush)
self.projView.wordCountsChanged.emit()
else:
# The item is not already in the trash folder, so we
# move it there.
msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
)
if msgYes:
logger.debug("Moving item '%s' to trash", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex)
self._alertTreeChange(tHandle, flush=autoFlush)
self._alertTreeChange(tHandle, flush=flush)
self.projView.wordCountsChanged.emit()
return True
@@ -885,7 +915,9 @@ class GuiProjectTree(QTreeWidget):
dstItem = self._lastMove.get("parent", None)
dstIndex = self._lastMove.get("index", None)
if srcItem is None or dstItem is None or dstIndex is None:
srcOK = isinstance(srcItem, QTreeWidgetItem)
dstOk = isinstance(dstItem, QTreeWidgetItem)
if not srcOK or not dstOk or dstIndex is None:
logger.verbose("No tree move to undo")
return False
@@ -981,7 +1013,7 @@ class GuiProjectTree(QTreeWidget):
return
@pyqtSlot("QTreeWidgetItem*", int)
def _treeDoubleClick(self, tItem, colNo):
def _treeDoubleClick(self, trItem, colNo):
"""Capture a double-click event and either request the document
for editing if it is a file, or expand/close the node it is not.
"""
@@ -996,9 +1028,7 @@ class GuiProjectTree(QTreeWidget):
if tItem.isFileType():
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "")
else:
trItem = self._getTreeItem(tHandle)
if trItem is not None:
trItem.setExpanded(not trItem.isExpanded())
trItem.setExpanded(not trItem.isExpanded())
return
@@ -1077,43 +1107,58 @@ class GuiProjectTree(QTreeWidget):
lambda n, key=key: self._changeItemImport(tHandle, key)
)
if isFile and tItem.documentAllowed():
if tItem.isNoteLayout():
ctxMenu.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
),
# Transform Item
# ==============
if not isRoot:
mTrans = ctxMenu.addMenu(self.tr("Transform"))
trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
isDocFile = isFile and tItem.isDocumentLayout()
isNoteFile = isFile and tItem.isNoteLayout()
if (isNoteFile or isFolder) and tItem.documentAllowed():
mTrans.addAction(
self.tr("Convert to {0}").format(trDoc),
lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT)
)
else:
ctxMenu.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
),
if isDocFile or isFolder:
mTrans.addAction(
self.tr("Convert to {0}").format(trNote),
lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE)
)
elif isFolder:
if tItem.documentAllowed():
ctxMenu.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
),
lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT)
if hasChild and isFile:
mTrans.addAction(
self.tr("Merge Child Items into Self"),
lambda: self._mergeDocuments(tHandle, False)
)
ctxMenu.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
),
lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE)
)
mTrans.addAction(
self.tr("Merge Child Items into New"),
lambda: self._mergeDocuments(tHandle, True)
)
if hasChild and isFolder:
mTrans.addAction(
self.tr("Combine Documents in Folder"),
lambda: self._mergeDocuments(tHandle, True)
)
if isFile:
mTrans.addAction(
self.tr("Split Document by Header"),
lambda: self._splitDocument(tHandle)
)
# Expand/Collapse/Delete
# ======================
ctxMenu.addSeparator()
# Expand/Collapse
# ===============
if hasChild:
ctxMenu.addSeparator()
ctxMenu.addAction(
self.tr("Expand All"),
lambda: self.setExpandedFromHandle(tHandle, True)
@@ -1123,18 +1168,16 @@ class GuiProjectTree(QTreeWidget):
lambda: self.setExpandedFromHandle(tHandle, False)
)
# Delete Item
# ===========
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
ctxMenu.addAction(
self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle)
self.tr("Delete Permanently"), lambda: self.permanentlyDeleteItem(tHandle)
)
else:
ctxMenu.addAction(
self.tr("Move to Trash"), lambda: self.deleteItem(tHandle)
self.tr("Move to Trash"), lambda: self.moveItemToTrash(tHandle)
)
# Show Context Menu
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
return True
@@ -1255,23 +1298,6 @@ class GuiProjectTree(QTreeWidget):
"""
return self._treeMap.get(tHandle, None)
def _deleteTreeItem(self, tHandle):
"""Permanently delete a tree item from the project and the map.
"""
if self.theProject.tree.checkType(tHandle, nwItemType.FILE):
delDoc = NWDoc(self.theProject, tHandle)
if not delDoc.deleteDocument():
self.mainGui.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
], nwAlert.ERROR)
return False
self.theProject.index.deleteHandle(tHandle)
del self.theProject.tree[tHandle]
self._treeMap.pop(tHandle, None)
return True
def _toggleItemExported(self, tHandle):
"""Toggle the exported status of an item.
"""
@@ -1355,6 +1381,85 @@ class GuiProjectTree(QTreeWidget):
logger.info("Folder conversion cancelled")
return
def _mergeDocuments(self, tHandle, newFile):
"""Merge an item's child documents into a single document.
"""
logger.info("Request to merge items under handle '%s'", tHandle)
itemList = self.getTreeFromHandle(tHandle)
tItem = self.theProject.tree[tHandle]
if tItem is None:
return False
if tItem.isRootType():
logger.error("Cannot merge root item")
return False
if not newFile:
itemList.remove(tHandle)
dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
dlgMerge.exec_()
if dlgMerge.result() == QDialog.Accepted:
mrgData = dlgMerge.getData()
mrgList = mrgData.get("finalItems", [])
if not mrgList:
self.mainGui.makeAlert([
self.tr("No documents selected for merging.")
], nwAlert.INFO)
return False
# Save the open document first, in case it's part of merge
self.mainGui.saveDocument()
# Create merge object, and append docs
docMerger = DocMerger(self.theProject)
mLabel = self.tr("Merged")
if newFile:
docLabel = f"[{mLabel}] {tItem.itemName}"
mHandle = docMerger.newTargetDoc(tHandle, docLabel)
elif tItem.isFileType():
docMerger.setTargetDoc(tHandle)
mHandle = tHandle
else:
return False
for sHandle in mrgList:
docMerger.appendText(sHandle, True, mLabel)
if not docMerger.writeTargetDoc():
self.mainGui.makeAlert([
self.tr("Could not save document."), docMerger.getError()
], nwAlert.ERROR)
return False
if newFile:
self.mainGui.projView.revealNewTreeItem(mHandle, tHandle)
self.theProject.index.reIndexHandle(mHandle)
self.mainGui.openDocument(mHandle, doScroll=True)
if mrgData.get("moveToTrash", False):
for sHandle in reversed(mrgData.get("finalItems", [])):
trItem = self._getTreeItem(sHandle)
if isinstance(trItem, QTreeWidgetItem) and trItem.childCount() == 0:
self.moveItemToTrash(sHandle, askFirst=False, flush=False)
self._alertTreeChange(mHandle, flush=True)
self.projView.wordCountsChanged.emit()
else:
logger.info("Action cancelled by user")
return False
return True
def _splitDocument(self, tHandle):
return
def _scanChildren(self, theList, tItem, tIndex):
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
@@ -1380,7 +1485,7 @@ class GuiProjectTree(QTreeWidget):
"""
tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent
newItem = QTreeWidgetItem([""]*4)
newItem = QTreeWidgetItem()
newItem.setText(self.C_NAME, "")
newItem.setText(self.C_COUNT, "0")
@@ -1455,10 +1560,10 @@ class GuiProjectTree(QTreeWidget):
if tHandle is None:
return
tItem = self.theProject.tree[tHandle]
if tItem is None:
if tHandle not in self.theProject.tree:
return
tItem = self.theProject.tree[tHandle]
if tItem.isRootType():
self.projView.rootFolderChanged.emit(tHandle)
+2 -14
View File
@@ -44,8 +44,8 @@ from novelwriter.gui import (
GuiViewsBar
)
from novelwriter.dialogs import (
GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
GuiAbout, GuiDocSplit, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
GuiProjectSettings, GuiUpdates, GuiWordList
)
from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
@@ -745,18 +745,6 @@ class GuiMain(QMainWindow):
return True
def mergeDocuments(self):
"""Merge multiple documents to one single new document.
"""
if not self.hasProject:
logger.error("No project open")
return False
dlgMerge = GuiDocMerge(self)
dlgMerge.exec_()
return True
def splitDocument(self):
"""Split a single document into multiple documents.
"""
+12 -6
View File
@@ -190,14 +190,20 @@ def mockRnd(monkeypatch):
from 0. This one will generate status/importance flags and handles
in a predictable sequence.
"""
def rnd(n):
for x in range(n):
yield x
class MockRnd:
gen = rnd(1000)
monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
def __init__(self):
self.reset()
return
def _rnd(self, n):
for x in range(n):
yield x
def reset(self):
gen = self._rnd(1000)
monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
return MockRnd()
##
@@ -0,0 +1,33 @@
%%~name: Chapter 1
%%~path: 0000000000008/0000000000010
%%~kind: NOVEL/DOCUMENT
## Chapter 1
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.
Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
% Merge Novel Document: Scene 1.1 [New]
### Scene 1.1
Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
% Merge Novel Document: Scene 1.2 [New]
### Scene 1.2
Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
% Merge Novel Document: Scene 1.3 [New]
### Scene 1.3
Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
Integer ac gravida quam. Quisque eleifend nisl nec pretium tincidunt. Quisque sollicitudin nisi in hendrerit scelerisque. Sed ornare nisl lacus, sit amet consectetur lectus egestas et. Vivamus nec arcu lorem. Donec rhoncus, purus a porta accumsan, nunc lectus iaculis libero, et fringilla tellus augue et velit. Integer varius felis scelerisque, vulputate tellus eu, laoreet justo. Suspendisse sit amet sem vehicula, auctor odio sed, aliquet enim. In ac tortor sed tortor fringilla elementum. Nulla non odio at magna vulputate scelerisque. Nam elementum diam eu rutrum scelerisque. Sed fermentum, felis quis vulputate fermentum, libero metus sollicitudin est, in faucibus purus nulla non dolor. Ut vitae felis porta, feugiat nunc et, bibendum neque. Nullam nec lorem nec metus ullamcorper malesuada ut a nisl. Etiam eget tristique dui. Nulla sed mi finibus, venenatis tellus non, maximus enim.
@@ -0,0 +1,35 @@
%%~name: All of Chapter 1
%%~path: 0000000000008/0000000000014
%%~kind: NOVEL/DOCUMENT
% Merge Novel Document: Chapter 1 [New]
## Chapter 1
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.
Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
% Merge Novel Document: Scene 1.1 [New]
### Scene 1.1
Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
% Merge Novel Document: Scene 1.2 [New]
### Scene 1.2
Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
% Merge Novel Document: Scene 1.3 [New]
### Scene 1.3
Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
Integer ac gravida quam. Quisque eleifend nisl nec pretium tincidunt. Quisque sollicitudin nisi in hendrerit scelerisque. Sed ornare nisl lacus, sit amet consectetur lectus egestas et. Vivamus nec arcu lorem. Donec rhoncus, purus a porta accumsan, nunc lectus iaculis libero, et fringilla tellus augue et velit. Integer varius felis scelerisque, vulputate tellus eu, laoreet justo. Suspendisse sit amet sem vehicula, auctor odio sed, aliquet enim. In ac tortor sed tortor fringilla elementum. Nulla non odio at magna vulputate scelerisque. Nam elementum diam eu rutrum scelerisque. Sed fermentum, felis quis vulputate fermentum, libero metus sollicitudin est, in faucibus purus nulla non dolor. Ut vitae felis porta, feugiat nunc et, bibendum neque. Nullam nec lorem nec metus ullamcorper malesuada ut a nisl. Etiam eget tristique dui. Nulla sed mi finibus, venenatis tellus non, maximus enim.
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
<novelWriterXML appVersion="2.0-beta1" hexVersion="0x020000b1" fileVersion="1.4" timeStamp="2022-10-11 21:40:10">
<project>
<name>New Project</name>
<title>New Novel</title>
@@ -17,9 +17,9 @@
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>2</lastWordCount>
<lastWordCount>4</lastWordCount>
<novelWordCount>1</novelWordCount>
<notesWordCount>1</notesWordCount>
<notesWordCount>3</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
@@ -29,62 +29,62 @@
<section></section>
</titleFormat>
<status>
<entry key="s000008" count="7" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
<entry key="s000000" count="7" red="100" green="100" blue="100">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s000002" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="i00000c" count="4" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
<entry key="i000004" count="4" red="100" green="100" blue="100">New</entry>
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="11">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
<name status="s000000" import="i000004">Novel</name>
</item>
<item handle="0000000000011" parent="None" root="0000000000011" order="0" type="ROOT" class="PLOT">
<item handle="0000000000009" parent="None" root="0000000000009" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
<name status="s000000" import="i000004">Plot</name>
</item>
<item handle="0000000000012" parent="None" root="0000000000012" order="0" type="ROOT" class="CHARACTER">
<item handle="000000000000a" parent="None" root="000000000000a" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
<name status="s000000" import="i000004">Characters</name>
</item>
<item handle="0000000000013" parent="None" root="0000000000013" order="0" type="ROOT" class="WORLD">
<item handle="000000000000b" parent="None" root="000000000000b" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">World</name>
<name status="s000000" import="i000004">World</name>
</item>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
<name status="s000000" import="i000004" exported="True">Title Page</name>
</item>
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<item handle="000000000000d" parent="0000000000008" root="0000000000008" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">New Chapter</name>
<name status="s000000" import="i000004">New Chapter</name>
</item>
<item handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
<name status="s000000" import="i000004" exported="True">New Chapter</name>
</item>
<item handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Scene</name>
<name status="s000000" import="i000004" exported="True">New Scene</name>
</item>
<item handle="0000000000028" parent="0000000000015" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<item handle="0000000000020" parent="0000000000008" root="0000000000008" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Stuff</name>
<name status="s000000" import="i000004">Stuff</name>
</item>
<item handle="0000000000029" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="0000000000021" parent="0000000000020" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="5" wordCount="1" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Hello</name>
<name status="s000000" import="i000004" exported="True">Hello</name>
</item>
<item handle="000000000002a" parent="0000000000012" root="0000000000012" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="4" wordCount="1" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Jane</name>
<item handle="0000000000022" parent="000000000000a" root="000000000000a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="11" wordCount="3" paraCount="1" cursorPos="0"/>
<name status="s000000" import="i000004" exported="True">Jane</name>
</item>
</content>
</novelWriterXML>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
<novelWriterXML appVersion="2.0-beta1" hexVersion="0x020000b1" fileVersion="1.4" timeStamp="2022-10-11 21:30:35">
<project>
<name>New Project</name>
<title>New Novel</title>
@@ -29,82 +29,82 @@
<section></section>
</titleFormat>
<status>
<entry key="s000008" count="6" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
<entry key="s000000" count="6" red="100" green="100" blue="100">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s000002" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="i00000c" count="10" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
<entry key="i000004" count="10" red="100" green="100" blue="100">New</entry>
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="16">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
<name status="s000000" import="i000004">Novel</name>
</item>
<item handle="0000000000011" parent="None" root="0000000000011" order="0" type="ROOT" class="PLOT">
<item handle="0000000000009" parent="None" root="0000000000009" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
<name status="s000000" import="i000004">Plot</name>
</item>
<item handle="0000000000012" parent="None" root="0000000000012" order="0" type="ROOT" class="CHARACTER">
<item handle="000000000000a" parent="None" root="000000000000a" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
<name status="s000000" import="i000004">Characters</name>
</item>
<item handle="0000000000013" parent="None" root="0000000000013" order="0" type="ROOT" class="WORLD">
<item handle="000000000000b" parent="None" root="000000000000b" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">World</name>
<name status="s000000" import="i000004">World</name>
</item>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
<name status="s000000" import="i000004" exported="True">Title Page</name>
</item>
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<item handle="000000000000d" parent="0000000000008" root="0000000000008" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">New Chapter</name>
<name status="s000000" import="i000004">New Chapter</name>
</item>
<item handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
<name status="s000000" import="i000004" exported="True">New Chapter</name>
</item>
<item handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Scene</name>
<name status="s000000" import="i000004" exported="True">New Scene</name>
</item>
<item handle="0000000000028" parent="None" root="0000000000028" order="0" type="ROOT" class="NOVEL">
<item handle="0000000000020" parent="None" root="0000000000020" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
<name status="s000000" import="i000004">Novel</name>
</item>
<item handle="0000000000029" parent="None" root="0000000000029" order="0" type="ROOT" class="PLOT">
<item handle="0000000000021" parent="None" root="0000000000021" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
<name status="s000000" import="i000004">Plot</name>
</item>
<item handle="000000000002a" parent="None" root="000000000002a" order="0" type="ROOT" class="CHARACTER">
<item handle="0000000000022" parent="None" root="0000000000022" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
<name status="s000000" import="i000004">Characters</name>
</item>
<item handle="000000000002b" parent="None" root="000000000002b" order="0" type="ROOT" class="WORLD">
<item handle="0000000000023" parent="None" root="0000000000023" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Locations</name>
<name status="s000000" import="i000004">Locations</name>
</item>
<item handle="000000000002c" parent="None" root="000000000002c" order="0" type="ROOT" class="TIMELINE">
<item handle="0000000000024" parent="None" root="0000000000024" order="0" type="ROOT" class="TIMELINE">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Timeline</name>
<name status="s000000" import="i000004">Timeline</name>
</item>
<item handle="000000000002d" parent="None" root="000000000002d" order="0" type="ROOT" class="OBJECT">
<item handle="0000000000025" parent="None" root="0000000000025" order="0" type="ROOT" class="OBJECT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Objects</name>
<name status="s000000" import="i000004">Objects</name>
</item>
<item handle="000000000002e" parent="None" root="000000000002e" order="0" type="ROOT" class="CUSTOM">
<item handle="0000000000026" parent="None" root="0000000000026" order="0" type="ROOT" class="CUSTOM">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Custom</name>
<name status="s000000" import="i000004">Custom</name>
</item>
<item handle="000000000002f" parent="None" root="000000000002f" order="0" type="ROOT" class="CUSTOM">
<item handle="0000000000027" parent="None" root="0000000000027" order="0" type="ROOT" class="CUSTOM">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Custom</name>
<name status="s000000" import="i000004">Custom</name>
</item>
</content>
</novelWriterXML>
+120
View File
@@ -0,0 +1,120 @@
"""
novelWriter Project Document Tools Tester
===========================================
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
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 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
@pytest.mark.core
def testCoreDocTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRnd, ipsumText):
"""Test the DocMerger utility.
"""
theProject = NWProject(mockGUI)
mockRnd.reset()
buildTestProject(theProject, fncDir)
# Create File to Merge
# ====================
hChapter1 = theProject.newFile("Chapter 1", C.hNovelRoot)
hSceneOne11 = theProject.newFile("Scene 1.1", hChapter1)
hSceneOne12 = theProject.newFile("Scene 1.2", hChapter1)
hSceneOne13 = theProject.newFile("Scene 1.3", hChapter1)
docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
docText3 = "\n\n".join(ipsumText[2:4]) + "\n\n"
docText4 = "\n\n".join(ipsumText[3:5]) + "\n\n"
theProject.writeNewFile(hChapter1, 2, True, docText1)
theProject.writeNewFile(hSceneOne11, 3, True, docText2)
theProject.writeNewFile(hSceneOne12, 3, True, docText3)
theProject.writeNewFile(hSceneOne13, 3, True, docText4)
# Basic Checks
# ============
docMerger = DocMerger(theProject)
# No writing without a target set
assert docMerger.writeTargetDoc() is False
# Cannot append invalid handle
assert docMerger.appendText(C.hInvalid, True, "Merge") is False
# Cannot create new target from invalid handle
assert docMerger.newTargetDoc(C.hInvalid, "Test") is None
# Merge to New
# ============
saveFile = os.path.join(fncDir, "content", "0000000000014.nwd")
testFile = os.path.join(outDir, "coreDocTools_DocMerger_0000000000014.nwd")
compFile = os.path.join(refDir, "coreDocTools_DocMerger_0000000000014.nwd")
assert docMerger.newTargetDoc(hChapter1, "All of Chapter 1") == "0000000000014"
assert docMerger.appendText(hChapter1, True, "Merge") is True
assert docMerger.appendText(hSceneOne11, True, "Merge") is True
assert docMerger.appendText(hSceneOne12, True, "Merge") is True
assert docMerger.appendText(hSceneOne13, True, "Merge") is True
# Block writing and check error handling
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert docMerger.writeTargetDoc() is False
assert not os.path.isfile(saveFile)
assert docMerger.getError() != ""
# Write properly, and compare
assert docMerger.writeTargetDoc() is True
copyfile(saveFile, testFile)
assert cmpFiles(testFile, compFile)
# Merge into Existing
# ===================
saveFile = os.path.join(fncDir, "content", "0000000000010.nwd")
testFile = os.path.join(outDir, "coreDocTools_DocMerger_0000000000010.nwd")
compFile = os.path.join(refDir, "coreDocTools_DocMerger_0000000000010.nwd")
docMerger.setTargetDoc(hChapter1)
assert docMerger.appendText(hSceneOne11, True, "Merge") is True
assert docMerger.appendText(hSceneOne12, True, "Merge") is True
assert docMerger.appendText(hSceneOne13, True, "Merge") is True
assert docMerger.writeTargetDoc() is True
copyfile(saveFile, testFile)
assert cmpFiles(testFile, compFile)
# Just for debugging
docMerger.writeTargetDoc()
# END Test testCoreDocTools_DocMerger
+68 -24
View File
@@ -22,11 +22,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import pytest
from lxml import etree
from shutil import copyfile
from zipfile import ZipFile
from lxml import etree
from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE
from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE, C
from mock import causeOSError
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@@ -256,6 +256,7 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx")
theProject = NWProject(mockGUI)
mockRnd.reset()
buildTestProject(theProject, fncDir)
assert theProject.setProjectPath(fncDir) is True
@@ -263,14 +264,14 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
assert isinstance(theProject.newRoot(nwItemClass.NOVEL), str)
assert isinstance(theProject.newRoot(nwItemClass.PLOT), str)
assert isinstance(theProject.newRoot(nwItemClass.CHARACTER), str)
assert isinstance(theProject.newRoot(nwItemClass.WORLD), str)
assert isinstance(theProject.newRoot(nwItemClass.TIMELINE), str)
assert isinstance(theProject.newRoot(nwItemClass.OBJECT), str)
assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str)
assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str)
assert theProject.newRoot(nwItemClass.NOVEL) == "0000000000020"
assert theProject.newRoot(nwItemClass.PLOT) == "0000000000021"
assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000022"
assert theProject.newRoot(nwItemClass.WORLD) == "0000000000023"
assert theProject.newRoot(nwItemClass.TIMELINE) == "0000000000024"
assert theProject.newRoot(nwItemClass.OBJECT) == "0000000000025"
assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000026"
assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000027"
assert theProject.projChanged is True
assert theProject.saveProject() is True
@@ -280,11 +281,30 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
assert theProject.projChanged is False
# Delete the new items
assert theProject.removeItem("0000000000020") is True
assert theProject.removeItem("0000000000021") is True
assert theProject.removeItem("0000000000022") is True
assert theProject.removeItem("0000000000023") is True
assert theProject.removeItem("0000000000024") is True
assert theProject.removeItem("0000000000025") is True
assert theProject.removeItem("0000000000026") is True
assert theProject.removeItem("0000000000027") is True
assert "0000000000020" not in theProject.tree
assert "0000000000021" not in theProject.tree
assert "0000000000022" not in theProject.tree
assert "0000000000023" not in theProject.tree
assert "0000000000024" not in theProject.tree
assert "0000000000025" not in theProject.tree
assert "0000000000026" not in theProject.tree
assert "0000000000027" not in theProject.tree
# END Test testCoreProject_NewRoot
@pytest.mark.core
def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd):
def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd):
"""Check that new files can be added to the project.
"""
projFile = os.path.join(fncDir, "nwProject.nwx")
@@ -292,6 +312,7 @@ def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd):
compFile = os.path.join(refDir, "coreProject_NewFileFolder_nwProject.nwx")
theProject = NWProject(mockGUI)
mockRnd.reset()
buildTestProject(theProject, fncDir)
assert theProject.setProjectPath(fncDir) is True
@@ -304,35 +325,58 @@ def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd):
assert theProject.newFile("New File", "1234567890abc") is None
# Add files properly
assert theProject.newFolder("Stuff", "0000000000015") == "0000000000028"
assert theProject.newFile("Hello", "0000000000015") == "0000000000029"
assert theProject.newFile("Jane", "0000000000012") == "000000000002a"
assert theProject.newFolder("Stuff", C.hNovelRoot) == "0000000000020"
assert theProject.newFile("Hello", "0000000000020") == "0000000000021"
assert theProject.newFile("Jane", C.hCharRoot) == "0000000000022"
assert "0000000000028" in theProject.tree
assert "0000000000029" in theProject.tree
assert "000000000002a" in theProject.tree
assert "0000000000020" in theProject.tree
assert "0000000000021" in theProject.tree
assert "0000000000022" in theProject.tree
# Write to file, failed
assert theProject.writeNewFile("blabla", 1, True) is False # Not a handle
assert theProject.writeNewFile("0000000000028", 1, True) is False # Not a file
assert theProject.writeNewFile("0000000000014", 1, True) is False # Already has content
assert theProject.writeNewFile("0000000000020", 1, True) is False # Not a file
assert theProject.writeNewFile(C.hTitlePage, 1, True) is False # Already has content
# Write to file, success
assert theProject.writeNewFile("0000000000029", 2, True) is True
assert NWDoc(theProject, "0000000000029").readDocument() == "## Hello\n\n"
assert theProject.writeNewFile("0000000000021", 2, True) is True
assert NWDoc(theProject, "0000000000021").readDocument() == "## Hello\n\n"
assert theProject.writeNewFile("000000000002a", 1, False) is True
assert NWDoc(theProject, "000000000002a").readDocument() == "# Jane\n\n"
# Write to file with additional text, success
assert theProject.writeNewFile("0000000000022", 1, False, "Hi Jane\n\n") is True
assert NWDoc(theProject, "0000000000022").readDocument() == "# Jane\n\nHi Jane\n\n"
# Save, close and check
assert theProject.projChanged is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
assert theProject.projChanged is False
# Delete new file, but block access
with monkeypatch.context() as mp:
mp.setattr("os.unlink", causeOSError)
assert theProject.removeItem("0000000000021") is False
assert "0000000000021" in theProject.tree
# Delete new files and folders
assert os.path.isfile(os.path.join(fncDir, "content", "0000000000022.nwd"))
assert os.path.isfile(os.path.join(fncDir, "content", "0000000000021.nwd"))
assert theProject.removeItem("0000000000022") is True
assert theProject.removeItem("0000000000021") is True
assert theProject.removeItem("0000000000020") is True
assert not os.path.isfile(os.path.join(fncDir, "content", "0000000000022.nwd"))
assert not os.path.isfile(os.path.join(fncDir, "content", "0000000000021.nwd"))
assert "0000000000020" not in theProject.tree
assert "0000000000021" not in theProject.tree
assert "0000000000022" not in theProject.tree
assert theProject.closeProject() is True
# END Test testCoreProject_NewFileFolder
+40 -127
View File
@@ -19,17 +19,14 @@ 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 buildTestProject, C
from PyQt5.QtWidgets import QAction, QMessageBox
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
from novelwriter.enum import nwItemType, nwWidget
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
from novelwriter.core.tree import NWTree
from novelwriter.dialogs import GuiDocMerge
@pytest.mark.gui
@@ -39,142 +36,58 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
# Create a new project
buildTestProject(nwGUI, fncProj)
# Handles for new objects
hNovelRoot = "0000000000008"
hChapterDir = "000000000000d"
hChapterOne = "000000000000e"
hSceneOne = "000000000000f"
hSceneTwo = "0000000000010"
hSceneThree = "0000000000011"
hSceneFour = "0000000000012"
hMergedDoc = "0000000000023"
# Add Project Content
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
assert nwGUI.saveProject() is True
assert nwGUI.closeProject() is True
tChapterOne = "## Chapter One\n\n% Chapter one comment\n"
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."
contentDir = os.path.join(fncProj, "content")
writeFile(os.path.join(contentDir, hChapterOne+".nwd"), tChapterOne)
writeFile(os.path.join(contentDir, hSceneOne+".nwd"), tSceneOne)
writeFile(os.path.join(contentDir, hSceneTwo+".nwd"), tSceneTwo)
writeFile(os.path.join(contentDir, hSceneThree+".nwd"), tSceneThree)
writeFile(os.path.join(contentDir, hSceneFour+".nwd"), tSceneFour)
assert nwGUI.openProject(fncProj) is True
# Open the Merge tool
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None)
nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiDocMerge") is not None, timeout=1000)
nwMerge = getGuiItem("GuiDocMerge")
assert isinstance(nwMerge, GuiDocMerge)
# Check that the dialog kan handle invalid items
nwMerge = GuiDocMerge(nwGUI, C.hInvalid, [C.hInvalid])
qtbot.addWidget(nwMerge)
nwMerge.show()
qtbot.wait(50)
# Populate List
# =============
nwMerge.listBox.clear()
assert nwMerge.listBox.count() == 0
nwMerge.reject()
# No item selected
nwGUI.projView.projTree.clearSelection()
assert nwMerge._populateList() is False
assert nwMerge.listBox.count() == 0
# Load items from chapter dir
nwMerge = GuiDocMerge(nwGUI, C.hChapterDir, [C.hChapterDir, C.hChapterDoc, C.hSceneDoc])
qtbot.addWidget(nwMerge)
nwMerge.show()
# Non-existing item
with monkeypatch.context() as mp:
mp.setattr(NWTree, "__getitem__", lambda *a: None)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
assert nwMerge._populateList() is False
assert nwMerge.listBox.count() == 0
assert nwMerge.listBox.count() == 2
# Select a non-folder
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterOne).setSelected(True)
assert nwMerge._populateList() is False
assert nwMerge.listBox.count() == 0
itemOne = nwMerge.listBox.item(0)
itemTwo = nwMerge.listBox.item(1)
# Select the chapter folder
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
assert nwMerge._populateList() is True
assert nwMerge.listBox.count() == 5
assert itemOne.data(Qt.UserRole) == C.hChapterDoc
assert itemTwo.data(Qt.UserRole) == C.hSceneDoc
# Merge Documents
# ===============
assert itemOne.checkState() == Qt.Checked
assert itemTwo.checkState() == Qt.Checked
# First, a successful merge
with monkeypatch.context() as mp:
mp.setattr(GuiDocMerge, "_doClose", lambda *a: None)
assert nwMerge._doMerge() is True
assert nwGUI.saveProject() is True
mergedFile = os.path.join(contentDir, hMergedDoc+".nwd")
assert os.path.isfile(mergedFile)
assert readFile(mergedFile) == (
"%%%%~name: New Chapter\n"
"%%%%~path: %s/%s\n"
"%%%%~kind: NOVEL/DOCUMENT\n"
"%s\n\n"
"%s\n\n"
"%s\n\n"
"%s\n\n"
"%s\n\n"
) % (
hNovelRoot,
hMergedDoc,
tChapterOne.strip(),
tSceneOne.strip(),
tSceneTwo.strip(),
tSceneThree.strip(),
tSceneFour.strip(),
)
data = nwMerge.getData()
assert data["sHandle"] == C.hChapterDir
assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc]
assert data["moveToTrash"] is False
assert data["finalItems"] == [C.hChapterDoc, C.hSceneDoc]
# OS error
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert nwMerge._doMerge() is False
# Uncheck second item and toggle trash switch
itemTwo.setCheckState(Qt.Unchecked)
nwMerge.trashSwitch.setChecked(True)
# Can't find the source item
with monkeypatch.context() as mp:
mp.setattr(NWTree, "__getitem__", lambda *a: None)
assert nwMerge._doMerge() is False
data = nwMerge.getData()
assert data["sHandle"] == C.hChapterDir
assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc]
assert data["moveToTrash"] is True
assert data["finalItems"] == [C.hChapterDoc]
# No source handle set
nwMerge.sourceItem = None
assert nwMerge._doMerge() is False
# Restore default values
nwMerge._resetList()
# No documents to merge
nwMerge.listBox.clear()
assert nwMerge._doMerge() is False
data = nwMerge.getData()
assert data["sHandle"] == C.hChapterDir
assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc]
assert data["moveToTrash"] is True
assert data["finalItems"] == [C.hChapterDoc, C.hSceneDoc]
# Close up
nwMerge._doClose()
# qtbot.stopForInteraction()
# qtbot.stop()
# END Test testDlgMerge_Main
+21 -22
View File
@@ -23,7 +23,7 @@ import os
import pytest
from shutil import copyfile
from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile
from tools import C, cmpFiles, buildTestProject, XML_IGNORE, writeFile
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox, QInputDialog
@@ -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.mergeDocuments() is False
assert nwGUI.splitDocument() is False
assert nwGUI.openSelectedItem() is False
assert nwGUI.editItemLabel() is False
@@ -217,14 +216,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert nwGUI.theProject.spellCheck is False
# Check that tree items have been created
assert nwGUI.projView.projTree._getTreeItem("0000000000008") is not None
assert nwGUI.projView.projTree._getTreeItem("0000000000009") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000a") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000b") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000c") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000d") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000e") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None
assert nwGUI.projView.projTree._getTreeItem(C.hNovelRoot) is not None
assert nwGUI.projView.projTree._getTreeItem(C.hPlotRoot) is not None
assert nwGUI.projView.projTree._getTreeItem(C.hCharRoot) is not None
assert nwGUI.projView.projTree._getTreeItem(C.hWorldRoot) is not None
assert nwGUI.projView.projTree._getTreeItem(C.hTitlePage) is not None
assert nwGUI.projView.projTree._getTreeItem(C.hChapterDir) is not None
assert nwGUI.projView.projTree._getTreeItem(C.hChapterDoc) is not None
assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None
nwGUI.mainMenu.aSpellCheck.setChecked(True)
assert nwGUI.mainMenu._toggleSpellCheck()
@@ -238,7 +237,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Add a Character File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True)
nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
@@ -260,7 +259,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Add a Plot File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("0000000000009").setSelected(True)
nwGUI.projView.projTree._getTreeItem(C.hPlotRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
@@ -282,7 +281,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Add a World File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("000000000000b").setSelected(True)
nwGUI.projView.projTree._getTreeItem(C.hWorldRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
@@ -313,9 +312,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Select the 'New Scene' file
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("0000000000008").setExpanded(True)
nwGUI.projView.projTree._getTreeItem("000000000000d").setExpanded(True)
nwGUI.projView.projTree._getTreeItem("000000000000f").setSelected(True)
nwGUI.projView.projTree._getTreeItem(C.hNovelRoot).setExpanded(True)
nwGUI.projView.projTree._getTreeItem(C.hChapterDir).setExpanded(True)
nwGUI.projView.projTree._getTreeItem(C.hSceneDoc).setSelected(True)
assert nwGUI.openSelectedItem()
# Type something into the document
@@ -493,8 +492,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
# Open and view the edited document
nwGUI.switchFocus(nwWidget.VIEWER)
assert nwGUI.openDocument("000000000000f")
assert nwGUI.viewDocument("000000000000f")
assert nwGUI.openDocument(C.hSceneDoc)
assert nwGUI.viewDocument(C.hSceneDoc)
qtbot.wait(stepDelay)
assert nwGUI.saveProject()
assert nwGUI.closeDocViewer()
@@ -504,9 +503,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None)
newHandle = nwGUI.projView.getSelectedHandle()
assert nwGUI.theProject.tree["0000000000020"] is not None
assert nwGUI.projView.deleteItem()
assert nwGUI.projView.requestDeleteItem()
assert nwGUI.projView.setSelectedHandle(newHandle)
assert nwGUI.projView.deleteItem()
assert nwGUI.projView.requestDeleteItem()
assert nwGUI.theProject.tree["0000000000024"] is not None # Trash
assert nwGUI.saveProject()
@@ -562,8 +561,8 @@ def testGuiMain_FocusFullMode(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
assert nwGUI.toggleFocusMode() is False
# Open a file in editor and viewer
assert nwGUI.openDocument("000000000000f")
assert nwGUI.viewDocument("000000000000f")
assert nwGUI.openDocument(C.hSceneDoc)
assert nwGUI.viewDocument(C.hSceneDoc)
# Enable focus mode
assert nwGUI.toggleFocusMode() is True
+4 -4
View File
@@ -26,7 +26,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCursor, QTextBlock
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
from tools import writeFile, buildTestProject
from tools import C, writeFile, buildTestProject
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.enum import nwDocAction, nwDocInsert
@@ -467,8 +467,8 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd):
buildTestProject(nwGUI, fncProj)
assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None
assert nwGUI.openDocument("000000000000f") is True
assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None
assert nwGUI.openDocument(C.hSceneDoc) is True
nwGUI.docEditor.clear()
# Test Faulty Inserts
@@ -677,7 +677,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd):
assert not nwGUI.importDocument()
# Open the document from before, and add some text to it
nwGUI.openDocument("000000000000f")
nwGUI.openDocument(C.hSceneDoc)
nwGUI.docEditor.setText("Bar")
assert nwGUI.docEditor.getText() == "Bar"
+9 -9
View File
@@ -22,7 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import pytest
from tools import buildTestProject, writeFile
from tools import C, buildTestProject, writeFile
from PyQt5.QtGui import QFocusEvent
from PyQt5.QtCore import Qt, QEvent
@@ -46,7 +46,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True)
nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
writeFile(
@@ -94,7 +94,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
assert not topItem.isSelected()
topItem.setSelected(True)
assert novelTree.selectedItems()[0] == topItem
assert novelView.getSelectedHandle() == ("000000000000c", 0)
assert novelView.getSelectedHandle() == (C.hTitlePage, 0)
# Refresh using the slot for the butoom
novelBar._refreshNovelTree()
@@ -119,7 +119,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
assert scItem.isSelected()
assert nwGUI.docEditor.docHandle() is None
novelTree._treeDoubleClick(scItem, 0)
assert nwGUI.docEditor.docHandle() == "000000000000f"
assert nwGUI.docEditor.docHandle() == C.hSceneDoc
# Open item with middle mouse button
scItem.setSelected(True)
@@ -136,7 +136,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData)
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
assert nwGUI.docViewer.docHandle() == "000000000000f"
assert nwGUI.docViewer.docHandle() == C.hSceneDoc
# Last Column
# ===========
@@ -144,26 +144,26 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
novelBar.setLastColType(NovelTreeColumn.HIDDEN)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
assert novelTree.lastColType == NovelTreeColumn.HIDDEN
assert novelTree._getLastColumnText("000000000000f", "T000001") == ("", "")
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ("", "")
novelBar.setLastColType(NovelTreeColumn.POV)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.POV
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == (
"Jane", "Point of View: Jane"
)
novelBar.setLastColType(NovelTreeColumn.FOCUS)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.FOCUS
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == (
"Jane", "Focus: Jane"
)
novelBar.setLastColType(NovelTreeColumn.PLOT)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.PLOT
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == (
"", "Plot: "
)
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -22,7 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import time
import pytest
from tools import buildTestProject
from tools import C, buildTestProject
from PyQt5.QtWidgets import QMessageBox
@@ -37,7 +37,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
buildTestProject(nwGUI, fncProj)
cHandle = nwGUI.theProject.newFile("A Note", "000000000000a")
cHandle = nwGUI.theProject.newFile("A Note", C.hCharRoot)
newDoc = NWDoc(nwGUI.theProject, cHandle)
newDoc.writeDocument("# A Note\n\n")
nwGUI.projView.revealNewTreeItem(cHandle)
+2 -2
View File
@@ -21,7 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import pytest
from tools import getGuiItem, buildTestProject
from tools import C, getGuiItem, buildTestProject
from PyQt5.QtWidgets import QAction, QMessageBox
@@ -41,7 +41,7 @@ def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
# Create a new project
buildTestProject(nwGUI, fncProj)
assert nwGUI.openDocument("000000000000f") is True
assert nwGUI.openDocument(C.hSceneDoc) is True
assert len(nwGUI.docEditor.getText()) == 15
# Open the tool
+16
View File
@@ -28,6 +28,22 @@ from PyQt5.QtWidgets import qApp
XML_IGNORE = ("<novelWriterXML", "<saveCount", "<autoCount", "<editTime")
class C:
# Handles from test project when random generator is mocked
hInvalid = "0000000000000"
hNovelRoot = "0000000000008"
hPlotRoot = "0000000000009"
hCharRoot = "000000000000a"
hWorldRoot = "000000000000b"
hTitlePage = "000000000000c"
hChapterDir = "000000000000d"
hChapterDoc = "000000000000e"
hSceneDoc = "000000000000f"
# END Class C
def cmpFiles(fileOne, fileTwo, ignoreLines=None, ignoreStart=None):
"""Compare two files, but optionally ignore lines given by a list.
"""