project renamed as 'bauh'

This commit is contained in:
Vinicius Moreira
2019-08-12 11:50:24 -03:00
parent 6d891683da
commit 19a11434fe
69 changed files with 202 additions and 321 deletions

0
bauh/view/__init__.py Executable file
View File

0
bauh/view/qt/__init__.py Executable file
View File

71
bauh/view/qt/about.py Normal file
View File

@@ -0,0 +1,71 @@
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel
from bauh import __version__, __app_name__
from bauh.core import resource
PROJECT_URL = 'https://github.com/vinifmor/' + __app_name__
LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/{}/master/LICENSE'.format(__app_name__)
class AboutDialog(QDialog):
def __init__(self, locale_keys: dict):
super(AboutDialog, self).__init__()
self.setWindowTitle(locale_keys['tray.action.about'])
layout = QVBoxLayout()
self.setLayout(layout)
label_logo = QLabel(self)
label_logo.setPixmap(QPixmap(resource.get_path('img/logo.svg')))
label_logo.setAlignment(Qt.AlignCenter)
layout.addWidget(label_logo)
label_name = QLabel('{} ( {} {} )'.format(__app_name__, locale_keys['flatpak.info.version'].lower(), __version__))
label_name.setStyleSheet('font-weight: bold;')
label_name.setAlignment(Qt.AlignCenter)
layout.addWidget(label_name)
layout.addWidget(QLabel(''))
line_desc = QLabel(self)
line_desc.setStyleSheet('font-size: 10px; font-weight: bold;')
line_desc.setText(locale_keys['about.info.desc'])
line_desc.setAlignment(Qt.AlignCenter)
line_desc.setMinimumWidth(400)
layout.addWidget(line_desc)
layout.addWidget(QLabel(''))
label_more_info = QLabel()
label_more_info.setStyleSheet('font-size: 9px;')
label_more_info.setText(locale_keys['about.info.link'] + ": <a href='{url}'>{url}</a>".format(url=PROJECT_URL))
label_more_info.setOpenExternalLinks(True)
label_more_info.setAlignment(Qt.AlignCenter)
layout.addWidget(label_more_info)
label_license = QLabel()
label_license.setStyleSheet('font-size: 9px;')
label_license.setText("<a href='{}'>{}</a>".format(LICENSE_URL, locale_keys['about.info.license']))
label_license.setOpenExternalLinks(True)
label_license.setAlignment(Qt.AlignCenter)
layout.addWidget(label_license)
layout.addWidget(QLabel(''))
label_rate = QLabel()
label_rate.setStyleSheet('font-size: 9px; font-weight: bold;')
label_rate.setText(locale_keys['about.info.rate'] + ' :)')
label_rate.setOpenExternalLinks(True)
label_rate.setAlignment(Qt.AlignCenter)
layout.addWidget(label_rate)
layout.addWidget(QLabel(''))
self.adjustSize()
self.setFixedSize(self.size())
def closeEvent(self, event):
event.ignore()
self.hide()

389
bauh/view/qt/apps_table.py Normal file
View File

