r323: Improved pinboard support. Files can be dropped onto the pinboard
[rox-filer/ma.git] / ROX-Filer / src / pinboard.c
blob8ce7b1fc16f3004d2689b1452ce9aaa2459bb09d
1 /*
2 * $Id$
4 * ROX-Filer, filer for the ROX desktop project
5 * Copyright (C) 2000, Thomas Leonard, <tal197@ecs.soton.ac.uk>.
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 /* pinboard.c - icons on the background */
24 #include "config.h"
26 #include <string.h>
27 #include <stdio.h>
28 #include <errno.h>
29 #include <gtk/gtk.h>
30 #include <gdk/gdkx.h>
31 #include <gtk/gtkinvisible.h>
33 #include "main.h"
34 #include "dnd.h"
35 #include "pinboard.h"
36 #include "type.h"
37 #include "choices.h"
38 #include "gui_support.h"
39 #include "run.h"
41 /* The number of pixels between the bottom of the image and the top
42 * of the text.
44 #define GAP 4
46 /* The size of the border around the icon which is used when winking */
47 #define WINK_FRAME 2
49 struct _PinIcon {
50 GtkWidget *win, *paper;
51 GdkBitmap *mask;
52 guchar *path;
53 DirItem item;
54 int x, y;
57 struct _Pinboard {
58 guchar *name; /* Leaf name */
59 GList *icons;
62 extern int collection_menu_button;
64 static Pinboard *current_pinboard = NULL;
65 static gint loading_pinboard = 0; /* Non-zero => loading */
67 static PinIcon *current_wink_icon = NULL;
68 static gint wink_timeout;
70 static GdkColor mask_solid = {1, 1, 1, 1};
71 static GdkColor mask_transp = {0, 0, 0, 0};
72 static GdkGC *mask_gc = NULL;
74 /* Proxy window for DnD and clicks on the desktop */
75 static GtkWidget *proxy_invisible;
77 static gint drag_start_x, drag_start_y;
79 /* Static prototypes */
80 static void set_size_and_shape(PinIcon *icon, int *rwidth, int *rheight);
81 static gint draw_icon(GtkWidget *widget, GdkEventExpose *event, PinIcon *icon);
82 static void mask_wink_border(PinIcon *icon, GdkColor *alpha);
83 static gint end_wink(gpointer data);
84 static gboolean button_release_event(GtkWidget *widget,
85 GdkEventButton *event,
86 PinIcon *icon);
87 static gboolean button_press_event(GtkWidget *widget,
88 GdkEventButton *event,
89 PinIcon *icon);
90 static gint icon_motion_notify(GtkWidget *widget,
91 GdkEventMotion *event,
92 PinIcon *icon);
93 static char *pin_from_file(guchar *line);
94 static gboolean add_root_handlers(void);
95 static void pinboard_save(void);
96 static GdkFilterReturn proxy_filter(GdkXEvent *xevent,
97 GdkEvent *event,
98 gpointer data);
99 static void icon_destroyed(GtkWidget *widget, PinIcon *icon);
100 static void snap_to_grid(int *x, int *y);
101 static void offset_from_centre(PinIcon *icon,
102 int width, int height,
103 int *x, int *y);
105 /****************************************************************
106 * EXTERNAL INTERFACE *
107 ****************************************************************/
109 /* Load 'pb_<pinboard>' config file from Choices (if it exists)
110 * and make it the current pinboard.
111 * Any existing pinned items are removed. You must call this
112 * at least once before using the pinboard. NULL disables the
113 * pinboard.
115 void pinboard_activate(guchar *name)
117 Pinboard *old_board = current_pinboard;
118 guchar *path, *slash;
120 if (old_board)
122 pinboard_clear();
123 number_of_windows--;
126 if (!name)
128 if (number_of_windows < 1)
129 gtk_main_quit();
130 return;
133 number_of_windows++;
135 if (!proxy_invisible)
137 proxy_invisible = gtk_invisible_new();
138 gtk_widget_show(proxy_invisible);
139 gdk_window_add_filter(proxy_invisible->window,
140 proxy_filter, NULL);
141 gdk_window_add_filter(GDK_ROOT_PARENT(),
142 proxy_filter, NULL);
145 if (!add_root_handlers())
147 delayed_error(PROJECT, _("Another application is already "
148 "managing the pinboard!"));
149 return;
152 slash = strchr(name, '/');
153 if (slash)
154 path = g_strdup(name);
155 else
157 guchar *leaf;
159 leaf = g_strconcat("pb_", name, NULL);
160 path = choices_find_path_load(leaf, "ROX-Filer");
161 g_free(leaf);
164 current_pinboard = g_new(Pinboard, 1);
165 current_pinboard->name = g_strdup(name);
166 current_pinboard->icons = NULL;
168 if (path)
170 loading_pinboard++;
171 parse_file(path, pin_from_file);
172 g_free(path);
173 loading_pinboard--;
177 /* Add a new icon to the background.
178 * 'path' should be an absolute pathname.
179 * 'x' and 'y' are the coordinates of the point in the middle of the text.
180 * 'name' is the name to use. If NULL then the leafname of path is used.
182 void pinboard_pin(guchar *path, guchar *name, int x, int y)
184 PinIcon *icon;
185 int path_len;
186 int width, height;
188 g_return_if_fail(path != NULL);
189 g_return_if_fail(current_pinboard != NULL);
191 path_len = strlen(path);
192 while (path_len > 0 && path[path_len - 1] == '/')
193 path_len--;
195 icon = g_new(PinIcon, 1);
196 icon->path = g_strndup(path, path_len);
197 icon->mask = NULL;
198 snap_to_grid(&x, &y);
199 icon->x = x;
200 icon->y = y;
202 dir_stat(icon->path, &icon->item);
204 if (!name)
206 name = strrchr(icon->path, '/');
207 if (name)
208 name++;
209 else
210 name = icon->path; /* Shouldn't happen */
213 icon->item.leafname = g_strdup(name);
215 icon->win = gtk_window_new(GTK_WINDOW_DIALOG);
216 gtk_window_set_wmclass(GTK_WINDOW(icon->win), "ROX-Pinboard", PROJECT);
218 icon->paper = gtk_drawing_area_new();
219 gtk_container_add(GTK_CONTAINER(icon->win), icon->paper);
221 gtk_widget_realize(icon->win);
222 gdk_window_set_decorations(icon->win->window, 0);
223 gdk_window_set_functions(icon->win->window, 0);
225 set_size_and_shape(icon, &width, &height);
226 offset_from_centre(icon, width, height, &x, &y);
227 gtk_widget_set_uposition(icon->win, x, y);
229 gtk_widget_add_events(icon->paper,
230 GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
231 GDK_BUTTON2_MOTION_MASK | GDK_BUTTON3_MOTION_MASK);
232 gtk_signal_connect(GTK_OBJECT(icon->paper), "button-press-event",
233 GTK_SIGNAL_FUNC(button_press_event), icon);
234 gtk_signal_connect(GTK_OBJECT(icon->paper), "button-release-event",
235 GTK_SIGNAL_FUNC(button_release_event), icon);
236 gtk_signal_connect(GTK_OBJECT(icon->paper), "motion-notify-event",
237 GTK_SIGNAL_FUNC(icon_motion_notify), icon);
238 gtk_signal_connect(GTK_OBJECT(icon->paper), "expose-event",
239 GTK_SIGNAL_FUNC(draw_icon), icon);
240 gtk_signal_connect(GTK_OBJECT(icon->win), "destroy",
241 GTK_SIGNAL_FUNC(icon_destroyed), icon);
243 current_pinboard->icons = g_list_prepend(current_pinboard->icons,
244 icon);
245 gtk_widget_show_all(icon->win);
247 if (!loading_pinboard)
248 pinboard_save();
251 /* Remove an icon from the pinboard */
252 void pinboard_unpin(PinIcon *icon)
254 g_return_if_fail(icon != NULL);
256 gtk_widget_destroy(icon->win);
257 pinboard_save();
260 /* Put a border around the icon, briefly.
261 * If icon is NULL then cancel any existing wink.
263 void pinboard_wink_item(PinIcon *icon)
265 if (current_wink_icon)
267 mask_wink_border(current_wink_icon, &mask_transp);
268 gtk_timeout_remove(wink_timeout);
271 current_wink_icon = icon;
273 if (current_wink_icon)
275 mask_wink_border(current_wink_icon, &mask_solid);
276 wink_timeout = gtk_timeout_add(300, end_wink, NULL);
280 /* Remove everything on the current pinboard and disables the pinboard.
281 * Does not change any files. Does not change number_of_windows.
283 void pinboard_clear(void)
285 g_return_if_fail(current_pinboard != NULL);
286 g_return_if_fail(current_pinboard->icons = NULL); /* XXX */
288 g_free(current_pinboard->name);
289 g_free(current_pinboard);
290 current_pinboard = NULL;
291 /* TODO: Remove handlers */
294 /****************************************************************
295 * INTERNAL FUNCTIONS *
296 ****************************************************************/
298 static gint end_wink(gpointer data)
300 pinboard_wink_item(NULL);
301 return FALSE;
304 /* Make the wink border solid or transparent */
305 static void mask_wink_border(PinIcon *icon, GdkColor *alpha)
307 int width, height;
309 gdk_window_get_size(icon->paper->window, &width, &height);
311 gdk_gc_set_foreground(mask_gc, alpha);
312 gdk_draw_rectangle(icon->mask, mask_gc, FALSE,
313 0, 0, width - 1, height - 1);
314 gdk_draw_rectangle(icon->mask, mask_gc, FALSE,
315 1, 1, width - 3, height - 3);
317 gtk_widget_shape_combine_mask(icon->win, icon->mask, 0, 0);
319 gtk_widget_draw(icon->paper, NULL);
322 /* Updates the name_width field and resizes and masks the window.
323 * Returns the new width and height.
325 static void set_size_and_shape(PinIcon *icon, int *rwidth, int *rheight)
327 int width, height;
328 GdkFont *font = icon->win->style->font;
329 int font_height;
330 GdkBitmap *mask;
331 MaskedPixmap *image = icon->item.image;
333 font_height = font->ascent + font->descent;
334 icon->item.name_width = gdk_string_width(font, icon->item.leafname);
336 width = MAX(image->width, icon->item.name_width + 2) +
337 2 * WINK_FRAME;
338 height = image->height + GAP + (font_height + 2) + 2 * WINK_FRAME;
339 gtk_widget_set_usize(icon->win, width, height);
341 mask = gdk_pixmap_new(icon->win->window, width, height, 1);
342 if (!mask_gc)
343 mask_gc = gdk_gc_new(mask);
345 /* Clear the mask to transparent */
346 gdk_gc_set_foreground(mask_gc, &mask_transp);
347 gdk_draw_rectangle(mask, mask_gc, TRUE, 0, 0, width, height);
349 gdk_gc_set_foreground(mask_gc, &mask_solid);
350 gdk_draw_pixmap(mask, mask_gc, image->mask,
351 0, 0,
352 (width - image->width) >> 1,
353 WINK_FRAME,
354 image->width,
355 image->height);
357 /* Mask off an area for the text */
358 gdk_draw_rectangle(mask, mask_gc, TRUE,
359 (width - (icon->item.name_width + 2)) >> 1,
360 WINK_FRAME + image->height + GAP,
361 icon->item.name_width + 2, font_height + 2);
363 gtk_widget_shape_combine_mask(icon->win, mask, 0, 0);
365 if (icon->mask)
366 gdk_pixmap_unref(icon->mask);
367 icon->mask = mask;
369 *rwidth = width;
370 *rheight = height;
373 static gint draw_icon(GtkWidget *widget, GdkEventExpose *event, PinIcon *icon)
375 GdkFont *font = icon->win->style->font;
376 int font_height;
377 int width, height;
378 int text_x, text_y;
379 MaskedPixmap *image = icon->item.image;
381 font_height = font->ascent + font->descent;
383 gdk_window_get_size(widget->window, &width, &height);
385 /* TODO: If the shape extension is missing we might need to set
386 * the clip mask here...
388 gdk_draw_pixmap(widget->window, widget->style->black_gc,
389 image->pixmap,
390 0, 0,
391 (width - image->width) >> 1,
392 WINK_FRAME,
393 image->width,
394 image->height);
396 text_x = (width - icon->item.name_width) >> 1;
397 text_y = WINK_FRAME + image->height + GAP + 1;
399 gtk_paint_flat_box(widget->style, widget->window,
400 GTK_STATE_NORMAL, GTK_SHADOW_NONE,
401 NULL, widget, "text",
402 text_x - 1,
403 text_y - 1,
404 icon->item.name_width + 2,
405 font_height + 2);
407 gtk_paint_string(widget->style, widget->window,
408 GTK_STATE_NORMAL,
409 NULL, widget, "text",
410 text_x,
411 text_y + font->ascent,
412 icon->item.leafname);
414 if (current_wink_icon == icon)
416 gdk_draw_rectangle(icon->paper->window,
417 icon->paper->style->white_gc,
418 FALSE,
419 0, 0, width - 1, height - 1);
420 gdk_draw_rectangle(icon->paper->window,
421 icon->paper->style->black_gc,
422 FALSE,
423 1, 1, width - 3, height - 3);
426 return FALSE;
429 static gboolean button_release_event(GtkWidget *widget,
430 GdkEventButton *event,
431 PinIcon *icon)
433 int b = event->button;
435 if (b == 1 || b > 3 || b == collection_menu_button)
436 return TRUE;
438 g_print("[ stop drag ]\n");
440 gtk_grab_remove(widget);
442 pinboard_save();
444 return TRUE;
447 static gboolean button_press_event(GtkWidget *widget,
448 GdkEventButton *event,
449 PinIcon *icon)
451 int b = event->button;
453 if (b == collection_menu_button)
454 pinboard_unpin(icon);
455 else if (b == 1)
457 pinboard_wink_item(icon);
459 run_diritem(icon->path, &icon->item, NULL,
460 (event->state & GDK_SHIFT_MASK) != 0);
462 else if (b < 4)
464 drag_start_x = event->x_root;
465 drag_start_y = event->y_root;
466 g_print("[ start drag ]\n");
467 gtk_grab_add(widget);
470 return TRUE;
473 /* An icon is being dragged around... */
474 static gint icon_motion_notify(GtkWidget *widget,
475 GdkEventMotion *event,
476 PinIcon *icon)
478 int x = event->x_root;
479 int y = event->y_root;
480 int width, height;
482 snap_to_grid(&x, &y);
483 icon->x = x;
484 icon->y = y;
485 gdk_window_get_size(icon->win->window, &width, &height);
486 offset_from_centre(icon, width, height, &x, &y);
488 gdk_window_move(icon->win->window, x, y);
490 return TRUE;
493 /* Called for each line in the pinboard file while loading a new board */
494 static char *pin_from_file(guchar *line)
496 int x, y, n;
498 if (sscanf(line, " %d , %d , %n", &x, &y, &n) < 2)
499 return NULL; /* Ignore format errors */
501 pinboard_pin(line + n, NULL, x, y);
503 return NULL;
506 /* Make sure that clicks and drops on the root window come to us...
507 * False if an error occurred (ie, someone else is using it).
509 static gboolean add_root_handlers(void)
511 GdkWindow *root;
513 root = gdk_window_lookup(GDK_ROOT_WINDOW());
514 if (!root)
515 root = gdk_window_foreign_new(GDK_ROOT_WINDOW());
517 if (!setup_xdnd_proxy(GDK_ROOT_WINDOW(), proxy_invisible->window))
518 return FALSE;
520 /* Forward events from the root window to our proxy window */
521 gdk_window_set_user_data(GDK_ROOT_PARENT(), proxy_invisible);
523 drag_set_pinboard_dest(proxy_invisible);
525 return TRUE;
528 /* Write the current state of the pinboard to the current pinboard file */
529 static void pinboard_save(void)
531 guchar *save;
532 guchar *leaf;
533 GString *tmp;
534 FILE *file = NULL;
535 GList *next;
536 guchar *save_new = NULL;
538 g_return_if_fail(current_pinboard != NULL);
540 leaf = g_strconcat("pb_", current_pinboard->name, NULL);
541 save = choices_find_path_save(leaf, "ROX-Filer", TRUE);
542 g_free(leaf);
544 if (!save)
545 return;
547 save_new = g_strconcat(save, ".new", NULL);
548 file = fopen(save_new, "wb");
549 if (!file)
550 goto err;
552 tmp = g_string_new(NULL);
553 for (next = current_pinboard->icons; next; next = next->next)
555 PinIcon *icon = (PinIcon *) next->data;
557 g_string_sprintf(tmp, "%d, %d, %s\n",
558 icon->x, icon->y, icon->path);
559 if (fwrite(tmp->str, 1, tmp->len, file) < tmp->len)
560 goto err;
563 g_string_free(tmp, TRUE);
565 if (fclose(file))
567 file = NULL;
568 goto err;
571 file = NULL;
573 if (rename(save_new, save))
574 goto err;
576 goto out;
577 err:
578 delayed_error(_("Error saving pinboard"), g_strerror(errno));
579 out:
580 if (file)
581 fclose(file);
582 g_free(save_new);
586 * Filter that translates proxied events from virtual root windows into normal
587 * Gdk events for the proxy_invisible widget. Stolen from gmc.
589 static GdkFilterReturn proxy_filter(GdkXEvent *xevent,
590 GdkEvent *event,
591 gpointer data)
593 XEvent *xev;
594 GdkWindow *proxy = proxy_invisible->window;
596 xev = xevent;
598 switch (xev->type) {
599 case ButtonPress:
600 case ButtonRelease:
601 /* Translate button events into events that come from
602 * the proxy window, so that we can catch them as a
603 * signal from the invisible widget.
605 if (xev->type == ButtonPress)
606 event->button.type = GDK_BUTTON_PRESS;
607 else
608 event->button.type = GDK_BUTTON_RELEASE;
610 gdk_window_ref(proxy);
612 event->button.window = proxy;
613 event->button.send_event = xev->xbutton.send_event;
614 event->button.time = xev->xbutton.time;
615 event->button.x = xev->xbutton.x;
616 event->button.y = xev->xbutton.y;
617 event->button.state = xev->xbutton.state;
618 event->button.button = xev->xbutton.button;
620 return GDK_FILTER_TRANSLATE;
622 case DestroyNotify:
623 /* XXX: I have no idea why this helps, but it does! */
624 /* The proxy window was destroyed (i.e. the window
625 * manager died), so we have to cope with it
627 if (((GdkEventAny *) event)->window == proxy)
628 gdk_window_destroy_notify(proxy);
630 return GDK_FILTER_REMOVE;
632 default:
633 break;
636 return GDK_FILTER_CONTINUE;
639 /* Does not save the new state */
640 static void icon_destroyed(GtkWidget *widget, PinIcon *icon)
642 g_print("[ icon '%s' destroyed! ]\n", icon->item.leafname);
644 gdk_pixmap_unref(icon->mask);
645 dir_item_clear(&icon->item);
646 g_free(icon->path);
647 g_free(icon);
649 if (current_pinboard)
650 current_pinboard->icons =
651 g_list_remove(current_pinboard->icons, icon);
654 #define GRID_STEP 32
656 static void snap_to_grid(int *x, int *y)
658 *x = ((*x + GRID_STEP / 2) / GRID_STEP) * GRID_STEP;
659 *y = ((*y + GRID_STEP / 2) / GRID_STEP) * GRID_STEP;
662 /* Convert (x,y) from a centre point to a window position */
663 static void offset_from_centre(PinIcon *icon,
664 int width, int height,
665 int *x, int *y)
667 *x -= width >> 1;
668 *y -= height - (icon->paper->style->font->descent >> 1);
669 *x = CLAMP(*x, 0, screen_width - width);
670 *y = CLAMP(*y, 0, screen_height - height);