diff --git a/gotify_tray/gui/MainApplication.py b/gotify_tray/gui/MainApplication.py index c36115d..0a1253a 100644 --- a/gotify_tray/gui/MainApplication.py +++ b/gotify_tray/gui/MainApplication.py @@ -18,7 +18,6 @@ from gotify_tray.tasks import ( GetApplicationMessagesTask, GetMessagesTask, ProcessMessageTask, - ProcessMessagesTask, ServerConnectionWatchdogTask, ) from gotify_tray.gui.themes import set_theme @@ -153,9 +152,9 @@ class MainApplication(QtWidgets.QApplication): for message in page.messages: if message.id > last_id: if settings.value("message/check_missed/notify", type=bool): - self.new_message_callback(message) + self.new_message_callback(message, process=False) else: - self.add_message_to_model(message) + self.add_message_to_model(message, process=False) ids.append(message.id) if ids: @@ -184,49 +183,6 @@ class MainApplication(QtWidgets.QApplication): else: self.gotify_client.stop(reset_wait=True) - def insert_message( - self, - row: int, - message: gotify.GotifyMessageModel, - application: gotify.GotifyApplicationModel, - ): - """Insert a message gotify message into the messages model. Also add the message widget to the listview - - Args: - row (int): >=0: insert message at specified position, <0: append message to the end of the model - message (gotify.GotifyMessageModel): message - application (gotify.GotifyApplicationModel): application - """ - self.update_last_id(message.id) - message_item = MessagesModelItem(message) - - if row >= 0: - self.messages_model.insertRow(row, message_item) - else: - self.messages_model.appendRow(message_item) - - self.main_window.insert_message_widget( - message_item, - self.downloader.get_filename( - f"{self.gotify_client.url}/{application.image}" - ), - ) - - def get_messages_finished_callback(self, page: gotify.GotifyPagedMessagesModel): - """Process messages before inserting them into the main window - """ - - def insert_helper(message: gotify.GotifyMessageModel): - if item := self.application_model.itemFromId(message.appid): - self.insert_message( - -1, message, item.data(ApplicationItemDataRole.ApplicationRole) - ) - self.processEvents() - - self.process_messages_task = ProcessMessagesTask(page) - self.process_messages_task.message_processed.connect(insert_helper) - self.process_messages_task.start() - def application_selection_changed_callback( self, item: Union[ApplicationModelItem, ApplicationAllMessagesItem] ): @@ -237,62 +193,44 @@ class MainApplication(QtWidgets.QApplication): item.data(ApplicationItemDataRole.ApplicationRole).id, self.gotify_client, ) - self.get_application_messages_task.success.connect( - self.get_messages_finished_callback - ) + self.get_application_messages_task.message.connect(self.messages_model.append_message) self.get_application_messages_task.start() elif isinstance(item, ApplicationAllMessagesItem): self.get_messages_task = GetMessagesTask(self.gotify_client) - self.get_messages_task.success.connect(self.get_messages_finished_callback) + self.get_messages_task.message.connect(self.messages_model.append_message) self.get_messages_task.start() - def add_message_to_model(self, message: gotify.GotifyMessageModel): - if application_item := self.application_model.itemFromId(message.appid): + def add_message_to_model(self, message: gotify.GotifyMessageModel, process: bool = True): + if self.application_model.itemFromId(message.appid): application_index = self.main_window.currentApplicationIndex() - if selected_application_item := self.application_model.itemFromIndex( - application_index - ): + if selected_application_item := self.application_model.itemFromIndex(application_index): def insert_message_helper(): if isinstance(selected_application_item, ApplicationModelItem): # A single application is selected + # -> Only insert the message if the appid matches the selected appid if ( - message.appid - == selected_application_item.data( - ApplicationItemDataRole.ApplicationRole - ).id + message.appid + == selected_application_item.data(ApplicationItemDataRole.ApplicationRole).id ): - self.insert_message( - 0, - message, - application_item.data( - ApplicationItemDataRole.ApplicationRole - ), - ) - elif isinstance( - selected_application_item, ApplicationAllMessagesItem - ): + self.messages_model.insert_message(0, message) + elif isinstance(selected_application_item, ApplicationAllMessagesItem): # "All messages' is selected - self.insert_message( - 0, - message, - application_item.data( - ApplicationItemDataRole.ApplicationRole - ), - ) + self.messages_model.insert_message(0, message) - self.process_message_task = ProcessMessageTask(message) - self.process_message_task.finished.connect(insert_message_helper) - self.process_message_task.start() + if process: + self.process_message_task = ProcessMessageTask(message) + self.process_message_task.finished.connect(insert_message_helper) + self.process_message_task.start() + else: + insert_message_helper() else: - logger.error( - f"App id {message.appid} could not be found. Refreshing applications." - ) + logger.error(f"App id {message.appid} could not be found. Refreshing applications.") self.refresh_applications() - def new_message_callback(self, message: gotify.GotifyMessageModel): - self.add_message_to_model(message) + def new_message_callback(self, message: gotify.GotifyMessageModel, process: bool = True): + self.add_message_to_model(message, process=process) # Change the tray icon to show there are unread notifications if ( @@ -301,23 +239,19 @@ class MainApplication(QtWidgets.QApplication): ): self.tray.set_icon_unread() - # Show a notification + # Don't show a notification if it's low priority or the window is active if ( message.priority < settings.value("tray/notifications/priority", type=int) or self.main_window.isActiveWindow() ): return - if settings.value("tray/notifications/icon/show", type=bool): - if application_item := self.application_model.itemFromId(message.appid): - image_url = f"{self.gotify_client.url}/{application_item.data(ApplicationItemDataRole.ApplicationRole).image}" - icon = QtGui.QIcon(self.downloader.get_filename(image_url)) - else: - logger.error( - f"MainWindow.new_message_callback: App id {message.appid} could not be found. Refreshing applications." - ) - self.refresh_applications() - icon = QtWidgets.QSystemTrayIcon.MessageIcon.Information + # Get the application icon + if ( + settings.value("tray/notifications/icon/show", type=bool) + and (application_item := self.application_model.itemFromId(message.appid)) + ): + icon = application_item.icon() else: icon = QtWidgets.QSystemTrayIcon.MessageIcon.Information @@ -438,6 +372,8 @@ class MainApplication(QtWidgets.QApplication): self.styleHints().colorSchemeChanged.connect(lambda _: self.theme_change_requested_callback(settings.value("theme", type=str))) + self.messages_model.rowsInserted.connect(self.main_window.display_message_widgets) + self.watchdog.closed.connect(lambda: self.listener_closed_callback(None, None)) def init_shortcuts(self): diff --git a/gotify_tray/gui/models/MessagesModel.py b/gotify_tray/gui/models/MessagesModel.py index b74d895..16de087 100644 --- a/gotify_tray/gui/models/MessagesModel.py +++ b/gotify_tray/gui/models/MessagesModel.py @@ -3,6 +3,10 @@ import enum from typing import cast from PyQt6 import QtCore, QtGui from gotify_tray import gotify +from gotify_tray.database import Settings + + +settings = Settings("gotify-tray") class MessageItemDataRole(enum.IntEnum): @@ -16,6 +20,20 @@ class MessagesModelItem(QtGui.QStandardItem): class MessagesModel(QtGui.QStandardItemModel): + def update_last_id(self, i: int): + if i > settings.value("message/last", type=int): + settings.setValue("message/last", i) + + def insert_message(self, row: int, message: gotify.GotifyMessageModel): + self.update_last_id(message.id) + message_item = MessagesModelItem(message) + self.insertRow(row, message_item) + + def append_message(self, message: gotify.GotifyMessageModel): + self.update_last_id(message.id) + message_item = MessagesModelItem(message) + self.appendRow(message_item) + def setItem(self, row: int, column: int, item: MessagesModelItem) -> None: super(MessagesModel, self).setItem(row, column, item) diff --git a/gotify_tray/gui/widgets/MainWindow.py b/gotify_tray/gui/widgets/MainWindow.py index e502949..629440c 100644 --- a/gotify_tray/gui/widgets/MainWindow.py +++ b/gotify_tray/gui/widgets/MainWindow.py @@ -11,6 +11,8 @@ from . import MessageWidget from gotify_tray.__version__ import __title__ from gotify_tray.database import Settings from gotify_tray.gui.themes import get_theme_file +from gotify_tray.gui.models import MessageItemDataRole +from gotify_tray import gotify settings = Settings("gotify-tray") @@ -104,17 +106,20 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): def set_error(self): self.status_widget.set_error() - def insert_message_widget( - self, message_item: MessagesModelItem, image_path: str = "" - ): - message_widget = MessageWidget( - self.app, self.listView_messages, message_item, image_path=image_path - ) - self.listView_messages.setIndexWidget( - self.messages_model.indexFromItem(message_item), message_widget - ) - message_widget.deletion_requested.connect(self.delete_message.emit) - message_widget.image_popup.connect(self.image_popup.emit) + def display_message_widgets(self, parent: QtCore.QModelIndex, first: int, last: int): + for i in range(first, last+1): + if index := self.messages_model.index(i, 0, parent): + message_item = self.messages_model.itemFromIndex(index) + + message: gotify.GotifyMessageModel = self.messages_model.data(index, MessageItemDataRole.MessageRole) + + application_item = self.application_model.itemFromId(message.appid) + + message_widget = MessageWidget(self.app, self.listView_messages, message_item, icon=application_item.icon()) + message_widget.deletion_requested.connect(self.delete_message.emit) + message_widget.image_popup.connect(self.image_popup.emit) + + self.listView_messages.setIndexWidget(self.messages_model.indexFromItem(message_item), message_widget) def currentApplicationIndex(self) -> QtCore.QModelIndex: return self.listView_applications.selectionModel().currentIndex() diff --git a/gotify_tray/gui/widgets/MessageWidget.py b/gotify_tray/gui/widgets/MessageWidget.py index 0f87eae..267e80d 100644 --- a/gotify_tray/gui/widgets/MessageWidget.py +++ b/gotify_tray/gui/widgets/MessageWidget.py @@ -23,9 +23,9 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form): app: QtWidgets.QApplication, parent: QtWidgets.QWidget, message_item: MessagesModelItem, - image_path: str = "", + icon: QtGui.QIcon = None, ): - super(MessageWidget, self).__init__() + super(MessageWidget, self).__init__(parent) self.app = app self.parent = parent self.setupUi(self) @@ -59,15 +59,9 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form): self.label_message.setText(convert_links(message.message)) # Show the application icon - if image_path: + if icon: image_size = settings.value("MessageWidget/image/size", type=int) - self.label_image.setFixedSize(QtCore.QSize(image_size, image_size)) - pixmap = QtGui.QPixmap(image_path).scaled( - image_size, - image_size, - aspectRatioMode=QtCore.Qt.AspectRatioMode.KeepAspectRatioByExpanding, - transformMode=QtCore.Qt.TransformationMode.SmoothTransformation, - ) + pixmap = icon.pixmap(QtCore.QSize(image_size, image_size)) self.label_image.setPixmap(pixmap) else: self.label_image.hide() diff --git a/gotify_tray/gui/widgets/SettingsDialog.py b/gotify_tray/gui/widgets/SettingsDialog.py index 10c47db..7c621e1 100644 --- a/gotify_tray/gui/widgets/SettingsDialog.py +++ b/gotify_tray/gui/widgets/SettingsDialog.py @@ -118,7 +118,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): } ) ), - get_icon("gotify-small"), + QtGui.QIcon(get_icon("gotify-small")), ) self.layout_fonts_message.addWidget(self.message_widget) diff --git a/gotify_tray/tasks.py b/gotify_tray/tasks.py index 29f23a8..03d4853 100644 --- a/gotify_tray/tasks.py +++ b/gotify_tray/tasks.py @@ -8,10 +8,10 @@ from functools import reduce from PyQt6 import QtCore from PyQt6.QtCore import pyqtSignal -from gotify_tray.database import Cache, Downloader, Settings +from gotify_tray.database import Cache, Settings from gotify_tray.gotify.api import GotifyClient from gotify_tray.gotify.models import GotifyVersionModel -from gotify_tray.utils import get_image +from gotify_tray.utils import process_messages from . import gotify @@ -97,7 +97,7 @@ class GetApplicationsTask(BaseTask): class GetApplicationMessagesTask(BaseTask): - success = pyqtSignal(gotify.GotifyPagedMessagesModel) + message = pyqtSignal(gotify.GotifyMessageModel) error = pyqtSignal(gotify.GotifyErrorModel) def __init__(self, appid: int, gotify_client: gotify.GotifyClient): @@ -110,10 +110,16 @@ class GetApplicationMessagesTask(BaseTask): if isinstance(result, gotify.GotifyErrorModel): self.error.emit(result) else: - self.success.emit(result) + for message in process_messages(result.messages): + self.message.emit(message) + + # Prevent locking up the UI when there are a lot of messages ready at the same time + # -- side effect: switching application while the previous messages are still being inserted causes mixing of messages + time.sleep(0.001) class GetMessagesTask(BaseTask): + message = pyqtSignal(gotify.GotifyMessageModel) success = pyqtSignal(gotify.GotifyPagedMessagesModel) error = pyqtSignal(gotify.GotifyErrorModel) @@ -126,9 +132,22 @@ class GetMessagesTask(BaseTask): if isinstance(result, gotify.GotifyErrorModel): self.error.emit(result) else: + for message in process_messages(result.messages): + self.message.emit(message) + time.sleep(0.001) self.success.emit(result) +class ProcessMessageTask(BaseTask): + def __init__(self, message: gotify.GotifyMessageModel): + super(ProcessMessageTask, self).__init__() + self.message = message + + def task(self): + for _ in process_messages([self.message]): + pass + + class VerifyServerInfoTask(BaseTask): success = pyqtSignal(GotifyVersionModel) incorrect_token = pyqtSignal(GotifyVersionModel) @@ -205,35 +224,6 @@ class ImportSettingsTask(BaseTask): self.success.emit() -class ProcessMessageTask(BaseTask): - def __init__(self, message: gotify.GotifyMessageModel): - super(ProcessMessageTask, self).__init__() - self.message = message - - def task(self): - if image_url := get_image(self.message.message): - downloader = Downloader() - downloader.get_filename(image_url) - - -class ProcessMessagesTask(BaseTask): - message_processed = pyqtSignal(gotify.GotifyMessageModel) - - def __init__(self, page: gotify.GotifyPagedMessagesModel): - super(ProcessMessagesTask, self).__init__() - self.page = page - - def task(self): - downloader = Downloader() - for message in self.page.messages: - if image_url := get_image(message.message): - downloader.get_filename(image_url) - self.message_processed.emit(message) - - # Prevent locking up the UI when there are a lot of messages with images ready at the same time - time.sleep(0.001) - - class CacheSizeTask(BaseTask): size = pyqtSignal(int) diff --git a/gotify_tray/utils.py b/gotify_tray/utils.py index b087872..e40bd79 100644 --- a/gotify_tray/utils.py +++ b/gotify_tray/utils.py @@ -4,7 +4,10 @@ import re import subprocess from pathlib import Path -from typing import Optional +from typing import Iterator, List, Optional + +from gotify_tray import gotify +from gotify_tray.database import Downloader def verify_server(force_new: bool = False, enable_import: bool = True) -> bool: @@ -28,6 +31,14 @@ def verify_server(force_new: bool = False, enable_import: bool = True) -> bool: return True +def process_messages(messages: List[gotify.GotifyMessageModel]) -> Iterator[gotify.GotifyMessageModel]: + downloader = Downloader() + for message in messages: + if image_url := get_image(message.message): + downloader.get_filename(image_url) + yield message + + def convert_links(text): _link = re.compile( r'(?:(https://|http://)|(www\.))(\S+\b/?)([!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}~]*)(\s|$)',