#include "MenuFixtures.h" #include "InlineTextDrawFixtures.h" #include #include #include #include #include #include // The editor on screen: its text, frame, caret, selection, focus. class InlineTextDrawTests : public QObject { Q_OBJECT private slots: void theEditorShowsTheDraftInsteadOfItsLayer(); void theFrameAndHandlesSitOnTheBox(); void aTurnedFlippedLayerIsEditedInPlace(); void theCaretShowsWhileTheCanvasHasFocus(); void aSelectionShowsBehindItsText(); void textPastTheBoxShowsAPlus(); void aFreshDraftTakesTheKeysButSparesAField(); void theDraftTakesItsKeysAheadOfShortcuts(); void theEditMenuReachesOpenText(); void editsRepaintTheEditor(); void openingTextRepaintsItsWholeLayer(); }; void InlineTextDrawTests::theEditorShowsTheDraftInsteadOfItsLayer() { DrawnText shown; shown.text(QPointF(20, 30), QStringLiteral("Hide")); QVERIFY(where(shown.grab(), reddish).isValid()); shown.session.editActiveText(); shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("Hi"); style.red = 0; style.green = 1; }); // The layer's red is gone; the draft's green shows. const QImage editing = shown.grab(); QVERIFY(!where(editing, reddish).isValid()); QVERIFY(where(editing, greenish).isValid()); shown.session.cancelText(); QVERIFY(where(shown.grab(), reddish).isValid()); } void InlineTextDrawTests::theFrameAndHandlesSitOnTheBox() { DrawnText shown; shown.session.beginText(QRectF(100, 80, 200, 120)); const QImage image = shown.grab(); const QColor accent = shown.canvas->palette().color(QPalette::Highlight); const QSizeF size = shown.editor().logicalSize(); for (const QPointF &unit : LayerTransform::handles) QCOMPARE(image.pixelColor(shown.onScreen(QPointF(unit.x() * size.width(), unit.y() * size.height()))), QColor(Qt::white)); // The frame: an accent line, a sixth of a handle. const QColor frame = image.pixelColor(shown.onScreen(QPointF(size.width() / 4, 0.25))); QVERIFY2(std::abs(frame.red() - accent.red()) < 40 && std::abs(frame.blue() - accent.blue()) < 40, qPrintable(frame.name())); QVERIFY(image.pixelColor(shown.onScreen(QPointF(size.width() / 4, 4))) != frame); // No plus without overflow. QVERIFY(image.pixelColor(shown.onScreen(QPointF(size.width(), size.height()))) == QColor(Qt::white)); // Tight leading lifts the H into the frame, still over. shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("H"); style.leading = 30; }); QCOMPARE(qAlpha(EditorSession::textImage(shown.session.textDraft().value().style).pixel(24, 0)), 255); const QColor over = shown.grab().pixelColor(shown.onScreen(QPointF(24, 0.25))); QVERIFY2(!reddish(over) && std::abs(over.red() - accent.red()) < 40 && std::abs(over.blue() - accent.blue()) < 40, qPrintable(over.name())); QVERIFY(where(shown.grab(), reddish).isValid()); } void InlineTextDrawTests::aTurnedFlippedLayerIsEditedInPlace() { DrawnText shown; const QUuid id = shown.text(QPointF(150, 100), QStringLiteral("Turn")); rewrite(shown.session, [&](ProjectSnapshot &snapshot) { LayerTransform &transform = record(snapshot, id).transform; transform.rotation = 90; transform.flipX = true; }); const QRect drawn = where(shown.grab(), reddish); // A field keeps the keys, so no caret draws. auto *field = new QLineEdit(&shown.window); field->show(); field->setFocus(); QTRY_VERIFY(field->hasFocus()); shown.session.editActiveText(); QTest::qWait(20); QVERIFY(!shown.editor().caretShown()); const QRect edited = where(shown.grab(), reddish); QVERIFY2(std::abs(edited.left() - drawn.left()) <= 2 && std::abs(edited.top() - drawn.top()) <= 2 && std::abs(edited.right() - drawn.right()) <= 2 && std::abs(edited.bottom() - drawn.bottom()) <= 2, qPrintable(QString("%1,%2 %3x%4 against %5,%6 %7x%8") .arg(edited.x()).arg(edited.y()).arg(edited.width()).arg(edited.height()) .arg(drawn.x()).arg(drawn.y()).arg(drawn.width()).arg(drawn.height()))); // Turned, the text runs down the screen. QVERIFY(edited.height() > edited.width()); } void InlineTextDrawTests::theCaretShowsWhileTheCanvasHasFocus() { DrawnText shown; auto *field = new QLineEdit(&shown.window); field->setGeometry(0, 0, 10, 10); field->show(); shown.canvas->setFocus(); QTRY_VERIFY(shown.canvas->hasFocus()); beginTextAt(shown.session, QPointF(20, 30)); QTest::keyClicks(shown.canvas, QStringLiteral("ab")); const QRectF caret = shown.editor().textTransform().mapRect( QRectF(QPointF(LayerTextStyle::padding, LayerTextStyle::padding), QSizeF(1, shown.session.textDraft().value().style.lineHeight())) .translated(QFontMetricsF(TextLayout::font(shown.session.textDraft().value().style)).horizontalAdvance(QStringLiteral("ab")), 0)); const QPoint middle = caret.center().toPoint(); QVERIFY(shown.editor().caretShown()); QVERIFY2(reddish(shown.grab().pixelColor(middle)), qPrintable(shown.grab().pixelColor(middle).name())); // Input methods see the caret where it draws. const QRectF asked = static_cast(shown.canvas)->inputMethodQuery(Qt::ImCursorRectangle).toRectF(); QVERIFY(asked.adjusted(-1, -1, 1, 1).contains(middle)); // It blinks at half the system's flash time, repainting. QTest::keyClick(shown.canvas, Qt::Key_End); QElapsedTimer clock; clock.start(); // The key's own repaint lands first; then only the blink's. QTest::qWait(50); PaintSpy spy(*shown.canvas); QTRY_VERIFY(!shown.editor().caretShown()); QVERIFY(clock.elapsed() >= QGuiApplication::styleHints()->cursorFlashTime() / 2 - 50); QTRY_VERIFY(spy.painted.contains(middle)); QTRY_VERIFY(shown.editor().caretShown()); // Without the keys it stops; with them, shows at once. const QTimer *blink = shown.canvas->findChild(QStringLiteral("caretBlink")); QVERIFY(blink && blink->isActive()); field->setFocus(); QVERIFY(!shown.editor().caretShown() && !blink->isActive()); QVERIFY(!reddish(shown.grab().pixelColor(middle))); shown.canvas->setFocus(); QVERIFY(shown.editor().caretShown() && blink->isActive()); // A move or an edit shows the caret at once. QTRY_VERIFY(!shown.editor().caretShown()); QTest::keyClick(shown.canvas, Qt::Key_Left); QVERIFY(shown.editor().caretShown()); QTRY_VERIFY(!shown.editor().caretShown()); QTest::keyClicks(shown.canvas, QStringLiteral("c")); QVERIFY(shown.editor().caretShown()); // The blink ends with its editor. shown.session.cancelText(); QVERIFY(!shown.canvas->findChild(QStringLiteral("caretBlink"))); } void InlineTextDrawTests::aSelectionShowsBehindItsText() { DrawnText shown; shown.canvas->setFocus(); QTRY_VERIFY(shown.canvas->hasFocus()); beginTextAt(shown.session, QPointF(20, 30)); QTest::keyClicks(shown.canvas, QStringLiteral("iiii")); const QImage plain = shown.grab(); QTest::keyClick(shown.canvas, Qt::Key_A, Qt::ControlModifier); const auto mixed = [&](QPoint at, QColor highlight) { return highlightedOver(plain.pixelColor(at), shown.grab().pixelColor(at), highlight); }; const auto highlighted = [&](QPoint at) { return mixed(at, shown.canvas->palette().color(QPalette::Active, QPalette::Highlight)); }; // Above the letters, the line's height is highlighted. const QPoint above = shown.editor().textTransform().map(QPointF(LayerTextStyle::padding + 3, LayerTextStyle::padding + 3)).toPoint(); QVERIFY(highlighted(above)); const double line = shown.session.textDraft().value().style.lineHeight(); QVERIFY(highlighted(shown.editor().textTransform().map(QPointF(LayerTextStyle::padding + 3, LayerTextStyle::padding + line - 3)).toPoint())); // To the last selected letter's edge. const double selected = QFontMetricsF(TextLayout::font(shown.session.textDraft().value().style)).horizontalAdvance(QStringLiteral("iiii")); QVERIFY(highlighted(shown.editor().textTransform().map(QPointF(LayerTextStyle::padding + selected - 3, LayerTextStyle::padding + 3)).toPoint())); // No caret while something is selected. const QPointF mapped = shown.editor().textTransform().map( QPointF(LayerTextStyle::padding + QFontMetricsF(TextLayout::font(shown.session.textDraft().value().style)).horizontalAdvance(QStringLiteral("iiii")) + 0.25, LayerTextStyle::padding + 40)); const QPoint end(int(std::floor(mapped.x())), int(std::floor(mapped.y()))); QVERIFY(!reddish(shown.grab().pixelColor(end))); QTest::keyClick(shown.canvas, Qt::Key_End); QVERIFY(reddish(shown.grab().pixelColor(end))); QCOMPARE(shown.grab().pixelColor(above), plain.pixelColor(above)); // Unfocused, a selection shows in the inactive colour. QPalette palette = shown.canvas->palette(); palette.setColor(QPalette::Inactive, QPalette::Highlight, QColor(40, 200, 40)); shown.canvas->setPalette(palette); QTest::keyClick(shown.canvas, Qt::Key_A, Qt::ControlModifier); auto *field = new QLineEdit(&shown.window); field->show(); field->setFocus(); QTRY_VERIFY(!shown.canvas->hasFocus()); QVERIFY(mixed(above, QColor(40, 200, 40))); } void InlineTextDrawTests::textPastTheBoxShowsAPlus() { DrawnText shown; shown.session.beginText(QRectF(100, 80, 120, 60)); const QPointF corner(120, 60); QCOMPARE(shown.grab().pixelColor(shown.onScreen(corner)), QColor(Qt::white)); QTest::keyClicks(shown.canvas, QStringLiteral("more words than the box holds")); QVERIFY(shown.grab().pixelColor(shown.onScreen(corner)).lightness() < 100); } void InlineTextDrawTests::aFreshDraftTakesTheKeysButSparesAField() { DrawnText shown; auto *field = new QLineEdit(&shown.window); auto *button = new QPushButton(&shown.window); field->setGeometry(0, 0, 10, 10); button->setGeometry(20, 0, 10, 10); field->show(); button->show(); field->setFocus(); QTRY_VERIFY(field->hasFocus()); beginTextAt(shown.session, QPointF(20, 30)); QTest::qWait(50); QVERIFY(field->hasFocus()); shown.session.cancelText(); button->setFocus(); QTRY_VERIFY(button->hasFocus()); beginTextAt(shown.session, QPointF(20, 30)); QTRY_VERIFY(shown.canvas->hasFocus()); // A focus request leaves open text's keys alone. button->setFocus(); QTRY_VERIFY(button->hasFocus()); shown.canvas->consumeFocusRequest(7); QTest::qWait(50); QVERIFY(button->hasFocus()); shown.session.cancelText(); shown.canvas->consumeFocusRequest(8); QTRY_VERIFY(shown.canvas->hasFocus()); } void InlineTextDrawTests::theDraftTakesItsKeysAheadOfShortcuts() { DrawnText shown; auto *selectAll = new QAction(&shown.window); selectAll->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_A)); shown.window.addAction(selectAll); int triggered = 0; QObject::connect(selectAll, &QAction::triggered, [&] { ++triggered; }); shown.canvas->setFocus(); QTRY_VERIFY(shown.canvas->hasFocus()); QTest::keyClick(shown.canvas, Qt::Key_A, Qt::ControlModifier); QCOMPARE(triggered, 1); beginTextAt(shown.session, QPointF(20, 30)); QTest::keyClicks(shown.canvas, QStringLiteral("all")); QTest::keyClick(shown.canvas, Qt::Key_A, Qt::ControlModifier); QCOMPARE(triggered, 1); QCOMPARE(shown.editor().anchor(), 0); QCOMPARE(shown.editor().caretPosition(), 3); // Shift-Tab keeps the keys where the text is. auto *next = new QPushButton(&shown.window); next->show(); QTest::keyClick(shown.canvas, Qt::Key_Backtab, Qt::ShiftModifier); QVERIFY(shown.canvas->hasFocus()); QCOMPARE(shown.session.textDraft().value().style.content, QString("all")); // So do its other keys, and Backspace over the fills. int stolen = 0; const auto steal = [&](const QList &keys) { auto *action = new QAction(&shown.window); action->setShortcuts(keys); shown.window.addAction(action); QObject::connect(action, &QAction::triggered, [&] { ++stolen; }); }; for (const QKeySequence::StandardKey key : {QKeySequence::Undo, QKeySequence::Redo, QKeySequence::Copy, QKeySequence::Cut, QKeySequence::Paste}) steal(QKeySequence::keyBindings(key)); steal({QKeySequence(Qt::ALT | Qt::Key_Backspace), QKeySequence(Qt::CTRL | Qt::Key_Backspace), QKeySequence(Qt::SHIFT | Qt::Key_Backspace)}); QTest::keyClick(shown.canvas, Qt::Key_End); QTest::keyClick(shown.canvas, Qt::Key_Left, Qt::ShiftModifier); QTest::keyClick(shown.canvas, Qt::Key_C, Qt::ControlModifier); QTest::keyClick(shown.canvas, Qt::Key_X, Qt::ControlModifier); QTest::keyClick(shown.canvas, Qt::Key_V, Qt::ControlModifier); QTest::keyClick(shown.canvas, Qt::Key_Z, Qt::ControlModifier); QTest::keyClick(shown.canvas, Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier); QCOMPARE(shown.session.textDraft().value().style.content, QString("all")); QTest::keyClick(shown.canvas, Qt::Key_Backspace, Qt::ShiftModifier); QTest::keyClick(shown.canvas, Qt::Key_Backspace, Qt::AltModifier); QTest::keyClick(shown.canvas, Qt::Key_Backspace, Qt::ControlModifier); QCOMPARE(shown.session.textDraft().value().style.content, QString()); QCOMPARE(stolen, 0); } void InlineTextDrawTests::theEditMenuReachesOpenText() { Bar bar; bar.window.show(); QVERIFY(QTest::qWaitForWindowActive(&bar.window)); EditorSession &session = bar.session(); session.createDocument(400, 300, true); session.selectTool(NavigationTool::type); beginTextAt(session, QPointF(20, 30)); auto *canvas = bar.window.findChild(); QTRY_VERIFY(canvas->hasFocus()); QTest::keyClicks(canvas, QStringLiteral("menu")); const auto content = [&] { return session.textDraft().value().style.content; }; // Swift's text branch: bare entries, enabled, the text view's. QCOMPARE(bar.action("undo").text(), QString("Undo")); QVERIFY(bar.action("undo").isEnabled() && bar.action("redo").isEnabled()); bar.action("undo").trigger(); QCOMPARE(content(), QString()); bar.action("redo").trigger(); QCOMPARE(content(), QString("menu")); bar.action("selectAll").trigger(); QCOMPARE(canvas->inlineTextEditor()->anchor(), 0); QVERIFY(!session.selection()); bar.action("copy").trigger(); QCOMPARE(QGuiApplication::clipboard()->text(), QString("menu")); bar.action("cut").trigger(); QCOMPARE(content(), QString()); bar.action("paste").trigger(); bar.action("paste").trigger(); QCOMPARE(content(), QString("menumenu")); // Closed, the entries carry the step's name again. QVERIFY(session.finishText()); QCOMPARE(bar.action("undo").text(), QString("Undo New Text Layer")); } void InlineTextDrawTests::editsRepaintTheEditor() { DrawnText shown; // A field keeps the keys: no blink repaints the edits. auto *field = new QLineEdit(&shown.window); field->show(); field->setFocus(); QTRY_VERIFY(field->hasFocus()); beginTextAt(shown.session, QPointF(20, 30)); QTest::qWait(50); PaintSpy spy(*shown.canvas); QTest::keyClicks(shown.canvas, QStringLiteral("Wide words")); QTRY_VERIFY(spy.painted.contains(shown.editor().drawnRect().toAlignedRect() & shown.canvas->rect())); // Closing new text repaints where it stood. const QRect stood = shown.editor().drawnRect().toAlignedRect() & shown.canvas->rect(); spy.painted = QRect(); shown.session.cancelText(); QTRY_VERIFY(spy.painted.contains(stood)); // A paste grows the box past its old margins. beginTextAt(shown.session, QPointF(20, 30)); QTest::qWait(50); QVERIFY(!shown.canvas->findChild(QStringLiteral("caretBlink"))->isActive()); spy.painted = QRect(); QGuiApplication::clipboard()->setText(QStringLiteral("iiiiiiiiiiiiiiii")); QTest::keyClick(shown.canvas, Qt::Key_V, Qt::ControlModifier); QTRY_VERIFY(spy.painted.contains(shown.editor().drawnRect().toAlignedRect() & shown.canvas->rect())); shown.session.cancelText(); // A layer whose text shrank shows whole again when left. const QUuid id = shown.text(QPointF(20, 30), QStringLiteral("Hi you all")); const QPoint right = layerWith(shown.session, id).transform.point(QPointF(0.95, 0.5)).toPoint(); const auto shrink = [&] { shown.session.editActiveText(); QTest::keyClick(shown.canvas, Qt::Key_End); QTest::keyClick(shown.canvas, Qt::Key_Backspace, Qt::AltModifier); QTest::keyClick(shown.canvas, Qt::Key_Backspace, Qt::AltModifier); QVERIFY(!shown.editor().drawnRect().contains(right)); QTest::qWait(50); spy.painted = QRect(); }; shrink(); shown.session.cancelText(); QTRY_VERIFY(spy.painted.contains(right)); // So does one whose draft another replaces. shrink(); TextDraft other = shown.session.textDraft().value(); other.id = QUuid::createUuid(); other.layerID = std::nullopt; other.transform = std::nullopt; other.origin = QPointF(20, 200); shown.session.setTextDraft(other); QTRY_VERIFY(spy.painted.contains(right)); // The caret's line after a final newline falls far below. shown.session.cancelText(); beginTextAt(shown.session, QPointF(20, 20)); shown.session.changeTextStyle([](LayerTextStyle &style) { style.fontSize = 24; style.leading = 120; }); QTest::keyClicks(shown.canvas, QStringLiteral("A")); QTest::qWait(50); spy.painted = QRect(); QTest::keyClick(shown.canvas, Qt::Key_Return); const QPoint below = shown.editor().textTransform().map(QPointF(LayerTextStyle::padding, LayerTextStyle::padding + 180)).toPoint(); QVERIFY(below.y() > shown.editor().textTransform().map(QPointF(0, shown.editor().logicalSize().height())).y() + 40); QTRY_VERIFY(spy.painted.contains(below)); } void InlineTextDrawTests::openingTextRepaintsItsWholeLayer() { DrawnText shown; beginTextAt(shown.session, QPointF(20, 20)); shown.session.changeTextStyle([](LayerTextStyle &style) { style.fontSize = 24; }); QTest::keyClicks(shown.canvas, QStringLiteral("Tall")); QVERIFY(shown.session.finishText()); const QUuid id = shown.session.activeLayerID().value(); const QSize pixels = layerWith(shown.session, id).asset.value().size(); rewrite(shown.session, [&](ProjectSnapshot &snapshot) { record(snapshot, id).transform = LayerTransform{.origin = {20, 20}, .size = QSizeF(pixels.width(), pixels.height() * 5)}; }); // Point text shows at the width's scale; the rest goes. const QPoint low = layerWith(shown.session, id).transform.point(QPointF(0.5, 0.9)).toPoint(); QTest::qWait(50); PaintSpy spy(*shown.canvas); shown.session.editActiveText(); QVERIFY(!shown.editor().drawnRect().contains(low)); QTRY_VERIFY(spy.painted.contains(low)); } QTEST_MAIN(InlineTextDrawTests) #include "InlineTextDrawTests.moc"