import glob import os import re from typing import Optional, Dict, Tuple, Set from bauh.api.paths import USER_THEMES_DIR from bauh.view.util import resource from bauh.view.util.translation import I18n # RE_WIDTH_PERCENT = re.compile(r'[\d\\.]+%w') TODO percentage measures disabled for the moment (requires more testing) # RE_HEIGHT_PERCENT = re.compile(r'[\d\\.]+%h') TODO percentage measures disabled for the moment (requires more testing) RE_META_I18N_FIELDS = re.compile(r'((name|description)(\[\w+])?)') RE_VAR_PATTERN = re.compile(r'^@[\w.\-_]+') RE_QSS_EXT = re.compile(r'\.qss$') class ThemeMetadata: def __init__(self, file_path: str, default: bool, default_name: Optional[str] = None, default_description: Optional[str] = None, version: Optional[str] = None, root_theme: Optional[str] = None, abstract: bool = False): self.names = {} self.default_name = default_name self.descriptions = {} self.default_description = default_description self.root_theme = root_theme self.version = version self.file_path = file_path self.file_dir = '/'.join(file_path.split('/')[0:-1]) self.default = default self.key = self.file_path.split('/')[-1].split('.')[0] if self.default else self.file_path self.abstract = abstract def __eq__(self, other) -> bool: if isinstance(other, ThemeMetadata): return self.file_path == other.file_path return False def __hash__(self): return self.file_path.__hash__() def __repr__(self): return self.file_path if self.file_path else '' def get_i18n_name(self, i18n: I18n) -> str: if self.names: name = self.names.get(i18n.current_key, self.names.get(i18n.default_key)) if name: return name if self.default_name: return self.default_name else: return self.file_path.split('/')[-1] def get_i18n_description(self, i18n: I18n) -> Optional[str]: if self.descriptions: des = self.descriptions.get(i18n.current_key, self.descriptions.get(i18n.default_key)) if des: return des return self.default_description def read_theme_metada(key: str, file_path: str) -> ThemeMetadata: meta_file = RE_QSS_EXT.sub('.meta', file_path) meta_obj = ThemeMetadata(file_path=file_path, default_name=key, default=not key.startswith('/')) if os.path.exists(meta_file): meta_dict = {} with open(meta_file) as f: for line in f.readlines(): if line: field_split = line.split('=') if len(field_split) > 1: meta_dict[field_split[0].strip()] = field_split[1].strip() if meta_dict: for field, val in meta_dict.items(): if field == 'version': meta_obj.version = val elif field == 'root_theme': meta_obj.root_theme = val elif field == 'name': meta_obj.default_name = val elif field == 'description': meta_obj.default_description = val elif field == 'abstract': boolean = val.lower() if boolean == 'true': meta_obj.abstract = True elif boolean == 'false': meta_obj.abstract = False else: i18n_field = RE_META_I18N_FIELDS.findall(field) if i18n_field: if i18n_field[0][1] == 'name': meta_obj.names[i18n_field[0][2][1:-1]] = val else: meta_obj.descriptions[i18n_field[0][2][1:-1]] = val return meta_obj def read_default_themes() -> Dict[str, str]: return {f.split('/')[-1].split('.')[0].lower(): f for f in glob.glob(resource.get_path('style/**/*.qss'))} def read_user_themes() -> Dict[str, str]: return {f: f for f in glob.glob('{}/**/*.qss'.format(USER_THEMES_DIR), recursive=True)} def read_all_themes_metadata() -> Set[ThemeMetadata]: themes = set() for key, file_path in read_default_themes().items(): themes.add(read_theme_metada(key=key, file_path=file_path)) for key, file_path in read_user_themes().items(): themes.add(read_theme_metada(key=key, file_path=file_path)) return themes def process_theme(file_path: str, theme_str: str, metadata: ThemeMetadata, available_themes: Optional[Dict[str, str]]) -> Optional[Tuple[str, ThemeMetadata]]: if theme_str and metadata: root_theme = None if metadata.root_theme and metadata.root_theme in available_themes: root_file = available_themes[metadata.root_theme] if os.path.isfile(root_file): with open(root_file) as f: root_theme_str = f.read() if root_theme_str: root_metadata = read_theme_metada(key=metadata.root_theme, file_path=root_file) root_theme = process_theme(file_path=root_file, theme_str=root_theme_str, metadata=root_metadata, available_themes=available_themes) var_map = _read_var_file(file_path) var_map['images'] = resource.get_path('img') var_map['style_dir'] = metadata.file_dir if var_map: var_list = [*var_map.keys()] var_list.sort(key=_by_str_len, reverse=True) for var in var_list: theme_str = theme_str.replace('@' + var, var_map[var]) # TODO percentage measures disabled for the moment (requires more testing) # screen_size = QApplication.primaryScreen().size() # theme_str = process_width_percent_measures(theme_str, screen_size.width()) # theme_str = process_height_percent_measures(theme_str, screen_size.height()) return theme_str if not root_theme else '{}\n{}'.format(root_theme[0], theme_str), metadata def _by_str_len(string: str) -> int: return len(string) def _read_var_file(theme_file: str) -> dict: vars_file = theme_file.replace('.qss', '.vars') var_map = {} if os.path.isfile(vars_file): with open(vars_file) as f: for line in f.readlines(): if line: line_strip = line.strip() if line_strip: var_value = line_strip.split('=') if var_value and len(var_value) == 2: var, value = var_value[0].strip(), var_value[1].strip() if var and value: var_map[var] = value if var_map: process_var_of_vars(var_map) # mapping keys that point to others return var_map def process_var_of_vars(var_map: dict): while True: pending_vars, invalid = {}, set() for k, v in var_map.items(): var_match = RE_VAR_PATTERN.match(v) if var_match: var_name = var_match.group()[1:] if var_name not in var_map or var_name == k: invalid.add(k) else: pending_vars[k] = var_name for key in invalid: del var_map[key] if not pending_vars: break resolved = 0 for key, val in pending_vars.items(): real_val = var_map[val] if not RE_VAR_PATTERN.match(real_val): var_map[key] = real_val resolved += 1 if resolved == len(pending_vars): break # TODO percentage measures disabled for the moment (requires more testing) # def process_width_percent_measures(theme: str, screen_width: int) -> str: # width_measures = RE_WIDTH_PERCENT.findall(theme) # # final_theme = theme # if width_measures: # for m in width_measures: # try: # percent = float(m.split('%')[0]) # final_theme = final_theme.replace(m, '{}px'.format(round(screen_width * percent))) # except ValueError: # traceback.print_exc() # # return final_theme # def process_height_percent_measures(theme: str, screen_height: int) -> str: # width_measures = RE_HEIGHT_PERCENT.findall(theme) # # final_sheet = theme # if width_measures: # for m in width_measures: # try: # percent = float(m.split('%')[0]) # final_sheet = final_sheet.replace(m, '{}px'.format(round(screen_height * percent))) # except ValueError: # traceback.print_exc() # # return final_sheet