// 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
		\author Tim Shead (tshead@k-3d.com)
*/

#include "k3dobjectdialog.h"
#include "keyboard.h"
#include "spin_button.h"

#include <k3dsdk/application.h>
#include <k3dsdk/basic_math.h>
#include <k3dsdk/bezier.h>
#include <k3dsdk/classes.h>
#include <k3dsdk/computed_property.h>
#include <k3dsdk/geometry.h>
#include <k3dsdk/ibezier_channel.h>
#include <k3dsdk/ichannel.h>
#include <k3dsdk/itime_sink.h>
#include <k3dsdk/iuser_interface.h>
#include <k3dsdk/object.h>
#include <k3dsdk/persistence.h>
#include <k3dsdk/mouse_event_observer.h>
#include <k3dsdk/ioptions.h>
#include <k3dsdk/property.h>
#include <k3dsdk/state_change_set.h>
#include <k3dsdk/string_modifiers.h>
#include <k3dsdk/time_source.h>
#include <k3dsdk/user_interface.h>
#include <k3dsdk/vectors.h>

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

#include <iterator>
#include <set>

// We have an unfortunate clash with X
#ifdef RootWindow
#undef RootWindow
#endif // RootWindow

namespace k3d
{

/////////////////////////////////////////////////////////////////////////////
// scalar_bezier_channel_properties

namespace
{

const rectangle frustum(-1, 1, 1, -1);

vector3 background_color(0.8, 0.8, 0.8);
vector3 current_time_color(0.0, 1.0, 0.0);
vector3 cursor_color(1.0, 0.0, 0.0);
vector3 normal_color(0.0, 0.0, 0.0);
vector3 value_node_color(1.0, 1.0, 0.0);
vector3 selected_color(1.0, 1.0, 1.0);
vector3 tangent_color(0.0, 0.0, 1.0);
vector3 selector_color(0.215, 0.215, 0.8);

const std::string control_straightenchannel = "straightenchannel";
const std::string control_mirrorxchannel = "mirrorxchannel";
const std::string control_mirrorychannel = "mirrorychannel";
const std::string control_cursorx = "cursorx";
const std::string control_cycles = "cycles";
const std::string control_phase = "phase";
const std::string control_straightenselected = "straightenselected";
const std::string control_deleteselected = "deleteselected";
const std::string control_resetview = "resetview";
const std::string control_mousemove = "mousemove";
const std::string control_lbuttondown = "lbuttondown";
const std::string control_lbuttonclick = "lbuttonclick";
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_mbuttonclick = "mbuttonclick";
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_rbuttonclick = "rbuttonclick";
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";

} // namespace

/// The main user interface for a bezier control channel object
class scalar_bezier_channel_properties :
	public k3dObjectDialog,
	public sdpGtkMouseInput
{
	typedef k3dObjectDialog base;

	/// Convenience typedef for our channel type
	typedef ibezier_channel<void> channel_t;
	/// Convenience typedef for control-point storage for our channel type
	typedef channel_t::control_points_t control_points_t;
	/// Stores a set of control points by zero-based index
	typedef std::set<unsigned long> control_point_set_t;

public:

	scalar_bezier_channel_properties(iobject& Object) :
		base(Object, false, false, true, "properties", new options_window_geometry_store()),
		m_object(Object),
		m_channel(dynamic_cast<channel_t*>(&Object))
	{
		// Load the dialog template ...
		return_if_fail(LoadGTKMLTemplate("scalar_bezier_channel.gtkml"));

		// Create the OpenGL widgets ...
		sdpGtkContainer curvecontainer(Container("curve"));
		if(!m_curve_widget.Create(curvecontainer, true, true, 8, 8, 8, 0))
			if(!m_curve_widget.Create(curvecontainer, true, true, 5, 5, 5, 0))
				if(!m_curve_widget.Create(curvecontainer, false, false, 4, 4, 4, 0))
					std::cerr << "scalar_bezier_channel_properties(): Could not find useable OpenGL visual" << std::endl;

		// Setup some events ...
		if(m_curve_widget.Initialized())
			{
				MapEvent("configure-event", "configurecurve", false, m_curve_widget, true);
				MapEvent("expose-event", "exposecurve", false, m_curve_widget, true);
				MapEvent("motion-notify-event", "mousemovecurve", false, m_curve_widget, true);
				MapEvent("button-press-event", "buttondowncurve", false, m_curve_widget, true);
				MapEvent("button-release-event", "buttonupcurve", false, m_curve_widget, true);
			}

		// We want to be notified if the channel is modified ...
		ichannel<double>* const channel = dynamic_cast<ichannel<double>*>(&m_object);
		return_if_fail(channel);
		channel->changed_signal().connect(SigC::slot(*this, &scalar_bezier_channel_properties::on_channel_modified));

		// We want to be notified when the document time changes ...
		iproperty* const time = get_time(m_object.document());
		if(time)
			time->changed_signal().connect(SigC::slot(*this, &scalar_bezier_channel_properties::on_frame_time_changed));

		// Initialize the cursor ...
		set_cursor_x(0.0);

		// Cache the new curve ...
		m_channel->get_curve(m_control_points);
		m_active_control_point = m_control_points.end();

		// Reset our view so it's properly scaled & centered ...
		reset_view();

		Show();

		// The Win32 port of GTK+ has some kind of refresh problem, so force one to get us going ...
#ifdef SDPWIN32
		RootWidget().QueueClear();
		sdpGtkHandlePendingEvents();
#endif
	}

	// icommand_node implementation ...
	bool execute_command(const std::string& Command, const std::string& Arguments)
	{
		// Extract state information from the arguments ...
		std::istringstream arguments(Arguments);
		int temp = 0;
		vector2 currentmouse;
		vector2 lastmouse;
		vector2 startmouse;
		arguments >> temp >> currentmouse >> lastmouse >> startmouse;
		GdkModifierType modifiers = GdkModifierType(temp);

		// Convert the mouse from world coordinates to screen ...
		currentmouse = screen(currentmouse);
		lastmouse = screen(lastmouse);
		startmouse = screen(startmouse);

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

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

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

		return true;
	}

private:
	void on_channel_modified()
	{
		// Cache the new curve ...
		m_channel->get_curve(m_control_points);
		m_active_control_point = m_control_points.end();

		// Update the current selection (in case it contains references to control points that no longer exist) ...
		m_selection.erase(m_selection.upper_bound(m_control_points.size()-1), m_selection.end());

		redraw_all();
	}

	/// Dispatches custom events
	void OnEvent(sdpGtkEvent* Event)
	{
		if(Event->Name() == "configurecurve" || Event->Name() == "exposecurve")
			draw_curve();
		else if(Event->Name() == "mousemovecurve")
			RawMouseMove(Event);
		else if(Event->Name() == "buttondowncurve")
			RawButtonDown(Event);
		else if(Event->Name() == "buttonupcurve")
			RawButtonUp(Event);
		else if(Event->Name() == "vscale")
			on_vertical_scale();
		else if(Event->Name() == control_cursorx)
			on_cursor_x();
		else if(Event->Name() == control_straightenchannel)
			on_straighten_channel();
		else if(Event->Name() == control_mirrorxchannel)
			on_mirror_channel_x();
		else if(Event->Name() == control_mirrorychannel)
			on_mirror_channel_y();
		else if(Event->Name() == control_straightenselected)
			on_straighten_selected();
		else if(Event->Name() == control_deleteselected)
			on_delete_selected();
		else if(Event->Name() == control_resetview)
			on_reset_view();
		else
			base::OnEvent(Event);
	}

	/// Called to straighten the entire channel
	void on_straighten_channel()
	{
		record_command(*this, icommand_node::command_t::USER_INTERFACE, control_straightenchannel);
		record_state_change_set undo(m_object.document(), "Straighten Channel");

		// Get the maximum extents of the curve ...
		rectangle extents = value_extents(m_control_points);

		// Take a guess at whether the slope of the result should be positive or negative ...
		if(m_control_points.front()[1] > m_control_points.back()[1])
			std::swap(extents.top, extents.bottom);

		const vector2 left = vector2(extents.Left(), extents.Bottom());
		const vector2 right = vector2(extents.Right(), extents.Top());

		// Space the nodes out evenly to create a linear curve ...
		for(unsigned long i = 0; i < m_control_points.size(); ++i)
			m_control_points[i] = mix(left, right, static_cast<double>(i) / static_cast<double>(m_control_points.size() - 1));

		// Set the new curve ...
		m_channel->set_curve(m_control_points);
	}

	/// Called to mirror the entire channel
	void on_mirror_channel_x()
	{
		record_command(*this, icommand_node::command_t::USER_INTERFACE, control_mirrorxchannel);
		record_state_change_set undo(m_object.document(), "Channel Mirror X");

		// Pick a reflection axis through the "middle" of the curve ...
		const double reflection_axis = value_extents(m_control_points).Center()[0];

		// Mirror each control point around the axis ...
		for(control_points_t::iterator control_point = m_control_points.begin(); control_point != m_control_points.end(); ++control_point)
			*control_point = vector2(reflection_axis - ((*control_point)[0] - reflection_axis), (*control_point)[1]);

		// Reverse the order of the control points and valuesnodes ...
		std::reverse(m_control_points.begin(), m_control_points.end());

		// Set the new curve ...
		m_channel->set_curve(m_control_points);
	}

	/// Called to mirror the entire channel
	void on_mirror_channel_y()
	{
		record_command(*this, icommand_node::command_t::USER_INTERFACE, control_mirrorychannel);
		record_state_change_set undo(m_object.document(), "Channel Mirror Y");

		// Pick a reflection axis througharound the "middle" of the curveGet the curve extents ...
		const double reflection_axis = value_extents(m_control_points).Center()[1];

		// Mirror each control point around the axis ...
		for(control_points_t::iterator control_point = m_control_points.begin(); control_point != m_control_points.end(); ++control_point)
			*control_point = vector2((*control_point)[0], reflection_axis - ((*control_point)[1] - reflection_axis));

		// Set the new curve ...
		m_channel->set_curve(m_control_points);
	}

	void draw_curve()
	{
		// If we're not completely initialized, we're done ...
		if(!m_curve_widget.Initialized())
			return;

		m_curve_widget.Begin();

		// Get the widget sizes ...
		unsigned long width = m_curve_widget.Width();
		unsigned long height = m_curve_widget.Height();
		glViewport(0, 0, width, height);

		// Clear the background ...
		glClearColor(background_color[0], background_color[1], background_color[2], 1.0);
		glClear(GL_COLOR_BUFFER_BIT);

		// Setup the viewing projection ...
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glOrtho(frustum.Left(), frustum.Right(), frustum.Bottom(), frustum.Top(), -1, 1);

		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glScaled(1.0 / m_scale[0], 1.0 / m_scale[1], 1.0);
		glTranslated(-m_origin[0], -m_origin[1], 0.0);
		glScaled(1, 1.0 / m_vertical_scale, 1.0);

		// Setup drawing options ...
		glDisable(GL_DEPTH_TEST);
		glDisable(GL_LIGHTING);
		glShadeModel(GL_FLAT);

		// Make it happen ...
		draw_grid();
		draw_cursor();
		draw_current_time();
		draw_control_curve();
		draw_tangents();
		draw_nodes();

		// Cleanup ...
		m_curve_widget.SwapBuffers();
		m_curve_widget.End();
	}

	void draw_grid()
	{
		glPushAttrib(GL_ALL_ATTRIB_BITS);

		glColor3dv(background_color * 0.8);

		// Draw the grid ...
		const static int xdivisions = 10;
		const static int ydivisions = 10;

		glBegin(GL_LINES);

		for(int i = 0; i <= xdivisions; i++)
			{
				glVertex2d(double(i) / double(xdivisions), 0.0);
				glVertex2d(double(i) / double(xdivisions), 1.0);
			}

		for(int j = 0; j <= ydivisions; j++)
			{
				glVertex2d(0.0, double(j) / double(ydivisions));
				glVertex2d(1.0, double(j) / double(ydivisions));
			}

		glEnd();

		const double left = world(vector2(0.0, 0.0))[0];
		const double top = world(vector2(0, 0.0))[1];
		const double right = world(vector2(0.0, static_cast<double>(m_curve_widget.Width())))[0];
		const double bottom = world(vector2(0.0, static_cast<double>(m_curve_widget.Height())))[1];

		// Draw the origin / axes ...
		glColor3dv(background_color * 0.6);
		glLineWidth(2.0f);

		glBegin(GL_LINES);

		glVertex2d(0.0, top);
		glVertex2d(0.0, bottom);

		glVertex2d(left, 0.0);
		glVertex2d(right, 0.0);

		glEnd();

		glPopAttrib();
	}

	void draw_cursor()
	{
		double top = world(vector2(0.0, 0.0))[1];
		double bottom = world(vector2(0.0, static_cast<double>(m_curve_widget.Height())))[1];

		glColor3dv(cursor_color);
		glBegin(GL_LINES);

		// Vertical cursor ...
		glVertex2d(m_cursor[0], top);
		glVertex2d(m_cursor[0], bottom);

		glEnd();
	}

	void draw_current_time()
	{
		iproperty* const time_property = get_time(m_object.document());
		if(!time_property)
			return;

		const double time = boost::any_cast<double>(get_property_value(m_object.document().dag(), *time_property));

		const double top = world(vector2(0.0, 0.0))[1];
		const double bottom = world(vector2(0.0, static_cast<double>(m_curve_widget.Height())))[1];

		glColor3dv(current_time_color);
		glBegin(GL_LINES);
		glVertex2d(time, top);
		glVertex2d(time, bottom);
		glEnd();
	}

	void draw_control_curve()
	{
		// Draw initial straight line node before first node
		glColor3dv(normal_color);
		glBegin(GL_LINES);
		glVertex2d(-10000.0, m_control_points.front()[1]);
		glVertex2dv(m_control_points.front());
		glEnd();

		// Draw straight line node after last node
		glBegin(GL_LINES);
		glVertex2dv(m_control_points.back());
		glVertex2d(10000.0, m_control_points.back()[1]);
		glEnd();

		// We may be done at this point
		if(1 == m_control_points.size())
			return;

		// Setup Bezier curve drawing
		glEnable(GL_MAP1_VERTEX_3);

		GLdouble controlpoints[4][3];
		memset(controlpoints, 0, sizeof(controlpoints));

		for(unsigned long i = 0; i < m_control_points.size()-1; i += 3)
			{
				controlpoints[0][0] = m_control_points[i][0];
				controlpoints[0][1] = m_control_points[i][1];

				controlpoints[1][0] = m_control_points[i+1][0];
				controlpoints[1][1] = m_control_points[i+1][1];

				controlpoints[2][0] = m_control_points[i+2][0];
				controlpoints[2][1] = m_control_points[i+2][1];

				controlpoints[3][0] = m_control_points[i+3][0];
				controlpoints[3][1] = m_control_points[i+3][1];

				glMap1d(GL_MAP1_VERTEX_3, 0.0, 1.0, 3, 4, &controlpoints[0][0]);

				glBegin(GL_LINE_STRIP);
				const static int stepsize = 50;
				for(int j = 0; j <= stepsize; ++j)
					glEvalCoord1d(static_cast<GLdouble>(j) / static_cast<GLdouble>(stepsize));
				glEnd();
			}
	}

	void draw_tangents()
	{
		glColor3dv(tangent_color);
		glBegin(GL_LINES);

		for(unsigned long i = 0; i < m_control_points.size(); ++i)
			{
				if(1 == i % 3)
					{
						glVertex2dv(m_control_points[i-1]);
						glVertex2dv(m_control_points[i]);
					}
				else if(2 == i % 3)
					{
						glVertex2dv(m_control_points[i]);
						glVertex2dv(m_control_points[i+1]);
					}
			}

		glEnd();
	}

	void draw_nodes()
	{
		// Set ourselves up to draw some big round points ...
		glEnable(GL_POINT_SMOOTH);
		glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);

		for(unsigned long i = 0; i < m_control_points.size(); ++i)
			{
				if(0 == i % 3)
					{
						glPointSize(6.0);
						glColor3dv(selected(i) ? selected_color : value_node_color);
					}
				else
					{
						glPointSize(5.0);
						glColor3dv(selected(i) ? selected_color : normal_color);
					}

				glBegin(GL_POINTS);
				glVertex2dv(m_control_points[i]);
				glEnd();
			}
	}

