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