Files
oak-editor/app/widget/keyframeview/keyframeview.cpp
T
Mike-Solar bb40b4923e style: unify identifier naming per updated conventions
Automated with clang-tidy readability-identifier-naming (config added to
.clang-tidy) plus scripted passes, per the updated rules now documented
in CONTRIBUTING.md:

- types (class/struct/enum/alias/template params): PascalCase
- functions, variables, members: snake_case (incl. rational -> Rational)
- private/protected members: trailing underscore; static member
  variables likewise (instance_, available_themes_)
- constants and enum values: snake_case (kLinear -> k_linear,
  F32P -> f32p); ALL_CAPS reserved for macros
- macros: OAK_ prefix (OLIVE_ADD_TEST/OLIVE_ASSERT/OLIVE_CONFIG ->
  OAK_ADD_TEST/OAK_ASSERT/OAK_CONFIG, GL_PREAMBLE -> OAK_GL_PREAMBLE,
  include guards -> OAK_*)
- file names: all lowercase (Current/Plugin/OliveHost/OliveClip/
  OlivePluginInstance -> current/plugin/olivehost/oliveclip/
  oliveplugininstance)
- getters share the member name sans underscore, setters set_foo()
- Qt and third-party (OpenFX) virtual overrides and framework callbacks
  keep their original names (exempt in .clang-tidy)

Manual follow-ups required where automation could not reach:
- string-based QMetaObject/SIGNAL/SLOT references updated to renamed
  methods (AddTask, CreatedFile, DeleteSpecificFile, moveSelectionUp, ...)
- macro bodies referencing renamed methods (OLIVE_CONFIG,
  NODE_DEFAULT_DESTRUCTOR, MANAGEDDISPLAYWIDGET_*)
- self-shadowing locals renamed where signals/methods became same-named
  (size_changed, worker_count, selected_items, import param, filters)
- third_party OFX member/namespace usages restored (OFX::Host::*,
  _created, _clipPrefsDirty, createInstance, clearPersistentMessage)
- STL protocol aliases restored (const_iterator) with .clang-tidy
  ignore rules; qHash overloads restored

Full build and test suite pass: ctest 4/4, ~1960 gtest cases green.
2026-07-19 16:10:54 +08:00

678 lines
16 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 "keyframeviewundo.h"
#include "node/group/group.h"
#include "node/node.h"
#include "node/nodeundo.h"
#include "node/project/serializer/serializer.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;
}
MultiUndoCommand *command = new MultiUndoCommand();
foreach (NodeKeyframe *item, get_selected_keyframes()) {
command->add_child(new KeyframeSetTypeCommand(item, new_type));
}
Core::instance()->undo_stack()->push(
command, tr("Set Type of %1 Keyframe(s)")
.arg(get_selected_keyframes().size()));
}
}
}
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();
}
}