Add a working implementation of multi-item move

This commit is contained in:
Veronica Berglyd Olsen
2023-11-21 22:26:10 +01:00
parent 1a2bd7d640
commit f75ed33cec
+65 -38
View File
@@ -31,7 +31,7 @@ from enum import Enum
from time import time from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtGui import QDragMoveEvent, QDropEvent, QMouseEvent, QPalette from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent, QPalette
from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel, QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
@@ -142,7 +142,6 @@ class GuiProjectView(QWidget):
# Function Mappings # Function Mappings
self.emptyTrash = self.projTree.emptyTrash self.emptyTrash = self.projTree.emptyTrash
self.requestDeleteItem = self.projTree.requestDeleteItem self.requestDeleteItem = self.projTree.requestDeleteItem
self.propagateCount = self.projTree.propagateCount
self.getSelectedHandle = self.projTree.getSelectedHandle self.getSelectedHandle = self.projTree.getSelectedHandle
self.setSelectedHandle = self.projTree.setSelectedHandle self.setSelectedHandle = self.projTree.setSelectedHandle
self.changedSince = self.projTree.changedSince self.changedSince = self.projTree.changedSince
@@ -478,6 +477,7 @@ class GuiProjectTree(QTreeWidget):
self._treeMap = {} self._treeMap = {}
self._lastMove = {} self._lastMove = {}
self._timeChanged = 0.0 self._timeChanged = 0.0
self._popAlert = None
# Build GUI # Build GUI
# ========= # =========
@@ -514,7 +514,6 @@ class GuiProjectTree(QTreeWidget):
# Allow Move by Drag & Drop # Allow Move by Drag & Drop
self.setDragEnabled(True) self.setDragEnabled(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self.setDropIndicatorShown(True)
# Disable built-in auto scroll as it isn't working in some Qt # Disable built-in auto scroll as it isn't working in some Qt
# releases (see #1561) and instead use our own implementation # releases (see #1561) and instead use our own implementation
@@ -523,8 +522,8 @@ class GuiProjectTree(QTreeWidget):
# But don't allow drop on root level # But don't allow drop on root level
# Due to a bug, this stops working somewhere between Qt 5.15.3 # Due to a bug, this stops working somewhere between Qt 5.15.3
# and 5.15.8, so this is also blocked in dropEvent (see #1569) # and 5.15.8, so this is also blocked in dropEvent (see #1569)
trRoot = self.invisibleRootItem() # trRoot = self.invisibleRootItem()
trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled) # trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
# Cached values # Cached values
self._lblActive = self.tr("Active") self._lblActive = self.tr("Active")
@@ -926,14 +925,13 @@ class GuiProjectTree(QTreeWidget):
logger.info("Action cancelled by user") logger.info("Action cancelled by user")
return False return False
wCount = self._getItemWordCount(tHandle)
self.propagateCount(tHandle, 0) self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS) tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex) trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC) trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount) self._postItemMove(tHandle)
self._recordLastMove(trItemS, trItemP, tIndex) self._recordLastMove(trItemS, trItemP, tIndex)
self._alertTreeChange(tHandle, flush=flush) self._alertTreeChange(tHandle, flush=flush)
@@ -1120,14 +1118,13 @@ class GuiProjectTree(QTreeWidget):
dHandle = dstItem.data(self.C_DATA, self.D_HANDLE) dHandle = dstItem.data(self.C_DATA, self.D_HANDLE)
logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex) logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex)
wCount = self._getItemWordCount(sHandle)
self.propagateCount(sHandle, 0) self.propagateCount(sHandle, 0)
parItem = srcItem.parent() parItem = srcItem.parent()
srcIndex = parItem.indexOfChild(srcItem) srcIndex = parItem.indexOfChild(srcItem)
movItem = parItem.takeChild(srcIndex) movItem = parItem.takeChild(srcIndex)
dstItem.insertChild(dstIndex, movItem) dstItem.insertChild(dstIndex, movItem)
self._postItemMove(sHandle, wCount) self._postItemMove(sHandle)
self._alertTreeChange(sHandle, flush=True) self._alertTreeChange(sHandle, flush=True)
self.setCurrentItem(movItem) self.setCurrentItem(movItem)
@@ -1190,6 +1187,15 @@ class GuiProjectTree(QTreeWidget):
tHandle = self.getSelectedHandle() tHandle = self.getSelectedHandle()
if tHandle is not None: if tHandle is not None:
self.projView.selectedItemChanged.emit(tHandle) self.projView.selectedItemChanged.emit(tHandle)
# When selecting multiple items, don't allow including root
# items in the selection and instead deselect them
items = self.selectedItems()
if items and len(items) > 1:
for item in items:
if item.parent() is None:
item.setSelected(False)
return return
@pyqtSlot("QTreeWidgetItem*", int) @pyqtSlot("QTreeWidgetItem*", int)
@@ -1419,6 +1425,31 @@ class GuiProjectTree(QTreeWidget):
return return
def startDrag(self, dropAction: Qt.DropActions) -> None:
"""Capture the drag and drop handling to pop alerts."""
super().startDrag(dropAction)
if self._popAlert:
SHARED.error(self._popAlert)
self._popAlert = None
return
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
"""Check that we're only dragging items that are siblings, and
not a root level item.
"""
items = self.selectedItems()
if items and (parent := items[0].parent()) and all(x.parent() is parent for x in items):
super().dragEnterEvent(event)
else:
logger.warning("Drag action is not allowed and has been cancelled")
self._popAlert = self.tr(
"Drag and drop is only allowed for single, non-root items, "
"or multiple items with the same parent."
)
event.mimeData().clear()
event.ignore()
return
def dragMoveEvent(self, event: QDragMoveEvent) -> None: def dragMoveEvent(self, event: QDragMoveEvent) -> None:
"""Capture the drag move event to enable edge auto scroll.""" """Capture the drag move event to enable edge auto scroll."""
y = event.pos().y() y = event.pos().y()
@@ -1437,35 +1468,35 @@ class GuiProjectTree(QTreeWidget):
"""Overload the drop item event to ensure the drag and drop """Overload the drop item event to ensure the drag and drop
action is allowed, and update relevant data. action is allowed, and update relevant data.
""" """
sHandle = self.getSelectedHandle()
sItem = self._getTreeItem(sHandle) if sHandle else None
if sHandle is None or sItem is None or sItem.parent() is None:
logger.error("Invalid drag and drop event")
event.ignore()
return
if not self.indexAt(event.pos()).isValid(): if not self.indexAt(event.pos()).isValid():
# Needed due to a bug somewhere around Qt 5.15.8 that # Make sure nothing can be dropped on invisible root
# ignores the invisible root item flags
logger.error("Invalid drop location") logger.error("Invalid drop location")
event.ignore() event.ignore()
return return
logger.debug("Drag'n'drop of item '%s' accepted", sHandle) mItems: dict[str, tuple[QTreeWidgetItem, QTreeWidgetItem, bool]] = {}
sItems = self.selectedItems()
if sItems and (parent := sItems[0].parent()) and all(x.parent() is parent for x in sItems):
for sItem in sItems:
if (pItem := sItem.parent()):
mHandle = str(sItem.data(self.C_DATA, self.D_HANDLE))
mItems[mHandle] = (sItem, pItem, sItem.isExpanded())
else:
logger.error("Cannot drag and drop a root item")
event.ignore()
return
isExpanded = sItem.isExpanded() for mHandle in mItems:
pItem = sItem.parent() self.propagateCount(mHandle, 0)
pIndex = pItem.indexOfChild(sItem) if pItem else 0
wCount = self._getItemWordCount(sHandle) super().dropEvent(event)
self.propagateCount(sHandle, 0)
super().dropEvent(event) for mHandle, (sItem, pItem, isExpanded) in mItems.items():
self._postItemMove(sHandle, wCount) self._postItemMove(mHandle)
self._recordLastMove(sItem, pItem, pIndex) sItem.setExpanded(isExpanded)
self._alertTreeChange(sHandle, flush=True) self._alertTreeChange(mHandle, flush=False)
sItem.setExpanded(isExpanded) self.saveTreeOrder()
return return
@@ -1473,17 +1504,16 @@ class GuiProjectTree(QTreeWidget):
# Internal Functions # Internal Functions
## ##
def _postItemMove(self, tHandle: str, wCount: int) -> bool: def _postItemMove(self, tHandle: str) -> None:
"""Run various maintenance tasks for a moved item.""" """Run various maintenance tasks for a moved item."""
trItemS = self._getTreeItem(tHandle) trItemS = self._getTreeItem(tHandle)
nwItemS = SHARED.project.tree[tHandle] nwItemS = SHARED.project.tree[tHandle]
trItemP = trItemS.parent() if trItemS else None trItemP = trItemS.parent() if trItemS else None
if trItemP is None or nwItemS is None: if trItemP is None or nwItemS is None:
logger.error("Failed to find new parent item of '%s'", tHandle) logger.error("Failed to find new parent item of '%s'", tHandle)
return False return
# Update item parent handle in the project, make sure meta data # Update item parent handle in the project
# is updated accordingly, and update word count
pHandle = trItemP.data(self.C_DATA, self.D_HANDLE) pHandle = trItemP.data(self.C_DATA, self.D_HANDLE)
nwItemS.setParent(pHandle) nwItemS.setParent(pHandle)
trItemP.setExpanded(True) trItemP.setExpanded(True)
@@ -1494,19 +1524,16 @@ class GuiProjectTree(QTreeWidget):
for mHandle in mHandles: for mHandle in mHandles:
logger.debug("Updating item '%s'", mHandle) logger.debug("Updating item '%s'", mHandle)
SHARED.project.tree.updateItemData(mHandle) SHARED.project.tree.updateItemData(mHandle)
# Update the index
if nwItemS.isInactiveClass(): if nwItemS.isInactiveClass():
SHARED.project.index.deleteHandle(mHandle) SHARED.project.index.deleteHandle(mHandle)
else: else:
SHARED.project.index.reIndexHandle(mHandle) SHARED.project.index.reIndexHandle(mHandle)
self.setTreeItemValues(mHandle) self.setTreeItemValues(mHandle)
# Trigger dependent updates # Trigger dependent updates
self.propagateCount(tHandle, wCount) self.propagateCount(tHandle, nwItemS.wordCount)
return True return
def _getItemWordCount(self, tHandle: str) -> int: def _getItemWordCount(self, tHandle: str) -> int:
"""Return the word count of a given item handle.""" """Return the word count of a given item handle."""