From bd61f7bdda7cbcecf478e5410299b0945a907d3e Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Wed, 19 Jun 2024 13:18:13 +0200 Subject: [PATCH 1/9] gdk/wayland: Require the xdg-session-management protocol This is not upstreamed yet, so use an internal xx-session-management protocol that we can eventually move to use the upstream protocol. --- gdk/wayland/meson.build | 5 + .../protocol/xx-session-management-v1.xml | 264 ++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 gdk/wayland/protocol/xx-session-management-v1.xml diff --git a/gdk/wayland/meson.build b/gdk/wayland/meson.build index 706da22c83b..caef045be75 100644 --- a/gdk/wayland/meson.build +++ b/gdk/wayland/meson.build @@ -151,6 +151,11 @@ proto_sources = [ 'stability': 'private', 'version': 4, }, + { + 'name': 'xx-session-management', + 'stability': 'private', + 'version': 1, + }, ] gdk_wayland_gen_headers = [] diff --git a/gdk/wayland/protocol/xx-session-management-v1.xml b/gdk/wayland/protocol/xx-session-management-v1.xml new file mode 100644 index 00000000000..e21694424f1 --- /dev/null +++ b/gdk/wayland/protocol/xx-session-management-v1.xml @@ -0,0 +1,264 @@ + + + + Copyright 2018 Mike Blumenkrantz + Copyright 2018 Samsung Electronics Co., Ltd + Copyright 2018 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This description provides a high-level overview of the interplay between + the interfaces defined this protocol. For details, see the protocol + specification. + + The xx_session_manager protocol declares interfaces necessary to + allow clients to restore toplevel state from previous executions. The + xx_session_manager_v1.get_session request can be used to obtain a + xx_session_v1 resource representing the state of a set of toplevels. + + Clients may obtain the session string to use in future calls through + the xx_session_v1.created event. Compositors will use this string + as an identifiable token for future runs, possibly storing data about + the related toplevels in persistent storage. + + Toplevels are managed through the xx_session_v1.add_toplevel and + xx_session_toplevel_v1.remove pair of requests. Clients will explicitly + request a toplevel to be restored according to prior state through the + xx_session_v1.restore_toplevel request before the toplevel is mapped. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + The xx_session_manager interface defines base requests for creating and + managing a session for an application. Sessions persist across application + and compositor restarts unless explicitly destroyed. A session is created + for the purpose of maintaining an application's xdg_toplevel surfaces + across compositor or application restarts. The compositor should remember + as many states as possible for surfaces in a given session, but there is + no requirement for which states must be remembered. + + + + + + + + + The reason may determine in what way a session restores the window + management state of associated toplevels. + + For example newly launched applications might be launched on the active + workspace with restored size and position, while a recovered + applications might restore additional state such as active workspace and + stacking order. + + + + A new app instance is launched, for example from an app launcher. + + + + + A app instance is recovering from for example a compositor or app crash. + + + + + A app instance is restored, for example part of a restored session, or + restored from having been temporarily terminated due to resource + constraints. + + + + + + + This has no effect other than to destroy the xx_session_manager object. + + + + + + Create a session object corresponding to either an existing session + identified by the given session identifier string or a new session. + While the session object exists, the session is considered to be "in + use". + + If a identifier string represents a session that is currently actively + in use by the the same client, an 'in_use' error is raised. If some + other client is currently using the same session, the new session will + replace managing the associated state. + + NULL is passed to initiate a new session. If an id is passed which does + not represent a valid session, the compositor treats it as if NULL had + been passed. + + A client is allowed to have any number of in use sessions at the same + time. + + + + + + + + + + A xx_session_v1 object represents a session for an application. While the + object exists, all surfaces which have been added to the session will + have states stored by the compositor which can be reapplied at a later + time. Two sessions cannot exist for the same identifier string. + + States for surfaces added to a session are automatically updated by the + compositor when they are changed. + + Surfaces which have been added to a session are automatically removed from + the session if xdg_toplevel.destroy is called for the surface. + + + + + + + + + + + Destroy a session object, preserving the current state but not continuing + to make further updates if state changes occur. This makes the associated + xx_toplevel_session_v1 objects inert. + + + + + + Remove the session, making it no longer available for restoration. A + compositor should in response to this request remove the data related to + this session from its storage. + + + + + + Attempt to add a given surface to the session. The passed name is used + to identify what window is being restored, and may be used store window + specific state within the session. + + Calling this with a toplevel that is already managed by the session with + the same associated will raise an in_use error. + + + + + + + + + Inform the compositor that the toplevel associated with the passed name + should have its window management state restored. + + Calling this with a toplevel that is already managed by the session with + the same associated will raise an in_use error. + + This request must be called prior to the first commit on the associated + wl_surface, otherwise an already_mapped error is raised. + + As part of the initial configure sequence, if the toplevel was + successfully restored, a xx_toplevel_session_v1.restored event is + emitted. See the xx_toplevel_session_v1.restored event for further + details. + + + + + + + + + Emitted at most once some time after getting a new session object. It + means that no previous state was restored, and a new session was created. + The passed id can be used to restore previous sessions. + + + + + + + Emitted at most once some time after getting a new session object. It + means that previous state was at least partially restored. The same id + can again be used to restore previous sessions. + + + + + + Emitted at most once, if the session was taken over by some other + client. When this happens, the session and all its toplevel session + objects become inert, and should be destroyed. + + + + + + + + Destroy the object. This has no effect window management of the + associated toplevel. + + + + + + Remove a specified surface from the session and render any corresponding + xx_toplevel_session_v1 object inert. The compositor should remove any + data related to the toplevel in the corresponding session from its internal + storage. + + + + + + The "restored" event is emitted prior to the first + xdg_toplevel.configure for the toplevel. It will only be emitted after + xx_session_v1.restore_toplevel, and the initial empty surface state has + been applied, and it indicates that the surface's session is being + restored with this configure event. + + + + + -- GitLab From 2b28059745010fe052ac5435d58f56690c57f834 Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Wed, 19 Jun 2024 13:20:38 +0200 Subject: [PATCH 2/9] gdk/wayland: Get/initialize a xdg_session_v1 object Add the internal API to let the upper layers define a session ID (e.g. saved from previous runs), and retrieve the session ID eventually used (e.g. in case the session is created from scratch). --- gdk/wayland/gdkdisplay-wayland.c | 86 ++++++++++++++++++++++++++++++++ gdk/wayland/gdkdisplay-wayland.h | 5 ++ gdk/wayland/gdkwaylanddisplay.h | 7 +++ 3 files changed, 98 insertions(+) diff --git a/gdk/wayland/gdkdisplay-wayland.c b/gdk/wayland/gdkdisplay-wayland.c index 63eade8534e..af84a0d7476 100644 --- a/gdk/wayland/gdkdisplay-wayland.c +++ b/gdk/wayland/gdkdisplay-wayland.c @@ -61,6 +61,7 @@ #include #include "linux-dmabuf-unstable-v1-client-protocol.h" #include "presentation-time-client-protocol.h" +#include "xx-session-management-v1-client-protocol.h" #include "wm-button-layout-translation.h" @@ -100,6 +101,7 @@ #define NO_XDG_OUTPUT_DONE_SINCE_VERSION 3 #define OUTPUT_VERSION 3 #define XDG_WM_DIALOG_VERSION 1 +#define XX_SESSION_MANAGEMENT_VERSION 1 #ifdef HAVE_TOPLEVEL_STATE_SUSPENDED #define XDG_WM_BASE_VERSION 6 @@ -310,6 +312,38 @@ static const struct org_kde_kwin_server_decoration_manager_listener server_decor .default_mode = server_decoration_manager_default_mode }; +static void +session_listener_created (void *data, + struct xx_session_v1 *xdg_session_v1, + const char *id) +{ + GdkWaylandDisplay *display_wayland = data; + + if (g_strcmp0 (display_wayland->session_id, id) != 0) + { + g_clear_pointer (&display_wayland->session_id, g_free); + display_wayland->session_id = g_strdup (id); + } +} + +static void +session_listener_restored (void *data, + struct xx_session_v1 *xdg_session_v1) +{ +} + +static void +session_listener_replaced (void *data, + struct xx_session_v1 *xdg_session_v1) +{ +} + +static const struct xx_session_v1_listener xdg_session_listener = { + .created = session_listener_created, + .restored = session_listener_restored, + .replaced = session_listener_replaced, +}; + /* * gdk_wayland_display_prefers_ssd: * @display: (type GdkWaylandDisplay): a `GdkDisplay` @@ -392,6 +426,13 @@ gdk_registry_handle_global (void *data, &xdg_wm_dialog_v1_interface, MIN (version, XDG_WM_DIALOG_VERSION)); } + else if (strcmp (interface, "xx_session_manager_v1") == 0) + { + display_wayland->xdg_session_manager = + wl_registry_bind (display_wayland->wl_registry, id, + &xx_session_manager_v1_interface, + MIN (version, XX_SESSION_MANAGEMENT_VERSION)); + } else if (strcmp (interface, "gtk_shell1") == 0) { display_wayland->gtk_shell = @@ -765,6 +806,8 @@ gdk_wayland_display_dispose (GObject *object) g_clear_pointer (&display_wayland->linux_dmabuf, zwp_linux_dmabuf_v1_destroy); g_clear_pointer (&display_wayland->dmabuf_formats_info, dmabuf_formats_info_free); g_clear_pointer (&display_wayland->color, gdk_wayland_color_free); + g_clear_pointer (&display_wayland->xdg_session, xx_session_v1_destroy); + g_clear_pointer (&display_wayland->xdg_session_manager, xx_session_manager_v1_destroy); g_clear_pointer (&display_wayland->shm, wl_shm_destroy); g_clear_pointer (&display_wayland->wl_registry, wl_registry_destroy); @@ -2806,3 +2849,46 @@ gdk_wayland_display_dispatch_queue (GdkDisplay *display, _exit (1); } } + +void +gdk_wayland_display_register_session (GdkDisplay *display, + const char *name) +{ + GdkWaylandDisplay *display_wayland = GDK_WAYLAND_DISPLAY (display); + + if (!display_wayland->xdg_session_manager) + return; + + g_clear_pointer (&display_wayland->session_id, g_free); + display_wayland->session_id = g_strdup (name); + + display_wayland->xdg_session = + xx_session_manager_v1_get_session (display_wayland->xdg_session_manager, + XX_SESSION_MANAGER_V1_REASON_LAUNCH, + name); + xx_session_v1_add_listener (display_wayland->xdg_session, + &xdg_session_listener, + display_wayland); + + wl_display_roundtrip (display_wayland->wl_display); +} + +void +gdk_wayland_display_unregister_session (GdkDisplay *display) +{ + GdkWaylandDisplay *display_wayland = GDK_WAYLAND_DISPLAY (display); + + if (!display_wayland->xdg_session_manager) + return; + + if (display_wayland->xdg_session) + xx_session_v1_remove (display_wayland->xdg_session); +} + +const char * +gdk_wayland_display_get_current_session_id (GdkDisplay *display) +{ + GdkWaylandDisplay *display_wayland = GDK_WAYLAND_DISPLAY (display); + + return display_wayland->session_id; +} diff --git a/gdk/wayland/gdkdisplay-wayland.h b/gdk/wayland/gdkdisplay-wayland.h index 9b23f541788..c9741c808c0 100644 --- a/gdk/wayland/gdkdisplay-wayland.h +++ b/gdk/wayland/gdkdisplay-wayland.h @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -124,6 +125,8 @@ struct _GdkWaylandDisplay struct wp_viewporter *viewporter; struct wp_presentation *presentation; struct wp_single_pixel_buffer_manager_v1 *single_pixel_buffer; + struct xdg_session_manager_v1 *xdg_session_manager; + struct xdg_session_v1 *xdg_session; GdkWaylandColor *color; GList *async_roundtrips; @@ -153,6 +156,8 @@ struct _GdkWaylandDisplay GListStore *monitors; + char *session_id; + gint64 last_bell_time_ms; }; diff --git a/gdk/wayland/gdkwaylanddisplay.h b/gdk/wayland/gdkwaylanddisplay.h index 396a6f98f21..2637194ac17 100644 --- a/gdk/wayland/gdkwaylanddisplay.h +++ b/gdk/wayland/gdkwaylanddisplay.h @@ -65,5 +65,12 @@ gboolean gdk_wayland_display_query_registry (GdkDisplay *di GDK_AVAILABLE_IN_4_4 gpointer gdk_wayland_display_get_egl_display (GdkDisplay *display); +void gdk_wayland_display_register_session (GdkDisplay *display, + const char *name); + +void gdk_wayland_display_unregister_session (GdkDisplay *display); + +const char * gdk_wayland_display_get_current_session_id (GdkDisplay *display); + G_END_DECLS -- GitLab From 0292c8ced246f727861d5acb60de31ddf8a822b3 Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Wed, 19 Jun 2024 13:25:51 +0200 Subject: [PATCH 3/9] gdk/wayland: Add API to restore GdkToplevels from an ID in the session Any toplevel may be referred by an ID, that will be used by the compositor to restore window state for it. Add the glue so that the upper layers may restore a toplevel from one of these IDs. --- gdk/wayland/gdkdisplay-wayland.h | 6 +-- gdk/wayland/gdktoplevel-wayland.c | 65 +++++++++++++++++++++++++++++++ gdk/wayland/gdkwaylandtoplevel.h | 7 ++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/gdk/wayland/gdkdisplay-wayland.h b/gdk/wayland/gdkdisplay-wayland.h index c9741c808c0..f939498796f 100644 --- a/gdk/wayland/gdkdisplay-wayland.h +++ b/gdk/wayland/gdkdisplay-wayland.h @@ -42,7 +42,7 @@ #include #include #include -#include +#include #include #include @@ -125,8 +125,8 @@ struct _GdkWaylandDisplay struct wp_viewporter *viewporter; struct wp_presentation *presentation; struct wp_single_pixel_buffer_manager_v1 *single_pixel_buffer; - struct xdg_session_manager_v1 *xdg_session_manager; - struct xdg_session_v1 *xdg_session; + struct xx_session_manager_v1 *xdg_session_manager; + struct xx_session_v1 *xdg_session; GdkWaylandColor *color; GList *async_roundtrips; diff --git a/gdk/wayland/gdktoplevel-wayland.c b/gdk/wayland/gdktoplevel-wayland.c index a8d15bc208c..2bbcfe18565 100644 --- a/gdk/wayland/gdktoplevel-wayland.c +++ b/gdk/wayland/gdktoplevel-wayland.c @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -126,6 +127,7 @@ struct _GdkWaylandToplevel struct wl_output *initial_fullscreen_output; struct wp_presentation_feedback *feedback; + struct xx_toplevel_session_v1 *toplevel_session; struct { GdkToplevelState unset_flags; @@ -141,6 +143,7 @@ struct _GdkWaylandToplevel gboolean has_bounds; char *title; + char *session_id; GdkGeometry geometry_hints; GdkSurfaceHints geometry_mask; @@ -818,6 +821,23 @@ create_zxdg_toplevel_v6_resources (GdkWaylandToplevel *toplevel) toplevel); } +static void +attempt_restore_toplevel (GdkWaylandToplevel *wayland_toplevel) +{ + GdkDisplay *display = gdk_surface_get_display (GDK_SURFACE (wayland_toplevel)); + GdkWaylandDisplay *display_wayland = GDK_WAYLAND_DISPLAY (display); + + if (display_wayland->xdg_session && + wayland_toplevel->session_id && + wayland_toplevel->display_server.xdg_toplevel) + { + wayland_toplevel->toplevel_session = + xx_session_v1_restore_toplevel (display_wayland->xdg_session, + wayland_toplevel->display_server.xdg_toplevel, + wayland_toplevel->session_id); + } +} + static void gdk_wayland_surface_create_xdg_toplevel (GdkWaylandToplevel *wayland_toplevel) { @@ -884,6 +904,8 @@ gdk_wayland_surface_create_xdg_toplevel (GdkWaylandToplevel *wayland_toplevel) if (!maybe_set_xdg_dialog_modal (wayland_toplevel)) maybe_set_gtk_surface_modal (wayland_toplevel); + attempt_restore_toplevel (wayland_toplevel); + gdk_profiler_add_mark (GDK_PROFILER_CURRENT_TIME, 0, "Wayland surface commit", NULL); wl_surface_commit (wayland_surface->display_server.wl_surface); } @@ -1380,6 +1402,9 @@ gdk_wayland_toplevel_finalize (GObject *object) g_free (self->application.application_object_path); g_free (self->application.unique_bus_name); + if (self->toplevel_session) + xx_toplevel_session_v1_destroy (self->toplevel_session); + g_free (self->title); g_clear_pointer (&self->shortcuts_inhibitors, g_hash_table_unref); @@ -2773,5 +2798,45 @@ gdk_wayland_toplevel_set_transient_for_exported (GdkToplevel *toplevel, return TRUE; } +void +gdk_wayland_toplevel_set_session_id (GdkToplevel *toplevel, + const char *session_id) +{ + GdkWaylandToplevel *wayland_toplevel = GDK_WAYLAND_TOPLEVEL (toplevel); + + g_clear_pointer (&wayland_toplevel->session_id, g_free); + wayland_toplevel->session_id = g_strdup (session_id); +} + +void +gdk_wayland_toplevel_restore_from_session (GdkToplevel *toplevel) +{ + GdkWaylandToplevel *wayland_toplevel = GDK_WAYLAND_TOPLEVEL (toplevel); + GdkDisplay *display = gdk_surface_get_display (GDK_SURFACE (toplevel)); + GdkWaylandDisplay *display_wayland = GDK_WAYLAND_DISPLAY (display); + + if (display_wayland->xdg_session && wayland_toplevel->display_server.xdg_toplevel) + { + wayland_toplevel->toplevel_session = + xx_session_v1_restore_toplevel (display_wayland->xdg_session, + wayland_toplevel->display_server.xdg_toplevel, + wayland_toplevel->session_id); + } +} + +void +gdk_wayland_toplevel_remove_from_session (GdkToplevel *toplevel) +{ + GdkWaylandToplevel *wayland_toplevel = GDK_WAYLAND_TOPLEVEL (toplevel); + GdkDisplay *display = gdk_surface_get_display (GDK_SURFACE (toplevel)); + GdkWaylandDisplay *display_wayland = GDK_WAYLAND_DISPLAY (display); + + if (display_wayland->xdg_session && wayland_toplevel->toplevel_session) + { + xx_toplevel_session_v1_remove (wayland_toplevel->toplevel_session); + wayland_toplevel->toplevel_session = NULL; + } +} + /* }}} */ /* vim:set foldmethod=marker expandtab: */ diff --git a/gdk/wayland/gdkwaylandtoplevel.h b/gdk/wayland/gdkwaylandtoplevel.h index 0240f9d19fa..08725489c53 100644 --- a/gdk/wayland/gdkwaylandtoplevel.h +++ b/gdk/wayland/gdkwaylandtoplevel.h @@ -67,4 +67,11 @@ GDK_AVAILABLE_IN_ALL void gdk_wayland_toplevel_set_application_id (GdkToplevel *toplevel, const char *application_id); +void gdk_wayland_toplevel_set_session_id (GdkToplevel *toplevel, + const char *session_id); + +void gdk_wayland_toplevel_restore_from_session (GdkToplevel *toplevel); + +void gdk_wayland_toplevel_remove_from_session (GdkToplevel *toplevel); + G_END_DECLS -- GitLab From f174aa642fc931560d6946a9cd5c4053dc8dab8f Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Wed, 19 Jun 2024 13:36:18 +0200 Subject: [PATCH 4/9] gtk: Add GtkWindow property/setter/getter for session identifiers This identifier will be used for session management purposes, and is meant to be stable across executions. --- gtk/gtkwindow.c | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ gtk/gtkwindow.h | 7 +++++ 2 files changed, 77 insertions(+) diff --git a/gtk/gtkwindow.c b/gtk/gtkwindow.c index df089a16bf0..dabdf984d7a 100644 --- a/gtk/gtkwindow.c +++ b/gtk/gtkwindow.c @@ -284,6 +284,8 @@ typedef struct GdkCursor *resize_cursor; + char *session_id; + GtkEventController *menubar_controller; } GtkWindowPrivate; @@ -333,6 +335,8 @@ enum { PROP_MAXIMIZED, PROP_FULLSCREENED, + PROP_SESSION_ID, + LAST_ARG }; @@ -1076,6 +1080,24 @@ gtk_window_class_init (GtkWindowClass *klass) TRUE, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + /** + * GtkWindow:session-id: (attributes org.gtk.Property.get=gtk_window_get_session_id org.gtk.Property.set=gtk_window_set_session_id) + * + * The identifier of this toplevel in the session. + * + * In the windowing environments that allow it, this identifier will + * be used to identify windows in a persistent manner across runs, and + * restore window state (e.g. position, size) for them. + * + * Currently, this is only implemented for the Wayland backend. + * + * Since: 4.16 + */ + window_props[PROP_SESSION_ID] = + g_param_spec_string ("session-id", NULL, NULL, + NULL, + GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + g_object_class_install_properties (gobject_class, LAST_ARG, window_props); /** @@ -1895,6 +1917,9 @@ gtk_window_set_property (GObject *object, case PROP_HANDLE_MENUBAR_ACCEL: gtk_window_set_handle_menubar_accel (window, g_value_get_boolean (value)); break; + case PROP_SESSION_ID: + gtk_window_set_session_id (window, g_value_get_string (value)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -1984,6 +2009,9 @@ gtk_window_get_property (GObject *object, case PROP_HANDLE_MENUBAR_ACCEL: g_value_set_boolean (value, gtk_window_get_handle_menubar_accel (window)); break; + case PROP_SESSION_ID: + g_value_set_string (value, gtk_window_get_session_id (window)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -6990,3 +7018,45 @@ gtk_window_get_handle_menubar_accel (GtkWindow *window) return phase == GTK_PHASE_CAPTURE; } + +/** + * gtk_window_set_session_id: + * @window: a `GtkWindow` + * @session_id: (nullable): A persistent identifier for this window + * + * Sets the identifier to be used for session management purposes. + * + * State of this window may be restored from prior executions, by using this + * identifier to match the related state. This identifier should be constant, or + * at least stable between executions. + * + * Since: 4.16 + **/ +void +gtk_window_set_session_id (GtkWindow *window, + const gchar *session_id) +{ + GtkWindowPrivate *priv = gtk_window_get_instance_private (window); + + if (g_set_str (&priv->session_id, session_id)) + g_object_notify_by_pspec (G_OBJECT (window), window_props[PROP_SESSION_ID]); +} + +/** + * gtk_window_get_session_id: + * @window: a `GtkWindow` + * + * Gets the window identifier to be used for session management purposes. + * + * See [method@Gtk.Window.set_session_id] for more details about window + * identifiers for session management. + * + * Returns: (nullable): the session identifier + **/ +const gchar * +gtk_window_get_session_id (GtkWindow *window) +{ + GtkWindowPrivate *priv = gtk_window_get_instance_private (window); + + return priv->session_id; +} diff --git a/gtk/gtkwindow.h b/gtk/gtkwindow.h index a9c0228b011..9553b655ea7 100644 --- a/gtk/gtkwindow.h +++ b/gtk/gtkwindow.h @@ -259,6 +259,13 @@ void gtk_window_set_handle_menubar_accel (GtkWindow *window, GDK_AVAILABLE_IN_4_2 gboolean gtk_window_get_handle_menubar_accel (GtkWindow *window); +GDK_AVAILABLE_IN_4_16 +void gtk_window_set_session_id (GtkWindow *window, + const gchar *session_id); + +GDK_AVAILABLE_IN_4_16 +const gchar * gtk_window_get_session_id (GtkWindow *window); + G_DEFINE_AUTOPTR_CLEANUP_FUNC(GtkWindow, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC(GtkWindowGroup, g_object_unref) -- GitLab From 22ad71d4d94f739d36acd8c916a81b1c1a7b711f Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Wed, 19 Jun 2024 14:00:37 +0200 Subject: [PATCH 5/9] gtk: Add GtkApplication property/setter/getter for session IDs This API has two "phases": - An initialization call, calling gtk_application_set_session_id() to set a session identifier from previous runs. - A post-initialization call, to retrieve the session identifier in use for this session. This may be the same identifier than previously specified, if the session could be recovered, or a brand new one if the session is created from scratch. The storage of this session identifier is left up to the applications, and could be material for later GNOME session integration with persistent save states. --- gtk/gtkapplication.c | 114 ++++++++++++++++++++++++++++++++++++ gtk/gtkapplication.h | 10 ++++ gtk/gtkapplicationimpl.c | 9 +++ gtk/gtkapplicationprivate.h | 4 +- 4 files changed, 136 insertions(+), 1 deletion(-) diff --git a/gtk/gtkapplication.c b/gtk/gtkapplication.c index 4d81ef56db3..c9ec81d2967 100644 --- a/gtk/gtkapplication.c +++ b/gtk/gtkapplication.c @@ -127,6 +127,7 @@ enum { PROP_SCREENSAVER_ACTIVE, PROP_MENUBAR, PROP_ACTIVE_WINDOW, + PROP_SESSION_ID, NUM_PROPERTIES }; @@ -147,6 +148,7 @@ typedef struct GtkActionMuxer *muxer; GtkBuilder *menus_builder; char *help_overlay_path; + char *session_id; } GtkApplicationPrivate; G_DEFINE_TYPE_WITH_PRIVATE (GtkApplication, gtk_application, G_TYPE_APPLICATION) @@ -450,6 +452,10 @@ gtk_application_get_property (GObject *object, g_value_set_object (value, gtk_application_get_active_window (application)); break; + case PROP_SESSION_ID: + g_value_set_string (value, gtk_application_get_session_id (application)); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -475,6 +481,10 @@ gtk_application_set_property (GObject *object, gtk_application_set_menubar (application, g_value_get_object (value)); break; + case PROP_SESSION_ID: + gtk_application_set_session_id (application, g_value_get_string (value)); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -635,6 +645,18 @@ gtk_application_class_init (GtkApplicationClass *class) GTK_TYPE_WINDOW, G_PARAM_READABLE|G_PARAM_STATIC_STRINGS); + /** + * GtkApplication:session-id: (attributes org.gtk.Property.get=gtk_application_get_session_id org.gtk.Property.set=gtk_application_set_session_id) + * + * The identifier of the session used to restore window state. + * + * Since: 4.16 + */ + gtk_application_props[PROP_SESSION_ID] = + g_param_spec_object ("session-id", NULL, NULL, + GTK_TYPE_WINDOW, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, NUM_PROPERTIES, gtk_application_props); } @@ -1223,3 +1245,95 @@ gtk_application_set_screensaver_active (GtkApplication *application, g_object_notify (G_OBJECT (application), "screensaver-active"); } } + +/** + * gtk_application_set_session_id: + * @application: a `GtkApplication` + * @session_id: (nullable): The identifier used for session management purposes + * + * Sets the identifier used for session management purposes. + * + * This identifier would have been typically retrieved in prior runs + * through [method@Gtk.Application.get_current_session_id]. + * + * This session identifier must be set before `GApplication::startup` happens, + * as it will be applied there. In the case %NULL was passed, or the request + * resulted in a new session being created from scratch, + * [method@Gtk.Application.get_current_session_id] may return a different + * identifier than the one passed here. + **/ +void +gtk_application_set_session_id (GtkApplication *application, + const char *session_id) +{ + GtkApplicationPrivate *priv = + gtk_application_get_instance_private (application); + + g_return_if_fail (GTK_IS_APPLICATION (application)); + + if (g_set_str (&priv->session_id, session_id)) + g_object_notify (G_OBJECT (application), "session-id"); +} + +/** + * gtk_application_get_session_id: + * @application: a `GtkApplication` + * + * Gets the session identifier that this `GtkApplication` will use + * for session management registration. + * + * See [method@Gtk.Application.set_session_id] for more information about + * session management. + * + * Returns: (nullable): The session management ID + **/ +const char * +gtk_application_get_session_id (GtkApplication *application) +{ + GtkApplicationPrivate *priv = + gtk_application_get_instance_private (application); + + return priv->session_id; +} + +/** + * gtk_application_get_current_session_id: + * @application: a `GtkApplication` + * + * Retrieves the session management identifier that the application + * is using during execution. + * + * This identifier may be saved for future executions of the application + * to have their window state recovered. + * + * This function may be used during or after [signal@Gio.Application.startup] + * to retrieve a session identifier string on windowing environments where + * session management is supported. + * + * In order to restore application window state in future runs of the same + * program, this string should be stored so it can be passed through + * [method@Gtk.Application.set_session_id] in these future executions. + * + * Note that this identifier may end up being different from the one + * initially specified through [method@Gtk.Application.set_session_id], + * e.g. in the case the specified session ID could not be recovered and a + * new session was created. + * + * If called on an environment/backend that does not support session + * management, or called prior to application startup, this method will + * return %NULL. Currently, session management is only supported in the + * Wayland backend. + * + * Returns: (nullable): The session identifier to preserve for future runs + **/ +const char * +gtk_application_get_current_session_id (GtkApplication *application) +{ + GtkApplicationPrivate *priv = + gtk_application_get_instance_private (application); + + if (!priv->impl) + return NULL; + + return gtk_application_impl_get_current_session_id (priv->impl); +} diff --git a/gtk/gtkapplication.h b/gtk/gtkapplication.h index 7260de6dc7e..0ca9bd2d053 100644 --- a/gtk/gtkapplication.h +++ b/gtk/gtkapplication.h @@ -134,6 +134,16 @@ GDK_AVAILABLE_IN_ALL GMenu * gtk_application_get_menu_by_id (GtkApplication *application, const char *id); +GDK_AVAILABLE_IN_4_16 +void gtk_application_set_session_id (GtkApplication *application, + const char *session_id); + +GDK_AVAILABLE_IN_4_16 +const char * gtk_application_get_session_id (GtkApplication *application); + +GDK_AVAILABLE_IN_4_16 +const char * gtk_application_get_current_session_id (GtkApplication *application); + G_DEFINE_AUTOPTR_CLEANUP_FUNC(GtkApplication, g_object_unref) G_END_DECLS diff --git a/gtk/gtkapplicationimpl.c b/gtk/gtkapplicationimpl.c index 6948fa11e06..1403625a3ef 100644 --- a/gtk/gtkapplicationimpl.c +++ b/gtk/gtkapplicationimpl.c @@ -153,6 +153,15 @@ gtk_application_impl_prefers_app_menu (GtkApplicationImpl *impl) return GTK_APPLICATION_IMPL_GET_CLASS (impl)->prefers_app_menu (impl); } +const char * +gtk_application_impl_get_current_session_id (GtkApplicationImpl *impl) +{ + if (!GTK_APPLICATION_IMPL_GET_CLASS (impl)->get_current_session_id) + return NULL; + + return GTK_APPLICATION_IMPL_GET_CLASS (impl)->get_current_session_id (impl); +} + GtkApplicationImpl * gtk_application_impl_new (GtkApplication *application, GdkDisplay *display) diff --git a/gtk/gtkapplicationprivate.h b/gtk/gtkapplicationprivate.h index a6c56c89dfb..246ab8d654a 100644 --- a/gtk/gtkapplicationprivate.h +++ b/gtk/gtkapplicationprivate.h @@ -100,7 +100,7 @@ typedef struct gboolean (* prefers_app_menu) (GtkApplicationImpl *impl); - + const char * (* get_current_session_id) (GtkApplicationImpl *impl); } GtkApplicationImplClass; #define GTK_TYPE_APPLICATION_IMPL_DBUS (gtk_application_impl_dbus_get_type ()) @@ -195,5 +195,7 @@ gboolean gtk_application_impl_prefers_app_menu (GtkAppl void gtk_application_impl_quartz_setup_menu (GMenuModel *model, GtkActionMuxer *muxer); +const char * gtk_application_impl_get_current_session_id (GtkApplicationImpl *impl); + G_END_DECLS -- GitLab From 0116d8f0d60a343bc9130a83dd1ff5dbc6ece604 Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Wed, 19 Jun 2024 14:04:01 +0200 Subject: [PATCH 6/9] gtk: Hook up session management in wayland's GtkApplication impl Make this application impl object handle the integration with the private GDK/wayland API, so this is hooked up to the xdg-session-management-v1 protocol. As a nicety, use GtkWindow::hide-on-close to determine whether a window should be remembered, or immediately forgotten by the session after being closed. --- gtk/gtkapplication-wayland.c | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/gtk/gtkapplication-wayland.c b/gtk/gtkapplication-wayland.c index f002c3fc665..2648a8f3035 100644 --- a/gtk/gtkapplication-wayland.c +++ b/gtk/gtkapplication-wayland.c @@ -77,6 +77,7 @@ gtk_application_impl_wayland_handle_window_realize (GtkApplicationImpl *impl, GtkApplicationImplDBus *dbus = (GtkApplicationImplDBus *) impl; GdkSurface *gdk_surface; char *window_path; + const char *session_id; gdk_surface = gtk_native_get_surface (GTK_NATIVE (window)); @@ -95,6 +96,14 @@ gtk_application_impl_wayland_handle_window_realize (GtkApplicationImpl *impl, g_free (window_path); + session_id = gtk_window_get_session_id (window); + if (session_id) + { + gdk_wayland_toplevel_set_session_id (GDK_TOPLEVEL (gdk_surface), + session_id); + gdk_wayland_toplevel_restore_from_session (GDK_TOPLEVEL (gdk_surface)); + } + impl_class->handle_window_realize (impl, window); } @@ -178,6 +187,38 @@ gtk_application_impl_wayland_uninhibit (GtkApplicationImpl *impl, g_warning ("Invalid inhibitor cookie"); } +static void +gtk_application_impl_wayland_startup (GtkApplicationImpl *impl, + gboolean register_session) +{ + const char *session_id; + + GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class)->startup (impl, register_session); + + /* Note: This is independent of the register_session argument */ + session_id = gtk_application_get_session_id (impl->application); + gdk_wayland_display_register_session (gdk_display_get_default (), session_id); +} + +static void +gtk_application_impl_wayland_window_removed (GtkApplicationImpl *impl, + GtkWindow *window) +{ + GdkSurface *gdk_surface; + + if (!gtk_window_get_hide_on_close (window)) + { + gdk_surface = gtk_native_get_surface (GTK_NATIVE (window)); + gdk_wayland_toplevel_remove_from_session (GDK_TOPLEVEL (gdk_surface)); + } +} + +static const char * +gtk_application_impl_wayland_get_current_session_id (GtkApplicationImpl *impl) +{ + return gdk_wayland_display_get_current_session_id (gdk_display_get_default ()); +} + static void gtk_application_impl_wayland_init (GtkApplicationImplWayland *wayland) { @@ -191,6 +232,8 @@ gtk_application_impl_wayland_class_init (GtkApplicationImplWaylandClass *class) class->dbus_inhibit = impl_class->inhibit; class->dbus_uninhibit = impl_class->uninhibit; + impl_class->startup = + gtk_application_impl_wayland_startup; impl_class->handle_window_realize = gtk_application_impl_wayland_handle_window_realize; impl_class->before_emit = @@ -199,4 +242,8 @@ gtk_application_impl_wayland_class_init (GtkApplicationImplWaylandClass *class) gtk_application_impl_wayland_inhibit; impl_class->uninhibit = gtk_application_impl_wayland_uninhibit; + impl_class->window_removed = + gtk_application_impl_wayland_window_removed; + impl_class->get_current_session_id = + gtk_application_impl_wayland_get_current_session_id; } -- GitLab From 0106fbacac716938b6945dd6d4ee46e88488c570 Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Wed, 19 Jun 2024 14:06:47 +0200 Subject: [PATCH 7/9] demo: Make gtk4-demo use session management Use it as an example of what is to be expected to make your application windows restore-able between executions, the session ID is preserved in a setting between runs, so that it's fed to the GtkApplication on startup. The actual session ID used is likewise always stored in this setting, for later runs. The main window gets an identifier within the session, so that its state may be restored when first mapped. --- demos/gtk-demo/main.c | 23 +++++++++++++++++++++++ demos/gtk-demo/main.ui | 1 + demos/gtk-demo/org.gtk.Demo4.gschema.xml | 4 ++++ 3 files changed, 28 insertions(+) diff --git a/demos/gtk-demo/main.c b/demos/gtk-demo/main.c index 0e27a441e91..bf96f2a67a7 100644 --- a/demos/gtk-demo/main.c +++ b/demos/gtk-demo/main.c @@ -1138,6 +1138,19 @@ local_options (GApplication *app, return -1; } +static void +startup (GApplication *app, + gpointer data) +{ + GSettings *settings = data; + const char *session_id; + + session_id = gtk_application_get_current_session_id (GTK_APPLICATION (app)); + + if (session_id) + g_settings_set_string (settings, "session-id", session_id); +} + int main (int argc, char **argv) { @@ -1154,6 +1167,8 @@ main (int argc, char **argv) { "app.about", { "F1", NULL } }, { "app.quit", { "q", NULL } }, }; + GSettings *settings; + char *session_id; int i; app = gtk_application_new ("org.gtk.Demo4", G_APPLICATION_NON_UNIQUE|G_APPLICATION_HANDLES_COMMAND_LINE); @@ -1162,6 +1177,11 @@ main (int argc, char **argv) app_entries, G_N_ELEMENTS (app_entries), app); + settings = g_settings_new ("org.gtk.Demo4"); + session_id = g_settings_get_string (settings, "session-id"); + gtk_application_set_session_id (app, session_id); + g_free (session_id); + for (i = 0; i < G_N_ELEMENTS (accels); i++) gtk_application_set_accels_for_action (app, accels[i].action_and_target, accels[i].accelerators); @@ -1173,8 +1193,11 @@ main (int argc, char **argv) g_signal_connect (app, "activate", G_CALLBACK (activate), NULL); g_signal_connect (app, "command-line", G_CALLBACK (command_line), NULL); g_signal_connect (app, "handle-local-options", G_CALLBACK (local_options), NULL); + g_signal_connect (app, "startup", G_CALLBACK (startup), settings); g_application_run (G_APPLICATION (app), argc, argv); + g_object_unref (settings); + return 0; } diff --git a/demos/gtk-demo/main.ui b/demos/gtk-demo/main.ui index e162ca1118b..c7391b22bd4 100644 --- a/demos/gtk-demo/main.ui +++ b/demos/gtk-demo/main.ui @@ -19,6 +19,7 @@ 800 600 + main-window diff --git a/demos/gtk-demo/org.gtk.Demo4.gschema.xml b/demos/gtk-demo/org.gtk.Demo4.gschema.xml index 3eaa6e8b40d..0332824dcd3 100644 --- a/demos/gtk-demo/org.gtk.Demo4.gschema.xml +++ b/demos/gtk-demo/org.gtk.Demo4.gschema.xml @@ -21,6 +21,10 @@ false + + + '' + -- GitLab From 3073e94bb2a8fb71ead762ac40501175ea342e75 Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Mon, 29 Jul 2024 18:13:31 +0200 Subject: [PATCH 8/9] demos: Restore Node Editor window state through session management Save the session id, and restore the main window position from previous runs. --- demos/node-editor/meson.build | 3 +++ demos/node-editor/node-editor-application.c | 13 +++++++++++++ demos/node-editor/node-editor-window.ui | 1 + demos/node-editor/org.gtk.NodeEditor4.gschema.xml | 11 +++++++++++ 4 files changed, 28 insertions(+) create mode 100644 demos/node-editor/org.gtk.NodeEditor4.gschema.xml diff --git a/demos/node-editor/meson.build b/demos/node-editor/meson.build index 7df42e3f6d3..7bbd4ce2fea 100644 --- a/demos/node-editor/meson.build +++ b/demos/node-editor/meson.build @@ -30,6 +30,9 @@ endforeach # desktop file install_data('org.gtk.gtk4.NodeEditor.desktop', install_dir: gtk_applicationsdir) +install_data('org.gtk.NodeEditor4.gschema.xml', install_dir: gtk_schemasdir) +gnome.compile_schemas() + # appdata configure_file( input: 'org.gtk.gtk4.NodeEditor.appdata.xml.in', diff --git a/demos/node-editor/node-editor-application.c b/demos/node-editor/node-editor-application.c index 5c0ff7bf83b..0331cca1c61 100644 --- a/demos/node-editor/node-editor-application.c +++ b/demos/node-editor/node-editor-application.c @@ -191,6 +191,16 @@ node_editor_application_startup (GApplication *app) const char *quit_accels[2] = { "Q", NULL }; const char *open_accels[2] = { "O", NULL }; GtkCssProvider *provider; + GSettings *settings; + gchar *session_id; + + settings = g_settings_new ("org.gtk.NodeEditor4"); + session_id = g_settings_get_string (settings, "session-id"); + if (session_id) + { + gtk_application_set_session_id (GTK_APPLICATION (app), session_id); + g_free (session_id); + } G_APPLICATION_CLASS (node_editor_application_parent_class)->startup (app); @@ -207,6 +217,9 @@ node_editor_application_startup (GApplication *app) gtk_style_context_add_provider_for_display (gdk_display_get_default (), GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + g_settings_set_string (settings, "session-id", + gtk_application_get_current_session_id (GTK_APPLICATION (app))); } static void diff --git a/demos/node-editor/node-editor-window.ui b/demos/node-editor/node-editor-window.ui index 68d43ba27e7..c245330481f 100644 --- a/demos/node-editor/node-editor-window.ui +++ b/demos/node-editor/node-editor-window.ui @@ -105,6 +105,7 @@ 1024 768 text_view + main-window diff --git a/demos/node-editor/org.gtk.NodeEditor4.gschema.xml b/demos/node-editor/org.gtk.NodeEditor4.gschema.xml new file mode 100644 index 00000000000..9bb332b6c1b --- /dev/null +++ b/demos/node-editor/org.gtk.NodeEditor4.gschema.xml @@ -0,0 +1,11 @@ + + + + + + + '' + + + + -- GitLab From 84339b1dc4e1e5cdc08b37827eb073a9def9f4a7 Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Mon, 29 Jul 2024 18:27:35 +0200 Subject: [PATCH 9/9] gtk: Add blurb for session management in GtkApplication docs --- gtk/gtkapplication.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/gtk/gtkapplication.c b/gtk/gtkapplication.c index c9ec81d2967..e0abcd8df90 100644 --- a/gtk/gtkapplication.c +++ b/gtk/gtkapplication.c @@ -106,6 +106,31 @@ * inform the user about the negative consequences of ending the * session while inhibitors are present. * + * ## Restoring window state + * + * In the windowing environments that support it, `GtkApplication` supports + * restoring window state (this is up to the environment, but typically + * includes the position, and size states such as maximized). + * + * In order to restore state from a previous execution, the application code + * will need to specify a session identifier through + * [method@Gtk.Application.set_session_id] obtained from a previous session. + * In order to obtain a brand new session, %NULL should be used. + * + * The session will be registered during [signal@Gio.Application.startup], + * at which point [method@Gtk.Application.get_current_session_id] will return + * the session identifier effective during the current session. This is the + * session identifier that needs to be preserved for future runs. If the + * session is being restored from prior state, this identifier will match + * the one given through [method@Gtk.Application.set_session_id], otherwise + * a brand new identifier will be given. + * + * Individual windows are identified through [method@Gtk.Window.set_session_id] + * so application toplevel windows can be matched with the previously saved + * session state. Windows will be restored to their original positions when + * mapped. This state is saved in the background, thus no explicit save() call + * is necessary. + * * ## See Also * * - [Using GtkApplication](https://developer.gnome.org/documentation/tutorials/application.html) -- GitLab