Blob Blame Raw
/* === S Y N F I G ========================================================= */
/*!	\file state_width.cpp
**	\brief Template File
**
**	$Id$
**
**	\legal
**	Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
**  Copyright (c) 2008 Chris Moore
**
**	This package 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 package 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.
**	\endlegal
*/
/* ========================================================================= */

/* === H E A D E R S ======================================================= */

#ifdef USING_PCH
#	include "pch.h"
#else
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include <gtkmm/dialog.h>
#include <gtkmm/entry.h>

#include <ETL/bezier>

#include <synfig/valuenode_dynamiclist.h>
#include <synfigapp/action_system.h>

#include "state_width.h"
#include "state_normal.h"
#include "canvasview.h"
#include "workarea.h"
#include "app.h"

#include <synfigapp/action.h>
#include "event_mouse.h"
#include "event_layerclick.h"
#include "toolbox.h"
#include "dialog_tooloptions.h"
#include <gtkmm/optionmenu.h>
#include "duck.h"

//#include <synfigapp/value_desc.h>
#include <synfigapp/main.h>

#include <ETL/clock>

#include "general.h"

#endif

/* === U S I N G =========================================================== */

using namespace std;
using namespace etl;
using namespace synfig;
using namespace synfigapp;
using namespace studio;

/* === M A C R O S ========================================================= */

/* === G L O B A L S ======================================================= */

StateWidth studio::state_width;

/* === C L A S S E S & S T R U C T S ======================================= */

class studio::StateWidth_Context : public sigc::trackable
{
	etl::handle<CanvasView> canvas_view_;
	CanvasView::IsWorking is_working;

	//Point mouse_pos;

	handle<Duck> center;
	handle<Duck> radius;
	handle<Duck> closestpoint;

	map<handle<Duck>,Real>	changetable;

	etl::clock	clocktime;
	Real		lastt;

	bool added;

	void refresh_ducks();

	bool prev_workarea_layer_clicking;
	bool prev_workarea_duck_clicking;
	Duckmatic::Type old_duckmask;

	//Toolbox settings
	synfigapp::Settings& settings;

	//Toolbox display
	Gtk::Table options_table;

	Gtk::Adjustment	adj_delta;
	Gtk::SpinButton	spin_delta;

	Gtk::Adjustment	adj_radius;
	Gtk::SpinButton	spin_radius;

	Gtk::CheckButton check_relative;

	void AdjustWidth(handle<Duckmatic::Bezier> c, float t, Real mult, bool invert);

public:

	Real get_delta()const { return adj_delta.get_value(); }
	void set_delta(Real f) { adj_delta.set_value(f); }

	Real get_radius()const { return adj_radius.get_value(); }
	void set_radius(Real f) { adj_radius.set_value(f); }

	bool get_relative() const { return check_relative.get_active(); }
	void set_relative(bool r) { check_relative.set_active(r); }

	void refresh_tool_options(); //to refresh the toolbox

	//events
	Smach::event_result event_stop_handler(const Smach::event& x);
	Smach::event_result event_refresh_handler(const Smach::event& x);
	Smach::event_result event_mouse_handler(const Smach::event& x);
	Smach::event_result event_refresh_tool_options(const Smach::event& x);

	//constructor destructor
	StateWidth_Context(CanvasView* canvas_view);
	~StateWidth_Context();

	//Canvas interaction
	const etl::handle<CanvasView>& get_canvas_view()const{return canvas_view_;}
	etl::handle<synfigapp::CanvasInterface> get_canvas_interface()const{return canvas_view_->canvas_interface();}
	synfig::Canvas::Handle get_canvas()const{return canvas_view_->get_canvas();}
	WorkArea * get_work_area()const{return canvas_view_->get_work_area();}

	//Modifying settings etc.
	void load_settings();
	void save_settings();
	void reset();

};	// END of class StateWidth_Context

/* === M E T H O D S ======================================================= */

