| 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 | *  |
| 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 | |