1/* GTK - The GIMP Toolkit
2 * Copyright (C) 2012 Red Hat, Inc.
3 *
4 * Authors:
5 * - Bastien Nocera <bnocera@redhat.com>
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21/*
22 * Modified by the GTK+ Team and others 2012. See the AUTHORS
23 * file for a list of people on the GTK+ Team. See the ChangeLog
24 * files for a list of changes. These files are distributed with
25 * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
26 */
27
28#include "config.h"
29
30#include "gtksearchentryprivate.h"
31
32#include "gtkaccessibleprivate.h"
33#include "gtkeditable.h"
34#include "gtkboxlayout.h"
35#include "gtkgestureclick.h"
36#include "gtktextprivate.h"
37#include "gtkimage.h"
38#include "gtkintl.h"
39#include "gtkprivate.h"
40#include "gtkmarshalers.h"
41#include "gtkstylecontext.h"
42#include "gtkeventcontrollerkey.h"
43#include "gtkwidgetprivate.h"
44
45
46/**
47 * GtkSearchEntry:
48 *
49 * `GtkSearchEntry` is an entry widget that has been tailored for use
50 * as a search entry.
51 *
52 * The main API for interacting with a `GtkSearchEntry` as entry
53 * is the `GtkEditable` interface.
54 *
55 * ![An example GtkSearchEntry](search-entry.png)
56 *
57 * It will show an inactive symbolic “find” icon when the search
58 * entry is empty, and a symbolic “clear” icon when there is text.
59 * Clicking on the “clear” icon will empty the search entry.
60 *
61 * To make filtering appear more reactive, it is a good idea to
62 * not react to every change in the entry text immediately, but
63 * only after a short delay. To support this, `GtkSearchEntry`
64 * emits the [signal@Gtk.SearchEntry::search-changed] signal which
65 * can be used instead of the [signal@Gtk.Editable::changed] signal.
66 *
67 * The [signal@Gtk.SearchEntry::previous-match],
68 * [signal@Gtk.SearchEntry::next-match] and
69 * [signal@Gtk.SearchEntry::stop-search] signals can be used to
70 * implement moving between search results and ending the search.
71 *
72 * Often, `GtkSearchEntry` will be fed events by means of being
73 * placed inside a [class@Gtk.SearchBar]. If that is not the case,
74 * you can use [method@Gtk.SearchEntry.set_key_capture_widget] to
75 * let it capture key input from another widget.
76 *
77 * `GtkSearchEntry` provides only minimal API and should be used with
78 * the [iface@Gtk.Editable] API.
79 *
80 * ## CSS Nodes
81 *
82 * ```
83 * entry.search
84 * ╰── text
85 * ```
86 *
87 * `GtkSearchEntry` has a single CSS node with name entry that carries
88 * a `.search` style class, and the text node is a child of that.
89 *
90 * ## Accessibility
91 *
92 * `GtkSearchEntry` uses the %GTK_ACCESSIBLE_ROLE_SEARCH_BOX role.
93 */
94
95enum {
96 ACTIVATE,
97 SEARCH_CHANGED,
98 NEXT_MATCH,
99 PREVIOUS_MATCH,
100 STOP_SEARCH,
101 SEARCH_STARTED,
102 LAST_SIGNAL
103};
104
105enum {
106 PROP_0,
107 PROP_PLACEHOLDER_TEXT,
108 PROP_ACTIVATES_DEFAULT,
109 NUM_PROPERTIES,
110};
111
112static guint signals[LAST_SIGNAL] = { 0 };
113
114static GParamSpec *props[NUM_PROPERTIES] = { NULL, };
115
116typedef struct _GtkSearchEntryClass GtkSearchEntryClass;
117
118struct _GtkSearchEntry
119{
120 GtkWidget parent;
121
122 GtkWidget *capture_widget;
123 GtkEventController *capture_widget_controller;
124
125 GtkWidget *entry;
126 GtkWidget *icon;
127
128 guint delayed_changed_id;
129 gboolean content_changed;
130 gboolean search_stopped;
131};
132
133struct _GtkSearchEntryClass
134{
135 GtkWidgetClass parent_class;
136
137 void (* activate) (GtkSearchEntry *entry);
138 void (* search_changed) (GtkSearchEntry *entry);
139 void (* next_match) (GtkSearchEntry *entry);
140 void (* previous_match) (GtkSearchEntry *entry);
141 void (* stop_search) (GtkSearchEntry *entry);
142};
143
144static void gtk_search_entry_editable_init (GtkEditableInterface *iface);
145static void gtk_search_entry_accessible_init (GtkAccessibleInterface *iface);
146
147G_DEFINE_TYPE_WITH_CODE (GtkSearchEntry, gtk_search_entry, GTK_TYPE_WIDGET,
148 G_IMPLEMENT_INTERFACE (GTK_TYPE_ACCESSIBLE,
149 gtk_search_entry_accessible_init)
150 G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE,
151 gtk_search_entry_editable_init))
152
153/* 150 mseconds of delay */
154#define DELAYED_TIMEOUT_ID 150
155
156static void
157text_changed (GtkSearchEntry *entry)
158{
159 entry->content_changed = TRUE;
160}
161
162static void
163gtk_search_entry_finalize (GObject *object)
164{
165 GtkSearchEntry *entry = GTK_SEARCH_ENTRY (object);
166
167 gtk_editable_finish_delegate (GTK_EDITABLE (entry));
168
169 gtk_widget_unparent (widget: gtk_widget_get_first_child (GTK_WIDGET (entry)));
170
171 g_clear_pointer (&entry->entry, gtk_widget_unparent);
172 g_clear_pointer (&entry->icon, gtk_widget_unparent);
173
174 if (entry->delayed_changed_id > 0)
175 g_source_remove (tag: entry->delayed_changed_id);
176
177 gtk_search_entry_set_key_capture_widget (GTK_SEARCH_ENTRY (object), NULL);
178
179 G_OBJECT_CLASS (gtk_search_entry_parent_class)->finalize (object);
180}
181
182static void
183gtk_search_entry_stop_search (GtkSearchEntry *entry)
184{
185 entry->search_stopped = TRUE;
186}
187
188static void
189gtk_search_entry_set_property (GObject *object,
190 guint prop_id,
191 const GValue *value,
192 GParamSpec *pspec)
193{
194 GtkSearchEntry *entry = GTK_SEARCH_ENTRY (object);
195 const char *text;
196
197 if (gtk_editable_delegate_set_property (object, prop_id, value, pspec))
198 {
199 if (prop_id == NUM_PROPERTIES + GTK_EDITABLE_PROP_EDITABLE)
200 {
201 gtk_accessible_update_property (self: GTK_ACCESSIBLE (ptr: entry),
202 first_property: GTK_ACCESSIBLE_PROPERTY_READ_ONLY, !g_value_get_boolean (value),
203 -1);
204 }
205
206 return;
207 }
208
209 switch (prop_id)
210 {
211 case PROP_PLACEHOLDER_TEXT:
212 text = g_value_get_string (value);
213 gtk_text_set_placeholder_text (GTK_TEXT (entry->entry), text);
214 gtk_accessible_update_property (self: GTK_ACCESSIBLE (ptr: entry),
215 first_property: GTK_ACCESSIBLE_PROPERTY_PLACEHOLDER, text,
216 -1);
217 break;
218
219 case PROP_ACTIVATES_DEFAULT:
220 if (gtk_text_get_activates_default (GTK_TEXT (entry->entry)) != g_value_get_boolean (value))
221 {
222 gtk_text_set_activates_default (GTK_TEXT (entry->entry), activates: g_value_get_boolean (value));
223 g_object_notify_by_pspec (object, pspec);
224 }
225 break;
226
227 default:
228 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
229 }
230}
231
232static void
233gtk_search_entry_get_property (GObject *object,
234 guint prop_id,
235 GValue *value,
236 GParamSpec *pspec)
237{
238 GtkSearchEntry *entry = GTK_SEARCH_ENTRY (object);
239
240 if (gtk_editable_delegate_get_property (object, prop_id, value, pspec))
241 return;
242
243 switch (prop_id)
244 {
245 case PROP_PLACEHOLDER_TEXT:
246 g_value_set_string (value, v_string: gtk_text_get_placeholder_text (GTK_TEXT (entry->entry)));
247 break;
248
249 case PROP_ACTIVATES_DEFAULT:
250 g_value_set_boolean (value, v_boolean: gtk_text_get_activates_default (GTK_TEXT (entry->entry)));
251 break;
252
253 default:
254 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
255 }
256}
257
258static gboolean
259gtk_search_entry_grab_focus (GtkWidget *widget)
260{
261 GtkSearchEntry *entry = GTK_SEARCH_ENTRY (widget);
262
263 return gtk_text_grab_focus_without_selecting (GTK_TEXT (entry->entry));
264}
265
266static gboolean
267gtk_search_entry_mnemonic_activate (GtkWidget *widget,
268 gboolean group_cycling)
269{
270 GtkSearchEntry *entry = GTK_SEARCH_ENTRY (widget);
271
272 gtk_widget_grab_focus (widget: entry->entry);
273
274 return TRUE;
275}
276
277static void
278gtk_search_entry_class_init (GtkSearchEntryClass *klass)
279{
280 GObjectClass *object_class = G_OBJECT_CLASS (klass);
281 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
282
283 object_class->finalize = gtk_search_entry_finalize;
284 object_class->get_property = gtk_search_entry_get_property;
285 object_class->set_property = gtk_search_entry_set_property;
286
287 widget_class->grab_focus = gtk_search_entry_grab_focus;
288 widget_class->focus = gtk_widget_focus_child;
289 widget_class->mnemonic_activate = gtk_search_entry_mnemonic_activate;
290
291 klass->stop_search = gtk_search_entry_stop_search;
292
293 /**
294 * GtkSearchEntry:placeholder-text:
295 *
296 * The text that will be displayed in the `GtkSearchEntry`
297 * when it is empty and unfocused.
298 */
299 props[PROP_PLACEHOLDER_TEXT] =
300 g_param_spec_string (name: "placeholder-text",
301 P_("Placeholder text"),
302 P_("Show text in the entry when it’s empty and unfocused"),
303 NULL,
304 GTK_PARAM_READWRITE);
305
306 /**
307 * GtkSearchEntry:activates-default:
308 *
309 * Whether to activate the default widget when Enter is pressed.
310 */
311 props[PROP_ACTIVATES_DEFAULT] =
312 g_param_spec_boolean (name: "activates-default",
313 P_("Activates default"),
314 P_("Whether to activate the default widget (such as the default button in a dialog) when Enter is pressed"),
315 FALSE,
316 GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
317
318 g_object_class_install_properties (oclass: object_class, n_pspecs: NUM_PROPERTIES, pspecs: props);
319 gtk_editable_install_properties (object_class, first_prop: NUM_PROPERTIES);
320
321 /**
322 * GtkSearchEntry::activate:
323 * @self: The widget on which the signal is emitted
324 *
325 * Emitted when the entry is activated.
326 *
327 * The keybindings for this signal are all forms of the Enter key.
328 */
329 signals[ACTIVATE] =
330 g_signal_new (I_("activate"),
331 G_OBJECT_CLASS_TYPE (object_class),
332 signal_flags: G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
333 G_STRUCT_OFFSET (GtkSearchEntryClass, activate),
334 NULL, NULL,
335 NULL,
336 G_TYPE_NONE, n_params: 0);
337
338 /**
339 * GtkSearchEntry::search-changed:
340 * @entry: the entry on which the signal was emitted
341 *
342 * Emitted with a short delay of 150 milliseconds after the
343 * last change to the entry text.
344 */
345 signals[SEARCH_CHANGED] =
346 g_signal_new (I_("search-changed"),
347 G_OBJECT_CLASS_TYPE (object_class),
348 signal_flags: G_SIGNAL_RUN_LAST,
349 G_STRUCT_OFFSET (GtkSearchEntryClass, search_changed),
350 NULL, NULL,
351 NULL,
352 G_TYPE_NONE, n_params: 0);
353
354 /**
355 * GtkSearchEntry::next-match:
356 * @entry: the entry on which the signal was emitted
357 *
358 * Emitted when the user initiates a move to the next match
359 * for the current search string.
360 *
361 * This is a [keybinding signal](class.SignalAction.html).
362 *
363 * Applications should connect to it, to implement moving
364 * between matches.
365 *
366 * The default bindings for this signal is Ctrl-g.
367 */
368 signals[NEXT_MATCH] =
369 g_signal_new (I_("next-match"),
370 G_OBJECT_CLASS_TYPE (object_class),
371 signal_flags: G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
372 G_STRUCT_OFFSET (GtkSearchEntryClass, next_match),
373 NULL, NULL,
374 NULL,
375 G_TYPE_NONE, n_params: 0);
376
377 /**
378 * GtkSearchEntry::previous-match:
379 * @entry: the entry on which the signal was emitted
380 *
381 * Emitted when the user initiates a move to the previous match
382 * for the current search string.
383 *
384 * This is a [keybinding signal](class.SignalAction.html).
385 *
386 * Applications should connect to it, to implement moving
387 * between matches.
388 *
389 * The default bindings for this signal is Ctrl-Shift-g.
390 */
391 signals[PREVIOUS_MATCH] =
392 g_signal_new (I_("previous-match"),
393 G_OBJECT_CLASS_TYPE (object_class),
394 signal_flags: G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
395 G_STRUCT_OFFSET (GtkSearchEntryClass, previous_match),
396 NULL, NULL,
397 NULL,
398 G_TYPE_NONE, n_params: 0);
399
400 /**
401 * GtkSearchEntry::stop-search:
402 * @entry: the entry on which the signal was emitted
403 *
404 * Emitted when the user stops a search via keyboard input.
405 *
406 * This is a [keybinding signal](class.SignalAction.html).
407 *
408 * Applications should connect to it, to implement hiding
409 * the search entry in this case.
410 *
411 * The default bindings for this signal is Escape.
412 */
413 signals[STOP_SEARCH] =
414 g_signal_new (I_("stop-search"),
415 G_OBJECT_CLASS_TYPE (object_class),
416 signal_flags: G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
417 G_STRUCT_OFFSET (GtkSearchEntryClass, stop_search),
418 NULL, NULL,
419 NULL,
420 G_TYPE_NONE, n_params: 0);
421
422 /**
423 * GtkSearchEntry::search-started:
424 * @entry: the entry on which the signal was emitted
425 *
426 * Emitted when the user initiated a search on the entry.
427 */
428 signals[SEARCH_STARTED] =
429 g_signal_new (I_("search-started"),
430 G_OBJECT_CLASS_TYPE (object_class),
431 signal_flags: G_SIGNAL_RUN_LAST, class_offset: 0,
432 NULL, NULL,
433 NULL,
434 G_TYPE_NONE, n_params: 0);
435
436 gtk_widget_class_add_binding_signal (widget_class,
437 GDK_KEY_g, mods: GDK_CONTROL_MASK,
438 signal: "next-match",
439 NULL);
440 gtk_widget_class_add_binding_signal (widget_class,
441 GDK_KEY_g, mods: GDK_SHIFT_MASK | GDK_CONTROL_MASK,
442 signal: "previous-match",
443 NULL);
444 gtk_widget_class_add_binding_signal (widget_class,
445 GDK_KEY_Escape, mods: 0,
446 signal: "stop-search",
447 NULL);
448
449 gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT);
450 gtk_widget_class_set_css_name (widget_class, I_("entry"));
451 gtk_widget_class_set_accessible_role (widget_class, accessible_role: GTK_ACCESSIBLE_ROLE_SEARCH_BOX);
452}
453
454static GtkEditable *
455gtk_search_entry_get_delegate (GtkEditable *editable)
456{
457 return GTK_EDITABLE (GTK_SEARCH_ENTRY (editable)->entry);
458}
459
460static void
461gtk_search_entry_editable_init (GtkEditableInterface *iface)
462{
463 iface->get_delegate = gtk_search_entry_get_delegate;
464}
465
466static gboolean
467gtk_search_entry_accessible_get_platform_state (GtkAccessible *self,
468 GtkAccessiblePlatformState state)
469{
470 GtkSearchEntry *entry = GTK_SEARCH_ENTRY (self);
471
472 switch (state)
473 {
474 case GTK_ACCESSIBLE_PLATFORM_STATE_FOCUSABLE:
475 return gtk_widget_get_focusable (GTK_WIDGET (entry->entry));
476 case GTK_ACCESSIBLE_PLATFORM_STATE_FOCUSED:
477 return gtk_widget_has_focus (GTK_WIDGET (entry->entry));
478 case GTK_ACCESSIBLE_PLATFORM_STATE_ACTIVE:
479 return FALSE;
480 default:
481 g_assert_not_reached ();
482 }
483}
484
485static void
486gtk_search_entry_accessible_init (GtkAccessibleInterface *iface)
487{
488 GtkAccessibleInterface *parent_iface = g_type_interface_peek_parent (g_iface: iface);
489 iface->get_at_context = parent_iface->get_at_context;
490 iface->get_platform_state = gtk_search_entry_accessible_get_platform_state;
491}
492
493static void
494gtk_search_entry_icon_press (GtkGestureClick *press,
495 int n_press,
496 double x,
497 double y,
498 GtkSearchEntry *entry)
499{
500 gtk_gesture_set_state (GTK_GESTURE (press), state: GTK_EVENT_SEQUENCE_CLAIMED);
501}
502
503static void
504gtk_search_entry_icon_release (GtkGestureClick *press,
505 int n_press,
506 double x,
507 double y,
508 GtkSearchEntry *entry)
509{
510 gtk_editable_set_text (GTK_EDITABLE (entry->entry), text: "");
511}
512
513static gboolean
514gtk_search_entry_changed_timeout_cb (gpointer user_data)
515{
516 GtkSearchEntry *entry = user_data;
517
518 g_signal_emit (instance: entry, signal_id: signals[SEARCH_CHANGED], detail: 0);
519 entry->delayed_changed_id = 0;
520
521 return G_SOURCE_REMOVE;
522}
523
524static void
525reset_timeout (GtkSearchEntry *entry)
526{
527 if (entry->delayed_changed_id > 0)
528 g_source_remove (tag: entry->delayed_changed_id);
529 entry->delayed_changed_id = g_timeout_add (DELAYED_TIMEOUT_ID,
530 function: gtk_search_entry_changed_timeout_cb,
531 data: entry);
532 gdk_source_set_static_name_by_id (tag: entry->delayed_changed_id, name: "[gtk] gtk_search_entry_changed_timeout_cb");
533}
534
535static void
536gtk_search_entry_changed (GtkEditable *editable,
537 GtkSearchEntry *entry)
538{
539 const char *str;
540
541 /* Update the icons first */
542 str = gtk_editable_get_text (GTK_EDITABLE (entry->entry));
543
544 if (str == NULL || *str == '\0')
545 {
546 gtk_widget_set_child_visible (widget: entry->icon, FALSE);
547
548 if (entry->delayed_changed_id > 0)
549 {
550 g_source_remove (tag: entry->delayed_changed_id);
551 entry->delayed_changed_id = 0;
552 }
553 g_signal_emit (instance: entry, signal_id: signals[SEARCH_CHANGED], detail: 0);
554 }
555 else
556 {
557 gtk_widget_set_child_visible (widget: entry->icon, TRUE);
558
559 /* Queue up the timeout */
560 reset_timeout (entry);
561 }
562}
563
564static void
565notify_cb (GObject *object,
566 GParamSpec *pspec,
567 gpointer data)
568{
569 /* The editable interface properties are already forwarded by the editable delegate setup */
570 if (g_str_equal (v1: pspec->name, v2: "placeholder-text") ||
571 g_str_equal (v1: pspec->name, v2: "activates-default"))
572 g_object_notify (object: data, property_name: pspec->name);
573}
574
575static void
576activate_cb (GtkText *text,
577 gpointer data)
578{
579 g_signal_emit (instance: data, signal_id: signals[ACTIVATE], detail: 0);
580}
581
582static void
583catchall_click_press (GtkGestureClick *gesture,
584 int n_press,
585 double x,
586 double y,
587 gpointer user_data)
588{
589 gtk_gesture_set_state (GTK_GESTURE (gesture), state: GTK_EVENT_SEQUENCE_CLAIMED);
590}
591
592static void
593gtk_search_entry_init (GtkSearchEntry *entry)
594{
595 GtkWidget *icon;
596 GtkGesture *press, *catchall;
597
598 /* The search icon is purely presentational */
599 icon = g_object_new (GTK_TYPE_IMAGE,
600 first_property_name: "accessible-role", GTK_ACCESSIBLE_ROLE_PRESENTATION,
601 "icon-name", "system-search-symbolic",
602 NULL);
603 gtk_widget_set_parent (widget: icon, GTK_WIDGET (entry));
604
605 entry->entry = gtk_text_new ();
606 gtk_widget_set_parent (widget: entry->entry, GTK_WIDGET (entry));
607 gtk_widget_set_hexpand (widget: entry->entry, TRUE);
608 gtk_editable_init_delegate (GTK_EDITABLE (entry));
609 g_signal_connect_swapped (entry->entry, "changed", G_CALLBACK (text_changed), entry);
610 g_signal_connect_after (entry->entry, "changed", G_CALLBACK (gtk_search_entry_changed), entry);
611 g_signal_connect_swapped (entry->entry, "preedit-changed", G_CALLBACK (text_changed), entry);
612 g_signal_connect (entry->entry, "notify", G_CALLBACK (notify_cb), entry);
613 g_signal_connect (entry->entry, "activate", G_CALLBACK (activate_cb), entry);
614
615 entry->icon = g_object_new (GTK_TYPE_IMAGE,
616 first_property_name: "accessible-role", GTK_ACCESSIBLE_ROLE_PRESENTATION,
617 "icon-name", "edit-clear-symbolic",
618 NULL);
619 gtk_widget_set_tooltip_text (widget: entry->icon, _("Clear entry"));
620 gtk_widget_set_parent (widget: entry->icon, GTK_WIDGET (entry));
621 gtk_widget_set_child_visible (widget: entry->icon, FALSE);
622
623 press = gtk_gesture_click_new ();
624 g_signal_connect (press, "pressed", G_CALLBACK (gtk_search_entry_icon_press), entry);
625 g_signal_connect (press, "released", G_CALLBACK (gtk_search_entry_icon_release), entry);
626 gtk_widget_add_controller (widget: entry->icon, GTK_EVENT_CONTROLLER (press));
627
628 catchall = gtk_gesture_click_new ();
629 g_signal_connect (catchall, "pressed",
630 G_CALLBACK (catchall_click_press), entry);
631 gtk_widget_add_controller (GTK_WIDGET (entry),
632 GTK_EVENT_CONTROLLER (catchall));
633
634 gtk_widget_add_css_class (GTK_WIDGET (entry), I_("search"));
635}
636
637/**
638 * gtk_search_entry_new:
639 *
640 * Creates a `GtkSearchEntry`.
641 *
642 * Returns: a new `GtkSearchEntry`
643 */
644GtkWidget *
645gtk_search_entry_new (void)
646{
647 return GTK_WIDGET (g_object_new (GTK_TYPE_SEARCH_ENTRY, NULL));
648}
649
650gboolean
651gtk_search_entry_is_keynav (guint keyval,
652 GdkModifierType state)
653{
654 if (keyval == GDK_KEY_Tab || keyval == GDK_KEY_KP_Tab ||
655 keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up ||
656 keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down ||
657 keyval == GDK_KEY_Left || keyval == GDK_KEY_KP_Left ||
658 keyval == GDK_KEY_Right || keyval == GDK_KEY_KP_Right ||
659 keyval == GDK_KEY_Home || keyval == GDK_KEY_KP_Home ||
660 keyval == GDK_KEY_End || keyval == GDK_KEY_KP_End ||
661 keyval == GDK_KEY_Page_Up || keyval == GDK_KEY_KP_Page_Up ||
662 keyval == GDK_KEY_Page_Down || keyval == GDK_KEY_KP_Page_Down ||
663 ((state & (GDK_CONTROL_MASK | GDK_ALT_MASK)) != 0))
664 return TRUE;
665
666 /* Other navigation events should get automatically
667 * ignored as they will not change the content of the entry
668 */
669 return FALSE;
670}
671
672static gboolean
673capture_widget_key_handled (GtkEventControllerKey *controller,
674 guint keyval,
675 guint keycode,
676 GdkModifierType state,
677 GtkWidget *widget)
678{
679 GtkSearchEntry *entry = GTK_SEARCH_ENTRY (widget);
680 gboolean handled, was_empty;
681
682 if (gtk_search_entry_is_keynav (keyval, state) ||
683 keyval == GDK_KEY_space ||
684 keyval == GDK_KEY_Menu)
685 return FALSE;
686
687 entry->content_changed = FALSE;
688 entry->search_stopped = FALSE;
689 was_empty = (gtk_text_get_text_length (GTK_TEXT (entry->entry)) == 0);
690
691 handled = gtk_event_controller_key_forward (controller, widget: entry->entry);
692
693 if (handled)
694 {
695 if (was_empty && entry->content_changed && !entry->search_stopped)
696 g_signal_emit (instance: entry, signal_id: signals[SEARCH_STARTED], detail: 0);
697
698 return GDK_EVENT_STOP;
699 }
700
701 return GDK_EVENT_PROPAGATE;
702}
703
704/**
705 * gtk_search_entry_set_key_capture_widget:
706 * @entry: a `GtkSearchEntry`
707 * @widget: (nullable) (transfer none): a `GtkWidget`
708 *
709 * Sets @widget as the widget that @entry will capture key
710 * events from.
711 *
712 * Key events are consumed by the search entry to start or
713 * continue a search.
714 *
715 * If the entry is part of a `GtkSearchBar`, it is preferable
716 * to call [method@Gtk.SearchBar.set_key_capture_widget] instead,
717 * which will reveal the entry in addition to triggering the
718 * search entry.
719 *
720 * Note that despite the name of this function, the events
721 * are only 'captured' in the bubble phase, which means that
722 * editable child widgets of @widget will receive text input
723 * before it gets captured. If that is not desired, you can
724 * capture and forward the events yourself with
725 * [method@Gtk.EventControllerKey.forward].
726 */
727void
728gtk_search_entry_set_key_capture_widget (GtkSearchEntry *entry,
729 GtkWidget *widget)
730{
731 g_return_if_fail (GTK_IS_SEARCH_ENTRY (entry));
732 g_return_if_fail (!widget || GTK_IS_WIDGET (widget));
733
734 if (entry->capture_widget)
735 {
736 gtk_widget_remove_controller (widget: entry->capture_widget,
737 controller: entry->capture_widget_controller);
738 g_object_remove_weak_pointer (G_OBJECT (entry->capture_widget),
739 weak_pointer_location: (gpointer *) &entry->capture_widget);
740 }
741
742 entry->capture_widget = widget;
743
744 if (widget)
745 {
746 g_object_add_weak_pointer (G_OBJECT (entry->capture_widget),
747 weak_pointer_location: (gpointer *) &entry->capture_widget);
748
749 entry->capture_widget_controller = gtk_event_controller_key_new ();
750 gtk_event_controller_set_propagation_phase (controller: entry->capture_widget_controller,
751 phase: GTK_PHASE_BUBBLE);
752 g_signal_connect (entry->capture_widget_controller, "key-pressed",
753 G_CALLBACK (capture_widget_key_handled), entry);
754 g_signal_connect (entry->capture_widget_controller, "key-released",
755 G_CALLBACK (capture_widget_key_handled), entry);
756 gtk_widget_add_controller (widget, controller: entry->capture_widget_controller);
757 }
758}
759
760/**
761 * gtk_search_entry_get_key_capture_widget:
762 * @entry: a `GtkSearchEntry`
763 *
764 * Gets the widget that @entry is capturing key events from.
765 *
766 * Returns: (nullable) (transfer none): The key capture widget.
767 */
768GtkWidget *
769gtk_search_entry_get_key_capture_widget (GtkSearchEntry *entry)
770{
771 g_return_val_if_fail (GTK_IS_SEARCH_ENTRY (entry), NULL);
772
773 return entry->capture_widget;
774}
775
776GtkEventController *
777gtk_search_entry_get_key_controller (GtkSearchEntry *entry)
778{
779 return gtk_text_get_key_controller (GTK_TEXT (entry->entry));
780}
781
782GtkText *
783gtk_search_entry_get_text_widget (GtkSearchEntry *entry)
784{
785 return GTK_TEXT (entry->entry);
786}
787

source code of gtk/gtk/gtksearchentry.c