view src/info.c @ 737:8a8873e7a552

Make shell command and its option rc file options instead of hardcoded strings. This allows users to modify the shell command that execute "editors". Two new options appear in rc file: - shell.path (default to "/bin/sh") - shell.options (default to "-c") These options can only be changed from the rc file, not at runtime. Tests are made to check that shell.path is not empty and lead to an executable file.
author zas_
date Thu, 22 May 2008 20:22:13 +0000
parents a7289f9e8d29
children ae618ebec3e9
line wrap: on
line source

/*
 * Geeqie
 * (C) 2004 John Ellis
 * Copyright (C) 2008 The Geeqie Team
 *
 * Author: John Ellis
 *
 * This software is released under the GNU General Public License (GNU GPL).
 * Please read the included file COPYING for more information.
 * This software comes with no warranty of any kind, use at your own risk!
 */


#include "main.h"
#include "info.h"

#include "bar_info.h"
#include "bar_exif.h"
#include "dnd.h"
#include "filedata.h"
#include "image.h"
#include "image-load.h"
#include "pixbuf-renderer.h"
#include "ui_bookmark.h"
#include "ui_fileops.h"
#include "ui_misc.h"
#include "window.h"

#include <pwd.h>
#include <grp.h>


#define IMAGE_SIZE_W 200
#define IMAGE_SIZE_H 200

#define DEF_PROPERTY_WIDTH  600
#define DEF_PROPERTY_HEIGHT 400

typedef struct _TabData TabData;
struct _TabData
{
	void (*func_free)(gpointer data);
	void (*func_sync)(InfoData *id, gpointer data);
	void (*func_image)(InfoData *id, gpointer data);
	gpointer data;
	TabData *(*func_new)(InfoData *id);
	GtkWidget *child;
};

typedef struct _InfoTabsPos InfoTabsPos;
struct _InfoTabsPos
{
	TabData *(*func)(InfoData *id);
	guint pos;
};

static GList *info_tabs_pos_list = NULL;

static void notebook_set_tab_reorderable(GtkNotebook *notebook, GtkWidget *child, gboolean reorderable)
{
#if GTK_CHECK_VERSION(2,10,0)
	gtk_notebook_set_tab_reorderable(notebook, child, reorderable);
#endif
}

/*
 *-------------------------------------------------------------------
 * table utils
 *-------------------------------------------------------------------
 */

GtkWidget *table_add_line(GtkWidget *table, gint x, gint y,
			  const gchar *description, const gchar *text)
{
	GtkWidget *label;

	if (!text) text = "";

	label = pref_table_label(table, x, y, description, 1.0);
	pref_label_bold(label, TRUE, FALSE);

	label = pref_table_label(table, x + 1, y, text, 0.0);
	return label;
}

/*
 *-------------------------------------------------------------------
 * EXIF tab
 *-------------------------------------------------------------------
 */

static void info_tab_exif_image(InfoData *id, gpointer data)
{
	GtkWidget *bar = data;
	FileData *fd;

	if (id->image->unknown)
		{
		fd = NULL;
		}
	else
       		{
		fd = id->image->image_fd;
		}

	bar_exif_set(bar, fd);
}

static void info_tab_exif_sync(InfoData *id, gpointer data)
{
	GtkWidget *bar = data;

	bar_exif_set(bar, NULL);
}

static TabData *info_tab_exif_new(InfoData *id)
{
	TabData *td;
	GtkWidget *bar;
	GtkWidget *label;

	bar = bar_exif_new(FALSE, NULL, FALSE, NULL);
	gtk_container_set_border_width(GTK_CONTAINER(bar), PREF_PAD_BORDER);

	label = gtk_label_new(_("Exif"));
	gtk_notebook_append_page(GTK_NOTEBOOK(id->notebook), bar, label);
	notebook_set_tab_reorderable(GTK_NOTEBOOK(id->notebook), bar, TRUE);
	gtk_widget_show(bar);

	/* register */
	td = g_new0(TabData, 1);
	td->func_free = NULL;
	td->func_sync = info_tab_exif_sync;
	td->func_image = info_tab_exif_image;
	td->data = bar;
	td->func_new = info_tab_exif_new;
	td->child = bar;

	return td;
}

/*
 *-------------------------------------------------------------------
 * file attributes tab
 *-------------------------------------------------------------------
 */

