""" novelWriter – Config Class ========================== Class holding the user preferences and handling the config file File History: Created: 2018-09-22 [0.0.1] This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen 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 . """ import sys import json import logging from time import time from pathlib import Path from PyQt5.Qt import PYQT_VERSION_STR from PyQt5.QtCore import ( QT_VERSION_STR, QStandardPaths, QSysInfo, QLocale, QLibraryInfo, QTranslator ) from novelwriter.error import logException, formatException from novelwriter.common import checkPath, splitVersionNumber, formatTimeStamp, NWConfigParser from novelwriter.constants import nwFiles, nwUnicode logger = logging.getLogger(__name__) class Config: LANG_NW = 1 LANG_PROJ = 2 def __init__(self): # Initialisation # ============== # Set Application Variables self.appName = "novelWriter" self.appHandle = "novelwriter" # Set Paths confRoot = Path(QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)) dataRoot = Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)) self._confPath = confRoot.absolute() / self.appHandle # The user config location self._dataPath = dataRoot.absolute() / self.appHandle # The user data location self._homePath = Path.home().absolute() # The user's home directory self._appPath = Path(__file__).parent.absolute() self._appRoot = self._appPath.parent if self._appRoot.is_file(): # novelWriter is packaged as a single file self._appRoot = self._appRoot.parent self._appPath = self._appRoot # Runtime Settings and Variables self._hasError = False # True if the config class encountered an error self._errData = [] # List of error messages # Localisation # Note that these paths must be strings self._qLocale = QLocale.system() self._qtTrans = {} self._qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath) self._nwLangPath = str(self._appPath / "assets" / "i18n") # PDF Manual pdfDocs = self._appPath / "assets" / "manual.pdf" self.pdfDocs = pdfDocs if pdfDocs.is_file() else None # User Settings # ============= self._recentProj = RecentProjects(self) # General GUI Settings self.guiLocale = self._qLocale.name() self.guiTheme = "default" # GUI theme self.guiSyntax = "default_light" # Syntax theme self.guiFont = "" # Defaults to system default font in theme class self.guiFontSize = 11 # Is overridden if system default is loaded self.guiScale = 1.0 # Set automatically by Theme class self.hideVScroll = False # Hide vertical scroll bars on main widgets self.hideHScroll = False # Hide horizontal scroll bars on main widgets self.lastNotes = "0x0" # The latest release notes that have been shown self._lastPath = self._homePath # The user's last used path # Size Settings self._mainWinSize = [1200, 650] # Last size of the main GUI window self._prefsWinSize = [700, 615] # Last size of the Preferences dialog self._projLoadCols = [280, 60, 160] # Last columns withs of the Project Load dialog self._mainPanePos = [300, 800] # Last position of the main window splitter self._viewPanePos = [500, 150] # Last position of the document viewer splitter self._outlnPanePos = [500, 150] # Last position of the outline panel splitter # Project Settings self.autoSaveProj = 60 # Interval for auto-saving project, in seconds self.autoSaveDoc = 30 # Interval for auto-saving document, in seconds self.emphLabels = True # Add emphasis to H1 and H2 item labels self._backupPath = None # Backup path to use, can be none self.backupOnClose = False # Flag for running automatic backups self.askBeforeBackup = True # Flag for asking before running automatic backup # Text Editor Settings self.textFont = None # Editor font self.textSize = 12 # Editor font size self.textWidth = 700 # Editor text width self.textMargin = 40 # Editor/viewer text margin self.tabWidth = 40 # Editor tabulator width self.focusWidth = 800 # Focus Mode text width self.hideFocusFooter = False # Hide document footer in Focus Mode self.showFullPath = True # Show full document path in editor header self.autoSelect = True # Auto-select word when applying format with no selection self.doJustify = False # Justify text self.showTabsNSpaces = False # Show tabs and spaces in edior self.showLineEndings = False # Show line endings in editor self.showMultiSpaces = True # Highlight multiple spaces in the text self.doReplace = True # Enable auto-replace as you type self.doReplaceSQuote = True # Smart single quotes self.doReplaceDQuote = True # Smart double quotes self.doReplaceDash = True # Replace multiple hyphens with dashes self.doReplaceDots = True # Replace three dots with ellipsis self.scrollPastEnd = 25 # Number of lines to scroll past end of document self.autoScroll = False # Typewriter-like scrolling self.autoScrollPos = 30 # Start point for typewriter-like scrolling self.wordCountTimer = 5.0 # Interval for word count update in seconds self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes self.incNotesWCount = True # The status bar word count includes notes self.highlightQuotes = True # Highlight text in quotes self.allowOpenSQuote = False # Allow open-ended single quotes self.allowOpenDQuote = True # Allow open-ended double quotes self.highlightEmph = True # Add colour to text emphasis self.stopWhenIdle = True # Stop the status bar clock when the user is idle self.userIdleTime = 300 # Time of inactivity to consider user idle # User-Selected Symbol Settings self.fmtApostrophe = nwUnicode.U_RSQUO self.fmtSQuoteOpen = nwUnicode.U_LSQUO self.fmtSQuoteClose = nwUnicode.U_RSQUO self.fmtDQuoteOpen = nwUnicode.U_LDQUO self.fmtDQuoteClose = nwUnicode.U_RDQUO self.fmtPadBefore = "" self.fmtPadAfter = "" self.fmtPadThin = False # Spell Checking Settings self.spellLanguage = "en" # State self.isFullScreen = False # Last fullscreen state self.showRefPanel = True # The reference panel for the viewer is visible self.viewComments = True # Comments are shown in the viewer self.viewSynopsis = True # Synopsis is shown in the viewer # Search Bar Switches self.searchCase = False self.searchWord = False self.searchRegEx = False self.searchLoop = False self.searchNextFile = False self.searchMatchCap = False # System and App Information # ========================== # Check Qt5 Versions verQt = splitVersionNumber(QT_VERSION_STR) self.verQtString = QT_VERSION_STR self.verQtMajor = verQt[0] self.verQtMinor = verQt[1] self.verQtPatch = verQt[2] self.verQtValue = verQt[3] verQt = splitVersionNumber(PYQT_VERSION_STR) self.verPyQtString = PYQT_VERSION_STR self.verPyQtMajor = verQt[0] self.verPyQtMinor = verQt[1] self.verPyQtPatch = verQt[2] self.verPyQtValue = verQt[3] # Check Python Version self.verPyString = sys.version.split()[0] self.verPyMajor = sys.version_info[0] self.verPyMinor = sys.version_info[1] self.verPyPatch = sys.version_info[2] self.verPyHexVal = sys.hexversion # Check OS Type self.osType = sys.platform self.osLinux = False self.osWindows = False self.osDarwin = False self.osUnknown = False if self.osType.startswith("linux"): self.osLinux = True elif self.osType.startswith("darwin"): self.osDarwin = True elif self.osType.startswith("win32"): self.osWindows = True elif self.osType.startswith("cygwin"): self.osWindows = True else: self.osUnknown = True # Other System Info self.hostName = QSysInfo.machineHostName() self.kernelVer = QSysInfo.kernelVersion() # Packages self.hasEnchant = False # The pyenchant package return ## # Properties ## @property def hasError(self): return self._hasError @property def recentProjects(self): return self._recentProj @property def mainWinSize(self): return [int(x*self.guiScale) for x in self._mainWinSize] @property def preferencesWinSize(self): return [int(x*self.guiScale) for x in self._prefsWinSize] @property def projLoadColWidths(self): return [int(x*self.guiScale) for x in self._projLoadCols] @property def mainPanePos(self): return [int(x*self.guiScale) for x in self._mainPanePos] @property def viewPanePos(self): return [int(x*self.guiScale) for x in self._viewPanePos] @property def outlinePanePos(self): return [int(x*self.guiScale) for x in self._outlnPanePos] ## # Getters ## def getTextWidth(self, focusMode=False): """Get the text with for the correct editor mode.""" if focusMode: return self.pxInt(max(self.focusWidth, 200)) else: return self.pxInt(max(self.textWidth, 200)) def getTextMargin(self): """Get the scaled text margin.""" return self.pxInt(max(self.textMargin, 0)) def getTabWidth(self): """Get the scaled tab width.""" return self.pxInt(max(self.tabWidth, 0)) ## # Setters ## def setMainWinSize(self, newWidth, newHeight): """Set the size of the main window, but only if the change is larger than 5 pixels. The OS window manager will sometimes adjust it a bit, and we don't want the main window to shrink or grow each time the app is opened. """ newWidth = int(newWidth/self.guiScale) newHeight = int(newHeight/self.guiScale) if abs(self._mainWinSize[0] - newWidth) > 5: self._mainWinSize[0] = newWidth if abs(self._mainWinSize[1] - newHeight) > 5: self._mainWinSize[1] = newHeight return def setPreferencesWinSize(self, newWidth, newHeight): """Set the size of the Preferences dialog window.""" self._prefsWinSize[0] = int(newWidth/self.guiScale) self._prefsWinSize[1] = int(newHeight/self.guiScale) return def setProjLoadColWidths(self, colWidths): """Set the column widths of the Load Project dialog.""" self._projLoadCols = [int(x/self.guiScale) for x in colWidths] return def setMainPanePos(self, panePos): """Set the position of the main GUI splitter.""" self._mainPanePos = [int(x/self.guiScale) for x in panePos] return def setViewPanePos(self, panePos): """Set the position of the viewer meta data splitter.""" self._viewPanePos = [int(x/self.guiScale) for x in panePos] return def setOutlinePanePos(self, panePos): """Set the position of the outline details splitter.""" self._outlnPanePos = [int(x/self.guiScale) for x in panePos] return def setLastPath(self, lastPath): """Set the last used path. Only the folder is saved, so if the path is not a folder, the parent of the path is used instead. """ if isinstance(lastPath, (str, Path)): lastPath = checkPath(lastPath, self._homePath) if not lastPath.is_dir(): lastPath = lastPath.parent if lastPath.is_dir(): self._lastPath = lastPath logger.debug("Last path updated: %s" % self._lastPath) return def setBackupPath(self, backupPath): """Set the current backup path.""" self._backupPath = checkPath(backupPath, None) return ## # Methods ## def pxInt(self, theSize): """Used to scale fixed gui sizes by the screen scale factor. This function returns an int, which is always rounded down. """ return int(theSize*self.guiScale) def rpxInt(self, theSize): """Used to un-scale fixed gui sizes by the screen scale factor. This function returns an int, which is always rounded down. """ return int(theSize/self.guiScale) def dataPath(self, target=None): """Return a path in the data folder.""" if isinstance(target, str): return self._dataPath / target return self._dataPath def assetPath(self, target=None): """Return a path in the assets folder.""" if isinstance(target, str): return self._appPath / "assets" / target return self._appPath / "assets" def lastPath(self): """Return the last path used by the user, but ensure it exists. """ if isinstance(self._lastPath, Path): if self._lastPath.is_dir(): return self._lastPath return self._homePath def backupPath(self): """Return the backup path.""" if isinstance(self._backupPath, Path): if self._backupPath.is_dir(): return self._backupPath return None def errorText(self): """Compile and return error messages from the initialisation of the Config class, and clear the error buffer. """ errMessage = "
".join(self._errData) self._hasError = False self._errData = [] return errMessage def listLanguages(self, lngSet): """List localisation files in the i18n folder. The default GUI language is British English (en_GB). """ if lngSet == self.LANG_NW: fPre = "nw_" fExt = ".qm" langList = {"en_GB": QLocale("en_GB").nativeLanguageName().title()} elif lngSet == self.LANG_PROJ: fPre = "project_" fExt = ".json" langList = {"en_GB": QLocale("en_GB").nativeLanguageName().title()} else: return [] for qmFile in Path(self._nwLangPath).iterdir(): qmName = qmFile.name if not (qmFile.is_file() and qmName.startswith(fPre) and qmName.endswith(fExt)): continue qmLang = qmName[len(fPre):-len(fExt)] qmName = QLocale(qmLang).nativeLanguageName().title() if qmLang and qmName and qmLang != "en_GB": langList[qmLang] = qmName return sorted(langList.items(), key=lambda x: x[0]) ## # Config Actions ## def initConfig(self, confPath=None, dataPath=None): """Initialise the config class. The manual setting of confPath and dataPath is mainly intended for the test suite. """ logger.debug("Initialising Config ...") if isinstance(confPath, (str, Path)): logger.info("Setting config from alternative path: %s", confPath) self._confPath = Path(confPath) if isinstance(dataPath, (str, Path)): logger.info("Setting data path from alternative path: %s", dataPath) self._dataPath = Path(dataPath) logger.debug("Config Path: %s", self._confPath) logger.debug("Data Path: %s", self._dataPath) logger.debug("App Root: %s", self._appRoot) logger.debug("App Path: %s", self._appPath) logger.debug("Last Path: %s", self._lastPath) logger.debug("PDF Manual: %s", self.pdfDocs) # If the config and data folders don't exist, create them # This assumes that the os config and data folders exist self._confPath.mkdir(exist_ok=True) self._dataPath.mkdir(exist_ok=True) # Also create the syntax and themes folders if possible if self._dataPath.is_dir(): (self._dataPath / "syntax").mkdir(exist_ok=True) (self._dataPath / "themes").mkdir(exist_ok=True) # Check if config file exists, and load it. If not, we save defaults if (self._confPath / nwFiles.CONF_FILE).is_file(): self.loadConfig() else: self.saveConfig() self._recentProj.loadCache() self._checkOptionalPackages() logger.debug("Config initialisation complete") return def initLocalisation(self, nwApp): """Initialise the localisation of the GUI. """ self._qLocale = QLocale(self.guiLocale) QLocale.setDefault(self._qLocale) self._qtTrans = {} langList = [ (self._qtLangPath, "qtbase"), # Qt 5.x (self._nwLangPath, "nw"), # novelWriter ] for lngPath, lngBase in langList: for lngCode in self._qLocale.uiLanguages(): qTrans = QTranslator() lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_")) if lngFile not in self._qtTrans: if qTrans.load(lngFile, lngPath): logger.debug("Loaded: %s.qm", lngFile) nwApp.installTranslator(qTrans) self._qtTrans[lngFile] = qTrans return def loadConfig(self): """Load preferences from file and replace default settings. """ logger.debug("Loading config file") theConf = NWConfigParser() cnfPath = self._confPath / nwFiles.CONF_FILE try: with open(cnfPath, mode="r", encoding="utf-8") as inFile: theConf.read_file(inFile) except Exception as exc: logger.error("Could not load config file") logException() self._hasError = True self._errData.append("Could not load config file") self._errData.append(formatException(exc)) return False # Main cnfSec = "Main" self.guiTheme = theConf.rdStr(cnfSec, "theme", self.guiTheme) self.guiSyntax = theConf.rdStr(cnfSec, "syntax", self.guiSyntax) self.guiFont = theConf.rdStr(cnfSec, "font", self.guiFont) self.guiFontSize = theConf.rdInt(cnfSec, "fontsize", self.guiFontSize) self.guiLocale = theConf.rdStr(cnfSec, "localisation", self.guiLocale) self.hideVScroll = theConf.rdBool(cnfSec, "hidevscroll", self.hideVScroll) self.hideHScroll = theConf.rdBool(cnfSec, "hidehscroll", self.hideHScroll) self.lastNotes = theConf.rdStr(cnfSec, "lastnotes", self.lastNotes) self._lastPath = theConf.rdPath(cnfSec, "lastpath", self._lastPath) # Sizes cnfSec = "Sizes" self._mainWinSize = theConf.rdIntList(cnfSec, "mainwindow", self._mainWinSize) self._prefsWinSize = theConf.rdIntList(cnfSec, "preferences", self._prefsWinSize) self._projLoadCols = theConf.rdIntList(cnfSec, "projloadcols", self._projLoadCols) self._mainPanePos = theConf.rdIntList(cnfSec, "mainpane", self._mainPanePos) self._viewPanePos = theConf.rdIntList(cnfSec, "viewpane", self._viewPanePos) self._outlnPanePos = theConf.rdIntList(cnfSec, "outlinepane", self._outlnPanePos) # Project cnfSec = "Project" self.autoSaveProj = theConf.rdInt(cnfSec, "autosaveproject", self.autoSaveProj) self.autoSaveDoc = theConf.rdInt(cnfSec, "autosavedoc", self.autoSaveDoc) self.emphLabels = theConf.rdBool(cnfSec, "emphlabels", self.emphLabels) self._backupPath = theConf.rdPath(cnfSec, "backuppath", self._backupPath) self.backupOnClose = theConf.rdBool(cnfSec, "backuponclose", self.backupOnClose) self.askBeforeBackup = theConf.rdBool(cnfSec, "askbeforebackup", self.askBeforeBackup) # Editor cnfSec = "Editor" self.textFont = theConf.rdStr(cnfSec, "textfont", self.textFont) self.textSize = theConf.rdInt(cnfSec, "textsize", self.textSize) self.textWidth = theConf.rdInt(cnfSec, "width", self.textWidth) self.textMargin = theConf.rdInt(cnfSec, "margin", self.textMargin) self.tabWidth = theConf.rdInt(cnfSec, "tabwidth", self.tabWidth) self.focusWidth = theConf.rdInt(cnfSec, "focuswidth", self.focusWidth) self.hideFocusFooter = theConf.rdBool(cnfSec, "hidefocusfooter", self.hideFocusFooter) self.doJustify = theConf.rdBool(cnfSec, "justify", self.doJustify) self.autoSelect = theConf.rdBool(cnfSec, "autoselect", self.autoSelect) self.doReplace = theConf.rdBool(cnfSec, "autoreplace", self.doReplace) self.doReplaceSQuote = theConf.rdBool(cnfSec, "repsquotes", self.doReplaceSQuote) self.doReplaceDQuote = theConf.rdBool(cnfSec, "repdquotes", self.doReplaceDQuote) self.doReplaceDash = theConf.rdBool(cnfSec, "repdash", self.doReplaceDash) self.doReplaceDots = theConf.rdBool(cnfSec, "repdots", self.doReplaceDots) self.scrollPastEnd = theConf.rdInt(cnfSec, "scrollpastend", self.scrollPastEnd) self.autoScroll = theConf.rdBool(cnfSec, "autoscroll", self.autoScroll) self.autoScrollPos = theConf.rdInt(cnfSec, "autoscrollpos", self.autoScrollPos) self.fmtSQuoteOpen = theConf.rdStr(cnfSec, "fmtsquoteopen", self.fmtSQuoteOpen) self.fmtSQuoteClose = theConf.rdStr(cnfSec, "fmtsquoteclose", self.fmtSQuoteClose) self.fmtDQuoteOpen = theConf.rdStr(cnfSec, "fmtdquoteopen", self.fmtDQuoteOpen) self.fmtDQuoteClose = theConf.rdStr(cnfSec, "fmtdquoteclose", self.fmtDQuoteClose) self.fmtPadBefore = theConf.rdStr(cnfSec, "fmtpadbefore", self.fmtPadBefore) self.fmtPadAfter = theConf.rdStr(cnfSec, "fmtpadafter", self.fmtPadAfter) self.fmtPadThin = theConf.rdBool(cnfSec, "fmtpadthin", self.fmtPadThin) self.spellLanguage = theConf.rdStr(cnfSec, "spellcheck", self.spellLanguage) self.showTabsNSpaces = theConf.rdBool(cnfSec, "showtabsnspaces", self.showTabsNSpaces) self.showLineEndings = theConf.rdBool(cnfSec, "showlineendings", self.showLineEndings) self.showMultiSpaces = theConf.rdBool(cnfSec, "showmultispaces", self.showMultiSpaces) self.wordCountTimer = theConf.rdFlt(cnfSec, "wordcounttimer", self.wordCountTimer) self.bigDocLimit = theConf.rdInt(cnfSec, "bigdoclimit", self.bigDocLimit) self.incNotesWCount = theConf.rdBool(cnfSec, "incnoteswcount", self.incNotesWCount) self.showFullPath = theConf.rdBool(cnfSec, "showfullpath", self.showFullPath) self.highlightQuotes = theConf.rdBool(cnfSec, "highlightquotes", self.highlightQuotes) self.allowOpenSQuote = theConf.rdBool(cnfSec, "allowopensquote", self.allowOpenSQuote) self.allowOpenDQuote = theConf.rdBool(cnfSec, "allowopendquote", self.allowOpenDQuote) self.highlightEmph = theConf.rdBool(cnfSec, "highlightemph", self.highlightEmph) self.stopWhenIdle = theConf.rdBool(cnfSec, "stopwhenidle", self.stopWhenIdle) self.userIdleTime = theConf.rdInt(cnfSec, "useridletime", self.userIdleTime) # State cnfSec = "State" self.isFullScreen = theConf.rdBool(cnfSec, "fullscreen", self.isFullScreen) self.showRefPanel = theConf.rdBool(cnfSec, "showrefpanel", self.showRefPanel) self.viewComments = theConf.rdBool(cnfSec, "viewcomments", self.viewComments) self.viewSynopsis = theConf.rdBool(cnfSec, "viewsynopsis", self.viewSynopsis) self.searchCase = theConf.rdBool(cnfSec, "searchcase", self.searchCase) self.searchWord = theConf.rdBool(cnfSec, "searchword", self.searchWord) self.searchRegEx = theConf.rdBool(cnfSec, "searchregex", self.searchRegEx) self.searchLoop = theConf.rdBool(cnfSec, "searchloop", self.searchLoop) self.searchNextFile = theConf.rdBool(cnfSec, "searchnextfile", self.searchNextFile) self.searchMatchCap = theConf.rdBool(cnfSec, "searchmatchcap", self.searchMatchCap) # Deprecated Settings or Locations as of 2.0 # ToDo: These will be loaded for a few minor releases until the users have converted them self.guiFont = theConf.rdStr("Main", "guifont", self.guiFont) self.guiFontSize = theConf.rdInt("Main", "guifontsize", self.guiFontSize) self.guiLocale = theConf.rdStr("Main", "guilang", self.guiLocale) self._backupPath = theConf.rdPath("Backup", "backuppath", self._backupPath) self.backupOnClose = theConf.rdBool("Backup", "backuponclose", self.backupOnClose) self.askBeforeBackup = theConf.rdBool("Backup", "askbeforebackup", self.askBeforeBackup) fmtSingleQuotes = theConf.rdStrList(cnfSec, "fmtsinglequote", []) fmtDoubleQuotes = theConf.rdStrList(cnfSec, "fmtdoublequote", []) if isinstance(fmtSingleQuotes, list) and len(fmtSingleQuotes) == 2: self.fmtSQuoteOpen = fmtSingleQuotes[0] self.fmtSQuoteClose = fmtSingleQuotes[1] if isinstance(fmtDoubleQuotes, list) and len(fmtDoubleQuotes) == 2: self.fmtDQuoteOpen = fmtDoubleQuotes[0] self.fmtDQuoteClose = fmtDoubleQuotes[1] # Check Values # ============ # Check Certain Values for None self.spellLanguage = self._checkNone(self.spellLanguage) # If we're using straight quotes, disable auto-replace if self.fmtSQuoteOpen == self.fmtSQuoteClose == "'" and self.doReplaceSQuote: logger.info("Using straight single quotes, so disabling auto-replace") self.doReplaceSQuote = False if self.fmtDQuoteOpen == self.fmtDQuoteClose == '"' and self.doReplaceDQuote: logger.info("Using straight double quotes, so disabling auto-replace") self.doReplaceDQuote = False return True def saveConfig(self): """Save the current preferences to file. """ logger.debug("Saving config file") theConf = NWConfigParser() theConf["Meta"] = { "timestamp": formatTimeStamp(time()), } theConf["Main"] = { "theme": str(self.guiTheme), "syntax": str(self.guiSyntax), "font": str(self.guiFont), "fontsize": str(self.guiFontSize), "localisation": str(self.guiLocale), "hidevscroll": str(self.hideVScroll), "hidehscroll": str(self.hideHScroll), "lastnotes": str(self.lastNotes), "lastpath": str(self._lastPath), } theConf["Sizes"] = { "mainwindow": self._packList(self._mainWinSize), "preferences": self._packList(self._prefsWinSize), "projloadcols": self._packList(self._projLoadCols), "mainpane": self._packList(self._mainPanePos), "viewpane": self._packList(self._viewPanePos), "outlinepane": self._packList(self._outlnPanePos), } theConf["Project"] = { "autosaveproject": str(self.autoSaveProj), "autosavedoc": str(self.autoSaveDoc), "emphlabels": str(self.emphLabels), "backuppath": str(self._backupPath or ""), "backuponclose": str(self.backupOnClose), "askbeforebackup": str(self.askBeforeBackup), } theConf["Editor"] = { "textfont": str(self.textFont), "textsize": str(self.textSize), "width": str(self.textWidth), "margin": str(self.textMargin), "tabwidth": str(self.tabWidth), "focuswidth": str(self.focusWidth), "hidefocusfooter": str(self.hideFocusFooter), "justify": str(self.doJustify), "autoselect": str(self.autoSelect), "autoreplace": str(self.doReplace), "repsquotes": str(self.doReplaceSQuote), "repdquotes": str(self.doReplaceDQuote), "repdash": str(self.doReplaceDash), "repdots": str(self.doReplaceDots), "scrollpastend": str(self.scrollPastEnd), "autoscroll": str(self.autoScroll), "autoscrollpos": str(self.autoScrollPos), "fmtsquoteopen": str(self.fmtSQuoteOpen), "fmtsquoteclose": str(self.fmtSQuoteClose), "fmtdquoteopen": str(self.fmtDQuoteOpen), "fmtdquoteclose": str(self.fmtDQuoteClose), "fmtpadbefore": str(self.fmtPadBefore), "fmtpadafter": str(self.fmtPadAfter), "fmtpadthin": str(self.fmtPadThin), "spellcheck": str(self.spellLanguage), "showtabsnspaces": str(self.showTabsNSpaces), "showlineendings": str(self.showLineEndings), "showmultispaces": str(self.showMultiSpaces), "wordcounttimer": str(self.wordCountTimer), "bigdoclimit": str(self.bigDocLimit), "incnoteswcount": str(self.incNotesWCount), "showfullpath": str(self.showFullPath), "highlightquotes": str(self.highlightQuotes), "allowopensquote": str(self.allowOpenSQuote), "allowopendquote": str(self.allowOpenDQuote), "highlightemph": str(self.highlightEmph), "stopwhenidle": str(self.stopWhenIdle), "useridletime": str(self.userIdleTime), } theConf["State"] = { "fullscreen": str(self.isFullScreen), "showrefpanel": str(self.showRefPanel), "viewcomments": str(self.viewComments), "viewsynopsis": str(self.viewSynopsis), "searchcase": str(self.searchCase), "searchword": str(self.searchWord), "searchregex": str(self.searchRegEx), "searchloop": str(self.searchLoop), "searchnextfile": str(self.searchNextFile), "searchmatchcap": str(self.searchMatchCap), } # Write config file cnfPath = self._confPath / nwFiles.CONF_FILE try: with open(cnfPath, mode="w", encoding="utf-8") as outFile: theConf.write(outFile) except Exception as exc: logger.error("Could not save config file") logException() self._hasError = True self._errData.append("Could not save config file") self._errData.append(formatException(exc)) return False return True ## # Internal Functions ## def _packList(self, inData): """Pack a list of items into a comma-separated string for saving to the config file. """ return ", ".join([str(inVal) for inVal in inData]) def _checkNone(self, checkVal): """Return a NoneType if the value corresponds to None, otherwise return the value unchanged. """ if checkVal is None: return None if isinstance(checkVal, str): if checkVal.lower() == "none": return None return checkVal def _checkOptionalPackages(self): """Cheks if we have the optional packages used by some features. """ try: import enchant # noqa: F401 except ImportError: self.hasEnchant = False logger.debug("Checking package 'pyenchant': Missing") else: self.hasEnchant = True logger.debug("Checking package 'pyenchant': OK") return # END Class Config class RecentProjects: def __init__(self, mainConf): self.mainConf = mainConf self._data = {} return def loadCache(self): """Load the cache file for recent projects. """ self._data = {} cacheFile = self.mainConf.dataPath(nwFiles.RECENT_FILE) if not cacheFile.is_file(): return True try: with open(cacheFile, mode="r", encoding="utf-8") as inFile: theData = json.load(inFile) for projPath, theEntry in theData.items(): self._data[projPath] = { "title": theEntry.get("title", ""), "words": theEntry.get("words", 0), "time": theEntry.get("time", 0), } except Exception: logger.error("Could not load recent project cache") logException() return False return True def saveCache(self): """Save the cache dictionary of recent projects. """ cacheFile = self.mainConf.dataPath(nwFiles.RECENT_FILE) cacheTemp = cacheFile.with_suffix(".tmp") try: with open(cacheTemp, mode="w+", encoding="utf-8") as outFile: json.dump(self._data, outFile, indent=2) cacheTemp.replace(cacheFile) except Exception: logger.error("Could not save recent project cache") logException() return False return True def listEntries(self): """List all items in the cache. """ return [(k, e["title"], e["words"], e["time"]) for k, e in self._data.items()] def update(self, projPath, projTitle, wordCount, saveTime): """Add or update recent cache information on a given project. """ self._data[str(projPath)] = { "title": projTitle, "words": int(wordCount), "time": int(saveTime), } self.saveCache() return def remove(self, projPath): """Try to remove a path from the recent projects cache. """ if self._data.pop(str(projPath), None) is not None: logger.debug("Removed recent: %s", projPath) self.saveCache() return # END Class RecentProjects