diff --git a/app.py b/app.py new file mode 100644 index 00000000..0f0b6820 --- /dev/null +++ b/app.py @@ -0,0 +1,17 @@ +#!env/bin/python +import sys + +from PyQt5.QtWidgets import QApplication, QWidget + +from core.controller import FlatpakController +from core.model import FlatpakManager +from view.qt.systray import TrayIcon + +app = QApplication(sys.argv) +manager = FlatpakManager() +controller = FlatpakController(manager) +hidden_widget = QWidget() +trayIcon = TrayIcon(parent=hidden_widget, controller=controller, check_interval=30) +trayIcon.show() + +sys.exit(app.exec_()) diff --git a/app_qt.py b/app_qt.py deleted file mode 100755 index ab4389d7..00000000 --- a/app_qt.py +++ /dev/null @@ -1,20 +0,0 @@ -#!env/bin/python -import sys - -from PyQt5.QtWidgets import QApplication - -from core.controller import FlatpakController -from core.model import FlatpakManager -from view.qt.window import MainWindow - -app = QApplication(sys.argv) -app.setStyle('Fusion') -manager = FlatpakManager() -controller = FlatpakController(manager) -window_main = MainWindow(controller) -window_main.refresh() - -# tray_icon = SystemTrayIcon(window_main) -window_main.show() -# tray_icon.show() -sys.exit(app.exec_()) diff --git a/resources/img/flathub_logo.svg b/resources/img/flathub_logo.svg index bbd82f4b..2c72e02e 100755 --- a/resources/img/flathub_logo.svg +++ b/resources/img/flathub_logo.svg @@ -14,7 +14,7 @@ viewBox="0 0 11.495958 11.042967" version="1.1" id="svg3871" - inkscape:version="0.92.3 (2405546, 2018-03-11)" + inkscape:version="0.92.4 5da689c313, 2019-01-14" sodipodi:docname="flathub_logo.svg"> @@ -27,15 +27,15 @@ inkscape:pageshadow="2" inkscape:zoom="0.35" inkscape:cx="354.2246" - inkscape:cy="117.98097" + inkscape:cy="117.98098" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" showborder="false" - inkscape:window-width="1366" - inkscape:window-height="739" + inkscape:window-width="1360" + inkscape:window-height="707" inkscape:window-x="0" - inkscape:window-y="260" + inkscape:window-y="0" inkscape:window-maximized="1" fit-margin-bottom="-0.2" fit-margin-top="0" diff --git a/resources/img/update_logo.svg b/resources/img/update_logo.svg new file mode 100644 index 00000000..fdfd0c3a --- /dev/null +++ b/resources/img/update_logo.svg @@ -0,0 +1,179 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/view/qt/systray.py b/view/qt/systray.py index 4ec445ca..739b6bbf 100755 --- a/view/qt/systray.py +++ b/view/qt/systray.py @@ -1,29 +1,76 @@ import sys +import time +from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QApplication, QWidget +from PyQt5.QtWidgets import QSystemTrayIcon, QMenu from core import resource # to update -#apps = flatpak.list_installed() -#print(apps) +from core.controller import FlatpakController +from view.qt.window import ManageWindow -class SystemTrayIcon(QSystemTrayIcon): +class UpdateCheck(QThread): - def __init__(self, parent=None): - QSystemTrayIcon.__init__(self, QIcon(resource.get_path('img/flathub_logo.svg')), parent) + signal = pyqtSignal(int) + + def __init__(self, check_interval: int, controller: FlatpakController, parent=None): + super(UpdateCheck, self).__init__(parent) + self.controller = controller + self.check_interval = check_interval + + def run(self): + + while True: + + apps = self.controller.refresh() + + updates = len([app for app in apps if app['update']]) + + self.signal.emit(updates) + + time.sleep(self.check_interval) + + +class TrayIcon(QSystemTrayIcon): + + def __init__(self, controller: FlatpakController, check_interval: int = 60, parent=None): + self.icon_default = QIcon(resource.get_path('img/flathub_logo.svg')) + self.icon_update = QIcon(resource.get_path('img/update_logo.svg')) + QSystemTrayIcon.__init__(self, self.icon_default, parent) self.menu = QMenu(parent) - exitAction = self.menu.addAction("Exit") + + self.action_manage = self.menu.addAction("Manage applications") + self.action_manage.triggered.connect(self.show_manage_window) + + self.action_exit = self.menu.addAction("Exit") + self.action_exit.triggered.connect(lambda: QCoreApplication.exit()) self.setContextMenu(self.menu) + self.controller = controller + self.check_thread = UpdateCheck(check_interval=check_interval, controller=self.controller) + self.check_thread.signal.connect(self.notify_update) + self.check_thread.start() + self.manage_window = ManageWindow(controller=controller, tray_icon=self) + def notify_update(self, updates: int): + if updates > 0: + if self.icon() != self.icon_update: + self.setIcon(self.icon_update) + self.setToolTip('Updates found: {}'.format(updates)) -if __name__ == '__main__': - app = QApplication(sys.argv) - w = QWidget() - trayIcon = SystemTrayIcon(w) - trayIcon.show() + if self.manage_window: + self.manage_window.refresh() + else: + self.setIcon(self.icon_default) + self.setToolTip(None) - sys.exit(app.exec_()) + def show_manage_window(self): + + if not self.manage_window: + self.manage_window = ManageWindow(controller=self.controller) + + self.manage_window.refresh() + self.manage_window.show() diff --git a/view/qt/window.py b/view/qt/window.py index 55959838..9e2527bb 100755 --- a/view/qt/window.py +++ b/view/qt/window.py @@ -1,18 +1,16 @@ import operator from functools import reduce - -from PyQt5 import QtCore -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest - -from core import resource, __version__ +from threading import Lock from typing import List from PyQt5.QtCore import Qt, QThread, pyqtSignal, QUrl from PyQt5.QtGui import QIcon, QColor, QPixmap +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QTableWidget, \ QTableWidgetItem, QTableView, QCheckBox, QHeaderView, QToolButton, QToolBar, \ QSizePolicy, QLabel, QMessageBox +from core import resource, __version__ from core.controller import FlatpakController @@ -38,23 +36,24 @@ class UpdateToggleButton(QToolButton): self.root.change_update_button_state() -class MainWindow(QWidget): +class ManageWindow(QWidget): - __COLUMNS__ = ['Package', 'Version', 'Branch', 'Arch', 'Ref', 'Latest Version', 'Origin', 'Update ?'] + __COLUMNS__ = ['Package', 'Version', 'Latest Version', 'Branch', 'Arch', 'Ref', 'Origin', 'Update ?'] __BASE_HEIGHT__ = 400 - def __init__(self, controller: FlatpakController): - super(MainWindow, self).__init__() + def __init__(self, controller: FlatpakController, tray_icon = None): + super(ManageWindow, self).__init__() self.controller = controller self.icon_cache = {} + self.tray_icon = tray_icon self.network_man = QNetworkAccessManager() self.network_man.finished.connect(self._load_icon) self.icon_flathub = QIcon(resource.get_path('img/flathub_logo.svg')) self._check_flatpak_installed() - self.resize(MainWindow.__BASE_HEIGHT__, MainWindow.__BASE_HEIGHT__) - self.setWindowTitle('flatman ({})'.format(__version__)) + self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__) + self.setWindowTitle('fpakman ({})'.format(__version__)) self.setWindowIcon(self.icon_flathub) self.layout = QVBoxLayout() @@ -94,17 +93,17 @@ class MainWindow(QWidget): self.layout.addWidget(toolbar) self.table_apps = QTableWidget() - self.table_apps.setColumnCount(len(MainWindow.__COLUMNS__)) + self.table_apps.setColumnCount(len(ManageWindow.__COLUMNS__)) self.table_apps.setFocusPolicy(Qt.NoFocus) self.table_apps.setShowGrid(False) self.table_apps.verticalHeader().setVisible(False) self.table_apps.setSelectionBehavior(QTableView.SelectRows) - self.table_apps.setHorizontalHeaderLabels(MainWindow.__COLUMNS__) + self.table_apps.setHorizontalHeaderLabels(ManageWindow.__COLUMNS__) self.table_apps.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) header_horizontal = self.table_apps.horizontalHeader() - for i in range(0, len(MainWindow.__COLUMNS__)): - header_horizontal.setSectionResizeMode(i, QHeaderView.ResizeToContents) + for i in range(0, len(ManageWindow.__COLUMNS__)): + header_horizontal.setSectionResizeMode(i, QHeaderView.Stretch + QHeaderView.AdjustToContents) self.layout.addWidget(self.table_apps) @@ -117,7 +116,24 @@ class MainWindow(QWidget): self.refresh_thread = RefreshPackages(self.controller) self.refresh_thread.signal.connect(self._finish_refresh) - self.layout.addWidget(QLabel('flatpak: ' + self.controller.get_version()), alignment=Qt.AlignRight) + self.toolbar_bottom = QToolBar() + self.label_updates = QLabel('') + self.toolbar_bottom.addWidget(self.label_updates) + spacer = QWidget() + spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.toolbar_bottom.addWidget(spacer) + + self.toolbar_bottom.addWidget(QLabel('flatpak: ' + self.controller.get_version())) + self.layout.addWidget(self.toolbar_bottom) + + self.thread_lock = Lock() + self.working = False # restrict the number of threaded actions + + def closeEvent(self, event): + + if self.tray_icon: + event.ignore() + self.hide() def _load_icon(self, http_response): icon_url = http_response.url().toString() @@ -143,22 +159,37 @@ class MainWindow(QWidget): error_msg.exec_() exit(1) - def refresh(self, threaded: bool = False): + def _acquire_lock(self): - self._check_flatpak_installed() + self.thread_lock.acquire() - self._begin_action('Refreshing...') + if not self.working: + self.working = True - if threaded: + self.thread_lock.release() + return self.working + + def _release_lock(self): + + self.thread_lock.acquire() + + if self.working: + self.working = False + + self.thread_lock.release() + + def refresh(self): + + if self._acquire_lock(): + self._check_flatpak_installed() + self._begin_action('Refreshing...') self.refresh_thread.start() - else: - apps = self.controller.refresh() - self.update_packages(apps) - self.finish_action() def _finish_refresh(self): + self.update_packages(self.refresh_thread.apps) self.finish_action() + self._release_lock() def filter_only_apps(self, only_apps: int): @@ -170,12 +201,19 @@ class MainWindow(QWidget): self.table_apps.setRowHidden(idx, hidden) app['visible'] = not hidden - self.change_update_button_state() + self.change_update_state() - def change_update_button_state(self): + def change_update_state(self): enable_bt_update = False + updates = len([app for app in self.apps if app['model']['update']]) + + if updates > 0: + self.label_updates.setText('Updates: {}'.format(updates)) + else: + self.label_updates.setText('') + for app in self.apps: if app['visible'] and app['update_checked']: enable_bt_update = True @@ -223,28 +261,28 @@ class MainWindow(QWidget): col_version.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self.table_apps.setItem(idx, 1, col_version) + col_release = QTableWidgetItem() + col_release.setText(app['latest_version']) + col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + self.table_apps.setItem(idx, 2, col_release) + + if app['update']: + col_release.setForeground(QColor('orange')) + col_branch = QTableWidgetItem() col_branch.setText(app['branch']) col_branch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 2, col_branch) + self.table_apps.setItem(idx, 3, col_branch) col_arch = QTableWidgetItem() col_arch.setText(app['arch']) col_arch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 3, col_arch) + self.table_apps.setItem(idx, 4, col_arch) col_package = QTableWidgetItem() col_package.setText(app['ref']) col_package.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 4, col_package) - - col_release = QTableWidgetItem() - col_release.setText(app['latest_version']) - col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 5, col_release) - - if app['update']: - col_release.setForeground(QColor('yellow')) + self.table_apps.setItem(idx, 5, col_package) col_origin = QTableWidgetItem() col_origin.setText(app['origin']) @@ -260,28 +298,35 @@ class MainWindow(QWidget): self.apps.append(app_model) - self.change_update_button_state() + self.change_update_state() self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0) self.resize_and_center() def resize_and_center(self): - new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(MainWindow.__COLUMNS__))]) * 1.05 + new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(ManageWindow.__COLUMNS__))]) * 1.05 self.resize(new_width, self.height()) self.centralize() def update_selected(self): - if self.apps: - to_update = [pak['model']['ref'] for pak in self.apps if pak['visible'] and pak['update_checked']] - if to_update: - self._begin_action('Updating...') - self.update_thread.refs_to_update = to_update - self.update_thread.start() + if self._acquire_lock(): + if self.apps: + to_update = [pak['model']['ref'] for pak in self.apps if pak['visible'] and pak['update_checked']] + + if to_update: + self._begin_action('Updating...') + self.update_thread.refs_to_update = to_update + self.update_thread.start() def _finish_update_selected(self): self.update_packages(self.update_thread.updated_apps) self.finish_action() + if self.tray_icon and self.update_thread.updated_apps: + self.tray_icon.notify_update(len([app for app in self.update_thread.updated_apps if app['update']])) + + self._release_lock() + def _begin_action(self, action_label: str): self.label_status.setText(action_label) self.bt_update.setEnabled(False)