Compare commits

...

15 Commits

Author SHA1 Message Date
Matthias Clasen
e9ce1959b4 fixup: use cache dir 2021-06-03 10:24:47 -04:00
Matthias Clasen
a7edd0992e exampleapp9: Add state saving
This tests the GtkApplication state saving support.
2021-06-03 10:07:43 -04:00
Matthias Clasen
9f35250330 application: Add state saving
Add the necessary plumbing to save and restore application
windows and their state.
2021-06-03 10:07:43 -04:00
Matthias Clasen
7e8dfd5bda Add a state-saving test 2021-06-03 10:07:43 -04:00
Matthias Clasen
aa0df60606 window: Add default state saving
This is somewhat temporary, since need compositor
support to do this properly.
2021-06-03 10:07:43 -04:00
Matthias Clasen
9ee07d3974 togglebutton: Add default state saving 2021-06-03 10:07:43 -04:00
Matthias Clasen
1f7ca109c9 checkbutton: Add default state saving 2021-06-03 10:07:43 -04:00
Matthias Clasen
bc2d8bfa33 passwordentry: Add default state saving
In addition to the text, save the visibility.
2021-06-03 10:07:43 -04:00
Matthias Clasen
ee0e82610c stack: Add default state saving
Save the current child.
2021-06-03 10:07:43 -04:00
Matthias Clasen
26c5240dcb paned: Add default state saving
Save the paned position.
2021-06-03 10:07:43 -04:00
Matthias Clasen
ecf6c354b5 switch: Add default state saving 2021-06-03 10:07:43 -04:00
Matthias Clasen
fd499188bd colorchooserwidget: Add default state saving
Save the rgba and the show-editor state. We
currently don't save the contents of the entry.
2021-06-03 10:07:43 -04:00
Matthias Clasen
0c6fb610e6 text: Add default state saving
Add default handlers for ::save/restore-state. Also,
since GtkText is usually used inside other widgets,
set a save-id.
2021-06-03 10:07:43 -04:00
Matthias Clasen
1962f535e8 range: Add default state saving
Add default handlers for ::save/restore-state. All that is
needed to make state saving work now is setting save-id.
2021-06-03 10:07:43 -04:00
Matthias Clasen
8db1b89ace widget: Add state saving plumbing
Add ::save-state and ::restore signals that let widgets
save and restore their state to a GVariant.
2021-06-03 10:07:43 -04:00
20 changed files with 1135 additions and 6 deletions

View File

@@ -59,12 +59,14 @@ example_app_startup (GApplication *app)
}
static void
example_app_activate (GApplication *app)
example_app_create_window (GtkApplication *app,
const char *save_id)
{
ExampleAppWindow *win;
win = example_app_window_new (EXAMPLE_APP (app));
gtk_window_present (GTK_WINDOW (win));
gtk_widget_set_save_id (GTK_WIDGET (win), save_id);
/* FIXME: differentiate save ids */
}
static void
@@ -93,8 +95,8 @@ static void
example_app_class_init (ExampleAppClass *class)
{
G_APPLICATION_CLASS (class)->startup = example_app_startup;
G_APPLICATION_CLASS (class)->activate = example_app_activate;
G_APPLICATION_CLASS (class)->open = example_app_open;
GTK_APPLICATION_CLASS (class)->create_window = example_app_create_window;
}
ExampleApp *
@@ -103,5 +105,7 @@ example_app_new (void)
return g_object_new (EXAMPLE_APP_TYPE,
"application-id", "org.gtk.exampleapp",
"flags", G_APPLICATION_HANDLES_OPEN,
"register-session", TRUE,
"save-state", TRUE,
NULL);
}

View File

@@ -17,6 +17,8 @@ struct _ExampleAppWindow
GtkWidget *words;
GtkWidget *lines;
GtkWidget *lines_label;
GList *files;
};
G_DEFINE_TYPE (ExampleAppWindow, example_app_window, GTK_TYPE_APPLICATION_WINDOW)
@@ -211,14 +213,68 @@ example_app_window_dispose (GObject *object)
g_clear_object (&win->settings);
g_list_free_full (win->files, g_object_unref);
win->files = NULL;
G_OBJECT_CLASS (example_app_window_parent_class)->dispose (object);
}
static gboolean
example_app_window_save_state (GtkWidget *widget,
GVariantDict *dict,
gboolean *save_children)
{
ExampleAppWindow *win = EXAMPLE_APP_WINDOW (widget);
GVariantBuilder builder;
g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
for (GList *l = win->files; l; l = l->next)
{
g_variant_builder_add (&builder, "s", g_file_peek_path (G_FILE (l->data)));
}
g_variant_dict_insert_value (dict, "files", g_variant_builder_end (&builder));
/* Save window state */
GTK_WIDGET_CLASS (example_app_window_parent_class)->save_state (widget, dict, save_children);
*save_children = TRUE;
return TRUE;
}
static gboolean
example_app_window_restore_state (GtkWidget *widget,
GVariant *state)
{
ExampleAppWindow *win = EXAMPLE_APP_WINDOW (widget);
GVariantIter *iter;
/* Restore window state */
GTK_WIDGET_CLASS (example_app_window_parent_class)->restore_state (widget, state);
if (g_variant_lookup (state, "files", "as", &iter))
{
const char *path;
while (g_variant_iter_next (iter, "&s", &path))
{
GFile *file = g_file_new_for_path (path);
example_app_window_open (win, file);
g_object_unref (file);
}
}
return TRUE;
}
static void
example_app_window_class_init (ExampleAppWindowClass *class)
{
G_OBJECT_CLASS (class)->dispose = example_app_window_dispose;
GTK_WIDGET_CLASS (class)->save_state = example_app_window_save_state;
GTK_WIDGET_CLASS (class)->restore_state = example_app_window_restore_state;
gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
"/org/gtk/exampleapp/window.ui");
@@ -288,4 +344,6 @@ example_app_window_open (ExampleAppWindow *win,
update_words (win);
update_lines (win);
win->files = g_list_append (win->files, g_object_ref (file));
}

View File

