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
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
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