From 371dab51bb527fdbf417aed3217cde428b2b6863 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sun, 8 Dec 2019 20:22:06 -0500 Subject: [PATCH] Add GtkDropDown This is a simple drop down control using list models. --- docs/reference/gtk/gtk4-docs.xml | 2 + docs/reference/gtk/gtk4-sections.txt | 24 + docs/reference/gtk/gtk4.types.in | 1 + gtk/gtk.h | 1 + gtk/gtkdropdown.c | 1224 ++++++++++++++++++++++++++ gtk/gtkdropdown.h | 80 ++ gtk/meson.build | 2 + gtk/ui/gtkdropdown.ui | 78 ++ tests/meson.build | 1 + tests/testdropdown.c | 321 +++++++ testsuite/gtk/defaultvalue.c | 4 + testsuite/gtk/notify.c | 5 + 12 files changed, 1743 insertions(+) create mode 100644 gtk/gtkdropdown.c create mode 100644 gtk/gtkdropdown.h create mode 100644 gtk/ui/gtkdropdown.ui create mode 100644 tests/testdropdown.c diff --git a/docs/reference/gtk/gtk4-docs.xml b/docs/reference/gtk/gtk4-docs.xml index ae44def8e5..4cdda20611 100644 --- a/docs/reference/gtk/gtk4-docs.xml +++ b/docs/reference/gtk/gtk4-docs.xml @@ -86,6 +86,7 @@ + @@ -258,6 +259,7 @@ + diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt index 660ea5b068..808ef6fc4b 100644 --- a/docs/reference/gtk/gtk4-sections.txt +++ b/docs/reference/gtk/gtk4-sections.txt @@ -7443,3 +7443,27 @@ gtk_string_filter_set_match_mode gtk_string_filter_get_type + +
+gtkdropdown +GtkDropDown +GtkDropDown +gtk_drop_down_new +gtk_drop_down_set_from_strings +gtk_drop_down_set_model +gtk_drop_down_get_model +gtk_drop_down_set_selected +gtk_drop_down_get_selected +gtk_drop_down_set_factory +gtk_drop_down_get_factory +gtk_drop_down_set_list_factory +gtk_drop_down_get_list_factory +gtk_drop_down_set_expression +gtk_drop_down_get_expression +gtk_drop_down_set_enable_search +gtk_drop_down_get_enable_search + +GTK_TYPE_DROP_DOWN + +gtk_drop_down_get_type +
diff --git a/docs/reference/gtk/gtk4.types.in b/docs/reference/gtk/gtk4.types.in index be5ab6d20c..3accd6fa6d 100644 --- a/docs/reference/gtk/gtk4.types.in +++ b/docs/reference/gtk/gtk4.types.in @@ -66,6 +66,7 @@ gtk_drag_source_get_type gtk_drawing_area_get_type gtk_drop_target_get_type gtk_drop_target_async_get_type +gtk_drop_down_get_type gtk_editable_get_type gtk_emoji_chooser_get_type gtk_entry_buffer_get_type diff --git a/gtk/gtk.h b/gtk/gtk.h index bea638962a..ae2c76977f 100644 --- a/gtk/gtk.h +++ b/gtk/gtk.h @@ -98,6 +98,7 @@ #include #include #include +#include #include #include #include diff --git a/gtk/gtkdropdown.c b/gtk/gtkdropdown.c new file mode 100644 index 0000000000..4759186a49 --- /dev/null +++ b/gtk/gtkdropdown.c @@ -0,0 +1,1224 @@ +/* + * Copyright © 2019 Red Hat, Inc. + * + * 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: Matthias Clasen + */ + +#include "config.h" + +#include "gtkdropdown.h" + +#include "gtkbuiltiniconprivate.h" +#include "gtkintl.h" +#include "gtklistview.h" +#include "gtklistitemfactory.h" +#include "gtksignallistitemfactory.h" +#include "gtklistitemwidgetprivate.h" +#include "gtkpopover.h" +#include "gtkprivate.h" +#include "gtksingleselection.h" +#include "gtkfilterlistmodel.h" +#include "gtkstringfilter.h" +#include "gtkmultifilter.h" +#include "gtkstylecontext.h" +#include "gtkwidgetprivate.h" +#include "gtknative.h" +#include "gtktogglebutton.h" +#include "gtkexpression.h" +#include "gtkstack.h" +#include "gtksearchentry.h" +#include "gtklabel.h" +#include "gtklistitem.h" +#include "gtkbuildable.h" +#include "gtkbuilderprivate.h" + +/** + * SECTION:gtkdropdown + * @Title: GtkDropDown + * @Short_description: Choose an item from a list + * @See_also: #GtkComboBox + * + * GtkDropDown is a widget that allows the user to choose an item + * from a list of options. The GtkDropDown displays the selected + * choice. + * + * The options are given to GtkDropDown in the form of #GListModel, + * and how the individual options are represented is determined by + * a #GtkListItemFactory. The default factory displays simple strings, + * and expects to obtain these from the model by evaluating an expression + * that has to be provided via gtk_drop_down_set_expression(). + * + * The convenience method gtk_drop_down_set_from_strings() can be used + * to set up a model that is populated from an array of strings and + * an expression for obtaining those strings. + * + * GtkDropDown can optionally allow search in the popup, which is + * useful if the list of options is long. To enable the search entry, + * use gtk_drop_down_set_enable_search(). + * + * # GtkDropDown as GtkBuildable + * + * The GtkDropDown implementation of the GtkBuildable interface supports + * adding items directly using the element and specifying + * elements for each item. Using is equivalent to calling + * gtk_drop_down_set_from_strings(). Each element supports + * the regular translation attributes “translatable”, “context” + * and “comments”. + * + * Here is a UI definition fragment specifying GtkDropDown items: + * |[ + * + * + * Factory + * Home + * Subway + * + * + * ]| + + */ + +struct _GtkDropDown +{ + GtkWidget parent_instance; + + GtkListItemFactory *factory; + GtkListItemFactory *list_factory; + GListModel *model; + GListModel *selection; + GListModel *filter_model; + GListModel *popup_selection; + + GtkWidget *popup; + GtkWidget *button; + + GtkWidget *popup_list; + GtkWidget *button_stack; + GtkWidget *button_item; + GtkWidget *button_placeholder; + GtkWidget *search_entry; + + gboolean enable_search; + GtkExpression *expression; +}; + +struct _GtkDropDownClass +{ + GtkWidgetClass parent_class; +}; + +enum +{ + PROP_0, + PROP_FACTORY, + PROP_LIST_FACTORY, + PROP_MODEL, + PROP_SELECTED, + PROP_ENABLE_SEARCH, + PROP_EXPRESSION, + + N_PROPS +}; + +static void gtk_drop_down_buildable_interface_init (GtkBuildableIface *iface); + +static GtkBuildableIface *buildable_parent_iface = NULL; + +G_DEFINE_TYPE_WITH_CODE (GtkDropDown, gtk_drop_down, GTK_TYPE_WIDGET, + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + gtk_drop_down_buildable_interface_init)) + +static GParamSpec *properties[N_PROPS] = { NULL, }; + +static void +button_toggled (GtkWidget *widget, + gpointer data) +{ + GtkDropDown *self = data; + + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget))) + gtk_popover_popup (GTK_POPOVER (self->popup)); + else + gtk_popover_popdown (GTK_POPOVER (self->popup)); +} + +static void +popover_closed (GtkPopover *popover, + gpointer data) +{ + GtkDropDown *self = data; + + gtk_editable_set_text (GTK_EDITABLE (self->search_entry), ""); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->button), FALSE); +} + +static void +row_activated (GtkListView *listview, + guint position, + gpointer data) +{ + GtkDropDown *self = data; + GtkFilter *filter; + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->button), FALSE); + gtk_popover_popdown (GTK_POPOVER (self->popup)); + + /* reset the filter so positions are 1-1 */ + filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (self->filter_model)); + if (GTK_IS_STRING_FILTER (filter)) + gtk_string_filter_set_search (GTK_STRING_FILTER (filter), ""); + gtk_drop_down_set_selected (self, gtk_single_selection_get_selected (GTK_SINGLE_SELECTION (self->popup_selection))); +} + +static void +selection_changed (GtkSingleSelection *selection, + GParamSpec *pspec, + gpointer data) +{ + GtkDropDown *self = data; + guint selected; + gpointer item; + GtkFilter *filter; + + selected = gtk_single_selection_get_selected (GTK_SINGLE_SELECTION (self->selection)); + item = gtk_single_selection_get_selected_item (GTK_SINGLE_SELECTION (self->selection)); + + if (selected == GTK_INVALID_LIST_POSITION) + { + gtk_stack_set_visible_child_name (GTK_STACK (self->button_stack), "empty"); + } + else + { + gtk_stack_set_visible_child_name (GTK_STACK (self->button_stack), "item"); + gtk_list_item_widget_update (GTK_LIST_ITEM_WIDGET (self->button_item), selected, item, FALSE); + } + + /* reset the filter so positions are 1-1 */ + filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (self->filter_model)); + if (GTK_IS_STRING_FILTER (filter)) + gtk_string_filter_set_search (GTK_STRING_FILTER (filter), ""); + gtk_single_selection_set_selected (GTK_SINGLE_SELECTION (self->popup_selection), selected); +} + +static void +update_filter (GtkDropDown *self) +{ + if (self->filter_model) + { + GtkFilter *filter; + + if (self->expression) + { + filter = gtk_string_filter_new (); + gtk_string_filter_set_match_mode (GTK_STRING_FILTER (filter), GTK_STRING_FILTER_MATCH_MODE_PREFIX); + gtk_string_filter_set_expression (GTK_STRING_FILTER (filter), self->expression); + } + else + filter = gtk_every_filter_new (); + gtk_filter_list_model_set_filter (GTK_FILTER_LIST_MODEL (self->filter_model), filter); + g_object_unref (filter); + } +} + +static void +search_changed (GtkSearchEntry *entry, gpointer data) +{ + GtkDropDown *self = data; + const char *text; + GtkFilter *filter; + + text = gtk_editable_get_text (GTK_EDITABLE (entry)); + + filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (self->filter_model)); + if (GTK_IS_STRING_FILTER (filter)) + gtk_string_filter_set_search (GTK_STRING_FILTER (filter), text); +} + +static void +search_stop (GtkSearchEntry *entry, gpointer data) +{ + GtkDropDown *self = data; + GtkFilter *filter; + + filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (self->filter_model)); + if (GTK_IS_STRING_FILTER (filter)) + { + if (gtk_string_filter_get_search (GTK_STRING_FILTER (filter))) + gtk_string_filter_set_search (GTK_STRING_FILTER (filter), NULL); + else + gtk_popover_popdown (GTK_POPOVER (self->popup)); + } +} + +static void +gtk_drop_down_dispose (GObject *object) +{ + GtkDropDown *self = GTK_DROP_DOWN (object); + + g_clear_pointer (&self->popup, gtk_widget_unparent); + g_clear_pointer (&self->button, gtk_widget_unparent); + + g_clear_object (&self->model); + if (self->selection) + g_signal_handlers_disconnect_by_func (self->selection, selection_changed, self); + g_clear_object (&self->filter_model); + g_clear_pointer (&self->expression, gtk_expression_unref); + g_clear_object (&self->selection); + g_clear_object (&self->popup_selection); + g_clear_object (&self->factory); + g_clear_object (&self->list_factory); + + G_OBJECT_CLASS (gtk_drop_down_parent_class)->dispose (object); +} + +static void +gtk_drop_down_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GtkDropDown *self = GTK_DROP_DOWN (object); + + switch (property_id) + { + case PROP_FACTORY: + g_value_set_object (value, self->factory); + break; + + case PROP_LIST_FACTORY: + g_value_set_object (value, self->list_factory); + break; + + case PROP_MODEL: + g_value_set_object (value, self->model); + break; + + case PROP_SELECTED: + g_value_set_uint (value, gtk_drop_down_get_selected (self)); + break; + + case PROP_ENABLE_SEARCH: + g_value_set_boolean (value, self->enable_search); + break; + + case PROP_EXPRESSION: + g_value_set_boxed (value, self->expression); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gtk_drop_down_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkDropDown *self = GTK_DROP_DOWN (object); + + switch (property_id) + { + case PROP_FACTORY: + gtk_drop_down_set_factory (self, g_value_get_object (value)); + break; + + case PROP_LIST_FACTORY: + gtk_drop_down_set_list_factory (self, g_value_get_object (value)); + break; + + case PROP_MODEL: + gtk_drop_down_set_model (self, g_value_get_object (value)); + break; + + case PROP_SELECTED: + gtk_drop_down_set_selected (self, g_value_get_uint (value)); + break; + + case PROP_ENABLE_SEARCH: + gtk_drop_down_set_enable_search (self, g_value_get_boolean (value)); + break; + + case PROP_EXPRESSION: + gtk_drop_down_set_expression (self, g_value_get_boxed (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gtk_drop_down_measure (GtkWidget *widget, + GtkOrientation orientation, + int size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + GtkDropDown *self = GTK_DROP_DOWN (widget); + + gtk_widget_measure (self->button, + orientation, + size, + minimum, natural, + minimum_baseline, natural_baseline); +} + +static void +gtk_drop_down_size_allocate (GtkWidget *widget, + int width, + int height, + int baseline) +{ + GtkDropDown *self = GTK_DROP_DOWN (widget); + + gtk_widget_size_allocate (self->button, &(GtkAllocation) { 0, 0, width, height }, baseline); + + gtk_native_check_resize (GTK_NATIVE (self->popup)); +} + +static gboolean +gtk_drop_down_focus (GtkWidget *widget, + GtkDirectionType direction) +{ + GtkDropDown *self = GTK_DROP_DOWN (widget); + + if (self->popup && gtk_widget_get_visible (self->popup)) + return gtk_widget_child_focus (self->popup, direction); + else + return gtk_widget_child_focus (self->button, direction); +} + +static gboolean +gtk_drop_down_grab_focus (GtkWidget *widget) +{ + GtkDropDown *self = GTK_DROP_DOWN (widget); + + return gtk_widget_grab_focus (self->button); +} + + +static void +gtk_drop_down_class_init (GtkDropDownClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->dispose = gtk_drop_down_dispose; + gobject_class->get_property = gtk_drop_down_get_property; + gobject_class->set_property = gtk_drop_down_set_property; + + widget_class->measure = gtk_drop_down_measure; + widget_class->size_allocate = gtk_drop_down_size_allocate; + widget_class->focus = gtk_drop_down_focus; + widget_class->grab_focus = gtk_drop_down_grab_focus; + + /** + * GtkDropDown:factory: + * + * Factory for populating list items. + */ + properties[PROP_FACTORY] = + g_param_spec_object ("factory", + P_("Factory"), + P_("Factory for populating list items"), + GTK_TYPE_LIST_ITEM_FACTORY, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkDropDown:list-factory: + * + * The factory for populating list items in the popup. + * + * If this is not set, #GtkDropDown:factory is used. + */ + properties[PROP_LIST_FACTORY] = + g_param_spec_object ("list-factory", + P_("List Factory"), + P_("Factory for populating list items"), + GTK_TYPE_LIST_ITEM_FACTORY, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkDropDown:model: + * + * Model for the displayed items. + */ + properties[PROP_MODEL] = + g_param_spec_object ("model", + P_("Model"), + P_("Model for the displayed items"), + G_TYPE_LIST_MODEL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkDropDown:selected: + * + * The position of the selected item in #GtkDropDown:model, + * or #GTK_INVALID_LIST_POSITION if no item is selected. + */ + 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); + + /** + * GtkDropDown:enable-search: + * + * Whether to show a search entry in the popup. + * + * Note that search requires #GtkDropDown:expression to be set. + */ + properties[PROP_ENABLE_SEARCH] = + g_param_spec_boolean ("enable-search", + P_("Enable search"), + P_("Whether to show a search entry in the popup"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkDropDown:expression: + * + * An expression to evaluate to obtain strings to match against the search + * term (see #GtkDropDown:enable-search). If #GtkDropDown:factory is not set, + * the expression is also used to bind strings to labels produced by a + * default factory. + */ + properties[PROP_EXPRESSION] = + g_param_spec_boxed ("expression", + P_("Expression"), + P_("Expression to determine strings to search for"), + GTK_TYPE_EXPRESSION, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkdropdown.ui"); + gtk_widget_class_bind_template_child (widget_class, GtkDropDown, button); + gtk_widget_class_bind_template_child (widget_class, GtkDropDown, button_stack); + gtk_widget_class_bind_template_child (widget_class, GtkDropDown, button_item); + gtk_widget_class_bind_template_child (widget_class, GtkDropDown, popup); + gtk_widget_class_bind_template_child (widget_class, GtkDropDown, popup_list); + gtk_widget_class_bind_template_child (widget_class, GtkDropDown, search_entry); + + gtk_widget_class_bind_template_callback (widget_class, row_activated); + gtk_widget_class_bind_template_callback (widget_class, button_toggled); + gtk_widget_class_bind_template_callback (widget_class, popover_closed); + gtk_widget_class_bind_template_callback (widget_class, search_changed); + gtk_widget_class_bind_template_callback (widget_class, search_stop); + + gtk_widget_class_set_css_name (widget_class, I_("combobox")); +} + +static void +setup_item (GtkSignalListItemFactory *factory, + GtkListItem *list_item, + gpointer data) +{ + GtkWidget *label; + + label = gtk_label_new (NULL); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_list_item_set_child (list_item, label); +} + +static void +bind_item (GtkSignalListItemFactory *factory, + GtkListItem *list_item, + gpointer data) +{ + GtkDropDown *self = data; + gpointer item; + GtkWidget *label; + GValue value = G_VALUE_INIT; + + if (self->expression == NULL) + { + g_critical ("Either GtkDropDown::factory or GtkDropDown::expression must be set"); + return; + } + + item = gtk_list_item_get_item (list_item); + label = gtk_list_item_get_child (list_item); + + if (gtk_expression_evaluate (self->expression, item, &value)) + { + gtk_label_set_label (GTK_LABEL (label), g_value_get_string (&value)); + g_value_unset (&value); + } +} + +static void +set_default_factory (GtkDropDown *self) +{ + GtkListItemFactory *factory; + + factory = gtk_signal_list_item_factory_new (); + + g_signal_connect (factory, "setup", G_CALLBACK (setup_item), self); + g_signal_connect (factory, "bind", G_CALLBACK (bind_item), self); + + gtk_drop_down_set_factory (self, factory); + + g_object_unref (factory); +} + +static void +gtk_drop_down_init (GtkDropDown *self) +{ + g_type_ensure (GTK_TYPE_BUILTIN_ICON); + g_type_ensure (GTK_TYPE_LIST_ITEM_WIDGET); + + gtk_widget_init_template (GTK_WIDGET (self)); + + set_default_factory (self); +} + +/** + * gtk_drop_down_new: + * + * Creates a new empty #GtkDropDown. + * + * You most likely want to call gtk_drop_down_set_factory() to + * set up a way to map its items to widgets and gtk_drop_down_set_model() + * to set a model to provide items next. + * + * Returns: a new #GtkDropDown + **/ +GtkWidget * +gtk_drop_down_new (void) +{ + return g_object_new (GTK_TYPE_DROP_DOWN, NULL); +} + +/** + * gtk_drop_down_get_model: + * @self: a #GtkDropDown + * + * Gets the model that provides the displayed items. + * + * Returns: (nullable) (transfer none): The model in use + **/ +GListModel * +gtk_drop_down_get_model (GtkDropDown *self) +{ + g_return_val_if_fail (GTK_IS_DROP_DOWN (self), NULL); + + return self->model; +} + +/** + * gtk_drop_down_set_model: + * @self: a #GtkDropDown + * @model: (allow-none) (transfer none): the model to use or %NULL for none + * + * Sets the #GListModel to use. + */ +void +gtk_drop_down_set_model (GtkDropDown *self, + GListModel *model) +{ + g_return_if_fail (GTK_IS_DROP_DOWN (self)); + g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); + + if (!g_set_object (&self->model, model)) + return; + + if (model == NULL) + { + gtk_list_view_set_model (GTK_LIST_VIEW (self->popup_list), NULL); + + if (self->selection) + g_signal_handlers_disconnect_by_func (self->selection, selection_changed, self); + + g_clear_object (&self->selection); + g_clear_object (&self->filter_model); + g_clear_object (&self->popup_selection); + } + else + { + GListModel *filter_model; + GListModel *selection; + + filter_model = G_LIST_MODEL (gtk_filter_list_model_new (model, NULL)); + g_set_object (&self->filter_model, filter_model); + g_object_unref (filter_model); + + update_filter (self); + + selection = G_LIST_MODEL (gtk_single_selection_new (filter_model)); + g_set_object (&self->popup_selection, selection); + gtk_list_view_set_model (GTK_LIST_VIEW (self->popup_list), selection); + g_object_unref (selection); + + selection = G_LIST_MODEL (gtk_single_selection_new (model)); + g_set_object (&self->selection, selection); + g_object_unref (selection); + + g_signal_connect (self->selection, "notify::selected", G_CALLBACK (selection_changed), self); + selection_changed (GTK_SINGLE_SELECTION (self->selection), NULL, self); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]); +} + +/** + * gtk_drop_down_get_factory: + * @self: a #GtkDropDown + * + * Gets the factory that's currently used to populate list items. + * + * The factory returned by this function is always used for the + * item in the button. It is also used for items in the popup + * if #GtkDropDown:list-factory is not set. + * + * Returns: (nullable) (transfer none): The factory in use + **/ +GtkListItemFactory * +gtk_drop_down_get_factory (GtkDropDown *self) +{ + g_return_val_if_fail (GTK_IS_DROP_DOWN (self), NULL); + + return self->factory; +} + +/** + * gtk_drop_down_set_factory: + * @self: a #GtkDropDown + * @factory: (allow-none) (transfer none): the factory to use or %NULL for none + * + * Sets the #GtkListItemFactory to use for populating list items. + **/ +void +gtk_drop_down_set_factory (GtkDropDown *self, + GtkListItemFactory *factory) +{ + g_return_if_fail (GTK_IS_DROP_DOWN (self)); + g_return_if_fail (factory == NULL || GTK_LIST_ITEM_FACTORY (factory)); + + if (!g_set_object (&self->factory, factory)) + return; + + gtk_list_item_widget_set_factory (GTK_LIST_ITEM_WIDGET (self->button_item), factory); + if (self->list_factory == NULL) + gtk_list_view_set_factory (GTK_LIST_VIEW (self->popup_list), factory); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FACTORY]); +} + +/** + * gtk_drop_down_get_list_factory: + * @self: a #GtkDropDown + * + * Gets the factory that's currently used to populate list items in the popup. + * + * Returns: (nullable) (transfer none): The factory in use + **/ +GtkListItemFactory * +gtk_drop_down_get_list_factory (GtkDropDown *self) +{ + g_return_val_if_fail (GTK_IS_DROP_DOWN (self), NULL); + + return self->list_factory; +} + +/** + * gtk_drop_down_set_list_factory: + * @self: a #GtkDropDown + * @factory: (allow-none) (transfer none): the factory to use or %NULL for none + * + * Sets the #GtkListItemFactory to use for populating list items in the popup. + **/ +void +gtk_drop_down_set_list_factory (GtkDropDown *self, + GtkListItemFactory *factory) +{ + g_return_if_fail (GTK_IS_DROP_DOWN (self)); + g_return_if_fail (factory == NULL || GTK_LIST_ITEM_FACTORY (factory)); + + if (!g_set_object (&self->list_factory, factory)) + return; + + if (self->list_factory != NULL) + gtk_list_view_set_factory (GTK_LIST_VIEW (self->popup_list), self->list_factory); + else + gtk_list_view_set_factory (GTK_LIST_VIEW (self->popup_list), self->factory); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LIST_FACTORY]); +} + +/** + * gtk_drop_down_set_selected: + * @self: a #GtkDropDown + * @position: the position of the item to select, or #GTK_INVALID_LIST_POSITION + * + * Selects the item at the given position. + **/ +void +gtk_drop_down_set_selected (GtkDropDown *self, + guint position) +{ + g_return_if_fail (GTK_IS_DROP_DOWN (self)); + + if (self->selection == NULL) + return; + + if (gtk_single_selection_get_selected (GTK_SINGLE_SELECTION (self->selection)) == position) + return; + + gtk_single_selection_set_selected (GTK_SINGLE_SELECTION (self->selection), position); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED]); +} + +/** + * gtk_drop_down_get_selected: + * @self: a #GtkDropDown + * + * Gets the position of the selected item. + * + * Returns: the position of the selected item, or #GTK_INVALID_LIST_POSITION + * if not item is selected + **/ +guint +gtk_drop_down_get_selected (GtkDropDown *self) +{ + g_return_val_if_fail (GTK_IS_DROP_DOWN (self), GTK_INVALID_LIST_POSITION); + + if (self->selection == NULL) + return GTK_INVALID_LIST_POSITION; + + return gtk_single_selection_get_selected (GTK_SINGLE_SELECTION (self->selection)); +} + +/** + * gtk_drop_down_set_enable_search: + * @self: a #GtkDropDown + * @enable_search: whether to enable search + * + * Sets whether a search entry will be shown in the popup that + * allows to search for items in the list. + * + * Note that #GtkDropDown:expression must be set for search to work. + **/ +void +gtk_drop_down_set_enable_search (GtkDropDown *self, + gboolean enable_search) +{ + g_return_if_fail (GTK_IS_DROP_DOWN (self)); + + if (self->enable_search == enable_search) + return; + + self->enable_search = enable_search; + + gtk_editable_set_text (GTK_EDITABLE (self->search_entry), ""); + gtk_widget_set_visible (self->search_entry, enable_search); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENABLE_SEARCH]); +} + +/** + * gtk_drop_down_get_enable_search: + * + * Returns whether search is enabled. + * + * Returns: %TRUE if the popup includes a search entry + **/ +gboolean +gtk_drop_down_get_enable_search (GtkDropDown *self) +{ + g_return_val_if_fail (GTK_IS_DROP_DOWN (self), FALSE); + + return self->enable_search; +} + +/** + * gtk_drop_down_set_expression: + * @self: a #GtkDropDown + * @expression: (nullable): a #GtkExpression, or %NULL + * + * Sets the expression that gets evaluated to obtain strings from items + * when searching in the popup. The expression must have a value type of + * #GTK_TYPE_STRING. + */ +void +gtk_drop_down_set_expression (GtkDropDown *self, + GtkExpression *expression) +{ + g_return_if_fail (GTK_IS_DROP_DOWN (self)); + g_return_if_fail (expression == NULL || + gtk_expression_get_value_type (expression) == G_TYPE_STRING); + + if (self->expression == expression) + return; + + if (self->expression) + gtk_expression_unref (self->expression); + self->expression = expression; + if (self->expression) + gtk_expression_ref (self->expression); + + update_filter (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_EXPRESSION]); +} + +/** + * gtk_drop_down_get_expression: + * @self: a #GtkDropDown + * + * Gets the expression set with gtk_drop_down_set_expression(). + * + * Returns: (nullable) (transfer none): a #GtkExpression or %NULL + */ +GtkExpression * +gtk_drop_down_get_expression (GtkDropDown *self) +{ + g_return_val_if_fail (GTK_IS_DROP_DOWN (self), NULL); + + return self->expression; +} + + +#define GTK_TYPE_DROP_DOWN_STRING_HOLDER (gtk_drop_down_string_holder_get_type ()) +G_DECLARE_FINAL_TYPE (GtkDropDownStringHolder, gtk_drop_down_string_holder, GTK, DROP_DOWN_STRING_HOLDER, GObject) + +struct _GtkDropDownStringHolder { + GObject parent_instance; + char *string; +}; + +enum { + PROP_STRING = 1, + PROP_NUM_PROPERTIES +}; + +G_DEFINE_TYPE (GtkDropDownStringHolder, gtk_drop_down_string_holder, G_TYPE_OBJECT); + +static void +gtk_drop_down_string_holder_init (GtkDropDownStringHolder *holder) +{ +} + +static void +gtk_drop_down_string_holder_finalize (GObject *object) +{ + GtkDropDownStringHolder *holder = GTK_DROP_DOWN_STRING_HOLDER (object); + + g_free (holder->string); + + G_OBJECT_CLASS (gtk_drop_down_string_holder_parent_class)->finalize (object); +} + +static void +gtk_drop_down_string_holder_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkDropDownStringHolder *holder = GTK_DROP_DOWN_STRING_HOLDER (object); + + switch (property_id) + { + case PROP_STRING: + g_free (holder->string); + holder->string = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gtk_drop_down_string_holder_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GtkDropDownStringHolder *holder = GTK_DROP_DOWN_STRING_HOLDER (object); + + switch (property_id) + { + case PROP_STRING: + g_value_set_string (value, holder->string); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gtk_drop_down_string_holder_class_init (GtkDropDownStringHolderClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GParamSpec *pspec; + + object_class->finalize = gtk_drop_down_string_holder_finalize; + object_class->set_property = gtk_drop_down_string_holder_set_property; + object_class->get_property = gtk_drop_down_string_holder_get_property; + + pspec = g_param_spec_string ("string", "String", "String", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_property (object_class, PROP_STRING, pspec); + +} + +static GtkDropDownStringHolder * +gtk_drop_down_string_holder_new (const char *string) +{ + return g_object_new (GTK_TYPE_DROP_DOWN_STRING_HOLDER, "string", string, NULL); +} + +static GListModel * +gtk_drop_down_strings_model_new (const char *const *text) +{ + GListStore *store; + int i; + + store = g_list_store_new (GTK_TYPE_DROP_DOWN_STRING_HOLDER); + for (i = 0; text[i]; i++) + { + GtkDropDownStringHolder *holder = gtk_drop_down_string_holder_new (text[i]); + g_list_store_append (store, holder); + g_object_unref (holder); + } + return G_LIST_MODEL (store); +} + +/** + * gtk_drop_down_set_from_strings: + * @self: a #GtkDropDown + * text: a %NULL-terminated string array + * + * Populates @self with the strings in @text, + * by creating a suitable model and factory. + */ +void +gtk_drop_down_set_from_strings (GtkDropDown *self, + const char *const *text) +{ + GtkExpression *expression; + GListModel *model; + + g_return_if_fail (GTK_IS_DROP_DOWN (self)); + g_return_if_fail (text != NULL); + + set_default_factory (self); + + expression = gtk_property_expression_new (GTK_TYPE_DROP_DOWN_STRING_HOLDER, NULL, "string"); + gtk_drop_down_set_expression (self, expression); + gtk_expression_unref (expression); + + model = gtk_drop_down_strings_model_new (text); + gtk_drop_down_set_model (self, model); + g_object_unref (model); +} + +typedef struct { + GtkBuilder *builder; + GObject *object; + const gchar *domain; + + gchar *context; + guint translatable : 1; + guint is_text : 1; + + GString *string; + GPtrArray *strings; +} ItemParserData; + +static void +item_start_element (GtkBuildableParseContext *context, + const gchar *element_name, + const gchar **names, + const gchar **values, + gpointer user_data, + GError **error) +{ + ItemParserData *data = (ItemParserData*)user_data; + + if (strcmp (element_name, "items") == 0) + { + if (!_gtk_builder_check_parent (data->builder, context, "object", error)) + return; + + if (!g_markup_collect_attributes (element_name, names, values, error, + G_MARKUP_COLLECT_INVALID, NULL, NULL, + G_MARKUP_COLLECT_INVALID)) + _gtk_builder_prefix_error (data->builder, context, error); + } + else if (strcmp (element_name, "item") == 0) + { + gboolean translatable = FALSE; + const gchar *msg_context = NULL; + + if (!_gtk_builder_check_parent (data->builder, context, "items", error)) + return; + + if (!g_markup_collect_attributes (element_name, names, values, error, + G_MARKUP_COLLECT_BOOLEAN|G_MARKUP_COLLECT_OPTIONAL, "translatable", &translatable, + G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "comments", NULL, + G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "context", &msg_context, + G_MARKUP_COLLECT_INVALID)) + { + _gtk_builder_prefix_error (data->builder, context, error); + return; + } + + data->is_text = TRUE; + data->translatable = translatable; + data->context = g_strdup (msg_context); + } + else + { + _gtk_builder_error_unhandled_tag (data->builder, context, + "GtkDropDown", element_name, + error); + } +} + +static void +item_text (GtkBuildableParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + ItemParserData *data = (ItemParserData*)user_data; + + if (data->is_text) + g_string_append_len (data->string, text, text_len); +} + +static void +item_end_element (GtkBuildableParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + ItemParserData *data = (ItemParserData*)user_data; + + /* Append the translated strings */ + if (data->string->len) + { + if (data->translatable) + { + const gchar *translated; + + translated = _gtk_builder_parser_translate (data->domain, + data->context, + data->string->str); + g_string_assign (data->string, translated); + } + + g_ptr_array_add (data->strings, g_strdup (data->string->str)); + } + + data->translatable = FALSE; + g_string_set_size (data->string, 0); + g_clear_pointer (&data->context, g_free); + data->is_text = FALSE; +} + +static const GtkBuildableParser item_parser = +{ + item_start_element, + item_end_element, + item_text +}; + +static gboolean +gtk_drop_down_buildable_custom_tag_start (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + GtkBuildableParser *parser, + gpointer *parser_data) +{ + if (buildable_parent_iface->custom_tag_start (buildable, builder, child, + tagname, parser, parser_data)) + return TRUE; + + if (strcmp (tagname, "items") == 0) + { + ItemParserData *data; + + data = g_slice_new0 (ItemParserData); + data->builder = g_object_ref (builder); + data->object = g_object_ref (G_OBJECT (buildable)); + data->domain = gtk_builder_get_translation_domain (builder); + data->string = g_string_new (""); + data->strings = g_ptr_array_new_with_free_func (g_free); + + *parser = item_parser; + *parser_data = data; + + return TRUE; + } + + return FALSE; +} + +static void +gtk_drop_down_buildable_custom_finished (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + gpointer user_data) +{ + ItemParserData *data; + + buildable_parent_iface->custom_finished (buildable, builder, child, + tagname, user_data); + + if (strcmp (tagname, "items") == 0) + { + data = (ItemParserData*)user_data; + + g_ptr_array_add (data->strings, NULL); + + gtk_drop_down_set_from_strings (GTK_DROP_DOWN (data->object), (const char **)data->strings->pdata); + + g_object_unref (data->object); + g_object_unref (data->builder); + g_string_free (data->string, TRUE); + g_ptr_array_unref (data->strings); + g_slice_free (ItemParserData, data); + } +} + +static void +gtk_drop_down_buildable_interface_init (GtkBuildableIface *iface) +{ + buildable_parent_iface = g_type_interface_peek_parent (iface); + + iface->custom_tag_start = gtk_drop_down_buildable_custom_tag_start; + iface->custom_finished = gtk_drop_down_buildable_custom_finished; +} diff --git a/gtk/gtkdropdown.h b/gtk/gtkdropdown.h new file mode 100644 index 0000000000..84813b3877 --- /dev/null +++ b/gtk/gtkdropdown.h @@ -0,0 +1,80 @@ +/* + * Copyright © 2018 Red Hat, Inc. + * + * 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: Matthias Clasen + */ + +#ifndef __GTK_DROP_DOWN_H__ +#define __GTK_DROP_DOWN_H__ + +#include +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_DROP_DOWN (gtk_drop_down_get_type ()) + +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkDropDown, gtk_drop_down, GTK, DROP_DOWN, GtkWidget) + +GDK_AVAILABLE_IN_ALL +GtkWidget * gtk_drop_down_new (void); + +GDK_AVAILABLE_IN_ALL +void gtk_drop_down_set_from_strings (GtkDropDown *self, + const char *const *texts); + +GDK_AVAILABLE_IN_ALL +void gtk_drop_down_set_model (GtkDropDown *self, + GListModel *model); +GDK_AVAILABLE_IN_ALL +GListModel * gtk_drop_down_get_model (GtkDropDown *self); + +GDK_AVAILABLE_IN_ALL +void gtk_drop_down_set_selected (GtkDropDown *self, + guint position); +GDK_AVAILABLE_IN_ALL +guint gtk_drop_down_get_selected (GtkDropDown *self); + +GDK_AVAILABLE_IN_ALL +void gtk_drop_down_set_factory (GtkDropDown *self, + GtkListItemFactory *factory); +GDK_AVAILABLE_IN_ALL +GtkListItemFactory * + gtk_drop_down_get_factory (GtkDropDown *self); + +GDK_AVAILABLE_IN_ALL +void gtk_drop_down_set_list_factory (GtkDropDown *self, + GtkListItemFactory *factory); +GDK_AVAILABLE_IN_ALL +GtkListItemFactory * + gtk_drop_down_get_list_factory (GtkDropDown *self); + +GDK_AVAILABLE_IN_ALL +void gtk_drop_down_set_expression (GtkDropDown *self, + GtkExpression *expression); +GDK_AVAILABLE_IN_ALL +GtkExpression * gtk_drop_down_get_expression (GtkDropDown *self); + +GDK_AVAILABLE_IN_ALL +void gtk_drop_down_set_enable_search (GtkDropDown *self, + gboolean enable_search); +GDK_AVAILABLE_IN_ALL +gboolean gtk_drop_down_get_enable_search (GtkDropDown *self); + +G_END_DECLS + +#endif /* __GTK_DROP_DOWN_H__ */ diff --git a/gtk/meson.build b/gtk/meson.build index 6c02f597d4..448de76888 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -219,6 +219,7 @@ gtk_public_sources = files([ 'gtkdropcontrollermotion.c', 'gtkdroptarget.c', 'gtkdroptargetasync.c', + 'gtkdropdown.c', 'gtkeditable.c', 'gtkemojichooser.c', 'gtkemojicompletion.c', @@ -499,6 +500,7 @@ gtk_public_headers = files([ 'gtkdropcontrollermotion.h', 'gtkdroptarget.h', 'gtkdroptargetasync.h', + 'gtkdropdown.h', 'gtkeditable.h', 'gtkemojichooser.h', 'gtkentry.h', diff --git a/gtk/ui/gtkdropdown.ui b/gtk/ui/gtkdropdown.ui new file mode 100644 index 0000000000..08305533a0 --- /dev/null +++ b/gtk/ui/gtkdropdown.ui @@ -0,0 +1,78 @@ + + + + diff --git a/tests/meson.build b/tests/meson.build index 36857d2cd8..531e078fbc 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,5 +1,6 @@ gtk_tests = [ # testname, optional extra sources + ['testdropdown'], ['rendernode'], ['rendernode-create-tests'], ['overlayscroll'], diff --git a/tests/testdropdown.c b/tests/testdropdown.c new file mode 100644 index 0000000000..877109f563 --- /dev/null +++ b/tests/testdropdown.c @@ -0,0 +1,321 @@ +/* simple.c + * Copyright (C) 2017 Red Hat, Inc + * Author: Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ +#include + + +#define STRING_TYPE_HOLDER (string_holder_get_type ()) +G_DECLARE_FINAL_TYPE (StringHolder, string_holder, STRING, HOLDER, GObject) + +struct _StringHolder { + GObject parent_instance; + char *title; + char *icon; + char *description; +}; + +G_DEFINE_TYPE (StringHolder, string_holder, G_TYPE_OBJECT); + +static void +string_holder_init (StringHolder *holder) +{ +} + +static void +string_holder_finalize (GObject *object) +{ + StringHolder *holder = STRING_HOLDER (object); + + g_free (holder->title); + g_free (holder->icon); + g_free (holder->description); + + G_OBJECT_CLASS (string_holder_parent_class)->finalize (object); +} + +static void +string_holder_class_init (StringHolderClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = string_holder_finalize; +} + +static StringHolder * +string_holder_new (const char *title, const char *icon, const char *description) +{ + StringHolder *holder = g_object_new (STRING_TYPE_HOLDER, NULL); + holder->title = g_strdup (title); + holder->icon = g_strdup (icon); + holder->description = g_strdup (description); + return holder; +} + +static void +strings_setup_item_single_line (GtkSignalListItemFactory *factory, + GtkListItem *item) +{ + GtkWidget *box, *image, *title; + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); + + image = gtk_image_new (); + title = gtk_label_new (""); + gtk_label_set_xalign (GTK_LABEL (title), 0.0); + + gtk_box_append (GTK_BOX (box), image); + gtk_box_append (GTK_BOX (box), title); + + g_object_set_data (G_OBJECT (item), "title", title); + g_object_set_data (G_OBJECT (item), "image", image); + + gtk_list_item_set_child (item, box); +} + +static void +strings_setup_item_full (GtkSignalListItemFactory *factory, + GtkListItem *item) +{ + GtkWidget *box, *box2, *image, *title, *description; + + image = gtk_image_new (); + title = gtk_label_new (""); + gtk_label_set_xalign (GTK_LABEL (title), 0.0); + description = gtk_label_new (""); + gtk_label_set_xalign (GTK_LABEL (description), 0.0); + gtk_style_context_add_class (gtk_widget_get_style_context (description), "dim-label"); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); + box2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2); + + gtk_box_append (GTK_BOX (box), image); + gtk_box_append (GTK_BOX (box), box2); + gtk_box_append (GTK_BOX (box2), title); + gtk_box_append (GTK_BOX (box2), description); + + g_object_set_data (G_OBJECT (item), "title", title); + g_object_set_data (G_OBJECT (item), "image", image); + g_object_set_data (G_OBJECT (item), "description", description); + + gtk_list_item_set_child (item, box); +} + +static void +strings_bind_item (GtkSignalListItemFactory *factory, + GtkListItem *item) +{ + GtkWidget *image, *title, *description; + StringHolder *holder; + + holder = gtk_list_item_get_item (item); + + title = g_object_get_data (G_OBJECT (item), "title"); + image = g_object_get_data (G_OBJECT (item), "image"); + description = g_object_get_data (G_OBJECT (item), "description"); + + gtk_label_set_label (GTK_LABEL (title), holder->title); + if (image) + { + gtk_image_set_from_icon_name (GTK_IMAGE (image), holder->icon); + gtk_widget_set_visible (image, holder->icon != NULL); + } + if (description) + { + gtk_label_set_label (GTK_LABEL (description), holder->description); + gtk_widget_set_visible (description , holder->description != NULL); + } +} + +static GtkListItemFactory * +strings_factory_new (gboolean full) +{ + GtkListItemFactory *factory; + + factory = gtk_signal_list_item_factory_new (); + if (full) + g_signal_connect (factory, "setup", G_CALLBACK (strings_setup_item_full), NULL); + else + g_signal_connect (factory, "setup", G_CALLBACK (strings_setup_item_single_line), NULL); + g_signal_connect (factory, "bind", G_CALLBACK (strings_bind_item), NULL); + + return factory; +} + +static GListModel * +strings_model_new (const char *const *titles, + const char *const *icons, + const char *const *descriptions) +{ + GListStore *store; + int i; + + store = g_list_store_new (STRING_TYPE_HOLDER); + for (i = 0; titles[i]; i++) + { + StringHolder *holder = string_holder_new (titles[i], + icons ? icons[i] : NULL, + descriptions ? descriptions[i] : NULL); + g_list_store_append (store, holder); + g_object_unref (holder); + } + + return G_LIST_MODEL (store); +} + +static GtkWidget * +drop_down_new_from_strings (const char *const *titles, + const char *const *icons, + const char *const *descriptions) +{ + GtkWidget *widget; + GListModel *model; + GtkListItemFactory *factory; + GtkListItemFactory *list_factory; + + g_return_val_if_fail (titles != NULL, NULL); + g_return_val_if_fail (icons == NULL || g_strv_length ((char **)icons) == g_strv_length ((char **)titles), NULL); + g_return_val_if_fail (descriptions == NULL || g_strv_length ((char **)icons) == g_strv_length ((char **)descriptions), NULL); + + model = strings_model_new (titles, icons, descriptions); + factory = strings_factory_new (FALSE); + if (icons != NULL || descriptions != NULL) + list_factory = strings_factory_new (TRUE); + else + list_factory = NULL; + + widget = g_object_new (GTK_TYPE_DROP_DOWN, + "model", model, + "factory", factory, + "list-factory", list_factory, + NULL); + + g_object_unref (model); + g_object_unref (factory); + if (list_factory) + g_object_unref (list_factory); + + return widget; +} + +static char * +get_family_name (gpointer item) +{ + return g_strdup (pango_font_family_get_name (PANGO_FONT_FAMILY (item))); +} + +static char * +get_title (gpointer item) +{ + return g_strdup (STRING_HOLDER (item)->title); +} + +static gboolean +quit_cb (GtkWindow *window, + gpointer data) +{ + *((gboolean*)data) = TRUE; + + g_main_context_wakeup (NULL); + + return TRUE; +} + +int +main (int argc, char *argv[]) +{ + GtkWidget *window, *button, *box, *spin, *check; + GListModel *model; + GtkExpression *expression; + const char * const times[] = { "1 minute", "2 minutes", "5 minutes", "20 minutes", NULL }; + const char * const many_times[] = { + "1 minute", "2 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", + "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", + "55 minutes", "1 hour", "2 hours", "3 hours", "5 hours", "6 hours", "7 hours", + "8 hours", "9 hours", "10 hours", "11 hours", "12 hours", NULL + }; + const char * const device_titles[] = { "Digital Output", "Headphones", "Digital Output", "Analog Output", NULL }; + const char * const device_icons[] = { "audio-card-symbolic", "audio-headphones-symbolic", "audio-card-symbolic", "audio-card-symbolic", NULL }; + const char * const device_descriptions[] = { + "Built-in Audio", "Built-in audio", "Thinkpad Tunderbolt 3 Dock USB Audio", "Thinkpad Tunderbolt 3 Dock USB Audio", NULL + }; + gboolean quit = FALSE; + + gtk_init (); + + window = gtk_window_new (); + gtk_window_set_title (GTK_WINDOW (window), "hello world"); + gtk_window_set_resizable (GTK_WINDOW (window), TRUE); + g_signal_connect (window, "close-request", G_CALLBACK (quit_cb), &quit); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); + gtk_widget_set_margin_start (box, 10); + gtk_widget_set_margin_end (box, 10); + gtk_widget_set_margin_top (box, 10); + gtk_widget_set_margin_bottom (box, 10); + gtk_window_set_child (GTK_WINDOW (window), box); + + button = gtk_drop_down_new (); + + model = G_LIST_MODEL (pango_cairo_font_map_get_default ()); + gtk_drop_down_set_model (GTK_DROP_DOWN (button), model); + gtk_drop_down_set_selected (GTK_DROP_DOWN (button), 0); + + expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL, + 0, NULL, + (GCallback)get_family_name, + NULL, NULL); + gtk_drop_down_set_expression (GTK_DROP_DOWN (button), expression); + gtk_expression_unref (expression); + gtk_box_append (GTK_BOX (box), button); + + spin = gtk_spin_button_new_with_range (-1, g_list_model_get_n_items (G_LIST_MODEL (model)), 1); + gtk_widget_set_halign (spin, GTK_ALIGN_START); + g_object_bind_property (button, "selected", spin, "value", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + gtk_box_append (GTK_BOX (box), spin); + + check = gtk_check_button_new_with_label ("Enable search"); + g_object_bind_property (button, "enable-search", check, "active", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + gtk_box_append (GTK_BOX (box), check); + + g_object_unref (model); + + button = drop_down_new_from_strings (times, NULL, NULL); + gtk_box_append (GTK_BOX (box), button); + + button = drop_down_new_from_strings (many_times, NULL, NULL); + gtk_box_append (GTK_BOX (box), button); + + button = drop_down_new_from_strings (many_times, NULL, NULL); + gtk_drop_down_set_enable_search (GTK_DROP_DOWN (button), TRUE); + expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL, + 0, NULL, + (GCallback)get_title, + NULL, NULL); + gtk_drop_down_set_expression (GTK_DROP_DOWN (button), expression); + gtk_expression_unref (expression); + gtk_box_append (GTK_BOX (box), button); + + button = drop_down_new_from_strings (device_titles, device_icons, device_descriptions); + gtk_box_append (GTK_BOX (box), button); + + gtk_window_present (GTK_WINDOW (window)); + + while (!quit) + g_main_context_iteration (NULL, TRUE); + + return 0; +} diff --git a/testsuite/gtk/defaultvalue.c b/testsuite/gtk/defaultvalue.c index 79ed6179f3..98f6dc7b43 100644 --- a/testsuite/gtk/defaultvalue.c +++ b/testsuite/gtk/defaultvalue.c @@ -411,6 +411,10 @@ G_GNUC_END_IGNORE_DEPRECATIONS strcmp (pspec->name, "adjustment") == 0) continue; + if (g_type_is_a (type, GTK_TYPE_DROP_DOWN) && + strcmp (pspec->name, "factory") == 0) + continue; + /* All the icontheme properties depend on the environment */ if (g_type_is_a (type, GTK_TYPE_ICON_THEME)) continue; diff --git a/testsuite/gtk/notify.c b/testsuite/gtk/notify.c index 62c22f2920..a5944a2b5c 100644 --- a/testsuite/gtk/notify.c +++ b/testsuite/gtk/notify.c @@ -647,6 +647,11 @@ test_type (gconstpointer data) g_str_equal (pspec->name, "selected-item"))) continue; + /* can't select items without an underlying, populated model */ + if (g_type_is_a (type, GTK_TYPE_DROP_DOWN) && + g_str_equal (pspec->name, "selected")) + continue; + /* can't set position without a notebook */ if (g_type_is_a (type, GTK_TYPE_NOTEBOOK_PAGE) && g_str_equal (pspec->name, "position"))