[gtk/prop-list: 207/220] Add GtkDropDown



commit b91449a464681450afdd71cbcd86f5448d78a067
Author: Matthias Clasen <mclasen redhat com>
Date:   Sun Dec 8 20:22:06 2019 -0500

    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                    | 1208 ++++++++++++++++++++++++++++++++++
 gtk/gtkdropdown.h                    |   80 +++
 gtk/meson.build                      |    2 +
 gtk/ui/gtkdropdown.ui                |   75 +++
 tests/meson.build                    |    1 +
 tests/testdropdown.c                 |  305 +++++++++
 10 files changed, 1699 insertions(+)
---
diff --git a/docs/reference/gtk/gtk4-docs.xml b/docs/reference/gtk/gtk4-docs.xml
index c70313380a..dd4876623d 100644
--- a/docs/reference/gtk/gtk4-docs.xml
+++ b/docs/reference/gtk/gtk4-docs.xml
@@ -87,6 +87,7 @@
       <xi:include href="xml/gtkgridview.xml" />
       <xi:include href="xml/gtkcolumnview.xml" />
       <xi:include href="xml/gtkcolumnviewcolumn.xml" />
+      <xi:include href="xml/gtkdropdown.xml" />
     </chapter>
 
     <chapter id="Trees">
@@ -270,6 +271,7 @@
       <xi:include href="xml/gtkpopover.xml" />
       <xi:include href="xml/gtkpopovermenu.xml" />
       <xi:include href="xml/gtkpopovermenubar.xml" />
+      <xi:include href="xml/gtkdropdown.xml" />
     </chapter>
 
     <chapter id="SelectorWidgets">
diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt
index 9ab86b2332..5f8f12e970 100644
--- a/docs/reference/gtk/gtk4-sections.txt
+++ b/docs/reference/gtk/gtk4-sections.txt
@@ -7823,3 +7823,27 @@ gtk_string_filter_set_match_mode
 <SUBSECTION Private>
 gtk_string_filter_get_type
 </SECTION>
+
+<SECTION>
+<FILE>gtkdropdown</FILE>
+<TITLE>GtkDropDown</TITLE>
+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
+<SUBSECTION Standard>
+GTK_TYPE_DROP_DOWN
+<SUBSECTION Private>
+gtk_drop_down_get_type
+</SECTION>
diff --git a/docs/reference/gtk/gtk4.types.in b/docs/reference/gtk/gtk4.types.in
index 65b0469c26..4a64ecef46 100644
--- a/docs/reference/gtk/gtk4.types.in
+++ b/docs/reference/gtk/gtk4.types.in
@@ -66,6 +66,7 @@ gtk_custom_filter_get_type
 gtk_dialog_get_type
 gtk_directory_list_get_type
 gtk_drawing_area_get_type
+gtk_drop_down_get_type
 gtk_editable_get_type
 gtk_entry_buffer_get_type
 gtk_entry_completion_get_type
diff --git a/gtk/gtk.h b/gtk/gtk.h
index e606c7f0b5..6a1e8f40fd 100644
--- a/gtk/gtk.h
+++ b/gtk/gtk.h
@@ -101,6 +101,7 @@
 #include <gtk/gtkdragdest.h>
 #include <gtk/gtkdragsource.h>
 #include <gtk/gtkdrawingarea.h>
+#include <gtk/gtkdropdown.h>
 #include <gtk/gtkeditable.h>
 #include <gtk/gtkentry.h>
 #include <gtk/gtkentrybuffer.h>
