1 | /* |
2 | * Copyright © 2019 Red Hat, Inc. |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2.1 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | * |
17 | * Authors: Matthias Clasen <mclasen@redhat.com> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkdropdown.h" |
23 | |
24 | #include "gtkbuiltiniconprivate.h" |
25 | #include "gtkintl.h" |
26 | #include "gtklistview.h" |
27 | #include "gtklistitemfactory.h" |
28 | #include "gtksignallistitemfactory.h" |
29 | #include "gtklistitemwidgetprivate.h" |
30 | #include "gtkpopover.h" |
31 | #include "gtkprivate.h" |
32 | #include "gtksingleselection.h" |
33 | #include "gtkfilterlistmodel.h" |
34 | #include "gtkstringfilter.h" |
35 | #include "gtkmultifilter.h" |
36 | #include "gtkwidgetprivate.h" |
37 | #include "gtknative.h" |
38 | #include "gtktogglebutton.h" |
39 | #include "gtkexpression.h" |
40 | #include "gtkstack.h" |
41 | #include "gtksearchentry.h" |
42 | #include "gtklabel.h" |
43 | #include "gtklistitem.h" |
44 | #include "gtkbuildable.h" |
45 | #include "gtkbuilderprivate.h" |
46 | #include "gtkstringlist.h" |
47 | #include "gtkbox.h" |
48 | |
49 | /** |
50 | * GtkDropDown: |
51 | * |
52 | * `GtkDropDown` is a widget that allows the user to choose an item |
53 | * from a list of options. |
54 | * |
55 | * ![An example GtkDropDown](drop-down.png) |
56 | * |
57 | * The `GtkDropDown` displays the selected choice. |
58 | * |
59 | * The options are given to `GtkDropDown` in the form of `GListModel` |
60 | * and how the individual options are represented is determined by |
61 | * a [class@Gtk.ListItemFactory]. The default factory displays simple strings. |
62 | * |
63 | * `GtkDropDown` knows how to obtain strings from the items in a |
64 | * [class@Gtk.StringList]; for other models, you have to provide an expression |
65 | * to find the strings via [method@Gtk.DropDown.set_expression]. |
66 | * |
67 | * `GtkDropDown` can optionally allow search in the popup, which is |
68 | * useful if the list of options is long. To enable the search entry, |
69 | * use [method@Gtk.DropDown.set_enable_search]. |
70 | * |
71 | * # CSS nodes |
72 | * |
73 | * `GtkDropDown` has a single CSS node with name dropdown, |
74 | * with the button and popover nodes as children. |
75 | * |
76 | * # Accessibility |
77 | * |
78 | * `GtkDropDown` uses the %GTK_ACCESSIBLE_ROLE_COMBO_BOX role. |
79 | */ |
80 | |
81 | struct _GtkDropDown |
82 | { |
83 | GtkWidget parent_instance; |
84 | |
85 | GtkListItemFactory *factory; |
86 | GtkListItemFactory *list_factory; |
87 | GListModel *model; |
88 | GtkSelectionModel *selection; |
89 | GListModel *filter_model; |
90 | GtkSelectionModel *; |
91 | |
92 | GtkWidget *; |
93 | GtkWidget *button; |
94 | GtkWidget *arrow; |
95 | |
96 | GtkWidget *; |
97 | GtkWidget *button_stack; |
98 | GtkWidget *button_item; |
99 | GtkWidget *button_placeholder; |
100 | GtkWidget *search_box; |
101 | GtkWidget *search_entry; |
102 | |
103 | GtkExpression *expression; |
104 | |
105 | guint enable_search : 1; |
106 | guint show_arrow : 1; |
107 | }; |
108 | |
109 | struct _GtkDropDownClass |
110 | { |
111 | GtkWidgetClass parent_class; |
112 | }; |
113 | |
114 | enum |
115 | { |
116 | PROP_0, |
117 | PROP_FACTORY, |
118 | PROP_LIST_FACTORY, |
119 | PROP_MODEL, |
120 | PROP_SELECTED, |
121 | PROP_SELECTED_ITEM, |
122 | PROP_ENABLE_SEARCH, |
123 | PROP_EXPRESSION, |
124 | PROP_SHOW_ARROW, |
125 | |
126 | N_PROPS |
127 | }; |
128 | |
129 | enum |
130 | { |
131 | ACTIVATE, |
132 | LAST_SIGNAL |
133 | }; |
134 | |
135 | G_DEFINE_TYPE (GtkDropDown, gtk_drop_down, GTK_TYPE_WIDGET) |
136 | |
137 | static GParamSpec *properties[N_PROPS] = { NULL, }; |
138 | static guint signals[LAST_SIGNAL] = { 0 }; |
139 | |
140 | static void |
141 | button_toggled (GtkWidget *widget, |
142 | gpointer data) |
143 | { |
144 | GtkDropDown *self = data; |
145 | |
146 | if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget))) |
147 | gtk_popover_popup (GTK_POPOVER (self->popup)); |
148 | else |
149 | gtk_popover_popdown (GTK_POPOVER (self->popup)); |
150 | } |
151 | |
152 | static void |
153 | popover_closed (GtkPopover *popover, |
154 | gpointer data) |
155 | { |
156 | GtkDropDown *self = data; |
157 | |
158 | gtk_editable_set_text (GTK_EDITABLE (self->search_entry), text: "" ); |
159 | gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->button), FALSE); |
160 | } |
161 | |
162 | static void |
163 | row_activated (GtkListView *listview, |
164 | guint position, |
165 | gpointer data) |
166 | { |
167 | GtkDropDown *self = data; |
168 | GtkFilter *filter; |
169 | |
170 | gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->button), FALSE); |
171 | gtk_popover_popdown (GTK_POPOVER (self->popup)); |
172 | |
173 | /* reset the filter so positions are 1-1 */ |
174 | filter = gtk_filter_list_model_get_filter (self: GTK_FILTER_LIST_MODEL (ptr: self->filter_model)); |
175 | if (GTK_IS_STRING_FILTER (ptr: filter)) |
176 | gtk_string_filter_set_search (self: GTK_STRING_FILTER (ptr: filter), search: "" ); |
177 | gtk_drop_down_set_selected (self, position: gtk_single_selection_get_selected (self: GTK_SINGLE_SELECTION (ptr: self->popup_selection))); |
178 | } |
179 | |
180 | static void |
181 | selection_changed (GtkSingleSelection *selection, |
182 | GParamSpec *pspec, |
183 | gpointer data) |
184 | { |
185 | GtkDropDown *self = data; |
186 | guint selected; |
187 | gpointer item; |
188 | GtkFilter *filter; |
189 | |
190 | selected = gtk_single_selection_get_selected (self: GTK_SINGLE_SELECTION (ptr: self->selection)); |
191 | item = gtk_single_selection_get_selected_item (self: GTK_SINGLE_SELECTION (ptr: self->selection)); |
192 | |
193 | if (selected == GTK_INVALID_LIST_POSITION) |
194 | { |
195 | gtk_stack_set_visible_child_name (GTK_STACK (self->button_stack), name: "empty" ); |
196 | } |
197 | else |
198 | { |
199 | gtk_stack_set_visible_child_name (GTK_STACK (self->button_stack), name: "item" ); |
200 | gtk_list_item_widget_update (GTK_LIST_ITEM_WIDGET (self->button_item), position: selected, item, FALSE); |
201 | } |
202 | |
203 | /* reset the filter so positions are 1-1 */ |
204 | filter = gtk_filter_list_model_get_filter (self: GTK_FILTER_LIST_MODEL (ptr: self->filter_model)); |
205 | if (GTK_IS_STRING_FILTER (ptr: filter)) |
206 | gtk_string_filter_set_search (self: GTK_STRING_FILTER (ptr: filter), search: "" ); |
207 | gtk_single_selection_set_selected (self: GTK_SINGLE_SELECTION (ptr: self->popup_selection), position: selected); |
208 | |
209 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SELECTED]); |
210 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SELECTED_ITEM]); |
211 | } |
212 | |
213 | static void |
214 | gtk_drop_down_activate (GtkDropDown *self) |
215 | { |
216 | gtk_widget_activate (widget: self->button); |
217 | } |
218 | |
219 | static void |
220 | update_filter (GtkDropDown *self) |
221 | { |
222 | if (self->filter_model) |
223 | { |
224 | GtkFilter *filter; |
225 | |
226 | if (self->expression) |
227 | { |
228 | filter = GTK_FILTER (ptr: gtk_string_filter_new (expression: gtk_expression_ref (self: self->expression))); |
229 | gtk_string_filter_set_match_mode (self: GTK_STRING_FILTER (ptr: filter), mode: GTK_STRING_FILTER_MATCH_MODE_PREFIX); |
230 | } |
231 | else |
232 | filter = GTK_FILTER (ptr: gtk_every_filter_new ()); |
233 | gtk_filter_list_model_set_filter (self: GTK_FILTER_LIST_MODEL (ptr: self->filter_model), filter); |
234 | g_object_unref (object: filter); |
235 | } |
236 | } |
237 | |
238 | static void |
239 | search_changed (GtkSearchEntry *entry, gpointer data) |
240 | { |
241 | GtkDropDown *self = data; |
242 | const char *text; |
243 | GtkFilter *filter; |
244 | |
245 | text = gtk_editable_get_text (GTK_EDITABLE (entry)); |
246 | |
247 | filter = gtk_filter_list_model_get_filter (self: GTK_FILTER_LIST_MODEL (ptr: self->filter_model)); |
248 | if (GTK_IS_STRING_FILTER (ptr: filter)) |
249 | gtk_string_filter_set_search (self: GTK_STRING_FILTER (ptr: filter), search: text); |
250 | } |
251 | |
252 | static void |
253 | search_stop (GtkSearchEntry *entry, gpointer data) |
254 | { |
255 | GtkDropDown *self = data; |
256 | GtkFilter *filter; |
257 | |
258 | filter = gtk_filter_list_model_get_filter (self: GTK_FILTER_LIST_MODEL (ptr: self->filter_model)); |
259 | if (GTK_IS_STRING_FILTER (ptr: filter)) |
260 | { |
261 | if (gtk_string_filter_get_search (self: GTK_STRING_FILTER (ptr: filter))) |
262 | gtk_string_filter_set_search (self: GTK_STRING_FILTER (ptr: filter), NULL); |
263 | else |
264 | gtk_popover_popdown (GTK_POPOVER (self->popup)); |
265 | } |
266 | } |
267 | |
268 | static void |
269 | gtk_drop_down_dispose (GObject *object) |
270 | { |
271 | GtkDropDown *self = GTK_DROP_DOWN (ptr: object); |
272 | |
273 | g_clear_pointer (&self->popup, gtk_widget_unparent); |
274 | g_clear_pointer (&self->button, gtk_widget_unparent); |
275 | |
276 | g_clear_object (&self->model); |
277 | if (self->selection) |
278 | g_signal_handlers_disconnect_by_func (self->selection, selection_changed, self); |
279 | g_clear_object (&self->filter_model); |
280 | g_clear_pointer (&self->expression, gtk_expression_unref); |
281 | g_clear_object (&self->selection); |
282 | g_clear_object (&self->popup_selection); |
283 | g_clear_object (&self->factory); |
284 | g_clear_object (&self->list_factory); |
285 | |
286 | G_OBJECT_CLASS (gtk_drop_down_parent_class)->dispose (object); |
287 | } |
288 | |
289 | static void |
290 | gtk_drop_down_get_property (GObject *object, |
291 | guint property_id, |
292 | GValue *value, |
293 | GParamSpec *pspec) |
294 | { |
295 | GtkDropDown *self = GTK_DROP_DOWN (ptr: object); |
296 | |
297 | switch (property_id) |
298 | { |
299 | case PROP_FACTORY: |
300 | g_value_set_object (value, v_object: self->factory); |
301 | break; |
302 | |
303 | case PROP_LIST_FACTORY: |
304 | g_value_set_object (value, v_object: self->list_factory); |
305 | break; |
306 | |
307 | case PROP_MODEL: |
308 | g_value_set_object (value, v_object: self->model); |
309 | break; |
310 | |
311 | case PROP_SELECTED: |
312 | g_value_set_uint (value, v_uint: gtk_drop_down_get_selected (self)); |
313 | break; |
314 | |
315 | case PROP_SELECTED_ITEM: |
316 | g_value_set_object (value, v_object: gtk_drop_down_get_selected_item (self)); |
317 | break; |
318 | |
319 | case PROP_ENABLE_SEARCH: |
320 | g_value_set_boolean (value, v_boolean: self->enable_search); |
321 | break; |
322 | |
323 | case PROP_EXPRESSION: |
324 | gtk_value_set_expression (value, expression: self->expression); |
325 | break; |
326 | |
327 | case PROP_SHOW_ARROW: |
328 | g_value_set_boolean (value, v_boolean: gtk_drop_down_get_show_arrow (self)); |
329 | break; |
330 | |
331 | default: |
332 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
333 | break; |
334 | } |
335 | } |
336 | |
337 | static void |
338 | gtk_drop_down_set_property (GObject *object, |
339 | guint property_id, |
340 | const GValue *value, |
341 | GParamSpec *pspec) |
342 | { |
343 | GtkDropDown *self = GTK_DROP_DOWN (ptr: object); |
344 | |
345 | switch (property_id) |
346 | { |
347 | case PROP_FACTORY: |
348 | gtk_drop_down_set_factory (self, factory: g_value_get_object (value)); |
349 | break; |
350 | |
351 | case PROP_LIST_FACTORY: |
352 | gtk_drop_down_set_list_factory (self, factory: g_value_get_object (value)); |
353 | break; |
354 | |
355 | case PROP_MODEL: |
356 | gtk_drop_down_set_model (self, model: g_value_get_object (value)); |
357 | break; |
358 | |
359 | case PROP_SELECTED: |
360 | gtk_drop_down_set_selected (self, position: g_value_get_uint (value)); |
361 | break; |
362 | |
363 | case PROP_ENABLE_SEARCH: |
364 | gtk_drop_down_set_enable_search (self, enable_search: g_value_get_boolean (value)); |
365 | break; |
366 | |
367 | case PROP_EXPRESSION: |
368 | gtk_drop_down_set_expression (self, expression: gtk_value_get_expression (value)); |
369 | break; |
370 | |
371 | case PROP_SHOW_ARROW: |
372 | gtk_drop_down_set_show_arrow (self, show_arrow: g_value_get_boolean (value)); |
373 | break; |
374 | |
375 | default: |
376 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
377 | break; |
378 | } |
379 | } |
380 | |
381 | static void |
382 | gtk_drop_down_measure (GtkWidget *widget, |
383 | GtkOrientation orientation, |
384 | int size, |
385 | int *minimum, |
386 | int *natural, |
387 | int *minimum_baseline, |
388 | int *natural_baseline) |
389 | { |
390 | GtkDropDown *self = GTK_DROP_DOWN (ptr: widget); |
391 | |
392 | gtk_widget_measure (widget: self->button, |
393 | orientation, |
394 | for_size: size, |
395 | minimum, natural, |
396 | minimum_baseline, natural_baseline); |
397 | } |
398 | |
399 | static void |
400 | gtk_drop_down_size_allocate (GtkWidget *widget, |
401 | int width, |
402 | int height, |
403 | int baseline) |
404 | { |
405 | GtkDropDown *self = GTK_DROP_DOWN (ptr: widget); |
406 | |
407 | gtk_widget_size_allocate (widget: self->button, allocation: &(GtkAllocation) { 0, 0, width, height }, baseline); |
408 | |
409 | gtk_widget_set_size_request (widget: self->popup, width, height: -1); |
410 | gtk_widget_queue_resize (widget: self->popup); |
411 | |
412 | gtk_popover_present (GTK_POPOVER (self->popup)); |
413 | } |
414 | |
415 | static gboolean |
416 | gtk_drop_down_focus (GtkWidget *widget, |
417 | GtkDirectionType direction) |
418 | { |
419 | GtkDropDown *self = GTK_DROP_DOWN (ptr: widget); |
420 | |
421 | if (self->popup && gtk_widget_get_visible (widget: self->popup)) |
422 | return gtk_widget_child_focus (widget: self->popup, direction); |
423 | else |
424 | return gtk_widget_child_focus (widget: self->button, direction); |
425 | } |
426 | |
427 | static gboolean |
428 | gtk_drop_down_grab_focus (GtkWidget *widget) |
429 | { |
430 | GtkDropDown *self = GTK_DROP_DOWN (ptr: widget); |
431 | |
432 | return gtk_widget_grab_focus (widget: self->button); |
433 | } |
434 | |
435 | |
436 | static void |
437 | gtk_drop_down_class_init (GtkDropDownClass *klass) |
438 | { |
439 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
440 | GObjectClass *gobject_class = G_OBJECT_CLASS (klass); |
441 | |
442 | gobject_class->dispose = gtk_drop_down_dispose; |
443 | gobject_class->get_property = gtk_drop_down_get_property; |
444 | gobject_class->set_property = gtk_drop_down_set_property; |
445 | |
446 | widget_class->measure = gtk_drop_down_measure; |
447 | widget_class->size_allocate = gtk_drop_down_size_allocate; |
448 | widget_class->focus = gtk_drop_down_focus; |
449 | widget_class->grab_focus = gtk_drop_down_grab_focus; |
450 | |
451 | /** |
452 | * GtkDropDown:factory: (attributes org.gtk.Property.get=gtk_drop_down_get_factory org.gtk.Property.set=gtk_drop_down_set_factory) |
453 | * |
454 | * Factory for populating list items. |
455 | */ |
456 | properties[PROP_FACTORY] = |
457 | g_param_spec_object (name: "factory" , |
458 | P_("Factory" ), |
459 | P_("Factory for populating list items" ), |
460 | GTK_TYPE_LIST_ITEM_FACTORY, |
461 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
462 | |
463 | /** |
464 | * GtkDropDown:list-factory: (attributes org.gtk.Property.get=gtk_drop_down_get_list_factory org.gtk.Property.set=gtk_drop_down_set_list_factory) |
465 | * |
466 | * The factory for populating list items in the popup. |
467 | * |
468 | * If this is not set, [property@Gtk.DropDown:factory] is used. |
469 | */ |
470 | properties[PROP_LIST_FACTORY] = |
471 | g_param_spec_object (name: "list-factory" , |
472 | P_("List Factory" ), |
473 | P_("Factory for populating list items" ), |
474 | GTK_TYPE_LIST_ITEM_FACTORY, |
475 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
476 | |
477 | /** |
478 | * GtkDropDown:model: (attributes org.gtk.Property.get=gtk_drop_down_get_model org.gtk.Property.set=gtk_drop_down_set_model) |
479 | * |
480 | * Model for the displayed items. |
481 | */ |
482 | properties[PROP_MODEL] = |
483 | g_param_spec_object (name: "model" , |
484 | P_("Model" ), |
485 | P_("Model for the displayed items" ), |
486 | G_TYPE_LIST_MODEL, |
487 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
488 | |
489 | /** |
490 | * GtkDropDown:selected: (attributes org.gtk.Property.get=gtk_drop_down_get_selected org.gtk.Property.set=gtk_drop_down_set_selected) |
491 | * |
492 | * The position of the selected item. |
493 | * |
494 | * If no item is selected, the property has the value |
495 | * %GTK_INVALID_LIST_POSITION. |
496 | */ |
497 | properties[PROP_SELECTED] = |
498 | g_param_spec_uint (name: "selected" , |
499 | P_("Selected" ), |
500 | P_("Position of the selected item" ), |
501 | minimum: 0, G_MAXUINT, GTK_INVALID_LIST_POSITION, |
502 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
503 | |
504 | /** |
505 | * GtkDropDown:selected-item: (attributes org.gtk.Property.get=gtk_drop_down_get_selected_item) |
506 | * |
507 | * The selected item. |
508 | */ |
509 | properties[PROP_SELECTED_ITEM] = |
510 | g_param_spec_object (name: "selected-item" , |
511 | P_("Selected Item" ), |
512 | P_("The selected item" ), |
513 | G_TYPE_OBJECT, |
514 | flags: G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); |
515 | |
516 | /** |
517 | * GtkDropDown:enable-search: (attributes org.gtk.Property.get=gtk_drop_down_get_enable_search org.gtk.Property.set=gtk_drop_down_set_enable_search) |
518 | * |
519 | * Whether to show a search entry in the popup. |
520 | * |
521 | * Note that search requires [property@Gtk.DropDown:expression] |
522 | * to be set. |
523 | */ |
524 | properties[PROP_ENABLE_SEARCH] = |
525 | g_param_spec_boolean (name: "enable-search" , |
526 | P_("Enable search" ), |
527 | P_("Whether to show a search entry in the popup" ), |
528 | FALSE, |
529 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
530 | |
531 | /** |
532 | * GtkDropDown:expression: (type GtkExpression) (attributes org.gtk.Property.get=gtk_drop_down_get_expression org.gtk.Property.set=gtk_drop_down_set_expression) |
533 | * |
534 | * An expression to evaluate to obtain strings to match against the search |
535 | * term. |
536 | * |
537 | * See [property@Gtk.DropDown:enable-search] for how to enable search. |
538 | * If [property@Gtk.DropDown:factory] is not set, the expression is also |
539 | * used to bind strings to labels produced by a default factory. |
540 | */ |
541 | properties[PROP_EXPRESSION] = |
542 | gtk_param_spec_expression (name: "expression" , |
543 | P_("Expression" ), |
544 | P_("Expression to determine strings to search for" ), |
545 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
546 | |
547 | /** |
548 | * GtkDropDown:show-arrow: (attributes org.gtk.Property.get=gtk_drop_down_get_show_arrow org.gtk.Property.set=gtk_drop_down_set_show_arrow) |
549 | * |
550 | * Whether to show an arrow within the GtkDropDown widget. |
551 | * |
552 | * Since: 4.6 |
553 | */ |
554 | properties[PROP_SHOW_ARROW] = |
555 | g_param_spec_boolean (name: "show-arrow" , |
556 | P_("Show arrow" ), |
557 | P_("Whether to show an arrow within the widget" ), |
558 | TRUE, |
559 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
560 | |
561 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties); |
562 | |
563 | /** |
564 | * GtkDropDown::activate: |
565 | * @widget: the object which received the signal. |
566 | * |
567 | * Emitted to when the drop down is activated. |
568 | * |
569 | * The `::activate` signal on `GtkDropDown` is an action signal and |
570 | * emitting it causes the drop down to pop up its dropdown. |
571 | * |
572 | * Since: 4.6 |
573 | */ |
574 | signals[ACTIVATE] = |
575 | g_signal_new_class_handler (I_ ("activate" ), |
576 | G_OBJECT_CLASS_TYPE (gobject_class), |
577 | signal_flags: G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, |
578 | G_CALLBACK (gtk_drop_down_activate), |
579 | NULL, NULL, |
580 | NULL, |
581 | G_TYPE_NONE, n_params: 0); |
582 | |
583 | gtk_widget_class_set_activate_signal (widget_class, signal_id: signals[ACTIVATE]); |
584 | |
585 | gtk_widget_class_set_template_from_resource (widget_class, resource_name: "/org/gtk/libgtk/ui/gtkdropdown.ui" ); |
586 | gtk_widget_class_bind_template_child (widget_class, GtkDropDown, arrow); |
587 | gtk_widget_class_bind_template_child (widget_class, GtkDropDown, button); |
588 | gtk_widget_class_bind_template_child (widget_class, GtkDropDown, button_stack); |
589 | gtk_widget_class_bind_template_child (widget_class, GtkDropDown, button_item); |
590 | gtk_widget_class_bind_template_child (widget_class, GtkDropDown, popup); |
591 | gtk_widget_class_bind_template_child (widget_class, GtkDropDown, popup_list); |
592 | gtk_widget_class_bind_template_child (widget_class, GtkDropDown, search_box); |
593 | gtk_widget_class_bind_template_child (widget_class, GtkDropDown, search_entry); |
594 | |
595 | gtk_widget_class_bind_template_callback (widget_class, row_activated); |
596 | gtk_widget_class_bind_template_callback (widget_class, button_toggled); |
597 | gtk_widget_class_bind_template_callback (widget_class, popover_closed); |
598 | gtk_widget_class_bind_template_callback (widget_class, search_changed); |
599 | gtk_widget_class_bind_template_callback (widget_class, search_stop); |
600 | |
601 | gtk_widget_class_set_css_name (widget_class, I_("dropdown" )); |
602 | gtk_widget_class_set_accessible_role (widget_class, accessible_role: GTK_ACCESSIBLE_ROLE_COMBO_BOX); |
603 | } |
604 | |
605 | static void |
606 | setup_item (GtkSignalListItemFactory *factory, |
607 | GtkListItem *list_item, |
608 | gpointer data) |
609 | { |
610 | GtkWidget *box; |
611 | GtkWidget *label; |
612 | GtkWidget *icon; |
613 | |
614 | box = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 0); |
615 | label = gtk_label_new (NULL); |
616 | gtk_label_set_xalign (GTK_LABEL (label), xalign: 0.0); |
617 | gtk_box_append (GTK_BOX (box), child: label); |
618 | icon = gtk_image_new_from_icon_name (icon_name: "object-select-symbolic" ); |
619 | gtk_box_append (GTK_BOX (box), child: icon); |
620 | gtk_list_item_set_child (self: list_item, child: box); |
621 | } |
622 | |
623 | static void |
624 | selected_item_changed (GtkDropDown *self, |
625 | GParamSpec *pspec, |
626 | GtkListItem *list_item) |
627 | { |
628 | GtkWidget *box; |
629 | GtkWidget *icon; |
630 | |
631 | box = gtk_list_item_get_child (self: list_item); |
632 | icon = gtk_widget_get_last_child (widget: box); |
633 | |
634 | if (gtk_drop_down_get_selected_item (self) == gtk_list_item_get_item (self: list_item)) |
635 | gtk_widget_set_opacity (widget: icon, opacity: 1.0); |
636 | else |
637 | gtk_widget_set_opacity (widget: icon, opacity: 0.0); |
638 | } |
639 | |
640 | static void |
641 | bind_item (GtkSignalListItemFactory *factory, |
642 | GtkListItem *list_item, |
643 | gpointer data) |
644 | { |
645 | GtkDropDown *self = data; |
646 | gpointer item; |
647 | GtkWidget *box; |
648 | GtkWidget *label; |
649 | GtkWidget *icon; |
650 | GValue value = G_VALUE_INIT; |
651 | |
652 | item = gtk_list_item_get_item (self: list_item); |
653 | box = gtk_list_item_get_child (self: list_item); |
654 | label = gtk_widget_get_first_child (widget: box); |
655 | icon = gtk_widget_get_last_child (widget: box); |
656 | |
657 | if (self->expression && |
658 | gtk_expression_evaluate (self: self->expression, this_: item, value: &value)) |
659 | { |
660 | gtk_label_set_label (GTK_LABEL (label), str: g_value_get_string (value: &value)); |
661 | g_value_unset (value: &value); |
662 | } |
663 | else if (GTK_IS_STRING_OBJECT (ptr: item)) |
664 | { |
665 | const char *string; |
666 | |
667 | string = gtk_string_object_get_string (self: GTK_STRING_OBJECT (ptr: item)); |
668 | gtk_label_set_label (GTK_LABEL (label), str: string); |
669 | } |
670 | else |
671 | { |
672 | g_critical ("Either GtkDropDown:factory or GtkDropDown:expression must be set" ); |
673 | } |
674 | |
675 | if (gtk_widget_get_ancestor (widget: box, GTK_TYPE_POPOVER) == self->popup) |
676 | { |
677 | gtk_widget_show (widget: icon); |
678 | g_signal_connect (self, "notify::selected-item" , |
679 | G_CALLBACK (selected_item_changed), list_item); |
680 | selected_item_changed (self, NULL, list_item); |
681 | } |
682 | else |
683 | { |
684 | gtk_widget_hide (widget: icon); |
685 | } |
686 | } |
687 | |
688 | static void |
689 | unbind_item (GtkSignalListItemFactory *factory, |
690 | GtkListItem *list_item, |
691 | gpointer data) |
692 | { |
693 | GtkDropDown *self = data; |
694 | |
695 | g_signal_handlers_disconnect_by_func (self, selected_item_changed, list_item); |
696 | } |
697 | |
698 | static void |
699 | set_default_factory (GtkDropDown *self) |
700 | { |
701 | GtkListItemFactory *factory; |
702 | |
703 | factory = gtk_signal_list_item_factory_new (); |
704 | |
705 | g_signal_connect (factory, "setup" , G_CALLBACK (setup_item), self); |
706 | g_signal_connect (factory, "bind" , G_CALLBACK (bind_item), self); |
707 | g_signal_connect (factory, "unbind" , G_CALLBACK (unbind_item), self); |
708 | |
709 | gtk_drop_down_set_factory (self, factory); |
710 | |
711 | g_object_unref (object: factory); |
712 | } |
713 | |
714 | static void |
715 | gtk_drop_down_init (GtkDropDown *self) |
716 | { |
717 | g_type_ensure (GTK_TYPE_BUILTIN_ICON); |
718 | g_type_ensure (GTK_TYPE_LIST_ITEM_WIDGET); |
719 | |
720 | gtk_widget_init_template (GTK_WIDGET (self)); |
721 | |
722 | self->show_arrow = gtk_widget_get_visible (widget: self->arrow); |
723 | |
724 | set_default_factory (self); |
725 | } |
726 | |
727 | /** |
728 | * gtk_drop_down_new: |
729 | * @model: (transfer full) (nullable): the model to use |
730 | * @expression: (transfer full) (nullable): the expression to use |
731 | * |
732 | * Creates a new `GtkDropDown`. |
733 | * |
734 | * You may want to call [method@Gtk.DropDown.set_factory] |
735 | * to set up a way to map its items to widgets. |
736 | * |
737 | * Returns: a new `GtkDropDown` |
738 | */ |
739 | GtkWidget * |
740 | gtk_drop_down_new (GListModel *model, |
741 | GtkExpression *expression) |
742 | { |
743 | GtkWidget *self; |
744 | |
745 | self = g_object_new (GTK_TYPE_DROP_DOWN, |
746 | first_property_name: "model" , model, |
747 | "expression" , expression, |
748 | NULL); |
749 | |
750 | /* we're consuming the references */ |
751 | g_clear_object (&model); |
752 | g_clear_pointer (&expression, gtk_expression_unref); |
753 | |
754 | return self; |
755 | } |
756 | |
757 | /** |
758 | * gtk_drop_down_new_from_strings: |
759 | * @strings: (array zero-terminated=1): The strings to put in the dropdown |
760 | * |
761 | * Creates a new `GtkDropDown` that is populated with |
762 | * the strings. |
763 | * |
764 | * Returns: a new `GtkDropDown` |
765 | */ |
766 | GtkWidget * |
767 | gtk_drop_down_new_from_strings (const char * const *strings) |
768 | { |
769 | return gtk_drop_down_new (model: G_LIST_MODEL (ptr: gtk_string_list_new (strings)), NULL); |
770 | } |
771 | |
772 | /** |
773 | * gtk_drop_down_get_model: (attributes org.gtk.Method.get_property=model) |
774 | * @self: a `GtkDropDown` |
775 | * |
776 | * Gets the model that provides the displayed items. |
777 | * |
778 | * Returns: (nullable) (transfer none): The model in use |
779 | */ |
780 | GListModel * |
781 | gtk_drop_down_get_model (GtkDropDown *self) |
782 | { |
783 | g_return_val_if_fail (GTK_IS_DROP_DOWN (self), NULL); |
784 | |
785 | return self->model; |
786 | } |
787 | |
788 | /** |
789 | * gtk_drop_down_set_model: (attributes org.gtk.Method.set_property=model) |
790 | * @self: a `GtkDropDown` |
791 | * @model: (nullable) (transfer none): the model to use |
792 | * |
793 | * Sets the `GListModel` to use. |
794 | */ |
795 | void |
796 | gtk_drop_down_set_model (GtkDropDown *self, |
797 | GListModel *model) |
798 | { |
799 | g_return_if_fail (GTK_IS_DROP_DOWN (self)); |
800 | g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); |
801 | |
802 | if (!g_set_object (&self->model, model)) |
803 | return; |
804 | |
805 | if (model == NULL) |
806 | { |
807 | gtk_list_view_set_model (GTK_LIST_VIEW (self->popup_list), NULL); |
808 | |
809 | if (self->selection) |
810 | g_signal_handlers_disconnect_by_func (self->selection, selection_changed, self); |
811 | |
812 | g_clear_object (&self->selection); |
813 | g_clear_object (&self->filter_model); |
814 | g_clear_object (&self->popup_selection); |
815 | } |
816 | else |
817 | { |
818 | GListModel *filter_model; |
819 | GtkSelectionModel *selection; |
820 | |
821 | filter_model = G_LIST_MODEL (ptr: gtk_filter_list_model_new (g_object_ref (model), NULL)); |
822 | g_set_object (&self->filter_model, filter_model); |
823 | |
824 | update_filter (self); |
825 | |
826 | selection = GTK_SELECTION_MODEL (ptr: gtk_single_selection_new (model: filter_model)); |
827 | g_set_object (&self->popup_selection, selection); |
828 | gtk_list_view_set_model (GTK_LIST_VIEW (self->popup_list), model: selection); |
829 | g_object_unref (object: selection); |
830 | |
831 | selection = GTK_SELECTION_MODEL (ptr: gtk_single_selection_new (g_object_ref (model))); |
832 | g_set_object (&self->selection, selection); |
833 | g_object_unref (object: selection); |
834 | |
835 | g_signal_connect (self->selection, "notify::selected" , G_CALLBACK (selection_changed), self); |
836 | selection_changed (selection: GTK_SINGLE_SELECTION (ptr: self->selection), NULL, data: self); |
837 | } |
838 | |
839 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_MODEL]); |
840 | } |
841 | |
842 | /** |
843 | * gtk_drop_down_get_factory: (attributes org.gtk.Method.get_property=factory) |
844 | * @self: a `GtkDropDown` |
845 | * |
846 | * Gets the factory that's currently used to populate list items. |
847 | * |
848 | * The factory returned by this function is always used for the |
849 | * item in the button. It is also used for items in the popup |
850 | * if [property@Gtk.DropDown:list-factory] is not set. |
851 | * |
852 | * Returns: (nullable) (transfer none): The factory in use |
853 | */ |
854 | GtkListItemFactory * |
855 | gtk_drop_down_get_factory (GtkDropDown *self) |
856 | { |
857 | g_return_val_if_fail (GTK_IS_DROP_DOWN (self), NULL); |
858 | |
859 | return self->factory; |
860 | } |
861 | |
862 | /** |
863 | * gtk_drop_down_set_factory: (attributes org.gtk.Method.set_property=factory) |
864 | * @self: a `GtkDropDown` |
865 | * @factory: (nullable) (transfer none): the factory to use |
866 | * |
867 | * Sets the `GtkListItemFactory` to use for populating list items. |
868 | */ |
869 | void |
870 | gtk_drop_down_set_factory (GtkDropDown *self, |
871 | GtkListItemFactory *factory) |
872 | { |
873 | g_return_if_fail (GTK_IS_DROP_DOWN (self)); |
874 | g_return_if_fail (factory == NULL || GTK_LIST_ITEM_FACTORY (factory)); |
875 | |
876 | if (!g_set_object (&self->factory, factory)) |
877 | return; |
878 | |
879 | gtk_list_item_widget_set_factory (GTK_LIST_ITEM_WIDGET (self->button_item), factory); |
880 | if (self->list_factory == NULL) |
881 | gtk_list_view_set_factory (GTK_LIST_VIEW (self->popup_list), factory); |
882 | |
883 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_FACTORY]); |
884 | } |
885 | |
886 | /** |
887 | * gtk_drop_down_get_list_factory: (attributes org.gtk.Method.get_property=list-factory) |
888 | * @self: a `GtkDropDown` |
889 | * |
890 | * Gets the factory that's currently used to populate list items in the popup. |
891 | * |
892 | * Returns: (nullable) (transfer none): The factory in use |
893 | */ |
894 | GtkListItemFactory * |
895 | gtk_drop_down_get_list_factory (GtkDropDown *self) |
896 | { |
897 | g_return_val_if_fail (GTK_IS_DROP_DOWN (self), NULL); |
898 | |
899 | return self->list_factory; |
900 | } |
901 | |
902 | /** |
903 | * gtk_drop_down_set_list_factory: (attributes org.gtk.Method.set_property=list-factory) |
904 | * @self: a `GtkDropDown` |
905 | * @factory: (nullable) (transfer none): the factory to use |
906 | * |
907 | * Sets the `GtkListItemFactory` to use for populating list items in the popup. |
908 | */ |
909 | void |
910 | gtk_drop_down_set_list_factory (GtkDropDown *self, |
911 | GtkListItemFactory *factory) |
912 | { |
913 | g_return_if_fail (GTK_IS_DROP_DOWN (self)); |
914 | g_return_if_fail (factory == NULL || GTK_LIST_ITEM_FACTORY (factory)); |
915 | |
916 | if (!g_set_object (&self->list_factory, factory)) |
917 | return; |
918 | |
919 | if (self->list_factory != NULL) |
920 | gtk_list_view_set_factory (GTK_LIST_VIEW (self->popup_list), factory: self->list_factory); |
921 | else |
922 | gtk_list_view_set_factory (GTK_LIST_VIEW (self->popup_list), factory: self->factory); |
923 | |
924 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LIST_FACTORY]); |
925 | } |
926 | |
927 | /** |
928 | * gtk_drop_down_set_selected: (attributes org.gtk.Method.set_property=selected) |
929 | * @self: a `GtkDropDown` |
930 | * @position: the position of the item to select, or %GTK_INVALID_LIST_POSITION |
931 | * |
932 | * Selects the item at the given position. |
933 | */ |
934 | void |
935 | gtk_drop_down_set_selected (GtkDropDown *self, |
936 | guint position) |
937 | { |
938 | g_return_if_fail (GTK_IS_DROP_DOWN (self)); |
939 | |
940 | if (self->selection == NULL) |
941 | return; |
942 | |
943 | if (gtk_single_selection_get_selected (self: GTK_SINGLE_SELECTION (ptr: self->selection)) == position) |
944 | return; |
945 | |
946 | gtk_single_selection_set_selected (self: GTK_SINGLE_SELECTION (ptr: self->selection), position); |
947 | } |
948 | |
949 | /** |
950 | * gtk_drop_down_get_selected: (attributes org.gtk.Method.get_property=selected) |
951 | * @self: a `GtkDropDown` |
952 | * |
953 | * Gets the position of the selected item. |
954 | * |
955 | * Returns: the position of the selected item, or %GTK_INVALID_LIST_POSITION |
956 | * if not item is selected |
957 | */ |
958 | guint |
959 | gtk_drop_down_get_selected (GtkDropDown *self) |
960 | { |
961 | g_return_val_if_fail (GTK_IS_DROP_DOWN (self), GTK_INVALID_LIST_POSITION); |
962 | |
963 | if (self->selection == NULL) |
964 | return GTK_INVALID_LIST_POSITION; |
965 | |
966 | return gtk_single_selection_get_selected (self: GTK_SINGLE_SELECTION (ptr: self->selection)); |
967 | } |
968 | |
969 | /** |
970 | * gtk_drop_down_get_selected_item: (attributes org.gtk.Method.get_property=selected-item) |
971 | * @self: a `GtkDropDown` |
972 | * |
973 | * Gets the selected item. If no item is selected, %NULL is returned. |
974 | * |
975 | * Returns: (transfer none) (type GObject) (nullable): The selected item |
976 | */ |
977 | gpointer |
978 | gtk_drop_down_get_selected_item (GtkDropDown *self) |
979 | { |
980 | g_return_val_if_fail (GTK_IS_DROP_DOWN (self), NULL); |
981 | |
982 | if (self->selection == NULL) |
983 | return NULL; |
984 | |
985 | return gtk_single_selection_get_selected_item (self: GTK_SINGLE_SELECTION (ptr: self->selection)); |
986 | } |
987 | |
988 | /** |
989 | * gtk_drop_down_set_enable_search: (attributes org.gtk.Method.set_property=enable-search) |
990 | * @self: a `GtkDropDown` |
991 | * @enable_search: whether to enable search |
992 | * |
993 | * Sets whether a search entry will be shown in the popup that |
994 | * allows to search for items in the list. |
995 | * |
996 | * Note that [property@Gtk.DropDown:expression] must be set for |
997 | * search to work. |
998 | */ |
999 | void |
1000 | gtk_drop_down_set_enable_search (GtkDropDown *self, |
1001 | gboolean enable_search) |
1002 | { |
1003 | g_return_if_fail (GTK_IS_DROP_DOWN (self)); |
1004 | |
1005 | enable_search = !!enable_search; |
1006 | |
1007 | if (self->enable_search == enable_search) |
1008 | return; |
1009 | |
1010 | self->enable_search = enable_search; |
1011 | |
1012 | gtk_editable_set_text (GTK_EDITABLE (self->search_entry), text: "" ); |
1013 | gtk_widget_set_visible (widget: self->search_box, visible: enable_search); |
1014 | |
1015 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ENABLE_SEARCH]); |
1016 | } |
1017 | |
1018 | /** |
1019 | * gtk_drop_down_get_enable_search: (attributes org.gtk.Method.set_property=enable-search) |
1020 | * @self: a `GtkDropDown` |
1021 | * |
1022 | * Returns whether search is enabled. |
1023 | * |
1024 | * Returns: %TRUE if the popup includes a search entry |
1025 | */ |
1026 | gboolean |
1027 | gtk_drop_down_get_enable_search (GtkDropDown *self) |
1028 | { |
1029 | g_return_val_if_fail (GTK_IS_DROP_DOWN (self), FALSE); |
1030 | |
1031 | return self->enable_search; |
1032 | } |
1033 | |
1034 | /** |
1035 | * gtk_drop_down_set_expression: (attributes org.gtk.Method.set_property=expression) |
1036 | * @self: a `GtkDropDown` |
1037 | * @expression: (nullable): a `GtkExpression` |
1038 | * |
1039 | * Sets the expression that gets evaluated to obtain strings from items. |
1040 | * |
1041 | * This is used for search in the popup. The expression must have |
1042 | * a value type of %G_TYPE_STRING. |
1043 | */ |
1044 | void |
1045 | gtk_drop_down_set_expression (GtkDropDown *self, |
1046 | GtkExpression *expression) |
1047 | { |
1048 | g_return_if_fail (GTK_IS_DROP_DOWN (self)); |
1049 | g_return_if_fail (expression == NULL || |
1050 | gtk_expression_get_value_type (expression) == G_TYPE_STRING); |
1051 | |
1052 | if (self->expression == expression) |
1053 | return; |
1054 | |
1055 | if (self->expression) |
1056 | gtk_expression_unref (self: self->expression); |
1057 | self->expression = expression; |
1058 | if (self->expression) |
1059 | gtk_expression_ref (self: self->expression); |
1060 | |
1061 | update_filter (self); |
1062 | |
1063 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_EXPRESSION]); |
1064 | } |
1065 | |
1066 | /** |
1067 | * gtk_drop_down_get_expression: (attributes org.gtk.Method.get_property=expression) |
1068 | * @self: a `GtkDropDown` |
1069 | * |
1070 | * Gets the expression set that is used to obtain strings from items. |
1071 | * |
1072 | * See [method@Gtk.DropDown.set_expression]. |
1073 | * |
1074 | * Returns: (nullable) (transfer none): a `GtkExpression` |
1075 | */ |
1076 | GtkExpression * |
1077 | gtk_drop_down_get_expression (GtkDropDown *self) |
1078 | { |
1079 | g_return_val_if_fail (GTK_IS_DROP_DOWN (self), NULL); |
1080 | |
1081 | return self->expression; |
1082 | } |
1083 | |
1084 | /** |
1085 | * gtk_drop_down_set_show_arrow: (attributes org.gtk.Method.set_property=show-arrow) |
1086 | * @self: a `GtkDropDown` |
1087 | * @show_arrow: whether to show an arrow within the widget |
1088 | * |
1089 | * Sets whether an arrow will be displayed within the widget. |
1090 | * |
1091 | * Since: 4.6 |
1092 | */ |
1093 | void |
1094 | gtk_drop_down_set_show_arrow (GtkDropDown *self, |
1095 | gboolean show_arrow) |
1096 | { |
1097 | g_return_if_fail (GTK_IS_DROP_DOWN (self)); |
1098 | |
1099 | show_arrow = !!show_arrow; |
1100 | |
1101 | if (self->show_arrow == show_arrow) |
1102 | return; |
1103 | |
1104 | self->show_arrow = show_arrow; |
1105 | |
1106 | gtk_widget_set_visible (widget: self->arrow, visible: show_arrow); |
1107 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SHOW_ARROW]); |
1108 | } |
1109 | |
1110 | /** |
1111 | * gtk_drop_down_get_show_arrow: (attributes org.gtk.Method.set_property=show-arrow) |
1112 | * @self: a `GtkDropDown` |
1113 | * |
1114 | * Returns whether to show an arrow within the widget. |
1115 | * |
1116 | * Returns: %TRUE if an arrow will be shown. |
1117 | * |
1118 | * Since: 4.6 |
1119 | */ |
1120 | gboolean |
1121 | gtk_drop_down_get_show_arrow (GtkDropDown *self) |
1122 | { |
1123 | g_return_val_if_fail (GTK_IS_DROP_DOWN (self), FALSE); |
1124 | |
1125 | return self->show_arrow; |
1126 | } |
1127 | |