From 0c18f1d3c90655fcc6b827e97fa6c5065cbc1836 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jun 2020 10:57:30 +0200 Subject: [PATCH 1/4] Added project tree context menu --- nw/gui/projtree.py | 207 ++++++++++++++++++++++++++++++--------------- 1 file changed, 140 insertions(+), 67 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 72246c3a..b6b26143 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -6,7 +6,8 @@ Class holding the left side document tree view File History: - Created: 2018-09-29 [0.0.1] + Created: 2018-09-29 [0.0.1] GuiProjectTree + Created: 2020-06-04 [0.7.0] GuiProjectTreeMenu This file is a part of novelWriter Copyright 2020, Veronica Berglyd Olsen @@ -32,7 +33,7 @@ from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QFont, QColor, QIcon from PyQt5.QtWidgets import ( qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMessageBox, - QHeaderView + QHeaderView, QMenu, QAction ) from nw.core import NWDoc @@ -57,6 +58,7 @@ class GuiProjectTree(QTreeWidget): self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theProject + self.ctxMenu = GuiProjectTreeMenu(self) # Tree Settings self.theMap = None @@ -71,6 +73,8 @@ class GuiProjectTree(QTreeWidget): self.setIndentation(iPx) self.setColumnCount(4) self.setHeaderLabels(["Label", "Words", "Inc", "Flags"]) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._rightClickMenu) treeHeadItem = self.headerItem() treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) @@ -556,6 +560,83 @@ class GuiProjectTree(QTreeWidget): return True return False + ## + # Slots + ## + + def _rightClickMenu(self, clickPos): + """The user right clicked an element in the project tree, so we + open a context menu in-place. + """ + selItem = self.itemAt(clickPos) + self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) + return + + ## + # Events + ## + + def mousePressEvent(self, theEvent): + """Overload mousePressEvent to clear selection if clicking the + mouse in a blank area of the tree view. + """ + QTreeWidget.mousePressEvent(self, theEvent) + selItem = self.indexAt(theEvent.pos()) + if not selItem.isValid(): + self.clearSelection() + return + + def dropEvent(self, theEvent): + """Overload the drop of dragged item event to check whether the + drop is allowed or not. Disallowed drops are cancelled. + """ + sHandle = self.getSelectedHandle() + if sHandle is None: + logger.error("No handle selected") + return + + dIndex = self.indexAt(theEvent.pos()) + if not dIndex.isValid(): + logger.error("Invalid drop index") + return + + dItem = self.itemFromIndex(dIndex) + dHandle = dItem.data(self.C_NAME, Qt.UserRole) + snItem = self.theProject.projTree[sHandle] + dnItem = self.theProject.projTree[dHandle] + if dnItem is None: + self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) + return + + isSame = snItem.itemClass == dnItem.itemClass + isNone = snItem.itemClass == nwItemClass.NO_CLASS + isNote = snItem.itemLayout == nwItemLayout.NOTE + onFile = dnItem.itemType == nwItemType.FILE + isRoot = snItem.itemType == nwItemType.ROOT + onRoot = dnItem.itemType == nwItemType.ROOT + isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem + if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot: + logger.debug("Drag'n'drop of item %s accepted" % sHandle) + QTreeWidget.dropEvent(self, theEvent) + if isNone: + self._moveOrphanedItem(sHandle, dHandle) + self._cleanOrphanedRoot() + else: + self._updateItemParent(sHandle) + if not isSame: + logger.debug("Item %s class has been changed from %s to %s" % ( + sHandle, + snItem.itemClass.name, + dnItem.itemClass.name + )) + snItem.setClass(dnItem.itemClass) + self.setTreeItemValues(sHandle) + else: + logger.debug("Drag'n'drop of item %s not accepted" % sHandle) + self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) + + return + ## # Internal Functions ## @@ -717,69 +798,61 @@ class GuiProjectTree(QTreeWidget): self.theProject.setProjectChanged(True) return - ## - # Event Overloading - ## - - def mousePressEvent(self, theEvent): - """Overload mousePressEvent to clear selection if clicking the - mouse in a blank area of the tree view. - """ - QTreeWidget.mousePressEvent(self, theEvent) - selItem = self.indexAt(theEvent.pos()) - if not selItem.isValid(): - self.clearSelection() - return - - def dropEvent(self, theEvent): - """Overload the drop of dragged item event to check whether the - drop is allowed or not. Disallowed drops are cancelled. - """ - sHandle = self.getSelectedHandle() - if sHandle is None: - logger.error("No handle selected") - return - - dIndex = self.indexAt(theEvent.pos()) - if not dIndex.isValid(): - logger.error("Invalid drop index") - return - - dItem = self.itemFromIndex(dIndex) - dHandle = dItem.data(self.C_NAME, Qt.UserRole) - snItem = self.theProject.projTree[sHandle] - dnItem = self.theProject.projTree[dHandle] - if dnItem is None: - self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) - return - - isSame = snItem.itemClass == dnItem.itemClass - isNone = snItem.itemClass == nwItemClass.NO_CLASS - isNote = snItem.itemLayout == nwItemLayout.NOTE - onFile = dnItem.itemType == nwItemType.FILE - isRoot = snItem.itemType == nwItemType.ROOT - onRoot = dnItem.itemType == nwItemType.ROOT - isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem - if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot: - logger.debug("Drag'n'drop of item %s accepted" % sHandle) - QTreeWidget.dropEvent(self, theEvent) - if isNone: - self._moveOrphanedItem(sHandle, dHandle) - self._cleanOrphanedRoot() - else: - self._updateItemParent(sHandle) - if not isSame: - logger.debug("Item %s class has been changed from %s to %s" % ( - sHandle, - snItem.itemClass.name, - dnItem.itemClass.name - )) - snItem.setClass(dnItem.itemClass) - self.setTreeItemValues(sHandle) - else: - logger.debug("Drag'n'drop of item %s not accepted" % sHandle) - self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) - - return - # END Class GuiProjectTree + +class GuiProjectTreeMenu(QMenu): + + def __init__(self, theTree): + QMenu.__init__(self, theTree) + + self.theTree = theTree + self.theItem = theItem + + self.editItem = QAction("Edit Item", self) + self.editItem.triggered.connect(self._doEditItem) + self.addAction(self.editItem) + + self.toggleExp = QAction("Toggle Exported", self) + self.toggleExp.triggered.connect(self._doToggleExported) + self.addAction(self.toggleExp) + + self.addSeparator() + + self.newFolder = QAction("New Folder", self) + self.newFolder.triggered.connect(self._doMakeFolder) + self.addAction(self.newFolder) + + self.newFile = QAction("New File", self) + self.newFile.triggered.connect(self._doMakeFile) + self.addAction(self.newFile) + + self.deleteItem = QAction("Delete Item", self) + self.deleteItem.triggered.connect(self._doDeleteItem) + self.addAction(self.deleteItem) + + return + + def updateFromItem(self, theItem): + self.theItem = theItem + return + + ## + # Slots + ## + + def _doEditItem(self): + return + + def _doDeleteItem(self): + return + + def _doMakeFolder(self): + return + + def _doMakeFile(self): + return + + def _doToggleExported(self): + return + +# END Class GuiProjectTreeMenu From 72e5778e4f9fff6d2a032a1e048ef56934e1442d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jun 2020 17:09:36 +0200 Subject: [PATCH 2/4] Context menu now works for basic functions --- nw/gui/projtree.py | 125 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 98 insertions(+), 27 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index b6b26143..5a7c9032 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -339,7 +339,7 @@ class GuiProjectTree(QTreeWidget): return True - def deleteItem(self, tHandle=None, alreadyAsked=False): + def deleteItem(self, tHandle=None, alreadyAsked=False, askForTrash=False): """Delete items from the tree. Note that this does not delete the item from the item tree in the project object. However, since this is only meta data, there isn't really a need to do @@ -370,7 +370,6 @@ class GuiProjectTree(QTreeWidget): if pHandle is not None and pHandle == self.theProject.projTree.trashRoot(): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. - doPermanent = False if self.mainConf.showGUI and not alreadyAsked: msgBox = QMessageBox() @@ -399,17 +398,28 @@ class GuiProjectTree(QTreeWidget): else: # The file is not already in the trash folder, so we # move it there. + doTrash = False + if self.mainConf.showGUI and askForTrash: + msgBox = QMessageBox() + msgRes = msgBox.question( + self, "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName + ) + if msgRes == QMessageBox.Yes: + doTrash = True + else: + doTrash = True - if pHandle is None: - logger.warning("File has no parent item") + if doTrash: + if pHandle is None: + logger.warning("File has no parent item") - tIndex = trItemP.indexOfChild(trItemS) - trItemC = trItemP.takeChild(tIndex) - trItemT.addChild(trItemC) - nwItemS.setParent(self.theProject.projTree.trashRoot()) + tIndex = trItemP.indexOfChild(trItemS) + trItemC = trItemP.takeChild(tIndex) + trItemT.addChild(trItemC) + nwItemS.setParent(self.theProject.projTree.trashRoot()) - self.theProject.setProjectChanged(True) - self.theParent.theIndex.deleteHandle(tHandle) + self.theProject.setProjectChanged(True) + self.theParent.theIndex.deleteHandle(tHandle) elif nwItemS.itemType == nwItemType.FOLDER: logger.debug("User requested folder %s deleted" % tHandle) @@ -569,7 +579,13 @@ class GuiProjectTree(QTreeWidget): open a context menu in-place. """ selItem = self.itemAt(clickPos) - self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) + if isinstance(selItem, QTreeWidgetItem): + tHandle = selItem.data(self.C_NAME, Qt.UserRole) + tItem = self.theProject.projTree[tHandle] + self.setSelectedHandle(tHandle) # Just to be safe + if self.ctxMenu.filterActions(tItem): + # Only open menu if any actions remain after filter + self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) return ## @@ -806,53 +822,108 @@ class GuiProjectTreeMenu(QMenu): QMenu.__init__(self, theTree) self.theTree = theTree - self.theItem = theItem + self.theItem = None self.editItem = QAction("Edit Item", self) self.editItem.triggered.connect(self._doEditItem) self.addAction(self.editItem) - self.toggleExp = QAction("Toggle Exported", self) + self.toggleExp = QAction("Toggle Included Flag", self) self.toggleExp.triggered.connect(self._doToggleExported) self.addAction(self.toggleExp) - self.addSeparator() - - self.newFolder = QAction("New Folder", self) - self.newFolder.triggered.connect(self._doMakeFolder) - self.addAction(self.newFolder) - self.newFile = QAction("New File", self) self.newFile.triggered.connect(self._doMakeFile) self.addAction(self.newFile) + self.newFolder = QAction("New Folder", self) + self.newFolder.triggered.connect(self._doMakeFolder) + self.addAction(self.newFolder) + self.deleteItem = QAction("Delete Item", self) self.deleteItem.triggered.connect(self._doDeleteItem) self.addAction(self.deleteItem) + self.emptyTrash = QAction("Empty Trash", self) + self.emptyTrash.triggered.connect(self._doEmptyTrash) + self.addAction(self.emptyTrash) + return - def updateFromItem(self, theItem): + def filterActions(self, theItem): + """Update item settings from the nwItem. + """ self.theItem = theItem - return + trashHandle = self.theTree.theProject.projTree.trashRoot() + + if theItem is None: + return False + + inTrash = theItem.parHandle == trashHandle + isTrash = theItem.itemHandle == trashHandle + isFile = theItem.itemType == nwItemType.FILE + isOrph = isFile and theItem.parHandle is None + + showEdit = not isTrash and not isOrph + showExport = isFile and not inTrash and not isOrph + showNewFile = not isTrash and not inTrash and not isOrph + showNewFolder = not isTrash and not inTrash and not isOrph + showDelete = not isTrash + showEmpty = isTrash + + self.editItem.setVisible(showEdit) + self.toggleExp.setVisible(showExport) + self.newFile.setVisible(showNewFile) + self.newFolder.setVisible(showNewFolder) + self.deleteItem.setVisible(showDelete) + self.emptyTrash.setVisible(showEmpty) + + return True ## # Slots ## def _doEditItem(self): - return - - def _doDeleteItem(self): - return - - def _doMakeFolder(self): + """Forward the edit item call to the main GUI window. + """ + if self.theItem is not None: + self.theTree.theParent.editItem() return def _doMakeFile(self): + """Forward the new file call to the project tree. + """ + if self.theItem is not None: + self.theTree.newTreeItem(nwItemType.FILE, None) + return + + def _doMakeFolder(self): + """Forward the new folder call to the project tree. + """ + if self.theItem is not None: + self.theTree.newTreeItem(nwItemType.FOLDER, None) return def _doToggleExported(self): + """Flip the isExported flag of the current item. + """ + if self.theItem is not None: + self.theItem.setExported(not self.theItem.isExported) + self.theTree.setTreeItemValues(self.theItem.itemHandle) + return + + def _doDeleteItem(self): + """Forward the delete item call to the project tree. + """ + if self.theItem is not None: + self.theTree.deleteItem(askForTrash=True) + return + + def _doEmptyTrash(self): + """Forward the delete item call to the project tree. + """ + self.theTree.emptyTrash() return # END Class GuiProjectTreeMenu From f75028e4a76df41364e0c12e075e337dd0ce7037 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jun 2020 18:04:40 +0200 Subject: [PATCH 3/4] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d90087a..bfc35434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **User Interface** * A details panel below the Outline tree view has been added. The panel shows all the information of a selected row in the tree view above, including hidden columns, and some additional information. The tags and references also become clickable links that when clicked will open in the document viewer. PR #281. +* Added a context menu to the project tree for easier access to some of the most use actions on the tree. PR #282. ## Version 0.7 [2020-06-01] From 4c9e3f3440a347594085e5b4f9d4672347b8985b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jun 2020 19:15:22 +0200 Subject: [PATCH 4/4] New files are now inserted after the file selected, if a file is selected --- nw/gui/projtree.py | 23 +++++++++++++++++------ sample/nwProject.nwx | 2 +- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 9b95d8bb..97b87a2e 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -145,6 +145,7 @@ class GuiProjectTree(QTreeWidget): meta data is set correctly to ensure a valid project tree. """ pHandle = self.getSelectedHandle() + nHandle = None if not self.theParent.hasProject: return False @@ -193,6 +194,7 @@ class GuiProjectTree(QTreeWidget): # the new file will be a sibling pItem = self.theProject.projTree[pHandle] if pItem.itemType == nwItemType.FILE: + nHandle = pHandle pHandle = pItem.parHandle # If we again has no home, give up @@ -218,18 +220,18 @@ class GuiProjectTree(QTreeWidget): return False # Add the new item to the tree - self.revealTreeItem(tHandle) + self.revealTreeItem(tHandle, nHandle) self.theParent.editItem() return True - def revealTreeItem(self, tHandle): + def revealTreeItem(self, tHandle, nHandle=None): """Reveal a newly added project item in the project tree. """ nwItem = self.theProject.projTree[tHandle] - trItem = self._addTreeItem(nwItem) + trItem = self._addTreeItem(nwItem, nHandle) pHandle = nwItem.parHandle - if pHandle is not None and pHandle in self.theMap.keys(): + if pHandle is not None and pHandle in self.theMap: self.theMap[pHandle].setExpanded(True) self.clearSelection() trItem.setSelected(True) @@ -677,7 +679,7 @@ class GuiProjectTree(QTreeWidget): self._scanChildren(theList, theItem.child(i), i) return theList - def _addTreeItem(self, nwItem): + def _addTreeItem(self, nwItem, nHandle=None): """Create a QTreeWidgetItem from an NWItem and add it to the project tree. """ @@ -713,7 +715,16 @@ class GuiProjectTree(QTreeWidget): self._addOrphanedRoot() self.orphRoot.addChild(newItem) else: - self.theMap[pHandle].addChild(newItem) + byIndex = -1 + if nHandle is not None and nHandle in self.theMap: + try: + byIndex = self.theMap[pHandle].indexOfChild(self.theMap[nHandle]) + except: + logger.error("Failed to get index of item with handle %s" % nHandle) + if byIndex >= 0: + self.theMap[pHandle].insertChild(byIndex+1, newItem) + else: + self.theMap[pHandle].addChild(newItem) self.propagateCount(tHandle, nwItem.wordCount) self.setTreeItemValues(tHandle) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 092d28d1..b7b0bde0 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project