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
+7 -98
View File
@@ -26,7 +26,7 @@
#include "widget/menu/menushared.h"
#include "window/mainwindow/mainstatusbar.h"
#include "window/mainwindow/mainwindow.h"
#include "node/project/serializer/serializedlayoutinfo.h"
#include "common/serializedlayoutinfoapp.h"
using namespace olive;
@@ -67,16 +67,16 @@ TEST(SerializedLayoutInfo, AccessorsStoreAndRetrieve)
EXPECT_TRUE(info.panel_data.empty());
EXPECT_TRUE(info.state.isEmpty());
info.open_folders.push_back(folder);
info.open_sequences.push_back(sequence);
info.open_viewers.push_back(viewer);
info.open_folders.push_back(reinterpret_cast<OakEngineNode *>(folder));
info.open_sequences.push_back(reinterpret_cast<OakEngineNode *>(sequence));
info.open_viewers.push_back(reinterpret_cast<OakEngineNode *>(viewer));
ASSERT_EQ(info.open_folders.size(), 1);
EXPECT_EQ(info.open_folders.front(), folder);
EXPECT_EQ(info.open_folders.front(), reinterpret_cast<OakEngineNode *>(folder));
ASSERT_EQ(info.open_sequences.size(), 1);
EXPECT_EQ(info.open_sequences.front(), sequence);
EXPECT_EQ(info.open_sequences.front(), reinterpret_cast<OakEngineNode *>(sequence));
ASSERT_EQ(info.open_viewers.size(), 1);
EXPECT_EQ(info.open_viewers.front(), viewer);
EXPECT_EQ(info.open_viewers.front(), reinterpret_cast<OakEngineNode *>(viewer));
PanelWidget::Info data;
data[QStringLiteral("key")] = QStringLiteral("value");
@@ -102,97 +102,6 @@ TEST(SerializedLayoutInfo, AccessorsStoreAndRetrieve)
EXPECT_EQ(info.state, QByteArray("layout-state"));
}
TEST(SerializedLayoutInfo, XmlRoundTripPreservesEverything)
{
Project project;
project.initialize();
auto *folder = new Folder();
folder->setParent(&project);
auto *sequence = new Sequence();
sequence->setParent(&project);
auto *viewer = new ViewerOutput();
viewer->setParent(&project);
SerializedLayoutInfo info;
info.open_folders.push_back(folder);
info.open_sequences.push_back(sequence);
info.open_viewers.push_back(viewer);
PanelWidget::Info data;
data[QStringLiteral("splitter")] = QStringLiteral("AAA=");
info.panel_data[QStringLiteral("TimelinePanel")] = data;
info.state = QByteArray("binary\x01\x02state", 12);
QString xml;
QXmlStreamWriter writer(&xml);
writer.writeStartDocument();
writer.writeStartElement(QStringLiteral("layout"));
info.to_xml(&writer);
writer.writeEndElement();
writer.writeEndDocument();
QHash<quintptr, Node *> node_map;
node_map.insert(reinterpret_cast<quintptr>(folder), folder);
node_map.insert(reinterpret_cast<quintptr>(sequence), sequence);
node_map.insert(reinterpret_cast<quintptr>(viewer), viewer);
QXmlStreamReader reader(xml);
ASSERT_TRUE(reader.readNextStartElement());
ASSERT_EQ(reader.name(), QStringLiteral("layout"));
SerializedLayoutInfo loaded = SerializedLayoutInfo::from_xml(&reader, node_map);
ASSERT_EQ(loaded.open_folders.size(), 1);
EXPECT_EQ(loaded.open_folders.front(), folder);
ASSERT_EQ(loaded.open_sequences.size(), 1);
EXPECT_EQ(loaded.open_sequences.front(), sequence);
// Open viewers must survive the round trip too
ASSERT_EQ(loaded.open_viewers.size(), 1);
EXPECT_EQ(loaded.open_viewers.front(), viewer);
EXPECT_EQ(loaded.state, info.state);
ASSERT_EQ(loaded.panel_data.count(QStringLiteral("TimelinePanel")), 1);
EXPECT_EQ(loaded.panel_data
.at(QStringLiteral("TimelinePanel"))
.at(QStringLiteral("splitter")),
QStringLiteral("AAA="));
// No unknown nodes leak into the viewers list: it must contain exactly the
// viewer that was added, not a duplicate of the sequences list
EXPECT_NE(loaded.open_viewers.front(),
static_cast<ViewerOutput *>(sequence));
}
TEST(SerializedLayoutInfo, FromXmlSkipsUnknownElementsAndNodes)
{
const QString xml = QStringLiteral(
"<layout version=\"1\">"
"<folders><folder>1</folder><unknown><nested/></unknown></folders>"
"<timeline><sequence>2</sequence></timeline>"
"<viewers><viewer>3</viewer></viewers>"
"<mystery><deep><deeper/></deep></mystery>"
"<state>QUJD</state>"
"</layout>");
// No node map entries: pointers resolve to nullptr
QXmlStreamReader reader(xml);
ASSERT_TRUE(reader.readNextStartElement());
ASSERT_EQ(reader.name(), QStringLiteral("layout"));
SerializedLayoutInfo info = SerializedLayoutInfo::from_xml(&reader, {});
// Unknown pointers resolve to null but are still listed
ASSERT_EQ(info.open_folders.size(), 1);
EXPECT_EQ(info.open_folders.front(), nullptr);
ASSERT_EQ(info.open_sequences.size(), 1);
EXPECT_EQ(info.open_sequences.front(), nullptr);
ASSERT_EQ(info.open_viewers.size(), 1);
EXPECT_EQ(info.open_viewers.front(), nullptr);
EXPECT_EQ(info.state, QByteArray("ABC"));
EXPECT_TRUE(info.panel_data.empty());
}
TEST(MainWindowStatusBar, ConstructionDefaults)
{
MainStatusBar bar;