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
286 changes: 286 additions & 0 deletions src/QtTrayMenu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
#include "QtTrayMenu.h"

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

namespace {
int defaultArgc = 1;

Check failure on line 9 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Global variables should be const.

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4Z6ckDN8V99na91Trq&open=AZ4Z6ckDN8V99na91Trq&pullRequest=123
char defaultArgv0[] = "TrayMenuApp";

Check failure on line 10 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Global variables should be const.

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4Z6ckDN8V99na91Trs&open=AZ4Z6ckDN8V99na91Trs&pullRequest=123

Check warning on line 10 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "std::string" instead of a C-style char array.

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4Z6ckDN8V99na91Trr&open=AZ4Z6ckDN8V99na91Trr&pullRequest=123
char *defaultArgv[] = {defaultArgv0, nullptr};

Check failure on line 11 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Global variables should be const.

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4Z6ckDN8V99na91Tru&open=AZ4Z6ckDN8V99na91Tru&pullRequest=123

Check warning on line 11 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "std::array" or "std::vector" instead of a C-style array.

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4Z6ckDN8V99na91Trt&open=AZ4Z6ckDN8V99na91Trt&pullRequest=123
} // namespace

QtTrayMenu::QtTrayMenu(QObject *parent):
QtTrayMenu(-1, nullptr, false, parent) {
};

QtTrayMenu::QtTrayMenu(int argc, char **argv, const bool debug, QObject *parent):
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() {
// 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
}
}

int QtTrayMenu::init(struct tray *tray) {
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();

createNotification();

return 0;
}

void QtTrayMenu::update(struct tray *tray) {
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);
}
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);
}
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) {
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->popup(QCursor::pos());
}
}

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