Blob Blame Raw
/* === S Y N F I G ========================================================= */
/*!	\file widget_timeslider.cpp
**	\brief Time Slider Widget Implementation File
**
**	$Id$
**
**	\legal
**	Copyright (c) 2004 Adrian Bentley
**	Copyright (c) 2007, 2008 Chris Moore
**	Copyright (c) 2012, Carlos López
**
**	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 <synfig/general.h>

#include <cmath>
#include <ETL/misc>

#include "widgets/widget_timeslider.h"
#include "app.h"

#include <gui/localization.h>

#endif

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

using namespace std;
using namespace etl;
using namespace synfig;

using studio::Widget_Timeslider;

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

/* === G L O B A L S ======================================================= */
const double zoominfactor = 0.75;
const double zoomoutfactor = 1/zoominfactor;

/* === P R O C E D U R E S ================================================= */

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

/* === E N T R Y P O I N T ================================================= */

const double defaultfps = 24;
const int fullheight = 20;

Widget_Timeslider::Widget_Timeslider():
	layout(Pango::Layout::create(get_pango_context())),
	adj_default(Gtk::Adjustment::create(0,0,2,1/defaultfps,10/defaultfps)),
	adj_timescale(),
	time_per_tickmark(0),
	//invalidated(false),
	last_event_time(0),
	fps(defaultfps),
	dragscroll(false),
	lastx(0)
{
	set_size_request(-1, fullheight);

	// click / scroll / zoom
	add_events(
		Gdk::BUTTON_PRESS_MASK   |
		Gdk::BUTTON_RELEASE_MASK |
		Gdk::BUTTON_MOTION_MASK  | 
		Gdk::SCROLL_MASK
	);

	set_time_adjustment(adj_default);
	//update_times();
}

Widget_Timeslider::~Widget_Timeslider()
{
}

void Widget_Timeslider::set_time_adjustment(const Glib::RefPtr<Gtk::Adjustment> &x)
{
	if (adj_timescale == x) return;

	//disconnect old connections
	time_value_change.disconnect();
	time_other_change.disconnect();

	//connect update function to new adjustment
	adj_timescale = x;

	if (adj_timescale) {
		time_value_change = adj_timescale->signal_value_changed().connect(sigc::mem_fun(*this,&Widget_Timeslider::queue_draw));
		time_other_change = adj_timescale->signal_changed().connect(sigc::mem_fun(*this,&Widget_Timeslider::queue_draw));
		//invalidated = true;
		//refresh();
	}
}

void Widget_Timeslider::set_global_fps(float d)
{
	if(fps != d)
	{
		fps = d;

		//update everything since we need to redraw already
		//invalidated = true;
		//refresh();
		queue_draw();
	}
}

/*void Widget_Timeslider::update_times()
{
	if(adj_timescale)
	{
		start = adj_timescale->get_lower();
		end = adj_timescale->get_upper();
		current = adj_timescale->get_value();
	}
}*/

void Widget_Timeslider::refresh()
{
}
/*
{
	if(invalidated)
	{
		queue_draw();
	}else if(adj_timescale)
	{
		double 	l = adj_timescale->get_lower(),
				u = adj_timescale->get_upper(),
				v = adj_timescale->get_value();

		bool invalid = (l != start) || (u != end) || (v != current);

		start = l;
		end = u;
		current = v;

		if(invalid) queue_draw();
	}
}*/

void Widget_Timeslider::draw_background(const Cairo::RefPtr<Cairo::Context> &cr)
{
	//draw grey rectangle
	cr->save();
	cr->set_source_rgb(0.5, 0.5, 0.5);
	cr->rectangle(0.0, 0.0, (double)get_width(), (double)get_height());
	cr->fill();
	cr->restore();
}

