diff --git a/novelwriter/constants.py b/novelwriter/constants.py index dc4a44eb..237b715b 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -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 diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index 182ea296..c29abbc3 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -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 ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index ff725f80..f029647e 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -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