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
+39 -25
View File
@@ -14,7 +14,10 @@
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include "common/keyframetypes.h"
#include "core.h"
#include "oakengine/app.h"
#include "undo/undostack.h"
#include "dialog/footageproperties/footageproperties.h"
#include "dialog/footagerelink/footagerelinkdialog.h"
#include "dialog/keyframeproperties/keyframeproperties.h"
@@ -55,8 +58,10 @@ void ensure_app_singletons()
void clear_undo_stack()
{
if (olive::Core::instance()) {
olive::Core::instance()->undo_stack()->clear();
// The process-wide undo stack previously reached via Core::undo_stack()
if (auto *stack =
static_cast<olive::UndoStack *>(oakengine_app_undo_stack())) {
stack->clear();
}
}
@@ -114,7 +119,7 @@ TEST(DialogSpeedDuration, InitialValuesReflectSingleClip)
auto *clip = create_clip(project.get(), olive::Rational(4));
create_track_with_clip(project.get(), clip);
olive::SpeedDurationDialog dialog({ clip }, olive::Rational(1, 24));
olive::SpeedDurationDialog dialog({ reinterpret_cast<OakEngineBlock *>(clip) }, olive::Rational(1, 24));
auto *speed_slider = dialog.findChild<olive::FloatSlider *>();
auto *dur_slider = dialog.findChild<olive::RationalSlider *>();
@@ -134,7 +139,7 @@ TEST(DialogSpeedDuration, LinkedSpeedChangeUpdatesDuration)
auto *clip = create_clip(project.get(), olive::Rational(4));
create_track_with_clip(project.get(), clip);
olive::SpeedDurationDialog dialog({ clip }, olive::Rational(1, 24));
olive::SpeedDurationDialog dialog({ reinterpret_cast<OakEngineBlock *>(clip) }, olive::Rational(1, 24));
auto *speed_slider = dialog.findChild<olive::FloatSlider *>();
auto *dur_slider = dialog.findChild<olive::RationalSlider *>();
@@ -157,7 +162,7 @@ TEST(DialogSpeedDuration, LinkedDurationChangeUpdatesSpeed)
auto *clip = create_clip(project.get(), olive::Rational(4));
create_track_with_clip(project.get(), clip);
olive::SpeedDurationDialog dialog({ clip }, olive::Rational(1, 24));
olive::SpeedDurationDialog dialog({ reinterpret_cast<OakEngineBlock *>(clip) }, olive::Rational(1, 24));
auto *speed_slider = dialog.findChild<olive::FloatSlider *>();
auto *dur_slider = dialog.findChild<olive::RationalSlider *>();
@@ -181,7 +186,7 @@ TEST(DialogSpeedDuration, AcceptAppliesSpeedAndLength)
create_track_with_clip(project.get(), clip);
{
olive::SpeedDurationDialog dialog({ clip }, olive::Rational(1, 24));
olive::SpeedDurationDialog dialog({ reinterpret_cast<OakEngineBlock *>(clip) }, olive::Rational(1, 24));
// Doubling the speed with the link checked halves the duration
auto *speed_slider = dialog.findChild<olive::FloatSlider *>();
@@ -207,7 +212,7 @@ TEST(DialogSpeedDuration, DifferingSpeedsAcrossClipsProduceTristate)
clip_b->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
create_track_with_clip(project.get(), clip_b);
olive::SpeedDurationDialog dialog({ clip_a, clip_b },
olive::SpeedDurationDialog dialog({ reinterpret_cast<OakEngineBlock *>(clip_a), reinterpret_cast<OakEngineBlock *>(clip_b) },
olive::Rational(1, 24));
EXPECT_TRUE(dialog.findChild<olive::FloatSlider *>()->is_tristate());
@@ -226,7 +231,7 @@ TEST(DialogSpeedDuration, AcceptDerivesPerClipSpeedFromDuration)
create_track_with_clip(project.get(), clip_b);
{
olive::SpeedDurationDialog dialog({ clip_a, clip_b },
olive::SpeedDurationDialog dialog({ reinterpret_cast<OakEngineBlock *>(clip_a), reinterpret_cast<OakEngineBlock *>(clip_b) },
olive::Rational(1, 24));
// Speed is tristate, so accept() must compute each clip's speed
// from its own length/speed ratio: speed = old_speed * old_len / new_len
@@ -269,7 +274,8 @@ TEST(DialogKeyframeProperties, SingleKeyAcceptWritesAllFields)
key->setParent(node);
// The dialog stores the keyframe vector by reference, so it must outlive it
std::vector<olive::NodeKeyframe *> keys = { key };
QVector<oak::Keyframe> keys = { oak::Keyframe(
reinterpret_cast<OakEngineKeyframe *>(key)) };
{
olive::KeyframePropertiesDialog dialog(keys, olive::Rational(1, 24));
@@ -288,10 +294,12 @@ TEST(DialogKeyframeProperties, SingleKeyAcceptWritesAllFields)
EXPECT_EQ(type_select->currentData().toInt(), olive::NodeKeyframe::k_linear);
EXPECT_FALSE(bezier_group->isEnabled());
// Switching to Bezier enables the bezier handle editors
// Switching to Bezier enables the bezier handle editors. Item data
// carries facade easing ordinals (linear=0, bezier=1, hold=2), not
// the engine NodeKeyframe::Type values
type_select->setCurrentIndex(2);
ASSERT_EQ(type_select->currentData().toInt(),
olive::NodeKeyframe::k_bezier);
olive::KeyframeTypes::k_facade_bezier);
EXPECT_TRUE(bezier_group->isEnabled());
time_slider->set_value(olive::Rational(1, 2));
@@ -334,7 +342,10 @@ TEST(DialogKeyframeProperties, MixedTypesAddPlaceholderItem)
key_b->setParent(node);
// The dialog stores the keyframe vector by reference, so it must outlive it
std::vector<olive::NodeKeyframe *> keys = { key_a, key_b };
QVector<oak::Keyframe> keys = {
oak::Keyframe(reinterpret_cast<OakEngineKeyframe *>(key_a)),
oak::Keyframe(reinterpret_cast<OakEngineKeyframe *>(key_b)),
};
{
olive::KeyframePropertiesDialog dialog(keys, olive::Rational(1, 24));
@@ -372,7 +383,10 @@ TEST(DialogKeyframeProperties, KeysOnSameTrackDisableTimeEdit)
key_b->setParent(node);
// The dialog stores the keyframe vector by reference, so it must outlive it
std::vector<olive::NodeKeyframe *> keys = { key_a, key_b };
QVector<oak::Keyframe> keys = {
oak::Keyframe(reinterpret_cast<OakEngineKeyframe *>(key_a)),
oak::Keyframe(reinterpret_cast<OakEngineKeyframe *>(key_b)),
};
olive::KeyframePropertiesDialog dialog(keys, olive::Rational(1, 24));
@@ -394,7 +408,7 @@ TEST(DialogMarkerProperties, SingleMarkerAcceptWritesFields)
QStringLiteral("Marker A"));
{
olive::MarkerPropertiesDialog dialog({ &marker },
olive::MarkerPropertiesDialog dialog({ reinterpret_cast<OakEngineMarker *>(&marker) },
olive::Rational(1, 24));
auto *label_edit = dialog.findChild<QLineEdit *>();
@@ -439,7 +453,7 @@ TEST(DialogMarkerProperties, MultipleMarkersDisableTimeAndShowPlaceholder)
QStringLiteral("Beta"));
{
olive::MarkerPropertiesDialog dialog({ &marker_a, &marker_b },
olive::MarkerPropertiesDialog dialog({ reinterpret_cast<OakEngineMarker *>(&marker_a), reinterpret_cast<OakEngineMarker *>(&marker_b) },
olive::Rational(1, 24));
auto *label_edit = dialog.findChild<QLineEdit *>();
@@ -561,7 +575,7 @@ TEST(DialogSequenceParameterTab, ReflectsSequenceParameters)
48000, olive::core::k_channel_layout_stereo,
olive::Sequence::k_default_sample_format));
olive::SequenceDialogParameterTab tab(sequence);
olive::SequenceDialogParameterTab tab(reinterpret_cast<OakEngineNode *>(sequence));
EXPECT_EQ(tab.get_selected_video_width(), 1920);
EXPECT_EQ(tab.get_selected_video_height(), 1080);
@@ -590,7 +604,7 @@ TEST(DialogSequenceParameterTab, PresetChangedAppliesValues)
48000, olive::core::k_channel_layout_stereo,
olive::Sequence::k_default_sample_format));
olive::SequenceDialogParameterTab tab(sequence);
olive::SequenceDialogParameterTab tab(reinterpret_cast<OakEngineNode *>(sequence));
tab.preset_changed(olive::SequencePreset(
QStringLiteral("Preset"), 1280, 720, olive::Rational(24, 1),
@@ -625,7 +639,7 @@ TEST(DialogSequenceDialog, AcceptNonUndoableAppliesParameters)
olive::Sequence::k_default_sample_format));
{
olive::SequenceDialog dialog(sequence, olive::SequenceDialog::k_existing);
olive::SequenceDialog dialog(reinterpret_cast<OakEngineNode *>(sequence), olive::SequenceDialog::k_existing);
dialog.set_undoable(false);
auto *tab = dialog.findChild<olive::SequenceDialogParameterTab *>();
@@ -672,7 +686,7 @@ TEST(DialogSequenceDialog, AcceptUndoablePushesCommand)
olive::Sequence::k_default_sample_format));
{
olive::SequenceDialog dialog(sequence, olive::SequenceDialog::k_existing);
olive::SequenceDialog dialog(reinterpret_cast<OakEngineNode *>(sequence), olive::SequenceDialog::k_existing);
auto *tab = dialog.findChild<olive::SequenceDialogParameterTab *>();
ASSERT_NE(tab, nullptr);
@@ -701,7 +715,7 @@ TEST(DialogSequenceDialog, PresetTabListsDefaultPresets)
auto *sequence = new olive::Sequence();
sequence->setParent(project.get());
olive::SequenceDialog dialog(sequence, olive::SequenceDialog::k_existing);
olive::SequenceDialog dialog(reinterpret_cast<OakEngineNode *>(sequence), olive::SequenceDialog::k_existing);
auto *tree = dialog.findChild<QTreeWidget *>();
ASSERT_NE(tree, nullptr);
@@ -723,7 +737,7 @@ TEST(DialogFootageProperties, AcceptRenamesAndSetsSourceStartTime)
footage->set_filename(QStringLiteral("/tmp/oak-nonexistent.mp4"));
{
olive::FootagePropertiesDialog dialog(nullptr, footage);
olive::FootagePropertiesDialog dialog(nullptr, reinterpret_cast<OakEngineNode *>(footage));
auto *name_field = dialog.findChild<QLineEdit *>();
auto *start_enable = dialog.findChild<QCheckBox *>();
@@ -752,7 +766,7 @@ TEST(DialogFootageProperties, AcceptRenamesAndSetsSourceStartTime)
{
// Unchecking the box must clear the source start time again
olive::FootagePropertiesDialog dialog(nullptr, footage);
olive::FootagePropertiesDialog dialog(nullptr, reinterpret_cast<OakEngineNode *>(footage));
auto *start_enable = dialog.findChild<QCheckBox *>();
ASSERT_NE(start_enable, nullptr);
@@ -785,7 +799,7 @@ TEST(DialogFootageRelink, TableListsFootageAndFilenames)
footage_b->set_label(QStringLiteral("Footage B"));
footage_b->set_filename(QStringLiteral("/old/path/b.mp4"));
olive::FootageRelinkDialog dialog({ footage_a, footage_b });
olive::FootageRelinkDialog dialog({ reinterpret_cast<OakEngineNode *>(footage_a), reinterpret_cast<OakEngineNode *>(footage_b) });
auto *table = dialog.findChild<QTreeWidget *>();
ASSERT_NE(table, nullptr);
@@ -806,7 +820,7 @@ TEST(DialogProjectProperties, OcioValidationTogglesOnInvalidFilename)
ensure_app_singletons();
auto project = create_project();
olive::ProjectPropertiesDialog dialog(project.get(), nullptr);
olive::ProjectPropertiesDialog dialog(reinterpret_cast<OakEngineProject *>(project.get()), nullptr);
EXPECT_TRUE(dialog.windowTitle().contains(project->name()));
@@ -843,7 +857,7 @@ TEST(DialogProjectProperties, AcceptWithDefaultsClosesDialog)
ensure_app_singletons();
auto project = create_project();
olive::ProjectPropertiesDialog dialog(project.get(), nullptr);
olive::ProjectPropertiesDialog dialog(reinterpret_cast<OakEngineProject *>(project.get()), nullptr);
dialog.accept();
EXPECT_EQ(dialog.result(), QDialog::Accepted);