R8: finish app/ pure C ABI migration (P3-P9) and make OTIO required

- app/ no longer includes engine C++ headers nor holds engine C++ types:
  engine access goes through the oakengine C ABI plus C++ wrappers
  (oakutil/oaknode.h, oakutil/oakvideo.h) and app-local mirror types
  (tooltypes, trackreferencehandle, timelinecommonapp, keyframetypes,
  subtitleapp, serializedlayoutinfoapp, nodevaluehandle, sliderdisplaytypeapp)
- engine: new C ABI functions for block/track/clip/transition navigation
  and predicates, links, caches, waveform/playback, disk folder,
  sequence_track_list, node_free, footage_is_valid, block_get_track,
  get_brush; loadotio/saveotio ported to the current engine API
- OTIO is now a required dependency: CI and CD build it on every
  platform, FindOpenTimelineIO fixed for OTIO 0.16/0.19 (the old deps
  include requirement silently disabled OTIO everywhere), runtime
  libraries are bundled into packages and copied next to macOS binaries
  (oak_copy_otio_runtime)
- fix ProjectViewModel drag&drop mime read/write size mismatch (segfault)
- unify color label naming (k_olive -> "Oak") in the app-side mirror
- docs: OTIO required, FFmpeg minimum corrected to 6.0 (en/zh)
- gtest suite: 1925 passed, 0 failed
This commit is contained in:
2026-07-31 22:46:52 +08:00
parent 18aed979a2
commit 66d761b4b7
285 changed files with 13261 additions and 5895 deletions
+303 -170
View File
@@ -30,11 +30,13 @@
#include "engineeventbridge.h"
#include "common/current.h"
#include "dialog/markerproperties/markerpropertiesdialog.h"
#include "node/project/sequence/sequence.h"
#include "oakengine/node.h"
#include "oakengine/timeline.h"
#include "oakengine/viewer.h"
#include "oakengine/timeline.h"
#include "oakengine/undo.h"
#include "widget/keyframeview/keyframehandle.h"
#include "widget/timeruler/markerhandle.h"
#include "widget/timeruler/timeruler.h"
#include "widget/timelinewidget/cliphandle.h"
@@ -86,21 +88,29 @@ void TimeBasedWidget::set_scale_and_center_on_playhead(const double &scale)
QTimer::singleShot(0, this, &TimeBasedWidget::center_scroll_on_playhead);
}
ViewerOutput *TimeBasedWidget::get_connected_node() const
OakEngineNode *TimeBasedWidget::get_connected_node() const
{
return viewer_node_.data();
// The handle is the same object the QPointer tracks (the documented
// QPointer<ViewerOutput> exception in timebasedwidget.h); bridge once
// here so the rest of the app only ever sees the C ABI handle.
return reinterpret_cast<OakEngineNode *>(viewer_node_.data());
}
void TimeBasedWidget::connect_viewer_node(ViewerOutput *node)
void TimeBasedWidget::connect_viewer_node(OakEngineNode *node)
{
// viewer_node_ is the documented QPointer<ViewerOutput> exception (see
// timebasedwidget.h): the incoming handle is the same engine object,
// bridged once here for QPointer's null-on-destroy semantics.
ViewerOutput *viewer = reinterpret_cast<ViewerOutput *>(node);
// Ignore no-op
if (viewer_node_ == node) {
if (viewer_node_ == viewer) {
return;
}
// Set viewer node
ViewerOutput *old = viewer_node_.data();
viewer_node_ = node;
OakEngineNode *old = get_connected_node();
viewer_node_ = viewer;
// Disconnect old bridge subscriptions and connections
disconnect(bridge_, nullptr, this, nullptr);
@@ -108,15 +118,23 @@ void TimeBasedWidget::connect_viewer_node(ViewerOutput *node)
if (viewer_node_) {
oak_video_params vp;
oakengine_viewer_get_video_params(
reinterpret_cast<OakEngineNode *>(viewer_node_.data()), 0, &vp);
// We still need Current class - keep using it for now
oakengine_viewer_get_video_params(node, 0, &vp);
// We still need Current class - keep using it for now. It stores an
// engine-side olive::VideoParams, so bridge through the C ABI
// create/free pair (setCurrentVideoParams copies the value, the
// engine object is released immediately).
void *engine_vp =
viewer_output_video_params(viewer_node_).create_engine_params();
Current::getInstance().setCurrentVideoParams(
viewer_output_video_params(viewer_node_));
*static_cast<olive::VideoParams *>(engine_vp));
oakengine_video_params_free(engine_vp);
Current::getInstance().setCurrentAudioParams(
viewer_output_audio_params(viewer_node_));
} else {
Current::getInstance().setCurrentVideoParams(empty_video_params());
void *engine_vp = empty_video_params().create_engine_params();
Current::getInstance().setCurrentVideoParams(
*static_cast<olive::VideoParams *>(engine_vp));
oakengine_video_params_free(engine_vp);
Current::getInstance().setCurrentAudioParams(AudioParams());
}
if (old) {
@@ -133,18 +151,15 @@ void TimeBasedWidget::connect_viewer_node(ViewerOutput *node)
// Call derivatives
for (TimeBasedView *view : timeline_views_) {
view->set_viewer_node(viewer_node_.data());
view->set_viewer_node(reinterpret_cast<OakEngineNode *>(viewer_node_.data()));
}
ConnectedNodeChangeEvent(viewer_node_.data());
ConnectedNodeChangeEvent(node);
if (viewer_node_) {
OakEngineNode *handle =
reinterpret_cast<OakEngineNode *>(viewer_node_.data());
// Subscribe to viewer events via bridge
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED);
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED);
bridge_->subscribe(handle, OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH);
bridge_->subscribe(node, OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED);
bridge_->subscribe(node, OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED);
bridge_->subscribe(node, OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH);
connect(bridge_, &EngineEventBridge::viewer_length_changed, this,
[this](OakEngineNode *, qint64, qint64) {
@@ -162,14 +177,14 @@ void TimeBasedWidget::connect_viewer_node(ViewerOutput *node)
});
// Connect ruler and scrollbar to timeline points
connect_work_area(viewer_node_->get_work_area());
connect_markers(viewer_node_->get_markers());
connect_work_area(oakengine_viewer_get_workarea_handle(node));
connect_markers(oakengine_viewer_get_marker_list(node));
// If we're setting the timebase, set it automatically based on the video and audio parameters
if (auto_set_timebase_) {
auto_update_timebase();
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED);
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED);
bridge_->subscribe(node, OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED);
bridge_->subscribe(node, OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED);
connect(bridge_, &EngineEventBridge::viewer_frame_rate_changed, this,
[this](OakEngineNode *, qint64, qint64) {
auto_update_timebase();
@@ -181,23 +196,22 @@ void TimeBasedWidget::connect_viewer_node(ViewerOutput *node)
}
// Call derivatives
ConnectNodeEvent(viewer_node_.data());
ConnectNodeEvent(node);
}
update_maximum_scroll();
emit connected_node_changed(reinterpret_cast<OakEngineNode *>(old),
reinterpret_cast<OakEngineNode *>(node));
emit connected_node_changed(old, node);
}
void TimeBasedWidget::connect_work_area(TimelineWorkArea *workarea)
void TimeBasedWidget::connect_work_area(OakEngineWorkarea *workarea)
{
workarea_ = workarea;
ruler()->set_work_area(workarea);
scrollbar_->connect_work_area(workarea);
}
void TimeBasedWidget::connect_markers(TimelineMarkerList *markers)
void TimeBasedWidget::connect_markers(OakEngineMarkerList *markers)
{
markers_ = markers;
ruler()->set_markers(markers);
@@ -206,7 +220,7 @@ void TimeBasedWidget::connect_markers(TimelineMarkerList *markers)
void TimeBasedWidget::update_maximum_scroll()
{
Rational length = (viewer_node_) ? viewer_node_->get_length() : 0;
Rational length = (viewer_node_) ? viewer_output_length(viewer_node_.data()) : 0;
if (auto_max_scrollbar_) {
scrollbar_->setMaximum(
@@ -267,7 +281,7 @@ void TimeBasedWidget::page_scroll_to_playhead()
{
if (get_connected_node()) {
page_scroll_internal(
qRound(time_to_scene(get_connected_node()->get_playhead())), true);
qRound(time_to_scene(viewer_output_playhead(get_connected_node()))), true);
}
}
@@ -275,7 +289,7 @@ void TimeBasedWidget::catch_up_scroll_to_playhead()
{
if (get_connected_node()) {
catch_up_scroll_to_point(
qRound(time_to_scene(get_connected_node()->get_playhead())));
qRound(time_to_scene(viewer_output_playhead(get_connected_node()))));
}
}
@@ -486,68 +500,125 @@ void TimeBasedWidget::zoom_out()
void TimeBasedWidget::go_to_prev_cut()
{
// Cuts are only possible in sequences
Sequence *sequence = dynamic_cast<Sequence *>(viewer_node_.data());
OakEngineNode *sequence_node = get_connected_node();
if (!sequence) {
if (!oakengine_node_is_sequence(sequence_node)) {
return;
}
if (get_connected_node()->get_playhead().isNull()) {
const Rational playhead = viewer_output_playhead(sequence_node);
if (playhead.isNull()) {
return;
}
OakEngineSequence *sequence =
reinterpret_cast<OakEngineSequence *>(sequence_node);
Rational closest_cut = 0;
for (Track *track : sequence->get_tracks()) {
Rational this_track_closest_cut = 0;
// Iterate all track lists (video, audio, subtitle), mirroring
// Sequence::get_tracks(). The per-type counts line up with the
// OAKENGINE_TRACK_TYPE_* ordinals (0..2).
int track_counts[3] = { 0, 0, 0 };
oakengine_sequence_track_count(sequence, &track_counts[0],
&track_counts[1], &track_counts[2]);
for (Block *block : track->blocks()) {
if (block->out() < get_connected_node()->get_playhead()) {
this_track_closest_cut = block->out();
} else {
break;
for (int type = 0; type < 3; type++) {
for (int ti = 0; ti < track_counts[type]; ti++) {
OakEngineTrack *track =
oakengine_sequence_track_at(sequence, type, ti);
if (!track) {
continue;
}
}
closest_cut = qMax(closest_cut, this_track_closest_cut);
Rational this_track_closest_cut = 0;
const int block_count = oakengine_track_block_count(track);
for (int bi = 0; bi < block_count; bi++) {
OakEngineBlock *block = oakengine_track_block_at(track, bi);
int out_num = 0, out_den = 1;
oakengine_block_get_out_rational(
reinterpret_cast<const OakEngineNode *>(block), &out_num,
&out_den);
const Rational block_out(out_num, out_den);
if (block_out < playhead) {
this_track_closest_cut = block_out;
} else {
break;
}
}
closest_cut = qMax(closest_cut, this_track_closest_cut);
}
}
oakengine_viewer_set_playhead(
reinterpret_cast<OakEngineNode *>(get_connected_node()),
oakengine_viewer_set_playhead(sequence_node,
closest_cut.numerator(), closest_cut.denominator());
}
void TimeBasedWidget::go_to_next_cut()
{
// Cuts are only possible in sequences
Sequence *sequence = dynamic_cast<Sequence *>(viewer_node_.data());
OakEngineNode *sequence_node = get_connected_node();
if (!sequence) {
if (!oakengine_node_is_sequence(sequence_node)) {
return;
}
OakEngineSequence *sequence =
reinterpret_cast<OakEngineSequence *>(sequence_node);
const Rational playhead = viewer_output_playhead(sequence_node);
Rational closest_cut = RATIONAL_MAX;
for (Track *track : sequence->get_tracks()) {
Rational this_track_closest_cut = track->track_length();
// See go_to_prev_cut() for the track iteration convention.
int track_counts[3] = { 0, 0, 0 };
oakengine_sequence_track_count(sequence, &track_counts[0],
&track_counts[1], &track_counts[2]);
if (this_track_closest_cut <= get_connected_node()->get_playhead()) {
this_track_closest_cut = RATIONAL_MAX;
}
for (Block *block : track->blocks()) {
if (block->in() > get_connected_node()->get_playhead()) {
this_track_closest_cut = block->in();
break;
for (int type = 0; type < 3; type++) {
for (int ti = 0; ti < track_counts[type]; ti++) {
OakEngineTrack *track =
oakengine_sequence_track_at(sequence, type, ti);
if (!track) {
continue;
}
}
closest_cut = qMin(closest_cut, this_track_closest_cut);
const int block_count = oakengine_track_block_count(track);
// Track::track_length(): out of the last block (0 when empty)
Rational this_track_closest_cut = 0;
if (block_count > 0) {
OakEngineBlock *last =
oakengine_track_block_at(track, block_count - 1);
int out_num = 0, out_den = 1;
oakengine_block_get_out_rational(
reinterpret_cast<const OakEngineNode *>(last), &out_num,
&out_den);
this_track_closest_cut = Rational(out_num, out_den);
}
if (this_track_closest_cut <= playhead) {
this_track_closest_cut = RATIONAL_MAX;
}
for (int bi = 0; bi < block_count; bi++) {
OakEngineBlock *block = oakengine_track_block_at(track, bi);
int in_num = 0, in_den = 1;
oakengine_block_get_in_rational(
reinterpret_cast<const OakEngineNode *>(block), &in_num,
&in_den);
const Rational block_in(in_num, in_den);
if (block_in > playhead) {
this_track_closest_cut = block_in;
break;
}
}
closest_cut = qMin(closest_cut, this_track_closest_cut);
}
}
if (closest_cut < RATIONAL_MAX) {
oakengine_viewer_set_playhead(
reinterpret_cast<OakEngineNode *>(get_connected_node()),
oakengine_viewer_set_playhead(sequence_node,
closest_cut.numerator(), closest_cut.denominator());
}
}
@@ -555,8 +626,7 @@ void TimeBasedWidget::go_to_next_cut()
void TimeBasedWidget::go_to_start()
{
if (viewer_node_) {
oakengine_viewer_set_playhead(
reinterpret_cast<OakEngineNode *>(viewer_node_.data()), 0, 1);
oakengine_viewer_set_playhead(get_connected_node(), 0, 1);
}
}
@@ -564,16 +634,15 @@ void TimeBasedWidget::prev_frame()
{
if (viewer_node_) {
Rational proposed_time = Timecode::snap_time_to_timebase(
get_connected_node()->get_playhead() - timebase(), timebase(),
viewer_output_playhead(viewer_node_.data()) - timebase(), timebase(),
Timecode::k_ceil);
if (proposed_time == get_connected_node()->get_playhead()) {
if (proposed_time == viewer_output_playhead(viewer_node_.data())) {
// Catch rounding error, assume this time is snapped and just subtract a timebase
proposed_time -= timebase();
}
{
Rational _pt = qMax(Rational(0), proposed_time);
oakengine_viewer_set_playhead(
reinterpret_cast<OakEngineNode *>(viewer_node_.data()),
oakengine_viewer_set_playhead(get_connected_node(),
_pt.numerator(), _pt.denominator());
}
}
@@ -583,14 +652,13 @@ void TimeBasedWidget::next_frame()
{
if (viewer_node_) {
Rational proposed_time = Timecode::snap_time_to_timebase(
get_connected_node()->get_playhead() + timebase(), timebase(),
viewer_output_playhead(viewer_node_.data()) + timebase(), timebase(),
Timecode::k_floor);
if (proposed_time == get_connected_node()->get_playhead()) {
if (proposed_time == viewer_output_playhead(viewer_node_.data())) {
// Catch rounding error, assume this time is snapped and just add a timebase
proposed_time += timebase();
}
oakengine_viewer_set_playhead(
reinterpret_cast<OakEngineNode *>(viewer_node_.data()),
oakengine_viewer_set_playhead(get_connected_node(),
proposed_time.numerator(), proposed_time.denominator());
}
}
@@ -598,10 +666,9 @@ void TimeBasedWidget::next_frame()
void TimeBasedWidget::go_to_end()
{
if (viewer_node_) {
oakengine_viewer_set_playhead(
reinterpret_cast<OakEngineNode *>(viewer_node_.data()),
viewer_node_->get_length().numerator(),
viewer_node_->get_length().denominator());
const Rational length = viewer_output_length(viewer_node_.data());
oakengine_viewer_set_playhead(get_connected_node(),
length.numerator(), length.denominator());
}
}
@@ -609,7 +676,7 @@ void TimeBasedWidget::center_scroll_on_playhead()
{
if (get_connected_node()) {
scrollbar_->setValue(
qRound(time_to_scene(get_connected_node()->get_playhead())) -
qRound(time_to_scene(viewer_output_playhead(get_connected_node()))) -
scrollbar_->width() / 2);
}
}
@@ -619,39 +686,48 @@ void TimeBasedWidget::set_auto_set_timebase(bool e)
auto_set_timebase_ = e;
}
void TimeBasedWidget::set_point(Timeline::MovementMode m, const Rational &time)
void TimeBasedWidget::set_point(TimelineApp::MovementMode m, const Rational &time)
{
if (!viewer_node_) {
return;
}
void *command = oakengine_undo_command_create_multi();
TimelineWorkArea *points = viewer_node_->get_work_area();
OakEngineWorkarea *points =
oakengine_viewer_get_workarea_handle(get_connected_node());
// Enable workarea if it isn't already enabled
if (!points->enabled()) {
oakengine_workarea_set_enabled_undoable(
reinterpret_cast<OakEngineWorkarea *>(points), 1, command);
int64_t wa_in_num = 0, wa_in_den = 1, wa_out_num = 0, wa_out_den = 1;
int wa_enabled = 0;
oakengine_workarea_get(points, &wa_in_num, &wa_in_den, &wa_out_num,
&wa_out_den, &wa_enabled);
// Enable workarea if it isn't already enabled. Note the enable is only
// queued on `command` (applied on push below), so the in/out logic keeps
// using this pre-enable snapshot, mirroring the original undo semantics.
if (!wa_enabled) {
oakengine_workarea_set_enabled_undoable(points, 1, command);
}
// Determine our new range
Rational in_point, out_point;
if (m == Timeline::k_trim_in) {
if (m == TimelineApp::k_trim_in) {
in_point = time;
if (!points->enabled() || points->out() < in_point) {
if (!wa_enabled ||
Rational(int(wa_out_num), int(wa_out_den)) < in_point) {
out_point = RATIONAL_MAX;
} else {
out_point = points->out();
out_point = Rational(int(wa_out_num), int(wa_out_den));
}
} else {
out_point = time;
if (!points->enabled() || points->in() > out_point) {
if (!wa_enabled ||
Rational(int(wa_in_num), int(wa_in_den)) > out_point) {
in_point = Rational(0, 1);
} else {
in_point = points->in();
in_point = Rational(int(wa_in_num), int(wa_in_den));
}
}
@@ -659,12 +735,11 @@ void TimeBasedWidget::set_point(Timeline::MovementMode m, const Rational &time)
{
int64_t old_in_num, old_in_den, old_out_num, old_out_den;
int old_enabled;
oakengine_workarea_get(
reinterpret_cast<OakEngineWorkarea *>(points),
oakengine_workarea_get(points,
&old_in_num, &old_in_den, &old_out_num, &old_out_den,
&old_enabled);
oakengine_workarea_set_range_undoable(
reinterpret_cast<OakEngineWorkarea *>(points),
points,
in_point.numerator(), in_point.denominator(),
out_point.numerator(), out_point.denominator(),
old_in_num, old_in_den, old_out_num, old_out_den, command);
@@ -673,21 +748,28 @@ void TimeBasedWidget::set_point(Timeline::MovementMode m, const Rational &time)
oakengine_undo_push(command, tr("Set In/Out Point").toUtf8().constData());
}
void TimeBasedWidget::reset_point(Timeline::MovementMode m)
void TimeBasedWidget::reset_point(TimelineApp::MovementMode m)
{
if (!get_connected_node()) {
return;
}
TimelineWorkArea *points = get_connected_node()->get_work_area();
OakEngineWorkarea *points =
oakengine_viewer_get_workarea_handle(get_connected_node());
if (!points->enabled()) {
int64_t wa_in_num = 0, wa_in_den = 1, wa_out_num = 0, wa_out_den = 1;
int wa_enabled = 0;
oakengine_workarea_get(points, &wa_in_num, &wa_in_den, &wa_out_num,
&wa_out_den, &wa_enabled);
if (!wa_enabled) {
return;
}
TimeRange r = points->range();
TimeRange r{Rational(int(wa_in_num), int(wa_in_den)),
Rational(int(wa_out_num), int(wa_out_den))};
if (m == Timeline::k_trim_in) {
if (m == TimelineApp::k_trim_in) {
r.set_in(Rational(0, 1));
} else {
r.set_out(RATIONAL_MAX);
@@ -697,12 +779,11 @@ void TimeBasedWidget::reset_point(Timeline::MovementMode m)
auto reset_cmd = oakengine_undo_command_create_multi();
int64_t old_in_num, old_in_den, old_out_num, old_out_den;
int old_enabled;
oakengine_workarea_get(
reinterpret_cast<OakEngineWorkarea *>(points),
oakengine_workarea_get(points,
&old_in_num, &old_in_den, &old_out_num, &old_out_den,
&old_enabled);
oakengine_workarea_set_range_undoable(
reinterpret_cast<OakEngineWorkarea *>(points),
points,
r.in().numerator(), r.in().denominator(),
r.out().numerator(), r.out().denominator(),
old_in_num, old_in_den, old_out_num, old_out_den, reset_cmd);
@@ -756,22 +837,24 @@ bool TimeBasedWidget::user_is_dragging_playhead() const
void TimeBasedWidget::set_in_at_playhead()
{
set_point(Timeline::k_trim_in, get_connected_node()->get_playhead());
set_point(TimelineApp::k_trim_in,
viewer_output_playhead(get_connected_node()));
}
void TimeBasedWidget::set_out_at_playhead()
{
set_point(Timeline::k_trim_out, get_connected_node()->get_playhead());
set_point(TimelineApp::k_trim_out,
viewer_output_playhead(get_connected_node()));
}
void TimeBasedWidget::reset_in()
{
reset_point(Timeline::k_trim_in);
reset_point(TimelineApp::k_trim_in);
}
void TimeBasedWidget::reset_out()
{
reset_point(Timeline::k_trim_out);
reset_point(TimelineApp::k_trim_out);
}
void TimeBasedWidget::clear_in_out_points()
@@ -783,53 +866,83 @@ void TimeBasedWidget::clear_in_out_points()
{
auto clear_cmd = oakengine_undo_command_create_multi();
oakengine_workarea_set_enabled_undoable(
reinterpret_cast<OakEngineWorkarea *>(
get_connected_node()->get_work_area()),
oakengine_viewer_get_workarea_handle(get_connected_node()),
0, clear_cmd);
oakengine_undo_push(clear_cmd,
tr("Cleared In/Out Points").toUtf8().constData());
}
}
/**
* @brief TimelineMarkerList::get_closest_marker_to_time() through the C ABI:
* the list is sorted by time, so the walk stops as soon as the distance
* starts increasing.
*/
static OakEngineMarker *marker_list_closest_to_time(
OakEngineMarkerList *markers, const Rational &t)
{
OakEngineMarker *closest = nullptr;
const int count = oakengine_marker_list_count(markers);
for (int i = 0; i < count; i++) {
OakEngineMarker *m = oakengine_marker_list_at(markers, i);
Rational this_diff = qAbs(marker_time(m).in() - t);
if (closest) {
Rational stored_diff = qAbs(marker_time(closest).in() - t);
if (this_diff > stored_diff) {
// Since the list is organized by time, if the diff increases, assume we are only going
// to move further away from here and there's no need to check
break;
}
}
closest = m;
}
return closest;
}
void TimeBasedWidget::set_marker()
{
if (!get_connected_node()) {
OakEngineNode *viewer = get_connected_node();
if (!viewer) {
return;
}
TimelineMarkerList *markers = get_connected_node()->get_markers();
OakEngineMarkerList *markers = oakengine_viewer_get_marker_list(viewer);
const Rational playhead = viewer_output_playhead(viewer);
if (TimelineMarker *existing =
markers->get_marker_at_time(get_connected_node()->get_playhead())) {
if (OakEngineMarker *existing =
oakengine_marker_list_marker_at_time(
markers, playhead.numerator(), playhead.denominator())) {
// We already have a marker here, so pop open the edit dialog
MarkerPropertiesDialog mpd({ existing }, timebase(), this);
mpd.exec();
} else {
// Create a new marker and place it here
int color;
if (TimelineMarker *closest = markers->get_closest_marker_to_time(
get_connected_node()->get_playhead())) {
if (OakEngineMarker *closest =
marker_list_closest_to_time(markers, playhead)) {
// Copy color of closest marker to this time
color = closest->color();
color = marker_color(closest);
} else {
// Fallback to default color in preferences
color = OAK_CONFIG("MarkerColor").toInt();
}
const Rational playhead = get_connected_node()->get_playhead();
OakEngineMarker *marker = oakengine_marker_create(
color, playhead.numerator(), playhead.denominator(),
playhead.numerator(), playhead.denominator(), "");
TimelineMarker *cpp_marker =
reinterpret_cast<TimelineMarker *>(marker);
bool edited_in_dialog = false;
if (OAK_CONFIG("SetNameWithMarker").toBool()) {
MarkerPropertiesDialog mpd({ cpp_marker }, timebase(), this);
MarkerPropertiesDialog mpd({ marker }, timebase(), this);
if (mpd.exec() != QDialog::Accepted) {
oakengine_marker_free(marker);
marker = nullptr;
cpp_marker = nullptr;
} else {
edited_in_dialog = true;
}
@@ -839,18 +952,16 @@ void TimeBasedWidget::set_marker()
if (edited_in_dialog) {
// The dialog pushed undo commands referencing this exact
// marker object, so it must be the one added to the list.
oakengine_marker_list_add_existing(
reinterpret_cast<OakEngineMarkerList *>(markers), marker);
oakengine_marker_list_add_existing(markers, marker);
} else {
// Pristine marker: add through the liboakengine C ABI
// facade (one undoable command) and drop the temporary.
oakengine_sequence_marker_add_ex(
reinterpret_cast<OakEngineSequence *>(
get_connected_node()),
Timecode::time_to_timestamp(cpp_marker->time().in(),
reinterpret_cast<OakEngineSequence *>(viewer),
Timecode::time_to_timestamp(marker_time(marker).in(),
timebase(),
Timecode::k_round),
"", cpp_marker->color());
"", marker_color(marker));
oakengine_marker_free(marker);
}
}
@@ -880,7 +991,7 @@ void TimeBasedWidget::toggle_show_all()
toggle_show_all_old_scale_ = get_scale();
toggle_show_all_old_scroll_ = scrollbar_->value();
set_scale_from_dimensions(w, get_connected_node()->get_length().to_double());
set_scale_from_dimensions(w, viewer_output_length(get_connected_node()).to_double());
scrollbar_->setValue(0);
// Must explicitly do this because SetScale() will automatically set this to false
@@ -891,11 +1002,11 @@ void TimeBasedWidget::toggle_show_all()
void TimeBasedWidget::go_to_in()
{
if (get_connected_node()) {
if (get_connected_node()->get_work_area()->enabled()) {
oakengine_viewer_set_playhead(
reinterpret_cast<OakEngineNode *>(get_connected_node()),
get_connected_node()->get_work_area()->in().numerator(),
get_connected_node()->get_work_area()->in().denominator());
oakengine_viewer_workarea wa;
oakengine_viewer_get_workarea(get_connected_node(), &wa);
if (wa.enabled) {
oakengine_viewer_set_playhead(get_connected_node(),
wa.in_num, wa.in_den);
} else {
go_to_start();
}
@@ -905,11 +1016,11 @@ void TimeBasedWidget::go_to_in()
void TimeBasedWidget::go_to_out()
{
if (get_connected_node()) {
if (get_connected_node()->get_work_area()->enabled()) {
oakengine_viewer_set_playhead(
reinterpret_cast<OakEngineNode *>(get_connected_node()),
get_connected_node()->get_work_area()->out().numerator(),
get_connected_node()->get_work_area()->out().denominator());
oakengine_viewer_workarea wa;
oakengine_viewer_get_workarea(get_connected_node(), &wa);
if (wa.enabled) {
oakengine_viewer_set_playhead(get_connected_node(),
wa.out_num, wa.out_den);
} else {
go_to_end();
}
@@ -956,7 +1067,8 @@ bool TimeBasedWidget::snap_point(const std::vector<Rational> &start_times,
std::vector<SnapData> potential_snaps;
if (snap_points & k_snap_to_playhead) {
Rational playhead_abs_time = get_connected_node()->get_playhead();
Rational playhead_abs_time =
viewer_output_playhead(get_connected_node());
qreal playhead_pos = time_to_scene(playhead_abs_time);
attempt_snap(potential_snaps, screen_pt, playhead_pos, start_times,
playhead_abs_time);
@@ -965,31 +1077,43 @@ bool TimeBasedWidget::snap_point(const std::vector<Rational> &start_times,
if ((snap_points & k_snap_to_clips) && get_snap_blocks()) {
for (auto it = get_snap_blocks()->cbegin(); it != get_snap_blocks()->cend();
it++) {
Block *b = *it;
OakEngineBlock *b = *it;
qreal rect_left = time_to_scene(b->in());
qreal rect_right = time_to_scene(b->out());
int in_num = 0, in_den = 1, out_num = 0, out_den = 1;
oakengine_block_get_in_rational(
reinterpret_cast<const OakEngineNode *>(b), &in_num, &in_den);
oakengine_block_get_out_rational(
reinterpret_cast<const OakEngineNode *>(b), &out_num, &out_den);
const Rational block_in(in_num, in_den);
const Rational block_out(out_num, out_den);
qreal rect_left = time_to_scene(block_in);
qreal rect_right = time_to_scene(block_out);
// Attempt snapping to clip in point
attempt_snap(potential_snaps, screen_pt, rect_left, start_times,
b->in());
block_in);
// Attempt snapping to clip out point
attempt_snap(potential_snaps, screen_pt, rect_right, start_times,
b->out());
block_out);
if (snap_points & k_snap_to_markers) {
// Snap to clip markers too
if (ClipBlock *clip = dynamic_cast<ClipBlock *>(b)) {
if (clip->connected_viewer()) {
TimelineMarkerList *markers =
clip->connected_viewer()->get_markers();
for (auto jt = markers->cbegin(); jt != markers->cend();
jt++) {
TimelineMarker *marker = *jt;
if (oakengine_node_is_clip(
reinterpret_cast<OakEngineNode *>(b))) {
if (OakEngineNode *clip_viewer =
oakengine_clip_get_connected_viewer(b)) {
OakEngineMarkerList *markers =
oakengine_viewer_get_marker_list(clip_viewer);
const int marker_count =
oakengine_marker_list_count(markers);
for (int mi = 0; mi < marker_count; mi++) {
OakEngineMarker *marker =
oakengine_marker_list_at(markers, mi);
TimeRange marker_range =
marker->time() + clip->in() - clip_media_in(clip);
marker_time(marker) + block_in - clip_media_in(b);
qreal marker_in_screen =
time_to_scene(marker_range.in());
@@ -1010,9 +1134,10 @@ bool TimeBasedWidget::snap_point(const std::vector<Rational> &start_times,
}
if ((snap_points & k_snap_to_markers) && ruler()->get_markers()) {
for (auto it = ruler()->get_markers()->cbegin();
it != ruler()->get_markers()->cend(); it++) {
TimelineMarker *m = *it;
OakEngineMarkerList *ruler_markers = ruler()->get_markers();
const int marker_count = oakengine_marker_list_count(ruler_markers);
for (int mi = 0; mi < marker_count; mi++) {
OakEngineMarker *m = oakengine_marker_list_at(ruler_markers, mi);
// Ignore selected markers
if (std::find(ruler()->get_selected_markers().cbegin(),
@@ -1021,35 +1146,43 @@ bool TimeBasedWidget::snap_point(const std::vector<Rational> &start_times,
continue;
}
qreal marker_pos = time_to_scene(m->time().in());
attempt_snap(potential_snaps, screen_pt, marker_pos, start_times,
m->time().in());
const TimeRange m_time = marker_time(m);
if (m->time().in() != m->time().out()) {
marker_pos = time_to_scene(m->time().out());
qreal marker_pos = time_to_scene(m_time.in());
attempt_snap(potential_snaps, screen_pt, marker_pos, start_times,
m_time.in());
if (m_time.in() != m_time.out()) {
marker_pos = time_to_scene(m_time.out());
attempt_snap(potential_snaps, screen_pt, marker_pos, start_times,
m->time().out());
m_time.out());
}
}
}
if ((snap_points & k_snap_to_workarea) && ruler()->get_work_area() &&
ruler()->get_work_area()->enabled()) {
const Rational &workarea_in = ruler()->get_work_area()->in();
const Rational &workarea_out = ruler()->get_work_area()->out();
if ((snap_points & k_snap_to_workarea) && ruler()->get_work_area()) {
int64_t wa_in_num = 0, wa_in_den = 1, wa_out_num = 0, wa_out_den = 1;
int wa_enabled = 0;
oakengine_workarea_get(ruler()->get_work_area(), &wa_in_num,
&wa_in_den, &wa_out_num, &wa_out_den,
&wa_enabled);
if (wa_enabled) {
const Rational workarea_in{int(wa_in_num), int(wa_in_den)};
const Rational workarea_out{int(wa_out_num), int(wa_out_den)};
attempt_snap(potential_snaps, screen_pt, time_to_scene(workarea_in),
start_times, workarea_in);
attempt_snap(potential_snaps, screen_pt, time_to_scene(workarea_out),
start_times, workarea_out);
attempt_snap(potential_snaps, screen_pt, time_to_scene(workarea_in),
start_times, workarea_in);
attempt_snap(potential_snaps, screen_pt, time_to_scene(workarea_out),
start_times, workarea_out);
}
}
if ((snap_points & k_snap_to_keyframes) && get_snap_keyframes()) {
for (auto it = get_snap_keyframes()->cbegin();
it != get_snap_keyframes()->cend(); it++) {
const QVector<NodeKeyframe *> &keys = (*it)->get_keyframes();
const QVector<oak::Keyframe> &keys = (*it)->get_keyframes();
for (auto jt = keys.cbegin(); jt != keys.cend(); jt++) {
NodeKeyframe *key = *jt;
OakEngineKeyframe *key = jt->handle();
auto ignore = get_snap_ignore_keyframes();
if (ignore && std::find(ignore->cbegin(), ignore->cend(),
@@ -1057,12 +1190,12 @@ bool TimeBasedWidget::snap_point(const std::vector<Rational> &start_times,
continue;
}
Rational time = key->time();
Rational time = key_time(key);
if (const TimeTargetObject *target = get_keyframe_time_target()) {
if (Node *parent = key->parent()) {
if (OakEngineNode *parent = key_node(key)) {
time = target->get_adjusted_time(
parent, target->get_time_target(), time,
Node::k_transform_towards_output);
k_transform_towards_output);
}
}