supporting snaps

This commit is contained in:
Vinícius Moreira
2019-07-23 17:16:44 -03:00
committed by GitHub
parent 332d3cb787
commit 854ad2a102
39 changed files with 1739 additions and 650 deletions

View File

@@ -3,10 +3,10 @@ from threading import Lock
from typing import List
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor
from PyQt5.QtGui import QPixmap, QIcon, QCursor
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
QHeaderView, QLabel
QHeaderView, QLabel, QHBoxLayout
from fpakman.core import resource
from fpakman.core.model import ApplicationStatus
@@ -26,7 +26,7 @@ class UpdateToggleButton(QToolButton):
self.icon_on = QIcon(resource.get_path('img/toggle_on.svg'))
self.icon_off = QIcon(resource.get_path('img/toggle_off.svg'))
self.setIcon(self.icon_on)
self.setStyleSheet('border: 0px;')
self.setStyleSheet('QToolButton { border: 0px; }')
self.setToolTip(locale_keys['manage_window.apps_table.upgrade_toggle.tooltip'])
if not checked:
@@ -40,15 +40,15 @@ class UpdateToggleButton(QToolButton):
class AppsTable(QTableWidget):
def __init__(self, parent: QWidget, icon_cache: Cache, disk_cache: bool):
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 = [parent.locale_keys[key].capitalize() for key in ['name',
'version',
'latest_version',
'flatpak.info.description',
'description',
'type',
'installed',
'manage_window.columns.update']]
@@ -56,6 +56,7 @@ class AppsTable(QTableWidget):
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)
@@ -74,13 +75,19 @@ class AppsTable(QTableWidget):
menu_row = QMenu()
if app.model.has_info():
action_info = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.info"])
action_info.setIcon(QIcon(resource.get_path('img/info.svg')))
action_info.triggered.connect(self._get_app_info)
menu_row.addAction(action_info)
if app.model.installed:
if app.model.has_info():
action_info = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.info"])
action_info.setIcon(QIcon(resource.get_path('img/info.svg')))
action_info.triggered.connect(self._get_app_info)
menu_row.addAction(action_info)
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')))
action_history.triggered.connect(self._refresh_app)
menu_row.addAction(action_history)
if app.model.has_history():
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"])
@@ -99,6 +106,7 @@ class AppsTable(QTableWidget):
action_downgrade.triggered.connect(self._downgrade_app)
action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg')))
menu_row.addAction(action_downgrade)
else:
if app.model.can_be_installed():
action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"])
@@ -119,15 +127,17 @@ class AppsTable(QTableWidget):
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:
self.network_man.get(QNetworkRequest(QUrl(app_v.model.base_data.icon_url)))
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.cellWidget(idx, 2).setText(app_v.model.base_data.latest_version)
self._set_col_description(self.item(idx, 3), app_v)
self._set_col_version(idx, app_v)
self._set_col_description(idx, app_v)
app_v.status = ApplicationViewStatus.READY
self.window.resize_and_center()
@@ -156,6 +166,9 @@ class AppsTable(QTableWidget):
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())
@@ -195,43 +208,67 @@ class AppsTable(QTableWidget):
if app_views:
for idx, app_v in enumerate(app_views):
col_name = self._gen_col_name(app_v)
self.setItem(idx, 0, col_name)
col_version = QLabel(app_v.model.base_data.version)
col_version.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 1, col_version)
col_release = QLabel(app_v.get_async_attr('latest_version'))
col_release.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 2, col_release)
if 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:
col_release.setStyleSheet("color: orange")
col_description = QTableWidgetItem()
col_description.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self._set_col_description(col_description, app_v)
self.setItem(idx, 3, col_description)
col_type = QLabel(app_v.model.get_type())
col_type.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 4, col_type)
col_installed = QLabel()
col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app_v.model.installed else 'red_cross')))))
col_installed.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 5, col_installed)
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)
col_update = UpdateToggleButton(app_v, self.window, self.window.locale_keys, app_v.model.update) if app_v.model.update else None
self.setCellWidget(idx, 6, col_update)
self.setCellWidget(idx, 5, col_update)
def _gen_col_name(self, app_v: ApplicationView):
def _set_col_installed(self, idx: int, app_v: ApplicationView):
col_installed = QLabel()
if app_v.model.installed:
img_name = 'checked'
tooltip = self.window.locale_keys['installed']
else:
img_name = 'red_cross'
tooltip = self.window.locale_keys['uninstalled']
col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format(img_name)))))
col_installed.setAlignment(Qt.AlignCenter)
col_installed.setToolTip(tooltip)
self.setCellWidget(idx, 4, col_installed)
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: #32CD32")
tooltip = self.window.locale_keys['version.installed_outdated']
if 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)
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:
@@ -240,6 +277,7 @@ class AppsTable(QTableWidget):
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:
@@ -247,9 +285,11 @@ class AppsTable(QTableWidget):
icon = icon_data['icon'] if icon_data else QIcon(app_v.model.get_default_icon_path())
col.setIcon(icon)
return col
self.setItem(idx, 0, col)
def _set_col_description(self, col: QTableWidgetItem, app_v: ApplicationView):
def _set_col_description(self, idx: int, app_v: ApplicationView):
col = QTableWidgetItem()
col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
desc = app_v.get_async_attr('description', strip_html=True)
if desc:
@@ -258,6 +298,8 @@ class AppsTable(QTableWidget):
if app_v.model.status == ApplicationStatus.READY:
col.setToolTip(desc)
self.setItem(idx, 2, col)
def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents):
header_horizontal = self.horizontalHeader()
for i in range(self.columnCount()):

