1 | /* gtkentrycompletion.c |
2 | * Copyright (C) 2003 Kristian Rietveld <kris@gtk.org> |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Library General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2 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 | * Library General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Library General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | */ |
17 | |
18 | /** |
19 | * GtkEntryCompletion: |
20 | * |
21 | * `GtkEntryCompletion` is an auxiliary object to provide completion functionality |
22 | * for `GtkEntry`. |
23 | * |
24 | * It implements the [iface@Gtk.CellLayout] interface, to allow the user |
25 | * to add extra cells to the `GtkTreeView` with completion matches. |
26 | * |
27 | * “Completion functionality” means that when the user modifies the text |
28 | * in the entry, `GtkEntryCompletion` checks which rows in the model match |
29 | * the current content of the entry, and displays a list of matches. |
30 | * By default, the matching is done by comparing the entry text |
31 | * case-insensitively against the text column of the model (see |
32 | * [method@Gtk.EntryCompletion.set_text_column]), but this can be overridden |
33 | * with a custom match function (see [method@Gtk.EntryCompletion.set_match_func]). |
34 | * |
35 | * When the user selects a completion, the content of the entry is |
36 | * updated. By default, the content of the entry is replaced by the |
37 | * text column of the model, but this can be overridden by connecting |
38 | * to the [signal@Gtk.EntryCompletion::match-selected] signal and updating the |
39 | * entry in the signal handler. Note that you should return %TRUE from |
40 | * the signal handler to suppress the default behaviour. |
41 | * |
42 | * To add completion functionality to an entry, use |
43 | * [method@Gtk.Entry.set_completion]. |
44 | * |
45 | * `GtkEntryCompletion` uses a [class@Gtk.TreeModelFilter] model to |
46 | * represent the subset of the entire model that is currently matching. |
47 | * While the `GtkEntryCompletion` signals |
48 | * [signal@Gtk.EntryCompletion::match-selected] and |
49 | * [signal@Gtk.EntryCompletion::cursor-on-match] take the original model |
50 | * and an iter pointing to that model as arguments, other callbacks and |
51 | * signals (such as `GtkCellLayoutDataFunc` or |
52 | * [signal@Gtk.CellArea::apply-attributes)] |
53 | * will generally take the filter model as argument. As long as you are |
54 | * only calling [method@Gtk.TreeModel.get], this will make no difference to |
55 | * you. If for some reason, you need the original model, use |
56 | * [method@Gtk.TreeModelFilter.get_model]. Don’t forget to use |
57 | * [method@Gtk.TreeModelFilter.convert_iter_to_child_iter] to obtain a |
58 | * matching iter. |
59 | */ |
60 | |
61 | #include "config.h" |
62 | |
63 | #include "gtkentrycompletion.h" |
64 | |
65 | #include "gtkentryprivate.h" |
66 | #include "gtktextprivate.h" |
67 | #include "gtkcelllayout.h" |
68 | #include "gtkcellareabox.h" |
69 | |
70 | #include "gtkintl.h" |
71 | #include "gtkcellrenderertext.h" |
72 | #include "gtktreeselection.h" |
73 | #include "gtktreeview.h" |
74 | #include "gtkscrolledwindow.h" |
75 | #include "gtksizerequest.h" |
76 | #include "gtkbox.h" |
77 | #include "gtkpopover.h" |
78 | #include "gtkentry.h" |
79 | #include "gtkmain.h" |
80 | #include "gtkmarshalers.h" |
81 | #include "gtkeventcontrollerfocus.h" |
82 | #include "gtkeventcontrollerkey.h" |
83 | #include "gtkgestureclick.h" |
84 | |
85 | #include "gtkprivate.h" |
86 | #include "gtkwindowprivate.h" |
87 | #include "gtkwidgetprivate.h" |
88 | #include "gtknative.h" |
89 | |
90 | #include <string.h> |
91 | |
92 | #define PAGE_STEP 14 |
93 | #define COMPLETION_TIMEOUT 100 |
94 | |
95 | /* signals */ |
96 | enum |
97 | { |
98 | INSERT_PREFIX, |
99 | MATCH_SELECTED, |
100 | CURSOR_ON_MATCH, |
101 | NO_MATCHES, |
102 | LAST_SIGNAL |
103 | }; |
104 | |
105 | /* properties */ |
106 | enum |
107 | { |
108 | PROP_0, |
109 | PROP_MODEL, |
110 | PROP_MINIMUM_KEY_LENGTH, |
111 | PROP_TEXT_COLUMN, |
112 | PROP_INLINE_COMPLETION, |
113 | , |
114 | , |
115 | , |
116 | PROP_INLINE_SELECTION, |
117 | PROP_CELL_AREA, |
118 | NUM_PROPERTIES |
119 | }; |
120 | |
121 | |
122 | static void gtk_entry_completion_cell_layout_init (GtkCellLayoutIface *iface); |
123 | static GtkCellArea* gtk_entry_completion_get_area (GtkCellLayout *cell_layout); |
124 | |
125 | static void gtk_entry_completion_constructed (GObject *object); |
126 | static void gtk_entry_completion_set_property (GObject *object, |
127 | guint prop_id, |
128 | const GValue *value, |
129 | GParamSpec *pspec); |
130 | static void gtk_entry_completion_get_property (GObject *object, |
131 | guint prop_id, |
132 | GValue *value, |
133 | GParamSpec *pspec); |
134 | static void gtk_entry_completion_finalize (GObject *object); |
135 | static void gtk_entry_completion_dispose (GObject *object); |
136 | |
137 | static gboolean gtk_entry_completion_visible_func (GtkTreeModel *model, |
138 | GtkTreeIter *iter, |
139 | gpointer data); |
140 | static void gtk_entry_completion_list_activated (GtkTreeView *treeview, |
141 | GtkTreePath *path, |
142 | GtkTreeViewColumn *column, |
143 | gpointer user_data); |
144 | static void gtk_entry_completion_selection_changed (GtkTreeSelection *selection, |
145 | gpointer data); |
146 | |
147 | static gboolean gtk_entry_completion_match_selected (GtkEntryCompletion *completion, |
148 | GtkTreeModel *model, |
149 | GtkTreeIter *iter); |
150 | static gboolean gtk_entry_completion_real_insert_prefix (GtkEntryCompletion *completion, |
151 | const char *prefix); |
152 | static gboolean gtk_entry_completion_cursor_on_match (GtkEntryCompletion *completion, |
153 | GtkTreeModel *model, |
154 | GtkTreeIter *iter); |
155 | static gboolean gtk_entry_completion_insert_completion (GtkEntryCompletion *completion, |
156 | GtkTreeModel *model, |
157 | GtkTreeIter *iter); |
158 | static void gtk_entry_completion_insert_completion_text (GtkEntryCompletion *completion, |
159 | const char *text); |
160 | static void connect_completion_signals (GtkEntryCompletion *completion); |
161 | static void disconnect_completion_signals (GtkEntryCompletion *completion); |
162 | |
163 | |
164 | static GParamSpec *entry_completion_props[NUM_PROPERTIES] = { NULL, }; |
165 | |
166 | static guint entry_completion_signals[LAST_SIGNAL] = { 0 }; |
167 | |
168 | /* GtkBuildable */ |
169 | static void gtk_entry_completion_buildable_init (GtkBuildableIface *iface); |
170 | |
171 | G_DEFINE_TYPE_WITH_CODE (GtkEntryCompletion, gtk_entry_completion, G_TYPE_OBJECT, |
172 | G_IMPLEMENT_INTERFACE (GTK_TYPE_CELL_LAYOUT, |
173 | gtk_entry_completion_cell_layout_init) |
174 | G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, |
175 | gtk_entry_completion_buildable_init)) |
176 | |
177 | |
178 | static void |
179 | gtk_entry_completion_class_init (GtkEntryCompletionClass *klass) |
180 | { |
181 | GObjectClass *object_class; |
182 | |
183 | object_class = (GObjectClass *)klass; |
184 | |
185 | object_class->constructed = gtk_entry_completion_constructed; |
186 | object_class->set_property = gtk_entry_completion_set_property; |
187 | object_class->get_property = gtk_entry_completion_get_property; |
188 | object_class->dispose = gtk_entry_completion_dispose; |
189 | object_class->finalize = gtk_entry_completion_finalize; |
190 | |
191 | klass->match_selected = gtk_entry_completion_match_selected; |
192 | klass->insert_prefix = gtk_entry_completion_real_insert_prefix; |
193 | klass->cursor_on_match = gtk_entry_completion_cursor_on_match; |
194 | klass->no_matches = NULL; |
195 | |
196 | /** |
197 | * GtkEntryCompletion::insert-prefix: |
198 | * @widget: the object which received the signal |
199 | * @prefix: the common prefix of all possible completions |
200 | * |
201 | * Emitted when the inline autocompletion is triggered. |
202 | * |
203 | * The default behaviour is to make the entry display the |
204 | * whole prefix and select the newly inserted part. |
205 | * |
206 | * Applications may connect to this signal in order to insert only a |
207 | * smaller part of the @prefix into the entry - e.g. the entry used in |
208 | * the `GtkFileChooser` inserts only the part of the prefix up to the |
209 | * next '/'. |
210 | * |
211 | * Returns: %TRUE if the signal has been handled |
212 | */ |
213 | entry_completion_signals[INSERT_PREFIX] = |
214 | g_signal_new (I_("insert-prefix" ), |
215 | G_TYPE_FROM_CLASS (klass), |
216 | signal_flags: G_SIGNAL_RUN_LAST, |
217 | G_STRUCT_OFFSET (GtkEntryCompletionClass, insert_prefix), |
218 | accumulator: _gtk_boolean_handled_accumulator, NULL, |
219 | c_marshaller: _gtk_marshal_BOOLEAN__STRING, |
220 | G_TYPE_BOOLEAN, n_params: 1, |
221 | G_TYPE_STRING); |
222 | |
223 | /** |
224 | * GtkEntryCompletion::match-selected: |
225 | * @widget: the object which received the signal |
226 | * @model: the `GtkTreeModel` containing the matches |
227 | * @iter: a `GtkTreeIter` positioned at the selected match |
228 | * |
229 | * Emitted when a match from the list is selected. |
230 | * |
231 | * The default behaviour is to replace the contents of the |
232 | * entry with the contents of the text column in the row |
233 | * pointed to by @iter. |
234 | * |
235 | * Note that @model is the model that was passed to |
236 | * [method@Gtk.EntryCompletion.set_model]. |
237 | * |
238 | * Returns: %TRUE if the signal has been handled |
239 | */ |
240 | entry_completion_signals[MATCH_SELECTED] = |
241 | g_signal_new (I_("match-selected" ), |
242 | G_TYPE_FROM_CLASS (klass), |
243 | signal_flags: G_SIGNAL_RUN_LAST, |
244 | G_STRUCT_OFFSET (GtkEntryCompletionClass, match_selected), |
245 | accumulator: _gtk_boolean_handled_accumulator, NULL, |
246 | c_marshaller: _gtk_marshal_BOOLEAN__OBJECT_BOXED, |
247 | G_TYPE_BOOLEAN, n_params: 2, |
248 | GTK_TYPE_TREE_MODEL, |
249 | GTK_TYPE_TREE_ITER); |
250 | |
251 | /** |
252 | * GtkEntryCompletion::cursor-on-match: |
253 | * @widget: the object which received the signal |
254 | * @model: the `GtkTreeModel` containing the matches |
255 | * @iter: a `GtkTreeIter` positioned at the selected match |
256 | * |
257 | * Emitted when a match from the cursor is on a match of the list. |
258 | * |
259 | * The default behaviour is to replace the contents |
260 | * of the entry with the contents of the text column in the row |
261 | * pointed to by @iter. |
262 | * |
263 | * Note that @model is the model that was passed to |
264 | * [method@Gtk.EntryCompletion.set_model]. |
265 | * |
266 | * Returns: %TRUE if the signal has been handled |
267 | */ |
268 | entry_completion_signals[CURSOR_ON_MATCH] = |
269 | g_signal_new (I_("cursor-on-match" ), |
270 | G_TYPE_FROM_CLASS (klass), |
271 | signal_flags: G_SIGNAL_RUN_LAST, |
272 | G_STRUCT_OFFSET (GtkEntryCompletionClass, cursor_on_match), |
273 | accumulator: _gtk_boolean_handled_accumulator, NULL, |
274 | c_marshaller: _gtk_marshal_BOOLEAN__OBJECT_BOXED, |
275 | G_TYPE_BOOLEAN, n_params: 2, |
276 | GTK_TYPE_TREE_MODEL, |
277 | GTK_TYPE_TREE_ITER); |
278 | |
279 | /** |
280 | * GtkEntryCompletion::no-matches: |
281 | * @widget: the object which received the signal |
282 | * |
283 | * Emitted when the filter model has zero |
284 | * number of rows in completion_complete method. |
285 | * |
286 | * In other words when `GtkEntryCompletion` is out of suggestions. |
287 | */ |
288 | entry_completion_signals[NO_MATCHES] = |
289 | g_signal_new (I_("no-matches" ), |
290 | G_TYPE_FROM_CLASS (klass), |
291 | signal_flags: G_SIGNAL_RUN_LAST, |
292 | G_STRUCT_OFFSET (GtkEntryCompletionClass, no_matches), |
293 | NULL, NULL, |
294 | NULL, |
295 | G_TYPE_NONE, n_params: 0); |
296 | |
297 | entry_completion_props[PROP_MODEL] = |
298 | g_param_spec_object (name: "model" , |
299 | P_("Completion Model" ), |
300 | P_("The model to find matches in" ), |
301 | GTK_TYPE_TREE_MODEL, |
302 | GTK_PARAM_READWRITE); |
303 | |
304 | entry_completion_props[PROP_MINIMUM_KEY_LENGTH] = |
305 | g_param_spec_int (name: "minimum-key-length" , |
306 | P_("Minimum Key Length" ), |
307 | P_("Minimum length of the search key in order to look up matches" ), |
308 | minimum: 0, G_MAXINT, default_value: 1, |
309 | GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); |
310 | |
311 | /** |
312 | * GtkEntryCompletion:text-column: (attributes org.gtk.Property.get=gtk_entry_completion_get_text_column org.gtk.Property.set=gtk_entry_completion_set_text_column) |
313 | * |
314 | * The column of the model containing the strings. |
315 | * |
316 | * Note that the strings must be UTF-8. |
317 | */ |
318 | entry_completion_props[PROP_TEXT_COLUMN] = |
319 | g_param_spec_int (name: "text-column" , |
320 | P_("Text column" ), |
321 | P_("The column of the model containing the strings." ), |
322 | minimum: -1, G_MAXINT, default_value: -1, |
323 | GTK_PARAM_READWRITE); |
324 | |
325 | /** |
326 | * GtkEntryCompletion:inline-completion: (attributes org.gtk.Property.get=gtk_entry_completion_get_inline_completion org.gtk.Property.set=gtk_entry_completion_set_inline_completion) |
327 | * |
328 | * Determines whether the common prefix of the possible completions |
329 | * should be inserted automatically in the entry. |
330 | * |
331 | * Note that this requires text-column to be set, even if you are |
332 | * using a custom match function. |
333 | */ |
334 | entry_completion_props[PROP_INLINE_COMPLETION] = |
335 | g_param_spec_boolean (name: "inline-completion" , |
336 | P_("Inline completion" ), |
337 | P_("Whether the common prefix should be inserted automatically" ), |
338 | FALSE, |
339 | GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); |
340 | |
341 | /** |
342 | * GtkEntryCompletion:popup-completion: (attributes org.gtk.Property.get=gtk_entry_completion_get_popup_completion org.gtk.Property.set=gtk_entry_completion_set_popup_completion) |
343 | * |
344 | * Determines whether the possible completions should be |
345 | * shown in a popup window. |
346 | */ |
347 | entry_completion_props[PROP_POPUP_COMPLETION] = |
348 | g_param_spec_boolean (name: "popup-completion" , |
349 | P_("Popup completion" ), |
350 | P_("Whether the completions should be shown in a popup window" ), |
351 | TRUE, |
352 | GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); |
353 | |
354 | /** |
355 | * GtkEntryCompletion:popup-set-width: (attributes org.gtk.Property.get=gtk_entry_completion_get_popup_set_width org.gtk.Property.set=gtk_entry_completion_set_popup_set_width) |
356 | * |
357 | * Determines whether the completions popup window will be |
358 | * resized to the width of the entry. |
359 | */ |
360 | entry_completion_props[PROP_POPUP_SET_WIDTH] = |
361 | g_param_spec_boolean (name: "popup-set-width" , |
362 | P_("Popup set width" ), |
363 | P_("If TRUE, the popup window will have the same size as the entry" ), |
364 | TRUE, |
365 | GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); |
366 | |
367 | /** |
368 | * GtkEntryCompletion:popup-single-match: (attributes org.gtk.Property.get=gtk_entry_completion_get_popup_single_match org.gtk.Property.set=gtk_entry_completion_set_popup_single_match) |
369 | * |
370 | * Determines whether the completions popup window will shown |
371 | * for a single possible completion. |
372 | * |
373 | * You probably want to set this to %FALSE if you are using |
374 | * [property@Gtk.EntryCompletion:inline-completion]. |
375 | */ |
376 | entry_completion_props[PROP_POPUP_SINGLE_MATCH] = |
377 | g_param_spec_boolean (name: "popup-single-match" , |
378 | P_("Popup single match" ), |
379 | P_("If TRUE, the popup window will appear for a single match." ), |
380 | TRUE, |
381 | GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); |
382 | |
383 | /** |
384 | * GtkEntryCompletion:inline-selection: (attributes org.gtk.Property.get=gtk_entry_completion_get_inline_selection org.gtk.Property.set=gtk_entry_completion_set_inline_selection) |
385 | * |
386 | * Determines whether the possible completions on the popup |
387 | * will appear in the entry as you navigate through them. |
388 | */ |
389 | entry_completion_props[PROP_INLINE_SELECTION] = |
390 | g_param_spec_boolean (name: "inline-selection" , |
391 | P_("Inline selection" ), |
392 | P_("Your description here" ), |
393 | FALSE, |
394 | GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); |
395 | |
396 | /** |
397 | * GtkEntryCompletion:cell-area: |
398 | * |
399 | * The `GtkCellArea` used to layout cell renderers in the treeview column. |
400 | * |
401 | * If no area is specified when creating the entry completion with |
402 | * [ctor@Gtk.EntryCompletion.new_with_area], a horizontally oriented |
403 | * [class@Gtk.CellAreaBox] will be used. |
404 | */ |
405 | entry_completion_props[PROP_CELL_AREA] = |
406 | g_param_spec_object (name: "cell-area" , |
407 | P_("Cell Area" ), |
408 | P_("The GtkCellArea used to layout cells" ), |
409 | GTK_TYPE_CELL_AREA, |
410 | GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); |
411 | |
412 | g_object_class_install_properties (oclass: object_class, n_pspecs: NUM_PROPERTIES, pspecs: entry_completion_props); |
413 | } |
414 | |
415 | |
416 | static void |
417 | gtk_entry_completion_buildable_custom_tag_end (GtkBuildable *buildable, |
418 | GtkBuilder *builder, |
419 | GObject *child, |
420 | const char *tagname, |
421 | gpointer data) |
422 | { |
423 | /* Just ignore the boolean return from here */ |
424 | _gtk_cell_layout_buildable_custom_tag_end (buildable, builder, child, tagname, data); |
425 | } |
426 | |
427 | static void |
428 | gtk_entry_completion_buildable_init (GtkBuildableIface *iface) |
429 | { |
430 | iface->add_child = _gtk_cell_layout_buildable_add_child; |
431 | iface->custom_tag_start = _gtk_cell_layout_buildable_custom_tag_start; |
432 | iface->custom_tag_end = gtk_entry_completion_buildable_custom_tag_end; |
433 | } |
434 | |
435 | static void |
436 | gtk_entry_completion_cell_layout_init (GtkCellLayoutIface *iface) |
437 | { |
438 | iface->get_area = gtk_entry_completion_get_area; |
439 | } |
440 | |
441 | static void |
442 | gtk_entry_completion_init (GtkEntryCompletion *completion) |
443 | { |
444 | completion->minimum_key_length = 1; |
445 | completion->text_column = -1; |
446 | completion->has_completion = FALSE; |
447 | completion->inline_completion = FALSE; |
448 | completion->popup_completion = TRUE; |
449 | completion->popup_set_width = TRUE; |
450 | completion->popup_single_match = TRUE; |
451 | completion->inline_selection = FALSE; |
452 | |
453 | completion->filter_model = NULL; |
454 | } |
455 | |
456 | static gboolean |
457 | propagate_to_entry (GtkEventControllerKey *key, |
458 | guint keyval, |
459 | guint keycode, |
460 | GdkModifierType modifiers, |
461 | GtkEntryCompletion *completion) |
462 | { |
463 | GtkText *text = gtk_entry_get_text_widget (GTK_ENTRY (completion->entry)); |
464 | |
465 | return gtk_event_controller_key_forward (controller: key, GTK_WIDGET (text)); |
466 | } |
467 | |
468 | static void |
469 | gtk_entry_completion_constructed (GObject *object) |
470 | { |
471 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (object); |
472 | GtkTreeSelection *sel; |
473 | GtkEventController *controller; |
474 | |
475 | G_OBJECT_CLASS (gtk_entry_completion_parent_class)->constructed (object); |
476 | |
477 | if (!completion->cell_area) |
478 | { |
479 | completion->cell_area = gtk_cell_area_box_new (); |
480 | g_object_ref_sink (completion->cell_area); |
481 | } |
482 | |
483 | /* completions */ |
484 | completion->tree_view = gtk_tree_view_new (); |
485 | g_signal_connect (completion->tree_view, "row-activated" , |
486 | G_CALLBACK (gtk_entry_completion_list_activated), |
487 | completion); |
488 | |
489 | gtk_tree_view_set_enable_search (GTK_TREE_VIEW (completion->tree_view), FALSE); |
490 | gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (completion->tree_view), FALSE); |
491 | gtk_tree_view_set_hover_selection (GTK_TREE_VIEW (completion->tree_view), TRUE); |
492 | gtk_tree_view_set_activate_on_single_click (GTK_TREE_VIEW (completion->tree_view), TRUE); |
493 | |
494 | sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (completion->tree_view)); |
495 | gtk_tree_selection_set_mode (selection: sel, type: GTK_SELECTION_SINGLE); |
496 | gtk_tree_selection_unselect_all (selection: sel); |
497 | g_signal_connect (sel, "changed" , |
498 | G_CALLBACK (gtk_entry_completion_selection_changed), |
499 | completion); |
500 | completion->first_sel_changed = TRUE; |
501 | |
502 | completion->column = gtk_tree_view_column_new_with_area (area: completion->cell_area); |
503 | gtk_tree_view_append_column (GTK_TREE_VIEW (completion->tree_view), column: completion->column); |
504 | |
505 | completion->scrolled_window = gtk_scrolled_window_new (); |
506 | gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (completion->scrolled_window), |
507 | hscrollbar_policy: GTK_POLICY_NEVER, |
508 | vscrollbar_policy: GTK_POLICY_AUTOMATIC); |
509 | |
510 | /* a nasty hack to get the completions treeview to size nicely */ |
511 | gtk_widget_set_size_request (widget: gtk_scrolled_window_get_vscrollbar (GTK_SCROLLED_WINDOW (completion->scrolled_window)), |
512 | width: -1, height: 0); |
513 | |
514 | /* pack it all */ |
515 | completion->popup_window = gtk_popover_new (); |
516 | gtk_popover_set_position (GTK_POPOVER (completion->popup_window), position: GTK_POS_BOTTOM); |
517 | gtk_popover_set_autohide (GTK_POPOVER (completion->popup_window), FALSE); |
518 | gtk_popover_set_has_arrow (GTK_POPOVER (completion->popup_window), FALSE); |
519 | gtk_widget_add_css_class (widget: completion->popup_window, css_class: "entry-completion" ); |
520 | |
521 | controller = gtk_event_controller_key_new (); |
522 | g_signal_connect (controller, "key-pressed" , |
523 | G_CALLBACK (propagate_to_entry), completion); |
524 | g_signal_connect (controller, "key-released" , |
525 | G_CALLBACK (propagate_to_entry), completion); |
526 | gtk_widget_add_controller (widget: completion->popup_window, controller); |
527 | |
528 | controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ()); |
529 | g_signal_connect_swapped (controller, "released" , |
530 | G_CALLBACK (_gtk_entry_completion_popdown), |
531 | completion); |
532 | gtk_widget_add_controller (widget: completion->popup_window, controller); |
533 | |
534 | gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (completion->scrolled_window), |
535 | child: completion->tree_view); |
536 | gtk_widget_set_hexpand (widget: completion->scrolled_window, TRUE); |
537 | gtk_widget_set_vexpand (widget: completion->scrolled_window, TRUE); |
538 | gtk_popover_set_child (GTK_POPOVER (completion->popup_window), child: completion->scrolled_window); |
539 | } |
540 | |
541 | |
542 | static void |
543 | gtk_entry_completion_set_property (GObject *object, |
544 | guint prop_id, |
545 | const GValue *value, |
546 | GParamSpec *pspec) |
547 | { |
548 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (object); |
549 | GtkCellArea *area; |
550 | |
551 | switch (prop_id) |
552 | { |
553 | case PROP_MODEL: |
554 | gtk_entry_completion_set_model (completion, |
555 | model: g_value_get_object (value)); |
556 | break; |
557 | |
558 | case PROP_MINIMUM_KEY_LENGTH: |
559 | gtk_entry_completion_set_minimum_key_length (completion, |
560 | length: g_value_get_int (value)); |
561 | break; |
562 | |
563 | case PROP_TEXT_COLUMN: |
564 | completion->text_column = g_value_get_int (value); |
565 | break; |
566 | |
567 | case PROP_INLINE_COMPLETION: |
568 | gtk_entry_completion_set_inline_completion (completion, |
569 | inline_completion: g_value_get_boolean (value)); |
570 | break; |
571 | |
572 | case PROP_POPUP_COMPLETION: |
573 | gtk_entry_completion_set_popup_completion (completion, |
574 | popup_completion: g_value_get_boolean (value)); |
575 | break; |
576 | |
577 | case PROP_POPUP_SET_WIDTH: |
578 | gtk_entry_completion_set_popup_set_width (completion, |
579 | popup_set_width: g_value_get_boolean (value)); |
580 | break; |
581 | |
582 | case PROP_POPUP_SINGLE_MATCH: |
583 | gtk_entry_completion_set_popup_single_match (completion, |
584 | popup_single_match: g_value_get_boolean (value)); |
585 | break; |
586 | |
587 | case PROP_INLINE_SELECTION: |
588 | gtk_entry_completion_set_inline_selection (completion, |
589 | inline_selection: g_value_get_boolean (value)); |
590 | break; |
591 | |
592 | case PROP_CELL_AREA: |
593 | /* Construct-only, can only be assigned once */ |
594 | area = g_value_get_object (value); |
595 | if (area) |
596 | { |
597 | if (completion->cell_area != NULL) |
598 | { |
599 | g_warning ("cell-area has already been set, ignoring construct property" ); |
600 | g_object_ref_sink (area); |
601 | g_object_unref (object: area); |
602 | } |
603 | else |
604 | completion->cell_area = g_object_ref_sink (area); |
605 | } |
606 | break; |
607 | |
608 | default: |
609 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
610 | break; |
611 | } |
612 | } |
613 | |
614 | static void |
615 | gtk_entry_completion_get_property (GObject *object, |
616 | guint prop_id, |
617 | GValue *value, |
618 | GParamSpec *pspec) |
619 | { |
620 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (object); |
621 | |
622 | switch (prop_id) |
623 | { |
624 | case PROP_MODEL: |
625 | g_value_set_object (value, |
626 | v_object: gtk_entry_completion_get_model (completion)); |
627 | break; |
628 | |
629 | case PROP_MINIMUM_KEY_LENGTH: |
630 | g_value_set_int (value, v_int: gtk_entry_completion_get_minimum_key_length (completion)); |
631 | break; |
632 | |
633 | case PROP_TEXT_COLUMN: |
634 | g_value_set_int (value, v_int: gtk_entry_completion_get_text_column (completion)); |
635 | break; |
636 | |
637 | case PROP_INLINE_COMPLETION: |
638 | g_value_set_boolean (value, v_boolean: gtk_entry_completion_get_inline_completion (completion)); |
639 | break; |
640 | |
641 | case PROP_POPUP_COMPLETION: |
642 | g_value_set_boolean (value, v_boolean: gtk_entry_completion_get_popup_completion (completion)); |
643 | break; |
644 | |
645 | case PROP_POPUP_SET_WIDTH: |
646 | g_value_set_boolean (value, v_boolean: gtk_entry_completion_get_popup_set_width (completion)); |
647 | break; |
648 | |
649 | case PROP_POPUP_SINGLE_MATCH: |
650 | g_value_set_boolean (value, v_boolean: gtk_entry_completion_get_popup_single_match (completion)); |
651 | break; |
652 | |
653 | case PROP_INLINE_SELECTION: |
654 | g_value_set_boolean (value, v_boolean: gtk_entry_completion_get_inline_selection (completion)); |
655 | break; |
656 | |
657 | case PROP_CELL_AREA: |
658 | g_value_set_object (value, v_object: completion->cell_area); |
659 | break; |
660 | |
661 | default: |
662 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
663 | break; |
664 | } |
665 | } |
666 | |
667 | static void |
668 | gtk_entry_completion_finalize (GObject *object) |
669 | { |
670 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (object); |
671 | |
672 | g_free (mem: completion->case_normalized_key); |
673 | g_free (mem: completion->completion_prefix); |
674 | |
675 | if (completion->match_notify) |
676 | (* completion->match_notify) (completion->match_data); |
677 | |
678 | G_OBJECT_CLASS (gtk_entry_completion_parent_class)->finalize (object); |
679 | } |
680 | |
681 | static void |
682 | gtk_entry_completion_dispose (GObject *object) |
683 | { |
684 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (object); |
685 | |
686 | if (completion->entry) |
687 | gtk_entry_set_completion (GTK_ENTRY (completion->entry), NULL); |
688 | |
689 | g_clear_object (&completion->cell_area); |
690 | |
691 | G_OBJECT_CLASS (gtk_entry_completion_parent_class)->dispose (object); |
692 | } |
693 | |
694 | /* implement cell layout interface (only need to return the underlying cell area) */ |
695 | static GtkCellArea* |
696 | gtk_entry_completion_get_area (GtkCellLayout *cell_layout) |
697 | { |
698 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (cell_layout); |
699 | |
700 | if (G_UNLIKELY (!completion->cell_area)) |
701 | { |
702 | completion->cell_area = gtk_cell_area_box_new (); |
703 | g_object_ref_sink (completion->cell_area); |
704 | } |
705 | |
706 | return completion->cell_area; |
707 | } |
708 | |
709 | /* all those callbacks */ |
710 | static gboolean |
711 | gtk_entry_completion_default_completion_func (GtkEntryCompletion *completion, |
712 | const char *key, |
713 | GtkTreeIter *iter, |
714 | gpointer user_data) |
715 | { |
716 | char *item = NULL; |
717 | char *normalized_string; |
718 | char *case_normalized_string; |
719 | |
720 | gboolean ret = FALSE; |
721 | |
722 | GtkTreeModel *model; |
723 | |
724 | model = gtk_tree_model_filter_get_model (filter: completion->filter_model); |
725 | |
726 | g_return_val_if_fail (gtk_tree_model_get_column_type (model, completion->text_column) == G_TYPE_STRING, |
727 | FALSE); |
728 | |
729 | gtk_tree_model_get (tree_model: model, iter, |
730 | completion->text_column, &item, |
731 | -1); |
732 | |
733 | if (item != NULL) |
734 | { |
735 | normalized_string = g_utf8_normalize (str: item, len: -1, mode: G_NORMALIZE_ALL); |
736 | |
737 | if (normalized_string != NULL) |
738 | { |
739 | case_normalized_string = g_utf8_casefold (str: normalized_string, len: -1); |
740 | |
741 | if (!strncmp (s1: key, s2: case_normalized_string, n: strlen (s: key))) |
742 | ret = TRUE; |
743 | |
744 | g_free (mem: case_normalized_string); |
745 | } |
746 | g_free (mem: normalized_string); |
747 | } |
748 | g_free (mem: item); |
749 | |
750 | return ret; |
751 | } |
752 | |
753 | static gboolean |
754 | gtk_entry_completion_visible_func (GtkTreeModel *model, |
755 | GtkTreeIter *iter, |
756 | gpointer data) |
757 | { |
758 | gboolean ret = FALSE; |
759 | |
760 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (data); |
761 | |
762 | if (!completion->case_normalized_key) |
763 | return ret; |
764 | |
765 | if (completion->match_func) |
766 | ret = (* completion->match_func) (completion, |
767 | completion->case_normalized_key, |
768 | iter, |
769 | completion->match_data); |
770 | else if (completion->text_column >= 0) |
771 | ret = gtk_entry_completion_default_completion_func (completion, |
772 | key: completion->case_normalized_key, |
773 | iter, |
774 | NULL); |
775 | |
776 | return ret; |
777 | } |
778 | |
779 | static void |
780 | gtk_entry_completion_list_activated (GtkTreeView *treeview, |
781 | GtkTreePath *path, |
782 | GtkTreeViewColumn *column, |
783 | gpointer user_data) |
784 | { |
785 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (user_data); |
786 | GtkTreeIter iter; |
787 | gboolean entry_set; |
788 | GtkTreeModel *model; |
789 | GtkTreeIter child_iter; |
790 | GtkText *text = gtk_entry_get_text_widget (GTK_ENTRY (completion->entry)); |
791 | |
792 | gtk_tree_model_get_iter (GTK_TREE_MODEL (completion->filter_model), iter: &iter, path); |
793 | gtk_tree_model_filter_convert_iter_to_child_iter (filter: completion->filter_model, |
794 | child_iter: &child_iter, |
795 | filter_iter: &iter); |
796 | model = gtk_tree_model_filter_get_model (filter: completion->filter_model); |
797 | |
798 | g_signal_handler_block (instance: text, handler_id: completion->changed_id); |
799 | g_signal_emit (instance: completion, signal_id: entry_completion_signals[MATCH_SELECTED], |
800 | detail: 0, model, &child_iter, &entry_set); |
801 | g_signal_handler_unblock (instance: text, handler_id: completion->changed_id); |
802 | |
803 | _gtk_entry_completion_popdown (completion); |
804 | } |
805 | |
806 | static void |
807 | gtk_entry_completion_selection_changed (GtkTreeSelection *selection, |
808 | gpointer data) |
809 | { |
810 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (data); |
811 | |
812 | if (completion->first_sel_changed) |
813 | { |
814 | completion->first_sel_changed = FALSE; |
815 | if (gtk_widget_is_focus (widget: completion->tree_view)) |
816 | gtk_tree_selection_unselect_all (selection); |
817 | } |
818 | } |
819 | |
820 | /* public API */ |
821 | |
822 | /** |
823 | * gtk_entry_completion_new: |
824 | * |
825 | * Creates a new `GtkEntryCompletion` object. |
826 | * |
827 | * Returns: A newly created `GtkEntryCompletion` object |
828 | */ |
829 | GtkEntryCompletion * |
830 | gtk_entry_completion_new (void) |
831 | { |
832 | GtkEntryCompletion *completion; |
833 | |
834 | completion = g_object_new (GTK_TYPE_ENTRY_COMPLETION, NULL); |
835 | |
836 | return completion; |
837 | } |
838 | |
839 | /** |
840 | * gtk_entry_completion_new_with_area: |
841 | * @area: the `GtkCellArea` used to layout cells |
842 | * |
843 | * Creates a new `GtkEntryCompletion` object using the |
844 | * specified @area. |
845 | * |
846 | * The `GtkCellArea` is used to layout cells in the underlying |
847 | * `GtkTreeViewColumn` for the drop-down menu. |
848 | * |
849 | * Returns: A newly created `GtkEntryCompletion` object |
850 | */ |
851 | GtkEntryCompletion * |
852 | gtk_entry_completion_new_with_area (GtkCellArea *area) |
853 | { |
854 | GtkEntryCompletion *completion; |
855 | |
856 | completion = g_object_new (GTK_TYPE_ENTRY_COMPLETION, first_property_name: "cell-area" , area, NULL); |
857 | |
858 | return completion; |
859 | } |
860 | |
861 | /** |
862 | * gtk_entry_completion_get_entry: |
863 | * @completion: a `GtkEntryCompletion` |
864 | * |
865 | * Gets the entry @completion has been attached to. |
866 | * |
867 | * Returns: (transfer none): The entry @completion has been attached to |
868 | */ |
869 | GtkWidget * |
870 | gtk_entry_completion_get_entry (GtkEntryCompletion *completion) |
871 | { |
872 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), NULL); |
873 | |
874 | return completion->entry; |
875 | } |
876 | |
877 | /** |
878 | * gtk_entry_completion_set_model: (attributes org.gtk.Method.set_property=model) |
879 | * @completion: a `GtkEntryCompletion` |
880 | * @model: (nullable): the `GtkTreeModel` |
881 | * |
882 | * Sets the model for a `GtkEntryCompletion`. |
883 | * |
884 | * If @completion already has a model set, it will remove it |
885 | * before setting the new model. If model is %NULL, then it |
886 | * will unset the model. |
887 | */ |
888 | void |
889 | gtk_entry_completion_set_model (GtkEntryCompletion *completion, |
890 | GtkTreeModel *model) |
891 | { |
892 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
893 | g_return_if_fail (model == NULL || GTK_IS_TREE_MODEL (model)); |
894 | |
895 | if (!model) |
896 | { |
897 | gtk_tree_view_set_model (GTK_TREE_VIEW (completion->tree_view), |
898 | NULL); |
899 | _gtk_entry_completion_popdown (completion); |
900 | completion->filter_model = NULL; |
901 | return; |
902 | } |
903 | |
904 | /* code will unref the old filter model (if any) */ |
905 | completion->filter_model = |
906 | GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (model, NULL)); |
907 | gtk_tree_model_filter_set_visible_func (filter: completion->filter_model, |
908 | func: gtk_entry_completion_visible_func, |
909 | data: completion, |
910 | NULL); |
911 | |
912 | gtk_tree_view_set_model (GTK_TREE_VIEW (completion->tree_view), |
913 | GTK_TREE_MODEL (completion->filter_model)); |
914 | g_object_unref (object: completion->filter_model); |
915 | |
916 | g_object_notify_by_pspec (G_OBJECT (completion), pspec: entry_completion_props[PROP_MODEL]); |
917 | |
918 | if (gtk_widget_get_visible (widget: completion->popup_window)) |
919 | _gtk_entry_completion_resize_popup (completion); |
920 | } |
921 | |
922 | /** |
923 | * gtk_entry_completion_get_model: (attributes org.gtk.Method.get_property=model) |
924 | * @completion: a `GtkEntryCompletion` |
925 | * |
926 | * Returns the model the `GtkEntryCompletion` is using as data source. |
927 | * |
928 | * Returns %NULL if the model is unset. |
929 | * |
930 | * Returns: (nullable) (transfer none): A `GtkTreeModel` |
931 | */ |
932 | GtkTreeModel * |
933 | gtk_entry_completion_get_model (GtkEntryCompletion *completion) |
934 | { |
935 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), NULL); |
936 | |
937 | if (!completion->filter_model) |
938 | return NULL; |
939 | |
940 | return gtk_tree_model_filter_get_model (filter: completion->filter_model); |
941 | } |
942 | |
943 | /** |
944 | * gtk_entry_completion_set_match_func: |
945 | * @completion: a `GtkEntryCompletion` |
946 | * @func: the `GtkEntryCompletion`MatchFunc to use |
947 | * @func_data: user data for @func |
948 | * @func_notify: destroy notify for @func_data. |
949 | * |
950 | * Sets the match function for @completion to be @func. |
951 | * |
952 | * The match function is used to determine if a row should or |
953 | * should not be in the completion list. |
954 | */ |
955 | void |
956 | gtk_entry_completion_set_match_func (GtkEntryCompletion *completion, |
957 | GtkEntryCompletionMatchFunc func, |
958 | gpointer func_data, |
959 | GDestroyNotify func_notify) |
960 | { |
961 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
962 | |
963 | if (completion->match_notify) |
964 | (* completion->match_notify) (completion->match_data); |
965 | |
966 | completion->match_func = func; |
967 | completion->match_data = func_data; |
968 | completion->match_notify = func_notify; |
969 | } |
970 | |
971 | /** |
972 | * gtk_entry_completion_set_minimum_key_length: |
973 | * @completion: a `GtkEntryCompletion` |
974 | * @length: the minimum length of the key in order to start completing |
975 | * |
976 | * Requires the length of the search key for @completion to be at least |
977 | * @length. |
978 | * |
979 | * This is useful for long lists, where completing using a small |
980 | * key takes a lot of time and will come up with meaningless results anyway |
981 | * (ie, a too large dataset). |
982 | */ |
983 | void |
984 | gtk_entry_completion_set_minimum_key_length (GtkEntryCompletion *completion, |
985 | int length) |
986 | { |
987 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
988 | g_return_if_fail (length >= 0); |
989 | |
990 | if (completion->minimum_key_length != length) |
991 | { |
992 | completion->minimum_key_length = length; |
993 | |
994 | g_object_notify_by_pspec (G_OBJECT (completion), |
995 | pspec: entry_completion_props[PROP_MINIMUM_KEY_LENGTH]); |
996 | } |
997 | } |
998 | |
999 | /** |
1000 | * gtk_entry_completion_get_minimum_key_length: |
1001 | * @completion: a `GtkEntryCompletion` |
1002 | * |
1003 | * Returns the minimum key length as set for @completion. |
1004 | * |
1005 | * Returns: The currently used minimum key length |
1006 | */ |
1007 | int |
1008 | gtk_entry_completion_get_minimum_key_length (GtkEntryCompletion *completion) |
1009 | { |
1010 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), 0); |
1011 | |
1012 | return completion->minimum_key_length; |
1013 | } |
1014 | |
1015 | /** |
1016 | * gtk_entry_completion_complete: |
1017 | * @completion: a `GtkEntryCompletion` |
1018 | * |
1019 | * Requests a completion operation, or in other words a refiltering of the |
1020 | * current list with completions, using the current key. |
1021 | * |
1022 | * The completion list view will be updated accordingly. |
1023 | */ |
1024 | void |
1025 | gtk_entry_completion_complete (GtkEntryCompletion *completion) |
1026 | { |
1027 | char *tmp; |
1028 | GtkTreeIter iter; |
1029 | |
1030 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
1031 | g_return_if_fail (GTK_IS_ENTRY (completion->entry)); |
1032 | |
1033 | if (!completion->filter_model) |
1034 | return; |
1035 | |
1036 | g_free (mem: completion->case_normalized_key); |
1037 | |
1038 | tmp = g_utf8_normalize (str: gtk_editable_get_text (GTK_EDITABLE (completion->entry)), |
1039 | len: -1, mode: G_NORMALIZE_ALL); |
1040 | completion->case_normalized_key = g_utf8_casefold (str: tmp, len: -1); |
1041 | g_free (mem: tmp); |
1042 | |
1043 | gtk_tree_model_filter_refilter (filter: completion->filter_model); |
1044 | |
1045 | if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (completion->filter_model), iter: &iter)) |
1046 | g_signal_emit (instance: completion, signal_id: entry_completion_signals[NO_MATCHES], detail: 0); |
1047 | |
1048 | if (gtk_widget_get_visible (widget: completion->popup_window)) |
1049 | _gtk_entry_completion_resize_popup (completion); |
1050 | } |
1051 | |
1052 | /** |
1053 | * gtk_entry_completion_set_text_column: (attributes org.gtk.Method.set_property=text-column) |
1054 | * @completion: a `GtkEntryCompletion` |
1055 | * @column: the column in the model of @completion to get strings from |
1056 | * |
1057 | * Convenience function for setting up the most used case of this code: a |
1058 | * completion list with just strings. |
1059 | * |
1060 | * This function will set up @completion |
1061 | * to have a list displaying all (and just) strings in the completion list, |
1062 | * and to get those strings from @column in the model of @completion. |
1063 | * |
1064 | * This functions creates and adds a `GtkCellRendererText` for the selected |
1065 | * column. If you need to set the text column, but don't want the cell |
1066 | * renderer, use g_object_set() to set the |
1067 | * [property@Gtk.EntryCompletion:text-column] property directly. |
1068 | */ |
1069 | void |
1070 | gtk_entry_completion_set_text_column (GtkEntryCompletion *completion, |
1071 | int column) |
1072 | { |
1073 | GtkCellRenderer *cell; |
1074 | |
1075 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
1076 | g_return_if_fail (column >= 0); |
1077 | |
1078 | if (completion->text_column == column) |
1079 | return; |
1080 | |
1081 | completion->text_column = column; |
1082 | |
1083 | cell = gtk_cell_renderer_text_new (); |
1084 | gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (completion), |
1085 | cell, TRUE); |
1086 | gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (completion), |
1087 | cell, |
1088 | attribute: "text" , column); |
1089 | |
1090 | g_object_notify_by_pspec (G_OBJECT (completion), pspec: entry_completion_props[PROP_TEXT_COLUMN]); |
1091 | } |
1092 | |
1093 | /** |
1094 | * gtk_entry_completion_get_text_column: (attributes org.gtk.Method.get_property=text-column) |
1095 | * @completion: a `GtkEntryCompletion` |
1096 | * |
1097 | * Returns the column in the model of @completion to get strings from. |
1098 | * |
1099 | * Returns: the column containing the strings |
1100 | */ |
1101 | int |
1102 | gtk_entry_completion_get_text_column (GtkEntryCompletion *completion) |
1103 | { |
1104 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), -1); |
1105 | |
1106 | return completion->text_column; |
1107 | } |
1108 | |
1109 | /* private */ |
1110 | |
1111 | /* some nasty size requisition */ |
1112 | void |
1113 | (GtkEntryCompletion *completion) |
1114 | { |
1115 | GtkAllocation allocation; |
1116 | int matches, items, height; |
1117 | GdkSurface *surface; |
1118 | GtkRequisition entry_req; |
1119 | GtkRequisition tree_req; |
1120 | int width; |
1121 | |
1122 | surface = gtk_native_get_surface (self: gtk_widget_get_native (widget: completion->entry)); |
1123 | |
1124 | if (!surface) |
1125 | return; |
1126 | |
1127 | if (!completion->filter_model) |
1128 | return; |
1129 | |
1130 | gtk_widget_get_surface_allocation (widget: completion->entry, allocation: &allocation); |
1131 | gtk_widget_get_preferred_size (widget: completion->entry, |
1132 | minimum_size: &entry_req, NULL); |
1133 | |
1134 | matches = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (completion->filter_model), NULL); |
1135 | |
1136 | /* Call get preferred size on the on the tree view to force it to validate its |
1137 | * cells before calling into the cell size functions. |
1138 | */ |
1139 | gtk_widget_get_preferred_size (widget: completion->tree_view, |
1140 | minimum_size: &tree_req, NULL); |
1141 | gtk_tree_view_column_cell_get_size (tree_column: completion->column, |
1142 | NULL, NULL, NULL, height: &height); |
1143 | |
1144 | gtk_widget_realize (widget: completion->tree_view); |
1145 | |
1146 | items = MIN (matches, 10); |
1147 | |
1148 | if (items <= 0) |
1149 | gtk_widget_hide (widget: completion->scrolled_window); |
1150 | else |
1151 | gtk_widget_show (widget: completion->scrolled_window); |
1152 | |
1153 | if (completion->popup_set_width) |
1154 | width = allocation.width; |
1155 | else |
1156 | width = -1; |
1157 | |
1158 | gtk_tree_view_columns_autosize (GTK_TREE_VIEW (completion->tree_view)); |
1159 | gtk_scrolled_window_set_min_content_width (GTK_SCROLLED_WINDOW (completion->scrolled_window), width); |
1160 | gtk_widget_set_size_request (widget: completion->popup_window, width, height: -1); |
1161 | gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (completion->scrolled_window), height: items * height); |
1162 | |
1163 | gtk_popover_present (GTK_POPOVER (completion->popup_window)); |
1164 | } |
1165 | |
1166 | static void |
1167 | (GtkEntryCompletion *completion) |
1168 | { |
1169 | GtkText *text = gtk_entry_get_text_widget (GTK_ENTRY (completion->entry)); |
1170 | |
1171 | if (gtk_widget_get_mapped (widget: completion->popup_window)) |
1172 | return; |
1173 | |
1174 | if (!gtk_widget_get_mapped (GTK_WIDGET (text))) |
1175 | return; |
1176 | |
1177 | if (!gtk_widget_has_focus (GTK_WIDGET (text))) |
1178 | return; |
1179 | |
1180 | /* default on no match */ |
1181 | completion->current_selected = -1; |
1182 | |
1183 | gtk_widget_realize (widget: completion->popup_window); |
1184 | |
1185 | _gtk_entry_completion_resize_popup (completion); |
1186 | |
1187 | if (completion->filter_model) |
1188 | { |
1189 | GtkTreePath *path; |
1190 | |
1191 | path = gtk_tree_path_new_from_indices (first_index: 0, -1); |
1192 | gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (completion->tree_view), path, |
1193 | NULL, FALSE, row_align: 0.0, col_align: 0.0); |
1194 | gtk_tree_path_free (path); |
1195 | } |
1196 | |
1197 | gtk_popover_popup (GTK_POPOVER (completion->popup_window)); |
1198 | } |
1199 | |
1200 | void |
1201 | _gtk_entry_completion_popdown (GtkEntryCompletion *completion) |
1202 | { |
1203 | if (!gtk_widget_get_mapped (widget: completion->popup_window)) |
1204 | return; |
1205 | |
1206 | gtk_popover_popdown (GTK_POPOVER (completion->popup_window)); |
1207 | } |
1208 | |
1209 | static gboolean |
1210 | gtk_entry_completion_match_selected (GtkEntryCompletion *completion, |
1211 | GtkTreeModel *model, |
1212 | GtkTreeIter *iter) |
1213 | { |
1214 | g_assert (completion->entry != NULL); |
1215 | |
1216 | char *str = NULL; |
1217 | |
1218 | gtk_tree_model_get (tree_model: model, iter, completion->text_column, &str, -1); |
1219 | gtk_editable_set_text (GTK_EDITABLE (completion->entry), text: str ? str : "" ); |
1220 | |
1221 | /* move cursor to the end */ |
1222 | gtk_editable_set_position (GTK_EDITABLE (completion->entry), position: -1); |
1223 | |
1224 | g_free (mem: str); |
1225 | |
1226 | return TRUE; |
1227 | } |
1228 | |
1229 | static gboolean |
1230 | gtk_entry_completion_cursor_on_match (GtkEntryCompletion *completion, |
1231 | GtkTreeModel *model, |
1232 | GtkTreeIter *iter) |
1233 | { |
1234 | g_assert (completion->entry != NULL); |
1235 | |
1236 | gtk_entry_completion_insert_completion (completion, model, iter); |
1237 | |
1238 | return TRUE; |
1239 | } |
1240 | |
1241 | /** |
1242 | * gtk_entry_completion_compute_prefix: |
1243 | * @completion: the entry completion |
1244 | * @key: The text to complete for |
1245 | * |
1246 | * Computes the common prefix that is shared by all rows in @completion |
1247 | * that start with @key. |
1248 | * |
1249 | * If no row matches @key, %NULL will be returned. |
1250 | * Note that a text column must have been set for this function to work, |
1251 | * see [method@Gtk.EntryCompletion.set_text_column] for details. |
1252 | * |
1253 | * Returns: (nullable) (transfer full): The common prefix all rows |
1254 | * starting with @key |
1255 | */ |
1256 | char * |
1257 | gtk_entry_completion_compute_prefix (GtkEntryCompletion *completion, |
1258 | const char *key) |
1259 | { |
1260 | GtkTreeIter iter; |
1261 | char *prefix = NULL; |
1262 | gboolean valid; |
1263 | |
1264 | if (completion->text_column < 0) |
1265 | return NULL; |
1266 | |
1267 | valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (completion->filter_model), |
1268 | iter: &iter); |
1269 | |
1270 | while (valid) |
1271 | { |
1272 | char *text; |
1273 | |
1274 | gtk_tree_model_get (GTK_TREE_MODEL (completion->filter_model), |
1275 | iter: &iter, completion->text_column, &text, |
1276 | -1); |
1277 | |
1278 | if (text && g_str_has_prefix (str: text, prefix: key)) |
1279 | { |
1280 | if (!prefix) |
1281 | prefix = g_strdup (str: text); |
1282 | else |
1283 | { |
1284 | char *p = prefix; |
1285 | char *q = text; |
1286 | |
1287 | while (*p && *p == *q) |
1288 | { |
1289 | p++; |
1290 | q++; |
1291 | } |
1292 | |
1293 | *p = '\0'; |
1294 | |
1295 | if (p > prefix) |
1296 | { |
1297 | /* strip a partial multibyte character */ |
1298 | q = g_utf8_find_prev_char (str: prefix, p); |
1299 | switch (g_utf8_get_char_validated (p: q, max_len: p - q)) |
1300 | { |
1301 | case (gunichar)-2: |
1302 | case (gunichar)-1: |
1303 | *q = 0; |
1304 | break; |
1305 | default: ; |
1306 | } |
1307 | } |
1308 | } |
1309 | } |
1310 | |
1311 | g_free (mem: text); |
1312 | valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (completion->filter_model), |
1313 | iter: &iter); |
1314 | } |
1315 | |
1316 | return prefix; |
1317 | } |
1318 | |
1319 | |
1320 | static gboolean |
1321 | gtk_entry_completion_real_insert_prefix (GtkEntryCompletion *completion, |
1322 | const char *prefix) |
1323 | { |
1324 | g_assert (completion->entry != NULL); |
1325 | |
1326 | if (prefix) |
1327 | { |
1328 | int key_len; |
1329 | int prefix_len; |
1330 | const char *key; |
1331 | |
1332 | prefix_len = g_utf8_strlen (p: prefix, max: -1); |
1333 | |
1334 | key = gtk_editable_get_text (GTK_EDITABLE (completion->entry)); |
1335 | key_len = g_utf8_strlen (p: key, max: -1); |
1336 | |
1337 | if (prefix_len > key_len) |
1338 | { |
1339 | int pos = prefix_len; |
1340 | |
1341 | gtk_editable_insert_text (GTK_EDITABLE (completion->entry), |
1342 | text: prefix + strlen (s: key), length: -1, position: &pos); |
1343 | gtk_editable_select_region (GTK_EDITABLE (completion->entry), |
1344 | start_pos: key_len, end_pos: prefix_len); |
1345 | |
1346 | completion->has_completion = TRUE; |
1347 | } |
1348 | } |
1349 | |
1350 | return TRUE; |
1351 | } |
1352 | |
1353 | /** |
1354 | * gtk_entry_completion_get_completion_prefix: |
1355 | * @completion: a `GtkEntryCompletion` |
1356 | * |
1357 | * Get the original text entered by the user that triggered |
1358 | * the completion or %NULL if there’s no completion ongoing. |
1359 | * |
1360 | * Returns: (nullable): the prefix for the current completion |
1361 | */ |
1362 | const char * |
1363 | gtk_entry_completion_get_completion_prefix (GtkEntryCompletion *completion) |
1364 | { |
1365 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), NULL); |
1366 | |
1367 | return completion->completion_prefix; |
1368 | } |
1369 | |
1370 | static void |
1371 | gtk_entry_completion_insert_completion_text (GtkEntryCompletion *completion, |
1372 | const char *new_text) |
1373 | { |
1374 | int len; |
1375 | GtkText *text = gtk_entry_get_text_widget (GTK_ENTRY (completion->entry)); |
1376 | GtkEntryBuffer *buffer = gtk_text_get_buffer (self: text); |
1377 | |
1378 | if (completion->changed_id > 0) |
1379 | g_signal_handler_block (instance: text, handler_id: completion->changed_id); |
1380 | |
1381 | if (completion->insert_text_id > 0) |
1382 | g_signal_handler_block (instance: buffer, handler_id: completion->insert_text_id); |
1383 | |
1384 | gtk_editable_set_text (GTK_EDITABLE (completion->entry), text: new_text); |
1385 | |
1386 | len = g_utf8_strlen (p: completion->completion_prefix, max: -1); |
1387 | gtk_editable_select_region (GTK_EDITABLE (completion->entry), start_pos: len, end_pos: -1); |
1388 | |
1389 | if (completion->changed_id > 0) |
1390 | g_signal_handler_unblock (instance: text, handler_id: completion->changed_id); |
1391 | |
1392 | if (completion->insert_text_id > 0) |
1393 | g_signal_handler_unblock (instance: buffer, handler_id: completion->insert_text_id); |
1394 | } |
1395 | |
1396 | static gboolean |
1397 | gtk_entry_completion_insert_completion (GtkEntryCompletion *completion, |
1398 | GtkTreeModel *model, |
1399 | GtkTreeIter *iter) |
1400 | { |
1401 | char *str = NULL; |
1402 | |
1403 | if (completion->text_column < 0) |
1404 | return FALSE; |
1405 | |
1406 | gtk_tree_model_get (tree_model: model, iter, |
1407 | completion->text_column, &str, |
1408 | -1); |
1409 | |
1410 | gtk_entry_completion_insert_completion_text (completion, new_text: str); |
1411 | |
1412 | g_free (mem: str); |
1413 | |
1414 | return TRUE; |
1415 | } |
1416 | |
1417 | /** |
1418 | * gtk_entry_completion_insert_prefix: |
1419 | * @completion: a `GtkEntryCompletion` |
1420 | * |
1421 | * Requests a prefix insertion. |
1422 | */ |
1423 | void |
1424 | gtk_entry_completion_insert_prefix (GtkEntryCompletion *completion) |
1425 | { |
1426 | g_return_if_fail (completion->entry != NULL); |
1427 | |
1428 | gboolean done; |
1429 | char *prefix; |
1430 | GtkText *text = gtk_entry_get_text_widget (GTK_ENTRY (completion->entry)); |
1431 | GtkEntryBuffer *buffer = gtk_text_get_buffer (self: text); |
1432 | |
1433 | if (completion->insert_text_id > 0) |
1434 | g_signal_handler_block (instance: buffer, handler_id: completion->insert_text_id); |
1435 | |
1436 | prefix = gtk_entry_completion_compute_prefix (completion, |
1437 | key: gtk_editable_get_text (GTK_EDITABLE (completion->entry))); |
1438 | |
1439 | if (prefix) |
1440 | { |
1441 | g_signal_emit (instance: completion, signal_id: entry_completion_signals[INSERT_PREFIX], |
1442 | detail: 0, prefix, &done); |
1443 | g_free (mem: prefix); |
1444 | } |
1445 | |
1446 | if (completion->insert_text_id > 0) |
1447 | g_signal_handler_unblock (instance: buffer, handler_id: completion->insert_text_id); |
1448 | } |
1449 | |
1450 | /** |
1451 | * gtk_entry_completion_set_inline_completion: (attributes org.gtk.Method.set_property=inline-completion) |
1452 | * @completion: a `GtkEntryCompletion` |
1453 | * @inline_completion: %TRUE to do inline completion |
1454 | * |
1455 | * Sets whether the common prefix of the possible completions should |
1456 | * be automatically inserted in the entry. |
1457 | */ |
1458 | void |
1459 | gtk_entry_completion_set_inline_completion (GtkEntryCompletion *completion, |
1460 | gboolean inline_completion) |
1461 | { |
1462 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
1463 | |
1464 | inline_completion = inline_completion != FALSE; |
1465 | |
1466 | if (completion->inline_completion != inline_completion) |
1467 | { |
1468 | completion->inline_completion = inline_completion; |
1469 | |
1470 | g_object_notify_by_pspec (G_OBJECT (completion), pspec: entry_completion_props[PROP_INLINE_COMPLETION]); |
1471 | } |
1472 | } |
1473 | |
1474 | /** |
1475 | * gtk_entry_completion_get_inline_completion: (attributes org.gtk.Method.get_property=inline-completion) |
1476 | * @completion: a `GtkEntryCompletion` |
1477 | * |
1478 | * Returns whether the common prefix of the possible completions should |
1479 | * be automatically inserted in the entry. |
1480 | * |
1481 | * Returns: %TRUE if inline completion is turned on |
1482 | */ |
1483 | gboolean |
1484 | gtk_entry_completion_get_inline_completion (GtkEntryCompletion *completion) |
1485 | { |
1486 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), FALSE); |
1487 | |
1488 | return completion->inline_completion; |
1489 | } |
1490 | |
1491 | /** |
1492 | * gtk_entry_completion_set_popup_completion: (attributes org.gtk.Method.set_property=popup-completion) |
1493 | * @completion: a `GtkEntryCompletion` |
1494 | * @popup_completion: %TRUE to do popup completion |
1495 | * |
1496 | * Sets whether the completions should be presented in a popup window. |
1497 | */ |
1498 | void |
1499 | (GtkEntryCompletion *completion, |
1500 | gboolean ) |
1501 | { |
1502 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
1503 | |
1504 | popup_completion = popup_completion != FALSE; |
1505 | |
1506 | if (completion->popup_completion != popup_completion) |
1507 | { |
1508 | completion->popup_completion = popup_completion; |
1509 | |
1510 | g_object_notify_by_pspec (G_OBJECT (completion), pspec: entry_completion_props[PROP_POPUP_COMPLETION]); |
1511 | } |
1512 | } |
1513 | |
1514 | |
1515 | /** |
1516 | * gtk_entry_completion_get_popup_completion: (attributes org.gtk.Method.get_property=popup-completion) |
1517 | * @completion: a `GtkEntryCompletion` |
1518 | * |
1519 | * Returns whether the completions should be presented in a popup window. |
1520 | * |
1521 | * Returns: %TRUE if popup completion is turned on |
1522 | */ |
1523 | gboolean |
1524 | (GtkEntryCompletion *completion) |
1525 | { |
1526 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), TRUE); |
1527 | |
1528 | return completion->popup_completion; |
1529 | } |
1530 | |
1531 | /** |
1532 | * gtk_entry_completion_set_popup_set_width: (attributes org.gtk.Method.set_property=popup-set-width) |
1533 | * @completion: a `GtkEntryCompletion` |
1534 | * @popup_set_width: %TRUE to make the width of the popup the same as the entry |
1535 | * |
1536 | * Sets whether the completion popup window will be resized to be the same |
1537 | * width as the entry. |
1538 | */ |
1539 | void |
1540 | (GtkEntryCompletion *completion, |
1541 | gboolean ) |
1542 | { |
1543 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
1544 | |
1545 | popup_set_width = popup_set_width != FALSE; |
1546 | |
1547 | if (completion->popup_set_width != popup_set_width) |
1548 | { |
1549 | completion->popup_set_width = popup_set_width; |
1550 | |
1551 | g_object_notify_by_pspec (G_OBJECT (completion), pspec: entry_completion_props[PROP_POPUP_SET_WIDTH]); |
1552 | } |
1553 | } |
1554 | |
1555 | /** |
1556 | * gtk_entry_completion_get_popup_set_width: (attributes org.gtk.Method.get_property=popup-set-width) |
1557 | * @completion: a `GtkEntryCompletion` |
1558 | * |
1559 | * Returns whether the completion popup window will be resized to the |
1560 | * width of the entry. |
1561 | * |
1562 | * Returns: %TRUE if the popup window will be resized to the width of |
1563 | * the entry |
1564 | */ |
1565 | gboolean |
1566 | (GtkEntryCompletion *completion) |
1567 | { |
1568 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), TRUE); |
1569 | |
1570 | return completion->popup_set_width; |
1571 | } |
1572 | |
1573 | |
1574 | /** |
1575 | * gtk_entry_completion_set_popup_single_match: (attributes org.gtk.Method.set_property=popup-single-match) |
1576 | * @completion: a `GtkEntryCompletion` |
1577 | * @popup_single_match: %TRUE if the popup should appear even for a single match |
1578 | * |
1579 | * Sets whether the completion popup window will appear even if there is |
1580 | * only a single match. |
1581 | * |
1582 | * You may want to set this to %FALSE if you |
1583 | * are using [property@Gtk.EntryCompletion:inline-completion]. |
1584 | */ |
1585 | void |
1586 | (GtkEntryCompletion *completion, |
1587 | gboolean ) |
1588 | { |
1589 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
1590 | |
1591 | popup_single_match = popup_single_match != FALSE; |
1592 | |
1593 | if (completion->popup_single_match != popup_single_match) |
1594 | { |
1595 | completion->popup_single_match = popup_single_match; |
1596 | |
1597 | g_object_notify_by_pspec (G_OBJECT (completion), pspec: entry_completion_props[PROP_POPUP_SINGLE_MATCH]); |
1598 | } |
1599 | } |
1600 | |
1601 | /** |
1602 | * gtk_entry_completion_get_popup_single_match: (attributes org.gtk.Method.get_property=popup-single-match) |
1603 | * @completion: a `GtkEntryCompletion` |
1604 | * |
1605 | * Returns whether the completion popup window will appear even if there is |
1606 | * only a single match. |
1607 | * |
1608 | * Returns: %TRUE if the popup window will appear regardless of the |
1609 | * number of matches |
1610 | */ |
1611 | gboolean |
1612 | (GtkEntryCompletion *completion) |
1613 | { |
1614 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), TRUE); |
1615 | |
1616 | return completion->popup_single_match; |
1617 | } |
1618 | |
1619 | /** |
1620 | * gtk_entry_completion_set_inline_selection: (attributes org.gtk.Method.set_property=inline-selection) |
1621 | * @completion: a `GtkEntryCompletion` |
1622 | * @inline_selection: %TRUE to do inline selection |
1623 | * |
1624 | * Sets whether it is possible to cycle through the possible completions |
1625 | * inside the entry. |
1626 | */ |
1627 | void |
1628 | gtk_entry_completion_set_inline_selection (GtkEntryCompletion *completion, |
1629 | gboolean inline_selection) |
1630 | { |
1631 | g_return_if_fail (GTK_IS_ENTRY_COMPLETION (completion)); |
1632 | |
1633 | inline_selection = inline_selection != FALSE; |
1634 | |
1635 | if (completion->inline_selection != inline_selection) |
1636 | { |
1637 | completion->inline_selection = inline_selection; |
1638 | |
1639 | g_object_notify_by_pspec (G_OBJECT (completion), pspec: entry_completion_props[PROP_INLINE_SELECTION]); |
1640 | } |
1641 | } |
1642 | |
1643 | /** |
1644 | * gtk_entry_completion_get_inline_selection: (attributes org.gtk.Method.get_property=inline-selection) |
1645 | * @completion: a `GtkEntryCompletion` |
1646 | * |
1647 | * Returns %TRUE if inline-selection mode is turned on. |
1648 | * |
1649 | * Returns: %TRUE if inline-selection mode is on |
1650 | */ |
1651 | gboolean |
1652 | gtk_entry_completion_get_inline_selection (GtkEntryCompletion *completion) |
1653 | { |
1654 | g_return_val_if_fail (GTK_IS_ENTRY_COMPLETION (completion), FALSE); |
1655 | |
1656 | return completion->inline_selection; |
1657 | } |
1658 | |
1659 | |
1660 | static int |
1661 | gtk_entry_completion_timeout (gpointer data) |
1662 | { |
1663 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (data); |
1664 | |
1665 | completion->completion_timeout = 0; |
1666 | |
1667 | if (completion->filter_model && |
1668 | g_utf8_strlen (p: gtk_editable_get_text (GTK_EDITABLE (completion->entry)), max: -1) |
1669 | >= completion->minimum_key_length) |
1670 | { |
1671 | int matches; |
1672 | gboolean ; |
1673 | |
1674 | gtk_entry_completion_complete (completion); |
1675 | matches = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (completion->filter_model), NULL); |
1676 | gtk_tree_selection_unselect_all (selection: gtk_tree_view_get_selection (GTK_TREE_VIEW (completion->tree_view))); |
1677 | |
1678 | g_object_get (object: completion, first_property_name: "popup-single-match" , &popup_single, NULL); |
1679 | if (matches > (popup_single ? 0: 1)) |
1680 | { |
1681 | if (gtk_widget_get_visible (widget: completion->popup_window)) |
1682 | _gtk_entry_completion_resize_popup (completion); |
1683 | else |
1684 | gtk_entry_completion_popup (completion); |
1685 | } |
1686 | else |
1687 | _gtk_entry_completion_popdown (completion); |
1688 | } |
1689 | else if (gtk_widget_get_visible (widget: completion->popup_window)) |
1690 | _gtk_entry_completion_popdown (completion); |
1691 | return G_SOURCE_REMOVE; |
1692 | } |
1693 | |
1694 | static inline gboolean |
1695 | keyval_is_cursor_move (guint keyval) |
1696 | { |
1697 | if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up) |
1698 | return TRUE; |
1699 | |
1700 | if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down) |
1701 | return TRUE; |
1702 | |
1703 | if (keyval == GDK_KEY_Page_Up) |
1704 | return TRUE; |
1705 | |
1706 | if (keyval == GDK_KEY_Page_Down) |
1707 | return TRUE; |
1708 | |
1709 | return FALSE; |
1710 | } |
1711 | |
1712 | static gboolean |
1713 | gtk_entry_completion_key_pressed (GtkEventControllerKey *controller, |
1714 | guint keyval, |
1715 | guint keycode, |
1716 | GdkModifierType state, |
1717 | gpointer user_data) |
1718 | { |
1719 | int matches; |
1720 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (user_data); |
1721 | GtkWidget *widget = completion->entry; |
1722 | GtkText *text = gtk_entry_get_text_widget (GTK_ENTRY (widget)); |
1723 | |
1724 | if (!completion->popup_completion) |
1725 | return FALSE; |
1726 | |
1727 | if (keyval == GDK_KEY_Return || |
1728 | keyval == GDK_KEY_KP_Enter || |
1729 | keyval == GDK_KEY_ISO_Enter || |
1730 | keyval == GDK_KEY_Escape) |
1731 | { |
1732 | if (completion->completion_timeout) |
1733 | { |
1734 | g_source_remove (tag: completion->completion_timeout); |
1735 | completion->completion_timeout = 0; |
1736 | } |
1737 | } |
1738 | |
1739 | if (!gtk_widget_get_mapped (widget: completion->popup_window)) |
1740 | return FALSE; |
1741 | |
1742 | matches = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (completion->filter_model), NULL); |
1743 | |
1744 | if (keyval_is_cursor_move (keyval)) |
1745 | { |
1746 | GtkTreePath *path = NULL; |
1747 | |
1748 | if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up) |
1749 | { |
1750 | if (completion->current_selected < 0) |
1751 | completion->current_selected = matches - 1; |
1752 | else |
1753 | completion->current_selected--; |
1754 | } |
1755 | else if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down) |
1756 | { |
1757 | if (completion->current_selected < matches - 1) |
1758 | completion->current_selected++; |
1759 | else |
1760 | completion->current_selected = -1; |
1761 | } |
1762 | else if (keyval == GDK_KEY_Page_Up) |
1763 | { |
1764 | if (completion->current_selected < 0) |
1765 | completion->current_selected = matches - 1; |
1766 | else if (completion->current_selected == 0) |
1767 | completion->current_selected = -1; |
1768 | else if (completion->current_selected < matches) |
1769 | { |
1770 | completion->current_selected -= PAGE_STEP; |
1771 | if (completion->current_selected < 0) |
1772 | completion->current_selected = 0; |
1773 | } |
1774 | else |
1775 | { |
1776 | completion->current_selected -= PAGE_STEP; |
1777 | if (completion->current_selected < matches - 1) |
1778 | completion->current_selected = matches - 1; |
1779 | } |
1780 | } |
1781 | else if (keyval == GDK_KEY_Page_Down) |
1782 | { |
1783 | if (completion->current_selected < 0) |
1784 | completion->current_selected = 0; |
1785 | else if (completion->current_selected < matches - 1) |
1786 | { |
1787 | completion->current_selected += PAGE_STEP; |
1788 | if (completion->current_selected > matches - 1) |
1789 | completion->current_selected = matches - 1; |
1790 | } |
1791 | else if (completion->current_selected == matches - 1) |
1792 | { |
1793 | completion->current_selected = -1; |
1794 | } |
1795 | else |
1796 | { |
1797 | completion->current_selected += PAGE_STEP; |
1798 | if (completion->current_selected > matches - 1) |
1799 | completion->current_selected = matches - 1; |
1800 | } |
1801 | } |
1802 | |
1803 | if (completion->current_selected < 0) |
1804 | { |
1805 | gtk_tree_selection_unselect_all (selection: gtk_tree_view_get_selection (GTK_TREE_VIEW (completion->tree_view))); |
1806 | |
1807 | if (completion->inline_selection && |
1808 | completion->completion_prefix) |
1809 | { |
1810 | gtk_editable_set_text (GTK_EDITABLE (completion->entry), |
1811 | text: completion->completion_prefix); |
1812 | gtk_editable_set_position (GTK_EDITABLE (widget), position: -1); |
1813 | } |
1814 | } |
1815 | else if (completion->current_selected < matches) |
1816 | { |
1817 | path = gtk_tree_path_new_from_indices (first_index: completion->current_selected, -1); |
1818 | gtk_tree_view_set_cursor (GTK_TREE_VIEW (completion->tree_view), |
1819 | path, NULL, FALSE); |
1820 | |
1821 | if (completion->inline_selection) |
1822 | { |
1823 | |
1824 | GtkTreeIter iter; |
1825 | GtkTreeIter child_iter; |
1826 | GtkTreeModel *model = NULL; |
1827 | GtkTreeSelection *sel; |
1828 | gboolean entry_set; |
1829 | |
1830 | sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (completion->tree_view)); |
1831 | if (!gtk_tree_selection_get_selected (selection: sel, model: &model, iter: &iter)) |
1832 | return FALSE; |
1833 | gtk_tree_model_filter_convert_iter_to_child_iter (GTK_TREE_MODEL_FILTER (model), child_iter: &child_iter, filter_iter: &iter); |
1834 | model = gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER (model)); |
1835 | |
1836 | if (completion->completion_prefix == NULL) |
1837 | completion->completion_prefix = g_strdup (str: gtk_editable_get_text (GTK_EDITABLE (completion->entry))); |
1838 | |
1839 | g_signal_emit_by_name (instance: completion, detailed_signal: "cursor-on-match" , model, |
1840 | &child_iter, &entry_set); |
1841 | } |
1842 | } |
1843 | |
1844 | gtk_tree_path_free (path); |
1845 | |
1846 | return TRUE; |
1847 | } |
1848 | else if (keyval == GDK_KEY_Escape || |
1849 | keyval == GDK_KEY_Left || |
1850 | keyval == GDK_KEY_KP_Left || |
1851 | keyval == GDK_KEY_Right || |
1852 | keyval == GDK_KEY_KP_Right) |
1853 | { |
1854 | gboolean retval = TRUE; |
1855 | |
1856 | gtk_entry_reset_im_context (GTK_ENTRY (widget)); |
1857 | _gtk_entry_completion_popdown (completion); |
1858 | |
1859 | if (completion->current_selected < 0) |
1860 | { |
1861 | retval = FALSE; |
1862 | goto keypress_completion_out; |
1863 | } |
1864 | else if (completion->inline_selection) |
1865 | { |
1866 | /* Escape rejects the tentative completion */ |
1867 | if (keyval == GDK_KEY_Escape) |
1868 | { |
1869 | if (completion->completion_prefix) |
1870 | gtk_editable_set_text (GTK_EDITABLE (completion->entry), |
1871 | text: completion->completion_prefix); |
1872 | else |
1873 | gtk_editable_set_text (GTK_EDITABLE (completion->entry), text: "" ); |
1874 | } |
1875 | |
1876 | /* Move the cursor to the end for Right/Esc */ |
1877 | if (keyval == GDK_KEY_Right || |
1878 | keyval == GDK_KEY_KP_Right || |
1879 | keyval == GDK_KEY_Escape) |
1880 | gtk_editable_set_position (GTK_EDITABLE (widget), position: -1); |
1881 | /* Let the default keybindings run for Left, i.e. either move to the |
1882 | * * previous character or select word if a modifier is used */ |
1883 | else |
1884 | retval = FALSE; |
1885 | } |
1886 | |
1887 | keypress_completion_out: |
1888 | if (completion->inline_selection) |
1889 | g_clear_pointer (&completion->completion_prefix, g_free); |
1890 | |
1891 | return retval; |
1892 | } |
1893 | else if (keyval == GDK_KEY_Tab || |
1894 | keyval == GDK_KEY_KP_Tab || |
1895 | keyval == GDK_KEY_ISO_Left_Tab) |
1896 | { |
1897 | gtk_entry_reset_im_context (GTK_ENTRY (widget)); |
1898 | _gtk_entry_completion_popdown (completion); |
1899 | |
1900 | g_clear_pointer (&completion->completion_prefix, g_free); |
1901 | |
1902 | return FALSE; |
1903 | } |
1904 | else if (keyval == GDK_KEY_ISO_Enter || |
1905 | keyval == GDK_KEY_KP_Enter || |
1906 | keyval == GDK_KEY_Return) |
1907 | { |
1908 | GtkTreeIter iter; |
1909 | GtkTreeModel *model = NULL; |
1910 | GtkTreeModel *child_model; |
1911 | GtkTreeIter child_iter; |
1912 | GtkTreeSelection *sel; |
1913 | gboolean retval = TRUE; |
1914 | |
1915 | gtk_entry_reset_im_context (GTK_ENTRY (widget)); |
1916 | _gtk_entry_completion_popdown (completion); |
1917 | |
1918 | if (completion->current_selected < matches) |
1919 | { |
1920 | gboolean entry_set; |
1921 | |
1922 | sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (completion->tree_view)); |
1923 | if (gtk_tree_selection_get_selected (selection: sel, model: &model, iter: &iter)) |
1924 | { |
1925 | gtk_tree_model_filter_convert_iter_to_child_iter (GTK_TREE_MODEL_FILTER (model), child_iter: &child_iter, filter_iter: &iter); |
1926 | child_model = gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER (model)); |
1927 | g_signal_handler_block (instance: text, handler_id: completion->changed_id); |
1928 | g_signal_emit_by_name (instance: completion, detailed_signal: "match-selected" , |
1929 | child_model, &child_iter, &entry_set); |
1930 | g_signal_handler_unblock (instance: text, handler_id: completion->changed_id); |
1931 | |
1932 | if (!entry_set) |
1933 | { |
1934 | char *str = NULL; |
1935 | |
1936 | gtk_tree_model_get (tree_model: model, iter: &iter, |
1937 | completion->text_column, &str, |
1938 | -1); |
1939 | |
1940 | gtk_editable_set_text (GTK_EDITABLE (widget), text: str); |
1941 | |
1942 | /* move the cursor to the end */ |
1943 | gtk_editable_set_position (GTK_EDITABLE (widget), position: -1); |
1944 | g_free (mem: str); |
1945 | } |
1946 | } |
1947 | else |
1948 | retval = FALSE; |
1949 | } |
1950 | |
1951 | g_clear_pointer (&completion->completion_prefix, g_free); |
1952 | |
1953 | return retval; |
1954 | } |
1955 | |
1956 | g_clear_pointer (&completion->completion_prefix, g_free); |
1957 | |
1958 | return FALSE; |
1959 | } |
1960 | |
1961 | static void |
1962 | gtk_entry_completion_changed (GtkWidget *widget, |
1963 | gpointer user_data) |
1964 | { |
1965 | GtkEntryCompletion *completion = GTK_ENTRY_COMPLETION (user_data); |
1966 | |
1967 | if (!completion->popup_completion) |
1968 | return; |
1969 | |
1970 | /* (re)install completion timeout */ |
1971 | if (completion->completion_timeout) |
1972 | { |
1973 | g_source_remove (tag: completion->completion_timeout); |
1974 | completion->completion_timeout = 0; |
1975 | } |
1976 | |
1977 | if (!gtk_editable_get_text (GTK_EDITABLE (widget))) |
1978 | return; |
1979 | |
1980 | /* no need to normalize for this test */ |
1981 | if (completion->minimum_key_length > 0 && |
1982 | strcmp (s1: "" , s2: gtk_editable_get_text (GTK_EDITABLE (widget))) == 0) |
1983 | { |
1984 | if (gtk_widget_get_visible (widget: completion->popup_window)) |
1985 | _gtk_entry_completion_popdown (completion); |
1986 | return; |
1987 | } |
1988 | |
1989 | completion->completion_timeout = |
1990 | g_timeout_add (COMPLETION_TIMEOUT, |
1991 | function: gtk_entry_completion_timeout, |
1992 | data: completion); |
1993 | gdk_source_set_static_name_by_id (tag: completion->completion_timeout, name: "[gtk] gtk_entry_completion_timeout" ); |
1994 | } |
1995 | |
1996 | static gboolean |
1997 | check_completion_callback (GtkEntryCompletion *completion) |
1998 | { |
1999 | completion->check_completion_idle = NULL; |
2000 | |
2001 | gtk_entry_completion_complete (completion); |
2002 | gtk_entry_completion_insert_prefix (completion); |
2003 | |
2004 | return FALSE; |
2005 | } |
2006 | |
2007 | static void |
2008 | clear_completion_callback (GObject *text, |
2009 | GParamSpec *pspec, |
2010 | GtkEntryCompletion *completion) |
2011 | { |
2012 | if (!completion->inline_completion) |
2013 | return; |
2014 | |
2015 | if (pspec->name == I_("cursor-position" ) || |
2016 | pspec->name == I_("selection-bound" )) |
2017 | completion->has_completion = FALSE; |
2018 | } |
2019 | |
2020 | static gboolean |
2021 | accept_completion_callback (GtkEntryCompletion *completion) |
2022 | { |
2023 | if (!completion->inline_completion) |
2024 | return FALSE; |
2025 | |
2026 | if (completion->has_completion) |
2027 | gtk_editable_set_position (GTK_EDITABLE (completion->entry), |
2028 | position: gtk_entry_buffer_get_length (buffer: gtk_entry_get_buffer (GTK_ENTRY (completion->entry)))); |
2029 | |
2030 | return FALSE; |
2031 | } |
2032 | |
2033 | static void |
2034 | text_focus_out (GtkEntryCompletion *completion) |
2035 | { |
2036 | if (!gtk_widget_get_mapped (widget: completion->popup_window)) |
2037 | accept_completion_callback (completion); |
2038 | } |
2039 | |
2040 | static void |
2041 | completion_inserted_text_callback (GtkEntryBuffer *buffer, |
2042 | guint position, |
2043 | const char *text, |
2044 | guint length, |
2045 | GtkEntryCompletion *completion) |
2046 | { |
2047 | if (!completion->inline_completion) |
2048 | return; |
2049 | |
2050 | /* idle to update the selection based on the file list */ |
2051 | if (completion->check_completion_idle == NULL) |
2052 | { |
2053 | completion->check_completion_idle = g_idle_source_new (); |
2054 | g_source_set_priority (source: completion->check_completion_idle, G_PRIORITY_HIGH); |
2055 | g_source_set_closure (source: completion->check_completion_idle, |
2056 | closure: g_cclosure_new_object (G_CALLBACK (check_completion_callback), |
2057 | G_OBJECT (completion))); |
2058 | g_source_attach (source: completion->check_completion_idle, NULL); |
2059 | g_source_set_static_name (completion->check_completion_idle, "[gtk] check_completion_callback" ); |
2060 | } |
2061 | } |
2062 | |
2063 | static void |
2064 | connect_completion_signals (GtkEntryCompletion *completion) |
2065 | { |
2066 | GtkEventController *controller; |
2067 | GtkText *text = gtk_entry_get_text_widget (GTK_ENTRY (completion->entry)); |
2068 | GtkEntryBuffer *buffer = gtk_text_get_buffer (self: text); |
2069 | |
2070 | controller = completion->entry_key_controller = gtk_event_controller_key_new (); |
2071 | gtk_event_controller_set_name (controller, name: "gtk-entry-completion" ); |
2072 | g_signal_connect (controller, "key-pressed" , |
2073 | G_CALLBACK (gtk_entry_completion_key_pressed), completion); |
2074 | gtk_widget_add_controller (GTK_WIDGET (text), controller); |
2075 | controller = completion->entry_focus_controller = gtk_event_controller_focus_new (); |
2076 | gtk_event_controller_set_name (controller, name: "gtk-entry-completion" ); |
2077 | g_signal_connect_swapped (controller, "leave" , G_CALLBACK (text_focus_out), completion); |
2078 | gtk_widget_add_controller (GTK_WIDGET (text), controller); |
2079 | |
2080 | completion->changed_id = |
2081 | g_signal_connect (text, "changed" , G_CALLBACK (gtk_entry_completion_changed), completion); |
2082 | |
2083 | completion->insert_text_id = |
2084 | g_signal_connect (buffer, "inserted-text" , G_CALLBACK (completion_inserted_text_callback), completion); |
2085 | g_signal_connect (text, "notify" , G_CALLBACK (clear_completion_callback), completion); |
2086 | g_signal_connect_swapped (text, "activate" , G_CALLBACK (accept_completion_callback), completion); |
2087 | } |
2088 | |
2089 | static void |
2090 | disconnect_completion_signals (GtkEntryCompletion *completion) |
2091 | { |
2092 | GtkText *text = gtk_entry_get_text_widget (GTK_ENTRY (completion->entry)); |
2093 | GtkEntryBuffer *buffer = gtk_text_get_buffer (self: text); |
2094 | |
2095 | gtk_widget_remove_controller (GTK_WIDGET (text), controller: completion->entry_key_controller); |
2096 | gtk_widget_remove_controller (GTK_WIDGET (text), controller: completion->entry_focus_controller); |
2097 | |
2098 | if (completion->changed_id > 0 && |
2099 | g_signal_handler_is_connected (instance: text, handler_id: completion->changed_id)) |
2100 | { |
2101 | g_signal_handler_disconnect (instance: text, handler_id: completion->changed_id); |
2102 | completion->changed_id = 0; |
2103 | } |
2104 | if (completion->insert_text_id > 0 && |
2105 | g_signal_handler_is_connected (instance: buffer, handler_id: completion->insert_text_id)) |
2106 | { |
2107 | g_signal_handler_disconnect (instance: buffer, handler_id: completion->insert_text_id); |
2108 | completion->insert_text_id = 0; |
2109 | } |
2110 | g_signal_handlers_disconnect_by_func (text, G_CALLBACK (clear_completion_callback), completion); |
2111 | g_signal_handlers_disconnect_by_func (text, G_CALLBACK (accept_completion_callback), completion); |
2112 | } |
2113 | |
2114 | void |
2115 | _gtk_entry_completion_disconnect (GtkEntryCompletion *completion) |
2116 | { |
2117 | if (completion->completion_timeout) |
2118 | { |
2119 | g_source_remove (tag: completion->completion_timeout); |
2120 | completion->completion_timeout = 0; |
2121 | } |
2122 | if (completion->check_completion_idle) |
2123 | { |
2124 | g_source_destroy (source: completion->check_completion_idle); |
2125 | completion->check_completion_idle = NULL; |
2126 | } |
2127 | |
2128 | if (gtk_widget_get_mapped (widget: completion->popup_window)) |
2129 | _gtk_entry_completion_popdown (completion); |
2130 | |
2131 | disconnect_completion_signals (completion); |
2132 | |
2133 | gtk_widget_unparent (widget: completion->popup_window); |
2134 | |
2135 | completion->entry = NULL; |
2136 | } |
2137 | |
2138 | void |
2139 | _gtk_entry_completion_connect (GtkEntryCompletion *completion, |
2140 | GtkEntry *entry) |
2141 | { |
2142 | completion->entry = GTK_WIDGET (entry); |
2143 | |
2144 | gtk_widget_set_parent (widget: completion->popup_window, GTK_WIDGET (entry)); |
2145 | |
2146 | connect_completion_signals (completion); |
2147 | } |
2148 | |