def init_translations(app): """ Loads translations for a given input app. If a scaling factor is defined for testing, we use our own subclass of QTranslator. """ global application, translator, locale # if we don't keep a reference on these, it stops working. pyqt bug? application = app translator = QTranslator() if trans_scale == 100 else VortaTranslator() # Use LANG var if set, else let Qt detect it. env_lang = os.environ.get('LANG', False) if env_lang: locale = QLocale(env_lang) else: locale = QLocale() qm_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'qm')) ui_langs = locale.uiLanguages() succeeded = translator.load( locale, 'vorta', prefix='.', directory=qm_path) # e.g. vorta/i18n/qm/vorta.de_DE.qm if succeeded: app.installTranslator(translator) logger.debug('Loading translation %s for %r.' % ('succeeded' if succeeded else 'failed', ui_langs))
def init_translations(app): global application, translator, locale # if we don't keep a reference on these, it stops working. pyqt bug? application = app translator = VortaTranslator() locale = QLocale(os.environ.get('LANG', None)) qm_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'qm')) ui_langs = locale.uiLanguages() succeeded = translator.load( locale, 'vorta', prefix='.', directory=qm_path) # e.g. vorta/i18n/qm/vorta.de_DE.qm if succeeded: app.installTranslator(translator) logger.debug('Loading translation %s for %r.' % ('succeeded' if succeeded else 'failed', ui_langs))
def get_translator(language=None): system_locale = QLocale.system() # Remapping the language from uiLanguages is a workaround for an annoying bug in Qt, # which makes QTranslator use the system language (e.g. the language the OS was installed in), # instead of the user-display language the user installed later. locale = QLocale(language) if language is not None else QLocale( system_locale.uiLanguages()[0]) tribler_gui.logger.info("Available Tribler translations %s", AVAILABLE_TRANSLATIONS) tribler_gui.logger.info("System language: %s, Tribler language: %s", system_locale.uiLanguages(), locale.uiLanguages()) translator = QTranslator() filename = "" translator.load(locale, filename, directory=TRANSLATIONS_DIR) return translator
class Config: CNF_STR = 0 CNF_INT = 1 CNF_BOOL = 2 CNF_S_LST = 3 CNF_I_LST = 4 LANG_NW = 1 LANG_PROJ = 2 def __init__(self): # Set Application Variables self.appName = "novelWriter" self.appHandle = self.appName.lower() # Config Error Handling self.hasError = False # True if the config class encountered an error self.errData = [] # List of error messages # Set Paths self.cmdOpen = None # Path from command line for project to be opened on launch self.confPath = None # Folder where the config is saved self.confFile = None # The config file name self.dataPath = None # Folder where app data is stored self.lastPath = None # The last user-selected folder (browse dialogs) self.appPath = None # The full path to the novelwriter package folder self.appRoot = None # The full path to the novelwriter root folder self.appIcon = None # The full path to the novelwriter icon file self.assetPath = None # The full path to the nw/assets folder self.themeRoot = None # The full path to the nw/assets/themes folder self.dictPath = None # The full path to the nw/assets/dict folder self.iconPath = None # The full path to the nw/assets/icons folder self.helpPath = None # The full path to the novelwriter .qhc help file # Runtime Settings and Variables self.confChanged = False # True whenever the config has chenged, false after save self.hasHelp = False # True if the Qt help files are present in the assets folder ## General self.guiTheme = "default" self.guiSyntax = "default_light" self.guiIcons = "typicons_colour_light" self.guiDark = False # Load icons for dark backgrounds, if available self.guiFont = "" # Defaults to system default font self.guiFontSize = 11 # Is overridden if system default is loaded self.guiScale = 1.0 # Set automatically by Theme class self.lastNotes = "0x0" # The latest release notes that have been shown ## Localisation self.qLocal = QLocale.system() self.guiLang = self.qLocal.name() self.qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath) self.nwLangPath = None self.qtTrans = {} ## Sizes self.winGeometry = [1200, 650] self.prefGeometry = [700, 615] self.treeColWidth = [200, 50, 30] self.novelColWidth = [200, 50] self.projColWidth = [200, 60, 140] self.mainPanePos = [300, 800] self.docPanePos = [400, 400] self.viewPanePos = [500, 150] self.outlnPanePos = [500, 150] self.isFullScreen = False ## Features self.hideVScroll = False # Hide vertical scroll bars on main widgets self.hideHScroll = False # Hide horizontal scroll bars on main widgets ## Project self.autoSaveProj = 60 # Interval for auto-saving project in seconds self.autoSaveDoc = 30 # Interval for auto-saving document in seconds ## Text Editor self.textFont = None # Editor font self.textSize = 12 # Editor font size self.textFixedW = True # Keep editor text fixed width self.textWidth = 600 # 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.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 = True # Allow scrolling 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.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 Symbols self.fmtApostrophe = nwUnicode.U_RSQUO self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO] self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO] self.fmtPadBefore = "" self.fmtPadAfter = "" self.fmtPadThin = False ## Spell Checking self.spellTool = None self.spellLanguage = None ## Search Bar Switches self.searchCase = False self.searchWord = False self.searchRegEx = False self.searchLoop = False self.searchNextFile = False self.searchMatchCap = False ## Backup self.backupPath = "" self.backupOnClose = False self.askBeforeBackup = True ## State self.showRefPanel = True self.viewComments = True self.viewSynopsis = True # 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 = "Unknown" self.kernelVer = "Unknown" # Packages self.hasEnchant = False # The pyenchant package self.hasAssistant = False # The Qt Assistant executable # Recent Cache self.recentProj = {} 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) ## # 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 confPath is None: confRoot = QStandardPaths.writableLocation( QStandardPaths.ConfigLocation) self.confPath = os.path.join(os.path.abspath(confRoot), self.appHandle) else: logger.info("Setting config from alternative path: %s" % confPath) self.confPath = confPath if dataPath is None: if self.verQtValue >= 50400: dataRoot = QStandardPaths.writableLocation( QStandardPaths.AppDataLocation) else: dataRoot = QStandardPaths.writableLocation( QStandardPaths.DataLocation) self.dataPath = os.path.join(os.path.abspath(dataRoot), self.appHandle) else: logger.info("Setting data path from alternative path: %s" % dataPath) self.dataPath = dataPath logger.verbose("Config path: %s" % self.confPath) logger.verbose("Data path: %s" % self.dataPath) self.confFile = self.appHandle + ".conf" self.lastPath = os.path.expanduser("~") self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) self.appRoot = os.path.abspath( os.path.join(self.appPath, os.path.pardir)) if os.path.isfile(self.appRoot): # novelWriter is packaged as a single file, so the app and # root paths are the same, and equal to the folder that # contains the single executable. self.appRoot = os.path.dirname(self.appRoot) self.appPath = self.appRoot # Assets self.assetPath = os.path.join(self.appPath, "assets") self.themeRoot = os.path.join(self.assetPath, "themes") self.dictPath = os.path.join(self.assetPath, "dict") self.iconPath = os.path.join(self.assetPath, "icons") self.appIcon = os.path.join(self.iconPath, "novelwriter.svg") # Internationalisation self.nwLangPath = os.path.join(self.appRoot, "i18n") logger.verbose("App path: %s" % self.appPath) logger.verbose("Last path: %s" % self.lastPath) # If config folder does not exist, create it. # This assumes that the os config folder itself exists. if not os.path.isdir(self.confPath): try: os.mkdir(self.confPath) except Exception as e: logger.error("Could not create folder: %s" % self.confPath) logException() self.hasError = True self.errData.append("Could not create folder: %s" % self.confPath) self.errData.append(str(e)) self.confPath = None # Check if config file exists if self.confPath is not None: if os.path.isfile(os.path.join(self.confPath, self.confFile)): # If it exists, load it self.loadConfig() else: # If it does not exist, save a copy of the default values self.saveConfig() # If data folder does not exist, make it. # This assumes that the os data folder itself exists. if self.dataPath is not None: if not os.path.isdir(self.dataPath): try: os.mkdir(self.dataPath) except Exception as e: logger.error("Could not create folder: %s" % self.dataPath) logException() self.hasError = True self.errData.append("Could not create folder: %s" % self.dataPath) self.errData.append(str(e)) self.dataPath = None # Host and Kernel if self.verQtValue >= 50600: self.hostName = QSysInfo.machineHostName() self.kernelVer = QSysInfo.kernelVersion() # Load recent projects cache self.loadRecentCache() # Check the availability of optional packages self._checkOptionalPackages() if self.spellTool is None: self.spellTool = nwConst.SP_INTERNAL if self.spellLanguage is None: self.spellLanguage = "en" # Check if local help files exist self.helpPath = os.path.join(self.assetPath, "help", "novelWriter.qhc") self.hasHelp = os.path.isfile(self.helpPath) self.hasHelp &= os.path.isfile( os.path.join(self.assetPath, "help", "novelWriter.qch")) logger.debug("Config initialisation complete") return True def initLocalisation(self, nwApp): """Initialise the localisation of the GUI. """ self.qLocal = QLocale(self.guiLang) QLocale.setDefault(self.qLocal) self.qtTrans = {} langList = [ (self.qtLangPath, "qtbase"), # Qt 5.x (self.nwLangPath, "qtbase"), # Alternative Qt 5.x (self.nwLangPath, "nw"), # novelWriter ] for lngPath, lngBase in langList: for lngCode in self.qLocal.uiLanguages(): qTrans = QTranslator() lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_")) if lngFile not in self.qtTrans: if qTrans.load(lngFile, lngPath): logger.debug("Loaded: %s" % qTrans.filePath()) nwApp.installTranslator(qTrans) self.qtTrans[lngFile] = qTrans return def listLanguages(self, lngSet): """List localisation files in the i18n folder. The default GUI language 'en_GB' is British English. """ 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": "English"} else: return [] for qmFile in os.listdir(self.nwLangPath): if not os.path.isfile(os.path.join(self.nwLangPath, qmFile)): continue if not qmFile.startswith(fPre) or not qmFile.endswith(fExt): continue qmLang = qmFile[len(fPre):-len(fExt)] qmName = QLocale(qmLang).nativeLanguageName().title() if qmLang and qmName and qmLang != "en": langList[qmLang] = qmName return sorted(langList.items(), key=lambda x: x[0]) def loadConfig(self): """Load preferences from file and replace default settings. """ logger.debug("Loading config file") if self.confPath is None: return False cnfParse = configparser.ConfigParser() cnfPath = os.path.join(self.confPath, self.confFile) try: with open(cnfPath, mode="r", encoding="utf8") as inFile: cnfParse.read_file(inFile) except Exception as e: logger.error("Could not load config file") logException() self.hasError = True self.errData.append("Could not load config file") self.errData.append(str(e)) return False ## Main cnfSec = "Main" self.guiTheme = self._parseLine(cnfParse, cnfSec, "theme", self.CNF_STR, self.guiTheme) self.guiSyntax = self._parseLine(cnfParse, cnfSec, "syntax", self.CNF_STR, self.guiSyntax) self.guiIcons = self._parseLine(cnfParse, cnfSec, "icons", self.CNF_STR, self.guiIcons) self.guiDark = self._parseLine(cnfParse, cnfSec, "guidark", self.CNF_BOOL, self.guiDark) self.guiFont = self._parseLine(cnfParse, cnfSec, "guifont", self.CNF_STR, self.guiFont) self.guiFontSize = self._parseLine(cnfParse, cnfSec, "guifontsize", self.CNF_INT, self.guiFontSize) self.lastNotes = self._parseLine(cnfParse, cnfSec, "lastnotes", self.CNF_STR, self.lastNotes) self.guiLang = self._parseLine(cnfParse, cnfSec, "guilang", self.CNF_STR, self.guiLang) ## Sizes cnfSec = "Sizes" self.winGeometry = self._parseLine(cnfParse, cnfSec, "geometry", self.CNF_I_LST, self.winGeometry) self.prefGeometry = self._parseLine(cnfParse, cnfSec, "preferences", self.CNF_I_LST, self.prefGeometry) self.treeColWidth = self._parseLine(cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth) self.novelColWidth = self._parseLine(cnfParse, cnfSec, "novelcols", self.CNF_I_LST, self.novelColWidth) self.projColWidth = self._parseLine(cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth) self.mainPanePos = self._parseLine(cnfParse, cnfSec, "mainpane", self.CNF_I_LST, self.mainPanePos) self.docPanePos = self._parseLine(cnfParse, cnfSec, "docpane", self.CNF_I_LST, self.docPanePos) self.viewPanePos = self._parseLine(cnfParse, cnfSec, "viewpane", self.CNF_I_LST, self.viewPanePos) self.outlnPanePos = self._parseLine(cnfParse, cnfSec, "outlinepane", self.CNF_I_LST, self.outlnPanePos) self.isFullScreen = self._parseLine(cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen) self.hideVScroll = self._parseLine(cnfParse, cnfSec, "hidevscroll", self.CNF_BOOL, self.hideVScroll) self.hideHScroll = self._parseLine(cnfParse, cnfSec, "hidehscroll", self.CNF_BOOL, self.hideHScroll) ## Project cnfSec = "Project" self.autoSaveProj = self._parseLine(cnfParse, cnfSec, "autosaveproject", self.CNF_INT, self.autoSaveProj) self.autoSaveDoc = self._parseLine(cnfParse, cnfSec, "autosavedoc", self.CNF_INT, self.autoSaveDoc) ## Editor cnfSec = "Editor" self.textFont = self._parseLine(cnfParse, cnfSec, "textfont", self.CNF_STR, self.textFont) self.textSize = self._parseLine(cnfParse, cnfSec, "textsize", self.CNF_INT, self.textSize) self.textFixedW = self._parseLine(cnfParse, cnfSec, "fixedwidth", self.CNF_BOOL, self.textFixedW) self.textWidth = self._parseLine(cnfParse, cnfSec, "width", self.CNF_INT, self.textWidth) self.textMargin = self._parseLine(cnfParse, cnfSec, "margin", self.CNF_INT, self.textMargin) self.tabWidth = self._parseLine(cnfParse, cnfSec, "tabwidth", self.CNF_INT, self.tabWidth) self.focusWidth = self._parseLine(cnfParse, cnfSec, "focuswidth", self.CNF_INT, self.focusWidth) self.hideFocusFooter = self._parseLine(cnfParse, cnfSec, "hidefocusfooter", self.CNF_BOOL, self.hideFocusFooter) self.doJustify = self._parseLine(cnfParse, cnfSec, "justify", self.CNF_BOOL, self.doJustify) self.autoSelect = self._parseLine(cnfParse, cnfSec, "autoselect", self.CNF_BOOL, self.autoSelect) self.doReplace = self._parseLine(cnfParse, cnfSec, "autoreplace", self.CNF_BOOL, self.doReplace) self.doReplaceSQuote = self._parseLine(cnfParse, cnfSec, "repsquotes", self.CNF_BOOL, self.doReplaceSQuote) self.doReplaceDQuote = self._parseLine(cnfParse, cnfSec, "repdquotes", self.CNF_BOOL, self.doReplaceDQuote) self.doReplaceDash = self._parseLine(cnfParse, cnfSec, "repdash", self.CNF_BOOL, self.doReplaceDash) self.doReplaceDots = self._parseLine(cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots) self.scrollPastEnd = self._parseLine(cnfParse, cnfSec, "scrollpastend", self.CNF_BOOL, self.scrollPastEnd) self.autoScroll = self._parseLine(cnfParse, cnfSec, "autoscroll", self.CNF_BOOL, self.autoScroll) self.autoScrollPos = self._parseLine(cnfParse, cnfSec, "autoscrollpos", self.CNF_INT, self.autoScrollPos) self.fmtSingleQuotes = self._parseLine(cnfParse, cnfSec, "fmtsinglequote", self.CNF_S_LST, self.fmtSingleQuotes) self.fmtDoubleQuotes = self._parseLine(cnfParse, cnfSec, "fmtdoublequote", self.CNF_S_LST, self.fmtDoubleQuotes) self.fmtPadBefore = self._parseLine(cnfParse, cnfSec, "fmtpadbefore", self.CNF_STR, self.fmtPadBefore) self.fmtPadAfter = self._parseLine(cnfParse, cnfSec, "fmtpadafter", self.CNF_STR, self.fmtPadAfter) self.fmtPadThin = self._parseLine(cnfParse, cnfSec, "fmtpadthin", self.CNF_BOOL, self.fmtPadThin) self.spellTool = self._parseLine(cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool) self.spellLanguage = self._parseLine(cnfParse, cnfSec, "spellcheck", self.CNF_STR, self.spellLanguage) self.showTabsNSpaces = self._parseLine(cnfParse, cnfSec, "showtabsnspaces", self.CNF_BOOL, self.showTabsNSpaces) self.showLineEndings = self._parseLine(cnfParse, cnfSec, "showlineendings", self.CNF_BOOL, self.showLineEndings) self.bigDocLimit = self._parseLine(cnfParse, cnfSec, "bigdoclimit", self.CNF_INT, self.bigDocLimit) self.showFullPath = self._parseLine(cnfParse, cnfSec, "showfullpath", self.CNF_BOOL, self.showFullPath) self.highlightQuotes = self._parseLine(cnfParse, cnfSec, "highlightquotes", self.CNF_BOOL, self.highlightQuotes) self.allowOpenSQuote = self._parseLine(cnfParse, cnfSec, "allowopensquote", self.CNF_BOOL, self.allowOpenSQuote) self.allowOpenDQuote = self._parseLine(cnfParse, cnfSec, "allowopendquote", self.CNF_BOOL, self.allowOpenDQuote) self.highlightEmph = self._parseLine(cnfParse, cnfSec, "highlightemph", self.CNF_BOOL, self.highlightEmph) self.stopWhenIdle = self._parseLine(cnfParse, cnfSec, "stopwhenidle", self.CNF_BOOL, self.stopWhenIdle) self.userIdleTime = self._parseLine(cnfParse, cnfSec, "useridletime", self.CNF_INT, self.userIdleTime) ## Backup cnfSec = "Backup" self.backupPath = self._parseLine(cnfParse, cnfSec, "backuppath", self.CNF_STR, self.backupPath) self.backupOnClose = self._parseLine(cnfParse, cnfSec, "backuponclose", self.CNF_BOOL, self.backupOnClose) self.askBeforeBackup = self._parseLine(cnfParse, cnfSec, "askbeforebackup", self.CNF_BOOL, self.askBeforeBackup) ## State cnfSec = "State" self.showRefPanel = self._parseLine(cnfParse, cnfSec, "showrefpanel", self.CNF_BOOL, self.showRefPanel) self.viewComments = self._parseLine(cnfParse, cnfSec, "viewcomments", self.CNF_BOOL, self.viewComments) self.viewSynopsis = self._parseLine(cnfParse, cnfSec, "viewsynopsis", self.CNF_BOOL, self.viewSynopsis) self.searchCase = self._parseLine(cnfParse, cnfSec, "searchcase", self.CNF_BOOL, self.searchCase) self.searchWord = self._parseLine(cnfParse, cnfSec, "searchword", self.CNF_BOOL, self.searchWord) self.searchRegEx = self._parseLine(cnfParse, cnfSec, "searchregex", self.CNF_BOOL, self.searchRegEx) self.searchLoop = self._parseLine(cnfParse, cnfSec, "searchloop", self.CNF_BOOL, self.searchLoop) self.searchNextFile = self._parseLine(cnfParse, cnfSec, "searchnextfile", self.CNF_BOOL, self.searchNextFile) self.searchMatchCap = self._parseLine(cnfParse, cnfSec, "searchmatchcap", self.CNF_BOOL, self.searchMatchCap) ## Path cnfSec = "Path" self.lastPath = self._parseLine(cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath) # Check Certain Values for None self.spellLanguage = self._checkNone(self.spellLanguage) # If we're using straight quotes, disable auto-replace if self.fmtSingleQuotes == ["'", "'"] and self.doReplaceSQuote: logger.info( "Using straight single quotes, so disabling auto-replace") self.doReplaceSQuote = False if self.fmtDoubleQuotes == ["\"", "\""] 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") if self.confPath is None: return False cnfParse = configparser.ConfigParser() # Set options ## Main cnfSec = "Main" cnfParse.add_section(cnfSec) cnfParse.set(cnfSec, "timestamp", formatTimeStamp(time())) cnfParse.set(cnfSec, "theme", str(self.guiTheme)) cnfParse.set(cnfSec, "syntax", str(self.guiSyntax)) cnfParse.set(cnfSec, "icons", str(self.guiIcons)) cnfParse.set(cnfSec, "guidark", str(self.guiDark)) cnfParse.set(cnfSec, "guifont", str(self.guiFont)) cnfParse.set(cnfSec, "guifontsize", str(self.guiFontSize)) cnfParse.set(cnfSec, "lastnotes", str(self.lastNotes)) cnfParse.set(cnfSec, "guilang", str(self.guiLang)) ## Sizes cnfSec = "Sizes" cnfParse.add_section(cnfSec) cnfParse.set(cnfSec, "geometry", self._packList(self.winGeometry)) cnfParse.set(cnfSec, "preferences", self._packList(self.prefGeometry)) cnfParse.set(cnfSec, "treecols", self._packList(self.treeColWidth)) cnfParse.set(cnfSec, "novelcols", self._packList(self.novelColWidth)) cnfParse.set(cnfSec, "projcols", self._packList(self.projColWidth)) cnfParse.set(cnfSec, "mainpane", self._packList(self.mainPanePos)) cnfParse.set(cnfSec, "docpane", self._packList(self.docPanePos)) cnfParse.set(cnfSec, "viewpane", self._packList(self.viewPanePos)) cnfParse.set(cnfSec, "outlinepane", self._packList(self.outlnPanePos)) cnfParse.set(cnfSec, "fullscreen", str(self.isFullScreen)) cnfParse.set(cnfSec, "hidevscroll", str(self.hideVScroll)) cnfParse.set(cnfSec, "hidehscroll", str(self.hideHScroll)) ## Project cnfSec = "Project" cnfParse.add_section(cnfSec) cnfParse.set(cnfSec, "autosaveproject", str(self.autoSaveProj)) cnfParse.set(cnfSec, "autosavedoc", str(self.autoSaveDoc)) ## Editor cnfSec = "Editor" cnfParse.add_section(cnfSec) cnfParse.set(cnfSec, "textfont", str(self.textFont)) cnfParse.set(cnfSec, "textsize", str(self.textSize)) cnfParse.set(cnfSec, "fixedwidth", str(self.textFixedW)) cnfParse.set(cnfSec, "width", str(self.textWidth)) cnfParse.set(cnfSec, "margin", str(self.textMargin)) cnfParse.set(cnfSec, "tabwidth", str(self.tabWidth)) cnfParse.set(cnfSec, "focuswidth", str(self.focusWidth)) cnfParse.set(cnfSec, "hidefocusfooter", str(self.hideFocusFooter)) cnfParse.set(cnfSec, "justify", str(self.doJustify)) cnfParse.set(cnfSec, "autoselect", str(self.autoSelect)) cnfParse.set(cnfSec, "autoreplace", str(self.doReplace)) cnfParse.set(cnfSec, "repsquotes", str(self.doReplaceSQuote)) cnfParse.set(cnfSec, "repdquotes", str(self.doReplaceDQuote)) cnfParse.set(cnfSec, "repdash", str(self.doReplaceDash)) cnfParse.set(cnfSec, "repdots", str(self.doReplaceDots)) cnfParse.set(cnfSec, "scrollpastend", str(self.scrollPastEnd)) cnfParse.set(cnfSec, "autoscroll", str(self.autoScroll)) cnfParse.set(cnfSec, "autoscrollpos", str(self.autoScrollPos)) cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes)) cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes)) cnfParse.set(cnfSec, "fmtpadbefore", str(self.fmtPadBefore)) cnfParse.set(cnfSec, "fmtpadafter", str(self.fmtPadAfter)) cnfParse.set(cnfSec, "fmtpadthin", str(self.fmtPadThin)) cnfParse.set(cnfSec, "spelltool", str(self.spellTool)) cnfParse.set(cnfSec, "spellcheck", str(self.spellLanguage)) cnfParse.set(cnfSec, "showtabsnspaces", str(self.showTabsNSpaces)) cnfParse.set(cnfSec, "showlineendings", str(self.showLineEndings)) cnfParse.set(cnfSec, "bigdoclimit", str(self.bigDocLimit)) cnfParse.set(cnfSec, "showfullpath", str(self.showFullPath)) cnfParse.set(cnfSec, "highlightquotes", str(self.highlightQuotes)) cnfParse.set(cnfSec, "allowopensquote", str(self.allowOpenSQuote)) cnfParse.set(cnfSec, "allowopendquote", str(self.allowOpenDQuote)) cnfParse.set(cnfSec, "highlightemph", str(self.highlightEmph)) cnfParse.set(cnfSec, "stopwhenidle", str(self.stopWhenIdle)) cnfParse.set(cnfSec, "useridletime", str(self.userIdleTime)) ## Backup cnfSec = "Backup" cnfParse.add_section(cnfSec) cnfParse.set(cnfSec, "backuppath", str(self.backupPath)) cnfParse.set(cnfSec, "backuponclose", str(self.backupOnClose)) cnfParse.set(cnfSec, "askbeforebackup", str(self.askBeforeBackup)) ## State cnfSec = "State" cnfParse.add_section(cnfSec) cnfParse.set(cnfSec, "showrefpanel", str(self.showRefPanel)) cnfParse.set(cnfSec, "viewcomments", str(self.viewComments)) cnfParse.set(cnfSec, "viewsynopsis", str(self.viewSynopsis)) cnfParse.set(cnfSec, "searchcase", str(self.searchCase)) cnfParse.set(cnfSec, "searchword", str(self.searchWord)) cnfParse.set(cnfSec, "searchregex", str(self.searchRegEx)) cnfParse.set(cnfSec, "searchloop", str(self.searchLoop)) cnfParse.set(cnfSec, "searchnextfile", str(self.searchNextFile)) cnfParse.set(cnfSec, "searchmatchcap", str(self.searchMatchCap)) ## Path cnfSec = "Path" cnfParse.add_section(cnfSec) cnfParse.set(cnfSec, "lastpath", str(self.lastPath)) # Write config file cnfPath = os.path.join(self.confPath, self.confFile) try: with open(cnfPath, mode="w", encoding="utf8") as outFile: cnfParse.write(outFile) self.confChanged = False except Exception as e: logger.error("Could not save config file") logException() self.hasError = True self.errData.append("Could not save config file") self.errData.append(str(e)) return False return True def loadRecentCache(self): """Load the cache file for recent projects. """ if self.dataPath is None: return False cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE) self.recentProj = {} if os.path.isfile(cacheFile): try: with open(cacheFile, mode="r", encoding="utf8") as inFile: theData = json.load(inFile) for projPath in theData.keys(): theEntry = theData[projPath] theTitle = "" lastTime = 0 wordCount = 0 if "title" in theEntry.keys(): theTitle = theEntry["title"] if "time" in theEntry.keys(): lastTime = int(theEntry["time"]) if "words" in theEntry.keys(): wordCount = int(theEntry["words"]) self.recentProj[projPath] = { "title": theTitle, "time": lastTime, "words": wordCount, } except Exception as e: self.hasError = True self.errData.append("Could not load recent project cache") self.errData.append(str(e)) return False return True def saveRecentCache(self): """Save the cache dictionary of recent projects. """ if self.dataPath is None: return False cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE) cacheTemp = os.path.join(self.dataPath, nwFiles.RECENT_FILE + "~") try: with open(cacheTemp, mode="w+", encoding="utf8") as outFile: json.dump(self.recentProj, outFile, indent=2) except Exception as e: self.hasError = True self.errData.append("Could not save recent project cache") self.errData.append(str(e)) return False if os.path.isfile(cacheFile): os.unlink(cacheFile) os.rename(cacheTemp, cacheFile) return True def updateRecentCache(self, projPath, projTitle, wordCount, saveTime): """Add or update recent cache information o9n a given project. """ self.recentProj[os.path.abspath(projPath)] = { "title": projTitle, "time": int(saveTime), "words": int(wordCount), } return True def removeFromRecentCache(self, thePath): """Trying to remove a path from the recent projects cache. """ if thePath in self.recentProj: del self.recentProj[thePath] logger.verbose("Removed recent: %s" % thePath) self.saveRecentCache() else: logger.error("Unknown recent: %s" % thePath) return False return True ## # Setters ## def setConfPath(self, newPath): if newPath is None: return True if not os.path.isfile(newPath): logger.error("File not found, using default config path instead") return False self.confPath = os.path.dirname(newPath) self.confFile = os.path.basename(newPath) return True def setDataPath(self, newPath): if newPath is None: return True if not os.path.isdir(newPath): logger.error("Path not found, using default data path instead") return False self.dataPath = os.path.abspath(newPath) return True def setLastPath(self, lastPath): if lastPath is None or lastPath == "": self.lastPath = "" else: self.lastPath = os.path.dirname(lastPath) return True def setWinSize(self, newWidth, newHeight): newWidth = int(newWidth / self.guiScale) newHeight = int(newHeight / self.guiScale) if abs(self.winGeometry[0] - newWidth) > 5: self.winGeometry[0] = newWidth self.confChanged = True if abs(self.winGeometry[1] - newHeight) > 5: self.winGeometry[1] = newHeight self.confChanged = True return True def setPreferencesSize(self, newWidth, newHeight): self.prefGeometry[0] = int(newWidth / self.guiScale) self.prefGeometry[1] = int(newHeight / self.guiScale) self.confChanged = True return True def setTreeColWidths(self, colWidths): self.treeColWidth = [int(x / self.guiScale) for x in colWidths] self.confChanged = True return True def setNovelColWidths(self, colWidths): self.novelColWidth = [int(x / self.guiScale) for x in colWidths] self.confChanged = True return True def setProjColWidths(self, colWidths): self.projColWidth = [int(x / self.guiScale) for x in colWidths] self.confChanged = True return True def setMainPanePos(self, panePos): self.mainPanePos = [int(x / self.guiScale) for x in panePos] self.confChanged = True return True def setDocPanePos(self, panePos): self.docPanePos = [int(x / self.guiScale) for x in panePos] self.confChanged = True return True def setViewPanePos(self, panePos): self.viewPanePos = [int(x / self.guiScale) for x in panePos] self.confChanged = True return True def setOutlinePanePos(self, panePos): self.outlnPanePos = [int(x / self.guiScale) for x in panePos] self.confChanged = True return True def setShowRefPanel(self, checkState): self.showRefPanel = checkState self.confChanged = True return self.showRefPanel def getErrData(self): errMessage = "<br>".join(self.errData) self.hasError = False self.errData = [] return errMessage def setViewComments(self, viewState): self.viewComments = viewState self.confChanged = True return self.viewComments def setViewSynopsis(self, viewState): self.viewSynopsis = viewState self.confChanged = True return self.viewSynopsis ## # Getters ## def getWinSize(self): return [int(x * self.guiScale) for x in self.winGeometry] def getPreferencesSize(self): return [int(x * self.guiScale) for x in self.prefGeometry] def getTreeColWidths(self): return [int(x * self.guiScale) for x in self.treeColWidth] def getNovelColWidths(self): return [int(x * self.guiScale) for x in self.novelColWidth] def getProjColWidths(self): return [int(x * self.guiScale) for x in self.projColWidth] def getMainPanePos(self): return [int(x * self.guiScale) for x in self.mainPanePos] def getDocPanePos(self): return [int(x * self.guiScale) for x in self.docPanePos] def getViewPanePos(self): return [int(x * self.guiScale) for x in self.viewPanePos] def getOutlinePanePos(self): return [int(x * self.guiScale) for x in self.outlnPanePos] def getTextWidth(self): return self.pxInt(self.textWidth) def getTextMargin(self): return self.pxInt(self.textMargin) def getTabWidth(self): return self.pxInt(self.tabWidth) def getFocusWidth(self): return self.pxInt(self.focusWidth) ## # Internal Functions ## def _packList(self, inData): """Pack a list of items into a comma-separated string. """ return ", ".join([str(inVal) for inVal in inData]) def _unpackList(self, inStr, listDefault, cnfType): """Unpack a comma-separated string of items into a list. """ inData = inStr.split(",") outData = listDefault.copy() for i in range(min(len(inData), len(listDefault))): try: if cnfType == self.CNF_S_LST: outData[i] = inData[i].strip() elif cnfType == self.CNF_I_LST: outData[i] = int(inData[i].strip()) else: continue except Exception: continue return outData def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault): """Parse a line and return the correct datatype. """ if cnfParse.has_section(cnfSec): if cnfParse.has_option(cnfSec, cnfName): try: if cnfType == self.CNF_STR: return cnfParse.get(cnfSec, cnfName) elif cnfType == self.CNF_INT: return cnfParse.getint(cnfSec, cnfName) elif cnfType == self.CNF_BOOL: return cnfParse.getboolean(cnfSec, cnfName) elif cnfType == self.CNF_I_LST: return self._unpackList(cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_I_LST) elif cnfType == self.CNF_S_LST: return self._unpackList(cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST) except ValueError: logger.error("Failed to load value from config file.") logException() return cnfDefault return cnfDefault def _checkNone(self, checkVal): """Return a NoneType if the value correspomds 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 self.hasEnchant = True logger.debug("Checking package 'pyenchant': OK") except Exception: self.hasEnchant = False logger.debug("Checking package 'pyenchant': Missing") assistPath = shutil.which("assistant") self.hasAssistant = assistPath is not None if self.hasAssistant: logger.debug("Checking executable 'assistant': OK") else: logger.debug("Checking executable 'assistant': Missing") return
class Config: LANG_NW = 1 LANG_PROJ = 2 def __init__(self): # Set Application Variables self.appName = "novelWriter" self.appHandle = self.appName.lower() # Config Error Handling self.hasError = False # True if the config class encountered an error self.errData = [] # List of error messages # Set Paths self.cmdOpen = None # Path from command line for project to be opened on launch self.confPath = None # Folder where the config is saved self.confFile = None # The config file name self.dataPath = None # Folder where app data is stored self.lastPath = None # The last user-selected folder (browse dialogs) self.appPath = None # The full path to the novelwriter package folder self.appRoot = None # The full path to the novelwriter root folder self.appIcon = None # The full path to the novelwriter icon file self.assetPath = None # The full path to the novelwriter/assets folder self.pdfDocs = None # The location of the PDF manual, if it exists # Runtime Settings and Variables self.confChanged = False # True whenever the config has chenged, false after save # General self.guiTheme = "" # GUI theme self.guiSyntax = "" # Syntax theme self.guiIcons = "" # Icon theme self.guiFont = "" # Defaults to system default font self.guiFontSize = 11 # Is overridden if system default is loaded self.guiScale = 1.0 # Set automatically by Theme class self.lastNotes = "0x0" # The latest release notes that have been shown self.setDefaultGuiTheme() self.setDefaultSyntaxTheme() self.setDefaultIconTheme() # Localisation self.qLocal = QLocale.system() self.guiLang = self.qLocal.name() self.qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath) self.nwLangPath = None self.qtTrans = {} # Sizes self.winGeometry = [1200, 650] self.prefGeometry = [700, 615] self.projColWidth = [200, 60, 140] self.mainPanePos = [300, 800] self.docPanePos = [400, 400] self.viewPanePos = [500, 150] self.outlnPanePos = [500, 150] self.isFullScreen = False # Features self.hideVScroll = False # Hide vertical scroll bars on main widgets self.hideHScroll = False # Hide horizontal scroll bars on main widgets self.emphLabels = True # Add emphasis to H1 and H2 item labels # Project self.autoSaveProj = 60 # Interval for auto-saving project in seconds self.autoSaveDoc = 30 # Interval for auto-saving document in seconds # Text Editor 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 Symbols self.fmtApostrophe = nwUnicode.U_RSQUO self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO] self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO] self.fmtPadBefore = "" self.fmtPadAfter = "" self.fmtPadThin = False # Spell Checking self.spellLanguage = None # Search Bar Switches self.searchCase = False self.searchWord = False self.searchRegEx = False self.searchLoop = False self.searchNextFile = False self.searchMatchCap = False # Backup self.backupPath = "" self.backupOnClose = False self.askBeforeBackup = True # 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 # 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 = "Unknown" self.kernelVer = "Unknown" # Packages self.hasEnchant = False # The pyenchant package # Recent Cache self.recentProj = {} 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) ## # 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 confPath is None: confRoot = QStandardPaths.writableLocation( QStandardPaths.ConfigLocation) self.confPath = os.path.join(os.path.abspath(confRoot), self.appHandle) else: logger.info("Setting config from alternative path: %s", confPath) self.confPath = confPath if dataPath is None: if self.verQtValue >= 50400: dataRoot = QStandardPaths.writableLocation( QStandardPaths.AppDataLocation) else: dataRoot = QStandardPaths.writableLocation( QStandardPaths.DataLocation) self.dataPath = os.path.join(os.path.abspath(dataRoot), self.appHandle) else: logger.info("Setting data path from alternative path: %s", dataPath) self.dataPath = dataPath logger.verbose("Config path: %s", self.confPath) logger.verbose("Data path: %s", self.dataPath) # Check Data Path Subdirs dataDirs = ["syntax", "themes"] for dataDir in dataDirs: dirPath = os.path.join(self.dataPath, dataDir) if not os.path.isdir(dirPath): try: os.mkdir(dirPath) logger.info("Created folder: %s", dirPath) except Exception: logger.error("Could not create folder: %s", dirPath) logException() self.confFile = self.appHandle + ".conf" self.lastPath = os.path.expanduser("~") self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) self.appRoot = os.path.abspath( os.path.join(self.appPath, os.path.pardir)) if os.path.isfile(self.appRoot): # novelWriter is packaged as a single file, so the app and # root paths are the same, and equal to the folder that # contains the single executable. self.appRoot = os.path.dirname(self.appRoot) self.appPath = self.appRoot # Assets self.assetPath = os.path.join(self.appPath, "assets") self.appIcon = os.path.join(self.assetPath, "icons", "novelwriter.svg") # Internationalisation self.nwLangPath = os.path.join(self.assetPath, "i18n") logger.debug("Assets: %s", self.assetPath) logger.verbose("App path: %s", self.appPath) logger.verbose("Last path: %s", self.lastPath) # If the config folder does not exist, create it. # This assumes that the os config folder itself exists. if not os.path.isdir(self.confPath): try: os.mkdir(self.confPath) except Exception as exc: logger.error("Could not create folder: %s", self.confPath) logException() self.hasError = True self.errData.append("Could not create folder: %s" % self.confPath) self.errData.append(formatException(exc)) self.confPath = None # Check if config file exists if self.confPath is not None: if os.path.isfile(os.path.join(self.confPath, self.confFile)): # If it exists, load it self.loadConfig() else: # If it does not exist, save a copy of the default values self.saveConfig() # If the data folder does not exist, create it. # This assumes that the os data folder itself exists. if self.dataPath is not None: if not os.path.isdir(self.dataPath): try: os.mkdir(self.dataPath) except Exception as exc: logger.error("Could not create folder: %s", self.dataPath) logException() self.hasError = True self.errData.append("Could not create folder: %s" % self.dataPath) self.errData.append(formatException(exc)) self.dataPath = None # Host and Kernel if self.verQtValue >= 50600: self.hostName = QSysInfo.machineHostName() self.kernelVer = QSysInfo.kernelVersion() # Load recent projects cache self.loadRecentCache() # Check the availability of optional packages self._checkOptionalPackages() if self.spellLanguage is None: self.spellLanguage = "en" # Look for a PDF version of the manual pdfDocs = os.path.join(self.assetPath, "manual.pdf") if os.path.isfile(pdfDocs): logger.debug("Found manual: %s", pdfDocs) self.pdfDocs = pdfDocs logger.debug("Config initialisation complete") return True def initLocalisation(self, nwApp): """Initialise the localisation of the GUI. """ self.qLocal = QLocale(self.guiLang) QLocale.setDefault(self.qLocal) self.qtTrans = {} langList = [ (self.qtLangPath, "qtbase"), # Qt 5.x (self.nwLangPath, "qtbase"), # Alternative Qt 5.x (self.nwLangPath, "nw"), # novelWriter ] for lngPath, lngBase in langList: for lngCode in self.qLocal.uiLanguages(): qTrans = QTranslator() lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_")) if lngFile not in self.qtTrans: if qTrans.load(lngFile, lngPath): logger.debug("Loaded: %s", os.path.join(lngPath, lngFile)) nwApp.installTranslator(qTrans) self.qtTrans[lngFile] = qTrans return def listLanguages(self, lngSet): """List localisation files in the i18n folder. The default GUI language 'en_GB' is British English. """ 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 os.listdir(self.nwLangPath): if not os.path.isfile(os.path.join(self.nwLangPath, qmFile)): continue if not qmFile.startswith(fPre) or not qmFile.endswith(fExt): continue qmLang = qmFile[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]) def loadConfig(self): """Load preferences from file and replace default settings. """ logger.debug("Loading config file") if self.confPath is None: return False theConf = NWConfigParser() cnfPath = os.path.join(self.confPath, self.confFile) 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.guiIcons = theConf.rdStr(cnfSec, "icons", self.guiIcons) self.guiFont = theConf.rdStr(cnfSec, "guifont", self.guiFont) self.guiFontSize = theConf.rdInt(cnfSec, "guifontsize", self.guiFontSize) self.lastNotes = theConf.rdStr(cnfSec, "lastnotes", self.lastNotes) self.guiLang = theConf.rdStr(cnfSec, "guilang", self.guiLang) self.hideVScroll = theConf.rdBool(cnfSec, "hidevscroll", self.hideVScroll) self.hideHScroll = theConf.rdBool(cnfSec, "hidehscroll", self.hideHScroll) # Sizes cnfSec = "Sizes" self.winGeometry = theConf.rdIntList(cnfSec, "geometry", self.winGeometry) self.prefGeometry = theConf.rdIntList(cnfSec, "preferences", self.prefGeometry) self.projColWidth = theConf.rdIntList(cnfSec, "projcols", self.projColWidth) self.mainPanePos = theConf.rdIntList(cnfSec, "mainpane", self.mainPanePos) self.docPanePos = theConf.rdIntList(cnfSec, "docpane", self.docPanePos) self.viewPanePos = theConf.rdIntList(cnfSec, "viewpane", self.viewPanePos) self.outlnPanePos = theConf.rdIntList(cnfSec, "outlinepane", self.outlnPanePos) self.isFullScreen = theConf.rdBool(cnfSec, "fullscreen", self.isFullScreen) # 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) # 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.fmtSingleQuotes = theConf.rdStrList(cnfSec, "fmtsinglequote", self.fmtSingleQuotes) self.fmtDoubleQuotes = theConf.rdStrList(cnfSec, "fmtdoublequote", self.fmtDoubleQuotes) 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) # Backup cnfSec = "Backup" self.backupPath = theConf.rdStr(cnfSec, "backuppath", self.backupPath) self.backupOnClose = theConf.rdBool(cnfSec, "backuponclose", self.backupOnClose) self.askBeforeBackup = theConf.rdBool(cnfSec, "askbeforebackup", self.askBeforeBackup) # State cnfSec = "State" 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) # Path cnfSec = "Path" self.lastPath = theConf.rdStr(cnfSec, "lastpath", self.lastPath) # Check Certain Values for None self.spellLanguage = self._checkNone(self.spellLanguage) # If we're using straight quotes, disable auto-replace if self.fmtSingleQuotes == ["'", "'"] and self.doReplaceSQuote: logger.info( "Using straight single quotes, so disabling auto-replace") self.doReplaceSQuote = False if self.fmtDoubleQuotes == ['"', '"'] and self.doReplaceDQuote: logger.info( "Using straight double quotes, so disabling auto-replace") self.doReplaceDQuote = False # Check deprecated settings if self.guiIcons in ("typicons_colour_dark", "typicons_grey_dark"): self.guiIcons = "typicons_dark" elif self.guiIcons in ("typicons_colour_light", "typicons_grey_light"): self.guiIcons = "typicons_light" return True def saveConfig(self): """Save the current preferences to file. """ logger.debug("Saving config file") if self.confPath is None: return False theConf = NWConfigParser() theConf["Main"] = { "timestamp": formatTimeStamp(time()), "theme": str(self.guiTheme), "syntax": str(self.guiSyntax), "icons": str(self.guiIcons), "guifont": str(self.guiFont), "guifontsize": str(self.guiFontSize), "lastnotes": str(self.lastNotes), "guilang": str(self.guiLang), "hidevscroll": str(self.hideVScroll), "hidehscroll": str(self.hideHScroll), } theConf["Sizes"] = { "geometry": self._packList(self.winGeometry), "preferences": self._packList(self.prefGeometry), "projcols": self._packList(self.projColWidth), "mainpane": self._packList(self.mainPanePos), "docpane": self._packList(self.docPanePos), "viewpane": self._packList(self.viewPanePos), "outlinepane": self._packList(self.outlnPanePos), "fullscreen": str(self.isFullScreen), } theConf["Project"] = { "autosaveproject": str(self.autoSaveProj), "autosavedoc": str(self.autoSaveDoc), "emphlabels": str(self.emphLabels), } 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), "fmtsinglequote": self._packList(self.fmtSingleQuotes), "fmtdoublequote": self._packList(self.fmtDoubleQuotes), "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["Backup"] = { "backuppath": str(self.backupPath), "backuponclose": str(self.backupOnClose), "askbeforebackup": str(self.askBeforeBackup), } theConf["State"] = { "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), } theConf["Path"] = { "lastpath": str(self.lastPath), } # Write config file cnfPath = os.path.join(self.confPath, self.confFile) try: with open(cnfPath, mode="w", encoding="utf-8") as outFile: theConf.write(outFile) self.confChanged = False 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 def loadRecentCache(self): """Load the cache file for recent projects. """ if self.dataPath is None: return False self.recentProj = {} cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE) if not os.path.isfile(cacheFile): return True try: with open(cacheFile, mode="r", encoding="utf-8") as inFile: theData = json.load(inFile) for projPath, theEntry in theData.items(): self.recentProj[projPath] = { "title": theEntry.get("title", ""), "time": theEntry.get("time", 0), "words": theEntry.get("words", 0), } except Exception as exc: self.hasError = True self.errData.append("Could not load recent project cache") self.errData.append(formatException(exc)) return False return True def saveRecentCache(self): """Save the cache dictionary of recent projects. """ if self.dataPath is None: return False cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE) cacheTemp = os.path.join(self.dataPath, nwFiles.RECENT_FILE + "~") try: with open(cacheTemp, mode="w+", encoding="utf-8") as outFile: json.dump(self.recentProj, outFile, indent=2) except Exception as exc: self.hasError = True self.errData.append("Could not save recent project cache") self.errData.append(formatException(exc)) return False if os.path.isfile(cacheFile): os.unlink(cacheFile) os.rename(cacheTemp, cacheFile) return True def updateRecentCache(self, projPath, projTitle, wordCount, saveTime): """Add or update recent cache information on a given project. """ self.recentProj[os.path.abspath(projPath)] = { "title": projTitle, "time": int(saveTime), "words": int(wordCount), } return True def removeFromRecentCache(self, thePath): """Trying to remove a path from the recent projects cache. """ if thePath in self.recentProj: del self.recentProj[thePath] logger.verbose("Removed recent: %s", thePath) self.saveRecentCache() else: logger.error("Unknown recent: %s", thePath) return False return True ## # Setters ## def setConfPath(self, newPath): """Set the path and filename to the config file. """ if newPath is None: return True if not os.path.isfile(newPath): logger.error("File not found, using default config path instead") return False self.confPath = os.path.dirname(newPath) self.confFile = os.path.basename(newPath) return True def setDataPath(self, newPath): """Set the data path. """ if newPath is None: return True if not os.path.isdir(newPath): logger.error("Path not found, using default data path instead") return False self.dataPath = os.path.abspath(newPath) return True def setLastPath(self, lastPath): """Set the last used path (by the user). """ if lastPath is None or lastPath == "": self.lastPath = "" else: self.lastPath = os.path.dirname(lastPath) return True def setWinSize(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.winGeometry[0] - newWidth) > 5: self.winGeometry[0] = newWidth self.confChanged = True if abs(self.winGeometry[1] - newHeight) > 5: self.winGeometry[1] = newHeight self.confChanged = True return True def setPreferencesSize(self, newWidth, newHeight): """Sat the size of the Preferences dialog window. """ self.prefGeometry[0] = int(newWidth / self.guiScale) self.prefGeometry[1] = int(newHeight / self.guiScale) self.confChanged = True return True def setProjColWidths(self, colWidths): """Set the column widths of the Load Project dialog. """ self.projColWidth = [int(x / self.guiScale) for x in colWidths] self.confChanged = True return True def setMainPanePos(self, panePos): """Set the position of the main GUI splitter. """ self.mainPanePos = [int(x / self.guiScale) for x in panePos] self.confChanged = True return True def setDocPanePos(self, panePos): """Set the position of the main editor/viewer splitter. """ self.docPanePos = [int(x / self.guiScale) for x in panePos] self.confChanged = True return True def setViewPanePos(self, panePos): """Set the position of the viewer meta data splitter. """ self.viewPanePos = [int(x / self.guiScale) for x in panePos] self.confChanged = True return True def setOutlinePanePos(self, panePos): """Set the position of the outline details splitter. """ self.outlnPanePos = [int(x / self.guiScale) for x in panePos] self.confChanged = True return True def setShowRefPanel(self, checkState): """Set the visibility state of the reference panel. """ self.showRefPanel = checkState self.confChanged = True return self.showRefPanel def setViewComments(self, viewState): """Set the visibility state of comments in the viewer. """ self.viewComments = viewState self.confChanged = True return self.viewComments def setViewSynopsis(self, viewState): """Set the visibility state of synopsis comments in the viewer. """ self.viewSynopsis = viewState self.confChanged = True return self.viewSynopsis ## # Default Setters ## def setDefaultGuiTheme(self): """Reset the GUI theme to default value. """ self.guiTheme = "default" def setDefaultSyntaxTheme(self): """Reset the syntax theme to default value. """ self.guiSyntax = "default_light" def setDefaultIconTheme(self): """Reset the icon theme to default value. """ self.guiIcons = "typicons_light" ## # Getters ## def getWinSize(self): return [int(x * self.guiScale) for x in self.winGeometry] def getPreferencesSize(self): return [int(x * self.guiScale) for x in self.prefGeometry] def getProjColWidths(self): return [int(x * self.guiScale) for x in self.projColWidth] def getMainPanePos(self): return [int(x * self.guiScale) for x in self.mainPanePos] def getDocPanePos(self): return [int(x * self.guiScale) for x in self.docPanePos] def getViewPanePos(self): return [int(x * self.guiScale) for x in self.viewPanePos] def getOutlinePanePos(self): return [int(x * self.guiScale) for x in self.outlnPanePos] def getTextWidth(self, focusMode=False): if focusMode: return self.pxInt(max(self.focusWidth, 200)) else: return self.pxInt(max(self.textWidth, 200)) def getTextMargin(self): return self.pxInt(max(self.textMargin, 0)) def getTabWidth(self): return self.pxInt(max(self.tabWidth, 0)) def getErrData(self): """Compile and return error messages from the initialisation of the Config class, and clear the error buffer. """ errMessage = "<br>".join(self.errData) self.hasError = False self.errData = [] return errMessage ## # 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