import os import time import traceback from math import floor from threading import Thread from typing import List, Tuple, Optional, Dict, Type, Iterable from PyQt5.QtWidgets import QApplication, QStyleFactory from bauh import ROOT_DIR, __app_name__ from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager, SettingsController, SettingsView from bauh.api.abstract.view import TabComponent, InputOption, TextComponent, MultipleSelectComponent, \ PanelComponent, FormComponent, TabGroupComponent, SingleSelectComponent, SelectViewType, TextInputComponent, \ FileChooserComponent, RangeInputComponent, ViewComponentAlignment from bauh.commons.view_utils import new_select from bauh.view.core import timeshift from bauh.view.core.config import CoreConfigManager, BACKUP_REMOVE_METHODS, BACKUP_DEFAULT_REMOVE_METHOD from bauh.view.core.downloader import AdaptableFileDownloader from bauh.view.util import translation class GenericSettingsManager(SettingsController): def __init__(self, context: ApplicationContext, managers: List[SoftwareManager], working_managers: List[SoftwareManager], configman: CoreConfigManager): self.context = context self.i18n = context.i18n self.managers = managers self.working_managers = working_managers self.logger = context.logger self.file_downloader = self.context.file_downloader self.configman = configman self._settings_views: Optional[Dict[Type, List[SettingsView]]] = None def get_settings(self) -> TabGroupComponent: tabs = list() gem_opts, def_gem_opts, gem_tabs = [], set(), [] self._settings_views = dict() for man in self.managers: can_work, reason_not_work = man.can_work() modname = man.__module__.split('.')[-2] man_settings = man.get_settings() if can_work else None if man_settings: for view in man_settings: icon_path = view.icon_path if not icon_path: icon_path = f"{ROOT_DIR}/gems/{modname}/resources/img/{modname}.svg" tab_name = view.label if view.label else self.i18n.get(f'gem.{modname}.label', modname.capitalize()) gem_tabs.append(TabComponent(label=tab_name, content=view.component, icon_path=icon_path)) views = self._settings_views.get(man.__class__, list()) self._settings_views[man.__class__] = views views.append(view) help_tip = reason_not_work if not can_work and reason_not_work else self.i18n.get(f'gem.{modname}.info') opt = InputOption(label=self.i18n.get(f'gem.{modname}.label', modname.capitalize()), tooltip=help_tip, value=modname, icon_path=f'{ROOT_DIR}/gems/{modname}/resources/img/{modname}.svg', read_only=not can_work, extra_properties={'warning': 'true'} if not can_work else None) gem_opts.append(opt) if man.is_enabled() and man in self.working_managers: def_gem_opts.add(opt) core_config = self.configman.get_config() if gem_opts: type_help = TextComponent(html=self.i18n['core.config.types.tip']) gem_opts.sort(key=lambda o: o.value) gem_selector = MultipleSelectComponent(label=None, tooltip=None, options=gem_opts, max_width=floor(self.context.screen_width * 0.22), default_options=def_gem_opts, id_="gems") tabs.append(TabComponent(label=self.i18n['core.config.tab.types'], content=PanelComponent([type_help, FormComponent([gem_selector], spaces=False)]), id_='core.types')) tabs.append(self._gen_general_settings(core_config)) tabs.append(self._gen_interface_settings(core_config)) tabs.append(self._gen_tray_settings(core_config)) tabs.append(self._gen_adv_settings(core_config)) bkp_settings = self._gen_backup_settings(core_config) if bkp_settings: tabs.append(bkp_settings) for tab in gem_tabs: tabs.append(tab) return TabGroupComponent(tabs) def _gen_adv_settings(self, core_config: dict) -> TabComponent: input_data_exp = TextInputComponent(label=self.i18n['core.config.mem_cache.data_exp'], tooltip=self.i18n['core.config.mem_cache.data_exp.tip'], value=str(core_config['memory_cache']['data_expiration']), only_int=True, id_="data_exp") input_icon_exp = TextInputComponent(label=self.i18n['core.config.mem_cache.icon_exp'], tooltip=self.i18n['core.config.mem_cache.icon_exp.tip'], value=str(core_config['memory_cache']['icon_expiration']), only_int=True, id_="icon_exp") select_trim = new_select(label=self.i18n['core.config.trim.after_upgrade'], tip=self.i18n['core.config.trim.after_upgrade.tip'], value=core_config['disk']['trim']['after_upgrade'], opts=[(self.i18n['yes'].capitalize(), True, None), (self.i18n['no'].capitalize(), False, None), (self.i18n['ask'].capitalize(), None, None)], id_='trim_after_upgrade') select_dep_check = self._gen_bool_component(label=self.i18n['core.config.system.dep_checking'], tooltip=self.i18n['core.config.system.dep_checking.tip'], value=core_config['system']['single_dependency_checking'], id_='dep_check') select_check_ssl = self._gen_bool_component(label=self.i18n['core.config.download.check_ssl'], tooltip=self.i18n['core.config.download.check_ssl.tip'], value=core_config['download']['check_ssl'], id_='download.check_ssl') select_dmthread = self._gen_bool_component(label=self.i18n['core.config.download.multithreaded'], tooltip=self.i18n['core.config.download.multithreaded.tip'], id_="down_mthread", value=core_config['download']['multithreaded']) select_mthread_client = self._gen_multithread_client_select(core_config) inputs = [select_dmthread, select_mthread_client, select_check_ssl, select_trim, select_dep_check, input_data_exp, input_icon_exp] panel = PanelComponent([FormComponent(inputs, spaces=False)], id_='advanced') return TabComponent(self.i18n['core.config.tab.advanced'].capitalize(), panel, None, 'core.adv') def _gen_multithread_client_select(self, core_config: dict) -> SingleSelectComponent: available_mthread_clients = self.file_downloader.list_available_multithreaded_clients() available_mthread_clients.sort() default_i18n_key = 'default' if available_mthread_clients else 'core.config.download.multithreaded_client.none' mthread_client_opts = [(self.i18n[default_i18n_key].capitalize(), None, None)] for client in available_mthread_clients: mthread_client_opts.append((client, client, None)) current_mthread_client = core_config['download']['multithreaded_client'] if current_mthread_client not in available_mthread_clients: current_mthread_client = None return new_select(label=self.i18n['core.config.download.multithreaded_client'], tip=self.i18n['core.config.download.multithreaded_client.tip'], id_="mthread_client", opts=mthread_client_opts, value=current_mthread_client) def _gen_tray_settings(self, core_config: dict) -> TabComponent: input_update_interval = TextInputComponent(label=self.i18n['core.config.updates.interval'].capitalize(), tooltip=self.i18n['core.config.updates.interval.tip'], only_int=True, value=str(core_config['updates']['check_interval']), id_="updates_interval") allowed_exts = {'png', 'svg', 'jpg', 'jpeg', 'ico', 'xpm'} de_path = str(core_config['ui']['tray']['default_icon']) if core_config['ui']['tray']['default_icon'] else None select_def_icon = FileChooserComponent(id_='def_icon', label=self.i18n["core.config.ui.tray.default_icon"], tooltip=self.i18n["core.config.ui.tray.default_icon.tip"], file_path=de_path, allowed_extensions=allowed_exts) up_path = str(core_config['ui']['tray']['updates_icon']) if core_config['ui']['tray']['updates_icon'] else None select_up_icon = FileChooserComponent(id_='up_icon', label=self.i18n["core.config.ui.tray.updates_icon"].capitalize(), tooltip=self.i18n["core.config.ui.tray.updates_icon.tip"].capitalize(), file_path=up_path, allowed_extensions=allowed_exts) sub_comps = [FormComponent([select_def_icon, select_up_icon, input_update_interval], spaces=False)] return TabComponent(self.i18n['core.config.tab.tray'].capitalize(), PanelComponent(sub_comps, id_='tray'), None, 'core.tray') def _gen_interface_settings(self, core_config: dict) -> TabComponent: select_hdpi = self._gen_bool_component(label=self.i18n['core.config.ui.hdpi'], tooltip=self.i18n['core.config.ui.hdpi.tip'], value=bool(core_config['ui']['hdpi']), id_='hdpi') scale_tip = self.i18n['core.config.ui.auto_scale.tip'].format('QT_AUTO_SCREEN_SCALE_FACTOR') select_ascale = self._gen_bool_component(label=self.i18n['core.config.ui.auto_scale'], tooltip=scale_tip, value=bool(core_config['ui']['auto_scale']), id_='auto_scale') try: scale = float(core_config['ui']['scale_factor']) if scale < 1.0: scale = 1.0 except ValueError: scale = 1.0 select_scale = RangeInputComponent(id_="scalef", label=self.i18n['core.config.ui.scale_factor'] + ' (%)', tooltip=self.i18n['core.config.ui.scale_factor.tip'], min_value=100, max_value=400, step_value=5, value=int(scale * 100)) if not core_config['ui']['qt_style']: cur_style = QApplication.instance().property('qt_style') else: cur_style = core_config['ui']['qt_style'] style_opts = [InputOption(label=s.capitalize(), value=s.lower()) for s in QStyleFactory.keys()] default_style = [o for o in style_opts if o.value == cur_style] if not default_style: if cur_style: default_style = InputOption(label=cur_style, value=cur_style) style_opts.append(default_style) else: default_style = style_opts[0] else: default_style = default_style[0] select_style = SingleSelectComponent(label=self.i18n['style'].capitalize(), tooltip=self.i18n['core.config.ui.qt_style.tooltip'], options=style_opts, default_option=default_style, type_=SelectViewType.COMBO, alignment=ViewComponentAlignment.CENTER, id_="style") systheme_tip = self.i18n['core.config.ui.system_theme.tip'].format(app=__app_name__) select_system_theme = self._gen_bool_component(label=self.i18n['core.config.ui.system_theme'], tooltip=systheme_tip, value=bool(core_config['ui']['system_theme']), id_='system_theme') input_maxd = TextInputComponent(label=self.i18n['core.config.ui.max_displayed'], tooltip=self.i18n['core.config.ui.max_displayed.tip'], only_int=True, id_="table_max", value=str(core_config['ui']['table']['max_displayed'])) select_dicons = self._gen_bool_component(label=self.i18n['core.config.download.icons'], tooltip=self.i18n['core.config.download.icons.tip'], id_="down_icons", value=core_config['download']['icons']) sub_comps = [FormComponent([select_hdpi, select_ascale, select_scale, select_dicons, select_system_theme, select_style, input_maxd], spaces=False)] return TabComponent(self.i18n['core.config.tab.ui'].capitalize(), PanelComponent(sub_comps, id_='interface'), None, 'core.ui') def _gen_general_settings(self, core_config: dict) -> TabComponent: locale_keys = translation.get_available_keys() locale_opts = [InputOption(label=self.i18n[f'locale.{k}'].capitalize(), value=k) for k in locale_keys] current_locale = None if core_config['locale']: current_locale = [loc for loc in locale_opts if loc.value == core_config['locale']] if not current_locale: if self.i18n.current_key: current_locale = [loc for loc in locale_opts if loc.value == self.i18n.current_key] if not current_locale: current_locale = [loc for loc in locale_opts if loc.value == self.i18n.default_key] current_locale = current_locale[0] if current_locale else None sel_locale = SingleSelectComponent(label=self.i18n['core.config.locale.label'], options=locale_opts, default_option=current_locale, type_=SelectViewType.COMBO, alignment=ViewComponentAlignment.CENTER, id_='locale') sel_store_pwd = self._gen_bool_component(label=self.i18n['core.config.store_password'].capitalize(), tooltip=self.i18n['core.config.store_password.tip'].capitalize(), id_="store_pwd", value=bool(core_config['store_root_password'])) notify_tip = self.i18n['core.config.system.notifications.tip'].capitalize() sel_sys_notify = self._gen_bool_component(label=self.i18n['core.config.system.notifications'].capitalize(), tooltip=notify_tip, value=bool(core_config['system']['notifications']), id_="sys_notify") sel_load_apps = self._gen_bool_component(label=self.i18n['core.config.boot.load_apps'], tooltip=self.i18n['core.config.boot.load_apps.tip'], value=bool(core_config['boot']['load_apps']), id_='boot.load_apps') sel_sugs = self._gen_bool_component(label=self.i18n['core.config.suggestions.activated'].capitalize(), tooltip=self.i18n['core.config.suggestions.activated.tip'].capitalize(), id_="sugs_enabled", value=bool(core_config['suggestions']['enabled'])) inp_sugs = TextInputComponent(label=self.i18n['core.config.suggestions.by_type'], tooltip=self.i18n['core.config.suggestions.by_type.tip'], value=str(core_config['suggestions']['by_type']), only_int=True, id_="sugs_by_type") inp_reboot = new_select(label=self.i18n['core.config.updates.reboot'], tip=self.i18n['core.config.updates.reboot.tip'], id_='ask_for_reboot', max_width=None, value=bool(core_config['updates']['ask_for_reboot']), opts=[(self.i18n['ask'].capitalize(), True, None), (self.i18n['no'].capitalize(), False, None)]) inputs = [sel_locale, sel_store_pwd, sel_sys_notify, sel_load_apps, inp_reboot, sel_sugs, inp_sugs] panel = PanelComponent([FormComponent(inputs, spaces=False)], id_='general') return TabComponent(self.i18n['core.config.tab.general'].capitalize(), panel, None, 'core.gen') def _gen_bool_component(self, label: str, tooltip: Optional[str], value: bool, id_: str) -> SingleSelectComponent: opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True), InputOption(label=self.i18n['no'].capitalize(), value=False)] return SingleSelectComponent(label=label, options=opts, default_option=[o for o in opts if o.value == value][0], type_=SelectViewType.RADIO, tooltip=tooltip, max_per_line=len(opts), id_=id_) def _save_settings(self, general: PanelComponent, advanced: PanelComponent, backup: PanelComponent, ui: PanelComponent, tray: PanelComponent, gems_panel: PanelComponent) -> Tuple[bool, Optional[List[str]]]: core_config = self.configman.get_config() # general gen_form = general.get_component_by_idx(0, FormComponent) locale = gen_form.get_component('locale', SingleSelectComponent).get_selected() if locale != self.i18n.current_key: core_config['locale'] = locale notifications = gen_form.get_component('sys_notify', SingleSelectComponent).get_selected() core_config['system']['notifications'] = notifications suggestions = gen_form.get_component('sugs_enabled', SingleSelectComponent).get_selected() core_config['suggestions']['enabled'] = suggestions store_root_pwd = gen_form.get_component('store_pwd', SingleSelectComponent).get_selected() core_config['store_root_password'] = store_root_pwd sugs_by_type = gen_form.get_component('sugs_by_type', TextInputComponent).get_int_value() core_config['suggestions']['by_type'] = sugs_by_type ask_reboot = gen_form.get_component('ask_for_reboot', SingleSelectComponent).get_selected() core_config['updates']['ask_for_reboot'] = ask_reboot load_apps = gen_form.get_component('boot.load_apps', SingleSelectComponent).get_selected() core_config['boot']['load_apps'] = load_apps # advanced adv_form = advanced.get_component_by_idx(0, FormComponent) download_mthreaded = adv_form.get_component('down_mthread', SingleSelectComponent).get_selected() core_config['download']['multithreaded'] = download_mthreaded mthread_client = adv_form.get_component('mthread_client', SingleSelectComponent).get_selected() core_config['download']['multithreaded_client'] = mthread_client check_ssl = adv_form.get_component('download.check_ssl', SingleSelectComponent).get_selected() core_config['download']['check_ssl'] = check_ssl if isinstance(self.file_downloader, AdaptableFileDownloader): self.file_downloader.multithread_client = mthread_client self.file_downloader.multithread_enabled = download_mthreaded self.file_downloader.check_ssl = check_ssl single_dep_check = adv_form.get_component('dep_check', SingleSelectComponent).get_selected() core_config['system']['single_dependency_checking'] = single_dep_check data_exp = adv_form.get_component('data_exp', TextInputComponent).get_int_value() core_config['memory_cache']['data_expiration'] = data_exp icon_exp = adv_form.get_component('icon_exp', TextInputComponent).get_int_value() core_config['memory_cache']['icon_expiration'] = icon_exp trim_after_upgrade = adv_form.get_component('trim_after_upgrade', SingleSelectComponent).get_selected() core_config['disk']['trim']['after_upgrade'] = trim_after_upgrade # backup if backup: bkp_form = backup.get_component_by_idx(0, FormComponent) core_config['backup']['enabled'] = bkp_form.get_component('enabled', SingleSelectComponent).get_selected() core_config['backup']['mode'] = bkp_form.get_component('mode', SingleSelectComponent).get_selected() core_config['backup']['type'] = bkp_form.get_component('type', SingleSelectComponent).get_selected() core_config['backup']['install'] = bkp_form.get_component('install', SingleSelectComponent).get_selected() core_config['backup']['upgrade'] = bkp_form.get_component('upgrade', SingleSelectComponent).get_selected() bkp_remove_method = bkp_form.get_component('remove_method', SingleSelectComponent).get_selected() core_config['backup']['remove_method'] = bkp_remove_method bkp_uninstall = bkp_form.get_component('uninstall', SingleSelectComponent).get_selected() core_config['backup']['uninstall'] = bkp_uninstall bkp_downgrade = bkp_form.get_component('downgrade', SingleSelectComponent).get_selected() core_config['backup']['downgrade'] = bkp_downgrade # tray tray_form = tray.get_component_by_idx(0, FormComponent) updates_interval = tray_form.get_component('updates_interval', TextInputComponent).get_int_value() core_config['updates']['check_interval'] = updates_interval def_icon_path = tray_form.get_component('def_icon', FileChooserComponent).file_path core_config['ui']['tray']['default_icon'] = def_icon_path if def_icon_path else None up_icon_path = tray_form.get_component('up_icon', FileChooserComponent).file_path core_config['ui']['tray']['updates_icon'] = up_icon_path if up_icon_path else None # ui ui_form = ui.get_component_by_idx(0, FormComponent) core_config['download']['icons'] = ui_form.get_component('down_icons', SingleSelectComponent).get_selected() core_config['ui']['hdpi'] = ui_form.get_component('hdpi', SingleSelectComponent).get_selected() previous_autoscale = core_config['ui']['auto_scale'] core_config['ui']['auto_scale'] = ui_form.get_component('auto_scale', SingleSelectComponent).get_selected() if previous_autoscale and not core_config['ui']['auto_scale']: self.logger.info("Deleting environment variable QT_AUTO_SCREEN_SCALE_FACTOR") del os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] core_config['ui']['scale_factor'] = ui_form.get_component('scalef').value / 100 table_max = ui_form.get_component('table_max', TextInputComponent).get_int_value() core_config['ui']['table']['max_displayed'] = table_max style = ui_form.get_component('style', SingleSelectComponent).get_selected() if core_config['ui']['qt_style']: cur_style = core_config['ui']['qt_style'] else: cur_style = QApplication.instance().property('qt_style') if style != cur_style: core_config['ui']['qt_style'] = style QApplication.instance().setProperty('qt_style', style) core_config['ui']['system_theme'] = ui_form.get_component('system_theme', SingleSelectComponent).get_selected() # gems checked_gems = gems_panel.components[1].get_component('gems', MultipleSelectComponent).get_selected_values() for man in self.managers: modname = man.__module__.split('.')[-2] enabled = modname in checked_gems man.set_enabled(enabled) if core_config['gems'] is None and len(checked_gems) == len(self.managers): sel_gems = None else: sel_gems = checked_gems core_config['gems'] = sel_gems try: self.configman.save_config(core_config) return True, None except Exception: return False, [traceback.format_exc()] def _save_views(self, views: Iterable[SettingsView], success_list: List[bool], warnings: List[str]): success = False for view in views: try: res = view.save() if res: success, errors = res[0], res[1] if errors: warnings.extend(errors) except Exception: self.logger.error(f"An exception happened while {view.controller.__class__.__name__}" f" was trying to save settings") traceback.print_exc() finally: success_list.append(success) def _save_core_settings(self, tabs: TabGroupComponent, success_list: List[bool], warnings: List[str]): success = False try: bkp = tabs.get_tab('core.bkp') success, errors = self._save_settings(general=tabs.get_tab('core.gen').get_content(PanelComponent), advanced=tabs.get_tab('core.adv').get_content(PanelComponent), tray=tabs.get_tab('core.tray').get_content(PanelComponent), backup=bkp.get_content(PanelComponent) if bkp else None, ui=tabs.get_tab('core.ui').get_content(PanelComponent), gems_panel=tabs.get_tab('core.types').get_content(PanelComponent)) if errors: warnings.extend(errors) except Exception: self.logger.error("An exception happened while saving the core settings") traceback.print_exc() finally: success_list.append(success) def save_settings(self, component: TabGroupComponent) -> Tuple[bool, Optional[List[str]]]: ti = time.time() save_threads, warnings, success_list = [], [], [] save_core = Thread(target=self._save_core_settings, args=(component, success_list, warnings)) save_core.start() save_threads.append(save_core) if self._settings_views: for views in self._settings_views.values(): save_view = Thread(target=self._save_views, args=(views, success_list, warnings)) save_view.start() save_threads.append(save_view) for t in save_threads: t.join() success = all(success_list) tf = time.time() self.logger.info(f"Saving all settings took {tf - ti:.8f} seconds") return success, warnings def _gen_backup_settings(self, core_config: dict) -> Optional[TabComponent]: if timeshift.is_available(): enabled_opt = self._gen_bool_component(label=self.i18n['core.config.backup'], tooltip=None, value=bool(core_config['backup']['enabled']), id_='enabled') ops_opts = [(self.i18n['yes'].capitalize(), True, None), (self.i18n['no'].capitalize(), False, None), (self.i18n['ask'].capitalize(), None, None)] install_mode = new_select(label=self.i18n['core.config.backup.install'], tip=None, value=core_config['backup']['install'], opts=ops_opts, id_='install') uninstall_mode = new_select(label=self.i18n['core.config.backup.uninstall'], tip=None, value=core_config['backup']['uninstall'], opts=ops_opts, id_='uninstall') upgrade_mode = new_select(label=self.i18n['core.config.backup.upgrade'], tip=None, value=core_config['backup']['upgrade'], opts=ops_opts, id_='upgrade') downgrade_mode = new_select(label=self.i18n['core.config.backup.downgrade'], tip=None, value=core_config['backup']['downgrade'], opts=ops_opts, id_='downgrade') mode = new_select( label=self.i18n['core.config.backup.mode'], tip=None, value=core_config['backup']['mode'], opts=[ (self.i18n['core.config.backup.mode.incremental'], 'incremental', self.i18n['core.config.backup.mode.incremental.tip']), (self.i18n['core.config.backup.mode.only_one'], 'only_one', self.i18n['core.config.backup.mode.only_one.tip']) ], id_='mode') type_ = new_select(label=self.i18n['type'].capitalize(), tip=None, value=core_config['backup']['type'], opts=[('rsync', 'rsync', None), ('btrfs', 'btrfs', None)], id_='type') remove_method = core_config['backup']['remove_method'] if not remove_method or remove_method not in BACKUP_REMOVE_METHODS: remove_method = BACKUP_DEFAULT_REMOVE_METHOD remove_i18n = 'core.config.backup.remove_method' remove_opts = ((self.i18n[f'{remove_i18n}.{m}'], m, self.i18n[f'{remove_i18n}.{m}.tip']) for m in sorted(BACKUP_REMOVE_METHODS)) remove_label = f'{self.i18n[remove_i18n]} ({self.i18n["core.config.backup.mode"]} ' \ f'"{self.i18n["core.config.backup.mode.only_one"].capitalize()}")' sel_remove = new_select(label=remove_label, tip=None, value=remove_method, opts=remove_opts, capitalize_label=False, id_='remove_method') inputs = [enabled_opt, type_, mode, sel_remove, install_mode, uninstall_mode, upgrade_mode, downgrade_mode] panel = PanelComponent([FormComponent(inputs, spaces=False)], id_='backup') return TabComponent(self.i18n['core.config.tab.backup'].capitalize(), panel, None, 'core.bkp')