Add model support for drag and drop
This commit is contained in:
@@ -53,6 +53,9 @@ class nwConst:
|
||||
# Requests
|
||||
USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))"
|
||||
|
||||
# Mime Types
|
||||
MIME_HANDLE = "text/vnd.novelwriter.handle"
|
||||
|
||||
# Gui Settings
|
||||
STATUS_MSG_TIMEOUT = 15000 # milliseconds
|
||||
MAX_SEARCH_RESULT = 1000
|
||||
|
||||
@@ -28,11 +28,12 @@ import logging
|
||||
|
||||
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 novelwriter import SHARED
|
||||
from novelwriter.common import minmax
|
||||
from novelwriter.constants import nwConst
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.enum import nwItemClass
|
||||
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_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
|
||||
|
||||
|
||||
class ProjectNode:
|
||||
|
||||
__slots__ = ("_item", "_children", "_parent", "_row", "_cache", "_count")
|
||||
__slots__ = ("_item", "_children", "_parent", "_row", "_cache", "_flags", "_count")
|
||||
|
||||
def __init__(self, item: NWItem) -> None:
|
||||
self._item = item
|
||||
@@ -67,6 +72,7 @@ class ProjectNode:
|
||||
self._parent: ProjectNode | None = None
|
||||
self._row = 0
|
||||
self._cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {}
|
||||
self._flags = NODE_FLAGS
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
@@ -145,6 +151,10 @@ class ProjectNode:
|
||||
"""Return cached node data."""
|
||||
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:
|
||||
return self._parent
|
||||
|
||||
@@ -213,9 +223,10 @@ class ProjectNode:
|
||||
def _updateRelationships(self, child: ProjectNode) -> None:
|
||||
"""Update a child item's relationships."""
|
||||
if self._parent:
|
||||
child.item.setParent(self.item.itemHandle)
|
||||
child.item.setRoot(self.item.itemRoot)
|
||||
child.item.setClassDefaults(self.item.itemClass)
|
||||
child.item.setParent(self._item.itemHandle)
|
||||
child.item.setRoot(self._item.itemRoot)
|
||||
child.item.setClassDefaults(self._item.itemClass)
|
||||
child._flags |= Qt.ItemFlag.ItemIsDragEnabled
|
||||
else:
|
||||
child.item.setParent(None)
|
||||
child.item.setRoot(child.item.itemHandle)
|
||||
@@ -230,6 +241,7 @@ class ProjectModel(QAbstractItemModel):
|
||||
super().__init__()
|
||||
self._tree = tree
|
||||
self._root = ProjectNode(NWItem(tree._project, "invisibleRoot"))
|
||||
self._root.item.setName("Invisible Root")
|
||||
logger.debug("Ready: ProjectModel")
|
||||
return
|
||||
|
||||
@@ -276,6 +288,55 @@ class ProjectModel(QAbstractItemModel):
|
||||
return index.internalPointer().data(index.column(), role)
|
||||
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
|
||||
##
|
||||
@@ -353,12 +414,11 @@ class ProjectModel(QAbstractItemModel):
|
||||
for index in indices:
|
||||
if index.isValid():
|
||||
node: ProjectNode = index.internalPointer()
|
||||
print(node)
|
||||
if node.item.itemClass != nwItemClass.TRASH:
|
||||
return False
|
||||
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."""
|
||||
if target.isValid():
|
||||
# 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:
|
||||
index = self.indexFromNode(node)
|
||||
if child := self.removeChild(index.parent(), index.row()):
|
||||
self.insertChild(child, target, -1)
|
||||
self.insertChild(child, target, pos)
|
||||
return
|
||||
|
||||
##
|
||||
|
||||
@@ -3,10 +3,11 @@ novelWriter – GUI Project Tree
|
||||
==============================
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1] GuiProjectTree
|
||||
Created: 2022-06-06 [2.0rc1] GuiProjectView
|
||||
Created: 2022-06-06 [2.0rc1] GuiProjectToolBar
|
||||
Created: 2023-11-22 [2.2rc1] _TreeContextMenu
|
||||
Created: 2018-09-29 [0.0.1] GuiProjectTree
|
||||
Created: 2022-06-06 [2.0rc1] GuiProjectView
|
||||
Created: 2022-06-06 [2.0rc1] GuiProjectToolBar
|
||||
Created: 2023-11-22 [2.2rc1] _TreeContextMenu
|
||||
Rewritten: 2024-11-17 [2.7b1] GuiProjectTree
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
@@ -543,8 +544,8 @@ class GuiProjectTree(QTreeView):
|
||||
self.setIndentation(iPx)
|
||||
|
||||
# Allow Move by Drag & Drop
|
||||
# self.setDragEnabled(True)
|
||||
# self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||
|
||||
# Disable built-in auto scroll as it isn't working in some Qt
|
||||
# releases (see #1561) and instead use our own implementation
|
||||
|
||||
Reference in New Issue
Block a user