# coding:utf-8 import json from enum import Enum from pathlib import Path from typing import List import darkdetect from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtGui import QColor from PyQt5.QtWidgets import qApp from .exception_handler import exceptionHandler class Theme(Enum): """ Theme enumeration """ LIGHT = "Light" DARK = "Dark" AUTO = "Auto" class ConfigValidator: """ Config validator """ def validate(self, value): """ Verify whether the value is legal """ return True def correct(self, value): """ correct illegal value """ return value class RangeValidator(ConfigValidator): """ Range validator """ def __init__(self, min, max): self.min = min self.max = max self.range = (min, max) def validate(self, value): return self.min <= value <= self.max def correct(self, value): return min(max(self.min, value), self.max) class OptionsValidator(ConfigValidator): """ Options validator """ def __init__(self, options): if not options: raise ValueError("The `options` can't be empty.") if isinstance(options, Enum): options = options._member_map_.values() self.options = list(options) def validate(self, value): return value in self.options def correct(self, value): return value if self.validate(value) else self.options[0] class BoolValidator(OptionsValidator): """ Boolean validator """ def __init__(self): super().__init__([True, False]) class FolderValidator(ConfigValidator): """ Folder validator """ def validate(self, value): return Path(value).exists() def correct(self, value): path = Path(value) path.mkdir(exist_ok=True, parents=True) return str(path.absolute()).replace("\\", "/") class FolderListValidator(ConfigValidator): """ Folder list validator """ def validate(self, value): return all(Path(i).exists() for i in value) def correct(self, value: List[str]): folders = [] for folder in value: path = Path(folder) if path.exists(): folders.append(str(path.absolute()).replace("\\", "/")) return folders class ColorValidator(ConfigValidator): """ RGB color validator """ def __init__(self, default): self.default = QColor(default) def validate(self, color): try: return QColor(color).isValid() except: return False def correct(self, value): return QColor(value) if self.validate(value) else self.default class ConfigSerializer: """ Config serializer """ def serialize(self, value): """ serialize config value """ return value def deserialize(self, value): """ deserialize config from config file's value """ return value class EnumSerializer(ConfigSerializer): """ enumeration class serializer """ def __init__(self, enumClass): self.enumClass = enumClass def serialize(self, value): return value.value def deserialize(self, value): return self.enumClass(value) class ColorSerializer(ConfigSerializer): """ QColor serializer """ def serialize(self, value: QColor): return value.name() def deserialize(self, value): if isinstance(value, list): return QColor(*value) return QColor(value) class ConfigItem(QObject): """ Config item """ valueChanged = pyqtSignal(object) def __init__(self, group, name, default, validator=None, serializer=None, restart=False): """ Parameters ---------- group: str config group name name: str config item name, can be empty default: default value options: list options value serializer: ConfigSerializer config serializer restart: bool whether to restart the application after updating value """ super().__init__() self.group = group self.name = name self.validator = validator or ConfigValidator() self.serializer = serializer or ConfigSerializer() self.__value = default self.value = default self.restart = restart self.defaultValue = self.validator.correct(default) @property def value(self): """ get the value of config item """ return self.__value @value.setter def value(self, v): v = self.validator.correct(v) ov = self.__value self.__value = v if ov != v: self.valueChanged.emit(v) @property def key(self): """ get the config key separated by `.` """ return self.group+"."+self.name if self.name else self.group def __str__(self): return f'{self.__class__.__name__}[value={self.value}]' def serialize(self): return self.serializer.serialize(self.value) def deserializeFrom(self, value): self.value = self.serializer.deserialize(value) class RangeConfigItem(ConfigItem): """ Config item of range """ @property def range(self): """ get the available range of config """ return self.validator.range def __str__(self): return f'{self.__class__.__name__}[range={self.range}, value={self.value}]' class OptionsConfigItem(ConfigItem): """ Config item with options """ @property def options(self): return self.validator.options def __str__(self): return f'{self.__class__.__name__}[options={self.options}, value={self.value}]' class ColorConfigItem(ConfigItem): """ Color config item """ def __init__(self, group, name, default, restart=False): super().__init__(group, name, QColor(default), ColorValidator(default), ColorSerializer(), restart) def __str__(self): return f'{self.__class__.__name__}[value={self.value.name()}]' class QConfig(QObject): """ Config of app """ appRestartSig = pyqtSignal() themeChanged = pyqtSignal(Theme) themeColorChanged = pyqtSignal(QColor) themeMode = OptionsConfigItem( "QFluentWidgets", "ThemeMode", Theme.AUTO, OptionsValidator(Theme), EnumSerializer(Theme)) themeColor = ColorConfigItem("QFluentWidgets", "ThemeColor", '#009faa') def __init__(self): super().__init__() self.file = Path("config/config.json") self._theme = Theme.LIGHT self._cfg = self def get(self, item): """ get the value of config item """ return item.value def set(self, item, value, save=True): """ set the value of config item Parameters ---------- item: ConfigItem config item value: the new value of config item save: bool whether to save the change to config file """ if item.value == value: return item.value = value if save: self.save() if item.restart: self._cfg.appRestartSig.emit() if item is self._cfg.themeMode: self.theme = value self._cfg.themeChanged.emit(value) if item is self._cfg.themeColor: self._cfg.themeColorChanged.emit(value) def toDict(self, serialize=True): """ convert config items to `dict` """ items = {} for name in dir(self._cfg.__class__): item = getattr(self._cfg.__class__, name) if not isinstance(item, ConfigItem): continue value = item.serialize() if serialize else item.value if not items.get(item.group): if not item.name: items[item.group] = value else: items[item.group] = {} if item.name: items[item.group][item.name] = value return items def save(self): """ save config """ self._cfg.file.parent.mkdir(parents=True, exist_ok=True) with open(self._cfg.file, "w", encoding="utf-8") as f: json.dump(self._cfg.toDict(), f, ensure_ascii=False, indent=4) @exceptionHandler() def load(self, file=None, config=None): """ load config Parameters ---------- file: str or Path the path of json config file config: Config config object to be initialized """ if isinstance(config, QConfig): self._cfg = config if isinstance(file, (str, Path)): self._cfg.file = Path(file) try: with open(self._cfg.file, encoding="utf-8") as f: cfg = json.load(f) except: cfg = {} # map config items'key to item items = {} for name in dir(self._cfg.__class__): item = getattr(self._cfg.__class__, name) if isinstance(item, ConfigItem): items[item.key] = item # update the value of config item for k, v in cfg.items(): if not isinstance(v, dict) and items.get(k) is not None: items[k].deserializeFrom(v) elif isinstance(v, dict): for key, value in v.items(): key = k + "." + key if items.get(key) is not None: items[key].deserializeFrom(value) self.theme = self.get(self.themeMode) @property def theme(self): """ get theme mode, can be `Theme.Light` or `Theme.Dark` """ return self._cfg._theme @theme.setter def theme(self, t): """ chaneg the theme without modifying the config file """ if t == Theme.AUTO: t = darkdetect.theme() t = Theme(t) if t else Theme.LIGHT self._cfg._theme = t qconfig = QConfig() def isDarkTheme(): """ whether the theme is dark mode """ return qconfig.theme == Theme.DARK def theme(): """ get theme mode """ return qconfig.theme