// K-3D
// Copyright (c) 1995-2004, Timothy M. Shead
//
// Contact: tshead@k-3d.com
//
// 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

/** \file
		\brief Implements the k3d::viewport namespace, which provides a MVC UI for viewing the document
		\author Tim Shead (tshead@k-3d.com)
*/

#include "context_menu.h"
#include "gtkml.h"
#include "k3ddialog.h"
#include "keyboard.h"
#include "viewport_control.h"

#include <k3dsdk/algebra.h>
#include <k3dsdk/color.h>
#include <k3dsdk/frames.h>
#include <k3dsdk/geometry.h>
#include <k3dsdk/glutility.h>
#include <k3dsdk/high_res_timer.h>
#include <k3dsdk/icommand_tree.h>
#include <k3dsdk/idag.h>
#include <k3dsdk/idocument.h>
#include <k3dsdk/ihandle.h>
#include <k3dsdk/iobject_collection.h>
#include <k3dsdk/ioptions.h>
#include <k3dsdk/iplugin_factory.h>
#include <k3dsdk/iprojection.h>
#include <k3dsdk/iuser_interface.h>
#include <k3dsdk/iviewport.h>
#include <k3dsdk/iviewport_host.h>
#include <k3dsdk/mesh.h>
#include <k3dsdk/mouse_event_observer.h>
#include <k3dsdk/objects.h>
#include <k3dsdk/plugins.h>
#include <k3dsdk/property.h>
#include <k3dsdk/property_collection.h>
#include <k3dsdk/selection.h>
#include <k3dsdk/state_change_set.h>
#include <k3dsdk/string_modifiers.h>
#include <k3dsdk/time_source.h>
#include <k3dsdk/transform.h>
#include <k3dsdk/viewport.h>

#include <sdpgtk/sdpgtkevents.h>
#include <sdpgtk/sdpgtkmouseinput.h>
#include <sdpgtk/sdpgtkopengldrawingarea.h>
#include <sdpgtk/sdpgtkutility.h>

#include <boost/any.hpp>
#include <boost/mem_fn.hpp>

#include <iomanip>

#ifdef	WIN32
#include <GL/glext.h>
#endif	//WIN32

namespace
{

const std::string control_frame("frame");
const std::string control_mousemove = "mousemove";
const std::string control_lbuttondown = "lbuttondown";
const std::string control_lbuttonclick = "lbuttonclick";
const std::string control_lbuttonup = "lbuttonup";
const std::string control_lbuttondoubleclick = "lbuttondoubleclick";
const std::string control_lbuttonstartdrag = "lbuttonstartdrag";
const std::string control_lbuttondrag = "lbuttondrag";
const std::string control_lbuttonenddrag = "lbuttonenddrag";
const std::string control_mbuttondown = "mbuttondown";
const std::string control_mbuttonclick = "mbuttonclick";
const std::string control_mbuttonup = "mbuttonup";
const std::string control_mbuttondoubleclick = "mbuttondoubleclick";
const std::string control_mbuttonstartdrag = "mbuttonstartdrag";
const std::string control_mbuttondrag = "mbuttondrag";
const std::string control_mbuttonenddrag = "mbuttonenddrag";
const std::string control_rbuttondown = "rbuttondown";
const std::string control_rbuttonclick = "rbuttonclick";
const std::string control_rbuttonup = "rbuttonup";
const std::string control_rbuttondoubleclick = "rbuttondoubleclick";
const std::string control_rbuttonstartdrag = "rbuttonstartdrag";
const std::string control_rbuttondrag = "rbuttondrag";
const std::string control_rbuttonenddrag = "rbuttonenddrag";
const std::string control_lrbuttonstartdrag = "lrbuttonstartdrag";
const std::string control_lrbuttondrag = "lrbuttondrag";
const std::string control_lrbuttonenddrag = "lrbuttonenddrag";

/// Defines the color used to XOR selection rubber-bands and lassos into the viewport
const k3d::color g_selection_color(0.215, 0.215, 0.8);

/// Defines storage for a collection of OpenGL hit records
typedef std::vector<GLuint> gl_selection_buffer_t;

/// Wrapper class for OpenGL hit records - designed to resemble an STL container
class hit_record
{
public:
	explicit hit_record(GLuint* const Storage) :
		m_storage(Storage)
	{
		assert(m_storage);
	}

	/// Returns the minimum Z depth of the hit
	GLuint zmin() const
	{
		return *(m_storage+1);
	}

	/// Returns the maximum Z depth of the hit
	GLuint zmax() const
	{
		return *(m_storage+2);
	}

	/// Returns true iff the hit was empty (i.e. doesn't contain any names)
	bool empty() const
	{
		return 0 == size();
	}

	/// Returns the number of names contained in the hit
	unsigned int size() const
	{
		return *m_storage;
	}

	/// Defines an iterator type for accessing hit names
	typedef GLuint* const_name_iterator;

	/// Returns an iterator designating the beginning of the range of hit names
	const_name_iterator name_begin() const
	{
		return m_storage+3;
	}

	/// Returns an iterator designating one-past-the-end of the range of hit names
	const_name_iterator name_end() const
	{
		return m_storage+3+size();
	}

	/// Defines a strict ordering for non-empty hit records based on minimum Z depth, since we typically want to find the "closest" hit
	friend bool operator<(const hit_record& LHS, const hit_record& RHS)
	{
		if(LHS.empty())
			return false;

		return LHS.zmin() < RHS.zmin();
	}

	/// hit_record serialization, mainly intended for debugging
	friend std::ostream& operator<<(std::ostream& Stream, const hit_record& RHS)
	{
		Stream << RHS.zmin() << " " << RHS.zmax() << " ";
		for(hit_record::const_name_iterator name = RHS.name_begin(); name != RHS.name_end(); )
			{
				k3d::iunknown* const unknown = k3d::glGetName(name);
				Stream << typeid(*unknown).name() << " ";
/*
				// If we hit a "handle", we short-circuit the rest of the process and "grab" it ...
				k3d::ihandle* const handle = dynamic_cast<k3d::ihandle*>(unknown);
				if(handle)
					{
						handle->grab();

						k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
						return true;
					}

				// Nope, so see if we hit something selectable ...
				k3d::iselectable* const selectable = dynamic_cast<k3d::iselectable*>(unknown);
				if(!selectable)
					continue;

				// Handle different selection modes ...
				if(select_objects && dynamic_cast<k3d::iobject*>(unknown))
					selection = selectable;
				else if(select_objects && dynamic_cast<k3d::mesh*>(unknown))
					selection = selectable;
				else if(select_polygons && dynamic_cast<k3d::point_group*>(unknown))
					selection = selectable;
				else if(select_polygons && dynamic_cast<k3dIPath*>(unknown))
					selection = selectable;
				else if(select_polygons && dynamic_cast<k3d::split_edge*>(unknown))
					selection = selectable;
				else if(select_polygons && dynamic_cast<k3d::face*>(unknown))
					selection = selectable;
				else if(select_polygons && dynamic_cast<k3d::bilinear_patch*>(unknown))
					selection = selectable;
				else if(select_polygons && dynamic_cast<k3d::bicubic_patch*>(unknown))
					selection = selectable;
				else if(select_polygons && dynamic_cast<k3d::nucurve*>(unknown))
					selection = selectable;
				else if(select_points && dynamic_cast<k3dIPoint*>(unknown))
					selection = selectable;
				else if(select_points && dynamic_cast<k3d::point*>(unknown))
					selection = selectable;
*/
			}

		return Stream;
	}

private:
	GLuint* const m_storage;
};

/// Input iterator that extracts objects of type hit_record from a flat buffer
class hit_iterator
{
public:
	hit_iterator() :
		m_current(0),
		m_remaining(0)
	{
	}

	hit_iterator(gl_selection_buffer_t& Buffer, const unsigned int HitCount) :
		m_current(HitCount ? &Buffer[0] : 0),
		m_remaining(HitCount)
	{
	}

	hit_record operator*() const
	{
		return hit_record(m_current);
	}

	hit_record operator->() const
	{
		return hit_record(m_current);
	}

	hit_iterator& operator++()
	{
		if(m_remaining)
			{
				if(0 == --m_remaining)
					m_current = 0;
				else
					m_current += (3 + hit_record(m_current).size());
			}

		return *this;
	}

	hit_iterator operator++(int)
	{
		hit_iterator temp(*this);
		this->operator++();

		return temp;
	}

	friend bool operator == (const hit_iterator& LHS, const hit_iterator& RHS)
	{
		return LHS.m_current == RHS.m_current;
	}

