From f6d17fe02b5561a6a375db080c2e6011777c5813 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:38:45 +0000 Subject: [PATCH 1/2] Initial plan From 8a7807fba0efe03e531266cec9fab73fcb5027da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:51:35 +0000 Subject: [PATCH 2/2] Replace libayatana-appindicator with D-Bus StatusNotifierItem implementation Co-authored-by: lijy91 <3889523+lijy91@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- src/CMakeLists.txt | 3 +- src/platform/linux/tray_icon_linux.cpp | 580 +++++++++++++++------- src/platform/linux/tray_manager_linux.cpp | 52 +- 4 files changed, 404 insertions(+), 233 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd2e0a8..a8e835d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: run: | cmake --version sudo apt-get update - sudo apt-get install -y ninja-build libgtk-3-dev libx11-dev libxi-dev libayatana-appindicator3-dev + sudo apt-get install -y ninja-build libgtk-3-dev libx11-dev libxi-dev - name: Configure CMake shell: bash diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index de2ad29..e31336c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,7 +33,6 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11) pkg_check_modules(XI REQUIRED IMPORTED_TARGET xi) - pkg_check_modules(AYATANA_APPINDICATOR REQUIRED IMPORTED_TARGET ayatana-appindicator3-0.1) elseif(APPLE) file(GLOB PLATFORM_SOURCES "platform/macos/*.mm") elseif(CMAKE_SYSTEM_NAME STREQUAL "OHOS") @@ -73,7 +72,7 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS") target_link_libraries(nativeapi PUBLIC "-framework UIKit" "-framework Foundation" "-framework CoreGraphics") target_compile_options(nativeapi PRIVATE "-x" "objective-c++") elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_link_libraries(nativeapi PUBLIC PkgConfig::GTK PkgConfig::X11 PkgConfig::XI PkgConfig::AYATANA_APPINDICATOR pthread) + target_link_libraries(nativeapi PUBLIC PkgConfig::GTK PkgConfig::X11 PkgConfig::XI pthread) elseif(APPLE) target_link_libraries(nativeapi PUBLIC "-framework Cocoa") target_link_libraries(nativeapi PUBLIC "-framework Carbon") diff --git a/src/platform/linux/tray_icon_linux.cpp b/src/platform/linux/tray_icon_linux.cpp index 940257e..bb10852 100644 --- a/src/platform/linux/tray_icon_linux.cpp +++ b/src/platform/linux/tray_icon_linux.cpp @@ -1,190 +1,436 @@ -#include -#include -#include -#include +// Linux tray icon implemented via the StatusNotifierItem D-Bus specification. +// https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ +// +// Uses GDBus (part of GLib/GIO, already a transitive dependency of GTK) so that +// no GPL-licensed libayatana-appindicator dependency is required. -// Platform-specific includes for Linux -#ifdef __linux__ +#include #include #include #include -#include -#define HAS_GTK 1 -#define HAS_AYATANA_APPINDICATOR 1 -#else -#define HAS_GTK 0 -#define HAS_AYATANA_APPINDICATOR 0 -#endif +#include +#include +#include +#include +#include +#include +#include +#include #include "../../foundation/id_allocator.h" #include "../../image.h" #include "../../menu.h" +#include "../../positioning_strategy.h" #include "../../tray_icon.h" +#include "../../tray_icon_event.h" namespace nativeapi { -// Private implementation class +// ── D-Bus introspection XML for org.kde.StatusNotifierItem ─────────────────── + +static const char kSniIntrospectionXml[] = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +// ── Icon pixel-data conversion ─────────────────────────────────────────────── + +// Returns a GVariant of type a(iiay) containing one entry for the supplied +// pixbuf (or an empty array when pixbuf is nullptr). Each pixel is encoded as +// four bytes in network byte order: Alpha, Red, Green, Blue (ARGB32). +static GVariant* PixbufToSniIconPixmaps(GdkPixbuf* pixbuf) { + GVariantBuilder array_builder; + g_variant_builder_init(&array_builder, G_VARIANT_TYPE("a(iiay)")); + + if (pixbuf) { + const int width = gdk_pixbuf_get_width(pixbuf); + const int height = gdk_pixbuf_get_height(pixbuf); + const int rowstride = gdk_pixbuf_get_rowstride(pixbuf); + const int n_channels = gdk_pixbuf_get_n_channels(pixbuf); + const gboolean has_alpha = gdk_pixbuf_get_has_alpha(pixbuf); + const guchar* pixels = gdk_pixbuf_get_pixels(pixbuf); + + std::vector argb; + argb.reserve(static_cast(width * height * 4)); + + for (int row = 0; row < height; ++row) { + const guchar* p = pixels + row * rowstride; + for (int col = 0; col < width; ++col) { + const uint8_t r = p[0]; + const uint8_t g = p[1]; + const uint8_t b = p[2]; + const uint8_t a = has_alpha ? p[3] : 255u; + argb.push_back(a); + argb.push_back(r); + argb.push_back(g); + argb.push_back(b); + p += n_channels; + } + } + + GVariantBuilder entry_builder; + g_variant_builder_init(&entry_builder, G_VARIANT_TYPE("(iiay)")); + g_variant_builder_add(&entry_builder, "i", width); + g_variant_builder_add(&entry_builder, "i", height); + g_variant_builder_add_value( + &entry_builder, + g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, argb.data(), argb.size(), sizeof(uint8_t))); + g_variant_builder_add_value(&array_builder, g_variant_builder_end(&entry_builder)); + } + + return g_variant_builder_end(&array_builder); +} + +// ── Private implementation ─────────────────────────────────────────────────── + class TrayIcon::Impl { public: + TrayIcon* owner_; + TrayIconId id_; + std::shared_ptr image_; + std::optional title_; + std::optional tooltip_; + std::shared_ptr context_menu_; + bool visible_; + ContextMenuTrigger context_menu_trigger_; + + // D-Bus state + GDBusConnection* connection_; + guint registration_id_; + guint name_owner_id_; + std::string service_name_; - Impl(AppIndicator* indicator) - : app_indicator_(indicator), + explicit Impl(TrayIcon* owner) + : owner_(owner), + image_(nullptr), title_(std::nullopt), tooltip_(std::nullopt), context_menu_(nullptr), visible_(false), - context_menu_trigger_(ContextMenuTrigger::None) { + context_menu_trigger_(ContextMenuTrigger::None), + connection_(nullptr), + registration_id_(0), + name_owner_id_(0) { id_ = IdAllocator::Allocate(); } - ~Impl() { - // Cancel any pending cleanup timeouts - // Check if source exists before removing to avoid GLib warnings - GMainContext* context = g_main_context_default(); - for (guint source_id : pending_cleanup_sources_) { - if (source_id > 0) { - GSource* source = g_main_context_find_source_by_id(context, source_id); - if (source) { - g_source_remove(source_id); - } + ~Impl() { Cleanup(); } + + // Connect to the session bus, register the SNI object, and request a + // well-known service name. Returns false on error (icon will be invisible). + bool Init() { + GError* error = nullptr; + connection_ = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error); + if (!connection_) { + if (error) { + std::cerr << "[nativeapi] SNI: D-Bus session connection failed: " << error->message + << std::endl; + g_error_free(error); } + return false; } - pending_cleanup_sources_.clear(); - } - AppIndicator* app_indicator_; - std::shared_ptr context_menu_; // Store menu shared_ptr to keep it alive - std::optional title_; - std::optional tooltip_; - bool visible_; - TrayIconId id_; - std::vector pending_cleanup_sources_; // Track GLib timeout source IDs - ContextMenuTrigger context_menu_trigger_; -}; + static std::atomic next_sni_index{1}; + service_name_ = "org.kde.StatusNotifierItem-" + + std::to_string(static_cast(getpid())) + "-" + + std::to_string(next_sni_index++); -TrayIcon::TrayIcon() : pimpl_(std::make_unique(nullptr)) { -#if HAS_GTK && HAS_AYATANA_APPINDICATOR - // Create a unique ID for this tray icon - static int next_indicator_id = 1; - std::string indicator_id = "nativeapi-tray-" + std::to_string(next_indicator_id++); - - // Create a new tray using AppIndicator - G_GNUC_BEGIN_IGNORE_DEPRECATIONS // TODO: Use libayatana-appindicator-glib instead of - // libayatana-appindicator in the future - AppIndicator* app_indicator = - app_indicator_new(indicator_id.c_str(), - "application-default-icon", // Default icon name - APP_INDICATOR_CATEGORY_APPLICATION_STATUS); - G_GNUC_END_IGNORE_DEPRECATIONS - - if (app_indicator) { - // Reinitialize the Impl with the created indicator - pimpl_ = std::make_unique(app_indicator); - pimpl_->visible_ = true; + GDBusNodeInfo* node_info = g_dbus_node_info_new_for_xml(kSniIntrospectionXml, &error); + if (!node_info) { + if (error) { + std::cerr << "[nativeapi] SNI: Bad introspection XML: " << error->message << std::endl; + g_error_free(error); + } + return false; + } + + GDBusInterfaceInfo* iface_info = + g_dbus_node_info_lookup_interface(node_info, "org.kde.StatusNotifierItem"); + + static const GDBusInterfaceVTable vtable = { + &Impl::OnMethodCall, + &Impl::OnGetProperty, + nullptr // no writable properties + }; + + registration_id_ = g_dbus_connection_register_object(connection_, "/StatusNotifierItem", + iface_info, &vtable, + this, // user_data + nullptr, // user_data_free_func + &error); + g_dbus_node_info_unref(node_info); + + if (registration_id_ == 0) { + if (error) { + std::cerr << "[nativeapi] SNI: Object registration failed: " << error->message + << std::endl; + g_error_free(error); + } + return false; + } + + name_owner_id_ = g_bus_own_name_on_connection(connection_, service_name_.c_str(), + G_BUS_NAME_OWNER_FLAGS_NONE, &Impl::OnNameAcquired, + &Impl::OnNameLost, this, nullptr); + return true; } -#endif -} -TrayIcon::TrayIcon(void* tray) : pimpl_(std::make_unique((AppIndicator*)tray)) { - // Make the indicator visible by default - if (pimpl_->app_indicator_) { - pimpl_->visible_ = true; + void Cleanup() { + // Nullify the back-pointer first so any in-flight GLib callbacks that + // haven't been dispatched yet will see a null owner and skip event emission. + owner_ = nullptr; + + if (name_owner_id_ != 0) { + g_bus_unown_name(name_owner_id_); + name_owner_id_ = 0; + } + if (connection_ && registration_id_ != 0) { + g_dbus_connection_unregister_object(connection_, registration_id_); + registration_id_ = 0; + } + if (connection_) { + g_object_unref(connection_); + connection_ = nullptr; + } } -} -TrayIcon::~TrayIcon() { - if (pimpl_->app_indicator_) { - app_indicator_set_status(pimpl_->app_indicator_, APP_INDICATOR_STATUS_PASSIVE); - g_object_unref(pimpl_->app_indicator_); - pimpl_->app_indicator_ = nullptr; + void EmitSignal(const char* signal_name, GVariant* params = nullptr) { + if (!connection_ || registration_id_ == 0) return; + GError* error = nullptr; + g_dbus_connection_emit_signal(connection_, nullptr, "/StatusNotifierItem", + "org.kde.StatusNotifierItem", signal_name, params, &error); + if (error) g_error_free(error); } -} -TrayIconId TrayIcon::GetId() { - return pimpl_->id_; -} + // ── D-Bus name callbacks ────────────────────────────────────────────────── -void TrayIcon::SetIcon(std::shared_ptr image) { - if (!pimpl_->app_indicator_) { - return; + static void OnNameAcquired(GDBusConnection* conn, const gchar* name, gpointer user_data) { + Impl* self = static_cast(user_data); + if (self) self->RegisterWithWatcher(conn, name); } - // Store the image reference - pimpl_->image_ = image; - - // If no image provided, use default icon - if (!image) { - app_indicator_set_icon_full(pimpl_->app_indicator_, "application-default-icon", "Tray Icon"); - return; + static void OnNameLost(GDBusConnection*, const gchar* name, gpointer) { + std::cerr << "[nativeapi] SNI: lost D-Bus name " << (name ? name : "(null)") << std::endl; } - // Get the native GdkPixbuf object - GdkPixbuf* pixbuf = static_cast(image->GetNativeObject()); - if (!pixbuf) { - app_indicator_set_icon_full(pimpl_->app_indicator_, "application-default-icon", "Tray Icon"); - return; + // Try both KDE and Canonical watcher service names. + void RegisterWithWatcher(GDBusConnection* conn, const gchar* service_name) { + static const char* const kWatchers[] = { + "org.kde.StatusNotifierWatcher", + "com.canonical.StatusNotifierWatcher", + nullptr, + }; + for (int i = 0; kWatchers[i]; ++i) { + GError* error = nullptr; + GVariant* reply = g_dbus_connection_call_sync( + conn, kWatchers[i], "/StatusNotifierWatcher", kWatchers[i], + "RegisterStatusNotifierItem", g_variant_new("(s)", service_name), nullptr, + G_DBUS_CALL_FLAGS_NONE, 2000, nullptr, &error); + if (reply) { + g_variant_unref(reply); + return; + } + if (error) g_error_free(error); + } + std::cerr << "[nativeapi] SNI: no StatusNotifierWatcher found; tray icon may not appear" + << std::endl; } - // Create temporary PNG file - char temp_path[] = "/tmp/tray_icon_XXXXXX"; - int fd = mkstemp(temp_path); - if (fd == -1) { - app_indicator_set_icon_full(pimpl_->app_indicator_, "application-default-icon", "Tray Icon"); - return; + // ── D-Bus method-call handler ───────────────────────────────────────────── + + static void OnMethodCall(GDBusConnection*, const gchar*, const gchar*, const gchar*, + const gchar* method_name, GVariant* parameters, + GDBusMethodInvocation* invocation, gpointer user_data) { + Impl* self = static_cast(user_data); + if (!self) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Internal error"); + return; + } + + gint x = 0, y = 0; + + if (g_strcmp0(method_name, "Activate") == 0) { + if (parameters) g_variant_get(parameters, "(ii)", &x, &y); + g_dbus_method_invocation_return_value(invocation, nullptr); + if (self->owner_) { + self->owner_->Emit(TrayIconClickedEvent(self->id_)); + if (self->context_menu_trigger_ == ContextMenuTrigger::Clicked) { + self->owner_->OpenContextMenu(); + } + } + + } else if (g_strcmp0(method_name, "SecondaryActivate") == 0) { + if (parameters) g_variant_get(parameters, "(ii)", &x, &y); + g_dbus_method_invocation_return_value(invocation, nullptr); + if (self->owner_) { + self->owner_->Emit(TrayIconDoubleClickedEvent(self->id_)); + if (self->context_menu_trigger_ == ContextMenuTrigger::DoubleClicked) { + self->owner_->OpenContextMenu(); + } + } + + } else if (g_strcmp0(method_name, "ContextMenu") == 0) { + if (parameters) g_variant_get(parameters, "(ii)", &x, &y); + g_dbus_method_invocation_return_value(invocation, nullptr); + if (self->owner_) { + self->owner_->Emit(TrayIconRightClickedEvent(self->id_)); + if (self->context_menu_trigger_ == ContextMenuTrigger::RightClicked) { + self->owner_->OpenContextMenu(); + } + } + + } else if (g_strcmp0(method_name, "Scroll") == 0) { + g_dbus_method_invocation_return_value(invocation, nullptr); + + } else { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_METHOD, + "Unknown method: %s", method_name); + } } - close(fd); - // Append .png extension - std::string png_path(temp_path); - png_path += ".png"; + // ── D-Bus property getter ───────────────────────────────────────────────── + + static GVariant* OnGetProperty(GDBusConnection*, const gchar*, const gchar*, const gchar*, + const gchar* property_name, GError** error, + gpointer user_data) { + Impl* self = static_cast(user_data); + if (!self) return nullptr; + + if (g_strcmp0(property_name, "Category") == 0) + return g_variant_new_string("ApplicationStatus"); + + if (g_strcmp0(property_name, "Id") == 0) + return g_variant_new_string("nativeapi-tray"); + + if (g_strcmp0(property_name, "Title") == 0) + return g_variant_new_string(self->title_.value_or("").c_str()); - // Save pixbuf to PNG file - GError* error = nullptr; - gboolean success = gdk_pixbuf_save(pixbuf, png_path.c_str(), "png", &error, nullptr); + if (g_strcmp0(property_name, "Status") == 0) + return g_variant_new_string(self->visible_ ? "Active" : "Passive"); - // Always clean up the original temporary file - unlink(temp_path); + if (g_strcmp0(property_name, "WindowId") == 0) + return g_variant_new_uint32(0); - if (error) { - g_error_free(error); + if (g_strcmp0(property_name, "IconName") == 0) + return g_variant_new_string(""); // we use IconPixmap instead + + if (g_strcmp0(property_name, "IconPixmap") == 0) { + // image_linux.cpp's GetNativeObjectInternal() returns GdkPixbuf* on Linux. + GdkPixbuf* pb = + self->image_ ? static_cast(self->image_->GetNativeObject()) : nullptr; + return PixbufToSniIconPixmaps(pb); + } + + if (g_strcmp0(property_name, "OverlayIconName") == 0) return g_variant_new_string(""); + if (g_strcmp0(property_name, "OverlayIconPixmap") == 0) return PixbufToSniIconPixmaps(nullptr); + if (g_strcmp0(property_name, "AttentionIconName") == 0) return g_variant_new_string(""); + if (g_strcmp0(property_name, "AttentionIconPixmap") == 0) + return PixbufToSniIconPixmaps(nullptr); + if (g_strcmp0(property_name, "AttentionMovieName") == 0) return g_variant_new_string(""); + + if (g_strcmp0(property_name, "ToolTip") == 0) { + // (sa(iiay)ss): iconName, iconPixmap[], title, description + const std::string& tip = self->tooltip_.value_or(self->title_.value_or("")); + return g_variant_new("(s@a(iiay)ss)", "", PixbufToSniIconPixmaps(nullptr), + self->title_.value_or("").c_str(), tip.c_str()); + } + + if (g_strcmp0(property_name, "ItemIsMenu") == 0) return g_variant_new_boolean(FALSE); + if (g_strcmp0(property_name, "Menu") == 0) return g_variant_new_object_path("/"); + + if (error) { + *error = g_error_new(G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, + "Unknown property: %s", property_name); + } + return nullptr; } +}; + +// ── TrayIcon public interface ───────────────────────────────────────────────── + +TrayIcon::TrayIcon() : pimpl_(std::make_unique(this)) { + if (pimpl_->Init()) { + pimpl_->visible_ = true; + } else { + std::cerr << "[nativeapi] TrayIcon: D-Bus initialisation failed; icon will not appear" + << std::endl; + } +} - if (success) { - // Set the icon and schedule cleanup - app_indicator_set_icon_full(pimpl_->app_indicator_, png_path.c_str(), ""); - - // Track the cleanup timeout source ID so we can cancel it if needed - guint source_id = g_timeout_add( - 5000, - [](gpointer data) -> gboolean { - unlink(static_cast(data)); - g_free(data); - return FALSE; // Don't repeat - }, - g_strdup(png_path.c_str())); - - // Store source ID for cleanup in destructor - pimpl_->pending_cleanup_sources_.push_back(source_id); +TrayIcon::TrayIcon(void* /*tray*/) : pimpl_(std::make_unique(this)) { + // For API compatibility; create a fresh SNI tray icon ignoring the raw pointer. + if (pimpl_->Init()) { + pimpl_->visible_ = true; } else { - // Fallback to default icon - app_indicator_set_icon_full(pimpl_->app_indicator_, "application-default-icon", "Tray Icon"); - unlink(png_path.c_str()); + std::cerr << "[nativeapi] TrayIcon: D-Bus initialisation failed; icon will not appear" + << std::endl; } } +TrayIcon::~TrayIcon() { + // Impl::~Impl calls Cleanup(), which unregisters the D-Bus object and + // releases the connection before pimpl_ is destroyed. +} + +TrayIconId TrayIcon::GetId() { + return pimpl_->id_; +} + +void TrayIcon::SetIcon(std::shared_ptr image) { + pimpl_->image_ = image; + pimpl_->EmitSignal("NewIcon"); +} + std::shared_ptr TrayIcon::GetIcon() const { return pimpl_->image_; } void TrayIcon::SetTitle(std::optional title) { pimpl_->title_ = title; - // AppIndicator uses the title as the accessible name and in some desktop - // environments - if (pimpl_->app_indicator_) { - const char* title_str = title.has_value() ? title->c_str() : ""; - app_indicator_set_label(pimpl_->app_indicator_, title_str, NULL); - } + pimpl_->EmitSignal("NewTitle"); } std::optional TrayIcon::GetTitle() { @@ -193,9 +439,7 @@ std::optional TrayIcon::GetTitle() { void TrayIcon::SetTooltip(std::optional tooltip) { pimpl_->tooltip_ = tooltip; - // AppIndicator doesn't have direct tooltip support like GtkStatusIcon - // The tooltip functionality is typically handled through the title - // or through custom menu items. We'll store it for potential future use. + pimpl_->EmitSignal("NewToolTip"); } std::optional TrayIcon::GetTooltip() { @@ -203,18 +447,7 @@ std::optional TrayIcon::GetTooltip() { } void TrayIcon::SetContextMenu(std::shared_ptr menu) { - // Store the menu shared_ptr to keep it alive pimpl_->context_menu_ = menu; - - // AppIndicator requires a menu to be set - if (pimpl_->app_indicator_ && menu && menu->GetNativeObject()) { - GtkMenu* gtk_menu = static_cast(menu->GetNativeObject()); - app_indicator_set_menu(pimpl_->app_indicator_, gtk_menu); - // Ensure the menu and its children are realized; actual popup is controlled - // by the indicator. Using map/unmap signals in menu_linux.cpp prevents - // premature opened/closed emissions. - gtk_widget_show_all(GTK_WIDGET(gtk_menu)); - } } std::shared_ptr TrayIcon::GetContextMenu() { @@ -222,66 +455,33 @@ std::shared_ptr TrayIcon::GetContextMenu() { } Rectangle TrayIcon::GetBounds() { - Rectangle bounds = {0, 0, 0, 0}; - - // AppIndicator doesn't provide geometry information like GtkStatusIcon did - // This is a limitation of the AppIndicator API as it's handled by the - // system tray implementation. We return empty bounds. - // In most modern desktop environments, this information isn't available - // to applications for security reasons. - - return bounds; + // The SNI specification does not expose icon geometry; return empty bounds. + return {0, 0, 0, 0}; } bool TrayIcon::SetVisible(bool visible) { - if (!pimpl_->app_indicator_) { - return false; - } - - if (visible) { - app_indicator_set_status(pimpl_->app_indicator_, APP_INDICATOR_STATUS_ACTIVE); - } else { - app_indicator_set_status(pimpl_->app_indicator_, APP_INDICATOR_STATUS_PASSIVE); - } - pimpl_->visible_ = visible; + const char* status = visible ? "Active" : "Passive"; + pimpl_->EmitSignal("NewStatus", g_variant_new("(s)", status)); return true; } bool TrayIcon::IsVisible() { - if (pimpl_->app_indicator_) { - AppIndicatorStatus status = app_indicator_get_status(pimpl_->app_indicator_); - return status == APP_INDICATOR_STATUS_ACTIVE; - } - return false; + return pimpl_->visible_; } bool TrayIcon::OpenContextMenu() { - if (!pimpl_->context_menu_ || !pimpl_->context_menu_->GetNativeObject()) { - return false; - } - - // AppIndicator shows context menu automatically on right-click - // We don't need to manually show it as it's managed by the indicator - // framework - return true; + if (!pimpl_->context_menu_) return false; + return pimpl_->context_menu_->Open(PositioningStrategy::CursorPosition()); } bool TrayIcon::CloseContextMenu() { - if (!pimpl_->context_menu_) { - return true; // No menu to close, consider success - } - - // AppIndicator manages menu visibility automatically - // There's no direct way to programmatically close the menu - // but we can return true as the operation is conceptually successful - return true; + if (!pimpl_->context_menu_) return true; + return pimpl_->context_menu_->Close(); } void TrayIcon::SetContextMenuTrigger(ContextMenuTrigger trigger) { pimpl_->context_menu_trigger_ = trigger; - // Note: On Linux with AppIndicator, the menu is automatically shown on right-click - // This setting is stored for compatibility but has limited effect on AppIndicator behavior } ContextMenuTrigger TrayIcon::GetContextMenuTrigger() { @@ -289,17 +489,15 @@ ContextMenuTrigger TrayIcon::GetContextMenuTrigger() { } void* TrayIcon::GetNativeObjectInternal() const { - return static_cast(pimpl_->app_indicator_); + return static_cast(pimpl_->connection_); } void TrayIcon::StartEventListening() { - // Called automatically when first listener is added - // AppIndicator handles events automatically, no platform-specific setup needed + // GDBus dispatches D-Bus method calls via the GLib main loop automatically. } void TrayIcon::StopEventListening() { - // Called automatically when last listener is removed - // AppIndicator handles events automatically, no platform-specific cleanup needed + // Nothing to tear down; GDBus uses the GLib main loop. } } // namespace nativeapi diff --git a/src/platform/linux/tray_manager_linux.cpp b/src/platform/linux/tray_manager_linux.cpp index 1567e29..69edef8 100644 --- a/src/platform/linux/tray_manager_linux.cpp +++ b/src/platform/linux/tray_manager_linux.cpp @@ -1,34 +1,10 @@ +#include #include #include #include "../../tray_icon.h" #include "../../tray_manager.h" -// Import headers -#ifdef __has_include -#if __has_include() -#include -#define HAS_GTK 1 -#else -#define HAS_GTK 0 -#endif -#else -// Fallback for older compilers -#define HAS_GTK 0 -#endif - -#ifdef __has_include -#if __has_include() -#include -#define HAS_AYATANA_APPINDICATOR 1 -#else -#define HAS_AYATANA_APPINDICATOR 0 -#endif -#else -// Fallback for older compilers -#define HAS_AYATANA_APPINDICATOR 0 -#endif - namespace nativeapi { class TrayManager::Impl { @@ -41,24 +17,22 @@ TrayManager::TrayManager() : next_tray_id_(1), pimpl_(std::make_unique()) TrayManager::~TrayManager() { std::lock_guard lock(mutex_); - // Clean up all managed tray icons - for (auto& pair : trays_) { - auto tray = pair.second; - if (tray) { - // The TrayIcon destructor will handle cleanup of the AppIndicator - } - } trays_.clear(); } bool TrayManager::IsSupported() { -#if HAS_GTK && HAS_AYATANA_APPINDICATOR - // Check if GTK is initialized and AppIndicator is available - return gtk_init_check(nullptr, nullptr); -#else - // If GTK or AppIndicator is not available, assume no system tray support - return false; -#endif + // Cache the result: session bus availability does not change during runtime. + static bool checked = false; + static bool supported = false; + if (!checked) { + GDBusConnection* conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); + if (conn) { + g_object_unref(conn); + supported = true; + } + checked = true; + } + return supported; } std::shared_ptr TrayManager::Get(TrayIconId id) {