Redesign the Document Merge dialog box

This commit is contained in:
Veronica Berglyd Olsen
2022-10-09 15:30:02 +02:00
parent 43bb5aab5c
commit e1c3a12d07
5 changed files with 238 additions and 131 deletions
+2
View File
@@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. 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",
+102
View File
@@ -0,0 +1,102 @@
"""
novelWriter Project Document Tools
====================================
A collection of tools to create and manipulate documents
File History:
Created: 2022-10-02 [2.0b1]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
logger = logging.getLogger(__name__)
# logger.verbose("GuiDocMerge merge button clicked")
# finalOrder = []
# for i in range(self.listBox.count()):
# finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
# if len(finalOrder) == 0:
# self.mainGui.makeAlert(self.tr(
# "No source documents found. Nothing to do."
# ), nwAlert.ERROR)
# return False
# theText = ""
# for tHandle in finalOrder:
# inDoc = NWDoc(self.theProject, tHandle)
# docText = inDoc.readDocument()
# docErr = inDoc.getError()
# if docText is None and docErr:
# self.mainGui.makeAlert([
# self.tr("Failed to open document file."), docErr
# ], nwAlert.ERROR)
# if docText:
# theText += docText.rstrip("\n")+"\n\n"
# if self.sourceItem is None:
# self.mainGui.makeAlert(self.tr(
# "No source folder selected. Nothing to do."
# ), nwAlert.ERROR)
# return False
# srcItem = self.theProject.tree[self.sourceItem]
# if srcItem is None:
# self.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
# return False
# nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
# newItem = self.theProject.tree[nHandle]
# newItem.setStatus(srcItem.itemStatus)
# newItem.setImport(srcItem.itemImport)
# outDoc = NWDoc(self.theProject, nHandle)
# if not outDoc.writeDocument(theText):
# self.mainGui.makeAlert([
# self.tr("Could not save document."), outDoc.getError()
# ], nwAlert.ERROR)
# return False
# self.mainGui.projView.revealNewTreeItem(nHandle)
# self.mainGui.openDocument(nHandle, doScroll=True)
# self._doClose()
class DocMerger:
def __init__(self, theProject):
self.theProject = theProject
self._targetDoc = None
return
def setTargetDoc(self, tHandle):
return
def createNewDoc(self, docLabel, pHandle, itemLayout):
return
def appendDoc(self, tHandle):
return
# END Class DocMerger
+76 -113
View File
@@ -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 20182022, Veronica Berglyd Olsen Copyright 20182022, Veronica Berglyd Olsen
@@ -26,169 +27,131 @@ 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, QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
QListWidgetItem, QDialogButtonBox QListWidgetItem, QDialogButtonBox, QGridLayout
) )
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)
# 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(
"No source folder selected. Nothing to do."
), nwAlert.ERROR)
return False
srcItem = self.theProject.tree[self.sourceItem]
if srcItem is None:
self.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
return False
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
newItem = self.theProject.tree[nHandle]
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
outDoc = NWDoc(self.theProject, nHandle)
if not outDoc.writeDocument(theText):
self.mainGui.makeAlert([
self.tr("Could not save document."), outDoc.getError()
], nwAlert.ERROR)
return False
self.mainGui.projView.revealNewTreeItem(nHandle)
self.mainGui.openDocument(nHandle, doScroll=True)
self._doClose()
return True
def _doClose(self):
"""Close the dialog window without doing anything.
"""
self.close()
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
+7 -7
View File
@@ -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
+51 -11
View File
@@ -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, QFrame, QHBoxLayout, QHeaderView, QLabel, QDialog,
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 NWDoc
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__)
@@ -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
@@ -1077,16 +1076,21 @@ class GuiProjectTree(QTreeWidget):
lambda n, key=key: self._changeItemImport(tHandle, key) lambda n, key=key: self._changeItemImport(tHandle, key)
) )
# Transform Item
# ==============
mTrans = ctxMenu.addMenu(self.tr("Transform"))
if isFile and tItem.documentAllowed(): if isFile and tItem.documentAllowed():
if tItem.isNoteLayout(): if tItem.isNoteLayout():
ctxMenu.addAction( mTrans.addAction(
self.tr("Convert to {0}").format( self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT]) trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
), ),
lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT) lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT)
) )
else: else:
ctxMenu.addAction( mTrans.addAction(
self.tr("Convert to {0}").format( self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE]) trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
), ),
@@ -1094,19 +1098,37 @@ class GuiProjectTree(QTreeWidget):
) )
elif isFolder: elif isFolder:
if tItem.documentAllowed(): if tItem.documentAllowed():
ctxMenu.addAction( mTrans.addAction(
self.tr("Convert to {0}").format( self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT]) trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
), ),
lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT) lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT)
) )
ctxMenu.addAction( mTrans.addAction(
self.tr("Convert to {0}").format( self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE]) trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
), ),
lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE) lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE)
) )
if hasChild:
if isFile:
mTrans.addAction(
self.tr("Merge Child Documents"),
lambda: self._mergeDocuments(tHandle, isFile)
)
else:
mTrans.addAction(
self.tr("Combine Documents in Folder"),
lambda: self._mergeDocuments(tHandle, isFile)
)
if isFile:
mTrans.addAction(
self.tr("Split Document by Header"),
lambda: self._splitDocument(tHandle)
)
ctxMenu.addSeparator() ctxMenu.addSeparator()
# Expand/Collapse # Expand/Collapse
@@ -1355,6 +1377,24 @@ class GuiProjectTree(QTreeWidget):
logger.info("Folder conversion cancelled") logger.info("Folder conversion cancelled")
return return
def _mergeDocuments(self, tHandle, isFile):
"""Merge an item's child documents into a single document.
"""
logger.info("Request to merge items under handle '%s'", tHandle)
itemList = self.getTreeFromHandle(tHandle)
itemList.remove(tHandle)
dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
dlgMerge.exec_()
if dlgMerge.result() == QDialog.Accepted:
print(dlgMerge.getData())
return
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 +1420,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")