typedef struct _InfoTabMeta InfoTabMeta;
struct _InfoTabMeta
{
	GtkWidget *bar_info;
};

static void info_tab_meta_free(gpointer data)
{
	InfoTabMeta *tab = data;

	g_free(tab);
}

static void info_tab_meta_sync(InfoData *id, gpointer data)
{
	InfoTabMeta *tab = data;

	bar_info_set(tab->bar_info, id->fd);
}

static GList *info_tab_meta_list_cb(gpointer data)
{
	InfoData *id = data;

	return filelist_copy(id->list);
}

static TabData *info_tab_meta_new(InfoData *id)
{
	TabData *td;
	InfoTabMeta *tab;
	GtkWidget *label;

	tab = g_new0(InfoTabMeta, 1);

	tab->bar_info = bar_info_new(NULL, TRUE, NULL);
	bar_info_set_selection_func(tab->bar_info, info_tab_meta_list_cb, id);
	bar_info_selection(tab->bar_info, g_list_length(id->list) - 1);

	gtk_container_set_border_width(GTK_CONTAINER(tab->bar_info), PREF_PAD_BORDER);

	label = gtk_label_new(_("Keywords"));
	gtk_notebook_append_page(GTK_NOTEBOOK(id->notebook), tab->bar_info, label);
	notebook_set_tab_reorderable(GTK_NOTEBOOK(id->notebook), tab->bar_info, TRUE);
	gtk_widget_show(tab->bar_info);

	/* register */
	td = g_new0(TabData, 1);
	td->func_free = info_tab_meta_free;
	td->func_sync = info_tab_meta_sync;
	td->func_image = NULL;
	td->data = tab;
	td->func_new = info_tab_meta_new;
	td->child = tab->bar_info;

	return td;
}

/*
 *-------------------------------------------------------------------
 * general tab
 *-------------------------------------------------------------------
 */

typedef struct _InfoTabGeneral InfoTabGeneral;
struct _InfoTabGeneral
{
	GtkWidget *label_file_time;
	GtkWidget *label_file_size;
	GtkWidget *label_dimensions;
	GtkWidget *label_transparent;
	GtkWidget *label_image_size;
	GtkWidget *label_compression;
	GtkWidget *label_mime_type;

	GtkWidget *label_user;
	GtkWidget *label_group;
	GtkWidget *label_perms;

	gint compression_done;
	gint64 byte_size;
};

static void info_tab_general_image(InfoData *id, gpointer data)
{
	InfoTabGeneral *tab = data;
	gchar *buf;
	guint mem_size;
	gint has_alpha;
	GdkPixbuf *pixbuf;
	gint width, height;

	if (id->image->unknown) return;

	image_get_image_size(id->image, &width, &height);

	buf = g_strdup_printf("%d x %d", width, height);
	gtk_label_set_text(GTK_LABEL(tab->label_dimensions), buf);
	g_free(buf);

	pixbuf = image_get_pixbuf(id->image);
	if (pixbuf)
		{
		has_alpha = gdk_pixbuf_get_has_alpha(pixbuf);
		}
	else
		{
		has_alpha = FALSE;
		}
	gtk_label_set_text(GTK_LABEL(tab->label_transparent), has_alpha ? _("yes") : _("no"));

	mem_size = width * height * ((has_alpha) ? 4 : 3);
	buf = text_from_size_abrev(mem_size);
	gtk_label_set_text(GTK_LABEL(tab->label_image_size), buf);
	g_free(buf);

	if (!tab->compression_done && mem_size > 0)
		{
		buf = g_strdup_printf("%.1f%%", (double)tab->byte_size / mem_size * 100.0);
		gtk_label_set_text(GTK_LABEL(tab->label_compression), buf);
		g_free(buf);

		tab->compression_done = TRUE;
		}

	buf = image_loader_get_format(id->image->il);
	if (buf)
	gtk_label_set_text(GTK_LABEL(tab->label_mime_type), buf);
	g_free(buf);
}

