From dab70cdfb84901176de4564ff32c7cea261ce2dd Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 22 Jun 2020 17:00:32 -0300 Subject: [PATCH] [improvement][ui] big refactoring regarding the components states --- CHANGELOG.md | 8 +- bauh/gems/web/controller.py | 1 - bauh/gems/web/model.py | 3 +- bauh/view/core/controller.py | 7 +- bauh/view/qt/apps_table.py | 24 +- bauh/view/qt/commons.py | 28 +- bauh/view/qt/components.py | 245 +++++++++- bauh/view/qt/gem_selector.py | 2 +- bauh/view/qt/info.py | 22 +- bauh/view/qt/prepare.py | 2 +- bauh/view/qt/qt_utils.py | 12 + bauh/view/qt/root.py | 24 +- bauh/view/qt/settings.py | 4 +- bauh/view/qt/systray.py | 2 +- bauh/view/qt/thread.py | 80 ++-- bauh/view/qt/view_utils.py | 12 - bauh/view/qt/window.py | 865 ++++++++++++++++++++--------------- 17 files changed, 868 insertions(+), 473 deletions(-) delete mode 100644 bauh/view/qt/view_utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf77aa7..16b41964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - UI - not performing a full table refresh after installing and uninstalling packages - filters algorithm speed and sorting - - displaying wait cursor over some components + - "ignore updates" action now takes less time to update the table content + - displaying the "wait cursor" over some components while performing some actions - sorting installed packages by their names + - big refactoring regarding the components states (now it is easier to maintain the code) ### Fixes - AppImage @@ -30,8 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - displaying empty categories - not scrolling the table to top after updating its content - not calling initial required tasks after enabling a new package type on settings - - not updating the type filters if only type is available after refreshing the table - - displaying the type filters on the search results when there is only one type available and some actions are done + - not updating the type filters if only one type is available after refreshing the table + - displaying the type filters on the search results when there is only one type available after some actions finish - minor fixes - Tray - update notifications not working on Ubuntu 18.04 diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index 6dfcf86a..b0ec0426 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -623,7 +623,6 @@ class WebApplicationManager(SoftwareManager): continue_install, install_options = self._ask_install_options(pkg, watcher) if not continue_install: - watcher.print("Installation aborted by the user") return TransactionResult(success=False, installed=[], removed=[]) watcher.change_substatus(self.i18n['web.env.checking']) diff --git a/bauh/gems/web/model.py b/bauh/gems/web/model.py index 7ac91ba8..866538f3 100644 --- a/bauh/gems/web/model.py +++ b/bauh/gems/web/model.py @@ -21,11 +21,12 @@ class WebApplication(SoftwarePackage): self.source_url = source_url if source_url else url self.installation_dir = installation_dir self.desktop_entry = desktop_entry - self.set_custom_icon(custom_icon) self.preset_options = preset_options self.save_icon = save_icon # if the icon_url should be used instead of the one retrieved by nativefier self.options_set = options_set self.package_name = package_name + self.custom_icon = custom_icon + self.set_custom_icon(custom_icon) def get_source_url(self): if self.source_url: diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index 221194b7..b7379f9e 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -365,7 +365,12 @@ class GenericSoftwareManager(SoftwareManager): return history def get_managed_types(self) -> Set[Type[SoftwarePackage]]: - pass + available_types = set() + + for man in self.get_working_managers(): + available_types.update(man.get_managed_types()) + + return available_types def is_enabled(self): return True diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index dfae405e..e274ee1e 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -128,7 +128,7 @@ class AppsTable(QTableWidget): action_history.setIcon(QIcon(resource.get_path('img/history.svg'))) def show_history(): - self.window.get_app_history(pkg) + self.window.begin_show_history(pkg) action_history.triggered.connect(show_history) menu_row.addAction(action_history) @@ -141,7 +141,7 @@ class AppsTable(QTableWidget): title=self.i18n['manage_window.apps_table.row.actions.downgrade'], body=self._parag(self.i18n['manage_window.apps_table.row.actions.downgrade.popup.body'].format(self._bold(str(pkg)))), i18n=self.i18n): - self.window.downgrade(pkg) + self.window.begin_downgrade(pkg) action_downgrade.triggered.connect(downgrade) action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg'))) @@ -157,7 +157,7 @@ class AppsTable(QTableWidget): action_ignore_updates.setIcon(QIcon(resource.get_path('img/ignore_update.svg'))) def ignore_updates(): - self.window.ignore_updates(pkg) + self.window.begin_ignore_updates(pkg) action_ignore_updates.triggered.connect(ignore_updates) menu_row.addAction(action_ignore_updates) @@ -174,7 +174,7 @@ class AppsTable(QTableWidget): title=self.i18n[action.i18_label_key], body=self._parag('{} {} ?'.format(self.i18n[action.i18_label_key], self._bold(str(pkg)))), i18n=self.i18n): - self.window.execute_custom_action(pkg, action) + self.window.begin_execute_custom_action(pkg, action) item.triggered.connect(custom_action) menu_row.addAction(item) @@ -198,7 +198,7 @@ class AppsTable(QTableWidget): if dialog.ask_confirmation(title=self.i18n['manage_window.apps_table.row.actions.uninstall.popup.title'], body=self._parag(self.i18n['manage_window.apps_table.row.actions.uninstall.popup.body'].format(self._bold(str(pkg)))), i18n=self.i18n): - self.window.uninstall_package(pkg) + self.window.begin_uninstall(pkg) def _bold(self, text: str) -> str: return '{}'.format(text) @@ -495,7 +495,7 @@ class AppsTable(QTableWidget): if pkg.model.installed: def run(): - self.window.run_app(pkg) + self.window.begin_launch_package(pkg) bt = IconButton(QIcon(resource.get_path('img/app_play.svg')), i18n=self.i18n, action=run, tooltip=self.i18n['action.run.tooltip']) bt.setEnabled(pkg.model.can_be_run()) @@ -511,18 +511,18 @@ class AppsTable(QTableWidget): item.addWidget(bt) if not pkg.model.installed: - def get_screenshots(): - self.window.get_screenshots(pkg) + def show_screenshots(): + self.window.begin_show_screenshots(pkg) - bt = IconButton(QIcon(resource.get_path('img/camera.svg')), i18n=self.i18n, action=get_screenshots, + bt = IconButton(QIcon(resource.get_path('img/camera.svg')), i18n=self.i18n, action=show_screenshots, tooltip=self.i18n['action.screenshots.tooltip']) bt.setEnabled(bool(pkg.model.has_screenshots())) item.addWidget(bt) - def get_info(): - self.window.get_app_info(pkg) + def show_info(): + self.window.begin_show_info(pkg) - bt = IconButton(QIcon(resource.get_path('img/app_info.svg')), i18n=self.i18n, action=get_info, tooltip=self.i18n['action.info.tooltip']) + bt = IconButton(QIcon(resource.get_path('img/app_info.svg')), i18n=self.i18n, action=show_info, tooltip=self.i18n['action.info.tooltip']) bt.setEnabled(bool(pkg.model.has_info())) item.addWidget(bt) diff --git a/bauh/view/qt/commons.py b/bauh/view/qt/commons.py index d9858b2f..b8e0639b 100644 --- a/bauh/view/qt/commons.py +++ b/bauh/view/qt/commons.py @@ -41,21 +41,25 @@ def update_info(pkgv: PackageView, pkgs_info: dict): pkgs_info['not_installed'] += 1 if not pkgv.model.installed else 0 -def apply_filters(pkgv: PackageView, filters: dict, info: dict, limit: bool = True): +def apply_filters(pkg: PackageView, filters: dict, info: dict, limit: bool = True): if not limit or not filters['display_limit'] or len(info['pkgs_displayed']) < filters['display_limit']: - hidden = filters['only_apps'] and pkgv.model.installed and not pkgv.model.is_application() + if not is_package_hidden(pkg, filters): + info['pkgs_displayed'].append(pkg) - if not hidden and filters['updates']: - hidden = not pkgv.model.update or pkgv.model.is_update_ignored() - if not hidden and filters['type'] is not None and filters['type'] != 'any': - hidden = pkgv.model.get_type() != filters['type'] +def is_package_hidden(pkg: PackageView, filters: dict) -> bool: + hidden = filters['only_apps'] and pkg.model.installed and not pkg.model.is_application() - if not hidden and filters['category'] is not None and filters['category'] != 'any': - hidden = not pkgv.model.categories or not [c for c in pkgv.model.categories if c.lower() == filters['category']] + if not hidden and filters['updates']: + hidden = not pkg.model.update or pkg.model.is_update_ignored() - if not hidden and filters['name']: - hidden = not filters['name'] in pkgv.model.name.lower() + if not hidden and filters['type'] is not None and filters['type'] != 'any': + hidden = pkg.model.get_type() != filters['type'] - if not hidden: - info['pkgs_displayed'].append(pkgv) + if not hidden and filters['category'] is not None and filters['category'] != 'any': + hidden = not pkg.model.categories or not [c for c in pkg.model.categories if c.lower() == filters['category']] + + if not hidden and filters['name']: + hidden = not filters['name'] in pkg.model.name.lower() + + return hidden diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 2bb34e14..41e54a2e 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -3,13 +3,13 @@ import time import traceback from pathlib import Path from threading import Thread -from typing import Tuple +from typing import Tuple, Dict, Optional, Set from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QIcon, QPixmap, QIntValidator, QCursor from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \ QLabel, QSizePolicy, QLineEdit, QToolButton, QHBoxLayout, QFormLayout, QFileDialog, QTabWidget, QVBoxLayout, \ - QSlider, QScrollArea, QFrame + QSlider, QScrollArea, QFrame, QAction from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType, \ TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, TabGroupComponent, PanelComponent, \ @@ -20,6 +20,247 @@ from bauh.view.util import resource from bauh.view.util.translation import I18n +class QtComponentsManager: + + def __init__(self): + self.components = {} + self.groups = {} + self.group_of_groups = {} + self._saved_states = {} + + def register_component(self, component_id: int, instance: QWidget, action: QAction = None): + comp = (instance, action, {'v': True, 'e': True, 'r': False}) + self.components[component_id] = comp + self._save_state(comp) + + def register_group(self, group_id: int, subgroups: bool, *ids: int): + if not subgroups: + self.groups[group_id] = {*ids} + else: + self.group_of_groups[group_id] = {*ids} + + def get_subgroups(self, root_group: int) -> Set[str]: + return self.group_of_groups.get(root_group, set()) + + def set_components_visible(self, visible: bool, *ids: int): + if ids: + for cid in ids: + self.set_component_visible(cid, visible) + else: + for cid in self.components: + self.set_component_visible(cid, visible) + + def set_component_visible(self, cid: int, visible: bool): + comp = self.components.get(cid) + if comp and self._is_visible(comp) != visible: + self._save_state(comp) + self._set_visible(comp, visible) + + def set_component_enabled(self, cid: int, enabled: bool): + comp = self.components.get(cid) + if comp and self._is_enabled(comp) != enabled: + self._save_state(comp) + self._set_enabled(comp, enabled) + + def set_component_read_only(self, cid: int, read_only: bool): + comp = self.components.get(cid) + if comp and self._supports_read_only(comp) and self._is_read_only(comp) != read_only: + self._save_state(comp) + self._set_read_only(comp, read_only) + + def set_components_enabled(self, enabled: bool, *ids: int): + if ids: + for cid in ids: + self.set_component_enabled(cid, enabled) + else: + for cid in self.components: + self.set_component_enabled(cid, enabled) + + def restore_previous_states(self, *ids: int): + if ids: + for cid in ids: + self.restore_previous_state(cid) + else: + for cid in self.components: + self.restore_previous_state(cid) + + def restore_previous_group_state(self, group_id: int): + ids = self.groups.get(group_id) + + if ids: + self.restore_previous_states(*ids) + + def restore_previous_groups_states(self, *groups: int): + if groups: + for group in groups: + self.restore_previous_group_state(group) + + def set_group_visible(self, group_id: int, visible: bool): + ids = self.groups.get(group_id) + + if ids: + self.set_components_visible(visible, *ids) + + def set_groups_visible(self, visible: bool, *groups: int): + if groups: + for group in groups: + self.set_group_visible(group, visible) + + def set_group_enabled(self, group_id: int, enabled: bool): + ids = self.groups.get(group_id) + + if ids: + self.set_components_enabled(enabled, *ids) + + def restore_previous_state(self, cid: int): + comp = self.components.get(cid) + + if comp: + previous_state = {**comp[2]} + self._restore_state(comp, previous_state) + + def _set_visible(self, comp: Tuple[QWidget, Optional[QAction], Dict[str, bool]], visible: bool): + if comp[1]: + comp[1].setVisible(visible) + else: + comp[0].setVisible(visible) + + def _set_enabled(self, comp: Tuple[QWidget, Optional[QAction], Dict[str, bool]], enabled: bool): + comp[0].setEnabled(enabled) + + def _set_read_only(self, comp: Tuple[QWidget, Optional[QAction], Dict[str, bool]], read_only: bool): + comp[0].setReadOnly(read_only) + + def _supports_read_only(self, comp: Tuple[QWidget, Optional[QAction], Dict[str, bool]]) -> bool: + return isinstance(comp, QLineEdit) + + def is_visible(self, cid: int) -> bool: + comp = self.components.get(cid) + return self._is_visible(comp) if comp else False + + def _is_visible(self, comp: Tuple[QWidget, Optional[QAction], Dict[str, bool]]) -> bool: + return comp[1].isVisible() if comp[1] else comp[0].isVisible() + + def _is_enabled(self, comp: Tuple[QWidget, Optional[QAction], Dict[str, bool]]) -> bool: + return comp[0].isEnabled() + + def _is_read_only(self, comp: Tuple[QWidget, Optional[QAction], Dict[str, bool]]) -> bool: + return comp[0].isReadOnly() if self._supports_read_only(comp) else False + + def _save_state(self, comp: Tuple[QWidget, Optional[QAction], Dict[str, bool]]): + comp[2]['v'] = self._is_visible(comp) + comp[2]['e'] = self._is_enabled(comp) + comp[2]['r'] = self._is_read_only(comp) + + def list_visible_from_group(self, group_id: int) -> Set[str]: + ids = self.groups.get(group_id) + if ids: + return {cid for cid in ids if self.is_visible(cid)} + + def disable_visible_from_groups(self, *groups): + if groups: + for group in groups: + ids = self.list_visible_from_group(group) + + if ids: + self.set_components_enabled(False, *ids) + + def disable_visible(self): + self.set_components_enabled(False, *{cid for cid in self.components if self.is_visible(cid)}) + + def enable_visible(self): + self.set_components_enabled(True, *{cid for cid in self.components if self.is_visible(cid)}) + + def enable_visible_from_groups(self, *groups): + if groups: + for group in groups: + ids = self.list_visible_from_group(group) + + if ids: + self.set_components_enabled(True, *ids) + + def save_state(self, cid: int, state_id: int): + comp = self.components.get(cid) + + if comp: + self._save_state(comp) + states = self._saved_states.get(state_id) + + if states is None: + states = {} + self._saved_states[state_id] = states + + states[cid] = {**comp[2]} + + def save_states(self, state_id: int, *ids, only_visible: bool = False): + for cid in (ids if ids else self.components): + if not only_visible or self.is_visible(cid): + self.save_state(cid, state_id) + + def save_group_state(self, group_id: int, state_id: int): + ids = self.groups.get(group_id) + + if ids: + self.save_states(state_id, *ids) + + def save_groups_states(self, state_id: int, *group_ids): + if group_ids: + for group_id in group_ids: + self.save_group_state(group_id, state_id) + + def _restore_state(self, comp: Tuple[QWidget, Optional[QAction], Dict[str, bool]], state: Dict[str, bool]): + self._save_state(comp) + + if state['v'] != self._is_visible(comp): + self._set_visible(comp, state['v']) + + if state['e'] != self._is_enabled(comp): + self._set_enabled(comp, state['e']) + + if state['r'] != self._is_read_only(comp): + self._set_read_only(comp, state['r']) + + def restore_group_state(self, group_id: int, state_id: int): + states = self._saved_states.get(state_id) + + if states: + ids = self.groups.get(group_id) + + if ids: + for cid in ids: + comp_state = states.get(cid) + + if comp_state: + comp = self.components.get(cid) + + if comp: + self._restore_state(comp, comp_state) + + def restore_groups_state(self, state_id: int, *group_ids): + if group_ids: + for group_id in group_ids: + self.restore_group_state(group_id, state_id) + + def restore_state(self, state_id: int): + state = self._saved_states.get(state_id) + + if state: + for cid, cstate in state.items(): + comp = self.components.get(cid) + + if comp: + self._restore_state(comp, cstate) + + del self._saved_states[state_id] + + def clear_saved_states(self): + self._saved_states.clear() + + def remove_saved_state(self, state_id: int): + if state_id in self._saved_states: + del self._saved_states[state_id] + + class RadioButtonQt(QRadioButton): def __init__(self, model: InputOption, model_parent: SingleSelectComponent): diff --git a/bauh/view/qt/gem_selector.py b/bauh/view/qt/gem_selector.py index 8cacff66..c4f812d5 100644 --- a/bauh/view/qt/gem_selector.py +++ b/bauh/view/qt/gem_selector.py @@ -96,7 +96,7 @@ class GemSelectorPanel(QWidget): self.manager.prepare() self.window.verify_warnings() self.window.types_changed = True - self.window.refresh_packages() + self.window.begin_refresh_packages() self.close() def exit(self): diff --git a/bauh/view/qt/info.py b/bauh/view/qt/info.py index dded3bf8..9f6ba1c3 100644 --- a/bauh/view/qt/info.py +++ b/bauh/view/qt/info.py @@ -12,9 +12,9 @@ IGNORED_ATTRS = {'name', '__app__'} class InfoDialog(QDialog): - def __init__(self, app: dict, icon_cache: MemoryCache, i18n: I18n, screen_size: QSize): + def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n, screen_size: QSize): super(InfoDialog, self).__init__(flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) - self.setWindowTitle(str(app['__app__'])) + self.setWindowTitle(str(pkg_info['__app__'])) self.screen_size = screen_size self.i18n = i18n layout = QVBoxLayout() @@ -44,17 +44,17 @@ class InfoDialog(QDialog): # # if icon_data and icon_data.get('icon'): # self.setWindowIcon(icon_data.get('icon')) - self.setWindowIcon(QIcon(app['__app__'].model.get_type_icon_path())) + self.setWindowIcon(QIcon(pkg_info['__app__'].model.get_type_icon_path())) - for idx, attr in enumerate(sorted(app.keys())): - if attr not in IGNORED_ATTRS and app[attr]: - i18n_key = app['__app__'].model.gem_name + '.info.' + attr.lower() + for idx, attr in enumerate(sorted(pkg_info.keys())): + if attr not in IGNORED_ATTRS and pkg_info[attr]: + i18n_key = pkg_info['__app__'].model.gem_name + '.info.' + attr.lower() - if isinstance(app[attr], list): - val = ' '.join([str(e).strip() for e in app[attr] if e]) - show_val = '\n'.join(['* ' + str(e).strip() for e in app[attr] if e]) + if isinstance(pkg_info[attr], list): + val = ' '.join([str(e).strip() for e in pkg_info[attr] if e]) + show_val = '\n'.join(['* ' + str(e).strip() for e in pkg_info[attr] if e]) else: - val = str(app[attr]).strip() + val = str(pkg_info[attr]).strip() show_val = val i18n_val = i18n.get('{}.{}'.format(i18n_key, val.lower())) @@ -90,7 +90,7 @@ class InfoDialog(QDialog): bt_close = QPushButton(self.i18n['close'].capitalize()) bt_close.setCursor(QCursor(Qt.PointingHandCursor)) - bt_close.clicked.connect(lambda: self.close()) + bt_close.clicked.connect(self.close) lower_bar.addWidget(bt_close) layout.addWidget(lower_bar) diff --git a/bauh/view/qt/prepare.py b/bauh/view/qt/prepare.py index b6e238d6..c3ccad04 100644 --- a/bauh/view/qt/prepare.py +++ b/bauh/view/qt/prepare.py @@ -393,7 +393,7 @@ class PreparePanel(QWidget, TaskManager): def finish(self): if self.isVisible(): - self.manage_window.refresh_packages() + self.manage_window.begin_refresh_packages() self.manage_window.show() self.self_close = True self.close() diff --git a/bauh/view/qt/qt_utils.py b/bauh/view/qt/qt_utils.py index e167c865..30ba55dd 100644 --- a/bauh/view/qt/qt_utils.py +++ b/bauh/view/qt/qt_utils.py @@ -1,5 +1,9 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtWidgets import QWidget, QApplication +from bauh.view.util import resource + def centralize(widget: QWidget): geo = widget.frameGeometry() @@ -7,3 +11,11 @@ def centralize(widget: QWidget): center_point = QApplication.desktop().screenGeometry(screen).center() geo.moveCenter(center_point) widget.move(geo.topLeft()) + + +def load_icon(path: str, width: int, height: int = None) -> QIcon: + return QIcon(QPixmap(path).scaled(width, height if height else width, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + + +def load_resource_icon(path: str, width: int, height: int = None) -> QIcon: + return load_icon(resource.get_path(path), width, height) diff --git a/bauh/view/qt/root.py b/bauh/view/qt/root.py index d35a8ff0..fc86950f 100644 --- a/bauh/view/qt/root.py +++ b/bauh/view/qt/root.py @@ -3,23 +3,28 @@ from typing import Tuple from PyQt5.QtCore import Qt from PyQt5.QtGui import QCursor, QIcon -from PyQt5.QtWidgets import QInputDialog, QLineEdit, QDialogButtonBox +from PyQt5.QtWidgets import QInputDialog, QLineEdit, QDialogButtonBox, QApplication from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.view import MessageType from bauh.commons.system import new_subprocess from bauh.view.core.config import read_config from bauh.view.qt import css +from bauh.view.qt.components import QtComponentsManager from bauh.view.qt.dialog import show_message from bauh.view.util import util from bauh.view.util.translation import I18n +ACTION_ASK_ROOT = 99 + def is_root(): return os.getuid() == 0 -def ask_root_password(context: ApplicationContext, i18n: I18n, app_config: dict = None) -> Tuple[str, bool]: +def ask_root_password(context: ApplicationContext, i18n: I18n, + app_config: dict = None, + comp_manager: QtComponentsManager = None) -> Tuple[str, bool]: cur_config = read_config() if not app_config else app_config store_password = bool(cur_config['store_root_password']) @@ -48,13 +53,20 @@ def ask_root_password(context: ApplicationContext, i18n: I18n, app_config: dict diag.resize(400, 200) + if comp_manager: + comp_manager.save_states(state_id=ACTION_ASK_ROOT, only_visible=True) + comp_manager.disable_visible() + for attempt in range(3): ok = diag.exec_() if ok: - if not validate_password(diag.textValue()): + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + valid = validate_password(diag.textValue()) + QApplication.restoreOverrideCursor() + if not valid: body = i18n['popup.root.bad_password.body'] if attempt == 2: @@ -70,10 +82,16 @@ def ask_root_password(context: ApplicationContext, i18n: I18n, app_config: dict if store_password: context.root_password = diag.textValue() + if comp_manager: + comp_manager.restore_state(ACTION_ASK_ROOT) + return diag.textValue(), ok else: break + if comp_manager: + comp_manager.restore_state(ACTION_ASK_ROOT) + return '', False diff --git a/bauh/view/qt/settings.py b/bauh/view/qt/settings.py index eeb29086..43180933 100644 --- a/bauh/view/qt/settings.py +++ b/bauh/view/qt/settings.py @@ -18,7 +18,7 @@ from bauh.view.util.translation import I18n class SettingsWindow(QWidget): def __init__(self, manager: SoftwareManager, i18n: I18n, screen_size: QSize, window: QWidget, parent: QWidget = None): - super(SettingsWindow, self).__init__(parent=parent) + super(SettingsWindow, self).__init__(parent=parent, flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) self.setWindowTitle('{} ({})'.format(i18n['settings'].capitalize(), __app_name__)) self.setLayout(QVBoxLayout()) self.manager = manager @@ -96,7 +96,7 @@ class SettingsWindow(QWidget): self.window.update_custom_actions() self.window.verify_warnings() self.window.types_changed = True - self.window.refresh_packages() + self.window.begin_refresh_packages() self.close() else: msg = StringIO() diff --git a/bauh/view/qt/systray.py b/bauh/view/qt/systray.py index a7b26c0a..481b5a2c 100755 --- a/bauh/view/qt/systray.py +++ b/bauh/view/qt/systray.py @@ -22,7 +22,7 @@ from bauh.context import generate_i18n from bauh.view.core.tray_client import TRAY_CHECK_FILE from bauh.view.core.update import check_for_update from bauh.view.qt.about import AboutDialog -from bauh.view.qt.view_utils import load_resource_icon +from bauh.view.qt.qt_utils import load_resource_icon from bauh.view.util import util, resource from bauh.view.util.translation import I18n diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 74f61a43..b2f20a92 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -1,15 +1,14 @@ import os import re +import requests import time import traceback +from PyQt5.QtCore import QThread, pyqtSignal from datetime import datetime, timedelta from io import StringIO from pathlib import Path from typing import List, Type, Set, Tuple -import requests -from PyQt5.QtCore import QThread, pyqtSignal - from bauh import LOGS_PATH from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.controller import SoftwareManager, UpgradeRequirement, UpgradeRequirements @@ -83,7 +82,7 @@ class AsyncAction(QThread, ProcessWatcher): def show_message(self, title: str, body: str, type_: MessageType = MessageType.INFO): self.signal_message.emit({'title': title, 'body': body, 'type': type_}) - def notify_finished(self, res: object): + def notify_finished(self, res: object = None): self.signal_finished.emit(res) def change_status(self, status: str): @@ -534,68 +533,68 @@ class UninstallPackage(AsyncAction): self.root_pwd = None -class DowngradeApp(AsyncAction): +class DowngradePackage(AsyncAction): - def __init__(self, manager: SoftwareManager, i18n: I18n, app: PackageView = None): - super(DowngradeApp, self).__init__() + def __init__(self, manager: SoftwareManager, i18n: I18n, pkg: PackageView = None): + super(DowngradePackage, self).__init__() self.manager = manager - self.app = app + self.pkg = pkg self.i18n = i18n self.root_pwd = None def run(self): - if self.app: + if self.pkg: success = False - if self.app.model.supports_backup(): + if self.pkg.model.supports_backup(): if not self.request_backup(read_config(), 'downgrade', self.i18n, self.root_pwd): - self.notify_finished({'app': self.app, 'success': success}) - self.app = None + self.notify_finished({'app': self.pkg, 'success': success}) + self.pkg = None self.root_pwd = None return try: - success = self.manager.downgrade(self.app.model, self.root_pwd, self) + success = self.manager.downgrade(self.pkg.model, self.root_pwd, self) except (requests.exceptions.ConnectionError, NoInternetException) as e: success = False self.print(self.i18n['internet.required']) finally: - self.notify_finished({'app': self.app, 'success': success}) - self.app = None + self.notify_finished({'app': self.pkg, 'success': success}) + self.pkg = None self.root_pwd = None -class GetAppInfo(AsyncAction): +class ShowPackageInfo(AsyncAction): - def __init__(self, manager: SoftwareManager, app: PackageView = None): - super(GetAppInfo, self).__init__() - self.app = app + def __init__(self, manager: SoftwareManager, pkg: PackageView = None): + super(ShowPackageInfo, self).__init__() + self.pkg = pkg self.manager = manager def run(self): - if self.app: - info = {'__app__': self.app} - info.update(self.manager.get_info(self.app.model)) + if self.pkg: + info = {'__app__': self.pkg} + info.update(self.manager.get_info(self.pkg.model)) self.notify_finished(info) - self.app = None + self.pkg = None -class GetAppHistory(AsyncAction): +class ShowPackageHistory(AsyncAction): - def __init__(self, manager: SoftwareManager, i18n: I18n, app: PackageView = None): - super(GetAppHistory, self).__init__() - self.app = app + def __init__(self, manager: SoftwareManager, i18n: I18n, pkg: PackageView = None): + super(ShowPackageHistory, self).__init__() + self.pkg = pkg self.manager = manager self.i18n = i18n def run(self): - if self.app: + if self.pkg: try: - self.notify_finished({'history': self.manager.get_history(self.app.model)}) + self.notify_finished({'history': self.manager.get_history(self.pkg.model)}) except (requests.exceptions.ConnectionError, NoInternetException) as e: self.notify_finished({'error': self.i18n['internet.required']}) finally: - self.app = None + self.pkg = None class SearchPackages(AsyncAction): @@ -816,21 +815,22 @@ class ListWarnings(QThread): self.signal_warnings.emit(warnings) -class LaunchApp(AsyncAction): +class LaunchPackage(AsyncAction): - def __init__(self, manager: SoftwareManager, app: PackageView = None): - super(LaunchApp, self).__init__() - self.app = app + def __init__(self, manager: SoftwareManager, pkg: PackageView = None): + super(LaunchPackage, self).__init__() + self.pkg = pkg self.manager = manager def run(self): - - if self.app: + if self.pkg: try: time.sleep(0.25) - self.manager.launch(self.app.model) + self.manager.launch(self.pkg.model) self.notify_finished(True) except: + traceback.print_exc() + finally: self.notify_finished(False) @@ -910,7 +910,7 @@ class ApplyFilters(AsyncAction): while self.wait_table_update: time.sleep(0.005) - self.notify_finished(True) + self.notify_finished() class CustomAction(AsyncAction): @@ -948,10 +948,10 @@ class CustomAction(AsyncAction): self.root_pwd = None -class GetScreenshots(AsyncAction): +class ShowScreenshots(AsyncAction): def __init__(self, manager: SoftwareManager, pkg: PackageView = None): - super(GetScreenshots, self).__init__() + super(ShowScreenshots, self).__init__() self.pkg = pkg self.manager = manager diff --git a/bauh/view/qt/view_utils.py b/bauh/view/qt/view_utils.py deleted file mode 100644 index 4801da86..00000000 --- a/bauh/view/qt/view_utils.py +++ /dev/null @@ -1,12 +0,0 @@ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QIcon, QPixmap - -from bauh.view.util import resource - - -def load_icon(path: str, width: int, height: int = None) -> QIcon: - return QIcon(QPixmap(path).scaled(width, height if height else width, Qt.KeepAspectRatio, Qt.SmoothTransformation)) - - -def load_resource_icon(path: str, width: int, height: int = None) -> QIcon: - return load_icon(resource.get_path(path), width, height) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 075c4d4c..4e404777 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -25,17 +25,18 @@ from bauh.view.qt import dialog, commons, qt_utils, root, styles from bauh.view.qt.about import AboutDialog from bauh.view.qt.apps_table import AppsTable, UpdateToggleButton from bauh.view.qt.colors import GREEN -from bauh.view.qt.components import new_spacer, InputFilter, IconButton +from bauh.view.qt.components import new_spacer, InputFilter, IconButton, QtComponentsManager from bauh.view.qt.confirmation import ConfirmationDialog from bauh.view.qt.history import HistoryDialog from bauh.view.qt.info import InfoDialog from bauh.view.qt.root import ask_root_password from bauh.view.qt.screenshots import ScreenshotsDialog from bauh.view.qt.settings import SettingsWindow -from bauh.view.qt.thread import UpgradeSelected, RefreshApps, UninstallPackage, DowngradeApp, GetAppInfo, \ - GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, NotifyPackagesReady, FindSuggestions, \ +from bauh.view.qt.thread import UpgradeSelected, RefreshApps, UninstallPackage, DowngradePackage, ShowPackageInfo, \ + ShowPackageHistory, SearchPackages, InstallPackage, AnimateProgress, NotifyPackagesReady, FindSuggestions, \ ListWarnings, \ - AsyncAction, LaunchApp, ApplyFilters, CustomSoftwareAction, GetScreenshots, CustomAction, NotifyInstalledLoaded, \ + AsyncAction, LaunchPackage, ApplyFilters, CustomSoftwareAction, ShowScreenshots, CustomAction, \ + NotifyInstalledLoaded, \ IgnorePackageUpdates from bauh.view.qt.view_model import PackageView, PackageViewStatus from bauh.view.util import util, resource @@ -57,6 +58,44 @@ def toolbar_button_style(bg: str = None, color: str = None): return style +# action ids +ACTION_APPLY_FILTERS = 1 +ACTION_SEARCH = 2 +ACTION_INSTALL = 3 +ACTION_UNINSTALL = 4 +ACTION_INFO = 5 +ACTION_HISTORY = 6 +ACTION_DOWNGRADE = 7 +ACTION_UPGRADE = 8 +ACTION_LAUNCH = 9 +ACTION_CUSTOM_ACTION = 10 +ACTION_SCREENSHOTS = 11 +ACTION_IGNORE_UPDATES = 12 + +# components ids +SEARCH_BAR = 1 +BT_INSTALLED = 2 +BT_REFRESH = 3 +BT_SUGGESTIONS = 4 +BT_UPGRADE = 5 +CHECK_UPDATES = 6 +CHECK_APPS = 7 +COMBO_TYPES = 8 +COMBO_CATEGORIES = 9 +INP_NAME = 10 +CHECK_CONSOLE = 11 +BT_SETTINGS = 12 +BT_CUSTOM_ACTIONS = 13 +BT_ABOUT = 14 + +# component groups ids +GROUP_FILTERS = 1 +GROUP_VIEW_INSTALLED = 2 +GROUP_VIEW_SEARCH = 3 +GROUP_UPPER_BAR = 4 +GROUP_LOWER_BTS = 5 + + class ManageWindow(QWidget): signal_user_res = pyqtSignal(bool) signal_root_password = pyqtSignal(str, bool) @@ -65,6 +104,7 @@ class ManageWindow(QWidget): def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, screen_size, config: dict, context: ApplicationContext, http_client: HttpClient, logger: logging.Logger, icon: QIcon): super(ManageWindow, self).__init__() + self.comp_manager = QtComponentsManager() self.i18n = i18n self.logger = logger self.manager = manager @@ -93,9 +133,9 @@ class ManageWindow(QWidget): self.label_status.setStyleSheet("font-weight: bold") self.toolbar_top.addWidget(self.label_status) - self.toolbar_search = QToolBar() - self.toolbar_search.setStyleSheet("spacing: 0px;") - self.toolbar_search.setContentsMargins(0, 0, 0, 0) + self.search_bar = QToolBar() + self.search_bar.setStyleSheet("spacing: 0px;") + self.search_bar.setContentsMargins(0, 0, 0, 0) label_pre_search = QLabel() label_pre_search.setStyleSheet(""" @@ -107,13 +147,13 @@ class ManageWindow(QWidget): border-bottom: 1px solid lightgrey; """) - self.toolbar_search.addWidget(label_pre_search) + self.search_bar.addWidget(label_pre_search) - self.input_search = QLineEdit() - self.input_search.setFrame(False) - self.input_search.setPlaceholderText(self.i18n['window_manage.input_search.placeholder'] + "...") - self.input_search.setToolTip(self.i18n['window_manage.input_search.tooltip']) - self.input_search.setStyleSheet("""QLineEdit { + self.inp_search = QLineEdit() + self.inp_search.setFrame(False) + self.inp_search.setPlaceholderText(self.i18n['window_manage.input_search.placeholder'] + "...") + self.inp_search.setToolTip(self.i18n['window_manage.input_search.tooltip']) + self.inp_search.setStyleSheet("""QLineEdit { background-color: white; color: grey; spacing: 0; @@ -124,8 +164,8 @@ class ManageWindow(QWidget): border-top: 1px solid lightgrey; } """) - self.input_search.returnPressed.connect(self.search) - self.toolbar_search.addWidget(self.input_search) + self.inp_search.returnPressed.connect(self.search) + self.search_bar.addWidget(self.inp_search) label_pos_search = QLabel() label_pos_search.setPixmap(QIcon(resource.get_path('img/search.svg')).pixmap(QSize(10, 10))) @@ -138,9 +178,10 @@ class ManageWindow(QWidget): border-bottom: 1px solid lightgrey; """) - self.toolbar_search.addWidget(label_pos_search) + self.search_bar.addWidget(label_pos_search) + + self.comp_manager.register_component(SEARCH_BAR, self.search_bar, self.toolbar_top.addWidget(self.search_bar)) - self.ref_toolbar_search = self.toolbar_top.addWidget(self.toolbar_search) self.toolbar_top.addWidget(new_spacer()) self.layout.addWidget(self.toolbar_top) @@ -149,18 +190,20 @@ class ManageWindow(QWidget): self.toolbar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.toolbar.setContentsMargins(0, 0, 0, 0) - self.checkbox_updates = QCheckBox() - self.checkbox_updates.setCursor(QCursor(Qt.PointingHandCursor)) - self.checkbox_updates.setText(self.i18n['updates'].capitalize()) - self.checkbox_updates.stateChanged.connect(self._handle_updates_filter) - self.ref_checkbox_updates = self.toolbar.addWidget(self.checkbox_updates) + self.check_updates = QCheckBox() + self.check_updates.setCursor(QCursor(Qt.PointingHandCursor)) + self.check_updates.setText(self.i18n['updates'].capitalize()) + self.check_updates.stateChanged.connect(self._handle_updates_filter) + self.check_updates.sizePolicy().setRetainSizeWhenHidden(True) + self.comp_manager.register_component(CHECK_UPDATES, self.check_updates, self.toolbar.addWidget(self.check_updates)) - self.checkbox_only_apps = QCheckBox() - self.checkbox_only_apps.setCursor(QCursor(Qt.PointingHandCursor)) - self.checkbox_only_apps.setText(self.i18n['manage_window.checkbox.only_apps']) - self.checkbox_only_apps.setChecked(True) - self.checkbox_only_apps.stateChanged.connect(self._handle_filter_only_apps) - self.ref_checkbox_only_apps = self.toolbar.addWidget(self.checkbox_only_apps) + self.check_apps = QCheckBox() + self.check_apps.setCursor(QCursor(Qt.PointingHandCursor)) + self.check_apps.setText(self.i18n['manage_window.checkbox.only_apps']) + self.check_apps.setChecked(True) + self.check_apps.stateChanged.connect(self._handle_filter_only_apps) + self.check_apps.sizePolicy().setRetainSizeWhenHidden(True) + self.comp_manager.register_component(CHECK_APPS, self.check_apps, self.toolbar.addWidget(self.check_apps)) self.any_type_filter = 'any' self.cache_type_filter_icons = {} @@ -175,7 +218,8 @@ class ManageWindow(QWidget): self.combo_filter_type.lineEdit().setAlignment(Qt.AlignCenter) self.combo_filter_type.activated.connect(self._handle_type_filter) self.combo_filter_type.addItem('--- {} ---'.format(self.i18n['type'].capitalize()), self.any_type_filter) - self.ref_combo_filter_type = self.toolbar.addWidget(self.combo_filter_type) + self.combo_filter_type.sizePolicy().setRetainSizeWhenHidden(True) + self.comp_manager.register_component(COMBO_TYPES, self.combo_filter_type, self.toolbar.addWidget(self.combo_filter_type)) self.any_category_filter = 'any' self.combo_categories = QComboBox() @@ -186,53 +230,56 @@ class ManageWindow(QWidget): self.combo_categories.lineEdit().setReadOnly(True) self.combo_categories.lineEdit().setAlignment(Qt.AlignCenter) self.combo_categories.activated.connect(self._handle_category_filter) + self.combo_categories.sizePolicy().setRetainSizeWhenHidden(True) self.combo_categories.addItem('--- {} ---'.format(self.i18n['category'].capitalize()), self.any_category_filter) - self.ref_combo_categories = self.toolbar.addWidget(self.combo_categories) + self.comp_manager.register_component(COMBO_CATEGORIES, self.combo_categories, self.toolbar.addWidget(self.combo_categories)) - self.input_name_filter = InputFilter(self.apply_filters_async) - self.input_name_filter.setPlaceholderText(self.i18n['manage_window.name_filter.placeholder'] + '...') - self.input_name_filter.setToolTip(self.i18n['manage_window.name_filter.tooltip']) - self.input_name_filter.setStyleSheet("QLineEdit { background-color: white; color: gray;}") - self.input_name_filter.setFixedWidth(130) - self.ref_input_name_filter = self.toolbar.addWidget(self.input_name_filter) + self.input_name = InputFilter(self.begin_apply_filters) + self.input_name.setPlaceholderText(self.i18n['manage_window.name_filter.placeholder'] + '...') + self.input_name.setToolTip(self.i18n['manage_window.name_filter.tooltip']) + self.input_name.setStyleSheet("QLineEdit { background-color: white; color: gray;}") + self.input_name.setFixedWidth(130) + self.input_name.sizePolicy().setRetainSizeWhenHidden(True) + self.comp_manager.register_component(INP_NAME, self.input_name, self.toolbar.addWidget(self.input_name)) self.toolbar.addWidget(new_spacer()) toolbar_bts = [] if config['suggestions']['enabled']: - self.bt_suggestions = QPushButton() - self.bt_suggestions.setCursor(QCursor(Qt.PointingHandCursor)) - self.bt_suggestions.setToolTip(self.i18n['manage_window.bt.suggestions.tooltip']) - self.bt_suggestions.setText(self.i18n['manage_window.bt.suggestions.text'].capitalize()) - self.bt_suggestions.setIcon(QIcon(resource.get_path('img/suggestions.svg'))) - self.bt_suggestions.setStyleSheet(toolbar_button_style()) - self.bt_suggestions.clicked.connect(self.read_suggestions) - self.ref_bt_suggestions = self.toolbar.addWidget(self.bt_suggestions) - toolbar_bts.append(self.bt_suggestions) - else: - self.bt_suggestions = None - self.ref_bt_suggestions = None + bt_sugs = QPushButton() + bt_sugs.setCursor(QCursor(Qt.PointingHandCursor)) + bt_sugs.setToolTip(self.i18n['manage_window.bt.suggestions.tooltip']) + bt_sugs.setText(self.i18n['manage_window.bt.suggestions.text'].capitalize()) + bt_sugs.setIcon(QIcon(resource.get_path('img/suggestions.svg'))) + bt_sugs.setStyleSheet(toolbar_button_style()) + bt_sugs.clicked.connect(lambda: self._begin_load_suggestions(filter_installed=True)) + bt_sugs.sizePolicy().setRetainSizeWhenHidden(True) + ref_bt_sugs = self.toolbar.addWidget(bt_sugs) + toolbar_bts.append(bt_sugs) + self.comp_manager.register_component(BT_SUGGESTIONS, bt_sugs, ref_bt_sugs) - self.bt_installed = QPushButton() - self.bt_installed.setCursor(QCursor(Qt.PointingHandCursor)) - self.bt_installed.setToolTip(self.i18n['manage_window.bt.installed.tooltip']) - self.bt_installed.setIcon(QIcon(resource.get_path('img/disk.svg'))) - self.bt_installed.setText(self.i18n['manage_window.bt.installed.text'].capitalize()) - self.bt_installed.clicked.connect(self._begin_loading_installed) - self.bt_installed.setStyleSheet(toolbar_button_style()) - self.ref_bt_installed = self.toolbar.addWidget(self.bt_installed) - toolbar_bts.append(self.bt_installed) + bt_inst = QPushButton() + bt_inst.setCursor(QCursor(Qt.PointingHandCursor)) + bt_inst.setToolTip(self.i18n['manage_window.bt.installed.tooltip']) + bt_inst.setIcon(QIcon(resource.get_path('img/disk.svg'))) + bt_inst.setText(self.i18n['manage_window.bt.installed.text'].capitalize()) + bt_inst.clicked.connect(self._begin_loading_installed) + bt_inst.setStyleSheet(toolbar_button_style()) + bt_inst.sizePolicy().setRetainSizeWhenHidden(True) + toolbar_bts.append(bt_inst) + self.comp_manager.register_component(BT_INSTALLED, bt_inst, self.toolbar.addWidget(bt_inst)) - self.bt_refresh = QPushButton() - self.bt_refresh.setCursor(QCursor(Qt.PointingHandCursor)) - self.bt_refresh.setToolTip(i18n['manage_window.bt.refresh.tooltip']) - self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg'))) - self.bt_refresh.setText(self.i18n['manage_window.bt.refresh.text']) - self.bt_refresh.setStyleSheet(toolbar_button_style()) - self.bt_refresh.clicked.connect(lambda: self.refresh_packages(keep_console=False)) - toolbar_bts.append(self.bt_refresh) - self.ref_bt_refresh = self.toolbar.addWidget(self.bt_refresh) + bt_ref = QPushButton() + bt_ref.setCursor(QCursor(Qt.PointingHandCursor)) + bt_ref.setToolTip(i18n['manage_window.bt.refresh.tooltip']) + bt_ref.setIcon(QIcon(resource.get_path('img/refresh.svg'))) + bt_ref.setText(self.i18n['manage_window.bt.refresh.text']) + bt_ref.setStyleSheet(toolbar_button_style()) + bt_ref.clicked.connect(self.begin_refresh_packages) + bt_ref.sizePolicy().setRetainSizeWhenHidden(True) + toolbar_bts.append(bt_ref) + self.comp_manager.register_component(BT_REFRESH, bt_ref, self.toolbar.addWidget(bt_ref)) self.bt_upgrade = QPushButton() self.bt_upgrade.setCursor(QCursor(Qt.PointingHandCursor)) @@ -240,9 +287,10 @@ class ManageWindow(QWidget): self.bt_upgrade.setIcon(QIcon(resource.get_path('img/app_update.svg'))) self.bt_upgrade.setText(i18n['manage_window.bt.upgrade.text']) self.bt_upgrade.setStyleSheet(toolbar_button_style(GREEN, 'white')) - self.bt_upgrade.clicked.connect(self.update_selected) + self.bt_upgrade.clicked.connect(self.upgrade_selected) + self.bt_upgrade.sizePolicy().setRetainSizeWhenHidden(True) toolbar_bts.append(self.bt_upgrade) - self.ref_bt_upgrade = self.toolbar.addWidget(self.bt_upgrade) + self.comp_manager.register_component(BT_UPGRADE, self.bt_upgrade, self.toolbar.addWidget(self.bt_upgrade)) # setting all buttons to the same size: bt_biggest_size = 0 @@ -271,12 +319,11 @@ class ManageWindow(QWidget): toolbar_console = QToolBar() - self.checkbox_console = QCheckBox() - self.checkbox_console.setCursor(QCursor(Qt.PointingHandCursor)) - self.checkbox_console.setText(self.i18n['manage_window.checkbox.show_details']) - self.checkbox_console.stateChanged.connect(self._handle_console) - self.checkbox_console.setVisible(False) - self.ref_checkbox_console = toolbar_console.addWidget(self.checkbox_console) + self.check_console = QCheckBox() + self.check_console.setCursor(QCursor(Qt.PointingHandCursor)) + self.check_console.setText(self.i18n['manage_window.checkbox.show_details']) + self.check_console.stateChanged.connect(self._handle_console) + self.comp_manager.register_component(CHECK_CONSOLE, self.check_console, toolbar_console.addWidget(self.check_console)) toolbar_console.addWidget(new_spacer()) @@ -302,19 +349,19 @@ class ManageWindow(QWidget): self._change_label_substatus('') self.thread_update = self._bind_async_action(UpgradeSelected(self.manager, self.i18n), finished_call=self._finish_upgrade_selected) - self.thread_refresh = self._bind_async_action(RefreshApps(self.manager), finished_call=self._finish_refresh_apps, only_finished=True) + self.thread_refresh = self._bind_async_action(RefreshApps(self.manager), finished_call=self._finish_refresh_packages, only_finished=True) self.thread_uninstall = self._bind_async_action(UninstallPackage(self.manager, self.icon_cache, self.i18n), finished_call=self._finish_uninstall) - self.thread_get_info = self._bind_async_action(GetAppInfo(self.manager), finished_call=self._finish_get_info) - self.thread_get_history = self._bind_async_action(GetAppHistory(self.manager, self.i18n), finished_call=self._finish_get_history) + self.thread_show_info = self._bind_async_action(ShowPackageInfo(self.manager), finished_call=self._finish_show_info) + self.thread_show_history = self._bind_async_action(ShowPackageHistory(self.manager, self.i18n), finished_call=self._finish_show_history) self.thread_search = self._bind_async_action(SearchPackages(self.manager), finished_call=self._finish_search, only_finished=True) - self.thread_downgrade = self._bind_async_action(DowngradeApp(self.manager, self.i18n), finished_call=self._finish_downgrade) - self.thread_suggestions = self._bind_async_action(FindSuggestions(man=self.manager), finished_call=self._finish_search, only_finished=True) - self.thread_run_app = self._bind_async_action(LaunchApp(self.manager), finished_call=self._finish_run_app, only_finished=False) - self.thread_custom_action = self._bind_async_action(CustomAction(manager=self.manager, i18n=self.i18n), finished_call=self._finish_custom_action) - self.thread_screenshots = self._bind_async_action(GetScreenshots(self.manager), finished_call=self._finish_get_screenshots) + self.thread_downgrade = self._bind_async_action(DowngradePackage(self.manager, self.i18n), finished_call=self._finish_downgrade) + self.thread_suggestions = self._bind_async_action(FindSuggestions(man=self.manager), finished_call=self._finish_load_suggestions, only_finished=True) + self.thread_launch = self._bind_async_action(LaunchPackage(self.manager), finished_call=self._finish_launch_package, only_finished=False) + self.thread_custom_action = self._bind_async_action(CustomAction(manager=self.manager, i18n=self.i18n), finished_call=self._finish_execute_custom_action) + self.thread_screenshots = self._bind_async_action(ShowScreenshots(self.manager), finished_call=self._finish_show_screenshots) self.thread_apply_filters = ApplyFilters() - self.thread_apply_filters.signal_finished.connect(self._finish_apply_filters_async) + self.thread_apply_filters.signal_finished.connect(self._finish_apply_filters) self.thread_apply_filters.signal_table.connect(self._update_table_and_upgrades) self.signal_table_update.connect(self.thread_apply_filters.stop_waiting) @@ -353,19 +400,19 @@ class ManageWindow(QWidget): i18n=self.i18n, tooltip=self.i18n['manage_window.bt_custom_actions.tip']) bt_custom_actions.setVisible(bool(self.custom_actions)) - self.ref_bt_custom_actions = self.toolbar_bottom.addWidget(bt_custom_actions) + self.comp_manager.register_component(BT_CUSTOM_ACTIONS, bt_custom_actions, self.toolbar_bottom.addWidget(bt_custom_actions)) bt_settings = IconButton(QIcon(resource.get_path('img/settings.svg')), action=self.show_settings, i18n=self.i18n, tooltip=self.i18n['manage_window.bt_settings.tooltip']) - self.ref_bt_settings = self.toolbar_bottom.addWidget(bt_settings) + self.comp_manager.register_component(BT_SETTINGS, bt_settings, self.toolbar_bottom.addWidget(bt_settings)) bt_about = IconButton(QIcon(resource.get_path('img/info.svg')), action=self._show_about, i18n=self.i18n, tooltip=self.i18n['manage_window.settings.about']) - self.ref_bt_about = self.toolbar_bottom.addWidget(bt_about) + self.comp_manager.register_component(BT_ABOUT, bt_about, self.toolbar_bottom.addWidget(bt_about)) self.layout.addWidget(self.toolbar_bottom) @@ -382,6 +429,7 @@ class ManageWindow(QWidget): self.dialog_about = None self.load_suggestions = bool(config['suggestions']['enabled']) + self.suggestions_requested = False self.first_refresh = True self.thread_warnings = ListWarnings(man=manager, i18n=i18n) @@ -393,6 +441,25 @@ class ManageWindow(QWidget): self.thread_load_installed.signal_loaded.connect(self._finish_loading_installed) self.setMinimumHeight(int(screen_size.height() * 0.5)) self.setMinimumWidth(int(screen_size.width() * 0.6)) + self._register_groups() + + def _register_groups(self): + filters = (CHECK_APPS, CHECK_UPDATES, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME) + self.comp_manager.register_group(GROUP_FILTERS, False, *filters) + + self.comp_manager.register_group(GROUP_VIEW_SEARCH, False, + COMBO_CATEGORIES, COMBO_TYPES, INP_NAME, # filters + BT_INSTALLED, BT_SUGGESTIONS) # buttons + + self.comp_manager.register_group(GROUP_VIEW_INSTALLED, False, + BT_SUGGESTIONS, BT_REFRESH, BT_UPGRADE, # buttons + *filters) + + self.comp_manager.register_group(GROUP_UPPER_BAR, False, + CHECK_APPS, CHECK_UPDATES, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME, + BT_INSTALLED, BT_SUGGESTIONS, BT_REFRESH, BT_UPGRADE) + + self.comp_manager.register_group(GROUP_LOWER_BTS, False, BT_ABOUT, BT_SETTINGS, BT_CUSTOM_ACTIONS) def update_custom_actions(self): self.custom_actions = self.manager.get_custom_actions() @@ -401,33 +468,40 @@ class ManageWindow(QWidget): if self.progress_controll_enabled: self.thread_animate_progress.set_progress(val) - def apply_filters_async(self): + def _change_status(self, status: str = None): + if status: + self.label_status.setText(status + '...') + self.label_status.setCursor(QCursor(Qt.WaitCursor)) + else: + self.label_status.setText('') + self.label_status.unsetCursor() + + def _set_table_enabled(self, enabled: bool): + self.table_apps.setEnabled(enabled) + if enabled: + self.table_container.unsetCursor() + else: + self.table_container.setCursor(QCursor(Qt.WaitCursor)) + + def begin_apply_filters(self): self.thread_notify_pkgs_ready.work = False self.thread_notify_pkgs_ready.wait(5) - self.label_status.setText(self.i18n['manage_window.status.filtering'] + '...') - self.label_status.setCursor(QCursor(Qt.WaitCursor)) - - self.ref_toolbar_search.setVisible(False) - - if self.ref_input_name_filter.isVisible(): - self.input_name_filter.setReadOnly(True) + self._begin_action(action_label=self.i18n['manage_window.status.filtering'], + action_id=ACTION_APPLY_FILTERS) + self.comp_manager.disable_visible_from_groups(GROUP_UPPER_BAR, GROUP_LOWER_BTS) + self.comp_manager.set_component_read_only(INP_NAME, True) self.thread_apply_filters.filters = self._gen_filters() self.thread_apply_filters.pkgs = self.pkgs_available self.thread_apply_filters.start() - self.table_container.setCursor(QCursor(Qt.WaitCursor)) - self.table_apps.setEnabled(False) - self.checkbox_only_apps.setEnabled(False) - self.combo_categories.setEnabled(False) - self.combo_filter_type.setEnabled(False) - self.input_name_filter.setEnabled(False) - self.checkbox_updates.setEnabled(False) self.setFocus(Qt.NoFocusReason) + def _finish_apply_filters(self): + self._finish_action(ACTION_APPLY_FILTERS) + self.update_bt_upgrade() + def _update_table_and_upgrades(self, pkgs_info: dict): self._update_table(pkgs_info=pkgs_info, signal=True) - self.table_apps.setEnabled(False) - self.update_bt_upgrade(pkgs_info) if self.pkgs: self._update_state_when_pkgs_ready() @@ -437,22 +511,6 @@ class ManageWindow(QWidget): self.thread_notify_pkgs_ready.work = True self.thread_notify_pkgs_ready.start() - def _finish_apply_filters_async(self, success: bool): - self.table_container.unsetCursor() - self.table_apps.setEnabled(True) - self.checkbox_only_apps.setEnabled(True) - self.checkbox_updates.setEnabled(True) - self.combo_categories.setEnabled(True) - self.combo_filter_type.setEnabled(True) - self.input_name_filter.setEnabled(True) - self.table_apps.setEnabled(True) - self.label_status.setText('') - self.label_status.unsetCursor() - self.ref_toolbar_search.setVisible(True) - - if self.ref_input_name_filter.isVisible(): - self.input_name_filter.setReadOnly(False) - def _bind_async_action(self, action: AsyncAction, finished_call, only_finished: bool = False) -> AsyncAction: action.signal_finished.connect(finished_call) @@ -488,7 +546,7 @@ class ManageWindow(QWidget): def _pause_and_ask_root_password(self): self.thread_animate_progress.pause() - password, valid = root.ask_root_password(self.context, self.i18n) + password, valid = root.ask_root_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) self.thread_animate_progress.animate() self.signal_root_password.emit(password, valid) @@ -514,18 +572,31 @@ class ManageWindow(QWidget): self.thread_warnings.start() def _begin_loading_installed(self): - if self.pkgs_installed: - self.search_performed = False - self.ref_bt_upgrade.setVisible(True) - self.ref_checkbox_only_apps.setVisible(True) - self.input_search.setText('') - self.input_name_filter.setText('') - self._begin_action(self.i18n['manage_window.status.installed'], keep_bt_installed=False) - self.thread_load_installed.start() + self.inp_search.setText('') + self.input_name.setText('') + self._begin_action(self.i18n['manage_window.status.installed']) + self.comp_manager.set_components_visible(False) + self.suggestions_requested = False + self.search_performed = False + self.thread_load_installed.start() def _finish_loading_installed(self): - self.finish_action() + self._finish_action() + self.comp_manager.set_group_visible(GROUP_VIEW_INSTALLED, True) self.update_pkgs(new_pkgs=None, as_installed=True) + self._hide_filters_no_packages() + self._update_bts_installed_and_suggestions() + self._set_lower_buttons_visible(True) + self._reorganize() + + def _update_bts_installed_and_suggestions(self): + available_types = len(self.manager.get_managed_types()) + self.comp_manager.set_component_visible(BT_INSTALLED, available_types > 0 and any([self.suggestions_requested, self.search_performed])) + self.comp_manager.set_component_visible(BT_SUGGESTIONS, available_types > 0) + + def _hide_filters_no_packages(self): + if not self.pkgs: + self.comp_manager.set_group_visible(GROUP_FILTERS, False) def _show_about(self): if self.dialog_about is None: @@ -535,27 +606,27 @@ class ManageWindow(QWidget): def _handle_updates_filter(self, status: int): self.filter_updates = status == 2 - self.apply_filters_async() + self.begin_apply_filters() def _handle_filter_only_apps(self, status: int): self.filter_only_apps = status == 2 - self.apply_filters_async() + self.begin_apply_filters() def _handle_type_filter(self, idx: int): self.type_filter = self.combo_filter_type.itemData(idx) self.combo_filter_type.adjustSize() - self.apply_filters_async() + self.begin_apply_filters() def _handle_category_filter(self, idx: int): self.category_filter = self.combo_categories.itemData(idx) - self.apply_filters_async() + self.begin_apply_filters() def _update_state_when_pkgs_ready(self): if self.progress_bar.isVisible(): return self._reload_categories() - self._resize() + self._reorganize() def _update_package_data(self, idx: int): if self.table_apps.isEnabled(): @@ -590,66 +661,71 @@ class ManageWindow(QWidget): self.textarea_output.hide() def _handle_console_option(self, enable: bool): - if enable: self.textarea_output.clear() - self.ref_checkbox_console.setVisible(enable) - self.checkbox_console.setChecked(False) + self.comp_manager.set_component_visible(CHECK_CONSOLE, True) + self.check_console.setChecked(False) self.textarea_output.hide() - def refresh_packages(self, keep_console: bool = True, pkg_types: Set[Type[SoftwarePackage]] = None): - self.input_search.clear() + def begin_refresh_packages(self, pkg_types: Set[Type[SoftwarePackage]] = None): + self.inp_search.clear() - if not keep_console: - self._handle_console_option(False) + self._begin_action(self.i18n['manage_window.status.refreshing']) + self.comp_manager.set_components_visible(False) - self.ref_checkbox_updates.setVisible(False) - self.ref_checkbox_only_apps.setVisible(False) - self._begin_action(self.i18n['manage_window.status.refreshing'], keep_bt_installed=False) + self.suggestions_requested = False + self.search_performed = False self.thread_refresh.pkg_types = pkg_types self.thread_refresh.start() - def read_suggestions(self): - self.input_search.clear() - self._handle_console_option(False) - self.ref_checkbox_updates.setVisible(False) - self.ref_checkbox_only_apps.setVisible(False) - self._begin_action(self.i18n['manage_window.status.suggestions'], keep_bt_installed=False) - self.thread_suggestions.filter_installed = True - self.thread_suggestions.start() + def _finish_refresh_packages(self, res: dict, as_installed: bool = True): + self._finish_action() + self._set_lower_buttons_visible(True) + self.comp_manager.set_component_visible(SEARCH_BAR, True) - def _finish_refresh_apps(self, res: dict, as_installed: bool = True): - self.finish_action() - self.search_performed = False + if self.search_performed or self.suggestions_requested: + self.comp_manager.set_group_visible(GROUP_VIEW_SEARCH, True) + else: + self.comp_manager.set_group_visible(GROUP_VIEW_INSTALLED, True) + + if self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types']): + self._hide_filters_no_packages() + self._update_bts_installed_and_suggestions() + self._reorganize() - self.ref_checkbox_only_apps.setVisible(bool(res['installed'])) - self.ref_bt_upgrade.setVisible(True) - self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types']) self.load_suggestions = False self.types_changed = False - def uninstall_package(self, pkg: PackageView): + def _begin_load_suggestions(self, filter_installed: bool): + self.inp_search.clear() + self._begin_action(self.i18n['manage_window.status.suggestions']) + self.comp_manager.set_components_visible(False) + self.suggestions_requested = True + self.thread_suggestions.filter_installed = filter_installed + self.thread_suggestions.start() + + def _finish_load_suggestions(self, res: dict): + self._finish_search(res) + + def begin_uninstall(self, pkg: PackageView): pwd, proceed = self._ask_root_password('uninstall', pkg) if not proceed: return + self._begin_action(action_label='{} {}'.format(self.i18n['manage_window.status.uninstalling'], pkg.model.name), + action_id=ACTION_UNINSTALL) + self.comp_manager.set_groups_visible(False, GROUP_UPPER_BAR, GROUP_LOWER_BTS) self._handle_console_option(True) - self._begin_action('{} {}'.format(self.i18n['manage_window.status.uninstalling'], pkg.model.name), clear_filters=False) self.thread_uninstall.pkg = pkg self.thread_uninstall.root_pwd = pwd self.thread_uninstall.start() - def run_app(self, app: PackageView): - self._begin_action(self.i18n['manage_window.status.running_app'].format(app.model.name)) - self.thread_run_app.app = app - self.thread_run_app.start() - def _finish_uninstall(self, res: dict): - self.finish_action(keep_filters=True) + self._finish_action(action_id=ACTION_UNINSTALL) if res['success']: src_pkg = res['pkg'] @@ -687,47 +763,37 @@ class ManageWindow(QWidget): for decrement, idx in enumerate(removed_idxs): self.table_apps.removeRow(idx - decrement) - for new_idx, pkgv in enumerate(self.pkgs): # updating the package indexes - pkgv.table_index = new_idx + self._update_table_indexes() self.update_bt_upgrade() self.update_custom_actions() - + self._show_console_checkbox_if_output() notify_tray() else: + self._show_console_errors() + if self._can_notify_user(): util.notify_user('{}: {}'.format(res['pkg'].model.name, self.i18n['notification.uninstall.failed'])) - self.checkbox_console.setChecked(True) + def _update_table_indexes(self): + if self.pkgs: + for new_idx, pkgv in enumerate(self.pkgs): # updating the package indexes + pkgv.table_index = new_idx - for ref in (self.ref_combo_categories, self.ref_input_name_filter): - if not ref.isVisible(): - ref.setVisible(True) + def begin_launch_package(self, pkg: PackageView): + self._begin_action(action_label=self.i18n['manage_window.status.running_app'].format(pkg.model.name), + action_id=ACTION_LAUNCH) + self.comp_manager.disable_visible() + self.thread_launch.pkg = pkg + self.thread_launch.start() - if self.combo_filter_type.count() > 2 and not self.ref_combo_filter_type.isVisible(): - self.ref_combo_filter_type.setVisible(True) + def _finish_launch_package(self, success: bool): + self._finish_action(action_id=ACTION_LAUNCH) def _can_notify_user(self): return bool(self.config['system']['notifications']) and (self.isHidden() or self.isMinimized()) - def _finish_downgrade(self, res: dict): - self.finish_action() - - if res['success']: - if self._can_notify_user(): - util.notify_user('{} {}'.format(res['app'], self.i18n['downgraded'])) - - self.refresh_packages(pkg_types={res['app'].model.__class__} if len(self.pkgs) > 1 else None) - self.update_custom_actions() - - notify_tray() - else: - if self._can_notify_user(): - util.notify_user(self.i18n['notification.downgrade.failed']) - - self.checkbox_console.setChecked(True) - def _change_label_status(self, status: str): self.label_status.setText(status) @@ -738,6 +804,12 @@ class ManageWindow(QWidget): elif not self.toolbar_substatus.isVisible() and self.progress_bar.isVisible(): self.toolbar_substatus.show() + def _reorganize(self): + if not self._maximized: + self.table_apps.change_headers_policy(QHeaderView.Stretch) + self.table_apps.change_headers_policy() + self._resize(accept_lower_width=len(self.pkgs) > 0) + def _update_table(self, pkgs_info: dict, signal: bool = False): self.pkgs = pkgs_info['pkgs_displayed'] @@ -757,33 +829,32 @@ class ManageWindow(QWidget): def update_bt_upgrade(self, pkgs_info: dict = None): show_bt_upgrade = False - if not pkgs_info or pkgs_info['not_installed'] == 0: - for app_v in (pkgs_info['pkgs_displayed'] if pkgs_info else self.pkgs): - if not app_v.model.is_update_ignored() and app_v.update_checked: + if not any([self.suggestions_requested, self.search_performed]) and (not pkgs_info or pkgs_info['not_installed'] == 0): + for pkg in (pkgs_info['pkgs_displayed'] if pkgs_info else self.pkgs): + if not pkg.model.is_update_ignored() and pkg.update_checked: show_bt_upgrade = True break - self.ref_bt_upgrade.setVisible(show_bt_upgrade) + self.comp_manager.set_component_visible(BT_UPGRADE, show_bt_upgrade) def change_update_state(self, pkgs_info: dict, trigger_filters: bool = True, keep_selected: bool = False): self.update_bt_upgrade(pkgs_info) if pkgs_info['updates'] > 0: - if pkgs_info['not_installed'] == 0: - if not self.ref_checkbox_updates.isVisible(): - self.ref_checkbox_updates.setVisible(True) + if not self.comp_manager.is_visible(CHECK_UPDATES): + self.comp_manager.set_component_visible(CHECK_UPDATES, True) if not self.filter_updates and not keep_selected: - self._change_checkbox(self.checkbox_updates, True, 'filter_updates', trigger_filters) + self._change_checkbox(self.check_updates, True, 'filter_updates', trigger_filters) if pkgs_info['napp_updates'] > 0 and self.filter_only_apps and not keep_selected: - self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger_filters) + self._change_checkbox(self.check_apps, False, 'filter_only_apps', trigger_filters) else: if not keep_selected: - self._change_checkbox(self.checkbox_updates, False, 'filter_updates', trigger_filters) + self._change_checkbox(self.check_updates, False, 'filter_updates', trigger_filters) - self.ref_checkbox_updates.setVisible(False) + self.comp_manager.set_component_visible(CHECK_UPDATES, False) def _change_checkbox(self, checkbox: QCheckBox, checked: bool, attr: str = None, trigger: bool = True): if not trigger: @@ -801,12 +872,12 @@ class ManageWindow(QWidget): 'type': self.type_filter, 'category': self.category_filter, 'updates': False if ignore_updates else self.filter_updates, - 'name': self.input_name_filter.get_text().lower() if self.input_name_filter.get_text() else None, + 'name': self.input_name.get_text().lower() if self.input_name.get_text() else None, 'display_limit': None if self.filter_updates else self.display_limit } - def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False, keep_filters: bool = False): - self.input_name_filter.setText('') + def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False, keep_filters: bool = False) -> bool: + self.input_name.setText('') pkgs_info = commons.new_pkgs_info() filters = self._gen_filters(ignore_updates=ignore_updates) @@ -838,18 +909,17 @@ class ManageWindow(QWidget): if as_installed: self.pkgs_installed = pkgs_info['pkgs'] - self._begin_search('') - self.thread_suggestions.filter_installed = False - self.thread_suggestions.start() - return + self._begin_load_suggestions(filter_installed=False) + self.load_suggestions = False + return False else: if not keep_filters: - self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False) - self.checkbox_only_apps.setCheckable(False) + self._change_checkbox(self.check_apps, False, 'filter_only_apps', trigger=False) + self.check_apps.setCheckable(False) else: if not keep_filters: - self.checkbox_only_apps.setCheckable(True) - self._change_checkbox(self.checkbox_only_apps, True, 'filter_only_apps', trigger=False) + self.check_apps.setCheckable(True) + self._change_checkbox(self.check_apps, True, 'filter_only_apps', trigger=False) self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) self._update_categories(pkgs_info['categories'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) @@ -863,45 +933,44 @@ class ManageWindow(QWidget): self.pkgs_installed = pkgs_info['pkgs'] self.pkgs = pkgs_info['pkgs_displayed'] - - if self.pkgs: - self.ref_input_name_filter.setVisible(True) - self._update_table(pkgs_info=pkgs_info) if new_pkgs: self.thread_notify_pkgs_ready.pkgs = self.pkgs self.thread_notify_pkgs_ready.start() - if self.pkgs_installed: - self.ref_bt_installed.setVisible(not as_installed) - self._resize(accept_lower_width=bool(self.pkgs_installed)) if self.first_refresh: qt_utils.centralize(self) self.first_refresh = False + return True + def _apply_filters(self, pkgs_info: dict, ignore_updates: bool): pkgs_info['pkgs_displayed'] = [] filters = self._gen_filters(ignore_updates=ignore_updates) for pkgv in pkgs_info['pkgs']: commons.apply_filters(pkgv, filters, pkgs_info) - def _update_type_filters(self, available_types: dict = None, keep_selected: bool = False): + def _clean_combo_types(self): + if self.combo_filter_type.count() > 1: + for _ in range(self.combo_filter_type.count() - 1): + self.combo_filter_type.removeItem(1) + def _update_type_filters(self, available_types: dict = None, keep_selected: bool = False): if available_types is None: - self.ref_combo_filter_type.setVisible(self.combo_filter_type.count() > 2) + self.comp_manager.set_component_visible(COMBO_TYPES, self.combo_filter_type.count() > 2) else: keeping_selected = keep_selected and available_types and self.type_filter in available_types if not keeping_selected: self.type_filter = self.any_type_filter + if not available_types: + self._clean_combo_types() if available_types: - if self.combo_filter_type.count() > 1: - for _ in range(self.combo_filter_type.count() - 1): - self.combo_filter_type.removeItem(1) + self._clean_combo_types() sel_type = -1 for idx, item in enumerate(available_types.items()): @@ -921,13 +990,13 @@ class ManageWindow(QWidget): self.combo_filter_type.blockSignals(True) self.combo_filter_type.setCurrentIndex(sel_type if sel_type > -1 else 0) self.combo_filter_type.blockSignals(False) - self.ref_combo_filter_type.setVisible(len(available_types) > 1) + self.comp_manager.set_component_visible(COMBO_TYPES, len(available_types) > 1) else: - self.ref_combo_filter_type.setVisible(False) + self.comp_manager.set_component_visible(COMBO_TYPES, False) def _update_categories(self, categories: Set[str] = None, keep_selected: bool = False): if categories is None: - self.ref_combo_categories.setVisible(self.combo_categories.count() > 1) + self.comp_manager.set_component_visible(COMBO_CATEGORIES, self.combo_categories.count() > 1) else: keeping_selected = keep_selected and categories and self.category_filter in categories @@ -944,8 +1013,7 @@ class ManageWindow(QWidget): cat_list.sort() for idx, c in enumerate(cat_list): - i18n_cat = self.i18n.get('category.{}'.format(c), self.i18n.get(c, c)) - self.combo_categories.addItem(i18n_cat.capitalize(), c) + self.__add_category(c) if keeping_selected and c == self.category_filter: selected_cat = idx + 1 @@ -953,10 +1021,18 @@ class ManageWindow(QWidget): self.combo_categories.blockSignals(True) self.combo_categories.setCurrentIndex(selected_cat if selected_cat > -1 else 0) self.combo_categories.blockSignals(False) - self.ref_combo_categories.setVisible(True) + self.comp_manager.set_component_visible(COMBO_CATEGORIES, True) else: - self.ref_combo_categories.setVisible(False) + self.comp_manager.set_component_visible(COMBO_CATEGORIES, False) + + def __add_category(self, category: str): + i18n_cat = self.i18n.get('category.{}'.format(category), self.i18n.get(category, category)) + self.combo_categories.addItem(i18n_cat.capitalize(), category) + + def _get_current_categories(self) -> Set[str]: + if self.combo_categories.count() > 1: + return {self.combo_categories.itemData(idx) for idx in range(self.combo_categories.count()) if idx > 0} def _resize(self, accept_lower_width: bool = True): table_width = self.table_apps.get_width() @@ -970,21 +1046,26 @@ class ManageWindow(QWidget): self.resize(new_width, self.height()) def set_progress_controll(self, enabled: bool): - self.progress_controll_enabled = enumerate + self.progress_controll_enabled = enabled - def update_selected(self): + def upgrade_selected(self): if dialog.ask_confirmation(title=self.i18n['manage_window.upgrade_all.popup.title'], body=self.i18n['manage_window.upgrade_all.popup.body'], i18n=self.i18n, - widgets=[UpdateToggleButton(None, self, self.i18n, clickable=False)]): + widgets=[UpdateToggleButton(pkg=None, + root=self, + i18n=self.i18n, + clickable=False)]): + self._begin_action(action_label=self.i18n['manage_window.status.upgrading'], + action_id=ACTION_UPGRADE) + self.comp_manager.set_components_visible(False) self._handle_console_option(True) - self._begin_action(self.i18n['manage_window.status.upgrading']) self.thread_update.pkgs = self.pkgs self.thread_update.start() def _finish_upgrade_selected(self, res: dict): - self.finish_action() + self._finish_action() if res.get('id'): output = self.textarea_output.toPlainText() @@ -1002,131 +1083,126 @@ class ManageWindow(QWidget): traceback.print_exc() if res['success']: + self.comp_manager.remove_saved_state(ACTION_UPGRADE) + self.begin_refresh_packages(pkg_types=res['types']) + self._show_console_checkbox_if_output() + if self._can_notify_user(): util.notify_user('{} {}'.format(res['updated'], self.i18n['notification.update_selected.success'])) - self.refresh_packages(pkg_types=res['types']) - notify_tray() else: + self.comp_manager.restore_state(ACTION_UPGRADE) + self._show_console_errors() + if self._can_notify_user(): util.notify_user(self.i18n['notification.update_selected.failed']) - self.ref_bt_upgrade.setVisible(True) - self.checkbox_console.setChecked(True) - self.update_custom_actions() + def _show_console_errors(self): + if self.textarea_output.toPlainText(): + self.check_console.setChecked(True) + else: + self._handle_console_option(False) + self.comp_manager.set_component_visible(CHECK_CONSOLE, False) + def _update_action_output(self, output: str): self.textarea_output.appendPlainText(output) - def _begin_action(self, action_label: str, keep_search: bool = False, keep_bt_installed: bool = True, clear_filters: bool = False): - self.table_container.setCursor(QCursor(Qt.WaitCursor)) - self.ref_input_name_filter.setVisible(False) - self.ref_combo_filter_type.setVisible(False) - self.ref_combo_categories.setVisible(False) - self.ref_bt_custom_actions.setVisible(False) - self.ref_bt_settings.setVisible(False) - self.ref_bt_about.setVisible(False) + def _begin_action(self, action_label: str, action_id: int = None): self.thread_animate_progress.stop = False self.thread_animate_progress.start() self.ref_progress_bar.setVisible(True) - self.label_status.setText(action_label + "...") - self.label_status.setCursor(QCursor(Qt.WaitCursor)) - self.ref_bt_upgrade.setVisible(False) - self.ref_bt_refresh.setVisible(False) + if action_id is not None: + self.comp_manager.save_states(action_id, only_visible=True) - if self.ref_bt_suggestions: - self.ref_bt_suggestions.setVisible(False) + self._set_table_enabled(False) + self.comp_manager.set_component_visible(SEARCH_BAR, False) + self._change_status(action_label) - self.checkbox_only_apps.setEnabled(False) - self.table_apps.setEnabled(False) - self.checkbox_updates.setEnabled(False) + def _set_lower_buttons_visible(self, visible: bool): + self.comp_manager.set_group_visible(GROUP_LOWER_BTS, visible) - if not keep_bt_installed: - self.ref_bt_installed.setVisible(False) - elif self.ref_bt_installed.isVisible(): - self.ref_bt_installed.setEnabled(False) + if visible: + self.comp_manager.set_component_visible(BT_CUSTOM_ACTIONS, bool(self.custom_actions)) - if keep_search: - self.ref_toolbar_search.setVisible(True) - else: - self.ref_toolbar_search.setVisible(False) - - if clear_filters: - self._update_type_filters({}) - self._update_categories(set()) - else: - self.combo_filter_type.setEnabled(False) - - def finish_action(self, keep_filters: bool = False): - self.table_container.unsetCursor() + def _finish_action(self, action_id: int = None): self.thread_animate_progress.stop = True self.thread_animate_progress.wait(msecs=1000) + self.ref_progress_bar.setVisible(False) self.progress_bar.setValue(0) self.progress_bar.setTextVisible(False) - self._change_label_substatus('') - self.ref_bt_custom_actions.setVisible(bool(self.custom_actions)) - self.ref_bt_settings.setVisible(True) - self.ref_bt_about.setVisible(True) + if action_id is not None: + self.comp_manager.restore_state(action_id) - self.ref_bt_refresh.setVisible(True) - self.checkbox_only_apps.setEnabled(True) - self.table_apps.setEnabled(True) - self.input_search.setEnabled(True) - self.label_status.setText('') - self.label_status.unsetCursor() - self.label_substatus.setText('') - self.ref_toolbar_search.setVisible(True) - self.ref_toolbar_search.setEnabled(True) - self.combo_filter_type.setEnabled(True) - self.checkbox_updates.setEnabled(True) + self.comp_manager.set_component_visible(SEARCH_BAR, True) + self._change_status() + self._change_label_substatus('') + self._set_table_enabled(True) self.progress_controll_enabled = True - if self.ref_bt_suggestions: - self.ref_bt_suggestions.setVisible(True) - - if self.pkgs: - self.ref_input_name_filter.setVisible(True) - self.update_bt_upgrade() - self._update_type_filters(keep_selected=keep_filters) - self._update_categories(keep_selected=keep_filters) - - if self.ref_bt_installed.isVisible(): - self.ref_bt_installed.setEnabled(True) - - def downgrade(self, pkgv: PackageView): - pwd, proceed = self._ask_root_password('downgrade', pkgv) + def begin_downgrade(self, pkg: PackageView): + pwd, proceed = self._ask_root_password('downgrade', pkg) if not proceed: return + self._begin_action(action_label='{} {}'.format(self.i18n['manage_window.status.downgrading'], pkg.model.name), + action_id=ACTION_DOWNGRADE) + self.comp_manager.set_components_visible(False) self._handle_console_option(True) - self._begin_action('{} {}'.format(self.i18n['manage_window.status.downgrading'], pkgv.model.name)) - self.thread_downgrade.app = pkgv + self.thread_downgrade.pkg = pkg self.thread_downgrade.root_pwd = pwd self.thread_downgrade.start() - def get_app_info(self, pkg: dict): - self._handle_console_option(False) - self._begin_action(self.i18n['manage_window.status.info']) + def _finish_downgrade(self, res: dict): + self._finish_action() - self.thread_get_info.app = pkg - self.thread_get_info.start() + if res['success']: + self.comp_manager.remove_saved_state(ACTION_DOWNGRADE) - def get_screenshots(self, pkg: PackageView): - self._handle_console_option(False) - self._begin_action(self.i18n['manage_window.status.screenshots'].format(bold(pkg.model.name))) + if self._can_notify_user(): + util.notify_user('{} {}'.format(res['app'], self.i18n['downgraded'])) + + self.begin_refresh_packages(pkg_types={res['app'].model.__class__} if len(self.pkgs) > 1 else None) + self._show_console_checkbox_if_output() + self.update_custom_actions() + notify_tray() + else: + self.comp_manager.restore_state(ACTION_DOWNGRADE) + self._show_console_errors() + + if self._can_notify_user(): + util.notify_user(self.i18n['notification.downgrade.failed']) + + def begin_show_info(self, pkg: dict): + self._begin_action(self.i18n['manage_window.status.info'], action_id=ACTION_INFO) + self.comp_manager.disable_visible() + + self.thread_show_info.pkg = pkg + self.thread_show_info.start() + + def _finish_show_info(self, pkg_info: dict): + self._finish_action(action_id=ACTION_INFO) + dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache, + i18n=self.i18n, screen_size=self.screen_size) + dialog_info.exec_() + + def begin_show_screenshots(self, pkg: PackageView): + self._begin_action(action_label=self.i18n['manage_window.status.screenshots'].format(bold(pkg.model.name)), + action_id=ACTION_SCREENSHOTS) + self.comp_manager.disable_visible() self.thread_screenshots.pkg = pkg self.thread_screenshots.start() - def _finish_get_screenshots(self, res: dict): - self.finish_action() + def _finish_show_screenshots(self, res: dict): + self._finish_action(ACTION_SCREENSHOTS) if res.get('screenshots'): diag = ScreenshotsDialog(pkg=res['pkg'], @@ -1141,25 +1217,20 @@ class ManageWindow(QWidget): body=self.i18n['popup.screenshots.no_screenshot.body'].format(bold(res['pkg'].model.name)), type_=MessageType.ERROR) - def get_app_history(self, app: PackageView): - self._handle_console_option(False) - self._begin_action(self.i18n['manage_window.status.history']) + def begin_show_history(self, pkg: PackageView): + self._begin_action(self.i18n['manage_window.status.history'], action_id=ACTION_HISTORY) + self.comp_manager.disable_visible() - self.thread_get_history.app = app - self.thread_get_history.start() + self.thread_show_history.pkg = pkg + self.thread_show_history.start() - def _finish_get_info(self, app_info: dict): - self.finish_action() - dialog_info = InfoDialog(app=app_info, icon_cache=self.icon_cache, i18n=self.i18n, screen_size=self.screen_size) - dialog_info.exec_() - - def _finish_get_history(self, res: dict): - self.finish_action() + def _finish_show_history(self, res: dict): + self._finish_action(ACTION_HISTORY) if res.get('error'): self._handle_console_option(True) self.textarea_output.appendPlainText(res['error']) - self.checkbox_console.setChecked(True) + self.check_console.setChecked(True) elif not res['history'].history: dialog.show_message(title=self.i18n['action.history.no_history.title'], body=self.i18n['action.history.no_history.body'].format(bold(res['history'].pkg.name)), @@ -1168,28 +1239,31 @@ class ManageWindow(QWidget): dialog_history = HistoryDialog(res['history'], self.icon_cache, self.i18n) dialog_history.exec_() - def _begin_search(self, word): - self._handle_console_option(False) - self.ref_checkbox_only_apps.setVisible(False) - self.ref_checkbox_updates.setVisible(False) + def _begin_search(self, word, action_id: int = None): self.filter_updates = False - self._begin_action('{} {}'.format(self.i18n['manage_window.status.searching'], word if word else ''), clear_filters=True) + self._begin_action('{} {}'.format(self.i18n['manage_window.status.searching'], word if word else ''), action_id=action_id) def search(self): - word = self.input_search.text().strip() + word = self.inp_search.text().strip() if word: - self._begin_search(word) + self._begin_search(word, action_id=ACTION_SEARCH) + self.comp_manager.set_components_visible(False) self.thread_search.word = word self.thread_search.start() def _finish_search(self, res: dict): - self.finish_action() + self._finish_action() self.search_performed = True if not res['error']: - self.ref_bt_upgrade.setVisible(False) + self.comp_manager.set_group_visible(GROUP_VIEW_SEARCH, True) self.update_pkgs(res['pkgs_found'], as_installed=False, ignore_updates=True) + self._set_lower_buttons_visible(True) + self._update_bts_installed_and_suggestions() + self._hide_filters_no_packages() + self._reorganize() else: + self.comp_manager.restore_state(ACTION_SEARCH) dialog.show_message(title=self.i18n['warning'].capitalize(), body=self.i18n[res['error']], type_=MessageType.WARNING) def _ask_root_password(self, action: str, pkg: PackageView) -> Tuple[str, bool]: @@ -1197,8 +1271,7 @@ class ManageWindow(QWidget): requires_root = self.manager.requires_root(action, pkg.model) if not user.is_root() and requires_root: - pwd, ok = ask_root_password(self.context, self.i18n) - + pwd, ok = ask_root_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) if not ok: return pwd, False @@ -1210,15 +1283,16 @@ class ManageWindow(QWidget): if not proceed: return + self._begin_action('{} {}'.format(self.i18n['manage_window.status.installing'], pkg.model.name), action_id=ACTION_INSTALL) + self.comp_manager.set_groups_visible(False, GROUP_UPPER_BAR, GROUP_LOWER_BTS) self._handle_console_option(True) - self._begin_action('{} {}'.format(self.i18n['manage_window.status.installing'], pkg.model.name)) self.thread_install.pkg = pkg self.thread_install.root_pwd = pwd self.thread_install.start() def _finish_install(self, res: dict): - self.finish_action() + self._finish_action(action_id=ACTION_INSTALL) console_output = self.textarea_output.toPlainText() @@ -1277,9 +1351,9 @@ class ManageWindow(QWidget): # updating installed packages if res['removed'] and self.pkgs_installed: to_remove = [] - for idx, model in enumerate(self.pkgs_installed): - for model in res['removed']: - if model.model == model: + for idx, installed in enumerate(self.pkgs_installed): + for removed in res['removed']: + if installed.model == removed: to_remove.append(idx) if to_remove: @@ -1292,22 +1366,16 @@ class ManageWindow(QWidget): for idx, model in enumerate(res['installed']): self.pkgs_installed.insert(idx, PackageView(model, self.i18n)) - self.ref_checkbox_only_apps.setVisible(False) self.update_custom_actions() else: + self._show_console_errors() if self._can_notify_user(): util.notify_user('{}: {}'.format(res['pkg'].model.name, self.i18n['notification.install.failed'])) - self.checkbox_console.setChecked(True) - def _update_progress(self, value: int): self.progress_bar.setValue(value) - def _finish_run_app(self, success: bool): - self.finish_action() - - def execute_custom_action(self, pkg: PackageView, action: CustomSoftwareAction): - + def begin_execute_custom_action(self, pkg: PackageView, action: CustomSoftwareAction): if pkg is None and not dialog.ask_confirmation(title=self.i18n['confirmation'].capitalize(), body=self.i18n['custom_action.proceed_with'].capitalize().format('"{}"'.format(self.i18n[action.i18_label_key])), icon=QIcon(action.icon_path) if action.icon_path else QIcon(resource.get_path('img/logo.svg')), @@ -1317,26 +1385,41 @@ class ManageWindow(QWidget): pwd = None if not user.is_root() and action.requires_root: - pwd, ok = ask_root_password(self.context, self.i18n) + pwd, ok = ask_root_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) if not ok: return + self._begin_action(action_label='{}{}'.format(self.i18n[action.i18n_status_key], ' {}'.format(pkg.model.name) if pkg else ''), + action_id=ACTION_CUSTOM_ACTION) + self.comp_manager.set_components_visible(False) self._handle_console_option(True) - self._begin_action('{}{}'.format(self.i18n[action.i18n_status_key], ' {}'.format(pkg.model.name) if pkg else '')) self.thread_custom_action.pkg = pkg self.thread_custom_action.root_pwd = pwd self.thread_custom_action.custom_action = action self.thread_custom_action.start() - def _finish_custom_action(self, res: dict): - self.finish_action() + def _finish_execute_custom_action(self, res: dict): + self._finish_action() + if res['success']: if res['action'].refresh: - self.refresh_packages(pkg_types={res['pkg'].model.__class__} if res['pkg'] else None) + self.comp_manager.remove_saved_state(ACTION_CUSTOM_ACTION) + self.begin_refresh_packages(pkg_types={res['pkg'].model.__class__} if res['pkg'] else None) + else: + self.comp_manager.restore_state(ACTION_CUSTOM_ACTION) + + self._show_console_checkbox_if_output() else: - self.checkbox_console.setChecked(True) + self.comp_manager.restore_state(ACTION_CUSTOM_ACTION) + self._show_console_errors() + + def _show_console_checkbox_if_output(self): + if self.textarea_output.toPlainText(): + self.comp_manager.set_component_visible(CHECK_CONSOLE, True) + else: + self.comp_manager.set_component_visible(CHECK_CONSOLE, False) def show_settings(self): if self.settings_window: @@ -1364,7 +1447,7 @@ class ManageWindow(QWidget): except: pass - custom_action.triggered.connect(lambda: self.execute_custom_action(None, action)) + custom_action.triggered.connect(lambda: self.begin_execute_custom_action(None, action)) return custom_action def show_custom_actions(self): @@ -1377,23 +1460,48 @@ class ManageWindow(QWidget): menu_row.popup(QCursor.pos()) menu_row.exec_() - def ignore_updates(self, pkg: PackageView): + def begin_ignore_updates(self, pkg: PackageView): status_key = 'ignore_updates' if not pkg.model.is_update_ignored() else 'ignore_updates_reverse' - self._begin_action(self.i18n['manage_window.status.{}'.format(status_key)].format(pkg.model.name)) + self._begin_action(action_label=self.i18n['manage_window.status.{}'.format(status_key)].format(pkg.model.name), + action_id=ACTION_IGNORE_UPDATES) + self.comp_manager.disable_visible() self.thread_ignore_updates.pkg = pkg self.thread_ignore_updates.start() def finish_ignore_updates(self, res: dict): - self.finish_action() + self._finish_action(action_id=ACTION_IGNORE_UPDATES) if res['success']: - self.apply_filters_async() + hide_package = commons.is_package_hidden(res['pkg'], self._gen_filters()) - if self.pkgs_installed: - cached_installed = [idx for idx, p in enumerate(self.pkgs_installed) if p == res['pkg']] + if hide_package: + idx_to_remove = None + for pkg in self.pkgs: + if pkg == res['pkg']: + idx_to_remove = pkg.table_index + break - for idx in cached_installed: - self.pkgs_installed[idx] = res['pkg'] + if idx_to_remove is not None: + del self.pkgs[idx_to_remove] + self.table_apps.removeRow(idx_to_remove) + self._update_table_indexes() + self.update_bt_upgrade() + else: + for pkg in self.pkgs: + if pkg == res['pkg']: + pkg.update_model(res['pkg'].model) + self.table_apps.update_package(pkg, change_update_col=not any([self.search_performed, self.suggestions_requested])) + self.update_bt_upgrade() + break + + for pkg_list in (self.pkgs_available, self.pkgs_installed): + if pkg_list: + for pkg in pkg_list: + if pkg == res['pkg']: + pkg.update_model(res['pkg'].model) + break + + self._add_pkg_categories(res['pkg']) dialog.show_message(title=self.i18n['success'].capitalize(), body=self.i18n['action.{}.success'.format(res['action'])].format(bold(res['pkg'].model.name)), @@ -1403,4 +1511,21 @@ class ManageWindow(QWidget): body=self.i18n['action.{}.fail'.format(res['action'])].format(bold(res['pkg'].model.name)), type_=MessageType.ERROR) + def _add_pkg_categories(self, pkg: PackageView): + if pkg.model.categories: + pkg_categories = {c.strip().lower() for c in pkg.model.categories if c and c.strip()} + if pkg_categories: + current_categories = self._get_current_categories() + + if current_categories: + pkg_categories = {c.strip().lower() for c in pkg.model.categories if c} + + if pkg_categories: + categories_to_add = {c for c in pkg_categories if c and c not in current_categories} + + if categories_to_add: + for cat in categories_to_add: + self.__add_category(cat) + else: + self._update_categories(pkg_categories)