r2228: Made 'Automatic' an icon size, rather than a separate option.
[rox-filer.git] / ROX-Filer / src / usericons.c
blobcfd72d2a1a53d9d14c3a3813045a38007d3b513d
1 /*
2 * $Id$
4 * ROX-Filer, filer for the ROX desktop project
5 * Copyright (C) 2002, the ROX-Filer team.
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the Free
9 * Software Foundation; either version 2 of the License, or (at your option)
10 * any later version.
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 * more details.
17 * You should have received a copy of the GNU General Public License along with
18 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
19 * Place, Suite 330, Boston, MA 02111-1307 USA
22 /* usericons.c - handle user-defined icons. Diego Zamboni, Feb 7, 2001. */
24 #include "config.h"
26 #include <gtk/gtk.h>
27 #include <errno.h>
28 #include <stdio.h>
29 #include <string.h>
30 #include <unistd.h>
31 #include <fnmatch.h>
32 #include <libxml/parser.h>
33 #include <time.h>
35 #include "global.h"
37 #include "fscache.h"
38 #include "diritem.h"
39 #include "dir.h"
40 #include "gui_support.h"
41 #include "choices.h"
42 #include "pixmaps.h"
43 #include "type.h"
44 #include "run.h"
45 #include "dnd.h"
46 #include "support.h"
47 #include "usericons.h"
48 #include "main.h"
49 #include "menu.h"
50 #include "filer.h"
51 #include "action.h"
52 #include "display.h"
53 #include "xml.h"
55 #define DELETE_ICON 1
57 /* Used to index the 'radios' array... */
58 #define SET_MEDIA 2
59 #define SET_TYPE 1
60 #define SET_PATH 0 /* Store in globicons */
61 #define SET_COPY 3 /* Create .DirIcon */
63 static GHashTable *glob_icons = NULL; /* Pathname -> Icon pathname */
65 /* Static prototypes */
66 static const char *process_globicons_line(gchar *line);
67 static gboolean free_globicon(gpointer key, gpointer value, gpointer data);
68 static void get_path_set_icon(GtkWidget *dialog);
69 static void show_icon_help(gpointer data);
70 static void write_globicons(void);
71 static void show_current_dirs_menu(GtkWidget *button, gpointer data);
72 static void add_globicon(const gchar *path, const gchar *icon);
73 static void drag_icon_dropped(GtkWidget *frame,
74 GdkDragContext *context,
75 gint x,
76 gint y,
77 GtkSelectionData *selection_data,
78 guint info,
79 guint32 time,
80 GtkWidget *dialog);
81 static gboolean set_icon_for_type(MIME_type *type, const gchar *iconpath,
82 gboolean just_media);
83 static void delete_globicon(const gchar *path);
84 static gboolean convert_to_png(const gchar *src, const gchar *dest);
86 /****************************************************************
87 * EXTERNAL INTERFACE *
88 ****************************************************************/
90 /* Read glob-pattern -> icon mappings from "globicons" in Choices */
91 void read_globicons()
93 static time_t last_read = (time_t) 0;
94 struct stat info;
95 guchar *path;
96 xmlDocPtr doc;
98 if (!glob_icons)
99 glob_icons = g_hash_table_new(g_str_hash, g_str_equal);
101 path = choices_find_path_load("globicons", PROJECT);
102 if (!path)
103 return; /* Nothing to load */
105 if (mc_stat(path, &info))
106 goto out;
108 if (info.st_mtime <= last_read)
109 goto out; /* File hasn't been modified since we last read it */
111 g_hash_table_foreach_remove(glob_icons, free_globicon, NULL);
113 doc = xmlParseFile(path);
114 if (doc)
116 xmlNodePtr node, icon, root;
117 char *match;
119 root = xmlDocGetRootElement(doc);
121 /* Handle the new XML file format */
122 for (node = root->xmlChildrenNode; node; node = node->next)
124 gchar *path, *icon_path;
126 if (node->type != XML_ELEMENT_NODE)
127 continue;
128 if (strcmp(node->name, "rule") != 0)
129 continue;
130 icon = get_subnode(node, NULL, "icon");
131 if (!icon)
132 continue;
133 match = xmlGetProp(node, "match");
134 if (!match)
135 continue;
137 icon_path = xmlNodeGetContent(icon);
138 path = expand_path(match);
139 g_hash_table_insert(glob_icons, path, icon_path);
140 g_free(match);
143 xmlFreeDoc(doc);
145 else
147 /* Handle the old non-XML format */
148 parse_file(path, process_globicons_line);
149 if (g_hash_table_size(glob_icons))
150 write_globicons(); /* Upgrade to new format */
153 last_read = time(NULL); /* Update time stamp */
154 out:
155 g_free(path);
158 /* Set an item's image field according to the globicons patterns if
159 * it matches one of them and the file exists.
161 void check_globicon(const guchar *path, DirItem *item)
163 gchar *gi;
165 g_return_if_fail(item && !item->image);
167 gi = g_hash_table_lookup(glob_icons, path);
168 if (gi)
169 item->image = g_fscache_lookup(pixmap_cache, gi);
172 static gboolean create_diricon(const guchar *filepath, const guchar *iconpath)
174 if (!convert_to_png(iconpath, make_path(filepath, ".DirIcon")))
175 return FALSE;
177 dir_check_this(filepath);
179 return TRUE;
182 /* Add a globicon mapping for the given file to the given icon path */
183 gboolean set_icon_path(const guchar *filepath, const guchar *iconpath)
185 MaskedPixmap *pic;
187 /* Check if file exists */
188 if (!file_exists(iconpath))
190 delayed_error(_("The pathname you gave does not exist. "
191 "The icon has not been changed."));
192 return FALSE;
195 /* Check if we can load the image, warn the user if not. */
196 pic = g_fscache_lookup(pixmap_cache, iconpath);
197 if (!pic)
199 delayed_error(
200 _("Unable to load image file -- maybe it's not in a "
201 "format I understand, or maybe the permissions are "
202 "wrong?\n"
203 "The icon has not been changed."));
204 return FALSE;
206 g_object_unref(pic);
208 /* Add the globicon mapping and update visible icons */
209 add_globicon(filepath, iconpath);
211 return TRUE;
214 static void dialog_response(GtkWidget *dialog, gint response, gpointer data)
216 if (response == GTK_RESPONSE_OK)
217 get_path_set_icon(dialog);
218 else if (response == GTK_RESPONSE_CANCEL)
219 gtk_widget_destroy(dialog);
220 else if (response == DELETE_ICON)
222 const guchar *path;
223 const guchar *icon_path;
225 path = g_object_get_data(G_OBJECT(dialog), "pathname");
226 g_return_if_fail(path != NULL);
228 delete_globicon(path);
230 icon_path = make_path(path, ".DirIcon");
231 if (access(icon_path, F_OK) == 0)
233 GList *list;
235 list = g_list_prepend(NULL, (char *) icon_path);
236 action_delete(list);
237 g_list_free(list);
240 gtk_widget_destroy(dialog);
244 /* Display a dialog box allowing the user to set the icon for
245 * a file or directory.
247 void icon_set_handler_dialog(DirItem *item, const guchar *path)
249 struct stat info;
250 guchar *tmp;
251 GtkDialog *dialog;
252 GtkWidget *frame, *hbox, *vbox2;
253 GtkWidget *entry, *label, *button, *align, *icon;
254 GtkWidget **radio;
255 GtkRadioButton *group;
256 GtkTargetEntry targets[] = {
257 {"text/uri-list", 0, TARGET_URI_LIST},
259 char *gi;
261 g_return_if_fail(item != NULL && path != NULL);
263 gi = g_hash_table_lookup(glob_icons, path);
265 dialog = GTK_DIALOG(gtk_dialog_new());
266 gtk_dialog_set_has_separator(dialog, FALSE);
267 gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_MOUSE);
268 g_object_set_data_full(G_OBJECT(dialog), "pathname",
269 strdup(path), g_free);
271 gtk_window_set_title(GTK_WINDOW(dialog), _("Set icon"));
273 radio = g_new(GtkWidget *, 4);
275 tmp = g_strdup_printf(_("Set icon for all `%s/<anything>'"),
276 item->mime_type->media_type);
277 radio[SET_MEDIA] = gtk_radio_button_new_with_label(NULL, tmp);
278 gtk_box_pack_start(GTK_BOX(dialog->vbox),
279 radio[SET_MEDIA], FALSE, TRUE, 0);
280 g_free(tmp);
281 group = GTK_RADIO_BUTTON(radio[SET_MEDIA]);
282 gtk_tooltips_set_tip(tooltips, radio[SET_MEDIA],
283 _("Use a copy of the image as the default for all "
284 "files of these MIME types."), NULL);
286 tmp = g_strdup_printf(_("Only for the type `%s/%s'"),
287 item->mime_type->media_type,
288 item->mime_type->subtype);
289 radio[SET_TYPE] = gtk_radio_button_new_with_label_from_widget(group,
290 tmp);
291 gtk_box_pack_start(GTK_BOX(dialog->vbox), radio[SET_TYPE],
292 FALSE, TRUE, 0);
293 g_free(tmp);
294 gtk_tooltips_set_tip(tooltips, radio[SET_TYPE],
295 _("Use a copy of the image for all files of this MIME "
296 "type."), NULL);
298 tmp = g_strdup_printf(_("Only for the file `%s'"), path);
299 radio[SET_PATH] = gtk_radio_button_new_with_label_from_widget(group,
300 tmp);
301 gtk_box_pack_start(GTK_BOX(dialog->vbox), radio[SET_PATH],
302 FALSE, TRUE, 0);
303 gtk_tooltips_set_tip(tooltips, radio[SET_PATH],
304 _("Add the file and image filenames to your "
305 "personal list. The setting will be lost if the image "
306 "or the file is moved."), NULL);
307 g_free(tmp);
308 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio[SET_PATH]), TRUE);
310 /* If it's a directory, offer to create a .DirIcon */
311 if (mc_stat(path, &info) == 0 && S_ISDIR(info.st_mode))
313 radio[3] = gtk_radio_button_new_with_label_from_widget(
314 group, _("Copy image into directory"));
315 gtk_box_pack_start(GTK_BOX(dialog->vbox), radio[SET_COPY],
316 FALSE, TRUE, 0);
317 gtk_tooltips_set_tip(tooltips, radio[SET_COPY],
318 _("Copy the image inside the directory, as "
319 "a hidden file called '.DirIcon'. "
320 "All users will then see the "
321 "icon, and you can move the directory around safely. "
322 "This is usually the best option if you can write to "
323 "the directory."), NULL);
325 if (access(path, W_OK) == 0)
326 gtk_toggle_button_set_active(
327 GTK_TOGGLE_BUTTON(radio[SET_COPY]),
328 TRUE);
330 else
331 radio[SET_COPY] = NULL;
333 g_object_set_data_full(G_OBJECT(dialog), "radios", radio, g_free);
334 g_object_set_data(G_OBJECT(dialog), "mime_type", item->mime_type);
336 frame = gtk_frame_new(NULL);
337 gtk_box_pack_start(GTK_BOX(dialog->vbox), frame, TRUE, TRUE, 4);
338 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
339 gtk_container_set_border_width(GTK_CONTAINER(frame), 4);
341 gtk_drag_dest_set(frame, GTK_DEST_DEFAULT_ALL,
342 targets, sizeof(targets) / sizeof(*targets),
343 GDK_ACTION_COPY);
344 g_signal_connect(frame, "drag_data_received",
345 G_CALLBACK(drag_icon_dropped), dialog);
347 vbox2 = gtk_vbox_new(FALSE, 0);
348 gtk_container_add(GTK_CONTAINER(frame), vbox2);
350 label = gtk_label_new(_("Drop an icon file here"));
351 gtk_misc_set_padding(GTK_MISC(label), 10, 10);
352 gtk_box_pack_start(GTK_BOX(vbox2), label, TRUE, TRUE, 0);
353 align = gtk_alignment_new(1, 1, 0, 0);
354 gtk_box_pack_start(GTK_BOX(vbox2), align, FALSE, TRUE, 0);
355 button = gtk_button_new();
356 gtk_container_add(GTK_CONTAINER(align), button);
357 icon = gtk_image_new_from_pixbuf(im_dirs->pixbuf);
358 gtk_container_add(GTK_CONTAINER(button), icon);
359 gtk_tooltips_set_tip(tooltips, button,
360 _("Menu of directories previously used for icons"),
361 NULL);
362 g_signal_connect(button, "clicked",
363 G_CALLBACK(show_current_dirs_menu), NULL);
365 hbox = gtk_hbox_new(FALSE, 4);
366 gtk_box_pack_start(GTK_BOX(dialog->vbox), hbox, FALSE, TRUE, 4);
367 gtk_box_pack_start(GTK_BOX(hbox), gtk_hseparator_new(), TRUE, TRUE, 0);
368 gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(_("OR")),
369 FALSE, TRUE, 0);
370 gtk_box_pack_start(GTK_BOX(hbox), gtk_hseparator_new(), TRUE, TRUE, 0);
372 hbox = gtk_hbox_new(FALSE, 4);
373 gtk_box_pack_start(GTK_BOX(dialog->vbox), hbox, FALSE, TRUE, 0);
375 label = gtk_label_new(_("Enter the path of an icon file:")),
376 gtk_misc_set_alignment(GTK_MISC(label), 0, .5);
377 gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 4);
379 gtk_box_pack_start(GTK_BOX(hbox),
380 new_help_button(show_icon_help, NULL), FALSE, TRUE, 0);
382 entry = gtk_entry_new();
383 /* Set the current icon as the default text if there is one */
384 if (gi)
385 gtk_entry_set_text(GTK_ENTRY(entry), gi);
387 gtk_box_pack_start(GTK_BOX(dialog->vbox), entry, FALSE, TRUE, 0);
388 gtk_widget_grab_focus(entry);
389 g_object_set_data(G_OBJECT(dialog), "icon_path", entry);
390 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
392 button = button_new_mixed(GTK_STOCK_DELETE, _("_Remove"));
393 GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
394 gtk_dialog_add_action_widget(dialog, button, DELETE_ICON);
395 gtk_dialog_add_buttons(dialog,
396 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
397 GTK_STOCK_OK, GTK_RESPONSE_OK,
398 NULL);
399 g_signal_connect(dialog, "response", G_CALLBACK(dialog_response), NULL);
400 gtk_dialog_set_default_response(dialog, GTK_RESPONSE_OK);
402 gtk_widget_show_all(GTK_WIDGET(dialog));
406 /****************************************************************
407 * INTERNAL FUNCTIONS *
408 ****************************************************************/
410 static gboolean free_globicon(gpointer key, gpointer value, gpointer data)
412 g_free(key);
413 g_free(value);
415 return TRUE; /* For g_hash_table_foreach_remove() */
418 static void write_globicon(gpointer key, gpointer value, gpointer data)
420 xmlNodePtr doc = (xmlNodePtr) data;
421 xmlNodePtr tree;
423 tree = xmlNewTextChild(doc, NULL, "rule", NULL);
424 xmlSetProp(tree, "match", key);
425 xmlNewChild(tree, NULL, "icon", value);
428 /* Write globicons file */
429 static void write_globicons(void)
431 gchar *save = NULL, *save_new = NULL;
432 xmlDocPtr doc = NULL;
434 save = choices_find_path_save("globicons", PROJECT, TRUE);
436 if (!save)
437 return; /* Saving is disabled */
439 save_new = g_strconcat(save, ".new", NULL);
441 doc = xmlNewDoc("1.0");
442 xmlDocSetRootElement(doc,
443 xmlNewDocNode(doc, NULL, "special-files", NULL));
445 g_hash_table_foreach(glob_icons, write_globicon,
446 xmlDocGetRootElement(doc));
448 if (save_xml_file(doc, save_new) || rename(save_new, save))
449 delayed_error(_("Error saving %s: %s"),
450 save, g_strerror(errno));
452 g_free(save_new);
453 g_free(save);
455 if (doc)
456 xmlFreeDoc(doc);
459 /* Process a globicon line. Format:
460 glob-pattern icon-path
461 Example:
462 /home/<*>/Mail /usr/local/icons/mailbox.xpm
463 (<*> represents a single asterisk, enclosed in brackets to not break
464 the C comment).
466 static const char *process_globicons_line(gchar *line)
468 guchar *pattern, *iconpath;
470 pattern = strtok(line, " \t");
471 /* We ignore empty lines, but they are no cause for a message */
472 if (pattern == NULL)
473 return NULL;
475 iconpath = strtok(NULL, " \t");
477 /* If there is no icon, then we worry */
478 g_return_val_if_fail(iconpath != NULL,
479 "Invalid line in globicons: no icon specified");
481 g_hash_table_insert(glob_icons, g_strdup(pattern), g_strdup(iconpath));
483 return NULL;
486 /* Add a globicon entry to the list. If another one with the same
487 * path exists, it is replaced. Otherwise, the new entry is
488 * added to the top of the list (so that it takes precedence over
489 * other entries).
491 static void add_globicon(const gchar *path, const gchar *icon)
493 g_hash_table_insert(glob_icons, g_strdup(path), g_strdup(icon));
495 /* Rewrite the globicons file */
496 write_globicons();
498 /* Make sure any visible icons for the file are updated */
499 examine(path);
502 /* Remove the globicon for a certain path */
503 static void delete_globicon(const gchar *path)
505 gpointer key, value;
507 if (!g_hash_table_lookup_extended(glob_icons, path, &key, &value))
508 return;
510 g_hash_table_remove(glob_icons, path);
512 g_free(key);
513 g_free(value);
515 write_globicons();
516 examine(path);
519 /* Set the icon for this dialog's file to 'icon' */
520 static void do_set_icon(GtkWidget *dialog, const gchar *icon)
522 guchar *path = NULL;
523 GtkToggleButton **radio;
525 path = g_object_get_data(G_OBJECT(dialog), "pathname");
526 g_return_if_fail(path != NULL);
528 radio = g_object_get_data(G_OBJECT(dialog), "radios");
529 g_return_if_fail(radio != NULL);
531 if (gtk_toggle_button_get_active(radio[0]))
533 if (!set_icon_path(path, icon))
534 return;
536 else if (radio[SET_COPY] &&
537 gtk_toggle_button_get_active(radio[SET_COPY]))
539 if (!create_diricon(path, icon))
540 return;
542 else
544 gboolean just_media;
545 MIME_type *type;
547 type = g_object_get_data(G_OBJECT(dialog), "mime_type");
548 just_media = gtk_toggle_button_get_active(radio[2]);
550 if (!set_icon_for_type(type, icon, just_media))
551 return;
554 destroy_on_idle(dialog);
557 /* Called when a URI list is dropped onto the box in the Set Icon
558 * dialog. Make that the default icon.
560 static void drag_icon_dropped(GtkWidget *frame,
561 GdkDragContext *context,
562 gint x,
563 gint y,
564 GtkSelectionData *selection_data,
565 guint info,
566 guint32 time,
567 GtkWidget *dialog)
569 GList *uris;
570 guchar *icon = NULL;
572 if (!selection_data->data)
573 return;
575 uris = uri_list_to_glist(selection_data->data);
577 if (g_list_length(uris) == 1)
578 icon = g_strdup(get_local_path((guchar *) uris->data));
580 destroy_glist(&uris);
582 if (!icon)
584 delayed_error(_("You should drop a single local icon file "
585 "onto the drop box - that icon will be "
586 "used for this file from now on."));
587 return;
590 do_set_icon(dialog, icon);
592 g_free(icon);
595 /* Called if the user clicks on the OK button on the Set Icon dialog */
596 static void get_path_set_icon(GtkWidget *dialog)
598 GtkEntry *entry;
599 const gchar *icon;
601 entry = g_object_get_data(G_OBJECT(dialog), "icon_path");
602 g_return_if_fail(entry != NULL);
604 icon = gtk_entry_get_text(entry);
606 do_set_icon(dialog, icon);
609 static void show_icon_help(gpointer data)
611 info_message(
612 _("Enter the full path of a file that contains a valid "
613 "image to be used as the icon for this file or directory."));
616 /* Set the icon for the given MIME type. We copy the file. */
617 static gboolean set_icon_for_type(MIME_type *type, const gchar *iconpath,
618 gboolean just_media)
620 gchar *target;
621 gchar *leaf;
623 if (just_media)
624 leaf = g_strconcat(type->media_type, ".png", NULL);
625 else
626 leaf = g_strconcat(type->media_type, "_", type->subtype,
627 ".png", NULL);
629 target = choices_find_path_save(leaf, "MIME-icons", TRUE);
630 g_free(leaf);
632 if (!target)
634 delayed_error(_("Setting icon disabled by CHOICESPATH"));
635 return FALSE;
638 if (!convert_to_png(iconpath, target))
640 g_free(target);
641 return FALSE;
644 g_free(target);
646 return TRUE;
649 static void get_dir(gpointer key, gpointer value, gpointer data)
651 GHashTable *names = (GHashTable *) data;
652 gchar *dir;
654 dir = g_path_get_dirname(value); /* Freed in add_dir_to_menu */
655 if (dir)
657 g_hash_table_insert(names, dir, NULL);
661 static void open_icon_dir(GtkMenuItem *item, gpointer data)
663 FilerWindow *filer;
664 const char *dir;
666 dir = gtk_label_get_text(GTK_LABEL(GTK_BIN(item)->child));
667 filer = filer_opendir(dir, NULL, NULL);
668 if (filer)
669 display_set_thumbs(filer, TRUE);
672 static void add_dir_to_menu(gpointer key, gpointer value, gpointer data)
674 GtkMenuShell *menu = (GtkMenuShell *) data;
675 GtkWidget *item;
677 item = gtk_menu_item_new_with_label(key);
678 gtk_widget_set_accel_path(item, NULL, NULL); /* XXX */
679 g_signal_connect(item, "activate",
680 G_CALLBACK(open_icon_dir), NULL);
681 g_free(key);
682 gtk_menu_shell_append(menu, item);
685 static void show_current_dirs_menu(GtkWidget *button, gpointer data)
687 GHashTable *names;
688 GtkWidget *menu;
690 names = g_hash_table_new(g_str_hash, g_str_equal);
692 g_hash_table_foreach(glob_icons, get_dir, names);
693 if (g_hash_table_size(glob_icons) == 0)
695 /* TODO: Include MIME-icons? */
696 delayed_error(_("You have not yet set any special icons; "
697 "therefore, I have no directories to show you"));
698 return;
701 menu = gtk_menu_new();
703 g_hash_table_foreach(names, add_dir_to_menu, menu);
705 g_hash_table_destroy(names);
707 show_popup_menu(menu, gtk_get_current_event(), 0);
710 /* Load image 'src', and save it as an icon-sized image in png format.
711 * TRUE on success, error is already reported on failure.
713 static gboolean convert_to_png(const gchar *src, const gchar *dest)
715 MaskedPixmap *pic;
716 GError *error = NULL;
718 pic = g_fscache_lookup(pixmap_cache, src);
719 if (!pic)
721 delayed_error(
722 _("Unable to load image file -- maybe it's not in a "
723 "format I understand, or maybe the permissions are "
724 "wrong?\n"
725 "The icon has not been changed."));
726 return FALSE;
729 gdk_pixbuf_save(pic->src_pixbuf, dest,
730 "png", &error,
731 "tEXt::Software", PROJECT,
732 NULL);
733 g_object_unref(pic);
735 if (error)
737 delayed_error(_("Error creating image '%s':\n%s"),
738 dest, error->message);
739 g_error_free(error);
740 return FALSE;
743 return TRUE;