1 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
2 /* e-action-combo-box.c
3  *
4  * Copyright (C) 2008 Novell, Inc.
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of version 2 of the GNU Lesser General Public
8  * License as published by the Free Software Foundation.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this program; if not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "e-action-combo-box.h"
20 
21 #include <glib/gi18n.h>
22 
23 #define E_ACTION_COMBO_BOX_GET_PRIVATE(obj) \
24 	(G_TYPE_INSTANCE_GET_PRIVATE \
25 	((obj), E_TYPE_ACTION_COMBO_BOX, EActionComboBoxPrivate))
26 
27 enum {
28 	COLUMN_ACTION,
29 	COLUMN_SORT
30 };
31 
32 enum {
33 	PROP_0,
34 	PROP_ACTION
35 };
36 
37 struct _EActionComboBoxPrivate {
38 	GtkRadioAction *action;
39 	GtkActionGroup *action_group;
40 	GHashTable *index;
41 	guint changed_handler_id;		/* action::changed */
42 	guint group_sensitive_handler_id;	/* action-group::sensitive */
43 	guint group_visible_handler_id;		/* action-group::visible */
44 	gboolean group_has_icons : 1;
45 };
46 
47 static gpointer parent_class;
48 
49 static void
action_combo_box_action_changed_cb(GtkRadioAction * action G_GNUC_UNUSED,GtkRadioAction * current,EActionComboBox * combo_box)50 action_combo_box_action_changed_cb (GtkRadioAction *action G_GNUC_UNUSED,
51                                     GtkRadioAction *current,
52                                     EActionComboBox *combo_box)
53 {
54 	GtkTreeRowReference *reference;
55 	GtkTreeModel *model;
56 	GtkTreePath *path;
57 	GtkTreeIter iter;
58 	gboolean valid;
59 
60 	reference = g_hash_table_lookup (
61 		combo_box->priv->index, GINT_TO_POINTER (
62 		gtk_radio_action_get_current_value (current)));
63 	g_return_if_fail (reference != NULL);
64 
65 	model = gtk_tree_row_reference_get_model (reference);
66 	path = gtk_tree_row_reference_get_path (reference);
67 	valid = gtk_tree_model_get_iter (model, &iter, path);
68 	gtk_tree_path_free (path);
69 	g_return_if_fail (valid);
70 
71 	gtk_combo_box_set_active_iter (GTK_COMBO_BOX (combo_box), &iter);
72 }
73 
74 static void
action_combo_box_action_group_notify_cb(GtkActionGroup * action_group,GParamSpec * pspec G_GNUC_UNUSED,EActionComboBox * combo_box)75 action_combo_box_action_group_notify_cb (GtkActionGroup *action_group,
76                                          GParamSpec *pspec G_GNUC_UNUSED,
77                                          EActionComboBox *combo_box)
78 {
79 	g_object_set (
80 		combo_box, "sensitive",
81 		gtk_action_group_get_sensitive (action_group), "visible",
82 		gtk_action_group_get_visible (action_group), NULL);
83 }
84 
85 static void
action_combo_box_render_pixbuf(GtkCellLayout * layout G_GNUC_UNUSED,GtkCellRenderer * renderer,GtkTreeModel * model,GtkTreeIter * iter,EActionComboBox * combo_box)86 action_combo_box_render_pixbuf (GtkCellLayout *layout G_GNUC_UNUSED,
87                                 GtkCellRenderer *renderer,
88                                 GtkTreeModel *model,
89                                 GtkTreeIter *iter,
90                                 EActionComboBox *combo_box)
91 {
92 	GtkRadioAction *action;
93 	gchar *icon_name;
94 	gchar *stock_id;
95 	gboolean sensitive;
96 	gboolean visible;
97 	gint width;
98 
99 	gtk_tree_model_get (model, iter, COLUMN_ACTION, &action, -1);
100 
101 	/* Do any of the actions have an icon? */
102 	if (!combo_box->priv->group_has_icons)
103 		return;
104 
105 	/* A NULL action means the row is a separator. */
106 	if (action == NULL)
107 		return;
108 
109 	g_object_get (
110 		G_OBJECT (action),
111 		"icon-name", &icon_name,
112 		"sensitive", &sensitive,
113 		"stock-id", &stock_id,
114 		"visible", &visible,
115 		NULL);
116 
117 	/* Keep the pixbuf renderer a fixed size for proper alignment. */
118 	gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, NULL);
119 
120 	/* We can't set both "icon-name" and "stock-id" because setting
121 	 * one unsets the other.  So pick the one that has a non-NULL
122 	 * value.  If both are non-NULL, "stock-id" wins. */
123 
124 	if (stock_id != NULL)
125 		g_object_set (
126 			G_OBJECT (renderer),
127 			"sensitive", sensitive,
128 			"stock-id", stock_id,
129 			"stock-size", GTK_ICON_SIZE_MENU,
130 			"visible", visible,
131 			"width", width,
132 			NULL);
133 	else
134 		g_object_set (
135 			G_OBJECT (renderer),
136 			"icon-name", icon_name,
137 			"sensitive", sensitive,
138 			"stock-size", GTK_ICON_SIZE_MENU,
139 			"visible", visible,
140 			"width", width,
141 			NULL);
142 
143 	g_free (icon_name);
144 	g_free (stock_id);
145 }
146 
147 static void
action_combo_box_render_text(GtkCellLayout * layout G_GNUC_UNUSED,GtkCellRenderer * renderer,GtkTreeModel * model,GtkTreeIter * iter,EActionComboBox * combo_box)148 action_combo_box_render_text (GtkCellLayout *layout G_GNUC_UNUSED,
149                               GtkCellRenderer *renderer,
150                               GtkTreeModel *model,
151                               GtkTreeIter *iter,
152                               EActionComboBox *combo_box)
153 {
154 	GtkRadioAction *action;
155 	gchar **strv;
156 	gchar *label;
157 	gboolean sensitive;
158 	gboolean visible;
159 	gint xpad;
160 
161 	gtk_tree_model_get (model, iter, COLUMN_ACTION, &action, -1);
162 
163 	/* A NULL action means the row is a separator. */
164 	if (action == NULL)
165 		return;
166 
167 	g_object_get (
168 		G_OBJECT (action),
169 		"label", &label,
170 		"sensitive", &sensitive,
171 		"visible", &visible,
172 		NULL);
173 
174 	/* Strip out underscores. */
175 	strv = g_strsplit (label, "_", -1);
176 	g_free (label);
177 	label = g_strjoinv (NULL, strv);
178 	g_strfreev (strv);
179 
180 	xpad = combo_box->priv->group_has_icons ? 3 : 0;
181 
182 	g_object_set (
183 		G_OBJECT (renderer),
184 		"sensitive", sensitive,
185 		"text", label,
186 		"visible", visible,
187 		"xpad", xpad,
188 		NULL);
189 
190 	g_free (label);
191 }
192 
193 static gboolean
action_combo_box_is_row_separator(GtkTreeModel * model,GtkTreeIter * iter)194 action_combo_box_is_row_separator (GtkTreeModel *model,
195                                    GtkTreeIter *iter)
196 {
197 	GtkAction *action;
198 	gboolean separator;
199 
200 	/* NULL actions are rendered as separators. */
201 	gtk_tree_model_get (model, iter, COLUMN_ACTION, &action, -1);
202 	separator = (action == NULL);
203 	if (action != NULL)
204 		g_object_unref (action);
205 
206 	return separator;
207 }
208 
209 static void
action_combo_box_update_model(EActionComboBox * combo_box)210 action_combo_box_update_model (EActionComboBox *combo_box)
211 {
212 	GtkListStore *list_store;
213 	GSList *list;
214 
215 	g_hash_table_remove_all (combo_box->priv->index);
216 
217 	if (combo_box->priv->action == NULL) {
218 		gtk_combo_box_set_model (GTK_COMBO_BOX (combo_box), NULL);
219 		return;
220 	}
221 
222 	/* We store values in the sort column as floats so that we can
223 	 * insert separators in between consecutive integer values and
224 	 * still maintain the proper ordering. */
225 	list_store = gtk_list_store_new (
226 		2, GTK_TYPE_RADIO_ACTION, G_TYPE_FLOAT);
227 
228 	list = gtk_radio_action_get_group (combo_box->priv->action);
229 	combo_box->priv->group_has_icons = FALSE;
230 
231 	while (list != NULL) {
232 		GtkTreeRowReference *reference;
233 		GtkRadioAction *action = list->data;
234 		GtkTreePath *path;
235 		GtkTreeIter iter;
236 		gchar *icon_name;
237 		gchar *stock_id;
238 		gint value;
239 
240 		g_object_get (
241 			action, "icon-name", &icon_name,
242 			"stock-id", &stock_id, NULL);
243 		combo_box->priv->group_has_icons |=
244 			(icon_name != NULL || stock_id != NULL);
245 		g_free (icon_name);
246 		g_free (stock_id);
247 
248 		gtk_list_store_append (list_store, &iter);
249 		g_object_get (G_OBJECT (action), "value", &value, NULL);
250 		gtk_list_store_set (
251 			list_store, &iter, COLUMN_ACTION,
252 			list->data, COLUMN_SORT, (gfloat) value, -1);
253 
254 		path = gtk_tree_model_get_path (
255 			GTK_TREE_MODEL (list_store), &iter);
256 		reference = gtk_tree_row_reference_new (
257 			GTK_TREE_MODEL (list_store), path);
258 		g_hash_table_insert (
259 			combo_box->priv->index,
260 			GINT_TO_POINTER (value), reference);
261 		gtk_tree_path_free (path);
262 
263 		list = g_slist_next (list);
264 	}
265 
266 	gtk_tree_sortable_set_sort_column_id (
267 		GTK_TREE_SORTABLE (list_store),
268 		COLUMN_SORT, GTK_SORT_ASCENDING);
269 	gtk_combo_box_set_model (
270 		GTK_COMBO_BOX (combo_box), GTK_TREE_MODEL (list_store));
271 
272 	action_combo_box_action_changed_cb (
273 		combo_box->priv->action,
274 		combo_box->priv->action,
275 		combo_box);
276 }
277 
278 static void
action_combo_box_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)279 action_combo_box_set_property (GObject *object,
280                                guint property_id,
281                                const GValue *value,
282                                GParamSpec *pspec)
283 {
284 	switch (property_id) {
285 		case PROP_ACTION:
286 			e_action_combo_box_set_action (
287 				E_ACTION_COMBO_BOX (object),
288 				g_value_get_object (value));
289 			return;
290 	}
291 
292 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
293 }
294 
295 static void
action_combo_box_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)296 action_combo_box_get_property (GObject *object,
297                                guint property_id,
298                                GValue *value,
299                                GParamSpec *pspec)
300 {
301 	switch (property_id) {
302 		case PROP_ACTION:
303 			g_value_set_object (
304 				value, e_action_combo_box_get_action (
305 				E_ACTION_COMBO_BOX (object)));
306 			return;
307 	}
308 
309 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
310 }
311 
312 static void
action_combo_box_dispose(GObject * object)313 action_combo_box_dispose (GObject *object)
314 {
315 	EActionComboBoxPrivate *priv = E_ACTION_COMBO_BOX_GET_PRIVATE (object);
316 
317 	if (priv->action != NULL) {
318 		g_object_unref (priv->action);
319 		priv->action = NULL;
320 	}
321 
322 	if (priv->action_group != NULL) {
323 		g_object_unref (priv->action_group);
324 		priv->action_group = NULL;
325 	}
326 
327 	g_hash_table_remove_all (priv->index);
328 
329 	/* Chain up to parent's dispose() method. */
330 	G_OBJECT_CLASS (parent_class)->dispose (object);
331 }
332 
333 static void
action_combo_box_finalize(GObject * object)334 action_combo_box_finalize (GObject *object)
335 {
336 	EActionComboBoxPrivate *priv = E_ACTION_COMBO_BOX_GET_PRIVATE (object);
337 
338 	g_hash_table_destroy (priv->index);
339 
340 	/* Chain up to parent's finalize() method. */
341 	G_OBJECT_CLASS (parent_class)->finalize (object);
342 }
343 
344 static void
action_combo_box_changed(GtkComboBox * combo_box)345 action_combo_box_changed (GtkComboBox *combo_box)
346 {
347 	GtkRadioAction *action;
348 	GtkTreeModel *model;
349 	GtkTreeIter iter;
350 	gint value;
351 
352 	/* This method is virtual, so no need to chain up. */
353 
354 	if (!gtk_combo_box_get_active_iter (combo_box, &iter))
355 		return;
356 
357 	model = gtk_combo_box_get_model (combo_box);
358 	gtk_tree_model_get (model, &iter, COLUMN_ACTION, &action, -1);
359 	g_object_get (G_OBJECT (action), "value", &value, NULL);
360 	gtk_radio_action_set_current_value (action, value);
361 }
362 
363 static void
action_combo_box_class_init(EActionComboBoxClass * class)364 action_combo_box_class_init (EActionComboBoxClass *class)
365 {
366 	GObjectClass *object_class;
367 	GtkComboBoxClass *combo_box_class;
368 
369 	parent_class = g_type_class_peek_parent (class);
370 	g_type_class_add_private (class, sizeof (EActionComboBoxPrivate));
371 
372 	object_class = G_OBJECT_CLASS (class);
373 	object_class->set_property = action_combo_box_set_property;
374 	object_class->get_property = action_combo_box_get_property;
375 	object_class->dispose = action_combo_box_dispose;
376 	object_class->finalize = action_combo_box_finalize;
377 
378 	combo_box_class = GTK_COMBO_BOX_CLASS (class);
379 	combo_box_class->changed = action_combo_box_changed;
380 
381 	g_object_class_install_property (
382 		object_class,
383 		PROP_ACTION,
384 		g_param_spec_object (
385 			"action",
386 			_("Action"),
387 			_("A GtkRadioAction"),
388 			GTK_TYPE_RADIO_ACTION,
389 			G_PARAM_READWRITE));
390 }
391 
392 static void
action_combo_box_init(EActionComboBox * combo_box)393 action_combo_box_init (EActionComboBox *combo_box)
394 {
395 	GtkCellRenderer *renderer;
396 
397 	combo_box->priv = E_ACTION_COMBO_BOX_GET_PRIVATE (combo_box);
398 
399 	renderer = gtk_cell_renderer_pixbuf_new ();
400 	gtk_cell_layout_pack_start (
401 		GTK_CELL_LAYOUT (combo_box), renderer, FALSE);
402 	gtk_cell_layout_set_cell_data_func (
403 		GTK_CELL_LAYOUT (combo_box), renderer,
404 		(GtkCellLayoutDataFunc) action_combo_box_render_pixbuf,
405 		combo_box, NULL);
406 
407 	renderer = gtk_cell_renderer_text_new ();
408 	gtk_cell_layout_pack_start (
409 		GTK_CELL_LAYOUT (combo_box), renderer, TRUE);
410 	gtk_cell_layout_set_cell_data_func (
411 		GTK_CELL_LAYOUT (combo_box), renderer,
412 		(GtkCellLayoutDataFunc) action_combo_box_render_text,
413 		combo_box, NULL);
414 
415 	gtk_combo_box_set_row_separator_func (
416 		GTK_COMBO_BOX (combo_box), (GtkTreeViewRowSeparatorFunc)
417 		action_combo_box_is_row_separator, NULL, NULL);
418 
419 	combo_box->priv->index = g_hash_table_new_full (
420 		g_direct_hash, g_direct_equal,
421 		(GDestroyNotify) NULL,
422 		(GDestroyNotify) gtk_tree_row_reference_free);
423 }
424 
425 GType
e_action_combo_box_get_type(void)426 e_action_combo_box_get_type (void)
427 {
428 	static GType type = 0;
429 
430 	if (G_UNLIKELY (type == 0)) {
431 		static const GTypeInfo type_info = {
432 			sizeof (EActionComboBoxClass),
433 			(GBaseInitFunc) NULL,
434 			(GBaseFinalizeFunc) NULL,
435 			(GClassInitFunc) action_combo_box_class_init,
436 			(GClassFinalizeFunc) NULL,
437 			NULL,  /* class_data */
438 			sizeof (EActionComboBox),
439 			0,     /* n_preallocs */
440 			(GInstanceInitFunc) action_combo_box_init,
441 			NULL   /* value_table */
442 		};
443 
444 		type = g_type_register_static (
445 			GTK_TYPE_COMBO_BOX, "EActionComboBox",
446 			&type_info, 0);
447 	}
448 
449 	return type;
450 }
451 
452 GtkWidget *
e_action_combo_box_new(void)453 e_action_combo_box_new (void)
454 {
455 	return e_action_combo_box_new_with_action (NULL);
456 }
457 
458 GtkWidget *
e_action_combo_box_new_with_action(GtkRadioAction * action)459 e_action_combo_box_new_with_action (GtkRadioAction *action)
460 {
461 	return g_object_new (E_TYPE_ACTION_COMBO_BOX, "action", action, NULL);
462 }
463 
464 GtkRadioAction *
e_action_combo_box_get_action(EActionComboBox * combo_box)465 e_action_combo_box_get_action (EActionComboBox *combo_box)
466 {
467 	g_return_val_if_fail (E_ACTION_IS_COMBO_BOX (combo_box), NULL);
468 
469 	return combo_box->priv->action;
470 }
471 
472 void
e_action_combo_box_set_action(EActionComboBox * combo_box,GtkRadioAction * action)473 e_action_combo_box_set_action (EActionComboBox *combo_box,
474                                GtkRadioAction *action)
475 {
476 	g_return_if_fail (E_ACTION_IS_COMBO_BOX (combo_box));
477 
478 	if (action != NULL)
479 		g_return_if_fail (GTK_IS_RADIO_ACTION (action));
480 
481 	if (combo_box->priv->action != NULL) {
482 		g_signal_handler_disconnect (
483 			combo_box->priv->action,
484 			combo_box->priv->changed_handler_id);
485 		g_object_unref (combo_box->priv->action);
486 	}
487 
488 	if (combo_box->priv->action_group != NULL) {
489 		g_signal_handler_disconnect (
490 			combo_box->priv->action_group,
491 			combo_box->priv->group_sensitive_handler_id);
492 		g_signal_handler_disconnect (
493 			combo_box->priv->action_group,
494 			combo_box->priv->group_visible_handler_id);
495 		g_object_unref (combo_box->priv->action_group);
496 		combo_box->priv->action_group = NULL;
497 	}
498 
499 	if (action != NULL)
500 		g_object_get (
501 			g_object_ref (action), "action-group",
502 			&combo_box->priv->action_group, NULL);
503 	combo_box->priv->action = action;
504 	action_combo_box_update_model (combo_box);
505 
506 	if (combo_box->priv->action != NULL)
507 		combo_box->priv->changed_handler_id = g_signal_connect (
508 			combo_box->priv->action, "changed",
509 			G_CALLBACK (action_combo_box_action_changed_cb),
510 			combo_box);
511 
512 	if (combo_box->priv->action_group != NULL) {
513 		g_object_ref (combo_box->priv->action_group);
514 		combo_box->priv->group_sensitive_handler_id =
515 			g_signal_connect (
516 				combo_box->priv->action_group,
517 				"notify::sensitive", G_CALLBACK (
518 				action_combo_box_action_group_notify_cb),
519 				combo_box);
520 		combo_box->priv->group_visible_handler_id =
521 			g_signal_connect (
522 				combo_box->priv->action_group,
523 				"notify::visible", G_CALLBACK (
524 				action_combo_box_action_group_notify_cb),
525 				combo_box);
526 	}
527 }
528 
529 gint
e_action_combo_box_get_current_value(EActionComboBox * combo_box)530 e_action_combo_box_get_current_value (EActionComboBox *combo_box)
531 {
532 	g_return_val_if_fail (E_ACTION_IS_COMBO_BOX (combo_box), 0);
533 	g_return_val_if_fail (combo_box->priv->action != NULL, 0);
534 
535 	return gtk_radio_action_get_current_value (combo_box->priv->action);
536 }
537 
538 void
e_action_combo_box_set_current_value(EActionComboBox * combo_box,gint current_value)539 e_action_combo_box_set_current_value (EActionComboBox *combo_box,
540                                       gint current_value)
541 {
542 	g_return_if_fail (E_ACTION_IS_COMBO_BOX (combo_box));
543 	g_return_if_fail (combo_box->priv->action != NULL);
544 
545 	gtk_radio_action_set_current_value (
546 		combo_box->priv->action, current_value);
547 }
548 
549 void
e_action_combo_box_add_separator_before(EActionComboBox * combo_box,gint action_value)550 e_action_combo_box_add_separator_before (EActionComboBox *combo_box,
551                                          gint action_value)
552 {
553 	GtkTreeModel *model;
554 	GtkTreeIter iter;
555 
556 	g_return_if_fail (E_ACTION_IS_COMBO_BOX (combo_box));
557 
558 	/* NULL actions are rendered as separators. */
559 	model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo_box));
560 	gtk_list_store_append (GTK_LIST_STORE (model), &iter);
561 	gtk_list_store_set (
562 		GTK_LIST_STORE (model), &iter, COLUMN_ACTION,
563 		NULL, COLUMN_SORT, (gfloat) action_value - 0.5, -1);
564 }
565 
566 void
e_action_combo_box_add_separator_after(EActionComboBox * combo_box,gint action_value)567 e_action_combo_box_add_separator_after (EActionComboBox *combo_box,
568                                         gint action_value)
569 {
570 	GtkTreeModel *model;
571 	GtkTreeIter iter;
572 
573 	g_return_if_fail (E_ACTION_IS_COMBO_BOX (combo_box));
574 
575 	/* NULL actions are rendered as separators. */
576 	model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo_box));
577 	gtk_list_store_append (GTK_LIST_STORE (model), &iter);
578 	gtk_list_store_set (
579 		GTK_LIST_STORE (model), &iter, COLUMN_ACTION,
580 		NULL, COLUMN_SORT, (gfloat) action_value + 0.5, -1);
581 }
582