Add more Ruff rules (#2509)

This commit is contained in:
Veronica Berglyd Olsen
2025-08-27 21:36:46 +02:00
committed by GitHub
160 changed files with 588 additions and 1845 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
""" """
Configuration file for the Sphinx documentation builder. Configuration file for the Sphinx documentation builder.
Documentation: http://www.sphinx-doc.org/en/master/config Documentation: http://www.sphinx-doc.org/en/master/config
""" """ # noqa
# -- Imports ----------------------------------------------------------------- # -- Imports -----------------------------------------------------------------
+1 -1
View File
@@ -8,7 +8,7 @@ not yet have a qtbase_xx.qm file shipped with Qt.
If a qtbase_xx.qm file already exists, do not add a translation for the If a qtbase_xx.qm file already exists, do not add a translation for the
entries generated from this file. entries generated from this file.
""" """ # noqa
from PyQt6.QtCore import QT_TRANSLATE_NOOP from PyQt6.QtCore import QT_TRANSLATE_NOOP
+1 -1
View File
@@ -2,7 +2,7 @@
""" """
novelWriter Start Script novelWriter Start Script
========================== ==========================
""" """ # noqa
import os import os
import sys import sys
+1 -1
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import getopt import getopt
+14 -17
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -135,7 +135,7 @@ def checkPath(value: Any, default: Path) -> Path:
def isHandle(value: Any) -> TypeGuard[str]: def isHandle(value: Any) -> TypeGuard[str]:
"""Check if a string is a valid novelWriter handle. """Check if a string is a valid novelWriter handle.
Note: This is case sensitive. Must be lower case! Note: This is case sensitive. Must be lower case.
""" """
if not isinstance(value, str): if not isinstance(value, str):
return False return False
@@ -219,7 +219,7 @@ def firstFloat(*args: Any) -> float:
## ##
def formatInt(value: int) -> str: def formatInt(value: int) -> str:
"""Formats an integer with k, M, G etc.""" """Format an integer with k, M, G etc."""
if not isinstance(value, int): if not isinstance(value, int):
return "ERR" return "ERR"
@@ -464,21 +464,21 @@ def fontMatcher(font: QFont) -> QFont:
def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable: def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable:
"""A replacement for Python lambdas that works for Qt slots.""" """A replacement for Python lambdas that works for Qt slots.""" # noqa: D401
def wrapper(*a_: Any) -> None: def wrapper(*a_: Any) -> None:
func(*args, **kwargs) func(*args, **kwargs)
return wrapper return wrapper
def qtAddAction(parent: QWidget, label: str) -> QAction: def qtAddAction(parent: QWidget, label: str) -> QAction:
"""Helper to add action to widget and always return the action.""" """Helper to add action to widget and always return the action.""" # noqa: D401
action = QAction(label, parent) action = QAction(label, parent)
parent.addAction(action) parent.addAction(action)
return action return action
def qtAddMenu(parent: QMenuBar | QMenu, label: str) -> QMenu: def qtAddMenu(parent: QMenuBar | QMenu, label: str) -> QMenu:
"""Helper to add menu to menu and always return the menu.""" """Helper to add menu to menu and always return the menu.""" # noqa: D401
menu = QMenu(label, parent) menu = QMenu(label, parent)
parent.addMenu(menu) parent.addMenu(menu)
return menu return menu
@@ -487,7 +487,6 @@ def qtAddMenu(parent: QMenuBar | QMenu, label: str) -> QMenu:
def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None: def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None:
"""Encode handles into a mime data object.""" """Encode handles into a mime data object."""
mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles)) mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles))
return
def decodeMimeHandles(mimeData: QMimeData) -> list[str]: def decodeMimeHandles(mimeData: QMimeData) -> list[str]:
@@ -536,7 +535,7 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
elif first in ("{", "["): elif first in ("{", "["):
n += 1 n += 1
indent = "\n"+" "*n indent = "\n"+" "*n
if n > nmax and nmax > 0: if n > nmax > 0:
buffer.append(chunk) buffer.append(chunk)
else: else:
buffer.append(chunk[0] + indent + chunk[1:]) buffer.append(chunk[0] + indent + chunk[1:])
@@ -544,13 +543,13 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
elif first in ("}", "]"): elif first in ("}", "]"):
n -= 1 n -= 1
indent = "\n"+" "*n indent = "\n"+" "*n
if n >= nmax and nmax > 0: if n >= nmax > 0:
buffer.append(chunk) buffer.append(chunk)
else: else:
buffer.append(indent + chunk) buffer.append(indent + chunk)
elif first == ",": elif first == ",":
if n > nmax and nmax > 0: if n > nmax > 0:
buffer.append(chunk) buffer.append(chunk)
else: else:
buffer.append(chunk[0] + indent + chunk[1:].lstrip()) buffer.append(chunk[0] + indent + chunk[1:].lstrip())
@@ -568,7 +567,7 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
def xmlIndent(xml: ET.Element | ET.ElementTree) -> None: def xmlIndent(xml: ET.Element | ET.ElementTree) -> None:
"""A modified version of the XML indent function in the standard """A modified version of the XML indent function in the standard
library. It behaves more closely to how the one from lxml does. library. It behaves more closely to how the one from lxml does.
""" """ # noqa: D401
tree = xml.getroot() if isinstance(xml, ET.ElementTree) else xml tree = xml.getroot() if isinstance(xml, ET.ElementTree) else xml
if not isinstance(tree, ET.Element): if not isinstance(tree, ET.Element):
return return
@@ -598,8 +597,6 @@ def xmlIndent(xml: ET.Element | ET.ElementTree) -> None:
if last is not None: if last is not None:
last.tail = indentations[level] last.tail = indentations[level]
return
if len(tree): if len(tree):
indentChildren(tree, 0) indentChildren(tree, 0)
tree.tail = "\n" tree.tail = "\n"
@@ -614,7 +611,7 @@ def xmlElement(
attrib: dict | None = None, attrib: dict | None = None,
tail: str | None = None, tail: str | None = None,
) -> ET.Element: ) -> ET.Element:
"""A custom implementation of Element with more arguments.""" """A custom implementation of Element with more arguments.""" # noqa: D401
xSub = ET.Element(tag, attrib=attrib or {}) xSub = ET.Element(tag, attrib=attrib or {})
if text is not None: if text is not None:
if isinstance(text, bool): if isinstance(text, bool):
@@ -634,7 +631,7 @@ def xmlSubElem(
attrib: dict | None = None, attrib: dict | None = None,
tail: str | None = None, tail: str | None = None,
) -> ET.Element: ) -> ET.Element:
"""A custom implementation of SubElement with more arguments.""" """A custom implementation of SubElement with more arguments.""" # noqa: D401
xSub = ET.SubElement(parent, tag, attrib=attrib or {}) xSub = ET.SubElement(parent, tag, attrib=attrib or {})
if text is not None: if text is not None:
if isinstance(text, bool): if isinstance(text, bool):
@@ -665,6 +662,7 @@ def readTextFile(path: str | Path) -> str:
def makeFileNameSafe(text: str) -> str: def makeFileNameSafe(text: str) -> str:
"""Return a filename-safe string. """Return a filename-safe string.
See: https://unicode.org/reports/tr15/#Norm_Forms See: https://unicode.org/reports/tr15/#Norm_Forms
""" """
text = unicodedata.normalize("NFKC", text).strip() text = unicodedata.normalize("NFKC", text).strip()
@@ -697,7 +695,7 @@ _T_Enum = TypeVar("_T_Enum", bound=Enum)
class NWConfigParser(ConfigParser): class NWConfigParser(ConfigParser):
"""Common: Adapted Config Parser """Common: Adapted Config Parser.
This is a subclass of the standard config parser that adds type safe This is a subclass of the standard config parser that adds type safe
helper functions, and support for lists. It also turns off helper functions, and support for lists. It also turns off
@@ -706,7 +704,6 @@ class NWConfigParser(ConfigParser):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(interpolation=None) super().__init__(interpolation=None)
return
def rdStr(self, section: str, option: str, default: str) -> str: def rdStr(self, section: str, option: str, default: str) -> str:
"""Read string value.""" """Read string value."""
+11 -26
View File
@@ -22,7 +22,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -63,6 +63,13 @@ DEF_TREECOL = "theme"
class Config: class Config:
"""User Config.
The main user config. The state of the config is stored in the
novelwriter.conf file between sessions. Most of the settings can be
modified by the user in the Preferences dialog, but some just record
various states of the GUI.
"""
__slots__ = ( __slots__ = (
"_appPath", "_appRoot", "_backPath", "_backupPath", "_confPath", "_dLocale", "_dShortDate", "_appPath", "_appRoot", "_backPath", "_backupPath", "_confPath", "_dLocale", "_dShortDate",
@@ -300,8 +307,6 @@ class Config:
# Packages # Packages
self.hasEnchant = False # The pyenchant package self.hasEnchant = False # The pyenchant package
return
## ##
# Properties # Properties
## ##
@@ -350,7 +355,6 @@ class Config:
def setLastAuthor(self, value: str) -> None: def setLastAuthor(self, value: str) -> None:
"""Set tle last used author name.""" """Set tle last used author name."""
self._lastAuthor = simplified(value) self._lastAuthor = simplified(value)
return
def setMainWinSize(self, width: int, height: int) -> None: def setMainWinSize(self, width: int, height: int) -> None:
"""Set the size of the main window, but only if the change is """Set the size of the main window, but only if the change is
@@ -362,17 +366,14 @@ class Config:
self.mainWinSize[0] = width self.mainWinSize[0] = width
if abs(self.mainWinSize[1] - height) > 5: if abs(self.mainWinSize[1] - height) > 5:
self.mainWinSize[1] = height self.mainWinSize[1] = height
return
def setWelcomeWinSize(self, width: int, height: int) -> None: def setWelcomeWinSize(self, width: int, height: int) -> None:
"""Set the size of the Preferences dialog window.""" """Set the size of the Preferences dialog window."""
self.welcomeWinSize = [width, height] self.welcomeWinSize = [width, height]
return
def setPreferencesWinSize(self, width: int, height: int) -> None: def setPreferencesWinSize(self, width: int, height: int) -> None:
"""Set the size of the Preferences dialog window.""" """Set the size of the Preferences dialog window."""
self.prefsWinSize = [width, height] self.prefsWinSize = [width, height]
return
def setLastPath(self, key: str, path: str | Path) -> None: def setLastPath(self, key: str, path: str | Path) -> None:
"""Set the last used path. Only the folder is saved, so if the """Set the last used path. Only the folder is saved, so if the
@@ -384,12 +385,10 @@ class Config:
path = path.parent path = path.parent
if path.is_dir(): if path.is_dir():
self._recentPaths.setPath(key, path) self._recentPaths.setPath(key, path)
return
def setBackupPath(self, path: Path | str) -> None: def setBackupPath(self, path: Path | str) -> None:
"""Set the current backup path.""" """Set the current backup path."""
self._backupPath = checkPath(path, self._backPath) self._backupPath = checkPath(path, self._backPath)
return
def setGuiFont(self, value: QFont | str | None) -> None: def setGuiFont(self, value: QFont | str | None) -> None:
"""Update the GUI's font style from settings.""" """Update the GUI's font style from settings."""
@@ -410,7 +409,6 @@ class Config:
self.guiFont = fontMatcher(font) self.guiFont = fontMatcher(font)
logger.debug("Main font set to: %s", describeFont(font)) logger.debug("Main font set to: %s", describeFont(font))
QApplication.setFont(self.guiFont) QApplication.setFont(self.guiFont)
return
def setTextFont(self, value: QFont | str | None) -> None: def setTextFont(self, value: QFont | str | None) -> None:
"""Set the text font if it exists. If it doesn't, or is None, """Set the text font if it exists. If it doesn't, or is None,
@@ -436,14 +434,13 @@ class Config:
font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont)
self.textFont = fontMatcher(font) self.textFont = fontMatcher(font)
logger.debug("Text font set to: %s", describeFont(self.textFont)) logger.debug("Text font set to: %s", describeFont(self.textFont))
return
## ##
# Methods # Methods
## ##
def homePath(self) -> Path: def homePath(self) -> Path:
"""The user's home folder.""" """Return the user's home folder."""
return self._homePath return self._homePath
def dataPath(self, target: str | None = None) -> Path: def dataPath(self, target: str | None = None) -> Path:
@@ -525,7 +522,6 @@ class Config:
"""Send a message to the splash screen.""" """Send a message to the splash screen."""
if self._splash: if self._splash:
self._splash.showStatus(message) self._splash.showStatus(message)
return
## ##
# Config Actions # Config Actions
@@ -568,8 +564,6 @@ class Config:
logger.debug("Config instance initialised") logger.debug("Config instance initialised")
return
def initLocalisation(self, nwApp: QApplication) -> None: def initLocalisation(self, nwApp: QApplication) -> None:
"""Initialise the localisation of the GUI.""" """Initialise the localisation of the GUI."""
self.splashMessage("Loading localisation ...") self.splashMessage("Loading localisation ...")
@@ -597,8 +591,6 @@ class Config:
nwApp.installTranslator(qTrans) nwApp.installTranslator(qTrans)
self._qtTrans[lngFile] = qTrans self._qtTrans[lngFile] = qTrans
return
def loadConfig(self, splash: NSplashScreen | None = None) -> bool: def loadConfig(self, splash: NSplashScreen | None = None) -> bool:
"""Load preferences from file and replace default settings.""" """Load preferences from file and replace default settings."""
self._splash = splash self._splash = splash
@@ -872,7 +864,6 @@ class Config:
def finishStartup(self) -> None: def finishStartup(self) -> None:
"""Call after startup is complete.""" """Call after startup is complete."""
self._splash = None self._splash = None
return
## ##
# Internal Functions # Internal Functions
@@ -894,7 +885,6 @@ class Config:
else: else:
self.hasEnchant = True self.hasEnchant = True
logger.debug("Checking package 'pyenchant': OK") logger.debug("Checking package 'pyenchant': OK")
return
def _prepareFont(self, font: QFont, kind: str) -> None: def _prepareFont(self, font: QFont, kind: str) -> None:
"""Check Unicode availability in font. This also initialises any """Check Unicode availability in font. This also initialises any
@@ -905,16 +895,15 @@ class Config:
for char in nwUnicode.UI_SYMBOLS: for char in nwUnicode.UI_SYMBOLS:
if not metrics.inFont(char): # type: ignore if not metrics.inFont(char): # type: ignore
logger.warning("No glyph U+%04x in font", ord(char)) # pragma: no cover logger.warning("No glyph U+%04x in font", ord(char)) # pragma: no cover
return
class RecentProjects: class RecentProjects:
"""A record of recently opened projects."""
def __init__(self, config: Config) -> None: def __init__(self, config: Config) -> None:
self._conf = config self._conf = config
self._data: dict[str, dict[str, str | int]] = {} self._data: dict[str, dict[str, str | int]] = {}
self._map: dict[str, str] = {} self._map: dict[str, str] = {}
return
def loadCache(self) -> bool: def loadCache(self) -> bool:
"""Load the cache file for recent projects.""" """Load the cache file for recent projects."""
@@ -976,14 +965,12 @@ class RecentProjects:
self.saveCache() self.saveCache()
except Exception: except Exception:
pass pass
return
def remove(self, path: str | Path) -> None: def remove(self, path: str | Path) -> None:
"""Try to remove a path from the recent projects cache.""" """Try to remove a path from the recent projects cache."""
if self._data.pop(str(path), None) is not None: if self._data.pop(str(path), None) is not None:
logger.debug("Removed recent: %s", path) logger.debug("Removed recent: %s", path)
self.saveCache() self.saveCache()
return
def _setEntry( def _setEntry(
self, puuid: str, path: str, title: str, words: int, chars: int, saved: int self, puuid: str, path: str, title: str, words: int, chars: int, saved: int
@@ -998,24 +985,22 @@ class RecentProjects:
} }
if puuid: if puuid:
self._map[puuid] = path self._map[puuid] = path
return
class RecentPaths: class RecentPaths:
"""A record of recently used file paths."""
KEYS: Final[list[str]] = ["default", "project", "import", "outline", "stats"] KEYS: Final[list[str]] = ["default", "project", "import", "outline", "stats"]
def __init__(self, config: Config) -> None: def __init__(self, config: Config) -> None:
self._conf = config self._conf = config
self._data = {} self._data = {}
return
def setPath(self, key: str, path: Path | str) -> None: def setPath(self, key: str, path: Path | str) -> None:
"""Set a path for a given key, and save the cache.""" """Set a path for a given key, and save the cache."""
if key in self.KEYS: if key in self.KEYS:
self._data[key] = str(path) self._data[key] = str(path)
self.saveCache() self.saveCache()
return
def getPath(self, key: str) -> str | None: def getPath(self, key: str) -> str | None:
"""Get a path for a given key, or return None.""" """Get a path for a given key, or return None."""
+17 -3
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from typing import Final from typing import Final
@@ -34,16 +34,17 @@ from novelwriter.enum import (
def trConst(text: str) -> str: def trConst(text: str) -> str:
"""Wrapper function for locally translating constants.""" """Translate a constant."""
return QCoreApplication.translate("Constant", text) return QCoreApplication.translate("Constant", text)
def trStats(text: str) -> str: def trStats(text: str) -> str:
"""Wrapper function for locally translating stats constants.""" """Translate a stats constants."""
return QCoreApplication.translate("Stats", text) return QCoreApplication.translate("Stats", text)
class nwConst: class nwConst:
"""Various Constants."""
# Date and Time Formats # Date and Time Formats
FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format
@@ -70,6 +71,7 @@ class nwConst:
class nwRegEx: class nwRegEx:
"""Common RegExes."""
URL = r"https?://(?:www\.|(?!www))[\w/()@:%_\+-.~#?&=]+" URL = r"https?://(?:www\.|(?!www))[\w/()@:%_\+-.~#?&=]+"
WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b" WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b"
@@ -83,6 +85,7 @@ class nwRegEx:
class nwShortcode: class nwShortcode:
"""Document ShortCodes."""
BOLD_O = "[b]" BOLD_O = "[b]"
BOLD_C = "[/b]" BOLD_C = "[/b]"
@@ -112,6 +115,7 @@ class nwShortcode:
class nwStyles: class nwStyles:
"""Style Settings for Headings."""
H_VALID = ("H0", "H1", "H2", "H3", "H4") H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL: Final[dict[str, int]] = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} H_LEVEL: Final[dict[str, int]] = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
@@ -143,6 +147,7 @@ class nwStyles:
class nwFiles: class nwFiles:
"""novelWriter Files."""
# Config Files # Config Files
CONF_FILE = "novelwriter.conf" CONF_FILE = "novelwriter.conf"
@@ -163,6 +168,7 @@ class nwFiles:
class nwKeyWords: class nwKeyWords:
"""Meta Data KeyWord Constants."""
TAG_KEY = "@tag" TAG_KEY = "@tag"
POV_KEY = "@pov" POV_KEY = "@pov"
@@ -210,6 +216,7 @@ class nwKeyWords:
class nwLists: class nwLists:
"""Various Lists."""
USER_CLASSES: Final[list[nwItemClass]] = [ USER_CLASSES: Final[list[nwItemClass]] = [
nwItemClass.CHARACTER, nwItemClass.CHARACTER,
@@ -223,6 +230,7 @@ class nwLists:
class nwStats: class nwStats:
"""Text Statistics."""
CHARS = "allChars" CHARS = "allChars"
CHARS_TEXT = "textChars" CHARS_TEXT = "textChars"
@@ -246,6 +254,7 @@ class nwStats:
class nwLabels: class nwLabels:
"""Various Common GUI Labels."""
CLASS_NAME: Final[dict[nwItemClass, str]] = { CLASS_NAME: Final[dict[nwItemClass, str]] = {
nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"), nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"),
@@ -472,6 +481,7 @@ class nwLabels:
class nwHeadFmt: class nwHeadFmt:
"""Manuscript Header Formats."""
BR = "{BR}" BR = "{BR}"
TITLE = "{Title}" TITLE = "{Title}"
@@ -498,8 +508,10 @@ class nwHeadFmt:
class nwQuotes: class nwQuotes:
"""Allowed quotation marks. """Allowed quotation marks.
Source: https://en.wikipedia.org/wiki/Quotation_mark Source: https://en.wikipedia.org/wiki/Quotation_mark
""" """
SYMBOLS: Final[dict[str, str]] = { SYMBOLS: Final[dict[str, str]] = {
"\u0027": QT_TRANSLATE_NOOP("Constant", "Straight single quotation mark"), "\u0027": QT_TRANSLATE_NOOP("Constant", "Straight single quotation mark"),
"\u0022": QT_TRANSLATE_NOOP("Constant", "Straight double quotation mark"), "\u0022": QT_TRANSLATE_NOOP("Constant", "Straight double quotation mark"),
@@ -541,6 +553,7 @@ class nwQuotes:
class nwUnicode: class nwUnicode:
"""Supported unicode character constants and their HTML equivalents.""" """Supported unicode character constants and their HTML equivalents."""
# Unicode Constants # Unicode Constants
# ================= # =================
@@ -672,6 +685,7 @@ class nwUnicode:
class nwHtmlUnicode: class nwHtmlUnicode:
"""Unicode to HTML Map."""
U_TO_H: Final[dict[str, str]] = { U_TO_H: Final[dict[str, str]] = {
# Quotes # Quotes
+3 -23
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -211,7 +211,7 @@ class FilterMode(Enum):
class BuildSettings: class BuildSettings:
"""Core: Build Settings Class """Core: Build Settings Class.
This class manages the build settings for a Manuscript build job. This class manages the build settings for a Manuscript build job.
The settings can be packed/unpacked to/from a dictionary for JSON. The settings can be packed/unpacked to/from a dictionary for JSON.
@@ -229,7 +229,6 @@ class BuildSettings:
self._included = set() self._included = set()
self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()} self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
self._changed = False self._changed = False
return
@classmethod @classmethod
def fromDict(cls, data: dict) -> BuildSettings: def fromDict(cls, data: dict) -> BuildSettings:
@@ -315,7 +314,6 @@ class BuildSettings:
def setName(self, name: str) -> None: def setName(self, name: str) -> None:
"""Set the build setting display name.""" """Set the build setting display name."""
self._name = str(name) self._name = str(name)
return
def setBuildID(self, value: str | uuid.UUID) -> None: def setBuildID(self, value: str | uuid.UUID) -> None:
"""Set a UUID build ID.""" """Set a UUID build ID."""
@@ -324,13 +322,11 @@ class BuildSettings:
self._uuid = str(uuid.uuid4()) self._uuid = str(uuid.uuid4())
elif value != self._uuid: elif value != self._uuid:
self._uuid = value self._uuid = value
return
def setOrder(self, value: int) -> None: def setOrder(self, value: int) -> None:
"""Set the build order.""" """Set the build order."""
if isinstance(value, int): if isinstance(value, int):
self._order = value self._order = value
return
def setLastBuildPath(self, path: Path | str | None) -> None: def setLastBuildPath(self, path: Path | str | None) -> None:
"""Set the last used build path.""" """Set the last used build path."""
@@ -341,41 +337,35 @@ class BuildSettings:
else: else:
self._path = CONFIG.homePath() self._path = CONFIG.homePath()
self._changed = True self._changed = True
return
def setLastBuildName(self, name: str) -> None: def setLastBuildName(self, name: str) -> None:
"""Set the last used build name.""" """Set the last used build name."""
self._build = str(name).strip() self._build = str(name).strip()
self._changed = True self._changed = True
return
def setLastFormat(self, value: nwBuildFmt) -> None: def setLastFormat(self, value: nwBuildFmt) -> None:
"""Set the last used build format.""" """Set the last used build format."""
if isinstance(value, nwBuildFmt): if isinstance(value, nwBuildFmt):
self._format = value self._format = value
self._changed = True self._changed = True
return
def setFiltered(self, tHandle: str) -> None: def setFiltered(self, tHandle: str) -> None:
"""Set an item as filtered.""" """Set an item as filtered."""
self._excluded.discard(tHandle) self._excluded.discard(tHandle)
self._included.discard(tHandle) self._included.discard(tHandle)
self._changed = True self._changed = True
return
def setIncluded(self, tHandle: str) -> None: def setIncluded(self, tHandle: str) -> None:
"""Set an item as explicitly included.""" """Set an item as explicitly included."""
self._excluded.discard(tHandle) self._excluded.discard(tHandle)
self._included.add(tHandle) self._included.add(tHandle)
self._changed = True self._changed = True
return
def setExcluded(self, tHandle: str) -> None: def setExcluded(self, tHandle: str) -> None:
"""Set an item as explicitly excluded.""" """Set an item as explicitly excluded."""
self._excluded.add(tHandle) self._excluded.add(tHandle)
self._included.discard(tHandle) self._included.discard(tHandle)
self._changed = True self._changed = True
return
def setAllowRoot(self, tHandle: str, state: bool) -> None: def setAllowRoot(self, tHandle: str, state: bool) -> None:
"""Set a specific root folder as allowed or not.""" """Set a specific root folder as allowed or not."""
@@ -385,14 +375,12 @@ class BuildSettings:
elif state is False: elif state is False:
self._skipRoot.add(tHandle) self._skipRoot.add(tHandle)
self._changed = True self._changed = True
return
def setValue(self, key: str, value: T_BuildValue) -> None: def setValue(self, key: str, value: T_BuildValue) -> None:
"""Set a specific value for a build setting.""" """Set a specific value for a build setting."""
if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]): if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]):
self._changed |= (value != self._settings[key]) self._changed |= (value != self._settings[key])
self._settings[key] = value self._settings[key] = value
return
## ##
# Methods # Methods
@@ -463,7 +451,6 @@ class BuildSettings:
called when the changes have been safely saved or passed on. called when the changes have been safely saved or passed on.
""" """
self._changed = False self._changed = False
return
def pack(self) -> dict: def pack(self) -> dict:
"""Pack all content into a JSON compatible dictionary.""" """Pack all content into a JSON compatible dictionary."""
@@ -516,8 +503,6 @@ class BuildSettings:
self._changed = False self._changed = False
return
@classmethod @classmethod
def duplicate(cls, source: BuildSettings) -> BuildSettings: def duplicate(cls, source: BuildSettings) -> BuildSettings:
"""Make a copy of another build.""" """Make a copy of another build."""
@@ -529,7 +514,7 @@ class BuildSettings:
class BuildCollection: class BuildCollection:
"""Core: Build Collection Class """Core: Build Collection Class.
This object holds all the build setting objects defined by the given This object holds all the build setting objects defined by the given
project. The build settings are saved as a single JSON file in the project. The build settings are saved as a single JSON file in the
@@ -542,7 +527,6 @@ class BuildCollection:
self._defaultBuild = "" self._defaultBuild = ""
self._builds: dict[str, BuildSettings] = {} self._builds: dict[str, BuildSettings] = {}
self._loadCollection() self._loadCollection()
return
def __len__(self) -> int: def __len__(self) -> int:
"""Return the number of builds.""" """Return the number of builds."""
@@ -581,21 +565,18 @@ class BuildCollection:
build.setOrder(i) build.setOrder(i)
self._lastBuild = lastBuild self._lastBuild = lastBuild
self._saveCollection() self._saveCollection()
return
def setDefaultBuild(self, buildID: str) -> None: def setDefaultBuild(self, buildID: str) -> None:
"""Set the default build id.""" """Set the default build id."""
if buildID != self._defaultBuild: if buildID != self._defaultBuild:
self._defaultBuild = buildID self._defaultBuild = buildID
self._saveCollection() self._saveCollection()
return
def setBuild(self, build: BuildSettings) -> None: def setBuild(self, build: BuildSettings) -> None:
"""Set build settings data in the collection.""" """Set build settings data in the collection."""
if isinstance(build, BuildSettings): if isinstance(build, BuildSettings):
self._builds[build.buildID] = build self._builds[build.buildID] = build
self._saveCollection() self._saveCollection()
return
## ##
# Methods # Methods
@@ -605,7 +586,6 @@ class BuildCollection:
"""Remove a build from the collection.""" """Remove a build from the collection."""
self._builds.pop(buildID, None) self._builds.pop(buildID, None)
self._saveCollection() self._saveCollection()
return
def builds(self) -> Iterable[tuple[str, str]]: def builds(self) -> Iterable[tuple[str, str]]:
"""Iterate over all available builds.""" """Iterate over all available builds."""
+10 -23
View File
@@ -23,7 +23,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -52,7 +52,9 @@ logger = logging.getLogger(__name__)
class DocMerger: class DocMerger:
"""Document tool for merging a set of documents into a single new """Tool: Merge Documents.
Document tool for merging a set of documents into a single new
document. The parameters are defined by the user using the document. The parameters are defined by the user using the
GuiDocMerge dialog. GuiDocMerge dialog.
""" """
@@ -62,7 +64,6 @@ class DocMerger:
self._error = "" self._error = ""
self._target = None self._target = None
self._text = [] self._text = []
return
@property @property
def targetHandle(self) -> str | None: def targetHandle(self) -> str | None:
@@ -85,7 +86,6 @@ class DocMerger:
""" """
self._target = self._project.tree[tHandle] self._target = self._project.tree[tHandle]
self._text = [] self._text = []
return
def newTargetDoc(self, sHandle: str, label: str) -> None: def newTargetDoc(self, sHandle: str, label: str) -> None:
"""Create a brand new target document based on a source handle """Create a brand new target document based on a source handle
@@ -101,7 +101,6 @@ class DocMerger:
nwItem.notifyToRefresh() nwItem.notifyToRefresh()
self._target = nwItem self._target = nwItem
self._text = [] self._text = []
return
def appendText(self, sHandle: str, addComment: bool, cmtPrefix: str) -> None: def appendText(self, sHandle: str, addComment: bool, cmtPrefix: str) -> None:
"""Append text from an existing document to the text buffer.""" """Append text from an existing document to the text buffer."""
@@ -112,7 +111,6 @@ class DocMerger:
status, _ = item.getImportStatus() status, _ = item.getImportStatus()
text = f"% {cmtPrefix} {info}: {item.itemName} [{status}]\n\n{text}" text = f"% {cmtPrefix} {info}: {item.itemName} [{status}]\n\n{text}"
self._text.append(text) self._text.append(text)
return
def writeTargetDoc(self) -> bool: def writeTargetDoc(self) -> bool:
"""Write the accumulated text into the designated target """Write the accumulated text into the designated target
@@ -158,10 +156,7 @@ class DocSplitter:
self._srcHandle = sHandle self._srcHandle = sHandle
self._srcItem = srcItem self._srcItem = srcItem
return
def __len__(self) -> int: def __len__(self) -> int:
"""The length of the split job."""
return len(self._rawData) return len(self._rawData)
## ##
@@ -178,7 +173,6 @@ class DocSplitter:
""" """
self._parHandle = pHandle self._parHandle = pHandle
self._inFolder = False self._inFolder = False
return
def newParentFolder(self, pHandle: str, folderLabel: str) -> None: def newParentFolder(self, pHandle: str, folderLabel: str) -> None:
"""Create a new folder that will be the top level parent item """Create a new folder that will be the top level parent item
@@ -192,7 +186,6 @@ class DocSplitter:
nwItem.notifyToRefresh() nwItem.notifyToRefresh()
self._parHandle = nHandle self._parHandle = nHandle
self._inFolder = True self._inFolder = True
return
def splitDocument(self, splitData: list, splitText: list[str]) -> None: def splitDocument(self, splitData: list, splitText: list[str]) -> None:
"""Loop through the split data record and perform the split job """Loop through the split data record and perform the split job
@@ -204,12 +197,9 @@ class DocSplitter:
chunk = buffer[lineNo:] chunk = buffer[lineNo:]
buffer = buffer[:lineNo] buffer = buffer[:lineNo]
self._rawData.insert(0, (chunk, hLevel, hLabel)) self._rawData.insert(0, (chunk, hLevel, hLabel))
return
def writeDocuments(self, docHierarchy: bool) -> Iterable[bool]: def writeDocuments(self, docHierarchy: bool) -> Iterable[bool]:
"""An iterator that will write each document in the buffer, and """Write each document in the buffer and yield if successful."""
return its new handle, parent handle, and sibling handle.
"""
if self._srcHandle and self._srcItem and self._parHandle: if self._srcHandle and self._srcItem and self._parHandle:
pHandle = self._parHandle pHandle = self._parHandle
hHandle = [self._parHandle, None, None, None, None] hHandle = [self._parHandle, None, None, None, None]
@@ -260,7 +250,6 @@ class DocDuplicator:
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject) -> None:
self._project = project self._project = project
return
## ##
# Methods # Methods
@@ -293,13 +282,16 @@ class DocDuplicator:
class DocSearch: class DocSearch:
"""Tool: Search Documents.
A global document search class.
"""
def __init__(self) -> None: def __init__(self) -> None:
self._regEx = re.compile(r"") self._regEx = re.compile(r"")
self._opts = re.IGNORECASE self._opts = re.IGNORECASE
self._words = False self._words = False
self._escape = True self._escape = True
return
## ##
# Methods # Methods
@@ -308,22 +300,19 @@ class DocSearch:
def setCaseSensitive(self, state: bool) -> None: def setCaseSensitive(self, state: bool) -> None:
"""Set the case sensitive search flag.""" """Set the case sensitive search flag."""
self._opts = 0 if state else re.IGNORECASE self._opts = 0 if state else re.IGNORECASE
return
def setWholeWords(self, state: bool) -> None: def setWholeWords(self, state: bool) -> None:
"""Set the whole words search flag.""" """Set the whole words search flag."""
self._words = state self._words = state
return
def setUserRegEx(self, state: bool) -> None: def setUserRegEx(self, state: bool) -> None:
"""Set the escape flag to the opposite state.""" """Set the escape flag to the opposite state."""
self._escape = not state self._escape = not state
return
def iterSearch( def iterSearch(
self, project: NWProject, search: str self, project: NWProject, search: str
) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]: ) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]:
"""Iteratively search through documents in a project.""" """Iterate through documents in a project and apply search."""
self._regEx = re.compile(self._buildPattern(search), self._opts) self._regEx = re.compile(self._buildPattern(search), self._opts)
logger.debug("Searching with pattern '%s'", self._regEx.pattern) logger.debug("Searching with pattern '%s'", self._regEx.pattern)
storage = project.storage storage = project.storage
@@ -376,7 +365,6 @@ class ProjectBuilder:
def __init__(self) -> None: def __init__(self) -> None:
self._path = None self._path = None
self.tr = partial(QCoreApplication.translate, "ProjectBuilder") self.tr = partial(QCoreApplication.translate, "ProjectBuilder")
return
@property @property
def projPath(self) -> Path | None: def projPath(self) -> Path | None:
@@ -620,4 +608,3 @@ class ProjectBuilder:
project.index.rebuild() project.index.rebuild()
project.saveProject() project.saveProject()
project.closeProject() project.closeProject()
return
+4 -9
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -53,7 +53,7 @@ logger = logging.getLogger(__name__)
class NWBuildDocument: class NWBuildDocument:
"""Core: Manuscript Document Build Class """Core: Manuscript Document Build Class.
This is the core tool that assembles a project and outputs a This is the core tool that assembles a project and outputs a
manuscript, based on a build definition object (BuildSettings). manuscript, based on a build definition object (BuildSettings).
@@ -72,7 +72,6 @@ class NWBuildDocument:
self._cache = None self._cache = None
self._count = False self._count = False
self._outline = False self._outline = False
return
## ##
# Properties # Properties
@@ -106,7 +105,6 @@ class NWBuildDocument:
def addDocument(self, tHandle: str) -> None: def addDocument(self, tHandle: str) -> None:
"""Add a document to the build queue manually.""" """Add a document to the build queue manually."""
self._queue.append(tHandle) self._queue.append(tHandle)
return
def queueAll(self) -> None: def queueAll(self) -> None:
"""Queue all document as defined by the build settings.""" """Queue all document as defined by the build settings."""
@@ -115,7 +113,6 @@ class NWBuildDocument:
for item in self._project.tree: for item in self._project.tree:
if filtered.get(item.itemHandle, False): if filtered.get(item.itemHandle, False):
self._queue.append(item.itemHandle) self._queue.append(item.itemHandle)
return
def iterBuildPreview(self, newPage: bool) -> Iterable[tuple[int, bool]]: def iterBuildPreview(self, newPage: bool) -> Iterable[tuple[int, bool]]:
"""Build a preview QTextDocument.""" """Build a preview QTextDocument."""
@@ -131,7 +128,7 @@ class NWBuildDocument:
return return
def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]: def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
"""Wrapper for builders based on format.""" """Select a builder based on format."""
self._error = None self._error = None
self._cache = None self._cache = None
@@ -341,12 +338,10 @@ class NWBuildDocument:
scale*self._build.getFloat("format.rightMargin"), scale*self._build.getFloat("format.rightMargin"),
) )
filtered = self._build.buildItemFilter( return self._build.buildItemFilter(
self._project, withRoots=self._build.getBool("text.addNoteHeadings") self._project, withRoots=self._build.getBool("text.addNoteHeadings")
) )
return filtered
def _doBuild(self, bldObj: Tokenizer, tHandle: str, convert: bool = True) -> bool: def _doBuild(self, bldObj: Tokenizer, tHandle: str, convert: bool = True) -> bool:
"""Build a single document and add it to the build object.""" """Build a single document and add it to the build object."""
tItem = self._project.tree[tHandle] tItem = self._project.tree[tHandle]
+2 -6
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
class NWDocument: class NWDocument:
"""Core: Document Class """Core: Document Class.
A Class wrapping a single novelWriter document file. It represents A Class wrapping a single novelWriter document file. It represents
a project item of nwItemType FILE. The file is not guaranteed to a project item of nwItemType FILE. The file is not guaranteed to
@@ -68,8 +68,6 @@ class NWDocument:
if self._handle is not None: if self._handle is not None:
self._item = self._project.tree[tHandle] self._item = self._project.tree[tHandle]
return
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<NWDocument handle={self._handle}>" return f"<NWDocument handle={self._handle}>"
@@ -357,5 +355,3 @@ class NWDocument:
else: else:
logger.debug("Unknown meta data: '%s'", metaLine.strip()) logger.debug("Unknown meta data: '%s'", metaLine.strip())
return
+6 -42
View File
@@ -22,7 +22,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -56,7 +56,7 @@ KEY_SOURCE = "0123456789bcdfghjklmnpqrstvwxz"
class Index: class Index:
"""Core: Project Index """Core: Project Index.
This class holds the entire index for a given project. The index This class holds the entire index for a given project. The index
contains the data that isn't stored in the project items themselves. contains the data that isn't stored in the project items themselves.
@@ -99,8 +99,6 @@ class Index:
self._indexChange = 0.0 self._indexChange = 0.0
self._rootChange = {} self._rootChange = {}
return
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Index project='{self._project.data.name}'>" return f"<Index project='{self._project.data.name}'>"
@@ -129,7 +127,6 @@ class Index:
def setNovelModelExtraColumn(self, extra: nwNovelExtra) -> None: def setNovelModelExtraColumn(self, extra: nwNovelExtra) -> None:
"""Set the data content type of the novel model extra column.""" """Set the data content type of the novel model extra column."""
self._novelExtra = extra self._novelExtra = extra
return
## ##
# Public Methods # Public Methods
@@ -142,7 +139,6 @@ class Index:
self._indexChange = 0.0 self._indexChange = 0.0
self._rootChange = {} self._rootChange = {}
SHARED.emitIndexCleared(self._project) SHARED.emitIndexCleared(self._project)
return
def rebuild(self) -> None: def rebuild(self) -> None:
"""Rebuild the entire index from scratch.""" """Rebuild the entire index from scratch."""
@@ -158,7 +154,6 @@ class Index:
for tHandle in self._novelModels: for tHandle in self._novelModels:
self.refreshNovelModel(tHandle) self.refreshNovelModel(tHandle)
SHARED.clearMainProgress() SHARED.clearMainProgress()
return
def deleteHandle(self, tHandle: str) -> None: def deleteHandle(self, tHandle: str) -> None:
"""Delete all entries of a given document handle.""" """Delete all entries of a given document handle."""
@@ -168,7 +163,6 @@ class Index:
del self._tagsIndex[tTag] del self._tagsIndex[tTag]
del self._itemIndex[tHandle] del self._itemIndex[tHandle]
SHARED.emitIndexChangedTags(self._project, [], delTags) SHARED.emitIndexChangedTags(self._project, [], delTags)
return
def reIndexHandle(self, tHandle: str | None) -> None: def reIndexHandle(self, tHandle: str | None) -> None:
"""Put a file back into the index. This is used when files are """Put a file back into the index. This is used when files are
@@ -178,7 +172,6 @@ class Index:
if tHandle and self._project.tree.checkType(tHandle, nwItemType.FILE): if tHandle and self._project.tree.checkType(tHandle, nwItemType.FILE):
logger.debug("Re-indexing item '%s'", tHandle) logger.debug("Re-indexing item '%s'", tHandle)
self.scanText(tHandle, self._project.storage.getDocumentText(tHandle)) self.scanText(tHandle, self._project.storage.getDocumentText(tHandle))
return
def refreshHandle(self, tHandle: str) -> None: def refreshHandle(self, tHandle: str) -> None:
"""Update the class for all tags of a handle.""" """Update the class for all tags of a handle."""
@@ -188,7 +181,6 @@ class Index:
self.deleteHandle(tHandle) self.deleteHandle(tHandle)
else: else:
self._tagsIndex.updateClass(tHandle, item.itemClass.name) self._tagsIndex.updateClass(tHandle, item.itemClass.name)
return
def indexChangedSince(self, checkTime: int | float) -> bool: def indexChangedSince(self, checkTime: int | float) -> bool:
"""Check if the index has changed since a given time.""" """Check if the index has changed since a given time."""
@@ -211,7 +203,6 @@ class Index:
model.setExtraColumn(self._novelExtra) model.setExtraColumn(self._novelExtra)
self._appendSubTreeToModel(tHandle, model) self._appendSubTreeToModel(tHandle, model)
model.endResetModel() model.endResetModel()
return
def updateNovelModelData(self, nwItem: NWItem) -> bool: def updateNovelModelData(self, nwItem: NWItem) -> bool:
"""Refresh a novel model.""" """Refresh a novel model."""
@@ -428,8 +419,6 @@ class Index:
if updated or deleted: if updated or deleted:
SHARED.emitIndexChangedTags(self._project, updated, deleted) SHARED.emitIndexChangedTags(self._project, updated, deleted)
return
def _scanInactive(self, nwItem: NWItem, text: str) -> None: def _scanInactive(self, nwItem: NWItem, text: str) -> None:
"""Scan an inactive document for meta data.""" """Scan an inactive document for meta data."""
for line in text.splitlines(): for line in text.splitlines():
@@ -438,7 +427,6 @@ class Index:
if hDepth != "H0": if hDepth != "H0":
nwItem.setMainHeading(hDepth) nwItem.setMainHeading(hDepth)
break break
return
def _splitHeading(self, line: str) -> tuple[str, str]: def _splitHeading(self, line: str) -> tuple[str, str]:
"""Split a heading into its heading level and text value.""" """Split a heading into its heading level and text value."""
@@ -462,7 +450,6 @@ class Index:
"""Count text stats and save the counts to the index.""" """Count text stats and save the counts to the index."""
cC, wC, pC = standardCounter(text) cC, wC, pC = standardCounter(text)
self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC) self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
return
def _indexKeyword( def _indexKeyword(
self, tHandle: str, line: str, sTitle: str, itemClass: nwItemClass, tags: dict[str, bool] self, tHandle: str, line: str, sTitle: str, itemClass: nwItemClass, tags: dict[str, bool]
@@ -498,7 +485,6 @@ class Index:
model.setExtraColumn(self._novelExtra) model.setExtraColumn(self._novelExtra)
self._appendSubTreeToModel(tHandle, model) self._appendSubTreeToModel(tHandle, model)
self._novelModels[tHandle] = model self._novelModels[tHandle] = model
return
def _appendSubTreeToModel(self, tHandle: str, model: NovelModel) -> None: def _appendSubTreeToModel(self, tHandle: str, model: NovelModel) -> None:
"""Append all active novel documents to a novel model.""" """Append all active novel documents to a novel model."""
@@ -509,7 +495,6 @@ class Index:
and node.item.isActive and node.item.isActive
): ):
model.append(node) model.append(node)
return
## ##
# Check @ Lines # Check @ Lines
@@ -683,15 +668,13 @@ class Index:
"words": hItem.wordCount, "words": hItem.wordCount,
} }
result = [( return [(
tKey, tKey,
tData[tKey]["level"], tData[tKey]["level"],
tData[tKey]["title"], tData[tKey]["title"],
tData[tKey]["words"] tData[tKey]["words"]
) for tKey in tOrder] ) for tKey in tOrder]
return result
def getCounts(self, tHandle: str, sTitle: str | None = None) -> tuple[int, int, int]: def getCounts(self, tHandle: str, sTitle: str | None = None) -> tuple[int, int, int]:
"""Return the counts for a file, or a section of a file, """Return the counts for a file, or a section of a file,
starting at title sTitle if it is provided. starting at title sTitle if it is provided.
@@ -796,7 +779,7 @@ class Index:
# ===================== # =====================
class TagsIndex: class TagsIndex:
"""Core: Tags Index Wrapper Class """Core: Tags Index Wrapper Class.
A wrapper class that holds the reverse lookup tags index. This is A wrapper class that holds the reverse lookup tags index. This is
just a simple wrapper around a single dictionary to keep tighter just a simple wrapper around a single dictionary to keep tighter
@@ -807,14 +790,12 @@ class TagsIndex:
def __init__(self) -> None: def __init__(self) -> None:
self._tags: dict[str, dict[str, str]] = {} self._tags: dict[str, dict[str, str]] = {}
return
def __contains__(self, tagKey: str) -> bool: def __contains__(self, tagKey: str) -> bool:
return tagKey.lower() in self._tags return tagKey.lower() in self._tags
def __delitem__(self, tagKey: str) -> None: def __delitem__(self, tagKey: str) -> None:
self._tags.pop(tagKey.lower(), None) self._tags.pop(tagKey.lower(), None)
return
def __getitem__(self, tagKey: str) -> dict | None: def __getitem__(self, tagKey: str) -> dict | None:
return self._tags.get(tagKey.lower(), None) return self._tags.get(tagKey.lower(), None)
@@ -826,7 +807,6 @@ class TagsIndex:
def clear(self) -> None: def clear(self) -> None:
"""Clear the index.""" """Clear the index."""
self._tags = {} self._tags = {}
return
def items(self) -> ItemsView: def items(self) -> ItemsView:
"""Return a dictionary view of all tags.""" """Return a dictionary view of all tags."""
@@ -842,7 +822,6 @@ class TagsIndex:
"heading": sTitle, "heading": sTitle,
"class": className, "class": className,
} }
return
def tagName(self, tagKey: str, default: str = "") -> str: def tagName(self, tagKey: str, default: str = "") -> str:
"""Get the name of a given tag.""" """Get the name of a given tag."""
@@ -882,7 +861,6 @@ class TagsIndex:
for entry in self._tags.values(): for entry in self._tags.values():
if entry.get("handle") == tHandle: if entry.get("handle") == tHandle:
entry["class"] = className entry["class"] = className
return
## ##
# Pack/Unpack # Pack/Unpack
@@ -925,11 +903,9 @@ class TagsIndex:
self.add(name, display, handle, heading, className) self.add(name, display, handle, heading, className)
return
class IndexCache: class IndexCache:
"""Core: Item Index Lookup Data Class """Core: Item Index Lookup Data Class.
A small data class passed between all objects of the Item Index A small data class passed between all objects of the Item Index
which provides lookup capabilities and caching for shared data. which provides lookup capabilities and caching for shared data.
@@ -941,14 +917,13 @@ class IndexCache:
self.tags: TagsIndex = tagsIndex self.tags: TagsIndex = tagsIndex
self.story: set[str] = set() self.story: set[str] = set()
self.note: set[str] = set() self.note: set[str] = set()
return
# The Item Index Objects # The Item Index Objects
# ====================== # ======================
class ItemIndex: class ItemIndex:
"""Core: Item Index Wrapper Class """Core: Item Index Wrapper Class.
A wrapper object holding the indexed items. This is a wrapper A wrapper object holding the indexed items. This is a wrapper
class around a single storage dictionary with a set of utility class around a single storage dictionary with a set of utility
@@ -963,14 +938,12 @@ class ItemIndex:
self._project = project self._project = project
self._cache = IndexCache(tagsIndex) self._cache = IndexCache(tagsIndex)
self._items: dict[str, IndexNode] = {} self._items: dict[str, IndexNode] = {}
return
def __contains__(self, tHandle: str) -> bool: def __contains__(self, tHandle: str) -> bool:
return tHandle in self._items return tHandle in self._items
def __delitem__(self, tHandle: str) -> None: def __delitem__(self, tHandle: str) -> None:
self._items.pop(tHandle, None) self._items.pop(tHandle, None)
return
def __getitem__(self, tHandle: str) -> IndexNode | None: def __getitem__(self, tHandle: str) -> IndexNode | None:
return self._items.get(tHandle, None) return self._items.get(tHandle, None)
@@ -982,14 +955,12 @@ class ItemIndex:
def clear(self) -> None: def clear(self) -> None:
"""Clear the index.""" """Clear the index."""
self._items = {} self._items = {}
return
def add(self, tHandle: str, nwItem: NWItem) -> None: def add(self, tHandle: str, nwItem: NWItem) -> None:
"""Add a new item to the index. This will overwrite the item if """Add a new item to the index. This will overwrite the item if
it already exists. it already exists.
""" """
self._items[tHandle] = IndexNode(self._cache, tHandle, nwItem) self._items[tHandle] = IndexNode(self._cache, tHandle, nwItem)
return
def allStoryKeys(self) -> set[str]: def allStoryKeys(self) -> set[str]:
"""Return all story structure keys.""" """Return all story structure keys."""
@@ -1064,7 +1035,6 @@ class ItemIndex:
""" """
if tHandle in self._items: if tHandle in self._items:
self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC) self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC)
return
def setHeadingComment( def setHeadingComment(
self, tHandle: str, sTitle: str, self, tHandle: str, sTitle: str,
@@ -1073,25 +1043,21 @@ class ItemIndex:
"""Set a story comment for a heading on a given item.""" """Set a story comment for a heading on a given item."""
if tHandle in self._items: if tHandle in self._items:
self._items[tHandle].setHeadingComment(sTitle, comment, key, text) self._items[tHandle].setHeadingComment(sTitle, comment, key, text)
return
def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str) -> None: def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str) -> None:
"""Set the main tag for a heading on a given item.""" """Set the main tag for a heading on a given item."""
if tHandle in self._items: if tHandle in self._items:
self._items[tHandle].setHeadingTag(sTitle, tagKey) self._items[tHandle].setHeadingTag(sTitle, tagKey)
return
def addHeadingRef(self, tHandle: str, sTitle: str, tagKeys: list[str], refType: str) -> None: def addHeadingRef(self, tHandle: str, sTitle: str, tagKeys: list[str], refType: str) -> None:
"""Set the reference tags for a heading on a given item.""" """Set the reference tags for a heading on a given item."""
if tHandle in self._items: if tHandle in self._items:
self._items[tHandle].addHeadingRef(sTitle, tagKeys, refType) self._items[tHandle].addHeadingRef(sTitle, tagKeys, refType)
return
def addNoteKey(self, tHandle: str, style: T_NoteTypes, key: str) -> None: def addNoteKey(self, tHandle: str, style: T_NoteTypes, key: str) -> None:
"""Set notes key for a given item.""" """Set notes key for a given item."""
if tHandle in self._items: if tHandle in self._items:
self._items[tHandle].addNoteKey(style, key) self._items[tHandle].addNoteKey(style, key)
return
def genNewNoteKey(self, tHandle: str, style: T_NoteTypes) -> str: def genNewNoteKey(self, tHandle: str, style: T_NoteTypes) -> str:
"""Set notes key for a given item.""" """Set notes key for a given item."""
@@ -1131,5 +1097,3 @@ class ItemIndex:
tItem = IndexNode(self._cache, tHandle, nwItem) tItem = IndexNode(self._cache, tHandle, nwItem)
tItem.unpackData(tData) tItem.unpackData(tData)
self._items[tHandle] = tItem self._items[tHandle] = tItem
return
+3 -19
View File
@@ -23,7 +23,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -50,7 +50,7 @@ NOTE_TYPES: list[T_NoteTypes] = ["footnotes", "comments"]
class IndexNode: class IndexNode:
"""Core: Single Index Item Node Class """Core: Single Index Item Node Class.
This object represents the index data of a project item (NWItem). This object represents the index data of a project item (NWItem).
It holds a record of all the headings in the text, and the meta data It holds a record of all the headings in the text, and the meta data
@@ -68,7 +68,6 @@ class IndexNode:
self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(self._cache, TT_NONE)} self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(self._cache, TT_NONE)}
self._notes: dict[str, set[str]] = {} self._notes: dict[str, set[str]] = {}
self._count = 0 self._count = 0
return
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<IndexNode handle='{self._handle}'>" return f"<IndexNode handle='{self._handle}'>"
@@ -107,39 +106,33 @@ class IndexNode:
if TT_NONE in self._headings: if TT_NONE in self._headings:
self._headings.pop(TT_NONE) self._headings.pop(TT_NONE)
self._headings[tHeading.key] = tHeading self._headings[tHeading.key] = tHeading
return
def setHeadingCounts(self, sTitle: str, cCount: int, wCount: int, pCount: int) -> None: def setHeadingCounts(self, sTitle: str, cCount: int, wCount: int, pCount: int) -> None:
"""Set the character, word and paragraph count of a heading.""" """Set the character, word and paragraph count of a heading."""
if sTitle in self._headings: if sTitle in self._headings:
self._headings[sTitle].setCounts([cCount, wCount, pCount]) self._headings[sTitle].setCounts([cCount, wCount, pCount])
return
def setHeadingComment(self, sTitle: str, comment: nwComment, key: str, text: str) -> None: def setHeadingComment(self, sTitle: str, comment: nwComment, key: str, text: str) -> None:
"""Set the comment text of a heading.""" """Set the comment text of a heading."""
if sTitle in self._headings: if sTitle in self._headings:
self._headings[sTitle].setComment(comment.name, key, text) self._headings[sTitle].setComment(comment.name, key, text)
return
def setHeadingTag(self, sTitle: str, tag: str) -> None: def setHeadingTag(self, sTitle: str, tag: str) -> None:
"""Set the tag of a heading.""" """Set the tag of a heading."""
if sTitle in self._headings: if sTitle in self._headings:
self._headings[sTitle].setTag(tag) self._headings[sTitle].setTag(tag)
return
def addHeadingRef(self, sTitle: str, tags: list[str], keyword: str) -> None: def addHeadingRef(self, sTitle: str, tags: list[str], keyword: str) -> None:
"""Add a reference key and all its types to a heading.""" """Add a reference key and all its types to a heading."""
if sTitle in self._headings: if sTitle in self._headings:
for tag in tags: for tag in tags:
self._headings[sTitle].addReference(tag, keyword) self._headings[sTitle].addReference(tag, keyword)
return
def addNoteKey(self, style: T_NoteTypes, key: str) -> None: def addNoteKey(self, style: T_NoteTypes, key: str) -> None:
"""Add a note key to the index.""" """Add a note key to the index."""
if style not in self._notes: if style not in self._notes:
self._notes[style] = set() self._notes[style] = set()
self._notes[style].add(key) self._notes[style].add(key)
return
## ##
# Data Methods # Data Methods
@@ -195,11 +188,10 @@ class IndexNode:
self._notes[style] = set(keys) self._notes[style] = set(keys)
else: else:
raise KeyError("Index node contains an invalid key") raise KeyError("Index node contains an invalid key")
return
class IndexHeading: class IndexHeading:
"""Core: Single Index Heading Class """Core: Single Index Heading Class.
This object represents a section of text in a project item This object represents a section of text in a project item
associated with a single (valid) heading. It holds a separate record associated with a single (valid) heading. It holds a separate record
@@ -224,7 +216,6 @@ class IndexHeading:
self._tag = "" self._tag = ""
self._refs: dict[str, set[str]] = {} self._refs: dict[str, set[str]] = {}
self._comments: dict[str, str] = {} self._comments: dict[str, str] = {}
return
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<IndexHeading key='{self._key}'>" return f"<IndexHeading key='{self._key}'>"
@@ -289,12 +280,10 @@ class IndexHeading:
"""Set the level of the heading if it's a valid value.""" """Set the level of the heading if it's a valid value."""
if level in nwStyles.H_VALID: if level in nwStyles.H_VALID:
self._level = level self._level = level
return
def setLine(self, line: int) -> None: def setLine(self, line: int) -> None:
"""Set the line number of a heading.""" """Set the line number of a heading."""
self._line = max(0, checkInt(line, 0)) self._line = max(0, checkInt(line, 0))
return
def setCounts(self, counts: Sequence[int]) -> None: def setCounts(self, counts: Sequence[int]) -> None:
"""Set the character, word and paragraph count. Make sure the """Set the character, word and paragraph count. Make sure the
@@ -306,7 +295,6 @@ class IndexHeading:
max(0, checkInt(counts[1], 0)), max(0, checkInt(counts[1], 0)),
max(0, checkInt(counts[2], 0)), max(0, checkInt(counts[2], 0)),
) )
return
def setComment(self, comment: str, key: str, text: str) -> None: def setComment(self, comment: str, key: str, text: str) -> None:
"""Set the text for a comment and make sure it is a string.""" """Set the text for a comment and make sure it is a string."""
@@ -319,12 +307,10 @@ class IndexHeading:
case "note" if key: case "note" if key:
self._cache.note.add(key) self._cache.note.add(key)
self._comments[f"note.{key}"] = str(text) self._comments[f"note.{key}"] = str(text)
return
def setTag(self, tag: str) -> None: def setTag(self, tag: str) -> None:
"""Set the tag for references, and make sure it is a string.""" """Set the tag for references, and make sure it is a string."""
self._tag = str(tag).lower() self._tag = str(tag).lower()
return
def addReference(self, tag: str, keyword: str) -> None: def addReference(self, tag: str, keyword: str) -> None:
"""Add a record of a reference tag, and what keyword types it is """Add a record of a reference tag, and what keyword types it is
@@ -335,7 +321,6 @@ class IndexHeading:
if tag not in self._refs: if tag not in self._refs:
self._refs[tag] = set() self._refs[tag] = set()
self._refs[tag].add(keyword) self._refs[tag].add(keyword)
return
## ##
# Getters # Getters
@@ -409,4 +394,3 @@ class IndexHeading:
self.setComment(comment, compact(kind), str(entry)) self.setComment(comment, compact(kind), str(entry))
else: else:
raise KeyError("Unknown key in heading entry") raise KeyError("Unknown key in heading entry")
return
+7 -31
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class NWItem: class NWItem:
"""Core: Item Data Class """Core: Item Data Class.
This class holds all the project information about a project item. This class holds all the project information about a project item.
Each item must be associated with a project and have a valid handle. Each item must be associated with a project and have a valid handle.
@@ -84,16 +84,14 @@ class NWItem:
self._wordInit = 0 # Initial character count self._wordInit = 0 # Initial character count
self._charInit = 0 # Initial word count self._charInit = 0 # Initial word count
return
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>" return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>"
def __bool__(self) -> bool: def __bool__(self) -> bool:
"""The truthiness of the class. The handle used to be initiated """Check the truthiness of the class. The handle used to be
to None, but this is no longer the case. It should always initiated to None, but this is no longer the case. It should
evaluate to True since 2.1-beta1, although unpack and the NWTree always evaluate to True since 2.1-beta1, although unpack and the
class can leave it as an empty string. NWTree class can leave it as an empty string.
""" """
return bool(self._handle) return bool(self._handle)
@@ -206,15 +204,13 @@ class NWItem:
meta["cursorPos"] = str(self._cursorPos) meta["cursorPos"] = str(self._cursorPos)
name["active"] = yesNo(self._active) name["active"] = yesNo(self._active)
data = { return {
"name": str(self._name), "name": str(self._name),
"itemAttr": item, "itemAttr": item,
"metaAttr": meta, "metaAttr": meta,
"nameAttr": name, "nameAttr": name,
} }
return data
def unpack(self, data: dict) -> bool: def unpack(self, data: dict) -> bool:
"""Set the values from a data dictionary.""" """Set the values from a data dictionary."""
item = data.get("itemAttr", {}) item = data.get("itemAttr", {})
@@ -298,13 +294,11 @@ class NWItem:
def notifyToRefresh(self) -> None: def notifyToRefresh(self) -> None:
"""Notify GUI that item info needs to be refreshed.""" """Notify GUI that item info needs to be refreshed."""
self._project.tree.refreshItems([self._handle]) self._project.tree.refreshItems([self._handle])
return
def notifyNovelStructureChange(self) -> None: def notifyNovelStructureChange(self) -> None:
"""Notify that the structure of a novel has changed.""" """Notify that the structure of a novel has changed."""
if self._root and self._class == nwItemClass.NOVEL: if self._root and self._class == nwItemClass.NOVEL:
self._project.tree.novelStructureChanged(self._root) self._project.tree.novelStructureChanged(self._root)
return
## ##
# Lookup Methods # Lookup Methods
@@ -457,8 +451,6 @@ class NWItem:
if self._import is None: if self._import is None:
self.setImport("New") # This forces a default value lookup self.setImport("New") # This forces a default value lookup
return
## ##
# Set Item Values # Set Item Values
## ##
@@ -469,7 +461,6 @@ class NWItem:
self._name = simplified(name) self._name = simplified(name)
else: else:
self._name = "" self._name = ""
return
def setParent(self, handle: Any) -> None: def setParent(self, handle: Any) -> None:
"""Set the parent handle, and ensure it is valid.""" """Set the parent handle, and ensure it is valid."""
@@ -479,7 +470,6 @@ class NWItem:
self._parent = handle self._parent = handle
else: else:
self._parent = None self._parent = None
return
def setRoot(self, handle: Any) -> None: def setRoot(self, handle: Any) -> None:
"""Set the root handle, and ensure it is valid.""" """Set the root handle, and ensure it is valid."""
@@ -489,7 +479,6 @@ class NWItem:
self._root = handle self._root = handle
else: else:
self._root = None self._root = None
return
def setOrder(self, order: Any) -> None: def setOrder(self, order: Any) -> None:
"""Set the item order, and ensure that it is valid. This value """Set the item order, and ensure that it is valid. This value
@@ -497,7 +486,6 @@ class NWItem:
the moment. the moment.
""" """
self._order = checkInt(order, 0) self._order = checkInt(order, 0)
return
def setType(self, value: Any) -> None: def setType(self, value: Any) -> None:
"""Set the item type from either a proper nwItemType, or set it """Set the item type from either a proper nwItemType, or set it
@@ -510,7 +498,6 @@ class NWItem:
else: else:
logger.error("Unrecognised item type '%s'", value) logger.error("Unrecognised item type '%s'", value)
self._type = nwItemType.NO_TYPE self._type = nwItemType.NO_TYPE
return
def setClass(self, value: Any) -> None: def setClass(self, value: Any) -> None:
"""Set the item class from either a proper nwItemClass, or set """Set the item class from either a proper nwItemClass, or set
@@ -523,7 +510,6 @@ class NWItem:
else: else:
logger.error("Unrecognised item class '%s'", value) logger.error("Unrecognised item class '%s'", value)
self._class = nwItemClass.NO_CLASS self._class = nwItemClass.NO_CLASS
return
def setLayout(self, value: Any) -> None: def setLayout(self, value: Any) -> None:
"""Set the item layout from either a proper nwItemLayout, or set """Set the item layout from either a proper nwItemLayout, or set
@@ -536,21 +522,18 @@ class NWItem:
else: else:
logger.error("Unrecognised item layout '%s'", value) logger.error("Unrecognised item layout '%s'", value)
self._layout = nwItemLayout.NO_LAYOUT self._layout = nwItemLayout.NO_LAYOUT
return
def setStatus(self, value: Any) -> None: def setStatus(self, value: Any) -> None:
"""Set the item status by looking it up in the valid status """Set the item status by looking it up in the valid status
items of the current project. items of the current project.
""" """
self._status = self._project.data.itemStatus.check(value) self._status = self._project.data.itemStatus.check(value)
return
def setImport(self, value: Any) -> None: def setImport(self, value: Any) -> None:
"""Set the item importance by looking it up in the valid import """Set the item importance by looking it up in the valid import
items of the current project. items of the current project.
""" """
self._import = self._project.data.itemImport.check(value) self._import = self._project.data.itemImport.check(value)
return
def setActive(self, state: Any) -> None: def setActive(self, state: Any) -> None:
"""Set the active flag.""" """Set the active flag."""
@@ -558,7 +541,6 @@ class NWItem:
self._active = state self._active = state
else: else:
self._active = False self._active = False
return
def setExpanded(self, state: Any) -> None: def setExpanded(self, state: Any) -> None:
"""Set the expanded status of an item in the project tree.""" """Set the expanded status of an item in the project tree."""
@@ -566,7 +548,6 @@ class NWItem:
self._expanded = state self._expanded = state
else: else:
self._expanded = False self._expanded = False
return
## ##
# Set Document Meta Data # Set Document Meta Data
@@ -576,7 +557,6 @@ class NWItem:
"""Set the main heading level.""" """Set the main heading level."""
if value in nwStyles.H_LEVEL: if value in nwStyles.H_LEVEL:
self._heading = value self._heading = value
return
def setCharCount(self, count: Any) -> None: def setCharCount(self, count: Any) -> None:
"""Set the character count, and ensure that it is an integer.""" """Set the character count, and ensure that it is an integer."""
@@ -584,7 +564,6 @@ class NWItem:
self._charCount = max(0, count) self._charCount = max(0, count)
else: else:
self._charCount = 0 self._charCount = 0
return
def setWordCount(self, count: Any) -> None: def setWordCount(self, count: Any) -> None:
"""Set the word count, and ensure that it is an integer.""" """Set the word count, and ensure that it is an integer."""
@@ -592,7 +571,6 @@ class NWItem:
self._wordCount = max(0, count) self._wordCount = max(0, count)
else: else:
self._wordCount = 0 self._wordCount = 0
return
def setParaCount(self, count: Any) -> None: def setParaCount(self, count: Any) -> None:
"""Set the paragraph count, and ensure that it is an integer.""" """Set the paragraph count, and ensure that it is an integer."""
@@ -600,7 +578,6 @@ class NWItem:
self._paraCount = max(0, count) self._paraCount = max(0, count)
else: else:
self._paraCount = 0 self._paraCount = 0
return
def setCursorPos(self, position: Any) -> None: def setCursorPos(self, position: Any) -> None:
"""Set the cursor position, and ensure that it is an integer.""" """Set the cursor position, and ensure that it is an integer."""
@@ -608,4 +585,3 @@ class NWItem:
self._cursorPos = max(0, position) self._cursorPos = max(0, position)
else: else:
self._cursorPos = 0 self._cursorPos = 0
return
+5 -21
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -66,7 +66,7 @@ T_NodeData = str | QIcon | QFont | Qt.AlignmentFlag | None
class ProjectNode: class ProjectNode:
"""Core: Project Model Node Class """Core: Project Model Node Class.
The project tree structure is saved as nodes in a tree, starting The project tree structure is saved as nodes in a tree, starting
from a root node. This class makes up these nodes. from a root node. This class makes up these nodes.
@@ -103,7 +103,6 @@ class ProjectNode:
self._count = 0 self._count = 0
self.refresh() self.refresh()
self.updateCount() self.updateCount()
return
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@@ -114,7 +113,7 @@ class ProjectNode:
) )
def __bool__(self) -> bool: def __bool__(self) -> bool:
"""A node should always evaluate to True.""" # A node should always evaluate to True.
return True return True
## ##
@@ -162,15 +161,12 @@ class ProjectNode:
self._cache[C_STATUS_TIP] = sText self._cache[C_STATUS_TIP] = sText
self._cache[C_STATUS_ACCESS] = sText self._cache[C_STATUS_ACCESS] = sText
return
def updateCount(self, propagate: bool = True) -> None: def updateCount(self, propagate: bool = True) -> None:
"""Update counts, and propagate upwards in the tree.""" """Update counts, and propagate upwards in the tree."""
self._count = self._item.mainCount + sum(c._count for c in self._children) # noqa: SLF001 self._count = self._item.mainCount + sum(c._count for c in self._children) # noqa: SLF001
self._cache[C_COUNT_TEXT] = f"{self._count:n}" self._cache[C_COUNT_TEXT] = f"{self._count:n}"
if propagate and (parent := self._parent): if propagate and (parent := self._parent):
parent.updateCount() parent.updateCount()
return
## ##
# Data Access # Data Access
@@ -223,7 +219,6 @@ class ProjectNode:
self._children.append(child) self._children.append(child)
self._refreshChildrenPos() self._refreshChildrenPos()
self._item.notifyNovelStructureChange() self._item.notifyNovelStructureChange()
return
def takeChild(self, pos: int) -> ProjectNode | None: def takeChild(self, pos: int) -> ProjectNode | None:
"""Remove a child item and return it.""" """Remove a child item and return it."""
@@ -243,7 +238,6 @@ class ProjectNode:
self._children.insert(target, node) self._children.insert(target, node)
self._refreshChildrenPos() self._refreshChildrenPos()
self._item.notifyNovelStructureChange() self._item.notifyNovelStructureChange()
return
def setExpanded(self, state: bool) -> None: def setExpanded(self, state: bool) -> None:
"""Set the node's expanded state.""" """Set the node's expanded state."""
@@ -251,7 +245,6 @@ class ProjectNode:
self._item.setExpanded(True) self._item.setExpanded(True)
else: else:
self._item.setExpanded(False) self._item.setExpanded(False)
return
## ##
# Internal Functions # Internal Functions
@@ -262,14 +255,12 @@ class ProjectNode:
for node in self._children: for node in self._children:
children.append(node) children.append(node)
node._recursiveAppendChildren(children) # noqa: SLF001 node._recursiveAppendChildren(children) # noqa: SLF001
return
def _refreshChildrenPos(self) -> None: def _refreshChildrenPos(self) -> None:
"""Update the row value on all children.""" """Update the row value on all children."""
for n, child in enumerate(self._children): for n, child in enumerate(self._children):
child._row = n # noqa: SLF001 child._row = n # noqa: SLF001
child.item.setOrder(n) child.item.setOrder(n)
return
def _updateRelationships(self, child: ProjectNode) -> None: def _updateRelationships(self, child: ProjectNode) -> None:
"""Update a child item's relationships.""" """Update a child item's relationships."""
@@ -282,11 +273,10 @@ class ProjectNode:
child.item.setParent(None) child.item.setParent(None)
child.item.setRoot(child.item.itemHandle) child.item.setRoot(child.item.itemHandle)
child.item.setClassDefaults(child.item.itemClass) child.item.setClassDefaults(child.item.itemClass)
return
class ProjectModel(QAbstractItemModel): class ProjectModel(QAbstractItemModel):
"""Core: Project Model Class """Core: Project Model Class.
This class provides the interface for the tree widget used on the This class provides the interface for the tree widget used on the
GUI. It implements the QModelIndex based interface required, adds GUI. It implements the QModelIndex based interface required, adds
@@ -302,11 +292,9 @@ class ProjectModel(QAbstractItemModel):
self._root = ProjectNode(NWItem(tree.project, INV_ROOT)) self._root = ProjectNode(NWItem(tree.project, INV_ROOT))
self._root.item.setName("Invisible Root") self._root.item.setName("Invisible Root")
logger.debug("Ready: ProjectModel") logger.debug("Ready: ProjectModel")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: ProjectModel") logger.debug("Delete: ProjectModel")
return
## ##
# Properties # Properties
@@ -363,7 +351,7 @@ class ProjectModel(QAbstractItemModel):
## ##
def supportedDropActions(self) -> Qt.DropAction: def supportedDropActions(self) -> Qt.DropAction:
"""Return supported drop actions""" """Return supported drop actions."""
return Qt.DropAction.MoveAction return Qt.DropAction.MoveAction
def mimeTypes(self) -> list[str]: def mimeTypes(self) -> list[str]:
@@ -445,7 +433,6 @@ class ProjectModel(QAbstractItemModel):
self.beginInsertRows(parent, row, row) self.beginInsertRows(parent, row, row)
node.addChild(child, row) node.addChild(child, row)
self.endInsertRows() self.endInsertRows()
return
def removeChild(self, parent: QModelIndex, pos: int) -> ProjectNode | None: def removeChild(self, parent: QModelIndex, pos: int) -> ProjectNode | None:
"""Remove a node from the model and return it.""" """Remove a node from the model and return it."""
@@ -469,7 +456,6 @@ class ProjectModel(QAbstractItemModel):
self.beginMoveRows(index.parent(), pos, pos, index.parent(), end) self.beginMoveRows(index.parent(), pos, pos, index.parent(), end)
parent.moveChild(pos, new) parent.moveChild(pos, new)
self.endMoveRows() self.endMoveRows()
return
def multiMove(self, indices: list[QModelIndex], target: QModelIndex, pos: int = -1) -> None: def multiMove(self, indices: list[QModelIndex], target: QModelIndex, pos: int = -1) -> None:
"""Move multiple items to a new location.""" """Move multiple items to a new location."""
@@ -497,7 +483,6 @@ class ProjectModel(QAbstractItemModel):
node._updateRelationships(child) # noqa: SLF001 node._updateRelationships(child) # noqa: SLF001
child.item.notifyToRefresh() child.item.notifyToRefresh()
node.item.notifyToRefresh() node.item.notifyToRefresh()
return
## ##
# Other Methods # Other Methods
@@ -506,7 +491,6 @@ class ProjectModel(QAbstractItemModel):
def clear(self) -> None: def clear(self) -> None:
"""Clear the project model.""" """Clear the project model."""
self._root.children.clear() self._root.children.clear()
return
def allExpanded(self) -> list[QModelIndex]: def allExpanded(self) -> list[QModelIndex]:
"""Return a list of all expanded items.""" """Return a list of all expanded items."""
+2 -6
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -54,6 +54,7 @@ T_NodeData = str | QIcon | QPixmap | Qt.AlignmentFlag | None
class NovelModel(QAbstractTableModel): class NovelModel(QAbstractTableModel):
"""Core: Novel Model CLass."""
__slots__ = ("_columns", "_extraKey", "_extraLabel", "_more", "_rows") __slots__ = ("_columns", "_extraKey", "_extraLabel", "_more", "_rows")
@@ -64,11 +65,9 @@ class NovelModel(QAbstractTableModel):
self._columns = 3 self._columns = 3
self._extraKey = "" self._extraKey = ""
self._extraLabel = "" self._extraLabel = ""
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NovelModel") logger.debug("Delete: NovelModel")
return
## ##
# Properties # Properties
@@ -102,7 +101,6 @@ class NovelModel(QAbstractTableModel):
self._columns = 4 self._columns = 4
self._extraKey = nwKeyWords.PLOT_KEY self._extraKey = nwKeyWords.PLOT_KEY
self._extraLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]) self._extraLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
return
## ##
# Model Interface # Model Interface
@@ -147,7 +145,6 @@ class NovelModel(QAbstractTableModel):
def clear(self) -> None: def clear(self) -> None:
"""Clear the model.""" """Clear the model."""
self._rows.clear() self._rows.clear()
return
def append(self, node: IndexNode) -> None: def append(self, node: IndexNode) -> None:
"""Append a node to the model.""" """Append a node to the model."""
@@ -155,7 +152,6 @@ class NovelModel(QAbstractTableModel):
for key, head in node.items(): for key, head in node.items():
if key != "T0000": if key != "T0000":
self._rows.append(self._generateEntry(handle, key, head)) self._rows.append(self._generateEntry(handle, key, head))
return
def refresh(self, node: IndexNode) -> bool: def refresh(self, node: IndexNode) -> bool:
"""Refresh an index node.""" """Refresh an index node."""
+2 -3
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -80,7 +80,7 @@ VALID_MAP: dict[str, set[str]] = {
class OptionState: class OptionState:
"""Core: GUI Options Storage """Core: GUI Options Storage.
A class for storing the state of the GUI. The data is stored per A class for storing the state of the GUI. The data is stored per
project. Settings that should be project-independent are stored in project. Settings that should be project-independent are stored in
@@ -90,7 +90,6 @@ class OptionState:
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject) -> None:
self._project = project self._project = project
self._state = {} self._state = {}
return
## ##
# Load and Save Cache # Load and Save Cache
+7 -13
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -57,6 +57,7 @@ logger = logging.getLogger(__name__)
class NWProjectState(Enum): class NWProjectState(Enum):
"""The state of the loaded project."""
UNKNOWN = 0 UNKNOWN = 0
LOCKED = 1 LOCKED = 1
@@ -65,6 +66,11 @@ class NWProjectState(Enum):
class NWProject: class NWProject:
"""Core: novelWriter Project Class.
This class is the parent class of the project, and holds instances
of project data, the project tree, and the project index.
"""
__slots__ = ( __slots__ = (
"_changed", "_data", "_index", "_langData", "_options", "_session", "_changed", "_data", "_index", "_langData", "_options", "_session",
@@ -92,17 +98,13 @@ class NWProject:
logger.debug("Ready: NWProject") logger.debug("Ready: NWProject")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWProject") logger.debug("Delete: NWProject")
return
def clear(self) -> None: def clear(self) -> None:
"""Clear the project.""" """Clear the project."""
self._tree.clear() self._tree.clear()
self._index.clear() self._index.clear()
return
## ##
# Properties # Properties
@@ -263,7 +265,6 @@ class NWProject:
if rHandle and (tHandle := SHARED.project.newFile(tag.title(), rHandle)): if rHandle and (tHandle := SHARED.project.newFile(tag.title(), rHandle)):
self.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n") self.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
self._tree.refreshItems([tHandle]) self._tree.refreshItems([tHandle])
return
## ##
# Project Methods # Project Methods
@@ -441,7 +442,6 @@ class NWProject:
self._tree.writeToCFile() self._tree.writeToCFile()
self._session.appendSession(idleTime) self._session.appendSession(idleTime)
self._storage.closeSession() self._storage.closeSession()
return
def backupProject(self, doNotify: bool) -> bool: def backupProject(self, doNotify: bool) -> bool:
"""Create a zip file of the entire project.""" """Create a zip file of the entire project."""
@@ -499,7 +499,6 @@ class NWProject:
self._data.itemImport.add(None, self.tr("Minor"), "purple", "BLOCK_2", 0) self._data.itemImport.add(None, self.tr("Minor"), "purple", "BLOCK_2", 0)
self._data.itemImport.add(None, self.tr("Major"), "purple", "BLOCK_3", 0) self._data.itemImport.add(None, self.tr("Major"), "purple", "BLOCK_3", 0)
self._data.itemImport.add(None, self.tr("Main"), "purple", "BLOCK_4", 0) self._data.itemImport.add(None, self.tr("Main"), "purple", "BLOCK_4", 0)
return
def setProjectLang(self, language: str | None) -> None: def setProjectLang(self, language: str | None) -> None:
"""Set the project-specific language.""" """Set the project-specific language."""
@@ -508,7 +507,6 @@ class NWProject:
self._data.setLanguage(language) self._data.setLanguage(language)
self._loadProjectLocalisation() self._loadProjectLocalisation()
self.setProjectChanged(True) self.setProjectChanged(True)
return
def setProjectChanged(self, status: bool) -> bool: def setProjectChanged(self, status: bool) -> bool:
"""Toggle the project changed flag, and propagate the """Toggle the project changed flag, and propagate the
@@ -527,7 +525,6 @@ class NWProject:
"""Update the total word and character count values.""" """Update the total word and character count values."""
wNovel, wNotes, cNovel, cNotes = self._tree.sumCounts() wNovel, wNotes, cNovel, cNotes = self._tree.sumCounts()
self._data.setCurrCounts(wNovel=wNovel, wNotes=wNotes, cNovel=cNovel, cNotes=cNotes) self._data.setCurrCounts(wNovel=wNovel, wNotes=wNotes, cNovel=cNovel, cNotes=cNotes)
return
def countStatus(self) -> None: def countStatus(self) -> None:
"""Count how many times the various status flags are used in the """Count how many times the various status flags are used in the
@@ -541,7 +538,6 @@ class NWProject:
self._data.itemStatus.increment(nwItem.itemStatus) self._data.itemStatus.increment(nwItem.itemStatus)
else: else:
self._data.itemImport.increment(nwItem.itemImport) self._data.itemImport.increment(nwItem.itemImport)
return
def updateStatus(self, kind: T_StatusKind, update: T_UpdateEntry) -> None: def updateStatus(self, kind: T_StatusKind, update: T_UpdateEntry) -> None:
"""Update status or import entries.""" """Update status or import entries."""
@@ -553,13 +549,11 @@ class NWProject:
self._data.itemImport.update(update) self._data.itemImport.update(update)
SHARED.emitStatusLabelsChanged(self, kind) SHARED.emitStatusLabelsChanged(self, kind)
self._tree.refreshAllItems() self._tree.refreshAllItems()
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self._data.itemStatus.refreshIcons() self._data.itemStatus.refreshIcons()
self._data.itemImport.refreshIcons() self._data.itemImport.refreshIcons()
return
def localLookup(self, word: str | int) -> str: def localLookup(self, word: str | int) -> str:
"""Look up a word or number in the translation map for the """Look up a word or number in the translation map for the
+2 -21
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
class NWProjectData: class NWProjectData:
"""Core: Project Data Class """Core: Project Data Class.
The class holds all project data from the main XML file, aside from The class holds all project data from the main XML file, aside from
the list of project items. the list of project items.
@@ -86,8 +86,6 @@ class NWProjectData:
self._status = NWStatus(NWStatus.STATUS) self._status = NWStatus(NWStatus.STATUS)
self._import = NWStatus(NWStatus.IMPORT) self._import = NWStatus(NWStatus.IMPORT)
return
## ##
# Properties # Properties
## ##
@@ -191,13 +189,11 @@ class NWProjectData:
"""Increment the save count by one.""" """Increment the save count by one."""
self._saveCount += 1 self._saveCount += 1
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def incAutoCount(self) -> None: def incAutoCount(self) -> None:
"""Increment the auto save count by one.""" """Increment the auto save count by one."""
self._autoCount += 1 self._autoCount += 1
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
## ##
# Getters # Getters
@@ -219,67 +215,57 @@ class NWProjectData:
elif value != self._uuid: elif value != self._uuid:
self._uuid = value self._uuid = value
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setName(self, value: str | None) -> None: def setName(self, value: str | None) -> None:
"""Set a new project name.""" """Set a new project name."""
if value != self._name: if value != self._name:
self._name = simplified(str(value or "")) self._name = simplified(str(value or ""))
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setAuthor(self, value: str | None) -> None: def setAuthor(self, value: str | None) -> None:
"""Set the author value.""" """Set the author value."""
if value != self._author: if value != self._author:
self._author = simplified(str(value or "")) self._author = simplified(str(value or ""))
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setSaveCount(self, value: Any) -> None: def setSaveCount(self, value: Any) -> None:
"""Set the save count from last session.""" """Set the save count from last session."""
self._saveCount = checkInt(value, 0) self._saveCount = checkInt(value, 0)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setAutoCount(self, value: Any) -> None: def setAutoCount(self, value: Any) -> None:
"""Set the auto save count from last session.""" """Set the auto save count from last session."""
self._autoCount = checkInt(value, 0) self._autoCount = checkInt(value, 0)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setEditTime(self, value: Any) -> None: def setEditTime(self, value: Any) -> None:
"""Set the edit time from last session.""" """Set the edit time from last session."""
self._editTime = checkInt(value, 0) self._editTime = checkInt(value, 0)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setDoBackup(self, value: Any) -> None: def setDoBackup(self, value: Any) -> None:
"""Set the do write backup flag.""" """Set the do write backup flag."""
if value != self._doBackup: if value != self._doBackup:
self._doBackup = checkBool(value, False) self._doBackup = checkBool(value, False)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setLanguage(self, value: str | None) -> None: def setLanguage(self, value: str | None) -> None:
"""Set the project language.""" """Set the project language."""
if value != self._language: if value != self._language:
self._language = checkStringNone(value, None) self._language = checkStringNone(value, None)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setSpellCheck(self, value: Any) -> None: def setSpellCheck(self, value: Any) -> None:
"""Set the spell check flag.""" """Set the spell check flag."""
if value != self._spellCheck: if value != self._spellCheck:
self._spellCheck = checkBool(value, False) self._spellCheck = checkBool(value, False)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setSpellLang(self, value: str | None) -> None: def setSpellLang(self, value: str | None) -> None:
"""Set the spell check language.""" """Set the spell check language."""
if value != self._spellLang: if value != self._spellLang:
self._spellLang = checkStringNone(value, None) self._spellLang = checkStringNone(value, None)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setLastHandle(self, value: str | None, component: str) -> None: def setLastHandle(self, value: str | None, component: str) -> None:
"""Set a last used handle into the handle registry for a given """Set a last used handle into the handle registry for a given
@@ -288,7 +274,6 @@ class NWProjectData:
if isinstance(component, str): if isinstance(component, str):
self._lastHandle[component] = checkStringNone(value, None) self._lastHandle[component] = checkStringNone(value, None)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setLastHandles(self, value: dict) -> None: def setLastHandles(self, value: dict) -> None:
"""Set the full last handles dictionary to a new set of values. """Set the full last handles dictionary to a new set of values.
@@ -299,7 +284,6 @@ class NWProjectData:
if key in self._lastHandle: if key in self._lastHandle:
self._lastHandle[key] = str(entry) if isHandle(entry) else None self._lastHandle[key] = str(entry) if isHandle(entry) else None
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
def setInitCounts( def setInitCounts(
self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None
@@ -321,7 +305,6 @@ class NWProjectData:
count = checkInt(cNotes, 0) count = checkInt(cNotes, 0)
self._initCounts[3] = count self._initCounts[3] = count
self._currCounts[3] = count self._currCounts[3] = count
return
def setCurrCounts( def setCurrCounts(
self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None
@@ -335,7 +318,6 @@ class NWProjectData:
self._currCounts[2] = checkInt(cNovel, 0) self._currCounts[2] = checkInt(cNovel, 0)
if cNotes is not None: if cNotes is not None:
self._currCounts[3] = checkInt(cNotes, 0) self._currCounts[3] = checkInt(cNotes, 0)
return
def setAutoReplace(self, value: dict) -> None: def setAutoReplace(self, value: dict) -> None:
"""Set the auto-replace dictionary.""" """Set the auto-replace dictionary."""
@@ -345,4 +327,3 @@ class NWProjectData:
if isinstance(entry, str): if isinstance(entry, str):
self._autoReplace[key] = simplified(entry) self._autoReplace[key] = simplified(entry)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return
+3 -16
View File
@@ -22,7 +22,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -72,7 +72,7 @@ class XMLReadState(Enum):
class ProjectXMLReader: class ProjectXMLReader:
"""Core: Project XML Reader """Core: Project XML Reader.
All data is read into a NWProjectData instance, which must be All data is read into a NWProjectData instance, which must be
provided. provided.
@@ -124,7 +124,6 @@ class ProjectXMLReader:
self._appVersion = "" self._appVersion = ""
self._hexVersion = 0x0 self._hexVersion = 0x0
self._timeStamp = "" self._timeStamp = ""
return
## ##
# Properties # Properties
@@ -254,8 +253,6 @@ class ProjectXMLReader:
elif xItem.tag == "editTime": # Moved to attribute in 1.5 elif xItem.tag == "editTime": # Moved to attribute in 1.5
data.setEditTime(xItem.text) data.setEditTime(xItem.text)
return
def _parseProjectSettings(self, xSection: ET.Element, data: NWProjectData) -> None: def _parseProjectSettings(self, xSection: ET.Element, data: NWProjectData) -> None:
"""Parse the settings section of the XML file.""" """Parse the settings section of the XML file."""
logger.debug("Parsing <settings> section") logger.debug("Parsing <settings> section")
@@ -294,8 +291,6 @@ class ProjectXMLReader:
elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5 elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
data.setInitCounts(wNotes=xItem.text) data.setInitCounts(wNotes=xItem.text)
return
def _parseProjectContent( def _parseProjectContent(
self, xSection: ET.Element, data: NWProjectData, content: list self, xSection: ET.Element, data: NWProjectData, content: list
) -> None: ) -> None:
@@ -356,8 +351,6 @@ class ProjectXMLReader:
"nameAttr": name, "nameAttr": name,
}) })
return
def _parseProjectContentLegacy( def _parseProjectContentLegacy(
self, xSection: ET.Element, data: NWProjectData, content: list self, xSection: ET.Element, data: NWProjectData, content: list
) -> None: ) -> None:
@@ -434,8 +427,6 @@ class ProjectXMLReader:
"nameAttr": name, "nameAttr": name,
}) })
return
def _parseStatusImport(self, xItem: ET.Element, sObject: NWStatus) -> None: def _parseStatusImport(self, xItem: ET.Element, sObject: NWStatus) -> None:
"""Parse a status or importance entry.""" """Parse a status or importance entry."""
for xEntry in xItem: for xEntry in xItem:
@@ -450,7 +441,6 @@ class ProjectXMLReader:
if color is None: if color is None:
color = f"{red}, {green}, {blue}" color = f"{red}, {green}, {blue}"
sObject.add(key, xEntry.text or "", color, shape, count) sObject.add(key, xEntry.text or "", color, shape, count)
return
def _parseDictKeyText(self, xItem: ET.Element) -> dict: def _parseDictKeyText(self, xItem: ET.Element) -> dict:
"""Parse a dictionary stored with key as an attribute and the """Parse a dictionary stored with key as an attribute and the
@@ -470,7 +460,7 @@ class ProjectXMLReader:
class ProjectXMLWriter: class ProjectXMLWriter:
"""Core: Project XML Writer """Core: Project XML Writer.
The project writer class will only write a file according to the The project writer class will only write a file according to the
very latest spec. very latest spec.
@@ -479,7 +469,6 @@ class ProjectXMLWriter:
def __init__(self, path: str | Path) -> None: def __init__(self, path: str | Path) -> None:
self._path = Path(path) self._path = Path(path)
self._error = None self._error = None
return
## ##
# Properties # Properties
@@ -580,7 +569,6 @@ class ProjectXMLWriter:
"""Pack a single value into an XML element.""" """Pack a single value into an XML element."""
xItem = ET.SubElement(xParent, name, attrib=attrib or {}) xItem = ET.SubElement(xParent, name, attrib=attrib or {})
xItem.text = str(value) or "" xItem.text = str(value) or ""
return
def _packDictKeyValue(self, xParent: ET.Element, name: str, data: dict) -> None: def _packDictKeyValue(self, xParent: ET.Element, name: str, data: dict) -> None:
"""Pack the entries of a dictionary into an XML element.""" """Pack the entries of a dictionary into an XML element."""
@@ -589,4 +577,3 @@ class ProjectXMLWriter:
if len(key) > 0: if len(key) > 0:
xEntry = ET.SubElement(xItem, "entry", attrib={"key": key}) xEntry = ET.SubElement(xItem, "entry", attrib={"key": key})
xEntry.text = str(value) or "" xEntry.text = str(value) or ""
return
+2 -4
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -43,7 +43,7 @@ logger = logging.getLogger(__name__)
class NWSessionLog: class NWSessionLog:
"""Core: Session JSON Lines Log File """Core: Session JSON Lines Log File.
The class that wraps the session log file, which is in JSON Lines The class that wraps the session log file, which is in JSON Lines
format. That is, one JSON object per line. format. That is, one JSON object per line.
@@ -52,7 +52,6 @@ class NWSessionLog:
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject) -> None:
self._project = project self._project = project
self._start = 0.0 self._start = 0.0
return
## ##
# Properties # Properties
@@ -70,7 +69,6 @@ class NWSessionLog:
def startSession(self) -> None: def startSession(self) -> None:
"""Start the writing session.""" """Start the writing session."""
self._start = time() self._start = time()
return
def appendSession(self, idleTime: float) -> bool: def appendSession(self, idleTime: float) -> bool:
"""Append session statistics to the sessions log file.""" """Append session statistics to the sessions log file."""
+12 -13
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class NWSpellEnchant: class NWSpellEnchant:
"""Core: Enchant Spell Checking Wrapper """Core: Enchant Spell Checking Wrapper.
This is a rapper class for Enchant to keep the API consistent This is a rapper class for Enchant to keep the API consistent
between spell check tools. between spell check tools.
@@ -57,11 +57,9 @@ class NWSpellEnchant:
self._language = None self._language = None
self._broker = None self._broker = None
logger.debug("Ready: NWSpellEnchant") logger.debug("Ready: NWSpellEnchant")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWSpellEnchant") logger.debug("Delete: NWSpellEnchant")
return
## ##
# Properties # Properties
@@ -106,21 +104,19 @@ class NWSpellEnchant:
for word in self._userDict: for word in self._userDict:
self._enchant.add_to_session(word) self._enchant.add_to_session(word)
return
## ##
# Methods # Methods
## ##
def checkWord(self, word: str) -> bool: def checkWord(self, word: str) -> bool:
"""Wrapper function for pyenchant.""" """Forward check to pyenchant."""
try: try:
return bool(self._enchant.check(word)) return bool(self._enchant.check(word))
except Exception: except Exception:
return True return True
def suggestWords(self, word: str) -> list[str]: def suggestWords(self, word: str) -> list[str]:
"""Wrapper function for pyenchant.""" """Ask pyenchant for suggestions."""
try: try:
return self._enchant.suggest(word) return self._enchant.suggest(word)
except Exception: except Exception:
@@ -172,24 +168,29 @@ class FakeEnchant:
self.tag = "" self.tag = ""
self.provider = FakeProvider() self.provider = FakeProvider()
return
def check(self, word: str) -> bool: def check(self, word: str) -> bool:
"""Return True for all words."""
return True return True
def suggest(self, word: str) -> list[str]: def suggest(self, word: str) -> list[str]:
"""Return an empty suggestion list."""
return [] return []
def add_to_session(self, word: str) -> None: def add_to_session(self, word: str) -> None:
"""Do nothing."""
return return
class UserDictionary: class UserDictionary:
"""Core: User Word Dictionary.
This class holds all the user's own words for spell checking
purposes. The dictionary is per-project.
"""
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject) -> None:
self._project = project self._project = project
self._words = set() self._words = set()
return
def __contains__(self, word: str) -> bool: def __contains__(self, word: str) -> bool:
return word in self._words return word in self._words
@@ -219,7 +220,6 @@ class UserDictionary:
except Exception: except Exception:
logger.error("Failed to load user dictionary") logger.error("Failed to load user dictionary")
logException() logException()
return
def save(self) -> None: def save(self) -> None:
"""Save the user's dictionary.""" """Save the user's dictionary."""
@@ -232,4 +232,3 @@ class UserDictionary:
except Exception: except Exception:
logger.error("Failed to save user dictionary") logger.error("Failed to save user dictionary")
logException() logException()
return
+3 -8
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
@@ -49,6 +49,7 @@ CUSTOM_COL = "custom"
@dataclasses.dataclass @dataclasses.dataclass
class StatusEntry: class StatusEntry:
"""DataClass: Status Label Values."""
name: str name: str
color: QColor color: QColor
@@ -73,6 +74,7 @@ T_StatusKind = Literal["s", "i"]
class NWStatus: class NWStatus:
"""Core: Status/Importance Label Class."""
STATUS = "s" STATUS = "s"
IMPORT = "i" IMPORT = "i"
@@ -84,7 +86,6 @@ class NWStatus:
self._default = None self._default = None
self._prefix = prefix[:1] self._prefix = prefix[:1]
self._height = SHARED.theme.baseIconHeight self._height = SHARED.theme.baseIconHeight
return
def __len__(self) -> int: def __len__(self) -> int:
return len(self._store) return len(self._store)
@@ -133,8 +134,6 @@ class NWStatus:
if self._default not in self._store: if self._default not in self._store:
self._default = next(iter(self._store)) if self._store else None self._default = next(iter(self._store)) if self._store else None
return
def check(self, value: str) -> str: def check(self, value: str) -> str:
"""Check the key against the stored status names.""" """Check the key against the stored status names."""
if self._isKey(value) and value in self._store: if self._isKey(value) and value in self._store:
@@ -147,13 +146,11 @@ class NWStatus:
"""Clear the counts of references to the status entries.""" """Clear the counts of references to the status entries."""
for entry in self._store.values(): for entry in self._store.values():
entry.count = 0 entry.count = 0
return
def increment(self, key: str | None) -> None: def increment(self, key: str | None) -> None:
"""Increment the counter for a given entry.""" """Increment the counter for a given entry."""
if key and key in self._store: if key and key in self._store:
self._store[key].count += 1 self._store[key].count += 1
return
def pack(self) -> Iterable[tuple[str, dict]]: def pack(self) -> Iterable[tuple[str, dict]]:
"""Pack the status entries into a dictionary.""" """Pack the status entries into a dictionary."""
@@ -195,7 +192,6 @@ class NWStatus:
if entry.theme != CUSTOM_COL: if entry.theme != CUSTOM_COL:
entry.color = SHARED.theme.parseColor(entry.theme) entry.color = SHARED.theme.parseColor(entry.theme)
entry.icon = NWStatus.createIcon(self._height, entry.color, entry.shape) entry.icon = NWStatus.createIcon(self._height, entry.color, entry.shape)
return
@staticmethod @staticmethod
def createIcon(height: int, color: QColor, shape: nwStatusShape) -> QIcon: def createIcon(height: int, color: QColor, shape: nwStatusShape) -> QIcon:
@@ -252,7 +248,6 @@ class _ShapeCache:
def __init__(self) -> None: def __init__(self) -> None:
self._cache: dict[nwStatusShape, QPainterPath] = {} self._cache: dict[nwStatusShape, QPainterPath] = {}
return
def getShape(self, shape: nwStatusShape) -> QPainterPath: def getShape(self, shape: nwStatusShape) -> QPainterPath:
"""Return a painter shape for an icon.""" """Return a painter shape for an icon."""
+5 -10
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -47,6 +47,7 @@ logger = logging.getLogger(__name__)
class NWStorageOpen(Enum): class NWStorageOpen(Enum):
"""The status of a storage location."""
UNKOWN = 0 UNKOWN = 0
NOT_FOUND = 1 NOT_FOUND = 1
@@ -56,6 +57,7 @@ class NWStorageOpen(Enum):
class NWStorageCreate(Enum): class NWStorageCreate(Enum):
"""The status of a new storage location."""
NOT_EMPTY = 0 NOT_EMPTY = 0
OS_ERROR = 1 OS_ERROR = 1
@@ -63,7 +65,7 @@ class NWStorageCreate(Enum):
class NWStorage: class NWStorage:
"""Core: Project Storage Class """Core: Project Storage Class.
The class that handles all paths related to the project storage. The class that handles all paths related to the project storage.
""" """
@@ -81,7 +83,6 @@ class NWStorage:
self._openMode = self.MODE_INACTIVE self._openMode = self.MODE_INACTIVE
self._ready = False self._ready = False
self._exception = None self._exception = None
return
def clear(self) -> None: def clear(self) -> None:
"""Reset internal variables.""" """Reset internal variables."""
@@ -90,7 +91,6 @@ class NWStorage:
self._lockFilePath = None self._lockFilePath = None
self._openMode = self.MODE_INACTIVE self._openMode = self.MODE_INACTIVE
self._ready = False self._ready = False
return
## ##
# Properties # Properties
@@ -252,13 +252,11 @@ class NWStorage:
"""Lock the session when the project is successfully opened.""" """Lock the session when the project is successfully opened."""
if self._ready: if self._ready:
self._writeLockFile() self._writeLockFile()
return
def closeSession(self) -> None: def closeSession(self) -> None:
"""Run tasks related to closing the session.""" """Run tasks related to closing the session."""
self._clearLockFile() self._clearLockFile()
self.clear() self.clear()
return
## ##
# Content Access Methods # Content Access Methods
@@ -394,7 +392,7 @@ class NWStorage:
class _LegacyStorage: class _LegacyStorage:
"""Core: Legacy Storage Converter Utils """Core: Legacy Storage Converter Utils.
A class with various functions to convert old file formats and A class with various functions to convert old file formats and
file/folder layouts to the current project format. file/folder layouts to the current project format.
@@ -402,7 +400,6 @@ class _LegacyStorage:
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject) -> None:
self._project = project self._project = project
return
def legacyDataFolder(self, path: Path, child: Path) -> None: def legacyDataFolder(self, path: Path, child: Path) -> None:
"""Handle the content of a legacy data folder from a version 1.0 """Handle the content of a legacy data folder from a version 1.0
@@ -484,8 +481,6 @@ class _LegacyStorage:
except Exception as exc: except Exception as exc:
logger.warning("Failed to delete: %s", item, exc_info=exc) logger.warning("Failed to delete: %s", item, exc_info=exc)
return
## ##
# Internal Functions # Internal Functions
## ##
+5 -14
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -50,7 +50,7 @@ MAX_DEPTH = 999 # Cap of tree traversing for loops (recursion limit)
class NWTree: class NWTree:
"""Core: Project Tree Data Class """Core: Project Tree Data Class.
Only one instance of this class should exist in the project class. Only one instance of this class should exist in the project class.
This class holds all the project items of the project as instances This class holds all the project items of the project as instances
@@ -71,18 +71,16 @@ class NWTree:
self._trash = None self._trash = None
self._ready = False self._ready = False
logger.debug("Ready: NWTree") logger.debug("Ready: NWTree")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWTree") logger.debug("Delete: NWTree")
return
def __len__(self) -> int: def __len__(self) -> int:
"""The number of items in the project.""" """Return the number of items in the project."""
return len(self._items) return len(self._items)
def __bool__(self) -> bool: def __bool__(self) -> bool:
"""True if there are any items in the project.""" """Return True if there are any items in the project."""
return bool(self._items) return bool(self._items)
def __getitem__(self, tHandle: str | None) -> NWItem | None: def __getitem__(self, tHandle: str | None) -> NWItem | None:
@@ -95,7 +93,7 @@ class NWTree:
return None return None
def __contains__(self, tHandle: str) -> bool: def __contains__(self, tHandle: str) -> bool:
"""Checks if a handle exists in the tree.""" """Check if a handle exists in the tree."""
return tHandle in self._items return tHandle in self._items
def __iter__(self) -> Iterator[NWItem]: def __iter__(self) -> Iterator[NWItem]:
@@ -142,7 +140,6 @@ class NWTree:
self._trash = None self._trash = None
oldModel.deleteLater() oldModel.deleteLater()
del oldModel del oldModel
return
def add(self, item: NWItem, pos: int = -1) -> bool: def add(self, item: NWItem, pos: int = -1) -> bool:
"""Add a project item into the project tree.""" """Add a project item into the project tree."""
@@ -260,8 +257,6 @@ class NWTree:
self._model.endInsertRows() self._model.endInsertRows()
self._model.layoutChanged.emit() self._model.layoutChanged.emit()
return
def pickParent(self, sNode: ProjectNode, hLevel: int, isNote: bool) -> tuple[str | None, int]: def pickParent(self, sNode: ProjectNode, hLevel: int, isNote: bool) -> tuple[str | None, int]:
"""Pick an appropriate parent handle for adding a new item.""" """Pick an appropriate parent handle for adding a new item."""
if sNode.item.isFolderType() or sNode.item.isRootType(): if sNode.item.isFolderType() or sNode.item.isRootType():
@@ -299,7 +294,6 @@ class NWTree:
indexE = self._model.indexFromNode(node, 3) indexE = self._model.indexFromNode(node, 3)
self._model.dataChanged.emit(indexS, indexE) self._model.dataChanged.emit(indexS, indexE)
self._itemChange(node.item, nwChange.UPDATE) self._itemChange(node.item, nwChange.UPDATE)
return
def refreshAllItems(self) -> None: def refreshAllItems(self) -> None:
"""Refresh all items in the tree.""" """Refresh all items in the tree."""
@@ -309,13 +303,11 @@ class NWTree:
self._model.root.refresh() self._model.root.refresh()
self._model.root.updateCount(propagate=False) self._model.root.updateCount(propagate=False)
self._model.layoutChanged.emit() self._model.layoutChanged.emit()
return
def novelStructureChanged(self, tHandle: str) -> None: def novelStructureChanged(self, tHandle: str) -> None:
"""Emit a novel structure change signal.""" """Emit a novel structure change signal."""
if self._ready: if self._ready:
SHARED.novelStructureChanged.emit(tHandle) SHARED.novelStructureChanged.emit(tHandle)
return
def checkConsistency(self, prefix: str) -> tuple[int, int]: def checkConsistency(self, prefix: str) -> tuple[int, int]:
"""Check the project tree consistency. Also check the content """Check the project tree consistency. Also check the content
@@ -496,7 +488,6 @@ class NWTree:
SHARED.emitProjectItemChanged(self._project, tHandle, change) SHARED.emitProjectItemChanged(self._project, tHandle, change)
if item.isRootType(): if item.isRootType():
SHARED.emitRootFolderChanged(self._project, tHandle, change) SHARED.emitRootFolderChanged(self._project, tHandle, change)
return
def _getTrashNode(self) -> ProjectNode | None: def _getTrashNode(self) -> ProjectNode | None:
"""Get the trash node. If it doesn't exist, create it.""" """Get the trash node. If it doesn't exist, create it."""
+2 -7
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -45,6 +45,7 @@ logger = logging.getLogger(__name__)
class GuiAbout(NDialog): class GuiAbout(NDialog):
"""GUI: About novelWriter Dialog."""
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -109,11 +110,8 @@ class GuiAbout(NDialog):
logger.debug("Ready: GuiAbout") logger.debug("Ready: GuiAbout")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiAbout") logger.debug("Delete: GuiAbout")
return
## ##
# Events # Events
@@ -123,7 +121,6 @@ class GuiAbout(NDialog):
"""Capture the close event and perform cleanup.""" """Capture the close event and perform cleanup."""
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Internal Functions # Internal Functions
@@ -135,7 +132,6 @@ class GuiAbout(NDialog):
self.txtCredits.setHtml(html) self.txtCredits.setHtml(html)
else: else:
self.txtCredits.setHtml("Error loading credits text ...") self.txtCredits.setHtml("Error loading credits text ...")
return
def _setStyleSheet(self) -> None: def _setStyleSheet(self) -> None:
"""Set stylesheet text document.""" """Set stylesheet text document."""
@@ -143,4 +139,3 @@ class GuiAbout(NDialog):
self.txtCredits.setStyleSheet( self.txtCredits.setStyleSheet(
f"QTextBrowser {{border: none; background: {baseCol};}} " f"QTextBrowser {{border: none; background: {baseCol};}} "
) )
return
+2 -6
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
class GuiDocMerge(NDialog): class GuiDocMerge(NDialog):
"""GUI: Document Merge Tool."""
D_HANDLE = QtUserRole D_HANDLE = QtUserRole
@@ -110,11 +111,8 @@ class GuiDocMerge(NDialog):
logger.debug("Ready: GuiDocMerge") logger.debug("Ready: GuiDocMerge")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiDocMerge") logger.debug("Delete: GuiDocMerge")
return
def data(self) -> dict: def data(self) -> dict:
"""Return the user's choices.""" """Return the user's choices."""
@@ -150,7 +148,6 @@ class GuiDocMerge(NDialog):
if sHandle := self._data.get("sHandle"): if sHandle := self._data.get("sHandle"):
itemList = self._data.get("origItems", []) itemList = self._data.get("origItems", [])
self._loadContent(sHandle, itemList) self._loadContent(sHandle, itemList)
return
## ##
# Internal Functions # Internal Functions
@@ -170,4 +167,3 @@ class GuiDocMerge(NDialog):
item.setData(self.D_HANDLE, tHandle) item.setData(self.D_HANDLE, tHandle)
item.setCheckState(Qt.CheckState.Checked) item.setCheckState(Qt.CheckState.Checked)
self.listBox.addItem(item) self.listBox.addItem(item)
return
+2 -5
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
class GuiDocSplit(NDialog): class GuiDocSplit(NDialog):
"""GUI: Document Split Tool."""
LINE_ROLE = QtUserRole LINE_ROLE = QtUserRole
LEVEL_ROLE = QtUserRole + 1 LEVEL_ROLE = QtUserRole + 1
@@ -139,11 +140,8 @@ class GuiDocSplit(NDialog):
logger.debug("Ready: GuiDocSplit") logger.debug("Ready: GuiDocSplit")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiDocSplit") logger.debug("Delete: GuiDocSplit")
return
def data(self) -> tuple[dict, list[str]]: def data(self) -> tuple[dict, list[str]]:
"""Return the user's choices. Also save the users options for """Return the user's choices. Also save the users options for
@@ -197,7 +195,6 @@ class GuiDocSplit(NDialog):
"""Reload the content of the list box.""" """Reload the content of the list box."""
if sHandle := self._data.get("sHandle"): if sHandle := self._data.get("sHandle"):
self._loadContent(sHandle) self._loadContent(sHandle)
return
## ##
# Internal Functions # Internal Functions
+2 -4
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -34,6 +34,7 @@ logger = logging.getLogger(__name__)
class GuiEditLabel(NDialog): class GuiEditLabel(NDialog):
"""GUI: Edit Item Label Dialog."""
def __init__(self, parent: QWidget, text: str = "") -> None: def __init__(self, parent: QWidget, text: str = "") -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -72,11 +73,8 @@ class GuiEditLabel(NDialog):
logger.debug("Ready: GuiEditLabel") logger.debug("Ready: GuiEditLabel")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiEditLabel") logger.debug("Delete: GuiEditLabel")
return
@property @property
def itemLabel(self) -> str: def itemLabel(self) -> str:
+2 -23
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
class GuiPreferences(NDialog): class GuiPreferences(NDialog):
"""GUI: Preferences Dialog."""
newPreferencesReady = pyqtSignal(bool, bool, bool, bool) newPreferencesReady = pyqtSignal(bool, bool, bool, bool)
@@ -125,11 +126,8 @@ class GuiPreferences(NDialog):
logger.debug("Ready: GuiPreferences") logger.debug("Ready: GuiPreferences")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiPreferences") logger.debug("Delete: GuiPreferences")
return
def buildForm(self) -> None: def buildForm(self) -> None:
"""Build the settings form.""" """Build the settings form."""
@@ -856,8 +854,6 @@ class GuiPreferences(NDialog):
self.mainForm.finalise() self.mainForm.finalise()
self.sidebar.setSelected(1) self.sidebar.setSelected(1)
return
## ##
# Events # Events
## ##
@@ -868,7 +864,6 @@ class GuiPreferences(NDialog):
self._saveWindowSize() self._saveWindowSize()
event.accept() event.accept()
self.softDelete() self.softDelete()
return
def keyPressEvent(self, event: QKeyEvent) -> None: def keyPressEvent(self, event: QKeyEvent) -> None:
"""Overload keyPressEvent and only accept escape. The main """Overload keyPressEvent and only accept escape. The main
@@ -878,7 +873,6 @@ class GuiPreferences(NDialog):
if event.matches(QKeySequence.StandardKey.Cancel): if event.matches(QKeySequence.StandardKey.Cancel):
self.close() self.close()
event.ignore() event.ignore()
return
## ##
# Private Slots # Private Slots
@@ -888,13 +882,11 @@ class GuiPreferences(NDialog):
def _sidebarClicked(self, section: int) -> None: def _sidebarClicked(self, section: int) -> None:
"""Process a user request to switch page.""" """Process a user request to switch page."""
self.mainForm.scrollToSection(section) self.mainForm.scrollToSection(section)
return
@pyqtSlot() @pyqtSlot()
def _gotoSearch(self) -> None: def _gotoSearch(self) -> None:
"""Go to the setting indicated by the search text.""" """Go to the setting indicated by the search text."""
self.mainForm.scrollToLabel(self.searchText.text().strip()) self.mainForm.scrollToLabel(self.searchText.text().strip())
return
@pyqtSlot() @pyqtSlot()
def _selectGuiFont(self) -> None: def _selectGuiFont(self) -> None:
@@ -904,7 +896,6 @@ class GuiPreferences(NDialog):
self.guiFont.setText(describeFont(font)) self.guiFont.setText(describeFont(font))
self.guiFont.setCursorPosition(0) self.guiFont.setCursorPosition(0)
self._guiFont = font self._guiFont = font
return
@pyqtSlot() @pyqtSlot()
def _selectTextFont(self) -> None: def _selectTextFont(self) -> None:
@@ -914,7 +905,6 @@ class GuiPreferences(NDialog):
self.textFont.setText(describeFont(font)) self.textFont.setText(describeFont(font))
self.textFont.setCursorPosition(0) self.textFont.setCursorPosition(0)
self._textFont = font self._textFont = font
return
@pyqtSlot() @pyqtSlot()
def _backupFolder(self) -> None: def _backupFolder(self) -> None:
@@ -925,13 +915,11 @@ class GuiPreferences(NDialog):
): ):
self.backupPath = path self.backupPath = path
self.mainForm.setHelpText("backupPath", self.tr("Path: {0}").format(path)) self.mainForm.setHelpText("backupPath", self.tr("Path: {0}").format(path))
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _toggledBackupOnClose(self, state: bool) -> None: def _toggledBackupOnClose(self, state: bool) -> None:
"""Toggle switch that depends on the backup on close switch.""" """Toggle switch that depends on the backup on close switch."""
self.askBeforeBackup.setEnabled(state) self.askBeforeBackup.setEnabled(state)
return
@pyqtSlot(str) @pyqtSlot(str)
def _insertDialogLineSymbol(self, symbol: str) -> None: def _insertDialogLineSymbol(self, symbol: str) -> None:
@@ -939,7 +927,6 @@ class GuiPreferences(NDialog):
current = self.dialogLine.text() current = self.dialogLine.text()
values = processDialogSymbols(f"{current} {symbol}") values = processDialogSymbols(f"{current} {symbol}")
self.dialogLine.setText(" ".join(values)) self.dialogLine.setText(" ".join(values))
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _toggleAutoReplaceMain(self, state: bool) -> None: def _toggleAutoReplaceMain(self, state: bool) -> None:
@@ -949,7 +936,6 @@ class GuiPreferences(NDialog):
self.doReplaceDash.setEnabled(state) self.doReplaceDash.setEnabled(state)
self.doReplaceDots.setEnabled(state) self.doReplaceDots.setEnabled(state)
self.fmtPadThin.setEnabled(state) self.fmtPadThin.setEnabled(state)
return
@pyqtSlot() @pyqtSlot()
def _changeSingleQuoteOpen(self) -> None: def _changeSingleQuoteOpen(self) -> None:
@@ -957,7 +943,6 @@ class GuiPreferences(NDialog):
quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtSQuoteOpen.text()) quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtSQuoteOpen.text())
if status: if status:
self.fmtSQuoteOpen.setText(quote) self.fmtSQuoteOpen.setText(quote)
return
@pyqtSlot() @pyqtSlot()
def _changeSingleQuoteClose(self) -> None: def _changeSingleQuoteClose(self) -> None:
@@ -965,7 +950,6 @@ class GuiPreferences(NDialog):
quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtSQuoteClose.text()) quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtSQuoteClose.text())
if status: if status:
self.fmtSQuoteClose.setText(quote) self.fmtSQuoteClose.setText(quote)
return
@pyqtSlot() @pyqtSlot()
def _changeDoubleQuoteOpen(self) -> None: def _changeDoubleQuoteOpen(self) -> None:
@@ -973,7 +957,6 @@ class GuiPreferences(NDialog):
quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtDQuoteOpen.text()) quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtDQuoteOpen.text())
if status: if status:
self.fmtDQuoteOpen.setText(quote) self.fmtDQuoteOpen.setText(quote)
return
@pyqtSlot() @pyqtSlot()
def _changeDoubleQuoteClose(self) -> None: def _changeDoubleQuoteClose(self) -> None:
@@ -981,7 +964,6 @@ class GuiPreferences(NDialog):
quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtDQuoteClose.text()) quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtDQuoteClose.text())
if status: if status:
self.fmtDQuoteClose.setText(quote) self.fmtDQuoteClose.setText(quote)
return
## ##
# Internal Functions # Internal Functions
@@ -990,7 +972,6 @@ class GuiPreferences(NDialog):
def _saveWindowSize(self) -> None: def _saveWindowSize(self) -> None:
"""Save the dialog window size.""" """Save the dialog window size."""
CONFIG.setPreferencesWinSize(self.width(), self.height()) CONFIG.setPreferencesWinSize(self.width(), self.height())
return
def _doSave(self) -> None: def _doSave(self) -> None:
"""Save the values set in the form.""" """Save the values set in the form."""
@@ -1134,5 +1115,3 @@ class GuiPreferences(NDialog):
self.newPreferencesReady.emit(needsRestart, refreshTree, updateTheme, updateSyntax) self.newPreferencesReady.emit(needsRestart, refreshTree, updateTheme, updateSyntax)
self.close() self.close()
return
+2 -33
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import csv import csv
@@ -55,6 +55,7 @@ logger = logging.getLogger(__name__)
class GuiProjectSettings(NDialog): class GuiProjectSettings(NDialog):
"""GUI: Project Settings DIalog."""
PAGE_SETTINGS = 0 PAGE_SETTINGS = 0
PAGE_STATUS = 1 PAGE_STATUS = 1
@@ -137,11 +138,8 @@ class GuiProjectSettings(NDialog):
logger.debug("Ready: GuiProjectSettings") logger.debug("Ready: GuiProjectSettings")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiProjectSettings") logger.debug("Delete: GuiProjectSettings")
return
## ##
# Events # Events
@@ -152,7 +150,6 @@ class GuiProjectSettings(NDialog):
self._saveSettings() self._saveSettings()
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Private Slots # Private Slots
@@ -169,7 +166,6 @@ class GuiProjectSettings(NDialog):
self.mainStack.setCurrentWidget(self.importPage) self.mainStack.setCurrentWidget(self.importPage)
elif pageId == self.PAGE_REPLACE: elif pageId == self.PAGE_REPLACE:
self.mainStack.setCurrentWidget(self.replacePage) self.mainStack.setCurrentWidget(self.replacePage)
return
@pyqtSlot() @pyqtSlot()
def _doSave(self) -> None: def _doSave(self) -> None:
@@ -203,8 +199,6 @@ class GuiProjectSettings(NDialog):
QApplication.processEvents() QApplication.processEvents()
self.close() self.close()
return
## ##
# Internal Functions # Internal Functions
## ##
@@ -223,8 +217,6 @@ class GuiProjectSettings(NDialog):
options.setValue("GuiProjectSettings", "importColW", importColW) options.setValue("GuiProjectSettings", "importColW", importColW)
options.setValue("GuiProjectSettings", "replaceColW", replaceColW) options.setValue("GuiProjectSettings", "replaceColW", replaceColW)
return
class _SettingsPage(NScrollableForm): class _SettingsPage(NScrollableForm):
@@ -295,8 +287,6 @@ class _SettingsPage(NScrollableForm):
self.finalise() self.finalise()
return
class _StatusPage(NFixedPage): class _StatusPage(NFixedPage):
@@ -478,8 +468,6 @@ class _StatusPage(NFixedPage):
self.setCentralLayout(self.outerBox) self.setCentralLayout(self.outerBox)
self._setButtonIcons() self._setButtonIcons()
return
@property @property
def changed(self) -> bool: def changed(self) -> bool:
"""The user changed these settings.""" """The user changed these settings."""
@@ -518,7 +506,6 @@ class _StatusPage(NFixedPage):
entry.name = name entry.name = name
item.setText(self.C_LABEL, name) item.setText(self.C_LABEL, name)
self._changed = True self._changed = True
return
@pyqtSlot(int) @pyqtSlot(int)
def _onThemeSelect(self, index: int) -> None: def _onThemeSelect(self, index: int) -> None:
@@ -526,7 +513,6 @@ class _StatusPage(NFixedPage):
self._theme = str(self.iconColor.currentData()) self._theme = str(self.iconColor.currentData())
self._setButtonIcons() self._setButtonIcons()
self._updateIcon() self._updateIcon()
return
@pyqtSlot() @pyqtSlot()
def _onColorSelect(self) -> None: def _onColorSelect(self) -> None:
@@ -536,7 +522,6 @@ class _StatusPage(NFixedPage):
self._theme = CUSTOM_COL self._theme = CUSTOM_COL
self._setButtonIcons() self._setButtonIcons()
self._updateIcon() self._updateIcon()
return
@pyqtSlot() @pyqtSlot()
def _onItemCreate(self) -> None: def _onItemCreate(self) -> None:
@@ -547,7 +532,6 @@ class _StatusPage(NFixedPage):
theme = str(self.iconColor.currentData()) theme = str(self.iconColor.currentData())
self._addItem(None, StatusEntry(self.tr("New Item"), color, theme, shape, icon, 0)) self._addItem(None, StatusEntry(self.tr("New Item"), color, theme, shape, icon, 0))
self._changed = True self._changed = True
return
@pyqtSlot() @pyqtSlot()
def _onItemDelete(self) -> None: def _onItemDelete(self) -> None:
@@ -560,7 +544,6 @@ class _StatusPage(NFixedPage):
else: else:
self.listBox.takeTopLevelItem(iRow) self.listBox.takeTopLevelItem(iRow)
self._changed = True self._changed = True
return
@pyqtSlot() @pyqtSlot()
def _onSelectionChanged(self) -> None: def _onSelectionChanged(self) -> None:
@@ -593,7 +576,6 @@ class _StatusPage(NFixedPage):
self.iconColor.setEnabled(False) self.iconColor.setEnabled(False)
self.colorButton.setEnabled(False) self.colorButton.setEnabled(False)
self.shapeButton.setEnabled(False) self.shapeButton.setEnabled(False)
return
@pyqtSlot() @pyqtSlot()
def _importLabels(self) -> None: def _importLabels(self) -> None:
@@ -630,7 +612,6 @@ class _StatusPage(NFixedPage):
writer.writerow([entry.shape.name, entry.color.name(), entry.name]) writer.writerow([entry.shape.name, entry.color.name(), entry.name])
except Exception as exc: except Exception as exc:
SHARED.error("Could not write file.", exc=exc) SHARED.error("Could not write file.", exc=exc)
return
## ##
# Internal Functions # Internal Functions
@@ -641,7 +622,6 @@ class _StatusPage(NFixedPage):
self._shape = shape self._shape = shape
self._setButtonIcons() self._setButtonIcons()
self._updateIcon() self._updateIcon()
return
def _updateIcon(self) -> None: def _updateIcon(self) -> None:
"""Apply changes made to a status icon.""" """Apply changes made to a status icon."""
@@ -654,7 +634,6 @@ class _StatusPage(NFixedPage):
entry.icon = icon entry.icon = icon
item.setIcon(self.C_LABEL, icon) item.setIcon(self.C_LABEL, icon)
self._changed = True self._changed = True
return
def _addItem(self, key: str | None, entry: StatusEntry) -> None: def _addItem(self, key: str | None, entry: StatusEntry) -> None:
"""Add a status item to the list.""" """Add a status item to the list."""
@@ -665,7 +644,6 @@ class _StatusPage(NFixedPage):
item.setData(self.C_DATA, self.D_KEY, key) item.setData(self.C_DATA, self.D_KEY, key)
item.setData(self.C_DATA, self.D_ENTRY, entry) item.setData(self.C_DATA, self.D_ENTRY, entry)
self.listBox.addTopLevelItem(item) self.listBox.addTopLevelItem(item)
return
def _moveItem(self, step: int) -> None: def _moveItem(self, step: int) -> None:
"""Move and item up or down step.""" """Move and item up or down step."""
@@ -678,7 +656,6 @@ class _StatusPage(NFixedPage):
self.listBox.clearSelection() self.listBox.clearSelection()
cItem.setSelected(True) cItem.setSelected(True)
self._changed = True self._changed = True
return
def _getSelectedItem(self) -> QTreeWidgetItem | None: def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Get the currently selected item.""" """Get the currently selected item."""
@@ -701,7 +678,6 @@ class _StatusPage(NFixedPage):
self.iconColor.setCurrentData(self._theme, CUSTOM_COL) self.iconColor.setCurrentData(self._theme, CUSTOM_COL)
self.colorButton.setIcon(icon) self.colorButton.setIcon(icon)
self.shapeButton.setIcon(self._icons[self._shape]) self.shapeButton.setIcon(self._icons[self._shape])
return
def _pickColor(self) -> QColor: def _pickColor(self) -> QColor:
"""Get the correct colour value based on selections.""" """Get the correct colour value based on selections."""
@@ -789,8 +765,6 @@ class _ReplacePage(NFixedPage):
self.setCentralLayout(self.outerBox) self.setCentralLayout(self.outerBox)
return
@property @property
def changed(self) -> bool: def changed(self) -> bool:
"""The user changed these settings.""" """The user changed these settings."""
@@ -823,7 +797,6 @@ class _ReplacePage(NFixedPage):
if (item := self._getSelectedItem()) and (key := self._stripKey(text)): if (item := self._getSelectedItem()) and (key := self._stripKey(text)):
item.setText(self.C_KEY, f"<{key}>") item.setText(self.C_KEY, f"<{key}>")
self._changed = True self._changed = True
return
@pyqtSlot(str) @pyqtSlot(str)
def _onValueEdit(self, text: str) -> None: def _onValueEdit(self, text: str) -> None:
@@ -831,7 +804,6 @@ class _ReplacePage(NFixedPage):
if item := self._getSelectedItem(): if item := self._getSelectedItem():
item.setText(self.C_REPL, text) item.setText(self.C_REPL, text)
self._changed = True self._changed = True
return
@pyqtSlot() @pyqtSlot()
def _onSelectionChanged(self) -> None: def _onSelectionChanged(self) -> None:
@@ -850,14 +822,12 @@ class _ReplacePage(NFixedPage):
self.editValue.setText("") self.editValue.setText("")
self.editKey.setEnabled(False) self.editKey.setEnabled(False)
self.editValue.setEnabled(False) self.editValue.setEnabled(False)
return
@pyqtSlot() @pyqtSlot()
def _onEntryCreated(self) -> None: def _onEntryCreated(self) -> None:
"""Add a new list entry.""" """Add a new list entry."""
key = f"<keyword{self.listBox.topLevelItemCount() + 1:d}>" key = f"<keyword{self.listBox.topLevelItemCount() + 1:d}>"
self.listBox.addTopLevelItem(QTreeWidgetItem([key, ""])) self.listBox.addTopLevelItem(QTreeWidgetItem([key, ""]))
return
@pyqtSlot() @pyqtSlot()
def _onEntryDeleted(self) -> None: def _onEntryDeleted(self) -> None:
@@ -865,7 +835,6 @@ class _ReplacePage(NFixedPage):
if item := self._getSelectedItem(): if item := self._getSelectedItem():
self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(item)) self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(item))
self._changed = True self._changed = True
return
## ##
# Internal Functions # Internal Functions
+2 -5
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -43,6 +43,7 @@ logger = logging.getLogger(__name__)
class GuiQuoteSelect(NDialog): class GuiQuoteSelect(NDialog):
"""GUI: Quote Selector Dialog."""
_selected = "" _selected = ""
@@ -108,11 +109,8 @@ class GuiQuoteSelect(NDialog):
logger.debug("Ready: GuiQuoteSelect") logger.debug("Ready: GuiQuoteSelect")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiQuoteSelect") logger.debug("Delete: GuiQuoteSelect")
return
@property @property
def selectedQuote(self) -> str: def selectedQuote(self) -> str:
@@ -140,4 +138,3 @@ class GuiQuoteSelect(NDialog):
quote = items[0].data(self.D_KEY) quote = items[0].data(self.D_KEY)
self.previewLabel.setText(quote) self.previewLabel.setText(quote)
self._selected = quote self._selected = quote
return
+2 -12
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -48,6 +48,7 @@ logger = logging.getLogger(__name__)
class GuiWordList(NDialog): class GuiWordList(NDialog):
"""GUI: User Dictionary Edit Tool."""
newWordListReady = pyqtSignal() newWordListReady = pyqtSignal()
@@ -128,11 +129,8 @@ class GuiWordList(NDialog):
logger.debug("Ready: GuiWordList") logger.debug("Ready: GuiWordList")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiWordList") logger.debug("Delete: GuiWordList")
return
## ##
# Events # Events
@@ -143,7 +141,6 @@ class GuiWordList(NDialog):
self._saveGuiSettings() self._saveGuiSettings()
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Private Slots # Private Slots
@@ -159,14 +156,12 @@ class GuiWordList(NDialog):
if items := self.listBox.findItems(word, Qt.MatchFlag.MatchExactly): if items := self.listBox.findItems(word, Qt.MatchFlag.MatchExactly):
self.listBox.setCurrentItem(items[0]) self.listBox.setCurrentItem(items[0])
self.listBox.scrollToItem(items[0], QAbstractItemView.ScrollHint.PositionAtCenter) self.listBox.scrollToItem(items[0], QAbstractItemView.ScrollHint.PositionAtCenter)
return
@pyqtSlot() @pyqtSlot()
def _doDelete(self) -> None: def _doDelete(self) -> None:
"""Delete the selected items.""" """Delete the selected items."""
for item in self.listBox.selectedItems(): for item in self.listBox.selectedItems():
self.listBox.takeItem(self.listBox.row(item)) self.listBox.takeItem(self.listBox.row(item))
return
@pyqtSlot() @pyqtSlot()
def _doSave(self) -> None: def _doSave(self) -> None:
@@ -178,7 +173,6 @@ class GuiWordList(NDialog):
self.newWordListReady.emit() self.newWordListReady.emit()
QApplication.processEvents() QApplication.processEvents()
self.close() self.close()
return
@pyqtSlot() @pyqtSlot()
def _importWords(self) -> None: def _importWords(self) -> None:
@@ -213,7 +207,6 @@ class GuiWordList(NDialog):
fo.write("\n".join(self._listWords())) fo.write("\n".join(self._listWords()))
except Exception as exc: except Exception as exc:
SHARED.error("Could not write file.", exc=exc) SHARED.error("Could not write file.", exc=exc)
return
## ##
# Internal Functions # Internal Functions
@@ -226,7 +219,6 @@ class GuiWordList(NDialog):
self.listBox.clear() self.listBox.clear()
for word in userDict: for word in userDict:
self.listBox.addItem(word) self.listBox.addItem(word)
return
def _saveGuiSettings(self) -> None: def _saveGuiSettings(self) -> None:
"""Save GUI settings.""" """Save GUI settings."""
@@ -234,14 +226,12 @@ class GuiWordList(NDialog):
pOptions = SHARED.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiWordList", "winWidth", self.width()) pOptions.setValue("GuiWordList", "winWidth", self.width())
pOptions.setValue("GuiWordList", "winHeight", self.height()) pOptions.setValue("GuiWordList", "winHeight", self.height())
return
def _addWord(self, word: str) -> None: def _addWord(self, word: str) -> None:
"""Add a single word to the list.""" """Add a single word to the list."""
if word and not self.listBox.findItems(word, Qt.MatchFlag.MatchExactly): if word and not self.listBox.findItems(word, Qt.MatchFlag.MatchExactly):
self.listBox.addItem(word) self.listBox.addItem(word)
self._changed = True self._changed = True
return
def _listWords(self) -> list[str]: def _listWords(self) -> list[str]:
"""List all words in the list box.""" """List all words in the list box."""
+16 -1
View File
@@ -20,12 +20,13 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from enum import Enum from enum import Enum
class nwItemType(Enum): class nwItemType(Enum):
"""Enum: Project Item Types."""
NO_TYPE = 0 NO_TYPE = 0
ROOT = 1 ROOT = 1
@@ -34,6 +35,7 @@ class nwItemType(Enum):
class nwItemClass(Enum): class nwItemClass(Enum):
"""Enum: Project Item Classes."""
NO_CLASS = 0 NO_CLASS = 0
NOVEL = 1 NOVEL = 1
@@ -50,6 +52,7 @@ class nwItemClass(Enum):
class nwItemLayout(Enum): class nwItemLayout(Enum):
"""A project item's layout."""
NO_LAYOUT = 0 NO_LAYOUT = 0
DOCUMENT = 1 DOCUMENT = 1
@@ -57,6 +60,7 @@ class nwItemLayout(Enum):
class nwComment(Enum): class nwComment(Enum):
"""Types of text comments."""
PLAIN = 0 PLAIN = 0
IGNORE = 1 IGNORE = 1
@@ -69,6 +73,7 @@ class nwComment(Enum):
class nwChange(Enum): class nwChange(Enum):
"""Change request modes."""
CREATE = 0 CREATE = 0
UPDATE = 1 UPDATE = 1
@@ -76,12 +81,14 @@ class nwChange(Enum):
class nwDocMode(Enum): class nwDocMode(Enum):
"""Document open modes."""
VIEW = 0 VIEW = 0
EDIT = 1 EDIT = 1
class nwDocAction(Enum): class nwDocAction(Enum):
"""Document actions."""
NO_ACTION = 0 NO_ACTION = 0
UNDO = 1 UNDO = 1
@@ -125,6 +132,7 @@ class nwDocAction(Enum):
class nwDocInsert(Enum): class nwDocInsert(Enum):
"""Document insert actions."""
NO_INSERT = 0 NO_INSERT = 0
QUOTE_LS = 1 QUOTE_LS = 1
@@ -142,6 +150,7 @@ class nwDocInsert(Enum):
class nwView(Enum): class nwView(Enum):
"""Main GUI view modes."""
EDITOR = 0 EDITOR = 0
PROJECT = 1 PROJECT = 1
@@ -151,6 +160,7 @@ class nwView(Enum):
class nwFocus(Enum): class nwFocus(Enum):
"""Main GUI panel focus."""
TREE = 1 TREE = 1
DOCUMENT = 2 DOCUMENT = 2
@@ -158,6 +168,7 @@ class nwFocus(Enum):
class nwTheme(Enum): class nwTheme(Enum):
"""GUI theme colour modes."""
AUTO = 0 AUTO = 0
LIGHT = 1 LIGHT = 1
@@ -165,6 +176,7 @@ class nwTheme(Enum):
class nwOutline(Enum): class nwOutline(Enum):
"""Enum: Project Outline Columns."""
TITLE = 0 TITLE = 0
LEVEL = 1 LEVEL = 1
@@ -189,6 +201,7 @@ class nwOutline(Enum):
class nwNovelExtra(Enum): class nwNovelExtra(Enum):
"""Enum: Novel View Extra Columns."""
HIDDEN = 0 HIDDEN = 0
POV = 1 POV = 1
@@ -197,6 +210,7 @@ class nwNovelExtra(Enum):
class nwBuildFmt(Enum): class nwBuildFmt(Enum):
"""Enum: Manuscript Document Formats."""
ODT = 0 ODT = 0
FODT = 1 FODT = 1
@@ -211,6 +225,7 @@ class nwBuildFmt(Enum):
class nwStatusShape(Enum): class nwStatusShape(Enum):
"""Enum: Status/Importance Icon Shapes."""
SQUARE = 0 SQUARE = 0
TRIANGLE = 1 TRIANGLE = 1
+5 -10
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -47,7 +47,6 @@ def logException() -> None:
exType, exValue, _ = sys.exc_info() exType, exValue, _ = sys.exc_info()
if exType is not None: if exType is not None:
logger.error(f"{exType.__name__}: {exValue!s}", stacklevel=2) logger.error(f"{exType.__name__}: {exValue!s}", stacklevel=2)
return
def formatException(exc: BaseException) -> str: def formatException(exc: BaseException) -> str:
@@ -58,6 +57,7 @@ def formatException(exc: BaseException) -> str:
class NWErrorMessage(QDialog): class NWErrorMessage(QDialog):
"""GUI: Error Dialog."""
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -110,8 +110,6 @@ class NWErrorMessage(QDialog):
self.setSizeGripEnabled(True) self.setSizeGripEnabled(True)
self.resize(800, 400) self.resize(800, 400)
return
def setMessage(self, exType: type, exValue: BaseException, exTrace: TracebackType) -> None: def setMessage(self, exType: type, exValue: BaseException, exTrace: TracebackType) -> None:
"""Generate a message and append session data, error info and """Generate a message and append session data, error info and
error traceback. error traceback.
@@ -143,7 +141,7 @@ class NWErrorMessage(QDialog):
enchantVersion = "Unknown" enchantVersion = "Unknown"
try: try:
txtTrace = "\n".join(format_tb(exTrace)) trace = "\n".join(format_tb(exTrace))
self.msgBody.setPlainText( self.msgBody.setPlainText(
"Environment:\n" "Environment:\n"
f"novelWriter Version: {__version__}\n" f"novelWriter Version: {__version__}\n"
@@ -152,13 +150,11 @@ class NWErrorMessage(QDialog):
f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n" f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n"
f"enchant: {enchantVersion}\n\n" f"enchant: {enchantVersion}\n\n"
f"{exType.__name__}:\n{exValue!s}\n\n" f"{exType.__name__}:\n{exValue!s}\n\n"
f"Traceback:\n{txtTrace}\n" f"Traceback:\n{trace}\n"
) )
except Exception: except Exception:
self.msgBody.setPlainText("Failed to generate error report ...") self.msgBody.setPlainText("Failed to generate error report ...")
return
## ##
# Slots # Slots
## ##
@@ -167,11 +163,10 @@ class NWErrorMessage(QDialog):
def _doClose(self) -> None: def _doClose(self) -> None:
"""Close the dialog.""" """Close the dialog."""
self.close() self.close()
return
def exceptionHandler(exType: type, exValue: BaseException, exTrace: TracebackType) -> None: def exceptionHandler(exType: type, exValue: BaseException, exTrace: TracebackType) -> None:
"""Function to catch unhandled global exceptions.""" """Catch unhandled global exceptions."""
from traceback import print_tb from traceback import print_tb
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
+6 -28
View File
@@ -24,7 +24,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from PyQt6.QtGui import QColor, QFont, QPalette, QPixmap from PyQt6.QtGui import QColor, QFont, QPalette, QPixmap
@@ -39,7 +39,7 @@ DEFAULT_SCALE = 0.9
class NFixedPage(QFrame): class NFixedPage(QFrame):
"""Extension: Fixed Page Widget """Extension: Fixed Page Widget.
A custom widget that holds a layout. This is just a wrapper around a A custom widget that holds a layout. This is just a wrapper around a
QFrame that sets the same frame style as the other Page widgets. QFrame that sets the same frame style as the other Page widgets.
@@ -49,23 +49,20 @@ class NFixedPage(QFrame):
super().__init__(parent=parent) super().__init__(parent=parent)
self.setFrameShadow(QFrame.Shadow.Sunken) self.setFrameShadow(QFrame.Shadow.Sunken)
self.setFrameShape(QFrame.Shape.StyledPanel) self.setFrameShape(QFrame.Shape.StyledPanel)
return
def setCentralLayout(self, layout: QLayout) -> None: def setCentralLayout(self, layout: QLayout) -> None:
"""Set a layout as the central object.""" """Set a layout as the central object."""
self.setLayout(layout) self.setLayout(layout)
return
def setCentralWidget(self, widget: QWidget) -> None: def setCentralWidget(self, widget: QWidget) -> None:
"""Set a layout as the central object.""" """Set a layout as the central object."""
layout = QHBoxLayout() layout = QHBoxLayout()
layout.addWidget(widget) layout.addWidget(widget)
self.setLayout(layout) self.setLayout(layout)
return
class NScrollablePage(QScrollArea): class NScrollablePage(QScrollArea):
"""Extension: Scrollable Page Widget """Extension: Scrollable Page Widget.
A custom widget that holds a layout within a scrollable area. A custom widget that holds a layout within a scrollable area.
""" """
@@ -79,16 +76,14 @@ class NScrollablePage(QScrollArea):
self.setVerticalScrollBarPolicy(QtScrollAsNeeded) self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
self.setFrameShadow(QFrame.Shadow.Sunken) self.setFrameShadow(QFrame.Shadow.Sunken)
self.setFrameShape(QFrame.Shape.StyledPanel) self.setFrameShape(QFrame.Shape.StyledPanel)
return
def setCentralLayout(self, layout: QLayout) -> None: def setCentralLayout(self, layout: QLayout) -> None:
"""Set the central layout of the scroll page.""" """Set the central layout of the scroll page."""
self._widget.setLayout(layout) self._widget.setLayout(layout)
return
class NScrollableForm(QScrollArea): class NScrollableForm(QScrollArea):
"""Extension: Scrollable Form Widget """Extension: Scrollable Form Widget.
A custom widget that creates a form within a scrollable area. A custom widget that creates a form within a scrollable area.
""" """
@@ -117,8 +112,6 @@ class NScrollableForm(QScrollArea):
self.setFrameShadow(QFrame.Shadow.Sunken) self.setFrameShadow(QFrame.Shadow.Sunken)
self.setFrameShape(QFrame.Shape.StyledPanel) self.setFrameShape(QFrame.Shape.StyledPanel)
return
## ##
# Properties # Properties
## ##
@@ -135,18 +128,15 @@ class NScrollableForm(QScrollArea):
"""Set the text color for the help text.""" """Set the text color for the help text."""
self._helpCol = color self._helpCol = color
self._fontScale = scale self._fontScale = scale
return
def setHelpText(self, key: str, text: str) -> None: def setHelpText(self, key: str, text: str) -> None:
"""Set the text for the help label.""" """Set the text for the help label."""
if qHelp := self._editable.get(key): if qHelp := self._editable.get(key):
qHelp.setText(text) qHelp.setText(text)
return
def setRowIndent(self, indent: int) -> None: def setRowIndent(self, indent: int) -> None:
"""Set the indentation of each row.""" """Set the indentation of each row."""
self._indent = max(indent, 0) self._indent = max(indent, 0)
return
## ##
# Methods # Methods
@@ -158,7 +148,6 @@ class NScrollableForm(QScrollArea):
yPos = self._sections[identifier].pos().y() - 8 yPos = self._sections[identifier].pos().y() - 8
if vBar := self.verticalScrollBar(): if vBar := self.verticalScrollBar():
vBar.setValue(yPos) vBar.setValue(yPos)
return
def scrollToLabel(self, label: str) -> None: def scrollToLabel(self, label: str) -> None:
"""Scroll to the requested label.""" """Scroll to the requested label."""
@@ -166,7 +155,6 @@ class NScrollableForm(QScrollArea):
yPos = self._index[label].pos().y() - 8 yPos = self._index[label].pos().y() - 8
if vBar := self.verticalScrollBar(): if vBar := self.verticalScrollBar():
vBar.setValue(yPos) vBar.setValue(yPos)
return
def addGroupLabel(self, label: str, identifier: int | None = None) -> None: def addGroupLabel(self, label: str, identifier: int | None = None) -> None:
"""Add a text label to separate groups of settings.""" """Add a text label to separate groups of settings."""
@@ -178,7 +166,6 @@ class NScrollableForm(QScrollArea):
self._first = False self._first = False
if identifier is not None: if identifier is not None:
self._sections[identifier] = qLabel self._sections[identifier] = qLabel
return
def addRow( def addRow(
self, self,
@@ -252,17 +239,14 @@ class NScrollableForm(QScrollArea):
self._index[label.strip()] = qWidget self._index[label.strip()] = qWidget
qLabel.setAccessibleName(text) qLabel.setAccessibleName(text)
return
def finalise(self) -> None: def finalise(self) -> None:
"""Finalise the layout when the form is built.""" """Finalise the layout when the form is built."""
self._layout.addSpacing(20) self._layout.addSpacing(20)
self._layout.addStretch(1) self._layout.addStretch(1)
return
class NColorLabel(QLabel): class NColorLabel(QLabel):
"""Extension: A Coloured Label """Extension: A Coloured Label.
A custom widget that draws a label in a specific colour, and A custom widget that draws a label in a specific colour, and
optionally at a specific size, and word wrapped. optionally at a specific size, and word wrapped.
@@ -298,21 +282,17 @@ class NColorLabel(QLabel):
self.setWordWrap(wrap) self.setWordWrap(wrap)
self.setColorState(True) self.setColorState(True)
return
def setTextColors(self, *, color: QColor | None = None, faded: QColor | None = None) -> None: def setTextColors(self, *, color: QColor | None = None, faded: QColor | None = None) -> None:
"""Set or update the text colours.""" """Set or update the text colours."""
self._color = color or self._color self._color = color or self._color
self._faded = faded or self._faded self._faded = faded or self._faded
self._refeshTextColor() self._refeshTextColor()
return
def setColorState(self, state: bool) -> None: def setColorState(self, state: bool) -> None:
"""Change the colour state.""" """Change the colour state."""
if self._state is not state: if self._state is not state:
self._state = state self._state = state
self._refeshTextColor() self._refeshTextColor()
return
def _refeshTextColor(self) -> None: def _refeshTextColor(self) -> None:
"""Refresh the colour of the text on the label.""" """Refresh the colour of the text on the label."""
@@ -322,11 +302,10 @@ class NColorLabel(QLabel):
self._color if self._state else self._faded, self._color if self._state else self._faded,
) )
self.setPalette(palette) self.setPalette(palette)
return
class NWrappedWidgetBox(QHBoxLayout): class NWrappedWidgetBox(QHBoxLayout):
"""Extension: A Text-Wrapped Widget Box """Extension: A Text-Wrapped Widget Box.
A custom layout box where a widget is wrapped in text labels on A custom layout box where a widget is wrapped in text labels on
either side within a layout box. The widget is inserted at the {0} either side within a layout box. The widget is inserted at the {0}
@@ -341,4 +320,3 @@ class NWrappedWidgetBox(QHBoxLayout):
self.addWidget(widget) self.addWidget(widget)
if after: if after:
self.addWidget(QLabel(after.lstrip())) self.addWidget(QLabel(after.lstrip()))
return
+3 -3
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -34,7 +34,7 @@ if TYPE_CHECKING:
class WheelEventFilter(QObject): class WheelEventFilter(QObject):
"""Extensions: Wheel Event Filter """Extensions: Wheel Event Filter.
An event filter that filters mouse wheel events for a widget and An event filter that filters mouse wheel events for a widget and
forward them to the root widget. This solves the lack of mouse wheel forward them to the root widget. This solves the lack of mouse wheel
@@ -50,7 +50,6 @@ class WheelEventFilter(QObject):
super().__init__(parent=parent) super().__init__(parent=parent)
self._parent = parent self._parent = parent
self._locked = False self._locked = False
return
def eventFilter(self, obj: QObject, event: QEvent) -> bool: def eventFilter(self, obj: QObject, event: QEvent) -> bool:
"""Filter events of type QWheelEvent and forward them to the """Filter events of type QWheelEvent and forward them to the
@@ -69,6 +68,7 @@ class WheelEventFilter(QObject):
class StatusTipFilter(QObject): class StatusTipFilter(QObject):
"""Filter: Remove StatusBar ToolTips."""
def eventFilter(self, obj: QObject, event: QEvent) -> bool: def eventFilter(self, obj: QObject, event: QEvent) -> bool:
"""Filter out status tip events on menus.""" """Filter out status tip events on menus."""
+57 -38
View File
@@ -24,7 +24,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -47,6 +47,7 @@ if TYPE_CHECKING:
class NDialog(QDialog): class NDialog(QDialog):
"""Custom: Modified QDialog."""
def softDelete(self) -> None: def softDelete(self) -> None:
"""Since calling deleteLater is sometimes not safe from Python, """Since calling deleteLater is sometimes not safe from Python,
@@ -55,53 +56,54 @@ class NDialog(QDialog):
so that it gets garbage collected when it runs out of scope. so that it gets garbage collected when it runs out of scope.
""" """
self.setParent(None) # type: ignore self.setParent(None) # type: ignore
return
@pyqtSlot() @pyqtSlot()
def reject(self) -> None: def reject(self) -> None:
"""Overload the reject slot and also call close.""" """Overload the reject slot and also call close."""
super().reject() super().reject()
self.close() self.close()
return
class NToolDialog(NDialog): class NToolDialog(NDialog):
"""Custom: Modified QDialog for Tools."""
def __init__(self, parent: GuiMain) -> None: def __init__(self, parent: GuiMain) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self.setModal(False) self.setModal(False)
if CONFIG.osDarwin: if CONFIG.osDarwin:
self.setWindowFlag(Qt.WindowType.Tool) self.setWindowFlag(Qt.WindowType.Tool)
return
def activateDialog(self) -> None: def activateDialog(self) -> None:
"""Helper function to activate dialog on various systems.""" """Activate dialog on various operating systems."""
self.show() self.show()
if CONFIG.osWindows: if CONFIG.osWindows:
self.activateWindow() self.activateWindow()
self.raise_() self.raise_()
QApplication.processEvents() QApplication.processEvents()
return
class NNonBlockingDialog(NDialog): class NNonBlockingDialog(NDialog):
"""Custom: Modified Non-Blocking QDialog."""
def __init__(self, parent: QWidget | None = None) -> None: def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self.setModal(True) self.setModal(True)
return
def activateDialog(self) -> None: def activateDialog(self) -> None:
"""Helper function to activate dialog on various systems.""" """Activate dialog on various operating systems."""
self.show() self.show()
if CONFIG.osWindows: if CONFIG.osWindows:
self.activateWindow() self.activateWindow()
self.raise_() self.raise_()
QApplication.processEvents() QApplication.processEvents()
return
class NTreeView(QTreeView): class NTreeView(QTreeView):
"""Custom: Modified QTreeView.
The main purpose is to provide the middleClicked signal that matches
clicked and doubleCLicked.
"""
middleClicked = pyqtSignal(QModelIndex) middleClicked = pyqtSignal(QModelIndex)
@@ -116,6 +118,12 @@ class NTreeView(QTreeView):
class NComboBox(QComboBox): class NComboBox(QComboBox):
"""Custom: Modified QComboBox.
The main purpose is to provide a combo box that doesn't scroll when
the mousewheel is active on it while scrolling through a scrollable
window of many widgets.
"""
def __init__(self, parent: QWidget | None = None, maxItems: int = 15) -> None: def __init__(self, parent: QWidget | None = None, maxItems: int = 15) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -126,7 +134,30 @@ class NComboBox(QComboBox):
# and allows for scrolling of long lists of items # and allows for scrolling of long lists of items
self.setStyleSheet("QComboBox {combobox-popup: 0;}") self.setStyleSheet("QComboBox {combobox-popup: 0;}")
return def wheelEvent(self, event: QWheelEvent) -> None:
"""Only capture the mouse wheel if the widget has focus."""
if self.hasFocus():
super().wheelEvent(event)
else:
event.ignore()
def setCurrentData(self, data: str | int | Enum, default: str | int | Enum) -> None:
"""Set the current index from data, with a fallback."""
idx = self.findData(data)
self.setCurrentIndex(self.findData(default) if idx < 0 else idx)
class NSpinBox(QSpinBox):
"""Custom: Modified QSpinBox.
The main purpose is to provide a spin box that doesn't scroll when
the mousewheel is active on it while scrolling through a scrollable
window of many widgets.
"""
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent=parent)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
def wheelEvent(self, event: QWheelEvent) -> None: def wheelEvent(self, event: QWheelEvent) -> None:
"""Only capture the mouse wheel if the widget has focus.""" """Only capture the mouse wheel if the widget has focus."""
@@ -134,31 +165,15 @@ class NComboBox(QComboBox):
super().wheelEvent(event) super().wheelEvent(event)
else: else:
event.ignore() event.ignore()
return
def setCurrentData(self, data: str | int | Enum, default: str | int | Enum) -> None:
"""Set the current index from data, with a fallback."""
idx = self.findData(data)
self.setCurrentIndex(self.findData(default) if idx < 0 else idx)
return
class NSpinBox(QSpinBox):
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent=parent)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
return
def wheelEvent(self, event: QWheelEvent) -> None:
if self.hasFocus():
super().wheelEvent(event)
else:
event.ignore()
return
class NDoubleSpinBox(QDoubleSpinBox): class NDoubleSpinBox(QDoubleSpinBox):
"""Custom: Modified QDoubleSpinBox.
The main purpose is to provide a float spin box that doesn't scroll
when the mousewheel is active on it while scrolling through a
scrollable window of many widgets.
"""
def __init__( def __init__(
self, self,
@@ -175,17 +190,20 @@ class NDoubleSpinBox(QDoubleSpinBox):
self.setMaximum(maxVal) self.setMaximum(maxVal)
self.setSingleStep(step) self.setSingleStep(step)
self.setDecimals(prec) self.setDecimals(prec)
return
def wheelEvent(self, event: QWheelEvent) -> None: def wheelEvent(self, event: QWheelEvent) -> None:
"""Only capture the mouse wheel if the widget has focus."""
if self.hasFocus(): if self.hasFocus():
super().wheelEvent(event) super().wheelEvent(event)
else: else:
event.ignore() event.ignore()
return
class NIconToolButton(QToolButton): class NIconToolButton(QToolButton):
"""Custom: Modified QToolButton.
A quicker way to create a tool button using the app theme.
"""
def __init__( def __init__(
self, parent: QWidget, iconSize: QSize, self, parent: QWidget, iconSize: QSize,
@@ -197,15 +215,17 @@ class NIconToolButton(QToolButton):
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
if icon: if icon:
self.setThemeIcon(icon, color) self.setThemeIcon(icon, color)
return
def setThemeIcon(self, iconKey: str, color: str | None = None) -> None: def setThemeIcon(self, iconKey: str, color: str | None = None) -> None:
"""Set an icon from the current theme.""" """Set an icon from the current theme."""
self.setIcon(SHARED.theme.getIcon(iconKey, color)) self.setIcon(SHARED.theme.getIcon(iconKey, color))
return
class NIconToggleButton(QToolButton): class NIconToggleButton(QToolButton):
"""Custom: Modified QToolButton.
A quicker way to create a toggle button using the app theme.
"""
def __init__(self, parent: QWidget, iconSize: QSize, icon: str | None = None) -> None: def __init__(self, parent: QWidget, iconSize: QSize, icon: str | None = None) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -216,16 +236,15 @@ class NIconToggleButton(QToolButton):
self.setStyleSheet("border: none; background: transparent;") self.setStyleSheet("border: none; background: transparent;")
if icon: if icon:
self.setThemeIcon(icon) self.setThemeIcon(icon)
return
def setThemeIcon(self, iconKey: str) -> None: def setThemeIcon(self, iconKey: str) -> None:
"""Set an icon from the current theme.""" """Set an icon from the current theme."""
iconSize = self.iconSize() iconSize = self.iconSize()
self.setIcon(SHARED.theme.getToggleIcon(iconKey, (iconSize.width(), iconSize.height()))) self.setIcon(SHARED.theme.getToggleIcon(iconKey, (iconSize.width(), iconSize.height())))
return
class NClickableLabel(QLabel): class NClickableLabel(QLabel):
"""Custom: Clickable QLabel."""
mouseClicked = pyqtSignal() mouseClicked = pyqtSignal()
+2 -9
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
class NovelSelector(QComboBox): class NovelSelector(QComboBox):
"""Custom: Novel Root Folder Selector."""
novelSelectionChanged = pyqtSignal(str) novelSelectionChanged = pyqtSignal(str)
@@ -53,7 +54,6 @@ class NovelSelector(QComboBox):
self._listFormat = None self._listFormat = None
self.currentIndexChanged.connect(self._indexChanged) self.currentIndexChanged.connect(self._indexChanged)
self.updateTheme() self.updateTheme()
return
## ##
# Properties # Properties
@@ -80,18 +80,15 @@ class NovelSelector(QComboBox):
self._blockSignal = blockSignal self._blockSignal = blockSignal
self.setCurrentIndex(index) self.setCurrentIndex(index)
self._blockSignal = False self._blockSignal = False
return
def setIncludeAll(self, value: bool) -> None: def setIncludeAll(self, value: bool) -> None:
"""Set flag to add an "All Novel Folders" option.""" """Set flag to add an "All Novel Folders" option."""
self._includeAll = value self._includeAll = value
return
def setListFormat(self, value: str | None) -> None: def setListFormat(self, value: str | None) -> None:
"""Set a format string for the list entries.""" """Set a format string for the list entries."""
if value is None or "{0}" in value: if value is None or "{0}" in value:
self._listFormat = value self._listFormat = value
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme colours.""" """Update theme colours."""
@@ -99,7 +96,6 @@ class NovelSelector(QComboBox):
palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, palette.text()) palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, palette.text())
self.setPalette(palette) self.setPalette(palette)
self.refreshNovelList() self.refreshNovelList()
return
## ##
# Public Slots # Public Slots
@@ -133,8 +129,6 @@ class NovelSelector(QComboBox):
self.setEnabled(self.count() > 1) self.setEnabled(self.count() > 1)
self._blockSignal = False self._blockSignal = False
return
## ##
# Private Slots # Private Slots
## ##
@@ -144,4 +138,3 @@ class NovelSelector(QComboBox):
"""Re-emit the change of selection signal, unless blocked.""" """Re-emit the change of selection signal, unless blocked."""
if not self._blockSignal: if not self._blockSignal:
self.novelSelectionChanged.emit(self.currentData()) self.novelSelectionChanged.emit(self.currentData())
return
+3 -18
View File
@@ -22,7 +22,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from PyQt6.QtCore import QPoint, QRectF, QSize, Qt, pyqtSignal, pyqtSlot from PyQt6.QtCore import QPoint, QRectF, QSize, Qt, pyqtSignal, pyqtSlot
@@ -39,7 +39,7 @@ from novelwriter.types import (
class NPagedSideBar(QToolBar): class NPagedSideBar(QToolBar):
"""Extensions: Paged Side Bar """Extensions: Paged Side Bar.
A side bar widget that holds buttons that mimic tabs. It is designed A side bar widget that holds buttons that mimic tabs. It is designed
to be used in combination with a QStackedWidget for options panels. to be used in combination with a QStackedWidget for options panels.
@@ -65,8 +65,6 @@ class NPagedSideBar(QToolBar):
stretch.setSizePolicy(QtSizeExpanding, QtSizeExpanding) stretch.setSizePolicy(QtSizeExpanding, QtSizeExpanding)
self._stretchAction = self.addWidget(stretch) self._stretchAction = self.addWidget(stretch)
return
def button(self, buttonId: int) -> _PagedToolButton: def button(self, buttonId: int) -> _PagedToolButton:
"""Return a specific button.""" """Return a specific button."""
return self._buttons[buttonId] return self._buttons[buttonId]
@@ -74,14 +72,12 @@ class NPagedSideBar(QToolBar):
def setLabelColor(self, color: QColor) -> None: def setLabelColor(self, color: QColor) -> None:
"""Set the text color for the labels.""" """Set the text color for the labels."""
self._labelCol = color self._labelCol = color
return
def addLabel(self, text: str) -> None: def addLabel(self, text: str) -> None:
"""Add a new label to the toolbar.""" """Add a new label to the toolbar."""
label = _NPagedToolLabel(self, self._labelCol) label = _NPagedToolLabel(self, self._labelCol)
label.setText(text) label.setText(text)
self.insertWidget(self._stretchAction, label) self.insertWidget(self._stretchAction, label)
return
def addButton(self, text: str, buttonId: int = -1) -> None: def addButton(self, text: str, buttonId: int = -1) -> None:
"""Add a new button to the toolbar.""" """Add a new button to the toolbar."""
@@ -90,13 +86,11 @@ class NPagedSideBar(QToolBar):
self.insertWidget(self._stretchAction, button) self.insertWidget(self._stretchAction, button)
self._group.addButton(button, id=buttonId) self._group.addButton(button, id=buttonId)
self._buttons[buttonId] = button self._buttons[buttonId] = button
return
def setSelected(self, buttonId: int) -> None: def setSelected(self, buttonId: int) -> None:
"""Set the selected button.""" """Set the selected button."""
if button := self._group.button(buttonId): if button := self._group.button(buttonId):
button.setChecked(True) button.setChecked(True)
return
## ##
# Private Slots # Private Slots
@@ -104,11 +98,10 @@ class NPagedSideBar(QToolBar):
@pyqtSlot("QAbstractButton*") @pyqtSlot("QAbstractButton*")
def _buttonClicked(self, button: QAbstractButton) -> None: def _buttonClicked(self, button: QAbstractButton) -> None:
"""A button was clicked in the group, emit its id.""" """Handle a button click in the group and emit its id."""
buttonId = self._group.id(button) buttonId = self._group.id(button)
if buttonId != -1: if buttonId != -1:
self.buttonClicked.emit(buttonId) self.buttonClicked.emit(buttonId)
return
class _PagedToolButton(QToolButton): class _PagedToolButton(QToolButton):
@@ -127,8 +120,6 @@ class _PagedToolButton(QToolButton):
self._aH = 2*fH//7 self._aH = 2*fH//7
self.setFixedHeight(self._bH) self.setFixedHeight(self._bH)
return
def sizeHint(self) -> QSize: def sizeHint(self) -> QSize:
"""Return a size hint that includes the arrow.""" """Return a size hint that includes the arrow."""
return super().sizeHint() + QSize(4*self._aH, 0) return super().sizeHint() + QSize(4*self._aH, 0)
@@ -180,8 +171,6 @@ class _PagedToolButton(QToolButton):
])) ]))
painter.end() painter.end()
return
class _NPagedToolLabel(QLabel): class _NPagedToolLabel(QLabel):
@@ -199,8 +188,6 @@ class _NPagedToolLabel(QLabel):
self._textCol = textColor or self.palette().text().color() self._textCol = textColor or self.palette().text().color()
return
def paintEvent(self, event: QPaintEvent) -> None: def paintEvent(self, event: QPaintEvent) -> None:
"""Overload the paint event to draw a simple, left aligned text """Overload the paint event to draw a simple, left aligned text
label that matches the button style. label that matches the button style.
@@ -215,5 +202,3 @@ class _NPagedToolLabel(QLabel):
painter.setOpacity(1.0) painter.setOpacity(1.0)
painter.drawText(QRectF(4, self._tM, tW, tH), QtAlignLeft, self.text()) painter.drawText(QRectF(4, self._tM, tW, tH), QtAlignLeft, self.text())
painter.end() painter.end()
return
+5 -11
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from math import ceil from math import ceil
@@ -37,7 +37,7 @@ from novelwriter.types import (
class NProgressCircle(QProgressBar): class NProgressCircle(QProgressBar):
"""Extension: Circular Progress Widget """Extension: Circular Progress Widget.
A custom widget that paints a circular progress indicator instead of A custom widget that paints a circular progress indicator instead of
a straight bar. It is also possible to set custom text for iṫ. a straight bar. It is also possible to set custom text for iṫ.
@@ -64,7 +64,6 @@ class NProgressCircle(QProgressBar):
self.setSizePolicy(QtSizeFixed, QtSizeFixed) self.setSizePolicy(QtSizeFixed, QtSizeFixed)
self.setFixedWidth(size) self.setFixedWidth(size)
self.setFixedHeight(size) self.setFixedHeight(size)
return
def setColors( def setColors(
self, back: QColor | None = None, track: QColor | None = None, self, back: QColor | None = None, track: QColor | None = None,
@@ -80,16 +79,14 @@ class NProgressCircle(QProgressBar):
self._bPen = QPen(QBrush(track), self._point, QtSolidLine, QtRoundCap) self._bPen = QPen(QBrush(track), self._point, QtSolidLine, QtRoundCap)
if isinstance(text, QColor): if isinstance(text, QColor):
self._tColor = text self._tColor = text
return
def setCentreText(self, text: str | None) -> None: def setCentreText(self, text: str | None) -> None:
"""Replace the progress text with a custom string.""" """Replace the progress text with a custom string."""
self._text = text self._text = text
self.setValue(self.value()) # Triggers a redraw self.setValue(self.value()) # Triggers a redraw
return
def paintEvent(self, event: QPaintEvent) -> None: def paintEvent(self, event: QPaintEvent) -> None:
"""Custom painter for the progress bar.""" """Paint the progress bar."""
progress = 100.0*self.value()/self.maximum() progress = 100.0*self.value()/self.maximum()
angle = ceil(16*3.6*progress) angle = ceil(16*3.6*progress)
painter = QPainter(self) painter = QPainter(self)
@@ -103,21 +100,19 @@ class NProgressCircle(QProgressBar):
painter.drawArc(self._cRect, 90*16, -angle) painter.drawArc(self._cRect, 90*16, -angle)
painter.setPen(self._tColor) painter.setPen(self._tColor)
painter.drawText(self._cRect, QtAlignCenter, self._text or f"{progress:.1f} %") painter.drawText(self._cRect, QtAlignCenter, self._text or f"{progress:.1f} %")
return
class NProgressSimple(QProgressBar): class NProgressSimple(QProgressBar):
"""Extension: Simple Progress Widget """Extension: Simple Progress Widget.
A custom widget that paints a plain bar with no other styling. A custom widget that paints a plain bar with no other styling.
""" """
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
return
def paintEvent(self, event: QPaintEvent) -> None: def paintEvent(self, event: QPaintEvent) -> None:
"""Custom painter for the progress bar.""" """Paint the progress bar."""
if (value := self.value()) > 0: if (value := self.value()) > 0:
progress = ceil(self.width()*float(value)/self.maximum()) progress = ceil(self.width()*float(value)/self.maximum())
painter = QPainter(self) painter = QPainter(self)
@@ -125,4 +120,3 @@ class NProgressSimple(QProgressBar):
painter.setPen(self.palette().highlight().color()) painter.setPen(self.palette().highlight().color())
painter.setBrush(self.palette().highlight()) painter.setBrush(self.palette().highlight())
painter.drawRect(0, 0, progress, self.height()) painter.drawRect(0, 0, progress, self.height())
return
+2 -5
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -34,6 +34,7 @@ logger = logging.getLogger(__name__)
class StatusLED(QAbstractButton): class StatusLED(QAbstractButton):
"""Custom: LED Style Indicator."""
__slots__ = ("_color", "_negative", "_neutral", "_postitve", "_state") __slots__ = ("_color", "_negative", "_neutral", "_postitve", "_state")
@@ -46,7 +47,6 @@ class StatusLED(QAbstractButton):
self._state = None self._state = None
self.setFixedWidth(sW) self.setFixedWidth(sW)
self.setFixedHeight(sH) self.setFixedHeight(sH)
return
@property @property
def state(self) -> bool | None: def state(self) -> bool | None:
@@ -59,7 +59,6 @@ class StatusLED(QAbstractButton):
self._postitve = positive self._postitve = positive
self._negative = negative self._negative = negative
self.setState(self._state) self.setState(self._state)
return
def setState(self, state: bool | None) -> None: def setState(self, state: bool | None) -> None:
"""Set the colour state.""" """Set the colour state."""
@@ -71,7 +70,6 @@ class StatusLED(QAbstractButton):
self._color = self._neutral self._color = self._neutral
self._state = state self._state = state
self.update() self.update()
return
def paintEvent(self, event: QPaintEvent) -> None: def paintEvent(self, event: QPaintEvent) -> None:
"""Draw the LED.""" """Draw the LED."""
@@ -82,4 +80,3 @@ class StatusLED(QAbstractButton):
painter.setOpacity(1.0) painter.setOpacity(1.0)
painter.drawEllipse(1, 1, self.width() - 2, self.height() - 2) painter.drawEllipse(1, 1, self.width() - 2, self.height() - 2)
painter.end() painter.end()
return
+2 -10
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty, pyqtSlot # pyright: ignore from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty, pyqtSlot # pyright: ignore
@@ -32,6 +32,7 @@ from novelwriter.types import QtNoPen, QtPaintAntiAlias, QtSizeFixed
class NSwitch(QAbstractButton): class NSwitch(QAbstractButton):
"""Custom: Toggle Switch."""
__slots__ = ("_cOff", "_cOn", "_offset", "_rH", "_rR", "_xH", "_xR", "_xW") __slots__ = ("_cOff", "_cOn", "_offset", "_rH", "_rR", "_xH", "_xR", "_xW")
@@ -55,8 +56,6 @@ class NSwitch(QAbstractButton):
self.clicked.connect(self._onClick) self.clicked.connect(self._onClick)
return
## ##
# Properties # Properties
## ##
@@ -69,7 +68,6 @@ class NSwitch(QAbstractButton):
def offset(self, offset: int) -> None: def offset(self, offset: int) -> None:
self._offset = offset self._offset = offset
self.update() self.update()
return
## ##
# Getters and Setters # Getters and Setters
@@ -79,7 +77,6 @@ class NSwitch(QAbstractButton):
"""Overload setChecked to also alter the offset.""" """Overload setChecked to also alter the offset."""
super().setChecked(checked) super().setChecked(checked)
self._offset = (self._xW - self._xR) if checked else self._xR self._offset = (self._xW - self._xR) if checked else self._xR
return
## ##
# Events # Events
@@ -89,7 +86,6 @@ class NSwitch(QAbstractButton):
"""Overload resize to ensure correct offset.""" """Overload resize to ensure correct offset."""
super().resizeEvent(event) super().resizeEvent(event)
self._offset = (self._xW - self._xR) if self.isChecked() else self._xR self._offset = (self._xW - self._xR) if self.isChecked() else self._xR
return
def paintEvent(self, event: QPaintEvent) -> None: def paintEvent(self, event: QPaintEvent) -> None:
"""Drawing the switch itself.""" """Drawing the switch itself."""
@@ -109,13 +105,10 @@ class NSwitch(QAbstractButton):
painter.end() painter.end()
return
def enterEvent(self, event: QEnterEvent) -> None: def enterEvent(self, event: QEnterEvent) -> None:
"""Change the cursor when hovering the button.""" """Change the cursor when hovering the button."""
self.setCursor(Qt.CursorShape.PointingHandCursor) self.setCursor(Qt.CursorShape.PointingHandCursor)
super().enterEvent(event) super().enterEvent(event)
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _onClick(self, checked: bool) -> None: def _onClick(self, checked: bool) -> None:
@@ -125,4 +118,3 @@ class NSwitch(QAbstractButton):
anim.setStartValue(self._offset) anim.setStartValue(self._offset)
anim.setEndValue((self._xW - self._xR) if checked else self._xR) anim.setEndValue((self._xW - self._xR) if checked else self._xR)
anim.start() anim.start()
return
+2 -11
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -39,7 +39,7 @@ if TYPE_CHECKING:
class NSwitchBox(QScrollArea): class NSwitchBox(QScrollArea):
"""Extension: Switch Box Widget """Extension: Switch Box Widget.
A widget that can hold a list of switches with labels and optional A widget that can hold a list of switches with labels and optional
icons. The switch toggles emits a common signal with a switch key. icons. The switch toggles emits a common signal with a switch key.
@@ -55,7 +55,6 @@ class NSwitchBox(QScrollArea):
self._sIcon = baseSize self._sIcon = baseSize
self._widgets = [] self._widgets = []
self.clear() self.clear()
return
def clear(self) -> None: def clear(self) -> None:
"""Rebuild the content of the core widget.""" """Rebuild the content of the core widget."""
@@ -72,8 +71,6 @@ class NSwitchBox(QScrollArea):
self.setWidgetResizable(True) self.setWidgetResizable(True)
self.setWidget(self._widget) self.setWidget(self._widget)
return
def addLabel(self, text: str) -> None: def addLabel(self, text: str) -> None:
"""Add a header label to the content box.""" """Add a header label to the content box."""
label = QLabel(text, self) label = QLabel(text, self)
@@ -83,7 +80,6 @@ class NSwitchBox(QScrollArea):
self._content.addWidget(label, self._index, 0, 1, 3, QtAlignLeft) self._content.addWidget(label, self._index, 0, 1, 3, QtAlignLeft)
self._widgets.append(label) self._widgets.append(label)
self._bumpIndex() self._bumpIndex()
return
def addItem(self, qIcon: QIcon, text: str, identifier: str, default: bool = False) -> None: def addItem(self, qIcon: QIcon, text: str, identifier: str, default: bool = False) -> None:
"""Add an item to the content box.""" """Add an item to the content box."""
@@ -104,8 +100,6 @@ class NSwitchBox(QScrollArea):
self._widgets.append(switch) self._widgets.append(switch)
self._bumpIndex() self._bumpIndex()
return
def addSeparator(self) -> None: def addSeparator(self) -> None:
"""Add a blank entry in the content box.""" """Add a blank entry in the content box."""
spacer = QWidget(self) spacer = QWidget(self)
@@ -113,7 +107,6 @@ class NSwitchBox(QScrollArea):
self._content.addWidget(spacer, self._index, 0, 1, 3, QtAlignLeft) self._content.addWidget(spacer, self._index, 0, 1, 3, QtAlignLeft)
self._widgets.append(spacer) self._widgets.append(spacer)
self._bumpIndex() self._bumpIndex()
return
## ##
# Internal Functions # Internal Functions
@@ -122,7 +115,6 @@ class NSwitchBox(QScrollArea):
def _emitSwitchSignal(self, identifier: str, state: bool) -> None: def _emitSwitchSignal(self, identifier: str, state: bool) -> None:
"""Emit a signal for a switch toggle.""" """Emit a signal for a switch toggle."""
self.switchToggled.emit(identifier, state) self.switchToggled.emit(identifier, state)
return
def _bumpIndex(self) -> None: def _bumpIndex(self) -> None:
"""Increase the index counter and make sure only the last """Increase the index counter and make sure only the last
@@ -131,4 +123,3 @@ class NSwitchBox(QScrollArea):
self._content.setRowStretch(self._index, 0) self._content.setRowStretch(self._index, 0)
self._content.setRowStretch(self._index + 1, 1) self._content.setRowStretch(self._index + 1, 1)
self._index += 1 self._index += 1
return
+6 -7
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -45,6 +45,11 @@ API_URL = "https://api.github.com/repos/vkbo/novelwriter/releases/latest"
class VersionInfoWidget(QWidget): class VersionInfoWidget(QWidget):
"""Custom: version Info Label.
A custom widget that will show a clickable area for contacting
GitHub and pulling the latest release version info.
"""
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -75,8 +80,6 @@ class VersionInfoWidget(QWidget):
self.setLayout(self._layout) self.setLayout(self._layout)
return
## ##
# Private Slots # Private Slots
## ##
@@ -93,7 +96,6 @@ class VersionInfoWidget(QWidget):
lookup = _Retriever() lookup = _Retriever()
lookup.signals.dataReady.connect(self._updateReleaseInfo) lookup.signals.dataReady.connect(self._updateReleaseInfo)
SHARED.runInThreadPool(lookup) SHARED.runInThreadPool(lookup)
return
## ##
# Private Slots # Private Slots
@@ -109,7 +111,6 @@ class VersionInfoWidget(QWidget):
)) ))
else: else:
self._lblRelease.setText(self._trLatest.format(reason or self.tr("Failed"))) self._lblRelease.setText(self._trLatest.format(reason or self.tr("Failed")))
return
class _Retriever(QRunnable): class _Retriever(QRunnable):
@@ -117,7 +118,6 @@ class _Retriever(QRunnable):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.signals = _RetrieverSignal() self.signals = _RetrieverSignal()
return
@pyqtSlot() @pyqtSlot()
def run(self) -> None: def run(self) -> None:
@@ -140,7 +140,6 @@ class _Retriever(QRunnable):
except Exception as e: except Exception as e:
logger.error("Failed to retrieve release info") logger.error("Failed to retrieve release info")
self.signals.dataReady.emit("", str(e)) self.signals.dataReady.emit("", str(e))
return
class _RetrieverSignal(QObject): class _RetrieverSignal(QObject):
+1 -1
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import re import re
+12 -34
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -100,7 +100,7 @@ def _wText(parent: ET.Element, text: str) -> ET.Element:
def _mmToSz(value: float) -> int: def _mmToSz(value: float) -> int:
"""Convert millimetres to internal margin size units""" """Convert millimetres to internal margin size units."""
return int(value*20.0*72.0/25.4) return int(value*20.0*72.0/25.4)
@@ -143,6 +143,7 @@ S_FNOTE = "FootnoteText"
class DocXXmlRel(NamedTuple): class DocXXmlRel(NamedTuple):
"""DocX XML Rel Data."""
rId: str rId: str
relType: str relType: str
@@ -150,6 +151,7 @@ class DocXXmlRel(NamedTuple):
class DocXXmlFile(NamedTuple): class DocXXmlFile(NamedTuple):
"""DocX XML File Data."""
xml: ET.Element xml: ET.Element
path: str path: str
@@ -157,6 +159,7 @@ class DocXXmlFile(NamedTuple):
class DocXParStyle(NamedTuple): class DocXParStyle(NamedTuple):
"""DocX XML Paragraph Style Data."""
name: str name: str
styleId: str styleId: str
@@ -176,7 +179,7 @@ class DocXParStyle(NamedTuple):
class ToDocX(Tokenizer): class ToDocX(Tokenizer):
"""Core: DocX Document Writer """Core: DocX Document Writer.
Extend the Tokenizer class to writer DocX Document files. Extend the Tokenizer class to writer DocX Document files.
""" """
@@ -202,8 +205,6 @@ class ToDocX(Tokenizer):
self._usedNotes: dict[str, int] = {} self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[ET.Element, str]] = [] self._usedFields: list[tuple[ET.Element, str]] = []
return
## ##
# Setters # Setters
## ##
@@ -214,25 +215,22 @@ class ToDocX(Tokenizer):
"""Set the document page size and margins in millimetres.""" """Set the document page size and margins in millimetres."""
self._pageSize = QSize(_mmToSz(width), _mmToSz(height)) self._pageSize = QSize(_mmToSz(width), _mmToSz(height))
self._pageMargins = QMargins(_mmToSz(left), _mmToSz(top), _mmToSz(right), _mmToSz(bottom)) self._pageMargins = QMargins(_mmToSz(left), _mmToSz(top), _mmToSz(right), _mmToSz(bottom))
return
def setHeaderFormat(self, value: str, offset: int) -> None: def setHeaderFormat(self, value: str, offset: int) -> None:
"""Set the document header format.""" """Set the document header format."""
self._headerFormat = value.strip() self._headerFormat = value.strip()
self._pageOffset = offset self._pageOffset = offset
return
## ##
# Class Methods # Class Methods
## ##
def initDocument(self) -> None: def initDocument(self) -> None:
"""Initialises the DocX document structure.""" """Initialise the DocX document structure."""
super().initDocument() super().initDocument()
self._fontFamily = self._textFont.family() self._fontFamily = self._textFont.family()
self._fontSize = self._textFont.pointSizeF() self._fontSize = self._textFont.pointSizeF()
self._generateStyles() self._generateStyles()
return
def doConvert(self) -> None: def doConvert(self) -> None:
"""Convert the list of text tokens into XML elements.""" """Convert the list of text tokens into XML elements."""
@@ -302,8 +300,6 @@ class ToDocX(Tokenizer):
elif tType == BlockTyp.KEYWORD: elif tType == BlockTyp.KEYWORD:
self._processFragments(par, S_META, tText, tFormat) self._processFragments(par, S_META, tText, tFormat)
return
def closeDocument(self) -> None: def closeDocument(self) -> None:
"""Generate all the XML.""" """Generate all the XML."""
self._coreXml() self._coreXml()
@@ -322,8 +318,6 @@ class ToDocX(Tokenizer):
if self._usedNotes: if self._usedNotes:
self._footnotesXml() self._footnotesXml()
return
def saveDocument(self, path: Path) -> None: def saveDocument(self, path: Path) -> None:
"""Save the data to a .docx file.""" """Save the data to a .docx file."""
# Content Lists # Content Lists
@@ -373,8 +367,6 @@ class ToDocX(Tokenizer):
xmlToZip(f"{rel.path}/{name}", rel.xml, outZip) xmlToZip(f"{rel.path}/{name}", rel.xml, outZip)
xmlToZip("[Content_Types].xml", dTypes, outZip) xmlToZip("[Content_Types].xml", dTypes, outZip)
return
## ##
# Internal Functions # Internal Functions
## ##
@@ -454,8 +446,6 @@ class ToDocX(Tokenizer):
if temp := text[fStart:]: if temp := text[fStart:]:
par.addContent(self._textRunToXml(temp, xFmt, fClass, fLink)) par.addContent(self._textRunToXml(temp, xFmt, fClass, fLink))
return
def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element: def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element:
"""Encode the text run into XML.""" """Encode the text run into XML."""
xR = xmlElement(_wTag("r")) xR = xmlElement(_wTag("r"))
@@ -668,8 +658,6 @@ class ToDocX(Tokenizer):
for style in styles: for style in styles:
self._styles[style.styleId] = style self._styles[style.styleId] = style
return
def _nextRelId(self) -> str: def _nextRelId(self) -> str:
"""Generate the next unique rId.""" """Generate the next unique rId."""
return f"rId{len(self._rels) + 1}" return f"rId{len(self._rels) + 1}"
@@ -1054,6 +1042,10 @@ class ToDocX(Tokenizer):
class DocXParagraph: class DocXParagraph:
"""DocX Text Paragraph.
This class holds a single paragraph of a DocX document.
"""
__slots__ = ( __slots__ = (
"_bottomMargin", "_breakAfter", "_breakBefore", "_content", "_bottomMargin", "_breakAfter", "_breakBefore", "_content",
@@ -1073,7 +1065,6 @@ class DocXParagraph:
self._breakBefore = False self._breakBefore = False
self._breakAfter = False self._breakAfter = False
self._footnoteRef = False self._footnoteRef = False
return
## ##
# Properties # Properties
@@ -1091,53 +1082,43 @@ class DocXParagraph:
def setStyle(self, style: DocXParStyle | None) -> None: def setStyle(self, style: DocXParStyle | None) -> None:
"""Set the paragraph style.""" """Set the paragraph style."""
self._style = style self._style = style
return
def setAlignment(self, value: str) -> None: def setAlignment(self, value: str) -> None:
"""Set paragraph alignment.""" """Set paragraph alignment."""
if value in ("left", "center", "right", "both"): if value in ("left", "center", "right", "both"):
self._textAlign = value self._textAlign = value
return
def setMarginTop(self, value: float) -> None: def setMarginTop(self, value: float) -> None:
"""Set margin above in pt.""" """Set margin above in pt."""
self._topMargin = value self._topMargin = value
return
def setMarginBottom(self, value: float) -> None: def setMarginBottom(self, value: float) -> None:
"""Set margin below in pt.""" """Set margin below in pt."""
self._bottomMargin = value self._bottomMargin = value
return
def setMarginLeft(self, value: float) -> None: def setMarginLeft(self, value: float) -> None:
"""Set margin left in pt.""" """Set margin left in pt."""
self._leftMargin = value self._leftMargin = value
return
def setMarginRight(self, value: float) -> None: def setMarginRight(self, value: float) -> None:
"""Set margin right in pt.""" """Set margin right in pt."""
self._rightMargin = value self._rightMargin = value
return
def setIndentFirst(self, state: bool) -> None: def setIndentFirst(self, state: bool) -> None:
"""Set first line indent.""" """Set first line indent."""
self._indentFirst = state self._indentFirst = state
return
def setPageBreakBefore(self, state: bool) -> None: def setPageBreakBefore(self, state: bool) -> None:
"""Set page break before flag.""" """Set page break before flag."""
self._breakBefore = state self._breakBefore = state
return
def setPageBreakAfter(self, state: bool) -> None: def setPageBreakAfter(self, state: bool) -> None:
"""Set page break after flag.""" """Set page break after flag."""
self._breakAfter = state self._breakAfter = state
return
def setIsFootnote(self, state: bool) -> None: def setIsFootnote(self, state: bool) -> None:
"""Set is footnote flag.""" """Set is footnote flag."""
self._footnoteRef = state self._footnoteRef = state
return
## ##
# Methods # Methods
@@ -1146,10 +1127,9 @@ class DocXParagraph:
def addContent(self, run: ET.Element) -> None: def addContent(self, run: ET.Element) -> None:
"""Add a run segment to the paragraph.""" """Add a run segment to the paragraph."""
self._content.append(run) self._content.append(run)
return
def toXml(self, body: ET.Element) -> None: def toXml(self, body: ET.Element) -> None:
"""Called after all content is set.""" """Generate the XML. Call after all content is set."""
if style := self._style: if style := self._style:
xP = xmlSubElem(body, _wTag("p")) xP = xmlSubElem(body, _wTag("p"))
@@ -1191,5 +1171,3 @@ class DocXParagraph:
if self._breakAfter: if self._breakAfter:
xR = xmlSubElem(xP, _wTag("r")) xR = xmlSubElem(xP, _wTag("r"))
xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"}) xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"})
return
+2 -13
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -77,7 +77,7 @@ HTML_NONE = (0, "")
class ToHtml(Tokenizer): class ToHtml(Tokenizer):
"""Core: HTML Document Writer """Core: HTML Document Writer.
Extend the Tokenizer class to writer HTML output. This class is Extend the Tokenizer class to writer HTML output. This class is
also used by the Document Viewer, and Manuscript Build Preview. also used by the Document Viewer, and Manuscript Build Preview.
@@ -90,7 +90,6 @@ class ToHtml(Tokenizer):
self._usedNotes: dict[str, int] = {} self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = [] self._usedFields: list[tuple[int, str]] = []
self.setReplaceUnicode(False) self.setReplaceUnicode(False)
return
## ##
# Setters # Setters
@@ -101,7 +100,6 @@ class ToHtml(Tokenizer):
class tags. class tags.
""" """
self._cssStyles = cssStyles self._cssStyles = cssStyles
return
def setReplaceUnicode(self, doReplace: bool) -> None: def setReplaceUnicode(self, doReplace: bool) -> None:
"""Set the translation map to either minimal or full unicode for """Set the translation map to either minimal or full unicode for
@@ -114,7 +112,6 @@ class ToHtml(Tokenizer):
if doReplace: if doReplace:
# Extend to all relevant Unicode characters # Extend to all relevant Unicode characters
self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H)) self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
return
## ##
# Class Methods # Class Methods
@@ -130,7 +127,6 @@ class ToHtml(Tokenizer):
""" """
super().doPreProcessing() super().doPreProcessing()
self._text = self._text.translate(self._trMap) self._text = self._text.translate(self._trMap)
return
def doConvert(self) -> None: def doConvert(self) -> None:
"""Convert the list of text tokens into an HTML document.""" """Convert the list of text tokens into an HTML document."""
@@ -237,8 +233,6 @@ class ToHtml(Tokenizer):
self._pages.append("".join(lines)) self._pages.append("".join(lines))
return
def closeDocument(self) -> None: def closeDocument(self) -> None:
"""Run close document tasks.""" """Run close document tasks."""
# Replace fields if there are stats available # Replace fields if there are stats available
@@ -265,8 +259,6 @@ class ToHtml(Tokenizer):
self._pages.append("".join(lines)) self._pages.append("".join(lines))
return
def saveDocument(self, path: Path) -> None: def saveDocument(self, path: Path) -> None:
"""Save the data to an HTML file.""" """Save the data to an HTML file."""
if path.suffix.lower() == ".json": if path.suffix.lower() == ".json":
@@ -309,14 +301,11 @@ class ToHtml(Tokenizer):
logger.info("Wrote file: %s", path) logger.info("Wrote file: %s", path)
return
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = "&nbsp;") -> None: def replaceTabs(self, nSpaces: int = 8, spaceChar: str = "&nbsp;") -> None:
"""Replace tabs with spaces in the html.""" """Replace tabs with spaces in the html."""
tabSpace = spaceChar*nSpaces tabSpace = spaceChar*nSpaces
pages = [aLine.replace("\t", tabSpace) for aLine in self._pages] pages = [aLine.replace("\t", tabSpace) for aLine in self._pages]
self._pages = pages self._pages = pages
return
def getStyleSheet(self) -> list[str]: def getStyleSheet(self) -> list[str]:
"""Generate a stylesheet for the current settings.""" """Generate a stylesheet for the current settings."""
+11 -55
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -55,6 +55,7 @@ logger = logging.getLogger(__name__)
class ComStyle(NamedTuple): class ComStyle(NamedTuple):
"""Comment style info."""
label: str = "" label: str = ""
labelClass: str = "" labelClass: str = ""
@@ -92,7 +93,7 @@ B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE)
class Tokenizer(ABC): class Tokenizer(ABC):
"""Core: Text Tokenizer Abstract Base Class """Core: Text Tokenizer Abstract Base Class.
This is the base class for all document build classes. It parses the This is the base class for all document build classes. It parses the
novelWriter markup format and generates a registry of tokens and novelWriter markup format and generates a registry of tokens and
@@ -224,8 +225,6 @@ class Tokenizer(ABC):
self._dialogParser = DialogParser() self._dialogParser = DialogParser()
self._dialogParser.initParser() self._dialogParser.initParser()
return
## ##
# Properties # Properties
## ##
@@ -253,94 +252,78 @@ class Tokenizer(ABC):
"""Set language for the document.""" """Set language for the document."""
if language: if language:
self._dLocale = QLocale(language) self._dLocale = QLocale(language)
return
def setTheme(self, theme: TextDocumentTheme) -> None: def setTheme(self, theme: TextDocumentTheme) -> None:
"""Set the document colour theme.""" """Set the document colour theme."""
self._theme = theme self._theme = theme
return
def setPartitionFormat(self, hFormat: str, hide: bool = False) -> None: def setPartitionFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the partition format pattern.""" """Set the partition format pattern."""
self._fmtPart = hFormat.strip() self._fmtPart = hFormat.strip()
self._hidePart = hide self._hidePart = hide
return
def setChapterFormat(self, hFormat: str, hide: bool = False) -> None: def setChapterFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the chapter format pattern.""" """Set the chapter format pattern."""
self._fmtChapter = hFormat.strip() self._fmtChapter = hFormat.strip()
self._hideChapter = hide self._hideChapter = hide
return
def setUnNumberedFormat(self, hFormat: str, hide: bool = False) -> None: def setUnNumberedFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the unnumbered format pattern.""" """Set the unnumbered format pattern."""
self._fmtUnNum = hFormat.strip() self._fmtUnNum = hFormat.strip()
self._hideUnNum = hide self._hideUnNum = hide
return
def setSceneFormat(self, hFormat: str, hide: bool = False) -> None: def setSceneFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the scene format pattern and hidden status.""" """Set the scene format pattern and hidden status."""
self._fmtScene = hFormat.strip() self._fmtScene = hFormat.strip()
self._hideScene = hide self._hideScene = hide
return
def setHardSceneFormat(self, hFormat: str, hide: bool = False) -> None: def setHardSceneFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the hard scene format pattern and hidden status.""" """Set the hard scene format pattern and hidden status."""
self._fmtHScene = hFormat.strip() self._fmtHScene = hFormat.strip()
self._hideHScene = hide self._hideHScene = hide
return
def setSectionFormat(self, hFormat: str, hide: bool = False) -> None: def setSectionFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the section format pattern and hidden status.""" """Set the section format pattern and hidden status."""
self._fmtSection = hFormat.strip() self._fmtSection = hFormat.strip()
self._hideSection = hide self._hideSection = hide
return
def setTitleStyle(self, center: bool, pageBreak: bool) -> None: def setTitleStyle(self, center: bool, pageBreak: bool) -> None:
"""Set the title heading style.""" """Set the title heading style."""
self._titleStyle = BlockFmt.CENTRE if center else BlockFmt.NONE self._titleStyle = BlockFmt.CENTRE if center else BlockFmt.NONE
self._titleStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE self._titleStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE
return
def setPartitionStyle(self, center: bool, pageBreak: bool) -> None: def setPartitionStyle(self, center: bool, pageBreak: bool) -> None:
"""Set the partition heading style.""" """Set the partition heading style."""
self._partStyle = BlockFmt.CENTRE if center else BlockFmt.NONE self._partStyle = BlockFmt.CENTRE if center else BlockFmt.NONE
self._partStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE self._partStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE
return
def setChapterStyle(self, center: bool, pageBreak: bool) -> None: def setChapterStyle(self, center: bool, pageBreak: bool) -> None:
"""Set the chapter heading style.""" """Set the chapter heading style."""
self._chapterStyle = BlockFmt.CENTRE if center else BlockFmt.NONE self._chapterStyle = BlockFmt.CENTRE if center else BlockFmt.NONE
self._chapterStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE self._chapterStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE
return
def setSceneStyle(self, center: bool, pageBreak: bool) -> None: def setSceneStyle(self, center: bool, pageBreak: bool) -> None:
"""Set the scene heading style.""" """Set the scene heading style."""
self._sceneStyle = BlockFmt.CENTRE if center else BlockFmt.NONE self._sceneStyle = BlockFmt.CENTRE if center else BlockFmt.NONE
self._sceneStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE self._sceneStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE
return
def setTextFont(self, font: QFont) -> None: def setTextFont(self, font: QFont) -> None:
"""Set the build font.""" """Set the build font."""
self._textFont = fontMatcher(font) self._textFont = fontMatcher(font)
return
def setLineHeight(self, height: float) -> None: def setLineHeight(self, height: float) -> None:
"""Set the line height between 0.5 and 5.0.""" """Set the line height between 0.5 and 5.0."""
self._lineHeight = min(max(float(height), 0.5), 5.0) self._lineHeight = min(max(float(height), 0.5), 5.0)
return
def setHeadingStyles(self, color: bool, scale: bool, bold: bool) -> None: def setHeadingStyles(self, color: bool, scale: bool, bold: bool) -> None:
"""Set text style for headings.""" """Set text style for headings."""
self._colorHeads = color self._colorHeads = color
self._scaleHeads = scale self._scaleHeads = scale
self._boldHeads = bold self._boldHeads = bold
return
def setBlockIndent(self, indent: float) -> None: def setBlockIndent(self, indent: float) -> None:
"""Set the block indent between 0.0 and 10.0.""" """Set the block indent between 0.0 and 10.0."""
self._blockIndent = min(max(float(indent), 0.0), 10.0) self._blockIndent = min(max(float(indent), 0.0), 10.0)
return
def setFirstLineIndent(self, state: bool, indent: float, first: bool) -> None: def setFirstLineIndent(self, state: bool, indent: float, first: bool) -> None:
"""Set first line indent and whether to also indent first """Set first line indent and whether to also indent first
@@ -349,67 +332,54 @@ class Tokenizer(ABC):
self._firstIndent = state self._firstIndent = state
self._firstWidth = indent self._firstWidth = indent
self._indentFirst = first self._indentFirst = first
return
def setJustify(self, state: bool) -> None: def setJustify(self, state: bool) -> None:
"""Enable or disable text justification.""" """Enable or disable text justification."""
self._doJustify = state self._doJustify = state
return
def setDialogHighlight(self, state: bool) -> None: def setDialogHighlight(self, state: bool) -> None:
"""Enable or disable dialogue highlighting.""" """Enable or disable dialogue highlighting."""
self._hlightDialog = state self._hlightDialog = state
return
def setTitleMargins(self, upper: float, lower: float) -> None: def setTitleMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower title margin.""" """Set the upper and lower title margin."""
self._marginTitle = (float(upper), float(lower)) self._marginTitle = (float(upper), float(lower))
return
def setHead1Margins(self, upper: float, lower: float) -> None: def setHead1Margins(self, upper: float, lower: float) -> None:
"""Set the upper and lower heading 1 margin.""" """Set the upper and lower heading 1 margin."""
self._marginHead1 = (float(upper), float(lower)) self._marginHead1 = (float(upper), float(lower))
return
def setHead2Margins(self, upper: float, lower: float) -> None: def setHead2Margins(self, upper: float, lower: float) -> None:
"""Set the upper and lower heading 2 margin.""" """Set the upper and lower heading 2 margin."""
self._marginHead2 = (float(upper), float(lower)) self._marginHead2 = (float(upper), float(lower))
return
def setHead3Margins(self, upper: float, lower: float) -> None: def setHead3Margins(self, upper: float, lower: float) -> None:
"""Set the upper and lower heading 3 margin.""" """Set the upper and lower heading 3 margin."""
self._marginHead3 = (float(upper), float(lower)) self._marginHead3 = (float(upper), float(lower))
return
def setHead4Margins(self, upper: float, lower: float) -> None: def setHead4Margins(self, upper: float, lower: float) -> None:
"""Set the upper and lower heading 4 margin.""" """Set the upper and lower heading 4 margin."""
self._marginHead4 = (float(upper), float(lower)) self._marginHead4 = (float(upper), float(lower))
return
def setTextMargins(self, upper: float, lower: float) -> None: def setTextMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower text margin.""" """Set the upper and lower text margin."""
self._marginText = (float(upper), float(lower)) self._marginText = (float(upper), float(lower))
return
def setMetaMargins(self, upper: float, lower: float) -> None: def setMetaMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower meta text margin.""" """Set the upper and lower meta text margin."""
self._marginMeta = (float(upper), float(lower)) self._marginMeta = (float(upper), float(lower))
return
def setSeparatorMargins(self, upper: float, lower: float) -> None: def setSeparatorMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower meta text margin.""" """Set the upper and lower meta text margin."""
self._marginSep = (float(upper), float(lower)) self._marginSep = (float(upper), float(lower))
return
def setLinkHeadings(self, state: bool) -> None: def setLinkHeadings(self, state: bool) -> None:
"""Enable or disable adding an anchor before headings.""" """Enable or disable adding an anchor before headings."""
self._linkHeadings = state self._linkHeadings = state
return
def setBodyText(self, state: bool) -> None: def setBodyText(self, state: bool) -> None:
"""Include body text in build.""" """Include body text in build."""
self._doBodyText = state self._doBodyText = state
return
def setCommentType(self, comment: nwComment, state: bool) -> None: def setCommentType(self, comment: nwComment, state: bool) -> None:
"""Toggle the inclusion og certain comment types.""" """Toggle the inclusion og certain comment types."""
@@ -417,22 +387,18 @@ class Tokenizer(ABC):
self._doComments.add(comment) self._doComments.add(comment)
else: else:
self._doComments.discard(comment) self._doComments.discard(comment)
return
def setKeywords(self, state: bool) -> None: def setKeywords(self, state: bool) -> None:
"""Include keywords in build.""" """Include keywords in build."""
self._doKeywords = state self._doKeywords = state
return
def setIgnoredKeywords(self, keywords: str) -> None: def setIgnoredKeywords(self, keywords: str) -> None:
"""Comma separated string of keywords to ignore.""" """Comma separated string of keywords to ignore."""
self._skipKeywords = set(x.lower().strip() for x in keywords.split(",")) self._skipKeywords = set(x.lower().strip() for x in keywords.split(","))
return
def setKeepLineBreaks(self, state: bool) -> None: def setKeepLineBreaks(self, state: bool) -> None:
"""Keep line breaks in paragraphs.""" """Keep line breaks in paragraphs."""
self._keepBreaks = state self._keepBreaks = state
return
## ##
# Class Methods # Class Methods
@@ -460,12 +426,10 @@ class Tokenizer(ABC):
self._classes["tag"] = self._theme.tag self._classes["tag"] = self._theme.tag
self._classes["keyword"] = self._theme.keyword self._classes["keyword"] = self._theme.keyword
self._classes["optional"] = self._theme.optional self._classes["optional"] = self._theme.optional
return
def setBreakNext(self) -> None: def setBreakNext(self) -> None:
"""Set a page break for next block.""" """Set a page break for next block."""
self._breakNext = True self._breakNext = True
return
def addRootHeading(self, tHandle: str) -> None: def addRootHeading(self, tHandle: str) -> None:
"""Add a heading at the start of a new root folder.""" """Add a heading at the start of a new root folder."""
@@ -491,8 +455,6 @@ class Tokenizer(ABC):
if self._keepRaw: if self._keepRaw:
self._raw.append(f"#! {title}\n\n") self._raw.append(f"#! {title}\n\n")
return
def setText(self, tHandle: str, text: str | None = None) -> None: def setText(self, tHandle: str, text: str | None = None) -> None:
"""Set the text for the tokenizer from a handle. If text is not """Set the text for the tokenizer from a handle. If text is not
set, it's is loaded from the file. set, it's is loaded from the file.
@@ -503,7 +465,6 @@ class Tokenizer(ABC):
self._text = text or self._project.storage.getDocumentText(tHandle) self._text = text or self._project.storage.getDocumentText(tHandle)
self._handle = tHandle self._handle = tHandle
self._isNovel = nwItem.itemLayout == nwItemLayout.DOCUMENT self._isNovel = nwItem.itemLayout == nwItemLayout.DOCUMENT
return
def doPreProcessing(self) -> None: def doPreProcessing(self) -> None:
"""Run pre-processing jobs before the text is tokenized.""" """Run pre-processing jobs before the text is tokenized."""
@@ -512,7 +473,6 @@ class Tokenizer(ABC):
replace = {f"<{k}>": v for k, v in entry.items()} replace = {f"<{k}>": v for k, v in entry.items()}
rxRep = re.compile("|".join([re.escape(k) for k in replace]), flags=re.DOTALL) rxRep = re.compile("|".join([re.escape(k) for k in replace]), flags=re.DOTALL)
self._text = rxRep.sub(lambda x: replace[x.group(0)], self._text) self._text = rxRep.sub(lambda x: replace[x.group(0)], self._text)
return
def tokenizeText(self) -> None: def tokenizeText(self) -> None:
"""Scan the text for either lines starting with specific """Scan the text for either lines starting with specific
@@ -590,13 +550,13 @@ class Tokenizer(ABC):
self._breakNext = True self._breakNext = True
continue continue
elif sLine == "[vspace]": if sLine == "[vspace]":
tBlocks.append( tBlocks.append(
(BlockTyp.SKIP, "", "", [], tStyle) (BlockTyp.SKIP, "", "", [], tStyle)
) )
continue continue
elif sLine.startswith("[vspace:") and sLine.endswith("]"): if sLine.startswith("[vspace:") and sLine.endswith("]"):
nSkip = checkInt(sLine[8:-1], 0) nSkip = checkInt(sLine[8:-1], 0)
if nSkip >= 1: if nSkip >= 1:
tBlocks.append( tBlocks.append(
@@ -962,8 +922,6 @@ class Tokenizer(ABC):
text = tText.replace(nwHeadFmt.BR, " ").replace("&amp;", "&") text = tText.replace(nwHeadFmt.BR, " ").replace("&amp;", "&")
self._outline[tKey] = f"{prefix}|{text}" self._outline[tKey] = f"{prefix}|{text}"
return
def countStats(self) -> None: def countStats(self) -> None:
"""Count stats on the tokenized text.""" """Count stats on the tokenized text."""
titleCount = self._counts.get(nwStats.TITLES, 0) titleCount = self._counts.get(nwStats.TITLES, 0)
@@ -1039,8 +997,6 @@ class Tokenizer(ABC):
self._counts[nwStats.WCHARS_TEXT] = textWordChars self._counts[nwStats.WCHARS_TEXT] = textWordChars
self._counts[nwStats.WCHARS_TITLE] = titleWordChars self._counts[nwStats.WCHARS_TITLE] = titleWordChars
return
## ##
# Internal Functions # Internal Functions
## ##
@@ -1182,6 +1138,12 @@ class Tokenizer(ABC):
class HeadingFormatter: class HeadingFormatter:
"""Core: Format Text Headings.
This class holds the various chapter and scene counters and can
apply the Build Settings header format settings based on internal
counter state.
"""
def __init__( def __init__(
self, self,
@@ -1195,35 +1157,29 @@ class HeadingFormatter:
self._chapter = chapter self._chapter = chapter
self._scene = scene self._scene = scene
self._absolute = absolute self._absolute = absolute
return
def setHandle(self, tHandle: str | None) -> None: def setHandle(self, tHandle: str | None) -> None:
"""Set the handle currently being processed.""" """Set the handle currently being processed."""
self._handle = tHandle self._handle = tHandle
return
def incChapter(self) -> None: def incChapter(self) -> None:
"""Increment the chapter counter.""" """Increment the chapter counter."""
self._chapter += 1 self._chapter += 1
return
def incScene(self) -> None: def incScene(self) -> None:
"""Increment the scene counters.""" """Increment the scene counters."""
self._scene += 1 self._scene += 1
self._absolute += 1 self._absolute += 1
return
def resetAll(self) -> None: def resetAll(self) -> None:
"""Reset all counters.""" """Reset all counters."""
self._chapter = 0 self._chapter = 0
self._scene = 0 self._scene = 0
self._absolute = 0 self._absolute = 0
return
def resetScene(self) -> None: def resetScene(self) -> None:
"""Reset the chapter scene counter.""" """Reset the chapter scene counter."""
self._scene = 0 self._scene = 0
return
def apply(self, hFormat: str, text: str, nHead: int) -> str: def apply(self, hFormat: str, text: str, nHead: int) -> str:
"""Apply formatting to a specific heading.""" """Apply formatting to a specific heading."""
+2 -9
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -79,7 +79,7 @@ EXT_MD = {
class ToMarkdown(Tokenizer): class ToMarkdown(Tokenizer):
"""Core: Markdown Document Writer """Core: Markdown Document Writer.
Extend the Tokenizer class to writer Markdown output. It supports Extend the Tokenizer class to writer Markdown output. It supports
both Standard Markdown and Extended Markdown. The class also both Standard Markdown and Extended Markdown. The class also
@@ -91,7 +91,6 @@ class ToMarkdown(Tokenizer):
self._extended = extended self._extended = extended
self._usedNotes: dict[str, int] = {} self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = [] self._usedFields: list[tuple[int, str]] = []
return
## ##
# Class Methods # Class Methods
@@ -153,8 +152,6 @@ class ToMarkdown(Tokenizer):
self._pages.append("".join(lines)) self._pages.append("".join(lines))
return
def closeDocument(self) -> None: def closeDocument(self) -> None:
"""Run close document tasks.""" """Run close document tasks."""
# Replace fields if there are stats available # Replace fields if there are stats available
@@ -181,20 +178,16 @@ class ToMarkdown(Tokenizer):
lines.append("\n") lines.append("\n")
self._pages.append("".join(lines)) self._pages.append("".join(lines))
return
def saveDocument(self, path: Path) -> None: def saveDocument(self, path: Path) -> None:
"""Save the data to a plain text file.""" """Save the data to a plain text file."""
with open(path, mode="w", encoding="utf-8") as outFile: with open(path, mode="w", encoding="utf-8") as outFile:
outFile.write("".join(self._pages)) outFile.write("".join(self._pages))
logger.info("Wrote file: %s", path) logger.info("Wrote file: %s", path)
return
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None: def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None:
"""Replace tabs with spaces.""" """Replace tabs with spaces."""
spaces = spaceChar*nSpaces spaces = spaceChar*nSpaces
self._pages = [p.replace("\t", spaces) for p in self._pages] self._pages = [p.replace("\t", spaces) for p in self._pages]
return
## ##
# Internal Functions # Internal Functions
+10 -69
View File
@@ -23,7 +23,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -128,7 +128,7 @@ FONT_WEIGHT_MAP = {"400": "normal", "700": "bold"}
class ToOdt(Tokenizer): class ToOdt(Tokenizer):
"""Core: Open Document Writer """Core: Open Document Writer.
Extend the Tokenizer class to writer Open Document files. The output Extend the Tokenizer class to writer Open Document files. The output
should conform to the 1.3 Extended standard. should conform to the 1.3 Extended standard.
@@ -189,8 +189,6 @@ class ToOdt(Tokenizer):
self._mDocLeft = "2.000cm" self._mDocLeft = "2.000cm"
self._mDocRight = "2.000cm" self._mDocRight = "2.000cm"
return
## ##
# Setters # Setters
## ##
@@ -205,20 +203,18 @@ class ToOdt(Tokenizer):
self._mDocBtm = f"{bottom/10.0:.3f}cm" self._mDocBtm = f"{bottom/10.0:.3f}cm"
self._mDocLeft = f"{left/10.0:.3f}cm" self._mDocLeft = f"{left/10.0:.3f}cm"
self._mDocRight = f"{right/10.0:.3f}cm" self._mDocRight = f"{right/10.0:.3f}cm"
return
def setHeaderFormat(self, value: str, offset: int) -> None: def setHeaderFormat(self, value: str, offset: int) -> None:
"""Set the document header format.""" """Set the document header format."""
self._headerFormat = value.strip() self._headerFormat = value.strip()
self._pageOffset = offset self._pageOffset = offset
return
## ##
# Class Methods # Class Methods
## ##
def initDocument(self) -> None: def initDocument(self) -> None:
"""Initialises a new open document XML tree.""" """Initialise a new open document XML tree."""
super().initDocument() super().initDocument()
# Initialise Variables # Initialise Variables
@@ -325,8 +321,6 @@ class ToOdt(Tokenizer):
self._useableStyles() self._useableStyles()
self._writeHeader() self._writeHeader()
return
def doConvert(self) -> None: def doConvert(self) -> None:
"""Convert the list of text tokens into XML elements.""" """Convert the list of text tokens into XML elements."""
xText = self._xText xText = self._xText
@@ -395,8 +389,6 @@ class ToOdt(Tokenizer):
elif tType == BlockTyp.KEYWORD: elif tType == BlockTyp.KEYWORD:
self._addTextPar(xText, S_META, oStyle, tText, tFmt=tFormat) self._addTextPar(xText, S_META, oStyle, tText, tFmt=tFormat)
return
def closeDocument(self) -> None: def closeDocument(self) -> None:
"""Add additional collected information to the XML.""" """Add additional collected information to the XML."""
for style in self._autoPara.values(): for style in self._autoPara.values():
@@ -412,7 +404,6 @@ class ToOdt(Tokenizer):
_mkTag("text", "name"): f"Manuscript{key[:1].upper()}{key[1:]}", _mkTag("text", "name"): f"Manuscript{key[:1].upper()}{key[1:]}",
}) })
self._xText.insert(0, xFields) self._xText.insert(0, xFields)
return
def saveDocument(self, path: Path) -> None: def saveDocument(self, path: Path) -> None:
"""Save the data to an .fodt or .odt file.""" """Save the data to an .fodt or .odt file."""
@@ -456,8 +447,6 @@ class ToOdt(Tokenizer):
logger.info("Wrote file: %s", path) logger.info("Wrote file: %s", path)
return
## ##
# Internal Functions # Internal Functions
## ##
@@ -670,7 +659,7 @@ class ToOdt(Tokenizer):
return None return None
def _emToCm(self, value: float) -> str: def _emToCm(self, value: float) -> str:
"""Converts an em value to centimetres.""" """Convert an em value to centimetres."""
return f"{value*self._fontSize*2.54/72.0:.3f}cm" return f"{value*self._fontSize*2.54/72.0:.3f}cm"
def _emToPt(self, scale: float) -> str: def _emToPt(self, scale: float) -> str:
@@ -705,8 +694,6 @@ class ToOdt(Tokenizer):
_mkTag("fo", "margin-bottom"): self._emToCm(0.5), _mkTag("fo", "margin-bottom"): self._emToCm(0.5),
}) })
return
def _defaultStyles(self) -> None: def _defaultStyles(self) -> None:
"""Set the default styles.""" """Set the default styles."""
hScale = self._scaleHeads hScale = self._scaleHeads
@@ -783,8 +770,6 @@ class ToOdt(Tokenizer):
_mkTag("number", "min-integer-digits"): "1", _mkTag("number", "min-integer-digits"): "1",
}) })
return
def _useableStyles(self) -> None: def _useableStyles(self) -> None:
"""Set the usable styles.""" """Set the usable styles."""
hScale = self._scaleHeads hScale = self._scaleHeads
@@ -954,8 +939,6 @@ class ToOdt(Tokenizer):
style.packXML(self._xStyl) style.packXML(self._xStyl)
self._mainPara[style.name] = style self._mainPara[style.name] = style
return
def _writeHeader(self) -> None: def _writeHeader(self) -> None:
"""Write the header elements.""" """Write the header elements."""
xPage = ET.SubElement(self._xMast, _mkTag("style", "master-page"), attrib={ xPage = ET.SubElement(self._xMast, _mkTag("style", "master-page"), attrib={
@@ -993,8 +976,6 @@ class ToOdt(Tokenizer):
_mkTag("text", "style-name"): "Header" _mkTag("text", "style-name"): "Header"
}) })
return
# Auto-Style Classes # Auto-Style Classes
# ================== # ==================
@@ -1004,6 +985,7 @@ class ODTParagraphStyle:
exporter. Only the used settings are exposed here to keep the class exporter. Only the used settings are exposed here to keep the class
minimal and fast. minimal and fast.
""" """
VALID_ALIGN: Final[list[str]] = ["start", "center", "end", "justify", "left", "right"] VALID_ALIGN: Final[list[str]] = ["start", "center", "end", "justify", "left", "right"]
VALID_BREAK: Final[list[str]] = ["auto", "page", "even-page", "odd-page", "inherit"] VALID_BREAK: Final[list[str]] = ["auto", "page", "even-page", "odd-page", "inherit"]
VALID_LEVEL: Final[list[str]] = ["1", "2", "3", "4"] VALID_LEVEL: Final[list[str]] = ["1", "2", "3", "4"]
@@ -1046,8 +1028,6 @@ class ODTParagraphStyle:
"opacity": ["loext", None], "opacity": ["loext", None],
} }
return
@property @property
def name(self) -> str: def name(self) -> str:
return self._name return self._name
@@ -1059,7 +1039,6 @@ class ODTParagraphStyle:
def setName(self, name: str) -> None: def setName(self, name: str) -> None:
"""Set the paragraph style name.""" """Set the paragraph style name."""
self._name = name self._name = name
return
## ##
# Attribute Setters # Attribute Setters
@@ -1068,17 +1047,14 @@ class ODTParagraphStyle:
def setDisplayName(self, value: str | None) -> None: def setDisplayName(self, value: str | None) -> None:
"""Set style display name.""" """Set style display name."""
self._mAttr["display-name"][1] = value self._mAttr["display-name"][1] = value
return
def setParentStyleName(self, value: str | None) -> None: def setParentStyleName(self, value: str | None) -> None:
"""Set parent style name.""" """Set parent style name."""
self._mAttr["parent-style-name"][1] = value self._mAttr["parent-style-name"][1] = value
return
def setNextStyleName(self, value: str | None) -> None: def setNextStyleName(self, value: str | None) -> None:
"""Set next style name.""" """Set next style name."""
self._mAttr["next-style-name"][1] = value self._mAttr["next-style-name"][1] = value
return
def setOutlineLevel(self, value: str | None) -> None: def setOutlineLevel(self, value: str | None) -> None:
"""Set paragraph outline level.""" """Set paragraph outline level."""
@@ -1086,7 +1062,6 @@ class ODTParagraphStyle:
self._mAttr["default-outline-level"][1] = value self._mAttr["default-outline-level"][1] = value
else: else:
self._mAttr["default-outline-level"][1] = None self._mAttr["default-outline-level"][1] = None
return
def setClass(self, value: str | None) -> None: def setClass(self, value: str | None) -> None:
"""Set paragraph class.""" """Set paragraph class."""
@@ -1094,7 +1069,6 @@ class ODTParagraphStyle:
self._mAttr["class"][1] = value self._mAttr["class"][1] = value
else: else:
self._mAttr["class"][1] = None self._mAttr["class"][1] = None
return
## ##
# Paragraph Setters # Paragraph Setters
@@ -1103,32 +1077,26 @@ class ODTParagraphStyle:
def setMarginTop(self, value: str | None) -> None: def setMarginTop(self, value: str | None) -> None:
"""Set paragraph top margin.""" """Set paragraph top margin."""
self._pAttr["margin-top"][1] = value self._pAttr["margin-top"][1] = value
return
def setMarginBottom(self, value: str | None) -> None: def setMarginBottom(self, value: str | None) -> None:
"""Set paragraph bottom margin.""" """Set paragraph bottom margin."""
self._pAttr["margin-bottom"][1] = value self._pAttr["margin-bottom"][1] = value
return
def setMarginLeft(self, value: str | None) -> None: def setMarginLeft(self, value: str | None) -> None:
"""Set paragraph left margin.""" """Set paragraph left margin."""
self._pAttr["margin-left"][1] = value self._pAttr["margin-left"][1] = value
return
def setMarginRight(self, value: str | None) -> None: def setMarginRight(self, value: str | None) -> None:
"""Set paragraph right margin.""" """Set paragraph right margin."""
self._pAttr["margin-right"][1] = value self._pAttr["margin-right"][1] = value
return
def setTextIndent(self, value: str | None) -> None: def setTextIndent(self, value: str | None) -> None:
"""Set text indentation.""" """Set text indentation."""
self._pAttr["text-indent"][1] = value self._pAttr["text-indent"][1] = value
return
def setLineHeight(self, value: str | None) -> None: def setLineHeight(self, value: str | None) -> None:
"""Set line height.""" """Set line height."""
self._pAttr["line-height"][1] = value self._pAttr["line-height"][1] = value
return
def setTextAlign(self, value: str | None) -> None: def setTextAlign(self, value: str | None) -> None:
"""Set paragraph text alignment.""" """Set paragraph text alignment."""
@@ -1136,7 +1104,6 @@ class ODTParagraphStyle:
self._pAttr["text-align"][1] = value self._pAttr["text-align"][1] = value
else: else:
self._pAttr["text-align"][1] = None self._pAttr["text-align"][1] = None
return
def setBreakBefore(self, value: str | None) -> None: def setBreakBefore(self, value: str | None) -> None:
"""Set page break before policy.""" """Set page break before policy."""
@@ -1144,7 +1111,6 @@ class ODTParagraphStyle:
self._pAttr["break-before"][1] = value self._pAttr["break-before"][1] = value
else: else:
self._pAttr["break-before"][1] = None self._pAttr["break-before"][1] = None
return
def setBreakAfter(self, value: str | None) -> None: def setBreakAfter(self, value: str | None) -> None:
"""Set page break after policy.""" """Set page break after policy."""
@@ -1152,7 +1118,6 @@ class ODTParagraphStyle:
self._pAttr["break-after"][1] = value self._pAttr["break-after"][1] = value
else: else:
self._pAttr["break-after"][1] = None self._pAttr["break-after"][1] = None
return
## ##
# Text Setters # Text Setters
@@ -1161,17 +1126,14 @@ class ODTParagraphStyle:
def setFontName(self, value: str | None) -> None: def setFontName(self, value: str | None) -> None:
"""Set font name.""" """Set font name."""
self._tAttr["font-name"][1] = value self._tAttr["font-name"][1] = value
return
def setFontFamily(self, value: str | None) -> None: def setFontFamily(self, value: str | None) -> None:
"""Set font family.""" """Set font family."""
self._tAttr["font-family"][1] = value self._tAttr["font-family"][1] = value
return
def setFontSize(self, value: str | None) -> None: def setFontSize(self, value: str | None) -> None:
"""Set font size.""" """Set font size."""
self._tAttr["font-size"][1] = value self._tAttr["font-size"][1] = value
return
def setFontWeight(self, value: str | None) -> None: def setFontWeight(self, value: str | None) -> None:
"""Set font weight.""" """Set font weight."""
@@ -1179,7 +1141,6 @@ class ODTParagraphStyle:
self._tAttr["font-weight"][1] = value self._tAttr["font-weight"][1] = value
else: else:
self._tAttr["font-weight"][1] = None self._tAttr["font-weight"][1] = None
return
def setColor(self, value: QColor | None) -> None: def setColor(self, value: QColor | None) -> None:
"""Set text colour.""" """Set text colour."""
@@ -1189,7 +1150,6 @@ class ODTParagraphStyle:
else: else:
self._tAttr["color"][1] = None self._tAttr["color"][1] = None
self._tAttr["opacity"][1] = None self._tAttr["opacity"][1] = None
return
## ##
# Methods # Methods
@@ -1233,14 +1193,13 @@ class ODTParagraphStyle:
if attr := {_mkTag(n, m): v for m, (n, v) in self._tAttr.items() if v}: if attr := {_mkTag(n, m): v for m, (n, v) in self._tAttr.items() if v}:
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr) ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
return
class ODTTextStyle: class ODTTextStyle:
"""Wrapper class for the text style setting used by the exporter. """Wrapper class for the text style setting used by the exporter.
Only the used settings are exposed here to keep the class minimal Only the used settings are exposed here to keep the class minimal
and fast. and fast.
""" """
VALID_WEIGHT: Final[list[str]] = ["normal", "bold", *FONT_WEIGHT_NUM] VALID_WEIGHT: Final[list[str]] = ["normal", "bold", *FONT_WEIGHT_NUM]
VALID_STYLE: Final[list[str]] = ["normal", "italic", "oblique"] VALID_STYLE: Final[list[str]] = ["normal", "italic", "oblique"]
VALID_POS: Final[list[str]] = ["super", "sub"] VALID_POS: Final[list[str]] = ["super", "sub"]
@@ -1263,7 +1222,6 @@ class ODTTextStyle:
"text-underline-width": ["style", None], "text-underline-width": ["style", None],
"text-underline-color": ["style", None], "text-underline-color": ["style", None],
} }
return
@property @property
def name(self) -> str: def name(self) -> str:
@@ -1279,7 +1237,6 @@ class ODTTextStyle:
self._tAttr["font-weight"][1] = value self._tAttr["font-weight"][1] = value
else: else:
self._tAttr["font-weight"][1] = None self._tAttr["font-weight"][1] = None
return
def setFontStyle(self, value: str | None) -> None: def setFontStyle(self, value: str | None) -> None:
"""Set text font style.""" """Set text font style."""
@@ -1287,7 +1244,6 @@ class ODTTextStyle:
self._tAttr["font-style"][1] = value self._tAttr["font-style"][1] = value
else: else:
self._tAttr["font-style"][1] = None self._tAttr["font-style"][1] = None
return
def setColor(self, value: QColor | None) -> None: def setColor(self, value: QColor | None) -> None:
"""Set text colour.""" """Set text colour."""
@@ -1295,7 +1251,6 @@ class ODTTextStyle:
self._tAttr["color"][1] = value.name(QtHexRgb) self._tAttr["color"][1] = value.name(QtHexRgb)
else: else:
self._tAttr["color"][1] = None self._tAttr["color"][1] = None
return
def setBackgroundColor(self, value: QColor | None) -> None: def setBackgroundColor(self, value: QColor | None) -> None:
"""Set text background colour.""" """Set text background colour."""
@@ -1303,7 +1258,6 @@ class ODTTextStyle:
self._tAttr["background-color"][1] = value.name(QtHexRgb) self._tAttr["background-color"][1] = value.name(QtHexRgb)
else: else:
self._tAttr["background-color"][1] = None self._tAttr["background-color"][1] = None
return
def setTextPosition(self, value: str | None) -> None: def setTextPosition(self, value: str | None) -> None:
"""Set text vertical position.""" """Set text vertical position."""
@@ -1311,7 +1265,6 @@ class ODTTextStyle:
self._tAttr["text-position"][1] = f"{value} 58%" self._tAttr["text-position"][1] = f"{value} 58%"
else: else:
self._tAttr["text-position"][1] = None self._tAttr["text-position"][1] = None
return
def setStrikeStyle(self, value: str | None) -> None: def setStrikeStyle(self, value: str | None) -> None:
"""Set text line-trough style.""" """Set text line-trough style."""
@@ -1319,7 +1272,6 @@ class ODTTextStyle:
self._tAttr["text-line-through-style"][1] = value self._tAttr["text-line-through-style"][1] = value
else: else:
self._tAttr["text-line-through-style"][1] = None self._tAttr["text-line-through-style"][1] = None
return
def setStrikeType(self, value: str | None) -> None: def setStrikeType(self, value: str | None) -> None:
"""Set text line-through type.""" """Set text line-through type."""
@@ -1327,7 +1279,6 @@ class ODTTextStyle:
self._tAttr["text-line-through-type"][1] = value self._tAttr["text-line-through-type"][1] = value
else: else:
self._tAttr["text-line-through-type"][1] = None self._tAttr["text-line-through-type"][1] = None
return
def setUnderlineStyle(self, value: str | None) -> None: def setUnderlineStyle(self, value: str | None) -> None:
"""Set text underline style.""" """Set text underline style."""
@@ -1335,7 +1286,6 @@ class ODTTextStyle:
self._tAttr["text-underline-style"][1] = value self._tAttr["text-underline-style"][1] = value
else: else:
self._tAttr["text-underline-style"][1] = None self._tAttr["text-underline-style"][1] = None
return
def setUnderlineWidth(self, value: str | None) -> None: def setUnderlineWidth(self, value: str | None) -> None:
"""Set text underline width.""" """Set text underline width."""
@@ -1343,7 +1293,6 @@ class ODTTextStyle:
self._tAttr["text-underline-width"][1] = value self._tAttr["text-underline-width"][1] = value
else: else:
self._tAttr["text-underline-width"][1] = None self._tAttr["text-underline-width"][1] = None
return
def setUnderlineColor(self, value: str | None) -> None: def setUnderlineColor(self, value: str | None) -> None:
"""Set text underline colour.""" """Set text underline colour."""
@@ -1351,7 +1300,6 @@ class ODTTextStyle:
self._tAttr["text-underline-color"][1] = value self._tAttr["text-underline-color"][1] = value
else: else:
self._tAttr["text-underline-color"][1] = None self._tAttr["text-underline-color"][1] = None
return
## ##
# Methods # Methods
@@ -1365,7 +1313,6 @@ class ODTTextStyle:
}) })
if attr := {_mkTag(n, m): v for m, (n, v) in self._tAttr.items() if v}: if attr := {_mkTag(n, m): v for m, (n, v) in self._tAttr.items() if v}:
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr) ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
return
# XML Complex Element Helper Class # XML Complex Element Helper Class
@@ -1378,7 +1325,9 @@ X_SPAN_SING = 3
class XMLParagraph: class XMLParagraph:
"""This is a helper class to manage the text content of a single """ODT Text Paragraph.
This is a helper class to manage the text content of a single
XML element using mixed content tags. XML element using mixed content tags.
Rules: Rules:
@@ -1408,8 +1357,6 @@ class XMLParagraph:
self._rawTxt = "" self._rawTxt = ""
self._xRoot.text = "" self._xRoot.text = ""
return
def appendText(self, text: str) -> None: def appendText(self, text: str) -> None:
"""Append text to the XML element. We do this one character at """Append text to the XML element. We do this one character at
the time in order to be able to process line breaks, tabs and the time in order to be able to process line breaks, tabs and
@@ -1424,7 +1371,7 @@ class XMLParagraph:
if c == " ": if c == " ":
nSpaces += 1 nSpaces += 1
continue continue
elif nSpaces > 0: if nSpaces > 0:
self._processSpaces(nSpaces) self._processSpaces(nSpaces)
nSpaces = 0 nSpaces = 0
@@ -1468,8 +1415,6 @@ class XMLParagraph:
# Handle trailing spaces # Handle trailing spaces
self._processSpaces(nSpaces) self._processSpaces(nSpaces)
return
def appendSpan(self, text: str, style: str, link: str) -> None: def appendSpan(self, text: str, style: str, link: str) -> None:
"""Append a text span to the XML element. The span is always """Append a text span to the XML element. The span is always
closed since we do not produce nested spans (like Libre Office). closed since we do not produce nested spans (like Libre Office).
@@ -1491,7 +1436,6 @@ class XMLParagraph:
self._nState = X_SPAN_TEXT self._nState = X_SPAN_TEXT
self.appendText(text) self.appendText(text)
self._nState = X_ROOT_TAIL self._nState = X_ROOT_TAIL
return
def appendNode(self, xNode: ET.Element | None) -> None: def appendNode(self, xNode: ET.Element | None) -> None:
"""Append an XML node to the paragraph. We only check for the """Append an XML node to the paragraph. We only check for the
@@ -1504,7 +1448,6 @@ class XMLParagraph:
self._xTail = xNode self._xTail = xNode
self._xTail.tail = "" self._xTail.tail = ""
self._nState = X_ROOT_TAIL self._nState = X_ROOT_TAIL
return
def checkError(self) -> tuple[int, str]: def checkError(self) -> tuple[int, str]:
"""Check that the number of characters written matches the """Check that the number of characters written matches the
@@ -1575,5 +1518,3 @@ class XMLParagraph:
self._xSing.tail = "" self._xSing.tail = ""
self._nState = X_SPAN_SING self._nState = X_SPAN_SING
self._chrPos += nSpaces - 1 self._chrPos += nSpaces - 1
return
+3 -17
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -56,6 +56,7 @@ T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None: def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
"""Insert a new block if not at the beginning of the document."""
if cursor.position() > 0: if cursor.position() > 0:
cursor.insertBlock(bFmt) cursor.insertBlock(bFmt)
else: else:
@@ -63,7 +64,7 @@ def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
class ToQTextDocument(Tokenizer): class ToQTextDocument(Tokenizer):
"""Core: QTextDocument Writer """Core: QTextDocument Writer.
Extend the Tokenizer class to generate a QTextDocument output. This Extend the Tokenizer class to generate a QTextDocument output. This
is intended for usage in the document viewer and build tool preview. is intended for usage in the document viewer and build tool preview.
@@ -92,8 +93,6 @@ class ToQTextDocument(Tokenizer):
self._pageSize = QPageSize(QPageSize.PageSizeId.A4) self._pageSize = QPageSize(QPageSize.PageSizeId.A4)
self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0) self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
return
## ##
# Properties # Properties
## ##
@@ -113,17 +112,14 @@ class ToQTextDocument(Tokenizer):
"""Set the document page size and margins in millimetres.""" """Set the document page size and margins in millimetres."""
self._pageSize = QPageSize(QSizeF(width, height), QPageSize.Unit.Millimeter) self._pageSize = QPageSize(QSizeF(width, height), QPageSize.Unit.Millimeter)
self._pageMargins = QMarginsF(left, top, right, bottom) self._pageMargins = QMarginsF(left, top, right, bottom)
return
def setShowNewPage(self, state: bool) -> None: def setShowNewPage(self, state: bool) -> None:
"""Add markers for page breaks.""" """Add markers for page breaks."""
self._newPage = state self._newPage = state
return
def disableAnchors(self) -> None: def disableAnchors(self) -> None:
"""Disable anchors for when writing to file.""" """Disable anchors for when writing to file."""
self._anchors = False self._anchors = False
return
## ##
# Class Methods # Class Methods
@@ -200,8 +196,6 @@ class ToQTextDocument(Tokenizer):
self._init = True self._init = True
return
def doConvert(self) -> None: def doConvert(self) -> None:
"""Write text tokens into the document.""" """Write text tokens into the document."""
if not self._init: if not self._init:
@@ -297,8 +291,6 @@ class ToQTextDocument(Tokenizer):
self._document.setPageSize(printer.pageRect(QPrinter.Unit.DevicePixel).size()) self._document.setPageSize(printer.pageRect(QPrinter.Unit.DevicePixel).size())
self._document.print(printer) self._document.print(printer)
return
def closeDocument(self) -> None: def closeDocument(self) -> None:
"""Run close document tasks.""" """Run close document tasks."""
self._document.blockSignals(True) self._document.blockSignals(True)
@@ -333,8 +325,6 @@ class ToQTextDocument(Tokenizer):
self._document.blockSignals(False) self._document.blockSignals(False)
return
## ##
# Internal Functions # Internal Functions
## ##
@@ -439,8 +429,6 @@ class ToQTextDocument(Tokenizer):
# Insert whatever is left in the buffer # Insert whatever is left in the buffer
cursor.insertText(stripEscape(temp[start:]), cFmt) cursor.insertText(stripEscape(temp[start:]), cFmt)
return
def _insertNewPageMarker(self, cursor: QTextCursor) -> None: def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
"""Insert a new page marker.""" """Insert a new page marker."""
if self._newPage: if self._newPage:
@@ -475,8 +463,6 @@ class ToQTextDocument(Tokenizer):
if root := self._document.rootFrame(): if root := self._document.rootFrame():
cursor.swap(root.lastCursorPosition()) cursor.swap(root.lastCursorPosition())
return
def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle: def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
"""Generate a heading style set.""" """Generate a heading style set."""
mTop, mBottom = self._mHead.get(hType, (0.0, 0.0)) mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
+2 -6
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
class ToRaw(Tokenizer): class ToRaw(Tokenizer):
"""Core: Raw novelWriter Text Writer """Core: Raw novelWriter Text Writer.
A class that will collect the minimally altered original source text A class that will collect the minimally altered original source text
and write it to either a text or JSON file. and write it to either a text or JSON file.
@@ -51,7 +51,6 @@ class ToRaw(Tokenizer):
super().__init__(project) super().__init__(project)
self._keepRaw = True self._keepRaw = True
self._noTokens = True self._noTokens = True
return
def doConvert(self) -> None: def doConvert(self) -> None:
"""No conversion to perform.""" """No conversion to perform."""
@@ -86,10 +85,7 @@ class ToRaw(Tokenizer):
logger.info("Wrote file: %s", path) logger.info("Wrote file: %s", path)
return
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None: def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None:
"""Replace tabs with spaces.""" """Replace tabs with spaces."""
spaces = spaceChar*nSpaces spaces = spaceChar*nSpaces
self._raw = [p.replace("\t", spaces) for p in self._raw] self._raw = [p.replace("\t", spaces) for p in self._raw]
return
+23 -134
View File
@@ -29,7 +29,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import bisect import bisect
@@ -98,7 +98,7 @@ class _TagAction(IntFlag):
class GuiDocEditor(QPlainTextEdit): class GuiDocEditor(QPlainTextEdit):
"""Gui Widget: Main Document Editor""" """Gui Widget: Main Document Editor."""
__slots__ = ( __slots__ = (
"_autoReplace", "_completer", "_doReplace", "_docChanged", "_docHandle", "_followTag1", "_autoReplace", "_completer", "_doReplace", "_docChanged", "_docHandle", "_followTag1",
@@ -236,8 +236,6 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Ready: GuiDocEditor") logger.debug("Ready: GuiDocEditor")
return
## ##
# Properties # Properties
## ##
@@ -290,15 +288,12 @@ class GuiDocEditor(QPlainTextEdit):
self.itemHandleChanged.emit("") self.itemHandleChanged.emit("")
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self.docSearch.updateTheme() self.docSearch.updateTheme()
self.docHeader.updateTheme() self.docHeader.updateTheme()
self.docFooter.updateTheme() self.docFooter.updateTheme()
self.docToolBar.updateTheme() self.docToolBar.updateTheme()
return
def updateSyntaxColors(self) -> None: def updateSyntaxColors(self) -> None:
"""Update the syntax highlighting theme.""" """Update the syntax highlighting theme."""
@@ -323,8 +318,6 @@ class GuiDocEditor(QPlainTextEdit):
self._selection.format.setBackground(self._lineColor) self._selection.format.setBackground(self._lineColor)
self._selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True) self._selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True)
return
def initEditor(self) -> None: def initEditor(self) -> None:
"""Initialise or re-initialise the editor with the user's """Initialise or re-initialise the editor with the user's
settings. This function is both called when the editor is settings. This function is both called when the editor is
@@ -392,8 +385,6 @@ class GuiDocEditor(QPlainTextEdit):
else: else:
self.clearEditor() self.clearEditor()
return
def loadText(self, tHandle: str, tLine: int | None = None) -> bool: def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
"""Load text from a document into the editor. If we have an I/O """Load text from a document into the editor. If we have an I/O
error, we must handle this and clear the editor so that we don't error, we must handle this and clear the editor so that we don't
@@ -471,7 +462,6 @@ class GuiDocEditor(QPlainTextEdit):
self.updateDocMargins() self.updateDocMargins()
self.setDocumentChanged(True) self.setDocumentChanged(True)
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
return
def saveText(self) -> bool: def saveText(self) -> bool:
"""Save the text currently in the editor to the NWDocument """Save the text currently in the editor to the NWDocument
@@ -539,7 +529,6 @@ class GuiDocEditor(QPlainTextEdit):
vBar.setValue(vBar.value() + 1) vBar.setValue(vBar.value() + 1)
count += 1 count += 1
QApplication.processEvents() QApplication.processEvents()
return
def updateDocMargins(self) -> None: def updateDocMargins(self) -> None:
"""Automatically adjust the margins so the text is centred if """Automatically adjust the margins so the text is centred if
@@ -580,8 +569,6 @@ class GuiDocEditor(QPlainTextEdit):
lM = max(self._vpMargin, fH) lM = max(self._vpMargin, fH)
self.setViewportMargins(tM, uM, tM, lM) self.setViewportMargins(tM, uM, tM, lM)
return
## ##
# Getters # Getters
## ##
@@ -591,20 +578,19 @@ class GuiDocEditor(QPlainTextEdit):
QTextDocument->toRawText instead of toPlainText. The former preserves QTextDocument->toRawText instead of toPlainText. The former preserves
non-breaking spaces, the latter does not. We still want to get rid of non-breaking spaces, the latter does not. We still want to get rid of
paragraph and line separators though. paragraph and line separators though.
See: https://doc.qt.io/qt-6/qtextdocument.html#toPlainText See: https://doc.qt.io/qt-6/qtextdocument.html#toPlainText
""" """
text = self._qDocument.toRawText() text = self._qDocument.toRawText()
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators return text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
return text
def getSelectedText(self) -> str: def getSelectedText(self) -> str:
"""Get currently selected text.""" """Get currently selected text."""
if (cursor := self.textCursor()).hasSelection(): if (cursor := self.textCursor()).hasSelection():
text = cursor.selectedText() text = cursor.selectedText()
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators return text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
return text
return "" return ""
def getCursorPosition(self) -> int: def getCursorPosition(self) -> int:
@@ -625,7 +611,6 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Document changed status is '%s'", state) logger.debug("Document changed status is '%s'", state)
self._docChanged = state self._docChanged = state
self.editedStatusChanged.emit(self._docChanged) self.editedStatusChanged.emit(self._docChanged)
return
def setCursorPosition(self, position: int) -> None: def setCursorPosition(self, position: int) -> None:
"""Move the cursor to a given position in the document.""" """Move the cursor to a given position in the document."""
@@ -634,14 +619,12 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(minmax(position, 0, chars-1)) cursor.setPosition(minmax(position, 0, chars-1))
self.setTextCursor(cursor) self.setTextCursor(cursor)
self.centerCursor() self.centerCursor()
return
def saveCursorPosition(self) -> None: def saveCursorPosition(self) -> None:
"""Save the cursor position to the current project item.""" """Save the cursor position to the current project item."""
if self._nwItem is not None: if self._nwItem is not None:
cursPos = self.getCursorPosition() cursPos = self.getCursorPosition()
self._nwItem.setCursorPos(cursPos) self._nwItem.setCursorPos(cursPos)
return
def setCursorLine(self, line: int | None) -> None: def setCursorLine(self, line: int | None) -> None:
"""Move the cursor to a given line in the document.""" """Move the cursor to a given line in the document."""
@@ -650,7 +633,6 @@ class GuiDocEditor(QPlainTextEdit):
if block: if block:
self.setCursorPosition(block.position()) self.setCursorPosition(block.position())
logger.debug("Cursor moved to line %d", line) logger.debug("Cursor moved to line %d", line)
return
def setCursorSelection(self, start: int, length: int) -> None: def setCursorSelection(self, start: int, length: int) -> None:
"""Make a text selection.""" """Make a text selection."""
@@ -659,14 +641,15 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(start, QtMoveAnchor) cursor.setPosition(start, QtMoveAnchor)
cursor.setPosition(start + length, QtKeepAnchor) cursor.setPosition(start + length, QtKeepAnchor)
self.setTextCursor(cursor) self.setTextCursor(cursor)
return
## ##
# Spell Checking # Spell Checking
## ##
def toggleSpellCheck(self, state: bool | None) -> None: def toggleSpellCheck(self, state: bool | None) -> None:
"""This is the main spell check setting function, and this one """Toggle spell checking.
This is the main spell check setting function, and this one
should call all other setSpellCheck functions in other classes. should call all other setSpellCheck functions in other classes.
If the spell check state is not defined (None), then toggle the If the spell check state is not defined (None), then toggle the
current status saved in this class. current status saved in this class.
@@ -690,8 +673,6 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Spell check is set to '%s'", str(state)) logger.debug("Spell check is set to '%s'", str(state))
return
def spellCheckDocument(self) -> None: def spellCheckDocument(self) -> None:
"""Rerun the highlighter to update spell checking status of the """Rerun the highlighter to update spell checking status of the
currently loaded text. currently loaded text.
@@ -703,7 +684,6 @@ class GuiDocEditor(QPlainTextEdit):
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
logger.debug("Document highlighted in %.3f ms", 1000*(time() - start)) logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
self.updateStatusMessage.emit(self.tr("Spell check complete")) self.updateStatusMessage.emit(self.tr("Spell check complete"))
return
## ##
# General Class Methods # General Class Methods
@@ -832,7 +812,6 @@ class GuiDocEditor(QPlainTextEdit):
details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation), details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation),
log=False log=False
) )
return
def insertText(self, insert: str | nwDocInsert) -> None: def insertText(self, insert: str | nwDocInsert) -> None:
"""Insert a specific type of text at the cursor position.""" """Insert a specific type of text at the cursor position."""
@@ -974,7 +953,6 @@ class GuiDocEditor(QPlainTextEdit):
event.acceptProposedAction() event.acceptProposedAction()
else: else:
super().dragEnterEvent(event) super().dragEnterEvent(event)
return
def dragMoveEvent(self, event: QDragMoveEvent) -> None: def dragMoveEvent(self, event: QDragMoveEvent) -> None:
"""Overload drag move event to handle dragged items.""" """Overload drag move event to handle dragged items."""
@@ -982,7 +960,6 @@ class GuiDocEditor(QPlainTextEdit):
event.acceptProposedAction() event.acceptProposedAction()
else: else:
super().dragMoveEvent(event) super().dragMoveEvent(event)
return
def dropEvent(self, event: QDropEvent) -> None: def dropEvent(self, event: QDropEvent) -> None:
"""Overload drop event to handle dragged items.""" """Overload drop event to handle dragged items."""
@@ -992,7 +969,6 @@ class GuiDocEditor(QPlainTextEdit):
self.openDocumentRequest.emit(handles[0], nwDocMode.EDIT, "", True) self.openDocumentRequest.emit(handles[0], nwDocMode.EDIT, "", True)
else: else:
super().dropEvent(event) super().dropEvent(event)
return
def focusNextPrevChild(self, _next: bool) -> bool: def focusNextPrevChild(self, _next: bool) -> bool:
"""Capture the focus request from the tab key on the text """Capture the focus request from the tab key on the text
@@ -1019,7 +995,6 @@ class GuiDocEditor(QPlainTextEdit):
else: else:
self._processTag(cursor) self._processTag(cursor)
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
return
def resizeEvent(self, event: QResizeEvent) -> None: def resizeEvent(self, event: QResizeEvent) -> None:
"""If the text editor is resized, we must make sure the document """If the text editor is resized, we must make sure the document
@@ -1027,7 +1002,6 @@ class GuiDocEditor(QPlainTextEdit):
""" """
self.updateDocMargins() self.updateDocMargins()
super().resizeEvent(event) super().resizeEvent(event)
return
## ##
# Public Slots # Public Slots
@@ -1035,14 +1009,13 @@ class GuiDocEditor(QPlainTextEdit):
@pyqtSlot(str, Enum) @pyqtSlot(str, Enum)
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None: def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
"""Called when an item label is changed to check if the document """Process project item change. Called when an item label is
title bar needs updating, changed to check if the document title bar needs updating.
""" """
if tHandle == self._docHandle and change == nwChange.UPDATE: if tHandle == self._docHandle and change == nwChange.UPDATE:
self.docHeader.setHandle(tHandle) self.docHeader.setHandle(tHandle)
self.docFooter.updateInfo() self.docFooter.updateInfo()
self.updateDocMargins() self.updateDocMargins()
return
@pyqtSlot(str) @pyqtSlot(str)
def insertKeyWord(self, keyword: str) -> bool: def insertKeyWord(self, keyword: str) -> bool:
@@ -1053,8 +1026,7 @@ class GuiDocEditor(QPlainTextEdit):
logger.error("Invalid keyword '%s'", keyword) logger.error("Invalid keyword '%s'", keyword)
return False return False
logger.debug("Inserting keyword '%s'", keyword) logger.debug("Inserting keyword '%s'", keyword)
state = self.insertNewBlock(f"{keyword}: ") return self.insertNewBlock(f"{keyword}: ")
return state
@pyqtSlot() @pyqtSlot()
def toggleSearch(self) -> None: def toggleSearch(self) -> None:
@@ -1063,14 +1035,12 @@ class GuiDocEditor(QPlainTextEdit):
self.closeSearch() self.closeSearch()
else: else:
self.beginSearch() self.beginSearch()
return
@pyqtSlot(list, list) @pyqtSlot(list, list)
def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None: def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
"""Tags have changed, so just in case we rehighlight them.""" """Tags have changed, so just in case we rehighlight them."""
if updated or deleted: if updated or deleted:
self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META) self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META)
return
## ##
# Private Slots # Private Slots
@@ -1116,8 +1086,6 @@ class GuiDocEditor(QPlainTextEdit):
if self._autoReplace.process(text, cursor): if self._autoReplace.process(text, cursor):
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block()) self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
return
@pyqtSlot() @pyqtSlot()
def _cursorMoved(self) -> None: def _cursorMoved(self) -> None:
"""Triggered when the cursor moved in the editor.""" """Triggered when the cursor moved in the editor."""
@@ -1126,7 +1094,6 @@ class GuiDocEditor(QPlainTextEdit):
self._selection.cursor = self.textCursor() self._selection.cursor = self.textCursor()
self._selection.cursor.clearSelection() self._selection.cursor.clearSelection()
self.setExtraSelections([self._selection]) self.setExtraSelections([self._selection])
return
@pyqtSlot(int, int, str) @pyqtSlot(int, int, str)
def _insertCompletion(self, pos: int, length: int, text: str) -> None: def _insertCompletion(self, pos: int, length: int, text: str) -> None:
@@ -1138,13 +1105,11 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(check + length, QtKeepAnchor) cursor.setPosition(check + length, QtKeepAnchor)
cursor.insertText(text) cursor.insertText(text)
self._completer.hide() self._completer.hide()
return
@pyqtSlot() @pyqtSlot()
def _openContextFromCursor(self) -> None: def _openContextFromCursor(self) -> None:
"""Open the spell check context menu at the cursor.""" """Open the spell check context menu at the cursor."""
self._openContextMenu(self.cursorRect().center()) self._openContextMenu(self.cursorRect().center())
return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
def _openContextMenu(self, pos: QPoint) -> None: def _openContextMenu(self, pos: QPoint) -> None:
@@ -1231,8 +1196,6 @@ class GuiDocEditor(QPlainTextEdit):
ctxMenu.setParent(None) ctxMenu.setParent(None)
return
@pyqtSlot() @pyqtSlot()
def _runDocumentTasks(self) -> None: def _runDocumentTasks(self) -> None:
"""Run timer document tasks.""" """Run timer document tasks."""
@@ -1269,11 +1232,10 @@ class GuiDocEditor(QPlainTextEdit):
if not self.textCursor().hasSelection(): if not self.textCursor().hasSelection():
# Selection counter should take precedence (#2155) # Selection counter should take precedence (#2155)
self.docFooter.updateMainCount(mCount, False) self.docFooter.updateMainCount(mCount, False)
return
@pyqtSlot() @pyqtSlot()
def _updateSelectedStatus(self) -> None: def _updateSelectedStatus(self) -> None:
"""The user made a change in text selection. Forward this """Process user change in text selection. Forward this
information to the footer, and start the selection word counter. information to the footer, and start the selection word counter.
""" """
if self.textCursor().hasSelection(): if self.textCursor().hasSelection():
@@ -1282,7 +1244,6 @@ class GuiDocEditor(QPlainTextEdit):
else: else:
self._timerSel.stop() self._timerSel.stop()
self.docFooter.updateMainCount(0, False) self.docFooter.updateMainCount(0, False)
return
@pyqtSlot() @pyqtSlot()
def _runSelCounter(self) -> None: def _runSelCounter(self) -> None:
@@ -1300,14 +1261,12 @@ class GuiDocEditor(QPlainTextEdit):
if self._docHandle and self._nwItem: if self._docHandle and self._nwItem:
self.docFooter.updateMainCount(cCount if CONFIG.useCharCount else wCount, True) self.docFooter.updateMainCount(cCount if CONFIG.useCharCount else wCount, True)
self._timerSel.stop() self._timerSel.stop()
return
@pyqtSlot() @pyqtSlot()
def _closeCurrentDocument(self) -> None: def _closeCurrentDocument(self) -> None:
"""Close the document. Forwarded to the main Gui.""" """Close the document. Forwarded to the main Gui."""
self.closeEditorRequest.emit() self.closeEditorRequest.emit()
self.docToolBar.setVisible(False) self.docToolBar.setVisible(False)
return
@pyqtSlot() @pyqtSlot()
def _toggleToolBarVisibility(self) -> None: def _toggleToolBarVisibility(self) -> None:
@@ -1315,7 +1274,6 @@ class GuiDocEditor(QPlainTextEdit):
state = not self.docToolBar.isVisible() state = not self.docToolBar.isVisible()
self.docToolBar.setVisible(state) self.docToolBar.setVisible(state)
CONFIG.showEditToolBar = state CONFIG.showEditToolBar = state
return
## ##
# Search & Replace # Search & Replace
@@ -1326,14 +1284,12 @@ class GuiDocEditor(QPlainTextEdit):
self.docSearch.setSearchText(self.getSelectedText() or None) self.docSearch.setSearchText(self.getSelectedText() or None)
resS, _ = self.findAllOccurences() resS, _ = self.findAllOccurences()
self.docSearch.setResultCount(None, len(resS)) self.docSearch.setResultCount(None, len(resS))
return
def beginReplace(self) -> None: def beginReplace(self) -> None:
"""Initialise the search box and reset the replace text box.""" """Initialise the search box and reset the replace text box."""
self.beginSearch() self.beginSearch()
self.docSearch.setReplaceText("") self.docSearch.setReplaceText("")
self.updateDocMargins() self.updateDocMargins()
return
def findNext(self, goBack: bool = False) -> None: def findNext(self, goBack: bool = False) -> None:
"""Search for the next or previous occurrence of the search bar """Search for the next or previous occurrence of the search bar
@@ -1621,8 +1577,6 @@ class GuiDocEditor(QPlainTextEdit):
self.setTextCursor(cursor) self.setTextCursor(cursor)
return
def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> None: def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> None:
"""Replace all straight quotes in the selected text.""" """Replace all straight quotes in the selected text."""
cursor = self.textCursor() cursor = self.textCursor()
@@ -1882,8 +1836,6 @@ class GuiDocEditor(QPlainTextEdit):
cursor.insertText(cleanText.rstrip() + "\n") cursor.insertText(cleanText.rstrip() + "\n")
cursor.endEditBlock() cursor.endEditBlock()
return
def _insertCommentStructure(self, style: nwComment) -> None: def _insertCommentStructure(self, style: nwComment) -> None:
"""Insert a shortcut/comment combo.""" """Insert a shortcut/comment combo."""
if self._docHandle and style == nwComment.FOOTNOTE: if self._docHandle and style == nwComment.FOOTNOTE:
@@ -1925,7 +1877,6 @@ class GuiDocEditor(QPlainTextEdit):
cursor.endEditBlock() cursor.endEditBlock()
cursor.setPosition(pos) cursor.setPosition(pos)
self.setTextCursor(cursor) self.setTextCursor(cursor)
return
def _addWord(self, word: str, block: QTextBlock, save: bool) -> None: def _addWord(self, word: str, block: QTextBlock, save: bool) -> None:
"""Slot for the spell check context menu triggered when the user """Slot for the spell check context menu triggered when the user
@@ -1934,7 +1885,6 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Added '%s' to project dictionary, %s", word, "saved" if save else "unsaved") logger.debug("Added '%s' to project dictionary, %s", word, "saved" if save else "unsaved")
SHARED.spelling.addWord(word, save=save) SHARED.spelling.addWord(word, save=save)
self._qDocument.syntaxHighlighter.rehighlightBlock(block) self._qDocument.syntaxHighlighter.rehighlightBlock(block)
return
def _processTag( def _processTag(
self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False
@@ -2008,7 +1958,6 @@ class GuiDocEditor(QPlainTextEdit):
if self._docHandle: if self._docHandle:
text = block.text().lstrip("#").lstrip("!").strip() text = block.text().lstrip("#").lstrip("!").strip()
self.requestProjectItemRenamed.emit(self._docHandle, text) self.requestProjectItemRenamed.emit(self._docHandle, text)
return
def _autoSelect(self) -> QTextCursor: def _autoSelect(self) -> QTextCursor:
"""Return a cursor which may or may not have a selection based """Return a cursor which may or may not have a selection based
@@ -2078,14 +2027,11 @@ class GuiDocEditor(QPlainTextEdit):
self.setTextCursor(cursor) self.setTextCursor(cursor)
return
def _makePosSelection(self, mode: QTextCursor.SelectionType, pos: QPoint) -> None: def _makePosSelection(self, mode: QTextCursor.SelectionType, pos: QPoint) -> None:
"""Select text based on selection mode, but first move cursor.""" """Select text based on selection mode, but first move cursor."""
cursor = self.cursorForPosition(pos) cursor = self.cursorForPosition(pos)
self.setTextCursor(cursor) self.setTextCursor(cursor)
self._makeSelection(mode) self._makeSelection(mode)
return
def _allowAutoReplace(self, state: bool) -> None: def _allowAutoReplace(self, state: bool) -> None:
"""Enable/disable the auto-replace feature temporarily.""" """Enable/disable the auto-replace feature temporarily."""
@@ -2093,11 +2039,10 @@ class GuiDocEditor(QPlainTextEdit):
self._doReplace = CONFIG.doReplace self._doReplace = CONFIG.doReplace
else: else:
self._doReplace = False self._doReplace = False
return
class CommandCompleter(QMenu): class CommandCompleter(QMenu):
"""GuiWidget: Command Completer Menu """GuiWidget: Command Completer Menu.
This is a context menu with options populated from the user's This is a context menu with options populated from the user's
defined tags and keys. It also helps to type the meta data keyword defined tags and keys. It also helps to type the meta data keyword
@@ -2109,7 +2054,6 @@ class CommandCompleter(QMenu):
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
return
def updateMetaText(self, text: str, pos: int) -> bool: def updateMetaText(self, text: str, pos: int) -> bool:
"""Update the menu options based on the line of text.""" """Update the menu options based on the line of text."""
@@ -2203,7 +2147,6 @@ class CommandCompleter(QMenu):
super().keyPressEvent(event) super().keyPressEvent(event)
elif isinstance(parent, GuiDocEditor): elif isinstance(parent, GuiDocEditor):
parent.keyPressEvent(event) parent.keyPressEvent(event)
return
## ##
# Internal Functions # Internal Functions
@@ -2212,11 +2155,10 @@ class CommandCompleter(QMenu):
def _emitComplete(self, pos: int, length: int, value: str) -> None: def _emitComplete(self, pos: int, length: int, value: str) -> None:
"""Emit the signal to indicate a selection has been made.""" """Emit the signal to indicate a selection has been made."""
self.complete.emit(pos, length, value) self.complete.emit(pos, length, value)
return
class BackgroundWordCounter(QRunnable): class BackgroundWordCounter(QRunnable):
"""The Off-GUI Thread Word Counter """The Off-GUI Thread Word Counter.
A runnable for the word counter to be run in the thread pool off the A runnable for the word counter to be run in the thread pool off the
main GUI thread. main GUI thread.
@@ -2228,9 +2170,9 @@ class BackgroundWordCounter(QRunnable):
self._forSelection = forSelection self._forSelection = forSelection
self._isRunning = False self._isRunning = False
self.signals = BackgroundWordCounterSignals() self.signals = BackgroundWordCounterSignals()
return
def isRunning(self) -> bool: def isRunning(self) -> bool:
"""Return True if the word counter is already running."""
return self._isRunning return self._isRunning
@pyqtSlot() @pyqtSlot()
@@ -2248,17 +2190,17 @@ class BackgroundWordCounter(QRunnable):
self.signals.countsReady.emit(cC, wC, pC) self.signals.countsReady.emit(cC, wC, pC)
self._isRunning = False self._isRunning = False
return
class BackgroundWordCounterSignals(QObject): class BackgroundWordCounterSignals(QObject):
"""The QRunnable cannot emit a signal, so we need a simple QObject """The QRunnable cannot emit a signal, so we need a simple QObject
to hold the word counter signal. to hold the word counter signal.
""" """
countsReady = pyqtSignal(int, int, int) countsReady = pyqtSignal(int, int, int)
class TextAutoReplace: class TextAutoReplace:
"""Encapsulates the editor auto replace feature."""
__slots__ = ( __slots__ = (
"_doPadAfter", "_doPadBefore", "_padAfter", "_padBefore", "_padChar", "_doPadAfter", "_doPadBefore", "_padAfter", "_padBefore", "_padChar",
@@ -2268,7 +2210,6 @@ class TextAutoReplace:
def __init__(self) -> None: def __init__(self) -> None:
self.initSettings() self.initSettings()
return
def initSettings(self) -> None: def initSettings(self) -> None:
"""Initialise the auto-replace settings from config.""" """Initialise the auto-replace settings from config."""
@@ -2287,7 +2228,6 @@ class TextAutoReplace:
self._padAfter = CONFIG.fmtPadAfter self._padAfter = CONFIG.fmtPadAfter
self._doPadBefore = bool(CONFIG.fmtPadBefore) self._doPadBefore = bool(CONFIG.fmtPadBefore)
self._doPadAfter = bool(CONFIG.fmtPadAfter) self._doPadAfter = bool(CONFIG.fmtPadAfter)
return
def process(self, text: str, cursor: QTextCursor) -> bool: def process(self, text: str, cursor: QTextCursor) -> bool:
"""Auto-replace text elements based on main configuration. """Auto-replace text elements based on main configuration.
@@ -2401,7 +2341,7 @@ class TextAutoReplace:
class GuiDocToolBar(QWidget): class GuiDocToolBar(QWidget):
"""The Formatting and Options Fold Out Menu """The Formatting and Options Fold Out Menu.
Only used by DocEditor, and is opened by the first button in the Only used by DocEditor, and is opened by the first button in the
header. header.
@@ -2513,8 +2453,6 @@ class GuiDocToolBar(QWidget):
logger.debug("Ready: GuiDocToolBar") logger.debug("Ready: GuiDocToolBar")
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Initialise GUI elements that depend on specific settings.""" """Initialise GUI elements that depend on specific settings."""
syntax = SHARED.theme.syntaxTheme syntax = SHARED.theme.syntaxTheme
@@ -2537,11 +2475,9 @@ class GuiDocToolBar(QWidget):
self.tbSuperscript.setThemeIcon("fmt_superscript") self.tbSuperscript.setThemeIcon("fmt_superscript")
self.tbSubscript.setThemeIcon("fmt_subscript") self.tbSubscript.setThemeIcon("fmt_subscript")
return
class GuiDocEditSearch(QFrame): class GuiDocEditSearch(QFrame):
"""The Embedded Document Search/Replace Feature """The Embedded Document Search/Replace Feature.
Only used by DocEditor, and is at a fixed position in the Only used by DocEditor, and is at a fixed position in the
QTextEdit's viewport. QTextEdit's viewport.
@@ -2671,8 +2607,6 @@ class GuiDocEditSearch(QFrame):
logger.debug("Ready: GuiDocEditSearch") logger.debug("Ready: GuiDocEditSearch")
return
## ##
# Properties # Properties
## ##
@@ -2721,14 +2655,12 @@ class GuiDocEditSearch(QFrame):
self.searchBox.selectAll() self.searchBox.selectAll()
if CONFIG.searchRegEx: if CONFIG.searchRegEx:
self._alertSearchValid(True) self._alertSearchValid(True)
return
def setReplaceText(self, text: str) -> None: def setReplaceText(self, text: str) -> None:
"""Set the replace text.""" """Set the replace text."""
self.showReplace.setChecked(True) self.showReplace.setChecked(True)
self.replaceBox.setFocus() self.replaceBox.setFocus()
self.replaceBox.setText(text) self.replaceBox.setText(text)
return
def setResultCount(self, currRes: int | None, resCount: int | None) -> None: def setResultCount(self, currRes: int | None, resCount: int | None) -> None:
"""Set the count values for the current search.""" """Set the count values for the current search."""
@@ -2743,7 +2675,6 @@ class GuiDocEditSearch(QFrame):
self.resultLabel.setMinimumWidth(minWidth) self.resultLabel.setMinimumWidth(minWidth)
self.adjustSize() self.adjustSize()
self.docEditor.updateDocMargins() self.docEditor.updateDocMargins()
return
## ##
# Methods # Methods
@@ -2759,7 +2690,6 @@ class GuiDocEditSearch(QFrame):
self.resultLabel.setMinimumWidth( self.resultLabel.setMinimumWidth(
SHARED.theme.getTextWidth("?/?", SHARED.theme.guiFontSmall) SHARED.theme.getTextWidth("?/?", SHARED.theme.guiFontSmall)
) )
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
@@ -2784,11 +2714,9 @@ class GuiDocEditSearch(QFrame):
self.searchOpt.setStyleSheet("QToolBar {padding: 0;}") self.searchOpt.setStyleSheet("QToolBar {padding: 0;}")
self.showReplace.setStyleSheet("QToolButton {border: none; background: transparent;}") self.showReplace.setStyleSheet("QToolButton {border: none; background: transparent;}")
return
def cycleFocus(self) -> bool: def cycleFocus(self) -> bool:
"""The tab key just alternates focus between the two input """Cycle focus on tab key press. This just alternates focus
boxes, if the replace box is visible. between the two input boxes, if the replace box is visible.
""" """
if self.searchBox.hasFocus(): if self.searchBox.hasFocus():
self.replaceBox.setFocus() self.replaceBox.setFocus()
@@ -2813,7 +2741,6 @@ class GuiDocEditSearch(QFrame):
self.setVisible(False) self.setVisible(False)
self.docEditor.updateDocMargins() self.docEditor.updateDocMargins()
self.docEditor.setFocus() self.docEditor.setFocus()
return
## ##
# Private Slots # Private Slots
@@ -2823,13 +2750,11 @@ class GuiDocEditSearch(QFrame):
def _doSearch(self) -> None: def _doSearch(self) -> None:
"""Call the search action function for the document editor.""" """Call the search action function for the document editor."""
self.docEditor.findNext(goBack=(QApplication.keyboardModifiers() == QtModShift)) self.docEditor.findNext(goBack=(QApplication.keyboardModifiers() == QtModShift))
return
@pyqtSlot() @pyqtSlot()
def _doReplace(self) -> None: def _doReplace(self) -> None:
"""Call the replace action function for the document editor.""" """Call the replace action function for the document editor."""
self.docEditor.replaceNext() self.docEditor.replaceNext()
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _doToggleReplace(self, state: bool) -> None: def _doToggleReplace(self, state: bool) -> None:
@@ -2838,43 +2763,36 @@ class GuiDocEditSearch(QFrame):
self.replaceButton.setVisible(state) self.replaceButton.setVisible(state)
self.adjustSize() self.adjustSize()
self.docEditor.updateDocMargins() self.docEditor.updateDocMargins()
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _doToggleCase(self, state: bool) -> None: def _doToggleCase(self, state: bool) -> None:
"""Enable/disable case sensitive mode.""" """Enable/disable case sensitive mode."""
CONFIG.searchCase = state CONFIG.searchCase = state
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _doToggleWord(self, state: bool) -> None: def _doToggleWord(self, state: bool) -> None:
"""Enable/disable whole word search mode.""" """Enable/disable whole word search mode."""
CONFIG.searchWord = state CONFIG.searchWord = state
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _doToggleRegEx(self, state: bool) -> None: def _doToggleRegEx(self, state: bool) -> None:
"""Enable/disable regular expression search mode.""" """Enable/disable regular expression search mode."""
CONFIG.searchRegEx = state CONFIG.searchRegEx = state
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _doToggleLoop(self, state: bool) -> None: def _doToggleLoop(self, state: bool) -> None:
"""Enable/disable looping the search.""" """Enable/disable looping the search."""
CONFIG.searchLoop = state CONFIG.searchLoop = state
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _doToggleProject(self, state: bool) -> None: def _doToggleProject(self, state: bool) -> None:
"""Enable/disable continuing search in next project file.""" """Enable/disable continuing search in next project file."""
CONFIG.searchNextFile = state CONFIG.searchNextFile = state
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _doToggleMatchCap(self, state: bool) -> None: def _doToggleMatchCap(self, state: bool) -> None:
"""Enable/disable preserving capitalisation when replacing.""" """Enable/disable preserving capitalisation when replacing."""
CONFIG.searchMatchCap = state CONFIG.searchMatchCap = state
return
## ##
# Internal Functions # Internal Functions
@@ -2890,11 +2808,10 @@ class GuiDocEditSearch(QFrame):
palette.text().color() if isValid else SHARED.theme.errorText palette.text().color() if isValid else SHARED.theme.errorText
) )
self.searchBox.setPalette(palette) self.searchBox.setPalette(palette)
return
class GuiDocEditHeader(QWidget): class GuiDocEditHeader(QWidget):
"""The Embedded Document Header """The Embedded Document Header.
Only used by DocEditor, and is at a fixed position in the Only used by DocEditor, and is at a fixed position in the
QTextEdit's viewport. QTextEdit's viewport.
@@ -2985,8 +2902,6 @@ class GuiDocEditHeader(QWidget):
logger.debug("Ready: GuiDocEditHeader") logger.debug("Ready: GuiDocEditHeader")
return
## ##
# Methods # Methods
## ##
@@ -3003,7 +2918,6 @@ class GuiDocEditHeader(QWidget):
self.searchButton.setVisible(False) self.searchButton.setVisible(False)
self.closeButton.setVisible(False) self.closeButton.setVisible(False)
self.minmaxButton.setVisible(False) self.minmaxButton.setVisible(False)
return
def setOutline(self, data: dict[int, str]) -> None: def setOutline(self, data: dict[int, str]) -> None:
"""Set the document outline dataset.""" """Set the document outline dataset."""
@@ -3015,13 +2929,11 @@ class GuiDocEditHeader(QWidget):
action.triggered.connect(qtLambda(self._gotoBlock, number)) action.triggered.connect(qtLambda(self._gotoBlock, number))
self._docOutline = data self._docOutline = data
logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart)) logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
return
def updateFont(self) -> None: def updateFont(self) -> None:
"""Update the font settings.""" """Update the font settings."""
self.setFont(SHARED.theme.guiFont) self.setFont(SHARED.theme.guiFont)
self.itemTitle.setFont(SHARED.theme.guiFontSmall) self.itemTitle.setFont(SHARED.theme.guiFontSmall)
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
@@ -3040,8 +2952,6 @@ class GuiDocEditHeader(QWidget):
self.matchColors() self.matchColors()
return
def matchColors(self) -> None: def matchColors(self) -> None:
"""Update the colours of the widget to match those of the syntax """Update the colours of the widget to match those of the syntax
theme rather than the main GUI. theme rather than the main GUI.
@@ -3055,12 +2965,10 @@ class GuiDocEditHeader(QWidget):
self.itemTitle.setTextColors( self.itemTitle.setTextColors(
color=palette.windowText().color(), faded=SHARED.theme.fadedText color=palette.windowText().color(), faded=SHARED.theme.fadedText
) )
return
def changeFocusState(self, state: bool) -> None: def changeFocusState(self, state: bool) -> None:
"""Toggle focus state.""" """Toggle focus state."""
self.itemTitle.setColorState(state) self.itemTitle.setColorState(state)
return
def setHandle(self, tHandle: str) -> None: def setHandle(self, tHandle: str) -> None:
"""Set the document title from the handle, or alternatively, set """Set the document title from the handle, or alternatively, set
@@ -3081,8 +2989,6 @@ class GuiDocEditHeader(QWidget):
self.closeButton.setVisible(True) self.closeButton.setVisible(True)
self.minmaxButton.setVisible(True) self.minmaxButton.setVisible(True)
return
## ##
# Private Slots # Private Slots
## ##
@@ -3092,19 +2998,16 @@ class GuiDocEditHeader(QWidget):
"""Trigger the close editor on the main window.""" """Trigger the close editor on the main window."""
self.clearHeader() self.clearHeader()
self.closeDocumentRequest.emit() self.closeDocumentRequest.emit()
return
@pyqtSlot(int) @pyqtSlot(int)
def _gotoBlock(self, blockNumber: int) -> None: def _gotoBlock(self, blockNumber: int) -> None:
"""Move cursor to a specific heading.""" """Move cursor to a specific heading."""
self.docEditor.setCursorLine(blockNumber + 1) self.docEditor.setCursorLine(blockNumber + 1)
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _focusModeChanged(self, focusMode: bool) -> None: def _focusModeChanged(self, focusMode: bool) -> None:
"""Update minimise/maximise icon of the Focus Mode button.""" """Update minimise/maximise icon of the Focus Mode button."""
self.minmaxButton.setThemeIcon("minimise" if focusMode else "maximise", "blue") self.minmaxButton.setThemeIcon("minimise" if focusMode else "maximise", "blue")
return
## ##
# Events # Events
@@ -3116,11 +3019,10 @@ class GuiDocEditHeader(QWidget):
""" """
if event.button() == QtMouseLeft: if event.button() == QtMouseLeft:
self.docEditor.requestProjectItemSelected.emit(self._docHandle or "", True) self.docEditor.requestProjectItemSelected.emit(self._docHandle or "", True)
return
class GuiDocEditFooter(QWidget): class GuiDocEditFooter(QWidget):
"""The Embedded Document Footer """The Embedded Document Footer.
Only used by DocEditor, and is at a fixed position in the Only used by DocEditor, and is at a fixed position in the
QTextEdit's viewport. QTextEdit's viewport.
@@ -3205,8 +3107,6 @@ class GuiDocEditFooter(QWidget):
logger.debug("Ready: GuiDocEditFooter") logger.debug("Ready: GuiDocEditFooter")
return
## ##
# Methods # Methods
## ##
@@ -3216,7 +3116,6 @@ class GuiDocEditFooter(QWidget):
self._trMainCount = trStats(nwLabels.STATS_DISPLAY[ self._trMainCount = trStats(nwLabels.STATS_DISPLAY[
nwStats.CHARS if CONFIG.useCharCount else nwStats.WORDS nwStats.CHARS if CONFIG.useCharCount else nwStats.WORDS
]) ])
return
def updateFont(self) -> None: def updateFont(self) -> None:
"""Update the font settings.""" """Update the font settings."""
@@ -3224,7 +3123,6 @@ class GuiDocEditFooter(QWidget):
self.statusText.setFont(SHARED.theme.guiFontSmall) self.statusText.setFont(SHARED.theme.guiFontSmall)
self.linesText.setFont(SHARED.theme.guiFontSmall) self.linesText.setFont(SHARED.theme.guiFontSmall)
self.wordsText.setFont(SHARED.theme.guiFontSmall) self.wordsText.setFont(SHARED.theme.guiFontSmall)
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
@@ -3232,7 +3130,6 @@ class GuiDocEditFooter(QWidget):
self.linesIcon.setPixmap(SHARED.theme.getPixmap("lines", (iPx, iPx))) self.linesIcon.setPixmap(SHARED.theme.getPixmap("lines", (iPx, iPx)))
self.wordsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx))) self.wordsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx)))
self.matchColors() self.matchColors()
return
def matchColors(self) -> None: def matchColors(self) -> None:
"""Update the colours of the widget to match those of the syntax """Update the colours of the widget to match those of the syntax
@@ -3250,8 +3147,6 @@ class GuiDocEditFooter(QWidget):
self.linesText.setPalette(palette) self.linesText.setPalette(palette)
self.wordsText.setPalette(palette) self.wordsText.setPalette(palette)
return
def setHandle(self, tHandle: str | None) -> None: def setHandle(self, tHandle: str | None) -> None:
"""Set the handle that will populate the footer's data.""" """Set the handle that will populate the footer's data."""
self._docHandle = tHandle self._docHandle = tHandle
@@ -3264,8 +3159,6 @@ class GuiDocEditFooter(QWidget):
self.updateInfo() self.updateInfo()
self.updateMainCount(0, False) self.updateMainCount(0, False)
return
def updateInfo(self) -> None: def updateInfo(self) -> None:
"""Update the content of text labels.""" """Update the content of text labels."""
if self._tItem is None: if self._tItem is None:
@@ -3280,8 +3173,6 @@ class GuiDocEditFooter(QWidget):
self.statusIcon.setPixmap(sIcon) self.statusIcon.setPixmap(sIcon)
self.statusText.setText(sText) self.statusText.setText(sText)
return
def updateLineCount(self, cursor: QTextCursor) -> None: def updateLineCount(self, cursor: QTextCursor) -> None:
"""Update the line and document position counter.""" """Update the line and document position counter."""
if document := cursor.document(): if document := cursor.document():
@@ -3291,7 +3182,6 @@ class GuiDocEditFooter(QWidget):
self.linesText.setText( self.linesText.setText(
self._trLineCount.format(f"{cLine:n}", f"{100*cPos//cCount:d} %") self._trLineCount.format(f"{cLine:n}", f"{100*cPos//cCount:d} %")
) )
return
def updateMainCount(self, count: int, selection: bool) -> None: def updateMainCount(self, count: int, selection: bool) -> None:
"""Update main counter information.""" """Update main counter information."""
@@ -3304,4 +3194,3 @@ class GuiDocEditFooter(QWidget):
else: else:
text = self._trMainCount.format("0", "+0") text = self._trMainCount.format("0", "+0")
self.wordsText.setText(text) self.wordsText.setText(text)
return
+7 -13
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -57,6 +57,7 @@ BLOCK_TITLE = 4
class GuiDocHighlighter(QSyntaxHighlighter): class GuiDocHighlighter(QSyntaxHighlighter):
"""GUI: Editor Syntax Highlighter."""
__slots__ = ( __slots__ = (
"_cmnRules", "_dialogParser", "_hStyles", "_isInactive", "_isNovel", "_cmnRules", "_dialogParser", "_hStyles", "_isInactive", "_isNovel",
@@ -85,8 +86,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
logger.debug("Ready: GuiDocHighlighter") logger.debug("Ready: GuiDocHighlighter")
return
def initHighlighter(self) -> None: def initHighlighter(self) -> None:
"""Initialise the syntax highlighter, setting all the colour """Initialise the syntax highlighter, setting all the colour
rules and building the RegExes. rules and building the RegExes.
@@ -255,8 +254,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule))
return
## ##
# Setters # Setters
## ##
@@ -264,7 +261,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
def setSpellCheck(self, state: bool) -> None: def setSpellCheck(self, state: bool) -> None:
"""Enable/disable the real time spell checker.""" """Enable/disable the real time spell checker."""
self._spellCheck = state self._spellCheck = state
return
def setHandle(self, tHandle: str) -> None: def setHandle(self, tHandle: str) -> None:
"""Set the handle of the currently highlighted document.""" """Set the handle of the currently highlighted document."""
@@ -275,7 +271,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._isNovel = item.isDocumentLayout() self._isNovel = item.isDocumentLayout()
self._isInactive = item.isInactiveClass() self._isInactive = item.isInactiveClass()
logger.debug("Syntax highlighter enabled for item '%s'", tHandle) logger.debug("Syntax highlighter enabled for item '%s'", tHandle)
return
## ##
# Methods # Methods
@@ -293,7 +288,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if block.userState() & cType > 0: if block.userState() & cType > 0:
self.rehighlightBlock(block) self.rehighlightBlock(block)
logger.debug("Document highlighted in %.3f ms" % (1000*(time() - tStart))) logger.debug("Document highlighted in %.3f ms" % (1000*(time() - tStart)))
return
## ##
# Highlight Block # Highlight Block
@@ -506,10 +500,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._hStyles[name] = charFormat self._hStyles[name] = charFormat
return
class TextBlockData(QTextBlockUserData): class TextBlockData(QTextBlockUserData):
"""Custom QTextBlock Data.
Custom data stored in a single text block. The spell check state is
cached here and used when correcting misspelled text.
"""
__slots__ = ("_metaData", "_offset", "_spellErrors", "_text") __slots__ = ("_metaData", "_offset", "_spellErrors", "_text")
@@ -519,7 +516,6 @@ class TextBlockData(QTextBlockUserData):
self._offset = 0 self._offset = 0
self._metaData: list[tuple[int, int, str, str]] = [] self._metaData: list[tuple[int, int, str, str]] = []
self._spellErrors: list[tuple[int, int, str]] = [] self._spellErrors: list[tuple[int, int, str]] = []
return
@property @property
def metaData(self) -> list[tuple[int, int, str, str]]: def metaData(self) -> list[tuple[int, int, str, str]]:
@@ -553,8 +549,6 @@ class TextBlockData(QTextBlockUserData):
self._text = text.replace("\u02bc", "'").replace("_", " ") self._text = text.replace("\u02bc", "'").replace("_", " ")
self._offset = offset self._offset = offset
return
def spellCheck(self, utf16Map: list[int] | None) -> list[tuple[int, int, str]]: def spellCheck(self, utf16Map: list[int] | None) -> list[tuple[int, int, str]]:
"""Run the spell checker and cache the result, and return the """Run the spell checker and cache the result, and return the
list of spell check errors. list of spell check errors.
+10 -63
View File
@@ -23,7 +23,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -60,6 +60,7 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser): class GuiDocViewer(QTextBrowser):
"""GUI: Document Viewer."""
closeDocumentRequest = pyqtSignal() closeDocumentRequest = pyqtSignal()
documentLoaded = pyqtSignal(str) documentLoaded = pyqtSignal(str)
@@ -110,8 +111,6 @@ class GuiDocViewer(QTextBrowser):
logger.debug("Ready: GuiDocViewer") logger.debug("Ready: GuiDocViewer")
return
## ##
# Properties # Properties
## ##
@@ -138,13 +137,11 @@ class GuiDocViewer(QTextBrowser):
self.setSearchPaths([""]) self.setSearchPaths([""])
self._docHandle = None self._docHandle = None
self.docHeader.clearHeader() self.docHeader.clearHeader()
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self.docHeader.updateTheme() self.docHeader.updateTheme()
self.docFooter.updateTheme() self.docFooter.updateTheme()
return
def initViewer(self) -> None: def initViewer(self) -> None:
"""Set editor settings from main config.""" """Set editor settings from main config."""
@@ -206,8 +203,6 @@ class GuiDocViewer(QTextBrowser):
# If we have a document open, we should reload it in case the font changed # If we have a document open, we should reload it in case the font changed
self.reloadText() self.reloadText()
return
def loadText(self, tHandle: str, updateHistory: bool = True) -> bool: def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
"""Load text into the viewer from an item handle.""" """Load text into the viewer from an item handle."""
if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE): if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
@@ -280,7 +275,6 @@ class GuiDocViewer(QTextBrowser):
"""Reload the text in the current document.""" """Reload the text in the current document."""
if self._docHandle: if self._docHandle:
self.loadText(self._docHandle, updateHistory=False) self.loadText(self._docHandle, updateHistory=False)
return
def docAction(self, action: nwDocAction) -> bool: def docAction(self, action: nwDocAction) -> bool:
"""Process document actions on the current document.""" """Process document actions on the current document."""
@@ -308,7 +302,6 @@ class GuiDocViewer(QTextBrowser):
def clearNavHistory(self) -> None: def clearNavHistory(self) -> None:
"""Clear the navigation history.""" """Clear the navigation history."""
self.docHistory.clear() self.docHistory.clear()
return
def updateDocMargins(self) -> None: def updateDocMargins(self) -> None:
"""Automatically adjust the margins so the text is centred.""" """Automatically adjust the margins so the text is centred."""
@@ -337,8 +330,6 @@ class GuiDocViewer(QTextBrowser):
self.docFooter.setGeometry(tB, fY, tW, fH) self.docFooter.setGeometry(tB, fY, tW, fH)
self.setViewportMargins(tM, max(cM, tH), tM, max(cM, fH)) self.setViewportMargins(tM, max(cM, tH), tM, max(cM, fH))
return
## ##
# Setters # Setters
## ##
@@ -347,7 +338,6 @@ class GuiDocViewer(QTextBrowser):
"""Set the scrollbar position.""" """Set the scrollbar position."""
if (vBar := self.verticalScrollBar()) and vBar.isVisible(): if (vBar := self.verticalScrollBar()) and vBar.isVisible():
vBar.setValue(pos) vBar.setValue(pos)
return
## ##
# Public Slots # Public Slots
@@ -359,7 +349,6 @@ class GuiDocViewer(QTextBrowser):
if tHandle == self._docHandle and change == nwChange.UPDATE: if tHandle == self._docHandle and change == nwChange.UPDATE:
self.docHeader.setHandle(tHandle) self.docHeader.setHandle(tHandle)
self.updateDocMargins() self.updateDocMargins()
return
@pyqtSlot(str) @pyqtSlot(str)
def navigateTo(self, anchor: str) -> None: def navigateTo(self, anchor: str) -> None:
@@ -367,7 +356,6 @@ class GuiDocViewer(QTextBrowser):
if isinstance(anchor, str) and anchor.startswith("#"): if isinstance(anchor, str) and anchor.startswith("#"):
logger.debug("Moving to anchor '%s'", anchor) logger.debug("Moving to anchor '%s'", anchor)
self.setSource(QUrl(anchor)) self.setSource(QUrl(anchor))
return
## ##
# Private Slots # Private Slots
@@ -377,13 +365,11 @@ class GuiDocViewer(QTextBrowser):
def navBackward(self) -> None: def navBackward(self) -> None:
"""Navigate backwards in the document view history.""" """Navigate backwards in the document view history."""
self.docHistory.backward() self.docHistory.backward()
return
@pyqtSlot() @pyqtSlot()
def navForward(self) -> None: def navForward(self) -> None:
"""Navigate forwards in the document view history.""" """Navigate forwards in the document view history."""
self.docHistory.forward() self.docHistory.forward()
return
@pyqtSlot("QUrl") @pyqtSlot("QUrl")
def _linkClicked(self, url: QUrl) -> None: def _linkClicked(self, url: QUrl) -> None:
@@ -396,7 +382,6 @@ class GuiDocViewer(QTextBrowser):
self.navigateTo(link) self.navigateTo(link)
elif link.startswith("http"): elif link.startswith("http"):
QDesktopServices.openUrl(QUrl(url)) QDesktopServices.openUrl(QUrl(url))
return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
def _openContextMenu(self, point: QPoint) -> None: def _openContextMenu(self, point: QPoint) -> None:
@@ -430,8 +415,6 @@ class GuiDocViewer(QTextBrowser):
ctxMenu.setParent(None) ctxMenu.setParent(None)
return
## ##
# Events # Events
## ##
@@ -440,7 +423,6 @@ class GuiDocViewer(QTextBrowser):
"""Update document margins when widget is resized.""" """Update document margins when widget is resized."""
self.updateDocMargins() self.updateDocMargins()
super().resizeEvent(event) super().resizeEvent(event)
return
def mouseReleaseEvent(self, event: QMouseEvent) -> None: def mouseReleaseEvent(self, event: QMouseEvent) -> None:
"""Capture mouse click events on the document.""" """Capture mouse click events on the document."""
@@ -450,7 +432,6 @@ class GuiDocViewer(QTextBrowser):
self.navForward() self.navForward()
else: else:
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
return
def dragEnterEvent(self, event: QDragEnterEvent) -> None: def dragEnterEvent(self, event: QDragEnterEvent) -> None:
"""Overload drag enter event to handle dragged items.""" """Overload drag enter event to handle dragged items."""
@@ -458,7 +439,6 @@ class GuiDocViewer(QTextBrowser):
event.acceptProposedAction() event.acceptProposedAction()
else: else:
super().dragEnterEvent(event) super().dragEnterEvent(event)
return
def dragMoveEvent(self, event: QDragMoveEvent) -> None: def dragMoveEvent(self, event: QDragMoveEvent) -> None:
"""Overload drag move event to handle dragged items.""" """Overload drag move event to handle dragged items."""
@@ -466,7 +446,6 @@ class GuiDocViewer(QTextBrowser):
event.acceptProposedAction() event.acceptProposedAction()
else: else:
super().dragMoveEvent(event) super().dragMoveEvent(event)
return
def dropEvent(self, event: QDropEvent) -> None: def dropEvent(self, event: QDropEvent) -> None:
"""Overload drop event to handle dragged items.""" """Overload drop event to handle dragged items."""
@@ -476,7 +455,6 @@ class GuiDocViewer(QTextBrowser):
self.openDocumentRequest.emit(handles[0], nwDocMode.VIEW, "", True) self.openDocumentRequest.emit(handles[0], nwDocMode.VIEW, "", True)
else: else:
super().dropEvent(event) super().dropEvent(event)
return
## ##
# Internal Functions # Internal Functions
@@ -500,16 +478,18 @@ class GuiDocViewer(QTextBrowser):
self.setTextCursor(cursor) self.setTextCursor(cursor)
return
def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None: def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None:
"""Handle text selection at a given location.""" """Handle text selection at a given location."""
self.setTextCursor(self.cursorForPosition(pos)) self.setTextCursor(self.cursorForPosition(pos))
self._makeSelection(selType) self._makeSelection(selType)
return
class GuiDocViewHistory: class GuiDocViewHistory:
"""GUI: Document Viewer History.
This class holds the navigation history for the viewer panel, which
is used for backward/forward navigation.
"""
def __init__(self, docViewer: GuiDocViewer) -> None: def __init__(self, docViewer: GuiDocViewer) -> None:
self.docViewer = docViewer self.docViewer = docViewer
@@ -517,7 +497,6 @@ class GuiDocViewHistory:
self._posHistory = [] self._posHistory = []
self._currPos = -1 self._currPos = -1
self._prevPos = -1 self._prevPos = -1
return
def clear(self) -> None: def clear(self) -> None:
"""Clear the view history.""" """Clear the view history."""
@@ -526,7 +505,6 @@ class GuiDocViewHistory:
self._posHistory = [] self._posHistory = []
self._currPos = -1 self._currPos = -1
self._prevPos = -1 self._prevPos = -1
return
def append(self, tHandle: str) -> bool: def append(self, tHandle: str) -> bool:
"""Append a document handle and its scroll bar position to the """Append a document handle and its scroll bar position to the
@@ -566,7 +544,6 @@ class GuiDocViewHistory:
self._currPos = newPos self._currPos = newPos
self._updateNavButtons() self._updateNavButtons()
self._dumpHistory() self._dumpHistory()
return
def backward(self) -> None: def backward(self) -> None:
"""Navigate to the previous entry in the view history.""" """Navigate to the previous entry in the view history."""
@@ -580,7 +557,6 @@ class GuiDocViewHistory:
self._currPos = newPos self._currPos = newPos
self._updateNavButtons() self._updateNavButtons()
self._dumpHistory() self._dumpHistory()
return
## ##
# Internal Functions # Internal Functions
@@ -590,12 +566,10 @@ class GuiDocViewHistory:
"""Update the scrollbar position of the previous entry.""" """Update the scrollbar position of the previous entry."""
if self._prevPos >= 0 and self._prevPos < len(self._posHistory): if self._prevPos >= 0 and self._prevPos < len(self._posHistory):
self._posHistory[self._prevPos] = self.docViewer.scrollPosition self._posHistory[self._prevPos] = self.docViewer.scrollPosition
return
def _updateNavButtons(self) -> None: def _updateNavButtons(self) -> None:
"""Update the navigation buttons in the document header.""" """Update the navigation buttons in the document header."""
self.docViewer.docHeader.updateNavButtons(0, len(self._navHistory) - 1, self._currPos) self.docViewer.docHeader.updateNavButtons(0, len(self._navHistory) - 1, self._currPos)
return
def _truncateHistory(self, atPos: int) -> None: def _truncateHistory(self, atPos: int) -> None:
"""Truncate the navigation history to the given position. Also """Truncate the navigation history to the given position. Also
@@ -606,7 +580,6 @@ class GuiDocViewHistory:
self._posHistory = self._posHistory[nSkip:atPos + 1] self._posHistory = self._posHistory[nSkip:atPos + 1]
self._currPos -= nSkip self._currPos -= nSkip
self._prevPos -= nSkip self._prevPos -= nSkip
return
def _dumpHistory(self) -> None: def _dumpHistory(self) -> None:
"""Debug function to dump history to the logger. Since it is a """Debug function to dump history to the logger. Since it is a
@@ -616,11 +589,10 @@ class GuiDocViewHistory:
for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory, strict=False)): for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory, strict=False)):
a = ">" if i == self._currPos else " " a = ">" if i == self._currPos else " "
logger.debug(f"History {i + 1:02d}: {a} {h:13s} [x:{p}]") logger.debug(f"History {i + 1:02d}: {a} {h:13s} [x:{p}]")
return
class GuiDocViewHeader(QWidget): class GuiDocViewHeader(QWidget):
"""The Embedded Document Header """The Embedded Document Header.
Only used by DocViewer, and is at a fixed position in the Only used by DocViewer, and is at a fixed position in the
QTextBrowser's viewport. QTextBrowser's viewport.
@@ -711,8 +683,6 @@ class GuiDocViewHeader(QWidget):
logger.debug("Ready: GuiDocViewHeader") logger.debug("Ready: GuiDocViewHeader")
return
## ##
# Methods # Methods
## ##
@@ -730,7 +700,6 @@ class GuiDocViewHeader(QWidget):
self.editButton.setVisible(False) self.editButton.setVisible(False)
self.refreshButton.setVisible(False) self.refreshButton.setVisible(False)
self.closeButton.setVisible(False) self.closeButton.setVisible(False)
return
def setOutline(self, data: dict[str, tuple[str, int]]) -> None: def setOutline(self, data: dict[str, tuple[str, int]]) -> None:
"""Set the document outline dataset.""" """Set the document outline dataset."""
@@ -750,13 +719,11 @@ class GuiDocViewHeader(QWidget):
lambda _, title=title: self.docViewer.navigateTo(f"#{tHandle}:{title}") lambda _, title=title: self.docViewer.navigateTo(f"#{tHandle}:{title}")
) )
self._docOutline = data self._docOutline = data
return
def updateFont(self) -> None: def updateFont(self) -> None:
"""Update the font settings.""" """Update the font settings."""
self.setFont(SHARED.theme.guiFont) self.setFont(SHARED.theme.guiFont)
self.itemTitle.setFont(SHARED.theme.guiFontSmall) self.itemTitle.setFont(SHARED.theme.guiFontSmall)
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
@@ -777,8 +744,6 @@ class GuiDocViewHeader(QWidget):
self.matchColors() self.matchColors()
return
def matchColors(self) -> None: def matchColors(self) -> None:
"""Update the colours of the widget to match those of the syntax """Update the colours of the widget to match those of the syntax
theme rather than the main GUI. theme rather than the main GUI.
@@ -792,15 +757,13 @@ class GuiDocViewHeader(QWidget):
self.itemTitle.setTextColors( self.itemTitle.setTextColors(
color=palette.windowText().color(), faded=SHARED.theme.fadedText color=palette.windowText().color(), faded=SHARED.theme.fadedText
) )
return
def changeFocusState(self, state: bool) -> None: def changeFocusState(self, state: bool) -> None:
"""Toggle focus state.""" """Toggle focus state."""
self.itemTitle.setColorState(state) self.itemTitle.setColorState(state)
return
def setHandle(self, tHandle: str) -> None: def setHandle(self, tHandle: str) -> None:
"""Sets the document title from the handle, or alternatively, """Set the document title from the handle, or alternatively,
set the whole document path. set the whole document path.
""" """
self._docHandle = tHandle self._docHandle = tHandle
@@ -819,13 +782,10 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setVisible(True) self.refreshButton.setVisible(True)
self.closeButton.setVisible(True) self.closeButton.setVisible(True)
return
def updateNavButtons(self, firstIdx: int, lastIdx: int, currIdx: int) -> None: def updateNavButtons(self, firstIdx: int, lastIdx: int, currIdx: int) -> None:
"""Enable and disable nav buttons based on index in history.""" """Enable and disable nav buttons based on index in history."""
self.backButton.setEnabled(currIdx > firstIdx) self.backButton.setEnabled(currIdx > firstIdx)
self.forwardButton.setEnabled(currIdx < lastIdx) self.forwardButton.setEnabled(currIdx < lastIdx)
return
## ##
# Private Slots # Private Slots
@@ -836,20 +796,17 @@ class GuiDocViewHeader(QWidget):
"""Trigger the close editor/viewer on the main window.""" """Trigger the close editor/viewer on the main window."""
self.clearHeader() self.clearHeader()
self.docViewer.closeDocumentRequest.emit() self.docViewer.closeDocumentRequest.emit()
return
@pyqtSlot() @pyqtSlot()
def _refreshDocument(self) -> None: def _refreshDocument(self) -> None:
"""Reload the content of the document.""" """Reload the content of the document."""
self.docViewer.reloadDocumentRequest.emit() self.docViewer.reloadDocumentRequest.emit()
return
@pyqtSlot() @pyqtSlot()
def _editDocument(self) -> None: def _editDocument(self) -> None:
"""Open the document in the editor.""" """Open the document in the editor."""
if tHandle := self._docHandle: if tHandle := self._docHandle:
self.docViewer.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True) self.docViewer.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
return
## ##
# Events # Events
@@ -861,11 +818,10 @@ class GuiDocViewHeader(QWidget):
""" """
if event.button() == QtMouseLeft: if event.button() == QtMouseLeft:
self.docViewer.requestProjectItemSelected.emit(self._docHandle, True) self.docViewer.requestProjectItemSelected.emit(self._docHandle, True)
return
class GuiDocViewFooter(QWidget): class GuiDocViewFooter(QWidget):
"""The Embedded Document Footer """The Embedded Document Footer.
Only used by DocViewer, and is at a fixed position in the Only used by DocViewer, and is at a fixed position in the
QTextBrowser's viewport. QTextBrowser's viewport.
@@ -944,8 +900,6 @@ class GuiDocViewFooter(QWidget):
logger.debug("Ready: GuiDocViewFooter") logger.debug("Ready: GuiDocViewFooter")
return
## ##
# Methods # Methods
## ##
@@ -956,7 +910,6 @@ class GuiDocViewFooter(QWidget):
self.showComments.setFont(SHARED.theme.guiFontSmall) self.showComments.setFont(SHARED.theme.guiFontSmall)
self.showSynopsis.setFont(SHARED.theme.guiFontSmall) self.showSynopsis.setFont(SHARED.theme.guiFontSmall)
self.showNotes.setFont(SHARED.theme.guiFontSmall) self.showNotes.setFont(SHARED.theme.guiFontSmall)
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
@@ -977,8 +930,6 @@ class GuiDocViewFooter(QWidget):
self.matchColors() self.matchColors()
return
def matchColors(self) -> None: def matchColors(self) -> None:
"""Update the colours of the widget to match those of the syntax """Update the colours of the widget to match those of the syntax
theme rather than the main GUI. theme rather than the main GUI.
@@ -989,7 +940,6 @@ class GuiDocViewFooter(QWidget):
palette.setColor(QPalette.ColorRole.WindowText, syntax.text) palette.setColor(QPalette.ColorRole.WindowText, syntax.text)
palette.setColor(QPalette.ColorRole.Text, syntax.text) palette.setColor(QPalette.ColorRole.Text, syntax.text)
self.setPalette(palette) self.setPalette(palette)
return
## ##
# Private Slots # Private Slots
@@ -1000,18 +950,15 @@ class GuiDocViewFooter(QWidget):
"""Toggle the view comment button and reload the document.""" """Toggle the view comment button and reload the document."""
CONFIG.viewComments = state CONFIG.viewComments = state
self.docViewer.reloadText() self.docViewer.reloadText()
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _doToggleSynopsis(self, state: bool) -> None: def _doToggleSynopsis(self, state: bool) -> None:
"""Toggle the view synopsis button and reload the document.""" """Toggle the view synopsis button and reload the document."""
CONFIG.viewSynopsis = state CONFIG.viewSynopsis = state
self.docViewer.reloadText() self.docViewer.reloadText()
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _doToggleNotes(self, state: bool) -> None: def _doToggleNotes(self, state: bool) -> None:
"""Toggle the view notes button and reload the document.""" """Toggle the view notes button and reload the document."""
CONFIG.viewNotes = state CONFIG.viewNotes = state
self.docViewer.reloadText() self.docViewer.reloadText()
return
+5 -34
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -49,6 +49,10 @@ logger = logging.getLogger(__name__)
class GuiDocViewerPanel(QWidget): class GuiDocViewerPanel(QWidget):
"""GUI: Document Viewer Panel.
The panel of project meta data below the viewer.
"""
openDocumentRequest = pyqtSignal(str, Enum, str, bool) openDocumentRequest = pyqtSignal(str, Enum, str, bool)
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
@@ -96,8 +100,6 @@ class GuiDocViewerPanel(QWidget):
logger.debug("Ready: GuiDocViewerPanel") logger.debug("Ready: GuiDocViewerPanel")
return
## ##
# Methods # Methods
## ##
@@ -113,7 +115,6 @@ class GuiDocViewerPanel(QWidget):
for tab in self.kwTabs.values(): for tab in self.kwTabs.values():
tab.updateTheme() tab.updateTheme()
self._loadAllTags() self._loadAllTags()
return
def openProjectTasks(self) -> None: def openProjectTasks(self) -> None:
"""Run open project tasks.""" """Run open project tasks."""
@@ -124,7 +125,6 @@ class GuiDocViewerPanel(QWidget):
for key, value in colWidths.items(): for key, value in colWidths.items():
if key in self.kwTabs and isinstance(value, list): if key in self.kwTabs and isinstance(value, list):
self.kwTabs[key].setColumnWidths(value) self.kwTabs[key].setColumnWidths(value)
return
def closeProjectTasks(self) -> None: def closeProjectTasks(self) -> None:
"""Run close project tasks.""" """Run close project tasks."""
@@ -133,7 +133,6 @@ class GuiDocViewerPanel(QWidget):
hideInactive = self.aInactive.isChecked() hideInactive = self.aInactive.isChecked()
SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", colWidths) SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", colWidths)
SHARED.project.options.setValue("GuiDocViewerPanel", "hideInactive", hideInactive) SHARED.project.options.setValue("GuiDocViewerPanel", "hideInactive", hideInactive)
return
## ##
# Public Slots # Public Slots
@@ -145,7 +144,6 @@ class GuiDocViewerPanel(QWidget):
self.tabBackRefs.clearContent() self.tabBackRefs.clearContent()
for cTab in self.kwTabs.values(): for cTab in self.kwTabs.values():
cTab.clearContent() cTab.clearContent()
return
@pyqtSlot() @pyqtSlot()
def indexHasAppeared(self) -> None: def indexHasAppeared(self) -> None:
@@ -153,7 +151,6 @@ class GuiDocViewerPanel(QWidget):
self._loadAllTags() self._loadAllTags()
self._updateTabVisibility() self._updateTabVisibility()
self.updateHandle(self._lastHandle) self.updateHandle(self._lastHandle)
return
@pyqtSlot(str, Enum) @pyqtSlot(str, Enum)
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None: def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
@@ -168,14 +165,12 @@ class GuiDocViewerPanel(QWidget):
else: else:
self.kwTabs[tClass].removeEntry(key) self.kwTabs[tClass].removeEntry(key)
self._updateTabVisibility() self._updateTabVisibility()
return
@pyqtSlot(str) @pyqtSlot(str)
def updateHandle(self, tHandle: str | None) -> None: def updateHandle(self, tHandle: str | None) -> None:
"""Update the document handle.""" """Update the document handle."""
self._lastHandle = tHandle self._lastHandle = tHandle
self.tabBackRefs.refreshContent(tHandle or None) self.tabBackRefs.refreshContent(tHandle or None)
return
@pyqtSlot(list, list) @pyqtSlot(list, list)
def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None: def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
@@ -191,14 +186,12 @@ class GuiDocViewerPanel(QWidget):
else: else:
logger.warning("Could not remove tag '%s' from view panel", key) logger.warning("Could not remove tag '%s' from view panel", key)
self._updateTabVisibility() self._updateTabVisibility()
return
@pyqtSlot(str) @pyqtSlot(str)
def updateStatusLabels(self, kind: str) -> None: def updateStatusLabels(self, kind: str) -> None:
"""Update the importance labels.""" """Update the importance labels."""
if kind == "i": if kind == "i":
self._loadAllTags() self._loadAllTags()
return
## ##
# Private Slots # Private Slots
@@ -212,7 +205,6 @@ class GuiDocViewerPanel(QWidget):
cTab.clearContent() cTab.clearContent()
self._loadAllTags() self._loadAllTags()
self._updateTabVisibility() self._updateTabVisibility()
return
## ##
# Internal Functions # Internal Functions
@@ -222,7 +214,6 @@ class GuiDocViewerPanel(QWidget):
"""Hide class tabs with no content.""" """Hide class tabs with no content."""
for tClass, cTab in self.kwTabs.items(): for tClass, cTab in self.kwTabs.items():
self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0) self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
return
def _loadAllTags(self) -> None: def _loadAllTags(self) -> None:
"""Load all tags into the tabs.""" """Load all tags into the tabs."""
@@ -230,7 +221,6 @@ class GuiDocViewerPanel(QWidget):
for key, name, tClass, iItem, hItem in data: for key, name, tClass, iItem, hItem in data:
if tClass in self.kwTabs and iItem and hItem: if tClass in self.kwTabs and iItem and hItem:
self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem) self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
return
class _ViewPanelBackRefs(QTreeWidget): class _ViewPanelBackRefs(QTreeWidget):
@@ -278,8 +268,6 @@ class _ViewPanelBackRefs(QTreeWidget):
self.clicked.connect(self._treeItemClicked) self.clicked.connect(self._treeItemClicked)
self.doubleClicked.connect(self._treeItemDoubleClicked) self.doubleClicked.connect(self._treeItemDoubleClicked)
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self._editIcon = SHARED.theme.getIcon("edit", "green") self._editIcon = SHARED.theme.getIcon("edit", "green")
@@ -288,13 +276,11 @@ class _ViewPanelBackRefs(QTreeWidget):
if item := self.topLevelItem(i): if item := self.topLevelItem(i):
item.setIcon(self.C_EDIT, self._editIcon) item.setIcon(self.C_EDIT, self._editIcon)
item.setIcon(self.C_VIEW, self._viewIcon) item.setIcon(self.C_VIEW, self._viewIcon)
return
def clearContent(self) -> None: def clearContent(self) -> None:
"""Clear the widget.""" """Clear the widget."""
self.clear() self.clear()
self._treeMap = {} self._treeMap = {}
return
def refreshContent(self, dHandle: str | None) -> None: def refreshContent(self, dHandle: str | None) -> None:
"""Update the content.""" """Update the content."""
@@ -303,7 +289,6 @@ class _ViewPanelBackRefs(QTreeWidget):
refs = SHARED.project.index.getBackReferenceList(dHandle) refs = SHARED.project.index.getBackReferenceList(dHandle)
for tHandle, (sTitle, hItem) in refs.items(): for tHandle, (sTitle, hItem) in refs.items():
self._setTreeItemValues(tHandle, sTitle, hItem) self._setTreeItemValues(tHandle, sTitle, hItem)
return
def refreshDocument(self, tHandle: str) -> None: def refreshDocument(self, tHandle: str) -> None:
"""Refresh document meta data.""" """Refresh document meta data."""
@@ -311,7 +296,6 @@ class _ViewPanelBackRefs(QTreeWidget):
for sTitle, hItem in iItem.items(): for sTitle, hItem in iItem.items():
if f"{tHandle}:{sTitle}" in self._treeMap: if f"{tHandle}:{sTitle}" in self._treeMap:
self._setTreeItemValues(tHandle, sTitle, hItem) self._setTreeItemValues(tHandle, sTitle, hItem)
return
## ##
# Private Slots # Private Slots
@@ -325,7 +309,6 @@ class _ViewPanelBackRefs(QTreeWidget):
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True) self._parent.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
elif index.column() == self.C_VIEW: elif index.column() == self.C_VIEW:
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True) self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
return
@pyqtSlot("QModelIndex") @pyqtSlot("QModelIndex")
def _treeItemDoubleClicked(self, index: QModelIndex) -> None: def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
@@ -333,7 +316,6 @@ class _ViewPanelBackRefs(QTreeWidget):
tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE) tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
if index.column() not in (self.C_EDIT, self.C_VIEW): if index.column() not in (self.C_EDIT, self.C_VIEW):
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True) self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
return
## ##
# Internal Functions # Internal Functions
@@ -362,8 +344,6 @@ class _ViewPanelBackRefs(QTreeWidget):
self.addTopLevelItem(trItem) self.addTopLevelItem(trItem)
self._treeMap[tKey] = trItem self._treeMap[tKey] = trItem
return
class _ViewPanelKeyWords(QTreeWidget): class _ViewPanelKeyWords(QTreeWidget):
@@ -418,14 +398,11 @@ class _ViewPanelKeyWords(QTreeWidget):
self.clicked.connect(self._treeItemClicked) self.clicked.connect(self._treeItemClicked)
self.doubleClicked.connect(self._treeItemDoubleClicked) self.doubleClicked.connect(self._treeItemDoubleClicked)
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[self._class], "root") self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[self._class], "root")
self._editIcon = SHARED.theme.getIcon("edit", "green") self._editIcon = SHARED.theme.getIcon("edit", "green")
self._viewIcon = SHARED.theme.getIcon("view", "blue") self._viewIcon = SHARED.theme.getIcon("view", "blue")
return
def countEntries(self) -> int: def countEntries(self) -> int:
"""Return the number of items in the list.""" """Return the number of items in the list."""
@@ -435,7 +412,6 @@ class _ViewPanelKeyWords(QTreeWidget):
"""Clear the list.""" """Clear the list."""
self._treeMap = {} self._treeMap = {}
self.clear() self.clear()
return
def addUpdateEntry(self, tag: str, name: str, iItem: IndexNode, hItem: IndexHeading) -> None: def addUpdateEntry(self, tag: str, name: str, iItem: IndexNode, hItem: IndexHeading) -> None:
"""Add a new entry, or update an existing one.""" """Add a new entry, or update an existing one."""
@@ -470,8 +446,6 @@ class _ViewPanelKeyWords(QTreeWidget):
self.addTopLevelItem(trItem) self.addTopLevelItem(trItem)
self._treeMap[tag] = trItem self._treeMap[tag] = trItem
return
def removeEntry(self, tag: str) -> bool: def removeEntry(self, tag: str) -> bool:
"""Remove a tag from the list.""" """Remove a tag from the list."""
if tag in self._treeMap: if tag in self._treeMap:
@@ -487,7 +461,6 @@ class _ViewPanelKeyWords(QTreeWidget):
self.setColumnWidth(self.C_IMPORT, checkInt(widths[1], 100)) self.setColumnWidth(self.C_IMPORT, checkInt(widths[1], 100))
self.setColumnWidth(self.C_DOC, checkInt(widths[2], 100)) self.setColumnWidth(self.C_DOC, checkInt(widths[2], 100))
self.setColumnWidth(self.C_TITLE, checkInt(widths[3], 100)) self.setColumnWidth(self.C_TITLE, checkInt(widths[3], 100))
return
def getColumnWidths(self) -> list[int]: def getColumnWidths(self) -> list[int]:
"""Get the widths of the user-adjustable columns.""" """Get the widths of the user-adjustable columns."""
@@ -510,7 +483,6 @@ class _ViewPanelKeyWords(QTreeWidget):
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT) self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT)
elif index.column() == self.C_VIEW: elif index.column() == self.C_VIEW:
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW) self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
return
@pyqtSlot("QModelIndex") @pyqtSlot("QModelIndex")
def _treeItemDoubleClicked(self, index: QModelIndex) -> None: def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
@@ -518,4 +490,3 @@ class _ViewPanelKeyWords(QTreeWidget):
tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG) tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
if index.column() not in (self.C_EDIT, self.C_VIEW): if index.column() not in (self.C_EDIT, self.C_VIEW):
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW) self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
return
+6 -7
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -42,6 +42,11 @@ logger = logging.getLogger(__name__)
class GuiTextDocument(QTextDocument): class GuiTextDocument(QTextDocument):
"""Custom: Modified QTextDocument.
A special text document format that incorporates a few additional
features including spell checking.
"""
def __init__(self, parent: QObject) -> None: def __init__(self, parent: QObject) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -52,11 +57,8 @@ class GuiTextDocument(QTextDocument):
logger.debug("Ready: GuiTextDocument") logger.debug("Ready: GuiTextDocument")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiTextDocument") logger.debug("Delete: GuiTextDocument")
return
## ##
# Properties # Properties
@@ -96,8 +98,6 @@ class GuiTextDocument(QTextDocument):
logger.debug("Loaded %d text blocks in %.3f ms", count, 1000*(tMid - tStart)) logger.debug("Loaded %d text blocks in %.3f ms", count, 1000*(tMid - tStart))
logger.debug("Highlighted document in %.3f ms", 1000*(tEnd - tMid)) logger.debug("Highlighted document in %.3f ms", 1000*(tEnd - tMid))
return
def metaDataAtPos(self, pos: int) -> tuple[str, str]: def metaDataAtPos(self, pos: int) -> tuple[str, str]:
"""Check if there is meta data available at a given position in """Check if there is meta data available at a given position in
the document, and if so, return it. the document, and if so, return it.
@@ -146,4 +146,3 @@ class GuiTextDocument(QTextDocument):
def setSpellCheckState(self, state: bool) -> None: def setSpellCheckState(self, state: bool) -> None:
"""Set the spell check state of the syntax highlighter.""" """Set the spell check state of the syntax highlighter."""
self._syntax.setSpellCheck(state) self._syntax.setSpellCheck(state)
return
+2 -6
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -43,6 +43,7 @@ logger = logging.getLogger(__name__)
class GuiItemDetails(QWidget): class GuiItemDetails(QWidget):
"""GUI: Project Item Details Panel."""
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -190,8 +191,6 @@ class GuiItemDetails(QWidget):
logger.debug("Ready: GuiItemDetails") logger.debug("Ready: GuiItemDetails")
return
### ###
# Class Methods # Class Methods
## ##
@@ -210,7 +209,6 @@ class GuiItemDetails(QWidget):
self.cCountData.clear() self.cCountData.clear()
self.wCountData.clear() self.wCountData.clear()
self.pCountData.clear() self.pCountData.clear()
return
def refreshDetails(self) -> None: def refreshDetails(self) -> None:
"""Reload the content of the details panel.""" """Reload the content of the details panel."""
@@ -219,7 +217,6 @@ class GuiItemDetails(QWidget):
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self.updateViewBox(self._handle) self.updateViewBox(self._handle)
return
def updateViewBox(self, tHandle: str | None) -> None: def updateViewBox(self, tHandle: str | None) -> None:
"""Populate the details box from a given handle.""" """Populate the details box from a given handle."""
@@ -283,4 +280,3 @@ class GuiItemDetails(QWidget):
self.updateViewBox(tHandle) self.updateViewBox(tHandle)
elif change == nwChange.DELETE: elif change == nwChange.DELETE:
self.updateViewBox(None) self.updateViewBox(None)
return
+1 -25
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -82,8 +82,6 @@ class GuiMainMenu(QMenuBar):
logger.debug("Ready: GuiMainMenu") logger.debug("Ready: GuiMainMenu")
return
## ##
# Public Slots # Public Slots
## ##
@@ -92,7 +90,6 @@ class GuiMainMenu(QMenuBar):
def setSpellCheckState(self, state: bool) -> None: def setSpellCheckState(self, state: bool) -> None:
"""Forward spell check check state to its action.""" """Forward spell check check state to its action."""
self.aSpellCheck.setChecked(state) self.aSpellCheck.setChecked(state)
return
## ##
# Private Slots # Private Slots
@@ -105,21 +102,18 @@ class GuiMainMenu(QMenuBar):
decision, just pass a None to the function and let it decide. decision, just pass a None to the function and let it decide.
""" """
self.mainGui.docEditor.toggleSpellCheck(None) self.mainGui.docEditor.toggleSpellCheck(None)
return
@pyqtSlot() @pyqtSlot()
def _openUserManualFile(self) -> None: def _openUserManualFile(self) -> None:
"""Open the documentation in PDF format.""" """Open the documentation in PDF format."""
if isinstance(CONFIG.pdfDocs, Path): if isinstance(CONFIG.pdfDocs, Path):
openExternalPath(CONFIG.pdfDocs) openExternalPath(CONFIG.pdfDocs)
return
@pyqtSlot(str) @pyqtSlot(str)
def _changeSpelling(self, language: str) -> None: def _changeSpelling(self, language: str) -> None:
"""Change the spell check language.""" """Change the spell check language."""
SHARED.project.data.setSpellLang(language) SHARED.project.data.setSpellLang(language)
SHARED.updateSpellCheckLanguage() SHARED.updateSpellCheckLanguage()
return
## ##
# Internal Functions # Internal Functions
@@ -188,8 +182,6 @@ class GuiMainMenu(QMenuBar):
self.aExitNW.triggered.connect(qtLambda(self.mainGui.closeMain)) self.aExitNW.triggered.connect(qtLambda(self.mainGui.closeMain))
self.mainGui.addAction(self.aExitNW) self.mainGui.addAction(self.aExitNW)
return
def _buildDocumentMenu(self) -> None: def _buildDocumentMenu(self) -> None:
"""Assemble the Document menu.""" """Assemble the Document menu."""
# Document # Document
@@ -236,8 +228,6 @@ class GuiMainMenu(QMenuBar):
self.aImportFile = qtAddAction(self.docuMenu, self.tr("Import Text from File")) self.aImportFile = qtAddAction(self.docuMenu, self.tr("Import Text from File"))
self.aImportFile.triggered.connect(qtLambda(self.mainGui.importDocument)) self.aImportFile.triggered.connect(qtLambda(self.mainGui.importDocument))
return
def _buildEditMenu(self) -> None: def _buildEditMenu(self) -> None:
"""Assemble the Edit menu.""" """Assemble the Edit menu."""
# Edit # Edit
@@ -305,8 +295,6 @@ class GuiMainMenu(QMenuBar):
) )
self.mainGui.addAction(self.aSelectPar) self.mainGui.addAction(self.aSelectPar)
return
def _buildViewMenu(self) -> None: def _buildViewMenu(self) -> None:
"""Assemble the View menu.""" """Assemble the View menu."""
# View # View
@@ -367,8 +355,6 @@ class GuiMainMenu(QMenuBar):
self.aFullScreen.triggered.connect(self.mainGui.toggleFullScreenMode) self.aFullScreen.triggered.connect(self.mainGui.toggleFullScreenMode)
self.mainGui.addAction(self.aFullScreen) self.mainGui.addAction(self.aFullScreen)
return
def _buildInsertMenu(self) -> None: def _buildInsertMenu(self) -> None:
"""Assemble the Insert menu.""" """Assemble the Insert menu."""
# Insert # Insert
@@ -646,8 +632,6 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocInsert.emit(nwDocInsert.FOOTNOTE) lambda: self.requestDocInsert.emit(nwDocInsert.FOOTNOTE)
) )
return
def _buildFormatMenu(self) -> None: def _buildFormatMenu(self) -> None:
"""Assemble the Format menu.""" """Assemble the Format menu."""
# Format # Format
@@ -901,8 +885,6 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocAction.emit(nwDocAction.RM_BREAKS) lambda: self.requestDocAction.emit(nwDocAction.RM_BREAKS)
) )
return
def _buildSearchMenu(self) -> None: def _buildSearchMenu(self) -> None:
"""Assemble the Search menu.""" """Assemble the Search menu."""
# Search # Search
@@ -948,8 +930,6 @@ class GuiMainMenu(QMenuBar):
self.aFindProj.setShortcut("Ctrl+Shift+F") self.aFindProj.setShortcut("Ctrl+Shift+F")
self.aFindProj.triggered.connect(qtLambda(self.requestViewChange.emit, nwView.SEARCH)) self.aFindProj.triggered.connect(qtLambda(self.requestViewChange.emit, nwView.SEARCH))
return
def _buildToolsMenu(self) -> None: def _buildToolsMenu(self) -> None:
"""Assemble the Tools menu.""" """Assemble the Tools menu."""
# Tools # Tools
@@ -1019,8 +999,6 @@ class GuiMainMenu(QMenuBar):
self.aPreferences.triggered.connect(self.mainGui.showPreferencesDialog) self.aPreferences.triggered.connect(self.mainGui.showPreferencesDialog)
self.mainGui.addAction(self.aPreferences) self.mainGui.addAction(self.aPreferences)
return
def _buildHelpMenu(self) -> None: def _buildHelpMenu(self) -> None:
"""Assemble the Help menu.""" """Assemble the Help menu."""
# Help # Help
@@ -1066,5 +1044,3 @@ class GuiMainMenu(QMenuBar):
# Document > Main Website # Document > Main Website
self.aWebsite = qtAddAction(self.helpMenu, self.tr("The novelWriter Website")) self.aWebsite = qtAddAction(self.helpMenu, self.tr("The novelWriter Website"))
self.aWebsite.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_WEB)) self.aWebsite.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_WEB))
return
+7 -46
View File
@@ -24,7 +24,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -55,6 +55,7 @@ logger = logging.getLogger(__name__)
class GuiNovelView(QWidget): class GuiNovelView(QWidget):
"""GUI: Novel View Panel."""
# Signals for user interaction with the novel tree # Signals for user interaction with the novel tree
selectedItemChanged = pyqtSignal(str) selectedItemChanged = pyqtSignal(str)
@@ -82,8 +83,6 @@ class GuiNovelView(QWidget):
self.getSelectedHandle = self.novelTree.getSelectedHandle self.getSelectedHandle = self.novelTree.getSelectedHandle
self.refreshCurrentTree = self.novelBar.forceRefreshNovelTree self.refreshCurrentTree = self.novelBar.forceRefreshNovelTree
return
## ##
# Methods # Methods
## ##
@@ -91,19 +90,16 @@ class GuiNovelView(QWidget):
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self.novelBar.updateTheme() self.novelBar.updateTheme()
return
def initSettings(self) -> None: def initSettings(self) -> None:
"""Initialise GUI elements that depend on specific settings.""" """Initialise GUI elements that depend on specific settings."""
self.novelTree.initSettings() self.novelTree.initSettings()
return
def clearNovelView(self) -> None: def clearNovelView(self) -> None:
"""Clear project-related GUI content.""" """Clear project-related GUI content."""
self.novelBar.clearContent() self.novelBar.clearContent()
self.novelBar.setEnabled(False) self.novelBar.setEnabled(False)
self.novelTree.clearContent() self.novelTree.clearContent()
return
def openProjectTasks(self) -> None: def openProjectTasks(self) -> None:
"""Run open project tasks.""" """Run open project tasks."""
@@ -125,8 +121,6 @@ class GuiNovelView(QWidget):
self.novelTree.setLastColSize(lastColSize) self.novelTree.setLastColSize(lastColSize)
return
def closeProjectTasks(self) -> None: def closeProjectTasks(self) -> None:
"""Run closing project tasks.""" """Run closing project tasks."""
logger.debug("Saving State: GuiNovelView") logger.debug("Saving State: GuiNovelView")
@@ -140,12 +134,9 @@ class GuiNovelView(QWidget):
self.clearNovelView() self.clearNovelView()
return
def setTreeFocus(self) -> None: def setTreeFocus(self) -> None:
"""Set the focus to the tree widget.""" """Set the focus to the tree widget."""
self.novelTree.setFocus() self.novelTree.setFocus()
return
def treeHasFocus(self) -> bool: def treeHasFocus(self) -> bool:
"""Check if the novel tree has focus.""" """Check if the novel tree has focus."""
@@ -159,22 +150,20 @@ class GuiNovelView(QWidget):
def setCurrentNovel(self, rootHandle: str | None) -> None: def setCurrentNovel(self, rootHandle: str | None) -> None:
"""Set the current novel to display.""" """Set the current novel to display."""
self.novelTree.setNovelModel(rootHandle) self.novelTree.setNovelModel(rootHandle)
return
@pyqtSlot(str) @pyqtSlot(str)
def setActiveHandle(self, tHandle: str) -> None: def setActiveHandle(self, tHandle: str) -> None:
"""Highlight the rows associated with a given handle.""" """Highlight the rows associated with a given handle."""
self.novelTree.setActiveHandle(tHandle) self.novelTree.setActiveHandle(tHandle)
return
@pyqtSlot(str, Enum) @pyqtSlot(str, Enum)
def updateRootItem(self, tHandle: str, change: nwChange) -> None: def updateRootItem(self, tHandle: str, change: nwChange) -> None:
"""If any root item changes, rebuild the novel root menu.""" """If any root item changes, rebuild the novel root menu."""
self.novelBar.buildNovelRootMenu() self.novelBar.buildNovelRootMenu()
return
class GuiNovelToolBar(QWidget): class GuiNovelToolBar(QWidget):
"""GUI: Novel View Panel ToolBar."""
def __init__(self, novelView: GuiNovelView) -> None: def __init__(self, novelView: GuiNovelView) -> None:
super().__init__(parent=novelView) super().__init__(parent=novelView)
@@ -249,8 +238,6 @@ class GuiNovelToolBar(QWidget):
logger.debug("Ready: GuiNovelToolBar") logger.debug("Ready: GuiNovelToolBar")
return
## ##
# Methods # Methods
## ##
@@ -277,20 +264,16 @@ class GuiNovelToolBar(QWidget):
self.forceRefreshNovelTree() self.forceRefreshNovelTree()
return
def clearContent(self) -> None: def clearContent(self) -> None:
"""Run clearing project tasks.""" """Run clearing project tasks."""
self.novelValue.clear() self.novelValue.clear()
self.novelValue.setToolTip("") self.novelValue.setToolTip("")
return
def buildNovelRootMenu(self) -> None: def buildNovelRootMenu(self) -> None:
"""Build the novel root menu.""" """Build the novel root menu."""
self.novelValue.refreshNovelList() self.novelValue.refreshNovelList()
self.novelView.setCurrentNovel(self.novelValue.handle) self.novelView.setCurrentNovel(self.novelValue.handle)
self.tbNovel.setVisible(self.novelValue.count() > 1) self.tbNovel.setVisible(self.novelValue.count() > 1)
return
def setCurrentRoot(self, rootHandle: str | None) -> None: def setCurrentRoot(self, rootHandle: str | None) -> None:
"""Set the current active root handle.""" """Set the current active root handle."""
@@ -300,7 +283,6 @@ class GuiNovelToolBar(QWidget):
SHARED.project.data.setLastHandle(rootHandle, "novel") SHARED.project.data.setLastHandle(rootHandle, "novel")
self.novelView.setCurrentNovel(rootHandle) self.novelView.setCurrentNovel(rootHandle)
self.novelView.novelTree.setAccessibleName(self.novelValue.currentText()) self.novelView.novelTree.setAccessibleName(self.novelValue.currentText())
return
def setLastColType(self, colType: nwNovelExtra, doRefresh: bool = True) -> None: def setLastColType(self, colType: nwNovelExtra, doRefresh: bool = True) -> None:
"""Set the last column type.""" """Set the last column type."""
@@ -309,7 +291,6 @@ class GuiNovelToolBar(QWidget):
if doRefresh: if doRefresh:
self.forceRefreshNovelTree() self.forceRefreshNovelTree()
self.novelView.novelTree.resizeColumns() self.novelView.novelTree.resizeColumns()
return
def setActive(self, state: bool) -> None: def setActive(self, state: bool) -> None:
"""Set the widget active state, which enables automatic tree """Set the widget active state, which enables automatic tree
@@ -322,7 +303,6 @@ class GuiNovelToolBar(QWidget):
and self._refresh.get(handle, False) and self._refresh.get(handle, False)
): ):
self._refreshNovelTree(self.novelValue.handle) self._refreshNovelTree(self.novelValue.handle)
return
## ##
# Public Slots # Public Slots
@@ -335,7 +315,6 @@ class GuiNovelToolBar(QWidget):
self.novelView.setCurrentNovel(tHandle) self.novelView.setCurrentNovel(tHandle)
SHARED.project.index.refreshNovelModel(tHandle) SHARED.project.index.refreshNovelModel(tHandle)
self._refresh[tHandle] = False self._refresh[tHandle] = False
return
## ##
# Private Slots # Private Slots
@@ -349,7 +328,6 @@ class GuiNovelToolBar(QWidget):
self._refresh[tHandle] = False self._refresh[tHandle] = False
else: else:
self._refresh[tHandle] = True self._refresh[tHandle] = True
return
@pyqtSlot() @pyqtSlot()
def _selectLastColumnSize(self) -> None: def _selectLastColumnSize(self) -> None:
@@ -361,7 +339,6 @@ class GuiNovelToolBar(QWidget):
if isOk: if isOk:
self.novelView.novelTree.setLastColSize(newSize) self.novelView.novelTree.setLastColSize(newSize)
self.novelView.novelTree.resizeColumns() self.novelView.novelTree.resizeColumns()
return
## ##
# Internal Functions # Internal Functions
@@ -374,10 +351,10 @@ class GuiNovelToolBar(QWidget):
aLast.setActionGroup(self.gLastCol) aLast.setActionGroup(self.gLastCol)
aLast.triggered.connect(qtLambda(self.setLastColType, colType)) aLast.triggered.connect(qtLambda(self.setLastColType, colType))
self.aLastCol[colType] = aLast self.aLastCol[colType] = aLast
return
class GuiNovelTree(NTreeView): class GuiNovelTree(NTreeView):
"""GUI: Novel View Panel Tree."""
def __init__(self, novelView: GuiNovelView) -> None: def __init__(self, novelView: GuiNovelView) -> None:
super().__init__(parent=novelView) super().__init__(parent=novelView)
@@ -414,8 +391,6 @@ class GuiNovelTree(NTreeView):
logger.debug("Ready: GuiNovelTree") logger.debug("Ready: GuiNovelTree")
return
def initSettings(self) -> None: def initSettings(self) -> None:
"""Set or update tree widget settings.""" """Set or update tree widget settings."""
if CONFIG.hideVScroll: if CONFIG.hideVScroll:
@@ -426,7 +401,6 @@ class GuiNovelTree(NTreeView):
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff) self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
else: else:
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
return
## ##
# Properties # Properties
@@ -466,25 +440,21 @@ class GuiNovelTree(NTreeView):
self.resizeColumns() self.resizeColumns()
else: else:
self.clearContent() self.clearContent()
return
def setActiveHandle(self, tHandle: str | None) -> None: def setActiveHandle(self, tHandle: str | None) -> None:
"""Set the handle to be highlighted.""" """Set the handle to be highlighted."""
self._actHandle = tHandle self._actHandle = tHandle
if viewport := self.viewport(): if viewport := self.viewport():
viewport.repaint() viewport.repaint()
return
def setLastColType(self, colType: nwNovelExtra) -> None: def setLastColType(self, colType: nwNovelExtra) -> None:
"""Set the extra column type.""" """Set the extra column type."""
self._lastColType = colType self._lastColType = colType
SHARED.project.index.setNovelModelExtraColumn(colType) SHARED.project.index.setNovelModelExtraColumn(colType)
return
def setLastColSize(self, colSize: int) -> None: def setLastColSize(self, colSize: int) -> None:
"""Set the extra column size between 15% and 75%.""" """Set the extra column size between 15% and 75%."""
self._lastColSize = minmax(colSize, 15, 75)/100.0 self._lastColSize = minmax(colSize, 15, 75)/100.0
return
## ##
# Class Methods # Class Methods
@@ -493,7 +463,6 @@ class GuiNovelTree(NTreeView):
def clearContent(self) -> None: def clearContent(self) -> None:
"""Clear the tree view.""" """Clear the tree view."""
self.setModel(None) self.setModel(None)
return
def resizeColumns(self) -> None: def resizeColumns(self) -> None:
"""Set the correct column sizes.""" """Set the correct column sizes."""
@@ -506,7 +475,6 @@ class GuiNovelTree(NTreeView):
if model.columns == 4: if model.columns == 4:
header.setSectionResizeMode(3, QtHeaderToContents) header.setSectionResizeMode(3, QtHeaderToContents)
header.setMaximumSectionSize(int(self._lastColSize * vp.width())) header.setMaximumSectionSize(int(self._lastColSize * vp.width()))
return
## ##
# Overloads # Overloads
@@ -517,7 +485,6 @@ class GuiNovelTree(NTreeView):
if (model := self._getModel()) and model.handle(index) == self._actHandle: if (model := self._getModel()) and model.handle(index) == self._actHandle:
painter.fillRect(opt.rect, self.palette().alternateBase()) painter.fillRect(opt.rect, self.palette().alternateBase())
super().drawRow(painter, opt, index) super().drawRow(painter, opt, index)
return
## ##
# Events # Events
@@ -527,7 +494,6 @@ class GuiNovelTree(NTreeView):
"""Process size changed.""" """Process size changed."""
super().resizeEvent(event) super().resizeEvent(event)
self.resizeColumns() self.resizeColumns()
return
## ##
# Private Slots # Private Slots
@@ -535,36 +501,33 @@ class GuiNovelTree(NTreeView):
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _onSingleClick(self, index: QModelIndex) -> None: def _onSingleClick(self, index: QModelIndex) -> None:
"""The user single-clicked an index.""" """Process user single-click on an index."""
if index.isValid() and (model := self._getModel()): if index.isValid() and (model := self._getModel()):
if (tHandle := model.handle(index)) and (sTitle := model.key(index)): if (tHandle := model.handle(index)) and (sTitle := model.key(index)):
self.novelView.selectedItemChanged.emit(tHandle) self.novelView.selectedItemChanged.emit(tHandle)
if index.column() == model.columnCount(index) - 1: if index.column() == model.columnCount(index) - 1:
pos = self.mapToGlobal(self.visualRect(index).topRight()) pos = self.mapToGlobal(self.visualRect(index).topRight())
self._popMetaBox(pos, tHandle, sTitle) self._popMetaBox(pos, tHandle, sTitle)
return
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _onDoubleClick(self, index: QModelIndex) -> None: def _onDoubleClick(self, index: QModelIndex) -> None:
"""The user double-clicked an index.""" """Process user double-click on an index."""
if ( if (
(model := self._getModel()) (model := self._getModel())
and (tHandle := model.handle(index)) and (tHandle := model.handle(index))
and (sTitle := model.key(index)) and (sTitle := model.key(index))
): ):
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle, False) self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle, False)
return
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _onMiddleClick(self, index: QModelIndex) -> None: def _onMiddleClick(self, index: QModelIndex) -> None:
"""The user middle-clicked an index.""" """Process user middle-click on an index."""
if ( if (
(model := self._getModel()) (model := self._getModel())
and (tHandle := model.handle(index)) and (tHandle := model.handle(index))
and (sTitle := model.key(index)) and (sTitle := model.key(index))
): ):
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle, False) self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle, False)
return
## ##
# Internal Functions # Internal Functions
@@ -582,7 +545,6 @@ class GuiNovelTree(NTreeView):
"""Generate a reference list for a given reference key.""" """Generate a reference list for a given reference key."""
if tags := ", ".join(refs.get(key, [])): if tags := ", ".join(refs.get(key, [])):
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {tags}") lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {tags}")
return
if head := SHARED.project.index.getItemHeading(tHandle, sTitle): if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle) logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
@@ -610,4 +572,3 @@ class GuiNovelTree(NTreeView):
text = f"<p>{refs}</p>" text = f"<p>{refs}</p>"
if tooltip := (text + synopsis or self.tr("No meta data")): if tooltip := (text + synopsis or self.tr("No meta data")):
QToolTip.showText(qPos, tooltip) QToolTip.showText(qPos, tooltip)
return
+10 -57
View File
@@ -24,7 +24,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import csv import csv
@@ -58,6 +58,7 @@ logger = logging.getLogger(__name__)
class GuiOutlineView(QWidget): class GuiOutlineView(QWidget):
"""GUI: Project Outline Panel."""
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
openDocumentRequest = pyqtSignal(str, Enum, str, bool) openDocumentRequest = pyqtSignal(str, Enum, str, bool)
@@ -96,8 +97,6 @@ class GuiOutlineView(QWidget):
# Function Mappings # Function Mappings
self.getSelectedHandle = self.outlineTree.getSelectedHandle self.getSelectedHandle = self.outlineTree.getSelectedHandle
return
## ##
# Methods # Methods
## ##
@@ -109,24 +108,20 @@ class GuiOutlineView(QWidget):
self.outlineTree.refreshTree( self.outlineTree.refreshTree(
rootHandle=SHARED.project.data.getLastHandle("outline"), overRide=True rootHandle=SHARED.project.data.getLastHandle("outline"), overRide=True
) )
return
def initSettings(self) -> None: def initSettings(self) -> None:
"""Initialise GUI elements that depend on specific settings.""" """Initialise GUI elements that depend on specific settings."""
self.outlineTree.initSettings() self.outlineTree.initSettings()
self.outlineData.initSettings() self.outlineData.initSettings()
return
def refreshTree(self) -> None: def refreshTree(self) -> None:
"""Refresh the current tree.""" """Refresh the current tree."""
self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline")) self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline"))
return
def clearOutline(self) -> None: def clearOutline(self) -> None:
"""Clear project-related GUI content.""" """Clear project-related GUI content."""
self.outlineData.clearDetails() self.outlineData.clearDetails()
self.outlineBar.setEnabled(False) self.outlineBar.setEnabled(False)
return
def openProjectTasks(self) -> None: def openProjectTasks(self) -> None:
"""Run open project tasks.""" """Run open project tasks."""
@@ -142,8 +137,6 @@ class GuiOutlineView(QWidget):
self.outlineBar.setEnabled(True) self.outlineBar.setEnabled(True)
self.outlineData.loadGuiSettings() self.outlineData.loadGuiSettings()
return
def closeProjectTasks(self) -> None: def closeProjectTasks(self) -> None:
"""Run closing project tasks.""" """Run closing project tasks."""
if self.outlineTree.wasRendered: if self.outlineTree.wasRendered:
@@ -152,7 +145,6 @@ class GuiOutlineView(QWidget):
self.outlineTree.closeProjectTasks() self.outlineTree.closeProjectTasks()
self.outlineData.updateClasses() self.outlineData.updateClasses()
self.clearOutline() self.clearOutline()
return
def splitSizes(self) -> list[int]: def splitSizes(self) -> list[int]:
"""Get the sizes of the splitter widget.""" """Get the sizes of the splitter widget."""
@@ -175,7 +167,6 @@ class GuiOutlineView(QWidget):
"""Handle tasks whenever a root folders changes.""" """Handle tasks whenever a root folders changes."""
self.outlineBar.populateNovelList() self.outlineBar.populateNovelList()
self.outlineData.updateClasses() self.outlineData.updateClasses()
return
## ##
# Private Slots # Private Slots
@@ -188,23 +179,21 @@ class GuiOutlineView(QWidget):
of columns has changed. of columns has changed.
""" """
self.outlineBar.setColumnHiddenState(self.outlineTree.hiddenColumns) self.outlineBar.setColumnHiddenState(self.outlineTree.hiddenColumns)
return
@pyqtSlot(str) @pyqtSlot(str)
def _tagClicked(self, link: str) -> None: def _tagClicked(self, link: str) -> None:
"""Capture the click of a tag in the details panel.""" """Capture the click of a tag in the details panel."""
if link: if link:
self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW) self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW)
return
@pyqtSlot(str) @pyqtSlot(str)
def _rootItemChanged(self, tHandle: str) -> None: def _rootItemChanged(self, tHandle: str) -> None:
"""Handle root novel changed or needs to be refreshed.""" """Handle root novel changed or needs to be refreshed."""
self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True) self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True)
return
class GuiOutlineToolBar(QToolBar): class GuiOutlineToolBar(QToolBar):
"""GUI: Project Outline Panel ToolBar."""
loadNovelRootRequest = pyqtSignal(str) loadNovelRootRequest = pyqtSignal(str)
outlineExportRequest = pyqtSignal() outlineExportRequest = pyqtSignal()
@@ -263,8 +252,6 @@ class GuiOutlineToolBar(QToolBar):
logger.debug("Ready: GuiOutlineToolBar") logger.debug("Ready: GuiOutlineToolBar")
return
## ##
# Methods # Methods
## ##
@@ -278,22 +265,18 @@ class GuiOutlineToolBar(QToolBar):
self.tbColumns.setIcon(SHARED.theme.getIcon("more_vertical")) self.tbColumns.setIcon(SHARED.theme.getIcon("more_vertical"))
self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}") self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
self.novelLabel.setTextColors(color=self.palette().windowText().color()) self.novelLabel.setTextColors(color=self.palette().windowText().color())
return
def populateNovelList(self) -> None: def populateNovelList(self) -> None:
"""Reload the content of the novel list.""" """Reload the content of the novel list."""
self.novelValue.refreshNovelList() self.novelValue.refreshNovelList()
return
def setCurrentRoot(self, rootHandle: str | None) -> None: def setCurrentRoot(self, rootHandle: str | None) -> None:
"""Set the current active root handle.""" """Set the current active root handle."""
self.novelValue.setHandle(rootHandle) self.novelValue.setHandle(rootHandle)
return
def setColumnHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None: def setColumnHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
"""Forward the change of column hidden states to the menu.""" """Forward the change of column hidden states to the menu."""
self.mColumns.setHiddenState(hiddenState) self.mColumns.setHiddenState(hiddenState)
return
## ##
# Private Slots # Private Slots
@@ -303,22 +286,20 @@ class GuiOutlineToolBar(QToolBar):
def _novelValueChanged(self, tHandle: str) -> None: def _novelValueChanged(self, tHandle: str) -> None:
"""Emit a signal containing the handle of the selected item.""" """Emit a signal containing the handle of the selected item."""
self.loadNovelRootRequest.emit(tHandle) self.loadNovelRootRequest.emit(tHandle)
return
@pyqtSlot() @pyqtSlot()
def _refreshRequested(self) -> None: def _refreshRequested(self) -> None:
"""Emit a signal containing the handle of the selected item.""" """Emit a signal containing the handle of the selected item."""
self.loadNovelRootRequest.emit(self.novelValue.handle) self.loadNovelRootRequest.emit(self.novelValue.handle)
return
@pyqtSlot() @pyqtSlot()
def _exportRequested(self) -> None: def _exportRequested(self) -> None:
"""Emit a signal that an export of the outline was requested.""" """Emit a signal that an export of the outline was requested."""
self.outlineExportRequest.emit() self.outlineExportRequest.emit()
return
class GuiOutlineTree(QTreeWidget): class GuiOutlineTree(QTreeWidget):
"""GUI: Project Outline Panel Tree."""
DEF_WIDTH: Final[dict[nwOutline, int]] = { DEF_WIDTH: Final[dict[nwOutline, int]] = {
nwOutline.TITLE: 200, nwOutline.TITLE: 200,
@@ -422,8 +403,6 @@ class GuiOutlineTree(QTreeWidget):
logger.debug("Ready: GuiOutlineTree") logger.debug("Ready: GuiOutlineTree")
return
## ##
# Properties # Properties
## ##
@@ -451,7 +430,6 @@ class GuiOutlineTree(QTreeWidget):
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff) self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
else: else:
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
return
def clearContent(self) -> None: def clearContent(self) -> None:
"""Clear the tree and header and set the default values for the """Clear the tree and header and set the default values for the
@@ -474,8 +452,6 @@ class GuiOutlineTree(QTreeWidget):
self._treeNCols = len(self._treeOrder) self._treeNCols = len(self._treeOrder)
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
iType = nwItemType.FILE iType = nwItemType.FILE
@@ -488,15 +464,14 @@ class GuiOutlineTree(QTreeWidget):
"H3": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H3"), "H3": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H3"),
"H4": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H4"), "H4": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H4"),
} }
return
def refreshTree( def refreshTree(
self, rootHandle: str | None = None, self, rootHandle: str | None = None,
overRide: bool = False, novelChanged: bool = False overRide: bool = False, novelChanged: bool = False
) -> None: ) -> None:
"""Called whenever the Outline tab is activated and controls """Refresh the outline tree. Called whenever the Outline tab is
what data to load, and if necessary, force a rebuild of the activated and controls what data to load, and if necessary,
tree. force a rebuild of the tree.
""" """
# If it's the first time, we always build # If it's the first time, we always build
if self._firstView or (self._firstView and overRide): if self._firstView or (self._firstView and overRide):
@@ -518,11 +493,10 @@ class GuiOutlineTree(QTreeWidget):
return return
def closeProjectTasks(self) -> None: def closeProjectTasks(self) -> None:
"""Called before a project is closed.""" """Call before a project is closed."""
self._saveHeaderState() self._saveHeaderState()
self.clearContent() self.clearContent()
self._firstView = True self._firstView = True
return
def getSelectedHandle(self) -> tuple[str | None, str | None]: def getSelectedHandle(self) -> tuple[str | None, str | None]:
"""Get the currently selected handle. If multiple items are """Get the currently selected handle. If multiple items are
@@ -546,7 +520,6 @@ class GuiOutlineTree(QTreeWidget):
if hItem in self._colIdx: if hItem in self._colIdx:
self.setColumnHidden(self._colIdx[hItem], not isChecked) self.setColumnHidden(self._colIdx[hItem], not isChecked)
self._saveHeaderState() self._saveHeaderState()
return
@pyqtSlot() @pyqtSlot()
def exportOutline(self) -> None: def exportOutline(self) -> None:
@@ -562,7 +535,6 @@ class GuiOutlineTree(QTreeWidget):
writer.writerows( writer.writerows(
self._dumpNovelData(self.outlineView.outlineBar.novelValue.handle) self._dumpNovelData(self.outlineView.outlineBar.novelValue.handle)
) )
return
## ##
# Private Slots # Private Slots
@@ -577,7 +549,6 @@ class GuiOutlineTree(QTreeWidget):
tHandle, sTitle = self.getSelectedHandle() tHandle, sTitle = self.getSelectedHandle()
if tHandle: if tHandle:
self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True) self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
return
@pyqtSlot() @pyqtSlot()
def _onItemSelectionChanged(self) -> None: def _onItemSelectionChanged(self) -> None:
@@ -588,7 +559,6 @@ class GuiOutlineTree(QTreeWidget):
tHandle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) tHandle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
sTitle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE) sTitle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
self.activeItemChanged.emit(tHandle, sTitle) self.activeItemChanged.emit(tHandle, sTitle)
return
@pyqtSlot(int, int, int) @pyqtSlot(int, int, int)
def _columnMoved(self, logIdx: int, oldVisualIdx: int, newVisualIdx: int) -> None: def _columnMoved(self, logIdx: int, oldVisualIdx: int, newVisualIdx: int) -> None:
@@ -597,7 +567,6 @@ class GuiOutlineTree(QTreeWidget):
""" """
self._treeOrder.insert(newVisualIdx, self._treeOrder.pop(oldVisualIdx)) self._treeOrder.insert(newVisualIdx, self._treeOrder.pop(oldVisualIdx))
self._saveHeaderState() self._saveHeaderState()
return
## ##
# Internal Functions # Internal Functions
@@ -637,8 +606,6 @@ class GuiOutlineTree(QTreeWidget):
self.hiddenStateChanged.emit() self.hiddenStateChanged.emit()
return
def _saveHeaderState(self) -> None: def _saveHeaderState(self) -> None:
"""Save the state of the main tree header, that is, column """Save the state of the main tree header, that is, column
order, column width and column hidden state. We don't want to order, column width and column hidden state. We don't want to
@@ -661,7 +628,6 @@ class GuiOutlineTree(QTreeWidget):
pOptions = SHARED.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiOutline", "columnState", colState) pOptions.setValue("GuiOutline", "columnState", colState)
pOptions.saveSettings() pOptions.saveSettings()
return
def _populateTree(self, rootHandle: str | None) -> None: def _populateTree(self, rootHandle: str | None) -> None:
"""Build the tree based on the project index, and the header """Build the tree based on the project index, and the header
@@ -746,8 +712,6 @@ class GuiOutlineTree(QTreeWidget):
self._lastBuild = time() self._lastBuild = time()
logger.debug("Project outline built in %.3f ms", 1000.0*(time() - tStart)) logger.debug("Project outline built in %.3f ms", 1000.0*(time() - tStart))
return
def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]: def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]:
"""Dump all novel data into a table.""" """Dump all novel data into a table."""
sLabel = SHARED.project.localLookup("Story Structure") sLabel = SHARED.project.localLookup("Story Structure")
@@ -821,6 +785,7 @@ class GuiOutlineTree(QTreeWidget):
class GuiOutlineHeaderMenu(QMenu): class GuiOutlineHeaderMenu(QMenu):
"""GUI: Project Outline Panel Header Selection Menu."""
columnToggled = pyqtSignal(bool, Enum) columnToggled = pyqtSignal(bool, Enum)
@@ -844,8 +809,6 @@ class GuiOutlineHeaderMenu(QMenu):
) )
self.addAction(self.actionMap[hItem]) self.addAction(self.actionMap[hItem])
return
def setHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None: def setHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
"""Overwrite the checked state of the columns as the inverse of """Overwrite the checked state of the columns as the inverse of
the hidden state. Skip the TITLE column as it cannot be hidden. the hidden state. Skip the TITLE column as it cannot be hidden.
@@ -859,10 +822,9 @@ class GuiOutlineHeaderMenu(QMenu):
self.acceptToggle = True self.acceptToggle = True
return
class GuiOutlineDetails(QScrollArea): class GuiOutlineDetails(QScrollArea):
"""GUI: Project Outline Panel Details View."""
LVL_MAP: Final[dict[str, str]] = { LVL_MAP: Final[dict[str, str]] = {
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"), "H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
@@ -1007,8 +969,6 @@ class GuiOutlineDetails(QScrollArea):
logger.debug("Ready: GuiOutlineDetails") logger.debug("Ready: GuiOutlineDetails")
return
def initSettings(self) -> None: def initSettings(self) -> None:
"""Set or update outline settings.""" """Set or update outline settings."""
if CONFIG.hideVScroll: if CONFIG.hideVScroll:
@@ -1020,7 +980,6 @@ class GuiOutlineDetails(QScrollArea):
else: else:
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
self.updateClasses() self.updateClasses()
return
def loadGuiSettings(self) -> None: def loadGuiSettings(self) -> None:
"""Run open project tasks.""" """Run open project tasks."""
@@ -1031,7 +990,6 @@ class GuiOutlineDetails(QScrollArea):
pOptions.getInt("GuiOutlineDetails", "detailsWidth", width//3), pOptions.getInt("GuiOutlineDetails", "detailsWidth", width//3),
pOptions.getInt("GuiOutlineDetails", "tagsWidth", 2*width//3), pOptions.getInt("GuiOutlineDetails", "tagsWidth", 2*width//3),
]) ])
return
def saveGuiSettings(self) -> None: def saveGuiSettings(self) -> None:
"""Run close project tasks.""" """Run close project tasks."""
@@ -1040,7 +998,6 @@ class GuiOutlineDetails(QScrollArea):
pOptions = SHARED.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiOutlineDetails", "detailsWidth", mainSplit[0]) pOptions.setValue("GuiOutlineDetails", "detailsWidth", mainSplit[0])
pOptions.setValue("GuiOutlineDetails", "tagsWidth", mainSplit[1]) pOptions.setValue("GuiOutlineDetails", "tagsWidth", mainSplit[1])
return
def clearDetails(self) -> None: def clearDetails(self) -> None:
"""Clear all the data labels.""" """Clear all the data labels."""
@@ -1057,7 +1014,6 @@ class GuiOutlineDetails(QScrollArea):
value.clear() value.clear()
self.updateClasses() self.updateClasses()
return
## ##
# Slots # Slots
@@ -1090,8 +1046,6 @@ class GuiOutlineDetails(QScrollArea):
for key, (_, value) in self.tagValues.items(): for key, (_, value) in self.tagValues.items():
value.setText(self._formatTags(novRefs, key)) value.setText(self._formatTags(novRefs, key))
return
@pyqtSlot() @pyqtSlot()
def updateClasses(self) -> None: def updateClasses(self) -> None:
"""Update the visibility status of class details.""" """Update the visibility status of class details."""
@@ -1102,7 +1056,6 @@ class GuiOutlineDetails(QScrollArea):
label, value = self.tagValues[key] label, value = self.tagValues[key]
label.setVisible(visible) label.setVisible(visible)
value.setVisible(visible) value.setVisible(visible)
return
@staticmethod @staticmethod
def _formatTags(refs: dict[str, list[str]], key: str) -> str: def _formatTags(refs: dict[str, list[str]], key: str) -> str:
+9 -88
View File
@@ -25,7 +25,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -61,7 +61,9 @@ logger = logging.getLogger(__name__)
class GuiProjectView(QWidget): class GuiProjectView(QWidget):
"""This is a wrapper class holding all the elements of the project """GUI: Project View.
This is a wrapper class holding all the elements of the project
tree. The core object is the project tree itself. Most methods tree. The core object is the project tree itself. Most methods
available are mapped through to the project tree class. available are mapped through to the project tree class.
""" """
@@ -130,8 +132,6 @@ class GuiProjectView(QWidget):
# Function Mappings # Function Mappings
self.getSelectedHandle = self.projTree.getSelectedHandle self.getSelectedHandle = self.projTree.getSelectedHandle
return
## ##
# Methods # Methods
## ##
@@ -139,19 +139,16 @@ class GuiProjectView(QWidget):
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self.projBar.updateTheme() self.projBar.updateTheme()
return
def initSettings(self) -> None: def initSettings(self) -> None:
"""Initialise GUI elements that depend on specific settings.""" """Initialise GUI elements that depend on specific settings."""
self.projTree.initSettings() self.projTree.initSettings()
return
def closeProjectTasks(self) -> None: def closeProjectTasks(self) -> None:
"""Clear project-related GUI content.""" """Clear project-related GUI content."""
self.projBar.clearContent() self.projBar.clearContent()
self.projBar.setEnabled(False) self.projBar.setEnabled(False)
self.projTree.clearTree() self.projTree.clearTree()
return
def openProjectTasks(self) -> None: def openProjectTasks(self) -> None:
"""Run open project tasks.""" """Run open project tasks."""
@@ -159,26 +156,23 @@ class GuiProjectView(QWidget):
self.projBar.buildTemplatesMenu() self.projBar.buildTemplatesMenu()
self.projBar.buildQuickLinksMenu() self.projBar.buildQuickLinksMenu()
self.projBar.setEnabled(True) self.projBar.setEnabled(True)
return
def setTreeFocus(self) -> None: def setTreeFocus(self) -> None:
"""Forward the set focus call to the tree widget.""" """Forward the set focus call to the tree widget."""
self.projTree.setFocus() self.projTree.setFocus()
return
def treeHasFocus(self) -> bool: def treeHasFocus(self) -> bool:
"""Check if the project tree has focus.""" """Check if the project tree has focus."""
return self.projTree.hasFocus() return self.projTree.hasFocus()
def connectMenuActions(self, rename: QAction, delete: QAction, trash: QAction) -> None: def connectMenuActions(self, rename: QAction, delete: QAction, trash: QAction) -> None:
"""Main menu actions passed to the project tree.""" """Connect main menu actions passed to the project tree."""
self.projTree.addAction(rename) self.projTree.addAction(rename)
self.projTree.addAction(delete) self.projTree.addAction(delete)
self.projTree.addAction(trash) self.projTree.addAction(trash)
rename.triggered.connect(self.renameTreeItem) rename.triggered.connect(self.renameTreeItem)
delete.triggered.connect(self.projTree.processDeleteRequest) delete.triggered.connect(self.projTree.processDeleteRequest)
trash.triggered.connect(self.projTree.emptyTrash) trash.triggered.connect(self.projTree.emptyTrash)
return
## ##
# Public Slots # Public Slots
@@ -197,41 +191,36 @@ class GuiProjectView(QWidget):
if dlgOk: if dlgOk:
nwItem.setName(newLabel) nwItem.setName(newLabel)
nwItem.notifyToRefresh() nwItem.notifyToRefresh()
return
@pyqtSlot(str, bool) @pyqtSlot(str, bool)
def setSelectedHandle(self, tHandle: str, doScroll: bool = False) -> None: def setSelectedHandle(self, tHandle: str, doScroll: bool = False) -> None:
"""Select an item and optionally scroll it into view.""" """Select an item and optionally scroll it into view."""
self.projTree.setSelectedHandle(tHandle, doScroll=doScroll) self.projTree.setSelectedHandle(tHandle, doScroll=doScroll)
return
@pyqtSlot(str) @pyqtSlot(str)
def setActiveHandle(self, tHandle: str | None) -> None: def setActiveHandle(self, tHandle: str | None) -> None:
"""Highlight the active handle.""" """Highlight the active handle."""
self.projTree.setActiveHandle(tHandle) self.projTree.setActiveHandle(tHandle)
return
@pyqtSlot(str, Enum) @pyqtSlot(str, Enum)
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None: def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
"""Refresh other content when project item changed.""" """Refresh other content when project item changed."""
self.projBar.processTemplateDocuments(tHandle) self.projBar.processTemplateDocuments(tHandle)
return
@pyqtSlot(str) @pyqtSlot(str)
def createFileFromTemplate(self, tHandle: str) -> None: def createFileFromTemplate(self, tHandle: str) -> None:
"""Create a new document from a template.""" """Create a new document from a template."""
logger.debug("Template selected: '%s'", tHandle) logger.debug("Template selected: '%s'", tHandle)
self.projTree.newTreeItem(nwItemType.FILE, copyDoc=tHandle) self.projTree.newTreeItem(nwItemType.FILE, copyDoc=tHandle)
return
@pyqtSlot(str, Enum) @pyqtSlot(str, Enum)
def updateRootItem(self, tHandle: str, change: nwChange) -> None: def updateRootItem(self, tHandle: str, change: nwChange) -> None:
"""Process root item changes.""" """Process root item changes."""
self.projBar.buildQuickLinksMenu() self.projBar.buildQuickLinksMenu()
return
class GuiProjectToolBar(QWidget): class GuiProjectToolBar(QWidget):
"""GUI> Project View ToolBar."""
newDocumentFromTemplate = pyqtSignal(str) newDocumentFromTemplate = pyqtSignal(str)
@@ -351,8 +340,6 @@ class GuiProjectToolBar(QWidget):
logger.debug("Ready: GuiProjectToolBar") logger.debug("Ready: GuiProjectToolBar")
return
## ##
# Methods # Methods
## ##
@@ -383,13 +370,10 @@ class GuiProjectToolBar(QWidget):
self.buildQuickLinksMenu() self.buildQuickLinksMenu()
self._buildRootMenu() self._buildRootMenu()
return
def clearContent(self) -> None: def clearContent(self) -> None:
"""Clear dynamic content on the tool bar.""" """Clear dynamic content on the tool bar."""
self.mQuick.clear() self.mQuick.clear()
self.mTemplates.clearMenu() self.mTemplates.clearMenu()
return
def buildQuickLinksMenu(self) -> None: def buildQuickLinksMenu(self) -> None:
"""Build the quick link menu.""" """Build the quick link menu."""
@@ -402,14 +386,12 @@ class GuiProjectToolBar(QWidget):
action.triggered.connect( action.triggered.connect(
qtLambda(self.projView.setSelectedHandle, tHandle, doScroll=True) qtLambda(self.projView.setSelectedHandle, tHandle, doScroll=True)
) )
return
def buildTemplatesMenu(self) -> None: def buildTemplatesMenu(self) -> None:
"""Build the templates menu.""" """Build the templates menu."""
for tHandle, _ in SHARED.project.tree.iterRoots(nwItemClass.TEMPLATE): for tHandle, _ in SHARED.project.tree.iterRoots(nwItemClass.TEMPLATE):
for dHandle in SHARED.project.tree.subTree(tHandle): for dHandle in SHARED.project.tree.subTree(tHandle):
self.processTemplateDocuments(dHandle) self.processTemplateDocuments(dHandle)
return
def processTemplateDocuments(self, tHandle: str) -> None: def processTemplateDocuments(self, tHandle: str) -> None:
"""Process change in tree items to update menu content.""" """Process change in tree items to update menu content."""
@@ -418,7 +400,6 @@ class GuiProjectToolBar(QWidget):
self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon()) self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon())
elif tHandle in self.mTemplates: elif tHandle in self.mTemplates:
self.mTemplates.remove(tHandle) self.mTemplates.remove(tHandle)
return
## ##
# Public Slots # Public Slots
@@ -436,7 +417,6 @@ class GuiProjectToolBar(QWidget):
self.aAddChap.setVisible(allowDoc) self.aAddChap.setVisible(allowDoc)
self.aAddPart.setVisible(allowDoc) self.aAddPart.setVisible(allowDoc)
self.aAddEmpty.setVisible(allowDoc) self.aAddEmpty.setVisible(allowDoc)
return
## ##
# Internal Functions # Internal Functions
@@ -451,7 +431,6 @@ class GuiProjectToolBar(QWidget):
qtLambda(self.projTree.newTreeItem, nwItemType.ROOT, itemClass) qtLambda(self.projTree.newTreeItem, nwItemType.ROOT, itemClass)
) )
self.mAddRoot.addAction(aNew) self.mAddRoot.addAction(aNew)
return
self.mAddRoot.clear() self.mAddRoot.clear()
addClass(nwItemClass.NOVEL) addClass(nwItemClass.NOVEL)
@@ -467,10 +446,9 @@ class GuiProjectToolBar(QWidget):
addClass(nwItemClass.ARCHIVE) addClass(nwItemClass.ARCHIVE)
addClass(nwItemClass.TEMPLATE) addClass(nwItemClass.TEMPLATE)
return
class GuiProjectTree(QTreeView): class GuiProjectTree(QTreeView):
"""GUI: Project View Tree."""
def __init__(self, projView: GuiProjectView) -> None: def __init__(self, projView: GuiProjectView) -> None:
super().__init__(parent=projView) super().__init__(parent=projView)
@@ -520,8 +498,6 @@ class GuiProjectTree(QTreeView):
logger.debug("Ready: GuiProjectTree") logger.debug("Ready: GuiProjectTree")
return
def initSettings(self) -> None: def initSettings(self) -> None:
"""Set or update tree widget settings.""" """Set or update tree widget settings."""
if CONFIG.hideVScroll: if CONFIG.hideVScroll:
@@ -532,7 +508,6 @@ class GuiProjectTree(QTreeView):
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff) self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
else: else:
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
return
## ##
# External Methods # External Methods
@@ -541,7 +516,6 @@ class GuiProjectTree(QTreeView):
def setActiveHandle(self, tHandle: str | None) -> None: def setActiveHandle(self, tHandle: str | None) -> None:
"""Set the handle to be highlighted.""" """Set the handle to be highlighted."""
self._actHandle = tHandle self._actHandle = tHandle
return
def getSelectedHandle(self) -> str | None: def getSelectedHandle(self) -> str | None:
"""Get the currently selected handle.""" """Get the currently selected handle."""
@@ -556,7 +530,6 @@ class GuiProjectTree(QTreeView):
def clearTree(self) -> None: def clearTree(self) -> None:
"""Clear the tree view.""" """Clear the tree view."""
self.setModel(None) self.setModel(None)
return
def loadModel(self) -> None: def loadModel(self) -> None:
"""Load and prepare a new project model.""" """Load and prepare a new project model."""
@@ -583,8 +556,6 @@ class GuiProjectTree(QTreeView):
self.restoreExpandedState() self.restoreExpandedState()
return
def restoreExpandedState(self) -> None: def restoreExpandedState(self) -> None:
"""Expand all nodes that were previously expanded.""" """Expand all nodes that were previously expanded."""
if model := self._getModel(): if model := self._getModel():
@@ -592,7 +563,6 @@ class GuiProjectTree(QTreeView):
for index in model.allExpanded(): for index in model.allExpanded():
self.setExpanded(index, True) self.setExpanded(index, True)
self.blockSignals(False) self.blockSignals(False)
return
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None: def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
"""Set a specific handle as the selected item.""" """Set a specific handle as the selected item."""
@@ -601,7 +571,6 @@ class GuiProjectTree(QTreeView):
if doScroll: if doScroll:
self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter) self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter)
self.projView.selectedItemChanged.emit(tHandle) self.projView.selectedItemChanged.emit(tHandle)
return
def newTreeItem( def newTreeItem(
self, itemType: nwItemType, itemClass: nwItemClass | None = None, self, itemType: nwItemType, itemClass: nwItemClass | None = None,
@@ -805,7 +774,6 @@ class GuiProjectTree(QTreeView):
SHARED.warn(self.tr("Could not duplicate all items.")) SHARED.warn(self.tr("Could not duplicate all items."))
self.setEnabled(True) self.setEnabled(True)
self.restoreExpandedState() self.restoreExpandedState()
return
## ##
# Events and Overloads # Events and Overloads
@@ -825,14 +793,12 @@ class GuiProjectTree(QTreeView):
self.projView.openDocumentRequest.emit( self.projView.openDocumentRequest.emit(
node.item.itemHandle, nwDocMode.VIEW, "", False node.item.itemHandle, nwDocMode.VIEW, "", False
) )
return
def drawRow(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None: def drawRow(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None:
"""Draw a box on the active row.""" """Draw a box on the active row."""
if (node := self._getNode(index)) and node.item.itemHandle == self._actHandle: if (node := self._getNode(index)) and node.item.itemHandle == self._actHandle:
painter.fillRect(opt.rect, self.palette().alternateBase()) painter.fillRect(opt.rect, self.palette().alternateBase())
super().drawRow(painter, opt, index) super().drawRow(painter, opt, index)
return
## ##
# Public Slots # Public Slots
@@ -843,14 +809,12 @@ class GuiProjectTree(QTreeView):
"""Move an item up in the tree.""" """Move an item up in the tree."""
if model := self._getModel(): if model := self._getModel():
model.internalMove(self.currentIndex(), -1) model.internalMove(self.currentIndex(), -1)
return
@pyqtSlot() @pyqtSlot()
def moveItemDown(self) -> None: def moveItemDown(self) -> None:
"""Move an item down in the tree.""" """Move an item down in the tree."""
if model := self._getModel(): if model := self._getModel():
model.internalMove(self.currentIndex(), 1) model.internalMove(self.currentIndex(), 1)
return
@pyqtSlot() @pyqtSlot()
def goToSiblingUp(self) -> None: def goToSiblingUp(self) -> None:
@@ -858,7 +822,6 @@ class GuiProjectTree(QTreeView):
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()): if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
if (move := parent.child(node.row() - 1)) and (model := self._getModel()): if (move := parent.child(node.row() - 1)) and (model := self._getModel()):
self.setCurrentIndex(model.indexFromNode(move)) self.setCurrentIndex(model.indexFromNode(move))
return
@pyqtSlot() @pyqtSlot()
def goToSiblingDown(self) -> None: def goToSiblingDown(self) -> None:
@@ -866,7 +829,6 @@ class GuiProjectTree(QTreeView):
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()): if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
if (move := parent.child(node.row() + 1)) and (model := self._getModel()): if (move := parent.child(node.row() + 1)) and (model := self._getModel()):
self.setCurrentIndex(model.indexFromNode(move)) self.setCurrentIndex(model.indexFromNode(move))
return
@pyqtSlot() @pyqtSlot()
def goToParent(self) -> None: def goToParent(self) -> None:
@@ -877,7 +839,6 @@ class GuiProjectTree(QTreeView):
and (parent := node.parent()) and (parent := node.parent())
): ):
self.setCurrentIndex(model.indexFromNode(parent)) self.setCurrentIndex(model.indexFromNode(parent))
return
@pyqtSlot() @pyqtSlot()
def goToFirstChild(self) -> None: def goToFirstChild(self) -> None:
@@ -888,13 +849,11 @@ class GuiProjectTree(QTreeView):
and (child := node.child(0)) and (child := node.child(0))
): ):
self.setCurrentIndex(model.indexFromNode(child)) self.setCurrentIndex(model.indexFromNode(child))
return
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def expandFromIndex(self, index: QModelIndex) -> None: def expandFromIndex(self, index: QModelIndex) -> None:
"""Expand all nodes from index.""" """Expand all nodes from index."""
self.expandRecursively(index) self.expandRecursively(index)
return
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def collapseFromIndex(self, index: QModelIndex) -> None: def collapseFromIndex(self, index: QModelIndex) -> None:
@@ -902,7 +861,6 @@ class GuiProjectTree(QTreeView):
if (model := self._getModel()) and (node := model.node(index)): if (model := self._getModel()) and (node := model.node(index)):
for child in node.allChildren(): for child in node.allChildren():
self.setExpanded(model.indexFromNode(child), False) self.setExpanded(model.indexFromNode(child), False)
return
@pyqtSlot() @pyqtSlot()
def processDeleteRequest( def processDeleteRequest(
@@ -968,9 +926,7 @@ class GuiProjectTree(QTreeView):
@pyqtSlot() @pyqtSlot()
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
def openContextMenu(self, point: QPoint | None = None) -> None: def openContextMenu(self, point: QPoint | None = None) -> None:
"""The user right clicked an element in the project tree, so we """Open a context menu in-place where the user clicked."""
open a context menu in-place.
"""
if model := self._getModel(): if model := self._getModel():
if point is None: if point is None:
point = self.visualRect(self.currentIndex()).center() point = self.visualRect(self.currentIndex()).center()
@@ -987,7 +943,6 @@ class GuiProjectTree(QTreeView):
if viewport := self.viewport(): if viewport := self.viewport():
ctxMenu.exec(viewport.mapToGlobal(point)) ctxMenu.exec(viewport.mapToGlobal(point))
ctxMenu.setParent(None) ctxMenu.setParent(None)
return
## ##
# Private Slots # Private Slots
@@ -995,10 +950,9 @@ class GuiProjectTree(QTreeView):
@pyqtSlot(QModelIndex, QModelIndex) @pyqtSlot(QModelIndex, QModelIndex)
def _onSelectionChange(self, current: QModelIndex, previous: QModelIndex) -> None: def _onSelectionChange(self, current: QModelIndex, previous: QModelIndex) -> None:
"""The user changed which item is selected.""" """Process user changing which item is selected."""
if node := self._getNode(current): if node := self._getNode(current):
self.projView.selectedItemChanged.emit(node.item.itemHandle) self.projView.selectedItemChanged.emit(node.item.itemHandle)
return
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _onDoubleClick(self, index: QModelIndex) -> None: def _onDoubleClick(self, index: QModelIndex) -> None:
@@ -1012,21 +966,18 @@ class GuiProjectTree(QTreeView):
) )
else: else:
self.setExpanded(index, not self.isExpanded(index)) self.setExpanded(index, not self.isExpanded(index))
return
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _onNodeCollapsed(self, index: QModelIndex) -> None: def _onNodeCollapsed(self, index: QModelIndex) -> None:
"""Capture a node collapse, and pass it to the model.""" """Capture a node collapse, and pass it to the model."""
if node := self._getNode(index): if node := self._getNode(index):
node.setExpanded(False) node.setExpanded(False)
return
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _onNodeExpanded(self, index: QModelIndex) -> None: def _onNodeExpanded(self, index: QModelIndex) -> None:
"""Capture a node expand, and pass it to the model.""" """Capture a node expand, and pass it to the model."""
if node := self._getNode(index): if node := self._getNode(index):
node.setExpanded(True) node.setExpanded(True)
return
## ##
# Internal Functions # Internal Functions
@@ -1038,7 +989,6 @@ class GuiProjectTree(QTreeView):
if model := self.selectionModel(): if model := self.selectionModel():
# Selection model can be None (#2173) # Selection model can be None (#2173)
model.clearCurrentIndex() model.clearCurrentIndex()
return
def _selectedRows(self) -> list[QModelIndex]: def _selectedRows(self) -> list[QModelIndex]:
"""Return all column 0 indexes.""" """Return all column 0 indexes."""
@@ -1066,7 +1016,6 @@ class _UpdatableMenu(QMenu):
self._map: dict[str, QAction] = {} self._map: dict[str, QAction] = {}
self.setTitle(self.tr("From Template")) self.setTitle(self.tr("From Template"))
self.triggered.connect(self._actionTriggered) self.triggered.connect(self._actionTriggered)
return
def __contains__(self, tHandle: str) -> bool: def __contains__(self, tHandle: str) -> bool:
"""Look up a handle in the menu.""" """Look up a handle in the menu."""
@@ -1088,7 +1037,6 @@ class _UpdatableMenu(QMenu):
self.addAction(action) self.addAction(action)
self._map[tHandle] = action self._map[tHandle] = action
self.setActionsVisible(True) self.setActionsVisible(True)
return
def remove(self, tHandle: str) -> None: def remove(self, tHandle: str) -> None:
"""Remove a template item.""" """Remove a template item."""
@@ -1096,19 +1044,16 @@ class _UpdatableMenu(QMenu):
self.removeAction(action) self.removeAction(action)
if not self._map: if not self._map:
self.setActionsVisible(False) self.setActionsVisible(False)
return
def clearMenu(self) -> None: def clearMenu(self) -> None:
"""Clear all menu content.""" """Clear all menu content."""
self._map.clear() self._map.clear()
self.clear() self.clear()
return
def setActionsVisible(self, value: bool) -> None: def setActionsVisible(self, value: bool) -> None:
"""Set the visibility of root action.""" """Set the visibility of root action."""
if action := self.menuAction(): if action := self.menuAction():
action.setVisible(value) action.setVisible(value)
return
## ##
# Private Slots # Private Slots
@@ -1118,7 +1063,6 @@ class _UpdatableMenu(QMenu):
def _actionTriggered(self, action: QAction) -> None: def _actionTriggered(self, action: QAction) -> None:
"""Translate the menu trigger into an item trigger.""" """Translate the menu trigger into an item trigger."""
self.menuItemTriggered.emit(str(action.data())) self.menuItemTriggered.emit(str(action.data()))
return
class _TreeContextMenu(QMenu): class _TreeContextMenu(QMenu):
@@ -1139,11 +1083,9 @@ class _TreeContextMenu(QMenu):
self._indices = indices self._indices = indices
self._children = node.childCount() > 0 self._children = node.childCount() > 0
logger.debug("Ready: _TreeContextMenu") logger.debug("Ready: _TreeContextMenu")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: _TreeContextMenu") logger.debug("Delete: _TreeContextMenu")
return
## ##
# Methods # Methods
@@ -1155,7 +1097,6 @@ class _TreeContextMenu(QMenu):
action.triggered.connect(self._tree.emptyTrash) action.triggered.connect(self._tree.emptyTrash)
if self._children: if self._children:
self._expandCollapse() self._expandCollapse()
return
def buildSingleSelectMenu(self) -> None: def buildSingleSelectMenu(self) -> None:
"""Build the single-select menu.""" """Build the single-select menu."""
@@ -1191,15 +1132,12 @@ class _TreeContextMenu(QMenu):
action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle)) action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle))
self._deleteOrTrash() self._deleteOrTrash()
return
def buildMultiSelectMenu(self) -> None: def buildMultiSelectMenu(self) -> None:
"""Build the multi-select menu.""" """Build the multi-select menu."""
self._itemActive() self._itemActive()
self._itemStatusImport(True) self._itemStatusImport(True)
self.addSeparator() self.addSeparator()
self._deleteOrTrash() self._deleteOrTrash()
return
## ##
# Menu Builders # Menu Builders
@@ -1217,7 +1155,6 @@ class _TreeContextMenu(QMenu):
self._view.openDocumentRequest.emit, self._view.openDocumentRequest.emit,
self._handle, nwDocMode.VIEW, "", False self._handle, nwDocMode.VIEW, "", False
)) ))
return
def _itemCreation(self) -> None: def _itemCreation(self) -> None:
"""Add create item actions.""" """Add create item actions."""
@@ -1228,7 +1165,6 @@ class _TreeContextMenu(QMenu):
menu.addAction(self._view.projBar.aAddEmpty) menu.addAction(self._view.projBar.aAddEmpty)
menu.addAction(self._view.projBar.aAddNote) menu.addAction(self._view.projBar.aAddNote)
menu.addAction(self._view.projBar.aAddFolder) menu.addAction(self._view.projBar.aAddFolder)
return
def _itemHeader(self) -> None: def _itemHeader(self) -> None:
"""Check if there is a header that can be used for rename.""" """Check if there is a header that can be used for rename."""
@@ -1238,7 +1174,6 @@ class _TreeContextMenu(QMenu):
action.triggered.connect( action.triggered.connect(
qtLambda(self._view.renameTreeItem, self._handle, hItem.title) qtLambda(self._view.renameTreeItem, self._handle, hItem.title)
) )
return
def _itemActive(self) -> None: def _itemActive(self) -> None:
"""Add Active/Inactive actions.""" """Add Active/Inactive actions."""
@@ -1253,7 +1188,6 @@ class _TreeContextMenu(QMenu):
else: else:
action = qtAddAction(self, self.tr("Toggle Active")) action = qtAddAction(self, self.tr("Toggle Active"))
action.triggered.connect(self._toggleItemActive) action.triggered.connect(self._toggleItemActive)
return
def _itemStatusImport(self, multi: bool) -> None: def _itemStatusImport(self, multi: bool) -> None:
"""Add actions for changing status or importance.""" """Add actions for changing status or importance."""
@@ -1295,7 +1229,6 @@ class _TreeContextMenu(QMenu):
self._view.projectSettingsRequest.emit, self._view.projectSettingsRequest.emit,
GuiProjectSettings.PAGE_IMPORT GuiProjectSettings.PAGE_IMPORT
)) ))
return
def _itemTransform(self, isFile: bool, isFolder: bool) -> None: def _itemTransform(self, isFile: bool, isFolder: bool) -> None:
"""Add actions for the Transform menu.""" """Add actions for the Transform menu."""
@@ -1338,15 +1271,12 @@ class _TreeContextMenu(QMenu):
action = qtAddAction(menu, self.tr("Split Document by Headings")) action = qtAddAction(menu, self.tr("Split Document by Headings"))
action.triggered.connect(qtLambda(self._tree.splitDocument, self._handle)) action.triggered.connect(qtLambda(self._tree.splitDocument, self._handle))
return
def _expandCollapse(self) -> None: def _expandCollapse(self) -> None:
"""Add actions for expand and collapse.""" """Add actions for expand and collapse."""
action = qtAddAction(self, self.tr("Expand All")) action = qtAddAction(self, self.tr("Expand All"))
action.triggered.connect(qtLambda(self._tree.expandFromIndex, self._indices[0])) action.triggered.connect(qtLambda(self._tree.expandFromIndex, self._indices[0]))
action = qtAddAction(self, self.tr("Collapse All")) action = qtAddAction(self, self.tr("Collapse All"))
action.triggered.connect(qtLambda(self._tree.collapseFromIndex, self._indices[0])) action.triggered.connect(qtLambda(self._tree.collapseFromIndex, self._indices[0]))
return
def _deleteOrTrash(self) -> None: def _deleteOrTrash(self) -> None:
"""Add move to Trash action.""" """Add move to Trash action."""
@@ -1359,7 +1289,6 @@ class _TreeContextMenu(QMenu):
text = self.tr("Move to Trash") text = self.tr("Move to Trash")
action = qtAddAction(self, text) action = qtAddAction(self, text)
action.triggered.connect(self._tree.processDeleteRequest) action.triggered.connect(self._tree.processDeleteRequest)
return
## ##
# Private Slots # Private Slots
@@ -1371,7 +1300,6 @@ class _TreeContextMenu(QMenu):
if self._item.isFileType(): if self._item.isFileType():
self._item.setActive(not self._item.isActive) self._item.setActive(not self._item.isActive)
self._item.notifyToRefresh() self._item.notifyToRefresh()
return
## ##
# Internal Functions # Internal Functions
@@ -1385,13 +1313,11 @@ class _TreeContextMenu(QMenu):
node.item.setActive(state) node.item.setActive(state)
refresh.append(node.item.itemHandle) refresh.append(node.item.itemHandle)
SHARED.project.tree.refreshItems(refresh) SHARED.project.tree.refreshItems(refresh)
return
def _changeItemStatus(self, key: str) -> None: def _changeItemStatus(self, key: str) -> None:
"""Set a new status value of an item.""" """Set a new status value of an item."""
self._item.setStatus(key) self._item.setStatus(key)
self._item.notifyToRefresh() self._item.notifyToRefresh()
return
def _iterSetItemStatus(self, key: str) -> None: def _iterSetItemStatus(self, key: str) -> None:
"""Change the status value for multiple items.""" """Change the status value for multiple items."""
@@ -1401,13 +1327,11 @@ class _TreeContextMenu(QMenu):
node.item.setStatus(key) node.item.setStatus(key)
refresh.append(node.item.itemHandle) refresh.append(node.item.itemHandle)
SHARED.project.tree.refreshItems(refresh) SHARED.project.tree.refreshItems(refresh)
return
def _changeItemImport(self, key: str) -> None: def _changeItemImport(self, key: str) -> None:
"""Set a new importance value of an item.""" """Set a new importance value of an item."""
self._item.setImport(key) self._item.setImport(key)
self._item.notifyToRefresh() self._item.notifyToRefresh()
return
def _iterSetItemImport(self, key: str) -> None: def _iterSetItemImport(self, key: str) -> None:
"""Change the status value for multiple items.""" """Change the status value for multiple items."""
@@ -1417,7 +1341,6 @@ class _TreeContextMenu(QMenu):
node.item.setImport(key) node.item.setImport(key)
refresh.append(node.item.itemHandle) refresh.append(node.item.itemHandle)
SHARED.project.tree.refreshItems(refresh) SHARED.project.tree.refreshItems(refresh)
return
def _changeItemLayout(self, itemLayout: nwItemLayout) -> None: def _changeItemLayout(self, itemLayout: nwItemLayout) -> None:
"""Set a new item layout value of an item.""" """Set a new item layout value of an item."""
@@ -1428,7 +1351,6 @@ class _TreeContextMenu(QMenu):
elif itemLayout == nwItemLayout.NOTE: elif itemLayout == nwItemLayout.NOTE:
self._item.setLayout(nwItemLayout.NOTE) self._item.setLayout(nwItemLayout.NOTE)
self._item.notifyToRefresh() self._item.notifyToRefresh()
return
def _convertFolderToFile(self, itemLayout: nwItemLayout) -> None: def _convertFolderToFile(self, itemLayout: nwItemLayout) -> None:
"""Convert a folder to a note or document.""" """Convert a folder to a note or document."""
@@ -1448,4 +1370,3 @@ class _TreeContextMenu(QMenu):
self._item.notifyToRefresh() self._item.notifyToRefresh()
else: else:
logger.info("Folder conversion cancelled") logger.info("Folder conversion cancelled")
return
+2 -19
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
class GuiProjectSearch(QWidget): class GuiProjectSearch(QWidget):
"""GUI: Project Search Panel."""
C_NAME = 0 C_NAME = 0
C_RESULT = 0 C_RESULT = 0
@@ -151,8 +152,6 @@ class GuiProjectSearch(QWidget):
logger.debug("Ready: GuiProjectSearch") logger.debug("Ready: GuiProjectSearch")
return
## ##
# Methods # Methods
## ##
@@ -174,8 +173,6 @@ class GuiProjectSearch(QWidget):
self.toggleWord.setIcon(SHARED.theme.getIcon("search_word")) self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex")) self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
return
def processReturn(self) -> None: def processReturn(self) -> None:
"""Process a return keypress forwarded from the main GUI.""" """Process a return keypress forwarded from the main GUI."""
if self.searchText.hasFocus(): if self.searchText.hasFocus():
@@ -189,7 +186,6 @@ class GuiProjectSearch(QWidget):
self.openDocumentSelectRequest.emit( self.openDocumentSelectRequest.emit(
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
) )
return
def beginSearch(self, text: str = "") -> None: def beginSearch(self, text: str = "") -> None:
"""Focus the search box and select its text, if any.""" """Focus the search box and select its text, if any."""
@@ -198,20 +194,17 @@ class GuiProjectSearch(QWidget):
if text: if text:
self.searchText.setText(text.partition("\n")[0]) self.searchText.setText(text.partition("\n")[0])
self.searchText.selectAll() self.searchText.selectAll()
return
def closeProjectTasks(self) -> None: def closeProjectTasks(self) -> None:
"""Run close project tasks.""" """Run close project tasks."""
self._map = {} self._map = {}
self.searchText.clear() self.searchText.clear()
self.searchResult.clear() self.searchResult.clear()
return
def refreshCurrentSearch(self) -> None: def refreshCurrentSearch(self) -> None:
"""Refresh the search if there is one.""" """Refresh the search if there is one."""
if self.searchResult.topLevelItemCount() > 0: if self.searchResult.topLevelItemCount() > 0:
self._processSearch() self._processSearch()
return
## ##
# Events # Events
@@ -238,7 +231,6 @@ class GuiProjectSearch(QWidget):
self.searchText.setFocus() self.searchText.setFocus()
else: else:
super().keyPressEvent(event) super().keyPressEvent(event)
return
## ##
# Public Slots # Public Slots
@@ -252,7 +244,6 @@ class GuiProjectSearch(QWidget):
results, capped = self._search.searchText(SHARED.mainGui.docEditor.getText()) results, capped = self._search.searchText(SHARED.mainGui.docEditor.getText())
self._displayResultSet(SHARED.project.tree[tHandle], results, capped) self._displayResultSet(SHARED.project.tree[tHandle], results, capped)
logger.debug("Updated search for '%s' in %.3f ms", tHandle, 1000*(time() - start)) logger.debug("Updated search for '%s' in %.3f ms", tHandle, 1000*(time() - start))
return
## ##
# Private Slots # Private Slots
@@ -278,7 +269,6 @@ class GuiProjectSearch(QWidget):
self._time = time() self._time = time()
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
self._blocked = False self._blocked = False
return
@pyqtSlot() @pyqtSlot()
def _searchResultSelected(self) -> None: def _searchResultSelected(self) -> None:
@@ -288,7 +278,6 @@ class GuiProjectSearch(QWidget):
self.selectedItemChanged.emit(str(data[0])) self.selectedItemChanged.emit(str(data[0]))
elif data := items[0].data(0, self.D_HANDLE): elif data := items[0].data(0, self.D_HANDLE):
self.selectedItemChanged.emit(str(data)) self.selectedItemChanged.emit(str(data))
return
@pyqtSlot("QTreeWidgetItem*", int) @pyqtSlot("QTreeWidgetItem*", int)
def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None: def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
@@ -297,28 +286,24 @@ class GuiProjectSearch(QWidget):
self.openDocumentSelectRequest.emit( self.openDocumentSelectRequest.emit(
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), True str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), True
) )
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _toggleCase(self, state: bool) -> None: def _toggleCase(self, state: bool) -> None:
"""Enable/disable case sensitive mode.""" """Enable/disable case sensitive mode."""
CONFIG.searchProjCase = state CONFIG.searchProjCase = state
self.refreshCurrentSearch() self.refreshCurrentSearch()
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _toggleWord(self, state: bool) -> None: def _toggleWord(self, state: bool) -> None:
"""Enable/disable whole word search mode.""" """Enable/disable whole word search mode."""
CONFIG.searchProjWord = state CONFIG.searchProjWord = state
self.refreshCurrentSearch() self.refreshCurrentSearch()
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _toggleRegEx(self, state: bool) -> None: def _toggleRegEx(self, state: bool) -> None:
"""Enable/disable regular expression search mode.""" """Enable/disable regular expression search mode."""
CONFIG.searchProjRegEx = state CONFIG.searchProjRegEx = state
self.refreshCurrentSearch() self.refreshCurrentSearch()
return
## ##
# Internal Functions # Internal Functions
@@ -360,5 +345,3 @@ class GuiProjectSearch(QWidget):
self.searchResult.setFirstColumnSpanned(i, parent, True) self.searchResult.setFirstColumnSpanned(i, parent, True)
QApplication.processEvents() QApplication.processEvents()
return
+2 -7
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -45,6 +45,7 @@ logger = logging.getLogger(__name__)
class GuiSideBar(QWidget): class GuiSideBar(QWidget):
"""GUI: Main Window SideBar."""
requestViewChange = pyqtSignal(nwView) requestViewChange = pyqtSignal(nwView)
@@ -126,8 +127,6 @@ class GuiSideBar(QWidget):
logger.debug("Ready: GuiSideBar") logger.debug("Ready: GuiSideBar")
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Initialise GUI elements that depend on specific settings.""" """Initialise GUI elements that depend on specific settings."""
buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
@@ -153,8 +152,6 @@ class GuiSideBar(QWidget):
self._setThemeModeIcon() self._setThemeModeIcon()
return
## ##
# Private Slots # Private Slots
## ##
@@ -171,7 +168,6 @@ class GuiSideBar(QWidget):
CONFIG.themeMode = nwTheme.AUTO CONFIG.themeMode = nwTheme.AUTO
self.mainGui.checkThemeUpdate() self.mainGui.checkThemeUpdate()
self._setThemeModeIcon() self._setThemeModeIcon()
return
## ##
# Internal Functions # Internal Functions
@@ -181,7 +177,6 @@ class GuiSideBar(QWidget):
"""Set the theme button icon.""" """Set the theme button icon."""
self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode]) self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode])
self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode])) self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode]))
return
class _PopRightMenu(QMenu): class _PopRightMenu(QMenu):
+2 -19
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -41,6 +41,7 @@ logger = logging.getLogger(__name__)
class GuiMainStatus(QStatusBar): class GuiMainStatus(QStatusBar):
"""GUI: Main Window Status Bar."""
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -112,8 +113,6 @@ class GuiMainStatus(QStatusBar):
self.updateTheme() self.updateTheme()
self.clearStatus() self.clearStatus()
return
def initSettings(self) -> None: def initSettings(self) -> None:
"""Apply user settings.""" """Apply user settings."""
if CONFIG.useCharCount: if CONFIG.useCharCount:
@@ -122,7 +121,6 @@ class GuiMainStatus(QStatusBar):
else: else:
self._trStatsCount = trStats(nwLabels.STATS_DISPLAY[nwStats.WORDS]) self._trStatsCount = trStats(nwLabels.STATS_DISPLAY[nwStats.WORDS])
self._trStatsTip = self.tr("Total word count (session change)") self._trStatsTip = self.tr("Total word count (session change)")
return
def clearStatus(self) -> None: def clearStatus(self) -> None:
"""Reset all widgets on the status bar to default values.""" """Reset all widgets on the status bar to default values."""
@@ -132,7 +130,6 @@ class GuiMainStatus(QStatusBar):
self.setProjectStatus(None) self.setProjectStatus(None)
self.setDocumentStatus(None) self.setDocumentStatus(None)
self.updateTime() self.updateTime()
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
@@ -149,8 +146,6 @@ class GuiMainStatus(QStatusBar):
self.docIcon.setColors(colNone, colSaved, colUnsaved) self.docIcon.setColors(colNone, colSaved, colUnsaved)
self.projIcon.setColors(colNone, colSaved, colUnsaved) self.projIcon.setColors(colNone, colSaved, colUnsaved)
return
## ##
# Setters # Setters
## ##
@@ -158,17 +153,14 @@ class GuiMainStatus(QStatusBar):
def setRefTime(self, refTime: float) -> None: def setRefTime(self, refTime: float) -> None:
"""Set the reference time for the status bar clock.""" """Set the reference time for the status bar clock."""
self._refTime = refTime self._refTime = refTime
return
def setProjectStatus(self, state: bool | None) -> None: def setProjectStatus(self, state: bool | None) -> None:
"""Set the project status colour icon.""" """Set the project status colour icon."""
self.projIcon.setState(state) self.projIcon.setState(state)
return
def setDocumentStatus(self, state: bool | None) -> None: def setDocumentStatus(self, state: bool | None) -> None:
"""Set the document status colour icon.""" """Set the document status colour icon."""
self.docIcon.setState(state) self.docIcon.setState(state)
return
def setUserIdle(self, idle: bool) -> None: def setUserIdle(self, idle: bool) -> None:
"""Change the idle status icon.""" """Change the idle status icon."""
@@ -180,13 +172,11 @@ class GuiMainStatus(QStatusBar):
else: else:
self.timeIcon.setPixmap(self.timePixmap) self.timeIcon.setPixmap(self.timePixmap)
self._userIdle = idle self._userIdle = idle
return
def setProjectStats(self, pWC: int, sWC: int) -> None: def setProjectStats(self, pWC: int, sWC: int) -> None:
"""Update the current project statistics.""" """Update the current project statistics."""
self.statsText.setText(self._trStatsCount.format(f"{pWC:n}", f"{sWC:+n}")) self.statsText.setText(self._trStatsCount.format(f"{pWC:n}", f"{sWC:+n}"))
self.statsText.setToolTip(self._trStatsTip) self.statsText.setToolTip(self._trStatsTip)
return
def updateTime(self, idleTime: float = 0.0) -> None: def updateTime(self, idleTime: float = 0.0) -> None:
"""Update the session clock.""" """Update the session clock."""
@@ -198,7 +188,6 @@ class GuiMainStatus(QStatusBar):
else: else:
sessTime = round(time() - self._refTime) sessTime = round(time() - self._refTime)
self.timeText.setText(formatTime(sessTime)) self.timeText.setText(formatTime(sessTime))
return
## ##
# Public Slots # Public Slots
@@ -209,7 +198,6 @@ class GuiMainStatus(QStatusBar):
"""Set the status bar message to display.""" """Set the status bar message to display."""
self.showMessage(message, nwConst.STATUS_MSG_TIMEOUT) self.showMessage(message, nwConst.STATUS_MSG_TIMEOUT)
QApplication.processEvents() QApplication.processEvents()
return
@pyqtSlot(str, str) @pyqtSlot(str, str)
def setLanguage(self, language: str, provider: str) -> None: def setLanguage(self, language: str, provider: str) -> None:
@@ -220,19 +208,16 @@ class GuiMainStatus(QStatusBar):
else: else:
self.langText.setText(QLocale(language).nativeLanguageName().title()) self.langText.setText(QLocale(language).nativeLanguageName().title())
self.langText.setToolTip(f"{language} ({provider})" if provider else language) self.langText.setToolTip(f"{language} ({provider})" if provider else language)
return
@pyqtSlot(bool) @pyqtSlot(bool)
def updateProjectStatus(self, status: bool) -> None: def updateProjectStatus(self, status: bool) -> None:
"""Update the project status.""" """Update the project status."""
self.setProjectStatus(not status) self.setProjectStatus(not status)
return
@pyqtSlot(bool) @pyqtSlot(bool)
def updateDocumentStatus(self, status: bool) -> None: def updateDocumentStatus(self, status: bool) -> None:
"""Update the document status.""" """Update the document status."""
self.setDocumentStatus(not status) self.setDocumentStatus(not status)
return
## ##
# Private Slots # Private Slots
@@ -244,7 +229,6 @@ class GuiMainStatus(QStatusBar):
state = not CONFIG.showSessionTime state = not CONFIG.showSessionTime
self.timeText.setVisible(state) self.timeText.setVisible(state)
CONFIG.showSessionTime = state CONFIG.showSessionTime = state
return
## ##
# Debug # Debug
@@ -279,4 +263,3 @@ class GuiMainStatus(QStatusBar):
) )
self.showMessage(f"Debug [{stamp}] {message}", 6000) self.showMessage(f"Debug [{stamp}] {message}", 6000)
logger.debug("[MEMINFO] %s", message) logger.debug("[MEMINFO] %s", message)
return
+8 -23
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -58,6 +58,7 @@ STYLES_BIG_TOOLBUTTON = "bigToolButton"
@dataclass @dataclass
class ThemeEntry: class ThemeEntry:
"""Theme data."""
name: str name: str
dark: bool dark: bool
@@ -65,6 +66,7 @@ class ThemeEntry:
class ThemeMeta: class ThemeMeta:
"""Theme meta data."""
name: str = "" name: str = ""
mode: str = "" mode: str = ""
@@ -74,6 +76,7 @@ class ThemeMeta:
class IconsMeta: class IconsMeta:
"""Icon theme meta data."""
name: str = "" name: str = ""
author: str = "" author: str = ""
@@ -81,6 +84,7 @@ class IconsMeta:
class SyntaxColors: class SyntaxColors:
"""Colours for the syntax highlighter."""
back: QColor = QColor(255, 255, 255) back: QColor = QColor(255, 255, 255)
text: QColor = QColor(0, 0, 0) text: QColor = QColor(0, 0, 0)
@@ -106,7 +110,7 @@ class SyntaxColors:
class GuiTheme: class GuiTheme:
"""Gui Theme Class """Gui Theme Class.
Handles the look and feel of novelWriter. Handles the look and feel of novelWriter.
""" """
@@ -190,8 +194,6 @@ class GuiTheme:
logger.debug("Text 'N' Height: %d", self.textNHeight) logger.debug("Text 'N' Height: %d", self.textNHeight)
logger.debug("Text 'N' Width: %d", self.textNWidth) logger.debug("Text 'N' Width: %d", self.textNWidth)
return
## ##
# Properties # Properties
## ##
@@ -206,7 +208,7 @@ class GuiTheme:
## ##
def getTextWidth(self, text: str, font: QFont | None = None) -> int: def getTextWidth(self, text: str, font: QFont | None = None) -> int:
"""Returns the width needed to contain a given piece of text in """Return the width needed to contain a given piece of text in
pixels. pixels.
""" """
if isinstance(font, QFont): if isinstance(font, QFont):
@@ -238,8 +240,6 @@ class GuiTheme:
self.iconCache.initIcons() self.iconCache.initIcons()
self.loadTheme() self.loadTheme()
return
def isDesktopDarkMode(self) -> bool: def isDesktopDarkMode(self) -> bool:
"""Check if the desktop is in dark mode.""" """Check if the desktop is in dark mode."""
if CONFIG.verQtValue >= 0x060500 and (hint := QGuiApplication.styleHints()): if CONFIG.verQtValue >= 0x060500 and (hint := QGuiApplication.styleHints()):
@@ -507,7 +507,6 @@ class GuiTheme:
"""Set the colour for a named colour.""" """Set the colour for a named colour."""
self._qColors[key] = QColor(color) self._qColors[key] = QColor(color)
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8") self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
return
def _resetTheme(self) -> None: def _resetTheme(self) -> None:
"""Reset GUI colours to default values.""" """Reset GUI colours to default values."""
@@ -559,8 +558,6 @@ class GuiTheme:
self._setBaseColor("inactive", red) self._setBaseColor("inactive", red)
self._setBaseColor("disabled", faded) self._setBaseColor("disabled", faded)
return
def _readColor(self, parser: ConfigParser, section: str, name: str) -> QColor: def _readColor(self, parser: ConfigParser, section: str, name: str) -> QColor:
"""Parse a colour value from a config string.""" """Parse a colour value from a config string."""
return self.parseColor(parser.get(section, name, fallback="default")) return self.parseColor(parser.get(section, name, fallback="default"))
@@ -570,7 +567,6 @@ class GuiTheme:
) -> None: ) -> None:
"""Set a palette colour value from a config string.""" """Set a palette colour value from a config string."""
self._guiPalette.setBrush(value, self._readColor(parser, section, name)) self._guiPalette.setBrush(value, self._readColor(parser, section, name))
return
def _buildStyleSheets(self, palette: QPalette) -> None: def _buildStyleSheets(self, palette: QPalette) -> None:
"""Build default style sheets.""" """Build default style sheets."""
@@ -602,8 +598,6 @@ class GuiTheme:
"QToolButton::menu-indicator {image: none;} " "QToolButton::menu-indicator {image: none;} "
) )
return
def _scanThemes(self, files: list[Path]) -> None: def _scanThemes(self, files: list[Path]) -> None:
"""Scan the GUI themes folder and list all themes.""" """Scan the GUI themes folder and list all themes."""
parser = ConfigParser() parser = ConfigParser()
@@ -631,8 +625,6 @@ class GuiTheme:
logger.debug("Checking theme config '%s'", key) logger.debug("Checking theme config '%s'", key)
self._allThemes[key] = ThemeEntry(name, dark, item) self._allThemes[key] = ThemeEntry(name, dark, item)
return
class GuiIcons: class GuiIcons:
"""The icon class manages the content of the assets/icons folder, """The icon class manages the content of the assets/icons folder,
@@ -672,8 +664,6 @@ class GuiIcons:
# None Icon # None Icon
self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg")) self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg"))
return
def clear(self) -> None: def clear(self) -> None:
"""Clear the icon cache.""" """Clear the icon cache."""
self._svgData = {} self._svgData = {}
@@ -681,7 +671,6 @@ class GuiIcons:
self._headerDec = [] self._headerDec = []
self._headerDecNarrow = [] self._headerDecNarrow = []
self._meta = ThemeMeta() self._meta = ThemeMeta()
return
## ##
# Properties # Properties
@@ -703,7 +692,6 @@ class GuiIcons:
_listContent(icons, CONFIG.assetPath("icons"), ".icons") _listContent(icons, CONFIG.assetPath("icons"), ".icons")
_listContent(icons, CONFIG.dataPath("icons"), ".icons") _listContent(icons, CONFIG.dataPath("icons"), ".icons")
self._scanThemes(icons) self._scanThemes(icons)
return
def loadTheme(self, theme: str) -> None: def loadTheme(self, theme: str) -> None:
"""Update the theme map. This is more of an init, since many of """Update the theme map. This is more of an init, since many of
@@ -781,7 +769,7 @@ class GuiIcons:
self, tType: nwItemType, tClass: nwItemClass, tLayout: nwItemLayout, hLevel: str = "H0" self, tType: nwItemType, tClass: nwItemClass, tLayout: nwItemLayout, hLevel: str = "H0"
) -> QIcon: ) -> QIcon:
"""Get the correct icon for a project item based on type, class """Get the correct icon for a project item based on type, class
and heading level and heading level.
""" """
name = None name = None
color = "default" color = "default"
@@ -942,8 +930,6 @@ class GuiIcons:
logger.debug("Checking icon theme '%s'", key) logger.debug("Checking icon theme '%s'", key)
self._allThemes[key] = ThemeEntry(name, False, item) self._allThemes[key] = ThemeEntry(name, False, item)
return
# Module Functions # Module Functions
# ================ # ================
@@ -952,4 +938,3 @@ def _listContent(data: list[Path], path: Path, extension: str) -> None:
"""List files of a specific type and extend the list.""" """List files of a specific type and extend the list."""
if path.is_dir(): if path.is_dir():
data.extend(n for n in path.iterdir() if n.is_file() and n.suffix == extension) data.extend(n for n in path.iterdir() if n.is_file() and n.suffix == extension)
return
+3 -64
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -68,7 +68,7 @@ logger = logging.getLogger(__name__)
class GuiMain(QMainWindow): class GuiMain(QMainWindow):
"""Main GUI Window """Main GUI Window.
The Main GUI window class is the entry point of the application. It The Main GUI window class is the entry point of the application. It
is split up into GUI components, assembled in the init function. is split up into GUI components, assembled in the init function.
@@ -323,13 +323,10 @@ class GuiMain(QMainWindow):
self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ...")) self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ..."))
CONFIG.splashMessage("novelWriter is ready ...") CONFIG.splashMessage("novelWriter is ready ...")
return
def initMain(self) -> None: def initMain(self) -> None:
"""Initialise elements that depend on user settings.""" """Initialise elements that depend on user settings."""
self.asProjTimer.setInterval(int(CONFIG.autoSaveProj*1000)) self.asProjTimer.setInterval(int(CONFIG.autoSaveProj*1000))
self.asDocTimer.setInterval(int(CONFIG.autoSaveDoc*1000)) self.asDocTimer.setInterval(int(CONFIG.autoSaveDoc*1000))
return
def postLaunchTasks(self, cmdOpen: str | None) -> None: def postLaunchTasks(self, cmdOpen: str | None) -> None:
"""Process tasks after the main window has been created.""" """Process tasks after the main window has been created."""
@@ -351,8 +348,6 @@ class GuiMain(QMainWindow):
# before showing any dialogs # before showing any dialogs
QTimer.singleShot(50, self.showPostLaunchDialogs) QTimer.singleShot(50, self.showPostLaunchDialogs)
return
@pyqtSlot() @pyqtSlot()
def showPostLaunchDialogs(self) -> None: def showPostLaunchDialogs(self) -> None:
"""Show post launch dialogs.""" """Show post launch dialogs."""
@@ -370,8 +365,6 @@ class GuiMain(QMainWindow):
).format(f"<a href='{nwConst.URL_RELEASES}'>", "</a>") ).format(f"<a href='{nwConst.URL_RELEASES}'>", "</a>")
SHARED.info(f"{trVersion}<br>{trRelease}") SHARED.info(f"{trVersion}<br>{trRelease}")
return
## ##
# Project Actions # Project Actions
## ##
@@ -534,7 +527,6 @@ class GuiMain(QMainWindow):
SHARED.setFocusMode(False) SHARED.setFocusMode(False)
self.saveDocument() self.saveDocument()
self.docEditor.clearEditor() self.docEditor.clearEditor()
return
def openDocument( def openDocument(
self, self,
@@ -591,7 +583,6 @@ class GuiMain(QMainWindow):
self.openDocument(nHandle, tLine=1, doScroll=True) self.openDocument(nHandle, tLine=1, doScroll=True)
elif wrapAround: elif wrapAround:
self.openDocument(fHandle, tLine=1, doScroll=True) self.openDocument(fHandle, tLine=1, doScroll=True)
return
def saveDocument(self, force: bool = False) -> None: def saveDocument(self, force: bool = False) -> None:
"""Save the current documents.""" """Save the current documents."""
@@ -599,13 +590,11 @@ class GuiMain(QMainWindow):
self.docEditor.saveCursorPosition() self.docEditor.saveCursorPosition()
if force or self.docEditor.docChanged: if force or self.docEditor.docChanged:
self.docEditor.saveText() self.docEditor.saveText()
return
@pyqtSlot() @pyqtSlot()
def forceSaveDocument(self) -> None: def forceSaveDocument(self) -> None:
"""Save document even of it has not changed.""" """Save document even of it has not changed."""
self.saveDocument(force=True) self.saveDocument(force=True)
return
def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool: def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool:
"""Load a document for viewing in the view panel.""" """Load a document for viewing in the view panel."""
@@ -760,8 +749,6 @@ class GuiMain(QMainWindow):
if not beQuiet: if not beQuiet:
SHARED.info(self.tr("The project index has been successfully rebuilt.")) SHARED.info(self.tr("The project index has been successfully rebuilt."))
return
## ##
# Main Dialogs # Main Dialogs
## ##
@@ -772,7 +759,6 @@ class GuiMain(QMainWindow):
dialog = GuiWelcome(self) dialog = GuiWelcome(self)
dialog.openProjectRequest.connect(self._openProjectFromWelcome) dialog.openProjectRequest.connect(self._openProjectFromWelcome)
dialog.exec() dialog.exec()
return
@pyqtSlot() @pyqtSlot()
def showPreferencesDialog(self) -> None: def showPreferencesDialog(self) -> None:
@@ -780,7 +766,6 @@ class GuiMain(QMainWindow):
dialog = GuiPreferences(self) dialog = GuiPreferences(self)
dialog.newPreferencesReady.connect(self._processConfigChanges) dialog.newPreferencesReady.connect(self._processConfigChanges)
dialog.exec() dialog.exec()
return
@pyqtSlot() @pyqtSlot()
@pyqtSlot(int) @pyqtSlot(int)
@@ -790,7 +775,6 @@ class GuiMain(QMainWindow):
dialog = GuiProjectSettings(self, gotoPage=focusTab) dialog = GuiProjectSettings(self, gotoPage=focusTab)
dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges) dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges)
dialog.exec() dialog.exec()
return
@pyqtSlot() @pyqtSlot()
def showNovelDetailsDialog(self) -> None: def showNovelDetailsDialog(self) -> None:
@@ -799,7 +783,6 @@ class GuiMain(QMainWindow):
dialog = GuiNovelDetails(self) dialog = GuiNovelDetails(self)
dialog.activateDialog() dialog.activateDialog()
dialog.updateValues() dialog.updateValues()
return
@pyqtSlot() @pyqtSlot()
def showBuildManuscriptDialog(self) -> None: def showBuildManuscriptDialog(self) -> None:
@@ -809,7 +792,6 @@ class GuiMain(QMainWindow):
dialog = GuiManuscript(self) dialog = GuiManuscript(self)
dialog.activateDialog() dialog.activateDialog()
dialog.loadContent() dialog.loadContent()
return
@pyqtSlot() @pyqtSlot()
def showProjectWordListDialog(self) -> None: def showProjectWordListDialog(self) -> None:
@@ -818,7 +800,6 @@ class GuiMain(QMainWindow):
dialog = GuiWordList(self) dialog = GuiWordList(self)
dialog.newWordListReady.connect(self._processWordListChanges) dialog.newWordListReady.connect(self._processWordListChanges)
dialog.exec() dialog.exec()
return
@pyqtSlot() @pyqtSlot()
def showWritingStatsDialog(self) -> None: def showWritingStatsDialog(self) -> None:
@@ -828,21 +809,18 @@ class GuiMain(QMainWindow):
dialog = GuiWritingStats(self) dialog = GuiWritingStats(self)
dialog.activateDialog() dialog.activateDialog()
dialog.populateGUI() dialog.populateGUI()
return
@pyqtSlot() @pyqtSlot()
def showAboutNWDialog(self) -> None: def showAboutNWDialog(self) -> None:
"""Show the novelWriter about dialog.""" """Show the novelWriter about dialog."""
dialog = GuiAbout(self) dialog = GuiAbout(self)
dialog.exec() dialog.exec()
return
@pyqtSlot() @pyqtSlot()
def showAboutQtDialog(self) -> None: def showAboutQtDialog(self) -> None:
"""Show the Qt about dialog.""" """Show the Qt about dialog."""
msgBox = QMessageBox(self) msgBox = QMessageBox(self)
msgBox.aboutQt(self, "About Qt") msgBox.aboutQt(self, "About Qt")
return
@pyqtSlot() @pyqtSlot()
def showDictionariesDialog(self) -> None: def showDictionariesDialog(self) -> None:
@@ -852,7 +830,6 @@ class GuiMain(QMainWindow):
if not dialog.initDialog(): if not dialog.initDialog():
dialog.close() dialog.close()
SHARED.error(self.tr("Could not initialise the dialog.")) SHARED.error(self.tr("Could not initialise the dialog."))
return
## ##
# Main Window Actions # Main Window Actions
@@ -915,7 +892,6 @@ class GuiMain(QMainWindow):
self.refreshThemeColors(syntax=True) self.refreshThemeColors(syntax=True)
self.docEditor.initEditor() self.docEditor.initEditor()
self.docViewer.initViewer() self.docViewer.initViewer()
return
def refreshThemeColors(self, syntax: bool = False, force: bool = False) -> None: def refreshThemeColors(self, syntax: bool = False, force: bool = False) -> None:
"""Refresh the GUI theme.""" """Refresh the GUI theme."""
@@ -937,8 +913,6 @@ class GuiMain(QMainWindow):
if syntax: if syntax:
self.docEditor.updateSyntaxColors() self.docEditor.updateSyntaxColors()
return
## ##
# Events # Events
## ##
@@ -947,14 +921,12 @@ class GuiMain(QMainWindow):
"""Capture application change events.""" """Capture application change events."""
if int(event.type()) == 210: # ThemeChange if int(event.type()) == 210: # ThemeChange
self.checkThemeUpdate() self.checkThemeUpdate()
return
def closeEvent(self, event: QCloseEvent) -> None: def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the closing event of the GUI and call the close """Capture the closing event of the GUI and call the close
function to handle all the close process steps. function to handle all the close process steps.
""" """
event.accept() if self.closeMain() else event.ignore() event.accept() if self.closeMain() else event.ignore()
return
## ##
# Public Slots # Public Slots
@@ -962,30 +934,26 @@ class GuiMain(QMainWindow):
@pyqtSlot() @pyqtSlot()
def toggleFullScreenMode(self) -> None: def toggleFullScreenMode(self) -> None:
"""Toggle full screen mode""" """Toggle full screen mode."""
self.setWindowState(self.windowState() ^ Qt.WindowState.WindowFullScreen) self.setWindowState(self.windowState() ^ Qt.WindowState.WindowFullScreen)
return
@pyqtSlot() @pyqtSlot()
def closeDocEditor(self) -> None: def closeDocEditor(self) -> None:
"""Close the document editor. This does not hide the editor.""" """Close the document editor. This does not hide the editor."""
self.closeDocument() self.closeDocument()
SHARED.project.data.setLastHandle(None, "editor") SHARED.project.data.setLastHandle(None, "editor")
return
@pyqtSlot() @pyqtSlot()
def closeDocViewer(self) -> None: def closeDocViewer(self) -> None:
"""Close the document viewer.""" """Close the document viewer."""
self.closeViewerPanel() self.closeViewerPanel()
SHARED.project.data.setLastHandle(None, "viewer") SHARED.project.data.setLastHandle(None, "viewer")
return
@pyqtSlot() @pyqtSlot()
def toggleFocusMode(self) -> None: def toggleFocusMode(self) -> None:
"""Toggle focus mode.""" """Toggle focus mode."""
if self.docEditor.docHandle: if self.docEditor.docHandle:
SHARED.setFocusMode(not SHARED.focusMode) SHARED.setFocusMode(not SHARED.focusMode)
return
## ##
# Private Slots # Private Slots
@@ -1003,7 +971,6 @@ class GuiMain(QMainWindow):
docViewer = True docViewer = True
self.docEditor.changeFocusState(docEditor) self.docEditor.changeFocusState(docEditor)
self.docViewer.changeFocusState(docViewer) self.docViewer.changeFocusState(docViewer)
return
@pyqtSlot(bool) @pyqtSlot(bool)
def _focusModeChanged(self, focusMode: bool) -> None: def _focusModeChanged(self, focusMode: bool) -> None:
@@ -1034,7 +1001,6 @@ class GuiMain(QMainWindow):
if cursorVisible: if cursorVisible:
self.docEditor.ensureCursorVisibleNoCentre() self.docEditor.ensureCursorVisibleNoCentre()
return
@pyqtSlot(nwFocus) @pyqtSlot(nwFocus)
def _switchFocus(self, paneNo: nwFocus) -> None: def _switchFocus(self, paneNo: nwFocus) -> None:
@@ -1082,8 +1048,6 @@ class GuiMain(QMainWindow):
self._changeView(nwView.OUTLINE, exitFocus=True) self._changeView(nwView.OUTLINE, exitFocus=True)
self.outlineView.setTreeFocus() self.outlineView.setTreeFocus()
return
@pyqtSlot(bool, bool, bool, bool) @pyqtSlot(bool, bool, bool, bool)
def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: bool) -> None: def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: bool) -> None:
"""Refresh GUI based on flags from the Preferences dialog.""" """Refresh GUI based on flags from the Preferences dialog."""
@@ -1115,8 +1079,6 @@ class GuiMain(QMainWindow):
"Some changes will not be applied until novelWriter has been restarted." "Some changes will not be applied until novelWriter has been restarted."
)) ))
return
@pyqtSlot() @pyqtSlot()
def _processProjectSettingsChanges(self) -> None: def _processProjectSettingsChanges(self) -> None:
"""Refresh data dependent on project settings.""" """Refresh data dependent on project settings."""
@@ -1124,7 +1086,6 @@ class GuiMain(QMainWindow):
SHARED.updateSpellCheckLanguage() SHARED.updateSpellCheckLanguage()
self.itemDetails.refreshDetails() self.itemDetails.refreshDetails()
self._updateWindowTitle(SHARED.project.data.name) self._updateWindowTitle(SHARED.project.data.name)
return
@pyqtSlot() @pyqtSlot()
def _processWordListChanges(self) -> None: def _processWordListChanges(self) -> None:
@@ -1132,7 +1093,6 @@ class GuiMain(QMainWindow):
logger.debug("Reloading word list") logger.debug("Reloading word list")
SHARED.updateSpellCheckLanguage(reload=True) SHARED.updateSpellCheckLanguage(reload=True)
self.docEditor.spellCheckDocument() self.docEditor.spellCheckDocument()
return
@pyqtSlot(str, nwDocMode) @pyqtSlot(str, nwDocMode)
def _followTag(self, tag: str, mode: nwDocMode) -> None: def _followTag(self, tag: str, mode: nwDocMode) -> None:
@@ -1151,7 +1111,6 @@ class GuiMain(QMainWindow):
self.openDocument(tHandle, sTitle=sTitle) self.openDocument(tHandle, sTitle=sTitle)
elif mode == nwDocMode.VIEW: elif mode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, sTitle=sTitle) self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return
@pyqtSlot(Path) @pyqtSlot(Path)
def _openProjectFromWelcome(self, path: Path) -> None: def _openProjectFromWelcome(self, path: Path) -> None:
@@ -1160,7 +1119,6 @@ class GuiMain(QMainWindow):
self.openProject(path) self.openProject(path)
if not SHARED.hasProject: if not SHARED.hasProject:
self.showWelcomeDialog() self.showWelcomeDialog()
return
@pyqtSlot(str, nwDocMode, str, bool) @pyqtSlot(str, nwDocMode, str, bool)
def _openDocument(self, tHandle: str, mode: nwDocMode, sTitle: str, setFocus: bool) -> None: def _openDocument(self, tHandle: str, mode: nwDocMode, sTitle: str, setFocus: bool) -> None:
@@ -1170,7 +1128,6 @@ class GuiMain(QMainWindow):
self.openDocument(tHandle, sTitle=sTitle, changeFocus=setFocus) self.openDocument(tHandle, sTitle=sTitle, changeFocus=setFocus)
elif mode == nwDocMode.VIEW: elif mode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, sTitle=sTitle) self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return
@pyqtSlot(str, int, int, bool) @pyqtSlot(str, int, int, bool)
def _openDocumentSelection( def _openDocumentSelection(
@@ -1179,7 +1136,6 @@ class GuiMain(QMainWindow):
"""Open a document and select a section of the text.""" """Open a document and select a section of the text."""
if self.openDocument(tHandle, changeFocus=changeFocus): if self.openDocument(tHandle, changeFocus=changeFocus):
self.docEditor.setCursorSelection(selStart, selLength) self.docEditor.setCursorSelection(selStart, selLength)
return
@pyqtSlot() @pyqtSlot()
def _reloadViewer(self) -> None: def _reloadViewer(self) -> None:
@@ -1188,7 +1144,6 @@ class GuiMain(QMainWindow):
# If the two panels have the same document, save any changes in the editor # If the two panels have the same document, save any changes in the editor
self.saveDocument() self.saveDocument()
self.docViewer.reloadText() self.docViewer.reloadText()
return
@pyqtSlot(nwView) @pyqtSlot(nwView)
def _changeView(self, view: nwView, exitFocus: bool = False) -> None: def _changeView(self, view: nwView, exitFocus: bool = False) -> None:
@@ -1219,8 +1174,6 @@ class GuiMain(QMainWindow):
isNovel = self.projStack.currentWidget() == self.novelView isNovel = self.projStack.currentWidget() == self.novelView
self.novelView.setActive(isMain and isNovel) self.novelView.setActive(isMain and isNovel)
return
@pyqtSlot(nwDocAction) @pyqtSlot(nwDocAction)
def _passDocumentAction(self, action: nwDocAction) -> None: def _passDocumentAction(self, action: nwDocAction) -> None:
"""Pass on a document action to the editor or viewer based on """Pass on a document action to the editor or viewer based on
@@ -1230,7 +1183,6 @@ class GuiMain(QMainWindow):
self.docEditor.docAction(action) self.docEditor.docAction(action)
elif self.docViewer.hasFocus(): elif self.docViewer.hasFocus():
self.docViewer.docAction(action) self.docViewer.docAction(action)
return
@pyqtSlot(str) @pyqtSlot(str)
@pyqtSlot(nwDocInsert) @pyqtSlot(nwDocInsert)
@@ -1240,14 +1192,12 @@ class GuiMain(QMainWindow):
""" """
if self.docEditor.hasFocus(): if self.docEditor.hasFocus():
self.docEditor.insertText(content) self.docEditor.insertText(content)
return
@pyqtSlot() @pyqtSlot()
def _toggleViewerPanelVisibility(self) -> None: def _toggleViewerPanelVisibility(self) -> None:
"""Toggle the visibility of the document viewer panel.""" """Toggle the visibility of the document viewer panel."""
CONFIG.showViewerPanel = not CONFIG.showViewerPanel CONFIG.showViewerPanel = not CONFIG.showViewerPanel
self.docViewerPanel.setVisible(CONFIG.showViewerPanel) self.docViewerPanel.setVisible(CONFIG.showViewerPanel)
return
@pyqtSlot() @pyqtSlot()
def _timeTick(self) -> None: def _timeTick(self) -> None:
@@ -1263,7 +1213,6 @@ class GuiMain(QMainWindow):
self._updateStatusWordCount() self._updateStatusWordCount()
if CONFIG.memInfo: # pragma: no cover if CONFIG.memInfo: # pragma: no cover
self.mainStatus.memInfo() self.mainStatus.memInfo()
return
@pyqtSlot() @pyqtSlot()
def _autoSaveProject(self) -> None: def _autoSaveProject(self) -> None:
@@ -1274,7 +1223,6 @@ class GuiMain(QMainWindow):
if doSave: if doSave:
logger.debug("Auto-saving project") logger.debug("Auto-saving project")
self.saveProject(autoSave=True) self.saveProject(autoSave=True)
return
@pyqtSlot() @pyqtSlot()
def _autoSaveDocument(self) -> None: def _autoSaveDocument(self) -> None:
@@ -1282,7 +1230,6 @@ class GuiMain(QMainWindow):
if SHARED.hasProject and self.docEditor.docChanged: if SHARED.hasProject and self.docEditor.docChanged:
logger.debug("Auto-saving document") logger.debug("Auto-saving document")
self.saveDocument() self.saveDocument()
return
@pyqtSlot() @pyqtSlot()
def _updateStatusWordCount(self) -> None: def _updateStatusWordCount(self) -> None:
@@ -1312,8 +1259,6 @@ class GuiMain(QMainWindow):
self.mainStatus.setProjectStats(cTotal, cTotal - iTotal) self.mainStatus.setProjectStats(cTotal, cTotal - iTotal)
return
@pyqtSlot() @pyqtSlot()
def _keyPressReturn(self) -> None: def _keyPressReturn(self) -> None:
"""Process a return or enter keypress in the main window.""" """Process a return or enter keypress in the main window."""
@@ -1321,7 +1266,6 @@ class GuiMain(QMainWindow):
self.projSearch.processReturn() self.projSearch.processReturn()
else: else:
self.openSelectedItem() self.openSelectedItem()
return
@pyqtSlot() @pyqtSlot()
def _keyPressEscape(self) -> None: def _keyPressEscape(self) -> None:
@@ -1330,7 +1274,6 @@ class GuiMain(QMainWindow):
self.docEditor.closeSearch() self.docEditor.closeSearch()
elif SHARED.focusMode: elif SHARED.focusMode:
SHARED.setFocusMode(False) SHARED.setFocusMode(False)
return
@pyqtSlot(int) @pyqtSlot(int)
def _mainStackChanged(self, index: int) -> None: def _mainStackChanged(self, index: int) -> None:
@@ -1338,7 +1281,6 @@ class GuiMain(QMainWindow):
if self.mainStack.widget(index) == self.outlineView: if self.mainStack.widget(index) == self.outlineView:
if SHARED.hasProject: if SHARED.hasProject:
self.outlineView.refreshTree() self.outlineView.refreshTree()
return
@pyqtSlot(int) @pyqtSlot(int)
def _projStackChanged(self, index: int) -> None: def _projStackChanged(self, index: int) -> None:
@@ -1350,7 +1292,6 @@ class GuiMain(QMainWindow):
elif widget == self.novelView: elif widget == self.novelView:
sHandle, _ = self.novelView.getSelectedHandle() sHandle, _ = self.novelView.getSelectedHandle()
self.itemDetails.updateViewBox(sHandle) self.itemDetails.updateViewBox(sHandle)
return
## ##
# Internal Functions # Internal Functions
@@ -1363,9 +1304,7 @@ class GuiMain(QMainWindow):
width = minmax(size[0], 900, availSize.width()) width = minmax(size[0], 900, availSize.width())
height = minmax(size[1], 500, availSize.height()) height = minmax(size[1], 500, availSize.height())
self.resize(width, height) self.resize(width, height)
return
def _updateWindowTitle(self, projName: str | None = None) -> None: def _updateWindowTitle(self, projName: str | None = None) -> None:
"""Set the window title and add the project's name.""" """Set the window title and add the project's name."""
self.setWindowTitle(" - ".join(filter(None, [projName, CONFIG.appName]))) self.setWindowTitle(" - ".join(filter(None, [projName, CONFIG.appName])))
return
+8 -39
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -57,6 +57,12 @@ RX_HTML = re.compile(r"<.*?>")
class SharedData(QObject): class SharedData(QObject):
"""Shared Data Singleton.
This is the class instantiated as the SHARED singleton. It holds
various globally needed data and pointers to important objects like
the main GUI, the current project, and the GUI theme.
"""
__slots__ = ( __slots__ = (
"_gui", "_idleRefTime", "_idleTime", "_lastAlert", "_lockedBy", "_gui", "_idleRefTime", "_idleTime", "_lastAlert", "_lockedBy",
@@ -96,8 +102,6 @@ class SharedData(QObject):
self._clock.setInterval(1000) self._clock.setInterval(1000)
self._clock.timeout.connect(lambda: self.mainClockTick.emit()) self._clock.timeout.connect(lambda: self.mainClockTick.emit())
return
## ##
# Properties # Properties
## ##
@@ -169,7 +173,6 @@ class SharedData(QObject):
if state is not self._focusMode: if state is not self._focusMode:
self._focusMode = state self._focusMode = state
self.focusModeChanged.emit(state) self.focusModeChanged.emit(state)
return
## ##
# Methods # Methods
@@ -181,7 +184,6 @@ class SharedData(QObject):
""" """
self._theme = theme self._theme = theme
self._theme.initThemes() self._theme.initThemes()
return
def initSharedData(self, gui: GuiMain) -> None: def initSharedData(self, gui: GuiMain) -> None:
"""Initialise the SharedData instance. This must be called as """Initialise the SharedData instance. This must be called as
@@ -194,7 +196,6 @@ class SharedData(QObject):
logger.debug("Ready: SharedData") logger.debug("Ready: SharedData")
if pool := QThreadPool.globalInstance(): if pool := QThreadPool.globalInstance():
logger.debug("Thread Pool Max Count: %d", pool.maxThreadCount()) logger.debug("Thread Pool Max Count: %d", pool.maxThreadCount())
return
def closeDocument(self, tHandle: str | None = None) -> None: def closeDocument(self, tHandle: str | None = None) -> None:
"""Close the document editor, optionally a specific document.""" """Close the document editor, optionally a specific document."""
@@ -202,7 +203,6 @@ class SharedData(QObject):
self.mainGui.closeDocument() self.mainGui.closeDocument()
if tHandle is None or tHandle == self.mainGui.docViewer.docHandle: if tHandle is None or tHandle == self.mainGui.docViewer.docHandle:
self.mainGui.closeViewerPanel() self.mainGui.closeViewerPanel()
return
def saveEditor(self, tHandle: str | None = None) -> None: def saveEditor(self, tHandle: str | None = None) -> None:
"""Save the editor content, optionally a specific document.""" """Save the editor content, optionally a specific document."""
@@ -213,7 +213,6 @@ class SharedData(QObject):
): ):
logger.debug("Saving editor document before action") logger.debug("Saving editor document before action")
docEditor.saveText() docEditor.saveText()
return
def openProject(self, path: str | Path, clearLock: bool = False) -> bool: def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
"""Open a project.""" """Open a project."""
@@ -246,7 +245,6 @@ class SharedData(QObject):
self.project.closeProject(self._idleTime) self.project.closeProject(self._idleTime)
self._resetProject() self._resetProject()
self._resetIdleTimer() self._resetIdleTimer()
return
def updateSpellCheckLanguage(self, reload: bool = False) -> None: def updateSpellCheckLanguage(self, reload: bool = False) -> None:
"""Update the active spell check language from settings.""" """Update the active spell check language from settings."""
@@ -256,7 +254,6 @@ class SharedData(QObject):
self.spelling.setLanguage(language) self.spelling.setLanguage(language)
_, provider = self.spelling.describeDict() _, provider = self.spelling.describeDict()
self.spellLanguageChanged.emit(language, provider) self.spellLanguageChanged.emit(language, provider)
return
def updateIdleTime(self, currTime: float, userIdle: bool) -> None: def updateIdleTime(self, currTime: float, userIdle: bool) -> None:
"""Update the idle time record. If the userIdle flag is True, """Update the idle time record. If the userIdle flag is True,
@@ -267,47 +264,40 @@ class SharedData(QObject):
if userIdle: if userIdle:
self._idleTime += currTime - self._idleRefTime self._idleTime += currTime - self._idleRefTime
self._idleRefTime = currTime self._idleRefTime = currTime
return
def initMainProgress(self, maximum: int, inclusive: bool = False) -> None: def initMainProgress(self, maximum: int, inclusive: bool = False) -> None:
"""Start a session for the main progress bar.""" """Start a session for the main progress bar."""
if gui := self._gui: if gui := self._gui:
gui.mainProgress.setMaximum(maximum - (1 if inclusive else 0)) gui.mainProgress.setMaximum(maximum - (1 if inclusive else 0))
gui.mainProgress.setValue(0) gui.mainProgress.setValue(0)
return
def incMainProgress(self) -> None: def incMainProgress(self) -> None:
"""Increment the value for the main progress bar.""" """Increment the value for the main progress bar."""
if gui := self._gui: if gui := self._gui:
gui.mainProgress.setValue(gui.mainProgress.value() + 1) gui.mainProgress.setValue(gui.mainProgress.value() + 1)
QApplication.processEvents() QApplication.processEvents()
return
def clearMainProgress(self, delay: float = 1.0) -> None: def clearMainProgress(self, delay: float = 1.0) -> None:
"""Clear the main progress bar.""" """Clear the main progress bar."""
if gui := self._gui: if gui := self._gui:
QTimer.singleShot(int(delay*1000), gui.mainProgress.reset) QTimer.singleShot(int(delay*1000), gui.mainProgress.reset)
return
def newStatusMessage(self, message: str) -> None: def newStatusMessage(self, message: str) -> None:
"""Request a new status message. This is a callable function for """Request a new status message. This is a callable function for
core classes that cannot emit signals on their own. core classes that cannot emit signals on their own.
""" """
self.projectStatusMessage.emit(message) self.projectStatusMessage.emit(message)
return
def setGlobalProjectState(self, state: bool) -> None: def setGlobalProjectState(self, state: bool) -> None:
"""Change the global project status. This is a callable function """Change the global project status. This is a callable function
for core classes that cannot emit signals on their own. for core classes that cannot emit signals on their own.
""" """
self.projectStatusChanged.emit(state) self.projectStatusChanged.emit(state)
return
def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None: def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None:
"""Queue a runnable in the application thread pool.""" """Queue a runnable in the application thread pool."""
if pool := QThreadPool.globalInstance(): if pool := QThreadPool.globalInstance():
pool.start(runnable, priority=priority) pool.start(runnable, priority=priority)
return
def getProjectPath( def getProjectPath(
self, parent: QWidget, self, parent: QWidget,
@@ -346,13 +336,11 @@ class SharedData(QObject):
def openWebsite(self, url: str) -> None: def openWebsite(self, url: str) -> None:
"""Open a URL in the system's default browser.""" """Open a URL in the system's default browser."""
QDesktopServices.openUrl(QUrl(url)) QDesktopServices.openUrl(QUrl(url))
return
@pyqtSlot(str, nwItemClass) @pyqtSlot(str, nwItemClass)
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None: def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
"""Process new note request.""" """Process new note request."""
self.project.createNewNote(tag, itemClass) self.project.createNewNote(tag, itemClass)
return
## ##
# Signal Proxies # Signal Proxies
@@ -364,37 +352,31 @@ class SharedData(QObject):
"""Emit the indexChangedTags signal.""" """Emit the indexChangedTags signal."""
if self._project and self._project.data.uuid == project.data.uuid: if self._project and self._project.data.uuid == project.data.uuid:
self.indexChangedTags.emit(updated, deleted) self.indexChangedTags.emit(updated, deleted)
return
def emitIndexCleared(self, project: NWProject) -> None: def emitIndexCleared(self, project: NWProject) -> None:
"""Emit the indexCleared signal.""" """Emit the indexCleared signal."""
if self._project and self._project.data.uuid == project.data.uuid: if self._project and self._project.data.uuid == project.data.uuid:
self.indexCleared.emit() self.indexCleared.emit()
return
def emitIndexAvailable(self, project: NWProject) -> None: def emitIndexAvailable(self, project: NWProject) -> None:
"""Emit the indexAvailable signal.""" """Emit the indexAvailable signal."""
if self._project and self._project.data.uuid == project.data.uuid: if self._project and self._project.data.uuid == project.data.uuid:
self.indexAvailable.emit() self.indexAvailable.emit()
return
def emitStatusLabelsChanged(self, project: NWProject, kind: T_StatusKind) -> None: def emitStatusLabelsChanged(self, project: NWProject, kind: T_StatusKind) -> None:
"""Emit the statusLabelsChanged signal.""" """Emit the statusLabelsChanged signal."""
if self._project and self._project.data.uuid == project.data.uuid: if self._project and self._project.data.uuid == project.data.uuid:
self.statusLabelsChanged.emit(kind) self.statusLabelsChanged.emit(kind)
return
def emitProjectItemChanged(self, project: NWProject, handle: str, change: nwChange) -> None: def emitProjectItemChanged(self, project: NWProject, handle: str, change: nwChange) -> None:
"""Emit the projectItemChanged signal.""" """Emit the projectItemChanged signal."""
if self._project and self._project.data.uuid == project.data.uuid: if self._project and self._project.data.uuid == project.data.uuid:
self.projectItemChanged.emit(handle, change) self.projectItemChanged.emit(handle, change)
return
def emitRootFolderChanged(self, project: NWProject, handle: str, change: nwChange) -> None: def emitRootFolderChanged(self, project: NWProject, handle: str, change: nwChange) -> None:
"""Emit the rootFolderChanged signal.""" """Emit the rootFolderChanged signal."""
if self._project and self._project.data.uuid == project.data.uuid: if self._project and self._project.data.uuid == project.data.uuid:
self.rootFolderChanged.emit(handle, change) self.rootFolderChanged.emit(handle, change)
return
## ##
# Alert Boxes # Alert Boxes
@@ -409,7 +391,6 @@ class SharedData(QObject):
if log: if log:
self._logMessage(self._lastAlert, logger.info) self._logMessage(self._lastAlert, logger.info)
alert.exec() alert.exec()
return
def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None: def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None:
"""Open a warning alert box.""" """Open a warning alert box."""
@@ -420,7 +401,6 @@ class SharedData(QObject):
if log: if log:
self._logMessage(self._lastAlert, logger.warning) self._logMessage(self._lastAlert, logger.warning)
alert.exec() alert.exec()
return
def error(self, text: str, info: str = "", details: str = "", log: bool = True, def error(self, text: str, info: str = "", details: str = "", log: bool = True,
exc: Exception | None = None) -> None: exc: Exception | None = None) -> None:
@@ -434,7 +414,6 @@ class SharedData(QObject):
if log: if log:
self._logMessage(self._lastAlert, logger.error) self._logMessage(self._lastAlert, logger.error)
alert.exec() alert.exec()
return
def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool: def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool:
"""Open a question box.""" """Open a question box."""
@@ -443,8 +422,7 @@ class SharedData(QObject):
alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True) alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True)
self._lastAlert = alert.logMessage self._lastAlert = alert.logMessage
alert.exec() alert.exec()
isYes = alert.result() == QMessageBox.StandardButton.Yes return alert.result() == QMessageBox.StandardButton.Yes
return isYes
## ##
# Internal Functions # Internal Functions
@@ -454,7 +432,6 @@ class SharedData(QObject):
"""Print message to log.""" """Print message to log."""
for text in message.split("<br>"): for text in message.split("<br>"):
log(RX_HTML.sub("", text), stacklevel=3) log(RX_HTML.sub("", text), stacklevel=3)
return
def _resetProject(self) -> None: def _resetProject(self) -> None:
"""Create a new project and spell checking instance.""" """Create a new project and spell checking instance."""
@@ -467,13 +444,11 @@ class SharedData(QObject):
self._spelling = NWSpellEnchant(self._project) self._spelling = NWSpellEnchant(self._project)
self.updateSpellCheckLanguage() self.updateSpellCheckLanguage()
self._focusMode = False self._focusMode = False
return
def _resetIdleTimer(self) -> None: def _resetIdleTimer(self) -> None:
"""Reset the timer data for the idle timer.""" """Reset the timer data for the idle timer."""
self._idleRefTime = time() self._idleRefTime = time()
self._idleTime = 0.0 self._idleTime = 0.0
return
def _closeToolDialogs(self) -> None: def _closeToolDialogs(self) -> None:
"""Close all open tool dialogs.""" """Close all open tool dialogs."""
@@ -481,7 +456,6 @@ class SharedData(QObject):
for widget in self.mainGui.children(): for widget in self.mainGui.children():
if isinstance(widget, NToolDialog): if isinstance(widget, NToolDialog):
widget.close() widget.close()
return
class _GuiAlert(QMessageBox): class _GuiAlert(QMessageBox):
@@ -496,11 +470,9 @@ class _GuiAlert(QMessageBox):
self._theme = theme self._theme = theme
self._message = "" self._message = ""
logger.debug("Ready: _GuiAlert") logger.debug("Ready: _GuiAlert")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: _GuiAlert") logger.debug("Delete: _GuiAlert")
return
@property @property
def logMessage(self) -> str: def logMessage(self) -> str:
@@ -512,14 +484,12 @@ class _GuiAlert(QMessageBox):
self.setText(text) self.setText(text)
self.setInformativeText(info) self.setInformativeText(info)
self.setDetailedText(details) self.setDetailedText(details)
return
def setException(self, exception: Exception) -> None: def setException(self, exception: Exception) -> None:
"""Add exception details.""" """Add exception details."""
info = self.informativeText() info = self.informativeText()
text = f"<b>{type(exception).__name__}</b>: {exception!s}" text = f"<b>{type(exception).__name__}</b>: {exception!s}"
self.setInformativeText(f"{info}<br>{text}" if info else text) self.setInformativeText(f"{info}<br>{text}" if info else text)
return
def setAlertType(self, level: int, isYesNo: bool) -> None: def setAlertType(self, level: int, isYesNo: bool) -> None:
"""Set the type of alert and whether the dialog should have """Set the type of alert and whether the dialog should have
@@ -542,4 +512,3 @@ class _GuiAlert(QMessageBox):
elif level == self.ASK: elif level == self.ASK:
self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz), "blue")) self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz), "blue"))
self.setWindowTitle(self.tr("Question")) self.setWindowTitle(self.tr("Question"))
return
+9 -5
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -38,6 +38,14 @@ SPLASH_IMG = Path(__file__).parent / "assets" / "images" / "splash.png"
class NSplashScreen(QSplashScreen): class NSplashScreen(QSplashScreen):
"""GUI: App Launch Splash Screen.
A small splash screen that is shown as novelWriter starts up. Its
primary purpose is to provide user feedback that the app is being
initiated when there are delays in the process while Qt waits for
responses from the OS, or has to load particularly large data sets
like when the system has a lot of fonts installed.
"""
__slots__ = ("_color", "_rect", "_text") __slots__ = ("_color", "_rect", "_text")
@@ -52,17 +60,14 @@ class NSplashScreen(QSplashScreen):
self._color = QColor(26, 52, 78) self._color = QColor(26, 52, 78)
self._rect = QRect(144, 110, 440, 30) self._rect = QRect(144, 110, 440, 30)
self._text = "" self._text = ""
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NSplashScreen") logger.debug("Delete: NSplashScreen")
return
def drawContents(self, painter: QPainter) -> None: def drawContents(self, painter: QPainter) -> None:
"""Draw the text message.""" """Draw the text message."""
painter.setPen(self._color) painter.setPen(self._color)
painter.drawText(self._rect, Qt.AlignmentFlag.AlignLeft, self._text) painter.drawText(self._rect, Qt.AlignmentFlag.AlignLeft, self._text)
return
def showStatus(self, message: str) -> None: def showStatus(self, message: str) -> None:
"""Update the status message.""" """Update the status message."""
@@ -71,4 +76,3 @@ class NSplashScreen(QSplashScreen):
if message: if message:
logger.info("[Splash] %s", message) logger.info("[Splash] %s", message)
sleep(0.025) sleep(0.025)
return
+1 -1
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from novelwriter.enum import nwComment from novelwriter.enum import nwComment
+9 -5
View File
@@ -22,7 +22,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import re import re
@@ -74,9 +74,11 @@ def preProcessText(text: str, keepHeaders: bool = True) -> list[str]:
def standardCounter(text: str) -> tuple[int, int, int]: def standardCounter(text: str) -> tuple[int, int, int]:
"""A counter that counts paragraphs, words and characters. """Return a standard count.
This is the standard counter that includes headings in the word and
character counts. A counter that counts paragraphs, words and characters. This is the
standard counter that includes headings in the word and character
counts.
""" """
cCount = 0 cCount = 0
wCount = 0 wCount = 0
@@ -124,7 +126,9 @@ def standardCounter(text: str) -> tuple[int, int, int]:
def bodyTextCounter(text: str) -> tuple[int, int, int]: def bodyTextCounter(text: str) -> tuple[int, int, int]:
"""A counter that counts body text words, characters, and characters """Return a body text count.
A counter that counts body text words, characters, and characters
without white spaces. without white spaces.
""" """
wCount = 0 wCount = 0
+3 -4
View File
@@ -21,7 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import re import re
@@ -32,6 +32,7 @@ from novelwriter.constants import nwRegEx, nwUnicode
class RegExPatterns: class RegExPatterns:
"""Compiled RegEx Patterns."""
AMBIGUOUS = (nwUnicode.U_APOS, nwUnicode.U_RSQUO) AMBIGUOUS = (nwUnicode.U_APOS, nwUnicode.U_RSQUO)
@@ -132,6 +133,7 @@ REGEX_PATTERNS = RegExPatterns()
class DialogParser: class DialogParser:
"""A callable parser for finding dialog regions in text."""
__slots__ = ( __slots__ = (
"_alternate", "_breakD", "_breakQ", "_dialog", "_enabled", "_mode", "_alternate", "_breakD", "_breakQ", "_dialog", "_enabled", "_mode",
@@ -147,7 +149,6 @@ class DialogParser:
self._breakD = None self._breakD = None
self._breakQ = None self._breakQ = None
self._mode = "" self._mode = ""
return
@property @property
def enabled(self) -> bool: def enabled(self) -> bool:
@@ -174,8 +175,6 @@ class DialogParser:
self._narrator = narrator self._narrator = narrator
self._mode = f" {narrator}" self._mode = f" {narrator}"
return
def __call__(self, text: str) -> list[tuple[int, int]]: def __call__(self, text: str) -> list[tuple[int, int]]:
"""Caller wrapper for dialogue processing.""" """Caller wrapper for dialogue processing."""
temp: list[int] = [] temp: list[int] = []
+7 -9
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -45,6 +45,12 @@ logger = logging.getLogger(__name__)
class GuiDictionaries(NNonBlockingDialog): class GuiDictionaries(NNonBlockingDialog):
"""GUI: Spell Check Dictionary Tool.
A helper tool for downloading and extracting dictionaries to a
location where Enchant can find them. This tool is only needed on
Windows.
"""
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -123,11 +129,8 @@ class GuiDictionaries(NNonBlockingDialog):
logger.debug("Ready: GuiDictionaries") logger.debug("Ready: GuiDictionaries")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiDictionaries") logger.debug("Delete: GuiDictionaries")
return
def initDialog(self) -> bool: def initDialog(self) -> bool:
"""Prepare and check that we can proceed.""" """Prepare and check that we can proceed."""
@@ -164,7 +167,6 @@ class GuiDictionaries(NNonBlockingDialog):
"""Capture the user closing the window.""" """Capture the user closing the window."""
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Private Slots # Private Slots
@@ -182,7 +184,6 @@ class GuiDictionaries(NNonBlockingDialog):
if soxFile: if soxFile:
path = Path(soxFile).absolute() path = Path(soxFile).absolute()
self.huInput.setText(str(path)) self.huInput.setText(str(path))
return
@pyqtSlot() @pyqtSlot()
def _doImportHunspell(self) -> None: def _doImportHunspell(self) -> None:
@@ -202,14 +203,12 @@ class GuiDictionaries(NNonBlockingDialog):
self._appendLog(formatException(exc), err=True) self._appendLog(formatException(exc), err=True)
else: else:
self._appendLog(procErr, err=True) self._appendLog(procErr, err=True)
return
@pyqtSlot() @pyqtSlot()
def _doOpenInstallLocation(self) -> None: def _doOpenInstallLocation(self) -> None:
"""Open the dictionary folder.""" """Open the dictionary folder."""
if not openExternalPath(Path(self.inPath.text())): if not openExternalPath(Path(self.inPath.text())):
SHARED.error("Path not found.") SHARED.error("Path not found.")
return
## ##
# Internal Functions # Internal Functions
@@ -247,4 +246,3 @@ class GuiDictionaries(NNonBlockingDialog):
cursor.movePosition(QTextCursor.MoveOperation.End) cursor.movePosition(QTextCursor.MoveOperation.End)
cursor.deleteChar() cursor.deleteChar()
self.infoBox.setTextCursor(cursor) self.infoBox.setTextCursor(cursor)
return
+2 -5
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
class GuiLipsum(NDialog): class GuiLipsum(NDialog):
"""GUI: Lorem Ipsum Text Tool."""
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -111,11 +112,8 @@ class GuiLipsum(NDialog):
logger.debug("Ready: GuiLipsum") logger.debug("Ready: GuiLipsum")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiLipsum") logger.debug("Delete: GuiLipsum")
return
@property @property
def lipsumText(self) -> str: def lipsumText(self) -> str:
@@ -145,4 +143,3 @@ class GuiLipsum(NDialog):
pCount = self.paraCount.value() pCount = self.paraCount.value()
self._lipsumText = "\n\n".join(lipsumText[0:pCount]) + "\n\n" self._lipsumText = "\n\n".join(lipsumText[0:pCount]) + "\n\n"
self.close() self.close()
return
+2 -14
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -54,7 +54,7 @@ logger = logging.getLogger(__name__)
class GuiManuscriptBuild(NDialog): class GuiManuscriptBuild(NDialog):
"""GUI Tools: Manuscript Build Dialog """GUI Tools: Manuscript Build Dialog.
This is the tool for running the build itself. It can be accessed This is the tool for running the build itself. It can be accessed
independently of the Manuscript Build Tool. independently of the Manuscript Build Tool.
@@ -244,11 +244,8 @@ class GuiManuscriptBuild(NDialog):
logger.debug("Ready: GuiManuscriptBuild") logger.debug("Ready: GuiManuscriptBuild")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiManuscriptBuild") logger.debug("Delete: GuiManuscriptBuild")
return
## ##
# Events # Events
@@ -261,7 +258,6 @@ class GuiManuscriptBuild(NDialog):
self._saveSettings() self._saveSettings()
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Private Slots # Private Slots
@@ -278,7 +274,6 @@ class GuiManuscriptBuild(NDialog):
self._openOutputFolder() self._openOutputFolder()
elif role == QtRoleReject: elif role == QtRoleReject:
self.close() self.close()
return
@pyqtSlot() @pyqtSlot()
def _doSelectPath(self) -> None: def _doSelectPath(self) -> None:
@@ -290,7 +285,6 @@ class GuiManuscriptBuild(NDialog):
) )
if savePath: if savePath:
self.buildPath.setText(savePath) self.buildPath.setText(savePath)
return
@pyqtSlot() @pyqtSlot()
def _doResetBuildName(self) -> None: def _doResetBuildName(self) -> None:
@@ -298,13 +292,11 @@ class GuiManuscriptBuild(NDialog):
bName = f"{SHARED.project.data.name} - {self._build.name}" bName = f"{SHARED.project.data.name} - {self._build.name}"
self.buildName.setText(bName) self.buildName.setText(bName)
self._build.setLastBuildName(bName) self._build.setLastBuildName(bName)
return
@pyqtSlot() @pyqtSlot()
def _resetProgress(self) -> None: def _resetProgress(self) -> None:
"""Set the progress bar back to 0.""" """Set the progress bar back to 0."""
self.buildProgress.setValue(0) self.buildProgress.setValue(0)
return
## ##
# Internal Functions # Internal Functions
@@ -371,7 +363,6 @@ class GuiManuscriptBuild(NDialog):
pOptions.setValue("GuiManuscriptBuild", "fmtWidth", mainSplit[0]) pOptions.setValue("GuiManuscriptBuild", "fmtWidth", mainSplit[0])
pOptions.setValue("GuiManuscriptBuild", "sumWidth", mainSplit[1]) pOptions.setValue("GuiManuscriptBuild", "sumWidth", mainSplit[1])
pOptions.saveSettings() pOptions.saveSettings()
return
def _populateContentList(self) -> None: def _populateContentList(self) -> None:
"""Build the content list.""" """Build the content list."""
@@ -396,9 +387,6 @@ class GuiManuscriptBuild(NDialog):
item.setIcon(nwItem.getMainIcon()) item.setIcon(nwItem.getMainIcon())
self.listContent.addItem(item) self.listContent.addItem(item)
return
def _openOutputFolder(self) -> None: def _openOutputFolder(self) -> None:
"""Open the build folder in the system's file explorer.""" """Open the build folder in the system's file explorer."""
openExternalPath(Path(self.buildPath.text())) openExternalPath(Path(self.buildPath.text()))
return
+2 -54
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -66,7 +66,7 @@ logger = logging.getLogger(__name__)
class GuiManuscript(NToolDialog): class GuiManuscript(NToolDialog):
"""GUI Tools: Manuscript Tool """GUI Tools: Manuscript Tool.
The dialog displays all the users build definitions, a preview panel The dialog displays all the users build definitions, a preview panel
for the manuscript, and can trigger the actual build dialog to build for the manuscript, and can trigger the actual build dialog to build
@@ -251,11 +251,8 @@ class GuiManuscript(NToolDialog):
logger.debug("Ready: GuiManuscript") logger.debug("Ready: GuiManuscript")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiManuscript") logger.debug("Delete: GuiManuscript")
return
def loadContent(self) -> None: def loadContent(self) -> None:
"""Load dialog content from project data.""" """Load dialog content from project data."""
@@ -272,8 +269,6 @@ class GuiManuscript(NToolDialog):
self.buildList.setCurrentItem(self._buildMap[selected]) self.buildList.setCurrentItem(self._buildMap[selected])
QTimer.singleShot(200, self._generatePreview) QTimer.singleShot(200, self._generatePreview)
return
## ##
# Events # Events
## ##
@@ -290,7 +285,6 @@ class GuiManuscript(NToolDialog):
obj.close() obj.close()
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Private Slots # Private Slots
@@ -302,14 +296,12 @@ class GuiManuscript(NToolDialog):
build = BuildSettings() build = BuildSettings()
build.setName(self.tr("My Manuscript")) build.setName(self.tr("My Manuscript"))
self._openSettingsDialog(build) self._openSettingsDialog(build)
return
@pyqtSlot() @pyqtSlot()
def _editSelectedBuild(self) -> None: def _editSelectedBuild(self) -> None:
"""Edit the currently selected build settings entry.""" """Edit the currently selected build settings entry."""
if build := self._getSelectedBuild(): if build := self._getSelectedBuild():
self._openSettingsDialog(build) self._openSettingsDialog(build)
return
@pyqtSlot() @pyqtSlot()
def _copySelectedBuild(self) -> None: def _copySelectedBuild(self) -> None:
@@ -320,14 +312,12 @@ class GuiManuscript(NToolDialog):
self._updateBuildsList() self._updateBuildsList()
if item := self._buildMap.get(new.buildID): if item := self._buildMap.get(new.buildID):
item.setSelected(True) item.setSelected(True)
return
@pyqtSlot("QListWidgetItem*", "QListWidgetItem*") @pyqtSlot("QListWidgetItem*", "QListWidgetItem*")
def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None: def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None:
"""Process change of build selection to update the details.""" """Process change of build selection to update the details."""
if current and (build := self._builds.getBuild(current.data(self.D_KEY))): if current and (build := self._builds.getBuild(current.data(self.D_KEY))):
self.buildDetails.updateInfo(build) self.buildDetails.updateInfo(build)
return
@pyqtSlot() @pyqtSlot()
def _deleteSelectedBuild(self) -> None: def _deleteSelectedBuild(self) -> None:
@@ -338,7 +328,6 @@ class GuiManuscript(NToolDialog):
dialog.close() dialog.close()
self._builds.removeBuild(build.buildID) self._builds.removeBuild(build.buildID)
self._updateBuildsList() self._updateBuildsList()
return
@pyqtSlot(BuildSettings) @pyqtSlot(BuildSettings)
def _processNewSettings(self, build: BuildSettings) -> None: def _processNewSettings(self, build: BuildSettings) -> None:
@@ -347,7 +336,6 @@ class GuiManuscript(NToolDialog):
self._updateBuildItem(build) self._updateBuildItem(build)
if (current := self.buildList.currentItem()) and current.data(self.D_KEY) == build.buildID: if (current := self.buildList.currentItem()) and current.data(self.D_KEY) == build.buildID:
self._updateBuildDetails(current, current) self._updateBuildDetails(current, current)
return
@pyqtSlot() @pyqtSlot()
def _generatePreview(self) -> None: def _generatePreview(self) -> None:
@@ -399,15 +387,12 @@ class GuiManuscript(NToolDialog):
if build.changed: if build.changed:
self._builds.setBuild(build) self._builds.setBuild(build)
return
@pyqtSlot() @pyqtSlot()
def _printDocument(self) -> None: def _printDocument(self) -> None:
"""Open the print preview dialog.""" """Open the print preview dialog."""
preview = QPrintPreviewDialog(self) preview = QPrintPreviewDialog(self)
preview.paintRequested.connect(self.docPreview.printPreview) preview.paintRequested.connect(self.docPreview.printPreview)
preview.exec() preview.exec()
return
## ##
# Internal Functions # Internal Functions
@@ -456,8 +441,6 @@ class GuiManuscript(NToolDialog):
pOptions.setValue("GuiManuscript", "showNewPage", showNewPage) pOptions.setValue("GuiManuscript", "showNewPage", showNewPage)
pOptions.saveSettings() pOptions.saveSettings()
return
def _openSettingsDialog(self, build: BuildSettings) -> None: def _openSettingsDialog(self, build: BuildSettings) -> None:
"""Open the build settings dialog.""" """Open the build settings dialog."""
if dialog := self._findSettingsDialog(build.buildID): if dialog := self._findSettingsDialog(build.buildID):
@@ -482,7 +465,6 @@ class GuiManuscript(NToolDialog):
bItem.setData(self.D_KEY, key) bItem.setData(self.D_KEY, key)
self.buildList.addItem(bItem) self.buildList.addItem(bItem)
self._buildMap[key] = bItem self._buildMap[key] = bItem
return
def _updateBuildItem(self, build: BuildSettings) -> None: def _updateBuildItem(self, build: BuildSettings) -> None:
"""Update the entry of a specific build item.""" """Update the entry of a specific build item."""
@@ -490,7 +472,6 @@ class GuiManuscript(NToolDialog):
item.setText(build.name) item.setText(build.name)
else: # Probably a new item else: # Probably a new item
self._updateBuildsList() self._updateBuildsList()
return
def _findSettingsDialog(self, buildID: str) -> GuiBuildSettings | None: def _findSettingsDialog(self, buildID: str) -> GuiBuildSettings | None:
"""Return an open build settings dialog for a given build, if """Return an open build settings dialog for a given build, if
@@ -521,8 +502,6 @@ class _DetailsWidget(QWidget):
self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
return
## ##
# Getters # Getters
## ##
@@ -547,7 +526,6 @@ class _DetailsWidget(QWidget):
def setColumnWidth(self, value: int) -> None: def setColumnWidth(self, value: int) -> None:
"""Set the width of the first column.""" """Set the width of the first column."""
self.listView.setColumnWidth(0, value) self.listView.setColumnWidth(0, value)
return
def setExpandedState(self, state: list[bool]) -> None: def setExpandedState(self, state: list[bool]) -> None:
"""Set the expanded state of each top level item.""" """Set the expanded state of each top level item."""
@@ -556,7 +534,6 @@ class _DetailsWidget(QWidget):
item = self.listView.topLevelItem(i) item = self.listView.topLevelItem(i)
if isinstance(item, QTreeWidgetItem): if isinstance(item, QTreeWidgetItem):
item.setExpanded((state[i] if i < count else True) and item.childCount() > 0) item.setExpanded((state[i] if i < count else True) and item.childCount() > 0)
return
## ##
# Methods # Methods
@@ -637,8 +614,6 @@ class _DetailsWidget(QWidget):
# Restore expanded state # Restore expanded state
self.setExpandedState(expanded) self.setExpandedState(expanded)
return
class _OutlineWidget(QWidget): class _OutlineWidget(QWidget):
@@ -663,8 +638,6 @@ class _OutlineWidget(QWidget):
self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
return
def updateOutline(self, data: dict[str, str]) -> None: def updateOutline(self, data: dict[str, str]) -> None:
"""Update the outline.""" """Update the outline."""
if isinstance(data, dict) and data != self._outline: if isinstance(data, dict) and data != self._outline:
@@ -705,8 +678,6 @@ class _OutlineWidget(QWidget):
self.listView.setIndentation(SHARED.theme.baseIconHeight if indent else 4) self.listView.setIndentation(SHARED.theme.baseIconHeight if indent else 4)
self._outline = data self._outline = data
return
## ##
# Private Slots # Private Slots
## ##
@@ -714,7 +685,6 @@ class _OutlineWidget(QWidget):
def _onItemClick(self, item: QTreeWidgetItem) -> None: def _onItemClick(self, item: QTreeWidgetItem) -> None:
"""Process tree item click.""" """Process tree item click."""
self.outlineEntryClicked.emit(str(item.data(0, self.D_LINE))) self.outlineEntryClicked.emit(str(item.data(0, self.D_LINE)))
return
class _PreviewWidget(QTextBrowser): class _PreviewWidget(QTextBrowser):
@@ -787,8 +757,6 @@ class _PreviewWidget(QTextBrowser):
self.ageTimer.timeout.connect(self._updateBuildAge) self.ageTimer.timeout.connect(self._updateBuildAge)
self.ageTimer.start() self.ageTimer.start()
return
## ##
# Setters # Setters
## ##
@@ -797,7 +765,6 @@ class _PreviewWidget(QTextBrowser):
"""Set the build name for the document label.""" """Set the build name for the document label."""
self._buildName = name self._buildName = name
self._updateBuildAge() self._updateBuildAge()
return
def setTextFont(self, font: QFont) -> None: def setTextFont(self, font: QFont) -> None:
"""Set the text font properties and then reset for sub-widgets. """Set the text font properties and then reset for sub-widgets.
@@ -807,7 +774,6 @@ class _PreviewWidget(QTextBrowser):
self.setFont(font) self.setFont(font)
self.buildProgress.setFont(SHARED.theme.guiFont) self.buildProgress.setFont(SHARED.theme.guiFont)
self.ageLabel.setFont(SHARED.theme.guiFontSmall) self.ageLabel.setFont(SHARED.theme.guiFontSmall)
return
## ##
# Methods # Methods
@@ -823,13 +789,11 @@ class _PreviewWidget(QTextBrowser):
self._scrollPos = vBar.value() self._scrollPos = vBar.value()
self.setPlaceholderText("") self.setPlaceholderText("")
self.clear() self.clear()
return
def buildStep(self, value: int) -> None: def buildStep(self, value: int) -> None:
"""Update the progress bar value.""" """Update the progress bar value."""
self.buildProgress.setValue(value) self.buildProgress.setValue(value)
QApplication.processEvents() QApplication.processEvents()
return
def setContent(self, document: QTextDocument) -> None: def setContent(self, document: QTextDocument) -> None:
"""Set the content of the preview widget.""" """Set the content of the preview widget."""
@@ -850,8 +814,6 @@ class _PreviewWidget(QTextBrowser):
QApplication.processEvents() QApplication.processEvents()
QTimer.singleShot(300, self._postUpdate) QTimer.singleShot(300, self._postUpdate)
return
## ##
# Events # Events
## ##
@@ -860,7 +822,6 @@ class _PreviewWidget(QTextBrowser):
"""Capture resize and update the document margins.""" """Capture resize and update the document margins."""
super().resizeEvent(event) super().resizeEvent(event)
self._updateDocMargins() self._updateDocMargins()
return
## ##
# Public Slots # Public Slots
@@ -874,14 +835,12 @@ class _PreviewWidget(QTextBrowser):
printer.setPageOrientation(QPageLayout.Orientation.Portrait) printer.setPageOrientation(QPageLayout.Orientation.Portrait)
document.print(printer) document.print(printer)
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
return
@pyqtSlot(str) @pyqtSlot(str)
def navigateTo(self, anchor: str) -> None: def navigateTo(self, anchor: str) -> None:
"""Go to a specific #link in the document.""" """Go to a specific #link in the document."""
logger.debug("Moving to anchor '#%s'", anchor) logger.debug("Moving to anchor '#%s'", anchor)
self.setSource(QUrl(f"#{anchor}")) self.setSource(QUrl(f"#{anchor}"))
return
## ##
# Private Slots # Private Slots
@@ -896,7 +855,6 @@ class _PreviewWidget(QTextBrowser):
self.navigateTo(link.lstrip("#")) self.navigateTo(link.lstrip("#"))
elif link.startswith("http"): elif link.startswith("http"):
QDesktopServices.openUrl(QUrl(url)) QDesktopServices.openUrl(QUrl(url))
return
@pyqtSlot() @pyqtSlot()
def _updateBuildAge(self) -> None: def _updateBuildAge(self) -> None:
@@ -909,7 +867,6 @@ class _PreviewWidget(QTextBrowser):
)) ))
else: else:
self.ageLabel.setText("<b>{0}</b>".format(self.tr("No Preview"))) self.ageLabel.setText("<b>{0}</b>".format(self.tr("No Preview")))
return
@pyqtSlot() @pyqtSlot()
def _postUpdate(self) -> None: def _postUpdate(self) -> None:
@@ -917,7 +874,6 @@ class _PreviewWidget(QTextBrowser):
self.buildProgress.setVisible(False) self.buildProgress.setVisible(False)
if vBar := self.verticalScrollBar(): if vBar := self.verticalScrollBar():
vBar.setValue(self._scrollPos) vBar.setValue(self._scrollPos)
return
## ##
# Internal Functions # Internal Functions
@@ -936,7 +892,6 @@ class _PreviewWidget(QTextBrowser):
self.ageLabel.setGeometry(tB, tB, vW, tH) self.ageLabel.setGeometry(tB, tB, vW, tH)
self.setViewportMargins(0, tH, 0, 0) self.setViewportMargins(0, tH, 0, 0)
self.buildProgress.move((vW-pS)//2, (vH-pS)//2) self.buildProgress.move((vW-pS)//2, (vH-pS)//2)
return
class _StatsWidget(QWidget): class _StatsWidget(QWidget):
@@ -969,8 +924,6 @@ class _StatsWidget(QWidget):
self._toggleView(False) self._toggleView(False)
return
def updateStats(self, data: dict[str, int]) -> None: def updateStats(self, data: dict[str, int]) -> None:
"""Update the stats values from a Tokenizer stats dict.""" """Update the stats values from a Tokenizer stats dict."""
# Minimal # Minimal
@@ -992,8 +945,6 @@ class _StatsWidget(QWidget):
self.maxHeadWordChars.setText(f"{data.get(nwStats.WCHARS_TITLE, 0):n}") self.maxHeadWordChars.setText(f"{data.get(nwStats.WCHARS_TITLE, 0):n}")
self.maxTextWordChars.setText(f"{data.get(nwStats.WCHARS_TEXT, 0):n}") self.maxTextWordChars.setText(f"{data.get(nwStats.WCHARS_TEXT, 0):n}")
return
## ##
# Private Slots # Private Slots
## ##
@@ -1013,7 +964,6 @@ class _StatsWidget(QWidget):
self.minWidget.adjustSize() self.minWidget.adjustSize()
self.mainStack.adjustSize() self.mainStack.adjustSize()
self.adjustSize() self.adjustSize()
return
## ##
# Internal Functions # Internal Functions
@@ -1105,5 +1055,3 @@ class _StatsWidget(QWidget):
self.minWidget.setLayout(self.minLayout) self.minWidget.setLayout(self.minLayout)
self.maxWidget.setLayout(self.maxLayout) self.maxWidget.setLayout(self.maxLayout)
return
+8 -54
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -62,7 +62,7 @@ logger = logging.getLogger(__name__)
class GuiBuildSettings(NToolDialog): class GuiBuildSettings(NToolDialog):
"""GUI Tools: Manuscript Build Settings Dialog """GUI Tools: Manuscript Build Settings Dialog.
The main tool for configuring manuscript builds. It's a GUI tool for The main tool for configuring manuscript builds. It's a GUI tool for
editing JSON build definitions, wrapped as a BuildSettings object. editing JSON build definitions, wrapped as a BuildSettings object.
@@ -153,11 +153,8 @@ class GuiBuildSettings(NToolDialog):
logger.debug("Ready: GuiBuildSettings") logger.debug("Ready: GuiBuildSettings")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiBuildSettings") logger.debug("Delete: GuiBuildSettings")
return
def loadContent(self) -> None: def loadContent(self) -> None:
"""Populate the child widgets.""" """Populate the child widgets."""
@@ -165,7 +162,6 @@ class GuiBuildSettings(NToolDialog):
self.optTabSelect.loadContent() self.optTabSelect.loadContent()
self.optTabHeadings.loadContent() self.optTabHeadings.loadContent()
self.optTabFormatting.loadContent() self.optTabFormatting.loadContent()
return
## ##
# Properties # Properties
@@ -190,7 +186,6 @@ class GuiBuildSettings(NToolDialog):
self._saveSettings() self._saveSettings()
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Private Slots # Private Slots
@@ -206,7 +201,6 @@ class GuiBuildSettings(NToolDialog):
elif pageId >= self.OPT_FORMATTING: elif pageId >= self.OPT_FORMATTING:
self.toolStack.setCurrentWidget(self.optTabFormatting) self.toolStack.setCurrentWidget(self.optTabFormatting)
self.optTabFormatting.scrollToSection(pageId) self.optTabFormatting.scrollToSection(pageId)
return
@pyqtSlot("QAbstractButton*") @pyqtSlot("QAbstractButton*")
def _dialogButtonClicked(self, button: QAbstractButton) -> None: def _dialogButtonClicked(self, button: QAbstractButton) -> None:
@@ -222,7 +216,6 @@ class GuiBuildSettings(NToolDialog):
elif role == QtRoleReject: elif role == QtRoleReject:
self._build.resetChangedState() self._build.resetChangedState()
self.close() self.close()
return
## ##
# Internal Functions # Internal Functions
@@ -238,7 +231,6 @@ class GuiBuildSettings(NToolDialog):
).format(self._build.name)): ).format(self._build.name)):
self._emitBuildData() self._emitBuildData()
self._build.resetChangedState() self._build.resetChangedState()
return
def _saveSettings(self) -> None: def _saveSettings(self) -> None:
"""Save the various user settings.""" """Save the various user settings."""
@@ -250,20 +242,17 @@ class GuiBuildSettings(NToolDialog):
pOptions.setValue("GuiBuildSettings", "treeWidth", treeWidth) pOptions.setValue("GuiBuildSettings", "treeWidth", treeWidth)
pOptions.setValue("GuiBuildSettings", "filterWidth", filterWidth) pOptions.setValue("GuiBuildSettings", "filterWidth", filterWidth)
pOptions.saveSettings() pOptions.saveSettings()
return
def _applyChanges(self) -> None: def _applyChanges(self) -> None:
"""Apply all settings changes to the build object.""" """Apply all settings changes to the build object."""
self._build.setName(self.editBuildName.text()) self._build.setName(self.editBuildName.text())
self.optTabHeadings.saveContent() self.optTabHeadings.saveContent()
self.optTabFormatting.saveContent() self.optTabFormatting.saveContent()
return
def _emitBuildData(self) -> None: def _emitBuildData(self) -> None:
"""Assemble the build data and emit the signal.""" """Assemble the build data and emit the signal."""
self.newSettingsReady.emit(self._build) self.newSettingsReady.emit(self._build)
self._build.resetChangedState() self._build.resetChangedState()
return
class _FilterTab(NFixedPage): class _FilterTab(NFixedPage):
@@ -382,13 +371,10 @@ class _FilterTab(NFixedPage):
self.setCentralWidget(self.mainSplit) self.setCentralWidget(self.mainSplit)
return
def loadContent(self) -> None: def loadContent(self) -> None:
"""Populate the widgets.""" """Populate the widgets."""
self._populateTree() self._populateTree()
self._populateFilters() self._populateFilters()
return
def mainSplitSizes(self) -> tuple[int, int]: def mainSplitSizes(self) -> tuple[int, int]:
"""Extract the sizes of the main splitter.""" """Extract the sizes of the main splitter."""
@@ -409,7 +395,6 @@ class _FilterTab(NFixedPage):
elif key.startswith("root:"): elif key.startswith("root:"):
self._build.setAllowRoot(key[5:], state) self._build.setAllowRoot(key[5:], state)
self._populateTree() self._populateTree()
return
## ##
# Internal Functions # Internal Functions
@@ -453,8 +438,6 @@ class _FilterTab(NFixedPage):
self._setTreeItemMode() self._setTreeItemMode()
return
def _populateFilters(self) -> None: def _populateFilters(self) -> None:
"""Populate the filter options switches.""" """Populate the filter options switches."""
self.filterOpt.clear() self.filterOpt.clear()
@@ -489,8 +472,6 @@ class _FilterTab(NFixedPage):
default=self._build.isRootAllowed(tHandle) default=self._build.isRootAllowed(tHandle)
) )
return
def _setSelectedMode(self, mode: int) -> None: def _setSelectedMode(self, mode: int) -> None:
"""Set the mode for the selected items.""" """Set the mode for the selected items."""
items = self.optTree.selectedItems() items = self.optTree.selectedItems()
@@ -511,8 +492,6 @@ class _FilterTab(NFixedPage):
self._setTreeItemMode() self._setTreeItemMode()
return
def _setTreeItemMode(self) -> None: def _setTreeItemMode(self) -> None:
"""Update the filtered mode icon on all items.""" """Update the filtered mode icon on all items."""
filtered = self._build.buildItemFilter(SHARED.project) filtered = self._build.buildItemFilter(SHARED.project)
@@ -529,11 +508,10 @@ class _FilterTab(NFixedPage):
item.setToolTip(self.C_STATUS, self._trIncluded) item.setToolTip(self.C_STATUS, self._trIncluded)
else: else:
item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE]) item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE])
return
def _scanChildren(self, item: QTreeWidgetItem | None, items: list) -> list[QTreeWidgetItem]: def _scanChildren(self, item: QTreeWidgetItem | None, items: list) -> list[QTreeWidgetItem]:
"""This is a recursive function returning all items in a tree """Recursively return all items in a tree starting at a given
starting at a given QTreeWidgetItem. QTreeWidgetItem.
""" """
if isinstance(item, QTreeWidgetItem): if isinstance(item, QTreeWidgetItem):
items.append(item) items.append(item)
@@ -791,8 +769,6 @@ class _HeadingsTab(NScrollablePage):
self.setCentralLayout(self.outerBox) self.setCentralLayout(self.outerBox)
return
def loadContent(self) -> None: def loadContent(self) -> None:
"""Populate the widgets.""" """Populate the widgets."""
def fmtBreak(text: str) -> str: def fmtBreak(text: str) -> str:
@@ -821,7 +797,6 @@ class _HeadingsTab(NScrollablePage):
self.breakPart.setChecked(self._build.getBool("headings.breakPart")) self.breakPart.setChecked(self._build.getBool("headings.breakPart"))
self.breakChapter.setChecked(self._build.getBool("headings.breakChapter")) self.breakChapter.setChecked(self._build.getBool("headings.breakChapter"))
self.breakScene.setChecked(self._build.getBool("headings.breakScene")) self.breakScene.setChecked(self._build.getBool("headings.breakScene"))
return
def saveContent(self) -> None: def saveContent(self) -> None:
"""Save choices back into build object.""" """Save choices back into build object."""
@@ -841,7 +816,6 @@ class _HeadingsTab(NScrollablePage):
self._build.setValue("headings.breakPart", self.breakPart.isChecked()) self._build.setValue("headings.breakPart", self.breakPart.isChecked())
self._build.setValue("headings.breakChapter", self.breakChapter.isChecked()) self._build.setValue("headings.breakChapter", self.breakChapter.isChecked())
self._build.setValue("headings.breakScene", self.breakScene.isChecked()) self._build.setValue("headings.breakScene", self.breakScene.isChecked())
return
## ##
# Internal Functions # Internal Functions
@@ -853,7 +827,6 @@ class _HeadingsTab(NScrollablePage):
cursor = self.editTextBox.textCursor() cursor = self.editTextBox.textCursor()
cursor.insertText(text) cursor.insertText(text)
self.editTextBox.setFocus() self.editTextBox.setFocus()
return
def _editHeading(self, heading: int) -> None: def _editHeading(self, heading: int) -> None:
"""Populate the form with a specific heading format.""" """Populate the form with a specific heading format."""
@@ -886,8 +859,6 @@ class _HeadingsTab(NScrollablePage):
self.editTextBox.setPlainText(text.replace(nwUnicode.U_LBREAK, "\n")) self.editTextBox.setPlainText(text.replace(nwUnicode.U_LBREAK, "\n"))
self.lblEditForm.setText(self.tr("Editing: {0}").format(label)) self.lblEditForm.setText(self.tr("Editing: {0}").format(label))
return
## ##
# Private Slots # Private Slots
## ##
@@ -934,7 +905,6 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter):
self._fmtSymbol.setForeground(syntax.head) self._fmtSymbol.setForeground(syntax.head)
self._fmtFormat = QTextCharFormat() self._fmtFormat = QTextCharFormat()
self._fmtFormat.setForeground(syntax.emph) self._fmtFormat.setForeground(syntax.emph)
return
def highlightBlock(self, text: str) -> None: def highlightBlock(self, text: str) -> None:
"""Add syntax highlighting to the text block.""" """Add syntax highlighting to the text block."""
@@ -947,7 +917,6 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter):
ddots = heading.find(":") ddots = heading.find(":")
if ddots > 0: if ddots > 0:
self.setFormat(pos + ddots, 1, self._fmtSymbol) self.setFormat(pos + ddots, 1, self._fmtSymbol)
return
class _FormattingTab(NScrollableForm): class _FormattingTab(NScrollableForm):
@@ -961,8 +930,6 @@ class _FormattingTab(NScrollableForm):
self.setHelpTextStyle(SHARED.theme.helpText) self.setHelpTextStyle(SHARED.theme.helpText)
self.buildForm() self.buildForm()
return
def buildForm(self) -> None: def buildForm(self) -> None:
"""Build the formatting form.""" """Build the formatting form."""
section = 10 section = 10
@@ -1290,8 +1257,6 @@ class _FormattingTab(NScrollableForm):
# Finalise # Finalise
self.finalise() self.finalise()
return
def loadContent(self) -> None: def loadContent(self) -> None:
"""Populate the widgets.""" """Populate the widgets."""
# Text Content # Text Content
@@ -1391,8 +1356,6 @@ class _FormattingTab(NScrollableForm):
self.htmlAddStyles.setChecked(self._build.getBool("html.addStyles")) self.htmlAddStyles.setChecked(self._build.getBool("html.addStyles"))
self.htmlPreserveTabs.setChecked(self._build.getBool("html.preserveTabs")) self.htmlPreserveTabs.setChecked(self._build.getBool("html.preserveTabs"))
return
def saveContent(self) -> None: def saveContent(self) -> None:
"""Save choices back into build object.""" """Save choices back into build object."""
# Text Content # Text Content
@@ -1458,8 +1421,6 @@ class _FormattingTab(NScrollableForm):
self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked()) self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked())
self._build.setValue("html.preserveTabs", self.htmlPreserveTabs.isChecked()) self._build.setValue("html.preserveTabs", self.htmlPreserveTabs.isChecked())
return
## ##
# Private Slots # Private Slots
## ##
@@ -1472,11 +1433,10 @@ class _FormattingTab(NScrollableForm):
self._textFont = fontMatcher(font) self._textFont = fontMatcher(font)
self.textFont.setText(describeFont(self._textFont)) self.textFont.setText(describeFont(self._textFont))
self.textFont.setCursorPosition(0) self.textFont.setCursorPosition(0)
return
@pyqtSlot(int) @pyqtSlot(int)
def _changeUnit(self, index: int) -> None: def _changeUnit(self, index: int) -> None:
"""The current unit change, so recalculate sizes.""" """Process current unit change to recalculate sizes."""
newUnit = self.pageUnit.itemData(index) newUnit = self.pageUnit.itemData(index)
newScale = nwLabels.UNIT_SCALE.get(newUnit, 1.0) newScale = nwLabels.UNIT_SCALE.get(newUnit, 1.0)
reScale = self._unitScale/newScale reScale = self._unitScale/newScale
@@ -1531,11 +1491,9 @@ class _FormattingTab(NScrollableForm):
self._unitScale = newScale self._unitScale = newScale
self._changePageSize(self.pageSize.currentIndex()) self._changePageSize(self.pageSize.currentIndex())
return
@pyqtSlot(int) @pyqtSlot(int)
def _changePageSize(self, index: int) -> None: def _changePageSize(self, index: int) -> None:
"""The page size has changed.""" """Process page size change."""
w, h = nwLabels.PAPER_SIZE[self.pageSize.itemData(index)] if index >= 0 else (-1.0, -1.0) w, h = nwLabels.PAPER_SIZE[self.pageSize.itemData(index)] if index >= 0 else (-1.0, -1.0)
if w > 0.0 and h > 0.0: if w > 0.0 and h > 0.0:
self.pageWidth.blockSignals(True) self.pageWidth.blockSignals(True)
@@ -1544,23 +1502,20 @@ class _FormattingTab(NScrollableForm):
self.pageHeight.blockSignals(True) self.pageHeight.blockSignals(True)
self.pageHeight.setValue(h/self._unitScale) self.pageHeight.setValue(h/self._unitScale)
self.pageHeight.blockSignals(False) self.pageHeight.blockSignals(False)
return
@pyqtSlot() @pyqtSlot()
def _pageSizeValueChanged(self) -> None: def _pageSizeValueChanged(self) -> None:
"""The user has changed the page size spin boxes, so we flip """Process that the user has changed the page size spin boxes,
the page size box to Custom. so we flip the page size box to Custom.
""" """
index = self.pageSize.findData("Custom") index = self.pageSize.findData("Custom")
if index >= 0: if index >= 0:
self.pageSize.setCurrentIndex(index) self.pageSize.setCurrentIndex(index)
return
def _resetPageHeader(self) -> None: def _resetPageHeader(self) -> None:
"""Reset the ODT header format to default.""" """Reset the ODT header format to default."""
self.odtPageHeader.setText(nwHeadFmt.DOC_AUTO) self.odtPageHeader.setText(nwHeadFmt.DOC_AUTO)
self.odtPageHeader.setCursorPosition(0) self.odtPageHeader.setCursorPosition(0)
return
## ##
# Internal Functions # Internal Functions
@@ -1573,4 +1528,3 @@ class _FormattingTab(NScrollableForm):
current.append(keyword) current.append(keyword)
verified = set(x for x in current if x in nwKeyWords.VALID_KEYS) verified = set(x for x in current if x in nwKeyWords.VALID_KEYS)
self.ignoredKeywords.setText(", ".join(verified)) self.ignoredKeywords.setText(", ".join(verified))
return
+2 -20
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -52,6 +52,7 @@ logger = logging.getLogger(__name__)
class GuiNovelDetails(NNonBlockingDialog): class GuiNovelDetails(NNonBlockingDialog):
"""GUI: Novel Details Tool."""
PAGE_OVERVIEW = 1 PAGE_OVERVIEW = 1
PAGE_CONTENTS = 2 PAGE_CONTENTS = 2
@@ -130,11 +131,8 @@ class GuiNovelDetails(NNonBlockingDialog):
logger.debug("Ready: GuiNovelDetails") logger.debug("Ready: GuiNovelDetails")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiNovelDetails") logger.debug("Delete: GuiNovelDetails")
return
## ##
# Methods # Methods
@@ -146,7 +144,6 @@ class GuiNovelDetails(NNonBlockingDialog):
self.overviewPage.updateProjectData() self.overviewPage.updateProjectData()
self.overviewPage.novelValueChanged(handle) self.overviewPage.novelValueChanged(handle)
self.contentsPage.novelValueChanged(handle) self.contentsPage.novelValueChanged(handle)
return
## ##
# Events # Events
@@ -157,7 +154,6 @@ class GuiNovelDetails(NNonBlockingDialog):
self._saveSettings() self._saveSettings()
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Private Slots # Private Slots
@@ -170,7 +166,6 @@ class GuiNovelDetails(NNonBlockingDialog):
self.mainStack.setCurrentWidget(self.overviewPage) self.mainStack.setCurrentWidget(self.overviewPage)
elif pageId == self.PAGE_CONTENTS: elif pageId == self.PAGE_CONTENTS:
self.mainStack.setCurrentWidget(self.contentsPage) self.mainStack.setCurrentWidget(self.contentsPage)
return
## ##
# Internal Functions # Internal Functions
@@ -185,7 +180,6 @@ class GuiNovelDetails(NNonBlockingDialog):
options.setValue("GuiNovelDetails", "winHeight", self.height()) options.setValue("GuiNovelDetails", "winHeight", self.height())
options.setValue("GuiNovelDetails", "novelRoot", novelRoot) options.setValue("GuiNovelDetails", "novelRoot", novelRoot)
self.contentsPage.saveSettings() self.contentsPage.saveSettings()
return
class _OverviewPage(NScrollablePage): class _OverviewPage(NScrollablePage):
@@ -248,8 +242,6 @@ class _OverviewPage(NScrollablePage):
self.setCentralLayout(self.outerBox) self.setCentralLayout(self.outerBox)
return
## ##
# Methods # Methods
## ##
@@ -266,7 +258,6 @@ class _OverviewPage(NScrollablePage):
self.projWords.setText(f"{wcNovel + wcNotes:n}") self.projWords.setText(f"{wcNovel + wcNotes:n}")
self.projNovels.setText(f"{wcNovel:n}") self.projNovels.setText(f"{wcNovel:n}")
self.projNotes.setText(f"{wcNotes:n}") self.projNotes.setText(f"{wcNotes:n}")
return
## ##
# Public Slots # Public Slots
@@ -286,8 +277,6 @@ class _OverviewPage(NScrollablePage):
self.novelChapters.setText(f"{hCounts[2]:n}") self.novelChapters.setText(f"{hCounts[2]:n}")
self.novelScenes.setText(f"{hCounts[3]:n}") self.novelScenes.setText(f"{hCounts[3]:n}")
return
class _ContentsPage(NFixedPage): class _ContentsPage(NFixedPage):
@@ -400,8 +389,6 @@ class _ContentsPage(NFixedPage):
self.setCentralLayout(self.outerBox) self.setCentralLayout(self.outerBox)
return
def saveSettings(self) -> None: def saveSettings(self) -> None:
"""Save the user GUI settings.""" """Save the user GUI settings."""
options = SHARED.project.options options = SHARED.project.options
@@ -413,7 +400,6 @@ class _ContentsPage(NFixedPage):
options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value()) options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value())
options.setValue("GuiNovelDetails", "countFrom", self.poValue.value()) options.setValue("GuiNovelDetails", "countFrom", self.poValue.value())
options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked()) options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked())
return
## ##
# Public Slots # Public Slots
@@ -426,7 +412,6 @@ class _ContentsPage(NFixedPage):
self._prepareData(tHandle) self._prepareData(tHandle)
self._populateTree() self._populateTree()
self._currentRoot = tHandle self._currentRoot = tHandle
return
## ##
# Private Slots # Private Slots
@@ -496,8 +481,6 @@ class _ContentsPage(NFixedPage):
self.tocTree.addTopLevelItem(newItem) self.tocTree.addTopLevelItem(newItem)
return
## ##
# Internal Functions # Internal Functions
## ##
@@ -507,4 +490,3 @@ class _ContentsPage(NFixedPage):
logger.debug("Populating ToC from handle '%s'", rootHandle) logger.debug("Populating ToC from handle '%s'", rootHandle)
self._data = SHARED.project.index.getTableOfContents(rootHandle, 2) self._data = SHARED.project.index.getTableOfContents(rootHandle, 2)
self._data.append(("", 0, self.tr("END"), 0)) self._data.append(("", 0, self.tr("END"), 0))
return
+6 -39
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -56,6 +56,11 @@ PANEL_ALPHA = 178
class GuiWelcome(NDialog): class GuiWelcome(NDialog):
"""GUI: Welcome Dialog.
This is the main dialog shown when novelWriter launches or when the
user wants to create or open another project.
"""
openProjectRequest = pyqtSignal(Path) openProjectRequest = pyqtSignal(Path)
@@ -161,11 +166,8 @@ class GuiWelcome(NDialog):
logger.debug("Ready: GuiWelcome") logger.debug("Ready: GuiWelcome")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiWelcome") logger.debug("Delete: GuiWelcome")
return
## ##
# Events # Events
@@ -180,14 +182,12 @@ class GuiWelcome(NDialog):
painter.drawPixmap(0, hWin - hPix, self.bgImage.scaledToHeight(hPix, tMode)) painter.drawPixmap(0, hWin - hPix, self.bgImage.scaledToHeight(hPix, tMode))
painter.end() painter.end()
super().paintEvent(event) super().paintEvent(event)
return
def closeEvent(self, event: QCloseEvent) -> None: def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the user closing the window and save settings.""" """Capture the user closing the window and save settings."""
self._saveSettings() self._saveSettings()
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Private Slots # Private Slots
@@ -199,28 +199,24 @@ class GuiWelcome(NDialog):
self.mainStack.setCurrentWidget(self.tabNew) self.mainStack.setCurrentWidget(self.tabNew)
self._setButtonVisibility() self._setButtonVisibility()
self.tabNew.enterForm() self.tabNew.enterForm()
return
@pyqtSlot() @pyqtSlot()
def _showOpenProjectPage(self) -> None: def _showOpenProjectPage(self) -> None:
"""Show the open exiting project page.""" """Show the open exiting project page."""
self.mainStack.setCurrentWidget(self.tabOpen) self.mainStack.setCurrentWidget(self.tabOpen)
self._setButtonVisibility() self._setButtonVisibility()
return
@pyqtSlot() @pyqtSlot()
def _browseForProject(self) -> None: def _browseForProject(self) -> None:
"""Browse for a project to open.""" """Browse for a project to open."""
if path := SHARED.getProjectPath(self, path=CONFIG.homePath(), allowZip=False): if path := SHARED.getProjectPath(self, path=CONFIG.homePath(), allowZip=False):
self._openProjectPath(path) self._openProjectPath(path)
return
@pyqtSlot() @pyqtSlot()
def _openSelectedItem(self) -> None: def _openSelectedItem(self) -> None:
"""Open the currently selected project item.""" """Open the currently selected project item."""
if self.mainStack.currentWidget() == self.tabOpen: if self.mainStack.currentWidget() == self.tabOpen:
self.tabOpen.openSelectedItem() self.tabOpen.openSelectedItem()
return
@pyqtSlot(Path) @pyqtSlot(Path)
def _openProjectPath(self, path: Path) -> None: def _openProjectPath(self, path: Path) -> None:
@@ -231,7 +227,6 @@ class GuiWelcome(NDialog):
self.hide() self.hide()
self.openProjectRequest.emit(path) self.openProjectRequest.emit(path)
self.close() self.close()
return
## ##
# Internal Functions # Internal Functions
@@ -241,7 +236,6 @@ class GuiWelcome(NDialog):
"""Save the user GUI settings.""" """Save the user GUI settings."""
logger.debug("Saving State: GuiWelcome") logger.debug("Saving State: GuiWelcome")
CONFIG.setWelcomeWinSize(self.width(), self.height()) CONFIG.setWelcomeWinSize(self.width(), self.height())
return
def _setButtonVisibility(self) -> None: def _setButtonVisibility(self) -> None:
"""Change the visibility of the dialog buttons.""" """Change the visibility of the dialog buttons."""
@@ -255,7 +249,6 @@ class GuiWelcome(NDialog):
self.btnOpen.setFocus() self.btnOpen.setFocus()
else: else:
self.btnCreate.setFocus() self.btnCreate.setFocus()
return
class _OpenProjectPage(QWidget): class _OpenProjectPage(QWidget):
@@ -308,8 +301,6 @@ class _OpenProjectPage(QWidget):
f"QLineEdit {{border: none; background: {baseCol}; padding: 4px;}} " f"QLineEdit {{border: none; background: {baseCol}; padding: 4px;}} "
) )
return
## ##
# Public Slots # Public Slots
## ##
@@ -319,7 +310,6 @@ class _OpenProjectPage(QWidget):
"""Open the currently selected project item.""" """Open the currently selected project item."""
if (selection := self.listWidget.selectedIndexes()) and (index := selection[0]).isValid(): if (selection := self.listWidget.selectedIndexes()) and (index := selection[0]).isValid():
self.openProjectRequest.emit(Path(str(index.data()[1]))) self.openProjectRequest.emit(Path(str(index.data()[1])))
return
## ##
# Private Slots # Private Slots
@@ -335,14 +325,12 @@ class _OpenProjectPage(QWidget):
self.selectedPath.setToolTip(text) self.selectedPath.setToolTip(text)
self.selectedPath.setCursorPosition(0) self.selectedPath.setCursorPosition(0)
self.aMissing.setVisible(not (Path(value) / nwFiles.PROJ_FILE).is_file()) self.aMissing.setVisible(not (Path(value) / nwFiles.PROJ_FILE).is_file())
return
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _projectDoubleClicked(self, index: QModelIndex) -> None: def _projectDoubleClicked(self, index: QModelIndex) -> None:
"""Process double click on project item.""" """Process double click on project item."""
if index.isValid(): if index.isValid():
self.openProjectRequest.emit(Path(str(index.data()[1]))) self.openProjectRequest.emit(Path(str(index.data()[1])))
return
@pyqtSlot() @pyqtSlot()
def _deleteSelectedItem(self) -> None: def _deleteSelectedItem(self) -> None:
@@ -355,7 +343,6 @@ class _OpenProjectPage(QWidget):
if SHARED.question(text): if SHARED.question(text):
self.listModel.removeEntry(index) self.listModel.removeEntry(index)
self._selectFirstItem() self._selectFirstItem()
return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
def _openContextMenu(self, pos: QPoint) -> None: def _openContextMenu(self, pos: QPoint) -> None:
@@ -368,7 +355,6 @@ class _OpenProjectPage(QWidget):
action.triggered.connect(self._deleteSelectedItem) action.triggered.connect(self._deleteSelectedItem)
ctxMenu.exec(self.mapToGlobal(pos)) ctxMenu.exec(self.mapToGlobal(pos))
ctxMenu.setParent(None) ctxMenu.setParent(None)
return
## ##
# Internal Functions # Internal Functions
@@ -379,7 +365,6 @@ class _OpenProjectPage(QWidget):
index = self.listModel.index(0) index = self.listModel.index(0)
self.listWidget.setCurrentIndex(index) self.listWidget.setCurrentIndex(index)
self._projectClicked(index) self._projectClicked(index)
return
class _ProjectListItem(QStyledItemDelegate): class _ProjectListItem(QStyledItemDelegate):
@@ -407,8 +392,6 @@ class _ProjectListItem(QStyledItemDelegate):
self._icon = SHARED.theme.getPixmap("proj_nwx", (iPx, iPx)) self._icon = SHARED.theme.getPixmap("proj_nwx", (iPx, iPx))
return
def paint(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None: def paint(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None:
"""Paint a project entry on the canvas.""" """Paint a project entry on the canvas."""
rect = opt.rect rect = opt.rect
@@ -430,8 +413,6 @@ class _ProjectListItem(QStyledItemDelegate):
painter.drawText(rect.adjusted(x, y, 0, 0), tFlag, details) painter.drawText(rect.adjusted(x, y, 0, 0), tFlag, details)
painter.restore() painter.restore()
return
def sizeHint(self, opt: QStyleOptionViewItem, index: QModelIndex) -> QSize: def sizeHint(self, opt: QStyleOptionViewItem, index: QModelIndex) -> QSize:
"""Set the size hint to fixed height.""" """Set the size hint to fixed height."""
return QSize(opt.rect.width(), self._hPx) return QSize(opt.rect.width(), self._hPx)
@@ -449,7 +430,6 @@ class _ProjectListModel(QAbstractListModel):
when = CONFIG.localDate(datetime.fromtimestamp(time)) when = CONFIG.localDate(datetime.fromtimestamp(time))
data.append((title, path, f"{opened}: {when}, {words}: {formatInt(count)}")) data.append((title, path, f"{opened}: {when}, {words}: {formatInt(count)}"))
self._data = data self._data = data
return
def rowCount(self, parent: QModelIndex | None = None) -> int: def rowCount(self, parent: QModelIndex | None = None) -> int:
"""Return the size of the model.""" """Return the size of the model."""
@@ -516,8 +496,6 @@ class _NewProjectPage(QWidget):
f"_NewProjectForm {{border: none; background: {baseCol};}} " f"_NewProjectForm {{border: none; background: {baseCol};}} "
) )
return
## ##
# Public Slots # Public Slots
## ##
@@ -689,13 +667,10 @@ class _NewProjectForm(QWidget):
self._updateProjPath() self._updateProjPath()
self._updateFillInfo() self._updateFillInfo()
return
def enterForm(self) -> None: def enterForm(self) -> None:
"""Focus the project name field when entering the form.""" """Focus the project name field when entering the form."""
self.projName.setFocus() self.projName.setFocus()
self.projName.selectAll() self.projName.selectAll()
return
def getProjectData(self) -> dict: def getProjectData(self) -> dict:
"""Collect form data and return it as a dictionary.""" """Collect form data and return it as a dictionary."""
@@ -733,14 +708,12 @@ class _NewProjectForm(QWidget):
self._basePath = Path(path) self._basePath = Path(path)
self._updateProjPath() self._updateProjPath()
CONFIG.setLastPath("project", path) CONFIG.setLastPath("project", path)
return
@pyqtSlot() @pyqtSlot()
def _updateProjPath(self) -> None: def _updateProjPath(self) -> None:
"""Update the path box to show the full project path.""" """Update the path box to show the full project path."""
projName = makeFileNameSafe(self.projName.text().strip()) projName = makeFileNameSafe(self.projName.text().strip())
self.projPath.setText(str(self._basePath / projName)) self.projPath.setText(str(self._basePath / projName))
return
@pyqtSlot() @pyqtSlot()
def _syncSwitches(self) -> None: def _syncSwitches(self) -> None:
@@ -750,21 +723,18 @@ class _NewProjectForm(QWidget):
addWorld = self.addWorld.isChecked() addWorld = self.addWorld.isChecked()
if not (addPlot or addChar or addWorld): if not (addPlot or addChar or addWorld):
self.addNotes.setChecked(False) self.addNotes.setChecked(False)
return
@pyqtSlot() @pyqtSlot()
def _setFillBlank(self) -> None: def _setFillBlank(self) -> None:
"""Set fill mode to blank project.""" """Set fill mode to blank project."""
self._fillMode = self.FILL_BLANK self._fillMode = self.FILL_BLANK
self._updateFillInfo() self._updateFillInfo()
return
@pyqtSlot() @pyqtSlot()
def _setFillSample(self) -> None: def _setFillSample(self) -> None:
"""Set fill mode to sample project.""" """Set fill mode to sample project."""
self._fillMode = self.FILL_SAMPLE self._fillMode = self.FILL_SAMPLE
self._updateFillInfo() self._updateFillInfo()
return
@pyqtSlot() @pyqtSlot()
def _setFillCopy(self) -> None: def _setFillCopy(self) -> None:
@@ -773,7 +743,6 @@ class _NewProjectForm(QWidget):
self._fillMode = self.FILL_COPY self._fillMode = self.FILL_COPY
self._copyPath = copyPath self._copyPath = copyPath
self._updateFillInfo() self._updateFillInfo()
return
## ##
# Internal Functions # Internal Functions
@@ -793,5 +762,3 @@ class _NewProjectForm(QWidget):
self.projFill.setToolTip(text) self.projFill.setToolTip(text)
self.projFill.setCursorPosition(0) self.projFill.setCursorPosition(0)
self.extraWidget.setVisible(self._fillMode == self.FILL_BLANK) self.extraWidget.setVisible(self._fillMode == self.FILL_BLANK)
return
+2 -13
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -54,7 +54,7 @@ logger = logging.getLogger(__name__)
class GuiWritingStats(NToolDialog): class GuiWritingStats(NToolDialog):
"""GUI Tools: Writing Statistics """GUI Tools: Writing Statistics.
Displays data from the NWSessionLog object. Displays data from the NWSessionLog object.
""" """
@@ -311,11 +311,8 @@ class GuiWritingStats(NToolDialog):
logger.debug("Ready: GuiWritingStats") logger.debug("Ready: GuiWritingStats")
return
def __del__(self) -> None: # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiWritingStats") logger.debug("Delete: GuiWritingStats")
return
def populateGUI(self) -> None: def populateGUI(self) -> None:
"""Populate list box with data from the log file.""" """Populate list box with data from the log file."""
@@ -323,7 +320,6 @@ class GuiWritingStats(NToolDialog):
self._loadLogFile() self._loadLogFile()
self._updateListBox() self._updateListBox()
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
return
## ##
# Events # Events
@@ -333,7 +329,6 @@ class GuiWritingStats(NToolDialog):
"""Capture the user closing the window.""" """Capture the user closing the window."""
event.accept() event.accept()
self.softDelete() self.softDelete()
return
## ##
# Private Slots # Private Slots
@@ -377,8 +372,6 @@ class GuiWritingStats(NToolDialog):
self.close() self.close()
return
def _saveData(self, dataFmt: int) -> bool: def _saveData(self, dataFmt: int) -> bool:
"""Save the content of the list box to a file.""" """Save the content of the list box to a file."""
fileExt = "" fileExt = ""
@@ -498,8 +491,6 @@ class GuiWritingStats(NToolDialog):
self.notesWords.setText(f"{ttNotes:n}") self.notesWords.setText(f"{ttNotes:n}")
self.totalWords.setText(f"{ttWords:n}") self.totalWords.setText(f"{ttWords:n}")
return
## ##
# Private Slots # Private Slots
## ##
@@ -622,5 +613,3 @@ class GuiWritingStats(NToolDialog):
self.timeFilter += sDiff self.timeFilter += sDiff
self.labelFilter.setText(formatTime(round(self.timeFilter))) self.labelFilter.setText(formatTime(round(self.timeFilter)))
return
+1 -1
View File
@@ -20,7 +20,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
+1 -8
View File
@@ -23,7 +23,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import argparse import argparse
@@ -50,7 +50,6 @@ OS_WIN = sys.platform.startswith("win32")
def printVersion(args: argparse.Namespace) -> None: def printVersion(args: argparse.Namespace) -> None:
"""Print the novelWriter version and exit.""" """Print the novelWriter version and exit."""
print(extractVersion(beQuiet=True)[0], end=None) print(extractVersion(beQuiet=True)[0], end=None)
return
def installPackages(args: argparse.Namespace) -> None: def installPackages(args: argparse.Namespace) -> None:
@@ -79,8 +78,6 @@ def installPackages(args: argparse.Namespace) -> None:
print(str(exc)) print(str(exc))
sys.exit(1) sys.exit(1)
return
def cleanBuildDirs(args: argparse.Namespace) -> None: def cleanBuildDirs(args: argparse.Namespace) -> None:
"""Recursively delete the 'build' and 'dist' folders.""" """Recursively delete the 'build' and 'dist' folders."""
@@ -111,8 +108,6 @@ def cleanBuildDirs(args: argparse.Namespace) -> None:
print("") print("")
return
def genMacOSPlist(args: argparse.Namespace) -> None: def genMacOSPlist(args: argparse.Namespace) -> None:
"""Set necessary values for .plist file for MacOS build.""" """Set necessary values for .plist file for MacOS build."""
@@ -134,8 +129,6 @@ def genMacOSPlist(args: argparse.Namespace) -> None:
print(f"Writing Info.plist to {outDir}/Info.plist") print(f"Writing Info.plist to {outDir}/Info.plist")
writeFile(outDir / "Info.plist", plistXML) writeFile(outDir / "Info.plist", plistXML)
return
if __name__ == "__main__": if __name__ == "__main__":
"""Parse command line options and run the commands.""" """Parse command line options and run the commands."""
+39 -19
View File
@@ -66,26 +66,34 @@ preview = true
# Rules: https://docs.astral.sh/ruff/rules # Rules: https://docs.astral.sh/ruff/rules
select = [ select = [
"A", # flake8-builtins (A) "A", # flake8-builtins (A)
"ANN", # flake8-annotations (ANN) "ANN", # flake8-annotations (ANN)
"B", # flake8-bugbear (B) "B", # flake8-bugbear (B)
"E", # pycodestyle (E) "D", # pydocstyle (D)
"F", # Pyflakes (F) "E", # pycodestyle (E)
"FA", # flake8-future-annotations (FA) "F", # Pyflakes (F)
"PERF", # Perflint (PERF) "FA", # flake8-future-annotations (FA)
"PLC", # Pylint Convention (PLC) "PERF", # Perflint (PERF)
"PLE", # Pylint Error (PLE) "PLC", # Pylint Convention (PLC)
"PLW", # Pylint Warning (PLW) "PLE", # Pylint Error (PLE)
"Q", # flake8-quotes (Q) "PLR17", # Refactor (PLR) - Only PLR17xx
"RUF", # Ruff-specific rules (RUF) "PLW", # Pylint Warning (PLW)
"SLF", # flake8-self (SLF) "Q", # flake8-quotes (Q)
"SLOT", # flake8-slots (SLOT) "RET", # flake8-return (RET)
"TC", # flake8-type-checking (TC) "RUF", # Ruff-specific rules (RUF)
"UP", # pyupgrade (UP) "SLF", # flake8-self (SLF)
"W", # pycodestyle (W) "SLOT", # flake8-slots (SLOT)
"TC", # flake8-type-checking (TC)
"UP", # pyupgrade (UP)
"W", # pycodestyle (W)
] ]
ignore = [ ignore = [
"ANN401", # any-type "ANN401", # any-type
"D105", # undocumented-magic-method
"D107", # undocumented-public-init
"D203", # incorrect-blank-line-before-class
"D205", # missing-blank-line-after-summary
"D213", # multi-line-summary-second-line
"E221", # multiple-spaces-before-operator "E221", # multiple-spaces-before-operator
"E226", # missing-whitespace-around-arithmetic-operator "E226", # missing-whitespace-around-arithmetic-operator
"E228", # missing-whitespace-around-modulo-operator "E228", # missing-whitespace-around-modulo-operator
@@ -95,6 +103,7 @@ ignore = [
"PLC1901", # compare-to-empty-string "PLC1901", # compare-to-empty-string
"PLW0108", # unnecessary-lambda "PLW0108", # unnecessary-lambda
"PLW2901", # redefined-loop-name "PLW2901", # redefined-loop-name
"RET505", # superfluous-else-return
"RUF001", # ambiguous-unicode-character-string "RUF001", # ambiguous-unicode-character-string
"RUF002", # ambiguous-unicode-character-docstring "RUF002", # ambiguous-unicode-character-docstring
"RUF015", # unnecessary-iterable-allocation-for-first-element "RUF015", # unnecessary-iterable-allocation-for-first-element
@@ -103,9 +112,21 @@ ignore = [
] ]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"tests/*" = ["ANN", "SLF", "TC", "PLC2701"] "tests/*" = ["ANN", "SLF", "TC", "PLC2701", "D101", "D102"]
"utils/*" = ["ANN", "SLF", "TC"] "utils/*" = ["ANN", "SLF", "TC"]
[tool.ruff.lint.pydocstyle]
ignore-decorators = [
"abc.abstractmethod",
"property",
"PyQt6.QtCore.pyqtProperty",
"typing.overload",
"pytest.fixture",
]
[tool.ruff.lint.pylint]
max-nested-blocks = 10
[tool.ruff.format] [tool.ruff.format]
quote-style = "double" quote-style = "double"
@@ -114,7 +135,6 @@ include = ["novelwriter"]
exclude = ["**/__pycache__"] exclude = ["**/__pycache__"]
reportIncompatibleMethodOverride = false reportIncompatibleMethodOverride = false
pythonVersion = "3.10" pythonVersion = "3.10"
[tool.pytest.ini_options] [tool.pytest.ini_options]
+2 -10
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -70,8 +70,6 @@ def resetConfigVars():
CONFIG.emphLabels = True CONFIG.emphLabels = True
CONFIG.lineHighlight = True CONFIG.lineHighlight = True
return
## ##
# Auto Fixtures # Auto Fixtures
@@ -88,7 +86,6 @@ def sessionFixture():
(_TMP_ROOT / "manual.pdf").touch() (_TMP_ROOT / "manual.pdf").touch()
(_SRC_ROOT / "novelwriter" / "assets"/ "manual.pdf").touch() (_SRC_ROOT / "novelwriter" / "assets"/ "manual.pdf").touch()
(_SRC_ROOT / "novelwriter" / "assets"/ "manual_fr.pdf").touch() (_SRC_ROOT / "novelwriter" / "assets"/ "manual_fr.pdf").touch()
return
@pytest.fixture(scope="function", autouse=True) @pytest.fixture(scope="function", autouse=True)
@@ -107,8 +104,6 @@ def functionFixture(qtbot):
resetConfigVars() resetConfigVars()
logging.getLogger("novelwriter").setLevel(logging.INFO) logging.getLogger("novelwriter").setLevel(logging.INFO)
return
## ##
# Core Test Folders # Core Test Folders
@@ -251,13 +246,11 @@ def prjLipsum():
if dstDir.exists(): if dstDir.exists():
shutil.rmtree(dstDir) shutil.rmtree(dstDir)
return
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def ipsumText(): def ipsumText():
"""Return five paragraphs of Lorem Ipsum text.""" """Return five paragraphs of Lorem Ipsum text."""
thatIpsum = [( return [(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum co" "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum co"
"mmodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, e" "mmodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, e"
"get euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, ve" "get euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, ve"
@@ -305,4 +298,3 @@ def ipsumText():
" a nisl. Etiam eget tristique dui. Nulla sed mi finibus, venenatis tellus non, maximus en" " a nisl. Etiam eget tristique dui. Nulla sed mi finibus, venenatis tellus non, maximus en"
"im." "im."
)] )]
return thatIpsum
+3 -4
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from unittest.mock import MagicMock from unittest.mock import MagicMock
@@ -35,7 +35,6 @@ class MockGuiMain(QWidget):
self.docViewer = MagicMock() self.docViewer = MagicMock()
self.mainProgress = MagicMock() self.mainProgress = MagicMock()
self.projPath = "" self.projPath = ""
return
def postLaunchTasks(self, cmdOpen): def postLaunchTasks(self, cmdOpen):
return return
@@ -45,7 +44,6 @@ class MockGuiMain(QWidget):
def openProject(self, projPath): def openProject(self, projPath):
self.projPath = projPath self.projPath = projPath
return
def rebuildIndex(self): def rebuildIndex(self):
return return
@@ -64,7 +62,6 @@ class MockTheme:
self.guiFont = QFont() self.guiFont = QFont()
self.guiFontB = QFont() self.guiFontB = QFont()
self.guiFontBU = QFont() self.guiFontBU = QFont()
return
def initThemes(self) -> None: def initThemes(self) -> None:
return return
@@ -96,8 +93,10 @@ class MockApp:
# Mock functions that will raise errors instead. # Mock functions that will raise errors instead.
def causeOSError(*args, **kwargs): def causeOSError(*args, **kwargs):
"""Raise an OSError."""
raise OSError("Mock OSError") raise OSError("Mock OSError")
def causeException(*args, **kwargs): def causeException(*args, **kwargs):
"""Raise an Exception."""
raise Exception("Mock Exception") raise Exception("Mock Exception")
+1 -2
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import time import time
@@ -794,7 +794,6 @@ def testBaseCommon_openExternalPath(monkeypatch, tstPaths):
def mockOpenUrl(url: QUrl) -> None: def mockOpenUrl(url: QUrl) -> None:
nonlocal lastUrl nonlocal lastUrl
lastUrl = url.toString() lastUrl = url.toString()
return
monkeypatch.setattr(QDesktopServices, "openUrl", mockOpenUrl) monkeypatch.setattr(QDesktopServices, "openUrl", mockOpenUrl)
assert openExternalPath(Path("/foo/bar")) is False assert openExternalPath(Path("/foo/bar")) is False
+1 -1
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import datetime import datetime
+1 -1
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import sys import sys
+2 -2
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -40,7 +40,7 @@ from tests.tools import clearLogHandlers
@pytest.mark.base @pytest.mark.base
def testBaseInit_Launch(caplog, monkeypatch, fncPath): def testBaseInit_Launch(caplog, monkeypatch, fncPath):
"""Check launching the main GUI. This test """ """Check launching the main GUI."""
monkeypatch.setattr(NSplashScreen, "finish", lambda *a: None) monkeypatch.setattr(NSplashScreen, "finish", lambda *a: None)
monkeypatch.setattr("novelwriter.splash.sleep", lambda *a: None) monkeypatch.setattr("novelwriter.splash.sleep", lambda *a: None)
monkeypatch.setattr("novelwriter._createApp", lambda *a: Mock()) monkeypatch.setattr("novelwriter._createApp", lambda *a: Mock())
+1 -1
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
from unittest.mock import MagicMock from unittest.mock import MagicMock
+2 -2
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
@@ -39,7 +39,7 @@ from tests.tools import C, buildTestProject
def isUUID(value): def isUUID(value):
"""Checks if a value is a valid UUID object.""" """Check if a value is a valid UUID object."""
try: try:
uuid.UUID(value) uuid.UUID(value)
return True return True
+1 -2
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import shutil import shutil
@@ -669,7 +669,6 @@ def testCoreTools_ProjectBuilderCopyPlain(monkeypatch, caplog, mockGUI, prjLipsu
@pytest.mark.core @pytest.mark.core
def testCoreTools_ProjectBuilderCopyZipped(monkeypatch, caplog, mockGUI, fncPath, mockRnd): def testCoreTools_ProjectBuilderCopyZipped(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
"""Create a new project copied from existing zipped project.""" """Create a new project copied from existing zipped project."""
# Create a project # Create a project
origPath = fncPath / "original" origPath = fncPath / "original"
srcProject = NWProject() srcProject = NWProject()
+1 -1
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
+1 -1
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import pytest import pytest
+1 -1
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import json import json
+1 -1
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import pytest import pytest
+2 -3
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import copy import copy
@@ -491,8 +491,7 @@ def testCoreItem_LayoutSetter(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreItem_ClassDefaults(mockGUI): def testCoreItem_ClassDefaults(mockGUI):
"""Test the setter for the default values. """Test the setter for the default values."""
"""
project = NWProject() project = NWProject()
item = NWItem(project, "0000000000000") item = NWItem(project, "0000000000000")
+1 -1
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import pytest import pytest
+1 -1
View File
@@ -17,7 +17,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """ # noqa
from __future__ import annotations from __future__ import annotations
import pytest import pytest

Some files were not shown because too many files have changed in this diff Show More