summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenjamin Otte <otte@redhat.com>2018-08-20 06:42:22 +0200
committerMatthias Clasen <mclasen@redhat.com>2020-03-22 11:14:07 -0400
commitfad0e1c5c5d38d05d72fc022b96916e1b27736fd (patch)
tree1d0cd756cbef0974e7431c9262140537d972a27a
parent59bf971e925b6bf8b33c01025a3e9f4d006f4949 (diff)
downloadgtk+-fad0e1c5c5d38d05d72fc022b96916e1b27736fd.tar.gz
shortcuts: Mananage managed shortcuts with a custom model
Reduce the amount of special casing by using a list model for global and managed shortcuts, too. This way, the ListModel API will work for the ShortcutController in the GtkShortcutManager and GtkRoot. The only special case remaining is shortcut activation, which needs to pass the right widget to the controller in the global/managed case.
-rw-r--r--gtk/gtkconcatmodel.c254
-rw-r--r--gtk/gtkconcatmodelprivate.h52
-rw-r--r--gtk/gtkshortcutcontroller.c41
-rw-r--r--gtk/gtkshortcutcontrollerprivate.h3
-rw-r--r--gtk/gtkshortcutmanager.c41
-rw-r--r--gtk/gtkwidget.c13
-rw-r--r--gtk/meson.build1
-rw-r--r--testsuite/gtk/concatmodel.c372
-rw-r--r--testsuite/gtk/meson.build1
9 files changed, 732 insertions, 46 deletions
diff --git a/gtk/gtkconcatmodel.c b/gtk/gtkconcatmodel.c
new file mode 100644
index 0000000000..d96290e6f3
--- /dev/null
+++ b/gtk/gtkconcatmodel.c
@@ -0,0 +1,254 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+
+/*
+ * SECTION:gtkconcatmodel
+ * @Short_description: concatenation list model
+ * @Title: GtkConcatModel
+ * @See_also: #GListModel
+ *
+ * #GtkConcatModel is a #GListModel implementation that takes a list of list models
+ * and presents them as one concatenated list.
+ *
+ * Node that all the types of the passed in list models must match the concat model's
+ * type. If they are not, you must use a common ancestor type for the #GtkConcatModel,
+ * %G_TYPE_OBJECT being the ultimate option.
+ **/
+
+#include "config.h"
+
+#include "gtkconcatmodelprivate.h"
+
+struct _GtkConcatModel
+{
+ GObject parent_instance;
+
+ GType item_type;
+ guint n_items;
+
+ GList *models;
+};
+
+struct _GtkConcatModelClass
+{
+ GObjectClass parent_class;
+};
+
+static GType
+gtk_concat_model_list_model_get_item_type (GListModel *list)
+{
+ GtkConcatModel *self = GTK_CONCAT_MODEL (list);
+
+ return self->item_type;
+}
+
+static guint
+gtk_concat_model_list_model_get_n_items (GListModel *list)
+{
+ GtkConcatModel *self = GTK_CONCAT_MODEL (list);
+
+ return self->n_items;
+}
+
+static gpointer
+gtk_concat_model_list_model_get_item (GListModel *list,
+ guint position)
+{
+ GtkConcatModel *self = GTK_CONCAT_MODEL (list);
+ GList *l;
+
+ /* FIXME: Use an RBTree to make this O(log N) */
+ for (l = self->models; l; l = l->next)
+ {
+ guint n = g_list_model_get_n_items (l->data);
+
+ if (position < n)
+ return g_list_model_get_item (l->data, position);
+
+ position -= n;
+ }
+
+ return NULL;
+}
+
+static void
+gtk_concat_model_list_model_init (GListModelInterface *iface)
+{
+ iface->get_item_type = gtk_concat_model_list_model_get_item_type;
+ iface->get_n_items = gtk_concat_model_list_model_get_n_items;
+ iface->get_item = gtk_concat_model_list_model_get_item;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GtkConcatModel, gtk_concat_model,
+ G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_concat_model_list_model_init))
+
+static void
+gtk_concat_model_items_changed (GListModel *model,
+ guint position,
+ guint removed,
+ guint added,
+ GtkConcatModel *self)
+{
+ GList *l;
+
+ for (l = self->models; l; l = l->next)
+ {
+ if (l->data == model)
+ break;
+
+ position += g_list_model_get_n_items (l->data);
+ }
+
+ self->n_items -= removed;
+ self->n_items += added;
+
+ g_list_model_items_changed (G_LIST_MODEL (self),
+ position,
+ removed,
+ added);
+}
+
+static void
+gtk_concat_model_remove_internal (GtkConcatModel *self,
+ GListModel *model,
+ gboolean emit_signals)
+{
+ guint n_items, position;
+ GList *l;
+
+ position = 0;
+ for (l = self->models; l; l = l->next)
+ {
+ if (l->data == model)
+ break;
+
+ position += g_list_model_get_n_items (l->data);
+ }
+
+ g_return_if_fail (l != NULL);
+
+ self->models = g_list_delete_link (self->models, l);
+ n_items = g_list_model_get_n_items (model);
+ self->n_items -= n_items;
+ g_signal_handlers_disconnect_by_func (model, gtk_concat_model_items_changed, self);
+ g_object_unref (model);
+
+ if (n_items && emit_signals)
+ g_list_model_items_changed (G_LIST_MODEL (self),
+ position,
+ n_items,
+ 0);
+}
+
+static void
+gtk_concat_model_dispose (GObject *object)
+{
+ GtkConcatModel *self = GTK_CONCAT_MODEL (object);
+
+ /* FIXME: Make this work without signal emissions */
+ while (self->models)
+ gtk_concat_model_remove_internal (self, self->models->data, FALSE);
+
+ G_OBJECT_CLASS (gtk_concat_model_parent_class)->dispose (object);
+}
+
+static void
+gtk_concat_model_class_init (GtkConcatModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = gtk_concat_model_dispose;
+}
+
+static void
+gtk_concat_model_init (GtkConcatModel *self)
+{
+}
+
+GtkConcatModel *
+gtk_concat_model_new (GType item_type)
+{
+ GtkConcatModel *self;
+
+ g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL);
+
+ self = g_object_new (GTK_TYPE_CONCAT_MODEL, NULL);
+
+ self->item_type = item_type;
+
+ return self;
+}
+
+void
+gtk_concat_model_append (GtkConcatModel *self,
+ GListModel *model)
+{
+ guint n_items;
+
+ g_return_if_fail (GTK_IS_CONCAT_MODEL (self));
+ g_return_if_fail (G_IS_LIST_MODEL (model));
+ g_return_if_fail (g_type_is_a (g_list_model_get_item_type (model), self->item_type));
+
+ g_object_ref (model);
+ g_signal_connect (model, "items-changed", G_CALLBACK (gtk_concat_model_items_changed), self);
+ self->models = g_list_append (self->models, model);
+ n_items = g_list_model_get_n_items (model);
+ self->n_items += n_items;
+
+ if (n_items)
+ g_list_model_items_changed (G_LIST_MODEL (self),
+ self->n_items - n_items,
+ 0,
+ n_items);
+}
+
+void
+gtk_concat_model_remove (GtkConcatModel *self,
+ GListModel *model)
+{
+ g_return_if_fail (GTK_IS_CONCAT_MODEL (self));
+ g_return_if_fail (G_IS_LIST_MODEL (model));
+
+ gtk_concat_model_remove_internal (self, model, TRUE);
+}
+
+GListModel *
+gtk_concat_model_get_model_for_item (GtkConcatModel *self,
+ guint position)
+{
+ GList *l;
+
+ g_return_val_if_fail (GTK_IS_CONCAT_MODEL (self), NULL);
+
+ /* FIXME: Use an RBTree to make this O(log N) */
+ for (l = self->models; l; l = l->next)
+ {
+ guint n = g_list_model_get_n_items (l->data);
+
+ if (position < n)
+ return l->data;
+
+ position -= n;
+ }
+
+ return NULL;
+}
+
diff --git a/gtk/gtkconcatmodelprivate.h b/gtk/gtkconcatmodelprivate.h
new file mode 100644
index 0000000000..005a9f3da8
--- /dev/null
+++ b/gtk/gtkconcatmodelprivate.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+
+#ifndef __GTK_CONCAT_MODEL_H__
+#define __GTK_CONCAT_MODEL_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_CONCAT_MODEL (gtk_concat_model_get_type ())
+#define GTK_CONCAT_MODEL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GTK_TYPE_CONCAT_MODEL, GtkConcatModel))
+#define GTK_CONCAT_MODEL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GTK_TYPE_CONCAT_MODEL, GtkConcatModelClass))
+#define GTK_IS_CONCAT_MODEL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GTK_TYPE_CONCAT_MODEL))
+#define GTK_IS_CONCAT_MODEL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GTK_TYPE_CONCAT_MODEL))
+#define GTK_CONCAT_MODEL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GTK_TYPE_CONCAT_MODEL, GtkConcatModelClass))
+
+typedef struct _GtkConcatModel GtkConcatModel;
+typedef struct _GtkConcatModelClass GtkConcatModelClass;
+
+GType gtk_concat_model_get_type (void) G_GNUC_CONST;
+
+GtkConcatModel * gtk_concat_model_new (GType item_type);
+
+void gtk_concat_model_append (GtkConcatModel *self,
+ GListModel *model);
+void gtk_concat_model_remove (GtkConcatModel *self,
+ GListModel *model);
+
+GListModel * gtk_concat_model_get_model_for_item (GtkConcatModel *self,
+ guint position);
+
+G_END_DECLS
+
+#endif /* __GTK_CONCAT_MODEL_H__ */
diff --git a/gtk/gtkshortcutcontroller.c b/gtk/gtkshortcutcontroller.c
index f2afe0354c..045e60df5a 100644
--- a/gtk/gtkshortcutcontroller.c
+++ b/gtk/gtkshortcutcontroller.c
@@ -34,6 +34,7 @@
#include "gtkshortcutcontrollerprivate.h"
+#include "gtkconcatmodelprivate.h"
#include "gtkeventcontrollerprivate.h"
#include "gtkintl.h"
#include "gtkshortcut.h"
@@ -53,7 +54,6 @@ struct _GtkShortcutController
GdkModifierType mnemonics_modifiers;
guint custom_shortcuts : 1;
- guint run_managed : 1;
};
struct _GtkShortcutControllerClass
@@ -212,15 +212,27 @@ gtk_shortcut_controller_finalize (GObject *object)
static gboolean
gtk_shortcut_controller_trigger_shortcut (GtkShortcutController *self,
GtkShortcut *shortcut,
+ guint position,
GdkEvent *event,
gboolean enable_mnemonics)
{
+ GtkWidget *widget;
+
if (!gtk_shortcut_trigger_trigger (gtk_shortcut_get_trigger (shortcut), event, enable_mnemonics))
return FALSE;
+ widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (self));
+ if (!self->custom_shortcuts &&
+ GTK_IS_CONCAT_MODEL (self->shortcuts))
+ {
+ GListModel *model = gtk_concat_model_get_model_for_item (GTK_CONCAT_MODEL (self->shortcuts), position);
+ if (GTK_IS_SHORTCUT_CONTROLLER (model))
+ widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (model));
+ }
+
return gtk_shortcut_action_activate (gtk_shortcut_get_action (shortcut),
GTK_SHORTCUT_ACTION_EXCLUSIVE, /* FIXME */
- gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (self)),
+ widget,
gtk_shortcut_get_arguments (shortcut));
}
@@ -232,34 +244,18 @@ gtk_shortcut_controller_run_controllers (GtkEventController *controller,
gboolean enable_mnemonics)
{
GtkShortcutController *self = GTK_SHORTCUT_CONTROLLER (controller);
- GtkWidget *widget;
- const GSList *l;
guint i;
for (i = 0; i < g_list_model_get_n_items (self->shortcuts); i++)
{
if (gtk_shortcut_controller_trigger_shortcut (self,
g_list_model_get_item (self->shortcuts, i),
+ i,
event,
enable_mnemonics))
return TRUE;
}
- if (self->run_managed)
- {
- GtkPropagationPhase current_phase = gtk_event_controller_get_propagation_phase (controller);
- widget = gtk_event_controller_get_widget (controller);
-
- for (l = g_object_get_data (G_OBJECT (widget), "gtk-shortcut-controllers"); l; l = l->next)
- {
- if (gtk_event_controller_get_propagation_phase (l->data) != current_phase)
- continue;
-
- if (gtk_shortcut_controller_run_controllers (l->data, event, x, y, enable_mnemonics))
- return TRUE;
- }
- }
-
return FALSE;
}
@@ -483,13 +479,6 @@ gtk_shortcut_controller_new_for_model (GListModel *model)
NULL);
}
-void
-gtk_shortcut_controller_set_run_managed (GtkShortcutController *controller,
- gboolean run_managed)
-{
- controller->run_managed = run_managed;
-}
-
/**
* gtk_shortcut_controller_add_shortcut:
* @self: the controller
diff --git a/gtk/gtkshortcutcontrollerprivate.h b/gtk/gtkshortcutcontrollerprivate.h
index a7086cd678..db1b52cecc 100644
--- a/gtk/gtkshortcutcontrollerprivate.h
+++ b/gtk/gtkshortcutcontrollerprivate.h
@@ -22,9 +22,6 @@
#include "gtkshortcutcontroller.h"
-void gtk_shortcut_controller_set_run_managed (GtkShortcutController *controller,
- gboolean run_managed);
-
void gtk_shortcut_controller_root (GtkShortcutController *controller);
void gtk_shortcut_controller_unroot (GtkShortcutController *controller);
diff --git a/gtk/gtkshortcutmanager.c b/gtk/gtkshortcutmanager.c
index 6853db9ce5..db97448b67 100644
--- a/gtk/gtkshortcutmanager.c
+++ b/gtk/gtkshortcutmanager.c
@@ -20,6 +20,7 @@
#include "config.h"
#include "gtkshortcutmanager.h"
+#include "gtkconcatmodelprivate.h"
/**
* SECTION:gtkshortcutmanager
@@ -29,36 +30,50 @@
* The GtkShortcutManager interface is used to implement
* shortcut scopes.
*/
+
G_DEFINE_INTERFACE (GtkShortcutManager, gtk_shortcut_manager, G_TYPE_OBJECT)
-static void
-complain_if_reached (gpointer should_be_gone)
+static GtkConcatModel *
+gtk_shortcut_manager_get_model (GtkShortcutManager *self,
+ GtkPropagationPhase phase)
{
- g_critical ("Shortcut controllers failed to clean up.");
+ switch (phase)
+ {
+ case GTK_PHASE_CAPTURE:
+ return g_object_get_data (G_OBJECT (self), "gtk-shortcut-manager-capture");
+ case GTK_PHASE_BUBBLE:
+ return g_object_get_data (G_OBJECT (self), "gtk-shortcut-manager-bubble");
+ case GTK_PHASE_NONE:
+ case GTK_PHASE_TARGET:
+ return NULL;
+ default:
+ g_assert_not_reached ();
+ return NULL;
+ }
}
static void
gtk_shortcut_manager_default_add_controller (GtkShortcutManager *self,
GtkShortcutController *controller)
{
- GSList *controllers;
+ GtkConcatModel *model;
- controllers = g_object_steal_data (G_OBJECT (self), "gtk-shortcut-controllers");
- controllers = g_slist_prepend (controllers, g_object_ref (controller));
- g_object_set_data_full (G_OBJECT (self), "gtk-shortcut-controllers", controllers, complain_if_reached);
+ model = gtk_shortcut_manager_get_model (self,
+ gtk_event_controller_get_propagation_phase (GTK_EVENT_CONTROLLER (controller)));
+ if (model)
+ gtk_concat_model_append (model, G_LIST_MODEL (controller));
}
static void
gtk_shortcut_manager_default_remove_controller (GtkShortcutManager *self,
GtkShortcutController *controller)
{
- GSList *controllers;
+ GtkConcatModel *model;
- controllers = g_object_steal_data (G_OBJECT (self), "gtk-shortcut-controllers");
- controllers = g_slist_remove (controllers, controller);
- if (controllers)
- g_object_set_data_full (G_OBJECT (self), "gtk-shortcut-controllers", controllers, complain_if_reached);
- g_object_unref (controller);
+ model = gtk_shortcut_manager_get_model (self,
+ gtk_event_controller_get_propagation_phase (GTK_EVENT_CONTROLLER (controller)));
+ if (model)
+ gtk_concat_model_remove (model, G_LIST_MODEL (controller));
}
static void
diff --git a/gtk/gtkwidget.c b/gtk/gtkwidget.c
index 71d31fda46..4be35728df 100644
--- a/gtk/gtkwidget.c
+++ b/gtk/gtkwidget.c
@@ -31,6 +31,7 @@
#include "gtkapplicationprivate.h"
#include "gtkbuildable.h"
#include "gtkbuilderprivate.h"
+#include "gtkconcatmodelprivate.h"
#include "gtkcontainerprivate.h"
#include "gtkcssboxesprivate.h"
#include "gtkcssfiltervalueprivate.h"
@@ -2462,13 +2463,17 @@ gtk_widget_init (GTypeInstance *instance, gpointer g_class)
if (g_type_is_a (G_TYPE_FROM_CLASS (g_class), GTK_TYPE_SHORTCUT_MANAGER))
{
- controller = gtk_shortcut_controller_new ();
- gtk_shortcut_controller_set_run_managed (GTK_SHORTCUT_CONTROLLER (controller), TRUE);
+ GtkConcatModel *model;
+
+ model = gtk_concat_model_new (GTK_TYPE_SHORTCUT);
+ g_object_set_data_full (G_OBJECT (widget), "gtk-shortcut-manager-bubble", model, g_object_unref);
+ controller = gtk_shortcut_controller_new_for_model (G_LIST_MODEL (model));
gtk_widget_add_controller (widget, controller);
- controller = gtk_shortcut_controller_new ();
+ model = gtk_concat_model_new (GTK_TYPE_SHORTCUT);
+ g_object_set_data_full (G_OBJECT (widget), "gtk-shortcut-manager-capture", model, g_object_unref);
+ controller = gtk_shortcut_controller_new_for_model (G_LIST_MODEL (model));
gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_CAPTURE);
- gtk_shortcut_controller_set_run_managed (GTK_SHORTCUT_CONTROLLER (controller), TRUE);
gtk_widget_add_controller (widget, controller);
}
diff --git a/gtk/meson.build b/gtk/meson.build
index e85dceac45..095b71158d 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -199,6 +199,7 @@ gtk_public_sources = files([
'gtkcombobox.c',
'gtkcomboboxtext.c',
'gtkcomposetable.c',
+ 'gtkconcatmodel.c',
'gtkconstraintguide.c',
'gtkconstraintlayout.c',
'gtkconstraint.c',
diff --git a/testsuite/gtk/concatmodel.c b/testsuite/gtk/concatmodel.c
new file mode 100644
index 0000000000..3e6292b536
--- /dev/null
+++ b/testsuite/gtk/concatmodel.c
@@ -0,0 +1,372 @@
+/* GtkRBTree tests.
+ *
+ * Copyright (C) 2011, Red Hat, Inc.
+ * Authors: Benjamin Otte <otte@gnome.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <locale.h>
+
+#include "../../gtk/gtkconcatmodelprivate.h"
+
+/* _gtk_rbtree_test */
+
+static GQuark number_quark;
+static GQuark changes_quark;
+
+static guint
+get (GListModel *model,
+ guint position)
+{
+ GObject *object = g_list_model_get_item (model, position);
+ g_assert (object != NULL);
+ return GPOINTER_TO_UINT (g_object_get_qdata (object, number_quark));
+}
+
+static char *
+model_to_string (GListModel *model)
+{
+ GString *string = g_string_new (NULL);
+ guint i;
+
+ for (i = 0; i < g_list_model_get_n_items (model); i++)
+ {
+ if (i > 0)
+ g_string_append (string, " ");
+ g_string_append_printf (string, "%u", get (model, i));
+ }
+
+ return g_string_free (string, FALSE);
+}
+
+static void
+add (GListStore *store,
+ guint number)
+{
+ GObject *object;
+
+ /* o cannot be differentiated from NULL, so don't use it */
+ g_assert (number != 0);
+
+ object = g_object_new (G_TYPE_OBJECT, NULL);
+ g_object_set_qdata (object, number_quark, GUINT_TO_POINTER (number));
+ g_list_store_append (store, object);
+ g_object_unref (object);
+}
+
+static void
+remove (GListStore *store,
+ guint position)
+{
+ g_list_store_remove (store, position);
+}
+
+#define assert_model(model, expected) G_STMT_START{ \
+ char *s = model_to_string (G_LIST_MODEL (model)); \
+ if (!g_str_equal (s, expected)) \
+ g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+ #model " == " #expected, s, "==", expected); \
+ g_free (s); \
+}G_STMT_END
+
+#define assert_changes(model, expected) G_STMT_START{ \
+ GString *changes = g_object_get_qdata (G_OBJECT (model), changes_quark); \
+ if (!g_str_equal (changes->str, expected)) \
+ g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+ #model " == " #expected, changes->str, "==", expected); \
+ g_string_set_size (changes, 0); \
+}G_STMT_END
+
+static GListStore *
+new_empty_store (void)
+{
+ return g_list_store_new (G_TYPE_OBJECT);
+}
+
+static GListStore *
+new_store (guint start,
+ guint end)
+{
+ GListStore *store = new_empty_store ();
+ guint i;
+
+ for (i = start; i <= end; i++)
+ add (store, i);
+
+ return store;
+}
+
+static void
+items_changed (GListModel *model,
+ guint position,
+ guint removed,
+ guint added,
+ GString *changes)
+{
+ g_assert (removed != 0 || added != 0);
+
+ if (changes->len)
+ g_string_append (changes, ", ");
+
+ if (removed == 1 && added == 0)
+ {
+ g_string_append_printf (changes, "-%u", position);
+ }
+ else if (removed == 0 && added == 1)
+ {
+ g_string_append_printf (changes, "+%u", position);
+ }
+ else
+ {
+ g_string_append_printf (changes, "%u", position);
+ if (removed > 0)
+ g_string_append_printf (changes, "-%u", removed);
+ if (added > 0)
+ g_string_append_printf (changes, "+%u", added);
+ }
+}
+
+static void
+free_changes (gpointer data)
+{
+ GString *changes = data;
+
+ /* all changes must have been checked via assert_changes() before */
+ g_assert_cmpstr (changes->str, ==, "");
+
+ g_string_free (changes, TRUE);
+}
+
+static GtkConcatModel *
+new_model (void)
+{
+ GtkConcatModel *model = gtk_concat_model_new (G_TYPE_OBJECT);
+ GString *changes;
+
+ changes = g_string_new ("");
+ g_object_set_qdata_full (G_OBJECT(model), changes_quark, changes, free_changes);
+ g_signal_connect (model, "items-changed", G_CALLBACK (items_changed), changes);
+
+ return model;
+}
+
+static void
+test_append (void)
+{
+ GListStore *store = new_store (1, 3);
+ GtkConcatModel *concat = new_model ();
+
+ gtk_concat_model_append (concat, G_LIST_MODEL (store));
+
+ assert_model (concat, "1 2 3");
+ assert_changes (concat, "0+3");
+
+ g_object_unref (store);
+ g_object_unref (concat);
+}
+
+static void
+test_append_and_add (void)
+{
+ GListStore *store = new_empty_store ();
+ GtkConcatModel *concat = new_model ();
+
+ gtk_concat_model_append (concat, G_LIST_MODEL (store));
+
+ add (store, 1);
+ add (store, 2);
+ add (store, 3);
+ assert_model (concat, "1 2 3");
+ assert_changes (concat, "+0, +1, +2");
+
+ g_object_unref (store);
+ g_object_unref (concat);
+}
+
+static void
+test_append_and_remove (void)
+{
+ GListStore *store = new_store (1, 3);
+ GtkConcatModel *concat = new_model ();
+
+ gtk_concat_model_append (concat, G_LIST_MODEL (store));
+ gtk_concat_model_remove (concat, G_LIST_MODEL (store));
+
+ assert_model (concat, "");
+ assert_changes (concat, "0+3, 0-3");
+
+ /* Check that all signal handlers are gone */
+ g_list_store_remove_all (store);
+
+ g_object_unref (store);
+ g_object_unref (concat);
+}
+
+static void
+test_append_and_remove_items (void)
+{
+ GListStore *store = new_empty_store ();
+ GtkConcatModel *concat = new_model ();
+
+ gtk_concat_model_append (concat, G_LIST_MODEL (store));
+
+ add (store, 1);
+ add (store, 2);
+ add (store, 3);
+ remove (store, 0);
+ remove (store, 1);
+ remove (store, 0);
+
+ assert_model (concat, "");
+ assert_changes (concat, "+0, +1, +2, -0, -1, -0");
+
+ g_object_unref (store);
+ g_object_unref (concat);
+}
+
+static void
+test_append_many (void)
+{
+ GListStore *store[5] = { new_store (1, 3), new_store (4, 4), new_store (5, 10), new_empty_store (), new_store (11, 20) };
+ GtkConcatModel *concat = new_model ();
+ guint i;
+
+ for (i = 0; i < G_N_ELEMENTS (store); i++)
+ gtk_concat_model_append (concat, G_LIST_MODEL (store[i]));
+
+ assert_model (concat, "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20");
+ assert_changes (concat, "0+3, +3, 4+6, 10+10");
+
+ for (i = 0; i < G_N_ELEMENTS (store); i++)
+ g_object_unref (store[i]);
+ g_object_unref (concat);
+}
+
+static void
+test_append_many_and_add (void)
+{
+ GListStore *store[3];
+ GtkConcatModel *concat = new_model ();
+ guint i, j;
+
+ for (i = 0; i < G_N_ELEMENTS (store); i++)
+ {
+ store[i] = new_empty_store ();
+ gtk_concat_model_append (concat, G_LIST_MODEL (store[i]));
+ }
+
+ for (i = 0; i < G_N_ELEMENTS (store); i++)
+ {
+ for (j = 0; j < G_N_ELEMENTS (store); j++)
+ {
+ add (store[(i + j) % G_N_ELEMENTS (store)], i * 3 + j + 1);
+ }
+ }
+
+ assert_model (concat, "1 6 8 2 4 9 3 5 7");
+ assert_changes (concat, "+0, +1, +2, +2, +4, +1, +6, +2, +5");
+
+ for (i = 0; i < G_N_ELEMENTS (store); i++)
+ g_object_unref (store[i]);
+ g_object_unref (concat);
+}
+
+static void
+test_append_many_and_remove (void)
+{
+ GListStore *store[5];
+ GtkConcatModel *concat = new_model ();
+ guint i;
+
+ store[0] = new_empty_store ();
+ gtk_concat_model_append (concat, G_LIST_MODEL (store[0]));
+ for (i = 1; i < G_N_ELEMENTS (store); i++)
+ {
+ store[i] = new_store (i * (i - 1) / 2 + 1, i * (i + 1) / 2);
+ gtk_concat_model_append (concat, G_LIST_MODEL (store[i]));
+ }
+
+ assert_model (concat, "1 2 3 4 5 6 7 8 9 10");
+ assert_changes (concat, "+0, 1+2, 3+3, 6+4");
+
+ for (i = 0; i < G_N_ELEMENTS (store); i++)
+ {
+ gtk_concat_model_remove (concat, G_LIST_MODEL (store[(3 * i) % G_N_ELEMENTS (store)]));
+ }
+
+ assert_model (concat, "");
+ assert_changes (concat, "3-3, -0, 2-4, 0-2");
+
+ for (i = 0; i < G_N_ELEMENTS (store); i++)
+ {
+ g_list_store_remove_all (store[i]);
+ g_object_unref (store[i]);
+ }
+
+ g_object_unref (concat);
+}
+
+static void
+test_append_many_and_remove_items (void)
+{
+ GListStore *store[5];
+ GtkConcatModel *concat = new_model ();
+ guint i;
+
+ store[0] = new_empty_store ();
+ gtk_concat_model_append (concat, G_LIST_MODEL (store[0]));
+ for (i = 1; i < G_N_ELEMENTS (store); i++)
+ {
+ store[i] = new_store (i * (i - 1) / 2 + 1, i * (i + 1) / 2);
+ gtk_concat_model_append (concat, G_LIST_MODEL (store[i]));
+ }
+
+ assert_model (concat, "1 2 3 4 5 6 7 8 9 10");
+ assert_changes (concat, "+0, 1+2, 3+3, 6+4");
+
+ for (i = 1; i < G_N_ELEMENTS (store); i++)
+ {
+ remove (store[i], 3 % i);
+ }
+
+ assert_model (concat, "2 5 6 7 8 9");
+ assert_changes (concat, "-0, -1, -1, -6");
+
+ for (i = 0; i < G_N_ELEMENTS (store); i++)
+ g_object_unref (store[i]);
+ g_object_unref (concat);
+}
+
+int
+main (int argc, char *argv[])
+{
+ g_test_init (&argc, &argv, NULL);
+ setlocale (LC_ALL, "C");
+ g_test_bug_base ("http://bugzilla.gnome.org/show_bug.cgi?id=%s");
+
+ number_quark = g_quark_from_static_string ("Hell and fire was spawned to be released.");
+ changes_quark = g_quark_from_static_string ("What did I see? Can I believe what I saw?");
+
+ g_test_add_func ("/compatmodel/append", test_append);
+ g_test_add_func ("/compatmodel/append_and_add", test_append_and_add);
+ g_test_add_func ("/compatmodel/append_and_remove", test_append_and_remove);
+ g_test_add_func ("/compatmodel/append_and_remove_items", test_append_and_remove_items);
+ g_test_add_func ("/compatmodel/append_many", test_append_many);
+ g_test_add_func ("/compatmodel/append_many_and_add", test_append_many_and_add);
+ g_test_add_func ("/compatmodel/append_many_and_remove", test_append_many_and_remove);
+ g_test_add_func ("/compatmodel/append_many_and_remove_items", test_append_many_and_remove_items);
+
+ return g_test_run ();
+}
diff --git a/testsuite/gtk/meson.build b/testsuite/gtk/meson.build
index 983a33ffad..fb084ad15c 100644
--- a/testsuite/gtk/meson.build
+++ b/testsuite/gtk/meson.build
@@ -17,6 +17,7 @@ tests = [
['builderparser'],
['cellarea'],
['check-icon-names'],
+ ['concatmodel', ['../../gtk/gtkconcatmodel.c'], ['-DGTK_COMPILATION', '-UG_ENABLE_DEBUG']],
['constraint-solver', [
'../../gtk/gtkconstraintsolver.c',
'../../gtk/gtkconstraintexpression.c',