diff --git a/gotify_tray/database/default_settings.py b/gotify_tray/database/default_settings.py index db8d6dc..2c8b65a 100644 --- a/gotify_tray/database/default_settings.py +++ b/gotify_tray/database/default_settings.py @@ -20,4 +20,8 @@ DEFAULT_SETTINGS = { "MainWindow/label/size": 25, "MainWindow/button/size": 33, "MainWindow/application/icon/size": 40, + "ImagePopup/enabled": False, + "ImagePopup/extensions": [".jpg", ".jpeg", ".png", ".svg"], + "ImagePopup/w": 400, + "ImagePopup/h": 400, } diff --git a/gotify_tray/gui/MainApplication.py b/gotify_tray/gui/MainApplication.py index 1d16eb5..d5651b6 100644 --- a/gotify_tray/gui/MainApplication.py +++ b/gotify_tray/gui/MainApplication.py @@ -31,7 +31,7 @@ from .models import ( MessagesModelItem, MessageItemDataRole, ) -from .widgets import MainWindow, SettingsDialog, Tray +from .widgets import ImagePopup, MainWindow, SettingsDialog, Tray settings = Settings("gotify-tray") @@ -309,6 +309,17 @@ class MainApplication(QtWidgets.QApplication): self.messages_model.clear() + def image_popup_callback(self, link: str, pos: QtCore.QPoint): + if filename := self.downloader.get_filename(link): + self.image_popup = ImagePopup(filename, pos, link) + self.image_popup.show() + else: + logger.warning(f"Image {link} is not in the cache") + + def main_window_hidden_callback(self): + if image_popup := getattr(self, "image_popup", None): + image_popup.close() + def refresh_callback(self): # Manual refresh -> also reset the image cache Cache().clear() @@ -362,6 +373,8 @@ class MainApplication(QtWidgets.QApplication): self.application_selection_changed_callback ) self.main_window.delete_message.connect(self.delete_message_callback) + self.main_window.image_popup.connect(self.image_popup_callback) + self.main_window.hidden.connect(self.main_window_hidden_callback) self.watchdog.closed.connect(lambda: self.listener_closed_callback(None, None)) diff --git a/gotify_tray/gui/designs/widget_settings.py b/gotify_tray/gui/designs/widget_settings.py index 530e3e3..fe146ac 100644 --- a/gotify_tray/gui/designs/widget_settings.py +++ b/gotify_tray/gui/designs/widget_settings.py @@ -105,37 +105,66 @@ class Ui_Dialog(object): self.verticalLayout.setObjectName("verticalLayout") self.groupBox = QtWidgets.QGroupBox(self.tab_advanced) self.groupBox.setObjectName("groupBox") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.pb_export = QtWidgets.QPushButton(self.groupBox) - self.pb_export.setObjectName("pb_export") - self.verticalLayout_2.addWidget(self.pb_export) - self.pb_import = QtWidgets.QPushButton(self.groupBox) - self.pb_import.setObjectName("pb_import") - self.verticalLayout_2.addWidget(self.pb_import) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.groupBox) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.pb_reset = QtWidgets.QPushButton(self.groupBox) self.pb_reset.setObjectName("pb_reset") - self.verticalLayout_2.addWidget(self.pb_reset) + self.horizontalLayout_2.addWidget(self.pb_reset) + self.pb_import = QtWidgets.QPushButton(self.groupBox) + self.pb_import.setObjectName("pb_import") + self.horizontalLayout_2.addWidget(self.pb_import) + self.pb_export = QtWidgets.QPushButton(self.groupBox) + self.pb_export.setObjectName("pb_export") + self.horizontalLayout_2.addWidget(self.pb_export) self.verticalLayout.addWidget(self.groupBox) + self.groupbox_image_popup = QtWidgets.QGroupBox(self.tab_advanced) + self.groupbox_image_popup.setCheckable(True) + self.groupbox_image_popup.setObjectName("groupbox_image_popup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.groupbox_image_popup) + self.gridLayout_2.setObjectName("gridLayout_2") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.label = QtWidgets.QLabel(self.groupbox_image_popup) + self.label.setObjectName("label") + self.horizontalLayout_4.addWidget(self.label) + self.spin_popup_w = QtWidgets.QSpinBox(self.groupbox_image_popup) + self.spin_popup_w.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.spin_popup_w.setMinimum(100) + self.spin_popup_w.setMaximum(10000) + self.spin_popup_w.setObjectName("spin_popup_w") + self.horizontalLayout_4.addWidget(self.spin_popup_w) + self.label_2 = QtWidgets.QLabel(self.groupbox_image_popup) + self.label_2.setObjectName("label_2") + self.horizontalLayout_4.addWidget(self.label_2) + self.spin_popup_h = QtWidgets.QSpinBox(self.groupbox_image_popup) + self.spin_popup_h.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.spin_popup_h.setMinimum(100) + self.spin_popup_h.setMaximum(10000) + self.spin_popup_h.setObjectName("spin_popup_h") + self.horizontalLayout_4.addWidget(self.spin_popup_h) + spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_4.addItem(spacerItem4) + self.gridLayout_2.addLayout(self.horizontalLayout_4, 0, 0, 1, 1) + self.verticalLayout.addWidget(self.groupbox_image_popup) self.groupBox_logging = QtWidgets.QGroupBox(self.tab_advanced) self.groupBox_logging.setObjectName("groupBox_logging") self.gridLayout_6 = QtWidgets.QGridLayout(self.groupBox_logging) self.gridLayout_6.setObjectName("gridLayout_6") - self.label_logging = QtWidgets.QLabel(self.groupBox_logging) - self.label_logging.setObjectName("label_logging") - self.gridLayout_6.addWidget(self.label_logging, 0, 0, 1, 1) self.combo_logging = QtWidgets.QComboBox(self.groupBox_logging) self.combo_logging.setObjectName("combo_logging") self.gridLayout_6.addWidget(self.combo_logging, 0, 1, 1, 1) + spacerItem5 = QtWidgets.QSpacerItem(190, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.gridLayout_6.addItem(spacerItem5, 0, 3, 1, 1) self.pb_open_log = QtWidgets.QPushButton(self.groupBox_logging) self.pb_open_log.setMaximumSize(QtCore.QSize(30, 16777215)) self.pb_open_log.setObjectName("pb_open_log") self.gridLayout_6.addWidget(self.pb_open_log, 0, 2, 1, 1) - spacerItem4 = QtWidgets.QSpacerItem(190, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.gridLayout_6.addItem(spacerItem4, 0, 3, 1, 1) + self.label_logging = QtWidgets.QLabel(self.groupBox_logging) + self.label_logging.setObjectName("label_logging") + self.gridLayout_6.addWidget(self.label_logging, 0, 0, 1, 1) self.verticalLayout.addWidget(self.groupBox_logging) - spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout.addItem(spacerItem5) + spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout.addItem(spacerItem6) self.tabWidget.addTab(self.tab_advanced, "") self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1) @@ -170,13 +199,20 @@ class Ui_Dialog(object): self.pb_font_message_content.setText(_translate("Dialog", "Message")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_fonts), _translate("Dialog", "Fonts")) self.groupBox.setTitle(_translate("Dialog", "Settings")) - self.pb_export.setText(_translate("Dialog", "Export")) - self.pb_import.setText(_translate("Dialog", "Import")) self.pb_reset.setText(_translate("Dialog", "Reset")) + self.pb_import.setText(_translate("Dialog", "Import")) + self.pb_export.setText(_translate("Dialog", "Export")) + self.groupbox_image_popup.setTitle(_translate("Dialog", "Image pop-up for URLs")) + self.label.setToolTip(_translate("Dialog", "Maximum pop-up width")) + self.label.setText(_translate("Dialog", "Width")) + self.spin_popup_w.setToolTip(_translate("Dialog", "Maximum pop-up width")) + self.label_2.setToolTip(_translate("Dialog", "Maximum pop-up height")) + self.label_2.setText(_translate("Dialog", "Height")) + self.spin_popup_h.setToolTip(_translate("Dialog", "Maximum pop-up height")) self.groupBox_logging.setTitle(_translate("Dialog", "Logging")) - self.label_logging.setText(_translate("Dialog", "Level")) self.pb_open_log.setToolTip(_translate("Dialog", "Open logfile")) self.pb_open_log.setText(_translate("Dialog", "...")) + self.label_logging.setText(_translate("Dialog", "Level")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_advanced), _translate("Dialog", "Advanced")) diff --git a/gotify_tray/gui/designs/widget_settings.ui b/gotify_tray/gui/designs/widget_settings.ui index 3abaad3..2b32ffa 100644 --- a/gotify_tray/gui/designs/widget_settings.ui +++ b/gotify_tray/gui/designs/widget_settings.ui @@ -240,11 +240,11 @@ Settings - + - + - Export + Reset @@ -256,31 +256,118 @@ - + - Reset + Export + + + + Image pop-up for URLs + + + true + + + + + + + + Maximum pop-up width + + + Width + + + + + + + Maximum pop-up width + + + Qt::AlignCenter + + + 100 + + + 10000 + + + + + + + Maximum pop-up height + + + Height + + + + + + + Maximum pop-up height + + + Qt::AlignCenter + + + 100 + + + 10000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Logging - - - - Level - - - + + + + Qt::Horizontal + + + + 190 + 20 + + + + @@ -297,18 +384,12 @@ - - - - Qt::Horizontal + + + + Level - - - 190 - 20 - - - + diff --git a/gotify_tray/gui/widgets/ImagePopup.py b/gotify_tray/gui/widgets/ImagePopup.py new file mode 100644 index 0000000..8850c6d --- /dev/null +++ b/gotify_tray/gui/widgets/ImagePopup.py @@ -0,0 +1,65 @@ +import platform +from PyQt6 import QtCore, QtGui, QtWidgets + +from gotify_tray.database import Settings + + +settings = Settings("gotify-tray") + + +class ImagePopup(QtWidgets.QLabel): + def __init__(self, filename: str, pos: QtCore.QPoint, link: str = None): + """Create and show a pop-up image under the cursor + + Args: + filename (str): The path to the image to display + pos (QtCore.QPoint): The location at which the image should be displayed + link (str, optional): The URL of the image. Defaults to None. + """ + super(ImagePopup, self).__init__() + self.link = link + + self.setWindowFlags(QtCore.Qt.WindowType.Popup) + self.installEventFilter(self) + + # Prevent leaving the pop-up open when moving quickly out of the widget + self.popup_timer = QtCore.QTimer() + self.popup_timer.timeout.connect(self.check_mouse) + + pixmap = QtGui.QPixmap(filename) + W = settings.value("ImagePopup/w", type=int) + H = settings.value("ImagePopup/h", type=int) + if pixmap.height() > H or pixmap.width() > W: + pixmap = pixmap.scaled( + W, + H, + aspectRatioMode=QtCore.Qt.AspectRatioMode.KeepAspectRatio, + transformMode=QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self.setPixmap(pixmap) + + self.move(pos - QtCore.QPoint(15, 15)) + + self.popup_timer.start(500) + + def check_mouse(self): + if not self.underMouse(): + self.close() + + def close(self): + self.popup_timer.stop() + super(ImagePopup, self).close() + + def eventFilter(self, object: QtCore.QObject, event: QtCore.QEvent) -> bool: + if platform.system() != "Darwin" and event.type() == QtCore.QEvent.Type.Leave: + # Close the pop-up on mouse leave + self.close() + elif ( + event.type() == QtCore.QEvent.Type.MouseButtonPress + and event.button() == QtCore.Qt.MouseButton.LeftButton + and self.link + ): + # Open the image URL on left click + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.link)) + + return super().eventFilter(object, event) diff --git a/gotify_tray/gui/widgets/MainWindow.py b/gotify_tray/gui/widgets/MainWindow.py index 21faa98..26ed8b3 100644 --- a/gotify_tray/gui/widgets/MainWindow.py +++ b/gotify_tray/gui/widgets/MainWindow.py @@ -21,6 +21,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): 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() def __init__( self, application_model: ApplicationModel, messages_model: MessagesModel @@ -103,6 +105,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): 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 currentApplicationIndex(self) -> QtCore.QModelIndex: return self.listView_applications.selectionModel().currentIndex() @@ -173,3 +176,4 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): def closeEvent(self, e: QtGui.QCloseEvent) -> None: self.hide() + self.hidden.emit() diff --git a/gotify_tray/gui/widgets/MessageWidget.py b/gotify_tray/gui/widgets/MessageWidget.py index db1cde6..3aab648 100644 --- a/gotify_tray/gui/widgets/MessageWidget.py +++ b/gotify_tray/gui/widgets/MessageWidget.py @@ -1,3 +1,5 @@ +import os + from PyQt6 import QtCore, QtGui, QtWidgets from ..models.MessagesModel import MessageItemDataRole, MessagesModelItem @@ -11,6 +13,7 @@ settings = Settings("gotify-tray") class MessageWidget(QtWidgets.QWidget, Ui_Form): deletion_requested = QtCore.pyqtSignal(MessagesModelItem) + image_popup = QtCore.pyqtSignal(str, QtCore.QPoint) def __init__(self, message_item: MessagesModelItem, image_path: str = ""): super(MessageWidget, self).__init__() @@ -86,7 +89,17 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form): self.label_date.setFont(font_date) self.label_message.setFont(font_content) + def link_hovered_callback(self, link: str): + if not settings.value("ImagePopup/enabled", type=bool): + return + + qurl = QtCore.QUrl(link) + _, ext = os.path.splitext(qurl.fileName()) + if ext in settings.value("ImagePopup/extensions", type=list): + self.image_popup.emit(link, QtGui.QCursor.pos()) + def link_callbacks(self): self.pb_delete.clicked.connect( lambda: self.deletion_requested.emit(self.message_item) ) + self.label_message.linkHovered.connect(self.link_hovered_callback) diff --git a/gotify_tray/gui/widgets/SettingsDialog.py b/gotify_tray/gui/widgets/SettingsDialog.py index 45fb368..90ee6aa 100644 --- a/gotify_tray/gui/widgets/SettingsDialog.py +++ b/gotify_tray/gui/widgets/SettingsDialog.py @@ -86,6 +86,11 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): ) self.layout_fonts_message.addWidget(self.message_widget) + # Advanced + self.groupbox_image_popup.setChecked(settings.value("ImagePopup/enabled", type=bool)) + self.spin_popup_w.setValue(settings.value("ImagePopup/w", type=int)) + self.spin_popup_h.setValue(settings.value("ImagePopup/h", type=int)) + def change_server_info_callback(self): self.server_changed = verify_server(force_new=True, enable_import=False) @@ -179,6 +184,9 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): self.pb_export.clicked.connect(self.export_callback) self.pb_import.clicked.connect(self.import_callback) self.pb_reset.clicked.connect(self.reset_callback) + self.groupbox_image_popup.toggled.connect(self.settings_changed_callback) + self.spin_popup_w.valueChanged.connect(self.settings_changed_callback) + self.spin_popup_h.valueChanged.connect(self.settings_changed_callback) def apply_settings(self): # Priority @@ -211,6 +219,11 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): self.message_widget.label_message.font().toString(), ) + # Advanced + settings.setValue("ImagePopup/enabled", self.groupbox_image_popup.isChecked()) + settings.setValue("ImagePopup/w", self.spin_popup_w.value()) + settings.setValue("ImagePopup/h", self.spin_popup_h.value()) + self.settings_changed = False self.buttonBox.button( QtWidgets.QDialogButtonBox.StandardButton.Apply diff --git a/gotify_tray/gui/widgets/__init__.py b/gotify_tray/gui/widgets/__init__.py index d43f65f..2f9952f 100644 --- a/gotify_tray/gui/widgets/__init__.py +++ b/gotify_tray/gui/widgets/__init__.py @@ -1,3 +1,4 @@ +from .ImagePopup import ImagePopup from .MessageWidget import MessageWidget from .MainWindow import MainWindow from .ServerInfoDialog import ServerInfoDialog