	void on_frame_time_changed()
	{
		redraw_all();
	}

	void redraw_all()
	{
		Widget("curve").QueueDraw();
	}

	vector2 world(const vector2 ScreenPosition)
	{
		// Get the dimensions of the curve window ...
		const unsigned long width = m_curve_widget.Width();
		const unsigned long height = m_curve_widget.Height();

		vector2 result;

		// Convert pixel coordinates to "screen" coordinates ...
		// (Make sure we handle (possible) minimized / hidden widget)
		if(width)
			result[0] = mix(frustum.Left(), frustum.Right(), ScreenPosition[0] / width);

		if(height)
			result[1] = mix(frustum.Top(), frustum.Bottom(), ScreenPosition[1] / height);

		// Scale the results to account for pan/zoom ...
		result = Product(result, m_scale) + m_origin;
		result[1] *= m_vertical_scale;

		return result;
	}

	vector2 screen(const vector2 WorldPosition)
	{
		// Get the dimensions of the curve window ...
		const unsigned long width = m_curve_widget.Width();
		const unsigned long height = m_curve_widget.Height();

		// Convert world coordinates to "screen" coordinates ...
		vector2 world_position(WorldPosition);
		world_position[1] *= (1.0 / m_vertical_scale);
		vector2 result = Product((world_position - m_origin), vector2(1.0 / m_scale[0], 1.0 / m_scale[1]));

		// Convert to pixel coordinates ...
		result[0] = width * (result[0] - frustum.Left()) / frustum.Width();
		result[1] = height * (1 - ((result[1] - frustum.Bottom()) / frustum.Height()));

		return result;
	}

