diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index e504444e..8c872245 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -154,13 +154,19 @@ class ProjectNode: class ProjectModel(QAbstractItemModel): - __slots__ = ("_root", "_map") + __slots__ = ("_tree", "_root") def __init__(self, tree: NWTree) -> None: super().__init__(None) + logger.debug("Create: ProjectModel") + self._tree = tree self._root = ProjectNode(NWItem(tree._project, "")) return + def __del__(self) -> None: + logger.debug("Delete: ProjectModel") + return + @property def root(self) -> ProjectNode: return self._root @@ -220,6 +226,16 @@ class ProjectModel(QAbstractItemModel): return index.internalPointer() return None + def indexFromHandle(self, handle: str | None) -> QModelIndex: + """Get the index representing a node in the model.""" + if handle and (node := self._tree.nodes.get(handle)): + return self.createIndex(node.row(), 0, node) + return QModelIndex() + + def indexFromNode(self, node: ProjectNode) -> QModelIndex: + """Get the index representing a node in the model.""" + return self.createIndex(node.row(), 0, node) + ## # Methods ## diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index ff8a5292..5ce9b554 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -98,6 +98,10 @@ class NWTree: def model(self) -> ProjectModel: return self._model + @property + def nodes(self) -> dict[str, ProjectNode]: + return self._nodes + ## # Class Methods ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index cb1d0354..159ce44d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -41,7 +41,7 @@ from novelwriter import CONFIG, SHARED from novelwriter.common import qtLambda from novelwriter.constants import nwLabels, nwUnicode, trConst from novelwriter.core.item import NWItem -from novelwriter.core.itemmodel import ProjectModel +from novelwriter.core.itemmodel import ProjectModel, ProjectNode from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType from novelwriter.extensions.modified import NIconToolButton @@ -165,13 +165,12 @@ class GuiProjectView(QWidget): def saveProjectTasks(self) -> None: """Run save project tasks.""" - self.projTree.saveTreeOrder() + # self.projTree.saveTreeOrder() return def populateTree(self) -> None: """Build the tree structure from project data.""" self.projTree.loadModel() - # self.projTree.buildTree() return def setTreeFocus(self) -> None: @@ -562,8 +561,6 @@ class GuiProjectTree(QTreeView): self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # Connect signals - # self.itemDoubleClicked.connect(self._treeDoubleClick) - # self.itemSelectionChanged.connect(self._treeSelectionChange) self.clicked.connect(self._treeSingleClick) self.doubleClicked.connect(self._treeDoubleClick) @@ -595,10 +592,26 @@ class GuiProjectTree(QTreeView): return ## - # Class Methods + # External Methods ## + def getSelectedHandle(self) -> str | None: + """Get the currently selected handle.""" + if (indexes := self.selectedIndexes()) and (node := self._getNode(indexes[0])): + return node.item.itemHandle + return None + + ## + # Module Internal Methods + ## + + def clearTree(self) -> None: + """Clear the tree view.""" + self.setModel(None) + return + def loadModel(self) -> None: + """Load and prepare a new project model.""" self.setModel(SHARED.project.tree.model) # Lock the column sizes @@ -622,12 +635,64 @@ class GuiProjectTree(QTreeView): return - def clearTree(self) -> None: - """Clear the GUI content and the related map.""" - # self.clear() - # self._treeMap = {} + 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(): + self.setCurrentIndex(index) + if doScroll: + self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter) + self.projView.selectedItemChanged.emit(tHandle) return + ## + # Private Slots + ## + + ## + # Private Slots + ## + + @pyqtSlot(QModelIndex) + def _treeSingleClick(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: + """Capture a double-click event and either request the document + for editing if it is a file, or expand/close the node if not. + """ + if node := self._getNode(index): + if node.item.isFileType(): + self.projView.openDocumentRequest.emit( + node.item.itemHandle, nwDocMode.EDIT, "", True + ) + else: + self.setExpanded(index, not self.isExpanded(index)) + return + + ## + # Internal Functions + ## + + def _getModel(self) -> ProjectModel | None: + """Return a project node corresponding to a model index.""" + if isinstance(model := self.model(), ProjectModel): + return model + return None + + def _getNode(self, index: QModelIndex) -> ProjectNode | None: + """Return a project node corresponding to a model index.""" + if isinstance(model := self.model(), ProjectModel) and (node := model.node(index)): + return node + return None + + # =========================================================================================== # + # Old Code + # =========================================================================================== # + def createNewNote(self, tag: str, itemClass: nwItemClass) -> None: """Create a new note. This function is used by the document editor to create note files for unknown tags. @@ -838,20 +903,20 @@ class GuiProjectTree(QTreeView): # self._alertTreeChange(tHandle, flush=False) return - def saveTreeOrder(self) -> None: - """Build a list of the items in the project tree and send them - to the project class. This syncs up the two versions of the - project structure, and must be called before any code that - depends on this order to be up to date. - """ - # items = [] - # for i in range(self.topLevelItemCount()): - # item = self.topLevelItem(i) - # if isinstance(item, QTreeWidgetItem): - # items = self._scanChildren(items, item, i) - # logger.debug("Saving project tree item order") - # SHARED.project.setTreeOrder(items) - return + # def saveTreeOrder(self) -> None: + # """Build a list of the items in the project tree and send them + # to the project class. This syncs up the two versions of the + # project structure, and must be called before any code that + # depends on this order to be up to date. + # """ + # # items = [] + # # for i in range(self.topLevelItemCount()): + # # item = self.topLevelItem(i) + # # if isinstance(item, QTreeWidgetItem): + # # items = self._scanChildren(items, item, i) + # # logger.debug("Saving project tree item order") + # # SHARED.project.setTreeOrder(items) + # return def getTreeFromHandle(self, tHandle: str) -> list[str]: """Recursively return all the child items starting from a given @@ -1140,35 +1205,21 @@ class GuiProjectTree(QTreeView): return - def buildTree(self) -> None: - """Build the entire project tree from scratch. This depends on - the save project item iterator in the project class which will - always make sure items with a parent have had their parent item - sent first. - """ - # logger.debug("Building the project tree ...") - # self.clearTree() - # for nwItem in SHARED.project.iterProjectItems(): - # self._addTreeItem(nwItem) - # self.setActiveHandle(self._actHandle) - # logger.info("%d item(s) added to the project tree", len(self._treeMap)) - return + # def getSelectedHandle(self) -> str | None: + # """Get the currently selected handle. If multiple items are + # selected, return the first. + # """ + # # if items := self.selectedItems(): + # # return items[0].data(self.C_DATA, self.D_HANDLE) + # return None - def getSelectedHandle(self) -> str | None: - """Get the currently selected handle. If multiple items are - selected, return the first. - """ - # if items := self.selectedItems(): - # return items[0].data(self.C_DATA, self.D_HANDLE) - return None - - def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None: - """Set a specific handle as the selected item.""" - # if tHandle in self._treeMap: - # self.setCurrentItem(self._treeMap[tHandle]) - # if (indexes := self.selectedIndexes()) and doScroll: - # self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter) - return + # def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None: + # """Set a specific handle as the selected item.""" + # # if tHandle in self._treeMap: + # # self.setCurrentItem(self._treeMap[tHandle]) + # # if (indexes := self.selectedIndexes()) and doScroll: + # # self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter) + # return def setActiveHandle(self, tHandle: str | None) -> None: """Highlight the rows associated with a given handle.""" @@ -1239,35 +1290,35 @@ class GuiProjectTree(QTreeView): # Private Slots ## - @pyqtSlot(QModelIndex) - def _treeSingleClick(self, index: QModelIndex) -> None: - """The user changed which item is selected.""" - if isinstance(model := self.model(), ProjectModel) and (node := model.node(index)): - self.projView.selectedItemChanged.emit(node.item.itemHandle) + # @pyqtSlot(QModelIndex) + # def _treeSingleClick(self, index: QModelIndex) -> None: + # """The user changed which item is selected.""" + # if isinstance(model := self.model(), ProjectModel) and (node := model.node(index)): + # self.projView.selectedItemChanged.emit(node.item.itemHandle) - # # When selecting multiple items, don't allow including root - # # items in the selection and instead deselect them - # items = self.selectedItems() - # if items and len(items) > 1: - # for item in items: - # if item.parent() is None: - # item.setSelected(False) + # # # When selecting multiple items, don't allow including root + # # # items in the selection and instead deselect them + # # items = self.selectedItems() + # # if items and len(items) > 1: + # # for item in items: + # # if item.parent() is None: + # # item.setSelected(False) - return + # return - @pyqtSlot(QModelIndex) - def _treeDoubleClick(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. - """ - if isinstance(model := self.model(), ProjectModel) and (node := model.node(index)): - if node.item.isFileType(): - self.projView.openDocumentRequest.emit( - node.item.itemHandle, nwDocMode.EDIT, "", True - ) - else: - self.setExpanded(index, not self.isExpanded(index)) - return + # @pyqtSlot(QModelIndex) + # def _treeDoubleClick(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. + # """ + # if isinstance(model := self.model(), ProjectModel) and (node := model.node(index)): + # if node.item.isFileType(): + # self.projView.openDocumentRequest.emit( + # node.item.itemHandle, nwDocMode.EDIT, "", True + # ) + # else: + # self.setExpanded(index, not self.isExpanded(index)) + # return @pyqtSlot() def _doAutoScroll(self) -> None: @@ -2007,7 +2058,7 @@ class _TreeContextMenu(QMenu): for tItem in self._items: if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH: self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False) - self.projTree.saveTreeOrder() + # self.projTree.saveTreeOrder() return @pyqtSlot() @@ -2017,7 +2068,7 @@ class _TreeContextMenu(QMenu): for tItem in self._items: if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH: self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False) - self.projTree.saveTreeOrder() + # self.projTree.saveTreeOrder() return @pyqtSlot()