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);