12.1: feathered selections, Select Subject, Soft Light, the fixed-width font menu (Compositor 1.1.1–1.1.4)
This commit is contained in:
1 parent
3e52fd90ee
commit
ee34c338d4
42 files changed
+1087
-94
No files matched your search
@@ -70,6 +70,11 @@ Rgb reference(LayerBlendMode mode, const Rgb &backdrop, const Rgb &source)
|
||||
case LayerBlendMode::multiply: result[channel] = b * s; break;
|
||||
case LayerBlendMode::screen: result[channel] = b + s - b * s; break;
|
||||
case LayerBlendMode::overlay: result[channel] = hardLight(s, b); break;
|
||||
case LayerBlendMode::softLight: {
|
||||
const double d = b <= 0.25 ? ((16 * b - 12) * b + 4) * b : std::sqrt(b);
|
||||
result[channel] = s <= 0.5 ? b - (1 - 2 * s) * b * (1 - b) : b + (2 * s - 1) * (d - b);
|
||||
break;
|
||||
}
|
||||
case LayerBlendMode::darken: result[channel] = std::min(b, s); break;
|
||||
case LayerBlendMode::lighten: result[channel] = std::max(b, s); break;
|
||||
case LayerBlendMode::difference: result[channel] = std::abs(b - s); break;
|
||||
@@ -132,7 +137,7 @@ void BlendModeTests::everyBlendModeMatchesThePdfFormulas_data()
|
||||
QTest::addColumn<LayerBlendMode>("mode");
|
||||
const std::pair<const char *, LayerBlendMode> modes[] = {
|
||||
{"normal", LayerBlendMode::normal}, {"multiply", LayerBlendMode::multiply}, {"screen", LayerBlendMode::screen},
|
||||
{"overlay", LayerBlendMode::overlay}, {"darken", LayerBlendMode::darken}, {"lighten", LayerBlendMode::lighten},
|
||||
{"overlay", LayerBlendMode::overlay}, {"softLight", LayerBlendMode::softLight}, {"darken", LayerBlendMode::darken}, {"lighten", LayerBlendMode::lighten},
|
||||
{"difference", LayerBlendMode::difference}, {"colorDodge", LayerBlendMode::colorDodge},
|
||||
{"colorBurn", LayerBlendMode::colorBurn}, {"hue", LayerBlendMode::hue}, {"saturation", LayerBlendMode::saturation},
|
||||
{"color", LayerBlendMode::color}, {"luminosity", LayerBlendMode::luminosity}};
|
||||
|
||||
@@ -36,8 +36,8 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
|
||||
{"layerViaCopy", "Ctrl+J"}, {"cut", "Ctrl+X"}, {"copy", "Ctrl+C"}, {"copyMerged", "Ctrl+Shift+C"}, {"paste", "Ctrl+V"}, {"fillForeground", "Alt+Backspace"}, {"fillBackground", "Ctrl+Backspace"}, {"clearSelectionPixels", ""}, {"contentAwareFill", "Shift+Backspace"}, {"clippingMask", "Ctrl+Alt+G"}, {"groupLayers", "Ctrl+G"},
|
||||
{"moveOutOfFolder", ""}, {"newBlankLayer", "Ctrl+Shift+N"}, {"renameLayer", ""}, {"layerVisibility", ""}, {"moveLayerUp", "Ctrl+]"}, {"moveLayerDown", "Ctrl+["},
|
||||
{"mergeLayers", "Ctrl+E"}, {"flipHorizontal", ""}, {"flipVertical", ""}, {"deleteLayer", ""},
|
||||
{"selectAll", "Ctrl+A"}, {"deselect", "Ctrl+D"}, {"inverse", "Ctrl+Shift+I"}, {"layerPixels", ""}, {"maskBlackAreas", ""},
|
||||
{"expandSelection", ""}, {"contractSelection", ""}, {"curves", "Ctrl+M"}, {"levels", "Ctrl+L"}, {"hueSaturation", "Ctrl+U"}, {"exposure", ""}, {"gradientMap", ""}, {"grain", ""}, {"invert", "Ctrl+I"},
|
||||
{"selectAll", "Ctrl+A"}, {"deselect", "Ctrl+D"}, {"inverse", "Ctrl+Shift+I"}, {"layerPixels", ""}, {"subject", "Ctrl+Alt+A"}, {"maskBlackAreas", ""},
|
||||
{"expandSelection", ""}, {"contractSelection", ""}, {"featherSelection", ""}, {"curves", "Ctrl+M"}, {"levels", "Ctrl+L"}, {"hueSaturation", "Ctrl+U"}, {"exposure", ""}, {"gradientMap", ""}, {"grain", ""}, {"invert", "Ctrl+I"},
|
||||
{"canvasSize", "Ctrl+Alt+C"}, {"imageSize", "Ctrl+Alt+I"}, {"flipCanvasHorizontal", ""}, {"flipCanvasVertical", ""},
|
||||
{"gaussianBlur", ""}, {"motionBlur", ""}, {"addNoise", ""}, {"lensCorrection", ""}, {"removeBackground", ""}, {"newAdjustmentLayer", ""}, {"editAdjustment", ""},
|
||||
{"newHueSaturationAdjustment", ""}, {"newLevelsAdjustment", ""}, {"newCurvesAdjustment", ""}, {"newExposureAdjustment", ""},
|
||||
@@ -54,7 +54,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
|
||||
QCOMPARE(menus[0]->actions().size(), 11);
|
||||
QCOMPARE(menus[1]->actions().size(), 12);
|
||||
QCOMPARE(menus[2]->actions().size(), 6);
|
||||
QCOMPARE(menus[3]->actions().size(), 8);
|
||||
QCOMPARE(menus[3]->actions().size(), 10);
|
||||
QCOMPARE(menus[4]->actions().size(), 13);
|
||||
QCOMPARE(menus[5]->actions().size(), 5);
|
||||
QCOMPARE(menus[6]->actions().size(), 22);
|
||||
|
||||
@@ -311,7 +311,7 @@ void LayerAppearanceTests::theBlendModeCyclesBothWaysAndWraps()
|
||||
QCOMPARE(session.blendPreview(), std::optional(EditorSession::BlendPreview{top, LayerBlendMode::hue}));
|
||||
QCOMPARE(int(changes.count()), 0);
|
||||
QCOMPARE(session.activeLayer().value().blendMode, LayerBlendMode::overlay);
|
||||
QCOMPARE(int(allLayerBlendModes.size()), 13);
|
||||
QCOMPARE(int(allLayerBlendModes.size()), 14);
|
||||
for (size_t index = 0; index < allLayerBlendModes.size(); ++index)
|
||||
QCOMPARE(int(allLayerBlendModes[index]), int(index));
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ private slots:
|
||||
void everyPatternFillsBackToItself_data();
|
||||
void everyPatternFillsBackToItself();
|
||||
void darkMeansBelowHalfGray();
|
||||
void whiteMeansAtLeastHalfGray();
|
||||
void opaqueMeansAtLeastHalfAlpha();
|
||||
void nothingToTraceIsNil();
|
||||
void otherPixelFormatsAreConverted();
|
||||
@@ -129,6 +130,12 @@ void MaskTracingTests::darkMeansBelowHalfGray()
|
||||
QVERIFY(!MaskTracing::darkPixels(mask({"#.", ".."}, 128, 255)).has_value());
|
||||
}
|
||||
|
||||
void MaskTracingTests::whiteMeansAtLeastHalfGray()
|
||||
{
|
||||
QCOMPARE(filled(MaskTracing::whitePixels(mask({"#.", ".."}, 128, 127)).value(), 2, 2), QStringList({"#.", ".."}));
|
||||
QVERIFY(!MaskTracing::whitePixels(mask({"#.", ".."}, 127, 0)).has_value());
|
||||
}
|
||||
|
||||
void MaskTracingTests::opaqueMeansAtLeastHalfAlpha()
|
||||
{
|
||||
QCOMPARE(filled(MaskTracing::opaquePixels(layer({"#.", ".."}, 128, 127)).value(), 2, 2), QStringList({"#.", ".."}));
|
||||
|
||||
@@ -202,7 +202,7 @@ void ProjectManifestTests::encodingWritesSwiftsKeysAndLeavesAbsentOnesOut()
|
||||
|
||||
void ProjectManifestTests::everyRawValueRoundTrips()
|
||||
{
|
||||
const QStringList blends{"Normal", "Multiply", "Screen", "Overlay", "Darken", "Lighten", "Difference", "Color Dodge", "Color Burn",
|
||||
const QStringList blends{"Normal", "Multiply", "Screen", "Overlay", "Soft Light", "Darken", "Lighten", "Difference", "Color Dodge", "Color Burn",
|
||||
"Hue", "Saturation", "Color", "Luminosity"};
|
||||
for (int index = 0; index < blends.size(); ++index) {
|
||||
QCOMPARE(rawValue(LayerBlendMode(index)), blends[index]);
|
||||
|
||||
@@ -23,9 +23,11 @@ private slots:
|
||||
void SelectMenuTests::selectEntriesFollowTheSelectionAndActOnIt()
|
||||
{
|
||||
Bar bar;
|
||||
for (const char *name : {"selectAll", "deselect", "inverse", "layerPixels", "maskBlackAreas", "expandSelection", "contractSelection"})
|
||||
for (const char *name : {"selectAll", "deselect", "inverse", "layerPixels", "subject", "maskBlackAreas", "expandSelection", "contractSelection",
|
||||
"featherSelection"})
|
||||
QVERIFY2(!bar.action(name).isEnabled(), name);
|
||||
bar.session().createDocument(50, 50, true);
|
||||
QVERIFY(bar.action("subject").isEnabled());
|
||||
QVERIFY(bar.action("selectAll").isEnabled());
|
||||
// A blank layer has no pixels and no mask.
|
||||
QVERIFY(!bar.action("layerPixels").isEnabled() && !bar.action("maskBlackAreas").isEnabled());
|
||||
@@ -34,18 +36,25 @@ void SelectMenuTests::selectEntriesFollowTheSelectionAndActOnIt()
|
||||
QCOMPARE(bar.session().history.undoName(), QString("Select All"));
|
||||
QVERIFY(bar.action("deselect").isEnabled() && bar.action("inverse").isEnabled());
|
||||
QVERIFY(bar.action("expandSelection").isEnabled() && bar.action("contractSelection").isEnabled());
|
||||
QCOMPARE(bar.action("expandSelection").text(), QString("Expand by 1 px"));
|
||||
QCOMPARE(bar.action("contractSelection").text(), QString("Contract by 1 px"));
|
||||
bar.session().setSelectionExpandAmount(3);
|
||||
bar.session().setSelectionContractAmount(7);
|
||||
QCOMPARE(bar.action("expandSelection").text(), QString("Expand by 3 px"));
|
||||
QCOMPARE(bar.action("contractSelection").text(), QString("Contract by 7 px"));
|
||||
QCOMPARE(bar.action("expandSelection").text(), QString("Expand…"));
|
||||
QCOMPARE(bar.action("contractSelection").text(), QString("Contract…"));
|
||||
QCOMPARE(bar.action("featherSelection").text(), QString("Feather…"));
|
||||
// Each asks for its amount, which the sheet confirms.
|
||||
bar.action("contractSelection").trigger();
|
||||
QCOMPARE(bar.session().selectionAmountOperation(), std::optional(SelectionAmountOperation::contract));
|
||||
QVERIFY(!bar.action("contractSelection").isEnabled() && !bar.action("subject").isEnabled());
|
||||
bar.session().confirmSelectionAmount(7);
|
||||
QCOMPARE(bar.session().history.undoName(), QString("Contract Selection"));
|
||||
QCOMPARE(bar.session().selection().value().path.boundingRect(), QRectF(7, 7, 36, 36));
|
||||
bar.action("expandSelection").trigger();
|
||||
QCOMPARE(bar.session().selectionAmountOperation(), std::optional(SelectionAmountOperation::expand));
|
||||
bar.session().confirmSelectionAmount(3);
|
||||
QCOMPARE(bar.session().history.undoName(), QString("Expand Selection"));
|
||||
QCOMPARE(bar.session().selection().value().path.boundingRect(), QRectF(4, 4, 42, 42));
|
||||
bar.action("featherSelection").trigger();
|
||||
QCOMPARE(bar.session().selectionAmountOperation(), std::optional(SelectionAmountOperation::feather));
|
||||
bar.session().confirmSelectionAmount(2);
|
||||
QCOMPARE(bar.session().history.undoName(), QString("Feather Selection"));
|
||||
bar.action("inverse").trigger();
|
||||
QCOMPARE(bar.session().history.undoName(), QString("Inverse"));
|
||||
bar.action("deselect").trigger();
|
||||
@@ -65,10 +74,17 @@ void SelectMenuTests::selectEntriesFollowTheSelectionAndActOnIt()
|
||||
QCOMPARE(bar.session().history.undoName(), QString("Load Mask Selection"));
|
||||
// Busy, every entry waits; a draft holds Expand and Contract.
|
||||
bar.session().setIsProjectBusy(true);
|
||||
for (const char *name : {"deselect", "inverse", "layerPixels", "maskBlackAreas", "expandSelection", "contractSelection"})
|
||||
for (const char *name : {"deselect", "inverse", "layerPixels", "subject", "maskBlackAreas", "expandSelection", "contractSelection", "featherSelection"})
|
||||
QVERIFY2(!bar.action(name).isEnabled(), name);
|
||||
QVERIFY(bar.action("selectAll").isEnabled());
|
||||
bar.session().setIsProjectBusy(false);
|
||||
// Subject runs the model on what the canvas shows.
|
||||
bar.action("subject").trigger();
|
||||
QVERIFY(bar.session().isProjectBusy());
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!bar.session().isProjectBusy(), 20000);
|
||||
// A white layer has no subject, and says so.
|
||||
QVERIFY(bar.session().brushError().has_value());
|
||||
bar.session().setBrushError(std::nullopt);
|
||||
bar.session().selectTool(NavigationTool::lasso);
|
||||
bar.session().beginLasso(QPointF(1, 1), SelectionMode::replace);
|
||||
QVERIFY(!bar.action("expandSelection").isEnabled() && bar.action("deselect").isEnabled());
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "SelectionFixtures.h"
|
||||
#include <QPainter>
|
||||
|
||||
// Select ▸ Subject: U²-Net's subject becomes the selection.
|
||||
namespace {
|
||||
// A red disc on white, or flat white.
|
||||
std::unique_ptr<EditorSession> withImage(bool disc)
|
||||
{
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(400, 300);
|
||||
QImage image = BrushRaster::context(400, 300, false);
|
||||
image.fill(Qt::white);
|
||||
QPainter painter(&image);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(217, 26, 26));
|
||||
if (disc)
|
||||
painter.drawEllipse(QPointF(200, 150), 80, 80);
|
||||
painter.end();
|
||||
session->insert(ImportedImage(image, image, QStringLiteral("Picture")));
|
||||
return session;
|
||||
}
|
||||
|
||||
bool selected(EditorSession &session, SelectionMode mode)
|
||||
{
|
||||
bool done = false;
|
||||
session.selectSubject(mode, [&done] { done = true; });
|
||||
return QTest::qWaitFor([&done] { return done; }, 20000);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectSubjectTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theSubjectBecomesTheSelection();
|
||||
void theModeCombinesWithTheSelection();
|
||||
void aFlatPictureSelectsNothingAndSaysSo();
|
||||
void aRefusalAnswersAndChangesNothing();
|
||||
void anotherDocumentTakesNoOutline();
|
||||
};
|
||||
|
||||
void SelectSubjectTests::theSubjectBecomesTheSelection()
|
||||
{
|
||||
const auto session = withImage(true);
|
||||
QVERIFY(session->canSelectSubject());
|
||||
bool done = false;
|
||||
session->selectSubject(SelectionMode::replace, [&done] { done = true; });
|
||||
QVERIFY(session->isProjectBusy());
|
||||
QVERIFY(!session->canSelectSubject());
|
||||
QVERIFY(QTest::qWaitFor([&done] { return done; }, 20000));
|
||||
QVERIFY(!session->isProjectBusy());
|
||||
QCOMPARE(session->history.undoName(), QString("Select Subject"));
|
||||
QCOMPARE(coverage(*session, 200, 150), 255);
|
||||
QCOMPARE(coverage(*session, 270, 150), 255);
|
||||
QCOMPARE(coverage(*session, 295, 150), 0);
|
||||
QCOMPARE(coverage(*session, 10, 10), 0);
|
||||
session->undo();
|
||||
QVERIFY(!session->selection());
|
||||
}
|
||||
|
||||
void SelectSubjectTests::theModeCombinesWithTheSelection()
|
||||
{
|
||||
const auto session = withImage(true);
|
||||
session->applySelection(rectPath(QRectF(10, 10, 30, 30)), SelectionMode::replace, "Select");
|
||||
QVERIFY(selected(*session, SelectionMode::add));
|
||||
QCOMPARE(coverage(*session, 20, 20), 255);
|
||||
QCOMPARE(coverage(*session, 200, 150), 255);
|
||||
QVERIFY(selected(*session, SelectionMode::subtract));
|
||||
QCOMPARE(coverage(*session, 20, 20), 255);
|
||||
QCOMPARE(coverage(*session, 200, 150), 0);
|
||||
QVERIFY(selected(*session, SelectionMode::replace));
|
||||
QCOMPARE(coverage(*session, 20, 20), 0);
|
||||
QCOMPARE(coverage(*session, 200, 150), 255);
|
||||
}
|
||||
|
||||
void SelectSubjectTests::aFlatPictureSelectsNothingAndSaysSo()
|
||||
{
|
||||
const auto session = withImage(false);
|
||||
QVERIFY(selected(*session, SelectionMode::replace));
|
||||
QVERIFY(!session->selection());
|
||||
QVERIFY(session->brushError().value().contains("subject"));
|
||||
QVERIFY(!session->isProjectBusy());
|
||||
}
|
||||
|
||||
void SelectSubjectTests::aRefusalAnswersAndChangesNothing()
|
||||
{
|
||||
EditorSession empty;
|
||||
QVERIFY(!empty.canSelectSubject());
|
||||
QSignalSpy quiet(&empty, &EditorSession::changed);
|
||||
QVERIFY(selected(empty, SelectionMode::replace));
|
||||
QCOMPARE(int(quiet.count()), 0);
|
||||
const auto session = withImage(true);
|
||||
session->setIsProjectBusy(true);
|
||||
QVERIFY(!session->canSelectSubject());
|
||||
QSignalSpy changes(session.get(), &EditorSession::changed);
|
||||
QVERIFY(selected(*session, SelectionMode::replace));
|
||||
QCOMPARE(int(changes.count()), 0);
|
||||
QVERIFY(!session->selection());
|
||||
}
|
||||
|
||||
void SelectSubjectTests::anotherDocumentTakesNoOutline()
|
||||
{
|
||||
const auto session = withImage(true);
|
||||
EditorSession other;
|
||||
other.createDocument(400, 300);
|
||||
bool done = false;
|
||||
session->selectSubject(SelectionMode::replace, [&done] { done = true; });
|
||||
session->installProject(other.projectSnapshot().value(), QString());
|
||||
QVERIFY(QTest::qWaitFor([&done] { return done; }, 20000));
|
||||
QVERIFY(!session->selection());
|
||||
QCOMPARE(session->history.undoName(), QString());
|
||||
QVERIFY(!session->isProjectBusy());
|
||||
// A closed project takes none either.
|
||||
const auto closing = withImage(true);
|
||||
done = false;
|
||||
closing->selectSubject(SelectionMode::replace, [&done] { done = true; });
|
||||
closing->clearProject();
|
||||
QVERIFY(QTest::qWaitFor([&done] { return done; }, 20000));
|
||||
QVERIFY(!closing->document());
|
||||
QVERIFY(!closing->brushError());
|
||||
QVERIFY(!closing->isProjectBusy());
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(SelectSubjectTests)
|
||||
#include "SelectSubjectTests.moc"
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "ContentView.h"
|
||||
#include "SelectionFixtures.h"
|
||||
#include "UI/LassoControls.h"
|
||||
#include <QApplication>
|
||||
#include <QLabel>
|
||||
#include <QSlider>
|
||||
|
||||
// Select's amount sheet, its panel, and the bar's Feather.
|
||||
namespace {
|
||||
template <typename Widget> Widget &find(QWidget &root, const char *name)
|
||||
{
|
||||
Widget *found = root.findChild<Widget *>(QString::fromLatin1(name));
|
||||
if (!found)
|
||||
throw std::runtime_error(std::string("no widget named ") + name);
|
||||
return *found;
|
||||
}
|
||||
|
||||
QWidget *amountPanel()
|
||||
{
|
||||
for (QWidget *widget : QApplication::topLevelWidgets()) {
|
||||
if (widget->objectName() == QStringLiteral("selectionAmountPanel") && widget->isVisible())
|
||||
return widget;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
class SelectionAmountSheetTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theSheetTakesAWholeNumberInRange();
|
||||
void cancelAndThePanelsCloseForgetTheQuestion();
|
||||
void theBarFeathersByItsAmount();
|
||||
};
|
||||
|
||||
void SelectionAmountSheetTests::theSheetTakesAWholeNumberInRange()
|
||||
{
|
||||
const auto session = selectionSession();
|
||||
lasso(*session, square(20, 20, 40));
|
||||
session->setSelectionFeatherAmount(9);
|
||||
session->promptSelectionAmount(SelectionAmountOperation::feather);
|
||||
SelectionAmountSheet sheet(*session, SelectionAmountOperation::feather);
|
||||
sheet.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&sheet));
|
||||
QCOMPARE(sheet.width(), 380);
|
||||
auto &field = find<QLineEdit>(sheet, "amountField");
|
||||
auto &slider = find<QSlider>(sheet, "amountSlider");
|
||||
auto ¬e = find<QLabel>(sheet, "amountNote");
|
||||
auto &ok = find<QPushButton>(sheet, "amountOK");
|
||||
QTRY_VERIFY(field.hasFocus());
|
||||
QCOMPARE(field.text(), QString("9"));
|
||||
QCOMPARE(slider.maximum(), 250);
|
||||
QCOMPARE(slider.value(), 9);
|
||||
QVERIFY(!note.isVisible() && ok.isEnabled() && ok.isDefault());
|
||||
QCOMPARE(note.text(), QString("Enter a whole number from 1 to 250 px."));
|
||||
const int height = sheet.height();
|
||||
// Out of range: the note shows, OK rests.
|
||||
for (const char *typed : {"251", "0", "2.5", "x", ""}) {
|
||||
field.setText(QString::fromLatin1(typed));
|
||||
QVERIFY2(note.isVisible() && !ok.isEnabled(), typed);
|
||||
QCOMPARE(sheet.height(), height);
|
||||
}
|
||||
slider.setValue(40);
|
||||
QCOMPARE(field.text(), QString("40"));
|
||||
QVERIFY(ok.isEnabled());
|
||||
field.setText(QStringLiteral(" 12 "));
|
||||
QCOMPARE(slider.value(), 12);
|
||||
QCOMPARE(note.font().pixelSize(), 12);
|
||||
QTest::mouseClick(&ok, Qt::LeftButton);
|
||||
QVERIFY(!session->selectionAmountOperation());
|
||||
QCOMPARE(session->selectionFeatherAmount(), 12);
|
||||
QCOMPARE(session->selection().value().feather, 12.0);
|
||||
// Expand and Contract reach 500.
|
||||
SelectionAmountSheet expand(*session, SelectionAmountOperation::expand);
|
||||
QCOMPARE(find<QSlider>(expand, "amountSlider").maximum(), 500);
|
||||
QCOMPARE(find<QLineEdit>(expand, "amountField").text(), QString("1"));
|
||||
QCOMPARE(find<QLabel>(expand, "amountNote").text(), QString("Enter a whole number from 1 to 500 px."));
|
||||
}
|
||||
|
||||
void SelectionAmountSheetTests::cancelAndThePanelsCloseForgetTheQuestion()
|
||||
{
|
||||
EditorSession session;
|
||||
ContentView view(session);
|
||||
view.resize(900, 600);
|
||||
view.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
session.createDocument(100, 100, true);
|
||||
session.selectAll();
|
||||
session.promptSelectionAmount(SelectionAmountOperation::contract);
|
||||
QWidget *panel = nullptr;
|
||||
QTRY_VERIFY((panel = amountPanel()));
|
||||
QCOMPARE(panel->windowTitle(), QString("Contract Selection"));
|
||||
QTest::mouseClick(&find<QPushButton>(*panel, "amountCancel"), Qt::LeftButton);
|
||||
QVERIFY(!session.selectionAmountOperation());
|
||||
QTRY_VERIFY(!amountPanel());
|
||||
QCOMPARE(session.history.undoName(), QString("Select All"));
|
||||
// Escape closes the panel, forgetting the question too.
|
||||
session.promptSelectionAmount(SelectionAmountOperation::expand);
|
||||
QTRY_VERIFY((panel = amountPanel()));
|
||||
QCOMPARE(panel->windowTitle(), QString("Expand Selection"));
|
||||
QTRY_COMPARE(QApplication::activeWindow(), panel);
|
||||
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Escape);
|
||||
QVERIFY(!session.selectionAmountOperation());
|
||||
QTRY_VERIFY(!amountPanel());
|
||||
// Return in the field is OK: it applies and closes.
|
||||
session.promptSelectionAmount(SelectionAmountOperation::feather);
|
||||
QTRY_VERIFY((panel = amountPanel()));
|
||||
QCOMPARE(panel->windowTitle(), QString("Feather Selection"));
|
||||
QTRY_COMPARE(QApplication::focusWidget(), &find<QLineEdit>(*panel, "amountField"));
|
||||
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return);
|
||||
QCOMPARE(session.history.undoName(), QString("Feather Selection"));
|
||||
QTRY_VERIFY(!amountPanel());
|
||||
}
|
||||
|
||||
void SelectionAmountSheetTests::theBarFeathersByItsAmount()
|
||||
{
|
||||
const auto session = selectionSession();
|
||||
LassoControls controls(*session);
|
||||
controls.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&controls));
|
||||
auto &feather = find<QPushButton>(controls, "featherSelection");
|
||||
auto &amount = find<QLineEdit>(controls, "selectionFeatherAmount");
|
||||
QVERIFY(!feather.isEnabled() && !amount.isEnabled());
|
||||
QCOMPARE(amount.text(), QString("2"));
|
||||
lasso(*session, square(20, 20, 40));
|
||||
QVERIFY(feather.isEnabled() && amount.isEnabled());
|
||||
amount.setFocus();
|
||||
QTRY_VERIFY(amount.hasFocus());
|
||||
amount.selectAll();
|
||||
QTest::keyClicks(&amount, "300");
|
||||
QCOMPARE(session->selectionFeatherAmount(), 250);
|
||||
amount.selectAll();
|
||||
QTest::keyClicks(&amount, "3");
|
||||
QTest::mouseClick(&feather, Qt::LeftButton);
|
||||
QCOMPARE(session->selection().value().feather, 3.0);
|
||||
QCOMPARE(session->history.undoName(), QString("Feather Selection"));
|
||||
}
|
||||
|
||||
QTEST_MAIN(SelectionAmountSheetTests)
|
||||
#include "SelectionAmountSheetTests.moc"
|
||||
@@ -0,0 +1,188 @@
|
||||
#include "SelectionFixtures.h"
|
||||
|
||||
// Feather softens the selection itself, so everything clipped fades.
|
||||
namespace {
|
||||
// Values strictly between clear and full along one row.
|
||||
int fading(const QImage &image, int row, int channel, int stride)
|
||||
{
|
||||
int count = 0;
|
||||
for (int x = 0; x < image.width(); ++x) {
|
||||
const int value = image.constScanLine(row)[x * stride + channel];
|
||||
count += value > 8 && value < 247;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
class SelectionFeatherTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void featherSoftensTheSelectionAndWhatItClips();
|
||||
void feathersAddAsBlursAndStopAtTheirLimit();
|
||||
void featherTravelsWithTheOutline();
|
||||
void theClipGrowsByTheFalloff();
|
||||
void theAmountSheetAsksThenApplies();
|
||||
void aMaskFromAFeatheredSelectionFades();
|
||||
};
|
||||
|
||||
void SelectionFeatherTests::featherSoftensTheSelectionAndWhatItClips()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(60, 20, true);
|
||||
session.addBlankLayer();
|
||||
session.selectAll();
|
||||
session.setSelectionFeatherAmount(6);
|
||||
session.featherSelection(6);
|
||||
QVERIFY(session.canModifySelection());
|
||||
QCOMPARE(session.selection().value().feather, 6.0);
|
||||
QCOMPARE(session.history.undoName(), QString("Feather Selection"));
|
||||
session.setSelection(DocumentSelection{rectPath(QRectF(20, 0, 20, 20)), true, 6}, "probe");
|
||||
const SelectionClip clip = session.selection().value().clip(QSizeF(60, 20));
|
||||
const QImage coverage = clip.coverage.value();
|
||||
QVERIFY2(fading(coverage, coverage.height() / 2, 0, 1) >= 4, "the clip has no soft edge");
|
||||
// A copy takes the fade too, within the canvas.
|
||||
QCOMPARE(session.selectionCopyRegion().value(), QRectF(8, 0, 44, 20));
|
||||
session.setForegroundColor(PaletteColor{1, 0, 0});
|
||||
bool done = false;
|
||||
session.fillSelection(EditorSession::FillSource::foreground, [&] { done = true; });
|
||||
QTRY_VERIFY(done);
|
||||
const QImage filled = session.activeLayer().value().asset.value().image().convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
QVERIFY2(fading(filled, filled.height() / 2, 3, 4) >= 4, "the fill has a hard edge");
|
||||
// Trimmed to the fade: whole inside, faint at the ends.
|
||||
QCOMPARE(session.activeLayer().value().transform.origin, QPointF(12, 0));
|
||||
QCOMPARE(filled.width(), 36);
|
||||
QCOMPARE(int(filled.constScanLine(10)[18 * 4 + 3]), 255);
|
||||
QCOMPARE(int(filled.constScanLine(10)[3]), 1);
|
||||
}
|
||||
|
||||
void SelectionFeatherTests::feathersAddAsBlursAndStopAtTheirLimit()
|
||||
{
|
||||
const auto session = selectionSession();
|
||||
QSignalSpy changes(session.get(), &EditorSession::changed);
|
||||
session->featherSelection(4);
|
||||
QVERIFY(!session->selection());
|
||||
QCOMPARE(int(changes.count()), 0);
|
||||
lasso(*session, square(10, 10, 40));
|
||||
session->featherSelection(3);
|
||||
session->featherSelection(4);
|
||||
QCOMPARE(session->selection().value().feather, 5.0);
|
||||
session->featherSelection(0);
|
||||
session->featherSelection(-3);
|
||||
QCOMPARE(session->selection().value().feather, 5.0);
|
||||
session->featherSelection(250);
|
||||
QCOMPARE(session->selection().value().feather, 250.0);
|
||||
session->undo();
|
||||
QCOMPARE(session->selection().value().feather, 5.0);
|
||||
// A hard edge stays hard: clear or full, nothing between.
|
||||
session->setSelectionAntialiased(false);
|
||||
lasso(*session, {QPointF(10, 10), QPointF(60, 20), QPointF(20, 70)});
|
||||
const QImage hard = session->selection().value().coverage(100, 100);
|
||||
for (int y = 0; y < 100; ++y)
|
||||
QCOMPARE(fading(hard, y, 0, 1), 0);
|
||||
}
|
||||
|
||||
void SelectionFeatherTests::featherTravelsWithTheOutline()
|
||||
{
|
||||
const auto session = selectionSession();
|
||||
lasso(*session, square(20, 20, 40));
|
||||
session->featherSelection(4);
|
||||
session->expandSelection(2);
|
||||
QCOMPARE(session->selection().value().feather, 4.0);
|
||||
session->contractSelection(1);
|
||||
QCOMPARE(session->selection().value().feather, 4.0);
|
||||
session->invertSelection();
|
||||
QCOMPARE(session->selection().value().feather, 4.0);
|
||||
session->invertSelection();
|
||||
QVERIFY(session->beginSelectionMove());
|
||||
session->moveSelection(QSizeF(5, 0));
|
||||
QCOMPARE(session->selection().value().feather, 4.0);
|
||||
QCOMPARE(bounds(*session).left(), 24.0);
|
||||
}
|
||||
|
||||
void SelectionFeatherTests::theClipGrowsByTheFalloff()
|
||||
{
|
||||
const DocumentSelection soft{rectPath(QRectF(10, 10, 20, 20)), true, 3};
|
||||
QCOMPARE(soft.coverageBounds(), QRectF(4, 4, 32, 32));
|
||||
QCOMPARE(soft.clip(QSizeF(100, 100)).rect, QRectF(3, 3, 34, 34));
|
||||
QCOMPARE(soft.clip(QSizeF(20, 20)).rect, QRectF(3, 3, 17, 17));
|
||||
// A feathered edge is antialiased first, whatever the choice.
|
||||
QPainterPath triangle;
|
||||
triangle.addPolygon(QPolygonF{QPointF(5, 5), QPointF(35, 12), QPointF(12, 33)});
|
||||
QCOMPARE((DocumentSelection{triangle, false, 1}.coverage(40, 40)), (DocumentSelection{triangle, true, 1}.coverage(40, 40)));
|
||||
// The canvas edge repeats: a whole canvas stays whole there.
|
||||
const QImage whole = DocumentSelection{rectPath(QRectF(0, 0, 40, 40)), true, 6}.coverage(40, 40);
|
||||
QCOMPARE(int(whole.constScanLine(0)[0]), 255);
|
||||
QCOMPARE(int(whole.constScanLine(39)[20]), 255);
|
||||
const DocumentSelection hard{rectPath(QRectF(10, 10, 20, 20)), true, 0};
|
||||
QCOMPARE(hard.coverageBounds(), QRectF(10, 10, 20, 20));
|
||||
// Past the outline it fades, then nothing.
|
||||
const QImage image = soft.coverage(40, 40);
|
||||
QVERIFY(image.constScanLine(20)[8] > 0);
|
||||
QCOMPARE(int(image.constScanLine(20)[2]), 0);
|
||||
QCOMPARE(int(image.constScanLine(20)[20]), 255);
|
||||
// Either side of the outline mirrors the other.
|
||||
QCOMPARE(image.constScanLine(20)[9] + image.constScanLine(20)[10], 255);
|
||||
}
|
||||
|
||||
void SelectionFeatherTests::theAmountSheetAsksThenApplies()
|
||||
{
|
||||
const auto session = selectionSession();
|
||||
QSignalSpy changes(session.get(), &EditorSession::changed);
|
||||
session->promptSelectionAmount(SelectionAmountOperation::feather);
|
||||
QVERIFY(!session->selectionAmountOperation());
|
||||
QCOMPARE(int(changes.count()), 0);
|
||||
lasso(*session, square(20, 20, 40));
|
||||
session->promptSelectionAmount(SelectionAmountOperation::feather);
|
||||
QCOMPARE(session->selectionAmountOperation(), std::optional(SelectionAmountOperation::feather));
|
||||
QVERIFY(!session->canEditLayers());
|
||||
QVERIFY(!session->canUseHistory());
|
||||
QVERIFY(!session->canStartProjectOperation());
|
||||
session->confirmSelectionAmount(0);
|
||||
session->confirmSelectionAmount(251);
|
||||
QVERIFY(session->selectionAmountOperation().has_value());
|
||||
QCOMPARE(session->selection().value().feather, 0.0);
|
||||
session->confirmSelectionAmount(250);
|
||||
QVERIFY(!session->selectionAmountOperation());
|
||||
QCOMPARE(session->selectionFeatherAmount(), 250);
|
||||
QCOMPARE(session->selection().value().feather, 250.0);
|
||||
QVERIFY(session->canEditLayers());
|
||||
// A file request waits for the question, then runs.
|
||||
session->promptSelectionAmount(SelectionAmountOperation::expand);
|
||||
bool ran = false;
|
||||
session->waitForFileRequest([&ran] { ran = true; });
|
||||
QTest::qWait(10);
|
||||
QVERIFY(!ran);
|
||||
session->setSelectionAmountOperation(std::nullopt);
|
||||
QTRY_VERIFY(ran);
|
||||
session->promptSelectionAmount(SelectionAmountOperation::expand);
|
||||
session->confirmSelectionAmount(501);
|
||||
QVERIFY(session->selectionAmountOperation().has_value());
|
||||
session->confirmSelectionAmount(3);
|
||||
QCOMPARE(session->selectionExpandAmount(), 3);
|
||||
QCOMPARE(session->history.undoName(), QString("Expand Selection"));
|
||||
QCOMPARE(bounds(*session), QRectF(17, 17, 46, 46));
|
||||
session->promptSelectionAmount(SelectionAmountOperation::contract);
|
||||
session->confirmSelectionAmount(500);
|
||||
QCOMPARE(session->selectionContractAmount(), 500);
|
||||
QVERIFY(session->selection().value().isEmpty());
|
||||
// No question open: an amount does nothing.
|
||||
session->undo();
|
||||
session->confirmSelectionAmount(5);
|
||||
QCOMPARE(session->selectionContractAmount(), 500);
|
||||
}
|
||||
|
||||
void SelectionFeatherTests::aMaskFromAFeatheredSelectionFades()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(60, 20, true);
|
||||
session.setSelection(DocumentSelection{rectPath(QRectF(20, 0, 20, 20)), true, 6}, "probe");
|
||||
session.addMask(false);
|
||||
const QImage mask = session.activeLayer().value().mask.value().asset.image();
|
||||
QCOMPARE(mask, session.document().value().layers.back().mask.value().asset.image());
|
||||
QVERIFY2(fading(mask, 10, 0, 1) >= 4, "the mask has a hard edge");
|
||||
QCOMPARE(int(mask.constScanLine(10)[30]), 255);
|
||||
QCOMPARE(int(mask.constScanLine(10)[2]), 0);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(SelectionFeatherTests)
|
||||
#include "SelectionFeatherTests.moc"
|
||||
+16
-22
@@ -39,7 +39,7 @@ private slots:
|
||||
void sizeAndTrackingFollowTheLocale();
|
||||
void leadingIsAutoWhenEmptyAndClamps();
|
||||
void fieldsKeepTypingAndTheFocus();
|
||||
void alignmentAndFontChangeTheDraft();
|
||||
void alignmentChangesTheDraft();
|
||||
void theButtonsFollowTheDraft();
|
||||
void theBarRestsWithoutADocumentOrWhileBusy();
|
||||
void aTextRowShowsAndOpensItsText();
|
||||
@@ -50,17 +50,27 @@ void TypeControlsTests::theBarShowsTheStyleItEdits()
|
||||
Bar shown;
|
||||
QCOMPARE(shown.bar.title->text(), QString("Type"));
|
||||
auto &font = find<QComboBox>(shown.bar, "typeFont");
|
||||
QCOMPARE(font.width(), 185);
|
||||
QCOMPARE(font.width(), 210);
|
||||
QCOMPARE(font.toolTip(), QString("Font face, including bold and italic variants"));
|
||||
// Every face, and Helvetica though missing, sorted.
|
||||
QCOMPARE(font.accessibleName(), QString("Font"));
|
||||
// The catalog waits for the menu: the style's face alone.
|
||||
QCOMPARE(font.count(), 1);
|
||||
QCOMPARE(font.currentText(), QString("Helvetica"));
|
||||
shown.restyle([](LayerTextStyle &style) { style.fontName = QStringLiteral("Aachen"); });
|
||||
QCOMPARE(font.count(), 2);
|
||||
font.showPopup();
|
||||
font.hidePopup();
|
||||
// Every face, and the style's own though missing, sorted.
|
||||
QStringList names = TextLayout::availableFonts();
|
||||
names << QStringLiteral("Helvetica");
|
||||
names << QStringLiteral("Aachen");
|
||||
names.sort();
|
||||
QStringList listed;
|
||||
for (int index = 0; index < font.count(); ++index)
|
||||
listed << font.itemText(index);
|
||||
QCOMPARE(listed, names);
|
||||
QCOMPARE(font.currentText(), QString("Helvetica"));
|
||||
QCOMPARE(font.currentText(), QString("Aachen"));
|
||||
QCOMPARE(font.itemText(0), QString("Aachen"));
|
||||
shown.restyle([](LayerTextStyle &style) { style.fontName = QStringLiteral("Helvetica"); });
|
||||
TextStyleField &size = shown.field("typeSize"), &tracking = shown.field("typeTracking"), &leading = shown.field("typeLeading");
|
||||
QVERIFY(size.width() == 52 && tracking.width() == 45 && leading.width() == 52);
|
||||
QCOMPARE(size.text(), QString("72"));
|
||||
@@ -296,10 +306,9 @@ void TypeControlsTests::fieldsKeepTypingAndTheFocus()
|
||||
QCOMPARE(shown.session.currentTextStyle().fontSize, 1.0);
|
||||
}
|
||||
|
||||
void TypeControlsTests::alignmentAndFontChangeTheDraft()
|
||||
void TypeControlsTests::alignmentChangesTheDraft()
|
||||
{
|
||||
Bar shown;
|
||||
auto &font = find<QComboBox>(shown.bar, "typeFont");
|
||||
QToolButton &left = shown.align("typeAlignLeft"), ¢er = shown.align("typeAlignCenter");
|
||||
// Without a draft the defaults change.
|
||||
center.click();
|
||||
@@ -310,21 +319,6 @@ void TypeControlsTests::alignmentAndFontChangeTheDraft()
|
||||
left.click();
|
||||
QCOMPARE(shown.session.textDraft().value().style.alignment, TextAlignment::left);
|
||||
QVERIFY(left.isChecked() && !center.isChecked());
|
||||
// A face writes its name; Helvetica then leaves the list.
|
||||
const int bold = font.findText(QStringLiteral("DejaVuSans-Bold"));
|
||||
QVERIFY(bold >= 0);
|
||||
emit font.activated(bold);
|
||||
QCOMPARE(shown.session.textDraft().value().style.fontName, QString("DejaVuSans-Bold"));
|
||||
QCOMPARE(font.currentText(), QString("DejaVuSans-Bold"));
|
||||
QCOMPARE(font.findText(QStringLiteral("Helvetica")), -1);
|
||||
QCOMPARE(font.count(), TextLayout::availableFonts().size());
|
||||
// A missing face named elsewhere joins the list.
|
||||
shown.restyle([](LayerTextStyle &style) { style.fontName = QStringLiteral("Zapfino"); });
|
||||
QCOMPARE(font.currentText(), QString("Zapfino"));
|
||||
QCOMPARE(font.count(), TextLayout::availableFonts().size() + 1);
|
||||
QCOMPARE(font.itemText(font.count() - 1), QString("Zapfino"));
|
||||
shown.restyle([](LayerTextStyle &style) { style.fontName = QStringLiteral("Aachen"); });
|
||||
QVERIFY(font.currentText() == QString("Aachen") && font.itemText(0) == QString("Aachen") && font.findText("Zapfino") < 0);
|
||||
// Editing an existing text layer opens its draft.
|
||||
shown.session.cancelText();
|
||||
const QUuid text = addText(shown.session, QStringLiteral("Aligned"));
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "TypeControlsFixtures.h"
|
||||
#include "Rendering/TextLayout.h"
|
||||
#include "UI/TypeControls.h"
|
||||
#include <QAbstractItemView>
|
||||
#include <QComboBox>
|
||||
#include <QStyleOptionComboBox>
|
||||
#include <QtTest>
|
||||
|
||||
// The Type bar's font menu: loaded late, cut short.
|
||||
class TypeFontPickerTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void aFaceIsChosenKeptAndCutShort();
|
||||
void theFaceShownChangesNothing();
|
||||
};
|
||||
|
||||
void TypeFontPickerTests::aFaceIsChosenKeptAndCutShort()
|
||||
{
|
||||
Bar shown;
|
||||
auto &font = find<QComboBox>(shown.bar, "typeFont");
|
||||
// Open text on a layer: a face restyles its draft.
|
||||
addText(shown.session, QStringLiteral("Face"));
|
||||
shown.session.editActiveText();
|
||||
QVERIFY(shown.session.textDraft().has_value());
|
||||
font.showPopup();
|
||||
font.hidePopup();
|
||||
// A face writes its name; the loaded list keeps Helvetica.
|
||||
const int bold = font.findText(QStringLiteral("DejaVuSans-Bold"));
|
||||
QVERIFY(bold >= 0);
|
||||
emit font.activated(bold);
|
||||
QCOMPARE(shown.session.textDraft().value().style.fontName, QString("DejaVuSans-Bold"));
|
||||
QCOMPARE(font.currentText(), QString("DejaVuSans-Bold"));
|
||||
QVERIFY(font.findText(QStringLiteral("Helvetica")) >= 0);
|
||||
QCOMPARE(font.count(), TextLayout::availableFonts().size() + 1);
|
||||
// A missing face named elsewhere joins the list's end.
|
||||
shown.restyle([](LayerTextStyle &style) { style.fontName = QStringLiteral("Zapfino"); });
|
||||
QCOMPARE(font.currentText(), QString("Zapfino"));
|
||||
QCOMPARE(font.count(), TextLayout::availableFonts().size() + 2);
|
||||
QCOMPARE(font.itemText(font.count() - 1), QString("Zapfino"));
|
||||
shown.restyle([](LayerTextStyle &style) { style.fontName = QStringLiteral("Aachen"); });
|
||||
QVERIFY(font.currentText() == QString("Aachen") && font.itemText(font.count() - 1) == QString("Aachen") && font.findText("Zapfino") >= 0);
|
||||
// An open menu is left alone; closed, it follows.
|
||||
font.showPopup();
|
||||
QVERIFY(font.findText("Zapfino") >= 0);
|
||||
shown.restyle([](LayerTextStyle &style) { style.fontName = QStringLiteral("Zapfino"); });
|
||||
QCOMPARE(font.currentText(), QString("Aachen"));
|
||||
font.hidePopup();
|
||||
QTRY_VERIFY(!font.view()->isVisible());
|
||||
shown.restyle([](LayerTextStyle &style) { style.fontName = QStringLiteral("Zapfino"); });
|
||||
QCOMPARE(font.currentText(), QString("Zapfino"));
|
||||
// A long name is cut, the width kept.
|
||||
QStyleOptionComboBox option;
|
||||
option.initFrom(&font);
|
||||
const int room = font.style()->subControlRect(QStyle::CC_ComboBox, &option, QStyle::SC_ComboBoxEditField, &font).width() - 2;
|
||||
const auto image = [](QWidget &widget) { return widget.grab().toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); };
|
||||
// One name far past the room, one just past.
|
||||
QString nearly;
|
||||
for (int wide = 0; wide < 40 && nearly.isEmpty(); ++wide) {
|
||||
for (int thin = 0; thin < 40 && nearly.isEmpty(); ++thin) {
|
||||
const QString name = QString(wide, QLatin1Char('W')) + QString(thin, QLatin1Char('i'));
|
||||
const int advance = font.fontMetrics().horizontalAdvance(name);
|
||||
if (advance > room && advance <= room + 2)
|
||||
nearly = name;
|
||||
}
|
||||
}
|
||||
QVERIFY(!nearly.isEmpty());
|
||||
for (const QString &name : {QString(120, QLatin1Char('W')), nearly}) {
|
||||
shown.restyle([&](LayerTextStyle &style) { style.fontName = name; });
|
||||
QCOMPARE(font.width(), 210);
|
||||
QComboBox plain;
|
||||
plain.setFixedSize(font.size());
|
||||
plain.setPalette(font.palette());
|
||||
plain.setFont(font.font());
|
||||
plain.addItem(font.fontMetrics().elidedText(name, Qt::ElideRight, room));
|
||||
QVERIFY(plain.currentText().endsWith(QChar(0x2026)));
|
||||
QCOMPARE(image(font), image(plain));
|
||||
}
|
||||
}
|
||||
|
||||
void TypeFontPickerTests::theFaceShownChangesNothing()
|
||||
{
|
||||
Bar shown;
|
||||
auto &font = find<QComboBox>(shown.bar, "typeFont");
|
||||
addText(shown.session, QStringLiteral("Face"));
|
||||
shown.session.selectTool(NavigationTool::move);
|
||||
QVERIFY(!shown.session.textDraft());
|
||||
font.showPopup();
|
||||
font.hidePopup();
|
||||
QSignalSpy changes(&shown.session, &EditorSession::changed);
|
||||
emit font.activated(font.findText(QStringLiteral("Helvetica")));
|
||||
QVERIFY(!shown.session.textDraft());
|
||||
QCOMPARE(shown.session.tool(), NavigationTool::move);
|
||||
QCOMPARE(int(changes.count()), 0);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TypeFontPickerTests)
|
||||
#include "TypeFontPickerTests.moc"
|
||||
Reference in new issue
Block a user