test(app): add gtest coverage for previously untested app modules
Add 312 test cases across 14 new files under tests/gtest, covering: - app/common utility headers (tooltypes, projecttypes, nodedatatypes, trackreferencehandle, undowrapper, configwrapper, subtitleapp, ...) - all 8 standardcombos widgets - slider base classes (NumericSliderBase, SliderLadder) - timeline/track handles, marker handle/painting, drag button, elapsed counter - timebased views, selection manager, time target, resizable timeline scrollbar - viewer sizer, display buffer, playback timer, text editor - timeline common helpers, TimelineAndTrackView, TrackView(-item/-splitter) - project explorer views, navigation and undo commands - node view items/edges/connectors/minimap/toolbar/context - node param view sub-widgets - about/export/sequence/footage-properties dialogs - scope widgets (histogram/vectorscope/waveform), managed display - main menu, main window undo commands, viewer/footage panels, engine event bridge - keyframe view handle/input connection, multicam display, node combo box, audio monitor Includes regression tests for the TimelineAndTrackView teardown and TimeScaledObject inverted-limits fixes. Full suite: 2341 tests, 0 failures (64 skips are pre-existing GL-dependent cases under the offscreen QPA).
This commit is contained in:
@@ -0,0 +1,781 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCloseEvent>
|
||||
#include <QFontComboBox>
|
||||
#include <QFontDatabase>
|
||||
#include <QTextCursor>
|
||||
#include <QImage>
|
||||
#include <QMatrix4x4>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QScrollBar>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "widget/viewer/displaybuffer.h"
|
||||
#include "widget/viewer/vieweroutpututils.h"
|
||||
#include "widget/viewer/viewerplaybacktimer.h"
|
||||
#include "widget/viewer/viewerpreventsleep.h"
|
||||
#include "widget/viewer/viewersizer.h"
|
||||
#include "widget/viewer/viewertexteditor.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// ViewerSizer's scrollbars are created in a fixed order (horizontal first),
|
||||
// and findChildren preserves child creation order
|
||||
QList<QScrollBar *> sizer_scrollbars(olive::ViewerSizer *sizer)
|
||||
{
|
||||
return sizer->findChildren<QScrollBar *>();
|
||||
}
|
||||
|
||||
// ViewerTextEditorToolBar push buttons in creation order (see
|
||||
// viewertexteditor.cpp): underline, strikethrough, color, align left/center/
|
||||
// right/justify, align top/middle/bottom, small caps
|
||||
QList<QPushButton *> toolbar_buttons(olive::ViewerTextEditorToolBar *toolbar)
|
||||
{
|
||||
return toolbar->findChildren<QPushButton *>();
|
||||
}
|
||||
|
||||
enum ToolBarButton {
|
||||
k_underline = 0,
|
||||
k_strikethrough = 1,
|
||||
k_color = 2,
|
||||
k_align_left = 3,
|
||||
k_align_center = 4,
|
||||
k_align_right = 5,
|
||||
k_align_justify = 6,
|
||||
k_align_top = 7,
|
||||
k_align_middle = 8,
|
||||
k_align_bottom = 9,
|
||||
k_small_caps = 10,
|
||||
};
|
||||
|
||||
QMatrix4x4 last_matrix(QSignalSpy &spy)
|
||||
{
|
||||
return spy.last().first().value<QMatrix4x4>();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ViewerSizer, ScrollBarsStartHiddenAndZoomIsFit)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
const auto bars = sizer_scrollbars(&sizer);
|
||||
ASSERT_EQ(bars.size(), 2);
|
||||
EXPECT_EQ(bars.at(0)->orientation(), Qt::Horizontal);
|
||||
EXPECT_EQ(bars.at(1)->orientation(), Qt::Vertical);
|
||||
EXPECT_FALSE(bars.at(0)->isVisibleTo(&sizer));
|
||||
EXPECT_FALSE(bars.at(1)->isVisibleTo(&sizer));
|
||||
}
|
||||
|
||||
TEST(ViewerSizer, ZeroChildSizeFillsContainerWithoutScaleRequest)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
sizer.resize(400, 300);
|
||||
|
||||
auto *child = new QWidget();
|
||||
sizer.set_widget(child);
|
||||
|
||||
QSignalSpy scale_spy(&sizer, &olive::ViewerSizer::request_scale);
|
||||
|
||||
// width_/height_ default to 0, so the child simply fills the container
|
||||
EXPECT_EQ(child->geometry(), QRect(0, 0, 400, 300));
|
||||
EXPECT_EQ(scale_spy.count(), 0);
|
||||
}
|
||||
|
||||
TEST(ViewerSizer, SetWidgetDestroysPreviousWidget)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
sizer.resize(200, 200);
|
||||
|
||||
QPointer<QWidget> first = new QWidget();
|
||||
sizer.set_widget(first);
|
||||
ASSERT_FALSE(first.isNull());
|
||||
EXPECT_EQ(first->parentWidget(), &sizer);
|
||||
|
||||
auto *second = new QWidget();
|
||||
sizer.set_widget(second);
|
||||
|
||||
// set_widget() deletes the previously installed widget
|
||||
EXPECT_TRUE(first.isNull());
|
||||
EXPECT_EQ(second->parentWidget(), &sizer);
|
||||
}
|
||||
|
||||
TEST(ViewerSizer, WideContainerScalesByHeight)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
sizer.resize(400, 300);
|
||||
sizer.set_widget(new QWidget());
|
||||
|
||||
QSignalSpy scale_spy(&sizer, &olive::ViewerSizer::request_scale);
|
||||
|
||||
// Square image in a 4:3 container: container is wider, so the matrix
|
||||
// compresses X by sequence_aspect / container_aspect = 1 / (4/3)
|
||||
sizer.set_child_size(100, 100);
|
||||
|
||||
ASSERT_GE(scale_spy.count(), 1);
|
||||
const QMatrix4x4 m = last_matrix(scale_spy);
|
||||
EXPECT_NEAR(m(0, 0), 0.75, 1e-9);
|
||||
EXPECT_NEAR(m(1, 1), 1.0, 1e-9);
|
||||
}
|
||||
|
||||
TEST(ViewerSizer, TallContainerScalesByWidth)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
sizer.resize(300, 300);
|
||||
sizer.set_widget(new QWidget());
|
||||
|
||||
QSignalSpy scale_spy(&sizer, &olive::ViewerSizer::request_scale);
|
||||
|
||||
// 2:1 image in a square container: container is taller, so the matrix
|
||||
// compresses Y by container_aspect / sequence_aspect = 1 / 2
|
||||
sizer.set_child_size(200, 100);
|
||||
|
||||
ASSERT_GE(scale_spy.count(), 1);
|
||||
const QMatrix4x4 m = last_matrix(scale_spy);
|
||||
EXPECT_NEAR(m(0, 0), 1.0, 1e-9);
|
||||
EXPECT_NEAR(m(1, 1), 0.5, 1e-9);
|
||||
}
|
||||
|
||||
TEST(ViewerSizer, PixelAspectRatioFactorsIntoScale)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
sizer.resize(300, 300);
|
||||
sizer.set_widget(new QWidget());
|
||||
sizer.set_child_size(100, 100);
|
||||
|
||||
QSignalSpy scale_spy(&sizer, &olive::ViewerSizer::request_scale);
|
||||
|
||||
// 2:1 pixel aspect doubles the sequence aspect -> 2:1 in a square container
|
||||
sizer.set_pixel_aspect_ratio(olive::Rational(2, 1));
|
||||
ASSERT_GE(scale_spy.count(), 1);
|
||||
QMatrix4x4 m = last_matrix(scale_spy);
|
||||
EXPECT_NEAR(m(0, 0), 1.0, 1e-9);
|
||||
EXPECT_NEAR(m(1, 1), 0.5, 1e-9);
|
||||
|
||||
// 1:2 pixel aspect halves it -> 1:2 image, container wider than image
|
||||
sizer.set_pixel_aspect_ratio(olive::Rational(1, 2));
|
||||
m = last_matrix(scale_spy);
|
||||
EXPECT_NEAR(m(0, 0), 0.5, 1e-9);
|
||||
EXPECT_NEAR(m(1, 1), 1.0, 1e-9);
|
||||
}
|
||||
|
||||
TEST(ViewerSizer, ExplicitZoomShowsScrollBars)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
sizer.resize(200, 200);
|
||||
sizer.set_widget(new QWidget());
|
||||
sizer.set_child_size(100, 100);
|
||||
sizer.show();
|
||||
ASSERT_TRUE(QTest::qWaitForWindowExposed(&sizer));
|
||||
|
||||
const auto bars = sizer_scrollbars(&sizer);
|
||||
ASSERT_EQ(bars.size(), 2);
|
||||
|
||||
QSignalSpy scale_spy(&sizer, &olive::ViewerSizer::request_scale);
|
||||
|
||||
// 100px child at 400% is 400px > 200px container, so both bars appear
|
||||
sizer.set_zoom(4.0);
|
||||
EXPECT_TRUE(bars.at(0)->isVisible());
|
||||
EXPECT_TRUE(bars.at(1)->isVisible());
|
||||
|
||||
// The zoom is folded into the requested scale on top of the fit scale
|
||||
ASSERT_GE(scale_spy.count(), 1);
|
||||
const QMatrix4x4 m = last_matrix(scale_spy);
|
||||
EXPECT_GT(m(0, 0), 1.0);
|
||||
EXPECT_GT(m(1, 1), 1.0);
|
||||
|
||||
// Back to fit: scrollbars disappear again
|
||||
sizer.set_zoom(-1);
|
||||
EXPECT_FALSE(bars.at(0)->isVisible());
|
||||
EXPECT_FALSE(bars.at(1)->isVisible());
|
||||
}
|
||||
|
||||
TEST(ViewerSizer, ScrollBarMovementEmitsTranslate)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
sizer.resize(200, 200);
|
||||
sizer.set_widget(new QWidget());
|
||||
sizer.set_child_size(100, 100);
|
||||
sizer.show();
|
||||
ASSERT_TRUE(QTest::qWaitForWindowExposed(&sizer));
|
||||
sizer.set_zoom(4.0);
|
||||
|
||||
const auto bars = sizer_scrollbars(&sizer);
|
||||
ASSERT_EQ(bars.size(), 2);
|
||||
QScrollBar *horiz = bars.at(0);
|
||||
ASSERT_TRUE(horiz->isVisible());
|
||||
ASSERT_GT(horiz->maximum(), 0);
|
||||
|
||||
QSignalSpy translate_spy(&sizer, &olive::ViewerSizer::request_translate);
|
||||
|
||||
// Scrolled to the far right the offset is negative. Go to the maximum
|
||||
// first so the subsequent setValue(0) is a real change (QScrollBar only
|
||||
// emits valueChanged, which drives scroll_bar_moved, on actual changes)
|
||||
horiz->setValue(horiz->maximum());
|
||||
ASSERT_GE(translate_spy.count(), 1);
|
||||
const float at_max = last_matrix(translate_spy)(0, 3);
|
||||
EXPECT_LT(at_max, 0.0f);
|
||||
|
||||
// Back at 0 the view centers on the left edge: positive X offset
|
||||
translate_spy.clear();
|
||||
horiz->setValue(0);
|
||||
ASSERT_GE(translate_spy.count(), 1);
|
||||
const float at_min = last_matrix(translate_spy)(0, 3);
|
||||
EXPECT_GT(at_min, 0.0f);
|
||||
}
|
||||
|
||||
TEST(ViewerSizer, HandDragMoveAdjustsVisibleScrollBars)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
sizer.resize(200, 200);
|
||||
sizer.set_widget(new QWidget());
|
||||
sizer.set_child_size(100, 100);
|
||||
sizer.show();
|
||||
ASSERT_TRUE(QTest::qWaitForWindowExposed(&sizer));
|
||||
sizer.set_zoom(4.0);
|
||||
|
||||
const auto bars = sizer_scrollbars(&sizer);
|
||||
ASSERT_EQ(bars.size(), 2);
|
||||
bars.at(0)->setValue(50);
|
||||
bars.at(1)->setValue(30);
|
||||
|
||||
// A drag of (+10, +5) pulls the content, decreasing both scroll values
|
||||
sizer.hand_drag_move(10, 5);
|
||||
EXPECT_EQ(bars.at(0)->value(), 40);
|
||||
EXPECT_EQ(bars.at(1)->value(), 25);
|
||||
}
|
||||
|
||||
TEST(ViewerSizer, AnchoredZoomOutToFitResetsScrollPositions)
|
||||
{
|
||||
olive::ViewerSizer sizer;
|
||||
sizer.resize(200, 200);
|
||||
sizer.set_widget(new QWidget());
|
||||
sizer.set_child_size(100, 100);
|
||||
sizer.show();
|
||||
ASSERT_TRUE(QTest::qWaitForWindowExposed(&sizer));
|
||||
|
||||
const auto bars = sizer_scrollbars(&sizer);
|
||||
ASSERT_EQ(bars.size(), 2);
|
||||
|
||||
sizer.set_zoom_anchored(4.0, 10.0, 10.0);
|
||||
ASSERT_TRUE(bars.at(0)->isVisible());
|
||||
|
||||
// A non-positive anchored zoom is the "fit" command: zoom clears and the
|
||||
// scroll offsets reset
|
||||
sizer.set_zoom_anchored(0.0, 0.0, 0.0);
|
||||
EXPECT_FALSE(bars.at(0)->isVisible());
|
||||
EXPECT_FALSE(bars.at(1)->isVisible());
|
||||
EXPECT_EQ(bars.at(0)->value(), 0);
|
||||
EXPECT_EQ(bars.at(1)->value(), 0);
|
||||
}
|
||||
|
||||
TEST(ViewerPlaybackTimer, ZeroSpeedKeepsStartTimestamp)
|
||||
{
|
||||
olive::ViewerPlaybackTimer timer;
|
||||
timer.start(42, 0, 1.0 / 30.0);
|
||||
|
||||
// No elapsed real time can move the timestamp when the speed is zero
|
||||
EXPECT_EQ(timer.get_timestamp_now(), 42);
|
||||
}
|
||||
|
||||
TEST(ViewerPlaybackTimer, HugeTimebaseFreezesFrameAdvance)
|
||||
{
|
||||
olive::ViewerPlaybackTimer timer;
|
||||
// 1000-second frames: any sub-second test run still rounds to zero frames
|
||||
timer.start(7, 3, 1000.0);
|
||||
EXPECT_EQ(timer.get_timestamp_now(), 7);
|
||||
}
|
||||
|
||||
TEST(ViewerPlaybackTimer, NegativeSpeedAlsoStartsAtTimestamp)
|
||||
{
|
||||
olive::ViewerPlaybackTimer timer;
|
||||
timer.start(100, -2, 1000.0);
|
||||
EXPECT_EQ(timer.get_timestamp_now(), 100);
|
||||
}
|
||||
|
||||
TEST(ViewerPlaybackTimer, RestartReplacesAnchor)
|
||||
{
|
||||
olive::ViewerPlaybackTimer timer;
|
||||
timer.start(1, 0, 1.0 / 30.0);
|
||||
EXPECT_EQ(timer.get_timestamp_now(), 1);
|
||||
|
||||
timer.start(500, 0, 1.0 / 30.0);
|
||||
EXPECT_EQ(timer.get_timestamp_now(), 500);
|
||||
}
|
||||
|
||||
TEST(ViewerPreventSleep, ToggleOnOffDoesNotCrash)
|
||||
{
|
||||
// No observable state is exposed; on macOS this creates/releases an IOPM
|
||||
// assertion, elsewhere it talks to the OS sleep-inhibit service. Toggling
|
||||
// twice exercises the released-assertion no-op path.
|
||||
olive::prevent_sleep(true);
|
||||
olive::prevent_sleep(false);
|
||||
olive::prevent_sleep(false);
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST(DisplayBuffer, NullHandlesAreSafeToWrapAndDestroy)
|
||||
{
|
||||
auto tex = olive::oak_make_shared_texture(nullptr);
|
||||
EXPECT_EQ(tex->handle, nullptr);
|
||||
EXPECT_EQ(tex->type, olive::OakSharedBuffer::k_texture);
|
||||
|
||||
auto frame = olive::oak_make_shared_frame(nullptr);
|
||||
EXPECT_EQ(frame->handle, nullptr);
|
||||
EXPECT_EQ(frame->type, olive::OakSharedBuffer::k_frame);
|
||||
|
||||
// Both facade free functions ignore nullptr, so destroying these is safe
|
||||
tex.reset();
|
||||
frame.reset();
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST(DisplayBuffer, SharedCopiesExtendHandleLifetime)
|
||||
{
|
||||
void *raw = oakengine_codec_frame_create();
|
||||
ASSERT_NE(raw, nullptr);
|
||||
|
||||
auto frame = olive::oak_make_shared_frame(raw);
|
||||
EXPECT_EQ(frame->handle, raw);
|
||||
EXPECT_EQ(frame.use_count(), 1);
|
||||
|
||||
{
|
||||
auto copy = frame;
|
||||
EXPECT_EQ(frame.use_count(), 2);
|
||||
EXPECT_EQ(copy->handle, raw);
|
||||
}
|
||||
|
||||
// The copy is gone but the original still holds the handle
|
||||
EXPECT_EQ(frame.use_count(), 1);
|
||||
}
|
||||
|
||||
TEST(DisplayBuffer, RoundTripsThroughVariant)
|
||||
{
|
||||
auto frame = olive::oak_make_shared_frame(nullptr);
|
||||
|
||||
QVariant v = QVariant::fromValue(frame);
|
||||
EXPECT_TRUE(v.isValid());
|
||||
|
||||
const olive::OakSharedBufferPtr out = v.value<olive::OakSharedBufferPtr>();
|
||||
EXPECT_EQ(out, frame);
|
||||
EXPECT_EQ(out.use_count(), frame.use_count());
|
||||
}
|
||||
|
||||
TEST(ViewerOutputUtils, NullViewerReturnsDefaults)
|
||||
{
|
||||
const oak::VideoParams vp = olive::viewer_output_video_params(nullptr);
|
||||
EXPECT_EQ(vp.width(), 0);
|
||||
EXPECT_EQ(vp.height(), 0);
|
||||
EXPECT_EQ(vp, olive::empty_video_params());
|
||||
|
||||
const olive::AudioParams ap = olive::viewer_output_audio_params(nullptr);
|
||||
EXPECT_EQ(ap.sample_rate(), 0);
|
||||
EXPECT_FALSE(ap.is_valid());
|
||||
|
||||
EXPECT_DOUBLE_EQ(olive::viewer_output_playhead(nullptr).to_double(), 0.0);
|
||||
EXPECT_DOUBLE_EQ(olive::viewer_output_length(nullptr).to_double(), 0.0);
|
||||
EXPECT_DOUBLE_EQ(olive::viewer_output_video_length(nullptr).to_double(), 0.0);
|
||||
EXPECT_DOUBLE_EQ(olive::viewer_output_audio_length(nullptr).to_double(), 0.0);
|
||||
EXPECT_DOUBLE_EQ(olive::sequence_timebase(nullptr).to_double(), 0.0);
|
||||
|
||||
EXPECT_FALSE(olive::viewer_output_is_sequence(nullptr));
|
||||
EXPECT_FALSE(olive::viewer_output_is_footage(nullptr));
|
||||
EXPECT_FALSE(olive::viewer_output_node_type_is(nullptr, "anything"));
|
||||
}
|
||||
|
||||
TEST(ViewerOutputUtils, VideoParamsRoundTripThroughFacade)
|
||||
{
|
||||
olive::ViewerOutput viewer;
|
||||
viewer.set_video_params(olive::VideoParams(320, 240, olive::Rational(1, 25),
|
||||
olive::PixelFormat::u8,
|
||||
olive::VideoParams::k_rgba_channel_count));
|
||||
|
||||
const oak::VideoParams vp = olive::viewer_output_video_params(&viewer);
|
||||
EXPECT_EQ(vp.width(), 320);
|
||||
EXPECT_EQ(vp.height(), 240);
|
||||
EXPECT_TRUE(vp.is_valid());
|
||||
|
||||
// sequence_timebase is the frame duration as a rational
|
||||
const olive::Rational tb = olive::sequence_timebase(&viewer);
|
||||
EXPECT_EQ(tb.numerator(), 1);
|
||||
EXPECT_EQ(tb.denominator(), 25);
|
||||
|
||||
EXPECT_EQ(vp.time_base().numerator(), 1);
|
||||
EXPECT_EQ(vp.time_base().denominator(), 25);
|
||||
}
|
||||
|
||||
TEST(ViewerOutputUtils, PlayheadRoundTripsAndLengthDefaultsToZero)
|
||||
{
|
||||
olive::ViewerOutput viewer;
|
||||
|
||||
viewer.set_playhead(olive::Rational(3, 2));
|
||||
const olive::Rational ph = olive::viewer_output_playhead(&viewer);
|
||||
EXPECT_EQ(ph.numerator(), 3);
|
||||
EXPECT_EQ(ph.denominator(), 2);
|
||||
|
||||
// A fresh viewer has no content, so all lengths are zero
|
||||
EXPECT_DOUBLE_EQ(olive::viewer_output_length(&viewer).to_double(), 0.0);
|
||||
EXPECT_DOUBLE_EQ(olive::viewer_output_video_length(&viewer).to_double(), 0.0);
|
||||
EXPECT_DOUBLE_EQ(olive::viewer_output_audio_length(&viewer).to_double(), 0.0);
|
||||
}
|
||||
|
||||
TEST(ViewerOutputUtils, AudioParamsRoundTripThroughFacade)
|
||||
{
|
||||
olive::ViewerOutput viewer;
|
||||
const uint64_t stereo_mask = 0x3; // front-left | front-right
|
||||
viewer.set_audio_params(
|
||||
olive::AudioParams(48000, stereo_mask, olive::SampleFormat::s16));
|
||||
|
||||
const olive::AudioParams ap = olive::viewer_output_audio_params(&viewer);
|
||||
EXPECT_TRUE(ap.is_valid());
|
||||
EXPECT_EQ(ap.sample_rate(), 48000);
|
||||
EXPECT_EQ(ap.channel_layout(), stereo_mask);
|
||||
EXPECT_EQ(olive::SampleFormat::Format(ap.format()), olive::SampleFormat::s16);
|
||||
}
|
||||
|
||||
TEST(ViewerOutputUtils, TypeProbesMatchNodeIds)
|
||||
{
|
||||
olive::Sequence sequence;
|
||||
EXPECT_TRUE(olive::viewer_output_is_sequence(&sequence));
|
||||
EXPECT_FALSE(olive::viewer_output_is_footage(&sequence));
|
||||
EXPECT_TRUE(olive::viewer_output_node_type_is(
|
||||
&sequence, "org.olivevideoeditor.Olive.sequence"));
|
||||
EXPECT_FALSE(
|
||||
olive::viewer_output_node_type_is(&sequence, "org.oak.test.nonexistent"));
|
||||
|
||||
olive::Footage footage;
|
||||
EXPECT_TRUE(olive::viewer_output_is_footage(&footage));
|
||||
EXPECT_FALSE(olive::viewer_output_is_sequence(&footage));
|
||||
|
||||
// A plain ViewerOutput is neither a Sequence nor Footage
|
||||
olive::ViewerOutput viewer;
|
||||
EXPECT_FALSE(olive::viewer_output_is_sequence(&viewer));
|
||||
EXPECT_FALSE(olive::viewer_output_is_footage(&viewer));
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, SettersUpdateButtonsWithoutEmitting)
|
||||
{
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
const auto buttons = toolbar_buttons(&toolbar);
|
||||
ASSERT_EQ(buttons.size(), 11);
|
||||
|
||||
QSignalSpy underline_spy(&toolbar,
|
||||
&olive::ViewerTextEditorToolBar::underline_changed);
|
||||
QSignalSpy strike_spy(
|
||||
&toolbar, &olive::ViewerTextEditorToolBar::strikethrough_changed);
|
||||
QSignalSpy caps_spy(&toolbar,
|
||||
&olive::ViewerTextEditorToolBar::small_caps_changed);
|
||||
|
||||
toolbar.set_underline(true);
|
||||
EXPECT_TRUE(buttons.at(k_underline)->isChecked());
|
||||
toolbar.set_strikethrough(true);
|
||||
EXPECT_TRUE(buttons.at(k_strikethrough)->isChecked());
|
||||
toolbar.set_small_caps(true);
|
||||
EXPECT_TRUE(buttons.at(k_small_caps)->isChecked());
|
||||
|
||||
// Programmatic sync from the editor must not bounce back as user edits
|
||||
EXPECT_EQ(underline_spy.count(), 0);
|
||||
EXPECT_EQ(strike_spy.count(), 0);
|
||||
EXPECT_EQ(caps_spy.count(), 0);
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, SetAlignmentChecksMatchingButtonOnly)
|
||||
{
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
const auto buttons = toolbar_buttons(&toolbar);
|
||||
ASSERT_EQ(buttons.size(), 11);
|
||||
|
||||
toolbar.set_alignment(Qt::AlignHCenter);
|
||||
EXPECT_FALSE(buttons.at(k_align_left)->isChecked());
|
||||
EXPECT_TRUE(buttons.at(k_align_center)->isChecked());
|
||||
EXPECT_FALSE(buttons.at(k_align_right)->isChecked());
|
||||
EXPECT_FALSE(buttons.at(k_align_justify)->isChecked());
|
||||
|
||||
toolbar.set_alignment(Qt::AlignRight);
|
||||
EXPECT_FALSE(buttons.at(k_align_left)->isChecked());
|
||||
EXPECT_FALSE(buttons.at(k_align_center)->isChecked());
|
||||
EXPECT_TRUE(buttons.at(k_align_right)->isChecked());
|
||||
EXPECT_FALSE(buttons.at(k_align_justify)->isChecked());
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, SetVerticalAlignmentChecksMatchingButtonOnly)
|
||||
{
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
const auto buttons = toolbar_buttons(&toolbar);
|
||||
ASSERT_EQ(buttons.size(), 11);
|
||||
|
||||
toolbar.set_vertical_alignment(Qt::AlignVCenter);
|
||||
EXPECT_FALSE(buttons.at(k_align_top)->isChecked());
|
||||
EXPECT_TRUE(buttons.at(k_align_middle)->isChecked());
|
||||
EXPECT_FALSE(buttons.at(k_align_bottom)->isChecked());
|
||||
|
||||
toolbar.set_vertical_alignment(Qt::AlignBottom);
|
||||
EXPECT_FALSE(buttons.at(k_align_top)->isChecked());
|
||||
EXPECT_FALSE(buttons.at(k_align_middle)->isChecked());
|
||||
EXPECT_TRUE(buttons.at(k_align_bottom)->isChecked());
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, AlignmentButtonsEmitTheirAlignment)
|
||||
{
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
const auto buttons = toolbar_buttons(&toolbar);
|
||||
ASSERT_EQ(buttons.size(), 11);
|
||||
|
||||
// Lambda capture instead of QSignalSpy: Qt::Alignment is not guaranteed
|
||||
// to be a registered metatype for the spy on every platform
|
||||
QVector<Qt::Alignment> h_received;
|
||||
QObject::connect(&toolbar,
|
||||
&olive::ViewerTextEditorToolBar::alignment_changed,
|
||||
[&h_received](Qt::Alignment a) { h_received.append(a); });
|
||||
QVector<Qt::Alignment> v_received;
|
||||
QObject::connect(
|
||||
&toolbar, &olive::ViewerTextEditorToolBar::vertical_alignment_changed,
|
||||
[&v_received](Qt::Alignment a) { v_received.append(a); });
|
||||
|
||||
buttons.at(k_align_center)->click();
|
||||
ASSERT_EQ(h_received.size(), 1);
|
||||
EXPECT_EQ(h_received.first(), Qt::AlignHCenter);
|
||||
|
||||
buttons.at(k_align_justify)->click();
|
||||
ASSERT_EQ(h_received.size(), 2);
|
||||
EXPECT_EQ(h_received.last(), Qt::AlignJustify);
|
||||
|
||||
buttons.at(k_align_middle)->click();
|
||||
ASSERT_EQ(v_received.size(), 1);
|
||||
EXPECT_EQ(v_received.first(), Qt::AlignVCenter);
|
||||
|
||||
buttons.at(k_align_bottom)->click();
|
||||
ASSERT_EQ(v_received.size(), 2);
|
||||
EXPECT_EQ(v_received.last(), Qt::AlignBottom);
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, ToggleButtonsEmitTheirState)
|
||||
{
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
const auto buttons = toolbar_buttons(&toolbar);
|
||||
ASSERT_EQ(buttons.size(), 11);
|
||||
|
||||
QSignalSpy underline_spy(&toolbar,
|
||||
&olive::ViewerTextEditorToolBar::underline_changed);
|
||||
QSignalSpy strike_spy(
|
||||
&toolbar, &olive::ViewerTextEditorToolBar::strikethrough_changed);
|
||||
QSignalSpy caps_spy(&toolbar,
|
||||
&olive::ViewerTextEditorToolBar::small_caps_changed);
|
||||
|
||||
buttons.at(k_underline)->click();
|
||||
ASSERT_EQ(underline_spy.count(), 1);
|
||||
EXPECT_TRUE(underline_spy.first().first().toBool());
|
||||
|
||||
buttons.at(k_strikethrough)->click();
|
||||
ASSERT_EQ(strike_spy.count(), 1);
|
||||
EXPECT_TRUE(strike_spy.first().first().toBool());
|
||||
|
||||
buttons.at(k_small_caps)->click();
|
||||
ASSERT_EQ(caps_spy.count(), 1);
|
||||
EXPECT_TRUE(caps_spy.first().first().toBool());
|
||||
|
||||
// Unchecking emits false
|
||||
buttons.at(k_underline)->click();
|
||||
ASSERT_EQ(underline_spy.count(), 2);
|
||||
EXPECT_FALSE(underline_spy.last().first().toBool());
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, SetColorPaintsColorButton)
|
||||
{
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
const auto buttons = toolbar_buttons(&toolbar);
|
||||
ASSERT_EQ(buttons.size(), 11);
|
||||
|
||||
const QColor c(QStringLiteral("#FF0000"));
|
||||
toolbar.set_color(c);
|
||||
|
||||
QPushButton *color_btn = buttons.at(k_color);
|
||||
EXPECT_EQ(color_btn->property("color").value<QColor>(), c);
|
||||
EXPECT_TRUE(color_btn->styleSheet().contains(c.name()));
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, SetFontFamilyUpdatesComboWithoutEmitting)
|
||||
{
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
|
||||
QSignalSpy family_spy(&toolbar,
|
||||
&olive::ViewerTextEditorToolBar::family_changed);
|
||||
|
||||
// Pick a family from the combo's own model: QFontDatabase also lists
|
||||
// system fonts the QFontComboBox filters out (e.g. ".Apple Color Emoji"
|
||||
// on macOS), which setCurrentFont can only approximate
|
||||
auto *combo = toolbar.findChild<QFontComboBox *>();
|
||||
ASSERT_NE(combo, nullptr);
|
||||
ASSERT_GT(combo->count(), 1);
|
||||
const QString family = combo->itemText(combo->count() / 2);
|
||||
ASSERT_FALSE(family.isEmpty());
|
||||
|
||||
toolbar.set_font_family(family);
|
||||
|
||||
EXPECT_EQ(toolbar.get_font_family(), family);
|
||||
// set_font_family blocks the combo's signals while syncing
|
||||
EXPECT_EQ(family_spy.count(), 0);
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, FirstPaintEmittedOnce)
|
||||
{
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
QSignalSpy spy(&toolbar, &olive::ViewerTextEditorToolBar::first_paint);
|
||||
|
||||
// Deliver paint events directly: whether the offscreen QPA paints on
|
||||
// expose is platform-dependent, but QWidget::event always dispatches a
|
||||
// QPaintEvent to paintEvent
|
||||
QPaintEvent first(toolbar.rect());
|
||||
QApplication::sendEvent(&toolbar, &first);
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
|
||||
// Later repaints don't re-emit
|
||||
QPaintEvent second(toolbar.rect());
|
||||
QApplication::sendEvent(&toolbar, &second);
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, CloseEventIsIgnored)
|
||||
{
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
|
||||
QCloseEvent ev;
|
||||
QApplication::sendEvent(&toolbar, &ev);
|
||||
|
||||
// The toolbar is a floating child of the viewer; closing must never hide it
|
||||
EXPECT_FALSE(ev.isAccepted());
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditorToolBar, LeftDragMovesToolbarWithinParent)
|
||||
{
|
||||
QWidget parent;
|
||||
parent.resize(600, 400);
|
||||
|
||||
olive::ViewerTextEditorToolBar toolbar(&parent);
|
||||
toolbar.move(100, 100);
|
||||
parent.show();
|
||||
ASSERT_TRUE(QTest::qWaitForWindowExposed(&parent));
|
||||
|
||||
QTest::mousePress(&toolbar, Qt::LeftButton, Qt::NoModifier, QPoint(10, 10));
|
||||
|
||||
// QTest::mouseMove can't report held buttons, so deliver the move manually
|
||||
QMouseEvent move(QEvent::MouseMove, QPointF(30, 25), QPointF(130, 125),
|
||||
Qt::NoButton, Qt::LeftButton, Qt::NoModifier);
|
||||
QApplication::sendEvent(&toolbar, &move);
|
||||
|
||||
EXPECT_EQ(toolbar.pos(), QPoint(120, 115));
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditor, CursorWidthScalesWithInverseZoom)
|
||||
{
|
||||
olive::ViewerTextEditor at_full(1.0);
|
||||
EXPECT_EQ(at_full.cursorWidth(), 1);
|
||||
|
||||
olive::ViewerTextEditor zoomed_out(0.5);
|
||||
EXPECT_EQ(zoomed_out.cursorWidth(), 2);
|
||||
|
||||
olive::ViewerTextEditor far_out(0.25);
|
||||
EXPECT_EQ(far_out.cursorWidth(), 4);
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditor, ConstructionDefaults)
|
||||
{
|
||||
olive::ViewerTextEditor editor(1.0);
|
||||
|
||||
// Rich text paste is disabled; colors default to white on transparent
|
||||
EXPECT_FALSE(editor.acceptRichText());
|
||||
EXPECT_EQ(editor.palette().color(QPalette::Text), QColor(Qt::white));
|
||||
|
||||
// Scrolling is handled by the viewer gizmo, not the text edit
|
||||
EXPECT_EQ(editor.horizontalScrollBarPolicy(), Qt::ScrollBarAlwaysOff);
|
||||
EXPECT_EQ(editor.verticalScrollBarPolicy(), Qt::ScrollBarAlwaysOff);
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditor, ConnectToolBarSyncsCurrentFormat)
|
||||
{
|
||||
olive::ViewerTextEditor editor(1.0);
|
||||
|
||||
// Set a distinctive format before connecting; connect_tool_bar pushes the
|
||||
// editor's current format into the toolbar exactly once
|
||||
editor.setFontUnderline(true);
|
||||
editor.setAlignment(Qt::AlignRight);
|
||||
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
editor.connect_tool_bar(&toolbar);
|
||||
|
||||
const auto buttons = toolbar_buttons(&toolbar);
|
||||
ASSERT_EQ(buttons.size(), 11);
|
||||
EXPECT_TRUE(buttons.at(k_underline)->isChecked());
|
||||
EXPECT_TRUE(buttons.at(k_align_right)->isChecked());
|
||||
EXPECT_FALSE(buttons.at(k_align_left)->isChecked());
|
||||
EXPECT_FALSE(buttons.at(k_align_center)->isChecked());
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditor, ToolBarTogglesApplyToEditorFormat)
|
||||
{
|
||||
olive::ViewerTextEditor editor(1.0);
|
||||
olive::ViewerTextEditorToolBar toolbar;
|
||||
editor.connect_tool_bar(&toolbar);
|
||||
|
||||
const auto buttons = toolbar_buttons(&toolbar);
|
||||
ASSERT_EQ(buttons.size(), 11);
|
||||
|
||||
buttons.at(k_underline)->click();
|
||||
EXPECT_TRUE(editor.currentCharFormat().fontUnderline());
|
||||
|
||||
buttons.at(k_strikethrough)->click();
|
||||
EXPECT_TRUE(editor.currentCharFormat().fontStrikeOut());
|
||||
|
||||
buttons.at(k_small_caps)->click();
|
||||
EXPECT_EQ(editor.currentCharFormat().fontCapitalization(),
|
||||
QFont::SmallCaps);
|
||||
|
||||
buttons.at(k_align_center)->click();
|
||||
EXPECT_EQ(editor.alignment(), Qt::AlignHCenter);
|
||||
|
||||
// The toolbar follows the editor back (same alignment echo)
|
||||
EXPECT_TRUE(buttons.at(k_align_center)->isChecked());
|
||||
EXPECT_FALSE(buttons.at(k_align_left)->isChecked());
|
||||
}
|
||||
|
||||
TEST(ViewerTextEditor, PaintNeverRendersTextByDesign)
|
||||
{
|
||||
olive::ViewerTextEditor editor(1.0);
|
||||
editor.resize(200, 80);
|
||||
editor.setPlainText(QStringLiteral("Hello"));
|
||||
|
||||
// document_changed() swaps in a clone whose text is fully transparent (the
|
||||
// HACK in viewertexteditor.cpp: the gizmo underneath renders the text, the
|
||||
// editor must not render it a second time). Verify that contract: nothing
|
||||
// is painted, with or without a selection, at any vertical alignment.
|
||||
QTextCursor c = editor.textCursor();
|
||||
c.select(QTextCursor::Document);
|
||||
editor.setTextCursor(c);
|
||||
|
||||
for (const Qt::Alignment valign :
|
||||
{ Qt::AlignTop, Qt::AlignVCenter, Qt::AlignBottom }) {
|
||||
QImage img(200, 80, QImage::Format_ARGB32);
|
||||
img.fill(Qt::transparent);
|
||||
{
|
||||
QPainter p(&img);
|
||||
editor.paint(&p, valign);
|
||||
EXPECT_TRUE(p.isActive());
|
||||
}
|
||||
for (int y = 0; y < img.height(); y++) {
|
||||
for (int x = 0; x < img.width(); x++) {
|
||||
EXPECT_EQ(qAlpha(img.pixel(x, y)), 0)
|
||||
<< "painted pixel at " << x << "," << y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user