Add novel view test coverage
This commit is contained in:
@@ -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
|
||||
##
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {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"<p><b>{label}:</b> {synopsis}</p>"
|
||||
|
||||
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"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {tags}")
|
||||
return
|
||||
|
||||
lines = []
|
||||
if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
|
||||
tags = head.getReferences()
|
||||
|
||||
@@ -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 == (
|
||||
"<p><b>Point of View</b>: Jane<br><b>Focus</b>: Jane</p>"
|
||||
"<p><b>Synopsis</b>: This is a scene.</p>"
|
||||
toolTip = ""
|
||||
novelTree._onSingleClick(model.createIndex(2, 3))
|
||||
assert toolTip == (
|
||||
"<p><b>Point of View:</b> Jane<br><b>Focus:</b> Jane</p>"
|
||||
"<p><b>Synopsis:</b> This is a scene.</p>"
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user