diff --git a/demos/gtk-demo/main.c b/demos/gtk-demo/main.c index 0e27a441e915755d2cdffa5a89d4561b6c7c30ae..bf96f2a67a7dd988f5aff13b3f8c7add27b7d303 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 e162ca1118b615927424609b945e2ffaea951e73..c7391b22bd47278d60b5da362bc3c56362298526 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 3eaa6e8b40da53c1a492fd5688c3c33d859d7401..0332824dcd36022b23288042bdadbc4aa7c6f800 100644 --- a/demos/gtk-demo/org.gtk.Demo4.gschema.xml +++ b/demos/gtk-demo/org.gtk.Demo4.gschema.xml @@ -21,6 +21,10 @@ false + + + '' + diff --git a/demos/node-editor/meson.build b/demos/node-editor/meson.build index 7df42e3f6d3b598ab30a2a08f733d00e9f44a395..7bbd4ce2fea9230ce880f39216904565cd8a5e69 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 5c0ff7bf83beacbc6e742df30b42675a29bd1398..0331cca1c61c5e53ba934f88d128bd6974c0a925 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 68d43ba27e73e89d137223d46b8a5d86649bbd24..c245330481fcaf7f0d0f398729303a1dc23d76ba 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 0000000000000000000000000000000000000000..9bb332b6c1bcef88e113f1d1f59e1b754eb3c09b --- /dev/null +++ b/demos/node-editor/org.gtk.NodeEditor4.gschema.xml @@ -0,0 +1,11 @@ + + + + + + + '' + + + + diff --git a/gdk/wayland/gdkdisplay-wayland.c b/gdk/wayland/gdkdisplay-wayland.c index 63eade8534e0188a7c7fc7eb1ef8d723f2c20e6e..af84a0d74763bd1705ecd8bd6f8ecd12242535ea 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 9b23f541788ea82ad3744feba06256bc49878db9..f939498796f15397b05bde08e43815c9d2b3efda 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 xx_session_manager_v1 *xdg_session_manager; + struct xx_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/gdktoplevel-wayland.c b/gdk/wayland/gdktoplevel-wayland.c index a8d15bc208ce9c72de9b60087bd43fcecd7986e5..2bbcfe185650058a073726d5f363e1fa90acbdfa 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/gdkwaylanddisplay.h b/gdk/wayland/gdkwaylanddisplay.h index 396a6f98f218bb28ed334ab557fd2f292ed10aaf..2637194ac179f627217315ec2c7828a29380466b 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 diff --git a/gdk/wayland/gdkwaylandtoplevel.h b/gdk/wayland/gdkwaylandtoplevel.h index 0240f9d19fa78eef3c383e6a4f7578532f177a54..08725489c531ec5615176bfad2e920590bbc0e50 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 diff --git a/gdk/wayland/meson.build b/gdk/wayland/meson.build index 706da22c83ba8f891bc3ed6f17c9af3f05dfba64..caef045be7513c482cc5f66af54b747b274b553c 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 0000000000000000000000000000000000000000..e21694424f1db1a0eafd564a74d5d003adaf32b6 --- /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. + + + + + diff --git a/gtk/gtkapplication-wayland.c b/gtk/gtkapplication-wayland.c index f002c3fc6650f02b6c2d400212372ff63ff66a5b..2648a8f3035906f5bcc277607e110e79cc68b156 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; } diff --git a/gtk/gtkapplication.c b/gtk/gtkapplication.c index 4d81ef56db384c6e1c8c35f1e3aed1c2b6a65c92..e0abcd8df90ca4ce1a21bbcc600a5de3a7f9e087 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) @@ -127,6 +152,7 @@ enum { PROP_SCREENSAVER_ACTIVE, PROP_MENUBAR, PROP_ACTIVE_WINDOW, + PROP_SESSION_ID, NUM_PROPERTIES }; @@ -147,6 +173,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 +477,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 +506,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 +670,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 +1270,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 7260de6dc7ef9c296b6ef322b2dd68ce140988ca..0ca9bd2d053a5fd994302b756c9ba37d146dc123 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 6948fa11e065e118b95e1380239729275fb8c137..1403625a3ef5464d5e4d2c7d964c835bd29c3173 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 a6c56c89dfbde83ab1f73465699575513eeda805..246ab8d654ade9e67e546824ab28eea754458c07 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 diff --git a/gtk/gtkwindow.c b/gtk/gtkwindow.c index df089a16bf0636a1d3cd64d4e1ed92613efd883b..dabdf984d7a9ada6acec5c42340d1410b075d0a8 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 a9c0228b011cf51c442076879b3c339418cb62f1..9553b655ea73a1b03266fbce39b9c60640f89194 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)