From f37e66a63cb7b7a1b0592e01b8e063b65246638a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Moreira?= Date: Fri, 24 Jan 2020 13:21:46 -0300 Subject: [PATCH] [ui][settings] improvements --- bauh/api/abstract/controller.py | 8 ++++- bauh/api/abstract/view.py | 37 ++++++++++++++++++++--- bauh/gems/appimage/controller.py | 23 ++++++++++++-- bauh/gems/arch/controller.py | 4 +-- bauh/gems/flatpak/controller.py | 4 +-- bauh/gems/web/config.py | 1 + bauh/gems/web/controller.py | 51 ++++++++++++++++++++++++++++++-- bauh/view/core/controller.py | 29 ++++++++++++++++-- bauh/view/qt/components.py | 35 ++++++++++++++++++---- bauh/view/qt/settings.py | 40 +++++++++++++++++++++---- 10 files changed, 205 insertions(+), 27 deletions(-) diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index c3a9adf1..f1c32b60 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -3,7 +3,7 @@ import os import shutil from abc import ABC, abstractmethod from pathlib import Path -from typing import List, Set, Type +from typing import List, Set, Type, Tuple import yaml @@ -279,3 +279,9 @@ class SoftwareManager(ABC): :return: a form abstraction with all available settings """ pass + + def save_settings(self, component: ViewComponent) -> Tuple[bool, List[str]]: + """ + :return: a tuple with a bool informing if the settings were saved and a list of error messages + """ + pass diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index 8b9871ab..6d07539a 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -17,6 +17,18 @@ class ViewComponent(ABC): self.id = id_ +class PanelComponent(ViewComponent): + + def __init__(self, components: List[ViewComponent], id_: str = None): + super(PanelComponent, self).__init__(id_=id_) + self.components = components + self.component_map = {c.id: c for c in components if c.id is not None} if components else None + + def get_component(self, id_: str) -> ViewComponent: + if self.component_map: + return self.component_map.get(id_) + + class InputViewComponent(ViewComponent): """ Represents an component which needs a user interaction to provide its value @@ -99,13 +111,14 @@ class TextComponent(ViewComponent): class TextInputComponent(ViewComponent): - def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip: str = None, read_only: bool =False, id_: str = None): + def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip: str = None, read_only: bool =False, id_: str = None, only_int: bool = False): super(TextInputComponent, self).__init__(id_=id_) self.label = label self.value = value self.tooltip = tooltip self.placeholder = placeholder self.read_only = read_only + self.only_int = only_int def get_value(self) -> str: if self.value is not None: @@ -113,6 +126,11 @@ class TextInputComponent(ViewComponent): else: return '' + def get_int_value(self) -> int: + if self.value is not None: + return int(self.value) + return None + class FormComponent(ViewComponent): @@ -120,6 +138,11 @@ class FormComponent(ViewComponent): super(FormComponent, self).__init__(id_=id_) self.label = label self.components = components + self.component_map = {c.id : c for c in components if c.id} if components else None + + def get_component(self, id_: str) -> ViewComponent: + if self.component_map: + return self.component_map.get(id_) class FileChooserComponent(ViewComponent): @@ -133,14 +156,20 @@ class FileChooserComponent(ViewComponent): class TabComponent(ViewComponent): - def __init__(self, label: str, content: ViewComponent, id_: str = None): + def __init__(self, label: str, content: ViewComponent, icon_path: str = None, id_: str = None): super(TabComponent, self).__init__(id_=id_) self.label = label self.content = content + self.icon_path = icon_path class TabGroupComponent(ViewComponent): - def __init__(self, components: List[TabComponent], id_: str = None): + def __init__(self, tabs: List[TabComponent], id_: str = None): super(TabGroupComponent, self).__init__(id_=id_) - self.components = components + self.tabs = tabs + self.tab_map = {c.id: c for c in tabs if c.id} if tabs else None + + def get_tab(self, id_: str) -> TabComponent: + if self.tab_map: + return self.tab_map.get(id_) diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 9a4543d0..a69e6ac6 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -19,7 +19,8 @@ from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ SuggestionPriority -from bauh.api.abstract.view import MessageType +from bauh.api.abstract.view import MessageType, ViewComponent, FormComponent, InputOption, SingleSelectComponent, \ + SelectViewType, TextInputComponent, PanelComponent from bauh.commons.html import bold from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, run_cmd, SimpleProcess from bauh.gems.appimage import query, INSTALLATION_PATH, LOCAL_PATH, SUGGESTIONS_FILE @@ -494,5 +495,23 @@ class AppImageManager(SoftwareManager): os.remove(f) print('{}[bauh][appimage] {} deleted{}'.format(Fore.YELLOW, f, Fore.RESET)) except: - print('{}[bauh][appimage] An exception has happened when deleting {}{}'.format(Fore.RED, Fore.RESET)) + print('{}[bauh][appimage] An exception has happened when deleting {}{}'.format(Fore.RED, f, Fore.RESET)) traceback.print_exc() + + def get_settings(self) -> ViewComponent: + + config = read_config() + + enabled_opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True), + InputOption(label=self.i18n['no'].capitalize(), value=False)] + + updater_opts = [ + SingleSelectComponent(label='Enabled', + options=enabled_opts, + default_option=[o for o in enabled_opts if o.value == config['db_updater']['enabled']][0], + max_per_line=len(enabled_opts), + type_=SelectViewType.RADIO), + TextInputComponent(label='Interval', value=str(config['db_updater']['interval']), tooltip="Update interval in SECONDS", only_int=True) + ] + + return PanelComponent([FormComponent(updater_opts, 'Database updates')]) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 046b7b2f..5106a69e 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -16,7 +16,7 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus, \ SuggestionPriority from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \ - ViewComponent + ViewComponent, PanelComponent from bauh.commons.category import CategoriesDownloader from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess, \ @@ -906,4 +906,4 @@ class ArchManager(SoftwareManager): max_per_line=len(trans_check_opts), type_=SelectViewType.RADIO)] - return FormComponent([FormComponent(fields, label='Installation')]) + return PanelComponent([FormComponent(fields, label='Installation')]) diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 3e73502c..ccd76e6b 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -9,7 +9,7 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import PackageHistory, PackageUpdate, SoftwarePackage, PackageSuggestion, \ SuggestionPriority from bauh.api.abstract.view import MessageType, FormComponent, SingleSelectComponent, InputOption, SelectViewType, \ - ViewComponent + ViewComponent, PanelComponent from bauh.commons import user from bauh.commons.html import strip_html, bold from bauh.commons.system import SystemProcess, ProcessHandler, SimpleProcess @@ -431,4 +431,4 @@ class FlatpakManager(SoftwareManager): max_per_line=len(install_opts), type_=SelectViewType.RADIO)) - return FormComponent([FormComponent(fields, 'Installation')]) + return PanelComponent([FormComponent(fields, 'Installation')]) diff --git a/bauh/gems/web/config.py b/bauh/gems/web/config.py index b532704a..6c8e402b 100644 --- a/bauh/gems/web/config.py +++ b/bauh/gems/web/config.py @@ -12,3 +12,4 @@ def read_config(update_file: bool = False) -> dict: return read(CONFIG_FILE, default_config, update_file) + diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index d58db812..08eb762b 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -21,13 +21,14 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import SoftwarePackage, PackageAction, PackageSuggestion, PackageUpdate, PackageHistory, \ SuggestionPriority, PackageStatus from bauh.api.abstract.view import MessageType, MultipleSelectComponent, InputOption, SingleSelectComponent, \ - SelectViewType, TextInputComponent, FormComponent, FileChooserComponent + SelectViewType, TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, PanelComponent from bauh.api.constants import DESKTOP_ENTRIES_DIR from bauh.commons import resource +from bauh.commons.config import save_config from bauh.commons.html import bold from bauh.commons.system import ProcessHandler, get_dir_size, get_human_size_str from bauh.gems.web import INSTALLED_PATH, nativefier, DESKTOP_ENTRY_PATH_PATTERN, URL_FIX_PATTERN, ENV_PATH, UA_CHROME, \ - SEARCH_INDEX_FILE, SUGGESTIONS_CACHE_FILE, ROOT_DIR + SEARCH_INDEX_FILE, SUGGESTIONS_CACHE_FILE, ROOT_DIR, CONFIG_FILE from bauh.gems.web.config import read_config from bauh.gems.web.environment import EnvironmentUpdater, EnvironmentComponent from bauh.gems.web.model import WebApplication @@ -889,3 +890,49 @@ class WebApplicationManager(SoftwareManager): except: print('{}[bauh][web] An exception has happened when deleting {}{}'.format(Fore.RED, ENV_PATH, Fore.RESET)) traceback.print_exc() + + def get_settings(self) -> ViewComponent: + config = read_config() + + input_electron = TextInputComponent(label="Custom Electron version", + value=config['environment']['electron']['version'], + tooltip="Electron version to render the applications", + id_='electron_version') + + native_opts = [ + InputOption(label="Environment", value=False, tooltip="If a nativefier version from the isolated environment should be used to install the applications"), + InputOption(label="System", value=True, tooltip="If a nativefier installed on your system should be used to install the applications") + ] + + select_nativefier = SingleSelectComponent(label="Nativefier", + options=native_opts, + default_option=[o for o in native_opts if o.value == config['environment']['system']][0], + type_=SelectViewType.COMBO, + id_='nativefier') + + form_env = FormComponent(label="Environment", components=[input_electron, select_nativefier]) + + return PanelComponent([form_env]) + + def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: + config = read_config() + + form_env = component.components[0] + + config['environment']['electron']['version'] = str(form_env.get_component('electron_version').get_value()).strip() + + if len(config['environment']['electron']['version']) == 0: + config['environment']['electron']['version'] = None + + system_nativefier = form_env.get_component('nativefier').get_selected() + + if system_nativefier and not nativefier.is_available(): + return False, ['Nativefier seems not to be installed on your system'] + + config['environment']['system'] = system_nativefier + + try: + save_config(config, CONFIG_FILE) + return True, None + except: + return False, [traceback.format_exc()] diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index 91bd25a9..34a5f78e 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -2,8 +2,9 @@ import re import time import traceback from threading import Thread -from typing import List, Set, Type +from typing import List, Set, Type, Tuple +from bauh import ROOT_DIR from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher @@ -410,6 +411,30 @@ class GenericSoftwareManager(SoftwareManager): man_comp = man.get_settings() if man_comp: - settings_forms.append(TabComponent(man.__class__.__name__, man_comp)) + modname = man.__module__.split('.')[-2] + icon_path = "{r}/gems/{n}/resources/img/{n}.svg".format(r=ROOT_DIR, n=modname) + settings_forms.append(TabComponent(None, man_comp, icon_path, modname)) return TabGroupComponent(settings_forms) + + def save_settings(self, component: TabGroupComponent) -> Tuple[bool, List[str]]: + + saved, warnings = True, [] + + for man in self.managers: + if man: + modname = man.__module__.split('.')[-2] + tab = component.get_tab(modname) + + if not tab: + self.logger.warning("Tab for {} was not found".format(man.__class__.__name__)) + else: + success, errors = man.save_settings(tab.content) + + if not success: + saved = False + + if errors: + warnings.extend(errors) + + return saved, warnings diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index fb0e1261..f95488cc 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -2,12 +2,12 @@ from pathlib import Path from typing import Tuple from PyQt5.QtCore import Qt -from PyQt5.QtGui import QIcon, QPixmap +from PyQt5.QtGui import QIcon, QPixmap, QIntValidator from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \ - QLabel, QSizePolicy, QLineEdit, QToolButton, QHBoxLayout, QFormLayout, QFileDialog, QTabWidget + QLabel, QSizePolicy, QLineEdit, QToolButton, QHBoxLayout, QFormLayout, QFileDialog, QTabWidget, QVBoxLayout from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType, \ - TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, TabGroupComponent + TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, TabGroupComponent, PanelComponent from bauh.view.qt import css from bauh.view.util import resource from bauh.view.util.translation import I18n @@ -162,6 +162,9 @@ class TextInputQt(QGroupBox): self.text_input = QLineEdit() + if model.only_int: + self.text_input.setValidator(QIntValidator()) + if model.placeholder: self.text_input.setPlaceholderText(model.placeholder) @@ -302,6 +305,20 @@ class IconButton(QWidget): self.bt.setToolTip(self.default_tootip) +class PanelQt(QWidget): + + def __init__(self, model: PanelComponent, i18n: I18n, parent: QWidget = None): + super(PanelQt, self).__init__(parent=parent) + self.model = model + self.i18n = i18n + self.setLayout(QVBoxLayout()) + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + + if model.components: + for c in model.components: + self.layout().addWidget(to_widget(c, i18n)) + + class FormQt(QGroupBox): def __init__(self, model: FormComponent, i18n: I18n): @@ -334,6 +351,9 @@ class FormQt(QGroupBox): def _new_text_input(self, c: TextInputComponent) -> Tuple[QLabel, QLineEdit]: line_edit = QLineEdit() + if c.only_int: + line_edit.setValidator(QIntValidator()) + if c.tooltip: line_edit.setToolTip(c.tooltip) @@ -388,10 +408,11 @@ class TabGroupQt(QTabWidget): def __init__(self, model: TabGroupComponent, i18n: I18n, parent: QWidget = None): super(TabGroupQt, self).__init__(parent=parent) self.model = model - self.setTabPosition(QTabWidget.West) + self.setTabPosition(QTabWidget.North) - for c in model.components: - self.addTab(to_widget(c.content, i18n), c.label) + for c in model.tabs: + icon = QIcon(c.icon_path) if c.icon_path else None + self.addTab(to_widget(c.content, i18n), icon, c.label) def new_single_select(model: SingleSelectComponent) -> QWidget: @@ -424,5 +445,7 @@ def to_widget(comp: ViewComponent, i18n: I18n, parent: QWidget = None) -> QWidge return FormQt(comp, i18n) elif isinstance(comp, TabGroupComponent): return TabGroupQt(comp, i18n, parent) + elif isinstance(comp, PanelComponent): + return PanelQt(comp, i18n, parent) else: raise Exception("Cannot render instances of " + comp.__class__.__name__) diff --git a/bauh/view/qt/settings.py b/bauh/view/qt/settings.py index 965c74e1..42139316 100644 --- a/bauh/view/qt/settings.py +++ b/bauh/view/qt/settings.py @@ -1,7 +1,11 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QToolBar, QSizePolicy, QToolButton +from io import StringIO + +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QToolBar, QSizePolicy, QToolButton, QPushButton from bauh.api.abstract.controller import SoftwareManager -from bauh.view.qt.components import to_widget +from bauh.api.abstract.view import MessageType +from bauh.view.qt import dialog +from bauh.view.qt.components import to_widget, new_spacer from bauh.view.util.translation import I18n @@ -15,15 +19,39 @@ class SettingsWindow(QWidget): self.i18n = i18n self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) - settings_model = self.manager.get_settings() + self.settings_model = self.manager.get_settings() - self.layout().addWidget(to_widget(settings_model, i18n)) + self.layout().addWidget(to_widget(self.settings_model, i18n)) action_bar = QToolBar() action_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) - bt_save = QToolButton() - bt_save.setText('Save') + bt_close = QPushButton() + bt_close.setText(self.i18n['close'].capitalize()) + bt_close.clicked.connect(lambda: self.close()) + action_bar.addWidget(bt_close) + + action_bar.addWidget(new_spacer()) + + bt_save = QPushButton() + bt_save.setText(self.i18n['save'].capitalize()) + bt_save.clicked.connect(self._save_settings) action_bar.addWidget(bt_save) self.layout().addWidget(action_bar) + + def _save_settings(self): + success, warnings = self.manager.save_settings(self.settings_model) + + if success: + self.close() + else: + msg = StringIO() + msg.write("It was not possible to properly the settings\n") + + for w in warnings: + msg.write(w + '\n') + + msg.seek(0) + + dialog.show_message(title="Warning", body=msg.read(), type_=MessageType.WARNING)