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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user