This commit is contained in:
Vinícius Moreira
2020-04-13 11:49:28 -03:00
parent 83aab1ff55
commit 01a60ea686
165 changed files with 12778 additions and 6171 deletions

View File

@@ -1,12 +1,12 @@
from glob import glob
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel, QWidget, QHBoxLayout
from bauh import __version__, __app_name__, ROOT_DIR
from bauh.view.util import resource, util
from bauh.view.util.translation import I18n
from bauh.context import generate_i18n
from bauh.view.util import resource
PROJECT_URL = 'https://github.com/vinifmor/' + __app_name__
LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/{}/master/LICENSE'.format(__app_name__)
@@ -14,14 +14,15 @@ LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/{}/master/LICENSE'.for
class AboutDialog(QDialog):
def __init__(self, i18n: I18n):
def __init__(self, app_config: dict):
super(AboutDialog, self).__init__()
self.setWindowTitle(i18n['tray.action.about'])
i18n = generate_i18n(app_config, resource.get_path('locale/about'))
self.setWindowTitle('{} ({})'.format(i18n['about.title'].capitalize(), __app_name__))
layout = QVBoxLayout()
self.setLayout(layout)
label_logo = QLabel()
icon = util.get_default_icon()[1].pixmap(64, 64)
icon = QIcon(resource.get_path('img/logo.svg')).pixmap(64, 64)
label_logo.setPixmap(icon)
label_logo.setAlignment(Qt.AlignCenter)
layout.addWidget(label_logo)
@@ -31,7 +32,7 @@ class AboutDialog(QDialog):
label_name.setAlignment(Qt.AlignCenter)
layout.addWidget(label_name)
label_version = QLabel(i18n['version'].lower() + ' ' + __version__)
label_version = QLabel(i18n['about.version'].lower() + ' ' + __version__)
label_version.setStyleSheet('QLabel { font-size: 11px; font-weight: bold }')
label_version.setAlignment(Qt.AlignCenter)
layout.addWidget(label_version)

View File

