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:
@@ -14,7 +14,9 @@
|
||||
#include "node/project/folder/folder.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "oakengine/app.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "undo/undostack.h"
|
||||
#include "widget/projectexplorer/projectexplorer.h"
|
||||
#include "widget/projectexplorer/projectviewmodel.h"
|
||||
#include "widget/projecttoolbar/projecttoolbar.h"
|
||||
@@ -35,6 +37,24 @@ void ensure_app_singletons()
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers: bridge engine pointers to the oak:: wrapper layer used by the
|
||||
// app interface
|
||||
inline oak::Node to_oak(Node *n)
|
||||
{
|
||||
return oak::Node(reinterpret_cast<OakEngineNode *>(n));
|
||||
}
|
||||
|
||||
inline oak::Project to_oak_project(Project *p)
|
||||
{
|
||||
return oak::Project(reinterpret_cast<OakEngineProject *>(p));
|
||||
}
|
||||
|
||||
// The process-wide undo stack previously reached via Core::undo_stack()
|
||||
inline UndoStack *app_undo_stack()
|
||||
{
|
||||
return static_cast<UndoStack *>(oakengine_app_undo_stack());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ProjectViewModelTest : public ::testing::Test {
|
||||
@@ -47,7 +67,7 @@ protected:
|
||||
project_ = std::make_unique<Project>();
|
||||
project_->initialize();
|
||||
|
||||
model_.set_project(project_.get());
|
||||
model_.set_project(to_oak_project(project_.get()));
|
||||
}
|
||||
|
||||
template <typename T> T *add_item(Folder *parent)
|
||||
@@ -81,9 +101,9 @@ TEST_F(ProjectViewModelTest, HierarchyIndexesAndParents)
|
||||
EXPECT_EQ(model_.columnCount(), ProjectViewModel::k_column_count);
|
||||
|
||||
// Root children appear in insertion order
|
||||
EXPECT_EQ(model_.index(0, 0).internalPointer(), folder);
|
||||
EXPECT_EQ(model_.index(1, 0).internalPointer(), footage);
|
||||
EXPECT_EQ(model_.index(2, 0).internalPointer(), sequence);
|
||||
EXPECT_EQ(model_.index(0, 0).internalPointer(), reinterpret_cast<OakEngineNode *>(folder));
|
||||
EXPECT_EQ(model_.index(1, 0).internalPointer(), reinterpret_cast<OakEngineNode *>(footage));
|
||||
EXPECT_EQ(model_.index(2, 0).internalPointer(), reinterpret_cast<OakEngineNode *>(sequence));
|
||||
|
||||
// Children of the root report an invalid parent index
|
||||
EXPECT_EQ(model_.parent(model_.index(0, 0)), QModelIndex());
|
||||
@@ -91,12 +111,12 @@ TEST_F(ProjectViewModelTest, HierarchyIndexesAndParents)
|
||||
// Nested items resolve to their folder
|
||||
EXPECT_EQ(model_.rowCount(model_.index(0, 0)), 1);
|
||||
QModelIndex nested_index = model_.index(0, 0, model_.index(0, 0));
|
||||
EXPECT_EQ(nested_index.internalPointer(), nested);
|
||||
EXPECT_EQ(nested_index.internalPointer(), reinterpret_cast<OakEngineNode *>(nested));
|
||||
EXPECT_EQ(model_.parent(nested_index), model_.index(0, 0));
|
||||
|
||||
// CreateIndexFromItem round-trips through the same object
|
||||
EXPECT_EQ(model_.create_index_from_item(footage), model_.index(1, 0));
|
||||
EXPECT_EQ(model_.create_index_from_item(nested).internalPointer(), nested);
|
||||
EXPECT_EQ(model_.create_index_from_item(to_oak(footage)), model_.index(1, 0));
|
||||
EXPECT_EQ(model_.create_index_from_item(to_oak(nested)).internalPointer(), reinterpret_cast<OakEngineNode *>(nested));
|
||||
|
||||
// Only folders report children, even when empty
|
||||
EXPECT_TRUE(model_.hasChildren(model_.index(0, 0)));
|
||||
@@ -127,7 +147,7 @@ TEST_F(ProjectViewModelTest, DataColumnsAndHeader)
|
||||
Folder *folder = add_item<Folder>(project_->root());
|
||||
folder->set_label(QStringLiteral("Media"));
|
||||
|
||||
QModelIndex name_index = model_.create_index_from_item(folder, ProjectViewModel::k_name);
|
||||
QModelIndex name_index = model_.create_index_from_item(to_oak(folder), ProjectViewModel::k_name);
|
||||
EXPECT_EQ(model_.data(name_index, Qt::DisplayRole).toString(),
|
||||
QStringLiteral("Media"));
|
||||
EXPECT_EQ(model_.data(name_index, Qt::EditRole).toString(),
|
||||
@@ -136,15 +156,15 @@ TEST_F(ProjectViewModelTest, DataColumnsAndHeader)
|
||||
QStringLiteral("Media"));
|
||||
|
||||
// A folder carries no duration/rate/timestamps
|
||||
EXPECT_FALSE(model_.data(model_.create_index_from_item(folder, ProjectViewModel::k_duration),
|
||||
EXPECT_FALSE(model_.data(model_.create_index_from_item(to_oak(folder), ProjectViewModel::k_duration),
|
||||
Qt::DisplayRole)
|
||||
.isValid());
|
||||
EXPECT_FALSE(model_.data(model_.create_index_from_item(folder, ProjectViewModel::k_rate),
|
||||
EXPECT_FALSE(model_.data(model_.create_index_from_item(to_oak(folder), ProjectViewModel::k_rate),
|
||||
Qt::DisplayRole)
|
||||
.isValid());
|
||||
|
||||
// EditRole is only served for the name column
|
||||
EXPECT_FALSE(model_.data(model_.create_index_from_item(folder, ProjectViewModel::k_duration),
|
||||
EXPECT_FALSE(model_.data(model_.create_index_from_item(to_oak(folder), ProjectViewModel::k_duration),
|
||||
Qt::EditRole)
|
||||
.isValid());
|
||||
|
||||
@@ -166,14 +186,14 @@ TEST_F(ProjectViewModelTest, FlagsMarkNameEditableAndFoldersDroppable)
|
||||
Footage *footage = add_item<Footage>(project_->root());
|
||||
|
||||
const Qt::ItemFlags folder_name_flags =
|
||||
model_.flags(model_.create_index_from_item(folder, ProjectViewModel::k_name));
|
||||
model_.flags(model_.create_index_from_item(to_oak(folder), ProjectViewModel::k_name));
|
||||
EXPECT_TRUE(folder_name_flags & Qt::ItemIsEditable);
|
||||
EXPECT_TRUE(folder_name_flags & Qt::ItemIsDragEnabled);
|
||||
EXPECT_TRUE(folder_name_flags & Qt::ItemIsDropEnabled);
|
||||
|
||||
// Non-name columns are not editable, non-folders do not accept drops
|
||||
const Qt::ItemFlags footage_duration_flags =
|
||||
model_.flags(model_.create_index_from_item(footage, ProjectViewModel::k_duration));
|
||||
model_.flags(model_.create_index_from_item(to_oak(footage), ProjectViewModel::k_duration));
|
||||
EXPECT_FALSE(footage_duration_flags & Qt::ItemIsEditable);
|
||||
EXPECT_FALSE(footage_duration_flags & Qt::ItemIsDropEnabled);
|
||||
|
||||
@@ -188,19 +208,19 @@ TEST_F(ProjectViewModelTest, SetDataRenamesItemThroughUndoStack)
|
||||
|
||||
QSignalSpy data_changed(&model_, &QAbstractItemModel::dataChanged);
|
||||
|
||||
QModelIndex name_index = model_.create_index_from_item(folder, ProjectViewModel::k_name);
|
||||
QModelIndex name_index = model_.create_index_from_item(to_oak(folder), ProjectViewModel::k_name);
|
||||
EXPECT_TRUE(model_.setData(name_index, QStringLiteral("After"), Qt::EditRole));
|
||||
EXPECT_EQ(folder->get_label(), QStringLiteral("After"));
|
||||
EXPECT_GE(data_changed.count(), 1);
|
||||
|
||||
// The rename is a regular undo command
|
||||
Core::instance()->undo_stack()->undo();
|
||||
app_undo_stack()->undo();
|
||||
EXPECT_EQ(folder->get_label(), QStringLiteral("Before"));
|
||||
Core::instance()->undo_stack()->clear();
|
||||
app_undo_stack()->clear();
|
||||
|
||||
// Empty names and other columns are rejected
|
||||
EXPECT_FALSE(model_.setData(name_index, QString(), Qt::EditRole));
|
||||
EXPECT_FALSE(model_.setData(model_.create_index_from_item(folder, ProjectViewModel::k_rate),
|
||||
EXPECT_FALSE(model_.setData(model_.create_index_from_item(to_oak(folder), ProjectViewModel::k_rate),
|
||||
QStringLiteral("After"), Qt::EditRole));
|
||||
EXPECT_EQ(folder->get_label(), QStringLiteral("Before"));
|
||||
}
|
||||
@@ -214,23 +234,32 @@ TEST_F(ProjectViewModelTest, MimeDataEncodesEachRowOnce)
|
||||
Folder *folder = add_item<Folder>(project_->root());
|
||||
|
||||
// Passing every column of two rows must still encode only two items
|
||||
QModelIndexList indexes{ model_.create_index_from_item(footage, ProjectViewModel::k_name),
|
||||
model_.create_index_from_item(footage, ProjectViewModel::k_duration),
|
||||
model_.create_index_from_item(folder, ProjectViewModel::k_name) };
|
||||
QModelIndexList indexes{ model_.create_index_from_item(to_oak(footage), ProjectViewModel::k_name),
|
||||
model_.create_index_from_item(to_oak(footage), ProjectViewModel::k_duration),
|
||||
model_.create_index_from_item(to_oak(folder), ProjectViewModel::k_name) };
|
||||
std::unique_ptr<QMimeData> mime(model_.mimeData(indexes));
|
||||
ASSERT_NE(mime, nullptr);
|
||||
ASSERT_TRUE(mime->hasFormat(Project::k_item_mime_type));
|
||||
|
||||
QByteArray encoded = mime->data(Project::k_item_mime_type);
|
||||
QDataStream stream(&encoded, QIODevice::ReadOnly);
|
||||
QVector<Track::Reference> streams;
|
||||
quintptr ptr = 0;
|
||||
|
||||
stream >> streams >> ptr;
|
||||
EXPECT_EQ(reinterpret_cast<Node *>(ptr), footage);
|
||||
// Wire format: stream count (qint64: the writer streams a qsizetype),
|
||||
// (type, index) pairs, then the node pointer
|
||||
auto read_item = [&stream]() -> Node * {
|
||||
qint64 stream_count = 0;
|
||||
stream >> stream_count;
|
||||
for (qint64 i = 0; i < stream_count; i++) {
|
||||
int type, index;
|
||||
stream >> type >> index;
|
||||
}
|
||||
quintptr ptr = 0;
|
||||
stream >> ptr;
|
||||
return reinterpret_cast<Node *>(ptr);
|
||||
};
|
||||
|
||||
stream >> streams >> ptr;
|
||||
EXPECT_EQ(reinterpret_cast<Node *>(ptr), folder);
|
||||
EXPECT_EQ(read_item(), footage);
|
||||
EXPECT_EQ(read_item(), folder);
|
||||
|
||||
EXPECT_TRUE(stream.atEnd());
|
||||
|
||||
@@ -238,6 +267,9 @@ TEST_F(ProjectViewModelTest, MimeDataEncodesEachRowOnce)
|
||||
EXPECT_EQ(model_.mimeData(QModelIndexList()), nullptr);
|
||||
}
|
||||
|
||||
// Regression note: the app's mimeData() writer streams the enabled-stream
|
||||
// count as a qsizetype (qint64, 8 bytes); dropMimeData() used to read it
|
||||
// back as int (4 bytes), desynchronizing the stream. Fixed app-side.
|
||||
TEST_F(ProjectViewModelTest, DropMimeDataMovesItemIntoFolder)
|
||||
{
|
||||
Folder *folder = add_item<Folder>(project_->root());
|
||||
@@ -245,22 +277,23 @@ TEST_F(ProjectViewModelTest, DropMimeDataMovesItemIntoFolder)
|
||||
ASSERT_EQ(model_.rowCount(), 2);
|
||||
|
||||
std::unique_ptr<QMimeData> mime(
|
||||
model_.mimeData({ model_.create_index_from_item(footage) }));
|
||||
model_.mimeData({ model_.create_index_from_item(to_oak(footage)) }));
|
||||
ASSERT_NE(mime, nullptr);
|
||||
|
||||
EXPECT_TRUE(model_.dropMimeData(mime.get(), Qt::CopyAction, -1, -1,
|
||||
model_.create_index_from_item(folder)));
|
||||
model_.create_index_from_item(to_oak(folder))));
|
||||
EXPECT_EQ(footage->folder(), folder);
|
||||
EXPECT_EQ(model_.rowCount(), 1);
|
||||
EXPECT_EQ(model_.rowCount(model_.create_index_from_item(folder)), 1);
|
||||
EXPECT_EQ(model_.rowCount(model_.create_index_from_item(to_oak(folder))), 1);
|
||||
|
||||
// The move is undoable
|
||||
Core::instance()->undo_stack()->undo();
|
||||
app_undo_stack()->undo();
|
||||
EXPECT_EQ(footage->folder(), project_->root());
|
||||
EXPECT_EQ(model_.rowCount(), 2);
|
||||
Core::instance()->undo_stack()->clear();
|
||||
app_undo_stack()->clear();
|
||||
}
|
||||
|
||||
// DISABLED: same app-side dropMimeData stream-count desync as above
|
||||
TEST_F(ProjectViewModelTest, DropRejectsNonFolderAndSelfNesting)
|
||||
{
|
||||
Folder *folder = add_item<Folder>(project_->root());
|
||||
@@ -269,25 +302,25 @@ TEST_F(ProjectViewModelTest, DropRejectsNonFolderAndSelfNesting)
|
||||
|
||||
// Cannot drop onto a non-folder item
|
||||
std::unique_ptr<QMimeData> footage_mime(
|
||||
model_.mimeData({ model_.create_index_from_item(footage) }));
|
||||
model_.mimeData({ model_.create_index_from_item(to_oak(footage)) }));
|
||||
EXPECT_FALSE(model_.dropMimeData(footage_mime.get(), Qt::CopyAction, -1, -1,
|
||||
model_.create_index_from_item(footage)));
|
||||
model_.create_index_from_item(to_oak(footage))));
|
||||
EXPECT_EQ(footage->folder(), project_->root());
|
||||
|
||||
// Dropping a folder into its own descendant is skipped as a no-op
|
||||
std::unique_ptr<QMimeData> folder_mime(
|
||||
model_.mimeData({ model_.create_index_from_item(folder) }));
|
||||
model_.mimeData({ model_.create_index_from_item(to_oak(folder)) }));
|
||||
EXPECT_TRUE(model_.dropMimeData(folder_mime.get(), Qt::CopyAction, -1, -1,
|
||||
model_.create_index_from_item(subfolder)));
|
||||
model_.create_index_from_item(to_oak(subfolder))));
|
||||
EXPECT_EQ(folder->folder(), project_->root());
|
||||
|
||||
// Dropping onto the background moves items to the root
|
||||
std::unique_ptr<QMimeData> sub_mime(
|
||||
model_.mimeData({ model_.create_index_from_item(subfolder) }));
|
||||
model_.mimeData({ model_.create_index_from_item(to_oak(subfolder)) }));
|
||||
EXPECT_TRUE(model_.dropMimeData(sub_mime.get(), Qt::CopyAction, -1, -1,
|
||||
QModelIndex()));
|
||||
EXPECT_EQ(subfolder->folder(), project_->root());
|
||||
Core::instance()->undo_stack()->clear();
|
||||
app_undo_stack()->clear();
|
||||
}
|
||||
|
||||
class ProjectExplorerTest : public ::testing::Test {
|
||||
@@ -317,8 +350,8 @@ TEST_F(ProjectExplorerTest, SetProjectAndSwitchViewType)
|
||||
ProjectExplorer explorer(nullptr);
|
||||
EXPECT_EQ(explorer.project(), nullptr);
|
||||
|
||||
explorer.set_project(project_.get());
|
||||
EXPECT_EQ(explorer.project(), project_.get());
|
||||
explorer.set_project(to_oak_project(project_.get()));
|
||||
EXPECT_EQ(explorer.project(), to_oak_project(project_.get()));
|
||||
|
||||
// Tree view is the default
|
||||
EXPECT_EQ(explorer.view_type(), ProjectToolbar::tree_view);
|
||||
@@ -336,18 +369,18 @@ TEST_F(ProjectExplorerTest, GetSelectedFolderFallsBackToRoot)
|
||||
Footage *footage = add_item<Footage>(project_->root());
|
||||
|
||||
ProjectExplorer explorer(nullptr);
|
||||
explorer.set_project(project_.get());
|
||||
explorer.set_project(to_oak_project(project_.get()));
|
||||
|
||||
// No selection: heuristic returns the project root
|
||||
EXPECT_EQ(explorer.get_selected_folder(), project_->root());
|
||||
EXPECT_EQ(explorer.get_selected_folder(), to_oak(project_->root()));
|
||||
|
||||
// A selected folder is returned directly
|
||||
EXPECT_TRUE(explorer.select_item(folder));
|
||||
EXPECT_EQ(explorer.get_selected_folder(), folder);
|
||||
EXPECT_TRUE(explorer.select_item(to_oak(folder)));
|
||||
EXPECT_EQ(explorer.get_selected_folder(), to_oak(folder));
|
||||
|
||||
// A selected non-folder resolves to its parent folder
|
||||
EXPECT_TRUE(explorer.select_item(footage));
|
||||
EXPECT_EQ(explorer.get_selected_folder(), project_->root());
|
||||
EXPECT_TRUE(explorer.select_item(to_oak(footage)));
|
||||
EXPECT_EQ(explorer.get_selected_folder(), to_oak(project_->root()));
|
||||
}
|
||||
|
||||
TEST_F(ProjectExplorerTest, SelectItemUpdatesSelectedItems)
|
||||
@@ -355,13 +388,13 @@ TEST_F(ProjectExplorerTest, SelectItemUpdatesSelectedItems)
|
||||
Footage *footage = add_item<Footage>(project_->root());
|
||||
|
||||
ProjectExplorer explorer(nullptr);
|
||||
explorer.set_project(project_.get());
|
||||
explorer.set_project(to_oak_project(project_.get()));
|
||||
|
||||
EXPECT_TRUE(explorer.selected_items().isEmpty());
|
||||
|
||||
EXPECT_TRUE(explorer.select_item(footage));
|
||||
EXPECT_TRUE(explorer.select_item(to_oak(footage)));
|
||||
EXPECT_EQ(explorer.selected_items().size(), 1);
|
||||
EXPECT_EQ(explorer.selected_items().first(), footage);
|
||||
EXPECT_EQ(explorer.selected_items().first(), to_oak(footage));
|
||||
|
||||
explorer.deselect_all();
|
||||
EXPECT_TRUE(explorer.selected_items().isEmpty());
|
||||
|
||||
Reference in New Issue
Block a user