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 4d2dbff9..131bb398 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -505,10 +505,15 @@ class GuiProjectTree(QTreeWidget): self.mainGui = projView.mainGui # Internal Variables - self._treeMap = {} + self._treeMap: dict[str, QTreeWidgetItem] = {} 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" @@ -1129,9 +1127,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 +1140,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 +1156,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 +1740,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 +1793,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 ## @@ -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")) @@ -1962,10 +1960,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 @@ -1977,10 +1979,19 @@ 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.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) self.projTree.saveTreeOrder() return @@ -1998,12 +2009,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: @@ -2015,12 +2025,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: @@ -2032,12 +2041,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: diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 3422beea..4934f26c 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -1402,6 +1402,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