	void on_reset_view()
	{
		record_command(*this, icommand_node::command_t::USER_INTERFACE, control_resetview);
		reset_view();
		redraw_all();
	}

	void reset_view()
	{
		// Get the maximum extents of the curve ...
		rectangle extents = control_point_extents(m_control_points);

		// Make sure we can always see the X axis ...
		if(extents.bottom > 0)
			extents.bottom = 0;

		if(extents.top < 0)
			extents.top = 0;

		// Make sure we can handle curves that are straight across the origin ...
		if(extents.top == extents.bottom)
			extents.top += 1;

		// Make sure we can always see the Y axis ...
		if(extents.left > 0)
			extents.left = 0;

		if(extents.right < 0)
			extents.right = 0;

		// Make sure we handle single-point curves ...
		if(extents.left == extents.right)
			extents.right += 1;

		// Initialize the scale & origin ...
		m_vertical_scale = 1.0;
		m_scale = vector2(extents.Width(), extents.Height()) * 0.55;
		m_origin = extents.Center();
	}

	void on_cursor_x()
	{
		record_command(*this, icommand_node::command_t::USER_INTERFACE, control_cursorx);

		const double xvalue = from_string<double>(Entry(control_cursorx.c_str()).GetText(), -1);

		set_cursor_x(xvalue);

		if(m_active_control_point != m_control_points.end())
			{
				record_state_change_set undo(m_object.document(), "Channel Node X Value change");
				*m_active_control_point = vector2(xvalue, (*m_active_control_point)[1]);
			}

		redraw_all();
	}

