No issues found
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2002 Jorn Baayen <jorn@nl.linux.org>
4 * Copyright (C) 2003 Colin Walters <walters@verbum.org>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * The Rhythmbox authors hereby grant permission for non-GPL compatible
12 * GStreamer plugins to be used and distributed together with GStreamer
13 * and Rhythmbox. This permission is above and beyond the permissions granted
14 * by the GPL license by which Rhythmbox is covered. If you modify this code
15 * you may extend this exception to your version of the code, but you are not
16 * obligated to do so. If you do not wish to do so, delete this exception
17 * statement from your version.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, write to the Free Software
26 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
27 *
28 */
29
30 #include <config.h>
31
32 #include <string.h>
33
34 #include <glib/gi18n.h>
35 #include <gtk/gtk.h>
36
37 #include "rb-search-entry.h"
38 #include "rb-util.h"
39
40 static void rb_search_entry_class_init (RBSearchEntryClass *klass);
41 static void rb_search_entry_init (RBSearchEntry *entry);
42 static void rb_search_entry_constructed (GObject *object);
43 static void rb_search_entry_finalize (GObject *object);
44 static gboolean rb_search_entry_timeout_cb (RBSearchEntry *entry);
45 static void rb_search_entry_changed_cb (GtkEditable *editable,
46 RBSearchEntry *entry);
47 static void rb_search_entry_activate_cb (GtkEntry *gtkentry,
48 RBSearchEntry *entry);
49 static void rb_search_entry_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
50 static void rb_search_entry_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
51 static void button_clicked_cb (GtkButton *button, RBSearchEntry *entry);
52 static gboolean rb_search_entry_focus_out_event_cb (GtkWidget *widget,
53 GdkEventFocus *event,
54 RBSearchEntry *entry);
55 static void rb_search_entry_clear_cb (GtkEntry *entry,
56 GtkEntryIconPosition icon_pos,
57 GdkEvent *event,
58 RBSearchEntry *search_entry);
59 static void rb_search_entry_update_icons (RBSearchEntry *entry);
60 static void rb_search_entry_widget_grab_focus (GtkWidget *widget);
61
62 struct RBSearchEntryPrivate
63 {
64 GtkWidget *entry;
65 GtkWidget *button;
66
67 gboolean has_popup;
68 gboolean explicit_mode;
69 gboolean clearing;
70 gboolean searching;
71
72 guint timeout;
73 };
74
75 G_DEFINE_TYPE (RBSearchEntry, rb_search_entry, GTK_TYPE_HBOX)
76 #define RB_SEARCH_ENTRY_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_SEARCH_ENTRY, RBSearchEntryPrivate))
77
78 /**
79 * SECTION:rb-search-entry
80 * @short_description: text entry widget for the search box
81 *
82 * The search entry contains a label and a text entry box.
83 * The text entry box contains an icon that acts as a 'clear'
84 * button.
85 *
86 * Signals are emitted when the search text changes,
87 * arbitrarily rate-limited to one every 300ms.
88 */
89
90 enum
91 {
92 SEARCH,
93 ACTIVATE,
94 SHOW_POPUP,
95 LAST_SIGNAL
96 };
97
98 enum
99 {
100 PROP_0,
101 PROP_EXPLICIT_MODE,
102 PROP_HAS_POPUP
103 };
104
105 static guint rb_search_entry_signals[LAST_SIGNAL] = { 0 };
106
107 static void
108 rb_search_entry_class_init (RBSearchEntryClass *klass)
109 {
110 GObjectClass *object_class = G_OBJECT_CLASS (klass);
111 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
112
113 object_class->constructed = rb_search_entry_constructed;
114 object_class->finalize = rb_search_entry_finalize;
115 object_class->set_property = rb_search_entry_set_property;
116 object_class->get_property = rb_search_entry_get_property;
117
118 widget_class->grab_focus = rb_search_entry_widget_grab_focus;
119
120 /**
121 * RBSearchEntry::search:
122 * @entry: the #RBSearchEntry
123 * @text: search text
124 *
125 * Emitted when the search text changes. A signal
126 * handler must initiate a search on the current
127 * source.
128 */
129 rb_search_entry_signals[SEARCH] =
130 g_signal_new ("search",
131 G_OBJECT_CLASS_TYPE (object_class),
132 G_SIGNAL_RUN_LAST,
133 G_STRUCT_OFFSET (RBSearchEntryClass, search),
134 NULL, NULL,
135 g_cclosure_marshal_VOID__STRING,
136 G_TYPE_NONE,
137 1,
138 G_TYPE_STRING);
139
140 /**
141 * RBSearchEntry::activate:
142 * @entry: the #RBSearchEntry
143 * @text: search text
144 *
145 * Emitted when the entry is activated.
146 */
147 rb_search_entry_signals[ACTIVATE] =
148 g_signal_new ("activate",
149 G_OBJECT_CLASS_TYPE (object_class),
150 G_SIGNAL_RUN_LAST,
151 G_STRUCT_OFFSET (RBSearchEntryClass, activate),
152 NULL, NULL,
153 g_cclosure_marshal_VOID__STRING,
154 G_TYPE_NONE,
155 1,
156 G_TYPE_STRING);
157
158 /**
159 * RBSearchEntry::show-popup:
160 * @entry: the #RBSearchEntry
161 *
162 * Emitted when a popup menu should be shown
163 */
164 rb_search_entry_signals[SHOW_POPUP] =
165 g_signal_new ("show-popup",
166 G_OBJECT_CLASS_TYPE (object_class),
167 G_SIGNAL_RUN_LAST,
168 G_STRUCT_OFFSET (RBSearchEntryClass, show_popup),
169 NULL, NULL,
170 g_cclosure_marshal_VOID__VOID,
171 G_TYPE_NONE,
172 0);
173
174 /**
175 * RBSearchEntry:explicit-mode:
176 *
177 * If TRUE, show a button and only emit the 'search' signal when
178 * the user presses it rather than when they stop typing.
179 */
180 g_object_class_install_property (object_class,
181 PROP_EXPLICIT_MODE,
182 g_param_spec_boolean ("explicit-mode",
183 "explicit mode",
184 "whether in explicit search mode or not",
185 FALSE,
186 G_PARAM_READWRITE));
187 /**
188 * RBSearchEntry:has-popup:
189 *
190 * If TRUE, show a primary icon and emit the show-popup when clicked.
191 */
192 g_object_class_install_property (object_class,
193 PROP_HAS_POPUP,
194 g_param_spec_boolean ("has-popup",
195 "has popup",
196 "whether to display the search menu icon",
197 FALSE,
198 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
199
200 g_type_class_add_private (klass, sizeof (RBSearchEntryPrivate));
201 }
202
203 static void
204 rb_search_entry_init (RBSearchEntry *entry)
205 {
206 entry->priv = RB_SEARCH_ENTRY_GET_PRIVATE (entry);
207 }
208
209 static void
210 rb_search_entry_constructed (GObject *object)
211 {
212 RBSearchEntry *entry;
213
214 RB_CHAIN_GOBJECT_METHOD (rb_search_entry_parent_class, constructed, object);
215
216 entry = RB_SEARCH_ENTRY (object);
217
218 gtk_widget_set_can_focus (GTK_WIDGET (entry), TRUE);
219 entry->priv->entry = gtk_entry_new ();
220 g_signal_connect_object (GTK_ENTRY (entry->priv->entry),
221 "icon-press",
222 G_CALLBACK (rb_search_entry_clear_cb),
223 entry, 0);
224
225 gtk_entry_set_icon_tooltip_text (GTK_ENTRY (entry->priv->entry),
226 GTK_ENTRY_ICON_SECONDARY,
227 _("Clear the search text"));
228 if (entry->priv->has_popup) {
229 gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry->priv->entry),
230 GTK_ENTRY_ICON_PRIMARY,
231 "edit-find-symbolic");
232 gtk_entry_set_icon_tooltip_text (GTK_ENTRY (entry->priv->entry),
233 GTK_ENTRY_ICON_PRIMARY,
234 _("Select the search type"));
235 } else {
236 gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry->priv->entry),
237 GTK_ENTRY_ICON_SECONDARY,
238 "edit-find-symbolic");
239 }
240
241 gtk_box_pack_start (GTK_BOX (entry), entry->priv->entry, TRUE, TRUE, 0);
242
243 g_signal_connect_object (G_OBJECT (entry->priv->entry),
244 "changed",
245 G_CALLBACK (rb_search_entry_changed_cb),
246 entry, 0);
247 g_signal_connect_object (G_OBJECT (entry->priv->entry),
248 "focus_out_event",
249 G_CALLBACK (rb_search_entry_focus_out_event_cb),
250 entry, 0);
251 g_signal_connect_object (G_OBJECT (entry->priv->entry),
252 "activate",
253 G_CALLBACK (rb_search_entry_activate_cb),
254 entry, 0);
255
256 entry->priv->button = gtk_button_new_with_label (_("Search"));
257 gtk_box_pack_start (GTK_BOX (entry), entry->priv->button, FALSE, FALSE, 0);
258 gtk_widget_set_no_show_all (entry->priv->button, TRUE);
259 g_signal_connect_object (entry->priv->button,
260 "clicked",
261 G_CALLBACK (button_clicked_cb),
262 entry, 0);
263 }
264
265 static void
266 rb_search_entry_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
267 {
268 RBSearchEntry *entry = RB_SEARCH_ENTRY (object);
269
270 switch (prop_id) {
271 case PROP_EXPLICIT_MODE:
272 entry->priv->explicit_mode = g_value_get_boolean (value);
273 gtk_widget_set_visible (entry->priv->button, entry->priv->explicit_mode == TRUE);
274 rb_search_entry_update_icons (entry);
275 break;
276 case PROP_HAS_POPUP:
277 entry->priv->has_popup = g_value_get_boolean (value);
278 break;
279 default:
280 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
281 break;
282 }
283 }
284
285 static void
286 rb_search_entry_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
287 {
288 RBSearchEntry *entry = RB_SEARCH_ENTRY (object);
289
290 switch (prop_id) {
291 case PROP_EXPLICIT_MODE:
292 g_value_set_boolean (value, entry->priv->explicit_mode);
293 break;
294 case PROP_HAS_POPUP:
295 g_value_set_boolean (value, entry->priv->has_popup);
296 break;
297 default:
298 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
299 break;
300 }
301 }
302
303 static void
304 rb_search_entry_finalize (GObject *object)
305 {
306 RBSearchEntry *entry;
307
308 g_return_if_fail (object != NULL);
309 g_return_if_fail (RB_IS_SEARCH_ENTRY (object));
310
311 entry = RB_SEARCH_ENTRY (object);
312
313 g_return_if_fail (entry->priv != NULL);
314
315 G_OBJECT_CLASS (rb_search_entry_parent_class)->finalize (object);
316 }
317
318 /**
319 * rb_search_entry_new:
320 *
321 * Creates a new search entry widget.
322 *
323 * Return value: new search entry widget.
324 */
325 RBSearchEntry *
326 rb_search_entry_new (gboolean has_popup)
327 {
328 RBSearchEntry *entry;
329
330 entry = RB_SEARCH_ENTRY (g_object_new (RB_TYPE_SEARCH_ENTRY,
331 "spacing", 5,
332 "has-popup", has_popup,
333 "hexpand", TRUE,
334 NULL));
335
336 g_return_val_if_fail (entry->priv != NULL, NULL);
337
338 return entry;
339 }
340
341 /**
342 * rb_search_entry_clear:
343 * @entry: a #RBSearchEntry
344 *
345 * Clears the search entry text. The 'search' signal will
346 * be emitted.
347 */
348 void
349 rb_search_entry_clear (RBSearchEntry *entry)
350 {
351 if (entry->priv->timeout != 0) {
352 g_source_remove (entry->priv->timeout);
353 entry->priv->timeout = 0;
354 }
355
356 entry->priv->clearing = TRUE;
357
358 gtk_entry_set_text (GTK_ENTRY (entry->priv->entry), "");
359
360 entry->priv->clearing = FALSE;
361 }
362
363 /**
364 * rb_search_entry_set_text:
365 * @entry: a #RBSearchEntry
366 * @text: new search text
367 *
368 * Sets the text in the search entry box.
369 * The 'search' signal will be emitted.
370 */
371 void
372 rb_search_entry_set_text (RBSearchEntry *entry, const char *text)
373 {
374 gtk_entry_set_text (GTK_ENTRY (entry->priv->entry),
375 text ? text : "");
376 }
377
378 /**
379 * rb_search_entry_set_placeholder:
380 * @entry: a #RBSearchEntry
381 * @text: placeholder text
382 *
383 * Sets the placeholder text in the search entry box.
384 */
385 void
386 rb_search_entry_set_placeholder (RBSearchEntry *entry, const char *text)
387 {
388 gtk_entry_set_placeholder_text (GTK_ENTRY (entry->priv->entry), text);
389 }
390
391 static void
392 rb_search_entry_update_icons (RBSearchEntry *entry)
393 {
394 const char *text;
395 const char *icon;
396 gboolean searching;
397
398 if (entry->priv->explicit_mode) {
399 searching = entry->priv->searching;
400 } else {
401 text = gtk_entry_get_text (GTK_ENTRY (entry->priv->entry));
402 searching = (text && *text);
403 }
404
405 if (searching) {
406 icon = "edit-clear-symbolic";
407 } else if (entry->priv->has_popup) {
408 /* we already use 'find' as the primary icon */
409 icon = NULL;
410 } else {
411 icon = "edit-find-symbolic";
412 }
413 gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry->priv->entry),
414 GTK_ENTRY_ICON_SECONDARY,
415 icon);
416 }
417
418 static void
419 rb_search_entry_changed_cb (GtkEditable *editable,
420 RBSearchEntry *entry)
421 {
422 const char *text;
423
424 if (entry->priv->clearing == TRUE) {
425 entry->priv->searching = FALSE;
426 rb_search_entry_update_icons (entry);
427 return;
428 }
429
430 if (entry->priv->timeout != 0) {
431 g_source_remove (entry->priv->timeout);
432 entry->priv->timeout = 0;
433 }
434
435 /* emit it now if we're clearing the entry */
436 text = gtk_entry_get_text (GTK_ENTRY (entry->priv->entry));
437 if (text != NULL && text[0] != '\0') {
438 gtk_widget_set_sensitive (entry->priv->button, TRUE);
439 entry->priv->timeout = g_timeout_add (300, (GSourceFunc) rb_search_entry_timeout_cb, entry);
440 } else {
441 entry->priv->searching = FALSE;
442 gtk_widget_set_sensitive (entry->priv->button, FALSE);
443 rb_search_entry_timeout_cb (entry);
444 }
445 rb_search_entry_update_icons (entry);
446 }
447
448 static gboolean
449 rb_search_entry_timeout_cb (RBSearchEntry *entry)
450 {
451 const char *text;
452 gdk_threads_enter ();
453
454 text = gtk_entry_get_text (GTK_ENTRY (entry->priv->entry));
455
456 if (entry->priv->explicit_mode == FALSE) {
457 g_signal_emit (G_OBJECT (entry), rb_search_entry_signals[SEARCH], 0, text);
458 }
459 entry->priv->timeout = 0;
460
461 gdk_threads_leave ();
462
463 return FALSE;
464 }
465
466 static gboolean
467 rb_search_entry_focus_out_event_cb (GtkWidget *widget,
468 GdkEventFocus *event,
469 RBSearchEntry *entry)
470 {
471 if (entry->priv->timeout == 0)
472 return FALSE;
473
474 g_source_remove (entry->priv->timeout);
475 entry->priv->timeout = 0;
476
477 if (entry->priv->explicit_mode == FALSE) {
478 g_signal_emit (G_OBJECT (entry), rb_search_entry_signals[SEARCH], 0,
479 gtk_entry_get_text (GTK_ENTRY (entry->priv->entry)));
480 }
481
482 return FALSE;
483 }
484
485 /**
486 * rb_search_entry_searching:
487 * @entry: a #RBSearchEntry
488 *
489 * Returns %TRUE if there is search text in the entry widget.
490 *
491 * Return value: %TRUE if searching
492 */
493 gboolean
494 rb_search_entry_searching (RBSearchEntry *entry)
495 {
496 if (entry->priv->explicit_mode) {
497 return entry->priv->searching;
498 } else {
499 return strcmp ("", gtk_entry_get_text (GTK_ENTRY (entry->priv->entry))) != 0;
500 }
501 }
502
503 static void
504 rb_search_entry_activate_cb (GtkEntry *gtkentry,
505 RBSearchEntry *entry)
506 {
507 entry->priv->searching = TRUE;
508 rb_search_entry_update_icons (entry);
509 g_signal_emit (G_OBJECT (entry), rb_search_entry_signals[ACTIVATE], 0,
510 gtk_entry_get_text (GTK_ENTRY (entry->priv->entry)));
511 }
512
513 static void
514 button_clicked_cb (GtkButton *button, RBSearchEntry *entry)
515 {
516 entry->priv->searching = TRUE;
517 rb_search_entry_update_icons (entry);
518 g_signal_emit (G_OBJECT (entry), rb_search_entry_signals[SEARCH], 0,
519 gtk_entry_get_text (GTK_ENTRY (entry->priv->entry)));
520 }
521
522 /**
523 * rb_search_entry_grab_focus:
524 * @entry: a #RBSearchEntry
525 *
526 * Grabs input focus for the text entry widget.
527 */
528 void
529 rb_search_entry_grab_focus (RBSearchEntry *entry)
530 {
531 gtk_widget_grab_focus (GTK_WIDGET (entry->priv->entry));
532 }
533
534 static void
535 rb_search_entry_widget_grab_focus (GtkWidget *widget)
536 {
537 rb_search_entry_grab_focus (RB_SEARCH_ENTRY (widget));
538 }
539
540 static void
541 rb_search_entry_clear_cb (GtkEntry *entry,
542 GtkEntryIconPosition icon_pos,
543 GdkEvent *event,
544 RBSearchEntry *search_entry)
545 {
546 if (icon_pos == GTK_ENTRY_ICON_PRIMARY) {
547 g_signal_emit (G_OBJECT (search_entry), rb_search_entry_signals[SHOW_POPUP], 0);
548 } else {
549 rb_search_entry_set_text (search_entry, "");
550 }
551 }
552
553 /**
554 * rb_search_entry_set_mnemonic:
555 * @entry: a #RBSearchEntry
556 * @enable: if %TRUE, enable the mnemonic
557 *
558 * Adds or removes a mnemonic allowing the user to focus
559 * the search entry.
560 */
561 void
562 rb_search_entry_set_mnemonic (RBSearchEntry *entry, gboolean enable)
563 {
564 GtkWidget *toplevel;
565 guint keyval;
566 gunichar accel = 0;
567
568 if (pango_parse_markup (_("_Search:"), -1, '_', NULL, NULL, &accel, NULL) && accel != 0) {
569 keyval = gdk_keyval_to_lower (gdk_unicode_to_keyval (accel));
570 } else {
571 keyval = gdk_unicode_to_keyval ('s');
572 }
573
574 toplevel = gtk_widget_get_toplevel (GTK_WIDGET (entry));
575 if (gtk_widget_is_toplevel (toplevel)) {
576 if (enable) {
577 gtk_window_add_mnemonic (GTK_WINDOW (toplevel), keyval, entry->priv->entry);
578 } else {
579 gtk_window_remove_mnemonic (GTK_WINDOW (toplevel), keyval, entry->priv->entry);
580 }
581 }
582 }