From 0eb4319fc110263b9537aa625c59d372a189deff Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 14 May 2020 18:38:41 +0200 Subject: [PATCH] Finished the code for icon themes --- nw/assets/icons/fallback/status_lang-dark.svg | 57 +++++++++++++++ nw/assets/icons/fallback/status_lang.svg | 56 +++++++++++++++ nw/assets/icons/fallback/status_time-dark.svg | 62 ++++++++++++++++ nw/assets/icons/fallback/status_time.svg | 61 ++++++++++++++++ .../typicons_colour_dark/input-checked.svg | 70 +++++++++---------- nw/config.py | 6 +- nw/gui/dialogs/configeditor.py | 32 +++++++-- nw/gui/dialogs/itemeditor.py | 3 +- nw/gui/dialogs/projecteditor.py | 22 +++--- nw/gui/icons.py | 52 ++++++++++++-- nw/gui/theme.py | 18 +++-- tests/reference/novelwriter.conf | 1 + 12 files changed, 376 insertions(+), 64 deletions(-) create mode 100644 nw/assets/icons/fallback/status_lang-dark.svg create mode 100644 nw/assets/icons/fallback/status_lang.svg create mode 100644 nw/assets/icons/fallback/status_time-dark.svg create mode 100644 nw/assets/icons/fallback/status_time.svg diff --git a/nw/assets/icons/fallback/status_lang-dark.svg b/nw/assets/icons/fallback/status_lang-dark.svg new file mode 100644 index 00000000..8966c4c9 --- /dev/null +++ b/nw/assets/icons/fallback/status_lang-dark.svg @@ -0,0 +1,57 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/nw/assets/icons/fallback/status_lang.svg b/nw/assets/icons/fallback/status_lang.svg new file mode 100644 index 00000000..255ac776 --- /dev/null +++ b/nw/assets/icons/fallback/status_lang.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/nw/assets/icons/fallback/status_time-dark.svg b/nw/assets/icons/fallback/status_time-dark.svg new file mode 100644 index 00000000..7d489bae --- /dev/null +++ b/nw/assets/icons/fallback/status_time-dark.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/status_time.svg b/nw/assets/icons/fallback/status_time.svg new file mode 100644 index 00000000..129d58cf --- /dev/null +++ b/nw/assets/icons/fallback/status_time.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/nw/assets/icons/typicons_colour_dark/input-checked.svg b/nw/assets/icons/typicons_colour_dark/input-checked.svg index e4b22265..a4b66a4d 100644 --- a/nw/assets/icons/typicons_colour_dark/input-checked.svg +++ b/nw/assets/icons/typicons_colour_dark/input-checked.svg @@ -7,13 +7,13 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)" - sodipodi:docname="input-checked.svg" - id="svg11367" - viewBox="0 0 24 24" - height="24" + version="1.2" width="24" - version="1.2"> + height="24" + viewBox="0 0 24 24" + id="svg11367" + sodipodi:docname="input-checked.svg" + inkscape:version="1.0rc1 (09960d6f05, 2020-04-09)"> @@ -28,38 +28,38 @@ + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="2560" + inkscape:window-height="1344" + id="namedview11369" + showgrid="false" + inkscape:zoom="38.125" + inkscape:cx="7.2131148" + inkscape:cy="12" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg11367" /> - + + style="display:inline;fill:#aeaeae;fill-opacity:1;stroke-width:1.28570998" + inkscape:connector-curvature="0" + id="path16196" + d="M 17.142857,21 H 6.8571429 C 4.7305714,21 3,19.269429 3,17.142857 V 6.8571429 C 3,4.7305714 4.7305714,3 6.8571429,3 h 6.4285711 c 0.711,0 1.285715,0.576 1.285715,1.2857143 0,0.7097143 -0.574715,1.2857143 -1.285715,1.2857143 H 6.8571429 c -0.7097143,0 -1.2857143,0.5772857 -1.2857143,1.2857143 V 17.142857 c 0,0.708429 0.576,1.285714 1.2857143,1.285714 H 17.142857 c 0.709714,0 1.285714,-0.577285 1.285714,-1.285714 V 13.285714 C 18.428571,12.576 19.003286,12 19.714286,12 20.425286,12 21,12.576 21,13.285714 v 3.857143 C 21,19.269429 19.269429,21 17.142857,21 Z" /> diff --git a/nw/config.py b/nw/config.py index 04685fb6..985f4b8d 100644 --- a/nw/config.py +++ b/nw/config.py @@ -83,7 +83,7 @@ class Config: ## General self.guiTheme = "default" self.guiSyntax = "default_light" - self.guiIcons = "typicons_colour_dark" + self.guiIcons = "default" self.guiDark = False ## Sizes @@ -304,6 +304,9 @@ class Config: 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 ) @@ -452,6 +455,7 @@ class Config: 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)) ## Sizes diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/configeditor.py index cb60a40e..e03d7bad 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/configeditor.py @@ -58,7 +58,11 @@ class GuiConfigEditor(QDialog): self.innerBox = QVBoxLayout() self.setWindowTitle("Preferences") - self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64)) + self.guiDeco = self.theParent.theTheme.loadDecoration("settings", (64,64)) + self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) + self.outerBox.addLayout(self.innerBox) + self.outerBox.setSpacing(16) + self.setLayout(self.outerBox) self.tabGeneral = GuiConfigEditGeneralTab(self.theParent) self.tabLayout = GuiConfigEditLayoutTab(self.theParent) @@ -72,10 +76,6 @@ class GuiConfigEditor(QDialog): self.tabWidget.addTab(self.tabEditing, "Editing") self.tabWidget.addTab(self.tabAutoRep, "Auto-Replace") - self.setLayout(self.outerBox) - self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) - self.outerBox.addLayout(self.innerBox) - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.accepted.connect(self._doSave) self.buttonBox.rejected.connect(self._doClose) @@ -169,6 +169,22 @@ class GuiConfigEditGeneralTab(QWidget): "Changing this requires restarting %s." % nw.__package__ ) + ## Select Icon Theme + self.selectIcons = QComboBox() + self.selectIcons.setMinimumWidth(200) + self.theIcons = self.theTheme.theIcons.listThemes() + for iconDir, iconName in self.theIcons: + self.selectIcons.addItem(iconName, iconDir) + iconIdx = self.selectIcons.findData(self.mainConf.guiIcons) + if iconIdx != -1: + self.selectIcons.setCurrentIndex(iconIdx) + + self.mainForm.addRow( + "Main icon theme", + self.selectIcons, + "Changing this requires restarting %s." % nw.__package__ + ) + ## Syntax Highlighting self.selectSyntax = QComboBox() self.selectSyntax.setMinimumWidth(200) @@ -181,7 +197,8 @@ class GuiConfigEditGeneralTab(QWidget): self.mainForm.addRow( "Syntax highlight theme", - self.selectSyntax + self.selectSyntax, + "" ) ## Dark Icons @@ -275,6 +292,7 @@ class GuiConfigEditGeneralTab(QWidget): guiTheme = self.selectTheme.currentData() guiSyntax = self.selectSyntax.currentData() + guiIcons = self.selectIcons.currentData() guiDark = self.preferDarkIcons.isChecked() showFullPath = self.showFullPath.isChecked() autoSaveDoc = self.autoSaveDoc.value() @@ -285,9 +303,11 @@ class GuiConfigEditGeneralTab(QWidget): # Check if restart is needed needsRestart |= self.mainConf.guiTheme != guiTheme + needsRestart |= self.mainConf.guiIcons != guiIcons self.mainConf.guiTheme = guiTheme self.mainConf.guiSyntax = guiSyntax + self.mainConf.guiIcons = guiIcons self.mainConf.guiDark = guiDark self.mainConf.showFullPath = showFullPath self.mainConf.autoSaveDoc = autoSaveDoc diff --git a/nw/gui/dialogs/itemeditor.py b/nw/gui/dialogs/itemeditor.py index 0b24e9d6..38af48d6 100644 --- a/nw/gui/dialogs/itemeditor.py +++ b/nw/gui/dialogs/itemeditor.py @@ -60,11 +60,10 @@ class GuiItemEditor(QDialog): nwLabels.CLASS_ICON[self.theItem.itemClass], (64,64) ) ) - - self.setLayout(self.outerBox) self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) self.outerBox.addLayout(self.innerBox) self.outerBox.setSpacing(16) + self.setLayout(self.outerBox) self.mainGroup = QGroupBox("Item Settings") self.mainForm = QFormLayout() diff --git a/nw/gui/dialogs/projecteditor.py b/nw/gui/dialogs/projecteditor.py index afa5dbe5..fd256c3a 100644 --- a/nw/gui/dialogs/projecteditor.py +++ b/nw/gui/dialogs/projecteditor.py @@ -52,12 +52,18 @@ class GuiProjectEditor(QDialog): self.theParent = theParent self.theProject = theProject - self.outerBox = QHBoxLayout() - self.innerBox = QVBoxLayout() - self.setWindowTitle("Project Settings") - self.setLayout(self.outerBox) + self.outerBox = QHBoxLayout() + self.innerBox = QVBoxLayout() - self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64)) + self.setWindowTitle("Project Settings") + self.guiDeco = QLabel() + self.guiDeco.setPixmap( + self.theParent.theTheme.getPixmap("cls_novel", (64,64)) + ) + self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) + self.outerBox.addLayout(self.innerBox) + self.outerBox.setSpacing(16) + self.setLayout(self.outerBox) self.theProject.countStatus() self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) @@ -71,9 +77,6 @@ class GuiProjectEditor(QDialog): self.tabWidget.addTab(self.tabImport, "Importance") self.tabWidget.addTab(self.tabReplace,"Auto-Replace") - self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) - self.outerBox.addLayout(self.innerBox) - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.accepted.connect(self._doSave) self.buttonBox.rejected.connect(self._doClose) @@ -363,6 +366,9 @@ class GuiProjectEditReplace(QWidget): self.saveButton = QPushButton(self.theTheme.getIcon("done"),"") self.addButton = QPushButton(self.theTheme.getIcon("add"),"") self.delButton = QPushButton(self.theTheme.getIcon("remove"),"") + self.saveButton.setToolTip("Save entry") + self.addButton.setToolTip("Add new entry") + self.delButton.setToolTip("Delete selected entry") self.editKey.setEnabled(False) self.editKey.setMaxLength(40) diff --git a/nw/gui/icons.py b/nw/gui/icons.py index c45a9aaf..35bc8122 100644 --- a/nw/gui/icons.py +++ b/nw/gui/icons.py @@ -29,7 +29,7 @@ import logging import configparser import nw -from os import path +from os import path, listdir from PyQt5.QtCore import QSize from PyQt5.QtSvg import QSvgWidget @@ -90,8 +90,11 @@ class GuiIcons: self.theParent = theParent # Storage - self.qIcons = {} - self.themeMap = {} + self.qIcons = {} + self.themeMap = {} + self.themeList = [] + self.fbackName = "fallback" + self.confName = "icons.conf" # Icon Theme Path self.iconPath = None @@ -121,7 +124,7 @@ class GuiIcons: if path.isdir(checkPath): logger.debug("Loading icon theme '%s'" % self.mainConf.guiIcons) self.iconPath = checkPath - self.confFile = path.join(checkPath, "icons.conf") + self.confFile = path.join(checkPath, self.confName) else: return False @@ -170,7 +173,6 @@ class GuiIcons: """Load graphical decoration element based on the decoration map. This function always returns a QSwgWidget. """ - if decoKey not in self.DECO_MAP: logger.error("Decoration with name '%s' does not exist" % decoKey) return QSvgWidget() @@ -209,6 +211,37 @@ class GuiIcons: qIcon = self.getIcon(iconKey) return qIcon.pixmap(iconSize[0], iconSize[1], QIcon.Normal) + def listThemes(self): + """Scan the icons themes folder and list all themes. + """ + if self.themeList: + return self.themeList + + confParser = configparser.ConfigParser() + for themeDir in listdir(self.mainConf.iconPath): + themePath = path.join(self.mainConf.iconPath, themeDir) + if not path.isdir(themePath) or themeDir == self.fbackName: + continue + themeConf = path.join(themePath, self.confName) + logger.verbose("Checking icon theme config for '%s'" % themeDir) + try: + confParser.read_file(open(themeConf, mode="r", encoding="utf8")) + except Exception as e: + self.theParent.makeAlert(["Could not load theme config file.",str(e)],nwAlert.ERROR) + continue + themeName = "" + if confParser.has_section("Main"): + if confParser.has_option("Main", "name"): + themeName = confParser.get("Main", "name") + logger.verbose("Theme name is '%s'" % themeName) + if themeName != "": + self.themeList.append((themeDir, themeName)) + + self.themeList = sorted(self.themeList, key=lambda x: x[1]) + self.themeList.insert(0, ("default", "System Icons")) + + return self.themeList + ## # Internal Functions ## @@ -238,6 +271,15 @@ class GuiIcons: logger.verbose("Loading icon '%s' from system theme" % iconKey) return QIcon().fromTheme(self.ICON_MAP[iconKey][1]) + # Finally. we check if we have a fallback icon + if self.mainConf.guiDark: + fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey) + else: + fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey) + if path.isfile(fbackIcon): + logger.verbose("Loading icon '%s' from fallback theme" % iconKey) + return QIcon(fbackIcon) + # Give up and return an empty icon logger.warning("Did not load an icon for '%s'" % iconKey) diff --git a/nw/gui/theme.py b/nw/gui/theme.py index b2f11357..31e862e0 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -260,8 +260,9 @@ class GuiTheme: return True def listThemes(self): - - if len(self.themeList) > 0: + """Scan the gui themes folder and list all themes. + """ + if self.themeList: return self.themeList confParser = configparser.ConfigParser() @@ -275,8 +276,9 @@ class GuiTheme: continue themeName = "" if confParser.has_section("Main"): - themeName = confParser.get("Main","name") - logger.verbose("Theme name is '%s'" % themeName) + if confParser.has_option("Main", "name"): + themeName = confParser.get("Main", "name") + logger.verbose("Theme name is '%s'" % themeName) if themeName != "": self.themeList.append((themeDir, themeName)) @@ -285,8 +287,9 @@ class GuiTheme: return self.themeList def listSyntax(self): - - if len(self.syntaxList) > 0: + """Scan the syntax themes folder and list all themes. + """ + if self.syntaxList: return self.syntaxList confParser = configparser.ConfigParser() @@ -303,7 +306,8 @@ class GuiTheme: return [] syntaxName = "" if confParser.has_section("Main"): - syntaxName = confParser.get("Main","name") + if confParser.has_option("Main", "name"): + syntaxName = confParser.get("Main", "name") if len(syntaxFile) > 5 and syntaxName != "": self.syntaxList.append((syntaxFile[:-5], syntaxName)) logger.verbose("Syntax name is '%s'" % syntaxName) diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index 7102783d..13c3637a 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -2,6 +2,7 @@ timestamp = 2020-05-01 21:28:25 theme = default syntax = default_light +icons = default guidark = False [Sizes]