bool Widget_Timeslider::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
{
	const double EPSILON = 1e-6;

	draw_background(cr);

	if(!adj_timescale || get_width() == 0 || get_height() == 0) return true;

	//Get the time information since we now know it's valid
	double 	start   = adj_timescale->get_lower(),
			end     = adj_timescale->get_upper(),
			current = adj_timescale->get_value();

	if(end-start < EPSILON) return true;

	//draw all the time stuff
	double dtdp = (end - start)/get_width();
	double dpdt = 1/dtdp;

	//lines


	//Draw the time line...
	double tpx = round_to_int((current-start)*dpdt)+0.5;
	cr->save();
	cr->set_source_rgb(1.0, 175.0/255.0, 0.0);
	cr->set_line_width(1.0);
	cr->move_to(tpx, 0.0);
	cr->line_to(tpx, fullheight);
	cr->stroke();
	cr->restore();

	// Calculate the line intervals
	int ifps = round_to_int(fps);
	if (ifps < 1) ifps = 1;

	std::vector<double> ranges;

	//unsigned int pos = 0;

	// build a list of all the factors of the frame rate
	for (int i = 1, pos = 0; i*i <= ifps; i++)
		if ((ifps%i) == 0)
		{
			ranges.insert(ranges.begin()+pos, i/fps);
			if (i*i != ifps)
				ranges.insert(ranges.begin()+pos+1, ifps/i/fps);
			pos++;
		}

	// fill in any gaps where one factor is more than 2 times the previous
	std::vector<double>::iterator iter, next;
	
	for (unsigned int pos = 0; pos < ranges.size()-1; pos++)
	{
		iter = ranges.begin()+pos;
		next = iter+1;
		if (*iter*2 < *next)
			ranges.insert(next, *iter*2);
	}

	double more_ranges[] = {
		2, 3, 5, 10, 20, 30, 60, 90, 120, 180,
		300, 600, 1200, 1800, 2700, 3600, 3600*2,
		3600*4, 3600*8, 3600*16, 3600*32, 3600*64,
		3600*128, 3600*256, 3600*512, 3600*1024 };

	ranges.insert(ranges.end(), more_ranges, more_ranges + sizeof(more_ranges)/sizeof(double));

	double lowerrange = dtdp*140, upperrange = dtdp*280;
	double midrange = (lowerrange + upperrange)/2;

	//find most ideal scale
	double scale;
	next = binary_find(ranges.begin(), ranges.end(), midrange);
	iter = next++;

	if (iter == ranges.end()) iter--;
	if (next == ranges.end()) next--;

	if (abs(*next - midrange) < abs(*iter - midrange))
		iter = next;

	scale = *iter;

	// subdivide into this many tick marks (8 or less)
	int subdiv = round_to_int(scale * ifps);

	if (subdiv > 8)
	{
		const int ideal = subdiv;

		// find a number of tick marks that nicely divides the scale
		// (5 minutes divided by 6 is 50s, but that's not 'nice' -
		//  5 ticks of 1m each is much simpler than 6 ticks of 50s)
		for (subdiv = 8; subdiv > 0; subdiv--)
			if ((ideal <= ifps*2       && (ideal % (subdiv           )) == 0) ||
				(ideal <= ifps*2*60    && (ideal % (subdiv*ifps      )) == 0) ||
				(ideal <= ifps*2*60*60 && (ideal % (subdiv*ifps*60   )) == 0) ||
				(true                  && (ideal % (subdiv*ifps*60*60)) == 0))
				break;

		// if we didn't find anything, use 4 ticks
		if (!subdiv)
			subdiv = 4;
	}

	time_per_tickmark = scale / subdiv;

	//get first valid line and its position in pixel space
	double time = 0;
	double pixel = 0;

	int sdindex = 0;

	double subr = scale / subdiv;

	//get its position inside...
	time = ceil(start/subr)*subr - start;
	pixel = time*dpdt;

	//absolute time of the line to be drawn
	time += start;

	{ //inside the big'n
		double t = (time/scale - floor(time/scale))*subdiv; // the difference from the big mark in 0:1
		//sdindex = (int)floor(t + 0.5); //get how far through the range it is...
		sdindex = round_to_int(t); //get how far through the range it is...
		if (sdindex == subdiv) sdindex = 0;

		//synfig::info("Extracted fr %.2lf -> %d", t, sdindex);
	}

	//synfig::info("Initial values: %.4lf t, %.1lf pixels, %d i", time,pixel,sdindex);

	//loop to draw
	const double heightbig = 12;
	const double heightsmall = 4;

	// Draw the lines and timecode
	//normal line/text color
	cr->save();
	cr->set_source_rgb(51.0/255.0,51.0/255.0,51.0/255.0);
	cr->set_line_width(1.0);

	int width = get_width();
	while( pixel < width )
	{
		double xpx = round_to_int(pixel)+0.5;

		//draw big
		if(sdindex == 0)
		{
			cr->move_to(xpx,0);
			cr->line_to(xpx,heightbig);
			cr->stroke();
			//round the time to nearest frame and draw the text
			Time tm((double)time);
			if(get_global_fps()) tm.round(get_global_fps());
			Glib::ustring timecode(tm.get_string(get_global_fps(),App::get_time_format()));

			layout->set_text(timecode);
			Pango::AttrList attr_list;
			// Aproximately a font size of 8 pixels.
			// Pango::SCALE = 1024
			// create_attr_size waits a number in 1000th of pixels.
			// Should be user customizable in the future. Now it is fixed to 10
			Pango::AttrInt pango_size(Pango::Attribute::create_attr_size(Pango::SCALE*10));
			pango_size.set_start_index(0);
			pango_size.set_end_index(64);
			attr_list.change(pango_size);
			layout->set_attributes(attr_list);
			cr->move_to(xpx+1.0,0);
			layout->show_in_cairo_context(cr);
		}else
		{
			cr->move_to(xpx,0);
			cr->line_to(xpx,heightsmall);
			cr->stroke();
		}

		//increment time and position
		pixel += subr / dtdp;
		time += subr;

		//increment index
		if(++sdindex >= subdiv) sdindex -= subdiv;
	}
	cr->restore();

	//Draw the time line afer all
	Gdk::RGBA c("#ffaf00");
	cr->set_source_rgb(c.get_red(), c.get_green(), c.get_blue());
	cr->set_line_width(3);
	tpx = (current-start)*dpdt;
	cr->move_to(round_to_int(tpx),0);
	cr->line_to(round_to_int(tpx),fullheight);
	cr->stroke();

	return true;
}