@@ -0,0 +1,389 @@
import os
from threading import Lock
from typing import List
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QPixmap, QIcon, QCursor
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QWidget, \
QHeaderView, QLabel, QHBoxLayout, QPushButton, QToolButton, QToolBar
from bauh.core import resource
from bauh.core.model import ApplicationStatus
from bauh.util import util
from bauh.util.cache import Cache
from bauh.view.qt import dialog
from bauh.view.qt.view_model import ApplicationView, ApplicationViewStatus
INSTALL_BT_STYLE = 'background: {back}; color: white; font-size: 10px; font-weight: bold'
class IconButton(QWidget):
def __init__(self, icon_path: str, action, background: str = None, align: int = Qt.AlignCenter, tooltip: str = None):
super(IconButton, self).__init__()
self.bt = QToolButton()
self.bt.setIcon(QIcon(icon_path))
self.bt.clicked.connect(action)
if background:
self.bt.setStyleSheet('QToolButton { color: white; background: ' + background + '}')
if tooltip:
self.bt.setToolTip(tooltip)
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setAlignment(align)
layout.addWidget(self.bt)
self.setLayout(layout)
class UpdateToggleButton(QWidget):
def __init__(self, app_view: ApplicationView, root: QWidget, locale_keys: dict, checked: bool = True):
super(UpdateToggleButton, self).__init__()
self.app_view = app_view
self.root = root
layout = QHBoxLayout()
layout.setContentsMargins(2, 2, 2, 0)
layout.setAlignment(Qt.AlignCenter)
self.setLayout(layout)
self.bt = QToolButton()
self.bt.setCheckable(True)
self.bt.clicked.connect(self.change_state)
self.bt.setIcon(QIcon(resource.get_path('img/app_update.svg')))
self.bt.setStyleSheet('QToolButton { background: #4EC306 } ' +
'QToolButton:checked { background: gray }')
layout.addWidget(self.bt)
self.setToolTip(locale_keys['manage_window.apps_table.upgrade_toggle.tooltip'])
if not checked:
self.bt.click()
def change_state(self, not_checked: bool):
self.app_view.update_checked = not not_checked
self.root.change_update_state(change_filters=False)
class AppsTable(QTableWidget):
def __init__(self, parent: QWidget, icon_cache: Cache, disk_cache: bool, download_icons: bool):
super(AppsTable, self).__init__()
self.setParent(parent)
self.window = parent
self.disk_cache = disk_cache
self.download_icons = download_icons
self.column_names = ['' for _ in range(7)]
self.setColumnCount(len(self.column_names))
self.setFocusPolicy(Qt.NoFocus)
self.setShowGrid(False)
self.verticalHeader().setVisible(False)
self.horizontalHeader().setVisible(False)
self.setSelectionBehavior(QTableView.SelectRows)
self.setHorizontalHeaderLabels(self.column_names)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.icon_flathub = QIcon(resource.get_path('img/flathub.svg'))
self.icon_logo = QIcon(resource.get_path('img/logo.svg'))
self.network_man = QNetworkAccessManager()
self.network_man.finished.connect(self._load_icon_and_cache)
self.icon_cache = icon_cache
self.lock_async_data = Lock()
def has_any_settings(self, app_v: ApplicationView):
return app_v.model.can_be_refreshed() or \
app_v.model.has_history() or \
app_v.model.can_be_downgraded()
def show_app_settings(self, app: ApplicationView):
menu_row = QMenu()
if app.model.installed:
if app.model.can_be_refreshed():
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.refresh"])
action_history.setIcon(QIcon(resource.get_path('img/refresh.svg')))
def refresh():
self.window.refresh(app)
action_history.triggered.connect(refresh)
menu_row.addAction(action_history)
if app.model.has_history():
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"])
action_history.setIcon(QIcon(resource.get_path('img/history.svg')))
def show_history():
self.window.get_app_history(app)
action_history.triggered.connect(show_history)
menu_row.addAction(action_history)
if app.model.can_be_downgraded():
action_downgrade = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.downgrade"])
def downgrade():
if dialog.ask_confirmation(
title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(app.model.base_data.name),
locale_keys=self.window.locale_keys):
self.window.downgrade_app(app)
action_downgrade.triggered.connect(downgrade)
action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg')))
menu_row.addAction(action_downgrade)
menu_row.adjustSize()
menu_row.popup(QCursor.pos())
menu_row.exec_()
def fill_async_data(self):
if self.window.apps:
for idx, app_v in enumerate(self.window.apps):
if app_v.visible and app_v.status == ApplicationViewStatus.LOADING and app_v.model.status == ApplicationStatus.READY:
if self.download_icons:
self.network_man.get(QNetworkRequest(QUrl(app_v.model.base_data.icon_url)))
app_name = self.item(idx, 0).text()
if not app_name or app_name == '...':
self.item(idx, 0).setText(app_v.model.base_data.name)
self._set_col_version(idx, app_v)
self._set_col_description(idx, app_v)
app_v.status = ApplicationViewStatus.READY
self.window.resize_and_center()
def get_selected_app(self) -> ApplicationView:
return self.window.apps[self.currentRow()]
def get_selected_app_icon(self) -> QIcon:
return self.item(self.currentRow(), 0).icon()
def _uninstall_app(self, app_v: ApplicationView):
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(app_v),
locale_keys=self.window.locale_keys):
self.window.uninstall_app(app_v)
def _downgrade_app(self):
selected_app = self.get_selected_app()
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app.model.base_data.name),
locale_keys=self.window.locale_keys):
self.window.downgrade_app(selected_app)
def _refresh_app(self):
self.window.refresh(self.get_selected_app())
def _get_app_info(self):
self.window.get_app_info(self.get_selected_app())
def _get_app_history(self):
self.window.get_app_history(self.get_selected_app())
def _install_app(self, app_v: ApplicationView):
if dialog.ask_confirmation(
title=self.window.locale_keys['manage_window.apps_table.row.actions.install.popup.title'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.install.popup.body'].format(app_v),
locale_keys=self.window.locale_keys):
self.window.install_app(app_v)
def _load_icon_and_cache(self, http_response):
icon_url = http_response.url().toString()
icon_data = self.icon_cache.get(icon_url)
icon_was_cached = True
if not icon_data:
icon_bytes = http_response.readAll()
if not icon_bytes:
return
icon_was_cached = False
pixmap = QPixmap()
pixmap.loadFromData(icon_bytes)
icon = QIcon(pixmap)
icon_data = {'icon': icon, 'bytes': icon_bytes}
self.icon_cache.add(icon_url, icon_data)
for idx, app in enumerate(self.window.apps):
if app.model.base_data.icon_url == icon_url:
col_name = self.item(idx, 0)
col_name.setIcon(icon_data['icon'])
if self.disk_cache and app.model.supports_disk_cache():
if not icon_was_cached or not os.path.exists(app.model.get_disk_icon_path()):
self.window.manager.cache_to_disk(app=app.model, icon_bytes=icon_data['bytes'], only_icon=True)
def update_apps(self, app_views: List[ApplicationView], update_check_enabled: bool = True):
self.setRowCount(len(app_views) if app_views else 0)
self.setEnabled(True)
if app_views:
for idx, app_v in enumerate(app_views):
self._set_col_name(idx, app_v)
self._set_col_version(idx, app_v)
self._set_col_description(idx, app_v)
self._set_col_type(idx, app_v)
self._set_col_installed(idx, app_v)
self._set_col_settings(idx, app_v)
col_update = None
if update_check_enabled and app_v.model.update:
col_update = UpdateToggleButton(app_v, self.window, self.window.locale_keys, app_v.model.update)
self.setCellWidget(idx, 6, col_update)
def _gen_row_button(self, text: str, style: str, callback) -> QWidget:
col = QWidget()
col_bt = QPushButton()
col_bt.setText(text)
col_bt.setStyleSheet('QPushButton { ' + style + '}')
col_bt.clicked.connect(callback)
layout = QHBoxLayout()
layout.setContentsMargins(2, 2, 2, 0)
layout.setAlignment(Qt.AlignCenter)
layout.addWidget(col_bt)
col.setLayout(layout)
return col
def _set_col_installed(self, idx: int, app_v: ApplicationView):
if app_v.model.installed:
if app_v.model.can_be_uninstalled():
def uninstall():
self._uninstall_app(app_v)
col = self._gen_row_button(self.window.locale_keys['uninstall'].capitalize(), INSTALL_BT_STYLE.format(back='#cc0000'), uninstall)
else:
col = QLabel()
col.setPixmap((QPixmap(resource.get_path('img/checked.svg'))))
col.setAlignment(Qt.AlignCenter)
col.setToolTip(self.window.locale_keys['installed'])
elif app_v.model.can_be_installed():
def install():
self._install_app(app_v)
col = self._gen_row_button(self.window.locale_keys['install'].capitalize(), INSTALL_BT_STYLE.format(back='#088A08'), install)
else:
col = None
self.setCellWidget(idx, 4, col)
def _set_col_type(self, idx: int, app_v: ApplicationView):
col_type = QLabel()
pixmap = QPixmap(app_v.model.get_default_icon_path())
col_type.setPixmap(pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation))
col_type.setAlignment(Qt.AlignCenter)
col_type.setToolTip('{}: {}'.format(self.window.locale_keys['type'], app_v.model.get_type()))
self.setCellWidget(idx, 3, col_type)
def _set_col_version(self, idx: int, app_v: ApplicationView):
label_version = QLabel(app_v.model.base_data.version if app_v.model.base_data.version else '?')
label_version.setAlignment(Qt.AlignCenter)
col_version = QWidget()
col_version.setLayout(QHBoxLayout())
col_version.layout().addWidget(label_version)
if app_v.model.base_data.version:
tooltip = self.window.locale_keys['version.installed'] if app_v.model.installed else self.window.locale_keys['version']
else:
tooltip = self.window.locale_keys['version.unknown']
if app_v.model.update:
label_version.setStyleSheet("color: #4EC306; font-weight: bold")
tooltip = self.window.locale_keys['version.installed_outdated']
if app_v.model.installed and app_v.model.base_data.version and app_v.model.base_data.latest_version and app_v.model.base_data.version < app_v.model.base_data.latest_version:
tooltip = '{}. {}: {}'.format(tooltip, self.window.locale_keys['version.latest'], app_v.model.base_data.latest_version)
label_version.setText(label_version.text() + ' > {}'.format(app_v.model.base_data.latest_version))
col_version.setToolTip(tooltip)
self.setCellWidget(idx, 1, col_version)
def _set_col_name(self, idx: int, app_v: ApplicationView):
col = QTableWidgetItem()
col.setText(app_v.model.base_data.name if app_v.model.base_data.name else '...')
col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col.setToolTip(self.window.locale_keys['app.name'].lower())
if self.disk_cache and app_v.model.supports_disk_cache() and os.path.exists(app_v.model.get_disk_icon_path()):
with open(app_v.model.get_disk_icon_path(), 'rb') as f:
icon_bytes = f.read()
pixmap = QPixmap()
pixmap.loadFromData(icon_bytes)
icon = QIcon(pixmap)
self.icon_cache.add_non_existing(app_v.model.base_data.icon_url, {'icon': icon, 'bytes': icon_bytes})
elif not app_v.model.base_data.icon_url:
icon = QIcon(app_v.model.get_default_icon_path())
else:
icon_data = self.icon_cache.get(app_v.model.base_data.icon_url)
icon = icon_data['icon'] if icon_data else QIcon(app_v.model.get_default_icon_path())
col.setIcon(icon)
self.setItem(idx, 0, col)
def _set_col_description(self, idx: int, app_v: ApplicationView):
col = QTableWidgetItem()
col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
if app_v.model.base_data.description is not None or app_v.model.is_library() or app_v.model.status == ApplicationStatus.READY:
desc = app_v.model.base_data.description
else:
desc = '...'
if desc and desc != '...':
desc = util.strip_html(desc[0:25]) + '...'
col.setText(desc)
if app_v.model.base_data.description:
col.setToolTip(app_v.model.base_data.description)
self.setItem(idx, 2, col)
def _set_col_settings(self, idx: int, app_v: ApplicationView):
tb = QToolBar()
if app_v.model.has_info():
def get_info():
self.window.get_app_info(app_v)
tb.addWidget(IconButton(icon_path=resource.get_path('img/app_info.svg'), action=get_info, background='#2E68D3'))
def handle_click():
self.show_app_settings(app_v)
if self.has_any_settings(app_v):
bt = IconButton(icon_path=resource.get_path('img/app_settings.svg'), action=handle_click, background='#12ABAB')
tb.addWidget(bt)
self.setCellWidget(idx, 5, tb)
def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents):
header_horizontal = self.horizontalHeader()
for i in range(self.columnCount()):
header_horizontal.setSectionResizeMode(i, policy)

