From b46ad86c315061425df9b9104ac9cc22f1be330f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 10 Nov 2022 13:20:26 +0100 Subject: [PATCH] Clean up config file format, modify how projects are launched, and add broken theme setting handling --- novelwriter/__init__.py | 7 +- novelwriter/config.py | 369 ++++++++---------- novelwriter/dialogs/preferences.py | 34 +- novelwriter/gui/docviewer.py | 4 +- novelwriter/gui/theme.py | 14 +- novelwriter/guimain.py | 24 +- tests/conftest.py | 4 +- tests/mock.py | 4 +- tests/reference/baseConfig_novelwriter.conf | 30 +- .../reference/guiPreferences_novelwriter.conf | 30 +- tests/test_base/test_base_config.py | 34 +- tests/test_base/test_base_init.py | 1 - tests/test_dialogs/test_dlg_preferences.py | 2 - tests/test_gui/test_gui_guimain.py | 43 +- tests/test_gui/test_gui_theme.py | 6 + 15 files changed, 277 insertions(+), 329 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 68a049f4..deff10a4 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -161,9 +161,6 @@ def main(sysArgs=None): elif inOpt == "--testmode": testMode = True - # Set Config Options - CONFIG.cmdOpen = cmdOpen - # Set Logging cHandle = logging.StreamHandler() cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{")) @@ -256,9 +253,7 @@ def main(sysArgs=None): # Launch main GUI CONFIG.initLocalisation(nwApp) nwGUI = GuiMain() - if not nwGUI.hasProject: - nwGUI.showProjectLoadDialog() - nwGUI.releaseNotes() + nwGUI.postLaunchTasks(cmdOpen) sys.exit(nwApp.exec_()) diff --git a/novelwriter/config.py b/novelwriter/config.py index 1a69fea5..53fe20fe 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -63,7 +63,7 @@ class Config: self._confPath = confRoot.absolute() / self.appHandle # The user config location self._dataPath = dataRoot.absolute() / self.appHandle # The user data location - self._lastPath = Path.home().absolute() # The user's last used path + self._homePath = Path.home().absolute() # The user's home directory self._appPath = Path(__file__).parent.absolute() self._appRoot = self._appPath.parent @@ -73,13 +73,12 @@ class Config: 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 - self._confChanged = False # True whenever the config has chenged, false after save - self.cmdOpen = None # Path from command line for project to be opened on launch + self._hasError = False # True if the config class encountered an error + self._errData = [] # List of error messages - # Localisation Info - self._qLocal = QLocale.system() + # 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") @@ -94,34 +93,32 @@ class Config: self._recentProj = RecentProjects(self) # General GUI Settings - self.guiLang = self._qLocal.name() - self.guiTheme = "" # GUI theme - self.guiSyntax = "" # Syntax 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.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._winGeometry = [1200, 650] - self._prefGeometry = [700, 615] - self._projColWidth = [200, 60, 140] - self._mainPanePos = [300, 800] - self._viewPanePos = [500, 150] - self._outlnPanePos = [500, 150] - self.isFullScreen = False - - # Feature Settings - 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 + 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.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 @@ -173,6 +170,12 @@ class Config: # 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 @@ -181,16 +184,6 @@ class Config: self.searchNextFile = False self.searchMatchCap = False - # Backup Settings - self._backupPath = None - 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 - # System and App Information # ========================== @@ -259,15 +252,15 @@ class Config: @property def mainWinSize(self): - return [int(x*self.guiScale) for x in self._winGeometry] + return [int(x*self.guiScale) for x in self._mainWinSize] @property def preferencesWinSize(self): - return [int(x*self.guiScale) for x in self._prefGeometry] + return [int(x*self.guiScale) for x in self._prefsWinSize] @property def projLoadColWidths(self): - return [int(x*self.guiScale) for x in self._projColWidth] + return [int(x*self.guiScale) for x in self._projLoadCols] @property def mainPanePos(self): @@ -281,6 +274,25 @@ class Config: 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 ## @@ -293,48 +305,36 @@ class Config: """ 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 + 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): - """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 + """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._projColWidth = [int(x/self.guiScale) for x in colWidths] - self._confChanged = True + """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. - """ + """Set the position of the main GUI splitter.""" self._mainPanePos = [int(x/self.guiScale) for x in panePos] - self._confChanged = True return def setViewPanePos(self, panePos): - """Set the position of the viewer meta data splitter. - """ + """Set the position of the viewer meta data splitter.""" self._viewPanePos = [int(x/self.guiScale) for x in panePos] - self._confChanged = True return def setOutlinePanePos(self, panePos): - """Set the position of the outline details splitter. - """ + """Set the position of the outline details splitter.""" self._outlnPanePos = [int(x/self.guiScale) for x in panePos] - self._confChanged = True return def setLastPath(self, lastPath): @@ -342,7 +342,7 @@ class Config: path is not a folder, the parent of the path is used instead. """ if isinstance(lastPath, (str, Path)): - lastPath = Path(lastPath) + lastPath = checkPath(lastPath, self._homePath) if not lastPath.is_dir(): lastPath = lastPath.parent if lastPath.is_dir(): @@ -351,15 +351,10 @@ class Config: return def setBackupPath(self, backupPath): - """Set the current backup path. - """ + """Set the current backup path.""" self._backupPath = checkPath(backupPath, None) return - def setConfigChanged(self, value): - self._confChanged = bool(value) - return - ## # Methods ## @@ -377,15 +372,13 @@ class Config: return int(theSize/self.guiScale) def dataPath(self, target=None): - """Return a path in the data folder. - """ + """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. - """ + """Return a path in the assets folder.""" if isinstance(target, str): return self._appPath / "assets" / target return self._appPath / "assets" @@ -396,11 +389,10 @@ class Config: if isinstance(self._lastPath, Path): if self._lastPath.is_dir(): return self._lastPath - return Path.home().absolute() + return self._homePath def backupPath(self): - """Return the backup path. - """ + """Return the backup path.""" if isinstance(self._backupPath, Path): if self._backupPath.is_dir(): return self._backupPath @@ -415,6 +407,33 @@ class Config: 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 ## @@ -464,54 +483,26 @@ class Config: def initLocalisation(self, nwApp): """Initialise the localisation of the GUI. """ - self._qLocal = QLocale(self.guiLang) - QLocale.setDefault(self._qLocal) + self._qLocale = QLocale(self.guiLocale) + QLocale.setDefault(self._qLocale) 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(): + for lngCode in self._qLocale.uiLanguages(): qTrans = QTranslator() lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_")) if lngFile not in self._qtTrans: - if qTrans.load(lngFile, str(lngPath)): - logger.debug("Loaded: %s/%s", lngPath, lngFile) + if qTrans.load(lngFile, lngPath): + logger.debug("Loaded: %s.qm", lngFile) nwApp.installTranslator(qTrans) self._qtTrans[lngFile] = qTrans return - 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]) - def loadConfig(self): """Load preferences from file and replace default settings. """ @@ -534,28 +525,31 @@ class Config: cnfSec = "Main" self.guiTheme = theConf.rdStr(cnfSec, "theme", self.guiTheme) self.guiSyntax = theConf.rdStr(cnfSec, "syntax", self.guiSyntax) - 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.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._winGeometry = theConf.rdIntList(cnfSec, "geometry", self._winGeometry) - self._prefGeometry = theConf.rdIntList(cnfSec, "preferences", self._prefGeometry) - self._projColWidth = theConf.rdIntList(cnfSec, "projcols", self._projColWidth) + 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) - 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) + 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" @@ -596,14 +590,9 @@ class Config: self.stopWhenIdle = theConf.rdBool(cnfSec, "stopwhenidle", self.stopWhenIdle) self.userIdleTime = theConf.rdInt(cnfSec, "useridletime", self.userIdleTime) - # Backup - cnfSec = "Backup" - self._backupPath = theConf.rdPath(cnfSec, "backuppath", self._backupPath) - self.backupOnClose = theConf.rdBool(cnfSec, "backuponclose", self.backupOnClose) - self.askBeforeBackup = theConf.rdBool(cnfSec, "askbeforebackup", self.askBeforeBackup) - # 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) @@ -614,9 +603,14 @@ class Config: self.searchNextFile = theConf.rdBool(cnfSec, "searchnextfile", self.searchNextFile) self.searchMatchCap = theConf.rdBool(cnfSec, "searchmatchcap", self.searchMatchCap) - # Path - cnfSec = "Path" - self._lastPath = theConf.rdPath(cnfSec, "lastpath", self._lastPath) + # Deprecated Settings or Locations as of 2.0 + # 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) # Check Certain Values for None self.spellLanguage = self._checkNone(self.spellLanguage) @@ -639,32 +633,38 @@ class Config: theConf = NWConfigParser() + theConf["Meta"] = { + "timestamp": formatTimeStamp(time()), + } + theConf["Main"] = { - "timestamp": formatTimeStamp(time()), - "theme": str(self.guiTheme), - "syntax": str(self.guiSyntax), - "guifont": str(self.guiFont), - "guifontsize": str(self.guiFontSize), - "lastnotes": str(self.lastNotes), - "guilang": str(self.guiLang), - "hidevscroll": str(self.hideVScroll), - "hidehscroll": str(self.hideHScroll), + "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"] = { - "geometry": self._packList(self._winGeometry), - "preferences": self._packList(self._prefGeometry), - "projcols": self._packList(self._projColWidth), - "mainpane": self._packList(self._mainPanePos), - "viewpane": self._packList(self._viewPanePos), - "outlinepane": self._packList(self._outlnPanePos), - "fullscreen": str(self.isFullScreen), + "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"] = { @@ -706,13 +706,8 @@ class Config: "useridletime": str(self.userIdleTime), } - theConf["Backup"] = { - "backuppath": str(self._backupPath or ""), - "backuponclose": str(self.backupOnClose), - "askbeforebackup": str(self.askBeforeBackup), - } - theConf["State"] = { + "fullscreen": str(self.isFullScreen), "showrefpanel": str(self.showRefPanel), "viewcomments": str(self.viewComments), "viewsynopsis": str(self.viewSynopsis), @@ -724,16 +719,11 @@ class Config: "searchmatchcap": str(self.searchMatchCap), } - theConf["Path"] = { - "lastpath": str(self._lastPath), - } - # Write config file cnfPath = self._confPath / nwFiles.CONF_FILE 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() @@ -744,61 +734,6 @@ class Config: return True - ## - # Setters - ## - - def setShowRefPanel(self, checkState): - """Set the visibility state of the reference panel. - """ - self.showRefPanel = checkState - self._confChanged = True - return - - def setViewComments(self, viewState): - """Set the visibility state of comments in the viewer. - """ - self.viewComments = viewState - self._confChanged = True - return - - def setViewSynopsis(self, viewState): - """Set the visibility state of synopsis comments in the viewer. - """ - self.viewSynopsis = viewState - self._confChanged = True - return - - ## - # 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" - - ## - # Getters - ## - - 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)) - ## # Internal Functions ## diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 5fe3626a..568ee265 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -125,6 +125,7 @@ class GuiPreferences(PagedDialog): self.tabQuote.saveValues() self._saveWindowSize() + self.mainConf.saveConfig() self.accept() return @@ -170,18 +171,18 @@ class GuiPreferencesGeneral(QWidget): minWidth = self.mainConf.pxInt(200) # Select Locale - self.guiLang = QComboBox() - self.guiLang.setMinimumWidth(minWidth) + self.guiLocale = QComboBox() + self.guiLocale.setMinimumWidth(minWidth) theLangs = self.mainConf.listLanguages(self.mainConf.LANG_NW) for lang, langName in theLangs: - self.guiLang.addItem(langName, lang) - langIdx = self.guiLang.findData(self.mainConf.guiLang) + self.guiLocale.addItem(langName, lang) + langIdx = self.guiLocale.findData(self.mainConf.guiLocale) if langIdx != -1: - self.guiLang.setCurrentIndex(langIdx) + self.guiLocale.setCurrentIndex(langIdx) self.mainForm.addRow( self.tr("Main GUI language"), - self.guiLang, + self.guiLocale, self.tr("Requires restart to take effect.") ) @@ -286,7 +287,7 @@ class GuiPreferencesGeneral(QWidget): def saveValues(self): """Save the values set for this tab. """ - guiLang = self.guiLang.currentData() + guiLocale = self.guiLocale.currentData() guiTheme = self.guiTheme.currentData() guiSyntax = self.guiSyntax.currentData() guiFont = self.guiFont.text() @@ -296,12 +297,12 @@ class GuiPreferencesGeneral(QWidget): # Update Flags self.prefsGui._updateTheme |= self.mainConf.guiTheme != guiTheme self.prefsGui._updateSyntax |= self.mainConf.guiSyntax != guiSyntax - self.prefsGui._needsRestart |= self.mainConf.guiLang != guiLang + self.prefsGui._needsRestart |= self.mainConf.guiLocale != guiLocale self.prefsGui._needsRestart |= self.mainConf.guiFont != guiFont self.prefsGui._needsRestart |= self.mainConf.guiFontSize != guiFontSize self.prefsGui._refreshTree |= self.mainConf.emphLabels != emphLabels - self.mainConf.guiLang = guiLang + self.mainConf.guiLocale = guiLocale self.mainConf.guiTheme = guiTheme self.mainConf.guiSyntax = guiSyntax self.mainConf.guiFont = guiFont @@ -311,8 +312,6 @@ class GuiPreferencesGeneral(QWidget): self.mainConf.hideVScroll = self.hideVScroll.isChecked() self.mainConf.hideHScroll = self.hideHScroll.isChecked() - self.mainConf.setConfigChanged(True) - return ## @@ -458,8 +457,6 @@ class GuiPreferencesProjects(QWidget): self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked() self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60) - self.mainConf.setConfigChanged(True) - return ## @@ -629,8 +626,6 @@ class GuiPreferencesDocuments(QWidget): self.mainConf.textMargin = self.textMargin.value() self.mainConf.tabWidth = self.tabWidth.value() - self.mainConf.setConfigChanged(True) - return ## @@ -820,8 +815,6 @@ class GuiPreferencesEditor(QWidget): self.mainConf.autoScroll = self.autoScroll.isChecked() self.mainConf.autoScrollPos = self.autoScrollPos.value() - self.mainConf.setConfigChanged(True) - return # END Class GuiPreferencesEditor @@ -911,8 +904,6 @@ class GuiPreferencesSyntax(QWidget): # Text Errors self.mainConf.showMultiSpaces = self.showMultiSpaces.isChecked() - self.mainConf.setConfigChanged(True) - return ## @@ -1065,8 +1056,6 @@ class GuiPreferencesAutomation(QWidget): self.mainConf.fmtPadAfter = self.fmtPadAfter.text().strip() self.mainConf.fmtPadThin = self.fmtPadThin.isChecked() - self.mainConf.setConfigChanged(True) - return ## @@ -1185,9 +1174,6 @@ class GuiPreferencesQuotes(QWidget): self.mainConf.fmtSingleQuotes[1] = self.quoteSym["SC"].text() self.mainConf.fmtDoubleQuotes[0] = self.quoteSym["DO"].text() self.mainConf.fmtDoubleQuotes[1] = self.quoteSym["DC"].text() - - self.mainConf.setConfigChanged(True) - return ## diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 808749df..e498a100 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -1148,7 +1148,7 @@ class GuiDocViewFooter(QWidget): def _doToggleComments(self, theState): """Toggle the view comment button and reload the document. """ - self.mainConf.setViewComments(theState) + self.mainConf.viewComments = theState self.docViewer.reloadText() return @@ -1156,7 +1156,7 @@ class GuiDocViewFooter(QWidget): def _doToggleSynopsis(self, theState): """Toggle the view synopsis button and reload the document. """ - self.mainConf.setViewSynopsis(theState) + self.mainConf.viewSynopsis = theState self.docViewer.reloadText() return diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 2d69806c..cf10f78a 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -185,9 +185,14 @@ class GuiTheme: """Load the currently specified GUI theme. """ guiTheme = self.mainConf.guiTheme + if guiTheme not in self._availThemes: + logger.error("Could not find GUI theme '%s'", guiTheme) + guiTheme = "default" + self.mainConf.guiTheme = guiTheme + themeFile = self._availThemes.get(guiTheme, None) if themeFile is None: - logger.error("Could not find GUI theme '%s'", guiTheme) + logger.error("Could not load GUI theme") return False # Config File @@ -266,9 +271,14 @@ class GuiTheme: """Load the currently specified syntax highlighter theme. """ guiSyntax = self.mainConf.guiSyntax + if guiSyntax not in self._availSyntax: + logger.error("Could not find syntax theme '%s'", guiSyntax) + guiSyntax = "default_light" + self.mainConf.guiSyntax = guiSyntax + syntaxFile = self._availSyntax.get(guiSyntax, None) if syntaxFile is None: - logger.error("Could not find syntax theme '%s'", guiSyntax) + logger.error("Could not load syntax theme") return False logger.info("Loading syntax theme '%s'", guiSyntax) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8c0e1cfb..6e96fdaf 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -79,7 +79,7 @@ class GuiMain(QMainWindow): logger.info("Qt5: %s (%d)", self.mainConf.verQtString, self.mainConf.verQtValue) logger.info("PyQt5: %s (%d)", self.mainConf.verPyQtString, self.mainConf.verPyQtValue) logger.info("Python: %s (0x%x)", self.mainConf.verPyString, self.mainConf.verPyHexVal) - logger.info("GUI Language: %s", self.mainConf.guiLang) + logger.info("GUI Language: %s", self.mainConf.guiLocale) # Core Classes # ============ @@ -290,11 +290,6 @@ class GuiMain(QMainWindow): "and make sure you take regular backups." ), nwAlert.WARN) - # If a project path was provided at command line, open it - if self.mainConf.cmdOpen is not None: - logger.debug("Opening project from additional command line option") - self.openProject(self.mainConf.cmdOpen) - logger.info("novelWriter is ready ...") self.setStatus(self.tr("novelWriter is ready ...")) @@ -327,13 +322,22 @@ class GuiMain(QMainWindow): self.asDocTimer.setInterval(int(self.mainConf.autoSaveDoc*1000)) return True - def releaseNotes(self): - """Determine whether release notes need to be shown, and show - them by calling the About dialog. + def postLaunchTasks(self, cmdOpen): + """This function is called after the main window is created to + determine what to open or show after initialisation. """ + if cmdOpen: + logger.info("Command line path: %s", cmdOpen) + self.openProject(cmdOpen) + + if not self.hasProject: + self.showProjectLoadDialog() + + # Determine whether release notes need to be shown or not if hexToInt(self.mainConf.lastNotes) < hexToInt(novelwriter.__hexversion__): self.mainConf.lastNotes = novelwriter.__hexversion__ self.showAboutNWDialog(showNotes=True) + return ## @@ -1173,7 +1177,7 @@ class GuiMain(QMainWindow): if self.viewMeta.isVisible(): self.mainConf.setViewPanePos(self.splitView.sizes()) - self.mainConf.setShowRefPanel(self.viewMeta.isVisible()) + self.mainConf.showRefPanel = self.viewMeta.isVisible() if not self.mainConf.isFullScreen: self.mainConf.setMainWinSize(self.width(), self.height()) diff --git a/tests/conftest.py b/tests/conftest.py index 28034a18..39cc96cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -113,7 +113,7 @@ def tmpConf(tmpPath): theConf = Config() theConf.initConfig(tmpPath, tmpPath) theConf.setLastPath(tmpPath) - theConf.guiLang = "en_GB" + theConf.guiLocale = "en_GB" return theConf @@ -127,7 +127,7 @@ def fncConf(fncPath): theConf = Config() theConf.initConfig(fncPath, fncPath) theConf.setLastPath(fncPath) - theConf.guiLang = "en_GB" + theConf.guiLocale = "en_GB" return theConf diff --git a/tests/mock.py b/tests/mock.py index 74623dd1..e70034bd 100644 --- a/tests/mock.py +++ b/tests/mock.py @@ -35,6 +35,7 @@ class MockGuiMain(QObject): self.hasProject = True self.theProject = None self.mainStatus = MockStatusBar() + self.projPath = "" # Test Variables self.askResponse = True @@ -43,7 +44,7 @@ class MockGuiMain(QObject): return - def releaseNotes(self): + def postLaunchTasks(self, cmdOpen): return def makeAlert(self, message, level=0, exception=None): @@ -61,6 +62,7 @@ class MockGuiMain(QObject): return def openProject(self, projPath): + self.projPath = projPath return def rebuildIndex(self): diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index 8c38cc09..ed621ba2 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -1,27 +1,32 @@ +[Meta] +timestamp = 2022-11-10 11:10:10 + [Main] -timestamp = 2022-10-26 11:19:49 theme = default syntax = default_light -guifont = -guifontsize = 11 -lastnotes = 0x0 -guilang = en_GB +font = +fontsize = 11 +localisation = en_GB hidevscroll = False hidehscroll = False +lastnotes = 0x0 +lastpath = /home/vkbo [Sizes] -geometry = 1200, 650 +mainwindow = 1200, 650 preferences = 700, 615 -projcols = 200, 60, 140 +projloadcols = 280, 60, 160 mainpane = 300, 800 viewpane = 500, 150 outlinepane = 500, 150 -fullscreen = False [Project] autosaveproject = 60 autosavedoc = 30 emphlabels = True +backuppath = +backuponclose = False +askbeforebackup = True [Editor] textfont = None @@ -61,12 +66,8 @@ highlightemph = True stopwhenidle = True useridletime = 300 -[Backup] -backuppath = -backuponclose = False -askbeforebackup = True - [State] +fullscreen = False showrefpanel = True viewcomments = True viewsynopsis = True @@ -77,6 +78,3 @@ searchloop = False searchnextfile = False searchmatchcap = False -[Path] -lastpath = - diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf index a97fa527..47c5579d 100644 --- a/tests/reference/guiPreferences_novelwriter.conf +++ b/tests/reference/guiPreferences_novelwriter.conf @@ -1,27 +1,32 @@ +[Meta] +timestamp = 2022-11-10 11:10:13 + [Main] -timestamp = 2022-10-26 11:19:51 theme = default syntax = default_light -guifont = Cantarell -guifontsize = 12 -lastnotes = 0x0 -guilang = en_GB +font = Cantarell +fontsize = 12 +localisation = en_GB hidevscroll = True hidehscroll = True +lastnotes = 0x0 +lastpath = /home/vkbo/Code/novelWriter/Source/tests/temp/function [Sizes] -geometry = 1200, 650 +mainwindow = 1200, 650 preferences = 699, 614 -projcols = 200, 60, 140 +projloadcols = 280, 60, 160 mainpane = 300, 800 viewpane = 500, 150 outlinepane = 500, 150 -fullscreen = False [Project] autosaveproject = 40 autosavedoc = 20 emphlabels = True +backuppath = some/dir +backuponclose = True +askbeforebackup = True [Editor] textfont = None @@ -61,12 +66,8 @@ highlightemph = False stopwhenidle = True useridletime = 300 -[Backup] -backuppath = some/dir -backuponclose = True -askbeforebackup = True - [State] +fullscreen = False showrefpanel = True viewcomments = True viewsynopsis = True @@ -77,6 +78,3 @@ searchloop = False searchnextfile = False searchmatchcap = False -[Path] -lastpath = - diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 8fb150cc..a11095c3 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -166,7 +166,7 @@ def testBaseConfig_Localisation(fncPath, tstPaths): i18nDir = fncPath / "i18n" i18nDir.mkdir() - tstConf._nwLangPath = i18nDir + tstConf._nwLangPath = str(i18nDir) copyfile(tstPaths.filesDir / "nw_en_GB.qm", i18nDir / "nw_en_GB.qm") writeFile(i18nDir / "nw_en_GB.ts", "") @@ -254,17 +254,17 @@ def testBaseConfig_SettersGetters(tmpConf): # Window Size tmpConf.guiScale = 1.0 tmpConf.setMainWinSize(1205, 655) - assert tmpConf._confChanged is False + assert tmpConf.mainWinSize == [1200, 650] tmpConf.guiScale = 2.0 tmpConf.setMainWinSize(70, 70) assert tmpConf.mainWinSize == [70, 70] - assert tmpConf._winGeometry == [35, 35] + assert tmpConf._mainWinSize == [35, 35] tmpConf.guiScale = 1.0 tmpConf.setMainWinSize(70, 70) assert tmpConf.mainWinSize == [70, 70] - assert tmpConf._winGeometry == [70, 70] + assert tmpConf._mainWinSize == [70, 70] tmpConf.setMainWinSize(1200, 650) @@ -272,12 +272,12 @@ def testBaseConfig_SettersGetters(tmpConf): tmpConf.guiScale = 2.0 tmpConf.setPreferencesWinSize(70, 70) assert tmpConf.preferencesWinSize == [70, 70] - assert tmpConf._prefGeometry == [35, 35] + assert tmpConf._prefsWinSize == [35, 35] tmpConf.guiScale = 1.0 tmpConf.setPreferencesWinSize(70, 70) assert tmpConf.preferencesWinSize == [70, 70] - assert tmpConf._prefGeometry == [70, 70] + assert tmpConf._prefsWinSize == [70, 70] tmpConf.setPreferencesWinSize(700, 615) @@ -285,12 +285,12 @@ def testBaseConfig_SettersGetters(tmpConf): tmpConf.guiScale = 2.0 tmpConf.setProjLoadColWidths([10, 20, 30]) assert tmpConf.projLoadColWidths == [10, 20, 30] - assert tmpConf._projColWidth == [5, 10, 15] + assert tmpConf._projLoadCols == [5, 10, 15] tmpConf.guiScale = 1.0 tmpConf.setProjLoadColWidths([10, 20, 30]) assert tmpConf.projLoadColWidths == [10, 20, 30] - assert tmpConf._projColWidth == [10, 20, 30] + assert tmpConf._projLoadCols == [10, 20, 30] tmpConf.setProjLoadColWidths([200, 60, 140]) @@ -348,24 +348,6 @@ def testBaseConfig_SettersGetters(tmpConf): assert tmpConf.getTextMargin() == 80 assert tmpConf.getTabWidth() == 80 - # Flag Setters - # ============ - - tmpConf.setShowRefPanel(False) - assert tmpConf.showRefPanel is False - tmpConf.setShowRefPanel(True) - assert tmpConf.showRefPanel is True - - tmpConf.setViewComments(False) - assert tmpConf.viewComments is False - tmpConf.setViewComments(True) - assert tmpConf.viewComments is True - - tmpConf.setViewSynopsis(False) - assert tmpConf.viewSynopsis is False - tmpConf.setViewSynopsis(True) - assert tmpConf.viewSynopsis is True - # END Test testBaseConfig_SettersGetters diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py index 2a41ea38..2cc4d255 100644 --- a/tests/test_base/test_base_init.py +++ b/tests/test_base/test_base_init.py @@ -138,7 +138,6 @@ def testBaseInit_Options(monkeypatch, tmpPath): nwGUI = novelwriter.main( ["--testmode", f"--config={tmpPath}", f"--data={tmpPath}", "sample/"] ) - assert novelwriter.CONFIG.cmdOpen == "sample/" assert nwGUI.closeMain() == "closeMain" # END Test testBaseInit_Options diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index ec83b428..b36cc02e 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -215,8 +215,6 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): qtbot.mouseClick(nwPrefs.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) nwPrefs._doClose() - assert theConf._confChanged is True - assert nwGUI.mainConf.saveConfig() projFile = fncPath / "novelwriter.conf" testFile = tstPaths.outDir / "guiPreferences_novelwriter.conf" diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 8c5670a8..2a6daf33 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -21,15 +21,18 @@ along with this program. If not, see . import pytest -from tools import C, cmpFiles, buildTestProject, XML_IGNORE, writeFile from shutil import copyfile +from tools import ( + C, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem, writeFile +) + from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox, QInputDialog +from PyQt5.QtWidgets import QDialog, QMessageBox, QInputDialog from novelwriter.enum import nwItemType, nwView, nwWidget from novelwriter.tools import GuiProjectWizard -from novelwriter.dialogs import GuiEditLabel +from novelwriter.dialogs import GuiEditLabel, GuiAbout, GuiProjectLoad from novelwriter.constants import nwFiles from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectTree @@ -62,7 +65,39 @@ def testGuiMain_ProjectBlocker(nwGUI): assert nwGUI.showProjectWordListDialog() is False assert nwGUI.showWritingStatsDialog() is False -# END Test testGuiMain_NoProject +# END Test testGuiMain_ProjectBlocker + + +@pytest.mark.gui +def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, prjLipsum): + """Test the handling of launch tasks. + """ + monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *a: None) + monkeypatch.setattr(GuiProjectLoad, "result", lambda *a: QDialog.Accepted) + nwGUI.mainConf.lastNotes = "0x0" + + # Open Lipsum project + nwGUI.postLaunchTasks(prjLipsum) + nwGUI.closeProject() + + # Check that release notes opened + qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000) + msgAbout = getGuiItem("GuiAbout") + assert isinstance(msgAbout, GuiAbout) + assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes + msgAbout.accept() + + # Check that project open dialog launches + nwGUI.postLaunchTasks(None) + qtbot.waitUntil(lambda: getGuiItem("GuiProjectLoad") is not None, timeout=1000) + nwLoad = getGuiItem("GuiProjectLoad") + assert isinstance(nwLoad, GuiProjectLoad) + nwLoad.show() + nwLoad.reject() + + # qtbot.stop() + +# END Test testGuiMain_Launch @pytest.mark.gui diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 42f5c7c5..5ec03c30 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -150,7 +150,10 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncPath): # Check handling of broken theme settings mainConf.guiTheme = "not_a_theme" + availThemes = mainTheme._availThemes + mainTheme._availThemes = {} assert mainTheme.loadTheme() is False + mainTheme._availThemes = availThemes # Check handling of unreadable file mainConf.guiTheme = "default" @@ -216,8 +219,11 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncPath): assert mainTheme.listSyntax() == mainTheme._syntaxList # Check handling of broken theme settings + availSyntax = mainTheme._availSyntax + mainTheme._availSyntax = {} mainConf.guiSyntax = "not_a_syntax" assert mainTheme.loadSyntax() is False + mainTheme._availSyntax = availSyntax # Check handling of unreadable file mainConf.guiSyntax = "default_light"