bool Widget_Timeslider::on_motion_notify_event(GdkEventMotion* event) //for dragging
{
	if(!adj_timescale) return false;

	Gdk::ModifierType mod = Gdk::ModifierType(event->state);

	//scrolling...

	//NOTE: we might want to address the possibility of dragging with both buttons held down

	if(mod & Gdk::BUTTON2_MASK)
	{

		//we need this for scrolling by dragging
		double 	curx = event->x;

		double 	start = adj_timescale->get_lower(),
				end = adj_timescale->get_upper();

		if(dragscroll)
		{
			if(event->time-last_event_time<30)
				return false;
			else
				last_event_time=event->time;

			if(abs(lastx - curx) < 1 && end != start) return true;
			//translate the window and correct it

			//update our stuff so we are operating correctly
			//invalidated = true;
			//update_times();

			//Note: Use inverse of mouse movement because of conceptual space relationship
			double diff = lastx - curx; //curx - lastx;

			//NOTE: This might be incorrect...
			//fraction to move...
			double dpx = (end - start)/get_width();
			lastx = curx;

			diff *= dpx;

			//Adjust...
			start += diff;
			end += diff;

			//But clamp to bounds if they exist...
			//HACK - bounds should not be required for this slider
			if (adj_bounds) {
				GlibFreezeNotify freeze(adj_bounds);
				if(start < adj_bounds->get_lower()) {
					diff = adj_bounds->get_lower() - start;
					start += diff;
					end += diff;
				}
				if(end > adj_bounds->get_upper()) {
					diff = adj_bounds->get_upper() - end;
					start += diff;
					end += diff;
				}
			}

			//synfig::info("Scrolling timerange to (%.4f,%.4f)",start,end);

			GlibFreezeNotify freeze(adj_timescale);
			adj_timescale->set_lower(start);
			adj_timescale->set_upper(end);
		}else
		{
			dragscroll = true;
			lastx = curx;
			//lasty = cury;
		}

		return true;
	}

	if(mod & Gdk::BUTTON1_MASK)
	{
		double curx = event->x;

		//get time from drag...
		double start = adj_timescale->get_lower();
		double end = adj_timescale->get_upper();
		double current = adj_timescale->get_value();
		Time t(start + curx*(end - start)/get_width());

		//snap it to fps - if they exist...
		if (fps) t = t.round(fps);

		//set time if needed
		if (current != t)
			adj_timescale->set_value((double)t);

		return true;
	}

	return false;
}

