Re-enable project tree move item and navigation

This commit is contained in:
Veronica Berglyd Olsen
2024-11-18 18:04:25 +01:00
parent df0244095f
commit a5575992d6
4 changed files with 162 additions and 109 deletions
+59 -7
View File
@@ -69,6 +69,14 @@ class ProjectNode:
self.refresh() self.refresh()
return return
def __repr__(self) -> str:
return (
f"<ProjectNode handle={self._item.itemHandle} "
f"parent={self._parent.item.itemHandle if self._parent else None} "
f"row={self._row} "
f"children={len(self._children)}>"
)
## ##
# Properties # Properties
## ##
@@ -81,6 +89,10 @@ class ProjectNode:
def children(self) -> list[ProjectNode]: def children(self) -> list[ProjectNode]:
return self._children return self._children
##
# Data Maintenance
##
def refresh(self) -> None: def refresh(self) -> None:
cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {} cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {}
@@ -121,6 +133,10 @@ class ProjectNode:
parent.updateCount() parent.updateCount()
return return
##
# Data Access
##
def row(self) -> int: def row(self) -> int:
return self._row return self._row
@@ -139,6 +155,15 @@ class ProjectNode:
return self._children[row] return self._children[row]
return None return None
def allChildren(self) -> list[ProjectNode]:
nodes: list[ProjectNode] = []
self._recursiveAppendChildren(nodes)
return nodes
##
# Data Edit
##
def addChild(self, child: ProjectNode) -> None: def addChild(self, child: ProjectNode) -> None:
child._parent = self child._parent = self
child._row = len(self._children) child._row = len(self._children)
@@ -146,10 +171,18 @@ class ProjectNode:
self.refresh() self.refresh()
return return
def allChildren(self) -> list[ProjectNode]: def moveChild(self, source: int, step: int) -> int:
nodes: list[ProjectNode] = [] """Move a child internally."""
self._recursiveAppendChildren(nodes) count = len(self._children)
return nodes if 0 <= source < count:
target = max(min(source + step, count - 1), 0)
if source != target:
node = self._children.pop(source)
self._children.insert(target, node)
for n, child in enumerate(self._children):
child._row = n
return target + 1 if target > source else target
return -1
## ##
# Internal Functions # Internal Functions
@@ -167,10 +200,10 @@ class ProjectModel(QAbstractItemModel):
__slots__ = ("_tree", "_root") __slots__ = ("_tree", "_root")
def __init__(self, tree: NWTree) -> None: def __init__(self, tree: NWTree) -> None:
super().__init__(None) super().__init__()
logger.debug("Create: ProjectModel")
self._tree = tree self._tree = tree
self._root = ProjectNode(NWItem(tree._project, "invisibleRoot")) self._root = ProjectNode(NWItem(tree._project, "invisibleRoot"))
logger.debug("Ready: ProjectModel")
return return
def __del__(self) -> None: def __del__(self) -> None:
@@ -254,7 +287,26 @@ class ProjectModel(QAbstractItemModel):
return self.createIndex(node.row(), 0, node) return self.createIndex(node.row(), 0, node)
## ##
# Methods # Model Edit
##
def internalMove(self, index: QModelIndex, step: int) -> None:
"""Move an item internally among its siblings."""
if index.isValid():
node: ProjectNode = index.internalPointer()
if parent := node.parent():
pos = index.row()
if (new := parent.moveChild(index.row(), step)) > -1:
self.beginMoveRows(index.parent(), pos, pos, index.parent(), new)
self.endMoveRows()
return
def moveRows(self, indices: list[QModelIndex], destination: QModelIndex, row: int) -> bool:
"""Move indices to destination."""
return False
##
# Other Methods
## ##
def allExpanded(self) -> list[QModelIndex]: def allExpanded(self) -> list[QModelIndex]:
+8
View File
@@ -75,6 +75,11 @@ class NWTree:
self._nodes: dict[str, ProjectNode] = {} self._nodes: dict[str, ProjectNode] = {}
self._trash = None # The handle of the trash root folder self._trash = None # The handle of the trash root folder
self._changed = False # True if tree structure has changed self._changed = False # True if tree structure has changed
logger.debug("Ready: NWTree")
return
def __del__(self) -> None:
logger.debug("Delete: NWTree")
return return
## ##
@@ -100,11 +105,14 @@ class NWTree:
def clear(self) -> None: def clear(self) -> None:
"""Clear the item tree entirely.""" """Clear the item tree entirely."""
oldModel = self._model
self._model = ProjectModel(self) self._model = ProjectModel(self)
self._items = {} self._items = {}
self._nodes = {} self._nodes = {}
self._trash = None self._trash = None
self._changed = False self._changed = False
oldModel.deleteLater()
del oldModel
return return
def handles(self) -> list[str]: def handles(self) -> list[str]:
+5 -1
View File
@@ -28,7 +28,7 @@ import logging
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtSignal, pyqtSlot from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QAction, QMenuBar from PyQt5.QtWidgets import QAction, QMenuBar
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
@@ -337,12 +337,16 @@ class GuiMainMenu(QMenuBar):
# View > Go Backward # View > Go Backward
self.aViewPrev = self.viewMenu.addAction(self.tr("Navigate Backward")) self.aViewPrev = self.viewMenu.addAction(self.tr("Navigate Backward"))
self.aViewPrev.setShortcut("Alt+Left") self.aViewPrev.setShortcut("Alt+Left")
self.aViewPrev.setShortcutContext(Qt.ShortcutContext.WidgetShortcut)
self.aViewPrev.triggered.connect(self.mainGui.docViewer.navBackward) self.aViewPrev.triggered.connect(self.mainGui.docViewer.navBackward)
self.mainGui.docViewer.addAction(self.aViewPrev)
# View > Go Forward # View > Go Forward
self.aViewNext = self.viewMenu.addAction(self.tr("Navigate Forward")) self.aViewNext = self.viewMenu.addAction(self.tr("Navigate Forward"))
self.aViewNext.setShortcut("Alt+Right") self.aViewNext.setShortcut("Alt+Right")
self.aViewNext.setShortcutContext(Qt.ShortcutContext.WidgetShortcut)
self.aViewNext.triggered.connect(self.mainGui.docViewer.navForward) self.aViewNext.triggered.connect(self.mainGui.docViewer.navForward)
self.mainGui.docViewer.addAction(self.aViewNext)
# View > Separator # View > Separator
self.viewMenu.addSeparator() self.viewMenu.addSeparator()
+90 -101
View File
@@ -31,7 +31,7 @@ import logging
from enum import Enum from enum import Enum
from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtGui import QIcon, QMouseEvent, QPalette
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QFrame, QHBoxLayout, QHeaderView, QLabel, QAbstractItemView, QAction, QFrame, QHBoxLayout, QHeaderView, QLabel,
QMenu, QShortcut, QTreeView, QTreeWidgetItem, QVBoxLayout, QWidget QMenu, QShortcut, QTreeView, QTreeWidgetItem, QVBoxLayout, QWidget
@@ -47,7 +47,10 @@ from novelwriter.dialogs.projectsettings import GuiProjectSettings
from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType
from novelwriter.extensions.modified import NIconToolButton from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.types import QtScrollAlwaysOff, QtScrollAsNeeded, QtSizeExpanding from novelwriter.types import (
QtMouseLeft, QtMouseMiddle, QtScrollAlwaysOff, QtScrollAsNeeded,
QtSizeExpanding
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -91,32 +94,32 @@ class GuiProjectView(QWidget):
self.keyMoveUp = QShortcut(self.projTree) self.keyMoveUp = QShortcut(self.projTree)
self.keyMoveUp.setKey("Ctrl+Up") self.keyMoveUp.setKey("Ctrl+Up")
self.keyMoveUp.setContext(Qt.ShortcutContext.WidgetShortcut) self.keyMoveUp.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyMoveUp.activated.connect(qtLambda(self.projTree.moveTreeItem, -1)) self.keyMoveUp.activated.connect(self.projTree.moveItemUp)
self.keyMoveDn = QShortcut(self.projTree) self.keyMoveDn = QShortcut(self.projTree)
self.keyMoveDn.setKey("Ctrl+Down") self.keyMoveDn.setKey("Ctrl+Down")
self.keyMoveDn.setContext(Qt.ShortcutContext.WidgetShortcut) self.keyMoveDn.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyMoveDn.activated.connect(qtLambda(self.projTree.moveTreeItem, 1)) self.keyMoveDn.activated.connect(self.projTree.moveItemDown)
self.keyGoPrev = QShortcut(self.projTree) self.keyGoPrev = QShortcut(self.projTree)
self.keyGoPrev.setKey("Alt+Up") self.keyGoPrev.setKey("Alt+Up")
self.keyGoPrev.setContext(Qt.ShortcutContext.WidgetShortcut) self.keyGoPrev.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyGoPrev.activated.connect(qtLambda(self.projTree.moveToNextItem, -1)) self.keyGoPrev.activated.connect(self.projTree.moveSiblingUp)
self.keyGoNext = QShortcut(self.projTree) self.keyGoNext = QShortcut(self.projTree)
self.keyGoNext.setKey("Alt+Down") self.keyGoNext.setKey("Alt+Down")
self.keyGoNext.setContext(Qt.ShortcutContext.WidgetShortcut) self.keyGoNext.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyGoNext.activated.connect(qtLambda(self.projTree.moveToNextItem, 1)) self.keyGoNext.activated.connect(self.projTree.moveSiblingDown)
self.keyGoUp = QShortcut(self.projTree) self.keyGoUp = QShortcut(self.projTree)
self.keyGoUp.setKey("Alt+Left") self.keyGoUp.setKey("Alt+Left")
self.keyGoUp.setContext(Qt.ShortcutContext.WidgetShortcut) self.keyGoUp.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyGoUp.activated.connect(qtLambda(self.projTree.moveToLevel, -1)) self.keyGoUp.activated.connect(self.projTree.moveToParent)
self.keyGoDown = QShortcut(self.projTree) self.keyGoDown = QShortcut(self.projTree)
self.keyGoDown.setKey("Alt+Right") self.keyGoDown.setKey("Alt+Right")
self.keyGoDown.setContext(Qt.ShortcutContext.WidgetShortcut) self.keyGoDown.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyGoDown.activated.connect(qtLambda(self.projTree.moveToLevel, 1)) self.keyGoDown.activated.connect(self.projTree.moveToFirstChild)
self.keyContext = QShortcut(self.projTree) self.keyContext = QShortcut(self.projTree)
self.keyContext.setKey("Ctrl+.") self.keyContext.setKey("Ctrl+.")
@@ -288,11 +291,11 @@ class GuiProjectToolBar(QWidget):
# Move Buttons # Move Buttons
self.tbMoveU = NIconToolButton(self, iSz) self.tbMoveU = NIconToolButton(self, iSz)
self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up")) self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
self.tbMoveU.clicked.connect(qtLambda(self.projTree.moveTreeItem, -1)) self.tbMoveU.clicked.connect(self.projTree.moveItemUp)
self.tbMoveD = NIconToolButton(self, iSz) self.tbMoveD = NIconToolButton(self, iSz)
self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
self.tbMoveD.clicked.connect(qtLambda(self.projTree.moveTreeItem, 1)) self.tbMoveD.clicked.connect(self.projTree.moveItemDown)
# Add Item Menu # Add Item Menu
self.mAdd = QMenu(self) self.mAdd = QMenu(self)
@@ -616,7 +619,11 @@ class GuiProjectTree(QTreeView):
def loadModel(self) -> None: def loadModel(self) -> None:
"""Load and prepare a new project model.""" """Load and prepare a new project model."""
selModel = self.selectionModel()
self.setModel(SHARED.project.tree.model) self.setModel(SHARED.project.tree.model)
if selModel:
selModel.deleteLater()
del selModel
# Lock the column sizes # Lock the column sizes
iPx = SHARED.theme.baseIconHeight iPx = SHARED.theme.baseIconHeight
@@ -649,9 +656,81 @@ class GuiProjectTree(QTreeView):
return return
## ##
# Private Slots # Events
## ##
def mousePressEvent(self, event: QMouseEvent) -> None:
"""Overload mousePressEvent to clear selection if clicking the
mouse in a blank area of the tree view, and to load a document
for viewing if the user middle-clicked.
"""
super().mousePressEvent(event)
if event.button() == QtMouseLeft:
if not self.indexAt(event.pos()).isValid():
self.selectionModel().clearCurrentIndex()
elif event.button() == QtMouseMiddle:
if (node := self._getNode(self.indexAt(event.pos()))) and node.item.isFileType():
self.projView.openDocumentRequest.emit(
node.item.itemHandle, nwDocMode.VIEW, "", False
)
return
##
# Public Slots
##
@pyqtSlot()
def moveItemUp(self) -> None:
"""Move an item up in the tree."""
if model := self._getModel():
model.internalMove(self.currentIndex(), -1)
return
@pyqtSlot()
def moveItemDown(self) -> None:
"""Move an item down in the tree."""
if model := self._getModel():
model.internalMove(self.currentIndex(), 1)
return
@pyqtSlot()
def moveSiblingUp(self) -> None:
"""Skip to the previous sibling."""
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
if (move := parent.child(node.row() - 1)) and (model := self._getModel()):
self.setCurrentIndex(model.indexFromNode(move))
return
@pyqtSlot()
def moveSiblingDown(self) -> None:
"""Skip to the next sibling."""
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
if (move := parent.child(node.row() + 1)) and (model := self._getModel()):
self.setCurrentIndex(model.indexFromNode(move))
return
@pyqtSlot()
def moveToParent(self) -> None:
"""Move to parent item."""
if (
(model := self._getModel())
and (node := model.node(self.currentIndex()))
and (parent := node.parent())
):
self.setCurrentIndex(model.indexFromNode(parent))
return
@pyqtSlot()
def moveToFirstChild(self) -> None:
"""Move to first child item."""
if (
(model := self._getModel())
and (node := model.node(self.currentIndex()))
and (child := node.child(0))
):
self.setCurrentIndex(model.indexFromNode(child))
return
## ##
# Private Slots # Private Slots
## ##
@@ -838,80 +917,6 @@ class GuiProjectTree(QTreeView):
return True return True
def moveTreeItem(self, step: int) -> bool:
"""Move an item up or down in the tree."""
# tHandle = self.getSelectedHandle()
# tItem = self._getTreeItem(tHandle)
# if tItem is None:
# logger.debug("No item selected")
# return False
# pItem = tItem.parent()
# isExp = tItem.isExpanded()
# if pItem is None:
# tIndex = self.indexOfTopLevelItem(tItem)
# nChild = self.topLevelItemCount()
# nIndex = tIndex + step
# if nIndex < 0 or nIndex >= nChild:
# return False
# cItem = self.takeTopLevelItem(tIndex)
# self.insertTopLevelItem(nIndex, cItem)
# else:
# tIndex = pItem.indexOfChild(tItem)
# nChild = pItem.childCount()
# nIndex = tIndex + step
# if nIndex < 0 or nIndex >= nChild:
# return False
# cItem = pItem.takeChild(tIndex)
# pItem.insertChild(nIndex, cItem)
# self._alertTreeChange(tHandle, flush=True)
# self.setCurrentItem(tItem)
# tItem.setExpanded(isExp)
return True
def moveToNextItem(self, step: int) -> None:
"""Move to the next item of the same tree level."""
# tHandle = self.getSelectedHandle()
# tItem = self._getTreeItem(tHandle) if tHandle else None
# if tItem:
# pItem = tItem.parent() or self.invisibleRootItem()
# next = minmax(pItem.indexOfChild(tItem) + step, 0, pItem.childCount() - 1)
# self.setCurrentItem(pItem.child(next))
return
def moveToLevel(self, step: int) -> None:
"""Move to the next item in the parent/child chain."""
# tHandle = self.getSelectedHandle()
# tItem = self._getTreeItem(tHandle) if tHandle else None
# if tItem:
# if step < 0 and tItem.parent():
# self.setCurrentItem(tItem.parent())
# elif step > 0 and tItem.childCount() > 0:
# self.setCurrentItem(tItem.child(0))
return
# def saveTreeOrder(self) -> None:
# """Build a list of the items in the project tree and send them
# to the project class. This syncs up the two versions of the
# project structure, and must be called before any code that
# depends on this order to be up to date.
# """
# # items = []
# # for i in range(self.topLevelItemCount()):
# # item = self.topLevelItem(i)
# # if isinstance(item, QTreeWidgetItem):
# # items = self._scanChildren(items, item, i)
# # logger.debug("Saving project tree item order")
# # SHARED.project.setTreeOrder(items)
# return
def getTreeFromHandle(self, tHandle: str) -> list[str]: def getTreeFromHandle(self, tHandle: str) -> list[str]:
"""Recursively return all the child items starting from a given """Recursively return all the child items starting from a given
item handle. item handle.
@@ -1455,22 +1460,6 @@ class GuiProjectTree(QTreeView):
return return
def _getTreeItem(self, tHandle: str | None) -> QTreeWidgetItem | None:
"""Return the QTreeWidgetItem of a given item handle."""
# return self._treeMap.get(tHandle, None) if tHandle else None
return None
def _recursiveSetExpanded(self, trItem: QTreeWidgetItem, isExpanded: bool) -> None:
"""Recursive function to set expanded status starting from (and
not including) a given item.
"""
# if isinstance(trItem, QTreeWidgetItem):
# for i in range(trItem.childCount()):
# chItem = trItem.child(i)
# chItem.setExpanded(isExpanded)
# self._recursiveSetExpanded(chItem, isExpanded)
return
def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool: def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool:
"""Merge an item's child documents into a single document.""" """Merge an item's child documents into a single document."""
# logger.info("Request to merge items under handle '%s'", tHandle) # logger.info("Request to merge items under handle '%s'", tHandle)