	friend bool operator != (const hit_iterator& LHS, const hit_iterator& RHS)
	{
		return !(LHS == RHS);
	}

private:
	GLuint* m_current;
	unsigned int m_remaining;
};

/// Creates an XOR GC in the special selection color
GdkGC* selection_gc(sdpGtkWidget& Widget)
{
	// Sanity checks ...
	return_val_if_fail(Widget.Attached(), 0);

	// Create the rubber-band color ...
	GdkColor color;
	color.red = static_cast<unsigned short>(g_selection_color.red * 0xffff);
	color.green = static_cast<unsigned short>(g_selection_color.green * 0xffff);
	color.blue = static_cast<unsigned short>(g_selection_color.blue * 0xffff);
	gdk_color_alloc(gdk_colormap_get_system(), &color);

	// Create an XOR gc for drawing ...
	GdkGC* gc = gdk_gc_new(static_cast<GtkWidget*>(Widget)->window);
	gdk_gc_set_foreground(gc, &color);
	gdk_gc_set_function(gc, GDK_XOR);

	return gc;
}

/// XORs a rubber-band rectangle into a widget
void draw_rubber_band(sdpGtkWidget& Widget, k3d::rectangle Rectangle)
{
	// Sanity checks ...
	return_if_fail(Widget.Attached());

	// Get our GC ...
	GdkGC* const gc = selection_gc(Widget);

	// Normalize coordinates ...
	Rectangle.Normalize();

	// Draw the rectangle ...
	gdk_draw_rectangle(static_cast<GtkWidget*>(Widget)->window, gc, 0, int(Rectangle.Left()), int(Rectangle.Top()), int(Rectangle.Width()), int(Rectangle.Height()));

	// Cleanup ...
	gdk_gc_destroy(gc);
}

/*

class CEditorEngine::manipulator
{
public:
	virtual ~manipulator() {}

	virtual void render(k3dIEditorEngine& Engine) = 0;

protected:
	manipulator() {}
	manipulator(const manipulator&) {}
	manipulator& operator=(const manipulator&) { return *this; }
};

namespace
{

/// Ripped-off from GLUT
void draw_box(GLfloat size, GLenum type)
{
	static GLfloat n[6][3] = {
		{-1.0, 0.0, 0.0},
		{0.0, 1.0, 0.0},
		{1.0, 0.0, 0.0},
		{0.0, -1.0, 0.0},
		{0.0, 0.0, 1.0},
		{0.0, 0.0, -1.0}
	};

	static GLint faces[6][4] = {
		{0, 1, 2, 3},
		{3, 2, 6, 7},
		{7, 6, 5, 4},
		{4, 5, 1, 0},
		{5, 6, 2, 1},
		{7, 4, 0, 3}
	};

	GLfloat v[8][3];

	v[0][0] = v[1][0] = v[2][0] = v[3][0] = -size / 2;
	v[4][0] = v[5][0] = v[6][0] = v[7][0] = size / 2;
	v[0][1] = v[1][1] = v[4][1] = v[5][1] = -size / 2;
	v[2][1] = v[3][1] = v[6][1] = v[7][1] = size / 2;
	v[0][2] = v[3][2] = v[4][2] = v[7][2] = -size / 2;
	v[1][2] = v[2][2] = v[5][2] = v[6][2] = size / 2;

	for(GLint i = 5; i >= 0; i--)
		{
			glBegin(type);
			glNormal3fv(&n[i][0]);
			glVertex3fv(&v[faces[i][0]][0]);
			glVertex3fv(&v[faces[i][1]][0]);
			glVertex3fv(&v[faces[i][2]][0]);
			glVertex3fv(&v[faces[i][3]][0]);
			glEnd();
		}
}

class handle :
	public k3d::ihandle,
	public k3d::mouse_event_observer
{
	typedef k3d::mouse_event_observer base;

public:
	virtual ~handle()
	{
		if(m_document.mouse_focus() == this)
			{
				m_document.set_mouse_focus(0);
				m_released_signal.emit();
			}
	}

	void grab()
	{
		m_document.set_mouse_focus(this);
		m_grabbed_signal.emit();
	}

	bool OnLButtonUp(k3dICamera& Camera, const k3d::key_modifiers Modifiers, const k3d::vector2 Current)
	{
		release();
		return true;
	}

	bool OnLButtonEndDrag(k3dICamera& Camera, const k3d::key_modifiers Modifiers, const k3d::vector2 Current, const k3d::vector2 Last, const k3d::vector2 Start)
	{
		return true;
	}

	grabbed_signal_t& grabbed_signal()
	{
		return m_grabbed_signal;
	}

	released_signal_t& released_signal()
	{
		return m_released_signal;
	}

	virtual void render(k3dIEditorEngine* Engine) = 0;

protected:
	handle(k3d::idocument& Document, const std::string& Message) :
		base(Message),
		m_document(Document)
	{
	}

	void release()
	{
		m_document.set_mouse_focus(0);
		m_released_signal.emit();
		k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
	}

private:
	k3d::idocument& m_document;
	grabbed_signal_t m_grabbed_signal;
	released_signal_t m_released_signal;

	handle(const handle&);
	handle& operator=(const handle&);
};

class position_handle_1d :
	public handle
{
	typedef handle base;

public:
	position_handle_1d(k3d::iobject& Object, const std::string& Message, const k3d::angle_axis Direction, const double Radius, const k3d::color Color) :
		base(Object.m_document, Message),
		m_object(Object),
		m_document(Object.m_document),
		m_direction(Direction),
		m_radius(Radius),
		m_quadrics(gluNewQuadric())
	{
		gluQuadricDrawStyle(m_quadrics, GLenum(GLU_FILL));
		gluQuadricNormals(m_quadrics, GLenum(GLU_SMOOTH));

		m_color.push_back(Color.red);
		m_color.push_back(Color.green);
		m_color.push_back(Color.blue);
		m_color.push_back(1);

		m_emissive_color.push_back(Color.red * 0.5);
		m_emissive_color.push_back(Color.green * 0.5);
		m_emissive_color.push_back(Color.blue * 0.5);
		m_emissive_color.push_back(1);
	}

	~position_handle_1d()
	{
		if(m_quadrics)
			gluDeleteQuadric(m_quadrics);
	}

	bool OnLButtonDrag(k3dICamera& Camera, const k3d::key_modifiers Modifiers, const k3d::vector2 Current, const k3d::vector2 Last, const k3d::vector2 Start)
	{
		// Sanity checks ...
		return_val_if_fail(Camera.Width(), false);
		return_val_if_fail(Camera.Height(), false);

		// If the mouse didn't really move, we're done ...
		if(Current == Last)
			return true;

		// Turn mouse coordinates into a vector in world coordinates ...
		k3d::vector3 current_origin, current_direction;
		k3d::mouse_to_world_ray(Camera, Current, current_origin, current_direction);

		k3d::vector3 last_origin, last_direction;
		k3d::mouse_to_world_ray(Camera, Last, last_origin, last_direction);

		// Get the object origin in world coordinates ...
		const k3d::vector3 world_position = k3d::world_position(m_object);
		// Create a screen-aligned plane through the object's origin ...
		const k3d::plane screen_plane(k3d::camera_to_world_matrix(Camera) * k3d::vector3(0, 0, 1), world_position);

		// Calculate intersections with the screen plane ...
		k3d::vector3 current_intersection;
		k3d::PlaneLineIntersection(screen_plane, current_origin, current_direction, current_intersection);

		k3d::vector3 last_intersection;
		k3d::PlaneLineIntersection(screen_plane, last_origin, last_direction, last_intersection);

		// Calculate the delta along the screen plane ...
		const k3d::vector3 screen_delta = current_intersection - last_intersection;

		// Calculate a direction vector for this handle ...
		const k3d::vector3 direction_vector = k3d::rotation3D(m_direction) * k3d::vector3(0, 0, 1);

		// Use the scalar projection of the screen delta onto the direction vector to calculate our final position
		const k3d::vector3 new_position = world_position + (direction_vector * (screen_delta * direction_vector));

		// Make it happen!
		assert_warning(k3d::set_property_value(m_object, "position", new_position));

		k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
		return true;
	}

	void render(k3dIEditorEngine* Engine)
	{
		sdpgl::store_attributes attributes;

		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		k3dPushOpenGLMatrix(k3d::object_to_world_matrix(m_object));
		glRotated(k3d::degrees(m_direction.angle), m_direction.axis[0], m_direction.axis[1], m_direction.axis[2]);

		// Draw the arrow ...
		glEnable(GL_LIGHTING);
		static GLfloat white[4] = { 1, 1, 1, 1 };
		static GLfloat black[4] = { 0, 0, 0, 1 };
		glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, black);
		glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, &m_color[0]);
		glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, white);
		glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, &m_emissive_color[0]);

		k3d::glPushName(this);

		gluCylinder(m_quadrics, m_radius * 0.025, m_radius * 0.025, m_radius, 8, 1);

		// Draw the head of the arrow ...
		glPushMatrix();
		glTranslated(0, 0, m_radius);
		gluCylinder(m_quadrics, m_radius * 0.05, m_radius * 0.025, 0, 8, 1);
		gluCylinder(m_quadrics, m_radius * 0.05, m_radius * 0.001, m_radius * 0.15, 8, 1);
		glPopMatrix();

		k3d::glPopName();

		glPopMatrix();
	}

private:
	k3d::iobject& m_object;
	k3d::idocument& m_document;
	const k3d::angle_axis m_direction;
	const double m_radius;

	std::vector<GLfloat> m_color;
	std::vector<GLfloat> m_emissive_color;

	GLUquadricObj*	const m_quadrics;
};

class position_handle_2d :
	public handle
{
	typedef handle base;

public:
	position_handle_2d(k3d::iobject& Object, const std::string& Message, const double Radius) :
		base(Object.m_document, Message),
		m_object(Object),
		m_document(Object.m_document),
		m_radius(Radius),
		m_quadrics(gluNewQuadric())
	{
		gluQuadricDrawStyle(m_quadrics, GLenum(GLU_FILL));
		gluQuadricNormals(m_quadrics, GLenum(GLU_SMOOTH));
	}

	~position_handle_2d()
	{
		if(m_quadrics)
			gluDeleteQuadric(m_quadrics);
	}

	bool OnLButtonDrag(k3dICamera& Camera, const k3d::key_modifiers Modifiers, const k3d::vector2 Current, const k3d::vector2 Last, const k3d::vector2 Start)
	{
		// Sanity checks ...
		return_val_if_fail(Camera.Width(), false);
		return_val_if_fail(Camera.Height(), false);

		// If the mouse didn't really move, we're done ...
		if(Current == Last)
			return true;

		// Turn mouse coordinates into a vector in world coordinates ...
		k3d::vector3 current_origin, current_direction;
		k3d::mouse_to_world_ray(Camera, Current, current_origin, current_direction);

		k3d::vector3 last_origin, last_direction;
		k3d::mouse_to_world_ray(Camera, Last, last_origin, last_direction);

		// Get the object origin in world coordinates ...
		const k3d::vector3 world_position = k3d::world_position(m_object);
		// Create a screen-aligned plane through the object's origin ...
		const k3d::plane screen_plane(k3d::camera_to_world_matrix(Camera) * k3d::vector3(0, 0, 1), world_position);

		// Calculate intersections with the screen plane ...
		k3d::vector3 current_intersection;
		k3d::PlaneLineIntersection(screen_plane, current_origin, current_direction, current_intersection);

		k3d::vector3 last_intersection;
		k3d::PlaneLineIntersection(screen_plane, last_origin, last_direction, last_intersection);

		// Calculate the delta along the screen plane ...
		const k3d::vector3 screen_delta = current_intersection - last_intersection;

		// Calculate our final position in screen space ...
		const k3d::vector3 new_position = world_position + screen_delta;

		// Make it happen!
		assert_warning(k3d::set_property_value(m_object, "position", new_position));

		k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
		return true;
	}

	void render(k3dIEditorEngine* Engine)
	{
		sdpgl::store_attributes attributes;
		glEnable(GL_LIGHTING);

		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		k3dPushOpenGLMatrix(k3d::object_to_world_matrix(m_object));

		// Draw ourselves ...
		static GLfloat color[4] = { 1, 1, 1, 1 };
		static GLfloat white[4] = { 1, 1, 1, 1 };
		static GLfloat grey[4] = { 0.5, 0.5, 0.5, 1 };
		static GLfloat black[4] = { 0, 0, 0, 1 };
		glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, black);
		glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color);
		glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, white);
		glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, grey);

		k3d::glPushName(this);
		gluSphere(m_quadrics, m_radius * 0.1, 16, 8);
		k3d::glPopName();

		glPopMatrix();
	}

private:
	k3d::iobject& m_object;
	k3d::idocument& m_document;
	const double m_radius;

	GLUquadricObj*	const m_quadrics;
};

k3d::vector3 trackball(const k3d::vector2 ScreenCoords)
{
	// Convert screen coordinates to trackball coordinates ...
	k3d::vector3 result(ScreenCoords, 0);
	const double radius = result.Length();
	if(radius < 1)
		result[2] = -sqrt(1 - radius * radius);
	else
		result.Normalize();

	return result;
}

class orientation_handle_1d :
	public handle
{
	typedef handle base;

public:
	orientation_handle_1d(k3d::iobject& Object, const std::string& Message, const k3d::angle_axis Orientation, const double Radius, const k3d::color Color) :
		base(Object.m_document, Message),
		m_object(Object),
		m_document(Object.m_document),
		m_orientation(Orientation),
		m_radius(Radius),
		m_color(Color),
		m_quadrics(gluNewQuadric())
	{
		gluQuadricDrawStyle(m_quadrics, GLenum(GLU_FILL));
	}

	~orientation_handle_1d()
	{
		if(m_quadrics)
			gluDeleteQuadric(m_quadrics);
	}

	bool OnLButtonDrag(k3dICamera& Camera, const k3d::key_modifiers Modifiers, const k3d::vector2 Current, const k3d::vector2 Last, const k3d::vector2 Start)
	{
assert_warning(0);
		// Sanity checks ...
		return_val_if_fail(Camera.Width(), true);
		return_val_if_fail(Camera.Height(), true);

		const k3d::vector2 CurrentMouse(Current);
		const k3d::vector2 LastMouse(Last);

		// If the mouse didn't really move, we're done ...
		if(LastMouse == CurrentMouse)
			return true;

		k3d::vector2 lastmouse, currentmouse;
		const double width = Camera.Width();
		const double height = Camera.Height();

		if(width > height) {
			lastmouse = k3d::transform_mouse_coordinates(Camera, LastMouse,
								-width / height, width / height, 1, -1, 2);
			currentmouse = k3d::transform_mouse_coordinates(Camera, CurrentMouse,
							-width / height, width / height, 1, -1, 2);
		} else {
			lastmouse = k3d::transform_mouse_coordinates(Camera, LastMouse, -1, 1,
							height / width, -height / width, 2);
			currentmouse = k3d::transform_mouse_coordinates(Camera, CurrentMouse, -
							1, 1, height / width, -height / width, 2);
		}

		// Convert screen coordinates to "trackball" coordinates ...
		static const double trackballsize = 1 / 0.9;
		k3d::vector3 last = trackball(lastmouse * trackballsize);
		k3d::vector3 current = trackball(currentmouse * trackballsize);

		// Transform "trackball" coordinates to model coordinates ...
		k3d::iobject* parent = m_object.m_document.hierarchy().ObjectParent(&m_object);
		k3d::matrix4 cameratoworld = parent ? k3d::camera_to_object_matrix(Camera, *parent) : k3d::camera_to_world_matrix(Camera);
		last = (cameratoworld * last) - (cameratoworld * k3d::vector3(0, 0, 0));
		current = (cameratoworld * current) - (cameratoworld * k3d::vector3(0, 0, 0));

		// Convert the change in mouse position to a quaternion ...
		k3d::quaternion delta(last * current, last ^ current);

		if(Modifiers.control()) {
			double w = k3d::Clamp(-1.0, delta.w, 1.0);
			double phi = acos(w);

			switch(Camera.ActiveAxis()) {
			case k3d::X:
				if(delta.v.n[0] >= 0)
					delta.v = k3d::vector3(sin(phi), 0, 0);
				else
					delta.v = k3d::vector3(-sin(phi), 0, 0);
				break;
			case k3d::Y:
				if(delta.v.n[1] >= 0)
					delta.v = k3d::vector3(0, sin(phi), 0);
				else
					delta.v = k3d::vector3(0, -sin(phi), 0);
				break;
			case k3d::Z:
				if(delta.v.n[2] >= 0)
					delta.v = k3d::vector3(0, 0, sin(phi));
				else
					delta.v = k3d::vector3(0, 0, -sin(phi));
				break;
			}
		}

		k3d::itransform* xform = dynamic_cast<k3d::itransform*>(&m_object);
		if(xform) {
			const k3d::angle_axis currentrotation = xform->orientation();
				xform->set_orientation(k3d::angle_axis(delta * k3d::quaternion(currentrotation)));
		}

		k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
		return true;
	}

	void render(k3dIEditorEngine* Engine)
	{
		sdpgl::store_attributes attributes;
		glDisable(GL_LIGHTING);

		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		k3dPushOpenGLMatrix(k3d::object_to_world_matrix(m_object));
		glRotated(k3d::degrees(m_orientation.angle), m_orientation.axis[0], m_orientation.axis[1], m_orientation.axis[2]);

		// Draw the disk ...
		glColor3d(m_color.red, m_color.green, m_color.blue);

		k3d::glPushName(this);
		gluCylinder(m_quadrics, m_radius * 1.1, m_radius * 0.9, 0, 36, 1);
		k3d::glPopName();

		glPopMatrix();
	}

private:
	k3d::iobject& m_object;
	k3d::idocument& m_document;
	const k3d::angle_axis m_orientation;
	const double m_radius;
	const k3d::color m_color;

	GLUquadricObj*	const m_quadrics;
};

class scale_handle_1d :
	public handle
{
	typedef handle base;

public:
	scale_handle_1d(k3d::iobject& Object, const std::string& Message, const k3d::angle_axis Direction, const double Radius, const k3d::color Color) :
		base(Object.m_document, Message),
		m_object(Object),
		m_document(Object.m_document),
		m_direction(Direction),
		m_radius(Radius),
		m_quadrics(gluNewQuadric())
	{
		gluQuadricDrawStyle(m_quadrics, GLenum(GLU_FILL));
		gluQuadricNormals(m_quadrics, GLenum(GLU_SMOOTH));

		m_color.push_back(Color.red);
		m_color.push_back(Color.green);
		m_color.push_back(Color.blue);
		m_color.push_back(1);

		m_emissive_color.push_back(Color.red * 0.5);
		m_emissive_color.push_back(Color.green * 0.5);
		m_emissive_color.push_back(Color.blue * 0.5);
		m_emissive_color.push_back(1);
	}

	~scale_handle_1d()
	{
		if(m_quadrics)
			gluDeleteQuadric(m_quadrics);
	}

	bool OnLButtonDrag(k3dICamera& Camera, const k3d::key_modifiers Modifiers, const k3d::vector2 Current, const k3d::vector2 Last, const k3d::vector2 Start)
	{
		// Sanity checks ...
		return_val_if_fail(Camera.Width(), true);
		return_val_if_fail(Camera.Height(), true);

		const k3d::vector2 CurrentMouse(Current);
		const k3d::vector2 LastMouse(Last);

		// If the mouse didn't really move, we're done ...
		if(LastMouse == CurrentMouse)
			return true;

		// Convert mouse coordinates to screen coordinates ...
		k3d::vector2 lastmouse, currentmouse;
		const double width = Camera.Width();
		const double height = Camera.Height();
		if(width > height) {
			lastmouse = k3d::transform_mouse_coordinates(Camera, LastMouse, -width / height, width / height, 1, -1);
			currentmouse = k3d::transform_mouse_coordinates(Camera, CurrentMouse, -width / height, width / height, 1, -1);
		} else {
			lastmouse = k3d::transform_mouse_coordinates(Camera, LastMouse, -1, 1, height / width, -height / width);
			currentmouse = k3d::transform_mouse_coordinates(Camera, CurrentMouse, -1, 1, height / width, -height / width);
		}

		k3d::vector2 deltamouse = currentmouse - lastmouse;

		k3d::vector3 delta;
		if(Modifiers.control()) {
			switch(Camera.ActiveAxis()) {
			case k3d::X:
				delta = k3d::vector3(deltamouse[0], 0, 0);
				break;
			case k3d::Y:
				delta = k3d::vector3(0, deltamouse[1], 0);
				break;
			case k3d::Z:
				delta = k3d::vector3(0, 0, deltamouse[0]);
				break;
			default:
				return_val_if_fail(0, true);
			}
		}else{
			double length = deltamouse.Length();
			double angle = k3d::degrees(deltamouse.Angle());
			if(angle < -45.0  || angle > 135.0) {
				length = -length;
			}
			delta = k3d::vector3(length, length, length);
		}

assert_warning(0);
		k3d::itransform* const xform = dynamic_cast<k3d::itransform*>(&m_object);
		if(xform)
			xform->set_scale(xform->scale() + delta);

		k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
		return true;
	}

	void render(k3dIEditorEngine* Engine)
	{
		sdpgl::store_attributes attributes;

		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		k3dPushOpenGLMatrix(k3d::object_to_world_matrix(m_object));
		glRotated(k3d::degrees(m_direction.angle), m_direction.axis[0], m_direction.axis[1], m_direction.axis[2]);

		// Draw the arrow ...
		glEnable(GL_LIGHTING);
		static GLfloat white[4] = { 1, 1, 1, 1 };
		static GLfloat black[4] = { 0, 0, 0, 1 };
		glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, black);
		glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, &m_color[0]);
		glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, white);
		glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, &m_emissive_color[0]);

		k3d::glPushName(this);

		gluCylinder(m_quadrics, m_radius * 0.025, m_radius * 0.025, m_radius, 8, 1);

		// Draw the head of the arrow ...
		glPushMatrix();
		glTranslated(0, 0, m_radius);
		draw_box(m_radius * 0.15, GL_QUADS);
		glPopMatrix();

		k3d::glPopName();

		glPopMatrix();
	}

private:
	k3d::iobject& m_object;
	k3d::idocument& m_document;
	const k3d::angle_axis m_direction;
	const double m_radius;

	std::vector<GLfloat> m_color;
	std::vector<GLfloat> m_emissive_color;

	GLUquadricObj*	const m_quadrics;
};

class scale_handle_3d :
	public handle
{
	typedef handle base;

public:
	scale_handle_3d(k3d::iobject& Object, const std::string& Message, const double Radius) :
		base(Object.m_document, Message),
		m_object(Object),
		m_document(Object.m_document),
		m_radius(Radius)
	{
	}

	bool OnLButtonDrag(k3dICamera& Camera, const k3d::key_modifiers Modifiers, const k3d::vector2 Current, const k3d::vector2 Last, const k3d::vector2 Start)
	{
		// Sanity checks ...
		return_val_if_fail(Camera.Width(), true);
		return_val_if_fail(Camera.Height(), true);

		const k3d::vector2 CurrentMouse(Current);
		const k3d::vector2 LastMouse(Last);

		// If the mouse didn't really move, we're done ...
		if(LastMouse == CurrentMouse)
			return true;

		// Convert mouse coordinates to screen coordinates ...
		k3d::vector2 lastmouse, currentmouse;
		const double width = Camera.Width();
		const double height = Camera.Height();
		if(width > height) {
			lastmouse = k3d::transform_mouse_coordinates(Camera, LastMouse, -width / height, width / height, 1, -1);
			currentmouse = k3d::transform_mouse_coordinates(Camera, CurrentMouse, -width / height, width / height, 1, -1);
		} else {
			lastmouse = k3d::transform_mouse_coordinates(Camera, LastMouse, -1, 1, height / width, -height / width);
			currentmouse = k3d::transform_mouse_coordinates(Camera, CurrentMouse, -1, 1, height / width, -height / width);
		}

		const k3d::vector2 deltamouse = currentmouse - lastmouse;
		const double length = deltamouse.Length();
		const double angle = k3d::degrees(deltamouse.Angle());


		const double zoomfactor = (angle < -45.0  || angle > 135.0) ? pow(0.5, length) : pow(2.0, length);

assert_warning(0);
		k3d::itransform* const xform = dynamic_cast<k3d::itransform*>(&m_object);
		if(xform)
			xform->set_scale(xform->scale() * zoomfactor);

		k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
		return true;
	}

	void render(k3dIEditorEngine* Engine)
	{
		sdpgl::store_attributes attributes;

		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		k3dPushOpenGLMatrix(k3d::object_to_world_matrix(m_object));

		glEnable(GL_LIGHTING);
		static GLfloat white[4] = { 1, 1, 1, 1 };
		static GLfloat grey[4] = { 0.5, 0.5, 0.5, 1 };
		static GLfloat black[4] = { 0, 0, 0, 1 };
		glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, black);
		glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, white);
		glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, white);
		glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, grey);

		k3d::glPushName(this);
		draw_box(m_radius * 0.25, GL_QUADS);
		k3d::glPopName();

		glPopMatrix();
	}

private:
	k3d::iobject& m_object;
	k3d::idocument& m_document;
	const double m_radius;
};

class basic_manipulator :
	public CEditorEngine::manipulator,
	public SigC::Object
{
public:
	basic_manipulator() :
		m_active_handle(0)
	{
	}

protected:
	void register_handle(handle* const Handle)
	{
		assert(Handle);

		m_handles.push_back(Handle);
		Handle->grabbed_signal().connect(SigC::bind(SigC::slot(*this, &basic_manipulator::on_grab_handle), Handle));
		Handle->released_signal().connect(SigC::bind(SigC::slot(*this, &basic_manipulator::on_grab_handle), static_cast<handle*>(0)));
	}

	~basic_manipulator()
	{
		std::for_each(m_handles.begin(), m_handles.end(), k3d::delete_object());
	}

private:
	void on_grab_handle(handle* const Handle)
	{
		m_active_handle = Handle;
	}

	void render(k3dIEditorEngine& Engine)
	{
		if(m_active_handle)
			m_active_handle->render(&Engine);
		else
			std::for_each(m_handles.begin(), m_handles.end(), std::bind2nd(std::mem_fun(&handle::render), &Engine));
	}

	typedef std::vector<handle*> handles_t;
	handles_t m_handles;

	handle* m_active_handle;
};

class position_manipulator :
	public basic_manipulator
{
public:
	position_manipulator(k3d::iobject& Object, const k3d::bounding_box Bounds)
	{
		const double radius = Bounds.max_distance() * 1.2;

		register_handle(new position_handle_1d(Object, "LMB drag along world X axis", k3d::angle_axis(k3d::radians(90.0), k3d::vector3(0, 1, 0)), radius, k3d::color(1, 0, 0)));
		register_handle(new position_handle_1d(Object, "LMB drag along world X axis", k3d::angle_axis(k3d::radians(-90.0), k3d::vector3(0, 1, 0)), radius, k3d::color(1, 0, 0)));
		register_handle(new position_handle_1d(Object, "LMB drag along world Y axis", k3d::angle_axis(k3d::radians(-90.0), k3d::vector3(1, 0, 0)), radius, k3d::color(1, 1, 0)));
		register_handle(new position_handle_1d(Object, "LMB drag along world Y axis", k3d::angle_axis(k3d::radians(90.0), k3d::vector3(1, 0, 0)), radius, k3d::color(1, 1, 0)));
		register_handle(new position_handle_1d(Object, "LMB drag along world Z axis", k3d::angle_axis(k3d::radians(0.0), k3d::vector3(0, 1, 0)), radius, k3d::color(0, 1, 0)));
		register_handle(new position_handle_1d(Object, "LMB drag along world Z axis", k3d::angle_axis(k3d::radians(180.0), k3d::vector3(0, 1, 0)), radius, k3d::color(0, 1, 0)));
		register_handle(new position_handle_2d(Object, "LMB drag in screen space", radius));
	}
};

class orientation_manipulator :
	public basic_manipulator
{
public:
	orientation_manipulator(k3d::iobject& Object, const k3d::bounding_box Bounds)
	{
		const double radius = Bounds.max_distance();

		register_handle(new orientation_handle_1d(Object, "LMB drag around world X axis", k3d::angle_axis(k3d::radians(90.0), k3d::vector3(0, 1, 0)), radius, k3d::color(1, 0, 0)));
		register_handle(new orientation_handle_1d(Object, "LMB drag around world Y axis", k3d::angle_axis(k3d::radians(90.0), k3d::vector3(1, 0, 0)), radius, k3d::color(1, 1, 0)));
		register_handle(new orientation_handle_1d(Object, "LMB drag around world Z axis", k3d::angle_axis(k3d::radians(0.0), k3d::vector3(0, 0, 1)), radius, k3d::color(0, 1, 0)));
	}
};

class scale_manipulator :
	public basic_manipulator
{
public:
	scale_manipulator(k3d::iobject& Object, const k3d::bounding_box Bounds)
	{
		const double radius = Bounds.max_distance() * 1.2;

		register_handle(new scale_handle_1d(Object, "LMB drag to scale along local X axis", k3d::angle_axis(k3d::radians(90.0), k3d::vector3(0, 1, 0)), radius, k3d::color(1, 0, 0)));
		register_handle(new scale_handle_1d(Object, "LMB drag to scale along local X axis", k3d::angle_axis(k3d::radians(-90.0), k3d::vector3(0, 1, 0)), radius, k3d::color(1, 0, 0)));
		register_handle(new scale_handle_1d(Object, "LMB drag to scale along local Y axis", k3d::angle_axis(k3d::radians(-90.0), k3d::vector3(1, 0, 0)), radius, k3d::color(1, 1, 0)));
		register_handle(new scale_handle_1d(Object, "LMB drag to scale along local Y axis", k3d::angle_axis(k3d::radians(90.0), k3d::vector3(1, 0, 0)), radius, k3d::color(1, 1, 0)));
		register_handle(new scale_handle_1d(Object, "LMB drag to scale along local Z axis", k3d::angle_axis(k3d::radians(0.0), k3d::vector3(0, 1, 0)), radius, k3d::color(0, 1, 0)));
		register_handle(new scale_handle_1d(Object, "LMB drag to scale along local Z axis", k3d::angle_axis(k3d::radians(180.0), k3d::vector3(0, 1, 0)), radius, k3d::color(0, 1, 0)));
		register_handle(new scale_handle_3d(Object, "LMB drag to scale", radius));
	}
};
*/

} // namespace

