/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/* f-image-view.c
 *
 * Copyright (C) 2003  Ettore Perazzoli
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * Author: Ettore Perazzoli <ettore@ximian.com>
 */

#include <config.h>

#include "f-image-view.h"

#include "f-marshal.h"
#include "f-utils.h"
#include "cairo.h"
#include "libeog/cursors.h"


#define PARENT_TYPE image_view_get_type ()
static ImageViewClass *parent_class = NULL;


/* Thickness of the rectangle drawn to show the current selection.  */
#define SELECTION_LINE_WIDTH  1

/* Minimum number of pixels to move the pointer on the screen before the
   is started.  */
#define SELECTION_THRESHOLD  3

/* Maximum distance in pixel required between the selection and the mouse for a
   drag operation to modify the current selection.  */
#define SELECTION_EDGE_DISTANCE 8


enum {
	SELECTION_CHANGED,
	NUM_SIGNALS
};
static unsigned int signals[NUM_SIGNALS] = { 0 };


enum _Mode {
	MODE_IDLE,
	MODE_DRAG_X1,
	MODE_DRAG_X2,
	MODE_DRAG_Y1,
	MODE_DRAG_Y2,
	MODE_DRAG_X1Y1,
	MODE_DRAG_X1Y2,
	MODE_DRAG_X2Y2,
	MODE_DRAG_X2Y1,
	MODE_MOVE
};
typedef enum _Mode Mode;

struct _Selection {
	int x1, y1;
	int x2, y2;
};
typedef struct _Selection Selection;

struct _FImageViewPrivate {
	Mode mode;

	GdkGC *selection_gc;

	FImageViewPointerMode pointer_mode;
	gdouble selection_xy_ratio;

	F_BOOLEAN_MEMBER (selection_active);
	F_BOOLEAN_MEMBER (is_new_selection);

	int button_press_x, button_press_y;
	int drag_x_offset, drag_y_offset;

	/* The current selection.  */
	Selection selection;

	/* The selection as it was before a drag operation was started (used
	   when in MODE_MOVE).  */
	Selection initial_selection;
};


/* Utility functions.  */
void
f_image_view_window_coords_to_image (FImageView *image_view,
				     int window_x, int window_y,
				     int *image_x_return, int *image_y_return)
{
	GdkPixbuf *pixbuf;
	int x_offset, y_offset;
	int scaled_width, scaled_height;

	pixbuf = image_view_get_pixbuf (IMAGE_VIEW (image_view));

	image_view_get_offsets_and_size (IMAGE_VIEW (image_view),
					 &x_offset, &y_offset,
					 &scaled_width, &scaled_height);

	window_x = CLAMP (window_x, x_offset, x_offset + scaled_width - 1);
	window_y = CLAMP (window_y, y_offset, y_offset + scaled_height - 1);

	if (image_x_return != NULL) {
		*image_x_return = floor ((window_x - x_offset)
					 * (double) (gdk_pixbuf_get_width (pixbuf) - 1)
					 / (double) (scaled_width - 1) + .5);
	}

	if (image_y_return != NULL) {
		*image_y_return = floor ((window_y - y_offset)
					 * (double) (gdk_pixbuf_get_height (pixbuf) - 1)
					 / (double) (scaled_height - 1) + .5);
	}

	g_object_unref (pixbuf);
}

static void
image_coords_to_window (FImageView *image_view,
			int image_x, int image_y,
			int *window_x_return, int *window_y_return)
{
	GtkAllocation *allocation;
	GdkPixbuf *pixbuf;
	int x_offset, y_offset;
	int scaled_width, scaled_height;

	pixbuf = image_view_get_pixbuf (IMAGE_VIEW (image_view));
	allocation = & GTK_WIDGET (image_view)->allocation;

	image_view_get_offsets_and_size (IMAGE_VIEW (image_view),
					 &x_offset, &y_offset,
					 &scaled_width, &scaled_height);

	if (window_x_return != NULL) {
		*window_x_return = floor (image_x
					  * (double) (scaled_width - 1)
					  / (gdk_pixbuf_get_width (pixbuf) - 1) + .5);
		*window_x_return += x_offset;
	}
	if (window_y_return != NULL) {
		*window_y_return = floor (image_y
					  * (double) (scaled_height - 1)
					  / (gdk_pixbuf_get_height (pixbuf) - 1) + .5);
		*window_y_return += y_offset;
	}

	g_object_unref (pixbuf);
}