StateWidth::StateWidth():
	Smach::state<StateWidth_Context>("width")
{
	insert(event_def(EVENT_STOP,&StateWidth_Context::event_stop_handler));
	insert(event_def(EVENT_REFRESH,&StateWidth_Context::event_refresh_handler));
	insert(event_def(EVENT_WORKAREA_MOUSE_BUTTON_DOWN,&StateWidth_Context::event_mouse_handler));
	insert(event_def(EVENT_WORKAREA_MOUSE_BUTTON_DRAG,&StateWidth_Context::event_mouse_handler));
	insert(event_def(EVENT_WORKAREA_MOUSE_BUTTON_UP,&StateWidth_Context::event_mouse_handler));
	insert(event_def(EVENT_REFRESH_TOOL_OPTIONS,&StateWidth_Context::event_refresh_tool_options));
}

StateWidth::~StateWidth()
{
}

void
StateWidth_Context::load_settings()
{
	String value;

	//parse the arguments yargh!
	if(settings.get_value("width.delta",value))
		set_delta(atof(value.c_str()));
	else
		set_delta(6);

	if(settings.get_value("width.radius",value))
		set_radius(atof(value.c_str()));
	else
		set_radius(15);

	//defaults to false
	if(settings.get_value("width.relative",value) && value == "1")
		set_relative(true);
	else
		set_relative(false);
}

void
StateWidth_Context::save_settings()
{
	settings.set_value("width.delta",strprintf("%f",get_delta()));
	settings.set_value("width.radius",strprintf("%f",get_radius()));
	settings.set_value("width.relative",get_relative()?"1":"0");
}

void
StateWidth_Context::reset()
{
	refresh_ducks();
}

StateWidth_Context::StateWidth_Context(CanvasView* canvas_view):
	canvas_view_(canvas_view),
	is_working(*canvas_view),
	prev_workarea_layer_clicking(get_work_area()->get_allow_layer_clicks()),
	prev_workarea_duck_clicking(get_work_area()->get_allow_duck_clicks()),
	old_duckmask(get_work_area()->get_type_mask()),

	settings(synfigapp::Main::get_selected_input_device()->settings()),

	adj_delta(6,0,20,0.01,0.1),
	spin_delta(adj_delta,0.01,3),

	adj_radius(200,0,1e50,1,10),
	spin_radius(adj_radius,1,1),

	check_relative(_("Relative Growth"))
{
	load_settings();

	// Set up the tool options dialog
	options_table.attach(*manage(new Gtk::Label(_("Width Tool"))),	0, 2, 0, 1, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);

	//expand stuff
	options_table.attach(*manage(new Gtk::Label(_("Growth:"))),		0, 1, 1, 2, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);
	options_table.attach(spin_delta,								1, 2, 1, 2, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);

	options_table.attach(*manage(new Gtk::Label(_("Radius:"))),		0, 1, 2, 3, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);
	options_table.attach(spin_radius,								1, 2, 2, 3, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);

	options_table.attach(check_relative,							0, 2, 3, 4, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);

	options_table.show_all();

	refresh_tool_options();
	App::dialog_tool_options->present();

	// Turn off layer clicking
	get_work_area()->set_allow_layer_clicks(false);

	// clear out the ducks
	//get_work_area()->clear_ducks();

	// Refresh the work area
	get_work_area()->queue_draw();

	//Create the new ducks
	added = false;

	if(!center)
	{
		center = new Duck();
		center->set_name("p1");
		center->set_type(Duck::TYPE_POSITION);
	}

	if(!radius)
	{
		radius = new Duck();
		radius->set_origin(center);
		radius->set_radius(true);
		radius->set_type(Duck::TYPE_RADIUS);
		radius->set_name("radius");
	}

	if(!closestpoint)
	{
		closestpoint = new Duck();
		closestpoint->set_name("closest");
		closestpoint->set_type(Duck::TYPE_POSITION);
	}

	//Disable duck clicking for the maximum coolness :)
	get_work_area()->set_allow_duck_clicks(false);
	get_work_area()->set_type_mask((Duck::Type)((int)Duck::TYPE_WIDTH + (int)Duck::TYPE_RADIUS));

	// Turn the mouse pointer to crosshairs
	get_work_area()->set_cursor(Gdk::CROSSHAIR);

	// Hide the tables if they are showing
	//prev_table_status=get_canvas_view()->tables_are_visible();
	//if(prev_table_status)get_canvas_view()->hide_tables();

	// Disable the time bar
	//get_canvas_view()->set_sensitive_timebar(false);

	// Connect a signal
	//get_work_area()->signal_user_click().connect(sigc::mem_fun(*this,&studio::StateWidth_Context::on_user_click));

	App::toolbox->refresh();
}

