/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
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 .
***/
#include "oakengine/events.h"
#include
#include
#include
#include
#include
#include
#include "audio/audiomanager.h"
#include "coreengine.h"
#include "node/keyframe.h"
#include "oakengine/node.h"
#include "node/block/block.h"
#include "node/color/colormanager/colormanager.h"
#include "node/group/group.h"
#include "node/output/track/track.h"
#include "node/output/track/tracklist.h"
#include "node/output/viewer/viewer.h"
#include "node/project.h"
#include "node/project/folder/folder.h"
#include "node/project/sequence/sequence.h"
#include "render/framehashcache.h"
#include "render/playbackcache.h"
#include "task/task.h"
#include "task/taskmanager.h"
#include "timeline/timelinemarker.h"
#include "timeline/timelineworkarea.h"
#include "undo/undostack.h"
namespace
{
// Subscription registry: id -> connections. Callbacks capture the function
// pointer and userdata directly, so delivery never touches the registry;
// the map only tracks lifecycle (unsubscribe, sender teardown).
struct Subscription {
QVector connections;
};
QMutex g_registry_mutex;
QHash g_registry;
std::atomic g_next_id{1};
// The observed engine object died: drop the registry entry. Qt has already
// torn down the connections themselves.
void drop_subscription(int64_t id)
{
QMutexLocker locker(&g_registry_mutex);
g_registry.remove(id);
}
void invoke(oakengine_event_fn fn, void *userdata, int32_t id, void *source,
int64_t a, int64_t b, void *related, int64_t c = 0,
const char *s = nullptr)
{
oakengine_event event;
event.id = id;
event.reserved = 0;
event.a = a;
event.b = b;
event.c = c;
event.source = source;
event.handle = related;
event.s = s;
fn(&event, userdata);
}
// Frame-timestamp timebase for node events: the frame rate of the
// project's first sequence, or the engine default (1001/30000 s per
// frame). Same convention as node.cpp's project_time_base().
olive::Rational node_frame_time_base(const olive::Node *node)
{
if (const olive::Project *p =
olive::Project::get_project_from_object(node)) {
for (olive::Node *n : p->nodes()) {
if (const olive::Sequence *s =
dynamic_cast(n)) {
const olive::Rational fr = s->get_video_params().frame_rate();
if (!fr.isNull() && !fr.isNaN()) {
return fr.flipped();
}
}
}
}
return olive::Rational(1001, 30000);
}
// NodeValue::Type -> facade value type (same mapping as node.cpp).
int node_value_type_to_c(olive::NodeValue::Type t)
{
switch (t) {
case olive::NodeValue::k_int:
return OAK_NODE_VALUE_INT;
case olive::NodeValue::k_float:
return OAK_NODE_VALUE_FLOAT;
case olive::NodeValue::k_boolean:
return OAK_NODE_VALUE_BOOL;
case olive::NodeValue::k_rational:
return OAK_NODE_VALUE_RATIONAL;
case olive::NodeValue::k_color:
return OAK_NODE_VALUE_COLOR;
case olive::NodeValue::k_vec2:
return OAK_NODE_VALUE_VEC2;
case olive::NodeValue::k_vec3:
return OAK_NODE_VALUE_VEC3;
case olive::NodeValue::k_vec4:
return OAK_NODE_VALUE_VEC4;
case olive::NodeValue::k_combo:
return OAK_NODE_VALUE_COMBO;
case olive::NodeValue::k_file:
return OAK_NODE_VALUE_STRING;
case olive::NodeValue::k_text:
return OAK_NODE_VALUE_TEXT;
case olive::NodeValue::k_font:
return OAK_NODE_VALUE_FONT;
case olive::NodeValue::k_str_combo:
return OAK_NODE_VALUE_STR_COMBO;
case olive::NodeValue::k_binary:
return OAK_NODE_VALUE_BINARY;
case olive::NodeValue::k_bezier:
return OAK_NODE_VALUE_BEZIER;
default:
return OAK_NODE_VALUE_NONE;
}
}
// Wire the node-family events (handle validated as a Node). Appended to
// `conns`; returns false when nothing matched.
bool connect_node_event(olive::Node *node, int32_t event_id,
oakengine_event_fn fn, void *userdata,
QVector *conns)
{
using namespace olive;
switch (event_id) {
case OAKENGINE_EVENT_NODE_LABEL_CHANGED:
conns->append(QObject::connect(
node, &Node::label_changed, node,
[fn, userdata, node](const QString &label) {
const QByteArray utf = label.toUtf8();
invoke(fn, userdata, OAKENGINE_EVENT_NODE_LABEL_CHANGED, node,
0, 0, nullptr, 0, utf.constData());
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED:
conns->append(QObject::connect(
node, &Node::value_changed, node,
[fn, userdata, node](const NodeInput &input,
const TimeRange &range) {
const Rational tb = node_frame_time_base(node);
const QByteArray utf = input.input().toUtf8();
invoke(fn, userdata,
OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED, node,
input.element(),
core::Timecode::time_to_timestamp(
range.in(), tb, core::Timecode::k_round),
nullptr,
core::Timecode::time_to_timestamp(
range.out(), tb, core::Timecode::k_round),
utf.constData());
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_INPUT_CONNECTED:
case OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED: {
const bool connected =
event_id == OAKENGINE_EVENT_NODE_INPUT_CONNECTED;
auto deliver = [fn, userdata, node, event_id](
Node *output, const NodeInput &input) {
const QByteArray utf = input.input().toUtf8();
invoke(fn, userdata, event_id, node, input.element(), 0, output, 0,
utf.constData());
};
if (connected) {
conns->append(QObject::connect(node, &Node::input_connected, node,
deliver, Qt::DirectConnection));
} else {
conns->append(QObject::connect(node, &Node::input_disconnected,
node, deliver,
Qt::DirectConnection));
}
return true;
}
case OAKENGINE_EVENT_NODE_INPUT_FLAGS_CHANGED:
conns->append(QObject::connect(
node, &Node::input_flags_changed, node,
[fn, userdata, node](const QString &input,
const InputFlags &flags) {
const QByteArray utf = input.toUtf8();
invoke(fn, userdata, OAKENGINE_EVENT_NODE_INPUT_FLAGS_CHANGED,
node, int64_t(flags.value()), 0, nullptr, 0,
utf.constData());
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED:
conns->append(QObject::connect(
node, &Node::input_property_changed, node,
[fn, userdata, node](const QString &input, const QString &,
const QVariant &) {
const QByteArray utf = input.toUtf8();
invoke(fn, userdata,
OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED, node, 0, 0,
nullptr, 0, utf.constData());
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_INPUT_DATA_TYPE_CHANGED:
conns->append(QObject::connect(
node, &Node::input_data_type_changed, node,
[fn, userdata, node](const QString &input, NodeValue::Type type) {
const QByteArray utf = input.toUtf8();
invoke(fn, userdata,
OAKENGINE_EVENT_NODE_INPUT_DATA_TYPE_CHANGED, node,
node_value_type_to_c(type), 0, nullptr, 0,
utf.constData());
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED:
conns->append(QObject::connect(
node, &Node::input_array_size_changed, node,
[fn, userdata, node](const QString &input, int old_size,
int new_size) {
const QByteArray utf = input.toUtf8();
invoke(fn, userdata,
OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED, node,
old_size, new_size, nullptr, 0, utf.constData());
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED:
conns->append(QObject::connect(
node, &Node::keyframe_enable_changed, node,
[fn, userdata, node](const NodeInput &input, bool enabled) {
const QByteArray utf = input.input().toUtf8();
invoke(fn, userdata,
OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED, node,
input.element(), enabled ? 1 : 0, nullptr, 0,
utf.constData());
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_KEYFRAME_ADDED:
case OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED: {
auto deliver = [fn, userdata, node, event_id](OakEngineKeyframe *k) {
auto *key = reinterpret_cast(k);
const QByteArray utf = key->input().toUtf8();
invoke(fn, userdata, event_id, node, key->element(), key->track(),
k, 0, utf.constData());
};
if (event_id == OAKENGINE_EVENT_NODE_KEYFRAME_ADDED) {
conns->append(QObject::connect(node, &Node::keyframe_added, node,
deliver, Qt::DirectConnection));
} else {
conns->append(QObject::connect(node, &Node::keyframe_removed,
node, deliver,
Qt::DirectConnection));
}
return true;
}
case OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED:
case OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED:
case OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED: {
auto deliver = [fn, userdata, node, event_id](OakEngineKeyframe *k) {
invoke(fn, userdata, event_id, node, 0, 0, k);
};
if (event_id == OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED) {
conns->append(QObject::connect(node, &Node::keyframe_time_changed,
node, deliver,
Qt::DirectConnection));
} else if (event_id == OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED) {
conns->append(QObject::connect(node, &Node::keyframe_type_changed,
node, deliver,
Qt::DirectConnection));
} else {
conns->append(QObject::connect(node,
&Node::keyframe_value_changed,
node, deliver,
Qt::DirectConnection));
}
return true;
}
case OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT:
case OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT: {
auto deliver = [fn, userdata, node, event_id](Node *child) {
invoke(fn, userdata, event_id, node, 0, 0, child);
};
if (event_id == OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT) {
conns->append(QObject::connect(node, &Node::node_added_to_context,
node, deliver,
Qt::DirectConnection));
} else {
conns->append(QObject::connect(node,
&Node::node_removed_from_context,
node, deliver,
Qt::DirectConnection));
}
return true;
}
case OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED:
conns->append(QObject::connect(
node, &Node::message_count_changed, node,
[fn, userdata, node]() {
invoke(fn, userdata, OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED,
node, 0, 0, nullptr);
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED:
case OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED: {
auto *group = dynamic_cast(node);
if (!group) {
return false;
}
auto deliver = [fn, userdata, node, event_id](NodeGroup *,
const NodeInput &input) {
const QByteArray id = input.input().toUtf8();
invoke(fn, userdata, event_id, node, input.element(), 0,
input.node(), 0, id.constData());
};
if (event_id == OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED) {
conns->append(QObject::connect(
group, &NodeGroup::input_passthrough_added, group, deliver,
Qt::DirectConnection));
} else {
conns->append(QObject::connect(
group, &NodeGroup::input_passthrough_removed, group, deliver,
Qt::DirectConnection));
}
return true;
}
case OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED: {
auto *group = dynamic_cast(node);
if (!group) {
return false;
}
conns->append(QObject::connect(
group, &NodeGroup::output_passthrough_changed, group,
[fn, userdata, node](NodeGroup *, Node *output) {
invoke(fn, userdata,
OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED, node,
0, 0, output);
},
Qt::DirectConnection));
return true;
}
case OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED:
conns->append(QObject::connect(
node, &Node::node_position_in_context_changed, node,
[fn, userdata, node](Node *child, const QPointF &pos) {
int64_t xb, yb;
const double x = pos.x(), y = pos.y();
memcpy(&xb, &x, sizeof(xb));
memcpy(&yb, &y, sizeof(yb));
invoke(fn, userdata,
OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED, node, xb,
yb, child);
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_LINKS_CHANGED:
conns->append(QObject::connect(
node, &Node::links_changed, node,
[fn, userdata, node]() {
invoke(fn, userdata, OAKENGINE_EVENT_NODE_LINKS_CHANGED, node,
0, 0, nullptr);
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_COLOR_CHANGED:
conns->append(QObject::connect(
node, &Node::color_changed, node,
[fn, userdata, node]() {
invoke(fn, userdata, OAKENGINE_EVENT_NODE_COLOR_CHANGED, node,
0, 0, nullptr);
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_INPUT_ADDED:
conns->append(QObject::connect(
node, &Node::input_added, node,
[fn, userdata, node](const QString &id) {
QByteArray utf = id.toUtf8();
invoke(fn, userdata, OAKENGINE_EVENT_NODE_INPUT_ADDED, node,
0, 0, nullptr, 0, utf.constData());
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_INPUT_REMOVED:
conns->append(QObject::connect(
node, &Node::input_removed, node,
[fn, userdata, node](const QString &id) {
QByteArray utf = id.toUtf8();
invoke(fn, userdata, OAKENGINE_EVENT_NODE_INPUT_REMOVED, node,
0, 0, nullptr, 0, utf.constData());
},
Qt::DirectConnection));
return true;
case OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH:
conns->append(QObject::connect(
node, &Node::removed_from_graph, node,
[fn, userdata, node](olive::Project *project) {
invoke(fn, userdata, OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH,
node,
0, 0, reinterpret_cast(project));
},
Qt::DirectConnection));
return true;
default:
return false;
}
}
// The sequence's frame duration as a Rational timebase, like timeline.cpp.
bool time_base_of(const olive::Sequence *s, olive::Rational *out)
{
const olive::Rational frame_rate = s->get_video_params().frame_rate();
if (frame_rate.isNull() || frame_rate.isNaN()) {
return false;
}
*out = frame_rate.flipped();
return true;
}
int64_t time_to_ts(const olive::Rational &time, const olive::Rational &tb)
{
return olive::core::Timecode::time_to_timestamp(
time, tb, olive::core::Timecode::k_round);
}
// Block range as frame timestamps in the track's sequence timebase; -1/-1
// when the block is not on a sequenced track at emission time.
void block_timestamps(const olive::Block *block, int64_t *in_ts,
int64_t *out_ts)
{
*in_ts = -1;
*out_ts = -1;
if (!block || !block->track() || !block->track()->sequence()) {
return;
}
olive::Rational tb;
if (!time_base_of(block->track()->sequence(), &tb)) {
return;
}
*in_ts = time_to_ts(block->in(), tb);
*out_ts = time_to_ts(block->out(), tb);
}
int64_t marker_timestamp(const olive::Sequence *seq,
const olive::TimelineMarker *marker)
{
olive::Rational tb;
if (!marker || !time_base_of(seq, &tb)) {
return -1;
}
return time_to_ts(marker->time().in(), tb);
}
// Wire the connections for one subscription. `obj` is the validated engine
// object (already cast-checked). Returns the connection list, empty when
// the event family does not match `obj`.
QVector connect_event(
QObject *obj, int32_t event_id, oakengine_event_fn fn, void *userdata)
{
using namespace olive;
QVector conns;
switch (event_id) {
case OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED: {
auto *project = dynamic_cast(obj);
if (!project) {
break;
}
conns.append(QObject::connect(
project, &Project::modified_changed, project,
[fn, userdata, project](bool modified) {
invoke(fn, userdata, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED,
project, modified ? 1 : 0, 0, nullptr);
},
Qt::DirectConnection));
break;
}
case OAKENGINE_EVENT_PROJECT_NAME_CHANGED: {
auto *project = dynamic_cast(obj);
if (!project) {
break;
}
conns.append(QObject::connect(
project, &Project::name_changed, project,
[fn, userdata, project]() {
invoke(fn, userdata, OAKENGINE_EVENT_PROJECT_NAME_CHANGED,
project, 0, 0, nullptr);
},
Qt::DirectConnection));
break;
}
case OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM:
case OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM:
case OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM:
case OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM: {
auto *folder = dynamic_cast(obj);
if (!folder) {
break;
}
if (event_id == OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM) {
conns.append(QObject::connect(
folder, &Folder::begin_insert_item, folder,
[fn, userdata, folder](Node *child, int index) {
invoke(fn, userdata, OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM,
folder, index, 0, child);
},
Qt::DirectConnection));
} else if (event_id == OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM) {
conns.append(QObject::connect(
folder, &Folder::end_insert_item, folder,
[fn, userdata, folder]() {
invoke(fn, userdata, OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM,
folder, 0, 0, nullptr);
},
Qt::DirectConnection));
} else if (event_id == OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM) {
conns.append(QObject::connect(
folder, &Folder::begin_remove_item, folder,
[fn, userdata, folder](Node *child, int index) {
invoke(fn, userdata, OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM,
folder, index, 0, child);
},
Qt::DirectConnection));
} else {
conns.append(QObject::connect(
folder, &Folder::end_remove_item, folder,
[fn, userdata, folder]() {
invoke(fn, userdata, OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM,
folder, 0, 0, nullptr);
},
Qt::DirectConnection));
}
break;
}
case OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED:
case OAKENGINE_EVENT_SEQUENCE_TRACK_REMOVED: {
auto *seq = dynamic_cast(obj);
if (!seq) {
break;
}
if (event_id == OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED) {
conns.append(QObject::connect(
seq, &Sequence::track_added, seq,
[fn, userdata, seq](Track *track) {
invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED,
seq, track ? int(track->type()) : -1, 0, track);
},
Qt::DirectConnection));
} else {
conns.append(QObject::connect(
seq, &Sequence::track_removed, seq,
[fn, userdata, seq](Track *track) {
invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_TRACK_REMOVED,
seq, track ? int(track->type()) : -1, 0, track);
},
Qt::DirectConnection));
}
break;
}
case OAKENGINE_EVENT_TRACK_BLOCK_ADDED:
case OAKENGINE_EVENT_TRACK_BLOCK_REMOVED: {
auto *track = dynamic_cast