Rewrite Merge Tool (#1148)
This commit is contained in:
@@ -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/>.
|
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.document import NWDoc
|
||||||
from novelwriter.core.index import countWords
|
from novelwriter.core.index import countWords
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
@@ -28,6 +29,7 @@ from novelwriter.core.toodt import ToOdt
|
|||||||
from novelwriter.core.tomd import ToMarkdown
|
from novelwriter.core.tomd import ToMarkdown
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"DocMerger",
|
||||||
"countWords",
|
"countWords",
|
||||||
"NWDoc",
|
"NWDoc",
|
||||||
"NWProject",
|
"NWProject",
|
||||||
|
|||||||
@@ -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 2018–2022, 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
|
||||||
@@ -292,16 +292,16 @@ class NWItem():
|
|||||||
|
|
||||||
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
|
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
|
"""Return the relevant importance or status label and icon for
|
||||||
the current item based on its class.
|
the current item based on its class.
|
||||||
"""
|
"""
|
||||||
if self.isNovelLike():
|
if self.isNovelLike():
|
||||||
stName = self.theProject.statusItems.name(self._status)
|
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:
|
else:
|
||||||
stName = self.theProject.importItems.name(self._import)
|
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
|
return stName, stIcon
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class NWProject():
|
|||||||
self._projTree.updateItemData(newItem.itemHandle)
|
self._projTree.updateItemData(newItem.itemHandle)
|
||||||
return 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
|
"""Write content to a new document after it is created. This
|
||||||
will not run if the file exists and is not empty.
|
will not run if the file exists and is not empty.
|
||||||
"""
|
"""
|
||||||
@@ -191,7 +191,7 @@ class NWProject():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
hshText = "#"*minmax(hLevel, 1, 4)
|
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:
|
if tItem.isNovelLike() and isDocument:
|
||||||
tItem.setLayout(nwItemLayout.DOCUMENT)
|
tItem.setLayout(nwItemLayout.DOCUMENT)
|
||||||
else:
|
else:
|
||||||
@@ -202,6 +202,23 @@ class NWProject():
|
|||||||
|
|
||||||
return True
|
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):
|
def trashFolder(self):
|
||||||
"""Add the special trash root folder to the project.
|
"""Add the special trash root folder to the project.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+89
-110
@@ -1,10 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
novelWriter – GUI Doc Merge Tool
|
novelWriter – GUI Doc Merge Dialog
|
||||||
================================
|
==================================
|
||||||
GUI class for merging multiple documents to one document
|
Custom dialog class for merging documents.
|
||||||
|
|
||||||
File History:
|
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
|
This file is a part of novelWriter
|
||||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||||
@@ -26,169 +27,147 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
import logging
|
import logging
|
||||||
import novelwriter
|
import novelwriter
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt, QSize
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
|
QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel,
|
||||||
QListWidgetItem, QDialogButtonBox
|
QListWidget, QListWidgetItem, QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter.core import NWDoc
|
from novelwriter.gui.custom import QHelpLabel, QSwitch
|
||||||
from novelwriter.enum import nwAlert, nwItemType
|
|
||||||
from novelwriter.gui.custom import QHelpLabel
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GuiDocMerge(QDialog):
|
class GuiDocMerge(QDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui, sHandle, itemList):
|
||||||
QDialog.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiDocMerge ...")
|
logger.debug("Initialising GuiDocMerge ...")
|
||||||
self.setObjectName("GuiDocMerge")
|
self.setObjectName("GuiDocMerge")
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = mainGui
|
||||||
|
self.mainTheme = mainGui.mainTheme
|
||||||
self.theProject = mainGui.theProject
|
self.theProject = mainGui.theProject
|
||||||
self.sourceItem = None
|
|
||||||
|
|
||||||
self.outerBox = QVBoxLayout()
|
self._data = {}
|
||||||
|
|
||||||
self.setWindowTitle(self.tr("Merge Documents"))
|
self.setWindowTitle(self.tr("Merge Documents"))
|
||||||
|
|
||||||
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
|
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
|
||||||
self.helpLabel = QHelpLabel(
|
self.helpLabel = QHelpLabel(self.tr(
|
||||||
self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText
|
"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 = QListWidget()
|
||||||
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
|
self.listBox.setIconSize(QSize(iPx, iPx))
|
||||||
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
||||||
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
|
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 = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
self.buttonBox.accepted.connect(self._doMerge)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self._doClose)
|
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.setSpacing(0)
|
||||||
self.outerBox.addWidget(self.headLabel)
|
self.outerBox.addWidget(self.headLabel)
|
||||||
self.outerBox.addWidget(self.helpLabel)
|
self.outerBox.addWidget(self.helpLabel)
|
||||||
self.outerBox.addSpacing(self.mainConf.pxInt(8))
|
self.outerBox.addSpacing(vSp)
|
||||||
self.outerBox.addWidget(self.listBox)
|
self.outerBox.addWidget(self.listBox)
|
||||||
self.outerBox.addSpacing(self.mainConf.pxInt(12))
|
self.outerBox.addSpacing(vSp)
|
||||||
|
self.outerBox.addLayout(self.optBox)
|
||||||
|
self.outerBox.addSpacing(bSp)
|
||||||
self.outerBox.addWidget(self.buttonBox)
|
self.outerBox.addWidget(self.buttonBox)
|
||||||
self.setLayout(self.outerBox)
|
self.setLayout(self.outerBox)
|
||||||
|
|
||||||
self.rejected.connect(self._doClose)
|
# Load Content
|
||||||
|
self._loadContent(sHandle, itemList)
|
||||||
self._populateList()
|
|
||||||
|
|
||||||
logger.debug("GuiDocMerge initialisation complete")
|
logger.debug("GuiDocMerge initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
def getData(self):
|
||||||
# Buttons
|
"""Return the user's choices.
|
||||||
##
|
|
||||||
|
|
||||||
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")
|
finalItems = []
|
||||||
|
|
||||||
finalOrder = []
|
|
||||||
for i in range(self.listBox.count()):
|
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._data["moveToTrash"] = self.trashSwitch.isChecked()
|
||||||
self.mainGui.makeAlert(self.tr(
|
self._data["finalItems"] = finalItems
|
||||||
"No source documents found. Nothing to do."
|
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
theText = ""
|
return self._data
|
||||||
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"
|
|
||||||
|
|
||||||
if self.sourceItem is None:
|
##
|
||||||
self.mainGui.makeAlert(self.tr(
|
# Slots
|
||||||
"No source folder selected. Nothing to do."
|
##
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
srcItem = self.theProject.tree[self.sourceItem]
|
def _resetList(self):
|
||||||
if srcItem is None:
|
"""Reset the content of the list box to its original state.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
self.close()
|
logger.debug("Resetting list box content")
|
||||||
|
sHandle = self._data.get("sHandle", None)
|
||||||
|
itemList = self._data.get("origItems", [])
|
||||||
|
self._loadContent(sHandle, itemList)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _populateList(self):
|
def _loadContent(self, sHandle, itemList):
|
||||||
"""Get the item selected in the tree, check that it is a folder,
|
"""Load content from a given list of items.
|
||||||
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.mainGui.projView.getSelectedHandle()
|
self._data = {}
|
||||||
self.sourceItem = tHandle
|
self._data["sHandle"] = sHandle
|
||||||
if tHandle is None:
|
self._data["origItems"] = itemList
|
||||||
return False
|
|
||||||
|
|
||||||
nwItem = self.theProject.tree[tHandle]
|
self.listBox.clear()
|
||||||
if nwItem is None:
|
for tHandle in itemList:
|
||||||
return False
|
nwItem = self.theProject.tree[tHandle]
|
||||||
|
if nwItem is None or not nwItem.isFileType():
|
||||||
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():
|
|
||||||
continue
|
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.setText(nwItem.itemName)
|
||||||
newItem.setData(Qt.UserRole, sHandle)
|
newItem.setData(Qt.UserRole, tHandle)
|
||||||
|
newItem.setCheckState(Qt.Checked)
|
||||||
|
|
||||||
self.listBox.addItem(newItem)
|
self.listBox.addItem(newItem)
|
||||||
|
|
||||||
return True
|
return
|
||||||
|
|
||||||
# END Class GuiDocMerge
|
# END Class GuiDocMerge
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class QConfigLayout(QGridLayout):
|
|||||||
class QHelpLabel(QLabel):
|
class QHelpLabel(QLabel):
|
||||||
|
|
||||||
def __init__(self, theText, textCol, fontSize=0.9):
|
def __init__(self, theText, textCol, fontSize=0.9):
|
||||||
QLabel.__init__(self, theText)
|
super().__init__(theText)
|
||||||
|
|
||||||
if isinstance(textCol, QColor):
|
if isinstance(textCol, QColor):
|
||||||
qCol = textCol
|
qCol = textCol
|
||||||
@@ -377,7 +377,7 @@ class QSwitch(QAbstractButton):
|
|||||||
class PagedDialog(QDialog):
|
class PagedDialog(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QDialog.__init__(self, parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
self._tabBar = VerticalTabBar(self)
|
self._tabBar = VerticalTabBar(self)
|
||||||
self._tabBar.setExpanding(False)
|
self._tabBar.setExpanding(False)
|
||||||
@@ -410,13 +410,13 @@ class PagedDialog(QDialog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def addTab(self, widget, label):
|
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)
|
self._tabBox.addTab(widget, label)
|
||||||
return
|
return
|
||||||
|
|
||||||
def addControls(self, buttonBar):
|
def addControls(self, buttonBar):
|
||||||
"""Adds a button bar to the dialog.
|
"""Add a button bar to the dialog.
|
||||||
"""
|
"""
|
||||||
self._buttonBox.addWidget(buttonBar)
|
self._buttonBox.addWidget(buttonBar)
|
||||||
return
|
return
|
||||||
@@ -427,14 +427,14 @@ class PagedDialog(QDialog):
|
|||||||
class VerticalTabBar(QTabBar):
|
class VerticalTabBar(QTabBar):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QTabBar.__init__(self, parent=parent)
|
super().__init__(parent=parent)
|
||||||
self._mW = novelwriter.CONFIG.pxInt(150)
|
self._mW = novelwriter.CONFIG.pxInt(150)
|
||||||
return
|
return
|
||||||
|
|
||||||
def tabSizeHint(self, index):
|
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.transpose()
|
||||||
tSize.setWidth(min(tSize.width(), self._mW))
|
tSize.setWidth(min(tSize.width(), self._mW))
|
||||||
return tSize
|
return tSize
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class GuiMainMenu(QMenuBar):
|
|||||||
# Project > Delete
|
# Project > Delete
|
||||||
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
|
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
|
||||||
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
|
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)
|
self.projMenu.addAction(self.aDeleteItem)
|
||||||
|
|
||||||
# Project > Empty Trash
|
# Project > Empty Trash
|
||||||
@@ -244,11 +244,6 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
|
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
|
||||||
self.docuMenu.addAction(self.aImportFile)
|
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
|
# Document > Split Document
|
||||||
self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self)
|
self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self)
|
||||||
self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument())
|
self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument())
|
||||||
|
|||||||
@@ -1048,7 +1048,7 @@ class GuiOutlineDetails(QScrollArea):
|
|||||||
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
|
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
|
||||||
self.titleValue.setText(novIdx.title)
|
self.titleValue.setText(novIdx.title)
|
||||||
|
|
||||||
itemStatus, _ = nwItem.getImportStatus()
|
itemStatus, _ = nwItem.getImportStatus(incIcon=False)
|
||||||
|
|
||||||
self.fileValue.setText(nwItem.itemName)
|
self.fileValue.setText(nwItem.itemName)
|
||||||
self.itemValue.setText(itemStatus)
|
self.itemValue.setText(itemStatus)
|
||||||
|
|||||||
+263
-158
@@ -34,15 +34,15 @@ from time import time
|
|||||||
from PyQt5.QtGui import QPalette
|
from PyQt5.QtGui import QPalette
|
||||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
||||||
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
|
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
|
||||||
QVBoxLayout, QWidget
|
QVBoxLayout, QWidget
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter.core import NWDoc
|
from novelwriter.core import DocMerger
|
||||||
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||||
|
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
|
||||||
from novelwriter.constants import nwHeaders, trConst, nwLabels
|
from novelwriter.constants import nwHeaders, trConst, nwLabels
|
||||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ class GuiProjectView(QWidget):
|
|||||||
self.renameTreeItem = self.projTree.renameTreeItem
|
self.renameTreeItem = self.projTree.renameTreeItem
|
||||||
self.getTreeFromHandle = self.projTree.getTreeFromHandle
|
self.getTreeFromHandle = self.projTree.getTreeFromHandle
|
||||||
self.emptyTrash = self.projTree.emptyTrash
|
self.emptyTrash = self.projTree.emptyTrash
|
||||||
self.deleteItem = self.projTree.deleteItem
|
self.requestDeleteItem = self.projTree.requestDeleteItem
|
||||||
self.setTreeItemValues = self.projTree.setTreeItemValues
|
self.setTreeItemValues = self.projTree.setTreeItemValues
|
||||||
self.propagateCount = self.projTree.propagateCount
|
self.propagateCount = self.projTree.propagateCount
|
||||||
self.getSelectedHandle = self.projTree.getSelectedHandle
|
self.getSelectedHandle = self.projTree.getSelectedHandle
|
||||||
@@ -334,7 +334,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.projView = projView
|
self.projView = projView
|
||||||
self.mainGui = projView.mainGui
|
self.mainGui = projView.mainGui
|
||||||
self.mainTheme = projView.mainGui.mainTheme
|
self.mainTheme = projView.mainGui.mainTheme
|
||||||
self.theProject = projView.mainGui.theProject
|
self.theProject = projView.mainGui.theProject
|
||||||
|
|
||||||
# Internal Variables
|
# Internal Variables
|
||||||
@@ -382,9 +382,8 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
trRoot = self.invisibleRootItem()
|
trRoot = self.invisibleRootItem()
|
||||||
trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled)
|
trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled)
|
||||||
|
|
||||||
# Set Multiple Selection by CTRL
|
# Set selection options
|
||||||
# Disabled for now, until the merge files option has been added
|
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
# self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
|
||||||
# Connect signals
|
# Connect signals
|
||||||
@@ -603,7 +602,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self.setTreeItemValues(tHandle)
|
self.setTreeItemValues(tHandle)
|
||||||
self._alertTreeChange(tHandle, flush=False)
|
self._alertTreeChange(tHandle, flush=False)
|
||||||
|
|
||||||
return
|
return True
|
||||||
|
|
||||||
def saveTreeOrder(self):
|
def saveTreeOrder(self):
|
||||||
"""Build a list of the items in the project tree and send them
|
"""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)
|
theList = self._scanChildren(theList, theItem, 0)
|
||||||
return theList
|
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):
|
def emptyTrash(self):
|
||||||
"""Permanently delete all documents in the Trash folder. This
|
"""Permanently delete all documents in the Trash folder. This
|
||||||
function only asks for confirmation once, and calls the regular
|
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)
|
self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
|
||||||
)
|
)
|
||||||
if not msgYes:
|
if not msgYes:
|
||||||
|
logger.info("Action cancelled by user")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.verbose("Deleting %d file(s) from Trash", nTrash)
|
logger.verbose("Deleting %d file(s) from Trash", nTrash)
|
||||||
for tHandle in reversed(self.getTreeFromHandle(trashHandle)):
|
for tHandle in reversed(self.getTreeFromHandle(trashHandle)):
|
||||||
if tHandle == trashHandle:
|
if tHandle == trashHandle:
|
||||||
continue
|
continue
|
||||||
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
|
self.permanentlyDeleteItem(tHandle, askFirst=False, flush=False)
|
||||||
|
|
||||||
if nTrash > 0:
|
if nTrash > 0:
|
||||||
self._alertTreeChange(trashHandle, flush=True)
|
self._alertTreeChange(trashHandle, flush=True)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False):
|
def moveItemToTrash(self, tHandle, askFirst=True, flush=True):
|
||||||
"""Delete an item from the project tree. As a first step, files are
|
"""Move an item to Trash. Root folders cannot be moved to Trash,
|
||||||
moved to the Trash folder. Permanent deletion is a second step. This
|
so such a request is cancelled.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
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)
|
trItemS = self._getTreeItem(tHandle)
|
||||||
nwItemS = self.theProject.tree[tHandle]
|
nwItemS = self.theProject.tree[tHandle]
|
||||||
|
|
||||||
@@ -704,87 +722,99 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
logger.error("Could not find tree item for deletion")
|
logger.error("Could not find tree item for deletion")
|
||||||
return False
|
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)
|
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():
|
if nwItemS.isRootType():
|
||||||
# Only an empty ROOT folder can be deleted
|
# Only an empty ROOT folder can be deleted
|
||||||
logger.debug("User requested a root folder '%s' deleted", tHandle)
|
if trItemS.childCount() > 0:
|
||||||
tIndex = self.indexOfTopLevelItem(trItemS)
|
|
||||||
if trItemS.childCount() == 0:
|
|
||||||
self.takeTopLevelItem(tIndex)
|
|
||||||
self._deleteTreeItem(tHandle)
|
|
||||||
self._alertTreeChange(tHandle, flush=True)
|
|
||||||
else:
|
|
||||||
self.mainGui.makeAlert(self.tr(
|
self.mainGui.makeAlert(self.tr(
|
||||||
"Cannot delete root folder. It is not empty. "
|
"Root folders can only be deleted when they are empty."
|
||||||
"Recursive deletion is not supported. "
|
|
||||||
"Please delete the content first."
|
|
||||||
), nwAlert.ERROR)
|
), nwAlert.ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif nwItemS.isFolderType() and trItemS.childCount() == 0:
|
logger.debug("Permanently deleting root folder '%s'", tHandle)
|
||||||
# An empty FOLDER is just deleted without any further checks
|
|
||||||
logger.debug("User requested an empty folder '%s' deleted", 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()
|
trItemP = trItemS.parent()
|
||||||
tIndex = trItemP.indexOfChild(trItemS)
|
tIndex = trItemP.indexOfChild(trItemS)
|
||||||
trItemP.takeChild(tIndex)
|
trItemP.takeChild(tIndex)
|
||||||
self._deleteTreeItem(tHandle)
|
|
||||||
self._alertTreeChange(tHandle, flush=autoFlush)
|
|
||||||
|
|
||||||
else:
|
for dHandle in reversed(itemList):
|
||||||
# A populated FOLDER or a FILE requires confirmtation
|
if self.mainGui.docEditor.docHandle() == dHandle:
|
||||||
logger.debug("User requested a file or folder '%s' deleted", tHandle)
|
self.mainGui.closeDocument()
|
||||||
trItemP = trItemS.parent()
|
self.theProject.removeItem(tHandle)
|
||||||
trItemT = self._addTrashRoot()
|
self._treeMap.pop(tHandle, None)
|
||||||
if trItemP is None or trItemT is None:
|
|
||||||
logger.error("Could not delete item")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.theProject.tree.isTrash(tHandle):
|
self._alertTreeChange(tHandle, flush=flush)
|
||||||
# If the file is in the trash folder already, as the
|
self.projView.wordCountsChanged.emit()
|
||||||
# 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)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -885,7 +915,9 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
dstItem = self._lastMove.get("parent", None)
|
dstItem = self._lastMove.get("parent", None)
|
||||||
dstIndex = self._lastMove.get("index", 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")
|
logger.verbose("No tree move to undo")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -981,7 +1013,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot("QTreeWidgetItem*", int)
|
@pyqtSlot("QTreeWidgetItem*", int)
|
||||||
def _treeDoubleClick(self, tItem, colNo):
|
def _treeDoubleClick(self, trItem, colNo):
|
||||||
"""Capture a double-click event and either request the document
|
"""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.
|
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():
|
if tItem.isFileType():
|
||||||
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "")
|
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "")
|
||||||
else:
|
else:
|
||||||
trItem = self._getTreeItem(tHandle)
|
trItem.setExpanded(not trItem.isExpanded())
|
||||||
if trItem is not None:
|
|
||||||
trItem.setExpanded(not trItem.isExpanded())
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1077,43 +1107,58 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
lambda n, key=key: self._changeItemImport(tHandle, key)
|
lambda n, key=key: self._changeItemImport(tHandle, key)
|
||||||
)
|
)
|
||||||
|
|
||||||
if isFile and tItem.documentAllowed():
|
# Transform Item
|
||||||
if tItem.isNoteLayout():
|
# ==============
|
||||||
ctxMenu.addAction(
|
|
||||||
self.tr("Convert to {0}").format(
|
if not isRoot:
|
||||||
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
|
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)
|
lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
ctxMenu.addAction(
|
if isDocFile or isFolder:
|
||||||
self.tr("Convert to {0}").format(
|
mTrans.addAction(
|
||||||
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
|
self.tr("Convert to {0}").format(trNote),
|
||||||
),
|
|
||||||
lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE)
|
lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE)
|
||||||
)
|
)
|
||||||
elif isFolder:
|
|
||||||
if tItem.documentAllowed():
|
if hasChild and isFile:
|
||||||
ctxMenu.addAction(
|
mTrans.addAction(
|
||||||
self.tr("Convert to {0}").format(
|
self.tr("Merge Child Items into Self"),
|
||||||
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
|
lambda: self._mergeDocuments(tHandle, False)
|
||||||
),
|
|
||||||
lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT)
|
|
||||||
)
|
)
|
||||||
ctxMenu.addAction(
|
mTrans.addAction(
|
||||||
self.tr("Convert to {0}").format(
|
self.tr("Merge Child Items into New"),
|
||||||
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
|
lambda: self._mergeDocuments(tHandle, True)
|
||||||
),
|
)
|
||||||
lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE)
|
|
||||||
)
|
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()
|
ctxMenu.addSeparator()
|
||||||
|
|
||||||
# Expand/Collapse
|
|
||||||
# ===============
|
|
||||||
|
|
||||||
if hasChild:
|
if hasChild:
|
||||||
ctxMenu.addSeparator()
|
|
||||||
ctxMenu.addAction(
|
ctxMenu.addAction(
|
||||||
self.tr("Expand All"),
|
self.tr("Expand All"),
|
||||||
lambda: self.setExpandedFromHandle(tHandle, True)
|
lambda: self.setExpandedFromHandle(tHandle, True)
|
||||||
@@ -1123,18 +1168,16 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
lambda: self.setExpandedFromHandle(tHandle, False)
|
lambda: self.setExpandedFromHandle(tHandle, False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete Item
|
|
||||||
# ===========
|
|
||||||
|
|
||||||
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
||||||
ctxMenu.addAction(
|
ctxMenu.addAction(
|
||||||
self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle)
|
self.tr("Delete Permanently"), lambda: self.permanentlyDeleteItem(tHandle)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ctxMenu.addAction(
|
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))
|
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -1255,23 +1298,6 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
"""
|
"""
|
||||||
return self._treeMap.get(tHandle, None)
|
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):
|
def _toggleItemExported(self, tHandle):
|
||||||
"""Toggle the exported status of an item.
|
"""Toggle the exported status of an item.
|
||||||
"""
|
"""
|
||||||
@@ -1355,6 +1381,85 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
logger.info("Folder conversion cancelled")
|
logger.info("Folder conversion cancelled")
|
||||||
return
|
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):
|
def _scanChildren(self, theList, tItem, tIndex):
|
||||||
"""This is a recursive function returning all items in a tree
|
"""This is a recursive function returning all items in a tree
|
||||||
starting at a given QTreeWidgetItem.
|
starting at a given QTreeWidgetItem.
|
||||||
@@ -1380,7 +1485,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
"""
|
"""
|
||||||
tHandle = nwItem.itemHandle
|
tHandle = nwItem.itemHandle
|
||||||
pHandle = nwItem.itemParent
|
pHandle = nwItem.itemParent
|
||||||
newItem = QTreeWidgetItem([""]*4)
|
newItem = QTreeWidgetItem()
|
||||||
|
|
||||||
newItem.setText(self.C_NAME, "")
|
newItem.setText(self.C_NAME, "")
|
||||||
newItem.setText(self.C_COUNT, "0")
|
newItem.setText(self.C_COUNT, "0")
|
||||||
@@ -1455,10 +1560,10 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
if tHandle is None:
|
if tHandle is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
tItem = self.theProject.tree[tHandle]
|
if tHandle not in self.theProject.tree:
|
||||||
if tItem is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
tItem = self.theProject.tree[tHandle]
|
||||||
if tItem.isRootType():
|
if tItem.isRootType():
|
||||||
self.projView.rootFolderChanged.emit(tHandle)
|
self.projView.rootFolderChanged.emit(tHandle)
|
||||||
|
|
||||||
|
|||||||
+2
-14
@@ -44,8 +44,8 @@ from novelwriter.gui import (
|
|||||||
GuiViewsBar
|
GuiViewsBar
|
||||||
)
|
)
|
||||||
from novelwriter.dialogs import (
|
from novelwriter.dialogs import (
|
||||||
GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
|
GuiAbout, GuiDocSplit, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
|
||||||
GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
|
GuiProjectSettings, GuiUpdates, GuiWordList
|
||||||
)
|
)
|
||||||
from novelwriter.tools import (
|
from novelwriter.tools import (
|
||||||
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
|
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
|
||||||
@@ -745,18 +745,6 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
return True
|
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):
|
def splitDocument(self):
|
||||||
"""Split a single document into multiple documents.
|
"""Split a single document into multiple documents.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+12
-6
@@ -190,14 +190,20 @@ def mockRnd(monkeypatch):
|
|||||||
from 0. This one will generate status/importance flags and handles
|
from 0. This one will generate status/importance flags and handles
|
||||||
in a predictable sequence.
|
in a predictable sequence.
|
||||||
"""
|
"""
|
||||||
def rnd(n):
|
class MockRnd:
|
||||||
for x in range(n):
|
|
||||||
yield x
|
|
||||||
|
|
||||||
gen = rnd(1000)
|
def __init__(self):
|
||||||
monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
|
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'?>
|
<?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>
|
<project>
|
||||||
<name>New Project</name>
|
<name>New Project</name>
|
||||||
<title>New Novel</title>
|
<title>New Novel</title>
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
<lastViewed>None</lastViewed>
|
<lastViewed>None</lastViewed>
|
||||||
<lastNovel>None</lastNovel>
|
<lastNovel>None</lastNovel>
|
||||||
<lastOutline>None</lastOutline>
|
<lastOutline>None</lastOutline>
|
||||||
<lastWordCount>2</lastWordCount>
|
<lastWordCount>4</lastWordCount>
|
||||||
<novelWordCount>1</novelWordCount>
|
<novelWordCount>1</novelWordCount>
|
||||||
<notesWordCount>1</notesWordCount>
|
<notesWordCount>3</notesWordCount>
|
||||||
<autoReplace/>
|
<autoReplace/>
|
||||||
<titleFormat>
|
<titleFormat>
|
||||||
<title>%title%</title>
|
<title>%title%</title>
|
||||||
@@ -29,62 +29,62 @@
|
|||||||
<section></section>
|
<section></section>
|
||||||
</titleFormat>
|
</titleFormat>
|
||||||
<status>
|
<status>
|
||||||
<entry key="s000008" count="7" red="100" green="100" blue="100">New</entry>
|
<entry key="s000000" 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="s000001" 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="s000002" 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="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
|
||||||
</status>
|
</status>
|
||||||
<importance>
|
<importance>
|
||||||
<entry key="i00000c" count="4" red="100" green="100" blue="100">New</entry>
|
<entry key="i000004" 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="i000005" 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="i000006" 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="i000007" count="0" red="50" green="200" blue="0">Main</entry>
|
||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</settings>
|
||||||
<content count="11">
|
<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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Novel</name>
|
<name status="s000000" import="i000004">Novel</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Plot</name>
|
<name status="s000000" import="i000004">Plot</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Characters</name>
|
<name status="s000000" import="i000004">Characters</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">World</name>
|
<name status="s000000" import="i000004">World</name>
|
||||||
</item>
|
</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"/>
|
<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>
|
||||||
<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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">New Chapter</name>
|
<name status="s000000" import="i000004">New Chapter</name>
|
||||||
</item>
|
</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"/>
|
<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>
|
||||||
<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"/>
|
<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>
|
||||||
<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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Stuff</name>
|
<name status="s000000" import="i000004">Stuff</name>
|
||||||
</item>
|
</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"/>
|
<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>
|
||||||
<item handle="000000000002a" parent="0000000000012" root="0000000000012" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
<item handle="0000000000022" parent="000000000000a" root="000000000000a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
||||||
<meta expanded="False" charCount="4" wordCount="1" paraCount="0" cursorPos="0"/>
|
<meta expanded="False" charCount="11" wordCount="3" paraCount="1" cursorPos="0"/>
|
||||||
<name status="s000008" import="i00000c" exported="True">Jane</name>
|
<name status="s000000" import="i000004" exported="True">Jane</name>
|
||||||
</item>
|
</item>
|
||||||
</content>
|
</content>
|
||||||
</novelWriterXML>
|
</novelWriterXML>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?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>
|
<project>
|
||||||
<name>New Project</name>
|
<name>New Project</name>
|
||||||
<title>New Novel</title>
|
<title>New Novel</title>
|
||||||
@@ -29,82 +29,82 @@
|
|||||||
<section></section>
|
<section></section>
|
||||||
</titleFormat>
|
</titleFormat>
|
||||||
<status>
|
<status>
|
||||||
<entry key="s000008" count="6" red="100" green="100" blue="100">New</entry>
|
<entry key="s000000" 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="s000001" 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="s000002" 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="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
|
||||||
</status>
|
</status>
|
||||||
<importance>
|
<importance>
|
||||||
<entry key="i00000c" count="10" red="100" green="100" blue="100">New</entry>
|
<entry key="i000004" 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="i000005" 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="i000006" 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="i000007" count="0" red="50" green="200" blue="0">Main</entry>
|
||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</settings>
|
||||||
<content count="16">
|
<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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Novel</name>
|
<name status="s000000" import="i000004">Novel</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Plot</name>
|
<name status="s000000" import="i000004">Plot</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Characters</name>
|
<name status="s000000" import="i000004">Characters</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">World</name>
|
<name status="s000000" import="i000004">World</name>
|
||||||
</item>
|
</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"/>
|
<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>
|
||||||
<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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">New Chapter</name>
|
<name status="s000000" import="i000004">New Chapter</name>
|
||||||
</item>
|
</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"/>
|
<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>
|
||||||
<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"/>
|
<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>
|
||||||
<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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Novel</name>
|
<name status="s000000" import="i000004">Novel</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Plot</name>
|
<name status="s000000" import="i000004">Plot</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Characters</name>
|
<name status="s000000" import="i000004">Characters</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Locations</name>
|
<name status="s000000" import="i000004">Locations</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Timeline</name>
|
<name status="s000000" import="i000004">Timeline</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Objects</name>
|
<name status="s000000" import="i000004">Objects</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Custom</name>
|
<name status="s000000" import="i000004">Custom</name>
|
||||||
</item>
|
</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"/>
|
<meta expanded="False"/>
|
||||||
<name status="s000008" import="i00000c">Custom</name>
|
<name status="s000000" import="i000004">Custom</name>
|
||||||
</item>
|
</item>
|
||||||
</content>
|
</content>
|
||||||
</novelWriterXML>
|
</novelWriterXML>
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – Project Document Tools Tester
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
This file is a part of novelWriter
|
||||||
|
Copyright 2018–2022, 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
|
||||||
@@ -22,11 +22,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from zipfile import ZipFile
|
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 mock import causeOSError
|
||||||
|
|
||||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
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")
|
compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx")
|
||||||
|
|
||||||
theProject = NWProject(mockGUI)
|
theProject = NWProject(mockGUI)
|
||||||
|
mockRnd.reset()
|
||||||
buildTestProject(theProject, fncDir)
|
buildTestProject(theProject, fncDir)
|
||||||
|
|
||||||
assert theProject.setProjectPath(fncDir) is True
|
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.closeProject() is True
|
||||||
assert theProject.openProject(projFile) is True
|
assert theProject.openProject(projFile) is True
|
||||||
|
|
||||||
assert isinstance(theProject.newRoot(nwItemClass.NOVEL), str)
|
assert theProject.newRoot(nwItemClass.NOVEL) == "0000000000020"
|
||||||
assert isinstance(theProject.newRoot(nwItemClass.PLOT), str)
|
assert theProject.newRoot(nwItemClass.PLOT) == "0000000000021"
|
||||||
assert isinstance(theProject.newRoot(nwItemClass.CHARACTER), str)
|
assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000022"
|
||||||
assert isinstance(theProject.newRoot(nwItemClass.WORLD), str)
|
assert theProject.newRoot(nwItemClass.WORLD) == "0000000000023"
|
||||||
assert isinstance(theProject.newRoot(nwItemClass.TIMELINE), str)
|
assert theProject.newRoot(nwItemClass.TIMELINE) == "0000000000024"
|
||||||
assert isinstance(theProject.newRoot(nwItemClass.OBJECT), str)
|
assert theProject.newRoot(nwItemClass.OBJECT) == "0000000000025"
|
||||||
assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str)
|
assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000026"
|
||||||
assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str)
|
assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000027"
|
||||||
|
|
||||||
assert theProject.projChanged is True
|
assert theProject.projChanged is True
|
||||||
assert theProject.saveProject() 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 cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||||
assert theProject.projChanged is False
|
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
|
# END Test testCoreProject_NewRoot
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@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.
|
"""Check that new files can be added to the project.
|
||||||
"""
|
"""
|
||||||
projFile = os.path.join(fncDir, "nwProject.nwx")
|
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")
|
compFile = os.path.join(refDir, "coreProject_NewFileFolder_nwProject.nwx")
|
||||||
|
|
||||||
theProject = NWProject(mockGUI)
|
theProject = NWProject(mockGUI)
|
||||||
|
mockRnd.reset()
|
||||||
buildTestProject(theProject, fncDir)
|
buildTestProject(theProject, fncDir)
|
||||||
|
|
||||||
assert theProject.setProjectPath(fncDir) is True
|
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
|
assert theProject.newFile("New File", "1234567890abc") is None
|
||||||
|
|
||||||
# Add files properly
|
# Add files properly
|
||||||
assert theProject.newFolder("Stuff", "0000000000015") == "0000000000028"
|
assert theProject.newFolder("Stuff", C.hNovelRoot) == "0000000000020"
|
||||||
assert theProject.newFile("Hello", "0000000000015") == "0000000000029"
|
assert theProject.newFile("Hello", "0000000000020") == "0000000000021"
|
||||||
assert theProject.newFile("Jane", "0000000000012") == "000000000002a"
|
assert theProject.newFile("Jane", C.hCharRoot) == "0000000000022"
|
||||||
|
|
||||||
assert "0000000000028" in theProject.tree
|
assert "0000000000020" in theProject.tree
|
||||||
assert "0000000000029" in theProject.tree
|
assert "0000000000021" in theProject.tree
|
||||||
assert "000000000002a" in theProject.tree
|
assert "0000000000022" in theProject.tree
|
||||||
|
|
||||||
# Write to file, failed
|
# Write to file, failed
|
||||||
assert theProject.writeNewFile("blabla", 1, True) is False # Not a handle
|
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("0000000000020", 1, True) is False # Not a file
|
||||||
assert theProject.writeNewFile("0000000000014", 1, True) is False # Already has content
|
assert theProject.writeNewFile(C.hTitlePage, 1, True) is False # Already has content
|
||||||
|
|
||||||
# Write to file, success
|
# Write to file, success
|
||||||
assert theProject.writeNewFile("0000000000029", 2, True) is True
|
assert theProject.writeNewFile("0000000000021", 2, True) is True
|
||||||
assert NWDoc(theProject, "0000000000029").readDocument() == "## Hello\n\n"
|
assert NWDoc(theProject, "0000000000021").readDocument() == "## Hello\n\n"
|
||||||
|
|
||||||
assert theProject.writeNewFile("000000000002a", 1, False) is True
|
# Write to file with additional text, success
|
||||||
assert NWDoc(theProject, "000000000002a").readDocument() == "# Jane\n\n"
|
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
|
# Save, close and check
|
||||||
assert theProject.projChanged is True
|
assert theProject.projChanged is True
|
||||||
assert theProject.saveProject() is True
|
assert theProject.saveProject() is True
|
||||||
assert theProject.closeProject() is True
|
|
||||||
|
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||||
assert theProject.projChanged is False
|
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
|
# END Test testCoreProject_NewFileFolder
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from mock import causeOSError
|
from tools import buildTestProject, C
|
||||||
from tools import getGuiItem, readFile, writeFile, buildTestProject
|
|
||||||
|
|
||||||
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
|
||||||
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
|
|
||||||
from novelwriter.core.tree import NWTree
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
@@ -39,142 +36,58 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
|||||||
# Block message box
|
# Block message box
|
||||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok)
|
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok)
|
||||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
|
||||||
|
|
||||||
# Create a new project
|
# Create a new project
|
||||||
buildTestProject(nwGUI, fncProj)
|
buildTestProject(nwGUI, fncProj)
|
||||||
|
|
||||||
# Handles for new objects
|
# Check that the dialog kan handle invalid items
|
||||||
hNovelRoot = "0000000000008"
|
nwMerge = GuiDocMerge(nwGUI, C.hInvalid, [C.hInvalid])
|
||||||
hChapterDir = "000000000000d"
|
qtbot.addWidget(nwMerge)
|
||||||
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)
|
|
||||||
nwMerge.show()
|
nwMerge.show()
|
||||||
qtbot.wait(50)
|
|
||||||
|
|
||||||
# Populate List
|
|
||||||
# =============
|
|
||||||
|
|
||||||
nwMerge.listBox.clear()
|
|
||||||
assert nwMerge.listBox.count() == 0
|
assert nwMerge.listBox.count() == 0
|
||||||
|
nwMerge.reject()
|
||||||
|
|
||||||
# No item selected
|
# Load items from chapter dir
|
||||||
nwGUI.projView.projTree.clearSelection()
|
nwMerge = GuiDocMerge(nwGUI, C.hChapterDir, [C.hChapterDir, C.hChapterDoc, C.hSceneDoc])
|
||||||
assert nwMerge._populateList() is False
|
qtbot.addWidget(nwMerge)
|
||||||
assert nwMerge.listBox.count() == 0
|
nwMerge.show()
|
||||||
|
|
||||||
# Non-existing item
|
assert nwMerge.listBox.count() == 2
|
||||||
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
|
|
||||||
|
|
||||||
# Select a non-folder
|
itemOne = nwMerge.listBox.item(0)
|
||||||
nwGUI.projView.projTree.clearSelection()
|
itemTwo = nwMerge.listBox.item(1)
|
||||||
nwGUI.projView.projTree._getTreeItem(hChapterOne).setSelected(True)
|
|
||||||
assert nwMerge._populateList() is False
|
|
||||||
assert nwMerge.listBox.count() == 0
|
|
||||||
|
|
||||||
# Select the chapter folder
|
assert itemOne.data(Qt.UserRole) == C.hChapterDoc
|
||||||
nwGUI.projView.projTree.clearSelection()
|
assert itemTwo.data(Qt.UserRole) == C.hSceneDoc
|
||||||
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
|
|
||||||
assert nwMerge._populateList() is True
|
|
||||||
assert nwMerge.listBox.count() == 5
|
|
||||||
|
|
||||||
# Merge Documents
|
assert itemOne.checkState() == Qt.Checked
|
||||||
# ===============
|
assert itemTwo.checkState() == Qt.Checked
|
||||||
|
|
||||||
# First, a successful merge
|
data = nwMerge.getData()
|
||||||
with monkeypatch.context() as mp:
|
assert data["sHandle"] == C.hChapterDir
|
||||||
mp.setattr(GuiDocMerge, "_doClose", lambda *a: None)
|
assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc]
|
||||||
assert nwMerge._doMerge() is True
|
assert data["moveToTrash"] is False
|
||||||
assert nwGUI.saveProject() is True
|
assert data["finalItems"] == [C.hChapterDoc, C.hSceneDoc]
|
||||||
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(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# OS error
|
# Uncheck second item and toggle trash switch
|
||||||
with monkeypatch.context() as mp:
|
itemTwo.setCheckState(Qt.Unchecked)
|
||||||
mp.setattr("builtins.open", causeOSError)
|
nwMerge.trashSwitch.setChecked(True)
|
||||||
assert nwMerge._doMerge() is False
|
|
||||||
|
|
||||||
# Can't find the source item
|
data = nwMerge.getData()
|
||||||
with monkeypatch.context() as mp:
|
assert data["sHandle"] == C.hChapterDir
|
||||||
mp.setattr(NWTree, "__getitem__", lambda *a: None)
|
assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc]
|
||||||
assert nwMerge._doMerge() is False
|
assert data["moveToTrash"] is True
|
||||||
|
assert data["finalItems"] == [C.hChapterDoc]
|
||||||
|
|
||||||
# No source handle set
|
# Restore default values
|
||||||
nwMerge.sourceItem = None
|
nwMerge._resetList()
|
||||||
assert nwMerge._doMerge() is False
|
|
||||||
|
|
||||||
# No documents to merge
|
data = nwMerge.getData()
|
||||||
nwMerge.listBox.clear()
|
assert data["sHandle"] == C.hChapterDir
|
||||||
assert nwMerge._doMerge() is False
|
assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc]
|
||||||
|
assert data["moveToTrash"] is True
|
||||||
|
assert data["finalItems"] == [C.hChapterDoc, C.hSceneDoc]
|
||||||
|
|
||||||
# Close up
|
# qtbot.stop()
|
||||||
nwMerge._doClose()
|
|
||||||
|
|
||||||
# qtbot.stopForInteraction()
|
|
||||||
|
|
||||||
# END Test testDlgMerge_Main
|
# END Test testDlgMerge_Main
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from shutil import copyfile
|
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.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import QMessageBox, QInputDialog
|
from PyQt5.QtWidgets import QMessageBox, QInputDialog
|
||||||
@@ -54,7 +54,6 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
|
|||||||
assert nwGUI.saveDocument() is False
|
assert nwGUI.saveDocument() is False
|
||||||
assert nwGUI.viewDocument(None) is False
|
assert nwGUI.viewDocument(None) is False
|
||||||
assert nwGUI.importDocument() is False
|
assert nwGUI.importDocument() is False
|
||||||
assert nwGUI.mergeDocuments() is False
|
|
||||||
assert nwGUI.splitDocument() is False
|
assert nwGUI.splitDocument() is False
|
||||||
assert nwGUI.openSelectedItem() is False
|
assert nwGUI.openSelectedItem() is False
|
||||||
assert nwGUI.editItemLabel() 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
|
assert nwGUI.theProject.spellCheck is False
|
||||||
|
|
||||||
# Check that tree items have been created
|
# Check that tree items have been created
|
||||||
assert nwGUI.projView.projTree._getTreeItem("0000000000008") is not None
|
assert nwGUI.projView.projTree._getTreeItem(C.hNovelRoot) is not None
|
||||||
assert nwGUI.projView.projTree._getTreeItem("0000000000009") is not None
|
assert nwGUI.projView.projTree._getTreeItem(C.hPlotRoot) is not None
|
||||||
assert nwGUI.projView.projTree._getTreeItem("000000000000a") is not None
|
assert nwGUI.projView.projTree._getTreeItem(C.hCharRoot) is not None
|
||||||
assert nwGUI.projView.projTree._getTreeItem("000000000000b") is not None
|
assert nwGUI.projView.projTree._getTreeItem(C.hWorldRoot) is not None
|
||||||
assert nwGUI.projView.projTree._getTreeItem("000000000000c") is not None
|
assert nwGUI.projView.projTree._getTreeItem(C.hTitlePage) is not None
|
||||||
assert nwGUI.projView.projTree._getTreeItem("000000000000d") is not None
|
assert nwGUI.projView.projTree._getTreeItem(C.hChapterDir) is not None
|
||||||
assert nwGUI.projView.projTree._getTreeItem("000000000000e") is not None
|
assert nwGUI.projView.projTree._getTreeItem(C.hChapterDoc) is not None
|
||||||
assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None
|
assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None
|
||||||
|
|
||||||
nwGUI.mainMenu.aSpellCheck.setChecked(True)
|
nwGUI.mainMenu.aSpellCheck.setChecked(True)
|
||||||
assert nwGUI.mainMenu._toggleSpellCheck()
|
assert nwGUI.mainMenu._toggleSpellCheck()
|
||||||
@@ -238,7 +237,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
|
|||||||
# Add a Character File
|
# Add a Character File
|
||||||
nwGUI.switchFocus(nwWidget.TREE)
|
nwGUI.switchFocus(nwWidget.TREE)
|
||||||
nwGUI.projView.projTree.clearSelection()
|
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)
|
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||||
assert nwGUI.openSelectedItem()
|
assert nwGUI.openSelectedItem()
|
||||||
|
|
||||||
@@ -260,7 +259,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
|
|||||||
# Add a Plot File
|
# Add a Plot File
|
||||||
nwGUI.switchFocus(nwWidget.TREE)
|
nwGUI.switchFocus(nwWidget.TREE)
|
||||||
nwGUI.projView.projTree.clearSelection()
|
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)
|
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||||
assert nwGUI.openSelectedItem()
|
assert nwGUI.openSelectedItem()
|
||||||
|
|
||||||
@@ -282,7 +281,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
|
|||||||
# Add a World File
|
# Add a World File
|
||||||
nwGUI.switchFocus(nwWidget.TREE)
|
nwGUI.switchFocus(nwWidget.TREE)
|
||||||
nwGUI.projView.projTree.clearSelection()
|
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)
|
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||||
assert nwGUI.openSelectedItem()
|
assert nwGUI.openSelectedItem()
|
||||||
|
|
||||||
@@ -313,9 +312,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
|
|||||||
# Select the 'New Scene' file
|
# Select the 'New Scene' file
|
||||||
nwGUI.switchFocus(nwWidget.TREE)
|
nwGUI.switchFocus(nwWidget.TREE)
|
||||||
nwGUI.projView.projTree.clearSelection()
|
nwGUI.projView.projTree.clearSelection()
|
||||||
nwGUI.projView.projTree._getTreeItem("0000000000008").setExpanded(True)
|
nwGUI.projView.projTree._getTreeItem(C.hNovelRoot).setExpanded(True)
|
||||||
nwGUI.projView.projTree._getTreeItem("000000000000d").setExpanded(True)
|
nwGUI.projView.projTree._getTreeItem(C.hChapterDir).setExpanded(True)
|
||||||
nwGUI.projView.projTree._getTreeItem("000000000000f").setSelected(True)
|
nwGUI.projView.projTree._getTreeItem(C.hSceneDoc).setSelected(True)
|
||||||
assert nwGUI.openSelectedItem()
|
assert nwGUI.openSelectedItem()
|
||||||
|
|
||||||
# Type something into the document
|
# 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
|
# Open and view the edited document
|
||||||
nwGUI.switchFocus(nwWidget.VIEWER)
|
nwGUI.switchFocus(nwWidget.VIEWER)
|
||||||
assert nwGUI.openDocument("000000000000f")
|
assert nwGUI.openDocument(C.hSceneDoc)
|
||||||
assert nwGUI.viewDocument("000000000000f")
|
assert nwGUI.viewDocument(C.hSceneDoc)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
assert nwGUI.saveProject()
|
assert nwGUI.saveProject()
|
||||||
assert nwGUI.closeDocViewer()
|
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)
|
assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None)
|
||||||
newHandle = nwGUI.projView.getSelectedHandle()
|
newHandle = nwGUI.projView.getSelectedHandle()
|
||||||
assert nwGUI.theProject.tree["0000000000020"] is not None
|
assert nwGUI.theProject.tree["0000000000020"] is not None
|
||||||
assert nwGUI.projView.deleteItem()
|
assert nwGUI.projView.requestDeleteItem()
|
||||||
assert nwGUI.projView.setSelectedHandle(newHandle)
|
assert nwGUI.projView.setSelectedHandle(newHandle)
|
||||||
assert nwGUI.projView.deleteItem()
|
assert nwGUI.projView.requestDeleteItem()
|
||||||
assert nwGUI.theProject.tree["0000000000024"] is not None # Trash
|
assert nwGUI.theProject.tree["0000000000024"] is not None # Trash
|
||||||
assert nwGUI.saveProject()
|
assert nwGUI.saveProject()
|
||||||
|
|
||||||
@@ -562,8 +561,8 @@ def testGuiMain_FocusFullMode(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
|||||||
assert nwGUI.toggleFocusMode() is False
|
assert nwGUI.toggleFocusMode() is False
|
||||||
|
|
||||||
# Open a file in editor and viewer
|
# Open a file in editor and viewer
|
||||||
assert nwGUI.openDocument("000000000000f")
|
assert nwGUI.openDocument(C.hSceneDoc)
|
||||||
assert nwGUI.viewDocument("000000000000f")
|
assert nwGUI.viewDocument(C.hSceneDoc)
|
||||||
|
|
||||||
# Enable focus mode
|
# Enable focus mode
|
||||||
assert nwGUI.toggleFocusMode() is True
|
assert nwGUI.toggleFocusMode() is True
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from PyQt5.QtCore import Qt
|
|||||||
from PyQt5.QtGui import QTextCursor, QTextBlock
|
from PyQt5.QtGui import QTextCursor, QTextBlock
|
||||||
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
|
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.gui.doceditor import GuiDocEditor
|
||||||
from novelwriter.enum import nwDocAction, nwDocInsert
|
from novelwriter.enum import nwDocAction, nwDocInsert
|
||||||
@@ -467,8 +467,8 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd):
|
|||||||
|
|
||||||
buildTestProject(nwGUI, fncProj)
|
buildTestProject(nwGUI, fncProj)
|
||||||
|
|
||||||
assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None
|
assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None
|
||||||
assert nwGUI.openDocument("000000000000f") is True
|
assert nwGUI.openDocument(C.hSceneDoc) is True
|
||||||
nwGUI.docEditor.clear()
|
nwGUI.docEditor.clear()
|
||||||
|
|
||||||
# Test Faulty Inserts
|
# Test Faulty Inserts
|
||||||
@@ -677,7 +677,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd):
|
|||||||
assert not nwGUI.importDocument()
|
assert not nwGUI.importDocument()
|
||||||
|
|
||||||
# Open the document from before, and add some text to it
|
# Open the document from before, and add some text to it
|
||||||
nwGUI.openDocument("000000000000f")
|
nwGUI.openDocument(C.hSceneDoc)
|
||||||
nwGUI.docEditor.setText("Bar")
|
nwGUI.docEditor.setText("Bar")
|
||||||
assert nwGUI.docEditor.getText() == "Bar"
|
assert nwGUI.docEditor.getText() == "Bar"
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tools import buildTestProject, writeFile
|
from tools import C, buildTestProject, writeFile
|
||||||
|
|
||||||
from PyQt5.QtGui import QFocusEvent
|
from PyQt5.QtGui import QFocusEvent
|
||||||
from PyQt5.QtCore import Qt, QEvent
|
from PyQt5.QtCore import Qt, QEvent
|
||||||
@@ -46,7 +46,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
|||||||
|
|
||||||
nwGUI.switchFocus(nwWidget.TREE)
|
nwGUI.switchFocus(nwWidget.TREE)
|
||||||
nwGUI.projView.projTree.clearSelection()
|
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)
|
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
|
||||||
|
|
||||||
writeFile(
|
writeFile(
|
||||||
@@ -94,7 +94,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
|||||||
assert not topItem.isSelected()
|
assert not topItem.isSelected()
|
||||||
topItem.setSelected(True)
|
topItem.setSelected(True)
|
||||||
assert novelTree.selectedItems()[0] == topItem
|
assert novelTree.selectedItems()[0] == topItem
|
||||||
assert novelView.getSelectedHandle() == ("000000000000c", 0)
|
assert novelView.getSelectedHandle() == (C.hTitlePage, 0)
|
||||||
|
|
||||||
# Refresh using the slot for the butoom
|
# Refresh using the slot for the butoom
|
||||||
novelBar._refreshNovelTree()
|
novelBar._refreshNovelTree()
|
||||||
@@ -119,7 +119,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
|||||||
assert scItem.isSelected()
|
assert scItem.isSelected()
|
||||||
assert nwGUI.docEditor.docHandle() is None
|
assert nwGUI.docEditor.docHandle() is None
|
||||||
novelTree._treeDoubleClick(scItem, 0)
|
novelTree._treeDoubleClick(scItem, 0)
|
||||||
assert nwGUI.docEditor.docHandle() == "000000000000f"
|
assert nwGUI.docEditor.docHandle() == C.hSceneDoc
|
||||||
|
|
||||||
# Open item with middle mouse button
|
# Open item with middle mouse button
|
||||||
scItem.setSelected(True)
|
scItem.setSelected(True)
|
||||||
@@ -136,7 +136,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
|||||||
|
|
||||||
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData)
|
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData)
|
||||||
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
|
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
|
||||||
assert nwGUI.docViewer.docHandle() == "000000000000f"
|
assert nwGUI.docViewer.docHandle() == C.hSceneDoc
|
||||||
|
|
||||||
# Last Column
|
# Last Column
|
||||||
# ===========
|
# ===========
|
||||||
@@ -144,26 +144,26 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
|||||||
novelBar.setLastColType(NovelTreeColumn.HIDDEN)
|
novelBar.setLastColType(NovelTreeColumn.HIDDEN)
|
||||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
|
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
|
||||||
assert novelTree.lastColType == NovelTreeColumn.HIDDEN
|
assert novelTree.lastColType == NovelTreeColumn.HIDDEN
|
||||||
assert novelTree._getLastColumnText("000000000000f", "T000001") == ("", "")
|
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ("", "")
|
||||||
|
|
||||||
novelBar.setLastColType(NovelTreeColumn.POV)
|
novelBar.setLastColType(NovelTreeColumn.POV)
|
||||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
||||||
assert novelTree.lastColType == NovelTreeColumn.POV
|
assert novelTree.lastColType == NovelTreeColumn.POV
|
||||||
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
|
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == (
|
||||||
"Jane", "Point of View: Jane"
|
"Jane", "Point of View: Jane"
|
||||||
)
|
)
|
||||||
|
|
||||||
novelBar.setLastColType(NovelTreeColumn.FOCUS)
|
novelBar.setLastColType(NovelTreeColumn.FOCUS)
|
||||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
||||||
assert novelTree.lastColType == NovelTreeColumn.FOCUS
|
assert novelTree.lastColType == NovelTreeColumn.FOCUS
|
||||||
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
|
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == (
|
||||||
"Jane", "Focus: Jane"
|
"Jane", "Focus: Jane"
|
||||||
)
|
)
|
||||||
|
|
||||||
novelBar.setLastColType(NovelTreeColumn.PLOT)
|
novelBar.setLastColType(NovelTreeColumn.PLOT)
|
||||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
||||||
assert novelTree.lastColType == NovelTreeColumn.PLOT
|
assert novelTree.lastColType == NovelTreeColumn.PLOT
|
||||||
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
|
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == (
|
||||||
"", "Plot: "
|
"", "Plot: "
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+536
-242
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tools import buildTestProject
|
from tools import C, buildTestProject
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
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)
|
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||||
|
|
||||||
buildTestProject(nwGUI, fncProj)
|
buildTestProject(nwGUI, fncProj)
|
||||||
cHandle = nwGUI.theProject.newFile("A Note", "000000000000a")
|
cHandle = nwGUI.theProject.newFile("A Note", C.hCharRoot)
|
||||||
newDoc = NWDoc(nwGUI.theProject, cHandle)
|
newDoc = NWDoc(nwGUI.theProject, cHandle)
|
||||||
newDoc.writeDocument("# A Note\n\n")
|
newDoc.writeDocument("# A Note\n\n")
|
||||||
nwGUI.projView.revealNewTreeItem(cHandle)
|
nwGUI.projView.revealNewTreeItem(cHandle)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tools import getGuiItem, buildTestProject
|
from tools import C, getGuiItem, buildTestProject
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QAction, QMessageBox
|
from PyQt5.QtWidgets import QAction, QMessageBox
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
|||||||
|
|
||||||
# Create a new project
|
# Create a new project
|
||||||
buildTestProject(nwGUI, fncProj)
|
buildTestProject(nwGUI, fncProj)
|
||||||
assert nwGUI.openDocument("000000000000f") is True
|
assert nwGUI.openDocument(C.hSceneDoc) is True
|
||||||
assert len(nwGUI.docEditor.getText()) == 15
|
assert len(nwGUI.docEditor.getText()) == 15
|
||||||
|
|
||||||
# Open the tool
|
# Open the tool
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ from PyQt5.QtWidgets import qApp
|
|||||||
XML_IGNORE = ("<novelWriterXML", "<saveCount", "<autoCount", "<editTime")
|
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):
|
def cmpFiles(fileOne, fileTwo, ignoreLines=None, ignoreStart=None):
|
||||||
"""Compare two files, but optionally ignore lines given by a list.
|
"""Compare two files, but optionally ignore lines given by a list.
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user