/* === S Y N F I G ========================================================= */
/*! \file widget_keyframe_list.cpp
** \brief A custom widget to manage keyframes in the timeline.
**
** $Id$
**
** \legal
** Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
** Copyright (c) 2007 Chris Moore
** Copyright (c) 2009 Carlos López
** Copyright (c) 2012-2013 Konstantin Dmitriev
** ......... ... 2018 Ivan Mahonin
**
** 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/menu.h>
#include <synfig/general.h>
#include <gui/app.h>
#include "widget_keyframe_list.h"
#include <gui/localization.h>
#endif
/* === U S I N G =========================================================== */
using namespace synfig;
using namespace synfigapp;
using namespace studio;
/* === M A C R O S ========================================================= */
/* === G L O B A L S ======================================================= */
/* === P R O C E D U R E S ================================================= */
/* === M E T H O D S ======================================================= */
Widget_Keyframe_List::Widget_Keyframe_List():
kf_list(),
editable(true),
dragging(),
changed(),
selected(),
moving_tooltip(Gtk::WINDOW_POPUP),
moving_tooltip_y()
{
set_size_request(-1, 10);
add_events( Gdk::BUTTON_PRESS_MASK
| Gdk::BUTTON_RELEASE_MASK
| Gdk::BUTTON1_MOTION_MASK
| Gdk::POINTER_MOTION_MASK );
// Window of the moving tooltip
moving_tooltip_label.set_alignment(0.5, 0.5);
moving_tooltip_label.show();
moving_tooltip.set_resizable(false);
moving_tooltip.set_name("gtk-tooltips");
moving_tooltip.set_border_width(4);
moving_tooltip.set_default_size(10, 10);
moving_tooltip.set_type_hint(Gdk::WINDOW_TYPE_HINT_TOOLTIP);
moving_tooltip.add(moving_tooltip_label);
}
Widget_Keyframe_List::~Widget_Keyframe_List()
{
set_time_model(etl::handle<TimeModel>());
set_kf_list(NULL);
set_canvas_interface(etl::loose_handle<CanvasInterface>());
}
void
Widget_Keyframe_List::draw_arrow(
const Cairo::RefPtr<Cairo::Context> &cr,
double x, double y,
double width, double height,
bool fill,
const Color &color )
{
cr->save();
cr->set_source_rgba(color.get_r(), color.get_g(), color.get_b(), color.get_a());
cr->set_line_width(1.0);
cr->move_to(x, y);
cr->line_to(x - 0.5*width, y - height);
cr->line_to(x + 0.5*width, y - height);
cr->close_path();
if (fill) cr->fill(); else cr->stroke();
cr->restore();
}
bool
Widget_Keyframe_List::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
{
// Check if the window we want draw is ready
if (!time_model)
return false;
// TODO: hardcoded colors
// Colors
Color background(0.46, 0.55, 0.70, 1.0);
Color normal(0.0, 0.0, 0.0, 1.0);
Color selected(1.0, 1.0, 1.0, 1.0);
Color drag_old_position(1.0, 1.0, 1.0, 0.6);
Color drag_new_position(1.0, 1.0, 1.0, 1.0);
if (!editable) {
normal.set_a(0.5);
selected.set_a(0.5);
drag_old_position.set_a(0.3);
drag_new_position.set_a(0.5);
}
const double h(get_height());
const double w(get_width());
const double y(h - 2);
const double ah(h - 4);
const double aw(2*ah);
if (w <= 0 || h <= 0)
return false;
// Boundaries of the drawing area in time units.
Time lower(time_model->get_visible_lower());
Time upper(time_model->get_visible_upper());
time_ratio = (upper - lower)*(0.5*(double)aw/(double)w);
Time lower_ex = lower - time_ratio;
Time upper_ex = upper + time_ratio;
double k = (double)w/(double)(upper - lower);
// Draw a background
cr->save();
cr->set_source_rgba(background.get_r(), background.get_g(), background.get_b(), background.get_a());
cr->rectangle(0.0, 0.0, w, h);
cr->fill();
cr->restore();
// Returns if there are not keyframes to draw.
if (!kf_list || kf_list->empty()) return true;
// draw all keyframes
Time selected_time = Time::end();
for(KeyframeList::const_iterator i = kf_list->begin(); i != kf_list->end(); ++i)
if (lower_ex < i->get_time() && i->get_time() < upper_ex) {
if (*i == selected_kf) {
selected_time = i->get_time();
} else {
const double x = k*(double)(i->get_time() - lower);
draw_arrow(cr, x, y, aw, ah, i->active(), normal);
}
}
// we do this so that we can be sure that
// the selected keyframe is shown on top
if (selected_time != Time::end()) {
const double x = k*(double)(selected_time - lower);
if (dragging) {
const double new_x = k*(double)(dragging_kf_time - lower);
draw_arrow(cr, x, y, aw, ah, selected_kf.active(), drag_old_position);
draw_arrow(cr, new_x, y, aw, ah, selected_kf.active(), drag_new_position);
} else {
draw_arrow(cr, x, y, aw, ah, selected_kf.active(), selected);
}
}
return true;
}
void
Widget_Keyframe_List::set_kf_list(KeyframeList *x)
{
if (kf_list == x) return;
kf_list = x;
reset_selected_keyframe();
}
void
Widget_Keyframe_List::reset_selected_keyframe()
{
selected = false;
dragging = false;
}
void
Widget_Keyframe_List::set_selected_keyframe(const Keyframe &x)
{
if (selected && x == selected_kf) {
// Keyframe::operator== only on uniqueid::operator==
// \see UniqueID::operator==
// In all case, refresh keyframe description, time and activation status to do not loose it
selected_kf = x;
return;
}
selected_kf = x;
selected = true;
dragging = false;
dragging_kf_time = selected_kf.get_time();
if (canvas_interface)
canvas_interface->signal_keyframe_selected()(selected_kf);
queue_draw();
}
void
Widget_Keyframe_List::on_keyframe_selected(Keyframe keyframe)
{ set_selected_keyframe(keyframe); }
bool
Widget_Keyframe_List::perform_move_kf(bool delta)
{
if (!kf_list)
return false;
if (!selected)
return false;
if (dragging_kf_time == selected_kf.get_time())
return false; // change this checking if not sticked to integer frames
Time selected_kf_time(selected_kf.get_time());
Time prev, next;
kf_list->find_prev_next(selected_kf_time, prev, next);
// Not possible to set delta to the first keyframe
// perform normal movement
// As suggested by Zelgadis it is better to not perform anything.
if (prev == Time::begin() && delta) {
info(_("Not possible to ALT-drag the first keyframe"));
return false;
}
if (!delta) {
Action::Handle action(Action::create("KeyframeSet"));
if (!action) return false;
selected_kf.set_time(dragging_kf_time);
action->set_param("canvas", canvas_interface->get_canvas());
action->set_param("canvas_interface", canvas_interface);
action->set_param("keyframe", selected_kf);
try {
canvas_interface->get_instance()->perform_action(action);
canvas_interface->signal_keyframe_selected()(selected_kf);
} catch(...) { return false; }
} else {
// find prev from selected kf time including deactivated kf
KeyframeList::iterator iter;
if (kf_list->find_prev(selected_kf_time, iter, false)) {
//Keyframe prev_kf(*kf_list->find_prev(selected_kf_time, false));
Keyframe prev_kf(*iter);
Time prev_kf_time(prev_kf.get_time());
if (prev_kf_time >= dragging_kf_time) { // Not allowed
warning(_("Delta set not allowed"));
info("Widget_Keyframe_List::perform_move_kf(%i)::prev_kf_time=%s", delta, prev_kf_time.get_string().c_str());
info("Widget_Keyframe_List::perform_move_kf(%i)::dragging_kf_time=%s", delta, dragging_kf_time.get_string().c_str());
return false;
} else {
Time old_delta_time(selected_kf_time-prev_kf_time);
Time new_delta_time(dragging_kf_time-prev_kf_time);
Time change_delta(new_delta_time-old_delta_time);
Action::Handle action(Action::create("KeyframeSetDelta"));
if (!action)
return false;
action->set_param("canvas", canvas_interface->get_canvas());
action->set_param("canvas_interface", canvas_interface);
action->set_param("keyframe", prev_kf);
action->set_param("delta", change_delta);
canvas_interface->get_instance()->perform_action(action);
}
}
}
queue_draw();
return true;
}
bool
Widget_Keyframe_List::on_event(GdkEvent *event)
{
if (!time_model || get_width() <= 0 || !kf_list || !editable)
return false;
const int x = (int)event->button.x;
// Boundaries of the drawing area in time units.
Time lower(time_model->get_visible_lower());
Time upper(time_model->get_visible_upper());
// The time where the event x is
Time t = lower + (upper - lower)*((double)x/(double)get_width());
t = std::max(lower, std::min(upper, t));
// here the guts of the event
switch(event->type) {
case GDK_MOTION_NOTIFY: {
if (event->motion.state & GDK_BUTTON1_MASK) {
// here is captured mouse motion
// AND left or right mouse button pressed
if (!selected)
return true;
// stick to integer frames
t = time_model->round_time(t);
dragging_kf_time = t;
dragging = true;
// Moving tooltip displaying the dragging time
int x_root = static_cast<int>(event->button.x_root);
int x_origin; int y_origin;
get_window()->get_origin (x_origin, y_origin);
moving_tooltip_label.set_text(
String(_("Time : "))
+ dragging_kf_time.get_string(time_model->get_frame_rate(), App::get_time_format())
+ "\n"
+ _("Old Time : ")
+ selected_kf.get_time().get_string(time_model->get_frame_rate(), App::get_time_format()) );
// Show the tooltip and move to a nice position
if (!moving_tooltip.get_visible()) moving_tooltip.show();
moving_tooltip.move(x_root, y_origin - moving_tooltip.get_height());
} else {
// here is captured mouse motion
// AND NOT left or right mouse button pressed
String ttip;
Time p_t, n_t;
kf_list->find_prev_next(t, p_t, n_t);
if ( (p_t == Time::begin() && n_t == Time::end())
|| (t - p_t > time_ratio && n_t - t > time_ratio) )
{
ttip = _("Click and drag keyframes");
} else
if (t - p_t < n_t - t) {
//Keyframe kf = *kf_list->find_prev(t);
//ttip = kf.get_description().empty() ? String(_("No name")) : kf.get_description();
KeyframeList::iterator iter;
if (kf_list->find_prev(t, iter)) {
Keyframe kf(*iter);
ttip = kf.get_description().empty() ? String(_("No name")) : kf.get_description();
}
} else {
//Keyframe kf(*kf_list->find_next(t));
//ttip = kf.get_description().empty() ? String(_("No name")) : kf.get_description();
KeyframeList::iterator iter;
if (kf_list->find_next(t, iter)) {
Keyframe kf(*iter);
ttip = kf.get_description().empty() ? String(_("No name")) : kf.get_description();
}
}
set_tooltip_text(ttip);
dragging = false;
}
queue_draw();
return true;
}
case GDK_BUTTON_PRESS: {
changed = false;
dragging = false;
const Keyframe *kf = NULL;
Time prev_t, next_t;
kf_list->find_prev_next(t, prev_t, next_t, false);
if (t - prev_t < next_t - t) {
if (t - prev_t <= time_ratio) {
//kf = &*kf_list->find_prev(t, false);
KeyframeList::iterator iter;
if (kf_list->find_prev(t, iter, false))
kf = &*iter;
}
} else {
if (next_t - t <= time_ratio) {
//kf = &*kf_list->find_next(t, false);
KeyframeList::iterator iter;
if (kf_list->find_next(t, iter, false))
kf = &*iter;
}
}
switch(event->button.button) {
case 1:
if (kf) set_selected_keyframe(*kf); else reset_selected_keyframe();
break;
case 3:
if (kf) set_selected_keyframe(*kf);
if (Gtk::Menu* menu = dynamic_cast<Gtk::Menu*>(App::ui_manager()->get_widget("/menu-keyframe")))
menu->popup(event->button.button,gtk_get_current_event_time());
break;
default:
return false;
}
queue_draw();
return true;
}
case GDK_2BUTTON_PRESS: {
if (event->button.button == 1) {
if (selected && canvas_interface)
canvas_interface->signal_keyframe_properties()();
return true;
}
break;
}
case GDK_BUTTON_RELEASE: {
if (event->button.button == 1 && dragging) {
moving_tooltip.hide();
perform_move_kf((bool)(event->button.state & GDK_MOD1_MASK));
dragging = false;
}
if (event->button.button == 1 || event->button.button == 3)
return true; // we captured press of buttons 1 and 3, so capture release too
break;
}
default:
break;
}
return false;
}
void Widget_Keyframe_List::set_time_model(const etl::handle<TimeModel> &x)
{
if (time_model == x) return;
time_model_change.disconnect();
time_model = x;
if (time_model)
time_model_change = x->signal_visible_changed().connect(
sigc::mem_fun(*this, &Widget_Keyframe_List::queue_draw) );
}
void
Widget_Keyframe_List::set_canvas_interface(const etl::loose_handle<CanvasInterface> &x)
{
if (canvas_interface == x) return;
keyframe_added.disconnect();
keyframe_changed.disconnect();
keyframe_removed.disconnect();
keyframe_selected.disconnect();
canvas_interface = x;
set_kf_list(canvas_interface ? &canvas_interface->get_canvas()->keyframe_list() : NULL);
if (canvas_interface) {
keyframe_added = canvas_interface->signal_keyframe_added().connect(
sigc::hide_return(
sigc::hide(
sigc::mem_fun(*this,&Widget_Keyframe_List::queue_draw) )));
keyframe_changed = canvas_interface->signal_keyframe_changed().connect(
sigc::hide_return(
sigc::hide(
sigc::mem_fun(*this,&Widget_Keyframe_List::queue_draw) )));
keyframe_removed = canvas_interface->signal_keyframe_removed().connect(
sigc::hide_return(
sigc::hide(
sigc::mem_fun(*this,&Widget_Keyframe_List::queue_draw) )));
keyframe_selected = canvas_interface->signal_keyframe_selected().connect(
sigc::mem_fun(*this,&Widget_Keyframe_List::on_keyframe_selected) );
}
}