bool Widget_Timeslider::on_scroll_event(GdkEventScroll* event) //for zooming
{
	if(!adj_timescale) return false;

	//Update so we are calculating based on current values
	//update_times();

	//figure out if we should center ourselves on the current time
	bool center = false;

	//we want to zoom in on the time value if control is held down
	if(Gdk::ModifierType(event->state) & Gdk::CONTROL_MASK)
		center = true;

	switch(event->direction)
	{
		case GDK_SCROLL_UP: //zoom in
			zoom_in(center);
			return true;

		case GDK_SCROLL_DOWN: //zoom out
			zoom_out(center);
			return true;

		case GDK_SCROLL_RIGHT:
		case GDK_SCROLL_LEFT:
		{
			double t = adj_timescale->get_value();
			double orig_t = t;
			double start = adj_timescale->get_lower();
			double end = adj_timescale->get_upper();
			double lower = adj_bounds->get_lower();
			double upper = adj_bounds->get_upper();
			double adj = time_per_tickmark;

			if( event->direction == GDK_SCROLL_RIGHT )
			{
				// step forward one tick
				t += adj;

				// don't go past the end of time
				if (t > upper)
					t = upper;

				// if we are already in the right half of the slider
				if ((t-start)*2 > (end-start))
				{
					// if we can't scroll the background left one whole tick, scroll it to the end
					if (end > upper - (t-orig_t)) {
						GlibFreezeNotify freeze(adj_timescale);
						adj_timescale->set_lower(upper - (end-start));
						adj_timescale->set_upper(upper);
					} else {
						// else scroll the background left
						GlibFreezeNotify freeze(adj_timescale);
						adj_timescale->set_lower(start + (t-orig_t));
						adj_timescale->set_upper(start + (t-orig_t) + (end-start));
					}
				}
			}
			else
			{
				// step backwards one tick
				t -= adj;

				// don't go past the start of time
				if (t < lower)
					t = lower;

				// if we are already in the left half of the slider
				if ((t-start)*2 < (end-start))
				{
					// if we can't scroll the background right one whole tick, scroll it to the beginning
					if (start < lower + (orig_t-t)) {
						GlibFreezeNotify freeze(adj_timescale);
						adj_timescale->set_lower(lower);
						adj_timescale->set_upper(lower + (end-start));
					} else {
						// else scroll the background left
						GlibFreezeNotify freeze(adj_timescale);
						adj_timescale->set_lower(start - (orig_t-t));
						adj_timescale->set_upper(start - (orig_t-t) + (end-start));
					}
				}
			}

			if (adj_timescale)
				adj_timescale->set_value(t);

			return true;
		}
		default:
			return false;
	}
}

void Widget_Timeslider::zoom_in(bool centerontime)
{
	if(!adj_timescale) return;

	double 	start = adj_timescale->get_lower(),
			end = adj_timescale->get_upper(),
			current = adj_timescale->get_value();

	double focuspoint = centerontime ? current : (start + end)/2;

	//calculate new beginning and end
	end = focuspoint + (end-focuspoint)*zoominfactor;
	start = focuspoint + (start-focuspoint)*zoominfactor;

	//synfig::info("Zooming in timerange to (%.4f,%.4f)",start,end);
	if (adj_bounds) {
		GlibFreezeNotify freeze(adj_bounds);
		if(start < adj_bounds->get_lower())
			start = adj_bounds->get_lower();
		if(end > adj_bounds->get_upper())
			end = adj_bounds->get_upper();
	}

	//reset values
	GlibFreezeNotify freeze(adj_timescale);
	adj_timescale->set_lower(start);
	adj_timescale->set_upper(end);
}

void Widget_Timeslider::zoom_out(bool centerontime)
{
	if(!adj_timescale) return;

	double 	start = adj_timescale->get_lower(),
			end = adj_timescale->get_upper(),
			current = adj_timescale->get_value();

	double focuspoint = centerontime ? current : (start + end)/2;

	//calculate new beginning and end
	end = focuspoint + (end-focuspoint)*zoomoutfactor;
	start = focuspoint + (start-focuspoint)*zoomoutfactor;

	//synfig::info("Zooming out timerange to (%.4f,%.4f)",start,end);
	if(adj_bounds) {
		GlibFreezeNotify freeze(adj_bounds);
		if(start < adj_bounds->get_lower())
			start = adj_bounds->get_lower();
		if(end > adj_bounds->get_upper())
			end = adj_bounds->get_upper();
	}

	//reset values
	GlibFreezeNotify freeze(adj_timescale);
	adj_timescale->set_lower(start);
	adj_timescale->set_upper(end);
}

bool Widget_Timeslider::on_button_press_event(GdkEventButton *event) //for clicking
{
	switch(event->button)
	{
		//time click...
		case 1:
		{
			double start   = adj_timescale->get_lower();
			double end     = adj_timescale->get_upper();
			double current = adj_timescale->get_value();
			double w = get_width();
			Time t(start + (end - start)*event->x/w);
			if (fps) t = t.round(fps);

			/*synfig::info("Clicking time from %.3lf to %.3lf [(%.2lf,%.2lf) %.2lf / %.2lf ... %.2lf",
						current, vt, start, end, event->x, w, fps);*/

			if (t != Time(current))
				if(adj_timescale)
					adj_timescale->set_value((double)t);

			break;
		}

		//scroll click
		case 2:
		{
			//start dragging
			dragscroll = true;
			lastx = event->x;
			//lasty = event->y;

			return true;
		}

		default:
		{
			break;
		}
	}

	return false;
}

bool Widget_Timeslider::on_button_release_event(GdkEventButton *event) //end drag
{
	switch(event->button)
	{
		case 2:
		{
			//start dragging
			dragscroll = false;
			return true;
		}

		default:
		{
			break;
		}
	}

	return false;
}