From d176fe37c2b7c55bc76384d04d174ad4ea8a960a Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Thu, 7 May 2026 08:52:35 +0200 Subject: [PATCH 01/15] Import adapted QtTrayMenu class from upstream --- CMakeLists.txt | 5 +- src/QtTrayMenu.cpp | 152 +++++++++++++++++++++++++++++++++++++++++++++ src/QtTrayMenu.h | 42 +++++++++++++ src/tray.h | 1 + 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 src/QtTrayMenu.cpp create mode 100644 src/QtTrayMenu.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c7a2b4f..212f83e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,7 +87,10 @@ else() CACHE INTERNAL "Qt major version selected by tray" ) set(CMAKE_AUTOMOC ON) - list(APPEND TRAY_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/tray_linux.cpp") + list(APPEND TRAY_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/src/tray_linux.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/QtTrayMenu.cpp" + ) endif() endif() endif() diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp new file mode 100644 index 0000000..59fe554 --- /dev/null +++ b/src/QtTrayMenu.cpp @@ -0,0 +1,152 @@ +#include "QtTrayMenu.h" + +#include + +int argc = 1; +char *argvArray[] = {(char *) "TrayMenuApp", nullptr}; +bool traydebug = false; + +QtTrayMenu::QtTrayMenu(): + trayIcon(nullptr), + trayStruct(nullptr), + continueRunning(true), + app(nullptr) { + if (QApplication::instance()) { + app = dynamic_cast(QApplication::instance()); + if (!app) { + fprintf(stderr, "QCoreApplication is not a QApplication, please contact support."); + } + } else { + app = new QApplication(argc, &argvArray[0]); + } + if (traydebug) { + app->installEventFilter(this); + } +} + +QtTrayMenu::~QtTrayMenu() { + delete trayIcon; + trayIcon = nullptr; // Set to nullptr after deletion + + // Delete app only if it was created within this class + if (app && app != QApplication::instance()) { + delete app; + app = nullptr; // Set to nullptr after deletion + } +} + +int QtTrayMenu::init(struct tray *tray) { + if (trayIcon) { + return -1; // Already initialized + } + + this->trayStruct = tray; + + if (app->applicationName().isEmpty() || app->applicationName() == "TrayMenuApp") { + app->setApplicationName(tray->tooltip); + } + + trayIcon = new QSystemTrayIcon(QIcon(tray->icon)); + trayIcon->setToolTip(QString::fromUtf8(tray->tooltip)); + + connect(trayIcon, &QSystemTrayIcon::activated, this, &QtTrayMenu::onTrayActivated); + connect(this, &QtTrayMenu::exitRequested, this, &QtTrayMenu::onExitRequested); + + auto *menu = new QMenu; + createMenu(tray->menu, menu); + + trayIcon->setContextMenu(menu); + trayIcon->show(); + + return 0; +} + +void QtTrayMenu::update(struct tray *tray) { + this->trayStruct = tray; + if (trayIcon) { + auto newIcon = QIcon(tray->icon); + if (!newIcon.isNull()) { + trayIcon->setIcon(newIcon); + } + trayIcon->setToolTip(QString::fromUtf8(tray->tooltip)); + } + + auto *existingMenu = trayIcon->contextMenu(); + if (existingMenu) { + existingMenu->clear(); // Remove all actions + createMenu(tray->menu, existingMenu); + } +} + +int QtTrayMenu::loop(int blocking) { + if (!continueRunning) { + return -1; + } + if (!app || app->closingDown()) { + printf("Application is not in a valid state or is closing down.\n"); + return -1; + } + if (blocking) { + app->exec(); + return -1; + } else { + app->processEvents(); + return 0; + } +} + +void QtTrayMenu::exit() { + continueRunning = false; + emit exitRequested(); +} + +void QtTrayMenu::createMenu(struct tray_menu *items, QMenu *menu) { + auto separator = "-"; + while (items && items->text) { + if (std::strcmp(items->text, separator) == 0) { + menu->addSeparator(); + } else { + auto *action = new QAction(QString::fromUtf8(items->text), menu); + action->setDisabled(items->disabled == 1); + action->setCheckable(items->checkbox == 1); + action->setChecked(items->checked == 1); + action->setProperty("tray_menu_item", QVariant::fromValue((void *) items)); + connect(action, &QAction::triggered, this, &QtTrayMenu::onMenuItemTriggered); + if (items->submenu) { + auto submenu = new QMenu; + createMenu(items->submenu, submenu); + action->setMenu(submenu); + } + menu->addAction(action); + } + items++; + } +} + +bool QtTrayMenu::eventFilter(QObject *watched, QEvent *event) { + qDebug() << "Event Type:" << event->type(); + return QObject::eventFilter(watched, event); +} + +void QtTrayMenu::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { + if (reason == QSystemTrayIcon::Trigger && trayStruct->cb) { + trayStruct->cb(trayStruct); + } +} + +void QtTrayMenu::onMenuItemTriggered() { + auto *action = qobject_cast(sender()); + struct tray_menu *menuItem = getTrayMenuItem(action); + + if (menuItem && menuItem->cb) { + menuItem->cb(menuItem); + } +} + +struct tray_menu *QtTrayMenu::getTrayMenuItem(QAction *action) { + return (struct tray_menu *) action->property("tray_menu_item").value(); +} + +void QtTrayMenu::onExitRequested() { + app->quit(); +} diff --git a/src/QtTrayMenu.h b/src/QtTrayMenu.h new file mode 100644 index 0000000..729c0a4 --- /dev/null +++ b/src/QtTrayMenu.h @@ -0,0 +1,42 @@ +#ifndef TRAYMENU_H +#define TRAYMENU_H + +#include "tray.h" + +#include +#include +#include + +class QtTrayMenu : public QObject + { + Q_OBJECT + + public: + QtTrayMenu(); + ~QtTrayMenu(); + virtual bool eventFilter(QObject *watched, QEvent *event) override; + int init(struct tray *tray); + void update(struct tray *tray); + int loop(int blocking); + void exit(); + + private: + void createMenu(struct tray_menu *items, QMenu *menu); + void onTrayActivated(QSystemTrayIcon::ActivationReason reason); + void onMenuItemTriggered(); + QApplication *app; + QSystemTrayIcon *trayIcon; + struct tray *trayStruct; + bool continueRunning; + struct tray_menu *getTrayMenuItem(QAction *action); + + signals: + void exitRequested(); + + private slots: + void onExitRequested(); + +}; + + +#endif // TRAYMENU_H diff --git a/src/tray.h b/src/tray.h index 513ffc3..abbe7ea 100644 --- a/src/tray.h +++ b/src/tray.h @@ -28,6 +28,7 @@ extern "C" { const char *notification_text; ///< Text to display in the notification. const char *notification_title; ///< Title to display in the notification. void (*notification_cb)(); ///< Callback to invoke when the notification is clicked. + void (*cb)(struct tray *); ///< Callback for left click, leave null to just open menu struct tray_menu *menu; ///< Menu items. const int iconPathCount; ///< Number of icon paths. const char *allIconPaths[]; ///< Array of icon paths. From e8a82bbfe181fb2f9157dc6b0e3d29ae99da7c4f Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Thu, 7 May 2026 14:48:31 +0200 Subject: [PATCH 02/15] Integrate upstream QtTrayMenu.h/cpp into tray_linux.cpp --- src/QtTrayMenu.cpp | 187 +++++++-- src/QtTrayMenu.h | 73 ++-- src/example.c | 3 + src/tray.h | 2 +- src/tray_linux.cpp | 959 ++------------------------------------------- 5 files changed, 219 insertions(+), 1005 deletions(-) diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp index 59fe554..a535fe1 100644 --- a/src/QtTrayMenu.cpp +++ b/src/QtTrayMenu.cpp @@ -1,96 +1,115 @@ #include "QtTrayMenu.h" #include +#include +#include -int argc = 1; -char *argvArray[] = {(char *) "TrayMenuApp", nullptr}; -bool traydebug = false; +QtTrayMenu::QtTrayMenu(QObject *parent): + QtTrayMenu(-1, nullptr, false, parent) { + }; -QtTrayMenu::QtTrayMenu(): +QtTrayMenu::QtTrayMenu(int argc, char **argv, const bool debug, QObject *parent): + QObject(parent), + app(nullptr), trayIcon(nullptr), trayStruct(nullptr), - continueRunning(true), - app(nullptr) { + continueRunning(true) { if (QApplication::instance()) { app = dynamic_cast(QApplication::instance()); if (!app) { - fprintf(stderr, "QCoreApplication is not a QApplication, please contact support."); + qDebug() << "QCoreApplication is not a QApplication, please contact support."; } } else { - app = new QApplication(argc, &argvArray[0]); + // Note: The following is ugly but QApplication requires an argv containing the application name. + // We might not have access to the real argc/argv here due to being called/pulled as a dependency. + if (argc < 0 && argv == nullptr) { + argc = 1; + char *argvArray[] = {(char *) "TrayMenuApp", nullptr}; + argv = &argvArray[0]; + } + app = new QApplication(argc, argv); // NOSONAR(cpp:S5025) - Qt has its own integrated memory management } - if (traydebug) { + if (debug) { app->installEventFilter(this); } } QtTrayMenu::~QtTrayMenu() { - delete trayIcon; - trayIcon = nullptr; // Set to nullptr after deletion - // Delete app only if it was created within this class if (app && app != QApplication::instance()) { - delete app; + delete app; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management app = nullptr; // Set to nullptr after deletion } + + // Remove custom references in correct order after app deletion to prevent SEGV + delete trayIcon; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management + trayIcon = nullptr; // Set to nullptr after deletion + + delete trayTopMenu; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management + trayTopMenu = nullptr; // Set to nullptr after deletion } int QtTrayMenu::init(struct tray *tray) { if (trayIcon) { - return -1; // Already initialized + // Running tray is initialized again. Fail with error. + return -1; } this->trayStruct = tray; - if (app->applicationName().isEmpty() || app->applicationName() == "TrayMenuApp") { - app->setApplicationName(tray->tooltip); + if (QApplication::applicationName().isEmpty() || QApplication::applicationName() == "TrayMenuApp") { + QApplication::setApplicationName(tray->tooltip); } - trayIcon = new QSystemTrayIcon(QIcon(tray->icon)); + trayIcon = new QSystemTrayIcon(QIcon(tray->icon), this); trayIcon->setToolTip(QString::fromUtf8(tray->tooltip)); - connect(trayIcon, &QSystemTrayIcon::activated, this, &QtTrayMenu::onTrayActivated); connect(this, &QtTrayMenu::exitRequested, this, &QtTrayMenu::onExitRequested); + connect(trayIcon, &QSystemTrayIcon::activated, this, &QtTrayMenu::onTrayActivated); + connect(trayIcon, &QSystemTrayIcon::messageClicked, this, &QtTrayMenu::onMessageClicked); - auto *menu = new QMenu; - createMenu(tray->menu, menu); + trayTopMenu = new QMenu(); // NOSONAR(cpp:S5025) - Qt has its own integrated memory management + createMenu(tray->menu, trayTopMenu); - trayIcon->setContextMenu(menu); + trayIcon->setContextMenu(trayTopMenu); trayIcon->show(); + createNotification(); + return 0; } void QtTrayMenu::update(struct tray *tray) { this->trayStruct = tray; if (trayIcon) { - auto newIcon = QIcon(tray->icon); - if (!newIcon.isNull()) { + if (const auto newIcon = QIcon(tray->icon); !newIcon.isNull()) { trayIcon->setIcon(newIcon); } trayIcon->setToolTip(QString::fromUtf8(tray->tooltip)); } - - auto *existingMenu = trayIcon->contextMenu(); - if (existingMenu) { + if (trayIcon == nullptr) { + return; + } + if (auto *existingMenu = trayIcon->contextMenu()) { existingMenu->clear(); // Remove all actions createMenu(tray->menu, existingMenu); } + createNotification(); } -int QtTrayMenu::loop(int blocking) { +int QtTrayMenu::loop(int blocking) const { if (!continueRunning) { return -1; } - if (!app || app->closingDown()) { - printf("Application is not in a valid state or is closing down.\n"); + if (!app || QApplication::closingDown()) { + qDebug() << "Application is not in a valid state or is closing down."; return -1; } if (blocking) { - app->exec(); + QApplication::exec(); return -1; } else { - app->processEvents(); + QApplication::processEvents(); return 0; } } @@ -101,9 +120,8 @@ void QtTrayMenu::exit() { } void QtTrayMenu::createMenu(struct tray_menu *items, QMenu *menu) { - auto separator = "-"; while (items && items->text) { - if (std::strcmp(items->text, separator) == 0) { + if (strcmp(items->text, "-") == 0) { menu->addSeparator(); } else { auto *action = new QAction(QString::fromUtf8(items->text), menu); @@ -113,7 +131,7 @@ void QtTrayMenu::createMenu(struct tray_menu *items, QMenu *menu) { action->setProperty("tray_menu_item", QVariant::fromValue((void *) items)); connect(action, &QAction::triggered, this, &QtTrayMenu::onMenuItemTriggered); if (items->submenu) { - auto submenu = new QMenu; + const auto submenu = new QMenu(menu); createMenu(items->submenu, submenu); action->setMenu(submenu); } @@ -123,14 +141,31 @@ void QtTrayMenu::createMenu(struct tray_menu *items, QMenu *menu) { } } +void QtTrayMenu::createNotification() const { + if (trayStruct->notification_title && trayStruct->notification_text) { + const auto title = QString::fromUtf8(trayStruct->notification_title); + const auto text = QString::fromUtf8(trayStruct->notification_text); + if (trayStruct->notification_icon) { + showMessage(title, text, QIcon(trayStruct->notification_icon)); + } else { + showMessage(title, text); + } + } +} + bool QtTrayMenu::eventFilter(QObject *watched, QEvent *event) { qDebug() << "Event Type:" << event->type(); return QObject::eventFilter(watched, event); } void QtTrayMenu::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { - if (reason == QSystemTrayIcon::Trigger && trayStruct->cb) { + if (reason != QSystemTrayIcon::Trigger) { + return; + } + if (trayStruct->cb) { trayStruct->cb(trayStruct); + } else { + showMenu(); } } @@ -143,10 +178,84 @@ void QtTrayMenu::onMenuItemTriggered() { } } -struct tray_menu *QtTrayMenu::getTrayMenuItem(QAction *action) { - return (struct tray_menu *) action->property("tray_menu_item").value(); +struct tray_menu *QtTrayMenu::getTrayMenuItem(QAction *action) { // NOSONAR(cpp:S995) - Use as defined in function interface + return static_cast(action->property("tray_menu_item").value()); +} + +void QtTrayMenu::onExitRequested() const { + QApplication::quit(); +} + +void QtTrayMenu::onMessageClicked() const { + if (trayStruct->notification_cb) { + trayStruct->notification_cb(); + } +} + +void QtTrayMenu::configureAppMetadata(const QString &appName, const QString &appDisplayName, const QString &desktopName) const { + const QString effective_name = !appName.isEmpty() ? appName : QStringLiteral("tray"); + if (QApplication::applicationName().isEmpty()) { + QApplication::setApplicationName(effective_name); + } + + if (QApplication::applicationDisplayName().isEmpty()) { + if (!appDisplayName.isEmpty()) { + QApplication::setApplicationDisplayName(appDisplayName); + } else { + const QString display_name = + (trayStruct != nullptr && trayStruct->tooltip != nullptr) ? QString::fromUtf8(trayStruct->tooltip) : effective_name; + QApplication::setApplicationDisplayName(display_name); + } + } + + if (!QApplication::desktopFileName().isEmpty()) { + return; + } + + if (!desktopName.isEmpty()) { + QApplication::setDesktopFileName(desktopName); + return; + } + + QString desktop_name = QApplication::applicationName(); + if (!desktop_name.endsWith(QStringLiteral(".desktop"))) { + desktop_name += QStringLiteral(".desktop"); + } + QApplication::setDesktopFileName(desktop_name); +} + +void QtTrayMenu::showMenu() const { + if (QMenu *menu = trayIcon->contextMenu(); menu != nullptr) { + // Due to QTBUG-139921 this is currently not working on Linux/Wayland + // with Qt-6.9+ unless menu has a transient parent (which we do not have here). + menu->show(); + } +} + +void QtTrayMenu::showMessage(const QString &title, const QString &msg, const QSystemTrayIcon::MessageIcon icon, const int msecs) const { + trayIcon->showMessage(title, msg, icon, msecs); +} + +void QtTrayMenu::showMessage(const QString &title, const QString &msg, const QIcon &icon, const int msecs) const { + trayIcon->showMessage(title, msg, icon, msecs); +} + +void QtTrayMenu::clickMenuItem(int index) const { + const QMenu *menu = trayIcon->contextMenu(); + if (menu == nullptr) { + return; + } + const QList actions = menu->actions(); + if (index >= actions.size()) { + return; + } + QAction *action = actions.at(index); + if (action == nullptr || action->isSeparator() || action->menu() != nullptr || !action->isEnabled()) { + return; + } + action->trigger(); } -void QtTrayMenu::onExitRequested() { - app->quit(); +void QtTrayMenu::clickMessage() const { + emit trayIcon->messageClicked(); } diff --git a/src/QtTrayMenu.h b/src/QtTrayMenu.h index 729c0a4..25dfc33 100644 --- a/src/QtTrayMenu.h +++ b/src/QtTrayMenu.h @@ -7,36 +7,45 @@ #include #include -class QtTrayMenu : public QObject - { - Q_OBJECT - - public: - QtTrayMenu(); - ~QtTrayMenu(); - virtual bool eventFilter(QObject *watched, QEvent *event) override; - int init(struct tray *tray); - void update(struct tray *tray); - int loop(int blocking); - void exit(); - - private: - void createMenu(struct tray_menu *items, QMenu *menu); - void onTrayActivated(QSystemTrayIcon::ActivationReason reason); - void onMenuItemTriggered(); - QApplication *app; - QSystemTrayIcon *trayIcon; - struct tray *trayStruct; - bool continueRunning; - struct tray_menu *getTrayMenuItem(QAction *action); - - signals: - void exitRequested(); - - private slots: - void onExitRequested(); - +/** + * @brief Wrapper class for platfrom-independent Qt-based tray menu. + */ +class QtTrayMenu: public QObject { + Q_OBJECT + +public: + explicit QtTrayMenu(QObject *parent = nullptr); + explicit QtTrayMenu(int argc, char **argv, bool debug, QObject *parent = nullptr); + ~QtTrayMenu() override; + bool eventFilter(QObject *watched, QEvent *event) override; + int init(struct tray *tray); + void update(struct tray *tray); + int loop(int blocking) const; + void exit(); + void configureAppMetadata(const QString &appName, const QString &appDisplayName, const QString &desktopName) const; + void showMenu() const; + void showMessage(const QString &title, const QString &msg, QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::Information, int msecs = 10000) const; + void showMessage(const QString &title, const QString &msg, const QIcon &icon, int msecs = 10000) const; + void clickMenuItem(int index) const; + void clickMessage() const; + +private: + void createMenu(struct tray_menu *items, QMenu *menu); + void createNotification() const; + QApplication *app; + QSystemTrayIcon *trayIcon; + QMenu *trayTopMenu; + struct tray *trayStruct; + bool continueRunning; + struct tray_menu *getTrayMenuItem(QAction *action); + +signals: + void exitRequested(); + +private slots: + void onExitRequested() const; + void onTrayActivated(QSystemTrayIcon::ActivationReason reason); + void onMessageClicked() const; + void onMenuItemTriggered(); }; - - -#endif // TRAYMENU_H +#endif // TRAYMENU_H diff --git a/src/example.c b/src/example.c index 411b5a9..66b92cc 100644 --- a/src/example.c +++ b/src/example.c @@ -52,6 +52,9 @@ static void submenu_cb(struct tray_menu *item) { static struct tray tray = { .icon = TRAY_ICON1, .tooltip = "Tray", + .notification_title = "Title", + .notification_text = "Text", + .notification_icon = TRAY_ICON1, .menu = (struct tray_menu[]) { {.text = "Hello", .cb = hello_cb}, diff --git a/src/tray.h b/src/tray.h index abbe7ea..e13026a 100644 --- a/src/tray.h +++ b/src/tray.h @@ -28,7 +28,7 @@ extern "C" { const char *notification_text; ///< Text to display in the notification. const char *notification_title; ///< Title to display in the notification. void (*notification_cb)(); ///< Callback to invoke when the notification is clicked. - void (*cb)(struct tray *); ///< Callback for left click, leave null to just open menu + void (*cb)(struct tray *); ///< Callback for left click, leave null to just open menu struct tray_menu *menu; ///< Menu items. const int iconPathCount; ///< Number of icon paths. const char *allIconPaths[]; ///< Array of icon paths. diff --git a/src/tray_linux.cpp b/src/tray_linux.cpp index a9facdf..93fef68 100644 --- a/src/tray_linux.cpp +++ b/src/tray_linux.cpp @@ -10,259 +10,15 @@ #include // local includes +#include "QtTrayMenu.h" #include "tray.h" // Qt includes -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/** - * @brief Handles D-Bus notification action signals. - * - * Receives the org.freedesktop.Notifications ActionInvoked signal so that - * notification click callbacks work when notifications are sent via D-Bus - * rather than Qt's built-in balloon (QSystemTrayIcon::showMessage). - * - * Defined in tray_linux.cpp rather than a separate header to keep the moc - * output self-contained via the inline `#include "tray_linux.moc"` at the - * bottom of this file. Any CMake target that compiles tray_linux.cpp with - * AUTOMOC ON will automatically generate and inline the moc output. - */ -class TrayNotificationHandler: public QObject { - Q_OBJECT - -public: - uint notification_id = 0; ///< ID of the most recently sent D-Bus notification. - void (*cb)() = nullptr; ///< Callback to invoke when the notification is activated. - -public slots: - - /** - * @brief Invoked when a D-Bus notification action is triggered. - * @param id The notification ID. - * @param action_key The action key that was triggered. - */ - void onActionInvoked(uint id, const QString &action_key) const { - if (id == notification_id && cb != nullptr && action_key == QLatin1String("default")) { - cb(); - } - } -}; +#include namespace { - std::unique_ptr g_app; // NOSONAR(cpp:S5421) - mutable state, not const - QSystemTrayIcon *g_tray_icon = nullptr; // NOSONAR(cpp:S5421) - mutable state, not const - TrayNotificationHandler *g_notification_handler = nullptr; // NOSONAR(cpp:S5421) - mutable state, not const - int g_loop_result = 0; // NOSONAR(cpp:S5421) - mutable state, not const - bool g_app_owned = false; // NOSONAR(cpp:S5421) - mutable state, not const - std::atomic g_exit_pending {false}; // NOSONAR(cpp:S5421) - written from any thread, read from tray_loop - uint g_notification_id = 0; // NOSONAR(cpp:S5421) - tracks last D-Bus notification ID for proper cleanup - std::uint64_t g_notification_generation = 0; // NOSONAR(cpp:S5421) - invalidates stale async Notify replies - std::uint64_t g_notification_active_generation = 0; // NOSONAR(cpp:S5421) - generation currently allowed to own notification_id + std::unique_ptr qt_tray_menu = nullptr; // NOSONAR(cpp:S5421) - mutable state, not const void (*g_log_cb)(int, const char *) = nullptr; // NOSONAR(cpp:S5421) - mutable state, not const - QString g_app_name; // NOSONAR(cpp:S5421) - set via tray_set_app_info before tray_init - QString g_app_display_name; // NOSONAR(cpp:S5421) - set via tray_set_app_info before tray_init - QString g_desktop_name; // NOSONAR(cpp:S5421) - set via tray_set_app_info before tray_init - - /** - * @brief Invoke @p f on the Qt application's thread. - * - * When the caller is already on the Qt thread (or there is no QApplication), - * @p f is called directly. When called from any other thread, - * QMetaObject::invokeMethod with Qt::BlockingQueuedConnection is used so that - * the caller blocks until the Qt thread finishes executing @p f. This ensures - * all Qt GUI operations happen on the thread that owns the QApplication, - * preventing cross-thread Qt object access that causes D-Bus relay warnings. - * - * Requires Qt 5.10+. - * - * @param f Callable to execute on the Qt thread. - */ - template - void run_on_qt_thread(Func f) { - QCoreApplication *app = QCoreApplication::instance(); - if (app == nullptr || QThread::currentThread() == app->thread()) { - f(); - return; - } - QMetaObject::invokeMethod(app, std::move(f), Qt::BlockingQueuedConnection); - } - - bool is_wayland_session() { - if (const QString platform = QGuiApplication::platformName().toLower(); - platform.contains(QStringLiteral("wayland"))) { - return true; - } - return !qgetenv("WAYLAND_DISPLAY").isEmpty(); - } - - bool has_wayland_display_endpoint() { - const QByteArray wayland_display = qgetenv("WAYLAND_DISPLAY"); - if (wayland_display.isEmpty()) { - return false; - } - - const QString display_name = QString::fromLocal8Bit(wayland_display).trimmed(); - if (display_name.isEmpty()) { - return false; - } - - if (const QFileInfo direct_path(display_name); direct_path.exists()) { - return true; - } - - const QByteArray runtime_dir = qgetenv("XDG_RUNTIME_DIR"); - if (runtime_dir.isEmpty()) { - return false; - } - - const QString socket_path = QDir(QString::fromLocal8Bit(runtime_dir)).filePath(display_name); - return QFileInfo::exists(socket_path); - } - - QString discover_wayland_display_name() { - if (!qEnvironmentVariableIsEmpty("WAYLAND_DISPLAY")) { - return QString(); - } - - const QByteArray runtime_dir_env = qgetenv("XDG_RUNTIME_DIR"); - if (runtime_dir_env.isEmpty()) { - return QString(); - } - - const QString runtime_dir_path = QString::fromLocal8Bit(runtime_dir_env).trimmed(); - if (runtime_dir_path.isEmpty()) { - return QString(); - } - - const QDir runtime_dir(runtime_dir_path); - if (!runtime_dir.exists()) { - return QString(); - } - - const QStringList entries = runtime_dir.entryList( - QStringList() << QStringLiteral("wayland-*"), - QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System, - QDir::Name - ); - if (entries.isEmpty()) { - return QString(); - } - - QString selected; - for (const QString &entry : entries) { - if (const QString candidate_path = runtime_dir.filePath(entry); !QFileInfo::exists(candidate_path)) { - continue; - } - if (entry == QStringLiteral("wayland-0")) { - return entry; - } - if (selected.isEmpty()) { - selected = entry; - } - } - return selected; - } - - bool try_autodiscover_wayland_display() { - const QString discovered = discover_wayland_display_name(); - if (discovered.isEmpty()) { - return false; - } - return qputenv("WAYLAND_DISPLAY", discovered.toLocal8Bit()); - } - - bool has_x11_display_endpoint() { - const QByteArray display_env = qgetenv("DISPLAY"); - if (display_env.isEmpty()) { - return false; - } - - const QString display = QString::fromLocal8Bit(display_env).trimmed(); - if (display.isEmpty()) { - return false; - } - - if (display.startsWith('/')) { - return QFileInfo::exists(display); - } - - if (!display.startsWith(':')) { - // Remote/TCP displays are not locally discoverable; treat as potentially usable. - return true; - } - - int digit_end = 1; - while (digit_end < display.size() && display.at(digit_end).isDigit()) { - digit_end++; - } - if (digit_end == 1) { - return true; - } - - bool ok = false; - const int display_number = display.mid(1, digit_end - 1).toInt(&ok); - if (!ok) { - return true; - } - - const QString socket_path = QStringLiteral("/tmp/.X11-unix/X%1").arg(display_number); - return QFileInfo::exists(socket_path); - } - - bool should_force_headless_qpa_fallback() { - if (!qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) { - return false; - } - return !has_wayland_display_endpoint() && !has_x11_display_endpoint(); - } - - QPoint screen_anchor_point(const QScreen *screen) { - if (screen == nullptr) { - return QPoint(); - } - - const QRect full = screen->geometry(); - const QRect avail = screen->availableGeometry(); - - if (avail.top() > full.top()) { - return QPoint(avail.right(), avail.top()); - } - if (avail.bottom() < full.bottom()) { - return QPoint(avail.right(), avail.bottom()); - } - if (avail.left() > full.left()) { - return QPoint(avail.left(), avail.bottom()); - } - if (avail.right() < full.right()) { - return QPoint(avail.right(), avail.bottom()); - } - - // Some compositors report no reserved panel area; top-right is a safer fallback than (0, 0). - return avail.topRight(); - } /** * @brief Qt message handler that forwards to the registered log callback. @@ -290,698 +46,35 @@ namespace { } g_log_cb(level, msg.toUtf8().constData()); } - - /** - * @brief Calculate the best position to show the context menu. - * - * Priority: - * 1. Tray icon geometry (reliable on X11/XEmbed, sometimes on SNI). - * 2. On a pure Xorg session, QCursor::pos() is accurate. - * 3. On a Wayland session (detected via WAYLAND_DISPLAY), QCursor::pos() goes - * through XWayland and reflects the last X11 cursor position, which is NOT - * updated when the pointer interacts with Wayland-native surfaces such as the - * GNOME Shell top bar. A screen-geometry heuristic is used instead: the panel - * edge is inferred from the difference between the screen's full and available - * geometries. - * - * Qt's QMenu::popup() will adjust the final position to keep the menu fully - * on-screen, including flipping it above the anchor point when needed. - * - * @return The point at which to show the context menu. - */ - QPoint calculateMenuPosition(const QPoint &preferred_pos = QPoint()) { - if (g_tray_icon != nullptr) { - const QRect iconGeo = g_tray_icon->geometry(); - if (iconGeo.isValid()) { - return iconGeo.bottomLeft(); - } - } - - if (!preferred_pos.isNull() && !is_wayland_session()) { - return preferred_pos; - } - - // When running under a Wayland compositor, XWayland cursor coordinates are stale - // for events originating from Wayland-native surfaces (e.g., the GNOME top bar). - // Detect a Wayland session regardless of the Qt platform plugin in use. - if (const bool wayland_session = is_wayland_session(); !wayland_session) { - // Pure Xorg: QCursor::pos() is accurate. - return QCursor::pos(); - } - - const QPoint cursor_pos = QCursor::pos(); - if (!cursor_pos.isNull()) { - const QScreen *cursor_screen = QGuiApplication::screenAt(cursor_pos); - if (cursor_screen != nullptr) { - return cursor_pos; - } - } - - // Wayland session fallback: infer panel anchor from the relevant screen. - const QScreen *screen = QGuiApplication::screenAt(cursor_pos); - if (screen == nullptr) { - screen = QGuiApplication::primaryScreen(); - } - if (const QPoint anchored = screen_anchor_point(screen); !anchored.isNull()) { - return anchored; - } - - return cursor_pos; - } - - QIcon icon_from_source(const QString &icon_source) { - if (icon_source.isEmpty()) { - return QIcon(); - } - - if (const QFileInfo icon_fi(icon_source); icon_fi.exists()) { - const QString file_path = icon_fi.absoluteFilePath(); - if (const QIcon file_icon(file_path); !file_icon.isNull()) { - return file_icon; - } - - const QPixmap pixmap(file_path); - if (!pixmap.isNull()) { - QIcon icon; - icon.addPixmap(pixmap); - return icon; - } - } - - if (const QIcon themed = QIcon::fromTheme(icon_source); !themed.isNull()) { - return themed; - } - - return QIcon(); - } - - QIcon resolve_tray_icon(const struct tray *tray_data) { - if (tray_data == nullptr) { - return QIcon(); - } - - if (tray_data->icon != nullptr) { - const QIcon icon = icon_from_source(QString::fromUtf8(tray_data->icon)); - if (!icon.isNull()) { - return icon; - } - } - - if (tray_data->iconPathCount > 0 && tray_data->iconPathCount < 64) { - for (int i = 0; i < tray_data->iconPathCount; i++) { - if (tray_data->allIconPaths[i] == nullptr) { - continue; - } - const QIcon icon = icon_from_source(QString::fromUtf8(tray_data->allIconPaths[i])); - if (!icon.isNull()) { - return icon; - } - } - } - - return QIcon(); - } - - void popup_menu_for_activation(const QPoint &preferred_pos, int retries_left = 3) { - if (g_tray_icon == nullptr) { - return; - } - - QMenu *menu = g_tray_icon->contextMenu(); - if (menu == nullptr || menu->isVisible()) { - return; - } - - menu->activateWindow(); - menu->setWindowFlag(Qt::Popup, true); - menu->popup(calculateMenuPosition(preferred_pos)); - menu->setFocus(Qt::PopupFocusReason); - - if (!menu->isVisible() && retries_left > 0) { - QTimer::singleShot(30, g_tray_icon, [preferred_pos, retries_left]() { - popup_menu_for_activation(preferred_pos, retries_left - 1); - }); - } - } - - void close_notification_id(uint notification_id) { - if (notification_id == 0) { - return; - } - QDBusInterface iface( - QStringLiteral("org.freedesktop.Notifications"), - QStringLiteral("/org/freedesktop/Notifications"), - QStringLiteral("org.freedesktop.Notifications") - ); - if (iface.isValid()) { - iface.asyncCall(QStringLiteral("CloseNotification"), notification_id); - } - } - - void close_notification() { - if (g_notification_id == 0) { - return; - } - const uint id_to_close = g_notification_id; - g_notification_id = 0; - close_notification_id(id_to_close); - } - - QMenu *build_menu(struct tray_menu *m, QWidget *parent) { - auto *menu = new QMenu(parent); // NOSONAR(cpp:S5025) - submenus owned by parent via Qt; top-level deleted manually - for (; m != nullptr && m->text != nullptr; m++) { - if (std::strcmp(m->text, "-") == 0) { - menu->addSeparator(); - } else if (m->submenu != nullptr) { - QMenu *sub = build_menu(m->submenu, menu); - sub->setTitle(QString::fromUtf8(m->text)); - QAction *sub_action = menu->addMenu(sub); - sub_action->setEnabled(m->disabled == 0); - } else { - auto *action = menu->addAction(QString::fromUtf8(m->text)); - action->setEnabled(m->disabled == 0); - if (m->checkbox) { - action->setCheckable(true); - action->setChecked(m->checked != 0); - } - action->setData(QVariant::fromValue(static_cast(m))); - QObject::connect(action, &QAction::triggered, menu, [action]() { - auto *item = static_cast(action->data().value()); - if (item != nullptr && item->cb != nullptr) { - item->cb(item); - } - }); - } - } - return menu; - } - - bool menu_layout_matches(const QMenu *menu, const struct tray_menu *items) { - if (menu == nullptr) { - return false; - } - - const QList actions = menu->actions(); - int action_index = 0; - for (const struct tray_menu *item = items; item != nullptr && item->text != nullptr; item++) { - if (action_index >= actions.size()) { - return false; - } - - const int current_action_index = action_index; - action_index++; - const QAction *action = actions.at(current_action_index); - if (std::strcmp(item->text, "-") == 0) { - if (!action->isSeparator()) { - return false; - } - continue; - } - - if (item->submenu != nullptr) { - const QMenu *submenu = action->menu(); - if (submenu == nullptr || !menu_layout_matches(submenu, item->submenu)) { - return false; - } - } else if (action->isSeparator() || action->menu() != nullptr) { - return false; - } - } - - return action_index == actions.size(); - } - - void update_menu_state(const QMenu *menu, struct tray_menu *items) { - if (menu == nullptr || items == nullptr) { - return; - } - - const QList actions = menu->actions(); - int action_index = 0; - for (struct tray_menu *item = items; item != nullptr && item->text != nullptr; item++) { - const int current_action_index = action_index; - action_index++; - QAction *action = actions.at(current_action_index); - if (std::strcmp(item->text, "-") == 0) { - continue; - } - - action->setText(QString::fromUtf8(item->text)); - action->setEnabled(item->disabled == 0); - if (item->submenu != nullptr) { - update_menu_state(action->menu(), item->submenu); - continue; - } - - action->setCheckable(item->checkbox != 0); - if (item->checkbox != 0) { - action->setChecked(item->checked != 0); - } - action->setData(QVariant::fromValue(static_cast(item))); - } - } - - void configure_app_metadata(const struct tray *tray) { - const QString effective_name = !g_app_name.isEmpty() ? g_app_name : QStringLiteral("tray"); - if (QCoreApplication::applicationName().isEmpty()) { - QCoreApplication::setApplicationName(effective_name); - } - - if (QGuiApplication::applicationDisplayName().isEmpty()) { - if (!g_app_display_name.isEmpty()) { - QGuiApplication::setApplicationDisplayName(g_app_display_name); - } else { - const QString display_name = - (tray != nullptr && tray->tooltip != nullptr) ? QString::fromUtf8(tray->tooltip) : effective_name; - QGuiApplication::setApplicationDisplayName(display_name); - } - } - - if (!QGuiApplication::desktopFileName().isEmpty()) { - return; - } - - if (!g_desktop_name.isEmpty()) { - QGuiApplication::setDesktopFileName(g_desktop_name); - return; - } - - QString desktop_name = QCoreApplication::applicationName(); - if (!desktop_name.endsWith(QStringLiteral(".desktop"))) { - desktop_name += QStringLiteral(".desktop"); - } - QGuiApplication::setDesktopFileName(desktop_name); - } - - void connect_activation_handler() { - // Show the context menu on left-click (Trigger). - // Qt handles right-click natively via setContextMenu on both X11/XEmbed and - // SNI (Wayland/AppIndicators), so we do not handle Context here. - // The menu position is captured immediately before deferring by a short timer. - // Deferring allows any platform pointer grab from the tray click to be released - // before the menu establishes its own grab. - QObject::connect(g_tray_icon, &QSystemTrayIcon::activated, [](QSystemTrayIcon::ActivationReason reason) { - if (const bool left_click_activation = - (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::Context); - !left_click_activation) { - return; - } - - const QPoint click_pos = QCursor::pos(); - QTimer::singleShot(30, g_tray_icon, [click_pos]() { - popup_menu_for_activation(click_pos); - }); - }); - } - - void ensure_notification_handler_connected() { - if (g_notification_handler != nullptr) { - return; - } - - g_notification_handler = new TrayNotificationHandler(); // NOSONAR(cpp:S5025) - deleted in destroy_app() - // Defer D-Bus ActionInvoked handler setup to the first event-loop iteration. - // Creating QDBusConnection socket notifiers before the event loop starts can - // trigger a "QSocketNotifier: Can only be used with threads started with QThread" - // warning when the tray runs in a std::thread. - QTimer::singleShot(0, g_notification_handler, []() { - if (g_notification_handler == nullptr) { - return; - } - QDBusConnection::sessionBus().connect( - QStringLiteral("org.freedesktop.Notifications"), - QStringLiteral("/org/freedesktop/Notifications"), - QStringLiteral("org.freedesktop.Notifications"), - QStringLiteral("ActionInvoked"), - g_notification_handler, - SLOT(onActionInvoked(uint, QString)) - ); - }); - } - - void update_context_menu(const struct tray *tray) { - if (tray->menu == nullptr) { - return; - } - - QMenu *existing_menu = g_tray_icon->contextMenu(); - if (existing_menu != nullptr && menu_layout_matches(existing_menu, tray->menu)) { - update_menu_state(existing_menu, tray->menu); - return; - } - - // setContextMenu does not take ownership; delete the old menu before replacing it. - QMenu *new_menu = build_menu(tray->menu, nullptr); // NOSONAR(cpp:S5025) - deleted via existing_menu path or on next update - g_tray_icon->setContextMenu(new_menu); - if (existing_menu == nullptr) { - return; - } - - // hide() before delete releases any X11 pointer grab held by the popup. - // Skipping this leaves the grab active, causing future popup menus to appear - // but receive no pointer events, so QAction::triggered is never emitted. - existing_menu->hide(); - delete existing_menu; // NOSONAR(cpp:S5025) - required; Qt does not own this - } - - void reset_notification_state() { - g_notification_generation++; - g_notification_active_generation = 0; - if (g_notification_handler != nullptr) { - g_notification_handler->notification_id = 0; - g_notification_handler->cb = nullptr; - } - if (g_tray_icon != nullptr) { - QObject::disconnect(g_tray_icon, &QSystemTrayIcon::messageClicked, nullptr, nullptr); - } - close_notification(); - } - - QString resolve_notification_icon(const struct tray *tray) { - const char *icon_path = tray->notification_icon != nullptr ? tray->notification_icon : tray->icon; - if (icon_path == nullptr) { - return QString(); - } - - if (const QFileInfo fi(QString::fromUtf8(icon_path)); fi.exists()) { - return QUrl::fromLocalFile(fi.absoluteFilePath()).toString(); - } - return QString::fromUtf8(icon_path); - } - - void destroy_tray(); - - void handle_notification_reply(QDBusPendingCallWatcher *watcher, const std::uint64_t notification_generation) { - const QDBusPendingReply reply = *watcher; - if (!reply.isValid() || g_tray_icon == nullptr) { - watcher->deleteLater(); - return; - } - - const uint reply_id = reply.value(); - const bool stale_reply = - notification_generation != g_notification_active_generation || g_notification_active_generation == 0; - if (stale_reply) { - // The request was cleared or superseded before Notify returned; close it immediately. - close_notification_id(reply_id); - watcher->deleteLater(); - return; - } - - g_notification_id = reply_id; - if (g_notification_handler != nullptr) { - g_notification_handler->notification_id = g_notification_id; - } - watcher->deleteLater(); - } - - bool send_dbus_notification( - const struct tray *tray, - const QString &title, - const QString &text, - const QString &icon, - const std::uint64_t notification_generation - ) { - QVariantMap hints; - if (!icon.isEmpty()) { - hints[QStringLiteral("image-path")] = icon; - } - - QDBusInterface iface( - QStringLiteral("org.freedesktop.Notifications"), - QStringLiteral("/org/freedesktop/Notifications"), - QStringLiteral("org.freedesktop.Notifications") - ); - if (!iface.isValid()) { - return false; - } - - QStringList actions; - if (tray->notification_cb != nullptr) { - actions << QStringLiteral("default") << QString(); - } - if (g_notification_handler != nullptr) { - g_notification_handler->cb = tray->notification_cb; - } - - QDBusPendingCall pending = iface.asyncCall( - QStringLiteral("Notify"), - QGuiApplication::applicationDisplayName(), - static_cast(0), - icon, - title, - text, - actions, - hints, - 5000 - ); - auto *watcher = new QDBusPendingCallWatcher(pending); // NOSONAR(cpp:S5025) - deleted via deleteLater in finished handler - QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [notification_generation](QDBusPendingCallWatcher *finished) { - handle_notification_reply(finished, notification_generation); - }); - return true; - } - - void send_qt_notification_fallback(const struct tray *tray, const QString &title, const QString &text) { - if (tray->notification_cb != nullptr && g_notification_handler != nullptr) { - g_notification_handler->cb = tray->notification_cb; - QObject::connect(g_tray_icon, &QSystemTrayIcon::messageClicked, []() { - if (g_notification_handler == nullptr || g_notification_handler->cb == nullptr) { - return; - } - g_notification_handler->cb(); - }); - } - g_tray_icon->showMessage(title, text, QSystemTrayIcon::Information, 5000); - } - - void update_notification(const struct tray *tray) { - const QString text = tray->notification_text != nullptr ? QString::fromUtf8(tray->notification_text) : QString(); - reset_notification_state(); - if (text.isEmpty()) { - return; - } - - const std::uint64_t notification_generation = g_notification_generation; - g_notification_active_generation = notification_generation; - - const QString title = tray->notification_title != nullptr ? QString::fromUtf8(tray->notification_title) : QString(); - const QString icon = resolve_notification_icon(tray); - - if (!send_dbus_notification(tray, title, text, icon, notification_generation)) { - // D-Bus may be unavailable; fall back to Qt's built-in balloon. - send_qt_notification_fallback(tray, title, text); - } - } - - void update_tray_state(const struct tray *tray) { - if (g_tray_icon == nullptr) { - return; - } - - QIcon tray_icon = resolve_tray_icon(tray); - if (tray_icon.isNull() && !g_tray_icon->icon().isNull()) { - tray_icon = g_tray_icon->icon(); - } - if (tray_icon.isNull()) { - tray_icon = QApplication::style()->standardIcon(QStyle::SP_ComputerIcon); - } - if (!tray_icon.isNull()) { - g_tray_icon->setIcon(tray_icon); - } - - if (tray->tooltip != nullptr) { - g_tray_icon->setToolTip(QString::fromUtf8(tray->tooltip)); - } - - update_context_menu(tray); - update_notification(tray); - } - - void initialize_tray(struct tray *tray, int *result) { - destroy_tray(); - g_loop_result = 0; - g_exit_pending = false; - - g_tray_icon = new QSystemTrayIcon(); // NOSONAR(cpp:S5025) - raw pointer; deleted in destroy_tray() before QApplication - if (!QSystemTrayIcon::isSystemTrayAvailable()) { - destroy_tray(); - *result = -1; - return; - } - - configure_app_metadata(tray); - connect_activation_handler(); - ensure_notification_handler_connected(); - update_tray_state(tray); - g_tray_icon->show(); - } - - void destroy_tray() { - reset_notification_state(); - if (g_tray_icon != nullptr) { - g_tray_icon->hide(); - QMenu *menu = g_tray_icon->contextMenu(); - g_tray_icon->setContextMenu(nullptr); - delete g_tray_icon; // NOSONAR(cpp:S5025) - raw pointer; deleted explicitly before QApplication is destroyed - g_tray_icon = nullptr; - if (menu != nullptr) { - menu->hide(); - delete menu; // NOSONAR(cpp:S5025) - QSystemTrayIcon does not own the context menu - } - } - } - - void destroy_app() { - if (g_notification_handler != nullptr) { - delete g_notification_handler; // NOSONAR(cpp:S5025) - raw pointer; deleted explicitly before QApplication - g_notification_handler = nullptr; - } - if (g_app_owned && g_app) { - // Destroy QApplication here (during active program execution) rather than letting - // the unique_ptr destructor run at static-destruction time. At static-destruction - // time, Qt's lazily-initialized D-Bus statics have already been destroyed (LIFO - // order), so calling QApplication::~QApplication() then would crash. - g_app.reset(); - g_app_owned = false; - } - } } // namespace extern "C" { + void tray_set_app_info(const char *app_name, const char *app_display_name, const char *desktop_name) { + const auto app_name_ = app_name != nullptr ? QString::fromUtf8(app_name) : QString(); + const auto app_display_name_ = app_display_name != nullptr ? QString::fromUtf8(app_display_name) : QString(); + const auto desktop_name_ = desktop_name != nullptr ? QString::fromUtf8(desktop_name) : QString(); + qt_tray_menu->configureAppMetadata(app_name_, app_display_name_, desktop_name_); + } int tray_init(struct tray *tray) { - if (QApplication::instance() == nullptr) { - if (try_autodiscover_wayland_display() && g_log_cb != nullptr) { - g_log_cb(1, "Qt tray: auto-discovered WAYLAND_DISPLAY from XDG_RUNTIME_DIR"); - } - if (should_force_headless_qpa_fallback()) { - qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("minimal")); - if (g_log_cb != nullptr) { - g_log_cb(2, "Qt tray: no reachable WAYLAND_DISPLAY or DISPLAY endpoint, forcing QT_QPA_PLATFORM=minimal"); - } - } - static int argc = 0; - g_app = std::make_unique(argc, nullptr); - g_app_owned = true; + if (qt_tray_menu == nullptr) { + qt_tray_menu = std::make_unique(); } - - int result = 0; - run_on_qt_thread([tray, &result]() { - initialize_tray(tray, &result); - }); - return result; + return qt_tray_menu->init(tray); } int tray_loop(int blocking) { - if (g_exit_pending) { - g_exit_pending = false; - run_on_qt_thread([]() { - destroy_tray(); - destroy_app(); - }); - return g_loop_result; - } - - if (blocking) { - if (g_app_owned) { - QApplication::exec(); - if (g_exit_pending) { - g_exit_pending = false; - destroy_tray(); - destroy_app(); - } - } else { - // An external event loop owns Qt processing; block until tray_exit() fires. - while (!g_exit_pending) { - QThread::msleep(10); - } - g_exit_pending = false; - run_on_qt_thread([]() { - destroy_tray(); - destroy_app(); - }); - } - } else { - if (g_app_owned) { - QApplication::processEvents(); - } else { - const QCoreApplication *app_inst = QCoreApplication::instance(); - if (app_inst != nullptr && QThread::currentThread() == app_inst->thread()) { - QApplication::processEvents(); - } - // On a non-Qt thread with an external app the external event loop handles processing. - } - } - return g_loop_result; + return qt_tray_menu->loop(blocking); } void tray_update(struct tray *tray) { // NOSONAR(cpp:S995) - C API requires this exact mutable-pointer signature - run_on_qt_thread([tray]() { - update_tray_state(tray); - }); - } - - void tray_show_menu(void) { - run_on_qt_thread([]() { - if (g_tray_icon != nullptr) { - const QMenu *menu = g_tray_icon->contextMenu(); - if (menu != nullptr) { - popup_menu_for_activation(QPoint()); - QApplication::processEvents(); - } - } - }); - } - - void tray_simulate_notification_click(void) { - run_on_qt_thread([]() { - if (g_notification_handler != nullptr && g_notification_handler->cb != nullptr) { - if (g_notification_handler->notification_id != 0) { - // Simulate the D-Bus ActionInvoked signal for the current notification. - g_notification_handler->onActionInvoked( - g_notification_handler->notification_id, - QStringLiteral("default") - ); - } else { - // Fallback path (no D-Bus): invoke the callback directly. - g_notification_handler->cb(); - } - } - }); - } - - void tray_simulate_menu_item_click(int index) { - run_on_qt_thread([index]() { - if (g_tray_icon == nullptr || index < 0) { - return; - } - const QMenu *menu = g_tray_icon->contextMenu(); - if (menu == nullptr) { - return; - } - const QList actions = menu->actions(); - if (index >= actions.size()) { - return; - } - QAction *action = actions.at(index); - if (action == nullptr || action->isSeparator() || action->menu() != nullptr || !action->isEnabled()) { - return; - } - action->trigger(); - }); + qt_tray_menu->update(tray); } void tray_exit(void) { - g_loop_result = -1; - g_exit_pending = true; - if (g_app_owned) { - run_on_qt_thread([]() { - if (QApplication::instance() != nullptr) { - QApplication::quit(); - } - }); - } + qt_tray_menu->exit(); + qt_tray_menu.reset(); + qt_tray_menu = nullptr; } void tray_set_log_callback(void (*cb)(int level, const char *msg)) { // NOSONAR(cpp:S5205) - C API requires a plain function pointer callback type @@ -993,15 +86,15 @@ extern "C" { } } - void tray_set_app_info(const char *app_name, const char *app_display_name, const char *desktop_name) { - g_app_name = app_name != nullptr ? QString::fromUtf8(app_name) : QString(); - g_app_display_name = app_display_name != nullptr ? QString::fromUtf8(app_display_name) : QString(); - g_desktop_name = desktop_name != nullptr ? QString::fromUtf8(desktop_name) : QString(); + void tray_show_menu(void) { + qt_tray_menu->showMenu(); } -} // extern "C" + void tray_simulate_menu_item_click(int index) { + qt_tray_menu->clickMenuItem(index); + } -// Must be included at the end of a .cpp file when Q_OBJECT classes are defined -// in that .cpp (not in a header). AUTOMOC sees this directive and generates -// tray_linux.moc, which is then inlined here at compile time. -#include "tray_linux.moc" + void tray_simulate_notification_click(void) { + qt_tray_menu->clickMessage(); + } +} // extern "C" From c83e123abb58f5d9e896110442c0eadb325dce8f Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Sun, 10 May 2026 11:42:27 +0200 Subject: [PATCH 03/15] doc: Add doxygen comments for public functions of QtTrayMenu --- src/QtTrayMenu.h | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/QtTrayMenu.h b/src/QtTrayMenu.h index 25dfc33..bf08f32 100644 --- a/src/QtTrayMenu.h +++ b/src/QtTrayMenu.h @@ -14,19 +14,96 @@ class QtTrayMenu: public QObject { Q_OBJECT public: + /** + * @brief Create a QtTrayMenu instance + * @param parent optional parent Qt object + */ explicit QtTrayMenu(QObject *parent = nullptr); + + /** + * @brief Create a QtTrayMenu instance + * @param argc argument count for QApplication (if that needs to be created) + * @param argv argument list for QApplication (if that needs to be created) + * @param debug if true isntall eventFilter for debug logging + * @param parent optional parent Qt object + */ explicit QtTrayMenu(int argc, char **argv, bool debug, QObject *parent = nullptr); + ~QtTrayMenu() override; + /** + * @brief QObject override to filter events on watched object + * @param watched object watched for event + * @param event event on object + * @return true if event should be filtered out and not be processed further + * @see https://doc.qt.io/qt-6/qobject.html#eventFilter + */ bool eventFilter(QObject *watched, QEvent *event) override; + + /** + * @brief Initialize tray with given structure + * @param tray struct containing tray configuration + * @return 0 on success + */ int init(struct tray *tray); + + /** + * @brief Update tray configuration + * @param tray struct containing tray configuration + */ void update(struct tray *tray); + + /** + * @brief Process tray loop events + * @param blocking if true the function call will block until QtTrayMenu exits + * @return 0 on successful processing if non-blocking, -1 otherwise + */ int loop(int blocking) const; + + /** + * @brief Initialize tray with given structure + */ void exit(); + + /** + * @brief Configure metadata for QApplication + * @param appName the applications name + * @param appDisplayName the applications display name + * @param desktopName the applications desktop file name + */ void configureAppMetadata(const QString &appName, const QString &appDisplayName, const QString &desktopName) const; + + /** + * @brief Show tray context menu + */ void showMenu() const; + + /** + * @brief Show tray message popup + * @param title popup title + * @param msg popup message + * @param icon popup icon + * @param msecs popup display duration + */ void showMessage(const QString &title, const QString &msg, QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::Information, int msecs = 10000) const; + + /** + * @brief Show tray message popup + * @param title popup title + * @param msg popup message + * @param icon popup icon + * @param msecs popup display duration + */ void showMessage(const QString &title, const QString &msg, const QIcon &icon, int msecs = 10000) const; + + /** + * @brief Simulate click on menu item + * @param index Menu item index to simulate click on + */ void clickMenuItem(int index) const; + + /** + * @brief Simulate click on popup message + */ void clickMessage() const; private: @@ -40,6 +117,9 @@ class QtTrayMenu: public QObject { struct tray_menu *getTrayMenuItem(QAction *action); signals: + /** + * @brief Signal emitted when exiting the tray + */ void exitRequested(); private slots: From 7d4a6a93f7d27c1fb44294b426b0af45df28bc72 Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Sun, 10 May 2026 12:39:20 +0200 Subject: [PATCH 04/15] fix: Add default initialized values and change running state tracking --- src/QtTrayMenu.cpp | 11 ++++------- src/QtTrayMenu.h | 10 +++++----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp index a535fe1..652e776 100644 --- a/src/QtTrayMenu.cpp +++ b/src/QtTrayMenu.cpp @@ -9,11 +9,7 @@ QtTrayMenu::QtTrayMenu(QObject *parent): }; QtTrayMenu::QtTrayMenu(int argc, char **argv, const bool debug, QObject *parent): - QObject(parent), - app(nullptr), - trayIcon(nullptr), - trayStruct(nullptr), - continueRunning(true) { + QObject(parent) { if (QApplication::instance()) { app = dynamic_cast(QApplication::instance()); if (!app) { @@ -56,6 +52,7 @@ int QtTrayMenu::init(struct tray *tray) { } this->trayStruct = tray; + this->running = true; if (QApplication::applicationName().isEmpty() || QApplication::applicationName() == "TrayMenuApp") { QApplication::setApplicationName(tray->tooltip); @@ -98,7 +95,7 @@ void QtTrayMenu::update(struct tray *tray) { } int QtTrayMenu::loop(int blocking) const { - if (!continueRunning) { + if (!running) { return -1; } if (!app || QApplication::closingDown()) { @@ -115,7 +112,7 @@ int QtTrayMenu::loop(int blocking) const { } void QtTrayMenu::exit() { - continueRunning = false; + running = false; emit exitRequested(); } diff --git a/src/QtTrayMenu.h b/src/QtTrayMenu.h index bf08f32..d765028 100644 --- a/src/QtTrayMenu.h +++ b/src/QtTrayMenu.h @@ -109,11 +109,11 @@ class QtTrayMenu: public QObject { private: void createMenu(struct tray_menu *items, QMenu *menu); void createNotification() const; - QApplication *app; - QSystemTrayIcon *trayIcon; - QMenu *trayTopMenu; - struct tray *trayStruct; - bool continueRunning; + QApplication *app = nullptr; + QSystemTrayIcon *trayIcon = nullptr; + QMenu *trayTopMenu = nullptr; + struct tray *trayStruct = nullptr; + bool running = false; struct tray_menu *getTrayMenuItem(QAction *action); signals: From 79126bb51d41bae8191d52c47e3d1703c78d2f56 Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Mon, 11 May 2026 12:32:44 +0200 Subject: [PATCH 05/15] Add support for spectacle screenshots on KDE --- tests/screenshot_utils.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/screenshot_utils.cpp b/tests/screenshot_utils.cpp index 859e831..a7c8548 100644 --- a/tests/screenshot_utils.cpp +++ b/tests/screenshot_utils.cpp @@ -120,6 +120,12 @@ namespace screenshot { return true; } } + if (std::system("which spectacle > /dev/null 2>&1") == 0) { + std::string cmd = "spectacle -f -b -n -o " + target; + if (std::system(cmd.c_str()) == 0) { + return true; + } + } std::string cmd = "gnome-screenshot -f " + target; return std::system(cmd.c_str()) == 0; } From 1b10b38383d83c1bbe06fba004697d6074aee5c4 Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Sun, 10 May 2026 20:10:50 +0200 Subject: [PATCH 06/15] Revert example to non-notification variant --- src/example.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/example.c b/src/example.c index 66b92cc..411b5a9 100644 --- a/src/example.c +++ b/src/example.c @@ -52,9 +52,6 @@ static void submenu_cb(struct tray_menu *item) { static struct tray tray = { .icon = TRAY_ICON1, .tooltip = "Tray", - .notification_title = "Title", - .notification_text = "Text", - .notification_icon = TRAY_ICON1, .menu = (struct tray_menu[]) { {.text = "Hello", .cb = hello_cb}, From 04599b8809aa90528072f2c61865185b949d765f Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Sun, 10 May 2026 12:47:56 +0200 Subject: [PATCH 07/15] fix: Segfaults from unit tests --- src/QtTrayMenu.cpp | 79 +++++++++++++++++++++++++++++----------------- src/QtTrayMenu.h | 7 ---- src/tray_linux.cpp | 23 ++++++++++++-- 3 files changed, 71 insertions(+), 38 deletions(-) diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp index 652e776..2401e84 100644 --- a/src/QtTrayMenu.cpp +++ b/src/QtTrayMenu.cpp @@ -31,18 +31,14 @@ QtTrayMenu::QtTrayMenu(int argc, char **argv, const bool debug, QObject *parent) } QtTrayMenu::~QtTrayMenu() { - // Delete app only if it was created within this class + // Quit QApplication + QApplication::quit(); + // Cleanup app only if it was created within this class if (app && app != QApplication::instance()) { + // Delete app and clear references delete app; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management app = nullptr; // Set to nullptr after deletion } - - // Remove custom references in correct order after app deletion to prevent SEGV - delete trayIcon; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management - trayIcon = nullptr; // Set to nullptr after deletion - - delete trayTopMenu; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management - trayTopMenu = nullptr; // Set to nullptr after deletion } int QtTrayMenu::init(struct tray *tray) { @@ -61,7 +57,6 @@ int QtTrayMenu::init(struct tray *tray) { trayIcon = new QSystemTrayIcon(QIcon(tray->icon), this); trayIcon->setToolTip(QString::fromUtf8(tray->tooltip)); - connect(this, &QtTrayMenu::exitRequested, this, &QtTrayMenu::onExitRequested); connect(trayIcon, &QSystemTrayIcon::activated, this, &QtTrayMenu::onTrayActivated); connect(trayIcon, &QSystemTrayIcon::messageClicked, this, &QtTrayMenu::onMessageClicked); @@ -77,16 +72,15 @@ int QtTrayMenu::init(struct tray *tray) { } void QtTrayMenu::update(struct tray *tray) { - this->trayStruct = tray; - if (trayIcon) { - if (const auto newIcon = QIcon(tray->icon); !newIcon.isNull()) { - trayIcon->setIcon(newIcon); - } - trayIcon->setToolTip(QString::fromUtf8(tray->tooltip)); - } - if (trayIcon == nullptr) { + if (!trayIcon) { return; } + this->trayStruct = tray; + if (const auto newIcon = QIcon(tray->icon); !newIcon.isNull()) { + trayIcon->setIcon(newIcon); + } + trayIcon->setToolTip(QString::fromUtf8(tray->tooltip)); + if (auto *existingMenu = trayIcon->contextMenu()) { existingMenu->clear(); // Remove all actions createMenu(tray->menu, existingMenu); @@ -113,7 +107,23 @@ int QtTrayMenu::loop(int blocking) const { void QtTrayMenu::exit() { running = false; - emit exitRequested(); + // Remove tray menu references + if (trayTopMenu) { + if (trayIcon) { + trayIcon->setContextMenu(nullptr); + } + delete trayTopMenu; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management + trayTopMenu = nullptr; // Set to nullptr after deletion + } + // Remove tray icon references; + if (trayIcon) { + trayIcon->hide(); + delete trayIcon; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management + trayIcon = nullptr; // Set to nullptr after deletion + } + + // Unset tray structure + trayStruct = nullptr; } void QtTrayMenu::createMenu(struct tray_menu *items, QMenu *menu) { @@ -139,7 +149,7 @@ void QtTrayMenu::createMenu(struct tray_menu *items, QMenu *menu) { } void QtTrayMenu::createNotification() const { - if (trayStruct->notification_title && trayStruct->notification_text) { + if (trayStruct && trayStruct->notification_title && trayStruct->notification_text) { const auto title = QString::fromUtf8(trayStruct->notification_title); const auto text = QString::fromUtf8(trayStruct->notification_text); if (trayStruct->notification_icon) { @@ -159,7 +169,7 @@ void QtTrayMenu::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { if (reason != QSystemTrayIcon::Trigger) { return; } - if (trayStruct->cb) { + if (trayStruct && trayStruct->cb) { trayStruct->cb(trayStruct); } else { showMenu(); @@ -179,12 +189,8 @@ struct tray_menu *QtTrayMenu::getTrayMenuItem(QAction *action) { // NOSONAR(cpp return static_cast(action->property("tray_menu_item").value()); } -void QtTrayMenu::onExitRequested() const { - QApplication::quit(); -} - void QtTrayMenu::onMessageClicked() const { - if (trayStruct->notification_cb) { + if (trayStruct && trayStruct->notification_cb) { trayStruct->notification_cb(); } } @@ -200,7 +206,7 @@ void QtTrayMenu::configureAppMetadata(const QString &appName, const QString &app QApplication::setApplicationDisplayName(appDisplayName); } else { const QString display_name = - (trayStruct != nullptr && trayStruct->tooltip != nullptr) ? QString::fromUtf8(trayStruct->tooltip) : effective_name; + (trayStruct && trayStruct->tooltip) ? QString::fromUtf8(trayStruct->tooltip) : effective_name; QApplication::setApplicationDisplayName(display_name); } } @@ -222,6 +228,9 @@ void QtTrayMenu::configureAppMetadata(const QString &appName, const QString &app } void QtTrayMenu::showMenu() const { + if (!trayIcon) { + return; + } if (QMenu *menu = trayIcon->contextMenu(); menu != nullptr) { // Due to QTBUG-139921 this is currently not working on Linux/Wayland // with Qt-6.9+ unless menu has a transient parent (which we do not have here). @@ -230,29 +239,41 @@ void QtTrayMenu::showMenu() const { } void QtTrayMenu::showMessage(const QString &title, const QString &msg, const QSystemTrayIcon::MessageIcon icon, const int msecs) const { + if (!trayIcon) { + return; + } trayIcon->showMessage(title, msg, icon, msecs); } void QtTrayMenu::showMessage(const QString &title, const QString &msg, const QIcon &icon, const int msecs) const { + if (!trayIcon) { + return; + } trayIcon->showMessage(title, msg, icon, msecs); } void QtTrayMenu::clickMenuItem(int index) const { + if (!trayIcon) { + return; + } const QMenu *menu = trayIcon->contextMenu(); - if (menu == nullptr) { + if (!menu) { return; } const QList actions = menu->actions(); - if (index >= actions.size()) { + if (index < 0 || index >= actions.size()) { return; } QAction *action = actions.at(index); - if (action == nullptr || action->isSeparator() || action->menu() != nullptr || !action->isEnabled()) { + if (!action || action->isSeparator() || action->menu() != nullptr || !action->isEnabled()) { return; } action->trigger(); } void QtTrayMenu::clickMessage() const { + if (!trayIcon) { + return; + } emit trayIcon->messageClicked(); } diff --git a/src/QtTrayMenu.h b/src/QtTrayMenu.h index d765028..49c9be1 100644 --- a/src/QtTrayMenu.h +++ b/src/QtTrayMenu.h @@ -116,14 +116,7 @@ class QtTrayMenu: public QObject { bool running = false; struct tray_menu *getTrayMenuItem(QAction *action); -signals: - /** - * @brief Signal emitted when exiting the tray - */ - void exitRequested(); - private slots: - void onExitRequested() const; void onTrayActivated(QSystemTrayIcon::ActivationReason reason); void onMessageClicked() const; void onMenuItemTriggered(); diff --git a/src/tray_linux.cpp b/src/tray_linux.cpp index 93fef68..1845c5a 100644 --- a/src/tray_linux.cpp +++ b/src/tray_linux.cpp @@ -50,6 +50,9 @@ namespace { extern "C" { void tray_set_app_info(const char *app_name, const char *app_display_name, const char *desktop_name) { + if (qt_tray_menu == nullptr) { + return; + } const auto app_name_ = app_name != nullptr ? QString::fromUtf8(app_name) : QString(); const auto app_display_name_ = app_display_name != nullptr ? QString::fromUtf8(app_display_name) : QString(); const auto desktop_name_ = desktop_name != nullptr ? QString::fromUtf8(desktop_name) : QString(); @@ -64,17 +67,24 @@ extern "C" { } int tray_loop(int blocking) { + if (qt_tray_menu == nullptr) { + return -1; + } return qt_tray_menu->loop(blocking); } void tray_update(struct tray *tray) { // NOSONAR(cpp:S995) - C API requires this exact mutable-pointer signature + if (qt_tray_menu == nullptr) { + return; + } qt_tray_menu->update(tray); } void tray_exit(void) { + if (qt_tray_menu == nullptr) { + return; + } qt_tray_menu->exit(); - qt_tray_menu.reset(); - qt_tray_menu = nullptr; } void tray_set_log_callback(void (*cb)(int level, const char *msg)) { // NOSONAR(cpp:S5205) - C API requires a plain function pointer callback type @@ -87,14 +97,23 @@ extern "C" { } void tray_show_menu(void) { + if (qt_tray_menu == nullptr) { + return; + } qt_tray_menu->showMenu(); } void tray_simulate_menu_item_click(int index) { + if (qt_tray_menu == nullptr) { + return; + } qt_tray_menu->clickMenuItem(index); } void tray_simulate_notification_click(void) { + if (qt_tray_menu == nullptr) { + return; + } qt_tray_menu->clickMessage(); } } // extern "C" From 498fba8c6477e9d9ffd3b1558e5487b9511ae324 Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Tue, 12 May 2026 08:04:17 +0200 Subject: [PATCH 08/15] WIP - process events during exit --- src/QtTrayMenu.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp index 2401e84..d54bd2d 100644 --- a/src/QtTrayMenu.cpp +++ b/src/QtTrayMenu.cpp @@ -111,6 +111,7 @@ void QtTrayMenu::exit() { if (trayTopMenu) { if (trayIcon) { trayIcon->setContextMenu(nullptr); + QApplication::processEvents(); } delete trayTopMenu; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management trayTopMenu = nullptr; // Set to nullptr after deletion @@ -118,6 +119,7 @@ void QtTrayMenu::exit() { // Remove tray icon references; if (trayIcon) { trayIcon->hide(); + QApplication::processEvents(); delete trayIcon; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management trayIcon = nullptr; // Set to nullptr after deletion } From 36ede20ca3de6e39c55e5d0d52f6e31733edfeec Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Mon, 11 May 2026 21:46:27 -0400 Subject: [PATCH 09/15] Update test_tray.cpp --- tests/unit/test_tray.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_tray.cpp b/tests/unit/test_tray.cpp index 4a91562..103896e 100644 --- a/tests/unit/test_tray.cpp +++ b/tests/unit/test_tray.cpp @@ -3,6 +3,7 @@ // standard includes #include +#include #include #include #include @@ -97,8 +98,8 @@ class TrayTest: public BaseTest { // NOSONAR(cpp:S3656) - fixture members must trayRunning = false; } - // Dismisses the open menu and exits the tray event loop from a background thread. - void closeMenuAndExit() { + // Dismisses the open menu from a background thread. + void closeMenu() { #if defined(TRAY_WINAPI) PostMessage(tray_get_hwnd(), WM_CANCELMODE, 0, 0); std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -111,18 +112,22 @@ class TrayTest: public BaseTest { // NOSONAR(cpp:S3656) - fixture members must CFRelease(event2); std::this_thread::sleep_for(std::chrono::milliseconds(100)); #endif - tray_exit(); } // Capture a screenshot while the tray menu is open, then dismiss and exit. void captureMenuStateAndExit(const char *screenshotName) { - std::thread capture_thread([this, screenshotName]() { // NOSONAR(cpp:S6168) - std::jthread is unavailable on AppleClang 17/libc++ used in CI + std::atomic_bool exitRequested {false}; + std::thread capture_thread([this, screenshotName, &exitRequested]() { // NOSONAR(cpp:S6168) - std::jthread is unavailable on AppleClang 17/libc++ used in CI EXPECT_TRUE(captureScreenshot(screenshotName)); - closeMenuAndExit(); + closeMenu(); + exitRequested.store(true, std::memory_order_release); }); tray_show_menu(); while (tray_loop(0) == 0) { + if (exitRequested.load(std::memory_order_acquire)) { + tray_exit(); + } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } capture_thread.join(); From f10b8f2f884d582070867265e81748a81de625ff Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Mon, 11 May 2026 21:59:02 -0400 Subject: [PATCH 10/15] Provide default argv, hide menu, use popup Add a persistent default argv (anonymous namespace) and include QCursor, then construct QApplication with the default argv when original argc/argv are missing to avoid using a short-lived stack array. Hide trayTopMenu before clearing tray references to prevent lingering UI. Replace menu->show() with menu->popup(QCursor::pos()) to work around QTBUG-139921 on Linux/Wayland. NOSONAR comments for QApplication allocation are preserved. --- src/QtTrayMenu.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp index d54bd2d..aaaf1f5 100644 --- a/src/QtTrayMenu.cpp +++ b/src/QtTrayMenu.cpp @@ -1,9 +1,16 @@ #include "QtTrayMenu.h" #include +#include #include #include +namespace { + int defaultArgc = 1; + char defaultArgv0[] = "TrayMenuApp"; + char *defaultArgv[] = {defaultArgv0, nullptr}; +} // namespace + QtTrayMenu::QtTrayMenu(QObject *parent): QtTrayMenu(-1, nullptr, false, parent) { }; @@ -19,11 +26,10 @@ QtTrayMenu::QtTrayMenu(int argc, char **argv, const bool debug, QObject *parent) // Note: The following is ugly but QApplication requires an argv containing the application name. // We might not have access to the real argc/argv here due to being called/pulled as a dependency. if (argc < 0 && argv == nullptr) { - argc = 1; - char *argvArray[] = {(char *) "TrayMenuApp", nullptr}; - argv = &argvArray[0]; + app = new QApplication(defaultArgc, defaultArgv); // NOSONAR(cpp:S5025) - Qt has its own integrated memory management + } else { + app = new QApplication(argc, argv); // NOSONAR(cpp:S5025) - Qt has its own integrated memory management } - app = new QApplication(argc, argv); // NOSONAR(cpp:S5025) - Qt has its own integrated memory management } if (debug) { app->installEventFilter(this); @@ -109,6 +115,7 @@ void QtTrayMenu::exit() { running = false; // Remove tray menu references if (trayTopMenu) { + trayTopMenu->hide(); if (trayIcon) { trayIcon->setContextMenu(nullptr); QApplication::processEvents(); @@ -236,7 +243,7 @@ void QtTrayMenu::showMenu() const { if (QMenu *menu = trayIcon->contextMenu(); menu != nullptr) { // Due to QTBUG-139921 this is currently not working on Linux/Wayland // with Qt-6.9+ unless menu has a transient parent (which we do not have here). - menu->show(); + menu->popup(QCursor::pos()); } } From 329b911e966a7e89446bbcfbb3c80afe6cd98b0c Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Tue, 12 May 2026 08:18:12 +0200 Subject: [PATCH 11/15] menu->popup does not work for Linux/Wayland either, stick to show() --- src/QtTrayMenu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp index aaaf1f5..045722c 100644 --- a/src/QtTrayMenu.cpp +++ b/src/QtTrayMenu.cpp @@ -243,7 +243,7 @@ void QtTrayMenu::showMenu() const { if (QMenu *menu = trayIcon->contextMenu(); menu != nullptr) { // Due to QTBUG-139921 this is currently not working on Linux/Wayland // with Qt-6.9+ unless menu has a transient parent (which we do not have here). - menu->popup(QCursor::pos()); + menu->show(); } } From 14f086120f59de5568ce6eb283d32ce658b483be Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Tue, 12 May 2026 08:21:05 +0200 Subject: [PATCH 12/15] QApplication::quit() only for apps that we created. --- src/QtTrayMenu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp index 045722c..ae195ff 100644 --- a/src/QtTrayMenu.cpp +++ b/src/QtTrayMenu.cpp @@ -37,10 +37,10 @@ QtTrayMenu::QtTrayMenu(int argc, char **argv, const bool debug, QObject *parent) } QtTrayMenu::~QtTrayMenu() { - // Quit QApplication - QApplication::quit(); // Cleanup app only if it was created within this class if (app && app != QApplication::instance()) { + // Quit QApplication + QApplication::quit(); // Delete app and clear references delete app; // NOSONAR(cpp:S5025) - Qt has its own integrated memory management app = nullptr; // Set to nullptr after deletion From 9f225932e3ab40725e028bc1335c833d3229af20 Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Tue, 12 May 2026 08:25:48 +0200 Subject: [PATCH 13/15] Cleanup QtTrayMenu constructor --- src/QtTrayMenu.cpp | 12 ++++++------ src/QtTrayMenu.h | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp index ae195ff..749b7b8 100644 --- a/src/QtTrayMenu.cpp +++ b/src/QtTrayMenu.cpp @@ -6,16 +6,16 @@ #include namespace { - int defaultArgc = 1; - char defaultArgv0[] = "TrayMenuApp"; - char *defaultArgv[] = {defaultArgv0, nullptr}; + int defaultArgc = 1; // NOSONAR(cpp:S5421): This is required for QApplication's argc/argv constructor + char defaultArgv0[] = "TrayMenuApp"; // NOSONAR(cpp:S5421): This is required for QApplication's argc/argv constructor + char *defaultArgv[] = {defaultArgv0, nullptr}; // NOSONAR(cpp:S5421,cpp:S5954): This is required for QApplication's argc/argv constructor } // namespace -QtTrayMenu::QtTrayMenu(QObject *parent): - QtTrayMenu(-1, nullptr, false, parent) { +QtTrayMenu::QtTrayMenu(QObject *parent, const bool debug): + QtTrayMenu(-1, nullptr, parent, debug) { }; -QtTrayMenu::QtTrayMenu(int argc, char **argv, const bool debug, QObject *parent): +QtTrayMenu::QtTrayMenu(int argc, char **argv, QObject *parent, const bool debug): QObject(parent) { if (QApplication::instance()) { app = dynamic_cast(QApplication::instance()); diff --git a/src/QtTrayMenu.h b/src/QtTrayMenu.h index 49c9be1..c4cda1d 100644 --- a/src/QtTrayMenu.h +++ b/src/QtTrayMenu.h @@ -17,17 +17,18 @@ class QtTrayMenu: public QObject { /** * @brief Create a QtTrayMenu instance * @param parent optional parent Qt object + * @param debug if true isntall eventFilter for debug logging */ - explicit QtTrayMenu(QObject *parent = nullptr); + explicit QtTrayMenu(QObject *parent = nullptr, bool debug = false); /** * @brief Create a QtTrayMenu instance * @param argc argument count for QApplication (if that needs to be created) * @param argv argument list for QApplication (if that needs to be created) - * @param debug if true isntall eventFilter for debug logging * @param parent optional parent Qt object + * @param debug if true isntall eventFilter for debug logging */ - explicit QtTrayMenu(int argc, char **argv, bool debug, QObject *parent = nullptr); + explicit QtTrayMenu(int argc, char **argv, QObject *parent = nullptr, bool debug = false); ~QtTrayMenu() override; /** From fc4122a15eacfbc159210a0e624f74253f5272cc Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Tue, 12 May 2026 12:52:47 +0200 Subject: [PATCH 14/15] Add option to fire notification using QSystemTray::showMessage on init/update --- src/QtTrayMenu.cpp | 16 ++++++++++------ src/QtTrayMenu.h | 6 ++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/QtTrayMenu.cpp b/src/QtTrayMenu.cpp index 749b7b8..f79608a 100644 --- a/src/QtTrayMenu.cpp +++ b/src/QtTrayMenu.cpp @@ -7,8 +7,8 @@ namespace { int defaultArgc = 1; // NOSONAR(cpp:S5421): This is required for QApplication's argc/argv constructor - char defaultArgv0[] = "TrayMenuApp"; // NOSONAR(cpp:S5421): This is required for QApplication's argc/argv constructor - char *defaultArgv[] = {defaultArgv0, nullptr}; // NOSONAR(cpp:S5421,cpp:S5954): This is required for QApplication's argc/argv constructor + char defaultArgv0[] = "TrayMenuApp"; // NOSONAR(cpp:S5421): This is required for QApplication's argc/argv constructor + char *defaultArgv[] = {defaultArgv0, nullptr}; // NOSONAR(cpp:S5421,cpp:S5954): This is required for QApplication's argc/argv constructor } // namespace QtTrayMenu::QtTrayMenu(QObject *parent, const bool debug): @@ -47,7 +47,7 @@ QtTrayMenu::~QtTrayMenu() { } } -int QtTrayMenu::init(struct tray *tray) { +int QtTrayMenu::init(struct tray *tray, const bool notification) { if (trayIcon) { // Running tray is initialized again. Fail with error. return -1; @@ -72,12 +72,14 @@ int QtTrayMenu::init(struct tray *tray) { trayIcon->setContextMenu(trayTopMenu); trayIcon->show(); - createNotification(); + if (notification) { + createNotification(); + } return 0; } -void QtTrayMenu::update(struct tray *tray) { +void QtTrayMenu::update(struct tray *tray, const bool notification) { if (!trayIcon) { return; } @@ -91,7 +93,9 @@ void QtTrayMenu::update(struct tray *tray) { existingMenu->clear(); // Remove all actions createMenu(tray->menu, existingMenu); } - createNotification(); + if (notification) { + createNotification(); + } } int QtTrayMenu::loop(int blocking) const { diff --git a/src/QtTrayMenu.h b/src/QtTrayMenu.h index c4cda1d..9a15a76 100644 --- a/src/QtTrayMenu.h +++ b/src/QtTrayMenu.h @@ -43,15 +43,17 @@ class QtTrayMenu: public QObject { /** * @brief Initialize tray with given structure * @param tray struct containing tray configuration + * @param notification fire tray notification if true * @return 0 on success */ - int init(struct tray *tray); + int init(struct tray *tray, bool notification = true); /** * @brief Update tray configuration * @param tray struct containing tray configuration + * @param notification fire tray notification if true */ - void update(struct tray *tray); + void update(struct tray *tray, bool notification = true); /** * @brief Process tray loop events From 8ebf0718521aa0101c76dd7b0a04d9015dc0f9f3 Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Tue, 12 May 2026 12:58:44 +0200 Subject: [PATCH 15/15] WIP - (Re-)Implement tray notifications using libnotify with fallback to basic QtTrayIcon::showMessage() --- src/tray_linux.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/tray_linux.cpp b/src/tray_linux.cpp index 1845c5a..1d93fbb 100644 --- a/src/tray_linux.cpp +++ b/src/tray_linux.cpp @@ -46,6 +46,16 @@ namespace { } g_log_cb(level, msg.toUtf8().constData()); } + + /** + * @brief Handle tray notifications via desktop-independent interface + * @param tray Tray structure containing notification information + * @return true if notified successfully, false otherwise + */ + bool tray_linux_notify(struct tray *tray) { + // TODO: Implement using libnotify + return false; + } } // namespace extern "C" { @@ -63,7 +73,8 @@ extern "C" { if (qt_tray_menu == nullptr) { qt_tray_menu = std::make_unique(); } - return qt_tray_menu->init(tray); + const bool notified = tray_linux_notify(tray); + return qt_tray_menu->init(tray, !notified); } int tray_loop(int blocking) { @@ -77,7 +88,8 @@ extern "C" { if (qt_tray_menu == nullptr) { return; } - qt_tray_menu->update(tray); + const bool notified = tray_linux_notify(tray); + qt_tray_menu->update(tray, !notified); } void tray_exit(void) {