From 306247e4ef9ebf04ab896a4828d09e3a5c41295f Mon Sep 17 00:00:00 2001 From: Mitch Curtis Date: Thu, 3 Jan 2019 08:55:47 +0100 Subject: [PATCH] Add Opacity dialog Fixes #108 --- app/qml/main.qml | 9 + app/qml/qml.qbs | 1 + app/qml/ui/+nativemenubar/MenuBar.qml | 8 + app/qml/ui/DoubleTextField.qml | 4 + app/qml/ui/HueSaturationDialog.qml | 8 +- app/qml/ui/MenuBar.qml | 8 + app/qml/ui/OpacityDialog.qml | 195 ++++++++++++++++++ lib/imagecanvas.cpp | 8 +- lib/imagecanvas.h | 17 +- lib/utils.cpp | 32 ++- lib/utils.h | 5 +- tests/auto/resources.qrc | 3 + .../resources/hueSaturation-hue-decreased.png | Bin 126 -> 126 bytes .../resources/hueSaturation-hue-increased.png | Bin 127 -> 127 bytes .../hueSaturation-lightness-decreased.png | Bin 126 -> 126 bytes .../hueSaturation-lightness-increased.png | Bin 126 -> 126 bytes .../hueSaturation-saturation-decreased.png | Bin 126 -> 126 bytes .../hueSaturation-saturation-increased.png | Bin 126 -> 126 bytes .../opacityDialog-alpha-decreased.png | Bin 0 -> 127 bytes .../opacityDialog-alpha-increased.png | Bin 0 -> 127 bytes .../auto/resources/opacityDialog-original.png | Bin 0 -> 127 bytes tests/auto/tst_app.cpp | 134 +++++++++++- 22 files changed, 417 insertions(+), 15 deletions(-) create mode 100644 app/qml/ui/OpacityDialog.qml create mode 100644 tests/auto/resources/opacityDialog-alpha-decreased.png create mode 100644 tests/auto/resources/opacityDialog-alpha-increased.png create mode 100644 tests/auto/resources/opacityDialog-original.png diff --git a/app/qml/main.qml b/app/qml/main.qml index 20100b61..7e2ede85 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -158,6 +158,7 @@ ApplicationWindow { id: menuBar canvas: window.canvas hueSaturationDialog: hueSaturationDialog + opacityDialog: opacityDialog canvasSizePopup: canvasSizePopup imageSizePopup: imageSizePopup moveContentsDialog: moveContentsDialog @@ -418,6 +419,14 @@ ApplicationWindow { canvas: window.canvas } + Ui.OpacityDialog { + id: opacityDialog + parent: Overlay.overlay + anchors.centerIn: parent + project: projectManager.project + canvas: window.canvas + } + Ui.CanvasSizePopup { id: canvasSizePopup parent: Overlay.overlay diff --git a/app/qml/qml.qbs b/app/qml/qml.qbs index 26c60b59..f477b0d4 100644 --- a/app/qml/qml.qbs +++ b/app/qml/qml.qbs @@ -51,6 +51,7 @@ Group { "ui/NewLayeredImageProjectPopup.qml", "ui/NewProjectPopup.qml", "ui/NewTilesetProjectPopup.qml", + "ui/OpacityDialog.qml", "ui/OptionsDialog.qml", "ui/Panel.qml", "ui/ProjectTemplateButton.qml", diff --git a/app/qml/ui/+nativemenubar/MenuBar.qml b/app/qml/ui/+nativemenubar/MenuBar.qml index 2bd6a410..403264bb 100644 --- a/app/qml/ui/+nativemenubar/MenuBar.qml +++ b/app/qml/ui/+nativemenubar/MenuBar.qml @@ -30,6 +30,7 @@ Item { readonly property bool isImageProjectType: projectType === Project.ImageType || projectType === Project.LayeredImageType property var hueSaturationDialog + property var opacityDialog property var canvasSizePopup property var imageSizePopup property var moveContentsDialog @@ -255,6 +256,13 @@ Item { enabled: isImageProjectType && canvas && canvas.hasSelection onTriggered: hueSaturationDialog.open() } + + Platform.MenuItem { + objectName: "opacityMenuItem" + text: qsTr("Opacity...") + enabled: isImageProjectType && canvas && canvas.hasSelection + onTriggered: opacityDialog.open() + } } Platform.MenuSeparator {} diff --git a/app/qml/ui/DoubleTextField.qml b/app/qml/ui/DoubleTextField.qml index 798aee74..2bc50d6f 100644 --- a/app/qml/ui/DoubleTextField.qml +++ b/app/qml/ui/DoubleTextField.qml @@ -33,6 +33,10 @@ TextField { property string propertyName signal valueModified + function clamp(value) { + return Math.max(-1, Math.min(value, 1)) + } + Keys.onDownPressed: { let newValue = clamp(propertySource[propertyName] - 0.01) if (propertySource[propertyName] !== newValue) { diff --git a/app/qml/ui/HueSaturationDialog.qml b/app/qml/ui/HueSaturationDialog.qml index c69293c1..c7448f78 100644 --- a/app/qml/ui/HueSaturationDialog.qml +++ b/app/qml/ui/HueSaturationDialog.qml @@ -23,6 +23,8 @@ import QtQuick.Controls 2.5 import App 1.0 +import "." as Ui + Dialog { id: root objectName: "hueSaturationDialog" @@ -39,6 +41,7 @@ Dialog { property real hslHue property real hslSaturation property real hslLightness + property real hslAlpha readonly property real sliderStepSize: 0.001 @@ -57,15 +60,12 @@ Dialog { canvas.modifySelectionHsl(hslHue, hslSaturation, hslLightness) } - function clamp(value) { - return Math.max(-1, Math.min(value, 1)) - } - onAboutToShow: { if (project) { hslHue = 0 hslSaturation = 0 hslLightness = 0 + hslAlpha = 0 hueTextField.forceActiveFocus() diff --git a/app/qml/ui/MenuBar.qml b/app/qml/ui/MenuBar.qml index 311cb71a..17ad12b2 100644 --- a/app/qml/ui/MenuBar.qml +++ b/app/qml/ui/MenuBar.qml @@ -34,6 +34,7 @@ Controls.MenuBar { readonly property bool isImageProjectType: projectType === Project.ImageType || projectType === Project.LayeredImageType property var hueSaturationDialog + property var opacityDialog property var canvasSizePopup property var imageSizePopup property var moveContentsDialog @@ -271,6 +272,13 @@ Controls.MenuBar { enabled: isImageProjectType && canvas && canvas.hasSelection onTriggered: hueSaturationDialog.open() } + + MenuItem { + objectName: "opacityMenuItem" + text: qsTr("Opacity...") + enabled: isImageProjectType && canvas && canvas.hasSelection + onTriggered: opacityDialog.open() + } } MenuSeparator {} diff --git a/app/qml/ui/OpacityDialog.qml b/app/qml/ui/OpacityDialog.qml new file mode 100644 index 00000000..ecd55e60 --- /dev/null +++ b/app/qml/ui/OpacityDialog.qml @@ -0,0 +1,195 @@ +/* + Copyright 2019, Mitch Curtis + + This file is part of Slate. + + Slate is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Slate is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Slate. If not, see . +*/ + +import QtQuick 2.12 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.5 + +import App 1.0 + +import "." as Ui + +Dialog { + id: root + objectName: "opacityDialog" + title: qsTr("Opacity") + modal: true + dim: false + focus: true + + property Project project + property ImageCanvas canvas + + property real hslAlpha + + readonly property real sliderStepSize: 0.001 + + TextMetrics { + id: valueTextMetrics + font: opacityTextField.font + text: "-0.0001" + } + + function modifySelectionHsl() { + var flags = ImageCanvas.DefaultAlphaAdjustment + if (doNotModifyFullyTransparentPixelsCheckBox.checked) + flags |= ImageCanvas.DoNotModifyFullyTransparentPixels + if (doNotModifyFullyOpaquePixelsCheckBox.checked) + flags |= ImageCanvas.DoNotModifyFullyOpaquePixels + canvas.modifySelectionHsl(0, 0, 0, hslAlpha, flags) + } + + onAboutToShow: { + if (project) { + hslAlpha = 0 + + opacityTextField.forceActiveFocus() + + canvas.beginModifyingSelectionHsl() + } + } + + onClosed: { + canvas.forceActiveFocus() + if (canvas.adjustingImage) { + // The dialog can be closed in two ways, so it's easier just to discard any adjustment here. + canvas.endModifyingSelectionHsl(ImageCanvas.RollbackAdjustment) + } + } + + contentItem: GridLayout { + columns: 3 + columnSpacing: 24 + rowSpacing: 0 + + Label { + text: qsTr("Opacity") + + Layout.fillWidth: true + } + Slider { + id: opacitySlider + objectName: root.objectName + "OpacitySlider" + from: -1 + to: 1 + value: hslAlpha + stepSize: sliderStepSize + leftPadding: 0 + rightPadding: 0 + + ToolTip.text: qsTr("Changes the opacity of the image") + + onMoved: { + hslAlpha = value + modifySelectionHsl() + } + + background: Item { + // Default Material values. + implicitWidth: 200 + implicitHeight: 48 + + HorizontalGradientRectangle { + width: parent.width + gradient: Gradient { + GradientStop { + position: 0 + color: "transparent" + } + GradientStop { + position: 1 + color: Ui.CanvasColours.focusColour + } + } + anchors.verticalCenter: parent.verticalCenter + } + } + } + DoubleTextField { + id: opacityTextField + objectName: root.objectName + "OpacityTextField" + propertySource: root + propertyName: "hslAlpha" + // If the maximum length matches the text metrics text, the sign + // character will sometimes be slightly cut off, so account for that. + maximumLength: valueTextMetrics.text.length - 1 + + Layout.maximumWidth: valueTextMetrics.width + Layout.fillWidth: true + + // We call this here instead of just doing it in e.g. onHslHueChanged + // because that would require us to block changes that occur when the + // HSL property values are reset upon showing the dialog. Modifying the + // selection only on user interaction instead feels nicer. + onValueModified: modifySelectionHsl() + } + + CheckBox { + id: doNotModifyFullyTransparentPixelsCheckBox + objectName: "doNotModifyFullyTransparentPixelsCheckBox" + text: qsTr("Do not modify fully transparent pixels") + checked: true + + Layout.columnSpan: 3 + + ToolTip.text: qsTr("Only change the alpha if it's non-zero to prevent fully transparent pixels from gaining opacity.") + ToolTip.visible: hovered + ToolTip.delay: toolTipDelay + + onClicked: modifySelectionHsl() + } + + CheckBox { + id: doNotModifyFullyOpaquePixelsCheckBox + objectName: "doNotModifyFullyOpaquePixelsCheckBox" + text: qsTr("Do not modify fully opaque pixels") + checked: true + + Layout.columnSpan: 3 + + ToolTip.text: qsTr("Only change the alpha if it's less than one to prevent fully opaque pixels from losing opacity.") + ToolTip.visible: hovered + ToolTip.delay: toolTipDelay + + onClicked: modifySelectionHsl() + } + } + + footer: DialogButtonBox { + Button { + objectName: "opacityDialogOkButton" + text: qsTr("OK") + + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + + onClicked: { + canvas.endModifyingSelectionHsl(ImageCanvas.CommitAdjustment) + root.close() + } + } + Button { + objectName: "opacityDialogCancelButton" + text: qsTr("Cancel") + + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + + onClicked: root.close() + } + } +} diff --git a/lib/imagecanvas.cpp b/lib/imagecanvas.cpp index 1d053b3f..48b69b66 100644 --- a/lib/imagecanvas.cpp +++ b/lib/imagecanvas.cpp @@ -1840,7 +1840,8 @@ void ImageCanvas::beginModifyingSelectionHsl() emit adjustingImageChanged(); } -void ImageCanvas::modifySelectionHsl(qreal hue, qreal saturation, qreal lightness) +void ImageCanvas::modifySelectionHsl(qreal hue, qreal saturation, qreal lightness, qreal alpha, + AlphaAdjustmentFlags alphaAdjustmentFlags) { if (!isAdjustingImage()) { qWarning() << "Not adjusting an image; can't modify selection's HSL"; @@ -1848,12 +1849,13 @@ void ImageCanvas::modifySelectionHsl(qreal hue, qreal saturation, qreal lightnes } qCDebug(lcImageCanvasSelection).nospace() << "modifying HSL of selection" - << mSelectionArea << " with h=" << hue << " s=" << saturation << " l=" << lightness; + << mSelectionArea << " with h=" << hue << " s=" << saturation << " l=" << lightness << " a=" << alpha + << "alpha flags=" << alphaAdjustmentFlags; // Copy the original so we don't just modify the result of the last adjustment (if any). mSelectionContents = mSelectionContentsBeforeImageAdjustment; - Utils::modifyHsl(mSelectionContents, hue, saturation, lightness); + Utils::modifyHsl(mSelectionContents, hue, saturation, lightness, alpha, alphaAdjustmentFlags); // Set this so that the check in shouldDrawSelectionPreviewImage() evaluates to true. setLastSelectionModification(SelectionHsl); diff --git a/lib/imagecanvas.h b/lib/imagecanvas.h index 48c99367..253f9c0b 100644 --- a/lib/imagecanvas.h +++ b/lib/imagecanvas.h @@ -288,6 +288,20 @@ class SLATE_EXPORT ImageCanvas : public QQuickItem Q_ENUM(AdjustmentAction) + enum AlphaAdjustmentOption { + // Modify the alpha regardless of what its current value is. + DefaultAlphaAdjustment = 0x00, + // Only change the alpha if it's non-zero to prevent fully transparent + // pixels (#00000000) gaining opacity. + DoNotModifyFullyTransparentPixels = 0x01, + // Only change the alpha if it's less than one to prevent fully opaque + // pixels (#FF000000) losing opacity. + DoNotModifyFullyOpaquePixels = 0x02 + }; + + Q_DECLARE_FLAGS(AlphaAdjustmentFlags, AlphaAdjustmentOption) + Q_ENUM(AlphaAdjustmentFlags) + signals: void projectChanged(); void zoomLevelChanged(); @@ -344,7 +358,8 @@ public slots: void flipSelection(Qt::Orientation orientation); void rotateSelection(int angle); void beginModifyingSelectionHsl(); - void modifySelectionHsl(qreal hue, qreal saturation, qreal lightness); + void modifySelectionHsl(qreal hue, qreal saturation, qreal lightness, qreal alpha = 0.0, + AlphaAdjustmentFlags alphaAdjustmentFlags = DefaultAlphaAdjustment); void endModifyingSelectionHsl(AdjustmentAction adjustmentAction); void copySelection(); void paste(); diff --git a/lib/utils.cpp b/lib/utils.cpp index f48cd0c6..2935de4a 100644 --- a/lib/utils.cpp +++ b/lib/utils.cpp @@ -159,17 +159,41 @@ QRect Utils::ensureWithinArea(const QRect &rect, const QSize &boundsSize) return newArea; } -void Utils::modifyHsl(QImage &image, qreal hue, qreal saturation, qreal lightness) +void Utils::modifyHsl(QImage &image, qreal hue, qreal saturation, qreal lightness, qreal alpha, + ImageCanvas::AlphaAdjustmentFlags alphaAdjustmentFlags) { for (int y = 0; y < image.height(); ++y) { for (int x = 0; x < image.width(); ++x) { const QColor rgb = image.pixelColor(x, y); QColor hsl = rgb.toHsl(); + + const bool doNotModifyFullyTransparentPixels = alphaAdjustmentFlags.testFlag(ImageCanvas::DoNotModifyFullyTransparentPixels); + const bool doNotModifyFullyOpaquePixels = alphaAdjustmentFlags.testFlag(ImageCanvas::DoNotModifyFullyOpaquePixels); + // By default, modify the alpha. + bool modifyAlpha = !doNotModifyFullyTransparentPixels && !doNotModifyFullyOpaquePixels; + qreal finalAlpha = hsl.alphaF(); + if (!modifyAlpha) { + // At least one of the flags was set, so check further if we should modify. + const bool isFullyTransparent = qFuzzyCompare(hsl.alphaF(), 0.0); + const bool isFullyOpaque = qFuzzyCompare(hsl.alphaF(), 1.0); + + if (doNotModifyFullyTransparentPixels && doNotModifyFullyOpaquePixels) + modifyAlpha = !isFullyTransparent && !isFullyOpaque; + else if (doNotModifyFullyTransparentPixels) + modifyAlpha = !isFullyTransparent; + else if (doNotModifyFullyOpaquePixels) + modifyAlpha = !isFullyOpaque; + } + if (modifyAlpha) + finalAlpha = hsl.alphaF() + alpha; + hsl.setHslF( - qBound(0.0, hsl.hueF() + hue, 1.0), - qBound(0.0, hsl.saturationF() + saturation, 1.0), + qBound(0.0, hsl.hslHueF() + hue, 1.0), + qBound(0.0, hsl.hslSaturationF() + saturation, 1.0), qBound(0.0, hsl.lightnessF() + lightness, 1.0), - rgb.alphaF()); + // Only increase the alpha if it's non-zero to prevent fully transparent + // pixels (#00000000) becoming black (#FF000000). + qBound(0.0, finalAlpha, 1.0)); image.setPixelColor(x, y, hsl.toRgb()); } } diff --git a/lib/utils.h b/lib/utils.h index eb713bae..6648b3a4 100644 --- a/lib/utils.h +++ b/lib/utils.h @@ -24,6 +24,8 @@ #include #include +#include "imagecanvas.h" + namespace Utils { QImage paintImageOntoPortionOfImage(const QImage &image, const QRect &portion, const QImage &replacementImage); @@ -34,7 +36,8 @@ namespace Utils { QImage rotate(const QImage &image, int angle); QImage rotateAreaWithinImage(const QImage &image, const QRect &area, int angle, QRect &inRotatedArea); - void modifyHsl(QImage &image, qreal hue, qreal saturation, qreal lightness); + void modifyHsl(QImage &image, qreal hue, qreal saturation, qreal lightness, qreal alpha, + ImageCanvas::AlphaAdjustmentFlags alphaAdjustmentFlags); void strokeRectWithDashes(QPainter *painter, const QRect &rect); diff --git a/tests/auto/resources.qrc b/tests/auto/resources.qrc index 8c66b432..8c790561 100644 --- a/tests/auto/resources.qrc +++ b/tests/auto/resources.qrc @@ -28,5 +28,8 @@ resources/version-check-v0.2.1.stp resources/swatch-paint.net-invalid-1.txt resources/swatch-paint.net-valid-1.txt + resources/opacityDialog-original.png + resources/opacityDialog-alpha-decreased.png + resources/opacityDialog-alpha-increased.png diff --git a/tests/auto/resources/hueSaturation-hue-decreased.png b/tests/auto/resources/hueSaturation-hue-decreased.png index 3d213e8676fbdb717f0939745c861ce198c4d4f1..71608dc791dbc90d1f8ce700206fc325ec1355f8 100644 GIT binary patch delta 58 zcmb=co8W1BCu;jUdpp-GN5>wYBeOheTXlGVAiU%W+kvb_qC&h3^>gTe~DWM4fNX!yB diff --git a/tests/auto/resources/hueSaturation-hue-increased.png b/tests/auto/resources/hueSaturation-hue-increased.png index f08ce081b3c86e9e59d5f1224fc52c431840238c..c12595457afae06c9fdad06ff268bc80e648dfea 100644 GIT binary patch delta 59 zcmb=gpWtbB$7w52D{?b4Gaq)e{mr;xRz|A}Gect}tHaKu%u)s* N@O1TaS?83{1OTB_6psJ^ delta 58 zcmb=co8W2sW~Sw5dplRXmoIyKj?D6?ZPnocf^yZV><6+Ii3;&D6#Zk>j_Y53kpT!i MUHx3vIVCg!0J%;Ti2wiq diff --git a/tests/auto/resources/hueSaturation-lightness-increased.png b/tests/auto/resources/hueSaturation-lightness-increased.png index f5a0960fac149ecd4b38d2e96443b02cee92fda1..a2c435f486889c836ef37cc1f3ce0313a81fc7b6 100644 GIT binary patch delta 58 zcmb=co8W1>XII5Xdpp;pU%z^Mj?D6?ZPnocg7d8^><6+Ii3;&DG$yjD2>3V5V*mnA LS3j3^P6<6+Ii3;&D)G)HjWd2>-!~g`I Lu6{1-oD!MgTe~DWM4fDIO7< delta 58 zcmb=co8W00b9>86dpp;ty1E{pBeOheTXlGVpnORP+kvb_qC&h3wa%f4aTa()7Bet#3xhBt!>lf4aTa()7Bet#3xhBt!>ljc)5 U6CTcp0%~UPboFyt=akR{0FiJY&Hw-a literal 0 HcmV?d00001 diff --git a/tests/auto/resources/opacityDialog-original.png b/tests/auto/resources/opacityDialog-original.png new file mode 100644 index 0000000000000000000000000000000000000000..ee779cd0ee4f47cd6d27966191707abcb9c3794d GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4aTa()7Bet#3xhBt!>l=9^yR>SSYh*vGoQ TVrCA|BnAdgS3j3^P6ETU literal 0 HcmV?d00001 diff --git a/tests/auto/tst_app.cpp b/tests/auto/tst_app.cpp index 4ca650f8..517ab068 100644 --- a/tests/auto/tst_app.cpp +++ b/tests/auto/tst_app.cpp @@ -138,6 +138,8 @@ private Q_SLOTS: void rotateSelectionTransparentBackground(); void hueSaturation_data(); void hueSaturation(); + void opacityDialog_data(); + void opacityDialog(); void fillImageCanvas_data(); void fillImageCanvas(); @@ -3677,7 +3679,8 @@ void tst_App::hueSaturation() canvas->currentPane()->setZoomLevel(48); // Paste in the original image. - const QImage originalImage(":/resources/hueSaturation-original.png"); + const QString originalImagePath = QLatin1String(":/resources/hueSaturation-original.png"); + const QImage originalImage(originalImagePath); QVERIFY(!originalImage.isNull()); qGuiApp->clipboard()->setImage(originalImage); QVERIFY2(triggerPaste(), failureMessage); @@ -3709,7 +3712,8 @@ void tst_App::hueSaturation() qPrintable(QString::fromLatin1("Expected HSL value of %1 but got %2").arg(expectedHslValue).arg(hslValue))); const QImage expectedImage(expectedImagePath); - QVERIFY(!expectedImage.isNull()); + QVERIFY2(!expectedImage.isNull(), qPrintable(QString::fromLatin1( + "Expected image at %1 could not be loaded").arg(expectedImagePath))); // The changes should be rendered... QCOMPARE(canvas->contentImage().convertToFormat(QImage::Format_ARGB32), expectedImage); // ... but not committed yet. @@ -3756,6 +3760,132 @@ void tst_App::hueSaturation() QCOMPARE(canvas->currentProjectImage()->convertToFormat(QImage::Format_ARGB32), expectedImage); } +void tst_App::opacityDialog_data() +{ + QTest::addColumn("projectType"); + QTest::addColumn("expectedImagePath"); + QTest::addColumn("textFieldObjectName"); + QTest::addColumn("increase"); + + QMap projectTypes; + projectTypes.insert("ImageType", Project::ImageType); + // TODO: investigate why layered projects are off one by pixel + // in the rendered image vs expected image comparison +// projectTypes.insert("LayeredImageType", Project::LayeredImageType); + foreach (const auto projectTypeString, projectTypes.keys()) { + const Project::Type projectType = projectTypes.value(projectTypeString); + + QTest::newRow(qPrintable(projectTypeString + QLatin1String(",increasedAlpha"))) + << projectType << ":/resources/opacityDialog-alpha-increased.png" << "opacityDialogOpacityTextField" << true; + QTest::newRow(qPrintable(projectTypeString + QLatin1String(",decreasedAlpha"))) + << projectType << ":/resources/opacityDialog-alpha-decreased.png" << "opacityDialogOpacityTextField" << false; + } +} + +void tst_App::opacityDialog() +{ + QFETCH(Project::Type, projectType); + QFETCH(QString, expectedImagePath); + QFETCH(QString, textFieldObjectName); + QFETCH(bool, increase); + + QVariantMap args; + args.insert("imageWidth", QVariant(10)); + args.insert("imageHeight", QVariant(10)); + args.insert("transparentImageBackground", QVariant(true)); + QVERIFY2(createNewProject(projectType, args), failureMessage); + + // Zoom in to make visual debugging easier. + canvas->setSplitScreen(false); + canvas->currentPane()->setZoomLevel(48); + + // Paste in the original image. + const QString originalImagePath = QLatin1String(":/resources/opacityDialog-original.png"); + const QImage originalImage(originalImagePath); + QVERIFY(!originalImage.isNull()); + qGuiApp->clipboard()->setImage(originalImage); + QVERIFY2(triggerPaste(), failureMessage); + QTest::keyClick(window, Qt::Key_Escape); + QVERIFY(project->hasUnsavedChanges()); + + // Select everything. + QTest::keySequence(window, QKeySequence::SelectAll); + + // Open the dialog manually cause native menus. + QObject *opacityDialog = window->findChild("opacityDialog"); + QVERIFY(opacityDialog); + QVERIFY(QMetaObject::invokeMethod(opacityDialog, "open")); + QTRY_COMPARE(opacityDialog->property("opened").toBool(), true); + + // Increase/decrease the value. + QQuickItem *opacityDialogOpacityTextField + = window->findChild(textFieldObjectName); + QVERIFY(opacityDialogOpacityTextField); + opacityDialogOpacityTextField->forceActiveFocus(); + QVERIFY(opacityDialogOpacityTextField->hasActiveFocus()); + // Tried to do this with QTest::keyClick() but I couldn't get it to work: + // hyphens and backspace (with text selected) did nothing with the default style. + // Also can't do it with up/down keys because of floating point precision issues. + const qreal expectedAlphaValue = increase ? 0.10 : -0.10; + QVERIFY(opacityDialogOpacityTextField->setProperty("text", QString::number(expectedAlphaValue))); + qreal alphaValue = opacityDialogOpacityTextField->property("text").toString().toDouble(); + QVERIFY2(qAbs(alphaValue - expectedAlphaValue) < 0.01, + qPrintable(QString::fromLatin1("Expected alhpa value of %1 but got %2").arg(expectedAlphaValue).arg(alphaValue))); + // TODO: more hacks until we get input in the test working properly + QVERIFY(opacityDialog->setProperty("hslAlpha", expectedAlphaValue)); + QVERIFY(QMetaObject::invokeMethod(opacityDialog, "modifySelectionHsl")); + + const QImage expectedImage(expectedImagePath); + QVERIFY2(!expectedImage.isNull(), qPrintable(QString::fromLatin1( + "Expected image at %1 could not be loaded").arg(expectedImagePath))); + // The changes should be rendered... + QCOMPARE(canvas->contentImage().convertToFormat(QImage::Format_ARGB32), expectedImage); + // ... but not committed yet. + QCOMPARE(canvas->currentProjectImage()->convertToFormat(QImage::Format_ARGB32), originalImage); + + // Cancel the dialog; the changes should not be applied. + QQuickItem *opacityDialogCancelButton + = window->findChild("opacityDialogCancelButton"); + QVERIFY(opacityDialogCancelButton); + mouseEventOnCentre(opacityDialogCancelButton, MouseClick); + QTRY_COMPARE(opacityDialog->property("visible").toBool(), false); + QCOMPARE(canvas->contentImage().convertToFormat(QImage::Format_ARGB32), originalImage); + QCOMPARE(canvas->currentProjectImage()->convertToFormat(QImage::Format_ARGB32), originalImage); + + // Re-open the dialog and increase/decrease the value again. + QVERIFY(QMetaObject::invokeMethod(opacityDialog, "open")); + QTRY_COMPARE(opacityDialog->property("opened").toBool(), true); + // There was an issue where reopening the dialog after changing some values the last + // time it was opened (even if it was cancelled) would cause the selection contents to disappear. + QCOMPARE(canvas->contentImage().convertToFormat(QImage::Format_ARGB32), originalImage); + QCOMPARE(canvas->currentProjectImage()->convertToFormat(QImage::Format_ARGB32), originalImage); + opacityDialogOpacityTextField->forceActiveFocus(); + QVERIFY(opacityDialogOpacityTextField->hasActiveFocus()); + QVERIFY(opacityDialogOpacityTextField->setProperty("text", QString::number(expectedAlphaValue))); + alphaValue = opacityDialogOpacityTextField->property("text").toString().toDouble(); + QVERIFY2(qAbs(alphaValue - expectedAlphaValue) < 0.01, + qPrintable(QString::fromLatin1("Expected HSL value of %1 but got %2").arg(expectedAlphaValue).arg(alphaValue))); + // TODO: more hacks until we get input in the test working properly + QVERIFY(opacityDialog->setProperty("hslAlpha", expectedAlphaValue)); + QVERIFY(QMetaObject::invokeMethod(opacityDialog, "modifySelectionHsl")); + + // The changes should be rendered... + QCOMPARE(canvas->contentImage().convertToFormat(QImage::Format_ARGB32), expectedImage); + // ... but not committed yet. + QCOMPARE(canvas->currentProjectImage()->convertToFormat(QImage::Format_ARGB32), originalImage); + + // Accept the dialog; the changes should be applied. + QQuickItem *opacityDialogOkButton + = window->findChild("opacityDialogOkButton"); + QVERIFY(opacityDialogOkButton); + mouseEventOnCentre(opacityDialogOkButton, MouseClick); + QTRY_COMPARE(opacityDialog->property("visible").toBool(), false); + // Confirm the selection to make the changes to the project's image. + QTest::keyClick(window, Qt::Key_Escape); + QCOMPARE(canvas->contentImage().convertToFormat(QImage::Format_ARGB32), expectedImage); + QCOMPARE(canvas->currentProjectImage()->convertToFormat(QImage::Format_ARGB32), expectedImage); +} + void tst_App::fillImageCanvas_data() { addImageProjectTypes();