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
+5
View File
@@ -140,6 +140,7 @@ add_executable(olive-gtest
widget_panels_model_test.cpp
panel_test.cpp
mainwindow_test.cpp
serialized_layout_xml_test.cpp
common_ratiodialog_test.cpp
)
@@ -147,6 +148,10 @@ find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)
target_sources(olive-gtest PRIVATE $<TARGET_OBJECTS:libolive-editor> $<TARGET_OBJECTS:olive-version-obj>)
if (COMMAND oak_copy_otio_runtime)
oak_copy_otio_runtime(olive-gtest)
endif()
target_include_directories(
olive-gtest
PRIVATE
+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);
+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;
+14 -3
View File
@@ -8,6 +8,17 @@
using namespace olive;
namespace
{
// Helper: wrap an engine Node* as oak::Node for the C ABI widget interface
inline oak::Node to_oak(olive::Node *n)
{
return oak::Node(reinterpret_cast<OakEngineNode *>(n));
}
} // namespace
class NodeViewTest : public ::testing::Test {
protected:
void SetUp() override
@@ -34,10 +45,10 @@ TEST_F(NodeViewTest, SetContextsUpdatesContextList)
solid->setParent(project_.get());
NodeView view;
view.set_contexts({ solid });
view.set_contexts({ to_oak(solid) });
EXPECT_EQ(view.get_contexts().size(), 1);
EXPECT_EQ(view.get_contexts().first(), solid);
EXPECT_EQ(view.get_contexts().first(), to_oak(solid));
}
TEST_F(NodeViewTest, ShowSelectedNodeInParamEditorNoSelectionIsNoOp)
@@ -71,7 +82,7 @@ TEST_F(NodeViewTest, ClearGraphRemovesContexts)
solid->setParent(project_.get());
NodeView view;
view.set_contexts({ solid });
view.set_contexts({ to_oak(solid) });
EXPECT_FALSE(view.get_contexts().isEmpty());
view.clear_graph();
+47 -24
View File
@@ -15,6 +15,8 @@
#include "node/project.h"
#include "node/project/folder/folder.h"
#include "node/project/sequence/sequence.h"
#include "oakengine/app.h"
#include "oakengine/undo.h"
#include "panel/audiomonitor/audiomonitor.h"
#include "panel/curve/curve.h"
#include "panel/footageviewer/footageviewer.h"
@@ -123,6 +125,24 @@ private:
QVariant saved_backend_;
};
// 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());
}
// Exposes the protected title/subtitle slots of the base class for testing
class TestPanel : public PanelWidget {
public:
@@ -411,7 +431,7 @@ TEST_F(PanelTest, CurvePanelSetNodes)
EXPECT_EQ(tree->topLevelItemCount(), 0);
// Same through the multi-node slot
panel.set_nodes({ math });
panel.set_nodes({ reinterpret_cast<OakEngineNode *>(math) });
EXPECT_EQ(tree->topLevelItemCount(), 1);
panel.set_nodes({});
EXPECT_EQ(tree->topLevelItemCount(), 0);
@@ -428,9 +448,9 @@ TEST_F(PanelTest, ParamPanelConstructionAndContexts)
ASSERT_NE(panel.get_param_view(), nullptr);
EXPECT_TRUE(panel.get_contexts().isEmpty());
panel.set_contexts({ project.root() });
panel.set_contexts({ to_oak(project.root()) });
ASSERT_EQ(panel.get_contexts().size(), 1);
EXPECT_EQ(panel.get_contexts().first(), project.root());
EXPECT_EQ(panel.get_contexts().first(), to_oak(project.root()));
// Selection slots on an empty selection are harmless
panel.select_all();
@@ -471,10 +491,10 @@ TEST_F(PanelTest, ProjectPanelTracksProject)
EXPECT_EQ(panel.project(), nullptr);
QSignalSpy name_spy(&panel, &ProjectPanel::project_name_changed);
panel.set_project(&project);
EXPECT_EQ(panel.project(), &project);
panel.set_project(to_oak_project(&project));
EXPECT_EQ(panel.project(), to_oak_project(&project));
EXPECT_EQ(name_spy.count(), 1);
EXPECT_EQ(panel.get_root(), project.root());
EXPECT_EQ(panel.get_root(), to_oak(project.root()));
// The subtitle reflects the project name in the panel title
EXPECT_TRUE(panel.title().contains(project.name()));
@@ -482,8 +502,8 @@ TEST_F(PanelTest, ProjectPanelTracksProject)
// A child folder can become the shown root
auto *folder = add_node<Folder>(&project);
FolderAddChild(project.root(), folder).redo_now();
panel.set_root(folder);
EXPECT_EQ(panel.get_root(), folder);
panel.set_root(to_oak(folder));
EXPECT_EQ(panel.get_root(), to_oak(folder));
}
TEST_F(PanelTest, ProjectPanelSelectsChildNodes)
@@ -494,12 +514,12 @@ TEST_F(PanelTest, ProjectPanelSelectsChildNodes)
FolderAddChild(project.root(), math).redo_now();
ProjectPanel panel(QStringLiteral("ProjectPanelSelectTest"));
panel.set_project(&project);
panel.set_project(to_oak_project(&project));
QSignalSpy selection_spy(&panel, &ProjectPanel::selection_changed);
ASSERT_TRUE(panel.select_item(math));
EXPECT_TRUE(panel.selected_items().contains(math));
ASSERT_TRUE(panel.select_item(to_oak(math)));
EXPECT_TRUE(panel.selected_items().contains(to_oak(math)));
EXPECT_GT(selection_spy.count(), 0);
}
@@ -544,8 +564,8 @@ TEST_F(PanelTest, TimeBasedPanelConnectViewerUpdatesSubtitle)
panel.set_time_based_widget(new TimeBasedWidget(false, false, &panel));
EXPECT_EQ(panel.get_connected_viewer(), nullptr);
panel.connect_viewer_node(viewer);
EXPECT_EQ(panel.get_connected_viewer(), viewer);
panel.connect_viewer_node(reinterpret_cast<OakEngineNode *>(viewer));
EXPECT_EQ(panel.get_connected_viewer(), reinterpret_cast<OakEngineNode *>(viewer));
EXPECT_TRUE(panel.title().contains(QStringLiteral("My Viewer")));
// Label changes on the viewer propagate to the panel title
@@ -574,12 +594,12 @@ TEST_F(PanelTest, NodeTablePanelConstruction)
EXPECT_EQ(view->topLevelItemCount(), 0);
// Selecting a node adds a top-level row labeled with the node
panel.select_nodes({ math });
panel.select_nodes({ reinterpret_cast<OakEngineNode *>(math) });
ASSERT_EQ(view->topLevelItemCount(), 1);
EXPECT_EQ(view->topLevelItem(0)->text(0), math->get_label_and_name());
// Deselecting removes it again
panel.deselect_nodes({ math });
panel.deselect_nodes({ reinterpret_cast<OakEngineNode *>(math) });
EXPECT_EQ(view->topLevelItemCount(), 0);
}
@@ -598,10 +618,13 @@ TEST_F(PanelTest, HistoryPanelReflectsUndoStack)
EXPECT_EQ(panel.objectName(), QStringLiteral("HistoryPanel"));
EXPECT_EQ(panel.title(), QStringLiteral("History"));
// The embedded HistoryWidget displays Core's undo stack
// The embedded HistoryWidget uses the app-side HistoryModel over the
// engine undo C ABI (no longer the engine UndoStack as a model)
auto *widget = panel.findChild<HistoryWidget *>();
ASSERT_NE(widget, nullptr);
EXPECT_EQ(widget->model(), Core::instance()->undo_stack());
ASSERT_NE(widget->model(), nullptr);
EXPECT_EQ(widget->model()->rowCount(),
static_cast<int>(oakengine_undo_count()));
}
TEST_F(PanelTest, FootageViewerPanelConstruction)
@@ -618,7 +641,7 @@ TEST_F(PanelTest, FootageViewerPanelConstruction)
// With nothing connected there is no selected footage
EXPECT_TRUE(panel.get_selected_footage().isEmpty());
panel.connect_viewer_node(viewer);
panel.connect_viewer_node(reinterpret_cast<OakEngineNode *>(viewer));
ASSERT_EQ(panel.get_selected_footage().size(), 1);
EXPECT_EQ(panel.get_selected_footage().first(), reinterpret_cast<OakEngineNode *>(viewer));
@@ -637,9 +660,9 @@ TEST_F(PanelTest, NodePanelConstructionAndContexts)
EXPECT_NE(panel.get_node_widget(), nullptr);
EXPECT_TRUE(panel.get_contexts().isEmpty());
panel.set_contexts({ project.root() });
panel.set_contexts({ to_oak(project.root()) });
ASSERT_EQ(panel.get_contexts().size(), 1);
EXPECT_EQ(panel.get_contexts().first(), project.root());
EXPECT_EQ(panel.get_contexts().first(), to_oak(project.root()));
// Node selection actions on an empty scene are harmless
panel.select_all();
@@ -653,7 +676,7 @@ TEST_F(PanelTest, NodePanelForwardsViewSignals)
auto *math = add_node<MathNode>(&project);
NodePanel panel;
panel.set_contexts({ project.root() });
panel.set_contexts({ to_oak(project.root()) });
QSignalSpy selected_spy(&panel, &NodePanel::nodes_selected);
emit panel.get_node_widget()->view()->nodes_selected({ reinterpret_cast<OakEngineNode *>(math) });
@@ -679,9 +702,9 @@ TEST_F(PanelTest, TimelinePanelConstructionAndSequence)
EXPECT_NE(panel.timeline_widget(), nullptr);
EXPECT_EQ(panel.get_sequence(), nullptr);
panel.connect_viewer_node(sequence);
EXPECT_EQ(panel.get_connected_viewer(), sequence);
EXPECT_EQ(panel.get_sequence(), sequence);
panel.connect_viewer_node(reinterpret_cast<OakEngineNode *>(sequence));
EXPECT_EQ(panel.get_connected_viewer(), reinterpret_cast<OakEngineNode *>(sequence));
EXPECT_EQ(panel.get_sequence(), reinterpret_cast<OakEngineSequence *>(sequence));
// Selection actions on an empty sequence are harmless
panel.select_all();
+7 -3
View File
@@ -555,8 +555,11 @@ TEST(NodeFactory, CreateMenuWithNoneItem)
}
const QString id = olive::get_node_id_from_menu_action(leaf);
EXPECT_FALSE(id.isEmpty());
// create_node_from_menu_action returns an owned oak::Node handle;
// reinterpret it back to the engine type to take ownership
const oak::Node created_handle = olive::create_node_from_menu_action(leaf);
std::unique_ptr<olive::Node> node(
olive::create_node_from_menu_action(leaf));
reinterpret_cast<olive::Node *>(created_handle.handle()));
ASSERT_NE(node, nullptr);
EXPECT_EQ(node->id(), id);
++created;
@@ -571,7 +574,7 @@ TEST(NodeFactory, CreateMenuRestrictedToCategory)
olive::NodeFactory::initialize();
std::unique_ptr<olive::Menu> menu(olive::create_node_menu(
nullptr, false, olive::Node::k_category_math));
nullptr, false, oak::k_category_math));
ASSERT_NE(menu, nullptr);
QList<QAction *> leaves;
@@ -579,8 +582,9 @@ TEST(NodeFactory, CreateMenuRestrictedToCategory)
ASSERT_FALSE(leaves.isEmpty());
for (QAction *leaf : leaves) {
const oak::Node created_handle = olive::create_node_from_menu_action(leaf);
std::unique_ptr<olive::Node> node(
olive::create_node_from_menu_action(leaf));
reinterpret_cast<olive::Node *>(created_handle.handle()));
ASSERT_NE(node, nullptr);
EXPECT_TRUE(node->category().contains(olive::Node::k_category_math))
<< node->id().toStdString();
+4 -1
View File
@@ -7,6 +7,7 @@
#include "config/config.h"
#include "dialog/proxy/proxydialog.h"
#include "node/project/footage/footage.h"
#include "oakengine/node.h"
namespace
{
@@ -40,7 +41,9 @@ TEST(ProxyDialog, ConstructsInGlobalModeWithNullParent)
TEST(ProxyDialog, ConstructsWithFootageList)
{
olive::Footage footage(QStringLiteral("/tmp/oak-proxy-test.mov"));
const QVector<olive::Footage *> items = { &footage };
const QVector<OakEngineNode *> items = {
reinterpret_cast<OakEngineNode *>(&footage)
};
olive::ProxyDialog dialog(nullptr, items);
EXPECT_EQ(dialog.windowTitle(), QStringLiteral("Proxy Settings"));
+2 -2
View File
@@ -678,11 +678,11 @@ TEST(ManagedColor, ColorInputAndOutputRoundTrip)
color.set_color_input(QStringLiteral("linear"));
EXPECT_EQ(color.color_input(), QStringLiteral("linear"));
color.set_color_output(olive::ColorTransform(QStringLiteral("sRGB")));
color.set_color_output(oak::ColorTransform(QStringLiteral("sRGB")));
EXPECT_FALSE(color.color_output().is_display());
EXPECT_EQ(color.color_output().output(), QStringLiteral("sRGB"));
color.set_color_output(olive::ColorTransform(QStringLiteral("sRGB"),
color.set_color_output(oak::ColorTransform(QStringLiteral("sRGB"),
QStringLiteral("Filmic"),
QStringLiteral("None")));
EXPECT_TRUE(color.color_output().is_display());
+110
View File
@@ -0,0 +1,110 @@
// Engine-side SerializedLayoutInfo XML (de)serialization tests.
//
// These live in their own translation unit because the app layer now carries
// a mirror type (app/common/serializedlayoutinfoapp.h) with the same
// fully-qualified name; including both headers in one TU is a redefinition
// error. The XML methods (to_xml/from_xml) exist only on the engine type.
#include <gtest/gtest.h>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include "node/output/viewer/viewer.h"
#include "node/project.h"
#include "node/project/folder/folder.h"
#include "node/project/sequence/sequence.h"
#include "node/project/serializer/serializedlayoutinfo.h"
using namespace olive;
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);
PanelLayoutInfo 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());
}
+6 -6
View File
@@ -15,8 +15,8 @@ TEST(TimeBasedWidget, ConnectedNodeClearsOnDelete)
{
olive::TimeBasedWidget widget(false, false);
auto *viewer = new olive::ViewerOutput();
widget.connect_viewer_node(viewer);
EXPECT_EQ(widget.get_connected_node(), viewer);
widget.connect_viewer_node(reinterpret_cast<OakEngineNode *>(viewer));
EXPECT_EQ(widget.get_connected_node(), reinterpret_cast<OakEngineNode *>(viewer));
delete viewer;
EXPECT_EQ(widget.get_connected_node(), nullptr);
}
@@ -63,11 +63,11 @@ TEST(SeekableWidget, SetMarkersAndWorkAreaAreReflected)
olive::TimelineMarkerList markers;
olive::TimelineWorkArea workarea;
widget.set_markers(&markers);
widget.set_work_area(&workarea);
widget.set_markers(reinterpret_cast<OakEngineMarkerList *>(&markers));
widget.set_work_area(reinterpret_cast<OakEngineWorkarea *>(&workarea));
EXPECT_EQ(widget.get_markers(), &markers);
EXPECT_EQ(widget.get_work_area(), &workarea);
EXPECT_EQ(widget.get_markers(), reinterpret_cast<OakEngineMarkerList *>(&markers));
EXPECT_EQ(widget.get_work_area(), reinterpret_cast<OakEngineWorkarea *>(&workarea));
}
TEST(SeekableWidget, MarkerEditingEnabledToggles)
+12 -12
View File
@@ -5,11 +5,11 @@
TEST(TimelineCoordinate, DefaultAndSetters)
{
olive::TimelineCoordinate coord;
EXPECT_EQ(coord.get_track().type(), olive::Track::k_none);
EXPECT_EQ(coord.get_track().type(), olive::TrackReference::k_none);
EXPECT_EQ(coord.get_track().index(), 0);
const olive::core::Rational frame(10, 1);
olive::Track::Reference ref(olive::Track::k_video, 2);
olive::TrackReference ref(olive::TrackReference::k_video, 2);
coord.set_frame(frame);
coord.set_track(ref);
@@ -21,22 +21,22 @@ TEST(TimelineCoordinate, DefaultAndSetters)
TEST(TimelineCoordinate, Constructors)
{
const olive::core::Rational frame(5, 1);
olive::Track::Reference ref(olive::Track::k_audio, 1);
olive::TrackReference ref(olive::TrackReference::k_audio, 1);
olive::TimelineCoordinate with_ref(frame, ref);
EXPECT_EQ(with_ref.get_frame(), frame);
EXPECT_EQ(with_ref.get_track(), ref);
olive::TimelineCoordinate with_type(frame, olive::Track::k_subtitle, 3);
olive::TimelineCoordinate with_type(frame, olive::TrackReference::k_subtitle, 3);
EXPECT_EQ(with_type.get_frame(), frame);
EXPECT_EQ(with_type.get_track().type(), olive::Track::k_subtitle);
EXPECT_EQ(with_type.get_track().type(), olive::TrackReference::k_subtitle);
EXPECT_EQ(with_type.get_track().index(), 3);
}
TEST(TimelineCoordinate, CopyAndAssignment)
{
const olive::core::Rational frame(7, 1);
olive::Track::Reference ref(olive::Track::k_video, 4);
olive::TrackReference ref(olive::TrackReference::k_video, 4);
olive::TimelineCoordinate original(frame, ref);
olive::TimelineCoordinate copy(original);
@@ -56,13 +56,13 @@ TEST(TimelineCoordinate, Equality)
// Track::Reference)
const olive::TimelineCoordinate a(
olive::core::Rational(5, 1),
olive::Track::Reference(olive::Track::k_video, 1));
olive::TrackReference(olive::TrackReference::k_video, 1));
// Distinct objects with identical frame and track compare equal in both
// components
const olive::TimelineCoordinate b(
olive::core::Rational(5, 1),
olive::Track::Reference(olive::Track::k_video, 1));
olive::TrackReference(olive::TrackReference::k_video, 1));
EXPECT_TRUE(a.get_frame() == b.get_frame());
EXPECT_TRUE(a.get_track() == b.get_track());
EXPECT_FALSE(a.get_frame() != b.get_frame());
@@ -71,7 +71,7 @@ TEST(TimelineCoordinate, Equality)
// A different frame breaks frame equality while the track stays equal
const olive::TimelineCoordinate c(
olive::core::Rational(6, 1),
olive::Track::Reference(olive::Track::k_video, 1));
olive::TrackReference(olive::TrackReference::k_video, 1));
EXPECT_FALSE(a.get_frame() == c.get_frame());
EXPECT_TRUE(a.get_frame() != c.get_frame());
EXPECT_TRUE(a.get_track() == c.get_track());
@@ -80,10 +80,10 @@ TEST(TimelineCoordinate, Equality)
// stays equal
const olive::TimelineCoordinate d(
olive::core::Rational(5, 1),
olive::Track::Reference(olive::Track::k_audio, 1));
olive::TrackReference(olive::TrackReference::k_audio, 1));
const olive::TimelineCoordinate e(
olive::core::Rational(5, 1),
olive::Track::Reference(olive::Track::k_video, 2));
olive::TrackReference(olive::TrackReference::k_video, 2));
EXPECT_TRUE(a.get_track() != d.get_track());
EXPECT_FALSE(a.get_track() == d.get_track());
EXPECT_TRUE(a.get_track() != e.get_track());
@@ -95,6 +95,6 @@ TEST(TimelineCoordinate, Equality)
mutated.set_frame(olive::core::Rational(7, 1));
EXPECT_TRUE(mutated.get_frame() != a.get_frame());
mutated.set_frame(olive::core::Rational(5, 1));
mutated.set_track(olive::Track::Reference(olive::Track::k_subtitle, 0));
mutated.set_track(olive::TrackReference(olive::TrackReference::k_subtitle, 0));
EXPECT_TRUE(mutated.get_track() != a.get_track());
}
+2 -2
View File
@@ -142,7 +142,7 @@ TEST(TimelineWaveformSync, PartialCacheIsConsideredReady)
Node::connect_edge(&footage, NodeInput(&clip, ClipBlock::k_buffer_in));
WaveformSyncClip out;
EXPECT_TRUE(timeline_waveform_sync::get_waveform_sync_clip(&clip, &out));
EXPECT_TRUE(timeline_waveform_sync::get_waveform_sync_clip(reinterpret_cast<OakEngineBlock *>(&clip), &out));
EXPECT_EQ(out.waveform, cache);
EXPECT_EQ(out.sample_rate, k_sample_rate);
EXPECT_EQ(out.media_range, TimeRange(0, 3));
@@ -163,5 +163,5 @@ TEST(TimelineWaveformSync, EmptyCacheIsNotReady)
Node::connect_edge(&footage, NodeInput(&clip, ClipBlock::k_buffer_in));
WaveformSyncClip out;
EXPECT_FALSE(timeline_waveform_sync::get_waveform_sync_clip(&clip, &out));
EXPECT_FALSE(timeline_waveform_sync::get_waveform_sync_clip(reinterpret_cast<OakEngineBlock *>(&clip), &out));
}
+5 -5
View File
@@ -239,7 +239,7 @@ TEST_P(ViewerDisplayReproTest, FootageViewerNotBlack)
viewer->resize(800, 600);
viewer->show();
viewer->connect_viewer_node(footage_);
viewer->connect_viewer_node(reinterpret_cast<OakEngineNode *>(footage_));
ASSERT_TRUE(wait_for_texture(viewer->display_widget()))
<< "Display widget never received a texture (backend="
@@ -269,7 +269,7 @@ TEST_P(ViewerDisplayReproTest, SequenceViewerIndirectNotBlack)
viewer->resize(800, 600);
viewer->show();
viewer->connect_viewer_node(sequence);
viewer->connect_viewer_node(reinterpret_cast<OakEngineNode *>(sequence));
ASSERT_TRUE(wait_for_texture(viewer->display_widget()))
<< "Display widget never received a texture (backend="
@@ -297,7 +297,7 @@ TEST_P(ViewerDisplayReproTest, SequenceViewerDirectNotBlack)
viewer->resize(800, 600);
viewer->show();
viewer->connect_viewer_node(sequence);
viewer->connect_viewer_node(reinterpret_cast<OakEngineNode *>(sequence));
ASSERT_TRUE(wait_for_texture(viewer->display_widget()))
<< "Display widget never received a texture (backend="
@@ -344,7 +344,7 @@ TEST_P(ViewerRuntimeRewireTest, RewireToDirectConnectionNotBlack)
TestViewerWidget *viewer = new TestViewerWidget();
viewer->resize(800, 600);
viewer->show();
viewer->connect_viewer_node(sequence);
viewer->connect_viewer_node(reinterpret_cast<OakEngineNode *>(sequence));
double brightness = pump_and_measure(viewer);
ASSERT_GT(brightness, 0.01)
@@ -376,7 +376,7 @@ TEST_P(ViewerRuntimeRewireTest, RewireToIndirectConnectionNotBlack)
TestViewerWidget *viewer = new TestViewerWidget();
viewer->resize(800, 600);
viewer->show();
viewer->connect_viewer_node(sequence);
viewer->connect_viewer_node(reinterpret_cast<OakEngineNode *>(sequence));
double brightness = pump_and_measure(viewer);
ASSERT_GT(brightness, 0.01)
+44 -18
View File
@@ -11,9 +11,11 @@
#include "node/math/math/math.h"
#include "node/nodeundo.h"
#include "node/project.h"
#include "oakengine/app.h"
#include "oakengine/node.h"
#include "oakengine/project.h"
#include "render/diskmanager.h"
#include "undo/undostack.h"
#include "widget/curvewidget/curveview.h"
#include "widget/curvewidget/curvewidget.h"
#include "widget/keyframeview/keyframeview.h"
@@ -35,6 +37,31 @@ void ensure_app_singletons()
}
}
// Helpers: wrap engine pointers as oak:: wrapper values for the C ABI
// widget interface
inline oak::Node to_oak(Node *n)
{
return oak::Node(reinterpret_cast<OakEngineNode *>(n));
}
inline oak::Keyframe to_oak_key(NodeKeyframe *k)
{
return oak::Keyframe(reinterpret_cast<OakEngineKeyframe *>(k));
}
inline oak::KeyframeTrackRef to_oak_ref(Node *n, const QString &input,
int track)
{
return oak::KeyframeTrackRef(
oak::Input(reinterpret_cast<OakEngineNode *>(n), input), track);
}
// The process-wide undo stack previously reached via Core::undo_stack()
inline UndoStack *app_undo_stack()
{
return static_cast<UndoStack *>(oakengine_app_undo_stack());
}
NodeKeyframe *insert_keyframe(Node *node, const QString &input,
const Rational &time, const QVariant &value,
int track = 0)
@@ -73,7 +100,7 @@ TEST_F(KeyframeViewTest, AddKeyframesOfNodeCreatesConnectionPerKeyframableInput)
MathNode *node = add_math_node();
KeyframeView view;
KeyframeView::NodeConnections map = view.add_keyframes_of_node(node);
KeyframeView::NodeConnections map = view.add_keyframes_of_node(to_oak(node));
// A float input has one element (the array-less -1) with one track
ASSERT_TRUE(map.contains(MathNode::k_param_a_in));
@@ -104,7 +131,7 @@ TEST_F(KeyframeViewTest, SelectAllAndDeselectAllUpdateSelection)
NodeKeyframe *key_b = insert_keyframe(node, MathNode::k_param_a_in, Rational(1), 1.0);
KeyframeView view;
view.add_keyframes_of_node(node);
view.add_keyframes_of_node(to_oak(node));
QSignalSpy selection_spy(&view, &KeyframeView::selection_changed);
@@ -129,7 +156,7 @@ TEST_F(KeyframeViewTest, RemoveKeyframesOfTrackDeselectsAndDetaches)
KeyframeView view;
KeyframeViewInputConnection *connection = view.add_keyframes_of_track(
NodeKeyframeTrackReference(NodeInput(node, MathNode::k_param_a_in), 0));
to_oak_ref(node, MathNode::k_param_a_in, 0));
ASSERT_NE(connection, nullptr);
ASSERT_EQ(view.get_keyframe_tracks().size(), 1);
@@ -154,7 +181,7 @@ TEST_F(KeyframeViewTest, ClearRemovesAllTracksAndSelection)
insert_keyframe(node, MathNode::k_param_a_in, Rational(0), 0.0);
KeyframeView view;
view.add_keyframes_of_node(node);
view.add_keyframes_of_node(to_oak(node));
view.select_all();
ASSERT_FALSE(view.get_keyframe_tracks().isEmpty());
ASSERT_FALSE(view.get_selected_keyframes().empty());
@@ -171,7 +198,7 @@ TEST_F(KeyframeViewTest, DeleteSelectedPushesUndoableRemoval)
KeyframeView view;
view.add_keyframes_of_track(
NodeKeyframeTrackReference(NodeInput(node, MathNode::k_param_a_in), 0));
to_oak_ref(node, MathNode::k_param_a_in, 0));
view.select_all();
view.delete_selected();
@@ -180,18 +207,18 @@ TEST_F(KeyframeViewTest, DeleteSelectedPushesUndoableRemoval)
.at(0)
.isEmpty());
Core::instance()->undo_stack()->undo();
app_undo_stack()->undo();
EXPECT_TRUE(node->get_keyframe_tracks(MathNode::k_param_a_in, -1)
.at(0)
.contains(key));
Core::instance()->undo_stack()->redo();
app_undo_stack()->redo();
EXPECT_TRUE(node->get_keyframe_tracks(MathNode::k_param_a_in, -1)
.at(0)
.isEmpty());
// Keep the shared undo stack clean for other suites
Core::instance()->undo_stack()->clear();
app_undo_stack()->clear();
}
class KeyframeViewUndoTest : public ::testing::Test {
@@ -295,10 +322,9 @@ protected:
solid_->setParent(project_.get());
}
NodeKeyframeTrackReference color_track_ref(int track) const
oak::KeyframeTrackRef color_track_ref(int track) const
{
return NodeKeyframeTrackReference(
NodeInput(solid_, SolidGenerator::k_color_input), track);
return to_oak_ref(solid_, SolidGenerator::k_color_input, track);
}
std::unique_ptr<Project> project_;
@@ -346,7 +372,7 @@ TEST_F(CurveViewTest, ConnectionReflectsLiveKeyframeList)
NodeKeyframe *key =
insert_keyframe(solid_, SolidGenerator::k_color_input, Rational(0), 0.5, 0);
EXPECT_EQ(connection->get_keyframes().size(), 1);
EXPECT_EQ(connection->get_keyframes().first(), key);
EXPECT_EQ(connection->get_keyframes().first(), to_oak_key(key));
}
TEST_F(CurveViewTest, SetKeyframeTrackColorAppliesToBrush)
@@ -385,13 +411,13 @@ TEST_F(CurveViewTest, SelectKeyframesOfInputSelectsOnlyRequestedTrack)
// Previously the reference was ignored and keyframes of every connected
// track got selected.
view.select_keyframes_of_input(color_track_ref(0));
EXPECT_TRUE(view.is_keyframe_selected(key0));
EXPECT_FALSE(view.is_keyframe_selected(key1));
EXPECT_TRUE(view.is_keyframe_selected(to_oak_key(key0)));
EXPECT_FALSE(view.is_keyframe_selected(to_oak_key(key1)));
// Selecting the other track replaces the selection (DeselectAll first)
view.select_keyframes_of_input(color_track_ref(1));
EXPECT_FALSE(view.is_keyframe_selected(key0));
EXPECT_TRUE(view.is_keyframe_selected(key1));
EXPECT_FALSE(view.is_keyframe_selected(to_oak_key(key0)));
EXPECT_TRUE(view.is_keyframe_selected(to_oak_key(key1)));
}
TEST(CurveWidget, VerticalScaleRoundTripsThroughView)
@@ -418,7 +444,7 @@ TEST(CurveWidget, TreeSelectionConnectsTracksAndResolvesNodeId)
solid->retranslate();
CurveWidget widget;
widget.set_nodes({ solid });
widget.set_nodes({ to_oak(solid) });
auto *tree = widget.findChild<NodeTreeView *>();
ASSERT_NE(tree, nullptr);
@@ -433,7 +459,7 @@ TEST(CurveWidget, TreeSelectionConnectsTracksAndResolvesNodeId)
QTreeWidgetItem *color_item = node_item->child(1);
color_item->setSelected(true);
EXPECT_EQ(widget.get_selected_node_with_id(solid->id()), solid);
EXPECT_EQ(widget.get_selected_node_with_id(solid->id()), to_oak(solid));
EXPECT_EQ(widget.get_selected_node_with_id(QStringLiteral("org.example.bogus")),
nullptr);
+5 -4
View File
@@ -19,7 +19,7 @@
#include "node/math/math/math.h"
#include "node/project.h"
#include "node/traverser.h"
#include "ui/colorcoding.h"
#include "common/colorcodingapp.h"
#include "ui/icons/icons.h"
#include "widget/bezier/bezierwidget.h"
#include "widget/clickablelabel/clickablelabel.h"
@@ -208,14 +208,14 @@ TEST(WidgetMenu, ConformItemStoresIdAndKeyDefault)
TEST(WidgetColorLabelMenu, ItemsCarryIndexAndEmitSelection)
{
olive::ColorLabelMenu menu;
const int color_count = int(olive::ColorCoding::standard_colors().size());
const int color_count = int(olive::AppColorCoding::standard_colors().size());
ASSERT_GE(color_count, 3);
ASSERT_EQ(menu.actions().size(), color_count);
for (int i = 0; i < color_count; i++) {
QAction *a = menu.actions().at(i);
EXPECT_EQ(a->data().toInt(), i);
EXPECT_EQ(a->text(), olive::ColorCoding::get_color_name(i));
EXPECT_EQ(a->text(), olive::AppColorCoding::get_color_name(i));
EXPECT_EQ(a->property("id").toString(),
QStringLiteral("colorlabel%1").arg(i));
}
@@ -864,7 +864,8 @@ TEST(WidgetNodeValueTree, PopulatesRowsAndSetsValueHint)
olive::NodeInput(consumer, olive::MathNode::k_param_a_in));
olive::NodeValueTree tree;
tree.set_node(olive::NodeInput(consumer, olive::MathNode::k_param_a_in),
tree.set_node(oak::Input(reinterpret_cast<OakEngineNode *>(consumer),
olive::MathNode::k_param_a_in),
olive::Rational(0));
// One row per pushed value
+100 -66
View File
@@ -22,6 +22,8 @@
#include "node/project/folder/folder.h"
#include "render/diskmanager.h"
#include "render/rendermanager.h"
#include "oakengine/app.h"
#include "oakengine/undo.h"
#include "task/task.h"
#include "undo/undostack.h"
#include "widget/colorbutton/colorbutton.h"
@@ -98,6 +100,24 @@ private:
const int k_item_type_role = Qt::UserRole;
const int k_item_input_reference_role = Qt::UserRole + 1;
// Helper: wrap an engine Node* as oak::Node for the C ABI widget interface
inline oak::Node to_oak(Node *n)
{
return oak::Node(reinterpret_cast<OakEngineNode *>(n));
}
// Helper: build an oak::Input value from an engine Node* + input id
inline oak::Input to_oak_input(Node *n, const QString &input, int element = -1)
{
return oak::Input(reinterpret_cast<OakEngineNode *>(n), input, element);
}
// 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 WidgetPanelsTest : public ::testing::Test {
@@ -128,18 +148,18 @@ TEST_F(WidgetPanelsTest, NodeTableSelectNodesCreatesTopLevelItems)
NodeTableView view;
EXPECT_EQ(view.topLevelItemCount(), 0);
view.select_nodes({ solid });
view.select_nodes({ to_oak(solid) });
ASSERT_EQ(view.topLevelItemCount(), 1);
EXPECT_EQ(view.topLevelItem(0)->text(0), solid->get_label_and_name());
auto *math = add_node<MathNode>();
view.select_nodes({ math });
view.select_nodes({ to_oak(math) });
EXPECT_EQ(view.topLevelItemCount(), 2);
view.deselect_nodes({ solid });
view.deselect_nodes({ to_oak(solid) });
EXPECT_EQ(view.topLevelItemCount(), 1);
view.deselect_nodes({ math });
view.deselect_nodes({ to_oak(math) });
EXPECT_EQ(view.topLevelItemCount(), 0);
}
@@ -149,7 +169,7 @@ TEST_F(WidgetPanelsTest, NodeTableSetTimePopulatesInputRows)
solid->retranslate();
NodeTableView view;
view.select_nodes({ solid });
view.select_nodes({ to_oak(solid) });
QTreeWidgetItem *top = view.topLevelItem(0);
ASSERT_NE(top, nullptr);
@@ -191,7 +211,7 @@ TEST_F(WidgetPanelsTest, NodeTreeSetNodesBuildsInputHierarchy)
math->retranslate();
NodeTreeView view;
view.set_nodes({ math });
view.set_nodes({ to_oak(math) });
ASSERT_EQ(view.topLevelItemCount(), 1);
QTreeWidgetItem *node_item = view.topLevelItem(0);
@@ -205,11 +225,11 @@ TEST_F(WidgetPanelsTest, NodeTreeSetNodesBuildsInputHierarchy)
for (int i = 0; i < expected_inputs.size(); i++) {
QTreeWidgetItem *input_item = node_item->child(i);
EXPECT_EQ(input_item->data(0, k_item_type_role).toInt(), 1); // kItemTypeInput
const NodeKeyframeTrackReference ref =
const oak::KeyframeTrackRef ref =
input_item->data(0, k_item_input_reference_role)
.value<NodeKeyframeTrackReference>();
EXPECT_EQ(ref.input().node(), math);
EXPECT_EQ(ref.input().input(), expected_inputs.at(i));
.value<oak::KeyframeTrackRef>();
EXPECT_EQ(ref.input().node_handle(), reinterpret_cast<OakEngineNode *>(math));
EXPECT_EQ(ref.input().input_id(), expected_inputs.at(i));
}
}
@@ -219,7 +239,7 @@ TEST_F(WidgetPanelsTest, NodeTreeOnlyShowKeyframableFiltersInputs)
NodeTreeView view;
view.set_only_show_keyframable(true);
view.set_nodes({ math });
view.set_nodes({ to_oak(math) });
// The method combo is flagged not-keyframable; enabled and the two
// float params remain
@@ -229,13 +249,13 @@ TEST_F(WidgetPanelsTest, NodeTreeOnlyShowKeyframableFiltersInputs)
// Of a bare viewer's inputs only "enabled" is keyframable, so it is the
// sole row left standing
auto *viewer = add_node<ViewerOutput>();
view.set_nodes({ viewer });
view.set_nodes({ to_oak(viewer) });
ASSERT_EQ(view.topLevelItemCount(), 1);
EXPECT_EQ(view.topLevelItem(0)->childCount(), 1);
// Without the filter its buffer inputs show up as well
view.set_only_show_keyframable(false);
view.set_nodes({ viewer });
view.set_nodes({ to_oak(viewer) });
ASSERT_EQ(view.topLevelItemCount(), 1);
EXPECT_EQ(view.topLevelItem(0)->childCount(), 3);
}
@@ -246,7 +266,7 @@ TEST_F(WidgetPanelsTest, NodeTreeCheckboxesToggleEnableStateAndEmit)
NodeTreeView view;
view.set_check_boxes_enabled(true);
view.set_nodes({ math });
view.set_nodes({ to_oak(math) });
Node *node_signal_node = nullptr;
bool node_signal_enabled = true;
@@ -259,12 +279,12 @@ TEST_F(WidgetPanelsTest, NodeTreeCheckboxesToggleEnableStateAndEmit)
++node_emissions;
});
NodeKeyframeTrackReference input_signal_ref;
oak::KeyframeTrackRef input_signal_ref;
bool input_signal_enabled = true;
int input_emissions = 0;
QObject::connect(&view, &NodeTreeView::input_enable_changed,
[&input_signal_ref, &input_signal_enabled,
&input_emissions](const NodeKeyframeTrackReference &ref,
&input_emissions](const oak::KeyframeTrackRef &ref,
bool e) {
input_signal_ref = ref;
input_signal_enabled = e;
@@ -280,20 +300,20 @@ TEST_F(WidgetPanelsTest, NodeTreeCheckboxesToggleEnableStateAndEmit)
EXPECT_EQ(node_emissions, 1);
EXPECT_EQ(node_signal_node, math);
EXPECT_FALSE(node_signal_enabled);
EXPECT_FALSE(view.is_node_enabled(math));
EXPECT_FALSE(view.is_node_enabled(to_oak(math)));
// Re-checking restores it
node_item->setCheckState(0, Qt::Checked);
EXPECT_EQ(node_emissions, 2);
EXPECT_TRUE(node_signal_enabled);
EXPECT_TRUE(view.is_node_enabled(math));
EXPECT_TRUE(view.is_node_enabled(to_oak(math)));
// Same behavior on input rows
QTreeWidgetItem *input_item = node_item->child(0);
ASSERT_NE(input_item, nullptr);
input_item->setCheckState(0, Qt::Unchecked);
EXPECT_EQ(input_emissions, 1);
EXPECT_EQ(input_signal_ref.input().input(), Node::k_enabled_input);
EXPECT_EQ(input_signal_ref.input().input_id(), Node::k_enabled_input);
EXPECT_FALSE(input_signal_enabled);
EXPECT_FALSE(view.is_input_enabled(input_signal_ref));
}
@@ -305,7 +325,7 @@ TEST_F(WidgetPanelsTest, NodeTreeKeyframeTracksBecomeRows)
NodeTreeView view;
view.set_show_keyframe_tracks_as_rows(true);
view.set_nodes({ solid });
view.set_nodes({ to_oak(solid) });
QTreeWidgetItem *node_item = view.topLevelItem(0);
ASSERT_NE(node_item, nullptr);
@@ -319,17 +339,17 @@ TEST_F(WidgetPanelsTest, NodeTreeKeyframeTracksBecomeRows)
QStringLiteral("B"), QStringLiteral("A") };
for (int i = 0; i < 4; i++) {
EXPECT_EQ(color_item->child(i)->text(0), track_names.at(i));
const NodeKeyframeTrackReference ref =
const oak::KeyframeTrackRef ref =
color_item->child(i)
->data(0, k_item_input_reference_role)
.value<NodeKeyframeTrackReference>();
.value<oak::KeyframeTrackRef>();
EXPECT_EQ(ref.track(), i);
}
// A single-track float input stays a single row
auto *math = add_node<MathNode>();
math->retranslate();
view.set_nodes({ math });
view.set_nodes({ to_oak(math) });
QTreeWidgetItem *param_item =
view.topLevelItem(0)->child(2); // after enabled and the method combo
ASSERT_NE(param_item, nullptr);
@@ -342,7 +362,9 @@ TEST_F(WidgetPanelsTest, BridgeCreatesSliderForFloatInput)
auto *math = add_node<MathNode>();
QWidget parent;
NodeParamViewWidgetBridge bridge(NodeInput(math, MathNode::k_param_a_in), &parent);
NodeParamViewWidgetBridge bridge(
to_oak_input(math, MathNode::k_param_a_in, -1),
&parent);
ASSERT_EQ(bridge.widgets().size(), 1);
EXPECT_NE(qobject_cast<FloatSlider *>(bridge.widgets().first()), nullptr);
@@ -353,8 +375,9 @@ TEST_F(WidgetPanelsTest, BridgeCreatesColorButtonForColorInput)
auto *solid = add_node<SolidGenerator>();
QWidget parent;
NodeParamViewWidgetBridge bridge(NodeInput(solid, SolidGenerator::k_color_input),
&parent);
NodeParamViewWidgetBridge bridge(
to_oak_input(solid, SolidGenerator::k_color_input, -1),
&parent);
ASSERT_EQ(bridge.widgets().size(), 1);
EXPECT_NE(qobject_cast<ColorButton *>(bridge.widgets().first()), nullptr);
@@ -366,7 +389,9 @@ TEST_F(WidgetPanelsTest, BridgeCreatesComboBoxForComboInput)
math->retranslate();
QWidget parent;
NodeParamViewWidgetBridge bridge(NodeInput(math, MathNode::k_method_in), &parent);
NodeParamViewWidgetBridge bridge(
to_oak_input(math, MathNode::k_method_in, -1),
&parent);
ASSERT_EQ(bridge.widgets().size(), 1);
auto *combo = qobject_cast<QComboBox *>(bridge.widgets().first());
@@ -380,8 +405,9 @@ TEST_F(WidgetPanelsTest, BridgeCreatesCheckBoxForBooleanInput)
auto *clip = add_node<ClipBlock>();
QWidget parent;
NodeParamViewWidgetBridge bridge(NodeInput(clip, ClipBlock::k_reverse_input),
&parent);
NodeParamViewWidgetBridge bridge(
to_oak_input(clip, ClipBlock::k_reverse_input, -1),
&parent);
ASSERT_EQ(bridge.widgets().size(), 1);
EXPECT_NE(qobject_cast<QCheckBox *>(bridge.widgets().first()), nullptr);
@@ -393,14 +419,16 @@ TEST_F(WidgetPanelsTest, BridgeUpdatesWidgetWhenNodeValueChanges)
auto *viewer = add_node<ViewerOutput>();
QWidget parent;
NodeParamViewWidgetBridge bridge(NodeInput(math, MathNode::k_param_a_in), &parent);
NodeParamViewWidgetBridge bridge(
to_oak_input(math, MathNode::k_param_a_in, -1),
&parent);
auto *slider = qobject_cast<FloatSlider *>(bridge.widgets().first());
ASSERT_NE(slider, nullptr);
EXPECT_DOUBLE_EQ(slider->get_value(), 0.0);
// The bridge only refreshes widgets for value changes at the playhead
// of a connected time target
bridge.set_time_target(viewer);
bridge.set_time_target(reinterpret_cast<OakEngineNode *>(viewer));
math->set_standard_value(MathNode::k_param_a_in, 2.5);
EXPECT_DOUBLE_EQ(slider->get_value(), 2.5);
@@ -413,7 +441,9 @@ TEST_F(WidgetPanelsTest, BridgePushesUndoCommandWhenWidgetChanges)
ASSERT_EQ(math->get_standard_value(MathNode::k_method_in).toInt(), 0);
QWidget parent;
NodeParamViewWidgetBridge bridge(NodeInput(math, MathNode::k_method_in), &parent);
NodeParamViewWidgetBridge bridge(
to_oak_input(math, MathNode::k_method_in, -1),
&parent);
auto *combo = qobject_cast<QComboBox *>(bridge.widgets().first());
ASSERT_NE(combo, nullptr);
ASSERT_GT(combo->count(), 1);
@@ -421,9 +451,9 @@ TEST_F(WidgetPanelsTest, BridgePushesUndoCommandWhenWidgetChanges)
combo->setCurrentIndex(1);
EXPECT_EQ(math->get_standard_value(MathNode::k_method_in).toInt(), 1);
Core::instance()->undo_stack()->undo();
app_undo_stack()->undo();
EXPECT_EQ(math->get_standard_value(MathNode::k_method_in).toInt(), 0);
Core::instance()->undo_stack()->clear();
app_undo_stack()->clear();
}
TEST(TaskView, TaskLifecycleUpdatesItems)
@@ -481,37 +511,41 @@ TEST(TaskView, TaskLifecycleUpdatesItems)
TEST(HistoryWidget, ReflectsAndDrivesUndoStack)
{
ensure_app_singletons();
UndoStack *stack = Core::instance()->undo_stack();
UndoStack *stack = app_undo_stack();
stack->clear();
HistoryWidget widget;
EXPECT_EQ(widget.model(), stack);
// The app-side HistoryModel mirrors the engine undo stack over the C
// ABI; it is NOT the engine UndoStack itself. The engine stack keeps a
// "New/Open Project" sentinel command after clear(), which is row 0.
QAbstractItemModel *model = widget.model();
ASSERT_NE(model, nullptr);
EXPECT_EQ(model->rowCount(), 1);
int counter = 0;
stack->push(new IncrementCommand(&counter), QStringLiteral("First"));
stack->push(new IncrementCommand(&counter), QStringLiteral("Second"));
EXPECT_EQ(counter, 2);
EXPECT_EQ(stack->rowCount(), 3);
EXPECT_EQ(model->rowCount(), 3);
// Row 0 is the "New/Open Project" sentinel the stack starts with, so
// "First" lives at row 1 and "Second" at row 2. Moving the current row
// jumps the stack to row+1 applied commands (this is how clicking a
// history row works).
widget.selectionModel()->setCurrentIndex(stack->index(1, 0),
widget.selectionModel()->setCurrentIndex(model->index(1, 0),
QItemSelectionModel::ClearAndSelect |
QItemSelectionModel::Rows);
EXPECT_EQ(counter, 1);
EXPECT_TRUE(stack->can_redo());
// Moving to the second entry redoes everything again
widget.selectionModel()->setCurrentIndex(stack->index(2, 0),
widget.selectionModel()->setCurrentIndex(model->index(2, 0),
QItemSelectionModel::ClearAndSelect |
QItemSelectionModel::Rows);
EXPECT_EQ(counter, 2);
EXPECT_FALSE(stack->can_redo());
// Moving back to the sentinel row undoes both commands
widget.selectionModel()->setCurrentIndex(stack->index(0, 0),
widget.selectionModel()->setCurrentIndex(model->index(0, 0),
QItemSelectionModel::ClearAndSelect |
QItemSelectionModel::Rows);
EXPECT_EQ(counter, 0);
@@ -523,7 +557,7 @@ TEST(HistoryWidget, ReflectsAndDrivesUndoStack)
TEST(TimelineSelections, ShiftTimeMovesAllRanges)
{
TimelineWidgetSelections sel;
const Track::Reference video0(Track::k_video, 0);
const TrackReference video0(TrackReference::k_video, 0);
sel.insert(video0,
TimeRangeList({ TimeRange(Rational(0), Rational(10)) }));
@@ -537,26 +571,26 @@ TEST(TimelineSelections, ShiftTimeMovesAllRanges)
TEST(TimelineSelections, ShiftTracksReindexesMatchingTypeOnly)
{
TimelineWidgetSelections sel;
sel.insert(Track::Reference(Track::k_video, 0),
sel.insert(TrackReference(TrackReference::k_video, 0),
TimeRangeList({ TimeRange(Rational(0), Rational(10)) }));
sel.insert(Track::Reference(Track::k_video, 1),
sel.insert(TrackReference(TrackReference::k_video, 1),
TimeRangeList({ TimeRange(Rational(0), Rational(10)) }));
sel.insert(Track::Reference(Track::k_audio, 0),
sel.insert(TrackReference(TrackReference::k_audio, 0),
TimeRangeList({ TimeRange(Rational(0), Rational(10)) }));
sel.shift_tracks(Track::k_video, 2);
sel.shift_tracks(TrackReference::k_video, 2);
EXPECT_FALSE(sel.contains(Track::Reference(Track::k_video, 0)));
EXPECT_FALSE(sel.contains(Track::Reference(Track::k_video, 1)));
EXPECT_TRUE(sel.contains(Track::Reference(Track::k_video, 2)));
EXPECT_TRUE(sel.contains(Track::Reference(Track::k_video, 3)));
EXPECT_TRUE(sel.contains(Track::Reference(Track::k_audio, 0)));
EXPECT_FALSE(sel.contains(TrackReference(TrackReference::k_video, 0)));
EXPECT_FALSE(sel.contains(TrackReference(TrackReference::k_video, 1)));
EXPECT_TRUE(sel.contains(TrackReference(TrackReference::k_video, 2)));
EXPECT_TRUE(sel.contains(TrackReference(TrackReference::k_video, 3)));
EXPECT_TRUE(sel.contains(TrackReference(TrackReference::k_audio, 0)));
}
TEST(TimelineSelections, TrimInAndOutAdjustRangeEnds)
{
TimelineWidgetSelections in_sel;
const Track::Reference video0(Track::k_video, 0);
const TrackReference video0(TrackReference::k_video, 0);
in_sel.insert(video0,
TimeRangeList({ TimeRange(Rational(0), Rational(10)) }));
in_sel.trim_in(Rational(2));
@@ -574,14 +608,14 @@ TEST(TimelineSelections, TrimInAndOutAdjustRangeEnds)
TEST(TimelineSelections, SubtractSplitsAndIgnoresForeignTracks)
{
TimelineWidgetSelections ours;
const Track::Reference video0(Track::k_video, 0);
const TrackReference video0(TrackReference::k_video, 0);
ours.insert(video0,
TimeRangeList({ TimeRange(Rational(0), Rational(10)) }));
TimelineWidgetSelections theirs;
theirs.insert(video0,
TimeRangeList({ TimeRange(Rational(3), Rational(5)) }));
theirs.insert(Track::Reference(Track::k_audio, 0),
theirs.insert(TrackReference(TrackReference::k_audio, 0),
TimeRangeList({ TimeRange(Rational(0), Rational(99)) }));
TimelineWidgetSelections result = ours.subtracted(theirs);
@@ -610,17 +644,17 @@ TEST(NodeViewScene, AddAndRemoveContexts)
NodeViewScene scene;
EXPECT_TRUE(scene.context_map().isEmpty());
NodeViewContext *ctx = scene.add_context(folder);
NodeViewContext *ctx = scene.add_context(to_oak(folder));
ASSERT_NE(ctx, nullptr);
EXPECT_TRUE(scene.context_map().contains(folder));
EXPECT_EQ(ctx->get_context(), folder);
EXPECT_TRUE(scene.context_map().contains(to_oak(folder)));
EXPECT_EQ(ctx->get_context(), to_oak(folder));
EXPECT_TRUE(scene.items().contains(ctx));
// Re-adding the same node returns the existing context item
EXPECT_EQ(scene.add_context(folder), ctx);
EXPECT_EQ(scene.add_context(to_oak(folder)), ctx);
EXPECT_EQ(scene.context_map().size(), 1);
scene.remove_context(folder);
scene.remove_context(to_oak(folder));
EXPECT_TRUE(scene.context_map().isEmpty());
}
@@ -687,8 +721,8 @@ TEST_F(MulticamWidgetTest, SwitchWithoutTimestampAppliesImmediately)
clip->setParent(project_.get());
MulticamWidget widget;
widget.set_multicam_node(viewer, node, clip, Rational());
EXPECT_EQ(widget.get_connected_node(), viewer);
widget.set_multicam_node(reinterpret_cast<OakEngineNode *>(viewer), reinterpret_cast<OakEngineNode *>(node), reinterpret_cast<OakEngineBlock *>(clip), Rational());
EXPECT_EQ(widget.get_connected_node(), reinterpret_cast<OakEngineNode *>(viewer));
}
TEST_F(MulticamWidgetTest, FutureSwitchWaitsForPlayheadToAdvance)
@@ -703,14 +737,14 @@ TEST_F(MulticamWidgetTest, FutureSwitchWaitsForPlayheadToAdvance)
clip->setParent(project_.get());
MulticamWidget widget;
widget.set_multicam_node(viewer_a, node, clip, Rational());
ASSERT_EQ(widget.get_connected_node(), viewer_a);
widget.set_multicam_node(reinterpret_cast<OakEngineNode *>(viewer_a), reinterpret_cast<OakEngineNode *>(node), reinterpret_cast<OakEngineBlock *>(clip), Rational());
ASSERT_EQ(widget.get_connected_node(), reinterpret_cast<OakEngineNode *>(viewer_a));
// A switch stamped for a later time is queued, not applied
widget.set_multicam_node(viewer_b, node, clip, Rational(5));
EXPECT_EQ(widget.get_connected_node(), viewer_a);
widget.set_multicam_node(reinterpret_cast<OakEngineNode *>(viewer_b), reinterpret_cast<OakEngineNode *>(node), reinterpret_cast<OakEngineBlock *>(clip), Rational(5));
EXPECT_EQ(widget.get_connected_node(), reinterpret_cast<OakEngineNode *>(viewer_a));
// Once playback time advances, the queued switch takes effect
viewer_a->set_playhead(Rational(1));
EXPECT_EQ(widget.get_connected_node(), viewer_b);
EXPECT_EQ(widget.get_connected_node(), reinterpret_cast<OakEngineNode *>(viewer_b));
}
+81 -48
View File
@@ -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());
@@ -102,7 +102,7 @@ TEST(TimeRuler, SeekToScenePointSeeksConnectedViewer)
ruler.set_scale(100.0);
ViewerOutput viewer;
ruler.set_viewer_node(&viewer);
ruler.set_viewer_node(reinterpret_cast<OakEngineNode *>(&viewer));
ruler.seek_to_scene_point(150.0);
EXPECT_EQ(viewer.get_playhead(), Rational(3, 2));