#include "UI/LayerAppearanceControls.h" #include "UI/LayerIcons.h" #include "UI/LayersPanel.h" #include "UI/NativeLayerList.h" #include "SessionFixtures.h" #include #include #include #include #include #include #include // The list under menus, themes and a short viewport. namespace { std::unique_ptr sessionWithLayers(int count) { auto session = std::make_unique(); session->createDocument(800, 600); for (int each = 0; each < count; ++each) session->addBlankLayer(); return session; } // A window with an Edit menu over the panel. struct Framed { QMainWindow window; explicit Framed(QWidget *central) { window.menuBar()->addMenu(QStringLiteral("&Edit"))->addAction(QStringLiteral("Nothing")); window.setCentralWidget(central); window.resize(400, 300); window.show(); if (!QTest::qWaitForWindowActive(&window)) throw std::runtime_error("the window never became active"); } void openEditMenuAndClose() { QTest::keyClick(&window, Qt::Key_E, Qt::AltModifier); QTRY_VERIFY(QApplication::activePopupWidget() != nullptr); QTest::keyClick(QApplication::activePopupWidget(), Qt::Key_Escape); QTest::keyClick(window.menuBar(), Qt::Key_Escape); } }; QImage iconImage(const QIcon &icon, int side) { return icon.pixmap(QSize(side, side)).toImage(); } } class LayerListFocusTests : public QObject { Q_OBJECT private slots: void aMenuBorrowingFocusKeepsTheRenameAndTheField(); void aSwipeInterruptedByARebuildStillEndsItsStep(); void aSwipeScrollsAShortListAndTheKeysKeepTheRowInView(); void aNewPaletteRedrawsEveryIcon(); void aSwipeReleasedOverAPopupStillEnds(); void shiftArrowsGrowAndShrinkARangeFromTheAnchor(); void altAndShiftOnAMaskThumbnailChooseAndToggleIt(); void tabAndTheBrushKeysStayWithTheList(); void aSliderDragReleasedOverAPopupStillEnds(); void anotherButtonsReleaseLeavesTheSwipe(); void altDoubleClickOnTheStripClipsTwiceAndAltShowsAMaskAlone(); void theNameFieldKeepsTheNamesSizeAndPlace(); }; void LayerListFocusTests::aMenuBorrowingFocusKeepsTheRenameAndTheField() { const auto session = sessionWithLayers(2); auto *panel = new LayersPanel(*session); Framed framed(panel); LayerCell &row = *panel->list().cells().at(0); QTest::mouseDClick(&row, Qt::LeftButton, Qt::NoModifier, QPoint(row.width() - 20, 20)); auto *editor = row.findChild("layerNameEditor"); QTRY_VERIFY(editor->hasFocus()); QTest::keyClicks(editor, "Sky"); framed.openEditMenuAndClose(); QTRY_VERIFY(editor->hasFocus()); QVERIFY(editor->isVisible() && session->renamingLayerID().has_value()); QCOMPARE(layerWith(*session, row.layerID()).name, QString("Layer 2")); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(layerWith(*session, row.layerID()).name, QString("Sky")); // The opacity field keeps its text and undo too. auto *field = panel->findChild("opacityPercent"); field->setFocus(); field->selectAll(); QTest::keyClicks(field, "40"); framed.openEditMenuAndClose(); QTRY_VERIFY(field->hasFocus()); QCOMPARE(field->text(), QString("40")); QCOMPARE(session->activeLayer().value().opacity, 1.0); QVERIFY(field->isUndoAvailable()); QTest::keyClick(field, Qt::Key_Return); QCOMPARE(session->activeLayer().value().opacity, 0.4); } void LayerListFocusTests::aSwipeInterruptedByARebuildStillEndsItsStep() { const auto session = sessionWithLayers(2); NativeLayerList list(*session); list.resize(252, 300); list.show(); QVERIFY(QTest::qWaitForWindowActive(&list)); auto *eye = list.cells().at(0)->findChild("layerEye"); const int steps = session->history.undoCount(); QTest::mousePress(eye, Qt::LeftButton, Qt::NoModifier, QPoint(10, 16)); QVERIFY(!session->canUndo()); // The shortcut lands mid-swipe: the rows are made anew. session->addBlankLayer(); QCOMPARE(list.cells().size(), size_t(3)); QTRY_VERIFY(session->canUndo()); QCOMPARE(session->history.undoCount(), steps + 1); QCOMPARE(session->document().value().layers.size(), size_t(3)); session->undo(); QCOMPARE(session->document().value().layers.size(), size_t(2)); QVERIFY(session->document().value().layers.back().isVisible); } void LayerListFocusTests::aSwipeScrollsAShortListAndTheKeysKeepTheRowInView() { const auto session = sessionWithLayers(15); NativeLayerList list(*session); list.resize(252, 200); list.show(); QVERIFY(QTest::qWaitForWindowActive(&list)); QCOMPARE(list.verticalScrollBar()->value(), 0); auto *eye = list.cells().at(0)->findChild("layerEye"); QTest::mousePress(eye, Qt::LeftButton, Qt::NoModifier, QPoint(10, 16)); // Held below the viewport, the list scrolls by the overshoot. const QPoint below(10, 16 + 250); QMouseEvent move(QEvent::MouseMove, QPointF(below), eye->mapToGlobal(below), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(eye, &move); QTest::mouseRelease(eye, Qt::LeftButton, Qt::NoModifier, below); QCOMPARE(list.verticalScrollBar()->value(), 76); // The pointer sat 276 points down the list, now scrolled. const int reached = list.rowAt(QPoint(10, 276)); QCOMPARE(reached, 6); QVERIFY(!layerWith(*session, list.cells().at(reached)->layerID()).isVisible); QVERIFY(layerWith(*session, list.cells().at(reached + 1)->layerID()).isVisible); session->undo(); list.verticalScrollBar()->setValue(0); // Down walks past the view and scrolls; Shift extends. session->selectLayers({list.cells().at(0)->layerID()}, list.cells().at(0)->layerID()); session->selectTool(NavigationTool::brush); list.setFocus(); for (int each = 0; each < 6; ++each) QTest::keyClick(&list, Qt::Key_Down); QCOMPARE(session->activeLayerID(), std::optional(list.cells().at(6)->layerID())); QVERIFY(list.verticalScrollBar()->value() > 0); const QRect shown = list.cells().at(6)->geometry().translated(0, -list.verticalScrollBar()->value()); QVERIFY(list.viewport()->rect().contains(shown)); QTest::keyClick(&list, Qt::Key_Down, Qt::ShiftModifier); QCOMPARE(session->selectedLayerIDs(), (QSet{list.cells().at(6)->layerID(), list.cells().at(7)->layerID()})); QCOMPARE(session->activeLayerID(), std::optional(list.cells().at(7)->layerID())); } void LayerListFocusTests::aNewPaletteRedrawsEveryIcon() { const auto session = sessionWithLayers(1); session->addGroup(); LayersPanel panel(*session); panel.show(); QVERIFY(QTest::qWaitForWindowExposed(&panel)); const QPalette before = QApplication::palette(); QPalette changed = before; changed.setColor(QPalette::PlaceholderText, QColor(10, 200, 30)); changed.setColor(QPalette::WindowText, QColor(200, 10, 30)); QApplication::setPalette(changed); const double ratio = panel.devicePixelRatio(); auto *add = panel.findChild("addBlankLayer"); // Buttons take the primary ink since 14.11; menus the secondary. QTRY_COMPARE(iconImage(add->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::newLayer, 16, QColor(200, 10, 30), ratio), 16)); QCOMPARE(iconImage(panel.findChild("addLayerMask")->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::addMask, 16, QColor(200, 10, 30), ratio), 16)); QCOMPARE(iconImage(panel.findChild("layerEffects")->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::sparkles, 16, QColor(10, 200, 30), ratio), 16)); LayerCell &folder = *panel.list().cells().at(0); QTRY_COMPARE(iconImage(folder.findChild("layerEye")->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::eye, 16, QColor(200, 10, 30), ratio), 16)); QCOMPARE(iconImage(folder.thumbnail().icon(), 36), iconImage(LayerIcons::pixmap(LayerIcon::folder, 36 * 0.8, QColor(200, 10, 30), ratio), 36)); QApplication::setPalette(before); QTRY_COMPARE(iconImage(add->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::newLayer, 16, before.color(QPalette::Active, QPalette::WindowText), ratio), 16)); } void LayerListFocusTests::aSwipeReleasedOverAPopupStillEnds() { const auto session = sessionWithLayers(2); NativeLayerList list(*session); list.resize(252, 300); list.show(); QVERIFY(QTest::qWaitForWindowActive(&list)); auto *eye = list.cells().at(0)->findChild("layerEye"); const int steps = session->history.undoCount(); QTest::mousePress(eye, Qt::LeftButton, Qt::NoModifier, QPoint(10, 16)); QVERIFY(!session->canUndo()); // A menu opened mid-press takes the release. QMenu menu; menu.addAction(QStringLiteral("Nothing")); menu.popup(list.mapToGlobal(QPoint(100, 100))); QTRY_VERIFY(menu.isVisible()); QTest::mouseRelease(&menu, Qt::LeftButton, Qt::NoModifier, QPoint(4, 4)); menu.close(); QVERIFY(session->canUndo()); QCOMPARE(session->history.undoCount(), steps + 1); QVERIFY(!layerWith(*session, list.cells().at(0)->layerID()).isVisible); // The next press begins a swipe of its own. QTest::mouseClick(eye, Qt::LeftButton, Qt::NoModifier, QPoint(10, 16)); QCOMPARE(session->history.undoCount(), steps + 2); QVERIFY(layerWith(*session, list.cells().at(0)->layerID()).isVisible); } void LayerListFocusTests::shiftArrowsGrowAndShrinkARangeFromTheAnchor() { const auto session = sessionWithLayers(4); NativeLayerList list(*session); list.resize(252, 300); list.show(); QVERIFY(QTest::qWaitForWindowActive(&list)); session->selectTool(NavigationTool::hand); const auto id = [&](int row) { return list.cells().at(size_t(row))->layerID(); }; const auto ids = [&](std::initializer_list rows) { QSet set; for (const int row : rows) set.insert(id(row)); return set; }; session->selectLayers({id(0)}, id(0)); list.setFocus(); QTest::keyClick(&list, Qt::Key_Down, Qt::ShiftModifier); QTest::keyClick(&list, Qt::Key_Down, Qt::ShiftModifier); QCOMPARE(session->selectedLayerIDs(), ids({0, 1, 2})); QCOMPARE(session->activeLayerID(), std::optional(id(2))); // Back up: the range shrinks to its anchor, then past. QTest::keyClick(&list, Qt::Key_Up, Qt::ShiftModifier); QCOMPARE(session->selectedLayerIDs(), ids({0, 1})); QCOMPARE(session->activeLayerID(), std::optional(id(1))); QTest::keyClick(&list, Qt::Key_Up, Qt::ShiftModifier); QCOMPARE(session->selectedLayerIDs(), ids({0})); QTest::keyClick(&list, Qt::Key_Up, Qt::ShiftModifier); QCOMPARE(session->selectedLayerIDs(), ids({0})); session->selectLayers({id(2)}, id(2)); QTest::keyClick(&list, Qt::Key_Up, Qt::ShiftModifier); QCOMPARE(session->selectedLayerIDs(), ids({1, 2})); QTest::keyClick(&list, Qt::Key_Down, Qt::ShiftModifier); QTest::keyClick(&list, Qt::Key_Down, Qt::ShiftModifier); QCOMPARE(session->selectedLayerIDs(), ids({2, 3})); QCOMPARE(session->activeLayerID(), std::optional(id(3))); } void LayerListFocusTests::altAndShiftOnAMaskThumbnailChooseAndToggleIt() { const auto session = sessionWithLayers(1); session->addLayerMask(); session->selectLayerTarget(session->activeLayerID().value(), false); NativeLayerList list(*session); list.resize(252, 300); list.show(); QVERIFY(QTest::qWaitForWindowActive(&list)); LayerThumbnailButton &mask = list.cells().at(0)->maskThumbnail(); // An Alt click chooses the mask; a drag away, nothing. QTest::mousePress(&mask, Qt::LeftButton, Qt::AltModifier, QPoint(5, 5)); QVERIFY(!session->isMaskSelected()); QTest::mouseRelease(&mask, Qt::LeftButton, Qt::AltModifier, QPoint(200, 5)); QVERIFY(!session->isMaskSelected()); QTest::mouseClick(&mask, Qt::LeftButton, Qt::AltModifier, QPoint(5, 5)); QVERIFY(session->isMaskSelected()); // A Shift double click toggles twice and renames nothing. QTest::mouseDClick(list.windowHandle(), Qt::LeftButton, Qt::ShiftModifier, mask.mapTo(&list, QPoint(5, 5))); QVERIFY(session->activeLayer().value().mask.value().isEnabled); QVERIFY(!session->renamingLayerID().has_value()); QTest::mouseClick(&mask, Qt::LeftButton, Qt::ShiftModifier, QPoint(5, 5)); QVERIFY(!session->activeLayer().value().mask.value().isEnabled); } void LayerListFocusTests::aSliderDragReleasedOverAPopupStillEnds() { const auto session = sessionWithLayers(1); LayerAppearanceControls controls(*session); controls.show(); QVERIFY(QTest::qWaitForWindowActive(&controls)); auto *slider = controls.findChild("opacitySlider"); const int steps = session->history.undoCount(); // The handle sits at the right end, at full opacity. QTest::mousePress(slider, Qt::LeftButton, Qt::NoModifier, QPoint(slider->width() - 4, slider->height() / 2)); QVERIFY(slider->isSliderDown() && !session->canUndo()); QMenu menu; menu.addAction(QStringLiteral("Nothing")); menu.popup(controls.mapToGlobal(QPoint(50, 50))); QTRY_VERIFY(menu.isVisible()); QTest::mouseRelease(&menu, Qt::LeftButton, Qt::NoModifier, QPoint(4, 4)); menu.close(); QVERIFY(!slider->isSliderDown()); QVERIFY(session->canUndo()); QCOMPARE(session->history.undoCount(), steps); // The next drag is its own step. QTest::mousePress(slider, Qt::LeftButton, Qt::NoModifier, QPoint(slider->width() - 4, slider->height() / 2)); QVERIFY(!session->canUndo()); const QPoint middle(slider->width() / 2, slider->height() / 2); QMouseEvent drag(QEvent::MouseMove, QPointF(middle), slider->mapToGlobal(middle), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(slider, &drag); QTest::mouseRelease(slider, Qt::LeftButton, Qt::NoModifier, middle); QVERIFY(session->canUndo()); QCOMPARE(session->history.undoCount(), steps + 1); QVERIFY(session->activeLayer().value().opacity < 0.6); } void LayerListFocusTests::anotherButtonsReleaseLeavesTheSwipe() { const auto session = sessionWithLayers(3); NativeLayerList list(*session); list.resize(252, 300); list.show(); QVERIFY(QTest::qWaitForWindowActive(&list)); auto *eye = list.cells().at(0)->findChild("layerEye"); QTest::mousePress(eye, Qt::LeftButton, Qt::NoModifier, QPoint(10, 16)); QTest::mousePress(eye, Qt::MiddleButton, Qt::NoModifier, QPoint(10, 16)); QTest::mouseRelease(eye, Qt::MiddleButton, Qt::NoModifier, QPoint(10, 16)); QVERIFY(!session->canUndo()); // Still swiping: the next row takes the state. const QPoint next(10, 16 + LayerCell::rowHeight); QMouseEvent move(QEvent::MouseMove, QPointF(next), eye->mapToGlobal(next), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(eye, &move); QVERIFY(!layerWith(*session, list.cells().at(1)->layerID()).isVisible); QTest::mouseRelease(eye, Qt::LeftButton, Qt::NoModifier, next); QVERIFY(session->canUndo()); } void LayerListFocusTests::altDoubleClickOnTheStripClipsTwiceAndAltShowsAMaskAlone() { const auto session = sessionWithLayers(2); NativeLayerList list(*session); list.resize(252, 300); list.show(); QVERIFY(QTest::qWaitForWindowActive(&list)); LayerCell &top = *list.cells().at(0); const QPoint strip = top.mapTo(&list, QPoint(top.width() - 20, LayerCell::rowHeight - 4)); const int steps = session->history.undoCount(); QTest::mouseDClick(list.windowHandle(), Qt::LeftButton, Qt::AltModifier, strip); QVERIFY(!layerWith(*session, top.layerID()).maskSourceID.has_value()); QVERIFY(!session->renamingLayerID().has_value()); // Clipped and released again: two steps, no rename. QCOMPARE(session->history.undoCount(), steps + 2); session->undo(); QCOMPARE(layerWith(*session, top.layerID()).maskSourceID, std::optional(list.cells().at(1)->layerID())); session->redo(); // Alt on a mask shows it alone; Shift adds nothing. session->selectLayers({top.layerID()}, top.layerID()); session->addLayerMask(); session->selectLayerTarget(top.layerID(), false); QTest::mouseClick(&top.maskThumbnail(), Qt::LeftButton, Qt::AltModifier | Qt::ShiftModifier, QPoint(5, 5)); QVERIFY(session->isMaskSelected() && session->viewsMaskAlone()); QVERIFY(session->activeLayer().value().mask.value().isEnabled); } void LayerListFocusTests::tabAndTheBrushKeysStayWithTheList() { const auto session = sessionWithLayers(2); auto *panel = new QWidget; auto *list = new NativeLayerList(*session, panel); list->setGeometry(0, 0, 252, 200); // A field beside it could take the focus from Tab. (new QLineEdit(panel))->setGeometry(0, 210, 100, 20); Framed framed(panel); list->setFocus(); QTRY_VERIFY(list->hasFocus()); session->selectTool(NavigationTool::marquee); // Tab and Shift-Tab both switch the shape, as Swift's table. QTest::keyClick(list, Qt::Key_Tab); QCOMPARE(session->marqueeKind(), LassoKind::ellipse); QTest::keyClick(list, Qt::Key_Backtab, Qt::ShiftModifier); QCOMPARE(session->marqueeKind(), LassoKind::rectangle); QTest::keyClick(list, Qt::Key_Tab, Qt::ControlModifier); QCOMPARE(session->marqueeKind(), LassoKind::rectangle); QVERIFY(list->hasFocus()); // E erases, B paints; Ctrl leaves the letter alone. QTest::keyClick(list, Qt::Key_E); QVERIFY(session->tool() == NavigationTool::brush && session->brushMode() == BrushToolMode::erase); QTest::keyClick(list, Qt::Key_B); QCOMPARE(session->brushMode(), BrushToolMode::paint); QTest::keyClick(list, Qt::Key_E, Qt::ControlModifier); QCOMPARE(session->brushMode(), BrushToolMode::paint); QTest::keyClick(list, Qt::Key_4); QCOMPARE(session->brushSettings().opacity, 0.4); // Where digits set nothing, the key goes to the parent. session->selectTool(NavigationTool::hand); QKeyEvent digit(QEvent::KeyPress, Qt::Key_5, Qt::NoModifier, QStringLiteral("5")); QApplication::sendEvent(list, &digit); QVERIFY(!digit.isAccepted()); } void LayerListFocusTests::theNameFieldKeepsTheNamesSizeAndPlace() { const auto session = sessionWithLayers(1); auto *panel = new LayersPanel(*session); Framed framed(panel); LayerCell &row = *panel->list().cells().at(0); QLabel &name = *row.findChild("layerName"); const QRect label = name.geometry(); QTest::mouseDClick(&row, Qt::LeftButton, Qt::NoModifier, QPoint(row.width() - 20, 20)); auto *editor = row.findChild("layerNameEditor"); QTRY_VERIFY(editor->isVisible() && editor->hasFocus()); // Swift 1.4.7: no bezel, the name's height, its text unmoved. QVERIFY(!editor->hasFrame()); QCOMPARE(editor->geometry(), label.adjusted(-2, 0, 0, 0)); QCOMPARE(name.geometry(), label); // Swift 1.4.7: no bezel, the name's height, text unmoved. QColor tint = row.palette().color(QPalette::Active, QPalette::WindowText); tint.setAlphaF(0.1f); QCOMPARE(editor->palette().color(QPalette::Base), tint); // A tenth of the ink marks it: quaternary label colour. editor->deselect(); const QImage drawn = row.grab().toImage(); const QColor ground = drawn.pixelColor(row.width() - 3, label.center().y()); const QColor inside = drawn.pixelColor(label.right() - 2, label.top() + 1); const QColor ink = row.palette().color(QPalette::Active, QPalette::WindowText); const auto near = [](int value, double expected) { return std::abs(value - expected) <= 2; }; QVERIFY2(near(inside.red(), ground.red() * 0.9 + ink.red() * 0.1) && near(inside.green(), ground.green() * 0.9 + ink.green() * 0.1) && near(inside.blue(), ground.blue() * 0.9 + ink.blue() * 0.1), qPrintable(inside.name() + " over " + ground.name())); // The old name is not drawn beneath: cleared, no ink. QVERIFY(!name.isVisible()); editor->clear(); const QImage cleared = row.grab().toImage(); int stray = 0; for (int x = label.left() + 1; x < label.left() + 60; ++x) for (int y = label.top() + 2; y < label.bottom() - 1; ++y) stray += cleared.pixelColor(x, y) != inside; QCOMPARE(stray, 0); QTest::keyClick(editor, Qt::Key_Escape); QVERIFY(name.isVisible() && !editor->isVisible()); } QTEST_MAIN(LayerListFocusTests) #include "LayerListFocusTests.moc"