mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-06 17:08:53 +00:00
[client/ui-wails] Add submenu support to the XEmbed tray popup
Recursively walk dbusmenu children-display="submenu" entries when flattening the SNI menu so the GTK popup can render nested items. The C side renders submenu folders as labeled buttons that open a child popup window aligned to the anchor row, kept on-screen with horizontal flipping; the top-level popup no longer self-destructs when focus transfers to one of its own submenus. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ package main
|
||||
#include "xembed_tray_linux.h"
|
||||
#include <X11/Xlib.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
@@ -28,17 +29,30 @@ var (
|
||||
activeMenuHostMu sync.Mutex
|
||||
)
|
||||
|
||||
//export goMenuItemClicked
|
||||
func goMenuItemClicked(id C.int) {
|
||||
activeMenuHostMu.Lock()
|
||||
h := activeMenuHost
|
||||
activeMenuHostMu.Unlock()
|
||||
|
||||
if h != nil {
|
||||
go h.sendMenuEvent(int32(id))
|
||||
}
|
||||
// menuItemInfo is the Go-side representation of one popup menu entry,
|
||||
// flattened from a dbusMenuLayout tree before it is handed to the C
|
||||
// popup builder. Submenus populate children; leaves leave it nil.
|
||||
type menuItemInfo struct {
|
||||
id int32
|
||||
label string
|
||||
enabled bool
|
||||
isCheck bool
|
||||
checked bool
|
||||
isSeparator bool
|
||||
children []menuItemInfo
|
||||
}
|
||||
|
||||
// dbusMenuLayout mirrors the (ia{sv}av) result returned by
|
||||
// com.canonical.dbusmenu.GetLayout. The Children variants each wrap a
|
||||
// nested dbusMenuLayout; we decode them lazily in flattenMenu.
|
||||
type dbusMenuLayout struct {
|
||||
ID int32
|
||||
Properties map[string]dbus.Variant
|
||||
Children []dbus.Variant
|
||||
}
|
||||
|
||||
|
||||
|
||||
// xembedHost manages one XEmbed tray icon for an SNI item.
|
||||
type xembedHost struct {
|
||||
conn *dbus.Conn
|
||||
@@ -58,6 +72,23 @@ type xembedHost struct {
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// goMenuItemClicked is the C callback invoked from the GTK main thread
|
||||
// when the user activates a popup-menu entry. C callbacks cannot carry
|
||||
// Go pointers, so the active xembedHost is looked up through the
|
||||
// activeMenuHost global instead. //export makes this symbol visible to
|
||||
// the C side; the function must therefore live in package main.
|
||||
//
|
||||
//export goMenuItemClicked
|
||||
func goMenuItemClicked(id C.int) {
|
||||
activeMenuHostMu.Lock()
|
||||
h := activeMenuHost
|
||||
activeMenuHostMu.Unlock()
|
||||
|
||||
if h != nil {
|
||||
go h.sendMenuEvent(int32(id))
|
||||
}
|
||||
}
|
||||
|
||||
// newXembedHost creates an XEmbed tray icon for the given SNI item.
|
||||
// Returns an error if no XEmbed tray manager is available (graceful fallback).
|
||||
func newXembedHost(conn *dbus.Conn, busName string, objPath dbus.ObjectPath) (*xembedHost, error) {
|
||||
@@ -253,25 +284,16 @@ func (h *xembedHost) contextMenu(x, y int32) {
|
||||
return
|
||||
}
|
||||
|
||||
// Build C menu item array.
|
||||
cItems := make([]C.xembed_menu_item, len(items))
|
||||
cLabels := make([]*C.char, len(items)) // track for freeing
|
||||
for i, mi := range items {
|
||||
cItems[i].id = C.int(mi.id)
|
||||
cItems[i].enabled = boolToInt(mi.enabled)
|
||||
cItems[i].is_check = boolToInt(mi.isCheck)
|
||||
cItems[i].checked = boolToInt(mi.checked)
|
||||
cItems[i].is_separator = boolToInt(mi.isSeparator)
|
||||
if mi.label != "" {
|
||||
cLabels[i] = C.CString(mi.label)
|
||||
cItems[i].label = cLabels[i]
|
||||
}
|
||||
}
|
||||
// Build a C-allocated tree from the Go menu. xembed_show_popup_menu
|
||||
// deep-copies into its own buffer (so it can outlive this stack
|
||||
// frame), but it expects valid C strings + pointers in the caller's
|
||||
// array — we still have to walk the items on the Go side and build
|
||||
// matching C.xembed_menu_item nodes recursively.
|
||||
var allocs []unsafe.Pointer
|
||||
cItems := buildCItems(items, &allocs)
|
||||
defer func() {
|
||||
for _, p := range cLabels {
|
||||
if p != nil {
|
||||
C.free(unsafe.Pointer(p))
|
||||
}
|
||||
for _, p := range allocs {
|
||||
C.free(p)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -280,26 +302,10 @@ func (h *xembedHost) contextMenu(x, y int32) {
|
||||
activeMenuHost = h
|
||||
activeMenuHostMu.Unlock()
|
||||
|
||||
C.xembed_show_popup_menu(&cItems[0], C.int(len(cItems)),
|
||||
C.xembed_show_popup_menu(cItems, C.int(len(items)),
|
||||
nil, C.int(x), C.int(y))
|
||||
}
|
||||
|
||||
// dbusMenuLayout represents a com.canonical.dbusmenu layout item.
|
||||
type dbusMenuLayout struct {
|
||||
ID int32
|
||||
Properties map[string]dbus.Variant
|
||||
Children []dbus.Variant
|
||||
}
|
||||
|
||||
type menuItemInfo struct {
|
||||
id int32
|
||||
label string
|
||||
enabled bool
|
||||
isCheck bool
|
||||
checked bool
|
||||
isSeparator bool
|
||||
}
|
||||
|
||||
func (h *xembedHost) flattenMenu(layout dbusMenuLayout) []menuItemInfo {
|
||||
var items []menuItemInfo
|
||||
|
||||
@@ -352,6 +358,16 @@ func (h *xembedHost) flattenMenu(layout dbusMenuLayout) []menuItemInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into nested submenus. The dbusmenu spec marks a folder
|
||||
// item with children-display=="submenu"; the children are already
|
||||
// in child.Children because GetLayout was called with
|
||||
// recursionDepth=-1 (all levels).
|
||||
if v, ok := child.Properties["children-display"]; ok {
|
||||
if s, ok := v.Value().(string); ok && s == "submenu" {
|
||||
mi.children = h.flattenMenu(child)
|
||||
}
|
||||
}
|
||||
|
||||
items = append(items, mi)
|
||||
}
|
||||
|
||||
@@ -369,13 +385,6 @@ func (h *xembedHost) sendMenuEvent(id int32) {
|
||||
}
|
||||
}
|
||||
|
||||
func boolToInt(b bool) C.int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (h *xembedHost) stop() {
|
||||
select {
|
||||
case <-h.stopCh:
|
||||
@@ -387,3 +396,49 @@ func (h *xembedHost) stop() {
|
||||
C.xembed_destroy_icon(h.dpy, h.iconWin)
|
||||
C.XCloseDisplay(h.dpy)
|
||||
}
|
||||
|
||||
// buildCItems recursively translates Go menuItemInfo slices into a
|
||||
// C-allocated array of xembed_menu_item suitable for passing across the
|
||||
// Cgo boundary. The C side deep-copies the structure when it stages
|
||||
// the popup, so any transient labels/children we allocate here can be
|
||||
// released as soon as xembed_show_popup_menu returns. Every malloc is
|
||||
// recorded in *allocs so the caller can free it via a single deferred
|
||||
// loop. Returns nil for empty slices.
|
||||
func buildCItems(items []menuItemInfo, allocs *[]unsafe.Pointer) *C.xembed_menu_item {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
size := C.size_t(len(items)) * C.size_t(unsafe.Sizeof(C.xembed_menu_item{}))
|
||||
arr := C.malloc(size)
|
||||
*allocs = append(*allocs, arr)
|
||||
C.memset(arr, 0, size)
|
||||
|
||||
slice := (*[1 << 16]C.xembed_menu_item)(arr)[:len(items):len(items)]
|
||||
for i, mi := range items {
|
||||
slice[i].id = C.int(mi.id)
|
||||
slice[i].enabled = boolToInt(mi.enabled)
|
||||
slice[i].is_check = boolToInt(mi.isCheck)
|
||||
slice[i].checked = boolToInt(mi.checked)
|
||||
slice[i].is_separator = boolToInt(mi.isSeparator)
|
||||
if mi.label != "" {
|
||||
cstr := C.CString(mi.label)
|
||||
*allocs = append(*allocs, unsafe.Pointer(cstr))
|
||||
slice[i].label = cstr
|
||||
}
|
||||
if len(mi.children) > 0 {
|
||||
slice[i].children = buildCItems(mi.children, allocs)
|
||||
slice[i].child_count = C.int(len(mi.children))
|
||||
}
|
||||
}
|
||||
|
||||
return (*C.xembed_menu_item)(arr)
|
||||
}
|
||||
|
||||
// boolToInt converts a Go bool to the C int the dbusmenu C API uses
|
||||
// for boolean flags.
|
||||
func boolToInt(b bool) C.int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -246,8 +246,11 @@ int xembed_poll_event(Display *dpy, Window icon_win,
|
||||
/* Implemented in Go via //export */
|
||||
extern void goMenuItemClicked(int id);
|
||||
|
||||
/* The popup window, reused across invocations. */
|
||||
/* The top-level popup window, reused across invocations. Submenu
|
||||
popups are tracked in a separate list so they all close when the
|
||||
top-level closes. */
|
||||
static GtkWidget *popup_win = NULL;
|
||||
static GList *submenu_popups = NULL; /* list of GtkWidget* */
|
||||
|
||||
typedef struct {
|
||||
xembed_menu_item *items;
|
||||
@@ -255,38 +258,219 @@ typedef struct {
|
||||
int x, y;
|
||||
} popup_data;
|
||||
|
||||
/* Deep-free a heap-owned xembed_menu_item array (label + children). */
|
||||
static void free_items(xembed_menu_item *items, int count) {
|
||||
if (!items) return;
|
||||
for (int i = 0; i < count; i++) {
|
||||
free((void *)items[i].label);
|
||||
free_items(items[i].children, items[i].child_count);
|
||||
}
|
||||
free(items);
|
||||
}
|
||||
|
||||
static void free_popup_data(popup_data *pd) {
|
||||
if (!pd) return;
|
||||
for (int i = 0; i < pd->count; i++)
|
||||
free((void *)pd->items[i].label);
|
||||
free(pd->items);
|
||||
free_items(pd->items, pd->count);
|
||||
free(pd);
|
||||
}
|
||||
|
||||
static void on_button_clicked(GtkButton *btn, gpointer user_data) {
|
||||
int id = GPOINTER_TO_INT(user_data);
|
||||
if (popup_win)
|
||||
/* Close every popup window — top-level plus any open submenus.
|
||||
Called when the user clicks an actionable item or focus leaves the
|
||||
top-level window. */
|
||||
static void close_all_popups(void) {
|
||||
for (GList *l = submenu_popups; l; l = l->next) {
|
||||
gtk_widget_destroy(GTK_WIDGET(l->data));
|
||||
}
|
||||
g_list_free(submenu_popups);
|
||||
submenu_popups = NULL;
|
||||
|
||||
if (popup_win) {
|
||||
gtk_widget_hide(popup_win);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_button_clicked(GtkButton *btn, gpointer user_data) {
|
||||
(void)btn;
|
||||
int id = GPOINTER_TO_INT(user_data);
|
||||
close_all_popups();
|
||||
goMenuItemClicked(id);
|
||||
}
|
||||
|
||||
static void on_check_toggled(GtkToggleButton *btn, gpointer user_data) {
|
||||
(void)btn;
|
||||
int id = GPOINTER_TO_INT(user_data);
|
||||
if (popup_win)
|
||||
gtk_widget_hide(popup_win);
|
||||
close_all_popups();
|
||||
goMenuItemClicked(id);
|
||||
}
|
||||
|
||||
static gboolean on_popup_focus_out(GtkWidget *widget, GdkEvent *event,
|
||||
gpointer user_data) {
|
||||
gtk_widget_hide(widget);
|
||||
(void)widget; (void)event; (void)user_data;
|
||||
/* Don't tear the menu down when the top-level loses focus to one of
|
||||
its own submenu popups. close_all_popups() would destroy the
|
||||
submenu we just opened. The submenu's own focus-out handler will
|
||||
close it (and us) when focus leaves the popup tree. */
|
||||
if (submenu_popups != NULL)
|
||||
return FALSE;
|
||||
close_all_popups();
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Forward declaration — submenu buttons need to schedule a child popup. */
|
||||
static GtkWidget *build_menu_box(xembed_menu_item *items, int count);
|
||||
|
||||
typedef struct {
|
||||
xembed_menu_item *items;
|
||||
int count;
|
||||
GtkWidget *anchor; /* the submenu button — used to position the popup */
|
||||
} submenu_open_data;
|
||||
|
||||
/* Tear-down companion for submenu_open_data: detach from submenu_popups
|
||||
and destroy. Used when the focus-out handler fires on a submenu. */
|
||||
static gboolean on_submenu_focus_out(GtkWidget *widget, GdkEvent *event,
|
||||
gpointer user_data) {
|
||||
(void)event; (void)user_data;
|
||||
submenu_popups = g_list_remove(submenu_popups, widget);
|
||||
gtk_widget_destroy(widget);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void on_submenu_button_clicked(GtkButton *btn, gpointer user_data) {
|
||||
submenu_open_data *sd = (submenu_open_data *)user_data;
|
||||
|
||||
GtkWidget *win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
gtk_window_set_type_hint(GTK_WINDOW(win), GDK_WINDOW_TYPE_HINT_POPUP_MENU);
|
||||
gtk_window_set_decorated(GTK_WINDOW(win), FALSE);
|
||||
gtk_window_set_resizable(GTK_WINDOW(win), FALSE);
|
||||
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(win), TRUE);
|
||||
gtk_window_set_skip_pager_hint(GTK_WINDOW(win), TRUE);
|
||||
gtk_window_set_keep_above(GTK_WINDOW(win), TRUE);
|
||||
|
||||
g_signal_connect(win, "focus-out-event",
|
||||
G_CALLBACK(on_submenu_focus_out), NULL);
|
||||
|
||||
GtkWidget *vbox = build_menu_box(sd->items, sd->count);
|
||||
gtk_container_add(GTK_CONTAINER(win), vbox);
|
||||
|
||||
/* GtkButton has no native GdkWindow of its own — gtk_widget_get_window
|
||||
returns the parent popup's window. To get the button's screen-space
|
||||
position we read the popup origin (ox, oy) and add the button's
|
||||
allocation within the popup. */
|
||||
gint ox, oy;
|
||||
gdk_window_get_origin(gtk_widget_get_window(GTK_WIDGET(btn)), &ox, &oy);
|
||||
GtkAllocation alloc;
|
||||
gtk_widget_get_allocation(GTK_WIDGET(btn), &alloc);
|
||||
int ax = ox + alloc.x;
|
||||
int ay = oy + alloc.y;
|
||||
|
||||
gtk_widget_show_all(win);
|
||||
gint sw, sh;
|
||||
gtk_window_get_size(GTK_WINDOW(win), &sw, &sh);
|
||||
|
||||
/* The parent popup grows upward from the tray, so submenu items
|
||||
sit closer to the bottom of the screen than to the top. Align
|
||||
the submenu's BOTTOM to the anchor button's bottom: the popup
|
||||
grows upward, level with the row that opened it. Don't clamp
|
||||
to the monitor top — that would re-position the submenu next
|
||||
to an unrelated sibling row above the anchor. */
|
||||
int final_x = ax + alloc.width;
|
||||
int final_y = ay + alloc.height - sh;
|
||||
|
||||
/* Horizontal flip against the monitor under the anchor button. */
|
||||
GdkDisplay *display = gtk_widget_get_display(win);
|
||||
GdkMonitor *monitor = gdk_display_get_monitor_at_point(display, ax, ay);
|
||||
if (monitor) {
|
||||
GdkRectangle geom;
|
||||
gdk_monitor_get_geometry(monitor, &geom);
|
||||
if (final_x + sw > geom.x + geom.width)
|
||||
final_x = ax - sw; /* flip to the left */
|
||||
}
|
||||
|
||||
gtk_window_move(GTK_WINDOW(win), final_x, final_y);
|
||||
gtk_window_present(GTK_WINDOW(win));
|
||||
|
||||
submenu_popups = g_list_prepend(submenu_popups, win);
|
||||
}
|
||||
|
||||
/* Build a vbox of GtkWidgets for the supplied items. Used for both the
|
||||
top-level popup and each submenu popup. The submenu_open_data attached
|
||||
to submenu buttons is freed when the submenu_popups list is cleared
|
||||
(we use the button's "destroy" signal). */
|
||||
static void on_button_destroy_free_data(GtkWidget *widget, gpointer user_data) {
|
||||
(void)widget;
|
||||
free(user_data);
|
||||
}
|
||||
|
||||
static GtkWidget *build_menu_box(xembed_menu_item *items, int count) {
|
||||
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
xembed_menu_item *mi = &items[i];
|
||||
|
||||
if (mi->is_separator) {
|
||||
GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_box_pack_start(GTK_BOX(vbox), sep, FALSE, FALSE, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mi->is_check) {
|
||||
GtkWidget *chk = gtk_check_button_new_with_label(
|
||||
mi->label ? mi->label : "");
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(chk), mi->checked);
|
||||
gtk_widget_set_sensitive(chk, mi->enabled);
|
||||
g_signal_connect(chk, "toggled",
|
||||
G_CALLBACK(on_check_toggled),
|
||||
GINT_TO_POINTER(mi->id));
|
||||
gtk_box_pack_start(GTK_BOX(vbox), chk, FALSE, FALSE, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Plain button (leaf) or submenu opener. Show "Label ▸" for
|
||||
submenu folders so users see they're nested. */
|
||||
const char *label_text = mi->label ? mi->label : "";
|
||||
char *display_label = NULL;
|
||||
if (mi->child_count > 0 && mi->children) {
|
||||
/* Compose "label ▸" (BLACK RIGHT-POINTING SMALL TRIANGLE). */
|
||||
size_t n = strlen(label_text) + 8; /* ascii + " ▸" + NUL */
|
||||
display_label = (char *)malloc(n);
|
||||
snprintf(display_label, n, "%s \xE2\x96\xB8", label_text);
|
||||
label_text = display_label;
|
||||
}
|
||||
|
||||
GtkWidget *btn = gtk_button_new_with_label(label_text);
|
||||
gtk_widget_set_sensitive(btn, mi->enabled);
|
||||
gtk_button_set_relief(GTK_BUTTON(btn), GTK_RELIEF_NONE);
|
||||
GtkWidget *lbl = gtk_bin_get_child(GTK_BIN(btn));
|
||||
if (lbl) gtk_label_set_xalign(GTK_LABEL(lbl), 0.0);
|
||||
|
||||
free(display_label);
|
||||
|
||||
if (mi->child_count > 0 && mi->children) {
|
||||
submenu_open_data *sd =
|
||||
(submenu_open_data *)calloc(1, sizeof(submenu_open_data));
|
||||
sd->items = mi->children;
|
||||
sd->count = mi->child_count;
|
||||
sd->anchor = btn;
|
||||
g_signal_connect(btn, "clicked",
|
||||
G_CALLBACK(on_submenu_button_clicked), sd);
|
||||
g_signal_connect(btn, "destroy",
|
||||
G_CALLBACK(on_button_destroy_free_data), sd);
|
||||
} else {
|
||||
g_signal_connect(btn, "clicked",
|
||||
G_CALLBACK(on_button_clicked),
|
||||
GINT_TO_POINTER(mi->id));
|
||||
}
|
||||
gtk_box_pack_start(GTK_BOX(vbox), btn, FALSE, FALSE, 0);
|
||||
}
|
||||
|
||||
return vbox;
|
||||
}
|
||||
|
||||
static gboolean popup_menu_idle(gpointer user_data) {
|
||||
popup_data *pd = (popup_data *)user_data;
|
||||
|
||||
/* Destroy old popup if it exists. */
|
||||
/* Destroy old top-level (and orphan submenus) before rebuilding. */
|
||||
close_all_popups();
|
||||
if (popup_win) {
|
||||
gtk_widget_destroy(popup_win);
|
||||
popup_win = NULL;
|
||||
@@ -305,43 +489,9 @@ static gboolean popup_menu_idle(gpointer user_data) {
|
||||
g_signal_connect(popup_win, "focus-out-event",
|
||||
G_CALLBACK(on_popup_focus_out), NULL);
|
||||
|
||||
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
GtkWidget *vbox = build_menu_box(pd->items, pd->count);
|
||||
gtk_container_add(GTK_CONTAINER(popup_win), vbox);
|
||||
|
||||
for (int i = 0; i < pd->count; i++) {
|
||||
xembed_menu_item *mi = &pd->items[i];
|
||||
|
||||
if (mi->is_separator) {
|
||||
GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_box_pack_start(GTK_BOX(vbox), sep, FALSE, FALSE, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mi->is_check) {
|
||||
GtkWidget *chk = gtk_check_button_new_with_label(
|
||||
mi->label ? mi->label : "");
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(chk), mi->checked);
|
||||
gtk_widget_set_sensitive(chk, mi->enabled);
|
||||
g_signal_connect(chk, "toggled",
|
||||
G_CALLBACK(on_check_toggled),
|
||||
GINT_TO_POINTER(mi->id));
|
||||
gtk_box_pack_start(GTK_BOX(vbox), chk, FALSE, FALSE, 0);
|
||||
} else {
|
||||
GtkWidget *btn = gtk_button_new_with_label(
|
||||
mi->label ? mi->label : "");
|
||||
gtk_widget_set_sensitive(btn, mi->enabled);
|
||||
gtk_button_set_relief(GTK_BUTTON(btn), GTK_RELIEF_NONE);
|
||||
/* Left-align label. */
|
||||
GtkWidget *label = gtk_bin_get_child(GTK_BIN(btn));
|
||||
if (label)
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0);
|
||||
g_signal_connect(btn, "clicked",
|
||||
G_CALLBACK(on_button_clicked),
|
||||
GINT_TO_POINTER(mi->id));
|
||||
gtk_box_pack_start(GTK_BOX(vbox), btn, FALSE, FALSE, 0);
|
||||
}
|
||||
}
|
||||
|
||||
gtk_widget_show_all(popup_win);
|
||||
|
||||
/* Position the window above the click point (menu grows upward from tray). */
|
||||
@@ -356,24 +506,45 @@ static gboolean popup_menu_idle(gpointer user_data) {
|
||||
/* Grab focus so focus-out-event works. */
|
||||
gtk_window_present(GTK_WINDOW(popup_win));
|
||||
|
||||
free_popup_data(pd);
|
||||
/* The vbox+children retain pointers into pd->items (via submenu
|
||||
click handlers). free_popup_data() walks the array recursively
|
||||
to release labels and children buffers — but we need to keep
|
||||
the items alive while the popup is open. Defer the free until
|
||||
the popup window is destroyed. */
|
||||
g_object_set_data_full(G_OBJECT(popup_win), "popup_data", pd,
|
||||
(GDestroyNotify)free_popup_data);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/* Recursively deep-copy a Go-supplied items array into freshly-allocated
|
||||
C memory. Each label is strdup'd, each children array is calloc'd. */
|
||||
static xembed_menu_item *copy_items(xembed_menu_item *src, int count) {
|
||||
if (count <= 0 || !src) return NULL;
|
||||
xembed_menu_item *dst =
|
||||
(xembed_menu_item *)calloc(count, sizeof(xembed_menu_item));
|
||||
for (int i = 0; i < count; i++) {
|
||||
dst[i] = src[i];
|
||||
if (src[i].label)
|
||||
dst[i].label = strdup(src[i].label);
|
||||
if (src[i].child_count > 0 && src[i].children) {
|
||||
dst[i].children = copy_items(src[i].children, src[i].child_count);
|
||||
dst[i].child_count = src[i].child_count;
|
||||
} else {
|
||||
dst[i].children = NULL;
|
||||
dst[i].child_count = 0;
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
void xembed_show_popup_menu(xembed_menu_item *items, int count,
|
||||
xembed_menu_click_cb cb, int x, int y) {
|
||||
(void)cb;
|
||||
popup_data *pd = (popup_data *)calloc(1, sizeof(popup_data));
|
||||
pd->items = (xembed_menu_item *)calloc(count, sizeof(xembed_menu_item));
|
||||
pd->items = copy_items(items, count);
|
||||
pd->count = count;
|
||||
pd->x = x;
|
||||
pd->y = y;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
pd->items[i] = items[i];
|
||||
if (items[i].label)
|
||||
pd->items[i].label = strdup(items[i].label);
|
||||
}
|
||||
|
||||
g_idle_add(popup_menu_idle, pd);
|
||||
}
|
||||
|
||||
@@ -55,13 +55,17 @@ typedef void (*xembed_menu_click_cb)(int id);
|
||||
// x, y are root coordinates for positioning the popup.
|
||||
// This must be called from the GTK main thread (use g_idle_add).
|
||||
|
||||
typedef struct {
|
||||
typedef struct xembed_menu_item {
|
||||
int id; // dbusmenu item ID
|
||||
const char *label; // display label (NULL for separator)
|
||||
int enabled; // whether the item is clickable
|
||||
int is_check; // whether this is a checkbox item
|
||||
int checked; // checkbox state (0 or 1)
|
||||
int is_separator;// 1 if this is a separator
|
||||
// children + child_count populate when this item is a submenu folder
|
||||
// (dbusmenu's children-display=="submenu"). NULL/0 means leaf item.
|
||||
struct xembed_menu_item *children;
|
||||
int child_count;
|
||||
} xembed_menu_item;
|
||||
|
||||
// Schedule a GTK popup menu on the main thread.
|
||||
|
||||
Reference in New Issue
Block a user