46
bauh/view/qt/dialog.py Normal file
View File

@@ -0,0 +1,46 @@
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QMessageBox
from bauh.core import resource
def show_error(title: str, body: str, icon: QIcon = QIcon(resource.get_path('img/logo.svg'))):
error_msg = QMessageBox()
error_msg.setIcon(QMessageBox.Critical)
error_msg.setWindowTitle(title)
error_msg.setText(body)
if icon:
error_msg.setWindowIcon(icon)
error_msg.exec_()
def show_warning(title: str, body: str, icon: QIcon = QIcon(resource.get_path('img/logo.svg'))):
msg = QMessageBox()
msg.setIcon(QMessageBox.Warning)
msg.setWindowTitle(title)
msg.setText(body)
if icon:
msg.setWindowIcon(icon)
msg.exec_()
def ask_confirmation(title: str, body: str, locale_keys: dict, icon: QIcon = QIcon(resource.get_path('img/logo.svg'))):
dialog_confirmation = QMessageBox()
dialog_confirmation.setIcon(QMessageBox.Question)
dialog_confirmation.setWindowTitle(title)
dialog_confirmation.setText(body)
dialog_confirmation.setStyleSheet('QLabel { margin-right: 25px; }')
bt_yes = dialog_confirmation.addButton(locale_keys['popup.button.yes'], QMessageBox.YesRole)
dialog_confirmation.addButton(locale_keys['popup.button.no'], QMessageBox.NoRole)
if icon:
dialog_confirmation.setWindowIcon(icon)
dialog_confirmation.exec_()
return dialog_confirmation.clickedButton() == bt_yes

60
bauh/view/qt/history.py Normal file
View File

@@ -0,0 +1,60 @@
import operator
from functools import reduce
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
from bauh.util.cache import Cache
class HistoryDialog(QDialog):
def __init__(self, app: dict, icon_cache: Cache, locale_keys: dict):
super(HistoryDialog, self).__init__()
self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model'].base_data.name))
layout = QVBoxLayout()
self.setLayout(layout)
table_history = QTableWidget()
table_history.setFocusPolicy(Qt.NoFocus)
table_history.setShowGrid(False)
table_history.verticalHeader().setVisible(False)
table_history.setAlternatingRowColors(True)
table_history.setColumnCount(len(app['history'][0]))
table_history.setRowCount(len(app['history']))
table_history.setHorizontalHeaderLabels([locale_keys['flatpak.info.' + key].capitalize() for key in sorted(app['history'][0].keys())])
for row, commit in enumerate(app['history']):
current_app_commit = app['model'].commit == commit['commit']
for col, key in enumerate(sorted(commit.keys())):
item = QTableWidgetItem()
item.setText(commit[key])
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
if current_app_commit:
item.setBackground(QColor('#ffbf00' if row != 0 else '#32CD32'))
tip = '{}. {}.'.format(locale_keys['popup.history.selected.tooltip'], locale_keys['version.{}'.format('updated'if row == 0 else 'outdated')].capitalize())
item.setToolTip(tip)
table_history.setItem(row, col, item)
layout.addWidget(table_history)
header_horizontal = table_history.horizontalHeader()
for i in range(0, table_history.columnCount()):
header_horizontal.setSectionResizeMode(i, QHeaderView.Stretch)
new_width = reduce(operator.add, [table_history.columnWidth(i) for i in range(table_history.columnCount())])
self.resize(new_width, table_history.height())
icon_data = icon_cache.get(app['model'].base_data.icon_url)
if icon_data and icon_data.get('icon'):
self.setWindowIcon(icon_data.get('icon'))

55
bauh/view/qt/info.py Normal file
View File

@@ -0,0 +1,55 @@
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QGroupBox, \
QLineEdit, QLabel
from bauh.util import util
from bauh.util.cache import Cache
IGNORED_ATTRS = {'name', '__app__'}
class InfoDialog(QDialog):
def __init__(self, app: dict, icon_cache: Cache, locale_keys: dict, screen_size: QSize()):
super(InfoDialog, self).__init__()
self.setWindowTitle(app['name'])
self.screen_size = screen_size
layout = QVBoxLayout()
self.setLayout(layout)
gbox_info = QGroupBox()
gbox_info.setMaximumHeight(self.screen_size.height() - self.screen_size.height() * 0.1)
gbox_info_layout = QFormLayout()
gbox_info.setLayout(gbox_info_layout)
layout.addWidget(gbox_info)
icon_data = icon_cache.get(app['__app__'].model.base_data.icon_url)
if icon_data and icon_data.get('icon'):
self.setWindowIcon(icon_data.get('icon'))
for attr in sorted(app.keys()):
if attr not in IGNORED_ATTRS and app[attr]:
val = app[attr]
text = QLineEdit()
text.setToolTip(val)
if attr == 'license' and val.strip() == 'unset':
val = locale_keys['license.unset']
if attr == 'description':
val = util.strip_html(val)
val = val[0:40] + '...'
text.setText(val)
text.setCursorPosition(0)
text.setStyleSheet("width: 400px")
text.setReadOnly(True)
label = QLabel("{}: ".format(locale_keys.get(app['__app__'].model.get_type() + '.info.' + attr, attr)).capitalize())
label.setStyleSheet("font-weight: bold")
gbox_info_layout.addRow(label, text)
self.adjustSize()

45
bauh/view/qt/root.py Normal file
View File