static gchar *mode_number(mode_t m)
{
	int mb, mu, mg, mo;

	mb = mu = mg = mo = 0;

	if (m & S_ISUID) mb |= 4;
	if (m & S_ISGID) mb |= 2;
	if (m & S_ISVTX) mb |= 1;

	if (m & S_IRUSR) mu |= 4;
	if (m & S_IWUSR) mu |= 2;
	if (m & S_IXUSR) mu |= 1;

	if (m & S_IRGRP) mg |= 4;
	if (m & S_IWGRP) mg |= 2;
	if (m & S_IXGRP) mg |= 1;

	if (m & S_IROTH) mo |= 4;
	if (m & S_IWOTH) mo |= 2;
	if (m & S_IXOTH) mo |= 1;

	return g_strdup_printf("%d%d%d%d", mb, mu, mg, mo);
}

static void info_tab_general_sync_perm(InfoTabGeneral *tab, InfoData *id)
{
	struct stat st;

	if (!stat_utf8(id->fd->path, &st))
		{
		gtk_label_set_text(GTK_LABEL(tab->label_user), "");
		gtk_label_set_text(GTK_LABEL(tab->label_group), "");
		gtk_label_set_text(GTK_LABEL(tab->label_perms), "");
		}
	else
		{
		struct passwd *user;
		struct group *grp;
		gchar pbuf[12];
		gchar *pmod;
		gchar *buf;

		user = getpwuid(st.st_uid);
		gtk_label_set_text(GTK_LABEL(tab->label_user), (user) ? user->pw_name : "");

		grp = getgrgid(st.st_gid);
		gtk_label_set_text(GTK_LABEL(tab->label_group), (grp) ? grp->gr_name : "");

		pbuf[0] = (st.st_mode & S_IRUSR) ? 'r' : '-';
		pbuf[1] = (st.st_mode & S_IWUSR) ? 'w' : '-';
		pbuf[2] = (st.st_mode & S_IXUSR) ? 'x' : '-';
		pbuf[3] = (st.st_mode & S_IRGRP) ? 'r' : '-';
		pbuf[4] = (st.st_mode & S_IWGRP) ? 'w' : '-';
		pbuf[5] = (st.st_mode & S_IXGRP) ? 'x' : '-';
		pbuf[6] = (st.st_mode & S_IROTH) ? 'r' : '-';
		pbuf[7] = (st.st_mode & S_IWOTH) ? 'w' : '-';
		pbuf[8] = (st.st_mode & S_IXOTH) ? 'x' : '-';
		pbuf[9] = '\0';

		pmod = mode_number(st.st_mode);
		buf = g_strdup_printf("%s (%s)", pbuf, pmod);
		gtk_label_set_text(GTK_LABEL(tab->label_perms), buf);
		g_free(buf);
		g_free(pmod);
		}
}

static void info_tab_general_sync(InfoData *id, gpointer data)
{
	InfoTabGeneral *tab = data;
	gchar *buf;

	gtk_label_set_text(GTK_LABEL(tab->label_file_time), text_from_time(id->fd->date));

	tab->byte_size = id->fd->size;

	buf = text_from_size(tab->byte_size);
	gtk_label_set_text(GTK_LABEL(tab->label_file_size), buf);
	g_free(buf);

	gtk_label_set_text(GTK_LABEL(tab->label_dimensions), "");
	gtk_label_set_text(GTK_LABEL(tab->label_transparent), "");
	gtk_label_set_text(GTK_LABEL(tab->label_image_size), "");

	gtk_label_set_text(GTK_LABEL(tab->label_compression), "");
	gtk_label_set_text(GTK_LABEL(tab->label_mime_type), "");

	info_tab_general_sync_perm(tab, id);

	tab->compression_done = FALSE;
}

static void info_tab_general_free(gpointer data)
{
	InfoTabGeneral *tab = data;

	g_free(tab);
}

