/* gtkcombobox.c * Copyright (C) 2002, 2003 Kristian Rietveld * * 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 "config.h" #include "gtkcomboboxprivate.h" #include "gtkbox.h" #include "gtkcellareabox.h" #include "gtkcelllayout.h" #include "gtkcellrenderertext.h" #include "gtkcellview.h" #include "gtkeventcontrollerkey.h" #include "gtkeventcontrollerscroll.h" #include "gtkframe.h" #include "gtkbuiltiniconprivate.h" #include "gtkintl.h" #include "gtkliststore.h" #include "gtkmain.h" #include "gtkmarshalers.h" #include "gtkprivate.h" #include "gtkshortcutcontroller.h" #include "gtktogglebutton.h" #include "gtktreepopoverprivate.h" #include "gtktypebuiltins.h" #include "gtkwidgetprivate.h" #include #include #include /** * GtkComboBox: * * A `GtkComboBox` is a widget that allows the user to choose from a list of * valid choices. * * ![An example GtkComboBox](combo-box.png) * * The `GtkComboBox` displays the selected choice; when activated, the * `GtkComboBox` displays a popup which allows the user to make a new choice. * * The `GtkComboBox` uses the model-view pattern; the list of valid choices * is specified in the form of a tree model, and the display of the choices * can be adapted to the data in the model by using cell renderers, as you * would in a tree view. This is possible since `GtkComboBox` implements the * [iface@Gtk.CellLayout] interface. The tree model holding the valid * choices is not restricted to a flat list, it can be a real tree, and the * popup will reflect the tree structure. * * To allow the user to enter values not in the model, the * [property@Gtk.ComboBox:has-entry] property allows the `GtkComboBox` to * contain a [class@Gtk.Entry]. This entry can be accessed by calling * [method@Gtk.ComboBox.get_child] on the combo box. * * For a simple list of textual choices, the model-view API of `GtkComboBox` * can be a bit overwhelming. In this case, [class@Gtk.ComboBoxText] offers * a simple alternative. Both `GtkComboBox` and `GtkComboBoxText` can contain * an entry. * * ## CSS nodes * * ``` * combobox * ├── box.linked * │ ╰── button.combo * │ ╰── box * │ ├── cellview * │ ╰── arrow * ╰── window.popup * ``` * * A normal combobox contains a box with the .linked class, a button * with the .combo class and inside those buttons, there are a cellview and * an arrow. * * ``` * combobox * ├── box.linked * │ ├── entry.combo * │ ╰── button.combo * │ ╰── box * │ ╰── arrow * ╰── window.popup * ``` * * A `GtkComboBox` with an entry has a single CSS node with name combobox. * It contains a box with the .linked class. That box contains an entry and * a button, both with the .combo class added. The button also contains another * node with name arrow. * * # Accessibility * * `GtkComboBox` uses the %GTK_ACCESSIBLE_ROLE_COMBO_BOX role. */ typedef struct { GtkWidget *child; GtkTreeModel *model; GtkCellArea *area; int active; /* Only temporary */ GtkTreeRowReference *active_row; GtkWidget *cell_view; GtkWidget *box; GtkWidget *button; GtkWidget *arrow; GtkWidget *popup_widget; guint popup_idle_id; guint scroll_timer; guint resize_idle_id; /* For "has-entry" specific behavior we track * an automated cell renderer and text column */ int text_column; GtkCellRenderer *text_renderer; int id_column; guint popup_in_progress : 1; guint popup_shown : 1; guint has_frame : 1; guint is_cell_renderer : 1; guint editing_canceled : 1; guint auto_scroll : 1; guint button_sensitivity : 2; guint has_entry : 1; guint popup_fixed_width : 1; GtkTreeViewRowSeparatorFunc row_separator_func; gpointer row_separator_data; GDestroyNotify row_separator_destroy; } GtkComboBoxPrivate; /* There are 2 modes to this widget, which can be characterized as follows: * * 1) no child added: * * cell_view -> GtkCellView, regular child * button -> GtkToggleButton set_parent to combo * arrow -> GtkArrow set_parent to button * popup_widget -> GtkMenu * * 2) child added: * * cell_view -> NULL * button -> GtkToggleButton set_parent to combo * arrow -> GtkArrow, child of button * popup_widget -> GtkMenu */ enum { CHANGED, MOVE_ACTIVE, POPUP, POPDOWN, FORMAT_ENTRY_TEXT, LAST_SIGNAL }; enum { PROP_0, PROP_MODEL, PROP_ACTIVE, PROP_HAS_FRAME, PROP_POPUP_SHOWN, PROP_BUTTON_SENSITIVITY, PROP_EDITING_CANCELED, PROP_HAS_ENTRY, PROP_ENTRY_TEXT_COLUMN, PROP_POPUP_FIXED_WIDTH, PROP_ID_COLUMN, PROP_ACTIVE_ID, PROP_CHILD }; static guint combo_box_signals[LAST_SIGNAL] = {0,}; /* common */ static void gtk_combo_box_cell_layout_init (GtkCellLayoutIface *iface); static void gtk_combo_box_cell_editable_init (GtkCellEditableIface *iface); static void gtk_combo_box_constructed (GObject *object); static void gtk_combo_box_dispose (GObject *object); static void gtk_combo_box_unmap (GtkWidget *widget); static void gtk_combo_box_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *spec); static void gtk_combo_box_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *spec); static gboolean gtk_combo_box_grab_focus (GtkWidget *widget); static void gtk_combo_box_button_toggled (GtkWidget *widget, gpointer data); static void gtk_combo_box_menu_show (GtkWidget *menu, gpointer user_data); static void gtk_combo_box_menu_hide (GtkWidget *menu, gpointer user_data); static void gtk_combo_box_unset_model (GtkComboBox *combo_box); static void gtk_combo_box_set_active_internal (GtkComboBox *combo_box, GtkTreePath *path); static void gtk_combo_box_real_move_active (GtkComboBox *combo_box, GtkScrollType scroll); static void gtk_combo_box_real_popup (GtkComboBox *combo_box); static gboolean gtk_combo_box_real_popdown (GtkComboBox *combo_box); static gboolean gtk_combo_box_scroll_controller_scroll (GtkEventControllerScroll *scroll, double dx, double dy, GtkComboBox *combo_box); /* listening to the model */ static void gtk_combo_box_model_row_inserted (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data); static void gtk_combo_box_model_row_deleted (GtkTreeModel *model, GtkTreePath *path, gpointer user_data); static void gtk_combo_box_model_rows_reordered (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, int *new_order, gpointer user_data); static void gtk_combo_box_model_row_changed (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data); static void gtk_combo_box_menu_activate (GtkWidget *menu, const char *path, GtkComboBox *combo_box); static void gtk_combo_box_update_sensitivity (GtkComboBox *combo_box); static gboolean gtk_combo_box_menu_key (GtkEventControllerKey *key, guint keyval, guint keycode, GdkModifierType modifiers, GtkComboBox *combo_box); static void gtk_combo_box_menu_popup (GtkComboBox *combo_box); /* cell layout */ static GtkCellArea *gtk_combo_box_cell_layout_get_area (GtkCellLayout *cell_layout); static gboolean gtk_combo_box_mnemonic_activate (GtkWidget *widget, gboolean group_cycling); static void gtk_combo_box_child_show (GtkWidget *widget, GtkComboBox *combo_box); static void gtk_combo_box_child_hide (GtkWidget *widget, GtkComboBox *combo_box); /* GtkComboBox:has-entry callbacks */ static void gtk_combo_box_entry_contents_changed (GtkEntry *entry, gpointer user_data); static void gtk_combo_box_entry_active_changed (GtkComboBox *combo_box, gpointer user_data); static char *gtk_combo_box_format_entry_text (GtkComboBox *combo_box, const char *path); /* GtkBuildable method implementation */ static GtkBuildableIface *parent_buildable_iface; static void gtk_combo_box_buildable_init (GtkBuildableIface *iface); static void gtk_combo_box_buildable_add_child (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const char *type); static gboolean gtk_combo_box_buildable_custom_tag_start (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const char *tagname, GtkBuildableParser *parser, gpointer *data); static void gtk_combo_box_buildable_custom_tag_end (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const char *tagname, gpointer data); static GObject *gtk_combo_box_buildable_get_internal_child (GtkBuildable *buildable, GtkBuilder *builder, const char *childname); /* GtkCellEditable method implementations */ static void gtk_combo_box_start_editing (GtkCellEditable *cell_editable, GdkEvent *event); G_DEFINE_TYPE_WITH_CODE (GtkComboBox, gtk_combo_box, GTK_TYPE_WIDGET, G_ADD_PRIVATE (GtkComboBox) G_IMPLEMENT_INTERFACE (GTK_TYPE_CELL_LAYOUT, gtk_combo_box_cell_layout_init) G_IMPLEMENT_INTERFACE (GTK_TYPE_CELL_EDITABLE, gtk_combo_box_cell_editable_init) G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, gtk_combo_box_buildable_init)) /* common */ static void gtk_combo_box_measure (GtkWidget *widget, GtkOrientation orientation, int size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { GtkComboBox *combo_box = GTK_COMBO_BOX (widget); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); gtk_widget_measure (priv->box, orientation, size, minimum, natural, minimum_baseline, natural_baseline); } static void gtk_combo_box_size_allocate (GtkWidget *widget, int width, int height, int baseline) { GtkComboBox *combo_box = GTK_COMBO_BOX (widget); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); int menu_width; gtk_widget_size_allocate (priv->box, &(GtkAllocation) { 0, 0, width, height }, baseline); gtk_widget_set_size_request (priv->popup_widget, -1, -1); if (priv->popup_fixed_width) gtk_widget_measure (priv->popup_widget, GTK_ORIENTATION_HORIZONTAL, -1, &menu_width, NULL, NULL, NULL); else gtk_widget_measure (priv->popup_widget, GTK_ORIENTATION_HORIZONTAL, -1, NULL, &menu_width, NULL, NULL); gtk_widget_set_size_request (priv->popup_widget, MAX (width, menu_width), -1); gtk_popover_present (GTK_POPOVER (priv->popup_widget)); } static void gtk_combo_box_compute_expand (GtkWidget *widget, gboolean *hexpand, gboolean *vexpand) { GtkComboBox *combo_box = GTK_COMBO_BOX (widget); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkWidget *child = priv->child; if (child && child != priv->cell_view) { *hexpand = gtk_widget_compute_expand (child, GTK_ORIENTATION_HORIZONTAL); *vexpand = gtk_widget_compute_expand (child, GTK_ORIENTATION_VERTICAL); } else { *hexpand = FALSE; *vexpand = FALSE; } } static void gtk_combo_box_class_init (GtkComboBoxClass *klass) { GObjectClass *object_class; GtkWidgetClass *widget_class; widget_class = (GtkWidgetClass *)klass; widget_class->mnemonic_activate = gtk_combo_box_mnemonic_activate; widget_class->grab_focus = gtk_combo_box_grab_focus; widget_class->focus = gtk_widget_focus_child; widget_class->measure = gtk_combo_box_measure; widget_class->size_allocate = gtk_combo_box_size_allocate; widget_class->unmap = gtk_combo_box_unmap; widget_class->compute_expand = gtk_combo_box_compute_expand; object_class = (GObjectClass *)klass; object_class->constructed = gtk_combo_box_constructed; object_class->dispose = gtk_combo_box_dispose; object_class->set_property = gtk_combo_box_set_property; object_class->get_property = gtk_combo_box_get_property; klass->format_entry_text = gtk_combo_box_format_entry_text; /* signals */ /** * GtkComboBox::changed: * @widget: the object which received the signal * * Emitted when the active item is changed. * * The can be due to the user selecting a different item from the list, * or due to a call to [method@Gtk.ComboBox.set_active_iter]. It will * also be emitted while typing into the entry of a combo box with an entry. */ combo_box_signals[CHANGED] = g_signal_new (I_("changed"), G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GtkComboBoxClass, changed), NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtkComboBox::move-active: * @widget: the object that received the signal * @scroll_type: a `GtkScrollType` * * Emitted to move the active selection. * * This is an [keybinding signal](class.SignalAction.html). */ combo_box_signals[MOVE_ACTIVE] = g_signal_new_class_handler (I_("move-active"), G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (gtk_combo_box_real_move_active), NULL, NULL, NULL, G_TYPE_NONE, 1, GTK_TYPE_SCROLL_TYPE); /** * GtkComboBox::popup: * @widget: the object that received the signal * * Emitted to popup the combo box list. * * This is an [keybinding signal](class.SignalAction.html). * * The default binding for this signal is Alt+Down. */ combo_box_signals[POPUP] = g_signal_new_class_handler (I_("popup"), G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (gtk_combo_box_real_popup), NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtkComboBox::popdown: * @button: the object which received the signal * * Emitted to popdown the combo box list. * * This is an [keybinding signal](class.SignalAction.html). * * The default bindings for this signal are Alt+Up and Escape. */ combo_box_signals[POPDOWN] = g_signal_new_class_handler (I_("popdown"), G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (gtk_combo_box_real_popdown), NULL, NULL, _gtk_marshal_BOOLEAN__VOID, G_TYPE_BOOLEAN, 0); /** * GtkComboBox::format-entry-text: * @combo: the object which received the signal * @path: the [struct@Gtk.TreePath] string from the combo box's current model * to format text for * * Emitted to allow changing how the text in a combo box's entry is displayed. * * See [property@Gtk.ComboBox:has-entry]. * * Connect a signal handler which returns an allocated string representing * @path. That string will then be used to set the text in the combo box's * entry. The default signal handler uses the text from the * [property@Gtk.ComboBox:entry-text-column] model column. * * Here's an example signal handler which fetches data from the model and * displays it in the entry. * ```c * static char * * format_entry_text_callback (GtkComboBox *combo, * const char *path, * gpointer user_data) * { * GtkTreeIter iter; * GtkTreeModel model; * double value; * * model = gtk_combo_box_get_model (combo); * * gtk_tree_model_get_iter_from_string (model, &iter, path); * gtk_tree_model_get (model, &iter, * THE_DOUBLE_VALUE_COLUMN, &value, * -1); * * return g_strdup_printf ("%g", value); * } * ``` * * Returns: (transfer full): a newly allocated string representing @path * for the current `GtkComboBox` model. */ combo_box_signals[FORMAT_ENTRY_TEXT] = g_signal_new (I_("format-entry-text"), G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GtkComboBoxClass, format_entry_text), _gtk_single_string_accumulator, NULL, _gtk_marshal_STRING__STRING, G_TYPE_STRING, 1, G_TYPE_STRING); /* key bindings */ gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Down, GDK_ALT_MASK, "popup", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Down, GDK_ALT_MASK, "popup", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Up, GDK_ALT_MASK, "popdown", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Up, GDK_ALT_MASK, "popdown", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Escape, 0, "popdown", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Up, 0, "move-active", "(i)", GTK_SCROLL_STEP_UP); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Up, 0, "move-active", "(i)", GTK_SCROLL_STEP_UP); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Page_Up, 0, "move-active", "(i)", GTK_SCROLL_PAGE_UP); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Page_Up, 0, "move-active", "(i)", GTK_SCROLL_PAGE_UP); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Home, 0, "move-active", "(i)", GTK_SCROLL_START); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Home, 0, "move-active", "(i)", GTK_SCROLL_START); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Down, 0, "move-active", "(i)", GTK_SCROLL_STEP_DOWN); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Down, 0, "move-active", "(i)", GTK_SCROLL_STEP_DOWN); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Page_Down, 0, "move-active", "(i)", GTK_SCROLL_PAGE_DOWN); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Page_Down, 0, "move-active", "(i)", GTK_SCROLL_PAGE_DOWN); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_End, 0, "move-active", "(i)", GTK_SCROLL_END); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_End, 0, "move-active", "(i)", GTK_SCROLL_END); /* properties */ g_object_class_override_property (object_class, PROP_EDITING_CANCELED, "editing-canceled"); /** * GtkComboBox:model: (attributes org.gtk.Property.get=gtk_combo_box_get_model org.gtk.Property.set=gtk_combo_box_set_model) * * The model from which the combo box takes its values. */ g_object_class_install_property (object_class, PROP_MODEL, g_param_spec_object ("model", P_("ComboBox model"), P_("The model for the combo box"), GTK_TYPE_TREE_MODEL, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); /** * GtkComboBox:active: (attributes org.gtk.Property.get=gtk_combo_box_get_active org.gtk.Property.set=gtk_combo_box_set_active) * * The item which is currently active. * * If the model is a non-flat treemodel, and the active item is not an * immediate child of the root of the tree, this property has the value * `gtk_tree_path_get_indices (path)[0]`, where `path` is the * [struct@Gtk.TreePath] of the active item. */ g_object_class_install_property (object_class, PROP_ACTIVE, g_param_spec_int ("active", P_("Active item"), P_("The item which is currently active"), -1, G_MAXINT, -1, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); /** * GtkComboBox:has-frame: * * The `has-frame` property controls whether a frame is drawn around the entry. */ g_object_class_install_property (object_class, PROP_HAS_FRAME, g_param_spec_boolean ("has-frame", P_("Has Frame"), P_("Whether the combo box draws a frame around the child"), TRUE, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); /** * GtkComboBox:popup-shown: * * Whether the combo boxes dropdown is popped up. * * Note that this property is mainly useful, because * it allows you to connect to notify::popup-shown. */ g_object_class_install_property (object_class, PROP_POPUP_SHOWN, g_param_spec_boolean ("popup-shown", P_("Popup shown"), P_("Whether the combo’s dropdown is shown"), FALSE, GTK_PARAM_READABLE)); /** * GtkComboBox:button-sensitivity: (attributes org.gtk.Property.get=gtk_combo_box_get_button_sensitivity org.gtk.Property.set=gtk_combo_box_set_button_sensitivity) * * Whether the dropdown button is sensitive when * the model is empty. */ g_object_class_install_property (object_class, PROP_BUTTON_SENSITIVITY, g_param_spec_enum ("button-sensitivity", P_("Button Sensitivity"), P_("Whether the dropdown button is sensitive when the model is empty"), GTK_TYPE_SENSITIVITY_TYPE, GTK_SENSITIVITY_AUTO, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); /** * GtkComboBox:has-entry: (attributes org.gtk.Property.get=gtk_combo_box_get_has_entry) * * Whether the combo box has an entry. */ g_object_class_install_property (object_class, PROP_HAS_ENTRY, g_param_spec_boolean ("has-entry", P_("Has Entry"), P_("Whether combo box has an entry"), FALSE, GTK_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); /** * GtkComboBox:entry-text-column: (attributes org.gtk.Property.get=gtk_combo_box_get_entry_text_column org.gtk.Property.set=gtk_combo_box_set_entry_text_column) * * The model column to associate with strings from the entry. * * This is property only relevant if the combo was created with * [property@Gtk.ComboBox:has-entry] is %TRUE. */ g_object_class_install_property (object_class, PROP_ENTRY_TEXT_COLUMN, g_param_spec_int ("entry-text-column", P_("Entry Text Column"), P_("The column in the combo box’s model to associate " "with strings from the entry if the combo was " "created with GtkComboBox:has-entry = %TRUE"), -1, G_MAXINT, -1, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); /** * GtkComboBox:id-column: (attributes org.gtk.Property.get=gtk_combo_box_get_id_column org.gtk.Property.set=gtk_combo_box_set_id_column) * * The model column that provides string IDs for the values * in the model, if != -1. */ g_object_class_install_property (object_class, PROP_ID_COLUMN, g_param_spec_int ("id-column", P_("ID Column"), P_("The column in the combo box’s model that provides " "string IDs for the values in the model"), -1, G_MAXINT, -1, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); /** * GtkComboBox:active-id: (attributes org.gtk.Property.get=gtk_combo_box_get_active_id org.gtk.Property.set=gtk_combo_box_set_active_id) * * The value of the ID column of the active row. */ g_object_class_install_property (object_class, PROP_ACTIVE_ID, g_param_spec_string ("active-id", P_("Active id"), P_("The value of the id column " "for the active row"), NULL, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); /** * GtkComboBox:popup-fixed-width: (attributes org.gtk.Property.get=gtk_combo_box_get_popup_fixed_width org.gtk.Property.set=gtk_combo_box_set_popup_fixed_width) * * Whether the popup's width should be a fixed width matching the * allocated width of the combo box. */ g_object_class_install_property (object_class, PROP_POPUP_FIXED_WIDTH, g_param_spec_boolean ("popup-fixed-width", P_("Popup Fixed Width"), P_("Whether the popup’s width should be a " "fixed width matching the allocated width " "of the combo box"), TRUE, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); /** * GtkComboBox:child: (attributes org.gtk.Property.get=gtk_combo_box_get_child org.gtk.Property.set=gtk_combo_box_set_child) * * The child widget. */ g_object_class_install_property (object_class, PROP_CHILD, g_param_spec_object ("child", P_("Child"), P_("The child_widget"), GTK_TYPE_WIDGET, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkcombobox.ui"); gtk_widget_class_bind_template_child_internal_private (widget_class, GtkComboBox, box); gtk_widget_class_bind_template_child_internal_private (widget_class, GtkComboBox, button); gtk_widget_class_bind_template_child_internal_private (widget_class, GtkComboBox, arrow); gtk_widget_class_bind_template_child_internal_private (widget_class, GtkComboBox, area); gtk_widget_class_bind_template_child_internal_private (widget_class, GtkComboBox, popup_widget); gtk_widget_class_bind_template_callback (widget_class, gtk_combo_box_button_toggled); gtk_widget_class_bind_template_callback (widget_class, gtk_combo_box_menu_activate); gtk_widget_class_bind_template_callback (widget_class, gtk_combo_box_menu_key); gtk_widget_class_bind_template_callback (widget_class, gtk_combo_box_menu_show); gtk_widget_class_bind_template_callback (widget_class, gtk_combo_box_menu_hide); gtk_widget_class_set_css_name (widget_class, I_("combobox")); gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_COMBO_BOX); } static void gtk_combo_box_buildable_init (GtkBuildableIface *iface) { parent_buildable_iface = g_type_interface_peek_parent (iface); iface->add_child = gtk_combo_box_buildable_add_child; iface->custom_tag_start = gtk_combo_box_buildable_custom_tag_start; iface->custom_tag_end = gtk_combo_box_buildable_custom_tag_end; iface->get_internal_child = gtk_combo_box_buildable_get_internal_child; } static void gtk_combo_box_cell_layout_init (GtkCellLayoutIface *iface) { iface->get_area = gtk_combo_box_cell_layout_get_area; } static void gtk_combo_box_cell_editable_init (GtkCellEditableIface *iface) { iface->start_editing = gtk_combo_box_start_editing; } static gboolean gtk_combo_box_row_separator_func (GtkTreeModel *model, GtkTreeIter *iter, GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->row_separator_func) return priv->row_separator_func (model, iter, priv->row_separator_data); return FALSE; } static void gtk_combo_box_init (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkEventController *controller; GtkEventController **controllers; guint n_controllers, i; priv->active = -1; priv->active_row = NULL; priv->popup_shown = FALSE; priv->has_frame = TRUE; priv->is_cell_renderer = FALSE; priv->editing_canceled = FALSE; priv->auto_scroll = FALSE; priv->button_sensitivity = GTK_SENSITIVITY_AUTO; priv->has_entry = FALSE; priv->popup_fixed_width = TRUE; priv->text_column = -1; priv->text_renderer = NULL; priv->id_column = -1; g_type_ensure (GTK_TYPE_BUILTIN_ICON); g_type_ensure (GTK_TYPE_TREE_POPOVER); gtk_widget_init_template (GTK_WIDGET (combo_box)); gtk_widget_remove_css_class (priv->button, "toggle"); gtk_widget_add_css_class (priv->button, "combo"); gtk_tree_popover_set_row_separator_func (GTK_TREE_POPOVER (priv->popup_widget), (GtkTreeViewRowSeparatorFunc)gtk_combo_box_row_separator_func, combo_box, NULL); controller = gtk_event_controller_scroll_new (GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE); g_signal_connect (controller, "scroll", G_CALLBACK (gtk_combo_box_scroll_controller_scroll), combo_box); gtk_widget_add_controller (GTK_WIDGET (combo_box), controller); controllers = gtk_widget_list_controllers (priv->popup_widget, GTK_PHASE_BUBBLE, &n_controllers); for (i = 0; i < n_controllers; i ++) { controller = controllers[i]; if (GTK_IS_SHORTCUT_CONTROLLER (controller)) { g_object_ref (controller); gtk_widget_remove_controller (priv->popup_widget, controller); gtk_widget_add_controller (priv->popup_widget, controller); break; } } g_free (controllers); } static void gtk_combo_box_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtkComboBox *combo_box = GTK_COMBO_BOX (object); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); switch (prop_id) { case PROP_MODEL: gtk_combo_box_set_model (combo_box, g_value_get_object (value)); break; case PROP_ACTIVE: gtk_combo_box_set_active (combo_box, g_value_get_int (value)); break; case PROP_HAS_FRAME: if (priv->has_frame != g_value_get_boolean (value)) { priv->has_frame = g_value_get_boolean (value); if (priv->has_entry) gtk_entry_set_has_frame (GTK_ENTRY (priv->child), priv->has_frame); g_object_notify (object, "has-frame"); } break; case PROP_POPUP_SHOWN: if (g_value_get_boolean (value)) gtk_combo_box_popup (combo_box); else gtk_combo_box_popdown (combo_box); break; case PROP_BUTTON_SENSITIVITY: gtk_combo_box_set_button_sensitivity (combo_box, g_value_get_enum (value)); break; case PROP_POPUP_FIXED_WIDTH: gtk_combo_box_set_popup_fixed_width (combo_box, g_value_get_boolean (value)); break; case PROP_EDITING_CANCELED: if (priv->editing_canceled != g_value_get_boolean (value)) { priv->editing_canceled = g_value_get_boolean (value); g_object_notify (object, "editing-canceled"); } break; case PROP_HAS_ENTRY: priv->has_entry = g_value_get_boolean (value); break; case PROP_ENTRY_TEXT_COLUMN: gtk_combo_box_set_entry_text_column (combo_box, g_value_get_int (value)); break; case PROP_ID_COLUMN: gtk_combo_box_set_id_column (combo_box, g_value_get_int (value)); break; case PROP_ACTIVE_ID: gtk_combo_box_set_active_id (combo_box, g_value_get_string (value)); break; case PROP_CHILD: gtk_combo_box_set_child (combo_box, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_combo_box_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtkComboBox *combo_box = GTK_COMBO_BOX (object); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); switch (prop_id) { case PROP_MODEL: g_value_set_object (value, priv->model); break; case PROP_ACTIVE: g_value_set_int (value, gtk_combo_box_get_active (combo_box)); break; case PROP_HAS_FRAME: g_value_set_boolean (value, priv->has_frame); break; case PROP_POPUP_SHOWN: g_value_set_boolean (value, priv->popup_shown); break; case PROP_BUTTON_SENSITIVITY: g_value_set_enum (value, priv->button_sensitivity); break; case PROP_POPUP_FIXED_WIDTH: g_value_set_boolean (value, priv->popup_fixed_width); break; case PROP_EDITING_CANCELED: g_value_set_boolean (value, priv->editing_canceled); break; case PROP_HAS_ENTRY: g_value_set_boolean (value, priv->has_entry); break; case PROP_ENTRY_TEXT_COLUMN: g_value_set_int (value, priv->text_column); break; case PROP_ID_COLUMN: g_value_set_int (value, priv->id_column); break; case PROP_ACTIVE_ID: g_value_set_string (value, gtk_combo_box_get_active_id (combo_box)); break; case PROP_CHILD: g_value_set_object (value, gtk_combo_box_get_child (combo_box)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_combo_box_button_toggled (GtkWidget *widget, gpointer data) { GtkComboBox *combo_box = GTK_COMBO_BOX (data); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget))) { if (!priv->popup_in_progress) gtk_combo_box_popup (combo_box); } else { gtk_combo_box_popdown (combo_box); } } static void gtk_combo_box_create_child (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->has_entry) { GtkWidget *entry; entry = gtk_entry_new (); gtk_combo_box_set_child (combo_box, entry); gtk_widget_add_css_class (GTK_WIDGET (entry), "combo"); g_signal_connect (combo_box, "changed", G_CALLBACK (gtk_combo_box_entry_active_changed), NULL); } else { priv->cell_view = gtk_cell_view_new_with_context (priv->area, NULL); gtk_widget_set_hexpand (priv->cell_view, TRUE); gtk_cell_view_set_fit_model (GTK_CELL_VIEW (priv->cell_view), TRUE); gtk_cell_view_set_model (GTK_CELL_VIEW (priv->cell_view), priv->model); gtk_box_insert_child_after (GTK_BOX (gtk_widget_get_parent (priv->arrow)), priv->cell_view, NULL); priv->child = priv->cell_view; } } static void gtk_combo_box_add (GtkComboBox *combo_box, GtkWidget *widget) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->box == NULL) { gtk_widget_set_parent (widget, GTK_WIDGET (combo_box)); return; } if (priv->has_entry && !GTK_IS_ENTRY (widget)) { g_warning ("Attempting to add a widget with type %s to a GtkComboBox that needs an entry " "(need an instance of GtkEntry or of a subclass)", G_OBJECT_TYPE_NAME (widget)); return; } g_clear_pointer (&priv->cell_view, gtk_widget_unparent); gtk_widget_set_hexpand (widget, TRUE); gtk_box_insert_child_after (GTK_BOX (priv->box), widget, NULL); priv->child = widget; if (priv->has_entry) { g_signal_connect (widget, "changed", G_CALLBACK (gtk_combo_box_entry_contents_changed), combo_box); gtk_entry_set_has_frame (GTK_ENTRY (widget), priv->has_frame); } } static void gtk_combo_box_remove (GtkComboBox *combo_box, GtkWidget *widget) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreePath *path; if (priv->has_entry) { if (widget && widget == priv->child) g_signal_handlers_disconnect_by_func (widget, gtk_combo_box_entry_contents_changed, combo_box); } gtk_box_remove (GTK_BOX (priv->box), widget); priv->child = NULL; if (gtk_widget_in_destruction (GTK_WIDGET (combo_box))) return; gtk_widget_queue_resize (GTK_WIDGET (combo_box)); gtk_combo_box_create_child (combo_box); if (gtk_tree_row_reference_valid (priv->active_row)) { path = gtk_tree_row_reference_get_path (priv->active_row); gtk_combo_box_set_active_internal (combo_box, path); gtk_tree_path_free (path); } else { gtk_combo_box_set_active_internal (combo_box, NULL); } } static void gtk_combo_box_menu_show (GtkWidget *menu, gpointer user_data) { GtkComboBox *combo_box = GTK_COMBO_BOX (user_data); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); gtk_combo_box_child_show (menu, user_data); priv->popup_in_progress = TRUE; gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->button), TRUE); priv->popup_in_progress = FALSE; } static void gtk_combo_box_menu_hide (GtkWidget *menu, gpointer user_data) { GtkComboBox *combo_box = GTK_COMBO_BOX (user_data); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); gtk_combo_box_child_hide (menu,user_data); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->button), FALSE); } static gboolean cell_layout_is_sensitive (GtkCellLayout *layout) { GList *cells, *list; gboolean sensitive; cells = gtk_cell_layout_get_cells (layout); sensitive = FALSE; for (list = cells; list; list = list->next) { g_object_get (list->data, "sensitive", &sensitive, NULL); if (sensitive) break; } g_list_free (cells); return sensitive; } static gboolean tree_column_row_is_sensitive (GtkComboBox *combo_box, GtkTreeIter *iter) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->row_separator_func) { if (priv->row_separator_func (priv->model, iter, priv->row_separator_data)) return FALSE; } gtk_cell_area_apply_attributes (priv->area, priv->model, iter, FALSE, FALSE); return cell_layout_is_sensitive (GTK_CELL_LAYOUT (priv->area)); } static void gtk_combo_box_menu_popup (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); #if 0 int active_item; GtkWidget *active; int width, min_width, nat_width; #endif gtk_tree_popover_open_submenu (GTK_TREE_POPOVER (priv->popup_widget), "main"); gtk_popover_popup (GTK_POPOVER (priv->popup_widget)); #if 0 active_item = -1; if (gtk_tree_row_reference_valid (priv->active_row)) { GtkTreePath *path; path = gtk_tree_row_reference_get_path (priv->active_row); active_item = gtk_tree_path_get_indices (path)[0]; gtk_tree_path_free (path); } /* FIXME handle nested menus better */ //gtk_tree_popover_set_active (GTK_TREE_POPOVER (priv->popup_widget), active_item); width = gtk_widget_get_width (GTK_WIDGET (combo_box)); gtk_widget_set_size_request (priv->popup_widget, -1, -1); gtk_widget_measure (priv->popup_widget, GTK_ORIENTATION_HORIZONTAL, -1, &min_width, &nat_width, NULL, NULL); if (priv->popup_fixed_width) width = MAX (width, min_width); else width = MAX (width, nat_width); gtk_widget_set_size_request (priv->popup_widget, width, -1); g_signal_handlers_disconnect_by_func (priv->popup_widget, gtk_menu_update_scroll_offset, NULL); if (priv->cell_view == NULL) { g_object_set (priv->popup_widget, "anchor-hints", (GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_SLIDE | GDK_ANCHOR_RESIZE), "rect-anchor-dx", 0, NULL); gtk_menu_popup_at_widget (GTK_MENU (priv->popup_widget), gtk_bin_get_child (GTK_BIN (combo_box)), GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL); } else { int rect_anchor_dy = -2; GList *i; GtkWidget *child; /* FIXME handle nested menus better */ active = gtk_menu_get_active (GTK_MENU (priv->popup_widget)); if (!(active && gtk_widget_get_visible (active))) { GList *children; children = gtk_menu_shell_get_items (GTK_MENU_SHELL (priv->popup_widget)); for (i = children; i && !active; i = i->next) { child = i->data; if (child && gtk_widget_get_visible (child)) active = child; } g_list_free (children); } if (active) { int child_height; GList *children; children = gtk_menu_shell_get_items (GTK_MENU_SHELL (priv->popup_widget)); for (i = children; i && i->data != active; i = i->next) { child = i->data; if (child && gtk_widget_get_visible (child)) { gtk_widget_measure (child, GTK_ORIENTATION_VERTICAL, -1, &child_height, NULL, NULL, NULL); rect_anchor_dy -= child_height; } } g_list_free (children); gtk_widget_measure (active, GTK_ORIENTATION_VERTICAL, -1, &child_height, NULL, NULL, NULL); rect_anchor_dy -= child_height / 2; } g_object_set (priv->popup_widget, "anchor-hints", (GDK_ANCHOR_SLIDE | GDK_ANCHOR_RESIZE), "rect-anchor-dy", rect_anchor_dy, NULL); g_signal_connect (priv->popup_widget, "popped-up", G_CALLBACK (gtk_menu_update_scroll_offset), NULL); gtk_menu_popup_at_widget (GTK_MENU (priv->popup_widget), GTK_WIDGET (combo_box), GDK_GRAVITY_WEST, GDK_GRAVITY_NORTH_WEST, NULL); } #endif } /** * gtk_combo_box_popup: * @combo_box: a `GtkComboBox` * * Pops up the menu or dropdown list of @combo_box. * * This function is mostly intended for use by accessibility technologies; * applications should have little use for it. * * Before calling this, @combo_box must be mapped, or nothing will happen. */ void gtk_combo_box_popup (GtkComboBox *combo_box) { g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); if (gtk_widget_get_mapped (GTK_WIDGET (combo_box))) g_signal_emit (combo_box, combo_box_signals[POPUP], 0); } /** * gtk_combo_box_popup_for_device: * @combo_box: a `GtkComboBox` * @device: a `GdkDevice` * * Pops up the menu of @combo_box. * * Note that currently this does not do anything with the device, as it was * previously only used for list-mode combo boxes, and those were removed * in GTK 4. However, it is retained in case similar functionality is added * back later. */ void gtk_combo_box_popup_for_device (GtkComboBox *combo_box, GdkDevice *device) { /* As above, this currently does not do anything useful, and nothing with the * passed-in device. But the bits that are not blatantly obsolete are kept. */ GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); g_return_if_fail (GDK_IS_DEVICE (device)); if (!gtk_widget_get_realized (GTK_WIDGET (combo_box))) return; if (gtk_widget_get_mapped (priv->popup_widget)) return; gtk_combo_box_menu_popup (combo_box); } static void gtk_combo_box_real_popup (GtkComboBox *combo_box) { gtk_combo_box_menu_popup (combo_box); } static gboolean gtk_combo_box_real_popdown (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->popup_shown) { gtk_combo_box_popdown (combo_box); return TRUE; } return FALSE; } /** * gtk_combo_box_popdown: * @combo_box: a `GtkComboBox` * * Hides the menu or dropdown list of @combo_box. * * This function is mostly intended for use by accessibility technologies; * applications should have little use for it. */ void gtk_combo_box_popdown (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); gtk_popover_popdown (GTK_POPOVER (priv->popup_widget)); } static void gtk_combo_box_unset_model (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->model) { g_signal_handlers_disconnect_by_func (priv->model, gtk_combo_box_model_row_inserted, combo_box); g_signal_handlers_disconnect_by_func (priv->model, gtk_combo_box_model_row_deleted, combo_box); g_signal_handlers_disconnect_by_func (priv->model, gtk_combo_box_model_rows_reordered, combo_box); g_signal_handlers_disconnect_by_func (priv->model, gtk_combo_box_model_row_changed, combo_box); g_object_unref (priv->model); priv->model = NULL; } if (priv->active_row) { gtk_tree_row_reference_free (priv->active_row); priv->active_row = NULL; } if (priv->cell_view) gtk_cell_view_set_model (GTK_CELL_VIEW (priv->cell_view), NULL); } static void gtk_combo_box_child_show (GtkWidget *widget, GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); priv->popup_shown = TRUE; g_object_notify (G_OBJECT (combo_box), "popup-shown"); } static void gtk_combo_box_child_hide (GtkWidget *widget, GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); priv->popup_shown = FALSE; g_object_notify (G_OBJECT (combo_box), "popup-shown"); } typedef struct { GtkComboBox *combo; GtkTreePath *path; GtkTreeIter iter; gboolean found; gboolean set; } SearchData; static gboolean tree_next_func (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { SearchData *search_data = (SearchData *)data; if (search_data->found) { if (!tree_column_row_is_sensitive (search_data->combo, iter)) return FALSE; search_data->set = TRUE; search_data->iter = *iter; return TRUE; } if (gtk_tree_path_compare (path, search_data->path) == 0) search_data->found = TRUE; return FALSE; } static gboolean tree_next (GtkComboBox *combo, GtkTreeModel *model, GtkTreeIter *iter, GtkTreeIter *next) { SearchData search_data; search_data.combo = combo; search_data.path = gtk_tree_model_get_path (model, iter); search_data.found = FALSE; search_data.set = FALSE; gtk_tree_model_foreach (model, tree_next_func, &search_data); *next = search_data.iter; gtk_tree_path_free (search_data.path); return search_data.set; } static gboolean tree_prev_func (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { SearchData *search_data = (SearchData *)data; if (gtk_tree_path_compare (path, search_data->path) == 0) { search_data->found = TRUE; return TRUE; } if (!tree_column_row_is_sensitive (search_data->combo, iter)) return FALSE; search_data->set = TRUE; search_data->iter = *iter; return FALSE; } static gboolean tree_prev (GtkComboBox *combo, GtkTreeModel *model, GtkTreeIter *iter, GtkTreeIter *prev) { SearchData search_data; search_data.combo = combo; search_data.path = gtk_tree_model_get_path (model, iter); search_data.found = FALSE; search_data.set = FALSE; gtk_tree_model_foreach (model, tree_prev_func, &search_data); *prev = search_data.iter; gtk_tree_path_free (search_data.path); return search_data.set; } static gboolean tree_last_func (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { SearchData *search_data = (SearchData *)data; if (!tree_column_row_is_sensitive (search_data->combo, iter)) return FALSE; search_data->set = TRUE; search_data->iter = *iter; return FALSE; } static gboolean tree_last (GtkComboBox *combo, GtkTreeModel *model, GtkTreeIter *last) { SearchData search_data; search_data.combo = combo; search_data.set = FALSE; gtk_tree_model_foreach (model, tree_last_func, &search_data); *last = search_data.iter; return search_data.set; } static gboolean tree_first_func (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { SearchData *search_data = (SearchData *)data; if (!tree_column_row_is_sensitive (search_data->combo, iter)) return FALSE; search_data->set = TRUE; search_data->iter = *iter; return TRUE; } static gboolean tree_first (GtkComboBox *combo, GtkTreeModel *model, GtkTreeIter *first) { SearchData search_data; search_data.combo = combo; search_data.set = FALSE; gtk_tree_model_foreach (model, tree_first_func, &search_data); *first = search_data.iter; return search_data.set; } static gboolean gtk_combo_box_scroll_controller_scroll (GtkEventControllerScroll *scroll, double dx, double dy, GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); gboolean found = FALSE; GtkTreeIter iter; GtkTreeIter new_iter; if (!gtk_combo_box_get_active_iter (combo_box, &iter)) return GDK_EVENT_PROPAGATE; if (dy < 0) found = tree_prev (combo_box, priv->model, &iter, &new_iter); else if (dy > 0) found = tree_next (combo_box, priv->model, &iter, &new_iter); if (found) gtk_combo_box_set_active_iter (combo_box, &new_iter); return found; } /* callbacks */ static void gtk_combo_box_menu_activate (GtkWidget *menu, const char *path, GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreeIter iter; if (gtk_tree_model_get_iter_from_string (priv->model, &iter, path)) gtk_combo_box_set_active_iter (combo_box, &iter); g_object_set (combo_box, "editing-canceled", FALSE, NULL); } static void gtk_combo_box_update_sensitivity (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreeIter iter; gboolean sensitive = TRUE; /* fool code checkers */ if (!priv->button) return; switch (priv->button_sensitivity) { case GTK_SENSITIVITY_ON: sensitive = TRUE; break; case GTK_SENSITIVITY_OFF: sensitive = FALSE; break; case GTK_SENSITIVITY_AUTO: sensitive = priv->model && gtk_tree_model_get_iter_first (priv->model, &iter); break; default: g_assert_not_reached (); break; } gtk_widget_set_sensitive (priv->button, sensitive); } static void gtk_combo_box_model_row_inserted (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data) { GtkComboBox *combo_box = GTK_COMBO_BOX (user_data); gtk_combo_box_update_sensitivity (combo_box); } static void gtk_combo_box_model_row_deleted (GtkTreeModel *model, GtkTreePath *path, gpointer user_data) { GtkComboBox *combo_box = GTK_COMBO_BOX (user_data); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (!gtk_tree_row_reference_valid (priv->active_row)) { if (priv->cell_view) gtk_cell_view_set_displayed_row (GTK_CELL_VIEW (priv->cell_view), NULL); g_signal_emit (combo_box, combo_box_signals[CHANGED], 0); } gtk_combo_box_update_sensitivity (combo_box); } static void gtk_combo_box_model_rows_reordered (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, int *new_order, gpointer user_data) { gtk_tree_row_reference_reordered (G_OBJECT (user_data), path, iter, new_order); } static void gtk_combo_box_model_row_changed (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data) { GtkComboBox *combo_box = GTK_COMBO_BOX (user_data); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreePath *active_path; /* FIXME this belongs to GtkCellView */ if (gtk_tree_row_reference_valid (priv->active_row)) { active_path = gtk_tree_row_reference_get_path (priv->active_row); if (gtk_tree_path_compare (path, active_path) == 0 && priv->cell_view) gtk_widget_queue_resize (GTK_WIDGET (priv->cell_view)); gtk_tree_path_free (active_path); } } static gboolean gtk_combo_box_menu_key (GtkEventControllerKey *key, guint keyval, guint keycode, GdkModifierType modifiers, GtkComboBox *combo_box) { gtk_event_controller_key_forward (key, GTK_WIDGET (combo_box)); return TRUE; } /* * GtkCellLayout implementation */ static GtkCellArea * gtk_combo_box_cell_layout_get_area (GtkCellLayout *cell_layout) { GtkComboBox *combo_box = GTK_COMBO_BOX (cell_layout); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); return priv->area; } /* * public API */ /** * gtk_combo_box_new: * * Creates a new empty `GtkComboBox`. * * Returns: A new `GtkComboBox` */ GtkWidget * gtk_combo_box_new (void) { return g_object_new (GTK_TYPE_COMBO_BOX, NULL); } /** * gtk_combo_box_new_with_entry: * * Creates a new empty `GtkComboBox` with an entry. * * In order to use a combo box with entry, you need to tell it * which column of the model contains the text for the entry * by calling [method@Gtk.ComboBox.set_entry_text_column]. * * Returns: A new `GtkComboBox` */ GtkWidget * gtk_combo_box_new_with_entry (void) { return g_object_new (GTK_TYPE_COMBO_BOX, "has-entry", TRUE, NULL); } /** * gtk_combo_box_new_with_model: * @model: a `GtkTreeModel` * * Creates a new `GtkComboBox` with a model. * * Returns: A new `GtkComboBox` */ GtkWidget * gtk_combo_box_new_with_model (GtkTreeModel *model) { GtkComboBox *combo_box; g_return_val_if_fail (GTK_IS_TREE_MODEL (model), NULL); combo_box = g_object_new (GTK_TYPE_COMBO_BOX, "model", model, NULL); return GTK_WIDGET (combo_box); } /** * gtk_combo_box_new_with_model_and_entry: * @model: A `GtkTreeModel` * * Creates a new empty `GtkComboBox` with an entry and a model. * * See also [ctor@Gtk.ComboBox.new_with_entry]. * * Returns: A new `GtkComboBox` */ GtkWidget * gtk_combo_box_new_with_model_and_entry (GtkTreeModel *model) { return g_object_new (GTK_TYPE_COMBO_BOX, "has-entry", TRUE, "model", model, NULL); } /** * gtk_combo_box_get_active: * @combo_box: A `GtkComboBox` * * Returns the index of the currently active item. * * If the model is a non-flat treemodel, and the active item is not * an immediate child of the root of the tree, this function returns * `gtk_tree_path_get_indices (path)[0]`, where `path` is the * [struct@Gtk.TreePath] of the active item. * * Returns: An integer which is the index of the currently active item, * or -1 if there’s no active item */ int gtk_combo_box_get_active (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); int result; g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), 0); if (gtk_tree_row_reference_valid (priv->active_row)) { GtkTreePath *path; path = gtk_tree_row_reference_get_path (priv->active_row); result = gtk_tree_path_get_indices (path)[0]; gtk_tree_path_free (path); } else result = -1; return result; } /** * gtk_combo_box_set_active: (attributes org.gtk.Method.set_property=active) * @combo_box: a `GtkComboBox` * @index_: An index in the model passed during construction, * or -1 to have no active item * * Sets the active item of @combo_box to be the item at @index. */ void gtk_combo_box_set_active (GtkComboBox *combo_box, int index_) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreePath *path = NULL; g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); g_return_if_fail (index_ >= -1); if (priv->model == NULL) { /* Save index, in case the model is set after the index */ priv->active = index_; if (index_ != -1) return; } if (index_ != -1) path = gtk_tree_path_new_from_indices (index_, -1); gtk_combo_box_set_active_internal (combo_box, path); if (path) gtk_tree_path_free (path); } static void gtk_combo_box_set_active_internal (GtkComboBox *combo_box, GtkTreePath *path) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreePath *active_path; int path_cmp; /* Remember whether the initially active row is valid. */ gboolean is_valid_row_reference = gtk_tree_row_reference_valid (priv->active_row); if (path && is_valid_row_reference) { active_path = gtk_tree_row_reference_get_path (priv->active_row); path_cmp = gtk_tree_path_compare (path, active_path); gtk_tree_path_free (active_path); if (path_cmp == 0) return; } if (priv->active_row) { gtk_tree_row_reference_free (priv->active_row); priv->active_row = NULL; } if (!path) { gtk_tree_popover_set_active (GTK_TREE_POPOVER (priv->popup_widget), -1); if (priv->cell_view) gtk_cell_view_set_displayed_row (GTK_CELL_VIEW (priv->cell_view), NULL); /* * Do not emit a "changed" signal when an already invalid selection was * now set to invalid. */ if (!is_valid_row_reference) return; } else { priv->active_row = gtk_tree_row_reference_new (priv->model, path); gtk_tree_popover_set_active (GTK_TREE_POPOVER (priv->popup_widget), gtk_tree_path_get_indices (path)[0]); if (priv->cell_view) gtk_cell_view_set_displayed_row (GTK_CELL_VIEW (priv->cell_view), path); } g_signal_emit (combo_box, combo_box_signals[CHANGED], 0); g_object_notify (G_OBJECT (combo_box), "active"); if (priv->id_column >= 0) g_object_notify (G_OBJECT (combo_box), "active-id"); } /** * gtk_combo_box_get_active_iter: * @combo_box: A `GtkComboBox` * @iter: (out): A `GtkTreeIter` * * Sets @iter to point to the currently active item. * * If no item is active, @iter is left unchanged. * * Returns: %TRUE if @iter was set, %FALSE otherwise */ gboolean gtk_combo_box_get_active_iter (GtkComboBox *combo_box, GtkTreeIter *iter) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreePath *path; gboolean result; g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), FALSE); if (!gtk_tree_row_reference_valid (priv->active_row)) return FALSE; path = gtk_tree_row_reference_get_path (priv->active_row); result = gtk_tree_model_get_iter (priv->model, iter, path); gtk_tree_path_free (path); return result; } /** * gtk_combo_box_set_active_iter: * @combo_box: A `GtkComboBox` * @iter: (nullable): The `GtkTreeIter` * * Sets the current active item to be the one referenced by @iter. * * If @iter is %NULL, the active item is unset. */ void gtk_combo_box_set_active_iter (GtkComboBox *combo_box, GtkTreeIter *iter) { GtkTreePath *path = NULL; g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); if (iter) path = gtk_tree_model_get_path (gtk_combo_box_get_model (combo_box), iter); gtk_combo_box_set_active_internal (combo_box, path); gtk_tree_path_free (path); } /** * gtk_combo_box_set_model: (attributes org.gtk.Method.set_property=model) * @combo_box: A `GtkComboBox` * @model: (nullable): A `GtkTreeModel` * * Sets the model used by @combo_box to be @model. * * Will unset a previously set model (if applicable). If model is %NULL, * then it will unset the model. * * Note that this function does not clear the cell renderers, you have to * call [method@Gtk.CellLayout.clear] yourself if you need to set up different * cell renderers for the new model. */ void gtk_combo_box_set_model (GtkComboBox *combo_box, GtkTreeModel *model) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); g_return_if_fail (model == NULL || GTK_IS_TREE_MODEL (model)); if (model == priv->model) return; gtk_combo_box_unset_model (combo_box); if (model == NULL) goto out; priv->model = model; g_object_ref (priv->model); g_signal_connect (priv->model, "row-inserted", G_CALLBACK (gtk_combo_box_model_row_inserted), combo_box); g_signal_connect (priv->model, "row-deleted", G_CALLBACK (gtk_combo_box_model_row_deleted), combo_box); g_signal_connect (priv->model, "rows-reordered", G_CALLBACK (gtk_combo_box_model_rows_reordered), combo_box); g_signal_connect (priv->model, "row-changed", G_CALLBACK (gtk_combo_box_model_row_changed), combo_box); gtk_tree_popover_set_model (GTK_TREE_POPOVER (priv->popup_widget), priv->model); if (priv->cell_view) gtk_cell_view_set_model (GTK_CELL_VIEW (priv->cell_view), priv->model); if (priv->active != -1) { /* If an index was set in advance, apply it now */ gtk_combo_box_set_active (combo_box, priv->active); priv->active = -1; } out: gtk_combo_box_update_sensitivity (combo_box); g_object_notify (G_OBJECT (combo_box), "model"); } /** * gtk_combo_box_get_model: (attributes org.gtk.Method.get_property=model) * @combo_box: A `GtkComboBox` * * Returns the `GtkTreeModel` of @combo_box. * * Returns: (nullable) (transfer none): A `GtkTreeModel` which was passed * during construction. */ GtkTreeModel * gtk_combo_box_get_model (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), NULL); return priv->model; } static void gtk_combo_box_real_move_active (GtkComboBox *combo_box, GtkScrollType scroll) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreeIter iter; GtkTreeIter new_iter; gboolean active_iter; gboolean found; if (!priv->model) { gtk_widget_error_bell (GTK_WIDGET (combo_box)); return; } active_iter = gtk_combo_box_get_active_iter (combo_box, &iter); switch (scroll) { case GTK_SCROLL_STEP_BACKWARD: case GTK_SCROLL_STEP_UP: case GTK_SCROLL_STEP_LEFT: if (active_iter) { found = tree_prev (combo_box, priv->model, &iter, &new_iter); break; } G_GNUC_FALLTHROUGH; case GTK_SCROLL_PAGE_FORWARD: case GTK_SCROLL_PAGE_DOWN: case GTK_SCROLL_PAGE_RIGHT: case GTK_SCROLL_END: found = tree_last (combo_box, priv->model, &new_iter); break; case GTK_SCROLL_STEP_FORWARD: case GTK_SCROLL_STEP_DOWN: case GTK_SCROLL_STEP_RIGHT: if (active_iter) { found = tree_next (combo_box, priv->model, &iter, &new_iter); break; } G_GNUC_FALLTHROUGH; case GTK_SCROLL_PAGE_BACKWARD: case GTK_SCROLL_PAGE_UP: case GTK_SCROLL_PAGE_LEFT: case GTK_SCROLL_START: found = tree_first (combo_box, priv->model, &new_iter); break; case GTK_SCROLL_NONE: case GTK_SCROLL_JUMP: default: return; } if (found && active_iter) { GtkTreePath *old_path; GtkTreePath *new_path; old_path = gtk_tree_model_get_path (priv->model, &iter); new_path = gtk_tree_model_get_path (priv->model, &new_iter); if (gtk_tree_path_compare (old_path, new_path) == 0) found = FALSE; gtk_tree_path_free (old_path); gtk_tree_path_free (new_path); } if (found) { gtk_combo_box_set_active_iter (combo_box, &new_iter); } else { gtk_widget_error_bell (GTK_WIDGET (combo_box)); } } static gboolean gtk_combo_box_mnemonic_activate (GtkWidget *widget, gboolean group_cycling) { GtkComboBox *combo_box = GTK_COMBO_BOX (widget); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->has_entry) { if (priv->child) gtk_widget_grab_focus (priv->child); } else gtk_widget_grab_focus (priv->button); return TRUE; } static gboolean gtk_combo_box_grab_focus (GtkWidget *widget) { GtkComboBox *combo_box = GTK_COMBO_BOX (widget); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->has_entry) { if (priv->child) return gtk_widget_grab_focus (priv->child); else return FALSE; } else return gtk_widget_grab_focus (priv->button); } static void gtk_combo_box_unmap (GtkWidget *widget) { gtk_combo_box_popdown (GTK_COMBO_BOX (widget)); GTK_WIDGET_CLASS (gtk_combo_box_parent_class)->unmap (widget); } static void gtk_combo_box_entry_contents_changed (GtkEntry *entry, gpointer user_data) { GtkComboBox *combo_box = GTK_COMBO_BOX (user_data); /* * Fixes regression reported in bug #574059. The old functionality relied on * bug #572478. As a bugfix, we now emit the "changed" signal ourselves * when the selection was already set to -1. */ if (gtk_combo_box_get_active(combo_box) == -1) g_signal_emit_by_name (combo_box, "changed"); else gtk_combo_box_set_active (combo_box, -1); } static void gtk_combo_box_entry_active_changed (GtkComboBox *combo_box, gpointer user_data) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreeModel *model; GtkTreeIter iter; if (gtk_combo_box_get_active_iter (combo_box, &iter)) { GtkEntry *entry = GTK_ENTRY (priv->child); if (entry) { GtkTreePath *path; char *path_str; char *text = NULL; model = gtk_combo_box_get_model (combo_box); path = gtk_tree_model_get_path (model, &iter); path_str = gtk_tree_path_to_string (path); g_signal_handlers_block_by_func (entry, gtk_combo_box_entry_contents_changed, combo_box); g_signal_emit (combo_box, combo_box_signals[FORMAT_ENTRY_TEXT], 0, path_str, &text); gtk_editable_set_text (GTK_EDITABLE (entry), text); g_signal_handlers_unblock_by_func (entry, gtk_combo_box_entry_contents_changed, combo_box); gtk_tree_path_free (path); g_free (text); g_free (path_str); } } } static char * gtk_combo_box_format_entry_text (GtkComboBox *combo_box, const char *path) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreeModel *model; GtkTreeIter iter; char *text = NULL; if (priv->text_column >= 0) { model = gtk_combo_box_get_model (combo_box); gtk_tree_model_get_iter_from_string (model, &iter, path); gtk_tree_model_get (model, &iter, priv->text_column, &text, -1); } return text; } static void gtk_combo_box_constructed (GObject *object) { GtkComboBox *combo_box = GTK_COMBO_BOX (object); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); G_OBJECT_CLASS (gtk_combo_box_parent_class)->constructed (object); gtk_combo_box_create_child (combo_box); if (priv->has_entry) { priv->text_renderer = gtk_cell_renderer_text_new (); gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), priv->text_renderer, TRUE); gtk_combo_box_set_active (GTK_COMBO_BOX (combo_box), -1); } } static void gtk_combo_box_dispose (GObject* object) { GtkComboBox *combo_box = GTK_COMBO_BOX (object); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->popup_idle_id > 0) { g_source_remove (priv->popup_idle_id); priv->popup_idle_id = 0; } if (priv->box) { /* destroy things (unparent will kill the latest ref from us) * last unref on button will destroy the arrow */ gtk_widget_unparent (priv->box); priv->box = NULL; priv->button = NULL; priv->arrow = NULL; priv->child = NULL; priv->cell_view = NULL; } if (priv->row_separator_destroy) priv->row_separator_destroy (priv->row_separator_data); priv->row_separator_func = NULL; priv->row_separator_data = NULL; priv->row_separator_destroy = NULL; if (priv->popup_widget) { /* Stop menu destruction triggering toggle on a now-invalid button */ g_signal_handlers_disconnect_by_func (priv->popup_widget, gtk_combo_box_menu_hide, combo_box); g_clear_pointer (&priv->popup_widget, gtk_widget_unparent); } gtk_combo_box_unset_model (combo_box); G_OBJECT_CLASS (gtk_combo_box_parent_class)->dispose (object); } static gboolean gtk_cell_editable_key_pressed (GtkEventControllerKey *key, guint keyval, guint keycode, GdkModifierType modifiers, GtkComboBox *combo_box) { if (keyval == GDK_KEY_Escape) { g_object_set (combo_box, "editing-canceled", TRUE, NULL); gtk_cell_editable_editing_done (GTK_CELL_EDITABLE (combo_box)); gtk_cell_editable_remove_widget (GTK_CELL_EDITABLE (combo_box)); return TRUE; } else if (keyval == GDK_KEY_Return || keyval == GDK_KEY_ISO_Enter || keyval == GDK_KEY_KP_Enter) { gtk_cell_editable_editing_done (GTK_CELL_EDITABLE (combo_box)); gtk_cell_editable_remove_widget (GTK_CELL_EDITABLE (combo_box)); return TRUE; } return FALSE; } static void gtk_combo_box_start_editing (GtkCellEditable *cell_editable, GdkEvent *event) { GtkComboBox *combo_box = GTK_COMBO_BOX (cell_editable); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkEventController *controller; priv->is_cell_renderer = TRUE; controller = gtk_event_controller_key_new (); g_signal_connect_object (controller, "key-pressed", G_CALLBACK (gtk_cell_editable_key_pressed), cell_editable, 0); if (priv->cell_view) { gtk_widget_add_controller (priv->button, controller); gtk_widget_grab_focus (priv->button); } else { gtk_widget_add_controller (priv->child, controller); gtk_widget_grab_focus (priv->child); gtk_widget_set_can_focus (priv->button, FALSE); } } /** * gtk_combo_box_set_popup_fixed_width: (attributes org.gtk.Method.set_property=popup-fixed-width) * @combo_box: a `GtkComboBox` * @fixed: whether to use a fixed popup width * * Specifies whether the popup’s width should be a fixed width. * * If @fixed is %TRUE, the popup's width is set to match the * allocated width of the combo box. */ void gtk_combo_box_set_popup_fixed_width (GtkComboBox *combo_box, gboolean fixed) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); if (priv->popup_fixed_width != fixed) { priv->popup_fixed_width = fixed; g_object_notify (G_OBJECT (combo_box), "popup-fixed-width"); } } /** * gtk_combo_box_get_popup_fixed_width: (attributes org.gtk.Method.get_property=popup-fixed-width) * @combo_box: a `GtkComboBox` * * Gets whether the popup uses a fixed width. * * Returns: %TRUE if the popup uses a fixed width */ gboolean gtk_combo_box_get_popup_fixed_width (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), FALSE); return priv->popup_fixed_width; } /** * gtk_combo_box_get_row_separator_func: (skip) * @combo_box: a `GtkComboBox` * * Returns the current row separator function. * * Returns: (nullable): the current row separator function. */ GtkTreeViewRowSeparatorFunc gtk_combo_box_get_row_separator_func (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), NULL); return priv->row_separator_func; } /** * gtk_combo_box_set_row_separator_func: * @combo_box: a `GtkComboBox` * @func: (nullable): a `GtkTreeViewRowSeparatorFunc` * @data: (nullable): user data to pass to @func * @destroy: (nullable): destroy notifier for @data * * Sets the row separator function, which is used to determine * whether a row should be drawn as a separator. * * If the row separator function is %NULL, no separators are drawn. * This is the default value. */ void gtk_combo_box_set_row_separator_func (GtkComboBox *combo_box, GtkTreeViewRowSeparatorFunc func, gpointer data, GDestroyNotify destroy) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); if (priv->row_separator_destroy) priv->row_separator_destroy (priv->row_separator_data); priv->row_separator_func = func; priv->row_separator_data = data; priv->row_separator_destroy = destroy; gtk_tree_popover_set_row_separator_func (GTK_TREE_POPOVER (priv->popup_widget), (GtkTreeViewRowSeparatorFunc)gtk_combo_box_row_separator_func, combo_box, NULL); gtk_widget_queue_draw (GTK_WIDGET (combo_box)); } /** * gtk_combo_box_set_button_sensitivity: (attributes org.gtk.Method.set_property=button-sensitivity) * @combo_box: a `GtkComboBox` * @sensitivity: specify the sensitivity of the dropdown button * * Sets whether the dropdown button of the combo box should update * its sensitivity depending on the model contents. */ void gtk_combo_box_set_button_sensitivity (GtkComboBox *combo_box, GtkSensitivityType sensitivity) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); if (priv->button_sensitivity != sensitivity) { priv->button_sensitivity = sensitivity; gtk_combo_box_update_sensitivity (combo_box); g_object_notify (G_OBJECT (combo_box), "button-sensitivity"); } } /** * gtk_combo_box_get_button_sensitivity: (attributes org.gtk.Method.get_property=button-sensitivity) * @combo_box: a `GtkComboBox` * * Returns whether the combo box sets the dropdown button * sensitive or not when there are no items in the model. * * Returns: %GTK_SENSITIVITY_ON if the dropdown button * is sensitive when the model is empty, %GTK_SENSITIVITY_OFF * if the button is always insensitive or %GTK_SENSITIVITY_AUTO * if it is only sensitive as long as the model has one item to * be selected. */ GtkSensitivityType gtk_combo_box_get_button_sensitivity (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), FALSE); return priv->button_sensitivity; } /** * gtk_combo_box_get_has_entry: (attributes org.gtk.Method.get_property=has-entry) * @combo_box: a `GtkComboBox` * * Returns whether the combo box has an entry. * * Returns: whether there is an entry in @combo_box. */ gboolean gtk_combo_box_get_has_entry (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), FALSE); return priv->has_entry; } /** * gtk_combo_box_set_entry_text_column: * @combo_box: A `GtkComboBox` * @text_column: A column in @model to get the strings from for * the internal entry * * Sets the model column which @combo_box should use to get strings * from to be @text_column. * * For this column no separate * [class@Gtk.CellRenderer] is needed. * * The column @text_column in the model of @combo_box must be of * type %G_TYPE_STRING. * * This is only relevant if @combo_box has been created with * [property@Gtk.ComboBox:has-entry] as %TRUE. */ void gtk_combo_box_set_entry_text_column (GtkComboBox *combo_box, int text_column) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); g_return_if_fail (text_column >= 0); g_return_if_fail (priv->model == NULL || text_column < gtk_tree_model_get_n_columns (priv->model)); if (priv->text_column != text_column) { priv->text_column = text_column; if (priv->text_renderer != NULL) gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), priv->text_renderer, "text", text_column, NULL); g_object_notify (G_OBJECT (combo_box), "entry-text-column"); } } /** * gtk_combo_box_get_entry_text_column: * @combo_box: A `GtkComboBox` * * Returns the column which @combo_box is using to get the strings * from to display in the internal entry. * * Returns: A column in the data source model of @combo_box. */ int gtk_combo_box_get_entry_text_column (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), 0); return priv->text_column; } static void gtk_combo_box_buildable_add_child (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const char *type) { if (GTK_IS_CELL_RENDERER (child)) _gtk_cell_layout_buildable_add_child (buildable, builder, child, type); else if (GTK_IS_WIDGET (child)) gtk_combo_box_set_child (GTK_COMBO_BOX (buildable), GTK_WIDGET (child)); else parent_buildable_iface->add_child (buildable, builder, child, type); } static gboolean gtk_combo_box_buildable_custom_tag_start (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const char *tagname, GtkBuildableParser *parser, gpointer *data) { if (parent_buildable_iface->custom_tag_start (buildable, builder, child, tagname, parser, data)) return TRUE; return _gtk_cell_layout_buildable_custom_tag_start (buildable, builder, child, tagname, parser, data); } static void gtk_combo_box_buildable_custom_tag_end (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const char *tagname, gpointer data) { if (!_gtk_cell_layout_buildable_custom_tag_end (buildable, builder, child, tagname, data)) parent_buildable_iface->custom_tag_end (buildable, builder, child, tagname, data); } static GObject * gtk_combo_box_buildable_get_internal_child (GtkBuildable *buildable, GtkBuilder *builder, const char *childname) { GtkComboBox *combo_box = GTK_COMBO_BOX (buildable); GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); if (priv->has_entry && strcmp (childname, "entry") == 0) return G_OBJECT (priv->child); return parent_buildable_iface->get_internal_child (buildable, builder, childname); } /** * gtk_combo_box_set_id_column: (attributes org.gtk.Method.set_property=id-column) * @combo_box: A `GtkComboBox` * @id_column: A column in @model to get string IDs for values from * * Sets the model column which @combo_box should use to get string IDs * for values from. * * The column @id_column in the model of @combo_box must be of type * %G_TYPE_STRING. */ void gtk_combo_box_set_id_column (GtkComboBox *combo_box, int id_column) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); if (id_column != priv->id_column) { g_return_if_fail (id_column >= 0); g_return_if_fail (priv->model == NULL || id_column < gtk_tree_model_get_n_columns (priv->model)); priv->id_column = id_column; g_object_notify (G_OBJECT (combo_box), "id-column"); g_object_notify (G_OBJECT (combo_box), "active-id"); } } /** * gtk_combo_box_get_id_column: (attributes org.gtk.Method.get_property=id-column) * @combo_box: A `GtkComboBox` * * Returns the column which @combo_box is using to get string IDs * for values from. * * Returns: A column in the data source model of @combo_box. */ int gtk_combo_box_get_id_column (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), 0); return priv->id_column; } /** * gtk_combo_box_get_active_id: (attributes org.gtk.Method.get_property=active-id) * @combo_box: a `GtkComboBox` * * Returns the ID of the active row of @combo_box. * * This value is taken from the active row and the column specified * by the [property@Gtk.ComboBox:id-column] property of @combo_box * (see [method@Gtk.ComboBox.set_id_column]). * * The returned value is an interned string which means that you can * compare the pointer by value to other interned strings and that you * must not free it. * * If the [property@Gtk.ComboBox:id-column] property of @combo_box is * not set, or if no row is active, or if the active row has a %NULL * ID value, then %NULL is returned. * * Returns: (nullable): the ID of the active row */ const char * gtk_combo_box_get_active_id (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreeModel *model; GtkTreeIter iter; int column; g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), NULL); column = priv->id_column; if (column < 0) return NULL; model = gtk_combo_box_get_model (combo_box); g_return_val_if_fail (gtk_tree_model_get_column_type (model, column) == G_TYPE_STRING, NULL); if (gtk_combo_box_get_active_iter (combo_box, &iter)) { const char *interned; char *id; gtk_tree_model_get (model, &iter, column, &id, -1); interned = g_intern_string (id); g_free (id); return interned; } return NULL; } /** * gtk_combo_box_set_active_id: (attributes org.gtk.Method.set_property=active-id) * @combo_box: a `GtkComboBox` * @active_id: (nullable): the ID of the row to select * * Changes the active row of @combo_box to the one that has an ID equal to * @active_id. * * If @active_id is %NULL, the active row is unset. Rows having * a %NULL ID string cannot be made active by this function. * * If the [property@Gtk.ComboBox:id-column] property of @combo_box is * unset or if no row has the given ID then the function does nothing * and returns %FALSE. * * Returns: %TRUE if a row with a matching ID was found. If a %NULL * @active_id was given to unset the active row, the function * always returns %TRUE. */ gboolean gtk_combo_box_set_active_id (GtkComboBox *combo_box, const char *active_id) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); GtkTreeModel *model; GtkTreeIter iter; gboolean match = FALSE; int column; g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), FALSE); if (active_id == NULL) { gtk_combo_box_set_active (combo_box, -1); return TRUE; /* active row was successfully unset */ } column = priv->id_column; if (column < 0) return FALSE; model = gtk_combo_box_get_model (combo_box); g_return_val_if_fail (gtk_tree_model_get_column_type (model, column) == G_TYPE_STRING, FALSE); if (gtk_tree_model_get_iter_first (model, &iter)) do { char *id; gtk_tree_model_get (model, &iter, column, &id, -1); if (id != NULL) match = strcmp (id, active_id) == 0; g_free (id); if (match) { gtk_combo_box_set_active_iter (combo_box, &iter); break; } } while (gtk_tree_model_iter_next (model, &iter)); g_object_notify (G_OBJECT (combo_box), "active-id"); return match; } GtkWidget * gtk_combo_box_get_popup (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); return priv->popup_widget; } /** * gtk_combo_box_set_child: (attributes org.gtk.Method.set_property=child) * @combo_box: a `GtkComboBox` * @child: (nullable): the child widget * * Sets the child widget of @combo_box. */ void gtk_combo_box_set_child (GtkComboBox *combo_box, GtkWidget *child) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_if_fail (GTK_IS_COMBO_BOX (combo_box)); g_return_if_fail (child == NULL || GTK_IS_WIDGET (child)); if (priv->child) gtk_combo_box_remove (combo_box, priv->child); if (child) gtk_combo_box_add (combo_box, child); g_object_notify (G_OBJECT (combo_box), "child"); } /** * gtk_combo_box_get_child: (attributes org.gtk.Method.get_property=child) * @combo_box: a `GtkComboBox` * * Gets the child widget of @combo_box. * * Returns: (nullable) (transfer none): the child widget of @combo_box */ GtkWidget * gtk_combo_box_get_child (GtkComboBox *combo_box) { GtkComboBoxPrivate *priv = gtk_combo_box_get_instance_private (combo_box); g_return_val_if_fail (GTK_IS_COMBO_BOX (combo_box), NULL); return priv->child; }