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()
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
##
@@ -81,6 +89,10 @@ class ProjectNode:
def children(self) -> list[ProjectNode]:
return self._children
##
# Data Maintenance
##
def refresh(self) -> None:
cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {}
@@ -121,6 +133,10 @@ class ProjectNode:
parent.updateCount()
return
##
# Data Access
##
def row(self) -> int:
return self._row
@@ -139,6 +155,15 @@ class ProjectNode:
return self._children[row]
return None
def allChildren(self) -> list[ProjectNode]:
nodes: list[ProjectNode] = []
self._recursiveAppendChildren(nodes)
return nodes
##
# Data Edit
##
def addChild(self, child: ProjectNode) -> None:
child._parent = self
child._row = len(self._children)
@@ -146,10 +171,18 @@ class ProjectNode:
self.refresh()
return
def allChildren(self) -> list[ProjectNode]:
nodes: list[ProjectNode] = []
self._recursiveAppendChildren(nodes)
return nodes
def moveChild(self, source: int, step: int) -> int:
"""Move a child internally."""
count = len(self._children)
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
@@ -167,10 +200,10 @@ class ProjectModel(QAbstractItemModel):
__slots__ = ("_tree", "_root")
def __init__(self, tree: NWTree) -> None:
super().__init__(None)
logger.debug("Create: ProjectModel")
super().__init__()
self._tree = tree
self._root = ProjectNode(NWItem(tree._project, "invisibleRoot"))
logger.debug("Ready: ProjectModel")
return
def __del__(self) -> None:
@@ -254,7 +287,26 @@ class ProjectModel(QAbstractItemModel):
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]:
+8
View File
@@ -75,6 +75,11 @@ class NWTree:
self._nodes: dict[str, ProjectNode] = {}
self._trash = None # The handle of the trash root folder
self._changed = False # True if tree structure has changed
logger.debug("Ready: NWTree")
return
def __del__(self) -> None:
logger.debug("Delete: NWTree")
return
##
@@ -100,11 +105,14 @@ class NWTree:
def clear(self) -> None:
"""Clear the item tree entirely."""
oldModel = self._model
self._model = ProjectModel(self)
self._items = {}
self._nodes = {}
self._trash = None
self._changed = False
oldModel.deleteLater()
del oldModel
return
def handles(self) -> list[str]:
+5 -1
View File
@@ -28,7 +28,7 @@ import logging
from pathlib import Path
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 novelwriter import CONFIG, SHARED
@@ -337,12 +337,16 @@ class GuiMainMenu(QMenuBar):
# View > Go Backward
self.aViewPrev = self.viewMenu.addAction(self.tr("Navigate Backward"))
self.aViewPrev.setShortcut("Alt+Left")
self.aViewPrev.setShortcutContext(Qt.ShortcutContext.WidgetShortcut)
self.aViewPrev.triggered.connect(self.mainGui.docViewer.navBackward)
self.mainGui.docViewer.addAction(self.aViewPrev)
# View > Go Forward
self.aViewNext = self.viewMenu.addAction(self.tr("Navigate Forward"))
self.aViewNext.setShortcut("Alt+Right")
self.aViewNext.setShortcutContext(Qt.ShortcutContext.WidgetShortcut)
self.aViewNext.triggered.connect(self.mainGui.docViewer.navForward)
self.mainGui.docViewer.addAction(self.aViewNext)
# View > Separator
self.viewMenu.addSeparator()
+90 -101
View File
@@ -31,7 +31,7 @@ import logging
from enum import Enum
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 (
QAbstractItemView, QAction, QFrame, QHBoxLayout, QHeaderView, QLabel,
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.extensions.modified import NIconToolButton
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__)
@@ -91,32 +94,32 @@ class GuiProjectView(QWidget):
self.keyMoveUp = QShortcut(self.projTree)
self.keyMoveUp.setKey("Ctrl+Up")
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.setKey("Ctrl+Down")
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.setKey("Alt+Up")
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.setKey("Alt+Down")
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.setKey("Alt+Left")
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.setKey("Alt+Right")
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.setKey("Ctrl+.")
@@ -288,11 +291,11 @@ class GuiProjectToolBar(QWidget):
# Move Buttons
self.tbMoveU = NIconToolButton(self, iSz)
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.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
self.mAdd = QMenu(self)
@@ -616,7 +619,11 @@ class GuiProjectTree(QTreeView):
def loadModel(self) -> None:
"""Load and prepare a new project model."""
selModel = self.selectionModel()
self.setModel(SHARED.project.tree.model)
if selModel:
selModel.deleteLater()
del selModel
# Lock the column sizes
iPx = SHARED.theme.baseIconHeight
@@ -649,9 +656,81 @@ class GuiProjectTree(QTreeView):
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
##
@@ -838,80 +917,6 @@ class GuiProjectTree(QTreeView):
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]:
"""Recursively return all the child items starting from a given
item handle.
@@ -1455,22 +1460,6 @@ class GuiProjectTree(QTreeView):
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:
"""Merge an item's child documents into a single document."""
# logger.info("Request to merge items under handle '%s'", tHandle)