static void 
draw_union (GtkWidget *widget, GdkRectangle *previous, GdkRectangle *current)
{
	gdk_rectangle_union (current, previous, current);
	gtk_widget_queue_draw_area (widget, current->x, current->y, current->width, current->height);
	//gtk_widget_queue_draw (widget);
}

static GdkCursor *
get_cursor_for_mode (FImageView *image_view, Mode mode)
{
	if (mode != MODE_IDLE && image_view->priv->is_new_selection)
		return gdk_cursor_new_for_display (gdk_display_get_default (), GDK_TOP_LEFT_ARROW);

	switch (mode) {
	case MODE_IDLE:
		return NULL;

	case MODE_DRAG_X1:
	case MODE_DRAG_X2:
		return gdk_cursor_new_for_display (gdk_display_get_default (), GDK_SB_H_DOUBLE_ARROW);
		
	case MODE_DRAG_Y1:
	case MODE_DRAG_Y2:
		return gdk_cursor_new_for_display (gdk_display_get_default (), GDK_SB_V_DOUBLE_ARROW);

	case MODE_DRAG_X1Y1:
		return gdk_cursor_new_for_display (gdk_display_get_default (), GDK_TOP_LEFT_CORNER);

	case MODE_DRAG_X1Y2:
		return gdk_cursor_new_for_display (gdk_display_get_default (), GDK_BOTTOM_LEFT_CORNER);

	case MODE_DRAG_X2Y2:
		return gdk_cursor_new_for_display (gdk_display_get_default (), GDK_BOTTOM_RIGHT_CORNER);

	case MODE_DRAG_X2Y1:
		return gdk_cursor_new_for_display (gdk_display_get_default (), GDK_TOP_RIGHT_CORNER);

	case MODE_MOVE:
		return cursor_get (GTK_WIDGET (image_view), CURSOR_HAND_OPEN);

	default:
		g_assert_not_reached ();
	}

	return NULL;
}

static void
set_cursor (FImageView *image_view)
{
	GdkCursor *cursor = NULL;

	if (image_view->priv->mode != MODE_IDLE) {
		cursor = get_cursor_for_mode (image_view, image_view->priv->mode);
	} else {
		switch (image_view->priv->pointer_mode) {
		case F_IMAGE_VIEW_POINTER_MODE_NONE:
		case F_IMAGE_VIEW_POINTER_MODE_SELECT:
			cursor = gdk_cursor_new_for_display (gdk_display_get_default (), GDK_TOP_LEFT_ARROW);
			break;

		case F_IMAGE_VIEW_POINTER_MODE_SCROLL:
			cursor = cursor_get (GTK_WIDGET (image_view), CURSOR_HAND_OPEN);
			break;

		default:
			g_assert_not_reached ();
		}
	}

	gdk_window_set_cursor (GTK_WIDGET (image_view)->window, cursor);
	gdk_cursor_unref (cursor);
}




/* Utilty functions for selection handling.  */

/* This uses GDK_INVERT so if called a second time without the selection
   changing it will remove the selection rectangle from the window.  */
static void
draw_selection (FImageView *image_view,
		GdkRectangle *area)
{
	FImageViewPrivate *priv = image_view->priv;
	int x1, y1, x2, y2;
	GdkRectangle zone;

	if (! priv->selection_active
	    || (area != NULL && (area->width == 0 || area->height == 0)))
		return;

	image_coords_to_window (image_view, priv->selection.x1, priv->selection.y1, &x1, &y1);
	image_coords_to_window (image_view, priv->selection.x2, priv->selection.y2, &x2, &y2);

	zone.x = MIN (x1, x2);
	zone.y = MIN (y1, y2);

	zone.width = ABS (x1 - x2);
	zone.height = ABS (y1 - y2);

	gtk_widget_queue_draw_area (GTK_WIDGET (image_view), zone.x, zone.y, zone.width, zone.height);
}

