""" novelWriter – Project Options Cache =================================== File History: Created: 2019-10-21 [0.3.1] OptionState Rewritten: 2020-02-19 [0.4.5] OptionState This file is a part of novelWriter Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ # noqa from __future__ import annotations import json import logging from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Any, TypeVar from novelwriter.common import checkBool, checkFloat, checkInt, checkString, jsonEncode from novelwriter.constants import nwFiles from novelwriter.error import logException if TYPE_CHECKING: from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) NWEnum = TypeVar("NWEnum", bound=Enum) VALID_MAP: dict[str, set[str]] = { "GuiWritingStats": { "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", "widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes", "hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax", }, "GuiDocSplit": {"spLevel", "intoFolder", "docHierarchy"}, "GuiOutline": {"columnState"}, "GuiProjectSettings": { "winWidth", "winHeight", "replaceColW", "statusColW", "importColW", }, "GuiWordList": {"winWidth", "winHeight"}, "GuiNovelView": {"lastCol", "lastColSize"}, "GuiBuildSettings": { "winWidth", "winHeight", "treeWidth", "filterWidth", "autoPreview", }, "GuiManuscript": { "winWidth", "winHeight", "optsWidth", "viewWidth", "listHeight", "detailsHeight", "detailsWidth", "detailsExpanded", "showNewPage", }, "GuiManuscriptBuild": { "winWidth", "winHeight", "fmtWidth", "sumWidth", }, "GuiDocViewerPanel": { "colWidths", "hideInactive", }, "GuiNovelDetails": { "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble", "novelRoot", }, "GuiOutlineDetails": { "detailsWidth", "tagsWidth", } } class OptionState: """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 the Config instead. """ def __init__(self, project: NWProject) -> None: self._project = project self._state = {} ## # Load and Save Cache ## def loadSettings(self) -> bool: """Load the options dictionary from the project.""" stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE) if not isinstance(stateFile, Path): return False data = {} if stateFile.exists(): logger.debug("Loading GUI options file") try: with open(stateFile, mode="r", encoding="utf-8") as inFile: data = json.load(inFile) except Exception: logger.error("Failed to load GUI options file") logException() return False # Filter out unused variables state = data.get("novelWriter.guiOptions", {}) for aGroup in state: if aGroup in VALID_MAP: self._state[aGroup] = {} for anOpt in state[aGroup]: if anOpt in VALID_MAP[aGroup]: self._state[aGroup][anOpt] = state[aGroup][anOpt] return True def saveSettings(self) -> bool: """Save the options dictionary to the project.""" stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE) if not isinstance(stateFile, Path): return False logger.debug("Saving GUI options file") try: with open(stateFile, mode="w+", encoding="utf-8") as fObj: data = {"novelWriter.guiOptions": self._state} fObj.write(jsonEncode(data, nmax=4)) except Exception: logger.error("Failed to save GUI options file") logException() return False return True ## # Setters ## def setValue(self, group: str, name: str, value: Any) -> bool: """Save a value, with a given group and name.""" if group not in VALID_MAP: logger.error("Unknown option group '%s'", group) return False if name not in VALID_MAP[group]: logger.error("Unknown option name '%s'", name) return False if group not in self._state: self._state[group] = {} if isinstance(value, Enum): self._state[group][name] = value.name else: self._state[group][name] = value return True ## # Getters ## def getValue(self, group: str, name: str, default: Any) -> Any: """Return an arbitrary type value, if it exists. Otherwise, return the default value. """ if group in self._state: return self._state[group].get(name, default) return default def getString(self, group: str, name: str, default: str) -> str: """Return the value as a string, if it exists. Otherwise, return the default value. """ if group in self._state: return checkString(self._state[group].get(name, default), default) return default def getInt(self, group: str, name: str, default: int) -> int: """Return the value as an int, if it exists. Otherwise, return the default value. """ if group in self._state: return checkInt(self._state[group].get(name, default), default) return default def getFloat(self, group: str, name: str, default: float) -> float: """Return the value as a float, if it exists. Otherwise, return the default value. """ if group in self._state: return checkFloat(self._state[group].get(name, default), default) return default def getBool(self, group: str, name: str, default: bool) -> bool: """Return the value as a bool, if it exists. Otherwise, return the default value. """ if group in self._state: return checkBool(self._state[group].get(name, default), default) return default def getEnum(self, group: str, name: str, lookup: type, default: NWEnum) -> NWEnum: """Return the value mapped to an enum. Otherwise return the default value. """ if issubclass(lookup, type(default)): if group in self._state: if name in self._state[group]: value = self._state[group][name] if value in lookup.__members__: return lookup[value] return default