1 /*
2  *      fm-gtk-utils.c
3  *
4  *      Copyright 2009 PCMan <pcman.tw@gmail.com>
5  *      Copyright 2012-2016 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
6  *      Copyright 2012 Vadim Ushakov <igeekless@gmail.com>
7  *
8  *      This program is free software; you can redistribute it and/or modify
9  *      it under the terms of the GNU General Public License as published by
10  *      the Free Software Foundation; either version 2 of the License, or
11  *      (at your option) any later version.
12  *
13  *      This program is distributed in the hope that it will be useful,
14  *      but WITHOUT ANY WARRANTY; without even the implied warranty of
15  *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  *      GNU General Public License for more details.
17  *
18  *      You should have received a copy of the GNU General Public License
19  *      along with this program; if not, write to the Free Software
20  *      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21  *      MA 02110-1301, USA.
22  */
23 
24 /**
25  * SECTION:fm-gtk-utils
26  * @short_description: Different widgets and utilities that use GTK+
27  * @title: Libfm-gtk utils
28  *
29  * @include: libfm/fm-gtk.h
30  *
31  */
32 
33 #ifdef HAVE_CONFIG_H
34 #include <config.h>
35 #endif
36 #include "gtk-compat.h"
37 
38 #include <glib/gi18n-lib.h>
39 #include <gio/gdesktopappinfo.h>
40 
41 #include "fm-gtk-utils.h"
42 #include "fm-file-ops-job.h"
43 #include "fm-progress-dlg.h"
44 #include "fm-path-entry.h"
45 #include "fm-app-chooser-dlg.h"
46 #include "fm-monitor.h"
47 
48 #include "fm-config.h"
49 
50 static GtkDialog*   _fm_get_user_input_dialog   (GtkWindow* parent, const char* title, const char* msg);
51 static gchar*       _fm_user_input_dialog_run   (GtkDialog* dlg, GtkEntry *entry, GtkWidget *extra);
52 
53 /**
54  * fm_show_error
55  * @parent: a window to place dialog over it
56  * @title: title for dialog window
57  * @msg: message to present
58  *
59  * Presents error message to user and gives user no choices but close.
60  *
61  * Before 0.1.16 this call had different arguments.
62  *
63  * Since: 0.1.0
64  */
fm_show_error(GtkWindow * parent,const char * title,const char * msg)65 void fm_show_error(GtkWindow* parent, const char* title, const char* msg)
66 {
67     GtkWidget* dlg = gtk_message_dialog_new(parent, 0,
68                                             GTK_MESSAGE_ERROR,
69                                             GTK_BUTTONS_OK, "%s", msg);
70     /* g_message("(!) %s", msg); */
71     gtk_window_set_title(GTK_WINDOW(dlg), title ? title : _("Error"));
72     /* #3606577: error window if parent is desktop is below other windows */
73     gtk_window_set_keep_above(GTK_WINDOW(dlg), TRUE);
74     gtk_dialog_run(GTK_DIALOG(dlg));
75     gtk_widget_destroy(dlg);
76 }
77 
78 /**
79  * fm_yes_no
80  * @parent: a window to place dialog over it
81  * @title: title for dialog window
82  * @question: the question to present to the user
83  * @default_yes: the default answer
84  *
85  * Presents the question to user and gives user choices 'Yes' and 'No'.
86  *
87  * Before 0.1.16 this call had different arguments.
88  *
89  * Returns: %TRUE if user chose 'Yes'.
90  *
91  * Since: 0.1.0
92  */
fm_yes_no(GtkWindow * parent,const char * title,const char * question,gboolean default_yes)93 gboolean fm_yes_no(GtkWindow* parent, const char* title, const char* question, gboolean default_yes)
94 {
95     int ret;
96     GtkDialog* dlg = GTK_DIALOG(gtk_message_dialog_new_with_markup(parent, 0,
97                                                         GTK_MESSAGE_QUESTION,
98                                                         GTK_BUTTONS_YES_NO,
99                                                         "%s", question));
100     gtk_window_set_title(GTK_WINDOW(dlg), title ? title : _("Confirm"));
101     gtk_dialog_set_default_response(dlg, default_yes ? GTK_RESPONSE_YES : GTK_RESPONSE_NO);
102     /* #3300797: Delete prompt isn't on the first layer */
103     gtk_window_set_keep_above(GTK_WINDOW(dlg), TRUE);
104     ret = gtk_dialog_run(dlg);
105     gtk_widget_destroy((GtkWidget*)dlg);
106     return ret == GTK_RESPONSE_YES;
107 }
108 
109 /**
110  * fm_ok_cancel
111  * @parent: a window to place dialog over it
112  * @title: title for dialog window
113  * @question: the question to show to the user
114  * @default_ok: the default answer
115  *
116  * Presents the question to user and gives user choices 'OK' and 'Cancel'.
117  *
118  * Before 0.1.16 this call had different arguments.
119  *
120  * Returns: %TRUE if user chose 'OK'.
121  *
122  * Since: 0.1.0
123  */
fm_ok_cancel(GtkWindow * parent,const char * title,const char * question,gboolean default_ok)124 gboolean fm_ok_cancel(GtkWindow* parent, const char* title, const char* question, gboolean default_ok)
125 {
126     int ret;
127     GtkDialog* dlg = GTK_DIALOG(gtk_message_dialog_new_with_markup(parent, 0,
128                                                         GTK_MESSAGE_QUESTION,
129                                                         GTK_BUTTONS_OK_CANCEL,
130                                                         "%s", question));
131     gtk_window_set_title(GTK_WINDOW(dlg), title ? title : _("Confirm"));
132     gtk_dialog_set_default_response(dlg, default_ok ? GTK_RESPONSE_OK : GTK_RESPONSE_CANCEL);
133     ret = gtk_dialog_run(dlg);
134     gtk_widget_destroy((GtkWidget*)dlg);
135     return ret == GTK_RESPONSE_OK;
136 }
137 
138 /**
139  * fm_ask
140  * @parent: toplevel parent widget
141  * @title: title for the window with question
142  * @question: the question to show to the user
143  * @...: a NULL terminated list of button labels
144  *
145  * Ask the user a question with several options provided.
146  *
147  * Before 0.1.16 this call had different arguments.
148  *
149  * Return value: the index of selected button, or -1 if the dialog is closed.
150  *
151  * Since: 0.1.0
152  */
fm_ask(GtkWindow * parent,const char * title,const char * question,...)153 int fm_ask(GtkWindow* parent, const char* title, const char* question, ...)
154 {
155     int ret;
156     va_list args;
157     va_start (args, question);
158     ret = fm_ask_valist(parent, title, question, args);
159     va_end (args);
160     return ret;
161 }
162 
163 /**
164  * fm_askv
165  * @parent: toplevel parent widget
166  * @title: title for the window with question
167  * @question: the question to show to the user
168  * @options: a NULL terminated list of button labels
169  *
170  * Ask the user a question with several options provided.
171  *
172  * Before 0.1.16 this call had different arguments.
173  *
174  * Return value: the index of selected button, or -1 if the dialog is closed.
175  *
176  * Since: 0.1.0
177  */
fm_askv(GtkWindow * parent,const char * title,const char * question,char * const * options)178 int fm_askv(GtkWindow* parent, const char* title, const char* question, char* const* options)
179 {
180     int ret;
181     guint id = 1;
182     GtkDialog* dlg = GTK_DIALOG(gtk_message_dialog_new_with_markup(parent, 0,
183                                                         GTK_MESSAGE_QUESTION, 0,
184                                                         "%s", question));
185     gtk_window_set_title(GTK_WINDOW(dlg), title ? title : _("Question"));
186     /* FIXME: need to handle defualt button and alternative button
187      * order problems. */
188     while(*options)
189     {
190         /* FIXME: handle button image and stock buttons */
191         /*GtkWidget* btn =*/
192         gtk_dialog_add_button(dlg, *options, id);
193         ++options;
194         ++id;
195     }
196     ret = gtk_dialog_run(dlg);
197     if(ret >= 1)
198         ret -= 1;
199     else
200         ret = -1;
201     gtk_widget_destroy((GtkWidget*)dlg);
202     return ret;
203 }
204 
205 /**
206  * fm_ask_valist
207  * @parent: toplevel parent widget
208  * @title: title for the window with question
209  * @question: the question to show to the user
210  * @options: va_arg list of button labels
211  *
212  * Ask the user a question with several options provided.
213  *
214  * Before 0.1.16 this call had different arguments.
215  *
216  * Return value: the index of selected button, or -1 if the dialog is closed.
217  *
218  * Since: 0.1.0
219  */
fm_ask_valist(GtkWindow * parent,const char * title,const char * question,va_list options)220 int fm_ask_valist(GtkWindow* parent, const char* title, const char* question, va_list options)
221 {
222     GArray* opts = g_array_sized_new(TRUE, TRUE, sizeof(char*), 6);
223     gint ret;
224     const char* opt = va_arg(options, const char*);
225     while(opt)
226     {
227         g_array_append_val(opts, opt);
228         opt = va_arg (options, const char *);
229     }
230     ret = fm_askv(parent, title, question, &opts->data);
231     g_array_free(opts, TRUE);
232     return ret;
233 }
234 
235 
236 /**
237  * fm_get_user_input
238  * @parent: a window to place dialog over it
239  * @title: title for dialog window
240  * @msg: the message to present to the user
241  * @default_text: the default answer
242  *
243  * Presents the message to user and retrieves entered text.
244  * Returned data should be freed with g_free() after usage.
245  *
246  * Returns: (transfer full): entered text.
247  *
248  * Since: 0.1.0
249  */
fm_get_user_input(GtkWindow * parent,const char * title,const char * msg,const char * default_text)250 gchar* fm_get_user_input(GtkWindow* parent, const char* title, const char* msg, const char* default_text)
251 {
252     GtkDialog* dlg = _fm_get_user_input_dialog( parent, title, msg);
253     GtkEntry* entry = GTK_ENTRY(gtk_entry_new());
254     gtk_entry_set_activates_default(entry, TRUE);
255 
256     if(default_text && default_text[0])
257         gtk_entry_set_text(entry, default_text);
258 
259     return _fm_user_input_dialog_run(dlg, entry, NULL);
260 }
261 
262 /**
263  * fm_get_user_input_n
264  * @parent: a window to place dialog over it
265  * @title: title for dialog window
266  * @msg: the message to present to the user
267  * @default_text: the default answer
268  * @n: which part of default answer should be selected
269  * @extra: additional widgets to display (can be focusable)
270  *
271  * Presents the message to user and retrieves entered text.
272  * In presented dialog the part of @default_text with length @n will be
273  * preselected for edition (n < 0 means select all).
274  * Returned data should be freed with g_free() after usage.
275  *
276  * Returns: (transfer full): entered text.
277  *
278  * Since: 1.2.0
279  */
fm_get_user_input_n(GtkWindow * parent,const char * title,const char * msg,const char * default_text,gint n,GtkWidget * extra)280 gchar* fm_get_user_input_n(GtkWindow* parent, const char* title, const char* msg,
281                            const char* default_text, gint n, GtkWidget* extra)
282 {
283     GtkDialog* dlg = _fm_get_user_input_dialog( parent, title, msg);
284     GtkEntry* entry = GTK_ENTRY(gtk_entry_new());
285     gtk_entry_set_activates_default(entry, TRUE);
286 
287     if(default_text && default_text[0])
288     {
289         gtk_entry_set_text(entry, default_text);
290         gtk_editable_select_region(GTK_EDITABLE(entry), 0, n);
291     }
292 
293     return _fm_user_input_dialog_run(dlg, entry, extra);
294 }
295 
296 /**
297  * fm_get_user_input_path
298  * @parent: a window to place dialog over it
299  * @title: title for dialog window
300  * @msg: the message to present to the user
301  * @default_path: the default path
302  *
303  * Presents the message to user and retrieves entered path string.
304  * Returned data should be freed with fm_path_unref() after usage.
305  *
306  * Returns: (transfer full): entered text.
307  *
308  * Since: 0.1.0
309  *
310  * Deprecated: 1.2.0:
311  */
fm_get_user_input_path(GtkWindow * parent,const char * title,const char * msg,FmPath * default_path)312 FmPath* fm_get_user_input_path(GtkWindow* parent, const char* title, const char* msg, FmPath* default_path)
313 {
314 
315     GtkDialog* dlg = _fm_get_user_input_dialog( parent, title, msg);
316     GtkEntry* entry = GTK_ENTRY(gtk_entry_new());
317     char *str, *path_str = NULL;
318     FmPath* path;
319 
320     gtk_entry_set_activates_default(entry, TRUE);
321 
322     if(default_path)
323     {
324         path_str = fm_path_display_name(default_path, FALSE);
325         gtk_entry_set_text(entry, path_str);
326     }
327 
328     str = _fm_user_input_dialog_run(dlg, entry, NULL);
329     path = fm_path_new_for_str(str);
330 
331     g_free(path_str);
332     g_free(str);
333     return path;
334 }
335 
336 
fm_get_user_input_rename(GtkWindow * parent,const char * title,const char * msg,const char * default_text)337 static gchar* fm_get_user_input_rename(GtkWindow* parent, const char* title, const char* msg, const char* default_text)
338 {
339     GtkDialog* dlg = _fm_get_user_input_dialog( parent, title, msg);
340     GtkEntry* entry = GTK_ENTRY(gtk_entry_new());
341     gtk_entry_set_activates_default(entry, TRUE);
342 
343     if(default_text && default_text[0])
344     {
345         gtk_entry_set_text(entry, default_text);
346         /* only select filename part without extension name. */
347         if(default_text[1])
348         {
349             /* FIXME: handle the special case for *.tar.gz or *.tar.bz2
350              * We should exam the file extension with g_content_type_guess, and
351              * find out a longest valid extension name.
352              * For example, the extension name of foo.tar.gz is .tar.gz, not .gz. */
353             const char* dot = g_utf8_strrchr(default_text, -1, '.');
354             if(dot)
355                 gtk_editable_select_region(GTK_EDITABLE(entry), 0, g_utf8_pointer_to_offset(default_text, dot));
356             else
357                 gtk_editable_select_region(GTK_EDITABLE(entry), 0, -1);
358             /*
359             const char* dot = default_text;
360             while( dot = g_utf8_strchr(dot + 1, -1, '.') )
361             {
362                 gboolean uncertain;
363                 char* type = g_content_type_guess(dot-1, NULL, 0, &uncertain);
364                 if(!g_content_type_is_unknown(type))
365                 {
366                     g_free(type);
367                     gtk_editable_select_region(entry, 0, g_utf8_pointer_to_offset(default_text, dot));
368                     break;
369                 }
370                 g_free(type);
371             }
372             */
373         }
374     }
375 
376     return _fm_user_input_dialog_run(dlg, entry, NULL);
377 }
378 
_fm_get_user_input_dialog(GtkWindow * parent,const char * title,const char * msg)379 static GtkDialog* _fm_get_user_input_dialog(GtkWindow* parent, const char* title, const char* msg)
380 {
381     GtkDialog* dlg = GTK_DIALOG(gtk_dialog_new_with_buttons(title, parent,
382 #if GTK_CHECK_VERSION(3, 0, 0)
383                                 0,
384 #else
385                                 GTK_DIALOG_NO_SEPARATOR,
386 #endif
387                                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
388                                 GTK_STOCK_OK, GTK_RESPONSE_OK, NULL));
389     GtkWidget* label = gtk_label_new(msg);
390     GtkBox* box = (GtkBox*)gtk_dialog_get_content_area(dlg);
391     gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
392 
393     gtk_dialog_set_alternative_button_order(dlg, GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1);
394     gtk_box_set_spacing(box, 6);
395     gtk_box_pack_start(box, label, FALSE, TRUE, 6);
396 
397     gtk_container_set_border_width(GTK_CONTAINER(box), 12);
398     gtk_container_set_border_width(GTK_CONTAINER(dlg), 5);
399     gtk_dialog_set_default_response(dlg, GTK_RESPONSE_OK);
400     gtk_window_set_default_size(GTK_WINDOW(dlg), 480, -1);
401 
402     return dlg;
403 }
404 
_fm_user_input_dialog_run(GtkDialog * dlg,GtkEntry * entry,GtkWidget * extra)405 static gchar* _fm_user_input_dialog_run(GtkDialog* dlg, GtkEntry *entry,
406                                         GtkWidget *extra)
407 {
408     char* str = NULL;
409     GtkBox *box = GTK_BOX(gtk_dialog_get_content_area(dlg));
410     int sel_start, sel_end;
411     gboolean has_sel;
412 
413     /* FIXME: this workaround is used to overcome bug of gtk+.
414      * gtk+ seems to ignore select region and select all text for entry in dialog. */
415     has_sel = gtk_editable_get_selection_bounds(GTK_EDITABLE(entry), &sel_start, &sel_end);
416     gtk_box_pack_start(box, GTK_WIDGET(entry), FALSE, TRUE, extra ? 0 : 6);
417     if(extra)
418         gtk_box_pack_start(box, extra, FALSE, TRUE, 0);
419     gtk_widget_show_all(GTK_WIDGET(dlg));
420 
421     if(has_sel)
422         gtk_editable_select_region(GTK_EDITABLE(entry), sel_start, sel_end);
423 
424     while(gtk_dialog_run(dlg) == GTK_RESPONSE_OK)
425     {
426         const char* pstr = gtk_entry_get_text(entry);
427         if( pstr && *pstr )
428         {
429             str = g_strdup(pstr);
430             break;
431         }
432     }
433     gtk_widget_destroy(GTK_WIDGET(dlg));
434     return str;
435 }
436 
on_update_img_preview(GtkFileChooser * chooser,GtkImage * img)437 static void on_update_img_preview( GtkFileChooser *chooser, GtkImage* img )
438 {
439     char* file = gtk_file_chooser_get_preview_filename(chooser);
440     GdkPixbuf* pix = NULL;
441     if(file)
442     {
443         pix = gdk_pixbuf_new_from_file_at_scale( file, 128, 128, TRUE, NULL );
444         g_free( file );
445     }
446     if(pix)
447     {
448         gtk_file_chooser_set_preview_widget_active(chooser, TRUE);
449         gtk_image_set_from_pixbuf(img, pix);
450         g_object_unref(pix);
451     }
452     else
453     {
454         gtk_image_clear(img);
455         gtk_file_chooser_set_preview_widget_active(chooser, FALSE);
456     }
457 }
458 
fm_add_image_preview_to_file_chooser(GtkFileChooser * chooser)459 static gulong fm_add_image_preview_to_file_chooser(GtkFileChooser* chooser)
460 {
461     GtkWidget* img_preview = gtk_image_new();
462     gtk_misc_set_alignment(GTK_MISC(img_preview), 0.5, 0.0);
463     gtk_widget_set_size_request(img_preview, 128, 128);
464     gtk_file_chooser_set_preview_widget(chooser, img_preview);
465     return g_signal_connect(chooser, "update-preview", G_CALLBACK(on_update_img_preview), img_preview);
466 }
467 
468 /**
469  * fm_select_file
470  * @parent: a window to place dialog over it
471  * @title: title for dialog window
472  * @default_folder: the starting folder path
473  * @local_only: %TRUE if select only local paths
474  * @show_preview: %TRUE to show file preview
475  * @...: (element-type GtkFileFilter): optional filters
476  *
477  * Presents the message to user and lets him/her to select a file.
478  * Returned data should be freed with fm_path_unref() after usage.
479  *
480  * Returns: (transfer full): selected file path or %NULL if dialog was closed.
481  *
482  * Since: 1.0.0
483  */
484 /* TODO: support selecting multiple files */
fm_select_file(GtkWindow * parent,const char * title,const char * default_folder,gboolean local_only,gboolean show_preview,...)485 FmPath* fm_select_file(GtkWindow* parent,
486                         const char* title,
487                         const char* default_folder,
488                         gboolean local_only,
489                         gboolean show_preview,
490                         /* filter1, filter2, ..., NULL */ ...)
491 {
492     FmPath* path;
493     GtkFileChooser* chooser;
494     GtkFileFilter* filter;
495     gulong handler_id = 0;
496     va_list args;
497 
498     chooser = (GtkFileChooser*)gtk_file_chooser_dialog_new(
499                                         title, parent, GTK_FILE_CHOOSER_ACTION_OPEN,
500                                         GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
501                                         GTK_STOCK_OK, GTK_RESPONSE_OK,
502                                         NULL);
503     gtk_dialog_set_alternative_button_order(GTK_DIALOG(chooser),
504                                         GTK_RESPONSE_CANCEL,
505                                         GTK_RESPONSE_OK, NULL);
506     if(local_only)
507         gtk_file_chooser_set_local_only(chooser, TRUE);
508 
509     if(default_folder)
510         gtk_file_chooser_set_current_folder(chooser, default_folder);
511 
512     va_start(args, show_preview);
513     while((filter = va_arg(args, GtkFileFilter*)))
514     {
515         gtk_file_chooser_add_filter(chooser, filter);
516     }
517     va_end (args);
518 
519     if(show_preview)
520         handler_id = fm_add_image_preview_to_file_chooser(chooser);
521 
522     if(gtk_dialog_run(GTK_DIALOG(chooser)) == GTK_RESPONSE_OK)
523     {
524         GFile* file = gtk_file_chooser_get_file(chooser);
525         path = fm_path_new_for_gfile(file);
526         g_object_unref(file);
527     }
528     else
529         path = NULL;
530     if(handler_id > 0)
531         g_signal_handler_disconnect(chooser, handler_id);
532     gtk_widget_destroy(GTK_WIDGET(chooser));
533     return path;
534 }
535 
536 /**
537  * fm_select_folder
538  * @parent: a window to place dialog over it
539  * @title: title for dialog window
540  *
541  * Presents the message to user and lets him/her to select a folder.
542  * Returned data should be freed with fm_path_unref() after usage.
543  *
544  * Before 0.1.16 this call had different arguments.
545  *
546  * Returns: (transfer full): selected folder path or %NULL if dialog was closed.
547  *
548  * Since: 0.1.0
549  */
fm_select_folder(GtkWindow * parent,const char * title)550 FmPath* fm_select_folder(GtkWindow* parent, const char* title)
551 {
552     FmPath* path;
553     GtkFileChooser* chooser;
554     chooser = (GtkFileChooser*)gtk_file_chooser_dialog_new(
555                                         title ? title : _("Select Folder"),
556                                         parent, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
557                                         GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
558                                         GTK_STOCK_OK, GTK_RESPONSE_OK,
559                                         NULL);
560     gtk_dialog_set_alternative_button_order(GTK_DIALOG(chooser),
561                                         GTK_RESPONSE_CANCEL,
562                                         GTK_RESPONSE_OK, NULL);
563     if( gtk_dialog_run(GTK_DIALOG(chooser)) == GTK_RESPONSE_OK )
564     {
565         GFile* file = gtk_file_chooser_get_file(chooser);
566         path = fm_path_new_for_gfile(file);
567         g_object_unref(file);
568     }
569     else
570         path = NULL;
571     gtk_widget_destroy(GTK_WIDGET(chooser));
572     return path;
573 }
574 
575 typedef enum
576 {
577     MOUNT_VOLUME,
578     MOUNT_GFILE,
579     UMOUNT_MOUNT,
580     EJECT_MOUNT,
581     EJECT_VOLUME
582 }MountAction;
583 
584 struct MountData
585 {
586     GMainLoop *loop;
587     MountAction action;
588     GError* err;
589     gboolean ret;
590 };
591 
on_mount_action_finished(GObject * src,GAsyncResult * res,gpointer user_data)592 static void on_mount_action_finished(GObject* src, GAsyncResult *res, gpointer user_data)
593 {
594     struct MountData* data = user_data;
595 
596     switch(data->action)
597     {
598     case MOUNT_VOLUME:
599         data->ret = g_volume_mount_finish(G_VOLUME(src), res, &data->err);
600         break;
601     case MOUNT_GFILE:
602         data->ret = g_file_mount_enclosing_volume_finish(G_FILE(src), res, &data->err);
603         break;
604     case UMOUNT_MOUNT:
605         data->ret = g_mount_unmount_with_operation_finish(G_MOUNT(src), res, &data->err);
606         break;
607     case EJECT_MOUNT:
608         data->ret = g_mount_eject_with_operation_finish(G_MOUNT(src), res, &data->err);
609         break;
610     case EJECT_VOLUME:
611         data->ret = g_volume_eject_with_operation_finish(G_VOLUME(src), res, &data->err);
612         break;
613     }
614     g_main_loop_quit(data->loop);
615 }
616 
prepare_unmount(GMount * mount)617 static void prepare_unmount(GMount* mount)
618 {
619     /* ensure that CWD is not on the mounted filesystem. */
620     char* cwd_str = g_get_current_dir();
621     GFile* cwd = g_file_new_for_path(cwd_str);
622     GFile* root = g_mount_get_root(mount);
623     g_free(cwd_str);
624     /* FIXME: This cannot cover 100% cases since symlinks are not checked.
625      * There may be other cases that cwd is actually under mount root
626      * but checking prefix is not enough. We already did our best, though. */
627     if(g_file_has_prefix(cwd, root))
628         g_chdir("/");
629     g_object_unref(cwd);
630     g_object_unref(root);
631 }
632 
fm_do_mount(GtkWindow * parent,GObject * obj,MountAction action,gboolean interactive)633 static gboolean fm_do_mount(GtkWindow* parent, GObject* obj, MountAction action, gboolean interactive)
634 {
635     gboolean ret;
636     struct MountData* data = g_new0(struct MountData, 1);
637     /* bug #3615234: it seems GtkMountOperations is buggy and sometimes leaves
638        parent window reference intact while destroys itself so it leads to
639        severe memory corruption, therefore we pass here NULL as parent window
640        to gtk_mount_operation_new() to not bind it to anything as a workaround */
641     GMountOperation* op = interactive ? gtk_mount_operation_new(NULL) : NULL;
642     GCancellable* cancellable = g_cancellable_new();
643 
644     data->loop = g_main_loop_new (NULL, TRUE);
645     data->action = action;
646 
647     switch(data->action)
648     {
649     case MOUNT_VOLUME:
650         g_volume_mount(G_VOLUME(obj), 0, op, cancellable, on_mount_action_finished, data);
651         break;
652     case MOUNT_GFILE:
653         g_file_mount_enclosing_volume(G_FILE(obj), 0, op, cancellable, on_mount_action_finished, data);
654         break;
655     case UMOUNT_MOUNT:
656         prepare_unmount(G_MOUNT(obj));
657         g_mount_unmount_with_operation(G_MOUNT(obj), G_MOUNT_UNMOUNT_NONE, op, cancellable, on_mount_action_finished, data);
658         break;
659     case EJECT_MOUNT:
660         prepare_unmount(G_MOUNT(obj));
661         g_mount_eject_with_operation(G_MOUNT(obj), G_MOUNT_UNMOUNT_NONE, op, cancellable, on_mount_action_finished, data);
662         break;
663     case EJECT_VOLUME:
664         {
665             GMount* mnt = g_volume_get_mount(G_VOLUME(obj));
666             if (mnt) /* it might be unmounted already */
667             {
668                 prepare_unmount(mnt);
669                 g_object_unref(mnt);
670             }
671             g_volume_eject_with_operation(G_VOLUME(obj), G_MOUNT_UNMOUNT_NONE, op, cancellable, on_mount_action_finished, data);
672         }
673         break;
674     }
675 
676     /* FIXME: create progress window with busy cursor */
677     if (g_main_loop_is_running(data->loop))
678     {
679         GDK_THREADS_LEAVE();
680         g_main_loop_run(data->loop);
681         GDK_THREADS_ENTER();
682     }
683 
684     g_main_loop_unref(data->loop);
685 
686     ret = data->ret;
687     if(data->err)
688     {
689         if(interactive)
690         {
691             if(data->err->domain == G_IO_ERROR)
692             {
693                 if(data->err->code == G_IO_ERROR_FAILED)
694                 {
695                     /* Generate a more human-readable error message instead of using a gvfs one. */
696 
697                     /* The original error message is something like:
698                      * Error unmounting: umount exited with exit code 1:
699                      * helper failed with: umount: only root can unmount
700                      * UUID=18cbf00c-e65f-445a-bccc-11964bdea05d from /media/sda4 */
701 
702                     /* Why they pass this back to us?
703                      * This is not human-readable for the users at all. */
704 
705                     if(strstr(data->err->message, "only root can "))
706                     {
707                         g_debug("%s", data->err->message);
708                         g_free(data->err->message);
709                         data->err->message = g_strdup(_("Only system administrators have the permission to do this."));
710                     }
711                 }
712                 else if(data->err->code == G_IO_ERROR_FAILED_HANDLED)
713                     interactive = FALSE;
714             }
715             if(interactive)
716                 fm_show_error(parent, NULL, data->err->message);
717         }
718         g_error_free(data->err);
719     }
720 
721     g_free(data);
722     g_object_unref(cancellable);
723     if(op)
724         g_object_unref(op);
725     return ret;
726 }
727 
728 /**
729  * fm_mount_path
730  * @parent: a window to place dialog over it
731  * @path: a path to the volume
732  * @interactive: %TRUE to open dialog window
733  *
734  * Mounts a volume.
735  *
736  * Returns: %TRUE in case of success.
737  *
738  * Since: 0.1.0
739  */
fm_mount_path(GtkWindow * parent,FmPath * path,gboolean interactive)740 gboolean fm_mount_path(GtkWindow* parent, FmPath* path, gboolean interactive)
741 {
742     GFile* gf = fm_path_to_gfile(path);
743     gboolean ret = fm_do_mount(parent, G_OBJECT(gf), MOUNT_GFILE, interactive);
744     g_object_unref(gf);
745     return ret;
746 }
747 
748 /**
749  * fm_mount_volume
750  * @parent: a window to place dialog over it
751  * @vol: a volume to mount
752  * @interactive: %TRUE to open dialog window
753  *
754  * Mounts a volume.
755  *
756  * Returns: %TRUE in case of success.
757  *
758  * Since: 0.1.0
759  */
fm_mount_volume(GtkWindow * parent,GVolume * vol,gboolean interactive)760 gboolean fm_mount_volume(GtkWindow* parent, GVolume* vol, gboolean interactive)
761 {
762     return fm_do_mount(parent, G_OBJECT(vol), MOUNT_VOLUME, interactive);
763 }
764 
765 /**
766  * fm_unmount_mount
767  * @parent: a window to place dialog over it
768  * @mount: the mounted volume
769  * @interactive: %TRUE to open dialog window
770  *
771  * Unmounts a volume.
772  *
773  * Returns: %TRUE in case of success.
774  *
775  * Since: 0.1.0
776  */
fm_unmount_mount(GtkWindow * parent,GMount * mount,gboolean interactive)777 gboolean fm_unmount_mount(GtkWindow* parent, GMount* mount, gboolean interactive)
778 {
779     return fm_do_mount(parent, G_OBJECT(mount), UMOUNT_MOUNT, interactive);
780 }
781 
782 /**
783  * fm_unmount_volume
784  * @parent: a window to place dialog over it
785  * @vol: the mounted volume
786  * @interactive: %TRUE to open dialog window
787  *
788  * Unmounts a volume.
789  *
790  * Returns: %TRUE in case of success.
791  *
792  * Since: 0.1.0
793  */
fm_unmount_volume(GtkWindow * parent,GVolume * vol,gboolean interactive)794 gboolean fm_unmount_volume(GtkWindow* parent, GVolume* vol, gboolean interactive)
795 {
796     GMount* mount = g_volume_get_mount(vol);
797     gboolean ret;
798     if(!mount)
799         return FALSE;
800     ret = fm_do_mount(parent, G_OBJECT(vol), UMOUNT_MOUNT, interactive);
801     g_object_unref(mount);
802     return ret;
803 }
804 
805 /**
806  * fm_eject_mount
807  * @parent: a window to place dialog over it
808  * @mount: the mounted media
809  * @interactive: %TRUE to open dialog window
810  *
811  * Ejects the media in @mount.
812  *
813  * Returns: %TRUE in case of success.
814  *
815  * Since: 0.1.0
816  */
fm_eject_mount(GtkWindow * parent,GMount * mount,gboolean interactive)817 gboolean fm_eject_mount(GtkWindow* parent, GMount* mount, gboolean interactive)
818 {
819     return fm_do_mount(parent, G_OBJECT(mount), EJECT_MOUNT, interactive);
820 }
821 
822 /**
823  * fm_eject_volume
824  * @parent: a window to place dialog over it
825  * @vol: the mounted media
826  * @interactive: %TRUE to open dialog window
827  *
828  * Ejects the media in @vol.
829  *
830  * Returns: %TRUE in case of success.
831  *
832  * Since: 0.1.0
833  */
fm_eject_volume(GtkWindow * parent,GVolume * vol,gboolean interactive)834 gboolean fm_eject_volume(GtkWindow* parent, GVolume* vol, gboolean interactive)
835 {
836     return fm_do_mount(parent, G_OBJECT(vol), EJECT_VOLUME, interactive);
837 }
838 
839 
840 /* File operations */
841 /* FIXME: only show the progress dialog if the job isn't finished
842  * in 1 sec. */
843 
844 /**
845  * fm_copy_files
846  * @parent: a window to place progress dialog over it
847  * @files: list of files to copy
848  * @dest_dir: target directory
849  *
850  * Copies files opening progress dialog if that operation takes some time.
851  *
852  * Before 0.1.15 this call had different arguments.
853  *
854  * Since: 0.1.0
855  */
fm_copy_files(GtkWindow * parent,FmPathList * files,FmPath * dest_dir)856 void fm_copy_files(GtkWindow* parent, FmPathList* files, FmPath* dest_dir)
857 {
858     FmFileOpsJob* job = fm_file_ops_job_new(FM_FILE_OP_COPY, files);
859     fm_file_ops_job_set_dest(job, dest_dir);
860     fm_file_ops_job_run_with_progress(parent, job); /* it eats reference! */
861 }
862 
863 /**
864  * fm_move_files
865  * @parent: a window to place progress dialog over it
866  * @files: list of files to move
867  * @dest_dir: directory where to move files to
868  *
869  * Moves files opening progress dialog if that operation takes some time.
870  *
871  * Before 0.1.15 this call had different arguments.
872  *
873  * Since: 0.1.0
874  */
fm_move_files(GtkWindow * parent,FmPathList * files,FmPath * dest_dir)875 void fm_move_files(GtkWindow* parent, FmPathList* files, FmPath* dest_dir)
876 {
877     FmFileOpsJob* job = fm_file_ops_job_new(FM_FILE_OP_MOVE, files);
878     fm_file_ops_job_set_dest(job, dest_dir);
879     fm_file_ops_job_run_with_progress(parent, job); /* it eats reference! */
880 }
881 
882 /**
883  * fm_link_files
884  * @parent:   window to base progress dialog over it
885  * @files:    list of files to make symbolic links to
886  * @dest_dir: directory where symbolic links should be created
887  *
888  * Create symbolic links for some files in the target directory with
889  * progress dialog.
890  *
891  * Since: 1.0.0
892  */
fm_link_files(GtkWindow * parent,FmPathList * files,FmPath * dest_dir)893 void fm_link_files(GtkWindow* parent, FmPathList* files, FmPath* dest_dir)
894 {
895     FmFileOpsJob* job = fm_file_ops_job_new(FM_FILE_OP_LINK, files);
896     fm_file_ops_job_set_dest(job, dest_dir);
897     fm_file_ops_job_run_with_progress(parent, job); /* it eats reference! */
898 }
899 
900 /**
901  * fm_trash_files
902  * @parent: a window to place progress dialog over it
903  * @files: list of files to move to trash
904  *
905  * Removes files into trash can opening progress dialog if that operation
906  * takes some time.
907  *
908  * Before 0.1.15 this call had different arguments.
909  *
910  * Since: 0.1.0
911  */
fm_trash_files(GtkWindow * parent,FmPathList * files)912 void fm_trash_files(GtkWindow* parent, FmPathList* files)
913 {
914     FmFileOpsJob *job;
915     char *msg, *name;
916     int len;
917 
918     if (fm_config->confirm_trash)
919     {
920         len = fm_path_list_get_length(files);
921         if (len == 1)
922         {
923             name = fm_path_display_basename(fm_path_list_peek_head(files));
924             msg = g_strdup_printf(_("Do you want to move the file '%s' to trash can?"), name);
925             g_free(name);
926         }
927         else
928             msg = g_strdup_printf(dngettext(GETTEXT_PACKAGE,
929                                             "Do you want to move the %d selected file to trash can?",
930                                             "Do you want to move the %d selected files to trash can?",
931                                             (gulong)len), len);
932         if (!fm_yes_no(parent, NULL, msg, TRUE))
933         {
934             g_free(msg);
935             return;
936         }
937         g_free(msg);
938     }
939     job = fm_file_ops_job_new(FM_FILE_OP_TRASH, files);
940     fm_file_ops_job_run_with_progress(parent, job); /* it eats reference! */
941 }
942 
943 /**
944  * fm_untrash_files
945  * @parent: a window to place progress dialog over it
946  * @files: list of files to restore
947  *
948  * Restores files from trash can into original place opening progress
949  * dialog if that operation takes some time.
950  *
951  * Before 0.1.15 this call had different arguments.
952  *
953  * Since: 0.1.11
954  */
fm_untrash_files(GtkWindow * parent,FmPathList * files)955 void fm_untrash_files(GtkWindow* parent, FmPathList* files)
956 {
957     FmFileOpsJob* job = fm_file_ops_job_new(FM_FILE_OP_UNTRASH, files);
958     fm_file_ops_job_run_with_progress(parent, job); /* it eats reference! */
959 }
960 
fm_delete_files_internal(GtkWindow * parent,FmPathList * files)961 static void fm_delete_files_internal(GtkWindow* parent, FmPathList* files)
962 {
963     FmFileOpsJob* job = fm_file_ops_job_new(FM_FILE_OP_DELETE, files);
964     fm_file_ops_job_run_with_progress(parent, job); /* it eats reference! */
965 }
966 
967 /**
968  * fm_delete_files
969  * @parent: a window to place progress dialog over it
970  * @files: list of files to delete
971  *
972  * Wipes out files opening progress dialog if that operation takes some time.
973  *
974  * Before 0.1.15 this call had different arguments.
975  *
976  * Since: 0.1.0
977  */
fm_delete_files(GtkWindow * parent,FmPathList * files)978 void fm_delete_files(GtkWindow* parent, FmPathList* files)
979 {
980     char *msg, *name;
981     int len;
982 
983     if (fm_config->confirm_del)
984     {
985         len = fm_path_list_get_length(files);
986         if (len == 1)
987         {
988             name = fm_path_display_basename(fm_path_list_peek_head(files));
989             msg = g_strdup_printf(_("Do you want to delete the file '%s'?"), name);
990             g_free(name);
991         }
992         else
993             msg = g_strdup_printf(dngettext(GETTEXT_PACKAGE,
994                                             "Do you want to delete the %d selected file?",
995                                             "Do you want to delete the %d selected files?",
996                                             (gulong)len), len);
997         if (!fm_yes_no(parent, NULL, msg, TRUE))
998         {
999             g_free(msg);
1000             return;
1001         }
1002         g_free(msg);
1003     }
1004     fm_delete_files_internal(parent, files);
1005 }
1006 
1007 /**
1008  * fm_trash_or_delete_files
1009  * @parent: a window to place progress dialog over it
1010  * @files: list of files to delete
1011  *
1012  * Removes files into trash can if that operation is supported.
1013  * Otherwise erases them. If that operation takes some time then progress
1014  * dialog will be opened.
1015  *
1016  * Before 0.1.15 this call had different arguments.
1017  *
1018  * Since: 0.1.0
1019  */
fm_trash_or_delete_files(GtkWindow * parent,FmPathList * files)1020 void fm_trash_or_delete_files(GtkWindow* parent, FmPathList* files)
1021 {
1022     if( !fm_path_list_is_empty(files) )
1023     {
1024         gboolean all_in_trash = TRUE;
1025         if(fm_config->use_trash)
1026         {
1027             GList* l = fm_path_list_peek_head_link(files);
1028             for(;l;l=l->next)
1029             {
1030                 FmPath* path = FM_PATH(l->data);
1031                 if(!fm_path_is_trash(path))
1032                     all_in_trash = FALSE;
1033             }
1034         }
1035 
1036         /* files already in trash:/// should only be deleted and cannot be trashed again. */
1037         if(fm_config->use_trash && !all_in_trash)
1038             fm_trash_files(parent, files);
1039         else
1040             fm_delete_files(parent, files);
1041     }
1042 }
1043 
1044 /**
1045  * fm_move_or_copy_files_to
1046  * @parent: a window to place progress dialog over it
1047  * @files: list of files
1048  * @is_move: %TRUE to move, %FALSE to copy
1049  *
1050  * Opens a dialog to choose destination directory. If it was not cancelled
1051  * by user then moves or copies @files into chosen directory with progress
1052  * dialog.
1053  *
1054  * Before 0.1.15 this call had different arguments.
1055  *
1056  * Since: 0.1.0
1057  */
fm_move_or_copy_files_to(GtkWindow * parent,FmPathList * files,gboolean is_move)1058 void fm_move_or_copy_files_to(GtkWindow* parent, FmPathList* files, gboolean is_move)
1059 {
1060     FmPath* dest = fm_select_folder(parent, NULL);
1061     if(dest)
1062     {
1063         if(is_move)
1064             fm_move_files(parent, files, dest);
1065         else
1066             fm_copy_files(parent, files, dest);
1067         fm_path_unref(dest);
1068     }
1069 }
1070 
1071 
1072 /**
1073  * fm_rename_file
1074  * @parent: a window to place dialog over it
1075  * @file: the file
1076  *
1077  * Opens a dialog to choose new name for @file. If it was not cancelled
1078  * by user then renames @file.
1079  *
1080  * Before 0.1.15 this call had different arguments.
1081  *
1082  * Since: 0.1.0
1083  */
fm_rename_file(GtkWindow * parent,FmPath * file)1084 void fm_rename_file(GtkWindow* parent, FmPath* file)
1085 {
1086     gchar *old_name, *new_name;
1087     FmPathList *files;
1088     FmFileOpsJob *job;
1089 
1090     /* NOTE: it's better to use fm_file_info_get_edit_name() to get a name
1091        but we cannot get it from FmPath */
1092     old_name = fm_path_display_basename(file);
1093     new_name = fm_get_user_input_rename(parent, _("Rename File"),
1094                                         _("Please enter a new name:"),
1095                                         old_name);
1096     /* if file name wasn't changed then do nothing */
1097     if (new_name == NULL || strcmp(old_name, new_name) == 0)
1098     {
1099         g_free(old_name);
1100         g_free(new_name);
1101         return;
1102     }
1103     g_free(old_name);
1104     files = fm_path_list_new();
1105     fm_path_list_push_tail(files, file);
1106     job = fm_file_ops_job_new(FM_FILE_OP_CHANGE_ATTR, files);
1107     fm_file_ops_job_set_display_name(job, new_name);
1108     g_free(new_name);
1109     fm_path_list_unref(files);
1110     fm_file_ops_job_run_with_progress(parent, job); /* it eats reference! */
1111 }
1112 
_fm_set_file_hidden(GtkWindow * parent,FmPath * file,gboolean hidden)1113 static void _fm_set_file_hidden(GtkWindow *parent, FmPath *file, gboolean hidden)
1114 {
1115     FmPathList *files;
1116     FmFileOpsJob *job;
1117 
1118     files = fm_path_list_new();
1119     fm_path_list_push_tail(files, file);
1120     job = fm_file_ops_job_new(FM_FILE_OP_CHANGE_ATTR, files);
1121     fm_file_ops_job_set_hidden(job, hidden);
1122     fm_path_list_unref(files);
1123     fm_file_ops_job_run_with_progress(parent, job); /* it eats reference! */
1124 }
1125 
1126 /**
1127  * fm_hide_file
1128  * @parent: a window to place progress dialog over it
1129  * @file: the file
1130  *
1131  * Sets file attribute "hidden" for @file.
1132  *
1133  * Since: 1.2.0
1134  */
fm_hide_file(GtkWindow * parent,FmPath * file)1135 void fm_hide_file(GtkWindow* parent, FmPath* file)
1136 {
1137     _fm_set_file_hidden(parent, file, TRUE);
1138 }
1139 
1140 /**
1141  * fm_unhide_file
1142  * @parent: a window to place progress dialog over it
1143  * @file: the file
1144  *
1145  * Unsets file attribute "hidden" for @file.
1146  *
1147  * Since: 1.2.0
1148  */
fm_unhide_file(GtkWindow * parent,FmPath * file)1149 void fm_unhide_file(GtkWindow* parent, FmPath* file)
1150 {
1151     _fm_set_file_hidden(parent, file, FALSE);
1152 }
1153 
1154 /**
1155  * fm_empty_trash
1156  * @parent: a window to place dialog over it
1157  *
1158  * Asks user to confirm the emptying trash can and empties it if confirmed.
1159  *
1160  * Before 0.1.15 this call had different arguments.
1161  *
1162  * Since: 0.1.0
1163  */
fm_empty_trash(GtkWindow * parent)1164 void fm_empty_trash(GtkWindow* parent)
1165 {
1166     if(fm_yes_no(parent, NULL, _("Are you sure you want to empty the trash can?"), TRUE))
1167     {
1168         FmPathList* paths = fm_path_list_new();
1169         fm_path_list_push_tail(paths, fm_path_get_trash());
1170         fm_delete_files_internal(parent, paths);
1171         fm_path_list_unref(paths);
1172     }
1173 }
1174 
1175 /**
1176  * fm_set_busy_cursor
1177  * @widget: a widget
1178  *
1179  * Sets cursor for @widget to "busy".
1180  *
1181  * See also: fm_unset_busy_cursor().
1182  *
1183  * Since: 1.0.0
1184  */
fm_set_busy_cursor(GtkWidget * widget)1185 void fm_set_busy_cursor(GtkWidget* widget)
1186 {
1187     if(gtk_widget_get_realized(widget))
1188     {
1189         GdkWindow* window = gtk_widget_get_window(widget);
1190         GdkCursor* cursor = gdk_cursor_new(GDK_WATCH);
1191         gdk_window_set_cursor(window, cursor);
1192     }
1193     else
1194     {
1195         /* FIXME: how to handle this case? */
1196         g_warning("fm_set_busy_cursor: widget is not realized");
1197     }
1198 }
1199 
1200 /**
1201  * fm_unset_busy_cursor
1202  * @widget: a widget
1203  *
1204  * Restores cursor for @widget to default.
1205  *
1206  * See also: fm_set_busy_cursor().
1207  *
1208  * Since: 1.0.0
1209  */
fm_unset_busy_cursor(GtkWidget * widget)1210 void fm_unset_busy_cursor(GtkWidget* widget)
1211 {
1212     if(gtk_widget_get_realized(widget))
1213     {
1214         GdkWindow* window = gtk_widget_get_window(widget);
1215         gdk_window_set_cursor(window, NULL);
1216     }
1217 }
1218 
assign_tooltip_from_action(GtkWidget * widget)1219 static void assign_tooltip_from_action(GtkWidget* widget)
1220 {
1221     GtkAction* action;
1222     const gchar * tooltip;
1223 
1224     action = gtk_activatable_get_related_action(GTK_ACTIVATABLE(widget));
1225     if (!action)
1226         return;
1227 
1228     if (!gtk_activatable_get_use_action_appearance(GTK_ACTIVATABLE(widget)))
1229         return;
1230 
1231     tooltip = gtk_action_get_tooltip(action);
1232     if (tooltip)
1233     {
1234         gtk_widget_set_tooltip_text(widget, tooltip);
1235         gtk_widget_set_has_tooltip(widget, TRUE);
1236     }
1237     else
1238     {
1239         gtk_widget_set_has_tooltip(widget, FALSE);
1240     }
1241 }
1242 
assign_tooltips_from_actions(GtkWidget * widget)1243 static void assign_tooltips_from_actions(GtkWidget* widget)
1244 {
1245     if(G_LIKELY(GTK_IS_MENU_ITEM(widget)))
1246     {
1247         if(GTK_IS_ACTIVATABLE(widget))
1248             assign_tooltip_from_action(widget);
1249         widget = gtk_menu_item_get_submenu((GtkMenuItem*)widget);
1250         if(widget)
1251             assign_tooltips_from_actions(widget);
1252     }
1253     else if (GTK_IS_CONTAINER(widget))
1254     {
1255         gtk_container_forall((GtkContainer*)widget,
1256                              (GtkCallback)assign_tooltips_from_actions, NULL);
1257     }
1258 }
1259 
1260 /**
1261  * fm_widget_menu_fix_tooltips
1262  * @menu: a #GtkMenu instance
1263  *
1264  * Fix on GTK bug: it does not assign tooltips of menu items from
1265  * appropriate #GtkAction objects. This API assigns them instead.
1266  *
1267  * Since: 1.2.0
1268  */
fm_widget_menu_fix_tooltips(GtkMenu * menu)1269 void fm_widget_menu_fix_tooltips(GtkMenu *menu)
1270 {
1271     GtkWidget *parent;
1272     GtkSettings *settings;
1273     gboolean tooltips_enabled;
1274 
1275     g_return_if_fail(GTK_IS_MENU(menu));
1276     parent = gtk_menu_get_attach_widget(menu);
1277     settings = parent ? gtk_settings_get_for_screen(gtk_widget_get_screen(parent))
1278                       : gtk_settings_get_default();
1279     g_object_get(G_OBJECT(settings), "gtk-enable-tooltips", &tooltips_enabled, NULL);
1280     if(tooltips_enabled)
1281         assign_tooltips_from_actions(GTK_WIDGET(menu));
1282 }
1283