static GdkRectangle
get_selection_box (FImageView *image_view)
{
	FImageViewPrivate *priv = image_view->priv;
	int x1, y1, x2, y2;
	GdkRectangle zone;
	GdkPixbuf *pixbuf = image_view_get_pixbuf (IMAGE_VIEW (image_view));

	if (! priv->selection_active) {
		//zone = (GdkRectangle) GTK_WIDGET (image_view)->allocation;
		image_coords_to_window (image_view, 0, 0, &x1, &y1);
		if (pixbuf != NULL)
			image_coords_to_window (image_view, gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf), &x2, &y2);
		else 
			image_coords_to_window (image_view, 0, 0, &x2, &y2);
	} else {
		image_coords_to_window (image_view, priv->selection.x1, priv->selection.y1, &x1, &y1);
		image_coords_to_window (image_view, priv->selection.x2, priv->selection.y2, &x2, &y2);
	}

	zone.x = MIN (x1, x2);
	zone.y = MIN (y1, y2);

	zone.width = ABS (x1 - x2);
	zone.height = ABS (y1 - y2);

	return zone;
}

static gboolean
adjust_height_for_constraints (FImageView *image_view)
{
	FImageViewPrivate *priv = image_view->priv;
	int offset = floor ((ABS (priv->selection.x1 - priv->selection.x2) / priv->selection_xy_ratio) + .5);
	int *py1, *py2;

	switch (priv->mode) {
	case MODE_DRAG_Y1:
	case MODE_DRAG_X1Y1:
	case MODE_DRAG_X2Y1:
		py1 = &priv->selection.y2;
		py2 = &priv->selection.y1;
		break;
	default:
		py1 = &priv->selection.y1;
		py2 = &priv->selection.y2;
		break;
	}

	if (*py2 < *py1) {
		if (*py1 - offset < 0) {
			*py2 = 0;
			return FALSE;
		}

		*py2 = *py1 - offset;
	} else {
		GdkPixbuf *pixbuf = image_view_get_pixbuf (IMAGE_VIEW (image_view));

		if (pixbuf == NULL)
			return FALSE;

		if (*py1 + offset >= gdk_pixbuf_get_height (pixbuf)) {
			*py2 = gdk_pixbuf_get_height (pixbuf) - 1;
			g_object_unref (pixbuf);
			return FALSE;
		}

		*py2 = *py1 + offset;

		g_object_unref (pixbuf);
	}

	return TRUE;
}

static gboolean
adjust_width_for_constraints (FImageView *image_view)
{
	FImageViewPrivate *priv = image_view->priv;
	int offset = floor ((ABS (priv->selection.y1 - priv->selection.y2) * priv->selection_xy_ratio) + .5);
	int *px1, *px2;

	switch (priv->mode) {
	case MODE_DRAG_X1:
	case MODE_DRAG_X1Y1:
	case MODE_DRAG_X1Y2:
		px1 = &priv->selection.x2;
		px2 = &priv->selection.x1;
		break;
	default:
		px1 = &priv->selection.x1;
		px2 = &priv->selection.x2;
		break;
	}

	if (*px2 < *px1) {
		if (*px1 - offset < 0) {
			*px2 = 0;
			return FALSE;
		}

		*px2 = *px1 - offset;
	} else {
		GdkPixbuf *pixbuf = image_view_get_pixbuf (IMAGE_VIEW (image_view));

		if (pixbuf == NULL)
			return FALSE;

		if (*px1 + offset >= gdk_pixbuf_get_width (pixbuf)) {
			*px2 = gdk_pixbuf_get_width (pixbuf) - 1;
			g_object_unref (pixbuf);
			return FALSE;
		}

		*px2 = *px1 + offset;

		g_object_unref (pixbuf);
	}

	return TRUE;
}