void
StateWidth_Context::refresh_tool_options()
{
	App::dialog_tool_options->clear();
	App::dialog_tool_options->set_widget(options_table);
	App::dialog_tool_options->set_local_name(_("Width Tool"));
	App::dialog_tool_options->set_name("width");
}

Smach::event_result
StateWidth_Context::event_refresh_tool_options(const Smach::event& /*x*/)
{
	refresh_tool_options();
	return Smach::RESULT_ACCEPT;
}

StateWidth_Context::~StateWidth_Context()
{
	save_settings();

	//remove ducks if need be
	if(added)
	{
		get_work_area()->erase_duck(center);
		get_work_area()->erase_duck(radius);
		get_work_area()->erase_duck(closestpoint);
		added = false;
	}

	// Restore Duck clicking
	get_work_area()->set_allow_duck_clicks(prev_workarea_duck_clicking);

	// Restore layer clicking
	get_work_area()->set_allow_layer_clicks(prev_workarea_layer_clicking);

	// Restore the mouse pointer
	get_work_area()->reset_cursor();

	// Restore duck masking
	get_work_area()->set_type_mask(old_duckmask);

	// Tool options be rid of ye!!
	App::dialog_tool_options->clear();

	// Enable the time bar
	//get_canvas_view()->set_sensitive_timebar(true);

	// Bring back the tables if they were out before
	//if(prev_table_status)get_canvas_view()->show_tables();

	// Refresh the work area
	get_work_area()->queue_draw();

	App::toolbox->refresh();
}

Smach::event_result
StateWidth_Context::event_stop_handler(const Smach::event& /*x*/)
{
	//throw Smach::egress_exception();
	throw &state_normal;
	return Smach::RESULT_OK;
}

Smach::event_result
StateWidth_Context::event_refresh_handler(const Smach::event& /*x*/)
{
	refresh_ducks();
	return Smach::RESULT_ACCEPT;
}

void
StateWidth_Context::AdjustWidth(handle<Duckmatic::Bezier> c, float t, Real mult, bool invert)
{
	//Leave the function if there is no curve
	if(!c)return;

	Real amount1=0,amount2=0;

	//decide how much to change each width
	/*
	t \in [0,1]

	both pressure and multiply amount are in mult
		(may want to change this to allow different types of falloff)

	rsq is the squared distance from the point on the curve (also part of the falloff)


	*/
	//may want to provide a different falloff function...
	if(t <= 0.2)
		amount1 = mult;
	else if(t >= 0.8)
		amount2 = mult;
	else
	{
		t = (t-0.2)/0.6;
		amount1 = (1-t)*mult;
		amount2 = t*mult;
	}

	if(invert)
	{
		amount1 *= -1;
		amount2 *= -1;
	}

	handle<Duck>	p1 = c->p1;
	handle<Duck>	p2 = c->p2;

	handle<Duck>	w1,w2;

	//find w1,w2
	{
		const DuckList dl = get_work_area()->get_duck_list();

		DuckList::const_iterator i = dl.begin();

		for(;i != dl.end(); ++i)
		{
			if((*i)->get_type() == Duck::TYPE_WIDTH)
			{
				if((*i)->get_origin_duck() == p1)
				{
					w1 = *i;
				}

				if((*i)->get_origin_duck() == p2)
				{
					w2 = *i;
				}
			}
		}
	}

	if(amount1 != 0 && w1)
	{
		Real width = w1->get_point().mag();

		width += amount1;
		w1->set_point(Vector(width,0));

		//log in the list of changes...
		//to truly be changed after everything is said and done
		changetable[w1] = width;
	}

	if(amount2 != 0 && w2)
	{
		Real width = w2->get_point().mag();

		width += amount2;
		w2->set_point(Vector(width,0));

		//log in the list of changes...
		//to truly be changed after everything is said and done
		changetable[w2] = width;
	}
}