@@ -4,6 +4,7 @@
<property name="title" translatable="yes">Example Application</property>
<property name="default-width">600</property>
<property name="default-height">400</property>
<property name="save-id">window</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="header">
<child>
@@ -26,6 +27,7 @@
<object class="GtkToggleButton" id="search">
<property name="sensitive">0</property>
<property name="icon-name">edit-find-symbolic</property>
<property name="save-id">search</property>
</object>
</child>
<child type="end">
@@ -38,6 +40,7 @@
<child>
<object class="GtkBox" id="content_box">
<property name="orientation">vertical</property>
<property name="save-id">content_box</property>
<child>
<object class="GtkSearchBar" id="searchbar">
<child>
@@ -49,8 +52,10 @@
</child>
<child>
<object class="GtkBox" id="hbox">
<property name="save-id">hbox</property>
<child>
<object class="GtkRevealer" id="sidebar">
<property name="save-id">sidebar</property>
<property name="transition-type">slide-right</property>
<child>
<object class="GtkScrolledWindow" id="sidebar-sw">
@@ -66,6 +71,7 @@
</child>
<child>
<object class="GtkStack" id="stack">
<property name="save-id">stack</property>
<signal name="notify::visible-child" handler="visible_child_changed"/>
</object>
</child>

View File

@@ -27,6 +27,8 @@
#include <gio/gunixfdlist.h>
#endif
#include <glib/gstdio.h>
#include <stdlib.h>
#ifdef HAVE_UNISTD_H
@@ -119,6 +121,7 @@ enum {
WINDOW_ADDED,
WINDOW_REMOVED,
QUERY_END,
CREATE_WINDOW,
LAST_SIGNAL
};
@@ -130,6 +133,7 @@ enum {
PROP_SCREENSAVER_ACTIVE,
PROP_MENUBAR,
PROP_ACTIVE_WINDOW,
PROP_SAVE_STATE,
NUM_PROPERTIES
};
@@ -147,6 +151,7 @@ typedef struct
gboolean register_session;
gboolean screensaver_active;
gboolean save_state;
GtkActionMuxer *muxer;
GtkBuilder *menus_builder;
char *help_overlay_path;
@@ -270,6 +275,26 @@ gtk_application_startup (GApplication *g_application)
gdk_profiler_end_mark (before, "gtk application startup", NULL);
}
static void
gtk_application_activate (GApplication *g_application)
{
GtkApplication *application = GTK_APPLICATION (g_application);
GtkApplicationPrivate *priv = gtk_application_get_instance_private (application);
guint activate_id;
activate_id = g_signal_lookup ("activate", G_TYPE_APPLICATION);
if (!g_signal_has_handler_pending (g_application, activate_id, 0, TRUE))
{
if (!priv->save_state ||
!gtk_application_restore (application))
{
g_signal_emit (application,
gtk_application_signals[CREATE_WINDOW], 0, NULL);
gtk_window_present (GTK_WINDOW (priv->windows->data));
}
}
}
static void
gtk_application_shutdown (GApplication *g_application)
{
@@ -284,6 +309,9 @@ gtk_application_shutdown (GApplication *g_application)
gtk_action_muxer_remove (priv->muxer, "app");
if (priv->save_state)
gtk_application_save (application);
gtk_main_sync ();
G_APPLICATION_CLASS (gtk_application_parent_class)->shutdown (g_application);
@@ -451,6 +479,10 @@ gtk_application_get_property (GObject *object,
g_value_set_object (value, gtk_application_get_active_window (application));
break;
case PROP_SAVE_STATE:
g_value_set_boolean (value, priv->save_state);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -476,6 +508,10 @@ gtk_application_set_property (GObject *object,
gtk_application_set_menubar (application, g_value_get_object (value));
break;
case PROP_SAVE_STATE:
priv->save_state = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -530,6 +566,7 @@ gtk_application_class_init (GtkApplicationClass *class)
application_class->after_emit = gtk_application_after_emit;
application_class->startup = gtk_application_startup;
application_class->shutdown = gtk_application_shutdown;
application_class->activate = gtk_application_activate;
application_class->dbus_register = gtk_application_dbus_register;
application_class->dbus_unregister = gtk_application_dbus_unregister;
@@ -586,6 +623,33 @@ gtk_application_class_init (GtkApplicationClass *class)
NULL,
G_TYPE_NONE, 0);
/**
* GtkApplication::create-window:
* @application: the `GtkApplication` which emitted the signal
* @save_id: (nullable): save-id to set on the new window
*
* In response to this signal, you should create a new application window,
* and add it to @application.
*
*`GtkApplication` will call [method@Gtk.Window.present] on the window.
* If @save_id is passed, it should be set as the [property@Gtk.Widget:save-id]
* of the newly created window.
*
* You should handle this signal instead of `::activate` to make automatic
* session saving work. See [property@Gtk.Application:save-state] for more
* information.
*
* Since: 4.4
*/
gtk_application_signals[CREATE_WINDOW] =
g_signal_new (I_("create-window"), GTK_TYPE_APPLICATION, G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET (GtkApplicationClass, create_window),
NULL, NULL,
NULL,
G_TYPE_NONE, 1,
G_TYPE_STRING);
/**
* GtkApplication:register-session:
*
@@ -644,6 +708,29 @@ gtk_application_class_init (GtkApplicationClass *class)
GTK_TYPE_WINDOW,
G_PARAM_READABLE|G_PARAM_STATIC_STRINGS);
/**
* GtkApplication:save-state:
*
* Whether to save and restore the application state automatically when the
* application is closed or the session ends.
*
* In order to make automatic state restoration work, you should handle
* [signal@Gtk.Application::create-window]. The default `::activate` handler
* will emit that signal as needed, and call [method@Gtk.Application.restore]
* when it finds saved state.
*
* In order to make state saving work, widgets need to have a
* [property@Gtk.Widget:save-id] set.
*
* Since: 4.4
*/
gtk_application_props[PROP_SAVE_STATE] =
g_param_spec_boolean ("save-state",
P_("Save state"),
P_("Whether to save application state automatically"),
FALSE,
G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, NUM_PROPERTIES, gtk_application_props);
}
@@ -1230,3 +1317,162 @@ gtk_application_set_screensaver_active (GtkApplication *application,
g_object_notify (G_OBJECT (application), "screensaver-active");
}
}
static char *
get_state_file (GtkApplication *application)
{
const char *app_id;
const char *dir;
app_id = g_application_get_application_id (G_APPLICATION (application));
dir = g_get_user_cache_dir ();
return g_strconcat (dir, G_DIR_SEPARATOR_S, app_id, ".state", NULL);
}
/**
* gtk_application_save:
* @application: a `GtkApplication`
*
* Saves the state of application.
*
* See [method@Gtk.Application.restore] for a way to restore the state.
*
* If [property@Gtk.Application:save-state] is set, `GtkApplication` calls this
* function automatically when the application is closed or the session ends.
*
* Since: 4.4
*/
void
gtk_application_save (GtkApplication *application)
{
GtkApplicationPrivate *priv = gtk_application_get_instance_private (application);
GList *l;
GVariantBuilder builder;
GVariant *state;
g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
for (l = priv->windows; l != NULL; l = l->next)
{
const char *id;
GVariant *v;
id = gtk_widget_get_save_id (GTK_WIDGET (l->data));
if (!id)
continue;
v = gtk_widget_save_state (GTK_WIDGET (l->data));
g_variant_builder_add (&builder, "{sv}", id, v);
}
state = g_variant_builder_end (&builder);
if (g_variant_n_children (state) > 0)
{
char *file = get_state_file (application);
g_file_set_contents (file,
g_variant_get_data (state),
g_variant_get_size (state),
NULL);
g_free (file);
}
g_variant_unref (state);
}
/**
* gtk_application_forget:
* @application: a `GtkApplication`
*
* Forget state that has been previously saved.
*
* Note that `GtkApplication` will save state automatically
* as long as [property@Gtk.Application:save-state] is set,
* so you probably want to unset that property as well, if
* your goal is to implement a “factory reset”.
*
* See [method@Gtk.Application.save] for more information.
*
* Since: 4.4
*/
void
gtk_application_forget (GtkApplication *application)
{
char *file = get_state_file (application);
g_remove (file);
g_free (file);
}
/**
* gtk_application_restore:
* @application: a `GtkApplication`
*
* Restores previously saved state.
*
* See [method@Gtk.Application.save] for a way to save application state.
*
* If [property@Gtk.Application:save-state] is set, `GtkApplication` calls this
* function automatically in the default `::activate` handler. Note that you
* need to handle the [signal@Gtk.Application::create-window] to make restoring
* state work.
*
* Returns: %TRUE if at least one window has been restored from saved state
*
* Since: 4.4
*/
gboolean
gtk_application_restore (GtkApplication *application)
{
GtkApplicationPrivate *priv = gtk_application_get_instance_private (application);
char *file = get_state_file (application);
char *contents;
gsize len;
GVariant *state;
GVariantIter iter;
const char *key;
gboolean restored;
if (!g_file_get_contents (file, &contents, &len, NULL))
{
g_free (file);
return FALSE;
}
state = g_variant_new_from_data (G_VARIANT_TYPE_VARDICT, contents, len, FALSE, NULL, NULL);
g_variant_iter_init (&iter, state);
while (g_variant_iter_next (&iter, "{sv}", &key, NULL))
g_signal_emit (application, gtk_application_signals[CREATE_WINDOW], 0, key);
restored = FALSE;
for (GList *l = priv->windows; l; l = l->next)
{
GtkWidget *window = GTK_WIDGET (l->data);
const char *id;
GVariant *v;
id = gtk_widget_get_save_id (window);
if (id &&
(v = g_variant_lookup_value (state, id, NULL)) != NULL)
{
restored = TRUE;
gtk_widget_restore_state (window, v);
g_variant_unref (v);
}
else
{
g_warning ("::create-window did not set save-id");
}
gtk_window_present (GTK_WINDOW (window));
}
g_free (file);
g_free (contents);
return restored;
}