static void
constrain_selection (FImageView *image_view)
{
	FImageViewPrivate *priv = image_view->priv;
	double ratio;

	if (F_DOUBLE_EQUAL (priv->selection_xy_ratio, 0.0))
		return;
	if ((ABS (priv->selection.x2 - priv->selection.x1) > ABS (priv->selection.y2 - priv->selection.y1) && priv->selection_xy_ratio < 1) ||
		(ABS (priv->selection.x2 - priv->selection.x1) < ABS (priv->selection.y2 - priv->selection.y1) && priv->selection_xy_ratio > 1)) 
		priv->selection_xy_ratio = (double)1.0 / priv->selection_xy_ratio;

	switch (priv->mode) {
	case MODE_DRAG_X1:
	case MODE_DRAG_X2:
		if (! adjust_height_for_constraints (image_view))
			adjust_width_for_constraints (image_view);
		break;

	case MODE_DRAG_Y1:
	case MODE_DRAG_Y2:
		if (! adjust_width_for_constraints (image_view))
			adjust_height_for_constraints (image_view);
		break;

	default:
		ratio = (double) ABS (priv->selection.x2 - priv->selection.x1) / ABS (priv->selection.y2 - priv->selection.y1);
		if (ratio > priv->selection_xy_ratio) {
			if (! adjust_height_for_constraints (image_view))
				adjust_width_for_constraints (image_view);
		} else {
			if (! adjust_width_for_constraints (image_view))
				adjust_height_for_constraints (image_view);
		}
	}
}

static gboolean
check_corner_for_drag (int x, int y,
		       int mouse_x, int mouse_y,
		       int *x_offset_return, int *y_offset_return)
{
	if (ABS (mouse_x - x) > SELECTION_EDGE_DISTANCE || ABS (mouse_y - y) > SELECTION_EDGE_DISTANCE)
		return FALSE;

	if (x_offset_return != NULL)
		*x_offset_return = mouse_x - x;
	if (y_offset_return != NULL)
		*y_offset_return = mouse_y - y;

	return TRUE;
}

static gboolean
check_side_for_drag (int coord,
		     int mouse_coord,
		     int *offset_return)
{
	if (ABS (mouse_coord - coord) > SELECTION_EDGE_DISTANCE)
		return FALSE;

	if (offset_return != NULL)
		*offset_return = mouse_coord - coord;

	return TRUE;
}

static Mode
get_drag_mode_for_mouse_position (FImageView *view,
				  int mouse_x, int mouse_y,
				  int *drag_x_offset_return, int *drag_y_offset_return)
{
	FImageViewPrivate *priv = view->priv;
	int x1, y1, x2, y2, swap;

	if (! priv->selection_active)
		return MODE_IDLE;

	image_coords_to_window (view, priv->selection.x1, priv->selection.y1, &x1, &y1);
	image_coords_to_window (view, priv->selection.x2, priv->selection.y2, &x2, &y2);

	//Swap coords so x1<x2 and y1<y2
	if (priv->selection.x1 > priv->selection.x2) {
		swap = priv->selection.x1 ;
		priv->selection.x1 = priv->selection.x2 ;
		priv->selection.x2 = swap ;
	}
	if (priv->selection.y1 > priv->selection.y2) {
		swap = priv->selection.y1 ;
		priv->selection.y1 = priv->selection.y2 ;
		priv->selection.y2 = swap ;
	}

	if (check_corner_for_drag (x1, y1, mouse_x, mouse_y,
				   drag_x_offset_return, drag_y_offset_return))
		return MODE_DRAG_X1Y1;

	if (check_corner_for_drag (x1, y2, mouse_x, mouse_y,
				   drag_x_offset_return, drag_y_offset_return))
		return MODE_DRAG_X1Y2;

	if (check_corner_for_drag (x2, y2, mouse_x, mouse_y,
				   drag_x_offset_return, drag_y_offset_return))
		return MODE_DRAG_X2Y2;

	if (check_corner_for_drag (x2, y1, mouse_x, mouse_y,
				   drag_x_offset_return, drag_y_offset_return))
		return MODE_DRAG_X2Y1;

	if (drag_x_offset_return != NULL)
		*drag_x_offset_return = 0;
	if (drag_y_offset_return != NULL)
		*drag_y_offset_return = 0;


	if (mouse_x >= MIN (x1, x2) - SELECTION_EDGE_DISTANCE && mouse_x <= MAX (x1, x2) + SELECTION_EDGE_DISTANCE) {
		if (check_side_for_drag (y1, mouse_y, drag_y_offset_return))
			return MODE_DRAG_Y1;

		if (check_side_for_drag (y2, mouse_y, drag_y_offset_return))
			return MODE_DRAG_Y2;
	}

	if (mouse_y >= MIN (y1, y2) - SELECTION_EDGE_DISTANCE && mouse_y <= MAX (y1, y2) + SELECTION_EDGE_DISTANCE) {
		if (check_side_for_drag (x1, mouse_x, drag_x_offset_return))
			return MODE_DRAG_X1;

		if (check_side_for_drag (x2, mouse_x, drag_x_offset_return))
			return MODE_DRAG_X2;
	}

	if (mouse_x >= MIN (x1, x2) && mouse_x <= MAX (x1, x2)
	    && mouse_y >= MIN (y1, y2) && mouse_y <= MAX (y1, y2))
		return MODE_MOVE;

	return MODE_IDLE;
}