Smach::event_result
StateWidth_Context::event_mouse_handler(const Smach::event& x)
{
	const EventMouse& event(*reinterpret_cast<const EventMouse*>(&x));

	//handle the click
	if( (event.key == EVENT_WORKAREA_MOUSE_BUTTON_DOWN || event.key == EVENT_WORKAREA_MOUSE_BUTTON_DRAG)
			&& event.button == BUTTON_LEFT )
	{
		const Real pw = get_work_area()->get_pw();
		const Real ph = get_work_area()->get_ph();
		const Real scale = sqrt(pw*pw+ph*ph);
		const Real rad = get_relative() ? scale * get_radius() : get_radius();

		bool invert = (event.modifier&Gdk::CONTROL_MASK);

		const Real threshold = 0.08;

		float t = 0;
		Real rsq = 0;

		Real dtime = 1/60.0;

		//if we're dragging get the difference in time between now and then
		if(event.key == EVENT_WORKAREA_MOUSE_BUTTON_DRAG)
		{
			dtime = min(1/15.0,clocktime());
		}
		clocktime.reset();

		//make way for new ducks
		//get_work_area()->clear_ducks();

		//update positions
		//mouse_pos = event.pos;

		center->set_point(event.pos);
		if(!added)get_work_area()->add_duck(center);

		radius->set_scalar(rad);
		if(!added)get_work_area()->add_duck(radius);

		//the other duck is at the current duck
		closestpoint->set_point(event.pos);
		if(!added)get_work_area()->add_duck(closestpoint);

		//get the closest curve...
		handle<Duckmatic::Bezier>	c;
		if(event.pressure >= threshold)
			c = get_work_area()->find_bezier(event.pos,scale*8,rad,&t);

		//run algorithm on event.pos to get 2nd placement
		if(!c.empty())
		{
			bezier<Point> curve;
			Point p;

			curve[0] = c->p1->get_trans_point();
			curve[1] = c->c1->get_trans_point();
			curve[2] = c->c2->get_trans_point();
			curve[3] = c->p2->get_trans_point();

			p = curve(t);
			rsq = (p-event.pos).mag_squared();

			const Real r = rad*rad;

			if(rsq < r)
			{
				closestpoint->set_point(curve(t));

				//adjust the width...
				//squared falloff for radius... [0,1]

				Real ri = (r - rsq)/r;
				AdjustWidth(c,t,ri*event.pressure*get_delta()*dtime,invert);
			}
		}

		//the points have been added
		added = true;

		//draw where it is yo!
		get_work_area()->queue_draw();

		return Smach::RESULT_ACCEPT;
	}

	if(event.key == EVENT_WORKAREA_MOUSE_BUTTON_UP && event.button == BUTTON_LEFT)
	{
		if(added)
		{
			get_work_area()->erase_duck(center);
			get_work_area()->erase_duck(radius);
			get_work_area()->erase_duck(closestpoint);
			added = false;
		}

		//Affect the width changes here...
		map<handle<Duck>,Real>::iterator i = changetable.begin();

		synfigapp::Action::PassiveGrouper group(get_canvas_interface()->get_instance().get(),_("Sketch Width"));
		for(; i != changetable.end(); ++i)
		{
			//for each duck modify IT!!!
			ValueDesc desc = i->first->get_value_desc();

			if(	desc.get_value_type() == ValueBase::TYPE_REAL )
			{
				Action::Handle action(Action::create("ValueDescSet"));
				assert(action);

				action->set_param("canvas",get_canvas());
				action->set_param("canvas_interface",get_canvas_interface());

				action->set_param("value_desc",desc);
				action->set_param("new_value",ValueBase(i->second));
				action->set_param("time",get_canvas_view()->get_time());

				if(!action->is_ready() || !get_canvas_view()->get_instance()->perform_action(action))
				{
					group.cancel();
					synfig::warning("Changing the width action has failed");
					return Smach::RESULT_ERROR;
				}
			}
		}

		changetable.clear();

		get_work_area()->queue_draw();

		return Smach::RESULT_ACCEPT;
	}

	return Smach::RESULT_OK;
}


void
StateWidth_Context::refresh_ducks()
{
	get_work_area()->clear_ducks();
	get_work_area()->queue_draw();
}