	void set_cursor_x(const double X)
	{
		m_cursor[0] = X;

		Entry("cursorx").SetText(sdpToString(m_cursor[0]));
		Entry("cursory").SetText("******");

		ichannel<double>* const channel = dynamic_cast<ichannel<double>*>(&m_object);
		return_if_fail(channel);
		Entry("cursorvalue").SetText(sdpToString(channel->value(m_cursor[0], 0.001)));
	}

	void set_cursor_xy(const vector2 XY)
	{
		m_cursor[0] = XY[0];
		m_cursor[1] = XY[1];

		Entry("cursorx").SetText(sdpToString(m_cursor[0]));
		Entry("cursory").SetText(sdpToString(m_cursor[1]));

		ichannel<double>* const channel = dynamic_cast<ichannel<double>*>(&m_object);
		return_if_fail(channel);
		Entry("cursorvalue").SetText(sdpToString(channel->value(m_cursor[0], 0.001)));
	}

	void record_event(const std::string Command, GdkModifierType Modifiers, const vector2 CurrentMouse)
	{
		// Sanity checks ...
		assert(Command.size());

		// Setup mouse coordinates ...
		vector2 currentmouse(world(CurrentMouse));

		// Record the event ...
		record_command(*this, icommand_node::command_t::USER_INTERFACE, Command, to_string(int(Modifiers)) + " " + to_string(currentmouse));
	}