@@ -0,0 +1,45 @@
import io
import os
import subprocess
from PyQt5.QtWidgets import QInputDialog, QLineEdit
from bauh.view.qt.dialog import show_error
def is_root():
return os.getuid() == 0
def ask_root_password(locale_keys: dict):
dialog_pwd = QInputDialog()
dialog_pwd.setInputMode(QInputDialog.TextInput)
dialog_pwd.setTextEchoMode(QLineEdit.Password)
dialog_pwd.setWindowTitle(locale_keys['popup.root.title'])
dialog_pwd.setLabelText(locale_keys['popup.root.password'] + ':')
dialog_pwd.setCancelButtonText(locale_keys['popup.button.cancel'])
dialog_pwd.resize(400, 200)
ok = dialog_pwd.exec_()
if ok:
if not validate_password(dialog_pwd.textValue()):
show_error(title=locale_keys['popup.root.bad_password.title'],
body=locale_keys['popup.root.bad_password.body'])
ok = False
return dialog_pwd.textValue(), ok
def validate_password(password: str) -> bool:
proc = subprocess.Popen('sudo -k && echo {} | sudo -S whoami'.format(password),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
bufsize=-1)
stream = os._wrap_close(io.TextIOWrapper(proc.stdout), proc)
res = stream.read()
stream.close()
return bool(res.strip())

128
bauh/view/qt/systray.py Executable file
View File

@@ -0,0 +1,128 @@
import time
from threading import Lock, Thread
from typing import List
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
from bauh import __app_name__
from bauh.core import resource, system
from bauh.core.controller import ApplicationManager
from bauh.core.model import ApplicationUpdate
from bauh.view.qt.about import AboutDialog
from bauh.view.qt.window import ManageWindow
class UpdateCheck(QThread):
signal = pyqtSignal(list)
def __init__(self, manager: ApplicationManager, check_interval: int, parent=None):
super(UpdateCheck, self).__init__(parent)
self.check_interval = check_interval
self.manager = manager
def run(self):
while True:
updates = self.manager.list_updates()
self.signal.emit(updates)
time.sleep(self.check_interval)
class TrayIcon(QSystemTrayIcon):
def __init__(self, locale_keys: dict, manager: ApplicationManager, manage_window: ManageWindow, check_interval: int = 60, update_notification: bool = True):
super(TrayIcon, self).__init__()
self.locale_keys = locale_keys
self.manager = manager
self.icon_default = QIcon(resource.get_path('img/logo.png'))
self.icon_update = QIcon(resource.get_path('img/logo_update.png'))
self.setIcon(self.icon_default)
self.menu = QMenu()
self.action_manage = self.menu.addAction(self.locale_keys['tray.action.manage'])
self.action_manage.triggered.connect(self.show_manage_window)
self.action_about = self.menu.addAction(self.locale_keys['tray.action.about'])
self.action_about.triggered.connect(self.show_about)
self.action_exit = self.menu.addAction(self.locale_keys['tray.action.exit'])
self.action_exit.triggered.connect(lambda: QCoreApplication.exit())
self.setContextMenu(self.menu)
self.manage_window = None
self.dialog_about = None
self.check_thread = UpdateCheck(check_interval=check_interval, manager=self.manager)
self.check_thread.signal.connect(self.notify_updates)
self.check_thread.start()
self.last_updates = set()
self.update_notification = update_notification
self.lock_notify = Lock()
self.activated.connect(self.handle_click)
self.set_default_tooltip()
self.manage_window = manage_window
def set_default_tooltip(self):
self.setToolTip('{} ({})'.format(self.locale_keys['manage_window.title'], __app_name__).lower())
def handle_click(self, reason):
if reason == self.Trigger:
self.show_manage_window()
def verify_updates(self, notify_user: bool = True):
Thread(target=self._verify_updates, args=(notify_user,)).start()
def _verify_updates(self, notify_user: bool):
self.notify_updates(self.manager.list_updates(), notify_user=notify_user)
def notify_updates(self, updates: List[ApplicationUpdate], notify_user: bool = True):
self.lock_notify.acquire()
try:
if len(updates) > 0:
update_keys = {'{}:{}:{}'.format(up.type, up.id, up.version) for up in updates}
new_icon = self.icon_update
if update_keys.difference(self.last_updates):
self.last_updates = update_keys
msg = '{}: {}'.format(self.locale_keys['notification.new_updates'], len(updates))
self.setToolTip(msg)
if self.update_notification and notify_user:
system.notify_user(msg)
else:
self.last_updates.clear()
new_icon = self.icon_default
self.set_default_tooltip()
if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed
self.setIcon(new_icon)
finally:
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()
def show_about(self):
if self.dialog_about is None:
self.dialog_about = AboutDialog(self.locale_keys)
if self.dialog_about.isHidden():
self.dialog_about.show()

364
bauh/view/qt/thread.py Normal file
View File

