/*** 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(obj); if (!track) { break; } if (event_id == OAKENGINE_EVENT_TRACK_BLOCK_ADDED) { conns.append(QObject::connect( track, &Track::block_added, track, [fn, userdata, track](Block *block) { int64_t in_ts, out_ts; block_timestamps(block, &in_ts, &out_ts); invoke(fn, userdata, OAKENGINE_EVENT_TRACK_BLOCK_ADDED, track, in_ts, out_ts, block); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( track, &Track::block_removed, track, [fn, userdata, track](Block *block) { int64_t in_ts, out_ts; block_timestamps(block, &in_ts, &out_ts); invoke(fn, userdata, OAKENGINE_EVENT_TRACK_BLOCK_REMOVED, track, in_ts, out_ts, block); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_TRACK_INDEX_CHANGED: case OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED: case OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED: case OAKENGINE_EVENT_TRACK_MUTED_CHANGED: { auto *track = dynamic_cast(obj); if (!track) { break; } if (event_id == OAKENGINE_EVENT_TRACK_INDEX_CHANGED) { conns.append(QObject::connect( track, &Track::index_changed, track, [fn, userdata, track](int old_index, int new_index) { invoke(fn, userdata, OAKENGINE_EVENT_TRACK_INDEX_CHANGED, track, old_index, new_index, nullptr); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED) { conns.append(QObject::connect( track, &Track::track_height_changed, track, [fn, userdata, track](qreal height) { int64_t bits; const double h = double(height); memcpy(&bits, &h, sizeof(bits)); invoke(fn, userdata, OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED, track, bits, 0, nullptr); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED) { conns.append(QObject::connect( track, &Track::blocks_refreshed, track, [fn, userdata, track]() { invoke(fn, userdata, OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED, track, 0, 0, nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( track, &Track::muted_changed, track, [fn, userdata, track](bool muted) { invoke(fn, userdata, OAKENGINE_EVENT_TRACK_MUTED_CHANGED, track, muted ? 1 : 0, 0, nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED: case OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED: { auto *block = dynamic_cast(obj); if (!block) { break; } if (event_id == OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED) { conns.append(QObject::connect( block, &Block::enabled_changed, block, [fn, userdata, block]() { invoke(fn, userdata, OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED, block, 0, 0, nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( block, &Block::preview_changed, block, [fn, userdata, block]() { invoke(fn, userdata, OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED, block, 0, 0, nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED: case OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED: { auto *seq = dynamic_cast(obj); if (!seq) { break; } for (int type = 0; type < 3; type++) { TrackList *list = seq->track_list(static_cast(type)); if (!list) { continue; } if (event_id == OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED) { conns.append(QObject::connect( list, &TrackList::track_list_changed, seq, [fn, userdata, seq, type]() { invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED, seq, type, 0, nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( list, &TrackList::track_height_changed, seq, [fn, userdata, seq, type](Track *track, int height) { invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED, seq, type, height, track); }, Qt::DirectConnection)); } } break; } case OAKENGINE_EVENT_SEQUENCE_SUBTITLES_CHANGED: { auto *seq = dynamic_cast(obj); if (!seq) { break; } conns.append(QObject::connect( seq, &Sequence::subtitles_changed, seq, [fn, userdata, seq](const TimeRange &range) { olive::Rational tb; int64_t in_ts = -1, out_ts = -1; if (time_base_of(seq, &tb)) { in_ts = time_to_ts(range.in(), tb); out_ts = time_to_ts(range.out(), tb); } invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_SUBTITLES_CHANGED, seq, in_ts, out_ts, nullptr); }, Qt::DirectConnection)); break; } case OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED: case OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED: case OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED: { auto *markers = dynamic_cast(obj); if (!markers) { break; } auto deliver = [fn, userdata, markers, event_id]( TimelineMarker *marker) { invoke(fn, userdata, event_id, markers, 0, 0, marker); }; if (event_id == OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED) { conns.append(QObject::connect(markers, &TimelineMarkerList::marker_added, markers, deliver, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED) { conns.append(QObject::connect(markers, &TimelineMarkerList::marker_removed, markers, deliver, Qt::DirectConnection)); } else { conns.append(QObject::connect(markers, &TimelineMarkerList::marker_modified, markers, deliver, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED: case OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED: { auto *workarea = dynamic_cast(obj); if (!workarea) { break; } if (event_id == OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED) { conns.append(QObject::connect( workarea, &TimelineWorkArea::range_changed, workarea, [fn, userdata, workarea](const TimeRange &) { invoke(fn, userdata, OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED, workarea, 0, 0, nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( workarea, &TimelineWorkArea::enabled_changed, workarea, [fn, userdata, workarea](bool enabled) { invoke(fn, userdata, OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED, workarea, enabled ? 1 : 0, 0, nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED: case OAKENGINE_EVENT_SEQUENCE_MARKER_REMOVED: case OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED: { auto *seq = dynamic_cast(obj); if (!seq) { break; } TimelineMarkerList *markers = seq->get_markers(); if (event_id == OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED) { conns.append(QObject::connect( markers, &TimelineMarkerList::marker_added, seq, [fn, userdata, seq](TimelineMarker *marker) { invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED, seq, marker_timestamp(seq, marker), 0, nullptr); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_SEQUENCE_MARKER_REMOVED) { conns.append(QObject::connect( markers, &TimelineMarkerList::marker_removed, seq, [fn, userdata, seq](TimelineMarker *marker) { invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_MARKER_REMOVED, seq, marker_timestamp(seq, marker), 0, nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( markers, &TimelineMarkerList::marker_modified, seq, [fn, userdata, seq](TimelineMarker *marker) { invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED, seq, marker_timestamp(seq, marker), 0, nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED: case OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED: { auto *seq = dynamic_cast(obj); if (!seq) { break; } TimelineWorkArea *workarea = seq->get_work_area(); if (event_id == OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED) { conns.append(QObject::connect( workarea, &TimelineWorkArea::range_changed, seq, [fn, userdata, seq](const TimeRange &range) { olive::Rational tb; int64_t in_ts = -1, out_ts = -1; if (time_base_of(seq, &tb)) { in_ts = time_to_ts(range.in(), tb); out_ts = time_to_ts(range.out(), tb); } invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED, seq, in_ts, out_ts, nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( workarea, &TimelineWorkArea::enabled_changed, seq, [fn, userdata, seq](bool enabled) { invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED, seq, enabled ? 1 : 0, 0, nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED: case OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED: { auto *cm = dynamic_cast(obj); if (!cm) { break; } if (event_id == OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED) { conns.append(QObject::connect( cm, &ColorManager::config_changed, cm, [fn, userdata, cm](const QString &) { invoke(fn, userdata, OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED, cm, 0, 0, nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( cm, &ColorManager::reference_space_changed, cm, [fn, userdata, cm](const QString &) { invoke(fn, userdata, OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED, cm, 0, 0, nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED: case OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED: case OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED: case OAKENGINE_EVENT_VIEWER_SIZE_CHANGED: case OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED: case OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED: case OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED: case OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED: case OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED: case OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED: case OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED: { auto *viewer = dynamic_cast(obj); if (!viewer) { break; } // Rational payloads are a = numerator, b = denominator (seconds). auto deliver_rational = [fn, userdata, viewer, event_id]( const Rational &r) { invoke(fn, userdata, event_id, viewer, r.numerator(), r.denominator(), nullptr); }; if (event_id == OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED) { conns.append(QObject::connect(viewer, &ViewerOutput::length_changed, viewer, deliver_rational, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED) { conns.append(QObject::connect(viewer, &ViewerOutput::playhead_changed, viewer, deliver_rational, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED) { conns.append(QObject::connect(viewer, &ViewerOutput::frame_rate_changed, viewer, deliver_rational, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_VIEWER_SIZE_CHANGED) { conns.append(QObject::connect( viewer, &ViewerOutput::size_changed, viewer, [fn, userdata, viewer](int width, int height) { invoke(fn, userdata, OAKENGINE_EVENT_VIEWER_SIZE_CHANGED, viewer, width, height, nullptr); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED) { conns.append(QObject::connect(viewer, &ViewerOutput::pixel_aspect_changed, viewer, deliver_rational, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED) { conns.append(QObject::connect( viewer, &ViewerOutput::interlacing_changed, viewer, [fn, userdata, viewer](VideoParams::Interlacing mode) { invoke(fn, userdata, OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED, viewer, int64_t(mode), 0, nullptr); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED) { conns.append(QObject::connect( viewer, &ViewerOutput::video_params_changed, viewer, [fn, userdata, viewer]() { invoke(fn, userdata, OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED, viewer, 0, 0, nullptr); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED) { conns.append(QObject::connect( viewer, &ViewerOutput::audio_params_changed, viewer, [fn, userdata, viewer]() { invoke(fn, userdata, OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED, viewer, 0, 0, nullptr); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED) { conns.append(QObject::connect( viewer, &ViewerOutput::texture_input_changed, viewer, [fn, userdata, viewer]() { invoke(fn, userdata, OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED, viewer, 0, 0, nullptr); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED) { conns.append(QObject::connect( viewer, &ViewerOutput::sample_rate_changed, viewer, [fn, userdata, viewer](int sr) { invoke(fn, userdata, OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED, viewer, sr, 0, nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( viewer, &ViewerOutput::connected_waveform_changed, viewer, [fn, userdata, viewer]() { invoke(fn, userdata, OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED, viewer, 0, 0, nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED: case OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED: case OAKENGINE_EVENT_TASK_MANAGER_TASK_FAILED: case OAKENGINE_EVENT_TASK_MANAGER_LIST_CHANGED: { auto *manager = dynamic_cast(obj); if (!manager) { break; } if (event_id == OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED) { conns.append(QObject::connect( manager, &TaskManager::task_added, manager, [fn, userdata, manager](Task *t) { const QByteArray title = t->get_title().toUtf8(); invoke(fn, userdata, OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED, manager, 0, 0, t, 0, title.constData()); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED) { conns.append(QObject::connect( manager, &TaskManager::task_removed, manager, [fn, userdata, manager](Task *t) { invoke(fn, userdata, OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED, manager, 0, 0, t); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_TASK_MANAGER_TASK_FAILED) { conns.append(QObject::connect( manager, &TaskManager::task_failed, manager, [fn, userdata, manager](Task *t) { invoke(fn, userdata, OAKENGINE_EVENT_TASK_MANAGER_TASK_FAILED, manager, 0, 0, t); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( manager, &TaskManager::task_list_changed, manager, [fn, userdata, manager]() { invoke(fn, userdata, OAKENGINE_EVENT_TASK_MANAGER_LIST_CHANGED, manager, 0, 0, nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_TASK_STARTED: case OAKENGINE_EVENT_TASK_PROGRESS: case OAKENGINE_EVENT_TASK_FINISHED: { auto *task = dynamic_cast(obj); if (!task) { break; } if (event_id == OAKENGINE_EVENT_TASK_STARTED) { conns.append(QObject::connect( task, &Task::started, task, [fn, userdata, task](qint64 start_time) { invoke(fn, userdata, OAKENGINE_EVENT_TASK_STARTED, task, start_time, 0, nullptr); }, Qt::DirectConnection)); } else if (event_id == OAKENGINE_EVENT_TASK_PROGRESS) { conns.append(QObject::connect( task, &Task::progress_changed, task, [fn, userdata, task](double d) { int64_t bits; static_assert(sizeof(bits) == sizeof(d)); memcpy(&bits, &d, sizeof(bits)); invoke(fn, userdata, OAKENGINE_EVENT_TASK_PROGRESS, task, bits, 0, nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( task, &Task::finished, task, [fn, userdata](Task *t, bool succeeded) { invoke(fn, userdata, OAKENGINE_EVENT_TASK_FINISHED, t, succeeded ? 1 : 0, 0, nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_UNDO_INDEX_CHANGED: { auto *undo_stack = dynamic_cast(obj); if (!undo_stack) { break; } conns.append(QObject::connect( undo_stack, &UndoStack::index_changed, undo_stack, [fn, userdata, undo_stack](int i) { invoke(fn, userdata, OAKENGINE_EVENT_UNDO_INDEX_CHANGED, undo_stack, i, 0, nullptr); }, Qt::DirectConnection)); break; } case OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED: { auto *audio_manager = dynamic_cast(obj); if (!audio_manager) { break; } conns.append(QObject::connect( audio_manager, &AudioManager::output_params_changed, audio_manager, [fn, userdata, audio_manager]() { invoke(fn, userdata, OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED, audio_manager, 0, 0, nullptr); }, Qt::DirectConnection)); break; } case OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_NOTIFY: { auto *audio_manager = dynamic_cast(obj); if (!audio_manager) { break; } conns.append(QObject::connect( audio_manager, &AudioManager::output_notify, audio_manager, [fn, userdata, audio_manager]() { invoke(fn, userdata, OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_NOTIFY, audio_manager, 0, 0, nullptr); }, Qt::DirectConnection)); break; } case OAKENGINE_EVENT_PLAYBACK_CACHE_INVALIDATED: case OAKENGINE_EVENT_PLAYBACK_CACHE_VALIDATED: { auto *cache = dynamic_cast(obj); if (!cache) { break; } if (event_id == OAKENGINE_EVENT_PLAYBACK_CACHE_INVALIDATED) { conns.append(QObject::connect( cache, &PlaybackCache::invalidated, cache, [fn, userdata, cache](const TimeRange &r) { invoke(fn, userdata, OAKENGINE_EVENT_PLAYBACK_CACHE_INVALIDATED, cache, r.in().numerator(), r.in().denominator(), nullptr); }, Qt::DirectConnection)); } else { conns.append(QObject::connect( cache, &PlaybackCache::validated, cache, [fn, userdata, cache](const TimeRange &r) { invoke(fn, userdata, OAKENGINE_EVENT_PLAYBACK_CACHE_VALIDATED, cache, r.in().numerator(), r.in().denominator(), nullptr); }, Qt::DirectConnection)); } break; } case OAKENGINE_EVENT_FRAME_CACHE_INVALIDATED: { auto *cache = dynamic_cast(obj); if (!cache) { break; } conns.append(QObject::connect( cache, &PlaybackCache::invalidated, cache, [fn, userdata, cache](const TimeRange &r) { invoke(fn, userdata, OAKENGINE_EVENT_FRAME_CACHE_INVALIDATED, cache, r.in().numerator(), r.in().denominator(), nullptr); }, Qt::DirectConnection)); break; } default: break; } if (conns.isEmpty()) { if (auto *node = dynamic_cast(obj)) { connect_node_event(node, event_id, fn, userdata, &conns); } } return conns; } } // namespace extern "C" int64_t oakengine_event_subscribe(void *handle, int32_t event_id, oakengine_event_fn fn, void *userdata) { if (!handle || !fn) { return 0; } // Every facade handle is the engine QObject pointer itself (see // timeline.cpp/project.cpp wrap()); dynamic_cast from QObject* both // validates the family match and is safe across the Node/Project split. auto *obj = reinterpret_cast(handle); QVector conns = connect_event(obj, event_id, fn, userdata); if (conns.isEmpty()) { return 0; } // Drop the registry entry automatically when the observed object dies so // a stale subscription id is never a dangling engine pointer. Qt removes // the signal connections itself; only the map entry needs cleanup. const int64_t id = g_next_id.fetch_add(1); conns.append(QObject::connect(obj, &QObject::destroyed, obj, [id]() { drop_subscription(id); }, Qt::DirectConnection)); QMutexLocker locker(&g_registry_mutex); g_registry.insert(id, Subscription{std::move(conns)}); return id; } extern "C" int oakengine_event_unsubscribe(int64_t id) { if (id <= 0) { return OAKENGINE_E_INVALID; } QMutexLocker locker(&g_registry_mutex); const auto it = g_registry.find(id); if (it == g_registry.end()) { return OAKENGINE_E_NOT_FOUND; } for (const QMetaObject::Connection &conn : it->connections) { QObject::disconnect(conn); } g_registry.erase(it); return OAKENGINE_OK; }