View File

@@ -64,8 +64,11 @@ struct _GtkApplicationClass
void (*window_removed) (GtkApplication *application,
GtkWindow *window);
void (*create_window) (GtkApplication *application,
const char *save_id);
/*< private >*/
gpointer padding[8];
gpointer padding[7];
};
GDK_AVAILABLE_IN_ALL
@@ -135,6 +138,14 @@ GDK_AVAILABLE_IN_ALL
GMenu * gtk_application_get_menu_by_id (GtkApplication *application,
const char *id);
GDK_AVAILABLE_IN_4_4
void gtk_application_save (GtkApplication *application);
GDK_AVAILABLE_IN_4_4
void gtk_application_forget (GtkApplication *application);
GDK_AVAILABLE_IN_4_4
gboolean gtk_application_restore (GtkApplication *application);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GtkApplication, g_object_unref)
G_END_DECLS

View File

@@ -498,6 +498,29 @@ gtk_check_button_real_activate (GtkCheckButton *self)
gtk_check_button_set_active (self, !gtk_check_button_get_active (self));
}
static gboolean
gtk_check_button_save_state (GtkWidget *self,
GVariantDict *dict,
gboolean *save_children)
{
g_variant_dict_insert (dict, "active", "b", gtk_check_button_get_active (GTK_CHECK_BUTTON (self)));
*save_children = FALSE;
return TRUE;
}
static gboolean
gtk_check_button_restore_state (GtkWidget *self,
GVariant *data)
{
gboolean value;
if (g_variant_lookup (data, "active", "b", &value))
gtk_check_button_set_active (GTK_CHECK_BUTTON (self), value);
return TRUE;
}
static void
gtk_check_button_class_init (GtkCheckButtonClass *class)
{
@@ -518,6 +541,8 @@ gtk_check_button_class_init (GtkCheckButtonClass *class)
widget_class->state_flags_changed = gtk_check_button_state_flags_changed;
widget_class->focus = gtk_check_button_focus;
widget_class->save_state = gtk_check_button_save_state;
widget_class->restore_state = gtk_check_button_restore_state;
class->activate = gtk_check_button_real_activate;

View File

@@ -263,6 +263,43 @@ update_from_editor (GtkColorEditor *editor,
g_object_notify (G_OBJECT (widget), "rgba");
}
static gboolean
gtk_color_chooser_save_state (GtkWidget *widget,
GVariantDict *dict,
gboolean *save_children)
{
GtkColorChooserWidget *cc = GTK_COLOR_CHOOSER_WIDGET (widget);
GdkRGBA c;
gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (cc), &c);
g_variant_dict_insert (dict, "show-editor", "b", gtk_widget_get_visible (cc->editor));
g_variant_dict_insert (dict, "rgba", "(dddd)", c.red, c.green, c.blue, c.alpha);
*save_children = TRUE;
return TRUE;
}
static gboolean
gtk_color_chooser_restore_state (GtkWidget *widget,
GVariant *data)
{
GtkColorChooserWidget *cc = GTK_COLOR_CHOOSER_WIDGET (widget);
gboolean show_editor;
double r, g, b, a;
if (g_variant_lookup (data, "show-editor", "b", &show_editor))
gtk_color_chooser_widget_set_show_editor (cc, show_editor);
if (g_variant_lookup (data, "rgba", "(dddd)", &r, &g, &b, &a))
{
GdkRGBA c = { r, g, b, a };
gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (cc), &c);
}
return TRUE;
}
/* UI construction {{{1 */
static void
@@ -706,6 +743,8 @@ gtk_color_chooser_widget_class_init (GtkColorChooserWidgetClass *class)
widget_class->grab_focus = gtk_widget_grab_focus_child;
widget_class->focus = gtk_widget_focus_child;
widget_class->save_state = gtk_color_chooser_save_state;
widget_class->restore_state = gtk_color_chooser_restore_state;
g_object_class_override_property (object_class, PROP_RGBA, "rgba");
g_object_class_override_property (object_class, PROP_USE_ALPHA, "use-alpha");

