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