#include #include #include #include #include #include #include #include #include #include #include #include #include #include "config/config.h" #include "core.h" #include "node/globals.h" #include "node/math/math/math.h" #include "node/project.h" #include "node/traverser.h" #include "common/colorcodingapp.h" #include "ui/icons/icons.h" #include "widget/bezier/bezierwidget.h" #include "widget/clickablelabel/clickablelabel.h" #include "widget/collapsebutton/collapsebutton.h" #include "widget/colorbutton/colorbutton.h" #include "widget/colorlabelmenu/colorlabelmenu.h" #include "widget/colorwheel/colorgradientwidget.h" #include "widget/colorwheel/colorpreviewbox.h" #include "widget/colorwheel/colorspacechooser.h" #include "widget/colorwheel/colorswatchchooser.h" #include "widget/colorwheel/colorvalueswidget.h" #include "widget/colorwheel/colorwheelwidget.h" #include "widget/filefield/filefield.h" #include "widget/focusablelineedit/focusablelineedit.h" #include "widget/handmovableview/handmovableview.h" #include "widget/manageddisplay/colorprocessorhandle.h" #include "widget/menu/menu.h" #include "widget/nodevaluetree/nodevaluetree.h" #include "widget/path/pathwidget.h" #include "widget/pixelsampler/pixelsampler.h" #include "widget/resizablescrollbar/resizablescrollbar.h" #include "widget/slider/stringslider.h" #include "widget/toolbar/toolbar.h" #include "widget/toolbar/toolbarbutton.h" namespace { // Widgets that connect to Core::instance() at construction require the // application singleton, but not a MainWindow void ensure_core() { if (!olive::Core::instance()) { new olive::Core(); // intentionally leaked } } // Exposes the protected slider rect calculation so tests can aim mouse events // at the resize handles deterministically class ProbeScrollBar : public olive::ResizableScrollBar { public: explicit ProbeScrollBar(Qt::Orientation orientation) : olive::ResizableScrollBar(orientation) { } QRect slider_rect() { QStyleOptionSlider opt; initStyleOption(&opt); return style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); } }; // Exposes the protected hand-drag state machine entry points class ProbeHandView : public olive::HandMovableView { public: bool pub_hand_press(QMouseEvent *e) { return hand_press(e); } bool pub_hand_move(QMouseEvent *e) { return hand_move(e); } bool pub_hand_release(QMouseEvent *e) { return hand_release(e); } void pub_set_default_drag_mode(DragMode mode) { set_default_drag_mode(mode); } const DragMode &pub_get_default_drag_mode() const { return get_default_drag_mode(); } }; // ToolbarButton has no Q_OBJECT, so findChildren doesn't // compile; every button in a Toolbar is a ToolbarButton, so fetch QPushButtons // and static_cast QList toolbar_buttons(olive::Toolbar *bar) { QList out; for (QPushButton *b : bar->findChildren()) { out.append(static_cast(b)); } return out; } // Minimal node that pushes a float and an integer row so NodeValueTree has // more than one value to choose from class TwoValueNode : public olive::Node { public: TwoValueNode() = default; NODE_DEFAULT_FUNCTIONS(TwoValueNode) virtual QString name() const override { return QStringLiteral("Test Two Value"); } virtual QString id() const override { return QStringLiteral("org.oak.test.twovalue"); } virtual QVector category() const override { return { k_category_math }; } virtual void value(const olive::NodeValueRow &value, const olive::NodeGlobals &globals, olive::NodeValueTable *table) const override { Q_UNUSED(value) Q_UNUSED(globals) table->push(olive::NodeValue::k_float, QVariant(1.5), this); table->push(olive::NodeValue::k_int, QVariant(2), this); } }; } // namespace TEST(WidgetMenu, InsertAlphabeticallySortsActions) { olive::Menu menu; menu.insert_alphabetically(QStringLiteral("Charlie")); menu.insert_alphabetically(QStringLiteral("Alpha")); menu.insert_alphabetically(QStringLiteral("Bravo")); ASSERT_EQ(menu.actions().size(), 3); EXPECT_EQ(menu.actions().at(0)->text(), QStringLiteral("Alpha")); EXPECT_EQ(menu.actions().at(1)->text(), QStringLiteral("Bravo")); EXPECT_EQ(menu.actions().at(2)->text(), QStringLiteral("Charlie")); // Submenus slot in by their title too auto *sub = new olive::Menu(&menu); sub->setTitle(QStringLiteral("Aardvark")); menu.insert_alphabetically(sub); ASSERT_EQ(menu.actions().size(), 4); EXPECT_EQ(menu.actions().at(0)->text(), QStringLiteral("Aardvark")); EXPECT_EQ(menu.actions().at(0)->menu(), sub); } TEST(WidgetMenu, AddActionWithDataChecksMatchingValue) { olive::Menu menu; QAction *match = menu.add_action_with_data(QStringLiteral("Five"), 5, 5); QAction *other = menu.add_action_with_data(QStringLiteral("Six"), 6, 5); EXPECT_TRUE(match->isCheckable()); EXPECT_TRUE(match->isChecked()); EXPECT_EQ(match->data().toInt(), 5); EXPECT_TRUE(other->isCheckable()); EXPECT_FALSE(other->isChecked()); EXPECT_EQ(other->data().toInt(), 6); } TEST(WidgetMenu, ConformItemStoresIdAndKeyDefault) { QAction a; olive::Menu::conform_item(&a, QStringLiteral("myaction"), QKeySequence(QStringLiteral("Ctrl+K"))); EXPECT_EQ(a.property("id").toString(), QStringLiteral("myaction")); EXPECT_EQ(a.shortcut(), QKeySequence(QStringLiteral("Ctrl+K"))); EXPECT_EQ(a.property("keydefault").value(), QKeySequence(QStringLiteral("Ctrl+K"))); EXPECT_EQ(a.shortcutContext(), Qt::ApplicationShortcut); // Without a key, no keydefault is stored QAction b; olive::Menu::conform_item(&b, QStringLiteral("plain")); EXPECT_EQ(b.property("id").toString(), QStringLiteral("plain")); EXPECT_FALSE(b.property("keydefault").isValid()); } TEST(WidgetColorLabelMenu, ItemsCarryIndexAndEmitSelection) { olive::ColorLabelMenu menu; 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::AppColorCoding::get_color_name(i)); EXPECT_EQ(a->property("id").toString(), QStringLiteral("colorlabel%1").arg(i)); } QSignalSpy spy(&menu, &olive::ColorLabelMenu::color_selected); menu.actions().at(2)->trigger(); ASSERT_EQ(spy.count(), 1); EXPECT_EQ(spy.first().first().toInt(), 2); } TEST(WidgetFileField, SetFilenameReadbackDoesNotSignal) { olive::FileField field; QSignalSpy spy(&field, &olive::FileField::filename_changed); field.set_filename(QStringLiteral("/some/file.txt")); EXPECT_EQ(field.get_filename(), QStringLiteral("/some/file.txt")); // Programmatic changes don't count as user edits EXPECT_EQ(spy.count(), 0); } TEST(WidgetFileField, TypingEmitsFilenameChanged) { olive::FileField field; QLineEdit *edit = field.findChild(); ASSERT_NE(edit, nullptr); QSignalSpy spy(&field, &olive::FileField::filename_changed); QTest::keyClicks(edit, QStringLiteral("a")); ASSERT_EQ(spy.count(), 1); EXPECT_EQ(spy.first().first().toString(), QStringLiteral("a")); EXPECT_EQ(field.get_filename(), QStringLiteral("a")); } TEST(WidgetFileField, InvalidPathMarkedRed) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString existing = QDir(dir.path()).filePath(QStringLiteral("f.txt")); ASSERT_TRUE(QFile(existing).open(QIODevice::WriteOnly)); olive::FileField field; QLineEdit *edit = field.findChild(); ASSERT_NE(edit, nullptr); edit->setText(existing); EXPECT_TRUE(edit->styleSheet().isEmpty()); edit->setText(QStringLiteral("/definitely/not/here.xyz")); EXPECT_TRUE(edit->styleSheet().contains(QStringLiteral("red"))); // An empty field is neutral again edit->setText(QString()); EXPECT_TRUE(edit->styleSheet().isEmpty()); } TEST(WidgetPathWidget, ReadbackAndDirectoryValidation) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); olive::PathWidget w(dir.path()); EXPECT_EQ(w.text(), dir.path()); QLineEdit *edit = w.findChild(); ASSERT_NE(edit, nullptr); edit->setText(QStringLiteral("/definitely/not/a/dir")); EXPECT_TRUE(edit->styleSheet().contains(QStringLiteral("red"))); edit->setText(dir.path()); EXPECT_TRUE(edit->styleSheet().isEmpty()); } TEST(WidgetCollapseButton, ToggleSwitchesIcon) { // Icons are normally loaded by the app style; pull them in explicitly olive::icon::load_all(QStringLiteral(":/style/olive-dark")); olive::CollapseButton btn; EXPECT_TRUE(btn.isCheckable()); EXPECT_TRUE(btn.isChecked()); EXPECT_FALSE(btn.icon().isNull()); const qint64 expanded_key = btn.icon().cacheKey(); btn.setChecked(false); EXPECT_FALSE(btn.icon().isNull()); EXPECT_NE(btn.icon().cacheKey(), expanded_key); } TEST(WidgetClickableLabel, ClickAndDoubleClickSignals) { olive::ClickableLabel label(QStringLiteral("Click me")); label.resize(120, 40); label.show(); EXPECT_TRUE(QTest::qWaitForWindowExposed(&label)); // mouseReleaseEvent requires the cursor to be over the widget; if the // offscreen platform can't track the cursor there's nothing to assert QTest::mouseMove(&label, QPoint(10, 10)); if (!label.underMouse()) { GTEST_SKIP() << "Platform does not track cursor position"; } QSignalSpy clicked_spy(&label, &olive::ClickableLabel::mouse_clicked); QSignalSpy dbl_spy(&label, &olive::ClickableLabel::mouse_double_clicked); QTest::mouseClick(&label, Qt::LeftButton); EXPECT_EQ(clicked_spy.count(), 1); EXPECT_EQ(dbl_spy.count(), 0); QTest::mouseDClick(&label, Qt::LeftButton); EXPECT_GE(dbl_spy.count(), 1); } TEST(WidgetFocusableLineEdit, EnterConfirmsEscapeCancels) { olive::FocusableLineEdit edit; QSignalSpy confirmed(&edit, &olive::FocusableLineEdit::confirmed); QSignalSpy cancelled(&edit, &olive::FocusableLineEdit::cancelled); QTest::keyClick(&edit, Qt::Key_Return); EXPECT_EQ(confirmed.count(), 1); EXPECT_EQ(cancelled.count(), 0); QTest::keyClick(&edit, Qt::Key_Enter); EXPECT_EQ(confirmed.count(), 2); QTest::keyClick(&edit, Qt::Key_Escape); EXPECT_EQ(cancelled.count(), 1); // Ordinary keys pass through to QLineEdit QTest::keyClick(&edit, Qt::Key_A); EXPECT_EQ(edit.text(), QStringLiteral("a")); EXPECT_EQ(confirmed.count(), 2); EXPECT_EQ(cancelled.count(), 1); } TEST(WidgetPixelSampler, LabelShowsColorComponents) { olive::PixelSamplerWidget w; QLabel *label = w.findChild(); ASSERT_NE(label, nullptr); w.set_values(olive::Color(1.0, 0.5, 0.0, 1.0)); const QString text = label->text(); EXPECT_TRUE(text.contains(QStringLiteral("R: 1 (255)"))); EXPECT_TRUE(text.contains(QStringLiteral("G: 0.5 (127)"))); EXPECT_TRUE(text.contains(QStringLiteral("B: 0 (0)"))); EXPECT_TRUE(text.contains(QStringLiteral("A: 1 (255)"))); } TEST(WidgetPixelSampler, ManagedSamplerForwardsValues) { olive::ManagedPixelSamplerWidget w; const auto samplers = w.findChildren(); ASSERT_EQ(samplers.size(), 2); // First child is the display view, second the reference view w.set_values(olive::Color(1.0, 0.0, 0.0, 1.0), olive::Color(0.0, 1.0, 0.0, 1.0)); EXPECT_TRUE(samplers.at(0)->findChild()->text().contains( QStringLiteral("G: 1 (255)"))); EXPECT_TRUE(samplers.at(1)->findChild()->text().contains( QStringLiteral("R: 1 (255)"))); } TEST(WidgetBezierWidget, ValueRoundTripsThroughSliders) { olive::BezierWidget w; olive::Bezier b(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); w.set_value(b); olive::Bezier out = w.get_value(); EXPECT_DOUBLE_EQ(out.x(), 1.0); EXPECT_DOUBLE_EQ(out.y(), 2.0); EXPECT_DOUBLE_EQ(out.cp1_x(), 3.0); EXPECT_DOUBLE_EQ(out.cp1_y(), 4.0); EXPECT_DOUBLE_EQ(out.cp2_x(), 5.0); EXPECT_DOUBLE_EQ(out.cp2_y(), 6.0); EXPECT_DOUBLE_EQ(w.x_slider()->get_value(), 1.0); EXPECT_DOUBLE_EQ(w.y_slider()->get_value(), 2.0); EXPECT_DOUBLE_EQ(w.cp1_x_slider()->get_value(), 3.0); EXPECT_DOUBLE_EQ(w.cp2_y_slider()->get_value(), 6.0); } TEST(WidgetResizableScrollBar, DefaultsMatchInit) { olive::ResizableScrollBar bar; EXPECT_EQ(bar.singleStep(), 20); EXPECT_EQ(bar.maximum(), 0); EXPECT_TRUE(bar.hasMouseTracking()); olive::ResizableScrollBar hbar(Qt::Horizontal); EXPECT_EQ(hbar.orientation(), Qt::Horizontal); } TEST(WidgetResizableScrollBar, HandleDragEmitsResizeSignals) { ProbeScrollBar bar(Qt::Horizontal); bar.resize(300, 20); bar.setRange(0, 1000); bar.setPageStep(200); bar.setValue(500); bar.show(); EXPECT_TRUE(QTest::qWaitForWindowExposed(&bar)); const QRect slider = bar.slider_rect(); ASSERT_GT(slider.width(), 30) << "slider too small to hold two handles and a middle"; QSignalSpy began(&bar, &olive::ResizableScrollBar::resize_began); QSignalSpy moved(&bar, &olive::ResizableScrollBar::resize_moved); QSignalSpy ended(&bar, &olive::ResizableScrollBar::resize_ended); // Hover the top (left) handle, then drag it 25px to the right const QPoint handle_pos(slider.left() + 1, slider.center().y()); QTest::mouseMove(&bar, handle_pos); QTest::mousePress(&bar, Qt::LeftButton, Qt::NoModifier, handle_pos); ASSERT_EQ(began.count(), 1); EXPECT_EQ(began.first().at(0).toInt(), slider.width()); EXPECT_TRUE(began.first().at(1).toBool()); QTest::mouseMove(&bar, handle_pos + QPoint(25, 0)); ASSERT_EQ(moved.count(), 1); EXPECT_EQ(moved.first().first().toInt(), 25); QTest::mouseRelease(&bar, Qt::LeftButton, Qt::NoModifier, handle_pos + QPoint(25, 0)); EXPECT_EQ(ended.count(), 1); // The bottom (right) handle reports top_handle=false const QPoint bottom_handle(slider.right() - 1, slider.center().y()); QTest::mouseMove(&bar, bottom_handle); QTest::mousePress(&bar, Qt::LeftButton, Qt::NoModifier, bottom_handle); ASSERT_EQ(began.count(), 2); EXPECT_FALSE(began.at(1).at(1).toBool()); QTest::mouseRelease(&bar, Qt::LeftButton, Qt::NoModifier, bottom_handle); EXPECT_EQ(ended.count(), 2); // Pressing the middle of the slider behaves like a normal scrollbar const QPoint middle = slider.center(); QTest::mouseMove(&bar, middle); QTest::mousePress(&bar, Qt::LeftButton, Qt::NoModifier, middle); EXPECT_EQ(began.count(), 2); QTest::mouseRelease(&bar, Qt::LeftButton, Qt::NoModifier, middle); EXPECT_EQ(ended.count(), 2); } TEST(WidgetHandMovableView, ToolSwitchChangesDragMode) { ensure_core(); ProbeHandView view; view.pub_set_default_drag_mode(QGraphicsView::RubberBandDrag); EXPECT_EQ(view.dragMode(), QGraphicsView::RubberBandDrag); EXPECT_EQ(view.pub_get_default_drag_mode(), QGraphicsView::RubberBandDrag); olive::Core::instance()->set_tool(olive::Tool::k_hand); EXPECT_EQ(view.dragMode(), QGraphicsView::ScrollHandDrag); EXPECT_FALSE(view.isInteractive()); // Restore the previous tool state for other tests olive::Core::instance()->set_tool(olive::Tool::k_pointer); EXPECT_EQ(view.dragMode(), QGraphicsView::RubberBandDrag); EXPECT_TRUE(view.isInteractive()); } TEST(WidgetHandMovableView, MiddleButtonHandDragStateMachine) { ensure_core(); ProbeHandView view; view.resize(200, 100); view.pub_set_default_drag_mode(QGraphicsView::NoDrag); // Left button is not a hand drag QMouseEvent left_press(QEvent::MouseButtonPress, QPointF(10, 10), QPointF(10, 10), QPointF(10, 10), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); EXPECT_FALSE(view.pub_hand_press(&left_press)); EXPECT_TRUE(view.isInteractive()); // Middle button starts a hand drag QMouseEvent mid_press(QEvent::MouseButtonPress, QPointF(10, 10), QPointF(10, 10), QPointF(10, 10), Qt::MiddleButton, Qt::MiddleButton, Qt::NoModifier); EXPECT_TRUE(view.pub_hand_press(&mid_press)); EXPECT_EQ(view.dragMode(), QGraphicsView::ScrollHandDrag); EXPECT_FALSE(view.isInteractive()); QMouseEvent move(QEvent::MouseMove, QPointF(30, 20), QPointF(30, 20), QPointF(30, 20), Qt::NoButton, Qt::MiddleButton, Qt::NoModifier); EXPECT_TRUE(view.pub_hand_move(&move)); // Release restores the pre-drag state QMouseEvent release(QEvent::MouseButtonRelease, QPointF(30, 20), QPointF(30, 20), QPointF(30, 20), Qt::MiddleButton, Qt::NoButton, Qt::NoModifier); EXPECT_TRUE(view.pub_hand_release(&release)); EXPECT_TRUE(view.isInteractive()); EXPECT_EQ(view.dragMode(), QGraphicsView::NoDrag); // Without an active hand drag, move/release are ignored EXPECT_FALSE(view.pub_hand_move(&move)); EXPECT_FALSE(view.pub_hand_release(&release)); } TEST(WidgetHandMovableView, WheelZoomHelpers) { const QVariant old_scroll_zooms = olive::Config::current()[QStringLiteral("ScrollZooms")]; QWheelEvent plain(QPointF(5, 5), QPointF(5, 5), QPoint(), QPoint(0, 120), Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false); QWheelEvent ctrl(QPointF(5, 5), QPointF(5, 5), QPoint(), QPoint(0, 120), Qt::NoButton, Qt::ControlModifier, Qt::NoScrollPhase, false); // With ScrollZooms off, only Ctrl+wheel zooms olive::Config::current()[QStringLiteral("ScrollZooms")] = false; EXPECT_TRUE(olive::HandMovableView::WheelEventIsAZoomEvent(&ctrl)); EXPECT_FALSE(olive::HandMovableView::WheelEventIsAZoomEvent(&plain)); // With ScrollZooms on, plain wheel zooms and Ctrl+wheel does not olive::Config::current()[QStringLiteral("ScrollZooms")] = true; EXPECT_TRUE(olive::HandMovableView::WheelEventIsAZoomEvent(&plain)); EXPECT_FALSE(olive::HandMovableView::WheelEventIsAZoomEvent(&ctrl)); // 120 wheel units -> 1.12x; inverted devices flip the sign EXPECT_NEAR(olive::HandMovableView::get_scroll_zoom_multiplier(&plain), 1.12, 1e-9); QWheelEvent inverted(QPointF(5, 5), QPointF(5, 5), QPoint(), QPoint(0, 120), Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, true); EXPECT_NEAR(olive::HandMovableView::get_scroll_zoom_multiplier(&inverted), 0.88, 1e-9); olive::Config::current()[QStringLiteral("ScrollZooms")] = old_scroll_zooms; } TEST(WidgetToolbarButton, StoresToolAndIsCheckable) { olive::ToolbarButton btn(nullptr, olive::Tool::k_slip); EXPECT_EQ(btn.tool(), olive::Tool::k_slip); EXPECT_TRUE(btn.isCheckable()); } TEST(WidgetToolbar, SetToolChecksMatchingButtonOnly) { olive::Toolbar bar(nullptr); const auto buttons = toolbar_buttons(&bar); // 13 tool buttons + 1 snapping toggle EXPECT_EQ(buttons.size(), 14); bar.set_tool(olive::Tool::k_razor); for (olive::ToolbarButton *b : buttons) { if (b->tool() == olive::Tool::k_none) { continue; } EXPECT_EQ(b->isChecked(), b->tool() == olive::Tool::k_razor) << int(b->tool()); } } TEST(WidgetToolbar, ClickingButtonEmitsToolChanged) { olive::Toolbar bar(nullptr); QVector received; QObject::connect(&bar, &olive::Toolbar::tool_changed, [&received](const olive::Tool::Item &t) { received.append(t); }); olive::ToolbarButton *pointer = nullptr; for (olive::ToolbarButton *b : toolbar_buttons(&bar)) { if (b->tool() == olive::Tool::k_pointer) { pointer = b; break; } } ASSERT_NE(pointer, nullptr); pointer->click(); ASSERT_EQ(received.size(), 1); EXPECT_EQ(received.first(), olive::Tool::k_pointer); } TEST(WidgetToolbar, SnappingToggleReflectsAndEmits) { olive::Toolbar bar(nullptr); QSignalSpy spy(&bar, &olive::Toolbar::snapping_changed); olive::ToolbarButton *snap = nullptr; for (olive::ToolbarButton *b : toolbar_buttons(&bar)) { if (b->tool() == olive::Tool::k_none) { snap = b; break; } } ASSERT_NE(snap, nullptr); bar.set_snapping(false); EXPECT_FALSE(snap->isChecked()); bar.set_snapping(true); EXPECT_TRUE(snap->isChecked()); snap->click(); ASSERT_EQ(spy.count(), 1); EXPECT_EQ(spy.first().first().toBool(), false); } TEST(WidgetColorButton, SetColorRoundTrips) { olive::ColorManager::set_up_default_config(); olive::Project project; olive::ColorButton btn(oak_color_manager(project.color_manager())); EXPECT_FLOAT_EQ(btn.get_color().red(), 1.0f); EXPECT_FLOAT_EQ(btn.get_color().green(), 1.0f); EXPECT_FLOAT_EQ(btn.get_color().blue(), 1.0f); btn.set_color(olive::ManagedColor(0.25, 0.5, 0.75, 1.0)); const olive::ManagedColor &out = btn.get_color(); EXPECT_FLOAT_EQ(out.red(), 0.25f); EXPECT_FLOAT_EQ(out.green(), 0.5f); EXPECT_FLOAT_EQ(out.blue(), 0.75f); EXPECT_FLOAT_EQ(out.alpha(), 1.0f); // An unset colorspace is conformed to the manager default EXPECT_FALSE(out.color_input().isEmpty()); } TEST(WidgetColorValuesTab, FloatModeRoundTripsColor) { olive::ColorValuesTab tab(false); tab.set_color(olive::Color(0.25, 0.5, 0.75)); EXPECT_NEAR(tab.get_red(), 0.25, 1e-6); EXPECT_NEAR(tab.get_green(), 0.5, 1e-6); EXPECT_NEAR(tab.get_blue(), 0.75, 1e-6); olive::Color out = tab.get_color(); EXPECT_NEAR(out.red(), 0.25, 1e-6); EXPECT_NEAR(out.green(), 0.5, 1e-6); EXPECT_NEAR(out.blue(), 0.75, 1e-6); // The web field shows the rgb() form in float mode auto *hex = tab.findChild(); ASSERT_NE(hex, nullptr); EXPECT_EQ(hex->get_value(), QStringLiteral("rgb(0.25, 0.5, 0.75)")); } TEST(WidgetColorValuesTab, LegacyToggleRescalesSliders) { const QVariant old_legacy = olive::Config::current()[QStringLiteral("UseLegacyColorInInputTab")]; olive::Config::current()[QStringLiteral("UseLegacyColorInInputTab")] = false; { olive::ColorValuesTab tab(true); tab.set_red(1.0); EXPECT_NEAR(tab.get_red(), 1.0, 1e-6); QCheckBox *legacy = tab.findChild(); ASSERT_NE(legacy, nullptr); EXPECT_FALSE(legacy->isChecked()); // Switching to legacy keeps the effective color but shows 0-255 legacy->click(); EXPECT_NEAR(tab.get_red(), 1.0, 1e-6); auto *hex = tab.findChild(); ASSERT_NE(hex, nullptr); EXPECT_EQ(hex->get_value(), QStringLiteral("FF0000")); // And back legacy->click(); EXPECT_NEAR(tab.get_red(), 1.0, 1e-6); EXPECT_EQ(hex->get_value(), QStringLiteral("rgb(1.0, 0.0, 0.0)")); } olive::Config::current()[QStringLiteral("UseLegacyColorInInputTab")] = old_legacy; } TEST(WidgetColorSwatchChooser, ClickingSwatchEmitsItsColor) { olive::ColorManager::set_up_default_config(); olive::Project project; olive::ColorSwatchChooser chooser(oak_color_manager(project.color_manager())); const auto buttons = chooser.findChildren(); EXPECT_EQ(buttons.size(), 32); QVector received; QObject::connect(&chooser, &olive::ColorSwatchChooser::color_clicked, [&received](const olive::ManagedColor &c) { received.append(c); }); buttons.first()->click(); ASSERT_EQ(received.size(), 1); // The emitted color is exactly the clicked button's color const olive::ManagedColor &expected = buttons.first()->get_color(); EXPECT_FLOAT_EQ(received.first().red(), expected.red()); EXPECT_FLOAT_EQ(received.first().green(), expected.green()); EXPECT_FLOAT_EQ(received.first().blue(), expected.blue()); } TEST(WidgetColorSpaceChooser, InputRoundTripsAndEmits) { olive::ColorManager::set_up_default_config(); olive::Project project; const QStringList spaces = project.color_manager()->list_available_colorspaces(); ASSERT_GE(spaces.size(), 2); // Input-only mode, as used by the export dialog olive::ColorSpaceChooser chooser(oak_color_manager(project.color_manager()), true, false); EXPECT_FALSE(chooser.input().isEmpty()); QSignalSpy spy(&chooser, &olive::ColorSpaceChooser::input_color_space_changed); // Pick whichever colorspace isn't currently selected QString target; for (const QString &s : spaces) { if (s != chooser.input()) { target = s; break; } } ASSERT_FALSE(target.isEmpty()); chooser.set_input(target); EXPECT_EQ(chooser.input(), target); ASSERT_EQ(spy.count(), 1); EXPECT_EQ(spy.first().first().toString(), target); } TEST(WidgetColorSpaceChooser, FullModePopulatesDisplayFields) { olive::ColorManager::set_up_default_config(); olive::Project project; olive::ColorSpaceChooser chooser(oak_color_manager(project.color_manager())); EXPECT_FALSE(chooser.input().isEmpty()); EXPECT_FALSE(chooser.output().display().isEmpty()); EXPECT_FALSE(chooser.output().view().isEmpty()); } TEST(WidgetColorPreviewBox, RendersManagedColor) { olive::ColorPreviewBox box; box.resize(20, 20); box.set_color(olive::Color(1.0, 0.0, 0.0, 1.0)); QImage img(box.size(), QImage::Format_ARGB32); img.fill(Qt::transparent); box.render(&img); const QColor px = img.pixelColor(img.rect().center()); EXPECT_GT(px.red(), 200); EXPECT_LT(px.green(), 60); EXPECT_LT(px.blue(), 60); } TEST(WidgetColorGradient, ClickPositionsMapToValueRange) { olive::ColorGradientWidget grad(Qt::Horizontal); grad.resize(100, 20); grad.set_selected_color(olive::Color(1.0, 0.0, 0.0)); QVector received; QObject::connect(&grad, &olive::ColorGradientWidget::selected_color_changed, [&received](const olive::Color &c) { received.append(c); }); // Left edge is the full-value end of the gradient float hue, sat, val; QTest::mouseClick(&grad, Qt::LeftButton, Qt::NoModifier, QPoint(0, 10)); ASSERT_EQ(received.size(), 1); EXPECT_FLOAT_EQ(grad.get_selected_color().red(), received.first().red()); received.first().to_hsv(&hue, &sat, &val); EXPECT_NEAR(val, 1.0, 1e-4); // Right edge approaches the zero-value end QTest::mouseClick(&grad, Qt::LeftButton, Qt::NoModifier, QPoint(99, 10)); ASSERT_EQ(received.size(), 2); received.at(1).to_hsv(&hue, &sat, &val); EXPECT_NEAR(val, 0.01, 0.02); } TEST(WidgetColorWheel, ResizeEmitsDiameter) { olive::ColorWheelWidget wheel; wheel.show(); EXPECT_TRUE(QTest::qWaitForWindowExposed(&wheel)); QSignalSpy spy(&wheel, &olive::ColorWheelWidget::diameter_changed); wheel.resize(200, 100); ASSERT_GE(spy.count(), 1); EXPECT_EQ(spy.last().first().toInt(), 100); wheel.resize(80, 120); EXPECT_EQ(spy.last().first().toInt(), 80); } TEST(WidgetColorWheel, SelectedColorRoundTrips) { olive::ColorWheelWidget wheel; wheel.resize(100, 100); wheel.set_selected_color(olive::Color(0.2, 0.4, 0.6)); EXPECT_FLOAT_EQ(wheel.get_selected_color().red(), 0.2f); EXPECT_FLOAT_EQ(wheel.get_selected_color().green(), 0.4f); EXPECT_FLOAT_EQ(wheel.get_selected_color().blue(), 0.6f); } TEST(WidgetNodeValueTree, PopulatesRowsAndSetsValueHint) { olive::ColorManager::set_up_default_config(); olive::Project project; project.initialize(); auto *source = new TwoValueNode(); source->setParent(&project); auto *consumer = new olive::MathNode(); consumer->setParent(&project); olive::Node::connect_edge(source, olive::NodeInput(consumer, olive::MathNode::k_param_a_in)); olive::NodeValueTree tree; tree.set_node(oak::Input(reinterpret_cast(consumer), olive::MathNode::k_param_a_in), olive::Rational(0)); // One row per pushed value ASSERT_EQ(tree.topLevelItemCount(), 2); // The row matching the input's float type is pre-selected int checked_row = -1; int float_row = -1; for (int i = 0; i < 2; i++) { if (tree.topLevelItem(i)->text(1) == QStringLiteral("Float")) { float_row = i; } auto *radio = qobject_cast(tree.itemWidget(tree.topLevelItem(i), 0)); ASSERT_NE(radio, nullptr); if (radio->isChecked()) { checked_row = i; } } EXPECT_EQ(checked_row, float_row); EXPECT_EQ(consumer->get_value_hint_for_input(olive::MathNode::k_param_a_in).index(), -1); // Clicking the other row writes its value hint back to the node const int other_row = 1 - checked_row; auto *other_radio = qobject_cast( tree.itemWidget(tree.topLevelItem(other_row), 0)); ASSERT_NE(other_radio, nullptr); other_radio->click(); const olive::Node::ValueHint hint = consumer->get_value_hint_for_input(olive::MathNode::k_param_a_in); EXPECT_EQ(hint.index(), 1 - other_row); // table.Count() - 1 - row EXPECT_TRUE(hint.types().contains(olive::NodeValue::k_int)); }