View File

@@ -390,6 +390,29 @@ gtk_paned_set_orientation (GtkPaned *self,
}
}
static gboolean
gtk_paned_save_state (GtkWidget *paned,
GVariantDict *dict,
gboolean *save_children)
{
g_variant_dict_insert (dict, "position", "i", gtk_paned_get_position (GTK_PANED (paned)));
*save_children = TRUE;
return TRUE;
}
static gboolean
gtk_paned_restore_state (GtkWidget *paned,
GVariant *data)
{
int value;
if (g_variant_lookup (data, "position", "i", &value))
gtk_paned_set_position(GTK_PANED (paned), value);
return TRUE;
}
static void
gtk_paned_class_init (GtkPanedClass *class)
{
@@ -408,6 +431,8 @@ gtk_paned_class_init (GtkPanedClass *class)
widget_class->css_changed = gtk_paned_css_changed;
widget_class->get_request_mode = gtk_paned_get_request_mode;
widget_class->compute_expand = gtk_paned_compute_expand;
widget_class->save_state = gtk_paned_save_state;
widget_class->restore_state = gtk_paned_restore_state;
class->cycle_child_focus = gtk_paned_cycle_child_focus;
class->toggle_handle_focus = gtk_paned_toggle_handle_focus;

View File

@@ -415,6 +415,32 @@ gtk_password_entry_mnemonic_activate (GtkWidget *widget,
return TRUE;
}
static gboolean
gtk_password_entry_save_state (GtkWidget *widget,
GVariantDict *dict,
gboolean *save_children)
{
GtkPasswordEntry *entry = GTK_PASSWORD_ENTRY (widget);
g_variant_dict_insert (dict, "visibility", "b", gtk_text_get_visibility (GTK_TEXT (entry->entry)));
*save_children = TRUE;
return TRUE;
}
static gboolean
gtk_password_entry_restore_state (GtkWidget *widget,
GVariant *data)
{
GtkPasswordEntry *entry = GTK_PASSWORD_ENTRY (widget);
gboolean value;
if (g_variant_lookup (data, "visibility", "b", &value))
gtk_text_set_visibility (GTK_TEXT (entry->entry), value);
return TRUE;
}
static void
gtk_password_entry_class_init (GtkPasswordEntryClass *klass)
{
@@ -431,6 +457,8 @@ gtk_password_entry_class_init (GtkPasswordEntryClass *klass)
widget_class->mnemonic_activate = gtk_password_entry_mnemonic_activate;
widget_class->grab_focus = gtk_widget_grab_focus_child;
widget_class->focus = gtk_widget_focus_child;
widget_class->save_state = gtk_password_entry_save_state;
widget_class->restore_state = gtk_password_entry_restore_state;
/**
* GtkPasswordEntry:placeholder-text:

View File

@@ -245,6 +245,12 @@ static gboolean gtk_range_scroll_controller_scroll (GtkEventControllerScrol
static void gtk_range_set_orientation (GtkRange *range,
GtkOrientation orientation);
static gboolean gtk_range_save_state (GtkWidget *range,
GVariantDict *dict,
gboolean *save);
static gboolean gtk_range_restore_state (GtkWidget *range,
GVariant *data);
G_DEFINE_TYPE_WITH_CODE (GtkRange, gtk_range, GTK_TYPE_WIDGET,
G_ADD_PRIVATE (GtkRange)
G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE,
@@ -271,6 +277,8 @@ gtk_range_class_init (GtkRangeClass *class)
widget_class->size_allocate = gtk_range_size_allocate;
widget_class->unmap = gtk_range_unmap;
widget_class->direction_changed = gtk_range_direction_changed;
widget_class->save_state = gtk_range_save_state;
widget_class->restore_state = gtk_range_restore_state;
class->move_slider = gtk_range_move_slider;
class->change_value = gtk_range_real_change_value;
@@ -2859,6 +2867,29 @@ _gtk_range_get_stop_positions (GtkRange *range,
return priv->n_marks;
}
static gboolean
gtk_range_save_state (GtkWidget *range,
GVariantDict *dict,
gboolean *save_children)
{
g_variant_dict_insert (dict, "value", "d", gtk_range_get_value (GTK_RANGE (range)));
*save_children = FALSE;
return TRUE;
}
static gboolean
gtk_range_restore_state (GtkWidget *range,
GVariant *data)
{
double value;
if (g_variant_lookup (data, "value", "d", &value))
gtk_range_set_value (GTK_RANGE (range), value);
return TRUE;
}
/**
* gtk_range_set_round_digits: (attributes org.gtk.Method.set_property=round-digits)
* @range: a `GtkRange`

View File

@@ -794,6 +794,29 @@ gtk_stack_set_property (GObject *object,
}
}
static gboolean
gtk_stack_save_state (GtkWidget *widget,
GVariantDict *dict,
gboolean *save_children)
{
g_variant_dict_insert (dict, "visible-page", "s", gtk_stack_get_visible_child_name (GTK_STACK (widget)));
*save_children = TRUE;
return TRUE;
}
static gboolean
gtk_stack_restore_state (GtkWidget *widget,
GVariant *data)
{
const char *name;
if (g_variant_lookup (data, "visible-page", "&s", &name))
gtk_stack_set_visible_child_name (GTK_STACK (widget), name);
return TRUE;
}
static void
gtk_stack_class_init (GtkStackClass *klass)
{
@@ -810,6 +833,8 @@ gtk_stack_class_init (GtkStackClass *klass)
widget_class->measure = gtk_stack_measure;
widget_class->compute_expand = gtk_stack_compute_expand;
widget_class->get_request_mode = gtk_stack_get_request_mode;
widget_class->save_state = gtk_stack_save_state;
widget_class->restore_state = gtk_stack_restore_state;
/**
* GtkStack:hhomogeneous: (attributes org.gtk.Property.get=gtk_stack_get_hhomogeneous org.gtk.Property.set=gtk_stack_set_hhomogeneous)

View File

@@ -506,6 +506,29 @@ state_set (GtkSwitch *self,
return TRUE;
}
static gboolean
gtk_switch_save_state (GtkWidget *self,
GVariantDict *dict,
gboolean *save_children)
{
g_variant_dict_insert (dict, "active", "b", gtk_switch_get_active (GTK_SWITCH (self)));
*save_children = FALSE;
return TRUE;
}
static gboolean
gtk_switch_restore_state (GtkWidget *self,
GVariant *data)
{
gboolean value;
if (g_variant_lookup (data, "active", "b", &value))
gtk_switch_set_active (GTK_SWITCH (self), value);
return TRUE;
}
static void
gtk_switch_class_init (GtkSwitchClass *klass)
{
@@ -545,6 +568,9 @@ gtk_switch_class_init (GtkSwitchClass *klass)
g_object_class_install_properties (gobject_class, LAST_PROP, switch_props);
widget_class->save_state = gtk_switch_save_state;
widget_class->restore_state = gtk_switch_restore_state;
klass->activate = gtk_switch_activate;
klass->state_set = state_set;

View File

@@ -595,6 +595,12 @@ static void gtk_text_history_select_cb (gpointer funcs_data,
int selection_insert,
int selection_bound);
static gboolean gtk_text_save_state (GtkWidget *widget,
GVariantDict *dict,
gboolean *save_children);
static gboolean gtk_text_restore_state (GtkWidget *widget,
GVariant *data);
/* GtkTextContent implementation
*/
#define GTK_TYPE_TEXT_CONTENT (gtk_text_content_get_type ())
@@ -734,6 +740,8 @@ gtk_text_class_init (GtkTextClass *class)
widget_class->direction_changed = gtk_text_direction_changed;
widget_class->state_flags_changed = gtk_text_state_flags_changed;
widget_class->mnemonic_activate = gtk_text_mnemonic_activate;
widget_class->save_state = gtk_text_save_state;
widget_class->restore_state = gtk_text_restore_state;
class->move_cursor = gtk_text_move_cursor;
class->insert_at_cursor = gtk_text_insert_at_cursor;
@@ -1870,6 +1878,7 @@ gtk_text_init (GtkText *self)
gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
gtk_widget_set_save_id (GTK_WIDGET (self), "text");
priv->editable = TRUE;
priv->visible = TRUE;
@@ -7181,3 +7190,26 @@ gtk_text_history_select_cb (gpointer funcs_data,
selection_insert,
selection_bound);
}
static gboolean
gtk_text_save_state (GtkWidget *widget,
GVariantDict *dict,
gboolean *save_children)
{
g_variant_dict_insert (dict, "text", "s", gtk_editable_get_text (GTK_EDITABLE (widget)));
*save_children = FALSE;
return TRUE;
}
static gboolean
gtk_text_restore_state (GtkWidget *widget,
GVariant *data)
{
const char *text;
if (g_variant_lookup (data, "text", "&s", &text))
gtk_editable_set_text (GTK_EDITABLE (widget), text);
return TRUE;
}

View File

@@ -249,6 +249,29 @@ get_group_first (GtkToggleButton *self)
return group_first;
}
static gboolean
gtk_toggle_button_save_state (GtkWidget *self,
GVariantDict *dict,
gboolean *save_children)
{
g_variant_dict_insert (dict, "active", "b", gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self)));
*save_children = FALSE;
return TRUE;
}
static gboolean
gtk_toggle_button_restore_state (GtkWidget *self,
GVariant *data)
{
gboolean value;
if (g_variant_lookup (data, "active", "b", &value))
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self), value);
return TRUE;
}
static void
gtk_toggle_button_class_init (GtkToggleButtonClass *class)
{
@@ -261,6 +284,8 @@ gtk_toggle_button_class_init (GtkToggleButtonClass *class)
gobject_class->get_property = gtk_toggle_button_get_property;
widget_class->mnemonic_activate = gtk_toggle_button_mnemonic_activate;
widget_class->save_state = gtk_toggle_button_save_state;
widget_class->restore_state = gtk_toggle_button_restore_state;
button_class->clicked = gtk_toggle_button_clicked;

View File

@@ -479,6 +479,8 @@ enum {
MOVE_FOCUS,
KEYNAV_FAILED,
QUERY_TOOLTIP,
SAVE_STATE,
RESTORE_STATE,
LAST_SIGNAL
};
@@ -518,6 +520,7 @@ enum {
PROP_CSS_NAME,
PROP_CSS_CLASSES,
PROP_LAYOUT_MANAGER,
PROP_SAVE_ID,
NUM_PROPERTIES,
/* GtkAccessible */
@@ -996,6 +999,9 @@ gtk_widget_set_property (GObject *object,
case PROP_ACCESSIBLE_ROLE:
gtk_widget_set_accessible_role (widget, g_value_get_enum (value));
break;
case PROP_SAVE_ID:
gtk_widget_set_save_id (widget, g_value_get_string (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -1129,6 +1135,9 @@ gtk_widget_get_property (GObject *object,
case PROP_ACCESSIBLE_ROLE:
g_value_set_enum (value, gtk_widget_get_accessible_role (widget));
break;
case PROP_SAVE_ID:
g_value_set_string (value, priv->save_id);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -1672,6 +1681,21 @@ gtk_widget_class_init (GtkWidgetClass *klass)
GTK_TYPE_LAYOUT_MANAGER,
GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkWidget:save-id: (attributes org.gtk.Property.get=gtk_widget_get_save_id org.gtk.Property.set=gtk_widget_set_save_id)
*
* The ID under which persistent state of this widget is saved.
*
* In order for a widget to have its state saved (and restored), the widget
* and all its ancestors must have a `save-id`.
*/
widget_props[PROP_SAVE_ID] =
g_param_spec_string ("save-id",
P_("Save ID"),
P_("The ID to save the widget state under"),
NULL,
GTK_PARAM_READWRITE);
g_object_class_install_properties (gobject_class, NUM_PROPERTIES, widget_props);
g_object_class_override_property (gobject_class, PROP_ACCESSIBLE_ROLE, "accessible-role");
@@ -1955,6 +1979,53 @@ gtk_widget_class_init (GtkWidgetClass *klass)
G_TYPE_FROM_CLASS (klass),
_gtk_marshal_BOOLEAN__INT_INT_BOOLEAN_OBJECTv);
/**
* GtkWidget:save-state:
* @widget: the object which received the signal
* @dict: a `GVariantDict`
* @save_children: (out): return location for whether to save children
*
* The handler for this signal should persist any state of @widget
* into @dict, and set @save_children if the child widgets may have
* state worth saving too.
*
* See [signal@Gtk.Widget:restore-state].
*
* Returns: %TRUE to stop stop further handlers from running
*/
widget_signals[SAVE_STATE] =
g_signal_new (I_("save-state"),
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GtkWidgetClass, save_state),
_gtk_boolean_handled_accumulator, NULL,
NULL,
G_TYPE_BOOLEAN, 2,
G_TYPE_VARIANT_DICT,
G_TYPE_POINTER);
/**
* GtkWidget:restore-state:
* @widget: the object which received the signal
* @state: an "a{sv}" `GVariant` with state to restore
*
* The handler for this signal should do the opposite of what the
* corresponding handler for [signal@Gtk.Widget:save-state] does.
*
* See [signal@Gtk.Widget:save-state].
*
* Returns: %TRUE to stop stop further handlers from running
*/
widget_signals[RESTORE_STATE] =
g_signal_new (I_("restore-state"),
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GtkWidgetClass, restore_state),
_gtk_boolean_handled_accumulator, NULL,
NULL,
G_TYPE_BOOLEAN, 1,
G_TYPE_VARIANT);
gtk_widget_class_set_css_name (klass, I_("widget"));
gtk_widget_class_set_accessible_role (klass, GTK_ACCESSIBLE_ROLE_WIDGET);
}
@@ -2034,7 +2105,6 @@ _gtk_widget_emulate_press (GtkWidget *widget,
NULL,
gdk_touch_event_get_emulating_pointer (event));
break;
case GDK_BUTTON_PRESS:
case GDK_BUTTON_RELEASE:
press = gdk_button_event_new (GDK_BUTTON_PRESS,
gdk_event_get_surface (event),
@@ -7576,6 +7646,7 @@ gtk_widget_finalize (GObject *object)
g_free (priv->name);
g_free (priv->tooltip_markup);
g_free (priv->tooltip_text);
g_free (priv->save_id);
g_clear_pointer (&priv->transform, gsk_transform_unref);
g_clear_pointer (&priv->allocated_transform, gsk_transform_unref);
@@ -12928,3 +12999,158 @@ gtk_widget_set_active_state (GtkWidget *widget,
gtk_widget_unset_state_flags (widget, GTK_STATE_FLAG_ACTIVE);
}
}
void
gtk_widget_set_save_id (GtkWidget *widget,
const char *id)
{
GtkWidgetPrivate *priv = gtk_widget_get_instance_private (widget);
g_free (priv->save_id);
priv->save_id = g_strdup (id);
g_object_notify_by_pspec (G_OBJECT (widget), widget_props[PROP_SAVE_ID]);
}
const char *
gtk_widget_get_save_id (GtkWidget *widget)
{
GtkWidgetPrivate *priv = gtk_widget_get_instance_private (widget);
return priv->save_id;
}
static void
gtk_widget_collect_child_state (GtkWidget *widget,
GVariantDict *dict)
{
const char *id;
GVariant *state;
id = gtk_widget_get_save_id (widget);
if (!id)
return;
if (g_variant_dict_contains (dict, id))
{
g_warning ("Duplicate save-id %s", id);
return;
}
state = gtk_widget_save_state (widget);
if (state)
g_variant_dict_insert_value (dict, id, state);
}
/**
* gtk_widget_save_state:
* @widget: a `GtkWidget`
*
* Save the state of @widget and its children to a `GVariant`.
*
* In order a widgets state to be saved by this, the widget and
* all its ancestors must have a [property@Gtk.Widget:save-id].
*
* See [signal@Gtk.Widget::save-state] for how to override what
* state is saved.
*
* This function is used by `GtkApplication` to implement automatic
* state saving. It is recommended that you use that functionality.
*
* Returns: (transfer full) (nullable): A `GVariant` with the saved state
*
* Since: 4.4
*/
GVariant *
gtk_widget_save_state (GtkWidget *widget)
{
const char *id;
GVariantDict *dict;
GVariantBuilder data;
gboolean save_children = TRUE;
gboolean ret;
GVariant *v;
id = gtk_widget_get_save_id (widget);
if (id == NULL)
return NULL;
dict = g_variant_dict_new (NULL);
g_signal_emit (widget, widget_signals[SAVE_STATE], 0, dict, &save_children, &ret);
g_variant_builder_init (&data, G_VARIANT_TYPE_VARDICT);
v = g_variant_dict_end (dict);
if (g_variant_n_children (v) > 0)
g_variant_builder_add (&data, "{sv}", "data", v);
else
g_variant_unref (v);
if (save_children)
{
g_variant_dict_init (dict, NULL);
gtk_widget_forall (widget, (GtkCallback) gtk_widget_collect_child_state, dict);
v = g_variant_dict_end (dict);
if (g_variant_n_children (v) > 0)
g_variant_builder_add (&data, "{sv}", "children", v);
else
g_variant_unref (v);
}
g_variant_dict_unref (dict);
v = g_variant_builder_end (&data);
if (g_variant_n_children (v) > 0)
return v;
g_variant_unref (v);
return NULL;
}
static void
gtk_widget_restore_child_state (GtkWidget *widget,
GVariant *data)
{
const char *id;
GVariant *v;
id = gtk_widget_get_save_id (widget);
if (!id)
return;
v = g_variant_lookup_value (data, id, G_VARIANT_TYPE_VARDICT);
if (v)
gtk_widget_restore_state (widget, v);
}
/**
* gtk_widget_restore_state:
* @widget: a `GtkWidget`
* @state: an "a{sv}" `GVariant` as returned by gtk_widget_save_state()
*
* Restores state of @widget and its children.
*
* See [method@Gtk.Widget.save_state] for how to save state.
*
* This function is used by `GtkApplication` to implement automatic
* state restoration. It is recommended that you use that functionality.
*
* Since: 4.4
*/
void
gtk_widget_restore_state (GtkWidget *widget,
GVariant *state)
{
GVariant *data;
gboolean ret;
data = g_variant_lookup_value (state, "data", G_VARIANT_TYPE_VARDICT);
if (data)
g_signal_emit (widget, widget_signals[RESTORE_STATE], 0, data, &ret);
data = g_variant_lookup_value (state, "children", G_VARIANT_TYPE_VARDICT);
if (data)
gtk_widget_forall (widget, (GtkCallback) gtk_widget_restore_child_state, data);
}

View File

@@ -257,11 +257,18 @@ struct _GtkWidgetClass
double x,
double y);
gboolean (* save_state) (GtkWidget *widget,
GVariantDict *dict,
gboolean *save_children);
gboolean (* restore_state) (GtkWidget *widget,
GVariant *data);
/*< private >*/
GtkWidgetClassPrivate *priv;
gpointer padding[8];
gpointer padding[6];
};
@@ -977,6 +984,21 @@ GtkAccessibleRole gtk_widget_class_get_accessible_role (GtkWidgetClass
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GtkWidget, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GtkRequisition, gtk_requisition_free)
GDK_AVAILABLE_IN_4_4
void gtk_widget_set_save_id (GtkWidget *widget,
const char *id);
GDK_AVAILABLE_IN_4_4
const char * gtk_widget_get_save_id (GtkWidget *widget);
GDK_AVAILABLE_IN_4_4
GVariant * gtk_widget_save_state (GtkWidget *widget);
GDK_AVAILABLE_IN_4_4
void gtk_widget_restore_state (GtkWidget *widget,
GVariant *state);
G_END_DECLS
#endif /* __GTK_WIDGET_H__ */

