diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py index 78aac8e1..ee585a0d 100644 --- a/novelwriter/extensions/novelselector.py +++ b/novelwriter/extensions/novelselector.py @@ -26,6 +26,7 @@ from __future__ import annotations import logging from PyQt6.QtCore import pyqtSignal, pyqtSlot +from PyQt6.QtGui import QPalette from PyQt6.QtWidgets import QComboBox, QWidget from novelwriter import SHARED @@ -46,6 +47,7 @@ class NovelSelector(QComboBox): self._includeAll = False self._listFormat = None self.currentIndexChanged.connect(self._indexChanged) + self.updateTheme() return ## @@ -86,6 +88,14 @@ class NovelSelector(QComboBox): self._listFormat = value return + def updateTheme(self) -> None: + """Update theme colours.""" + palette = self.palette() + palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, palette.text()) + self.setPalette(palette) + self.refreshNovelList() + return + ## # Public Slots ## diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 69e24763..8638c192 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -115,7 +115,6 @@ class GuiDocEditor(QPlainTextEdit): editedStatusChanged = pyqtSignal(bool) itemHandleChanged = pyqtSignal(str) loadDocumentTagRequest = pyqtSignal(str, Enum) - novelStructureChanged = pyqtSignal() openDocumentRequest = pyqtSignal(str, Enum, str, bool) requestNewNoteCreation = pyqtSignal(str, nwItemClass) requestNextDocument = pyqtSignal(str, bool) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index f3ee0778..ac98cd83 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -3,9 +3,11 @@ novelWriter – GUI Novel Tree ============================ File History: -Created: 2020-12-20 [1.1rc1] GuiNovelTree -Created: 2022-06-12 [2.0rc1] GuiNovelView -Created: 2022-06-12 [2.0rc1] GuiNovelToolBar +Created: 2020-12-20 [1.1rc1] GuiNovelTree +Created: 2022-06-12 [2.0rc1] GuiNovelView +Created: 2022-06-12 [2.0rc1] GuiNovelToolBar +Rewritten: 2025-02-22 [2.7b1] GuiNovelView +Rewritten: 2025-02-22 [2.7b1] GuiNovelToolBar This file is a part of novelWriter Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors @@ -40,7 +42,7 @@ from novelwriter import CONFIG, SHARED from novelwriter.common import minmax, qtAddAction, qtAddMenu, qtLambda from novelwriter.constants import nwKeyWords, nwLabels, trConst from novelwriter.core.novelmodel import NovelModel -from novelwriter.enum import nwChange, nwDocMode, nwItemClass, nwNovelExtra, nwOutline +from novelwriter.enum import nwChange, nwDocMode, nwNovelExtra, nwOutline from novelwriter.extensions.modified import NIconToolButton, NTreeView from novelwriter.extensions.novelselector import NovelSelector from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON @@ -105,9 +107,6 @@ class GuiNovelView(QWidget): def openProjectTasks(self) -> None: """Run open project tasks.""" lastNovel = SHARED.project.data.getLastHandle("novel") - if lastNovel and lastNovel not in SHARED.project.tree: - lastNovel = SHARED.project.tree.findRoot(nwItemClass.NOVEL) - logger.debug("Setting novel tree to root item '%s'", lastNovel) lastCol = SHARED.project.options.getEnum( @@ -129,13 +128,17 @@ class GuiNovelView(QWidget): def closeProjectTasks(self) -> None: """Run closing project tasks.""" + logger.debug("Saving State: GuiNovelView") + lastColType = self.novelTree.lastColType lastColSize = self.novelTree.lastColSize - logger.debug("Saving State: GuiNovelView") + options = SHARED.project.options options.setValue("GuiNovelView", "lastCol", lastColType) options.setValue("GuiNovelView", "lastColSize", lastColSize) + self.clearNovelView() + return def setTreeFocus(self) -> None: @@ -154,9 +157,7 @@ class GuiNovelView(QWidget): @pyqtSlot(str) def setCurrentNovel(self, rootHandle: str | None) -> None: """Set the current novel to display.""" - if rootHandle and (model := SHARED.project.index.getNovelModel(rootHandle)): - self.novelTree.setModel(model) - self.novelTree.resizeColumns() + self.novelTree.setNovelModel(rootHandle) return @pyqtSlot(str) @@ -270,7 +271,7 @@ class GuiNovelToolBar(QWidget): "QComboBox {border-style: none; padding-left: 0;} " "QComboBox::drop-down {border-style: none}" ) - self.novelValue.refreshNovelList() + self.novelValue.updateTheme() self.tbNovel.setVisible(self.novelValue.count() > 1) self._forceRefreshNovelTree() @@ -286,12 +287,13 @@ class GuiNovelToolBar(QWidget): def buildNovelRootMenu(self) -> None: """Build the novel root menu.""" self.novelValue.refreshNovelList() + self.novelView.setCurrentNovel(self.novelValue.handle) self.tbNovel.setVisible(self.novelValue.count() > 1) return def setCurrentRoot(self, rootHandle: str | None) -> None: """Set the current active root handle.""" - if rootHandle is None: + if rootHandle is None or rootHandle not in SHARED.project.tree: rootHandle = self.novelValue.firstHandle self.novelValue.setHandle(rootHandle) SHARED.project.data.setLastHandle(rootHandle, "novel") @@ -328,6 +330,7 @@ class GuiNovelToolBar(QWidget): def _forceRefreshNovelTree(self) -> None: """Rebuild the current tree.""" if tHandle := self.novelValue.handle: + self.novelView.setCurrentNovel(tHandle) SHARED.project.index.refreshNovelModel(tHandle) self._refresh[tHandle] = False return @@ -441,8 +444,7 @@ class GuiNovelTree(NTreeView): """Get the currently selected or active handle. If multiple items are selected, return the first. """ - if model := self._getModel(): - index = self.currentIndex() + if (model := self._getModel()) and (index := self.currentIndex()).isValid(): return model.handle(index), model.key(index) return None, None @@ -450,6 +452,16 @@ class GuiNovelTree(NTreeView): # Setters ## + def setNovelModel(self, tHandle: str | None) -> None: + """Set the current novel model.""" + if tHandle and (model := SHARED.project.index.getNovelModel(tHandle)): + if model is not self.model(): + self.setModel(model) + self.resizeColumns() + else: + self.clearContent() + return + def setActiveHandle(self, tHandle: str | None) -> None: """Set the handle to be highlighted.""" self._actHandle = tHandle @@ -560,18 +572,18 @@ class GuiNovelTree(NTreeView): def _popMetaBox(self, qPos: QPoint, tHandle: str, sTitle: str) -> None: """Show the novel meta data box.""" + def appendTags(refs: dict, key: str, lines: list[str]) -> None: + """Generate a reference list for a given reference key.""" + if tags := ", ".join(refs.get(key, [])): + lines.append(f"{trConst(nwLabels.KEY_NAME[key])}: {tags}") + return + if head := SHARED.project.index.getItemHeading(tHandle, sTitle): logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle) if synopsis := head.synopsis: label = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP]) synopsis = f"

