- Rename project name and package: novelWriter -> UmbraEdit, novelwriter -> umbraedit - Rename entry script novelWriter.py -> UmbraEdit.py - Rename package directory novelwriter/ -> umbraedit/ - Rename i18n translation sources nw_*.ts -> ue_*.ts - Rename packaging files (appdata, project xml template) - Update all imports, references, docs, workflows, tests and build config - Update pyproject.toml project name, GUI script and URLs - Preserve full upstream git history; origin repointed below
1731 lines
64 KiB
Python
1731 lines
64 KiB
Python
"""
|
|
UmbraEdit - Main GUI Windows Tests
|
|
====================================
|
|
|
|
This file is a part of UmbraEdit.
|
|
|
|
Copyright (C) 2020 Veronica Berglyd Olsen and UmbraEdit contributors.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful, but
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
""" # noqa
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
|
|
from pathlib import Path
|
|
from shutil import copyfile
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, Mock
|
|
|
|
import pytest
|
|
|
|
from PyQt6.QtCore import QRect, QSize, Qt
|
|
from PyQt6.QtGui import QCloseEvent, QGuiApplication, QPalette, QStyleHints
|
|
from PyQt6.QtWidgets import QApplication, QFileDialog, QInputDialog, QWidget
|
|
from pytestqt.qtbot import QtBot
|
|
|
|
from umbraedit import CONFIG, SHARED, __hexversion__
|
|
from umbraedit.common import jsonEncode
|
|
from umbraedit.config import DEF_GUI_DARK, DEF_GUI_LIGHT
|
|
from umbraedit.constants import nwFiles
|
|
from umbraedit.core.project import NWProject
|
|
from umbraedit.dialogs.editlabel import GuiEditLabel
|
|
from umbraedit.dialogs.preferences import GuiNeedsUpdate
|
|
from umbraedit.editor.editor import GuiDocEditor
|
|
from umbraedit.enum import (
|
|
nwChange,
|
|
nwDocAction,
|
|
nwDocMode,
|
|
nwFocus,
|
|
nwItemClass,
|
|
nwItemType,
|
|
nwState,
|
|
nwTheme,
|
|
nwView,
|
|
nwVimMode,
|
|
)
|
|
from umbraedit.gui.noveltree import GuiNovelView
|
|
from umbraedit.gui.projtree import GuiProjectTree, GuiProjectView
|
|
from umbraedit.guimain import GuiMain
|
|
from umbraedit.manuscript.manuscript import GuiManuscript
|
|
from umbraedit.shared import _GuiAlert
|
|
from umbraedit.tools.welcome import GuiWelcome
|
|
from umbraedit.tools.writingstats import GuiWritingStats
|
|
from umbraedit.types import (
|
|
QtKeyBackspace,
|
|
QtKeyEnter,
|
|
QtKeyEscape,
|
|
QtKeyLeft,
|
|
QtKeyReturn,
|
|
QtKeyRight,
|
|
QtModCtrl,
|
|
QtModShift,
|
|
QtScrollAlwaysOff,
|
|
)
|
|
|
|
from tests.helpers import NWD_IGNORE, XML_IGNORE, C, MockRnd, TstPaths, buildTestProject, cmpFiles
|
|
from tests.mocked import causeOSError
|
|
|
|
KEY_DELAY = 1
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_ProjectBlocker(caplog: pytest.LogCaptureFixture, nwGUI: GuiMain):
|
|
"""Test the blocking of features when there's no project open."""
|
|
# Test no-project blocking
|
|
assert nwGUI.closeProject() is True
|
|
assert nwGUI.saveProject() is False
|
|
assert nwGUI.docPanes.openDocument(None) is False
|
|
|
|
caplog.clear()
|
|
nwGUI.docPanes.viewDocument(None)
|
|
assert "No project open" in caplog.text
|
|
assert nwGUI.docViewer.docHandle is None
|
|
|
|
caplog.clear()
|
|
nwGUI.docPanes.importDocument()
|
|
assert "No project open" in caplog.text
|
|
|
|
# These should all just do nothing and not raise
|
|
nwGUI.docPanes.closeDocument()
|
|
nwGUI.rebuildIndex()
|
|
nwGUI.openSelectedItem()
|
|
nwGUI.docPanes.openNextDocument(C.hSceneDoc, False, True)
|
|
nwGUI._autoSaveProject()
|
|
nwGUI._runGarbageCollect()
|
|
nwGUI.showProjectSettingsDialog()
|
|
nwGUI.showBuildManuscriptDialog()
|
|
nwGUI.showProjectWordListDialog()
|
|
nwGUI.showWritingStatsDialog()
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_Launch(
|
|
qtbot: QtBot,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
fncPath: Path,
|
|
):
|
|
"""Test the handling of launch tasks."""
|
|
monkeypatch.setattr(GuiWelcome, "exec", lambda *a: None)
|
|
CONFIG.lastNotes = "0x0"
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
|
|
# Open Lipsum project
|
|
nwGUI.postLaunchTasks(str(projPath))
|
|
assert SHARED.hasProject is True
|
|
nwGUI.closeProject()
|
|
assert SHARED.hasProject is False
|
|
|
|
# Launch tasks still run fine without a QApplication instance to connect to
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(QApplication, "instance", lambda: None)
|
|
nwGUI.postLaunchTasks(None)
|
|
|
|
# Open as if called from Welcome
|
|
nwGUI._openProjectFromWelcome(projPath)
|
|
assert SHARED.hasProject is True
|
|
nwGUI.closeProject()
|
|
assert SHARED.hasProject is False
|
|
|
|
# Open as if called from Welcome, invalid path
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI, "showWelcomeDialog", lambda *a: None)
|
|
nwGUI._openProjectFromWelcome(None) # type: ignore
|
|
assert SHARED.hasProject is False
|
|
|
|
# Project open fails
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(SHARED, "openProject", lambda *a: False)
|
|
assert nwGUI.openProject(projPath) is False
|
|
|
|
# Handle locked project
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(SHARED, "openProject", lambda *a, **k: False)
|
|
SHARED._lockedBy = ["a", "b", "c", "d"]
|
|
assert nwGUI.openProject(projPath) is False
|
|
SHARED._lockedBy = None
|
|
|
|
assert nwGUI.openProject(projPath) is True
|
|
nwGUI.closeProject()
|
|
|
|
# Opening a project with no documents at all leaves the editor and
|
|
# viewer empty
|
|
emptyPath = fncPath / "empty"
|
|
emptyPath.mkdir()
|
|
project = SHARED.project
|
|
project.storage.createNewProject(emptyPath)
|
|
project.setDefaultStatusImport()
|
|
project.data.setName("Empty")
|
|
project.newRoot(nwItemClass.NOVEL)
|
|
project.session.startSession()
|
|
project.saveProject()
|
|
project.closeProject(0.0)
|
|
|
|
assert nwGUI.openProject(emptyPath) is True
|
|
assert nwGUI.docEditor.docHandle is None
|
|
assert nwGUI.docViewer.docHandle is None
|
|
nwGUI.closeProject()
|
|
|
|
# A broken index caused by a version upgrade does not show the
|
|
# "index is broken" warning, but still rebuilds the index
|
|
assert nwGUI.openProject(projPath) is True
|
|
nwGUI.closeProject()
|
|
|
|
warnMock = Mock()
|
|
rebuildMock = Mock()
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(type(SHARED.project.index), "indexRebuild", property(lambda self: True))
|
|
mp.setattr(type(SHARED.project.index), "indexUpgrade", property(lambda self: True))
|
|
mp.setattr(SHARED, "warn", warnMock)
|
|
mp.setattr(nwGUI, "rebuildIndex", rebuildMock)
|
|
assert nwGUI.openProject(projPath) is True
|
|
|
|
assert not any("index is broken" in str(call) for call in warnMock.call_args_list)
|
|
assert rebuildMock.called is True
|
|
nwGUI.closeProject()
|
|
|
|
# Check that closes can be blocked
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(_GuiAlert, "finalState", False)
|
|
assert nwGUI.openProject(projPath) is True
|
|
assert nwGUI.closeMain() is False
|
|
nwGUI.closeProject()
|
|
|
|
# Check that latest release info is set
|
|
assert CONFIG.lastNotes != "0x0"
|
|
|
|
# Set some config error
|
|
CONFIG._hasError = True
|
|
CONFIG._errData.append("Foo")
|
|
|
|
# Check that project open dialog launches
|
|
nwGUI.postLaunchTasks(None)
|
|
qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(GuiWelcome) is not None, timeout=1000)
|
|
welcome = SHARED.findTopLevelWidget(GuiWelcome)
|
|
assert isinstance(welcome, GuiWelcome)
|
|
welcome.show()
|
|
welcome.close()
|
|
|
|
# Config errors should be cleared
|
|
assert SHARED.lastAlert == ["Foo"]
|
|
assert CONFIG._hasError is False
|
|
|
|
# The welcome dialog isn't repositioned when moveMainWin is disabled
|
|
CONFIG.moveMainWin = False
|
|
nwGUI.showWelcomeDialog()
|
|
|
|
# Window sizing tolerates an invalid size list, and skips moving the
|
|
# window when the size already matches the available screen
|
|
nwGUI._setWindowSize([100]) # Wrong length, should not raise
|
|
|
|
mockScreen = MagicMock()
|
|
mockScreen.availableSize.return_value = QSize(1000, 700)
|
|
mockScreen.geometry.return_value = QRect(0, 0, 1000, 700)
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(type(SHARED), "mainScreen", property(lambda self: mockScreen))
|
|
CONFIG.moveMainWin = False
|
|
nwGUI._setWindowSize([1000, 700])
|
|
|
|
# qtbot.stop()
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_IndexRebuild(qtbot: QtBot, monkeypatch: pytest.MonkeyPatch, nwGUI: GuiMain, projPath: Path):
|
|
"""Test that a genuinely invalid index cache, as opposed to one
|
|
from a version upgrade, triggers the "index is broken" warning as
|
|
well as a rebuild of the index.
|
|
"""
|
|
buildTestProject(NWProject(), projPath)
|
|
indexFile = projPath / "meta" / nwFiles.INDEX_FILE
|
|
assert indexFile.is_file()
|
|
|
|
# Corrupt the cached index file on disk
|
|
indexFile.write_text("{not valid json", encoding="utf-8")
|
|
|
|
warnMock = Mock()
|
|
rebuildMock = Mock()
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(SHARED, "warn", warnMock)
|
|
mp.setattr(nwGUI, "rebuildIndex", rebuildMock)
|
|
assert nwGUI.openProject(projPath) is True
|
|
|
|
assert SHARED.project.index.indexUpgrade is False
|
|
assert any("index is broken" in str(call) for call in warnMock.call_args_list)
|
|
assert rebuildMock.called is True
|
|
nwGUI.closeProject()
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_ProjectTreeItems(
|
|
qtbot: QtBot,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
mockRnd: MockRnd,
|
|
):
|
|
"""Test handling of project tree items based on GUI focus states."""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
nwGUI.rebuildIndex() # A project built off-GUI has no index cache to load
|
|
nwGUI.docPanes.closeDocument() # Opening the project auto-restores a document
|
|
|
|
sHandle = "000000000000f"
|
|
nwGUI.openSelectedItem()
|
|
assert nwGUI.docEditor.docHandle is None
|
|
|
|
# Project Tree has focus
|
|
nwGUI.changeView(nwView.PROJECT)
|
|
nwGUI._switchFocus(nwFocus.TREE)
|
|
nwGUI.projStack.setCurrentIndex(0)
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
|
mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: False)
|
|
assert nwGUI.docEditor.docHandle is None
|
|
nwGUI.projView.projTree.setSelectedHandle(sHandle)
|
|
nwGUI.projView.projTree.openSelectedItem()
|
|
assert nwGUI.docEditor.docHandle == sHandle
|
|
nwGUI.docPanes.closeDocument()
|
|
|
|
# A selected item that isn't a file is ignored
|
|
nwGUI.projView.projTree.setSelectedHandle(C.hChapterDir)
|
|
nwGUI.openSelectedItem()
|
|
assert nwGUI.docEditor.docHandle is None
|
|
|
|
# Novel Tree has focus
|
|
nwGUI.changeView(nwView.NOVEL)
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(GuiProjectView, "treeHasFocus", lambda *a: False)
|
|
mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True)
|
|
assert nwGUI.docEditor.docHandle is None
|
|
|
|
model = nwGUI.novelView.novelTree._getModel()
|
|
assert model is not None
|
|
|
|
# No selection is ignored
|
|
nwGUI.novelView.novelTree.setCurrentIndex(model.createIndex(9999, 0))
|
|
nwGUI.novelView.novelTree.openSelectedItem()
|
|
assert nwGUI.docEditor.docHandle is None
|
|
|
|
nwGUI.novelView.novelTree.setCurrentIndex(model.createIndex(2, 0))
|
|
nwGUI.novelView.novelTree.openSelectedItem()
|
|
assert nwGUI.docEditor.docHandle == sHandle
|
|
nwGUI.docPanes.closeDocument()
|
|
|
|
# The main GUI dispatcher also routes to the novel tree
|
|
nwGUI.openSelectedItem()
|
|
assert nwGUI.docEditor.docHandle == sHandle
|
|
nwGUI.docPanes.closeDocument()
|
|
|
|
# ... and to viewing the document when Shift is held
|
|
with monkeypatch.context() as mpShift:
|
|
mpShift.setattr(QApplication, "keyboardModifiers", lambda: QtModShift)
|
|
nwGUI.openSelectedItem()
|
|
assert nwGUI.docViewer.docHandle == sHandle
|
|
nwGUI.docPanes.closeDocViewer()
|
|
|
|
# Internal open-document slots ignore a missing or invalid handle
|
|
nwGUI.docPanes.openDocumentInMode("", nwDocMode.EDIT, "", True)
|
|
assert nwGUI.docEditor.docHandle is None
|
|
|
|
nwGUI.docPanes.openDocumentSelection(C.hInvalid, 0, 3, True)
|
|
assert nwGUI.docEditor.docHandle is None
|
|
|
|
# qtbot.stop()
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_UpdateTheme(qtbot: QtBot, nwGUI: GuiMain):
|
|
"""Test updating the theme in the GUI."""
|
|
theme = SHARED.theme
|
|
CONFIG.themeMode = nwTheme.DARK
|
|
CONFIG.darkTheme = DEF_GUI_DARK
|
|
CONFIG.lightTheme = DEF_GUI_LIGHT
|
|
theme.loadTheme()
|
|
assert theme.isDarkTheme is True
|
|
|
|
nwGUI._processConfigChanges(GuiNeedsUpdate(False, True, False, False, False, False, False, False, False))
|
|
nwGUI._processConfigChanges(GuiNeedsUpdate(True, True, True, True, True, True, True, True, True))
|
|
|
|
# Check editor syntax
|
|
syntax = SHARED.theme.syntaxTheme
|
|
assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntax.back
|
|
assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back
|
|
assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntax.back
|
|
assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back
|
|
|
|
# Update by check
|
|
CONFIG.themeMode = nwTheme.LIGHT
|
|
nwGUI.checkThemeUpdate()
|
|
assert theme.isDarkTheme is False
|
|
|
|
# Checking again without any further change does nothing
|
|
nwGUI.checkThemeUpdate()
|
|
assert theme.isDarkTheme is False
|
|
|
|
# qtbot.stop()
|
|
|
|
|
|
@pytest.mark.gui
|
|
@pytest.mark.skipif(not CONFIG.checkMinQtVersion(0x060500), reason="Requires Qt 6.5+")
|
|
def testGuiMain_OSThemeChangeAndClose(qtbot: QtBot, monkeypatch: pytest.MonkeyPatch, nwGUI: GuiMain, projPath: Path):
|
|
"""Test that the OS colour scheme change signal reaches the theme
|
|
loader via the connected slot, that isDesktopDarkMode falls back to
|
|
the palette check when there is no styleHints object, and that the
|
|
close event handles both a blocked close and a missing theme hints
|
|
object.
|
|
"""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
|
|
CONFIG.themeMode = nwTheme.AUTO
|
|
CONFIG.darkTheme = DEF_GUI_DARK
|
|
CONFIG.lightTheme = DEF_GUI_LIGHT
|
|
|
|
# Emitting the real OS signal reaches the connected slot, which
|
|
# feeds the explicit colour scheme through to the theme loader. The
|
|
# desktop hint is mocked to agree, since checkThemeUpdate's own
|
|
# refreshThemeColors() call reloads the theme again without the
|
|
# explicit scheme, falling back to the live desktop hint.
|
|
assert nwGUI._themeHints is not None
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark)
|
|
nwGUI._themeHints.colorSchemeChanged.emit(Qt.ColorScheme.Dark)
|
|
assert SHARED.theme.isDarkTheme is True
|
|
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light)
|
|
nwGUI._themeHints.colorSchemeChanged.emit(Qt.ColorScheme.Light)
|
|
assert SHARED.theme.isDarkTheme is False
|
|
|
|
# With no styleHints object at all, isDesktopDarkMode falls back to
|
|
# the palette check instead of raising
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(QGuiApplication, "styleHints", staticmethod(lambda: None))
|
|
SHARED.theme.isDesktopDarkMode()
|
|
|
|
# A blocked close ignores the event and leaves the hints connected
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(_GuiAlert, "finalState", False)
|
|
event = QCloseEvent()
|
|
nwGUI.closeEvent(event)
|
|
assert event.isAccepted() is False
|
|
|
|
# The theme hint object must be explicitly disconnected, since it is
|
|
# connected to a singleton and will therefore bleed over into other tests
|
|
nwGUI._themeHints.colorSchemeChanged.disconnect(nwGUI._themeChangedSlot)
|
|
nwGUI._themeHints = None
|
|
event = QCloseEvent()
|
|
nwGUI.closeEvent(event)
|
|
assert event.isAccepted() is True
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_ProcessConfigChanges_LightweightSettings(
|
|
qtbot: QtBot,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
mockRnd: MockRnd,
|
|
):
|
|
"""Test the lightweight settings path of _processConfigChanges, which
|
|
must refresh the document headers on every call, but must only reset
|
|
Vim mode when the Vim setting itself changed (issue: an unrelated
|
|
preferences save must not silently drop the user out of Insert mode).
|
|
"""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
assert nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
assert nwGUI.docViewer.docHandle == C.hSceneDoc
|
|
|
|
docEditor = nwGUI.docEditor
|
|
docViewer = nwGUI.docViewer
|
|
|
|
CONFIG.vimMode = True
|
|
docEditor.initSettings(updateVimMode=True)
|
|
vimMode = docEditor._vimMode
|
|
assert vimMode is not None
|
|
vimMode._vim.setMode(nwVimMode.INSERT)
|
|
|
|
# Clear the headers so we can tell if they were refreshed
|
|
docEditor.docHeader._docHandle = None
|
|
docViewer.docHeader._docHandle = None
|
|
|
|
# A save where nothing relevant changed, and Vim was not touched,
|
|
# must still refresh the headers, but must not reset Vim mode
|
|
noChange = GuiNeedsUpdate(
|
|
restart=False,
|
|
tree=False,
|
|
theme=False,
|
|
syntax=False,
|
|
spelling=False,
|
|
vim=False,
|
|
editor=False,
|
|
viewer=False,
|
|
viewport=False,
|
|
)
|
|
nwGUI._processConfigChanges(noChange)
|
|
assert docEditor.docHeader._docHandle == C.hSceneDoc
|
|
assert docViewer.docHeader._docHandle == C.hSceneDoc
|
|
assert vimMode._vim.mode == nwVimMode.INSERT
|
|
|
|
# A save where the Vim setting changed must reset the mode to Normal
|
|
vimChanged = GuiNeedsUpdate(
|
|
restart=False,
|
|
tree=False,
|
|
theme=False,
|
|
syntax=False,
|
|
spelling=False,
|
|
vim=True,
|
|
editor=False,
|
|
viewer=False,
|
|
viewport=False,
|
|
)
|
|
nwGUI._processConfigChanges(vimChanged)
|
|
assert vimMode._vim.mode == nwVimMode.NORMAL
|
|
|
|
# A save that fully reinitialises the editor and viewer must not
|
|
# additionally run the lightweight path
|
|
vimMode._vim.setMode(nwVimMode.INSERT)
|
|
fullReinit = GuiNeedsUpdate(
|
|
restart=False,
|
|
tree=False,
|
|
theme=False,
|
|
syntax=False,
|
|
spelling=False,
|
|
vim=False,
|
|
editor=True,
|
|
viewer=True,
|
|
viewport=False,
|
|
)
|
|
nwGUI._processConfigChanges(fullReinit)
|
|
assert vimMode._vim.mode == nwVimMode.NORMAL
|
|
|
|
# A viewport-only change must reinitialise the viewport on both the
|
|
# editor and viewer even though neither is otherwise reinitialised
|
|
CONFIG.hideVScroll = True
|
|
CONFIG.hideHScroll = True
|
|
viewportOnly = GuiNeedsUpdate(
|
|
restart=False,
|
|
tree=False,
|
|
theme=False,
|
|
syntax=False,
|
|
spelling=False,
|
|
vim=False,
|
|
editor=False,
|
|
viewer=False,
|
|
viewport=True,
|
|
)
|
|
nwGUI._processConfigChanges(viewportOnly)
|
|
assert docEditor.verticalScrollBarPolicy() == QtScrollAlwaysOff
|
|
assert docEditor.horizontalScrollBarPolicy() == QtScrollAlwaysOff
|
|
assert docViewer.verticalScrollBarPolicy() == QtScrollAlwaysOff
|
|
assert docViewer.horizontalScrollBarPolicy() == QtScrollAlwaysOff
|
|
|
|
# qtbot.stop()
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_Editing(
|
|
qtbot: QtBot,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
tstPaths: TstPaths,
|
|
mockRnd: MockRnd,
|
|
):
|
|
"""Test the document editor."""
|
|
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
|
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True)
|
|
monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True))
|
|
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
|
|
|
# Create new, save, close project
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
assert nwGUI.saveProject()
|
|
assert nwGUI.closeProject()
|
|
|
|
assert len(SHARED.project.tree) == 0
|
|
assert len(SHARED.project.tree._items) == 0
|
|
assert len(SHARED.project.tree._nodes) == 0
|
|
assert SHARED.project.data.name == ""
|
|
assert SHARED.project.data.author == ""
|
|
assert SHARED.project.data.spellCheck is False
|
|
|
|
# Check the files
|
|
projFile = projPath / "nwProject.nwx"
|
|
testFile = tstPaths.outDir / "guiEditor_Main_Initial_nwProject.nwx"
|
|
compFile = tstPaths.refDir / "guiEditor_Main_Initial_nwProject.nwx"
|
|
copyfile(projFile, testFile)
|
|
assert cmpFiles(testFile, compFile, ignStart=XML_IGNORE)
|
|
|
|
# Re-open project
|
|
assert nwGUI.openProject(projPath)
|
|
|
|
# Check that we loaded the data
|
|
assert len(SHARED.project.tree) == 9
|
|
assert SHARED.project.tree.model.root.childCount() == 5
|
|
assert SHARED.project.tree.trash is not None # Created automatically
|
|
assert SHARED.project.data.name == "New Project"
|
|
assert SHARED.project.data.author == "Jane Doe"
|
|
assert SHARED.project.data.spellCheck is False
|
|
|
|
# Check that tree items have been created
|
|
assert SHARED.project.tree[C.hNovelRoot] is not None
|
|
assert SHARED.project.tree[C.hPlotRoot] is not None
|
|
assert SHARED.project.tree[C.hCharRoot] is not None
|
|
assert SHARED.project.tree[C.hWorldRoot] is not None
|
|
assert SHARED.project.tree[C.hTitlePage] is not None
|
|
assert SHARED.project.tree[C.hChapterDir] is not None
|
|
assert SHARED.project.tree[C.hChapterDoc] is not None
|
|
assert SHARED.project.tree[C.hSceneDoc] is not None
|
|
|
|
nwGUI.mainMenu.aSpellCheck.setChecked(True)
|
|
nwGUI.mainMenu._toggleSpellCheck()
|
|
|
|
# Change some settings
|
|
CONFIG.hideHScroll = True
|
|
CONFIG.hideVScroll = True
|
|
CONFIG.autoScroll = False
|
|
|
|
# Add a Character File
|
|
nwGUI.changeView(nwView.PROJECT)
|
|
nwGUI.projView.projTree.expandAll()
|
|
nwGUI.projView.projTree.clearSelection()
|
|
nwGUI.projView.projTree.setSelectedHandle(C.hCharRoot)
|
|
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
|
nwGUI.projView.projTree.expandAll()
|
|
nwGUI.openSelectedItem()
|
|
|
|
# Text Editor
|
|
# ===========
|
|
|
|
# Syntax Highlighting
|
|
CONFIG.dialogStyle = 3
|
|
CONFIG.dialogLine = "\u2013"
|
|
CONFIG.altDialogOpen = "<|"
|
|
CONFIG.altDialogClose = "|>"
|
|
|
|
docEditor = nwGUI.docEditor
|
|
docEditor._qDocument.syntaxHighlighter.initHighlighter()
|
|
|
|
# Type something into the document
|
|
nwGUI.docEditor.setFocus()
|
|
qtbot.keyClick(docEditor, "a", modifier=QtModCtrl, delay=KEY_DELAY)
|
|
for c in "# Jane Doe":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "@tag: Jane":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "This is a file about Jane.":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
# Add a Plot File
|
|
nwGUI.projView.projTree.expandAll()
|
|
nwGUI.projView.projTree.clearSelection()
|
|
nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot)
|
|
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
|
nwGUI.projView.projTree.expandAll()
|
|
nwGUI.openSelectedItem()
|
|
|
|
# Type something into the document
|
|
nwGUI.docEditor.setFocus()
|
|
qtbot.keyClick(docEditor, "a", modifier=QtModCtrl, delay=KEY_DELAY)
|
|
for c in "# Main Plot":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "@tag: MainPlot":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "This is a file [i]detailing[/i] the main plot.":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
# Add a World File
|
|
nwGUI.projView.projTree.expandAll()
|
|
nwGUI.projView.projTree.clearSelection()
|
|
nwGUI.projView.projTree.setSelectedHandle(C.hWorldRoot)
|
|
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
|
nwGUI.projView.projTree.expandAll()
|
|
nwGUI.openSelectedItem()
|
|
|
|
# Add Some Text
|
|
docEditor.replaceText("Hello World!")
|
|
assert docEditor.getText() == "Hello World!"
|
|
docEditor.replaceText("")
|
|
|
|
# Type something into the document
|
|
nwGUI.docEditor.setFocus()
|
|
qtbot.keyClick(docEditor, "a", modifier=QtModCtrl, delay=KEY_DELAY)
|
|
for c in "# Main Location":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "@tag: Home":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "This is a file describing Jane's home.":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
# Trigger autosaves before making more changes
|
|
nwGUI._autoSaveDocument()
|
|
nwGUI._autoSaveProject()
|
|
|
|
# Select the 'New Scene' file
|
|
nwGUI.projView.projTree.expandAll()
|
|
nwGUI.projView.projTree.clearSelection()
|
|
nwGUI.projView.projTree.setSelectedHandle(C.hSceneDoc)
|
|
nwGUI.openSelectedItem()
|
|
|
|
# Type something into the document
|
|
nwGUI.docEditor.setFocus()
|
|
qtbot.keyClick(docEditor, "a", modifier=QtModCtrl, delay=KEY_DELAY)
|
|
for c in "# Novel":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "## Chapter":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "@pov: Jane":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "@plot: MainPlot":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "### Scene":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "% How about a comment?":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "@pov: Jane":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "@plot: MainPlot":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
for c in "@location: Home":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "#### Some Section":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "@char: Jane":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "This is a paragraph of nonsense text.":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
# Don't allow Shift+Enter to insert a line separator (issue #1150)
|
|
for c in "This is another paragraph":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyEnter, modifier=QtModShift, delay=KEY_DELAY)
|
|
for c in "with a line separator in it.":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
# Auto-Replace
|
|
# ============
|
|
|
|
for c in (
|
|
"This is another paragraph of much longer nonsense text. It is in fact 1 very very NONSENSICAL nonsense text! "
|
|
):
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
for c in "We can also try replacing \"quotes\", even single 'quotes' are replaced. ":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
for c in "Isn't that nice? ":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
for c in "We can even go on to a ---- hotizontal bar. ":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
for c in "Ellipsis? Not a problem either ... ":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
for c in "How about three hyphens - -":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyLeft, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyBackspace, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyRight, delay=KEY_DELAY)
|
|
for c in "- for long dash? It works too. ":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
for c in "Even four hyphens - - -":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyLeft, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyBackspace, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyLeft, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyBackspace, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyRight, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyRight, delay=KEY_DELAY)
|
|
for c in "- for a horizontal works!":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
# Dialogue
|
|
# ========
|
|
|
|
for c in '"Full line double quoted text."':
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "'Full line single quoted text.'":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
# Insert spaces before and after quotes
|
|
CONFIG.fmtPadBefore = "\u201d"
|
|
CONFIG.fmtPadAfter = "\u201c"
|
|
docEditor.initEditor()
|
|
|
|
for c in 'Some "double quoted text with spaces padded". Ok?':
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
|
|
# Check that we can insert a line break in the text without
|
|
# triggering the padding (Issue #2586)
|
|
qtbot.keyClick(docEditor, QtKeyLeft, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyLeft, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyLeft, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyRight, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyRight, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyRight, delay=KEY_DELAY)
|
|
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
CONFIG.fmtPadBefore = ""
|
|
CONFIG.fmtPadAfter = ""
|
|
docEditor.initEditor()
|
|
|
|
# Dialogue Line
|
|
for c in "-- Hi, I am a character speaking.":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
# Narrator Break
|
|
CONFIG.narratorBreak = "\u2013"
|
|
docEditor = nwGUI.docEditor
|
|
docEditor._qDocument.syntaxHighlighter.initHighlighter()
|
|
|
|
for c in "-- Hi, I am also a character speaking, -- said another character. -- How are you?":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
# Special Formatting
|
|
# ==================
|
|
|
|
# Insert spaces before colon, but ignore tags
|
|
CONFIG.fmtPadBefore = ":"
|
|
docEditor.initEditor()
|
|
|
|
for c in "@object: NoSpaceAdded":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "% synopsis: Space before this is OK.":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "%Footnote.abc: A simple footnote.":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "Add space before this colon: See?":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "But don't add a double space : See?":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
CONFIG.fmtPadBefore = ""
|
|
docEditor.initEditor()
|
|
|
|
# Indent and Align
|
|
# ================
|
|
|
|
nwGUI._switchFocus(nwFocus.DOCUMENT)
|
|
for c in '\t"Tab-indented text"':
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in '>"Paragraph-indented text"':
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in '>>"Right-aligned text"':
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in "\t'Tab-indented text'":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in ">'Paragraph-indented text'":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
for c in ">>'Right-aligned text'":
|
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
qtbot.keyClick(docEditor, QtKeyReturn, delay=KEY_DELAY)
|
|
|
|
docEditor._updateDocCounts(0, 0, 0)
|
|
|
|
# Check Files
|
|
# ===========
|
|
|
|
# Save the document
|
|
assert docEditor.docChanged
|
|
nwGUI.docPanes.saveDocument()
|
|
assert docEditor.docChanged is False
|
|
nwGUI.docPanes.forceSaveDocument()
|
|
assert docEditor.docChanged is False
|
|
nwGUI.rebuildIndex()
|
|
|
|
# Open and view the edited document
|
|
nwGUI.docViewer.setFocus()
|
|
assert nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
assert nwGUI.docViewer.docHandle == C.hSceneDoc
|
|
assert nwGUI.saveProject()
|
|
nwGUI.docPanes.closeViewerPanel()
|
|
assert nwGUI.docPanes.splitView.isVisible() is False
|
|
|
|
# Check the files
|
|
projFile = projPath / "nwProject.nwx"
|
|
testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx"
|
|
compFile = tstPaths.refDir / "guiEditor_Main_Final_nwProject.nwx"
|
|
copyfile(projFile, testFile)
|
|
assert cmpFiles(testFile, compFile, ignStart=(*XML_IGNORE, "<spellCheck"))
|
|
|
|
projFile = projPath / "content" / "000000000000f.md"
|
|
testFile = tstPaths.outDir / "guiEditor_Main_Final_000000000000f.md"
|
|
compFile = tstPaths.refDir / "guiEditor_Main_Final_000000000000f.md"
|
|
copyfile(projFile, testFile)
|
|
assert cmpFiles(testFile, compFile, ignStart=NWD_IGNORE)
|
|
|
|
projFile = projPath / "content" / "0000000000011.md"
|
|
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000011.md"
|
|
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000011.md"
|
|
copyfile(projFile, testFile)
|
|
assert cmpFiles(testFile, compFile, ignStart=NWD_IGNORE)
|
|
|
|
projFile = projPath / "content" / "0000000000012.md"
|
|
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000012.md"
|
|
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000012.md"
|
|
copyfile(projFile, testFile)
|
|
assert cmpFiles(testFile, compFile, ignStart=NWD_IGNORE)
|
|
|
|
projFile = projPath / "content" / "0000000000013.md"
|
|
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000013.md"
|
|
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000013.md"
|
|
copyfile(projFile, testFile)
|
|
assert cmpFiles(testFile, compFile, ignStart=NWD_IGNORE)
|
|
|
|
# qtbot.stop()
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_Viewing(
|
|
qtbot: QtBot,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
mockRnd: MockRnd,
|
|
):
|
|
"""Test the document viewer."""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
nwGUI.closeProject()
|
|
|
|
# View before a project is open does nothing
|
|
assert nwGUI.docPanes.splitView.isVisible() is False
|
|
nwGUI.docPanes.viewDocument(None)
|
|
assert nwGUI.docPanes.splitView.isVisible() is False
|
|
|
|
# Open project requires a path
|
|
assert nwGUI.openProject(None) is False
|
|
assert SHARED.hasProject is False
|
|
|
|
# Open the test project, properly
|
|
nwGUI.openProject(projPath)
|
|
assert nwGUI.docEditor.docHandle == C.hTitlePage
|
|
|
|
# If editor has focus, open that document
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: True)
|
|
nwGUI.docPanes.viewDocument(None)
|
|
assert nwGUI.docViewer.docHandle == C.hTitlePage
|
|
nwGUI.docPanes.closeDocViewer()
|
|
|
|
# If editor does not have focus, open selected handle
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
|
|
nwGUI.projView.projTree.setSelectedHandle(C.hSceneDoc)
|
|
nwGUI.docPanes.viewDocument(None)
|
|
assert nwGUI.docViewer.docHandle == C.hSceneDoc
|
|
|
|
# If there is no selection, get last selected
|
|
SHARED.project.data.setLastHandle(C.hChapterDoc, "viewer")
|
|
assert SHARED.project.data.getLastHandle("viewer") == C.hChapterDoc
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
|
|
nwGUI.projView.projTree.clearSelection()
|
|
nwGUI.docPanes.viewDocument(None)
|
|
assert nwGUI.docViewer.docHandle == C.hChapterDoc
|
|
|
|
# If all fails, don't open anything
|
|
nwGUI.docPanes.closeDocViewer()
|
|
assert SHARED.project.data.getLastHandle("viewer") is None
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
|
|
nwGUI.projView.projTree.clearSelection()
|
|
nwGUI.docPanes.viewDocument(None)
|
|
assert nwGUI.docViewer.docHandle is None
|
|
|
|
# If editor doc was edited and requested for the viewer, save it first
|
|
nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
nwGUI.docEditor.setPlainText("### New Scene\n\nWith some stuff in it!\n\n")
|
|
assert nwGUI.docEditor.docChanged is True
|
|
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
assert nwGUI.docViewer.toPlainText() == "New Scene\nWith some stuff in it!"
|
|
|
|
# Open with keypress
|
|
nwGUI.docPanes.closeDocViewer()
|
|
assert nwGUI.docViewer.docHandle is None
|
|
nwGUI.projView.setSelectedHandle(C.hSceneDoc)
|
|
nwGUI.projView.projTree.setFocus()
|
|
qtbot.keyClick(nwGUI.projView.projTree, QtKeyReturn, modifier=QtModShift, delay=KEY_DELAY)
|
|
assert nwGUI.docViewer.docHandle == C.hSceneDoc
|
|
|
|
# If the viewer fails to load the document, nothing else happens
|
|
nwGUI.docPanes.closeDocViewer()
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docViewer, "loadText", lambda *a, **k: False)
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
assert nwGUI.docPanes.splitView.isVisible() is False
|
|
|
|
# If the editor's cursor isn't visible when the viewer panel first
|
|
# appears, its visibility isn't restored either
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "cursorIsVisible", lambda: False)
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
assert nwGUI.docPanes.splitView.isVisible() is True
|
|
|
|
# The viewer opens at half the width
|
|
edWidth, vwWidth = nwGUI.docPanes.splitMain.sizes()
|
|
assert vwWidth > 0
|
|
assert abs(edWidth - vwWidth) <= 1
|
|
|
|
# Closing the viewer panel doesn't restore cursor visibility either,
|
|
# if the cursor wasn't visible to begin with
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "cursorIsVisible", lambda: False)
|
|
nwGUI.docPanes.closeViewerPanel()
|
|
|
|
# The editor takes back the full width
|
|
assert nwGUI.docPanes.splitMain.sizes()[1] == 0
|
|
|
|
# Reopening the viewer always restores the even split
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
nwGUI.docPanes.splitMain.setSizes([3, 1])
|
|
nwGUI.docPanes.closeViewerPanel()
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
edWidth, vwWidth = nwGUI.docPanes.splitMain.sizes()
|
|
assert abs(edWidth - vwWidth) <= 1
|
|
nwGUI.docPanes.closeViewerPanel()
|
|
assert nwGUI.docPanes.splitView.isVisible() is False
|
|
|
|
# Reloading the viewer only saves the editor first if the two
|
|
# panels show the same document
|
|
nwGUI.docPanes.openDocument(C.hChapterDoc)
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
assert nwGUI.docEditor.docHandle != nwGUI.docViewer.docHandle
|
|
nwGUI.docPanes._reloadViewer() # Should not raise, and skip saving the editor
|
|
|
|
# qtbot.stop()
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_FocusMode(
|
|
qtbot: QtBot,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
mockRnd: MockRnd,
|
|
):
|
|
"""Test focus mode in the main window."""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
nwGUI.docPanes.closeDocument() # Opening the project auto-restores a document
|
|
|
|
assert SHARED.focusMode is False
|
|
|
|
# Header heights don't change when a document is opened
|
|
editHeight = nwGUI.docEditor.docHeader.sizeHint().height()
|
|
viewHeight = nwGUI.docViewer.docHeader.sizeHint().height()
|
|
|
|
# No document open, so not allowing focus mode
|
|
nwGUI.toggleFocusMode()
|
|
assert nwGUI.treePane.isVisible() is True
|
|
assert nwGUI.mainStatus.isVisible() is True
|
|
assert nwGUI.mainMenu.isVisible() is True
|
|
assert nwGUI.sideBar.isVisible() is True
|
|
|
|
# Open a file only in the editor, with the viewer untouched and the
|
|
# cursor hidden: toggling focus mode doesn't touch the viewer panel
|
|
# or try to restore cursor visibility
|
|
assert nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
assert nwGUI.docViewer.docHandle is None
|
|
assert nwGUI.docPanes.splitView.isVisible() is False
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "cursorIsVisible", lambda: False)
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is True
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is False
|
|
|
|
# Open a file in editor and viewer
|
|
assert nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
assert nwGUI.docViewer.docHandle == C.hSceneDoc
|
|
assert nwGUI.docEditor.docHeader.sizeHint().height() == editHeight
|
|
assert nwGUI.docViewer.docHeader.sizeHint().height() == viewHeight
|
|
|
|
# Enable focus mode
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is True
|
|
assert nwGUI.treePane.isVisible() is False
|
|
assert nwGUI.mainStatus.isVisible() is False
|
|
assert nwGUI.mainMenu.isVisible() is False
|
|
assert nwGUI.sideBar.isVisible() is False
|
|
assert nwGUI.docPanes.splitView.isVisible() is False
|
|
|
|
# Disable focus mode
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is False
|
|
assert nwGUI.treePane.isVisible() is True
|
|
assert nwGUI.mainStatus.isVisible() is True
|
|
assert nwGUI.mainMenu.isVisible() is True
|
|
assert nwGUI.sideBar.isVisible() is True
|
|
assert nwGUI.docPanes.splitView.isVisible() is True
|
|
|
|
# Clean header hides most header content in focus mode
|
|
docHeader = nwGUI.docEditor.docHeader
|
|
CONFIG.cleanFocusHeader = True
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is True
|
|
assert docHeader.tbButton.isVisible() is True
|
|
assert docHeader.outlineButton.isVisible() is False
|
|
assert docHeader.searchButton.isVisible() is False
|
|
assert docHeader.itemTitle.isVisible() is False
|
|
assert docHeader.minmaxButton.isVisible() is True
|
|
assert docHeader.closeButton.isVisible() is False
|
|
|
|
# Item updates, like after a save, don't restore the header content
|
|
nwGUI.docEditor.onProjectItemChanged(C.hSceneDoc, nwChange.UPDATE)
|
|
assert docHeader.outlineButton.isVisible() is False
|
|
assert docHeader.searchButton.isVisible() is False
|
|
assert docHeader.itemTitle.isVisible() is False
|
|
assert docHeader.closeButton.isVisible() is False
|
|
|
|
# Leaving focus mode restores the header content
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is False
|
|
assert docHeader.outlineButton.isVisible() is True
|
|
assert docHeader.searchButton.isVisible() is True
|
|
assert docHeader.itemTitle.isVisible() is True
|
|
assert docHeader.closeButton.isVisible() is True
|
|
|
|
# A hidden footer reserves no space in focus mode
|
|
docFooter = nwGUI.docEditor.docFooter
|
|
CONFIG.hideFocusFooter = True
|
|
assert docFooter.heightHint() > 0
|
|
nwGUI.toggleFocusMode()
|
|
assert docFooter.heightHint() == 0
|
|
nwGUI.toggleFocusMode()
|
|
assert docFooter.heightHint() > 0
|
|
|
|
# Closing editor disables focus mode
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is True
|
|
nwGUI.docPanes.closeDocument()
|
|
assert SHARED.focusMode is False
|
|
nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
|
|
# Pressing Escape turns off focus mode. Focus mode puts focus on the
|
|
# editor itself, which is where GuiDocEditor.keyPressEvent handles it.
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is True
|
|
assert nwGUI.docEditor.hasFocus() is True
|
|
qtbot.keyClick(nwGUI.docEditor, QtKeyEscape)
|
|
assert SHARED.focusMode is False
|
|
|
|
# If search is active, focus is in the search box instead, and Escape
|
|
# there just closes the search, leaving focus mode untouched
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is True
|
|
nwGUI.docEditor.beginSearch()
|
|
assert nwGUI.docEditor.docSearch.searchBox.hasFocus() is True
|
|
qtbot.keyClick(nwGUI.docEditor.docSearch.searchBox, QtKeyEscape)
|
|
assert nwGUI.docEditor.searchVisible() is False
|
|
assert SHARED.focusMode is True
|
|
|
|
# If search is active but focus is in the editor itself rather than
|
|
# the search box, Escape still closes the search, this time via the
|
|
# editor's own keyPressEvent rather than the search box's event filter
|
|
nwGUI.docEditor.beginSearch()
|
|
nwGUI.docEditor.setFocus()
|
|
assert nwGUI.docEditor.hasFocus() is True
|
|
assert nwGUI.docEditor.searchVisible() is True
|
|
qtbot.keyClick(nwGUI.docEditor, QtKeyEscape)
|
|
assert nwGUI.docEditor.searchVisible() is False
|
|
assert SHARED.focusMode is True
|
|
|
|
# With search closed, Vim mode off, and Focus Mode inactive, Escape
|
|
# does nothing
|
|
nwGUI.toggleFocusMode()
|
|
assert nwGUI.docEditor.searchVisible() is False
|
|
assert CONFIG.vimMode is False
|
|
assert SHARED.focusMode is False
|
|
qtbot.keyClick(nwGUI.docEditor, QtKeyEscape)
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_Features(
|
|
qtbot: QtBot,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
mockRnd: MockRnd,
|
|
):
|
|
"""Test various features of the main window."""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
nwGUI.docPanes.closeDocument() # Opening the project auto-restores a document
|
|
cHandle = SHARED.project.newFile("Jane", C.hCharRoot)
|
|
newDoc = SHARED.project.storage.getDocument(cHandle)
|
|
newDoc.writeDocument("# Jane\n\n@tag: Jane\n\n")
|
|
nwGUI.rebuildIndex()
|
|
|
|
# Full Screen Mode
|
|
# ================
|
|
|
|
fullScreen = Qt.WindowState.WindowFullScreen
|
|
assert nwGUI.windowState() & fullScreen != fullScreen
|
|
nwGUI.toggleFullScreenMode()
|
|
assert nwGUI.windowState() & fullScreen == fullScreen
|
|
nwGUI.toggleFullScreenMode()
|
|
assert nwGUI.windowState() & fullScreen != fullScreen
|
|
|
|
# SideBar Menu
|
|
# ============
|
|
|
|
# Just make sure the custom event handler executes and doesn't fail
|
|
nwGUI.sideBar.mSettings.show()
|
|
nwGUI.sideBar.mSettings.hide()
|
|
|
|
# Redirect Tag Open
|
|
# =================
|
|
|
|
nwGUI.docPanes.closeDocument()
|
|
nwGUI.docPanes.closeDocViewer()
|
|
assert nwGUI.docEditor.docHandle is None
|
|
assert nwGUI.docViewer.docHandle is None
|
|
nwGUI.docPanes._followTag("John", nwDocMode.EDIT) # Doesn't exist
|
|
assert nwGUI.docEditor.docHandle is None
|
|
assert nwGUI.docViewer.docHandle is None
|
|
nwGUI.docPanes._followTag("Jane", nwDocMode.EDIT)
|
|
assert nwGUI.docEditor.docHandle == cHandle
|
|
assert nwGUI.docViewer.docHandle is None
|
|
nwGUI.docPanes._followTag("Jane", nwDocMode.VIEW)
|
|
assert nwGUI.docEditor.docHandle == cHandle
|
|
assert nwGUI.docViewer.docHandle == cHandle
|
|
|
|
# Errors Handling
|
|
# ===============
|
|
|
|
# Cannot edit a folder
|
|
assert nwGUI.docPanes.openDocument(C.hChapterDir) is False
|
|
|
|
# Handle I/O error
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("builtins.open", causeOSError)
|
|
assert nwGUI.docPanes.openDocument(C.hChapterDoc) is False
|
|
|
|
# Import Document
|
|
# ===============
|
|
|
|
# Importing into an empty document does not prompt for overwrite
|
|
# confirmation
|
|
assert nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
nwGUI.docEditor.replaceText("")
|
|
assert nwGUI.docEditor.isEmpty is True
|
|
|
|
importFile = projPath / "import.txt"
|
|
importFile.write_text("Imported text.", encoding="utf-8")
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(importFile), ""))
|
|
nwGUI.docPanes.importDocument()
|
|
assert nwGUI.docEditor.getText() == "Imported text."
|
|
|
|
# Dialogs
|
|
# =======
|
|
|
|
# Reopening the manuscript and writing stats dialogs reuses the
|
|
# existing instance rather than creating a new one
|
|
nwGUI.showBuildManuscriptDialog()
|
|
manuscriptDialog = SHARED.findTopLevelWidget(GuiManuscript)
|
|
assert manuscriptDialog is not None
|
|
nwGUI.showBuildManuscriptDialog()
|
|
assert SHARED.findTopLevelWidget(GuiManuscript) is manuscriptDialog
|
|
manuscriptDialog.close()
|
|
manuscriptDialog.softDelete()
|
|
|
|
nwGUI.showWritingStatsDialog()
|
|
statsDialog = SHARED.findTopLevelWidget(GuiWritingStats)
|
|
assert statsDialog is not None
|
|
nwGUI.showWritingStatsDialog()
|
|
assert SHARED.findTopLevelWidget(GuiWritingStats) is statsDialog
|
|
statsDialog.close()
|
|
statsDialog.softDelete()
|
|
|
|
# qtbot.stop()
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_CloseMain(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd: MockRnd):
|
|
"""Test closeMain's pane-size and window-state save skips, across a
|
|
project close/reopen cycle..
|
|
"""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
|
|
# closeMain skips saving pane sizes while Focus Mode is active
|
|
assert nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is True
|
|
assert nwGUI.closeMain() is True
|
|
|
|
# closeMain skips saving the window size while in full screen mode
|
|
assert nwGUI.openProject(projPath) is True
|
|
nwGUI.setWindowState(nwGUI.windowState() | Qt.WindowState.WindowFullScreen)
|
|
assert nwGUI.closeMain() is True
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_ToggleTreePane(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd: MockRnd):
|
|
"""Test toggling the tree pane from the side bar."""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
assert nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
sideBar = nwGUI.sideBar
|
|
assert nwGUI.projStack.currentWidget() == nwGUI.projView
|
|
assert nwGUI.treePane.isHidden() is False
|
|
|
|
# Clicking the active view toggles the pane
|
|
sideBar.tbProject.click()
|
|
assert nwGUI.treePane.isHidden() is True
|
|
sideBar.tbProject.click()
|
|
assert nwGUI.treePane.isHidden() is False
|
|
|
|
# Switching to another view shows the pane
|
|
sideBar.tbProject.click()
|
|
sideBar.tbNovel.click()
|
|
assert nwGUI.projStack.currentWidget() == nwGUI.novelView
|
|
assert nwGUI.treePane.isHidden() is False
|
|
|
|
# Other sources don't toggle the pane
|
|
sideBar.tbNovel.click()
|
|
nwGUI.changeView(nwView.NOVEL)
|
|
assert nwGUI.treePane.isHidden() is True
|
|
|
|
# Opening a document doesn't show the pane
|
|
assert nwGUI.docPanes.openDocument(C.hChapterDoc)
|
|
assert nwGUI.treePane.isHidden() is True
|
|
|
|
# Returning from story view shows the pane
|
|
sideBar.tbStory.click()
|
|
sideBar.tbNovel.click()
|
|
assert nwGUI.treePane.isHidden() is False
|
|
|
|
# Focus mode restores the hidden state
|
|
sideBar.tbNovel.click()
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is True
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is False
|
|
assert nwGUI.treePane.isHidden() is True
|
|
|
|
# Focusing the tree shows the pane
|
|
nwGUI._switchFocus(nwFocus.TREE)
|
|
assert nwGUI.projStack.currentWidget() == nwGUI.novelView
|
|
assert nwGUI.treePane.isHidden() is False
|
|
|
|
# The menu action toggles the pane
|
|
aToggle = nwGUI.mainMenu.aHideTreePane
|
|
aToggle.trigger()
|
|
assert nwGUI.treePane.isHidden() is True
|
|
aToggle.trigger()
|
|
assert nwGUI.treePane.isHidden() is False
|
|
|
|
# The menu action does nothing in story view
|
|
sideBar.tbStory.click()
|
|
aToggle.trigger()
|
|
assert nwGUI.treePane.isHidden() is False
|
|
sideBar.tbNovel.click()
|
|
|
|
# The menu action does nothing in focus mode
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is True
|
|
aToggle.trigger()
|
|
assert nwGUI.treePane.isHidden() is True
|
|
nwGUI.toggleFocusMode()
|
|
assert SHARED.focusMode is False
|
|
assert nwGUI.treePane.isHidden() is False
|
|
|
|
# The pane size is not saved when hidden
|
|
sideBar.tbNovel.click()
|
|
assert nwGUI.treePane.isHidden() is True
|
|
CONFIG.mainPanePos = [123, 456]
|
|
assert nwGUI.closeMain() is True
|
|
assert CONFIG.mainPanePos == [123, 456]
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_OpenClose(
|
|
qtbot: QtBot,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
fncPath: Path,
|
|
mockRnd: MockRnd,
|
|
):
|
|
"""Test opening and closing projects and documents."""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
|
|
# Check open document and prev/next with no wraparound
|
|
nwGUI.docPanes.openNextDocument(C.hSceneDoc, False, True) # Backwards
|
|
assert nwGUI.docEditor._docHandle == C.hChapterDoc
|
|
nwGUI.docPanes.openNextDocument(C.hChapterDoc, False, False) # Forwards
|
|
assert nwGUI.docEditor._docHandle == C.hSceneDoc
|
|
|
|
# Check open document and prev/next with wraparound
|
|
nwGUI.docPanes.openNextDocument(C.hSceneDoc, True, False) # Forwards
|
|
assert nwGUI.docEditor._docHandle == C.hTitlePage
|
|
nwGUI.docPanes.openNextDocument(C.hTitlePage, True, True) # Backwards
|
|
assert nwGUI.docEditor._docHandle == C.hSceneDoc
|
|
|
|
# Check with handle that is not a document, should do nothing
|
|
nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
assert nwGUI.docEditor._docHandle == C.hSceneDoc
|
|
nwGUI.docPanes.openNextDocument(C.hNovelRoot, True, True)
|
|
assert nwGUI.docEditor._docHandle == C.hSceneDoc
|
|
|
|
# No wraparound at the very first or last document stays put
|
|
allDocs = SHARED.project.tree.allDocs()
|
|
firstDoc = allDocs[0]
|
|
lastDoc = allDocs[-1]
|
|
|
|
nwGUI.docPanes.openDocument(firstDoc)
|
|
nwGUI.docPanes.openNextDocument(firstDoc, False, True) # Backwards, no wraparound
|
|
assert nwGUI.docEditor.docHandle == firstDoc
|
|
|
|
nwGUI.docPanes.openDocument(lastDoc)
|
|
nwGUI.docPanes.openNextDocument(lastDoc, False, False) # Forwards, no wraparound
|
|
assert nwGUI.docEditor.docHandle == lastDoc
|
|
|
|
# Autosaving does nothing when there are no changes to save
|
|
assert nwGUI.saveProject() is True
|
|
assert SHARED.project.projChanged is False
|
|
nwGUI._autoSaveProject()
|
|
|
|
assert nwGUI.docEditor.docChanged is False
|
|
nwGUI._autoSaveDocument()
|
|
|
|
# Open some test documents
|
|
nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
nwGUI.docPanes.viewDocument(C.hTitlePage)
|
|
|
|
# Handle broken index on project open
|
|
idxData = jsonEncode({"UmbraEdit.meta": {"version": __hexversion__}})
|
|
nwGUI.closeProject()
|
|
idxPath: Path = projPath / "meta" / nwFiles.INDEX_FILE
|
|
assert idxPath.read_text(encoding="utf-8") != "{}"
|
|
idxPath.write_text(idxData, encoding="utf-8")
|
|
|
|
nwGUI.openProject(projPath)
|
|
nwGUI.saveProject()
|
|
assert idxPath.read_text(encoding="utf-8") != "{}"
|
|
assert nwGUI.docEditor.docHandle == C.hSceneDoc
|
|
assert nwGUI.docViewer.docHandle == C.hTitlePage
|
|
|
|
# Block closing
|
|
assert SHARED.hasProject is True
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(_GuiAlert, "finalState", False)
|
|
assert nwGUI.openProject(projPath) is False
|
|
assert SHARED.hasProject is True
|
|
|
|
# Don't open on lockfile question: No
|
|
lockPath: Path = projPath / nwFiles.PROJ_LOCK
|
|
lockBack: Path = projPath / f"{nwFiles.PROJ_LOCK}.bak"
|
|
|
|
shutil.copyfile(lockPath, lockBack)
|
|
nwGUI.closeProject()
|
|
shutil.copyfile(lockBack, lockPath)
|
|
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(_GuiAlert, "finalState", False)
|
|
assert nwGUI.openProject(projPath) is False
|
|
|
|
assert nwGUI.openProject(projPath) is True
|
|
|
|
# Backup on close
|
|
backDir = CONFIG.backupPath() / SHARED.project.data.name
|
|
assert not backDir.exists()
|
|
|
|
CONFIG.backupOnClose = True
|
|
assert nwGUI.openProject(projPath) is True
|
|
nwGUI.closeProject()
|
|
assert backDir.exists()
|
|
assert len(list(backDir.iterdir())) > 0
|
|
|
|
# Backing up without prompting first, when askBeforeBackup is disabled
|
|
CONFIG.askBeforeBackup = False
|
|
questionMock = Mock()
|
|
backupMock = Mock()
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(SHARED, "question", questionMock)
|
|
mp.setattr(type(SHARED.project), "backupProject", backupMock)
|
|
assert nwGUI.openProject(projPath) is True
|
|
assert nwGUI.closeProject(isYes=True) is True
|
|
assert questionMock.called is False
|
|
assert backupMock.called is True
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_OpenDocument_ReentrancyGuard(
|
|
qtbot: QtBot,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
mockRnd: MockRnd,
|
|
):
|
|
"""Test that openDocument ignores a re-entrant call made while a
|
|
document switch is already in progress.
|
|
|
|
This models what happens holding F3 with project-wide loop search
|
|
enabled: loadText calls QApplication.processEvents while a switch
|
|
is still in progress, and if that dispatches an already-queued
|
|
key-repeat event, it can re-trigger another document switch before
|
|
the first one has finished.
|
|
"""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
|
|
reentered = []
|
|
realProcessEvents = QApplication.processEvents
|
|
|
|
def reentrantProcessEvents(*args: Any, **kwargs: Any) -> None:
|
|
if not reentered:
|
|
reentered.append(True)
|
|
assert nwGUI.docPanes.openDocument(C.hChapterDoc) is False
|
|
return realProcessEvents(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(QApplication, "processEvents", staticmethod(reentrantProcessEvents))
|
|
|
|
assert nwGUI.docPanes.openDocument(C.hSceneDoc) is True
|
|
assert reentered == [True]
|
|
assert nwGUI.docEditor.docHandle == C.hSceneDoc
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testGuiMain_FocusView(
|
|
qtbot: QtBot,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
nwGUI: GuiMain,
|
|
projPath: Path,
|
|
mockRnd: MockRnd,
|
|
):
|
|
"""Test switching focus and view of the main window."""
|
|
buildTestProject(NWProject(), projPath)
|
|
nwGUI.openProject(projPath)
|
|
|
|
nwGUI.docPanes.openDocument(C.hSceneDoc)
|
|
nwGUI.docPanes.viewDocument(C.hSceneDoc)
|
|
|
|
widget = QWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
# Toggle Focus
|
|
# ============
|
|
nwGUI.docEditor.setFocus()
|
|
assert nwGUI.docEditor.anyFocus()
|
|
|
|
# Simulate focus change to viewer
|
|
nwGUI.docPanes.onFocusChanged(widget, nwGUI.docViewer)
|
|
assert nwGUI.docEditor.docHeader.itemTitle._state == nwState.INACTIVE
|
|
assert nwGUI.docViewer.docHeader.itemTitle._state == nwState.NORMAL
|
|
|
|
# Simulate focus change to editor
|
|
nwGUI.docPanes.onFocusChanged(widget, nwGUI.docEditor)
|
|
assert nwGUI.docEditor.docHeader.itemTitle._state == nwState.NORMAL
|
|
assert nwGUI.docViewer.docHeader.itemTitle._state == nwState.INACTIVE
|
|
|
|
# Simulate focus change to an unrelated widget: neither is focused
|
|
nwGUI.docPanes.onFocusChanged(widget, widget)
|
|
assert nwGUI.docEditor.docHeader.itemTitle._state == nwState.INACTIVE
|
|
assert nwGUI.docViewer.docHeader.itemTitle._state == nwState.INACTIVE
|
|
|
|
# Focus Tree
|
|
# ==========
|
|
assert nwGUI.projStack.currentWidget() == nwGUI.projView
|
|
|
|
# Switch from editor to project tree
|
|
nwGUI.docEditor.setFocus()
|
|
nwGUI._switchFocus(nwFocus.TREE)
|
|
assert nwGUI.projStack.currentWidget() == nwGUI.projView
|
|
|
|
# Triggering again should switch to novel view
|
|
nwGUI._switchFocus(nwFocus.TREE)
|
|
assert nwGUI.projStack.currentWidget() == nwGUI.novelView
|
|
|
|
# Switch from editor to novel view
|
|
nwGUI.docEditor.setFocus()
|
|
nwGUI._switchFocus(nwFocus.TREE)
|
|
assert nwGUI.projStack.currentWidget() == nwGUI.novelView
|
|
|
|
# Triggering again should switch back to project tree
|
|
nwGUI._switchFocus(nwFocus.TREE)
|
|
assert nwGUI.projStack.currentWidget() == nwGUI.projView
|
|
|
|
# If in search mode, should default to project tree
|
|
nwGUI.changeView(nwView.SEARCH)
|
|
nwGUI._switchFocus(nwFocus.TREE)
|
|
assert nwGUI.projStack.currentWidget() == nwGUI.projView
|
|
|
|
# Focus Document
|
|
# ==============
|
|
nwGUI._switchFocus(nwFocus.TREE)
|
|
|
|
def mockEmitEditorFocus(*a: Any) -> None:
|
|
nwGUI.docPanes.onFocusChanged(widget, nwGUI.docEditor)
|
|
|
|
def mockEmitViewerFocus(*a: Any) -> None:
|
|
nwGUI.docPanes.onFocusChanged(widget, nwGUI.docViewer)
|
|
|
|
# Switch to viewer
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: True)
|
|
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: False)
|
|
mp.setattr(nwGUI.docEditor, "setFocus", mockEmitEditorFocus)
|
|
mp.setattr(nwGUI.docViewer, "setFocus", mockEmitViewerFocus)
|
|
nwGUI._switchFocus(nwFocus.DOCUMENT)
|
|
assert nwGUI.docEditor.docHeader.itemTitle._state == nwState.INACTIVE
|
|
assert nwGUI.docViewer.docHeader.itemTitle._state == nwState.NORMAL
|
|
|
|
# Call again to switch to editor
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
|
|
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: True)
|
|
mp.setattr(nwGUI.docEditor, "setFocus", mockEmitEditorFocus)
|
|
mp.setattr(nwGUI.docViewer, "setFocus", mockEmitViewerFocus)
|
|
nwGUI._switchFocus(nwFocus.DOCUMENT)
|
|
assert nwGUI.docEditor.docHeader.itemTitle._state == nwState.NORMAL
|
|
assert nwGUI.docViewer.docHeader.itemTitle._state == nwState.INACTIVE
|
|
|
|
# Default to editor
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
|
|
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: False)
|
|
mp.setattr(nwGUI.docEditor, "setFocus", mockEmitEditorFocus)
|
|
mp.setattr(nwGUI.docViewer, "setFocus", mockEmitViewerFocus)
|
|
nwGUI._switchFocus(nwFocus.DOCUMENT)
|
|
assert nwGUI.docEditor.docHeader.itemTitle._state == nwState.NORMAL
|
|
assert nwGUI.docViewer.docHeader.itemTitle._state == nwState.INACTIVE
|
|
|
|
# Pass Actions
|
|
# ============
|
|
|
|
# Pass to editor
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: True)
|
|
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: False)
|
|
nwGUI.docPanes.passDocumentAction(nwDocAction.SEL_ALL)
|
|
assert nwGUI.docEditor.textCursor().hasSelection() is True
|
|
|
|
# Pass to viewer
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
|
|
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: True)
|
|
nwGUI.docPanes.passDocumentAction(nwDocAction.SEL_ALL)
|
|
assert nwGUI.docViewer.textCursor().hasSelection() is True
|
|
|
|
# Pass to neither, since neither has focus: nothing changes
|
|
hadSelection = nwGUI.docEditor.textCursor().hasSelection()
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
|
|
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: False)
|
|
nwGUI.docPanes.passDocumentAction(nwDocAction.MOVE_TEXT)
|
|
assert nwGUI.docEditor.textCursor().hasSelection() is hadSelection
|
|
|
|
# qtbot.stop()
|