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 | |
95 | enum { |
96 | ACTIVATE, |
97 | SEARCH_CHANGED, |
98 | NEXT_MATCH, |
99 | PREVIOUS_MATCH, |
100 | STOP_SEARCH, |
101 | SEARCH_STARTED, |
102 | LAST_SIGNAL |
103 | }; |
104 | |
105 | enum { |
106 | PROP_0, |
107 | PROP_PLACEHOLDER_TEXT, |
108 | PROP_ACTIVATES_DEFAULT, |
109 | NUM_PROPERTIES, |
110 | }; |
111 | |
112 | static guint signals[LAST_SIGNAL] = { 0 }; |
113 | |
114 | static GParamSpec *props[NUM_PROPERTIES] = { NULL, }; |
115 | |
116 | typedef struct _GtkSearchEntryClass GtkSearchEntryClass; |
117 | |
118 | struct _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 | |
133 | struct _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 | |
144 | static void gtk_search_entry_editable_init (GtkEditableInterface *iface); |
145 | static void gtk_search_entry_accessible_init (GtkAccessibleInterface *iface); |
146 | |
147 | G_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 | |
156 | static void |
157 | text_changed (GtkSearchEntry *entry) |
158 | { |
159 | entry->content_changed = TRUE; |
160 | } |
161 | |
162 | static void |
163 | gtk_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 | |
182 | static void |
183 | gtk_search_entry_stop_search (GtkSearchEntry *entry) |
184 | { |
185 | entry->search_stopped = TRUE; |
186 | } |
187 | |
188 | static void |
189 | gtk_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 | |
232 | static void |
233 | gtk_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 | |
258 | static gboolean |
259 | gtk_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 | |
266 | static gboolean |
267 | gtk_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 | |
277 | static void |
278 | gtk_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 | |
454 | static GtkEditable * |
455 | gtk_search_entry_get_delegate (GtkEditable *editable) |
456 | { |
457 | return GTK_EDITABLE (GTK_SEARCH_ENTRY (editable)->entry); |
458 | } |
459 | |
460 | static void |
461 | gtk_search_entry_editable_init (GtkEditableInterface *iface) |
462 | { |
463 | iface->get_delegate = gtk_search_entry_get_delegate; |
464 | } |
465 | |
466 | static gboolean |
467 | gtk_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 | |
485 | static void |
486 | gtk_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 | |
493 | static void |
494 | gtk_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 | |
503 | static void |
504 | gtk_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 | |
513 | static gboolean |
514 | gtk_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 | |
524 | static void |
525 | reset_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 | |
535 | static void |
536 | gtk_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 | |
564 | static void |
565 | notify_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 | |
575 | static void |
576 | activate_cb (GtkText *text, |
577 | gpointer data) |
578 | { |
579 | g_signal_emit (instance: data, signal_id: signals[ACTIVATE], detail: 0); |
580 | } |
581 | |
582 | static void |
583 | catchall_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 | |
592 | static void |
593 | gtk_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 | */ |
644 | GtkWidget * |
645 | gtk_search_entry_new (void) |
646 | { |
647 | return GTK_WIDGET (g_object_new (GTK_TYPE_SEARCH_ENTRY, NULL)); |
648 | } |
649 | |
650 | gboolean |
651 | gtk_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 | |
672 | static gboolean |
673 | capture_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 | */ |
727 | void |
728 | gtk_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 | */ |
768 | GtkWidget * |
769 | gtk_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 | |
776 | GtkEventController * |
777 | gtk_search_entry_get_key_controller (GtkSearchEntry *entry) |
778 | { |
779 | return gtk_text_get_key_controller (GTK_TEXT (entry->entry)); |
780 | } |
781 | |
782 | GtkText * |
783 | gtk_search_entry_get_text_widget (GtkSearchEntry *entry) |
784 | { |
785 | return GTK_TEXT (entry->entry); |
786 | } |
787 | |