static void
emit_selection_changed (FImageView *image_view)
{
	g_signal_emit (image_view, signals[SELECTION_CHANGED], 0);
}


/* ImageView methods.  */

static void
impl_paint_extra (ImageView *iv,
		  GdkRectangle *area)
{
#if FALSE
	FImageView *image_view = (FImageView *) iv;
	FImageViewPrivate *priv = image_view->priv;
	int x1, y1, x2, y2;
	cairo_t *ctx;
	GdkRegion *selection;
	GdkRegion *other;
	GdkRectangle rect;

	if (! priv->selection_active)
		return; 

	image_coords_to_window (image_view, priv->selection.x1, priv->selection.y1, &x1, &y1);
	image_coords_to_window (image_view, priv->selection.x2, priv->selection.y2, &x2, &y2);

	rect.x = MIN (x1, x2);
	rect.y = MIN (y1, y2);

	rect.width = ABS (x1 - x2);
	rect.height = ABS (y1 - y2);
	
	other = gdk_region_new ();
	gdk_region_union_with_rect (other, area);
	selection = gdk_region_new ();
	gdk_region_union_with_rect (selection, &rect);
	gdk_region_subtract (other, selection);
	gdk_region_destroy (selection);

	ctx = gdk_cairo_create (GTK_WIDGET (image_view)->window);
	cairo_set_source_rgba (ctx, .5, .5, .5, .7);
	gdk_cairo_region (ctx, other);
	cairo_fill (ctx);
	cairo_destroy (ctx);
	gdk_region_destroy (other);
#endif
}


static gboolean
impl_expose_event (GtkWidget *widget, GdkEventExpose *event)
{
	FImageView *image_view = F_IMAGE_VIEW (widget);
	FImageViewPrivate *priv = image_view->priv;
	int x1, y1, x2, y2;
	cairo_t *ctx;
	GdkRegion *selection;
	GdkRectangle rect;

	(* GTK_WIDGET_CLASS (parent_class)->expose_event) (widget, event);

	if (! priv->selection_active)
		return FALSE; 

	image_coords_to_window (image_view, priv->selection.x1, priv->selection.y1, &x1, &y1);
	image_coords_to_window (image_view, priv->selection.x2, priv->selection.y2, &x2, &y2);

	rect.x = MIN (x1, x2);
	rect.y = MIN (y1, y2);

	rect.width = ABS (x1 - x2);
	rect.height = ABS (y1 - y2);
	
	selection = gdk_region_new ();
	gdk_region_union_with_rect (selection, &rect);
	gdk_region_subtract (event->region, selection);
	gdk_region_destroy (selection);

	ctx = gdk_cairo_create (GTK_WIDGET (widget)->window);
	cairo_set_source_rgba (ctx, .5, .5, .5, .7);
	gdk_cairo_region (ctx, event->region);
	cairo_fill (ctx);
	cairo_destroy (ctx);
	return TRUE;
}

