Add model support for drag and drop

This commit is contained in:
Veronica Berglyd Olsen
2024-11-20 01:31:56 +01:00
parent 54685c8184
commit 72f1bc2d77
3 changed files with 78 additions and 14 deletions
+3
View File
@@ -53,6 +53,9 @@ class nwConst:
# Requests # Requests
USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))" USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))"
# Mime Types
MIME_HANDLE = "text/vnd.novelwriter.handle"
# Gui Settings # Gui Settings
STATUS_MSG_TIMEOUT = 15000 # milliseconds STATUS_MSG_TIMEOUT = 15000 # milliseconds
MAX_SEARCH_RESULT = 1000 MAX_SEARCH_RESULT = 1000
+68 -8
View File
@@ -28,11 +28,12 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt from PyQt5.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from novelwriter import SHARED from novelwriter import SHARED
from novelwriter.common import minmax from novelwriter.common import minmax
from novelwriter.constants import nwConst
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.enum import nwItemClass from novelwriter.enum import nwItemClass
from novelwriter.types import QtAlignRight from novelwriter.types import QtAlignRight
@@ -54,12 +55,16 @@ C_ACTIVE_TIP = 0x0200 | Qt.ItemDataRole.ToolTipRole
C_STATUS_ICON = 0x0300 | Qt.ItemDataRole.DecorationRole C_STATUS_ICON = 0x0300 | Qt.ItemDataRole.DecorationRole
C_STATUS_TIP = 0x0300 | Qt.ItemDataRole.ToolTipRole C_STATUS_TIP = 0x0300 | Qt.ItemDataRole.ToolTipRole
NODE_FLAGS = Qt.ItemFlag.ItemIsEnabled
NODE_FLAGS |= Qt.ItemFlag.ItemIsSelectable
NODE_FLAGS |= Qt.ItemFlag.ItemIsDropEnabled
T_NodeData = str | QIcon | Qt.AlignmentFlag | None T_NodeData = str | QIcon | Qt.AlignmentFlag | None
class ProjectNode: class ProjectNode:
__slots__ = ("_item", "_children", "_parent", "_row", "_cache", "_count") __slots__ = ("_item", "_children", "_parent", "_row", "_cache", "_flags", "_count")
def __init__(self, item: NWItem) -> None: def __init__(self, item: NWItem) -> None:
self._item = item self._item = item
@@ -67,6 +72,7 @@ class ProjectNode:
self._parent: ProjectNode | None = None self._parent: ProjectNode | None = None
self._row = 0 self._row = 0
self._cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {} self._cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {}
self._flags = NODE_FLAGS
self.refresh() self.refresh()
return return
@@ -145,6 +151,10 @@ class ProjectNode:
"""Return cached node data.""" """Return cached node data."""
return self._cache.get(COL_MASK*column | role) return self._cache.get(COL_MASK*column | role)
def flags(self) -> Qt.ItemFlag:
"""Return cached node flags."""
return self._flags
def parent(self) -> ProjectNode | None: def parent(self) -> ProjectNode | None:
return self._parent return self._parent
@@ -213,9 +223,10 @@ class ProjectNode:
def _updateRelationships(self, child: ProjectNode) -> None: def _updateRelationships(self, child: ProjectNode) -> None:
"""Update a child item's relationships.""" """Update a child item's relationships."""
if self._parent: if self._parent:
child.item.setParent(self.item.itemHandle) child.item.setParent(self._item.itemHandle)
child.item.setRoot(self.item.itemRoot) child.item.setRoot(self._item.itemRoot)
child.item.setClassDefaults(self.item.itemClass) child.item.setClassDefaults(self._item.itemClass)
child._flags |= Qt.ItemFlag.ItemIsDragEnabled
else: else:
child.item.setParent(None) child.item.setParent(None)
child.item.setRoot(child.item.itemHandle) child.item.setRoot(child.item.itemHandle)
@@ -230,6 +241,7 @@ class ProjectModel(QAbstractItemModel):
super().__init__() super().__init__()
self._tree = tree self._tree = tree
self._root = ProjectNode(NWItem(tree._project, "invisibleRoot")) self._root = ProjectNode(NWItem(tree._project, "invisibleRoot"))
self._root.item.setName("Invisible Root")
logger.debug("Ready: ProjectModel") logger.debug("Ready: ProjectModel")
return return
@@ -276,6 +288,55 @@ class ProjectModel(QAbstractItemModel):
return index.internalPointer().data(index.column(), role) return index.internalPointer().data(index.column(), role)
return None return None
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
"""Return flags for a project node."""
if index.isValid():
return index.internalPointer().flags()
return Qt.ItemFlag.NoItemFlags
##
# Drag and Drop
##
def supportedDropActions(self) -> Qt.DropAction:
"""Return supported drop actions"""
return Qt.DropAction.MoveAction
def mimeTypes(self) -> list[str]:
"""Return the supported mime types of the model."""
return [nwConst.MIME_HANDLE]
def mimeData(self, indices: list[QModelIndex]) -> QMimeData:
"""Encode mime data about a selection."""
handles = [
i.internalPointer().item.itemHandle.encode()
for i in indices if i.isValid() and i.column() == 0
]
mime = QMimeData()
mime.setData(nwConst.MIME_HANDLE, b"|".join(handles))
return mime
def canDropMimeData(
self, data: QMimeData, action: Qt.DropAction,
row: int, column: int, parent: QModelIndex
) -> bool:
"""Check if mime data can be dropped on the current location."""
return data.hasFormat(nwConst.MIME_HANDLE) and action == Qt.DropAction.MoveAction
def dropMimeData(
self, data: QMimeData, action: Qt.DropAction,
row: int, column: int, parent: QModelIndex
) -> bool:
"""Process mime data drop."""
if self.canDropMimeData(data, action, row, column, parent):
items = []
for handle in data.data(nwConst.MIME_HANDLE).data().decode().split("|"):
if (index := self.indexFromHandle(handle)).isValid():
items.append(index)
self.multiMove(items, parent, row)
return True
return False
## ##
# Data Access # Data Access
## ##
@@ -353,12 +414,11 @@ class ProjectModel(QAbstractItemModel):
for index in indices: for index in indices:
if index.isValid(): if index.isValid():
node: ProjectNode = index.internalPointer() node: ProjectNode = index.internalPointer()
print(node)
if node.item.itemClass != nwItemClass.TRASH: if node.item.itemClass != nwItemClass.TRASH:
return False return False
return True return True
def multiMove(self, indices: list[QModelIndex], target: QModelIndex) -> None: def multiMove(self, indices: list[QModelIndex], target: QModelIndex, pos: int = -1) -> None:
"""Move multiple items to a new location.""" """Move multiple items to a new location."""
if target.isValid(): if target.isValid():
# This is a two pass process. First we only select unique # This is a two pass process. First we only select unique
@@ -379,7 +439,7 @@ class ProjectModel(QAbstractItemModel):
if node.item.itemParent not in handles: if node.item.itemParent not in handles:
index = self.indexFromNode(node) index = self.indexFromNode(node)
if child := self.removeChild(index.parent(), index.row()): if child := self.removeChild(index.parent(), index.row()):
self.insertChild(child, target, -1) self.insertChild(child, target, pos)
return return
## ##
+7 -6
View File
@@ -3,10 +3,11 @@ novelWriter GUI Project Tree
============================== ==============================
File History: File History:
Created: 2018-09-29 [0.0.1] GuiProjectTree Created: 2018-09-29 [0.0.1] GuiProjectTree
Created: 2022-06-06 [2.0rc1] GuiProjectView Created: 2022-06-06 [2.0rc1] GuiProjectView
Created: 2022-06-06 [2.0rc1] GuiProjectToolBar Created: 2022-06-06 [2.0rc1] GuiProjectToolBar
Created: 2023-11-22 [2.2rc1] _TreeContextMenu Created: 2023-11-22 [2.2rc1] _TreeContextMenu
Rewritten: 2024-11-17 [2.7b1] GuiProjectTree
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen Copyright 20182024, Veronica Berglyd Olsen
@@ -543,8 +544,8 @@ class GuiProjectTree(QTreeView):
self.setIndentation(iPx) self.setIndentation(iPx)
# 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)
# 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