123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- ########################################################################
- # Searx-Qt - Lightweight desktop application for Searx.
- # Copyright (C) 2020-2022 CYBERDEViL
- #
- # This file is part of Searx-Qt.
- #
- # Searx-Qt is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # Searx-Qt is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
- #
- ########################################################################
- # Notes:
- # https://docs.python.org/3/library/json.html
- # https://docs.python.org/3/library/os.path.html
- # https://docs.python.org/3/library/sysconfig.html
- from PyQt5.QtWidgets import QApplication, QStyleFactory
- from PyQt5.QtCore import QResource
- import os
- import sysconfig # Get path prefix (example: /usr).
- import json # Theme manifest file is json.
- from searxqt import THEMES_PATH
- from searxqt.core.log import error, debug, warning
- # Paths
- USER_THEMES_PATH = os.path.join(
- sysconfig.get_config_var('userbase'),
- 'share/',
- THEMES_PATH
- )
- SYS_THEMES_PATH = os.path.join(
- sysconfig.get_config_var('prefix'),
- 'share/',
- THEMES_PATH
- )
- def replaceFileExt(filePath, ext):
- return os.path.splitext(filePath)[0] + ext
- class UserTheme:
- def __init__(
- self,
- key,
- name,
- cssFile,
- path,
- icons=None,
- resultsCssFile=None,
- failCssFile=None
- ):
- self.__key = key
- self.__name = name
- self.__path = path
- self.__icons = icons
- self.__cssFile = cssFile
- self.__resultsCssFile = resultsCssFile
- self.__failCssFile = failCssFile
- @property
- def key(self):
- """ This is also the directory name of the theme.
- """
- return self.__key
- @property
- def name(self):
- return self.__name
- @property
- def cssFile(self):
- return self.__cssFile
- @property
- def path(self):
- return self.__path
- @property
- def icons(self):
- return self.__icons
- @property
- def resultsCssFile(self):
- return self.__resultsCssFile
- @property
- def failCssFile(self):
- return self.__failCssFile
- def fullCssPath(self):
- return os.path.join(self.path, self.cssFile)
- def iconsPath(self, compiled=True):
- """
- @parem compiled: When set to True it will return path to .rcc else .qrc
- """
- if self.icons is not None:
- iconFile = self.icons
- if compiled:
- iconFile = replaceFileExt(iconFile, '.rcc')
- return os.path.join(self.path, iconFile)
- return ""
- def resultsCssPath(self):
- if self.resultsCssFile is not None:
- return os.path.join(self.path, self.resultsCssFile)
- return ""
- def failCssPath(self):
- if self.failCssFile is not None:
- return os.path.join(self.path, self.failCssFile)
- return ""
- class ThemesBase:
- def __init__(self):
- self.__currentTheme = ""
- self.__currentStyle = ""
- self.__themes = []
- self.__styles = []
- self.__resultsCss = ""
- self.__failCss = ""
- self.__loadedIcons = False
- """ properties
- """
- @property
- def currentTheme(self):
- return self.__currentTheme
- @property
- def currentStyle(self):
- return self.__currentStyle
- @property
- def themes(self):
- """ The available themes specific for searx-qt
- """
- return self.__themes
- @property
- def styles(self):
- """ The available system styles
- """
- return self.__styles
- @property
- def htmlCssResults(self):
- """ The css that will be includes in the results page (html).
- """
- return self.__resultsCss
- @property
- def htmlCssFail(self):
- """ The css that will be includes in the fail result page (html).
- """
- return self.__failCss
- """ class methods
- """
- def getTheme(self, key):
- for theme in self.themes:
- if theme.key == key:
- return theme
- return None
- def setStyle(self, name):
- debug(f"setSystemStyle {name}", self)
- qApp = QApplication.instance()
- if name not in self.styles:
- return
- qApp.setStyle(name)
- self.__currentStyle = name
- def setTheme(self, key):
- debug(f"setTheme {key}", self)
- qApp = QApplication.instance()
- # Unload old theme
- if self.currentTheme:
- # unload icons
- if self.__loadedIcons:
- self.__loadedIcons = False
- iconsPath = self.getTheme(self.currentTheme).iconsPath()
- if not QResource.unregisterResource(iconsPath):
- warning(f"Failed to unregister resource {iconsPath}",
- self)
- # reset values
- self.__resultsCss = ""
- self.__failCss = ""
- self.__loadedIcons = False
- self.__currentTheme = ""
- qApp.setStyleSheet("")
- if key not in [theme.key for theme in self.themes]:
- warning(f"Theme with key `{key}` requested but not found!", self)
- return
- if not key:
- # Do not set new theme.
- self.__currentTheme = ""
- return
- newTheme = self.getTheme(key)
- # Load icons
- if newTheme.icons:
- iconsPath = newTheme.iconsPath()
- if QResource.registerResource(iconsPath):
- self.__loadedIcons = True
- else:
- warning(f"Failed to register resource {iconsPath}", self)
- # Results CSS
- if newTheme.resultsCssFile:
- self.__resultsCss = ThemesBase.readCss(newTheme.resultsCssPath())
- # Fail CSS
- if newTheme.failCssFile:
- self.__failCss = ThemesBase.readCss(newTheme.failCssPath())
- # Load and apply the stylesheet
- cssFilePath = newTheme.fullCssPath()
- qApp.setStyleSheet(ThemesBase.readCss(cssFilePath))
- self.__currentTheme = key
- def serialize(self):
- return {
- "theme": self.currentTheme,
- "style": self.currentStyle
- }
- def deserialize(self, data):
- theme = data.get("theme", "")
- style = data.get("style", "")
- repolishNeeded = False
- if self.currentStyle != style and style in self.styles:
- self.setStyle(style)
- repolishNeeded = True
- if self.currentTheme != theme:
- self.setTheme(theme)
- repolishNeeded = True
- if repolishNeeded:
- ThemesBase.repolishAllWidgets()
- def populate(self):
- self.__themes = ThemesBase.getThemes()
- self.__styles = ThemesBase.getStyles()
- """ staticmethods
- """
- @staticmethod
- def readCss(path):
- css = ""
- try:
- cssFile = open(path, 'r')
- except OSError as err:
- warning(f"Failed to read file {path} error: {err}", ThemesBase)
- else:
- css = cssFile.read()
- cssFile.close()
- return css
- @staticmethod
- def getStyles():
- """ Returns a list with available system styles.
- """
- return QStyleFactory.keys()
- @staticmethod
- def getThemes():
- """ Will look for themes in the user's data location and system data
- location.
- Examples:
- user: ~/.local/searx-qt/themes
- sys: /usr/share/searx-qt/themes
- https://doc.qt.io/qt-5/qstandardpaths.html
- """
- return (
- ThemesBase.findThemes(os.getcwd() + "/themes/") +
- ThemesBase.findThemes(USER_THEMES_PATH) +
- ThemesBase.findThemes(SYS_THEMES_PATH)
- )
- @staticmethod
- def getCurrentStyle():
- return QApplication.instance().style().objectName()
- @staticmethod
- def findThemes(path, lookForCompiledResource=True):
- """
- @param path: Full path to the themes directory.
- @type pathL str
- @param lookForCompiledResource:
- When set to True it will exclude themes that defined a .qrc file
- and there is no .rcc file found.
- When set to False it will exclude themes that defined a .qrc file
- but the .qrc file is not found.
- @type lookForCompiledResource: bool
- """
- themes = []
- if not os.path.exists(path):
- debug(f"Themes path {path} not found.", ThemesBase)
- return themes
- elif not os.path.isdir(path):
- debug(f"Themes path {path} is not a directory.", ThemesBase)
- return themes
- for themeDir in os.listdir(path):
- fullThemePath = os.path.join(path, themeDir)
- # Get theme manifest data
- manifestFilePath = os.path.join(fullThemePath, 'manifest.json')
- if not os.path.isfile(manifestFilePath):
- error(f"{manifestFilePath} not found.", ThemesBase)
- continue
- name = ""
- appCssFile = ""
- resultsCssFile = None
- failCssFile = None
- icons = None
- try:
- manifestFile = open(manifestFilePath, 'r')
- except OSError as err:
- error(f"Could not open manifest file {manifestFilePath} " \
- f"error: {err}", ThemesBase)
- continue
- else:
- try:
- manifestJson = json.load(manifestFile)
- except json.JSONDecodeError as err:
- error(f"Malformed manifest {manifestFilePath} {err}",
- ThemesBase)
- manifestFile.close()
- continue
- else:
- manifestFile.close()
- del manifestFile
- # manifest.json key: name (str)
- # - *name is a required key.
- name = manifestJson.get('name', None)
- if type(name) is not str:
- error(f"Malformed manifest {manifestFilePath} name " \
- "is not set or is not a string.", ThemesBase)
- continue
- styles = manifestJson.get('styles', None)
- if type(styles) is not dict:
- warning(
- "manifest.json key 'style' is not a dict",
- ThemesBase
- )
- continue
- # manifest.json key: stylesheet (str)
- # - *stylesheet is a required key.
- # - It should have the .css filename that should be
- # present in the root of the theme directory.
- appCssPath = styles.get('app', None)
- if type(appCssPath) is not str:
- error(
- "Please set a valid value for ['styles']['app']",
- ThemesBase
- )
- continue
- appCssFile = appCssPath
- appCssFilePath = os.path.join(fullThemePath, appCssFile)
- if not os.path.isfile(appCssFilePath):
- error(f"{appCssFilePath} not found.", ThemesBase)
- continue
- # manifest.json key: html_results (str)
- # manifest.json key: html_fail (str)
- resultsCssFile = styles.get('html_results', None)
- failCssFile = styles.get('html_fail', None)
- # manifest.json key: icons (str)
- # - icons is a optional key.
- # - Example: icons.qrc
- # - When set the icons.qrc file should exist in the root
- # of the themes directory.
- iconsFilePath = manifestJson.get('icons', None)
- if type(iconsFilePath) is str:
- fullIconsFilePath = os.path.join(
- fullThemePath,
- iconsFilePath
- )
- if lookForCompiledResource:
- # Look for .rcc instead of .qrc
- fullIconsFilePath = replaceFileExt(
- fullIconsFilePath,
- '.rcc'
- )
- if not os.path.isfile(fullIconsFilePath):
- warning("The theme defined a qrc/rcc resource " \
- "file but it could not be located! " \
- f"{fullIconsFilePath}", ThemesBase)
- continue
- icons = iconsFilePath
- del manifestFilePath
- theme = UserTheme(
- themeDir,
- name,
- appCssFile,
- fullThemePath,
- icons=icons,
- resultsCssFile=resultsCssFile,
- failCssFile=failCssFile
- )
- themes.append(theme)
- return themes
- @staticmethod
- def repolishAllWidgets():
- """ Call this after another style or theme is set to update the view.
- """
- qApp = QApplication.instance()
- for widget in qApp.allWidgets():
- widget.style().unpolish(widget)
- widget.style().polish(widget)
- widget.update()
- if __name__ == '__main__':
- pass
- else:
- Themes = ThemesBase()
|