/* GtkWidget methods.  */

static void
impl_realize (GtkWidget *widget)
{
	FImageViewPrivate *priv = F_IMAGE_VIEW (widget)->priv;

	(* GTK_WIDGET_CLASS (parent_class)->realize) (widget);

	set_cursor (F_IMAGE_VIEW (widget));

	g_assert (priv->selection_gc == NULL);

	priv->selection_gc = gdk_gc_new (widget->window);
	gdk_gc_copy (priv->selection_gc, widget->style->fg_gc[GTK_STATE_NORMAL]);
	gdk_gc_set_function (priv->selection_gc, GDK_INVERT);
	gdk_gc_set_line_attributes (priv->selection_gc, SELECTION_LINE_WIDTH,
				    GDK_LINE_SOLID, GDK_CAP_NOT_LAST, GDK_JOIN_MITER);
}

static void
impl_unrealize (GtkWidget *widget)
{
	(* GTK_WIDGET_CLASS (parent_class)->unrealize) (widget);

	F_UNREF (F_IMAGE_VIEW (widget)->priv->selection_gc);
}

static gboolean
impl_button_press_event (GtkWidget *widget,
			 GdkEventButton *button_event)
{
	FImageView *image_view = F_IMAGE_VIEW (widget);
	FImageViewPrivate *priv = image_view->priv;
	Mode mode;

	if (priv->pointer_mode == F_IMAGE_VIEW_POINTER_MODE_SCROLL)
		return (* GTK_WIDGET_CLASS (parent_class)->button_press_event) (widget, button_event);
	else if (priv->pointer_mode == F_IMAGE_VIEW_POINTER_MODE_NONE)
		return FALSE;

	if (button_event->type == GDK_2BUTTON_PRESS && button_event->button == 1) {
		priv->is_new_selection = FALSE;
		priv->mode = MODE_IDLE;
		return FALSE;
	}

	if (button_event->type != GDK_BUTTON_PRESS || button_event->button != 1 || priv->mode != MODE_IDLE)
		return FALSE;

	if (! GTK_WIDGET_HAS_FOCUS (widget))
		gtk_widget_grab_focus (widget);

	mode = get_drag_mode_for_mouse_position (image_view, button_event->x, button_event->y,
						 & priv->drag_x_offset, & priv->drag_y_offset);
	if (mode == MODE_IDLE) {
		priv->mode = MODE_DRAG_X2Y2;
		priv->is_new_selection = TRUE;
	} else {
		priv->mode = mode;
		priv->is_new_selection = FALSE;
	}

	priv->initial_selection = priv->selection;

	priv->button_press_x = button_event->x;
	priv->button_press_y = button_event->y;

	if (priv->is_new_selection) {
		/* Erase existing selection rectangle.  */
		gtk_widget_queue_draw (widget);

		f_image_view_window_coords_to_image (image_view,
						     button_event->x, button_event->y,
						     &priv->selection.x1, &priv->selection.y1);
		priv->selection_active = FALSE;
		priv->selection.x2 = priv->selection.x1;
		priv->selection.y2 = priv->selection.y1;

		emit_selection_changed (F_IMAGE_VIEW (image_view));
	}

	set_cursor (image_view);

	return TRUE;
}

