From 53e70c118c6c008f489f80192e240c5c7627fdcf Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 19 Nov 2024 00:22:59 +0100 Subject: [PATCH] Add back create new project item functionality --- novelwriter/core/itemmodel.py | 78 +++++--- novelwriter/core/project.py | 36 ++-- novelwriter/core/tree.py | 281 ++++++++++++---------------- novelwriter/gui/projtree.py | 208 ++++++++++---------- tests/test_gui/test_gui_projtree.py | 8 +- 5 files changed, 279 insertions(+), 332 deletions(-) diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index d7e5f987..67253897 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -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 ## diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index e7025af6..ab960fe8 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -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. diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index d0b1ad42..d159e383 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -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 diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 10b37618..adcaad09 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -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.""" diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index af14b26c..5aaedacc 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -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