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 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.QtWidgets import (
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
@@ -142,7 +142,6 @@ class GuiProjectView(QWidget):
# Function Mappings
self.emptyTrash = self.projTree.emptyTrash
self.requestDeleteItem = self.projTree.requestDeleteItem
self.propagateCount = self.projTree.propagateCount
self.getSelectedHandle = self.projTree.getSelectedHandle
self.setSelectedHandle = self.projTree.setSelectedHandle
self.changedSince = self.projTree.changedSince
@@ -478,6 +477,7 @@ class GuiProjectTree(QTreeWidget):
self._treeMap = {}
self._lastMove = {}
self._timeChanged = 0.0
self._popAlert = None
# Build GUI
# =========
@@ -514,7 +514,6 @@ class GuiProjectTree(QTreeWidget):
# Allow Move by Drag & Drop
self.setDragEnabled(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self.setDropIndicatorShown(True)
# Disable built-in auto scroll as it isn't working in some Qt
# releases (see #1561) and instead use our own implementation
@@ -523,8 +522,8 @@ class GuiProjectTree(QTreeWidget):
# But don't allow drop on root level
# 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)
trRoot = self.invisibleRootItem()
trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
# trRoot = self.invisibleRootItem()
# trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
# Cached values
self._lblActive = self.tr("Active")
@@ -926,14 +925,13 @@ class GuiProjectTree(QTreeWidget):
logger.info("Action cancelled by user")
return False
wCount = self._getItemWordCount(tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount)
self._postItemMove(tHandle)
self._recordLastMove(trItemS, trItemP, tIndex)
self._alertTreeChange(tHandle, flush=flush)
@@ -1120,14 +1118,13 @@ class GuiProjectTree(QTreeWidget):
dHandle = dstItem.data(self.C_DATA, self.D_HANDLE)
logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex)
wCount = self._getItemWordCount(sHandle)
self.propagateCount(sHandle, 0)
parItem = srcItem.parent()
srcIndex = parItem.indexOfChild(srcItem)
movItem = parItem.takeChild(srcIndex)
dstItem.insertChild(dstIndex, movItem)
self._postItemMove(sHandle, wCount)
self._postItemMove(sHandle)
self._alertTreeChange(sHandle, flush=True)
self.setCurrentItem(movItem)
@@ -1190,6 +1187,15 @@ class GuiProjectTree(QTreeWidget):
tHandle = self.getSelectedHandle()
if tHandle is not None:
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
@pyqtSlot("QTreeWidgetItem*", int)
@@ -1419,6 +1425,31 @@ class GuiProjectTree(QTreeWidget):
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:
"""Capture the drag move event to enable edge auto scroll."""
y = event.pos().y()
@@ -1437,35 +1468,35 @@ class GuiProjectTree(QTreeWidget):
"""Overload the drop item event to ensure the drag and drop
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():
# Needed due to a bug somewhere around Qt 5.15.8 that
# ignores the invisible root item flags
# Make sure nothing can be dropped on invisible root
logger.error("Invalid drop location")
event.ignore()
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()
pItem = sItem.parent()
pIndex = pItem.indexOfChild(sItem) if pItem else 0
for mHandle in mItems:
self.propagateCount(mHandle, 0)
wCount = self._getItemWordCount(sHandle)
self.propagateCount(sHandle, 0)
super().dropEvent(event)
super().dropEvent(event)
self._postItemMove(sHandle, wCount)
self._recordLastMove(sItem, pItem, pIndex)
self._alertTreeChange(sHandle, flush=True)
for mHandle, (sItem, pItem, isExpanded) in mItems.items():
self._postItemMove(mHandle)
sItem.setExpanded(isExpanded)
self._alertTreeChange(mHandle, flush=False)
sItem.setExpanded(isExpanded)
self.saveTreeOrder()
return
@@ -1473,17 +1504,16 @@ class GuiProjectTree(QTreeWidget):
# Internal Functions
##
def _postItemMove(self, tHandle: str, wCount: int) -> bool:
def _postItemMove(self, tHandle: str) -> None:
"""Run various maintenance tasks for a moved item."""
trItemS = self._getTreeItem(tHandle)
nwItemS = SHARED.project.tree[tHandle]
trItemP = trItemS.parent() if trItemS else None
if trItemP is None or nwItemS is None:
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
# is updated accordingly, and update word count
# Update item parent handle in the project
pHandle = trItemP.data(self.C_DATA, self.D_HANDLE)
nwItemS.setParent(pHandle)
trItemP.setExpanded(True)
@@ -1494,19 +1524,16 @@ class GuiProjectTree(QTreeWidget):
for mHandle in mHandles:
logger.debug("Updating item '%s'", mHandle)
SHARED.project.tree.updateItemData(mHandle)
# Update the index
if nwItemS.isInactiveClass():
SHARED.project.index.deleteHandle(mHandle)
else:
SHARED.project.index.reIndexHandle(mHandle)
self.setTreeItemValues(mHandle)
# Trigger dependent updates
self.propagateCount(tHandle, wCount)
self.propagateCount(tHandle, nwItemS.wordCount)
return True
return
def _getItemWordCount(self, tHandle: str) -> int:
"""Return the word count of a given item handle."""