diff --git a/testsuite/gtk/Makefile.am b/testsuite/gtk/Makefile.am
index a2f6fc690d..822e7b45d2 100644
--- a/testsuite/gtk/Makefile.am
+++ b/testsuite/gtk/Makefile.am
@@ -45,6 +45,7 @@ TEST_PROGS += \
icontheme \
keyhash \
listbox \
+ notify \
no-gtk-init \
object \
objects-finalize \
diff --git a/testsuite/gtk/notify.c b/testsuite/gtk/notify.c
new file mode 100644
index 0000000000..3d6ce1865b
--- /dev/null
+++ b/testsuite/gtk/notify.c
@@ -0,0 +1,671 @@
+/* Gtk+ property notify tests
+ * Copyright (C) 2014 Matthias Clasen
+ *
+ * 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 .
+ */
+
+#include
+#include
+#include
+#include
+
+typedef struct
+{
+ const gchar *name;
+ gint count;
+} NotifyData;
+
+static void
+count_notify (GObject *obj, GParamSpec *pspec, NotifyData *data)
+{
+ if (g_strcmp0 (data->name, pspec->name) == 0)
+ data->count++;
+}
+
+/* Check that we get notifications when properties change.
+ * Also check that we don't emit redundant notifications for
+ * enum, flags, booleans, ints. We allow redundant notifications
+ * for strings, and floats
+ */
+static void
+check_property (GObject *instance, GParamSpec *pspec)
+{
+ if (G_TYPE_IS_ENUM (pspec->value_type))
+ {
+ GEnumClass *class;
+ gint i;
+ NotifyData data;
+ gulong id;
+ gint first;
+ gint value;
+ gint current_count;
+
+ class = g_type_class_ref (pspec->value_type);
+
+ data.name = pspec->name;
+ data.count = 0;
+ id = g_signal_connect (instance, "notify", G_CALLBACK (count_notify), &data);
+
+ g_object_get (instance, pspec->name, &value, NULL);
+ g_object_set (instance, pspec->name, value, NULL);
+
+ g_assert_cmpint (data.count, ==, 0);
+
+ if (class->values[0].value == value)
+ first = 1;
+ else
+ first = 0;
+
+ for (i = first; i < class->n_values; i++)
+ {
+ current_count = data.count + 1;
+ g_object_set (instance, pspec->name, class->values[i].value, NULL);
+ g_assert_cmpint (data.count, ==, current_count);
+
+ if (current_count == 10) /* just test a few */
+ break;
+ }
+
+ g_signal_handler_disconnect (instance, id);
+ g_type_class_unref (class);
+ }
+ else if (G_TYPE_IS_FLAGS (pspec->value_type))
+ {
+ GFlagsClass *class;
+ gint i;
+ NotifyData data;
+ gulong id;
+ guint value;
+ gint current_count;
+
+ class = g_type_class_ref (pspec->value_type);
+
+ data.name = pspec->name;
+ data.count = 0;
+ id = g_signal_connect (instance, "notify", G_CALLBACK (count_notify), &data);
+
+ g_object_get (instance, pspec->name, &value, NULL);
+ g_object_set (instance, pspec->name, value, NULL);
+
+ g_assert_cmpint (data.count, ==, 0);
+
+ for (i = 0; i < class->n_values; i++)
+ {
+ /* some flags have a 'none' member, skip it */
+ if (class->values[i].value == 0)
+ continue;
+
+ if ((value & class->values[i].value) != 0)
+ continue;
+
+ value |= class->values[i].value;
+ current_count = data.count + 1;
+ g_object_set (instance, pspec->name, value, NULL);
+ g_assert_cmpint (data.count, ==, current_count);
+
+ if (current_count == 10) /* just test a few */
+ break;
+ }
+
+ g_signal_handler_disconnect (instance, id);
+ g_type_class_unref (class);
+ }
+ else if (pspec->value_type == G_TYPE_BOOLEAN)
+ {
+ NotifyData data;
+ gboolean value;
+ gulong id;
+
+ data.name = pspec->name;
+ data.count = 0;
+ id = g_signal_connect (instance, "notify", G_CALLBACK (count_notify), &data);
+
+ g_object_get (instance, pspec->name, &value, NULL);
+ g_object_set (instance, pspec->name, value, NULL);
+
+ g_assert_cmpint (data.count, ==, 0);
+
+ g_object_set (instance, pspec->name, 1 - value, NULL);
+
+ g_assert_cmpint (data.count, ==, 1);
+
+ g_signal_handler_disconnect (instance, id);
+ }
+ else if (pspec->value_type == G_TYPE_INT)
+ {
+ GParamSpecInt *p = G_PARAM_SPEC_INT (pspec);
+ gint i;
+ NotifyData data;
+ gulong id;
+ gint value;
+ gint current_count;
+
+ data.name = pspec->name;
+ data.count = 0;
+ id = g_signal_connect (instance, "notify", G_CALLBACK (count_notify), &data);
+
+ g_object_get (instance, pspec->name, &value, NULL);
+ g_object_set (instance, pspec->name, value, NULL);
+
+ g_assert_cmpint (data.count, ==, 0);
+
+ for (i = p->minimum; i <= p->maximum; i++)
+ {
+ g_object_get (instance, pspec->name, &value, NULL);
+ if (value == i)
+ continue;
+
+ current_count = data.count + 1;
+ g_object_set (instance, pspec->name, i, NULL);
+ g_assert_cmpint (data.count, ==, current_count);
+
+ if (current_count == 10) /* just test a few */
+ break;
+ }
+
+ g_signal_handler_disconnect (instance, id);
+ }
+ else if (pspec->value_type == G_TYPE_UINT)
+ {
+ guint i;
+ NotifyData data;
+ gulong id;
+ guint value;
+ gint current_count;
+ guint minimum, maximum;
+
+ if (G_IS_PARAM_SPEC_UINT (pspec))
+ {
+ minimum = G_PARAM_SPEC_UINT (pspec)->minimum;
+ maximum = G_PARAM_SPEC_UINT (pspec)->maximum;
+ }
+ else /* random */
+ {
+ minimum = 0;
+ maximum = 1000;
+ }
+
+ data.name = pspec->name;
+ data.count = 0;
+ id = g_signal_connect (instance, "notify", G_CALLBACK (count_notify), &data);
+
+ g_object_get (instance, pspec->name, &value, NULL);
+ g_object_set (instance, pspec->name, value, NULL);
+
+ g_assert_cmpint (data.count, ==, 0);
+
+ for (i = minimum; i <= maximum; i++)
+ {
+ g_object_get (instance, pspec->name, &value, NULL);
+ if (value == i)
+ continue;
+
+ current_count = data.count + 1;
+ g_object_set (instance, pspec->name, i, NULL);
+ g_assert_cmpint (data.count, ==, current_count);
+
+ if (current_count == 10) /* just test a few */
+ break;
+ }
+
+ g_signal_handler_disconnect (instance, id);
+ }
+ else if (pspec->value_type == G_TYPE_STRING)
+ {
+ NotifyData data;
+ gulong id;
+ gchar *value;
+ gchar *new_value;
+
+ data.name = pspec->name;
+ data.count = 0;
+ id = g_signal_connect (instance, "notify", G_CALLBACK (count_notify), &data);
+
+ g_object_get (instance, pspec->name, &value, NULL);
+ /* don't check redundant notifications */
+
+ new_value = g_strconcat ("(", value, ".", value, ")", NULL);
+
+ g_object_set (instance, pspec->name, new_value, NULL);
+
+ g_assert_cmpint (data.count, ==, 1);
+
+ g_free (value);
+ g_free (new_value);
+
+ g_signal_handler_disconnect (instance, id);
+ }
+ else if (pspec->value_type == G_TYPE_DOUBLE)
+ {
+ GParamSpecDouble *p = G_PARAM_SPEC_DOUBLE (pspec);
+ guint i;
+ NotifyData data;
+ gulong id;
+ gdouble value;
+ gdouble new_value;
+ gint current_count;
+ gdouble delta;
+
+ data.name = pspec->name;
+ data.count = 0;
+ id = g_signal_connect (instance, "notify", G_CALLBACK (count_notify), &data);
+
+ /* don't check redundant notifications */
+ g_object_get (instance, pspec->name, &value, NULL);
+
+ if (p->maximum > 100 || p->minimum < -100)
+ delta = M_PI;
+ else
+ delta = (p->maximum - p->minimum) / 10.0;
+
+ new_value = p->minimum;
+ for (i = 0; i < 10; i++)
+ {
+ new_value += delta;
+
+ if (fabs (value - new_value) < p->epsilon)
+ continue;
+
+ if (new_value > p->maximum)
+ break;
+
+ current_count = data.count + 1;
+ g_object_set (instance, pspec->name, new_value, NULL);
+ g_assert_cmpint (data.count, ==, current_count);
+ }
+
+ g_signal_handler_disconnect (instance, id);
+ }
+ else if (pspec->value_type == G_TYPE_FLOAT)
+ {
+ GParamSpecFloat *p = G_PARAM_SPEC_FLOAT (pspec);
+ guint i;
+ NotifyData data;
+ gulong id;
+ gfloat value;
+ gfloat new_value;
+ gint current_count;
+
+ data.name = pspec->name;
+ data.count = 0;
+ id = g_signal_connect (instance, "notify", G_CALLBACK (count_notify), &data);
+
+ /* don't check redundant notifications */
+ g_object_get (instance, pspec->name, &value, NULL);
+
+ new_value = p->minimum;
+ for (i = 0; i < 10; i++)
+ {
+ if (fabs (value - new_value) < p->epsilon)
+ continue;
+
+ current_count = data.count + 1;
+ new_value += (p->maximum - p->minimum) / 10.0;
+
+ if (new_value > p->maximum)
+ break;
+
+ g_object_set (instance, pspec->name, new_value, NULL);
+ g_assert_cmpint (data.count, ==, current_count);
+ }
+
+ g_signal_handler_disconnect (instance, id);
+ }
+ else
+ {
+ if (g_test_verbose ())
+ g_print ("Skipping property %s.%s of type %s\n", g_type_name (pspec->owner_type), pspec->name, g_type_name (pspec->value_type));
+ }
+}
+
+static void
+test_type (gconstpointer data)
+{
+ GObjectClass *klass;
+ GObject *instance;
+ GParamSpec **pspecs;
+ guint n_pspecs, i;
+ GType type;
+ GdkDisplay *display;
+
+ type = * (GType *) data;
+
+ display = gdk_display_get_default ();
+
+ if (!G_TYPE_IS_CLASSED (type))
+ return;
+
+ if (G_TYPE_IS_ABSTRACT (type))
+ return;
+
+ if (!g_type_is_a (type, G_TYPE_OBJECT))
+ return;
+
+ /* non-GTK+ */
+ if (g_str_equal (g_type_name (type), "AtkObject") ||
+ g_str_equal (g_type_name (type), "GdkPixbufSimpleAnim"))
+ return;
+
+ /* Deprecated, not getting fixed */
+ if (g_str_equal (g_type_name (type), "GtkColorSelection") ||
+ g_str_equal (g_type_name (type), "GtkHandleBox") ||
+ g_str_equal (g_type_name (type), "GtkHPaned") ||
+ g_str_equal (g_type_name (type), "GtkVPaned") ||
+ g_str_equal (g_type_name (type), "GtkHScale") ||
+ g_str_equal (g_type_name (type), "GtkVScale") ||
+ g_str_equal (g_type_name (type), "GtkHScrollbar") ||
+ g_str_equal (g_type_name (type), "GtkVScrollbar") ||
+ g_str_equal (g_type_name (type), "GtkHSeparator") ||
+ g_str_equal (g_type_name (type), "GtkVSeparator") ||
+ g_str_equal (g_type_name (type), "GtkHBox") ||
+ g_str_equal (g_type_name (type), "GtkVBox") ||
+ g_str_equal (g_type_name (type), "GtkArrow") ||
+ g_str_equal (g_type_name (type), "GtkNumerableIcon") ||
+ g_str_equal (g_type_name (type), "GtkRadioAction") ||
+ g_str_equal (g_type_name (type), "GtkToggleAction") ||
+ g_str_equal (g_type_name (type), "GtkTable") ||
+ g_str_equal (g_type_name (type), "GtkUIManager") ||
+ g_str_equal (g_type_name (type), "GtkImageMenuItem"))
+ return;
+
+ /* These can't be freely constructed/destroyed */
+ if (g_type_is_a (type, GTK_TYPE_APPLICATION) ||
+ g_type_is_a (type, GDK_TYPE_PIXBUF_LOADER) ||
+#ifdef G_OS_UNIX
+ g_type_is_a (type, GTK_TYPE_PRINT_JOB) ||
+#endif
+ g_type_is_a (type, gdk_pixbuf_simple_anim_iter_get_type ()) ||
+ g_str_equal (g_type_name (type), "GdkX11DeviceManagerXI2") ||
+ g_str_equal (g_type_name (type), "GdkX11Display") ||
+ g_str_equal (g_type_name (type), "GdkX11DisplayManager") ||
+ g_str_equal (g_type_name (type), "GdkX11Screen"))
+ return;
+
+ /* This throws a critical when the connection is dropped */
+ if (g_type_is_a (type, GTK_TYPE_APP_CHOOSER_DIALOG))
+ return;
+
+ /* These leak their GDBusConnections */
+ if (g_type_is_a (type, GTK_TYPE_FILE_CHOOSER_BUTTON) ||
+ g_type_is_a (type, GTK_TYPE_FILE_CHOOSER_DIALOG) ||
+ g_type_is_a (type, GTK_TYPE_FILE_CHOOSER_WIDGET) ||
+ g_type_is_a (type, GTK_TYPE_PLACES_SIDEBAR))
+ return;
+
+ klass = g_type_class_ref (type);
+
+ if (g_type_is_a (type, GTK_TYPE_SETTINGS))
+ instance = g_object_ref (gtk_settings_get_default ());
+ else if (g_type_is_a (type, GDK_TYPE_WINDOW))
+ {
+ GdkWindowAttr attributes;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.window_type = GDK_WINDOW_TEMP;
+ attributes.event_mask = 0;
+ attributes.width = 100;
+ attributes.height = 100;
+ instance = g_object_ref (gdk_window_new (NULL, &attributes, 0));
+ }
+ else if (g_str_equal (g_type_name (type), "GdkX11Cursor"))
+ instance = g_object_new (type, "display", display, NULL);
+ else
+ instance = g_object_new (type, NULL);
+
+ if (g_type_is_a (type, G_TYPE_INITIALLY_UNOWNED))
+ g_object_ref_sink (instance);
+
+ pspecs = g_object_class_list_properties (klass, &n_pspecs);
+ for (i = 0; i < n_pspecs; ++i)
+ {
+ GParamSpec *pspec = pspecs[i];
+
+ if ((pspec->flags & G_PARAM_READABLE) == 0)
+ continue;
+
+ if ((pspec->flags & G_PARAM_WRITABLE) == 0)
+ continue;
+
+ if ((pspec->flags & G_PARAM_CONSTRUCT_ONLY) != 0)
+ continue;
+
+ /* non-GTK+ */
+ if (g_str_equal (g_type_name (pspec->owner_type), "AtkObject") ||
+ g_str_equal (g_type_name (pspec->owner_type), "GdkPixbufSimpleAnim") ||
+ g_str_equal (g_type_name (pspec->owner_type), "GMountOperation"))
+ continue;
+
+ /* set properties are best skipped */
+ if (pspec->value_type == G_TYPE_BOOLEAN &&
+ g_str_has_suffix (pspec->name, "-set"))
+ continue;
+
+ /* These are special */
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_WIDGET) &&
+ (g_str_equal (pspec->name, "has-focus") ||
+ g_str_equal (pspec->name, "has-default") ||
+ g_str_equal (pspec->name, "is-focus") ||
+ g_str_equal (pspec->name, "margin") ||
+ g_str_equal (pspec->name, "hexpand") ||
+ g_str_equal (pspec->name, "vexpand") ||
+ g_str_equal (pspec->name, "expand")
+ ))
+ continue;
+
+ if (type == GTK_TYPE_SETTINGS)
+ continue;
+
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_MENU_ITEM) &&
+ g_str_equal (pspec->name, "accel-path"))
+ continue;
+
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_MENU) &&
+ (g_str_equal (pspec->name, "accel-path") ||
+ g_str_equal (pspec->name, "active")))
+ continue;
+
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_CHECK_MENU_ITEM) &&
+ g_str_equal (pspec->name, "active"))
+ continue;
+
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_COLOR_CHOOSER) &&
+ g_str_equal (pspec->name, "show-editor"))
+ continue;
+
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_NOTEBOOK) &&
+ g_str_equal (pspec->name, "page"))
+ continue;
+
+ /* Not supported in subclass */
+ if (g_str_equal (g_type_name (type), "GtkRecentAction") &&
+ g_str_equal (pspec->name, "select-multiple"))
+ continue;
+
+ if (g_str_equal (g_type_name (type), "GtkRecentChooserMenu") &&
+ g_str_equal (pspec->name, "select-multiple"))
+ continue;
+
+ /* Really a bug in the way GtkButton and its subclasses interact:
+ * setting label etc on a subclass destroys the content, breaking
+ * e.g. GtkColorButton pretty badly
+ */
+ if (type == GTK_TYPE_COLOR_BUTTON && pspec->owner_type == GTK_TYPE_BUTTON)
+ continue;
+
+ /* GdkOffscreenWindow is missing many implementations */
+ if (type == GTK_TYPE_OFFSCREEN_WINDOW)
+ continue;
+
+ /* Too many special cases involving -set properties */
+ if (g_str_equal (g_type_name (pspec->owner_type), "GtkCellRendererText") ||
+ g_str_equal (g_type_name (pspec->owner_type), "GtkTextTag"))
+ continue;
+
+ /* Most things assume a model is set */
+ if (g_str_equal (g_type_name (pspec->owner_type), "GtkComboBox"))
+ continue;
+
+ /* Deprecated, not getting fixed */
+ if (g_str_equal (g_type_name (pspec->owner_type), "GtkActivatable") ||
+ g_str_equal (g_type_name (pspec->owner_type), "GtkActionGroup") ||
+ g_str_equal (g_type_name (pspec->owner_type), "GtkAction"))
+ continue;
+
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_CONTAINER) &&
+ g_str_equal (pspec->name, "resize-mode"))
+ continue;
+
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_COLOR_BUTTON) &&
+ g_str_equal (pspec->name, "alpha"))
+ continue;
+
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_CELL_RENDERER_PIXBUF) &&
+ (g_str_equal (pspec->name, "follow-state") ||
+ g_str_equal (pspec->name, "stock-id") ||
+ g_str_equal (pspec->name, "stock-size") ||
+ g_str_equal (pspec->name, "stock-detail")))
+ continue;
+
+ if (g_str_equal (g_type_name (pspec->owner_type), "GtkArrow") ||
+ g_str_equal (g_type_name (pspec->owner_type), "GtkAlignment") ||
+ g_str_equal (g_type_name (pspec->owner_type), "GtkMisc"))
+ continue;
+
+ if (g_type_is_a (pspec->owner_type, GTK_TYPE_MENU) &&
+ g_str_equal (pspec->name, "tearoff-state"))
+ continue;
+
+ /* Can only be set on window widgets */
+ if (pspec->owner_type == GTK_TYPE_WIDGET &&
+ g_str_equal (pspec->name, "events"))
+ continue;
+
+ /* Can only be set on unmapped windows */
+ if (pspec->owner_type == GTK_TYPE_WINDOW &&
+ g_str_equal (pspec->name, "type-hint"))
+ continue;
+
+ /* Special restrictions on allowed values */
+ if (pspec->owner_type == GTK_TYPE_COMBO_BOX &&
+ (g_str_equal (pspec->name, "row-span-column") ||
+ g_str_equal (pspec->name, "column-span-column") ||
+ g_str_equal (pspec->name, "id-column") ||
+ g_str_equal (pspec->name, "active-id") ||
+ g_str_equal (pspec->name, "entry-text-column")))
+ continue;
+
+ if (pspec->owner_type == GTK_TYPE_ENTRY_COMPLETION &&
+ g_str_equal (pspec->name, "text-column"))
+ continue;
+
+ if (pspec->owner_type == GTK_TYPE_PRINT_OPERATION &&
+ (g_str_equal (pspec->name, "current-page") ||
+ g_str_equal (pspec->name, "n-pages")))
+ continue;
+
+ if (pspec->owner_type == GTK_TYPE_RANGE &&
+ g_str_equal (pspec->name, "fill-level"))
+ continue;
+
+ if (pspec->owner_type == GTK_TYPE_SPIN_BUTTON &&
+ g_str_equal (pspec->name, "value"))
+ continue;
+
+ if (pspec->owner_type == GTK_TYPE_STACK &&
+ g_str_equal (pspec->name, "visible-child-name"))
+ continue;
+
+ if (pspec->owner_type == GTK_TYPE_TEXT_VIEW &&
+ g_str_equal (pspec->name, "im-module"))
+ continue;
+
+ if (pspec->owner_type == GTK_TYPE_TOOLBAR &&
+ g_str_equal (pspec->name, "icon-size"))
+ continue;
+
+ if (pspec->owner_type == GTK_TYPE_TREE_SELECTION &&
+ g_str_equal (pspec->name, "mode")) /* requires a treeview */
+ continue;
+
+ if (pspec->owner_type == GTK_TYPE_TREE_VIEW &&
+ g_str_equal (pspec->name, "headers-clickable")) /* requires columns */
+ continue;
+
+ /* This one has a special-purpose default value */
+ if (g_type_is_a (type, GTK_TYPE_DIALOG) &&
+ g_str_equal (pspec->name, "use-header-bar"))
+ continue;
+
+ if (g_type_is_a (type, GTK_TYPE_ASSISTANT) &&
+ g_str_equal (pspec->name, "use-header-bar"))
+ continue;
+
+ if (g_test_verbose ())
+ g_print ("Property %s.%s\n", g_type_name (pspec->owner_type), pspec->name);
+
+ check_property (instance, pspec);
+ }
+ g_free (pspecs);
+
+ if (g_type_is_a (type, GDK_TYPE_WINDOW))
+ gdk_window_destroy (GDK_WINDOW (instance));
+ else
+ g_object_unref (instance);
+
+ g_type_class_unref (klass);
+}
+
+int
+main (int argc, char **argv)
+{
+ const GType *otypes;
+ guint i;
+ gchar *schema_dir;
+ GTestDBus *bus;
+ gint result;
+
+ /* These must be set before before gtk_test_init */
+ g_setenv ("GIO_USE_VFS", "local", TRUE);
+ g_setenv ("GSETTINGS_BACKEND", "memory", TRUE);
+
+ gtk_test_init (&argc, &argv);
+ gtk_test_register_all_types();
+
+ /* g_test_build_filename must be called after gtk_test_init */
+ schema_dir = g_test_build_filename (G_TEST_BUILT, "", NULL);
+ g_setenv ("GSETTINGS_SCHEMA_DIR", schema_dir, TRUE);
+
+ /* Create one test bus for all tests, as we have a lot of very small
+ * and quick tests.
+ */
+ bus = g_test_dbus_new (G_TEST_DBUS_NONE);
+ g_test_dbus_up (bus);
+
+ otypes = gtk_test_list_all_types (NULL);
+ for (i = 0; otypes[i]; i++)
+ {
+ gchar *testname;
+
+ testname = g_strdup_printf ("/Notification/%s", g_type_name (otypes[i]));
+ g_test_add_data_func (testname, &otypes[i], test_type);
+ g_free (testname);
+ }
+
+ result = g_test_run ();
+
+ g_test_dbus_down (bus);
+ g_object_unref (bus);
+ g_free (schema_dir);
+
+ return result;
+}