Add back create new project item functionality

This commit is contained in:
Veronica Berglyd Olsen
2024-11-19 00:22:59 +01:00
parent a5575992d6
commit 53e70c118c
5 changed files with 279 additions and 332 deletions
+51 -27
View File
@@ -32,6 +32,7 @@ from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from novelwriter import SHARED from novelwriter import SHARED
from novelwriter.common import minmax
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.types import QtAlignRight from novelwriter.types import QtAlignRight
@@ -164,36 +165,44 @@ class ProjectNode:
# Data Edit # Data Edit
## ##
def addChild(self, child: ProjectNode) -> None: def addChild(self, child: ProjectNode, pos: int = -1) -> None:
"""Add a child item to this item."""
child._parent = self child._parent = self
child._row = len(self._children) if 0 <= pos < len(self._children):
self._children.append(child) self._children.insert(pos, child)
self._refreshChildrenPos()
else:
child._row = len(self._children)
self._children.append(child)
self.refresh() self.refresh()
return return
def moveChild(self, source: int, step: int) -> int: def moveChild(self, source: int, target: int) -> None:
"""Move a child internally.""" """Move a child internally."""
count = len(self._children) count = len(self._children)
if 0 <= source < count: if (source != target) and (0 <= source < count) and (0 <= target <= count):
target = max(min(source + step, count - 1), 0) node = self._children.pop(source)
if source != target: self._children.insert(target, node)
node = self._children.pop(source) self._refreshChildrenPos()
self._children.insert(target, node) return
for n, child in enumerate(self._children):
child._row = n
return target + 1 if target > source else target
return -1
## ##
# Internal Functions # Internal Functions
## ##
def _recursiveAppendChildren(self, children: list[ProjectNode]) -> None: def _recursiveAppendChildren(self, children: list[ProjectNode]) -> None:
"""Recursively add all nodes to a list."""
for node in self._children: for node in self._children:
children.append(node) children.append(node)
node._recursiveAppendChildren(children) node._recursiveAppendChildren(children)
return return
def _refreshChildrenPos(self) -> None:
"""Update the row value on all children."""
for n, child in enumerate(self._children):
child._row = n
return
class ProjectModel(QAbstractItemModel): class ProjectModel(QAbstractItemModel):
@@ -258,18 +267,17 @@ class ProjectModel(QAbstractItemModel):
node: ProjectNode = index.internalPointer() node: ProjectNode = index.internalPointer()
return node.data(index.column(), role) return node.data(index.column(), role)
# def addChild(self, node: ProjectNode, parent: QModelIndex) -> None:
# if parent.isValid():
# item = parent.internalPointer()
# else:
# item = self._root
# item.addChild(node)
# return
## ##
# Data Access # Data Access
## ##
def row(self, index: QModelIndex) -> int:
"""Return the row number of the index."""
if index.isValid():
node: ProjectNode = index.internalPointer()
return node.row()
return -1
def node(self, index: QModelIndex) -> ProjectNode | None: def node(self, index: QModelIndex) -> ProjectNode | None:
"""Return the node for a given model index.""" """Return the node for a given model index."""
if index.isValid(): if index.isValid():
@@ -286,25 +294,41 @@ class ProjectModel(QAbstractItemModel):
"""Get the index representing a node in the model.""" """Get the index representing a node in the model."""
return self.createIndex(node.row(), 0, node) return self.createIndex(node.row(), 0, node)
def rootIndex(self) -> QModelIndex:
"""Get the index representing the root."""
return self.createIndex(0, 0, self._root)
## ##
# Model Edit # Model Edit
## ##
def insertChild(self, child: ProjectNode, parent: QModelIndex, pos: int) -> None:
"""Insert a node into the model at a given position."""
if parent.isValid():
node: ProjectNode = parent.internalPointer()
else:
node = self._root
count = node.childCount()
row = minmax(pos, 0, count) if pos >= 0 else count
self.beginInsertRows(parent, row, row)
node.addChild(child, row)
self.endInsertRows()
return
def internalMove(self, index: QModelIndex, step: int) -> None: def internalMove(self, index: QModelIndex, step: int) -> None:
"""Move an item internally among its siblings.""" """Move an item internally among its siblings."""
if index.isValid(): if index.isValid():
node: ProjectNode = index.internalPointer() node: ProjectNode = index.internalPointer()
if parent := node.parent(): if parent := node.parent():
pos = index.row() pos = index.row()
if (new := parent.moveChild(index.row(), step)) > -1: new = minmax(pos + step, 0, parent.childCount() - 1)
self.beginMoveRows(index.parent(), pos, pos, index.parent(), new) if new != pos:
end = new if new < pos else new + 1
self.beginMoveRows(index.parent(), pos, pos, index.parent(), end)
parent.moveChild(pos, new)
self.endMoveRows() self.endMoveRows()
return return
def moveRows(self, indices: list[QModelIndex], destination: QModelIndex, row: int) -> bool:
"""Move indices to destination."""
return False
## ##
# Other Methods # Other Methods
## ##
+12 -24
View File
@@ -154,20 +154,20 @@ class NWProject:
# Item Methods # Item Methods
## ##
def newRoot(self, itemClass: nwItemClass, label: str | None = None) -> str: def newRoot(self, itemClass: nwItemClass, pos: int = -1) -> str | None:
"""Add a new root folder to the project. If label is not set, """Add a new root folder to the project. If label is not set,
use the class label. use the class label.
""" """
label = label or trConst(nwLabels.CLASS_NAME[itemClass]) label = trConst(nwLabels.CLASS_NAME[itemClass])
return self._tree.create(label, None, nwItemType.ROOT, itemClass) return self._tree.create(label, None, nwItemType.ROOT, itemClass=itemClass, pos=pos)
def newFolder(self, label: str, parent: str) -> str | None: def newFolder(self, label: str, parent: str, pos: int = -1) -> str | None:
"""Add a new folder with a given label and parent item.""" """Add a new folder with a given label and parent item."""
return self._tree.create(label, parent, nwItemType.FOLDER) return self._tree.create(label, parent, nwItemType.FOLDER, pos=pos)
def newFile(self, label: str, parent: str) -> str | None: def newFile(self, label: str, parent: str, pos: int = -1) -> str | None:
"""Add a new file with a given label and parent item.""" """Add a new file with a given label and parent item."""
return self._tree.create(label, parent, nwItemType.FILE) return self._tree.create(label, parent, nwItemType.FILE, pos=pos)
def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, text: str = "") -> bool: def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, text: str = "") -> bool:
"""Write content to a new document after it is created. This """Write content to a new document after it is created. This
@@ -233,11 +233,11 @@ class NWProject:
def trashFolder(self) -> str: def trashFolder(self) -> str:
"""Add the special trash root folder to the project.""" """Add the special trash root folder to the project."""
trashHandle = self._tree.trashRoot # trashHandle = self._tree.trashRoot
if trashHandle is None: # if trashHandle is None:
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH]) # label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH) # return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
return trashHandle return ""
## ##
# Project Methods # Project Methods
@@ -329,7 +329,6 @@ class NWProject:
# ============ # ============
self._tree.unpack(projContent) self._tree.unpack(projContent)
self._tree.buildModel()
self._options.loadSettings() self._options.loadSettings()
self._loadProjectLocalisation() self._loadProjectLocalisation()
@@ -490,17 +489,6 @@ class NWProject:
self.setProjectChanged(True) self.setProjectChanged(True)
return return
def setTreeOrder(self, order: list[str]) -> None:
"""A list representing the linear/flattened order of project
items in the GUI project tree. The user can rearrange the order
by drag-and-drop. Forwarded to the NWTree class.
"""
if len(self._tree) != len(order):
logger.warning("Sizes of new and old tree order do not match")
self._tree.setOrder(order)
self.setProjectChanged(True)
return
def setProjectChanged(self, status: bool) -> bool: def setProjectChanged(self, status: bool) -> bool:
"""Toggle the project changed flag, and propagate the """Toggle the project changed flag, and propagate the
information to the GUI statusbar. information to the GUI statusbar.
+116 -165
View File
@@ -28,10 +28,12 @@ import random
from collections.abc import Iterable, Iterator from collections.abc import Iterable, Iterator
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Literal, overload from typing import TYPE_CHECKING
from PyQt5.QtCore import QModelIndex
from novelwriter import SHARED from novelwriter import SHARED
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles, nwLabels, trConst
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.core.itemmodel import ProjectModel, ProjectNode from novelwriter.core.itemmodel import ProjectModel, ProjectNode
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
@@ -73,8 +75,7 @@ class NWTree:
self._model = ProjectModel(self) self._model = ProjectModel(self)
self._items: dict[str, NWItem] = {} self._items: dict[str, NWItem] = {}
self._nodes: dict[str, ProjectNode] = {} self._nodes: dict[str, ProjectNode] = {}
self._trash = None # The handle of the trash root folder self._trash = None
self._changed = False # True if tree structure has changed
logger.debug("Ready: NWTree") logger.debug("Ready: NWTree")
return return
@@ -86,10 +87,10 @@ class NWTree:
# Properties # Properties
## ##
@property # @property
def trashRoot(self) -> str | None: # def trashRoot(self) -> str | None:
"""Return the handle of the trash folder, or None.""" # """Return the handle of the trash folder, or None."""
return self._trash # return self._trash
@property @property
def model(self) -> ProjectModel: def model(self) -> ProjectModel:
@@ -110,7 +111,6 @@ class NWTree:
self._items = {} self._items = {}
self._nodes = {} self._nodes = {}
self._trash = None self._trash = None
self._changed = False
oldModel.deleteLater() oldModel.deleteLater()
del oldModel del oldModel
return return
@@ -119,74 +119,58 @@ class NWTree:
"""Returns a copy of the list of all the active handles.""" """Returns a copy of the list of all the active handles."""
return list(self._items.keys()) return list(self._items.keys())
@overload # pragma: no cover def add(self, item: NWItem, pos: int = -1) -> bool:
def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT], """Add a project item into the project tree."""
itemClass: nwItemClass) -> str: if pHandle := item.itemParent:
pass if parent := self._nodes.get(pHandle):
node = ProjectNode(item)
index = self._model.indexFromNode(parent)
self._model.insertChild(node, index, pos)
self._nodes[item.itemHandle] = node
self._items[item.itemHandle] = item
else:
logger.error("Could not locate parent of '%s'", item.itemHandle)
return False
elif item.isRootType():
node = ProjectNode(item)
self._model.insertChild(node, QModelIndex(), pos)
self._nodes[item.itemHandle] = node
self._items[item.itemHandle] = item
else:
logger.error("Invalid project item '%s'", item.itemHandle)
return False
return True
@overload # pragma: no cover def create(
def create(self, label: str, parent: str | None, itemType: nwItemType, self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str | None: itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1,
pass ) -> str | None:
def create(self, label, parent, itemType, itemClass=nwItemClass.NO_CLASS) -> str | None:
"""Create a new item in the project tree, and return its handle. """Create a new item in the project tree, and return its handle.
If the item cannot be added to the project because of an invalid If the item cannot be added to the project because of an invalid
parent, None is returned. For root elements, this cannot occur. parent, None is returned. For root elements, this cannot occur.
""" """
# parent = None if itemType == nwItemType.ROOT else parent parent = None if itemType == nwItemType.ROOT else parent
# if parent is None or parent in self._order: if parent is None or parent in self._nodes:
# tHandle = self._makeHandle() tHandle = self._makeHandle()
# newItem = NWItem(self._project, tHandle) nwItem = NWItem(self._project, tHandle)
# newItem.setName(label) nwItem.setName(label)
# newItem.setParent(parent) nwItem.setParent(parent)
# newItem.setType(itemType) nwItem.setType(itemType)
# newItem.setClass(itemClass) nwItem.setClass(itemClass)
# self.append(newItem) if self.add(nwItem, pos):
# self.updateItemData(tHandle) self.updateItemData(tHandle)
# return tHandle self._project.setProjectChanged(True)
return tHandle
return None return None
def append(self, nwItem: NWItem) -> bool:
"""Add a new item to the end of the tree."""
# tHandle = nwItem.itemHandle
# pHandle = nwItem.itemParent
# if not isHandle(tHandle):
# logger.warning("Invalid item handle '%s' detected, skipping", tHandle)
# return False
# if tHandle in self._tree:
# logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
# return False
# logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
# if nwItem.isRootType():
# logger.debug("Item '%s' is a root item", str(tHandle))
# self._roots[tHandle] = nwItem
# if nwItem.itemClass == nwItemClass.TRASH:
# if self._trash is None:
# logger.debug("Item '%s' is the trash folder", str(tHandle))
# self._trash = tHandle
# else:
# logger.error("Only one trash folder allowed")
# return False
# self._tree[tHandle] = nwItem
# self._order.append(tHandle)
# self._setTreeChanged(True)
return True
def duplicate(self, sHandle: str) -> NWItem | None: def duplicate(self, sHandle: str) -> NWItem | None:
"""Duplicate an item and set a new handle.""" """Duplicate an item and set a new handle."""
sItem = self.__getitem__(sHandle) # sItem = self.__getitem__(sHandle)
if isinstance(sItem, NWItem): # if isinstance(sItem, NWItem):
nItem = NWItem.duplicate(sItem, self._makeHandle()) # nItem = NWItem.duplicate(sItem, self._makeHandle())
if self.append(nItem): # if self.append(nItem):
logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle) # logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle)
return nItem # return nItem
return None return None
def pack(self) -> list[dict]: def pack(self) -> list[dict]:
@@ -206,28 +190,22 @@ class NWTree:
project tree. project tree.
""" """
self.clear() self.clear()
items: dict[str, NWItem] = self._items.copy()
for item in data: for item in data:
nwItem = NWItem(self._project, "") nwItem = NWItem(self._project, "")
if nwItem.unpack(item): if nwItem.unpack(item):
self._items[nwItem.itemHandle] = nwItem items[nwItem.itemHandle] = nwItem
if nwItem.itemClass == nwItemClass.TRASH:
logger.debug("Item '%s' is the trash folder", str(nwItem.itemHandle))
self._trash = nwItem.itemHandle
return
def buildModel(self) -> None: later = items
""""""
self._model.beginInsertRows(self._model.index(0, 0), 0, 0) self._model.beginInsertRows(self._model.index(0, 0), 0, 0)
later: dict[str, NWItem] = self._items.copy()
for _ in range(999): for _ in range(999):
later = self._buildTree(later) later = self._addItems(later)
if len(later) == 0: if len(later) == 0:
break break
else: else:
logger.error("Not all items could be added to project tree") logger.error("Not all items could be added to project tree")
for item in later.values():
item.setParent(None)
self._trash = self._getTrashNode()
self._model.endInsertRows() self._model.endInsertRows()
self._model.layoutChanged.emit() self._model.layoutChanged.emit()
@@ -248,30 +226,9 @@ class NWTree:
else: else:
for index in indices: for index in indices:
self._model.dataChanged.emit(index, index) self._model.dataChanged.emit(index, index)
self._project.setProjectChanged(len(indices) > 0)
return return
def _buildTree(self, items: dict[str, NWItem]) -> dict[str, NWItem]:
""""""
remains: dict[str, NWItem] = {}
for handle, item in items.items():
if pHandle := item.itemParent:
if parent := self._nodes.get(pHandle):
node = ProjectNode(item)
parent.addChild(node)
self._nodes[handle] = node
elif pHandle in items:
remains[handle] = item
logger.warning("Item '%s' found before its parent", handle)
else:
item.setParent(None)
logger.error("Item '%s' has no parent in current tree", handle)
elif item.isRootType():
node = ProjectNode(item)
self._model.root.addChild(node)
self._nodes[handle] = node
return remains
def checkConsistency(self, prefix: str) -> tuple[int, int]: def checkConsistency(self, prefix: str) -> tuple[int, int]:
"""Check the project tree consistency. Also check the content """Check the project tree consistency. Also check the content
folder and add back files that were discovered but were not folder and add back files that were discovered but were not
@@ -324,7 +281,7 @@ class NWTree:
newItem.setType(nwItemType.FILE) newItem.setType(nwItemType.FILE)
newItem.setClass(oClass) newItem.setClass(oClass)
newItem.setLayout(oLayout) newItem.setLayout(oLayout)
if self.append(newItem): if self.add(newItem):
self.updateItemData(cHandle) self.updateItemData(cHandle)
recovered += 1 recovered += 1
@@ -393,22 +350,20 @@ class NWTree:
"""Update the root item handle of a given item. Returns True if """Update the root item handle of a given item. Returns True if
a root was found and data updated, otherwise False. a root was found and data updated, otherwise False.
""" """
tItem = self.__getitem__(tHandle) if tItem := self._items.get(tHandle):
if tItem is None: iItem = tItem
return False for _ in range(MAX_DEPTH):
if iItem.itemParent is None:
iItem = tItem tItem.setRoot(iItem.itemHandle)
for _ in range(MAX_DEPTH): tItem.setClassDefaults(iItem.itemClass)
if iItem.itemParent is None: return True
tItem.setRoot(iItem.itemHandle) else:
tItem.setClassDefaults(iItem.itemClass) iItem = self.__getitem__(iItem.itemParent)
return True if iItem is None:
return False
else: else:
iItem = self.__getitem__(iItem.itemParent) raise RecursionError("Critical internal error")
if iItem is None: return False
return False
else:
raise RecursionError("Critical internal error")
def checkType(self, tHandle: str, itemType: nwItemType) -> bool: def checkType(self, tHandle: str, itemType: nwItemType) -> bool:
"""Check if item exists and is of the specified item type.""" """Check if item exists and is of the specified item type."""
@@ -463,18 +418,18 @@ class NWTree:
def isTrash(self, tHandle: str) -> bool: def isTrash(self, tHandle: str) -> bool:
"""Check if an item is in or is the trash folder.""" """Check if an item is in or is the trash folder."""
tItem = self.__getitem__(tHandle) # tItem = self.__getitem__(tHandle)
if tItem is None: # if tItem is None:
return True # return True
if tItem.itemClass == nwItemClass.TRASH: # if tItem.itemClass == nwItemClass.TRASH:
return True # return True
if self._trash is not None: # if self._trash is not None:
if tHandle == self._trash: # if tHandle == self._trash:
return True # return True
elif tItem.itemParent == self._trash: # elif tItem.itemParent == self._trash:
return True # return True
elif tItem.itemRoot == self._trash: # elif tItem.itemRoot == self._trash:
return True # return True
return False return False
def findRoot(self, itemClass: nwItemClass | None) -> str | None: def findRoot(self, itemClass: nwItemClass | None) -> str | None:
@@ -484,29 +439,6 @@ class NWTree:
return node.item.itemHandle return node.item.itemHandle
return None return None
##
# Setters
##
def setOrder(self, newOrder: list[str]) -> None:
"""Reorders the tree based on a list of items."""
# tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._tree]
# if not (len(tmpOrder) == len(newOrder) == len(self._order)):
# # Something is wrong, so let's debug it
# for tHandle in newOrder:
# if tHandle not in self._tree:
# logger.error("Handle '%s' in new tree order is not in old order", tHandle)
# for tHandle in self._order:
# if tHandle not in tmpOrder:
# logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
# # Save the temp list
# self._order = tmpOrder
# self._setTreeChanged(True)
# logger.debug("Project tree order updated")
return
## ##
# Special Methods # Special Methods
## ##
@@ -542,8 +474,6 @@ class NWTree:
# if tHandle == self._trash: # if tHandle == self._trash:
# self._trash = None # self._trash = None
# self._setTreeChanged(True)
return return
def __contains__(self, tHandle: str) -> bool: def __contains__(self, tHandle: str) -> bool:
@@ -554,24 +484,45 @@ class NWTree:
"""Iterate through project items.""" """Iterate through project items."""
for node in self._model.root.allChildren(): for node in self._model.root.allChildren():
yield node.item yield node.item
# for tHandle in self._order:
# tItem = self._tree.get(tHandle)
# if isinstance(tItem, NWItem):
# yield tItem
return return
## ##
# Internal Functions # Internal Functions
## ##
def _setTreeChanged(self, state: bool) -> None: def _getTrashNode(self) -> ProjectNode | None:
"""Set the changed flag to state, and if being set to True, """Get the trash node. If it doesn't exist, create it."""
propagate that state change to the parent NWProject class. for node in self._model.root.children:
if node.item.itemClass == nwItemClass.TRASH:
return node
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
if handle := self.create(label, None, nwItemType.ROOT, nwItemClass.TRASH):
return self._nodes.get(handle)
return None
def _addItems(self, items: dict[str, NWItem]) -> dict[str, NWItem]:
"""Add a dictionary of items to the project tree. Returns a new
dictionary of items that could not be added yet, but can be.
""" """
self._changed = state remains: dict[str, NWItem] = {}
if state: for handle, item in items.items():
self._project.setProjectChanged(True) if pHandle := item.itemParent:
return if parent := self._nodes.get(pHandle):
node = ProjectNode(item)
parent.addChild(node)
self._items[handle] = item
self._nodes[handle] = node
elif pHandle in items:
remains[handle] = item
logger.warning("Item '%s' found before its parent", handle)
else:
logger.error("Item '%s' has no parent in current tree", handle)
elif item.isRootType():
node = ProjectNode(item)
self._model.root.addChild(node)
self._items[handle] = item
self._nodes[handle] = node
return remains
def _makeHandle(self) -> str: def _makeHandle(self) -> str:
"""Generate a unique item handle. In the event that the key """Generate a unique item handle. In the event that the key
+96 -112
View File
@@ -39,7 +39,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import qtLambda from novelwriter.common import qtLambda
from novelwriter.constants import nwLabels, nwUnicode, trConst from novelwriter.constants import nwLabels, nwStyles, nwUnicode, trConst
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.core.itemmodel import ProjectModel, ProjectNode from novelwriter.core.itemmodel import ProjectModel, ProjectNode
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
@@ -568,8 +568,8 @@ class GuiProjectTree(QTreeView):
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
# Connect signals # Connect signals
self.clicked.connect(self._treeSingleClick) self.clicked.connect(self._onSingleClick)
self.doubleClicked.connect(self._treeDoubleClick) self.doubleClicked.connect(self._onDoubleClick)
# Auto Scroll # Auto Scroll
# self._scrollMargin = SHARED.theme.baseIconHeight # self._scrollMargin = SHARED.theme.baseIconHeight
@@ -639,13 +639,19 @@ class GuiProjectTree(QTreeView):
treeHeader.resizeSection(self.C_ACTIVE, iPx + cMg) treeHeader.resizeSection(self.C_ACTIVE, iPx + cMg)
treeHeader.resizeSection(self.C_STATUS, iPx + cMg) treeHeader.resizeSection(self.C_STATUS, iPx + cMg)
self.blockSignals(True) self.restoreExpandedState()
for index in SHARED.project.tree.model.allExpanded():
self.setExpanded(index, True)
self.blockSignals(False)
return return
def restoreExpandedState(self) -> None:
"""Expand all nodes that were previously expanded."""
if model := self._getModel():
self.blockSignals(True)
for index in model.allExpanded():
self.setExpanded(index, True)
self.blockSignals(False)
return
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None: def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
"""Set a specific handle as the selected item.""" """Set a specific handle as the selected item."""
if (model := self._getModel()) and (index := model.indexFromHandle(tHandle)).isValid(): if (model := self._getModel()) and (index := model.indexFromHandle(tHandle)).isValid():
@@ -655,6 +661,87 @@ class GuiProjectTree(QTreeView):
self.projView.selectedItemChanged.emit(tHandle) self.projView.selectedItemChanged.emit(tHandle)
return return
def newTreeItem(
self, itemType: nwItemType, itemClass: nwItemClass | None = None,
hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None,
) -> None:
"""Add new item to the tree, with a given itemType (and
itemClass if Root), and attach it to the selected handle. Also
make sure the item is added in a place it can be added, and that
other meta data is set correctly to ensure a valid project tree.
"""
if not SHARED.hasProject:
logger.error("No project open")
return
tHandle = None
if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
pos = -1
if (node := self._getNode(self.currentIndex())) and (itemRoot := node.item.itemRoot):
if root := SHARED.project.tree.nodes.get(itemRoot):
pos = root.row() + 1
SHARED.project.newRoot(itemClass, pos)
self.restoreExpandedState()
elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
if not ((model := self._getModel()) and (node := model.node(self.currentIndex()))):
SHARED.error(self.tr("Did not find anywhere to add the file or folder!"))
return
if node.item.itemClass == nwItemClass.TRASH:
SHARED.error(self.tr("Cannot add new files or folders to the Trash folder."))
return
# Collect some information about the selected item
sLevel = nwStyles.H_LEVEL.get(node.item.mainHeading, 0)
sIsParent = node.childCount() > 0
# Set default label and determine if new item is to be added
# as child or sibling to the selected item
if itemType == nwItemType.FILE:
if copyDoc and (cItem := SHARED.project.tree[copyDoc]):
newLabel = cItem.itemName
asChild = sIsParent and node.item.isDocumentLayout()
elif isNote:
newLabel = self.tr("New Note")
asChild = sIsParent
elif hLevel == 2:
newLabel = self.tr("New Chapter")
asChild = sIsParent and node.item.isDocumentLayout() and sLevel < 2
elif hLevel == 3:
newLabel = self.tr("New Scene")
asChild = sIsParent and node.item.isDocumentLayout() and sLevel < 3
else:
newLabel = self.tr("New Document")
asChild = sIsParent and node.item.isDocumentLayout()
else:
newLabel = self.tr("New Folder")
asChild = False
pos = -1
sHandle = None
if not (asChild or node.item.isFolderType() or node.item.isRootType()):
pos = node.row() + 1
sHandle = node.item.itemParent
sHandle = sHandle or node.item.itemHandle
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=newLabel)
if dlgOk:
# Add the file or folder
if itemType == nwItemType.FILE:
tHandle = SHARED.project.newFile(newLabel, sHandle, pos)
if tHandle and copyDoc:
SHARED.project.copyFileContent(tHandle, copyDoc)
elif tHandle and hLevel > 0:
SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
else:
tHandle = SHARED.project.newFolder(newLabel, sHandle, pos)
return
## ##
# Events # Events
## ##
@@ -736,14 +823,14 @@ class GuiProjectTree(QTreeView):
## ##
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _treeSingleClick(self, index: QModelIndex) -> None: def _onSingleClick(self, index: QModelIndex) -> None:
"""The user changed which item is selected.""" """The user changed which item is selected."""
if node := self._getNode(index): if node := self._getNode(index):
self.projView.selectedItemChanged.emit(node.item.itemHandle) self.projView.selectedItemChanged.emit(node.item.itemHandle)
return return
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _treeDoubleClick(self, index: QModelIndex) -> None: def _onDoubleClick(self, index: QModelIndex) -> None:
"""Capture a double-click event and either request the document """Capture a double-click event and either request the document
for editing if it is a file, or expand/close the node if not. for editing if it is a file, or expand/close the node if not.
""" """
@@ -789,109 +876,6 @@ class GuiProjectTree(QTreeView):
# self.revealNewTreeItem(tHandle, wordCount=True) # self.revealNewTreeItem(tHandle, wordCount=True)
return return
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
"""Add new item to the tree, with a given itemType (and
itemClass if Root), and attach it to the selected handle. Also
make sure the item is added in a place it can be added, and that
other meta data is set correctly to ensure a valid project tree.
"""
# if not SHARED.hasProject:
# logger.error("No project open")
# return False
# nHandle = None
# tHandle = None
# if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
# tHandle = SHARED.project.newRoot(itemClass)
# sHandle = self.getSelectedHandle()
# pItem = SHARED.project.tree[sHandle] if sHandle else None
# nHandle = pItem.itemRoot if pItem else None
# elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
# sHandle = self.getSelectedHandle()
# pItem = SHARED.project.tree[sHandle] if sHandle else None
# if sHandle is None or pItem is None:
# SHARED.error(self.tr("Did not find anywhere to add the file or folder!"))
# return False
# # Collect some information about the selected item
# qItem = self._getTreeItem(sHandle)
# sLevel = nwStyles.H_LEVEL.get(pItem.mainHeading, 0)
# sIsParent = False if qItem is None else qItem.childCount() > 0
# if SHARED.project.tree.isTrash(sHandle):
# SHARED.error(self.tr("Cannot add new files or folders to the Trash folder."))
# return False
# # Set default label and determine if new item is to be added
# # as child or sibling to the selected item
# if itemType == nwItemType.FILE:
# if copyDoc and (cItem := SHARED.project.tree[copyDoc]):
# newLabel = cItem.itemName
# asChild = sIsParent and pItem.isDocumentLayout()
# elif isNote:
# newLabel = self.tr("New Note")
# asChild = sIsParent
# elif hLevel == 2:
# newLabel = self.tr("New Chapter")
# asChild = sIsParent and pItem.isDocumentLayout() and sLevel < 2
# elif hLevel == 3:
# newLabel = self.tr("New Scene")
# asChild = sIsParent and pItem.isDocumentLayout() and sLevel < 3
# else:
# newLabel = self.tr("New Document")
# asChild = sIsParent and pItem.isDocumentLayout()
# else:
# newLabel = self.tr("New Folder")
# asChild = False
# if not (asChild or pItem.isFolderType() or pItem.isRootType()):
# # Move to the parent item so that the new item is added
# # as a sibling instead
# nHandle = sHandle
# sHandle = pItem.itemParent
# if sHandle is None:
# # Bug: We have a condition that is unhandled
# logger.error("Internal error")
# return False
# # Ask for label
# newLabel, dlgOk = GuiEditLabel.getLabel(self, text=newLabel)
# if not dlgOk:
# logger.info("New item creation cancelled by user")
# return False
# # Add the file or folder
# if itemType == nwItemType.FILE:
# tHandle = SHARED.project.newFile(newLabel, sHandle)
# else:
# tHandle = SHARED.project.newFolder(newLabel, sHandle)
# else:
# logger.error("Failed to add new item")
# return False
# # If there is no handle set, return here. This is a bug.
# if tHandle is None: # pragma: no cover
# logger.error("Internal error")
# return True
# # Handle new file creation
# if itemType == nwItemType.FILE and copyDoc:
# SHARED.project.copyFileContent(tHandle, copyDoc)
# elif itemType == nwItemType.FILE and hLevel > 0:
# SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
# # Add the new item to the project tree
# self.revealNewTreeItem(tHandle, nHandle=nHandle, wordCount=True)
# self.projView.setTreeFocus() # See issue #1376
return True
def revealNewTreeItem(self, tHandle: str | None, nHandle: str | None = None, def revealNewTreeItem(self, tHandle: str | None, nHandle: str | None = None,
wordCount: bool = False) -> bool: wordCount: bool = False) -> bool:
"""Reveal a newly added project item in the project tree.""" """Reveal a newly added project item in the project tree."""
+4 -4
View File
@@ -987,25 +987,25 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
# Try to open a file with nothings selected # Try to open a file with nothings selected
projTree.clearSelection() projTree.clearSelection()
projTree._treeDoubleClick(QTreeWidgetItem(), 0) projTree._onDoubleClick(QTreeWidgetItem(), 0)
assert nwGUI.docEditor.docHandle is None assert nwGUI.docEditor.docHandle is None
# When the item cannot be found # When the item cannot be found
projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None) mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None)
projTree._treeDoubleClick(QTreeWidgetItem(), 0) projTree._onDoubleClick(QTreeWidgetItem(), 0)
assert nwGUI.docEditor.docHandle is None assert nwGUI.docEditor.docHandle is None
# Successfully open a file # Successfully open a file
projTree._treeDoubleClick(projTree._getTreeItem(C.hTitlePage), 0) projTree._onDoubleClick(projTree._getTreeItem(C.hTitlePage), 0)
assert nwGUI.docEditor.docHandle == C.hTitlePage assert nwGUI.docEditor.docHandle == C.hTitlePage
projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore
# A non-file item should be expanded instead # A non-file item should be expanded instead
projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore
projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore
projTree._treeDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1) projTree._onDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1)
assert nwGUI.docEditor.docHandle == C.hTitlePage assert nwGUI.docEditor.docHandle == C.hTitlePage
assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore