Files
bearhub/bauh/view/qt/window.py

1600 lines
70 KiB
Python
Executable File

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
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 import LOGS_PATH
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.commons import user
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 read_config
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
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, 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.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(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_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.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 = manager.get_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 = self.manager.get_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))
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='<p>{}</p>'.format('<br/><br/>'.join(warnings)), type_=MessageType.WARNING)
def show(self):
super(ManageWindow, self).show()
if not self.thread_warnings.isFinished():
self.thread_warnings.start()
qt_utils.centralize(self)
def verify_warnings(self):
self.thread_warnings.start()
def _begin_loading_installed(self):
self.search_bar.clear()
self.input_name.set_text('')
self._begin_action(self.i18n['manage_window.status.installed'])
self._handle_console_option(False)
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.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:
self.dialog_about = AboutDialog(self.config)
self.dialog_about.show()
def _handle_updates_filter(self, status: int):
self.filter_updates = status == 2
self.begin_apply_filters()
def _handle_filter_only_apps(self, status: int):
self.filter_only_apps = status == 2
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.begin_apply_filters()
def _handle_category_filter(self, idx: int):
self.category_filter = self.combo_categories.itemData(idx)
self.begin_apply_filters()
def _update_state_when_pkgs_ready(self):
if self.progress_bar.isVisible():
return
self._reload_categories()
self._reorganize()
def _update_package_data(self, idx: int):
if self.table_apps.isEnabled():
pkg = self.pkgs[idx]
pkg.status = PackageViewStatus.READY
self.table_apps.update_package(pkg)
def _reload_categories(self):
categories = set()
for p in self.pkgs_available:
if p.model.categories:
for c in p.model.categories:
if c:
cat = c.strip().lower()
if cat:
categories.add(cat)
if categories:
self._update_categories(categories, keep_selected=True)
def changeEvent(self, e: QEvent):
if isinstance(e, QWindowStateChangeEvent):
self._maximized = self.isMaximized()
self.table_apps.change_headers_policy(maximized=self._maximized)
def _handle_console(self, checked: bool):
if checked:
self.textarea_details.show()
else:
self.textarea_details.hide()
def _handle_console_option(self, enable: bool):
if enable:
self.textarea_details.clear()
self.comp_manager.set_component_visible(CHECK_DETAILS, enable)
self.check_details.setChecked(False)
self.textarea_details.hide()
def begin_refresh_packages(self, pkg_types: Set[Type[SoftwarePackage]] = None):
self.search_bar.clear()
self._begin_action(self.i18n['manage_window.status.refreshing'])
self.comp_manager.set_components_visible(False)
self._handle_console_option(False)
self.suggestions_requested = False
self.search_performed = False
self.thread_refresh.pkg_types = pkg_types
self.thread_refresh.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)
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.load_suggestions = False
self.types_changed = False
def _begin_load_suggestions(self, filter_installed: bool):
self.search_bar.clear()
self._begin_action(self.i18n['manage_window.status.suggestions'])
self._handle_console_option(False)
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(SoftwareAction.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.thread_uninstall.pkg = pkg
self.thread_uninstall.root_pwd = pwd
self.thread_uninstall.start()
def _finish_uninstall(self, res: dict):
self._finish_action(action_id=ACTION_UNINSTALL)
if res['success']:
src_pkg = res['pkg']
if self._can_notify_user():
util.notify_user('{} ({}) {}'.format(src_pkg.model.name, src_pkg.model.get_type(), self.i18n['uninstalled']))
if res['removed']:
for list_idx, pkg_list in enumerate((self.pkgs_available, self.pkgs, self.pkgs_installed)):
if pkg_list:
removed_idxs = []
for pkgv_idx, pkgv in enumerate(pkg_list):
if len(removed_idxs) == len(res['removed']):
break
for model in res['removed']:
if pkgv.model == model:
if list_idx == 0: # updates the model
pkgv.update_model(model)
if not self.search_performed or list_idx == 2: # always from the installed packages
removed_idxs.append(pkgv_idx)
if self.search_performed and list_idx == 1: # only for displayed
self.table_apps.update_package(pkgv, change_update_col=True)
break # as the model has been found, stops the loop
if removed_idxs:
# updating the list
removed_idxs.sort()
for decrement, pkg_idx in enumerate(removed_idxs):
del pkg_list[pkg_idx - decrement]
if list_idx == 1: # updates the rows if the current list reprents the displayed packages:
for decrement, idx in enumerate(removed_idxs):
self.table_apps.removeRow(idx - decrement)
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']))
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
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()
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 _change_label_status(self, status: str):
self.label_status.setText(status)
def _change_label_substatus(self, substatus: str):
self.label_substatus.setText('<p>{}</p>'.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)
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: List[SoftwarePackage], as_installed: bool, types: 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
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(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.LOGS_DIR).mkdir(parents=True, exist_ok=True)
logs_path = '{}/{}.log'.format(UpgradeSelected.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)
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_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 = '{}/install/{}/{}'.format(LOGS_PATH, res['pkg'].model.get_type(), res['pkg'].model.name)
try:
Path(log_path).mkdir(parents=True, exist_ok=True)
log_file = log_path + '/{}.log'.format(int(time.time()))
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 not ConfirmationDialog(title=self.i18n['confirmation'].capitalize(),
body='<p>{}</p>'.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
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.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.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
return QCustomMenuAction(parent=parent,
label=self.i18n[action.i18n_label_key],
action=lambda: self.begin_execute_custom_action(None, action),
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 = read_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