Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
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