Add move to trash functionality
This commit is contained in:
@@ -178,7 +178,7 @@ class ProjectNode:
|
|||||||
def takeChild(self, pos: int) -> ProjectNode | None:
|
def takeChild(self, pos: int) -> ProjectNode | None:
|
||||||
"""Remove a child item and return it."""
|
"""Remove a child item and return it."""
|
||||||
if 0 <= pos < len(self._children):
|
if 0 <= pos < len(self._children):
|
||||||
node = self._children.pop()
|
node = self._children.pop(pos)
|
||||||
self._refreshChildrenPos()
|
self._refreshChildrenPos()
|
||||||
return node
|
return node
|
||||||
return None
|
return None
|
||||||
@@ -256,32 +256,23 @@ class ProjectModel(QAbstractItemModel):
|
|||||||
|
|
||||||
def parent(self, index: QModelIndex) -> QModelIndex:
|
def parent(self, index: QModelIndex) -> QModelIndex:
|
||||||
"""Get the parent model index of another index."""
|
"""Get the parent model index of another index."""
|
||||||
if index.isValid():
|
if index.isValid() and (parent := index.internalPointer().parent()):
|
||||||
if parent := index.internalPointer().parent():
|
return self.createIndex(parent.row(), 0, parent)
|
||||||
return self.createIndex(parent.row(), 0, parent)
|
|
||||||
return QModelIndex()
|
return QModelIndex()
|
||||||
|
|
||||||
def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:
|
def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:
|
||||||
"""get the index of a child item of a parent."""
|
"""get the index of a child item of a parent."""
|
||||||
if parent.isValid():
|
if self.hasIndex(row, column, parent):
|
||||||
item: ProjectNode = parent.internalPointer()
|
node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
|
||||||
else:
|
if child := node.child(row):
|
||||||
item = self._root
|
return self.createIndex(row, column, child)
|
||||||
|
|
||||||
if not self.hasIndex(row, column, parent):
|
|
||||||
return QModelIndex()
|
|
||||||
|
|
||||||
if child := item.child(row):
|
|
||||||
return self.createIndex(row, column, child)
|
|
||||||
|
|
||||||
return QModelIndex()
|
return QModelIndex()
|
||||||
|
|
||||||
def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> T_NodeData:
|
def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> T_NodeData:
|
||||||
"""Return display data for a project node."""
|
"""Return display data for a project node."""
|
||||||
if not index.isValid():
|
if index.isValid():
|
||||||
return None
|
return index.internalPointer().data(index.column(), role)
|
||||||
node: ProjectNode = index.internalPointer()
|
return None
|
||||||
return node.data(index.column(), role)
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Data Access
|
# Data Access
|
||||||
@@ -290,8 +281,7 @@ class ProjectModel(QAbstractItemModel):
|
|||||||
def row(self, index: QModelIndex) -> int:
|
def row(self, index: QModelIndex) -> int:
|
||||||
"""Return the row number of the index."""
|
"""Return the row number of the index."""
|
||||||
if index.isValid():
|
if index.isValid():
|
||||||
node: ProjectNode = index.internalPointer()
|
return index.internalPointer().row()
|
||||||
return node.row()
|
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
def node(self, index: QModelIndex) -> ProjectNode | None:
|
def node(self, index: QModelIndex) -> ProjectNode | None:
|
||||||
@@ -320,10 +310,7 @@ class ProjectModel(QAbstractItemModel):
|
|||||||
|
|
||||||
def insertChild(self, child: ProjectNode, parent: QModelIndex, pos: int) -> None:
|
def insertChild(self, child: ProjectNode, parent: QModelIndex, pos: int) -> None:
|
||||||
"""Insert a node into the model at a given position."""
|
"""Insert a node into the model at a given position."""
|
||||||
if parent.isValid():
|
node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
|
||||||
node: ProjectNode = parent.internalPointer()
|
|
||||||
else:
|
|
||||||
node = self._root
|
|
||||||
count = node.childCount()
|
count = node.childCount()
|
||||||
row = minmax(pos, 0, count) if pos >= 0 else count
|
row = minmax(pos, 0, count) if pos >= 0 else count
|
||||||
self.beginInsertRows(parent, row, row)
|
self.beginInsertRows(parent, row, row)
|
||||||
@@ -331,6 +318,16 @@ class ProjectModel(QAbstractItemModel):
|
|||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def removeChild(self, parent: QModelIndex, pos: int) -> ProjectNode | None:
|
||||||
|
"""Remove a node from the model and return it."""
|
||||||
|
node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
|
||||||
|
if 0 <= pos < node.childCount():
|
||||||
|
self.beginRemoveRows(parent, pos, pos)
|
||||||
|
child = node.takeChild(pos)
|
||||||
|
self.endRemoveRows()
|
||||||
|
return child
|
||||||
|
return None
|
||||||
|
|
||||||
def internalMove(self, index: QModelIndex, step: int) -> None:
|
def internalMove(self, index: QModelIndex, step: int) -> None:
|
||||||
"""Move an item internally among its siblings."""
|
"""Move an item internally among its siblings."""
|
||||||
if index.isValid():
|
if index.isValid():
|
||||||
@@ -345,6 +342,30 @@ class ProjectModel(QAbstractItemModel):
|
|||||||
self.endMoveRows()
|
self.endMoveRows()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def multiMove(self, indices: list[QModelIndex], target: QModelIndex) -> None:
|
||||||
|
"""Move multiple items to a new location."""
|
||||||
|
if target.isValid():
|
||||||
|
# This is a two pass process. First we only select unique
|
||||||
|
# non-root items for move, then we do a second pass and only
|
||||||
|
# move those items that don't have a parent also scheduled
|
||||||
|
# for moving or have already been moved. Child items are
|
||||||
|
# moved with the parent.
|
||||||
|
pruned = []
|
||||||
|
handles = set()
|
||||||
|
for index in indices:
|
||||||
|
if index.isValid():
|
||||||
|
node: ProjectNode = index.internalPointer()
|
||||||
|
handle = node.item.itemHandle
|
||||||
|
if node.item.isRootType() is False and handle not in handles:
|
||||||
|
pruned.append(node)
|
||||||
|
handles.add(handle)
|
||||||
|
for node in pruned:
|
||||||
|
if node.item.itemParent not in handles:
|
||||||
|
index = self.indexFromNode(node)
|
||||||
|
if child := self.removeChild(index.parent(), index.row()):
|
||||||
|
self.insertChild(child, target, -1)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Other Methods
|
# Other Methods
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -162,19 +162,20 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.projMenu.addSeparator()
|
self.projMenu.addSeparator()
|
||||||
|
|
||||||
# Project > Edit
|
# Project > Edit
|
||||||
self.aEditItem = self.projMenu.addAction(self.tr("Rename Item"))
|
self.aRenameItem = self.projMenu.addAction(self.tr("Rename Item"))
|
||||||
self.aEditItem.setShortcut("F2")
|
self.aRenameItem.setShortcut("F2")
|
||||||
self.aEditItem.triggered.connect(qtLambda(self.mainGui.projView.renameTreeItem, None))
|
|
||||||
self.mainGui.addAction(self.aEditItem)
|
|
||||||
|
|
||||||
# Project > Delete
|
# Project > Delete
|
||||||
self.aDeleteItem = self.projMenu.addAction(self.tr("Delete Item"))
|
self.aDeleteItem = self.projMenu.addAction(self.tr("Delete Item"))
|
||||||
self.aDeleteItem.setShortcut("Ctrl+Shift+Del") # Cannot be Ctrl+Del, see #629
|
self.aDeleteItem.setShortcut("Del")
|
||||||
self.aDeleteItem.triggered.connect(qtLambda(self.mainGui.projView.requestDeleteItem, None))
|
self.aDeleteItem.setShortcutContext(Qt.ShortcutContext.WidgetShortcut)
|
||||||
|
|
||||||
# Project > Empty Trash
|
# Project > Empty Trash
|
||||||
self.aEmptyTrash = self.projMenu.addAction(self.tr("Empty Trash"))
|
self.aEmptyTrash = self.projMenu.addAction(self.tr("Empty Trash"))
|
||||||
self.aEmptyTrash.triggered.connect(qtLambda(self.mainGui.projView.emptyTrash))
|
|
||||||
|
self.mainGui.projView.connectMenuActions(
|
||||||
|
self.aRenameItem, self.aDeleteItem, self.aEmptyTrash
|
||||||
|
)
|
||||||
|
|
||||||
# Project > Separator
|
# Project > Separator
|
||||||
self.projMenu.addSeparator()
|
self.projMenu.addSeparator()
|
||||||
|
|||||||
+28
-50
@@ -131,8 +131,6 @@ class GuiProjectView(QWidget):
|
|||||||
self.projBar.newDocumentFromTemplate.connect(self.createFileFromTemplate)
|
self.projBar.newDocumentFromTemplate.connect(self.createFileFromTemplate)
|
||||||
|
|
||||||
# Function Mappings
|
# Function Mappings
|
||||||
self.emptyTrash = self.projTree.emptyTrash
|
|
||||||
self.requestDeleteItem = self.projTree.requestDeleteItem
|
|
||||||
self.getSelectedHandle = self.projTree.getSelectedHandle
|
self.getSelectedHandle = self.projTree.getSelectedHandle
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -185,10 +183,20 @@ class GuiProjectView(QWidget):
|
|||||||
"""Check if the project tree has focus."""
|
"""Check if the project tree has focus."""
|
||||||
return self.projTree.hasFocus()
|
return self.projTree.hasFocus()
|
||||||
|
|
||||||
|
def connectMenuActions(self, rename: QAction, delete: QAction, trash: QAction) -> None:
|
||||||
|
"""Main menu actions passed to the project tree."""
|
||||||
|
self.projTree.addAction(rename)
|
||||||
|
self.projTree.addAction(delete)
|
||||||
|
self.projTree.addAction(trash)
|
||||||
|
rename.triggered.connect(self.renameTreeItem)
|
||||||
|
delete.triggered.connect(self.projTree.moveItemsToTrash)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Public Slots
|
# Public Slots
|
||||||
##
|
##
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def renameTreeItem(self, tHandle: str | None = None, name: str = "") -> None:
|
def renameTreeItem(self, tHandle: str | None = None, name: str = "") -> None:
|
||||||
"""External request to rename an item or the currently selected
|
"""External request to rename an item or the currently selected
|
||||||
@@ -806,6 +814,20 @@ class GuiProjectTree(QTreeView):
|
|||||||
self.setCurrentIndex(model.indexFromNode(child))
|
self.setCurrentIndex(model.indexFromNode(child))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def moveItemsToTrash(self, askFirst: bool = True) -> None:
|
||||||
|
"""Move selected items to Trash."""
|
||||||
|
if (
|
||||||
|
(items := self._selectedRows())
|
||||||
|
and (model := self._getModel())
|
||||||
|
and (trashNode := SHARED.project.tree.trash)
|
||||||
|
):
|
||||||
|
if askFirst and not SHARED.question(self.tr("Move selected items to Trash?")):
|
||||||
|
logger.info("Action cancelled by user")
|
||||||
|
return
|
||||||
|
model.multiMove(items, model.indexFromNode(trashNode))
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Private Slots
|
# Private Slots
|
||||||
##
|
##
|
||||||
@@ -835,6 +857,10 @@ class GuiProjectTree(QTreeView):
|
|||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def _selectedRows(self) -> list[QModelIndex]:
|
||||||
|
"""Return all column 0 indexes."""
|
||||||
|
return [i for i in self.selectedIndexes() if i.column() == 0]
|
||||||
|
|
||||||
def _getModel(self) -> ProjectModel | None:
|
def _getModel(self) -> ProjectModel | None:
|
||||||
"""Return a project node corresponding to a model index."""
|
"""Return a project node corresponding to a model index."""
|
||||||
if isinstance(model := self.model(), ProjectModel):
|
if isinstance(model := self.model(), ProjectModel):
|
||||||
@@ -943,54 +969,6 @@ class GuiProjectTree(QTreeView):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def moveItemToTrash(self, tHandle: str, askFirst: bool = True, flush: bool = True) -> bool:
|
|
||||||
"""Move an item to Trash. Root folders cannot be moved to Trash,
|
|
||||||
so such a request is cancelled.
|
|
||||||
"""
|
|
||||||
# trItemS = self._getTreeItem(tHandle)
|
|
||||||
# nwItemS = SHARED.project.tree[tHandle]
|
|
||||||
|
|
||||||
# if trItemS is None or nwItemS is None:
|
|
||||||
# logger.error("Could not find tree item for deletion")
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# if SHARED.project.tree.isTrash(tHandle):
|
|
||||||
# logger.error("Item is already in the Trash folder")
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# if nwItemS.isRootType():
|
|
||||||
# logger.error("Root folders cannot be moved to Trash")
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# logger.debug("User requested file or folder '%s' move to Trash", tHandle)
|
|
||||||
|
|
||||||
# trItemP = trItemS.parent()
|
|
||||||
# trItemT = self._addTrashRoot()
|
|
||||||
# if trItemP is None or trItemT is None:
|
|
||||||
# logger.error("Could not delete item")
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# if askFirst:
|
|
||||||
# msgYes = SHARED.question(
|
|
||||||
# self.tr("Move '{0}' to Trash?").format(nwItemS.itemName)
|
|
||||||
# )
|
|
||||||
# if not msgYes:
|
|
||||||
# logger.info("Action cancelled by user")
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# self.propagateCount(tHandle, 0)
|
|
||||||
|
|
||||||
# tIndex = trItemP.indexOfChild(trItemS)
|
|
||||||
# trItemC = trItemP.takeChild(tIndex)
|
|
||||||
# trItemT.addChild(trItemC)
|
|
||||||
|
|
||||||
# self._postItemMove(tHandle)
|
|
||||||
# self._alertTreeChange(tHandle, flush=flush)
|
|
||||||
|
|
||||||
# logger.debug("Moved item '%s' to Trash", tHandle)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def permDeleteItem(self, tHandle: str, askFirst: bool = True, flush: bool = True) -> bool:
|
def permDeleteItem(self, tHandle: str, askFirst: bool = True, flush: bool = True) -> bool:
|
||||||
"""Permanently delete a tree item from the project and the map.
|
"""Permanently delete a tree item from the project and the map.
|
||||||
Root items are handled a little different than other items.
|
Root items are handled a little different than other items.
|
||||||
|
|||||||
Reference in New Issue
Block a user