diff --git a/gtk/gtkdropdown.c b/gtk/gtkdropdown.c
new file mode 100644
index 0000000000..8eda582b71
--- /dev/null
+++ b/gtk/gtkdropdown.c
@@ -0,0 +1,1208 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Matthias Clasen <mclasen redhat com>
+ */
+
+#include "config.h"
+
+#include "gtkdropdown.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 "gtkiconprivate.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 <items> element and specifying <item>
+ * elements for each item. Using <items> is equivalent to calling
+ * gtk_drop_down_set_from_strings(). Each <item> element supports
+ * the regular translation attributes “translatable”, “context”
+ * and “comments”.
+ *
+ * Here is a UI definition fragment specifying GtkDropDown items:
+ * |[
+ * <object class="GtkDropDown">
+ *   <items>
+ *     <item translatable="yes">Factory</item>
+ *     <item translatable="yes">Home</item>
+ *     <item translatable="yes">Subway</item>
+ *   </items>
+ * </object>
+ * ]|
+
+ */
+
+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
+{
+  GtkDropDownClass 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 (GtkSelectionModel *selection,
+                   guint              position,
+                   guint              n_items,
+                   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 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;
+
+  /**
+   * GtkDropDown:factory:
+   *
+   * Factory for populating list items.
+   */
+  properties[PROP_FACTORY] =
+    g_param_spec_object ("factory",
+                         P_("Factory"),
+                         P_("Factory for populating list item"),
+                         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 items displayed
+   */
+  properties[PROP_MODEL] =
+    g_param_spec_object ("model",
+                         P_("Model"),
+                         P_("Model for the items displayed"),
+                         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);
+
+  /**
+   * GtkDtropDown: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_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's currently used to read the items displayed.
+ *
+ * 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.
+ *
+ * If @model is a #GtkSingleSelection, then its selection will be updated to
+ * reflect the value of #GtkDropDown:selected.
+ */
+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);
+
+      if (GTK_IS_SINGLE_SELECTION (model))
+        selection = g_object_ref (model);
+      else
+        selection = G_LIST_MODEL (gtk_single_selection_new (model));
+      g_set_object (&self->selection, selection);
+      g_object_unref (selection);
+
+      g_signal_connect (self->selection, "selection-changed", G_CALLBACK (selection_changed), self);
+      selection_changed (GTK_SELECTION_MODEL (self->selection), 0, 0, 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 unset.
+ *
+ * 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
+ * @selected: 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Matthias Clasen
+ */
+
+#ifndef __GTK_DROP_DOWN_H__
+#define __GTK_DROP_DOWN_H__
+
+#include <gtk/gtkwidget.h>
+#include <gtk/gtkexpression.h>
+
+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 e41030ad5f..034392be20 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -230,6 +230,7 @@ gtk_public_sources = files([
   'gtkdragdest.c',
   'gtkdragsource.c',
   'gtkdrawingarea.c',
+  'gtkdropdown.c',
   'gtkeditable.c',
   'gtkemojichooser.c',
   'gtkemojicompletion.c',
@@ -523,6 +524,7 @@ gtk_public_headers = files([
   'gtkdragdest.h',
   'gtkdragsource.h',
   'gtkdrawingarea.h',
+  'gtkdropdown.h',
   'gtkeditable.h',
   'gtkentry.h',
   'gtkentrybuffer.h',
diff --git a/gtk/ui/gtkdropdown.ui b/gtk/ui/gtkdropdown.ui
new file mode 100644
index 0000000000..b9a1c9c38a
--- /dev/null
+++ b/gtk/ui/gtkdropdown.ui
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="gtk40">
+  <template class="GtkDropDown" parent="GtkWidget">
+    <child>
+      <object class="GtkToggleButton" id="button">
+        <signal name="toggled" handler="button_toggled"/>
+        <child>
+          <object class="GtkBox">
+            <child>
+              <object class="GtkStack" id="button_stack">
+                <property name="halign">fill</property>
+                <property name="hexpand">1</property>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">empty</property>
+                    <property name="child">
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">(None)</property>
+                        <property name="xalign">0</property>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">item</property>
+                    <property name="child">
+                      <object class="GtkListItemWidget" id="button_item"/>
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkIcon">
+                <property name="css-name">arrow</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkPopover" id="popup">
+    <signal name="closed" handler="popover_closed"/>
+    <property name="relative-to">GtkDropDown</property>
+    <property name="has-arrow">0</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkSearchEntry" id="search_entry">
+            <signal name="search-changed" handler="search_changed"/>
+            <property name="visible">0</property>
+            <property name="placeholder-text" translatable="yes">Search…</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow">
+            <property name="hscrollbar-policy">never</property>
+            <property name="vscrollbar-policy">automatic</property>
+            <property name="max-content-height">400</property>
+            <property name="propagate-natural-height">1</property>
+            <child>
+              <object class="GtkListView" id="popup_list">
+                <signal name="activate" handler="row_activated"/>
+                <property name="single-click-activate">1</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/tests/meson.build b/tests/meson.build
index 2be5d96dae..479ae8db9b 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..9fadfddc19
--- /dev/null
+++ b/tests/testdropdown.c
@@ -0,0 +1,305 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ */
+#include <gtk/gtk.h>
+
+
+#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_container_add (GTK_CONTAINER (box), image);
+  gtk_container_add (GTK_CONTAINER (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_container_add (GTK_CONTAINER (box), image);
+  gtk_container_add (GTK_CONTAINER (box), box2);
+  gtk_container_add (GTK_CONTAINER (box2), title);
+  gtk_container_add (GTK_CONTAINER (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);
+}
+
+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);
+}
+
+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 
+  }; 
+
+  gtk_init ();
+
+  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+  gtk_window_set_title (GTK_WINDOW (window), "hello world");
+  gtk_window_set_resizable (GTK_WINDOW (window), TRUE);
+  g_signal_connect (window, "destroy", G_CALLBACK (gtk_main_quit), NULL);
+
+  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
+  g_object_set (box, "margin", 10, NULL);
+  gtk_container_add (GTK_CONTAINER (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_container_add (GTK_CONTAINER (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_container_add (GTK_CONTAINER (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_container_add (GTK_CONTAINER (box), check);
+
+  g_object_unref (model);
+
+  button = drop_down_new_from_strings (times, NULL, NULL);
+  gtk_container_add (GTK_CONTAINER (box), button);
+
+  button = drop_down_new_from_strings (many_times, NULL, NULL);
+  gtk_container_add (GTK_CONTAINER (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_container_add (GTK_CONTAINER (box), button);
+
+  button = drop_down_new_from_strings (device_titles, device_icons, device_descriptions);
+  gtk_container_add (GTK_CONTAINER (box), button);
+
+  gtk_widget_show (window);
+
+  gtk_main ();
+
+  return 0;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]