1#include "suggestionentry.h"
2
3struct _MatchObject
4{
5 GObject parent_instance;
6
7 GObject *item;
8 char *string;
9 guint match_start;
10 guint match_end;
11 guint score;
12};
13
14typedef struct
15{
16 GObjectClass parent_class;
17} MatchObjectClass;
18
19enum
20{
21 PROP_ITEM = 1,
22 PROP_STRING,
23 PROP_MATCH_START,
24 PROP_MATCH_END,
25 PROP_SCORE,
26 N_MATCH_PROPERTIES
27};
28
29static GParamSpec *match_properties[N_MATCH_PROPERTIES];
30
31G_DEFINE_TYPE (MatchObject, match_object, G_TYPE_OBJECT)
32
33static void
34match_object_init (MatchObject *object)
35{
36}
37
38static void
39match_object_get_property (GObject *object,
40 guint property_id,
41 GValue *value,
42 GParamSpec *pspec)
43{
44 MatchObject *self = MATCH_OBJECT (object);
45
46 switch (property_id)
47 {
48 case PROP_ITEM:
49 g_value_set_object (value, v_object: self->item);
50 break;
51
52 case PROP_STRING:
53 g_value_set_string (value, v_string: self->string);
54 break;
55
56 case PROP_MATCH_START:
57 g_value_set_uint (value, v_uint: self->match_start);
58 break;
59
60 case PROP_MATCH_END:
61 g_value_set_uint (value, v_uint: self->match_end);
62 break;
63
64 case PROP_SCORE:
65 g_value_set_uint (value, v_uint: self->score);
66 break;
67
68 default:
69 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
70 break;
71 }
72}
73
74static void
75match_object_set_property (GObject *object,
76 guint property_id,
77 const GValue *value,
78 GParamSpec *pspec)
79{
80 MatchObject *self = MATCH_OBJECT (object);
81
82 switch (property_id)
83 {
84 case PROP_ITEM:
85 self->item = g_value_get_object (value);
86 break;
87
88 case PROP_STRING:
89 self->string = g_value_dup_string (value);
90 break;
91
92 case PROP_MATCH_START:
93 if (self->match_start != g_value_get_uint (value))
94 {
95 self->match_start = g_value_get_uint (value);
96 g_object_notify_by_pspec (object, pspec);
97 }
98 break;
99
100 case PROP_MATCH_END:
101 if (self->match_end != g_value_get_uint (value))
102 {
103 self->match_end = g_value_get_uint (value);
104 g_object_notify_by_pspec (object, pspec);
105 }
106 break;
107
108 case PROP_SCORE:
109 if (self->score != g_value_get_uint (value))
110 {
111 self->score = g_value_get_uint (value);
112 g_object_notify_by_pspec (object, pspec);
113 }
114 break;
115
116 default:
117 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
118 break;
119 }
120}
121
122static void
123match_object_dispose (GObject *object)
124{
125 MatchObject *self = MATCH_OBJECT (object);
126
127 g_clear_object (&self->item);
128 g_clear_pointer (&self->string, g_free);
129
130 G_OBJECT_CLASS (match_object_parent_class)->dispose (object);
131}
132
133static void
134match_object_class_init (MatchObjectClass *class)
135{
136 GObjectClass *object_class = G_OBJECT_CLASS (class);
137
138 object_class->dispose = match_object_dispose;
139 object_class->get_property = match_object_get_property;
140 object_class->set_property = match_object_set_property;
141
142 match_properties[PROP_ITEM]
143 = g_param_spec_object (name: "item", nick: "Item", blurb: "Item",
144 G_TYPE_OBJECT,
145 flags: G_PARAM_READWRITE |
146 G_PARAM_CONSTRUCT_ONLY |
147 G_PARAM_STATIC_STRINGS);
148 match_properties[PROP_STRING]
149 = g_param_spec_string (name: "string", nick: "String", blurb: "String",
150 NULL,
151 flags: G_PARAM_READWRITE |
152 G_PARAM_CONSTRUCT_ONLY |
153 G_PARAM_STATIC_STRINGS);
154 match_properties[PROP_MATCH_START]
155 = g_param_spec_uint (name: "match-start", nick: "Match Start", blurb: "Match Start",
156 minimum: 0, G_MAXUINT, default_value: 0,
157 flags: G_PARAM_READWRITE |
158 G_PARAM_EXPLICIT_NOTIFY |
159 G_PARAM_STATIC_STRINGS);
160 match_properties[PROP_MATCH_END]
161 = g_param_spec_uint (name: "match-end", nick: "Match End", blurb: "Match End",
162 minimum: 0, G_MAXUINT, default_value: 0,
163 flags: G_PARAM_READWRITE |
164 G_PARAM_EXPLICIT_NOTIFY |
165 G_PARAM_STATIC_STRINGS);
166 match_properties[PROP_SCORE]
167 = g_param_spec_uint (name: "score", nick: "Score", blurb: "Score",
168 minimum: 0, G_MAXUINT, default_value: 0,
169 flags: G_PARAM_READWRITE |
170 G_PARAM_EXPLICIT_NOTIFY |
171 G_PARAM_STATIC_STRINGS);
172
173 g_object_class_install_properties (oclass: object_class, n_pspecs: N_MATCH_PROPERTIES, pspecs: match_properties);
174}
175
176static MatchObject *
177match_object_new (gpointer item,
178 const char *string)
179{
180 return g_object_new (MATCH_TYPE_OBJECT,
181 first_property_name: "item", item,
182 "string", string,
183 NULL);
184}
185
186gpointer
187match_object_get_item (MatchObject *object)
188{
189 return object->item;
190}
191
192const char *
193match_object_get_string (MatchObject *object)
194{
195 return object->string;
196}
197
198guint
199match_object_get_match_start (MatchObject *object)
200{
201 return object->match_start;
202}
203
204guint
205match_object_get_match_end (MatchObject *object)
206{
207 return object->match_end;
208}
209
210guint
211match_object_get_score (MatchObject *object)
212{
213 return object->score;
214}
215
216void
217match_object_set_match (MatchObject *object,
218 guint start,
219 guint end,
220 guint score)
221{
222 g_object_freeze_notify (G_OBJECT (object));
223
224 g_object_set (object,
225 first_property_name: "match-start", start,
226 "match-end", end,
227 "score", score,
228 NULL);
229
230 g_object_thaw_notify (G_OBJECT (object));
231}
232
233/* ---- */
234
235struct _SuggestionEntry
236{
237 GtkWidget parent_instance;
238
239 GListModel *model;
240 GtkListItemFactory *factory;
241 GtkExpression *expression;
242
243 GtkFilter *filter;
244 GtkMapListModel *map_model;
245 GtkSingleSelection *selection;
246
247 GtkWidget *entry;
248 GtkWidget *arrow;
249 GtkWidget *popup;
250 GtkWidget *list;
251
252 char *search;
253
254 SuggestionEntryMatchFunc match_func;
255 gpointer match_data;
256 GDestroyNotify destroy;
257
258 gulong changed_id;
259
260 guint use_filter : 1;
261 guint show_arrow : 1;
262};
263
264typedef struct _SuggestionEntryClass SuggestionEntryClass;
265
266struct _SuggestionEntryClass
267{
268 GtkWidgetClass parent_class;
269};
270
271enum
272{
273 PROP_0,
274 PROP_MODEL,
275 PROP_FACTORY,
276 PROP_EXPRESSION,
277 PROP_PLACEHOLDER_TEXT,
278 PROP_POPUP_VISIBLE,
279 PROP_USE_FILTER,
280 PROP_SHOW_ARROW,
281
282 N_PROPERTIES,
283};
284
285static void suggestion_entry_set_popup_visible (SuggestionEntry *self,
286 gboolean visible);
287
288static GtkEditable *
289suggestion_entry_get_delegate (GtkEditable *editable)
290{
291 return GTK_EDITABLE (SUGGESTION_ENTRY (editable)->entry);
292}
293
294static void
295suggestion_entry_editable_init (GtkEditableInterface *iface)
296{
297 iface->get_delegate = suggestion_entry_get_delegate;
298}
299
300G_DEFINE_TYPE_WITH_CODE (SuggestionEntry, suggestion_entry, GTK_TYPE_WIDGET,
301 G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE,
302 suggestion_entry_editable_init))
303
304static GParamSpec *properties[N_PROPERTIES] = { NULL, };
305
306static void
307suggestion_entry_dispose (GObject *object)
308{
309 SuggestionEntry *self = SUGGESTION_ENTRY (object);
310
311 if (self->changed_id)
312 {
313 g_signal_handler_disconnect (instance: self->entry, handler_id: self->changed_id);
314 self->changed_id = 0;
315 }
316 g_clear_pointer (&self->entry, gtk_widget_unparent);
317 g_clear_pointer (&self->arrow, gtk_widget_unparent);
318 g_clear_pointer (&self->popup, gtk_widget_unparent);
319
320 g_clear_pointer (&self->expression, gtk_expression_unref);
321 g_clear_object (&self->factory);
322
323 g_clear_object (&self->model);
324 g_clear_object (&self->map_model);
325 g_clear_object (&self->selection);
326
327 g_clear_pointer (&self->search, g_free);
328
329 if (self->destroy)
330 self->destroy (self->match_data);
331
332 G_OBJECT_CLASS (suggestion_entry_parent_class)->dispose (object);
333}
334
335static void
336suggestion_entry_get_property (GObject *object,
337 guint property_id,
338 GValue *value,
339 GParamSpec *pspec)
340{
341 SuggestionEntry *self = SUGGESTION_ENTRY (object);
342
343 if (gtk_editable_delegate_get_property (object, prop_id: property_id, value, pspec))
344 return;
345
346 switch (property_id)
347 {
348 case PROP_MODEL:
349 g_value_set_object (value, v_object: suggestion_entry_get_model (self));
350 break;
351
352 case PROP_FACTORY:
353 g_value_set_object (value, v_object: suggestion_entry_get_factory (self));
354 break;
355
356 case PROP_EXPRESSION:
357 gtk_value_set_expression (value, expression: suggestion_entry_get_expression (self));
358 break;
359
360 case PROP_PLACEHOLDER_TEXT:
361 g_value_set_string (value, v_string: gtk_text_get_placeholder_text (GTK_TEXT (self->entry)));
362 break;
363
364 case PROP_POPUP_VISIBLE:
365 g_value_set_boolean (value, v_boolean: self->popup && gtk_widget_get_visible (widget: self->popup));
366 break;
367
368 case PROP_USE_FILTER:
369 g_value_set_boolean (value, v_boolean: suggestion_entry_get_use_filter (self));
370 break;
371
372 case PROP_SHOW_ARROW:
373 g_value_set_boolean (value, v_boolean: suggestion_entry_get_show_arrow (self));
374 break;
375
376 default:
377 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
378 break;
379 }
380}
381
382static void
383suggestion_entry_set_property (GObject *object,
384 guint property_id,
385 const GValue *value,
386 GParamSpec *pspec)
387{
388 SuggestionEntry *self = SUGGESTION_ENTRY (object);
389
390 if (gtk_editable_delegate_set_property (object, prop_id: property_id, value, pspec))
391 return;
392
393 switch (property_id)
394 {
395 case PROP_MODEL:
396 suggestion_entry_set_model (self, model: g_value_get_object (value));
397 break;
398
399 case PROP_FACTORY:
400 suggestion_entry_set_factory (self, factory: g_value_get_object (value));
401 break;
402
403 case PROP_EXPRESSION:
404 suggestion_entry_set_expression (self, expression: gtk_value_get_expression (value));
405 break;
406
407 case PROP_PLACEHOLDER_TEXT:
408 gtk_text_set_placeholder_text (GTK_TEXT (self->entry), text: g_value_get_string (value));
409 break;
410
411 case PROP_POPUP_VISIBLE:
412 suggestion_entry_set_popup_visible (self, visible: g_value_get_boolean (value));
413 break;
414
415 case PROP_USE_FILTER:
416 suggestion_entry_set_use_filter (self, use_ilter: g_value_get_boolean (value));
417 break;
418
419 case PROP_SHOW_ARROW:
420 suggestion_entry_set_show_arrow (self, show_arrow: g_value_get_boolean (value));
421 break;
422
423 default:
424 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
425 break;
426 }
427}
428
429static void
430suggestion_entry_measure (GtkWidget *widget,
431 GtkOrientation orientation,
432 int for_size,
433 int *minimum,
434 int *natural,
435 int *minimum_baseline,
436 int *natural_baseline)
437{
438 SuggestionEntry *self = SUGGESTION_ENTRY (widget);
439 int arrow_min = 0, arrow_nat = 0;
440
441 gtk_widget_measure (widget: self->entry, orientation, for_size,
442 minimum, natural,
443 minimum_baseline, natural_baseline);
444
445 if (self->arrow && gtk_widget_get_visible (widget: self->arrow))
446 gtk_widget_measure (widget: self->arrow, orientation, for_size,
447 minimum: &arrow_min, natural: &arrow_nat,
448 NULL, NULL);
449}
450
451static void
452suggestion_entry_size_allocate (GtkWidget *widget,
453 int width,
454 int height,
455 int baseline)
456{
457 SuggestionEntry *self = SUGGESTION_ENTRY (widget);
458 int arrow_min = 0, arrow_nat = 0;
459
460 if (self->arrow && gtk_widget_get_visible (widget: self->arrow))
461 gtk_widget_measure (widget: self->arrow, orientation: GTK_ORIENTATION_HORIZONTAL, for_size: -1,
462 minimum: &arrow_min, natural: &arrow_nat,
463 NULL, NULL);
464
465 gtk_widget_size_allocate (widget: self->entry,
466 allocation: &(GtkAllocation) { 0, 0, width - arrow_nat, height },
467 baseline);
468
469 if (self->arrow && gtk_widget_get_visible (widget: self->arrow))
470 gtk_widget_size_allocate (widget: self->arrow,
471 allocation: &(GtkAllocation) { width - arrow_nat, 0, arrow_nat, height },
472 baseline);
473
474 gtk_widget_set_size_request (widget: self->popup, width: gtk_widget_get_allocated_width (GTK_WIDGET (self)), height: -1);
475 gtk_widget_queue_resize (widget: self->popup);
476
477 gtk_popover_present (GTK_POPOVER (self->popup));
478}
479
480static gboolean
481suggestion_entry_grab_focus (GtkWidget *widget)
482{
483 SuggestionEntry *self = SUGGESTION_ENTRY (widget);
484
485 return gtk_widget_grab_focus (widget: self->entry);
486}
487
488static void
489suggestion_entry_class_init (SuggestionEntryClass *klass)
490{
491 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
492 GObjectClass *object_class = G_OBJECT_CLASS (klass);
493
494 object_class->dispose = suggestion_entry_dispose;
495 object_class->get_property = suggestion_entry_get_property;
496 object_class->set_property = suggestion_entry_set_property;
497
498 widget_class->measure = suggestion_entry_measure;
499 widget_class->size_allocate = suggestion_entry_size_allocate;
500 widget_class->grab_focus = suggestion_entry_grab_focus;
501
502 properties[PROP_MODEL] =
503 g_param_spec_object (name: "model",
504 nick: "Model",
505 blurb: "Model for the displayed items",
506 G_TYPE_LIST_MODEL,
507 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
508
509 properties[PROP_FACTORY] =
510 g_param_spec_object (name: "factory",
511 nick: "Factory",
512 blurb: "Factory for populating list items",
513 GTK_TYPE_LIST_ITEM_FACTORY,
514 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
515
516 properties[PROP_EXPRESSION] =
517 gtk_param_spec_expression (name: "expression",
518 nick: "Expression",
519 blurb: "Expression to determine strings to search for",
520 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
521
522 properties[PROP_PLACEHOLDER_TEXT] =
523 g_param_spec_string (name: "placeholder-text",
524 nick: "Placeholder text",
525 blurb: "Show text in the entry when it’s empty and unfocused",
526 NULL,
527 flags: G_PARAM_READWRITE);
528
529 properties[PROP_POPUP_VISIBLE] =
530 g_param_spec_boolean (name: "popup-visible",
531 nick: "Popup visible",
532 blurb: "Whether the popup with suggestions is currently visible",
533 FALSE,
534 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
535
536 properties[PROP_USE_FILTER] =
537 g_param_spec_boolean (name: "use-filter",
538 nick: "Use filter",
539 blurb: "Whether to filter the list for matches",
540 TRUE,
541 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
542
543 properties[PROP_SHOW_ARROW] =
544 g_param_spec_boolean (name: "show-arrow",
545 nick: "Show arrow",
546 blurb: "Whether to show a clickable arrow for presenting the popup",
547 FALSE,
548 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
549
550 g_object_class_install_properties (oclass: object_class, n_pspecs: N_PROPERTIES, pspecs: properties);
551 gtk_editable_install_properties (object_class, first_prop: N_PROPERTIES);
552
553 gtk_widget_class_install_property_action (widget_class, action_name: "popup.show", property_name: "popup-visible");
554
555 gtk_widget_class_add_binding_action (widget_class,
556 GDK_KEY_Down, mods: GDK_ALT_MASK,
557 action_name: "popup.show", NULL);
558
559 gtk_widget_class_set_css_name (widget_class, name: "entry");
560}
561
562static void
563setup_item (GtkSignalListItemFactory *factory,
564 GtkListItem *list_item,
565 gpointer data)
566{
567 GtkWidget *label;
568
569 label = gtk_label_new (NULL);
570 gtk_label_set_xalign (GTK_LABEL (label), xalign: 0.0);
571 gtk_list_item_set_child (self: list_item, child: label);
572}
573
574static void
575bind_item (GtkSignalListItemFactory *factory,
576 GtkListItem *list_item,
577 gpointer data)
578{
579 gpointer item;
580 GtkWidget *label;
581 GValue value = G_VALUE_INIT;
582
583 item = gtk_list_item_get_item (self: list_item);
584 label = gtk_list_item_get_child (self: list_item);
585
586 gtk_label_set_label (GTK_LABEL (label), str: match_object_get_string (MATCH_OBJECT (item)));
587 g_value_unset (value: &value);
588}
589
590static void
591suggestion_entry_set_popup_visible (SuggestionEntry *self,
592 gboolean visible)
593{
594 if (gtk_widget_get_visible (widget: self->popup) == visible)
595 return;
596
597 if (g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->selection)) == 0)
598 return;
599
600 if (visible)
601 {
602 if (!gtk_widget_has_focus (widget: self->entry))
603 gtk_text_grab_focus_without_selecting (GTK_TEXT (self->entry));
604
605 gtk_single_selection_set_selected (self: self->selection, GTK_INVALID_LIST_POSITION);
606 gtk_popover_popup (GTK_POPOVER (self->popup));
607 }
608 else
609 {
610 gtk_popover_popdown (GTK_POPOVER (self->popup));
611 }
612
613 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_POPUP_VISIBLE]);
614}
615
616static void update_map (SuggestionEntry *self);
617
618static gboolean
619text_changed_idle (gpointer data)
620{
621 SuggestionEntry *self = data;
622 const char *text;
623 guint matches;
624
625 if (!self->map_model)
626 return G_SOURCE_REMOVE;
627
628 text = gtk_editable_get_text (GTK_EDITABLE (self->entry));
629
630 g_free (mem: self->search);
631 self->search = g_strdup (str: text);
632
633 update_map (self);
634
635 matches = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->selection));
636
637 suggestion_entry_set_popup_visible (self, visible: matches > 0);
638
639 return G_SOURCE_REMOVE;
640}
641
642static void
643text_changed (GtkEditable *editable,
644 GParamSpec *pspec,
645 SuggestionEntry *self)
646{
647 /* We need to defer to an idle since GtkText sets selection bounds
648 * after notify::text
649 */
650 g_idle_add (function: text_changed_idle, data: self);
651}
652
653static void
654accept_current_selection (SuggestionEntry *self)
655{
656 gpointer item;
657
658 item = gtk_single_selection_get_selected_item (self: self->selection);
659 if (!item)
660 return;
661
662 g_signal_handler_block (instance: self->entry, handler_id: self->changed_id);
663
664 gtk_editable_set_text (GTK_EDITABLE (self->entry),
665 text: match_object_get_string (MATCH_OBJECT (item)));
666
667 gtk_editable_set_position (GTK_EDITABLE (self->entry), position: -1);
668
669 g_signal_handler_unblock (instance: self->entry, handler_id: self->changed_id);
670}
671
672static void
673suggestion_entry_row_activated (GtkListView *listview,
674 guint position,
675 SuggestionEntry *self)
676{
677 suggestion_entry_set_popup_visible (self, FALSE);
678 accept_current_selection (self);
679}
680
681static inline gboolean
682keyval_is_cursor_move (guint keyval)
683{
684 if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up)
685 return TRUE;
686
687 if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down)
688 return TRUE;
689
690 if (keyval == GDK_KEY_Page_Up || keyval == GDK_KEY_Page_Down)
691 return TRUE;
692
693 return FALSE;
694}
695
696#define PAGE_STEP 10
697
698static gboolean
699suggestion_entry_key_pressed (GtkEventControllerKey *controller,
700 guint keyval,
701 guint keycode,
702 GdkModifierType state,
703 SuggestionEntry *self)
704{
705 guint matches;
706 guint selected;
707
708 if (state & (GDK_SHIFT_MASK | GDK_ALT_MASK | GDK_CONTROL_MASK))
709 return FALSE;
710
711 if (keyval == GDK_KEY_Return ||
712 keyval == GDK_KEY_KP_Enter ||
713 keyval == GDK_KEY_ISO_Enter)
714 {
715 suggestion_entry_set_popup_visible (self, FALSE);
716 accept_current_selection (self);
717 g_free (mem: self->search);
718 self->search = g_strdup (str: gtk_editable_get_text (GTK_EDITABLE (self->entry)));
719 update_map (self);
720
721 return TRUE;
722 }
723 else if (keyval == GDK_KEY_Escape)
724 {
725 if (gtk_widget_get_mapped (widget: self->popup))
726 {
727 suggestion_entry_set_popup_visible (self, FALSE);
728
729 g_signal_handler_block (instance: self->entry, handler_id: self->changed_id);
730
731 gtk_editable_set_text (GTK_EDITABLE (self->entry), text: self->search ? self->search : "");
732
733 gtk_editable_set_position (GTK_EDITABLE (self->entry), position: -1);
734
735 g_signal_handler_unblock (instance: self->entry, handler_id: self->changed_id);
736 return TRUE;
737 }
738 }
739 else if (keyval == GDK_KEY_Right ||
740 keyval == GDK_KEY_KP_Right)
741 {
742 gtk_editable_set_position (GTK_EDITABLE (self->entry), position: -1);
743 return TRUE;
744 }
745 else if (keyval == GDK_KEY_Left ||
746 keyval == GDK_KEY_KP_Left)
747 {
748 return FALSE;
749 }
750 else if (keyval == GDK_KEY_Tab ||
751 keyval == GDK_KEY_KP_Tab ||
752 keyval == GDK_KEY_ISO_Left_Tab)
753 {
754 suggestion_entry_set_popup_visible (self, FALSE);
755 return FALSE; /* don't disrupt normal focus handling */
756 }
757
758 matches = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->selection));
759 selected = gtk_single_selection_get_selected (self: self->selection);
760
761 if (keyval_is_cursor_move (keyval))
762 {
763 if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up)
764 {
765 if (selected == 0)
766 selected = GTK_INVALID_LIST_POSITION;
767 else if (selected == GTK_INVALID_LIST_POSITION)
768 selected = matches - 1;
769 else
770 selected--;
771 }
772 else if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down)
773 {
774 if (selected == matches - 1)
775 selected = GTK_INVALID_LIST_POSITION;
776 else if (selected == GTK_INVALID_LIST_POSITION)
777 selected = 0;
778 else
779 selected++;
780 }
781 else if (keyval == GDK_KEY_Page_Up)
782 {
783 if (selected == 0)
784 selected = GTK_INVALID_LIST_POSITION;
785 else if (selected == GTK_INVALID_LIST_POSITION)
786 selected = matches - 1;
787 else if (selected >= PAGE_STEP)
788 selected -= PAGE_STEP;
789 else
790 selected = 0;
791 }
792 else if (keyval == GDK_KEY_Page_Down)
793 {
794 if (selected == matches - 1)
795 selected = GTK_INVALID_LIST_POSITION;
796 else if (selected == GTK_INVALID_LIST_POSITION)
797 selected = 0;
798 else if (selected + PAGE_STEP < matches)
799 selected += PAGE_STEP;
800 else
801 selected = matches - 1;
802 }
803
804 gtk_single_selection_set_selected (self: self->selection, position: selected);
805 return TRUE;
806 }
807
808 return FALSE;
809}
810
811static void
812suggestion_entry_focus_out (GtkEventController *controller,
813 SuggestionEntry *self)
814{
815 if (!gtk_widget_get_mapped (widget: self->popup))
816 return;
817
818 suggestion_entry_set_popup_visible (self, FALSE);
819 accept_current_selection (self);
820}
821
822static void
823set_default_factory (SuggestionEntry *self)
824{
825 GtkListItemFactory *factory;
826
827 factory = gtk_signal_list_item_factory_new ();
828
829 g_signal_connect (factory, "setup", G_CALLBACK (setup_item), self);
830 g_signal_connect (factory, "bind", G_CALLBACK (bind_item), self);
831
832 suggestion_entry_set_factory (self, factory);
833
834 g_object_unref (object: factory);
835}
836
837static void default_match_func (MatchObject *object,
838 const char *search,
839 gpointer data);
840
841static void
842suggestion_entry_init (SuggestionEntry *self)
843{
844 GtkWidget *sw;
845 GtkEventController *controller;
846
847 if (!g_object_get_data (G_OBJECT (gdk_display_get_default ()), key: "suggestion-style"))
848 {
849 GtkCssProvider *provider;
850
851 provider = gtk_css_provider_new ();
852 gtk_css_provider_load_from_resource (css_provider: provider, resource_path: "/dropdown/suggestionentry.css");
853 gtk_style_context_add_provider_for_display (display: gdk_display_get_default (),
854 GTK_STYLE_PROVIDER (provider),
855 priority: 800);
856 g_object_set_data (G_OBJECT (gdk_display_get_default ()), key: "suggestion-style", data: provider);
857 g_object_unref (object: provider);
858 }
859
860 self->use_filter = TRUE;
861 self->show_arrow = FALSE;
862
863 self->match_func = default_match_func;
864 self->match_data = NULL;
865 self->destroy = NULL;
866
867 gtk_widget_add_css_class (GTK_WIDGET (self), css_class: "suggestion");
868
869 self->entry = gtk_text_new ();
870 gtk_widget_set_parent (widget: self->entry, GTK_WIDGET (self));
871 gtk_widget_set_hexpand (widget: self->entry, TRUE);
872 gtk_editable_init_delegate (GTK_EDITABLE (self));
873 self->changed_id = g_signal_connect (self->entry, "notify::text", G_CALLBACK (text_changed), self);
874
875 self->popup = gtk_popover_new ();
876 gtk_popover_set_position (GTK_POPOVER (self->popup), position: GTK_POS_BOTTOM);
877 gtk_popover_set_autohide (GTK_POPOVER (self->popup), FALSE);
878 gtk_popover_set_has_arrow (GTK_POPOVER (self->popup), FALSE);
879 gtk_widget_set_halign (widget: self->popup, align: GTK_ALIGN_START);
880 gtk_widget_add_css_class (widget: self->popup, css_class: "menu");
881 gtk_widget_set_parent (widget: self->popup, GTK_WIDGET (self));
882 sw = gtk_scrolled_window_new ();
883 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
884 hscrollbar_policy: GTK_POLICY_NEVER,
885 vscrollbar_policy: GTK_POLICY_AUTOMATIC);
886 gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (sw), height: 400);
887 gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (sw), TRUE);
888
889 gtk_popover_set_child (GTK_POPOVER (self->popup), child: sw);
890 self->list = gtk_list_view_new (NULL, NULL);
891 gtk_list_view_set_single_click_activate (GTK_LIST_VIEW (self->list), TRUE);
892 g_signal_connect (self->list, "activate",
893 G_CALLBACK (suggestion_entry_row_activated), self);
894 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), child: self->list);
895
896 set_default_factory (self);
897
898 controller = gtk_event_controller_key_new ();
899 gtk_event_controller_set_name (controller, name: "gtk-suggestion-entry");
900 g_signal_connect (controller, "key-pressed",
901 G_CALLBACK (suggestion_entry_key_pressed), self);
902 gtk_widget_add_controller (widget: self->entry, controller);
903
904 controller = gtk_event_controller_focus_new ();
905 gtk_event_controller_set_name (controller, name: "gtk-suggestion-entry");
906 g_signal_connect (controller, "leave",
907 G_CALLBACK (suggestion_entry_focus_out), self);
908 gtk_widget_add_controller (widget: self->entry, controller);
909}
910
911GtkWidget *
912suggestion_entry_new (void)
913{
914 return g_object_new (SUGGESTION_TYPE_ENTRY, NULL);
915}
916
917GListModel *
918suggestion_entry_get_model (SuggestionEntry *self)
919{
920 g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), NULL);
921
922 return self->model;
923}
924
925static void
926selection_changed (GtkSingleSelection *selection,
927 GParamSpec *pspec,
928 SuggestionEntry *self)
929{
930 accept_current_selection (self);
931}
932
933static gboolean
934filter_func (gpointer item, gpointer user_data)
935{
936 SuggestionEntry *self = SUGGESTION_ENTRY (user_data);
937 guint min_score;
938
939 if (self->use_filter)
940 min_score = 1;
941 else
942 min_score = 0;
943
944 return match_object_get_score (MATCH_OBJECT (item)) >= min_score;
945}
946
947static void
948default_match_func (MatchObject *object,
949 const char *search,
950 gpointer data)
951{
952 char *tmp1, *tmp2, *tmp3, *tmp4;
953
954 tmp1 = g_utf8_normalize (str: match_object_get_string (object), len: -1, mode: G_NORMALIZE_ALL);
955 tmp2 = g_utf8_casefold (str: tmp1, len: -1);
956
957 tmp3 = g_utf8_normalize (str: search, len: -1, mode: G_NORMALIZE_ALL);
958 tmp4 = g_utf8_casefold (str: tmp3, len: -1);
959
960 if (g_str_has_prefix (str: tmp2, prefix: tmp4))
961 match_object_set_match (object, start: 0, end: g_utf8_strlen (p: search, max: -1), score: 1);
962 else
963 match_object_set_match (object, start: 0, end: 0, score: 0);
964
965 g_free (mem: tmp1);
966 g_free (mem: tmp2);
967 g_free (mem: tmp3);
968 g_free (mem: tmp4);
969}
970
971static gpointer
972map_func (gpointer item, gpointer user_data)
973{
974 SuggestionEntry *self = SUGGESTION_ENTRY (user_data);
975 GValue value = G_VALUE_INIT;
976 gpointer obj;
977
978 if (self->expression)
979 {
980 gtk_expression_evaluate (self: self->expression, this_: item, value: &value);
981 }
982 else if (GTK_IS_STRING_OBJECT (ptr: item))
983 {
984 g_object_get_property (G_OBJECT (item), property_name: "string", value: &value);
985 }
986 else
987 {
988 g_critical ("Either SuggestionEntry:expression must be set "
989 "or SuggestionEntry:model must be a GtkStringList");
990 g_value_set_string (value: &value, v_string: "No value");
991 }
992
993 obj = match_object_new (item, string: g_value_get_string (value: &value));
994
995 g_value_unset (value: &value);
996
997 if (self->search && self->search[0])
998 self->match_func (obj, self->search, self->match_data);
999 else
1000 match_object_set_match (object: obj, start: 0, end: 0, score: 1);
1001
1002 return obj;
1003}
1004
1005static void
1006update_map (SuggestionEntry *self)
1007{
1008 gtk_map_list_model_set_map_func (self: self->map_model, map_func, user_data: self, NULL);
1009}
1010
1011void
1012suggestion_entry_set_model (SuggestionEntry *self,
1013 GListModel *model)
1014{
1015 g_return_if_fail (SUGGESTION_IS_ENTRY (self));
1016 g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
1017
1018 if (!g_set_object (&self->model, model))
1019 return;
1020
1021 if (self->selection)
1022 g_signal_handlers_disconnect_by_func (self->selection, selection_changed, self);
1023
1024 if (model == NULL)
1025 {
1026 gtk_list_view_set_model (GTK_LIST_VIEW (self->list), NULL);
1027 g_clear_object (&self->selection);
1028 g_clear_object (&self->map_model);
1029 g_clear_object (&self->filter);
1030 }
1031 else
1032 {
1033 GtkMapListModel *map_model;
1034 GtkFilterListModel *filter_model;
1035 GtkFilter *filter;
1036 GtkSortListModel *sort_model;
1037 GtkSingleSelection *selection;
1038 GtkSorter *sorter;
1039
1040 map_model = gtk_map_list_model_new (g_object_ref (model), NULL, NULL, NULL);
1041 g_set_object (&self->map_model, map_model);
1042
1043 update_map (self);
1044
1045 filter = GTK_FILTER (ptr: gtk_custom_filter_new (match_func: filter_func, user_data: self, NULL));
1046 filter_model = gtk_filter_list_model_new (model: G_LIST_MODEL (ptr: self->map_model), filter);
1047 g_set_object (&self->filter, filter);
1048
1049 sorter = GTK_SORTER (ptr: gtk_numeric_sorter_new (expression: gtk_property_expression_new (MATCH_TYPE_OBJECT, NULL, property_name: "score")));
1050 gtk_numeric_sorter_set_sort_order (self: GTK_NUMERIC_SORTER (ptr: sorter), sort_order: GTK_SORT_DESCENDING);
1051 sort_model = gtk_sort_list_model_new (model: G_LIST_MODEL (ptr: filter_model), sorter);
1052
1053 update_map (self);
1054
1055 selection = gtk_single_selection_new (model: G_LIST_MODEL (ptr: sort_model));
1056 gtk_single_selection_set_autoselect (self: selection, FALSE);
1057 gtk_single_selection_set_can_unselect (self: selection, TRUE);
1058 gtk_single_selection_set_selected (self: selection, GTK_INVALID_LIST_POSITION);
1059 g_set_object (&self->selection, selection);
1060 gtk_list_view_set_model (GTK_LIST_VIEW (self->list), model: GTK_SELECTION_MODEL (ptr: selection));
1061 g_object_unref (object: selection);
1062 }
1063
1064 if (self->selection)
1065 {
1066 g_signal_connect (self->selection, "notify::selected",
1067 G_CALLBACK (selection_changed), self);
1068 selection_changed (selection: self->selection, NULL, self);
1069 }
1070
1071 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_MODEL]);
1072}
1073
1074GtkListItemFactory *
1075suggestion_entry_get_factory (SuggestionEntry *self)
1076{
1077 g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), NULL);
1078
1079 return self->factory;
1080}
1081
1082void
1083suggestion_entry_set_factory (SuggestionEntry *self,
1084 GtkListItemFactory *factory)
1085{
1086 g_return_if_fail (SUGGESTION_IS_ENTRY (self));
1087 g_return_if_fail (factory == NULL || GTK_LIST_ITEM_FACTORY (factory));
1088
1089 if (!g_set_object (&self->factory, factory))
1090 return;
1091
1092 if (self->list)
1093 gtk_list_view_set_factory (GTK_LIST_VIEW (self->list), factory);
1094
1095 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_FACTORY]);
1096}
1097
1098void
1099suggestion_entry_set_expression (SuggestionEntry *self,
1100 GtkExpression *expression)
1101{
1102 g_return_if_fail (SUGGESTION_IS_ENTRY (self));
1103 g_return_if_fail (expression == NULL ||
1104 gtk_expression_get_value_type (expression) == G_TYPE_STRING);
1105
1106 if (self->expression == expression)
1107 return;
1108
1109 if (self->expression)
1110 gtk_expression_unref (self: self->expression);
1111
1112 self->expression = expression;
1113
1114 if (self->expression)
1115 gtk_expression_ref (self: self->expression);
1116
1117 update_map (self);
1118
1119 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_EXPRESSION]);
1120}
1121
1122GtkExpression *
1123suggestion_entry_get_expression (SuggestionEntry *self)
1124{
1125 g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), NULL);
1126
1127 return self->expression;
1128}
1129
1130void
1131suggestion_entry_set_use_filter (SuggestionEntry *self,
1132 gboolean use_filter)
1133{
1134 g_return_if_fail (SUGGESTION_IS_ENTRY (self));
1135
1136 if (self->use_filter == use_filter)
1137 return;
1138
1139 self->use_filter = use_filter;
1140
1141 gtk_filter_changed (self: self->filter, change: GTK_FILTER_CHANGE_DIFFERENT);
1142
1143 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_USE_FILTER]);
1144}
1145
1146gboolean
1147suggestion_entry_get_use_filter (SuggestionEntry *self)
1148{
1149 g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), TRUE);
1150
1151 return self->use_filter;
1152}
1153
1154static void
1155suggestion_entry_arrow_clicked (SuggestionEntry *self)
1156{
1157 gboolean visible;
1158
1159 visible = gtk_widget_get_visible (widget: self->popup);
1160 suggestion_entry_set_popup_visible (self, visible: !visible);
1161}
1162
1163void
1164suggestion_entry_set_show_arrow (SuggestionEntry *self,
1165 gboolean show_arrow)
1166{
1167 g_return_if_fail (SUGGESTION_IS_ENTRY (self));
1168
1169 if (self->show_arrow == show_arrow)
1170 return;
1171
1172 self->show_arrow = show_arrow;
1173
1174 if (show_arrow)
1175 {
1176 GtkGesture *press;
1177
1178 self->arrow = gtk_image_new_from_icon_name (icon_name: "pan-down-symbolic");
1179 gtk_widget_set_tooltip_text (widget: self->arrow, text: "Show suggestions");
1180 gtk_widget_set_parent (widget: self->arrow, GTK_WIDGET (self));
1181
1182 press = gtk_gesture_click_new ();
1183 g_signal_connect_swapped (press, "released",
1184 G_CALLBACK (suggestion_entry_arrow_clicked), self);
1185 gtk_widget_add_controller (widget: self->arrow, GTK_EVENT_CONTROLLER (press));
1186
1187 }
1188 else
1189 {
1190 g_clear_pointer (&self->arrow, gtk_widget_unparent);
1191 }
1192
1193 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SHOW_ARROW]);
1194}
1195
1196gboolean
1197suggestion_entry_get_show_arrow (SuggestionEntry *self)
1198{
1199 g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), FALSE);
1200
1201 return self->show_arrow;
1202}
1203
1204void
1205suggestion_entry_set_match_func (SuggestionEntry *self,
1206 SuggestionEntryMatchFunc match_func,
1207 gpointer user_data,
1208 GDestroyNotify destroy)
1209{
1210 if (self->destroy)
1211 self->destroy (self->match_data);
1212 self->match_func = match_func;
1213 self->match_data = user_data;
1214 self->destroy = destroy;
1215}
1216

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