static gboolean
impl_motion_notify_event (GtkWidget *widget,
			  GdkEventMotion *motion_event)
{
	FImageView *image_view = F_IMAGE_VIEW (widget);
	FImageViewPrivate *priv = image_view->priv;
	GdkModifierType mods;
	int x, y;
	int image_x, image_y;
	GdkRectangle previous;
	GdkRectangle current;
	
	if (priv->pointer_mode == F_IMAGE_VIEW_POINTER_MODE_SCROLL)
		return (* GTK_WIDGET_CLASS (parent_class)->motion_notify_event) (widget, motion_event);

	if (motion_event->is_hint)
		gdk_window_get_pointer (widget->window, &x, &y, &mods);
	else {
		x = motion_event->x;
		y = motion_event->y;
	}

	if (priv->mode == MODE_IDLE) {
		Mode drag_mode = get_drag_mode_for_mouse_position (image_view, x, y, NULL, NULL);
		GdkCursor *cursor;

		cursor = get_cursor_for_mode (image_view, drag_mode);

		if (cursor != NULL) {
			gdk_window_set_cursor (widget->window, cursor);
			gdk_cursor_unref (cursor);
		} else {
			set_cursor (image_view);
		}

		return TRUE;
	}

	previous = get_selection_box (image_view);
	if (! priv->selection_active) {
		/* Start the drag selection if the pointer has moved enough from the
		   initial clicking position.  */
		if (ABS (x - priv->button_press_x) < SELECTION_THRESHOLD
		    && ABS (y - priv->button_press_y) < SELECTION_THRESHOLD)
			return TRUE;
	
		priv->selection_active = TRUE;
	}

	f_image_view_window_coords_to_image (image_view, x, y, &image_x, &image_y);

	switch (priv->mode) {
	case MODE_DRAG_X1:
		priv->selection.x1 = image_x;
		break;

	case MODE_DRAG_X2:
		priv->selection.x2 = image_x;
		break;

	case MODE_DRAG_Y1:
		priv->selection.y1 = image_y;
		break;

	case MODE_DRAG_Y2:
		priv->selection.y2 = image_y;
		break;

	case MODE_DRAG_X1Y1:
		priv->selection.x1 = image_x;
		priv->selection.y1 = image_y;
		break;

	case MODE_DRAG_X1Y2:
		priv->selection.x1 = image_x;
		priv->selection.y2 = image_y;
		break;

	case MODE_DRAG_X2Y2:
		priv->selection.x2 = image_x;
		priv->selection.y2 = image_y;
		break;

	case MODE_DRAG_X2Y1:
		priv->selection.x2 = image_x;
		priv->selection.y1 = image_y;
		break;

	case MODE_MOVE: {
		double x_zoom, y_zoom;
		int x_offset, y_offset;
		int x1, x2, y1, y2;
		GdkPixbuf *pixbuf = image_view_get_pixbuf (IMAGE_VIEW (image_view));

		image_view_get_zoom (IMAGE_VIEW (image_view), &x_zoom, &y_zoom);

		x_offset = (x - priv->button_press_x) / x_zoom;
		y_offset = (y - priv->button_press_y) / y_zoom;

		x1 = MIN (priv->initial_selection.x1, priv->initial_selection.x2);
		x2 = MAX (priv->initial_selection.x1, priv->initial_selection.x2);
		y1 = MIN (priv->initial_selection.y1, priv->initial_selection.y2);
		y2 = MAX (priv->initial_selection.y1, priv->initial_selection.y2);

		x_offset = CLAMP (x_offset, -x1, gdk_pixbuf_get_width (pixbuf) - 1 - x2);
		y_offset = CLAMP (y_offset, -y1, gdk_pixbuf_get_height (pixbuf) - 1 - y2);

		priv->selection.x1 = priv->initial_selection.x1 + x_offset;
		priv->selection.y1 = priv->initial_selection.y1 + y_offset;
		priv->selection.x2 = priv->initial_selection.x2 + x_offset;
		priv->selection.y2 = priv->initial_selection.y2 + y_offset;

		g_object_unref (pixbuf);
		break;
	}

	default:
		g_assert_not_reached ();
	}

	constrain_selection (image_view);
	current = get_selection_box (image_view);

	draw_union (widget, &current, &previous);

	emit_selection_changed (image_view);
	return TRUE;
}

static gboolean
impl_button_release_event (GtkWidget *widget,
			   GdkEventButton *button_event)
{
	FImageViewPrivate *priv = F_IMAGE_VIEW (widget)->priv;

	if (priv->pointer_mode == F_IMAGE_VIEW_POINTER_MODE_SCROLL)
		return (* GTK_WIDGET_CLASS (parent_class)->button_release_event) (widget, button_event);

	priv->is_new_selection = FALSE;
	priv->mode = MODE_IDLE;

	set_cursor (F_IMAGE_VIEW (widget));

	return TRUE;
}