View File

@@ -198,6 +198,9 @@ struct _GtkWidgetPrivate
/* Accessibility */
GtkAccessibleRole accessible_role;
GtkATContext *at_context;
/* State saving */
char *save_id;
};
typedef struct

View File

@@ -708,6 +708,54 @@ gtk_window_get_request_mode (GtkWidget *widget)
return GTK_SIZE_REQUEST_CONSTANT_SIZE;
}
static gboolean
gtk_window_save_state (GtkWidget *widget,
GVariantDict *dict,
gboolean *save_children)
{
GtkWindow *win = GTK_WINDOW (widget);
int width, height;
g_variant_dict_insert (dict, "maximized", "b", gtk_window_is_maximized (win));
g_variant_dict_insert (dict, "fullscreen", "b", gtk_window_is_fullscreen (win));
gtk_window_get_default_size (win, &width, &height);
g_variant_dict_insert (dict, "size", "(ii)", width, height);
*save_children = TRUE;
return TRUE;
}
static gboolean
gtk_window_restore_state (GtkWidget *widget,
GVariant *state)
{
GtkWindow *win = GTK_WINDOW (widget);
gboolean maximized;
gboolean fullscreen;
int width, height;
if (g_variant_lookup (state, "maximized", "b", &maximized))
{
if (maximized)
gtk_window_maximize (win);
}
if (g_variant_lookup (state, "fullscreen", "b", &fullscreen))
{
if (fullscreen)
gtk_window_fullscreen (win);
}
if (g_variant_lookup (state, "size", "(ii)", &width, &height))
{
gtk_window_set_default_size (win, width, height);
}
return TRUE;
}
static void
gtk_window_class_init (GtkWindowClass *klass)
{
@@ -739,6 +787,8 @@ gtk_window_class_init (GtkWindowClass *klass)
widget_class->move_focus = gtk_window_move_focus;
widget_class->measure = gtk_window_measure;
widget_class->css_changed = gtk_window_css_changed;
widget_class->save_state = gtk_window_save_state;
widget_class->restore_state = gtk_window_restore_state;
klass->activate_default = gtk_window_real_activate_default;
klass->activate_focus = gtk_window_real_activate_focus;

View File

@@ -1,5 +1,6 @@
gtk_tests = [
# testname, optional extra sources
['state-saving'],
['testpopup'],
['testupload'],
['testtransform'],

246
tests/state-saving.c Normal file
View File

@@ -0,0 +1,246 @@
/* state-saving.c
* Copyright (C) 2021 Red Hat, Inc
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtk/gtk.h>
static void
quit_cb (GtkWidget *widget,
gpointer data)
{
gboolean *done = data;
*done = TRUE;
g_main_context_wakeup (NULL);
}
static void
save_cb (GtkWidget *window)
{
GVariant *state;
char *s;
GError *error = NULL;
state = gtk_widget_save_state (window);
if (!state)
{
g_print ("no state\n");
return;
}
s = g_variant_print (state, TRUE);
g_print ("%s\n", s);
g_free (s);
if (!g_file_set_contents ("saved-state",
g_variant_get_data (state),
g_variant_get_size (state),
&error))
{
g_error ("Failed to save state: %s", error->message);
g_error_free (error);
}
g_variant_unref (state);
}
static void
restore_cb (GtkWidget *window)
{
char *contents;
gsize len;
GError *error = NULL;
GVariant *state;
if (!g_file_get_contents ("saved-state", &contents, &len, &error))
{
g_print ("Error loading state: %s\n", error->message);
g_error_free (error);
return;
}
state = g_variant_new_from_data (G_VARIANT_TYPE_VARDICT, contents, len, FALSE, NULL, NULL);
gtk_widget_restore_state (window, state);
g_variant_unref (state);
}
static void
add_to_list (GtkWidget *list)
{
GtkWidget *entry;
int i;
char *s;
GtkWidget *w;
entry = gtk_entry_new ();
gtk_widget_set_save_id (entry, "entry");
gtk_list_box_append (GTK_LIST_BOX (list), entry);
for (i = 0, w = gtk_widget_get_first_child (list);
w;
w = gtk_widget_get_next_sibling (w)) i++;
s = g_strdup_printf ("item%d", i);
gtk_widget_set_save_id (gtk_widget_get_parent (entry), s);
g_free (s);
}
static gboolean
save_list (GtkWidget *list,
GVariantDict *dict,
gboolean *save_children,
gpointer data)
{
int n_items;
GtkWidget *w;
for (n_items = 0, w = gtk_widget_get_first_child (list);
w;
w = gtk_widget_get_next_sibling (w)) n_items++;
g_variant_dict_insert (dict, "n-items", "i", n_items);
*save_children = TRUE;
return FALSE;
}
static gboolean
restore_list (GtkWidget *list,
GVariant *data)
{
int n_items;
if (g_variant_lookup (data, "n-items", "i", &n_items))
{
for (int i = 0; i < n_items; i++)
add_to_list (list);
}
return FALSE;
}
int
main (int argc, char *argv[])
{
GtkWidget *window, *box, *hbox, *button, *scale;
GtkWidget *entry, *cc, *sw, *stack, *switcher;
GtkWidget *list;
gboolean done = FALSE;
gtk_init ();
window = gtk_window_new ();
gtk_widget_set_save_id (window, "window");
gtk_window_set_title (GTK_WINDOW (window), "hello world");
gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
g_signal_connect (window, "destroy", G_CALLBACK (quit_cb), &done);
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_save_id (box, "box");
stack = gtk_stack_new ();
gtk_widget_set_save_id (stack, "stack");
switcher = gtk_stack_switcher_new ();
gtk_stack_switcher_set_stack (GTK_STACK_SWITCHER (switcher), GTK_STACK (stack));
gtk_box_append (GTK_BOX (box), switcher);
gtk_box_append (GTK_BOX (box), stack);
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_append (GTK_BOX (box), hbox);
button = gtk_button_new_with_label ("Save");
gtk_widget_set_hexpand (button, TRUE);
g_signal_connect_swapped (button, "clicked", G_CALLBACK (save_cb), window);
gtk_box_append (GTK_BOX (hbox), button);
button = gtk_button_new_with_label ("Restore");
gtk_widget_set_hexpand (button, TRUE);
g_signal_connect_swapped (button, "clicked", G_CALLBACK (restore_cb), window);
gtk_box_append (GTK_BOX (hbox), button);
gtk_window_set_child (GTK_WINDOW (window), box);
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_save_id (box, "box");
scale = gtk_scale_new_with_range (GTK_ORIENTATION_HORIZONTAL, 0, 100, 1);
gtk_widget_set_save_id (scale, "scale");
gtk_box_append (GTK_BOX (box), scale);
entry = gtk_entry_new ();
gtk_widget_set_save_id (entry, "entry");
gtk_box_append (GTK_BOX (box), entry);
cc = gtk_color_chooser_widget_new ();
gtk_widget_set_save_id (cc, "color");
gtk_box_append (GTK_BOX (box), cc);
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_save_id (hbox, "hbox");
gtk_box_append (GTK_BOX (box), hbox);
sw = gtk_switch_new ();
gtk_widget_set_save_id (sw, "switch");
gtk_widget_set_valign (sw, GTK_ALIGN_CENTER);
gtk_box_append (GTK_BOX (hbox), sw);
button = gtk_check_button_new_with_label ("Check");
gtk_widget_set_save_id (button, "check");
gtk_box_append (GTK_BOX (hbox), button);
button = gtk_toggle_button_new_with_label ("Toggle");
gtk_widget_set_save_id (button, "toggle");
gtk_box_append (GTK_BOX (hbox), button);
entry = gtk_spin_button_new_with_range (0, 100, 1);
gtk_widget_set_save_id (entry, "spin");
gtk_box_append (GTK_BOX (hbox), entry);
entry = gtk_password_entry_new ();
gtk_password_entry_set_show_peek_icon (GTK_PASSWORD_ENTRY (entry), TRUE);
gtk_widget_set_save_id (entry, "password");
gtk_box_append (GTK_BOX (box), entry);
gtk_stack_add_titled (GTK_STACK (stack), box, "page1", "Page 1");
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_save_id (box, "box2");
list = gtk_list_box_new ();
gtk_widget_set_save_id (list, "list");
g_signal_connect (list, "save-state", G_CALLBACK (save_list), NULL);
g_signal_connect (list, "restore-state", G_CALLBACK (restore_list), NULL);
button = gtk_button_new_with_label ("Add");
g_signal_connect_swapped (button, "clicked", G_CALLBACK (add_to_list), list);
gtk_box_append (GTK_BOX (box), button);
gtk_box_append (GTK_BOX (box), list);
gtk_stack_add_titled (GTK_STACK (stack), box, "page2", "Page 2");
gtk_widget_show (window);
while (!done)
g_main_context_iteration (NULL, TRUE);
return 0;
}