From c182cc4383c5ce4fbe8ce5dccda41f84bf3b8373 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Mar 2024 21:16:46 +0100 Subject: [PATCH 1/3] Fix move to trash and permanently delete for multi select menu --- novelwriter/gui/projtree.py | 80 +++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 24d42a32..3555f84c 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -505,7 +505,7 @@ class GuiProjectTree(QTreeWidget): self.mainGui = projView.mainGui # Internal Variables - self._treeMap = {} + self._treeMap: dict[str, QTreeWidgetItem] = {} self._timeChanged = 0.0 self._popAlert = None @@ -1129,9 +1129,8 @@ class GuiProjectTree(QTreeWidget): """Get the currently selected handle. If multiple items are selected, return the first. """ - selItem = self.selectedItems() - if selItem: - return selItem[0].data(self.C_DATA, self.D_HANDLE) + if items := self.selectedItems(): + return items[0].data(self.C_DATA, self.D_HANDLE) return None def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool: @@ -1143,9 +1142,8 @@ class GuiProjectTree(QTreeWidget): if tHandle in self._treeMap: self.setCurrentItem(self._treeMap[tHandle]) - selIndex = self.selectedIndexes() - if selIndex and doScroll: - self.scrollTo(selIndex[0], QAbstractItemView.ScrollHint.PositionAtCenter) + if (indexes := self.selectedIndexes()) and doScroll: + self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter) return True @@ -1160,10 +1158,8 @@ class GuiProjectTree(QTreeWidget): def openContextOnSelected(self) -> bool: """Open the context menu on the current selected item.""" - selItem = self.selectedItems() - if selItem: - pos = self.visualItemRect(selItem[0]).center() - return self._openContextMenu(pos) + if items := self.selectedItems(): + return self._openContextMenu(self.visualItemRect(items[0]).center()) return False def changedSince(self, checkTime: float) -> bool: @@ -1746,7 +1742,7 @@ class _TreeContextMenu(QMenu): self._item = nwItem self._handle = nwItem.itemHandle - self._items: list[str] = [] + self._items: list[NWItem] = [] logger.debug("Ready: _TreeContextMenu") @@ -1799,13 +1795,17 @@ class _TreeContextMenu(QMenu): return - def buildMultiSelectMenu(self, items: list[str]) -> None: + def buildMultiSelectMenu(self, handles: list[str]) -> None: """Build the multi-select menu.""" - self._items = items + self._items = [] + for tHandle in handles: + if (tItem := SHARED.project.tree[tHandle]): + self._items.append(tItem) + self._itemActive(True) self._itemStatusImport(True) self.addSeparator() - self._moveToTrash(True) + self._multiMoveToTrash() return ## @@ -1964,10 +1964,14 @@ class _TreeContextMenu(QMenu): return - def _moveToTrash(self, multi: bool) -> None: + def _multiMoveToTrash(self) -> None: """Add move to Trash action.""" - action = self.addAction(self.tr("Move to Trash")) - if multi: + areTrash = [i.itemClass == nwItemClass.TRASH for i in self._items] + if all(areTrash): + action = self.addAction(self.tr("Delete Permanently")) + action.triggered.connect(self._iterPermDelete) + elif not any(areTrash): + action = self.addAction(self.tr("Move to Trash")) action.triggered.connect(self._iterMoveToTrash) return @@ -1979,10 +1983,21 @@ class _TreeContextMenu(QMenu): def _iterMoveToTrash(self) -> None: """Iterate through files and move them to Trash.""" if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))): - for tHandle in self._items: - tItem = SHARED.project.tree[tHandle] - if tItem and tItem.isFileType(): - self.projTree.moveItemToTrash(tHandle, askFirst=False, flush=False) + for tItem in self._items: + if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH: + self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False) + self.projTree.saveTreeOrder() + return + + @pyqtSlot() + def _iterPermDelete(self) -> None: + """Iterate through files and delete them.""" + if SHARED.question( + self.tr("Permanently delete {0} documents in Trash?").format(len(self._items)) + ): + for tItem in self._items: + if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH: + self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False) self.projTree.saveTreeOrder() return @@ -2000,12 +2015,11 @@ class _TreeContextMenu(QMenu): def _iterItemActive(self, isActive: bool) -> None: """Set the active status of multiple items.""" - for tHandle in self._items: - tItem = SHARED.project.tree[tHandle] + for tItem in self._items: if tItem and tItem.isFileType(): tItem.setActive(isActive) - self.projTree.setTreeItemValues(tHandle) - self.projTree._alertTreeChange(tHandle, flush=False) + self.projTree.setTreeItemValues(tItem.itemHandle) + self.projTree._alertTreeChange(tItem.itemHandle, flush=False) return def _changeItemStatus(self, key: str) -> None: @@ -2017,12 +2031,11 @@ class _TreeContextMenu(QMenu): def _iterSetItemStatus(self, key: str) -> None: """Change the status value for multiple items.""" - for tHandle in self._items: - tItem = SHARED.project.tree[tHandle] + for tItem in self._items: if tItem and tItem.isNovelLike(): tItem.setStatus(key) - self.projTree.setTreeItemValues(tHandle) - self.projTree._alertTreeChange(tHandle, flush=False) + self.projTree.setTreeItemValues(tItem.itemHandle) + self.projTree._alertTreeChange(tItem.itemHandle, flush=False) return def _changeItemImport(self, key: str) -> None: @@ -2034,12 +2047,11 @@ class _TreeContextMenu(QMenu): def _iterSetItemImport(self, key: str) -> None: """Change the status value for multiple items.""" - for tHandle in self._items: - tItem = SHARED.project.tree[tHandle] + for tItem in self._items: if tItem and not tItem.isNovelLike(): tItem.setImport(key) - self.projTree.setTreeItemValues(tHandle) - self.projTree._alertTreeChange(tHandle, flush=False) + self.projTree.setTreeItemValues(tItem.itemHandle) + self.projTree._alertTreeChange(tItem.itemHandle, flush=False) return def _changeItemLayout(self, itemLayout: nwItemLayout) -> None: From d12e626bc4a1bdfdfb7c3ad27083c5816d275038 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Mar 2024 21:17:18 +0100 Subject: [PATCH 2/3] Update tests --- tests/test_gui/test_gui_projtree.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 18e41ea3..08a77861 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -1404,6 +1404,22 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): assert SHARED.project.tree[hNovelNote].itemRoot == hTrashRoot # type: ignore assert SHARED.project.tree[hSubNote].itemRoot == hTrashRoot # type: ignore + # Permanently Delete Menu + nwItem = SHARED.project.tree[hCharNote] + assert isinstance(nwItem, NWItem) + ctxMenu = _TreeContextMenu(projTree, nwItem) + ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote]) + actions = [x.text() for x in ctxMenu.actions() if x.text()] + assert actions == [ + "Set Active to ...", "Set Importance to ...", "Delete Permanently", + ] + + # Permanently Delete + ctxMenu._iterPermDelete() + assert SHARED.project.tree[hCharNote] is None + assert SHARED.project.tree[hNovelNote] is None + assert SHARED.project.tree[hSubNote] is None + # qtbot.stop() # END Test testGuiProjTree_ContextMenu From 925d9fd5ad69066bdca42048fa69d9e3954b16ae Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Mar 2024 21:25:49 +0100 Subject: [PATCH 3/3] Update translations --- i18n/nw_base.ts | 239 +++++++++++++++++------------------- novelwriter/gui/projtree.py | 24 ++-- 2 files changed, 125 insertions(+), 138 deletions(-) diff --git a/i18n/nw_base.ts b/i18n/nw_base.ts index da9cc372..f6837b16 100644 --- a/i18n/nw_base.ts +++ b/i18n/nw_base.ts @@ -3018,118 +3018,118 @@ GuiProjectTree - - Active - - - - - Inactive - - - - - Did not find anywhere to add the file or folder! - - - - - Cannot add new files or folders to the Trash folder. - - - - - New Note - - - - - New Chapter - - - - - New Scene - - - - - New Document - - - - - New Folder - - - - - There is currently no Trash folder in this project. - - - - - The Trash folder is already empty. - - - - + Permanently delete {0} file(s) from Trash? - + + Active + + + + + Inactive + + + + + Did not find anywhere to add the file or folder! + + + + + Cannot add new files or folders to the Trash folder. + + + + + New Note + + + + + New Chapter + + + + + New Scene + + + + + New Document + + + + + New Folder + + + + + There is currently no Trash folder in this project. + + + + + The Trash folder is already empty. + + + + Move '{0}' to Trash? - + Root folders can only be deleted when they are empty. - + Permanently delete '{0}'? - + Drag and drop is only allowed for single items, non-root items, or multiple items with the same parent. - + No documents selected for merging. - + Merged - - + + Could not write document content. - + Do you want to duplicate this document? - + Do you want to duplicate this item and all child items? - + Could not duplicate all items. - + There is nowhere to add item with name '{0}'. @@ -3474,73 +3474,73 @@ - + Backing up project ... - + Cannot backup project because no project name is set. Please set a Project Name in Project Settings. - + Could not create backup folder. - + Created a backup of your project of size {0}B. - + Path: {0} - + Could not write backup archive. - + Project backed up to '{0}' - - + + New - + Note - + Draft - + Finished - + Minor - + Major - + Main @@ -4520,142 +4520,133 @@ _TreeContextMenu - + Empty Trash - + Rename - + Open Document - + View Document - + Create New ... - + Rename to Heading - + Set Active to ... - - Active - - - - - Inactive - - - - + Toggle Active - + Set Status to ... - - + + Manage Labels ... - + Set Importance to ... - + Transform ... - - - - + + + + Convert to {0} - + Merge Child Items into Self - + Merge Child Items into New - + Merge Documents in Folder - + Split Document by Headers - + Expand All - + Collapse All - + Duplicate from Here - + Duplicate Document - + + Delete Permanently - - + + Move to Trash - + Move {0} items to Trash? - + Do you want to convert the folder to a {0}? This action cannot be reversed. @@ -4663,7 +4654,7 @@ _UpdatableMenu - + From Template diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 3555f84c..9e415091 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -509,6 +509,11 @@ class GuiProjectTree(QTreeWidget): self._timeChanged = 0.0 self._popAlert = None + # Cached Translations + self.trActive = self.tr("Active") + self.trInactive = self.tr("Inactive") + self.trPermDelete = self.tr("Permanently delete {0} file(s) from Trash?") + # Build GUI # ========= @@ -555,10 +560,6 @@ class GuiProjectTree(QTreeWidget): trRoot = self.invisibleRootItem() trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled) - # Cached values - self._lblActive = self.tr("Active") - self._lblInactive = self.tr("Inactive") - # Set selection options self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) @@ -902,10 +903,7 @@ class GuiProjectTree(QTreeWidget): SHARED.info(self.tr("The Trash folder is already empty.")) return False - msgYes = SHARED.question( - self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash) - ) - if not msgYes: + if not SHARED.question(self.trPermDelete.format(nTrash)): logger.info("Action cancelled by user") return False @@ -1052,7 +1050,7 @@ class GuiProjectTree(QTreeWidget): if nwItem.isFileType(): iconName = "checked" if nwItem.isActive else "unchecked" - toolTip = self._lblActive if nwItem.isActive else self._lblInactive + toolTip = self.trActive if nwItem.isActive else self.trInactive trItem.setToolTip(self.C_ACTIVE, toolTip) else: iconName = "noncheckable" @@ -1847,9 +1845,9 @@ class _TreeContextMenu(QMenu): """Add Active/Inactive actions.""" if multi: mSub = self.addMenu(self.tr("Set Active to ...")) - aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.tr("Active")) + aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive) aOne.triggered.connect(lambda: self._iterItemActive(True)) - aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.tr("Inactive")) + aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive) aTwo.triggered.connect(lambda: self._iterItemActive(False)) else: action = self.addAction(self.tr("Toggle Active")) @@ -1992,9 +1990,7 @@ class _TreeContextMenu(QMenu): @pyqtSlot() def _iterPermDelete(self) -> None: """Iterate through files and delete them.""" - if SHARED.question( - self.tr("Permanently delete {0} documents in Trash?").format(len(self._items)) - ): + if SHARED.question(self.projTree.trPermDelete.format(len(self._items))): for tItem in self._items: if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH: self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False)