import logging import operator import time import traceback from pathlib import Path from typing import List, Type, Set, Tuple, Optional from PyQt5.QtCore import QEvent, Qt, pyqtSignal, QSize from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHeaderView, QToolBar, \ QLabel, QPlainTextEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy, \ QMenu, QHBoxLayout from bauh.api import user from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager, SoftwareAction from bauh.api.abstract.model import SoftwarePackage from bauh.api.abstract.view import MessageType from bauh.api.http import HttpClient from bauh.api.paths import LOGS_DIR from bauh.commons.html import bold from bauh.context import set_theme from bauh.stylesheet import read_all_themes_metadata, ThemeMetadata from bauh.view.core.config import CoreConfigManager from bauh.view.core.tray_client import notify_tray from bauh.view.qt import dialog, commons, qt_utils from bauh.view.qt.about import AboutDialog from bauh.view.qt.apps_table import PackagesTable, UpgradeToggleButton from bauh.view.qt.commons import sum_updates_displayed from bauh.view.qt.components import new_spacer, IconButton, QtComponentsManager, to_widget, QSearchBar, \ QCustomMenuAction, QCustomToolbar from bauh.view.qt.dialog import ConfirmationDialog from bauh.view.qt.history import HistoryDialog from bauh.view.qt.info import InfoDialog from bauh.view.qt.root import RootDialog from bauh.view.qt.screenshots import ScreenshotsDialog from bauh.view.qt.settings import SettingsWindow from bauh.view.qt.thread import UpgradeSelected, RefreshApps, UninstallPackage, DowngradePackage, ShowPackageInfo, \ ShowPackageHistory, SearchPackages, InstallPackage, AnimateProgress, NotifyPackagesReady, FindSuggestions, \ ListWarnings, \ AsyncAction, LaunchPackage, ApplyFilters, CustomSoftwareAction, ShowScreenshots, CustomAction, \ NotifyInstalledLoaded, \ IgnorePackageUpdates, SaveTheme, StartAsyncAction from bauh.view.qt.view_model import PackageView, PackageViewStatus from bauh.view.util import util, resource from bauh.view.util.translation import I18n DARK_ORANGE = '#FF4500' # 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_DETAILS = 11 BT_SETTINGS = 12 BT_CUSTOM_ACTIONS = 13 BT_ABOUT = 14 BT_THEMES = 15 # 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(bool, str) signal_table_update = pyqtSignal() signal_stop_notifying = pyqtSignal() def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, screen_size: QSize, config: dict, context: ApplicationContext, http_client: HttpClient, logger: logging.Logger, icon: QIcon): super(ManageWindow, self).__init__() self.setObjectName('manage_window') self.comp_manager = QtComponentsManager() self.i18n = i18n self.logger = logger self.manager = manager self.working = False # restrict the number of threaded actions self.installed_loaded = False # used to control the state when the interface is set to not load the apps on startup self.pkgs = [] # packages current loaded in the table self.pkgs_available = [] # all packages loaded in memory self.pkgs_installed = [] # cached installed packages self.display_limit = config['ui']['table']['max_displayed'] self.icon_cache = icon_cache self.screen_size = screen_size self.config = config self.context = context self.http_client = http_client self.icon_app = icon self.setWindowIcon(self.icon_app) self.layout = QVBoxLayout() self.setLayout(self.layout) self.toolbar_status = QToolBar() self.toolbar_status.setObjectName('toolbar_status') self.toolbar_status.addWidget(new_spacer()) self.label_status = QLabel() self.label_status.setObjectName('label_status') self.label_status.setText('') self.toolbar_status.addWidget(self.label_status) self.search_bar = QSearchBar(search_callback=self.search) self.search_bar.set_placeholder(i18n['window_manage.search_bar.placeholder'] + "...") self.search_bar.set_tooltip(i18n['window_manage.search_bar.tooltip']) self.search_bar.set_button_tooltip(i18n['window_manage.search_bar.button_tooltip']) self.comp_manager.register_component(SEARCH_BAR, self.search_bar, self.toolbar_status.addWidget(self.search_bar)) self.toolbar_status.addWidget(new_spacer()) self.layout.addWidget(self.toolbar_status) self.toolbar_filters = QWidget() self.toolbar_filters.setObjectName('table_filters') self.toolbar_filters.setLayout(QHBoxLayout()) self.toolbar_filters.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.toolbar_filters.setContentsMargins(0, 0, 0, 0) self.check_updates = QCheckBox() self.check_updates.setObjectName('check_updates') 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.toolbar_filters.layout().addWidget(self.check_updates) self.comp_manager.register_component(CHECK_UPDATES, self.check_updates) self.check_apps = QCheckBox() self.check_apps.setObjectName('check_apps') 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.toolbar_filters.layout().addWidget(self.check_apps) self.comp_manager.register_component(CHECK_APPS, self.check_apps) self.any_type_filter = 'any' self.cache_type_filter_icons = {} self.combo_filter_type = QComboBox() self.combo_filter_type.setObjectName('combo_types') self.combo_filter_type.setCursor(QCursor(Qt.PointingHandCursor)) self.combo_filter_type.setView(QListView()) self.combo_filter_type.view().setCursor(QCursor(Qt.PointingHandCursor)) self.combo_filter_type.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.combo_filter_type.setEditable(True) self.combo_filter_type.lineEdit().setReadOnly(True) 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.combo_filter_type.sizePolicy().setRetainSizeWhenHidden(True) self.toolbar_filters.layout().addWidget(self.combo_filter_type) self.comp_manager.register_component(COMBO_TYPES, self.combo_filter_type) self.any_category_filter = 'any' self.combo_categories = QComboBox() self.combo_categories.setObjectName('combo_categories') self.combo_categories.setCursor(QCursor(Qt.PointingHandCursor)) self.combo_categories.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.combo_categories.view().setCursor(QCursor(Qt.PointingHandCursor)) self.combo_categories.setEditable(True) 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.toolbar_filters.layout().addWidget(self.combo_categories) self.comp_manager.register_component(COMBO_CATEGORIES, self.combo_categories) self.input_name = QSearchBar(search_callback=self.begin_apply_filters) self.input_name.palette().swap(self.combo_categories.palette()) self.input_name.setObjectName('name_filter') self.input_name.set_placeholder(self.i18n['manage_window.name_filter.placeholder'] + '...') self.input_name.set_tooltip(self.i18n['manage_window.name_filter.tooltip']) self.input_name.set_button_tooltip(self.i18n['manage_window.name_filter.button_tooltip']) self.input_name.sizePolicy().setRetainSizeWhenHidden(True) self.toolbar_filters.layout().addWidget(self.input_name) self.comp_manager.register_component(INP_NAME, self.input_name) self.toolbar_filters.layout().addWidget(new_spacer()) toolbar_bts = [] bt_inst = QPushButton() bt_inst.setObjectName('bt_installed') bt_inst.setProperty('root', 'true') bt_inst.setCursor(QCursor(Qt.PointingHandCursor)) bt_inst.setToolTip(self.i18n['manage_window.bt.installed.tooltip']) bt_inst.setText(self.i18n['manage_window.bt.installed.text'].capitalize()) bt_inst.clicked.connect(self._begin_loading_installed) bt_inst.sizePolicy().setRetainSizeWhenHidden(True) toolbar_bts.append(bt_inst) self.toolbar_filters.layout().addWidget(bt_inst) self.comp_manager.register_component(BT_INSTALLED, bt_inst) bt_ref = QPushButton() bt_ref.setObjectName('bt_refresh') bt_ref.setProperty('root', 'true') bt_ref.setCursor(QCursor(Qt.PointingHandCursor)) bt_ref.setToolTip(i18n['manage_window.bt.refresh.tooltip']) bt_ref.setText(self.i18n['manage_window.bt.refresh.text']) bt_ref.clicked.connect(self.begin_refresh_packages) bt_ref.sizePolicy().setRetainSizeWhenHidden(True) toolbar_bts.append(bt_ref) self.toolbar_filters.layout().addWidget(bt_ref) self.comp_manager.register_component(BT_REFRESH, bt_ref) self.bt_upgrade = QPushButton() self.bt_upgrade.setProperty('root', 'true') self.bt_upgrade.setObjectName('bt_upgrade') self.bt_upgrade.setCursor(QCursor(Qt.PointingHandCursor)) self.bt_upgrade.setToolTip(i18n['manage_window.bt.upgrade.tooltip']) self.bt_upgrade.setText(i18n['manage_window.bt.upgrade.text']) self.bt_upgrade.clicked.connect(self.upgrade_selected) self.bt_upgrade.sizePolicy().setRetainSizeWhenHidden(True) toolbar_bts.append(self.bt_upgrade) self.toolbar_filters.layout().addWidget(self.bt_upgrade) self.comp_manager.register_component(BT_UPGRADE, self.bt_upgrade) # setting all buttons to the same size: bt_biggest_size = 0 for bt in toolbar_bts: bt_width = bt.sizeHint().width() if bt_width > bt_biggest_size: bt_biggest_size = bt_width for bt in toolbar_bts: bt_width = bt.sizeHint().width() if bt_biggest_size > bt_width: bt.setFixedWidth(bt_biggest_size) self.layout.addWidget(self.toolbar_filters) self.table_container = QWidget() self.table_container.setObjectName('table_container') self.table_container.setContentsMargins(0, 0, 0, 0) self.table_container.setLayout(QVBoxLayout()) self.table_container.layout().setContentsMargins(0, 0, 0, 0) self.table_apps = PackagesTable(self, self.icon_cache, download_icons=bool(self.config['download']['icons'])) self.table_apps.change_headers_policy() self.table_container.layout().addWidget(self.table_apps) self.layout.addWidget(self.table_container) self.toolbar_console = QWidget() self.toolbar_console.setObjectName('console_toolbar') self.toolbar_console.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.toolbar_console.setLayout(QHBoxLayout()) self.toolbar_console.setContentsMargins(0, 0, 0, 0) self.check_details = QCheckBox() self.check_details.setObjectName('check_details') self.check_details.setCursor(QCursor(Qt.PointingHandCursor)) self.check_details.setText(self.i18n['manage_window.checkbox.show_details']) self.check_details.stateChanged.connect(self._handle_console) self.toolbar_console.layout().addWidget(self.check_details) self.comp_manager.register_component(CHECK_DETAILS, self.check_details) self.toolbar_console.layout().addWidget(new_spacer()) self.label_displayed = QLabel() self.label_displayed.setObjectName('apps_displayed') self.label_displayed.setCursor(QCursor(Qt.WhatsThisCursor)) self.label_displayed.setToolTip(self.i18n['manage_window.label.apps_displayed.tip']) self.toolbar_console.layout().addWidget(self.label_displayed) self.label_displayed.hide() self.layout.addWidget(self.toolbar_console) self.textarea_details = QPlainTextEdit(self) self.textarea_details.setObjectName('textarea_details') self.textarea_details.setProperty('console', 'true') self.textarea_details.resize(self.table_apps.size()) self.layout.addWidget(self.textarea_details) self.textarea_details.setVisible(False) self.textarea_details.setReadOnly(True) self.toolbar_substatus = QToolBar() self.toolbar_substatus.setObjectName('toolbar_substatus') self.toolbar_substatus.addWidget(new_spacer()) self.label_substatus = QLabel() self.label_substatus.setObjectName('label_substatus') self.label_substatus.setCursor(QCursor(Qt.WaitCursor)) self.toolbar_substatus.addWidget(self.label_substatus) self.toolbar_substatus.addWidget(new_spacer()) self.layout.addWidget(self.toolbar_substatus) self._change_label_substatus('') self.thread_update = self._bind_async_action(UpgradeSelected(manager=self.manager, i18n=self.i18n, internet_checker=context.internet_checker, screen_width=screen_size.width()), finished_call=self._finish_upgrade_selected) 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_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(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) self.thread_apply_filters.signal_table.connect(self._update_table_and_upgrades) self.signal_table_update.connect(self.thread_apply_filters.stop_waiting) self.thread_install = InstallPackage(manager=self.manager, icon_cache=self.icon_cache, i18n=self.i18n) self._bind_async_action(self.thread_install, finished_call=self._finish_install) self.thread_animate_progress = AnimateProgress() self.thread_animate_progress.signal_change.connect(self._update_progress) self.thread_notify_pkgs_ready = NotifyPackagesReady() self.thread_notify_pkgs_ready.signal_changed.connect(self._update_package_data) self.thread_notify_pkgs_ready.signal_finished.connect(self._update_state_when_pkgs_ready) self.signal_stop_notifying.connect(self.thread_notify_pkgs_ready.stop_working) self.thread_ignore_updates = IgnorePackageUpdates(manager=self.manager) self._bind_async_action(self.thread_ignore_updates, finished_call=self.finish_ignore_updates) self.thread_reload = StartAsyncAction(delay_in_milis=5) self.thread_reload.signal_start.connect(self._reload) self.container_bottom = QWidget() self.container_bottom.setObjectName('container_bottom') self.container_bottom.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.container_bottom.setLayout(QHBoxLayout()) self.container_bottom.layout().setContentsMargins(0, 0, 0, 0) self.container_bottom.layout().addWidget(new_spacer()) if config['suggestions']['enabled']: bt_sugs = IconButton(action=lambda: self._begin_load_suggestions(filter_installed=True), i18n=i18n, tooltip=self.i18n['manage_window.bt.suggestions.tooltip']) bt_sugs.setObjectName('suggestions') self.container_bottom.layout().addWidget(bt_sugs) self.comp_manager.register_component(BT_SUGGESTIONS, bt_sugs) bt_themes = IconButton(self.show_themes, i18n=self.i18n, tooltip=self.i18n['manage_window.bt_themes.tip']) bt_themes.setObjectName('themes') self.container_bottom.layout().addWidget(bt_themes) self.comp_manager.register_component(BT_THEMES, bt_themes) self.custom_actions = [a for a in manager.gen_custom_actions()] bt_custom_actions = IconButton(action=self.show_custom_actions, i18n=self.i18n, tooltip=self.i18n['manage_window.bt_custom_actions.tip']) bt_custom_actions.setObjectName('custom_actions') bt_custom_actions.setVisible(bool(self.custom_actions)) self.container_bottom.layout().addWidget(bt_custom_actions) self.comp_manager.register_component(BT_CUSTOM_ACTIONS, bt_custom_actions) bt_settings = IconButton(action=self.show_settings, i18n=self.i18n, tooltip=self.i18n['manage_window.bt_settings.tooltip']) bt_settings.setObjectName('settings') self.container_bottom.layout().addWidget(bt_settings) self.comp_manager.register_component(BT_SETTINGS, bt_settings) bt_about = IconButton(action=self._show_about, i18n=self.i18n, tooltip=self.i18n['manage_window.settings.about']) bt_about.setObjectName('about') self.container_bottom.layout().addWidget(bt_about) self.comp_manager.register_component(BT_ABOUT, bt_about) self.layout.addWidget(self.container_bottom) self.container_progress = QCustomToolbar(spacing=0, policy_height=QSizePolicy.Fixed) self.container_progress.setObjectName('container_progress') self.container_progress.add_space() self.progress_bar = QProgressBar() self.progress_bar.setObjectName('progress_manage') self.progress_bar.setCursor(QCursor(Qt.WaitCursor)) self.progress_bar.setTextVisible(False) self.container_progress.add_widget(self.progress_bar) self.container_progress.add_space() self.layout.addWidget(self.container_progress) qt_utils.centralize(self) self.filter_only_apps = True self.type_filter = self.any_type_filter self.category_filter = self.any_category_filter self.filter_updates = False self._maximized = False self.progress_controll_enabled = True self.recent_uninstall = False self.types_changed = False 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) self.thread_warnings.signal_warnings.connect(self._show_warnings) self.settings_window = None self.search_performed = False self.thread_save_theme = SaveTheme(theme_key='') self.thread_load_installed = NotifyInstalledLoaded() 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_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_SUGGESTIONS, BT_THEMES, BT_CUSTOM_ACTIONS, BT_SETTINGS, BT_ABOUT) def update_custom_actions(self): self.custom_actions = [a for a in self.manager.gen_custom_actions()] def _update_process_progress(self, val: int): if self.progress_controll_enabled: self.thread_animate_progress.set_progress(val) 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.stop_notifying_package_states() 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.setFocus(Qt.NoFocusReason) def _finish_apply_filters(self): self._finish_action(ACTION_APPLY_FILTERS) self.update_bt_upgrade() def stop_notifying_package_states(self): if self.thread_notify_pkgs_ready.isRunning(): self.signal_stop_notifying.emit() self.thread_notify_pkgs_ready.wait(1000) def _update_table_and_upgrades(self, pkgs_info: dict): self._update_table(pkgs_info=pkgs_info, signal=True) if self.pkgs: self._update_state_when_pkgs_ready() self.stop_notifying_package_states() self.thread_notify_pkgs_ready.pkgs = self.pkgs self.thread_notify_pkgs_ready.work = True self.thread_notify_pkgs_ready.start() def _bind_async_action(self, action: AsyncAction, finished_call, only_finished: bool = False) -> AsyncAction: action.signal_finished.connect(finished_call) if not only_finished: action.signal_confirmation.connect(self._ask_confirmation) action.signal_output.connect(self._update_action_output) action.signal_message.connect(self._show_message) action.signal_status.connect(self._change_label_status) action.signal_substatus.connect(self._change_label_substatus) action.signal_progress.connect(self._update_process_progress) action.signal_progress_control.connect(self.set_progress_controll) action.signal_root_password.connect(self._pause_and_ask_root_password) self.signal_user_res.connect(action.confirm) self.signal_root_password.connect(action.set_root_password) return action def _ask_confirmation(self, msg: dict): self.thread_animate_progress.pause() extra_widgets = [to_widget(comp=c, i18n=self.i18n) for c in msg['components']] if msg.get('components') else None diag = ConfirmationDialog(title=msg['title'], body=msg['body'], i18n=self.i18n, widgets=extra_widgets, confirmation_label=msg['confirmation_label'], deny_label=msg['deny_label'], deny_button=msg['deny_button'], window_cancel=msg['window_cancel'], confirmation_button=msg.get('confirmation_button', True), min_width=msg.get('min_width'), min_height=msg.get('min_height')) diag.ask() res = diag.confirmed self.thread_animate_progress.animate() self.signal_user_res.emit(res) def _pause_and_ask_root_password(self): self.thread_animate_progress.pause() valid, password = RootDialog.ask_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) self.thread_animate_progress.animate() self.signal_root_password.emit(valid, password) def _show_message(self, msg: dict): self.thread_animate_progress.pause() dialog.show_message(title=msg['title'], body=msg['body'], type_=msg['type']) self.thread_animate_progress.animate() def _show_warnings(self, warnings: List[str]): if warnings: dialog.show_message(title=self.i18n['warning'].capitalize(), body='
{}
'.format('{}
'.format(substatus)) if not substatus: self.toolbar_substatus.hide() 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'] if pkgs_info['not_installed'] == 0: update_check = sum_updates_displayed(pkgs_info) > 0 else: update_check = False self.table_apps.update_packages(self.pkgs, update_check_enabled=update_check) if not self._maximized: self.label_displayed.show() self.table_apps.change_headers_policy(QHeaderView.Stretch) self.table_apps.change_headers_policy() self._resize(accept_lower_width=len(self.pkgs) > 0) if len(self.pkgs) == 0 and len(self.pkgs_available) == 0: self.label_displayed.setText('') else: self.label_displayed.setText('{} / {}'.format(len(self.pkgs), len(self.pkgs_available))) else: self.label_displayed.hide() if signal: self.signal_table_update.emit() def update_bt_upgrade(self, pkgs_info: dict = None): show_bt_upgrade = False 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.comp_manager.set_component_visible(BT_UPGRADE, show_bt_upgrade) if show_bt_upgrade: self._reorganize() 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.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.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.check_apps, False, 'filter_only_apps', trigger_filters) else: if not keep_selected: self._change_checkbox(self.check_updates, False, 'filter_updates', trigger_filters) 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: checkbox.blockSignals(True) checkbox.setChecked(checked) if not trigger: setattr(self, attr, checked) checkbox.blockSignals(False) def _gen_filters(self, ignore_updates: bool = False) -> dict: return { 'only_apps': False if self.search_performed else self.filter_only_apps, 'type': self.type_filter, 'category': self.category_filter, 'updates': False if ignore_updates else self.filter_updates, 'name': self.input_name.text().lower() if self.input_name.text() else None, 'display_limit': None if self.filter_updates else self.display_limit } def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: bool, types: Optional[Set[type]] = None, ignore_updates: bool = False, keep_filters: bool = False) -> bool: self.input_name.set_text('') pkgs_info = commons.new_pkgs_info() filters = self._gen_filters(ignore_updates=ignore_updates) if new_pkgs is not None: old_installed = None if as_installed: old_installed = self.pkgs_installed self.pkgs_installed = [] for pkg in new_pkgs: app_model = PackageView(model=pkg, i18n=self.i18n) commons.update_info(app_model, pkgs_info) commons.apply_filters(app_model, filters, pkgs_info) if old_installed and types: for pkgv in old_installed: if pkgv.model.__class__ not in types: commons.update_info(pkgv, pkgs_info) commons.apply_filters(pkgv, filters, pkgs_info) else: # use installed for pkgv in self.pkgs_installed: commons.update_info(pkgv, pkgs_info) commons.apply_filters(pkgv, filters, pkgs_info) if pkgs_info['apps_count'] == 0: if self.load_suggestions or self.types_changed: if as_installed: self.pkgs_installed = pkgs_info['pkgs'] self._begin_load_suggestions(filter_installed=False) self.load_suggestions = False return False else: if not keep_filters: self._change_checkbox(self.check_apps, False, 'filter_only_apps', trigger=False) self.check_apps.setCheckable(False) else: if not keep_filters: 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'])) self._update_type_filters(pkgs_info['available_types'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) self._apply_filters(pkgs_info, ignore_updates=ignore_updates) self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) self.pkgs_available = pkgs_info['pkgs'] if as_installed: self.pkgs_installed = pkgs_info['pkgs'] self.pkgs = pkgs_info['pkgs_displayed'] self._update_table(pkgs_info=pkgs_info) if new_pkgs: self.stop_notifying_package_states() self.thread_notify_pkgs_ready.work = True self.thread_notify_pkgs_ready.pkgs = self.pkgs self.thread_notify_pkgs_ready.start() self._resize(accept_lower_width=bool(self.pkgs_installed)) if self.first_refresh: qt_utils.centralize(self) self.first_refresh = False if not self.installed_loaded and as_installed: self.installed_loaded = True 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 _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.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: self._clean_combo_types() sel_type = -1 for idx, item in enumerate(available_types.items()): app_type, icon_path, label = item[0], item[1]['icon'], item[1]['label'] icon = self.cache_type_filter_icons.get(app_type) if not icon: icon = QIcon(icon_path) self.cache_type_filter_icons[app_type] = icon self.combo_filter_type.addItem(icon, label, app_type) if keeping_selected and app_type == self.type_filter: sel_type = idx + 1 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.comp_manager.set_component_visible(COMBO_TYPES, len(available_types) > 1) else: 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.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 if not keeping_selected: self.category_filter = self.any_category_filter if categories: if self.combo_categories.count() > 1: for _ in range(self.combo_categories.count() - 1): self.combo_categories.removeItem(1) selected_cat = -1 cat_list = list(categories) cat_list.sort() for idx, c in enumerate(cat_list): self.__add_category(c) if keeping_selected and c == self.category_filter: selected_cat = idx + 1 self.combo_categories.blockSignals(True) self.combo_categories.setCurrentIndex(selected_cat if selected_cat > -1 else 0) self.combo_categories.blockSignals(False) self.comp_manager.set_component_visible(COMBO_CATEGORIES, True) else: 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() toolbar_width = self.toolbar_filters.sizeHint().width() topbar_width = self.toolbar_status.sizeHint().width() new_width = max(table_width, toolbar_width, topbar_width) new_width *= 1.05 # this extra size is not because of the toolbar button, but the table upgrade buttons if (self.pkgs and accept_lower_width) or new_width > self.width(): self.resize(int(new_width), self.height()) def set_progress_controll(self, enabled: bool): self.progress_controll_enabled = enabled def upgrade_selected(self): body = QWidget() body.setLayout(QHBoxLayout()) body.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) body.layout().addWidget(QLabel(self.i18n['manage_window.upgrade_all.popup.body'])) body.layout().addWidget(UpgradeToggleButton(pkg=None, root=self, i18n=self.i18n, clickable=False)) if ConfirmationDialog(title=self.i18n['manage_window.upgrade_all.popup.title'], i18n=self.i18n, body=None, widgets=[body]).ask(): 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.thread_update.pkgs = self.pkgs self.thread_update.start() def _finish_upgrade_selected(self, res: dict): self._finish_action() if res.get('id'): output = self.textarea_details.toPlainText() if output: try: Path(UpgradeSelected.UPGRADE_LOGS_DIR).mkdir(parents=True, exist_ok=True) logs_path = '{}/{}.log'.format(UpgradeSelected.UPGRADE_LOGS_DIR, res['id']) with open(logs_path, 'w+') as f: f.write(output) self.textarea_details.appendPlainText('\n*Upgrade summary generated at: {}'.format(UpgradeSelected.SUMMARY_FILE.format(res['id']))) self.textarea_details.appendPlainText('*Upgrade logs generated at: {}'.format(logs_path)) except: 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'])) 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.update_custom_actions() def _show_console_errors(self): if self.textarea_details.toPlainText(): self.check_details.setChecked(True) else: self._handle_console_option(False) self.comp_manager.set_component_visible(CHECK_DETAILS, False) def _update_action_output(self, output: str): self.textarea_details.appendPlainText(output) def _begin_action(self, action_label: str, action_id: int = None): self.thread_animate_progress.stop = False self.thread_animate_progress.start() self.progress_bar.setVisible(True) if action_id is not None: self.comp_manager.save_states(action_id, only_visible=True) self._set_table_enabled(False) self.comp_manager.set_component_visible(SEARCH_BAR, False) self._change_status(action_label) def _set_lower_buttons_visible(self, visible: bool): self.comp_manager.set_group_visible(GROUP_LOWER_BTS, visible) if visible: self.comp_manager.set_component_visible(BT_CUSTOM_ACTIONS, bool(self.custom_actions)) def _finish_action(self, action_id: int = None): self.thread_animate_progress.stop = True self.thread_animate_progress.wait(msecs=1000) self.progress_bar.setVisible(False) self.progress_bar.setValue(0) self.progress_bar.setTextVisible(False) if action_id is not None: self.comp_manager.restore_state(action_id) 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 def begin_downgrade(self, pkg: PackageView): pwd, proceed = self._ask_root_password(SoftwareAction.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.thread_downgrade.pkg = pkg self.thread_downgrade.root_pwd = pwd self.thread_downgrade.start() def _finish_downgrade(self, res: dict): self._finish_action() if res['success']: self.comp_manager.remove_saved_state(ACTION_DOWNGRADE) 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) if pkg_info: if len(pkg_info) > 1: dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache, i18n=self.i18n, screen_size=self.screen_size) dialog_info.exec_() else: dialog.show_message(title=self.i18n['warning'].capitalize(), body=self.i18n['manage_window.info.no_info'].format(bold(pkg_info['__app__'].model.name)), type_=MessageType.WARNING) 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_show_screenshots(self, res: dict): self._finish_action(ACTION_SCREENSHOTS) if res.get('screenshots'): diag = ScreenshotsDialog(pkg=res['pkg'], http_client=self.http_client, icon_cache=self.icon_cache, logger=self.logger, i18n=self.i18n, screenshots=res['screenshots']) diag.exec_() else: dialog.show_message(title=self.i18n['error'], body=self.i18n['popup.screenshots.no_screenshot.body'].format(bold(res['pkg'].model.name)), type_=MessageType.ERROR) 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_show_history.pkg = pkg self.thread_show_history.start() def _finish_show_history(self, res: dict): self._finish_action(ACTION_HISTORY) if res.get('error'): self._handle_console_option(True) self.textarea_details.appendPlainText(res['error']) self.check_details.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)), type_=MessageType.WARNING) else: dialog_history = HistoryDialog(res['history'], self.icon_cache, self.i18n) dialog_history.exec_() 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 ''), action_id=action_id) def search(self): word = self.search_bar.text().strip() if word: self._handle_console(False) 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.search_performed = True if not res['error']: 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: SoftwareAction, pkg: PackageView) -> Tuple[Optional[str], bool]: pwd = None requires_root = self.manager.requires_root(action, pkg.model) if not user.is_root() and requires_root: valid, pwd = RootDialog.ask_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) if not valid: return pwd, False return pwd, True def install(self, pkg: PackageView): pwd, proceed = self._ask_root_password(SoftwareAction.INSTALL, pkg) 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.thread_install.pkg = pkg self.thread_install.root_pwd = pwd self.thread_install.start() def _finish_install(self, res: dict): self._finish_action(action_id=ACTION_INSTALL) console_output = self.textarea_details.toPlainText() if console_output: log_path = f"{LOGS_DIR}/install/{res['pkg'].model.get_type()}/{res['pkg'].model.name}" try: Path(log_path).mkdir(parents=True, exist_ok=True) log_file = f'{log_path}/{int(time.time())}.log' with open(log_file, 'w+') as f: f.write(console_output) self.textarea_details.appendPlainText(self.i18n['console.install_logs.path'].format('"{}"'.format(log_file))) except: self.textarea_details.appendPlainText("[warning] Could not write install log file to '{}'".format(log_path)) if res['success']: if self._can_notify_user(): util.notify_user(msg='{} ({}) {}'.format(res['pkg'].model.name, res['pkg'].model.get_type(), self.i18n['installed'])) models_updated = [] for key in ('installed', 'removed'): if res.get(key): models_updated.extend(res[key]) if models_updated: installed_available_idxs = [] for idx, available in enumerate(self.pkgs_available): for pidx, model in enumerate(models_updated): if available.model == model: available.update_model(model) if model.installed: installed_available_idxs.append((idx, pidx, available)) # re-indexing all installed so they always will be be displayed when no filters are applied if installed_available_idxs: # removing from available installed_available_idxs.sort(key=operator.itemgetter(0)) for decrement, data in enumerate(installed_available_idxs): del self.pkgs_available[data[0] - decrement] # re-inserting into the available installed_available_idxs.sort(key=operator.itemgetter(1)) for new_idx, data in enumerate(installed_available_idxs): self.pkgs_available.insert(new_idx, data[2]) # updating the respective table rows: for displayed in self.pkgs: for model in models_updated: if displayed.model == model: self.table_apps.update_package(displayed, change_update_col=True) self.update_bt_upgrade() # updating installed packages if res['removed'] and self.pkgs_installed: to_remove = [] for idx, installed in enumerate(self.pkgs_installed): for removed in res['removed']: if installed.model == removed: to_remove.append(idx) if to_remove: to_remove.sort() for decrement, idx in enumerate(to_remove): del self.pkgs_installed[idx - decrement] if res['installed']: for idx, model in enumerate(res['installed']): self.pkgs_installed.insert(idx, PackageView(model, self.i18n)) self.update_custom_actions() self.table_apps.change_headers_policy(policy=QHeaderView.Stretch, maximized=self._maximized) self.table_apps.change_headers_policy(policy=QHeaderView.ResizeToContents, maximized=self._maximized) self._resize(accept_lower_width=False) else: self._show_console_errors() if self._can_notify_user(): util.notify_user('{}: {}'.format(res['pkg'].model.name, self.i18n['notification.install.failed'])) def _update_progress(self, value: int): self.progress_bar.setValue(value) def begin_execute_custom_action(self, pkg: Optional[PackageView], action: CustomSoftwareAction): if pkg is None and action.requires_confirmation and \ not ConfirmationDialog(title=self.i18n['confirmation'].capitalize(), body='{}
'.format(self.i18n['custom_action.proceed_with'].capitalize().format(bold(self.i18n[action.i18n_label_key]))), icon=QIcon(action.icon_path) if action.icon_path else QIcon(resource.get_path('img/logo.svg')), i18n=self.i18n).ask(): return False pwd = None if not user.is_root() and action.requires_root: valid, pwd = RootDialog.ask_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) if not valid: return action_label = self.i18n[action.i18n_status_key] if pkg: if '{}' in action_label: action_label = action_label.format(pkg.model.name) else: action_label += f' {pkg.model.name}' self._begin_action(action_label=action_label, action_id=ACTION_CUSTOM_ACTION) self.comp_manager.set_components_visible(False) self._handle_console_option(True) 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_execute_custom_action(self, res: dict): self._finish_action() if res['success']: if res['action'].refresh: self.comp_manager.remove_saved_state(ACTION_CUSTOM_ACTION) self.update_custom_actions() 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.comp_manager.restore_state(ACTION_CUSTOM_ACTION) self._show_console_errors() if res['error']: dialog.show_message(title=self.i18n['warning' if res['error_type'] == MessageType.WARNING else 'error'].capitalize(), body=self.i18n[res['error']], type_=res['error_type']) def _show_console_checkbox_if_output(self): if self.textarea_details.toPlainText(): self.comp_manager.set_component_visible(CHECK_DETAILS, True) else: self.comp_manager.set_component_visible(CHECK_DETAILS, False) def show_settings(self): if self.settings_window: self.settings_window.handle_display() else: self.settings_window = SettingsWindow(self.manager, self.i18n, self.screen_size, self) self.settings_window.setMinimumWidth(int(self.screen_size.width() / 4)) self.settings_window.resize(self.size()) self.settings_window.adjustSize() qt_utils.centralize(self.settings_window) self.settings_window.show() def _map_custom_action(self, action: CustomSoftwareAction, parent: QWidget) -> QCustomMenuAction: if action.icon_path: try: if action.icon_path.startswith('/'): icon = QIcon(action.icon_path) else: icon = QIcon.fromTheme(action.icon_path) except: icon = None else: icon = None tip = self.i18n[action.i18n_description_key] if action.i18n_description_key else None return QCustomMenuAction(parent=parent, label=self.i18n[action.i18n_label_key], action=lambda: self.begin_execute_custom_action(None, action), tooltip=tip, icon=icon) def show_custom_actions(self): if self.custom_actions: menu_row = QMenu() menu_row.setCursor(QCursor(Qt.PointingHandCursor)) actions = [self._map_custom_action(a, menu_row) for a in self.custom_actions] menu_row.addActions(actions) menu_row.adjustSize() menu_row.popup(QCursor.pos()) menu_row.exec_() 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(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(action_id=ACTION_IGNORE_UPDATES) if res['success']: hide_package = commons.is_package_hidden(res['pkg'], self._gen_filters()) if hide_package: idx_to_remove = None for pkg in self.pkgs: if pkg == res['pkg']: idx_to_remove = pkg.table_index break 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)), type_=MessageType.INFO) else: dialog.show_message(title=self.i18n['fail'].capitalize(), 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) def _map_theme_action(self, theme: ThemeMetadata, menu: QMenu) -> QCustomMenuAction: def _change_theme(): set_theme(theme_key=theme.key, app=QApplication.instance(), logger=self.context.logger) self.thread_save_theme.theme_key = theme.key self.thread_save_theme.start() return QCustomMenuAction(label=theme.get_i18n_name(self.i18n), action=_change_theme, parent=menu, tooltip=theme.get_i18n_description(self.i18n)) def show_themes(self): menu_row = QMenu() menu_row.setCursor(QCursor(Qt.PointingHandCursor)) menu_row.addActions(self._map_theme_actions(menu_row)) menu_row.adjustSize() menu_row.popup(QCursor.pos()) menu_row.exec_() def _map_theme_actions(self, menu: QMenu) -> List[QCustomMenuAction]: core_config = CoreConfigManager().get_config() current_theme_key, current_action = core_config['ui']['theme'], None actions = [] for t in read_all_themes_metadata(): if not t.abstract: action = self._map_theme_action(t, menu) if current_action is None and current_theme_key is not None and current_theme_key == t.key: action.button.setProperty('current', 'true') current_action = action else: actions.append(action) if not current_action: invalid_action = QCustomMenuAction(label=self.i18n['manage_window.bt_themes.option.invalid'], parent=menu) invalid_action.button.setProperty('current', 'true') current_action = invalid_action actions.sort(key=lambda a: a.get_label()) actions.insert(0, current_action) return actions def reload(self): self.thread_reload.start() def _reload(self): self.update_custom_actions() self.verify_warnings() self.types_changed = True self.begin_refresh_packages()