View File

@@ -2,7 +2,7 @@ import operator
from functools import reduce
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtGui import QIcon, QColor
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
@@ -37,7 +37,7 @@ class HistoryDialog(QDialog):
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
if current_app_commit:
item.setBackground(Qt.darkYellow if row != 0 else Qt.darkGreen)
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)

View File

@@ -1,37 +1,47 @@
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QGroupBox, \
QLineEdit, QLabel, QPlainTextEdit
QLineEdit, QLabel
from fpakman.util import util
class InfoDialog(QDialog):
def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict):
def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict, app_type: str, screen_size: QSize()):
super(InfoDialog, self).__init__()
self.setWindowTitle(app['name'])
self.setWindowIcon(app_icon)
self.screen_size = screen_size
layout = QVBoxLayout()
self.setLayout(layout)
gbox_info_layout = QFormLayout()
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)
for attr in sorted(app.keys()):
if attr != 'name' and app[attr]:
if attr == 'description':
text = QPlainTextEdit()
text.appendHtml(app[attr])
else:
text = QLineEdit()
text.setText(app[attr])
text.setCursorPosition(0)
text.setStyleSheet("width: 400px")
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('flatpak.info.' + attr, attr)).capitalize())
label = QLabel("{}: ".format(locale_keys.get(app_type + '.info.' + attr, attr)).capitalize())
label.setStyleSheet("font-weight: bold")
gbox_info_layout.addRow(label, text)

View File

