1 /*
2  * This file is part of Amtk - Actions, Menus and Toolbars Kit
3  *
4  * Copyright 2017 - Sébastien Wilmet <swilmet@gnome.org>
5  *
6  * Amtk is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU Lesser General Public License as published by the
8  * Free Software Foundation; either version 2.1 of the License, or (at your
9  * option) any later version.
10  *
11  * Amtk is distributed in the hope that it will be useful, but WITHOUT ANY
12  * WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14  * License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this library; if not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "amtk-utils.h"
21 #include <string.h>
22 #include "amtk-action-info.h"
23 #include "amtk-action-info-central-store.h"
24 
25 /**
26  * SECTION:amtk-utils
27  * @title: AmtkUtils
28  * @short_description: Utility functions
29  *
30  * Utility functions.
31  */
32 
33 /*
34  * _amtk_utils_replace_home_dir_with_tilde:
35  * @filename: the filename.
36  *
37  * Replaces the home directory with a tilde, if the home directory is present in
38  * the @filename.
39  *
40  * Returns: the new filename. Free with g_free().
41  */
42 /* This function is a copy from tepl-utils, which originally comes from gedit. */
43 gchar *
_amtk_utils_replace_home_dir_with_tilde(const gchar * filename)44 _amtk_utils_replace_home_dir_with_tilde (const gchar *filename)
45 {
46 	gchar *tmp;
47 	gchar *home;
48 
49 	g_return_val_if_fail (filename != NULL, NULL);
50 
51 	/* Note that g_get_home_dir returns a const string */
52 	tmp = (gchar *) g_get_home_dir ();
53 
54 	if (tmp == NULL)
55 	{
56 		return g_strdup (filename);
57 	}
58 
59 	home = g_filename_to_utf8 (tmp, -1, NULL, NULL, NULL);
60 	if (home == NULL)
61 	{
62 		return g_strdup (filename);
63 	}
64 
65 	if (g_str_equal (filename, home))
66 	{
67 		g_free (home);
68 		return g_strdup ("~");
69 	}
70 
71 	tmp = home;
72 	home = g_strdup_printf ("%s/", tmp);
73 	g_free (tmp);
74 
75 	if (g_str_has_prefix (filename, home))
76 	{
77 		gchar *res = g_strdup_printf ("~/%s", filename + strlen (home));
78 		g_free (home);
79 		return res;
80 	}
81 
82 	g_free (home);
83 	return g_strdup (filename);
84 }
85 
86 /**
87  * amtk_utils_remove_mnemonic:
88  * @str: a string.
89  *
90  * Removes the mnemonics from @str. Single underscores are removed, and two
91  * consecutive underscores are replaced by one underscore (see the documentation
92  * of gtk_label_new_with_mnemonic()).
93  *
94  * Returns: (transfer full): the new string with the mnemonics removed. Free
95  * with g_free() when no longer needed.
96  * Since: 5.0
97  */
98 gchar *
amtk_utils_remove_mnemonic(const gchar * str)99 amtk_utils_remove_mnemonic (const gchar *str)
100 {
101 	gchar *new_str;
102 	gint str_pos;
103 	gint new_str_pos = 0;
104 	gboolean prev_char_is_underscore_skipped = FALSE;
105 
106 	g_return_val_if_fail (str != NULL, NULL);
107 
108 	new_str = g_malloc (strlen (str) + 1);
109 
110 	for (str_pos = 0; str[str_pos] != '\0'; str_pos++)
111 	{
112 		gchar cur_char = str[str_pos];
113 
114 		if (cur_char == '_' && !prev_char_is_underscore_skipped)
115 		{
116 			prev_char_is_underscore_skipped = TRUE;
117 		}
118 		else
119 		{
120 			new_str[new_str_pos++] = cur_char;
121 			prev_char_is_underscore_skipped = FALSE;
122 		}
123 	}
124 
125 	new_str[new_str_pos] = '\0';
126 	return new_str;
127 }
128 
129 static gint
get_menu_item_position(GtkMenuShell * menu_shell,GtkMenuItem * item)130 get_menu_item_position (GtkMenuShell *menu_shell,
131 			GtkMenuItem  *item)
132 {
133 	GList *children;
134 	GList *l;
135 	gint pos;
136 	gboolean found = FALSE;
137 
138 	children = gtk_container_get_children (GTK_CONTAINER (menu_shell));
139 
140 	for (l = children, pos = 0; l != NULL; l = l->next, pos++)
141 	{
142 		GtkMenuItem *cur_item = l->data;
143 
144 		if (cur_item == item)
145 		{
146 			found = TRUE;
147 			break;
148 		}
149 	}
150 
151 	g_list_free (children);
152 
153 	return found ? pos : -1;
154 }
155 
156 /**
157  * amtk_utils_recent_chooser_menu_get_item_uri:
158  * @menu: a #GtkRecentChooserMenu.
159  * @item: a #GtkMenuItem.
160  *
161  * Gets the URI of @item. @item must be a child of @menu. @menu must be a
162  * #GtkRecentChooserMenu.
163  *
164  * This function has been written because the value returned by
165  * gtk_recent_chooser_get_current_uri() is not updated when #GtkMenuItem's of a
166  * #GtkRecentChooserMenu are selected/deselected.
167  *
168  * Returns: the URI of @item. Free with g_free() when no longer needed.
169  * Since: 2.0
170  */
171 gchar *
amtk_utils_recent_chooser_menu_get_item_uri(GtkRecentChooserMenu * menu,GtkMenuItem * item)172 amtk_utils_recent_chooser_menu_get_item_uri (GtkRecentChooserMenu *menu,
173 					     GtkMenuItem          *item)
174 {
175 	gint pos;
176 	gchar **all_uris;
177 	gsize length;
178 	gchar *item_uri = NULL;
179 
180 	g_return_val_if_fail (GTK_IS_RECENT_CHOOSER_MENU (menu), NULL);
181 	g_return_val_if_fail (GTK_IS_MENU_ITEM (item), NULL);
182 
183 	{
184 		GtkWidget *item_parent;
185 
186 		item_parent = gtk_widget_get_parent (GTK_WIDGET (item));
187 		g_return_val_if_fail (item_parent == GTK_WIDGET (menu), NULL);
188 	}
189 
190 	pos = get_menu_item_position (GTK_MENU_SHELL (menu), item);
191 	g_return_val_if_fail (pos >= 0, NULL);
192 
193 	all_uris = gtk_recent_chooser_get_uris (GTK_RECENT_CHOOSER (menu), &length);
194 
195 	if ((gsize)pos < length)
196 	{
197 		item_uri = g_strdup (all_uris[pos]);
198 	}
199 
200 	g_strfreev (all_uris);
201 	return item_uri;
202 }
203 
204 static gboolean
variant_type_equal_null_safe(const GVariantType * type1,const GVariantType * type2)205 variant_type_equal_null_safe (const GVariantType *type1,
206 			      const GVariantType *type2)
207 {
208 	if (type1 == NULL || type2 == NULL)
209 	{
210 		return type1 == NULL && type2 == NULL;
211 	}
212 
213 	return g_variant_type_equal (type1, type2);
214 }
215 
216 #define AMTK_GVARIANT_PARAM_KEY "amtk-gvariant-param-key"
217 
218 G_GNUC_BEGIN_IGNORE_DEPRECATIONS
219 static void
gtk_action_activate_cb(GtkAction * gtk_action,GAction * g_action)220 gtk_action_activate_cb (GtkAction *gtk_action,
221 			GAction   *g_action)
222 {
223 	GVariant *param;
224 
225 	param = g_object_get_data (G_OBJECT (gtk_action), AMTK_GVARIANT_PARAM_KEY);
226 	g_action_activate (g_action, param);
227 }
228 
229 /**
230  * amtk_utils_bind_g_action_to_gtk_action:
231  * @g_action_map: a #GActionMap.
232  * @detailed_g_action_name_without_prefix: a detailed #GAction name without the
233  *   #GActionMap prefix; the #GAction must be present in @g_action_map.
234  * @gtk_action_group: a #GtkActionGroup.
235  * @gtk_action_name: a #GtkAction name present in @gtk_action_group.
236  *
237  * Utility function to be able to port an application gradually to #GAction,
238  * when #GtkUIManager and #GtkAction are still used. Porting to #GAction should
239  * be the first step.
240  *
241  * For @detailed_g_action_name_without_prefix, see the
242  * g_action_parse_detailed_name() function.  The `"app."` or `"win."` prefix (or
243  * any other #GActionMap prefix) must not be included in
244  * @detailed_g_action_name_without_prefix. For example a valid
245  * @detailed_g_action_name_without_prefix is `"open"` or
246  * `"insert-command::foobar"`.
247  *
248  * The same #GAction can be bound to several #GtkAction's (with different
249  * parameter values for the #GAction), but the reverse is not true, one
250  * #GtkAction cannot be bound to several #GAction's.
251  *
252  * This function:
253  * - Calls g_action_activate() when the #GtkAction #GtkAction::activate signal
254  *   is emitted.
255  * - Binds the #GAction #GAction:enabled property to the #GtkAction
256  *   #GtkAction:sensitive property. The binding is done with the
257  *   %G_BINDING_BIDIRECTIONAL and %G_BINDING_SYNC_CREATE flags, the source is
258  *   the #GAction and the target is the #GtkAction.
259  *
260  * When using this function, you should set the callback to %NULL in the
261  * corresponding #GtkActionEntry.
262  *
263  * Since: 4.0
264  */
265 void
amtk_utils_bind_g_action_to_gtk_action(GActionMap * g_action_map,const gchar * detailed_g_action_name_without_prefix,GtkActionGroup * gtk_action_group,const gchar * gtk_action_name)266 amtk_utils_bind_g_action_to_gtk_action (GActionMap     *g_action_map,
267 					const gchar    *detailed_g_action_name_without_prefix,
268 					GtkActionGroup *gtk_action_group,
269 					const gchar    *gtk_action_name)
270 {
271 	gchar *g_action_name = NULL;
272 	GVariant *target_value = NULL;
273 	GAction *g_action;
274 	GtkAction *gtk_action;
275 	GError *error = NULL;
276 
277 	g_return_if_fail (G_IS_ACTION_MAP (g_action_map));
278 	g_return_if_fail (detailed_g_action_name_without_prefix != NULL);
279 	g_return_if_fail (GTK_IS_ACTION_GROUP (gtk_action_group));
280 	g_return_if_fail (gtk_action_name != NULL);
281 
282 	g_action_parse_detailed_name (detailed_g_action_name_without_prefix,
283 				      &g_action_name,
284 				      &target_value,
285 				      &error);
286 
287 	/* The doc of g_action_parse_detailed_name() doesn't explain if it
288 	 * returns a floating ref for the GVariant.
289 	 */
290 	if (target_value != NULL &&
291 	    g_variant_is_floating (target_value))
292 	{
293 		g_variant_ref_sink (target_value);
294 	}
295 
296 	if (error != NULL)
297 	{
298 		g_warning ("Error when parsing detailed GAction name '%s': %s",
299 			   detailed_g_action_name_without_prefix,
300 			   error->message);
301 
302 		g_clear_error (&error);
303 		goto out;
304 	}
305 
306 	g_action = g_action_map_lookup_action (g_action_map, g_action_name);
307 	if (g_action == NULL)
308 	{
309 		g_warn_if_reached ();
310 		goto out;
311 	}
312 
313 	/* Sanity check, ensure that the GVariant target has the good type. */
314 	{
315 		const GVariantType *g_action_param_type;
316 		const GVariantType *target_value_type = NULL;
317 
318 		g_action_param_type = g_action_get_parameter_type (g_action);
319 
320 		if (target_value != NULL)
321 		{
322 			target_value_type = g_variant_get_type (target_value);
323 		}
324 
325 		if (!variant_type_equal_null_safe (g_action_param_type, target_value_type))
326 		{
327 			g_warn_if_reached ();
328 			goto out;
329 		}
330 	}
331 
332 	gtk_action = gtk_action_group_get_action (gtk_action_group, gtk_action_name);
333 	if (gtk_action == NULL)
334 	{
335 		g_warn_if_reached ();
336 		goto out;
337 	}
338 
339 	if (target_value != NULL)
340 	{
341 		g_object_set_data_full (G_OBJECT (gtk_action),
342 					AMTK_GVARIANT_PARAM_KEY,
343 					g_variant_ref (target_value),
344 					(GDestroyNotify)g_variant_unref);
345 	}
346 
347 	g_signal_connect_object (gtk_action,
348 				 "activate",
349 				 G_CALLBACK (gtk_action_activate_cb),
350 				 g_action,
351 				 0);
352 
353 	g_object_bind_property (g_action, "enabled",
354 				gtk_action, "sensitive",
355 				G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
356 
357 out:
358 	g_free (g_action_name);
359 
360 	if (target_value != NULL)
361 	{
362 		g_variant_unref (target_value);
363 	}
364 }
365 
366 /**
367  * amtk_utils_create_gtk_action:
368  * @g_action_map: a #GActionMap.
369  * @detailed_g_action_name_with_prefix: a detailed #GAction name with the
370  *   #GActionMap prefix; the #GAction must be present in @g_action_map.
371  * @gtk_action_group: a #GtkActionGroup.
372  * @gtk_action_name: the name of the #GtkAction to create and add to
373  *   @gtk_action_group.
374  *
375  * Utility function to be able to port an application gradually to #GAction and
376  * #AmtkActionInfo, when #GtkUIManager is still used. This function goes one
377  * step further compared to amtk_utils_bind_g_action_to_gtk_action(). With
378  * amtk_utils_bind_g_action_to_gtk_action(), only the #GAction must exist. With
379  * amtk_utils_create_gtk_action(), both the #GAction and #AmtkActionInfo must
380  * exist (so typically you need to convert the #GtkActionEntry's into
381  * #AmtkActionInfoEntry's).
382  *
383  * This function creates a #GtkAction from a #GAction plus its corresponding
384  * #AmtkActionInfo.
385  *
386  * The #GtkAction is created with the information provided by the
387  * #AmtkActionInfo (retrieved with amtk_action_info_central_store_lookup() with
388  * @detailed_g_action_name_with_prefix as argument). Only the first accelerator
389  * is taken into account.
390  *
391  * Once the #GtkAction is created, it is added to the @gtk_action_group, and
392  * amtk_utils_bind_g_action_to_gtk_action() is called.
393  *
394  * Since: 4.0
395  */
396 void
amtk_utils_create_gtk_action(GActionMap * g_action_map,const gchar * detailed_g_action_name_with_prefix,GtkActionGroup * gtk_action_group,const gchar * gtk_action_name)397 amtk_utils_create_gtk_action (GActionMap     *g_action_map,
398 			      const gchar    *detailed_g_action_name_with_prefix,
399 			      GtkActionGroup *gtk_action_group,
400 			      const gchar    *gtk_action_name)
401 {
402 	AmtkActionInfoCentralStore *central_store;
403 	AmtkActionInfo *g_action_info;
404 	GtkAction *gtk_action;
405 	const gchar * const *accels;
406 	const gchar *first_accel;
407 	const gchar *detailed_g_action_name_without_prefix;
408 
409 	g_return_if_fail (G_IS_ACTION_MAP (g_action_map));
410 	g_return_if_fail (detailed_g_action_name_with_prefix != NULL);
411 	g_return_if_fail (GTK_IS_ACTION_GROUP (gtk_action_group));
412 	g_return_if_fail (gtk_action_name != NULL);
413 
414 	central_store = amtk_action_info_central_store_get_singleton ();
415 	g_action_info = amtk_action_info_central_store_lookup (central_store, detailed_g_action_name_with_prefix);
416 
417 	gtk_action = gtk_action_new (gtk_action_name,
418 				     amtk_action_info_get_label (g_action_info),
419 				     amtk_action_info_get_tooltip (g_action_info),
420 				     NULL);
421 
422 	gtk_action_set_icon_name (gtk_action, amtk_action_info_get_icon_name (g_action_info));
423 
424 	accels = amtk_action_info_get_accels (g_action_info);
425 	first_accel = accels != NULL ? accels[0] : NULL;
426 
427 	gtk_action_group_add_action_with_accel (gtk_action_group, gtk_action, first_accel);
428 	g_object_unref (gtk_action);
429 
430 	detailed_g_action_name_without_prefix = strchr (detailed_g_action_name_with_prefix, '.');
431 	if (detailed_g_action_name_without_prefix != NULL)
432 	{
433 		detailed_g_action_name_without_prefix++;
434 	}
435 	else
436 	{
437 		detailed_g_action_name_without_prefix = detailed_g_action_name_with_prefix;
438 	}
439 
440 	amtk_utils_bind_g_action_to_gtk_action (g_action_map,
441 						detailed_g_action_name_without_prefix,
442 						gtk_action_group,
443 						gtk_action_name);
444 }
445 G_GNUC_END_IGNORE_DEPRECATIONS
446