Clicked text on its baseline, live text on the Move tool's double click, SVG import (12.11c)
This commit is contained in:
1 parent
008493b0a3
commit
8fbb104c48
34 files changed
+572
-47
No files matched your search
@@ -323,7 +323,7 @@ void ContentViewTests::theImporterPicksFilesAndClearsItsFlag()
|
||||
QTest::qWait(50);
|
||||
QCOMPARE(int(session.document().value().layers.size()), 2);
|
||||
desk.typeBeforeCancel.clear();
|
||||
const QString panel = "panel|Open|open|file|||1 many Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd *.psb " + RawImporter::globs().join(" ") + ")";
|
||||
const QString panel = "panel|Open|open|file|||1 many Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd *.psb *.svg *.svgz " + RawImporter::globs().join(" ") + ")";
|
||||
QCOMPARE(desk.seen.mid(0, 2), (QStringList{panel, panel}));
|
||||
QCOMPARE(desk.seen.size(), 3);
|
||||
// The flag cleared from elsewhere takes the panel down.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QMimeDatabase>
|
||||
#include <QtTest>
|
||||
|
||||
// The desktop entry, the project type, and an install.
|
||||
@@ -70,7 +71,12 @@ void DesktopIntegrationTests::theEntryOpensSwiftsDocumentTypes()
|
||||
QCOMPARE(keys.value("Terminal"), QString("false"));
|
||||
QVERIFY(keys.value("Categories").split(QLatin1Char(';')).contains("Graphics"));
|
||||
// Swift's two document types: its projects, then four pictures.
|
||||
QCOMPARE(keys.value("MimeType"), QString("application/x-compositor-project;image/png;image/jpeg;image/heic;image/tiff;image/vnd.adobe.photoshop;image/x-photoshop-large-image;image/x-dcraw;"));
|
||||
QCOMPARE(keys.value("MimeType"), QString("application/x-compositor-project;image/png;image/jpeg;image/heic;image/tiff;image/vnd.adobe.photoshop;image/x-photoshop-large-image;image/svg+xml;image/svg+xml-compressed;image/x-dcraw;"));
|
||||
// Each SVG suffix's type as the system names it.
|
||||
for (const char *name : {"icon.svg", "icon.svgz"}) {
|
||||
const QString type = QMimeDatabase().mimeTypeForFile(QString::fromLatin1(name), QMimeDatabase::MatchExtension).name();
|
||||
QVERIFY2(keys.value("MimeType").split(';').contains(type), qPrintable(type));
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopIntegrationTests::aProjectFolderIsAType()
|
||||
|
||||
@@ -102,7 +102,7 @@ void InlineTextDrawTests::theCaretShowsWhileTheCanvasHasFocus()
|
||||
field->show();
|
||||
shown.canvas->setFocus();
|
||||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
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()))
|
||||
@@ -149,7 +149,7 @@ void InlineTextDrawTests::aSelectionShowsBehindItsText()
|
||||
DrawnText shown;
|
||||
shown.canvas->setFocus();
|
||||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("iiii"));
|
||||
QTest::keyClick(shown.canvas, Qt::Key_A, Qt::ControlModifier);
|
||||
// Above the letters, the line's height is highlighted.
|
||||
@@ -204,13 +204,13 @@ void InlineTextDrawTests::aFreshDraftTakesTheKeysButSparesAField()
|
||||
button->show();
|
||||
field->setFocus();
|
||||
QTRY_VERIFY(field->hasFocus());
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::qWait(50);
|
||||
QVERIFY(field->hasFocus());
|
||||
shown.session.cancelText();
|
||||
button->setFocus();
|
||||
QTRY_VERIFY(button->hasFocus());
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||||
// A focus request leaves open text's keys alone.
|
||||
button->setFocus();
|
||||
@@ -235,7 +235,7 @@ void InlineTextDrawTests::theDraftTakesItsKeysAheadOfShortcuts()
|
||||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||||
QTest::keyClick(shown.canvas, Qt::Key_A, Qt::ControlModifier);
|
||||
QCOMPARE(triggered, 1);
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("all"));
|
||||
QTest::keyClick(shown.canvas, Qt::Key_A, Qt::ControlModifier);
|
||||
QCOMPARE(triggered, 1);
|
||||
@@ -281,7 +281,7 @@ void InlineTextDrawTests::theEditMenuReachesOpenText()
|
||||
EditorSession &session = bar.session();
|
||||
session.createDocument(400, 300, true);
|
||||
session.selectTool(NavigationTool::type);
|
||||
session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(session, QPointF(20, 30));
|
||||
auto *canvas = bar.window.findChild<CanvasView *>();
|
||||
QTRY_VERIFY(canvas->hasFocus());
|
||||
QTest::keyClicks(canvas, QStringLiteral("menu"));
|
||||
@@ -316,7 +316,7 @@ void InlineTextDrawTests::editsRepaintTheEditor()
|
||||
field->show();
|
||||
field->setFocus();
|
||||
QTRY_VERIFY(field->hasFocus());
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::qWait(50);
|
||||
PaintSpy spy(*shown.canvas);
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("Wide words"));
|
||||
@@ -327,7 +327,7 @@ void InlineTextDrawTests::editsRepaintTheEditor()
|
||||
shown.session.cancelText();
|
||||
QTRY_VERIFY(spy.painted.contains(stood));
|
||||
// A paste grows the box past its old margins.
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::qWait(50);
|
||||
QVERIFY(!shown.canvas->findChild<QTimer *>(QStringLiteral("caretBlink"))->isActive());
|
||||
spy.painted = QRect();
|
||||
@@ -361,7 +361,7 @@ void InlineTextDrawTests::editsRepaintTheEditor()
|
||||
QTRY_VERIFY(spy.painted.contains(right));
|
||||
// The caret's line after a final newline falls far below.
|
||||
shown.session.cancelText();
|
||||
shown.session.beginText(QPointF(20, 20), true);
|
||||
beginTextAt(shown.session, QPointF(20, 20));
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) {
|
||||
style.fontSize = 24;
|
||||
style.leading = 120;
|
||||
@@ -378,7 +378,7 @@ void InlineTextDrawTests::editsRepaintTheEditor()
|
||||
void InlineTextDrawTests::openingTextRepaintsItsWholeLayer()
|
||||
{
|
||||
DrawnText shown;
|
||||
shown.session.beginText(QPointF(20, 20), true);
|
||||
beginTextAt(shown.session, QPointF(20, 20));
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.fontSize = 24; });
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("Tall"));
|
||||
QVERIFY(shown.session.finishText());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "SelectionCanvasFixtures.h"
|
||||
#include "Rendering/TextLayout.h"
|
||||
#include <QInputMethodEvent>
|
||||
|
||||
// Shared by the inline editor's key and input tests.
|
||||
@@ -11,7 +12,7 @@ struct TextCanvas : Canvas {
|
||||
QObject::connect(&session, &EditorSession::changed, canvas, [this] { canvas->synchronizeDisplay(); });
|
||||
session.addBlankLayer();
|
||||
session.selectTool(NavigationTool::type);
|
||||
session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(session, QPointF(20, 30));
|
||||
}
|
||||
InlineTextEditor &editor() const
|
||||
{
|
||||
@@ -20,6 +21,12 @@ struct TextCanvas : Canvas {
|
||||
return *canvas->inlineTextEditor();
|
||||
}
|
||||
QString content() const { return session.textDraft().value().style.content; }
|
||||
// Where a click puts new text's corner: baseline at it.
|
||||
QPointF clickedCorner(QPointF point) const
|
||||
{
|
||||
const double padding = LayerTextStyle::padding;
|
||||
return point - QPointF(padding, padding + TextLines(session.textDraft().value().style, QSizeF(100'000, 100'000)).baseline(0));
|
||||
}
|
||||
int caret() const { return editor().caretPosition(); }
|
||||
void key(Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier) { QTest::keyClick(canvas, key, modifiers); }
|
||||
void type(const QString &text) { QTest::keyClicks(canvas, text); }
|
||||
|
||||
@@ -47,7 +47,7 @@ void InlineTextPointerTests::aClickOrADragBeginsText()
|
||||
shown.session.cancelText();
|
||||
// A click is Swift's small box: point text there.
|
||||
shown.click(QPointF(50, 60));
|
||||
QCOMPARE(shown.session.textDraft().value().origin, QPointF(50, 60));
|
||||
QCOMPARE(shown.session.textDraft().value().origin, shown.clickedCorner(QPointF(50, 60)));
|
||||
QVERIFY(!shown.session.textDraft().value().style.boxSize);
|
||||
shown.session.cancelText();
|
||||
// A drag draws a box, shown in the accent meanwhile.
|
||||
@@ -112,7 +112,7 @@ void InlineTextPointerTests::aClickOpensTheTextUnderIt()
|
||||
const size_t count = shown.session.document().value().layers.size();
|
||||
shown.click(QPointF(300, 250));
|
||||
QCOMPARE(shown.session.document().value().layers.size(), count + 1);
|
||||
QCOMPARE(shown.session.textDraft().value().origin, QPointF(300, 250));
|
||||
QCOMPARE(shown.session.textDraft().value().origin, shown.clickedCorner(QPointF(300, 250)));
|
||||
shown.session.cancelText();
|
||||
// A click opens text, active or not, the caret there.
|
||||
const QUuid kept = shown.session.document().value().layers.back().id;
|
||||
@@ -287,7 +287,7 @@ void InlineTextPointerTests::aLostReleaseDropsTheBox()
|
||||
shown.press(QPointF(300, 250));
|
||||
QTRY_VERIFY(press.painted.contains(QPoint(40, 80)));
|
||||
shown.release(QPointF(300, 250));
|
||||
QCOMPARE(shown.session.textDraft().value().origin, QPointF(300, 250));
|
||||
QCOMPARE(shown.session.textDraft().value().origin, shown.clickedCorner(QPointF(300, 250)));
|
||||
QVERIFY(!shown.session.textDraft().value().style.boxSize);
|
||||
shown.session.cancelText();
|
||||
// So does a lost focus.
|
||||
|
||||
@@ -11,7 +11,7 @@ private slots:
|
||||
void InlineTextPreeditTests::markedTextIsUnderlined()
|
||||
{
|
||||
DrawnText shown;
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QInputMethodEvent composing(QStringLiteral("nn"), {});
|
||||
QApplication::sendEvent(shown.canvas, &composing);
|
||||
// Below the baseline, where no letter of these reaches.
|
||||
@@ -48,7 +48,7 @@ void InlineTextPreeditTests::theInputMethodStylesItsPreedit()
|
||||
DrawnText shown;
|
||||
shown.canvas->setFocus();
|
||||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
// Its clause's format replaces the underline; its caret hides.
|
||||
QTextCharFormat clause;
|
||||
clause.setBackground(Qt::yellow);
|
||||
@@ -63,14 +63,14 @@ void InlineTextPreeditTests::theInputMethodStylesItsPreedit()
|
||||
QCOMPARE(shown.grab().pixelColor(above), QColor(Qt::yellow));
|
||||
// Formats count from the preedit, wherever it sits.
|
||||
shown.session.cancelText();
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("nn"));
|
||||
QApplication::sendEvent(shown.canvas, &composing);
|
||||
QVERIFY(shown.grab().pixelColor(above) != QColor(Qt::yellow));
|
||||
const QPoint later = shown.editor().textTransform().map(QPointF(padding + width * 1.5, baseline - metrics.xHeight() - 4)).toPoint();
|
||||
QCOMPARE(shown.grab().pixelColor(later), QColor(Qt::yellow));
|
||||
shown.session.cancelText();
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QApplication::sendEvent(shown.canvas, &composing);
|
||||
const QRect band = shown.editor().textTransform().mapRect(QRectF(padding, baseline + 1, width - 4, metrics.descent() - 2)).toAlignedRect();
|
||||
QVERIFY(!where(shown.grab().copy(band), reddish).isValid());
|
||||
@@ -85,7 +85,7 @@ void InlineTextPreeditTests::theInputMethodStylesItsPreedit()
|
||||
QVERIFY(where(shown.grab().copy(band), reddish).isValid());
|
||||
// The input method's selection shows beside its preedit.
|
||||
shown.session.cancelText();
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("xx"));
|
||||
QInputMethodEvent selecting(QStringLiteral("nn"), {QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 0, 2, QVariant())});
|
||||
QApplication::sendEvent(shown.canvas, &selecting);
|
||||
@@ -95,7 +95,7 @@ void InlineTextPreeditTests::theInputMethodStylesItsPreedit()
|
||||
QCOMPARE(shown.grab().pixelColor(first), shown.canvas->palette().color(group, QPalette::Highlight));
|
||||
// Past the preedit, it sits after the preedit's letters.
|
||||
shown.session.cancelText();
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("xx"));
|
||||
QInputMethodEvent ahead(QStringLiteral("nn"), {QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 2, -2, QVariant())});
|
||||
QApplication::sendEvent(shown.canvas, &ahead);
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#include "SelectionCanvasFixtures.h"
|
||||
#include "Rendering/TextLayout.h"
|
||||
#include <QFontMetricsF>
|
||||
|
||||
// Swift 1.2.10 (a5721b2): the Move tool's double click opens text.
|
||||
namespace {
|
||||
// Live text whose box covers `box`, made in one step.
|
||||
QUuid addText(EditorSession &session, const QString &content, QRectF box)
|
||||
{
|
||||
session.selectTool(NavigationTool::type);
|
||||
session.beginText(box);
|
||||
TextDraft draft = session.textDraft().value();
|
||||
draft.style.content = content;
|
||||
if (!session.applyText(draft))
|
||||
throw std::runtime_error("the text was refused");
|
||||
return session.activeLayerID().value();
|
||||
}
|
||||
|
||||
// Opaque red pixels, as a layer's.
|
||||
QImage redPixels(int width, int height)
|
||||
{
|
||||
QImage image = BrushRaster::context(width, height, false);
|
||||
image.fill(Qt::red);
|
||||
return image;
|
||||
}
|
||||
|
||||
// A document point on the canvas's view.
|
||||
QPointF viewPoint(Canvas &shown, QPointF pixel)
|
||||
{
|
||||
return shown.session.viewport.viewPoint(pixel, shown.documentSize());
|
||||
}
|
||||
}
|
||||
|
||||
class LiveTextDoubleClickTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theTopmostShownTextOpens();
|
||||
void otherPressesKeepTheMoveTool();
|
||||
void anOpenDragAndPreviewEndFirst();
|
||||
void theCaretWaitsAfterTheText();
|
||||
void zoomAndTurnsPlaceThePress();
|
||||
};
|
||||
|
||||
void LiveTextDoubleClickTests::theTopmostShownTextOpens()
|
||||
{
|
||||
// No display sync: the gesture opens the editor itself.
|
||||
Canvas shown;
|
||||
const QUuid lower = addText(shown.session, QStringLiteral("Lower"), QRectF(20, 20, 200, 100));
|
||||
const QUuid upper = addText(shown.session, QStringLiteral("Upper"), QRectF(100, 60, 200, 100));
|
||||
shown.session.addBlankLayer();
|
||||
const QUuid blank = shown.session.activeLayerID().value();
|
||||
shown.session.selectTool(NavigationTool::move);
|
||||
// Where both lie, the upper opens, in the Type tool.
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(150, 90)).toPoint());
|
||||
QCOMPARE(shown.session.tool(), NavigationTool::type);
|
||||
QCOMPARE(shown.session.activeLayerID(), std::optional(upper));
|
||||
QCOMPARE(shown.session.textDraft().value().layerID, std::optional(upper));
|
||||
QVERIFY(shown.canvas->inlineTextEditor());
|
||||
shown.session.cancelText();
|
||||
// A hidden text is passed over for the one below.
|
||||
shown.session.selectTool(NavigationTool::move);
|
||||
shown.session.toggleLayerVisibility(upper);
|
||||
shown.session.selectLayer(blank);
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(150, 90)).toPoint());
|
||||
QCOMPARE(shown.session.textDraft().value().layerID, std::optional(lower));
|
||||
QCOMPARE(shown.session.activeLayerID(), std::optional(lower));
|
||||
shown.session.cancelText();
|
||||
// A text in a hidden folder is passed over too.
|
||||
shown.session.selectTool(NavigationTool::move);
|
||||
shown.session.toggleLayerVisibility(upper);
|
||||
shown.session.selectLayer(upper);
|
||||
shown.session.groupSelectedLayers();
|
||||
const QUuid folder = shown.session.activeLayerID().value();
|
||||
QCOMPARE(layerWith(shown.session, upper).parentID, std::optional(folder));
|
||||
shown.session.toggleLayerVisibility(folder);
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(150, 90)).toPoint());
|
||||
QCOMPARE(shown.session.textDraft().value().layerID, std::optional(lower));
|
||||
}
|
||||
|
||||
void LiveTextDoubleClickTests::anOpenDragAndPreviewEndFirst()
|
||||
{
|
||||
// On the text already active, Swift's commitTransform still cleans up.
|
||||
Canvas shown;
|
||||
const QUuid text = addText(shown.session, QStringLiteral("Words"), QRectF(20, 20, 200, 100));
|
||||
shown.session.selectTool(NavigationTool::move);
|
||||
shown.session.beginOpacityEdit();
|
||||
shown.session.setLayerOpacity(0.5);
|
||||
shown.session.previewBlendMode(LayerBlendMode::multiply, text);
|
||||
QVERIFY(!shown.session.canUndo());
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(60, 60)).toPoint());
|
||||
QCOMPARE(shown.session.textDraft().value().layerID, std::optional(text));
|
||||
QVERIFY(shown.session.displayedBlendMode(layerWith(shown.session, text)) == LayerBlendMode::normal);
|
||||
shown.session.cancelText();
|
||||
QVERIFY(shown.session.canUndo());
|
||||
}
|
||||
|
||||
void LiveTextDoubleClickTests::otherPressesKeepTheMoveTool()
|
||||
{
|
||||
Canvas shown;
|
||||
QObject::connect(&shown.session, &EditorSession::changed, shown.canvas, [&shown] { shown.canvas->synchronizeDisplay(); });
|
||||
const QUuid text = addText(shown.session, QStringLiteral("Words"), QRectF(20, 20, 200, 100));
|
||||
shown.session.selectTool(NavigationTool::move);
|
||||
// One click moves; it opens nothing.
|
||||
shown.click(viewPoint(shown, QPointF(60, 60)));
|
||||
QVERIFY(!shown.session.textDraft());
|
||||
QCOMPARE(shown.session.tool(), NavigationTool::move);
|
||||
// Off the text, a double click is the Move tool's.
|
||||
shown.session.addPixelLayer(redPixels(60, 40), QPointF(260, 220), QStringLiteral("Red"), QStringLiteral("Add"));
|
||||
const QUuid red = shown.session.activeLayerID().value();
|
||||
shown.session.selectLayer(text);
|
||||
shown.session.setTransformAutoSelect(true);
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(280, 240)).toPoint());
|
||||
QVERIFY(!shown.session.textDraft());
|
||||
QCOMPARE(shown.session.tool(), NavigationTool::move);
|
||||
QCOMPARE(shown.session.activeLayerID(), std::optional(red));
|
||||
// While a layer is renamed, layers rest: nothing opens.
|
||||
shown.session.setRenamingLayerID(red);
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(60, 60)).toPoint());
|
||||
QVERIFY(!shown.session.textDraft());
|
||||
QCOMPARE(shown.session.activeLayerID(), std::optional(red));
|
||||
shown.session.setRenamingLayerID(std::nullopt);
|
||||
// A busy project opens nothing.
|
||||
shown.session.setIsProjectBusy(true);
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(60, 60)).toPoint());
|
||||
QVERIFY(!shown.session.textDraft());
|
||||
shown.session.setIsProjectBusy(false);
|
||||
// Other tools' double clicks are theirs.
|
||||
shown.session.selectTool(NavigationTool::marquee);
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(60, 60)).toPoint());
|
||||
QVERIFY(!shown.session.textDraft());
|
||||
QCOMPARE(shown.session.activeLayerID(), std::optional(red));
|
||||
}
|
||||
|
||||
void LiveTextDoubleClickTests::theCaretWaitsAfterTheText()
|
||||
{
|
||||
Canvas shown;
|
||||
QObject::connect(&shown.session, &EditorSession::changed, shown.canvas, [&shown] { shown.canvas->synchronizeDisplay(); });
|
||||
addText(shown.session, QStringLiteral("Word"), QRectF(20, 20, 300, 150));
|
||||
shown.session.selectTool(NavigationTool::move);
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(60, 60)).toPoint());
|
||||
QTRY_COMPARE(shown.canvas->inlineTextEditor()->caretPosition(), 4);
|
||||
QCOMPARE(shown.canvas->inlineTextEditor()->anchor(), 4);
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("X"));
|
||||
QCOMPARE(shown.session.textDraft().value().style.content, QString("WordX"));
|
||||
// A caret a click placed stays where it was put.
|
||||
shown.session.cancelText();
|
||||
shown.session.selectTool(NavigationTool::type);
|
||||
const LayerTextStyle style = layerWith(shown.session, shown.session.activeLayerID().value()).liveText().value().style;
|
||||
const QFontMetricsF metrics(TextLayout::font(style));
|
||||
const double padding = LayerTextStyle::padding;
|
||||
const QPointF at = viewPoint(shown, QPointF(20 + padding + metrics.horizontalAdvance(QStringLiteral("Wo")), 20 + padding + style.lineHeight() / 2));
|
||||
shown.click(at);
|
||||
QCOMPARE(shown.canvas->inlineTextEditor()->caretPosition(), 2);
|
||||
QCoreApplication::processEvents();
|
||||
QCOMPARE(shown.canvas->inlineTextEditor()->caretPosition(), 2);
|
||||
QCOMPARE(shown.canvas->inlineTextEditor()->anchor(), 2);
|
||||
// New text keeps its caret, despite opened text's queued step.
|
||||
shown.session.cancelText();
|
||||
shown.session.editActiveText();
|
||||
shown.session.cancelText();
|
||||
TextDraft fresh{.documentID = shown.session.document().value().id, .layerID = std::nullopt, .origin = QPointF(20, 200), .style = style};
|
||||
fresh.style.content = QStringLiteral("New");
|
||||
shown.session.setTextDraft(fresh);
|
||||
QCoreApplication::processEvents();
|
||||
QCOMPARE(shown.session.textDraft().value().id, fresh.id);
|
||||
QCOMPARE(shown.canvas->inlineTextEditor()->caretPosition(), 0);
|
||||
QCOMPARE(shown.canvas->inlineTextEditor()->anchor(), 0);
|
||||
}
|
||||
|
||||
void LiveTextDoubleClickTests::zoomAndTurnsPlaceThePress()
|
||||
{
|
||||
Canvas shown;
|
||||
const QUuid text = addText(shown.session, QStringLiteral("Turned"), QRectF(100, 100, 160, 60));
|
||||
rewrite(shown.session, [&](ProjectSnapshot &snapshot) { record(snapshot, text).transform.rotation = 90; });
|
||||
shown.session.selectTool(NavigationTool::move);
|
||||
shown.session.zoom(1.5);
|
||||
QVERIFY(shown.session.viewport.viewPoint(QPointF(180, 70), shown.documentSize()) != QPointF(180, 70));
|
||||
// Outside the turned box, inside the upright one: nothing.
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(110, 130)).toPoint());
|
||||
QVERIFY(!shown.session.textDraft());
|
||||
shown.release(viewPoint(shown, QPointF(110, 130)));
|
||||
// Inside the turned box only: the text opens.
|
||||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, viewPoint(shown, QPointF(180, 70)).toPoint());
|
||||
QCOMPARE(shown.session.textDraft().value().layerID, std::optional(text));
|
||||
}
|
||||
|
||||
QTEST_MAIN(LiveTextDoubleClickTests)
|
||||
#include "LiveTextDoubleClickTests.moc"
|
||||
@@ -223,7 +223,7 @@ void PickingTests::theInlineEditorStandsAsideForThePicker()
|
||||
Picking shown;
|
||||
QObject::connect(&shown.session, &EditorSession::changed, shown.canvas, [&shown] { shown.canvas->synchronizeDisplay(); });
|
||||
shown.tool(NavigationTool::type);
|
||||
shown.session.beginText(QPointF(20, 30), true);
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("Hello there"));
|
||||
QTest::keyClick(shown.canvas, Qt::Key_Home);
|
||||
const InlineTextEditor &editor = *shown.canvas->inlineTextEditor();
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "BudgetFixtures.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "IO/ImageImporter.h"
|
||||
#include "IO/PSD/PSDReader.h"
|
||||
#include <QColorSpace>
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
#include <zlib.h>
|
||||
|
||||
// Swift 1.2.10 (0534e00): SVG drawn once into an image layer.
|
||||
namespace {
|
||||
// A red rectangle filling a document of the given size.
|
||||
QByteArray svg(const QByteArray &width, const QByteArray &height)
|
||||
{
|
||||
return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + width + "\" height=\"" + height + "\"><rect width=\"100%\" height=\"100%\" fill=\"red\"/></svg>";
|
||||
}
|
||||
|
||||
QString written(const QTemporaryDir &folder, const QString &name, const QByteArray &bytes)
|
||||
{
|
||||
QFile file(folder.filePath(name));
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size())
|
||||
throw std::runtime_error("cannot write a fixture");
|
||||
return file.fileName();
|
||||
}
|
||||
|
||||
// The same bytes in gzip, as `.svgz` holds them.
|
||||
QByteArray gzipped(const QByteArray &bytes)
|
||||
{
|
||||
z_stream stream{};
|
||||
if (deflateInit2(&stream, 9, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) != Z_OK)
|
||||
throw std::runtime_error("no zlib");
|
||||
QByteArray out(int(deflateBound(&stream, uLong(bytes.size()))), '\0');
|
||||
stream.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(bytes.constData()));
|
||||
stream.avail_in = uInt(bytes.size());
|
||||
stream.next_out = reinterpret_cast<Bytef *>(out.data());
|
||||
stream.avail_out = uInt(out.size());
|
||||
deflate(&stream, Z_FINISH);
|
||||
out.resize(qsizetype(stream.total_out));
|
||||
deflateEnd(&stream);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<ImageImportError::Kind> refusal(const std::function<void()> &run)
|
||||
{
|
||||
try {
|
||||
run();
|
||||
} catch (const ImageImportError &error) {
|
||||
return error.kind;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool imported(EditorSession &session, const QList<QUrl> &urls)
|
||||
{
|
||||
bool done = false;
|
||||
session.importImages(urls, std::nullopt, [&] { done = true; });
|
||||
return QTest::qWaitFor([&] { return done && !session.isImporting(); }, 10'000);
|
||||
}
|
||||
}
|
||||
|
||||
class SVGImportTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void anSVGFitsTheCanvasOrKeepsItsSize();
|
||||
void brokenOrOversizedSVGsRefuse();
|
||||
void theSessionImportsSVGBySuffix();
|
||||
};
|
||||
|
||||
void SVGImportTests::anSVGFitsTheCanvasOrKeepsItsSize()
|
||||
{
|
||||
const QTemporaryDir folder;
|
||||
const QString path = written(folder, QStringLiteral("icon.svg"), svg("40", "20"));
|
||||
const ImportedImage alone = ImageImporter::decodeSVG(path, std::nullopt);
|
||||
QCOMPARE(alone.size(), QSize(40, 20));
|
||||
QCOMPARE(alone.name, QString("icon"));
|
||||
QCOMPARE(alone.image().pixelColor(20, 10), QColor(255, 0, 0));
|
||||
QCOMPARE(alone.image().colorSpace(), QColorSpace(QColorSpace::SRgb));
|
||||
QVERIFY(!alone.thumbnail.isNull());
|
||||
// Fitted, the smaller side's ratio rules and sides round.
|
||||
QCOMPARE(ImageImporter::decodeSVG(path, QSizeF(400, 300)).size(), QSize(400, 200));
|
||||
QCOMPARE(ImageImporter::decodeSVG(path, QSizeF(10, 10)).size(), QSize(10, 5));
|
||||
const QString thin = written(folder, QStringLiteral("thin.svg"), svg("3", "1"));
|
||||
QCOMPARE(ImageImporter::decodeSVG(thin, QSizeF(10, 10)).size(), QSize(10, 3));
|
||||
QCOMPARE(ImageImporter::decodeSVG(thin, QSizeF(1, 1)).size(), QSize(1, 1));
|
||||
QCOMPARE(ImageImporter::decodeSVG(thin, QSizeF(5, 5)).size(), QSize(5, 2));
|
||||
QCOMPARE(ImageImporter::decodeSVG(path, QSizeF(400, 100)).size(), QSize(200, 100));
|
||||
const QString narrow = written(folder, QStringLiteral("narrow.svg"), svg("1", "3"));
|
||||
QCOMPARE(ImageImporter::decodeSVG(narrow, QSizeF(1, 1)).size(), QSize(1, 1));
|
||||
QCOMPARE(ImageImporter::decodeSVG(narrow, QSizeF(5, 5)).size(), QSize(2, 5));
|
||||
// Red top left, blue bottom right, clear between: upright.
|
||||
const QString corners = written(folder, QStringLiteral("corners.svg"),
|
||||
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"10\" height=\"20\"><rect width=\"5\" height=\"10\" fill=\"red\"/>"
|
||||
"<rect x=\"5\" y=\"10\" width=\"5\" height=\"10\" fill=\"blue\"/></svg>");
|
||||
const QImage fitted = ImageImporter::decodeSVG(corners, QSizeF(100, 100)).image();
|
||||
QCOMPARE(fitted.size(), QSize(50, 100));
|
||||
QCOMPARE(fitted.pixelColor(12, 25), QColor(255, 0, 0));
|
||||
QCOMPARE(fitted.pixelColor(37, 75), QColor(0, 0, 255));
|
||||
QCOMPARE(fitted.pixelColor(37, 25).alpha(), 0);
|
||||
QCOMPARE(fitted.pixelColor(12, 75).alpha(), 0);
|
||||
// A large drawing's thumbnail is 96 on its long side.
|
||||
const QString large = written(folder, QStringLiteral("large.svg"), svg("200", "400"));
|
||||
QCOMPARE(ImageImporter::decodeSVG(large, std::nullopt).thumbnail.size(), QSize(48, 96));
|
||||
// Compressed and upper-case names read as SVG.
|
||||
const QString compressed = written(folder, QStringLiteral("icon.SVGZ"), gzipped(svg("40", "20")));
|
||||
QVERIFY(ImageImporter::isSVG(compressed));
|
||||
QCOMPARE(ImageImporter::decodeSVG(compressed, std::nullopt).size(), QSize(40, 20));
|
||||
QVERIFY(ImageImporter::isSVG(QStringLiteral("/a/b.Svg")));
|
||||
QVERIFY(!ImageImporter::isSVG(QStringLiteral("/a/b.png")));
|
||||
QVERIFY(!ImageImporter::isSVG(QStringLiteral("/a/svg")));
|
||||
}
|
||||
|
||||
void SVGImportTests::brokenOrOversizedSVGsRefuse()
|
||||
{
|
||||
const QTemporaryDir folder;
|
||||
using Kind = ImageImportError::Kind;
|
||||
const QString broken = written(folder, QStringLiteral("broken.svg"), "<svg");
|
||||
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(broken, std::nullopt); }), std::optional(Kind::unreadable));
|
||||
// An empty drawing with a zero side has no size.
|
||||
const QString empty = written(folder, QStringLiteral("empty.svg"), "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"0\" height=\"20\"/>");
|
||||
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(empty, std::nullopt); }), std::optional(Kind::unreadable));
|
||||
for (const char *box : {"0 0 0 10", "0 0 10 0"}) {
|
||||
const QString flat = written(folder, QStringLiteral("flat.svg"), QByteArray("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"") + box + "\"/>");
|
||||
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(flat, std::nullopt); }), std::optional(Kind::unreadable));
|
||||
}
|
||||
const QString wide = written(folder, QStringLiteral("wide.svg"), svg("30001", "1"));
|
||||
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(wide, std::nullopt); }), std::optional(Kind::tooLarge));
|
||||
QCOMPARE(ImageImporter::decodeSVG(wide, QSizeF(30'000, 30'000)).size(), QSize(30'000, 1));
|
||||
const QString tall = written(folder, QStringLiteral("tall.svg"), svg("1", "30001"));
|
||||
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(tall, std::nullopt); }), std::optional(Kind::tooLarge));
|
||||
QCOMPARE(ImageImporter::decodeSVG(tall, QSizeF(30'000, 30'000)).size(), QSize(1, 30'000));
|
||||
// The budget counts the pixels drawn.
|
||||
const QString icon = written(folder, QStringLiteral("icon.svg"), svg("40", "20"));
|
||||
QCOMPARE(ImageImporter::decodeSVG(icon, std::nullopt, 800).size(), QSize(40, 20));
|
||||
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(icon, std::nullopt, 799); }), std::optional(Kind::tooLarge));
|
||||
// Fitted, the budget counts the drawn size, not the declared.
|
||||
QCOMPARE(ImageImporter::decodeSVG(icon, QSizeF(400, 300), 80'000).size(), QSize(400, 200));
|
||||
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(icon, QSizeF(400, 300), 79'999); }), std::optional(Kind::tooLarge));
|
||||
QCOMPARE(ImageImporter::decodeSVG(icon, QSizeF(10, 10), 50).size(), QSize(10, 5));
|
||||
}
|
||||
|
||||
void SVGImportTests::theSessionImportsSVGBySuffix()
|
||||
{
|
||||
const QTemporaryDir folder;
|
||||
const QString icon = written(folder, QStringLiteral("icon.svg"), svg("40", "20"));
|
||||
// Without a document, its declared size makes the canvas.
|
||||
EditorSession session;
|
||||
QVERIFY(imported(session, {QUrl::fromLocalFile(icon)}));
|
||||
QCOMPARE(session.document().value().size(), QSizeF(40, 20));
|
||||
QCOMPARE(session.activeLayer().value().name, QString("icon"));
|
||||
// Over a canvas, it is drawn to fit it.
|
||||
EditorSession canvas;
|
||||
canvas.createDocument(400, 300, true);
|
||||
QVERIFY(imported(canvas, {QUrl::fromLocalFile(icon)}));
|
||||
QCOMPARE(canvas.activeLayer().value().asset.value().size(), QSize(400, 200));
|
||||
QCOMPARE(canvas.activeLayer().value().transform.origin, QPointF(0, 50));
|
||||
// The suffix rules before Photoshop's bytes.
|
||||
const QString disguised = written(folder, QStringLiteral("disguised.svg"), QByteArray("8BPS\0\1", 6) + QByteArray(40, '\0'));
|
||||
QVERIFY(PSDReader::matches(disguised));
|
||||
QVERIFY(imported(canvas, {QUrl::fromLocalFile(disguised)}));
|
||||
QCOMPARE(canvas.importError().value(), QString("disguised.svg: The image could not be read. It may be damaged or unavailable."));
|
||||
// The drawing counts against what the canvas already holds.
|
||||
EditorSession full;
|
||||
full.createDocument(40, 20, true);
|
||||
for (const ImportedImage &layer : claiming(DocumentLimits::documentPixelBudget() - 800))
|
||||
full.insert(layer);
|
||||
QVERIFY(imported(full, {QUrl::fromLocalFile(icon)}));
|
||||
QCOMPARE(full.importError(), std::nullopt);
|
||||
QCOMPARE(full.activeLayer().value().asset.value().size(), QSize(40, 20));
|
||||
QVERIFY(imported(full, {QUrl::fromLocalFile(icon)}));
|
||||
QVERIFY(full.importError().value().startsWith("icon.svg: This import exceeds"));
|
||||
}
|
||||
|
||||
QTEST_MAIN(SVGImportTests)
|
||||
#include "SVGImportTests.moc"
|
||||
@@ -58,3 +58,12 @@ inline ImportedImage coverage()
|
||||
image.scanLine(index / 2)[index % 2] = values[index];
|
||||
return LayerMask::assetFrom(image);
|
||||
}
|
||||
|
||||
// New text whose box starts at `corner`, unlike a click.
|
||||
inline void beginTextAt(EditorSession &session, QPointF corner)
|
||||
{
|
||||
session.beginText(corner, true);
|
||||
TextDraft draft = session.textDraft().value();
|
||||
draft.origin = corner;
|
||||
session.setTextDraft(draft);
|
||||
}
|
||||
@@ -49,6 +49,8 @@ compositor_test(ShapeToolTests)
|
||||
compositor_test(ShapeLayerTests)
|
||||
compositor_test(ShapeCanvasTests)
|
||||
compositor_test(TypeToolTests)
|
||||
compositor_test(LiveTextDoubleClickTests)
|
||||
compositor_test(SVGImportTests)
|
||||
compositor_test(TextLayoutTests)
|
||||
compositor_test(TypeSessionTests)
|
||||
compositor_test(TypeStoreTests)
|
||||
@@ -284,3 +286,7 @@ compositor_test(ByteCountsTests)
|
||||
compositor_test(ImageFileDropTests)
|
||||
compositor_test(SliderSnapTests)
|
||||
compositor_test(ProjectResizeFailureTests)
|
||||
|
||||
# The `.svgz` fixture is gzip, written through zlib.
|
||||
find_package(ZLIB REQUIRED)
|
||||
target_link_libraries(SVGImportTests PRIVATE ZLIB::ZLIB)
|
||||
@@ -405,7 +405,7 @@ void TypeControlsTests::aTextRowShowsAndOpensItsText()
|
||||
QVERIFY(QTest::qWaitForWindowActive(&list));
|
||||
LayerCell &textRow = *list.cells().at(0), &blankRow = *list.cells().at(1);
|
||||
const auto dimensions = [](LayerCell &row) { return row.findChild<QLabel *>("layerDimensions")->text(); };
|
||||
QCOMPARE(dimensions(textRow), QString("Text"));
|
||||
QCOMPARE(dimensions(textRow), QString("Text · Double-click to edit"));
|
||||
QCOMPARE(dimensions(blankRow), QString("400 × 300 px"));
|
||||
QCOMPARE(textRow.thumbnail().toolTip(), QString("Editable text layer"));
|
||||
QCOMPARE(textRow.thumbnail().accessibleName(), QString("Select text: Row"));
|
||||
|
||||
@@ -19,7 +19,7 @@ std::unique_ptr<EditorSession> makeSession()
|
||||
QUuid textLayer(EditorSession &session, QPointF at, const QString &content)
|
||||
{
|
||||
session.selectTool(NavigationTool::type);
|
||||
session.beginText(at, true);
|
||||
beginTextAt(session, at);
|
||||
TextDraft draft = session.textDraft().value();
|
||||
draft.style.content = content;
|
||||
if (!session.applyText(draft))
|
||||
@@ -234,7 +234,7 @@ void TypeSessionTests::aClickFindsTheTextUnderIt()
|
||||
const TextDraft fresh = session->textDraft().value();
|
||||
QVERIFY(!fresh.layerID && fresh.style.content.isEmpty() && !fresh.style.boxSize);
|
||||
QVERIFY(fresh.style.red == 0.25 && fresh.style.green == 1 && fresh.style.blue == 0.5);
|
||||
QCOMPARE(fresh.origin, QPointF(700, 300));
|
||||
QVERIFY(fresh.origin.x() == 688 && std::abs(fresh.origin.y() + fresh.style.lineHeight() - 288) < fresh.style.fontSize / 3);
|
||||
session->cancelText();
|
||||
session->beginText(QPointF(NAN, 5));
|
||||
QVERIFY(!session->textDraft());
|
||||
|
||||
+50
-1
@@ -2,6 +2,8 @@
|
||||
#include "SessionFixtures.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Rendering/TextLayout.h"
|
||||
#include <QFontMetricsF>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// Swift's TypeToolTests: text layers made, edited, saved, clipped.
|
||||
@@ -60,6 +62,7 @@ private slots:
|
||||
void invalidAndStaleDraftsDoNotChangeDocument();
|
||||
void textColorPickerPreviewsAndRestoresDraft();
|
||||
void theForegroundPickerPreviewsOpenTextToo();
|
||||
void clickedTextSitsOnItsBaseline();
|
||||
};
|
||||
|
||||
void TypeToolTests::createEditCancelAndUndo()
|
||||
@@ -67,6 +70,12 @@ void TypeToolTests::createEditCancelAndUndo()
|
||||
const auto session = makeSession();
|
||||
const int before = session->history.undoCount();
|
||||
session->beginText(QPointF(30, 40));
|
||||
// The first letter starts at the pointer, on its baseline.
|
||||
const TextDraft start = session->textDraft().value();
|
||||
const double descent = QFontMetricsF(TextLayout::font(start.style)).descent();
|
||||
// The laid line's descent, as drawn, within a hundredth.
|
||||
QCOMPARE(start.origin.x(), 30 - LayerTextStyle::padding);
|
||||
QVERIFY(std::abs(start.origin.y() - (40 - (LayerTextStyle::padding + start.style.lineHeight() - descent))) < 0.02);
|
||||
type(*session, QStringLiteral("Text"));
|
||||
QCOMPARE(session->document().value().layers.size(), size_t(1));
|
||||
TextDraft draft = session->textDraft().value();
|
||||
@@ -74,7 +83,7 @@ void TypeToolTests::createEditCancelAndUndo()
|
||||
draft.style.fontSize = 48;
|
||||
QVERIFY(session->applyText(draft));
|
||||
QCOMPARE(session->activeLayer().value().liveText().value().style, draft.style);
|
||||
QCOMPARE(session->activeLayer().value().origin(), QPointF(30, 40));
|
||||
QCOMPARE(session->activeLayer().value().origin(), start.origin);
|
||||
QCOMPARE(session->history.undoCount(), before + 1);
|
||||
session->editActiveText();
|
||||
session->setTextDraft(std::nullopt);
|
||||
@@ -162,6 +171,10 @@ void TypeToolTests::clippingToTextExportsColoredGlyphsOnTransparency()
|
||||
{
|
||||
const auto session = makeSession();
|
||||
session->beginText(QPointF(0, 0));
|
||||
// The box on the canvas's corner, where the fill goes.
|
||||
TextDraft corner = session->textDraft().value();
|
||||
corner.origin = QPointF(0, 0);
|
||||
session->setTextDraft(corner);
|
||||
type(*session, QStringLiteral("Text"));
|
||||
QVERIFY(session->applyText(session->textDraft().value()));
|
||||
const QUuid source = session->activeLayerID().value();
|
||||
@@ -190,6 +203,8 @@ void TypeToolTests::paragraphBoxAndToolSwitchCommitEditableText()
|
||||
session->selectTool(NavigationTool::brush);
|
||||
QVERIFY(!session->textDraft() && session->tool() == NavigationTool::brush);
|
||||
QCOMPARE(session->activeLayer().value().size(), QSizeF(200, 120));
|
||||
// A dragged box sits exactly where it was drawn.
|
||||
QCOMPARE(session->activeLayer().value().origin(), QPointF(40, 60));
|
||||
QCOMPARE(session->activeLayer().value().liveText().value().style.boxSize, std::optional(QSizeF(200, 120)));
|
||||
session->selectTool(NavigationTool::type);
|
||||
session->editActiveText();
|
||||
@@ -317,5 +332,39 @@ void TypeToolTests::theForegroundPickerPreviewsOpenTextToo()
|
||||
QCOMPARE(colour(), (PaletteColor{1, 0, 0}));
|
||||
}
|
||||
|
||||
void TypeToolTests::clickedTextSitsOnItsBaseline()
|
||||
{
|
||||
// Ink ends on the clicked row, from its column.
|
||||
const auto session = makeSession();
|
||||
// A first text sets the face the next click uses.
|
||||
session->beginText(QPointF(600, 500));
|
||||
TextDraft draft = session->textDraft().value();
|
||||
draft.style.content = QStringLiteral("Face");
|
||||
draft.style.fontName = QStringLiteral("DejaVuSans");
|
||||
draft.style.fontSize = 40;
|
||||
QVERIFY(session->applyText(draft));
|
||||
session->beginText(QPointF(100, 200));
|
||||
draft = session->textDraft().value();
|
||||
draft.style.content = QStringLiteral("HHH");
|
||||
const LayerTextStyle before = draft.style;
|
||||
QCOMPARE(before.fontName, QString("DejaVuSans"));
|
||||
QVERIFY(session->applyText(draft));
|
||||
const ImageLayer layer = session->activeLayer().value();
|
||||
QCOMPARE(layer.liveText().value().style, before);
|
||||
const QImage image = layer.asset.value().image();
|
||||
int bottom = -1, left = image.width();
|
||||
for (int y = 0; y < image.height(); ++y) {
|
||||
for (int x = 0; x < image.width(); ++x) {
|
||||
if (qAlpha(image.pixel(x, y)) > 128) {
|
||||
bottom = y;
|
||||
left = std::min(left, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
const QPointF origin = layer.origin();
|
||||
QVERIFY2(std::abs(origin.y() + bottom + 1 - 200) < 1, qPrintable(QString::number(origin.y() + bottom + 1)));
|
||||
QVERIFY2(std::abs(origin.x() + left - 100) < 5, qPrintable(QString::number(origin.x() + left)));
|
||||
}
|
||||
|
||||
QTEST_MAIN(TypeToolTests)
|
||||
#include "TypeToolTests.moc"
|
||||
Reference in new issue
Block a user