From e1c3a12d07a28053ac7222a0e5f0a8d6f960e354 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 9 Oct 2022 15:30:02 +0200
Subject: [PATCH] Redesign the Document Merge dialog box
---
novelwriter/core/__init__.py | 2 +
novelwriter/core/doctools.py | 102 +++++++++++++++++
novelwriter/dialogs/docmerge.py | 189 +++++++++++++-------------------
novelwriter/gui/custom.py | 14 +--
novelwriter/gui/projtree.py | 62 +++++++++--
5 files changed, 238 insertions(+), 131 deletions(-)
create mode 100644 novelwriter/core/doctools.py
diff --git a/novelwriter/core/__init__.py b/novelwriter/core/__init__.py
index c91ca941..90b103f8 100644
--- a/novelwriter/core/__init__.py
+++ b/novelwriter/core/__init__.py
@@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+from novelwriter.core.doctools import DocMerger
from novelwriter.core.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
@@ -28,6 +29,7 @@ from novelwriter.core.toodt import ToOdt
from novelwriter.core.tomd import ToMarkdown
__all__ = [
+ "DocMerger",
"countWords",
"NWDoc",
"NWProject",
diff --git a/novelwriter/core/doctools.py b/novelwriter/core/doctools.py
new file mode 100644
index 00000000..b85d1c19
--- /dev/null
+++ b/novelwriter/core/doctools.py
@@ -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 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 .
+"""
+
+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
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index 22a6eb6b..2d248200 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -1,10 +1,11 @@
"""
-novelWriter – GUI Doc Merge Tool
-================================
-GUI class for merging multiple documents to one document
+novelWriter – GUI Doc Merge Dialog
+==================================
+Custom dialog class for merging documents.
File History:
-Created: 2020-01-23 [0.4.3]
+Created: 2020-01-23 [0.4.3]
+Rewritten: 2022-10-06 [2.0b1]
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -26,169 +27,131 @@ along with this program. If not, see .
import logging
import novelwriter
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
- QListWidgetItem, QDialogButtonBox
+ QListWidgetItem, QDialogButtonBox, QGridLayout
)
-from novelwriter.core import NWDoc
-from novelwriter.enum import nwAlert, nwItemType
-from novelwriter.gui.custom import QHelpLabel
+from novelwriter.gui.custom import QHelpLabel, QSwitch
logger = logging.getLogger(__name__)
class GuiDocMerge(QDialog):
- def __init__(self, mainGui):
- QDialog.__init__(self, mainGui)
+ def __init__(self, mainGui, sHandle, itemList):
+ super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocMerge ...")
self.setObjectName("GuiDocMerge")
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
+ self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
- self.sourceItem = None
- self.outerBox = QVBoxLayout()
+ self._data = {}
+
self.setWindowTitle(self.tr("Merge Documents"))
self.headLabel = QLabel("{0}".format(self.tr("Documents to Merge")))
- self.helpLabel = QHelpLabel(
- self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText
- )
+ self.helpLabel = QHelpLabel(self.tr(
+ "Drag and drop items to change the order, or uncheck to exclude."
+ ), self.mainTheme.helpText)
+
+ iPx = self.mainTheme.baseIconSize
+ hSp = self.mainConf.pxInt(12)
+ vSp = self.mainConf.pxInt(8)
+ bSp = self.mainConf.pxInt(12)
self.listBox = QListWidget()
- self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
+ self.listBox.setIconSize(QSize(iPx, iPx))
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
+ self.listBox.setSelectionBehavior(QAbstractItemView.SelectRows)
+ self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
+ # Merge Options
+ self.trashLabel = QLabel(self.tr("Move merged items to Trash"))
+ self.trashSwitch = QSwitch()
+
+ self.optBox = QGridLayout()
+ self.optBox.addWidget(self.trashLabel, 0, 0)
+ self.optBox.addWidget(self.trashSwitch, 0, 1)
+ self.optBox.setHorizontalSpacing(hSp)
+ self.optBox.setColumnStretch(2, 1)
+
+ # Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
- self.buttonBox.accepted.connect(self._doMerge)
- self.buttonBox.rejected.connect(self._doClose)
+ self.buttonBox.accepted.connect(self.accept)
+ self.buttonBox.rejected.connect(self.reject)
+ # Assemble
+ self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel)
- self.outerBox.addSpacing(self.mainConf.pxInt(8))
+ self.outerBox.addSpacing(vSp)
self.outerBox.addWidget(self.listBox)
- self.outerBox.addSpacing(self.mainConf.pxInt(12))
+ self.outerBox.addSpacing(vSp)
+ self.outerBox.addLayout(self.optBox)
+ self.outerBox.addSpacing(bSp)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
- self.rejected.connect(self._doClose)
-
- self._populateList()
+ # Load Content
+ self._loadContent(sHandle, itemList)
logger.debug("GuiDocMerge initialisation complete")
return
- ##
- # Buttons
- ##
-
- def _doMerge(self):
- """Perform the merge of the files in the selected folder, and
- create a new file in the same parent folder. The old files are
- not removed in the merge process, and must be deleted manually.
+ def getData(self):
+ """Return the user's choices.
"""
- logger.verbose("GuiDocMerge merge button clicked")
-
- finalOrder = []
+ finalItems = []
for i in range(self.listBox.count()):
- finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
+ item = self.listBox.item(i)
+ if item.checkState() == Qt.Checked:
+ finalItems.append(item.data(Qt.UserRole))
- if len(finalOrder) == 0:
- self.mainGui.makeAlert(self.tr(
- "No source documents found. Nothing to do."
- ), nwAlert.ERROR)
- return False
+ self._data["moveToTrash"] = self.trashSwitch.isChecked()
+ self._data["finalItems"] = finalItems
- theText = ""
- for tHandle in finalOrder:
- inDoc = NWDoc(self.theProject, tHandle)
- docText = inDoc.readDocument()
- docErr = inDoc.getError()
- if docText is None and docErr:
- self.mainGui.makeAlert([
- self.tr("Failed to open document file."), docErr
- ], nwAlert.ERROR)
- if docText:
- theText += docText.rstrip("\n")+"\n\n"
-
- 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
+ return self._data
##
# Internal Functions
##
- def _populateList(self):
- """Get the item selected in the tree, check that it is a folder,
- and try to find all files associated with it. The valid files
- are then added to the list view in order. The list itself can be
- reordered by the user.
+ def _loadContent(self, sHandle, itemList):
+ """Load content from a given list of items.
"""
- tHandle = self.mainGui.projView.getSelectedHandle()
- self.sourceItem = tHandle
- if tHandle is None:
- return False
+ self._data = {}
+ self._data["sHandle"] = sHandle
+ self._data["origItems"] = itemList
- nwItem = self.theProject.tree[tHandle]
- if nwItem is None:
- return False
-
- if nwItem.itemType is not nwItemType.FOLDER:
- self.mainGui.makeAlert(self.tr(
- "Element selected in the project tree must be a folder."
- ), nwAlert.ERROR)
- return False
-
- for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle):
- newItem = QListWidgetItem()
- nwItem = self.theProject.tree[sHandle]
- if not nwItem.isFileType():
+ self.listBox.clear()
+ for tHandle in itemList:
+ nwItem = self.theProject.tree[tHandle]
+ if nwItem is None or not nwItem.isFileType():
continue
+
+ hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
+ itemIcon = self.mainTheme.getItemIcon(
+ nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
+ )
+
+ newItem = QListWidgetItem()
+ newItem.setIcon(itemIcon)
newItem.setText(nwItem.itemName)
- newItem.setData(Qt.UserRole, sHandle)
+ newItem.setData(Qt.UserRole, tHandle)
+ newItem.setCheckState(Qt.Checked)
+
self.listBox.addItem(newItem)
- return True
+ return
# END Class GuiDocMerge
diff --git a/novelwriter/gui/custom.py b/novelwriter/gui/custom.py
index e571a5a1..2949ff6a 100644
--- a/novelwriter/gui/custom.py
+++ b/novelwriter/gui/custom.py
@@ -202,7 +202,7 @@ class QConfigLayout(QGridLayout):
class QHelpLabel(QLabel):
def __init__(self, theText, textCol, fontSize=0.9):
- QLabel.__init__(self, theText)
+ super().__init__(theText)
if isinstance(textCol, QColor):
qCol = textCol
@@ -377,7 +377,7 @@ class QSwitch(QAbstractButton):
class PagedDialog(QDialog):
def __init__(self, parent=None):
- QDialog.__init__(self, parent=parent)
+ super().__init__(parent=parent)
self._tabBar = VerticalTabBar(self)
self._tabBar.setExpanding(False)
@@ -410,13 +410,13 @@ class PagedDialog(QDialog):
return
def addTab(self, widget, label):
- """Forwards the adding of tabs to the QTabWidget.
+ """Forward the adding of tabs to the QTabWidget.
"""
self._tabBox.addTab(widget, label)
return
def addControls(self, buttonBar):
- """Adds a button bar to the dialog.
+ """Add a button bar to the dialog.
"""
self._buttonBox.addWidget(buttonBar)
return
@@ -427,14 +427,14 @@ class PagedDialog(QDialog):
class VerticalTabBar(QTabBar):
def __init__(self, parent=None):
- QTabBar.__init__(self, parent=parent)
+ super().__init__(parent=parent)
self._mW = novelwriter.CONFIG.pxInt(150)
return
def tabSizeHint(self, index):
- """Returns a transposed size hint for the rotated bar.
+ """Return a transposed size hint for the rotated bar.
"""
- tSize = QTabBar.tabSizeHint(self, index)
+ tSize = super().tabSizeHint(index)
tSize.transpose()
tSize.setWidth(min(tSize.width(), self._mW))
return tSize
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index e8fa541d..e841375a 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -34,15 +34,15 @@ from time import time
from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
- QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel,
+ QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel, QDialog,
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget
)
from novelwriter.core import NWDoc
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
+from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
from novelwriter.constants import nwHeaders, trConst, nwLabels
-from novelwriter.dialogs.editlabel import GuiEditLabel
logger = logging.getLogger(__name__)
@@ -334,7 +334,7 @@ class GuiProjectTree(QTreeWidget):
self.mainConf = novelwriter.CONFIG
self.projView = projView
self.mainGui = projView.mainGui
- self.mainTheme = projView.mainGui.mainTheme
+ self.mainTheme = projView.mainGui.mainTheme
self.theProject = projView.mainGui.theProject
# Internal Variables
@@ -382,9 +382,8 @@ class GuiProjectTree(QTreeWidget):
trRoot = self.invisibleRootItem()
trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled)
- # Set Multiple Selection by CTRL
- # Disabled for now, until the merge files option has been added
- # self.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ # Set selection options
+ self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
# Connect signals
@@ -1077,16 +1076,21 @@ class GuiProjectTree(QTreeWidget):
lambda n, key=key: self._changeItemImport(tHandle, key)
)
+ # Transform Item
+ # ==============
+
+ mTrans = ctxMenu.addMenu(self.tr("Transform"))
+
if isFile and tItem.documentAllowed():
if tItem.isNoteLayout():
- ctxMenu.addAction(
+ mTrans.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
),
lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT)
)
else:
- ctxMenu.addAction(
+ mTrans.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
),
@@ -1094,19 +1098,37 @@ class GuiProjectTree(QTreeWidget):
)
elif isFolder:
if tItem.documentAllowed():
- ctxMenu.addAction(
+ mTrans.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
),
lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT)
)
- ctxMenu.addAction(
+ mTrans.addAction(
self.tr("Convert to {0}").format(
trConst(nwLabels.LAYOUT_NAME[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()
# Expand/Collapse
@@ -1355,6 +1377,24 @@ class GuiProjectTree(QTreeWidget):
logger.info("Folder conversion cancelled")
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):
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
@@ -1380,7 +1420,7 @@ class GuiProjectTree(QTreeWidget):
"""
tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent
- newItem = QTreeWidgetItem([""]*4)
+ newItem = QTreeWidgetItem()
newItem.setText(self.C_NAME, "")
newItem.setText(self.C_COUNT, "0")