namespace k3d
{

namespace viewport
{

////////////////////////////////////////////////////////////////////////////
// control::implementation

class control::implementation :
	public k3dDialog,
	public mouse_event_observer,
	public property_collection,
	private sdpGtkMouseInput
{
	typedef k3dDialog base;

public:
	implementation(k3d::idocument& Document, k3d::iunknown& ParentCommandNode, const sdpGtkWidget& Owner) :
		base(&ParentCommandNode, "viewport_control", 0),
		mouse_event_observer("LMB drag to select, RMB drag to pan/tilt"),
		property_collection(Document.dag()),
		m_document(Document),
		m_viewport(0),
		m_last_pick(0),
		m_active_axis(k3d::init_name("active_axis") + k3d::init_description("Active Axis [enumeration]") + k3d::init_value(k3d::Z) + k3d::init_document(Document) + k3d::init_enumeration(k3d::axis_values())),
		m_navigation_mode(k3d::init_name("navigation_mode") + k3d::init_description("Navigation Mode [enumeration]") + k3d::init_value(MODELING) + k3d::init_document(Document) + k3d::init_enumeration(navigation_mode_values())),
		m_select_objects(k3d::init_name("select_objects") + k3d::init_description("Select Objects [boolean]") + k3d::init_value(true) + k3d::init_document(Document)),
		m_select_meshes(k3d::init_name("select_meshes") + k3d::init_description("Select Meshes [boolean]") + k3d::init_value(false) + k3d::init_document(Document)),
		m_select_edges(k3d::init_name("select_edges") + k3d::init_description("Select Edges [boolean]") + k3d::init_value(false) + k3d::init_document(Document)),
		m_select_faces(k3d::init_name("select_faces") + k3d::init_description("Select Faces [boolean]") + k3d::init_value(false) + k3d::init_document(Document)),
		m_select_curves(k3d::init_name("select_curves") + k3d::init_description("Select Curves [boolean]") + k3d::init_value(false) + k3d::init_document(Document)),
		m_select_patches(k3d::init_name("select_patches") + k3d::init_description("Select Patches [boolean]") + k3d::init_value(false) + k3d::init_document(Document)),
		m_select_point_groups(k3d::init_name("select_point_groups") + k3d::init_description("Select Point Groups [boolean]") + k3d::init_value(false) + k3d::init_document(Document)),
		m_select_points(k3d::init_name("select_points") + k3d::init_description("Select Points [boolean]") + k3d::init_value(false) + k3d::init_document(Document)),
		m_context_menu(Document)
	{
		// Sanity checks ...
		assert_warning(Owner.Attached());

		MapEvent("configure-event", "configure_1", false, Owner, true);

		// Create and load our UI template ...
		std::istringstream uitemplate(
			"<gtkml>"
				"<eventbox>"
					"<event signal=\"size-allocate\" name=\"size_allocate\"/>"
					"<aspectframe name=\"frame\" shadowtype=\"none\"/>"
				"</eventbox>"
			"</gtkml>\n");

		return_if_fail(load_gtkml(uitemplate, "viewport control builtin template", *this));

		// Attach ourselves to the parent widget ...
		sdpGtkContainer(GTK_CONTAINER(Owner.Object())).Attach(RootWidget());

		// Create the OpenGL drawing area ...
		sdpGtkAspectFrame aspect_frame(AspectFrame(control_frame));
		
		if(!m_drawing_area.Create(aspect_frame, true, true, 8, 8, 8, 16))
			{
				if(!m_drawing_area.Create(aspect_frame, true, true, 5, 5, 5, 16))
					{
						if(!m_drawing_area.Create(aspect_frame, false, false, 4, 4, 4, 16))
							{
								std::cerr << __PRETTY_FUNCTION__ << ": Could not find usable OpenGL visual" << std::endl;
							}
					}
			}

		if(m_drawing_area.Initialized())
			{
				return_if_fail(m_drawing_area.InitializeFont());

				// Setup some events ...
				MapEvent("expose-event", "expose_drawing_area", false, m_drawing_area, true);
				MapEvent("motion-notify-event", "mousemove", false, m_drawing_area, true);
				MapEvent("button-press-event", "buttondown", false, m_drawing_area, true);
				MapEvent("button-release-event", "buttonup", false, m_drawing_area, true);
			}

		register_property(m_active_axis);
		register_property(m_navigation_mode);
		register_property(m_select_objects);
		register_property(m_select_meshes);
		register_property(m_select_edges);
		register_property(m_select_faces);
		register_property(m_select_curves);
		register_property(m_select_patches);
		register_property(m_select_point_groups);
		register_property(m_select_points);
		
		Show();
	
		gdk_window_set_cursor(static_cast<GtkWidget*>(AspectFrame(control_frame))->window, gdk_cursor_new(GDK_PLUS));
	}

	~implementation()
	{
		// No more events from this point forward ...
		DisconnectAllEvents();

		// Clean-up the GTK+ tree ...
		if(Root())
			RootWidget().Destroy();

		// Get rid of our widget pointers ...
		Clear();
	}

	/// Attaches this control to a viewport (may be called zero-to-many times)
	bool attach(k3d::iviewport& Viewport)
	{
		m_viewport_deleted_connection.disconnect();
		m_viewport_host_changed_connection.disconnect();
		m_viewport_redraw_request_connection.disconnect();
		m_viewport_aspect_ratio_changed_connection.disconnect();
		
		m_viewport = &Viewport;
		
		k3d::iobject* const viewport_object = dynamic_cast<k3d::iobject*>(&Viewport);
		if(viewport_object)
			m_viewport_deleted_connection = viewport_object->deleted_signal().connect(SigC::slot(*this, &implementation::on_viewport_deleted));
		m_viewport_host_changed_connection = Viewport.host_changed_signal().connect(SigC::slot(*this, &implementation::on_viewport_host_changed));
		m_viewport_redraw_request_connection = Viewport.redraw_request_signal().connect(SigC::slot(*this, &implementation::on_redraw_request));
		m_viewport_aspect_ratio_changed_connection = Viewport.aspect_ratio_changed_signal().connect(SigC::slot(*this, &implementation::on_viewport_aspect_ratio_changed));
	
		m_viewport_changed_signal.emit(m_viewport);

		on_size_allocate();
		on_redraw_request(k3d::iviewport::ASYNCHRONOUS);

		return true;
	}

	void on_viewport_deleted()
	{
		m_viewport_deleted_connection.disconnect();
		m_viewport_host_changed_connection.disconnect();
		m_viewport_redraw_request_connection.disconnect();
		m_viewport_aspect_ratio_changed_connection.disconnect();
		
		m_viewport = 0;

		m_viewport_changed_signal.emit(m_viewport);
		
		on_size_allocate();		
		on_redraw_request(k3d::iviewport::SYNCHRONOUS);
	}

	void on_viewport_host_changed()
	{
		m_viewport_changed_signal.emit(m_viewport);
	}
	
	void on_viewport_aspect_ratio_changed()
	{
		// Update our aspect ratio to match ...
		on_size_allocate();
	}

	k3d::viewport::control::viewport_changed_signal_t& viewport_changed_signal()
	{
		return m_viewport_changed_signal;
	}
	
	/// Signal for notifying observers that the viewport has changed (note: the new viewport could be NULL)
	k3d::viewport::control::viewport_changed_signal_t m_viewport_changed_signal;

	bool render_preview()
	{
		/** \todo Figure out something better to do than just failing, here */
		return false;
	}
	
	bool render_frame(const boost::filesystem::path& OutputImage, const bool ViewCompletedImage)
	{
		return save_frame(OutputImage, ViewCompletedImage);
	}
	
	bool render_animation(const boost::filesystem::path& OutputImages, const bool ViewCompletedImages)
	{
		// Sanity checks ...
		return_val_if_fail(!OutputImages.empty(), false);

		// Ensure that the document has animation capabilities, first ...
		k3d::iproperty* const start_time_property = k3d::get_start_time(m_document);
		k3d::iproperty* const end_time_property = k3d::get_end_time(m_document);
		k3d::iproperty* const frame_rate_property = k3d::get_frame_rate(m_document);
		k3d::iwritable_property* const time_property = dynamic_cast<k3d::iwritable_property*>(k3d::get_time(m_document));
		return_val_if_fail(start_time_property && end_time_property && frame_rate_property && time_property, false);

		// Test the output images filepath to make sure it can hold all the frames we're going to generate ...
		const double start_time = boost::any_cast<double>(k3d::get_property_value(m_document.dag(), *start_time_property));
		const double end_time = boost::any_cast<double>(k3d::get_property_value(m_document.dag(), *end_time_property));
		const double frame_rate = boost::any_cast<double>(k3d::get_property_value(m_document.dag(), *frame_rate_property));
		
		const long start_frame = static_cast<long>(k3d::round(frame_rate * start_time));
		const long end_frame = static_cast<long>(k3d::round(frame_rate * end_time));
		
		k3d::frames frames(OutputImages, start_frame, end_frame);
		return_val_if_fail(frames.max_frame() >= end_frame, false);

		// For each frame to be rendered ...
		for(long view_frame = start_frame; view_frame < end_frame; ++view_frame)
			{
				// Set the frame time ...
				time_property->set_value(view_frame / frame_rate);

				// Save that baby ...
				boost::filesystem::path destination;
				frames.frame(view_frame, destination);
				
				return_val_if_fail(save_frame(destination, ViewCompletedImages), false);
			}

		return true;
	}
	
private:
	void OnEvent(sdpGtkEvent* Event)
	{
		// Sanity checks ...
		assert_warning(Event);

		if(Event->Name() == "expose_drawing_area")
			on_redraw();
		else if(Event->Name() == "size_allocate")
			on_size_allocate();
		else if(Event->Name() == "mousemove")
			RawMouseMove(Event);
		else if(Event->Name() == "buttondown")
			RawButtonDown(Event);
		else if(Event->Name() == "buttonup")
			RawButtonUp(Event);
		else
			base::OnEvent(Event);
	}

	void on_size_allocate()
	{
		// If we're minimized, we're done ...
		const double width = RootWidget().Width();
		const double height = RootWidget().Height();
		if(!width || !height)
			return;
				
		// If we're attached to a viewport, give it a chance to override the new aspect ratio ...
		double aspect_ratio = width / height;
		if(m_viewport)
			m_viewport->constrain_screen_aspect_ratio(aspect_ratio);

		// Update our frame with the new ratio ...
		AspectFrame(control_frame).Set(0.5, 0.5, aspect_ratio, false);
	}

	bool save_frame(const boost::filesystem::path& OutputImage, const bool ViewCompletedImage)
	{
		return_val_if_fail(m_drawing_area.Initialized(), false);
		
		// Draw the image as we normally would ...
		const unsigned long width = m_drawing_area.Width();
		const unsigned long height = m_drawing_area.Height();
		return_val_if_fail(width && height, false);
		
		m_drawing_area.Begin();

		if(m_viewport)
			{
				m_viewport->redraw(width, height, m_drawing_area.FontBase());
			}
		else
			{
				// Nothing attached, so just fill the screen
				glClearColor(0.6f, 0.6f, 0.6f, 0.0f);
				glClear(GL_COLOR_BUFFER_BIT);
			}

		glFlush();	
		
		// Get the rendered image ...
		std::vector<unsigned char> image_buffer(width * height * 3, 0);
		glReadBuffer(GL_BACK);
		glPixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE);
		glPixelStorei(GL_PACK_LSB_FIRST, GL_FALSE);
		glPixelStorei(GL_PACK_ROW_LENGTH, 0);
		glPixelStorei(GL_PACK_SKIP_ROWS, 0);
		glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
		glPixelStorei(GL_PACK_ALIGNMENT, 1);
		glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0);
		glPixelZoom(1.0, -1.0);
		glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, &image_buffer[0]);
		
