Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
"imagemagick"
"ninja-build"
"xvfb"
"libnotify-dev"
)

if [[ "${QT_VERSION}" == "5" ]]; then
Expand Down
11 changes: 10 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ else()
find_library(COCOA Cocoa REQUIRED)
list(APPEND TRAY_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/tray_darwin.m")
else()
find_package(LibNotify REQUIRED)
find_package(Qt6 COMPONENTS Widgets DBus Svg)
if(Qt6_FOUND)
set(TRAY_QT_VERSION 6)
Expand All @@ -87,7 +88,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()
Expand All @@ -114,6 +118,11 @@ else()
else()
list(APPEND TRAY_EXTERNAL_LIBRARIES Qt5::Widgets Qt5::DBus Qt5::Svg)
endif()
list(APPEND TRAY_LIBNOTIFY=1)
list(APPEND TRAY_EXTERNAL_LIBRARIES ${LIBNOTIFY_LIBRARIES})

include_directories(SYSTEM ${LIBNOTIFY_INCLUDE_DIRS})
link_directories(${LIBNOTIFY_LIBRARY_DIRS})
endif()
endif()
endif()
Expand Down
55 changes: 55 additions & 0 deletions cmake/FindLibNotify.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# - Try to find LibNotify
# This module defines the following variables:
#
# LIBNOTIFY_FOUND - LibNotify was found
# LIBNOTIFY_INCLUDE_DIRS - the LibNotify include directories
# LIBNOTIFY_LIBRARIES - link these to use LibNotify
#
# Copyright (C) 2012 Raphael Kubo da Costa <rakuco@webkit.org>
# Copyright (C) 2014 Collabora Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS
# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

find_package(PkgConfig)
pkg_check_modules(LIBNOTIFY QUIET libnotify)

find_path(LIBNOTIFY_INCLUDE_DIRS
NAMES notify.h
HINTS ${LIBNOTIFY_INCLUDEDIR}
${LIBNOTIFY_INCLUDE_DIRS}
PATH_SUFFIXES libnotify
)

find_library(LIBNOTIFY_LIBRARIES
NAMES notify
HINTS ${LIBNOTIFY_LIBDIR}
${LIBNOTIFY_LIBRARY_DIRS}
)

include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibNotify REQUIRED_VARS LIBNOTIFY_INCLUDE_DIRS LIBNOTIFY_LIBRARIES
VERSION_VAR LIBNOTIFY_VERSION)

mark_as_advanced(
LIBNOTIFY_INCLUDE_DIRS
LIBNOTIFY_LIBRARIES
)
292 changes: 292 additions & 0 deletions src/QtTrayMenu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
#include "QtTrayMenu.h"

#include <QApplication>
#include <QCursor>
#include <QDebug>
#include <QMouseEvent>

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
} // namespace

QtTrayMenu::QtTrayMenu(QObject *parent, const bool debug):
QtTrayMenu(-1, nullptr, parent, debug) {
};

QtTrayMenu::QtTrayMenu(int argc, char **argv, QObject *parent, const bool debug):
QObject(parent) {
if (QApplication::instance()) {
app = dynamic_cast<QApplication *>(QApplication::instance());
if (!app) {
qDebug() << "QCoreApplication is not a QApplication, please contact support.";
}
} else {
// 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) {
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
}
}
if (debug) {
app->installEventFilter(this);
}
}

QtTrayMenu::~QtTrayMenu() {
// 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
}
}

int QtTrayMenu::init(struct tray *tray, const bool notification) {
if (trayIcon) {
// Running tray is initialized again. Fail with error.
return -1;
}

this->trayStruct = tray;
this->running = true;

if (QApplication::applicationName().isEmpty() || QApplication::applicationName() == "TrayMenuApp") {
QApplication::setApplicationName(tray->tooltip);
}

trayIcon = new QSystemTrayIcon(QIcon(tray->icon), this);
trayIcon->setToolTip(QString::fromUtf8(tray->tooltip));

connect(trayIcon, &QSystemTrayIcon::activated, this, &QtTrayMenu::onTrayActivated);
connect(trayIcon, &QSystemTrayIcon::messageClicked, this, &QtTrayMenu::onMessageClicked);

trayTopMenu = new QMenu(); // NOSONAR(cpp:S5025) - Qt has its own integrated memory management
createMenu(tray->menu, trayTopMenu);

trayIcon->setContextMenu(trayTopMenu);
trayIcon->show();

if (notification) {
createNotification();
}

return 0;
}

void QtTrayMenu::update(struct tray *tray, const bool notification) {
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);
}
if (notification) {
createNotification();
}
}

int QtTrayMenu::loop(int blocking) const {
if (!running) {
return -1;
}
if (!app || QApplication::closingDown()) {
qDebug() << "Application is not in a valid state or is closing down.";
return -1;
}
if (blocking) {
QApplication::exec();
return -1;
} else {
QApplication::processEvents();
return 0;
}
}

void QtTrayMenu::exit() {
running = false;
// Remove tray menu references
if (trayTopMenu) {
trayTopMenu->hide();
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
}
// 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
}

// Unset tray structure
trayStruct = nullptr;
}

void QtTrayMenu::createMenu(struct tray_menu *items, QMenu *menu) {
while (items && items->text) {
if (strcmp(items->text, "-") == 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) {
const auto submenu = new QMenu(menu);
createMenu(items->submenu, submenu);
action->setMenu(submenu);
}
menu->addAction(action);
}
items++;
}
}

void QtTrayMenu::createNotification() const {
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) {
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) {
return;
}
if (trayStruct && trayStruct->cb) {
trayStruct->cb(trayStruct);
} else {
showMenu();
}
}

void QtTrayMenu::onMenuItemTriggered() {
auto *action = qobject_cast<QAction *>(sender());
struct tray_menu *menuItem = getTrayMenuItem(action);

if (menuItem && menuItem->cb) {
menuItem->cb(menuItem);
}
}

struct tray_menu *QtTrayMenu::getTrayMenuItem(QAction *action) { // NOSONAR(cpp:S995) - Use as defined in function interface
return static_cast<struct tray_menu *>(action->property("tray_menu_item").value<void *>());
}

void QtTrayMenu::onMessageClicked() const {
if (trayStruct && 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 && trayStruct->tooltip) ? 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 (!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).
menu->show();
}
}

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) {
return;
}
const QList<QAction *> actions = menu->actions();
if (index < 0 || index >= actions.size()) {
return;
}
QAction *action = actions.at(index);
if (!action || action->isSeparator() || action->menu() != nullptr || !action->isEnabled()) {
return;
}
action->trigger();
}

void QtTrayMenu::clickMessage() const {
if (!trayIcon) {
return;
}
emit trayIcon->messageClicked();
}
Loading
Loading