static TabData *info_tab_general_new(InfoData *id)
{
	TabData *td;
	InfoTabGeneral *tab;
	GtkWidget *table;
	GtkWidget *label;

	tab = g_new0(InfoTabGeneral, 1);

	table = pref_table_new(NULL, 2, 11, FALSE, FALSE);
	gtk_container_set_border_width(GTK_CONTAINER(table), PREF_PAD_BORDER);

	tab->label_file_time = table_add_line(table, 0, 0, _("File date:"), NULL);
	tab->label_file_size = table_add_line(table, 0, 1, _("File size:"), NULL);

	tab->label_dimensions = table_add_line(table, 0, 2, _("Dimensions:"), NULL);
	tab->label_transparent = table_add_line(table, 0, 3, _("Transparent:"), NULL);
	tab->label_image_size = table_add_line(table, 0, 4, _("Image size:"), NULL);

	tab->label_compression = table_add_line(table, 0, 5, _("Compress ratio:"), NULL);
	tab->label_mime_type = table_add_line(table, 0, 6, _("File type:"), NULL);

	tab->label_user = table_add_line(table, 0, 7, _("Owner:"), NULL);
	tab->label_group = table_add_line(table, 0, 8, _("Group:"), NULL);
	tab->label_perms = table_add_line(table, 0, 9, "", NULL);

	label = gtk_label_new(_("General"));
	gtk_notebook_append_page(GTK_NOTEBOOK(id->notebook), table, label);
	notebook_set_tab_reorderable(GTK_NOTEBOOK(id->notebook), table, TRUE);
	gtk_widget_show(table);

	/* register */
	td = g_new0(TabData, 1);
	td->func_free = info_tab_general_free;
	td->func_sync = info_tab_general_sync;
	td->func_image = info_tab_general_image;
	td->data = tab;
	td->func_new = info_tab_general_new;
	td->child = table;

	return td;
}

/*
 *-------------------------------------------------------------------
 * tabs
 *-------------------------------------------------------------------
 */

static void info_tabs_sync(InfoData *id, gint image)
{
	GList *work;

	work = id->tab_list;
	while (work)
		{
		TabData *td = work->data;
		work = work->next;

		if (image)
			{
			if (td->func_image) td->func_image(id, td->data);
			}
		else
			{
			if (td->func_sync) td->func_sync(id, td->data);
			}
		}
}

static void info_tabs_free(InfoData *id)
{
	GList *work;

	work = id->tab_list;
	while (work)
		{
		TabData *td = work->data;
		work = work->next;

		if (td->func_free) td->func_free(td->data);
		g_free(td);
		}
	g_list_free(id->tab_list);
	id->tab_list = NULL;
}

static InfoTabsPos *info_tabs_pos_new(gpointer func, gint pos)
{
	InfoTabsPos *t = g_new0(InfoTabsPos, 1);
	t->func = func;
	t->pos = pos;

	return t;
}

static void info_tabs_pos_list_append(gpointer func)
{
	static gint pos = 0;

	info_tabs_pos_list = g_list_append(info_tabs_pos_list, info_tabs_pos_new(func, pos++));
}

static gint compare_info_tabs_pos(gconstpointer a, gconstpointer b)
{
	InfoTabsPos *ta = (InfoTabsPos *) a;
	InfoTabsPos *tb = (InfoTabsPos *) b;

	if (ta->pos > tb->pos) return 1;
	return -1;
}

static gpointer info_tab_new_funcs[] = {
	info_tab_general_new,
	info_tab_meta_new,
	info_tab_exif_new,
};

gchar *info_tab_default_order(void)
{
	guint i;
	static gchar str[G_N_ELEMENTS(info_tab_new_funcs) + 1];

	for (i = 0; i < G_N_ELEMENTS(info_tab_new_funcs); i++)
		str[i] = i + '1';
	str[i] = '\0';

	return str;
}

static void info_tab_get_order_string(gchar **dest)
{
	GList *work;
	gchar str[G_N_ELEMENTS(info_tab_new_funcs) + 1];

	g_assert(dest);

	if (!info_tabs_pos_list)
		return;
	
	memset(str, 0, G_N_ELEMENTS(info_tab_new_funcs) + 1);

	work = info_tabs_pos_list;
	while (work)
		{
		guint i;
		InfoTabsPos *t = work->data;
		work = work->next;

		for (i = 0; i < G_N_ELEMENTS(info_tab_new_funcs); i++)
			{
			if (t->func == info_tab_new_funcs[i])
				{
				g_assert(t->pos >= 0 && t->pos < G_N_ELEMENTS(info_tab_new_funcs));
				str[t->pos] = i + '1';
				}
			}
		}
	
	if (strlen(str) != G_N_ELEMENTS(info_tab_new_funcs)) return;

	g_free(*dest);
	*dest = g_strdup(str);
}