	void record_drag_event(const std::string Command, GdkModifierType Modifiers, const vector2 CurrentMouse, const vector2 LastMouse, const vector2 StartMouse)
	{
		// Sanity checks ...
		assert(Command.size());

		// Setup mouse coordinates ...
		vector2 currentmouse(world(CurrentMouse));
		vector2 lastmouse(world(LastMouse));
		vector2 startmouse(world(StartMouse));

		// Record the event ...
		record_command(*this, icommand_node::command_t::USER_INTERFACE, Command, to_string(int(Modifiers)) + " " + to_string(currentmouse) + " " + to_string(lastmouse) + " " + to_string(startmouse));
	}

	void OnLButtonDown(GdkModifierType Modifiers, const vector2 CurrentMouse)
	{
		record_event(control_lbuttondown, Modifiers, CurrentMouse);

		control_point_set_t hits = hit_test(CurrentMouse);
		if(hits.empty())
			{
				set_cursor_x(world(CurrentMouse)[0]);
				redraw_all();
				return;
			}

		// Keep track of the "active" control point ...
		m_active_control_point = m_control_points.begin() + (*hits.begin());

		// Update our selection ...
		m_selection.insert(hits.begin(), hits.end());

		// Set the cursor ...
		set_cursor_xy(m_control_points[*hits.begin()]);

		// Handle optional additional selections
		if(Modifiers & GDK_SHIFT_MASK)
			{
				if(is_value_control_point(*hits.begin()))
					{
						m_selection.insert(previous_control_point(*hits.begin()));
						m_selection.insert(next_control_point(*hits.begin()));
					}
			}

		redraw_all();
	}

	void OnLButtonDoubleClick(GdkModifierType Modifiers, const vector2 CurrentMouse)
	{
		record_event(control_lbuttondoubleclick, Modifiers, CurrentMouse);

		// Convert mouse coordinates to world coordinates ...
		const vector2 worldcoords = world(CurrentMouse);

		// See if the user clicked on an existing node, first ...
		const control_point_set_t hits = hit_test(CurrentMouse);
		if(!hits.empty())
			{
				set_cursor_xy(vector2(worldcoords[0], worldcoords[1]));
				edit_values(hits);
				return;
			}

		// Otherwise, create a new value ...
		record_state_change_set undo(m_object.document(), "Create Node(s)");

		// Create control points ...
		control_point_set_t new_control_points = insert_value(worldcoords);

		// Select 'em ...
		m_selection = new_control_points;

		// Position the cursor ...
		set_cursor_xy(worldcoords);

		// Update the curve
		m_channel->set_curve(m_control_points);

		// Edit that baby!
		edit_values(m_selection);
	}

	control_point_set_t insert_value(const vector2 WorldCoords)
	{
		control_point_set_t results;

		// Handle the easiest special-case: new value precedes any existing values
		if(WorldCoords[0] <= m_control_points.front()[0])
			{
				const vector2 left(WorldCoords);
				const vector2 right(m_control_points.front());

				control_points_t new_control_points;
				new_control_points.push_back(left);
				new_control_points.push_back(vector2(mix<double>(left[0], right[0], 0.3), left[1]));
				new_control_points.push_back(vector2(mix<double>(left[0], right[0], 0.7), right[1]));

				m_control_points.insert(m_control_points.begin(), new_control_points.begin(), new_control_points.end());

				results.insert(0);
				results.insert(1);
				results.insert(2);

				return results;
			}

		// Next-easiest special-case: new value comes after all existing values
		if(WorldCoords[0] > m_control_points.back()[0])
			{
				const vector2 left(m_control_points.back());
				const vector2 right(WorldCoords);

				control_points_t new_control_points;
				new_control_points.push_back(vector2(mix<double>(left[0], right[0], 0.3), left[1]));
				new_control_points.push_back(vector2(mix<double>(left[0], right[0], 0.7), right[1]));
				new_control_points.push_back(right);

				m_control_points.insert(m_control_points.end(), new_control_points.begin(), new_control_points.end());

				results.insert(m_control_points.size() - 3);
				results.insert(m_control_points.size() - 2);
				results.insert(m_control_points.size() - 1);

				return results;
			}

		// OK, new value is somewhere in the middle ...
		for(unsigned int i = 0; i < m_control_points.size() - 1; i += 3)
			{
				if(WorldCoords[0] > m_control_points[i+3][0])
					continue;

				const vector2 left(m_control_points[i]);
				const vector2 right(m_control_points[i+3]);

				control_points_t new_control_points;
				new_control_points.push_back(vector2(mix<double>(left[0], WorldCoords[0], 0.7), WorldCoords[1]));
				new_control_points.push_back(WorldCoords);
				new_control_points.push_back(vector2(mix<double>(WorldCoords[0], right[0], 0.3), WorldCoords[1]));

				m_control_points.insert(m_control_points.begin() + i + 2, new_control_points.begin(), new_control_points.end());

				results.insert(i + 2);
				results.insert(i + 3);
				results.insert(i + 4);

				return results;
			}

		// Should never be reached!
		assert_warning(0);
		return results;
	}

	void OnLButtonStartDrag(GdkModifierType Modifiers, const vector2 CurrentMouse)
	{
		record_event(control_lbuttonstartdrag, Modifiers, CurrentMouse);

		if(!m_selection.empty())
			start_state_change_set(m_object.document());
	}

