1 /* === S Y N F I G ========================================================= */
2 /*! \file widget_timeslider.cpp
3 ** \brief Time Slider Widget Implementation File
8 ** Copyright (c) 2004 Adrian Bentley
9 ** Copyright (c) 2007 Chris Moore
11 ** This package is free software; you can redistribute it and/or
12 ** modify it under the terms of the GNU General Public License as
13 ** published by the Free Software Foundation; either version 2 of
14 ** the License, or (at your option) any later version.
16 ** This package is distributed in the hope that it will be useful,
17 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 ** General Public License for more details.
22 /* ========================================================================= */
24 /* === H E A D E R S ======================================================= */
33 #include "widget_timeslider.h"
43 /* === U S I N G =========================================================== */
47 using namespace synfig;
49 using studio::Widget_Timeslider;
51 /* === M A C R O S ========================================================= */
53 /* === G L O B A L S ======================================================= */
54 const double zoominfactor = 0.75;
55 const double zoomoutfactor = 1/zoominfactor;
57 /* === P R O C E D U R E S ================================================= */
59 Gdk::Color get_interp_color(synfig::Interpolation x)
63 case INTERPOLATION_TCB:
64 return Gdk::Color("#00B000");
68 case INTERPOLATION_LINEAR:
69 return Gdk::Color("#B0B000");
72 case INTERPOLATION_CONSTANT:
73 return Gdk::Color("#C70000");
76 case INTERPOLATION_HALT:
77 return Gdk::Color("#00b0b0");
80 case INTERPOLATION_MANUAL:
81 return Gdk::Color("#B000B0");
84 case INTERPOLATION_UNDEFINED: default:
85 return Gdk::Color("#808080");
91 color_darken(Gdk::Color x, float amount)
93 double red = x.get_red_p() * amount;
94 double green = x.get_green_p() * amount;
95 double blue = x.get_blue_p() * amount;
97 x.set_rgb_p( red > 1 ? 1 : red,
98 green > 1 ? 1 : green,
105 studio::render_time_point_to_window(
106 const Glib::RefPtr<Gdk::Drawable>& window,
107 const Gdk::Rectangle& area,
108 const synfig::TimePoint &tp,
112 Glib::RefPtr<Gdk::GC> gc(Gdk::GC::create(window));
113 const Gdk::Color black("#000000");
116 gc->set_line_attributes(2,Gdk::LINE_SOLID,Gdk::CAP_BUTT,Gdk::JOIN_MITER);
118 gc->set_line_attributes(1,Gdk::LINE_SOLID,Gdk::CAP_BUTT,Gdk::JOIN_MITER);
121 std::vector<Gdk::Point> points;
123 /*- BEFORE ------------------------------------- */
125 color=get_interp_color(tp.get_before());
126 color=color_darken(color,1.0f);
127 if(selected)color=color_darken(color,1.3f);
128 gc->set_rgb_fg_color(color);
130 switch(tp.get_before())
132 case INTERPOLATION_TCB:
143 gc->set_rgb_fg_color(black);
156 case INTERPOLATION_HALT:
167 gc->set_rgb_fg_color(black);
180 case INTERPOLATION_LINEAR:
182 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()));
183 points.push_back(Gdk::Point(area.get_x(),area.get_y()+area.get_height()));
184 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()+area.get_height()));
185 window->draw_polygon(gc,true,points);
186 gc->set_rgb_fg_color(black);
187 window->draw_lines(gc,points);
190 case INTERPOLATION_CONSTANT:
192 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()));
193 points.push_back(Gdk::Point(area.get_x()+area.get_width()/4,area.get_y()));
194 points.push_back(Gdk::Point(area.get_x()+area.get_width()/4,area.get_y()+area.get_height()/2));
195 points.push_back(Gdk::Point(area.get_x(),area.get_y()+area.get_height()/2));
196 points.push_back(Gdk::Point(area.get_x(),area.get_y()+area.get_height()));
197 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()+area.get_height()));
198 window->draw_polygon(gc,true,points);
199 gc->set_rgb_fg_color(black);
200 window->draw_lines(gc,points);
203 case INTERPOLATION_UNDEFINED: default:
205 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()));
206 points.push_back(Gdk::Point(area.get_x()+area.get_width()/3,area.get_y()));
207 points.push_back(Gdk::Point(area.get_x(),area.get_y()+area.get_height()/3));
208 points.push_back(Gdk::Point(area.get_x(),area.get_y()+area.get_height()-area.get_height()/3));
209 points.push_back(Gdk::Point(area.get_x()+area.get_width()/3,area.get_y()+area.get_height()));
210 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()+area.get_height()));
211 window->draw_polygon(gc,true,points);
212 gc->set_rgb_fg_color(black);
213 window->draw_lines(gc,points);
217 /*- AFTER -------------------------------------- */
219 color=get_interp_color(tp.get_after());
220 color=color_darken(color,0.8f);
221 if(selected)color=color_darken(color,1.3f);
222 gc->set_rgb_fg_color(color);
225 switch(tp.get_after())
227 case INTERPOLATION_TCB:
238 gc->set_rgb_fg_color(black);
251 case INTERPOLATION_HALT:
256 area.get_y()-area.get_height(),
262 gc->set_rgb_fg_color(black);
267 area.get_y()-area.get_height(),
275 case INTERPOLATION_LINEAR:
277 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()));
278 points.push_back(Gdk::Point(area.get_x()+area.get_width(),area.get_y()));
279 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()+area.get_height()));
280 window->draw_polygon(gc,true,points);
281 gc->set_rgb_fg_color(black);
282 window->draw_lines(gc,points);
285 case INTERPOLATION_CONSTANT:
287 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()));
288 points.push_back(Gdk::Point(area.get_x()+area.get_width(),area.get_y()));
289 points.push_back(Gdk::Point(area.get_x()+area.get_width(),area.get_y()+area.get_height()/2));
290 points.push_back(Gdk::Point(area.get_x()+area.get_width()-area.get_width()/4,area.get_y()+area.get_height()/2));
291 points.push_back(Gdk::Point(area.get_x()+area.get_width()-area.get_width()/4,area.get_y()+area.get_height()));
292 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()+area.get_height()));
293 window->draw_polygon(gc,true,points);
294 gc->set_rgb_fg_color(black);
295 window->draw_lines(gc,points);
298 case INTERPOLATION_UNDEFINED: default:
300 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()));
301 points.push_back(Gdk::Point(area.get_x()+area.get_width()-area.get_width()/3,area.get_y()));
302 points.push_back(Gdk::Point(area.get_x()+area.get_width(),area.get_y()+area.get_height()/3));
303 points.push_back(Gdk::Point(area.get_x()+area.get_width(),area.get_y()+area.get_height()-area.get_height()/3));
304 points.push_back(Gdk::Point(area.get_x()+area.get_width()-area.get_width()/3,area.get_y()+area.get_height()));
305 points.push_back(Gdk::Point(area.get_x()+area.get_width()/2,area.get_y()+area.get_height()));
306 window->draw_polygon(gc,true,points);
307 gc->set_rgb_fg_color(black);
308 window->draw_lines(gc,points);
314 /* === M E T H O D S ======================================================= */
316 /* === E N T R Y P O I N T ================================================= */
317 double defaultfps = 24;
318 const int fullheight = 20;
320 Widget_Timeslider::Widget_Timeslider()
321 :layout(Pango::Layout::create(get_pango_context())),
322 adj_default(0,0,2,1/defaultfps,10/defaultfps),
324 //invalidated(false),
329 set_size_request(-1,fullheight);
332 add_events( Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK
333 | Gdk::BUTTON_MOTION_MASK | Gdk::SCROLL_MASK );
335 set_time_adjustment(&adj_default);
339 Widget_Timeslider::~Widget_Timeslider()
343 void Widget_Timeslider::set_time_adjustment(Gtk::Adjustment *x)
345 //disconnect old connections
346 time_value_change.disconnect();
347 time_other_change.disconnect();
349 //connect update function to new adjustment
354 time_value_change = x->signal_value_changed().connect(sigc::mem_fun(*this,&Widget_Timeslider::queue_draw));
355 time_other_change = x->signal_changed().connect(sigc::mem_fun(*this,&Widget_Timeslider::queue_draw));
356 //invalidated = true;
361 void Widget_Timeslider::set_global_fps(float d)
367 //update everything since we need to redraw already
368 //invalidated = true;
374 /*void Widget_Timeslider::update_times()
378 start = adj_timescale->get_lower();
379 end = adj_timescale->get_upper();
380 current = adj_timescale->get_value();
384 void Widget_Timeslider::refresh()
392 }else if(adj_timescale)
394 double l = adj_timescale->get_lower(),
395 u = adj_timescale->get_upper(),
396 v = adj_timescale->get_value();
398 bool invalid = (l != start) || (u != end) || (v != current);
404 if(invalid) queue_draw();
408 bool Widget_Timeslider::redraw(bool /*doublebuffer*/)
410 Glib::RefPtr<Gdk::Window> window = get_window();
412 if(!window) return false;
414 Glib::RefPtr<Gdk::GC> gc = Gdk::GC::create(window);
415 if(!gc) return false;
417 //synfig::info("Drawing Timeslider");
418 //clear and update to current values
419 //invalidated = false;
422 //draw grey rectangle
423 Gdk::Color c("#7f7f7f");
424 gc->set_rgb_fg_color(c);
425 gc->set_background(c);
427 //Get the data for the window and the params to draw it...
428 int w = get_width(), h = get_height();
430 window->draw_rectangle(gc,true,0,0,w,h);
432 const double EPSILON = 1e-6;
433 if(!adj_timescale || w == 0) return true;
435 //Get the time information since we now know it's valid
436 double start = adj_timescale->get_lower(),
437 end = adj_timescale->get_upper(),
438 current = adj_timescale->get_value();
440 if(end-start < EPSILON) return true;
442 //synfig::info("Drawing Lines");
444 //draw all the time stuff
445 double dtdp = (end - start)/get_width();
446 double dpdt = 1/dtdp;
450 //Draw the time line...
451 double tpx = (current-start)*dpdt;
452 gc->set_rgb_fg_color(Gdk::Color("#ffaf00"));
453 window->draw_line(gc,round_to_int(tpx),0,round_to_int(tpx),fullheight);
455 //normal line/text color
456 gc->set_rgb_fg_color(Gdk::Color("#333333"));
458 //draw these lines... (always 5 between) maybe 6?
459 const int subdiv = 4;
462 //..., 3m, 2m, 1m30s, 1m, 30s, 20s, 10s, 5s, 3s, 2s, 1s, 0.5s
465 { 1.0/fps,subdiv/fps,0.25,0.5, 1, 2, 3, 5, 10, 20, 30, 60, 90, 120, 180, 300, 600, 1200, 1800, 2700, 3600 };
466 //{ 3600, 2700, 1800, 1200, 600, 300, 180, 120, 90, 60, 30, 20, 10, 5, 3, 2, 1, 0.5 };
467 const int ranges_size = sizeof(ranges)/sizeof(double);
469 double lowerrange = dtdp*75, upperrange = dtdp*150;
470 double midrange = (lowerrange + upperrange)/2;
472 //find most ideal scale
473 double scale = ranges[0];
475 double *val = binary_find(ranges, ranges+ranges_size, midrange);
476 double *after = val+1;
478 if(val >= ranges+ranges_size)
480 val = ranges+ranges_size-1;
483 if(after >= ranges+ranges_size)
485 after = ranges+ranges_size-1;
490 double diff = abs(scale - midrange), diff2 = abs(*after - midrange);
495 //synfig::info("Range found: (l %.2lf,u %.2lf - m %.2lf) -> %.2lf",lowerrange,upperrange,midrange,scale);
497 //search around this area to get the right one
500 //get first valid line and its position in pixel space
506 double subr = scale / subdiv;
508 //get its position inside...
509 time = ceil(start/subr)*subr - start;
512 //absolute time of the line to be drawn
516 double t = (time/scale - floor(time/scale))*subdiv; // the difference from the big mark in 0:1
517 //sdindex = (int)floor(t + 0.5); //get how far through the range it is...
518 sdindex = round_to_int(t); //get how far through the range it is...
520 //synfig::info("Extracted fr %.2lf -> %d", t, sdindex);
523 //synfig::info("Initial values: %.4lf t, %.1lf pixels, %d i", time,pixel,sdindex);
526 const int heightbig = 12;
527 const int heightsmall = 4;
529 int width = get_width();
530 while( pixel < width )
532 int xpx = round_to_int(pixel);
537 window->draw_line(gc,xpx,0,xpx,heightbig);
538 //round the time to nearest frame and draw the text
539 Time tm((double)time);
540 if(get_global_fps()) tm.round(get_global_fps());
541 Glib::ustring timecode(tm.get_string(get_global_fps(),App::get_time_format()));
543 //gc->set_rgb_fg_color(Gdk::Color("#000000"));
544 layout->set_text(timecode);
545 window->draw_layout(gc,xpx+2,heightsmall,layout);
548 window->draw_line(gc,xpx,0,xpx,heightsmall);
551 //increment time and position
552 pixel += subr / dtdp;
556 if(++sdindex >= subdiv) sdindex -= subdiv;
562 bool Widget_Timeslider::on_motion_notify_event(GdkEventMotion* event) //for dragging
564 if(!adj_timescale) return false;
566 Gdk::ModifierType mod = Gdk::ModifierType(event->state);
570 //NOTE: we might want to address the possibility of dragging with both buttons held down
572 if(mod & Gdk::BUTTON2_MASK)
575 //we need this for scrolling by dragging
576 double curx = event->x;
578 double start = adj_timescale->get_lower(),
579 end = adj_timescale->get_upper();
584 if(event->time-last_event_time<30)
587 last_event_time=event->time;
589 if(abs(lastx - curx) < 1 && end != start) return true;
590 //translate the window and correct it
592 //update our stuff so we are operating correctly
593 //invalidated = true;
596 //Note: Use inverse of mouse movement because of conceptual space relationship
597 double diff = lastx - curx; //curx - lastx;
599 //NOTE: This might be incorrect...
600 //fraction to move...
601 double dpx = (end - start)/get_width();
610 //But clamp to bounds if they exist...
611 //HACK - bounds should not be required for this slider
614 if(start < adj_bounds->get_lower())
616 diff = adj_bounds->get_lower() - start;
621 if(end > adj_bounds->get_upper())
623 diff = adj_bounds->get_upper() - end;
629 //synfig::info("Scrolling timerange to (%.4f,%.4f)",start,end);
631 adj_timescale->set_lower(start);
632 adj_timescale->set_upper(end);
634 adj_timescale->changed();
645 if(mod & Gdk::BUTTON1_MASK)
647 double curx = event->x;
649 //get time from drag...
650 double start = adj_timescale->get_lower(),
651 end = adj_timescale->get_upper(),
652 current = adj_timescale->get_value();
653 double t = start + curx*(end - start)/get_width();
655 //snap it to fps - if they exist...
658 t = floor(t*fps + 0.5)/fps;
664 adj_timescale->set_value(t);
666 //Fixed this to actually do what it's supposed to...
667 if(event->time-last_event_time>50)
669 adj_timescale->value_changed();
670 last_event_time = event->time;
680 bool Widget_Timeslider::on_scroll_event(GdkEventScroll* event) //for zooming
682 if(!adj_timescale) return false;
684 //Update so we are calculating based on current values
687 //figure out if we should center ourselves on the current time
690 //we want to zoom in on the time value if control is held down
691 if(Gdk::ModifierType(event->state) & Gdk::CONTROL_MASK)
696 switch(event->direction)
698 case GDK_SCROLL_UP: //zoom in
704 case GDK_SCROLL_DOWN: //zoom out
711 case GDK_SCROLL_RIGHT:
712 case GDK_SCROLL_LEFT:
714 double t = adj_timescale->get_value();
715 double start = adj_timescale->get_lower();
716 double end = adj_timescale->get_upper();
718 FIXME: be more intelligent about how far to scroll
719 Perhaps it should be based on the tickmarks?
720 for e.g. 1/4 of a tick mark per scroll event
721 Obviously this would need post-rounding to 1/fps
723 double adj = 1.0/fps;
725 if( event->direction == GDK_SCROLL_RIGHT )
731 adj_timescale->set_lower(t);
732 adj_timescale->set_upper(t+end-start);
733 } else if( t > end ){
734 adj_timescale->set_upper(t);
735 adj_timescale->set_lower(t-end+start);
740 adj_timescale->set_value(t);
741 adj_timescale->value_changed();
753 void Widget_Timeslider::zoom_in(bool centerontime)
755 if(!adj_timescale) return;
757 double start = adj_timescale->get_lower(),
758 end = adj_timescale->get_upper(),
759 current = adj_timescale->get_value();
761 double focuspoint = centerontime ? current : (start + end)/2;
763 //calculate new beginning and end
764 end = focuspoint + (end-focuspoint)*zoominfactor;
765 start = focuspoint + (start-focuspoint)*zoominfactor;
767 //synfig::info("Zooming in timerange to (%.4f,%.4f)",start,end);
770 if(start < adj_bounds->get_lower())
772 start = adj_bounds->get_lower();
775 if(end > adj_bounds->get_upper())
777 end = adj_bounds->get_upper();
782 adj_timescale->set_lower(start);
783 adj_timescale->set_upper(end);
785 //call changed function
786 adj_timescale->changed();
789 void Widget_Timeslider::zoom_out(bool centerontime)
791 if(!adj_timescale) return;
793 double start = adj_timescale->get_lower(),
794 end = adj_timescale->get_upper(),
795 current = adj_timescale->get_value();
797 double focuspoint = centerontime ? current : (start + end)/2;
799 //calculate new beginning and end
800 end = focuspoint + (end-focuspoint)*zoomoutfactor;
801 start = focuspoint + (start-focuspoint)*zoomoutfactor;
803 //synfig::info("Zooming out timerange to (%.4f,%.4f)",start,end);
806 if(start < adj_bounds->get_lower())
808 start = adj_bounds->get_lower();
811 if(end > adj_bounds->get_upper())
813 end = adj_bounds->get_upper();
818 adj_timescale->set_lower(start);
819 adj_timescale->set_upper(end);
821 //call changed function
822 adj_timescale->changed();
825 bool Widget_Timeslider::on_button_press_event(GdkEventButton *event) //for clicking
827 switch(event->button)
832 double start = adj_timescale->get_lower(),
833 end = adj_timescale->get_upper(),
834 current = adj_timescale->get_value();
836 double w = get_width();
837 double t = start + (end - start) * event->x / w;
839 t = floor(t*fps + 0.5)/fps;
841 /*synfig::info("Clicking time from %.3lf to %.3lf [(%.2lf,%.2lf) %.2lf / %.2lf ... %.2lf",
842 current, vt, start, end, event->x, w, fps);*/
850 adj_timescale->set_value(current);
851 adj_timescale->value_changed();
878 bool Widget_Timeslider::on_button_release_event(GdkEventButton *event) //end drag
880 switch(event->button)