from PyQt6 import QtCore, QtGui, QtWidgets from ..designs.widget_main import Ui_MainWindow from .StatusWidget import StatusWidget from ..models import ( ApplicationModel, MessagesModel, MessagesModelItem, MessagesProxyModel, ) 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") class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): refresh = QtCore.pyqtSignal() delete_all = QtCore.pyqtSignal(QtGui.QStandardItem) delete_message = QtCore.pyqtSignal(MessagesModelItem) application_selection_changed = QtCore.pyqtSignal(QtGui.QStandardItem) image_popup = QtCore.pyqtSignal(str, QtCore.QPoint) hidden = QtCore.pyqtSignal() activated = QtCore.pyqtSignal() priority_filter_changed = QtCore.pyqtSignal(set) def __init__( self, application_model: ApplicationModel, application_proxy_model: QtCore.QSortFilterProxyModel, messages_model: MessagesModel, messages_proxy_model: MessagesProxyModel, ): super(MainWindow, self).__init__() self.setupUi(self) self.installEventFilter(self) self.setWindowTitle(__title__) self.application_model = application_model self.application_proxy_model = application_proxy_model self.messages_model = messages_model self.messages_proxy_model = messages_proxy_model self.listView_applications.setModel(application_proxy_model) self.listView_messages.setModel(messages_proxy_model) self.messages_proxy_model.rowsInserted.connect(self.display_message_widgets) self.messages_proxy_model.layoutChanged.connect(self.redisplay_message_widgets) # Do not expand the applications listview when resizing self.splitter.setStretchFactor(0, 0) self.splitter.setStretchFactor(1, 1) # Do not collapse the message list self.splitter.setCollapsible(1, False) self.status_widget = StatusWidget() self.horizontalLayout.insertWidget(0, self.status_widget) self.set_icons() font_title = QtGui.QFont() if s := settings.value("MainWindow/font/application", type=str): font_title.fromString(s) else: font_title.setBold(True) font_title.setPointSize(font_title.pointSize() + 2) self.label_application.setFont(font_title) # Set tooltips self.pb_refresh.setToolTip("Refresh") self.pb_delete_all.setToolTip("Delete all messages") self.restore_state() self.link_callbacks() # Setup filters self.pb_low.toggled.connect(self.on_priority_button_toggled) self.pb_normal.toggled.connect(self.on_priority_button_toggled) self.pb_high.toggled.connect(self.on_priority_button_toggled) # Critical is always shown, no toggle # Set styles for priority buttons button_style = "QPushButton { background-color: grey; border: 1px solid black; } QPushButton:checked { background-color: green; }" self.pb_low.setStyleSheet(button_style) self.pb_normal.setStyleSheet(button_style) self.pb_high.setStyleSheet(button_style) # Critical always green self.pb_critical.setStyleSheet( "QPushButton { background-color: green; border: 1px solid black; }" ) self.pb_critical.setChecked(True) self.pb_critical.setCheckable(False) self.pb_remove_filters.clicked.connect(self.on_remove_filters_clicked) self.le_search.returnPressed.connect(self.on_search_return_pressed) # set refresh shortcut (usually ctrl-r) # unfortunately this cannot be done with designer self.pb_refresh.setShortcut( QtGui.QKeySequence(QtGui.QKeySequence.StandardKey.Refresh) ) def set_icons(self): # Set button icons self.pb_refresh.setIcon(QtGui.QIcon(get_theme_file("refresh.svg"))) self.pb_delete_all.setIcon(QtGui.QIcon(get_theme_file("trashcan.svg"))) # Resize the labels and icons size = settings.value("MainWindow/label/size", type=int) self.status_widget.setFixedSize(QtCore.QSize(size, size)) size = settings.value("MainWindow/button/size", type=int) self.pb_refresh.setFixedSize(QtCore.QSize(size, size)) self.pb_delete_all.setFixedSize(QtCore.QSize(size, size)) self.pb_refresh.setIconSize(QtCore.QSize(int(0.7 * size), int(0.7 * size))) self.pb_delete_all.setIconSize(QtCore.QSize(int(0.9 * size), int(0.9 * size))) size = settings.value("MainWindow/application/icon/size", type=int) self.listView_applications.setIconSize(QtCore.QSize(size, size)) # Refresh the status widget self.status_widget.refresh() def set_active(self): self.status_widget.set_active() def set_connecting(self): self.status_widget.set_connecting() def set_inactive(self): self.status_widget.set_inactive() def set_error(self): self.status_widget.set_error() def display_message_widgets( self, parent: QtCore.QModelIndex, first: int, last: int ): for i in range(first, last + 1): if proxy_index := self.messages_proxy_model.index(i, 0, parent): source_index = self.messages_proxy_model.mapToSource(proxy_index) message_item = self.messages_model.itemFromIndex(source_index) message: gotify.GotifyMessageModel = message_item.data( MessageItemDataRole.MessageRole ) application_item = self.application_model.itemFromId(message.appid) message_widget = MessageWidget( 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(proxy_index, message_widget) def redisplay_message_widgets(self): # Clear existing widgets for row in range(self.messages_proxy_model.rowCount()): index = self.messages_proxy_model.index(row, 0) self.listView_messages.setIndexWidget(index, None) # Redisplay for current visible rows self.display_message_widgets( QtCore.QModelIndex(), 0, self.messages_proxy_model.rowCount() - 1 ) def currentApplicationIndex(self) -> QtCore.QModelIndex: return self.listView_applications.selectionModel().currentIndex() def application_selection_changed_callback( self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex ): if item := self.application_model.itemFromIndex( self.application_proxy_model.mapToSource(current) ): self.label_application.setText(item.text()) self.application_selection_changed.emit(item) def delete_all_callback(self): if ( self.messages_proxy_model.rowCount() == 0 or QtWidgets.QMessageBox.warning( self, "Are you sure?", "Delete all messages?", QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, defaultButton=QtWidgets.QMessageBox.StandardButton.Cancel, ) != QtWidgets.QMessageBox.StandardButton.Ok ): return index = self.currentApplicationIndex() if item := self.application_model.itemFromIndex( self.application_proxy_model.mapToSource(index) ): self.delete_all.emit(item) def disable_applications(self): self.listView_applications.clearSelection() self.listView_applications.setDisabled(True) def enable_applications(self): self.listView_applications.setEnabled(True) self.listView_applications.setCurrentIndex( self.application_proxy_model.index(0, 0) ) def disable_buttons(self): self.pb_delete_all.setDisabled(True) self.pb_refresh.setDisabled(True) def enable_buttons(self): self.pb_delete_all.setEnabled(True) self.pb_refresh.setEnabled(True) def bring_to_front(self): self.ensurePolished() self.setWindowState( self.windowState() & ~QtCore.Qt.WindowState.WindowMinimized | QtCore.Qt.WindowState.WindowActive ) self.show() self.activateWindow() # pops on current desktop if window was visible on another desktop self.raise_() def link_callbacks(self): self.pb_refresh.clicked.connect(self.refresh.emit) self.pb_delete_all.clicked.connect(self.delete_all_callback) self.listView_applications.selectionModel().currentChanged.connect( self.application_selection_changed_callback ) def store_state(self): settings.setValue("MainWindow/geometry", self.saveGeometry()) settings.setValue("MainWindow/state", self.saveState()) settings.setValue("MainWindow/splitter", self.splitter.saveState()) def restore_state(self): if geometry := settings.value("MainWindow/geometry", type=QtCore.QByteArray): self.restoreGeometry(geometry) if state := settings.value("MainWindow/state", type=QtCore.QByteArray): self.restoreState(state) if splitter := settings.value("MainWindow/splitter", type=QtCore.QByteArray): self.splitter.restoreState(splitter) def closeEvent(self, e: QtGui.QCloseEvent) -> None: self.hide() e.ignore() self.hidden.emit() def eventFilter(self, object: QtCore.QObject, event: QtCore.QEvent) -> bool: if event.type() == QtCore.QEvent.Type.WindowActivate: self.activated.emit() return super().eventFilter(object, event) def on_priority_button_toggled(self, checked): priorities = {10} # Critical always included if self.pb_low.isChecked(): priorities.update({0, 1, 2, 3}) if self.pb_normal.isChecked(): priorities.update({4, 5, 6, 7, 8}) if self.pb_high.isChecked(): priorities.add(9) self.priority_filter_changed.emit(priorities) def on_remove_filters_clicked(self): # Reset priority buttons self.pb_low.setChecked(True) self.pb_normal.setChecked(True) self.pb_high.setChecked(True) # Critical is always on # Clear search self.le_search.clear() self.messages_proxy_model.set_text_filter("") def on_search_return_pressed(self): text = self.le_search.text() self.messages_proxy_model.set_text_filter(text)