@@ -0,0 +1,364 @@
import subprocess
import time
from datetime import datetime, timedelta
from typing import List
import requests
from PyQt5.QtCore import QThread, pyqtSignal
from bauh.core.controller import ApplicationManager
from bauh.core.exception import NoInternetException
from bauh.core.model import ApplicationStatus
from bauh.core.system import BauhProcess
from bauh.util.cache import Cache
from bauh.view.qt import dialog
from bauh.view.qt.view_model import ApplicationView
class AsyncAction(QThread):
def notify_subproc_outputs(self, proc: BauhProcess, signal) -> bool:
"""
:param subproc:
:param signal:
:param success:
:return: if the subprocess succeeded
"""
signal.emit(' '.join(proc.subproc.args) + '\n')
success, already_succeeded = True, False
for output in proc.subproc.stdout:
line = output.decode().strip()
if line:
signal.emit(line)
if proc.success_pgrase and proc.success_pgrase in line:
already_succeeded = True
if already_succeeded:
return True
for output in proc.subproc.stderr:
line = output.decode().strip()
if line:
if proc.wrong_error_phrase and proc.wrong_error_phrase in line:
continue
else:
success = False
signal.emit(line)
return success
class UpdateSelectedApps(AsyncAction):
signal_finished = pyqtSignal(bool, int)
signal_status = pyqtSignal(str)
signal_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, apps_to_update: List[ApplicationView] = None):
super(UpdateSelectedApps, self).__init__()
self.apps_to_update = apps_to_update
self.manager = manager
def run(self):
success = False
for app in self.apps_to_update:
self.signal_status.emit(app.model.base_data.name)
process = self.manager.update_and_stream(app.model)
success = self.notify_subproc_outputs(process, self.signal_output)
if not success:
break
else:
self.signal_output.emit('\n')
self.signal_finished.emit(success, len(self.apps_to_update))
self.apps_to_update = None
class RefreshApps(QThread):
signal = pyqtSignal(list)
def __init__(self, manager: ApplicationManager):
super(RefreshApps, self).__init__()
self.manager = manager
def run(self):
self.signal.emit(self.manager.read_installed())
class UninstallApp(AsyncAction):
signal_finished = pyqtSignal(object)
signal_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, icon_cache: Cache, app: ApplicationView = None):
super(UninstallApp, self).__init__()
self.app = app
self.manager = manager
self.icon_cache = icon_cache
self.root_password = None
def run(self):
if self.app:
process = self.manager.uninstall_and_stream(self.app.model, self.root_password)
success = self.notify_subproc_outputs(process, self.signal_output)
if success:
self.icon_cache.delete(self.app.model.base_data.icon_url)
self.manager.clean_cache_for(self.app.model)
self.signal_finished.emit(self.app if success else None)
self.app = None
self.root_password = None
class DowngradeApp(AsyncAction):
signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, locale_keys: dict, app: ApplicationView = None):
super(DowngradeApp, self).__init__()
self.manager = manager
self.app = app
self.locale_keys = locale_keys
self.root_password = None
def run(self):
if self.app:
success = False
try:
process = self.manager.downgrade_app(self.app.model, self.root_password)
if process is None:
dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'],
body=self.locale_keys['popup.downgrade.impossible.body'])
else:
success = self.notify_subproc_outputs(process, self.signal_output)
except (requests.exceptions.ConnectionError, NoInternetException):
success = False
self.signal_output.emit(self.locale_keys['internet.required'])
finally:
self.app = None
self.root_password = None
self.signal_finished.emit(success)
class GetAppInfo(QThread):
signal_finished = pyqtSignal(dict)
def __init__(self, manager: ApplicationManager, app: ApplicationView = None):
super(GetAppInfo, self).__init__()
self.app = app
self.manager = manager
def run(self):
if self.app:
info = {'__app__': self.app}
info.update(self.manager.get_info(self.app.model))
self.signal_finished.emit(info)
self.app = None
class GetAppHistory(QThread):
signal_finished = pyqtSignal(dict)
def __init__(self, manager: ApplicationManager, locale_keys: dict, app: ApplicationView = None):
super(GetAppHistory, self).__init__()
self.app = app
self.manager = manager
self.locale_keys = locale_keys
def run(self):
if self.app:
try:
res = {'model': self.app.model, 'history': self.manager.get_history(self.app.model)}
self.signal_finished.emit(res)
except (requests.exceptions.ConnectionError, NoInternetException):
self.signal_finished.emit({'error': self.locale_keys['internet.required']})
finally:
self.app = None
class SearchApps(QThread):
signal_finished = pyqtSignal(list)
def __init__(self, manager: ApplicationManager):
super(SearchApps, self).__init__()
self.word = None
self.manager = manager
def run(self):
apps_found = []
if self.word:
res = self.manager.search(self.word)
apps_found.extend(res['installed'])
apps_found.extend(res['new'])
self.signal_finished.emit(apps_found)
self.word = None
class InstallApp(AsyncAction):
signal_finished = pyqtSignal(object)
signal_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, disk_cache: bool, icon_cache: Cache, locale_keys: dict, app: ApplicationView = None):
super(InstallApp, self).__init__()
self.app = app
self.manager = manager
self.icon_cache = icon_cache
self.disk_cache = disk_cache
self.locale_keys = locale_keys
self.root_password = None
def run(self):
if self.app:
success = False
try:
process = self.manager.install_and_stream(self.app.model, self.root_password)
success = self.notify_subproc_outputs(process, self.signal_output)
if success and self.disk_cache:
self.app.model.installed = True
if self.app.model.supports_disk_cache():
icon_data = self.icon_cache.get(self.app.model.base_data.icon_url)
self.manager.cache_to_disk(app=self.app.model,
icon_bytes=icon_data.get('bytes') if icon_data else None,
only_icon=False)
except (requests.exceptions.ConnectionError, NoInternetException):
success = False
self.signal_output.emit(self.locale_keys['internet.required'])
finally:
self.signal_finished.emit(self.app if success else None)
self.app = None
class AnimateProgress(QThread):
signal_change = pyqtSignal(int)
def __init__(self):
super(AnimateProgress, self).__init__()
self.progress_value = 0
self.increment = 5
self.stop = False
def run(self):
current_increment = self.increment
while not self.stop:
self.signal_change.emit(self.progress_value)
if self.progress_value == 100:
current_increment = -current_increment
if self.progress_value == 0:
current_increment = self.increment
self.progress_value += current_increment
time.sleep(0.05)
self.progress_value = 0
class VerifyModels(QThread):
signal_updates = pyqtSignal()
def __init__(self, apps: List[ApplicationView] = None):
super(VerifyModels, self).__init__()
self.apps = apps
def run(self):
if self.apps:
stop_at = datetime.utcnow() + timedelta(seconds=30)
last_ready = 0
while True:
current_ready = 0
for app in self.apps:
current_ready += 1 if app.model.status == ApplicationStatus.READY else 0
if current_ready > last_ready:
last_ready = current_ready
self.signal_updates.emit()
if current_ready == len(self.apps):
self.signal_updates.emit()
break
if stop_at <= datetime.utcnow():
break
time.sleep(0.1)
self.apps = None
class RefreshApp(AsyncAction):
signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, app: ApplicationView = None):
super(RefreshApp, self).__init__()
self.app = app
self.manager = manager
self.root_password = None
def run(self):
if self.app:
success = False
try:
process = self.manager.refresh(self.app.model, self.root_password)
success = self.notify_subproc_outputs(process, self.signal_output)
except (requests.exceptions.ConnectionError, NoInternetException):
success = False
self.signal_output.emit(self.locale_keys['internet.required'])
finally:
self.app = None
self.signal_finished.emit(success)
class FindSuggestions(AsyncAction):
signal_finished = pyqtSignal(list)
def __init__(self, man: ApplicationManager):
super(FindSuggestions, self).__init__()
self.man = man
def run(self):
self.signal_finished.emit(self.man.list_suggestions(limit=-1))
class ListWarnings(QThread):
signal_warnings = pyqtSignal(list)
def __init__(self, man: ApplicationManager, locale_keys: dict):
super(QThread, self).__init__()
self.locale_keys = locale_keys
self.man = man
def run(self):
warnings = self.man.list_warnings()
if warnings:
self.signal_warnings.emit(warnings)

View File

@@ -0,0 +1,20 @@
from enum import Enum
from bauh.core.model import Application
class ApplicationViewStatus(Enum):
LOADING = 0
READY = 1
class ApplicationView:
def __init__(self, model: Application, visible: bool = True):
self.model = model
self.update_checked = model.update
self.visible = visible
self.status = ApplicationViewStatus.LOADING
def __repr__(self):
return '{} ( {} )'.format(self.model.base_data.name, self.model.get_type())

707
bauh/view/qt/window.py Executable file
View File

