Merge release 2.7.5
This commit is contained in:
@@ -17,7 +17,7 @@ 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
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
@@ -28,7 +28,7 @@ from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QThreadPool, QUrl
|
||||
from PyQt6.QtGui import (
|
||||
QAction, QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent,
|
||||
QDropEvent, QFont, QInputMethodEvent, QMouseEvent, QTextBlock, QTextCursor,
|
||||
QTextOption
|
||||
QTextDocument, QTextOption
|
||||
)
|
||||
from PyQt6.QtWidgets import QApplication, QMenu, QPlainTextEdit
|
||||
|
||||
@@ -37,7 +37,7 @@ from novelwriter.common import decodeMimeHandles
|
||||
from novelwriter.constants import nwKeyWords, nwUnicode
|
||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout
|
||||
from novelwriter.gui.doceditor import GuiDocEditor, _TagAction
|
||||
from novelwriter.gui.doceditor import GuiDocEditor, TextAutoReplace, _TagAction
|
||||
from novelwriter.gui.dochighlight import TextBlockData
|
||||
from novelwriter.text.counting import standardCounter
|
||||
from novelwriter.types import (
|
||||
@@ -515,32 +515,36 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
|
||||
# Run SpellCheck
|
||||
# ==============
|
||||
SHARED.project.data.setSpellCheck(True)
|
||||
LORAX = "Lorax\U0001F03A"
|
||||
|
||||
cursor = docEditor.textCursor()
|
||||
cursor.setPosition(16)
|
||||
data = cursor.block().userData()
|
||||
assert cursor.block().text().startswith("Lorem")
|
||||
assert isinstance(data, TextBlockData)
|
||||
data._spellErrors = [(0, 5)]
|
||||
data._spellErrors = [(0, 5, "Lorem")]
|
||||
|
||||
# No known position
|
||||
assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, -1, [])
|
||||
assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, [])
|
||||
|
||||
# With Suggestion
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(SHARED.spelling, "suggestWords", lambda *a: ["Lorax"])
|
||||
mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [LORAX])
|
||||
|
||||
ctxMenu = getMenuForPos(docEditor, 16)
|
||||
assert ctxMenu is not None
|
||||
actions = [x.text() for x in ctxMenu.actions() if x.text()]
|
||||
assert "Spelling Suggestion(s)" in actions
|
||||
assert f"{nwUnicode.U_ENDASH} Lorax" in actions
|
||||
assert f"{nwUnicode.U_ENDASH} {LORAX}" in actions
|
||||
ctxMenu.actions()[7].trigger()
|
||||
QApplication.processEvents()
|
||||
assert docEditor.getText() == text.replace("Lorem", "Lorax", 1)
|
||||
assert docEditor.getText() == text.replace("Lorem", LORAX, 1)
|
||||
ctxMenu.setObjectName("")
|
||||
ctxMenu.deleteLater()
|
||||
|
||||
# Update Entry
|
||||
data._spellErrors = [(0, 7, LORAX)]
|
||||
|
||||
# Without Suggestion
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [])
|
||||
@@ -549,7 +553,7 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
|
||||
assert ctxMenu is not None
|
||||
actions = [x.text() for x in ctxMenu.actions() if x.text()]
|
||||
assert f"{nwUnicode.U_ENDASH} No Suggestions" in actions
|
||||
assert docEditor.getText() == text.replace("Lorem", "Lorax", 1)
|
||||
assert docEditor.getText() == text.replace("Lorem", LORAX, 1)
|
||||
ctxMenu.setObjectName("")
|
||||
ctxMenu.deleteLater()
|
||||
|
||||
@@ -563,11 +567,11 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
|
||||
assert "Ignore Word" in actions
|
||||
assert "Add Word to Dictionary" in actions
|
||||
|
||||
assert "Lorax" not in SHARED.spelling._userDict
|
||||
assert LORAX not in SHARED.spelling._userDict
|
||||
ctxMenu.actions()[7].trigger() # Ignore
|
||||
assert "Lorax" not in SHARED.spelling._userDict
|
||||
assert LORAX not in SHARED.spelling._userDict
|
||||
ctxMenu.actions()[8].trigger() # Add
|
||||
assert "Lorax" in SHARED.spelling._userDict
|
||||
assert LORAX in SHARED.spelling._userDict
|
||||
ctxMenu.setObjectName("")
|
||||
ctxMenu.deleteLater()
|
||||
|
||||
@@ -675,9 +679,16 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
assert docEditor.docAction(nwDocAction.UNDO) is True
|
||||
assert docEditor.getText() == text
|
||||
|
||||
# Mark
|
||||
docEditor.setCursorPosition(50)
|
||||
assert docEditor.docAction(nwDocAction.MD_MARK) is True
|
||||
assert docEditor.getText() == text.replace("consectetur", "==consectetur==")
|
||||
assert docEditor.docAction(nwDocAction.UNDO) is True
|
||||
assert docEditor.getText() == text
|
||||
|
||||
# Redo
|
||||
assert docEditor.docAction(nwDocAction.REDO) is True
|
||||
assert docEditor.getText() == text.replace("consectetur", "~~consectetur~~")
|
||||
assert docEditor.getText() == text.replace("consectetur", "==consectetur==")
|
||||
assert docEditor.docAction(nwDocAction.UNDO) is True
|
||||
assert docEditor.getText() == text
|
||||
|
||||
@@ -2081,7 +2092,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
origText = docEditor.getText()
|
||||
|
||||
# Select the Word "est"
|
||||
docEditor.setCursorPosition(645)
|
||||
docEditor.setCursorPosition(663)
|
||||
docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
|
||||
cursor = docEditor.textCursor()
|
||||
assert cursor.selectedText() == "est"
|
||||
@@ -2094,11 +2105,11 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
# Find next by enter key
|
||||
monkeypatch.setattr(docSearch.searchBox, "hasFocus", lambda: True)
|
||||
qtbot.keyClick(docSearch.searchBox, Qt.Key.Key_Return, delay=KEY_DELAY)
|
||||
assert abs(docEditor.getCursorPosition() - 1299) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 1317) < 3
|
||||
|
||||
# Find next by button
|
||||
qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY)
|
||||
assert abs(docEditor.getCursorPosition() - 1513) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 1531) < 3
|
||||
|
||||
# Activate loop search
|
||||
docSearch.toggleLoop.activate(QAction.ActionEvent.Trigger)
|
||||
@@ -2107,7 +2118,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
|
||||
# Find next by menu Search > Find Next
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 647) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 665) < 3
|
||||
|
||||
# Close search
|
||||
docSearch.cancelSearch.activate(QAction.ActionEvent.Trigger)
|
||||
@@ -2146,13 +2157,13 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
# Set valid RegEx
|
||||
docSearch.setSearchText(r"\bSus")
|
||||
qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY)
|
||||
assert abs(docEditor.getCursorPosition() - 223) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 241) < 3
|
||||
|
||||
# Find next and then prev
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 324) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 342) < 3
|
||||
nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 223) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 241) < 3
|
||||
|
||||
# Make RegEx case sensitive
|
||||
docSearch.toggleCase.activate(QAction.ActionEvent.Trigger)
|
||||
@@ -2161,11 +2172,11 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
|
||||
# Find next/prev (one result)
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 626) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 644) < 3
|
||||
nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 626) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 644) < 3
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 626) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 644) < 3
|
||||
|
||||
# Trigger replace
|
||||
nwGUI.mainMenu.aReplace.activate(QAction.ActionEvent.Trigger)
|
||||
@@ -2182,22 +2193,22 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
assert CONFIG.searchMatchCap is True
|
||||
|
||||
# Replace "Sus" with "Foo" via menu
|
||||
docEditor.setCursorPosition(605)
|
||||
docEditor.setCursorPosition(623)
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
nwGUI.mainMenu.aReplaceNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[623:634] == "Foopendisse"
|
||||
assert docEditor.getText()[641:652] == "Foopendisse"
|
||||
|
||||
# Find next/prev to loop file
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 223) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 241) < 3
|
||||
nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 1805) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 1823) < 3
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 223) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 241) < 3
|
||||
|
||||
# Replace "sus" with "foo" via replace button
|
||||
qtbot.mouseClick(docSearch.replaceButton, QtMouseLeft, delay=KEY_DELAY)
|
||||
assert docEditor.getText()[220:228] == "foocipit"
|
||||
assert docEditor.getText()[238:246] == "foocipit"
|
||||
|
||||
# Revert last two replaces
|
||||
assert docEditor.docAction(nwDocAction.UNDO)
|
||||
@@ -2211,7 +2222,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
|
||||
# Close search and select "est" again
|
||||
docSearch.cancelSearch.activate(QAction.ActionEvent.Trigger)
|
||||
docEditor.setCursorPosition(645)
|
||||
docEditor.setCursorPosition(663)
|
||||
docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
|
||||
cursor = docEditor.textCursor()
|
||||
assert cursor.selectedText() == "est"
|
||||
@@ -2228,9 +2239,9 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
|
||||
# Only one match
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 647) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 665) < 3
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 647) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 665) < 3
|
||||
|
||||
# Enable next doc search
|
||||
docSearch.toggleProject.activate(QAction.ActionEvent.Trigger)
|
||||
@@ -2241,9 +2252,9 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.docHandle == "2426c6f0ca922" # Next document
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 620) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 651) < 3
|
||||
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
|
||||
assert abs(docEditor.getCursorPosition() - 1127) < 3
|
||||
assert abs(docEditor.getCursorPosition() - 1157) < 3
|
||||
|
||||
# Next doc, no match
|
||||
assert CONFIG.searchNextFile is True
|
||||
@@ -2345,3 +2356,142 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
assert docEditor.textCursor().selectedText() == ""
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiEditor_TextAutoReplaceSymbols():
|
||||
"""Test the editor auto-replace functionality."""
|
||||
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
|
||||
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
|
||||
CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO
|
||||
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
|
||||
|
||||
CONFIG.doReplaceSQuote = True
|
||||
CONFIG.doReplaceDQuote = True
|
||||
CONFIG.doReplaceDash = True
|
||||
CONFIG.doReplaceDots = True
|
||||
|
||||
ar = TextAutoReplace()
|
||||
|
||||
def prep(text: str) -> tuple[str, int]:
|
||||
return text, len(text)
|
||||
|
||||
# Double Quote Open
|
||||
assert ar._determine(*prep('"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('Stuff "')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('>"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('>>"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('_"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep(' _"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('\u00a0_"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('**"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep(' **"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('\u00a0**"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('=="')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep(' =="')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('\u00a0=="')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('~~"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep(' ~~"')) == (1, nwUnicode.U_LDQUO)
|
||||
assert ar._determine(*prep('\u00a0~~"')) == (1, nwUnicode.U_LDQUO)
|
||||
|
||||
# Double Quote Close
|
||||
assert ar._determine(*prep('Stuff"')) == (1, nwUnicode.U_RDQUO)
|
||||
|
||||
# Single Quote Open
|
||||
assert ar._determine(*prep("'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep("Stuff '")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep(">'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep(">>'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep("_'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep(" _'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep("\u00a0_'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep("**'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep(" **'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep("\u00a0**'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep("=='")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep(" =='")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep("\u00a0=='")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep("~~'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep(" ~~'")) == (1, nwUnicode.U_LSQUO)
|
||||
assert ar._determine(*prep("\u00a0~~'")) == (1, nwUnicode.U_LSQUO)
|
||||
|
||||
# Single Quote Close
|
||||
assert ar._determine(*prep("Stuff'")) == (1, nwUnicode.U_RSQUO)
|
||||
|
||||
# Dashes
|
||||
assert ar._determine(*prep("-")) == (0, "-")
|
||||
assert ar._determine(*prep("--")) == (2, nwUnicode.U_ENDASH)
|
||||
assert ar._determine(*prep("---")) == (3, nwUnicode.U_EMDASH)
|
||||
assert ar._determine(*prep("----")) == (4, nwUnicode.U_HBAR)
|
||||
assert ar._determine(*prep("\u2013-")) == (2, nwUnicode.U_EMDASH)
|
||||
assert ar._determine(*prep("\u2014-")) == (2, nwUnicode.U_HBAR)
|
||||
|
||||
# Ellipsis
|
||||
assert ar._determine(*prep(".")) == (0, ".")
|
||||
assert ar._determine(*prep("..")) == (0, ".")
|
||||
assert ar._determine(*prep("...")) == (3, nwUnicode.U_HELLIP)
|
||||
|
||||
# Block Typed Line Separator (#1150)
|
||||
assert ar._determine(*prep("Text\u2028")) == (1, nwUnicode.U_PSEP)
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiEditor_TextAutoReplaceProcess():
|
||||
"""Test the editor auto-replace functionality."""
|
||||
CONFIG.fmtDQuoteOpen = nwUnicode.U_LAQUO
|
||||
CONFIG.fmtDQuoteClose = nwUnicode.U_RAQUO
|
||||
|
||||
CONFIG.doReplaceDQuote = True
|
||||
CONFIG.doReplaceDots = True
|
||||
|
||||
ar = TextAutoReplace()
|
||||
doc = QTextDocument()
|
||||
|
||||
def prep(text: str) -> tuple[str, QTextCursor]:
|
||||
doc.setPlainText(text)
|
||||
cursor = QTextCursor(doc)
|
||||
cursor.setPosition(len(text))
|
||||
return text, cursor
|
||||
|
||||
# Nothing to Process
|
||||
assert ar.process(*prep("")) is False
|
||||
|
||||
# Standard Auto-Replace
|
||||
assert ar.process(*prep("Text ...")) is True
|
||||
assert doc.toRawText() == "Text \u2026"
|
||||
|
||||
# Pad Before, Normal
|
||||
CONFIG.fmtPadBefore = ":\u00bb"
|
||||
CONFIG.fmtPadThin = False
|
||||
ar.initSettings()
|
||||
assert ar.process(*prep("Text:")) is True
|
||||
assert doc.toRawText() == "Text\u00a0:"
|
||||
assert ar.process(*prep("Text :")) is True # See #1061
|
||||
assert doc.toRawText() == "Text\u00a0:"
|
||||
assert ar.process(*prep('Text"')) is True
|
||||
assert doc.toRawText() == "Text\u00a0»"
|
||||
assert ar.process(*prep("@Synopsis:")) is False
|
||||
assert doc.toRawText() == "@Synopsis:"
|
||||
|
||||
# Pad Before, Thin
|
||||
CONFIG.fmtPadBefore = ":\u00bb"
|
||||
CONFIG.fmtPadThin = True
|
||||
ar.initSettings()
|
||||
assert ar.process(*prep("Text:")) is True
|
||||
assert doc.toRawText() == "Text\u202f:"
|
||||
assert ar.process(*prep("Text :")) is True # See #1061
|
||||
assert doc.toRawText() == "Text\u202f:"
|
||||
|
||||
# Pad After, Normal
|
||||
CONFIG.fmtPadAfter = "\u00ab"
|
||||
CONFIG.fmtPadThin = False
|
||||
ar.initSettings()
|
||||
assert ar.process(*prep('Text "')) is True
|
||||
assert doc.toRawText() == "Text «\u00a0"
|
||||
|
||||
# Pad After, Thin
|
||||
CONFIG.fmtPadAfter = "\u00ab"
|
||||
CONFIG.fmtPadThin = True
|
||||
ar.initSettings()
|
||||
assert ar.process(*prep('Text "')) is True
|
||||
assert doc.toRawText() == "Text «\u202f"
|
||||
|
||||
@@ -0,0 +1,632 @@
|
||||
"""
|
||||
novelWriter – GUI Syntax Highlighter Tester
|
||||
===========================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter 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 pytest
|
||||
|
||||
from PyQt6.QtGui import QTextCharFormat, QTextCursor, QTextDocument
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
|
||||
from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE, GuiDocHighlighter, TextBlockData
|
||||
from novelwriter.types import QtKeepAnchor
|
||||
|
||||
R_HANDLE = "3456789abcdef"
|
||||
T_HANDLE = "0123456789abc"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def syntax(nwGUI):
|
||||
"""Create a syntax object for use with testing."""
|
||||
CONFIG.lightTheme = "default_light"
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
|
||||
CONFIG.dialogStyle = 3
|
||||
CONFIG.fmtDQuoteOpen = "\u201c"
|
||||
CONFIG.fmtDQuoteClose = "\u201d"
|
||||
CONFIG.altDialogOpen = "::"
|
||||
CONFIG.altDialogClose = "::"
|
||||
CONFIG.showMultiSpaces = True
|
||||
|
||||
theme = SHARED.theme
|
||||
theme.loadTheme(force=True)
|
||||
assert theme._guiPalette.base().color().getRgb() == (0xfc, 0xfc, 0xfc, 0xff)
|
||||
assert theme.syntaxTheme.text.getRgb() == (0x30, 0x30, 0x30, 0xff)
|
||||
|
||||
doc = QTextDocument()
|
||||
syntax = GuiDocHighlighter(doc)
|
||||
|
||||
# Add a Mock Item
|
||||
tRoot = NWItem(SHARED.project, R_HANDLE)
|
||||
tRoot.setClass(nwItemClass.NOVEL)
|
||||
tRoot.setType(nwItemType.ROOT)
|
||||
|
||||
tItem = NWItem(SHARED.project, T_HANDLE)
|
||||
tItem.setParent(R_HANDLE)
|
||||
tItem.setLayout(nwItemLayout.NOTE)
|
||||
tItem.setClass(nwItemClass.NOVEL)
|
||||
tItem.setType(nwItemType.FILE)
|
||||
|
||||
SHARED.project.tree.add(tRoot)
|
||||
SHARED.project.tree.add(tItem)
|
||||
|
||||
yield syntax
|
||||
|
||||
|
||||
def getFragments(
|
||||
syntax: GuiDocHighlighter
|
||||
) -> tuple[list[tuple[int, str]], list[QTextCharFormat]]:
|
||||
"""Extract all syntax highlighter fragments from a document."""
|
||||
pieces = []
|
||||
formats = []
|
||||
doc = syntax.document()
|
||||
cursor = QTextCursor(doc)
|
||||
assert doc is not None
|
||||
for b in range(doc.blockCount()):
|
||||
block = doc.findBlockByNumber(b)
|
||||
first = block.position()
|
||||
syntax.rehighlightBlock(block)
|
||||
if layout := block.layout():
|
||||
for fmt in layout.formats():
|
||||
cursor.setPosition(first + fmt.start)
|
||||
cursor.setPosition(first + fmt.start + fmt.length, QtKeepAnchor)
|
||||
pieces.append((b, fmt.start, fmt.length, cursor.selectedText()))
|
||||
formats.append(fmt.format)
|
||||
return pieces, formats
|
||||
|
||||
|
||||
def maxOrd(text: str) -> int:
|
||||
"""Get the max character value of a string."""
|
||||
return max(ord(c) for c in text)
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiDocHighlighter_Basic(syntax):
|
||||
"""Test the basic functionality of the syntax highlighter."""
|
||||
# Alternate Spell Check
|
||||
assert syntax._spellCheck is False
|
||||
syntax.setSpellCheck(True)
|
||||
assert syntax._spellCheck is True
|
||||
|
||||
# Check Handle
|
||||
assert syntax._tHandle is None
|
||||
syntax.setHandle(T_HANDLE)
|
||||
assert syntax._tHandle == T_HANDLE
|
||||
assert syntax._isNovel is False
|
||||
assert syntax._isInactive is False
|
||||
|
||||
tItem = SHARED.project.tree[T_HANDLE]
|
||||
assert tItem is not None
|
||||
tItem.setLayout(nwItemLayout.DOCUMENT)
|
||||
tItem.setClass(nwItemClass.ARCHIVE)
|
||||
syntax.setHandle(T_HANDLE)
|
||||
assert syntax._tHandle == T_HANDLE
|
||||
assert syntax._isNovel is True
|
||||
assert syntax._isInactive is True
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiDocHighlighter_Keywords(syntax):
|
||||
"""Test highlighting of keywords."""
|
||||
theme = SHARED.theme
|
||||
doc = syntax.document()
|
||||
assert doc is not None
|
||||
|
||||
# Settings
|
||||
syntax._tHandle = T_HANDLE
|
||||
|
||||
colKey = theme.syntaxTheme.key.getRgb()
|
||||
colTag = theme.syntaxTheme.tag.getRgb()
|
||||
colOpt = theme.syntaxTheme.opt.getRgb()
|
||||
colErr = theme.syntaxTheme.error.getRgb()
|
||||
|
||||
# Ascii
|
||||
doc.setPlainText(
|
||||
"@tag: Bob | Robert\n"
|
||||
"@char: Someone\n"
|
||||
)
|
||||
syntax.rehighlightByType(BLOCK_META)
|
||||
assert maxOrd(doc.toPlainText()) <= 0x7f
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 4, "@tag"), (0, 6, 3, "Bob"), (0, 12, 6, "Robert"),
|
||||
(1, 0, 5, "@char"), (1, 7, 7, "Someone"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colKey
|
||||
assert formats[1].foreground().color().getRgb() == colTag
|
||||
assert formats[2].foreground().color().getRgb() == colOpt
|
||||
assert formats[3].foreground().color().getRgb() == colKey
|
||||
assert formats[4].underlineColor().getRgb() == colErr
|
||||
|
||||
# # Unicode <= 0xFFFF
|
||||
doc.setPlainText(
|
||||
"@tag: Zoë | Zoë Smith\n"
|
||||
"@char: Олексій\n"
|
||||
)
|
||||
syntax.rehighlightByType(BLOCK_META)
|
||||
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 4, "@tag"), (0, 6, 3, "Zoë"), (0, 12, 9, "Zoë Smith"),
|
||||
(1, 0, 5, "@char"), (1, 7, 7, "Олексій"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colKey
|
||||
assert formats[1].foreground().color().getRgb() == colTag
|
||||
assert formats[2].foreground().color().getRgb() == colOpt
|
||||
assert formats[3].foreground().color().getRgb() == colKey
|
||||
assert formats[4].underlineColor().getRgb() == colErr
|
||||
|
||||
# # Unicode > 0xFFFF
|
||||
doc.setPlainText(
|
||||
"@tag: 😄 | Smiley 😄\n"
|
||||
"@char: 😎😎😎\n"
|
||||
)
|
||||
syntax.rehighlightByType(BLOCK_META)
|
||||
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 4, "@tag"), (0, 6, 2, "😄"), (0, 11, 9, "Smiley 😄"),
|
||||
(1, 0, 5, "@char"), (1, 7, 6, "😎😎😎"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colKey
|
||||
assert formats[1].foreground().color().getRgb() == colTag
|
||||
assert formats[2].foreground().color().getRgb() == colOpt
|
||||
assert formats[3].foreground().color().getRgb() == colKey
|
||||
assert formats[4].underlineColor().getRgb() == colErr
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiDocHighlighter_Titles(syntax):
|
||||
"""Test highlighting of titles."""
|
||||
theme = SHARED.theme
|
||||
doc = syntax.document()
|
||||
assert doc is not None
|
||||
|
||||
# Settings
|
||||
syntax._tHandle = T_HANDLE
|
||||
|
||||
colHeadMark = theme.syntaxTheme.headH.getRgb()
|
||||
colHeadText = theme.syntaxTheme.head.getRgb()
|
||||
|
||||
# Ascii
|
||||
doc.setPlainText(
|
||||
"# Heading 1\n\n"
|
||||
"## Heading 2\n\n"
|
||||
"### Heading 3\n\n"
|
||||
"#### Heading 4\n\n"
|
||||
"#! Heading A1\n\n"
|
||||
"##! Heading A2\n\n"
|
||||
"###! Heading A3\n\n"
|
||||
)
|
||||
syntax.rehighlightByType(BLOCK_TITLE)
|
||||
assert maxOrd(doc.toPlainText()) <= 0x7f
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 1, "#"), (0, 1, 10, " Heading 1"),
|
||||
(2, 0, 2, "##"), (2, 2, 10, " Heading 2"),
|
||||
(4, 0, 3, "###"), (4, 3, 10, " Heading 3"),
|
||||
(6, 0, 4, "####"), (6, 4, 10, " Heading 4"),
|
||||
(8, 0, 2, "#!"), (8, 2, 11, " Heading A1"),
|
||||
(10, 0, 3, "##!"), (10, 3, 11, " Heading A2"),
|
||||
(12, 0, 4, "###!"), (12, 4, 11, " Heading A3"),
|
||||
]
|
||||
for i in range(0, len(formats), 2):
|
||||
assert formats[i].foreground().color().getRgb() == colHeadMark
|
||||
assert formats[i+1].foreground().color().getRgb() == colHeadText
|
||||
|
||||
# Unicode <= 0xFFFF
|
||||
doc.setPlainText(
|
||||
"# Ȟǣđ 1\n\n"
|
||||
"## Ȟǣđ 2\n\n"
|
||||
"### Ȟǣđ 3\n\n"
|
||||
"#### Ȟǣđ 4\n\n"
|
||||
"#! Ȟǣđ A1\n\n"
|
||||
"##! Ȟǣđ A2\n\n"
|
||||
"###! Ȟǣđ A3\n\n"
|
||||
)
|
||||
syntax.rehighlightByType(BLOCK_TITLE)
|
||||
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 1, "#"), (0, 1, 6, " Ȟǣđ 1"),
|
||||
(2, 0, 2, "##"), (2, 2, 6, " Ȟǣđ 2"),
|
||||
(4, 0, 3, "###"), (4, 3, 6, " Ȟǣđ 3"),
|
||||
(6, 0, 4, "####"), (6, 4, 6, " Ȟǣđ 4"),
|
||||
(8, 0, 2, "#!"), (8, 2, 7, " Ȟǣđ A1"),
|
||||
(10, 0, 3, "##!"), (10, 3, 7, " Ȟǣđ A2"),
|
||||
(12, 0, 4, "###!"), (12, 4, 7, " Ȟǣđ A3"),
|
||||
]
|
||||
for i in range(0, len(formats), 2):
|
||||
assert formats[i].foreground().color().getRgb() == colHeadMark
|
||||
assert formats[i+1].foreground().color().getRgb() == colHeadText
|
||||
|
||||
# Unicode > 0xFFFF
|
||||
doc.setPlainText(
|
||||
"# 😇😎 1\n\n"
|
||||
"## 😇😎 2\n\n"
|
||||
"### 😇😎 3\n\n"
|
||||
"#### 😇😎 4\n\n"
|
||||
"#! 😇😎 A1\n\n"
|
||||
"##! 😇😎 A2\n\n"
|
||||
"###! 😇😎 A3\n\n"
|
||||
)
|
||||
syntax.rehighlightByType(BLOCK_TITLE)
|
||||
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 1, "#"), (0, 1, 7, " 😇😎 1"),
|
||||
(2, 0, 2, "##"), (2, 2, 7, " 😇😎 2"),
|
||||
(4, 0, 3, "###"), (4, 3, 7, " 😇😎 3"),
|
||||
(6, 0, 4, "####"), (6, 4, 7, " 😇😎 4"),
|
||||
(8, 0, 2, "#!"), (8, 2, 8, " 😇😎 A1"),
|
||||
(10, 0, 3, "##!"), (10, 3, 8, " 😇😎 A2"),
|
||||
(12, 0, 4, "###!"), (12, 4, 8, " 😇😎 A3"),
|
||||
]
|
||||
for i in range(0, len(formats), 2):
|
||||
assert formats[i].foreground().color().getRgb() == colHeadMark
|
||||
assert formats[i+1].foreground().color().getRgb() == colHeadText
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiDocHighlighter_Comments(syntax):
|
||||
"""Test highlighting of comments."""
|
||||
theme = SHARED.theme
|
||||
doc = syntax.document()
|
||||
assert doc is not None
|
||||
|
||||
# Settings
|
||||
syntax._tHandle = T_HANDLE
|
||||
|
||||
colHidden = theme.syntaxTheme.hidden.getRgb()
|
||||
colMod = theme.syntaxTheme.mod.getRgb()
|
||||
colValue = theme.syntaxTheme.val.getRgb()
|
||||
colNote = theme.syntaxTheme.note.getRgb()
|
||||
|
||||
# Ascii
|
||||
doc.setPlainText(
|
||||
"% Plain\n"
|
||||
"%~ Ignored\n"
|
||||
"%Synopsis: Synopsis\n"
|
||||
"%Note.Stuff: Note\n"
|
||||
)
|
||||
syntax.rehighlight()
|
||||
assert maxOrd(doc.toPlainText()) <= 0x7f
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 7, "% Plain"),
|
||||
(1, 0, 10, "%~ Ignored"),
|
||||
(2, 0, 10, "%Synopsis:"), (2, 10, 9, " Synopsis"),
|
||||
(3, 0, 6, "%Note."), (3, 6, 6, "Stuff:"), (3, 12, 5, " Note"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colHidden
|
||||
assert formats[1].foreground().color().getRgb() == colHidden
|
||||
assert formats[1].fontStrikeOut() is True
|
||||
assert formats[2].foreground().color().getRgb() == colMod
|
||||
assert formats[3].foreground().color().getRgb() == colNote
|
||||
assert formats[4].foreground().color().getRgb() == colMod
|
||||
assert formats[5].foreground().color().getRgb() == colValue
|
||||
assert formats[6].foreground().color().getRgb() == colNote
|
||||
|
||||
# Unicode <= 0xFFFF
|
||||
doc.setPlainText(
|
||||
"% Рівнина\n"
|
||||
"%~ Ігноровано\n"
|
||||
"%Synopsis: Синопсис\n"
|
||||
"%Note.Stuff: Примітка\n"
|
||||
)
|
||||
syntax.rehighlight()
|
||||
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 9, "% Рівнина"),
|
||||
(1, 0, 13, "%~ Ігноровано"),
|
||||
(2, 0, 10, "%Synopsis:"), (2, 10, 9, " Синопсис"),
|
||||
(3, 0, 6, "%Note."), (3, 6, 6, "Stuff:"), (3, 12, 9, " Примітка"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colHidden
|
||||
assert formats[1].foreground().color().getRgb() == colHidden
|
||||
assert formats[1].fontStrikeOut() is True
|
||||
assert formats[2].foreground().color().getRgb() == colMod
|
||||
assert formats[3].foreground().color().getRgb() == colNote
|
||||
assert formats[4].foreground().color().getRgb() == colMod
|
||||
assert formats[5].foreground().color().getRgb() == colValue
|
||||
assert formats[6].foreground().color().getRgb() == colNote
|
||||
|
||||
# Unicode > 0xFFFF
|
||||
doc.setPlainText(
|
||||
"% 😎😎\n"
|
||||
"%~ 🙈🙈🙈\n"
|
||||
"%Synopsis: 😍😍😍😍\n"
|
||||
"%Note.Stuff: 😡😡😡😡😡\n"
|
||||
)
|
||||
syntax.rehighlight()
|
||||
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 6, "% 😎😎"),
|
||||
(1, 0, 9, "%~ 🙈🙈🙈"),
|
||||
(2, 0, 10, "%Synopsis:"), (2, 10, 9, " 😍😍😍😍"),
|
||||
(3, 0, 6, "%Note."), (3, 6, 6, "Stuff:"), (3, 12, 11, " 😡😡😡😡😡"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colHidden
|
||||
assert formats[1].foreground().color().getRgb() == colHidden
|
||||
assert formats[1].fontStrikeOut() is True
|
||||
assert formats[2].foreground().color().getRgb() == colMod
|
||||
assert formats[3].foreground().color().getRgb() == colNote
|
||||
assert formats[4].foreground().color().getRgb() == colMod
|
||||
assert formats[5].foreground().color().getRgb() == colValue
|
||||
assert formats[6].foreground().color().getRgb() == colNote
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiDocHighlighter_Special(syntax):
|
||||
"""Test highlighting of special commands."""
|
||||
theme = SHARED.theme
|
||||
doc = syntax.document()
|
||||
assert doc is not None
|
||||
|
||||
# Settings
|
||||
syntax._tHandle = T_HANDLE
|
||||
|
||||
colErr = theme.syntaxTheme.error.getRgb()
|
||||
colCode = theme.syntaxTheme.code.getRgb()
|
||||
colValue = theme.syntaxTheme.val.getRgb()
|
||||
|
||||
# Ascii
|
||||
doc.setPlainText(
|
||||
"[NewPage]\n"
|
||||
"[New Page]\n"
|
||||
"[VSpace]\n"
|
||||
"[VSpace:123]\n"
|
||||
"[VSpace:Meh]\n"
|
||||
)
|
||||
syntax.rehighlight()
|
||||
assert maxOrd(doc.toPlainText()) <= 0x7f
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 9, "[NewPage]"),
|
||||
(1, 0, 10, "[New Page]"),
|
||||
(2, 0, 8, "[VSpace]"),
|
||||
(3, 0, 8, "[VSpace:"), (3, 8, 3, "123"), (3, 11, 1, "]"),
|
||||
(4, 0, 8, "[VSpace:"), (4, 8, 3, "Meh"), (4, 11, 1, "]"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colCode
|
||||
assert formats[1].foreground().color().getRgb() == colCode
|
||||
assert formats[2].foreground().color().getRgb() == colCode
|
||||
assert formats[3].foreground().color().getRgb() == colCode
|
||||
assert formats[4].foreground().color().getRgb() == colValue
|
||||
assert formats[5].foreground().color().getRgb() == colCode
|
||||
assert formats[6].foreground().color().getRgb() == colCode
|
||||
assert formats[7].underlineColor().getRgb() == colErr
|
||||
assert formats[8].foreground().color().getRgb() == colCode
|
||||
|
||||
# Unicode <= 0xFFFF
|
||||
doc.setPlainText(
|
||||
"[NewPage]\n"
|
||||
"[New Page]\n"
|
||||
"[VSpace]\n"
|
||||
"[VSpace:123]\n"
|
||||
"[VSpace:⅘]\n"
|
||||
)
|
||||
syntax.rehighlight()
|
||||
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 9, "[NewPage]"),
|
||||
(1, 0, 10, "[New Page]"),
|
||||
(2, 0, 8, "[VSpace]"),
|
||||
(3, 0, 8, "[VSpace:"), (3, 8, 3, "123"), (3, 11, 1, "]"),
|
||||
(4, 0, 8, "[VSpace:"), (4, 8, 1, "⅘"), (4, 9, 1, "]"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colCode
|
||||
assert formats[1].foreground().color().getRgb() == colCode
|
||||
assert formats[2].foreground().color().getRgb() == colCode
|
||||
assert formats[3].foreground().color().getRgb() == colCode
|
||||
assert formats[4].foreground().color().getRgb() == colValue
|
||||
assert formats[5].foreground().color().getRgb() == colCode
|
||||
assert formats[6].foreground().color().getRgb() == colCode
|
||||
assert formats[7].underlineColor().getRgb() == colErr
|
||||
assert formats[8].foreground().color().getRgb() == colCode
|
||||
|
||||
# Unicode > 0xFFFF
|
||||
doc.setPlainText(
|
||||
"[NewPage]\n"
|
||||
"[New Page]\n"
|
||||
"[VSpace]\n"
|
||||
"[VSpace:123]\n"
|
||||
"[VSpace:🙈🙈]\n"
|
||||
)
|
||||
syntax.rehighlight()
|
||||
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 9, "[NewPage]"),
|
||||
(1, 0, 10, "[New Page]"),
|
||||
(2, 0, 8, "[VSpace]"),
|
||||
(3, 0, 8, "[VSpace:"), (3, 8, 3, "123"), (3, 11, 1, "]"),
|
||||
(4, 0, 8, "[VSpace:"), (4, 8, 4, "🙈🙈"), (4, 12, 1, "]"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colCode
|
||||
assert formats[1].foreground().color().getRgb() == colCode
|
||||
assert formats[2].foreground().color().getRgb() == colCode
|
||||
assert formats[3].foreground().color().getRgb() == colCode
|
||||
assert formats[4].foreground().color().getRgb() == colValue
|
||||
assert formats[5].foreground().color().getRgb() == colCode
|
||||
assert formats[6].foreground().color().getRgb() == colCode
|
||||
assert formats[7].underlineColor().getRgb() == colErr
|
||||
assert formats[8].foreground().color().getRgb() == colCode
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiDocHighlighter_Text(monkeypatch, syntax):
|
||||
"""Test highlighting of text."""
|
||||
theme = SHARED.theme
|
||||
doc = syntax.document()
|
||||
assert doc is not None
|
||||
|
||||
# Settings
|
||||
syntax._tHandle = T_HANDLE
|
||||
syntax._isNovel = True
|
||||
syntax.setSpellCheck(True)
|
||||
monkeypatch.setattr(SHARED.spelling, "checkWord", lambda *a: False)
|
||||
|
||||
colHidden = theme.syntaxTheme.hidden.getRgb()
|
||||
colEmph = theme.syntaxTheme.emph.getRgb()
|
||||
colLink = theme.syntaxTheme.link.getRgb()
|
||||
colSpell = theme.syntaxTheme.spell.getRgb()
|
||||
colCode = theme.syntaxTheme.code.getRgb()
|
||||
colDialogue = theme.syntaxTheme.dialN.getRgb()
|
||||
colAltDialogue = theme.syntaxTheme.dialA.getRgb()
|
||||
|
||||
# Ascii
|
||||
doc.setPlainText(
|
||||
"Text **bold** text _italic_ text ~~strike~~ text [b]bold[/b], http://example.com\n\n"
|
||||
)
|
||||
syntax.rehighlight()
|
||||
assert maxOrd(doc.toPlainText()) <= 0x7f
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 4, "Text"),
|
||||
(0, 5, 2, "**"), (0, 7, 4, "bold"), (0, 11, 2, "**"),
|
||||
(0, 14, 4, "text"),
|
||||
(0, 19, 1, "_"), (0, 20, 6, "italic"), (0, 26, 1, "_"),
|
||||
(0, 28, 4, "text"),
|
||||
(0, 33, 2, "~~"), (0, 35, 6, "strike"), (0, 41, 2, "~~"),
|
||||
(0, 44, 4, "text"),
|
||||
(0, 49, 3, "[b]"), (0, 52, 4, "bold"), (0, 56, 4, "[/b]"),
|
||||
(0, 62, 18, "http://example.com"),
|
||||
]
|
||||
assert formats[0].underlineColor().getRgb() == colSpell # Text
|
||||
assert formats[1].foreground().color().getRgb() == colHidden # **
|
||||
assert formats[2].foreground().color().getRgb() == colEmph # bold
|
||||
assert formats[2].underlineColor().getRgb() == colSpell
|
||||
assert formats[3].foreground().color().getRgb() == colHidden # **
|
||||
assert formats[4].underlineColor().getRgb() == colSpell # text
|
||||
assert formats[5].foreground().color().getRgb() == colHidden # _
|
||||
assert formats[6].foreground().color().getRgb() == colEmph # italic
|
||||
assert formats[6].underlineColor().getRgb() == colSpell
|
||||
assert formats[7].foreground().color().getRgb() == colHidden # _
|
||||
assert formats[8].underlineColor().getRgb() == colSpell # text
|
||||
assert formats[9].foreground().color().getRgb() == colHidden # ~~
|
||||
assert formats[10].foreground().color().getRgb() == colHidden # strike
|
||||
assert formats[10].fontStrikeOut() is True
|
||||
assert formats[10].underlineColor().getRgb() == colSpell
|
||||
assert formats[11].foreground().color().getRgb() == colHidden # ~~
|
||||
assert formats[12].underlineColor().getRgb() == colSpell # text
|
||||
assert formats[13].foreground().color().getRgb() == colCode # [b]
|
||||
assert formats[14].underlineColor().getRgb() == colSpell # bold
|
||||
assert formats[15].foreground().color().getRgb() == colCode # [/b]
|
||||
assert formats[16].foreground().color().getRgb() == colLink # http://example.com
|
||||
|
||||
# Spell Check
|
||||
data = doc.findBlockByNumber(0).userData()
|
||||
assert isinstance(data, TextBlockData)
|
||||
assert data.metaData == [(62, 80, "http://example.com", "url")]
|
||||
assert data.spellErrors == [
|
||||
(0, 4, "Text"), (7, 11, "bold"),
|
||||
(14, 18, "text"), (20, 26, "italic"),
|
||||
(28, 32, "text"), (35, 41, "strike"),
|
||||
(44, 48, "text"), (52, 56, "bold"),
|
||||
]
|
||||
|
||||
# Unicode <= 0xFFFF
|
||||
doc.setPlainText(
|
||||
"\u201cDialogue,\u201d and then ::dialogue::, http://example.com\n\n"
|
||||
)
|
||||
syntax.rehighlight()
|
||||
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 1, "\u201c"), (0, 1, 8, "Dialogue"), (0, 9, 2, ",\u201d"),
|
||||
(0, 12, 3, "and"), (0, 16, 4, "then"),
|
||||
(0, 21, 2, "::"), (0, 23, 8, "dialogue"), (0, 31, 2, "::"),
|
||||
(0, 35, 18, "http://example.com"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colDialogue # Quote
|
||||
assert formats[1].foreground().color().getRgb() == colDialogue # Dialogue
|
||||
assert formats[1].underlineColor().getRgb() == colSpell
|
||||
assert formats[2].foreground().color().getRgb() == colDialogue # Quote
|
||||
assert formats[3].underlineColor().getRgb() == colSpell # and
|
||||
assert formats[4].underlineColor().getRgb() == colSpell # then
|
||||
assert formats[5].foreground().color().getRgb() == colAltDialogue # ::
|
||||
assert formats[6].foreground().color().getRgb() == colAltDialogue # dialogue
|
||||
assert formats[6].underlineColor().getRgb() == colSpell
|
||||
assert formats[7].foreground().color().getRgb() == colAltDialogue # ::
|
||||
assert formats[8].foreground().color().getRgb() == colLink # http://example.com
|
||||
|
||||
# Spell Check
|
||||
data = doc.findBlockByNumber(0).userData()
|
||||
assert isinstance(data, TextBlockData)
|
||||
assert data.metaData == [(35, 53, "http://example.com", "url")]
|
||||
assert data.spellErrors == [
|
||||
(1, 9, "Dialogue"), (12, 15, "and"),
|
||||
(16, 20, "then"), (23, 31, "dialogue"),
|
||||
]
|
||||
|
||||
# Unicode > 0xFFFF
|
||||
doc.setPlainText(
|
||||
"\u201c😁 Grinning 😁,\u201d and then ::🙊 shush 🙊::, http://example.com\n\n"
|
||||
)
|
||||
syntax.rehighlight()
|
||||
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
|
||||
|
||||
pieces, formats = getFragments(syntax)
|
||||
assert pieces == [
|
||||
(0, 0, 4, "\u201c😁 "), (0, 4, 8, "Grinning"), (0, 12, 5, " 😁,\u201d"),
|
||||
(0, 18, 3, "and"), (0, 22, 4, "then"),
|
||||
(0, 27, 5, "::🙊 "), (0, 32, 5, "shush"), (0, 37, 5, " 🙊::"),
|
||||
(0, 44, 18, "http://example.com"),
|
||||
]
|
||||
assert formats[0].foreground().color().getRgb() == colDialogue # Quote😁
|
||||
assert formats[1].foreground().color().getRgb() == colDialogue # Grinning
|
||||
assert formats[1].underlineColor().getRgb() == colSpell
|
||||
assert formats[2].foreground().color().getRgb() == colDialogue # 😁Quote
|
||||
assert formats[3].underlineColor().getRgb() == colSpell # and
|
||||
assert formats[4].underlineColor().getRgb() == colSpell # then
|
||||
assert formats[5].foreground().color().getRgb() == colAltDialogue # ::🙊
|
||||
assert formats[6].foreground().color().getRgb() == colAltDialogue # shush
|
||||
assert formats[6].underlineColor().getRgb() == colSpell
|
||||
assert formats[7].foreground().color().getRgb() == colAltDialogue # 🙊::
|
||||
assert formats[8].foreground().color().getRgb() == colLink # http://example.com
|
||||
|
||||
# Spell Check
|
||||
data = doc.findBlockByNumber(0).userData()
|
||||
assert isinstance(data, TextBlockData)
|
||||
assert data.metaData == [(40, 58, "http://example.com", "url")]
|
||||
assert data.spellErrors == [
|
||||
(4, 12, "Grinning"), (18, 21, "and"),
|
||||
(22, 26, "then"), (32, 37, "shush"),
|
||||
]
|
||||
@@ -17,7 +17,7 @@ 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
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
@@ -223,17 +223,23 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
CONFIG.showFullPath = True
|
||||
|
||||
# Document footer show/hide synopsis
|
||||
assert nwGUI.viewDocument("f96ec11c6a3da") is True
|
||||
nwGUI.viewDocument("f96ec11c6a3da")
|
||||
assert len(docViewer.toPlainText()) == 4314
|
||||
docViewer.docFooter._doToggleSynopsis(False)
|
||||
assert len(docViewer.toPlainText()) == 4098
|
||||
|
||||
# Document footer show/hide comments
|
||||
assert nwGUI.viewDocument("846352075de7d") is True
|
||||
nwGUI.viewDocument("846352075de7d")
|
||||
assert len(docViewer.toPlainText()) == 683
|
||||
docViewer.docFooter._doToggleComments(False)
|
||||
assert len(docViewer.toPlainText()) == 634
|
||||
|
||||
# Document footer show/hide notes
|
||||
nwGUI.viewDocument("88d59a277361b")
|
||||
assert len(docViewer.toPlainText()) == 900
|
||||
docViewer.docFooter._doToggleNotes(False)
|
||||
assert len(docViewer.toPlainText()) == 871
|
||||
|
||||
# Crash the HTML rendering
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(ToQTextDocument, "doConvert", causeException)
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 pytest
|
||||
@@ -224,7 +224,7 @@ def testGuiViewerPanel_Tags(qtbot, monkeypatch, caplog, nwGUI, projPath, mockRnd
|
||||
|
||||
# Update Labels
|
||||
assert charTab.topLevelItem(0).text(charTab.C_IMPORT) == "New"
|
||||
SHARED.project.data.itemImport.add(C.iNew, "Stuff", (100, 100, 100), "SQUARE", 0)
|
||||
SHARED.project.data.itemImport.add(C.iNew, "Stuff", "#646464", "SQUARE", 0)
|
||||
viewPanel.updateStatusLabels("i")
|
||||
assert charTab.topLevelItem(0).text(charTab.C_IMPORT) == "Stuff"
|
||||
|
||||
|
||||
@@ -17,13 +17,14 @@ 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 unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -32,9 +33,10 @@ from PyQt6.QtGui import QPalette
|
||||
from PyQt6.QtWidgets import QInputDialog, QMessageBox
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||
from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwView
|
||||
from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwTheme, nwView
|
||||
from novelwriter.gui.doceditor import GuiDocEditor
|
||||
from novelwriter.gui.noveltree import GuiNovelView
|
||||
from novelwriter.gui.outline import GuiOutlineView
|
||||
@@ -179,21 +181,35 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
@pytest.mark.gui
|
||||
def testGuiMain_UpdateTheme(qtbot, nwGUI):
|
||||
"""Test updating the theme in the GUI."""
|
||||
mainTheme = SHARED.theme
|
||||
CONFIG.guiTheme = "default_dark"
|
||||
CONFIG.guiSyntax = "default_dark"
|
||||
mainTheme.loadTheme()
|
||||
mainTheme.loadSyntax()
|
||||
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(False, True, False, False)
|
||||
nwGUI._processConfigChanges(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
|
||||
|
||||
# Through change event
|
||||
event = Mock()
|
||||
event.type.return_value = 210
|
||||
CONFIG.themeMode = nwTheme.DARK
|
||||
nwGUI.changeEvent(event)
|
||||
assert theme.isDarkTheme is True
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
|
||||
@@ -251,8 +267,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
# Change some settings
|
||||
CONFIG.hideHScroll = True
|
||||
CONFIG.hideVScroll = True
|
||||
CONFIG.autoScrollPos = 80
|
||||
CONFIG.autoScroll = True
|
||||
CONFIG.autoScroll = False
|
||||
|
||||
# Add a Character File
|
||||
nwGUI._changeView(nwView.PROJECT)
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 sys
|
||||
|
||||
@@ -17,7 +17,7 @@ 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
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
@@ -68,154 +68,155 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
assert docEditor.docAction(nwDocAction.COPY) is False
|
||||
|
||||
assert nwGUI.openProject(prjLipsum) is True
|
||||
x = 72
|
||||
|
||||
# Split By Chapter
|
||||
assert nwGUI.openDocument("4c4f28287af27") is True
|
||||
docEditor.setCursorPosition(57)
|
||||
cleanText = docEditor.getText()[54:101]
|
||||
docEditor.setCursorPosition(x+3)
|
||||
cleanText = docEditor.getText()[x:x+47]
|
||||
|
||||
# Bold
|
||||
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "**Pellentesque** nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:105] == fmtStr
|
||||
assert docEditor.getText()[x:x+51] == fmtStr
|
||||
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Italic
|
||||
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "_Pellentesque_ nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:103] == fmtStr
|
||||
assert docEditor.getText()[x:x+49] == fmtStr
|
||||
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Strikethrough
|
||||
mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "~~Pellentesque~~ nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:105] == fmtStr
|
||||
assert docEditor.getText()[x:x+51] == fmtStr
|
||||
mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Should get us back to plain
|
||||
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
|
||||
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
|
||||
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
|
||||
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Double Quotes
|
||||
mainMenu.aFmtDQuote.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "“Pellentesque” nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:103] == fmtStr
|
||||
assert docEditor.getText()[x:x+49] == fmtStr
|
||||
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Single Quotes
|
||||
mainMenu.aFmtSQuote.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "‘Pellentesque’ nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:103] == fmtStr
|
||||
assert docEditor.getText()[x:x+49] == fmtStr
|
||||
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Block Formats
|
||||
# =============
|
||||
# cSpell:ignore Pellentesque erat nulla posuere commodo
|
||||
docEditor.setCursorPosition(57)
|
||||
docEditor.setCursorPosition(x+3)
|
||||
|
||||
# Header 1
|
||||
mainMenu.aFmtHead1.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "# Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:103] == fmtStr
|
||||
assert docEditor.getText()[x:x+49] == fmtStr
|
||||
|
||||
# Header 2
|
||||
mainMenu.aFmtHead2.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "## Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:104] == fmtStr
|
||||
assert docEditor.getText()[x:x+50] == fmtStr
|
||||
|
||||
# Header 3
|
||||
mainMenu.aFmtHead3.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "### Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:105] == fmtStr
|
||||
assert docEditor.getText()[x:x+51] == fmtStr
|
||||
|
||||
# Header 4
|
||||
mainMenu.aFmtHead4.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "#### Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:106] == fmtStr
|
||||
assert docEditor.getText()[x:x+52] == fmtStr
|
||||
|
||||
# Title Format
|
||||
mainMenu.aFmtTitle.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "#! Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:104] == fmtStr
|
||||
assert docEditor.getText()[x:x+50] == fmtStr
|
||||
|
||||
# Unnumbered Chapter
|
||||
mainMenu.aFmtUnNum.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "##! Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:105] == fmtStr
|
||||
assert docEditor.getText()[x:x+51] == fmtStr
|
||||
|
||||
# Hard Scene
|
||||
mainMenu.aFmtHardSc.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "###! Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:106] == fmtStr
|
||||
assert docEditor.getText()[x:x+52] == fmtStr
|
||||
|
||||
# Clear Format
|
||||
mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Comment On
|
||||
mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "% Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:103] == fmtStr
|
||||
assert docEditor.getText()[x:x+49] == fmtStr
|
||||
|
||||
# Comment Off
|
||||
mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Check comment with no space before text
|
||||
docEditor.setCursorPosition(54)
|
||||
docEditor.setCursorPosition(x)
|
||||
docEditor.insertText("%")
|
||||
fmtStr = "%Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:102] == fmtStr
|
||||
assert docEditor.getText()[x:x+48] == fmtStr
|
||||
|
||||
mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Undo/Redo
|
||||
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
|
||||
fmtStr = "%Pellentesque nec erat ut nulla posuere commodo."
|
||||
assert docEditor.getText()[54:102] == fmtStr
|
||||
assert docEditor.getText()[x:x+48] == fmtStr
|
||||
mainMenu.aEditRedo.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:101] == cleanText
|
||||
assert docEditor.getText()[x:x+47] == cleanText
|
||||
|
||||
# Cut, Copy and Paste
|
||||
docEditor.setCursorPosition(54)
|
||||
docEditor.setCursorPosition(x)
|
||||
docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
|
||||
|
||||
mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:104] == (
|
||||
assert docEditor.getText()[x:x+50] == (
|
||||
" nec erat ut nulla posuere commodo. Curabitur nisi"
|
||||
)
|
||||
|
||||
mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:104] == (
|
||||
assert docEditor.getText()[x:x+50] == (
|
||||
"Pellentesque nec erat ut nulla posuere commodo. Cu"
|
||||
)
|
||||
|
||||
docEditor.setCursorPosition(54)
|
||||
docEditor.setCursorPosition(x)
|
||||
docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
|
||||
|
||||
mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:104] == (
|
||||
assert docEditor.getText()[x:x+50] == (
|
||||
"Pellentesque nec erat ut nulla posuere commodo. Cu"
|
||||
)
|
||||
|
||||
docEditor.setCursorPosition(54)
|
||||
docEditor.setCursorPosition(x)
|
||||
mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
|
||||
assert docEditor.getText()[54:104] == (
|
||||
assert docEditor.getText()[x:x+50] == (
|
||||
"PellentesquePellentesque nec erat ut nulla posuere"
|
||||
)
|
||||
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
|
||||
|
||||
# Select Paragraph/All
|
||||
docEditor.setCursorPosition(57)
|
||||
docEditor.setCursorPosition(x+3)
|
||||
mainMenu.aSelectPar.activate(QAction.ActionEvent.Trigger)
|
||||
cursor = docEditor.textCursor()
|
||||
assert cursor.selectedText() == (
|
||||
@@ -228,10 +229,10 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
"nunc lacus, imperdiet nec posuere ac, interdum non lectus."
|
||||
)
|
||||
|
||||
docEditor.setCursorPosition(57)
|
||||
docEditor.setCursorPosition(x+3)
|
||||
mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
|
||||
cursor = docEditor.textCursor()
|
||||
assert len(cursor.selectedText()) == 1910
|
||||
assert len(cursor.selectedText()) == 1928
|
||||
|
||||
# Clear the Text
|
||||
docEditor.clear()
|
||||
|
||||
@@ -17,7 +17,7 @@ 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
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 time
|
||||
|
||||
@@ -17,7 +17,7 @@ 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
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
@@ -1454,7 +1454,7 @@ def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
nwNewCharacter = project.tree[hNewCharacter]
|
||||
assert nwNewCharacter is not None
|
||||
assert nwNewCharacter.itemName == "Note"
|
||||
assert project.storage.getDocument(hNewCharacter).readDocument() == "# Jane\n\n@tag: Jane\n\n"
|
||||
assert project.storage.getDocument(hNewCharacter).readDocument() == "# Note\n\n@tag: Jane\n\n"
|
||||
|
||||
# Clearing the menu and rebuilding it should work
|
||||
projBar.mTemplates.clearMenu()
|
||||
|
||||
@@ -17,7 +17,7 @@ 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
|
||||
|
||||
from time import time
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
novelWriter – Side Bar Class Tester
|
||||
===================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter 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 pytest
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwLabels
|
||||
from novelwriter.enum import nwTheme
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiSideBar_CycleColourTheme(nwGUI):
|
||||
"""Test theme cycle feature on the side bar."""
|
||||
CONFIG.themeMode = nwTheme.AUTO
|
||||
sidebar = nwGUI.sideBar
|
||||
sidebar.mainGui.checkThemeUpdate = lambda *a: None
|
||||
|
||||
# Run 3 Cycles
|
||||
for _ in range(3):
|
||||
|
||||
# Cycle Light
|
||||
sidebar._cycleColurTheme()
|
||||
assert CONFIG.themeMode == nwTheme.LIGHT
|
||||
assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.LIGHT]
|
||||
|
||||
# Cycle Dark
|
||||
sidebar._cycleColurTheme()
|
||||
assert CONFIG.themeMode == nwTheme.DARK
|
||||
assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.DARK]
|
||||
|
||||
# Cycle Auto
|
||||
sidebar._cycleColurTheme()
|
||||
assert CONFIG.themeMode == nwTheme.AUTO
|
||||
assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.AUTO]
|
||||
@@ -17,7 +17,7 @@ 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 time
|
||||
@@ -31,7 +31,7 @@ from tests.tools import C, buildTestProject
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
"""Test the the various features of the status bar."""
|
||||
"""Test the various features of the status bar."""
|
||||
buildTestProject(nwGUI, projPath)
|
||||
cHandle = SHARED.project.newFile("A Note", C.hCharRoot)
|
||||
newDoc = SHARED.project.storage.getDocument(cHandle)
|
||||
|
||||
+466
-297
@@ -17,347 +17,405 @@ 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 sys
|
||||
import json
|
||||
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QColor, QFont, QFontDatabase, QIcon, QPalette, QPixmap, QStyleHints
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import NWConfigParser
|
||||
from novelwriter.config import DEF_GUI
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS
|
||||
from novelwriter.constants import nwLabels
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||
from novelwriter.gui.theme import _listConf
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
|
||||
from novelwriter.gui.theme import (
|
||||
STYLES_BIG_TOOLBUTTON, STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON, GuiTheme,
|
||||
ThemeMeta, _listContent
|
||||
)
|
||||
|
||||
from tests.mocked import causeOSError
|
||||
from tests.tools import writeFile
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
|
||||
"""Test the theme class init."""
|
||||
mainTheme = SHARED.theme
|
||||
def testGuiTheme_ParseColor():
|
||||
"""Test the colour parsing."""
|
||||
theme = GuiTheme()
|
||||
|
||||
# Methods
|
||||
# =======
|
||||
# Pre-Populate
|
||||
theme._qColors["red"] = QColor(255, 0, 0)
|
||||
theme._qColors["green"] = QColor(0, 255, 0)
|
||||
theme._qColors["blue"] = QColor(0, 0, 255)
|
||||
theme._qColors["grey"] = QColor(127, 127, 127)
|
||||
|
||||
mSize = mainTheme.getTextWidth("m")
|
||||
assert mSize > 0
|
||||
assert mainTheme.getTextWidth("m", mainTheme.guiFont) == mSize
|
||||
# By Name
|
||||
assert theme.parseColor("red").getRgb() == (255, 0, 0, 255)
|
||||
assert theme.parseColor("green").getRgb() == (0, 255, 0, 255)
|
||||
assert theme.parseColor("blue").getRgb() == (0, 0, 255, 255)
|
||||
assert theme.parseColor("bob").getRgb() == (0, 0, 0, 255)
|
||||
|
||||
# Scan for Themes
|
||||
# ===============
|
||||
# CSS Format
|
||||
assert theme.parseColor("#ff0000").getRgb() == (255, 0, 0, 255)
|
||||
assert theme.parseColor("#ff00007f").getRgb() == (255, 0, 0, 127)
|
||||
assert theme.parseColor("#ff00").getRgb() == (0, 0, 0, 255) # Too short -> ignored
|
||||
assert theme.parseColor("#ff00007f15").getRgb() == (0, 0, 0, 255) # Too long -> ignored
|
||||
|
||||
result = {}
|
||||
_listConf({}, Path("not_a_path"), ".conf")
|
||||
assert result == {}
|
||||
# Name + Alpha
|
||||
assert theme.parseColor("red:255").getRgb() == (255, 0, 0, 255)
|
||||
assert theme.parseColor("red:127").getRgb() == (255, 0, 0, 127)
|
||||
assert theme.parseColor("red:512").getRgb() == (255, 0, 0, 255) # Value truncated
|
||||
|
||||
themeOne = tstPaths.cnfDir / "themes" / "themeone.conf"
|
||||
themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf"
|
||||
writeFile(themeOne, "# Stuff")
|
||||
writeFile(themeTwo, "# Stuff")
|
||||
# Name + Lighter
|
||||
assert theme.parseColor("grey:L100").getRgb() == (127, 127, 127, 255)
|
||||
assert theme.parseColor("grey:L150").getRgb() == (190, 190, 190, 255)
|
||||
assert theme.parseColor("grey:L50").getRgb() == (63, 63, 63, 255)
|
||||
|
||||
_listConf(result, tstPaths.cnfDir / "themes", ".conf")
|
||||
assert result["themeone"] == themeOne
|
||||
assert result["themetwo"] == themeTwo
|
||||
# Name + Darker
|
||||
assert theme.parseColor("grey:D100").getRgb() == (127, 127, 127, 255)
|
||||
assert theme.parseColor("grey:D150").getRgb() == (85, 85, 85, 255)
|
||||
assert theme.parseColor("grey:D50").getRgb() == (254, 254, 254, 255)
|
||||
|
||||
# Parse Colours
|
||||
# =============
|
||||
|
||||
parser = NWConfigParser()
|
||||
parser["Palette"] = {
|
||||
"colour1": "100, 150, 200", # Valid
|
||||
"colour2": "100, 150, 200, 250", # With alpha
|
||||
"colour3": "100, 150, 200, 250, 300", # Too many values
|
||||
"colour4": "250, 250", # Missing blue
|
||||
"colour5": "-10, 127, 300", # Invalid red and blue
|
||||
"colour6": "bob, 127, 255", # Invalid red
|
||||
}
|
||||
|
||||
# Test the parser for several valid and invalid values
|
||||
assert mainTheme._parseColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255)
|
||||
assert mainTheme._parseColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250)
|
||||
assert mainTheme._parseColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250)
|
||||
assert mainTheme._parseColor(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255)
|
||||
assert mainTheme._parseColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0)
|
||||
assert mainTheme._parseColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255)
|
||||
|
||||
# The palette should load with the parsed values
|
||||
mainTheme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window)
|
||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255)
|
||||
mainTheme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window)
|
||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
|
||||
mainTheme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window)
|
||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
|
||||
mainTheme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window)
|
||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255)
|
||||
mainTheme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window)
|
||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0)
|
||||
mainTheme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window)
|
||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255)
|
||||
|
||||
# Non-existing value should return default colour
|
||||
mainTheme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window)
|
||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255)
|
||||
|
||||
# qtbot.stop()
|
||||
# Values
|
||||
assert theme.parseColor("255, 0, 0").getRgb() == (255, 0, 0, 255)
|
||||
assert theme.parseColor("255, 0, 0, 255").getRgb() == (255, 0, 0, 255)
|
||||
assert theme.parseColor("255, 0, 0, 127").getRgb() == (255, 0, 0, 127)
|
||||
assert theme.parseColor("255, 0, 0, 127, 42").getRgb() == (255, 0, 0, 127) # Truncated
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
|
||||
"""Test the theme part of the class."""
|
||||
mainTheme = SHARED.theme
|
||||
def testGuiTheme_ScanThemes(monkeypatch):
|
||||
"""Test the themes scanning."""
|
||||
theme = GuiTheme()
|
||||
|
||||
# List Themes
|
||||
# Load built-in themes
|
||||
files = []
|
||||
_listContent(files, CONFIG.assetPath("themes"), ".conf")
|
||||
assert len(files) > 0
|
||||
|
||||
# Block reading theme files
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(ConfigParser, "read", causeOSError)
|
||||
theme._scanThemes(files)
|
||||
assert theme.colourThemes == {}
|
||||
|
||||
# Read all themes correctly
|
||||
theme._scanThemes(files)
|
||||
assert len(theme.colourThemes) > 0
|
||||
|
||||
dark = theme.colourThemes[DEF_GUI_DARK]
|
||||
light = theme.colourThemes[DEF_GUI_LIGHT]
|
||||
|
||||
assert dark.name == "Default Dark Theme"
|
||||
assert dark.dark is True
|
||||
assert light.name == "Default Light Theme"
|
||||
assert light.dark is False
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_LoadThemes(monkeypatch):
|
||||
"""Test loading themes."""
|
||||
theme = GuiTheme()
|
||||
theme.iconCache = MagicMock()
|
||||
CONFIG.lightTheme = DEF_GUI_LIGHT
|
||||
CONFIG.darkTheme = DEF_GUI_DARK
|
||||
|
||||
# Load built-in themes
|
||||
files = []
|
||||
_listContent(files, CONFIG.assetPath("themes"), ".conf")
|
||||
theme._scanThemes(files)
|
||||
assert DEF_GUI_LIGHT in theme._allThemes
|
||||
assert DEF_GUI_DARK in theme._allThemes
|
||||
assert theme._currentTheme == ""
|
||||
|
||||
# Load light theme
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
theme.loadTheme()
|
||||
assert theme._currentTheme == DEF_GUI_LIGHT
|
||||
|
||||
# Load dark theme
|
||||
CONFIG.themeMode = nwTheme.DARK
|
||||
theme.loadTheme()
|
||||
assert theme._currentTheme == DEF_GUI_DARK
|
||||
|
||||
# Let auto switch back to light, then dark
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(CONFIG, "verQtValue", 0x060500)
|
||||
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light)
|
||||
CONFIG.themeMode = nwTheme.AUTO
|
||||
theme.loadTheme()
|
||||
assert theme._currentTheme == DEF_GUI_LIGHT
|
||||
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(CONFIG, "verQtValue", 0x060500)
|
||||
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark)
|
||||
CONFIG.themeMode = nwTheme.AUTO
|
||||
theme.loadTheme()
|
||||
assert theme._currentTheme == DEF_GUI_DARK
|
||||
|
||||
# Error Cases
|
||||
# ===========
|
||||
|
||||
# Block the reading of the files
|
||||
# Invalid light theme
|
||||
CONFIG.lightTheme = "not_a_theme"
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
theme.loadTheme()
|
||||
assert theme._currentTheme == DEF_GUI_LIGHT
|
||||
|
||||
# Invalid dark theme
|
||||
CONFIG.darkTheme = "not_a_theme"
|
||||
CONFIG.themeMode = nwTheme.DARK
|
||||
theme.loadTheme()
|
||||
assert theme._currentTheme == DEF_GUI_DARK
|
||||
|
||||
# Clear meta and check exit early cases
|
||||
theme._meta = ThemeMeta()
|
||||
assert theme._meta.name == ""
|
||||
|
||||
# Reload dark should not load anything
|
||||
CONFIG.darkTheme = DEF_GUI_DARK
|
||||
CONFIG.themeMode = nwTheme.DARK
|
||||
theme.loadTheme()
|
||||
assert theme._meta.name == ""
|
||||
|
||||
# Force reload, but fail parsing
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert mainTheme.listThemes() == []
|
||||
mp.setattr(ConfigParser, "read", causeOSError)
|
||||
CONFIG.darkTheme = DEF_GUI_DARK
|
||||
CONFIG.themeMode = nwTheme.DARK
|
||||
theme.loadTheme(force=True)
|
||||
assert theme._meta.name == ""
|
||||
|
||||
# Load the theme info, default themes first
|
||||
themesList = mainTheme.listThemes()
|
||||
assert themesList[0] == ("default_dark", "Default Dark Theme")
|
||||
assert themesList[1] == ("default_light", "Default Light Theme")
|
||||
assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night")
|
||||
assert themesList[3] == ("dracula", "Dracula")
|
||||
# Invalid theme, and defaults are missing
|
||||
del theme._allThemes[DEF_GUI_DARK]
|
||||
del theme._allThemes[DEF_GUI_LIGHT]
|
||||
|
||||
# A second call should returned the cached list
|
||||
assert mainTheme.listThemes() == mainTheme._themeList
|
||||
CONFIG.lightTheme = "not_a_theme"
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
theme.loadTheme(force=True)
|
||||
assert theme._meta.name == ""
|
||||
|
||||
# Check handling of broken theme settings
|
||||
CONFIG.guiTheme = "not_a_theme"
|
||||
availThemes = mainTheme._availThemes
|
||||
mainTheme._availThemes = {}
|
||||
assert mainTheme.loadTheme() is False
|
||||
mainTheme._availThemes = availThemes
|
||||
CONFIG.darkTheme = "not_a_theme"
|
||||
CONFIG.themeMode = nwTheme.DARK
|
||||
theme.loadTheme(force=True)
|
||||
assert theme._meta.name == ""
|
||||
|
||||
# Check handling of unreadable file
|
||||
CONFIG.guiTheme = DEF_GUI
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert mainTheme.loadTheme() is False
|
||||
|
||||
# Load Default Theme
|
||||
# ==================
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_SpecialColors(tstPaths):
|
||||
"""Test handling special cases for colours."""
|
||||
theme = GuiTheme()
|
||||
theme.iconCache = MagicMock()
|
||||
|
||||
if sys.platform != "win32":
|
||||
# Set a mock colour for the window background
|
||||
mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0)
|
||||
|
||||
# Load the default theme
|
||||
CONFIG.guiTheme = DEF_GUI
|
||||
assert mainTheme.loadTheme() is True
|
||||
|
||||
# This should load a standard palette
|
||||
wCol = QPalette().color(QPalette.ColorRole.Window).getRgb()
|
||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol
|
||||
|
||||
# Mock Dark Theme
|
||||
# ===============
|
||||
|
||||
mockTheme: Path = tstPaths.cnfDir / "themes" / "test.conf"
|
||||
mockTheme.write_text((
|
||||
testTheme: Path = tstPaths.cnfDir / "themes" / "test.conf"
|
||||
testTheme.write_text((
|
||||
"[Main]\n"
|
||||
"name = Test\n"
|
||||
"mode = light\n"
|
||||
"\n"
|
||||
"[Base]\n"
|
||||
"default = #cccccc\n"
|
||||
"faded = #949494\n"
|
||||
"red = #ff0000\n"
|
||||
"orange = #ff7f00\n"
|
||||
"yellow = #ffff00\n"
|
||||
"green = #00ff00\n"
|
||||
"cyan = #00ffff\n"
|
||||
"blue = #0000ff\n"
|
||||
"purple = #ff00ff\n"
|
||||
"\n"
|
||||
"[Project]\n"
|
||||
"root = blue\n"
|
||||
"folder = yellow\n"
|
||||
"file = default\n"
|
||||
"title = green\n"
|
||||
"chapter = red\n"
|
||||
"scene = blue\n"
|
||||
"note = yellow\n"
|
||||
"\n"
|
||||
"[Palette]\n"
|
||||
"window = 0, 0, 0\n"
|
||||
"text = 255, 255, 255\n"
|
||||
"window = #000000\n"
|
||||
"text = #ffffff\n"
|
||||
), encoding="utf-8")
|
||||
mainTheme._availThemes["test"] = mockTheme
|
||||
theme._scanThemes([testTheme])
|
||||
assert len(theme.colourThemes) == 1
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
CONFIG.lightTheme = "test"
|
||||
|
||||
CONFIG.guiTheme = "test"
|
||||
assert mainTheme.loadTheme() is True
|
||||
assert mainTheme._guiPalette.window().color().getRgb() == (0, 0, 0, 255)
|
||||
assert mainTheme._guiPalette.text().color().getRgb() == (255, 255, 255, 255)
|
||||
assert mainTheme._guiPalette.light().color().getRgb() == (57, 57, 57, 255)
|
||||
assert mainTheme.isDarkTheme is True
|
||||
# Load theme
|
||||
theme.loadTheme()
|
||||
|
||||
# Load Default Light Theme
|
||||
# ========================
|
||||
# Since window is black, a lighter version should be generated
|
||||
assert theme._guiPalette.light().color().getRgb() == (57, 57, 57, 255)
|
||||
|
||||
CONFIG.guiTheme = "default_light"
|
||||
assert mainTheme.loadTheme() is True
|
||||
# Reload with project override to red
|
||||
CONFIG.iconColTree = "red"
|
||||
CONFIG.iconColDocs = True
|
||||
theme.loadTheme(force=True)
|
||||
|
||||
# Check a few values
|
||||
assert mainTheme._guiPalette.color(
|
||||
QPalette.ColorRole.Window
|
||||
).getRgb() == (239, 239, 239, 255)
|
||||
assert mainTheme._guiPalette.color(
|
||||
QPalette.ColorRole.WindowText
|
||||
).getRgb() == (0, 0, 0, 255)
|
||||
assert mainTheme._guiPalette.color(
|
||||
QPalette.ColorRole.Base
|
||||
).getRgb() == (255, 255, 255, 255)
|
||||
assert mainTheme._guiPalette.color(
|
||||
QPalette.ColorRole.AlternateBase
|
||||
).getRgb() == (224, 224, 224, 255)
|
||||
assert theme.getBaseColor("root").getRgb() == (255, 0, 0, 255)
|
||||
assert theme.getBaseColor("folder").getRgb() == (255, 0, 0, 255)
|
||||
assert theme.getBaseColor("file").getRgb() == (204, 204, 204, 255)
|
||||
assert theme.getBaseColor("title").getRgb() == (0, 255, 0, 255)
|
||||
assert theme.getBaseColor("chapter").getRgb() == (255, 0, 0, 255)
|
||||
assert theme.getBaseColor("scene").getRgb() == (0, 0, 255, 255)
|
||||
assert theme.getBaseColor("note").getRgb() == (255, 255, 0, 255)
|
||||
|
||||
# Load Default Dark Theme
|
||||
# =======================
|
||||
|
||||
CONFIG.guiTheme = "default_dark"
|
||||
assert mainTheme.loadTheme() is True
|
||||
|
||||
# Check a few values
|
||||
assert mainTheme._guiPalette.color(
|
||||
QPalette.ColorRole.Window).getRgb() == (54, 54, 54, 255)
|
||||
assert mainTheme._guiPalette.color(
|
||||
QPalette.ColorRole.WindowText).getRgb() == (204, 204, 204, 255)
|
||||
assert mainTheme._guiPalette.color(
|
||||
QPalette.ColorRole.Base).getRgb() == (62, 62, 62, 255)
|
||||
assert mainTheme._guiPalette.color(
|
||||
QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255)
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
|
||||
"""Test the syntax part of the class."""
|
||||
mainTheme = SHARED.theme
|
||||
|
||||
# List Themes
|
||||
# ===========
|
||||
|
||||
# Block the reading of the files
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert mainTheme.listThemes() == []
|
||||
|
||||
# Load the syntax info
|
||||
syntaxList = mainTheme.listSyntax()
|
||||
assert syntaxList[0] == ("default_dark", "Default Dark")
|
||||
assert syntaxList[1] == ("default_light", "Default Light")
|
||||
|
||||
# A second call should returned the cached list
|
||||
assert mainTheme.listSyntax() == mainTheme._syntaxList
|
||||
|
||||
# Check handling of broken theme settings
|
||||
availSyntax = mainTheme._availSyntax
|
||||
mainTheme._availSyntax = {}
|
||||
CONFIG.guiSyntax = "not_a_syntax"
|
||||
assert mainTheme.loadSyntax() is False
|
||||
mainTheme._availSyntax = availSyntax
|
||||
|
||||
# Check handling of unreadable file
|
||||
CONFIG.guiSyntax = "default_light"
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert mainTheme.loadSyntax() is False
|
||||
|
||||
# Load Default Light Syntax
|
||||
# =========================
|
||||
|
||||
# Load the default syntax
|
||||
CONFIG.guiSyntax = "default_light"
|
||||
assert mainTheme.loadSyntax() is True
|
||||
|
||||
# Check some values
|
||||
assert mainTheme.syntaxMeta.name == "Default Light"
|
||||
assert mainTheme.syntaxTheme.back == QColor(255, 255, 255)
|
||||
assert mainTheme.syntaxTheme.text == QColor(0, 0, 0)
|
||||
assert mainTheme.syntaxTheme.link == QColor(0, 0, 200)
|
||||
|
||||
# Load Default Dark Theme
|
||||
# =======================
|
||||
|
||||
# Load the default syntax
|
||||
CONFIG.guiSyntax = "default_dark"
|
||||
assert mainTheme.loadSyntax() is True
|
||||
|
||||
# Check some values
|
||||
assert mainTheme.syntaxMeta.name == "Default Dark"
|
||||
assert mainTheme.syntaxTheme.back == QColor(42, 42, 42)
|
||||
assert mainTheme.syntaxTheme.text == QColor(204, 204, 204)
|
||||
assert mainTheme.syntaxTheme.link == QColor(102, 153, 204)
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
|
||||
"""Test the icon cache class."""
|
||||
iconCache = SHARED.theme.iconCache
|
||||
|
||||
# Load Theme
|
||||
# ==========
|
||||
|
||||
# Invalid theme name
|
||||
availThemes = iconCache._availThemes
|
||||
iconCache._availThemes = {}
|
||||
assert iconCache.loadTheme("not_a_theme") is False
|
||||
iconCache._availThemes = availThemes
|
||||
|
||||
# Check handling of unreadable file
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert iconCache.loadTheme("material_rounded_normal") is False
|
||||
|
||||
# Load working theme file
|
||||
assert iconCache.loadTheme("material_rounded_normal") is True
|
||||
assert iconCache.themeMeta.name == "Material Symbols - Rounded"
|
||||
|
||||
# Load with project colour override
|
||||
purple = iconCache._svgColors["purple"]
|
||||
assert iconCache._svgColors["root"] != purple
|
||||
assert iconCache._svgColors["folder"] != purple
|
||||
assert iconCache._svgColors["file"] != purple
|
||||
assert iconCache._svgColors["title"] != purple
|
||||
assert iconCache._svgColors["chapter"] != purple
|
||||
assert iconCache._svgColors["scene"] != purple
|
||||
assert iconCache._svgColors["note"] != purple
|
||||
assert theme.getRawBaseColor("root") == b"#ff0000"
|
||||
assert theme.getRawBaseColor("folder") == b"#ff0000"
|
||||
assert theme.getRawBaseColor("file") == b"#cccccc"
|
||||
assert theme.getRawBaseColor("title") == b"#00ff00"
|
||||
assert theme.getRawBaseColor("chapter") == b"#ff0000"
|
||||
assert theme.getRawBaseColor("scene") == b"#0000ff"
|
||||
assert theme.getRawBaseColor("note") == b"#ffff00"
|
||||
|
||||
# Reload with project override to purple, also for docs
|
||||
CONFIG.iconColTree = "purple"
|
||||
assert iconCache.loadTheme("material_rounded_normal") is True
|
||||
assert iconCache._svgColors["root"] == purple
|
||||
assert iconCache._svgColors["folder"] == purple
|
||||
assert iconCache._svgColors["file"] == purple
|
||||
assert iconCache._svgColors["title"] == purple
|
||||
assert iconCache._svgColors["chapter"] == purple
|
||||
assert iconCache._svgColors["scene"] == purple
|
||||
assert iconCache._svgColors["note"] == purple
|
||||
CONFIG.iconColDocs = False
|
||||
theme.loadTheme(force=True)
|
||||
|
||||
# Change some colours
|
||||
iconCache.setIconColor("root", QColor(255, 255, 255))
|
||||
assert iconCache._svgColors["root"] != purple
|
||||
assert iconCache._svgColors["root"] == b"#ffffff"
|
||||
assert theme.getBaseColor("root").getRgb() == (255, 0, 255, 255)
|
||||
assert theme.getBaseColor("folder").getRgb() == (255, 0, 255, 255)
|
||||
assert theme.getBaseColor("file").getRgb() == (255, 0, 255, 255)
|
||||
assert theme.getBaseColor("title").getRgb() == (255, 0, 255, 255)
|
||||
assert theme.getBaseColor("chapter").getRgb() == (255, 0, 255, 255)
|
||||
assert theme.getBaseColor("scene").getRgb() == (255, 0, 255, 255)
|
||||
assert theme.getBaseColor("note").getRgb() == (255, 0, 255, 255)
|
||||
|
||||
# List Themes
|
||||
# ===========
|
||||
|
||||
# Load error returns empty list
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
themes = iconCache.listThemes()
|
||||
assert themes == []
|
||||
|
||||
# Successful read
|
||||
themes = iconCache.listThemes()
|
||||
assert len(themes) > 1
|
||||
assert "material_rounded_normal" in dict(themes)
|
||||
|
||||
# Load error doesn't matter on second read since list is cached
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert iconCache.listThemes() == themes
|
||||
|
||||
# qtbot.stop()
|
||||
assert theme.getRawBaseColor("root") == b"#ff00ff"
|
||||
assert theme.getRawBaseColor("folder") == b"#ff00ff"
|
||||
assert theme.getRawBaseColor("file") == b"#ff00ff"
|
||||
assert theme.getRawBaseColor("title") == b"#ff00ff"
|
||||
assert theme.getRawBaseColor("chapter") == b"#ff00ff"
|
||||
assert theme.getRawBaseColor("scene") == b"#ff00ff"
|
||||
assert theme.getRawBaseColor("note") == b"#ff00ff"
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_LoadIcons(qtbot, nwGUI):
|
||||
def testGuiTheme_Methods(monkeypatch):
|
||||
"""Test other themes methods."""
|
||||
theme = GuiTheme()
|
||||
theme.iconCache = MagicMock()
|
||||
CONFIG.darkTheme = DEF_GUI_DARK
|
||||
CONFIG.themeMode = nwTheme.DARK
|
||||
|
||||
# Init theme
|
||||
assert theme.colourThemes == {}
|
||||
theme.initThemes()
|
||||
assert theme._meta.name == "Default Dark Theme"
|
||||
|
||||
# Text width
|
||||
theme.guiFont = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont)
|
||||
assert theme.getTextWidth("MMMMM") > theme.getTextWidth("MMM")
|
||||
font = QFont(theme.guiFont)
|
||||
font.setPointSizeF(0.5*font.pointSizeF())
|
||||
assert theme.getTextWidth("MMMMM", font) < theme.getTextWidth("MMMMM")
|
||||
|
||||
# Detect desktop mode Qt 6.5+
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(CONFIG, "verQtValue", 0x060500)
|
||||
|
||||
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light)
|
||||
assert theme.isDesktopDarkMode() is False
|
||||
|
||||
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark)
|
||||
assert theme.isDesktopDarkMode() is True
|
||||
|
||||
# Detect desktop mode Qt 6.4
|
||||
mockWhite = Mock()
|
||||
mockWhite.color.return_value = QColor(255, 255, 255)
|
||||
|
||||
mockBlack = Mock()
|
||||
mockBlack.color.return_value = QColor(0, 0, 0)
|
||||
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(CONFIG, "verQtValue", 0x060400)
|
||||
|
||||
mp.setattr(QPalette, "window", lambda *a: mockWhite)
|
||||
mp.setattr(QPalette, "windowText", lambda *a: mockBlack)
|
||||
assert theme.isDesktopDarkMode() is False
|
||||
|
||||
mp.setattr(QPalette, "window", lambda *a: mockBlack)
|
||||
mp.setattr(QPalette, "windowText", lambda *a: mockWhite)
|
||||
assert theme.isDesktopDarkMode() is True
|
||||
|
||||
# Stylesheets
|
||||
assert theme.getStyleSheet(STYLES_FLAT_TABS) != ""
|
||||
assert theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) != ""
|
||||
assert theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) != ""
|
||||
assert theme.getStyleSheet("stuff") == ""
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_ScanIcons(monkeypatch):
|
||||
"""Test the icon theme scanning."""
|
||||
theme = GuiTheme()
|
||||
|
||||
# Load built-in themes
|
||||
files = []
|
||||
_listContent(files, CONFIG.assetPath("icons"), ".icons")
|
||||
assert len(files) > 0
|
||||
|
||||
# Block reading theme files
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
theme.iconCache._scanThemes(files)
|
||||
assert theme.iconCache.iconThemes == {}
|
||||
|
||||
# Read all themes correctly
|
||||
theme.iconCache._scanThemes(files)
|
||||
assert len(theme.iconCache.iconThemes) > 0
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_IconThemes(monkeypatch):
|
||||
"""Test loading icon theme."""
|
||||
CONFIG.lightTheme = DEF_GUI_LIGHT
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
CONFIG.iconTheme = DEF_ICONS
|
||||
|
||||
theme = GuiTheme()
|
||||
|
||||
# Init should load default theme
|
||||
theme.initThemes()
|
||||
assert theme.iconCache._meta.name == "Material Symbols - Rounded"
|
||||
assert DEF_ICONS in theme.iconCache.iconThemes
|
||||
|
||||
# Load default theme directly
|
||||
theme.iconCache._meta = ThemeMeta()
|
||||
theme.iconCache.loadTheme("DEF_ICONS")
|
||||
assert theme.iconCache._meta.name == "Material Symbols - Rounded"
|
||||
|
||||
# Failed loading should load nothing
|
||||
theme.iconCache._meta = ThemeMeta()
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
theme.iconCache.loadTheme("DEF_ICONS")
|
||||
assert theme.iconCache._meta.name == ""
|
||||
|
||||
# Reload with non-existent theme should reload default
|
||||
theme.iconCache._meta = ThemeMeta()
|
||||
theme.iconCache.loadTheme("not_a_theme")
|
||||
assert theme.iconCache._meta.name == "Material Symbols - Rounded"
|
||||
|
||||
# If default theme is missing, load nothing
|
||||
del theme.iconCache._allThemes[DEF_ICONS]
|
||||
theme.iconCache._meta = ThemeMeta()
|
||||
theme.iconCache.loadTheme("not_a_theme")
|
||||
assert theme.iconCache._meta.name == ""
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_LoadIcons():
|
||||
"""Test the icon cache class."""
|
||||
iconCache = SHARED.theme.iconCache
|
||||
assert iconCache.loadTheme("material_rounded_normal") is True
|
||||
theme = GuiTheme()
|
||||
theme.initThemes()
|
||||
iconCache = theme.iconCache
|
||||
|
||||
# Load Icons
|
||||
# ==========
|
||||
@@ -373,11 +431,13 @@ def testGuiTheme_LoadIcons(qtbot, nwGUI):
|
||||
assert qIcon.isNull() is False
|
||||
|
||||
# Load it as a pixmap with a size
|
||||
# If this part of the test fails, you may need to set the
|
||||
# environment variable: QT_SCALE_FACTOR=1
|
||||
qPix = iconCache.getPixmap("add", (50, 50))
|
||||
assert isinstance(qPix, QPixmap)
|
||||
assert qPix.isNull() is False
|
||||
assert qPix.width() == 50
|
||||
assert qPix.height() == 50
|
||||
assert qPix.width() == 50, "If this fails, make sure QT_SCALE_FACTOR=1"
|
||||
assert qPix.height() == 50, "If this fails, make sure QT_SCALE_FACTOR=1"
|
||||
|
||||
# Load app icon
|
||||
qIcon = iconCache.getIcon("novelwriter")
|
||||
@@ -455,14 +515,13 @@ def testGuiTheme_LoadIcons(qtbot, nwGUI):
|
||||
nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0"
|
||||
) == iconCache._noIcon
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
|
||||
def testGuiTheme_LoadDecorations(monkeypatch):
|
||||
"""Test the icon cache class."""
|
||||
iconCache = SHARED.theme.iconCache
|
||||
assert iconCache.loadTheme("material_rounded_normal") is True
|
||||
theme = GuiTheme()
|
||||
theme.initThemes()
|
||||
iconCache = theme.iconCache
|
||||
|
||||
# Load Decorations
|
||||
# ================
|
||||
@@ -520,4 +579,114 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
|
||||
assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5]
|
||||
assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5]
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
THEMES = []
|
||||
_listContent(THEMES, CONFIG.assetPath("themes"), ".conf")
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.parametrize("theme", [a.stem for a in THEMES])
|
||||
def testGuiTheme_CheckTheme(theme):
|
||||
"""Test loading all themes."""
|
||||
themes = GuiTheme()
|
||||
themes.iconCache = MagicMock()
|
||||
themes._scanThemes(THEMES)
|
||||
|
||||
assert theme in themes.colourThemes
|
||||
current = themes.colourThemes[theme]
|
||||
if current.dark:
|
||||
CONFIG.darkTheme = theme
|
||||
CONFIG.themeMode = nwTheme.DARK
|
||||
else:
|
||||
CONFIG.lightTheme = theme
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
|
||||
# Check loading
|
||||
themes.loadTheme()
|
||||
assert themes._meta.name == current.name
|
||||
assert themes.isDarkTheme == current.dark
|
||||
|
||||
# Check completeness
|
||||
parser = ConfigParser()
|
||||
parser.read(current.path, encoding="utf-8")
|
||||
|
||||
sections = ["Main", "Base", "Project", "Palette", "GUI", "Syntax"]
|
||||
assert sorted(parser.sections()) == sorted(sections)
|
||||
|
||||
structure = {
|
||||
"Main": [
|
||||
"name", "mode", "author", # The rest are not required
|
||||
],
|
||||
"Base": [
|
||||
"base", "default", "faded", "red", "orange", "yellow", "green",
|
||||
"cyan", "blue", "purple",
|
||||
],
|
||||
"Project": [
|
||||
"root", "folder", "file", "title", "chapter", "scene", "note",
|
||||
"active", "inactive", "disabled",
|
||||
],
|
||||
"Palette": [
|
||||
"window", "windowtext", "base", "alternatebase", "text",
|
||||
"tooltipbase", "tooltiptext", "button", "buttontext", "brighttext",
|
||||
"highlight", "highlightedtext", "link", "linkvisited", "accent",
|
||||
],
|
||||
"GUI": [
|
||||
"helptext", "fadedtext", "errortext",
|
||||
],
|
||||
"Syntax": [
|
||||
"background", "text", "line", "link", "headertext", "headertag",
|
||||
"emphasis", "dialog", "altdialog", "hidden", "note", "shortcode",
|
||||
"keyword", "tag", "value", "optional", "spellcheckline",
|
||||
"errorline", "replacetag", "modifier", "texthighlight",
|
||||
],
|
||||
}
|
||||
optional = ["credit", "url"]
|
||||
missing = []
|
||||
for section, options in structure.items():
|
||||
missing.extend(opt for opt in options if opt not in parser[section])
|
||||
assert missing == [], "Missing options in theme file"
|
||||
|
||||
# Check deprecated
|
||||
deprecated = []
|
||||
for section in sections:
|
||||
deprecated.extend(
|
||||
opt for opt in parser[section]
|
||||
if opt not in structure[section] and opt not in optional
|
||||
)
|
||||
assert deprecated == [], "Deprecated options in theme file"
|
||||
|
||||
|
||||
ICONS = []
|
||||
_listContent(ICONS, CONFIG.assetPath("icons"), ".icons")
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.parametrize("icons", [a.stem for a in ICONS])
|
||||
def testGuiTheme_CheckIcons(icons, tstPaths):
|
||||
"""Test loading all icons."""
|
||||
keysFile: Path = tstPaths.filesDir / "all_icons.json"
|
||||
iconKeys = json.loads(keysFile.read_text(encoding="utf-8"))
|
||||
assert isinstance(iconKeys, list)
|
||||
|
||||
CONFIG.lightTheme = DEF_GUI_LIGHT
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
|
||||
themes = GuiTheme()
|
||||
themes.initThemes()
|
||||
iconCache = themes.iconCache
|
||||
|
||||
assert icons in iconCache.iconThemes
|
||||
current = iconCache.iconThemes[icons]
|
||||
CONFIG.iconTheme = icons
|
||||
|
||||
# Check loading
|
||||
themes.loadTheme(force=True)
|
||||
assert iconCache._meta.name == current.name
|
||||
|
||||
# Check completeness
|
||||
missing = [key for key in iconKeys if key not in iconCache._svgData]
|
||||
assert missing == [], "Missing keys in icons file"
|
||||
|
||||
# Check deprecated
|
||||
deprecated = [key for key in iconCache._svgData if key not in iconKeys]
|
||||
assert deprecated == [], "Deprecated keys in icons file"
|
||||
|
||||
Reference in New Issue
Block a user