@@ -2,14 +2,15 @@ import time
from threading import Lock
from typing import List
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
from fpakman.core import resource, system
from fpakman.core.controller import FlatpakManager, ApplicationManager
from fpakman.core.controller import ApplicationManager
from fpakman.core.model import Application
from fpakman.util.cache import Cache
from fpakman.view.qt import dialog
from fpakman.view.qt.about import AboutDialog
from fpakman.view.qt.window import ManageWindow
@@ -39,12 +40,14 @@ class UpdateCheck(QThread):
class TrayIcon(QSystemTrayIcon):
def __init__(self, locale_keys: dict, manager: ApplicationManager, icon_cache: Cache, disk_cache: bool, check_interval: int = 60, update_notification: bool = True):
def __init__(self, locale_keys: dict, manager: ApplicationManager, icon_cache: Cache, disk_cache: bool, download_icons: bool, screen_size: QSize, check_interval: int = 60, update_notification: bool = True):
super(TrayIcon, self).__init__()
self.locale_keys = locale_keys
self.manager = manager
self.icon_cache = icon_cache
self.disk_cache = disk_cache
self.download_icons = download_icons
self.screen_size = screen_size
self.icon_default = QIcon(resource.get_path('img/logo.png'))
self.icon_update = QIcon(resource.get_path('img/logo_update.png'))
@@ -74,6 +77,12 @@ class TrayIcon(QSystemTrayIcon):
self.update_notification = update_notification
self.lock_notify = Lock()
self.activated.connect(self.handle_click)
def handle_click(self, reason):
if reason == self.Trigger:
self.show_manage_window()
def notify_updates(self, updates: List[Application]):
self.lock_notify.acquire()
@@ -110,12 +119,14 @@ class TrayIcon(QSystemTrayIcon):
manager=self.manager,
icon_cache=self.icon_cache,
tray_icon=self,
disk_cache=self.disk_cache)
disk_cache=self.disk_cache,
download_icons=self.download_icons,
screen_size=self.screen_size)
if self.manage_window.isMinimized():
self.manage_window.setWindowState(Qt.WindowNoState)
else:
self.manage_window.refresh()
elif not self.manage_window.isVisible():
self.manage_window.refresh_apps()
self.manage_window.show()
def show_about(self):

View File

@@ -1,3 +1,4 @@
import subprocess
import time
from datetime import datetime, timedelta
from typing import List
@@ -8,14 +9,51 @@ from PyQt5.QtCore import QThread, pyqtSignal
from fpakman.core.controller import ApplicationManager
from fpakman.core.exception import NoInternetException
from fpakman.core.model import ApplicationStatus
from fpakman.core.system import FpakmanProcess
from fpakman.util.cache import Cache
from fpakman.view.qt import dialog
from fpakman.view.qt.view_model import ApplicationView
class UpdateSelectedApps(QThread):
class AsyncAction(QThread):
signal_finished = pyqtSignal(bool)
def notify_subproc_outputs(self, proc: FpakmanProcess, 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_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, apps_to_update: List[ApplicationView] = None):
@@ -25,31 +63,20 @@ class UpdateSelectedApps(QThread):
def run(self):
error = False
success = False
for app in self.apps_to_update:
subproc = self.manager.update_and_stream(app.model)
process = self.manager.update_and_stream(app.model)
success = self.notify_subproc_outputs(process, self.signal_output)
self.signal_output.emit(' '.join(subproc.args) + '\n')
for output in subproc.stdout:
line = output.decode().strip()
if line:
self.signal_output.emit(line)
for output in subproc.stderr:
line = output.decode().strip()
if line:
error = True
self.signal_output.emit(line)
self.signal_output.emit('\n')
if error:
if not success:
break
else:
self.signal_output.emit('\n')
self.signal_finished.emit(not error)
self.signal_finished.emit(success, len(self.apps_to_update))
self.apps_to_update = None
class RefreshApps(QThread):
@@ -64,8 +91,8 @@ class RefreshApps(QThread):
self.signal.emit(self.manager.read_installed())
class UninstallApp(QThread):
signal_finished = pyqtSignal()
class UninstallApp(AsyncAction):
signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, icon_cache: Cache, app: ApplicationView = None):
@@ -73,33 +100,23 @@ class UninstallApp(QThread):
self.app = app
self.manager = manager
self.icon_cache = icon_cache
self.root_password = None
def run(self):
if self.app:
subproc = self.manager.uninstall_and_stream(self.app.model)
self.signal_output.emit(' '.join(subproc.args) + '\n')
process = self.manager.uninstall_and_stream(self.app.model, self.root_password)
success = self.notify_subproc_outputs(process, self.signal_output)
for output in subproc.stdout:
line = output.decode().strip()
if line:
self.signal_output.emit(line)
error = False
for output in subproc.stderr:
line = output.decode().strip()
if line:
error = True
self.signal_output.emit(line)
if not error:
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.signal_finished.emit(success)
self.app = None
self.root_password = None
class DowngradeApp(QThread):
class DowngradeApp(AsyncAction):
signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str)
@@ -113,18 +130,15 @@ class DowngradeApp(QThread):
def run(self):
if self.app:
success = True
success = False
try:
stream = self.manager.downgrade_app(self.app.model, self.root_password)
process = self.manager.downgrade_app(self.app.model, self.root_password)
if stream is None:
if process is None:
dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'],
body=self.locale_keys['popup.downgrade.impossible.body'])
else:
for output in stream:
line = output.decode().strip()
if line:
self.signal_output.emit(line)
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'])
@@ -180,54 +194,51 @@ class SearchApps(QThread):
apps_found = []
if self.word:
apps_found = self.manager.search(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(QThread):
class InstallApp(AsyncAction):
signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, disk_cache: bool, icon_cache: Cache, app: ApplicationView = None):
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:
subproc = self.manager.install_and_stream(self.app.model)
self.signal_output.emit(' '.join(subproc.args) + '\n')
success = False
for output in subproc.stdout:
line = output.decode().strip()
if line:
self.signal_output.emit(line)
try:
process = self.manager.install_and_stream(self.app.model, self.root_password)
success = self.notify_subproc_outputs(process, self.signal_output)
error = False
if success and self.disk_cache:
self.app.model.installed = True
for output in subproc.stderr:
line = output.decode().strip()
if line:
error = True
self.signal_output.emit(line)
if not error 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)
self.app = None
self.signal_finished.emit(not error)
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.app = None
self.signal_finished.emit(success)
class AnimateProgress(QThread):
@@ -292,3 +303,30 @@ class VerifyModels(QThread):
break
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)

