diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index da03b291..cb0e4bab 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -244,7 +244,7 @@ class GuiTheme: self.statSaved = self._parseColour(confParser, cnfSec, "statussaved") # Icons - self.iconCache.updateTheme(self.themeIcons) + self.iconCache.loadTheme(self.themeIcons) # CSS File cssData = readTextFile(themeFile[:-5]+".qss") @@ -498,7 +498,7 @@ class GuiIcons: # Actions ## - def updateTheme(self, iconTheme): + def loadTheme(self, iconTheme): """Update the theme map. This is more of an init, since many of the GUI icons cannot really be replaced without writing specific update functions for the classes where they're used. @@ -585,7 +585,7 @@ class GuiIcons: return QPixmap() if not os.path.isfile(imgPath): - logger.error("Asset '%s' not found", self.IMAGE_MAP[decoKey]) + logger.error("Asset not found: %s", imgPath) return QPixmap() theDeco = QPixmap(imgPath) @@ -598,7 +598,7 @@ class GuiIcons: return theDeco - def getIcon(self, iconKey, iconSize=None): + def getIcon(self, iconKey): """Return an icon from the icon buffer. If it doesn't exist, return, load it, and if it still doesn't exist, return an empty icon. diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 3c6503cb..cd16ff86 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -26,13 +26,15 @@ import pytest from configparser import ConfigParser from mock import causeOSError +from novelwriter.constants import nwLabels +from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from tools import writeFile -from PyQt5.QtGui import QPalette +from PyQt5.QtGui import QIcon, QPalette, QPixmap from PyQt5.QtWidgets import QApplication from novelwriter.config import Config -from novelwriter.gui.theme import GuiTheme +from novelwriter.gui.theme import GuiIcons, GuiTheme @pytest.mark.gui @@ -121,8 +123,8 @@ def testGuiTheme_Main(qtbot, nwGUI, fncDir): @pytest.mark.gui -def testGuiTheme_Themes(qtbot, monkeypatch, nwGUI, fncDir): - """Test the theme class init. +def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncDir): + """Test the theme part of the class. """ mainTheme: GuiTheme = nwGUI.mainTheme mainConf: Config = nwGUI.mainConf @@ -140,6 +142,11 @@ def testGuiTheme_Themes(qtbot, monkeypatch, nwGUI, fncDir): ) writeFile(os.path.join(fncDir, "themes", "default.qss"), "/* Stuff */") + # Block the reading of the files + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert mainTheme.listThemes() == [] + # Load the theme info themesList = mainTheme.listThemes() assert themesList[0] == ("default_dark", "Default Dark Theme") @@ -186,4 +193,254 @@ def testGuiTheme_Themes(qtbot, monkeypatch, nwGUI, fncDir): # qtbot.stop() -# END Test testGuiTheme_Themes +# END Test testGuiTheme_Theme + + +@pytest.mark.gui +def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncDir): + """Test the syntax part of the class. + """ + mainTheme: GuiTheme = nwGUI.mainTheme + mainConf: Config = nwGUI.mainConf + + # List Themes + # =========== + + shutil.copy( + os.path.join(mainConf.assetPath, "syntax", "default_dark.conf"), + os.path.join(fncDir, "syntax") + ) + shutil.copy( + os.path.join(mainConf.assetPath, "syntax", "default_light.conf"), + os.path.join(fncDir, "syntax") + ) + + # Block the reading of the files + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert mainTheme.listThemes() == [] + + # Load the syntax info + syntaxList = mainTheme.listSyntax() + assert syntaxList[0] == ("default_dark", "Default Dark") + assert syntaxList[1] == ("default_light", "Default Light") + + # A second call should returned the cached list + assert mainTheme.listSyntax() == mainTheme._syntaxList + + # Check handling of broken theme settings + mainConf.guiSyntax = "not_a_syntax" + assert mainTheme.loadSyntax() is False + + # Check handling of unreadable file + mainConf.guiSyntax = "default_light" + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert mainTheme.loadSyntax() is False + + # Load Default Light Syntax + # ========================= + + # Load the default syntax + mainConf.guiSyntax = "default_light" + assert mainTheme.loadSyntax() is True + + # Check some values + assert mainTheme.syntaxName == "Default Light" + assert mainTheme.colBack == [255, 255, 255] + assert mainTheme.colText == [0, 0, 0] + assert mainTheme.colLink == [0, 0, 200] + + # Load Default Dark Theme + # ======================= + + # Load the default syntax + mainConf.guiSyntax = "default_dark" + assert mainTheme.loadSyntax() is True + + # Check some values + assert mainTheme.syntaxName == "Default Dark" + assert mainTheme.colBack == [54, 54, 54] + assert mainTheme.colText == [199, 207, 208] + assert mainTheme.colLink == [184, 200, 0] + + # qtbot.stop() + +# END Test testGuiTheme_Syntax + + +@pytest.mark.gui +def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncDir): + """Test the icon cache class. + """ + iconCache: GuiIcons = nwGUI.mainTheme.iconCache + mainConf: Config = nwGUI.mainConf + + # Load Theme + # ========== + + # Invalid theme name + assert iconCache.loadTheme("not_a_theme") is False + + # Check handling of unreadable file + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert iconCache.loadTheme("typicons_dark") is False + + # Load a broken theme file + iconsDir = os.path.join(fncDir, "icons") + os.mkdir(iconsDir) + os.mkdir(os.path.join(iconsDir, "testicons")) + writeFile(os.path.join(iconsDir, "testicons", "icons.conf"), ( + "[Main]\n" + "name = Test Icons\n" + "\n" + "[Map]\n" + "add = add.svg\n" + "stuff = stuff.svg\n" + )) + + assetPath = mainConf.assetPath + mainConf.assetPath = fncDir + + caplog.clear() + assert iconCache.loadTheme("testicons") is True + assert "Unknown icon name 'stuff' in config file" in caplog.text + assert "Icon file 'add.svg' not in theme folder" in caplog.text + + mainConf.assetPath = assetPath + + # Load working theme file + assert iconCache.loadTheme("typicons_dark") is True + assert "add" in iconCache._themeMap + + # Load Decorations + # ================ + + # Invalid name should return empty pixmap + qPix = iconCache.loadDecoration("stuff") + assert qPix.isNull() is True + + # Load an image + qPix = iconCache.loadDecoration("wiz-back") + assert qPix.isNull() is False + + # Fail finding the file + with monkeypatch.context() as mp: + mp.setattr("os.path.isfile", lambda *a: False) + qPix = iconCache.loadDecoration("wiz-back") + assert qPix.isNull() is True + + # Test image sizes + qPix = iconCache.loadDecoration("wiz-back", pxW=100, pxH=None) + assert qPix.isNull() is False + assert qPix.width() == 100 + assert qPix.height() > 100 + + qPix = iconCache.loadDecoration("wiz-back", pxW=None, pxH=100) + assert qPix.isNull() is False + assert qPix.width() < 100 + assert qPix.height() == 100 + + qPix = iconCache.loadDecoration("wiz-back", pxW=100, pxH=100) + assert qPix.isNull() is False + assert qPix.width() == 100 + assert qPix.height() == 100 + + # Load Icons + # ========== + + # Load an unknown icon + qIcon = iconCache.getIcon("stuff") + assert isinstance(qIcon, QIcon) + assert qIcon.isNull() is True + + # Load an icon, it is likelyu already cached + qIcon = iconCache.getIcon("add") + assert isinstance(qIcon, QIcon) + assert qIcon.isNull() is False + + # Load it as a pixmap with a size + qPix = iconCache.getPixmap("add", (50, 50)) + assert isinstance(qPix, QPixmap) + assert qPix.isNull() is False + assert qPix.width() == 50 + assert qPix.height() == 50 + + # Load app icon + qIcon = iconCache.getIcon("novelwriter") + assert isinstance(qIcon, QIcon) + assert qIcon.isNull() is False + + # Load mime icon + qIcon = iconCache.getIcon("proj_nwx") + assert isinstance(qIcon, QIcon) + assert qIcon.isNull() is False + + # Load Item Icons + # =============== + + # Root -> Not Null + assert iconCache.getItemIcon( + nwItemType.ROOT, nwItemClass.NOVEL, nwItemLayout.NO_LAYOUT, hLevel="H0" + ) == iconCache.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) + + # Folder -> Not Null + assert iconCache.getItemIcon( + nwItemType.FOLDER, nwItemClass.NOVEL, nwItemLayout.NO_LAYOUT, hLevel="H0" + ) == iconCache.getIcon("proj_folder") + + # Document H0 -> Not Null + assert iconCache.getItemIcon( + nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.NO_LAYOUT, hLevel="H0" + ) == iconCache.getIcon("proj_document") + + # Document H1 -> Not Null + assert iconCache.getItemIcon( + nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H1" + ) == iconCache.getIcon("proj_title") + + # Document H2 -> Not Null + assert iconCache.getItemIcon( + nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H2" + ) == iconCache.getIcon("proj_chapter") + + # Document H3 -> Not Null + assert iconCache.getItemIcon( + nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H3" + ) == iconCache.getIcon("proj_scene") + + # Document H4 -> Not Null + assert iconCache.getItemIcon( + nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H4" + ) == iconCache.getIcon("proj_section") + + # Document H5 -> Not Null + assert iconCache.getItemIcon( + nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.NO_LAYOUT, hLevel="H4" + ) == iconCache.getIcon("proj_document") + + # Note -> Not Null + assert iconCache.getItemIcon( + nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.NOTE, hLevel="H5" + ) == iconCache.getIcon("proj_note") + + # No Type -> Null + assert iconCache.getItemIcon( + nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0" + ).isNull() is True + + # Header Decorations + # ================== + + assert iconCache.getHeaderDecoration(-1) == iconCache._headerDec[0] + assert iconCache.getHeaderDecoration(0) == iconCache._headerDec[0] + assert iconCache.getHeaderDecoration(1) == iconCache._headerDec[1] + assert iconCache.getHeaderDecoration(2) == iconCache._headerDec[2] + assert iconCache.getHeaderDecoration(3) == iconCache._headerDec[3] + assert iconCache.getHeaderDecoration(4) == iconCache._headerDec[4] + assert iconCache.getHeaderDecoration(5) == iconCache._headerDec[4] + + # qtbot.stop() + +# END Test testGuiTheme_Icons