1/* Lists/Selections
2 *
3 * The GtkDropDown widget is a modern alternative to GtkComboBox.
4 * It uses list models instead of tree models, and the content is
5 * displayed using widgets instead of cell renderers.
6 *
7 * This example also shows a custom widget that can replace
8 * GtkEntryCompletion or GtkComboBoxText. It is not currently
9 * part of GTK.
10 */
11
12#include <gtk/gtk.h>
13#include "suggestionentry.h"
14
15#define STRING_TYPE_HOLDER (string_holder_get_type ())
16G_DECLARE_FINAL_TYPE (StringHolder, string_holder, STRING, HOLDER, GObject)
17
18struct _StringHolder {
19 GObject parent_instance;
20 char *title;
21 char *icon;
22 char *description;
23};
24
25G_DEFINE_TYPE (StringHolder, string_holder, G_TYPE_OBJECT);
26
27static void
28string_holder_init (StringHolder *holder)
29{
30}
31
32static void
33string_holder_finalize (GObject *object)
34{
35 StringHolder *holder = STRING_HOLDER (ptr: object);
36
37 g_free (mem: holder->title);
38 g_free (mem: holder->icon);
39 g_free (mem: holder->description);
40
41 G_OBJECT_CLASS (string_holder_parent_class)->finalize (object);
42}
43
44static void
45string_holder_class_init (StringHolderClass *class)
46{
47 GObjectClass *object_class = G_OBJECT_CLASS (class);
48
49 object_class->finalize = string_holder_finalize;
50}
51
52static StringHolder *
53string_holder_new (const char *title, const char *icon, const char *description)
54{
55 StringHolder *holder = g_object_new (STRING_TYPE_HOLDER, NULL);
56 holder->title = g_strdup (str: title);
57 holder->icon = g_strdup (str: icon);
58 holder->description = g_strdup (str: description);
59 return holder;
60}
61
62static void
63strings_setup_item_single_line (GtkSignalListItemFactory *factory,
64 GtkListItem *item)
65{
66 GtkWidget *box, *image, *title;
67 GtkWidget *checkmark;
68
69 box = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 10);
70
71 image = gtk_image_new ();
72 title = gtk_label_new (str: "");
73 gtk_label_set_xalign (GTK_LABEL (title), xalign: 0.0);
74 checkmark = gtk_image_new_from_icon_name (icon_name: "object-select-symbolic");
75
76 gtk_box_append (GTK_BOX (box), child: image);
77 gtk_box_append (GTK_BOX (box), child: title);
78 gtk_box_append (GTK_BOX (box), child: checkmark);
79
80 g_object_set_data (G_OBJECT (item), key: "title", data: title);
81 g_object_set_data (G_OBJECT (item), key: "image", data: image);
82 g_object_set_data (G_OBJECT (item), key: "checkmark", data: checkmark);
83
84 gtk_list_item_set_child (self: item, child: box);
85}
86
87static void
88strings_setup_item_full (GtkSignalListItemFactory *factory,
89 GtkListItem *item)
90{
91 GtkWidget *box, *box2, *image, *title, *description;
92 GtkWidget *checkmark;
93
94 image = gtk_image_new ();
95 title = gtk_label_new (str: "");
96 gtk_label_set_xalign (GTK_LABEL (title), xalign: 0.0);
97 description = gtk_label_new (str: "");
98 gtk_label_set_xalign (GTK_LABEL (description), xalign: 0.0);
99 gtk_widget_add_css_class (widget: description, css_class: "dim-label");
100 checkmark = gtk_image_new_from_icon_name (icon_name: "object-select-symbolic");
101
102 box = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 10);
103 box2 = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 2);
104
105 gtk_box_append (GTK_BOX (box), child: image);
106 gtk_box_append (GTK_BOX (box), child: box2);
107 gtk_box_append (GTK_BOX (box2), child: title);
108 gtk_box_append (GTK_BOX (box2), child: description);
109 gtk_box_append (GTK_BOX (box), child: checkmark);
110
111 g_object_set_data (G_OBJECT (item), key: "title", data: title);
112 g_object_set_data (G_OBJECT (item), key: "image", data: image);
113 g_object_set_data (G_OBJECT (item), key: "description", data: description);
114 g_object_set_data (G_OBJECT (item), key: "checkmark", data: checkmark);
115
116 gtk_list_item_set_child (self: item, child: box);
117}
118
119static void
120selected_item_changed (GtkDropDown *dropdown,
121 GParamSpec *pspec,
122 GtkListItem *item)
123{
124 GtkWidget *checkmark;
125
126 checkmark = g_object_get_data (G_OBJECT (item), key: "checkmark");
127
128 if (gtk_drop_down_get_selected_item (self: dropdown) == gtk_list_item_get_item (self: item))
129 gtk_widget_set_opacity (widget: checkmark, opacity: 1.0);
130 else
131 gtk_widget_set_opacity (widget: checkmark, opacity: 0.0);
132}
133
134static void
135strings_bind_item (GtkSignalListItemFactory *factory,
136 GtkListItem *item,
137 gpointer data)
138{
139 GtkDropDown *dropdown = data;
140 GtkWidget *image, *title, *description;
141 GtkWidget *checkmark;
142 StringHolder *holder;
143 GtkWidget *popup;
144
145 holder = gtk_list_item_get_item (self: item);
146
147 title = g_object_get_data (G_OBJECT (item), key: "title");
148 image = g_object_get_data (G_OBJECT (item), key: "image");
149 description = g_object_get_data (G_OBJECT (item), key: "description");
150 checkmark = g_object_get_data (G_OBJECT (item), key: "checkmark");
151
152 gtk_label_set_label (GTK_LABEL (title), str: holder->title);
153 if (image)
154 {
155 gtk_image_set_from_icon_name (GTK_IMAGE (image), icon_name: holder->icon);
156 gtk_widget_set_visible (widget: image, visible: holder->icon != NULL);
157 }
158 if (description)
159 {
160 gtk_label_set_label (GTK_LABEL (description), str: holder->description);
161 gtk_widget_set_visible (widget: description , visible: holder->description != NULL);
162 }
163
164 popup = gtk_widget_get_ancestor (widget: title, GTK_TYPE_POPOVER);
165 if (popup && gtk_widget_is_ancestor (widget: popup, GTK_WIDGET (dropdown)))
166 {
167 gtk_widget_show (widget: checkmark);
168 g_signal_connect (dropdown, "notify::selected-item",
169 G_CALLBACK (selected_item_changed), item);
170 selected_item_changed (dropdown, NULL, item);
171 }
172 else
173 {
174 gtk_widget_hide (widget: checkmark);
175 }
176}
177
178static void
179strings_unbind_item (GtkSignalListItemFactory *factory,
180 GtkListItem *list_item,
181 gpointer data)
182{
183 GtkDropDown *dropdown = data;
184
185 g_signal_handlers_disconnect_by_func (dropdown, selected_item_changed, list_item);
186}
187
188static GtkListItemFactory *
189strings_factory_new (gpointer data, gboolean full)
190{
191 GtkListItemFactory *factory;
192
193 factory = gtk_signal_list_item_factory_new ();
194 if (full)
195 g_signal_connect (factory, "setup", G_CALLBACK (strings_setup_item_full), data);
196 else
197 g_signal_connect (factory, "setup", G_CALLBACK (strings_setup_item_single_line), data);
198 g_signal_connect (factory, "bind", G_CALLBACK (strings_bind_item), data);
199 g_signal_connect (factory, "unbind", G_CALLBACK (strings_unbind_item), data);
200
201 return factory;
202}
203
204static GListModel *
205strings_model_new (const char *const *titles,
206 const char *const *icons,
207 const char *const *descriptions)
208{
209 GListStore *store;
210 int i;
211
212 store = g_list_store_new (STRING_TYPE_HOLDER);
213 for (i = 0; titles[i]; i++)
214 {
215 StringHolder *holder = string_holder_new (title: titles[i],
216 icon: icons ? icons[i] : NULL,
217 description: descriptions ? descriptions[i] : NULL);
218 g_list_store_append (store, item: holder);
219 g_object_unref (object: holder);
220 }
221
222 return G_LIST_MODEL (ptr: store);
223}
224
225static GtkWidget *
226drop_down_new_from_strings (const char *const *titles,
227 const char *const *icons,
228 const char *const *descriptions)
229{
230 GtkWidget *widget;
231 GListModel *model;
232 GtkListItemFactory *factory;
233 GtkListItemFactory *list_factory;
234
235 g_return_val_if_fail (titles != NULL, NULL);
236 g_return_val_if_fail (icons == NULL || g_strv_length ((char **)icons) == g_strv_length ((char **)titles), NULL);
237 g_return_val_if_fail (descriptions == NULL || g_strv_length ((char **)icons) == g_strv_length ((char **)descriptions), NULL);
238
239 model = strings_model_new (titles, icons, descriptions);
240 widget = g_object_new (GTK_TYPE_DROP_DOWN,
241 first_property_name: "model", model,
242 NULL);
243 g_object_unref (object: model);
244
245 factory = strings_factory_new (data: widget, FALSE);
246 if (icons != NULL || descriptions != NULL)
247 list_factory = strings_factory_new (data: widget, TRUE);
248 else
249 list_factory = NULL;
250
251 g_object_set (object: widget,
252 first_property_name: "factory", factory,
253 "list-factory", list_factory,
254 NULL);
255
256 g_object_unref (object: factory);
257 if (list_factory)
258 g_object_unref (object: list_factory);
259
260 return widget;
261}
262
263static char *
264get_family_name (gpointer item)
265{
266 return g_strdup (str: pango_font_family_get_name (PANGO_FONT_FAMILY (item)));
267}
268
269static char *
270get_title (gpointer item)
271{
272 return g_strdup (str: STRING_HOLDER (ptr: item)->title);
273}
274
275static char *
276get_file_name (gpointer item)
277{
278 return g_strdup (str: g_file_info_get_display_name (G_FILE_INFO (item)));
279}
280
281static void
282setup_item (GtkSignalListItemFactory *factory,
283 GtkListItem *item)
284{
285 GtkWidget *box;
286 GtkWidget *icon;
287 GtkWidget *label;
288
289 box = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 10);
290 icon = gtk_image_new ();
291 label = gtk_label_new (str: "");
292 gtk_label_set_xalign (GTK_LABEL (label), xalign: 0);
293 gtk_box_append (GTK_BOX (box), child: icon);
294 gtk_box_append (GTK_BOX (box), child: label);
295 gtk_list_item_set_child (self: item, child: box);
296}
297
298static void
299bind_item (GtkSignalListItemFactory *factory,
300 GtkListItem *item)
301{
302 MatchObject *match = MATCH_OBJECT (gtk_list_item_get_item (item));
303 GFileInfo *info = G_FILE_INFO (match_object_get_item (match));
304 GtkWidget *box = gtk_list_item_get_child (self: item);
305 GtkWidget *icon = gtk_widget_get_first_child (widget: box);
306 GtkWidget *label = gtk_widget_get_last_child (widget: box);
307
308 gtk_image_set_from_gicon (GTK_IMAGE (icon), icon: g_file_info_get_icon (info));
309 gtk_label_set_label (GTK_LABEL (label), str: g_file_info_get_display_name (info));
310}
311
312static void
313setup_highlight_item (GtkSignalListItemFactory *factory,
314 GtkListItem *item)
315{
316 GtkWidget *label;
317
318 label = gtk_label_new (str: "");
319 gtk_label_set_xalign (GTK_LABEL (label), xalign: 0);
320 gtk_list_item_set_child (self: item, child: label);
321}
322
323static void
324bind_highlight_item (GtkSignalListItemFactory *factory,
325 GtkListItem *item)
326{
327 MatchObject *obj;
328 GtkWidget *label;
329 PangoAttrList *attrs;
330 PangoAttribute *attr;
331 const char *str;
332
333 obj = MATCH_OBJECT (gtk_list_item_get_item (item));
334 label = gtk_list_item_get_child (self: item);
335
336 str = match_object_get_string (object: obj);
337
338 gtk_label_set_label (GTK_LABEL (label), str);
339 attrs = pango_attr_list_new ();
340 attr = pango_attr_weight_new (weight: PANGO_WEIGHT_BOLD);
341 attr->start_index = match_object_get_match_start (object: obj);
342 attr->end_index = match_object_get_match_end (object: obj);
343 pango_attr_list_insert (list: attrs, attr);
344 gtk_label_set_attributes (GTK_LABEL (label), attrs);
345 pango_attr_list_unref (list: attrs);
346}
347
348static void
349match_func (MatchObject *obj,
350 const char *search,
351 gpointer user_data)
352{
353 char *tmp1, *tmp2;
354 char *p;
355
356 tmp1 = g_utf8_normalize (str: match_object_get_string (object: obj), len: -1, mode: G_NORMALIZE_ALL);
357 tmp2 = g_utf8_normalize (str: search, len: -1, mode: G_NORMALIZE_ALL);
358
359 if ((p = strstr (haystack: tmp1, needle: tmp2)) != NULL)
360 match_object_set_match (object: obj,
361 start: p - tmp1,
362 end: (p - tmp1) + g_utf8_strlen (p: search, max: -1),
363 score: 1);
364 else
365 match_object_set_match (object: obj, start: 0, end: 0, score: 0);
366
367 g_free (mem: tmp1);
368 g_free (mem: tmp2);
369}
370
371GtkWidget *
372do_dropdown (GtkWidget *do_widget)
373{
374 static GtkWidget *window = NULL;
375 GtkWidget *button, *box, *spin, *check, *hbox, *label, *entry;
376 GListModel *model;
377 GtkExpression *expression;
378 GtkListItemFactory *factory;
379 const char * const times[] = { "1 minute", "2 minutes", "5 minutes", "20 minutes", NULL };
380 const char * const many_times[] = {
381 "1 minute", "2 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes",
382 "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes",
383 "55 minutes", "1 hour", "2 hours", "3 hours", "5 hours", "6 hours", "7 hours",
384 "8 hours", "9 hours", "10 hours", "11 hours", "12 hours", NULL
385 };
386 const char * const device_titles[] = { "Digital Output", "Headphones", "Digital Output", "Analog Output", NULL };
387 const char * const device_icons[] = { "audio-card-symbolic", "audio-headphones-symbolic", "audio-card-symbolic", "audio-card-symbolic", NULL };
388 const char * const device_descriptions[] = {
389 "Built-in Audio", "Built-in audio", "Thinkpad Tunderbolt 3 Dock USB Audio", "Thinkpad Tunderbolt 3 Dock USB Audio", NULL
390 };
391 char *cwd;
392 GFile *file;
393 GListModel *dir;
394 GtkStringList *strings;
395
396 if (!window)
397 {
398 window = gtk_window_new ();
399 gtk_window_set_display (GTK_WINDOW (window),
400 display: gtk_widget_get_display (widget: do_widget));
401 gtk_window_set_title (GTK_WINDOW (window), title: "Selections");
402 gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
403 g_object_add_weak_pointer (G_OBJECT (window), weak_pointer_location: (gpointer *)&window);
404
405 hbox = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 20);
406
407 gtk_widget_set_margin_start (widget: hbox, margin: 20);
408 gtk_widget_set_margin_end (widget: hbox, margin: 20);
409 gtk_widget_set_margin_top (widget: hbox, margin: 20);
410 gtk_widget_set_margin_bottom (widget: hbox, margin: 20);
411 gtk_window_set_child (GTK_WINDOW (window), child: hbox);
412
413 box = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 10);
414 gtk_box_append (GTK_BOX (hbox), child: box);
415
416 label = gtk_label_new (str: "Dropdowns");
417 gtk_widget_add_css_class (widget: label, css_class: "title-4");
418 gtk_box_append (GTK_BOX (box), child: label);
419
420 /* A basic dropdown */
421 button = drop_down_new_from_strings (titles: times, NULL, NULL);
422 gtk_box_append (GTK_BOX (box), child: button);
423
424 /* A dropdown using an expression to obtain strings */
425 button = drop_down_new_from_strings (titles: many_times, NULL, NULL);
426 gtk_drop_down_set_enable_search (self: GTK_DROP_DOWN (ptr: button), TRUE);
427 expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL,
428 n_params: 0, NULL,
429 callback_func: (GCallback)get_title,
430 NULL, NULL);
431 gtk_drop_down_set_expression (self: GTK_DROP_DOWN (ptr: button), expression);
432 gtk_expression_unref (self: expression);
433 gtk_box_append (GTK_BOX (box), child: button);
434
435 button = gtk_drop_down_new (NULL, NULL);
436
437 model = G_LIST_MODEL (ptr: pango_cairo_font_map_get_default ());
438 gtk_drop_down_set_model (self: GTK_DROP_DOWN (ptr: button), model);
439 gtk_drop_down_set_selected (self: GTK_DROP_DOWN (ptr: button), position: 0);
440
441 expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL,
442 n_params: 0, NULL,
443 callback_func: (GCallback)get_family_name,
444 NULL, NULL);
445 gtk_drop_down_set_expression (self: GTK_DROP_DOWN (ptr: button), expression);
446 gtk_expression_unref (self: expression);
447 gtk_box_append (GTK_BOX (box), child: button);
448
449 spin = gtk_spin_button_new_with_range (min: -1, max: g_list_model_get_n_items (list: G_LIST_MODEL (ptr: model)), step: 1);
450 gtk_widget_set_halign (widget: spin, align: GTK_ALIGN_START);
451 gtk_widget_set_margin_start (widget: spin, margin: 20);
452 g_object_bind_property (source: button, source_property: "selected", target: spin, target_property: "value", flags: G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
453 gtk_box_append (GTK_BOX (box), child: spin);
454
455 check = gtk_check_button_new_with_label (label: "Enable search");
456 gtk_widget_set_margin_start (widget: check, margin: 20);
457 g_object_bind_property (source: button, source_property: "enable-search", target: check, target_property: "active", flags: G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
458 gtk_box_append (GTK_BOX (box), child: check);
459
460 g_object_unref (object: model);
461
462 /* A dropdown with a separate list factory */
463 button = drop_down_new_from_strings (titles: device_titles, icons: device_icons, descriptions: device_descriptions);
464 gtk_box_append (GTK_BOX (box), child: button);
465
466 gtk_box_append (GTK_BOX (hbox), child: gtk_separator_new (orientation: GTK_ORIENTATION_VERTICAL));
467
468 box = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 10);
469 gtk_box_append (GTK_BOX (hbox), child: box);
470
471 label = gtk_label_new (str: "Suggestions");
472 gtk_widget_add_css_class (widget: label, css_class: "title-4");
473 gtk_box_append (GTK_BOX (box), child: label);
474
475 /* A basic suggestion entry */
476 entry = suggestion_entry_new ();
477 g_object_set (object: entry, first_property_name: "placeholder-text", "Words with T or G…", NULL);
478 strings = gtk_string_list_new (strings: (const char *[]){
479 "GNOME",
480 "gnominious",
481 "Gnomonic projection",
482 "total",
483 "totally",
484 "toto",
485 "tottery",
486 "totterer",
487 "Totten trust",
488 "totipotent",
489 "totipotency",
490 "totemism",
491 "totem pole",
492 "Totara",
493 "totalizer",
494 "totalizator",
495 "totalitarianism",
496 "total parenteral nutrition",
497 "total hysterectomy",
498 "total eclipse",
499 "Totipresence",
500 "Totipalmi",
501 "Tomboy",
502 "zombie",
503 NULL});
504 suggestion_entry_set_model (SUGGESTION_ENTRY (entry), model: G_LIST_MODEL (ptr: strings));
505 g_object_unref (object: strings);
506
507 gtk_box_append (GTK_BOX (box), child: entry);
508
509 /* A suggestion entry using a custom model, and no filtering */
510 entry = suggestion_entry_new ();
511
512 cwd = g_get_current_dir ();
513 file = g_file_new_for_path (path: cwd);
514 dir = G_LIST_MODEL (ptr: gtk_directory_list_new (attributes: "standard::display-name,standard::content-type,standard::icon,standard::size", file));
515 suggestion_entry_set_model (SUGGESTION_ENTRY (entry), model: dir);
516 g_object_unref (object: dir);
517 g_object_unref (object: file);
518 g_free (mem: cwd);
519
520 expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL,
521 n_params: 0, NULL,
522 callback_func: (GCallback)get_file_name,
523 NULL, NULL);
524 suggestion_entry_set_expression (SUGGESTION_ENTRY (entry), expression);
525 gtk_expression_unref (self: expression);
526
527 factory = gtk_signal_list_item_factory_new ();
528 g_signal_connect (factory, "setup", G_CALLBACK (setup_item), NULL);
529 g_signal_connect (factory, "bind", G_CALLBACK (bind_item), NULL);
530
531 suggestion_entry_set_factory (SUGGESTION_ENTRY (entry), factory);
532 g_object_unref (object: factory);
533
534 suggestion_entry_set_use_filter (SUGGESTION_ENTRY (entry), FALSE);
535 suggestion_entry_set_show_arrow (SUGGESTION_ENTRY (entry), TRUE);
536
537 gtk_box_append (GTK_BOX (box), child: entry);
538
539 /* A suggestion entry with match highlighting */
540 entry = suggestion_entry_new ();
541 g_object_set (object: entry, first_property_name: "placeholder-text", "Destination", NULL);
542
543 strings = gtk_string_list_new (strings: (const char *[]){
544 "app-mockups",
545 "settings-mockups",
546 "os-mockups",
547 "software-mockups",
548 "mocktails",
549 NULL});
550 suggestion_entry_set_model (SUGGESTION_ENTRY (entry), model: G_LIST_MODEL (ptr: strings));
551 g_object_unref (object: strings);
552
553 gtk_box_append (GTK_BOX (box), child: entry);
554
555 suggestion_entry_set_match_func (SUGGESTION_ENTRY (entry), func: match_func, NULL, NULL);
556
557 factory = gtk_signal_list_item_factory_new ();
558 g_signal_connect (factory, "setup", G_CALLBACK (setup_highlight_item), NULL);
559 g_signal_connect (factory, "bind", G_CALLBACK (bind_highlight_item), NULL);
560 suggestion_entry_set_factory (SUGGESTION_ENTRY (entry), factory);
561 g_object_unref (object: factory);
562
563 }
564
565 if (!gtk_widget_get_visible (widget: window))
566 gtk_widget_show (widget: window);
567 else
568 gtk_window_destroy (GTK_WINDOW (window));
569
570 return window;
571}
572

source code of gtk/demos/gtk-demo/dropdown.c