@@ -0,0 +1,707 @@
import operator
from functools import reduce
from typing import List, Set
from PyQt5.QtCore import QEvent, Qt, QSize
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QHBoxLayout
from bauh.core import resource, system
from bauh.core.controller import ApplicationManager
from bauh.core.model import Application
from bauh.util.cache import Cache
from bauh.view.qt import dialog
from bauh.view.qt.about import AboutDialog
from bauh.view.qt.apps_table import AppsTable
from bauh.view.qt.history import HistoryDialog
from bauh.view.qt.info import InfoDialog
from bauh.view.qt.root import is_root, ask_root_password
from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels, RefreshApp, FindSuggestions, ListWarnings
from bauh.view.qt.view_model import ApplicationView
DARK_ORANGE = '#FF4500'
class ManageWindow(QWidget):
__BASE_HEIGHT__ = 400
def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, download_icons: bool, screen_size, suggestions: bool, tray_icon=None):
super(ManageWindow, self).__init__()
self.locale_keys = locale_keys
self.manager = manager
self.tray_icon = tray_icon
self.working = False # restrict the number of threaded actions
self.apps = []
self.label_flatpak = None
self.icon_cache = icon_cache
self.disk_cache = disk_cache
self.download_icons = download_icons
self.screen_size = screen_size
self.icon_flathub = QIcon(resource.get_path('img/logo.svg'))
self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__)
self.setWindowIcon(self.icon_flathub)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.toolbar_top = QToolBar()
self.toolbar_top.addWidget(self._new_spacer())
self.label_status = QLabel()
self.label_status.setText('')
self.label_status.setStyleSheet("font-weight: bold")
self.toolbar_top.addWidget(self.label_status)
self.toolbar_search = QToolBar()
self.toolbar_search.setStyleSheet("spacing: 0px;")
self.toolbar_search.setContentsMargins(0, 0, 0, 0)
label_pre_search = QLabel()
label_pre_search.setStyleSheet("background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;")
self.toolbar_search.addWidget(label_pre_search)
self.input_search = QLineEdit()
self.input_search.setMaxLength(20)
self.input_search.setFrame(False)
self.input_search.setPlaceholderText(self.locale_keys['window_manage.input_search.placeholder'] + "...")
self.input_search.setToolTip(self.locale_keys['window_manage.input_search.tooltip'])
self.input_search.setStyleSheet("QLineEdit { background-color: white; color: gray; spacing: 0;}")
self.input_search.returnPressed.connect(self.search)
self.toolbar_search.addWidget(self.input_search)
label_pos_search = QLabel()
label_pos_search.setPixmap(QPixmap(resource.get_path('img/search.svg')))
label_pos_search.setStyleSheet("background: white; padding-right: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px;")
self.toolbar_search.addWidget(label_pos_search)
self.ref_toolbar_search = self.toolbar_top.addWidget(self.toolbar_search)
self.toolbar_top.addWidget(self._new_spacer())
self.layout.addWidget(self.toolbar_top)
toolbar = QToolBar()
self.checkbox_updates = QCheckBox()
self.checkbox_updates.setText(self.locale_keys['updates'].capitalize())
self.checkbox_updates.stateChanged.connect(self._handle_updates_filter)
self.ref_checkbox_updates = toolbar.addWidget(self.checkbox_updates)
self.checkbox_only_apps = QCheckBox()
self.checkbox_only_apps.setText(self.locale_keys['manage_window.checkbox.only_apps'])
self.checkbox_only_apps.setChecked(True)
self.checkbox_only_apps.stateChanged.connect(self._handle_filter_only_apps)
self.ref_checkbox_only_apps = toolbar.addWidget(self.checkbox_only_apps)
self.extra_filters = QWidget()
self.extra_filters.setLayout(QHBoxLayout())
toolbar.addWidget(self.extra_filters)
toolbar.addWidget(self._new_spacer())
self.bt_refresh = QToolButton()
self.bt_refresh.setToolTip(locale_keys['manage_window.bt.refresh.tooltip'])
self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg')))
self.bt_refresh.clicked.connect(lambda: self.refresh_apps(keep_console=False))
toolbar.addWidget(self.bt_refresh)
self.bt_upgrade = QToolButton()
self.bt_upgrade.setToolTip(locale_keys['manage_window.bt.upgrade.tooltip'])
self.bt_upgrade.setIcon(QIcon(resource.get_path('img/update_green.svg')))
self.bt_upgrade.setEnabled(False)
self.bt_upgrade.clicked.connect(self.update_selected)
self.ref_bt_upgrade = toolbar.addWidget(self.bt_upgrade)
self.layout.addWidget(toolbar)
self.table_apps = AppsTable(self, self.icon_cache, disk_cache=self.disk_cache, download_icons=self.download_icons)
self.table_apps.change_headers_policy()
self.layout.addWidget(self.table_apps)
toolbar_console = QToolBar()
self.checkbox_console = QCheckBox()
self.checkbox_console.setText(self.locale_keys['manage_window.checkbox.show_details'])
self.checkbox_console.stateChanged.connect(self._handle_console)
self.checkbox_console.setVisible(False)
self.ref_checkbox_console = toolbar_console.addWidget(self.checkbox_console)
toolbar_console.addWidget(self._new_spacer())
self.layout.addWidget(toolbar_console)
self.textarea_output = QPlainTextEdit(self)
self.textarea_output.resize(self.table_apps.size())
self.textarea_output.setStyleSheet("background: black; color: white;")
self.layout.addWidget(self.textarea_output)
self.textarea_output.setVisible(False)
self.textarea_output.setReadOnly(True)
self.thread_update = UpdateSelectedApps(self.manager)
self.thread_update.signal_output.connect(self._update_action_output)
self.thread_update.signal_finished.connect(self._finish_update_selected)
self.thread_update.signal_status.connect(self._change_updating_app_status)
self.thread_refresh = RefreshApps(self.manager)
self.thread_refresh.signal.connect(self._finish_refresh_apps)
self.thread_uninstall = UninstallApp(self.manager, self.icon_cache)
self.thread_uninstall.signal_output.connect(self._update_action_output)
self.thread_uninstall.signal_finished.connect(self._finish_uninstall)
self.thread_downgrade = DowngradeApp(self.manager, self.locale_keys)
self.thread_downgrade.signal_output.connect(self._update_action_output)
self.thread_downgrade.signal_finished.connect(self._finish_downgrade)
self.thread_get_info = GetAppInfo(self.manager)
self.thread_get_info.signal_finished.connect(self._finish_get_info)
self.thread_get_history = GetAppHistory(self.manager, self.locale_keys)
self.thread_get_history.signal_finished.connect(self._finish_get_history)
self.thread_search = SearchApps(self.manager)
self.thread_search.signal_finished.connect(self._finish_search)
self.thread_install = InstallApp(manager=self.manager, disk_cache=self.disk_cache, icon_cache=self.icon_cache, locale_keys=self.locale_keys)
self.thread_install.signal_output.connect(self._update_action_output)
self.thread_install.signal_finished.connect(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_refresh_app = RefreshApp(manager=self.manager)
self.thread_refresh_app.signal_finished.connect(self._finish_refresh)
self.thread_refresh_app.signal_output.connect(self._update_action_output)
self.thread_suggestions = FindSuggestions(man=self.manager)
self.thread_suggestions.signal_finished.connect(self._finish_search)
self.toolbar_bottom = QToolBar()
self.toolbar_bottom.setIconSize(QSize(16, 16))
self.label_updates = QLabel()
self.ref_label_updates = self.toolbar_bottom.addWidget(self.label_updates)
self.toolbar_bottom.addWidget(self._new_spacer())
self.progress_bar = QProgressBar()
self.progress_bar.setTextVisible(False)
self.ref_progress_bar = self.toolbar_bottom.addWidget(self.progress_bar)
self.toolbar_bottom.addWidget(self._new_spacer())
bt_about = QToolButton()
bt_about.setStyleSheet('QToolButton { border: 0px; }')
bt_about.setIcon(QIcon(resource.get_path('img/about.svg')))
bt_about.clicked.connect(self._show_about)
bt_about.setToolTip(self.locale_keys['manage_window.bt_about.tooltip'])
self.ref_bt_about = self.toolbar_bottom.addWidget(bt_about)
self.layout.addWidget(self.toolbar_bottom)
self.centralize()
self.filter_only_apps = True
self.filter_types = set()
self.filter_updates = False
self._maximized = False
self.dialog_about = None
self.first_refresh = suggestions
self.thread_warnings = ListWarnings(man=manager, locale_keys=locale_keys)
self.thread_warnings.signal_warnings.connect(self._show_warnings)
def _show_warnings(self, warnings: List[str]):
if warnings:
for warning in warnings:
dialog.show_warning(title=self.locale_keys['warning'].capitalize(), body=warning)
def show(self):
super(ManageWindow, self).show()
if not self.thread_warnings.isFinished():
self.thread_warnings.start()
def _show_about(self):
if self.dialog_about is None:
self.dialog_about = AboutDialog(self.locale_keys)
self.dialog_about.show()
def _handle_updates_filter(self, status: int):
self.filter_updates = status == 2
self.apply_filters()
def _handle_filter_only_apps(self, status: int):
self.filter_only_apps = status == 2
self.apply_filters()
def _handle_type_filter(self, status: int, app_type: str):
if status == 2:
self.filter_types.add(app_type)
elif app_type in self.filter_types:
self.filter_types.remove(app_type)
self.apply_filters()
def _notify_model_data_change(self):
self.table_apps.fill_async_data()
def _new_spacer(self):
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
return spacer
def changeEvent(self, e: QEvent):
if isinstance(e, QWindowStateChangeEvent):
self._maximized = self.isMaximized()
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)
def _handle_console(self, checked: bool):
if checked:
self.textarea_output.show()
else:
self.textarea_output.hide()
def _handle_console_option(self, enable: bool):
if enable:
self.textarea_output.clear()
self.ref_checkbox_console.setVisible(enable)
self.checkbox_console.setChecked(False)
self.textarea_output.hide()
def refresh_apps(self, keep_console: bool = True):
self.filter_types.clear()
self.input_search.clear()
if not keep_console:
self._handle_console_option(False)
self.ref_checkbox_updates.setVisible(False)
self.ref_checkbox_only_apps.setVisible(False)
self._begin_action(self.locale_keys['manage_window.status.refreshing'], clear_filters=True)
self.thread_refresh.start()
def _finish_refresh_apps(self, apps: List[Application]):
self.finish_action()
self.ref_checkbox_only_apps.setVisible(True)
self.ref_bt_upgrade.setVisible(True)
self.update_apps(apps)
self.first_refresh = False
def uninstall_app(self, app: ApplicationView):
pwd = None
requires_root = self.manager.requires_root('uninstall', app.model)
if not is_root() and requires_root:
pwd, ok = ask_root_password(self.locale_keys)
if not ok:
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.uninstalling'], app.model.base_data.name))
self.thread_uninstall.app = app
self.thread_uninstall.root_password = pwd
self.thread_uninstall.start()
def refresh(self, app: ApplicationView):
pwd = None
requires_root = self.manager.requires_root('refresh', app.model)
if not is_root() and requires_root:
pwd, ok = ask_root_password(self.locale_keys)
if not ok:
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.refreshing_app'], app.model.base_data.name))
self.thread_refresh_app.app = app
self.thread_refresh_app.root_password = pwd
self.thread_refresh_app.start()
def _finish_uninstall(self, app: ApplicationView):
self.finish_action()
if app:
if self._can_notify_user():
system.notify_user('{} ({}) {}'.format(app.model.base_data.name, app.model.get_type(), self.locale_keys['uninstalled']))
self.refresh_apps()
else:
if self._can_notify_user():
system.notify_user('{}: {}'.format(app.model.base_data.name, self.locale_keys['notification.uninstall.failed']))
self.checkbox_console.setChecked(True)
def _can_notify_user(self):
return self.isHidden() or self.isMinimized()
def _finish_downgrade(self, success: bool):
self.finish_action()
if success:
if self._can_notify_user():
app = self.table_apps.get_selected_app()
system.notify_user('{} ({}) {}'.format(app.model.base_data.name, app.model.get_type(), self.locale_keys['downgraded']))
self.refresh_apps()
if self.tray_icon:
self.tray_icon.verify_updates(notify_user=False)
else:
if self._can_notify_user():
system.notify_user(self.locale_keys['notification.downgrade.failed'])
self.checkbox_console.setChecked(True)
def _finish_refresh(self, success: bool):
self.finish_action()
if success:
self.refresh_apps()
else:
self.checkbox_console.setChecked(True)
def _change_updating_app_status(self, app_name: str):
self.label_status.setText('{} {}...'.format(self.locale_keys['manage_window.status.upgrading'], app_name))
def apply_filters(self):
if self.apps:
visible_apps = len(self.apps)
for idx, app_v in enumerate(self.apps):
hidden = self.filter_only_apps and app_v.model.is_library()
if not hidden and self.filter_types is not None:
hidden = app_v.model.get_type() not in self.filter_types
if not hidden and self.filter_updates:
hidden = not app_v.model.update
self.table_apps.setRowHidden(idx, hidden)
app_v.visible = not hidden
visible_apps -= 1 if hidden else 0
self.change_update_state(change_filters=False)
if not self._maximized:
self.table_apps.change_headers_policy(QHeaderView.Stretch)
self.table_apps.change_headers_policy()
self.resize_and_center(accept_lower_width=visible_apps > 0)
def change_update_state(self, change_filters: bool = True):
enable_bt_update = False
app_updates, library_updates, not_installed = 0, 0, 0
for app_v in self.apps:
if app_v.model.update:
if app_v.model.runtime:
library_updates += 1
else:
app_updates += 1
if not app_v.model.installed:
not_installed += 1
for app_v in self.apps:
if not_installed == 0 and app_v.visible and app_v.update_checked:
enable_bt_update = True
break
self.bt_upgrade.setEnabled(enable_bt_update)
total_updates = app_updates + library_updates
if total_updates > 0:
self.label_updates.setPixmap(QPixmap(resource.get_path('img/exclamation.svg')).scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.label_updates.setToolTip('{}: {} ( {} {} | {} {} )'.format(self.locale_keys['manage_window.label.updates'],
total_updates,
app_updates,
self.locale_keys['manage_window.checkbox.only_apps'].lower(),
library_updates,
self.locale_keys['others'].lower()))
if not_installed == 0:
if not self.ref_checkbox_updates.isVisible():
self.ref_checkbox_updates.setVisible(True)
if change_filters and not self.checkbox_updates.isChecked():
self.checkbox_updates.setChecked(True)
if change_filters and library_updates > 0 and self.checkbox_only_apps.isChecked():
self.checkbox_only_apps.setChecked(False)
else:
self.checkbox_updates.setChecked(False)
self.ref_checkbox_updates.setVisible(False)
self.label_updates.setPixmap(QPixmap())
def centralize(self):
geo = self.frameGeometry()
screen = QApplication.desktop().screenNumber(QApplication.desktop().cursor().pos())
center_point = QApplication.desktop().screenGeometry(screen).center()
geo.moveCenter(center_point)
self.move(geo.topLeft())
def update_apps(self, apps: List[Application], update_check_enabled: bool = True):
self.apps = []
napps = 0 # number of apps (not libraries)
available_types = set()
if apps:
for app in apps:
app_model = ApplicationView(model=app, visible=(not app.is_library()) or not self.checkbox_only_apps.isChecked())
available_types.add(app.get_type())
napps += 1 if not app.is_library() else 0
self.apps.append(app_model)
if napps == 0:
if self.first_refresh:
self._begin_search('')
self.thread_suggestions.start()
return
else:
self.checkbox_only_apps.setChecked(False)
self.checkbox_only_apps.setCheckable(False)
else:
self.checkbox_only_apps.setCheckable(True)
self.checkbox_only_apps.setChecked(True)
self._update_type_filters(available_types)
self.table_apps.update_apps(self.apps, update_check_enabled=update_check_enabled)
self.apply_filters()
self.change_update_state()
self.resize_and_center()
self.thread_verify_models.apps = self.apps
self.thread_verify_models.start()
def _update_type_filters(self, available_types: Set[str]):
self.filter_types = available_types
filters_layout = self.extra_filters.layout()
for i in reversed(range(filters_layout.count())):
filters_layout.itemAt(i).widget().setParent(None)
if available_types:
for app_type in sorted(list(available_types)):
checkbox_app_type = QCheckBox()
checkbox_app_type.setChecked(True)
checkbox_app_type.setText(app_type.capitalize())
def handle_click(status: int, filter_type: str = app_type):
self._handle_type_filter(status, filter_type)
checkbox_app_type.stateChanged.connect(handle_click)
filters_layout.addWidget(checkbox_app_type)
def resize_and_center(self, accept_lower_width: bool = True):
new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(self.table_apps.column_names))]) * 1.05
if accept_lower_width or new_width > self.width():
self.resize(new_width, self.height())
self.centralize()
def update_selected(self):
if self.apps:
to_update = [app_v for app_v in self.apps if app_v.visible and app_v.update_checked]
if to_update:
if dialog.ask_confirmation(
title=self.locale_keys['manage_window.upgrade_all.popup.title'],
body=self.locale_keys['manage_window.upgrade_all.popup.body'],
locale_keys=self.locale_keys):
self._handle_console_option(True)
self._begin_action(self.locale_keys['manage_window.status.upgrading'])
self.thread_update.apps_to_update = to_update
self.thread_update.start()
def _finish_update_selected(self, success: bool, updated: int):
self.finish_action()
if success:
if self._can_notify_user():
system.notify_user('{} {}'.format(updated, self.locale_keys['notification.update_selected.success']))
self.refresh_apps()
if self.tray_icon:
self.tray_icon.verify_updates()
else:
if self._can_notify_user():
system.notify_user(self.locale_keys['notification.update_selected.failed'])
self.bt_upgrade.setEnabled(True)
self.checkbox_console.setChecked(True)
def _update_action_output(self, output: str):
self.textarea_output.appendPlainText(output)
def _begin_action(self, action_label: str, keep_search: bool = False, clear_filters: bool = False):
self.ref_bt_about.setVisible(False)
self.ref_label_updates.setVisible(False)
self.thread_animate_progress.stop = False
self.thread_animate_progress.start()
self.ref_progress_bar.setVisible(True)
self.label_status.setText(action_label + "...")
self.bt_upgrade.setEnabled(False)
self.bt_refresh.setEnabled(False)
self.checkbox_only_apps.setEnabled(False)
self.table_apps.setEnabled(False)
self.checkbox_updates.setEnabled(False)
if keep_search:
self.ref_toolbar_search.setVisible(True)
else:
self.ref_toolbar_search.setVisible(False)
if clear_filters:
self._update_type_filters(set())
else:
self.extra_filters.setEnabled(False)
def finish_action(self):
self.ref_bt_about.setVisible(True)
self.ref_progress_bar.setVisible(False)
self.ref_label_updates.setVisible(True)
self.thread_animate_progress.stop = True
self.progress_bar.setValue(0)
self.bt_refresh.setEnabled(True)
self.checkbox_only_apps.setEnabled(True)
self.table_apps.setEnabled(True)
self.input_search.setEnabled(True)
self.label_status.setText('')
self.ref_toolbar_search.setVisible(True)
self.ref_toolbar_search.setEnabled(True)
self.extra_filters.setEnabled(True)
self.checkbox_updates.setEnabled(True)
def downgrade_app(self, app: ApplicationView):
pwd = None
requires_root = self.manager.requires_root('downgrade', self.table_apps.get_selected_app().model)
if not is_root() and requires_root:
pwd, ok = ask_root_password(self.locale_keys)
if not ok:
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.downgrading'], app.model.base_data.name))
self.thread_downgrade.app = app
self.thread_downgrade.root_password = pwd
self.thread_downgrade.start()
def get_app_info(self, app: dict):
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.info'])
self.thread_get_info.app = app
self.thread_get_info.start()
def get_app_history(self, app: dict):
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.history'])
self.thread_get_history.app = app
self.thread_get_history.start()
def _finish_get_info(self, app_info: dict):
self.finish_action()
self.change_update_state(change_filters=False)
dialog_info = InfoDialog(app=app_info, icon_cache=self.icon_cache, locale_keys=self.locale_keys, screen_size=self.screen_size)
dialog_info.exec_()
def _finish_get_history(self, app: dict):
self.finish_action()
self.change_update_state(change_filters=False)
if app.get('error'):
self._handle_console_option(True)
self.textarea_output.appendPlainText(app['error'])
self.checkbox_console.setChecked(True)
else:
dialog_history = HistoryDialog(app, self.icon_cache, self.locale_keys)
dialog_history.exec_()
def _begin_search(self, word):
self._handle_console_option(False)
self.ref_checkbox_only_apps.setVisible(False)
self.ref_checkbox_updates.setVisible(False)
self.filter_updates = False
self._begin_action('{}{}'.format(self.locale_keys['manage_window.status.searching'], '"{}"'.format(word) if word else ''), clear_filters=True)
def search(self):
word = self.input_search.text().strip()
if word:
self._begin_search(word)
self.thread_search.word = word
self.thread_search.start()
def _finish_search(self, apps_found: List[Application]):
self.finish_action()
self.ref_bt_upgrade.setVisible(False)
self.update_apps(apps_found, update_check_enabled=False)
def install_app(self, app: ApplicationView):
pwd = None
requires_root = self.manager.requires_root('install', app.model)
if not is_root() and requires_root:
pwd, ok = ask_root_password(self.locale_keys)
if not ok:
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.installing'], app.model.base_data.name))
self.thread_install.app = app
self.thread_install.root_password = pwd
self.thread_install.start()
def _finish_install(self, app: ApplicationView):
self.input_search.setText('')
self.finish_action()
if app:
if self._can_notify_user():
system.notify_user('{} ({}) {}'.format(app.model.base_data.name, app.model.get_type(), self.locale_keys['installed']))
self.refresh_apps()
else:
if self._can_notify_user():
system.notify_user('{}: {}'.format(app.model.base_data.name, self.locale_keys['notification.install.failed']))
self.checkbox_console.setChecked(True)
def _update_progress(self, value: int):
self.progress_bar.setValue(value)