View File

@@ -1,7 +1,7 @@
from enum import Enum
from fpakman.core.model import Application, ApplicationStatus
from fpakman.util import util
from fpakman.core.model import Application, ApplicationStatus, FlatpakApplication
class ApplicationViewStatus(Enum):
@@ -19,7 +19,7 @@ class ApplicationView:
def get_async_attr(self, attr: str, strip_html: bool = False, default: str = '...'):
if isinstance(self.model, FlatpakApplication) and self.model.runtime:
if self.model.is_library():
res = getattr(self.model.base_data, attr)
else:
res = getattr(self.model.base_data, attr) if self.model.status == ApplicationStatus.READY and getattr(self.model.base_data, attr) else default

View File

@@ -1,14 +1,14 @@
import operator
from functools import reduce
from threading import Lock
from typing import List
from typing import List, Set
from PyQt5.QtCore import QEvent
from PyQt5.QtCore import QEvent, Qt
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar
QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QHBoxLayout
from fpakman.core import resource
from fpakman.core import resource, system
from fpakman.core.controller import ApplicationManager
from fpakman.core.model import Application
from fpakman.util.cache import Cache
@@ -17,7 +17,7 @@ from fpakman.view.qt.history import HistoryDialog
from fpakman.view.qt.info import InfoDialog
from fpakman.view.qt.root import is_root, ask_root_password
from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels
GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels, RefreshApp
from fpakman.view.qt.view_model import ApplicationView
DARK_ORANGE = '#FF4500'
@@ -26,7 +26,7 @@ DARK_ORANGE = '#FF4500'
class ManageWindow(QWidget):
__BASE_HEIGHT__ = 400
def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, tray_icon=None):
def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, download_icons: bool, screen_size, tray_icon=None):
super(ManageWindow, self).__init__()
self.locale_keys = locale_keys
self.manager = manager
@@ -37,6 +37,8 @@ class ManageWindow(QWidget):
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__)
@@ -46,14 +48,20 @@ class ManageWindow(QWidget):
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)
self.toolbar_search.addWidget(self._new_spacer())
label_pre_search = QLabel()
label_pre_search.setStyleSheet(
"background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;")
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()
@@ -69,30 +77,33 @@ class ManageWindow(QWidget):
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.toolbar_search.addWidget(self._new_spacer())
self.layout.addWidget(self.toolbar_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_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.filter_only_apps)
toolbar.addWidget(self.checkbox_only_apps)
self.checkbox_only_apps.stateChanged.connect(self._handle_filter_only_apps)
self.ref_checkbox_only_apps = toolbar.addWidget(self.checkbox_only_apps)
toolbar.addWidget(self._new_spacer())
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.label_status = QLabel()
self.label_status.setText('')
self.label_status.setStyleSheet("font-weight: bold")
toolbar.addWidget(self.label_status)
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(keep_console=False))
self.bt_refresh.clicked.connect(lambda: self.refresh_apps(keep_console=False))
toolbar.addWidget(self.bt_refresh)
self.bt_upgrade = QToolButton()
@@ -104,7 +115,7 @@ class ManageWindow(QWidget):
self.layout.addWidget(toolbar)
self.table_apps = AppsTable(self, self.icon_cache, disk_cache=self.disk_cache)
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)
@@ -132,7 +143,7 @@ class ManageWindow(QWidget):
self.thread_update.signal_finished.connect(self._finish_update_selected)
self.thread_refresh = RefreshApps(self.manager)
self.thread_refresh.signal.connect(self._finish_refresh)
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)
@@ -151,7 +162,7 @@ class ManageWindow(QWidget):
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)
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)
@@ -161,10 +172,11 @@ class ManageWindow(QWidget):
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.toolbar_bottom = QToolBar()
self.label_updates = QLabel('')
self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE))
self.ref_label_updates = self.toolbar_bottom.addWidget(self.label_updates)
self.toolbar_bottom.addWidget(self._new_spacer())
@@ -174,10 +186,34 @@ class ManageWindow(QWidget):
self.toolbar_bottom.addWidget(self._new_spacer())
self.label_updates = QLabel()
self.ref_label_updates = self.toolbar_bottom.addWidget(self.label_updates)
self.layout.addWidget(self.toolbar_bottom)
self.centralize()
self.filter_only_apps = True
self.filter_types = set()
self.filter_updates = False
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()
@@ -234,61 +270,136 @@ class ManageWindow(QWidget):
self.checkbox_console.setChecked(False)
self.textarea_output.hide()
def refresh(self, keep_console: bool = True):
def refresh_apps(self, keep_console: bool = True):
if self._acquire_lock():
self.filter_types.clear()
self.input_search.clear()
if not keep_console:
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.refreshing'])
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(self, apps: List[Application]):
def _finish_refresh_apps(self, apps: List[Application]):
self.update_apps(apps)
self.finish_action()
self.ref_checkbox_only_apps.setVisible(True)
self._release_lock()
def uninstall_app(self, app: ApplicationView):
if self._acquire_lock():
pwd = None
requires_root = self.manager.requires_root('uninstall', 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:
self._release_lock()
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 _finish_uninstall(self):
def refresh(self, app: ApplicationView):
if self._acquire_lock():
pwd = None
requires_root = self.manager.requires_root('refresh', 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:
self._release_lock()
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.refreshing'], 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, success: bool):
self.finish_action()
self._release_lock()
self.refresh()
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['uninstalled']))
self.refresh_apps()
else:
if self._can_notify_user():
app = self.table_apps.get_selected_app()
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()
self._release_lock()
if success:
self.refresh()
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()
else:
if self._can_notify_user():
system.notify_user(self.locale_keys['notification.downgrade.failed'])
self.change_update_state(notify_tray=False)
self.checkbox_console.setChecked(True)
def filter_only_apps(self, only_apps: int):
def _finish_refresh(self, success: bool):
self.finish_action()
self._release_lock()
if success:
self.refresh_apps()
else:
self.change_update_state(notify_tray=False)
self.checkbox_console.setChecked(True)
def apply_filters(self):
if self.apps:
show_only_apps = True if only_apps == 2 else False
visible_apps = len(self.apps)
for idx, app_v in enumerate(self.apps):
hidden = show_only_apps and app_v.model.is_library()
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()
self.change_update_state(change_filters=False)
self.table_apps.change_headers_policy(QHeaderView.Stretch)
self.table_apps.change_headers_policy()
self.resize_and_center()
self.resize_and_center(accept_lower_width=visible_apps > 0)
def change_update_state(self):
def change_update_state(self, notify_tray: bool = True, change_filters: bool = True):
enable_bt_update = False
@@ -301,23 +412,38 @@ class ManageWindow(QWidget):
else:
app_updates += 1
total_updates = app_updates + library_updates
if total_updates > 0:
self.label_updates.setText('{}: {}'.format(self.locale_keys['manage_window.label.updates'], total_updates))
self.label_updates.setToolTip('{} {} | {} {}'.format(app_updates,
self.locale_keys['manage_window.checkbox.only_apps'].lower(),
library_updates,
self.locale_keys['others'].lower()))
else:
self.label_updates.setText('')
for app_v in self.apps:
if app_v.visible and app_v.update_checked:
enable_bt_update = True
break
self.bt_upgrade.setEnabled(enable_bt_update)
self.tray_icon.notify_updates([app.model for app in self.apps if app.model.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 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())
if notify_tray:
self.tray_icon.notify_updates([app.model for app in self.apps if app.model.update])
def centralize(self):
geo = self.frameGeometry()
@@ -330,12 +456,12 @@ class ManageWindow(QWidget):
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())
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)
@@ -346,17 +472,41 @@ class ManageWindow(QWidget):
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)
self.apply_filters()
self.change_update_state()
self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0)
self.resize_and_center()
self.thread_verify_models.apps = self.apps
self.thread_verify_models.start()
def resize_and_center(self):
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)
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
self.resize(new_width, self.height())
if accept_lower_width or new_width > self.width():
self.resize(new_width, self.height())
self.centralize()
def update_selected(self):
@@ -373,20 +523,26 @@ class ManageWindow(QWidget):
self.thread_update.apps_to_update = to_update
self.thread_update.start()
def _finish_update_selected(self, success: bool):
def _finish_update_selected(self, success: bool, updated: int):
self.finish_action()
self._release_lock()
if success:
self.refresh()
if self._can_notify_user():
system.notify_user('{} {}'.format(updated, self.locale_keys['notification.update_selected.success']))
self.refresh_apps()
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):
def _begin_action(self, action_label: str, keep_search: bool = False, clear_filters: bool = False):
self.ref_label_updates.setVisible(False)
self.thread_animate_progress.stop = False
self.thread_animate_progress.start()
@@ -397,11 +553,17 @@ class ManageWindow(QWidget):
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.toolbar_search.setEnabled(False)
self.ref_toolbar_search.setVisible(True)
else:
self.toolbar_search.setVisible(False)
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_progress_bar.setVisible(False)
@@ -413,15 +575,18 @@ class ManageWindow(QWidget):
self.table_apps.setEnabled(True)
self.input_search.setEnabled(True)
self.label_status.setText('')
self.toolbar_search.setVisible(True)
self.toolbar_search.setEnabled(True)
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):
if self._acquire_lock():
pwd = None
requires_root = self.manager.requires_root('downgrade', self.table_apps.get_selected_app().model)
if not is_root():
if not is_root() and requires_root:
pwd, ok = ask_root_password(self.locale_keys)
if not ok:
@@ -456,7 +621,7 @@ class ManageWindow(QWidget):
self._release_lock()
self.finish_action()
self.change_update_state()
dialog_info = InfoDialog(app_info, self.table_apps.get_selected_app_icon(), self.locale_keys)
dialog_info = InfoDialog(app_info, self.table_apps.get_selected_app_icon(), self.locale_keys, self.table_apps.get_selected_app().model.get_type(), self.screen_size)
dialog_info.exec_()
def _finish_get_history(self, app: dict):
@@ -478,7 +643,10 @@ class ManageWindow(QWidget):
if word and self._acquire_lock():
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.searching'], keep_search=True)
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'], word), clear_filters=True)
self.thread_search.word = word
self.thread_search.start()
@@ -489,10 +657,22 @@ class ManageWindow(QWidget):
def install_app(self, app: ApplicationView):
if self._acquire_lock():
pwd = None
requires_root = self.manager.requires_root('install', 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:
self._release_lock()
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, success: bool):
@@ -501,8 +681,16 @@ class ManageWindow(QWidget):
self._release_lock()
if success:
self.refresh()
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['installed']))
self.refresh_apps()
else:
if self._can_notify_user():
app = self.table_apps.get_selected_app()
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):