hythmbox-2.98/widgets/rb-search-entry.c

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 }