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