/* GObject methods.  */

static void
impl_finalize (GObject *object)
{
	FImageViewPrivate *priv = F_IMAGE_VIEW (object)->priv;

	g_free (priv);

	(* G_OBJECT_CLASS (parent_class)->finalize) (object);
}


/* Initialization.  */

static void
class_init (FImageViewClass *class)
{
	ImageViewClass *image_view_class = IMAGE_VIEW_CLASS (class);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
	GObjectClass *object_class = G_OBJECT_CLASS (class);

	image_view_class->paint_extra = impl_paint_extra;

	widget_class->realize              = impl_realize;
	widget_class->unrealize            = impl_unrealize;
	widget_class->button_press_event   = impl_button_press_event;
	widget_class->motion_notify_event  = impl_motion_notify_event;
	widget_class->button_release_event = impl_button_release_event;
	widget_class->expose_event         = impl_expose_event;

	object_class->finalize = impl_finalize;

	parent_class = g_type_class_peek_parent (class);

	signals[SELECTION_CHANGED] = g_signal_new ("selection_changed",
						   G_TYPE_FROM_CLASS (class),
						   G_SIGNAL_RUN_LAST,
						   G_STRUCT_OFFSET (FImageViewClass, selection_changed),
						   NULL, NULL,
						   f_marshal_VOID__VOID,
						   G_TYPE_NONE, 0);
}

static void
init (FImageView *image_view)
{
	FImageViewPrivate *priv;

	priv = g_new0 (FImageViewPrivate, 1);
	priv->mode         = MODE_IDLE;
	priv->pointer_mode = F_IMAGE_VIEW_POINTER_MODE_SELECT;

	image_view->priv = priv;
}


/* Instantiation.  */

GtkWidget *
f_image_view_new (void)
{
	return g_object_new (f_image_view_get_type (), NULL);
}


/* Accessor functions.  */

void
f_image_view_set_pointer_mode (FImageView *image_view,
			       FImageViewPointerMode mode)
{
	image_view->priv->pointer_mode = mode;
}

FImageViewPointerMode
f_image_view_get_pointer_mode (FImageView *image_view)
{
	return image_view->priv->pointer_mode;
}

void
f_image_view_set_selection_xy_ratio (FImageView *image_view,
				     gdouble selection_xy_ratio)
{
	FImageViewPrivate *priv = image_view->priv;
	GdkRectangle previous;
	GdkRectangle current;

	priv->selection_xy_ratio = selection_xy_ratio;

	previous = get_selection_box (image_view);
	constrain_selection (image_view);
	current = get_selection_box (image_view);
	draw_union (GTK_WIDGET (image_view), &current, &previous);

	emit_selection_changed (image_view);
}

gdouble
f_image_view_get_selection_xy_ratio (FImageView *image_view)
{
	return image_view->priv->selection_xy_ratio;
}


gboolean
f_image_view_get_selection (FImageView *image_view,
			    int *x_return, int *y_return,
			    int *width_return, int *height_return)
{
	FImageViewPrivate *priv = image_view->priv;

	if (! priv->selection_active) {
		*x_return = *y_return = 0;
		*width_return = *height_return = 0;
		return FALSE;
	}

	*x_return = MIN (priv->selection.x1, priv->selection.x2);
	*y_return = MIN (priv->selection.y1, priv->selection.y2);
	*width_return = ABS (priv->selection.x1 - priv->selection.x2);
	*height_return = ABS (priv->selection.y1 - priv->selection.y2);

	return TRUE;
}


void
f_image_view_unset_selection (FImageView *image_view)
{
	if (image_view->priv->selection_active) {
		image_view->priv->selection_active = FALSE;
		emit_selection_changed (image_view);
	}
}


F_MAKE_TYPE (FImageView, f_image_view)