@@ -15,7 +15,7 @@ from bauh.api.abstract.model import PackageStatus
from bauh.commons.html import strip_html
from bauh.view.qt import dialog
from bauh.view.qt.components import IconButton
from bauh.view.qt.view_model import PackageView, PackageViewStatus
from bauh.view.qt.view_model import PackageView
from bauh.view.util import resource
from bauh.view.util.translation import I18n
@@ -28,10 +28,10 @@ PUBLISHER_MAX_SIZE = 25
class UpdateToggleButton(QWidget):
def __init__(self, app_view: PackageView, root: QWidget, i18n: I18n, checked: bool = True, clickable: bool = True):
def __init__(self, pkg: PackageView, root: QWidget, i18n: I18n, checked: bool = True, clickable: bool = True):
super(UpdateToggleButton, self).__init__()
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.app_view = app_view
self.app_view = pkg
self.root = root
layout = QHBoxLayout()
@@ -40,22 +40,41 @@ class UpdateToggleButton(QWidget):
self.setLayout(layout)
self.bt = QToolButton()
self.bt.setCheckable(clickable)
self.bt.setCheckable(True)
self.bt.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
if clickable:
self.bt.clicked.connect(self.change_state)
self.bt.setIcon(QIcon(resource.get_path('img/app_update.svg')))
self.bt.setStyleSheet('QToolButton { background: #20A435 } ' +
('QToolButton:checked { background: gray }' if clickable else ''))
layout.addWidget(self.bt)
'QToolButton:checked { background: gray } ' +
('QToolButton:disabled { background: #d69003 }' if not clickable and not checked else ''))
self.setToolTip(i18n['manage_window.apps_table.upgrade_toggle.tooltip'])
layout.addWidget(self.bt)
if not checked:
self.bt.click()
if clickable:
self.bt.setIcon(QIcon(resource.get_path('img/app_update.svg')))
self.setToolTip('{} {}'.format(i18n['manage_window.apps_table.upgrade_toggle.tooltip'],
i18n['manage_window.apps_table.upgrade_toggle.enabled.tooltip']))
else:
if not checked:
self.bt.setIcon(QIcon(resource.get_path('img/exclamation.svg')))
self.bt.setEnabled(False)
tooltip = i18n['{}.update.disabled.tooltip'.format(pkg.model.gem_name)]
if tooltip:
self.setToolTip(tooltip)
else:
self.setToolTip('{} {}'.format(i18n['manage_window.apps_table.upgrade_toggle.tooltip'],
i18n['manage_window.apps_table.upgrade_toggle.disabled.tooltip']))
else:
self.bt.setIcon(QIcon(resource.get_path('img/app_update.svg')))
self.bt.setCheckable(False)
def change_state(self, not_checked: bool):
self.app_view.update_checked = not not_checked
self.root.update_bt_upgrade()
@@ -65,11 +84,10 @@ class AppsTable(QTableWidget):
COL_NUMBER = 8
def __init__(self, parent: QWidget, icon_cache: MemoryCache, disk_cache: bool, download_icons: bool):
def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool):
super(AppsTable, self).__init__()
self.setParent(parent)
self.window = parent
self.disk_cache = disk_cache
self.download_icons = download_icons
self.setColumnCount(self.COL_NUMBER)
self.setFocusPolicy(Qt.NoFocus)
@@ -150,22 +168,13 @@ class AppsTable(QTableWidget):
def refresh(self, pkg: PackageView):
self._update_row(pkg, update_check_enabled=False, change_update_col=False)
def fill_async_data(self):
if self.window.pkgs:
def update_package(self, pkg: PackageView):
if self.download_icons and pkg.model.icon_url:
icon_request = QNetworkRequest(QUrl(pkg.model.icon_url))
icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
self.network_man.get(icon_request)
for idx, app_v in enumerate(self.window.pkgs):
if app_v.status == PackageViewStatus.LOADING and app_v.model.status == PackageStatus.READY:
if self.download_icons:
icon_request = QNetworkRequest(QUrl(app_v.model.icon_url))
icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
self.network_man.get(icon_request)
self._update_row(app_v, change_update_col=False)
app_v.status = PackageViewStatus.READY
self.window.resize_and_center()
self._update_row(pkg, change_update_col=False)
def _uninstall_app(self, app_v: PackageView):
if dialog.ask_confirmation(title=self.i18n['manage_window.apps_table.row.actions.uninstall.popup.title'],
@@ -222,18 +231,24 @@ class AppsTable(QTableWidget):
col_name = self.item(idx, 0)
col_name.setIcon(icon_data['icon'])
if self.disk_cache and app.model.supports_disk_cache() and app.model.get_disk_icon_path():
if app.model.supports_disk_cache() and app.model.get_disk_icon_path():
if not icon_was_cached or not os.path.exists(app.model.get_disk_icon_path()):
self.window.manager.cache_to_disk(pkg=app.model, icon_bytes=icon_data['bytes'], only_icon=True)
def update_pkgs(self, pkg_views: List[PackageView], update_check_enabled: bool = True):
self.setRowCount(len(pkg_views) if pkg_views else 0)
def update_packages(self, pkgs: List[PackageView], update_check_enabled: bool = True):
self.setRowCount(len(pkgs) if pkgs else 0)
self.setEnabled(True)
if pkg_views:
for idx, pkgv in enumerate(pkg_views):
pkgv.table_index = idx
self._update_row(pkgv, update_check_enabled)
if pkgs:
for idx, pkg in enumerate(pkgs):
pkg.table_index = idx
if self.download_icons and pkg.model.status == PackageStatus.READY and pkg.model.icon_url:
icon_request = QNetworkRequest(QUrl(pkg.model.icon_url))
icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
self.network_man.get(icon_request)
self._update_row(pkg, update_check_enabled)
def _update_row(self, pkg: PackageView, update_check_enabled: bool = True, change_update_col: bool = True):
self._set_col_name(0, pkg)
@@ -250,7 +265,11 @@ class AppsTable(QTableWidget):
if update_check_enabled and pkg.model.update:
col_update = QToolBar()
col_update.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
col_update.addWidget(UpdateToggleButton(pkg, self.window, self.i18n, pkg.update_checked))
col_update.addWidget(UpdateToggleButton(pkg=pkg,
root=self.window,
i18n=self.i18n,
checked=pkg.update_checked if pkg.model.can_be_updated() else False,
clickable=pkg.model.can_be_updated()))
self.setCellWidget(pkg.table_index, 7, col_update)
@@ -301,17 +320,18 @@ class AppsTable(QTableWidget):
self.setCellWidget(pkg.table_index, col, toolbar)
def _set_col_type(self, col: int, pkg: PackageView):
icon_data = self.cache_type_icon.get(pkg.model.get_type())
pixmap = self.cache_type_icon.get(pkg.model.get_type())
if not pixmap:
if icon_data is None:
pixmap = QIcon(pkg.model.get_type_icon_path()).pixmap(QSize(16, 16))
self.cache_type_icon[pkg.model.get_type()] = pixmap
icon_data = {'px': pixmap, 'tip': '{}: {}'.format(self.i18n['type'], pkg.get_type_label())}
self.cache_type_icon[pkg.model.get_type()] = icon_data
item = QLabel()
item.setPixmap(pixmap)
item.setPixmap(icon_data['px'])
item.setAlignment(Qt.AlignCenter)
item.setToolTip('{}: {}'.format(self.i18n['type'], pkg.model.get_type().capitalize()))
item.setToolTip(icon_data['tip'])
self.setCellWidget(pkg.table_index, col, item)
def _set_col_version(self, col: int, pkg: PackageView):
@@ -343,8 +363,8 @@ class AppsTable(QTableWidget):
item = QTableWidgetItem()
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
if pkg.model.name:
name = pkg.model.name
name = pkg.model.get_display_name()
if name:
item.setToolTip('{}: {}'.format(self.i18n['app.name'].lower(), pkg.model.get_name_tooltip()))
else:
name = '...'
@@ -359,7 +379,7 @@ class AppsTable(QTableWidget):
item.setText(name)
icon_path = pkg.model.get_disk_icon_path()
if self.disk_cache and pkg.model.supports_disk_cache() and icon_path and os.path.isfile(icon_path):
if pkg.model.supports_disk_cache() and icon_path and os.path.isfile(icon_path):
with open(icon_path, 'rb') as f:
icon_bytes = f.read()
pixmap = QPixmap()

View File

@@ -15,7 +15,7 @@ def new_pkgs_info() -> dict:
def update_info(pkgv: PackageView, pkgs_info: dict):
pkgs_info['available_types'][pkgv.model.get_type()] = pkgv.model.get_type_icon_path()
pkgs_info['available_types'][pkgv.model.get_type()] = {'icon': pkgv.model.get_type_icon_path(), 'label': pkgv.get_type_label()}
if pkgv.model.is_application():
pkgs_info['apps_count'] += 1

View File

@@ -1,13 +1,15 @@
import os
import time
import traceback
from pathlib import Path
from threading import Thread
from typing import Tuple
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QIcon, QPixmap, QIntValidator
from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \
QLabel, QSizePolicy, QLineEdit, QToolButton, QHBoxLayout, QFormLayout, QFileDialog, QTabWidget, QVBoxLayout, \
QSlider
QSlider, QScrollArea, QFrame
from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType, \
TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, TabGroupComponent, PanelComponent, \
@@ -25,6 +27,9 @@ class RadioButtonQt(QRadioButton):
self.model_parent = model_parent
self.toggled.connect(self._set_checked)
if model.icon_path:
self.setIcon(QIcon(model.icon_path))
if self.model.read_only:
self.setAttribute(Qt.WA_TransparentForMouseEvents)
self.setFocusPolicy(Qt.NoFocus)
@@ -90,7 +95,8 @@ class FormComboBoxQt(QComboBox):
self.setMaximumWidth(model.max_width)
for idx, op in enumerate(self.model.options):
self.addItem(op.label, op.value)
icon = QIcon(op.icon_path) if op.icon_path else QIcon()
self.addItem(icon, op.label, op.value)
if op.tooltip:
self.setItemData(idx, op.tooltip, Qt.ToolTipRole)
@@ -178,7 +184,7 @@ class ComboSelectQt(QGroupBox):
self.model = model
self.setLayout(QGridLayout())
self.setStyleSheet('QGridLayout {margin-left: 0} QLabel { font-weight: bold}')
self.layout().addWidget(QLabel(model.label + ' :'), 0, 0)
self.layout().addWidget(QLabel(model.label + ' :' if model.label else ''), 0, 0)
self.layout().addWidget(FormComboBoxQt(model), 0, 1)
@@ -352,15 +358,27 @@ class InputFilter(QLineEdit):
super(InputFilter, self).__init__()
self.on_key_press = on_key_press
self.last_text = ''
self.typing = None
def keyPressEvent(self, event):
super(InputFilter, self).keyPressEvent(event)
def notify_text_change(self):
time.sleep(2)
text = self.text().strip()
if text != self.last_text:
self.last_text = text
self.on_key_press()
self.typing = None
def keyPressEvent(self, event):
super(InputFilter, self).keyPressEvent(event)
if self.typing:
return
self.typing = Thread(target=self.notify_text_change, daemon=True)
self.typing.start()
def get_text(self):
return self.last_text
@@ -464,11 +482,17 @@ class FormQt(QGroupBox):
label_comp = QLabel()
label.layout().addWidget(label_comp)
if hasattr(comp, 'size') and comp.size is not None:
label_comp.setStyleSheet("QLabel { font-size: " + str(comp.size) + "px }")
attr = 'label' if hasattr(comp,'label') else 'value'
text = getattr(comp, attr)
if text:
label_comp.setText(text.capitalize())
if hasattr(comp, 'capitalize_label') and getattr(comp, 'capitalize_label'):
label_comp.setText(text.capitalize())
else:
label_comp.setText(text)
if comp.tooltip:
label.layout().addWidget(self.gen_tip_icon(comp.tooltip))
@@ -499,7 +523,7 @@ class FormQt(QGroupBox):
line_edit.setPlaceholderText(c.placeholder)
if c.value:
line_edit.setText(c.value)
line_edit.setText(str(c.value) if c.value else '')
line_edit.setCursorPosition(0)
if c.read_only:
@@ -604,7 +628,11 @@ class TabGroupQt(QTabWidget):
traceback.print_exc()
icon = QIcon()
self.addTab(to_widget(c.content, i18n), icon, c.label)
scroll = QScrollArea()
scroll.setFrameShape(QFrame.NoFrame)
scroll.setWidgetResizable(True)
scroll.setWidget(to_widget(c.content, i18n))
self.addTab(scroll, icon, c.label)
def new_single_select(model: SingleSelectComponent) -> QWidget:
@@ -643,6 +671,10 @@ def to_widget(comp: ViewComponent, i18n: I18n, parent: QWidget = None) -> QWidge
return TwoStateButtonQt(comp)
elif isinstance(comp, TextComponent):
label = QLabel(comp.value)
if comp.size is not None:
label.setStyleSheet("QLabel { font-size: " + str(comp.size) + "px }")
label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
return label
elif isinstance(comp, SpacerComponent):

View File

@@ -12,7 +12,7 @@ from bauh.view.util.translation import I18n
class ConfirmationDialog(QMessageBox):
def __init__(self, title: str, body: str, i18n: I18n, screen_size: QSize, components: List[ViewComponent] = None,
confirmation_label: str = None, deny_label: str = None):
confirmation_label: str = None, deny_label: str = None, deny_button: bool = True):
super(ConfirmationDialog, self).__init__()
self.setWindowTitle(title)
self.setStyleSheet('QLabel { margin-right: 25px; }')
@@ -20,7 +20,8 @@ class ConfirmationDialog(QMessageBox):
self.bt_yes.setStyleSheet(css.OK_BUTTON)
self.setDefaultButton(self.bt_yes)
self.addButton(i18n['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole)
if deny_button:
self.addButton(i18n['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole)
label = None
if body:

View File

@@ -96,7 +96,7 @@ class GemSelectorPanel(QWidget):
self.manager.prepare()
self.window.verify_warnings()
self.window.types_changed = True
self.window.refresh_apps()
self.window.refresh_packages()
self.close()
def exit(self):

View File

@@ -7,6 +7,7 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.model import PackageHistory
from bauh.view.qt.view_model import get_type_label, PackageView
from bauh.view.util.translation import I18n
@@ -16,7 +17,9 @@ class HistoryDialog(QDialog):
super(HistoryDialog, self).__init__()
self.setWindowFlags(self.windowFlags() | Qt.WindowSystemMenuHint | Qt.WindowMinMaxButtonsHint)
self.setWindowTitle('{} - {} ({})'.format(i18n['popup.history.title'], history.pkg.name, history.pkg.get_type()))
view = PackageView(model=history.pkg, i18n=i18n)
self.setWindowTitle('{} - {}'.format(i18n['popup.history.title'], view))
layout = QVBoxLayout()
self.setLayout(layout)

View File

@@ -49,7 +49,7 @@ class InfoDialog(QDialog):
for idx, attr in enumerate(sorted(app.keys())):
if attr not in IGNORED_ATTRS and app[attr]:
i18n_key = app['__app__'].model.get_type().lower() + '.info.' + attr.lower()
i18n_key = app['__app__'].model.gem_name + '.info.' + attr.lower()
if isinstance(app[attr], list):
val = ' '.join([str(e).strip() for e in app[attr] if e])

311
bauh/view/qt/prepare.py Normal file
View File

@@ -0,0 +1,311 @@
import datetime
import operator
import time
from functools import reduce
from typing import Tuple
from PyQt5.QtCore import QSize, Qt, QThread, pyqtSignal, QCoreApplication
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy, QTableWidget, QHeaderView, QPushButton, QToolBar, \
QProgressBar, QApplication
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.handler import TaskManager
from bauh.view.qt import root
from bauh.view.qt.components import new_spacer
from bauh.view.qt.qt_utils import centralize
from bauh.view.qt.thread import AnimateProgress
from bauh.view.util.translation import I18n
class Prepare(QThread, TaskManager):
signal_register = pyqtSignal(str, str, object)
signal_update = pyqtSignal(str, float, str)
signal_finished = pyqtSignal(str)
signal_started = pyqtSignal()
signal_ask_password = pyqtSignal()
def __init__(self, context: ApplicationContext, manager: SoftwareManager, i18n: I18n):
super(Prepare, self).__init__()
self.manager = manager
self.i18n = i18n
self.context = context
self.waiting_password = False
self.password_response = None
def ask_password(self) -> Tuple[str, bool]:
self.waiting_password = True
self.signal_ask_password.emit()
while self.waiting_password:
time.sleep(0.1) # waiting for user input
return self.password_response
def set_password_reply(self, password: str, valid: bool):
self.password_response = password, valid
self.waiting_password = False
def run(self):
root_pwd = None
if self.manager.requires_root('prepare', None):
root_pwd, ok = self.ask_password()
if not ok:
QCoreApplication.exit(1)
self.signal_started.emit()
self.manager.prepare(self, root_pwd, None)
def update_progress(self, task_id: str, progress: float, substatus: str):
self.signal_update.emit(task_id, progress, substatus)
def register_task(self, id_: str, label: str, icon_path: str):
self.signal_register.emit(id_, label, icon_path)
def finish_task(self, task_id: str):
self.signal_finished.emit(task_id)
class CheckFinished(QThread):
signal_finished = pyqtSignal()
def __init__(self):
super(CheckFinished, self).__init__()
self.total = None
self.finished = None
def run(self):
while True:
if self.total is not None and self.finished is not None:
if self.total == self.finished:
break
time.sleep(0.01)
self.signal_finished.emit()
def update(self, total: int, finished: int):
if total is not None:
self.total = total
if finished is not None:
self.finished = finished
class EnableSkip(QThread):
signal_timeout = pyqtSignal()
def run(self):
ti = datetime.datetime.now()
while True:
if datetime.datetime.now() >= ti + datetime.timedelta(minutes=1.5):
self.signal_timeout.emit()
break
time.sleep(0.1)
class PreparePanel(QWidget, TaskManager):
signal_status = pyqtSignal(object, object)
signal_password_response = pyqtSignal(str, bool)
def __init__(self, context: ApplicationContext, manager: SoftwareManager, screen_size: QSize, i18n: I18n, manage_window: QWidget):
super(PreparePanel, self).__init__()
self.i18n = i18n
self.context = context
self.manage_window = manage_window
self.setWindowTitle(' ')
self.setMinimumWidth(screen_size.width() * 0.5)
self.setMinimumHeight(screen_size.height() * 0.35)
self.setMaximumHeight(screen_size.height() * 0.95)
self.setLayout(QVBoxLayout())
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.manager = manager
self.tasks = {}
self.ntasks = 0
self.ftasks = 0
self.self_close = False
self.prepare_thread = Prepare(self.context, manager, self.i18n)
self.prepare_thread.signal_register.connect(self.register_task)
self.prepare_thread.signal_update.connect(self.update_progress)
self.prepare_thread.signal_finished.connect(self.finish_task)
self.prepare_thread.signal_started.connect(self.start)
self.prepare_thread.signal_ask_password.connect(self.ask_root_password)
self.signal_password_response.connect(self.prepare_thread.set_password_reply)
self.check_thread = CheckFinished()
self.signal_status.connect(self.check_thread.update)
self.check_thread.signal_finished.connect(self.finish)
self.skip_thread = EnableSkip()
self.skip_thread.signal_timeout.connect(self._enable_skip_button)
self.progress_thread = AnimateProgress()
self.progress_thread.signal_change.connect(self._change_progress)
self.label_top = QLabel()
self.label_top.setText("{}...".format(self.i18n['prepare_panel.title.start'].capitalize()))
self.label_top.setAlignment(Qt.AlignHCenter)
self.label_top.setStyleSheet("QLabel { font-size: 14px; font-weight: bold; }")
self.layout().addWidget(self.label_top)
self.layout().addWidget(QLabel())
self.table = QTableWidget()
self.table.setStyleSheet("QTableWidget { background-color: transparent; }")
self.table.setFocusPolicy(Qt.NoFocus)
self.table.setShowGrid(False)
self.table.verticalHeader().setVisible(False)
self.table.horizontalHeader().setVisible(False)
self.table.horizontalHeader().setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(['' for _ in range(4)])
self.layout().addWidget(self.table)
toolbar = QToolBar()
self.bt_close = QPushButton(self.i18n['close'].capitalize())
self.bt_close.clicked.connect(self.close)
self.bt_close.setVisible(False)
self.ref_bt_close = toolbar.addWidget(self.bt_close)
toolbar.addWidget(new_spacer())
self.progress_bar = QProgressBar()
self.progress_bar.setMaximumHeight(10 if QApplication.instance().style().objectName().lower() == 'windows' else 4)
self.progress_bar.setTextVisible(False)
self.progress_bar.setVisible(False)
self.ref_progress_bar = toolbar.addWidget(self.progress_bar)
toolbar.addWidget(new_spacer())
self.bt_skip = QPushButton(self.i18n['prepare_panel.bt_skip.label'].capitalize())
self.bt_skip.clicked.connect(self.finish)
self.bt_skip.setEnabled(False)
toolbar.addWidget(self.bt_skip)
self.layout().addWidget(toolbar)
def ask_root_password(self):
root_pwd, ok = root.ask_root_password(self.context, self.i18n)
self.signal_password_response.emit(root_pwd, ok)
def _enable_skip_button(self):
self.bt_skip.setEnabled(True)
def _change_progress(self, value: int):
self.progress_bar.setValue(value)
def get_table_width(self) -> int:
return reduce(operator.add, [self.table.columnWidth(i) for i in range(self.table.columnCount())])
def _resize_columns(self):
header_horizontal = self.table.horizontalHeader()
for i in range(self.table.columnCount()):
header_horizontal.setSectionResizeMode(i, QHeaderView.ResizeToContents)
self.resize(self.get_table_width() * 1.05, self.sizeHint().height())
def show(self):
super(PreparePanel, self).show()
self.prepare_thread.start()
centralize(self)
def start(self):
self.ref_bt_close.setVisible(True)
self.check_thread.start()
self.skip_thread.start()
self.ref_progress_bar.setVisible(True)
self.progress_thread.start()
def closeEvent(self, QCloseEvent):
if not self.self_close:
QCoreApplication.exit()
def register_task(self, id_: str, label: str, icon_path: str):
self.ntasks += 1
self.table.setRowCount(self.ntasks)
task_row = self.ntasks - 1
lb_icon = QLabel()
lb_icon.setContentsMargins(0, 2, 0, 0)
lb_icon.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
if icon_path:
lb_icon.setPixmap(QIcon(icon_path).pixmap(12, 12))
lb_icon.setAlignment(Qt.AlignHCenter)
self.table.setCellWidget(task_row, 0, lb_icon)
lb_status = QLabel(label)
lb_status.setAlignment(Qt.AlignHCenter)
lb_status.setContentsMargins(2, 0, 2, 0)
lb_status.setMinimumWidth(50)
lb_status.setAlignment(Qt.AlignHCenter)
lb_status.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
lb_status.setStyleSheet("QLabel { color: blue; font-weight: bold; }")
self.table.setCellWidget(task_row, 1, lb_status)
lb_sub = QLabel()
lb_sub.setContentsMargins(2, 0, 2, 0)
lb_sub.setAlignment(Qt.AlignHCenter)
lb_sub.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
lb_sub.setMinimumWidth(50)
self.table.setCellWidget(task_row, 2, lb_sub)
lb_progress = QLabel('{0:.2f}'.format(0) + '%')
lb_progress.setContentsMargins(2, 2, 2, 2)
lb_progress.setStyleSheet("QLabel { color: blue; font-weight: bold; }")
lb_progress.setAlignment(Qt.AlignHCenter)
lb_progress.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.table.setCellWidget(task_row, 3, lb_progress)
self.tasks[id_] = {'lb_status': lb_status,
'lb_prog': lb_progress,
'progress': 0,
'lb_sub': lb_sub,
'finished': False}
self.signal_status.emit(self.ntasks, self.ftasks)
def update_progress(self, task_id: str, progress: float, substatus: str):
task = self.tasks[task_id]
if progress != task['progress']:
task['progress'] = progress
task['lb_prog'].setText('{0:.2f}'.format(progress) + '%')
if substatus:
task['lb_sub'].setText('( {} )'.format(substatus))
else:
task['lb_sub'].setText('')
self._resize_columns()
def finish_task(self, task_id: str):
task = self.tasks[task_id]
task['lb_sub'].setText('')
for key in ('lb_prog', 'lb_status'):
task[key].setStyleSheet('QLabel { color: green; text-decoration: line-through; }')
task['finished'] = True
self._resize_columns()
self.ftasks += 1
self.signal_status.emit(self.ntasks, self.ftasks)
if self.ntasks == self.ftasks:
self.label_top.setText("... {} ...".format(self.i18n['ready'].capitalize()))
def finish(self):
if self.isVisible():
self.manage_window.refresh_packages()
self.manage_window.show()
self.self_close = True
self.close()

View File

@@ -1,9 +1,12 @@
import os
from typing import Tuple
from PyQt5.QtWidgets import QInputDialog, QLineEdit
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.view import MessageType
from bauh.commons.system import new_subprocess
from bauh.view.core.config import read_config
from bauh.view.qt.dialog import show_message
from bauh.view.qt.view_utils import load_resource_icon
from bauh.view.util.translation import I18n
@@ -13,7 +16,14 @@ def is_root():
return os.getuid() == 0
def ask_root_password(i18n: I18n):
def ask_root_password(context: ApplicationContext, i18n: I18n, app_config: dict = None) -> Tuple[str, bool]:
cur_config = read_config() if not app_config else app_config
store_password = bool(cur_config['store_root_password'])
if store_password and context.root_password and validate_password(context.root_password):
return context.root_password, True
diag = QInputDialog()
diag.setStyleSheet("""QLineEdit { border-radius: 5px; font-size: 16px; border: 1px solid lightblue }""")
diag.setInputMode(QInputDialog.TextInput)
@@ -44,6 +54,9 @@ def ask_root_password(i18n: I18n):
diag.setTextValue('')
if ok:
if store_password:
context.root_password = diag.textValue()
return diag.textValue(), ok
else:
break

View File

@@ -1,28 +1,29 @@
import gc
from io import StringIO
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtCore import QSize, Qt, QCoreApplication
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QToolBar, QSizePolicy, QPushButton
from bauh import __app_name__
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.view import MessageType
from bauh.view.core.controller import GenericSoftwareManager
from bauh.view.qt import dialog, css
from bauh.view.qt.components import to_widget, new_spacer
from bauh.view.qt.qt_utils import centralize
from bauh.view.util import util
from bauh.view.util.translation import I18n
class SettingsWindow(QWidget):
def __init__(self, manager: SoftwareManager, i18n: I18n, screen_size: QSize, tray, window: QWidget, parent: QWidget = None):
def __init__(self, manager: SoftwareManager, i18n: I18n, screen_size: QSize, window: QWidget, parent: QWidget = None):
super(SettingsWindow, self).__init__(parent=parent)
self.setWindowTitle(i18n['settings'].capitalize())
self.setWindowTitle('{} ({})'.format(i18n['settings'].capitalize(), __app_name__))
self.setLayout(QVBoxLayout())
self.manager = manager
self.i18n = i18n
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.tray = tray
self.window = window
self.settings_model = self.manager.get_settings(screen_size.width(), screen_size.height())
@@ -49,13 +50,16 @@ class SettingsWindow(QWidget):
self.layout().addWidget(action_bar)
def show(self):
super(SettingsWindow, self).show()
centralize(self)
def closeEvent(self, event):
if self.window and self.window.settings_window == self:
self.deleteLater()
self.window.settings_window = None
elif self.tray and self.tray.settings_window == self:
self.deleteLater()
self.tray.settings_window = None
elif not self.window:
QCoreApplication.exit()
gc.collect()
@@ -70,24 +74,29 @@ class SettingsWindow(QWidget):
def _save_settings(self):
success, warnings = self.manager.save_settings(self.settings_model)
# Configurações alteradas com sucesso, porém algumas delas só surtirão após a reinicialização
if success:
if dialog.ask_confirmation(title=self.i18n['warning'].capitalize(),
body="<p>{}</p><p>{}</p>".format(self.i18n['settings.changed.success.warning'],
self.i18n['settings.changed.success.reboot']),
i18n=self.i18n):
util.restart_app(self.window and self.window.isVisible())
if not self.window:
dialog.show_message(title=self.i18n['success'].capitalize(),
body=self.i18n['settings.changed.success.warning'],
type_=MessageType.INFO)
QCoreApplication.exit()
elif dialog.ask_confirmation(title=self.i18n['warning'].capitalize(),
body="<p>{}</p><p>{}</p>".format(self.i18n['settings.changed.success.warning'],
self.i18n['settings.changed.success.reboot']),
i18n=self.i18n):
self.close()
util.restart_app()
else:
if isinstance(self.manager, GenericSoftwareManager):
self.manager.reset_cache()
self.manager.prepare()
self.manager.prepare(task_manager=None, root_password=None, internet_available=None)
if self.window and self.window.isVisible():
self.window.update_custom_actions()
self.window.verify_warnings()
self.window.types_changed = True
self.window.refresh_apps()
self.window.refresh_packages()
self.close()
else:
msg = StringIO()

View File

@@ -1,48 +1,82 @@
import json
import os
import sys
import time
import traceback
from io import StringIO
from subprocess import Popen
from threading import Lock, Thread
from typing import List
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt, QSize
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QWidget, QSizePolicy
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
from bauh import __app_name__
from bauh.api.abstract.controller import SoftwareManager
from bauh import __app_name__, ROOT_DIR
from bauh.api.abstract.model import PackageUpdate
from bauh.view.qt import qt_utils
from bauh.commons.system import run_cmd
from bauh.context import generate_i18n
from bauh.view.core.tray_client import TRAY_CHECK_FILE
from bauh.view.qt.about import AboutDialog
from bauh.view.qt.settings import SettingsWindow
from bauh.view.qt.view_utils import load_resource_icon
from bauh.view.qt.window import ManageWindow
from bauh.view.util import util
from bauh.view.util.translation import I18n
from bauh.view.util import util, resource
def list_updates() -> List[PackageUpdate]:
if run_cmd('which bauh-cli', print_error=False):
output = run_cmd('bauh-cli updates -f json')
if output:
return [PackageUpdate(pkg_id=o['id'], name=o['name'], version=o['version'], pkg_type=o['type']) for o in json.loads(output)]
return []
class UpdateCheck(QThread):
signal = pyqtSignal(list)
def __init__(self, manager: SoftwareManager, check_interval: int, parent=None):
def __init__(self, check_interval: int, lock: Lock, check_file: bool, parent=None):
super(UpdateCheck, self).__init__(parent)
self.check_interval = check_interval
self.manager = manager
self.lock = lock
self.check_file = check_file
def _notify_updates(self):
self.lock.acquire()
try:
updates = list_updates()
if updates is not None:
self.signal.emit(updates)
finally:
self.lock.release()
time.sleep(self.check_interval)
def run(self):
while True:
updates = self.manager.list_updates()
self.signal.emit(updates)
time.sleep(self.check_interval)
if self.check_file:
if os.path.exists(TRAY_CHECK_FILE):
self._notify_updates()
try:
os.remove(TRAY_CHECK_FILE)
except:
traceback.print_exc()
else:
time.sleep(self.check_interval)
else:
self._notify_updates()
class TrayIcon(QSystemTrayIcon):
def __init__(self, i18n: I18n, manager: SoftwareManager, manage_window: ManageWindow, config: dict, screen_size: QSize):
def __init__(self, config: dict, screen_size: QSize, manage_process: Popen = None, settings_process: Popen = None):
super(TrayIcon, self).__init__()
self.i18n = i18n
self.manager = manager
self.i18n = generate_i18n(config, resource.get_path('locale/tray'))
self.screen_size = screen_size
self.manage_process = manage_process
self.settings_process = settings_process
if config['ui']['tray']['default_icon']:
self.icon_default = QIcon(config['ui']['tray']['default_icon'])
@@ -67,7 +101,7 @@ class TrayIcon(QSystemTrayIcon):
self.action_manage = self.menu.addAction(self.i18n['tray.action.manage'])
self.action_manage.triggered.connect(self.show_manage_window)
self.action_settings = self.menu.addAction(self.i18n['settings'].capitalize())
self.action_settings = self.menu.addAction(self.i18n['tray.settings'].capitalize())
self.action_settings.triggered.connect(self.show_settings_window)
self.action_about = self.menu.addAction(self.i18n['tray.action.about'])
@@ -82,10 +116,15 @@ class TrayIcon(QSystemTrayIcon):
self.dialog_about = None
self.settings_window = None
self.check_thread = UpdateCheck(check_interval=int(config['updates']['check_interval']), manager=self.manager)
self.check_lock = Lock()
self.check_thread = UpdateCheck(check_interval=int(config['updates']['check_interval']), check_file=False, lock=self.check_lock)
self.check_thread.signal.connect(self.notify_updates)
self.check_thread.start()
self.recheck_thread = UpdateCheck(check_interval=2, check_file=True, lock=self.check_lock)
self.recheck_thread.signal.connect(self.notify_updates)
self.recheck_thread.start()
self.last_updates = set()
self.update_notification = bool(config['system']['notifications'])
self.lock_notify = Lock()
@@ -93,10 +132,8 @@ class TrayIcon(QSystemTrayIcon):
self.activated.connect(self.handle_click)
self.set_default_tooltip()
self.manage_window = manage_window
def set_default_tooltip(self):
self.setToolTip('{} ({})'.format(self.i18n['manage_window.title'], __app_name__).lower())
self.setToolTip('{} ({})'.format(self.i18n['tray.action.manage'], __app_name__).lower())
def handle_click(self, reason):
if reason == self.Trigger:
@@ -109,7 +146,6 @@ class TrayIcon(QSystemTrayIcon):
self.notify_updates(self.manager.list_updates(), notify_user=notify_user)
def notify_updates(self, updates: List[PackageUpdate], notify_user: bool = True):
self.lock_notify.acquire()
try:
@@ -133,8 +169,8 @@ class TrayIcon(QSystemTrayIcon):
msg.write(self.i18n['notification.update{}'.format('' if n_updates == 1 else 's')].format(n_updates))
if len(ups_by_type) > 1:
for ptype, count in ups_by_type.items():
msg.write('\n * {} ( {} )'.format(ptype.capitalize(), count))
for ptype in sorted(ups_by_type):
msg.write('\n * {} ( {} )'.format(ptype, ups_by_type[ptype]))
msg.seek(0)
msg = msg.read()
@@ -155,25 +191,18 @@ class TrayIcon(QSystemTrayIcon):
self.lock_notify.release()
def show_manage_window(self):
if self.manage_window.isMinimized():
self.manage_window.setWindowState(Qt.WindowNoState)
elif not self.manage_window.isVisible():
self.manage_window.refresh_apps()
self.manage_window.show()
if self.manage_process is None:
self.manage_process = Popen([sys.executable, '{}/app.py'.format(ROOT_DIR)])
elif self.manage_process.poll() is not None: # it means it has finished
self.manage_process = None
self.show_manage_window()
def show_settings_window(self):
if self.settings_window:
self.settings_window.handle_display()
else:
self.settings_window = SettingsWindow(manager=self.manager,
i18n=self.i18n,
screen_size=self.screen_size,
tray=self,
window=self.manage_window)
self.settings_window.setMinimumWidth(int(self.screen_size.width() / 4))
self.settings_window.adjustSize()
qt_utils.centralize(self.settings_window)
self.settings_window.show()
if self.settings_process is None:
self.settings_process = Popen([sys.executable, '{}/app.py'.format(ROOT_DIR), '--settings'])
elif self.settings_process.poll() is not None: # it means it has finished
self.settings_process = None
self.show_settings_window()
def show_about(self):
if self.dialog_about is None:

View File

@@ -1,22 +1,30 @@
import os
import re
import time
import traceback
from datetime import datetime, timedelta
from io import StringIO
from pathlib import Path
from typing import List, Type, Set, Tuple
import requests
from PyQt5.QtCore import QThread, pyqtSignal
from bauh import LOGS_PATH
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.controller import SoftwareManager, UpgradeRequirement, UpgradeRequirements
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import PackageStatus, SoftwarePackage, PackageAction
from bauh.api.abstract.view import InputViewComponent, MessageType, MultipleSelectComponent, InputOption
from bauh.api.abstract.model import PackageStatus, SoftwarePackage, CustomSoftwareAction
from bauh.api.abstract.view import MessageType, MultipleSelectComponent, InputOption, TextComponent, \
FormComponent, ViewComponent
from bauh.api.exception import NoInternetException
from bauh.commons import user
from bauh.commons.html import bold
from bauh.view.core import config
from bauh.commons.system import get_human_size_str, ProcessHandler, SimpleProcess
from bauh.view.core import timeshift
from bauh.view.core.config import read_config
from bauh.view.qt import commons
from bauh.view.qt.view_model import PackageView
from bauh.view.qt.view_model import PackageView, PackageViewStatus
from bauh.view.util.translation import I18n
RE_VERSION_IN_NAME = re.compile(r'\s+version\s+[\w\.]+\s*$')
@@ -32,6 +40,7 @@ class AsyncAction(QThread, ProcessWatcher):
signal_substatus = pyqtSignal(str) # changes the GUI substatus message
signal_progress = pyqtSignal(int)
signal_root_password = pyqtSignal()
signal_progress_control = pyqtSignal(bool)
def __init__(self):
super(AsyncAction, self).__init__()
@@ -40,9 +49,9 @@ class AsyncAction(QThread, ProcessWatcher):
self.root_password = None
self.stop = False
def request_confirmation(self, title: str, body: str, components: List[InputViewComponent] = None, confirmation_label: str = None, deny_label: str = None) -> bool:
def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None, deny_button: bool = True) -> bool:
self.wait_confirmation = True
self.signal_confirmation.emit({'title': title, 'body': body, 'components': components, 'confirmation_label': confirmation_label, 'deny_label': deny_label})
self.signal_confirmation.emit({'title': title, 'body': body, 'components': components, 'confirmation_label': confirmation_label, 'deny_label': deny_label, 'deny_button': deny_button})
self.wait_user()
return self.confirmation_res
@@ -90,117 +99,374 @@ class AsyncAction(QThread, ProcessWatcher):
def should_stop(self):
return self.stop
def enable_progress_controll(self):
self.signal_progress_control.emit(True)
class UpdateSelectedApps(AsyncAction):
def disable_progress_controll(self):
self.signal_progress_control.emit(False)
def __init__(self, manager: SoftwareManager, i18n: I18n, pkgs: List[PackageView] = None):
super(UpdateSelectedApps, self).__init__()
self.pkgs = pkgs
self.manager = manager
self.root_password = None
self.i18n = i18n
def request_reboot(self, msg: str) -> bool:
if self.request_confirmation(title=self.i18n['action.request_reboot.title'],
body=msg,
confirmation_label=self.i18n['yes'].capitalize(),
deny_label=self.i18n['bt.not_now']):
ProcessHandler(self).handle_simple(SimpleProcess(['reboot']))
return True
def _pkg_as_option(self, pkg: SoftwarePackage, tooltip: bool = True) -> InputOption:
if pkg.installed:
icon_path = pkg.get_disk_icon_path()
return False
if not icon_path or not os.path.isfile(icon_path):
icon_path = pkg.get_type_icon_path()
else:
icon_path = pkg.get_type_icon_path()
return InputOption(label='{}{}'.format(pkg.name, ' ( {} )'.format(pkg.latest_version) if pkg.latest_version else ''),
value=None,
tooltip=pkg.get_name_tooltip() if tooltip else None,
read_only=True,
icon_path=icon_path)
def _handle_update_requirements(self, pkgs: List[SoftwarePackage]) -> bool:
self.change_substatus(self.i18n['action.update.requirements.status'])
required_pkgs = self.manager.get_update_requirements(pkgs, self)
if required_pkgs:
opts = [self._pkg_as_option(p) for p in required_pkgs]
if not self.request_confirmation(self.i18n['action.update.requirements.title'],
self.i18n['action.update.requirements.body'].format(bold(str(len(opts)))),
[MultipleSelectComponent(label='', options=opts, default_options=set(opts))],
confirmation_label=self.i18n['continue'].capitalize(),
deny_label=self.i18n['cancel'].capitalize()):
self.notify_finished({'success': False, 'updated': 0, 'types': set()})
self.pkgs = None
def _generate_backup(self, app_config: dict, i18n: I18n, root_password: str) -> bool:
if timeshift.is_available():
if app_config['backup']['mode'] not in ('only_one', 'incremental'):
self.show_message(title=self.i18n['error'].capitalize(),
body='{}: {}'.format(self.i18n['action.backup.invalid_mode'], bold(app_config['backup']['mode'])),
type_=MessageType.ERROR)
return False
if not user.is_root() and not root_password:
root_pwd, valid = self.request_root_password()
else:
for pkg in required_pkgs:
if not self.manager.install(pkg, self.root_password, self):
self.notify_finished({'success': False, 'updated': 0, 'types': set()})
self.pkgs = None
label = '{}{}'.format(pkg.name, ' ( {} )'.format(pkg.version) if pkg.version else '')
self.show_message(title=self.i18n['action.update.install_req.fail.title'],
body=self.i18n['action.update.install_req.fail.body'].format(label),
type_=MessageType.ERROR)
return False
root_pwd, valid = root_password, True
if not valid:
return False
handler = ProcessHandler(self)
if app_config['backup']['mode'] == 'only_one':
self.change_substatus('[{}] {}'.format(i18n['core.config.tab.backup'].lower(), i18n['action.backup.substatus.delete']))
deleted, _ = handler.handle_simple(timeshift.delete_all_snapshots(root_pwd))
if not deleted and not self.request_confirmation(title=i18n['core.config.tab.backup'],
body='{}. {}'.format(i18n['action.backup.error.delete'], i18n['action.backup.error.proceed']),
confirmation_label=i18n['yes'].capitalize(),
deny_label=i18n['no'].capitalize()):
return False
self.change_substatus('[{}] {}'.format(i18n['core.config.tab.backup'].lower(), i18n['action.backup.substatus.create']))
created, _ = handler.handle_simple(timeshift.create_snapshot(root_pwd))
if not created and not self.request_confirmation(title=i18n['core.config.tab.backup'],
body='{}. {}'.format(i18n['action.backup.error.create'],
i18n['action.backup.error.proceed']),
confirmation_label=i18n['yes'].capitalize(),
deny_label=i18n['no'].capitalize()):
return False
return True
def _sort_packages(self, pkgs: List[SoftwarePackage], app_config: dict) -> Tuple[bool, List[SoftwarePackage]]:
if bool(app_config['updates']['sort_packages']):
self.change_substatus(self.i18n['action.update.status.sorting'])
sorted_pkgs = self.manager.sort_update_order([view.model for view in self.pkgs])
else:
sorted_pkgs = pkgs
def request_backup(self, app_config: dict, key: str, i18n: I18n, root_password: str = None) -> bool:
if bool(app_config['backup']['enabled']) and timeshift.is_available():
val = app_config['backup'][key] if key else None
if len(sorted_pkgs) > 1:
opts = [self._pkg_as_option(p, tooltip=False) for p in sorted_pkgs]
proceed = self.request_confirmation(title=self.i18n['action.update.order.title'],
body=self.i18n['action.update.order.body'] + ':',
components=[MultipleSelectComponent(label='', options=opts, default_options=set(opts))],
confirmation_label=self.i18n['continue'].capitalize(),
deny_label=self.i18n['cancel'].capitalize())
else:
proceed = True
if val is None: # ask mode
if self.request_confirmation(title=i18n['core.config.tab.backup'],
body=i18n['action.backup.msg'],
confirmation_label=i18n['yes'].capitalize(),
deny_label=i18n['no'].capitalize()):
res = self._generate_backup(app_config, i18n, root_password)
self.change_substatus('')
return res
return proceed, sorted_pkgs
elif val is True: # direct mode
res = self._generate_backup(app_config, i18n, root_password)
self.change_substatus('')
return res
return True
class UpgradeSelected(AsyncAction):
LOGS_DIR = '{}/upgrade'.format(LOGS_PATH)
SUMMARY_FILE = LOGS_DIR + '/{}_summary.txt'
def __init__(self, manager: SoftwareManager, i18n: I18n, pkgs: List[PackageView] = None):
super(UpgradeSelected, self).__init__()
self.pkgs = pkgs
self.manager = manager
self.i18n = i18n
def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None) -> InputOption:
if req.pkg.installed:
icon_path = req.pkg.get_disk_icon_path()
if not icon_path or not os.path.isfile(icon_path):
icon_path = req.pkg.get_type_icon_path()
else:
icon_path = req.pkg.get_type_icon_path()
size_str = '{}: {}'.format(self.i18n['size'].capitalize(),
'?' if req.extra_size is None else get_human_size_str(req.extra_size))
if req.extra_size != req.required_size:
size_str += ' ( {}: {} )'.format(self.i18n['action.update.pkg.required_size'].capitalize(),
'?' if req.required_size is None else get_human_size_str(req.required_size))
label = '{}{} - {}'.format(req.pkg.name,
' ( {} )'.format(req.pkg.latest_version) if req.pkg.latest_version else '',
size_str)
return InputOption(label=label,
value=None,
tooltip=custom_tooltip if custom_tooltip else (req.pkg.get_name_tooltip() if tooltip else None),
read_only=True,
icon_path=icon_path)
def filter_to_update(self) -> Tuple[List[PackageView], bool]: # packages to update and if they require root privileges
to_update, requires_root = [], False
root_user = user.is_root()
for pkg in self.pkgs:
if pkg.model.update and pkg.update_checked:
to_update.append(pkg)
if not root_user and not requires_root and self.manager.requires_root('update', pkg.model):
requires_root = True
return to_update, requires_root
def _sum_pkgs_size(self, reqs: List[UpgradeRequirement]) -> Tuple[int, int]:
required, extra = 0, 0
for r in reqs:
if r.required_size is not None:
required += r.required_size
if r.extra_size is not None:
extra += r.extra_size
return required, extra
def _gen_cannot_update_form(self, reqs: List[UpgradeRequirement]) -> FormComponent:
opts = [self._req_as_option(r.pkg, False, r.reason) for r in reqs]
comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))]
return FormComponent(label=self.i18n['action.update.cannot_update_label'], components=comps)
def _gen_to_install_form(self, reqs: List[UpgradeRequirement]) -> Tuple[FormComponent, Tuple[int, int]]:
opts = [self._req_as_option(r) for r in reqs]
comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))]
required_size, extra_size = self._sum_pkgs_size(reqs)
lb = '{} ( {}: {}. {}: {}. {}: {} )'.format(self.i18n['action.update.required_label'].capitalize(),
self.i18n['amount'].capitalize(),
len(opts),
self.i18n['size'].capitalize(),
'?' if extra_size is None else get_human_size_str(extra_size),
self.i18n['action.update.pkg.required_size'].capitalize(),
'?' if required_size is None else get_human_size_str(required_size))
return FormComponent(label=lb, components=comps), (required_size, extra_size)
def _gen_to_remove_form(self, reqs: List[UpgradeRequirement]) -> FormComponent:
opts = [self._req_as_option(r, False, r.reason) for r in reqs]
comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))]
required_size, extra_size = self._sum_pkgs_size(reqs)
lb = '{} ( {}: {}. {}: {} )'.format(self.i18n['action.update.label_to_remove'].capitalize(),
self.i18n['amount'].capitalize(),
len(opts),
self.i18n['size'].capitalize(),
'?' if extra_size is None else get_human_size_str(-extra_size))
return FormComponent(label=lb, components=comps)
def _gen_to_update_form(self, reqs: List[UpgradeRequirement]) -> Tuple[FormComponent, Tuple[int, int]]:
opts = [self._req_as_option(r, tooltip=False) for r in reqs]
comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))]
required_size, extra_size = self._sum_pkgs_size(reqs)
lb = '{} ( {}: {}. {}: {}. {}: {} )'.format(self.i18n['action.update.label_to_upgrade'].capitalize(),
self.i18n['amount'].capitalize(),
len(opts),
self.i18n['size'].capitalize(),
'?' if extra_size is None else get_human_size_str(extra_size),
self.i18n['action.update.pkg.required_size'].capitalize(),
'?' if required_size is None else get_human_size_str(required_size))
return FormComponent(label=lb, components=comps), (required_size, extra_size)
def _trim_disk(self, root_password: str):
if self.request_confirmation(title=self.i18n['confirmation'].capitalize(),
body=self.i18n['action.trim_disk.ask']):
pwd = root_password
if not pwd and user.is_root():
pwd, success = self.request_root_password()
if not success:
return
self.change_substatus(self.i18n['action.disk_trim'].capitalize())
success, output = ProcessHandler(self).handle_simple(SimpleProcess(['fstrim', '/', '-v'], root_password=pwd))
if not success:
self.show_message(title=self.i18n['success'].capitalize(),
body=self.i18n['action.disk_trim.error'],
type_=MessageType.ERROR)
def _write_summary_log(self, upgrade_id: str, requirements: UpgradeRequirements):
try:
Path(self.LOGS_DIR).mkdir(parents=True, exist_ok=True)
summary_text = StringIO()
summary_text.write('Upgrade summary ( id: {} )'.format(upgrade_id))
if requirements.cannot_upgrade:
summary_text.write('\nCannot upgrade:')
for dep in requirements.cannot_upgrade:
type_label = self.i18n.get('gem.{}.type.{}.label'.format(dep.pkg.gem_name, dep.pkg.get_type().lower()), dep.pkg.get_type().capitalize())
summary_text.write('\n * Type:{}\tName: {}\tVersion: {}\tReason: {}'.format(type_label, dep.pkg.name, dep.pkg.version if dep.pkg.version else '?', dep.reason if dep.reason else '?'))
summary_text.write('\n')
if requirements.to_remove:
summary_text.write('\nMust be removed:')
for dep in requirements.to_remove:
type_label = self.i18n.get('gem.{}.type.{}.label'.format(dep.pkg.gem_name, dep.pkg.get_type().lower()), dep.pkg.get_type().capitalize())
summary_text.write('\n * Type:{}\tName: {}\tVersion: {}\tReason: {}'.format(type_label, dep.pkg.name, dep.pkg.version if dep.pkg.version else '?', dep.reason if dep.reason else '?'))
summary_text.write('\n')
if requirements.to_install:
summary_text.write('\nMust be installed:')
for dep in requirements.to_install:
type_label = self.i18n.get(
'gem.{}.type.{}.label'.format(dep.pkg.gem_name, dep.pkg.get_type().lower()),
dep.pkg.get_type().capitalize())
summary_text.write('\n * Type:{}\tName: {}\tVersion: {}\tReason: {}'.format(type_label,
dep.pkg.name,
dep.pkg.version if dep.pkg.version else '?',
dep.reason if dep.reason else '?'))
summary_text.write('\n')
if requirements.to_upgrade:
summary_text.write('\nWill be upgraded:')
for dep in requirements.to_upgrade:
type_label = self.i18n.get('gem.{}.type.{}.label'.format(dep.pkg.gem_name, dep.pkg.get_type().lower()), dep.pkg.get_type().capitalize())
summary_text.write(
'\n * Type:{}\tName: {}\tVersion:{} \tNew version: {}'.format(type_label,
dep.pkg.name,
dep.pkg.version if dep.pkg.version else '?',
dep.pkg.latest_version if dep.pkg.latest_version else '?'))
summary_text.write('\n')
summary_text.seek(0)
with open(self.SUMMARY_FILE.format(upgrade_id), 'w+') as f:
f.write(summary_text.read())
except:
traceback.print_exc()
def run(self):
to_update, requires_root = self.filter_to_update()
root_password = None
success = False
if not user.is_root() and requires_root:
root_password, ok = self.request_root_password()
if self.pkgs:
updated, updated_types = 0, set()
app_config = config.read_config()
models = [view.model for view in self.pkgs]
if bool(app_config['updates']['pre_dependency_checking']) and not self._handle_update_requirements(models):
return
proceed, sorted_pkgs = self._sort_packages(models, app_config)
if not proceed:
self.notify_finished({'success': success, 'updated': updated, 'types': updated_types})
if not ok:
self.notify_finished({'success': False, 'updated': 0, 'types': set(), 'id': None})
self.pkgs = None
return
for pkg in sorted_pkgs:
self.change_substatus('')
name = pkg.name if not RE_VERSION_IN_NAME.findall(pkg.name) else pkg.name.split('version')[0].strip()
if len(to_update) > 1:
self.disable_progress_controll()
else:
self.enable_progress_controll()
self.change_status('{} {} {}...'.format(self.i18n['manage_window.status.upgrading'], name, pkg.version))
success = bool(self.manager.update(pkg, self.root_password, self))
self.change_substatus('')
success = False
if not success:
updated, updated_types = 0, set()
models = [view.model for view in to_update]
self.change_substatus(self.i18n['action.update.requirements.status'])
requirements = self.manager.get_upgrade_requirements(models, root_password, self)
if not requirements:
self.pkgs = None
self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': None})
return
comps, required_size, extra_size = [], 0, 0
if requirements.cannot_upgrade:
comps.append(self._gen_cannot_update_form(requirements.cannot_upgrade))
if requirements.to_install:
req_form, reqs_size = self._gen_to_install_form(requirements.to_install)
required_size += reqs_size[0]
extra_size += reqs_size[1]
comps.append(req_form)
if requirements.to_remove:
comps.append(self._gen_to_remove_form(requirements.to_remove))
updates_form, updates_size = self._gen_to_update_form(requirements.to_upgrade)
required_size += updates_size[0]
extra_size += updates_size[1]
comps.append(updates_form)
extra_size_text = '{}: {}'.format(self.i18n['action.update.total_size'].capitalize(), get_human_size_str(extra_size))
req_size_text = '{}: {}'.format(self.i18n['action.update.required_size'].capitalize(),
get_human_size_str(required_size))
comps.insert(0, TextComponent(bold('{} | {}').format(extra_size_text, req_size_text), size=14))
comps.insert(1, TextComponent(''))
if not self.request_confirmation(title=self.i18n['action.update.summary'].capitalize(), body='', components=comps):
self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': None})
self.pkgs = None
return
self.change_substatus('')
app_config = read_config()
if bool(app_config['backup']['enabled']) and app_config['backup']['upgrade'] in (True, None) and timeshift.is_available():
any_requires_bkp = False
for dep in requirements.to_upgrade:
if dep.pkg.supports_backup():
any_requires_bkp = True
break
else:
updated += 1
updated_types.add(pkg.__class__)
self.signal_output.emit('\n')
self.notify_finished({'success': success, 'updated': updated, 'types': updated_types})
if any_requires_bkp:
if not self.request_backup(app_config, 'upgrade', self.i18n, root_password):
self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': None})
self.pkgs = None
return
self.change_substatus('')
timestamp = datetime.now()
upgrade_id = 'upgrade_{}{}{}_{}'.format(timestamp.year, timestamp.month, timestamp.day, int(time.time()))
self._write_summary_log(upgrade_id, requirements)
success = bool(self.manager.upgrade(requirements, root_password, self))
self.change_substatus('')
if success:
updated = len(requirements.to_upgrade)
updated_types.update((req.pkg.__class__ for req in requirements.to_upgrade))
if bool(app_config['disk']['trim_after_update']):
self._trim_disk(root_password)
msg = '<p>{}</p>{}</p><br/><p>{}</p>'.format(self.i18n['action.update.success.reboot.line1'],
self.i18n['action.update.success.reboot.line2'],
self.i18n['action.update.success.reboot.line3'])
self.request_reboot(msg)
self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': upgrade_id})
self.pkgs = None
@@ -241,16 +507,24 @@ class RefreshApps(AsyncAction):
class UninstallApp(AsyncAction):
def __init__(self, manager: SoftwareManager, icon_cache: MemoryCache, app: PackageView = None):
def __init__(self, manager: SoftwareManager, icon_cache: MemoryCache, i18n: I18n, app: PackageView = None):
super(UninstallApp, self).__init__()
self.app = app
self.manager = manager
self.icon_cache = icon_cache
self.root_password = None
self.root_pwd = None
self.i18n = i18n
def run(self):
if self.app:
success = self.manager.uninstall(self.app.model, self.root_password, self)
if self.app.model.supports_backup():
if not self.request_backup(read_config(), 'uninstall', self.i18n, self.root_pwd):
self.notify_finished(False)
self.app = None
self.root_pwd = None
return
success = self.manager.uninstall(self.app.model, self.root_pwd, self)
if success:
self.icon_cache.delete(self.app.model.icon_url)
@@ -258,7 +532,7 @@ class UninstallApp(AsyncAction):
self.notify_finished(self.app if success else None)
self.app = None
self.root_password = None
self.root_pwd = None
class DowngradeApp(AsyncAction):
@@ -268,20 +542,28 @@ class DowngradeApp(AsyncAction):
self.manager = manager
self.app = app
self.i18n = i18n
self.root_password = None
self.root_pwd = None
def run(self):
if self.app:
success = False
if self.app.model.supports_backup():
if not self.request_backup(read_config(), 'downgrade', self.i18n, self.root_pwd):
self.notify_finished({'app': self.app, 'success': success})
self.app = None
self.root_pwd = None
return
try:
success = self.manager.downgrade(self.app.model, self.root_password, self)
success = self.manager.downgrade(self.app.model, self.root_pwd, self)
except (requests.exceptions.ConnectionError, NoInternetException) as e:
success = False
self.print(self.i18n['internet.required'])
finally:
self.notify_finished({'app': self.app, 'success': success})
self.app = None
self.root_password = None
self.root_pwd = None
class GetAppInfo(AsyncAction):
@@ -341,23 +623,27 @@ class SearchPackages(AsyncAction):
class InstallPackage(AsyncAction):
def __init__(self, manager: SoftwareManager, disk_cache: bool, icon_cache: MemoryCache, i18n: I18n, pkg: PackageView = None):
def __init__(self, manager: SoftwareManager, icon_cache: MemoryCache, i18n: I18n, pkg: PackageView = None):
super(InstallPackage, self).__init__()
self.pkg = pkg
self.manager = manager
self.icon_cache = icon_cache
self.disk_cache = disk_cache
self.i18n = i18n
self.root_password = None
self.root_pwd = None
def run(self):
if self.pkg:
success = False
try:
success = self.manager.install(self.pkg.model, self.root_password, self)
if self.pkg.model.supports_backup():
if not self.request_backup(read_config(), 'install', self.i18n, self.root_pwd):
self.signal_finished.emit({'success': False, 'pkg': self.pkg})
return
if success and self.disk_cache:
try:
success = self.manager.install(self.pkg.model, self.root_pwd, self)
if success:
self.pkg.model.installed = True
if self.pkg.model.supports_disk_cache():
@@ -405,6 +691,9 @@ class AnimateProgress(QThread):
self.increment = 0.5
self.paused = False
if self.progress_value >= val:
self.progress_value = val
def pause(self):
self.paused = True
@@ -440,47 +729,62 @@ class AnimateProgress(QThread):
self._reset()
class VerifyModels(QThread):
class NotifyPackagesReady(QThread):
signal_updates = pyqtSignal()
signal_finished = pyqtSignal()
signal_changed = pyqtSignal(int)
def __init__(self, apps: List[PackageView] = None):
super(VerifyModels, self).__init__()
self.apps = apps
def __init__(self, pkgs: List[PackageView] = None):
super(NotifyPackagesReady, self).__init__()
self.pkgs = pkgs
self.work = True
def run(self):
timeout = datetime.now() + timedelta(seconds=15)
if self.apps:
stop_at = datetime.utcnow() + timedelta(seconds=30)
last_ready = 0
while True:
to_verify = {p.table_index: p for p in self.pkgs}
while self.work and datetime.now() < timeout:
to_remove = []
for idx, pkg in to_verify.items():
if not self.work:
break
elif pkg.model.status == PackageStatus.READY:
to_remove.append(idx)
if pkg.status == PackageViewStatus.LOADING:
self.signal_changed.emit(pkg.table_index)
current_ready = 0
if not self.work:
break
for app in self.apps:
current_ready += 1 if app.model.status == PackageStatus.READY else 0
if datetime.now() >= timeout:
break
if current_ready > last_ready:
last_ready = current_ready
self.signal_updates.emit()
for idx in to_remove:
del to_verify[idx]
if current_ready == len(self.apps):
self.signal_updates.emit()
break
if not to_verify:
break
if stop_at <= datetime.utcnow():
break
time.sleep(0.1)
time.sleep(0.1)
self.pkgs = None
self.work = True
self.apps = None
self.signal_finished.emit()
class NotifyInstalledLoaded(QThread):
signal_loaded = pyqtSignal()
def __init__(self):
super(NotifyInstalledLoaded, self).__init__()
self.loaded = False
def notify_loaded(self):
self.loaded = True
def run(self):
time.sleep(0.1)
self.signal_loaded.emit()
class FindSuggestions(AsyncAction):
@@ -560,30 +864,37 @@ class ApplyFilters(AsyncAction):
class CustomAction(AsyncAction):
def __init__(self, manager: SoftwareManager, i18n: I18n, custom_action: PackageAction = None, pkg: PackageView = None, root_password: str = None):
def __init__(self, manager: SoftwareManager, i18n: I18n, custom_action: CustomSoftwareAction = None, pkg: PackageView = None, root_password: str = None):
super(CustomAction, self).__init__()
self.manager = manager
self.pkg = pkg
self.custom_action = custom_action
self.root_password = root_password
self.root_pwd = root_password
self.i18n = i18n
def run(self):
success = True
if self.pkg:
try:
success = self.manager.execute_custom_action(action=self.custom_action,
pkg=self.pkg.model,
root_password=self.root_password,
watcher=self)
except (requests.exceptions.ConnectionError, NoInternetException):
success = False
self.signal_output.emit(self.i18n['internet.required'])
if self.custom_action.backup:
app_config = read_config()
if not self.request_backup(app_config, None, self.i18n, self.root_pwd):
self.notify_finished({'success': False, 'pkg': self.pkg})
self.pkg = None
self.custom_action = None
self.root_pwd = None
return
try:
success = self.manager.execute_custom_action(action=self.custom_action,
pkg=self.pkg.model if self.pkg else None,
root_password=self.root_pwd,
watcher=self)
except (requests.exceptions.ConnectionError, NoInternetException):
success = False
self.signal_output.emit(self.i18n['internet.required'])
self.notify_finished({'success': success, 'pkg': self.pkg})
self.pkg = None
self.custom_action = None
self.root_password = None
self.root_pwd = None
class GetScreenshots(AsyncAction):