	void OnLButtonDrag(GdkModifierType Modifiers, const vector2 CurrentMouse, const vector2 LastMouse, const vector2 StartMouse)
	{
		record_drag_event(control_lbuttondrag, Modifiers, CurrentMouse, LastMouse, StartMouse);

		// Update the cursor position ...
		set_cursor_x(world(CurrentMouse)[0]);

		// Calculate the amount of mouse movement in world coordinates ...
		const vector2 delta(world(CurrentMouse) - world(LastMouse));

		// For each selected node ...
		for(control_point_set_t::iterator control_point = m_selection.begin(); control_point != m_selection.end(); ++control_point)
			move_control_point(*control_point, m_control_points[*control_point] + delta, Modifiers);

		// Set the new curve ...
		m_channel->set_curve(m_control_points);

		redraw_all();
	}

	/// Moves a control point, and possibly moves its neighbors, based on user input ...
	void move_control_point(const unsigned long ControlPoint, const vector2 Position, const GdkModifierType Modifiers)
	{
		// Move the control point ...
		move_control_point(ControlPoint, Position);

		// If there aren't any other points, we're done ...
		if(1 == m_control_points.size())
			return;

		// If it's a value control point ...
		if(is_value_control_point(ControlPoint))
			{
				// If the user holds down the shift key, constrain the first and last control points to the same position (good for looping animations)
				if(Modifiers & GDK_SHIFT_MASK)
					{
						if(is_first_value_control_point(ControlPoint))
							move_control_point(last_control_point(), vector2(m_control_points[last_control_point()][0], Position[1]));
						else if(is_last_value_control_point(ControlPoint))
							move_control_point(first_control_point(), vector2(m_control_points[first_control_point()][0], Position[1]));
					}
			}
		// Not a value control point ...
		else
			{
				// If the user holds down the shift key, constrain the slope of the opposite control point to be identical
				if(Modifiers & GDK_SHIFT_MASK)
					{
						const vector2 slope = m_control_points[nearest_value_control_point(ControlPoint)] - m_control_points[ControlPoint];
						move_control_point(opposite_control_point(ControlPoint), m_control_points[nearest_value_control_point(opposite_control_point(ControlPoint))] + slope);
					}
			}
	}

	/// Moves a control point, constraining to the valid range of values, and moving any of its neighbors as needed
	void move_control_point(const unsigned long ControlPoint, const vector2 Position)
	{
/*
		// Get the range of allowable values ...
		double minimum;
		double maximum;
		m_object.range(minimum, maximum);
		vector2 position(Position[0], std::max(minimum, std::min(maximum, Position[1])));
*/
		vector2 position(Position);

		// Move the control point ...
		m_control_points[ControlPoint] = position;

		// Recursively move its neighbors ...
		const unsigned long a = previous_control_point(previous_control_point(ControlPoint));
		const unsigned long b = previous_control_point(ControlPoint);
		const unsigned long c = next_control_point(ControlPoint);
		const unsigned long d = next_control_point(next_control_point(ControlPoint));

		if(is_value_control_point(ControlPoint))
			{
				if(a != ControlPoint && Position[0] < m_control_points[a][0])
					move_control_point(a, vector2(Position[0], m_control_points[a][1]));

				if(b != ControlPoint && Position[0] < m_control_points[b][0])
					move_control_point(b, vector2(Position[0], m_control_points[b][1]));

				if(c != ControlPoint && Position[0] > m_control_points[c][0])
					move_control_point(c, vector2(Position[0], m_control_points[c][1]));

				if(d != ControlPoint && Position[0] > m_control_points[d][0])
					move_control_point(d, vector2(Position[0], m_control_points[d][1]));
			}
		else
			{
				if(is_value_control_point(b))
					{
						if(Position[0] < m_control_points[b][0])
							move_control_point(b, vector2(Position[0], m_control_points[b][1]));

						if(Position[0] > m_control_points[d][0])
							move_control_point(d, vector2(Position[0], m_control_points[d][1]));
					}
				else
					{
						if(Position[0] < m_control_points[a][0])
							move_control_point(a, vector2(Position[0], m_control_points[a][1]));

						if(Position[0] > m_control_points[c][0])
							move_control_point(c, vector2(Position[0], m_control_points[c][1]));
					}
			}
	}

	void OnLButtonEndDrag(GdkModifierType Modifiers, const vector2 CurrentMouse, const vector2 LastMouse, const vector2 StartMouse)
	{
		record_drag_event(control_lbuttonenddrag, Modifiers, CurrentMouse, LastMouse, StartMouse);

		if(!m_selection.empty())
			finish_state_change_set(m_object.document(), "Move node(s)");
	}

	void on_straighten_selected()
	{
		record_command(*this, icommand_node::command_t::USER_INTERFACE, control_straightenselected);

		// If we don't have anything, we're done ...
		if(m_selection.empty())
			{
				error_message("You must select some control points to straighten!", "Straighten Selected:");
				return;
			}

		for(control_point_set_t::iterator control_point = m_selection.begin(); control_point != m_selection.end(); ++control_point)
			{
				if(is_value_control_point(*control_point))
					continue;

				const unsigned long a = nearest_value_control_point(*control_point);
				const unsigned long b = *control_point;
				const unsigned long c = neighbor_control_point(*control_point);
				const unsigned long d = nearest_value_control_point(neighbor_control_point(*control_point));

				const vector2 first(m_control_points[a]);
				const vector2 last(m_control_points[d]);

				m_control_points[b] = mix(first, last, 0.3);
				m_control_points[c] = mix(first, last, 0.7);
			}

		// Store the new curve ...
		record_state_change_set undo(m_object.document(), "Straighten selected segment(s)");
		m_channel->set_curve(m_control_points);
	}

