Add back create new project item functionality
This commit is contained in:
@@ -32,6 +32,7 @@ from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from novelwriter import SHARED
|
||||
from novelwriter.common import minmax
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.types import QtAlignRight
|
||||
|
||||
@@ -164,36 +165,44 @@ class ProjectNode:
|
||||
# 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._row = len(self._children)
|
||||
self._children.append(child)
|
||||
if 0 <= pos < len(self._children):
|
||||
self._children.insert(pos, child)
|
||||
self._refreshChildrenPos()
|
||||
else:
|
||||
child._row = len(self._children)
|
||||
self._children.append(child)
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
def moveChild(self, source: int, step: int) -> int:
|
||||
def moveChild(self, source: int, target: int) -> None:
|
||||
"""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
|
||||
if (source != target) and (0 <= source < count) and (0 <= target <= count):
|
||||
node = self._children.pop(source)
|
||||
self._children.insert(target, node)
|
||||
self._refreshChildrenPos()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _recursiveAppendChildren(self, children: list[ProjectNode]) -> None:
|
||||
"""Recursively add all nodes to a list."""
|
||||
for node in self._children:
|
||||
children.append(node)
|
||||
node._recursiveAppendChildren(children)
|
||||
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):
|
||||
|
||||
@@ -258,18 +267,17 @@ class ProjectModel(QAbstractItemModel):
|
||||
node: ProjectNode = index.internalPointer()
|
||||
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
|
||||
##
|
||||
|
||||
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:
|
||||
"""Return the node for a given model index."""
|
||||
if index.isValid():
|
||||
@@ -286,25 +294,41 @@ class ProjectModel(QAbstractItemModel):
|
||||
"""Get the index representing a node in the model."""
|
||||
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
|
||||
##
|
||||
|
||||
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:
|
||||
"""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)
|
||||
new = minmax(pos + step, 0, parent.childCount() - 1)
|
||||
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()
|
||||
return
|
||||
|
||||
def moveRows(self, indices: list[QModelIndex], destination: QModelIndex, row: int) -> bool:
|
||||
"""Move indices to destination."""
|
||||
return False
|
||||
|
||||
##
|
||||
# Other Methods
|
||||
##
|
||||
|
||||
+12
-24
@@ -154,20 +154,20 @@ class NWProject:
|
||||
# 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,
|
||||
use the class label.
|
||||
"""
|
||||
label = label or trConst(nwLabels.CLASS_NAME[itemClass])
|
||||
return self._tree.create(label, None, nwItemType.ROOT, itemClass)
|
||||
label = trConst(nwLabels.CLASS_NAME[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."""
|
||||
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."""
|
||||
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:
|
||||
"""Write content to a new document after it is created. This
|
||||
@@ -233,11 +233,11 @@ class NWProject:
|
||||
|
||||
def trashFolder(self) -> str:
|
||||
"""Add the special trash root folder to the project."""
|
||||
trashHandle = self._tree.trashRoot
|
||||
if trashHandle is None:
|
||||
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
|
||||
return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
|
||||
return trashHandle
|
||||
# trashHandle = self._tree.trashRoot
|
||||
# if trashHandle is None:
|
||||
# label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
|
||||
# return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
|
||||
return ""
|
||||
|
||||
##
|
||||
# Project Methods
|
||||
@@ -329,7 +329,6 @@ class NWProject:
|
||||
# ============
|
||||
|
||||
self._tree.unpack(projContent)
|
||||
self._tree.buildModel()
|
||||
self._options.loadSettings()
|
||||
self._loadProjectLocalisation()
|
||||
|
||||
@@ -490,17 +489,6 @@ class NWProject:
|
||||
self.setProjectChanged(True)
|
||||
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:
|
||||
"""Toggle the project changed flag, and propagate the
|
||||
information to the GUI statusbar.
|
||||
|
||||
+116
-165
@@ -28,10 +28,12 @@ import random
|
||||
|
||||
from collections.abc import Iterable, Iterator
|
||||
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.constants import nwFiles
|
||||
from novelwriter.constants import nwFiles, nwLabels, trConst
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.itemmodel import ProjectModel, ProjectNode
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||
@@ -73,8 +75,7 @@ class NWTree:
|
||||
self._model = ProjectModel(self)
|
||||
self._items: dict[str, NWItem] = {}
|
||||
self._nodes: dict[str, ProjectNode] = {}
|
||||
self._trash = None # The handle of the trash root folder
|
||||
self._changed = False # True if tree structure has changed
|
||||
self._trash = None
|
||||
logger.debug("Ready: NWTree")
|
||||
return
|
||||
|
||||
@@ -86,10 +87,10 @@ class NWTree:
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def trashRoot(self) -> str | None:
|
||||
"""Return the handle of the trash folder, or None."""
|
||||
return self._trash
|
||||
# @property
|
||||
# def trashRoot(self) -> str | None:
|
||||
# """Return the handle of the trash folder, or None."""
|
||||
# return self._trash
|
||||
|
||||
@property
|
||||
def model(self) -> ProjectModel:
|
||||
@@ -110,7 +111,6 @@ class NWTree:
|
||||
self._items = {}
|
||||
self._nodes = {}
|
||||
self._trash = None
|
||||
self._changed = False
|
||||
oldModel.deleteLater()
|
||||
del oldModel
|
||||
return
|
||||
@@ -119,74 +119,58 @@ class NWTree:
|
||||
"""Returns a copy of the list of all the active handles."""
|
||||
return list(self._items.keys())
|
||||
|
||||
@overload # pragma: no cover
|
||||
def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
|
||||
itemClass: nwItemClass) -> str:
|
||||
pass
|
||||
def add(self, item: NWItem, pos: int = -1) -> bool:
|
||||
"""Add a project item into the project tree."""
|
||||
if pHandle := item.itemParent:
|
||||
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(self, label: str, parent: str | None, itemType: nwItemType,
|
||||
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str | None:
|
||||
pass
|
||||
|
||||
def create(self, label, parent, itemType, itemClass=nwItemClass.NO_CLASS) -> str | None:
|
||||
def create(
|
||||
self, label: str, parent: str | None, itemType: nwItemType,
|
||||
itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1,
|
||||
) -> str | None:
|
||||
"""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
|
||||
parent, None is returned. For root elements, this cannot occur.
|
||||
"""
|
||||
# parent = None if itemType == nwItemType.ROOT else parent
|
||||
# if parent is None or parent in self._order:
|
||||
# tHandle = self._makeHandle()
|
||||
# newItem = NWItem(self._project, tHandle)
|
||||
# newItem.setName(label)
|
||||
# newItem.setParent(parent)
|
||||
# newItem.setType(itemType)
|
||||
# newItem.setClass(itemClass)
|
||||
# self.append(newItem)
|
||||
# self.updateItemData(tHandle)
|
||||
# return tHandle
|
||||
parent = None if itemType == nwItemType.ROOT else parent
|
||||
if parent is None or parent in self._nodes:
|
||||
tHandle = self._makeHandle()
|
||||
nwItem = NWItem(self._project, tHandle)
|
||||
nwItem.setName(label)
|
||||
nwItem.setParent(parent)
|
||||
nwItem.setType(itemType)
|
||||
nwItem.setClass(itemClass)
|
||||
if self.add(nwItem, pos):
|
||||
self.updateItemData(tHandle)
|
||||
self._project.setProjectChanged(True)
|
||||
return tHandle
|
||||
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:
|
||||
"""Duplicate an item and set a new handle."""
|
||||
sItem = self.__getitem__(sHandle)
|
||||
if isinstance(sItem, NWItem):
|
||||
nItem = NWItem.duplicate(sItem, self._makeHandle())
|
||||
if self.append(nItem):
|
||||
logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle)
|
||||
return nItem
|
||||
# sItem = self.__getitem__(sHandle)
|
||||
# if isinstance(sItem, NWItem):
|
||||
# nItem = NWItem.duplicate(sItem, self._makeHandle())
|
||||
# if self.append(nItem):
|
||||
# logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle)
|
||||
# return nItem
|
||||
return None
|
||||
|
||||
def pack(self) -> list[dict]:
|
||||
@@ -206,28 +190,22 @@ class NWTree:
|
||||
project tree.
|
||||
"""
|
||||
self.clear()
|
||||
items: dict[str, NWItem] = self._items.copy()
|
||||
for item in data:
|
||||
nwItem = NWItem(self._project, "")
|
||||
if nwItem.unpack(item):
|
||||
self._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
|
||||
items[nwItem.itemHandle] = nwItem
|
||||
|
||||
def buildModel(self) -> None:
|
||||
""""""
|
||||
later = items
|
||||
self._model.beginInsertRows(self._model.index(0, 0), 0, 0)
|
||||
later: dict[str, NWItem] = self._items.copy()
|
||||
for _ in range(999):
|
||||
later = self._buildTree(later)
|
||||
later = self._addItems(later)
|
||||
if len(later) == 0:
|
||||
break
|
||||
else:
|
||||
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.layoutChanged.emit()
|
||||
|
||||
@@ -248,30 +226,9 @@ class NWTree:
|
||||
else:
|
||||
for index in indices:
|
||||
self._model.dataChanged.emit(index, index)
|
||||
self._project.setProjectChanged(len(indices) > 0)
|
||||
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]:
|
||||
"""Check the project tree consistency. Also check the content
|
||||
folder and add back files that were discovered but were not
|
||||
@@ -324,7 +281,7 @@ class NWTree:
|
||||
newItem.setType(nwItemType.FILE)
|
||||
newItem.setClass(oClass)
|
||||
newItem.setLayout(oLayout)
|
||||
if self.append(newItem):
|
||||
if self.add(newItem):
|
||||
self.updateItemData(cHandle)
|
||||
recovered += 1
|
||||
|
||||
@@ -393,22 +350,20 @@ class NWTree:
|
||||
"""Update the root item handle of a given item. Returns True if
|
||||
a root was found and data updated, otherwise False.
|
||||
"""
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
return False
|
||||
|
||||
iItem = tItem
|
||||
for _ in range(MAX_DEPTH):
|
||||
if iItem.itemParent is None:
|
||||
tItem.setRoot(iItem.itemHandle)
|
||||
tItem.setClassDefaults(iItem.itemClass)
|
||||
return True
|
||||
if tItem := self._items.get(tHandle):
|
||||
iItem = tItem
|
||||
for _ in range(MAX_DEPTH):
|
||||
if iItem.itemParent is None:
|
||||
tItem.setRoot(iItem.itemHandle)
|
||||
tItem.setClassDefaults(iItem.itemClass)
|
||||
return True
|
||||
else:
|
||||
iItem = self.__getitem__(iItem.itemParent)
|
||||
if iItem is None:
|
||||
return False
|
||||
else:
|
||||
iItem = self.__getitem__(iItem.itemParent)
|
||||
if iItem is None:
|
||||
return False
|
||||
else:
|
||||
raise RecursionError("Critical internal error")
|
||||
raise RecursionError("Critical internal error")
|
||||
return False
|
||||
|
||||
def checkType(self, tHandle: str, itemType: nwItemType) -> bool:
|
||||
"""Check if item exists and is of the specified item type."""
|
||||
@@ -463,18 +418,18 @@ class NWTree:
|
||||
|
||||
def isTrash(self, tHandle: str) -> bool:
|
||||
"""Check if an item is in or is the trash folder."""
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
return True
|
||||
if tItem.itemClass == nwItemClass.TRASH:
|
||||
return True
|
||||
if self._trash is not None:
|
||||
if tHandle == self._trash:
|
||||
return True
|
||||
elif tItem.itemParent == self._trash:
|
||||
return True
|
||||
elif tItem.itemRoot == self._trash:
|
||||
return True
|
||||
# tItem = self.__getitem__(tHandle)
|
||||
# if tItem is None:
|
||||
# return True
|
||||
# if tItem.itemClass == nwItemClass.TRASH:
|
||||
# return True
|
||||
# if self._trash is not None:
|
||||
# if tHandle == self._trash:
|
||||
# return True
|
||||
# elif tItem.itemParent == self._trash:
|
||||
# return True
|
||||
# elif tItem.itemRoot == self._trash:
|
||||
# return True
|
||||
return False
|
||||
|
||||
def findRoot(self, itemClass: nwItemClass | None) -> str | None:
|
||||
@@ -484,29 +439,6 @@ class NWTree:
|
||||
return node.item.itemHandle
|
||||
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
|
||||
##
|
||||
@@ -542,8 +474,6 @@ class NWTree:
|
||||
# if tHandle == self._trash:
|
||||
# self._trash = None
|
||||
|
||||
# self._setTreeChanged(True)
|
||||
|
||||
return
|
||||
|
||||
def __contains__(self, tHandle: str) -> bool:
|
||||
@@ -554,24 +484,45 @@ class NWTree:
|
||||
"""Iterate through project items."""
|
||||
for node in self._model.root.allChildren():
|
||||
yield node.item
|
||||
# for tHandle in self._order:
|
||||
# tItem = self._tree.get(tHandle)
|
||||
# if isinstance(tItem, NWItem):
|
||||
# yield tItem
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _setTreeChanged(self, state: bool) -> None:
|
||||
"""Set the changed flag to state, and if being set to True,
|
||||
propagate that state change to the parent NWProject class.
|
||||
def _getTrashNode(self) -> ProjectNode | None:
|
||||
"""Get the trash node. If it doesn't exist, create it."""
|
||||
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
|
||||
if state:
|
||||
self._project.setProjectChanged(True)
|
||||
return
|
||||
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._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:
|
||||
"""Generate a unique item handle. In the event that the key
|
||||
|
||||
+96
-112
@@ -39,7 +39,7 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
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.itemmodel import ProjectModel, ProjectNode
|
||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||
@@ -568,8 +568,8 @@ class GuiProjectTree(QTreeView):
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
|
||||
# Connect signals
|
||||
self.clicked.connect(self._treeSingleClick)
|
||||
self.doubleClicked.connect(self._treeDoubleClick)
|
||||
self.clicked.connect(self._onSingleClick)
|
||||
self.doubleClicked.connect(self._onDoubleClick)
|
||||
|
||||
# Auto Scroll
|
||||
# self._scrollMargin = SHARED.theme.baseIconHeight
|
||||
@@ -639,13 +639,19 @@ class GuiProjectTree(QTreeView):
|
||||
treeHeader.resizeSection(self.C_ACTIVE, iPx + cMg)
|
||||
treeHeader.resizeSection(self.C_STATUS, iPx + cMg)
|
||||
|
||||
self.blockSignals(True)
|
||||
for index in SHARED.project.tree.model.allExpanded():
|
||||
self.setExpanded(index, True)
|
||||
self.blockSignals(False)
|
||||
self.restoreExpandedState()
|
||||
|
||||
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:
|
||||
"""Set a specific handle as the selected item."""
|
||||
if (model := self._getModel()) and (index := model.indexFromHandle(tHandle)).isValid():
|
||||
@@ -655,6 +661,87 @@ class GuiProjectTree(QTreeView):
|
||||
self.projView.selectedItemChanged.emit(tHandle)
|
||||
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
|
||||
##
|
||||
@@ -736,14 +823,14 @@ class GuiProjectTree(QTreeView):
|
||||
##
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def _treeSingleClick(self, index: QModelIndex) -> None:
|
||||
def _onSingleClick(self, index: QModelIndex) -> None:
|
||||
"""The user changed which item is selected."""
|
||||
if node := self._getNode(index):
|
||||
self.projView.selectedItemChanged.emit(node.item.itemHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def _treeDoubleClick(self, index: QModelIndex) -> None:
|
||||
def _onDoubleClick(self, index: QModelIndex) -> None:
|
||||
"""Capture a double-click event and either request the document
|
||||
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)
|
||||
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,
|
||||
wordCount: bool = False) -> bool:
|
||||
"""Reveal a newly added project item in the project tree."""
|
||||
|
||||
@@ -987,25 +987,25 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
|
||||
|
||||
# Try to open a file with nothings selected
|
||||
projTree.clearSelection()
|
||||
projTree._treeDoubleClick(QTreeWidgetItem(), 0)
|
||||
projTree._onDoubleClick(QTreeWidgetItem(), 0)
|
||||
assert nwGUI.docEditor.docHandle is None
|
||||
|
||||
# When the item cannot be found
|
||||
projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None)
|
||||
projTree._treeDoubleClick(QTreeWidgetItem(), 0)
|
||||
projTree._onDoubleClick(QTreeWidgetItem(), 0)
|
||||
assert nwGUI.docEditor.docHandle is None
|
||||
|
||||
# Successfully open a file
|
||||
projTree._treeDoubleClick(projTree._getTreeItem(C.hTitlePage), 0)
|
||||
projTree._onDoubleClick(projTree._getTreeItem(C.hTitlePage), 0)
|
||||
assert nwGUI.docEditor.docHandle == C.hTitlePage
|
||||
projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore
|
||||
|
||||
# A non-file item should be expanded instead
|
||||
projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # 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 projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore
|
||||
|
||||
|
||||
Reference in New Issue
Block a user