diff --git a/demos/gtk-demo/sidebar.c b/demos/gtk-demo/sidebar.c index 5ec3d16fd0..d5732cec69 100644 --- a/demos/gtk-demo/sidebar.c +++ b/demos/gtk-demo/sidebar.c @@ -73,7 +73,7 @@ do_sidebar (GtkWidget *do_widget) widget = gtk_label_new (c); } gtk_stack_add_named (GTK_STACK (stack), widget, c); - gtk_container_child_set (GTK_CONTAINER (stack), widget, "title", c, NULL); + g_object_set (gtk_stack_get_page (GTK_STACK (stack), widget), "title", c, NULL); } gtk_container_add (GTK_CONTAINER (window), box); diff --git a/docs/reference/gtk/gtk4-docs.xml b/docs/reference/gtk/gtk4-docs.xml index a5e25803fa..0611530b29 100644 --- a/docs/reference/gtk/gtk4-docs.xml +++ b/docs/reference/gtk/gtk4-docs.xml @@ -49,6 +49,8 @@ + + diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt index 1929eaf4a1..fd93df5a50 100644 --- a/docs/reference/gtk/gtk4-sections.txt +++ b/docs/reference/gtk/gtk4-sections.txt @@ -414,6 +414,47 @@ gtk_list_box_get_type gtk_list_box_row_get_type +
+gtkselectionmodel +GtkSelectionModel +GtkSelectionModel +gtk_selection_model_is_selected +gtk_selection_model_select_item +gtk_selection_model_unselect_item +gtk_selection_model_select_range +gtk_selection_model_unselect_range +gtk_selection_model_select_all +gtk_selection_model_unselect_all + +gtk_selection_model_selection_changed + +GTK_SELECTION_MODEL +GTK_SELECTION_MODEL_CLASS +GTK_SELECTION_MODEL_GET_CLASS +GTK_IS_SELECTION_MODEL +GTK_IS_SELECTION_MODEL_CLASS +GTK_TYPE_SELECTION_MODEL + +gtk_selection_model_get_type +
+ +
+gtksingleselection +GtkSingleSelection +GtkSingleSelection +GTK_INVALID_LIST_POSITION +gtk_single_selection_new +gtk_single_selection_get_selected +gtk_single_selection_set_selected +gtk_single_selection_get_selected_item +gtk_single_selection_get_autoselect +gtk_single_selection_set_autoselect +gtk_single_selection_get_can_unselect +gtk_single_selection_set_can_unselect + +gtk_single_selection_get_type +
+
gtkbuildable GtkBuildable diff --git a/docs/reference/gtk/gtk4.types.in b/docs/reference/gtk/gtk4.types.in index ecb383d45b..79d73c04a4 100644 --- a/docs/reference/gtk/gtk4.types.in +++ b/docs/reference/gtk/gtk4.types.in @@ -142,6 +142,7 @@ gtk_scrollbar_get_type gtk_scrolled_window_get_type gtk_search_bar_get_type gtk_search_entry_get_type +gtk_selection_model_get_type gtk_separator_get_type gtk_separator_menu_item_get_type gtk_separator_tool_item_get_type @@ -151,6 +152,7 @@ gtk_shortcuts_window_get_type gtk_shortcuts_section_get_type gtk_shortcuts_group_get_type gtk_shortcuts_shortcut_get_type +gtk_single_selection_get_type gtk_size_group_get_type gtk_snapshot_get_type gtk_spin_button_get_type diff --git a/gtk/gtk.h b/gtk/gtk.h index e5e1995bc0..c79f1a9dfb 100644 --- a/gtk/gtk.h +++ b/gtk/gtk.h @@ -184,6 +184,7 @@ #include #include #include +#include #include #include #include @@ -194,6 +195,7 @@ #include #include #include +#include #include #include #include diff --git a/gtk/gtkselectionmodel.c b/gtk/gtkselectionmodel.c new file mode 100644 index 0000000000..0be3961bc2 --- /dev/null +++ b/gtk/gtkselectionmodel.c @@ -0,0 +1,301 @@ +/* + * 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 . + * + * Authors: Benjamin Otte + */ + +#include "config.h" + +#include "gtkselectionmodel.h" + +#include "gtkintl.h" +#include "gtkmarshalers.h" + +/** + * SECTION:gtkselectionmodel + * @Title: GtkSelectionModel + * @Short_description: An extension of the list model interface that handles selections + * @See_also: #GListModel, #GtkSingleSelection + * + * #GtkSelectionModel is an interface that extends the #GListModel interface by adding + * support for selections. This support is then used by widgets using list models to add + * the ability to select and unselect various items. + * + * GTK provides default implementations of the mode common selection modes such as + * #GtkSingleSelection, so you will only need to implement this interface if you want + * detailed control about how selections should be handled. + * + * A #GtkSelectionModel supports a single boolean per row indicating if a row is selected + * or not. This can be queried via gtk_selection_model_is_selected(). When the selected + * state of one or more rows changes, the model will emit the + * GtkSelectionModel::selection-changed signal by calling the + * gtk_selection_model_selection_changed() function. The positions given in that signal + * may have their selection state changed, though that is not a requirement. + * If new items added to the model via the #GListModel::items-changed signal are selected + * or not is up to the implementation. + * + * Additionally, the interface can expose functionality to select and unselect items. + * If these functions are implemented, GTK's list widgets will allow users to select and + * unselect items. However, #GtkSelectionModels are free to only implement them + * partially or not at all. In that case the widgets will not support the unimplemented + * operations. + * + * When selecting or unselecting is supported by a model, the return values of the + * selection functions do NOT indicate if selection or unselection happened. They are + * only meant to indicate complete failure, like when this mode of selecting is not + * supported by the model. + * Selections may happen asynchronously, so the only reliable way to find out when an + * item was selected is to listen to the signals that indicate selection. + */ + +G_DEFINE_INTERFACE (GtkSelectionModel, gtk_selection_model, G_TYPE_LIST_MODEL) + +enum { + SELECTION_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static gboolean +gtk_selection_model_default_is_selected (GtkSelectionModel *model, + guint position) +{ + return FALSE; +} + +static gboolean +gtk_selection_model_default_select_item (GtkSelectionModel *model, + guint position, + gboolean exclusive) +{ + return FALSE; +} +static gboolean +gtk_selection_model_default_unselect_item (GtkSelectionModel *model, + guint position) +{ + return FALSE; +} + +static gboolean +gtk_selection_model_default_select_range (GtkSelectionModel *model, + guint position, + guint n_items, + gboolean exclusive) +{ + return FALSE; +} + +static gboolean +gtk_selection_model_default_unselect_range (GtkSelectionModel *model, + guint position, + guint n_items) +{ + return FALSE; +} + +static gboolean +gtk_selection_model_default_select_all (GtkSelectionModel *model) +{ + return gtk_selection_model_select_range (model, 0, g_list_model_get_n_items (G_LIST_MODEL (model)), FALSE); +} + +static gboolean +gtk_selection_model_default_unselect_all (GtkSelectionModel *model) +{ + return gtk_selection_model_unselect_range (model, 0, g_list_model_get_n_items (G_LIST_MODEL (model)));; +} + +static gboolean +gtk_selection_model_default_query_range (GtkSelectionModel *model, + guint *position, + guint *n_items) +{ + *n_items = 1; + return gtk_selection_model_is_selected (model, *position); +} + +static void +gtk_selection_model_default_init (GtkSelectionModelInterface *iface) +{ + iface->is_selected = gtk_selection_model_default_is_selected; + iface->select_item = gtk_selection_model_default_select_item; + iface->unselect_item = gtk_selection_model_default_unselect_item; + iface->select_range = gtk_selection_model_default_select_range; + iface->unselect_range = gtk_selection_model_default_unselect_range; + iface->select_all = gtk_selection_model_default_select_all; + iface->unselect_all = gtk_selection_model_default_unselect_all; + iface->query_range = gtk_selection_model_default_query_range; + + /** + * GtkSelectionModel::selection-changed + * @model: a #GtkSelectionModel + * @position: The first item that may have changed + * @n_items: number of items with changes + * + * Emitted when the selection state of some of the items in @model changes. + * + * Note that this signal does not specify the new selection state of the items, + * they need to be queried manually. + * It is also not necessary for a model to change the selection state of any of + * the items in the selection model, though it would be rather useless to emit + * such a signal. + */ + signals[SELECTION_CHANGED] = + g_signal_new ("selection-changed", + GTK_TYPE_SELECTION_MODEL, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + _gtk_marshal_VOID__UINT_UINT, + G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_UINT); + g_signal_set_va_marshaller (signals[SELECTION_CHANGED], + GTK_TYPE_SELECTION_MODEL, + _gtk_marshal_VOID__UINT_UINTv); +} + +/** + * gtk_selection_model_is_selected: + * @model: a #GtkSelectionModel + * @position: the position of the item to query + * + * Checks if the given item is selected. + * + * Returns: %TRUE if the item is selected + **/ +gboolean +gtk_selection_model_is_selected (GtkSelectionModel *model, + guint position) +{ + GtkSelectionModelInterface *iface; + + g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), 0); + + iface = GTK_SELECTION_MODEL_GET_IFACE (model); + return iface->is_selected (model, position); +} + +gboolean +gtk_selection_model_select_item (GtkSelectionModel *model, + guint position, + gboolean exclusive) +{ + GtkSelectionModelInterface *iface; + + g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), 0); + + iface = GTK_SELECTION_MODEL_GET_IFACE (model); + return iface->select_item (model, position, exclusive); +} + +gboolean +gtk_selection_model_unselect_item (GtkSelectionModel *model, + guint position) +{ + GtkSelectionModelInterface *iface; + + g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), 0); + + iface = GTK_SELECTION_MODEL_GET_IFACE (model); + return iface->unselect_item (model, position); +} + +gboolean +gtk_selection_model_select_range (GtkSelectionModel *model, + guint position, + guint n_items, + gboolean exclusive) +{ + GtkSelectionModelInterface *iface; + + g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), 0); + + iface = GTK_SELECTION_MODEL_GET_IFACE (model); + return iface->select_range (model, position, n_items, exclusive); +} + +gboolean +gtk_selection_model_unselect_range (GtkSelectionModel *model, + guint position, + guint n_items) +{ + GtkSelectionModelInterface *iface; + + g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), 0); + + iface = GTK_SELECTION_MODEL_GET_IFACE (model); + return iface->unselect_range (model, position, n_items); +} + +gboolean +gtk_selection_model_select_all (GtkSelectionModel *model) +{ + GtkSelectionModelInterface *iface; + + g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), 0); + + iface = GTK_SELECTION_MODEL_GET_IFACE (model); + return iface->select_all (model); +} + +gboolean +gtk_selection_model_unselect_all (GtkSelectionModel *model) +{ + GtkSelectionModelInterface *iface; + + g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), 0); + + iface = GTK_SELECTION_MODEL_GET_IFACE (model); + return iface->unselect_all (model); +} + +/** + * gtk_selection_model_query_range: + * @model: a #GtkSelectionModel + * @position: (inout): specifies the position on input, and the first element of the range on output + * @n_items: (out): returns the size of the range + * + * This function allows to query the selection status of multiple elements at once. + * It is passed a position and returns a range of elements of uniform selection status. + * The returned range is guaranteed to include the passed-in position. + * The selection status is returned from this function. + * + * Returns: %TRUE if the elements in the returned range are selected, %FALSE otherwise + */ +gboolean +gtk_selection_model_query_range (GtkSelectionModel *model, + guint *position, + guint *n_items) +{ + GtkSelectionModelInterface *iface; + + g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), FALSE); + + iface = GTK_SELECTION_MODEL_GET_IFACE (model); + return iface->query_range (model, position, n_items); +} + +void +gtk_selection_model_selection_changed (GtkSelectionModel *model, + guint position, + guint n_items) +{ + g_return_if_fail (GTK_IS_SELECTION_MODEL (model)); + + g_signal_emit (model, signals[SELECTION_CHANGED], 0, position, n_items); +} + diff --git a/gtk/gtkselectionmodel.h b/gtk/gtkselectionmodel.h new file mode 100644 index 0000000000..65c424716d --- /dev/null +++ b/gtk/gtkselectionmodel.h @@ -0,0 +1,125 @@ +/* + * 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 . + * + * Authors: Benjamin Otte + */ + +#ifndef __GTK_SELECTION_MODEL_H__ +#define __GTK_SELECTION_MODEL_H__ + +#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_SELECTION_MODEL (gtk_selection_model_get_type ()) + +GDK_AVAILABLE_IN_ALL +G_DECLARE_INTERFACE (GtkSelectionModel, gtk_selection_model, GTK, SELECTION_MODEL, GListModel) + +/** + * GtkSelectionModelInterface: + * @is_selected: Return if the item at the given position is selected. + * @select_item: Select the item in the given position. If the operation + * is known to fail, return %FALSE. + * @unselect_item: Unselect the item in the given position. If the + * operation is known to fail, return %FALSE. + * @select_range: Select all items in the given range. If the operation + * is unsupported or known to fail for all items, return %FALSE. + * @unselect_range: Unselect all items in the given range. If the + * operation is unsupported or known to fail for all items, return + * %FALSE. + * @select_all: Select all items in the model. If the operation is + * unsupported or known to fail for all items, return %FALSE. + * @unselect_all: Unselect all items in the model. If the operation is + * unsupported or known to fail for all items, return %FALSE. + * + * The list of virtual functions for the #GtkSelectionModel interface. + * All getter functions are mandatory to implement, but the model does + * not need to implement any functions to support selecting or unselecting + * items. Of course, if the model does not do that, it means that users + * cannot select or unselect items in a list widgets using the model. + */ +struct _GtkSelectionModelInterface +{ + /*< private >*/ + GTypeInterface g_iface; + + /*< public >*/ + gboolean (* is_selected) (GtkSelectionModel *model, + guint position); + + gboolean (* select_item) (GtkSelectionModel *model, + guint position, + gboolean exclusive); + gboolean (* unselect_item) (GtkSelectionModel *model, + guint position); + gboolean (* select_range) (GtkSelectionModel *model, + guint position, + guint n_items, + gboolean exclusive); + gboolean (* unselect_range) (GtkSelectionModel *model, + guint position, + guint n_items); + gboolean (* select_all) (GtkSelectionModel *model); + gboolean (* unselect_all) (GtkSelectionModel *model); + gboolean (* query_range) (GtkSelectionModel *model, + guint *position, + guint *n_items); +}; + +GDK_AVAILABLE_IN_ALL +gboolean gtk_selection_model_is_selected (GtkSelectionModel *model, + guint position); + +GDK_AVAILABLE_IN_ALL +gboolean gtk_selection_model_select_item (GtkSelectionModel *model, + guint position, + gboolean exclusive); +GDK_AVAILABLE_IN_ALL +gboolean gtk_selection_model_unselect_item (GtkSelectionModel *model, + guint position); +GDK_AVAILABLE_IN_ALL +gboolean gtk_selection_model_select_range (GtkSelectionModel *model, + guint position, + guint n_items, + gboolean exclusive); +GDK_AVAILABLE_IN_ALL +gboolean gtk_selection_model_unselect_range (GtkSelectionModel *model, + guint position, + guint n_items); +GDK_AVAILABLE_IN_ALL +gboolean gtk_selection_model_select_all (GtkSelectionModel *model); +GDK_AVAILABLE_IN_ALL +gboolean gtk_selection_model_unselect_all (GtkSelectionModel *model); + +GDK_AVAILABLE_IN_ALL +gboolean gtk_selection_model_query_range (GtkSelectionModel *model, + guint *position, + guint *n_items); + +/* for implementations only */ +GDK_AVAILABLE_IN_ALL +void gtk_selection_model_selection_changed (GtkSelectionModel *model, + guint position, + guint n_items); + +G_END_DECLS + +#endif /* __GTK_SELECTION_MODEL_H__ */ diff --git a/gtk/gtksingleselection.c b/gtk/gtksingleselection.c new file mode 100644 index 0000000000..17ae761c73 --- /dev/null +++ b/gtk/gtksingleselection.c @@ -0,0 +1,637 @@ +/* + * 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 . + * + * Authors: Benjamin Otte + */ + +#include "config.h" + +#include "gtksingleselection.h" + +#include "gtkintl.h" +#include "gtkselectionmodel.h" + +/** + * SECTION:gtksingleselection + * @Short_description: A selection model that allows selecting a single item + * @Title: GtkSingleSelection + * @see_also: #GtkSelectionModel + * + * GtkSingleSelection is an implementation of the #GtkSelectionModel interface + * that allows selecting a single element. It is the default selection method + * used by list widgets in GTK. + */ +struct _GtkSingleSelection +{ + GObject parent_instance; + + GListModel *model; + guint selected; + gpointer selected_item; + + guint autoselect : 1; + guint can_unselect : 1; +}; + +struct _GtkSingleSelectionClass +{ + GObjectClass parent_class; +}; + +enum { + PROP_0, + PROP_AUTOSELECT, + PROP_CAN_UNSELECT, + PROP_SELECTED, + PROP_SELECTED_ITEM, + PROP_MODEL, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS] = { NULL, }; + +static GType +gtk_single_selection_get_item_type (GListModel *list) +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (list); + + return g_list_model_get_item_type (self->model); +} + +static guint +gtk_single_selection_get_n_items (GListModel *list) +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (list); + + return g_list_model_get_n_items (self->model); +} + +static gpointer +gtk_single_selection_get_item (GListModel *list, + guint position) +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (list); + + return g_list_model_get_item (self->model, position); +} + +static void +gtk_single_selection_list_model_init (GListModelInterface *iface) +{ + iface->get_item_type = gtk_single_selection_get_item_type; + iface->get_n_items = gtk_single_selection_get_n_items; + iface->get_item = gtk_single_selection_get_item; +} + +static gboolean +gtk_single_selection_is_selected (GtkSelectionModel *model, + guint position) +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (model); + + return self->selected == position; +} + +static gboolean +gtk_single_selection_select_item (GtkSelectionModel *model, + guint position, + gboolean exclusive) +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (model); + + /* XXX: Should we check that position < n_items here? */ + gtk_single_selection_set_selected (self, position); + + return TRUE; +} + +static gboolean +gtk_single_selection_unselect_item (GtkSelectionModel *model, + guint position) +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (model); + + if (!self->can_unselect) + return FALSE; + + if (self->selected == position) + gtk_single_selection_set_selected (self, GTK_INVALID_LIST_POSITION); + + return TRUE; +} + +static gboolean +gtk_single_selection_query_range (GtkSelectionModel *model, + guint *position, + guint *n_items) +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (model); + + if (self->selected == GTK_INVALID_LIST_POSITION) + { + *position = 0; + *n_items = g_list_model_get_n_items (self->model); + return FALSE; + } + else if (*position < self->selected) + { + *position = 0; + *n_items = self->selected; + return FALSE; + } + else if (*position > self->selected) + { + *position = self->selected + 1; + *n_items = g_list_model_get_n_items (self->model) - *position; + return FALSE; + } + else + { + *position = self->selected; + *n_items = 1; + return TRUE; + } +} + +static void +gtk_single_selection_selection_model_init (GtkSelectionModelInterface *iface) +{ + iface->is_selected = gtk_single_selection_is_selected; + iface->select_item = gtk_single_selection_select_item; + iface->unselect_item = gtk_single_selection_unselect_item; + iface->query_range = gtk_single_selection_query_range; +} + +G_DEFINE_TYPE_EXTENDED (GtkSingleSelection, gtk_single_selection, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, + gtk_single_selection_list_model_init) + G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL, + gtk_single_selection_selection_model_init)) + +static void +gtk_single_selection_items_changed_cb (GListModel *model, + guint position, + guint removed, + guint added, + GtkSingleSelection *self) +{ + gboolean emit_selection_changed = FALSE; + + g_object_freeze_notify (G_OBJECT (self)); + + if (self->selected_item == NULL) + { + if (self->autoselect) + { + self->selected_item = g_list_model_get_item (self->model, 0); + if (self->selected_item) + { + self->selected = 0; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED]); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED_ITEM]); + } + } + } + else if (self->selected < position) + { + /* unchanged */ + } + else if (self->selected >= position + removed) + { + self->selected += added - removed; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED]); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED_ITEM]); + } + else + { + guint i; + + for (i = 0; i < added; i++) + { + gpointer item = g_list_model_get_item (model, position + i); + if (item == self->selected_item) + { + /* the item moved */ + if (self->selected != position + i) + { + self->selected = position + i; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED]); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED_ITEM]); + } + break; + } + } + if (i == added) + { + /* the item really was deleted */ + g_clear_object (&self->selected_item); + if (self->autoselect) + { + self->selected = position + (self->selected - position) * added / removed; + self->selected_item = g_list_model_get_item (self->model, self->selected); + if (self->selected_item == NULL) + { + if (position > 0) + { + self->selected = position - 1; + self->selected_item = g_list_model_get_item (self->model, self->selected); + g_assert (self->selected_item); + } + else + self->selected = GTK_INVALID_LIST_POSITION; + } + + emit_selection_changed = TRUE; + } + else + { + g_clear_object (&self->selected_item); + self->selected = GTK_INVALID_LIST_POSITION; + } + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED]); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED_ITEM]); + } + } + + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); + + if (emit_selection_changed && self->selected != GTK_INVALID_LIST_POSITION) + gtk_selection_model_selection_changed (GTK_SELECTION_MODEL (self), self->selected, 1); + + g_object_thaw_notify (G_OBJECT (self)); +} + +static void +gtk_single_selection_clear_model (GtkSingleSelection *self) +{ + if (self->model == NULL) + return; + + g_signal_handlers_disconnect_by_func (self->model, + gtk_single_selection_items_changed_cb, + self); + g_clear_object (&self->model); +} + +static void +gtk_single_selection_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) + +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (object); + + switch (prop_id) + { + case PROP_AUTOSELECT: + gtk_single_selection_set_autoselect (self, g_value_get_boolean (value)); + break; + + case PROP_CAN_UNSELECT: + gtk_single_selection_set_can_unselect (self, g_value_get_boolean (value)); + break; + + case PROP_MODEL: + gtk_single_selection_clear_model (self); + self->model = g_value_dup_object (value); + if (self->model) + g_signal_connect (self->model, "items-changed", + G_CALLBACK (gtk_single_selection_items_changed_cb), self); + if (self->autoselect) + gtk_single_selection_set_selected (self, 0); + break; + + case PROP_SELECTED: + gtk_single_selection_set_selected (self, g_value_get_uint (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_single_selection_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (object); + + switch (prop_id) + { + case PROP_AUTOSELECT: + g_value_set_boolean (value, self->autoselect); + break; + + case PROP_CAN_UNSELECT: + g_value_set_boolean (value, self->can_unselect); + break; + case PROP_MODEL: + g_value_set_object (value, self->model); + break; + + case PROP_SELECTED: + g_value_set_uint (value, self->selected); + break; + + case PROP_SELECTED_ITEM: + g_value_set_object (value, self->selected_item); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_single_selection_dispose (GObject *object) +{ + GtkSingleSelection *self = GTK_SINGLE_SELECTION (object); + + gtk_single_selection_clear_model (self); + + self->selected = GTK_INVALID_LIST_POSITION; + g_clear_object (&self->selected_item); + + G_OBJECT_CLASS (gtk_single_selection_parent_class)->dispose (object); +} + +static void +gtk_single_selection_class_init (GtkSingleSelectionClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = gtk_single_selection_get_property; + gobject_class->set_property = gtk_single_selection_set_property; + gobject_class->dispose = gtk_single_selection_dispose; + + /** + * GtkSingleSelection:autoselect: + * + * If the selection will always select an item + */ + properties[PROP_AUTOSELECT] = + g_param_spec_boolean ("autoselect", + P_("Autoselect"), + P_("If the selection will always select an item"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkSingleSelection:can-unselect: + * + * If unselecting the selected item is allowed + */ + properties[PROP_CAN_UNSELECT] = + g_param_spec_boolean ("can-unselect", + P_("Can unselect"), + P_("If unselecting the selected item is allowed"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkSingleSelection:selected: + * + * Position of the selected item + */ + properties[PROP_SELECTED] = + g_param_spec_uint ("selected", + P_("Selected"), + P_("Position of the selected item"), + 0, G_MAXUINT, GTK_INVALID_LIST_POSITION, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkSingleSelection:selected-item: + * + * The selected item + */ + properties[PROP_SELECTED_ITEM] = + g_param_spec_object ("selected-item", + P_("Selected Item"), + P_("The selected item"), + G_TYPE_OBJECT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + properties[PROP_MODEL] = + g_param_spec_object ("model", + P_("The model"), + P_("The model"), + G_TYPE_LIST_MODEL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, properties); +} + +static void +gtk_single_selection_init (GtkSingleSelection *self) +{ + self->selected = GTK_INVALID_LIST_POSITION; + self->autoselect = TRUE; +} + +/** + * gtk_single_selection_new: + * @model: (transfer none): the #GListModel to manage + * + * Creates a new selection to handle @model. + * + * Returns: (transfer full) (type GtkSingleSelection): a new #GtkSingleSelection + **/ +GtkSingleSelection * +gtk_single_selection_new (GListModel *model) +{ + g_return_val_if_fail (G_IS_LIST_MODEL (model), NULL); + + return g_object_new (GTK_TYPE_SINGLE_SELECTION, + "model", model, + NULL); +} + +/** + * gtk_single_selection_get_selected: + * @self: a #GtkSingleSelection + * + * Gets the position of the selected item. If no item is selected, + * #GTK_INVALID_LIST_POSITION is returned. + * + * Returns: The position of the selected item + **/ +guint +gtk_single_selection_get_selected (GtkSingleSelection *self) +{ + g_return_val_if_fail (GTK_IS_SINGLE_SELECTION (self), GTK_INVALID_LIST_POSITION); + + return self->selected; +} + +/** + * gtk_single_selection_set_selected: + * @self: a #GtkSingleSelection + * @position: the item to select or #GTK_INVALID_LIST_POSITION + * + * Selects the item at the given position. If the list does not have an item at + * @position or #GTK_INVALID_LIST_POSITION is given, the behavior depends on the + * value of the GtkSingleSelection:autoselect property: If it is set, no change + * will occur and the old item will stay selected. If it is unset, the selection + * will be unset and no item will be selected. + **/ +void +gtk_single_selection_set_selected (GtkSingleSelection *self, + guint position) +{ + gpointer new_selected = NULL; + guint old_position; + + g_return_if_fail (GTK_IS_SINGLE_SELECTION (self)); + + if (self->selected == position) + return; + + if (self->model) + new_selected = g_list_model_get_item (self->model, position); + + if (new_selected == NULL) + position = GTK_INVALID_LIST_POSITION; + + if (self->selected == position) + return; + + old_position = self->selected; + self->selected = position; + g_clear_object (&self->selected_item); + self->selected_item = new_selected; + + if (old_position == GTK_INVALID_LIST_POSITION) + gtk_selection_model_selection_changed (GTK_SELECTION_MODEL (self), position, 1); + else if (position == GTK_INVALID_LIST_POSITION) + gtk_selection_model_selection_changed (GTK_SELECTION_MODEL (self), old_position, 1); + else if (position < old_position) + gtk_selection_model_selection_changed (GTK_SELECTION_MODEL (self), position, old_position - position + 1); + else + gtk_selection_model_selection_changed (GTK_SELECTION_MODEL (self), old_position, position - old_position + 1); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED]); +} + +/** + * gtk_single_selection_get_selected_item: + * @self: a #GtkSingleSelection + * + * Gets the selected item. If no item is selected, %NULL is returned. + * + * Returns: (transfer none): The selected item + */ +gpointer +gtk_single_selection_get_selected_item (GtkSingleSelection *self) +{ + g_return_val_if_fail (GTK_IS_SINGLE_SELECTION (self), NULL); + + return self->selected_item; +} + +/** + * gtk_single_selection_get_autoselect: + * @self: a #GtkSingleSelection + * + * Checks if autoselect has been enabled or disabled via + * gtk_single_selection_set_autoselect(). + * + * Returns: %TRUE if autoselect is enabled + **/ +gboolean +gtk_single_selection_get_autoselect (GtkSingleSelection *self) +{ + g_return_val_if_fail (GTK_IS_SINGLE_SELECTION (self), TRUE); + + return self->autoselect; +} + +/** + * gtk_single_selection_set_autoselect: + * @self: a #GtkSingleSelection + * @autoselect: %TRUE to always select an item + * + * If @autoselect is %TRUE, @self will enforce that an item is always + * selected. It will select a new item when the currently selected + * item is deleted and it will disallow unselecting the current item. + **/ +void +gtk_single_selection_set_autoselect (GtkSingleSelection *self, + gboolean autoselect) +{ + g_return_if_fail (GTK_IS_SINGLE_SELECTION (self)); + + if (self->autoselect == autoselect) + return; + + self->autoselect = autoselect; + + g_object_freeze_notify (G_OBJECT (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_AUTOSELECT]); + + if (self->autoselect && !self->selected_item) + gtk_single_selection_set_selected (self, 0); + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtk_single_selection_get_can_unselect: + * @self: a #GtkSingleSelection + * + * If %TRUE, gtk_selection_model_unselect_item() is supported and allows + * unselecting the selected item. + * + * Returns: %TRUE to support unselecting + **/ +gboolean +gtk_single_selection_get_can_unselect (GtkSingleSelection *self) +{ + g_return_val_if_fail (GTK_IS_SINGLE_SELECTION (self), FALSE); + + return self->can_unselect; +} + +/** + * gtk_single_selection_set_can_unselect: + * @self: a #GtkSingleSelection + * @can_unselect: %TRUE to allow unselecting + * + * If %TRUE, unselecting the current item via + * gtk_selection_model_unselect_item() is supported. + * + * Note that setting GtkSingleSelection:autoselect will cause the + * unselecting to not work, so it practically makes no sense to set + * both at the same time the same time.. + **/ +void +gtk_single_selection_set_can_unselect (GtkSingleSelection *self, + gboolean can_unselect) +{ + g_return_if_fail (GTK_IS_SINGLE_SELECTION (self)); + + if (self->can_unselect == can_unselect) + return; + + self->can_unselect = can_unselect; + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CAN_UNSELECT]); +} diff --git a/gtk/gtksingleselection.h b/gtk/gtksingleselection.h new file mode 100644 index 0000000000..ca96a4e052 --- /dev/null +++ b/gtk/gtksingleselection.h @@ -0,0 +1,67 @@ +/* + * 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 . + * + * Authors: Benjamin Otte + */ + +#ifndef __GTK_SINGLE_SELECTION_H__ +#define __GTK_SINGLE_SELECTION_H__ + +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_SINGLE_SELECTION (gtk_single_selection_get_type ()) + +/** + * GTK_INVALID_LIST_POSITION: + * + * The value used to refer to a guaranteed invalid position in a #GListModel. This + * value may be returned from some functions, others may accept it as input. + * Its interpretion may differ for different functions. + * + * Refer to each function's documentation for if this value is allowed and what it + * does. + */ +#define GTK_INVALID_LIST_POSITION (G_MAXUINT) + +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkSingleSelection, gtk_single_selection, GTK, SINGLE_SELECTION, GObject) + +GDK_AVAILABLE_IN_ALL +GtkSingleSelection * gtk_single_selection_new (GListModel *model); + +GDK_AVAILABLE_IN_ALL +guint gtk_single_selection_get_selected (GtkSingleSelection *self); +GDK_AVAILABLE_IN_ALL +void gtk_single_selection_set_selected (GtkSingleSelection *self, + guint position); +GDK_AVAILABLE_IN_ALL +gpointer gtk_single_selection_get_selected_item (GtkSingleSelection *self); +GDK_AVAILABLE_IN_ALL +gboolean gtk_single_selection_get_autoselect (GtkSingleSelection *self); +GDK_AVAILABLE_IN_ALL +void gtk_single_selection_set_autoselect (GtkSingleSelection *self, + gboolean autoselect); +GDK_AVAILABLE_IN_ALL +gboolean gtk_single_selection_get_can_unselect (GtkSingleSelection *self); +GDK_AVAILABLE_IN_ALL +void gtk_single_selection_set_can_unselect (GtkSingleSelection *self, + gboolean can_unselect); + +G_END_DECLS + +#endif /* __GTK_SINGLE_SELECTION_H__ */ diff --git a/gtk/gtkstack.c b/gtk/gtkstack.c index 338199c319..2030e919f5 100644 --- a/gtk/gtkstack.c +++ b/gtk/gtkstack.c @@ -30,6 +30,8 @@ #include "gtksettingsprivate.h" #include "gtksnapshot.h" #include "gtkwidgetprivate.h" +#include "gtksingleselection.h" +#include "gtklistlistmodelprivate.h" #include "a11y/gtkstackaccessible.h" #include "a11y/gtkstackaccessibleprivate.h" #include @@ -138,6 +140,8 @@ typedef struct { GtkStackTransitionType active_transition_type; + GtkSelectionModel *pages; + } GtkStackPrivate; static void gtk_stack_buildable_interface_init (GtkBuildableIface *iface); @@ -167,8 +171,8 @@ enum CHILD_PROP_NAME, CHILD_PROP_TITLE, CHILD_PROP_ICON_NAME, - CHILD_PROP_POSITION, CHILD_PROP_NEEDS_ATTENTION, + CHILD_PROP_VISIBLE, LAST_CHILD_PROP }; @@ -179,6 +183,7 @@ struct _GtkStackPage { gchar *title; gchar *icon_name; gboolean needs_attention; + gboolean visible; GtkWidget *last_focus; }; @@ -194,6 +199,7 @@ G_DEFINE_TYPE (GtkStackPage, gtk_stack_page, G_TYPE_OBJECT) static void gtk_stack_page_init (GtkStackPage *page) { + page->visible = TRUE; } static void @@ -239,32 +245,20 @@ gtk_stack_page_get_property (GObject *object, g_value_set_string (value, info->icon_name); break; - case CHILD_PROP_POSITION: - if (info->widget) - { - GtkWidget *stack = gtk_widget_get_parent (GTK_WIDGET (info->widget)); - GtkStackPrivate *priv = gtk_stack_get_instance_private (GTK_STACK (stack)); - - g_value_set_int (value, g_list_index (priv->children, info)); - } - else - g_value_set_int (value, 0); - break; - case CHILD_PROP_NEEDS_ATTENTION: g_value_set_boolean (value, info->needs_attention); break; + case CHILD_PROP_VISIBLE: + g_value_set_boolean (value, info->visible); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } -static void reorder_child (GtkStack *stack, - GtkWidget *child, - gint position); - static void gtk_stack_page_set_property (GObject *object, guint property_id, @@ -327,11 +321,6 @@ gtk_stack_page_set_property (GObject *object, g_object_notify_by_pspec (object, pspec); break; - case CHILD_PROP_POSITION: - if (stack) - reorder_child (GTK_STACK (stack), info->widget, g_value_get_int (value)); - break; - case CHILD_PROP_NEEDS_ATTENTION: if (info->needs_attention != g_value_get_boolean (value)) { @@ -340,6 +329,16 @@ gtk_stack_page_set_property (GObject *object, } break; + case CHILD_PROP_VISIBLE: + if (info->visible != g_value_get_boolean (value)) + { + info->visible = g_value_get_boolean (value); + if (info->widget) + gtk_widget_set_visible (info->widget, info->visible); + g_object_notify_by_pspec (object, pspec); + } + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -382,14 +381,6 @@ gtk_stack_page_class_init (GtkStackPageClass *class) NULL, GTK_PARAM_READWRITE); - stack_child_props[CHILD_PROP_POSITION] = - g_param_spec_int ("position", - P_("Position"), - P_("The index of the child in the parent"), - -1, G_MAXINT, - 0, - GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); - /** * GtkStack:needs-attention: * @@ -405,9 +396,134 @@ gtk_stack_page_class_init (GtkStackPageClass *class) FALSE, GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + stack_child_props[CHILD_PROP_VISIBLE] = + g_param_spec_boolean ("visible", + P_("Visible"), + P_("Whether this page is visible"), + TRUE, + GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + g_object_class_install_properties (object_class, LAST_CHILD_PROP, stack_child_props); } +#define GTK_TYPE_STACK_PAGES (gtk_stack_pages_get_type ()) +G_DECLARE_FINAL_TYPE (GtkStackPages, gtk_stack_pages, GTK, STACK_PAGES, GObject) + +struct _GtkStackPages +{ + GObject parent_instance; + GtkStack *stack; +}; + +struct _GtkStackPagesClass +{ + GObjectClass parent_class; +}; + +static GType +gtk_stack_pages_get_item_type (GListModel *model) +{ + return GTK_TYPE_STACK_PAGE; +} + +static guint +gtk_stack_pages_get_n_items (GListModel *model) +{ + GtkStackPages *pages = GTK_STACK_PAGES (model); + GtkStackPrivate *priv = gtk_stack_get_instance_private (pages->stack); + + return g_list_length (priv->children); +} + +static gpointer +gtk_stack_pages_get_item (GListModel *model, + guint position) +{ + GtkStackPages *pages = GTK_STACK_PAGES (model); + GtkStackPrivate *priv = gtk_stack_get_instance_private (pages->stack); + GtkStackPage *page; + + page = g_list_nth_data (priv->children, position); + + return g_object_ref (page); +} + +static void +gtk_stack_pages_list_model_init (GListModelInterface *iface) +{ + iface->get_item_type = gtk_stack_pages_get_item_type; + iface->get_n_items = gtk_stack_pages_get_n_items; + iface->get_item = gtk_stack_pages_get_item; +} + +static gboolean +gtk_stack_pages_is_selected (GtkSelectionModel *model, + guint position) +{ + GtkStackPages *pages = GTK_STACK_PAGES (model); + GtkStackPrivate *priv = gtk_stack_get_instance_private (pages->stack); + GtkStackPage *page; + + page = GTK_STACK_PAGE (g_list_model_get_item (G_LIST_MODEL (model), position)); + g_object_unref (page); + + return page == priv->visible_child; +} + +static void set_visible_child (GtkStack *stack, + GtkStackPage *child_info, + GtkStackTransitionType transition_type, + guint transition_duration); + +static gboolean +gtk_stack_pages_select_item (GtkSelectionModel *model, + guint position, + gboolean exclusive) +{ + GtkStackPages *pages = GTK_STACK_PAGES (model); + GtkStackPrivate *priv = gtk_stack_get_instance_private (pages->stack); + GtkStackPage *page; + + page = GTK_STACK_PAGE (g_list_model_get_item (G_LIST_MODEL (model), position)); + g_object_unref (page); + + set_visible_child (pages->stack, page, priv->transition_type, priv->transition_duration); + + return TRUE; +} + +static void +gtk_stack_pages_selection_model_init (GtkSelectionModelInterface *iface) +{ + iface->is_selected = gtk_stack_pages_is_selected; + iface->select_item = gtk_stack_pages_select_item; +} + +G_DEFINE_TYPE_WITH_CODE (GtkStackPages, gtk_stack_pages, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_stack_pages_list_model_init) + G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL, gtk_stack_pages_selection_model_init)) + +static void +gtk_stack_pages_init (GtkStackPages *pages) +{ +} + +static void +gtk_stack_pages_class_init (GtkStackPagesClass *class) +{ +} + +static GtkStackPages * +gtk_stack_pages_new (GtkStack *stack) +{ + GtkStackPages *pages; + + pages = g_object_new (GTK_TYPE_STACK_PAGES, NULL); + pages->stack = stack; + + return pages; +} + static void gtk_stack_add (GtkContainer *widget, GtkWidget *child); static void gtk_stack_remove (GtkContainer *widget, @@ -431,6 +547,7 @@ static void gtk_stack_measure (GtkWidget *widget int *natural, int *minimum_baseline, int *natural_baseline); +static void gtk_stack_dispose (GObject *obj); static void gtk_stack_finalize (GObject *obj); static void gtk_stack_get_property (GObject *object, guint property_id, @@ -466,6 +583,29 @@ gtk_stack_buildable_interface_init (GtkBuildableIface *iface) iface->add_child = gtk_stack_buildable_add_child; } +static void stack_remove (GtkStack *stack, + GtkWidget *child, + gboolean in_dispose); + +static void +remove_child (GtkWidget *child, gpointer user_data) +{ + stack_remove (GTK_STACK (user_data), child, TRUE); +} + +static void +gtk_stack_dispose (GObject *obj) +{ + GtkStack *stack = GTK_STACK (obj); + GtkStackPrivate *priv = gtk_stack_get_instance_private (stack); + + if (priv->pages) + g_list_model_items_changed (G_LIST_MODEL (priv->pages), 0, g_list_length (priv->children), 0); + gtk_container_foreach (GTK_CONTAINER (obj), remove_child, obj); + + G_OBJECT_CLASS (gtk_stack_parent_class)->dispose (obj); +} + static void gtk_stack_finalize (GObject *obj) { @@ -571,6 +711,7 @@ gtk_stack_class_init (GtkStackClass *klass) object_class->get_property = gtk_stack_get_property; object_class->set_property = gtk_stack_set_property; + object_class->dispose = gtk_stack_dispose; object_class->finalize = gtk_stack_finalize; widget_class->size_allocate = gtk_stack_size_allocate; @@ -670,59 +811,6 @@ find_child_info_for_widget (GtkStack *stack, return NULL; } -static void -reorder_child (GtkStack *stack, - GtkWidget *child, - gint position) -{ - GtkStackPrivate *priv = gtk_stack_get_instance_private (stack); - GList *l; - GList *old_link = NULL; - GList *new_link = NULL; - GtkStackPage *child_info = NULL; - gint num = 0; - - l = priv->children; - - /* Loop to find the old position and link of child, new link of child and - * total number of children. new_link will be NULL if the child should be - * moved to the end (in case of position being < 0 || >= num) - */ - while (l && (new_link == NULL || old_link == NULL)) - { - /* Record the new position if found */ - if (position == num) - new_link = l; - - if (old_link == NULL) - { - GtkStackPage *info; - info = l->data; - - /* Keep trying to find the current position and link location of the child */ - if (info->widget == child) - { - old_link = l; - child_info = info; - } - } - - l = l->next; - num++; - } - - g_return_if_fail (old_link != NULL); - - if (old_link == new_link || (old_link->next == NULL && new_link == NULL)) - return; - - priv->children = g_list_delete_link (priv->children, old_link); - priv->children = g_list_insert_before (priv->children, new_link, child_info); - - gtk_container_child_notify_by_pspec (GTK_CONTAINER (stack), child, stack_child_props[CHILD_PROP_POSITION]); -} - - static inline gboolean is_left_transition (GtkStackTransitionType transition_type) { @@ -1028,6 +1116,8 @@ set_visible_child (GtkStack *stack, GtkWidget *toplevel; GtkWidget *focus; gboolean contains_focus = FALSE; + guint old_pos = 0; + guint new_pos = 0; /* if we are being destroyed, do not bother with transitions * and notifications @@ -1052,6 +1142,22 @@ set_visible_child (GtkStack *stack, if (child_info == priv->visible_child) return; + if (child_info == NULL) + return; + + if (priv->pages) + { + guint position; + for (l = priv->children, position = 0; l != NULL; l = l->next, position++) + { + info = l->data; + if (info == priv->visible_child) + old_pos = position; + else if (info == child_info) + new_pos = position; + } + } + toplevel = gtk_widget_get_toplevel (widget); if (GTK_IS_WINDOW (toplevel)) { @@ -1145,6 +1251,11 @@ set_visible_child (GtkStack *stack, g_object_notify_by_pspec (G_OBJECT (stack), stack_props[PROP_VISIBLE_CHILD_NAME]); + if (priv->pages) + gtk_selection_model_selection_changed (priv->pages, + MIN (old_pos, new_pos), + MAX (old_pos, new_pos) - MIN (old_pos, new_pos) + 1); + gtk_stack_start_transition (stack, transition_type, transition_duration); } @@ -1281,11 +1392,12 @@ gtk_stack_add_page (GtkStack *stack, gtk_widget_set_child_visible (child_info->widget, FALSE); gtk_widget_set_parent (child_info->widget, GTK_WIDGET (stack)); + if (priv->pages) + g_list_model_items_changed (G_LIST_MODEL (priv->pages), g_list_length (priv->children) - 1, 0, 1); + g_signal_connect (child_info->widget, "notify::visible", G_CALLBACK (stack_child_visibility_notify_cb), stack); - g_object_notify_by_pspec (G_OBJECT (child_info), stack_child_props[CHILD_PROP_POSITION]); - if (priv->visible_child == NULL && gtk_widget_get_visible (child_info->widget)) set_visible_child (stack, child_info, priv->transition_type, priv->transition_duration); @@ -1295,10 +1407,10 @@ gtk_stack_add_page (GtkStack *stack, } static void -gtk_stack_remove (GtkContainer *container, - GtkWidget *child) +stack_remove (GtkStack *stack, + GtkWidget *child, + gboolean in_dispose) { - GtkStack *stack = GTK_STACK (container); GtkStackPrivate *priv = gtk_stack_get_instance_private (stack); GtkStackPage *child_info; gboolean was_visible; @@ -1308,7 +1420,7 @@ gtk_stack_remove (GtkContainer *container, return; priv->children = g_list_remove (priv->children, child_info); - + g_signal_handlers_disconnect_by_func (child, stack_child_visibility_notify_cb, stack); @@ -1318,7 +1430,12 @@ gtk_stack_remove (GtkContainer *container, child_info->widget = NULL; if (priv->visible_child == child_info) - set_visible_child (stack, NULL, priv->transition_type, priv->transition_duration); + { + if (in_dispose) + priv->visible_child = NULL; + else + set_visible_child (stack, NULL, priv->transition_type, priv->transition_duration); + } if (priv->last_visible_child == child_info) priv->last_visible_child = NULL; @@ -1327,10 +1444,33 @@ gtk_stack_remove (GtkContainer *container, g_object_unref (child_info); - if ((priv->hhomogeneous || priv->vhomogeneous) && was_visible) + if (!in_dispose && + (priv->hhomogeneous || priv->vhomogeneous) && + was_visible) gtk_widget_queue_resize (GTK_WIDGET (stack)); } +static void +gtk_stack_remove (GtkContainer *container, + GtkWidget *child) +{ + GtkStackPrivate *priv = gtk_stack_get_instance_private (GTK_STACK (container)); + GList *l; + guint position; + + for (l = priv->children, position = 0; l; l = l->next, position++) + { + GtkStackPage *page = l->data; + if (page->widget == child) + break; + } + + stack_remove (GTK_STACK (container), child, FALSE); + + if (priv->pages) + g_list_model_items_changed (G_LIST_MODEL (priv->pages), position, 1, 0); +} + /** * gtk_stack_get_page: * @stack: a #GtkStack @@ -2294,3 +2434,30 @@ gtk_stack_init (GtkStack *stack) priv->transition_duration = 200; priv->transition_type = GTK_STACK_TRANSITION_TYPE_NONE; } + +/** + * gtk_stack_get_pages: + * @stack: a #GtkStack + * + * Returns a #GListModel that contains the pages of the stack, + * and can be used to keep and up-to-date view. The model also + * implements #GtkSelectionModel and can be used to track and + * modify the visible page.. + * + * Returns: (transfer full): a #GtkSelectionModel for the stack's children + */ +GtkSelectionModel * +gtk_stack_get_pages (GtkStack *stack) +{ + GtkStackPrivate *priv = gtk_stack_get_instance_private (stack); + + g_return_val_if_fail (GTK_IS_STACK (stack), NULL); + + if (priv->pages) + return g_object_ref (priv->pages); + + priv->pages = GTK_SELECTION_MODEL (gtk_stack_pages_new (stack)); + g_object_add_weak_pointer (G_OBJECT (priv->pages), (gpointer *)&priv->pages); + + return priv->pages; +} diff --git a/gtk/gtkstack.h b/gtk/gtkstack.h index 6379328041..658ef8bb6a 100644 --- a/gtk/gtkstack.h +++ b/gtk/gtkstack.h @@ -27,6 +27,7 @@ #endif #include +#include G_BEGIN_DECLS @@ -155,6 +156,10 @@ void gtk_stack_set_interpolate_size (GtkStack *stack, gboolean interpolate_size); GDK_AVAILABLE_IN_ALL gboolean gtk_stack_get_interpolate_size (GtkStack *stack); + +GDK_AVAILABLE_IN_ALL +GtkSelectionModel * gtk_stack_get_pages (GtkStack *stack); + G_END_DECLS #endif diff --git a/gtk/gtkstacksidebar.c b/gtk/gtkstacksidebar.c index bf1b13759d..4ac5c7f713 100644 --- a/gtk/gtkstacksidebar.c +++ b/gtk/gtkstacksidebar.c @@ -28,6 +28,7 @@ #include "gtkscrolledwindow.h" #include "gtkseparator.h" #include "gtkstylecontext.h" +#include "gtkselectionmodel.h" #include "gtkprivate.h" #include "gtkintl.h" @@ -58,8 +59,8 @@ struct _GtkStackSidebarPrivate { GtkListBox *list; GtkStack *stack; + GtkSelectionModel *pages; GHashTable *rows; - gboolean in_child_changed; }; G_DEFINE_TYPE_WITH_PRIVATE (GtkStackSidebar, gtk_stack_sidebar, GTK_TYPE_BIN) @@ -129,30 +130,14 @@ sort_list (GtkListBoxRow *row1, GtkListBoxRow *row2, gpointer userdata) { - GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (userdata); - GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); - GtkWidget *item; - GtkWidget *widget; gint left = 0; gint right = 0; if (row1) - { - item = gtk_bin_get_child (GTK_BIN (row1)); - widget = g_object_get_data (G_OBJECT (item), "stack-child"); - g_object_get (gtk_stack_get_page (GTK_STACK (priv->stack), widget), - "position", &left, - NULL); - } + left = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (row1), "child-index")); if (row2) - { - item = gtk_bin_get_child (GTK_BIN (row2)); - widget = g_object_get_data (G_OBJECT (item), "stack-child"); - g_object_get (gtk_stack_get_page (GTK_STACK (priv->stack), widget), - "position", &right, - NULL); - } + right = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (row2), "child-index")); if (left < right) return -1; @@ -170,18 +155,13 @@ gtk_stack_sidebar_row_selected (GtkListBox *box, { GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (userdata); GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); - GtkWidget *item; - GtkWidget *widget; + guint index; - if (priv->in_child_changed) + if (row == NULL) return; - if (!row) - return; - - item = gtk_bin_get_child (GTK_BIN (row)); - widget = g_object_get_data (G_OBJECT (item), "stack-child"); - gtk_stack_set_visible_child (priv->stack, widget); + index = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (row), "child-index")); + gtk_selection_model_select_item (priv->pages, index, TRUE); } static void @@ -218,24 +198,25 @@ gtk_stack_sidebar_init (GtkStackSidebar *sidebar) static void update_row (GtkStackSidebar *sidebar, - GtkWidget *widget, + GtkStackPage *page, GtkWidget *row) { - GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); GtkWidget *item; gchar *title; gboolean needs_attention; + gboolean visible; GtkStyleContext *context; - g_object_get (gtk_stack_get_page (GTK_STACK (priv->stack), widget), + g_object_get (page, "title", &title, "needs-attention", &needs_attention, + "visible", &visible, NULL); item = gtk_bin_get_child (GTK_BIN (row)); gtk_label_set_text (GTK_LABEL (item), title); - gtk_widget_set_visible (row, gtk_widget_get_visible (widget) && title != NULL); + gtk_widget_set_visible (row, visible && title != NULL); context = gtk_widget_get_style_context (row); if (needs_attention) @@ -247,29 +228,19 @@ update_row (GtkStackSidebar *sidebar, } static void -on_position_updated (GtkWidget *widget, - GParamSpec *pspec, - GtkStackSidebar *sidebar) -{ - GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); - - gtk_list_box_invalidate_sort (priv->list); -} - -static void -on_child_updated (GtkWidget *widget, - GParamSpec *pspec, - GtkStackSidebar *sidebar) +on_page_updated (GtkStackPage *page, + GParamSpec *pspec, + GtkStackSidebar *sidebar) { GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); GtkWidget *row; - row = g_hash_table_lookup (priv->rows, widget); - update_row (sidebar, widget, row); + row = g_hash_table_lookup (priv->rows, page); + update_row (sidebar, page, row); } static void -add_child (GtkWidget *widget, +add_child (guint position, GtkStackSidebar *sidebar) { GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); @@ -277,118 +248,92 @@ add_child (GtkWidget *widget, GtkWidget *row; GtkStackPage *page; - /* Check we don't actually already know about this widget */ - if (g_hash_table_lookup (priv->rows, widget)) - return; - /* Make a pretty item when we add kids */ item = gtk_label_new (""); gtk_widget_set_halign (item, GTK_ALIGN_START); gtk_widget_set_valign (item, GTK_ALIGN_CENTER); row = gtk_list_box_row_new (); gtk_container_add (GTK_CONTAINER (row), item); - gtk_widget_show (item); - update_row (sidebar, widget, row); + page = g_list_model_get_item (G_LIST_MODEL (priv->pages), position); + update_row (sidebar, page, row); - /* Hook up for events */ - page = gtk_stack_get_page (GTK_STACK (priv->stack), widget); - g_signal_connect (widget, "notify::visible", - G_CALLBACK (on_child_updated), sidebar); - g_signal_connect (page, "notify::title", - G_CALLBACK (on_child_updated), sidebar); - g_signal_connect (page, "notify::needs-attention", - G_CALLBACK (on_child_updated), sidebar); - g_signal_connect (page, "notify::position", - G_CALLBACK (on_position_updated), sidebar); - - g_object_set_data (G_OBJECT (item), I_("stack-child"), widget); - g_hash_table_insert (priv->rows, widget, row); gtk_container_add (GTK_CONTAINER (priv->list), row); -} -static void -remove_child (GtkWidget *widget, - GtkStackSidebar *sidebar) -{ - GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); - GtkWidget *row; + g_object_set_data (G_OBJECT (row), "child-index", GUINT_TO_POINTER (position)); + if (gtk_selection_model_is_selected (priv->pages, position)) + gtk_list_box_select_row (priv->list, GTK_LIST_BOX_ROW (row)); + else + gtk_list_box_unselect_row (priv->list, GTK_LIST_BOX_ROW (row)); - row = g_hash_table_lookup (priv->rows, widget); - if (!row) - return; + g_signal_connect (page, "notify", G_CALLBACK (on_page_updated), sidebar); - if (priv->stack) - { - GtkStackPage *page = gtk_stack_get_page (GTK_STACK (priv->stack), widget); - if (page) - { - g_signal_handlers_disconnect_by_func (page, on_child_updated, sidebar); - g_signal_handlers_disconnect_by_func (page, on_position_updated, sidebar); - } - } + g_hash_table_insert (priv->rows, page, row); - gtk_container_remove (GTK_CONTAINER (priv->list), row); - g_hash_table_remove (priv->rows, widget); + g_object_unref (page); } static void populate_sidebar (GtkStackSidebar *sidebar) { GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); - GtkWidget *widget, *row; + guint i; - gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)add_child, sidebar); - - widget = gtk_stack_get_visible_child (priv->stack); - if (widget) - { - row = g_hash_table_lookup (priv->rows, widget); - gtk_list_box_select_row (priv->list, GTK_LIST_BOX_ROW (row)); - } + for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (priv->pages)); i++) + add_child (i, sidebar); } static void clear_sidebar (GtkStackSidebar *sidebar) { GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); - - gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)remove_child, sidebar); -} - -static void -on_child_changed (GtkWidget *widget, - GParamSpec *pspec, - GtkStackSidebar *sidebar) -{ - GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); - GtkWidget *child; + GHashTableIter iter; + GtkStackPage *page; GtkWidget *row; - child = gtk_stack_get_visible_child (GTK_STACK (widget)); - row = g_hash_table_lookup (priv->rows, child); - if (row != NULL) + g_hash_table_iter_init (&iter, priv->rows); + while (g_hash_table_iter_next (&iter, (gpointer *)&page, (gpointer *)&row)) { - priv->in_child_changed = TRUE; - gtk_list_box_select_row (priv->list, GTK_LIST_BOX_ROW (row)); - priv->in_child_changed = FALSE; + gtk_container_remove (GTK_CONTAINER (priv->list), row); + g_hash_table_iter_remove (&iter); + g_signal_handlers_disconnect_by_func (page, on_page_updated, sidebar); } } static void -on_stack_child_added (GtkContainer *container, - GtkWidget *widget, - GtkStackSidebar *sidebar) +items_changed_cb (GListModel *model, + guint position, + guint removed, + guint added, + GtkStackSidebar *sidebar) { - add_child (widget, sidebar); + /* FIXME: we can do better */ + clear_sidebar (sidebar); + populate_sidebar (sidebar); } static void -on_stack_child_removed (GtkContainer *container, - GtkWidget *widget, - GtkStackSidebar *sidebar) +selection_changed_cb (GtkSelectionModel *model, + guint position, + guint n_items, + GtkStackSidebar *sidebar) { - remove_child (widget, sidebar); + GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); + guint i; + + for (i = position; i < position + n_items; i++) + { + GtkStackPage *page; + GtkWidget *row; + + page = g_list_model_get_item (G_LIST_MODEL (priv->pages), i); + row = g_hash_table_lookup (priv->rows, page); + if (gtk_selection_model_is_selected (priv->pages, i)) + gtk_list_box_select_row (priv->list, GTK_LIST_BOX_ROW (row)); + else + gtk_list_box_unselect_row (priv->list, GTK_LIST_BOX_ROW (row)); + g_object_unref (page); + } } static void @@ -396,10 +341,8 @@ disconnect_stack_signals (GtkStackSidebar *sidebar) { GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); - g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_added, sidebar); - g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_removed, sidebar); - g_signal_handlers_disconnect_by_func (priv->stack, on_child_changed, sidebar); - g_signal_handlers_disconnect_by_func (priv->stack, disconnect_stack_signals, sidebar); + g_signal_handlers_disconnect_by_func (priv->pages, items_changed_cb, sidebar); + g_signal_handlers_disconnect_by_func (priv->pages, selection_changed_cb, sidebar); } static void @@ -407,14 +350,37 @@ connect_stack_signals (GtkStackSidebar *sidebar) { GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); - g_signal_connect_after (priv->stack, "add", - G_CALLBACK (on_stack_child_added), sidebar); - g_signal_connect_after (priv->stack, "remove", - G_CALLBACK (on_stack_child_removed), sidebar); - g_signal_connect (priv->stack, "notify::visible-child", - G_CALLBACK (on_child_changed), sidebar); - g_signal_connect_swapped (priv->stack, "destroy", - G_CALLBACK (disconnect_stack_signals), sidebar); + g_signal_connect (priv->pages, "items-changed", G_CALLBACK (items_changed_cb), sidebar); + g_signal_connect (priv->pages, "selection-changed", G_CALLBACK (selection_changed_cb), sidebar); +} + +static void +set_stack (GtkStackSidebar *sidebar, + GtkStack *stack) +{ + GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); + + if (stack) + { + priv->stack = g_object_ref (stack); + priv->pages = gtk_stack_get_pages (stack); + populate_sidebar (sidebar); + connect_stack_signals (sidebar); + } +} + +static void +unset_stack (GtkStackSidebar *sidebar) +{ + GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); + + if (priv->stack) + { + disconnect_stack_signals (sidebar); + clear_sidebar (sidebar); + g_clear_object (&priv->stack); + g_clear_object (&priv->pages); + } } static void @@ -422,7 +388,7 @@ gtk_stack_sidebar_dispose (GObject *object) { GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (object); - gtk_stack_sidebar_set_stack (sidebar, NULL); + unset_stack (sidebar); G_OBJECT_CLASS (gtk_stack_sidebar_parent_class)->dispose (object); } @@ -487,28 +453,17 @@ void gtk_stack_sidebar_set_stack (GtkStackSidebar *sidebar, GtkStack *stack) { - GtkStackSidebarPrivate *priv; + GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar); g_return_if_fail (GTK_IS_STACK_SIDEBAR (sidebar)); g_return_if_fail (GTK_IS_STACK (stack) || stack == NULL); - priv = gtk_stack_sidebar_get_instance_private (sidebar); if (priv->stack == stack) return; - if (priv->stack) - { - disconnect_stack_signals (sidebar); - clear_sidebar (sidebar); - g_clear_object (&priv->stack); - } - if (stack) - { - priv->stack = g_object_ref (stack); - populate_sidebar (sidebar); - connect_stack_signals (sidebar); - } + unset_stack (sidebar); + set_stack (sidebar, stack); gtk_widget_queue_resize (GTK_WIDGET (sidebar)); diff --git a/gtk/gtkstackswitcher.c b/gtk/gtkstackswitcher.c index ab9d91b27d..e088ba1cca 100644 --- a/gtk/gtkstackswitcher.c +++ b/gtk/gtkstackswitcher.c @@ -29,6 +29,7 @@ #include "gtkwidgetprivate.h" #include "gtktypebuiltins.h" #include "gtkimage.h" +#include "gtkselectionmodel.h" /** * SECTION:gtkstackswitcher @@ -66,8 +67,8 @@ typedef struct _GtkStackSwitcherPrivate GtkStackSwitcherPrivate; struct _GtkStackSwitcherPrivate { GtkStack *stack; + GtkSelectionModel *pages; GHashTable *buttons; - gboolean in_child_changed; GtkWidget *switch_button; guint switch_timer; }; @@ -89,11 +90,9 @@ gtk_stack_switcher_init (GtkStackSwitcher *switcher) priv = gtk_stack_switcher_get_instance_private (switcher); - priv->stack = NULL; - priv->buttons = g_hash_table_new (g_direct_hash, g_direct_equal); + priv->buttons = g_hash_table_new_full (g_direct_hash, g_direct_equal, g_object_unref, NULL); context = gtk_widget_get_style_context (GTK_WIDGET (switcher)); - gtk_style_context_add_class (context, "stack-switcher"); gtk_style_context_add_class (context, GTK_STYLE_CLASS_LINKED); gtk_orientable_set_orientation (GTK_ORIENTABLE (switcher), GTK_ORIENTATION_HORIZONTAL); @@ -103,18 +102,25 @@ gtk_stack_switcher_init (GtkStackSwitcher *switcher) } static void -on_button_clicked (GtkWidget *widget, +on_button_toggled (GtkWidget *button, + GParamSpec *pspec, GtkStackSwitcher *self) { - GtkWidget *child; - GtkStackSwitcherPrivate *priv; + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (self); + gboolean active; + guint index; - priv = gtk_stack_switcher_get_instance_private (self); + active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)); + index = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (button), "child-index")); - if (!priv->in_child_changed) + if (active) { - child = g_object_get_data (G_OBJECT (widget), "stack-child"); - gtk_stack_set_visible_child (priv->stack, child); + gtk_selection_model_select_item (priv->pages, index, TRUE); + } + else + { + gboolean selected = gtk_selection_model_is_selected (priv->pages, index); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), selected); } } @@ -161,25 +167,25 @@ rebuild_child (GtkWidget *self, static void update_button (GtkStackSwitcher *self, - GtkWidget *widget, + GtkStackPage *page, GtkWidget *button) { gchar *title; gchar *icon_name; gboolean needs_attention; - GtkStackSwitcherPrivate *priv; + gboolean visible; GtkStyleContext *context; - priv = gtk_stack_switcher_get_instance_private (self); - g_object_get (gtk_stack_get_page (priv->stack, widget), + g_object_get (page, "title", &title, "icon-name", &icon_name, "needs-attention", &needs_attention, + "visible", &visible, NULL); rebuild_child (button, icon_name, title); - gtk_widget_set_visible (button, gtk_widget_get_visible (widget) && (title != NULL || icon_name != NULL)); + gtk_widget_set_visible (button, visible && (title != NULL || icon_name != NULL)); context = gtk_widget_get_style_context (button); if (needs_attention) @@ -192,88 +198,21 @@ update_button (GtkStackSwitcher *self, } static void -on_visible_updated (GtkWidget *widget, - GParamSpec *pspec, - GtkStackSwitcher *self) +on_page_updated (GtkStackPage *page, + GParamSpec *pspec, + GtkStackSwitcher *self) { + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (self); GtkWidget *button; - GtkStackSwitcherPrivate *priv; - priv = gtk_stack_switcher_get_instance_private (self); - - button = g_hash_table_lookup (priv->buttons, widget); - update_button (self, widget, button); -} - -static void -on_title_icon_updated (GtkStackPage *page, - GParamSpec *pspec, - GtkStackSwitcher *self) -{ - GtkWidget *widget; - GtkWidget *button; - GtkStackSwitcherPrivate *priv; - - priv = gtk_stack_switcher_get_instance_private (self); - - widget = gtk_stack_page_get_child (page); - button = g_hash_table_lookup (priv->buttons, widget); - update_button (self, widget, button); -} - -static void -on_position_updated (GtkStackPage *page, - GParamSpec *pspec, - GtkStackSwitcher *self) -{ - GtkWidget *widget; - GtkWidget *button; - gint position; - GtkStackSwitcherPrivate *priv; - - priv = gtk_stack_switcher_get_instance_private (self); - - widget = gtk_stack_page_get_child (page); - button = g_hash_table_lookup (priv->buttons, widget); - - gtk_container_child_get (GTK_CONTAINER (priv->stack), widget, - "position", &position, - NULL); - - if (position == 0) - gtk_box_reorder_child_after (GTK_BOX (self), button, NULL); - else - { - GtkWidget *sibling = gtk_widget_get_first_child (GTK_WIDGET (self)); - int i; - for (i = 1; i < position; i++) - sibling = gtk_widget_get_next_sibling (sibling); - gtk_box_reorder_child_after (GTK_BOX (self), button, sibling); - } -} - -static void -on_needs_attention_updated (GtkStackPage *page, - GParamSpec *pspec, - GtkStackSwitcher *self) -{ - GtkWidget *widget; - GtkWidget *button; - GtkStackSwitcherPrivate *priv; - - priv = gtk_stack_switcher_get_instance_private (self); - - widget = gtk_stack_page_get_child (page); - button = g_hash_table_lookup (priv->buttons, widget); - update_button (self, widget, button); + button = g_hash_table_lookup (priv->buttons, page); + update_button (self, page, button); } static void remove_switch_timer (GtkStackSwitcher *self) { - GtkStackSwitcherPrivate *priv; - - priv = gtk_stack_switcher_get_instance_private (self); + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (self); if (priv->switch_timer) { @@ -286,11 +225,9 @@ static gboolean gtk_stack_switcher_switch_timeout (gpointer data) { GtkStackSwitcher *self = data; - GtkStackSwitcherPrivate *priv; + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (self); GtkWidget *button; - priv = gtk_stack_switcher_get_instance_private (self); - priv->switch_timer = 0; button = priv->switch_button; @@ -309,14 +246,12 @@ gtk_stack_switcher_drag_motion (GtkWidget *widget, gint y) { GtkStackSwitcher *self = GTK_STACK_SWITCHER (widget); - GtkStackSwitcherPrivate *priv; + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (self); GtkWidget *button; GHashTableIter iter; gpointer value; gboolean retval = FALSE; - priv = gtk_stack_switcher_get_instance_private (self); - button = NULL; g_hash_table_iter_init (&iter, priv->buttons); while (g_hash_table_iter_next (&iter, NULL, &value)) @@ -355,160 +290,143 @@ gtk_stack_switcher_drag_leave (GtkWidget *widget, } static void -add_child (GtkWidget *widget, +add_child (guint position, GtkStackSwitcher *self) { + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (self); GtkWidget *button; - GList *group; - GtkStackSwitcherPrivate *priv; + gboolean selected; GtkStackPage *page; - priv = gtk_stack_switcher_get_instance_private (self); - - button = gtk_radio_button_new (NULL); - + button = gtk_toggle_button_new (); gtk_widget_set_focus_on_click (button, FALSE); - gtk_check_button_set_draw_indicator (GTK_CHECK_BUTTON (button), FALSE); - page = gtk_stack_get_page (GTK_STACK (priv->stack), widget); - update_button (self, widget, button); - - group = gtk_container_get_children (GTK_CONTAINER (self)); - if (group != NULL) - { - gtk_radio_button_join_group (GTK_RADIO_BUTTON (button), GTK_RADIO_BUTTON (group->data)); - g_list_free (group); - } + page = g_list_model_get_item (G_LIST_MODEL (priv->pages), position); + update_button (self, page, button); gtk_container_add (GTK_CONTAINER (self), button); - g_object_set_data (G_OBJECT (button), "stack-child", widget); - g_signal_connect (button, "clicked", G_CALLBACK (on_button_clicked), self); - g_signal_connect (widget, "notify::visible", G_CALLBACK (on_visible_updated), self); - g_signal_connect (page, "notify::title", G_CALLBACK (on_title_icon_updated), self); - g_signal_connect (page, "notify::icon-name", G_CALLBACK (on_title_icon_updated), self); - g_signal_connect (page, "notify::position", G_CALLBACK (on_position_updated), self); - g_signal_connect (page, "notify::needs-attention", G_CALLBACK (on_needs_attention_updated), self); + g_object_set_data (G_OBJECT (button), "child-index", GUINT_TO_POINTER (position)); + selected = gtk_selection_model_is_selected (priv->pages, position); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), selected); - g_hash_table_insert (priv->buttons, widget, button); -} + g_signal_connect (button, "notify::active", G_CALLBACK (on_button_toggled), self); + g_signal_connect (page, "notify", G_CALLBACK (on_page_updated), self); -static void -remove_child (GtkWidget *widget, - GtkStackSwitcher *self) -{ - GtkWidget *button; - GtkStackSwitcherPrivate *priv; + g_hash_table_insert (priv->buttons, g_object_ref (page), button); - priv = gtk_stack_switcher_get_instance_private (self); - - if (priv->stack) - { - GtkStackPage *page = gtk_stack_get_page (priv->stack, widget); - if (page) - { - g_signal_handlers_disconnect_by_func (page, on_title_icon_updated, self); - g_signal_handlers_disconnect_by_func (page, on_position_updated, self); - g_signal_handlers_disconnect_by_func (page, on_needs_attention_updated, self); - } - g_signal_handlers_disconnect_by_func (widget, on_visible_updated, self); - } - button = g_hash_table_lookup (priv->buttons, widget); - gtk_container_remove (GTK_CONTAINER (self), button); - g_hash_table_remove (priv->buttons, widget); + g_object_unref (page); } static void populate_switcher (GtkStackSwitcher *self) { - GtkStackSwitcherPrivate *priv; - GtkWidget *widget, *button; + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (self); + guint i; - priv = gtk_stack_switcher_get_instance_private (self); - gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)add_child, self); - - widget = gtk_stack_get_visible_child (priv->stack); - if (widget) - { - button = g_hash_table_lookup (priv->buttons, widget); - priv->in_child_changed = TRUE; - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE); - priv->in_child_changed = FALSE; - } + for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (priv->pages)); i++) + add_child (i, self); } static void clear_switcher (GtkStackSwitcher *self) { - GtkStackSwitcherPrivate *priv; - - priv = gtk_stack_switcher_get_instance_private (self); - gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)remove_child, self); -} - -static void -on_child_changed (GtkWidget *widget, - GParamSpec *pspec, - GtkStackSwitcher *self) -{ - GtkWidget *child; + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (self); + GHashTableIter iter; + GtkWidget *page; GtkWidget *button; - GtkStackSwitcherPrivate *priv; - priv = gtk_stack_switcher_get_instance_private (self); - - child = gtk_stack_get_visible_child (GTK_STACK (widget)); - button = g_hash_table_lookup (priv->buttons, child); - if (button != NULL) + g_hash_table_iter_init (&iter, priv->buttons); + while (g_hash_table_iter_next (&iter, (gpointer *)&page, (gpointer *)&button)) { - priv->in_child_changed = TRUE; - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE); - priv->in_child_changed = FALSE; + gtk_container_remove (GTK_CONTAINER (self), button); + g_signal_handlers_disconnect_by_func (page, on_page_updated, self); + g_hash_table_iter_remove (&iter); } } static void -on_stack_child_added (GtkContainer *container, - GtkWidget *widget, - GtkStackSwitcher *self) +items_changed_cb (GListModel *model, + guint position, + guint removed, + guint added, + GtkStackSwitcher *switcher) { - add_child (widget, self); + clear_switcher (switcher); + populate_switcher (switcher); } static void -on_stack_child_removed (GtkContainer *container, - GtkWidget *widget, - GtkStackSwitcher *self) +selection_changed_cb (GtkSelectionModel *model, + guint position, + guint n_items, + GtkStackSwitcher *switcher) { - remove_child (widget, self); + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (switcher); + guint i; + + for (i = position; i < position + n_items; i++) + { + GtkStackPage *page; + GtkWidget *button; + gboolean selected; + + page = g_list_model_get_item (G_LIST_MODEL (priv->pages), i); + button = g_hash_table_lookup (priv->buttons, page); + if (button) + { + selected = gtk_selection_model_is_selected (priv->pages, i); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), selected); + } + g_object_unref (page); + } } static void disconnect_stack_signals (GtkStackSwitcher *switcher) { - GtkStackSwitcherPrivate *priv; + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (switcher); - priv = gtk_stack_switcher_get_instance_private (switcher); - g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_added, switcher); - g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_removed, switcher); - g_signal_handlers_disconnect_by_func (priv->stack, on_child_changed, switcher); - g_signal_handlers_disconnect_by_func (priv->stack, disconnect_stack_signals, switcher); + g_signal_handlers_disconnect_by_func (priv->pages, items_changed_cb, switcher); + g_signal_handlers_disconnect_by_func (priv->pages, selection_changed_cb, switcher); } static void connect_stack_signals (GtkStackSwitcher *switcher) { - GtkStackSwitcherPrivate *priv; + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (switcher); - priv = gtk_stack_switcher_get_instance_private (switcher); - g_signal_connect_after (priv->stack, "add", - G_CALLBACK (on_stack_child_added), switcher); - g_signal_connect_after (priv->stack, "remove", - G_CALLBACK (on_stack_child_removed), switcher); - g_signal_connect (priv->stack, "notify::visible-child", - G_CALLBACK (on_child_changed), switcher); - g_signal_connect_swapped (priv->stack, "destroy", - G_CALLBACK (disconnect_stack_signals), switcher); + g_signal_connect (priv->pages, "items-changed", G_CALLBACK (items_changed_cb), switcher); + g_signal_connect (priv->pages, "selection-changed", G_CALLBACK (selection_changed_cb), switcher); +} + +static void +set_stack (GtkStackSwitcher *switcher, + GtkStack *stack) +{ + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (switcher); + + if (stack) + { + priv->stack = g_object_ref (stack); + priv->pages = gtk_stack_get_pages (stack); + populate_switcher (switcher); + connect_stack_signals (switcher); + } +} + +static void +unset_stack (GtkStackSwitcher *switcher) +{ + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (switcher); + + if (priv->stack) + { + disconnect_stack_signals (switcher); + clear_switcher (switcher); + g_clear_object (&priv->stack); + g_clear_object (&priv->pages); + } } /** @@ -522,28 +440,16 @@ void gtk_stack_switcher_set_stack (GtkStackSwitcher *switcher, GtkStack *stack) { - GtkStackSwitcherPrivate *priv; + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (switcher); g_return_if_fail (GTK_IS_STACK_SWITCHER (switcher)); g_return_if_fail (GTK_IS_STACK (stack) || stack == NULL); - priv = gtk_stack_switcher_get_instance_private (switcher); - if (priv->stack == stack) return; - if (priv->stack) - { - disconnect_stack_signals (switcher); - clear_switcher (switcher); - g_clear_object (&priv->stack); - } - if (stack) - { - priv->stack = g_object_ref (stack); - populate_switcher (switcher); - connect_stack_signals (switcher); - } + unset_stack (switcher); + set_stack (switcher, stack); gtk_widget_queue_resize (GTK_WIDGET (switcher)); @@ -618,7 +524,7 @@ gtk_stack_switcher_dispose (GObject *object) GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object); remove_switch_timer (switcher); - gtk_stack_switcher_set_stack (switcher, NULL); + unset_stack (switcher); G_OBJECT_CLASS (gtk_stack_switcher_parent_class)->dispose (object); } @@ -627,9 +533,7 @@ static void gtk_stack_switcher_finalize (GObject *object) { GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object); - GtkStackSwitcherPrivate *priv; - - priv = gtk_stack_switcher_get_instance_private (switcher); + GtkStackSwitcherPrivate *priv = gtk_stack_switcher_get_instance_private (switcher); g_hash_table_destroy (priv->buttons); diff --git a/gtk/meson.build b/gtk/meson.build index c700b37289..3ebb1b5867 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -323,6 +323,7 @@ gtk_public_sources = files([ 'gtksearchbar.c', 'gtksearchentry.c', 'gtkselection.c', + 'gtkselectionmodel.c', 'gtkseparator.c', 'gtkseparatormenuitem.c', 'gtkseparatortoolitem.c', @@ -334,6 +335,7 @@ gtk_public_sources = files([ 'gtkshortcutswindow.c', 'gtkshow.c', 'gtksidebarrow.c', + 'gtksingleselection.c', 'gtksizegroup.c', 'gtksizerequest.c', 'gtkslicelistmodel.c', @@ -557,6 +559,7 @@ gtk_public_headers = files([ 'gtksearchbar.h', 'gtksearchentry.h', 'gtkselection.h', + 'gtkselectionmodel.h', 'gtkseparator.h', 'gtkseparatormenuitem.h', 'gtkseparatortoolitem.h', @@ -567,6 +570,7 @@ gtk_public_headers = files([ 'gtkshortcutsshortcut.h', 'gtkshortcutswindow.h', 'gtkshow.h', + 'gtksingleselection.h', 'gtksizegroup.h', 'gtksizerequest.h', 'gtkslicelistmodel.h', diff --git a/testsuite/a11y/about.txt b/testsuite/a11y/about.txt index 5dd586cc95..fdb3ab1e2c 100644 --- a/testsuite/a11y/about.txt +++ b/testsuite/a11y/about.txt @@ -370,15 +370,11 @@ See the GNU General Public License, version 3 or later for details. layer: widget alpha: 1 - unnamed-GtkRadioButtonAccessible-2 - "radio button" + unnamed-GtkToggleButtonAccessible-2 + "toggle button" parent: stack_switcher index: 0 name: About - member-of: unnamed-GtkRadioButtonAccessible-3 - unnamed-GtkRadioButtonAccessible-4 - unnamed-GtkRadioButtonAccessible-5 - unnamed-GtkRadioButtonAccessible-2 state: checked enabled focusable sensitive visible toolkit: gtk @@ -390,15 +386,11 @@ See the GNU General Public License, version 3 or later for details. action 0 name: click action 0 description: Clicks the button - unnamed-GtkRadioButtonAccessible-5 - "radio button" + unnamed-GtkToggleButtonAccessible-3 + "toggle button" parent: stack_switcher index: 1 name: Credits - member-of: unnamed-GtkRadioButtonAccessible-3 - unnamed-GtkRadioButtonAccessible-4 - unnamed-GtkRadioButtonAccessible-5 - unnamed-GtkRadioButtonAccessible-2 state: enabled focusable sensitive toolkit: gtk @@ -410,15 +402,11 @@ See the GNU General Public License, version 3 or later for details. action 0 name: click action 0 description: Clicks the button - unnamed-GtkRadioButtonAccessible-4 - "radio button" + unnamed-GtkToggleButtonAccessible-4 + "toggle button" parent: stack_switcher index: 2 name: License - member-of: unnamed-GtkRadioButtonAccessible-3 - unnamed-GtkRadioButtonAccessible-4 - unnamed-GtkRadioButtonAccessible-5 - unnamed-GtkRadioButtonAccessible-2 state: enabled focusable sensitive toolkit: gtk @@ -430,15 +418,11 @@ See the GNU General Public License, version 3 or later for details. action 0 name: click action 0 description: Clicks the button - unnamed-GtkRadioButtonAccessible-3 - "radio button" + unnamed-GtkToggleButtonAccessible-5 + "toggle button" parent: stack_switcher index: 3 name: System - member-of: unnamed-GtkRadioButtonAccessible-3 - unnamed-GtkRadioButtonAccessible-4 - unnamed-GtkRadioButtonAccessible-5 - unnamed-GtkRadioButtonAccessible-2 state: enabled focusable sensitive toolkit: gtk diff --git a/testsuite/a11y/stack.txt b/testsuite/a11y/stack.txt index 5a82eea477..bb07325586 100644 --- a/testsuite/a11y/stack.txt +++ b/testsuite/a11y/stack.txt @@ -25,13 +25,11 @@ window1 layer: widget alpha: 1 - unnamed-GtkRadioButtonAccessible-0 - "radio button" + unnamed-GtkToggleButtonAccessible-0 + "toggle button" parent: stackswitcher1 index: 0 name: Page 1 - member-of: unnamed-GtkRadioButtonAccessible-1 - unnamed-GtkRadioButtonAccessible-0 state: checked enabled focusable sensitive showing visible toolkit: gtk @@ -43,13 +41,11 @@ window1 action 0 name: click action 0 description: Clicks the button - unnamed-GtkRadioButtonAccessible-1 - "radio button" + unnamed-GtkToggleButtonAccessible-1 + "toggle button" parent: stackswitcher1 index: 1 name: Page 2 - member-of: unnamed-GtkRadioButtonAccessible-1 - unnamed-GtkRadioButtonAccessible-0 state: enabled focusable sensitive showing visible toolkit: gtk diff --git a/testsuite/gtk/meson.build b/testsuite/gtk/meson.build index e4e9eec959..c02e4ab177 100644 --- a/testsuite/gtk/meson.build +++ b/testsuite/gtk/meson.build @@ -44,6 +44,7 @@ tests = [ ['regression-tests'], ['scrolledwindow'], ['searchbar'], + ['singleselection'], ['slicelistmodel'], ['sortlistmodel'], ['spinbutton'], diff --git a/testsuite/gtk/notify.c b/testsuite/gtk/notify.c index 228195b2d3..20315b22f2 100644 --- a/testsuite/gtk/notify.c +++ b/testsuite/gtk/notify.c @@ -611,6 +611,12 @@ test_type (gconstpointer data) g_str_equal (pspec->name, "expanded")) continue; + /* can't select items without an underlying, populated model */ + if (g_type_is_a (type, GTK_TYPE_SINGLE_SELECTION) && + (g_str_equal (pspec->name, "selected") || + g_str_equal (pspec->name, "selected-item"))) + continue; + if (g_test_verbose ()) g_print ("Property %s.%s\n", g_type_name (pspec->owner_type), pspec->name); diff --git a/testsuite/gtk/singleselection.c b/testsuite/gtk/singleselection.c new file mode 100644 index 0000000000..efe4a91ece --- /dev/null +++ b/testsuite/gtk/singleselection.c @@ -0,0 +1,557 @@ +/* + * Copyright (C) 2019, Red Hat, Inc. + * Authors: Matthias Clasen + * + * 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 . + */ + +#include + +#include + +static GQuark number_quark; +static GQuark changes_quark; +static GQuark selection_quark; + +static guint +get (GListModel *model, + guint position) +{ + guint number; + GObject *object = g_list_model_get_item (model, position); + g_assert (object != NULL); + number = GPOINTER_TO_UINT (g_object_get_qdata (object, number_quark)); + g_object_unref (object); + return number; +} + +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 char * +selection_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 (!gtk_selection_model_is_selected (GTK_SELECTION_MODEL (model), i)) + continue; + + if (string->len > 0) + g_string_append (string, " "); + g_string_append_printf (string, "%u", get (model, i)); + } + + return g_string_free (string, FALSE); +} + +static GListStore * +new_store (guint start, + guint end, + guint step); + +static GObject * +make_object (guint number) +{ + GObject *object; + + /* 0 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)); + + return object; +} + +static void +splice (GListStore *store, + guint pos, + guint removed, + guint *numbers, + guint added) +{ + GObject *objects[added]; + guint i; + + for (i = 0; i < added; i++) + objects[i] = make_object (numbers[i]); + + g_list_store_splice (store, pos, removed, (gpointer *) objects, added); + + for (i = 0; i < added; i++) + g_object_unref (objects[i]); +} + +static void +add (GListStore *store, + guint number) +{ + GObject *object = make_object (number); + g_list_store_append (store, object); + g_object_unref (object); +} + +static void +insert (GListStore *store, + guint position, + guint number) +{ + GObject *object = make_object (number); + g_list_store_insert (store, position, object); + g_object_unref (object); +} + +#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 ignore_changes(model) G_STMT_START{ \ + GString *changes = g_object_get_qdata (G_OBJECT (model), changes_quark); \ + g_string_set_size (changes, 0); \ +}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 + +#define assert_selection(model, expected) G_STMT_START{ \ + char *s = selection_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_selection_changes(model, expected) G_STMT_START{ \ + GString *changes = g_object_get_qdata (G_OBJECT (model), selection_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 + +#define ignore_selection_changes(model) G_STMT_START{ \ + GString *changes = g_object_get_qdata (G_OBJECT (model), selection_quark); \ + 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, + guint step) +{ + GListStore *store = new_empty_store (); + guint i; + + for (i = start; i <= end; i += step) + 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 +selection_changed (GListModel *model, + guint position, + guint n_items, + GString *changes) +{ + if (changes->len) + g_string_append (changes, ", "); + + g_string_append_printf (changes, "%u:%u", position, n_items); +} + +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 GtkSelectionModel * +new_model (GListStore *store, gboolean autoselect, gboolean can_unselect) +{ + GtkSelectionModel *result; + GString *changes; + + result = GTK_SELECTION_MODEL (gtk_single_selection_new (G_LIST_MODEL (store))); + + /* We want to return an empty selection unless autoselect is true, + * so undo the initial selection due to autoselect defaulting to TRUE. + */ + gtk_single_selection_set_autoselect (GTK_SINGLE_SELECTION (result), FALSE); + gtk_single_selection_set_can_unselect (GTK_SINGLE_SELECTION (result), TRUE); + gtk_selection_model_unselect_item (result, 0); + assert_selection (result, ""); + + gtk_single_selection_set_autoselect (GTK_SINGLE_SELECTION (result), autoselect); + gtk_single_selection_set_can_unselect (GTK_SINGLE_SELECTION (result), can_unselect); + + changes = g_string_new (""); + g_object_set_qdata_full (G_OBJECT(result), changes_quark, changes, free_changes); + g_signal_connect (result, "items-changed", G_CALLBACK (items_changed), changes); + + changes = g_string_new (""); + g_object_set_qdata_full (G_OBJECT(result), selection_quark, changes, free_changes); + g_signal_connect (result, "selection-changed", G_CALLBACK (selection_changed), changes); + + return result; +} + +static void +test_create (void) +{ + GtkSelectionModel *selection; + GListStore *store; + + store = new_store (1, 5, 2); + selection = new_model (store, FALSE, FALSE); + g_assert_false (gtk_single_selection_get_autoselect (GTK_SINGLE_SELECTION (selection))); + + assert_model (selection, "1 3 5"); + assert_changes (selection, ""); + assert_selection (selection, ""); + assert_selection_changes (selection, ""); + + g_object_unref (store); + + assert_model (selection, "1 3 5"); + assert_changes (selection, ""); + assert_selection (selection, ""); + assert_selection_changes (selection, ""); + + g_object_unref (selection); +} + +static void +test_changes (void) +{ + GtkSelectionModel *selection; + GListStore *store; + + store = new_store (1, 5, 1); + selection = new_model (store, FALSE, FALSE); + assert_model (selection, "1 2 3 4 5"); + assert_changes (selection, ""); + assert_selection (selection, ""); + assert_selection_changes (selection, ""); + + g_list_store_remove (store, 3); + assert_model (selection, "1 2 3 5"); + assert_changes (selection, "-3"); + assert_selection (selection, ""); + assert_selection_changes (selection, ""); + + insert (store, 3, 99); + assert_model (selection, "1 2 3 99 5"); + assert_changes (selection, "+3"); + assert_selection (selection, ""); + assert_selection_changes (selection, ""); + + splice (store, 3, 2, (guint[]) { 97 }, 1); + assert_model (selection, "1 2 3 97"); + assert_changes (selection, "3-2+1"); + assert_selection (selection, ""); + assert_selection_changes (selection, ""); + + g_object_unref (selection); + g_object_unref (store); +} + +static void +test_selection (void) +{ + GtkSelectionModel *selection; + GListStore *store; + gboolean ret; + + store = new_store (1, 5, 1); + selection = new_model (store, TRUE, FALSE); + assert_selection (selection, "1"); + assert_selection_changes (selection, ""); + + ret = gtk_selection_model_select_item (selection, 3, FALSE); + g_assert_true (ret); + assert_selection (selection, "4"); + assert_selection_changes (selection, "0:4"); + + ret = gtk_selection_model_unselect_item (selection, 3); + g_assert_false (ret); + assert_selection (selection, "4"); + assert_selection_changes (selection, ""); + + ret = gtk_selection_model_select_item (selection, 1, FALSE); + g_assert_true (ret); + assert_selection (selection, "2"); + assert_selection_changes (selection, "1:3"); + + ret = gtk_selection_model_select_range (selection, 3, 2, FALSE); + g_assert_false (ret); + assert_selection (selection, "2"); + assert_selection_changes (selection, ""); + + ret = gtk_selection_model_unselect_range (selection, 4, 2); + g_assert_false (ret); + assert_selection (selection, "2"); + assert_selection_changes (selection, ""); + + ret = gtk_selection_model_select_all (selection); + g_assert_false (ret); + assert_selection (selection, "2"); + assert_selection_changes (selection, ""); + + ret = gtk_selection_model_unselect_all (selection); + g_assert_false (ret); + assert_selection (selection, "2"); + assert_selection_changes (selection, ""); + + g_object_unref (store); + g_object_unref (selection); +} + +static void +test_autoselect (void) +{ + GtkSelectionModel *selection; + GListStore *store; + + store = new_store (2, 1, 1); + selection = new_model (store, TRUE, FALSE); + assert_selection (selection, ""); + assert_selection_changes (selection, ""); + + add (store, 1); + assert_selection (selection, "1"); + assert_selection_changes (selection, ""); + + splice (store, 0, 1, (guint[]) { 97 }, 1); + assert_selection (selection, "97"); + assert_selection_changes (selection, "0:1"); + + gtk_single_selection_set_autoselect (GTK_SINGLE_SELECTION (selection), FALSE); + gtk_single_selection_set_can_unselect (GTK_SINGLE_SELECTION (selection), TRUE); + gtk_selection_model_unselect_item (selection, 0); + assert_selection (selection, ""); + assert_selection_changes (selection, "0:1"); + + gtk_single_selection_set_autoselect (GTK_SINGLE_SELECTION (selection), TRUE); + assert_selection (selection, "97"); + assert_selection_changes (selection, "0:1"); + + ignore_changes (selection); + + g_object_unref (store); + g_object_unref (selection); +} + +static void +test_can_unselect (void) +{ + GtkSelectionModel *selection; + GListStore *store; + gboolean ret; + + store = new_store (1, 5, 1); + selection = new_model (store, TRUE, FALSE); + assert_selection (selection, "1"); + assert_selection_changes (selection, ""); + + ret = gtk_selection_model_unselect_item (selection, 0); + g_assert_false (ret); + assert_selection (selection, "1"); + assert_selection_changes (selection, ""); + + gtk_single_selection_set_can_unselect (GTK_SINGLE_SELECTION (selection), TRUE); + + assert_selection (selection, "1"); + ret = gtk_selection_model_unselect_item (selection, 0); + g_assert_true (ret); + assert_selection (selection, ""); + assert_selection_changes (selection, "0:1"); + + ignore_changes (selection); + + g_object_unref (store); + g_object_unref (selection); +} + +static int +sort_inverse (gconstpointer a, gconstpointer b, gpointer data) +{ + int ia = GPOINTER_TO_UINT (g_object_get_qdata (G_OBJECT (a), number_quark)); + int ib = GPOINTER_TO_UINT (g_object_get_qdata (G_OBJECT (b), number_quark)); + + return ib - ia; +} + +static void +test_persistence (void) +{ + GtkSelectionModel *selection; + GListStore *store; + + store = new_store (1, 5, 1); + selection = new_model (store, TRUE, FALSE); + assert_selection (selection, "1"); + assert_selection_changes (selection, ""); + g_assert_true (gtk_selection_model_is_selected (selection, 0)); + g_assert_false (gtk_selection_model_is_selected (selection, 4)); + + g_list_store_sort (store, sort_inverse, NULL); + + assert_selection (selection, "1"); + assert_selection_changes (selection, ""); + g_assert_false (gtk_selection_model_is_selected (selection, 0)); + g_assert_true (gtk_selection_model_is_selected (selection, 4)); + + ignore_changes (selection); + + g_object_unref (store); + g_object_unref (selection); +} + +static void +check_query_range (GtkSelectionModel *selection) +{ + guint i, j; + guint position, n_items; + gboolean selected; + + /* check that range always contains position, and has uniform selection */ + for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (selection)); i++) + { + position = i; + selected = gtk_selection_model_query_range (selection, &position, &n_items); + g_assert_cmpint (position, <=, i); + g_assert_cmpint (i, <, position + n_items); + for (j = position; j < position + n_items; j++) + g_assert_true (selected == gtk_selection_model_is_selected (selection, j)); + } + +} + +static void +test_query_range (void) +{ + GtkSelectionModel *selection; + GListStore *store; + + store = new_store (1, 5, 1); + selection = new_model (store, TRUE, TRUE); + check_query_range (selection); + + gtk_selection_model_unselect_item (selection, 0); + check_query_range (selection); + + gtk_selection_model_select_item (selection, 2, TRUE); + check_query_range (selection); + + gtk_selection_model_select_item (selection, 4, TRUE); + check_query_range (selection); + + ignore_selection_changes (selection); + + g_object_unref (store); + g_object_unref (selection); +} + +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?"); + selection_quark = g_quark_from_static_string ("Mana mana, badibidibi"); + + g_test_add_func ("/singleselection/create", test_create); +#if GLIB_CHECK_VERSION (2, 59, 0) /* g_list_store_get_item() has overflow issues before */ + g_test_add_func ("/singleselection/autoselect", test_autoselect); + g_test_add_func ("/singleselection/selection", test_selection); + g_test_add_func ("/singleselection/can-unselect", test_can_unselect); + g_test_add_func ("/singleselection/persistence", test_persistence); +#endif + g_test_add_func ("/singleselection/query-range", test_query_range); +#if GLIB_CHECK_VERSION (2, 58, 0) /* g_list_store_splice() is broken before 2.58 */ + g_test_add_func ("/singleselection/changes", test_changes); +#endif + + return g_test_run (); +}