static void info_tabs_init(InfoData *id)
{
	GList *work;

	if (!info_tabs_pos_list)
		{
		guint count = 0;
		guint i;
		gchar *order = options->properties.tabs_order;
		
		for (i = 0; i < strlen(order); i++)
			{
			guint n = order[i] - '1';

			if (n >= G_N_ELEMENTS(info_tab_new_funcs)) break;
			count++;
			}

		if (count != G_N_ELEMENTS(info_tab_new_funcs))
			order = info_tab_default_order();

		for (i = 0; i < strlen(order); i++)
			{
			guint n = order[i] - '1';

			if (n >= G_N_ELEMENTS(info_tab_new_funcs)) continue;
			if (g_list_find(info_tabs_pos_list, info_tab_new_funcs[n])) continue;
			info_tabs_pos_list_append(info_tab_new_funcs[n]);
			}
		}
	else
		info_tabs_pos_list = g_list_sort(info_tabs_pos_list, compare_info_tabs_pos);

	info_tab_get_order_string(&options->properties.tabs_order);

	work = info_tabs_pos_list;
	while (work)
		{
		InfoTabsPos *t = work->data;
		work = work->next;

		id->tab_list = g_list_append(id->tab_list, t->func(id));
		}
}

/*
 *-------------------------------------------------------------------
 * sync
 *-------------------------------------------------------------------
 */

static void info_window_sync(InfoData *id, FileData *fd)
{

	if (!fd) return;

	gtk_entry_set_text(GTK_ENTRY(id->name_entry), fd->name);

	if (id->label_count)
		{
		gchar *buf;
		buf = g_strdup_printf(_("Image %d of %d"),
				      g_list_index(id->list, (gpointer)fd) + 1,
				      g_list_length(id->list));
		gtk_label_set_text(GTK_LABEL(id->label_count), buf);
		g_free(buf);
		}

	info_tabs_sync(id, FALSE);

	id->updated = FALSE;
	image_change_fd(id->image, fd, 0.0);
}

static void info_notebook_reordered_cb(GtkNotebook *notebook, GtkWidget *child, guint page_num, gpointer data)
{
	InfoData *id = data;
	GList *work;

	/* Save current tabs position to be able to restore them later. */
	work = id->tab_list;
	while (work)
		{
		GList *tabpos;
		TabData *td = work->data;
		gint pos = gtk_notebook_page_num(GTK_NOTEBOOK(id->notebook), GTK_WIDGET(td->child));
		work = work->next;

		tabpos = info_tabs_pos_list;
		while (tabpos)
			{
			InfoTabsPos *t = tabpos->data;
			tabpos = tabpos->next;

			if (t->func == td->func_new)
				{
				t->pos = pos;
				break;
				}
			}
		}
	
	info_tabs_pos_list = g_list_sort(info_tabs_pos_list, compare_info_tabs_pos);
	info_tab_get_order_string(&options->properties.tabs_order);
}

/*
 *-------------------------------------------------------------------
 * drag n drop (dropping not supported!)
 *-------------------------------------------------------------------
 */

static void info_window_dnd_data_set(GtkWidget *widget, GdkDragContext *context,
				     GtkSelectionData *selection_data, guint info,
				     guint time, gpointer data)
{
	InfoData *id = data;
	FileData *fd;

	fd = image_get_fd(id->image);
	if (fd)
		{
		gchar *text;
		gint len;
		GList *list;
		gint plain_text;

		switch (info)
			{
			case TARGET_URI_LIST:
				plain_text = FALSE;
				break;
			case TARGET_TEXT_PLAIN:
			default:
				plain_text = TRUE;
				break;
			}
		list = g_list_append(NULL, fd);
		text = uri_text_from_filelist(list, &len, plain_text);
		g_list_free(list);

		gtk_selection_data_set(selection_data, selection_data->target,
				       8, (guchar *)text, len);
		g_free(text);
		}
}