	void on_delete_selected()
	{
		record_command(*this, icommand_node::command_t::USER_INTERFACE, control_deleteselected);

		// Figure-out exactly which control points need to be deleted
		control_point_set_t lambs;
		for(control_point_set_t::iterator control_point = m_selection.begin(); control_point != m_selection.end(); ++control_point)
			{
				lambs.insert(nearest_value_control_point(*control_point));
				if(is_first_value_control_point(nearest_value_control_point(*control_point)))
					{
						lambs.insert(next_control_point(nearest_value_control_point(*control_point)));
						lambs.insert(previous_control_point(next_value_control_point(nearest_value_control_point(*control_point))));
					}
				else if(is_last_value_control_point(nearest_value_control_point(*control_point)))
					{
						lambs.insert(previous_control_point(nearest_value_control_point(*control_point)));
						lambs.insert(next_control_point(previous_value_control_point(nearest_value_control_point(*control_point))));
					}
				else
					{
						lambs.insert(previous_control_point(nearest_value_control_point(*control_point)));
						lambs.insert(next_control_point(nearest_value_control_point(*control_point)));
					}
			}

		// If we don't have anything, we're done ...
		if(lambs.empty())
			{
				error_message("You must select some control points to delete!", "Delete Selected:");
				return;
			}

		// If we're trying to delete everything, no-can-do ...
		if(lambs.size() == m_control_points.size())
			{
				error_message("You can't delete the last control point!", "Delete Selected:");
				return;
			}

		// Clear the selection ...
		m_selection.clear();

		// Delete control points ...
		for(control_point_set_t::reverse_iterator control_point = lambs.rbegin(); control_point != lambs.rend(); ++control_point)
			m_control_points.erase(m_control_points.begin() + (*control_point));

		// Update the new curve ...
		record_state_change_set undo(m_object.document(), "Delete selected control point(s)");
		m_channel->set_curve(m_control_points);
	}

	void OnRButtonClick(GdkModifierType Modifiers, const vector2 CurrentMouse)
	{
		record_event(control_rbuttonclick, Modifiers, CurrentMouse);
		deselect_all();
		redraw_all();
	}

	void OnRButtonStartDrag(GdkModifierType Modifiers, const vector2 CurrentMouse)
	{
		record_event(control_rbuttonstartdrag, Modifiers, CurrentMouse);
	}

	void OnRButtonDrag(GdkModifierType Modifiers, const vector2 CurrentMouse, const vector2 LastMouse, const vector2 StartMouse)
	{
		record_drag_event(control_rbuttondrag, Modifiers, CurrentMouse, LastMouse, StartMouse);

		const vector2 delta = world(CurrentMouse) - world(LastMouse);
		m_origin += delta;

		redraw_all();
	}

	void OnRButtonEndDrag(GdkModifierType Modifiers, const vector2 CurrentMouse, const vector2 LastMouse, const vector2 StartMouse)
	{
		record_drag_event(control_rbuttonenddrag, Modifiers, CurrentMouse, LastMouse, StartMouse);
	}

	void OnMButtonStartDrag(GdkModifierType Modifiers, const vector2 CurrentMouse)
	{
		record_event(control_lrbuttonstartdrag, Modifiers, CurrentMouse);
	}

	void OnMButtonDrag(GdkModifierType Modifiers, const vector2 CurrentMouse, const vector2 LastMouse, const vector2 StartMouse)
	{
		record_drag_event(control_lrbuttondrag, Modifiers, CurrentMouse, LastMouse, StartMouse);

		const double zoomfactor = CurrentMouse[1] > LastMouse[1] ? pow(1.005, CurrentMouse[1] - LastMouse[1]) : pow(0.995, LastMouse[1] - CurrentMouse[1]);
		m_scale *= zoomfactor;
		redraw_all();
	}

	void OnMButtonEndDrag(GdkModifierType Modifiers, const vector2 CurrentMouse, const vector2 LastMouse, const vector2 StartMouse)
	{
		record_drag_event(control_lrbuttonenddrag, Modifiers, CurrentMouse, LastMouse, StartMouse);
	}

	void on_vertical_scale()
	{
		m_vertical_scale = pow(10.0, Adjustment("vscale").Value());
		redraw_all();
	}

	void edit_values(const control_point_set_t ControlPoints)
	{
		for(control_point_set_t::const_iterator control_point = ControlPoints.begin(); control_point != ControlPoints.end(); ++control_point)
			{
				// If it isn't a value, skip it ...
				if(!is_value_control_point(*control_point))
					continue;
			}
	}

	bool selected(const unsigned long ControlPointIndex)
	{
		return m_selection.find(ControlPointIndex) != m_selection.end();
	}

	bool is_value_control_point(const unsigned long ControlPointIndex)
	{
		return 0 == ControlPointIndex % 3;
	}