View File

@@ -1,6 +1,7 @@
from enum import Enum
from bauh.api.abstract.model import SoftwarePackage
from bauh.api.abstract.model import SoftwarePackage, PackageStatus
from bauh.view.util.translation import I18n
class PackageViewStatus(Enum):
@@ -8,13 +9,22 @@ class PackageViewStatus(Enum):
READY = 1
def get_type_label(type_: str, gem: str, i18n: I18n) -> str:
type_label = 'gem.{}.type.{}.label'.format(gem, type_)
return i18n.get(type_label, type_.capitalize()).strip()
class PackageView:
def __init__(self, model: SoftwarePackage):
def __init__(self, model: SoftwarePackage, i18n: I18n):
self.model = model
self.update_checked = model.update
self.status = PackageViewStatus.LOADING
self.status = PackageViewStatus.LOADING if model.status == PackageStatus.LOADING_DATA else PackageViewStatus.READY
self.table_index = -1
self.i18n = i18n
def get_type_label(self) -> str:
return get_type_label(self.model.get_type(), self.model.gem_name, self.i18n)
def __repr__(self):
return '{} ( {} )'.format(self.model.name, self.model.get_type())
return '{} ( {} )'.format(self.model.name, self.get_type_label())

View File

@@ -1,22 +1,25 @@
import logging
import time
import traceback
from pathlib import Path
from typing import List, Type, Set
from typing import List, Type, Set, Tuple
from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal, QCoreApplication
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent
from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHeaderView, QToolBar, \
QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy
QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy, \
QMenu, QAction
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
from bauh.api.abstract.model import SoftwarePackage, PackageAction
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.gems.web import TEMP_PATH
from bauh.view.core.tray_client import notify_tray
from bauh.view.qt import dialog, commons, qt_utils, root
from bauh.view.qt.about import AboutDialog
from bauh.view.qt.apps_table import AppsTable, UpdateToggleButton
@@ -27,10 +30,11 @@ from bauh.view.qt.info import InfoDialog
from bauh.view.qt.root import ask_root_password
from bauh.view.qt.screenshots import ScreenshotsDialog
from bauh.view.qt.settings import SettingsWindow
from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \
AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots
from bauh.view.qt.view_model import PackageView
from bauh.view.qt.thread import UpgradeSelected, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, NotifyPackagesReady, FindSuggestions, \
ListWarnings, \
AsyncAction, LaunchApp, ApplyFilters, CustomSoftwareAction, GetScreenshots, CustomAction, NotifyInstalledLoaded
from bauh.view.qt.view_model import PackageView, PackageViewStatus
from bauh.view.util import util, resource
from bauh.view.util.translation import I18n
@@ -49,12 +53,11 @@ class ManageWindow(QWidget):
signal_table_update = 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, tray_icon=None):
context: ApplicationContext, http_client: HttpClient, logger: logging.Logger, icon: QIcon):
super(ManageWindow, self).__init__()
self.i18n = i18n
self.logger = logger
self.manager = manager
self.tray_icon = tray_icon
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
@@ -188,7 +191,7 @@ class ManageWindow(QWidget):
self.bt_installed.setToolTip(self.i18n['manage_window.bt.installed.tooltip'])
self.bt_installed.setIcon(QIcon(resource.get_path('img/disk.svg')))
self.bt_installed.setText(self.i18n['manage_window.bt.installed.text'].capitalize())
self.bt_installed.clicked.connect(self._show_installed)
self.bt_installed.clicked.connect(self._begin_loading_installed)
self.bt_installed.setStyleSheet(toolbar_button_style('#A94E0A'))
self.ref_bt_installed = self.toolbar.addWidget(self.bt_installed)
toolbar_bts.append(self.bt_installed)
@@ -211,7 +214,7 @@ class ManageWindow(QWidget):
self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg')))
self.bt_refresh.setText(self.i18n['manage_window.bt.refresh.text'])
self.bt_refresh.setStyleSheet(toolbar_button_style('#2368AD'))
self.bt_refresh.clicked.connect(lambda: self.refresh_apps(keep_console=False))
self.bt_refresh.clicked.connect(lambda: self.refresh_packages(keep_console=False))
toolbar_bts.append(self.bt_refresh)
self.ref_bt_refresh = self.toolbar.addWidget(self.bt_refresh)
@@ -238,9 +241,7 @@ class ManageWindow(QWidget):
self.layout.addWidget(self.toolbar)
self.table_apps = AppsTable(self, self.icon_cache,
disk_cache=bool(self.config['disk_cache']['enabled']),
download_icons=bool(self.config['download']['icons']))
self.table_apps = AppsTable(self, self.icon_cache, download_icons=bool(self.config['download']['icons']))
self.table_apps.change_headers_policy()
self.layout.addWidget(self.table_apps)
@@ -275,9 +276,9 @@ class ManageWindow(QWidget):
self.layout.addWidget(self.toolbar_substatus)
self._change_label_substatus('')
self.thread_update = self._bind_async_action(UpdateSelectedApps(self.manager, self.i18n), finished_call=self._finish_update_selected)
self.thread_update = self._bind_async_action(UpgradeSelected(self.manager, self.i18n), finished_call=self._finish_upgrade_selected)
self.thread_refresh = self._bind_async_action(RefreshApps(self.manager), finished_call=self._finish_refresh_apps, only_finished=True)
self.thread_uninstall = self._bind_async_action(UninstallApp(self.manager, self.icon_cache), finished_call=self._finish_uninstall)
self.thread_uninstall = self._bind_async_action(UninstallApp(self.manager, self.icon_cache, self.i18n), finished_call=self._finish_uninstall)
self.thread_get_info = self._bind_async_action(GetAppInfo(self.manager), finished_call=self._finish_get_info)
self.thread_get_history = self._bind_async_action(GetAppHistory(self.manager, self.i18n), finished_call=self._finish_get_history)
self.thread_search = self._bind_async_action(SearchPackages(self.manager), finished_call=self._finish_search, only_finished=True)
@@ -292,14 +293,15 @@ class ManageWindow(QWidget):
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, disk_cache=bool(self.config['disk_cache']['enabled']), icon_cache=self.icon_cache, i18n=self.i18n)
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_verify_models = VerifyModels()
self.thread_verify_models.signal_updates.connect(self._notify_model_data_change)
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.toolbar_bottom = QToolBar()
self.toolbar_bottom.setIconSize(QSize(16, 16))
@@ -315,6 +317,15 @@ class ManageWindow(QWidget):
self.toolbar_bottom.addWidget(new_spacer())
self.custom_actions = manager.get_custom_actions()
bt_custom_actions = IconButton(QIcon(resource.get_path('img/custom_actions.svg')),
action=self.show_custom_actions,
background="#669900",
i18n=self.i18n,
tooltip=self.i18n['manage_window.bt_custom_actions.tip'])
bt_custom_actions.setVisible(bool(self.custom_actions))
self.ref_bt_custom_actions = self.toolbar_bottom.addWidget(bt_custom_actions)
bt_settings = IconButton(QIcon(resource.get_path('img/app_settings.svg')),
action=self.show_settings,
background='#12ABAB',
@@ -351,15 +362,20 @@ class ManageWindow(QWidget):
self.settings_window = None
self.search_performed = False
def set_tray_icon(self, tray_icon):
self.tray_icon = tray_icon
# self.combo_styles.show_panel_after_restart = bool(tray_icon)
self.thread_load_installed = NotifyInstalledLoaded()
self.thread_load_installed.signal_loaded.connect(self._finish_loading_installed)
def update_custom_actions(self):
self.custom_actions = self.manager.get_custom_actions()
self.ref_bt_custom_actions.setVisible(bool(self.custom_actions))
def _update_process_progress(self, val: int):
if self.progress_controll_enabled:
self.thread_animate_progress.set_progress(val)
def apply_filters_async(self):
self.thread_notify_pkgs_ready.work = False
self.thread_notify_pkgs_ready.wait(5)
self.label_status.setText(self.i18n['manage_window.status.filtering'] + '...')
self.ref_toolbar_search.setVisible(False)
@@ -370,19 +386,32 @@ class ManageWindow(QWidget):
self.thread_apply_filters.filters = self._gen_filters()
self.thread_apply_filters.pkgs = self.pkgs_available
self.thread_apply_filters.start()
self.checkbox_only_apps.setEnabled(False)
self.combo_categories.setEnabled(False)
self.combo_filter_type.setEnabled(False)
self.input_name_filter.setEnabled(False)
self.checkbox_updates.setEnabled(False)
self.table_apps.setEnabled(False)
def _update_table_and_upgrades(self, pkgs_info: dict):
self._update_table(pkgs_info=pkgs_info, signal=True)
self.update_bt_upgrade(pkgs_info)
if self.pkgs_available:
self._notify_model_data_change()
self.thread_verify_models.work = False
self.thread_verify_models.wait(50)
self.thread_verify_models.apps = self.pkgs_available
self.thread_verify_models.start()
if self.pkgs:
self._update_state_when_pkgs_ready()
self.thread_notify_pkgs_ready.work = False
self.thread_notify_pkgs_ready.wait(5)
self.thread_notify_pkgs_ready.pkgs = self.pkgs
self.thread_notify_pkgs_ready.work = True
self.thread_notify_pkgs_ready.start()
def _finish_apply_filters_async(self, success: bool):
self.checkbox_only_apps.setEnabled(True)
self.checkbox_updates.setEnabled(True)
self.combo_categories.setEnabled(True)
self.combo_filter_type.setEnabled(True)
self.input_name_filter.setEnabled(True)
self.table_apps.setEnabled(True)
self.label_status.setText('')
self.ref_toolbar_search.setVisible(True)
@@ -399,7 +428,8 @@ class ManageWindow(QWidget):
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_root_password.connect(self._ask_root_password)
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)
@@ -414,14 +444,16 @@ class ManageWindow(QWidget):
components=msg['components'],
confirmation_label=msg['confirmation_label'],
deny_label=msg['deny_label'],
deny_button=msg['deny_button'],
screen_size=self.screen_size)
res = diag.is_confirmed()
self.thread_animate_progress.animate()
self.signal_user_res.emit(res)
def _ask_root_password(self):
def _pause_and_ask_root_password(self):
self.thread_animate_progress.pause()
password, valid = root.ask_root_password(self.i18n)
password, valid = root.ask_root_password(self.context, self.i18n)
self.thread_animate_progress.animate()
self.signal_root_password.emit(password, valid)
@@ -443,19 +475,23 @@ class ManageWindow(QWidget):
def verify_warnings(self):
self.thread_warnings.start()
def _show_installed(self):
def _begin_loading_installed(self):
if self.pkgs_installed:
self.finish_action()
self.search_performed = False
self.ref_bt_upgrade.setVisible(True)
self.ref_checkbox_only_apps.setVisible(True)
self.input_search.setText('')
self.input_name_filter.setText('')
self.update_pkgs(new_pkgs=None, as_installed=True)
self._begin_action(self.i18n['manage_window.status.installed'], keep_bt_installed=False, clear_filters=not self.recent_uninstall)
self.thread_load_installed.start()
def _finish_loading_installed(self):
self.finish_action()
self.update_pkgs(new_pkgs=None, as_installed=True)
def _show_about(self):
if self.dialog_about is None:
self.dialog_about = AboutDialog(self.i18n)
self.dialog_about = AboutDialog(self.config)
self.dialog_about.show()
@@ -476,12 +512,20 @@ class ManageWindow(QWidget):
self.category_filter = self.combo_categories.itemData(idx)
self.apply_filters_async()
def _notify_model_data_change(self):
self.table_apps.fill_async_data()
def _update_state_when_pkgs_ready(self):
if self.progress_bar.isVisible():
return
if not self.recent_installation:
if not self.ref_progress_bar.isVisible():
self._reload_categories()
self._reload_categories()
self.resize_and_center()
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()
@@ -500,14 +544,6 @@ class ManageWindow(QWidget):
policy = QHeaderView.Stretch if self._maximized else QHeaderView.ResizeToContents
self.table_apps.change_headers_policy(policy)
def closeEvent(self, event):
if self.tray_icon:
event.ignore()
self.hide()
self._handle_console_option(False)
else:
QCoreApplication.exit() # needed because QuitOnLastWindowClosed is disabled
def _handle_console(self, checked: bool):
if checked:
@@ -524,7 +560,7 @@ class ManageWindow(QWidget):
self.checkbox_console.setChecked(False)
self.textarea_output.hide()
def refresh_apps(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None):
def refresh_packages(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None):
self.recent_installation = False
self.input_search.clear()
@@ -561,20 +597,16 @@ class ManageWindow(QWidget):
self._hide_fields_after_recent_installation()
def uninstall_app(self, app: PackageView):
pwd = None
requires_root = self.manager.requires_root('uninstall', app.model)
pwd, proceed = self._ask_root_password('uninstall', app)
if not user.is_root() and requires_root:
pwd, ok = ask_root_password(self.i18n)
if not ok:
return
if not proceed:
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.i18n['manage_window.status.uninstalling'], app.model.name), clear_filters=False)
self.thread_uninstall.app = app
self.thread_uninstall.root_password = pwd
self.thread_uninstall.root_pwd = pwd
self.thread_uninstall.start()
def run_app(self, app: PackageView):
@@ -595,10 +627,9 @@ class ManageWindow(QWidget):
only_pkg_type = False
self.recent_uninstall = True
self.refresh_apps(pkg_types={pkgv.model.__class__} if only_pkg_type else None)
self.refresh_packages(pkg_types={pkgv.model.__class__} if only_pkg_type else None)
if self.tray_icon:
self.tray_icon.verify_updates()
notify_tray()
else:
if self._can_notify_user():
util.notify_user('{}: {}'.format(pkgv.model.name, self.i18n['notification.uninstall.failed']))
@@ -615,10 +646,9 @@ class ManageWindow(QWidget):
if self._can_notify_user():
util.notify_user('{} {}'.format(res['app'], self.i18n['downgraded']))
self.refresh_apps(pkg_types={res['app'].model.__class__} if len(self.pkgs) > 1 else None)
self.refresh_packages(pkg_types={res['app'].model.__class__} if len(self.pkgs) > 1 else None)
if self.tray_icon:
self.tray_icon.verify_updates(notify_user=False)
notify_tray()
else:
if self._can_notify_user():
util.notify_user(self.i18n['notification.downgrade.failed'])
@@ -638,7 +668,7 @@ class ManageWindow(QWidget):
def _update_table(self, pkgs_info: dict, signal: bool = False):
self.pkgs = pkgs_info['pkgs_displayed']
self.table_apps.update_pkgs(self.pkgs, update_check_enabled=pkgs_info['not_installed'] == 0)
self.table_apps.update_packages(self.pkgs, update_check_enabled=pkgs_info['not_installed'] == 0)
if not self._maximized:
self.table_apps.change_headers_policy(QHeaderView.Stretch)
@@ -692,20 +722,20 @@ class ManageWindow(QWidget):
setattr(self, attr, checked)
checkbox.blockSignals(False)
def _gen_filters(self, updates: int = 0, ignore_updates: bool = False) -> dict:
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_filter.get_text().lower() if self.input_name_filter.get_text() else None,
'display_limit': self.display_limit if updates <= 0 else None
'display_limit': None if self.filter_updates else self.display_limit
}
def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False, keep_filters: bool = False):
self.input_name_filter.setText('')
pkgs_info = commons.new_pkgs_info()
filters = self._gen_filters(ignore_updates)
filters = self._gen_filters(ignore_updates=ignore_updates)
if new_pkgs is not None:
old_installed = None
@@ -715,7 +745,7 @@ class ManageWindow(QWidget):
self.pkgs_installed = []
for pkg in new_pkgs:
app_model = PackageView(model=pkg)
app_model = PackageView(model=pkg, i18n=self.i18n)
commons.update_info(app_model, pkgs_info)
commons.apply_filters(app_model, filters, pkgs_info)
@@ -764,8 +794,8 @@ class ManageWindow(QWidget):
self._update_table(pkgs_info=pkgs_info)
if new_pkgs:
self.thread_verify_models.apps = self.pkgs
self.thread_verify_models.start()
self.thread_notify_pkgs_ready.pkgs = self.pkgs
self.thread_notify_pkgs_ready.start()
if self.pkgs_installed:
self.ref_bt_installed.setVisible(not as_installed and not self.recent_installation)
@@ -774,7 +804,7 @@ class ManageWindow(QWidget):
def _apply_filters(self, pkgs_info: dict, ignore_updates: bool):
pkgs_info['pkgs_displayed'] = []
filters = self._gen_filters(updates=pkgs_info['updates'], ignore_updates=ignore_updates)
filters = self._gen_filters(ignore_updates=ignore_updates)
for pkgv in pkgs_info['pkgs']:
commons.apply_filters(pkgv, filters, pkgs_info)
@@ -795,7 +825,7 @@ class ManageWindow(QWidget):
sel_type = -1
for idx, item in enumerate(available_types.items()):
app_type, icon_path = item[0], item[1]
app_type, icon_path, label = item[0], item[1]['icon'], item[1]['label']
icon = self.cache_type_filter_icons.get(app_type)
@@ -803,7 +833,7 @@ class ManageWindow(QWidget):
icon = QIcon(icon_path)
self.cache_type_filter_icons[app_type] = icon
self.combo_filter_type.addItem(icon, app_type.capitalize(), app_type)
self.combo_filter_type.addItem(icon, label, app_type)
if keeping_selected and app_type == self.type_filter:
sel_type = idx + 1
@@ -816,7 +846,6 @@ class ManageWindow(QWidget):
self.ref_combo_filter_type.setVisible(False)
def _update_categories(self, categories: Set[str] = None, keep_selected: bool = False):
if categories is None:
self.ref_combo_categories.setVisible(self.combo_categories.count() > 1)
else:
@@ -835,9 +864,8 @@ class ManageWindow(QWidget):
cat_list.sort()
for idx, c in enumerate(cat_list):
i18n_cat = self.i18n.get(c)
cat_label = i18n_cat if i18n_cat else c
self.combo_categories.addItem(cat_label.capitalize(), c)
i18n_cat = self.i18n.get('category.{}'.format(c), self.i18n.get(c, c))
self.combo_categories.addItem(i18n_cat.capitalize(), c)
if keeping_selected and c == self.category_filter:
selected_cat = idx + 1
@@ -863,49 +891,45 @@ class ManageWindow(QWidget):
qt_utils.centralize(self)
def set_progress_controll(self, enabled: bool):
self.progress_controll_enabled = enumerate
def update_selected(self):
if self.pkgs:
requires_root = False
if dialog.ask_confirmation(title=self.i18n['manage_window.upgrade_all.popup.title'],
body=self.i18n['manage_window.upgrade_all.popup.body'],
i18n=self.i18n,
widgets=[UpdateToggleButton(None, self, self.i18n, clickable=False)]):
to_update = []
self._handle_console_option(True)
self._begin_action(self.i18n['manage_window.status.upgrading'])
self.thread_update.pkgs = self.pkgs
self.thread_update.start()
for app_v in self.pkgs:
if app_v.update_checked:
to_update.append(app_v)
if self.manager.requires_root('update', app_v.model):
requires_root = True
if to_update and dialog.ask_confirmation(title=self.i18n['manage_window.upgrade_all.popup.title'],
body=self.i18n['manage_window.upgrade_all.popup.body'],
i18n=self.i18n,
widgets=[UpdateToggleButton(None, self, self.i18n, clickable=False)]):
pwd = None
if not user.is_root() and requires_root:
pwd, ok = ask_root_password(self.i18n)
if not ok:
return
self._handle_console_option(True)
self.progress_controll_enabled = len(to_update) == 1
self._begin_action(self.i18n['manage_window.status.upgrading'])
self.thread_update.pkgs = to_update
self.thread_update.root_password = pwd
self.thread_update.start()
def _finish_update_selected(self, res: dict):
def _finish_upgrade_selected(self, res: dict):
self.finish_action()
if res.get('id'):
output = self.textarea_output.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_output.appendPlainText('\n*Upgrade summary generated at: {}'.format(UpgradeSelected.SUMMARY_FILE.format(res['id'])))
self.textarea_output.appendPlainText('*Upgrade logs generated at: {}'.format(logs_path))
except:
traceback.print_exc()
if res['success']:
if self._can_notify_user():
util.notify_user('{} {}'.format(res['updated'], self.i18n['notification.update_selected.success']))
self.refresh_apps(pkg_types=res['types'])
self.refresh_packages(pkg_types=res['types'])
if self.tray_icon:
self.tray_icon.verify_updates()
notify_tray()
else:
if self._can_notify_user():
util.notify_user(self.i18n['notification.update_selected.failed'])
@@ -920,6 +944,7 @@ class ManageWindow(QWidget):
self.ref_input_name_filter.setVisible(False)
self.ref_combo_filter_type.setVisible(False)
self.ref_combo_categories.setVisible(False)
self.ref_bt_custom_actions.setVisible(False)
self.ref_bt_settings.setVisible(False)
self.ref_bt_about.setVisible(False)
self.thread_animate_progress.stop = False
@@ -961,6 +986,7 @@ class ManageWindow(QWidget):
self.progress_bar.setTextVisible(False)
self._change_label_substatus('')
self.ref_bt_custom_actions.setVisible(bool(self.custom_actions))
self.ref_bt_settings.setVisible(True)
self.ref_bt_about.setVisible(True)
@@ -997,20 +1023,16 @@ class ManageWindow(QWidget):
self.ref_input_name_filter.setVisible(False)
def downgrade(self, pkgv: PackageView):
pwd = None
requires_root = self.manager.requires_root('downgrade', pkgv.model)
pwd, proceed = self._ask_root_password('downgrade', pkgv)
if not user.is_root() and requires_root:
pwd, ok = ask_root_password(self.i18n)
if not ok:
return
if not proceed:
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.i18n['manage_window.status.downgrading'], pkgv.model.name))
self.thread_downgrade.app = pkgv
self.thread_downgrade.root_password = pwd
self.thread_downgrade.root_pwd = pwd
self.thread_downgrade.start()
def get_app_info(self, pkg: dict):
@@ -1043,7 +1065,7 @@ class ManageWindow(QWidget):
body=self.i18n['popup.screenshots.no_screenshot.body'].format(bold(res['pkg'].model.name)),
type_=MessageType.ERROR)
def get_app_history(self, app: dict):
def get_app_history(self, app: PackageView):
self._handle_console_option(False)
self._begin_action(self.i18n['manage_window.status.history'])
@@ -1062,6 +1084,10 @@ class ManageWindow(QWidget):
self._handle_console_option(True)
self.textarea_output.appendPlainText(res['error'])
self.checkbox_console.setChecked(True)
elif not res['history'].history:
dialog.show_message(title=self.i18n['action.history.no_history.title'],
body=self.i18n['action.history.no_history.body'].format(bold(res['history'].pkg.name)),
type_=MessageType.WARNING)
else:
dialog_history = HistoryDialog(res['history'], self.icon_cache, self.i18n)
dialog_history.exec_()
@@ -1090,21 +1116,29 @@ class ManageWindow(QWidget):
else:
dialog.show_message(title=self.i18n['warning'].capitalize(), body=self.i18n[res['error']], type_=MessageType.WARNING)
def install(self, pkg: PackageView):
def _ask_root_password(self, action: str, pkg: PackageView) -> Tuple[str, bool]:
pwd = None
requires_root = self.manager.requires_root('install', pkg.model)
requires_root = self.manager.requires_root(action, pkg.model)
if not user.is_root() and requires_root:
pwd, ok = ask_root_password(self.i18n)
pwd, ok = ask_root_password(self.context, self.i18n)
if not ok:
return
return pwd, False
return pwd, True
def install(self, pkg: PackageView):
pwd, proceed = self._ask_root_password('install', pkg)
if not proceed:
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.i18n['manage_window.status.installing'], pkg.model.name))
self.thread_install.pkg = pkg
self.thread_install.root_password = pwd
self.thread_install.root_pwd = pwd
self.thread_install.start()
def _finish_install(self, res: dict):
@@ -1114,7 +1148,7 @@ class ManageWindow(QWidget):
console_output = self.textarea_output.toPlainText()
if console_output:
log_path = '{}/logs/install/{}/{}'.format(TEMP_PATH, res['pkg'].model.get_type(), res['pkg'].model.name)
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)
@@ -1146,27 +1180,34 @@ class ManageWindow(QWidget):
def _finish_run_app(self, success: bool):
self.finish_action()
def execute_custom_action(self, pkg: PackageView, action: PackageAction):
def execute_custom_action(self, pkg: PackageView, action: CustomSoftwareAction):
if pkg is None and not dialog.ask_confirmation(title=self.i18n['confirmation'].capitalize(),
body=self.i18n['custom_action.proceed_with'].capitalize().format('"{}"'.format(self.i18n[action.i18_label_key].capitalize())),
icon=QIcon(action.icon_path) if action.icon_path else QIcon(resource.get_path('img/logo.svg')),
i18n=self.i18n):
return False
pwd = None
if not user.is_root() and action.requires_root:
pwd, ok = ask_root_password(self.i18n)
pwd, ok = ask_root_password(self.context, self.i18n)
if not ok:
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.i18n[action.i18n_status_key], pkg.model.name))
self._begin_action('{}{}'.format(self.i18n[action.i18n_status_key], ' {}'.format(pkg.model.name) if pkg else ''))
self.thread_custom_action.pkg = pkg
self.thread_custom_action.root_password = pwd
self.thread_custom_action.root_pwd = pwd
self.thread_custom_action.custom_action = action
self.thread_custom_action.start()
def _finish_custom_action(self, res: dict):
self.finish_action()
if res['success']:
self.refresh_apps(pkg_types={res['pkg'].model.__class__})
self.refresh_packages(pkg_types={res['pkg'].model.__class__} if res['pkg'] else None)
else:
self.checkbox_console.setChecked(True)
@@ -1174,9 +1215,24 @@ class ManageWindow(QWidget):
if self.settings_window:
self.settings_window.handle_display()
else:
self.settings_window = SettingsWindow(self.manager, self.i18n, self.screen_size, self.tray_icon, self)
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) -> QAction:
custom_action = QAction(self.i18n[action.i18_label_key])
custom_action.setIcon(QIcon(action.icon_path))
custom_action.triggered.connect(lambda: self.execute_custom_action(None, action))
return custom_action
def show_custom_actions(self):
if self.custom_actions:
menu_row = QMenu()
actions = [self._map_custom_action(a) for a in self.custom_actions]
menu_row.addActions(actions)
menu_row.adjustSize()
menu_row.popup(QCursor.pos())
menu_row.exec_()