summaryrefslogtreecommitdiff
path: root/applets/wncklet
diff options
context:
space:
mode:
authorAleksey Samoilov <[email protected]>2026-09-14 23:29:50 +0400
committerGitHub <[email protected]>2026-09-14 19:29:50 +0000
commit6cdfb68cb38eaa8164a7ab60d9697bcfc8a22952 (patch)
treed22d42c10487b219bcb63894210d60ef116a11bc /applets/wncklet
parenta5e411313410f633dc86408bd8d7ccbe6226d894 (diff)
downloadmate-panel-6cdfb68cb38eaa8164a7ab60d9697bcfc8a22952.tar.bz2
mate-panel-6cdfb68cb38eaa8164a7ab60d9697bcfc8a22952.tar.xz
applets/window-selector: port to Wayland (#1582)
* applets/window-selector: port to Wayland * wncklet: fix invalid GtkWindow cast in about dialogs * wayland/window-selector: fix menu rebuild * wayland/window-selector: rebuild triggers on the submenu's show signal
Diffstat (limited to 'applets/wncklet')
-rw-r--r--applets/wncklet/Makefile.am4
-rw-r--r--applets/wncklet/meson.build2
-rw-r--r--applets/wncklet/org.mate.panel.Wncklet.mate-panel-applet.desktop.in.in2
-rw-r--r--applets/wncklet/showdesktop.c6
-rw-r--r--applets/wncklet/wayland-window-menu.c789
-rw-r--r--applets/wncklet/wayland-window-menu.h43
-rw-r--r--applets/wncklet/window-list.c6
-rw-r--r--applets/wncklet/window-menu.c9
-rw-r--r--applets/wncklet/workspace-switcher.c6
9 files changed, 860 insertions, 7 deletions
diff --git a/applets/wncklet/Makefile.am b/applets/wncklet/Makefile.am
index 08a82d11..dfcca241 100644
--- a/applets/wncklet/Makefile.am
+++ b/applets/wncklet/Makefile.am
@@ -42,7 +42,9 @@ WNCKLET_SOURCES += \
wayland-backend.c \
wayland-backend.h \
wayland-workspace.c \
- wayland-workspace.h
+ wayland-workspace.h \
+ wayland-window-menu.c \
+ wayland-window-menu.h
WNCKLET_LDADD += \
$(XFW_LIBS)
diff --git a/applets/wncklet/meson.build b/applets/wncklet/meson.build
index f5c7c155..55d19080 100644
--- a/applets/wncklet/meson.build
+++ b/applets/wncklet/meson.build
@@ -60,6 +60,8 @@ if have_wayland
'wayland-backend.h',
'wayland-workspace.c',
'wayland-workspace.h',
+ 'wayland-window-menu.c',
+ 'wayland-window-menu.h',
]
endif
diff --git a/applets/wncklet/org.mate.panel.Wncklet.mate-panel-applet.desktop.in.in b/applets/wncklet/org.mate.panel.Wncklet.mate-panel-applet.desktop.in.in
index 6a2285af..2b409779 100644
--- a/applets/wncklet/org.mate.panel.Wncklet.mate-panel-applet.desktop.in.in
+++ b/applets/wncklet/org.mate.panel.Wncklet.mate-panel-applet.desktop.in.in
@@ -11,7 +11,7 @@ Description=Switch between open windows using a menu
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=mate-panel-window-menu
MateComponentId=OAFIID:MATE_WindowMenuApplet;
-Platforms=X11;
+Platforms=X11;Wayland;
X-MATE-Bugzilla-Bugzilla=MATE
X-MATE-Bugzilla-Product=mate-panel
X-MATE-Bugzilla-Component=window selector
diff --git a/applets/wncklet/showdesktop.c b/applets/wncklet/showdesktop.c
index 9125a59e..d393d6c3 100644
--- a/applets/wncklet/showdesktop.c
+++ b/applets/wncklet/showdesktop.c
@@ -574,7 +574,11 @@ static void display_about_dialog(GtkAction* action, ShowDesktopData* sdd)
NULL
};
- gtk_show_about_dialog(GTK_WINDOW(sdd->applet),
+ GtkWidget* toplevel = gtk_widget_get_toplevel (sdd->applet);
+ if (!GTK_IS_WINDOW (toplevel))
+ toplevel = NULL;
+
+ gtk_show_about_dialog(toplevel ? GTK_WINDOW(toplevel) : NULL,
"program-name", _("Show Desktop Button"),
"title", _("About Show Desktop Button"),
"authors", authors,
diff --git a/applets/wncklet/wayland-window-menu.c b/applets/wncklet/wayland-window-menu.c
new file mode 100644
index 00000000..5080db15
--- /dev/null
+++ b/applets/wncklet/wayland-window-menu.c
@@ -0,0 +1,789 @@
+/* Wayland backend for the Window Selector applet */
+
+/*
+ * Copyright (C) 2026 MATE Desktop Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+#include <config.h>
+
+#ifndef HAVE_WAYLAND
+#error file should only be compiled when HAVE_WAYLAND is enabled
+#endif
+
+#include <gtk/gtk.h>
+#include <gdk/gdkwayland.h>
+#include <glib/gi18n.h>
+#include <libxfce4windowing/libxfce4windowing.h>
+
+#include "wayland-window-menu.h"
+
+#define SELECTOR_MAX_WIDTH 50
+
+/* a window in the drag-and-drop payload is identified in-process by the
+ * XfwWindow pointer, matching the tasklist backend (see wayland-backend.c) */
+static GtkTargetEntry source_targets[] = {
+ { "application/x-wnck-window-id", 0, 0 }
+};
+
+typedef struct {
+ GtkWidget *menu;
+ GtkWidget *root_item;
+ GtkWidget *image;
+ GtkWidget *submenu;
+ GtkWidget *no_windows_item;
+ GHashTable *window_hash;
+ XfwScreen *screen;
+ XfwWorkspaceManager *workspace_manager;
+} WaylandWindowMenu;
+
+static void rebuild_menu (WaylandWindowMenu *data);
+
+/* ---- Helpers ---- */
+
+static XfwWorkspace *
+get_active_workspace (WaylandWindowMenu *data)
+{
+ GList *workspaces, *l;
+
+ if (!data->workspace_manager)
+ return NULL;
+
+ workspaces = xfw_workspace_manager_list_workspaces (data->workspace_manager);
+ for (l = workspaces; l != NULL; l = l->next)
+ {
+ XfwWorkspace *ws = l->data;
+ XfwWorkspaceState state = xfw_workspace_get_state (ws);
+ if (state & XFW_WORKSPACE_STATE_ACTIVE)
+ return ws;
+ }
+
+ return NULL;
+}
+
+static void
+update_root_icon (WaylandWindowMenu *data)
+{
+ XfwWindow *window;
+ GIcon *icon = NULL;
+
+ window = xfw_screen_get_active_window (data->screen);
+ if (window)
+ {
+ XfwApplication *app = xfw_window_get_application (window);
+ if (app)
+ icon = xfw_application_get_gicon (app);
+ if (icon == NULL)
+ icon = xfw_window_get_gicon (window);
+ }
+
+ if (icon != NULL)
+ gtk_image_set_from_gicon (GTK_IMAGE (data->image), icon,
+ GTK_ICON_SIZE_LARGE_TOOLBAR);
+ else
+ gtk_image_set_from_icon_name (GTK_IMAGE (data->image),
+ "mate-panel-window-menu",
+ GTK_ICON_SIZE_LARGE_TOOLBAR);
+}
+
+/* ---- Per-window signal handlers ---- */
+
+static void
+on_menu_item_activate (GtkMenuItem *item, XfwWindow *window)
+{
+ xfw_window_activate (window, NULL,
+ gtk_get_current_event_time (), NULL);
+}
+
+static void
+on_drag_begin (GtkWidget *widget, GdkDragContext *context, XfwWindow *window)
+{
+ GdkPixbuf *icon;
+ gint scale = gtk_widget_get_scale_factor (widget);
+
+ icon = xfw_window_get_icon (window, 16, scale);
+ if (icon != NULL)
+ gtk_drag_set_icon_pixbuf (context, icon, 0, 0);
+}
+
+static void
+on_drag_data_get (GtkWidget *widget, GdkDragContext *context,
+ GtkSelectionData *selection_data, guint info, guint time,
+ XfwWindow *window)
+{
+ gulong wid = (gulong) window;
+ gtk_selection_data_set (selection_data,
+ gtk_selection_data_get_target (selection_data),
+ 8, (guchar *) &wid, sizeof (wid));
+}
+
+static void
+set_icon_dimmed (GtkWidget *image, gboolean dimmed)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (image);
+
+ if (dimmed)
+ gtk_style_context_add_class (context, "wnck-selector-dimmed");
+ else
+ gtk_style_context_remove_class (context, "wnck-selector-dimmed");
+}
+
+static void
+on_window_name_changed (XfwWindow *window, GtkWidget *item)
+{
+ GtkWidget *label = g_object_get_data (G_OBJECT (item), "label-widget");
+ const char *name;
+
+ if (label == NULL)
+ return;
+
+ name = xfw_window_get_name (window);
+ if (name == NULL)
+ name = "";
+ gtk_label_set_label (GTK_LABEL (label), name);
+}
+
+static void
+on_window_icon_changed (XfwWindow *window, GtkWidget *item)
+{
+ GtkWidget *image = g_object_get_data (G_OBJECT (item), "icon-widget");
+ GIcon *icon = NULL;
+ XfwApplication *app;
+
+ if (image == NULL)
+ return;
+
+ app = xfw_window_get_application (window);
+ if (app != NULL)
+ icon = xfw_application_get_gicon (app);
+ if (icon == NULL)
+ icon = xfw_window_get_gicon (window);
+
+ if (icon != NULL)
+ gtk_image_set_from_gicon (GTK_IMAGE (image), icon,
+ GTK_ICON_SIZE_MENU);
+ else
+ gtk_image_set_from_icon_name (GTK_IMAGE (image), "unknown",
+ GTK_ICON_SIZE_MENU);
+}
+
+static void
+on_window_state_changed (XfwWindow *window, XfwWindowState changed_mask,
+ XfwWindowState new_state, GtkWidget *item)
+{
+ if (changed_mask & XFW_WINDOW_STATE_SKIP_TASKLIST)
+ {
+ if (xfw_window_is_skip_tasklist (window))
+ gtk_widget_hide (item);
+ else
+ gtk_widget_show (item);
+ }
+
+ if (changed_mask & XFW_WINDOW_STATE_URGENT)
+ {
+ GtkWidget *label = g_object_get_data (G_OBJECT (item),
+ "label-widget");
+ if (label == NULL)
+ return;
+
+ if (xfw_window_is_urgent (window))
+ {
+ PangoAttrList *attrs = pango_attr_list_new ();
+ pango_attr_list_insert (attrs,
+ pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+ gtk_label_set_attributes (GTK_LABEL (label), attrs);
+ pango_attr_list_unref (attrs);
+ }
+ else
+ {
+ gtk_label_set_attributes (GTK_LABEL (label), NULL);
+ }
+ }
+
+ if (changed_mask & XFW_WINDOW_STATE_MINIMIZED)
+ {
+ GtkWidget *image = g_object_get_data (G_OBJECT (item),
+ "icon-widget");
+ if (image != NULL)
+ set_icon_dimmed (image,
+ (new_state & XFW_WINDOW_STATE_MINIMIZED) != 0);
+ }
+}
+
+/* ---- Menu item creation ---- */
+
+static GtkWidget *
+create_window_item (WaylandWindowMenu *data, XfwWindow *window)
+{
+ GtkWidget *item, *box, *image, *label;
+ const char *name;
+ GIcon *icon = NULL;
+ XfwApplication *app;
+ XfwWindowState state;
+ PangoAttrList *attrs;
+
+ name = xfw_window_get_name (window);
+ if (name == NULL)
+ name = "";
+
+ /* Keep the window alive while it is tracked by the menu */
+ g_object_ref (window);
+
+ item = gtk_menu_item_new ();
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
+
+ /* icon */
+ image = gtk_image_new ();
+ app = xfw_window_get_application (window);
+ if (app != NULL)
+ icon = xfw_application_get_gicon (app);
+ if (icon == NULL)
+ icon = xfw_window_get_gicon (window);
+
+ if (icon != NULL)
+ gtk_image_set_from_gicon (GTK_IMAGE (image), icon,
+ GTK_ICON_SIZE_MENU);
+ else
+ gtk_image_set_from_icon_name (GTK_IMAGE (image), "unknown",
+ GTK_ICON_SIZE_MENU);
+
+ /* label */
+ label = gtk_label_new (name);
+ gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
+ gtk_label_set_max_width_chars (GTK_LABEL (label), SELECTOR_MAX_WIDTH);
+ gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+
+ /* bold for urgent windows */
+ state = xfw_window_get_state (window);
+ if (state & XFW_WINDOW_STATE_URGENT)
+ {
+ attrs = pango_attr_list_new ();
+ pango_attr_list_insert (attrs,
+ pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+ gtk_label_set_attributes (GTK_LABEL (label), attrs);
+ pango_attr_list_unref (attrs);
+ }
+
+ /* dimmed for minimized windows */
+ if (state & XFW_WINDOW_STATE_MINIMIZED)
+ set_icon_dimmed (image, TRUE);
+
+ gtk_box_pack_start (GTK_BOX (box), image, FALSE, FALSE, 2);
+ gtk_box_pack_start (GTK_BOX (box), label, TRUE, TRUE, 2);
+ gtk_container_add (GTK_CONTAINER (item), box);
+
+ g_object_set_data (G_OBJECT (item), "icon-widget", image);
+ g_object_set_data (G_OBJECT (item), "label-widget", label);
+
+ g_signal_connect (item, "activate",
+ G_CALLBACK (on_menu_item_activate), window);
+
+ g_signal_connect (window, "name-changed",
+ G_CALLBACK (on_window_name_changed), item);
+ g_signal_connect (window, "icon-changed",
+ G_CALLBACK (on_window_icon_changed), item);
+ g_signal_connect (window, "state-changed",
+ G_CALLBACK (on_window_state_changed), item);
+
+ /* drag the menu item to move the window (matches WnckSelector) */
+ gtk_drag_source_set (item, GDK_BUTTON1_MASK,
+ source_targets, G_N_ELEMENTS (source_targets),
+ GDK_ACTION_MOVE);
+ g_signal_connect (item, "drag-begin",
+ G_CALLBACK (on_drag_begin), window);
+ g_signal_connect (item, "drag-data-get",
+ G_CALLBACK (on_drag_data_get), window);
+
+ g_hash_table_insert (data->window_hash, window, item);
+
+ gtk_widget_show_all (item);
+ return item;
+}
+
+static void
+append_window_item (WaylandWindowMenu *data, XfwWindow *window)
+{
+ GtkWidget *item = create_window_item (data, window);
+ gtk_menu_shell_append (GTK_MENU_SHELL (data->submenu), item);
+}
+
+static void
+add_workspace_label (WaylandWindowMenu *data, XfwWorkspace *ws)
+{
+ GtkWidget *item, *label;
+ const char *ws_name;
+ char *markup;
+
+ ws_name = xfw_workspace_get_name (ws);
+ if (ws_name == NULL || ws_name[0] == '\0')
+ ws_name = xfw_workspace_get_id (ws);
+
+ item = gtk_separator_menu_item_new ();
+ label = gtk_label_new ("");
+
+ markup = g_markup_printf_escaped ("<span size=\"small\" style=\"italic\">%s</span>",
+ ws_name);
+ gtk_label_set_markup (GTK_LABEL (label), markup);
+ gtk_label_set_xalign (GTK_LABEL (label), 1.0);
+ g_free (markup);
+
+ gtk_widget_show (label);
+ gtk_container_add (GTK_CONTAINER (item), label);
+ gtk_menu_shell_append (GTK_MENU_SHELL (data->submenu), item);
+}
+
+/* ---- Disconnect / rebuild ---- */
+
+static void
+disconnect_window_signals (WaylandWindowMenu *data)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, data->window_hash);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ g_signal_handlers_disconnect_by_data (key, value);
+
+ g_hash_table_remove_all (data->window_hash);
+}
+
+static void
+rebuild_menu (WaylandWindowMenu *data)
+{
+ GList *children, *l, *wins, *workspaces, *wl;
+ XfwWorkspace *active_ws;
+ gboolean added_separator;
+
+ /* 1. Disconnect per-window signals from previous menu items */
+ disconnect_window_signals (data);
+
+ /* 2. Remove all existing children from the submenu */
+ children = gtk_container_get_children (GTK_CONTAINER (data->submenu));
+ for (l = children; l != NULL; l = l->next)
+ gtk_widget_destroy (GTK_WIDGET (l->data));
+ g_list_free (children);
+
+ data->no_windows_item = NULL;
+
+ wins = xfw_screen_get_windows_stacked (data->screen);
+ active_ws = get_active_workspace (data);
+
+ if (data->workspace_manager == NULL)
+ {
+ /* No workspace support: flat list of all windows */
+ for (l = wins; l != NULL; l = l->next)
+ {
+ XfwWindow *w = l->data;
+ if (xfw_window_is_skip_tasklist (w))
+ continue;
+ append_window_item (data, w);
+ }
+ }
+ else
+ {
+ /* Add pinned + current workspace windows first */
+ for (l = wins; l != NULL; l = l->next)
+ {
+ XfwWindow *w = l->data;
+ XfwWorkspace *ws;
+
+ if (xfw_window_is_skip_tasklist (w))
+ continue;
+
+ ws = xfw_window_get_workspace (w);
+ /* Windows whose workspace is not known yet report NULL; keep
+ * them in the current-workspace group so they aren't dropped */
+ if (xfw_window_is_pinned (w) ||
+ ws == NULL ||
+ ws == active_ws)
+ {
+ append_window_item (data, w);
+ }
+ }
+
+ /* Add other workspace sections */
+ added_separator = FALSE;
+
+ workspaces = xfw_workspace_manager_list_workspaces (
+ data->workspace_manager);
+
+ for (wl = workspaces; wl != NULL; wl = wl->next)
+ {
+ XfwWorkspace *ws = wl->data;
+ XfwWorkspaceState ws_state;
+ gboolean has_windows;
+ GList *wwl;
+
+ if (ws == active_ws)
+ continue;
+
+ ws_state = xfw_workspace_get_state (ws);
+ if (ws_state & XFW_WORKSPACE_STATE_HIDDEN)
+ continue;
+
+ /* Check if this workspace has any windows */
+ has_windows = FALSE;
+ for (wwl = wins; wwl != NULL; wwl = wwl->next)
+ {
+ XfwWindow *w = wwl->data;
+ if (xfw_window_is_skip_tasklist (w))
+ continue;
+ if (xfw_window_get_workspace (w) == ws)
+ {
+ has_windows = TRUE;
+ break;
+ }
+ }
+
+ if (!has_windows)
+ continue;
+
+ if (!added_separator)
+ {
+ GtkWidget *sep =
+ gtk_separator_menu_item_new ();
+ gtk_menu_shell_append (
+ GTK_MENU_SHELL (data->submenu),
+ sep);
+ added_separator = TRUE;
+ }
+
+ add_workspace_label (data, ws);
+
+ for (wwl = wins; wwl != NULL; wwl = wwl->next)
+ {
+ XfwWindow *w = wwl->data;
+ if (xfw_window_is_skip_tasklist (w))
+ continue;
+ if (xfw_window_get_workspace (w) == ws)
+ append_window_item (data, w);
+ }
+ }
+ }
+
+ /* "No Windows Open" item */
+ {
+ GtkWidget *item;
+
+ item = gtk_menu_item_new_with_label (_ ("No Windows Open"));
+ gtk_widget_set_sensitive (item, FALSE);
+ gtk_menu_shell_append (GTK_MENU_SHELL (data->submenu), item);
+ data->no_windows_item = item;
+
+ if (g_hash_table_size (data->window_hash) > 0)
+ gtk_widget_hide (item);
+ else
+ gtk_widget_show (item);
+ }
+}
+
+/* ---- Screen signal handlers (live updates while menu is visible) ---- */
+
+static void
+on_window_opened (XfwScreen *screen, XfwWindow *window,
+ WaylandWindowMenu *data)
+{
+ /* refresh the button icon for the initially-enumerated windows too */
+ update_root_icon (data);
+
+ if (!gtk_widget_get_mapped (data->submenu))
+ return;
+
+ if (g_hash_table_contains (data->window_hash, window))
+ return;
+
+ if (xfw_window_is_skip_tasklist (window))
+ return;
+
+ append_window_item (data, window);
+ gtk_menu_reposition (GTK_MENU (data->submenu));
+
+ if (data->no_windows_item != NULL)
+ gtk_widget_hide (data->no_windows_item);
+}
+
+static void
+on_window_closed (XfwScreen *screen, XfwWindow *window,
+ WaylandWindowMenu *data)
+{
+ GtkWidget *item;
+
+ if (!gtk_widget_get_mapped (data->submenu))
+ return;
+
+ item = g_hash_table_lookup (data->window_hash, window);
+ if (item == NULL)
+ return;
+
+ g_signal_handlers_disconnect_by_data (window, item);
+ g_hash_table_remove (data->window_hash, window);
+ gtk_widget_destroy (item);
+ gtk_menu_reposition (GTK_MENU (data->submenu));
+
+ if (g_hash_table_size (data->window_hash) == 0 &&
+ data->no_windows_item != NULL)
+ {
+ gtk_widget_show (data->no_windows_item);
+ }
+}
+
+static void
+on_active_window_changed (XfwScreen *screen, XfwWindow *previous_window,
+ WaylandWindowMenu *data)
+{
+ update_root_icon (data);
+}
+
+/* ---- Workspace manager signal handlers ---- */
+
+static void
+on_workspace_created (XfwWorkspaceManager *manager, XfwWorkspace *workspace,
+ WaylandWindowMenu *data)
+{
+ /* Defer to next rebuild; the menu groups by workspace. */
+}
+
+static void
+on_workspace_destroyed (XfwWorkspaceManager *manager, XfwWorkspace *workspace,
+ WaylandWindowMenu *data)
+{
+ /* Defer to next rebuild. */
+}
+
+/* ---- Scroll event for window switching ---- */
+
+static gboolean
+on_scroll_event (GtkWidget *widget, GdkEventScroll *event,
+ WaylandWindowMenu *data)
+{
+ XfwWorkspace *active_ws;
+ GList *windows, *l;
+ GdkScrollDirection direction;
+ XfwWindow *prev;
+ gboolean found_active;
+
+ if (event->direction == GDK_SCROLL_SMOOTH)
+ {
+ if (event->delta_y < 0)
+ direction = GDK_SCROLL_UP;
+ else if (event->delta_y > 0)
+ direction = GDK_SCROLL_DOWN;
+ else
+ return TRUE;
+ }
+ else
+ {
+ direction = event->direction;
+ }
+
+ if (direction != GDK_SCROLL_UP && direction != GDK_SCROLL_DOWN)
+ return TRUE;
+
+ active_ws = get_active_workspace (data);
+ windows = xfw_screen_get_windows_stacked (data->screen);
+
+ prev = NULL;
+ found_active = FALSE;
+
+ for (l = windows; l != NULL; l = l->next)
+ {
+ XfwWindow *w = l->data;
+
+ if (xfw_window_is_skip_tasklist (w))
+ continue;
+
+ if (active_ws != NULL && !xfw_window_is_pinned (w) &&
+ !xfw_window_is_on_workspace (w, active_ws))
+ {
+ continue;
+ }
+
+ if (xfw_window_is_active (w))
+ {
+ if (direction == GDK_SCROLL_UP && prev != NULL)
+ {
+ xfw_window_activate (prev, NULL,
+ event->time, NULL);
+ return TRUE;
+ }
+ if (direction == GDK_SCROLL_DOWN)
+ found_active = TRUE;
+ continue;
+ }
+
+ if (found_active)
+ {
+ xfw_window_activate (w, NULL, event->time, NULL);
+ return TRUE;
+ }
+
+ prev = w;
+ }
+
+ /* Wrapping: if scrolling up at the beginning, activate last window */
+ if (direction == GDK_SCROLL_UP && prev != NULL)
+ {
+ xfw_window_activate (prev, NULL, event->time, NULL);
+ return TRUE;
+ }
+
+ return TRUE;
+}
+
+/* ---- Menu show handler ---- */
+
+static void
+on_menu_show (GtkWidget *menu, WaylandWindowMenu *data)
+{
+ rebuild_menu (data);
+}
+
+/* ---- Cleanup ---- */
+
+static void
+wayland_window_menu_free (WaylandWindowMenu *data)
+{
+ if (data->window_hash != NULL)
+ {
+ disconnect_window_signals (data);
+ g_hash_table_destroy (data->window_hash);
+ data->window_hash = NULL;
+ }
+
+ if (data->workspace_manager != NULL)
+ {
+ /* borrowed from the screen: only disconnect, do not unref */
+ g_signal_handlers_disconnect_by_data (data->workspace_manager,
+ data);
+ data->workspace_manager = NULL;
+ }
+
+ if (data->screen != NULL)
+ {
+ g_signal_handlers_disconnect_by_data (data->screen, data);
+ g_object_unref (data->screen);
+ data->screen = NULL;
+ }
+
+ g_free (data);
+}
+
+/* ---- Public API ---- */
+
+GtkWidget *
+wayland_window_menu_new (void)
+{
+ WaylandWindowMenu *data;
+ XfwScreen *screen;
+ GdkDisplay *display;
+ GtkCssProvider *provider;
+
+ display = gdk_display_get_default ();
+ if (display == NULL || !GDK_IS_WAYLAND_DISPLAY (display))
+ return gtk_label_new (_ ("[Window menu not supported on this platform]"));
+
+ xfw_set_client_type (XFW_CLIENT_TYPE_PAGER);
+
+ screen = xfw_screen_get_default ();
+ if (screen == NULL)
+ return gtk_label_new (_ ("[Shell does not support "
+ "WLR Foreign Toplevel Control]"));
+
+ data = g_new0 (WaylandWindowMenu, 1);
+ /* xfw_screen_get_default() returns a referenced screen */
+ data->screen = screen;
+ data->window_hash = g_hash_table_new_full (g_direct_hash,
+ g_direct_equal,
+ g_object_unref,
+ NULL);
+
+ /* borrowed from the screen, which we keep alive */
+ data->workspace_manager = xfw_screen_get_workspace_manager (screen);
+
+ /* Build the menubar (matches WnckSelector's GtkMenuBar structure) */
+ data->menu = gtk_menu_bar_new ();
+
+ /* Root item: icon of active window */
+ data->root_item = gtk_menu_item_new ();
+ data->image = gtk_image_new ();
+ gtk_container_add (GTK_CONTAINER (data->root_item), data->image);
+
+ /* Submenu: the actual window list */
+ data->submenu = gtk_menu_new ();
+ gtk_menu_item_set_submenu (GTK_MENU_ITEM (data->root_item),
+ data->submenu);
+ gtk_menu_shell_append (GTK_MENU_SHELL (data->menu), data->root_item);
+
+ /* Remove menubar borders to match panel aesthetics; the
+ * dimmed-window icon style is needed inside the popup submenu,
+ * which is a separate window, so use a screen-wide provider */
+ gtk_widget_set_name (data->menu, "wnck-selector-menubar");
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_data (provider,
+ "#wnck-selector-menubar {\n"
+ " border-width: 0px;\n"
+ "}\n"
+ ".wnck-selector-dimmed {\n"
+ " opacity: 0.5;\n"
+ "}",
+ -1, NULL);
+ gtk_style_context_add_provider_for_screen (
+ gtk_widget_get_screen (data->menu),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ g_object_unref (provider);
+
+ g_object_set_data_full (G_OBJECT (data->menu),
+ "wayland_window_menu_data",
+ data,
+ (GDestroyNotify) wayland_window_menu_free);
+
+ g_signal_connect (data->submenu, "show",
+ G_CALLBACK (on_menu_show), data);
+
+ /* Screen signals for live updates while menu is visible */
+ g_signal_connect (data->screen, "window-opened",
+ G_CALLBACK (on_window_opened), data);
+ g_signal_connect (data->screen, "window-closed",
+ G_CALLBACK (on_window_closed), data);
+ g_signal_connect (data->screen, "active-window-changed",
+ G_CALLBACK (on_active_window_changed), data);
+
+ /* Workspace manager signals */
+ if (data->workspace_manager != NULL)
+ {
+ g_signal_connect (data->workspace_manager,
+ "workspace-created",
+ G_CALLBACK (on_workspace_created), data);
+ g_signal_connect (data->workspace_manager,
+ "workspace-destroyed",
+ G_CALLBACK (on_workspace_destroyed), data);
+ }
+
+ /* Set initial root icon to active window */
+ update_root_icon (data);
+
+ /* Scroll-to-switch-windows */
+ gtk_widget_add_events (data->menu, GDK_SCROLL_MASK);
+ g_signal_connect (data->menu, "scroll-event",
+ G_CALLBACK (on_scroll_event), data);
+
+ gtk_widget_show (data->root_item);
+ gtk_widget_show (data->image);
+ gtk_widget_show (data->menu);
+ return data->menu;
+}
diff --git a/applets/wncklet/wayland-window-menu.h b/applets/wncklet/wayland-window-menu.h
new file mode 100644
index 00000000..2a1d39f0
--- /dev/null
+++ b/applets/wncklet/wayland-window-menu.h
@@ -0,0 +1,43 @@
+/* Wayland backend for the Window Selector applet */
+
+/*
+ * Copyright (C) 2026 MATE Desktop Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+#ifndef _WNCKLET_APPLET_WAYLAND_WINDOW_MENU_H_
+#define _WNCKLET_APPLET_WAYLAND_WINDOW_MENU_H_
+
+#ifdef PACKAGE_NAME
+#ifndef HAVE_WAYLAND
+#error file should only be included when HAVE_WAYLAND is enabled
+#endif
+#endif
+
+#include <gtk/gtk.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+GtkWidget *wayland_window_menu_new (void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _WNCKLET_APPLET_WAYLAND_WINDOW_MENU_H_ */
diff --git a/applets/wncklet/window-list.c b/applets/wncklet/window-list.c
index a880b26b..cc09c417 100644
--- a/applets/wncklet/window-list.c
+++ b/applets/wncklet/window-list.c
@@ -1055,7 +1055,11 @@ static void display_about_dialog(GtkAction* action, TasklistData* tasklist)
NULL
};
- gtk_show_about_dialog(GTK_WINDOW(tasklist->applet),
+ GtkWidget* toplevel = gtk_widget_get_toplevel (tasklist->applet);
+ if (!GTK_IS_WINDOW (toplevel))
+ toplevel = NULL;
+
+ gtk_show_about_dialog(toplevel ? GTK_WINDOW(toplevel) : NULL,
"program-name", _("Window List"),
"title", _("About Window List"),
"authors", authors,
diff --git a/applets/wncklet/window-menu.c b/applets/wncklet/window-menu.c
index 18f33f19..21d2117f 100644
--- a/applets/wncklet/window-menu.c
+++ b/applets/wncklet/window-menu.c
@@ -43,6 +43,7 @@
#ifdef HAVE_WAYLAND
#include <gdk/gdkwayland.h>
+#include "wayland-window-menu.h"
#endif /* HAVE_WAYLAND */
#ifndef HAVE_X11
@@ -86,7 +87,11 @@ static void window_menu_about(GtkAction* action, WindowMenu* window_menu)
NULL
};
- gtk_show_about_dialog(GTK_WINDOW(window_menu->applet),
+ GtkWidget* toplevel = gtk_widget_get_toplevel (window_menu->applet);
+ if (!GTK_IS_WINDOW (toplevel))
+ toplevel = NULL;
+
+ gtk_show_about_dialog(toplevel ? GTK_WINDOW(toplevel) : NULL,
"program-name", _("Window Selector"),
"title", _("About Window Selector"),
"authors", authors,
@@ -271,7 +276,7 @@ gboolean window_menu_applet_fill(MatePanelApplet* applet)
#ifdef HAVE_WAYLAND
if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ()))
{
- window_menu->selector = gtk_label_new ("[Window menu not supported on Wayland]");
+ window_menu->selector = wayland_window_menu_new ();
}
else
#endif /* HAVE_WAYLAND */
diff --git a/applets/wncklet/workspace-switcher.c b/applets/wncklet/workspace-switcher.c
index 93318314..b4ae493d 100644
--- a/applets/wncklet/workspace-switcher.c
+++ b/applets/wncklet/workspace-switcher.c
@@ -990,7 +990,11 @@ static void display_about_dialog(GtkAction* action, PagerData* pager)
NULL
};
- gtk_show_about_dialog(GTK_WINDOW(pager->applet),
+ GtkWidget* toplevel = gtk_widget_get_toplevel (pager->applet);
+ if (!GTK_IS_WINDOW (toplevel))
+ toplevel = NULL;
+
+ gtk_show_about_dialog(toplevel ? GTK_WINDOW(toplevel) : NULL,
"program-name", _("Workspace Switcher"),
"title", _("About Workspace Switcher"),
"authors", authors,