- new facade API: set_input_at_time (element addressing, track=-1 for all components at once), set_input_string_at_time, frame_time_base, array_insert_at/remove_at, disconnect_ex (element-aware), and keyframes_set_type_many (first cross-track keyframe op, addressed by (time,track) pairs) - the widget bridge's commit funnel, color path, array ops, label disconnect, and keyframe set-type actions in keyframeview/curvewidget now go through the facade; keyframeviewundo.h loses two consumers - deliberate leftovers with rationale: keyframecontrol's multi-track composite ops (documented track-0-only limitation of the keyframe family), keyframeproperties dialog (needs a set_time primitive), curveview's drag UX, and NodeInputDragger (already engine-side)
716 lines
18 KiB
C++
716 lines
18 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2022 Olive Team
|
|
Modifications Copyright (C) 2025 mikesolar
|
|
|
|
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 3 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, see <http://www.gnu.org/licenses/>.
|
|
|
|
***/
|
|
|
|
#include "keyframeview.h"
|
|
|
|
#include <QMouseEvent>
|
|
#include <QToolTip>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "common/qtutils.h"
|
|
#include "dialog/keyframeproperties/keyframeproperties.h"
|
|
#include "node/group/group.h"
|
|
#include "node/node.h"
|
|
#include "node/nodeundo.h"
|
|
#include "node/project/serializer/serializer.h"
|
|
#include "oakengine/node.h"
|
|
#include "widget/menu/menu.h"
|
|
#include "widget/menu/menushared.h"
|
|
|
|
namespace olive
|
|
{
|
|
|
|
#define super TimeBasedView
|
|
|
|
KeyframeView::KeyframeView(QWidget *parent)
|
|
: super(parent)
|
|
, selection_manager_(this)
|
|
, autoselect_siblings_(true)
|
|
, max_scroll_(0)
|
|
, first_chance_mouse_event_(false)
|
|
{
|
|
setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
|
set_default_drag_mode(RubberBandDrag);
|
|
setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
connect(this, &KeyframeView::customContextMenuRequested, this,
|
|
&KeyframeView::show_context_menu);
|
|
}
|
|
|
|
void KeyframeView::delete_selected()
|
|
{
|
|
if (!selection_manager_.is_dragging()) {
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
|
|
foreach (NodeKeyframe *key, get_selected_keyframes()) {
|
|
command->add_child(new NodeParamRemoveKeyframeCommand(key));
|
|
}
|
|
|
|
Core::instance()->undo_stack()->push(
|
|
command,
|
|
tr("Deleted %1 Keyframe(s)").arg(get_selected_keyframes().size()));
|
|
}
|
|
}
|
|
|
|
KeyframeView::NodeConnections KeyframeView::add_keyframes_of_node(Node *n)
|
|
{
|
|
NodeConnections map;
|
|
|
|
foreach (const QString &i, n->inputs()) {
|
|
map.insert(i, add_keyframes_of_input(n, i));
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
KeyframeView::InputConnections
|
|
KeyframeView::add_keyframes_of_input(Node *on, const QString &oinput)
|
|
{
|
|
InputConnections vec;
|
|
|
|
NodeInput resolved = NodeGroup::resolve_input(NodeInput(on, oinput));
|
|
Node *n = resolved.node();
|
|
const QString &input = resolved.input();
|
|
|
|
if (n->is_input_keyframable(input)) {
|
|
int arr_sz = n->input_array_size(input);
|
|
vec.resize(arr_sz + 1);
|
|
for (int i = -1; i < arr_sz; i++) {
|
|
vec[i + 1] = add_keyframes_of_element(NodeInput(n, input, i));
|
|
}
|
|
}
|
|
|
|
return vec;
|
|
}
|
|
|
|
KeyframeView::ElementConnections
|
|
KeyframeView::add_keyframes_of_element(const NodeInput &input)
|
|
{
|
|
const QVector<NodeKeyframeTrack> &tracks =
|
|
input.node()->get_keyframe_tracks(input);
|
|
ElementConnections vec(tracks.size());
|
|
|
|
for (int i = 0; i < tracks.size(); i++) {
|
|
vec[i] = add_keyframes_of_track(NodeKeyframeTrackReference(input, i));
|
|
}
|
|
|
|
return vec;
|
|
}
|
|
|
|
KeyframeViewInputConnection *
|
|
KeyframeView::add_keyframes_of_track(const NodeKeyframeTrackReference &ref)
|
|
{
|
|
KeyframeViewInputConnection *track =
|
|
new KeyframeViewInputConnection(ref, this);
|
|
connect(track, &KeyframeViewInputConnection::require_update, this,
|
|
&KeyframeView::redraw);
|
|
tracks_.append(track);
|
|
redraw();
|
|
return track;
|
|
}
|
|
|
|
void KeyframeView::remove_keyframes_of_track(
|
|
KeyframeViewInputConnection *connection)
|
|
{
|
|
if (tracks_.removeOne(connection)) {
|
|
foreach (NodeKeyframe *key, connection->get_keyframes()) {
|
|
selection_manager_.deselect(key);
|
|
}
|
|
delete connection;
|
|
redraw();
|
|
emit selection_changed();
|
|
}
|
|
}
|
|
|
|
void KeyframeView::select_all()
|
|
{
|
|
foreach (KeyframeViewInputConnection *track, tracks_) {
|
|
foreach (NodeKeyframe *key, track->get_keyframes()) {
|
|
select_keyframe(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
void KeyframeView::deselect_all()
|
|
{
|
|
selection_manager_.clear_selection();
|
|
|
|
redraw();
|
|
}
|
|
|
|
void KeyframeView::clear()
|
|
{
|
|
if (!tracks_.isEmpty()) {
|
|
qDeleteAll(tracks_);
|
|
tracks_.clear();
|
|
redraw();
|
|
}
|
|
|
|
selection_manager_.clear_selection();
|
|
}
|
|
|
|
void KeyframeView::SelectionManagerSelectEvent(void *obj)
|
|
{
|
|
if (autoselect_siblings_) {
|
|
NodeKeyframe *key = static_cast<NodeKeyframe *>(obj);
|
|
QVector<NodeKeyframe *> keys = key->parent()->get_keyframes_at_time(
|
|
key->input(), key->time(), key->element());
|
|
foreach (NodeKeyframe *k, keys) {
|
|
if (k != key) {
|
|
select_keyframe(k);
|
|
}
|
|
}
|
|
}
|
|
|
|
emit selection_changed();
|
|
}
|
|
|
|
void KeyframeView::SelectionManagerDeselectEvent(void *obj)
|
|
{
|
|
if (autoselect_siblings_) {
|
|
NodeKeyframe *key = static_cast<NodeKeyframe *>(obj);
|
|
QVector<NodeKeyframe *> keys = key->parent()->get_keyframes_at_time(
|
|
key->input(), key->time(), key->element());
|
|
foreach (NodeKeyframe *k, keys) {
|
|
if (k != key) {
|
|
deselect_keyframe(k);
|
|
}
|
|
}
|
|
}
|
|
|
|
emit selection_changed();
|
|
}
|
|
|
|
bool KeyframeView::copy_selected(bool cut)
|
|
{
|
|
if (!selection_manager_.get_selected_objects().empty()) {
|
|
ProjectSerializer::SaveData sdata(ProjectSerializer::k_only_keyframes);
|
|
sdata.set_only_serialize_keyframes(
|
|
selection_manager_.get_selected_objects());
|
|
|
|
ProjectSerializer::copy(sdata);
|
|
|
|
if (cut) {
|
|
delete_selected();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool KeyframeView::paste(
|
|
std::function<Node *(const QString &)> find_node_function)
|
|
{
|
|
if (!get_viewer_node()) {
|
|
return false;
|
|
}
|
|
|
|
ProjectSerializer::Result res =
|
|
ProjectSerializer::paste(ProjectSerializer::k_only_keyframes);
|
|
if (res == ProjectSerializer::k_success) {
|
|
const ProjectSerializer::SerializedKeyframes &keys =
|
|
res.get_load_data().keyframes;
|
|
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
|
|
Rational min = RATIONAL_MAX;
|
|
for (auto it = keys.cbegin(); it != keys.cend(); it++) {
|
|
for (NodeKeyframe *key : it.value()) {
|
|
min = std::min(min, key->time());
|
|
}
|
|
}
|
|
min -= get_viewer_node()->get_playhead();
|
|
|
|
for (auto it = keys.cbegin(); it != keys.cend(); it++) {
|
|
const QString &paste_id = it.key();
|
|
|
|
// Find a node with this ID
|
|
Node *node_with_id = find_node_function(paste_id);
|
|
|
|
if (node_with_id) {
|
|
for (NodeKeyframe *key : it.value()) {
|
|
// Adjust sequence time to node's time
|
|
Rational t = key->time() - min;
|
|
t = get_adjusted_time(get_time_target(), node_with_id, t,
|
|
Node::k_transform_towards_input);
|
|
key->set_time(t);
|
|
|
|
if (NodeKeyframe *existing =
|
|
node_with_id->get_keyframe_at_time_on_track(
|
|
key->input(), key->time(), key->track(),
|
|
key->element())) {
|
|
command->add_child(
|
|
new NodeParamRemoveKeyframeCommand(existing));
|
|
}
|
|
|
|
command->add_child(
|
|
new NodeParamInsertKeyframeCommand(node_with_id, key));
|
|
}
|
|
} else {
|
|
qDeleteAll(it.value());
|
|
}
|
|
}
|
|
|
|
Core::instance()->undo_stack()->push(
|
|
command, tr("Pasted %1 Keyframe(s)").arg(keys.size()));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void KeyframeView::CatchUpScrollEvent()
|
|
{
|
|
super::CatchUpScrollEvent();
|
|
|
|
this->selection_manager_.force_drag_update();
|
|
}
|
|
|
|
void KeyframeView::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
NodeKeyframe *key_under_cursor =
|
|
selection_manager_.get_object_at_point(event->pos());
|
|
|
|
if (hand_press(event) || (!key_under_cursor && playhead_press(event))) {
|
|
return;
|
|
}
|
|
|
|
// Do mouse press things
|
|
if (first_chance_mouse_press(event)) {
|
|
first_chance_mouse_event_ = true;
|
|
} else if (NodeKeyframe *initial_key =
|
|
selection_manager_.mouse_press(event)) {
|
|
selection_manager_.drag_start(initial_key, event, this);
|
|
keyframe_drag_start(event);
|
|
} else {
|
|
selection_manager_.rubber_band_start(event);
|
|
}
|
|
|
|
// Update view
|
|
redraw();
|
|
}
|
|
|
|
void KeyframeView::mouseMoveEvent(QMouseEvent *event)
|
|
{
|
|
if (hand_move(event) || playhead_move(event)) {
|
|
return;
|
|
}
|
|
|
|
if (first_chance_mouse_event_) {
|
|
first_chance_mouse_move(event);
|
|
} else if (selection_manager_.is_dragging()) {
|
|
QString tip;
|
|
keyframe_drag_move(event, tip);
|
|
selection_manager_.drag_move(event->pos(), tip);
|
|
} else if (selection_manager_.is_rubber_banding()) {
|
|
selection_manager_.rubber_band_move(event->pos());
|
|
redraw();
|
|
}
|
|
|
|
if (event->buttons()) {
|
|
// Signal cursor pos in case we should scroll to catch up to it
|
|
emit dragged(event->pos().x(), event->pos().y());
|
|
}
|
|
}
|
|
|
|
void KeyframeView::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
if (hand_release(event) || playhead_release(event)) {
|
|
return;
|
|
}
|
|
|
|
if (first_chance_mouse_event_) {
|
|
first_chance_mouse_release(event);
|
|
first_chance_mouse_event_ = false;
|
|
} else if (selection_manager_.is_dragging()) {
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
selection_manager_.drag_stop(command);
|
|
keyframe_drag_release(event, command);
|
|
Core::instance()->undo_stack()->push(
|
|
command, tr("Moved %1 Keyframe(s)")
|
|
.arg(selection_manager_.get_selected_objects().size()));
|
|
} else if (selection_manager_.is_rubber_banding()) {
|
|
selection_manager_.rubber_band_stop();
|
|
redraw();
|
|
emit selection_changed();
|
|
}
|
|
|
|
emit released();
|
|
}
|
|
|
|
int binary_search_first_keyframe_after_or_at(const QVector<NodeKeyframe *> &keys,
|
|
const Rational &time)
|
|
{
|
|
int low = 0;
|
|
int high = keys.size() - 1;
|
|
|
|
while (low <= high) {
|
|
int mid = low + (high - low) / 2;
|
|
NodeKeyframe *test_key = keys.at(mid);
|
|
|
|
if (test_key->time() == time ||
|
|
(test_key->time() > time &&
|
|
(mid == 0 || keys.at(mid - 1)->time() < time))) {
|
|
return mid;
|
|
} else if (test_key->time() < time) {
|
|
low = mid + 1;
|
|
} else {
|
|
high = mid - 1;
|
|
}
|
|
}
|
|
|
|
return keys.size();
|
|
}
|
|
|
|
void KeyframeView::drawForeground(QPainter *painter, const QRectF &rect)
|
|
{
|
|
int key_sz = QtUtils::q_font_metrics_width(fontMetrics(), "Oi");
|
|
int key_rad = key_sz / 2;
|
|
|
|
selection_manager_.clear_drawn_objects();
|
|
|
|
painter->setRenderHint(QPainter::Antialiasing);
|
|
|
|
foreach (KeyframeViewInputConnection *track, tracks_) {
|
|
const QVector<NodeKeyframe *> &keys = track->get_keyframes();
|
|
|
|
if (keys.isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
if (!is_y_axis_enabled()) {
|
|
// Filter out if the keyframes are offscreen Y
|
|
qreal y = get_keyframe_scene_y(track, keys.first());
|
|
if (y + key_rad < rect.top() || y - key_rad >= rect.bottom()) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Find first keyframe to show with binary search
|
|
Rational left_time = get_unadjusted_keyframe_time(
|
|
keys.first(), scene_to_time(rect.left() - key_sz));
|
|
int using_index = binary_search_first_keyframe_after_or_at(keys, left_time);
|
|
|
|
Rational next_key = RATIONAL_MIN;
|
|
NodeKeyframe::Type last_type = NodeKeyframe::k_invalid;
|
|
for (int i = using_index; i < keys.size(); i++) {
|
|
NodeKeyframe *key = keys.at(i);
|
|
|
|
if (key->time() < next_key && key->type() == last_type) {
|
|
// This key will be drawn at exactly the same location as the last one and therefore
|
|
// doesn't need to be drawn. See if the next one will be drawn.
|
|
i++;
|
|
if (i == keys.size()) {
|
|
break;
|
|
}
|
|
|
|
key = keys.at(i);
|
|
|
|
if (key->time() < next_key) {
|
|
// Next key still won't be drawn, so we'll switch to a binary search
|
|
i = binary_search_first_keyframe_after_or_at(keys, next_key);
|
|
|
|
if (i == keys.size()) {
|
|
break;
|
|
}
|
|
|
|
key = keys.at(i);
|
|
}
|
|
}
|
|
|
|
QRectF key_rect(-key_rad, -key_rad, key_sz, key_sz);
|
|
qreal key_x = get_keyframe_scene_x(key);
|
|
key_rect.translate(key_x, get_keyframe_scene_y(track, key));
|
|
|
|
if (key_rect.left() >= rect.right()) {
|
|
// Break after last keyframe
|
|
break;
|
|
}
|
|
|
|
draw_keyframe(painter, key, track, key_rect);
|
|
|
|
next_key = get_unadjusted_keyframe_time(key, scene_to_time(key_x + 1));
|
|
last_type = key->type();
|
|
}
|
|
}
|
|
|
|
super::drawForeground(painter, rect);
|
|
}
|
|
|
|
void KeyframeView::draw_keyframe(QPainter *painter, NodeKeyframe *key,
|
|
KeyframeViewInputConnection *track,
|
|
const QRectF &key_rect)
|
|
{
|
|
painter->setPen(Qt::black);
|
|
|
|
if (is_keyframe_selected(key)) {
|
|
painter->setBrush(palette().highlight());
|
|
} else {
|
|
painter->setBrush(track->get_brush());
|
|
}
|
|
|
|
selection_manager_.declare_drawn_object(key, key_rect);
|
|
|
|
switch (key->type()) {
|
|
case NodeKeyframe::k_invalid:
|
|
break;
|
|
case NodeKeyframe::k_linear: {
|
|
QPointF points[] = { QPointF(key_rect.center().x(), key_rect.top()),
|
|
QPointF(key_rect.right(), key_rect.center().y()),
|
|
QPointF(key_rect.center().x(), key_rect.bottom()),
|
|
QPointF(key_rect.left(), key_rect.center().y()) };
|
|
|
|
painter->drawPolygon(points, 4);
|
|
break;
|
|
}
|
|
case NodeKeyframe::k_bezier:
|
|
painter->drawEllipse(key_rect);
|
|
break;
|
|
case NodeKeyframe::k_hold:
|
|
painter->drawRect(key_rect);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void KeyframeView::ScaleChangedEvent(const double &scale)
|
|
{
|
|
super::ScaleChangedEvent(scale);
|
|
|
|
redraw();
|
|
}
|
|
|
|
void KeyframeView::TimeTargetChangedEvent(ViewerOutput *v)
|
|
{
|
|
redraw();
|
|
}
|
|
|
|
void KeyframeView::TimebaseChangedEvent(const Rational &timebase)
|
|
{
|
|
super::TimebaseChangedEvent(timebase);
|
|
|
|
selection_manager_.set_timebase(timebase);
|
|
}
|
|
|
|
void KeyframeView::ContextMenuEvent(Menu &m)
|
|
{
|
|
Q_UNUSED(m)
|
|
}
|
|
|
|
void KeyframeView::select_keyframe(NodeKeyframe *key)
|
|
{
|
|
if (selection_manager_.select(key)) {
|
|
redraw();
|
|
|
|
emit selection_changed();
|
|
}
|
|
}
|
|
|
|
void KeyframeView::deselect_keyframe(NodeKeyframe *key)
|
|
{
|
|
if (selection_manager_.deselect(key)) {
|
|
redraw();
|
|
|
|
emit selection_changed();
|
|
}
|
|
}
|
|
|
|
Rational KeyframeView::get_unadjusted_keyframe_time(NodeKeyframe *key,
|
|
const Rational &time)
|
|
{
|
|
return get_adjusted_time(get_time_target(), key->parent(), time,
|
|
Node::k_transform_towards_input);
|
|
}
|
|
|
|
Rational KeyframeView::get_adjusted_keyframe_time(NodeKeyframe *key)
|
|
{
|
|
return get_adjusted_time(key->parent(), get_time_target(), key->time(),
|
|
Node::k_transform_towards_output);
|
|
}
|
|
|
|
double KeyframeView::get_keyframe_scene_x(NodeKeyframe *key)
|
|
{
|
|
return time_to_scene(get_adjusted_keyframe_time(key));
|
|
}
|
|
|
|
qreal KeyframeView::get_keyframe_scene_y(KeyframeViewInputConnection *track,
|
|
NodeKeyframe *key)
|
|
{
|
|
return mapFromGlobal(QPoint(0, track->get_keyframe_y())).y();
|
|
}
|
|
|
|
void KeyframeView::SceneRectUpdateEvent(QRectF &rect)
|
|
{
|
|
rect.setY(0);
|
|
rect.setHeight(max_scroll_);
|
|
}
|
|
|
|
Rational KeyframeView::calculate_new_time_from_screen(const Rational &old_time,
|
|
double cursor_diff)
|
|
{
|
|
return Rational::from_double(old_time.to_double() + cursor_diff);
|
|
}
|
|
|
|
void KeyframeView::show_context_menu()
|
|
{
|
|
Menu m;
|
|
|
|
MenuShared::instance()->add_items_for_edit_menu(&m, false);
|
|
|
|
QAction *linear_key_action = nullptr;
|
|
QAction *bezier_key_action = nullptr;
|
|
QAction *hold_key_action = nullptr;
|
|
|
|
if (!get_selected_keyframes().empty()) {
|
|
bool all_keys_are_same_type = true;
|
|
NodeKeyframe::Type type = get_selected_keyframes().front()->type();
|
|
|
|
for (size_t i = 1; i < get_selected_keyframes().size(); i++) {
|
|
NodeKeyframe *key_item = get_selected_keyframes().at(i);
|
|
NodeKeyframe *prev_item = get_selected_keyframes().at(i - 1);
|
|
|
|
if (key_item->type() != prev_item->type()) {
|
|
all_keys_are_same_type = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
m.addSeparator();
|
|
|
|
linear_key_action = m.addAction(tr("Linear"));
|
|
bezier_key_action = m.addAction(tr("Bezier"));
|
|
hold_key_action = m.addAction(tr("Hold"));
|
|
|
|
if (all_keys_are_same_type) {
|
|
switch (type) {
|
|
case NodeKeyframe::k_invalid:
|
|
break;
|
|
case NodeKeyframe::k_linear:
|
|
linear_key_action->setChecked(true);
|
|
break;
|
|
case NodeKeyframe::k_bezier:
|
|
bezier_key_action->setChecked(true);
|
|
break;
|
|
case NodeKeyframe::k_hold:
|
|
hold_key_action->setChecked(true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
m.addSeparator();
|
|
|
|
ContextMenuEvent(m);
|
|
|
|
if (!get_selected_keyframes().empty()) {
|
|
m.addSeparator();
|
|
|
|
QAction *properties_action = m.addAction(tr("P&roperties"));
|
|
connect(properties_action, &QAction::triggered, this,
|
|
&KeyframeView::show_keyframe_properties_dialog);
|
|
}
|
|
|
|
QAction *selected = m.exec(QCursor::pos());
|
|
|
|
// Process keyframe type changes
|
|
if (selected) {
|
|
if (selected == linear_key_action || selected == bezier_key_action ||
|
|
selected == hold_key_action) {
|
|
NodeKeyframe::Type new_type;
|
|
|
|
if (selected == hold_key_action) {
|
|
new_type = NodeKeyframe::k_hold;
|
|
} else if (selected == bezier_key_action) {
|
|
new_type = NodeKeyframe::k_bezier;
|
|
} else {
|
|
new_type = NodeKeyframe::k_linear;
|
|
}
|
|
|
|
// Through the liboakengine C ABI facade: one undoable command
|
|
// per distinct input (usually just one), with the same batch
|
|
// semantics as the old per-keyframe commands.
|
|
struct TypeGroup {
|
|
Node *node;
|
|
QString input;
|
|
int element;
|
|
QVector<int64_t> times;
|
|
QVector<int> tracks;
|
|
};
|
|
QVector<TypeGroup> groups;
|
|
foreach (NodeKeyframe *item, get_selected_keyframes()) {
|
|
int g = 0;
|
|
for (; g < groups.size(); g++) {
|
|
if (groups.at(g).node == item->parent() &&
|
|
groups.at(g).input == item->input() &&
|
|
groups.at(g).element == item->element()) {
|
|
break;
|
|
}
|
|
}
|
|
if (g == groups.size()) {
|
|
groups.append({ item->parent(), item->input(),
|
|
item->element(), {}, {} });
|
|
}
|
|
OakEngineNode *handle =
|
|
reinterpret_cast<OakEngineNode *>(item->parent());
|
|
int tbn = 0, tbd = 0;
|
|
oakengine_node_frame_time_base(handle, &tbn, &tbd);
|
|
groups[g].times.append(Timecode::time_to_timestamp(
|
|
item->time(), Rational(tbn, tbd), Timecode::k_round));
|
|
groups[g].tracks.append(item->track());
|
|
}
|
|
int facade_type = 0;
|
|
if (new_type == NodeKeyframe::k_bezier) {
|
|
facade_type = 1;
|
|
} else if (new_type == NodeKeyframe::k_hold) {
|
|
facade_type = 2;
|
|
}
|
|
foreach (const TypeGroup &g, groups) {
|
|
oakengine_node_keyframes_set_type_many(
|
|
reinterpret_cast<OakEngineNode *>(g.node),
|
|
g.input.toUtf8().constData(), g.element,
|
|
g.times.constData(), g.tracks.data(), g.times.size(),
|
|
facade_type);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void KeyframeView::show_keyframe_properties_dialog()
|
|
{
|
|
if (!get_selected_keyframes().empty()) {
|
|
KeyframePropertiesDialog kd(get_selected_keyframes(), timebase(), this);
|
|
kd.exec();
|
|
}
|
|
}
|
|
|
|
void KeyframeView::update_rubber_band_for_scroll()
|
|
{
|
|
this->selection_manager_.force_drag_update();
|
|
}
|
|
|
|
void KeyframeView::redraw()
|
|
{
|
|
viewport()->update();
|
|
}
|
|
|
|
}
|