Files
OmaPhoto/tests/InlineTextInputTests.cpp
T
雷电芽衣andClaude c864304695
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 00:44:52 -04:00

380 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "InlineTextFixtures.h"
#include <QClipboard>
#include <QLineEdit>
// What types, the cap, the clipboard and input methods.
class InlineTextInputTests : public QObject {
Q_OBJECT
private slots:
void keysTypeAsQtsTextFieldsJudge();
void theTextHoldsAHundredThousandUnits();
void theClipboardTakesPlainText();
void inputMethodsComposeInTheDraft();
void inputMethodsReplaceOnlyWhatTheyName();
void inputMethodsFollowTheCaret();
void aPreeditLeavesTheHistoryAlone();
void theEditMenuWorksAroundAPreedit();
};
void InlineTextInputTests::keysTypeAsQtsTextFieldsJudge()
{
TextCanvas shown;
// A joiner types, even under Ctrl and Shift.
const QString joiner(QChar(0x200D));
shown.typeText(joiner, Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(shown.content(), joiner);
// Ctrl alone or with Shift commands; AltGr's Ctrl types.
shown.key(Qt::Key_K, Qt::ControlModifier);
shown.key(Qt::Key_K, Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(shown.content(), joiner);
shown.key(Qt::Key_K, Qt::ControlModifier | Qt::AltModifier);
QCOMPARE(shown.content(), joiner + QStringLiteral("k"));
// Shift-+ and Shift-− type here, as in any text view.
QTRY_VERIFY(shown.canvas->hasFocus());
shown.key(Qt::Key_Plus, Qt::ShiftModifier);
shown.key(Qt::Key_Underscore, Qt::ShiftModifier);
QCOMPARE(shown.content(), joiner + QStringLiteral("k+_"));
QCOMPARE(shown.session.activeLayer().value().blendMode, LayerBlendMode::normal);
}
void InlineTextInputTests::theTextHoldsAHundredThousandUnits()
{
TextCanvas shown;
TextDraft draft = shown.session.textDraft().value();
draft.style.content = QString(99'999, QLatin1Char('a'));
shown.session.setTextDraft(draft);
shown.key(Qt::Key_End, Qt::ControlModifier);
shown.type(QStringLiteral("bc"));
QCOMPARE(shown.content().size(), 100'000);
QVERIFY(shown.content().endsWith(QLatin1Char('b')));
// Replacing keeps within the cap too.
QGuiApplication::clipboard()->setText(QStringLiteral("xyz"));
shown.key(Qt::Key_Left, Qt::ShiftModifier);
shown.key(Qt::Key_V, Qt::ControlModifier);
QCOMPARE(shown.content().size(), 100'000);
QVERIFY(shown.content().endsWith(QLatin1Char('b')));
// So does an input method's text.
shown.commit(QStringLiteral("zz"));
QCOMPARE(shown.content().size(), 100'000);
QVERIFY(shown.content().endsWith(QLatin1Char('b')));
}
void InlineTextInputTests::theClipboardTakesPlainText()
{
TextCanvas shown;
shown.type(QStringLiteral("copy me"));
shown.key(Qt::Key_A, Qt::ControlModifier);
QCOMPARE(shown.selection(), (TextRange{0, 7}));
shown.key(Qt::Key_C, Qt::ControlModifier);
QCOMPARE(QGuiApplication::clipboard()->text(), QString("copy me"));
shown.key(Qt::Key_End);
shown.key(Qt::Key_Left, Qt::ControlModifier | Qt::ShiftModifier);
shown.key(Qt::Key_X, Qt::ControlModifier);
QCOMPARE(QGuiApplication::clipboard()->text(), QString("me"));
QCOMPARE(shown.content(), QString("copy "));
QMimeData *rich = new QMimeData;
rich->setHtml(QStringLiteral("<b>bold</b>"));
rich->setText(QStringLiteral("plain"));
QGuiApplication::clipboard()->setMimeData(rich);
shown.key(Qt::Key_V, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("copy plain"));
// Each paste is its own step.
shown.key(Qt::Key_V, Qt::ControlModifier);
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("copy plain"));
// Nothing selected, nothing cut, and the redo lasts.
shown.key(Qt::Key_X, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("copy plain"));
QCOMPARE(QGuiApplication::clipboard()->text(), QString("plain"));
shown.key(Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(shown.content(), QString("copy plainplain"));
// Shift-Delete is the platform's Cut, never a deletion.
QVERIFY(QKeySequence::keyBindings(QKeySequence::Cut).contains(QKeySequence(Qt::SHIFT | Qt::Key_Delete)));
shown.key(Qt::Key_Home);
shown.key(Qt::Key_Delete, Qt::ShiftModifier);
QCOMPARE(shown.content(), QString("copy plainplain"));
shown.key(Qt::Key_End);
shown.key(Qt::Key_Left, Qt::ShiftModifier);
shown.key(Qt::Key_Delete, Qt::ShiftModifier);
QCOMPARE(QGuiApplication::clipboard()->text(), QString("n"));
QCOMPARE(shown.content(), QString("copy plainplai"));
}
void InlineTextInputTests::inputMethodsComposeInTheDraft()
{
TextCanvas shown;
QVERIFY(shown.canvas->testAttribute(Qt::WA_InputMethodEnabled));
shown.type(QStringLiteral("x"));
shown.compose(QStringLiteral("ni"), {QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, 1, 1)});
// Marked text is in the draft, as in NSTextView's storage.
QCOMPARE(shown.content(), QString("xni"));
QCOMPARE(shown.editor().marked(), std::optional(TextRange{1, 3}));
QCOMPARE(shown.caret(), 2);
const QWidget &widget = *shown.canvas;
// Input methods see the text around, without their own.
QCOMPARE(widget.inputMethodQuery(Qt::ImSurroundingText).toString(), QString("x"));
QCOMPARE(widget.inputMethodQuery(Qt::ImCursorPosition).toInt(), 1);
shown.commit(QStringLiteral("你"));
QCOMPARE(shown.content(), QString("x你"));
QVERIFY(!shown.editor().marked());
QCOMPARE(shown.caret(), 2);
QCOMPARE(widget.inputMethodQuery(Qt::ImSurroundingText).toString(), QString("x你"));
QCOMPARE(widget.inputMethodQuery(Qt::ImCursorPosition).toInt(), 2);
QVERIFY(widget.inputMethodQuery(Qt::ImHints).toInt() & Qt::ImhMultiLine);
const QRectF caret = widget.inputMethodQuery(Qt::ImCursorRectangle).toRectF();
QVERIFY(caret.left() > 20 + 12 && caret.height() > 80);
// The typing and the composed word undo together.
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString());
// After a move, composing starts a step of its own.
shown.type(QStringLiteral("a"));
shown.key(Qt::Key_Left);
shown.compose(QStringLiteral("b"));
shown.commit(QStringLiteral("b"));
QCOMPARE(shown.content(), QString("ba"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("a"));
// A paste lands before the preedit, which follows, as Qt's.
shown.compose(QStringLiteral("ni"));
QGuiApplication::clipboard()->setText(QStringLiteral("P"));
shown.key(Qt::Key_V, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("Pnia"));
QCOMPARE(shown.editor().marked(), std::optional(TextRange{1, 3}));
// An empty preedit ends the composing.
shown.compose(QStringLiteral("ni"));
shown.compose(QString());
QCOMPARE(shown.content(), QString("Pa"));
QVERIFY(!shown.editor().marked());
// A preedit records nothing: undo takes the last step back.
shown.key(Qt::Key_End);
shown.compose(QStringLiteral("n"));
shown.key(Qt::Key_Left);
shown.compose(QStringLiteral("ni"));
QCOMPARE(shown.content(), QString("Pani"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("a"));
QVERIFY(!shown.editor().marked());
shown.key(Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(shown.content(), QString("Pa"));
shown.session.cancelText();
QVERIFY(!shown.canvas->testAttribute(Qt::WA_InputMethodEnabled));
}
void InlineTextInputTests::inputMethodsReplaceOnlyWhatTheyName()
{
TextCanvas shown;
// A commit alone types; one may replace text before.
shown.commit(QStringLiteral("helo"));
QCOMPARE(shown.content(), QString("helo"));
shown.commit(QStringLiteral("hello"), -4, 4);
QCOMPARE(shown.content(), QString("hello"));
QCOMPARE(shown.caret(), 5);
shown.commit(QString(), -1, 1);
QCOMPARE(shown.content(), QString("hell"));
shown.key(Qt::Key_Left);
shown.commit(QString(), -1, 1);
QCOMPARE(shown.content(), QString("hel"));
QCOMPARE(shown.caret(), 2);
// Qt removes a range whole or not at all.
shown.commit(QString(), -1, 5);
QCOMPARE(shown.content(), QString("hel"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("hell"));
shown.key(Qt::Key_End);
// One typing step, as keys make; input drops the redo.
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString());
shown.commit(QStringLiteral("h"));
shown.key(Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(shown.content(), QString("h"));
shown.commit(QStringLiteral("ell"));
// Nothing inserted or removed changes nothing.
shown.key(Qt::Key_A, Qt::ControlModifier);
shown.compose(QString());
QCOMPARE(shown.content(), QString("hell"));
QCOMPARE(shown.selection(), (TextRange{0, 4}));
shown.key(Qt::Key_Home);
shown.commit(QString(), 9, 1);
QCOMPARE(shown.caret(), 0);
QCOMPARE(shown.content(), QString("hell"));
// While composing, a replacement counts from the preedit.
shown.key(Qt::Key_End);
shown.compose(QStringLiteral("ni"));
shown.commit(QStringLiteral("X"), -2, 2);
QCOMPARE(shown.content(), QString("heX"));
QVERIFY(!shown.editor().marked());
QCOMPARE(shown.caret(), 3);
// A replacement of nothing still places what it inserts.
shown.commit(QStringLiteral("Y"), -1, 0);
QCOMPARE(shown.content(), QString("heYX"));
QCOMPARE(shown.caret(), 3);
// An input method may set the selection alone.
shown.key(Qt::Key_Left, Qt::ShiftModifier);
shown.compose(QString(), {QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 1, 2, QVariant())});
QCOMPARE(shown.selection(), (TextRange{1, 3}));
QCOMPARE(shown.content(), QString("heYX"));
// Input takes a selection first, then counts from there.
shown.commit(QStringLiteral("Z"), 1, 0);
QCOMPARE(shown.content(), QString("hXZ"));
QCOMPARE(shown.selection(), (TextRange{3, 3}));
// A selection beside a preedit goes with the next input.
TextDraft draft = shown.session.textDraft().value();
draft.style.content = QStringLiteral("abcdef");
shown.session.setTextDraft(draft);
shown.key(Qt::Key_End);
shown.compose(QStringLiteral("ni"), {QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 1, 2, QVariant())});
QCOMPARE(shown.content(), QString("abcnidef"));
QCOMPARE(shown.editor().inputMethodQuery(Qt::ImCurrentSelection).toString(), QString("bc"));
QCOMPARE(shown.editor().inputMethodQuery(Qt::ImAnchorPosition).toInt(), 1);
QCOMPARE(shown.editor().inputMethodQuery(Qt::ImCursorPosition).toInt(), 3);
// Its updates move the caret and keep that selection.
shown.compose(QStringLiteral("ni"), {QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, 0, 1, QVariant())});
QCOMPARE(shown.caret(), 3);
QCOMPARE(shown.editor().inputMethodQuery(Qt::ImCurrentSelection).toString(), QString("bc"));
shown.commit(QStringLiteral("X"));
QCOMPARE(shown.content(), QString("aXdef"));
// Its own move ends a typing run, as keys do.
shown.commit(QStringLiteral("abc"));
shown.compose(QString(), {QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 0, 0, QVariant())});
shown.commit(QStringLiteral("Q"));
QCOMPARE(shown.content(), QString("QaXabcdef"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("aXabcdef"));
// Queries read it in the text without the preedit.
shown.key(Qt::Key_Home);
shown.compose(QStringLiteral("ni"), {QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 3, -2, QVariant())});
QCOMPARE(shown.content(), QString("aniXabcdef"));
QCOMPARE(shown.editor().inputMethodQuery(Qt::ImCurrentSelection).toString(), QString("Xa"));
}
void InlineTextInputTests::inputMethodsFollowTheCaret()
{
TextCanvas shown;
QTRY_VERIFY(shown.canvas->hasFocus());
// Edits, moves and zooms move the candidates, as microfocus.
QSignalSpy moved(QGuiApplication::inputMethod(), &QInputMethod::cursorRectangleChanged);
shown.type(QStringLiteral("a"));
QVERIFY(!moved.isEmpty());
moved.clear();
shown.key(Qt::Key_Left);
QVERIFY(!moved.isEmpty());
moved.clear();
shown.session.zoom(2);
QVERIFY(!moved.isEmpty());
// Without the keys the canvas leaves input methods be.
auto *field = new QLineEdit(&shown.window);
field->show();
field->setFocus();
QTRY_VERIFY(field->hasFocus());
moved.clear();
shown.session.zoom(1);
shown.editor().selectAll();
QVERIFY(moved.isEmpty());
}
void InlineTextInputTests::aPreeditLeavesTheHistoryAlone()
{
TextCanvas shown;
// Qt's contract: a cancelled composition keeps the redo.
shown.type(QStringLiteral("abc"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
shown.compose(QStringLiteral("ni"));
shown.compose(QString());
QCOMPARE(shown.content(), QString());
shown.key(Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(shown.content(), QString("abc"));
// A commit is one step, from the text before composing.
shown.key(Qt::Key_End);
shown.compose(QStringLiteral("x"));
shown.compose(QStringLiteral("xy"));
shown.commit(QStringLiteral("XY"));
QCOMPARE(shown.content(), QString("abcXY"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("abc"));
shown.key(Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(shown.content(), QString("abcXY"));
// A preedit takes a selection in a typing step.
shown.key(Qt::Key_A, Qt::ControlModifier);
shown.compose(QStringLiteral("q"));
shown.compose(QString());
QCOMPARE(shown.content(), QString());
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("abcXY"));
// A partial commit lands; what follows counts from there.
shown.key(Qt::Key_End);
shown.compose(QStringLiteral("mn"));
QInputMethodEvent partial(QStringLiteral("n"), {});
partial.setCommitString(QStringLiteral("M"));
QApplication::sendEvent(shown.canvas, &partial);
shown.key(Qt::Key_Left);
shown.commit(QStringLiteral("N"));
QCOMPARE(shown.content(), QString("abcXYMN"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("abcXYM"));
}
void InlineTextInputTests::theEditMenuWorksAroundAPreedit()
{
TextCanvas shown;
const auto composing = [&] {
TextDraft draft = shown.session.textDraft().value();
draft.style.content = QStringLiteral("abcdef");
shown.session.setTextDraft(draft);
shown.key(Qt::Key_End);
shown.compose(QStringLiteral("ni"), {QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 1, 2, QVariant())});
};
// Copy and Cut take the selection beside it, as Qt's.
composing();
QGuiApplication::clipboard()->setText(QStringLiteral("sentinel"));
shown.editor().copy();
QCOMPARE(QGuiApplication::clipboard()->text(), QString("bc"));
shown.editor().cut();
QCOMPARE(shown.content(), QString("anidef"));
QCOMPARE(shown.editor().marked(), std::optional(TextRange{1, 3}));
QCOMPARE(shown.caret(), 3);
shown.commit(QStringLiteral("Y"));
QCOMPARE(shown.content(), QString("aYdef"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("adef"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("abcdef"));
// Paste replaces it; the preedit follows the pasted text.
composing();
QGuiApplication::clipboard()->setText(QStringLiteral("P"));
shown.editor().paste();
QCOMPARE(shown.content(), QString("aPnidef"));
QCOMPARE(shown.editor().marked(), std::optional(TextRange{2, 4}));
shown.commit(QStringLiteral("X"));
QCOMPARE(shown.content(), QString("aPXdef"));
// Select All selects around it; the next input takes it.
composing();
shown.editor().selectAll();
QCOMPARE(shown.content(), QString("abcdefni"));
QCOMPARE(shown.editor().inputMethodQuery(Qt::ImCurrentSelection).toString(), QString("abcdef"));
shown.commit(QStringLiteral("X"));
QCOMPARE(shown.content(), QString("X"));
// It ends a typing run, a preedit showing or not.
shown.type(QStringLiteral("y"));
shown.compose(QStringLiteral("ni"));
shown.editor().selectAll();
shown.commit(QStringLiteral("Z"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("Xy"));
// A paste is its own step and drops the redo.
shown.key(Qt::Key_End);
shown.type(QStringLiteral("c"));
QGuiApplication::clipboard()->setText(QStringLiteral("P"));
shown.key(Qt::Key_V, Qt::ControlModifier);
shown.type(QStringLiteral("b"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(shown.content(), QString("XycP"));
shown.key(Qt::Key_Z, Qt::ControlModifier);
QGuiApplication::clipboard()->setText(QStringLiteral("Q"));
shown.key(Qt::Key_V, Qt::ControlModifier);
shown.key(Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(shown.content(), QString("XycQ"));
}
QTEST_MAIN(InlineTextInputTests)
#include "InlineTextInputTests.moc"