mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-06 20:34:16 +02:00
Make bearhub stylesheet module independent from bauh
This commit is contained in:
@@ -1,5 +1,223 @@
|
||||
"""
|
||||
Temporary namespace migration bridge for stylesheet utilities.
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, Optional, Set, Tuple
|
||||
|
||||
from bearhub.api.paths import USER_THEMES_DIR
|
||||
from bearhub.view.util import resource
|
||||
from bearhub.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])
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
from bauh.stylesheet import * # noqa: F401,F403
|
||||
|
||||
Reference in New Issue
Block a user