diff --git a/debian/rules b/debian/rules
index 6849c555..23230e34 100755
--- a/debian/rules
+++ b/debian/rules
@@ -16,3 +16,6 @@ override_dh_auto_configure:
override_dh_strip:
dh_strip --dbg-package=libxapp-dbg
+
+override_dh_makeshlibs:
+
diff --git a/libxapp/meson.build b/libxapp/meson.build
index c419b8fc..722dd403 100644
--- a/libxapp/meson.build
+++ b/libxapp/meson.build
@@ -15,6 +15,7 @@ libdeps += gmodule_dep
libdeps += dependency('gdk-pixbuf-2.0', version: '>=2.22.0', required: true)
libdeps += dependency('cairo', required: true)
libdeps += dependency('x11', required: true)
+libdeps += dependency('dbusmenu-gtk3-0.4', required: true)
favorite_vfs_sources = [
'favorite-vfs-file.c',
@@ -56,6 +57,10 @@ xapp_sources = [
'xapp-preferences-window.c',
'xapp-stack-sidebar.c',
'xapp-status-icon.c',
+ 'xapp-status-icon-common.c',
+ 'xapp-status-icon-native.c',
+ 'xapp-status-icon-fallback.c',
+ 'xapp-status-icon-sni.c',
'xapp-status-icon-monitor.c',
'xapp-style-manager.c',
'xapp-util.c',
@@ -91,6 +96,18 @@ xapp_statusicon_interface_sources = gnome.gdbus_codegen(
dbus_headers = [ xapp_statusicon_interface_sources[1] ]
xapp_sources += xapp_statusicon_interface_sources[0]
+sn_item_interface_sources = gnome.gdbus_codegen(
+ 'sn-item-interface',
+ sources: 'sn-item.xml',
+ interface_prefix: 'org.kde',
+ namespace: 'Sn',
+ annotations: [[ 'org.kde.StatusNotifierItem', 'org.gtk.GDBus.C.Name', 'ItemInterface' ]],
+ install_header: true,
+ install_dir: join_paths(get_option('prefix'), get_option('includedir'), 'xapp/libxapp')
+)
+
+xapp_sources += sn_item_interface_sources[0]
+
xapp_enums = gnome.mkenums_simple('xapp-enums',
sources : xapp_headers,
identifier_prefix : 'XApp',
diff --git a/xapp-sn-watcher/sn-item.xml b/libxapp/sn-item.xml
similarity index 92%
rename from xapp-sn-watcher/sn-item.xml
rename to libxapp/sn-item.xml
index 1aaa609f..310b0a11 100644
--- a/xapp-sn-watcher/sn-item.xml
+++ b/libxapp/sn-item.xml
@@ -67,17 +67,13 @@
-
-
-
+
- -->
diff --git a/libxapp/xapp-status-icon-backend.h b/libxapp/xapp-status-icon-backend.h
new file mode 100644
index 00000000..c23020e7
--- /dev/null
+++ b/libxapp/xapp-status-icon-backend.h
@@ -0,0 +1,99 @@
+/* xapp-status-icon-backend.h
+ *
+ * Backend abstraction for XAppStatusIcon implementations
+ */
+
+#ifndef __XAPP_STATUS_ICON_BACKEND_H__
+#define __XAPP_STATUS_ICON_BACKEND_H__
+
+#include
+#include "xapp-status-icon.h"
+
+G_BEGIN_DECLS
+
+typedef struct _XAppStatusIcon XAppStatusIcon;
+
+/**
+ * XAppBackendType:
+ * @XAPP_BACKEND_NONE: No backend active
+ * @XAPP_BACKEND_NATIVE: Native XAppStatusIconMonitor backend (org.x.StatusIcon)
+ * @XAPP_BACKEND_SNI: StatusNotifier backend (org.kde.StatusNotifierItem)
+ * @XAPP_BACKEND_FALLBACK: GtkStatusIcon fallback backend (X11 xembed)
+ *
+ * Types of backends available for status icon display.
+ */
+typedef enum
+{
+ XAPP_BACKEND_NONE,
+ XAPP_BACKEND_NATIVE,
+ XAPP_BACKEND_SNI,
+ XAPP_BACKEND_FALLBACK
+} XAppBackendType;
+
+/**
+ * XAppBackend:
+ * @type: The backend type
+ * @init: Initialize the backend for an icon instance
+ * @cleanup: Clean up backend resources for an icon instance
+ * @sync: Sync all properties to the backend
+ * @set_icon_name: Update icon name
+ * @set_tooltip: Update tooltip text
+ * @set_visible: Update visibility state
+ * @set_label: Update label text
+ *
+ * Backend operations structure. Each backend implements these functions
+ * to handle icon lifecycle and property updates.
+ */
+typedef struct
+{
+ XAppBackendType type;
+
+ /* Lifecycle */
+ gboolean (*init)(XAppStatusIcon *icon);
+ void (*cleanup)(XAppStatusIcon *icon);
+ void (*sync)(XAppStatusIcon *icon);
+
+ /* Property updates */
+ void (*set_icon_name)(XAppStatusIcon *icon, const gchar *icon_name);
+ void (*set_tooltip)(XAppStatusIcon *icon, const gchar *tooltip);
+ void (*set_visible)(XAppStatusIcon *icon, gboolean visible);
+ void (*set_label)(XAppStatusIcon *icon, const gchar *label);
+} XAppBackend;
+
+/* Backend implementation declarations */
+extern XAppBackend native_backend_ops;
+extern XAppBackend sni_backend_ops;
+extern XAppBackend fallback_backend_ops;
+
+/* Backend initialization functions */
+gboolean native_backend_init(XAppStatusIcon *icon);
+void native_backend_cleanup(XAppStatusIcon *icon);
+void native_backend_sync(XAppStatusIcon *icon);
+void native_backend_set_icon_name(XAppStatusIcon *icon, const gchar *icon_name);
+void native_backend_set_tooltip(XAppStatusIcon *icon, const gchar *tooltip);
+void native_backend_set_visible(XAppStatusIcon *icon, gboolean visible);
+void native_backend_set_label(XAppStatusIcon *icon, const gchar *label);
+
+gboolean fallback_backend_init(XAppStatusIcon *icon);
+void fallback_backend_cleanup(XAppStatusIcon *icon);
+void fallback_backend_sync(XAppStatusIcon *icon);
+void fallback_backend_set_icon_name(XAppStatusIcon *icon, const gchar *icon_name);
+void fallback_backend_set_tooltip(XAppStatusIcon *icon, const gchar *tooltip);
+void fallback_backend_set_visible(XAppStatusIcon *icon, gboolean visible);
+void fallback_backend_set_label(XAppStatusIcon *icon, const gchar *label);
+
+gboolean sni_backend_init(XAppStatusIcon *icon);
+void sni_backend_cleanup(XAppStatusIcon *icon);
+void sni_backend_sync(XAppStatusIcon *icon);
+void sni_backend_set_icon_name(XAppStatusIcon *icon, const gchar *icon_name);
+void sni_backend_set_tooltip(XAppStatusIcon *icon, const gchar *tooltip);
+void sni_backend_set_visible(XAppStatusIcon *icon, gboolean visible);
+void sni_backend_set_label(XAppStatusIcon *icon, const gchar *label);
+
+/* Helper functions */
+const gchar *backend_type_to_string(XAppBackendType type);
+XAppStatusIconState backend_type_to_state(XAppBackendType type);
+
+G_END_DECLS
+
+#endif /* __XAPP_STATUS_ICON_BACKEND_H__ */
diff --git a/libxapp/xapp-status-icon-common.c b/libxapp/xapp-status-icon-common.c
new file mode 100644
index 00000000..e664e1c1
--- /dev/null
+++ b/libxapp/xapp-status-icon-common.c
@@ -0,0 +1,388 @@
+/* xapp-status-icon-common.c
+ *
+ * Common utility functions shared across XAppStatusIcon backends
+ */
+
+#include
+#include
+
+#include "xapp-status-icon.h"
+#include "xapp-status-icon-private.h"
+
+#define DEBUG_FLAG XAPP_DEBUG_STATUS_ICON
+#include "xapp-debug.h"
+
+/* Global variables defined in xapp-status-icon.c */
+extern guint status_icon_signals[SIGNAL_LAST];
+extern XAppStatusIconState process_icon_state;
+
+const gchar *
+panel_position_to_str (GtkPositionType type)
+{
+ switch (type)
+ {
+ case GTK_POS_LEFT:
+ return "Left";
+ case GTK_POS_RIGHT:
+ return "Right";
+ case GTK_POS_TOP:
+ return "Top";
+ case GTK_POS_BOTTOM:
+ default:
+ return "Bottom";
+ }
+}
+
+const gchar *
+button_to_str (guint button)
+{
+ switch (button)
+ {
+ case GDK_BUTTON_PRIMARY:
+ return "Left";
+ case GDK_BUTTON_SECONDARY:
+ return "Right";
+ case GDK_BUTTON_MIDDLE:
+ return "Middle";
+ default:
+ return "Unknown";
+ }
+}
+
+const gchar *
+state_to_str (XAppStatusIconState state)
+{
+ switch (state)
+ {
+ case XAPP_STATUS_ICON_STATE_NATIVE:
+ return "Native";
+ case XAPP_STATUS_ICON_STATE_FALLBACK:
+ return "Fallback";
+ case XAPP_STATUS_ICON_STATE_NO_SUPPORT:
+ return "NoSupport";
+ default:
+ return "Unknown";
+ }
+}
+
+const gchar *
+scroll_direction_to_str (XAppScrollDirection direction)
+{
+ switch (direction)
+ {
+ case XAPP_SCROLL_UP:
+ return "Up";
+ case XAPP_SCROLL_DOWN:
+ return "Down";
+ case XAPP_SCROLL_LEFT:
+ return "Left";
+ case XAPP_SCROLL_RIGHT:
+ return "Right";
+ default:
+ return "Unknown";
+ }
+}
+
+void
+cancellable_reset (XAppStatusIcon *self)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
+
+ if (priv->cancellable)
+ {
+ g_cancellable_cancel (priv->cancellable);
+ g_object_unref (priv->cancellable);
+ }
+
+ priv->cancellable = g_cancellable_new ();
+}
+
+static GdkEvent *
+synthesize_event (XAppStatusIcon *self,
+ gint x,
+ gint y,
+ guint button,
+ guint _time,
+ gint position,
+ GdkWindow **rect_window,
+ GdkRectangle *win_rect,
+ GdkGravity *rect_anchor,
+ GdkGravity *menu_anchor)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
+ GdkDisplay *display;
+ GdkWindow *window;
+ GdkSeat *seat;
+ GdkWindowAttr attributes;
+ gint attributes_mask;
+ gint fx, fy;
+
+ display = gdk_display_get_default ();
+ seat = gdk_display_get_default_seat (display);
+
+ switch (position)
+ {
+ case GTK_POS_TOP:
+ fx = x;
+ fy = y - priv->icon_size;
+ *rect_anchor = GDK_GRAVITY_SOUTH_WEST;
+ *menu_anchor = GDK_GRAVITY_NORTH_WEST;
+ break;
+ case GTK_POS_LEFT:
+ fx = x - priv->icon_size;
+ fy = y;
+ *rect_anchor = GDK_GRAVITY_NORTH_EAST;
+ *menu_anchor = GDK_GRAVITY_NORTH_WEST;
+ break;
+ case GTK_POS_RIGHT:
+ fx = x;
+ fy = y;
+ *rect_anchor = GDK_GRAVITY_NORTH_WEST;
+ *menu_anchor = GDK_GRAVITY_NORTH_EAST;
+ break;
+ case GTK_POS_BOTTOM:
+ default:
+ fx = x;
+ fy = y;
+ *rect_anchor = GDK_GRAVITY_NORTH_WEST;
+ *menu_anchor = GDK_GRAVITY_SOUTH_WEST;
+ break;
+ }
+
+ attributes.window_type = GDK_WINDOW_CHILD;
+ win_rect->x = 0;
+ win_rect->y = 0;
+ win_rect->width = priv->icon_size;
+ win_rect->height = priv->icon_size;
+ attributes.x = fx;
+ attributes.y = fy;
+ attributes.width = priv->icon_size;
+ attributes.height = priv->icon_size;
+ attributes_mask = GDK_WA_X | GDK_WA_Y;
+
+ window = gdk_window_new (NULL, &attributes, attributes_mask);
+ *rect_window = window;
+
+ GdkEvent *event = gdk_event_new (GDK_BUTTON_RELEASE);
+ event->any.window = window;
+ event->button.device = gdk_seat_get_pointer (seat);
+
+ return event;
+}
+
+static void
+primary_menu_unmapped (GtkWidget *widget,
+ gpointer user_data)
+{
+ g_return_if_fail (XAPP_IS_STATUS_ICON(user_data));
+ XAppStatusIcon *icon = XAPP_STATUS_ICON(user_data);
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ DEBUG("Primary menu unmapped");
+
+ if (process_icon_state == XAPP_STATUS_ICON_STATE_NATIVE && priv->interface_skeleton)
+ {
+ xapp_status_icon_interface_set_primary_menu_is_open (priv->interface_skeleton, FALSE);
+ }
+
+ g_signal_handlers_disconnect_by_func (widget, primary_menu_unmapped, icon);
+}
+
+static void
+secondary_menu_unmapped (GtkWidget *widget,
+ gpointer user_data)
+{
+ g_return_if_fail (XAPP_IS_STATUS_ICON(user_data));
+ XAppStatusIcon *icon = XAPP_STATUS_ICON(user_data);
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ DEBUG("Secondary menu unmapped");
+
+ if (process_icon_state == XAPP_STATUS_ICON_STATE_NATIVE && priv->interface_skeleton)
+ {
+ xapp_status_icon_interface_set_secondary_menu_is_open (priv->interface_skeleton, FALSE);
+ }
+
+ g_signal_handlers_disconnect_by_func (widget, secondary_menu_unmapped, icon);
+}
+
+void
+popup_menu (XAppStatusIcon *self,
+ GtkMenu *menu,
+ gint x,
+ gint y,
+ guint button,
+ guint _time,
+ gint panel_position)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
+ GdkWindow *rect_window;
+ GdkEvent *event;
+ GdkRectangle win_rect;
+ GdkGravity rect_anchor, menu_anchor;
+
+ DEBUG("Popup menu on behalf of application");
+
+ if (!gtk_widget_get_realized (GTK_WIDGET(menu)))
+ {
+ GtkWidget *toplevel;
+ GtkStyleContext *context;
+
+ gtk_widget_realize (GTK_WIDGET(menu));
+ toplevel = gtk_widget_get_toplevel (GTK_WIDGET(menu));
+ context = gtk_widget_get_style_context (toplevel);
+
+ /* GtkMenu uses a GtkWindow as its toplevel that is explicitly set to
+ * be client-decorated, and applies shadows outside the visible part of
+ * the menu. They interfere with clicks on the icon while the menu is open,
+ * as the invisible part takes the events instead (and this ends up doing
+ * nothing). It makes the menu a littly ugly, so here's a new class name we
+ * can use for themes to restore things bit if we want. Just avoid shadows. */
+ gtk_style_context_remove_class (context, "csd");
+ gtk_style_context_add_class (context, "xapp-status-icon-menu-window");
+ }
+
+ if (button == GDK_BUTTON_PRIMARY)
+ {
+ if (process_icon_state == XAPP_STATUS_ICON_STATE_NATIVE && priv->interface_skeleton)
+ {
+ xapp_status_icon_interface_set_primary_menu_is_open (priv->interface_skeleton, TRUE);
+ }
+
+ g_signal_connect (gtk_widget_get_toplevel (GTK_WIDGET(menu)),
+ "unmap",
+ G_CALLBACK(primary_menu_unmapped),
+ self);
+ }
+ else if (button == GDK_BUTTON_SECONDARY)
+ {
+ if (process_icon_state == XAPP_STATUS_ICON_STATE_NATIVE && priv->interface_skeleton)
+ {
+ xapp_status_icon_interface_set_secondary_menu_is_open (priv->interface_skeleton, TRUE);
+ }
+
+ g_signal_connect (gtk_widget_get_toplevel (GTK_WIDGET(menu)),
+ "unmap",
+ G_CALLBACK(secondary_menu_unmapped),
+ self);
+ }
+
+ event = synthesize_event (self,
+ x, y, button, _time, panel_position,
+ &rect_window, &win_rect, &rect_anchor, &menu_anchor);
+
+ g_object_set_data_full (G_OBJECT(menu),
+ "rect_window", rect_window,
+ (GDestroyNotify) gdk_window_destroy);
+
+ g_object_set (G_OBJECT(menu),
+ "anchor-hints", GDK_ANCHOR_SLIDE_X | GDK_ANCHOR_SLIDE_Y |
+ GDK_ANCHOR_RESIZE_X | GDK_ANCHOR_RESIZE_Y,
+ NULL);
+
+ gtk_menu_popup_at_rect (menu,
+ rect_window,
+ &win_rect,
+ rect_anchor,
+ menu_anchor,
+ event);
+
+ gdk_event_free (event);
+}
+
+gboolean
+should_send_activate (guint button, gboolean have_button_press)
+{
+ /* Middle button always activates */
+ if (button == GDK_BUTTON_MIDDLE)
+ {
+ return TRUE;
+ }
+
+ /* For primary/secondary, only activate if we saw the button press
+ * (this prevents activation when a menu is dismissed by clicking the icon again) */
+ return have_button_press;
+}
+
+GtkMenu *
+get_menu_to_use (XAppStatusIcon *icon, guint button)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+ GtkWidget *menu_to_use = NULL;
+
+ switch (button)
+ {
+ case GDK_BUTTON_PRIMARY:
+ menu_to_use = priv->primary_menu;
+ break;
+ case GDK_BUTTON_SECONDARY:
+ menu_to_use = priv->secondary_menu;
+ break;
+ }
+
+ return menu_to_use ? GTK_MENU(menu_to_use) : NULL;
+}
+
+/* Signal emission helpers */
+
+void
+emit_button_press (XAppStatusIcon *self,
+ gint x, gint y,
+ guint button,
+ guint time,
+ gint panel_position)
+{
+ g_signal_emit (self, status_icon_signals[SIGNAL_BUTTON_PRESS], 0,
+ x, y, button, time, panel_position);
+}
+
+void
+emit_button_release (XAppStatusIcon *self,
+ gint x, gint y,
+ guint button,
+ guint time,
+ gint panel_position)
+{
+ g_signal_emit (self, status_icon_signals[SIGNAL_BUTTON_RELEASE], 0,
+ x, y, button, time, panel_position);
+}
+
+void
+emit_activate (XAppStatusIcon *self,
+ guint button,
+ guint time)
+{
+ g_signal_emit (self, status_icon_signals[SIGNAL_ACTIVATE], 0,
+ button, time);
+}
+
+void
+emit_scroll (XAppStatusIcon *self,
+ gint delta,
+ XAppScrollDirection direction,
+ guint time)
+{
+ g_signal_emit (self, status_icon_signals[SIGNAL_SCROLL], 0,
+ delta, direction, time);
+}
+
+void
+emit_state_changed (XAppStatusIcon *self,
+ XAppStatusIconState state)
+{
+ g_signal_emit (self, status_icon_signals[SIGNAL_STATE_CHANGED], 0, state);
+}
+
+const gchar *
+get_process_bus_name (XAppStatusIcon *self)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
+ GDBusConnection *connection = priv->connection;
+
+ if (!connection)
+ {
+ return NULL;
+ }
+
+ return g_dbus_connection_get_unique_name (connection);
+}
diff --git a/libxapp/xapp-status-icon-fallback.c b/libxapp/xapp-status-icon-fallback.c
new file mode 100644
index 00000000..464fad0b
--- /dev/null
+++ b/libxapp/xapp-status-icon-fallback.c
@@ -0,0 +1,413 @@
+/* xapp-status-icon-fallback.c
+ *
+ * Fallback backend for XAppStatusIcon (GtkStatusIcon)
+ * Uses legacy X11 xembed system tray protocol
+ */
+
+#include
+#include
+
+#include "xapp-status-icon.h"
+#include "xapp-status-icon-private.h"
+#include "xapp-status-icon-backend.h"
+
+#define DEBUG_FLAG XAPP_DEBUG_STATUS_ICON
+#include "xapp-debug.h"
+
+/* Global variables from main file */
+extern guint status_icon_signals[];
+extern XAppStatusIconState process_icon_state;
+
+/* Forward declarations */
+static void calculate_gtk_status_icon_position_and_orientation (XAppStatusIcon *icon,
+ GtkStatusIcon *status_icon,
+ gint *x, gint *y, gint *orientation);
+static void update_fallback_icon (XAppStatusIcon *self);
+
+/* GtkStatusIcon event handlers */
+
+static void
+calculate_gtk_status_icon_position_and_orientation (XAppStatusIcon *icon,
+ GtkStatusIcon *status_icon,
+ gint *x, gint *y, gint *orientation)
+{
+ GdkScreen *screen;
+ GdkRectangle irect;
+ GtkOrientation iorientation;
+ gint final_x, final_y, final_o;
+
+ final_x = 0;
+ final_y = 0;
+ final_o = 0;
+
+ if (gtk_status_icon_get_geometry (status_icon,
+ &screen,
+ &irect,
+ &iorientation))
+ {
+ GdkDisplay *display = gdk_screen_get_display (screen);
+ GdkMonitor *monitor;
+ GdkRectangle mrect;
+
+ monitor = gdk_display_get_monitor_at_point (display,
+ irect.x + (irect.width / 2),
+ irect.y + (irect.height / 2));
+
+ gdk_monitor_get_workarea (monitor, &mrect);
+
+ switch (iorientation)
+ {
+ case GTK_ORIENTATION_HORIZONTAL:
+ final_x = irect.x;
+
+ if (irect.y + irect.height + 100 < mrect.y + mrect.height)
+ {
+ final_y = irect.y + irect.height;
+ final_o = GTK_POS_TOP;
+ }
+ else
+ {
+ final_y = irect.y;
+ final_o = GTK_POS_BOTTOM;
+ }
+
+ break;
+ case GTK_ORIENTATION_VERTICAL:
+ final_y = irect.y;
+
+ if (irect.x + irect.width + 100 < mrect.x + mrect.width)
+ {
+ final_x = irect.x + irect.width;
+ final_o = GTK_POS_LEFT;
+ }
+ else
+ {
+ final_x = irect.x;
+ final_o = GTK_POS_RIGHT;
+ }
+ }
+ }
+
+ *x = final_x;
+ *y = final_y;
+ *orientation = final_o;
+}
+
+static gboolean
+on_gtk_status_icon_button_press (GtkStatusIcon *status_icon,
+ GdkEvent *event,
+ gpointer user_data)
+{
+ XAppStatusIcon *icon = user_data;
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ guint _time;
+ guint button;
+ gint x, y, orientation;
+
+ button = event->button.button;
+ _time = event->button.time;
+
+ DEBUG ("GtkStatusIcon button-press-event with %s button", button_to_str (button));
+
+ /* We always send 'activate' for a button that has no corresponding menu,
+ * and for middle clicks. */
+ if (should_send_activate (button, priv->have_button_press))
+ {
+ DEBUG ("GtkStatusIcon activated by %s button", button_to_str (button));
+
+ emit_activate (icon, button, _time);
+ }
+
+ calculate_gtk_status_icon_position_and_orientation (icon,
+ status_icon,
+ &x,
+ &y,
+ &orientation);
+
+ priv->have_button_press = TRUE;
+
+ emit_button_press (icon, x, y, button, _time, orientation);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+on_gtk_status_icon_button_release (GtkStatusIcon *status_icon,
+ GdkEvent *event,
+ gpointer user_data)
+{
+ XAppStatusIcon *icon = user_data;
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+ GtkMenu *menu_to_use;
+ guint _time;
+ guint button;
+ gint x, y, orientation;
+
+ button = event->button.button;
+ _time = event->button.time;
+
+ DEBUG ("GtkStatusIcon button-release-event with %s button", button_to_str (button));
+
+ /* Native icons can have two menus, so we must determine which to use based
+ * on the gtk icon event's button. */
+
+ menu_to_use = get_menu_to_use (icon, button);
+
+ calculate_gtk_status_icon_position_and_orientation (icon,
+ status_icon,
+ &x,
+ &y,
+ &orientation);
+
+ if (menu_to_use)
+ {
+ DEBUG ("GtkStatusIcon popup menu for %s button", button_to_str (button));
+
+ popup_menu (icon,
+ menu_to_use,
+ x,
+ y,
+ button,
+ _time,
+ orientation);
+ }
+
+ priv->have_button_press = FALSE;
+
+ emit_button_release (icon, x, y, button, _time, orientation);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+on_gtk_status_icon_scroll (GtkStatusIcon *status_icon,
+ GdkEvent *event,
+ gpointer user_data)
+{
+ XAppStatusIcon *icon = user_data;
+ guint _time;
+
+ _time = event->scroll.time;
+ GdkScrollDirection direction;
+
+ if (gdk_event_get_scroll_direction (event, &direction))
+ {
+ XAppScrollDirection x_dir = XAPP_SCROLL_UP;
+ gint delta = 0;
+
+ if (direction != GDK_SCROLL_SMOOTH) {
+ if (direction == GDK_SCROLL_UP)
+ {
+ x_dir = XAPP_SCROLL_UP;
+ delta = -1;
+ }
+ else if (direction == GDK_SCROLL_DOWN)
+ {
+ x_dir = XAPP_SCROLL_DOWN;
+ delta = 1;
+ }
+ else if (direction == GDK_SCROLL_LEFT)
+ {
+ x_dir = XAPP_SCROLL_LEFT;
+ delta = -1;
+ }
+ else if (direction == GDK_SCROLL_RIGHT)
+ {
+ x_dir = XAPP_SCROLL_RIGHT;
+ delta = 1;
+ }
+ }
+
+ DEBUG ("Received Scroll from GtkStatusIcon %s: "
+ "delta: %d , direction: %s , time: %u",
+ gtk_status_icon_get_title (status_icon),
+ delta, scroll_direction_to_str (direction), _time);
+
+ emit_scroll (icon, delta, x_dir, _time);
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+on_gtk_status_icon_embedded_changed (GtkStatusIcon *gtk_icon,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ g_return_if_fail (GTK_IS_STATUS_ICON (gtk_icon));
+
+ XAppStatusIcon *self = XAPP_STATUS_ICON (user_data);
+
+ if (gtk_status_icon_is_embedded (gtk_icon))
+ {
+ process_icon_state = XAPP_STATUS_ICON_STATE_FALLBACK;
+ }
+ else
+ {
+ process_icon_state = XAPP_STATUS_ICON_STATE_NO_SUPPORT;
+ }
+
+ DEBUG ("Fallback icon embedded_changed. State is now %s",
+ state_to_str (process_icon_state));
+ emit_state_changed (self, process_icon_state);
+}
+
+static void
+update_fallback_icon (XAppStatusIcon *self)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
+
+ if (!priv->gtk_status_icon)
+ {
+ return;
+ }
+
+ gtk_status_icon_set_tooltip_text (priv->gtk_status_icon, priv->tooltip_text);
+
+ if (priv->icon_name)
+ {
+ gtk_status_icon_set_visible (priv->gtk_status_icon, priv->visible);
+
+ if (g_path_is_absolute (priv->icon_name))
+ {
+ gtk_status_icon_set_from_file (priv->gtk_status_icon, priv->icon_name);
+ }
+ else
+ {
+ gtk_status_icon_set_from_icon_name (priv->gtk_status_icon, priv->icon_name);
+ }
+ }
+ else
+ {
+ gtk_status_icon_set_visible (priv->gtk_status_icon, FALSE);
+ }
+}
+
+/* Backend operations implementations */
+
+gboolean
+fallback_backend_init (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ DEBUG("Fallback backend init - creating GtkStatusIcon");
+
+ process_icon_state = XAPP_STATUS_ICON_STATE_NO_SUPPORT;
+
+ if (priv->gtk_status_icon != NULL)
+ {
+ return TRUE;
+ }
+
+ priv->gtk_status_icon = gtk_status_icon_new ();
+
+ g_signal_connect (priv->gtk_status_icon,
+ "button-press-event",
+ G_CALLBACK (on_gtk_status_icon_button_press),
+ icon);
+
+ g_signal_connect (priv->gtk_status_icon,
+ "button-release-event",
+ G_CALLBACK (on_gtk_status_icon_button_release),
+ icon);
+
+ g_signal_connect (priv->gtk_status_icon,
+ "scroll-event",
+ G_CALLBACK (on_gtk_status_icon_scroll),
+ icon);
+
+ g_signal_connect (priv->gtk_status_icon,
+ "notify::embedded",
+ G_CALLBACK (on_gtk_status_icon_embedded_changed),
+ icon);
+
+ update_fallback_icon (icon);
+
+ emit_state_changed (icon, process_icon_state);
+
+ return TRUE;
+}
+
+void
+fallback_backend_cleanup (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ DEBUG("Fallback backend cleanup");
+
+ if (priv->gtk_status_icon)
+ {
+ g_signal_handlers_disconnect_by_data (priv->gtk_status_icon, icon);
+ g_clear_object (&priv->gtk_status_icon);
+ }
+}
+
+void
+fallback_backend_sync (XAppStatusIcon *icon)
+{
+ DEBUG("Fallback backend sync");
+ update_fallback_icon (icon);
+}
+
+void
+fallback_backend_set_icon_name (XAppStatusIcon *icon, const gchar *icon_name)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (priv->gtk_status_icon)
+ {
+ if (icon_name && g_path_is_absolute (icon_name))
+ {
+ gtk_status_icon_set_from_file (priv->gtk_status_icon, icon_name);
+ }
+ else if (icon_name)
+ {
+ gtk_status_icon_set_from_icon_name (priv->gtk_status_icon, icon_name);
+ }
+
+ gtk_status_icon_set_visible (priv->gtk_status_icon, priv->visible && icon_name != NULL);
+ }
+}
+
+void
+fallback_backend_set_tooltip (XAppStatusIcon *icon, const gchar *tooltip)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (priv->gtk_status_icon)
+ {
+ gtk_status_icon_set_tooltip_text (priv->gtk_status_icon, tooltip);
+ }
+}
+
+void
+fallback_backend_set_visible (XAppStatusIcon *icon, gboolean visible)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (priv->gtk_status_icon)
+ {
+ gtk_status_icon_set_visible (priv->gtk_status_icon, visible && priv->icon_name != NULL);
+ }
+}
+
+void
+fallback_backend_set_label (XAppStatusIcon *icon, const gchar *label)
+{
+ /* GtkStatusIcon doesn't support labels - this is a no-op */
+ DEBUG("Fallback backend set_label called (GtkStatusIcon doesn't support labels): %s",
+ label ? label : "(null)");
+}
+
+/* Backend operations structure */
+XAppBackend fallback_backend_ops = {
+ .type = XAPP_BACKEND_FALLBACK,
+ .init = fallback_backend_init,
+ .cleanup = fallback_backend_cleanup,
+ .sync = fallback_backend_sync,
+ .set_icon_name = fallback_backend_set_icon_name,
+ .set_tooltip = fallback_backend_set_tooltip,
+ .set_visible = fallback_backend_set_visible,
+ .set_label = fallback_backend_set_label,
+};
diff --git a/libxapp/xapp-status-icon-native.c b/libxapp/xapp-status-icon-native.c
new file mode 100644
index 00000000..39224019
--- /dev/null
+++ b/libxapp/xapp-status-icon-native.c
@@ -0,0 +1,554 @@
+/* xapp-status-icon-native.c
+ *
+ * Native backend for XAppStatusIcon (org.x.StatusIcon interface)
+ * Uses XAppStatusIconMonitor (Mint/Cinnamon/MATE/Xfce panel applets)
+ */
+
+#include
+#include
+
+#include "xapp-status-icon.h"
+#include "xapp-status-icon-private.h"
+#include "xapp-status-icon-backend.h"
+#include "xapp-statusicon-interface.h"
+
+#define DEBUG_FLAG XAPP_DEBUG_STATUS_ICON
+#include "xapp-debug.h"
+
+/* Constants */
+#define FDO_DBUS_NAME "org.freedesktop.DBus"
+#define FDO_DBUS_PATH "/org/freedesktop/DBus"
+
+#define ICON_BASE_PATH "/org/x/StatusIcon"
+#define ICON_SUB_PATH (ICON_BASE_PATH "/Icon")
+#define ICON_NAME "org.x.StatusIcon"
+
+#define STATUS_ICON_MONITOR_MATCH "org.x.StatusIconMonitor"
+
+#define MAX_NAME_FAILS 3
+
+/* Global variables from main file */
+extern guint status_icon_signals[];
+extern XAppStatusIconState process_icon_state;
+
+/* Native backend D-Bus object manager infrastructure */
+static GDBusObjectManagerServer *obj_server = NULL;
+static guint name_owner_id = 0;
+
+/* Forward declarations */
+static void obj_server_finalized (gpointer data, GObject *object);
+static void ensure_object_manager (XAppStatusIcon *self);
+
+static gboolean handle_click_method (XAppStatusIconInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint x,
+ gint y,
+ guint button,
+ guint _time,
+ gint panel_position,
+ XAppStatusIcon *icon);
+
+static gboolean handle_scroll_method (XAppStatusIconInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint delta,
+ XAppScrollDirection direction,
+ guint _time,
+ XAppStatusIcon *icon);
+
+/* Structure for signal connections */
+typedef struct
+{
+ const gchar *signal_name;
+ gpointer callback;
+} SkeletonSignal;
+
+static SkeletonSignal skeleton_status_icon_signals[] = {
+ { "handle-button-press", handle_click_method },
+ { "handle-button-release", handle_click_method },
+ { "handle-scroll", handle_scroll_method }
+};
+
+/* Native backend object manager infrastructure */
+
+static void
+obj_server_finalized (gpointer data,
+ GObject *object)
+{
+ DEBUG ("Final icon removed, clearing object manager (%s)", g_get_prgname ());
+
+ if (name_owner_id > 0)
+ {
+ g_bus_unown_name (name_owner_id);
+ name_owner_id = 0;
+ }
+
+ obj_server = NULL;
+}
+
+static void
+ensure_object_manager (XAppStatusIcon *self)
+{
+ if (obj_server == NULL)
+ {
+ DEBUG ("New object manager for (%s)", g_get_prgname ());
+
+ obj_server = g_dbus_object_manager_server_new (ICON_BASE_PATH);
+ g_dbus_object_manager_server_set_connection (obj_server, XAPP_STATUS_ICON_GET_PRIVATE(self)->connection);
+ g_object_weak_ref (G_OBJECT (obj_server), (GWeakNotify) obj_server_finalized, self);
+ }
+ else
+ {
+ g_object_ref (obj_server);
+ }
+}
+
+/* Native backend D-Bus method handlers */
+
+static gboolean
+handle_click_method (XAppStatusIconInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint x,
+ gint y,
+ guint button,
+ guint _time,
+ gint panel_position,
+ XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+ const gchar *name = g_dbus_method_invocation_get_method_name (invocation);
+
+ if (g_strcmp0 (name, "ButtonPress") == 0)
+ {
+ DEBUG ("Received ButtonPress from monitor %s: "
+ "pos:%d,%d , button: %s , time: %u , orientation: %s",
+ g_dbus_method_invocation_get_sender (invocation),
+ x, y, button_to_str (button), _time, panel_position_to_str (panel_position));
+
+ if (should_send_activate (button, priv->have_button_press))
+ {
+ DEBUG ("Native sending 'activate' for %s button", button_to_str (button));
+ emit_activate (icon, button, _time);
+ }
+
+ priv->have_button_press = TRUE;
+
+ emit_button_press (icon, x, y, button, _time, panel_position);
+
+ xapp_status_icon_interface_complete_button_press (skeleton,
+ invocation);
+ }
+ else
+ if (g_strcmp0 (name, "ButtonRelease") == 0)
+ {
+ DEBUG ("Received ButtonRelease from monitor %s: "
+ "pos:%d,%d , button: %s , time: %u , orientation: %s",
+ g_dbus_method_invocation_get_sender (invocation),
+ x, y, button_to_str (button), _time, panel_position_to_str (panel_position));
+
+ if (priv->have_button_press)
+ {
+ GtkMenu *menu_to_use = get_menu_to_use (icon, button);
+
+ if (menu_to_use)
+ {
+ popup_menu (icon,
+ menu_to_use,
+ x, y,
+ button,
+ _time,
+ panel_position);
+ }
+
+ emit_button_release (icon, x, y, button, _time, panel_position);
+ }
+
+ priv->have_button_press = FALSE;
+
+ xapp_status_icon_interface_complete_button_release (skeleton,
+ invocation);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+handle_scroll_method (XAppStatusIconInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint delta,
+ XAppScrollDirection direction,
+ guint _time,
+ XAppStatusIcon *icon)
+{
+ DEBUG ("Received Scroll from monitor %s: "
+ "delta: %d , direction: %s , time: %u",
+ g_dbus_method_invocation_get_sender (invocation),
+ delta, scroll_direction_to_str (direction), _time);
+
+ emit_scroll (icon, delta, direction, _time);
+
+ xapp_status_icon_interface_complete_scroll (skeleton,
+ invocation);
+
+ return TRUE;
+}
+
+/* Native backend lifecycle and management functions */
+
+static void
+sync_skeleton (XAppStatusIcon *self)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
+ DEBUG ("Syncing icon properties (%s)", priv->name);
+
+ priv->fail_counter = 0;
+
+ g_clear_object (&priv->gtk_status_icon);
+
+ g_object_set (G_OBJECT (priv->interface_skeleton),
+ "name", priv->name,
+ "label", priv->label,
+ "icon-name", priv->icon_name,
+ "tooltip-text", priv->tooltip_text,
+ "visible", priv->visible,
+ "metadata", priv->metadata,
+ NULL);
+
+ g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (priv->interface_skeleton));
+}
+
+static void
+on_name_acquired (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ process_icon_state = XAPP_STATUS_ICON_STATE_NATIVE;
+
+ GList *instances = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (obj_server));
+ GList *l;
+
+ for (l = instances; l != NULL; l = l->next)
+ {
+ GObject *instance = G_OBJECT (l->data);
+ XAppStatusIcon *icon = XAPP_STATUS_ICON (g_object_get_data (instance, "xapp-status-icon-instance"));
+
+ if (icon == NULL)
+ {
+ g_warning ("on_name_aquired: Could not retrieve xapp-status-icon-instance data: %s", name);
+ continue;
+ }
+
+ sync_skeleton (icon);
+
+ DEBUG ("Name acquired on dbus, state is now: %s",
+ state_to_str (process_icon_state));
+
+ emit_state_changed (icon, process_icon_state);
+ }
+
+ g_list_free_full (instances, g_object_unref);
+}
+
+static void
+on_name_lost (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ g_warning ("XAppStatusIcon: lost or could not acquire presence on dbus. Refreshing.");
+
+ GList *instances = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (obj_server));
+ GList *l;
+
+ for (l = instances; l != NULL; l = l->next)
+ {
+ GObject *instance = G_OBJECT (l->data);
+ XAppStatusIcon *icon = XAPP_STATUS_ICON (g_object_get_data (instance, "xapp-status-icon-instance"));
+
+ if (icon == NULL)
+ {
+ g_warning ("on_name_lost: Could not retrieve xapp-status-icon-instance data: %s", name);
+ continue;
+ }
+
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->fail_counter++;
+ refresh_icon (icon);
+ }
+
+ g_list_free_full (instances, g_object_unref);
+}
+
+static gboolean
+export_icon_interface (XAppStatusIcon *self)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
+ gint i;
+
+ ensure_object_manager (self);
+
+ if (priv->interface_skeleton)
+ {
+ return TRUE;
+ }
+
+ priv->object_skeleton = xapp_object_skeleton_new (ICON_SUB_PATH);
+ priv->interface_skeleton = xapp_status_icon_interface_skeleton_new ();
+
+ xapp_object_skeleton_set_status_icon_interface (priv->object_skeleton,
+ priv->interface_skeleton);
+
+ g_object_set_data (G_OBJECT (priv->object_skeleton), "xapp-status-icon-instance", self);
+
+ g_dbus_object_manager_server_export_uniquely (obj_server,
+ G_DBUS_OBJECT_SKELETON (priv->object_skeleton));
+
+ g_object_unref (priv->object_skeleton);
+ g_object_unref (priv->interface_skeleton);
+
+ for (i = 0; i < G_N_ELEMENTS (skeleton_status_icon_signals); i++) {
+ SkeletonSignal sig = skeleton_status_icon_signals[i];
+
+ g_signal_connect (priv->interface_skeleton,
+ sig.signal_name,
+ G_CALLBACK (sig.callback),
+ self);
+ }
+
+ return TRUE;
+}
+
+static void
+connect_with_status_applet (XAppStatusIcon *self)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
+ gchar **name_parts = NULL;
+ gchar *owner_name;
+
+ name_parts = g_strsplit (priv->name, ".", -1);
+
+ if (g_dbus_is_name (priv->name) &&
+ g_str_has_prefix (priv->name, ICON_NAME) &&
+ g_strv_length (name_parts) == 4)
+ {
+ owner_name = g_strdup (priv->name);
+ }
+ else
+ {
+ gchar *valid_app_name = g_strdelimit (g_strdup (g_get_prgname ()), " .-,=+~`/", '_');
+
+ owner_name = g_strdup_printf ("%s.%s",
+ ICON_NAME,
+ valid_app_name);
+ g_free (valid_app_name);
+ }
+
+ g_strfreev (name_parts);
+
+ if (name_owner_id == 0)
+ {
+ DEBUG ("Attempting to own name on bus '%s'", owner_name);
+ name_owner_id = g_bus_own_name_on_connection (priv->connection,
+ owner_name,
+ G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE,
+ on_name_acquired,
+ on_name_lost,
+ NULL,
+ NULL);
+ }
+
+ g_free (owner_name);
+}
+
+static void
+on_list_names_completed (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ XAppStatusIcon *self = XAPP_STATUS_ICON(user_data);
+ GVariant *result;
+ GVariantIter *iter;
+ gchar *str;
+ GError *error;
+ gboolean found;
+
+ error = NULL;
+
+ result = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source),
+ res,
+ &error);
+
+ if (error != NULL)
+ {
+ if (error->code != G_IO_ERROR_CANCELLED)
+ {
+ g_critical ("XAppStatusIcon: attempt to ListNames failed: %s\n", error->message);
+ // Fall back to GtkStatusIcon - signal to switch backends
+ refresh_icon (self);
+ }
+ else
+ {
+ DEBUG ("Attempt to ListNames cancelled");
+ }
+
+ g_error_free (error);
+ return;
+ }
+
+ g_variant_get (result, "(as)", &iter);
+
+ found = FALSE;
+
+ while (g_variant_iter_loop (iter, "s", &str))
+ {
+ if (g_str_has_prefix (str, STATUS_ICON_MONITOR_MATCH))
+ {
+ DEBUG ("Discovered active status monitor (%s)", str);
+ found = TRUE;
+ }
+ }
+
+ g_variant_iter_free (iter);
+ g_variant_unref (result);
+
+ if (found && export_icon_interface (self))
+ {
+ if (name_owner_id > 0)
+ {
+ sync_skeleton (self);
+ }
+ else
+ {
+ connect_with_status_applet (self);
+ return;
+ }
+ }
+ else
+ {
+ // No monitor found, signal to switch backends
+ refresh_icon (self);
+ }
+}
+
+static void
+look_for_status_applet (XAppStatusIcon *self)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
+
+ DEBUG("Looking for status monitors");
+
+ cancellable_reset (self);
+
+ g_dbus_connection_call (priv->connection,
+ FDO_DBUS_NAME,
+ FDO_DBUS_PATH,
+ FDO_DBUS_NAME,
+ "ListNames",
+ NULL,
+ G_VARIANT_TYPE ("(as)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ 3000, /* 3 secs */
+ priv->cancellable,
+ on_list_names_completed,
+ self);
+}
+
+/* Backend operations implementations */
+
+gboolean
+native_backend_init (XAppStatusIcon *icon)
+{
+ DEBUG("Native backend init - looking for status applet");
+ look_for_status_applet (icon);
+ return TRUE;
+}
+
+void
+native_backend_cleanup (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ DEBUG("Native backend cleanup");
+
+ if (priv->object_skeleton)
+ {
+ const gchar *path;
+ path = g_dbus_object_get_object_path (G_DBUS_OBJECT (priv->object_skeleton));
+
+ DEBUG ("Removing interface at path '%s'", path);
+
+ g_object_set_data (G_OBJECT (priv->object_skeleton), "xapp-status-icon-instance", NULL);
+ g_dbus_object_manager_server_unexport (obj_server, path);
+
+ priv->interface_skeleton = NULL;
+ priv->object_skeleton = NULL;
+
+ g_object_unref (obj_server);
+ }
+}
+
+void
+native_backend_sync (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ DEBUG("Native backend sync");
+
+ if (priv->interface_skeleton)
+ {
+ sync_skeleton (icon);
+ }
+}
+
+void
+native_backend_set_icon_name (XAppStatusIcon *icon, const gchar *icon_name)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (priv->interface_skeleton)
+ {
+ xapp_status_icon_interface_set_icon_name (priv->interface_skeleton, icon_name);
+ }
+}
+
+void
+native_backend_set_tooltip (XAppStatusIcon *icon, const gchar *tooltip)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (priv->interface_skeleton)
+ {
+ xapp_status_icon_interface_set_tooltip_text (priv->interface_skeleton, tooltip);
+ }
+}
+
+void
+native_backend_set_visible (XAppStatusIcon *icon, gboolean visible)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (priv->interface_skeleton)
+ {
+ xapp_status_icon_interface_set_visible (priv->interface_skeleton, visible);
+ }
+}
+
+void
+native_backend_set_label (XAppStatusIcon *icon, const gchar *label)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (priv->interface_skeleton)
+ {
+ xapp_status_icon_interface_set_label (priv->interface_skeleton, label);
+ }
+}
+
+/* Backend operations structure */
+XAppBackend native_backend_ops = {
+ .type = XAPP_BACKEND_NATIVE,
+ .init = native_backend_init,
+ .cleanup = native_backend_cleanup,
+ .sync = native_backend_sync,
+ .set_icon_name = native_backend_set_icon_name,
+ .set_tooltip = native_backend_set_tooltip,
+ .set_visible = native_backend_set_visible,
+ .set_label = native_backend_set_label,
+};
diff --git a/libxapp/xapp-status-icon-private.h b/libxapp/xapp-status-icon-private.h
new file mode 100644
index 00000000..a065e2cf
--- /dev/null
+++ b/libxapp/xapp-status-icon-private.h
@@ -0,0 +1,158 @@
+/* xapp-status-icon-private.h
+ *
+ * Private structures and functions shared across XAppStatusIcon implementations
+ */
+
+#ifndef __XAPP_STATUS_ICON_PRIVATE_H__
+#define __XAPP_STATUS_ICON_PRIVATE_H__
+
+#include
+#include
+#include
+#include "xapp-status-icon.h"
+#include "xapp-status-icon-backend.h"
+#include "xapp-statusicon-interface.h"
+#include "sn-item-interface.h"
+
+G_BEGIN_DECLS
+
+/* Global state shared across all icons */
+extern XAppStatusIconState process_icon_state;
+
+/* Signal indices */
+enum
+{
+ SIGNAL_BUTTON_PRESS,
+ SIGNAL_BUTTON_RELEASE,
+ SIGNAL_ACTIVATE,
+ SIGNAL_STATE_CHANGED,
+ SIGNAL_SCROLL,
+ SIGNAL_LAST
+};
+
+extern guint status_icon_signals[SIGNAL_LAST];
+
+/**
+ * XAppStatusIconPrivate:
+ *
+ * Private structure containing all state for a status icon instance.
+ * This is shared across all backend implementations.
+ */
+typedef struct
+{
+ /* D-Bus connection (shared) */
+ GDBusConnection *connection;
+ GCancellable *cancellable;
+
+ /* Backend management */
+ XAppBackendType active_backend;
+ XAppBackend *backend_ops;
+ gboolean sni_attempted; /* Whether SNI backend was tried */
+
+ /* Native backend state (org.x.StatusIcon) */
+ XAppStatusIconInterface *interface_skeleton;
+ XAppObjectSkeleton *object_skeleton;
+
+ /* SNI backend state (org.kde.StatusNotifierItem) */
+ SnItemInterface *sni_skeleton;
+ gchar *sni_item_path;
+ gchar *dbusmenu_path;
+ DbusmenuServer *dbusmenu_server;
+ gboolean sni_registered;
+ guint sni_watcher_watch_id;
+
+ /* Fallback backend state (GtkStatusIcon) */
+ GtkStatusIcon *gtk_status_icon;
+
+ /* Menus (shared by all backends) */
+ GtkWidget *primary_menu;
+ GtkWidget *secondary_menu;
+
+ /* Common icon properties */
+ gchar *name;
+ gchar *icon_name;
+ gchar *tooltip_text;
+ gchar *label;
+ gboolean visible;
+ gint icon_size;
+ gchar *metadata;
+
+ /* State tracking */
+ guint listener_id;
+ gint fail_counter;
+ gboolean have_button_press;
+} XAppStatusIconPrivate;
+
+struct _XAppStatusIcon
+{
+ GObject parent_instance;
+};
+
+/* Structure accessors */
+/* The G_DEFINE_TYPE_WITH_PRIVATE macro generates an inline static version of
+ * xapp_status_icon_get_instance_private that's only available in xapp-status-icon.c.
+ * We provide _xapp_status_icon_get_priv as a non-inline wrapper that other
+ * compilation units can link against. */
+XAppStatusIconPrivate *_xapp_status_icon_get_priv(XAppStatusIcon *self);
+
+#define XAPP_STATUS_ICON_GET_PRIVATE(obj) \
+ (_xapp_status_icon_get_priv((XAppStatusIcon *) (obj)))
+
+/* Common utility functions used across backends */
+const gchar *panel_position_to_str(GtkPositionType type);
+const gchar *button_to_str(guint button);
+const gchar *state_to_str(XAppStatusIconState state);
+const gchar *scroll_direction_to_str(XAppScrollDirection direction);
+
+void cancellable_reset(XAppStatusIcon *self);
+
+/* Menu handling */
+void popup_menu(XAppStatusIcon *self,
+ GtkMenu *menu,
+ gint x,
+ gint y,
+ guint button,
+ guint _time,
+ gint panel_position);
+
+gboolean should_send_activate(guint button, gboolean have_button_press);
+GtkMenu *get_menu_to_use(XAppStatusIcon *self, guint button);
+
+/* Signal emission helpers */
+void emit_button_press(XAppStatusIcon *self,
+ gint x, gint y,
+ guint button,
+ guint time,
+ gint panel_position);
+
+void emit_button_release(XAppStatusIcon *self,
+ gint x, gint y,
+ guint button,
+ guint time,
+ gint panel_position);
+
+void emit_activate(XAppStatusIcon *self,
+ guint button,
+ guint time);
+
+void emit_scroll(XAppStatusIcon *self,
+ gint delta,
+ XAppScrollDirection direction,
+ guint time);
+
+void emit_state_changed(XAppStatusIcon *self,
+ XAppStatusIconState state);
+
+/* Backend selection and switching */
+void refresh_icon(XAppStatusIcon *self);
+void switch_to_backend(XAppStatusIcon *self, XAppBackendType new_backend);
+
+/* Name ownership */
+const gchar *get_process_bus_name(XAppStatusIcon *self);
+
+/* SNI backend specific functions */
+void sni_backend_export_menu(XAppStatusIcon *self);
+
+G_END_DECLS
+
+#endif /* __XAPP_STATUS_ICON_PRIVATE_H__ */
diff --git a/libxapp/xapp-status-icon-sni.c b/libxapp/xapp-status-icon-sni.c
new file mode 100644
index 00000000..8d3f3173
--- /dev/null
+++ b/libxapp/xapp-status-icon-sni.c
@@ -0,0 +1,673 @@
+/* xapp-status-icon-sni.c
+ *
+ * StatusNotifier backend for XAppStatusIcon (org.kde.StatusNotifierItem)
+ * Provides support for KDE/GNOME and other desktops using StatusNotifier protocol
+ */
+
+#include
+#include
+#include
+
+#include "xapp-status-icon.h"
+#include "xapp-status-icon-private.h"
+#include "xapp-status-icon-backend.h"
+#include "sn-item-interface.h"
+
+#define DEBUG_FLAG XAPP_DEBUG_STATUS_ICON
+#include "xapp-debug.h"
+
+/* Constants */
+#define SNI_WATCHER_BUS_NAME "org.kde.StatusNotifierWatcher"
+#define SNI_WATCHER_OBJECT_PATH "/StatusNotifierWatcher"
+#define SNI_WATCHER_INTERFACE "org.kde.StatusNotifierWatcher"
+
+/* Global variables from main file */
+extern XAppStatusIconState process_icon_state;
+
+/* Forward declarations */
+static void register_with_sni_watcher (XAppStatusIcon *icon);
+static void on_sni_registration_complete (GObject *source, GAsyncResult *res, gpointer user_data);
+static void on_sni_watcher_appeared (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data);
+static void on_sni_watcher_vanished (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data);
+
+/* Method handlers */
+static gboolean handle_sni_activate (SnItemInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint x, gint y,
+ XAppStatusIcon *icon);
+static gboolean handle_sni_secondary_activate (SnItemInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint x, gint y,
+ XAppStatusIcon *icon);
+static gboolean handle_sni_context_menu (SnItemInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint x, gint y,
+ XAppStatusIcon *icon);
+static gboolean handle_sni_scroll (SnItemInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint delta,
+ const gchar *direction_str,
+ XAppStatusIcon *icon);
+
+/* Menu integration */
+void sni_backend_export_menu (XAppStatusIcon *icon);
+
+/* SNI Backend Core Implementation */
+
+static void
+on_sni_registration_complete (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ XAppStatusIcon *icon = XAPP_STATUS_ICON(user_data);
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+ GError *error = NULL;
+
+ GVariant *result = g_dbus_connection_call_finish (G_DBUS_CONNECTION(source), res, &error);
+
+ if (error != NULL)
+ {
+ if (error->code != G_IO_ERROR_CANCELLED)
+ {
+ g_critical ("SNI: Failed to register with StatusNotifierWatcher: %s", error->message);
+ priv->sni_registered = FALSE;
+ }
+ g_error_free (error);
+ return;
+ }
+
+ g_variant_unref (result);
+ priv->sni_registered = TRUE;
+
+ DEBUG("SNI: Successfully registered with StatusNotifierWatcher");
+
+ /* Update state */
+ process_icon_state = XAPP_STATUS_ICON_STATE_NATIVE;
+ emit_state_changed (icon, process_icon_state);
+}
+
+static void
+register_with_sni_watcher (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ /* The watcher expects either:
+ * - A path starting with "/" (it will use the sender as bus name)
+ * - A bus name (it will use "/StatusNotifierItem" as path)
+ * We send the path, and the watcher extracts our bus name from the method invocation sender.
+ */
+ DEBUG("SNI: Registering item with watcher at path: %s", priv->sni_item_path);
+
+ g_dbus_connection_call (priv->connection,
+ SNI_WATCHER_BUS_NAME,
+ SNI_WATCHER_OBJECT_PATH,
+ SNI_WATCHER_INTERFACE,
+ "RegisterStatusNotifierItem",
+ g_variant_new ("(s)", priv->sni_item_path),
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ priv->cancellable,
+ on_sni_registration_complete,
+ icon);
+}
+
+/* Watcher Monitoring */
+
+static void
+start_sni_watcher_monitoring (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (priv->sni_watcher_watch_id != 0)
+ {
+ return; /* Already watching */
+ }
+
+ DEBUG("SNI: Starting watcher monitoring");
+
+ priv->sni_watcher_watch_id = g_bus_watch_name_on_connection (
+ priv->connection,
+ SNI_WATCHER_BUS_NAME,
+ G_BUS_NAME_WATCHER_FLAGS_NONE,
+ on_sni_watcher_appeared,
+ on_sni_watcher_vanished,
+ icon,
+ NULL);
+}
+
+static void
+on_sni_watcher_appeared (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ XAppStatusIcon *icon = XAPP_STATUS_ICON(user_data);
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ DEBUG("SNI: StatusNotifierWatcher appeared: %s", name_owner);
+
+ /* Only switch if we're currently in fallback mode */
+ if (priv->active_backend == XAPP_BACKEND_FALLBACK)
+ {
+ DEBUG("SNI: Switching from fallback to SNI backend");
+ switch_to_backend (icon, XAPP_BACKEND_SNI);
+ }
+}
+
+static void
+on_sni_watcher_vanished (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ XAppStatusIcon *icon = XAPP_STATUS_ICON(user_data);
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ DEBUG("SNI: StatusNotifierWatcher vanished");
+
+ /* If we were using SNI backend, fall back to GtkStatusIcon */
+ if (priv->active_backend == XAPP_BACKEND_SNI)
+ {
+ DEBUG("SNI: Switching to fallback backend");
+ switch_to_backend (icon, XAPP_BACKEND_FALLBACK);
+ }
+}
+
+/* SNI Method Handlers */
+
+static gboolean
+handle_sni_activate (SnItemInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint x, gint y,
+ XAppStatusIcon *icon)
+{
+ DEBUG("SNI: Activate at %d,%d", x, y);
+
+ emit_button_press (icon, x, y, GDK_BUTTON_PRIMARY, GDK_CURRENT_TIME, GTK_POS_TOP);
+ emit_button_release (icon, x, y, GDK_BUTTON_PRIMARY, GDK_CURRENT_TIME, GTK_POS_TOP);
+
+ if (should_send_activate (GDK_BUTTON_PRIMARY, TRUE))
+ {
+ emit_activate (icon, GDK_BUTTON_PRIMARY, GDK_CURRENT_TIME);
+ }
+
+ sn_item_interface_complete_activate (skeleton, invocation);
+ return TRUE;
+}
+
+static gboolean
+handle_sni_secondary_activate (SnItemInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint x, gint y,
+ XAppStatusIcon *icon)
+{
+ DEBUG("SNI: SecondaryActivate at %d,%d", x, y);
+
+ emit_button_press (icon, x, y, GDK_BUTTON_MIDDLE, GDK_CURRENT_TIME, GTK_POS_TOP);
+ emit_button_release (icon, x, y, GDK_BUTTON_MIDDLE, GDK_CURRENT_TIME, GTK_POS_TOP);
+
+ if (should_send_activate (GDK_BUTTON_MIDDLE, TRUE))
+ {
+ emit_activate (icon, GDK_BUTTON_MIDDLE, GDK_CURRENT_TIME);
+ }
+
+ sn_item_interface_complete_secondary_activate (skeleton, invocation);
+ return TRUE;
+}
+
+static gboolean
+handle_sni_context_menu (SnItemInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint x, gint y,
+ XAppStatusIcon *icon)
+{
+ DEBUG("SNI: ContextMenu at %d,%d", x, y);
+
+ emit_button_press (icon, x, y, GDK_BUTTON_SECONDARY, GDK_CURRENT_TIME, GTK_POS_TOP);
+ emit_button_release (icon, x, y, GDK_BUTTON_SECONDARY, GDK_CURRENT_TIME, GTK_POS_TOP);
+
+ if (should_send_activate (GDK_BUTTON_SECONDARY, TRUE))
+ {
+ emit_activate (icon, GDK_BUTTON_SECONDARY, GDK_CURRENT_TIME);
+ }
+
+ sn_item_interface_complete_context_menu (skeleton, invocation);
+ return TRUE;
+}
+
+static gboolean
+handle_sni_scroll (SnItemInterface *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint delta,
+ const gchar *direction_str,
+ XAppStatusIcon *icon)
+{
+ DEBUG("SNI: Scroll: delta=%d, direction=%s", delta, direction_str);
+
+ XAppScrollDirection direction;
+
+ if (g_strcmp0 (direction_str, "vertical") == 0)
+ {
+ direction = delta > 0 ? XAPP_SCROLL_DOWN : XAPP_SCROLL_UP;
+ }
+ else
+ {
+ direction = delta > 0 ? XAPP_SCROLL_RIGHT : XAPP_SCROLL_LEFT;
+ }
+
+ emit_scroll (icon, abs (delta), direction, GDK_CURRENT_TIME);
+
+ sn_item_interface_complete_scroll (skeleton, invocation);
+ return TRUE;
+}
+
+/* DBusMenu Integration */
+
+void
+sni_backend_export_menu (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (!priv->sni_skeleton)
+ {
+ return;
+ }
+
+ DEBUG("SNI: Exporting DBusMenu");
+
+ /* Clean up existing server */
+ if (priv->dbusmenu_server)
+ {
+ g_object_unref (priv->dbusmenu_server);
+ priv->dbusmenu_server = NULL;
+ }
+
+ /* Determine which menu to export - prefer primary */
+ GtkMenu *gtk_menu = NULL;
+ if (priv->primary_menu)
+ {
+ gtk_menu = GTK_MENU(priv->primary_menu);
+ }
+ else if (priv->secondary_menu)
+ {
+ gtk_menu = GTK_MENU(priv->secondary_menu);
+ }
+
+ if (gtk_menu)
+ {
+ /* Create new DBusMenu server */
+ priv->dbusmenu_server = dbusmenu_server_new (priv->dbusmenu_path);
+
+ /* Convert GtkMenu to DbusmenuMenuitem hierarchy */
+ DbusmenuMenuitem *root = dbusmenu_gtk_parse_menu_structure (gtk_menu);
+ dbusmenu_server_set_root (priv->dbusmenu_server, root);
+ g_object_unref (root);
+
+ /* Update SNI Menu property */
+ sn_item_interface_set_menu (priv->sni_skeleton, priv->dbusmenu_path);
+
+ DEBUG("SNI: Menu exported at %s", priv->dbusmenu_path);
+ }
+ else
+ {
+ /* No menu available - set to empty path */
+ sn_item_interface_set_menu (priv->sni_skeleton, "/");
+ DEBUG("SNI: No menu to export");
+ }
+
+ sn_item_interface_emit_new_menu (priv->sni_skeleton);
+}
+
+gboolean
+sni_backend_init (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+ static gint icon_counter = 0;
+
+ DEBUG("SNI backend init");
+
+ /* Generate unique icon ID and paths */
+ gchar *icon_id = g_strdup_printf ("icon_%d", icon_counter++);
+ priv->sni_item_path = g_strdup_printf ("/StatusNotifierItem/%s", icon_id);
+ priv->dbusmenu_path = g_strdup_printf ("/MenuBar/%s", icon_id);
+ g_free (icon_id);
+
+ DEBUG("SNI: Creating interface at path %s", priv->sni_item_path);
+
+ /* Create StatusNotifierItem interface skeleton */
+ priv->sni_skeleton = sn_item_interface_skeleton_new ();
+
+ /* Set initial properties */
+ sn_item_interface_set_id (priv->sni_skeleton, priv->name);
+ sn_item_interface_set_category (priv->sni_skeleton, "ApplicationStatus");
+ sn_item_interface_set_status (priv->sni_skeleton, "Active");
+ sn_item_interface_set_icon_name (priv->sni_skeleton, "");
+ sn_item_interface_set_icon_theme_path (priv->sni_skeleton, "");
+ sn_item_interface_set_menu (priv->sni_skeleton, priv->dbusmenu_path);
+ sn_item_interface_set_title (priv->sni_skeleton, "");
+
+ /* Set empty pixmap arrays */
+ GVariant *empty_pixmap = g_variant_new_array (G_VARIANT_TYPE("(iiay)"), NULL, 0);
+ sn_item_interface_set_icon_pixmap (priv->sni_skeleton, empty_pixmap);
+ sn_item_interface_set_overlay_icon_pixmap (priv->sni_skeleton,
+ g_variant_new_array (G_VARIANT_TYPE("(iiay)"), NULL, 0));
+ sn_item_interface_set_attention_icon_pixmap (priv->sni_skeleton,
+ g_variant_new_array (G_VARIANT_TYPE("(iiay)"), NULL, 0));
+
+ /* Set empty tooltip */
+ GVariant *empty_tooltip = g_variant_new ("(s@a(iiay)ss)",
+ "",
+ g_variant_new_array (G_VARIANT_TYPE("(iiay)"), NULL, 0),
+ "",
+ "");
+ sn_item_interface_set_tool_tip (priv->sni_skeleton, empty_tooltip);
+
+ /* Set XAyatana label extension (Ubuntu/Ayatana indicators) */
+ sn_item_interface_set_xayatana_label (priv->sni_skeleton, "");
+ sn_item_interface_set_xayatana_label_guide (priv->sni_skeleton, "");
+
+ /* Connect method handlers */
+ g_signal_connect (priv->sni_skeleton, "handle-activate",
+ G_CALLBACK(handle_sni_activate), icon);
+ g_signal_connect (priv->sni_skeleton, "handle-secondary-activate",
+ G_CALLBACK(handle_sni_secondary_activate), icon);
+ g_signal_connect (priv->sni_skeleton, "handle-context-menu",
+ G_CALLBACK(handle_sni_context_menu), icon);
+ g_signal_connect (priv->sni_skeleton, "handle-scroll",
+ G_CALLBACK(handle_sni_scroll), icon);
+
+ /* Export to D-Bus */
+ GError *error = NULL;
+ g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON(priv->sni_skeleton),
+ priv->connection,
+ priv->sni_item_path,
+ &error);
+
+ if (error)
+ {
+ g_critical ("SNI: Failed to export interface: %s", error->message);
+ g_error_free (error);
+ g_clear_object (&priv->sni_skeleton);
+ g_free (priv->sni_item_path);
+ g_free (priv->dbusmenu_path);
+ priv->sni_item_path = NULL;
+ priv->dbusmenu_path = NULL;
+ return FALSE;
+ }
+
+ DEBUG("SNI: Interface exported successfully");
+
+ /* Register with StatusNotifierWatcher */
+ register_with_sni_watcher (icon);
+
+ /* Export menus if present */
+ sni_backend_export_menu (icon);
+
+ /* Start monitoring watcher for dynamic switching */
+ start_sni_watcher_monitoring (icon);
+
+ return TRUE;
+}
+
+void
+sni_backend_cleanup (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ DEBUG("SNI backend cleanup");
+
+ /* Unregistration happens automatically when we disconnect from D-Bus */
+
+ /* Unexport interface */
+ if (priv->sni_skeleton)
+ {
+ g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON(priv->sni_skeleton));
+ g_clear_object (&priv->sni_skeleton);
+ }
+
+ /* Clean up DBusMenu */
+ if (priv->dbusmenu_server)
+ {
+ g_object_unref (priv->dbusmenu_server);
+ priv->dbusmenu_server = NULL;
+ }
+
+ g_clear_pointer (&priv->sni_item_path, g_free);
+ g_clear_pointer (&priv->dbusmenu_path, g_free);
+
+ /* Stop watcher monitoring */
+ if (priv->sni_watcher_watch_id != 0)
+ {
+ g_bus_unwatch_name (priv->sni_watcher_watch_id);
+ priv->sni_watcher_watch_id = 0;
+ }
+
+ priv->sni_registered = FALSE;
+}
+
+/* Helper function to convert GdkPixbuf to SNI pixmap format (ARGB) */
+static GVariant *
+pixbuf_to_sni_pixmap_array (GdkPixbuf *pixbuf)
+{
+ gint width = gdk_pixbuf_get_width (pixbuf);
+ gint height = gdk_pixbuf_get_height (pixbuf);
+ gint rowstride = gdk_pixbuf_get_rowstride (pixbuf);
+ gint channels = gdk_pixbuf_get_n_channels (pixbuf);
+ gboolean has_alpha = gdk_pixbuf_get_has_alpha (pixbuf);
+ guchar *pixels = gdk_pixbuf_get_pixels (pixbuf);
+
+ /* Allocate ARGB buffer */
+ gsize data_size = width * height * 4;
+ guchar *argb_data = g_malloc (data_size);
+
+ /* Convert RGBA/RGB to ARGB (network byte order) */
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ guchar *src = pixels + y * rowstride + x * channels;
+ guchar *dst = argb_data + (y * width + x) * 4;
+
+ if (has_alpha)
+ {
+ dst[0] = src[3]; /* A */
+ dst[1] = src[0]; /* R */
+ dst[2] = src[1]; /* G */
+ dst[3] = src[2]; /* B */
+ }
+ else
+ {
+ dst[0] = 0xFF; /* A (opaque) */
+ dst[1] = src[0]; /* R */
+ dst[2] = src[1]; /* G */
+ dst[3] = src[2]; /* B */
+ }
+ }
+ }
+
+ GVariant *byte_array = g_variant_new_from_data (
+ G_VARIANT_TYPE("ay"),
+ argb_data,
+ data_size,
+ TRUE,
+ g_free,
+ argb_data);
+
+ GVariantBuilder builder;
+ g_variant_builder_init (&builder, G_VARIANT_TYPE("a(iiay)"));
+ g_variant_builder_add (&builder, "(ii@ay)", width, height, byte_array);
+
+ return g_variant_builder_end (&builder);
+}
+
+void
+sni_backend_set_icon_name (XAppStatusIcon *icon, const gchar *icon_name)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (!priv->sni_skeleton)
+ {
+ return;
+ }
+
+ DEBUG("SNI: set_icon_name: %s", icon_name ? icon_name : "(null)");
+
+ if (!icon_name || *icon_name == '\0')
+ {
+ /* Clear icon */
+ sn_item_interface_set_icon_name (priv->sni_skeleton, "");
+ sn_item_interface_set_icon_pixmap (priv->sni_skeleton,
+ g_variant_new_array (G_VARIANT_TYPE("(iiay)"), NULL, 0));
+ sn_item_interface_emit_new_icon (priv->sni_skeleton);
+ return;
+ }
+
+ if (g_path_is_absolute (icon_name))
+ {
+ /* Load file and convert to pixmap array */
+ GError *error = NULL;
+ gint icon_size = priv->icon_size > 0 ? priv->icon_size : 24;
+
+ GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file_at_scale (
+ icon_name,
+ icon_size,
+ icon_size,
+ TRUE,
+ &error);
+
+ if (error)
+ {
+ g_warning ("SNI: Failed to load icon from %s: %s", icon_name, error->message);
+ g_error_free (error);
+ /* Set empty icon */
+ sn_item_interface_set_icon_name (priv->sni_skeleton, "");
+ sn_item_interface_set_icon_pixmap (priv->sni_skeleton,
+ g_variant_new_array (G_VARIANT_TYPE("(iiay)"), NULL, 0));
+ }
+ else
+ {
+ GVariant *pixmap = pixbuf_to_sni_pixmap_array (pixbuf);
+ sn_item_interface_set_icon_pixmap (priv->sni_skeleton, pixmap);
+ sn_item_interface_set_icon_name (priv->sni_skeleton, "");
+ g_object_unref (pixbuf);
+ }
+ }
+ else
+ {
+ /* Use theme icon name */
+ sn_item_interface_set_icon_name (priv->sni_skeleton, icon_name);
+ /* Clear pixmap array */
+ sn_item_interface_set_icon_pixmap (priv->sni_skeleton,
+ g_variant_new_array (G_VARIANT_TYPE("(iiay)"), NULL, 0));
+ }
+
+ sn_item_interface_emit_new_icon (priv->sni_skeleton);
+}
+
+void
+sni_backend_set_tooltip (XAppStatusIcon *icon, const gchar *tooltip)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (!priv->sni_skeleton)
+ {
+ return;
+ }
+
+ DEBUG("SNI: set_tooltip: %s", tooltip ? tooltip : "(null)");
+
+ /* ToolTip format: (sa (iiay)ss)
+ * - icon name (string)
+ * - icon pixmap array (empty)
+ * - title (string) - we use this for the tooltip text
+ * - body (string) - empty
+ */
+ GVariant *empty_pixmap = g_variant_new_array (G_VARIANT_TYPE("(iiay)"), NULL, 0);
+
+ GVariant *tooltip_struct = g_variant_new (
+ "(s@a(iiay)ss)",
+ "", /* icon name */
+ empty_pixmap, /* icon pixmap */
+ tooltip ? tooltip : "", /* title */
+ "" /* body (empty) */
+ );
+
+ sn_item_interface_set_tool_tip (priv->sni_skeleton, tooltip_struct);
+ sn_item_interface_emit_new_tool_tip (priv->sni_skeleton);
+}
+
+void
+sni_backend_set_visible (XAppStatusIcon *icon, gboolean visible)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (!priv->sni_skeleton)
+ {
+ return;
+ }
+
+ DEBUG("SNI: set_visible: %s", visible ? "TRUE" : "FALSE");
+
+ /* SNI uses Status property: "Active", "Passive", or "NeedsAttention" */
+ const gchar *status = visible ? "Active" : "Passive";
+ sn_item_interface_set_status (priv->sni_skeleton, status);
+ sn_item_interface_emit_new_status (priv->sni_skeleton, status);
+}
+
+void
+sni_backend_set_label (XAppStatusIcon *icon, const gchar *label)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (!priv->sni_skeleton)
+ {
+ return;
+ }
+
+ DEBUG("SNI: set_label: %s", label ? label : "(null)");
+
+ const gchar *label_str = label ? label : "";
+
+ /* SNI uses Title property for label text */
+ sn_item_interface_set_title (priv->sni_skeleton, label_str);
+ sn_item_interface_emit_new_title (priv->sni_skeleton);
+
+ /* Also set XAyatana label extension for Ubuntu/Ayatana indicators */
+ sn_item_interface_set_xayatana_label (priv->sni_skeleton, label_str);
+ sn_item_interface_emit_xayatana_new_label (priv->sni_skeleton, label_str, "");
+}
+
+void
+sni_backend_sync (XAppStatusIcon *icon)
+{
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(icon);
+
+ if (!priv->sni_skeleton)
+ {
+ return;
+ }
+
+ DEBUG("SNI: Syncing all properties");
+
+ /* Sync all properties to the SNI interface */
+ sni_backend_set_icon_name (icon, priv->icon_name);
+ sni_backend_set_tooltip (icon, priv->tooltip_text);
+ sni_backend_set_visible (icon, priv->visible);
+ sni_backend_set_label (icon, priv->label);
+ sni_backend_export_menu (icon);
+}
+
+/* Backend operations structure */
+XAppBackend sni_backend_ops = {
+ .type = XAPP_BACKEND_SNI,
+ .init = sni_backend_init,
+ .cleanup = sni_backend_cleanup,
+ .sync = sni_backend_sync,
+ .set_icon_name = sni_backend_set_icon_name,
+ .set_tooltip = sni_backend_set_tooltip,
+ .set_visible = sni_backend_set_visible,
+ .set_label = sni_backend_set_label,
+};
diff --git a/libxapp/xapp-status-icon.c b/libxapp/xapp-status-icon.c
index a3e3ef23..c27ec60f 100644
--- a/libxapp/xapp-status-icon.c
+++ b/libxapp/xapp-status-icon.c
@@ -13,6 +13,8 @@
#include
#include "xapp-status-icon.h"
+#include "xapp-status-icon-private.h"
+#include "xapp-status-icon-backend.h"
#include "xapp-statusicon-interface.h"
#include "xapp-enums.h"
@@ -35,24 +37,10 @@
// This gets reffed and unreffed according to individual icon presence.
// For the first icon, it gets created when exporting the icon's interface.
-// For each additional icon, it gets reffed again. On destruction, the
-// opposite occurs - unrefs, and the final unref calls a weak notify
-// function, which clears this pointer and unown's the process's bus name.
-static GDBusObjectManagerServer *obj_server = NULL;
-static guint name_owner_id = 0;
XAppStatusIconState process_icon_state = XAPP_STATUS_ICON_STATE_NO_SUPPORT;
-enum
-{
- BUTTON_PRESS,
- BUTTON_RELEASE,
- ACTIVATE,
- STATE_CHANGED,
- SCROLL,
- LAST_SIGNAL
-};
-
-static guint signals[LAST_SIGNAL] = {0, };
+/* Signal array - exported for backend files */
+guint status_icon_signals[SIGNAL_LAST] = {0, };
enum
{
@@ -76,675 +64,27 @@ enum
* If used in an environment where no applet is handling XAppStatusIcons,
* the XAppStatusIcon delegates its calls to a Gtk.StatusIcon.
*/
-typedef struct
-{
- GDBusConnection *connection;
- XAppStatusIconInterface *interface_skeleton;
- XAppObjectSkeleton *object_skeleton;
-
- GCancellable *cancellable;
-
- GtkStatusIcon *gtk_status_icon;
- GtkWidget *primary_menu;
- GtkWidget *secondary_menu;
-
- gchar *name;
- gchar *icon_name;
- gchar *tooltip_text;
- gchar *label;
- gboolean visible;
- gint icon_size;
- gchar *metadata;
-
- guint listener_id;
-
- gint fail_counter;
- gboolean have_button_press;
-} XAppStatusIconPrivate;
-
-struct _XAppStatusIcon
-{
- GObject parent_instance;
- XAppStatusIconPrivate *priv;
-};
-
G_DEFINE_TYPE_WITH_PRIVATE (XAppStatusIcon, xapp_status_icon, G_TYPE_OBJECT)
-static void refresh_icon (XAppStatusIcon *self);
-static void use_gtk_status_icon (XAppStatusIcon *self);
-static void remove_icon_path_from_bus (XAppStatusIcon *self);
-
-static void
-cancellable_reset (XAppStatusIcon *self)
-{
- if (self->priv->cancellable)
- {
- g_cancellable_cancel (self->priv->cancellable);
- g_object_unref (self->priv->cancellable);
- }
-
- self->priv->cancellable = g_cancellable_new ();
-}
-
-static const gchar *
-panel_position_to_str (GtkPositionType type)
-{
- switch (type)
- {
- case GTK_POS_LEFT:
- return "Left";
- case GTK_POS_RIGHT:
- return "Right";
- case GTK_POS_TOP:
- return "Top";
- case GTK_POS_BOTTOM:
- default:
- return "Bottom";
- }
-}
-
-static const gchar *
-button_to_str (guint button)
-{
- switch (button)
- {
- case GDK_BUTTON_PRIMARY:
- return "Left";
- case GDK_BUTTON_SECONDARY:
- return "Right";
- case GDK_BUTTON_MIDDLE:
- return "Middle";
- default:
- return "Unknown";
- }
-}
-
-static const gchar *
-state_to_str (XAppStatusIconState state)
-{
- switch (state)
- {
- case XAPP_STATUS_ICON_STATE_NATIVE:
- return "Native";
- case XAPP_STATUS_ICON_STATE_FALLBACK:
- return "Fallback";
- case XAPP_STATUS_ICON_STATE_NO_SUPPORT:
- return "NoSupport";
- default:
- return "Unknown";
- }
-}
-
-static const gchar *
-direction_to_str (XAppScrollDirection direction)
-{
- switch (direction)
- {
- case XAPP_SCROLL_UP:
- return "Up";
- case XAPP_SCROLL_DOWN:
- return "Down";
- case XAPP_SCROLL_LEFT:
- return "Left";
- case XAPP_SCROLL_RIGHT:
- return "Right";
- default:
- return "Unknown";
- }
-}
-
-static GdkEvent *
-synthesize_event (XAppStatusIcon *self,
- gint x,
- gint y,
- guint button,
- guint _time,
- gint position,
- GdkWindow **rect_window,
- GdkRectangle *win_rect,
- GdkGravity *rect_anchor,
- GdkGravity *menu_anchor)
-{
- GdkDisplay *display;
- GdkWindow *window;
- GdkSeat *seat;
- GdkWindowAttr attributes;
- gint attributes_mask;
- gint fx, fy;
-
- display = gdk_display_get_default ();
- seat = gdk_display_get_default_seat (display);
-
- switch (position)
- {
- case GTK_POS_TOP:
- fx = x;
- fy = y - self->priv->icon_size;
- *rect_anchor = GDK_GRAVITY_SOUTH_WEST;
- *menu_anchor = GDK_GRAVITY_NORTH_WEST;
- break;
- case GTK_POS_LEFT:
- fx = x - self->priv->icon_size;
- fy = y;
- *rect_anchor = GDK_GRAVITY_NORTH_EAST;
- *menu_anchor = GDK_GRAVITY_NORTH_WEST;
- break;
- case GTK_POS_RIGHT:
- fx = x;
- fy = y;
- *rect_anchor = GDK_GRAVITY_NORTH_WEST;
- *menu_anchor = GDK_GRAVITY_NORTH_EAST;
- break;
- case GTK_POS_BOTTOM:
- default:
- fx = x;
- fy = y;
- *rect_anchor = GDK_GRAVITY_NORTH_WEST;
- *menu_anchor = GDK_GRAVITY_SOUTH_WEST;
- break;
- }
-
- attributes.window_type = GDK_WINDOW_CHILD;
- win_rect->x = 0;
- win_rect->y = 0;
- win_rect->width = self->priv->icon_size;
- win_rect->height = self->priv->icon_size;
- attributes.x = fx;
- attributes.y = fy;
- attributes.width = self->priv->icon_size;
- attributes.height = self->priv->icon_size;
- attributes_mask = GDK_WA_X | GDK_WA_Y;
-
- window = gdk_window_new (NULL, &attributes, attributes_mask);
- *rect_window = window;
-
- GdkEvent *event = gdk_event_new (GDK_BUTTON_RELEASE);
- event->any.window = window;
- event->button.device = gdk_seat_get_pointer (seat);
-
- return event;
-}
-
-static void
-primary_menu_unmapped (GtkWidget *widget,
- gpointer user_data)
+/* Wrapper function to access private data from other compilation units.
+ * G_DEFINE_TYPE_WITH_PRIVATE generates an inline static version,
+ * but we need a non-inline version for the backend files to link against. */
+XAppStatusIconPrivate *
+_xapp_status_icon_get_priv (XAppStatusIcon *self)
{
- g_return_if_fail (XAPP_IS_STATUS_ICON (user_data));
- XAppStatusIcon *icon = XAPP_STATUS_ICON (user_data);
-
- DEBUG ("Primary menu unmapped");
-
- if (process_icon_state == XAPP_STATUS_ICON_STATE_NATIVE)
- {
- xapp_status_icon_interface_set_primary_menu_is_open (icon->priv->interface_skeleton, FALSE);
- }
-
- g_signal_handlers_disconnect_by_func (widget, primary_menu_unmapped, icon);
+ return xapp_status_icon_get_instance_private (self);
}
-static void
-secondary_menu_unmapped (GtkWidget *widget,
- gpointer user_data)
-{
- g_return_if_fail (XAPP_IS_STATUS_ICON (user_data));
- XAppStatusIcon *icon = XAPP_STATUS_ICON (user_data);
-
- DEBUG ("Secondary menu unmapped");
+void refresh_icon (XAppStatusIcon *self);
- if (process_icon_state == XAPP_STATUS_ICON_STATE_NATIVE)
- {
- xapp_status_icon_interface_set_secondary_menu_is_open (icon->priv->interface_skeleton, FALSE);
- }
+/* Backend declarations */
+extern XAppBackend native_backend_ops;
+extern XAppBackend sni_backend_ops;
+extern XAppBackend fallback_backend_ops;
- g_signal_handlers_disconnect_by_func (widget, secondary_menu_unmapped, icon);
-}
-
-static void
-popup_menu (XAppStatusIcon *self,
- GtkMenu *menu,
- gint x,
- gint y,
- guint button,
- guint _time,
- gint panel_position)
-{
- GdkWindow *rect_window;
- GdkEvent *event;
- GdkRectangle win_rect;
- GdkGravity rect_anchor, menu_anchor;
-
- DEBUG ("Popup menu on behalf of application");
-
- if (!gtk_widget_get_realized (GTK_WIDGET (menu)))
- {
- GtkWidget *toplevel;
- GtkStyleContext *context;
-
- gtk_widget_realize (GTK_WIDGET (menu));
- toplevel = gtk_widget_get_toplevel (GTK_WIDGET (menu));
- context = gtk_widget_get_style_context (toplevel);
-
- /* GtkMenu uses a GtkWindow as its toplevel that is explicitly set to
- * be client-decorated, and applies shadows outside the visible part of
- * the menu. They interfere with clicks on the icon while the menu is open,
- * as the invisible part takes the events instead (and this ends up doing
- * nothing). It makes the menu a littly ugly, so here's a new class name we
- * can use for themes to restore things bit if we want. Just avoid shadows. */
- gtk_style_context_remove_class (context, "csd");
- gtk_style_context_add_class (context, "xapp-status-icon-menu-window");
- }
-
- if (button == GDK_BUTTON_PRIMARY)
- {
- if (process_icon_state == XAPP_STATUS_ICON_STATE_NATIVE)
- {
- xapp_status_icon_interface_set_primary_menu_is_open (self->priv->interface_skeleton, TRUE);
- }
-
- g_signal_connect (gtk_widget_get_toplevel (GTK_WIDGET (menu)),
- "unmap",
- G_CALLBACK (primary_menu_unmapped),
- self);
- }
- else
- if (button == GDK_BUTTON_SECONDARY)
- {
- if (process_icon_state == XAPP_STATUS_ICON_STATE_NATIVE)
- {
- xapp_status_icon_interface_set_secondary_menu_is_open (self->priv->interface_skeleton, TRUE);
- }
-
- g_signal_connect (gtk_widget_get_toplevel (GTK_WIDGET (menu)),
- "unmap",
- G_CALLBACK (secondary_menu_unmapped),
- self);
- }
-
- event = synthesize_event (self,
- x, y, button, _time, panel_position,
- &rect_window, &win_rect, &rect_anchor, &menu_anchor);
-
- g_object_set_data_full (G_OBJECT (menu),
- "rect_window", rect_window,
- (GDestroyNotify) gdk_window_destroy);
-
- g_object_set (G_OBJECT (menu),
- "anchor-hints", GDK_ANCHOR_SLIDE_X | GDK_ANCHOR_SLIDE_Y |
- GDK_ANCHOR_RESIZE_X | GDK_ANCHOR_RESIZE_Y,
- NULL);
-
- gtk_menu_popup_at_rect (menu,
- rect_window,
- &win_rect,
- rect_anchor,
- menu_anchor,
- event);
-
- gdk_event_free (event);
-}
-
-static gboolean
-should_send_activate (XAppStatusIcon *icon,
- guint button)
-{
- gboolean do_activate = TRUE;
-
- switch (button)
- {
- case GDK_BUTTON_PRIMARY:
- if (icon->priv->primary_menu)
- {
- do_activate = FALSE;
- }
- break;
- case GDK_BUTTON_SECONDARY:
- if (icon->priv->secondary_menu)
- {
- do_activate = FALSE;
- }
- break;
- default:
- break;
- }
-
- return do_activate;
-}
-
-static GtkWidget *
-get_menu_to_use (XAppStatusIcon *icon,
- guint button)
-{
- GtkWidget *menu_to_use = NULL;
-
- switch (button)
- {
- case GDK_BUTTON_PRIMARY:
- menu_to_use = icon->priv->primary_menu;
- break;
- case GDK_BUTTON_SECONDARY:
- menu_to_use = icon->priv->secondary_menu;
- break;
- }
-
- return menu_to_use;
-}
-
-static gboolean
-handle_click_method (XAppStatusIconInterface *skeleton,
- GDBusMethodInvocation *invocation,
- gint x,
- gint y,
- guint button,
- guint _time,
- gint panel_position,
- XAppStatusIcon *icon)
-{
- const gchar *name = g_dbus_method_invocation_get_method_name (invocation);
-
- if (g_strcmp0 (name, "ButtonPress") == 0)
- {
- DEBUG ("Received ButtonPress from monitor %s: "
- "pos:%d,%d , button: %s , time: %u , orientation: %s",
- g_dbus_method_invocation_get_sender (invocation),
- x, y, button_to_str (button), _time, panel_position_to_str (panel_position));
-
- if (should_send_activate (icon, button))
- {
- DEBUG ("Native sending 'activate' for %s button", button_to_str (button));
- g_signal_emit (icon, signals[ACTIVATE], 0,
- button,
- _time);
- }
-
- icon->priv->have_button_press = TRUE;
-
- g_signal_emit (icon, signals[BUTTON_PRESS], 0,
- x, y,
- button,
- _time,
- panel_position);
-
- xapp_status_icon_interface_complete_button_press (skeleton,
- invocation);
- }
- else
- if (g_strcmp0 (name, "ButtonRelease") == 0)
- {
- DEBUG ("Received ButtonRelease from monitor %s: "
- "pos:%d,%d , button: %s , time: %u , orientation: %s",
- g_dbus_method_invocation_get_sender (invocation),
- x, y, button_to_str (button), _time, panel_position_to_str (panel_position));
-
- if (icon->priv->have_button_press)
- {
- GtkWidget *menu_to_use = get_menu_to_use (icon, button);
-
- if (menu_to_use)
- {
- popup_menu (icon,
- GTK_MENU (menu_to_use),
- x, y,
- button,
- _time,
- panel_position);
- }
-
- g_signal_emit (icon, signals[BUTTON_RELEASE], 0,
- x, y,
- button,
- _time,
- panel_position);
- }
-
- icon->priv->have_button_press = FALSE;
-
- xapp_status_icon_interface_complete_button_release (skeleton,
- invocation);
- }
-
- return TRUE;
-}
-
-static gboolean
-handle_scroll_method (XAppStatusIconInterface *skeleton,
- GDBusMethodInvocation *invocation,
- gint delta,
- XAppScrollDirection direction,
- guint _time,
- XAppStatusIcon *icon)
-{
- DEBUG ("Received Scroll from monitor %s: "
- "delta: %d , direction: %s , time: %u",
- g_dbus_method_invocation_get_sender (invocation),
- delta, direction_to_str (direction), _time);
-
- g_signal_emit(icon, signals[SCROLL], 0,
- delta,
- direction,
- _time);
-
- xapp_status_icon_interface_complete_scroll (skeleton,
- invocation);
-
- return TRUE;
-}
-
-static void
-calculate_gtk_status_icon_position_and_orientation (XAppStatusIcon *icon,
- GtkStatusIcon *status_icon,
- gint *x, gint *y, gint *orientation)
-{
- GdkScreen *screen;
- GdkRectangle irect;
- GtkOrientation iorientation;
- gint final_x, final_y, final_o;
-
- final_x = 0;
- final_y = 0;
- final_o = 0;
-
- if (gtk_status_icon_get_geometry (status_icon,
- &screen,
- &irect,
- &iorientation))
- {
- GdkDisplay *display = gdk_screen_get_display (screen);
- GdkMonitor *monitor;
- GdkRectangle mrect;
-
- monitor = gdk_display_get_monitor_at_point (display,
- irect.x + (irect.width / 2),
- irect.y + (irect.height / 2));
-
- gdk_monitor_get_workarea (monitor, &mrect);
-
- switch (iorientation)
- {
- case GTK_ORIENTATION_HORIZONTAL:
- final_x = irect.x;
-
- if (irect.y + irect.height + 100 < mrect.y + mrect.height)
- {
- final_y = irect.y + irect.height;
- final_o = GTK_POS_TOP;
- }
- else
- {
- final_y = irect.y;
- final_o = GTK_POS_BOTTOM;
- }
-
- break;
- case GTK_ORIENTATION_VERTICAL:
- final_y = irect.y;
-
- if (irect.x + irect.width + 100 < mrect.x + mrect.width)
- {
- final_x = irect.x + irect.width;
- final_o = GTK_POS_LEFT;
- }
- else
- {
- final_x = irect.x;
- final_o = GTK_POS_RIGHT;
- }
- }
- }
-
- *x = final_x;
- *y = final_y;
- *orientation = final_o;
-}
-
-static gboolean
-on_gtk_status_icon_button_press (GtkStatusIcon *status_icon,
- GdkEvent *event,
- gpointer user_data)
-{
- XAppStatusIcon *icon = user_data;
-
- guint _time;
- guint button;
- gint x, y, orientation;
-
- button = event->button.button;
- _time = event->button.time;
-
- DEBUG ("GtkStatusIcon button-press-event with %s button", button_to_str (button));
-
- /* We always send 'activate' for a button that has no corresponding menu,
- * and for middle clicks. */
- if (should_send_activate (icon, button))
- {
- DEBUG ("GtkStatusIcon activated by %s button", button_to_str (button));
-
- g_signal_emit (icon, signals[ACTIVATE], 0,
- button,
- _time);
- }
-
- calculate_gtk_status_icon_position_and_orientation (icon,
- status_icon,
- &x,
- &y,
- &orientation);
-
- icon->priv->have_button_press = TRUE;
-
- g_signal_emit (icon, signals[BUTTON_PRESS], 0,
- x, y,
- button,
- _time,
- orientation);
-
- return GDK_EVENT_PROPAGATE;
-}
-
-static gboolean
-on_gtk_status_icon_button_release (GtkStatusIcon *status_icon,
- GdkEvent *event,
- gpointer user_data)
-{
- XAppStatusIcon *icon = user_data;
- GtkWidget *menu_to_use;
- guint _time;
- guint button;
- gint x, y, orientation;
-
- button = event->button.button;
- _time = event->button.time;
-
- DEBUG ("GtkStatusIcon button-release-event with %s button", button_to_str (button));
-
- /* Native icons can have two menus, so we must determine which to use based
- * on the gtk icon event's button. */
-
- menu_to_use = get_menu_to_use (icon, button);
-
- calculate_gtk_status_icon_position_and_orientation (icon,
- status_icon,
- &x,
- &y,
- &orientation);
-
- if (menu_to_use)
- {
- DEBUG ("GtkStatusIcon popup menu for %s button", button_to_str (button));
-
- popup_menu (icon,
- GTK_MENU (menu_to_use),
- x,
- y,
- button,
- _time,
- orientation);
- }
-
- icon->priv->have_button_press = FALSE;
-
- g_signal_emit (icon, signals[BUTTON_RELEASE], 0,
- x, y,
- button,
- _time,
- orientation);
-
- return GDK_EVENT_PROPAGATE;
-}
-
-static gboolean
-on_gtk_status_icon_scroll (GtkStatusIcon *status_icon,
- GdkEvent *event,
- gpointer user_data)
-{
- XAppStatusIcon *icon = user_data;
- guint _time;
-
- _time = event->scroll.time;
- GdkScrollDirection direction;
-
-
- if (gdk_event_get_scroll_direction (event, &direction))
- {
- XAppScrollDirection x_dir = XAPP_SCROLL_UP;
- gint delta = 0;
-
- if (direction != GDK_SCROLL_SMOOTH) {
- if (direction == GDK_SCROLL_UP)
- {
- x_dir = XAPP_SCROLL_UP;
- delta = -1;
- }
- else if (direction == GDK_SCROLL_DOWN)
- {
- x_dir = XAPP_SCROLL_DOWN;
- delta = 1;
- }
- else if (direction == GDK_SCROLL_LEFT)
- {
- x_dir = XAPP_SCROLL_LEFT;
- delta = -1;
- }
- else if (direction == GDK_SCROLL_RIGHT)
- {
- x_dir = XAPP_SCROLL_RIGHT;
- delta = 1;
- }
- }
-
- DEBUG ("Received Scroll from GtkStatusIcon %s: "
- "delta: %d , direction: %s , time: %u",
- gtk_status_icon_get_title (status_icon),
- delta, direction_to_str (direction), _time);
-
- g_signal_emit(icon, signals[SCROLL], 0,
- delta,
- x_dir,
- _time);
- }
-
- return GDK_EVENT_PROPAGATE;
-}
+/* Common utility functions now in xapp-status-icon-common.c */
+/* Monitor for status applets appearing/disappearing on D-Bus */
static void
name_owner_changed (GDBusConnection *connection,
const gchar *sender_name,
@@ -765,7 +105,7 @@ add_name_listener (XAppStatusIcon *self)
{
DEBUG ("Adding NameOwnerChanged listener for status monitors");
- self->priv->listener_id = g_dbus_connection_signal_subscribe (self->priv->connection,
+ XAPP_STATUS_ICON_GET_PRIVATE(self)->listener_id = g_dbus_connection_signal_subscribe (XAPP_STATUS_ICON_GET_PRIVATE(self)->connection,
FDO_DBUS_NAME,
FDO_DBUS_NAME,
"NameOwnerChanged",
@@ -777,410 +117,120 @@ add_name_listener (XAppStatusIcon *self)
NULL);
}
-static void
-on_name_lost (GDBusConnection *connection,
- const gchar *name,
- gpointer user_data)
-{
- g_warning ("XAppStatusIcon: lost or could not acquire presence on dbus. Refreshing.");
-
- GList *instances = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (obj_server));
- GList *l;
-
- for (l = instances; l != NULL; l = l->next)
- {
- GObject *instance = G_OBJECT (l->data);
- XAppStatusIcon *icon = XAPP_STATUS_ICON (g_object_get_data (instance, "xapp-status-icon-instance"));
-
- if (icon == NULL)
- {
- g_warning ("on_name_lost: Could not retrieve xapp-status-icon-instance data: %s", name);
- continue;
- }
-
- icon->priv->fail_counter++;
- refresh_icon (icon);
- }
-
- g_list_free_full (instances, g_object_unref);
-}
-
-static void
-sync_skeleton (XAppStatusIcon *self)
-{
- XAppStatusIconPrivate *priv = self->priv;
- DEBUG ("Syncing icon properties (%s)", priv->name);
-
- priv->fail_counter = 0;
-
- g_clear_object (&self->priv->gtk_status_icon);
-
- g_object_set (G_OBJECT (priv->interface_skeleton),
- "name", priv->name,
- "label", priv->label,
- "icon-name", priv->icon_name,
- "tooltip-text", priv->tooltip_text,
- "visible", priv->visible,
- "metadata", priv->metadata,
- NULL);
-
- g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (priv->interface_skeleton));
-}
-
-static void
-on_name_acquired (GDBusConnection *connection,
- const gchar *name,
- gpointer user_data)
-{
- process_icon_state = XAPP_STATUS_ICON_STATE_NATIVE;
-
- GList *instances = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (obj_server));
- GList *l;
-
- for (l = instances; l != NULL; l = l->next)
- {
- GObject *instance = G_OBJECT (l->data);
- XAppStatusIcon *icon = XAPP_STATUS_ICON (g_object_get_data (instance, "xapp-status-icon-instance"));
-
- if (icon == NULL)
- {
- g_warning ("on_name_aquired: Could not retrieve xapp-status-icon-instance data: %s", name);
- continue;
- }
-
- sync_skeleton (icon);
-
- DEBUG ("Name acquired on dbus, state is now: %s",
- state_to_str (process_icon_state));
-
- g_signal_emit (icon, signals[STATE_CHANGED], 0, process_icon_state);
- }
-
- g_list_free_full (instances, g_object_unref);
-}
-
-typedef struct
-{
- const gchar *signal_name;
- gpointer callback;
-} SkeletonSignal;
-
-static SkeletonSignal skeleton_signals[] = {
- // signal name callback
- { "handle-button-press", handle_click_method },
- { "handle-button-release", handle_click_method },
- { "handle-scroll", handle_scroll_method }
-};
-
-static void
-obj_server_finalized (gpointer data,
- GObject *object)
+/* Backend switching function */
+void
+switch_to_backend (XAppStatusIcon *self, XAppBackendType backend_type)
{
- DEBUG ("Final icon removed, clearing object manager (%s)", g_get_prgname ());
+ XAppStatusIconPrivate *priv = XAPP_STATUS_ICON_GET_PRIVATE(self);
- if (name_owner_id > 0)
+ if (priv->active_backend == backend_type)
{
- g_bus_unown_name(name_owner_id);
- name_owner_id = 0;
+ DEBUG ("Already using %d backend", backend_type);
+ return;
}
- obj_server = NULL;
-}
-
-static void
-ensure_object_manager (XAppStatusIcon *self)
-{
- if (obj_server == NULL)
- {
- DEBUG ("New object manager for (%s)", g_get_prgname ());
+ DEBUG ("Switching to backend type %d", backend_type);
- obj_server = g_dbus_object_manager_server_new (ICON_BASE_PATH);
- g_dbus_object_manager_server_set_connection (obj_server, self->priv->connection);
- g_object_weak_ref (G_OBJECT (obj_server),(GWeakNotify) obj_server_finalized, self);
- }
- else
+ /* Cleanup old backend */
+ if (priv->backend_ops && priv->backend_ops->cleanup)
{
- g_object_ref (obj_server);
+ priv->backend_ops->cleanup(self);
}
-}
-
-static gboolean
-export_icon_interface (XAppStatusIcon *self)
-{
- gint i;
- ensure_object_manager (self);
+ /* Set new backend */
+ priv->active_backend = backend_type;
- if (self->priv->interface_skeleton)
+ switch (backend_type)
{
- return TRUE;
- }
-
- self->priv->object_skeleton = xapp_object_skeleton_new (ICON_SUB_PATH);
- self->priv->interface_skeleton = xapp_status_icon_interface_skeleton_new ();
-
- xapp_object_skeleton_set_status_icon_interface (self->priv->object_skeleton,
- self->priv->interface_skeleton);
-
- g_object_set_data (G_OBJECT (self->priv->object_skeleton), "xapp-status-icon-instance", self);
-
- g_dbus_object_manager_server_export_uniquely (obj_server,
- G_DBUS_OBJECT_SKELETON (self->priv->object_skeleton));
-
- g_object_unref (self->priv->object_skeleton);
- g_object_unref (self->priv->interface_skeleton);
-
- for (i = 0; i < G_N_ELEMENTS (skeleton_signals); i++) {
- SkeletonSignal sig = skeleton_signals[i];
-
- g_signal_connect (self->priv->interface_skeleton,
- sig.signal_name,
- G_CALLBACK (sig.callback),
- self);
+ case XAPP_BACKEND_NATIVE:
+ priv->backend_ops = &native_backend_ops;
+ break;
+ case XAPP_BACKEND_SNI:
+ priv->backend_ops = &sni_backend_ops;
+ break;
+ case XAPP_BACKEND_FALLBACK:
+ priv->backend_ops = &fallback_backend_ops;
+ break;
+ default:
+ priv->backend_ops = NULL;
+ DEBUG ("Unknown backend type %d", backend_type);
+ return;
}
- return TRUE;
-}
-
-static void
-connect_with_status_applet (XAppStatusIcon *self)
-{
- gchar **name_parts = NULL;
- gchar *owner_name;
-
- name_parts = g_strsplit (self->priv->name, ".", -1);
-
- if (g_dbus_is_name (self->priv->name) &&
- g_str_has_prefix (self->priv->name, ICON_NAME) &&
- g_strv_length (name_parts) == 4)
- {
- owner_name = g_strdup (self->priv->name);
- }
- else
+ /* Initialize new backend */
+ gboolean success = FALSE;
+ if (priv->backend_ops && priv->backend_ops->init)
{
- gchar *valid_app_name = g_strdelimit (g_strdup (g_get_prgname ()), " .-,=+~`/", '_');
-
- owner_name = g_strdup_printf ("%s.%s",
- ICON_NAME,
- valid_app_name);
- g_free (valid_app_name);
+ success = priv->backend_ops->init(self);
}
- g_strfreev (name_parts);
-
- if (name_owner_id == 0)
+ if (success && priv->backend_ops && priv->backend_ops->sync)
{
- DEBUG ("Attempting to own name on bus '%s'", owner_name);
- name_owner_id = g_bus_own_name_on_connection (self->priv->connection,
- owner_name,
- G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE,
- on_name_acquired,
- on_name_lost,
- NULL,
- NULL);
+ /* Sync all properties to new backend */
+ priv->backend_ops->sync(self);
}
-
- g_free(owner_name);
}
-static void
-update_fallback_icon (XAppStatusIcon *self)
+static XAppBackendType
+get_forced_backend (void)
{
- XAppStatusIconPrivate *priv = self->priv;
+ const gchar *env = g_getenv ("XAPP_STATUS_ICON_USE_BACKEND");
- if (!priv->gtk_status_icon)
+ if (env == NULL)
{
- return;
+ return XAPP_BACKEND_NONE;
}
- gtk_status_icon_set_tooltip_text (priv->gtk_status_icon, priv->tooltip_text);
-
- if (priv->icon_name)
+ if (g_strcmp0 (env, "native") == 0)
{
- gtk_status_icon_set_visible (priv->gtk_status_icon, priv->visible);
-
- if (g_path_is_absolute (priv->icon_name))
- {
- gtk_status_icon_set_from_file (priv->gtk_status_icon, priv->icon_name);
- }
- else
- {
- gtk_status_icon_set_from_icon_name (priv->gtk_status_icon, priv->icon_name);
- }
+ DEBUG ("Forcing native backend via environment variable");
+ return XAPP_BACKEND_NATIVE;
}
- else
+ else if (g_strcmp0 (env, "sni") == 0)
{
- gtk_status_icon_set_visible (priv->gtk_status_icon, FALSE);
+ DEBUG ("Forcing SNI backend via environment variable");
+ return XAPP_BACKEND_SNI;
}
-}
-
-static void
-on_gtk_status_icon_embedded_changed (GtkStatusIcon *icon,
- GParamSpec *pspec,
- gpointer user_data)
-{
- g_return_if_fail (GTK_IS_STATUS_ICON (icon));
-
- XAppStatusIcon *self = XAPP_STATUS_ICON (user_data);
-
- if (gtk_status_icon_is_embedded (icon))
- {
- process_icon_state = XAPP_STATUS_ICON_STATE_FALLBACK;
- }
- else
+ else if (g_strcmp0 (env, "xembed") == 0)
{
- process_icon_state = XAPP_STATUS_ICON_STATE_NO_SUPPORT;
+ DEBUG ("Forcing xembed (fallback) backend via environment variable");
+ return XAPP_BACKEND_FALLBACK;
}
- DEBUG ("Fallback icon embedded_changed. State is now %s",
- state_to_str (process_icon_state));
- g_signal_emit (self, signals[STATE_CHANGED], 0, process_icon_state);
+ g_warning ("Invalid XAPP_STATUS_ICON_USE_BACKEND value '%s'. Valid values: native, sni, xembed", env);
+ return XAPP_BACKEND_NONE;
}
static void
-use_gtk_status_icon (XAppStatusIcon *self)
+complete_icon_setup (XAppStatusIcon *self)
{
- XAppStatusIconPrivate *priv = self->priv;
-
- DEBUG ("Falling back to GtkStatusIcon");
-
- remove_icon_path_from_bus (self);
-
- // Make sure there wasn't already one
- g_clear_object (&self->priv->gtk_status_icon);
-
- self->priv->gtk_status_icon = gtk_status_icon_new ();
-
- g_signal_connect (priv->gtk_status_icon,
- "button-press-event",
- G_CALLBACK (on_gtk_status_icon_button_press),
- self);
- g_signal_connect (priv->gtk_status_icon,
- "button-release-event",
- G_CALLBACK (on_gtk_status_icon_button_release),
- self);
- g_signal_connect (priv->gtk_status_icon,
- "scroll-event",
- G_CALLBACK (on_gtk_status_icon_scroll),
- self);
- g_signal_connect (priv->gtk_status_icon,
- "notify::embedded",
- G_CALLBACK (on_gtk_status_icon_embedded_changed),
- self);
-
- update_fallback_icon (self);
-}
+ XAppBackendType forced_backend;
-static void
-on_list_names_completed (GObject *source,
- GAsyncResult *res,
- gpointer user_data)
-{
- XAppStatusIcon *self = XAPP_STATUS_ICON(user_data);
- GVariant *result;
- GVariantIter *iter;
- gchar *str;
- GError *error;
- gboolean found;
-
- error = NULL;
-
- result = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source),
- res,
- &error);
-
- if (error != NULL)
- {
- if (error->code != G_IO_ERROR_CANCELLED)
- {
- g_critical ("XAppStatusIcon: attempt to ListNames failed: %s\n", error->message);
- use_gtk_status_icon (self);
- }
- else
- {
- DEBUG ("Attempt to ListNames cancelled");
- }
-
- g_error_free (error);
- return;
- }
-
- g_variant_get (result, "(as)", &iter);
-
- found = FALSE;
-
- while (g_variant_iter_loop (iter, "s", &str))
- {
- if (g_str_has_prefix (str, STATUS_ICON_MONITOR_MATCH))
+ if (XAPP_STATUS_ICON_GET_PRIVATE(self)->listener_id == 0)
{
- DEBUG ("Discovered active status monitor (%s)", str);
- found = TRUE;
- }
- }
-
- g_variant_iter_free (iter);
- g_variant_unref (result);
-
- if (found && export_icon_interface (self))
- {
- if (name_owner_id > 0)
- {
- sync_skeleton (self);
+ add_name_listener (self);
}
- else
- {
- connect_with_status_applet (self);
- return;
- }
- }
- else
+ /* Check if a backend is forced via environment variable */
+ forced_backend = get_forced_backend ();
+ if (forced_backend != XAPP_BACKEND_NONE)
{
- use_gtk_status_icon (self);
+ switch_to_backend (self, forced_backend);
+ return;
}
-}
-
-static void
-look_for_status_applet (XAppStatusIcon *self)
-{
- // Check that there is at least one applet on DBUS
- DEBUG("Looking for status monitors");
-
- cancellable_reset (self);
-
- g_dbus_connection_call (self->priv->connection,
- FDO_DBUS_NAME,
- FDO_DBUS_PATH,
- FDO_DBUS_NAME,
- "ListNames",
- NULL,
- G_VARIANT_TYPE ("(as)"),
- G_DBUS_CALL_FLAGS_NONE,
- 3000, /* 3 secs */
- self->priv->cancellable,
- on_list_names_completed,
- self);
-}
-
-static void
-complete_icon_setup (XAppStatusIcon *self)
-{
- if (self->priv->listener_id == 0)
- {
- add_name_listener (self);
- }
/* There is a potential loop in the g_bus_own_name sequence -
* if we fail to acquire a name, we refresh again and potentially
* fail again. If we fail more than MAX_NAME_FAILS, then quit trying
* and just use the fallback icon It's pretty unlikely for*/
- if (self->priv->fail_counter == MAX_NAME_FAILS)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(self)->fail_counter == MAX_NAME_FAILS)
{
- use_gtk_status_icon (self);
+ switch_to_backend (self, XAPP_BACKEND_FALLBACK);
return;
}
- look_for_status_applet (self);
+ /* Try native backend first */
+ switch_to_backend (self, XAPP_BACKEND_NATIVE);
}
static void
@@ -1193,7 +243,7 @@ on_session_bus_connected (GObject *source,
error = NULL;
- self->priv->connection = g_bus_get_finish (res, &error);
+ XAPP_STATUS_ICON_GET_PRIVATE(self)->connection = g_bus_get_finish (res, &error);
if (error != NULL)
{
@@ -1205,7 +255,7 @@ on_session_bus_connected (GObject *source,
/* If we never get a connection, we use the Gtk icon exclusively, and will never
* re-try. FIXME? this is unlikely to happen, so I don't see the point in trying
* later, as there are probably bigger problems in this case. */
- use_gtk_status_icon (self);
+ switch_to_backend (self, XAPP_BACKEND_FALLBACK);
}
else
{
@@ -1219,17 +269,17 @@ on_session_bus_connected (GObject *source,
complete_icon_setup (self);
}
-static void
+void
refresh_icon (XAppStatusIcon *self)
{
- if (self->priv->connection == NULL)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(self)->connection == NULL)
{
DEBUG ("Connecting to session bus");
cancellable_reset (self);
g_bus_get (G_BUS_TYPE_SESSION,
- self->priv->cancellable,
+ XAPP_STATUS_ICON_GET_PRIVATE(self)->cancellable,
on_session_bus_connected,
self);
}
@@ -1254,7 +304,7 @@ xapp_status_icon_set_property (GObject *object,
xapp_status_icon_set_secondary_menu (XAPP_STATUS_ICON (object), g_value_get_object (value));
break;
case PROP_ICON_SIZE:
- XAPP_STATUS_ICON (object)->priv->icon_size = CLAMP (g_value_get_int (value), 0, MAX_SANE_ICON_SIZE);
+ XAPP_STATUS_ICON_GET_PRIVATE(XAPP_STATUS_ICON (object))->icon_size = CLAMP (g_value_get_int (value), 0, MAX_SANE_ICON_SIZE);
break;
case PROP_NAME:
{
@@ -1285,16 +335,16 @@ xapp_status_icon_get_property (GObject *object,
switch (prop_id)
{
case PROP_PRIMARY_MENU:
- g_value_set_object (value, icon->priv->primary_menu);
+ g_value_set_object (value, XAPP_STATUS_ICON_GET_PRIVATE(icon)->primary_menu);
break;
case PROP_SECONDARY_MENU:
- g_value_set_object (value, icon->priv->secondary_menu);
+ g_value_set_object (value, XAPP_STATUS_ICON_GET_PRIVATE(icon)->secondary_menu);
break;
case PROP_ICON_SIZE:
- g_value_set_int (value, icon->priv->icon_size);
+ g_value_set_int (value, XAPP_STATUS_ICON_GET_PRIVATE(icon)->icon_size);
break;
case PROP_NAME:
- g_value_set_string (value, icon->priv->name);
+ g_value_set_string (value, XAPP_STATUS_ICON_GET_PRIVATE(icon)->name);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -1305,42 +355,19 @@ xapp_status_icon_get_property (GObject *object,
static void
xapp_status_icon_init (XAppStatusIcon *self)
{
- self->priv = xapp_status_icon_get_instance_private (self);
+ XAPP_STATUS_ICON_GET_PRIVATE(self)->name = g_strdup (g_get_prgname());
- self->priv->name = g_strdup (g_get_prgname());
+ XAPP_STATUS_ICON_GET_PRIVATE(self)->icon_size = FALLBACK_ICON_SIZE;
+ XAPP_STATUS_ICON_GET_PRIVATE(self)->icon_name = g_strdup (" ");
- self->priv->icon_size = FALLBACK_ICON_SIZE;
- self->priv->icon_name = g_strdup (" ");
-
- DEBUG ("Init: application name: '%s'", self->priv->name);
+ DEBUG ("Init: application name: '%s'", XAPP_STATUS_ICON_GET_PRIVATE(self)->name);
// Default to visible (the same behavior as GtkStatusIcon)
- self->priv->visible = TRUE;
+ XAPP_STATUS_ICON_GET_PRIVATE(self)->visible = TRUE;
refresh_icon (self);
}
-static void
-remove_icon_path_from_bus (XAppStatusIcon *self)
-{
- g_return_if_fail (XAPP_IS_STATUS_ICON (self));
-
- if (self->priv->object_skeleton)
- {
- const gchar *path;
- path = g_dbus_object_get_object_path (G_DBUS_OBJECT (self->priv->object_skeleton));
-
- DEBUG ("Removing interface at path '%s'", path);
-
- g_object_set_data (G_OBJECT (self->priv->object_skeleton), "xapp-status-icon-instance", NULL);
- g_dbus_object_manager_server_unexport (obj_server, path);
-
- self->priv->interface_skeleton = NULL;
- self->priv->object_skeleton = NULL;
-
- g_object_unref (obj_server);
- }
-}
static void
xapp_status_icon_dispose (GObject *object)
@@ -1349,34 +376,30 @@ xapp_status_icon_dispose (GObject *object)
DEBUG ("XAppStatusIcon dispose (%p)", object);
- g_free (self->priv->name);
- g_free (self->priv->icon_name);
- g_free (self->priv->tooltip_text);
- g_free (self->priv->label);
- g_free (self->priv->metadata);
+ g_free (XAPP_STATUS_ICON_GET_PRIVATE(self)->name);
+ g_free (XAPP_STATUS_ICON_GET_PRIVATE(self)->icon_name);
+ g_free (XAPP_STATUS_ICON_GET_PRIVATE(self)->tooltip_text);
+ g_free (XAPP_STATUS_ICON_GET_PRIVATE(self)->label);
+ g_free (XAPP_STATUS_ICON_GET_PRIVATE(self)->metadata);
- g_clear_object (&self->priv->cancellable);
+ g_clear_object (&XAPP_STATUS_ICON_GET_PRIVATE(self)->cancellable);
- g_clear_object (&self->priv->primary_menu);
- g_clear_object (&self->priv->secondary_menu);
+ g_clear_object (&XAPP_STATUS_ICON_GET_PRIVATE(self)->primary_menu);
+ g_clear_object (&XAPP_STATUS_ICON_GET_PRIVATE(self)->secondary_menu);
- if (self->priv->gtk_status_icon != NULL)
+ /* Cleanup active backend */
+ if (XAPP_STATUS_ICON_GET_PRIVATE(self)->backend_ops && XAPP_STATUS_ICON_GET_PRIVATE(self)->backend_ops->cleanup)
{
- g_signal_handlers_disconnect_by_func (self->priv->gtk_status_icon, on_gtk_status_icon_button_press, self);
- g_signal_handlers_disconnect_by_func (self->priv->gtk_status_icon, on_gtk_status_icon_button_release, self);
- g_object_unref (self->priv->gtk_status_icon);
- self->priv->gtk_status_icon = NULL;
+ XAPP_STATUS_ICON_GET_PRIVATE(self)->backend_ops->cleanup(self);
}
- remove_icon_path_from_bus (self);
-
- if (self->priv->listener_id > 0)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(self)->listener_id > 0)
{
- g_dbus_connection_signal_unsubscribe (self->priv->connection, self->priv->listener_id);
- self->priv->listener_id = 0;
+ g_dbus_connection_signal_unsubscribe (XAPP_STATUS_ICON_GET_PRIVATE(self)->connection, XAPP_STATUS_ICON_GET_PRIVATE(self)->listener_id);
+ XAPP_STATUS_ICON_GET_PRIVATE(self)->listener_id = 0;
}
- g_clear_object (&self->priv->connection);
+ g_clear_object (&XAPP_STATUS_ICON_GET_PRIVATE(self)->connection);
G_OBJECT_CLASS (xapp_status_icon_parent_class)->dispose (object);
}
@@ -1386,7 +409,7 @@ xapp_status_icon_finalize (GObject *object)
{
DEBUG ("XAppStatusIcon finalize (%p)", object);
- g_clear_object (&XAPP_STATUS_ICON (object)->priv->cancellable);
+ g_clear_object (&XAPP_STATUS_ICON_GET_PRIVATE(XAPP_STATUS_ICON (object))->cancellable);
G_OBJECT_CLASS (xapp_status_icon_parent_class)->finalize (object);
}
@@ -1505,7 +528,7 @@ xapp_status_icon_class_init (XAppStatusIconClass *klass)
*
* Gets emitted when there is a button press received from an applet
*/
- signals[BUTTON_PRESS] =
+ status_icon_signals[SIGNAL_BUTTON_PRESS] =
g_signal_new ("button-press-event",
XAPP_TYPE_STATUS_ICON,
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
@@ -1524,7 +547,7 @@ xapp_status_icon_class_init (XAppStatusIconClass *klass)
*
* Gets emitted when there is a button release received from an applet
*/
- signals[BUTTON_RELEASE] =
+ status_icon_signals[SIGNAL_BUTTON_RELEASE] =
g_signal_new ("button-release-event",
XAPP_TYPE_STATUS_ICON,
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
@@ -1542,7 +565,7 @@ xapp_status_icon_class_init (XAppStatusIconClass *klass)
* XAppStatusIcon:secondary-menu is not %NULL, this signal is skipped for the respective button
* presses. A middle button click will always send this signal when pressed.
*/
- signals [ACTIVATE] =
+ status_icon_signals [SIGNAL_ACTIVATE] =
g_signal_new ("activate",
XAPP_TYPE_STATUS_ICON,
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
@@ -1560,7 +583,7 @@ xapp_status_icon_class_init (XAppStatusIconClass *klass)
* (perhaps to alter the menu or other click behavior), you should
* connect to this - see #XAppStatusIconState for more details.
*/
- signals [STATE_CHANGED] =
+ status_icon_signals [SIGNAL_STATE_CHANGED] =
g_signal_new ("state-changed",
XAPP_TYPE_STATUS_ICON,
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
@@ -1579,7 +602,7 @@ xapp_status_icon_class_init (XAppStatusIconClass *klass)
* For the most part, amounts will always be 1, unless an applet supports smooth
* scrolling. Generally the direction value is most important.
*/
- signals [SCROLL] =
+ status_icon_signals [SIGNAL_SCROLL] =
g_signal_new ("scroll-event",
XAPP_TYPE_STATUS_ICON,
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
@@ -1602,7 +625,7 @@ xapp_status_icon_set_name (XAppStatusIcon *icon, const gchar *name)
{
g_return_if_fail (XAPP_IS_STATUS_ICON (icon));
- if (g_strcmp0 (name, icon->priv->name) == 0)
+ if (g_strcmp0 (name, XAPP_STATUS_ICON_GET_PRIVATE(icon)->name) == 0)
{
return;
}
@@ -1616,23 +639,23 @@ xapp_status_icon_set_name (XAppStatusIcon *icon, const gchar *name)
return;
}
- g_clear_pointer (&icon->priv->name, g_free);
- icon->priv->name = g_strdup (name);
+ g_clear_pointer (&XAPP_STATUS_ICON_GET_PRIVATE(icon)->name, g_free);
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->name = g_strdup (name);
DEBUG ("set_name: %s", name);
- if (icon->priv->interface_skeleton)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->interface_skeleton)
{
- xapp_status_icon_interface_set_name (icon->priv->interface_skeleton, name);
+ xapp_status_icon_interface_set_name (XAPP_STATUS_ICON_GET_PRIVATE(icon)->interface_skeleton, name);
}
/* Call this directly instead of in the update_fallback_icon() function,
* as every time this is called, Gtk re-creates the plug for the icon,
* so the tray thinks one icon has disappeared and a new one appeared,
* which can cause flicker and undesirable re-ordering of tray items. */
- if (icon->priv->gtk_status_icon != NULL)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->gtk_status_icon != NULL)
{
- gtk_status_icon_set_name (icon->priv->gtk_status_icon, name);
+ gtk_status_icon_set_name (XAPP_STATUS_ICON_GET_PRIVATE(icon)->gtk_status_icon, name);
}
}
@@ -1650,22 +673,20 @@ xapp_status_icon_set_icon_name (XAppStatusIcon *icon, const gchar *icon_name)
{
g_return_if_fail (XAPP_IS_STATUS_ICON (icon));
- if (g_strcmp0 (icon_name, icon->priv->icon_name) == 0)
+ if (g_strcmp0 (icon_name, XAPP_STATUS_ICON_GET_PRIVATE(icon)->icon_name) == 0)
{
return;
}
- g_clear_pointer (&icon->priv->icon_name, g_free);
- icon->priv->icon_name = g_strdup (icon_name);
+ g_clear_pointer (&XAPP_STATUS_ICON_GET_PRIVATE(icon)->icon_name, g_free);
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->icon_name = g_strdup (icon_name);
DEBUG ("set_icon_name: %s", icon_name);
- if (icon->priv->interface_skeleton)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops && XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops->set_icon_name)
{
- xapp_status_icon_interface_set_icon_name (icon->priv->interface_skeleton, icon_name);
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops->set_icon_name (icon, icon_name);
}
-
- update_fallback_icon (icon);
}
/**
@@ -1683,7 +704,7 @@ xapp_status_icon_get_icon_size (XAppStatusIcon *icon)
{
g_return_val_if_fail (XAPP_IS_STATUS_ICON (icon), FALLBACK_ICON_SIZE);
- if (icon->priv->interface_skeleton == NULL)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->interface_skeleton == NULL)
{
DEBUG ("get_icon_size: %d (fallback)", FALLBACK_ICON_SIZE);
@@ -1692,7 +713,7 @@ xapp_status_icon_get_icon_size (XAppStatusIcon *icon)
gint size;
- size = xapp_status_icon_interface_get_icon_size (icon->priv->interface_skeleton);
+ size = xapp_status_icon_interface_get_icon_size (XAPP_STATUS_ICON_GET_PRIVATE(icon)->interface_skeleton);
DEBUG ("get_icon_size: %d", size);
@@ -1713,22 +734,20 @@ xapp_status_icon_set_tooltip_text (XAppStatusIcon *icon, const gchar *tooltip_te
{
g_return_if_fail (XAPP_IS_STATUS_ICON (icon));
- if (g_strcmp0 (tooltip_text, icon->priv->tooltip_text) == 0)
+ if (g_strcmp0 (tooltip_text, XAPP_STATUS_ICON_GET_PRIVATE(icon)->tooltip_text) == 0)
{
return;
}
- g_clear_pointer (&icon->priv->tooltip_text, g_free);
- icon->priv->tooltip_text = g_strdup (tooltip_text);
+ g_clear_pointer (&XAPP_STATUS_ICON_GET_PRIVATE(icon)->tooltip_text, g_free);
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->tooltip_text = g_strdup (tooltip_text);
DEBUG ("set_tooltip_text: %s", tooltip_text);
- if (icon->priv->interface_skeleton)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops && XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops->set_tooltip)
{
- xapp_status_icon_interface_set_tooltip_text (icon->priv->interface_skeleton, tooltip_text);
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops->set_tooltip (icon, tooltip_text);
}
-
- update_fallback_icon (icon);
}
/**
@@ -1745,19 +764,19 @@ xapp_status_icon_set_label (XAppStatusIcon *icon, const gchar *label)
{
g_return_if_fail (XAPP_IS_STATUS_ICON (icon));
- if (g_strcmp0 (label, icon->priv->label) == 0)
+ if (g_strcmp0 (label, XAPP_STATUS_ICON_GET_PRIVATE(icon)->label) == 0)
{
return;
}
- g_clear_pointer (&icon->priv->label, g_free);
- icon->priv->label = g_strdup (label);
+ g_clear_pointer (&XAPP_STATUS_ICON_GET_PRIVATE(icon)->label, g_free);
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->label = g_strdup (label);
DEBUG ("set_label: '%s'", label);
- if (icon->priv->interface_skeleton)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops && XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops->set_label)
{
- xapp_status_icon_interface_set_label (icon->priv->interface_skeleton, label);
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops->set_label (icon, label);
}
}
@@ -1775,21 +794,19 @@ xapp_status_icon_set_visible (XAppStatusIcon *icon, const gboolean visible)
{
g_return_if_fail (XAPP_IS_STATUS_ICON (icon));
- if (visible == icon->priv->visible)
+ if (visible == XAPP_STATUS_ICON_GET_PRIVATE(icon)->visible)
{
return;
}
- icon->priv->visible = visible;
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->visible = visible;
DEBUG ("set_visible: %s", visible ? "TRUE" : "FALSE");
- if (icon->priv->interface_skeleton)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops && XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops->set_visible)
{
- xapp_status_icon_interface_set_visible (icon->priv->interface_skeleton, visible);
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->backend_ops->set_visible (icon, visible);
}
-
- update_fallback_icon (icon);
}
/**
@@ -1807,9 +824,9 @@ xapp_status_icon_get_visible (XAppStatusIcon *icon)
{
g_return_val_if_fail (XAPP_IS_STATUS_ICON (icon), FALSE);
- DEBUG ("get_visible: %s", icon->priv->visible ? "TRUE" : "FALSE");
+ DEBUG ("get_visible: %s", XAPP_STATUS_ICON_GET_PRIVATE(icon)->visible ? "TRUE" : "FALSE");
- return icon->priv->visible;
+ return XAPP_STATUS_ICON_GET_PRIVATE(icon)->visible;
}
/**
@@ -1865,18 +882,24 @@ xapp_status_icon_set_primary_menu (XAppStatusIcon *icon,
g_return_if_fail (XAPP_IS_STATUS_ICON (icon));
g_return_if_fail (GTK_IS_MENU (menu) || menu == NULL);
- if (menu == GTK_MENU (icon->priv->primary_menu))
+ if (menu == GTK_MENU (XAPP_STATUS_ICON_GET_PRIVATE(icon)->primary_menu))
{
return;
}
- g_clear_object (&icon->priv->primary_menu);
+ g_clear_object (&XAPP_STATUS_ICON_GET_PRIVATE(icon)->primary_menu);
- DEBUG ("%s: %p", icon->priv->name, menu);
+ DEBUG ("%s: %p", XAPP_STATUS_ICON_GET_PRIVATE(icon)->name, menu);
if (menu)
{
- icon->priv->primary_menu = GTK_WIDGET (g_object_ref_sink (menu));
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->primary_menu = GTK_WIDGET (g_object_ref_sink (menu));
+ }
+
+ /* Update SNI backend menu if active */
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->active_backend == XAPP_BACKEND_SNI)
+ {
+ sni_backend_export_menu(icon);
}
}
@@ -1896,9 +919,9 @@ xapp_status_icon_get_primary_menu (XAppStatusIcon *icon)
{
g_return_val_if_fail (XAPP_IS_STATUS_ICON (icon), NULL);
- DEBUG ("get_menu: %p", icon->priv->primary_menu);
+ DEBUG ("get_menu: %p", XAPP_STATUS_ICON_GET_PRIVATE(icon)->primary_menu);
- return icon->priv->primary_menu;
+ return XAPP_STATUS_ICON_GET_PRIVATE(icon)->primary_menu;
}
/**
@@ -1917,18 +940,24 @@ xapp_status_icon_set_secondary_menu (XAppStatusIcon *icon,
g_return_if_fail (XAPP_IS_STATUS_ICON (icon));
g_return_if_fail (GTK_IS_MENU (menu) || menu == NULL);
- if (menu == GTK_MENU (icon->priv->secondary_menu))
+ if (menu == GTK_MENU (XAPP_STATUS_ICON_GET_PRIVATE(icon)->secondary_menu))
{
return;
}
- g_clear_object (&icon->priv->secondary_menu);
+ g_clear_object (&XAPP_STATUS_ICON_GET_PRIVATE(icon)->secondary_menu);
- DEBUG ("%s: %p", icon->priv->name, menu);
+ DEBUG ("%s: %p", XAPP_STATUS_ICON_GET_PRIVATE(icon)->name, menu);
if (menu)
{
- icon->priv->secondary_menu = GTK_WIDGET (g_object_ref_sink (menu));
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->secondary_menu = GTK_WIDGET (g_object_ref_sink (menu));
+ }
+
+ /* Update SNI backend menu if active */
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->active_backend == XAPP_BACKEND_SNI)
+ {
+ sni_backend_export_menu(icon);
}
}
@@ -1948,9 +977,9 @@ xapp_status_icon_get_secondary_menu (XAppStatusIcon *icon)
{
g_return_val_if_fail (XAPP_IS_STATUS_ICON (icon), NULL);
- DEBUG ("get_menu: %p", icon->priv->secondary_menu);
+ DEBUG ("get_menu: %p", XAPP_STATUS_ICON_GET_PRIVATE(icon)->secondary_menu);
- return icon->priv->secondary_menu;
+ return XAPP_STATUS_ICON_GET_PRIVATE(icon)->secondary_menu;
}
/**
@@ -2029,18 +1058,18 @@ xapp_status_icon_set_metadata (XAppStatusIcon *icon,
DEBUG ("set_metadata: '%s'", metadata);
- if (g_strcmp0 (metadata, icon->priv->metadata) == 0)
+ if (g_strcmp0 (metadata, XAPP_STATUS_ICON_GET_PRIVATE(icon)->metadata) == 0)
{
return;
}
- old_meta = icon->priv->metadata;
- icon->priv->metadata = g_strdup (metadata);
+ old_meta = XAPP_STATUS_ICON_GET_PRIVATE(icon)->metadata;
+ XAPP_STATUS_ICON_GET_PRIVATE(icon)->metadata = g_strdup (metadata);
g_free (old_meta);
- if (icon->priv->interface_skeleton)
+ if (XAPP_STATUS_ICON_GET_PRIVATE(icon)->interface_skeleton)
{
- xapp_status_icon_interface_set_metadata (icon->priv->interface_skeleton, metadata);
+ xapp_status_icon_interface_set_metadata (XAPP_STATUS_ICON_GET_PRIVATE(icon)->interface_skeleton, metadata);
}
}
diff --git a/test-scripts/xapp-status-report.py b/test-scripts/xapp-status-report.py
new file mode 100755
index 00000000..d6ee9c12
--- /dev/null
+++ b/test-scripts/xapp-status-report.py
@@ -0,0 +1,484 @@
+#!/usr/bin/env python3
+"""
+XApp Status Icon Report Tool
+
+This script queries D-Bus for all XAppStatusIcon instances and displays
+detailed information about each one, including whether they're backed by
+StatusNotifier (SNI) items or are native XAppStatusIcons.
+"""
+
+import sys
+import json
+import argparse
+from gi.repository import Gio, GLib
+
+# ANSI color codes
+class Colors:
+ RESET = '\033[0m'
+ BOLD = '\033[1m'
+ DIM = '\033[2m'
+ HEADER = '\033[95m'
+ BLUE = '\033[94m'
+ CYAN = '\033[96m'
+ GREEN = '\033[92m'
+ YELLOW = '\033[93m'
+ RED = '\033[91m'
+
+ @staticmethod
+ def disable():
+ Colors.RESET = ''
+ Colors.BOLD = ''
+ Colors.DIM = ''
+ Colors.HEADER = ''
+ Colors.BLUE = ''
+ Colors.CYAN = ''
+ Colors.GREEN = ''
+ Colors.YELLOW = ''
+ Colors.RED = ''
+
+XAPP_ICON_BASE_PATH = "/org/x/StatusIcon"
+XAPP_ICON_INTERFACE = "org.x.StatusIcon"
+SNW_BUS_NAME = "org.kde.StatusNotifierWatcher"
+SNW_OBJECT_PATH = "/StatusNotifierWatcher"
+SNW_INTERFACE = "org.kde.StatusNotifierWatcher"
+
+class StatusIconReporter:
+ def __init__(self, use_colors=True, format_style='table'):
+ self.connection = None
+ self.icons = []
+ self.sni_items = []
+ self.format_style = format_style
+
+ if not use_colors:
+ Colors.disable()
+
+ def connect_to_bus(self):
+ """Connect to the session bus."""
+ try:
+ self.connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
+ return True
+ except GLib.Error as e:
+ print(f"ERROR: Could not connect to session bus: {e.message}")
+ return False
+
+ def get_sni_registered_items(self):
+ """Get list of registered StatusNotifier items from the watcher."""
+ try:
+ proxy = Gio.DBusProxy.new_sync(
+ self.connection,
+ Gio.DBusProxyFlags.NONE,
+ None,
+ SNW_BUS_NAME,
+ SNW_OBJECT_PATH,
+ SNW_INTERFACE,
+ None
+ )
+
+ variant = proxy.get_cached_property("RegisteredStatusNotifierItems")
+ if variant:
+ self.sni_items = variant.unpack()
+ return True
+ return False
+ except GLib.Error:
+ return False
+
+ def introspect_path(self, bus_name, path):
+ """Introspect a D-Bus path to find child objects."""
+ try:
+ result = self.connection.call_sync(
+ bus_name,
+ path,
+ "org.freedesktop.DBus.Introspectable",
+ "Introspect",
+ None,
+ GLib.VariantType.new("(s)"),
+ Gio.DBusCallFlags.NONE,
+ 5000,
+ None
+ )
+
+ xml_data = result.unpack()[0]
+
+ import xml.etree.ElementTree as ET
+ root = ET.fromstring(xml_data)
+
+ nodes = []
+ for node in root.findall('node'):
+ name = node.get('name')
+ if name and not name.startswith(':'):
+ nodes.append(name)
+
+ return nodes
+ except GLib.Error:
+ return []
+
+ def find_all_icon_objects(self):
+ """Find all XAppStatusIcon objects by introspecting the bus."""
+ icon_objects = []
+
+ try:
+ result = self.connection.call_sync(
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "ListNames",
+ None,
+ GLib.VariantType.new("(as)"),
+ Gio.DBusCallFlags.NONE,
+ 5000,
+ None
+ )
+
+ names = result.unpack()[0]
+
+ for bus_name in names:
+ if bus_name.startswith("org.x.StatusIcon"):
+ try:
+ nodes = self.introspect_path(bus_name, XAPP_ICON_BASE_PATH)
+
+ for node in nodes:
+ object_path = f"{XAPP_ICON_BASE_PATH}/{node}"
+ icon_objects.append((bus_name, object_path))
+
+ except Exception:
+ continue
+
+ except GLib.Error as e:
+ print(f"ERROR: Could not list bus names: {e.message}")
+
+ return icon_objects
+
+ def get_icon_properties(self, bus_name, object_path):
+ """Get all properties for an XAppStatusIcon object."""
+ try:
+ proxy = Gio.DBusProxy.new_sync(
+ self.connection,
+ Gio.DBusProxyFlags.NONE,
+ None,
+ bus_name,
+ object_path,
+ XAPP_ICON_INTERFACE,
+ None
+ )
+
+ properties = {}
+ property_names = [
+ "Name", "IconName", "TooltipText", "Label",
+ "Visible", "IconSize", "PrimaryMenuIsOpen",
+ "SecondaryMenuIsOpen", "Metadata"
+ ]
+
+ for prop_name in property_names:
+ try:
+ variant = proxy.get_cached_property(prop_name)
+ if variant:
+ properties[prop_name] = variant.unpack()
+ else:
+ properties[prop_name] = None
+ except:
+ properties[prop_name] = None
+
+ return properties
+
+ except GLib.Error as e:
+ return {"error": str(e)}
+
+ def is_sni_backed(self, bus_name):
+ """Determine if an icon is backed by a StatusNotifier item.
+
+ Icons created by xapp-sn-watcher have a bus name containing 'xapp_sn_watcher'.
+ """
+ return "xapp_sn_watcher" in bus_name
+
+ def collect_icon_info(self):
+ """Collect information about all status icons."""
+ print("Scanning for XAppStatusIcons...")
+
+ icon_objects = self.find_all_icon_objects()
+
+ if not icon_objects:
+ print("No XAppStatusIcon instances found.")
+ return
+
+ print(f"Found {len(icon_objects)} icon object(s).\n")
+
+ for bus_name, object_path in icon_objects:
+ properties = self.get_icon_properties(bus_name, object_path)
+ is_sni = self.is_sni_backed(bus_name)
+
+ icon_info = {
+ "bus_name": bus_name,
+ "object_path": object_path,
+ "is_sni_backed": is_sni,
+ "properties": properties
+ }
+
+ self.icons.append(icon_info)
+
+ def strip_ansi(self, text):
+ """Remove ANSI escape codes from text."""
+ import re
+ return re.sub(r'\033\[[0-9;]*m', '', str(text))
+
+ def visible_length(self, text):
+ """Get the visible length of text (excluding ANSI codes)."""
+ return len(self.strip_ansi(text))
+
+ def pad_to_width(self, text, width):
+ """Pad text to specified width, accounting for ANSI codes."""
+ visible_len = self.visible_length(text)
+
+ if visible_len > width:
+ # Truncate
+ stripped = self.strip_ansi(text)
+ return stripped[:width-2] + '..'
+
+ # Add padding
+ padding = width - visible_len
+ return text + (' ' * padding)
+
+ def print_table_row(self, cols, widths):
+ """Print a table row with proper column widths."""
+ formatted_cols = []
+ for col, width in zip(cols, widths):
+ col_str = str(col) if col is not None else ''
+ formatted_cols.append(self.pad_to_width(col_str, width))
+
+ return '│ ' + ' │ '.join(formatted_cols) + ' │'
+
+ def print_table_separator(self, widths):
+ """Print a table separator line."""
+ parts = ['─' * w for w in widths]
+ return '├─' + '─┼─'.join(parts) + '─┤'
+
+ def print_table_border(self, widths, style='top'):
+ """Print a table border."""
+ parts = ['─' * w for w in widths]
+ if style == 'top':
+ return '┌─' + '─┬─'.join(parts) + '─┐'
+ else: # bottom
+ return '└─' + '─┴─'.join(parts) + '─┘'
+
+ def print_report_table(self):
+ """Print report in table format."""
+ if not self.icons:
+ return
+
+ # Print header
+ print(f"\n{Colors.BOLD}{Colors.HEADER}╔{'═' * 78}╗{Colors.RESET}")
+ print(f"{Colors.BOLD}{Colors.HEADER}║{' ' * 24}XAPP STATUS ICON REPORT{' ' * 31}║{Colors.RESET}")
+ print(f"{Colors.BOLD}{Colors.HEADER}╚{'═' * 78}╝{Colors.RESET}\n")
+
+ # Summary
+ native_count = sum(1 for icon in self.icons if not icon['is_sni_backed'])
+ sni_count = sum(1 for icon in self.icons if icon['is_sni_backed'])
+
+ print(f"{Colors.BOLD}Summary:{Colors.RESET}")
+ print(f" Total Icons: {Colors.CYAN}{len(self.icons)}{Colors.RESET}")
+ print(f" ├─ {Colors.GREEN}Native XAppStatusIcons:{Colors.RESET} {native_count}")
+ print(f" └─ {Colors.YELLOW}StatusNotifier-backed:{Colors.RESET} {sni_count}")
+ print()
+
+ # SNI items if present
+ if self.sni_items:
+ print(f"{Colors.BOLD}Registered StatusNotifier Items:{Colors.RESET}")
+ for item in self.sni_items:
+ print(f" • {Colors.DIM}{item}{Colors.RESET}")
+ print()
+
+ # Table header
+ print(f"{Colors.BOLD}Status Icons:{Colors.RESET}\n")
+
+ # Calculate dynamic column widths
+ # Start with minimum widths
+ widths = [3, 20, 15, 8, 6, 30]
+ headers = ['#', 'Name', 'Type', 'Vis', 'Size', 'Icon Name']
+
+ # Adjust based on actual data
+ for icon in self.icons:
+ props = icon['properties']
+ name_len = len(props.get('Name', ''))
+ icon_len = len(props.get('IconName', ''))
+
+ widths[1] = max(widths[1], name_len + 2)
+ widths[5] = max(widths[5], min(icon_len + 2, 40)) # Cap at 40
+
+ # Top border
+ print(self.print_table_border(widths, 'top'))
+
+ # Headers
+ header_row = self.print_table_row([f"{Colors.BOLD}{h}{Colors.RESET}" for h in headers], widths)
+ print(header_row)
+
+ # Separator
+ print(self.print_table_separator(widths))
+
+ # Data rows
+ for idx, icon in enumerate(self.icons, 1):
+ props = icon['properties']
+
+ if icon['is_sni_backed']:
+ type_str = f"{Colors.YELLOW}SNI-backed{Colors.RESET}"
+ else:
+ type_str = f"{Colors.GREEN}Native{Colors.RESET}"
+
+ visible = props.get('Visible', False)
+ visible_str = f"{Colors.GREEN}✓{Colors.RESET}" if visible else f"{Colors.DIM}✗{Colors.RESET}"
+
+ size = props.get('IconSize', 0)
+ size_str = str(size) if size > 0 else f"{Colors.DIM}N/A{Colors.RESET}"
+
+ name = props.get('Name', 'N/A')
+ icon_name = props.get('IconName', 'N/A')
+
+ cols = [
+ f"{Colors.DIM}{idx}{Colors.RESET}",
+ name,
+ type_str,
+ visible_str,
+ size_str,
+ icon_name
+ ]
+
+ print(self.print_table_row(cols, widths))
+
+ # Bottom border
+ print(self.print_table_border(widths, 'bottom'))
+ print()
+
+ def print_report_detailed(self):
+ """Print detailed report for each icon."""
+ for idx, icon in enumerate(self.icons, 1):
+ print(f"\n{Colors.BOLD}{Colors.CYAN}╭{'─' * 78}╮{Colors.RESET}")
+ print(f"{Colors.BOLD}{Colors.CYAN}│{Colors.RESET} {Colors.BOLD}ICON #{idx}{Colors.RESET}{' ' * 71}{Colors.CYAN}│{Colors.RESET}")
+ print(f"{Colors.BOLD}{Colors.CYAN}╰{'─' * 78}╯{Colors.RESET}\n")
+
+ # Type indication
+ if icon['is_sni_backed']:
+ type_color = Colors.YELLOW
+ type_label = "StatusNotifier-backed (xapp-sn-watcher proxy)"
+ else:
+ type_color = Colors.GREEN
+ type_label = "Native XAppStatusIcon"
+
+ print(f"{Colors.BOLD}D-Bus Name:{Colors.RESET} {Colors.CYAN}{icon['bus_name']}{Colors.RESET}")
+ print(f"{Colors.BOLD}Object Path:{Colors.RESET} {Colors.DIM}{icon['object_path']}{Colors.RESET}")
+ print(f"{Colors.BOLD}Type:{Colors.RESET} {type_color}{type_label}{Colors.RESET}")
+ print()
+
+ if 'error' in icon['properties']:
+ print(f"{Colors.RED}ERROR: {icon['properties']['error']}{Colors.RESET}")
+ else:
+ props = icon['properties']
+
+ # Properties in a nice format
+ print(f"{Colors.BOLD}Properties:{Colors.RESET}")
+ print(f" ├─ Name: {props.get('Name', Colors.DIM + 'N/A' + Colors.RESET)}")
+ print(f" ├─ Icon Name: {props.get('IconName', Colors.DIM + 'N/A' + Colors.RESET)}")
+
+ tooltip = props.get('TooltipText', '')
+ if tooltip:
+ print(f" ├─ Tooltip: {tooltip}")
+ else:
+ print(f" ├─ Tooltip: {Colors.DIM}(none){Colors.RESET}")
+
+ label = props.get('Label', '')
+ if label:
+ print(f" ├─ Label: {label}")
+ else:
+ print(f" ├─ Label: {Colors.DIM}(none){Colors.RESET}")
+
+ visible = props.get('Visible', False)
+ visible_str = f"{Colors.GREEN}Yes{Colors.RESET}" if visible else f"{Colors.DIM}No{Colors.RESET}"
+ print(f" ├─ Visible: {visible_str}")
+
+ size = props.get('IconSize', 0)
+ size_str = str(size) if size > 0 else f"{Colors.DIM}N/A{Colors.RESET}"
+ print(f" ├─ Icon Size: {size_str}")
+
+ primary_open = props.get('PrimaryMenuIsOpen', False)
+ primary_str = f"{Colors.GREEN}Open{Colors.RESET}" if primary_open else f"{Colors.DIM}Closed{Colors.RESET}"
+ print(f" ├─ Primary Menu: {primary_str}")
+
+ secondary_open = props.get('SecondaryMenuIsOpen', False)
+ secondary_str = f"{Colors.GREEN}Open{Colors.RESET}" if secondary_open else f"{Colors.DIM}Closed{Colors.RESET}"
+ print(f" ├─ Secondary Menu: {secondary_str}")
+
+ metadata = props.get('Metadata')
+ if metadata:
+ try:
+ metadata_obj = json.loads(metadata)
+ metadata_str = json.dumps(metadata_obj, indent=2)
+ # Indent the JSON
+ metadata_lines = metadata_str.split('\n')
+ print(f" └─ Metadata:")
+ for line in metadata_lines:
+ print(f" {Colors.DIM}{line}{Colors.RESET}")
+ except:
+ print(f" └─ Metadata: {Colors.DIM}{metadata}{Colors.RESET}")
+ else:
+ print(f" └─ Metadata: {Colors.DIM}(none){Colors.RESET}")
+
+ def print_report(self):
+ """Print a formatted report of all status icons."""
+ if not self.icons:
+ return
+
+ if self.format_style == 'table':
+ self.print_report_table()
+ elif self.format_style == 'detailed':
+ self.print_report_table()
+ print(f"\n{Colors.BOLD}{Colors.HEADER}{'─' * 80}{Colors.RESET}")
+ print(f"{Colors.BOLD}{Colors.HEADER}DETAILED VIEW{Colors.RESET}")
+ print(f"{Colors.BOLD}{Colors.HEADER}{'─' * 80}{Colors.RESET}")
+ self.print_report_detailed()
+ elif self.format_style == 'full':
+ self.print_report_detailed()
+
+ def run(self):
+ """Main entry point for the reporter."""
+ if not self.connect_to_bus():
+ return 1
+
+ self.get_sni_registered_items()
+ self.collect_icon_info()
+ self.print_report()
+
+ return 0
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Report on XAppStatusIcon instances on D-Bus',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Format styles:
+ table - Compact table overview (default)
+ detailed - Table overview + detailed per-icon information
+ full - Only detailed per-icon information
+
+Examples:
+ %(prog)s # Show table overview
+ %(prog)s -f detailed # Show table + details
+ %(prog)s --no-color # Disable colors
+ """
+ )
+
+ parser.add_argument('-f', '--format',
+ choices=['table', 'detailed', 'full'],
+ default='table',
+ help='Output format style (default: table)')
+
+ parser.add_argument('--no-color',
+ action='store_true',
+ help='Disable colored output')
+
+ args = parser.parse_args()
+
+ reporter = StatusIconReporter(
+ use_colors=not args.no_color,
+ format_style=args.format
+ )
+ return reporter.run()
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/xapp-sn-watcher/meson.build b/xapp-sn-watcher/meson.build
index 16e74180..fed56611 100644
--- a/xapp-sn-watcher/meson.build
+++ b/xapp-sn-watcher/meson.build
@@ -4,11 +4,8 @@ sn_watcher_generated = gnome.gdbus_codegen(
interface_prefix: 'org.x.'
)
-sn_item_generated = gnome.gdbus_codegen(
- 'sn-item-interface',
- 'sn-item.xml',
- interface_prefix: 'org.x.'
-)
+# sn-item-interface is now generated in libxapp and shared
+# (libxapp generates it with org.kde prefix which is correct for org.kde.StatusNotifierItem)
conf = configuration_data()
@@ -37,7 +34,6 @@ cairo = dependency('cairo-gobject', required: true)
watcher_sources = [
sn_watcher_generated,
- sn_item_generated,
'xapp-sn-watcher.c',
'sn-item.c'
]
diff --git a/xapp-sn-watcher/sn-item.c b/xapp-sn-watcher/sn-item.c
index 9ced4d16..dad18e88 100644
--- a/xapp-sn-watcher/sn-item.c
+++ b/xapp-sn-watcher/sn-item.c
@@ -34,6 +34,7 @@ typedef struct
{
gchar *id;
gchar *title;
+ gchar *label;
gchar *status;
gchar *tooltip_heading;
gchar *tooltip_body;
@@ -56,6 +57,7 @@ typedef struct
gboolean update_menu;
gboolean update_icon;
gboolean update_id;
+ gboolean update_label;
} SnItemPropertiesResult;
struct _SnItem
@@ -78,6 +80,7 @@ struct _SnItem
guint update_properties_timeout;
gchar *sortable_name;
+ gchar *key; // Hash table key (busname + path)
gboolean should_activate;
gboolean should_replace_tooltip;
@@ -92,7 +95,9 @@ static void update_menu (SnItem *item, SnItemPropertiesResult *new_props);
static void update_status (SnItem *item, SnItemPropertiesResult *new_props);
static void update_tooltip (SnItem *item, SnItemPropertiesResult *new_props);
static void update_icon (SnItem *item, SnItemPropertiesResult *new_props);
+static void update_label (SnItem *item, SnItemPropertiesResult *new_props);
static void assign_sortable_name (SnItem *item, const gchar *title);
+static void sn_item_proxy_name_owner_changed (SnItem *item);
static void
props_free (SnItemPropertiesResult *props)
@@ -104,6 +109,7 @@ props_free (SnItemPropertiesResult *props)
g_free (props->id);
g_free (props->title);
+ g_free (props->label);
g_free (props->status);
g_free (props->tooltip_heading);
g_free (props->tooltip_body);
@@ -173,6 +179,7 @@ sn_item_dispose (GObject *object)
g_clear_handle_id (&item->update_properties_timeout, g_source_remove);
g_clear_pointer (&item->sortable_name, g_free);
+ g_clear_pointer (&item->key, g_free);
g_clear_object (&item->status_icon);
g_clear_object (&item->prop_proxy);
g_clear_object (&item->sn_item_proxy);
@@ -197,6 +204,8 @@ sn_item_finalize (GObject *object)
G_OBJECT_CLASS (sn_item_parent_class)->finalize (object);
}
+static guint sn_item_signals[1] = {0, };
+
static void
sn_item_class_init (SnItemClass *klass)
{
@@ -205,6 +214,19 @@ sn_item_class_init (SnItemClass *klass)
gobject_class->dispose = sn_item_dispose;
gobject_class->finalize = sn_item_finalize;
+ /**
+ * SnItem::removed:
+ * @item: the #SnItem
+ *
+ * Emitted when the item's D-Bus interface is removed/unexported
+ */
+ sn_item_signals[0] = g_signal_new ("removed",
+ SN_TYPE_ITEM,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
}
static guint
@@ -262,6 +284,23 @@ get_icon_size (SnItem *item)
return FALLBACK_ICON_SIZE;
}
+static void
+sn_item_proxy_name_owner_changed (SnItem *item)
+{
+ gchar *name_owner;
+
+ name_owner = g_dbus_proxy_get_name_owner (item->sn_item_proxy);
+
+ if (name_owner == NULL)
+ {
+ /* The interface has been removed/unexported - notify the watcher to remove this item */
+ DEBUG ("SNI proxy lost its name owner - interface was removed");
+ g_signal_emit_by_name (item, "removed");
+ }
+
+ g_free (name_owner);
+}
+
static cairo_surface_t *
surface_from_pixmap_data (gint width,
gint height,
@@ -703,6 +742,20 @@ update_status (SnItem *item, SnItemPropertiesResult *new_props)
DEBUG ("Status for '%s' is now '%s'", item->sortable_name, new_props->status);
}
+static void
+update_label (SnItem *item, SnItemPropertiesResult *new_props)
+{
+ if (new_props->label != NULL)
+ {
+ xapp_status_icon_set_label (item->status_icon, new_props->label);
+ DEBUG ("Label for '%s' set to: %s", item->sortable_name, new_props->label);
+ }
+ else
+ {
+ xapp_status_icon_set_label (item->status_icon, "");
+ }
+}
+
static gchar *
null_or_string_from_string (const gchar *str)
{
@@ -828,6 +881,16 @@ get_all_properties_callback (GObject *source_object,
}
}
else
+ if (g_strcmp0 (name, "XAyatanaLabel") == 0)
+ {
+ new_props->label = null_or_string_from_variant (value);
+
+ if (g_strcmp0 (new_props->label, item->current_props->label) != 0)
+ {
+ new_props->update_label = TRUE;
+ }
+ }
+ else
if (g_strcmp0 (name, "IconThemePath") == 0)
{
new_props->icon_theme_path = null_or_string_from_variant (value);
@@ -924,6 +987,11 @@ get_all_properties_callback (GObject *source_object,
update_menu (item, new_props);
}
+ if (new_props->update_label)
+ {
+ update_label (item, new_props);
+ }
+
if (new_props->update_icon || new_props->update_status)
{
update_icon (item, new_props);
@@ -997,7 +1065,8 @@ sn_signal_received (GDBusProxy *sn_item_proxy,
g_strcmp0 (signal_name, "NewToolTip") == 0 ||
g_strcmp0 (signal_name, "NewTitle") == 0 ||
g_strcmp0 (signal_name, "NewStatus") == 0 ||
- g_strcmp0 (signal_name, "NewMenu") == 0)
+ g_strcmp0 (signal_name, "NewMenu") == 0 ||
+ g_strcmp0 (signal_name, "XAyatanaNewLabel") == 0)
{
queue_update_properties (item, FALSE);
}
@@ -1169,6 +1238,12 @@ property_proxy_acquired (GObject *source,
G_CALLBACK (sn_signal_received),
item);
+ /* Monitor for when the proxy loses its name owner (interface removed/unexported) */
+ g_signal_connect_swapped (item->sn_item_proxy,
+ "notify::g-name-owner",
+ G_CALLBACK (sn_item_proxy_name_owner_changed),
+ item);
+
item->status_icon = xapp_status_icon_new ();
json = g_strdup_printf ("{ \"highlight-both-menus\": %s }", item->is_ai ? "true" : "false");
@@ -1201,13 +1276,21 @@ initialize_item (SnItem *item)
item);
}
+const gchar *
+sn_item_get_key (SnItem *item)
+{
+ return item->key;
+}
+
SnItem *
-sn_item_new (GDBusProxy *sn_item_proxy,
- gboolean is_ai)
+sn_item_new (GDBusProxy *sn_item_proxy,
+ const gchar *key,
+ gboolean is_ai)
{
SnItem *item = g_object_new (sn_item_get_type (), NULL);
item->sn_item_proxy = sn_item_proxy;
+ item->key = g_strdup (key);
item->is_ai = is_ai;
item->cancellable = g_cancellable_new ();
diff --git a/xapp-sn-watcher/sn-item.h b/xapp-sn-watcher/sn-item.h
index 0d277a6d..d0c764aa 100644
--- a/xapp-sn-watcher/sn-item.h
+++ b/xapp-sn-watcher/sn-item.h
@@ -12,7 +12,8 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE (SnItem, sn_item, SN, ITEM, GObject)
-SnItem *sn_item_new (GDBusProxy *sn_item_proxy, gboolean is_ai);
+const gchar *sn_item_get_key (SnItem *item);
+SnItem *sn_item_new (GDBusProxy *sn_item_proxy, const gchar *key, gboolean is_ai);
void sn_item_update_menus (SnItem *item);
#define STATUS_ICON_SCHEMA "org.x.apps.statusicon"
diff --git a/xapp-sn-watcher/xapp-sn-watcher.c b/xapp-sn-watcher/xapp-sn-watcher.c
index c92de86c..2bf7f857 100644
--- a/xapp-sn-watcher/xapp-sn-watcher.c
+++ b/xapp-sn-watcher/xapp-sn-watcher.c
@@ -51,6 +51,7 @@ GSettings *xapp_settings;
static void continue_startup (XAppSnWatcher *watcher);
static void update_published_items (XAppSnWatcher *watcher);
+static void sn_item_removed (XAppSnWatcher *watcher, SnItem *item);
static void
handle_status_applet_name_owner_appeared (XAppSnWatcher *watcher,
@@ -82,6 +83,23 @@ handle_status_applet_name_owner_appeared (XAppSnWatcher *watcher,
}
}
+static void
+remove_item_by_key (XAppSnWatcher *watcher,
+ const gchar *key,
+ const gchar *reason)
+{
+ DEBUG ("%s: %s", reason, key);
+ g_hash_table_remove (watcher->items, key);
+ update_published_items (watcher);
+}
+
+static void
+sn_item_removed (XAppSnWatcher *watcher,
+ SnItem *item)
+{
+ remove_item_by_key (watcher, sn_item_get_key (item), "SNI item removed (interface unexported)");
+}
+
static void
handle_sn_item_name_owner_lost (XAppSnWatcher *watcher,
const gchar *name,
@@ -97,10 +115,7 @@ handle_sn_item_name_owner_lost (XAppSnWatcher *watcher,
if (g_str_has_prefix (key, name))
{
- DEBUG ("Client %s has exited, removing status icon", key);
- g_hash_table_remove (watcher->items, key);
-
- update_published_items (watcher);
+ remove_item_by_key (watcher, key, "Client has exited");
break;
}
}
@@ -374,8 +389,15 @@ sn_item_proxy_new_completed (GObject *source,
}
item = sn_item_new ((GDBusProxy *) proxy,
+ stolen_ptr,
g_str_has_prefix (data->path, APPINDICATOR_PATH_PREFIX));
+ /* Connect to removed signal to handle interface removal/unexport */
+ g_signal_connect_swapped (item,
+ "removed",
+ G_CALLBACK (sn_item_removed),
+ watcher);
+
g_hash_table_insert (watcher->items,
stolen_ptr,
item);