{label}: {synopsis}

" - def appendTags(refs: dict, key: str, lines: list[str]) -> None: - """Generate a reference list for a given reference key.""" - if tags := ", ".join(refs.get(key, [])): - lines.append(f"{trConst(nwLabels.KEY_NAME[key])}: {tags}") - return - lines = [] if head := SHARED.project.index.getItemHeading(tHandle, sTitle): tags = head.getReferences() diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 3c9bfcea..2da937bd 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -24,22 +24,19 @@ from pathlib import Path import pytest -from PyQt6.QtCore import QEvent, QPoint, Qt -from PyQt6.QtGui import QFocusEvent +from PyQt6.QtCore import QModelIndex, QPoint, Qt from PyQt6.QtWidgets import QInputDialog, QToolTip from novelwriter import CONFIG, SHARED +from novelwriter.core.novelmodel import NovelModel from novelwriter.dialogs.editlabel import GuiEditLabel -from novelwriter.enum import nwFocus, nwItemType, nwNovelExtra -from novelwriter.gui.noveltree import GuiNovelTree -from novelwriter.types import QtMouseLeft, QtMouseMiddle +from novelwriter.enum import nwFocus, nwItemType, nwNovelExtra, nwView from tests.tools import C, buildTestProject @pytest.mark.gui -@pytest.mark.skip -def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): +def testGuiNovelView_Content(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test navigating the novel tree.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) @@ -52,20 +49,22 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): contentPath = SHARED.project.storage.contentPath assert isinstance(contentPath, Path) + cHandle = "0000000000010" - (contentPath / "0000000000010.nwd").write_text( + (contentPath / f"{cHandle}.nwd").write_text( "# Jane Doe\n\n@tag: Jane\n\n", encoding="utf-8" ) - (contentPath / "000000000000f.nwd").write_text(( + (contentPath / f"{C.hSceneDoc}.nwd").write_text(( "### Scene One\n\n" "@pov: Jane\n" "@focus: Jane\n\n" - "% Synopsis: This is a scene." + "% Synopsis: This is a scene.\n\n" + "This is some text in the edited scene." ), encoding="utf-8") novelView = nwGUI.novelView - novelTree = novelView.novelTree - novelBar = novelView.novelBar + novelTree = nwGUI.novelView.novelTree + novelBar = nwGUI.novelView.novelBar # Show/Hide Scrollbars # ==================== @@ -84,153 +83,101 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Populate Tree # ============= + root = QModelIndex() novelView.setTreeFocus() + nwGUI._changeView(nwView.NOVEL) - nwGUI.projStack.setCurrentWidget(nwGUI.novelView) - nwGUI.rebuildIndex() - novelTree._populateTree(rootHandle=None) - assert novelTree.topLevelItemCount() == 3 + # Clear tree + novelView.setCurrentNovel(None) + assert novelTree._getModel() is None - # Rebuild should preserve selection - topItem = novelTree.topLevelItem(0) - assert not topItem.isSelected() - topItem.setSelected(True) - assert novelTree.selectedItems()[0] == topItem - assert novelView.getSelectedHandle() == (C.hTitlePage, "T0001") + # Reload + novelBar._forceRefreshNovelTree() + model = novelTree._getModel() + assert isinstance(model, NovelModel) - # Refresh using the slot for the button - novelBar._refreshNovelTree() - assert novelTree.topLevelItem(0).isSelected() + # Check the items + assert model.rowCount(root) == 3 + assert model.columnCount(root) == 3 + assert model.data(model.createIndex(2, 1), Qt.ItemDataRole.DisplayRole) == "2" # Word Count + + nwGUI.rebuildIndex() # This should update the word count to the edited scene + assert model.data(model.createIndex(2, 1), Qt.ItemDataRole.DisplayRole) == "10" # Word Count + + # Extra Column + # ============ + novelBar.setLastColType(nwNovelExtra.POV) + assert model.rowCount(root) == 3 + assert model.columnCount(root) == 4 + + # Scene column should contain the POV character + assert model.data(model.createIndex(2, 2), Qt.ItemDataRole.DisplayRole) == "Jane" + + # Resize the last column + assert novelTree.lastColSize == 25 + with monkeypatch.context() as mp: + mp.setattr(QInputDialog, "getInt", lambda *a, **k: (40, True)) + novelBar._selectLastColumnSize() + assert novelTree.lastColSize == 40 # Open Items # ========== # Clear selection novelTree.clearSelection() - scItem = novelTree.topLevelItem(2) - scItem.setSelected(True) - assert scItem.isSelected() + assert novelView.getSelectedHandle() == (None, None) - # Clear selection with mouse - vPort = novelTree.viewport() - qtbot.mouseClick(vPort, QtMouseLeft, pos=vPort.rect().center(), delay=10) - assert not scItem.isSelected() + # Select scene + novelTree.setCurrentIndex(model.createIndex(2, 0)) + assert novelView.getSelectedHandle() == (C.hSceneDoc, "T0001") # Double-click item - scItem.setSelected(True) - assert scItem.isSelected() - assert nwGUI.docEditor.docHandle is None - novelTree._treeDoubleClick(scItem, 0) + novelTree._onDoubleClick(model.createIndex(2, 0)) assert nwGUI.docEditor.docHandle == C.hSceneDoc - # Open item with middle mouse button - scItem.setSelected(True) - assert scItem.isSelected() - assert nwGUI.docViewer.docHandle is None - qtbot.mouseClick(vPort, QtMouseMiddle, pos=vPort.rect().center(), delay=10) - assert nwGUI.docViewer.docHandle is None - - scRect = novelTree.visualItemRect(scItem) - oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE) - scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None) - qtbot.mouseClick(vPort, QtMouseMiddle, pos=scRect.center(), delay=10) - assert nwGUI.docViewer.docHandle is None - - scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData) - qtbot.mouseClick(vPort, QtMouseMiddle, pos=scRect.center(), delay=10) + # Middle-click item + novelTree._onMiddleClick(model.createIndex(2, 0)) assert nwGUI.docViewer.docHandle == C.hSceneDoc - # Last Column - # =========== - - novelBar.setLastColType(nwNovelExtra.HIDDEN) - assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True - assert novelTree.lastColType == nwNovelExtra.HIDDEN - assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ("", "") - - novelBar.setLastColType(nwNovelExtra.PLOT) - assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False - assert novelTree.lastColType == nwNovelExtra.PLOT - assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ( - "", "" - ) - - novelBar.setLastColType(nwNovelExtra.FOCUS) - assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False - assert novelTree.lastColType == nwNovelExtra.FOCUS - assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ( - "Jane", "Focus: Jane" - ) - - novelBar.setLastColType(nwNovelExtra.POV) - assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False - assert novelTree.lastColType == nwNovelExtra.POV - assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ( - "Jane", "Point of View: Jane" - ) - - novelTree._lastCol = None - assert novelTree._getLastColumnText("0000000000000", "T0000") == ("", "") - - # This forces the resizeEvent function to process labels - spSize = nwGUI.splitMain.sizes() - nwGUI.splitMain.setSizes([spSize[0] + 10, spSize[1] - 10]) - - # Resize the last column - with monkeypatch.context() as mp: - mp.setattr(QInputDialog, "getInt", lambda *a, **k: (40, True)) - novelBar._selectLastColumnSize() - # Item Meta # ========= - ttText = "" + toolTip = "" def showText(pos, text): - nonlocal ttText - ttText = text + nonlocal toolTip + toolTip = text - mIndex = novelTree.model().index(2, novelTree.C_MORE) with monkeypatch.context() as mp: mp.setattr(QToolTip, "showText", showText) - ttText = "" - novelTree._treeItemClicked(mIndex) - assert ttText == ( - "

Point of View: Jane
Focus: Jane

" - "

Synopsis: This is a scene.

" + toolTip = "" + novelTree._onSingleClick(model.createIndex(2, 3)) + assert toolTip == ( + "

Point of View: Jane
Focus: Jane

" + "

Synopsis: This is a scene.

" ) - ttText = "" + toolTip = "" novelTree._popMetaBox(QPoint(1, 1), C.hInvalid, "T0001") - assert ttText == "" + assert toolTip == "" - # Set Default Root - # ================ - SHARED.project.data.setLastHandle(C.hInvalid, "novel") - novelView.openProjectTasks() - assert novelBar.novelValue.handle == C.hNovelRoot + # Active Status + # ============= + assert novelBar._refresh == {C.hNovelRoot: False} - # Tree Focus - # ========== - with monkeypatch.context() as mp: - mp.setattr(GuiNovelTree, "hasFocus", lambda *a: False) - assert novelView.treeHasFocus() is False - mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True) - assert novelView.treeHasFocus() is True + # Add a document while tree in focus + nwGUI._changeView(nwView.PROJECT) + assert novelBar._active is False + nwGUI.projView.projTree.setSelectedHandle(C.hChapterDir) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3) + assert novelBar._refresh == {C.hNovelRoot: True} - # Other Checks - # ============ - - scItem = novelTree.topLevelItem(2) - scItem.setSelected(True) - assert scItem.isSelected() - novelTree.focusOutEvent(QFocusEvent(QEvent.Type.None_, Qt.FocusReason.MouseFocusReason)) - assert not scItem.isSelected() + # Switch back and check that the refresh status is reset + nwGUI._changeView(nwView.NOVEL) + assert novelBar._refresh == {C.hNovelRoot: False} # Close - # ===== - # qtbot.stop() nwGUI.closeProject() diff --git a/tests/tools.py b/tests/tools.py index f1414761..7695ae72 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -216,6 +216,7 @@ def buildTestProject(obj: object, projPath: Path) -> None: project.setProjectChanged(True) project.saveProject(autoSave=True) project._valid = True + project._tree._ready = True if nwGUI is not None: nwGUI.projView.openProjectTasks()