static void info_window_dnd_init(InfoData *id)
{
	ImageWindow *imd;

	imd = id->image;

	gtk_drag_source_set(imd->pr, GDK_BUTTON2_MASK,
			    dnd_file_drag_types, dnd_file_drag_types_count,
			    GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
	g_signal_connect(G_OBJECT(imd->pr), "drag_data_get",
			 G_CALLBACK(info_window_dnd_data_set), id);

#if 0
	gtk_drag_dest_set(imd->pr,
			  GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP,
			  dnd_file_drop_types, dnd_file_drop_types_count,
			  GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
	g_signal_connect(G_OBJECT(imd->pr), "drag_data_received",
			 G_CALLBACK(info_window_dnd_data_get), id);
#endif
}

/*
 *-------------------------------------------------------------------
 * base window
 *-------------------------------------------------------------------
 */

static gint info_window_last_width = DEF_PROPERTY_WIDTH;
static gint info_window_last_height = DEF_PROPERTY_HEIGHT;

static void info_window_image_update_cb(ImageWindow *imd, gpointer data)
{
	InfoData *id = data;

	/* only do this once after when loading a new image,
	 * for tabs that depend on image data (exif)
	 * Subsequent updates are ignored, as the image
	 * should not really changed if id->updated is TRUE.
	 */

	if (id->updated) return;
	if (imd->unknown) return;

	info_tabs_sync(id, TRUE);
	id->updated = TRUE;
}

static void info_window_back_cb(GtkWidget *widget, gpointer data)
{
	InfoData *id = data;
	GList *work;

	work = g_list_find(id->list, (gpointer)id->fd);
	if (!work || !work->prev) return;

	work = work->prev;
	id->fd = work->data;

	info_window_sync(id, id->fd);

	gtk_widget_set_sensitive(id->button_back, (work->prev != NULL));
	gtk_widget_set_sensitive(id->button_next, TRUE);
}

static void info_window_next_cb(GtkWidget *widget, gpointer data)
{
	InfoData *id = data;
	GList *work;

	work = g_list_find(id->list, (gpointer)id->fd);
	if (!work || !work->next) return;

	work = work->next;
	id->fd = work->data;

	info_window_sync(id, id->fd);

	gtk_widget_set_sensitive(id->button_next, (work->next != NULL));
	gtk_widget_set_sensitive(id->button_back, TRUE);
}

static void info_window_image_button_cb(ImageWindow *imd, gint button, guint32 time,
					gdouble x, gdouble y, guint state, gpointer data)
{
	if (button == MOUSE_BUTTON_LEFT)
		{
		info_window_next_cb(NULL, data);
		}
	else if (button == MOUSE_BUTTON_MIDDLE || button == MOUSE_BUTTON_RIGHT)
		{
		info_window_back_cb(NULL, data);
		}
}

static void info_window_image_scroll_cb(ImageWindow *imd, GdkScrollDirection direction, guint32 time,
					gdouble x, gdouble y, guint state, gpointer data)
{
	if (direction == GDK_SCROLL_UP)
		{
		info_window_back_cb(NULL, data);
		}
	else if (direction == GDK_SCROLL_DOWN)
		{
		info_window_next_cb(NULL, data);
		}
}

static void info_window_close(InfoData *id)
{
	gdk_drawable_get_size(id->window->window, &info_window_last_width, &info_window_last_height);
	info_window_last_width = MAX(info_window_last_width, DEF_PROPERTY_WIDTH);
	info_window_last_height = MAX(info_window_last_height, DEF_PROPERTY_HEIGHT);

	gtk_widget_destroy(id->window);
}

static void info_window_close_cb(GtkWidget *widget, gpointer data)
{
	InfoData *id = data;

	info_window_close(id);
}

static gint info_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer data)
{
	InfoData *id = data;

	info_window_close(id);
	return TRUE;
}

static void info_window_destroy_cb(GtkWidget *widget, gpointer data)
{
	InfoData *id = data;

	info_tabs_free(id);
	filelist_free(id->list);
	g_free(id);
}

void info_window_new(FileData *fd, GList *list, GtkWidget *parent)
{
	InfoData *id;
	GtkWidget *main_vbox;
	GtkWidget *paned;
	GtkWidget *hbox;
	GtkWidget *button;
	GtkWidget *label;
	GdkGeometry geometry;

	if (!fd && !list) return;

	if (!list)
		{
		list = g_list_append(NULL, file_data_ref(fd));
		}

	id = g_new0(InfoData, 1);

	id->list = list;
	id->fd = (FileData *)id->list->data;
	id->updated = FALSE;

	id->window = window_new(GTK_WINDOW_TOPLEVEL, "properties", NULL, NULL, _("Image properties"));
	gtk_window_set_type_hint(GTK_WINDOW(id->window), GDK_WINDOW_TYPE_HINT_DIALOG);
	id->parent = parent;
	if (GTK_IS_WINDOW(id->parent)) {
		gtk_window_set_keep_above(GTK_WINDOW(id->window), TRUE);
#if 0
		/* work, but behavior is not that great */
		gtk_window_set_transient_for(GTK_WINDOW(id->window), GTK_WINDOW(id->parent));
#endif
	}
	gtk_window_set_resizable(GTK_WINDOW(id->window), TRUE);

	geometry.min_width = 32;
	geometry.min_height = 32;
	geometry.base_width = DEF_PROPERTY_WIDTH;
	geometry.base_height = DEF_PROPERTY_HEIGHT;
	gtk_window_set_geometry_hints(GTK_WINDOW(id->window), NULL, &geometry,
				      GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE);


	gtk_window_set_default_size(GTK_WINDOW(id->window), info_window_last_width, info_window_last_height);
	gtk_container_set_border_width(GTK_CONTAINER(id->window), PREF_PAD_BORDER);

	g_signal_connect(G_OBJECT(id->window), "delete_event",
			 G_CALLBACK(info_window_delete_cb), id);
	g_signal_connect(G_OBJECT(id->window), "destroy",
			 G_CALLBACK(info_window_destroy_cb), id);

	paned = gtk_hpaned_new();
	gtk_container_add(GTK_CONTAINER(id->window), paned);
	gtk_widget_show(paned);

	id->image = image_new(FALSE);
	image_set_update_func(id->image, info_window_image_update_cb, id);

	image_set_button_func(id->image, info_window_image_button_cb, id);
	image_set_scroll_func(id->image, info_window_image_scroll_cb, id);

	gtk_widget_set_size_request(id->image->widget, IMAGE_SIZE_W, IMAGE_SIZE_H);
	gtk_paned_pack1(GTK_PANED(paned), id->image->widget, FALSE, TRUE);
	gtk_widget_show(id->image->widget);

	main_vbox = gtk_vbox_new(FALSE, 0);
	gtk_paned_pack2(GTK_PANED(paned), main_vbox, TRUE, TRUE);
	gtk_widget_show(main_vbox);

	hbox = pref_box_new(main_vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
	label = pref_label_new(hbox, _("Filename:"));
	pref_label_bold(label, TRUE, FALSE);

	id->name_entry = gtk_entry_new();
	gtk_editable_set_editable(GTK_EDITABLE(id->name_entry), FALSE);
	gtk_box_pack_start(GTK_BOX(hbox), id->name_entry, TRUE, TRUE, 0);
	gtk_widget_show(id->name_entry);

	id->notebook = gtk_notebook_new();
	gtk_notebook_set_tab_pos(GTK_NOTEBOOK(id->notebook), GTK_POS_TOP);
	gtk_box_pack_start(GTK_BOX(main_vbox), id->notebook, TRUE, TRUE, 5);
	g_signal_connect(G_OBJECT(id->notebook), "page-reordered",
			 G_CALLBACK(info_notebook_reordered_cb), id);

	gtk_widget_show(id->notebook);

	pref_spacer(main_vbox, PREF_PAD_GAP);

	hbox = pref_box_new(main_vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_GAP);

	id->button_back = pref_button_new(hbox, GTK_STOCK_GO_BACK, NULL, TRUE,
					  G_CALLBACK(info_window_back_cb), id);
	gtk_widget_set_sensitive(id->button_back, FALSE);

	id->button_next = pref_button_new(hbox, GTK_STOCK_GO_FORWARD, NULL, TRUE,
					  G_CALLBACK(info_window_next_cb), id);
	gtk_widget_set_sensitive(id->button_next, (id->list->next != NULL));

	if (id->list->next)
		{
		id->label_count = pref_label_new(hbox, "");
		}

	button = pref_button_new(NULL, GTK_STOCK_CLOSE, NULL, FALSE,
				 G_CALLBACK(info_window_close_cb), id);
	gtk_box_pack_end(GTK_BOX(hbox), button, FALSE, FALSE, 0);
	gtk_widget_show(button);

	/* set up tabs */

	info_tabs_init(id);

	/* fill it */

	info_window_sync(id, id->fd);

	/* finish */

	info_window_dnd_init(id);

	gtk_widget_show(id->window);
}