	bool is_first_value_control_point(const unsigned long ControlPointIndex)
	{
		return 0 == ControlPointIndex;
	}

	bool is_last_value_control_point(const unsigned long ControlPointIndex)
	{
		return m_control_points.size() - 1 == ControlPointIndex;
	}

	unsigned long first_control_point()
	{
		return 0;
	}

	unsigned long last_control_point()
	{
		return m_control_points.size() - 1;
	}

	unsigned long previous_control_point(const unsigned long ControlPointIndex)
	{
		return ControlPointIndex ? ControlPointIndex - 1 : 0;
	}

	unsigned long next_control_point(const unsigned long ControlPointIndex)
	{
		return ControlPointIndex < m_control_points.size() - 1 ? ControlPointIndex + 1 : ControlPointIndex;
	}

	unsigned long neighbor_control_point(const unsigned long ControlPointIndex)
	{
		assert_warning(!is_value_control_point(ControlPointIndex));
		switch(ControlPointIndex % 3)
			{
				case 1:
					return ControlPointIndex + 1;
				case 2:
					return ControlPointIndex - 1;
			}

		// Should never be reached
		return_val_if_fail(0, 0);
		return ControlPointIndex;
	}

	unsigned long opposite_control_point(const unsigned long ControlPointIndex)
	{
		assert_warning(!is_value_control_point(ControlPointIndex));
		switch(ControlPointIndex % 3)
			{
				case 1:
					return (ControlPointIndex + m_control_points.size() - 3) % (m_control_points.size() - 1);
				case 2:
					return (ControlPointIndex + 2) % (m_control_points.size() - 1);
			}

		// Should never be reached
		return_val_if_fail(0, 0);
		return ControlPointIndex;
	}

	unsigned long nearest_value_control_point(const unsigned long ControlPointIndex)
	{
		switch(ControlPointIndex % 3)
			{
				case 0:
					return ControlPointIndex;
				case 1:
					return ControlPointIndex - 1;
				case 2:
					return ControlPointIndex + 1;
			}

		// Should never be reached
		return_val_if_fail(0, 0);
		return ControlPointIndex;
	}

	unsigned long previous_value_control_point(const unsigned long ControlPointIndex)
	{
		assert(is_value_control_point(ControlPointIndex));
		return ControlPointIndex ? ControlPointIndex - 3 : ControlPointIndex;
	}

	unsigned long next_value_control_point(const unsigned long ControlPointIndex)
	{
		assert(is_value_control_point(ControlPointIndex));
		return ControlPointIndex < m_control_points.size() - 1 ? ControlPointIndex + 3 : ControlPointIndex;
	}

	void deselect_all()
	{
		m_selection.clear();
	}

	control_point_set_t hit_test(const vector2 Mouse)
	{
		control_point_set_t results;

		for(unsigned long i = 0; i < m_control_points.size(); ++i)
			if((Mouse - screen(m_control_points[i])).Length() < 6.0)
				results.insert(i);

		return results;
	}

	/// Returns the rectangle that encloses all control points
	static const rectangle control_point_extents(const control_points_t ControlPoints)
	{
		rectangle result(DBL_MAX, -DBL_MAX, -DBL_MAX, DBL_MAX);

		// For each control point ...
		for(control_points_t::const_iterator control_point = ControlPoints.begin(); control_point != ControlPoints.end(); ++control_point)
			{
				result.left = std::min(result.left, (*control_point)[0]);
				result.top = std::max(result.top, (*control_point)[1]);
				result.right = std::max(result.right, (*control_point)[0]);
				result.bottom = std::min(result.bottom, (*control_point)[1]);
			}

		return result;
	}

	/// Returns the rectangle that encloses all value nodes (i.e. non-value control points may lie outside)
	static const rectangle value_extents(const control_points_t ControlPoints)
	{
		rectangle result(DBL_MAX, -DBL_MAX, -DBL_MAX, DBL_MAX);

		// For each value node ...
		for(unsigned int i = 0; i < ControlPoints.size(); i += 3)
			{
				result.left = std::min(result.left, ControlPoints[i][0]);
				result.top = std::max(result.top, ControlPoints[i][1]);
				result.right = std::max(result.right, ControlPoints[i][0]);
				result.bottom = std::min(result.bottom, ControlPoints[i][1]);
			}

		return result;
	}

	static void mouse_command(GtkWidget* Widget, const iuser_interface::mouse_action_t Action, const GdkModifierType Modifiers, const vector2 CurrentMouse)
	{
		// Sanity checks ...
		return_if_fail(Widget);

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


	/// Stores a reference to the owning object
	iobject& m_object;
	channel_t* const m_channel;

	// Zoom / pan parameters ...
	vector2 m_origin;
	vector2 m_scale;
	double m_vertical_scale;

	/// Stores the coordinates of the user interface cursor
	vector2 m_cursor;

	/// Stores the OpenGL context for the control curve graph
	sdpGtkOpenGLDrawingArea m_curve_widget;

	/// Caches the node control points
	control_points_t m_control_points;

	/// Keeps track of currently-selected nodes (by index)
	control_point_set_t m_selection;
	/// Keeps track of currently-active node
	control_points_t::iterator m_active_control_point;
};

void create_scalar_bezier_channel_properties(iobject& Object)
{
	new scalar_bezier_channel_properties(Object);
}

} // namespace k3d


