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.
Documentation: http://www.sphinx-doc.org/en/master/config
"""
""" # noqa
# -- 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
entries generated from this file.
"""
""" # noqa
from PyQt6.QtCore import QT_TRANSLATE_NOOP
+1 -1
View File
@@ -2,7 +2,7 @@
"""
novelWriter Start Script
==========================
"""
""" # noqa
import os
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -135,7 +135,7 @@ def checkPath(value: Any, default: Path) -> Path:
def isHandle(value: Any) -> TypeGuard[str]:
"""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):
return False
@@ -219,7 +219,7 @@ def firstFloat(*args: Any) -> float:
##
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):
return "ERR"
@@ -464,21 +464,21 @@ def fontMatcher(font: QFont) -> QFont:
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:
func(*args, **kwargs)
return wrapper
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)
parent.addAction(action)
return action
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)
parent.addMenu(menu)
return menu
@@ -487,7 +487,6 @@ def qtAddMenu(parent: QMenuBar | QMenu, label: str) -> QMenu:
def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None:
"""Encode handles into a mime data object."""
mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles))
return
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 ("{", "["):
n += 1
indent = "\n"+" "*n
if n > nmax and nmax > 0:
if n > nmax > 0:
buffer.append(chunk)
else:
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 ("}", "]"):
n -= 1
indent = "\n"+" "*n
if n >= nmax and nmax > 0:
if n >= nmax > 0:
buffer.append(chunk)
else:
buffer.append(indent + chunk)
elif first == ",":
if n > nmax and nmax > 0:
if n > nmax > 0:
buffer.append(chunk)
else:
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:
"""A modified version of the XML indent function in the standard
library. It behaves more closely to how the one from lxml does.
"""
""" # noqa: D401
tree = xml.getroot() if isinstance(xml, ET.ElementTree) else xml
if not isinstance(tree, ET.Element):
return
@@ -598,8 +597,6 @@ def xmlIndent(xml: ET.Element | ET.ElementTree) -> None:
if last is not None:
last.tail = indentations[level]
return
if len(tree):
indentChildren(tree, 0)
tree.tail = "\n"
@@ -614,7 +611,7 @@ def xmlElement(
attrib: dict | None = None,
tail: str | None = None,
) -> 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 {})
if text is not None:
if isinstance(text, bool):
@@ -634,7 +631,7 @@ def xmlSubElem(
attrib: dict | None = None,
tail: str | None = None,
) -> 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 {})
if text is not None:
if isinstance(text, bool):
@@ -665,6 +662,7 @@ def readTextFile(path: str | Path) -> str:
def makeFileNameSafe(text: str) -> str:
"""Return a filename-safe string.
See: https://unicode.org/reports/tr15/#Norm_Forms
"""
text = unicodedata.normalize("NFKC", text).strip()
@@ -697,7 +695,7 @@ _T_Enum = TypeVar("_T_Enum", bound=Enum)
class NWConfigParser(ConfigParser):
"""Common: Adapted Config Parser
"""Common: Adapted Config Parser.
This is a subclass of the standard config parser that adds type safe
helper functions, and support for lists. It also turns off
@@ -706,7 +704,6 @@ class NWConfigParser(ConfigParser):
def __init__(self) -> None:
super().__init__(interpolation=None)
return
def rdStr(self, section: str, option: str, default: str) -> str:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -63,6 +63,13 @@ DEF_TREECOL = "theme"
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__ = (
"_appPath", "_appRoot", "_backPath", "_backupPath", "_confPath", "_dLocale", "_dShortDate",
@@ -300,8 +307,6 @@ class Config:
# Packages
self.hasEnchant = False # The pyenchant package
return
##
# Properties
##
@@ -350,7 +355,6 @@ class Config:
def setLastAuthor(self, value: str) -> None:
"""Set tle last used author name."""
self._lastAuthor = simplified(value)
return
def setMainWinSize(self, width: int, height: int) -> None:
"""Set the size of the main window, but only if the change is
@@ -362,17 +366,14 @@ class Config:
self.mainWinSize[0] = width
if abs(self.mainWinSize[1] - height) > 5:
self.mainWinSize[1] = height
return
def setWelcomeWinSize(self, width: int, height: int) -> None:
"""Set the size of the Preferences dialog window."""
self.welcomeWinSize = [width, height]
return
def setPreferencesWinSize(self, width: int, height: int) -> None:
"""Set the size of the Preferences dialog window."""
self.prefsWinSize = [width, height]
return
def setLastPath(self, key: str, path: str | Path) -> None:
"""Set the last used path. Only the folder is saved, so if the
@@ -384,12 +385,10 @@ class Config:
path = path.parent
if path.is_dir():
self._recentPaths.setPath(key, path)
return
def setBackupPath(self, path: Path | str) -> None:
"""Set the current backup path."""
self._backupPath = checkPath(path, self._backPath)
return
def setGuiFont(self, value: QFont | str | None) -> None:
"""Update the GUI's font style from settings."""
@@ -410,7 +409,6 @@ class Config:
self.guiFont = fontMatcher(font)
logger.debug("Main font set to: %s", describeFont(font))
QApplication.setFont(self.guiFont)
return
def setTextFont(self, value: QFont | str | None) -> 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)
self.textFont = fontMatcher(font)
logger.debug("Text font set to: %s", describeFont(self.textFont))
return
##
# Methods
##
def homePath(self) -> Path:
"""The user's home folder."""
"""Return the user's home folder."""
return self._homePath
def dataPath(self, target: str | None = None) -> Path:
@@ -525,7 +522,6 @@ class Config:
"""Send a message to the splash screen."""
if self._splash:
self._splash.showStatus(message)
return
##
# Config Actions
@@ -568,8 +564,6 @@ class Config:
logger.debug("Config instance initialised")
return
def initLocalisation(self, nwApp: QApplication) -> None:
"""Initialise the localisation of the GUI."""
self.splashMessage("Loading localisation ...")
@@ -597,8 +591,6 @@ class Config:
nwApp.installTranslator(qTrans)
self._qtTrans[lngFile] = qTrans
return
def loadConfig(self, splash: NSplashScreen | None = None) -> bool:
"""Load preferences from file and replace default settings."""
self._splash = splash
@@ -872,7 +864,6 @@ class Config:
def finishStartup(self) -> None:
"""Call after startup is complete."""
self._splash = None
return
##
# Internal Functions
@@ -894,7 +885,6 @@ class Config:
else:
self.hasEnchant = True
logger.debug("Checking package 'pyenchant': OK")
return
def _prepareFont(self, font: QFont, kind: str) -> None:
"""Check Unicode availability in font. This also initialises any
@@ -905,16 +895,15 @@ class Config:
for char in nwUnicode.UI_SYMBOLS:
if not metrics.inFont(char): # type: ignore
logger.warning("No glyph U+%04x in font", ord(char)) # pragma: no cover
return
class RecentProjects:
"""A record of recently opened projects."""
def __init__(self, config: Config) -> None:
self._conf = config
self._data: dict[str, dict[str, str | int]] = {}
self._map: dict[str, str] = {}
return
def loadCache(self) -> bool:
"""Load the cache file for recent projects."""
@@ -976,14 +965,12 @@ class RecentProjects:
self.saveCache()
except Exception:
pass
return
def remove(self, path: str | Path) -> None:
"""Try to remove a path from the recent projects cache."""
if self._data.pop(str(path), None) is not None:
logger.debug("Removed recent: %s", path)
self.saveCache()
return
def _setEntry(
self, puuid: str, path: str, title: str, words: int, chars: int, saved: int
@@ -998,24 +985,22 @@ class RecentProjects:
}
if puuid:
self._map[puuid] = path
return
class RecentPaths:
"""A record of recently used file paths."""
KEYS: Final[list[str]] = ["default", "project", "import", "outline", "stats"]
def __init__(self, config: Config) -> None:
self._conf = config
self._data = {}
return
def setPath(self, key: str, path: Path | str) -> None:
"""Set a path for a given key, and save the cache."""
if key in self.KEYS:
self._data[key] = str(path)
self.saveCache()
return
def getPath(self, key: str) -> str | 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
from typing import Final
@@ -34,16 +34,17 @@ from novelwriter.enum import (
def trConst(text: str) -> str:
"""Wrapper function for locally translating constants."""
"""Translate a constant."""
return QCoreApplication.translate("Constant", text)
def trStats(text: str) -> str:
"""Wrapper function for locally translating stats constants."""
"""Translate a stats constants."""
return QCoreApplication.translate("Stats", text)
class nwConst:
"""Various Constants."""
# Date and Time Formats
FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format
@@ -70,6 +71,7 @@ class nwConst:
class nwRegEx:
"""Common RegExes."""
URL = r"https?://(?:www\.|(?!www))[\w/()@:%_\+-.~#?&=]+"
WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b"
@@ -83,6 +85,7 @@ class nwRegEx:
class nwShortcode:
"""Document ShortCodes."""
BOLD_O = "[b]"
BOLD_C = "[/b]"
@@ -112,6 +115,7 @@ class nwShortcode:
class nwStyles:
"""Style Settings for Headings."""
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL: Final[dict[str, int]] = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
@@ -143,6 +147,7 @@ class nwStyles:
class nwFiles:
"""novelWriter Files."""
# Config Files
CONF_FILE = "novelwriter.conf"
@@ -163,6 +168,7 @@ class nwFiles:
class nwKeyWords:
"""Meta Data KeyWord Constants."""
TAG_KEY = "@tag"
POV_KEY = "@pov"
@@ -210,6 +216,7 @@ class nwKeyWords:
class nwLists:
"""Various Lists."""
USER_CLASSES: Final[list[nwItemClass]] = [
nwItemClass.CHARACTER,
@@ -223,6 +230,7 @@ class nwLists:
class nwStats:
"""Text Statistics."""
CHARS = "allChars"
CHARS_TEXT = "textChars"
@@ -246,6 +254,7 @@ class nwStats:
class nwLabels:
"""Various Common GUI Labels."""
CLASS_NAME: Final[dict[nwItemClass, str]] = {
nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"),
@@ -472,6 +481,7 @@ class nwLabels:
class nwHeadFmt:
"""Manuscript Header Formats."""
BR = "{BR}"
TITLE = "{Title}"
@@ -498,8 +508,10 @@ class nwHeadFmt:
class nwQuotes:
"""Allowed quotation marks.
Source: https://en.wikipedia.org/wiki/Quotation_mark
"""
SYMBOLS: Final[dict[str, str]] = {
"\u0027": QT_TRANSLATE_NOOP("Constant", "Straight single quotation mark"),
"\u0022": QT_TRANSLATE_NOOP("Constant", "Straight double quotation mark"),
@@ -541,6 +553,7 @@ class nwQuotes:
class nwUnicode:
"""Supported unicode character constants and their HTML equivalents."""
# Unicode Constants
# =================
@@ -672,6 +685,7 @@ class nwUnicode:
class nwHtmlUnicode:
"""Unicode to HTML Map."""
U_TO_H: Final[dict[str, str]] = {
# 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -211,7 +211,7 @@ class FilterMode(Enum):
class BuildSettings:
"""Core: Build Settings Class
"""Core: Build Settings Class.
This class manages the build settings for a Manuscript build job.
The settings can be packed/unpacked to/from a dictionary for JSON.
@@ -229,7 +229,6 @@ class BuildSettings:
self._included = set()
self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
self._changed = False
return
@classmethod
def fromDict(cls, data: dict) -> BuildSettings:
@@ -315,7 +314,6 @@ class BuildSettings:
def setName(self, name: str) -> None:
"""Set the build setting display name."""
self._name = str(name)
return
def setBuildID(self, value: str | uuid.UUID) -> None:
"""Set a UUID build ID."""
@@ -324,13 +322,11 @@ class BuildSettings:
self._uuid = str(uuid.uuid4())
elif value != self._uuid:
self._uuid = value
return
def setOrder(self, value: int) -> None:
"""Set the build order."""
if isinstance(value, int):
self._order = value
return
def setLastBuildPath(self, path: Path | str | None) -> None:
"""Set the last used build path."""
@@ -341,41 +337,35 @@ class BuildSettings:
else:
self._path = CONFIG.homePath()
self._changed = True
return
def setLastBuildName(self, name: str) -> None:
"""Set the last used build name."""
self._build = str(name).strip()
self._changed = True
return
def setLastFormat(self, value: nwBuildFmt) -> None:
"""Set the last used build format."""
if isinstance(value, nwBuildFmt):
self._format = value
self._changed = True
return
def setFiltered(self, tHandle: str) -> None:
"""Set an item as filtered."""
self._excluded.discard(tHandle)
self._included.discard(tHandle)
self._changed = True
return
def setIncluded(self, tHandle: str) -> None:
"""Set an item as explicitly included."""
self._excluded.discard(tHandle)
self._included.add(tHandle)
self._changed = True
return
def setExcluded(self, tHandle: str) -> None:
"""Set an item as explicitly excluded."""
self._excluded.add(tHandle)
self._included.discard(tHandle)
self._changed = True
return
def setAllowRoot(self, tHandle: str, state: bool) -> None:
"""Set a specific root folder as allowed or not."""
@@ -385,14 +375,12 @@ class BuildSettings:
elif state is False:
self._skipRoot.add(tHandle)
self._changed = True
return
def setValue(self, key: str, value: T_BuildValue) -> None:
"""Set a specific value for a build setting."""
if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]):
self._changed |= (value != self._settings[key])
self._settings[key] = value
return
##
# Methods
@@ -463,7 +451,6 @@ class BuildSettings:
called when the changes have been safely saved or passed on.
"""
self._changed = False
return
def pack(self) -> dict:
"""Pack all content into a JSON compatible dictionary."""
@@ -516,8 +503,6 @@ class BuildSettings:
self._changed = False
return
@classmethod
def duplicate(cls, source: BuildSettings) -> BuildSettings:
"""Make a copy of another build."""
@@ -529,7 +514,7 @@ class BuildSettings:
class BuildCollection:
"""Core: Build Collection Class
"""Core: Build Collection Class.
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
@@ -542,7 +527,6 @@ class BuildCollection:
self._defaultBuild = ""
self._builds: dict[str, BuildSettings] = {}
self._loadCollection()
return
def __len__(self) -> int:
"""Return the number of builds."""
@@ -581,21 +565,18 @@ class BuildCollection:
build.setOrder(i)
self._lastBuild = lastBuild
self._saveCollection()
return
def setDefaultBuild(self, buildID: str) -> None:
"""Set the default build id."""
if buildID != self._defaultBuild:
self._defaultBuild = buildID
self._saveCollection()
return
def setBuild(self, build: BuildSettings) -> None:
"""Set build settings data in the collection."""
if isinstance(build, BuildSettings):
self._builds[build.buildID] = build
self._saveCollection()
return
##
# Methods
@@ -605,7 +586,6 @@ class BuildCollection:
"""Remove a build from the collection."""
self._builds.pop(buildID, None)
self._saveCollection()
return
def builds(self) -> Iterable[tuple[str, str]]:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -52,7 +52,9 @@ logger = logging.getLogger(__name__)
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
GuiDocMerge dialog.
"""
@@ -62,7 +64,6 @@ class DocMerger:
self._error = ""
self._target = None
self._text = []
return
@property
def targetHandle(self) -> str | None:
@@ -85,7 +86,6 @@ class DocMerger:
"""
self._target = self._project.tree[tHandle]
self._text = []
return
def newTargetDoc(self, sHandle: str, label: str) -> None:
"""Create a brand new target document based on a source handle
@@ -101,7 +101,6 @@ class DocMerger:
nwItem.notifyToRefresh()
self._target = nwItem
self._text = []
return
def appendText(self, sHandle: str, addComment: bool, cmtPrefix: str) -> None:
"""Append text from an existing document to the text buffer."""
@@ -112,7 +111,6 @@ class DocMerger:
status, _ = item.getImportStatus()
text = f"% {cmtPrefix} {info}: {item.itemName} [{status}]\n\n{text}"
self._text.append(text)
return
def writeTargetDoc(self) -> bool:
"""Write the accumulated text into the designated target
@@ -158,10 +156,7 @@ class DocSplitter:
self._srcHandle = sHandle
self._srcItem = srcItem
return
def __len__(self) -> int:
"""The length of the split job."""
return len(self._rawData)
##
@@ -178,7 +173,6 @@ class DocSplitter:
"""
self._parHandle = pHandle
self._inFolder = False
return
def newParentFolder(self, pHandle: str, folderLabel: str) -> None:
"""Create a new folder that will be the top level parent item
@@ -192,7 +186,6 @@ class DocSplitter:
nwItem.notifyToRefresh()
self._parHandle = nHandle
self._inFolder = True
return
def splitDocument(self, splitData: list, splitText: list[str]) -> None:
"""Loop through the split data record and perform the split job
@@ -204,12 +197,9 @@ class DocSplitter:
chunk = buffer[lineNo:]
buffer = buffer[:lineNo]
self._rawData.insert(0, (chunk, hLevel, hLabel))
return
def writeDocuments(self, docHierarchy: bool) -> Iterable[bool]:
"""An iterator that will write each document in the buffer, and
return its new handle, parent handle, and sibling handle.
"""
"""Write each document in the buffer and yield if successful."""
if self._srcHandle and self._srcItem and self._parHandle:
pHandle = self._parHandle
hHandle = [self._parHandle, None, None, None, None]
@@ -260,7 +250,6 @@ class DocDuplicator:
def __init__(self, project: NWProject) -> None:
self._project = project
return
##
# Methods
@@ -293,13 +282,16 @@ class DocDuplicator:
class DocSearch:
"""Tool: Search Documents.
A global document search class.
"""
def __init__(self) -> None:
self._regEx = re.compile(r"")
self._opts = re.IGNORECASE
self._words = False
self._escape = True
return
##
# Methods
@@ -308,22 +300,19 @@ class DocSearch:
def setCaseSensitive(self, state: bool) -> None:
"""Set the case sensitive search flag."""
self._opts = 0 if state else re.IGNORECASE
return
def setWholeWords(self, state: bool) -> None:
"""Set the whole words search flag."""
self._words = state
return
def setUserRegEx(self, state: bool) -> None:
"""Set the escape flag to the opposite state."""
self._escape = not state
return
def iterSearch(
self, project: NWProject, search: str
) -> 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)
logger.debug("Searching with pattern '%s'", self._regEx.pattern)
storage = project.storage
@@ -376,7 +365,6 @@ class ProjectBuilder:
def __init__(self) -> None:
self._path = None
self.tr = partial(QCoreApplication.translate, "ProjectBuilder")
return
@property
def projPath(self) -> Path | None:
@@ -620,4 +608,3 @@ class ProjectBuilder:
project.index.rebuild()
project.saveProject()
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -53,7 +53,7 @@ logger = logging.getLogger(__name__)
class NWBuildDocument:
"""Core: Manuscript Document Build Class
"""Core: Manuscript Document Build Class.
This is the core tool that assembles a project and outputs a
manuscript, based on a build definition object (BuildSettings).
@@ -72,7 +72,6 @@ class NWBuildDocument:
self._cache = None
self._count = False
self._outline = False
return
##
# Properties
@@ -106,7 +105,6 @@ class NWBuildDocument:
def addDocument(self, tHandle: str) -> None:
"""Add a document to the build queue manually."""
self._queue.append(tHandle)
return
def queueAll(self) -> None:
"""Queue all document as defined by the build settings."""
@@ -115,7 +113,6 @@ class NWBuildDocument:
for item in self._project.tree:
if filtered.get(item.itemHandle, False):
self._queue.append(item.itemHandle)
return
def iterBuildPreview(self, newPage: bool) -> Iterable[tuple[int, bool]]:
"""Build a preview QTextDocument."""
@@ -131,7 +128,7 @@ class NWBuildDocument:
return
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._cache = None
@@ -341,12 +338,10 @@ class NWBuildDocument:
scale*self._build.getFloat("format.rightMargin"),
)
filtered = self._build.buildItemFilter(
return self._build.buildItemFilter(
self._project, withRoots=self._build.getBool("text.addNoteHeadings")
)
return filtered
def _doBuild(self, bldObj: Tokenizer, tHandle: str, convert: bool = True) -> bool:
"""Build a single document and add it to the build object."""
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import hashlib
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
class NWDocument:
"""Core: Document Class
"""Core: Document Class.
A Class wrapping a single novelWriter document file. It represents
a project item of nwItemType FILE. The file is not guaranteed to
@@ -68,8 +68,6 @@ class NWDocument:
if self._handle is not None:
self._item = self._project.tree[tHandle]
return
def __repr__(self) -> str:
return f"<NWDocument handle={self._handle}>"
@@ -357,5 +355,3 @@ class NWDocument:
else:
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -56,7 +56,7 @@ KEY_SOURCE = "0123456789bcdfghjklmnpqrstvwxz"
class Index:
"""Core: Project Index
"""Core: Project 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.
@@ -99,8 +99,6 @@ class Index:
self._indexChange = 0.0
self._rootChange = {}
return
def __repr__(self) -> str:
return f"<Index project='{self._project.data.name}'>"
@@ -129,7 +127,6 @@ class Index:
def setNovelModelExtraColumn(self, extra: nwNovelExtra) -> None:
"""Set the data content type of the novel model extra column."""
self._novelExtra = extra
return
##
# Public Methods
@@ -142,7 +139,6 @@ class Index:
self._indexChange = 0.0
self._rootChange = {}
SHARED.emitIndexCleared(self._project)
return
def rebuild(self) -> None:
"""Rebuild the entire index from scratch."""
@@ -158,7 +154,6 @@ class Index:
for tHandle in self._novelModels:
self.refreshNovelModel(tHandle)
SHARED.clearMainProgress()
return
def deleteHandle(self, tHandle: str) -> None:
"""Delete all entries of a given document handle."""
@@ -168,7 +163,6 @@ class Index:
del self._tagsIndex[tTag]
del self._itemIndex[tHandle]
SHARED.emitIndexChangedTags(self._project, [], delTags)
return
def reIndexHandle(self, tHandle: str | None) -> None:
"""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):
logger.debug("Re-indexing item '%s'", tHandle)
self.scanText(tHandle, self._project.storage.getDocumentText(tHandle))
return
def refreshHandle(self, tHandle: str) -> None:
"""Update the class for all tags of a handle."""
@@ -188,7 +181,6 @@ class Index:
self.deleteHandle(tHandle)
else:
self._tagsIndex.updateClass(tHandle, item.itemClass.name)
return
def indexChangedSince(self, checkTime: int | float) -> bool:
"""Check if the index has changed since a given time."""
@@ -211,7 +203,6 @@ class Index:
model.setExtraColumn(self._novelExtra)
self._appendSubTreeToModel(tHandle, model)
model.endResetModel()
return
def updateNovelModelData(self, nwItem: NWItem) -> bool:
"""Refresh a novel model."""
@@ -428,8 +419,6 @@ class Index:
if updated or deleted:
SHARED.emitIndexChangedTags(self._project, updated, deleted)
return
def _scanInactive(self, nwItem: NWItem, text: str) -> None:
"""Scan an inactive document for meta data."""
for line in text.splitlines():
@@ -438,7 +427,6 @@ class Index:
if hDepth != "H0":
nwItem.setMainHeading(hDepth)
break
return
def _splitHeading(self, line: str) -> tuple[str, str]:
"""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."""
cC, wC, pC = standardCounter(text)
self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
return
def _indexKeyword(
self, tHandle: str, line: str, sTitle: str, itemClass: nwItemClass, tags: dict[str, bool]
@@ -498,7 +485,6 @@ class Index:
model.setExtraColumn(self._novelExtra)
self._appendSubTreeToModel(tHandle, model)
self._novelModels[tHandle] = model
return
def _appendSubTreeToModel(self, tHandle: str, model: NovelModel) -> None:
"""Append all active novel documents to a novel model."""
@@ -509,7 +495,6 @@ class Index:
and node.item.isActive
):
model.append(node)
return
##
# Check @ Lines
@@ -683,15 +668,13 @@ class Index:
"words": hItem.wordCount,
}
result = [(
return [(
tKey,
tData[tKey]["level"],
tData[tKey]["title"],
tData[tKey]["words"]
) for tKey in tOrder]
return result
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,
starting at title sTitle if it is provided.
@@ -796,7 +779,7 @@ class Index:
# =====================
class TagsIndex:
"""Core: Tags Index Wrapper Class
"""Core: Tags Index Wrapper Class.
A wrapper class that holds the reverse lookup tags index. This is
just a simple wrapper around a single dictionary to keep tighter
@@ -807,14 +790,12 @@ class TagsIndex:
def __init__(self) -> None:
self._tags: dict[str, dict[str, str]] = {}
return
def __contains__(self, tagKey: str) -> bool:
return tagKey.lower() in self._tags
def __delitem__(self, tagKey: str) -> None:
self._tags.pop(tagKey.lower(), None)
return
def __getitem__(self, tagKey: str) -> dict | None:
return self._tags.get(tagKey.lower(), None)
@@ -826,7 +807,6 @@ class TagsIndex:
def clear(self) -> None:
"""Clear the index."""
self._tags = {}
return
def items(self) -> ItemsView:
"""Return a dictionary view of all tags."""
@@ -842,7 +822,6 @@ class TagsIndex:
"heading": sTitle,
"class": className,
}
return
def tagName(self, tagKey: str, default: str = "") -> str:
"""Get the name of a given tag."""
@@ -882,7 +861,6 @@ class TagsIndex:
for entry in self._tags.values():
if entry.get("handle") == tHandle:
entry["class"] = className
return
##
# Pack/Unpack
@@ -925,11 +903,9 @@ class TagsIndex:
self.add(name, display, handle, heading, className)
return
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
which provides lookup capabilities and caching for shared data.
@@ -941,14 +917,13 @@ class IndexCache:
self.tags: TagsIndex = tagsIndex
self.story: set[str] = set()
self.note: set[str] = set()
return
# The Item Index Objects
# ======================
class ItemIndex:
"""Core: Item Index Wrapper Class
"""Core: Item Index Wrapper Class.
A wrapper object holding the indexed items. This is a wrapper
class around a single storage dictionary with a set of utility
@@ -963,14 +938,12 @@ class ItemIndex:
self._project = project
self._cache = IndexCache(tagsIndex)
self._items: dict[str, IndexNode] = {}
return
def __contains__(self, tHandle: str) -> bool:
return tHandle in self._items
def __delitem__(self, tHandle: str) -> None:
self._items.pop(tHandle, None)
return
def __getitem__(self, tHandle: str) -> IndexNode | None:
return self._items.get(tHandle, None)
@@ -982,14 +955,12 @@ class ItemIndex:
def clear(self) -> None:
"""Clear the index."""
self._items = {}
return
def add(self, tHandle: str, nwItem: NWItem) -> None:
"""Add a new item to the index. This will overwrite the item if
it already exists.
"""
self._items[tHandle] = IndexNode(self._cache, tHandle, nwItem)
return
def allStoryKeys(self) -> set[str]:
"""Return all story structure keys."""
@@ -1064,7 +1035,6 @@ class ItemIndex:
"""
if tHandle in self._items:
self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC)
return
def setHeadingComment(
self, tHandle: str, sTitle: str,
@@ -1073,25 +1043,21 @@ class ItemIndex:
"""Set a story comment for a heading on a given item."""
if tHandle in self._items:
self._items[tHandle].setHeadingComment(sTitle, comment, key, text)
return
def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str) -> None:
"""Set the main tag for a heading on a given item."""
if tHandle in self._items:
self._items[tHandle].setHeadingTag(sTitle, tagKey)
return
def addHeadingRef(self, tHandle: str, sTitle: str, tagKeys: list[str], refType: str) -> None:
"""Set the reference tags for a heading on a given item."""
if tHandle in self._items:
self._items[tHandle].addHeadingRef(sTitle, tagKeys, refType)
return
def addNoteKey(self, tHandle: str, style: T_NoteTypes, key: str) -> None:
"""Set notes key for a given item."""
if tHandle in self._items:
self._items[tHandle].addNoteKey(style, key)
return
def genNewNoteKey(self, tHandle: str, style: T_NoteTypes) -> str:
"""Set notes key for a given item."""
@@ -1131,5 +1097,3 @@ class ItemIndex:
tItem = IndexNode(self._cache, tHandle, nwItem)
tItem.unpackData(tData)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -50,7 +50,7 @@ NOTE_TYPES: list[T_NoteTypes] = ["footnotes", "comments"]
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).
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._notes: dict[str, set[str]] = {}
self._count = 0
return
def __repr__(self) -> str:
return f"<IndexNode handle='{self._handle}'>"
@@ -107,39 +106,33 @@ class IndexNode:
if TT_NONE in self._headings:
self._headings.pop(TT_NONE)
self._headings[tHeading.key] = tHeading
return
def setHeadingCounts(self, sTitle: str, cCount: int, wCount: int, pCount: int) -> None:
"""Set the character, word and paragraph count of a heading."""
if sTitle in self._headings:
self._headings[sTitle].setCounts([cCount, wCount, pCount])
return
def setHeadingComment(self, sTitle: str, comment: nwComment, key: str, text: str) -> None:
"""Set the comment text of a heading."""
if sTitle in self._headings:
self._headings[sTitle].setComment(comment.name, key, text)
return
def setHeadingTag(self, sTitle: str, tag: str) -> None:
"""Set the tag of a heading."""
if sTitle in self._headings:
self._headings[sTitle].setTag(tag)
return
def addHeadingRef(self, sTitle: str, tags: list[str], keyword: str) -> None:
"""Add a reference key and all its types to a heading."""
if sTitle in self._headings:
for tag in tags:
self._headings[sTitle].addReference(tag, keyword)
return
def addNoteKey(self, style: T_NoteTypes, key: str) -> None:
"""Add a note key to the index."""
if style not in self._notes:
self._notes[style] = set()
self._notes[style].add(key)
return
##
# Data Methods
@@ -195,11 +188,10 @@ class IndexNode:
self._notes[style] = set(keys)
else:
raise KeyError("Index node contains an invalid key")
return
class IndexHeading:
"""Core: Single Index Heading Class
"""Core: Single Index Heading Class.
This object represents a section of text in a project item
associated with a single (valid) heading. It holds a separate record
@@ -224,7 +216,6 @@ class IndexHeading:
self._tag = ""
self._refs: dict[str, set[str]] = {}
self._comments: dict[str, str] = {}
return
def __repr__(self) -> str:
return f"<IndexHeading key='{self._key}'>"
@@ -289,12 +280,10 @@ class IndexHeading:
"""Set the level of the heading if it's a valid value."""
if level in nwStyles.H_VALID:
self._level = level
return
def setLine(self, line: int) -> None:
"""Set the line number of a heading."""
self._line = max(0, checkInt(line, 0))
return
def setCounts(self, counts: Sequence[int]) -> None:
"""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[2], 0)),
)
return
def setComment(self, comment: str, key: str, text: str) -> None:
"""Set the text for a comment and make sure it is a string."""
@@ -319,12 +307,10 @@ class IndexHeading:
case "note" if key:
self._cache.note.add(key)
self._comments[f"note.{key}"] = str(text)
return
def setTag(self, tag: str) -> None:
"""Set the tag for references, and make sure it is a string."""
self._tag = str(tag).lower()
return
def addReference(self, tag: str, keyword: str) -> None:
"""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:
self._refs[tag] = set()
self._refs[tag].add(keyword)
return
##
# Getters
@@ -409,4 +394,3 @@ class IndexHeading:
self.setComment(comment, compact(kind), str(entry))
else:
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class NWItem:
"""Core: Item Data Class
"""Core: Item Data Class.
This class holds all the project information about a project item.
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._charInit = 0 # Initial word count
return
def __repr__(self) -> str:
return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>"
def __bool__(self) -> bool:
"""The truthiness of the class. The handle used to be initiated
to None, but this is no longer the case. It should always
evaluate to True since 2.1-beta1, although unpack and the NWTree
class can leave it as an empty string.
"""Check the truthiness of the class. The handle used to be
initiated to None, but this is no longer the case. It should
always evaluate to True since 2.1-beta1, although unpack and the
NWTree class can leave it as an empty string.
"""
return bool(self._handle)
@@ -206,15 +204,13 @@ class NWItem:
meta["cursorPos"] = str(self._cursorPos)
name["active"] = yesNo(self._active)
data = {
return {
"name": str(self._name),
"itemAttr": item,
"metaAttr": meta,
"nameAttr": name,
}
return data
def unpack(self, data: dict) -> bool:
"""Set the values from a data dictionary."""
item = data.get("itemAttr", {})
@@ -298,13 +294,11 @@ class NWItem:
def notifyToRefresh(self) -> None:
"""Notify GUI that item info needs to be refreshed."""
self._project.tree.refreshItems([self._handle])
return
def notifyNovelStructureChange(self) -> None:
"""Notify that the structure of a novel has changed."""
if self._root and self._class == nwItemClass.NOVEL:
self._project.tree.novelStructureChanged(self._root)
return
##
# Lookup Methods
@@ -457,8 +451,6 @@ class NWItem:
if self._import is None:
self.setImport("New") # This forces a default value lookup
return
##
# Set Item Values
##
@@ -469,7 +461,6 @@ class NWItem:
self._name = simplified(name)
else:
self._name = ""
return
def setParent(self, handle: Any) -> None:
"""Set the parent handle, and ensure it is valid."""
@@ -479,7 +470,6 @@ class NWItem:
self._parent = handle
else:
self._parent = None
return
def setRoot(self, handle: Any) -> None:
"""Set the root handle, and ensure it is valid."""
@@ -489,7 +479,6 @@ class NWItem:
self._root = handle
else:
self._root = None
return
def setOrder(self, order: Any) -> None:
"""Set the item order, and ensure that it is valid. This value
@@ -497,7 +486,6 @@ class NWItem:
the moment.
"""
self._order = checkInt(order, 0)
return
def setType(self, value: Any) -> None:
"""Set the item type from either a proper nwItemType, or set it
@@ -510,7 +498,6 @@ class NWItem:
else:
logger.error("Unrecognised item type '%s'", value)
self._type = nwItemType.NO_TYPE
return
def setClass(self, value: Any) -> None:
"""Set the item class from either a proper nwItemClass, or set
@@ -523,7 +510,6 @@ class NWItem:
else:
logger.error("Unrecognised item class '%s'", value)
self._class = nwItemClass.NO_CLASS
return
def setLayout(self, value: Any) -> None:
"""Set the item layout from either a proper nwItemLayout, or set
@@ -536,21 +522,18 @@ class NWItem:
else:
logger.error("Unrecognised item layout '%s'", value)
self._layout = nwItemLayout.NO_LAYOUT
return
def setStatus(self, value: Any) -> None:
"""Set the item status by looking it up in the valid status
items of the current project.
"""
self._status = self._project.data.itemStatus.check(value)
return
def setImport(self, value: Any) -> None:
"""Set the item importance by looking it up in the valid import
items of the current project.
"""
self._import = self._project.data.itemImport.check(value)
return
def setActive(self, state: Any) -> None:
"""Set the active flag."""
@@ -558,7 +541,6 @@ class NWItem:
self._active = state
else:
self._active = False
return
def setExpanded(self, state: Any) -> None:
"""Set the expanded status of an item in the project tree."""
@@ -566,7 +548,6 @@ class NWItem:
self._expanded = state
else:
self._expanded = False
return
##
# Set Document Meta Data
@@ -576,7 +557,6 @@ class NWItem:
"""Set the main heading level."""
if value in nwStyles.H_LEVEL:
self._heading = value
return
def setCharCount(self, count: Any) -> None:
"""Set the character count, and ensure that it is an integer."""
@@ -584,7 +564,6 @@ class NWItem:
self._charCount = max(0, count)
else:
self._charCount = 0
return
def setWordCount(self, count: Any) -> None:
"""Set the word count, and ensure that it is an integer."""
@@ -592,7 +571,6 @@ class NWItem:
self._wordCount = max(0, count)
else:
self._wordCount = 0
return
def setParaCount(self, count: Any) -> None:
"""Set the paragraph count, and ensure that it is an integer."""
@@ -600,7 +578,6 @@ class NWItem:
self._paraCount = max(0, count)
else:
self._paraCount = 0
return
def setCursorPos(self, position: Any) -> None:
"""Set the cursor position, and ensure that it is an integer."""
@@ -608,4 +585,3 @@ class NWItem:
self._cursorPos = max(0, position)
else:
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -66,7 +66,7 @@ T_NodeData = str | QIcon | QFont | Qt.AlignmentFlag | None
class ProjectNode:
"""Core: Project Model Node Class
"""Core: Project Model Node Class.
The project tree structure is saved as nodes in a tree, starting
from a root node. This class makes up these nodes.
@@ -103,7 +103,6 @@ class ProjectNode:
self._count = 0
self.refresh()
self.updateCount()
return
def __repr__(self) -> str:
return (
@@ -114,7 +113,7 @@ class ProjectNode:
)
def __bool__(self) -> bool:
"""A node should always evaluate to True."""
# A node should always evaluate to True.
return True
##
@@ -162,15 +161,12 @@ class ProjectNode:
self._cache[C_STATUS_TIP] = sText
self._cache[C_STATUS_ACCESS] = sText
return
def updateCount(self, propagate: bool = True) -> None:
"""Update counts, and propagate upwards in the tree."""
self._count = self._item.mainCount + sum(c._count for c in self._children) # noqa: SLF001
self._cache[C_COUNT_TEXT] = f"{self._count:n}"
if propagate and (parent := self._parent):
parent.updateCount()
return
##
# Data Access
@@ -223,7 +219,6 @@ class ProjectNode:
self._children.append(child)
self._refreshChildrenPos()
self._item.notifyNovelStructureChange()
return
def takeChild(self, pos: int) -> ProjectNode | None:
"""Remove a child item and return it."""
@@ -243,7 +238,6 @@ class ProjectNode:
self._children.insert(target, node)
self._refreshChildrenPos()
self._item.notifyNovelStructureChange()
return
def setExpanded(self, state: bool) -> None:
"""Set the node's expanded state."""
@@ -251,7 +245,6 @@ class ProjectNode:
self._item.setExpanded(True)
else:
self._item.setExpanded(False)
return
##
# Internal Functions
@@ -262,14 +255,12 @@ class ProjectNode:
for node in self._children:
children.append(node)
node._recursiveAppendChildren(children) # noqa: SLF001
return
def _refreshChildrenPos(self) -> None:
"""Update the row value on all children."""
for n, child in enumerate(self._children):
child._row = n # noqa: SLF001
child.item.setOrder(n)
return
def _updateRelationships(self, child: ProjectNode) -> None:
"""Update a child item's relationships."""
@@ -282,11 +273,10 @@ class ProjectNode:
child.item.setParent(None)
child.item.setRoot(child.item.itemHandle)
child.item.setClassDefaults(child.item.itemClass)
return
class ProjectModel(QAbstractItemModel):
"""Core: Project Model Class
"""Core: Project Model Class.
This class provides the interface for the tree widget used on the
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.item.setName("Invisible Root")
logger.debug("Ready: ProjectModel")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: ProjectModel")
return
##
# Properties
@@ -363,7 +351,7 @@ class ProjectModel(QAbstractItemModel):
##
def supportedDropActions(self) -> Qt.DropAction:
"""Return supported drop actions"""
"""Return supported drop actions."""
return Qt.DropAction.MoveAction
def mimeTypes(self) -> list[str]:
@@ -445,7 +433,6 @@ class ProjectModel(QAbstractItemModel):
self.beginInsertRows(parent, row, row)
node.addChild(child, row)
self.endInsertRows()
return
def removeChild(self, parent: QModelIndex, pos: int) -> ProjectNode | None:
"""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)
parent.moveChild(pos, new)
self.endMoveRows()
return
def multiMove(self, indices: list[QModelIndex], target: QModelIndex, pos: int = -1) -> None:
"""Move multiple items to a new location."""
@@ -497,7 +483,6 @@ class ProjectModel(QAbstractItemModel):
node._updateRelationships(child) # noqa: SLF001
child.item.notifyToRefresh()
node.item.notifyToRefresh()
return
##
# Other Methods
@@ -506,7 +491,6 @@ class ProjectModel(QAbstractItemModel):
def clear(self) -> None:
"""Clear the project model."""
self._root.children.clear()
return
def allExpanded(self) -> list[QModelIndex]:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -54,6 +54,7 @@ T_NodeData = str | QIcon | QPixmap | Qt.AlignmentFlag | None
class NovelModel(QAbstractTableModel):
"""Core: Novel Model CLass."""
__slots__ = ("_columns", "_extraKey", "_extraLabel", "_more", "_rows")
@@ -64,11 +65,9 @@ class NovelModel(QAbstractTableModel):
self._columns = 3
self._extraKey = ""
self._extraLabel = ""
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NovelModel")
return
##
# Properties
@@ -102,7 +101,6 @@ class NovelModel(QAbstractTableModel):
self._columns = 4
self._extraKey = nwKeyWords.PLOT_KEY
self._extraLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
return
##
# Model Interface
@@ -147,7 +145,6 @@ class NovelModel(QAbstractTableModel):
def clear(self) -> None:
"""Clear the model."""
self._rows.clear()
return
def append(self, node: IndexNode) -> None:
"""Append a node to the model."""
@@ -155,7 +152,6 @@ class NovelModel(QAbstractTableModel):
for key, head in node.items():
if key != "T0000":
self._rows.append(self._generateEntry(handle, key, head))
return
def refresh(self, node: IndexNode) -> bool:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -80,7 +80,7 @@ VALID_MAP: dict[str, set[str]] = {
class OptionState:
"""Core: GUI Options Storage
"""Core: GUI Options Storage.
A class for storing the state of the GUI. The data is stored per
project. Settings that should be project-independent are stored in
@@ -90,7 +90,6 @@ class OptionState:
def __init__(self, project: NWProject) -> None:
self._project = project
self._state = {}
return
##
# 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -57,6 +57,7 @@ logger = logging.getLogger(__name__)
class NWProjectState(Enum):
"""The state of the loaded project."""
UNKNOWN = 0
LOCKED = 1
@@ -65,6 +66,11 @@ class NWProjectState(Enum):
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__ = (
"_changed", "_data", "_index", "_langData", "_options", "_session",
@@ -92,17 +98,13 @@ class NWProject:
logger.debug("Ready: NWProject")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWProject")
return
def clear(self) -> None:
"""Clear the project."""
self._tree.clear()
self._index.clear()
return
##
# Properties
@@ -263,7 +265,6 @@ class NWProject:
if rHandle and (tHandle := SHARED.project.newFile(tag.title(), rHandle)):
self.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
self._tree.refreshItems([tHandle])
return
##
# Project Methods
@@ -441,7 +442,6 @@ class NWProject:
self._tree.writeToCFile()
self._session.appendSession(idleTime)
self._storage.closeSession()
return
def backupProject(self, doNotify: bool) -> bool:
"""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("Major"), "purple", "BLOCK_3", 0)
self._data.itemImport.add(None, self.tr("Main"), "purple", "BLOCK_4", 0)
return
def setProjectLang(self, language: str | None) -> None:
"""Set the project-specific language."""
@@ -508,7 +507,6 @@ class NWProject:
self._data.setLanguage(language)
self._loadProjectLocalisation()
self.setProjectChanged(True)
return
def setProjectChanged(self, status: bool) -> bool:
"""Toggle the project changed flag, and propagate the
@@ -527,7 +525,6 @@ class NWProject:
"""Update the total word and character count values."""
wNovel, wNotes, cNovel, cNotes = self._tree.sumCounts()
self._data.setCurrCounts(wNovel=wNovel, wNotes=wNotes, cNovel=cNovel, cNotes=cNotes)
return
def countStatus(self) -> None:
"""Count how many times the various status flags are used in the
@@ -541,7 +538,6 @@ class NWProject:
self._data.itemStatus.increment(nwItem.itemStatus)
else:
self._data.itemImport.increment(nwItem.itemImport)
return
def updateStatus(self, kind: T_StatusKind, update: T_UpdateEntry) -> None:
"""Update status or import entries."""
@@ -553,13 +549,11 @@ class NWProject:
self._data.itemImport.update(update)
SHARED.emitStatusLabelsChanged(self, kind)
self._tree.refreshAllItems()
return
def updateTheme(self) -> None:
"""Update theme elements."""
self._data.itemStatus.refreshIcons()
self._data.itemImport.refreshIcons()
return
def localLookup(self, word: str | int) -> str:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
class NWProjectData:
"""Core: Project Data Class
"""Core: Project Data Class.
The class holds all project data from the main XML file, aside from
the list of project items.
@@ -86,8 +86,6 @@ class NWProjectData:
self._status = NWStatus(NWStatus.STATUS)
self._import = NWStatus(NWStatus.IMPORT)
return
##
# Properties
##
@@ -191,13 +189,11 @@ class NWProjectData:
"""Increment the save count by one."""
self._saveCount += 1
self._project.setProjectChanged(True)
return
def incAutoCount(self) -> None:
"""Increment the auto save count by one."""
self._autoCount += 1
self._project.setProjectChanged(True)
return
##
# Getters
@@ -219,67 +215,57 @@ class NWProjectData:
elif value != self._uuid:
self._uuid = value
self._project.setProjectChanged(True)
return
def setName(self, value: str | None) -> None:
"""Set a new project name."""
if value != self._name:
self._name = simplified(str(value or ""))
self._project.setProjectChanged(True)
return
def setAuthor(self, value: str | None) -> None:
"""Set the author value."""
if value != self._author:
self._author = simplified(str(value or ""))
self._project.setProjectChanged(True)
return
def setSaveCount(self, value: Any) -> None:
"""Set the save count from last session."""
self._saveCount = checkInt(value, 0)
self._project.setProjectChanged(True)
return
def setAutoCount(self, value: Any) -> None:
"""Set the auto save count from last session."""
self._autoCount = checkInt(value, 0)
self._project.setProjectChanged(True)
return
def setEditTime(self, value: Any) -> None:
"""Set the edit time from last session."""
self._editTime = checkInt(value, 0)
self._project.setProjectChanged(True)
return
def setDoBackup(self, value: Any) -> None:
"""Set the do write backup flag."""
if value != self._doBackup:
self._doBackup = checkBool(value, False)
self._project.setProjectChanged(True)
return
def setLanguage(self, value: str | None) -> None:
"""Set the project language."""
if value != self._language:
self._language = checkStringNone(value, None)
self._project.setProjectChanged(True)
return
def setSpellCheck(self, value: Any) -> None:
"""Set the spell check flag."""
if value != self._spellCheck:
self._spellCheck = checkBool(value, False)
self._project.setProjectChanged(True)
return
def setSpellLang(self, value: str | None) -> None:
"""Set the spell check language."""
if value != self._spellLang:
self._spellLang = checkStringNone(value, None)
self._project.setProjectChanged(True)
return
def setLastHandle(self, value: str | None, component: str) -> None:
"""Set a last used handle into the handle registry for a given
@@ -288,7 +274,6 @@ class NWProjectData:
if isinstance(component, str):
self._lastHandle[component] = checkStringNone(value, None)
self._project.setProjectChanged(True)
return
def setLastHandles(self, value: dict) -> None:
"""Set the full last handles dictionary to a new set of values.
@@ -299,7 +284,6 @@ class NWProjectData:
if key in self._lastHandle:
self._lastHandle[key] = str(entry) if isHandle(entry) else None
self._project.setProjectChanged(True)
return
def setInitCounts(
self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None
@@ -321,7 +305,6 @@ class NWProjectData:
count = checkInt(cNotes, 0)
self._initCounts[3] = count
self._currCounts[3] = count
return
def setCurrCounts(
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)
if cNotes is not None:
self._currCounts[3] = checkInt(cNotes, 0)
return
def setAutoReplace(self, value: dict) -> None:
"""Set the auto-replace dictionary."""
@@ -345,4 +327,3 @@ class NWProjectData:
if isinstance(entry, str):
self._autoReplace[key] = simplified(entry)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -72,7 +72,7 @@ class XMLReadState(Enum):
class ProjectXMLReader:
"""Core: Project XML Reader
"""Core: Project XML Reader.
All data is read into a NWProjectData instance, which must be
provided.
@@ -124,7 +124,6 @@ class ProjectXMLReader:
self._appVersion = ""
self._hexVersion = 0x0
self._timeStamp = ""
return
##
# Properties
@@ -254,8 +253,6 @@ class ProjectXMLReader:
elif xItem.tag == "editTime": # Moved to attribute in 1.5
data.setEditTime(xItem.text)
return
def _parseProjectSettings(self, xSection: ET.Element, data: NWProjectData) -> None:
"""Parse the settings section of the XML file."""
logger.debug("Parsing <settings> section")
@@ -294,8 +291,6 @@ class ProjectXMLReader:
elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
data.setInitCounts(wNotes=xItem.text)
return
def _parseProjectContent(
self, xSection: ET.Element, data: NWProjectData, content: list
) -> None:
@@ -356,8 +351,6 @@ class ProjectXMLReader:
"nameAttr": name,
})
return
def _parseProjectContentLegacy(
self, xSection: ET.Element, data: NWProjectData, content: list
) -> None:
@@ -434,8 +427,6 @@ class ProjectXMLReader:
"nameAttr": name,
})
return
def _parseStatusImport(self, xItem: ET.Element, sObject: NWStatus) -> None:
"""Parse a status or importance entry."""
for xEntry in xItem:
@@ -450,7 +441,6 @@ class ProjectXMLReader:
if color is None:
color = f"{red}, {green}, {blue}"
sObject.add(key, xEntry.text or "", color, shape, count)
return
def _parseDictKeyText(self, xItem: ET.Element) -> dict:
"""Parse a dictionary stored with key as an attribute and the
@@ -470,7 +460,7 @@ class ProjectXMLReader:
class ProjectXMLWriter:
"""Core: Project XML Writer
"""Core: Project XML Writer.
The project writer class will only write a file according to the
very latest spec.
@@ -479,7 +469,6 @@ class ProjectXMLWriter:
def __init__(self, path: str | Path) -> None:
self._path = Path(path)
self._error = None
return
##
# Properties
@@ -580,7 +569,6 @@ class ProjectXMLWriter:
"""Pack a single value into an XML element."""
xItem = ET.SubElement(xParent, name, attrib=attrib or {})
xItem.text = str(value) or ""
return
def _packDictKeyValue(self, xParent: ET.Element, name: str, data: dict) -> None:
"""Pack the entries of a dictionary into an XML element."""
@@ -589,4 +577,3 @@ class ProjectXMLWriter:
if len(key) > 0:
xEntry = ET.SubElement(xItem, "entry", attrib={"key": key})
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -43,7 +43,7 @@ logger = logging.getLogger(__name__)
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
format. That is, one JSON object per line.
@@ -52,7 +52,6 @@ class NWSessionLog:
def __init__(self, project: NWProject) -> None:
self._project = project
self._start = 0.0
return
##
# Properties
@@ -70,7 +69,6 @@ class NWSessionLog:
def startSession(self) -> None:
"""Start the writing session."""
self._start = time()
return
def appendSession(self, idleTime: float) -> bool:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class NWSpellEnchant:
"""Core: Enchant Spell Checking Wrapper
"""Core: Enchant Spell Checking Wrapper.
This is a rapper class for Enchant to keep the API consistent
between spell check tools.
@@ -57,11 +57,9 @@ class NWSpellEnchant:
self._language = None
self._broker = None
logger.debug("Ready: NWSpellEnchant")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWSpellEnchant")
return
##
# Properties
@@ -106,21 +104,19 @@ class NWSpellEnchant:
for word in self._userDict:
self._enchant.add_to_session(word)
return
##
# Methods
##
def checkWord(self, word: str) -> bool:
"""Wrapper function for pyenchant."""
"""Forward check to pyenchant."""
try:
return bool(self._enchant.check(word))
except Exception:
return True
def suggestWords(self, word: str) -> list[str]:
"""Wrapper function for pyenchant."""
"""Ask pyenchant for suggestions."""
try:
return self._enchant.suggest(word)
except Exception:
@@ -172,24 +168,29 @@ class FakeEnchant:
self.tag = ""
self.provider = FakeProvider()
return
def check(self, word: str) -> bool:
"""Return True for all words."""
return True
def suggest(self, word: str) -> list[str]:
"""Return an empty suggestion list."""
return []
def add_to_session(self, word: str) -> None:
"""Do nothing."""
return
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:
self._project = project
self._words = set()
return
def __contains__(self, word: str) -> bool:
return word in self._words
@@ -219,7 +220,6 @@ class UserDictionary:
except Exception:
logger.error("Failed to load user dictionary")
logException()
return
def save(self) -> None:
"""Save the user's dictionary."""
@@ -232,4 +232,3 @@ class UserDictionary:
except Exception:
logger.error("Failed to save user dictionary")
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import dataclasses
@@ -49,6 +49,7 @@ CUSTOM_COL = "custom"
@dataclasses.dataclass
class StatusEntry:
"""DataClass: Status Label Values."""
name: str
color: QColor
@@ -73,6 +74,7 @@ T_StatusKind = Literal["s", "i"]
class NWStatus:
"""Core: Status/Importance Label Class."""
STATUS = "s"
IMPORT = "i"
@@ -84,7 +86,6 @@ class NWStatus:
self._default = None
self._prefix = prefix[:1]
self._height = SHARED.theme.baseIconHeight
return
def __len__(self) -> int:
return len(self._store)
@@ -133,8 +134,6 @@ class NWStatus:
if self._default not in self._store:
self._default = next(iter(self._store)) if self._store else None
return
def check(self, value: str) -> str:
"""Check the key against the stored status names."""
if self._isKey(value) and value in self._store:
@@ -147,13 +146,11 @@ class NWStatus:
"""Clear the counts of references to the status entries."""
for entry in self._store.values():
entry.count = 0
return
def increment(self, key: str | None) -> None:
"""Increment the counter for a given entry."""
if key and key in self._store:
self._store[key].count += 1
return
def pack(self) -> Iterable[tuple[str, dict]]:
"""Pack the status entries into a dictionary."""
@@ -195,7 +192,6 @@ class NWStatus:
if entry.theme != CUSTOM_COL:
entry.color = SHARED.theme.parseColor(entry.theme)
entry.icon = NWStatus.createIcon(self._height, entry.color, entry.shape)
return
@staticmethod
def createIcon(height: int, color: QColor, shape: nwStatusShape) -> QIcon:
@@ -252,7 +248,6 @@ class _ShapeCache:
def __init__(self) -> None:
self._cache: dict[nwStatusShape, QPainterPath] = {}
return
def getShape(self, shape: nwStatusShape) -> QPainterPath:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -47,6 +47,7 @@ logger = logging.getLogger(__name__)
class NWStorageOpen(Enum):
"""The status of a storage location."""
UNKOWN = 0
NOT_FOUND = 1
@@ -56,6 +57,7 @@ class NWStorageOpen(Enum):
class NWStorageCreate(Enum):
"""The status of a new storage location."""
NOT_EMPTY = 0
OS_ERROR = 1
@@ -63,7 +65,7 @@ class NWStorageCreate(Enum):
class NWStorage:
"""Core: Project Storage Class
"""Core: Project Storage Class.
The class that handles all paths related to the project storage.
"""
@@ -81,7 +83,6 @@ class NWStorage:
self._openMode = self.MODE_INACTIVE
self._ready = False
self._exception = None
return
def clear(self) -> None:
"""Reset internal variables."""
@@ -90,7 +91,6 @@ class NWStorage:
self._lockFilePath = None
self._openMode = self.MODE_INACTIVE
self._ready = False
return
##
# Properties
@@ -252,13 +252,11 @@ class NWStorage:
"""Lock the session when the project is successfully opened."""
if self._ready:
self._writeLockFile()
return
def closeSession(self) -> None:
"""Run tasks related to closing the session."""
self._clearLockFile()
self.clear()
return
##
# Content Access Methods
@@ -394,7 +392,7 @@ class NWStorage:
class _LegacyStorage:
"""Core: Legacy Storage Converter Utils
"""Core: Legacy Storage Converter Utils.
A class with various functions to convert old file formats and
file/folder layouts to the current project format.
@@ -402,7 +400,6 @@ class _LegacyStorage:
def __init__(self, project: NWProject) -> None:
self._project = project
return
def legacyDataFolder(self, path: Path, child: Path) -> None:
"""Handle the content of a legacy data folder from a version 1.0
@@ -484,8 +481,6 @@ class _LegacyStorage:
except Exception as exc:
logger.warning("Failed to delete: %s", item, exc_info=exc)
return
##
# 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -50,7 +50,7 @@ MAX_DEPTH = 999 # Cap of tree traversing for loops (recursion limit)
class NWTree:
"""Core: Project Tree Data Class
"""Core: Project Tree Data 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
@@ -71,18 +71,16 @@ class NWTree:
self._trash = None
self._ready = False
logger.debug("Ready: NWTree")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWTree")
return
def __len__(self) -> int:
"""The number of items in the project."""
"""Return the number of items in the project."""
return len(self._items)
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)
def __getitem__(self, tHandle: str | None) -> NWItem | None:
@@ -95,7 +93,7 @@ class NWTree:
return None
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
def __iter__(self) -> Iterator[NWItem]:
@@ -142,7 +140,6 @@ class NWTree:
self._trash = None
oldModel.deleteLater()
del oldModel
return
def add(self, item: NWItem, pos: int = -1) -> bool:
"""Add a project item into the project tree."""
@@ -260,8 +257,6 @@ class NWTree:
self._model.endInsertRows()
self._model.layoutChanged.emit()
return
def pickParent(self, sNode: ProjectNode, hLevel: int, isNote: bool) -> tuple[str | None, int]:
"""Pick an appropriate parent handle for adding a new item."""
if sNode.item.isFolderType() or sNode.item.isRootType():
@@ -299,7 +294,6 @@ class NWTree:
indexE = self._model.indexFromNode(node, 3)
self._model.dataChanged.emit(indexS, indexE)
self._itemChange(node.item, nwChange.UPDATE)
return
def refreshAllItems(self) -> None:
"""Refresh all items in the tree."""
@@ -309,13 +303,11 @@ class NWTree:
self._model.root.refresh()
self._model.root.updateCount(propagate=False)
self._model.layoutChanged.emit()
return
def novelStructureChanged(self, tHandle: str) -> None:
"""Emit a novel structure change signal."""
if self._ready:
SHARED.novelStructureChanged.emit(tHandle)
return
def checkConsistency(self, prefix: str) -> tuple[int, int]:
"""Check the project tree consistency. Also check the content
@@ -496,7 +488,6 @@ class NWTree:
SHARED.emitProjectItemChanged(self._project, tHandle, change)
if item.isRootType():
SHARED.emitRootFolderChanged(self._project, tHandle, change)
return
def _getTrashNode(self) -> ProjectNode | None:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -45,6 +45,7 @@ logger = logging.getLogger(__name__)
class GuiAbout(NDialog):
"""GUI: About novelWriter Dialog."""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
@@ -109,11 +110,8 @@ class GuiAbout(NDialog):
logger.debug("Ready: GuiAbout")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiAbout")
return
##
# Events
@@ -123,7 +121,6 @@ class GuiAbout(NDialog):
"""Capture the close event and perform cleanup."""
event.accept()
self.softDelete()
return
##
# Internal Functions
@@ -135,7 +132,6 @@ class GuiAbout(NDialog):
self.txtCredits.setHtml(html)
else:
self.txtCredits.setHtml("Error loading credits text ...")
return
def _setStyleSheet(self) -> None:
"""Set stylesheet text document."""
@@ -143,4 +139,3 @@ class GuiAbout(NDialog):
self.txtCredits.setStyleSheet(
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
class GuiDocMerge(NDialog):
"""GUI: Document Merge Tool."""
D_HANDLE = QtUserRole
@@ -110,11 +111,8 @@ class GuiDocMerge(NDialog):
logger.debug("Ready: GuiDocMerge")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiDocMerge")
return
def data(self) -> dict:
"""Return the user's choices."""
@@ -150,7 +148,6 @@ class GuiDocMerge(NDialog):
if sHandle := self._data.get("sHandle"):
itemList = self._data.get("origItems", [])
self._loadContent(sHandle, itemList)
return
##
# Internal Functions
@@ -170,4 +167,3 @@ class GuiDocMerge(NDialog):
item.setData(self.D_HANDLE, tHandle)
item.setCheckState(Qt.CheckState.Checked)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
class GuiDocSplit(NDialog):
"""GUI: Document Split Tool."""
LINE_ROLE = QtUserRole
LEVEL_ROLE = QtUserRole + 1
@@ -139,11 +140,8 @@ class GuiDocSplit(NDialog):
logger.debug("Ready: GuiDocSplit")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiDocSplit")
return
def data(self) -> tuple[dict, list[str]]:
"""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."""
if sHandle := self._data.get("sHandle"):
self._loadContent(sHandle)
return
##
# 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -34,6 +34,7 @@ logger = logging.getLogger(__name__)
class GuiEditLabel(NDialog):
"""GUI: Edit Item Label Dialog."""
def __init__(self, parent: QWidget, text: str = "") -> None:
super().__init__(parent=parent)
@@ -72,11 +73,8 @@ class GuiEditLabel(NDialog):
logger.debug("Ready: GuiEditLabel")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiEditLabel")
return
@property
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
class GuiPreferences(NDialog):
"""GUI: Preferences Dialog."""
newPreferencesReady = pyqtSignal(bool, bool, bool, bool)
@@ -125,11 +126,8 @@ class GuiPreferences(NDialog):
logger.debug("Ready: GuiPreferences")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiPreferences")
return
def buildForm(self) -> None:
"""Build the settings form."""
@@ -856,8 +854,6 @@ class GuiPreferences(NDialog):
self.mainForm.finalise()
self.sidebar.setSelected(1)
return
##
# Events
##
@@ -868,7 +864,6 @@ class GuiPreferences(NDialog):
self._saveWindowSize()
event.accept()
self.softDelete()
return
def keyPressEvent(self, event: QKeyEvent) -> None:
"""Overload keyPressEvent and only accept escape. The main
@@ -878,7 +873,6 @@ class GuiPreferences(NDialog):
if event.matches(QKeySequence.StandardKey.Cancel):
self.close()
event.ignore()
return
##
# Private Slots
@@ -888,13 +882,11 @@ class GuiPreferences(NDialog):
def _sidebarClicked(self, section: int) -> None:
"""Process a user request to switch page."""
self.mainForm.scrollToSection(section)
return
@pyqtSlot()
def _gotoSearch(self) -> None:
"""Go to the setting indicated by the search text."""
self.mainForm.scrollToLabel(self.searchText.text().strip())
return
@pyqtSlot()
def _selectGuiFont(self) -> None:
@@ -904,7 +896,6 @@ class GuiPreferences(NDialog):
self.guiFont.setText(describeFont(font))
self.guiFont.setCursorPosition(0)
self._guiFont = font
return
@pyqtSlot()
def _selectTextFont(self) -> None:
@@ -914,7 +905,6 @@ class GuiPreferences(NDialog):
self.textFont.setText(describeFont(font))
self.textFont.setCursorPosition(0)
self._textFont = font
return
@pyqtSlot()
def _backupFolder(self) -> None:
@@ -925,13 +915,11 @@ class GuiPreferences(NDialog):
):
self.backupPath = path
self.mainForm.setHelpText("backupPath", self.tr("Path: {0}").format(path))
return
@pyqtSlot(bool)
def _toggledBackupOnClose(self, state: bool) -> None:
"""Toggle switch that depends on the backup on close switch."""
self.askBeforeBackup.setEnabled(state)
return
@pyqtSlot(str)
def _insertDialogLineSymbol(self, symbol: str) -> None:
@@ -939,7 +927,6 @@ class GuiPreferences(NDialog):
current = self.dialogLine.text()
values = processDialogSymbols(f"{current} {symbol}")
self.dialogLine.setText(" ".join(values))
return
@pyqtSlot(bool)
def _toggleAutoReplaceMain(self, state: bool) -> None:
@@ -949,7 +936,6 @@ class GuiPreferences(NDialog):
self.doReplaceDash.setEnabled(state)
self.doReplaceDots.setEnabled(state)
self.fmtPadThin.setEnabled(state)
return
@pyqtSlot()
def _changeSingleQuoteOpen(self) -> None:
@@ -957,7 +943,6 @@ class GuiPreferences(NDialog):
quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtSQuoteOpen.text())
if status:
self.fmtSQuoteOpen.setText(quote)
return
@pyqtSlot()
def _changeSingleQuoteClose(self) -> None:
@@ -965,7 +950,6 @@ class GuiPreferences(NDialog):
quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtSQuoteClose.text())
if status:
self.fmtSQuoteClose.setText(quote)
return
@pyqtSlot()
def _changeDoubleQuoteOpen(self) -> None:
@@ -973,7 +957,6 @@ class GuiPreferences(NDialog):
quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtDQuoteOpen.text())
if status:
self.fmtDQuoteOpen.setText(quote)
return
@pyqtSlot()
def _changeDoubleQuoteClose(self) -> None:
@@ -981,7 +964,6 @@ class GuiPreferences(NDialog):
quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtDQuoteClose.text())
if status:
self.fmtDQuoteClose.setText(quote)
return
##
# Internal Functions
@@ -990,7 +972,6 @@ class GuiPreferences(NDialog):
def _saveWindowSize(self) -> None:
"""Save the dialog window size."""
CONFIG.setPreferencesWinSize(self.width(), self.height())
return
def _doSave(self) -> None:
"""Save the values set in the form."""
@@ -1134,5 +1115,3 @@ class GuiPreferences(NDialog):
self.newPreferencesReady.emit(needsRestart, refreshTree, updateTheme, updateSyntax)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import csv
@@ -55,6 +55,7 @@ logger = logging.getLogger(__name__)
class GuiProjectSettings(NDialog):
"""GUI: Project Settings DIalog."""
PAGE_SETTINGS = 0
PAGE_STATUS = 1
@@ -137,11 +138,8 @@ class GuiProjectSettings(NDialog):
logger.debug("Ready: GuiProjectSettings")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiProjectSettings")
return
##
# Events
@@ -152,7 +150,6 @@ class GuiProjectSettings(NDialog):
self._saveSettings()
event.accept()
self.softDelete()
return
##
# Private Slots
@@ -169,7 +166,6 @@ class GuiProjectSettings(NDialog):
self.mainStack.setCurrentWidget(self.importPage)
elif pageId == self.PAGE_REPLACE:
self.mainStack.setCurrentWidget(self.replacePage)
return
@pyqtSlot()
def _doSave(self) -> None:
@@ -203,8 +199,6 @@ class GuiProjectSettings(NDialog):
QApplication.processEvents()
self.close()
return
##
# Internal Functions
##
@@ -223,8 +217,6 @@ class GuiProjectSettings(NDialog):
options.setValue("GuiProjectSettings", "importColW", importColW)
options.setValue("GuiProjectSettings", "replaceColW", replaceColW)
return
class _SettingsPage(NScrollableForm):
@@ -295,8 +287,6 @@ class _SettingsPage(NScrollableForm):
self.finalise()
return
class _StatusPage(NFixedPage):
@@ -478,8 +468,6 @@ class _StatusPage(NFixedPage):
self.setCentralLayout(self.outerBox)
self._setButtonIcons()
return
@property
def changed(self) -> bool:
"""The user changed these settings."""
@@ -518,7 +506,6 @@ class _StatusPage(NFixedPage):
entry.name = name
item.setText(self.C_LABEL, name)
self._changed = True
return
@pyqtSlot(int)
def _onThemeSelect(self, index: int) -> None:
@@ -526,7 +513,6 @@ class _StatusPage(NFixedPage):
self._theme = str(self.iconColor.currentData())
self._setButtonIcons()
self._updateIcon()
return
@pyqtSlot()
def _onColorSelect(self) -> None:
@@ -536,7 +522,6 @@ class _StatusPage(NFixedPage):
self._theme = CUSTOM_COL
self._setButtonIcons()
self._updateIcon()
return
@pyqtSlot()
def _onItemCreate(self) -> None:
@@ -547,7 +532,6 @@ class _StatusPage(NFixedPage):
theme = str(self.iconColor.currentData())
self._addItem(None, StatusEntry(self.tr("New Item"), color, theme, shape, icon, 0))
self._changed = True
return
@pyqtSlot()
def _onItemDelete(self) -> None:
@@ -560,7 +544,6 @@ class _StatusPage(NFixedPage):
else:
self.listBox.takeTopLevelItem(iRow)
self._changed = True
return
@pyqtSlot()
def _onSelectionChanged(self) -> None:
@@ -593,7 +576,6 @@ class _StatusPage(NFixedPage):
self.iconColor.setEnabled(False)
self.colorButton.setEnabled(False)
self.shapeButton.setEnabled(False)
return
@pyqtSlot()
def _importLabels(self) -> None:
@@ -630,7 +612,6 @@ class _StatusPage(NFixedPage):
writer.writerow([entry.shape.name, entry.color.name(), entry.name])
except Exception as exc:
SHARED.error("Could not write file.", exc=exc)
return
##
# Internal Functions
@@ -641,7 +622,6 @@ class _StatusPage(NFixedPage):
self._shape = shape
self._setButtonIcons()
self._updateIcon()
return
def _updateIcon(self) -> None:
"""Apply changes made to a status icon."""
@@ -654,7 +634,6 @@ class _StatusPage(NFixedPage):
entry.icon = icon
item.setIcon(self.C_LABEL, icon)
self._changed = True
return
def _addItem(self, key: str | None, entry: StatusEntry) -> None:
"""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_ENTRY, entry)
self.listBox.addTopLevelItem(item)
return
def _moveItem(self, step: int) -> None:
"""Move and item up or down step."""
@@ -678,7 +656,6 @@ class _StatusPage(NFixedPage):
self.listBox.clearSelection()
cItem.setSelected(True)
self._changed = True
return
def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Get the currently selected item."""
@@ -701,7 +678,6 @@ class _StatusPage(NFixedPage):
self.iconColor.setCurrentData(self._theme, CUSTOM_COL)
self.colorButton.setIcon(icon)
self.shapeButton.setIcon(self._icons[self._shape])
return
def _pickColor(self) -> QColor:
"""Get the correct colour value based on selections."""
@@ -789,8 +765,6 @@ class _ReplacePage(NFixedPage):
self.setCentralLayout(self.outerBox)
return
@property
def changed(self) -> bool:
"""The user changed these settings."""
@@ -823,7 +797,6 @@ class _ReplacePage(NFixedPage):
if (item := self._getSelectedItem()) and (key := self._stripKey(text)):
item.setText(self.C_KEY, f"<{key}>")
self._changed = True
return
@pyqtSlot(str)
def _onValueEdit(self, text: str) -> None:
@@ -831,7 +804,6 @@ class _ReplacePage(NFixedPage):
if item := self._getSelectedItem():
item.setText(self.C_REPL, text)
self._changed = True
return
@pyqtSlot()
def _onSelectionChanged(self) -> None:
@@ -850,14 +822,12 @@ class _ReplacePage(NFixedPage):
self.editValue.setText("")
self.editKey.setEnabled(False)
self.editValue.setEnabled(False)
return
@pyqtSlot()
def _onEntryCreated(self) -> None:
"""Add a new list entry."""
key = f"<keyword{self.listBox.topLevelItemCount() + 1:d}>"
self.listBox.addTopLevelItem(QTreeWidgetItem([key, ""]))
return
@pyqtSlot()
def _onEntryDeleted(self) -> None:
@@ -865,7 +835,6 @@ class _ReplacePage(NFixedPage):
if item := self._getSelectedItem():
self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(item))
self._changed = True
return
##
# 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -43,6 +43,7 @@ logger = logging.getLogger(__name__)
class GuiQuoteSelect(NDialog):
"""GUI: Quote Selector Dialog."""
_selected = ""
@@ -108,11 +109,8 @@ class GuiQuoteSelect(NDialog):
logger.debug("Ready: GuiQuoteSelect")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiQuoteSelect")
return
@property
def selectedQuote(self) -> str:
@@ -140,4 +138,3 @@ class GuiQuoteSelect(NDialog):
quote = items[0].data(self.D_KEY)
self.previewLabel.setText(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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -48,6 +48,7 @@ logger = logging.getLogger(__name__)
class GuiWordList(NDialog):
"""GUI: User Dictionary Edit Tool."""
newWordListReady = pyqtSignal()
@@ -128,11 +129,8 @@ class GuiWordList(NDialog):
logger.debug("Ready: GuiWordList")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiWordList")
return
##
# Events
@@ -143,7 +141,6 @@ class GuiWordList(NDialog):
self._saveGuiSettings()
event.accept()
self.softDelete()
return
##
# Private Slots
@@ -159,14 +156,12 @@ class GuiWordList(NDialog):
if items := self.listBox.findItems(word, Qt.MatchFlag.MatchExactly):
self.listBox.setCurrentItem(items[0])
self.listBox.scrollToItem(items[0], QAbstractItemView.ScrollHint.PositionAtCenter)
return
@pyqtSlot()
def _doDelete(self) -> None:
"""Delete the selected items."""
for item in self.listBox.selectedItems():
self.listBox.takeItem(self.listBox.row(item))
return
@pyqtSlot()
def _doSave(self) -> None:
@@ -178,7 +173,6 @@ class GuiWordList(NDialog):
self.newWordListReady.emit()
QApplication.processEvents()
self.close()
return
@pyqtSlot()
def _importWords(self) -> None:
@@ -213,7 +207,6 @@ class GuiWordList(NDialog):
fo.write("\n".join(self._listWords()))
except Exception as exc:
SHARED.error("Could not write file.", exc=exc)
return
##
# Internal Functions
@@ -226,7 +219,6 @@ class GuiWordList(NDialog):
self.listBox.clear()
for word in userDict:
self.listBox.addItem(word)
return
def _saveGuiSettings(self) -> None:
"""Save GUI settings."""
@@ -234,14 +226,12 @@ class GuiWordList(NDialog):
pOptions = SHARED.project.options
pOptions.setValue("GuiWordList", "winWidth", self.width())
pOptions.setValue("GuiWordList", "winHeight", self.height())
return
def _addWord(self, word: str) -> None:
"""Add a single word to the list."""
if word and not self.listBox.findItems(word, Qt.MatchFlag.MatchExactly):
self.listBox.addItem(word)
self._changed = True
return
def _listWords(self) -> list[str]:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from enum import Enum
class nwItemType(Enum):
"""Enum: Project Item Types."""
NO_TYPE = 0
ROOT = 1
@@ -34,6 +35,7 @@ class nwItemType(Enum):
class nwItemClass(Enum):
"""Enum: Project Item Classes."""
NO_CLASS = 0
NOVEL = 1
@@ -50,6 +52,7 @@ class nwItemClass(Enum):
class nwItemLayout(Enum):
"""A project item's layout."""
NO_LAYOUT = 0
DOCUMENT = 1
@@ -57,6 +60,7 @@ class nwItemLayout(Enum):
class nwComment(Enum):
"""Types of text comments."""
PLAIN = 0
IGNORE = 1
@@ -69,6 +73,7 @@ class nwComment(Enum):
class nwChange(Enum):
"""Change request modes."""
CREATE = 0
UPDATE = 1
@@ -76,12 +81,14 @@ class nwChange(Enum):
class nwDocMode(Enum):
"""Document open modes."""
VIEW = 0
EDIT = 1
class nwDocAction(Enum):
"""Document actions."""
NO_ACTION = 0
UNDO = 1
@@ -125,6 +132,7 @@ class nwDocAction(Enum):
class nwDocInsert(Enum):
"""Document insert actions."""
NO_INSERT = 0
QUOTE_LS = 1
@@ -142,6 +150,7 @@ class nwDocInsert(Enum):
class nwView(Enum):
"""Main GUI view modes."""
EDITOR = 0
PROJECT = 1
@@ -151,6 +160,7 @@ class nwView(Enum):
class nwFocus(Enum):
"""Main GUI panel focus."""
TREE = 1
DOCUMENT = 2
@@ -158,6 +168,7 @@ class nwFocus(Enum):
class nwTheme(Enum):
"""GUI theme colour modes."""
AUTO = 0
LIGHT = 1
@@ -165,6 +176,7 @@ class nwTheme(Enum):
class nwOutline(Enum):
"""Enum: Project Outline Columns."""
TITLE = 0
LEVEL = 1
@@ -189,6 +201,7 @@ class nwOutline(Enum):
class nwNovelExtra(Enum):
"""Enum: Novel View Extra Columns."""
HIDDEN = 0
POV = 1
@@ -197,6 +210,7 @@ class nwNovelExtra(Enum):
class nwBuildFmt(Enum):
"""Enum: Manuscript Document Formats."""
ODT = 0
FODT = 1
@@ -211,6 +225,7 @@ class nwBuildFmt(Enum):
class nwStatusShape(Enum):
"""Enum: Status/Importance Icon Shapes."""
SQUARE = 0
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -47,7 +47,6 @@ def logException() -> None:
exType, exValue, _ = sys.exc_info()
if exType is not None:
logger.error(f"{exType.__name__}: {exValue!s}", stacklevel=2)
return
def formatException(exc: BaseException) -> str:
@@ -58,6 +57,7 @@ def formatException(exc: BaseException) -> str:
class NWErrorMessage(QDialog):
"""GUI: Error Dialog."""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
@@ -110,8 +110,6 @@ class NWErrorMessage(QDialog):
self.setSizeGripEnabled(True)
self.resize(800, 400)
return
def setMessage(self, exType: type, exValue: BaseException, exTrace: TracebackType) -> None:
"""Generate a message and append session data, error info and
error traceback.
@@ -143,7 +141,7 @@ class NWErrorMessage(QDialog):
enchantVersion = "Unknown"
try:
txtTrace = "\n".join(format_tb(exTrace))
trace = "\n".join(format_tb(exTrace))
self.msgBody.setPlainText(
"Environment:\n"
f"novelWriter Version: {__version__}\n"
@@ -152,13 +150,11 @@ class NWErrorMessage(QDialog):
f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n"
f"enchant: {enchantVersion}\n\n"
f"{exType.__name__}:\n{exValue!s}\n\n"
f"Traceback:\n{txtTrace}\n"
f"Traceback:\n{trace}\n"
)
except Exception:
self.msgBody.setPlainText("Failed to generate error report ...")
return
##
# Slots
##
@@ -167,11 +163,10 @@ class NWErrorMessage(QDialog):
def _doClose(self) -> None:
"""Close the dialog."""
self.close()
return
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 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
from PyQt6.QtGui import QColor, QFont, QPalette, QPixmap
@@ -39,7 +39,7 @@ DEFAULT_SCALE = 0.9
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
QFrame that sets the same frame style as the other Page widgets.
@@ -49,23 +49,20 @@ class NFixedPage(QFrame):
super().__init__(parent=parent)
self.setFrameShadow(QFrame.Shadow.Sunken)
self.setFrameShape(QFrame.Shape.StyledPanel)
return
def setCentralLayout(self, layout: QLayout) -> None:
"""Set a layout as the central object."""
self.setLayout(layout)
return
def setCentralWidget(self, widget: QWidget) -> None:
"""Set a layout as the central object."""
layout = QHBoxLayout()
layout.addWidget(widget)
self.setLayout(layout)
return
class NScrollablePage(QScrollArea):
"""Extension: Scrollable Page Widget
"""Extension: Scrollable Page Widget.
A custom widget that holds a layout within a scrollable area.
"""
@@ -79,16 +76,14 @@ class NScrollablePage(QScrollArea):
self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
self.setFrameShadow(QFrame.Shadow.Sunken)
self.setFrameShape(QFrame.Shape.StyledPanel)
return
def setCentralLayout(self, layout: QLayout) -> None:
"""Set the central layout of the scroll page."""
self._widget.setLayout(layout)
return
class NScrollableForm(QScrollArea):
"""Extension: Scrollable Form Widget
"""Extension: Scrollable Form Widget.
A custom widget that creates a form within a scrollable area.
"""
@@ -117,8 +112,6 @@ class NScrollableForm(QScrollArea):
self.setFrameShadow(QFrame.Shadow.Sunken)
self.setFrameShape(QFrame.Shape.StyledPanel)
return
##
# Properties
##
@@ -135,18 +128,15 @@ class NScrollableForm(QScrollArea):
"""Set the text color for the help text."""
self._helpCol = color
self._fontScale = scale
return
def setHelpText(self, key: str, text: str) -> None:
"""Set the text for the help label."""
if qHelp := self._editable.get(key):
qHelp.setText(text)
return
def setRowIndent(self, indent: int) -> None:
"""Set the indentation of each row."""
self._indent = max(indent, 0)
return
##
# Methods
@@ -158,7 +148,6 @@ class NScrollableForm(QScrollArea):
yPos = self._sections[identifier].pos().y() - 8
if vBar := self.verticalScrollBar():
vBar.setValue(yPos)
return
def scrollToLabel(self, label: str) -> None:
"""Scroll to the requested label."""
@@ -166,7 +155,6 @@ class NScrollableForm(QScrollArea):
yPos = self._index[label].pos().y() - 8
if vBar := self.verticalScrollBar():
vBar.setValue(yPos)
return
def addGroupLabel(self, label: str, identifier: int | None = None) -> None:
"""Add a text label to separate groups of settings."""
@@ -178,7 +166,6 @@ class NScrollableForm(QScrollArea):
self._first = False
if identifier is not None:
self._sections[identifier] = qLabel
return
def addRow(
self,
@@ -252,17 +239,14 @@ class NScrollableForm(QScrollArea):
self._index[label.strip()] = qWidget
qLabel.setAccessibleName(text)
return
def finalise(self) -> None:
"""Finalise the layout when the form is built."""
self._layout.addSpacing(20)
self._layout.addStretch(1)
return
class NColorLabel(QLabel):
"""Extension: A Coloured Label
"""Extension: A Coloured Label.
A custom widget that draws a label in a specific colour, and
optionally at a specific size, and word wrapped.
@@ -298,21 +282,17 @@ class NColorLabel(QLabel):
self.setWordWrap(wrap)
self.setColorState(True)
return
def setTextColors(self, *, color: QColor | None = None, faded: QColor | None = None) -> None:
"""Set or update the text colours."""
self._color = color or self._color
self._faded = faded or self._faded
self._refeshTextColor()
return
def setColorState(self, state: bool) -> None:
"""Change the colour state."""
if self._state is not state:
self._state = state
self._refeshTextColor()
return
def _refeshTextColor(self) -> None:
"""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.setPalette(palette)
return
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
either side within a layout box. The widget is inserted at the {0}
@@ -341,4 +320,3 @@ class NWrappedWidgetBox(QHBoxLayout):
self.addWidget(widget)
if after:
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -34,7 +34,7 @@ if TYPE_CHECKING:
class WheelEventFilter(QObject):
"""Extensions: Wheel Event Filter
"""Extensions: Wheel Event Filter.
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
@@ -50,7 +50,6 @@ class WheelEventFilter(QObject):
super().__init__(parent=parent)
self._parent = parent
self._locked = False
return
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
"""Filter events of type QWheelEvent and forward them to the
@@ -69,6 +68,7 @@ class WheelEventFilter(QObject):
class StatusTipFilter(QObject):
"""Filter: Remove StatusBar ToolTips."""
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -47,6 +47,7 @@ if TYPE_CHECKING:
class NDialog(QDialog):
"""Custom: Modified QDialog."""
def softDelete(self) -> None:
"""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.
"""
self.setParent(None) # type: ignore
return
@pyqtSlot()
def reject(self) -> None:
"""Overload the reject slot and also call close."""
super().reject()
self.close()
return
class NToolDialog(NDialog):
"""Custom: Modified QDialog for Tools."""
def __init__(self, parent: GuiMain) -> None:
super().__init__(parent=parent)
self.setModal(False)
if CONFIG.osDarwin:
self.setWindowFlag(Qt.WindowType.Tool)
return
def activateDialog(self) -> None:
"""Helper function to activate dialog on various systems."""
"""Activate dialog on various operating systems."""
self.show()
if CONFIG.osWindows:
self.activateWindow()
self.raise_()
QApplication.processEvents()
return
class NNonBlockingDialog(NDialog):
"""Custom: Modified Non-Blocking QDialog."""
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent=parent)
self.setModal(True)
return
def activateDialog(self) -> None:
"""Helper function to activate dialog on various systems."""
"""Activate dialog on various operating systems."""
self.show()
if CONFIG.osWindows:
self.activateWindow()
self.raise_()
QApplication.processEvents()
return
class NTreeView(QTreeView):
"""Custom: Modified QTreeView.
The main purpose is to provide the middleClicked signal that matches
clicked and doubleCLicked.
"""
middleClicked = pyqtSignal(QModelIndex)
@@ -116,6 +118,12 @@ class NTreeView(QTreeView):
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:
super().__init__(parent=parent)
@@ -126,7 +134,30 @@ class NComboBox(QComboBox):
# and allows for scrolling of long lists of items
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:
"""Only capture the mouse wheel if the widget has focus."""
@@ -134,31 +165,15 @@ class NComboBox(QComboBox):
super().wheelEvent(event)
else:
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):
"""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__(
self,
@@ -175,17 +190,20 @@ class NDoubleSpinBox(QDoubleSpinBox):
self.setMaximum(maxVal)
self.setSingleStep(step)
self.setDecimals(prec)
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()
return
class NIconToolButton(QToolButton):
"""Custom: Modified QToolButton.
A quicker way to create a tool button using the app theme.
"""
def __init__(
self, parent: QWidget, iconSize: QSize,
@@ -197,15 +215,17 @@ class NIconToolButton(QToolButton):
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
if icon:
self.setThemeIcon(icon, color)
return
def setThemeIcon(self, iconKey: str, color: str | None = None) -> None:
"""Set an icon from the current theme."""
self.setIcon(SHARED.theme.getIcon(iconKey, color))
return
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:
super().__init__(parent=parent)
@@ -216,16 +236,15 @@ class NIconToggleButton(QToolButton):
self.setStyleSheet("border: none; background: transparent;")
if icon:
self.setThemeIcon(icon)
return
def setThemeIcon(self, iconKey: str) -> None:
"""Set an icon from the current theme."""
iconSize = self.iconSize()
self.setIcon(SHARED.theme.getToggleIcon(iconKey, (iconSize.width(), iconSize.height())))
return
class NClickableLabel(QLabel):
"""Custom: Clickable QLabel."""
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
class NovelSelector(QComboBox):
"""Custom: Novel Root Folder Selector."""
novelSelectionChanged = pyqtSignal(str)
@@ -53,7 +54,6 @@ class NovelSelector(QComboBox):
self._listFormat = None
self.currentIndexChanged.connect(self._indexChanged)
self.updateTheme()
return
##
# Properties
@@ -80,18 +80,15 @@ class NovelSelector(QComboBox):
self._blockSignal = blockSignal
self.setCurrentIndex(index)
self._blockSignal = False
return
def setIncludeAll(self, value: bool) -> None:
"""Set flag to add an "All Novel Folders" option."""
self._includeAll = value
return
def setListFormat(self, value: str | None) -> None:
"""Set a format string for the list entries."""
if value is None or "{0}" in value:
self._listFormat = value
return
def updateTheme(self) -> None:
"""Update theme colours."""
@@ -99,7 +96,6 @@ class NovelSelector(QComboBox):
palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, palette.text())
self.setPalette(palette)
self.refreshNovelList()
return
##
# Public Slots
@@ -133,8 +129,6 @@ class NovelSelector(QComboBox):
self.setEnabled(self.count() > 1)
self._blockSignal = False
return
##
# Private Slots
##
@@ -144,4 +138,3 @@ class NovelSelector(QComboBox):
"""Re-emit the change of selection signal, unless blocked."""
if not self._blockSignal:
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
from PyQt6.QtCore import QPoint, QRectF, QSize, Qt, pyqtSignal, pyqtSlot
@@ -39,7 +39,7 @@ from novelwriter.types import (
class NPagedSideBar(QToolBar):
"""Extensions: Paged Side Bar
"""Extensions: Paged Side Bar.
A side bar widget that holds buttons that mimic tabs. It is designed
to be used in combination with a QStackedWidget for options panels.
@@ -65,8 +65,6 @@ class NPagedSideBar(QToolBar):
stretch.setSizePolicy(QtSizeExpanding, QtSizeExpanding)
self._stretchAction = self.addWidget(stretch)
return
def button(self, buttonId: int) -> _PagedToolButton:
"""Return a specific button."""
return self._buttons[buttonId]
@@ -74,14 +72,12 @@ class NPagedSideBar(QToolBar):
def setLabelColor(self, color: QColor) -> None:
"""Set the text color for the labels."""
self._labelCol = color
return
def addLabel(self, text: str) -> None:
"""Add a new label to the toolbar."""
label = _NPagedToolLabel(self, self._labelCol)
label.setText(text)
self.insertWidget(self._stretchAction, label)
return
def addButton(self, text: str, buttonId: int = -1) -> None:
"""Add a new button to the toolbar."""
@@ -90,13 +86,11 @@ class NPagedSideBar(QToolBar):
self.insertWidget(self._stretchAction, button)
self._group.addButton(button, id=buttonId)
self._buttons[buttonId] = button
return
def setSelected(self, buttonId: int) -> None:
"""Set the selected button."""
if button := self._group.button(buttonId):
button.setChecked(True)
return
##
# Private Slots
@@ -104,11 +98,10 @@ class NPagedSideBar(QToolBar):
@pyqtSlot("QAbstractButton*")
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)
if buttonId != -1:
self.buttonClicked.emit(buttonId)
return
class _PagedToolButton(QToolButton):
@@ -127,8 +120,6 @@ class _PagedToolButton(QToolButton):
self._aH = 2*fH//7
self.setFixedHeight(self._bH)
return
def sizeHint(self) -> QSize:
"""Return a size hint that includes the arrow."""
return super().sizeHint() + QSize(4*self._aH, 0)
@@ -180,8 +171,6 @@ class _PagedToolButton(QToolButton):
]))
painter.end()
return
class _NPagedToolLabel(QLabel):
@@ -199,8 +188,6 @@ class _NPagedToolLabel(QLabel):
self._textCol = textColor or self.palette().text().color()
return
def paintEvent(self, event: QPaintEvent) -> None:
"""Overload the paint event to draw a simple, left aligned text
label that matches the button style.
@@ -215,5 +202,3 @@ class _NPagedToolLabel(QLabel):
painter.setOpacity(1.0)
painter.drawText(QRectF(4, self._tM, tW, tH), QtAlignLeft, self.text())
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
from math import ceil
@@ -37,7 +37,7 @@ from novelwriter.types import (
class NProgressCircle(QProgressBar):
"""Extension: Circular Progress Widget
"""Extension: Circular Progress Widget.
A custom widget that paints a circular progress indicator instead of
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.setFixedWidth(size)
self.setFixedHeight(size)
return
def setColors(
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)
if isinstance(text, QColor):
self._tColor = text
return
def setCentreText(self, text: str | None) -> None:
"""Replace the progress text with a custom string."""
self._text = text
self.setValue(self.value()) # Triggers a redraw
return
def paintEvent(self, event: QPaintEvent) -> None:
"""Custom painter for the progress bar."""
"""Paint the progress bar."""
progress = 100.0*self.value()/self.maximum()
angle = ceil(16*3.6*progress)
painter = QPainter(self)
@@ -103,21 +100,19 @@ class NProgressCircle(QProgressBar):
painter.drawArc(self._cRect, 90*16, -angle)
painter.setPen(self._tColor)
painter.drawText(self._cRect, QtAlignCenter, self._text or f"{progress:.1f} %")
return
class NProgressSimple(QProgressBar):
"""Extension: Simple Progress Widget
"""Extension: Simple Progress Widget.
A custom widget that paints a plain bar with no other styling.
"""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
return
def paintEvent(self, event: QPaintEvent) -> None:
"""Custom painter for the progress bar."""
"""Paint the progress bar."""
if (value := self.value()) > 0:
progress = ceil(self.width()*float(value)/self.maximum())
painter = QPainter(self)
@@ -125,4 +120,3 @@ class NProgressSimple(QProgressBar):
painter.setPen(self.palette().highlight().color())
painter.setBrush(self.palette().highlight())
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -34,6 +34,7 @@ logger = logging.getLogger(__name__)
class StatusLED(QAbstractButton):
"""Custom: LED Style Indicator."""
__slots__ = ("_color", "_negative", "_neutral", "_postitve", "_state")
@@ -46,7 +47,6 @@ class StatusLED(QAbstractButton):
self._state = None
self.setFixedWidth(sW)
self.setFixedHeight(sH)
return
@property
def state(self) -> bool | None:
@@ -59,7 +59,6 @@ class StatusLED(QAbstractButton):
self._postitve = positive
self._negative = negative
self.setState(self._state)
return
def setState(self, state: bool | None) -> None:
"""Set the colour state."""
@@ -71,7 +70,6 @@ class StatusLED(QAbstractButton):
self._color = self._neutral
self._state = state
self.update()
return
def paintEvent(self, event: QPaintEvent) -> None:
"""Draw the LED."""
@@ -82,4 +80,3 @@ class StatusLED(QAbstractButton):
painter.setOpacity(1.0)
painter.drawEllipse(1, 1, self.width() - 2, self.height() - 2)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty, pyqtSlot # pyright: ignore
@@ -32,6 +32,7 @@ from novelwriter.types import QtNoPen, QtPaintAntiAlias, QtSizeFixed
class NSwitch(QAbstractButton):
"""Custom: Toggle Switch."""
__slots__ = ("_cOff", "_cOn", "_offset", "_rH", "_rR", "_xH", "_xR", "_xW")
@@ -55,8 +56,6 @@ class NSwitch(QAbstractButton):
self.clicked.connect(self._onClick)
return
##
# Properties
##
@@ -69,7 +68,6 @@ class NSwitch(QAbstractButton):
def offset(self, offset: int) -> None:
self._offset = offset
self.update()
return
##
# Getters and Setters
@@ -79,7 +77,6 @@ class NSwitch(QAbstractButton):
"""Overload setChecked to also alter the offset."""
super().setChecked(checked)
self._offset = (self._xW - self._xR) if checked else self._xR
return
##
# Events
@@ -89,7 +86,6 @@ class NSwitch(QAbstractButton):
"""Overload resize to ensure correct offset."""
super().resizeEvent(event)
self._offset = (self._xW - self._xR) if self.isChecked() else self._xR
return
def paintEvent(self, event: QPaintEvent) -> None:
"""Drawing the switch itself."""
@@ -109,13 +105,10 @@ class NSwitch(QAbstractButton):
painter.end()
return
def enterEvent(self, event: QEnterEvent) -> None:
"""Change the cursor when hovering the button."""
self.setCursor(Qt.CursorShape.PointingHandCursor)
super().enterEvent(event)
return
@pyqtSlot(bool)
def _onClick(self, checked: bool) -> None:
@@ -125,4 +118,3 @@ class NSwitch(QAbstractButton):
anim.setStartValue(self._offset)
anim.setEndValue((self._xW - self._xR) if checked else self._xR)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -39,7 +39,7 @@ if TYPE_CHECKING:
class NSwitchBox(QScrollArea):
"""Extension: Switch Box Widget
"""Extension: Switch Box Widget.
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.
@@ -55,7 +55,6 @@ class NSwitchBox(QScrollArea):
self._sIcon = baseSize
self._widgets = []
self.clear()
return
def clear(self) -> None:
"""Rebuild the content of the core widget."""
@@ -72,8 +71,6 @@ class NSwitchBox(QScrollArea):
self.setWidgetResizable(True)
self.setWidget(self._widget)
return
def addLabel(self, text: str) -> None:
"""Add a header label to the content box."""
label = QLabel(text, self)
@@ -83,7 +80,6 @@ class NSwitchBox(QScrollArea):
self._content.addWidget(label, self._index, 0, 1, 3, QtAlignLeft)
self._widgets.append(label)
self._bumpIndex()
return
def addItem(self, qIcon: QIcon, text: str, identifier: str, default: bool = False) -> None:
"""Add an item to the content box."""
@@ -104,8 +100,6 @@ class NSwitchBox(QScrollArea):
self._widgets.append(switch)
self._bumpIndex()
return
def addSeparator(self) -> None:
"""Add a blank entry in the content box."""
spacer = QWidget(self)
@@ -113,7 +107,6 @@ class NSwitchBox(QScrollArea):
self._content.addWidget(spacer, self._index, 0, 1, 3, QtAlignLeft)
self._widgets.append(spacer)
self._bumpIndex()
return
##
# Internal Functions
@@ -122,7 +115,6 @@ class NSwitchBox(QScrollArea):
def _emitSwitchSignal(self, identifier: str, state: bool) -> None:
"""Emit a signal for a switch toggle."""
self.switchToggled.emit(identifier, state)
return
def _bumpIndex(self) -> None:
"""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 + 1, 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -45,6 +45,11 @@ API_URL = "https://api.github.com/repos/vkbo/novelwriter/releases/latest"
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:
super().__init__(parent=parent)
@@ -75,8 +80,6 @@ class VersionInfoWidget(QWidget):
self.setLayout(self._layout)
return
##
# Private Slots
##
@@ -93,7 +96,6 @@ class VersionInfoWidget(QWidget):
lookup = _Retriever()
lookup.signals.dataReady.connect(self._updateReleaseInfo)
SHARED.runInThreadPool(lookup)
return
##
# Private Slots
@@ -109,7 +111,6 @@ class VersionInfoWidget(QWidget):
))
else:
self._lblRelease.setText(self._trLatest.format(reason or self.tr("Failed")))
return
class _Retriever(QRunnable):
@@ -117,7 +118,6 @@ class _Retriever(QRunnable):
def __init__(self) -> None:
super().__init__()
self.signals = _RetrieverSignal()
return
@pyqtSlot()
def run(self) -> None:
@@ -140,7 +140,6 @@ class _Retriever(QRunnable):
except Exception as e:
logger.error("Failed to retrieve release info")
self.signals.dataReady.emit("", str(e))
return
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -100,7 +100,7 @@ def _wText(parent: ET.Element, text: str) -> ET.Element:
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)
@@ -143,6 +143,7 @@ S_FNOTE = "FootnoteText"
class DocXXmlRel(NamedTuple):
"""DocX XML Rel Data."""
rId: str
relType: str
@@ -150,6 +151,7 @@ class DocXXmlRel(NamedTuple):
class DocXXmlFile(NamedTuple):
"""DocX XML File Data."""
xml: ET.Element
path: str
@@ -157,6 +159,7 @@ class DocXXmlFile(NamedTuple):
class DocXParStyle(NamedTuple):
"""DocX XML Paragraph Style Data."""
name: str
styleId: str
@@ -176,7 +179,7 @@ class DocXParStyle(NamedTuple):
class ToDocX(Tokenizer):
"""Core: DocX Document Writer
"""Core: DocX Document Writer.
Extend the Tokenizer class to writer DocX Document files.
"""
@@ -202,8 +205,6 @@ class ToDocX(Tokenizer):
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[ET.Element, str]] = []
return
##
# Setters
##
@@ -214,25 +215,22 @@ class ToDocX(Tokenizer):
"""Set the document page size and margins in millimetres."""
self._pageSize = QSize(_mmToSz(width), _mmToSz(height))
self._pageMargins = QMargins(_mmToSz(left), _mmToSz(top), _mmToSz(right), _mmToSz(bottom))
return
def setHeaderFormat(self, value: str, offset: int) -> None:
"""Set the document header format."""
self._headerFormat = value.strip()
self._pageOffset = offset
return
##
# Class Methods
##
def initDocument(self) -> None:
"""Initialises the DocX document structure."""
"""Initialise the DocX document structure."""
super().initDocument()
self._fontFamily = self._textFont.family()
self._fontSize = self._textFont.pointSizeF()
self._generateStyles()
return
def doConvert(self) -> None:
"""Convert the list of text tokens into XML elements."""
@@ -302,8 +300,6 @@ class ToDocX(Tokenizer):
elif tType == BlockTyp.KEYWORD:
self._processFragments(par, S_META, tText, tFormat)
return
def closeDocument(self) -> None:
"""Generate all the XML."""
self._coreXml()
@@ -322,8 +318,6 @@ class ToDocX(Tokenizer):
if self._usedNotes:
self._footnotesXml()
return
def saveDocument(self, path: Path) -> None:
"""Save the data to a .docx file."""
# Content Lists
@@ -373,8 +367,6 @@ class ToDocX(Tokenizer):
xmlToZip(f"{rel.path}/{name}", rel.xml, outZip)
xmlToZip("[Content_Types].xml", dTypes, outZip)
return
##
# Internal Functions
##
@@ -454,8 +446,6 @@ class ToDocX(Tokenizer):
if temp := text[fStart:]:
par.addContent(self._textRunToXml(temp, xFmt, fClass, fLink))
return
def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element:
"""Encode the text run into XML."""
xR = xmlElement(_wTag("r"))
@@ -668,8 +658,6 @@ class ToDocX(Tokenizer):
for style in styles:
self._styles[style.styleId] = style
return
def _nextRelId(self) -> str:
"""Generate the next unique rId."""
return f"rId{len(self._rels) + 1}"
@@ -1054,6 +1042,10 @@ class ToDocX(Tokenizer):
class DocXParagraph:
"""DocX Text Paragraph.
This class holds a single paragraph of a DocX document.
"""
__slots__ = (
"_bottomMargin", "_breakAfter", "_breakBefore", "_content",
@@ -1073,7 +1065,6 @@ class DocXParagraph:
self._breakBefore = False
self._breakAfter = False
self._footnoteRef = False
return
##
# Properties
@@ -1091,53 +1082,43 @@ class DocXParagraph:
def setStyle(self, style: DocXParStyle | None) -> None:
"""Set the paragraph style."""
self._style = style
return
def setAlignment(self, value: str) -> None:
"""Set paragraph alignment."""
if value in ("left", "center", "right", "both"):
self._textAlign = value
return
def setMarginTop(self, value: float) -> None:
"""Set margin above in pt."""
self._topMargin = value
return
def setMarginBottom(self, value: float) -> None:
"""Set margin below in pt."""
self._bottomMargin = value
return
def setMarginLeft(self, value: float) -> None:
"""Set margin left in pt."""
self._leftMargin = value
return
def setMarginRight(self, value: float) -> None:
"""Set margin right in pt."""
self._rightMargin = value
return
def setIndentFirst(self, state: bool) -> None:
"""Set first line indent."""
self._indentFirst = state
return
def setPageBreakBefore(self, state: bool) -> None:
"""Set page break before flag."""
self._breakBefore = state
return
def setPageBreakAfter(self, state: bool) -> None:
"""Set page break after flag."""
self._breakAfter = state
return
def setIsFootnote(self, state: bool) -> None:
"""Set is footnote flag."""
self._footnoteRef = state
return
##
# Methods
@@ -1146,10 +1127,9 @@ class DocXParagraph:
def addContent(self, run: ET.Element) -> None:
"""Add a run segment to the paragraph."""
self._content.append(run)
return
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:
xP = xmlSubElem(body, _wTag("p"))
@@ -1191,5 +1171,3 @@ class DocXParagraph:
if self._breakAfter:
xR = xmlSubElem(xP, _wTag("r"))
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -77,7 +77,7 @@ HTML_NONE = (0, "")
class ToHtml(Tokenizer):
"""Core: HTML Document Writer
"""Core: HTML Document Writer.
Extend the Tokenizer class to writer HTML output. This class is
also used by the Document Viewer, and Manuscript Build Preview.
@@ -90,7 +90,6 @@ class ToHtml(Tokenizer):
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = []
self.setReplaceUnicode(False)
return
##
# Setters
@@ -101,7 +100,6 @@ class ToHtml(Tokenizer):
class tags.
"""
self._cssStyles = cssStyles
return
def setReplaceUnicode(self, doReplace: bool) -> None:
"""Set the translation map to either minimal or full unicode for
@@ -114,7 +112,6 @@ class ToHtml(Tokenizer):
if doReplace:
# Extend to all relevant Unicode characters
self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
return
##
# Class Methods
@@ -130,7 +127,6 @@ class ToHtml(Tokenizer):
"""
super().doPreProcessing()
self._text = self._text.translate(self._trMap)
return
def doConvert(self) -> None:
"""Convert the list of text tokens into an HTML document."""
@@ -237,8 +233,6 @@ class ToHtml(Tokenizer):
self._pages.append("".join(lines))
return
def closeDocument(self) -> None:
"""Run close document tasks."""
# Replace fields if there are stats available
@@ -265,8 +259,6 @@ class ToHtml(Tokenizer):
self._pages.append("".join(lines))
return
def saveDocument(self, path: Path) -> None:
"""Save the data to an HTML file."""
if path.suffix.lower() == ".json":
@@ -309,14 +301,11 @@ class ToHtml(Tokenizer):
logger.info("Wrote file: %s", path)
return
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = "&nbsp;") -> None:
"""Replace tabs with spaces in the html."""
tabSpace = spaceChar*nSpaces
pages = [aLine.replace("\t", tabSpace) for aLine in self._pages]
self._pages = pages
return
def getStyleSheet(self) -> list[str]:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -55,6 +55,7 @@ logger = logging.getLogger(__name__)
class ComStyle(NamedTuple):
"""Comment style info."""
label: str = ""
labelClass: str = ""
@@ -92,7 +93,7 @@ B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE)
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
novelWriter markup format and generates a registry of tokens and
@@ -224,8 +225,6 @@ class Tokenizer(ABC):
self._dialogParser = DialogParser()
self._dialogParser.initParser()
return
##
# Properties
##
@@ -253,94 +252,78 @@ class Tokenizer(ABC):
"""Set language for the document."""
if language:
self._dLocale = QLocale(language)
return
def setTheme(self, theme: TextDocumentTheme) -> None:
"""Set the document colour theme."""
self._theme = theme
return
def setPartitionFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the partition format pattern."""
self._fmtPart = hFormat.strip()
self._hidePart = hide
return
def setChapterFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the chapter format pattern."""
self._fmtChapter = hFormat.strip()
self._hideChapter = hide
return
def setUnNumberedFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the unnumbered format pattern."""
self._fmtUnNum = hFormat.strip()
self._hideUnNum = hide
return
def setSceneFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the scene format pattern and hidden status."""
self._fmtScene = hFormat.strip()
self._hideScene = hide
return
def setHardSceneFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the hard scene format pattern and hidden status."""
self._fmtHScene = hFormat.strip()
self._hideHScene = hide
return
def setSectionFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the section format pattern and hidden status."""
self._fmtSection = hFormat.strip()
self._hideSection = hide
return
def setTitleStyle(self, center: bool, pageBreak: bool) -> None:
"""Set the title heading style."""
self._titleStyle = BlockFmt.CENTRE if center else BlockFmt.NONE
self._titleStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE
return
def setPartitionStyle(self, center: bool, pageBreak: bool) -> None:
"""Set the partition heading style."""
self._partStyle = BlockFmt.CENTRE if center else BlockFmt.NONE
self._partStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE
return
def setChapterStyle(self, center: bool, pageBreak: bool) -> None:
"""Set the chapter heading style."""
self._chapterStyle = BlockFmt.CENTRE if center else BlockFmt.NONE
self._chapterStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE
return
def setSceneStyle(self, center: bool, pageBreak: bool) -> None:
"""Set the scene heading style."""
self._sceneStyle = BlockFmt.CENTRE if center else BlockFmt.NONE
self._sceneStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE
return
def setTextFont(self, font: QFont) -> None:
"""Set the build font."""
self._textFont = fontMatcher(font)
return
def setLineHeight(self, height: float) -> None:
"""Set the line height between 0.5 and 5.0."""
self._lineHeight = min(max(float(height), 0.5), 5.0)
return
def setHeadingStyles(self, color: bool, scale: bool, bold: bool) -> None:
"""Set text style for headings."""
self._colorHeads = color
self._scaleHeads = scale
self._boldHeads = bold
return
def setBlockIndent(self, indent: float) -> None:
"""Set the block indent between 0.0 and 10.0."""
self._blockIndent = min(max(float(indent), 0.0), 10.0)
return
def setFirstLineIndent(self, state: bool, indent: float, first: bool) -> None:
"""Set first line indent and whether to also indent first
@@ -349,67 +332,54 @@ class Tokenizer(ABC):
self._firstIndent = state
self._firstWidth = indent
self._indentFirst = first
return
def setJustify(self, state: bool) -> None:
"""Enable or disable text justification."""
self._doJustify = state
return
def setDialogHighlight(self, state: bool) -> None:
"""Enable or disable dialogue highlighting."""
self._hlightDialog = state
return
def setTitleMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower title margin."""
self._marginTitle = (float(upper), float(lower))
return
def setHead1Margins(self, upper: float, lower: float) -> None:
"""Set the upper and lower heading 1 margin."""
self._marginHead1 = (float(upper), float(lower))
return
def setHead2Margins(self, upper: float, lower: float) -> None:
"""Set the upper and lower heading 2 margin."""
self._marginHead2 = (float(upper), float(lower))
return
def setHead3Margins(self, upper: float, lower: float) -> None:
"""Set the upper and lower heading 3 margin."""
self._marginHead3 = (float(upper), float(lower))
return
def setHead4Margins(self, upper: float, lower: float) -> None:
"""Set the upper and lower heading 4 margin."""
self._marginHead4 = (float(upper), float(lower))
return
def setTextMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower text margin."""
self._marginText = (float(upper), float(lower))
return
def setMetaMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower meta text margin."""
self._marginMeta = (float(upper), float(lower))
return
def setSeparatorMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower meta text margin."""
self._marginSep = (float(upper), float(lower))
return
def setLinkHeadings(self, state: bool) -> None:
"""Enable or disable adding an anchor before headings."""
self._linkHeadings = state
return
def setBodyText(self, state: bool) -> None:
"""Include body text in build."""
self._doBodyText = state
return
def setCommentType(self, comment: nwComment, state: bool) -> None:
"""Toggle the inclusion og certain comment types."""
@@ -417,22 +387,18 @@ class Tokenizer(ABC):
self._doComments.add(comment)
else:
self._doComments.discard(comment)
return
def setKeywords(self, state: bool) -> None:
"""Include keywords in build."""
self._doKeywords = state
return
def setIgnoredKeywords(self, keywords: str) -> None:
"""Comma separated string of keywords to ignore."""
self._skipKeywords = set(x.lower().strip() for x in keywords.split(","))
return
def setKeepLineBreaks(self, state: bool) -> None:
"""Keep line breaks in paragraphs."""
self._keepBreaks = state
return
##
# Class Methods
@@ -460,12 +426,10 @@ class Tokenizer(ABC):
self._classes["tag"] = self._theme.tag
self._classes["keyword"] = self._theme.keyword
self._classes["optional"] = self._theme.optional
return
def setBreakNext(self) -> None:
"""Set a page break for next block."""
self._breakNext = True
return
def addRootHeading(self, tHandle: str) -> None:
"""Add a heading at the start of a new root folder."""
@@ -491,8 +455,6 @@ class Tokenizer(ABC):
if self._keepRaw:
self._raw.append(f"#! {title}\n\n")
return
def setText(self, tHandle: str, text: str | None = None) -> None:
"""Set the text for the tokenizer from a handle. If text is not
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._handle = tHandle
self._isNovel = nwItem.itemLayout == nwItemLayout.DOCUMENT
return
def doPreProcessing(self) -> None:
"""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()}
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)
return
def tokenizeText(self) -> None:
"""Scan the text for either lines starting with specific
@@ -590,13 +550,13 @@ class Tokenizer(ABC):
self._breakNext = True
continue
elif sLine == "[vspace]":
if sLine == "[vspace]":
tBlocks.append(
(BlockTyp.SKIP, "", "", [], tStyle)
)
continue
elif sLine.startswith("[vspace:") and sLine.endswith("]"):
if sLine.startswith("[vspace:") and sLine.endswith("]"):
nSkip = checkInt(sLine[8:-1], 0)
if nSkip >= 1:
tBlocks.append(
@@ -962,8 +922,6 @@ class Tokenizer(ABC):
text = tText.replace(nwHeadFmt.BR, " ").replace("&amp;", "&")
self._outline[tKey] = f"{prefix}|{text}"
return
def countStats(self) -> None:
"""Count stats on the tokenized text."""
titleCount = self._counts.get(nwStats.TITLES, 0)
@@ -1039,8 +997,6 @@ class Tokenizer(ABC):
self._counts[nwStats.WCHARS_TEXT] = textWordChars
self._counts[nwStats.WCHARS_TITLE] = titleWordChars
return
##
# Internal Functions
##
@@ -1182,6 +1138,12 @@ class Tokenizer(ABC):
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__(
self,
@@ -1195,35 +1157,29 @@ class HeadingFormatter:
self._chapter = chapter
self._scene = scene
self._absolute = absolute
return
def setHandle(self, tHandle: str | None) -> None:
"""Set the handle currently being processed."""
self._handle = tHandle
return
def incChapter(self) -> None:
"""Increment the chapter counter."""
self._chapter += 1
return
def incScene(self) -> None:
"""Increment the scene counters."""
self._scene += 1
self._absolute += 1
return
def resetAll(self) -> None:
"""Reset all counters."""
self._chapter = 0
self._scene = 0
self._absolute = 0
return
def resetScene(self) -> None:
"""Reset the chapter scene counter."""
self._scene = 0
return
def apply(self, hFormat: str, text: str, nHead: int) -> str:
"""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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -79,7 +79,7 @@ EXT_MD = {
class ToMarkdown(Tokenizer):
"""Core: Markdown Document Writer
"""Core: Markdown Document Writer.
Extend the Tokenizer class to writer Markdown output. It supports
both Standard Markdown and Extended Markdown. The class also
@@ -91,7 +91,6 @@ class ToMarkdown(Tokenizer):
self._extended = extended
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = []
return
##
# Class Methods
@@ -153,8 +152,6 @@ class ToMarkdown(Tokenizer):
self._pages.append("".join(lines))
return
def closeDocument(self) -> None:
"""Run close document tasks."""
# Replace fields if there are stats available
@@ -181,20 +178,16 @@ class ToMarkdown(Tokenizer):
lines.append("\n")
self._pages.append("".join(lines))
return
def saveDocument(self, path: Path) -> None:
"""Save the data to a plain text file."""
with open(path, mode="w", encoding="utf-8") as outFile:
outFile.write("".join(self._pages))
logger.info("Wrote file: %s", path)
return
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None:
"""Replace tabs with spaces."""
spaces = spaceChar*nSpaces
self._pages = [p.replace("\t", spaces) for p in self._pages]
return
##
# 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -128,7 +128,7 @@ FONT_WEIGHT_MAP = {"400": "normal", "700": "bold"}
class ToOdt(Tokenizer):
"""Core: Open Document Writer
"""Core: Open Document Writer.
Extend the Tokenizer class to writer Open Document files. The output
should conform to the 1.3 Extended standard.
@@ -189,8 +189,6 @@ class ToOdt(Tokenizer):
self._mDocLeft = "2.000cm"
self._mDocRight = "2.000cm"
return
##
# Setters
##
@@ -205,20 +203,18 @@ class ToOdt(Tokenizer):
self._mDocBtm = f"{bottom/10.0:.3f}cm"
self._mDocLeft = f"{left/10.0:.3f}cm"
self._mDocRight = f"{right/10.0:.3f}cm"
return
def setHeaderFormat(self, value: str, offset: int) -> None:
"""Set the document header format."""
self._headerFormat = value.strip()
self._pageOffset = offset
return
##
# Class Methods
##
def initDocument(self) -> None:
"""Initialises a new open document XML tree."""
"""Initialise a new open document XML tree."""
super().initDocument()
# Initialise Variables
@@ -325,8 +321,6 @@ class ToOdt(Tokenizer):
self._useableStyles()
self._writeHeader()
return
def doConvert(self) -> None:
"""Convert the list of text tokens into XML elements."""
xText = self._xText
@@ -395,8 +389,6 @@ class ToOdt(Tokenizer):
elif tType == BlockTyp.KEYWORD:
self._addTextPar(xText, S_META, oStyle, tText, tFmt=tFormat)
return
def closeDocument(self) -> None:
"""Add additional collected information to the XML."""
for style in self._autoPara.values():
@@ -412,7 +404,6 @@ class ToOdt(Tokenizer):
_mkTag("text", "name"): f"Manuscript{key[:1].upper()}{key[1:]}",
})
self._xText.insert(0, xFields)
return
def saveDocument(self, path: Path) -> None:
"""Save the data to an .fodt or .odt file."""
@@ -456,8 +447,6 @@ class ToOdt(Tokenizer):
logger.info("Wrote file: %s", path)
return
##
# Internal Functions
##
@@ -670,7 +659,7 @@ class ToOdt(Tokenizer):
return None
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"
def _emToPt(self, scale: float) -> str:
@@ -705,8 +694,6 @@ class ToOdt(Tokenizer):
_mkTag("fo", "margin-bottom"): self._emToCm(0.5),
})
return
def _defaultStyles(self) -> None:
"""Set the default styles."""
hScale = self._scaleHeads
@@ -783,8 +770,6 @@ class ToOdt(Tokenizer):
_mkTag("number", "min-integer-digits"): "1",
})
return
def _useableStyles(self) -> None:
"""Set the usable styles."""
hScale = self._scaleHeads
@@ -954,8 +939,6 @@ class ToOdt(Tokenizer):
style.packXML(self._xStyl)
self._mainPara[style.name] = style
return
def _writeHeader(self) -> None:
"""Write the header elements."""
xPage = ET.SubElement(self._xMast, _mkTag("style", "master-page"), attrib={
@@ -993,8 +976,6 @@ class ToOdt(Tokenizer):
_mkTag("text", "style-name"): "Header"
})
return
# Auto-Style Classes
# ==================
@@ -1004,6 +985,7 @@ class ODTParagraphStyle:
exporter. Only the used settings are exposed here to keep the class
minimal and fast.
"""
VALID_ALIGN: Final[list[str]] = ["start", "center", "end", "justify", "left", "right"]
VALID_BREAK: Final[list[str]] = ["auto", "page", "even-page", "odd-page", "inherit"]
VALID_LEVEL: Final[list[str]] = ["1", "2", "3", "4"]
@@ -1046,8 +1028,6 @@ class ODTParagraphStyle:
"opacity": ["loext", None],
}
return
@property
def name(self) -> str:
return self._name
@@ -1059,7 +1039,6 @@ class ODTParagraphStyle:
def setName(self, name: str) -> None:
"""Set the paragraph style name."""
self._name = name
return
##
# Attribute Setters
@@ -1068,17 +1047,14 @@ class ODTParagraphStyle:
def setDisplayName(self, value: str | None) -> None:
"""Set style display name."""
self._mAttr["display-name"][1] = value
return
def setParentStyleName(self, value: str | None) -> None:
"""Set parent style name."""
self._mAttr["parent-style-name"][1] = value
return
def setNextStyleName(self, value: str | None) -> None:
"""Set next style name."""
self._mAttr["next-style-name"][1] = value
return
def setOutlineLevel(self, value: str | None) -> None:
"""Set paragraph outline level."""
@@ -1086,7 +1062,6 @@ class ODTParagraphStyle:
self._mAttr["default-outline-level"][1] = value
else:
self._mAttr["default-outline-level"][1] = None
return
def setClass(self, value: str | None) -> None:
"""Set paragraph class."""
@@ -1094,7 +1069,6 @@ class ODTParagraphStyle:
self._mAttr["class"][1] = value
else:
self._mAttr["class"][1] = None
return
##
# Paragraph Setters
@@ -1103,32 +1077,26 @@ class ODTParagraphStyle:
def setMarginTop(self, value: str | None) -> None:
"""Set paragraph top margin."""
self._pAttr["margin-top"][1] = value
return
def setMarginBottom(self, value: str | None) -> None:
"""Set paragraph bottom margin."""
self._pAttr["margin-bottom"][1] = value
return
def setMarginLeft(self, value: str | None) -> None:
"""Set paragraph left margin."""
self._pAttr["margin-left"][1] = value
return
def setMarginRight(self, value: str | None) -> None:
"""Set paragraph right margin."""
self._pAttr["margin-right"][1] = value
return
def setTextIndent(self, value: str | None) -> None:
"""Set text indentation."""
self._pAttr["text-indent"][1] = value
return
def setLineHeight(self, value: str | None) -> None:
"""Set line height."""
self._pAttr["line-height"][1] = value
return
def setTextAlign(self, value: str | None) -> None:
"""Set paragraph text alignment."""
@@ -1136,7 +1104,6 @@ class ODTParagraphStyle:
self._pAttr["text-align"][1] = value
else:
self._pAttr["text-align"][1] = None
return
def setBreakBefore(self, value: str | None) -> None:
"""Set page break before policy."""
@@ -1144,7 +1111,6 @@ class ODTParagraphStyle:
self._pAttr["break-before"][1] = value
else:
self._pAttr["break-before"][1] = None
return
def setBreakAfter(self, value: str | None) -> None:
"""Set page break after policy."""
@@ -1152,7 +1118,6 @@ class ODTParagraphStyle:
self._pAttr["break-after"][1] = value
else:
self._pAttr["break-after"][1] = None
return
##
# Text Setters
@@ -1161,17 +1126,14 @@ class ODTParagraphStyle:
def setFontName(self, value: str | None) -> None:
"""Set font name."""
self._tAttr["font-name"][1] = value
return
def setFontFamily(self, value: str | None) -> None:
"""Set font family."""
self._tAttr["font-family"][1] = value
return
def setFontSize(self, value: str | None) -> None:
"""Set font size."""
self._tAttr["font-size"][1] = value
return
def setFontWeight(self, value: str | None) -> None:
"""Set font weight."""
@@ -1179,7 +1141,6 @@ class ODTParagraphStyle:
self._tAttr["font-weight"][1] = value
else:
self._tAttr["font-weight"][1] = None
return
def setColor(self, value: QColor | None) -> None:
"""Set text colour."""
@@ -1189,7 +1150,6 @@ class ODTParagraphStyle:
else:
self._tAttr["color"][1] = None
self._tAttr["opacity"][1] = None
return
##
# Methods
@@ -1233,14 +1193,13 @@ class ODTParagraphStyle:
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)
return
class ODTTextStyle:
"""Wrapper class for the text style setting used by the exporter.
Only the used settings are exposed here to keep the class minimal
and fast.
"""
VALID_WEIGHT: Final[list[str]] = ["normal", "bold", *FONT_WEIGHT_NUM]
VALID_STYLE: Final[list[str]] = ["normal", "italic", "oblique"]
VALID_POS: Final[list[str]] = ["super", "sub"]
@@ -1263,7 +1222,6 @@ class ODTTextStyle:
"text-underline-width": ["style", None],
"text-underline-color": ["style", None],
}
return
@property
def name(self) -> str:
@@ -1279,7 +1237,6 @@ class ODTTextStyle:
self._tAttr["font-weight"][1] = value
else:
self._tAttr["font-weight"][1] = None
return
def setFontStyle(self, value: str | None) -> None:
"""Set text font style."""
@@ -1287,7 +1244,6 @@ class ODTTextStyle:
self._tAttr["font-style"][1] = value
else:
self._tAttr["font-style"][1] = None
return
def setColor(self, value: QColor | None) -> None:
"""Set text colour."""
@@ -1295,7 +1251,6 @@ class ODTTextStyle:
self._tAttr["color"][1] = value.name(QtHexRgb)
else:
self._tAttr["color"][1] = None
return
def setBackgroundColor(self, value: QColor | None) -> None:
"""Set text background colour."""
@@ -1303,7 +1258,6 @@ class ODTTextStyle:
self._tAttr["background-color"][1] = value.name(QtHexRgb)
else:
self._tAttr["background-color"][1] = None
return
def setTextPosition(self, value: str | None) -> None:
"""Set text vertical position."""
@@ -1311,7 +1265,6 @@ class ODTTextStyle:
self._tAttr["text-position"][1] = f"{value} 58%"
else:
self._tAttr["text-position"][1] = None
return
def setStrikeStyle(self, value: str | None) -> None:
"""Set text line-trough style."""
@@ -1319,7 +1272,6 @@ class ODTTextStyle:
self._tAttr["text-line-through-style"][1] = value
else:
self._tAttr["text-line-through-style"][1] = None
return
def setStrikeType(self, value: str | None) -> None:
"""Set text line-through type."""
@@ -1327,7 +1279,6 @@ class ODTTextStyle:
self._tAttr["text-line-through-type"][1] = value
else:
self._tAttr["text-line-through-type"][1] = None
return
def setUnderlineStyle(self, value: str | None) -> None:
"""Set text underline style."""
@@ -1335,7 +1286,6 @@ class ODTTextStyle:
self._tAttr["text-underline-style"][1] = value
else:
self._tAttr["text-underline-style"][1] = None
return
def setUnderlineWidth(self, value: str | None) -> None:
"""Set text underline width."""
@@ -1343,7 +1293,6 @@ class ODTTextStyle:
self._tAttr["text-underline-width"][1] = value
else:
self._tAttr["text-underline-width"][1] = None
return
def setUnderlineColor(self, value: str | None) -> None:
"""Set text underline colour."""
@@ -1351,7 +1300,6 @@ class ODTTextStyle:
self._tAttr["text-underline-color"][1] = value
else:
self._tAttr["text-underline-color"][1] = None
return
##
# Methods
@@ -1365,7 +1313,6 @@ class ODTTextStyle:
})
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)
return
# XML Complex Element Helper Class
@@ -1378,7 +1325,9 @@ X_SPAN_SING = 3
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.
Rules:
@@ -1408,8 +1357,6 @@ class XMLParagraph:
self._rawTxt = ""
self._xRoot.text = ""
return
def appendText(self, text: str) -> None:
"""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
@@ -1424,7 +1371,7 @@ class XMLParagraph:
if c == " ":
nSpaces += 1
continue
elif nSpaces > 0:
if nSpaces > 0:
self._processSpaces(nSpaces)
nSpaces = 0
@@ -1468,8 +1415,6 @@ class XMLParagraph:
# Handle trailing spaces
self._processSpaces(nSpaces)
return
def appendSpan(self, text: str, style: str, link: str) -> None:
"""Append a text span to the XML element. The span is always
closed since we do not produce nested spans (like Libre Office).
@@ -1491,7 +1436,6 @@ class XMLParagraph:
self._nState = X_SPAN_TEXT
self.appendText(text)
self._nState = X_ROOT_TAIL
return
def appendNode(self, xNode: ET.Element | None) -> None:
"""Append an XML node to the paragraph. We only check for the
@@ -1504,7 +1448,6 @@ class XMLParagraph:
self._xTail = xNode
self._xTail.tail = ""
self._nState = X_ROOT_TAIL
return
def checkError(self) -> tuple[int, str]:
"""Check that the number of characters written matches the
@@ -1575,5 +1518,3 @@ class XMLParagraph:
self._xSing.tail = ""
self._nState = X_SPAN_SING
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -56,6 +56,7 @@ T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
"""Insert a new block if not at the beginning of the document."""
if cursor.position() > 0:
cursor.insertBlock(bFmt)
else:
@@ -63,7 +64,7 @@ def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
class ToQTextDocument(Tokenizer):
"""Core: QTextDocument Writer
"""Core: QTextDocument Writer.
Extend the Tokenizer class to generate a QTextDocument output. This
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._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
return
##
# Properties
##
@@ -113,17 +112,14 @@ class ToQTextDocument(Tokenizer):
"""Set the document page size and margins in millimetres."""
self._pageSize = QPageSize(QSizeF(width, height), QPageSize.Unit.Millimeter)
self._pageMargins = QMarginsF(left, top, right, bottom)
return
def setShowNewPage(self, state: bool) -> None:
"""Add markers for page breaks."""
self._newPage = state
return
def disableAnchors(self) -> None:
"""Disable anchors for when writing to file."""
self._anchors = False
return
##
# Class Methods
@@ -200,8 +196,6 @@ class ToQTextDocument(Tokenizer):
self._init = True
return
def doConvert(self) -> None:
"""Write text tokens into the document."""
if not self._init:
@@ -297,8 +291,6 @@ class ToQTextDocument(Tokenizer):
self._document.setPageSize(printer.pageRect(QPrinter.Unit.DevicePixel).size())
self._document.print(printer)
return
def closeDocument(self) -> None:
"""Run close document tasks."""
self._document.blockSignals(True)
@@ -333,8 +325,6 @@ class ToQTextDocument(Tokenizer):
self._document.blockSignals(False)
return
##
# Internal Functions
##
@@ -439,8 +429,6 @@ class ToQTextDocument(Tokenizer):
# Insert whatever is left in the buffer
cursor.insertText(stripEscape(temp[start:]), cFmt)
return
def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
"""Insert a new page marker."""
if self._newPage:
@@ -475,8 +463,6 @@ class ToQTextDocument(Tokenizer):
if root := self._document.rootFrame():
cursor.swap(root.lastCursorPosition())
return
def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
"""Generate a heading style set."""
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
class ToRaw(Tokenizer):
"""Core: Raw novelWriter Text Writer
"""Core: Raw novelWriter Text Writer.
A class that will collect the minimally altered original source text
and write it to either a text or JSON file.
@@ -51,7 +51,6 @@ class ToRaw(Tokenizer):
super().__init__(project)
self._keepRaw = True
self._noTokens = True
return
def doConvert(self) -> None:
"""No conversion to perform."""
@@ -86,10 +85,7 @@ class ToRaw(Tokenizer):
logger.info("Wrote file: %s", path)
return
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None:
"""Replace tabs with spaces."""
spaces = spaceChar*nSpaces
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import bisect
@@ -98,7 +98,7 @@ class _TagAction(IntFlag):
class GuiDocEditor(QPlainTextEdit):
"""Gui Widget: Main Document Editor"""
"""Gui Widget: Main Document Editor."""
__slots__ = (
"_autoReplace", "_completer", "_doReplace", "_docChanged", "_docHandle", "_followTag1",
@@ -236,8 +236,6 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Ready: GuiDocEditor")
return
##
# Properties
##
@@ -290,15 +288,12 @@ class GuiDocEditor(QPlainTextEdit):
self.itemHandleChanged.emit("")
return
def updateTheme(self) -> None:
"""Update theme elements."""
self.docSearch.updateTheme()
self.docHeader.updateTheme()
self.docFooter.updateTheme()
self.docToolBar.updateTheme()
return
def updateSyntaxColors(self) -> None:
"""Update the syntax highlighting theme."""
@@ -323,8 +318,6 @@ class GuiDocEditor(QPlainTextEdit):
self._selection.format.setBackground(self._lineColor)
self._selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True)
return
def initEditor(self) -> None:
"""Initialise or re-initialise the editor with the user's
settings. This function is both called when the editor is
@@ -392,8 +385,6 @@ class GuiDocEditor(QPlainTextEdit):
else:
self.clearEditor()
return
def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
"""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
@@ -471,7 +462,6 @@ class GuiDocEditor(QPlainTextEdit):
self.updateDocMargins()
self.setDocumentChanged(True)
QApplication.restoreOverrideCursor()
return
def saveText(self) -> bool:
"""Save the text currently in the editor to the NWDocument
@@ -539,7 +529,6 @@ class GuiDocEditor(QPlainTextEdit):
vBar.setValue(vBar.value() + 1)
count += 1
QApplication.processEvents()
return
def updateDocMargins(self) -> None:
"""Automatically adjust the margins so the text is centred if
@@ -580,8 +569,6 @@ class GuiDocEditor(QPlainTextEdit):
lM = max(self._vpMargin, fH)
self.setViewportMargins(tM, uM, tM, lM)
return
##
# Getters
##
@@ -591,20 +578,19 @@ class GuiDocEditor(QPlainTextEdit):
QTextDocument->toRawText instead of toPlainText. The former preserves
non-breaking spaces, the latter does not. We still want to get rid of
paragraph and line separators though.
See: https://doc.qt.io/qt-6/qtextdocument.html#toPlainText
"""
text = self._qDocument.toRawText()
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
return text
return text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
def getSelectedText(self) -> str:
"""Get currently selected text."""
if (cursor := self.textCursor()).hasSelection():
text = cursor.selectedText()
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
return text
return text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
return ""
def getCursorPosition(self) -> int:
@@ -625,7 +611,6 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Document changed status is '%s'", state)
self._docChanged = state
self.editedStatusChanged.emit(self._docChanged)
return
def setCursorPosition(self, position: int) -> None:
"""Move the cursor to a given position in the document."""
@@ -634,14 +619,12 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(minmax(position, 0, chars-1))
self.setTextCursor(cursor)
self.centerCursor()
return
def saveCursorPosition(self) -> None:
"""Save the cursor position to the current project item."""
if self._nwItem is not None:
cursPos = self.getCursorPosition()
self._nwItem.setCursorPos(cursPos)
return
def setCursorLine(self, line: int | None) -> None:
"""Move the cursor to a given line in the document."""
@@ -650,7 +633,6 @@ class GuiDocEditor(QPlainTextEdit):
if block:
self.setCursorPosition(block.position())
logger.debug("Cursor moved to line %d", line)
return
def setCursorSelection(self, start: int, length: int) -> None:
"""Make a text selection."""
@@ -659,14 +641,15 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(start, QtMoveAnchor)
cursor.setPosition(start + length, QtKeepAnchor)
self.setTextCursor(cursor)
return
##
# Spell Checking
##
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.
If the spell check state is not defined (None), then toggle the
current status saved in this class.
@@ -690,8 +673,6 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Spell check is set to '%s'", str(state))
return
def spellCheckDocument(self) -> None:
"""Rerun the highlighter to update spell checking status of the
currently loaded text.
@@ -703,7 +684,6 @@ class GuiDocEditor(QPlainTextEdit):
QApplication.restoreOverrideCursor()
logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
self.updateStatusMessage.emit(self.tr("Spell check complete"))
return
##
# General Class Methods
@@ -832,7 +812,6 @@ class GuiDocEditor(QPlainTextEdit):
details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation),
log=False
)
return
def insertText(self, insert: str | nwDocInsert) -> None:
"""Insert a specific type of text at the cursor position."""
@@ -974,7 +953,6 @@ class GuiDocEditor(QPlainTextEdit):
event.acceptProposedAction()
else:
super().dragEnterEvent(event)
return
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
"""Overload drag move event to handle dragged items."""
@@ -982,7 +960,6 @@ class GuiDocEditor(QPlainTextEdit):
event.acceptProposedAction()
else:
super().dragMoveEvent(event)
return
def dropEvent(self, event: QDropEvent) -> None:
"""Overload drop event to handle dragged items."""
@@ -992,7 +969,6 @@ class GuiDocEditor(QPlainTextEdit):
self.openDocumentRequest.emit(handles[0], nwDocMode.EDIT, "", True)
else:
super().dropEvent(event)
return
def focusNextPrevChild(self, _next: bool) -> bool:
"""Capture the focus request from the tab key on the text
@@ -1019,7 +995,6 @@ class GuiDocEditor(QPlainTextEdit):
else:
self._processTag(cursor)
super().mouseReleaseEvent(event)
return
def resizeEvent(self, event: QResizeEvent) -> None:
"""If the text editor is resized, we must make sure the document
@@ -1027,7 +1002,6 @@ class GuiDocEditor(QPlainTextEdit):
"""
self.updateDocMargins()
super().resizeEvent(event)
return
##
# Public Slots
@@ -1035,14 +1009,13 @@ class GuiDocEditor(QPlainTextEdit):
@pyqtSlot(str, Enum)
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
"""Called when an item label is changed to check if the document
title bar needs updating,
"""Process project item change. Called when an item label is
changed to check if the document title bar needs updating.
"""
if tHandle == self._docHandle and change == nwChange.UPDATE:
self.docHeader.setHandle(tHandle)
self.docFooter.updateInfo()
self.updateDocMargins()
return
@pyqtSlot(str)
def insertKeyWord(self, keyword: str) -> bool:
@@ -1053,8 +1026,7 @@ class GuiDocEditor(QPlainTextEdit):
logger.error("Invalid keyword '%s'", keyword)
return False
logger.debug("Inserting keyword '%s'", keyword)
state = self.insertNewBlock(f"{keyword}: ")
return state
return self.insertNewBlock(f"{keyword}: ")
@pyqtSlot()
def toggleSearch(self) -> None:
@@ -1063,14 +1035,12 @@ class GuiDocEditor(QPlainTextEdit):
self.closeSearch()
else:
self.beginSearch()
return
@pyqtSlot(list, list)
def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
"""Tags have changed, so just in case we rehighlight them."""
if updated or deleted:
self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META)
return
##
# Private Slots
@@ -1116,8 +1086,6 @@ class GuiDocEditor(QPlainTextEdit):
if self._autoReplace.process(text, cursor):
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
return
@pyqtSlot()
def _cursorMoved(self) -> None:
"""Triggered when the cursor moved in the editor."""
@@ -1126,7 +1094,6 @@ class GuiDocEditor(QPlainTextEdit):
self._selection.cursor = self.textCursor()
self._selection.cursor.clearSelection()
self.setExtraSelections([self._selection])
return
@pyqtSlot(int, int, str)
def _insertCompletion(self, pos: int, length: int, text: str) -> None:
@@ -1138,13 +1105,11 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(check + length, QtKeepAnchor)
cursor.insertText(text)
self._completer.hide()
return
@pyqtSlot()
def _openContextFromCursor(self) -> None:
"""Open the spell check context menu at the cursor."""
self._openContextMenu(self.cursorRect().center())
return
@pyqtSlot("QPoint")
def _openContextMenu(self, pos: QPoint) -> None:
@@ -1231,8 +1196,6 @@ class GuiDocEditor(QPlainTextEdit):
ctxMenu.setParent(None)
return
@pyqtSlot()
def _runDocumentTasks(self) -> None:
"""Run timer document tasks."""
@@ -1269,11 +1232,10 @@ class GuiDocEditor(QPlainTextEdit):
if not self.textCursor().hasSelection():
# Selection counter should take precedence (#2155)
self.docFooter.updateMainCount(mCount, False)
return
@pyqtSlot()
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.
"""
if self.textCursor().hasSelection():
@@ -1282,7 +1244,6 @@ class GuiDocEditor(QPlainTextEdit):
else:
self._timerSel.stop()
self.docFooter.updateMainCount(0, False)
return
@pyqtSlot()
def _runSelCounter(self) -> None:
@@ -1300,14 +1261,12 @@ class GuiDocEditor(QPlainTextEdit):
if self._docHandle and self._nwItem:
self.docFooter.updateMainCount(cCount if CONFIG.useCharCount else wCount, True)
self._timerSel.stop()
return
@pyqtSlot()
def _closeCurrentDocument(self) -> None:
"""Close the document. Forwarded to the main Gui."""
self.closeEditorRequest.emit()
self.docToolBar.setVisible(False)
return
@pyqtSlot()
def _toggleToolBarVisibility(self) -> None:
@@ -1315,7 +1274,6 @@ class GuiDocEditor(QPlainTextEdit):
state = not self.docToolBar.isVisible()
self.docToolBar.setVisible(state)
CONFIG.showEditToolBar = state
return
##
# Search & Replace
@@ -1326,14 +1284,12 @@ class GuiDocEditor(QPlainTextEdit):
self.docSearch.setSearchText(self.getSelectedText() or None)
resS, _ = self.findAllOccurences()
self.docSearch.setResultCount(None, len(resS))
return
def beginReplace(self) -> None:
"""Initialise the search box and reset the replace text box."""
self.beginSearch()
self.docSearch.setReplaceText("")
self.updateDocMargins()
return
def findNext(self, goBack: bool = False) -> None:
"""Search for the next or previous occurrence of the search bar
@@ -1621,8 +1577,6 @@ class GuiDocEditor(QPlainTextEdit):
self.setTextCursor(cursor)
return
def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> None:
"""Replace all straight quotes in the selected text."""
cursor = self.textCursor()
@@ -1882,8 +1836,6 @@ class GuiDocEditor(QPlainTextEdit):
cursor.insertText(cleanText.rstrip() + "\n")
cursor.endEditBlock()
return
def _insertCommentStructure(self, style: nwComment) -> None:
"""Insert a shortcut/comment combo."""
if self._docHandle and style == nwComment.FOOTNOTE:
@@ -1925,7 +1877,6 @@ class GuiDocEditor(QPlainTextEdit):
cursor.endEditBlock()
cursor.setPosition(pos)
self.setTextCursor(cursor)
return
def _addWord(self, word: str, block: QTextBlock, save: bool) -> None:
"""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")
SHARED.spelling.addWord(word, save=save)
self._qDocument.syntaxHighlighter.rehighlightBlock(block)
return
def _processTag(
self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False
@@ -2008,7 +1958,6 @@ class GuiDocEditor(QPlainTextEdit):
if self._docHandle:
text = block.text().lstrip("#").lstrip("!").strip()
self.requestProjectItemRenamed.emit(self._docHandle, text)
return
def _autoSelect(self) -> QTextCursor:
"""Return a cursor which may or may not have a selection based
@@ -2078,14 +2027,11 @@ class GuiDocEditor(QPlainTextEdit):
self.setTextCursor(cursor)
return
def _makePosSelection(self, mode: QTextCursor.SelectionType, pos: QPoint) -> None:
"""Select text based on selection mode, but first move cursor."""
cursor = self.cursorForPosition(pos)
self.setTextCursor(cursor)
self._makeSelection(mode)
return
def _allowAutoReplace(self, state: bool) -> None:
"""Enable/disable the auto-replace feature temporarily."""
@@ -2093,11 +2039,10 @@ class GuiDocEditor(QPlainTextEdit):
self._doReplace = CONFIG.doReplace
else:
self._doReplace = False
return
class CommandCompleter(QMenu):
"""GuiWidget: Command Completer Menu
"""GuiWidget: Command Completer Menu.
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
@@ -2109,7 +2054,6 @@ class CommandCompleter(QMenu):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
return
def updateMetaText(self, text: str, pos: int) -> bool:
"""Update the menu options based on the line of text."""
@@ -2203,7 +2147,6 @@ class CommandCompleter(QMenu):
super().keyPressEvent(event)
elif isinstance(parent, GuiDocEditor):
parent.keyPressEvent(event)
return
##
# Internal Functions
@@ -2212,11 +2155,10 @@ class CommandCompleter(QMenu):
def _emitComplete(self, pos: int, length: int, value: str) -> None:
"""Emit the signal to indicate a selection has been made."""
self.complete.emit(pos, length, value)
return
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
main GUI thread.
@@ -2228,9 +2170,9 @@ class BackgroundWordCounter(QRunnable):
self._forSelection = forSelection
self._isRunning = False
self.signals = BackgroundWordCounterSignals()
return
def isRunning(self) -> bool:
"""Return True if the word counter is already running."""
return self._isRunning
@pyqtSlot()
@@ -2248,17 +2190,17 @@ class BackgroundWordCounter(QRunnable):
self.signals.countsReady.emit(cC, wC, pC)
self._isRunning = False
return
class BackgroundWordCounterSignals(QObject):
"""The QRunnable cannot emit a signal, so we need a simple QObject
to hold the word counter signal.
"""
countsReady = pyqtSignal(int, int, int)
class TextAutoReplace:
"""Encapsulates the editor auto replace feature."""
__slots__ = (
"_doPadAfter", "_doPadBefore", "_padAfter", "_padBefore", "_padChar",
@@ -2268,7 +2210,6 @@ class TextAutoReplace:
def __init__(self) -> None:
self.initSettings()
return
def initSettings(self) -> None:
"""Initialise the auto-replace settings from config."""
@@ -2287,7 +2228,6 @@ class TextAutoReplace:
self._padAfter = CONFIG.fmtPadAfter
self._doPadBefore = bool(CONFIG.fmtPadBefore)
self._doPadAfter = bool(CONFIG.fmtPadAfter)
return
def process(self, text: str, cursor: QTextCursor) -> bool:
"""Auto-replace text elements based on main configuration.
@@ -2401,7 +2341,7 @@ class TextAutoReplace:
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
header.
@@ -2513,8 +2453,6 @@ class GuiDocToolBar(QWidget):
logger.debug("Ready: GuiDocToolBar")
return
def updateTheme(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
syntax = SHARED.theme.syntaxTheme
@@ -2537,11 +2475,9 @@ class GuiDocToolBar(QWidget):
self.tbSuperscript.setThemeIcon("fmt_superscript")
self.tbSubscript.setThemeIcon("fmt_subscript")
return
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
QTextEdit's viewport.
@@ -2671,8 +2607,6 @@ class GuiDocEditSearch(QFrame):
logger.debug("Ready: GuiDocEditSearch")
return
##
# Properties
##
@@ -2721,14 +2655,12 @@ class GuiDocEditSearch(QFrame):
self.searchBox.selectAll()
if CONFIG.searchRegEx:
self._alertSearchValid(True)
return
def setReplaceText(self, text: str) -> None:
"""Set the replace text."""
self.showReplace.setChecked(True)
self.replaceBox.setFocus()
self.replaceBox.setText(text)
return
def setResultCount(self, currRes: int | None, resCount: int | None) -> None:
"""Set the count values for the current search."""
@@ -2743,7 +2675,6 @@ class GuiDocEditSearch(QFrame):
self.resultLabel.setMinimumWidth(minWidth)
self.adjustSize()
self.docEditor.updateDocMargins()
return
##
# Methods
@@ -2759,7 +2690,6 @@ class GuiDocEditSearch(QFrame):
self.resultLabel.setMinimumWidth(
SHARED.theme.getTextWidth("?/?", SHARED.theme.guiFontSmall)
)
return
def updateTheme(self) -> None:
"""Update theme elements."""
@@ -2784,11 +2714,9 @@ class GuiDocEditSearch(QFrame):
self.searchOpt.setStyleSheet("QToolBar {padding: 0;}")
self.showReplace.setStyleSheet("QToolButton {border: none; background: transparent;}")
return
def cycleFocus(self) -> bool:
"""The tab key just alternates focus between the two input
boxes, if the replace box is visible.
"""Cycle focus on tab key press. This just alternates focus
between the two input boxes, if the replace box is visible.
"""
if self.searchBox.hasFocus():
self.replaceBox.setFocus()
@@ -2813,7 +2741,6 @@ class GuiDocEditSearch(QFrame):
self.setVisible(False)
self.docEditor.updateDocMargins()
self.docEditor.setFocus()
return
##
# Private Slots
@@ -2823,13 +2750,11 @@ class GuiDocEditSearch(QFrame):
def _doSearch(self) -> None:
"""Call the search action function for the document editor."""
self.docEditor.findNext(goBack=(QApplication.keyboardModifiers() == QtModShift))
return
@pyqtSlot()
def _doReplace(self) -> None:
"""Call the replace action function for the document editor."""
self.docEditor.replaceNext()
return
@pyqtSlot(bool)
def _doToggleReplace(self, state: bool) -> None:
@@ -2838,43 +2763,36 @@ class GuiDocEditSearch(QFrame):
self.replaceButton.setVisible(state)
self.adjustSize()
self.docEditor.updateDocMargins()
return
@pyqtSlot(bool)
def _doToggleCase(self, state: bool) -> None:
"""Enable/disable case sensitive mode."""
CONFIG.searchCase = state
return
@pyqtSlot(bool)
def _doToggleWord(self, state: bool) -> None:
"""Enable/disable whole word search mode."""
CONFIG.searchWord = state
return
@pyqtSlot(bool)
def _doToggleRegEx(self, state: bool) -> None:
"""Enable/disable regular expression search mode."""
CONFIG.searchRegEx = state
return
@pyqtSlot(bool)
def _doToggleLoop(self, state: bool) -> None:
"""Enable/disable looping the search."""
CONFIG.searchLoop = state
return
@pyqtSlot(bool)
def _doToggleProject(self, state: bool) -> None:
"""Enable/disable continuing search in next project file."""
CONFIG.searchNextFile = state
return
@pyqtSlot(bool)
def _doToggleMatchCap(self, state: bool) -> None:
"""Enable/disable preserving capitalisation when replacing."""
CONFIG.searchMatchCap = state
return
##
# Internal Functions
@@ -2890,11 +2808,10 @@ class GuiDocEditSearch(QFrame):
palette.text().color() if isValid else SHARED.theme.errorText
)
self.searchBox.setPalette(palette)
return
class GuiDocEditHeader(QWidget):
"""The Embedded Document Header
"""The Embedded Document Header.
Only used by DocEditor, and is at a fixed position in the
QTextEdit's viewport.
@@ -2985,8 +2902,6 @@ class GuiDocEditHeader(QWidget):
logger.debug("Ready: GuiDocEditHeader")
return
##
# Methods
##
@@ -3003,7 +2918,6 @@ class GuiDocEditHeader(QWidget):
self.searchButton.setVisible(False)
self.closeButton.setVisible(False)
self.minmaxButton.setVisible(False)
return
def setOutline(self, data: dict[int, str]) -> None:
"""Set the document outline dataset."""
@@ -3015,13 +2929,11 @@ class GuiDocEditHeader(QWidget):
action.triggered.connect(qtLambda(self._gotoBlock, number))
self._docOutline = data
logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
return
def updateFont(self) -> None:
"""Update the font settings."""
self.setFont(SHARED.theme.guiFont)
self.itemTitle.setFont(SHARED.theme.guiFontSmall)
return
def updateTheme(self) -> None:
"""Update theme elements."""
@@ -3040,8 +2952,6 @@ class GuiDocEditHeader(QWidget):
self.matchColors()
return
def matchColors(self) -> None:
"""Update the colours of the widget to match those of the syntax
theme rather than the main GUI.
@@ -3055,12 +2965,10 @@ class GuiDocEditHeader(QWidget):
self.itemTitle.setTextColors(
color=palette.windowText().color(), faded=SHARED.theme.fadedText
)
return
def changeFocusState(self, state: bool) -> None:
"""Toggle focus state."""
self.itemTitle.setColorState(state)
return
def setHandle(self, tHandle: str) -> None:
"""Set the document title from the handle, or alternatively, set
@@ -3081,8 +2989,6 @@ class GuiDocEditHeader(QWidget):
self.closeButton.setVisible(True)
self.minmaxButton.setVisible(True)
return
##
# Private Slots
##
@@ -3092,19 +2998,16 @@ class GuiDocEditHeader(QWidget):
"""Trigger the close editor on the main window."""
self.clearHeader()
self.closeDocumentRequest.emit()
return
@pyqtSlot(int)
def _gotoBlock(self, blockNumber: int) -> None:
"""Move cursor to a specific heading."""
self.docEditor.setCursorLine(blockNumber + 1)
return
@pyqtSlot(bool)
def _focusModeChanged(self, focusMode: bool) -> None:
"""Update minimise/maximise icon of the Focus Mode button."""
self.minmaxButton.setThemeIcon("minimise" if focusMode else "maximise", "blue")
return
##
# Events
@@ -3116,11 +3019,10 @@ class GuiDocEditHeader(QWidget):
"""
if event.button() == QtMouseLeft:
self.docEditor.requestProjectItemSelected.emit(self._docHandle or "", True)
return
class GuiDocEditFooter(QWidget):
"""The Embedded Document Footer
"""The Embedded Document Footer.
Only used by DocEditor, and is at a fixed position in the
QTextEdit's viewport.
@@ -3205,8 +3107,6 @@ class GuiDocEditFooter(QWidget):
logger.debug("Ready: GuiDocEditFooter")
return
##
# Methods
##
@@ -3216,7 +3116,6 @@ class GuiDocEditFooter(QWidget):
self._trMainCount = trStats(nwLabels.STATS_DISPLAY[
nwStats.CHARS if CONFIG.useCharCount else nwStats.WORDS
])
return
def updateFont(self) -> None:
"""Update the font settings."""
@@ -3224,7 +3123,6 @@ class GuiDocEditFooter(QWidget):
self.statusText.setFont(SHARED.theme.guiFontSmall)
self.linesText.setFont(SHARED.theme.guiFontSmall)
self.wordsText.setFont(SHARED.theme.guiFontSmall)
return
def updateTheme(self) -> None:
"""Update theme elements."""
@@ -3232,7 +3130,6 @@ class GuiDocEditFooter(QWidget):
self.linesIcon.setPixmap(SHARED.theme.getPixmap("lines", (iPx, iPx)))
self.wordsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx)))
self.matchColors()
return
def matchColors(self) -> None:
"""Update the colours of the widget to match those of the syntax
@@ -3250,8 +3147,6 @@ class GuiDocEditFooter(QWidget):
self.linesText.setPalette(palette)
self.wordsText.setPalette(palette)
return
def setHandle(self, tHandle: str | None) -> None:
"""Set the handle that will populate the footer's data."""
self._docHandle = tHandle
@@ -3264,8 +3159,6 @@ class GuiDocEditFooter(QWidget):
self.updateInfo()
self.updateMainCount(0, False)
return
def updateInfo(self) -> None:
"""Update the content of text labels."""
if self._tItem is None:
@@ -3280,8 +3173,6 @@ class GuiDocEditFooter(QWidget):
self.statusIcon.setPixmap(sIcon)
self.statusText.setText(sText)
return
def updateLineCount(self, cursor: QTextCursor) -> None:
"""Update the line and document position counter."""
if document := cursor.document():
@@ -3291,7 +3182,6 @@ class GuiDocEditFooter(QWidget):
self.linesText.setText(
self._trLineCount.format(f"{cLine:n}", f"{100*cPos//cCount:d} %")
)
return
def updateMainCount(self, count: int, selection: bool) -> None:
"""Update main counter information."""
@@ -3304,4 +3194,3 @@ class GuiDocEditFooter(QWidget):
else:
text = self._trMainCount.format("0", "+0")
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -57,6 +57,7 @@ BLOCK_TITLE = 4
class GuiDocHighlighter(QSyntaxHighlighter):
"""GUI: Editor Syntax Highlighter."""
__slots__ = (
"_cmnRules", "_dialogParser", "_hStyles", "_isInactive", "_isNovel",
@@ -85,8 +86,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
logger.debug("Ready: GuiDocHighlighter")
return
def initHighlighter(self) -> None:
"""Initialise the syntax highlighter, setting all the colour
rules and building the RegExes.
@@ -255,8 +254,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
return
##
# Setters
##
@@ -264,7 +261,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
def setSpellCheck(self, state: bool) -> None:
"""Enable/disable the real time spell checker."""
self._spellCheck = state
return
def setHandle(self, tHandle: str) -> None:
"""Set the handle of the currently highlighted document."""
@@ -275,7 +271,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._isNovel = item.isDocumentLayout()
self._isInactive = item.isInactiveClass()
logger.debug("Syntax highlighter enabled for item '%s'", tHandle)
return
##
# Methods
@@ -293,7 +288,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if block.userState() & cType > 0:
self.rehighlightBlock(block)
logger.debug("Document highlighted in %.3f ms" % (1000*(time() - tStart)))
return
##
# Highlight Block
@@ -506,10 +500,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._hStyles[name] = charFormat
return
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")
@@ -519,7 +516,6 @@ class TextBlockData(QTextBlockUserData):
self._offset = 0
self._metaData: list[tuple[int, int, str, str]] = []
self._spellErrors: list[tuple[int, int, str]] = []
return
@property
def metaData(self) -> list[tuple[int, int, str, str]]:
@@ -553,8 +549,6 @@ class TextBlockData(QTextBlockUserData):
self._text = text.replace("\u02bc", "'").replace("_", " ")
self._offset = offset
return
def spellCheck(self, utf16Map: list[int] | None) -> list[tuple[int, int, str]]:
"""Run the spell checker and cache the result, and return the
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -60,6 +60,7 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
"""GUI: Document Viewer."""
closeDocumentRequest = pyqtSignal()
documentLoaded = pyqtSignal(str)
@@ -110,8 +111,6 @@ class GuiDocViewer(QTextBrowser):
logger.debug("Ready: GuiDocViewer")
return
##
# Properties
##
@@ -138,13 +137,11 @@ class GuiDocViewer(QTextBrowser):
self.setSearchPaths([""])
self._docHandle = None
self.docHeader.clearHeader()
return
def updateTheme(self) -> None:
"""Update theme elements."""
self.docHeader.updateTheme()
self.docFooter.updateTheme()
return
def initViewer(self) -> None:
"""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
self.reloadText()
return
def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
"""Load text into the viewer from an item handle."""
if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
@@ -280,7 +275,6 @@ class GuiDocViewer(QTextBrowser):
"""Reload the text in the current document."""
if self._docHandle:
self.loadText(self._docHandle, updateHistory=False)
return
def docAction(self, action: nwDocAction) -> bool:
"""Process document actions on the current document."""
@@ -308,7 +302,6 @@ class GuiDocViewer(QTextBrowser):
def clearNavHistory(self) -> None:
"""Clear the navigation history."""
self.docHistory.clear()
return
def updateDocMargins(self) -> None:
"""Automatically adjust the margins so the text is centred."""
@@ -337,8 +330,6 @@ class GuiDocViewer(QTextBrowser):
self.docFooter.setGeometry(tB, fY, tW, fH)
self.setViewportMargins(tM, max(cM, tH), tM, max(cM, fH))
return
##
# Setters
##
@@ -347,7 +338,6 @@ class GuiDocViewer(QTextBrowser):
"""Set the scrollbar position."""
if (vBar := self.verticalScrollBar()) and vBar.isVisible():
vBar.setValue(pos)
return
##
# Public Slots
@@ -359,7 +349,6 @@ class GuiDocViewer(QTextBrowser):
if tHandle == self._docHandle and change == nwChange.UPDATE:
self.docHeader.setHandle(tHandle)
self.updateDocMargins()
return
@pyqtSlot(str)
def navigateTo(self, anchor: str) -> None:
@@ -367,7 +356,6 @@ class GuiDocViewer(QTextBrowser):
if isinstance(anchor, str) and anchor.startswith("#"):
logger.debug("Moving to anchor '%s'", anchor)
self.setSource(QUrl(anchor))
return
##
# Private Slots
@@ -377,13 +365,11 @@ class GuiDocViewer(QTextBrowser):
def navBackward(self) -> None:
"""Navigate backwards in the document view history."""
self.docHistory.backward()
return
@pyqtSlot()
def navForward(self) -> None:
"""Navigate forwards in the document view history."""
self.docHistory.forward()
return
@pyqtSlot("QUrl")
def _linkClicked(self, url: QUrl) -> None:
@@ -396,7 +382,6 @@ class GuiDocViewer(QTextBrowser):
self.navigateTo(link)
elif link.startswith("http"):
QDesktopServices.openUrl(QUrl(url))
return
@pyqtSlot("QPoint")
def _openContextMenu(self, point: QPoint) -> None:
@@ -430,8 +415,6 @@ class GuiDocViewer(QTextBrowser):
ctxMenu.setParent(None)
return
##
# Events
##
@@ -440,7 +423,6 @@ class GuiDocViewer(QTextBrowser):
"""Update document margins when widget is resized."""
self.updateDocMargins()
super().resizeEvent(event)
return
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
"""Capture mouse click events on the document."""
@@ -450,7 +432,6 @@ class GuiDocViewer(QTextBrowser):
self.navForward()
else:
super().mouseReleaseEvent(event)
return
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
"""Overload drag enter event to handle dragged items."""
@@ -458,7 +439,6 @@ class GuiDocViewer(QTextBrowser):
event.acceptProposedAction()
else:
super().dragEnterEvent(event)
return
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
"""Overload drag move event to handle dragged items."""
@@ -466,7 +446,6 @@ class GuiDocViewer(QTextBrowser):
event.acceptProposedAction()
else:
super().dragMoveEvent(event)
return
def dropEvent(self, event: QDropEvent) -> None:
"""Overload drop event to handle dragged items."""
@@ -476,7 +455,6 @@ class GuiDocViewer(QTextBrowser):
self.openDocumentRequest.emit(handles[0], nwDocMode.VIEW, "", True)
else:
super().dropEvent(event)
return
##
# Internal Functions
@@ -500,16 +478,18 @@ class GuiDocViewer(QTextBrowser):
self.setTextCursor(cursor)
return
def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None:
"""Handle text selection at a given location."""
self.setTextCursor(self.cursorForPosition(pos))
self._makeSelection(selType)
return
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:
self.docViewer = docViewer
@@ -517,7 +497,6 @@ class GuiDocViewHistory:
self._posHistory = []
self._currPos = -1
self._prevPos = -1
return
def clear(self) -> None:
"""Clear the view history."""
@@ -526,7 +505,6 @@ class GuiDocViewHistory:
self._posHistory = []
self._currPos = -1
self._prevPos = -1
return
def append(self, tHandle: str) -> bool:
"""Append a document handle and its scroll bar position to the
@@ -566,7 +544,6 @@ class GuiDocViewHistory:
self._currPos = newPos
self._updateNavButtons()
self._dumpHistory()
return
def backward(self) -> None:
"""Navigate to the previous entry in the view history."""
@@ -580,7 +557,6 @@ class GuiDocViewHistory:
self._currPos = newPos
self._updateNavButtons()
self._dumpHistory()
return
##
# Internal Functions
@@ -590,12 +566,10 @@ class GuiDocViewHistory:
"""Update the scrollbar position of the previous entry."""
if self._prevPos >= 0 and self._prevPos < len(self._posHistory):
self._posHistory[self._prevPos] = self.docViewer.scrollPosition
return
def _updateNavButtons(self) -> None:
"""Update the navigation buttons in the document header."""
self.docViewer.docHeader.updateNavButtons(0, len(self._navHistory) - 1, self._currPos)
return
def _truncateHistory(self, atPos: int) -> None:
"""Truncate the navigation history to the given position. Also
@@ -606,7 +580,6 @@ class GuiDocViewHistory:
self._posHistory = self._posHistory[nSkip:atPos + 1]
self._currPos -= nSkip
self._prevPos -= nSkip
return
def _dumpHistory(self) -> None:
"""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)):
a = ">" if i == self._currPos else " "
logger.debug(f"History {i + 1:02d}: {a} {h:13s} [x:{p}]")
return
class GuiDocViewHeader(QWidget):
"""The Embedded Document Header
"""The Embedded Document Header.
Only used by DocViewer, and is at a fixed position in the
QTextBrowser's viewport.
@@ -711,8 +683,6 @@ class GuiDocViewHeader(QWidget):
logger.debug("Ready: GuiDocViewHeader")
return
##
# Methods
##
@@ -730,7 +700,6 @@ class GuiDocViewHeader(QWidget):
self.editButton.setVisible(False)
self.refreshButton.setVisible(False)
self.closeButton.setVisible(False)
return
def setOutline(self, data: dict[str, tuple[str, int]]) -> None:
"""Set the document outline dataset."""
@@ -750,13 +719,11 @@ class GuiDocViewHeader(QWidget):
lambda _, title=title: self.docViewer.navigateTo(f"#{tHandle}:{title}")
)
self._docOutline = data
return
def updateFont(self) -> None:
"""Update the font settings."""
self.setFont(SHARED.theme.guiFont)
self.itemTitle.setFont(SHARED.theme.guiFontSmall)
return
def updateTheme(self) -> None:
"""Update theme elements."""
@@ -777,8 +744,6 @@ class GuiDocViewHeader(QWidget):
self.matchColors()
return
def matchColors(self) -> None:
"""Update the colours of the widget to match those of the syntax
theme rather than the main GUI.
@@ -792,15 +757,13 @@ class GuiDocViewHeader(QWidget):
self.itemTitle.setTextColors(
color=palette.windowText().color(), faded=SHARED.theme.fadedText
)
return
def changeFocusState(self, state: bool) -> None:
"""Toggle focus state."""
self.itemTitle.setColorState(state)
return
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.
"""
self._docHandle = tHandle
@@ -819,13 +782,10 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setVisible(True)
self.closeButton.setVisible(True)
return
def updateNavButtons(self, firstIdx: int, lastIdx: int, currIdx: int) -> None:
"""Enable and disable nav buttons based on index in history."""
self.backButton.setEnabled(currIdx > firstIdx)
self.forwardButton.setEnabled(currIdx < lastIdx)
return
##
# Private Slots
@@ -836,20 +796,17 @@ class GuiDocViewHeader(QWidget):
"""Trigger the close editor/viewer on the main window."""
self.clearHeader()
self.docViewer.closeDocumentRequest.emit()
return
@pyqtSlot()
def _refreshDocument(self) -> None:
"""Reload the content of the document."""
self.docViewer.reloadDocumentRequest.emit()
return
@pyqtSlot()
def _editDocument(self) -> None:
"""Open the document in the editor."""
if tHandle := self._docHandle:
self.docViewer.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
return
##
# Events
@@ -861,11 +818,10 @@ class GuiDocViewHeader(QWidget):
"""
if event.button() == QtMouseLeft:
self.docViewer.requestProjectItemSelected.emit(self._docHandle, True)
return
class GuiDocViewFooter(QWidget):
"""The Embedded Document Footer
"""The Embedded Document Footer.
Only used by DocViewer, and is at a fixed position in the
QTextBrowser's viewport.
@@ -944,8 +900,6 @@ class GuiDocViewFooter(QWidget):
logger.debug("Ready: GuiDocViewFooter")
return
##
# Methods
##
@@ -956,7 +910,6 @@ class GuiDocViewFooter(QWidget):
self.showComments.setFont(SHARED.theme.guiFontSmall)
self.showSynopsis.setFont(SHARED.theme.guiFontSmall)
self.showNotes.setFont(SHARED.theme.guiFontSmall)
return
def updateTheme(self) -> None:
"""Update theme elements."""
@@ -977,8 +930,6 @@ class GuiDocViewFooter(QWidget):
self.matchColors()
return
def matchColors(self) -> None:
"""Update the colours of the widget to match those of the syntax
theme rather than the main GUI.
@@ -989,7 +940,6 @@ class GuiDocViewFooter(QWidget):
palette.setColor(QPalette.ColorRole.WindowText, syntax.text)
palette.setColor(QPalette.ColorRole.Text, syntax.text)
self.setPalette(palette)
return
##
# Private Slots
@@ -1000,18 +950,15 @@ class GuiDocViewFooter(QWidget):
"""Toggle the view comment button and reload the document."""
CONFIG.viewComments = state
self.docViewer.reloadText()
return
@pyqtSlot(bool)
def _doToggleSynopsis(self, state: bool) -> None:
"""Toggle the view synopsis button and reload the document."""
CONFIG.viewSynopsis = state
self.docViewer.reloadText()
return
@pyqtSlot(bool)
def _doToggleNotes(self, state: bool) -> None:
"""Toggle the view notes button and reload the document."""
CONFIG.viewNotes = state
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -49,6 +49,10 @@ logger = logging.getLogger(__name__)
class GuiDocViewerPanel(QWidget):
"""GUI: Document Viewer Panel.
The panel of project meta data below the viewer.
"""
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
loadDocumentTagRequest = pyqtSignal(str, Enum)
@@ -96,8 +100,6 @@ class GuiDocViewerPanel(QWidget):
logger.debug("Ready: GuiDocViewerPanel")
return
##
# Methods
##
@@ -113,7 +115,6 @@ class GuiDocViewerPanel(QWidget):
for tab in self.kwTabs.values():
tab.updateTheme()
self._loadAllTags()
return
def openProjectTasks(self) -> None:
"""Run open project tasks."""
@@ -124,7 +125,6 @@ class GuiDocViewerPanel(QWidget):
for key, value in colWidths.items():
if key in self.kwTabs and isinstance(value, list):
self.kwTabs[key].setColumnWidths(value)
return
def closeProjectTasks(self) -> None:
"""Run close project tasks."""
@@ -133,7 +133,6 @@ class GuiDocViewerPanel(QWidget):
hideInactive = self.aInactive.isChecked()
SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", colWidths)
SHARED.project.options.setValue("GuiDocViewerPanel", "hideInactive", hideInactive)
return
##
# Public Slots
@@ -145,7 +144,6 @@ class GuiDocViewerPanel(QWidget):
self.tabBackRefs.clearContent()
for cTab in self.kwTabs.values():
cTab.clearContent()
return
@pyqtSlot()
def indexHasAppeared(self) -> None:
@@ -153,7 +151,6 @@ class GuiDocViewerPanel(QWidget):
self._loadAllTags()
self._updateTabVisibility()
self.updateHandle(self._lastHandle)
return
@pyqtSlot(str, Enum)
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
@@ -168,14 +165,12 @@ class GuiDocViewerPanel(QWidget):
else:
self.kwTabs[tClass].removeEntry(key)
self._updateTabVisibility()
return
@pyqtSlot(str)
def updateHandle(self, tHandle: str | None) -> None:
"""Update the document handle."""
self._lastHandle = tHandle
self.tabBackRefs.refreshContent(tHandle or None)
return
@pyqtSlot(list, list)
def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
@@ -191,14 +186,12 @@ class GuiDocViewerPanel(QWidget):
else:
logger.warning("Could not remove tag '%s' from view panel", key)
self._updateTabVisibility()
return
@pyqtSlot(str)
def updateStatusLabels(self, kind: str) -> None:
"""Update the importance labels."""
if kind == "i":
self._loadAllTags()
return
##
# Private Slots
@@ -212,7 +205,6 @@ class GuiDocViewerPanel(QWidget):
cTab.clearContent()
self._loadAllTags()
self._updateTabVisibility()
return
##
# Internal Functions
@@ -222,7 +214,6 @@ class GuiDocViewerPanel(QWidget):
"""Hide class tabs with no content."""
for tClass, cTab in self.kwTabs.items():
self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
return
def _loadAllTags(self) -> None:
"""Load all tags into the tabs."""
@@ -230,7 +221,6 @@ class GuiDocViewerPanel(QWidget):
for key, name, tClass, iItem, hItem in data:
if tClass in self.kwTabs and iItem and hItem:
self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
return
class _ViewPanelBackRefs(QTreeWidget):
@@ -278,8 +268,6 @@ class _ViewPanelBackRefs(QTreeWidget):
self.clicked.connect(self._treeItemClicked)
self.doubleClicked.connect(self._treeItemDoubleClicked)
return
def updateTheme(self) -> None:
"""Update theme elements."""
self._editIcon = SHARED.theme.getIcon("edit", "green")
@@ -288,13 +276,11 @@ class _ViewPanelBackRefs(QTreeWidget):
if item := self.topLevelItem(i):
item.setIcon(self.C_EDIT, self._editIcon)
item.setIcon(self.C_VIEW, self._viewIcon)
return
def clearContent(self) -> None:
"""Clear the widget."""
self.clear()
self._treeMap = {}
return
def refreshContent(self, dHandle: str | None) -> None:
"""Update the content."""
@@ -303,7 +289,6 @@ class _ViewPanelBackRefs(QTreeWidget):
refs = SHARED.project.index.getBackReferenceList(dHandle)
for tHandle, (sTitle, hItem) in refs.items():
self._setTreeItemValues(tHandle, sTitle, hItem)
return
def refreshDocument(self, tHandle: str) -> None:
"""Refresh document meta data."""
@@ -311,7 +296,6 @@ class _ViewPanelBackRefs(QTreeWidget):
for sTitle, hItem in iItem.items():
if f"{tHandle}:{sTitle}" in self._treeMap:
self._setTreeItemValues(tHandle, sTitle, hItem)
return
##
# Private Slots
@@ -325,7 +309,6 @@ class _ViewPanelBackRefs(QTreeWidget):
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
elif index.column() == self.C_VIEW:
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
return
@pyqtSlot("QModelIndex")
def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
@@ -333,7 +316,6 @@ class _ViewPanelBackRefs(QTreeWidget):
tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
if index.column() not in (self.C_EDIT, self.C_VIEW):
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
return
##
# Internal Functions
@@ -362,8 +344,6 @@ class _ViewPanelBackRefs(QTreeWidget):
self.addTopLevelItem(trItem)
self._treeMap[tKey] = trItem
return
class _ViewPanelKeyWords(QTreeWidget):
@@ -418,14 +398,11 @@ class _ViewPanelKeyWords(QTreeWidget):
self.clicked.connect(self._treeItemClicked)
self.doubleClicked.connect(self._treeItemDoubleClicked)
return
def updateTheme(self) -> None:
"""Update theme elements."""
self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[self._class], "root")
self._editIcon = SHARED.theme.getIcon("edit", "green")
self._viewIcon = SHARED.theme.getIcon("view", "blue")
return
def countEntries(self) -> int:
"""Return the number of items in the list."""
@@ -435,7 +412,6 @@ class _ViewPanelKeyWords(QTreeWidget):
"""Clear the list."""
self._treeMap = {}
self.clear()
return
def addUpdateEntry(self, tag: str, name: str, iItem: IndexNode, hItem: IndexHeading) -> None:
"""Add a new entry, or update an existing one."""
@@ -470,8 +446,6 @@ class _ViewPanelKeyWords(QTreeWidget):
self.addTopLevelItem(trItem)
self._treeMap[tag] = trItem
return
def removeEntry(self, tag: str) -> bool:
"""Remove a tag from the list."""
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_DOC, checkInt(widths[2], 100))
self.setColumnWidth(self.C_TITLE, checkInt(widths[3], 100))
return
def getColumnWidths(self) -> list[int]:
"""Get the widths of the user-adjustable columns."""
@@ -510,7 +483,6 @@ class _ViewPanelKeyWords(QTreeWidget):
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT)
elif index.column() == self.C_VIEW:
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
return
@pyqtSlot("QModelIndex")
def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
@@ -518,4 +490,3 @@ class _ViewPanelKeyWords(QTreeWidget):
tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
if index.column() not in (self.C_EDIT, self.C_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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -42,6 +42,11 @@ logger = logging.getLogger(__name__)
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:
super().__init__(parent=parent)
@@ -52,11 +57,8 @@ class GuiTextDocument(QTextDocument):
logger.debug("Ready: GuiTextDocument")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiTextDocument")
return
##
# Properties
@@ -96,8 +98,6 @@ class GuiTextDocument(QTextDocument):
logger.debug("Loaded %d text blocks in %.3f ms", count, 1000*(tMid - tStart))
logger.debug("Highlighted document in %.3f ms", 1000*(tEnd - tMid))
return
def metaDataAtPos(self, pos: int) -> tuple[str, str]:
"""Check if there is meta data available at a given position in
the document, and if so, return it.
@@ -146,4 +146,3 @@ class GuiTextDocument(QTextDocument):
def setSpellCheckState(self, state: bool) -> None:
"""Set the spell check state of the syntax highlighter."""
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -43,6 +43,7 @@ logger = logging.getLogger(__name__)
class GuiItemDetails(QWidget):
"""GUI: Project Item Details Panel."""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
@@ -190,8 +191,6 @@ class GuiItemDetails(QWidget):
logger.debug("Ready: GuiItemDetails")
return
###
# Class Methods
##
@@ -210,7 +209,6 @@ class GuiItemDetails(QWidget):
self.cCountData.clear()
self.wCountData.clear()
self.pCountData.clear()
return
def refreshDetails(self) -> None:
"""Reload the content of the details panel."""
@@ -219,7 +217,6 @@ class GuiItemDetails(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
self.updateViewBox(self._handle)
return
def updateViewBox(self, tHandle: str | None) -> None:
"""Populate the details box from a given handle."""
@@ -283,4 +280,3 @@ class GuiItemDetails(QWidget):
self.updateViewBox(tHandle)
elif change == nwChange.DELETE:
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -82,8 +82,6 @@ class GuiMainMenu(QMenuBar):
logger.debug("Ready: GuiMainMenu")
return
##
# Public Slots
##
@@ -92,7 +90,6 @@ class GuiMainMenu(QMenuBar):
def setSpellCheckState(self, state: bool) -> None:
"""Forward spell check check state to its action."""
self.aSpellCheck.setChecked(state)
return
##
# Private Slots
@@ -105,21 +102,18 @@ class GuiMainMenu(QMenuBar):
decision, just pass a None to the function and let it decide.
"""
self.mainGui.docEditor.toggleSpellCheck(None)
return
@pyqtSlot()
def _openUserManualFile(self) -> None:
"""Open the documentation in PDF format."""
if isinstance(CONFIG.pdfDocs, Path):
openExternalPath(CONFIG.pdfDocs)
return
@pyqtSlot(str)
def _changeSpelling(self, language: str) -> None:
"""Change the spell check language."""
SHARED.project.data.setSpellLang(language)
SHARED.updateSpellCheckLanguage()
return
##
# Internal Functions
@@ -188,8 +182,6 @@ class GuiMainMenu(QMenuBar):
self.aExitNW.triggered.connect(qtLambda(self.mainGui.closeMain))
self.mainGui.addAction(self.aExitNW)
return
def _buildDocumentMenu(self) -> None:
"""Assemble the Document menu."""
# Document
@@ -236,8 +228,6 @@ class GuiMainMenu(QMenuBar):
self.aImportFile = qtAddAction(self.docuMenu, self.tr("Import Text from File"))
self.aImportFile.triggered.connect(qtLambda(self.mainGui.importDocument))
return
def _buildEditMenu(self) -> None:
"""Assemble the Edit menu."""
# Edit
@@ -305,8 +295,6 @@ class GuiMainMenu(QMenuBar):
)
self.mainGui.addAction(self.aSelectPar)
return
def _buildViewMenu(self) -> None:
"""Assemble the View menu."""
# View
@@ -367,8 +355,6 @@ class GuiMainMenu(QMenuBar):
self.aFullScreen.triggered.connect(self.mainGui.toggleFullScreenMode)
self.mainGui.addAction(self.aFullScreen)
return
def _buildInsertMenu(self) -> None:
"""Assemble the Insert menu."""
# Insert
@@ -646,8 +632,6 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocInsert.emit(nwDocInsert.FOOTNOTE)
)
return
def _buildFormatMenu(self) -> None:
"""Assemble the Format menu."""
# Format
@@ -901,8 +885,6 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocAction.emit(nwDocAction.RM_BREAKS)
)
return
def _buildSearchMenu(self) -> None:
"""Assemble the Search menu."""
# Search
@@ -948,8 +930,6 @@ class GuiMainMenu(QMenuBar):
self.aFindProj.setShortcut("Ctrl+Shift+F")
self.aFindProj.triggered.connect(qtLambda(self.requestViewChange.emit, nwView.SEARCH))
return
def _buildToolsMenu(self) -> None:
"""Assemble the Tools menu."""
# Tools
@@ -1019,8 +999,6 @@ class GuiMainMenu(QMenuBar):
self.aPreferences.triggered.connect(self.mainGui.showPreferencesDialog)
self.mainGui.addAction(self.aPreferences)
return
def _buildHelpMenu(self) -> None:
"""Assemble the Help menu."""
# Help
@@ -1066,5 +1044,3 @@ class GuiMainMenu(QMenuBar):
# Document > Main Website
self.aWebsite = qtAddAction(self.helpMenu, self.tr("The novelWriter Website"))
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -55,6 +55,7 @@ logger = logging.getLogger(__name__)
class GuiNovelView(QWidget):
"""GUI: Novel View Panel."""
# Signals for user interaction with the novel tree
selectedItemChanged = pyqtSignal(str)
@@ -82,8 +83,6 @@ class GuiNovelView(QWidget):
self.getSelectedHandle = self.novelTree.getSelectedHandle
self.refreshCurrentTree = self.novelBar.forceRefreshNovelTree
return
##
# Methods
##
@@ -91,19 +90,16 @@ class GuiNovelView(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
self.novelBar.updateTheme()
return
def initSettings(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
self.novelTree.initSettings()
return
def clearNovelView(self) -> None:
"""Clear project-related GUI content."""
self.novelBar.clearContent()
self.novelBar.setEnabled(False)
self.novelTree.clearContent()
return
def openProjectTasks(self) -> None:
"""Run open project tasks."""
@@ -125,8 +121,6 @@ class GuiNovelView(QWidget):
self.novelTree.setLastColSize(lastColSize)
return
def closeProjectTasks(self) -> None:
"""Run closing project tasks."""
logger.debug("Saving State: GuiNovelView")
@@ -140,12 +134,9 @@ class GuiNovelView(QWidget):
self.clearNovelView()
return
def setTreeFocus(self) -> None:
"""Set the focus to the tree widget."""
self.novelTree.setFocus()
return
def treeHasFocus(self) -> bool:
"""Check if the novel tree has focus."""
@@ -159,22 +150,20 @@ class GuiNovelView(QWidget):
def setCurrentNovel(self, rootHandle: str | None) -> None:
"""Set the current novel to display."""
self.novelTree.setNovelModel(rootHandle)
return
@pyqtSlot(str)
def setActiveHandle(self, tHandle: str) -> None:
"""Highlight the rows associated with a given handle."""
self.novelTree.setActiveHandle(tHandle)
return
@pyqtSlot(str, Enum)
def updateRootItem(self, tHandle: str, change: nwChange) -> None:
"""If any root item changes, rebuild the novel root menu."""
self.novelBar.buildNovelRootMenu()
return
class GuiNovelToolBar(QWidget):
"""GUI: Novel View Panel ToolBar."""
def __init__(self, novelView: GuiNovelView) -> None:
super().__init__(parent=novelView)
@@ -249,8 +238,6 @@ class GuiNovelToolBar(QWidget):
logger.debug("Ready: GuiNovelToolBar")
return
##
# Methods
##
@@ -277,20 +264,16 @@ class GuiNovelToolBar(QWidget):
self.forceRefreshNovelTree()
return
def clearContent(self) -> None:
"""Run clearing project tasks."""
self.novelValue.clear()
self.novelValue.setToolTip("")
return
def buildNovelRootMenu(self) -> None:
"""Build the novel root menu."""
self.novelValue.refreshNovelList()
self.novelView.setCurrentNovel(self.novelValue.handle)
self.tbNovel.setVisible(self.novelValue.count() > 1)
return
def setCurrentRoot(self, rootHandle: str | None) -> None:
"""Set the current active root handle."""
@@ -300,7 +283,6 @@ class GuiNovelToolBar(QWidget):
SHARED.project.data.setLastHandle(rootHandle, "novel")
self.novelView.setCurrentNovel(rootHandle)
self.novelView.novelTree.setAccessibleName(self.novelValue.currentText())
return
def setLastColType(self, colType: nwNovelExtra, doRefresh: bool = True) -> None:
"""Set the last column type."""
@@ -309,7 +291,6 @@ class GuiNovelToolBar(QWidget):
if doRefresh:
self.forceRefreshNovelTree()
self.novelView.novelTree.resizeColumns()
return
def setActive(self, state: bool) -> None:
"""Set the widget active state, which enables automatic tree
@@ -322,7 +303,6 @@ class GuiNovelToolBar(QWidget):
and self._refresh.get(handle, False)
):
self._refreshNovelTree(self.novelValue.handle)
return
##
# Public Slots
@@ -335,7 +315,6 @@ class GuiNovelToolBar(QWidget):
self.novelView.setCurrentNovel(tHandle)
SHARED.project.index.refreshNovelModel(tHandle)
self._refresh[tHandle] = False
return
##
# Private Slots
@@ -349,7 +328,6 @@ class GuiNovelToolBar(QWidget):
self._refresh[tHandle] = False
else:
self._refresh[tHandle] = True
return
@pyqtSlot()
def _selectLastColumnSize(self) -> None:
@@ -361,7 +339,6 @@ class GuiNovelToolBar(QWidget):
if isOk:
self.novelView.novelTree.setLastColSize(newSize)
self.novelView.novelTree.resizeColumns()
return
##
# Internal Functions
@@ -374,10 +351,10 @@ class GuiNovelToolBar(QWidget):
aLast.setActionGroup(self.gLastCol)
aLast.triggered.connect(qtLambda(self.setLastColType, colType))
self.aLastCol[colType] = aLast
return
class GuiNovelTree(NTreeView):
"""GUI: Novel View Panel Tree."""
def __init__(self, novelView: GuiNovelView) -> None:
super().__init__(parent=novelView)
@@ -414,8 +391,6 @@ class GuiNovelTree(NTreeView):
logger.debug("Ready: GuiNovelTree")
return
def initSettings(self) -> None:
"""Set or update tree widget settings."""
if CONFIG.hideVScroll:
@@ -426,7 +401,6 @@ class GuiNovelTree(NTreeView):
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
return
##
# Properties
@@ -466,25 +440,21 @@ class GuiNovelTree(NTreeView):
self.resizeColumns()
else:
self.clearContent()
return
def setActiveHandle(self, tHandle: str | None) -> None:
"""Set the handle to be highlighted."""
self._actHandle = tHandle
if viewport := self.viewport():
viewport.repaint()
return
def setLastColType(self, colType: nwNovelExtra) -> None:
"""Set the extra column type."""
self._lastColType = colType
SHARED.project.index.setNovelModelExtraColumn(colType)
return
def setLastColSize(self, colSize: int) -> None:
"""Set the extra column size between 15% and 75%."""
self._lastColSize = minmax(colSize, 15, 75)/100.0
return
##
# Class Methods
@@ -493,7 +463,6 @@ class GuiNovelTree(NTreeView):
def clearContent(self) -> None:
"""Clear the tree view."""
self.setModel(None)
return
def resizeColumns(self) -> None:
"""Set the correct column sizes."""
@@ -506,7 +475,6 @@ class GuiNovelTree(NTreeView):
if model.columns == 4:
header.setSectionResizeMode(3, QtHeaderToContents)
header.setMaximumSectionSize(int(self._lastColSize * vp.width()))
return
##
# Overloads
@@ -517,7 +485,6 @@ class GuiNovelTree(NTreeView):
if (model := self._getModel()) and model.handle(index) == self._actHandle:
painter.fillRect(opt.rect, self.palette().alternateBase())
super().drawRow(painter, opt, index)
return
##
# Events
@@ -527,7 +494,6 @@ class GuiNovelTree(NTreeView):
"""Process size changed."""
super().resizeEvent(event)
self.resizeColumns()
return
##
# Private Slots
@@ -535,36 +501,33 @@ class GuiNovelTree(NTreeView):
@pyqtSlot(QModelIndex)
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 (tHandle := model.handle(index)) and (sTitle := model.key(index)):
self.novelView.selectedItemChanged.emit(tHandle)
if index.column() == model.columnCount(index) - 1:
pos = self.mapToGlobal(self.visualRect(index).topRight())
self._popMetaBox(pos, tHandle, sTitle)
return
@pyqtSlot(QModelIndex)
def _onDoubleClick(self, index: QModelIndex) -> None:
"""The user double-clicked an index."""
"""Process user double-click on an index."""
if (
(model := self._getModel())
and (tHandle := model.handle(index))
and (sTitle := model.key(index))
):
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle, False)
return
@pyqtSlot(QModelIndex)
def _onMiddleClick(self, index: QModelIndex) -> None:
"""The user middle-clicked an index."""
"""Process user middle-click on an index."""
if (
(model := self._getModel())
and (tHandle := model.handle(index))
and (sTitle := model.key(index))
):
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle, False)
return
##
# Internal Functions
@@ -582,7 +545,6 @@ class GuiNovelTree(NTreeView):
"""Generate a reference list for a given reference key."""
if tags := ", ".join(refs.get(key, [])):
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {tags}")
return
if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
@@ -610,4 +572,3 @@ class GuiNovelTree(NTreeView):
text = f"<p>{refs}</p>"
if tooltip := (text + synopsis or self.tr("No meta data")):
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import csv
@@ -58,6 +58,7 @@ logger = logging.getLogger(__name__)
class GuiOutlineView(QWidget):
"""GUI: Project Outline Panel."""
loadDocumentTagRequest = pyqtSignal(str, Enum)
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
@@ -96,8 +97,6 @@ class GuiOutlineView(QWidget):
# Function Mappings
self.getSelectedHandle = self.outlineTree.getSelectedHandle
return
##
# Methods
##
@@ -109,24 +108,20 @@ class GuiOutlineView(QWidget):
self.outlineTree.refreshTree(
rootHandle=SHARED.project.data.getLastHandle("outline"), overRide=True
)
return
def initSettings(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
self.outlineTree.initSettings()
self.outlineData.initSettings()
return
def refreshTree(self) -> None:
"""Refresh the current tree."""
self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline"))
return
def clearOutline(self) -> None:
"""Clear project-related GUI content."""
self.outlineData.clearDetails()
self.outlineBar.setEnabled(False)
return
def openProjectTasks(self) -> None:
"""Run open project tasks."""
@@ -142,8 +137,6 @@ class GuiOutlineView(QWidget):
self.outlineBar.setEnabled(True)
self.outlineData.loadGuiSettings()
return
def closeProjectTasks(self) -> None:
"""Run closing project tasks."""
if self.outlineTree.wasRendered:
@@ -152,7 +145,6 @@ class GuiOutlineView(QWidget):
self.outlineTree.closeProjectTasks()
self.outlineData.updateClasses()
self.clearOutline()
return
def splitSizes(self) -> list[int]:
"""Get the sizes of the splitter widget."""
@@ -175,7 +167,6 @@ class GuiOutlineView(QWidget):
"""Handle tasks whenever a root folders changes."""
self.outlineBar.populateNovelList()
self.outlineData.updateClasses()
return
##
# Private Slots
@@ -188,23 +179,21 @@ class GuiOutlineView(QWidget):
of columns has changed.
"""
self.outlineBar.setColumnHiddenState(self.outlineTree.hiddenColumns)
return
@pyqtSlot(str)
def _tagClicked(self, link: str) -> None:
"""Capture the click of a tag in the details panel."""
if link:
self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW)
return
@pyqtSlot(str)
def _rootItemChanged(self, tHandle: str) -> None:
"""Handle root novel changed or needs to be refreshed."""
self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True)
return
class GuiOutlineToolBar(QToolBar):
"""GUI: Project Outline Panel ToolBar."""
loadNovelRootRequest = pyqtSignal(str)
outlineExportRequest = pyqtSignal()
@@ -263,8 +252,6 @@ class GuiOutlineToolBar(QToolBar):
logger.debug("Ready: GuiOutlineToolBar")
return
##
# Methods
##
@@ -278,22 +265,18 @@ class GuiOutlineToolBar(QToolBar):
self.tbColumns.setIcon(SHARED.theme.getIcon("more_vertical"))
self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
self.novelLabel.setTextColors(color=self.palette().windowText().color())
return
def populateNovelList(self) -> None:
"""Reload the content of the novel list."""
self.novelValue.refreshNovelList()
return
def setCurrentRoot(self, rootHandle: str | None) -> None:
"""Set the current active root handle."""
self.novelValue.setHandle(rootHandle)
return
def setColumnHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
"""Forward the change of column hidden states to the menu."""
self.mColumns.setHiddenState(hiddenState)
return
##
# Private Slots
@@ -303,22 +286,20 @@ class GuiOutlineToolBar(QToolBar):
def _novelValueChanged(self, tHandle: str) -> None:
"""Emit a signal containing the handle of the selected item."""
self.loadNovelRootRequest.emit(tHandle)
return
@pyqtSlot()
def _refreshRequested(self) -> None:
"""Emit a signal containing the handle of the selected item."""
self.loadNovelRootRequest.emit(self.novelValue.handle)
return
@pyqtSlot()
def _exportRequested(self) -> None:
"""Emit a signal that an export of the outline was requested."""
self.outlineExportRequest.emit()
return
class GuiOutlineTree(QTreeWidget):
"""GUI: Project Outline Panel Tree."""
DEF_WIDTH: Final[dict[nwOutline, int]] = {
nwOutline.TITLE: 200,
@@ -422,8 +403,6 @@ class GuiOutlineTree(QTreeWidget):
logger.debug("Ready: GuiOutlineTree")
return
##
# Properties
##
@@ -451,7 +430,6 @@ class GuiOutlineTree(QTreeWidget):
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
return
def clearContent(self) -> None:
"""Clear the tree and header and set the default values for the
@@ -474,8 +452,6 @@ class GuiOutlineTree(QTreeWidget):
self._treeNCols = len(self._treeOrder)
return
def updateTheme(self) -> None:
"""Update theme elements."""
iType = nwItemType.FILE
@@ -488,15 +464,14 @@ class GuiOutlineTree(QTreeWidget):
"H3": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H3"),
"H4": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H4"),
}
return
def refreshTree(
self, rootHandle: str | None = None,
overRide: bool = False, novelChanged: bool = False
) -> None:
"""Called whenever the Outline tab is activated and controls
what data to load, and if necessary, force a rebuild of the
tree.
"""Refresh the outline tree. Called whenever the Outline tab is
activated and controls what data to load, and if necessary,
force a rebuild of the tree.
"""
# If it's the first time, we always build
if self._firstView or (self._firstView and overRide):
@@ -518,11 +493,10 @@ class GuiOutlineTree(QTreeWidget):
return
def closeProjectTasks(self) -> None:
"""Called before a project is closed."""
"""Call before a project is closed."""
self._saveHeaderState()
self.clearContent()
self._firstView = True
return
def getSelectedHandle(self) -> tuple[str | None, str | None]:
"""Get the currently selected handle. If multiple items are
@@ -546,7 +520,6 @@ class GuiOutlineTree(QTreeWidget):
if hItem in self._colIdx:
self.setColumnHidden(self._colIdx[hItem], not isChecked)
self._saveHeaderState()
return
@pyqtSlot()
def exportOutline(self) -> None:
@@ -562,7 +535,6 @@ class GuiOutlineTree(QTreeWidget):
writer.writerows(
self._dumpNovelData(self.outlineView.outlineBar.novelValue.handle)
)
return
##
# Private Slots
@@ -577,7 +549,6 @@ class GuiOutlineTree(QTreeWidget):
tHandle, sTitle = self.getSelectedHandle()
if tHandle:
self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
return
@pyqtSlot()
def _onItemSelectionChanged(self) -> None:
@@ -588,7 +559,6 @@ class GuiOutlineTree(QTreeWidget):
tHandle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
sTitle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
self.activeItemChanged.emit(tHandle, sTitle)
return
@pyqtSlot(int, int, int)
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._saveHeaderState()
return
##
# Internal Functions
@@ -637,8 +606,6 @@ class GuiOutlineTree(QTreeWidget):
self.hiddenStateChanged.emit()
return
def _saveHeaderState(self) -> None:
"""Save the state of the main tree header, that is, column
order, column width and column hidden state. We don't want to
@@ -661,7 +628,6 @@ class GuiOutlineTree(QTreeWidget):
pOptions = SHARED.project.options
pOptions.setValue("GuiOutline", "columnState", colState)
pOptions.saveSettings()
return
def _populateTree(self, rootHandle: str | None) -> None:
"""Build the tree based on the project index, and the header
@@ -746,8 +712,6 @@ class GuiOutlineTree(QTreeWidget):
self._lastBuild = time()
logger.debug("Project outline built in %.3f ms", 1000.0*(time() - tStart))
return
def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]:
"""Dump all novel data into a table."""
sLabel = SHARED.project.localLookup("Story Structure")
@@ -821,6 +785,7 @@ class GuiOutlineTree(QTreeWidget):
class GuiOutlineHeaderMenu(QMenu):
"""GUI: Project Outline Panel Header Selection Menu."""
columnToggled = pyqtSignal(bool, Enum)
@@ -844,8 +809,6 @@ class GuiOutlineHeaderMenu(QMenu):
)
self.addAction(self.actionMap[hItem])
return
def setHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
"""Overwrite the checked state of the columns as the inverse of
the hidden state. Skip the TITLE column as it cannot be hidden.
@@ -859,10 +822,9 @@ class GuiOutlineHeaderMenu(QMenu):
self.acceptToggle = True
return
class GuiOutlineDetails(QScrollArea):
"""GUI: Project Outline Panel Details View."""
LVL_MAP: Final[dict[str, str]] = {
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
@@ -1007,8 +969,6 @@ class GuiOutlineDetails(QScrollArea):
logger.debug("Ready: GuiOutlineDetails")
return
def initSettings(self) -> None:
"""Set or update outline settings."""
if CONFIG.hideVScroll:
@@ -1020,7 +980,6 @@ class GuiOutlineDetails(QScrollArea):
else:
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
self.updateClasses()
return
def loadGuiSettings(self) -> None:
"""Run open project tasks."""
@@ -1031,7 +990,6 @@ class GuiOutlineDetails(QScrollArea):
pOptions.getInt("GuiOutlineDetails", "detailsWidth", width//3),
pOptions.getInt("GuiOutlineDetails", "tagsWidth", 2*width//3),
])
return
def saveGuiSettings(self) -> None:
"""Run close project tasks."""
@@ -1040,7 +998,6 @@ class GuiOutlineDetails(QScrollArea):
pOptions = SHARED.project.options
pOptions.setValue("GuiOutlineDetails", "detailsWidth", mainSplit[0])
pOptions.setValue("GuiOutlineDetails", "tagsWidth", mainSplit[1])
return
def clearDetails(self) -> None:
"""Clear all the data labels."""
@@ -1057,7 +1014,6 @@ class GuiOutlineDetails(QScrollArea):
value.clear()
self.updateClasses()
return
##
# Slots
@@ -1090,8 +1046,6 @@ class GuiOutlineDetails(QScrollArea):
for key, (_, value) in self.tagValues.items():
value.setText(self._formatTags(novRefs, key))
return
@pyqtSlot()
def updateClasses(self) -> None:
"""Update the visibility status of class details."""
@@ -1102,7 +1056,6 @@ class GuiOutlineDetails(QScrollArea):
label, value = self.tagValues[key]
label.setVisible(visible)
value.setVisible(visible)
return
@staticmethod
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -61,7 +61,9 @@ logger = logging.getLogger(__name__)
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
available are mapped through to the project tree class.
"""
@@ -130,8 +132,6 @@ class GuiProjectView(QWidget):
# Function Mappings
self.getSelectedHandle = self.projTree.getSelectedHandle
return
##
# Methods
##
@@ -139,19 +139,16 @@ class GuiProjectView(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
self.projBar.updateTheme()
return
def initSettings(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
self.projTree.initSettings()
return
def closeProjectTasks(self) -> None:
"""Clear project-related GUI content."""
self.projBar.clearContent()
self.projBar.setEnabled(False)
self.projTree.clearTree()
return
def openProjectTasks(self) -> None:
"""Run open project tasks."""
@@ -159,26 +156,23 @@ class GuiProjectView(QWidget):
self.projBar.buildTemplatesMenu()
self.projBar.buildQuickLinksMenu()
self.projBar.setEnabled(True)
return
def setTreeFocus(self) -> None:
"""Forward the set focus call to the tree widget."""
self.projTree.setFocus()
return
def treeHasFocus(self) -> bool:
"""Check if the project tree has focus."""
return self.projTree.hasFocus()
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(delete)
self.projTree.addAction(trash)
rename.triggered.connect(self.renameTreeItem)
delete.triggered.connect(self.projTree.processDeleteRequest)
trash.triggered.connect(self.projTree.emptyTrash)
return
##
# Public Slots
@@ -197,41 +191,36 @@ class GuiProjectView(QWidget):
if dlgOk:
nwItem.setName(newLabel)
nwItem.notifyToRefresh()
return
@pyqtSlot(str, bool)
def setSelectedHandle(self, tHandle: str, doScroll: bool = False) -> None:
"""Select an item and optionally scroll it into view."""
self.projTree.setSelectedHandle(tHandle, doScroll=doScroll)
return
@pyqtSlot(str)
def setActiveHandle(self, tHandle: str | None) -> None:
"""Highlight the active handle."""
self.projTree.setActiveHandle(tHandle)
return
@pyqtSlot(str, Enum)
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
"""Refresh other content when project item changed."""
self.projBar.processTemplateDocuments(tHandle)
return
@pyqtSlot(str)
def createFileFromTemplate(self, tHandle: str) -> None:
"""Create a new document from a template."""
logger.debug("Template selected: '%s'", tHandle)
self.projTree.newTreeItem(nwItemType.FILE, copyDoc=tHandle)
return
@pyqtSlot(str, Enum)
def updateRootItem(self, tHandle: str, change: nwChange) -> None:
"""Process root item changes."""
self.projBar.buildQuickLinksMenu()
return
class GuiProjectToolBar(QWidget):
"""GUI> Project View ToolBar."""
newDocumentFromTemplate = pyqtSignal(str)
@@ -351,8 +340,6 @@ class GuiProjectToolBar(QWidget):
logger.debug("Ready: GuiProjectToolBar")
return
##
# Methods
##
@@ -383,13 +370,10 @@ class GuiProjectToolBar(QWidget):
self.buildQuickLinksMenu()
self._buildRootMenu()
return
def clearContent(self) -> None:
"""Clear dynamic content on the tool bar."""
self.mQuick.clear()
self.mTemplates.clearMenu()
return
def buildQuickLinksMenu(self) -> None:
"""Build the quick link menu."""
@@ -402,14 +386,12 @@ class GuiProjectToolBar(QWidget):
action.triggered.connect(
qtLambda(self.projView.setSelectedHandle, tHandle, doScroll=True)
)
return
def buildTemplatesMenu(self) -> None:
"""Build the templates menu."""
for tHandle, _ in SHARED.project.tree.iterRoots(nwItemClass.TEMPLATE):
for dHandle in SHARED.project.tree.subTree(tHandle):
self.processTemplateDocuments(dHandle)
return
def processTemplateDocuments(self, tHandle: str) -> None:
"""Process change in tree items to update menu content."""
@@ -418,7 +400,6 @@ class GuiProjectToolBar(QWidget):
self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon())
elif tHandle in self.mTemplates:
self.mTemplates.remove(tHandle)
return
##
# Public Slots
@@ -436,7 +417,6 @@ class GuiProjectToolBar(QWidget):
self.aAddChap.setVisible(allowDoc)
self.aAddPart.setVisible(allowDoc)
self.aAddEmpty.setVisible(allowDoc)
return
##
# Internal Functions
@@ -451,7 +431,6 @@ class GuiProjectToolBar(QWidget):
qtLambda(self.projTree.newTreeItem, nwItemType.ROOT, itemClass)
)
self.mAddRoot.addAction(aNew)
return
self.mAddRoot.clear()
addClass(nwItemClass.NOVEL)
@@ -467,10 +446,9 @@ class GuiProjectToolBar(QWidget):
addClass(nwItemClass.ARCHIVE)
addClass(nwItemClass.TEMPLATE)
return
class GuiProjectTree(QTreeView):
"""GUI: Project View Tree."""
def __init__(self, projView: GuiProjectView) -> None:
super().__init__(parent=projView)
@@ -520,8 +498,6 @@ class GuiProjectTree(QTreeView):
logger.debug("Ready: GuiProjectTree")
return
def initSettings(self) -> None:
"""Set or update tree widget settings."""
if CONFIG.hideVScroll:
@@ -532,7 +508,6 @@ class GuiProjectTree(QTreeView):
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
return
##
# External Methods
@@ -541,7 +516,6 @@ class GuiProjectTree(QTreeView):
def setActiveHandle(self, tHandle: str | None) -> None:
"""Set the handle to be highlighted."""
self._actHandle = tHandle
return
def getSelectedHandle(self) -> str | None:
"""Get the currently selected handle."""
@@ -556,7 +530,6 @@ class GuiProjectTree(QTreeView):
def clearTree(self) -> None:
"""Clear the tree view."""
self.setModel(None)
return
def loadModel(self) -> None:
"""Load and prepare a new project model."""
@@ -583,8 +556,6 @@ class GuiProjectTree(QTreeView):
self.restoreExpandedState()
return
def restoreExpandedState(self) -> None:
"""Expand all nodes that were previously expanded."""
if model := self._getModel():
@@ -592,7 +563,6 @@ class GuiProjectTree(QTreeView):
for index in model.allExpanded():
self.setExpanded(index, True)
self.blockSignals(False)
return
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
"""Set a specific handle as the selected item."""
@@ -601,7 +571,6 @@ class GuiProjectTree(QTreeView):
if doScroll:
self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter)
self.projView.selectedItemChanged.emit(tHandle)
return
def newTreeItem(
self, itemType: nwItemType, itemClass: nwItemClass | None = None,
@@ -805,7 +774,6 @@ class GuiProjectTree(QTreeView):
SHARED.warn(self.tr("Could not duplicate all items."))
self.setEnabled(True)
self.restoreExpandedState()
return
##
# Events and Overloads
@@ -825,14 +793,12 @@ class GuiProjectTree(QTreeView):
self.projView.openDocumentRequest.emit(
node.item.itemHandle, nwDocMode.VIEW, "", False
)
return
def drawRow(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None:
"""Draw a box on the active row."""
if (node := self._getNode(index)) and node.item.itemHandle == self._actHandle:
painter.fillRect(opt.rect, self.palette().alternateBase())
super().drawRow(painter, opt, index)
return
##
# Public Slots
@@ -843,14 +809,12 @@ class GuiProjectTree(QTreeView):
"""Move an item up in the tree."""
if model := self._getModel():
model.internalMove(self.currentIndex(), -1)
return
@pyqtSlot()
def moveItemDown(self) -> None:
"""Move an item down in the tree."""
if model := self._getModel():
model.internalMove(self.currentIndex(), 1)
return
@pyqtSlot()
def goToSiblingUp(self) -> None:
@@ -858,7 +822,6 @@ class GuiProjectTree(QTreeView):
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
if (move := parent.child(node.row() - 1)) and (model := self._getModel()):
self.setCurrentIndex(model.indexFromNode(move))
return
@pyqtSlot()
def goToSiblingDown(self) -> None:
@@ -866,7 +829,6 @@ class GuiProjectTree(QTreeView):
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
if (move := parent.child(node.row() + 1)) and (model := self._getModel()):
self.setCurrentIndex(model.indexFromNode(move))
return
@pyqtSlot()
def goToParent(self) -> None:
@@ -877,7 +839,6 @@ class GuiProjectTree(QTreeView):
and (parent := node.parent())
):
self.setCurrentIndex(model.indexFromNode(parent))
return
@pyqtSlot()
def goToFirstChild(self) -> None:
@@ -888,13 +849,11 @@ class GuiProjectTree(QTreeView):
and (child := node.child(0))
):
self.setCurrentIndex(model.indexFromNode(child))
return
@pyqtSlot(QModelIndex)
def expandFromIndex(self, index: QModelIndex) -> None:
"""Expand all nodes from index."""
self.expandRecursively(index)
return
@pyqtSlot(QModelIndex)
def collapseFromIndex(self, index: QModelIndex) -> None:
@@ -902,7 +861,6 @@ class GuiProjectTree(QTreeView):
if (model := self._getModel()) and (node := model.node(index)):
for child in node.allChildren():
self.setExpanded(model.indexFromNode(child), False)
return
@pyqtSlot()
def processDeleteRequest(
@@ -968,9 +926,7 @@ class GuiProjectTree(QTreeView):
@pyqtSlot()
@pyqtSlot("QPoint")
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.
"""
"""Open a context menu in-place where the user clicked."""
if model := self._getModel():
if point is None:
point = self.visualRect(self.currentIndex()).center()
@@ -987,7 +943,6 @@ class GuiProjectTree(QTreeView):
if viewport := self.viewport():
ctxMenu.exec(viewport.mapToGlobal(point))
ctxMenu.setParent(None)
return
##
# Private Slots
@@ -995,10 +950,9 @@ class GuiProjectTree(QTreeView):
@pyqtSlot(QModelIndex, QModelIndex)
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):
self.projView.selectedItemChanged.emit(node.item.itemHandle)
return
@pyqtSlot(QModelIndex)
def _onDoubleClick(self, index: QModelIndex) -> None:
@@ -1012,21 +966,18 @@ class GuiProjectTree(QTreeView):
)
else:
self.setExpanded(index, not self.isExpanded(index))
return
@pyqtSlot(QModelIndex)
def _onNodeCollapsed(self, index: QModelIndex) -> None:
"""Capture a node collapse, and pass it to the model."""
if node := self._getNode(index):
node.setExpanded(False)
return
@pyqtSlot(QModelIndex)
def _onNodeExpanded(self, index: QModelIndex) -> None:
"""Capture a node expand, and pass it to the model."""
if node := self._getNode(index):
node.setExpanded(True)
return
##
# Internal Functions
@@ -1038,7 +989,6 @@ class GuiProjectTree(QTreeView):
if model := self.selectionModel():
# Selection model can be None (#2173)
model.clearCurrentIndex()
return
def _selectedRows(self) -> list[QModelIndex]:
"""Return all column 0 indexes."""
@@ -1066,7 +1016,6 @@ class _UpdatableMenu(QMenu):
self._map: dict[str, QAction] = {}
self.setTitle(self.tr("From Template"))
self.triggered.connect(self._actionTriggered)
return
def __contains__(self, tHandle: str) -> bool:
"""Look up a handle in the menu."""
@@ -1088,7 +1037,6 @@ class _UpdatableMenu(QMenu):
self.addAction(action)
self._map[tHandle] = action
self.setActionsVisible(True)
return
def remove(self, tHandle: str) -> None:
"""Remove a template item."""
@@ -1096,19 +1044,16 @@ class _UpdatableMenu(QMenu):
self.removeAction(action)
if not self._map:
self.setActionsVisible(False)
return
def clearMenu(self) -> None:
"""Clear all menu content."""
self._map.clear()
self.clear()
return
def setActionsVisible(self, value: bool) -> None:
"""Set the visibility of root action."""
if action := self.menuAction():
action.setVisible(value)
return
##
# Private Slots
@@ -1118,7 +1063,6 @@ class _UpdatableMenu(QMenu):
def _actionTriggered(self, action: QAction) -> None:
"""Translate the menu trigger into an item trigger."""
self.menuItemTriggered.emit(str(action.data()))
return
class _TreeContextMenu(QMenu):
@@ -1139,11 +1083,9 @@ class _TreeContextMenu(QMenu):
self._indices = indices
self._children = node.childCount() > 0
logger.debug("Ready: _TreeContextMenu")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: _TreeContextMenu")
return
##
# Methods
@@ -1155,7 +1097,6 @@ class _TreeContextMenu(QMenu):
action.triggered.connect(self._tree.emptyTrash)
if self._children:
self._expandCollapse()
return
def buildSingleSelectMenu(self) -> None:
"""Build the single-select menu."""
@@ -1191,15 +1132,12 @@ class _TreeContextMenu(QMenu):
action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle))
self._deleteOrTrash()
return
def buildMultiSelectMenu(self) -> None:
"""Build the multi-select menu."""
self._itemActive()
self._itemStatusImport(True)
self.addSeparator()
self._deleteOrTrash()
return
##
# Menu Builders
@@ -1217,7 +1155,6 @@ class _TreeContextMenu(QMenu):
self._view.openDocumentRequest.emit,
self._handle, nwDocMode.VIEW, "", False
))
return
def _itemCreation(self) -> None:
"""Add create item actions."""
@@ -1228,7 +1165,6 @@ class _TreeContextMenu(QMenu):
menu.addAction(self._view.projBar.aAddEmpty)
menu.addAction(self._view.projBar.aAddNote)
menu.addAction(self._view.projBar.aAddFolder)
return
def _itemHeader(self) -> None:
"""Check if there is a header that can be used for rename."""
@@ -1238,7 +1174,6 @@ class _TreeContextMenu(QMenu):
action.triggered.connect(
qtLambda(self._view.renameTreeItem, self._handle, hItem.title)
)
return
def _itemActive(self) -> None:
"""Add Active/Inactive actions."""
@@ -1253,7 +1188,6 @@ class _TreeContextMenu(QMenu):
else:
action = qtAddAction(self, self.tr("Toggle Active"))
action.triggered.connect(self._toggleItemActive)
return
def _itemStatusImport(self, multi: bool) -> None:
"""Add actions for changing status or importance."""
@@ -1295,7 +1229,6 @@ class _TreeContextMenu(QMenu):
self._view.projectSettingsRequest.emit,
GuiProjectSettings.PAGE_IMPORT
))
return
def _itemTransform(self, isFile: bool, isFolder: bool) -> None:
"""Add actions for the Transform menu."""
@@ -1338,15 +1271,12 @@ class _TreeContextMenu(QMenu):
action = qtAddAction(menu, self.tr("Split Document by Headings"))
action.triggered.connect(qtLambda(self._tree.splitDocument, self._handle))
return
def _expandCollapse(self) -> None:
"""Add actions for expand and collapse."""
action = qtAddAction(self, self.tr("Expand All"))
action.triggered.connect(qtLambda(self._tree.expandFromIndex, self._indices[0]))
action = qtAddAction(self, self.tr("Collapse All"))
action.triggered.connect(qtLambda(self._tree.collapseFromIndex, self._indices[0]))
return
def _deleteOrTrash(self) -> None:
"""Add move to Trash action."""
@@ -1359,7 +1289,6 @@ class _TreeContextMenu(QMenu):
text = self.tr("Move to Trash")
action = qtAddAction(self, text)
action.triggered.connect(self._tree.processDeleteRequest)
return
##
# Private Slots
@@ -1371,7 +1300,6 @@ class _TreeContextMenu(QMenu):
if self._item.isFileType():
self._item.setActive(not self._item.isActive)
self._item.notifyToRefresh()
return
##
# Internal Functions
@@ -1385,13 +1313,11 @@ class _TreeContextMenu(QMenu):
node.item.setActive(state)
refresh.append(node.item.itemHandle)
SHARED.project.tree.refreshItems(refresh)
return
def _changeItemStatus(self, key: str) -> None:
"""Set a new status value of an item."""
self._item.setStatus(key)
self._item.notifyToRefresh()
return
def _iterSetItemStatus(self, key: str) -> None:
"""Change the status value for multiple items."""
@@ -1401,13 +1327,11 @@ class _TreeContextMenu(QMenu):
node.item.setStatus(key)
refresh.append(node.item.itemHandle)
SHARED.project.tree.refreshItems(refresh)
return
def _changeItemImport(self, key: str) -> None:
"""Set a new importance value of an item."""
self._item.setImport(key)
self._item.notifyToRefresh()
return
def _iterSetItemImport(self, key: str) -> None:
"""Change the status value for multiple items."""
@@ -1417,7 +1341,6 @@ class _TreeContextMenu(QMenu):
node.item.setImport(key)
refresh.append(node.item.itemHandle)
SHARED.project.tree.refreshItems(refresh)
return
def _changeItemLayout(self, itemLayout: nwItemLayout) -> None:
"""Set a new item layout value of an item."""
@@ -1428,7 +1351,6 @@ class _TreeContextMenu(QMenu):
elif itemLayout == nwItemLayout.NOTE:
self._item.setLayout(nwItemLayout.NOTE)
self._item.notifyToRefresh()
return
def _convertFolderToFile(self, itemLayout: nwItemLayout) -> None:
"""Convert a folder to a note or document."""
@@ -1448,4 +1370,3 @@ class _TreeContextMenu(QMenu):
self._item.notifyToRefresh()
else:
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
class GuiProjectSearch(QWidget):
"""GUI: Project Search Panel."""
C_NAME = 0
C_RESULT = 0
@@ -151,8 +152,6 @@ class GuiProjectSearch(QWidget):
logger.debug("Ready: GuiProjectSearch")
return
##
# Methods
##
@@ -174,8 +173,6 @@ class GuiProjectSearch(QWidget):
self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
return
def processReturn(self) -> None:
"""Process a return keypress forwarded from the main GUI."""
if self.searchText.hasFocus():
@@ -189,7 +186,6 @@ class GuiProjectSearch(QWidget):
self.openDocumentSelectRequest.emit(
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
)
return
def beginSearch(self, text: str = "") -> None:
"""Focus the search box and select its text, if any."""
@@ -198,20 +194,17 @@ class GuiProjectSearch(QWidget):
if text:
self.searchText.setText(text.partition("\n")[0])
self.searchText.selectAll()
return
def closeProjectTasks(self) -> None:
"""Run close project tasks."""
self._map = {}
self.searchText.clear()
self.searchResult.clear()
return
def refreshCurrentSearch(self) -> None:
"""Refresh the search if there is one."""
if self.searchResult.topLevelItemCount() > 0:
self._processSearch()
return
##
# Events
@@ -238,7 +231,6 @@ class GuiProjectSearch(QWidget):
self.searchText.setFocus()
else:
super().keyPressEvent(event)
return
##
# Public Slots
@@ -252,7 +244,6 @@ class GuiProjectSearch(QWidget):
results, capped = self._search.searchText(SHARED.mainGui.docEditor.getText())
self._displayResultSet(SHARED.project.tree[tHandle], results, capped)
logger.debug("Updated search for '%s' in %.3f ms", tHandle, 1000*(time() - start))
return
##
# Private Slots
@@ -278,7 +269,6 @@ class GuiProjectSearch(QWidget):
self._time = time()
QApplication.restoreOverrideCursor()
self._blocked = False
return
@pyqtSlot()
def _searchResultSelected(self) -> None:
@@ -288,7 +278,6 @@ class GuiProjectSearch(QWidget):
self.selectedItemChanged.emit(str(data[0]))
elif data := items[0].data(0, self.D_HANDLE):
self.selectedItemChanged.emit(str(data))
return
@pyqtSlot("QTreeWidgetItem*", int)
def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
@@ -297,28 +286,24 @@ class GuiProjectSearch(QWidget):
self.openDocumentSelectRequest.emit(
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), True
)
return
@pyqtSlot(bool)
def _toggleCase(self, state: bool) -> None:
"""Enable/disable case sensitive mode."""
CONFIG.searchProjCase = state
self.refreshCurrentSearch()
return
@pyqtSlot(bool)
def _toggleWord(self, state: bool) -> None:
"""Enable/disable whole word search mode."""
CONFIG.searchProjWord = state
self.refreshCurrentSearch()
return
@pyqtSlot(bool)
def _toggleRegEx(self, state: bool) -> None:
"""Enable/disable regular expression search mode."""
CONFIG.searchProjRegEx = state
self.refreshCurrentSearch()
return
##
# Internal Functions
@@ -360,5 +345,3 @@ class GuiProjectSearch(QWidget):
self.searchResult.setFirstColumnSpanned(i, parent, True)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -45,6 +45,7 @@ logger = logging.getLogger(__name__)
class GuiSideBar(QWidget):
"""GUI: Main Window SideBar."""
requestViewChange = pyqtSignal(nwView)
@@ -126,8 +127,6 @@ class GuiSideBar(QWidget):
logger.debug("Ready: GuiSideBar")
return
def updateTheme(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
@@ -153,8 +152,6 @@ class GuiSideBar(QWidget):
self._setThemeModeIcon()
return
##
# Private Slots
##
@@ -171,7 +168,6 @@ class GuiSideBar(QWidget):
CONFIG.themeMode = nwTheme.AUTO
self.mainGui.checkThemeUpdate()
self._setThemeModeIcon()
return
##
# Internal Functions
@@ -181,7 +177,6 @@ class GuiSideBar(QWidget):
"""Set the theme button icon."""
self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode])
self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode]))
return
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -41,6 +41,7 @@ logger = logging.getLogger(__name__)
class GuiMainStatus(QStatusBar):
"""GUI: Main Window Status Bar."""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
@@ -112,8 +113,6 @@ class GuiMainStatus(QStatusBar):
self.updateTheme()
self.clearStatus()
return
def initSettings(self) -> None:
"""Apply user settings."""
if CONFIG.useCharCount:
@@ -122,7 +121,6 @@ class GuiMainStatus(QStatusBar):
else:
self._trStatsCount = trStats(nwLabels.STATS_DISPLAY[nwStats.WORDS])
self._trStatsTip = self.tr("Total word count (session change)")
return
def clearStatus(self) -> None:
"""Reset all widgets on the status bar to default values."""
@@ -132,7 +130,6 @@ class GuiMainStatus(QStatusBar):
self.setProjectStatus(None)
self.setDocumentStatus(None)
self.updateTime()
return
def updateTheme(self) -> None:
"""Update theme elements."""
@@ -149,8 +146,6 @@ class GuiMainStatus(QStatusBar):
self.docIcon.setColors(colNone, colSaved, colUnsaved)
self.projIcon.setColors(colNone, colSaved, colUnsaved)
return
##
# Setters
##
@@ -158,17 +153,14 @@ class GuiMainStatus(QStatusBar):
def setRefTime(self, refTime: float) -> None:
"""Set the reference time for the status bar clock."""
self._refTime = refTime
return
def setProjectStatus(self, state: bool | None) -> None:
"""Set the project status colour icon."""
self.projIcon.setState(state)
return
def setDocumentStatus(self, state: bool | None) -> None:
"""Set the document status colour icon."""
self.docIcon.setState(state)
return
def setUserIdle(self, idle: bool) -> None:
"""Change the idle status icon."""
@@ -180,13 +172,11 @@ class GuiMainStatus(QStatusBar):
else:
self.timeIcon.setPixmap(self.timePixmap)
self._userIdle = idle
return
def setProjectStats(self, pWC: int, sWC: int) -> None:
"""Update the current project statistics."""
self.statsText.setText(self._trStatsCount.format(f"{pWC:n}", f"{sWC:+n}"))
self.statsText.setToolTip(self._trStatsTip)
return
def updateTime(self, idleTime: float = 0.0) -> None:
"""Update the session clock."""
@@ -198,7 +188,6 @@ class GuiMainStatus(QStatusBar):
else:
sessTime = round(time() - self._refTime)
self.timeText.setText(formatTime(sessTime))
return
##
# Public Slots
@@ -209,7 +198,6 @@ class GuiMainStatus(QStatusBar):
"""Set the status bar message to display."""
self.showMessage(message, nwConst.STATUS_MSG_TIMEOUT)
QApplication.processEvents()
return
@pyqtSlot(str, str)
def setLanguage(self, language: str, provider: str) -> None:
@@ -220,19 +208,16 @@ class GuiMainStatus(QStatusBar):
else:
self.langText.setText(QLocale(language).nativeLanguageName().title())
self.langText.setToolTip(f"{language} ({provider})" if provider else language)
return
@pyqtSlot(bool)
def updateProjectStatus(self, status: bool) -> None:
"""Update the project status."""
self.setProjectStatus(not status)
return
@pyqtSlot(bool)
def updateDocumentStatus(self, status: bool) -> None:
"""Update the document status."""
self.setDocumentStatus(not status)
return
##
# Private Slots
@@ -244,7 +229,6 @@ class GuiMainStatus(QStatusBar):
state = not CONFIG.showSessionTime
self.timeText.setVisible(state)
CONFIG.showSessionTime = state
return
##
# Debug
@@ -279,4 +263,3 @@ class GuiMainStatus(QStatusBar):
)
self.showMessage(f"Debug [{stamp}] {message}", 6000)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -58,6 +58,7 @@ STYLES_BIG_TOOLBUTTON = "bigToolButton"
@dataclass
class ThemeEntry:
"""Theme data."""
name: str
dark: bool
@@ -65,6 +66,7 @@ class ThemeEntry:
class ThemeMeta:
"""Theme meta data."""
name: str = ""
mode: str = ""
@@ -74,6 +76,7 @@ class ThemeMeta:
class IconsMeta:
"""Icon theme meta data."""
name: str = ""
author: str = ""
@@ -81,6 +84,7 @@ class IconsMeta:
class SyntaxColors:
"""Colours for the syntax highlighter."""
back: QColor = QColor(255, 255, 255)
text: QColor = QColor(0, 0, 0)
@@ -106,7 +110,7 @@ class SyntaxColors:
class GuiTheme:
"""Gui Theme Class
"""Gui Theme Class.
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' Width: %d", self.textNWidth)
return
##
# Properties
##
@@ -206,7 +208,7 @@ class GuiTheme:
##
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.
"""
if isinstance(font, QFont):
@@ -238,8 +240,6 @@ class GuiTheme:
self.iconCache.initIcons()
self.loadTheme()
return
def isDesktopDarkMode(self) -> bool:
"""Check if the desktop is in dark mode."""
if CONFIG.verQtValue >= 0x060500 and (hint := QGuiApplication.styleHints()):
@@ -507,7 +507,6 @@ class GuiTheme:
"""Set the colour for a named colour."""
self._qColors[key] = QColor(color)
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
return
def _resetTheme(self) -> None:
"""Reset GUI colours to default values."""
@@ -559,8 +558,6 @@ class GuiTheme:
self._setBaseColor("inactive", red)
self._setBaseColor("disabled", faded)
return
def _readColor(self, parser: ConfigParser, section: str, name: str) -> QColor:
"""Parse a colour value from a config string."""
return self.parseColor(parser.get(section, name, fallback="default"))
@@ -570,7 +567,6 @@ class GuiTheme:
) -> None:
"""Set a palette colour value from a config string."""
self._guiPalette.setBrush(value, self._readColor(parser, section, name))
return
def _buildStyleSheets(self, palette: QPalette) -> None:
"""Build default style sheets."""
@@ -602,8 +598,6 @@ class GuiTheme:
"QToolButton::menu-indicator {image: none;} "
)
return
def _scanThemes(self, files: list[Path]) -> None:
"""Scan the GUI themes folder and list all themes."""
parser = ConfigParser()
@@ -631,8 +625,6 @@ class GuiTheme:
logger.debug("Checking theme config '%s'", key)
self._allThemes[key] = ThemeEntry(name, dark, item)
return
class GuiIcons:
"""The icon class manages the content of the assets/icons folder,
@@ -672,8 +664,6 @@ class GuiIcons:
# None Icon
self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg"))
return
def clear(self) -> None:
"""Clear the icon cache."""
self._svgData = {}
@@ -681,7 +671,6 @@ class GuiIcons:
self._headerDec = []
self._headerDecNarrow = []
self._meta = ThemeMeta()
return
##
# Properties
@@ -703,7 +692,6 @@ class GuiIcons:
_listContent(icons, CONFIG.assetPath("icons"), ".icons")
_listContent(icons, CONFIG.dataPath("icons"), ".icons")
self._scanThemes(icons)
return
def loadTheme(self, theme: str) -> None:
"""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"
) -> QIcon:
"""Get the correct icon for a project item based on type, class
and heading level
and heading level.
"""
name = None
color = "default"
@@ -942,8 +930,6 @@ class GuiIcons:
logger.debug("Checking icon theme '%s'", key)
self._allThemes[key] = ThemeEntry(name, False, item)
return
# 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."""
if path.is_dir():
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -68,7 +68,7 @@ logger = logging.getLogger(__name__)
class GuiMain(QMainWindow):
"""Main GUI Window
"""Main GUI Window.
The Main GUI window class is the entry point of the application. It
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 ..."))
CONFIG.splashMessage("novelWriter is ready ...")
return
def initMain(self) -> None:
"""Initialise elements that depend on user settings."""
self.asProjTimer.setInterval(int(CONFIG.autoSaveProj*1000))
self.asDocTimer.setInterval(int(CONFIG.autoSaveDoc*1000))
return
def postLaunchTasks(self, cmdOpen: str | None) -> None:
"""Process tasks after the main window has been created."""
@@ -351,8 +348,6 @@ class GuiMain(QMainWindow):
# before showing any dialogs
QTimer.singleShot(50, self.showPostLaunchDialogs)
return
@pyqtSlot()
def showPostLaunchDialogs(self) -> None:
"""Show post launch dialogs."""
@@ -370,8 +365,6 @@ class GuiMain(QMainWindow):
).format(f"<a href='{nwConst.URL_RELEASES}'>", "</a>")
SHARED.info(f"{trVersion}<br>{trRelease}")
return
##
# Project Actions
##
@@ -534,7 +527,6 @@ class GuiMain(QMainWindow):
SHARED.setFocusMode(False)
self.saveDocument()
self.docEditor.clearEditor()
return
def openDocument(
self,
@@ -591,7 +583,6 @@ class GuiMain(QMainWindow):
self.openDocument(nHandle, tLine=1, doScroll=True)
elif wrapAround:
self.openDocument(fHandle, tLine=1, doScroll=True)
return
def saveDocument(self, force: bool = False) -> None:
"""Save the current documents."""
@@ -599,13 +590,11 @@ class GuiMain(QMainWindow):
self.docEditor.saveCursorPosition()
if force or self.docEditor.docChanged:
self.docEditor.saveText()
return
@pyqtSlot()
def forceSaveDocument(self) -> None:
"""Save document even of it has not changed."""
self.saveDocument(force=True)
return
def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool:
"""Load a document for viewing in the view panel."""
@@ -760,8 +749,6 @@ class GuiMain(QMainWindow):
if not beQuiet:
SHARED.info(self.tr("The project index has been successfully rebuilt."))
return
##
# Main Dialogs
##
@@ -772,7 +759,6 @@ class GuiMain(QMainWindow):
dialog = GuiWelcome(self)
dialog.openProjectRequest.connect(self._openProjectFromWelcome)
dialog.exec()
return
@pyqtSlot()
def showPreferencesDialog(self) -> None:
@@ -780,7 +766,6 @@ class GuiMain(QMainWindow):
dialog = GuiPreferences(self)
dialog.newPreferencesReady.connect(self._processConfigChanges)
dialog.exec()
return
@pyqtSlot()
@pyqtSlot(int)
@@ -790,7 +775,6 @@ class GuiMain(QMainWindow):
dialog = GuiProjectSettings(self, gotoPage=focusTab)
dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges)
dialog.exec()
return
@pyqtSlot()
def showNovelDetailsDialog(self) -> None:
@@ -799,7 +783,6 @@ class GuiMain(QMainWindow):
dialog = GuiNovelDetails(self)
dialog.activateDialog()
dialog.updateValues()
return
@pyqtSlot()
def showBuildManuscriptDialog(self) -> None:
@@ -809,7 +792,6 @@ class GuiMain(QMainWindow):
dialog = GuiManuscript(self)
dialog.activateDialog()
dialog.loadContent()
return
@pyqtSlot()
def showProjectWordListDialog(self) -> None:
@@ -818,7 +800,6 @@ class GuiMain(QMainWindow):
dialog = GuiWordList(self)
dialog.newWordListReady.connect(self._processWordListChanges)
dialog.exec()
return
@pyqtSlot()
def showWritingStatsDialog(self) -> None:
@@ -828,21 +809,18 @@ class GuiMain(QMainWindow):
dialog = GuiWritingStats(self)
dialog.activateDialog()
dialog.populateGUI()
return
@pyqtSlot()
def showAboutNWDialog(self) -> None:
"""Show the novelWriter about dialog."""
dialog = GuiAbout(self)
dialog.exec()
return
@pyqtSlot()
def showAboutQtDialog(self) -> None:
"""Show the Qt about dialog."""
msgBox = QMessageBox(self)
msgBox.aboutQt(self, "About Qt")
return
@pyqtSlot()
def showDictionariesDialog(self) -> None:
@@ -852,7 +830,6 @@ class GuiMain(QMainWindow):
if not dialog.initDialog():
dialog.close()
SHARED.error(self.tr("Could not initialise the dialog."))
return
##
# Main Window Actions
@@ -915,7 +892,6 @@ class GuiMain(QMainWindow):
self.refreshThemeColors(syntax=True)
self.docEditor.initEditor()
self.docViewer.initViewer()
return
def refreshThemeColors(self, syntax: bool = False, force: bool = False) -> None:
"""Refresh the GUI theme."""
@@ -937,8 +913,6 @@ class GuiMain(QMainWindow):
if syntax:
self.docEditor.updateSyntaxColors()
return
##
# Events
##
@@ -947,14 +921,12 @@ class GuiMain(QMainWindow):
"""Capture application change events."""
if int(event.type()) == 210: # ThemeChange
self.checkThemeUpdate()
return
def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the closing event of the GUI and call the close
function to handle all the close process steps.
"""
event.accept() if self.closeMain() else event.ignore()
return
##
# Public Slots
@@ -962,30 +934,26 @@ class GuiMain(QMainWindow):
@pyqtSlot()
def toggleFullScreenMode(self) -> None:
"""Toggle full screen mode"""
"""Toggle full screen mode."""
self.setWindowState(self.windowState() ^ Qt.WindowState.WindowFullScreen)
return
@pyqtSlot()
def closeDocEditor(self) -> None:
"""Close the document editor. This does not hide the editor."""
self.closeDocument()
SHARED.project.data.setLastHandle(None, "editor")
return
@pyqtSlot()
def closeDocViewer(self) -> None:
"""Close the document viewer."""
self.closeViewerPanel()
SHARED.project.data.setLastHandle(None, "viewer")
return
@pyqtSlot()
def toggleFocusMode(self) -> None:
"""Toggle focus mode."""
if self.docEditor.docHandle:
SHARED.setFocusMode(not SHARED.focusMode)
return
##
# Private Slots
@@ -1003,7 +971,6 @@ class GuiMain(QMainWindow):
docViewer = True
self.docEditor.changeFocusState(docEditor)
self.docViewer.changeFocusState(docViewer)
return
@pyqtSlot(bool)
def _focusModeChanged(self, focusMode: bool) -> None:
@@ -1034,7 +1001,6 @@ class GuiMain(QMainWindow):
if cursorVisible:
self.docEditor.ensureCursorVisibleNoCentre()
return
@pyqtSlot(nwFocus)
def _switchFocus(self, paneNo: nwFocus) -> None:
@@ -1082,8 +1048,6 @@ class GuiMain(QMainWindow):
self._changeView(nwView.OUTLINE, exitFocus=True)
self.outlineView.setTreeFocus()
return
@pyqtSlot(bool, bool, bool, bool)
def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: bool) -> None:
"""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."
))
return
@pyqtSlot()
def _processProjectSettingsChanges(self) -> None:
"""Refresh data dependent on project settings."""
@@ -1124,7 +1086,6 @@ class GuiMain(QMainWindow):
SHARED.updateSpellCheckLanguage()
self.itemDetails.refreshDetails()
self._updateWindowTitle(SHARED.project.data.name)
return
@pyqtSlot()
def _processWordListChanges(self) -> None:
@@ -1132,7 +1093,6 @@ class GuiMain(QMainWindow):
logger.debug("Reloading word list")
SHARED.updateSpellCheckLanguage(reload=True)
self.docEditor.spellCheckDocument()
return
@pyqtSlot(str, nwDocMode)
def _followTag(self, tag: str, mode: nwDocMode) -> None:
@@ -1151,7 +1111,6 @@ class GuiMain(QMainWindow):
self.openDocument(tHandle, sTitle=sTitle)
elif mode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return
@pyqtSlot(Path)
def _openProjectFromWelcome(self, path: Path) -> None:
@@ -1160,7 +1119,6 @@ class GuiMain(QMainWindow):
self.openProject(path)
if not SHARED.hasProject:
self.showWelcomeDialog()
return
@pyqtSlot(str, nwDocMode, str, bool)
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)
elif mode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return
@pyqtSlot(str, int, int, bool)
def _openDocumentSelection(
@@ -1179,7 +1136,6 @@ class GuiMain(QMainWindow):
"""Open a document and select a section of the text."""
if self.openDocument(tHandle, changeFocus=changeFocus):
self.docEditor.setCursorSelection(selStart, selLength)
return
@pyqtSlot()
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
self.saveDocument()
self.docViewer.reloadText()
return
@pyqtSlot(nwView)
def _changeView(self, view: nwView, exitFocus: bool = False) -> None:
@@ -1219,8 +1174,6 @@ class GuiMain(QMainWindow):
isNovel = self.projStack.currentWidget() == self.novelView
self.novelView.setActive(isMain and isNovel)
return
@pyqtSlot(nwDocAction)
def _passDocumentAction(self, action: nwDocAction) -> None:
"""Pass on a document action to the editor or viewer based on
@@ -1230,7 +1183,6 @@ class GuiMain(QMainWindow):
self.docEditor.docAction(action)
elif self.docViewer.hasFocus():
self.docViewer.docAction(action)
return
@pyqtSlot(str)
@pyqtSlot(nwDocInsert)
@@ -1240,14 +1192,12 @@ class GuiMain(QMainWindow):
"""
if self.docEditor.hasFocus():
self.docEditor.insertText(content)
return
@pyqtSlot()
def _toggleViewerPanelVisibility(self) -> None:
"""Toggle the visibility of the document viewer panel."""
CONFIG.showViewerPanel = not CONFIG.showViewerPanel
self.docViewerPanel.setVisible(CONFIG.showViewerPanel)
return
@pyqtSlot()
def _timeTick(self) -> None:
@@ -1263,7 +1213,6 @@ class GuiMain(QMainWindow):
self._updateStatusWordCount()
if CONFIG.memInfo: # pragma: no cover
self.mainStatus.memInfo()
return
@pyqtSlot()
def _autoSaveProject(self) -> None:
@@ -1274,7 +1223,6 @@ class GuiMain(QMainWindow):
if doSave:
logger.debug("Auto-saving project")
self.saveProject(autoSave=True)
return
@pyqtSlot()
def _autoSaveDocument(self) -> None:
@@ -1282,7 +1230,6 @@ class GuiMain(QMainWindow):
if SHARED.hasProject and self.docEditor.docChanged:
logger.debug("Auto-saving document")
self.saveDocument()
return
@pyqtSlot()
def _updateStatusWordCount(self) -> None:
@@ -1312,8 +1259,6 @@ class GuiMain(QMainWindow):
self.mainStatus.setProjectStats(cTotal, cTotal - iTotal)
return
@pyqtSlot()
def _keyPressReturn(self) -> None:
"""Process a return or enter keypress in the main window."""
@@ -1321,7 +1266,6 @@ class GuiMain(QMainWindow):
self.projSearch.processReturn()
else:
self.openSelectedItem()
return
@pyqtSlot()
def _keyPressEscape(self) -> None:
@@ -1330,7 +1274,6 @@ class GuiMain(QMainWindow):
self.docEditor.closeSearch()
elif SHARED.focusMode:
SHARED.setFocusMode(False)
return
@pyqtSlot(int)
def _mainStackChanged(self, index: int) -> None:
@@ -1338,7 +1281,6 @@ class GuiMain(QMainWindow):
if self.mainStack.widget(index) == self.outlineView:
if SHARED.hasProject:
self.outlineView.refreshTree()
return
@pyqtSlot(int)
def _projStackChanged(self, index: int) -> None:
@@ -1350,7 +1292,6 @@ class GuiMain(QMainWindow):
elif widget == self.novelView:
sHandle, _ = self.novelView.getSelectedHandle()
self.itemDetails.updateViewBox(sHandle)
return
##
# Internal Functions
@@ -1363,9 +1304,7 @@ class GuiMain(QMainWindow):
width = minmax(size[0], 900, availSize.width())
height = minmax(size[1], 500, availSize.height())
self.resize(width, height)
return
def _updateWindowTitle(self, projName: str | None = None) -> None:
"""Set the window title and add the project's name."""
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -57,6 +57,12 @@ RX_HTML = re.compile(r"<.*?>")
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__ = (
"_gui", "_idleRefTime", "_idleTime", "_lastAlert", "_lockedBy",
@@ -96,8 +102,6 @@ class SharedData(QObject):
self._clock.setInterval(1000)
self._clock.timeout.connect(lambda: self.mainClockTick.emit())
return
##
# Properties
##
@@ -169,7 +173,6 @@ class SharedData(QObject):
if state is not self._focusMode:
self._focusMode = state
self.focusModeChanged.emit(state)
return
##
# Methods
@@ -181,7 +184,6 @@ class SharedData(QObject):
"""
self._theme = theme
self._theme.initThemes()
return
def initSharedData(self, gui: GuiMain) -> None:
"""Initialise the SharedData instance. This must be called as
@@ -194,7 +196,6 @@ class SharedData(QObject):
logger.debug("Ready: SharedData")
if pool := QThreadPool.globalInstance():
logger.debug("Thread Pool Max Count: %d", pool.maxThreadCount())
return
def closeDocument(self, tHandle: str | None = None) -> None:
"""Close the document editor, optionally a specific document."""
@@ -202,7 +203,6 @@ class SharedData(QObject):
self.mainGui.closeDocument()
if tHandle is None or tHandle == self.mainGui.docViewer.docHandle:
self.mainGui.closeViewerPanel()
return
def saveEditor(self, tHandle: str | None = None) -> None:
"""Save the editor content, optionally a specific document."""
@@ -213,7 +213,6 @@ class SharedData(QObject):
):
logger.debug("Saving editor document before action")
docEditor.saveText()
return
def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
"""Open a project."""
@@ -246,7 +245,6 @@ class SharedData(QObject):
self.project.closeProject(self._idleTime)
self._resetProject()
self._resetIdleTimer()
return
def updateSpellCheckLanguage(self, reload: bool = False) -> None:
"""Update the active spell check language from settings."""
@@ -256,7 +254,6 @@ class SharedData(QObject):
self.spelling.setLanguage(language)
_, provider = self.spelling.describeDict()
self.spellLanguageChanged.emit(language, provider)
return
def updateIdleTime(self, currTime: float, userIdle: bool) -> None:
"""Update the idle time record. If the userIdle flag is True,
@@ -267,47 +264,40 @@ class SharedData(QObject):
if userIdle:
self._idleTime += currTime - self._idleRefTime
self._idleRefTime = currTime
return
def initMainProgress(self, maximum: int, inclusive: bool = False) -> None:
"""Start a session for the main progress bar."""
if gui := self._gui:
gui.mainProgress.setMaximum(maximum - (1 if inclusive else 0))
gui.mainProgress.setValue(0)
return
def incMainProgress(self) -> None:
"""Increment the value for the main progress bar."""
if gui := self._gui:
gui.mainProgress.setValue(gui.mainProgress.value() + 1)
QApplication.processEvents()
return
def clearMainProgress(self, delay: float = 1.0) -> None:
"""Clear the main progress bar."""
if gui := self._gui:
QTimer.singleShot(int(delay*1000), gui.mainProgress.reset)
return
def newStatusMessage(self, message: str) -> None:
"""Request a new status message. This is a callable function for
core classes that cannot emit signals on their own.
"""
self.projectStatusMessage.emit(message)
return
def setGlobalProjectState(self, state: bool) -> None:
"""Change the global project status. This is a callable function
for core classes that cannot emit signals on their own.
"""
self.projectStatusChanged.emit(state)
return
def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None:
"""Queue a runnable in the application thread pool."""
if pool := QThreadPool.globalInstance():
pool.start(runnable, priority=priority)
return
def getProjectPath(
self, parent: QWidget,
@@ -346,13 +336,11 @@ class SharedData(QObject):
def openWebsite(self, url: str) -> None:
"""Open a URL in the system's default browser."""
QDesktopServices.openUrl(QUrl(url))
return
@pyqtSlot(str, nwItemClass)
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
"""Process new note request."""
self.project.createNewNote(tag, itemClass)
return
##
# Signal Proxies
@@ -364,37 +352,31 @@ class SharedData(QObject):
"""Emit the indexChangedTags signal."""
if self._project and self._project.data.uuid == project.data.uuid:
self.indexChangedTags.emit(updated, deleted)
return
def emitIndexCleared(self, project: NWProject) -> None:
"""Emit the indexCleared signal."""
if self._project and self._project.data.uuid == project.data.uuid:
self.indexCleared.emit()
return
def emitIndexAvailable(self, project: NWProject) -> None:
"""Emit the indexAvailable signal."""
if self._project and self._project.data.uuid == project.data.uuid:
self.indexAvailable.emit()
return
def emitStatusLabelsChanged(self, project: NWProject, kind: T_StatusKind) -> None:
"""Emit the statusLabelsChanged signal."""
if self._project and self._project.data.uuid == project.data.uuid:
self.statusLabelsChanged.emit(kind)
return
def emitProjectItemChanged(self, project: NWProject, handle: str, change: nwChange) -> None:
"""Emit the projectItemChanged signal."""
if self._project and self._project.data.uuid == project.data.uuid:
self.projectItemChanged.emit(handle, change)
return
def emitRootFolderChanged(self, project: NWProject, handle: str, change: nwChange) -> None:
"""Emit the rootFolderChanged signal."""
if self._project and self._project.data.uuid == project.data.uuid:
self.rootFolderChanged.emit(handle, change)
return
##
# Alert Boxes
@@ -409,7 +391,6 @@ class SharedData(QObject):
if log:
self._logMessage(self._lastAlert, logger.info)
alert.exec()
return
def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None:
"""Open a warning alert box."""
@@ -420,7 +401,6 @@ class SharedData(QObject):
if log:
self._logMessage(self._lastAlert, logger.warning)
alert.exec()
return
def error(self, text: str, info: str = "", details: str = "", log: bool = True,
exc: Exception | None = None) -> None:
@@ -434,7 +414,6 @@ class SharedData(QObject):
if log:
self._logMessage(self._lastAlert, logger.error)
alert.exec()
return
def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool:
"""Open a question box."""
@@ -443,8 +422,7 @@ class SharedData(QObject):
alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True)
self._lastAlert = alert.logMessage
alert.exec()
isYes = alert.result() == QMessageBox.StandardButton.Yes
return isYes
return alert.result() == QMessageBox.StandardButton.Yes
##
# Internal Functions
@@ -454,7 +432,6 @@ class SharedData(QObject):
"""Print message to log."""
for text in message.split("<br>"):
log(RX_HTML.sub("", text), stacklevel=3)
return
def _resetProject(self) -> None:
"""Create a new project and spell checking instance."""
@@ -467,13 +444,11 @@ class SharedData(QObject):
self._spelling = NWSpellEnchant(self._project)
self.updateSpellCheckLanguage()
self._focusMode = False
return
def _resetIdleTimer(self) -> None:
"""Reset the timer data for the idle timer."""
self._idleRefTime = time()
self._idleTime = 0.0
return
def _closeToolDialogs(self) -> None:
"""Close all open tool dialogs."""
@@ -481,7 +456,6 @@ class SharedData(QObject):
for widget in self.mainGui.children():
if isinstance(widget, NToolDialog):
widget.close()
return
class _GuiAlert(QMessageBox):
@@ -496,11 +470,9 @@ class _GuiAlert(QMessageBox):
self._theme = theme
self._message = ""
logger.debug("Ready: _GuiAlert")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: _GuiAlert")
return
@property
def logMessage(self) -> str:
@@ -512,14 +484,12 @@ class _GuiAlert(QMessageBox):
self.setText(text)
self.setInformativeText(info)
self.setDetailedText(details)
return
def setException(self, exception: Exception) -> None:
"""Add exception details."""
info = self.informativeText()
text = f"<b>{type(exception).__name__}</b>: {exception!s}"
self.setInformativeText(f"{info}<br>{text}" if info else text)
return
def setAlertType(self, level: int, isYesNo: bool) -> None:
"""Set the type of alert and whether the dialog should have
@@ -542,4 +512,3 @@ class _GuiAlert(QMessageBox):
elif level == self.ASK:
self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz), "blue"))
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -38,6 +38,14 @@ SPLASH_IMG = Path(__file__).parent / "assets" / "images" / "splash.png"
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")
@@ -52,17 +60,14 @@ class NSplashScreen(QSplashScreen):
self._color = QColor(26, 52, 78)
self._rect = QRect(144, 110, 440, 30)
self._text = ""
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NSplashScreen")
return
def drawContents(self, painter: QPainter) -> None:
"""Draw the text message."""
painter.setPen(self._color)
painter.drawText(self._rect, Qt.AlignmentFlag.AlignLeft, self._text)
return
def showStatus(self, message: str) -> None:
"""Update the status message."""
@@ -71,4 +76,3 @@ class NSplashScreen(QSplashScreen):
if message:
logger.info("[Splash] %s", message)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import re
@@ -74,9 +74,11 @@ def preProcessText(text: str, keepHeaders: bool = True) -> list[str]:
def standardCounter(text: str) -> tuple[int, int, int]:
"""A counter that counts paragraphs, words and characters.
This is the standard counter that includes headings in the word and
character counts.
"""Return a standard count.
A counter that counts paragraphs, words and characters. This is the
standard counter that includes headings in the word and character
counts.
"""
cCount = 0
wCount = 0
@@ -124,7 +126,9 @@ def standardCounter(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.
"""
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import re
@@ -32,6 +32,7 @@ from novelwriter.constants import nwRegEx, nwUnicode
class RegExPatterns:
"""Compiled RegEx Patterns."""
AMBIGUOUS = (nwUnicode.U_APOS, nwUnicode.U_RSQUO)
@@ -132,6 +133,7 @@ REGEX_PATTERNS = RegExPatterns()
class DialogParser:
"""A callable parser for finding dialog regions in text."""
__slots__ = (
"_alternate", "_breakD", "_breakQ", "_dialog", "_enabled", "_mode",
@@ -147,7 +149,6 @@ class DialogParser:
self._breakD = None
self._breakQ = None
self._mode = ""
return
@property
def enabled(self) -> bool:
@@ -174,8 +175,6 @@ class DialogParser:
self._narrator = narrator
self._mode = f" {narrator}"
return
def __call__(self, text: str) -> list[tuple[int, int]]:
"""Caller wrapper for dialogue processing."""
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -45,6 +45,12 @@ logger = logging.getLogger(__name__)
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:
super().__init__(parent=parent)
@@ -123,11 +129,8 @@ class GuiDictionaries(NNonBlockingDialog):
logger.debug("Ready: GuiDictionaries")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiDictionaries")
return
def initDialog(self) -> bool:
"""Prepare and check that we can proceed."""
@@ -164,7 +167,6 @@ class GuiDictionaries(NNonBlockingDialog):
"""Capture the user closing the window."""
event.accept()
self.softDelete()
return
##
# Private Slots
@@ -182,7 +184,6 @@ class GuiDictionaries(NNonBlockingDialog):
if soxFile:
path = Path(soxFile).absolute()
self.huInput.setText(str(path))
return
@pyqtSlot()
def _doImportHunspell(self) -> None:
@@ -202,14 +203,12 @@ class GuiDictionaries(NNonBlockingDialog):
self._appendLog(formatException(exc), err=True)
else:
self._appendLog(procErr, err=True)
return
@pyqtSlot()
def _doOpenInstallLocation(self) -> None:
"""Open the dictionary folder."""
if not openExternalPath(Path(self.inPath.text())):
SHARED.error("Path not found.")
return
##
# Internal Functions
@@ -247,4 +246,3 @@ class GuiDictionaries(NNonBlockingDialog):
cursor.movePosition(QTextCursor.MoveOperation.End)
cursor.deleteChar()
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
class GuiLipsum(NDialog):
"""GUI: Lorem Ipsum Text Tool."""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
@@ -111,11 +112,8 @@ class GuiLipsum(NDialog):
logger.debug("Ready: GuiLipsum")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiLipsum")
return
@property
def lipsumText(self) -> str:
@@ -145,4 +143,3 @@ class GuiLipsum(NDialog):
pCount = self.paraCount.value()
self._lipsumText = "\n\n".join(lipsumText[0:pCount]) + "\n\n"
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -54,7 +54,7 @@ logger = logging.getLogger(__name__)
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
independently of the Manuscript Build Tool.
@@ -244,11 +244,8 @@ class GuiManuscriptBuild(NDialog):
logger.debug("Ready: GuiManuscriptBuild")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiManuscriptBuild")
return
##
# Events
@@ -261,7 +258,6 @@ class GuiManuscriptBuild(NDialog):
self._saveSettings()
event.accept()
self.softDelete()
return
##
# Private Slots
@@ -278,7 +274,6 @@ class GuiManuscriptBuild(NDialog):
self._openOutputFolder()
elif role == QtRoleReject:
self.close()
return
@pyqtSlot()
def _doSelectPath(self) -> None:
@@ -290,7 +285,6 @@ class GuiManuscriptBuild(NDialog):
)
if savePath:
self.buildPath.setText(savePath)
return
@pyqtSlot()
def _doResetBuildName(self) -> None:
@@ -298,13 +292,11 @@ class GuiManuscriptBuild(NDialog):
bName = f"{SHARED.project.data.name} - {self._build.name}"
self.buildName.setText(bName)
self._build.setLastBuildName(bName)
return
@pyqtSlot()
def _resetProgress(self) -> None:
"""Set the progress bar back to 0."""
self.buildProgress.setValue(0)
return
##
# Internal Functions
@@ -371,7 +363,6 @@ class GuiManuscriptBuild(NDialog):
pOptions.setValue("GuiManuscriptBuild", "fmtWidth", mainSplit[0])
pOptions.setValue("GuiManuscriptBuild", "sumWidth", mainSplit[1])
pOptions.saveSettings()
return
def _populateContentList(self) -> None:
"""Build the content list."""
@@ -396,9 +387,6 @@ class GuiManuscriptBuild(NDialog):
item.setIcon(nwItem.getMainIcon())
self.listContent.addItem(item)
return
def _openOutputFolder(self) -> None:
"""Open the build folder in the system's file explorer."""
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -66,7 +66,7 @@ logger = logging.getLogger(__name__)
class GuiManuscript(NToolDialog):
"""GUI Tools: Manuscript Tool
"""GUI Tools: Manuscript Tool.
The dialog displays all the users build definitions, a preview panel
for the manuscript, and can trigger the actual build dialog to build
@@ -251,11 +251,8 @@ class GuiManuscript(NToolDialog):
logger.debug("Ready: GuiManuscript")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiManuscript")
return
def loadContent(self) -> None:
"""Load dialog content from project data."""
@@ -272,8 +269,6 @@ class GuiManuscript(NToolDialog):
self.buildList.setCurrentItem(self._buildMap[selected])
QTimer.singleShot(200, self._generatePreview)
return
##
# Events
##
@@ -290,7 +285,6 @@ class GuiManuscript(NToolDialog):
obj.close()
event.accept()
self.softDelete()
return
##
# Private Slots
@@ -302,14 +296,12 @@ class GuiManuscript(NToolDialog):
build = BuildSettings()
build.setName(self.tr("My Manuscript"))
self._openSettingsDialog(build)
return
@pyqtSlot()
def _editSelectedBuild(self) -> None:
"""Edit the currently selected build settings entry."""
if build := self._getSelectedBuild():
self._openSettingsDialog(build)
return
@pyqtSlot()
def _copySelectedBuild(self) -> None:
@@ -320,14 +312,12 @@ class GuiManuscript(NToolDialog):
self._updateBuildsList()
if item := self._buildMap.get(new.buildID):
item.setSelected(True)
return
@pyqtSlot("QListWidgetItem*", "QListWidgetItem*")
def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None:
"""Process change of build selection to update the details."""
if current and (build := self._builds.getBuild(current.data(self.D_KEY))):
self.buildDetails.updateInfo(build)
return
@pyqtSlot()
def _deleteSelectedBuild(self) -> None:
@@ -338,7 +328,6 @@ class GuiManuscript(NToolDialog):
dialog.close()
self._builds.removeBuild(build.buildID)
self._updateBuildsList()
return
@pyqtSlot(BuildSettings)
def _processNewSettings(self, build: BuildSettings) -> None:
@@ -347,7 +336,6 @@ class GuiManuscript(NToolDialog):
self._updateBuildItem(build)
if (current := self.buildList.currentItem()) and current.data(self.D_KEY) == build.buildID:
self._updateBuildDetails(current, current)
return
@pyqtSlot()
def _generatePreview(self) -> None:
@@ -399,15 +387,12 @@ class GuiManuscript(NToolDialog):
if build.changed:
self._builds.setBuild(build)
return
@pyqtSlot()
def _printDocument(self) -> None:
"""Open the print preview dialog."""
preview = QPrintPreviewDialog(self)
preview.paintRequested.connect(self.docPreview.printPreview)
preview.exec()
return
##
# Internal Functions
@@ -456,8 +441,6 @@ class GuiManuscript(NToolDialog):
pOptions.setValue("GuiManuscript", "showNewPage", showNewPage)
pOptions.saveSettings()
return
def _openSettingsDialog(self, build: BuildSettings) -> None:
"""Open the build settings dialog."""
if dialog := self._findSettingsDialog(build.buildID):
@@ -482,7 +465,6 @@ class GuiManuscript(NToolDialog):
bItem.setData(self.D_KEY, key)
self.buildList.addItem(bItem)
self._buildMap[key] = bItem
return
def _updateBuildItem(self, build: BuildSettings) -> None:
"""Update the entry of a specific build item."""
@@ -490,7 +472,6 @@ class GuiManuscript(NToolDialog):
item.setText(build.name)
else: # Probably a new item
self._updateBuildsList()
return
def _findSettingsDialog(self, buildID: str) -> GuiBuildSettings | None:
"""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.setLayout(self.outerBox)
return
##
# Getters
##
@@ -547,7 +526,6 @@ class _DetailsWidget(QWidget):
def setColumnWidth(self, value: int) -> None:
"""Set the width of the first column."""
self.listView.setColumnWidth(0, value)
return
def setExpandedState(self, state: list[bool]) -> None:
"""Set the expanded state of each top level item."""
@@ -556,7 +534,6 @@ class _DetailsWidget(QWidget):
item = self.listView.topLevelItem(i)
if isinstance(item, QTreeWidgetItem):
item.setExpanded((state[i] if i < count else True) and item.childCount() > 0)
return
##
# Methods
@@ -637,8 +614,6 @@ class _DetailsWidget(QWidget):
# Restore expanded state
self.setExpandedState(expanded)
return
class _OutlineWidget(QWidget):
@@ -663,8 +638,6 @@ class _OutlineWidget(QWidget):
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox)
return
def updateOutline(self, data: dict[str, str]) -> None:
"""Update the 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._outline = data
return
##
# Private Slots
##
@@ -714,7 +685,6 @@ class _OutlineWidget(QWidget):
def _onItemClick(self, item: QTreeWidgetItem) -> None:
"""Process tree item click."""
self.outlineEntryClicked.emit(str(item.data(0, self.D_LINE)))
return
class _PreviewWidget(QTextBrowser):
@@ -787,8 +757,6 @@ class _PreviewWidget(QTextBrowser):
self.ageTimer.timeout.connect(self._updateBuildAge)
self.ageTimer.start()
return
##
# Setters
##
@@ -797,7 +765,6 @@ class _PreviewWidget(QTextBrowser):
"""Set the build name for the document label."""
self._buildName = name
self._updateBuildAge()
return
def setTextFont(self, font: QFont) -> None:
"""Set the text font properties and then reset for sub-widgets.
@@ -807,7 +774,6 @@ class _PreviewWidget(QTextBrowser):
self.setFont(font)
self.buildProgress.setFont(SHARED.theme.guiFont)
self.ageLabel.setFont(SHARED.theme.guiFontSmall)
return
##
# Methods
@@ -823,13 +789,11 @@ class _PreviewWidget(QTextBrowser):
self._scrollPos = vBar.value()
self.setPlaceholderText("")
self.clear()
return
def buildStep(self, value: int) -> None:
"""Update the progress bar value."""
self.buildProgress.setValue(value)
QApplication.processEvents()
return
def setContent(self, document: QTextDocument) -> None:
"""Set the content of the preview widget."""
@@ -850,8 +814,6 @@ class _PreviewWidget(QTextBrowser):
QApplication.processEvents()
QTimer.singleShot(300, self._postUpdate)
return
##
# Events
##
@@ -860,7 +822,6 @@ class _PreviewWidget(QTextBrowser):
"""Capture resize and update the document margins."""
super().resizeEvent(event)
self._updateDocMargins()
return
##
# Public Slots
@@ -874,14 +835,12 @@ class _PreviewWidget(QTextBrowser):
printer.setPageOrientation(QPageLayout.Orientation.Portrait)
document.print(printer)
QApplication.restoreOverrideCursor()
return
@pyqtSlot(str)
def navigateTo(self, anchor: str) -> None:
"""Go to a specific #link in the document."""
logger.debug("Moving to anchor '#%s'", anchor)
self.setSource(QUrl(f"#{anchor}"))
return
##
# Private Slots
@@ -896,7 +855,6 @@ class _PreviewWidget(QTextBrowser):
self.navigateTo(link.lstrip("#"))
elif link.startswith("http"):
QDesktopServices.openUrl(QUrl(url))
return
@pyqtSlot()
def _updateBuildAge(self) -> None:
@@ -909,7 +867,6 @@ class _PreviewWidget(QTextBrowser):
))
else:
self.ageLabel.setText("<b>{0}</b>".format(self.tr("No Preview")))
return
@pyqtSlot()
def _postUpdate(self) -> None:
@@ -917,7 +874,6 @@ class _PreviewWidget(QTextBrowser):
self.buildProgress.setVisible(False)
if vBar := self.verticalScrollBar():
vBar.setValue(self._scrollPos)
return
##
# Internal Functions
@@ -936,7 +892,6 @@ class _PreviewWidget(QTextBrowser):
self.ageLabel.setGeometry(tB, tB, vW, tH)
self.setViewportMargins(0, tH, 0, 0)
self.buildProgress.move((vW-pS)//2, (vH-pS)//2)
return
class _StatsWidget(QWidget):
@@ -969,8 +924,6 @@ class _StatsWidget(QWidget):
self._toggleView(False)
return
def updateStats(self, data: dict[str, int]) -> None:
"""Update the stats values from a Tokenizer stats dict."""
# Minimal
@@ -992,8 +945,6 @@ class _StatsWidget(QWidget):
self.maxHeadWordChars.setText(f"{data.get(nwStats.WCHARS_TITLE, 0):n}")
self.maxTextWordChars.setText(f"{data.get(nwStats.WCHARS_TEXT, 0):n}")
return
##
# Private Slots
##
@@ -1013,7 +964,6 @@ class _StatsWidget(QWidget):
self.minWidget.adjustSize()
self.mainStack.adjustSize()
self.adjustSize()
return
##
# Internal Functions
@@ -1105,5 +1055,3 @@ class _StatsWidget(QWidget):
self.minWidget.setLayout(self.minLayout)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -62,7 +62,7 @@ logger = logging.getLogger(__name__)
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
editing JSON build definitions, wrapped as a BuildSettings object.
@@ -153,11 +153,8 @@ class GuiBuildSettings(NToolDialog):
logger.debug("Ready: GuiBuildSettings")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiBuildSettings")
return
def loadContent(self) -> None:
"""Populate the child widgets."""
@@ -165,7 +162,6 @@ class GuiBuildSettings(NToolDialog):
self.optTabSelect.loadContent()
self.optTabHeadings.loadContent()
self.optTabFormatting.loadContent()
return
##
# Properties
@@ -190,7 +186,6 @@ class GuiBuildSettings(NToolDialog):
self._saveSettings()
event.accept()
self.softDelete()
return
##
# Private Slots
@@ -206,7 +201,6 @@ class GuiBuildSettings(NToolDialog):
elif pageId >= self.OPT_FORMATTING:
self.toolStack.setCurrentWidget(self.optTabFormatting)
self.optTabFormatting.scrollToSection(pageId)
return
@pyqtSlot("QAbstractButton*")
def _dialogButtonClicked(self, button: QAbstractButton) -> None:
@@ -222,7 +216,6 @@ class GuiBuildSettings(NToolDialog):
elif role == QtRoleReject:
self._build.resetChangedState()
self.close()
return
##
# Internal Functions
@@ -238,7 +231,6 @@ class GuiBuildSettings(NToolDialog):
).format(self._build.name)):
self._emitBuildData()
self._build.resetChangedState()
return
def _saveSettings(self) -> None:
"""Save the various user settings."""
@@ -250,20 +242,17 @@ class GuiBuildSettings(NToolDialog):
pOptions.setValue("GuiBuildSettings", "treeWidth", treeWidth)
pOptions.setValue("GuiBuildSettings", "filterWidth", filterWidth)
pOptions.saveSettings()
return
def _applyChanges(self) -> None:
"""Apply all settings changes to the build object."""
self._build.setName(self.editBuildName.text())
self.optTabHeadings.saveContent()
self.optTabFormatting.saveContent()
return
def _emitBuildData(self) -> None:
"""Assemble the build data and emit the signal."""
self.newSettingsReady.emit(self._build)
self._build.resetChangedState()
return
class _FilterTab(NFixedPage):
@@ -382,13 +371,10 @@ class _FilterTab(NFixedPage):
self.setCentralWidget(self.mainSplit)
return
def loadContent(self) -> None:
"""Populate the widgets."""
self._populateTree()
self._populateFilters()
return
def mainSplitSizes(self) -> tuple[int, int]:
"""Extract the sizes of the main splitter."""
@@ -409,7 +395,6 @@ class _FilterTab(NFixedPage):
elif key.startswith("root:"):
self._build.setAllowRoot(key[5:], state)
self._populateTree()
return
##
# Internal Functions
@@ -453,8 +438,6 @@ class _FilterTab(NFixedPage):
self._setTreeItemMode()
return
def _populateFilters(self) -> None:
"""Populate the filter options switches."""
self.filterOpt.clear()
@@ -489,8 +472,6 @@ class _FilterTab(NFixedPage):
default=self._build.isRootAllowed(tHandle)
)
return
def _setSelectedMode(self, mode: int) -> None:
"""Set the mode for the selected items."""
items = self.optTree.selectedItems()
@@ -511,8 +492,6 @@ class _FilterTab(NFixedPage):
self._setTreeItemMode()
return
def _setTreeItemMode(self) -> None:
"""Update the filtered mode icon on all items."""
filtered = self._build.buildItemFilter(SHARED.project)
@@ -529,11 +508,10 @@ class _FilterTab(NFixedPage):
item.setToolTip(self.C_STATUS, self._trIncluded)
else:
item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE])
return
def _scanChildren(self, item: QTreeWidgetItem | None, items: list) -> list[QTreeWidgetItem]:
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
"""Recursively return all items in a tree starting at a given
QTreeWidgetItem.
"""
if isinstance(item, QTreeWidgetItem):
items.append(item)
@@ -791,8 +769,6 @@ class _HeadingsTab(NScrollablePage):
self.setCentralLayout(self.outerBox)
return
def loadContent(self) -> None:
"""Populate the widgets."""
def fmtBreak(text: str) -> str:
@@ -821,7 +797,6 @@ class _HeadingsTab(NScrollablePage):
self.breakPart.setChecked(self._build.getBool("headings.breakPart"))
self.breakChapter.setChecked(self._build.getBool("headings.breakChapter"))
self.breakScene.setChecked(self._build.getBool("headings.breakScene"))
return
def saveContent(self) -> None:
"""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.breakChapter", self.breakChapter.isChecked())
self._build.setValue("headings.breakScene", self.breakScene.isChecked())
return
##
# Internal Functions
@@ -853,7 +827,6 @@ class _HeadingsTab(NScrollablePage):
cursor = self.editTextBox.textCursor()
cursor.insertText(text)
self.editTextBox.setFocus()
return
def _editHeading(self, heading: int) -> None:
"""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.lblEditForm.setText(self.tr("Editing: {0}").format(label))
return
##
# Private Slots
##
@@ -934,7 +905,6 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter):
self._fmtSymbol.setForeground(syntax.head)
self._fmtFormat = QTextCharFormat()
self._fmtFormat.setForeground(syntax.emph)
return
def highlightBlock(self, text: str) -> None:
"""Add syntax highlighting to the text block."""
@@ -947,7 +917,6 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter):
ddots = heading.find(":")
if ddots > 0:
self.setFormat(pos + ddots, 1, self._fmtSymbol)
return
class _FormattingTab(NScrollableForm):
@@ -961,8 +930,6 @@ class _FormattingTab(NScrollableForm):
self.setHelpTextStyle(SHARED.theme.helpText)
self.buildForm()
return
def buildForm(self) -> None:
"""Build the formatting form."""
section = 10
@@ -1290,8 +1257,6 @@ class _FormattingTab(NScrollableForm):
# Finalise
self.finalise()
return
def loadContent(self) -> None:
"""Populate the widgets."""
# Text Content
@@ -1391,8 +1356,6 @@ class _FormattingTab(NScrollableForm):
self.htmlAddStyles.setChecked(self._build.getBool("html.addStyles"))
self.htmlPreserveTabs.setChecked(self._build.getBool("html.preserveTabs"))
return
def saveContent(self) -> None:
"""Save choices back into build object."""
# Text Content
@@ -1458,8 +1421,6 @@ class _FormattingTab(NScrollableForm):
self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked())
self._build.setValue("html.preserveTabs", self.htmlPreserveTabs.isChecked())
return
##
# Private Slots
##
@@ -1472,11 +1433,10 @@ class _FormattingTab(NScrollableForm):
self._textFont = fontMatcher(font)
self.textFont.setText(describeFont(self._textFont))
self.textFont.setCursorPosition(0)
return
@pyqtSlot(int)
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)
newScale = nwLabels.UNIT_SCALE.get(newUnit, 1.0)
reScale = self._unitScale/newScale
@@ -1531,11 +1491,9 @@ class _FormattingTab(NScrollableForm):
self._unitScale = newScale
self._changePageSize(self.pageSize.currentIndex())
return
@pyqtSlot(int)
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)
if w > 0.0 and h > 0.0:
self.pageWidth.blockSignals(True)
@@ -1544,23 +1502,20 @@ class _FormattingTab(NScrollableForm):
self.pageHeight.blockSignals(True)
self.pageHeight.setValue(h/self._unitScale)
self.pageHeight.blockSignals(False)
return
@pyqtSlot()
def _pageSizeValueChanged(self) -> None:
"""The user has changed the page size spin boxes, so we flip
the page size box to Custom.
"""Process that the user has changed the page size spin boxes,
so we flip the page size box to Custom.
"""
index = self.pageSize.findData("Custom")
if index >= 0:
self.pageSize.setCurrentIndex(index)
return
def _resetPageHeader(self) -> None:
"""Reset the ODT header format to default."""
self.odtPageHeader.setText(nwHeadFmt.DOC_AUTO)
self.odtPageHeader.setCursorPosition(0)
return
##
# Internal Functions
@@ -1573,4 +1528,3 @@ class _FormattingTab(NScrollableForm):
current.append(keyword)
verified = set(x for x in current if x in nwKeyWords.VALID_KEYS)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -52,6 +52,7 @@ logger = logging.getLogger(__name__)
class GuiNovelDetails(NNonBlockingDialog):
"""GUI: Novel Details Tool."""
PAGE_OVERVIEW = 1
PAGE_CONTENTS = 2
@@ -130,11 +131,8 @@ class GuiNovelDetails(NNonBlockingDialog):
logger.debug("Ready: GuiNovelDetails")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiNovelDetails")
return
##
# Methods
@@ -146,7 +144,6 @@ class GuiNovelDetails(NNonBlockingDialog):
self.overviewPage.updateProjectData()
self.overviewPage.novelValueChanged(handle)
self.contentsPage.novelValueChanged(handle)
return
##
# Events
@@ -157,7 +154,6 @@ class GuiNovelDetails(NNonBlockingDialog):
self._saveSettings()
event.accept()
self.softDelete()
return
##
# Private Slots
@@ -170,7 +166,6 @@ class GuiNovelDetails(NNonBlockingDialog):
self.mainStack.setCurrentWidget(self.overviewPage)
elif pageId == self.PAGE_CONTENTS:
self.mainStack.setCurrentWidget(self.contentsPage)
return
##
# Internal Functions
@@ -185,7 +180,6 @@ class GuiNovelDetails(NNonBlockingDialog):
options.setValue("GuiNovelDetails", "winHeight", self.height())
options.setValue("GuiNovelDetails", "novelRoot", novelRoot)
self.contentsPage.saveSettings()
return
class _OverviewPage(NScrollablePage):
@@ -248,8 +242,6 @@ class _OverviewPage(NScrollablePage):
self.setCentralLayout(self.outerBox)
return
##
# Methods
##
@@ -266,7 +258,6 @@ class _OverviewPage(NScrollablePage):
self.projWords.setText(f"{wcNovel + wcNotes:n}")
self.projNovels.setText(f"{wcNovel:n}")
self.projNotes.setText(f"{wcNotes:n}")
return
##
# Public Slots
@@ -286,8 +277,6 @@ class _OverviewPage(NScrollablePage):
self.novelChapters.setText(f"{hCounts[2]:n}")
self.novelScenes.setText(f"{hCounts[3]:n}")
return
class _ContentsPage(NFixedPage):
@@ -400,8 +389,6 @@ class _ContentsPage(NFixedPage):
self.setCentralLayout(self.outerBox)
return
def saveSettings(self) -> None:
"""Save the user GUI settings."""
options = SHARED.project.options
@@ -413,7 +400,6 @@ class _ContentsPage(NFixedPage):
options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value())
options.setValue("GuiNovelDetails", "countFrom", self.poValue.value())
options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked())
return
##
# Public Slots
@@ -426,7 +412,6 @@ class _ContentsPage(NFixedPage):
self._prepareData(tHandle)
self._populateTree()
self._currentRoot = tHandle
return
##
# Private Slots
@@ -496,8 +481,6 @@ class _ContentsPage(NFixedPage):
self.tocTree.addTopLevelItem(newItem)
return
##
# Internal Functions
##
@@ -507,4 +490,3 @@ class _ContentsPage(NFixedPage):
logger.debug("Populating ToC from handle '%s'", rootHandle)
self._data = SHARED.project.index.getTableOfContents(rootHandle, 2)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -56,6 +56,11 @@ PANEL_ALPHA = 178
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)
@@ -161,11 +166,8 @@ class GuiWelcome(NDialog):
logger.debug("Ready: GuiWelcome")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiWelcome")
return
##
# Events
@@ -180,14 +182,12 @@ class GuiWelcome(NDialog):
painter.drawPixmap(0, hWin - hPix, self.bgImage.scaledToHeight(hPix, tMode))
painter.end()
super().paintEvent(event)
return
def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the user closing the window and save settings."""
self._saveSettings()
event.accept()
self.softDelete()
return
##
# Private Slots
@@ -199,28 +199,24 @@ class GuiWelcome(NDialog):
self.mainStack.setCurrentWidget(self.tabNew)
self._setButtonVisibility()
self.tabNew.enterForm()
return
@pyqtSlot()
def _showOpenProjectPage(self) -> None:
"""Show the open exiting project page."""
self.mainStack.setCurrentWidget(self.tabOpen)
self._setButtonVisibility()
return
@pyqtSlot()
def _browseForProject(self) -> None:
"""Browse for a project to open."""
if path := SHARED.getProjectPath(self, path=CONFIG.homePath(), allowZip=False):
self._openProjectPath(path)
return
@pyqtSlot()
def _openSelectedItem(self) -> None:
"""Open the currently selected project item."""
if self.mainStack.currentWidget() == self.tabOpen:
self.tabOpen.openSelectedItem()
return
@pyqtSlot(Path)
def _openProjectPath(self, path: Path) -> None:
@@ -231,7 +227,6 @@ class GuiWelcome(NDialog):
self.hide()
self.openProjectRequest.emit(path)
self.close()
return
##
# Internal Functions
@@ -241,7 +236,6 @@ class GuiWelcome(NDialog):
"""Save the user GUI settings."""
logger.debug("Saving State: GuiWelcome")
CONFIG.setWelcomeWinSize(self.width(), self.height())
return
def _setButtonVisibility(self) -> None:
"""Change the visibility of the dialog buttons."""
@@ -255,7 +249,6 @@ class GuiWelcome(NDialog):
self.btnOpen.setFocus()
else:
self.btnCreate.setFocus()
return
class _OpenProjectPage(QWidget):
@@ -308,8 +301,6 @@ class _OpenProjectPage(QWidget):
f"QLineEdit {{border: none; background: {baseCol}; padding: 4px;}} "
)
return
##
# Public Slots
##
@@ -319,7 +310,6 @@ class _OpenProjectPage(QWidget):
"""Open the currently selected project item."""
if (selection := self.listWidget.selectedIndexes()) and (index := selection[0]).isValid():
self.openProjectRequest.emit(Path(str(index.data()[1])))
return
##
# Private Slots
@@ -335,14 +325,12 @@ class _OpenProjectPage(QWidget):
self.selectedPath.setToolTip(text)
self.selectedPath.setCursorPosition(0)
self.aMissing.setVisible(not (Path(value) / nwFiles.PROJ_FILE).is_file())
return
@pyqtSlot(QModelIndex)
def _projectDoubleClicked(self, index: QModelIndex) -> None:
"""Process double click on project item."""
if index.isValid():
self.openProjectRequest.emit(Path(str(index.data()[1])))
return
@pyqtSlot()
def _deleteSelectedItem(self) -> None:
@@ -355,7 +343,6 @@ class _OpenProjectPage(QWidget):
if SHARED.question(text):
self.listModel.removeEntry(index)
self._selectFirstItem()
return
@pyqtSlot("QPoint")
def _openContextMenu(self, pos: QPoint) -> None:
@@ -368,7 +355,6 @@ class _OpenProjectPage(QWidget):
action.triggered.connect(self._deleteSelectedItem)
ctxMenu.exec(self.mapToGlobal(pos))
ctxMenu.setParent(None)
return
##
# Internal Functions
@@ -379,7 +365,6 @@ class _OpenProjectPage(QWidget):
index = self.listModel.index(0)
self.listWidget.setCurrentIndex(index)
self._projectClicked(index)
return
class _ProjectListItem(QStyledItemDelegate):
@@ -407,8 +392,6 @@ class _ProjectListItem(QStyledItemDelegate):
self._icon = SHARED.theme.getPixmap("proj_nwx", (iPx, iPx))
return
def paint(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None:
"""Paint a project entry on the canvas."""
rect = opt.rect
@@ -430,8 +413,6 @@ class _ProjectListItem(QStyledItemDelegate):
painter.drawText(rect.adjusted(x, y, 0, 0), tFlag, details)
painter.restore()
return
def sizeHint(self, opt: QStyleOptionViewItem, index: QModelIndex) -> QSize:
"""Set the size hint to fixed height."""
return QSize(opt.rect.width(), self._hPx)
@@ -449,7 +430,6 @@ class _ProjectListModel(QAbstractListModel):
when = CONFIG.localDate(datetime.fromtimestamp(time))
data.append((title, path, f"{opened}: {when}, {words}: {formatInt(count)}"))
self._data = data
return
def rowCount(self, parent: QModelIndex | None = None) -> int:
"""Return the size of the model."""
@@ -516,8 +496,6 @@ class _NewProjectPage(QWidget):
f"_NewProjectForm {{border: none; background: {baseCol};}} "
)
return
##
# Public Slots
##
@@ -689,13 +667,10 @@ class _NewProjectForm(QWidget):
self._updateProjPath()
self._updateFillInfo()
return
def enterForm(self) -> None:
"""Focus the project name field when entering the form."""
self.projName.setFocus()
self.projName.selectAll()
return
def getProjectData(self) -> dict:
"""Collect form data and return it as a dictionary."""
@@ -733,14 +708,12 @@ class _NewProjectForm(QWidget):
self._basePath = Path(path)
self._updateProjPath()
CONFIG.setLastPath("project", path)
return
@pyqtSlot()
def _updateProjPath(self) -> None:
"""Update the path box to show the full project path."""
projName = makeFileNameSafe(self.projName.text().strip())
self.projPath.setText(str(self._basePath / projName))
return
@pyqtSlot()
def _syncSwitches(self) -> None:
@@ -750,21 +723,18 @@ class _NewProjectForm(QWidget):
addWorld = self.addWorld.isChecked()
if not (addPlot or addChar or addWorld):
self.addNotes.setChecked(False)
return
@pyqtSlot()
def _setFillBlank(self) -> None:
"""Set fill mode to blank project."""
self._fillMode = self.FILL_BLANK
self._updateFillInfo()
return
@pyqtSlot()
def _setFillSample(self) -> None:
"""Set fill mode to sample project."""
self._fillMode = self.FILL_SAMPLE
self._updateFillInfo()
return
@pyqtSlot()
def _setFillCopy(self) -> None:
@@ -773,7 +743,6 @@ class _NewProjectForm(QWidget):
self._fillMode = self.FILL_COPY
self._copyPath = copyPath
self._updateFillInfo()
return
##
# Internal Functions
@@ -793,5 +762,3 @@ class _NewProjectForm(QWidget):
self.projFill.setToolTip(text)
self.projFill.setCursorPosition(0)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -54,7 +54,7 @@ logger = logging.getLogger(__name__)
class GuiWritingStats(NToolDialog):
"""GUI Tools: Writing Statistics
"""GUI Tools: Writing Statistics.
Displays data from the NWSessionLog object.
"""
@@ -311,11 +311,8 @@ class GuiWritingStats(NToolDialog):
logger.debug("Ready: GuiWritingStats")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiWritingStats")
return
def populateGUI(self) -> None:
"""Populate list box with data from the log file."""
@@ -323,7 +320,6 @@ class GuiWritingStats(NToolDialog):
self._loadLogFile()
self._updateListBox()
QApplication.restoreOverrideCursor()
return
##
# Events
@@ -333,7 +329,6 @@ class GuiWritingStats(NToolDialog):
"""Capture the user closing the window."""
event.accept()
self.softDelete()
return
##
# Private Slots
@@ -377,8 +372,6 @@ class GuiWritingStats(NToolDialog):
self.close()
return
def _saveData(self, dataFmt: int) -> bool:
"""Save the content of the list box to a file."""
fileExt = ""
@@ -498,8 +491,6 @@ class GuiWritingStats(NToolDialog):
self.notesWords.setText(f"{ttNotes:n}")
self.totalWords.setText(f"{ttWords:n}")
return
##
# Private Slots
##
@@ -622,5 +613,3 @@ class GuiWritingStats(NToolDialog):
self.timeFilter += sDiff
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import argparse
@@ -50,7 +50,6 @@ OS_WIN = sys.platform.startswith("win32")
def printVersion(args: argparse.Namespace) -> None:
"""Print the novelWriter version and exit."""
print(extractVersion(beQuiet=True)[0], end=None)
return
def installPackages(args: argparse.Namespace) -> None:
@@ -79,8 +78,6 @@ def installPackages(args: argparse.Namespace) -> None:
print(str(exc))
sys.exit(1)
return
def cleanBuildDirs(args: argparse.Namespace) -> None:
"""Recursively delete the 'build' and 'dist' folders."""
@@ -111,8 +108,6 @@ def cleanBuildDirs(args: argparse.Namespace) -> None:
print("")
return
def genMacOSPlist(args: argparse.Namespace) -> None:
"""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")
writeFile(outDir / "Info.plist", plistXML)
return
if __name__ == "__main__":
"""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
select = [
"A", # flake8-builtins (A)
"ANN", # flake8-annotations (ANN)
"B", # flake8-bugbear (B)
"E", # pycodestyle (E)
"F", # Pyflakes (F)
"FA", # flake8-future-annotations (FA)
"PERF", # Perflint (PERF)
"PLC", # Pylint Convention (PLC)
"PLE", # Pylint Error (PLE)
"PLW", # Pylint Warning (PLW)
"Q", # flake8-quotes (Q)
"RUF", # Ruff-specific rules (RUF)
"SLF", # flake8-self (SLF)
"SLOT", # flake8-slots (SLOT)
"TC", # flake8-type-checking (TC)
"UP", # pyupgrade (UP)
"W", # pycodestyle (W)
"A", # flake8-builtins (A)
"ANN", # flake8-annotations (ANN)
"B", # flake8-bugbear (B)
"D", # pydocstyle (D)
"E", # pycodestyle (E)
"F", # Pyflakes (F)
"FA", # flake8-future-annotations (FA)
"PERF", # Perflint (PERF)
"PLC", # Pylint Convention (PLC)
"PLE", # Pylint Error (PLE)
"PLR17", # Refactor (PLR) - Only PLR17xx
"PLW", # Pylint Warning (PLW)
"Q", # flake8-quotes (Q)
"RET", # flake8-return (RET)
"RUF", # Ruff-specific rules (RUF)
"SLF", # flake8-self (SLF)
"SLOT", # flake8-slots (SLOT)
"TC", # flake8-type-checking (TC)
"UP", # pyupgrade (UP)
"W", # pycodestyle (W)
]
ignore = [
"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
"E226", # missing-whitespace-around-arithmetic-operator
"E228", # missing-whitespace-around-modulo-operator
@@ -95,6 +103,7 @@ ignore = [
"PLC1901", # compare-to-empty-string
"PLW0108", # unnecessary-lambda
"PLW2901", # redefined-loop-name
"RET505", # superfluous-else-return
"RUF001", # ambiguous-unicode-character-string
"RUF002", # ambiguous-unicode-character-docstring
"RUF015", # unnecessary-iterable-allocation-for-first-element
@@ -103,9 +112,21 @@ ignore = [
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["ANN", "SLF", "TC", "PLC2701"]
"tests/*" = ["ANN", "SLF", "TC", "PLC2701", "D101", "D102"]
"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]
quote-style = "double"
@@ -114,7 +135,6 @@ include = ["novelwriter"]
exclude = ["**/__pycache__"]
reportIncompatibleMethodOverride = false
pythonVersion = "3.10"
[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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -70,8 +70,6 @@ def resetConfigVars():
CONFIG.emphLabels = True
CONFIG.lineHighlight = True
return
##
# Auto Fixtures
@@ -88,7 +86,6 @@ def sessionFixture():
(_TMP_ROOT / "manual.pdf").touch()
(_SRC_ROOT / "novelwriter" / "assets"/ "manual.pdf").touch()
(_SRC_ROOT / "novelwriter" / "assets"/ "manual_fr.pdf").touch()
return
@pytest.fixture(scope="function", autouse=True)
@@ -107,8 +104,6 @@ def functionFixture(qtbot):
resetConfigVars()
logging.getLogger("novelwriter").setLevel(logging.INFO)
return
##
# Core Test Folders
@@ -251,13 +246,11 @@ def prjLipsum():
if dstDir.exists():
shutil.rmtree(dstDir)
return
@pytest.fixture(scope="session")
def ipsumText():
"""Return five paragraphs of Lorem Ipsum text."""
thatIpsum = [(
return [(
"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"
"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"
"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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
from unittest.mock import MagicMock
@@ -35,7 +35,6 @@ class MockGuiMain(QWidget):
self.docViewer = MagicMock()
self.mainProgress = MagicMock()
self.projPath = ""
return
def postLaunchTasks(self, cmdOpen):
return
@@ -45,7 +44,6 @@ class MockGuiMain(QWidget):
def openProject(self, projPath):
self.projPath = projPath
return
def rebuildIndex(self):
return
@@ -64,7 +62,6 @@ class MockTheme:
self.guiFont = QFont()
self.guiFontB = QFont()
self.guiFontBU = QFont()
return
def initThemes(self) -> None:
return
@@ -96,8 +93,10 @@ class MockApp:
# Mock functions that will raise errors instead.
def causeOSError(*args, **kwargs):
"""Raise an OSError."""
raise OSError("Mock OSError")
def causeException(*args, **kwargs):
"""Raise an 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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import time
@@ -794,7 +794,6 @@ def testBaseCommon_openExternalPath(monkeypatch, tstPaths):
def mockOpenUrl(url: QUrl) -> None:
nonlocal lastUrl
lastUrl = url.toString()
return
monkeypatch.setattr(QDesktopServices, "openUrl", mockOpenUrl)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import logging
@@ -40,7 +40,7 @@ from tests.tools import clearLogHandlers
@pytest.mark.base
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("novelwriter.splash.sleep", lambda *a: None)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import json
@@ -39,7 +39,7 @@ from tests.tools import C, buildTestProject
def isUUID(value):
"""Checks if a value is a valid UUID object."""
"""Check if a value is a valid UUID object."""
try:
uuid.UUID(value)
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import shutil
@@ -669,7 +669,6 @@ def testCoreTools_ProjectBuilderCopyPlain(monkeypatch, caplog, mockGUI, prjLipsu
@pytest.mark.core
def testCoreTools_ProjectBuilderCopyZipped(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
"""Create a new project copied from existing zipped project."""
# Create a project
origPath = fncPath / "original"
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import copy
@@ -491,8 +491,7 @@ def testCoreItem_LayoutSetter(mockGUI):
@pytest.mark.core
def testCoreItem_ClassDefaults(mockGUI):
"""Test the setter for the default values.
"""
"""Test the setter for the default values."""
project = NWProject()
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
""" # noqa
from __future__ import annotations
import pytest

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