Re-enable project tree move item and navigation
This commit is contained in:
@@ -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]:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user