		m_drawing_area.SwapBuffers();
		m_drawing_area.End();

		// Save that bad-boy ...
		boost::filesystem::ofstream stream(OutputImage);
	    
		stream << "P6" << std::endl;
		stream << width << " " << height << std::endl;
		stream << "255" << std::endl;
																																											 
		// Write data ...
		for(unsigned long y = 0; y != height; ++y)
			std::copy(&image_buffer[(height - 1 - y) * width * 3], &image_buffer[(height - y) * width * 3], std::ostreambuf_iterator<char>(stream));
		
		return true;
	}

	void on_redraw()
	{
		return_if_fail(m_drawing_area.Initialized());
		
		m_drawing_area.Begin();

		if(m_viewport)
			{
				k3d::timer timer;

				m_viewport->redraw(m_drawing_area.Width(), m_drawing_area.Height(), m_drawing_area.FontBase());
				
				const double elapsed = timer.elapsed();
				if(elapsed)
					{
						glListBase(m_drawing_area.FontBase());
		
						std::stringstream buffer;
						buffer << std::fixed << std::setprecision(1) << 1.0 / elapsed << "fps" << std::endl;

						glMatrixMode(GL_PROJECTION);
						glLoadIdentity();
						glOrtho(-1, 1, -1, 1, -1, 1);
			
						glMatrixMode(GL_MODELVIEW);
						glLoadIdentity();
			
						glDisable(GL_LIGHTING);
						glDisable(GL_TEXTURE_1D);
						glDisable(GL_TEXTURE_2D);
						glDisable(GL_BLEND);

						glColor3d(0, 0, 0);
			
						glRasterPos3d(-0.95, -0.95, 0);
						glCallLists(buffer.str().size(), GL_UNSIGNED_BYTE, buffer.str().data());
					}
			}
		else
			{
				// Nothing attached, so just fill the screen
				glClearColor(0.6f, 0.6f, 0.6f, 0.0f);
				glClear(GL_COLOR_BUFFER_BIT);
			}

		glFlush();	
		m_drawing_area.SwapBuffers();
		m_drawing_area.End();
	}

	const GLint select(const k3d::rectangle& SelectionRegion)
	{
		// If width or height are zero, we're done ...
		if(!m_drawing_area.Width() || !m_drawing_area.Height())
			return 0;

		// Viewport doesn't support selection, so we're done ...
		k3d::viewport::iselection_engine* const selection_engine = dynamic_cast<k3d::viewport::iselection_engine*>(m_viewport);
		if(!selection_engine)
			return 0;

		// Set our selection buffer to a sensible minimum size ...
		if(m_selection_buffer.size() < 128)
			m_selection_buffer.resize(128);

		// Set an (arbitrary) upper-limit on how large we let the buffer grow ...
		while(m_selection_buffer.size() < 10000000)
			{
				// Draw the scene, recording hits ...
				m_drawing_area.Begin();
				glSelectBuffer(m_selection_buffer.size(), &m_selection_buffer[0]);
				glRenderMode(GL_SELECT);
				glInitNames();
				selection_engine->select(m_drawing_area.Width(), m_drawing_area.Height(), m_drawing_area.FontBase(), SelectionRegion);
				const GLint hits = glRenderMode(GL_RENDER);
				glFlush();
				m_drawing_area.End();

				// If we got a positive number of hits, we're done ...
				if(hits >= 0)
					return hits;

				// A negative number means there was buffer overflow, so try again ...
				m_selection_buffer.resize(m_selection_buffer.size() * 2);
			}

		// Ran out of buffer space!
		std::cerr << error << "Ran out of selection-buffer space" << std::endl;
		return 0;
	}

	void on_redraw_request(k3d::iviewport::redraw_type_t RedrawType)
	{
		// If this is a synchronous request, do a redraw right away ...
		if(k3d::iviewport::SYNCHRONOUS == RedrawType)
			{
				on_redraw();
				return;
			}
			
		// Otherwise, request a redraw the next time the UI catches up ...
		m_drawing_area.QueueDraw();
	}

	template<typename functor_t>
	void dispatch_mouse_event(functor_t Functor, const std::string& Command, const GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		// If there's no viewport attached, we're done ...
		if(!m_viewport)
			return;

		// If we're minimized, we're done ...
		const double width = m_drawing_area.Width();
		const double height = m_drawing_area.Height();
		if(0 == width || 0 == height)
			return;

		// Convert mouse coordinates to Normalized Device Coordinates in the range [0, 1] ...
		const k3d::vector2 current_mouse(CurrentMouse[0] / width, CurrentMouse[1] / height);
									
		k3d::imouse_event_observer::event_state state(*m_viewport, k3d::convert(Modifiers), m_active_axis.property_value());

		// Dispatch it ... see if there's an active observer, first ...
		bool result = false;
		k3d::imouse_event_observer* const focus = m_document.mouse_focus();
		if(focus)
			result = Functor(*focus, state, current_mouse);

		// If the active observer didn't handle it, give ourselves a chance ...
		if(!result)
			result = Functor(*this, state, current_mouse);

		// If the event was consumed, record it ...
		if(result)
			{
				k3d::record_command(*this, k3d::icommand_node::command_t::USER_INTERFACE, Command, k3d::to_string(k3d::convert(Modifiers)) + " " + k3d::to_string(current_mouse));
			}
	}

	template<typename functor_t>
	void dispatch_mouse_event(functor_t Functor, const std::string& Command, const GdkModifierType Modifiers, const k3d::vector2 CurrentMouse, const k3d::vector2 LastMouse, const k3d::vector2 StartMouse, const drag_type_t DragType)
	{
		// If there's no viewport attached, we're done ...
		if(!m_viewport)
			return;
			
		// If we're minimized, we're done ...
		const double width = m_drawing_area.Width();
		const double height = m_drawing_area.Height();
		if(0 == width || 0 == height)
			return;
									
		// Convert mouse coordinates to Normalized Device Coordinates in the range [0, 1] ...
		const k3d::vector2 current_mouse(CurrentMouse[0] / width, CurrentMouse[1] / height);
		const k3d::vector2 last_mouse(LastMouse[0] / width, LastMouse[1] / height);
		const k3d::vector2 start_mouse(StartMouse[0] / width, StartMouse[1] / height);
									
		k3d::imouse_event_observer::event_state state(*m_viewport, k3d::convert(Modifiers), m_active_axis.property_value());							
									
		// Dispatch it ... see if there's an active observer, first ...
		bool result = false;
		k3d::imouse_event_observer* const focus = m_document.mouse_focus();
		if(focus)
			result = Functor(*focus, state, current_mouse, last_mouse, start_mouse, DragType);

		// If the active observer didn't handle it, give ourselves a chance ...
		if(!result)
			result = Functor(*this, state, current_mouse, last_mouse, start_mouse, DragType);

		// If the event was consumed, record it ...
		if(result)
			{
				k3d::record_command(*this, k3d::icommand_node::command_t::USER_INTERFACE, Command, k3d::to_string(k3d::convert(Modifiers)) + " " + k3d::to_string(current_mouse) + " " + k3d::to_string(last_mouse) + " " + k3d::to_string(start_mouse));
			}
	}

	void OnMouseMove(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnMouseMove), control_mousemove, Modifiers, CurrentMouse);
	}

	void OnLButtonDown(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnLButtonDown), control_lbuttondown, Modifiers, CurrentMouse);
	}

	void OnLButtonClick(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnLButtonClick), control_lbuttonclick, Modifiers, CurrentMouse);
	}

	void OnLButtonUp(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnLButtonUp), control_lbuttonup, Modifiers, CurrentMouse);
	}

	void OnLButtonDoubleClick(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnLButtonDoubleClick), control_lbuttondoubleclick, Modifiers, CurrentMouse);
	}

	void OnLButtonStartDrag(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnLButtonDrag), control_lbuttonstartdrag, Modifiers, CurrentMouse, CurrentMouse, CurrentMouse, k3d::imouse_event_observer::START_DRAG);
	}

	void OnLButtonDrag(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse, const k3d::vector2 LastMouse, const k3d::vector2 StartMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnLButtonDrag), control_lbuttondrag, Modifiers, CurrentMouse, LastMouse, StartMouse, k3d::imouse_event_observer::CONTINUE_DRAG);
	}

	void OnLButtonEndDrag(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse, const k3d::vector2 LastMouse, const k3d::vector2 StartMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnLButtonDrag), control_lbuttonenddrag, Modifiers, CurrentMouse, LastMouse, StartMouse, k3d::imouse_event_observer::END_DRAG);
	}

	void OnMButtonDown(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnMButtonDown), control_mbuttondown, Modifiers, CurrentMouse);
	}

	void OnMButtonClick(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnMButtonClick), control_mbuttonclick, Modifiers, CurrentMouse);
	}

	void OnMButtonUp(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnMButtonUp), control_mbuttonup, Modifiers, CurrentMouse);
	}

	void OnMButtonDoubleClick(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnMButtonDoubleClick), control_mbuttondoubleclick, Modifiers, CurrentMouse);
	}

	void OnMButtonStartDrag(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnMButtonDrag), control_mbuttonstartdrag, Modifiers, CurrentMouse, CurrentMouse, CurrentMouse, k3d::imouse_event_observer::START_DRAG);
	}

	void OnMButtonDrag(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse, const k3d::vector2 LastMouse, const k3d::vector2 StartMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnMButtonDrag), control_mbuttondrag, Modifiers, CurrentMouse, LastMouse, StartMouse, k3d::imouse_event_observer::CONTINUE_DRAG);
	}

	void OnMButtonEndDrag(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse, const k3d::vector2 LastMouse, const k3d::vector2 StartMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnMButtonDrag), control_mbuttonenddrag, Modifiers, CurrentMouse, LastMouse, StartMouse, k3d::imouse_event_observer::END_DRAG);
	}

	void OnRButtonDown(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnRButtonDown), control_rbuttondown, Modifiers, CurrentMouse);
	}

	void OnRButtonClick(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnRButtonClick), control_rbuttonclick, Modifiers, CurrentMouse);
	}

	void OnRButtonUp(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnRButtonUp), control_rbuttonup, Modifiers, CurrentMouse);
	}

	void OnRButtonDoubleClick(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnRButtonDoubleClick), control_rbuttondoubleclick, Modifiers, CurrentMouse);
	}

	void OnRButtonStartDrag(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnRButtonDrag), control_rbuttonstartdrag, Modifiers, CurrentMouse, CurrentMouse, CurrentMouse, k3d::imouse_event_observer::START_DRAG);
	}

	void OnRButtonDrag(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse, const k3d::vector2 LastMouse, const k3d::vector2 StartMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnRButtonDrag), control_rbuttondrag, Modifiers, CurrentMouse, LastMouse, StartMouse, k3d::imouse_event_observer::CONTINUE_DRAG);
	}

	void OnRButtonEndDrag(GdkModifierType Modifiers, const k3d::vector2 CurrentMouse, const k3d::vector2 LastMouse, const k3d::vector2 StartMouse)
	{
		dispatch_mouse_event(boost::mem_fn(&k3d::imouse_event_observer::OnRButtonDrag), control_rbuttonenddrag, Modifiers, CurrentMouse, LastMouse, StartMouse, k3d::imouse_event_observer::END_DRAG);
	}

	bool OnLButtonDown(const k3d::imouse_event_observer::event_state& State, const k3d::vector2& Current)
	{
		k3d::viewport::iselection_engine* const selection_engine = dynamic_cast<k3d::viewport::iselection_engine*>(m_viewport);
		return_val_if_fail(selection_engine, true);
			
		m_last_pick = 0;
	
		const double sensitivity = 1;
		const k3d::rectangle selection_region(
			Current[0] * m_drawing_area.Width() - sensitivity,
			Current[0] * m_drawing_area.Width() + sensitivity,
			Current[1] * m_drawing_area.Height() - sensitivity,
			Current[1] * m_drawing_area.Height() + sensitivity);
		const unsigned int hit_count = select(selection_region);

		// We only care about the state of the shift and control keys ...
		const k3d::key_modifiers modifiers = State.modifiers & k3d::key_modifiers().set_shift().set_control();

		// Nothing selected, so deselect all
		if(0 == hit_count && !modifiers.control() && !modifiers.shift())
			{
				k3d::deselect_all(m_document);
				k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
				return true;
			}

		const hit_iterator nearest_hit = std::min_element(hit_iterator(m_selection_buffer, hit_count), hit_iterator());
		if(nearest_hit == hit_iterator())
			return true;

/*
std::cerr << __PRETTY_FUNCTION__ << std::endl;
for(hit_iterator hit(m_selection_buffer, hit_count); hit != hit_iterator(); ++hit)
	std::cerr << "   " << *hit << std::endl;

std::cerr << "nearest hit: " << *nearest_hit << std::endl;

std::cerr << std::endl;
*/

		const bool select_objects = m_select_objects.property_value();
		const bool select_meshes = m_select_meshes.property_value();
		const bool select_edges = m_select_edges.property_value();
		const bool select_faces = m_select_faces.property_value();
		const bool select_linear_curves = m_select_curves.property_value();
		const bool select_cubic_curves = m_select_curves.property_value();
		const bool select_nucurves = m_select_curves.property_value();
		const bool select_bilinear_patches = m_select_patches.property_value();
		const bool select_bicubic_patches = m_select_patches.property_value();
		const bool select_nupatches = m_select_patches.property_value();
		const bool select_point_groups = m_select_point_groups.property_value();
		const bool select_points = m_select_points.property_value();

		k3d::iselectable* selection = 0;
		for(hit_record::const_name_iterator name = (*nearest_hit).name_begin(); name != (*nearest_hit).name_end(); )
			{
				k3d::iunknown* const unknown = k3d::glGetName(name);

				// If we hit a "handle", we short-circuit the rest of the process and "grab" it ...
				k3d::ihandle* const handle = dynamic_cast<k3d::ihandle*>(unknown);
				if(handle)
					{
						handle->grab();

						k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
						return true;
					}

				// Nope, so see if we hit something selectable ...
				k3d::iselectable* const selectable = dynamic_cast<k3d::iselectable*>(unknown);
				if(!selectable)
					continue;

				// If it's an object, keep track of it for later ...
				if(dynamic_cast<k3d::iobject*>(unknown))
					m_last_pick = selectable;

				// Handle different selection modes ...
				if(select_objects && dynamic_cast<k3d::iobject*>(unknown))
					selection = selectable;
				else if(select_meshes && dynamic_cast<k3d::mesh*>(unknown))
					selection = selectable;
				else if(select_point_groups && dynamic_cast<k3d::point_group*>(unknown))
					selection = selectable;
				else if(select_edges && dynamic_cast<k3d::split_edge*>(unknown))
					selection = selectable;
				else if(select_faces && dynamic_cast<k3d::face*>(unknown))
					selection = selectable;
				else if(select_linear_curves && dynamic_cast<k3d::linear_curve*>(unknown))
					selection = selectable;
				else if(select_cubic_curves && dynamic_cast<k3d::cubic_curve*>(unknown))
					selection = selectable;
				else if(select_bilinear_patches && dynamic_cast<k3d::bilinear_patch*>(unknown))
					selection = selectable;
				else if(select_bicubic_patches && dynamic_cast<k3d::bicubic_patch*>(unknown))
					selection = selectable;
				else if(select_nucurves && dynamic_cast<k3d::nucurve*>(unknown))
					selection = selectable;
				else if(select_nupatches && dynamic_cast<k3d::nupatch*>(unknown))
					selection = selectable;
				else if(select_points && dynamic_cast<k3d::point*>(unknown))
					selection = selectable;
			}

		// If the control key is down, we subtract from the current selection
		if(modifiers.control())
			{
				m_last_pick = 0;
				if(selection)
					k3d::deselect(m_document, k3d::deep_selection(m_document.dag(), k3d::make_selection(*selection)));
			}
		// If the shift key is down, we add to the current selection
		else if(modifiers.shift())
			{
				if(selection)
					k3d::select(m_document, k3d::deep_selection(m_document.dag(), k3d::make_selection(*selection)));
			}
		// Otherwise, we replace the current selection
		else
			{
				k3d::deselect_all(m_document);
				if(selection)
					k3d::select(m_document, k3d::deep_selection(m_document.dag(), k3d::make_selection(*selection)));
			}

		k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
		return true;
	}

	bool OnLButtonDoubleClick(const k3d::imouse_event_observer::event_state& State, const k3d::vector2& Current)
	{
		if(!m_viewport)
			return true;

		if(!k3d::application().user_interface())
			return true;

		k3d::iselection::selection_t selection;
		if(m_last_pick)
			selection.insert(m_last_pick);

		if(selection.empty())
			selection.insert(dynamic_cast<k3d::iselectable*>(m_viewport));

		for(k3d::iselection::selection_t::const_iterator selected = selection.begin(); selected != selection.end(); ++selected)
			k3d::application().user_interface()->show(**selected);

		k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);

		return true;
	}

	bool OnLButtonDrag(const k3d::imouse_event_observer::event_state& State, const k3d::vector2& Current, const k3d::vector2& Last, const k3d::vector2& Start, const drag_type_t DragType)
	{
		if(!m_viewport)
			return false;
			
		if(START_DRAG == DragType)
			{
				// We always reset the lasso ...
				m_lasso.clear();

/*
				// Caps lock on?  If so, start drawing a lasso ...
				if(State.modifiers.lock())
					{
						m_lasso.push_back(k3d::vector2(Current[0] * m_drawing_area.Width(), Current[1] * m_drawing_area.Height()));
					}
				else
*/
					{
						// Start drawing a rubber-band box ...
						m_rubber_band = k3d::rectangle(Current[0] * m_drawing_area.Width(), Current[0] * m_drawing_area.Width(), Current[1] * m_drawing_area.Height(), Current[1] * m_drawing_area.Height());
						draw_rubber_band(m_drawing_area, m_rubber_band);
					}
			}
		else if(CONTINUE_DRAG == DragType)
			{
				// If we're in lasso mode ...
				if(!m_lasso.empty())
					{
						if((Current - m_lasso[m_lasso.size()-1]).Length2() > 10)
							{
								m_lasso.push_back(k3d::vector2(Current[0] * m_drawing_area.Width(), Current[1] * m_drawing_area.Height()));

								GdkGC* const gc = selection_gc(m_drawing_area);

								gdk_draw_line(static_cast<GtkWidget*>(m_drawing_area)->window,
									gc,
									int(m_lasso[m_lasso.size() - 2][0]),
									int(m_lasso[m_lasso.size() - 2][1]),
									int(m_lasso[m_lasso.size() - 1][0]),
									int(m_lasso[m_lasso.size() - 1][1]));

								gdk_gc_destroy(gc);
							}

						return true;
					}

				// Otherwise, we're in rubber-band box mode ...
				draw_rubber_band(m_drawing_area, m_rubber_band);
				m_rubber_band = k3d::rectangle(Start[0] * m_drawing_area.Width(), Last[0] * m_drawing_area.Width(), Start[1] * m_drawing_area.Height(), Last[1] * m_drawing_area.Height());
				draw_rubber_band(m_drawing_area, m_rubber_band);
				return true;
			}
		else if(END_DRAG == DragType)
			{
				k3d::viewport::iselection_engine* const selection_engine = dynamic_cast<k3d::viewport::iselection_engine*>(m_viewport);
				return_val_if_fail(selection_engine, true);
			
				const bool select_objects = m_select_objects.property_value();
				const bool select_meshes = m_select_meshes.property_value();
				const bool select_edges = m_select_edges.property_value();
				const bool select_faces = m_select_faces.property_value();
				const bool select_linear_curves = m_select_curves.property_value();
				const bool select_cubic_curves = m_select_curves.property_value();
				const bool select_nucurves = m_select_curves.property_value();
				const bool select_bilinear_patches = m_select_patches.property_value();
				const bool select_bicubic_patches = m_select_patches.property_value();
				const bool select_nupatches = m_select_patches.property_value();
				const bool select_point_groups = m_select_point_groups.property_value();
				const bool select_points = m_select_points.property_value();

				// Keep track of what we select ...
				k3d::iselection::selection_t selection;

				// If we're in lasso-selection mode
				if(!m_lasso.empty())
					{
					}
				else
				// Rubber-band box selection mode ...
					{
						// Clean-up the rubber-band ...
						draw_rubber_band(m_drawing_area, m_rubber_band);

						const k3d::rectangle selection_region(
							std::min(Start[0], Current[0]) * m_drawing_area.Width(),
							std::max(Start[0], Current[0]) * m_drawing_area.Width(),
							std::min(Start[1], Current[1]) * m_drawing_area.Height(),
							std::max(Start[1], Current[1]) * m_drawing_area.Height());
						const unsigned int hit_count = select(selection_region);

						for(hit_iterator hit(m_selection_buffer, hit_count); hit != hit_iterator(); ++hit)
							{
								for(hit_record::const_name_iterator name = (*hit).name_begin(); name != (*hit).name_end(); )
									{
										k3d::iunknown* const unknown = k3d::glGetName(name);

										k3d::iselectable* const selectable = dynamic_cast<k3d::iselectable*>(unknown);
										if(!selectable)
											continue;

										// Handle different selection modes ...
										if(select_objects && dynamic_cast<k3d::iobject*>(unknown))
											selection.insert(selectable);
										else if(select_meshes && dynamic_cast<k3d::mesh*>(unknown))
											selection.insert(selectable);
										else if(select_point_groups && dynamic_cast<k3d::point_group*>(unknown))
											selection.insert(selectable);
										else if(select_edges && dynamic_cast<k3d::split_edge*>(unknown))
											selection.insert(selectable);
										else if(select_faces && dynamic_cast<k3d::face*>(unknown))
											selection.insert(selectable);
										else if(select_linear_curves && dynamic_cast<k3d::linear_curve*>(unknown))
											selection.insert(selectable);
										else if(select_cubic_curves && dynamic_cast<k3d::cubic_curve*>(unknown))
											selection.insert(selectable);
										else if(select_bilinear_patches && dynamic_cast<k3d::bilinear_patch*>(unknown))
											selection.insert(selectable);
										else if(select_bicubic_patches && dynamic_cast<k3d::bicubic_patch*>(unknown))
											selection.insert(selectable);
										else if(select_nucurves && dynamic_cast<k3d::nucurve*>(unknown))
											selection.insert(selectable);
										else if(select_nupatches && dynamic_cast<k3d::nupatch*>(unknown))
											selection.insert(selectable);
										else if(select_points && dynamic_cast<k3d::point*>(unknown))
											selection.insert(selectable);
									}
							}
					}

				// Nothing selected?  We're done ...
				if(selection.empty())
					return true;

				// We only care about the state of the shift and control keys ...
				const k3d::key_modifiers modifiers = State.modifiers & k3d::key_modifiers().set_shift().set_control();

				// If the control key is down, we subtract from the current selection
				if(modifiers.control())
					{
						k3d::deselect(m_document, k3d::deep_selection(m_document.dag(), selection));
					}
				// If the shift key is down, we add to the current selection
				else if(modifiers.shift())
					{
						k3d::select(m_document, k3d::deep_selection(m_document.dag(), selection));
					}
				// Otherwise, we replace the current selection
				else
					{
						k3d::deselect_all(m_document);
						k3d::select(m_document, k3d::deep_selection(m_document.dag(), selection));
					}

				k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
				return true;
			}

		return true;
	}

	// Change focal length
	bool OnMButtonDrag(const k3d::imouse_event_observer::event_state& State, const k3d::vector2& Current, const k3d::vector2& Last, const k3d::vector2& Start, const drag_type_t DragType)
	{
		if(!m_viewport)
			return false;

		const double sensitivity = 4.0;
		const double zoom_factor = (Current[1] < Last[1]) ? std::pow(sensitivity, Current[1] - Last[1]) : std::pow(1 / sensitivity, Last[1] - Current[1]);

		k3d::iprojection& projection = m_viewport->projection();

		k3d::iperspective* const perspective = dynamic_cast<k3d::iperspective*>(&projection);
		if(perspective)
			{
				k3d::set_property_value(perspective->left(), boost::any_cast<double>(perspective->left().value()) * zoom_factor);
				k3d::set_property_value(perspective->right(), boost::any_cast<double>(perspective->right().value()) * zoom_factor);
				k3d::set_property_value(perspective->top(), boost::any_cast<double>(perspective->top().value()) * zoom_factor);
				k3d::set_property_value(perspective->bottom(), boost::any_cast<double>(perspective->bottom().value()) * zoom_factor);
				
				k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
				return true;
			}
			
		k3d::iorthographic* const orthographic = dynamic_cast<k3d::iorthographic*>(&projection);
		if(orthographic)
			{
				k3d::set_property_value(orthographic->left(), boost::any_cast<double>(orthographic->left().value()) * zoom_factor);
				k3d::set_property_value(orthographic->right(), boost::any_cast<double>(orthographic->right().value()) * zoom_factor);
				k3d::set_property_value(orthographic->top(), boost::any_cast<double>(orthographic->top().value()) * zoom_factor);
				k3d::set_property_value(orthographic->bottom(), boost::any_cast<double>(orthographic->bottom().value()) * zoom_factor);
				
				k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
				return true;
			}

		std::cerr << error << "Unknown projection type" << std::endl;
		return true;
	}

	bool OnRButtonClick(const k3d::imouse_event_observer::event_state& EventState, const k3d::vector2& Current)
	{
		const double sensitivity = 1;
		const k3d::rectangle selection_region(
			Current[0] * m_drawing_area.Width() - sensitivity,
			Current[0] * m_drawing_area.Width() + sensitivity,
			Current[1] * m_drawing_area.Height() - sensitivity,
			Current[1] * m_drawing_area.Height() + sensitivity);
		
		k3d::viewport::iselection_engine* const selection_engine = dynamic_cast<k3d::viewport::iselection_engine*>(m_viewport);
		return_val_if_fail(selection_engine, true);
			
		const unsigned int hit_count = select(selection_region);

		// See if the user clicked on anything (otherwise, default to the camera) ...
		k3d::iobject* object = dynamic_cast<k3d::iobject*>(m_viewport);
		const hit_iterator nearest_hit = std::min_element(hit_iterator(m_selection_buffer, hit_count), hit_iterator());
		if(nearest_hit != hit_iterator())
			{
				for(hit_record::const_name_iterator name = (*nearest_hit).name_begin(); name != (*nearest_hit).name_end(); )
					{
						k3d::iunknown* const unknown = k3d::glGetName(name);

						// If we hit a "handle", we short-circuit the rest of the process and "grab" it ...
						k3d::ihandle* const handle = dynamic_cast<k3d::ihandle*>(unknown);
						if(handle)
							break;

						// Nope, so see if we hit something selectable ...
						k3d::iselectable* const selectable = dynamic_cast<k3d::iselectable*>(unknown);
						if(!selectable)
							break;

						// See if we got an object ...
						if(dynamic_cast<k3d::iobject*>(unknown))
							{
								object = dynamic_cast<k3d::iobject*>(unknown);
								break;
							}
					}
			}

		if(!object)
			return true;

		m_context_menu.show(this, *object);

		return true;
	}

	// "Tripod" transformations, e.g. dolly, pan, tilt, dutch-tilt ...
	void tripod_mode_drag(const k3d::imouse_event_observer::event_state& State, const k3d::vector2& Current, const k3d::vector2& Last, const k3d::vector2& Start)
	{
		// Dolly forward - back
		if(State.modifiers.shift() && State.modifiers.control())
			{
				k3d::iproperty* const position = k3d::get_typed_property<k3d::vector3>(*interactive_target(), "position");
				k3d::iwritable_property* const writable_position = dynamic_cast<k3d::iwritable_property*>(position);
				if(position && writable_position)
					{
						k3d::iprojection& projection = m_viewport->projection();

						double focal_length = 1.0;
						k3d::iperspective* const perspective = dynamic_cast<k3d::iperspective*>(&projection);
						if(perspective)
							{
								focal_length = std::abs(boost::any_cast<double>(perspective->right().value()) - boost::any_cast<double>(perspective->left().value()));
							}
	
						k3d::iorthographic* const orthographic = dynamic_cast<k3d::iorthographic*>(&projection);
						if(orthographic)
							{
								focal_length = std::abs(boost::any_cast<double>(orthographic->right().value()) - boost::any_cast<double>(orthographic->left().value()));
							}
						const double sensitivity = focal_length ? 20.0 / focal_length : 0.0;
	
						const double deltay = Last[1] - Current[1];
		
						const k3d::matrix4 matrix = k3d::object_to_world_matrix(*interactive_target());
						const k3d::vector3 forward = (matrix * k3d::vector3(0, 0, 1)) - (matrix * k3d::vector3(0, 0, 0));
		
						writable_position->set_value(boost::any_cast<k3d::vector3>(position->value()) + (deltay * sensitivity * forward));
						
						k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
					}
				
				return;
			}
			
		// Dolly left - right - up - down
		if(State.modifiers.shift())
			{
				k3d::iproperty* const position = k3d::get_typed_property<k3d::vector3>(*interactive_target(), "position");
				k3d::iwritable_property* const writable_position = dynamic_cast<k3d::iwritable_property*>(position);
				if(position && writable_position)
					{
						k3d::iprojection& projection = m_viewport->projection();

						double focal_length = 1.0;
						k3d::iperspective* const perspective = dynamic_cast<k3d::iperspective*>(&projection);
						if(perspective)
							{
								focal_length = std::abs(boost::any_cast<double>(perspective->right().value()) - boost::any_cast<double>(perspective->left().value()));
							}
	
						k3d::iorthographic* const orthographic = dynamic_cast<k3d::iorthographic*>(&projection);
						if(orthographic)
							{
								focal_length = std::abs(boost::any_cast<double>(orthographic->right().value()) - boost::any_cast<double>(orthographic->left().value()));
							}
						const double sensitivity = focal_length ? 20.0 / focal_length : 0.0;
	
						const double deltax = Current[0] - Last[0];
						const double deltay = Last[1] - Current[1];
		
						const k3d::matrix4 matrix = k3d::object_to_world_matrix(*interactive_target());
						const k3d::vector3 right = (matrix * k3d::vector3(1, 0, 0)) - (matrix * k3d::vector3(0, 0, 0));
						const k3d::vector3 up = (matrix * k3d::vector3(0, 1, 0)) - (matrix * k3d::vector3(0, 0, 0));
		
						writable_position->set_value(boost::any_cast<k3d::vector3>(position->value()) + (sensitivity * deltax * right) + (sensitivity * deltay * up));
						k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
					}
				
				return;
			}
		
		// Dutch tilt ...
		if(State.modifiers.control())
			{
				k3d::iproperty* const orientation = k3d::get_typed_property<k3d::angle_axis>(*interactive_target(), "orientation");
				k3d::iwritable_property* const writable_orientation = dynamic_cast<k3d::iwritable_property*>(orientation);
				if(orientation && writable_orientation)
					{
						// We want mouse coordinates relative to the center of the window ...
						k3d::vector2 currentmouse(Current[0] - 0.5, 0.5 - Current[1]);
						k3d::vector2 lastmouse(Last[0] - 0.5, 0.5 - Last[1]);
						const double deltatheta = currentmouse.Angle() - lastmouse.Angle();

						k3d::quaternion quaternion(boost::any_cast<k3d::angle_axis>(orientation->value()));
						k3d::euler_angles angles(quaternion, k3d::euler_angles::ZXYstatic);
						angles[0] -= deltatheta;
						
						writable_orientation->set_value(k3d::angle_axis(angles));
						k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
					}
					
				return;
			}

		// Pan & tilt ...
		k3d::iproperty* const orientation = k3d::get_typed_property<k3d::angle_axis>(*interactive_target(), "orientation");
		k3d::iwritable_property* const writable_orientation = dynamic_cast<k3d::iwritable_property*>(orientation);
		if(orientation && writable_orientation)
			{
				k3d::quaternion quaternion(boost::any_cast<k3d::angle_axis>(orientation->value()));
				k3d::euler_angles angles(quaternion, k3d::euler_angles::ZXYstatic);
				
				angles[1] += Current[1] - Last[1];
				angles[2] += Current[0] - Last[0];
				
				writable_orientation->set_value(k3d::angle_axis(angles));
				k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
			}
		
		k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
	}
	
	// "Modeling" transformations, i.e. orbit around the origin ...
	void modeling_mode_drag(const k3d::imouse_event_observer::event_state& State, const k3d::vector2& Current, const k3d::vector2& Last, const k3d::vector2& Start)
	{
		// Dolly forward - back
		if(State.modifiers.shift() && State.modifiers.control())
			{
				k3d::iproperty* const position = k3d::get_typed_property<k3d::vector3>(*interactive_target(), "position");
				k3d::iwritable_property* const writable_position = dynamic_cast<k3d::iwritable_property*>(position);
				if(position && writable_position)
					{
						k3d::iprojection& projection = m_viewport->projection();

						const double sensitivity = 30.0;
						double focal_length = 1.0;
						k3d::iperspective* const perspective = dynamic_cast<k3d::iperspective*>(&projection);
						if(perspective)
							{
								focal_length = std::abs(boost::any_cast<double>(perspective->right().value()) - boost::any_cast<double>(perspective->left().value()));
							}
	
						k3d::iorthographic* const orthographic = dynamic_cast<k3d::iorthographic*>(&projection);
						if(orthographic)
							{
								focal_length = std::abs(boost::any_cast<double>(orthographic->right().value()) - boost::any_cast<double>(orthographic->left().value()));
							}
	
						const double deltay = Last[1] - Current[1];
		
						const k3d::matrix4 matrix = k3d::object_to_world_matrix(*interactive_target());
						const k3d::vector3 forward = (matrix * k3d::vector3(0, 0, 1)) - (matrix * k3d::vector3(0, 0, 0));
		
						writable_position->set_value(boost::any_cast<k3d::vector3>(position->value()) + deltay * (sensitivity / focal_length) * forward);
						
						k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
					}
				
				return;
			}
			
		// "Orbit" the origin ...
		k3d::iproperty* const position = k3d::get_typed_property<k3d::vector3>(*interactive_target(), "position");
		k3d::iwritable_property* const writable_position = dynamic_cast<k3d::iwritable_property*>(position);
		k3d::iproperty* const orientation = k3d::get_typed_property<k3d::angle_axis>(*interactive_target(), "orientation");
		k3d::iwritable_property* const writable_orientation = dynamic_cast<k3d::iwritable_property*>(orientation);

		if(position && writable_position && orientation && writable_orientation)
			{		
				// Calculate our field-of-view (based on a perspective view) ...
				const double deltax = k3d::pi();
				const double deltay = k3d::pi();

				k3d::matrix4 matCameraToWorld = k3d::object_to_world_matrix(*interactive_target());
				k3d::matrix4 matWorldToParent = k3d::identity3D();
				k3d::matrix4 matParentToWorld = k3d::identity3D();

				k3d::quaternion q(boost::any_cast<k3d::angle_axis>(orientation->value()));

				// Get the rotation centre in world coordinates, just default to the world origin for now.
				k3d::vector3 centre = k3d::vector3(0,0,0);
				// Get the camera offset position.
				k3d::vector3 pos = matCameraToWorld * k3d::vector3(0, 0, 0);
				pos = matParentToWorld * pos;
				// Move so that the requested rotation centre is at the origin of camera space
				pos -= centre;
				// Rotate about the local x axis using the y mouse movement.
				k3d::vector3 xaxis = matCameraToWorld * k3d::vector3(1,0,0);
				xaxis -= (matCameraToWorld * k3d::vector3(0,0,0));
				k3d::angle_axis axrot(-(double(Last[1] - Current[1]) * deltay), xaxis);
				pos = k3d::matrix4(k3d::rotation3D(axrot)) * pos;
				q = k3d::quaternion(axrot)*q;
				// Now rotate about the world y axis using the mouse x movement.
				k3d::vector3 yaxis = matWorldToParent * k3d::vector3(0,1,0);
				yaxis -= (matWorldToParent * k3d::vector3(0,0,0));
				k3d::angle_axis ayrot = k3d::angle_axis(-(double(Last[0] - Current[0]) * deltax), yaxis);
				pos = k3d::matrix4(k3d::rotation3D(ayrot)) * pos;
				q = k3d::quaternion(ayrot)*q;
				// Move back by the centre offset.
				pos += centre;
				// and convert back into parent coordinates.
				pos = matWorldToParent * pos;
				// Put the new position and orientation of the camera back.
				k3d::set_position(*interactive_target(), matWorldToParent * pos);
				k3d::set_orientation(*interactive_target(), q);
		
				k3d::viewport::redraw_all(m_document, k3d::iviewport::ASYNCHRONOUS);
			}
	}

	k3d::iunknown* interactive_target()
	{
		iunknown* result = m_viewport;
		if(m_viewport && m_viewport->host())
			result = m_viewport->host();

		return result;
	}

	bool OnRButtonDrag(const k3d::imouse_event_observer::event_state& State, const k3d::vector2& Current, const k3d::vector2& Last, const k3d::vector2& Start, const drag_type_t DragType)
	{
		if(!m_viewport)
			return false;
			
		switch(m_navigation_mode.property_value())
			{
				case TRIPOD:
					tripod_mode_drag(State, Current, Last, Start);
					break;
				case MODELING:
					modeling_mode_drag(State, Current, Last, Start);
					break;
			}
		
		return true;
	}

	void mouse_command(GtkWidget* const Widget, const k3d::iuser_interface::mouse_action_t Action, const k3d::key_modifiers& Modifiers, const k3d::vector2& CurrentMouse)
	{
		return_if_fail(Widget);

		if(k3d::application().user_interface())
			{
				k3d::application().user_interface()->tutorial_mouse_message("", Action, Modifiers);
				sdpGtkWarpPointer(Widget, int(CurrentMouse[0]), int(CurrentMouse[1]));
				sdpGtkHandlePendingEvents();
				sdpGtkSleep(20);
			}
	}

	bool execute_command(const std::string& Command, const std::string& Arguments)
	{
		// Extract state information from the arguments ...
		std::istringstream arguments(Arguments);
		k3d::key_modifiers modifiers;
		k3d::vector2 currentmouse;
		k3d::vector2 lastmouse;
		k3d::vector2 startmouse;
		arguments >> modifiers >> currentmouse >> lastmouse >> startmouse;

		// Convert the mouse coordinates from percentages to pixels ...
		const k3d::vector2 viewport_size(static_cast<double>(m_drawing_area.Width()), static_cast<double>(m_drawing_area.Height()));
		currentmouse = Product(currentmouse, viewport_size);
		lastmouse = Product(lastmouse, viewport_size);
		startmouse = Product(startmouse, viewport_size);

		const double tutorialspeed = k3d::application().options().tutorial_speed();
		const unsigned long delay = static_cast<unsigned long>(1200 / tutorialspeed);

		GtkWidget* widget = GTK_WIDGET(m_drawing_area.Object());

		if(Command == control_mousemove)
			{
				OnMouseMove(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::MB_NONE, modifiers, currentmouse);
			}
		else if(Command == control_lbuttondown)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnLButtonDown(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::LMB_CLICK, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_lbuttonclick)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnLButtonClick(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::LMB_CLICK, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_lbuttondoubleclick)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnLButtonDoubleClick(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::LMB_DOUBLE_CLICK, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_lbuttonstartdrag)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnLButtonStartDrag(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::LMB_DRAG, modifiers, currentmouse);
			}
		else if(Command == control_lbuttondrag)
			{
				OnLButtonDrag(k3d::convert(modifiers), currentmouse, lastmouse, startmouse);
				mouse_command(widget, k3d::iuser_interface::LMB_DRAG, modifiers, currentmouse);
			}
		else if(Command == control_lbuttonenddrag)
			{
				OnLButtonEndDrag(k3d::convert(modifiers), currentmouse, lastmouse, startmouse);
				mouse_command(widget, k3d::iuser_interface::LMB_DRAG, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_mbuttonclick)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnMButtonClick(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::MMB_CLICK, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_mbuttondoubleclick)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnMButtonDoubleClick(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::MMB_DOUBLE_CLICK, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_mbuttonstartdrag)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnMButtonStartDrag(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::MMB_DRAG, modifiers, currentmouse);
			}
		else if(Command == control_mbuttondrag)
			{
				OnMButtonDrag(k3d::convert(modifiers), currentmouse, lastmouse, startmouse);
				mouse_command(widget, k3d::iuser_interface::MMB_DRAG, modifiers, currentmouse);
			}
		else if(Command == control_mbuttonenddrag)
			{
				OnMButtonEndDrag(k3d::convert(modifiers), currentmouse, lastmouse, startmouse);
				mouse_command(widget, k3d::iuser_interface::MMB_DRAG, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_rbuttonclick)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnRButtonClick(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::RMB_CLICK, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_rbuttondoubleclick)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnRButtonDoubleClick(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::RMB_DOUBLE_CLICK, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_rbuttonstartdrag)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnRButtonStartDrag(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::RMB_DRAG, modifiers, currentmouse);
			}
		else if(Command == control_rbuttondrag)
			{
				OnRButtonDrag(k3d::convert(modifiers), currentmouse, lastmouse, startmouse);
				mouse_command(widget, k3d::iuser_interface::RMB_DRAG, modifiers, currentmouse);
			}
		else if(Command == control_rbuttonenddrag)
			{
				OnRButtonEndDrag(k3d::convert(modifiers), currentmouse, lastmouse, startmouse);
				mouse_command(widget, k3d::iuser_interface::RMB_DRAG, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else if(Command == control_lrbuttonstartdrag)
			{
				m_drawing_area.InteractiveWarpPointer(int(currentmouse[0]), int(currentmouse[1]), tutorialspeed, true, false);
				OnLRButtonStartDrag(k3d::convert(modifiers), currentmouse);
				mouse_command(widget, k3d::iuser_interface::LMBRMB_DRAG, modifiers, currentmouse);
			}
		else if(Command == control_lrbuttondrag)
			{
				OnLRButtonDrag(k3d::convert(modifiers), currentmouse, lastmouse, startmouse);
				mouse_command(widget, k3d::iuser_interface::LMBRMB_DRAG, modifiers, currentmouse);
			}
		else if(Command == control_lrbuttonenddrag)
			{
				OnLRButtonEndDrag(k3d::convert(modifiers), currentmouse, lastmouse, startmouse);
				mouse_command(widget, k3d::iuser_interface::LMBRMB_DRAG, modifiers, currentmouse);
				sdpGtkSleep(delay);
			}
		else
			{
				return base::execute_command(Command, Arguments);
			}

		return true;
	}

	/// Owning document
	k3d::idocument& m_document;
	/// OpenGL drawing area widget
	sdpGtkOpenGLDrawingArea m_drawing_area;
	/// Stores a reference to the (optional) attached viewport
	k3d::iviewport* m_viewport;
	/// Stores a connection to the attached viewport object's delete signal
	SigC::Connection m_viewport_deleted_connection;
	/// Stores a connection to the attached viewport object's host changed signal
	SigC::Connection m_viewport_host_changed_connection;
	/// Stores a connection to the attached viewport's redraw request signal
	SigC::Connection m_viewport_redraw_request_connection;
	/// Stores a connection to the attached viewport's aspect ratio changed signal
	SigC::Connection m_viewport_aspect_ratio_changed_connection;

	/// Stores the current rubber band (rectangular) selection	
	k3d::rectangle m_rubber_band;
	/// Stores the current lasso (arbitrary shape) selection
	std::vector<k3d::vector2> m_lasso;
	/// Buffers OpenGL selection data
	gl_selection_buffer_t m_selection_buffer;
	/// Caches a reference to the last item pick-selected
	k3d::iselectable* m_last_pick;

	/// Enumerates available "navigation modes" for interactively modifying the viewport
	typedef enum
	{
		TRIPOD,
		MODELING
	} navigation_mode_t;
	
	/// Returns descriptions of the allowed axis values, for use with enumeration properties
	const ienumeration_property::values_t& navigation_mode_values()
	{
		static ienumeration_property::values_t values;
		if(values.empty())
			{
				values.push_back(ienumeration_property::value_t("Tripod", "tripod", "Tripod"));
				values.push_back(ienumeration_property::value_t("Modeling", "modeling", "Modeling"));
			}
	
		return values;
	}

	/// Serialization
	friend std::ostream& operator<<(std::ostream& Stream, const navigation_mode_t& RHS)
	{
		switch(RHS)
			{
				case TRIPOD:
					Stream << "tripod";
					break;
				case MODELING:
					Stream << "modeling";
					break;
			}
			
		return Stream;
	}
	
	/// Serialization
	friend std::istream& operator>>(std::istream& Stream, navigation_mode_t& RHS)
	{
		std::string s;
		Stream >> s;
		
		if(s == "tripod")
			RHS = TRIPOD;
		else if(s == "modeling")
			RHS = MODELING;
		else
			std::cerr << error << __PRETTY_FUNCTION__ << " could not extract value [" << s << "]" << std::endl;
		
		return Stream;
	}

	/// Stores the active axis
	k3d_enumeration_property(k3d::axis, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_active_axis;
	/// Stores the navigation mode
	k3d_enumeration_property(navigation_mode_t, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_navigation_mode;

	// Selection behavior
	k3d_data_property(bool, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_select_objects;
	k3d_data_property(bool, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_select_meshes;
	k3d_data_property(bool, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_select_edges;
	k3d_data_property(bool, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_select_faces;
	k3d_data_property(bool, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_select_curves;
	k3d_data_property(bool, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_select_patches;
	k3d_data_property(bool, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_select_point_groups;
	k3d_data_property(bool, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_select_points;

	/// Context menu when the user right-clicks on something 
	k3d::context_menu::object m_context_menu;
};

////////////////////////////////////////////////////////////////////////////
// control

control::control(idocument& Document, iunknown& ParentCommandNode, const sdpGtkWidget& Owner) :
	m_implementation(new implementation(Document, ParentCommandNode, Owner))
{
}

control::~control()
{
	delete m_implementation;
}

bool control::attach(k3d::iviewport& Viewport)
{
	return m_implementation->attach(Viewport);
}

control::viewport_changed_signal_t& control::viewport_changed_signal()
{
	return m_implementation->viewport_changed_signal();
}

bool control::render_preview()
{
	return m_implementation->render_preview();
}

bool control::render_frame(const boost::filesystem::path& OutputImage, const bool ViewCompletedImage)
{
	return m_implementation->render_frame(OutputImage, ViewCompletedImage);
}

bool control::render_animation(const boost::filesystem::path& OutputImages, const bool ViewCompletedImages)
{
	return m_implementation->render_animation(OutputImages, ViewCompletedImages);
}

} // namespace viewport

} // namespace k3d
