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

#include <QApplication>

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

QtTrayMenu::QtTrayMenu(int argc, char **argv, const bool debug, QObject *parent):
QObject(parent),
app(nullptr),
trayIcon(nullptr),
trayStruct(nullptr),
continueRunning(true) {
if (QApplication::instance()) {
app = dynamic_cast<QApplication *>(QApplication::instance());
if (!app) {
fprintf(stderr, "QCoreApplication is not a QApplication, please contact support.");

Check warning on line 18 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "std::print" instead of "fprintf".

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4JCxUFHXxPIxnTytiy&open=AZ4JCxUFHXxPIxnTytiy&pullRequest=121
}
} else {
if (argc < 0 && argv == nullptr) {
argc = 1;
char *argvArray[] = {(char *) "TrayMenuApp", nullptr};

Check warning on line 23 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=AZ4JCxUFHXxPIxnTytiz&open=AZ4JCxUFHXxPIxnTytiz&pullRequest=121

Check failure on line 23 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

C-style cast removing const qualification from the type of a pointer may lead to an undefined behaviour.

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4JCxUFHXxPIxnTyti0&open=AZ4JCxUFHXxPIxnTyti0&pullRequest=121
argv = &argvArray[0];
}
app = new QApplication(argc, argv);

Check failure on line 26 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4JCxUFHXxPIxnTyti1&open=AZ4JCxUFHXxPIxnTyti1&pullRequest=121
}
if (debug) {
app->installEventFilter(this);
}
}

QtTrayMenu::~QtTrayMenu() {
delete trayIcon;

Check failure on line 34 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4JCxUFHXxPIxnTyti2&open=AZ4JCxUFHXxPIxnTyti2&pullRequest=121
trayIcon = nullptr; // Set to nullptr after deletion

// Delete app only if it was created within this class
if (app && app != QApplication::instance()) {
delete app;

Check failure on line 39 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4JCxUFHXxPIxnTyti3&open=AZ4JCxUFHXxPIxnTyti3&pullRequest=121
app = nullptr; // Set to nullptr after deletion
}
}

int QtTrayMenu::init(struct tray *tray) {
if (trayIcon) {
return -1; // Already initialized
}

this->trayStruct = tray;

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(this, &QtTrayMenu::exitRequested, this, &QtTrayMenu::onExitRequested);
connect(trayIcon, &QSystemTrayIcon::activated, this, &QtTrayMenu::onTrayActivated);
connect(trayIcon, &QSystemTrayIcon::messageClicked, this, &QtTrayMenu::onMessageClicked);

auto *menu = new QMenu();

Check failure on line 62 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4JCxUFHXxPIxnTyti4&open=AZ4JCxUFHXxPIxnTyti4&pullRequest=121
createMenu(tray->menu, menu);
createNotification();

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

return 0;
}

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) {
return;
}
if (auto *existingMenu = trayIcon->contextMenu()) {
existingMenu->clear(); // Remove all actions
createMenu(tray->menu, existingMenu);
}
createNotification();
}

int QtTrayMenu::loop(int blocking) const {
if (!continueRunning) {
return -1;
}
if (!app || QApplication::closingDown()) {
printf("Application is not in a valid state or is closing down.\n");

Check warning on line 95 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "std::print" instead of "printf".

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4JCxUFHXxPIxnTyti5&open=AZ4JCxUFHXxPIxnTyti5&pullRequest=121
return -1;
}
if (blocking) {
QApplication::exec();
return -1;
} else {
QApplication::processEvents();
return 0;
}
}

void QtTrayMenu::exit() {
continueRunning = false;
emit exitRequested();
}

void QtTrayMenu::createMenu(struct tray_menu *items, QMenu *menu) {
while (items && items->text) {
if (std::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{

Check failure on line 134 in src/QtTrayMenu.cpp

View workflow job for this annotation

GitHub Actions / Common Lint / Common Lint

code should be clang-formatted
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) {
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) {

Check warning on line 168 in src/QtTrayMenu.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make the type of this parameter a pointer-to-const. The current type of "action" is "class QAction *".

See more on https://sonarcloud.io/project/issues?id=LizardByte_tray&issues=AZ4JCxUFHXxPIxnTyti6&open=AZ4JCxUFHXxPIxnTyti6&pullRequest=121
return static_cast<struct tray_menu *>(action->property("tray_menu_item").value<void *>());
}

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) {
// NOTE: Due to QTBUG-139921 this is currently not working on Wayland with Qt-6.9+ unless a main windows is present and active
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<QAction *> 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::clickMessage() const {
trayIcon->messageClicked();
}
47 changes: 47 additions & 0 deletions src/QtTrayMenu.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#ifndef TRAYMENU_H
#define TRAYMENU_H

#include "tray.h"

#include <QMenu>
#include <QObject>
#include <QSystemTrayIcon>

class QtTrayMenu: public QObject {
Q_OBJECT

public:
explicit QtTrayMenu(QObject *parent = nullptr);
explicit QtTrayMenu(int argc, char *argvArray[], const 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;
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
1 change: 1 addition & 0 deletions src/tray.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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

Check failure on line 31 in src/tray.h

View workflow job for this annotation

GitHub Actions / Common Lint / Common Lint

code should be clang-formatted
struct tray_menu *menu; ///< Menu items.
const int iconPathCount; ///< Number of icon paths.
const char *allIconPaths[]; ///< Array of icon paths.
Expand Down
Loading
Loading