From 458cd9a0a39ce785018c37acee31ec4673d18527 Mon Sep 17 00:00:00 2001 From: Dmitrii Shcherbakov <12861810+dshcherb@users.noreply.github.com> Date: Sun, 17 May 2026 17:54:15 +0400 Subject: [PATCH 1/4] Open saved screenshot file when notification is clicked On GNOME (Wayland), clicking Flameshot's screenshot notification did nothing. The notification was sent via D-Bus with empty actions and only a KDE-specific x-kde-urls hint, which GNOME Shell ignores. Add a "default" action to the D-Bus notification when a file path is attached (per the freedesktop notification spec, this makes the notification body clickable). When the user clicks it, GNOME Shell emits the ActionInvoked signal, which the new handler receives and calls QDesktopServices::openUrl() to open the file. https://specifications.freedesktop.org/notification/1.3/basic-design.html SystemNotification instances are short-lived stack locals in AbstractLogger, so a persistent singleton (actionHandler) is used to receive the D-Bus signal. The notification ID-to-path mapping is stored in a static map. In the CLI capture flow, delay process exit by 10 seconds when notifications are pending, so the D-Bus handler stays alive long enough to receive the user's click. Platform impact: - macOS/Windows: no change (guarded by platform #if) - Linux X11: minor, notification now includes default action - Linux Wayland/KDE: improvement, click-to-open alongside x-kde-urls - Linux Wayland/GNOME: fixed, click now opens the saved file --- src/main.cpp | 10 ++++-- src/utils/systemnotification.cpp | 54 ++++++++++++++++++++++++++++++-- src/utils/systemnotification.h | 8 +++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index c57601611a..f000e8cefd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,6 +19,7 @@ #include "utils/confighandler.h" #include "utils/filenamehandler.h" #include "utils/pathinfo.h" +#include "utils/systemnotification.h" #include "utils/valuehandler.h" #if !(defined(Q_OS_MACOS) || defined(Q_OS_WIN)) @@ -74,8 +75,13 @@ int requestCaptureAndWait(const CaptureRequest& req) } #else // if this instance is not daemon, make sure it exit after caputre finish - if (FlameshotDaemon::instance() == nullptr && !Flameshot::instance()->haveExternalWidget()) { - qApp->exit(E_OK); + if (FlameshotDaemon::instance() == nullptr && + !Flameshot::instance()->haveExternalWidget()) { + if (SystemNotification::hasPendingPaths()) { + QTimer::singleShot(10000, qApp, &QCoreApplication::quit); + } else { + qApp->exit(E_OK); + } } #endif }); diff --git a/src/utils/systemnotification.cpp b/src/utils/systemnotification.cpp index 04aeadb02a..5ae94ee0c8 100644 --- a/src/utils/systemnotification.cpp +++ b/src/utils/systemnotification.cpp @@ -3,12 +3,14 @@ #include "utils/confighandler.h" #include +#include #include #if !(defined(Q_OS_MACOS) || defined(Q_OS_WIN)) #include #include #include #include +#include #else #include "core/flameshotdaemon.h" #endif @@ -45,6 +47,45 @@ SystemNotification::SystemNotification(QObject* parent) #endif } +QMap SystemNotification::s_pendingPaths; + +SystemNotification* SystemNotification::actionHandler() +{ + static SystemNotification* handler = [] { + auto* h = new SystemNotification(qApp); +#if !(defined(Q_OS_MACOS) || defined(Q_OS_WIN)) + QDBusConnection::sessionBus().connect( + QStringLiteral("org.freedesktop.Notifications"), + QStringLiteral("/org/freedesktop/Notifications"), + QStringLiteral("org.freedesktop.Notifications"), + QStringLiteral("ActionInvoked"), + h, + SLOT(onActionInvoked(uint, QString))); +#endif + return h; + }(); + return handler; +} + +void SystemNotification::onActionInvoked(uint id, const QString& actionKey) +{ + if (actionKey == QLatin1String("default")) { + auto it = s_pendingPaths.find(id); + if (it != s_pendingPaths.end()) { + QDesktopServices::openUrl(QUrl::fromLocalFile(it.value())); + s_pendingPaths.erase(it); + if (s_pendingPaths.isEmpty()) { + qApp->exit(); + } + } + } +} + +bool SystemNotification::hasPendingPaths() +{ + return !s_pendingPaths.isEmpty(); +} + void SystemNotification::sendMessage(const QString& text, const QString& savePath) { @@ -75,11 +116,14 @@ void SystemNotification::sendMessage(const QString& text, if (nullptr != m_interface && m_interface->isValid()) { QList args; QVariantMap hintsMap; + QStringList actions; if (!savePath.isEmpty()) { QUrl fullPath = QUrl::fromLocalFile(savePath); // allows the notification to be dragged and dropped hintsMap[QStringLiteral("x-kde-urls")] = QStringList({ fullPath.toString() }); + // makes the notification body clickable (freedesktop spec) + actions << QStringLiteral("default") << QString(); } args << (qAppName()) // appname @@ -87,11 +131,17 @@ void SystemNotification::sendMessage(const QString& text, << FLAMESHOT_ICON // icon << title // summary << text // body - << QStringList() // actions + << actions // actions << hintsMap // hints << timeout; // timeout - m_interface->callWithArgumentList( + + QDBusReply reply = m_interface->callWithArgumentList( QDBus::AutoDetect, QStringLiteral("Notify"), args); + + if (reply.isValid() && !savePath.isEmpty()) { + actionHandler(); + s_pendingPaths[reply.value()] = savePath; + } } #endif } diff --git a/src/utils/systemnotification.h b/src/utils/systemnotification.h index 51eb65b2db..7b8795a096 100644 --- a/src/utils/systemnotification.h +++ b/src/utils/systemnotification.h @@ -3,6 +3,7 @@ #pragma once +#include #include class QDBusInterface; @@ -20,6 +21,13 @@ class SystemNotification : public QObject const QString& savePath, const int timeout = 5000); + static bool hasPendingPaths(); + private: + Q_SLOT void onActionInvoked(uint id, const QString& actionKey); + + static SystemNotification* actionHandler(); + QDBusInterface* m_interface; + static QMap s_pendingPaths; }; From 4584e38725c54656f27df66baa85c112dcaeb616 Mon Sep 17 00:00:00 2001 From: Dmitrii Shcherbakov <12861810+dshcherb@users.noreply.github.com> Date: Tue, 19 May 2026 17:48:08 +0400 Subject: [PATCH 2/4] Don't kill daemon on notification click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit added qApp->exit() in onActionInvoked to exit the CLI process after the user clicks a notification. But the daemon also takes screenshots (e.g. via tray icon) and sends notifications with the same code path — calling qApp->exit() there kills the daemon. Guard the exit with a static flag (s_exitOnLastAction) that is only set to true in the CLI flow (requestCaptureAndWait). The daemon never sets it, so clicking notifications opened from daemon-triggered screenshots opens the file without terminating the daemon. --- src/main.cpp | 1 + src/utils/systemnotification.cpp | 8 +++++++- src/utils/systemnotification.h | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index f000e8cefd..c9c2a81400 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -78,6 +78,7 @@ int requestCaptureAndWait(const CaptureRequest& req) if (FlameshotDaemon::instance() == nullptr && !Flameshot::instance()->haveExternalWidget()) { if (SystemNotification::hasPendingPaths()) { + SystemNotification::setExitOnLastAction(true); QTimer::singleShot(10000, qApp, &QCoreApplication::quit); } else { qApp->exit(E_OK); diff --git a/src/utils/systemnotification.cpp b/src/utils/systemnotification.cpp index 5ae94ee0c8..65b1dcff44 100644 --- a/src/utils/systemnotification.cpp +++ b/src/utils/systemnotification.cpp @@ -48,6 +48,7 @@ SystemNotification::SystemNotification(QObject* parent) } QMap SystemNotification::s_pendingPaths; +bool SystemNotification::s_exitOnLastAction = false; SystemNotification* SystemNotification::actionHandler() { @@ -74,7 +75,7 @@ void SystemNotification::onActionInvoked(uint id, const QString& actionKey) if (it != s_pendingPaths.end()) { QDesktopServices::openUrl(QUrl::fromLocalFile(it.value())); s_pendingPaths.erase(it); - if (s_pendingPaths.isEmpty()) { + if (s_pendingPaths.isEmpty() && s_exitOnLastAction) { qApp->exit(); } } @@ -86,6 +87,11 @@ bool SystemNotification::hasPendingPaths() return !s_pendingPaths.isEmpty(); } +void SystemNotification::setExitOnLastAction(bool exit) +{ + s_exitOnLastAction = exit; +} + void SystemNotification::sendMessage(const QString& text, const QString& savePath) { diff --git a/src/utils/systemnotification.h b/src/utils/systemnotification.h index 7b8795a096..45691d96ac 100644 --- a/src/utils/systemnotification.h +++ b/src/utils/systemnotification.h @@ -22,6 +22,7 @@ class SystemNotification : public QObject const int timeout = 5000); static bool hasPendingPaths(); + static void setExitOnLastAction(bool exit); private: Q_SLOT void onActionInvoked(uint id, const QString& actionKey); @@ -30,4 +31,5 @@ class SystemNotification : public QObject QDBusInterface* m_interface; static QMap s_pendingPaths; + static bool s_exitOnLastAction; }; From 94c2405e19dc2eebdf9491c3dee98e4c709cde39 Mon Sep 17 00:00:00 2001 From: Dmitrii Shcherbakov <12861810+dshcherb@users.noreply.github.com> Date: Thu, 28 May 2026 17:25:29 +0400 Subject: [PATCH 3/4] Delegate notification handling to daemon The Flatpak D-Bus proxy only routes ActionInvoked signals to the sandbox that created the notification. When the CLI sends the notification and exits, the daemon never receives the click signal. This can be fixed by having the CLI suppress its own notification and sending the save path to the daemon via D-Bus. The daemon sends the notification itself, receives the click, and opens the file. The CLI exits immediately, allowing successive screenshots without a 10-second wait. --- src/core/flameshotdbusadapter.cpp | 16 +++++++++++ src/core/flameshotdbusadapter.h | 2 ++ src/main.cpp | 39 ++++++++++++++++++++++++--- src/utils/screenshotsaver.cpp | 20 +++++++++++--- src/utils/systemnotification.cpp | 36 +++++++++++++++++++++++++ src/utils/systemnotification.h | 7 +++++ src/widgets/capture/capturewidget.cpp | 5 ++-- 7 files changed, 115 insertions(+), 10 deletions(-) diff --git a/src/core/flameshotdbusadapter.cpp b/src/core/flameshotdbusadapter.cpp index 8718705739..e5fbccb145 100644 --- a/src/core/flameshotdbusadapter.cpp +++ b/src/core/flameshotdbusadapter.cpp @@ -4,6 +4,7 @@ #include "flameshotdbusadapter.h" #include "core/flameshot.h" #include "core/flameshotdaemon.h" +#include "utils/systemnotification.h" FlameshotDBusAdapter::FlameshotDBusAdapter(QObject* parent) : QDBusAbstractAdaptor(parent) @@ -31,3 +32,18 @@ void FlameshotDBusAdapter::attachPin(const QByteArray& data) { FlameshotDaemon::instance()->attachPin(data); } + +void FlameshotDBusAdapter::registerNotificationPath(uint id, + const QString& path) +{ + SystemNotification::registerNotificationPath(id, path); +} + +void FlameshotDBusAdapter::showSaveNotification(const QString& path) +{ + SystemNotification sysNotif; + sysNotif.sendMessage( + QObject::tr("Capture saved as ") + path, + QObject::tr("Flameshot Info"), + path); +} diff --git a/src/core/flameshotdbusadapter.h b/src/core/flameshotdbusadapter.h index 3372be0ce4..58c3e7b803 100644 --- a/src/core/flameshotdbusadapter.h +++ b/src/core/flameshotdbusadapter.h @@ -20,4 +20,6 @@ public slots: Q_NOREPLY void attachTextToClipboard(const QString& text, const QString& notification); Q_NOREPLY void attachPin(const QByteArray& data); + Q_NOREPLY void registerNotificationPath(uint id, const QString& path); + Q_NOREPLY void showSaveNotification(const QString& path); }; diff --git a/src/main.cpp b/src/main.cpp index c9c2a81400..cd50c7d32c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,6 +25,7 @@ #if !(defined(Q_OS_MACOS) || defined(Q_OS_WIN)) #include "core/flameshotdbusadapter.h" #include +#include #include #endif @@ -77,9 +78,41 @@ int requestCaptureAndWait(const CaptureRequest& req) // if this instance is not daemon, make sure it exit after caputre finish if (FlameshotDaemon::instance() == nullptr && !Flameshot::instance()->haveExternalWidget()) { - if (SystemNotification::hasPendingPaths()) { - SystemNotification::setExitOnLastAction(true); - QTimer::singleShot(10000, qApp, &QCoreApplication::quit); + auto pendingDaemon = + SystemNotification::takePendingDaemonNotifications(); + if (!pendingDaemon.isEmpty()) { + bool delegated = false; + auto bus = QDBusConnection::sessionBus(); + if (bus.isConnected() && + bus.interface()->isServiceRegistered( + QStringLiteral("org.flameshot.Flameshot"))) { + QDBusMessage msg = QDBusMessage::createMethodCall( + QStringLiteral("org.flameshot.Flameshot"), + QStringLiteral("/"), + QLatin1String(""), + QStringLiteral("showSaveNotification")); + delegated = true; + for (const auto& path : pendingDaemon) { + QDBusMessage m = msg; + m << path; + if (!bus.send(m)) { + delegated = false; + break; + } + } + } + if (delegated) { + qApp->exit(E_OK); + } else { + // Fallback: send notification ourselves and wait + SystemNotification sysNotif; + for (const auto& path : pendingDaemon) { + sysNotif.sendMessage( + QObject::tr("Capture saved as ") + path, path); + } + SystemNotification::setExitOnLastAction(true); + QTimer::singleShot(10000, qApp, &QCoreApplication::quit); + } } else { qApp->exit(E_OK); } diff --git a/src/utils/screenshotsaver.cpp b/src/utils/screenshotsaver.cpp index 29fb9d28ce..a83ef0d16b 100644 --- a/src/utils/screenshotsaver.cpp +++ b/src/utils/screenshotsaver.cpp @@ -8,6 +8,7 @@ #include "utils/desktopinfo.h" #include "utils/filenamehandler.h" #include "utils/globalvalues.h" +#include "utils/systemnotification.h" #include #include @@ -62,8 +63,14 @@ bool saveToFilesystem(const QPixmap& capture, if (okay) { saveMessage += QObject::tr("Capture saved as ") + completePath; - AbstractLogger::info().attachNotificationPath(notificationPath) - << saveMessage; + if (FlameshotDaemon::instance() == nullptr) { + SystemNotification::addPendingDaemonNotification( + notificationPath); + qDebug().noquote() << saveMessage; + } else { + AbstractLogger::info().attachNotificationPath(notificationPath) + << saveMessage; + } } else { saveMessage += QObject::tr("Error trying to save as ") + completePath; @@ -237,7 +244,7 @@ class ClipboardWatcherMimeData : public QMimeData if (m_notified || m_owner.isNull()) return; m_notified = true; - AbstractLogger::info() << QObject::tr("Capture saved to clipboard."); + qDebug() << "Capture saved to clipboard."; QPointer guard = m_owner; QTimer::singleShot(0, [guard]() { if (guard) @@ -317,7 +324,12 @@ bool saveToFilesystemGUI(const QPixmap& capture) ConfigHandler().setSavePath(pathNoFile); QString msg = QObject::tr("Capture saved as ") + savePath; - AbstractLogger().attachNotificationPath(savePath) << msg; + if (FlameshotDaemon::instance() == nullptr) { + SystemNotification::addPendingDaemonNotification(savePath); + qDebug().noquote() << msg; + } else { + AbstractLogger().attachNotificationPath(savePath) << msg; + } if (config.copyPathAfterSave()) { #ifdef Q_OS_WIN diff --git a/src/utils/systemnotification.cpp b/src/utils/systemnotification.cpp index 65b1dcff44..6dd1243200 100644 --- a/src/utils/systemnotification.cpp +++ b/src/utils/systemnotification.cpp @@ -48,6 +48,7 @@ SystemNotification::SystemNotification(QObject* parent) } QMap SystemNotification::s_pendingPaths; +QStringList SystemNotification::s_pendingDaemonNotifications; bool SystemNotification::s_exitOnLastAction = false; SystemNotification* SystemNotification::actionHandler() @@ -92,6 +93,41 @@ void SystemNotification::setExitOnLastAction(bool exit) s_exitOnLastAction = exit; } +void SystemNotification::registerNotificationPath(uint id, + const QString& path) +{ + actionHandler(); + s_pendingPaths[id] = path; +} + +void SystemNotification::registerNotificationPath( + const QMap& paths) +{ + actionHandler(); + for (auto it = paths.cbegin(); it != paths.cend(); ++it) { + s_pendingPaths.insert(it.key(), it.value()); + } +} + +QMap SystemNotification::takePendingPaths() +{ + auto paths = s_pendingPaths; + s_pendingPaths.clear(); + return paths; +} + +void SystemNotification::addPendingDaemonNotification(const QString& path) +{ + s_pendingDaemonNotifications.append(path); +} + +QStringList SystemNotification::takePendingDaemonNotifications() +{ + auto paths = s_pendingDaemonNotifications; + s_pendingDaemonNotifications.clear(); + return paths; +} + void SystemNotification::sendMessage(const QString& text, const QString& savePath) { diff --git a/src/utils/systemnotification.h b/src/utils/systemnotification.h index 45691d96ac..4ab28f7a15 100644 --- a/src/utils/systemnotification.h +++ b/src/utils/systemnotification.h @@ -23,6 +23,12 @@ class SystemNotification : public QObject static bool hasPendingPaths(); static void setExitOnLastAction(bool exit); + static void registerNotificationPath(uint id, const QString& path); + static void registerNotificationPath(const QMap& paths); + static QMap takePendingPaths(); + + static void addPendingDaemonNotification(const QString& path); + static QStringList takePendingDaemonNotifications(); private: Q_SLOT void onActionInvoked(uint id, const QString& actionKey); @@ -31,5 +37,6 @@ class SystemNotification : public QObject QDBusInterface* m_interface; static QMap s_pendingPaths; + static QStringList s_pendingDaemonNotifications; static bool s_exitOnLastAction; }; diff --git a/src/widgets/capture/capturewidget.cpp b/src/widgets/capture/capturewidget.cpp index 25a887a8dc..974b63e4b2 100644 --- a/src/widgets/capture/capturewidget.cpp +++ b/src/widgets/capture/capturewidget.cpp @@ -643,9 +643,8 @@ void CaptureWidget::closeEvent(QCloseEvent* event) event->ignore(); m_clipboardWorkaroundDone = true; m_context.request.removeTask(CaptureRequest::COPY); - AbstractLogger::info() - << "GNOME Wayland detected; keeping capture window alive until " - "clipboard data is fetched."; + qDebug() << "GNOME Wayland detected; keeping capture window alive " + "until clipboard data is fetched."; saveToClipboardGnomeWorkaround(pixmap(), this); return; } From 865ff11c6ce2ac06c45eb19e01d7503489f07e4a Mon Sep 17 00:00:00 2001 From: Dmitrii Shcherbakov <12861810+dshcherb@users.noreply.github.com> Date: Thu, 28 May 2026 23:27:51 +0400 Subject: [PATCH 4/4] Guard D-Bus delegation code with platform preprocessor check The notification delegation code uses QDBusConnection and QDBusMessage which are unavailable on Windows and macOS. Wrap it with the existing platform guard so non-Linux builds fall back to the simple exit path. --- src/main.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index cd50c7d32c..b97e16e1e0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -75,9 +75,9 @@ int requestCaptureAndWait(const CaptureRequest& req) qApp->exit(0); } #else - // if this instance is not daemon, make sure it exit after caputre finish if (FlameshotDaemon::instance() == nullptr && !Flameshot::instance()->haveExternalWidget()) { +#if !(defined(Q_OS_MACOS) || defined(Q_OS_WIN)) auto pendingDaemon = SystemNotification::takePendingDaemonNotifications(); if (!pendingDaemon.isEmpty()) { @@ -116,6 +116,9 @@ int requestCaptureAndWait(const CaptureRequest& req) } else { qApp->exit(E_OK); } +#else + qApp->exit(E_OK); +#endif } #endif });