Merge branch 'port-c'

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 18:06:09 -04:00
commit e243751f64
54 files changed
+1428 -233

No files matched your search

+13 -1
View File
@@ -11,6 +11,7 @@
#include "UI/ShapeControls.h"
#include "UI/TypeControls.h"
#include "UI/LassoControls.h"
#include "UI/LayerMaskMenu.h"
#include "UI/NavigationToolHeader.h"
#include "UI/TransformInspector.h"
#include "UI/NewCanvasSheet.h"
@@ -196,7 +197,8 @@ protected:
ContentView::ContentView(EditorSession &session, ProjectController *projects, QWidget *parent)
: QWidget(parent), m_session(session), m_projects(projects), m_column(new QVBoxLayout(this)), m_canvasSlot(new QGridLayout),
m_canvas(new CanvasView(session, this)), m_layersPanel(new LayersPanel(session, this)), m_dock(new QWidget(this)), m_dropRing(new DropRing(this)), m_zoom(statusText(new QLabel(this))), m_dimensions(statusText(new QLabel(this))), m_colour(statusText(new QLabel(this))),
m_canvas(new CanvasView(session, this)), m_layersPanel(new LayersPanel(session, this)), m_dock(new QWidget(this)), m_dropRing(new DropRing(this)),
m_maskAloneBadge(new MaskAloneBadge(session, this)), m_zoom(statusText(new QLabel(this))), m_dimensions(statusText(new QLabel(this))), m_colour(statusText(new QLabel(this))),
m_activity(statusText(new ElidedLabel(this))),
m_spinner(new QProgressBar(this))
{
@@ -240,6 +242,12 @@ ContentView::ContentView(EditorSession &session, ProjectController *projects, QW
// The welcome sits over the canvas, as Swift's ZStack.
m_canvasSlot->addWidget(m_canvas, 0, 0);
m_canvasSlot->addWidget(m_dropRing, 0, 0);
// A layout takes no clicks; the canvas keeps the rest.
auto *foot = new QVBoxLayout;
foot->addStretch(1);
foot->addWidget(m_maskAloneBadge, 0, Qt::AlignHCenter);
foot->addSpacing(14);
m_canvasSlot->addLayout(foot, 0, 0);
setAcceptDrops(true);
auto *middle = new QHBoxLayout;
@@ -390,6 +398,10 @@ void ContentView::synchronize()
}
const std::optional<CanvasDocument> &document = m_session.document();
showWelcome(!document);
const std::optional<ImageLayer> alone = m_session.maskAloneLayer();
if (alone)
m_maskAloneBadge->setLayerName(alone->name);
m_maskAloneBadge->setVisible(alone.has_value());
for (QWidget *ruler : m_rulers)
ruler->setVisible(m_session.showsRulers() && document);
m_zoom->setVisible(document.has_value());
+3
View File
@@ -14,6 +14,7 @@
#include <QWidget>
class LayersPanel;
class MaskAloneBadge;
class ToolButton;
class ProjectWorkspace;
class QFileDialog;
@@ -74,6 +75,8 @@ private:
QWidget *const m_dock;
// An accent ring while a drop may land.
QWidget *const m_dropRing;
// Over the canvas's foot while a mask shows alone.
MaskAloneBadge *const m_maskAloneBadge;
// Swift 1.1.7's rulers: the corner, then the two strips.
std::vector<QWidget *> m_rulers;
ProjectWorkspace *m_workspace = nullptr;
+2 -2
View File
@@ -1,4 +1,4 @@
// Swift's BlurTool extension: what a Blur stroke paints.
public:
// The active layer, or its mask, softened at document size.
std::optional<QImage> blurSample(const CanvasDocument &document, bool mask = false) const;
// The stroke's pixels or mask softened, in its own grid.
std::optional<BrushStroke::Clone> blurSample(const BrushStroke &stroke) const;
+25 -35
View File
@@ -2,49 +2,39 @@
#include "Document/PixelAdjust.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
#include "Rendering/LayerRenderer.h"
#include <cmath>
// Swift's BlurTool extension: the layer, or its mask, softened.
std::optional<QImage> EditorSession::blurSample(const CanvasDocument &document, bool mask) const
// Swift's BlurTool extension: the layer's pixels, or mask, softened.
std::optional<BrushStroke::Clone> EditorSession::blurSample(const BrushStroke &stroke) const
{
const std::optional<ImageLayer> layer = activeLayer();
if (!layer)
const ImageLayer &layer = stroke.layer;
const std::optional<ImportedImage> &asset = stroke.isMask ? (layer.mask ? std::optional(layer.mask->asset) : std::nullopt) : layer.asset;
if (!asset)
return std::nullopt;
// The softening follows the brush's size.
const double sigma = std::min(30.0, std::max(1.5, m_brushSettings.diameter / 10));
// The canvas's softening carried into the layer's pixels.
const QTransform &map = stroke.pixelToDocument;
const double perPixel = std::max(1e-6, std::sqrt(std::abs(map.m11() * map.m22() - map.m12() * map.m21())));
const double sidePixels = std::max(stroke.sourceRect.width(), stroke.sourceRect.height());
const double sigma = std::min(std::min(30.0, std::max(1.5, m_brushSettings.diameter / 10)) / perPixel, sidePixels / 2);
// Room for the blur to spread past the pixels' edges.
const double margin = std::ceil(3 * sigma);
const QRectF region = stroke.sourceRect.adjusted(-margin, -margin, margin, margin);
// A huge layer's sample is made coarser, within several canvases.
const qint64 canvas = m_document ? qint64(m_document->width) * m_document->height : 0;
const double budget = double(std::min(DocumentLimits::maxSurfacePixels, std::max<qint64>(16'000'000, 4 * canvas)));
const double fit = std::min(1.0, std::sqrt(budget / (region.width() * region.height())));
const int width = std::max(1, int(std::ceil(region.width() * fit))), height = std::max(1, int(std::ceil(region.height() * fit)));
const QRectF placed(margin * fit, margin * fit, stroke.sourceRect.width() * fit, stroke.sourceRect.height() * fit);
try {
if (mask) {
if (!layer->mask)
return std::nullopt;
QImage context = BrushRaster::context(document.width, document.height, true);
// Past its pixels a mask keeps its edge tone.
const int tone = int(std::round(LayerMask::background(layer->mask->asset.thumbnail) * 255));
context.fill(QColor(tone, tone, tone));
QPainter painter(&context);
// Coverage only adds white: the mask's area starts black.
const LayerTransform placement = layer->maskTransform();
painter.save();
QTransform box;
box.translate(placement.center().x(), placement.center().y());
box.rotateRadians(placement.radians());
painter.setTransform(box, true);
painter.fillRect(QRectF(-placement.size.width() / 2, -placement.size.height() / 2, placement.size.width(), placement.size.height()),
Qt::black);
painter.restore();
LayerRenderer::drawCoverage(layer->mask->asset.image(), placement, painter);
painter.end();
return PixelAdjust::gaussianBlur(context, sigma, true);
}
if (!layer->asset)
return std::nullopt;
QImage context = BrushRaster::context(document.width, document.height, false);
QImage context = BrushRaster::context(width, height, stroke.isMask);
// Past its pixels a mask keeps its edge tone.
if (stroke.isMask && layer.mask)
context.fill(int(std::round(LayerMask::background(layer.mask->asset.thumbnail) * 255)));
QPainter painter(&context);
// A painted asset flattens here; the catch covers it too.
const LayerTransform transform = displayedTransform(*layer);
LayerRenderer::draw(layer->asset->image(), transform, transform.center(), painter, {});
BrushRaster::draw(asset->image(), placed, painter);
painter.end();
return PixelAdjust::gaussianBlur(context, sigma, false);
return BrushStroke::Clone{PixelAdjust::gaussianBlur(context, sigma * fit, stroke.isMask), region, true};
} catch (const ExportError &error) {
qCWarning(lcApp) << "the Blur tool cannot sample the layer:" << error.what();
return std::nullopt;
+8
View File
@@ -58,6 +58,14 @@ LayerTransform BrushStroke::transform(const QRectF &bounds) const
return result;
}
// Offset into grid pixels, turned and scaled as the layer.
QRectF BrushStroke::gridRect(const QRectF &rect, QSizeF offset) const
{
const QTransform toGrid = pixelToDocument.inverted();
const QPointF shift = toGrid.map(QPointF(offset.width(), offset.height())) - toGrid.map(QPointF(0, 0));
return rect.translated(-shift);
}
// Swift's heal: the painted spot rebuilt from nearby texture.
void BrushStroke::heal()
{
+4 -3
View File
@@ -273,11 +273,12 @@ QImage BrushStroke::clonePixels(const Tile &tile) const
if (!clone)
return QImage();
const int width = int(tile.rect.width()), height = int(tile.rect.height());
// Tile pixels to clone pixels: grid, document, then offset.
const QTransform toClone = QTransform::fromTranslate(tile.rect.left(), tile.rect.top()) * pixelToDocument
* QTransform::fromTranslate(clone->offset.width(), clone->offset.height());
// Colour even for gray clones: edges carry their coverage.
const QImage &image = clone->image;
// Tile pixels to clone pixels: grid, maybe document, then placed.
const QRectF &placed = clone->placed;
const QTransform toClone = QTransform::fromTranslate(tile.rect.left(), tile.rect.top()) * (clone->inGrid ? QTransform() : pixelToDocument)
* QTransform::fromTranslate(-placed.left(), -placed.top()) * QTransform::fromScale(image.width() / placed.width(), image.height() / placed.height());
if (toClone.type() > QTransform::TxTranslate) {
// Swift's medium quality: bilinear under a scale or turn.
QImage sample = BrushRaster::context(width, height, false);
+5 -2
View File
@@ -111,10 +111,11 @@ public:
qint64 pixelLimit = DocumentLimits::documentPixelBudget();
// Limits every edit to the document selection; nil when none.
std::optional<SelectionClip> selectionClip;
// Clone Stamp: a document-sized image, and each point's offset.
// Painted through the tip, placed: document or `inGrid` pixels.
struct Clone {
QImage image;
QSizeF offset;
QRectF placed;
bool inGrid;
};
std::optional<Clone> clone;
// Blur: the clone is the layer softened, in place.
@@ -148,6 +149,8 @@ public:
QRectF committedBounds() const;
LayerTransform committedTransform() const;
LayerTransform transform(const QRectF &bounds) const;
// `rect` shifted to copy from `offset` document pixels away.
QRectF gridRect(const QRectF &rect, QSizeF offset) const;
// Spot Healing's end: the painted spot rebuilt from around it.
void heal();
PaintSnapshot paintSnapshot() const;
+2 -2
View File
@@ -6,5 +6,5 @@ public:
std::optional<QSizeF> cloneStrokeOffset(QPointF point) const;
// The source for a brush at `point`: the canvas's crosshair.
std::optional<QPointF> cloneSamplePoint(QPointF point) const;
// What a stroke copies from, document-sized, taken as it starts.
std::optional<QImage> cloneSample(const CanvasDocument &document) const;
// What `stroke` copies, `offset` away: its pixels or the canvas.
std::optional<BrushStroke::Clone> cloneSample(const CanvasDocument &document, const BrushStroke &stroke, QSizeF offset) const;
+9 -10
View File
@@ -1,7 +1,6 @@
#include "Document/EditorSession.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
#include "Rendering/LayerRenderer.h"
#include <cmath>
// Swift's CloneStamp extension: the source, its offset, the sample.
@@ -40,20 +39,20 @@ std::optional<QPointF> EditorSession::cloneSamplePoint(QPointF point) const
return point + QPointF(m_cloneOffset->width(), m_cloneOffset->height());
}
std::optional<QImage> EditorSession::cloneSample(const CanvasDocument &document) const
std::optional<BrushStroke::Clone> EditorSession::cloneSample(const CanvasDocument &document, const BrushStroke &stroke, QSizeF offset) const
{
try {
// A painted asset flattens here; the catch covers it too.
if (!m_cloneSettings.sampleAllLayers) {
if (!stroke.layer.asset)
return std::nullopt;
return BrushStroke::Clone{stroke.layer.asset->image(), stroke.gridRect(stroke.sourceRect, offset), true};
}
QImage context = BrushRaster::context(document.width, document.height, false);
QPainter painter(&context);
// A painted asset flattens here; the catch covers it too.
if (m_cloneSettings.sampleAllLayers) {
drawLiveComposite(document, painter);
} else if (const std::optional<ImageLayer> layer = activeLayer(); layer && layer->asset) {
const LayerTransform transform = displayedTransform(*layer);
LayerRenderer::draw(layer->asset->image(), transform, transform.center(), painter, {});
}
drawLiveComposite(document, painter);
painter.end();
return context;
return BrushStroke::Clone{context, QRectF(-offset.width(), -offset.height(), context.width(), context.height()), false};
} catch (const ExportError &error) {
qCWarning(lcApp) << "Clone Stamp cannot sample the canvas:" << error.what();
return std::nullopt;
+44 -19
View File
@@ -37,6 +37,27 @@ bool EditorSession::canPaint() const
&& (m_isMaskSelected || !layer->adjustment);
}
std::optional<QString> EditorSession::paintRefusal() const
{
const std::optional<ImageLayer> layer = activeLayer();
if (!canEditLayers() || !layer || canPaint())
return std::nullopt;
if (m_selectedLayerIDs.size() > 1)
return QStringLiteral("Several layers are selected. Select just one to paint on it.");
if (layer->isGroup && !m_isMaskSelected)
return QStringLiteral("“%1” is a folder, which has no pixels of its own. Paint on a layer inside it, or on the folder’s mask.").arg(layer->name);
if (!m_document->effectiveVisibleIDs().contains(layer->id))
return QStringLiteral("“%1” is hidden, or inside a hidden folder. Show it to paint on it.").arg(layer->name);
if (m_isMaskSelected && !(layer->mask && layer->mask->isEnabled))
return QStringLiteral("The layer mask is turned off. Shift-click its thumbnail to turn it on, then paint.");
if (!m_isMaskSelected && layer->adjustment)
return QStringLiteral("“%1” is an adjustment layer, with no pixels to paint. Paint on its mask instead.").arg(layer->name);
const std::optional<DocumentSelection> current = selection();
if (current && current->isEmpty())
return QStringLiteral("Nothing is selected, so there’s nowhere to paint. Choose Select › Deselect (Ctrl+D) to paint anywhere.");
return std::nullopt;
}
// A tiled raster edit of pixels or mask, within budget.
std::unique_ptr<BrushStroke> EditorSession::makeRasterEdit(const ImageLayer &layer, const BrushSettings &settings, bool growsMask) const
{
@@ -74,28 +95,20 @@ void EditorSession::beginBrush(QPointF point)
}
// Spot Healing and Clone Stamp rework pixels, never a mask.
const bool paintsMasks = m_tool == NavigationTool::brush || m_tool == NavigationTool::blur;
if ((!paintsMasks && (!isBrushTool(m_tool) || m_isMaskSelected)) || !canPaint())
if (!paintsMasks && (!isBrushTool(m_tool) || m_isMaskSelected))
return;
if (!canPaint()) {
setBrushError(paintRefusal());
return;
}
const ImageLayer layer = activeLayer().value();
std::optional<BrushStroke::Clone> clone;
std::optional<QSizeF> sourceOffset;
if (m_tool == NavigationTool::cloneStamp) {
const std::optional<QSizeF> offset = cloneStrokeOffset(point);
if (!offset) {
sourceOffset = cloneStrokeOffset(point);
if (!sourceOffset) {
setBrushError(QStringLiteral("Alt-click where Clone Stamp should copy from first."));
return;
}
const std::optional<QImage> image = cloneSample(m_document.value());
if (!image)
return;
m_cloneOffset = offset;
clone = BrushStroke::Clone{*image, *offset};
}
// Blur paints a softened copy of the layer, in place.
if (m_tool == NavigationTool::blur) {
const std::optional<QImage> image = blurSample(m_document.value(), m_isMaskSelected);
if (!image)
return;
clone = BrushStroke::Clone{*image, QSizeF(0, 0)};
}
finishOpacityEdit();
try {
@@ -108,9 +121,21 @@ void EditorSession::beginBrush(QPointF point)
settings.green = settings.red;
settings.blue = settings.red;
}
m_brushStroke = makeRasterEdit(layer, settings, m_tool == NavigationTool::brush);
m_brushStroke->clone = clone;
m_brushStroke->isBlur = m_tool == NavigationTool::blur;
std::unique_ptr<BrushStroke> stroke = makeRasterEdit(layer, settings, m_tool == NavigationTool::brush);
if (sourceOffset) {
stroke->clone = cloneSample(m_document.value(), *stroke, *sourceOffset);
if (!stroke->clone)
return;
m_cloneOffset = sourceOffset;
}
// Blur paints a softened copy of the layer, in place.
if (m_tool == NavigationTool::blur) {
stroke->clone = blurSample(*stroke);
if (!stroke->clone)
return;
}
stroke->isBlur = m_tool == NavigationTool::blur;
m_brushStroke = std::move(stroke);
m_brushStroke->append(point);
m_brushAnchor = point;
m_brushPointer = point;
+2
View File
@@ -1,6 +1,8 @@
// Swift's EditorSession+Brush extension: a stroke from press to commit.
public:
bool canPaint() const;
// Why the target refuses a stroke; none while busy.
std::optional<QString> paintRefusal() const;
std::unique_ptr<BrushStroke> makeRasterEdit(const ImageLayer &layer, const BrushSettings &settings = BrushSettings(), bool growsMask = false) const;
void beginBrush(QPointF point);
void continueBrush(QPointF point);
+2 -2
View File
@@ -27,7 +27,7 @@ std::optional<ProjectSnapshot> EditorSession::projectSnapshot() const
void EditorSession::installProject(const ProjectSnapshot &snapshot, const QString &path)
{
m_collapsedGroupIDs.clear();
m_isMaskSelected = false;
setIsMaskSelected(false);
m_cropRect.reset();
m_guideDrag = std::nullopt;
m_transformEdit = std::nullopt;
@@ -78,7 +78,7 @@ void EditorSession::setProjectPath(std::optional<QString> path)
void EditorSession::clearProject()
{
m_collapsedGroupIDs.clear();
m_isMaskSelected = false;
setIsMaskSelected(false);
m_cropRect.reset();
m_transformEdit = std::nullopt;
m_guideDrag = std::nullopt;
+17 -2
View File
@@ -35,7 +35,7 @@ void EditorSession::setActiveLayerID(std::optional<QUuid> id)
{
// Another layer: its pixels are the target again.
if (id != m_activeLayerID)
m_isMaskSelected = false;
setIsMaskSelected(false);
m_activeLayerID = id;
m_selectedLayerIDs = id ? QSet<QUuid>{*id} : QSet<QUuid>();
notify();
@@ -234,7 +234,7 @@ void EditorSession::restore(const DocumentHistory::Snapshot &snapshot)
m_document = snapshot.document;
setActiveLayerID(snapshot.activeLayerID);
const std::optional<ImageLayer> active = activeLayer();
m_isMaskSelected = keepsMaskTarget && active && active->mask;
setIsMaskSelected(keepsMaskTarget && active && active->mask);
if (changedCanvas && m_document)
viewport.fit(m_document->size());
qCInfo(lcApp) << "restored a history snapshot of" << (m_document ? int(m_document->layers.size()) : 0) << "layers";
@@ -381,3 +381,18 @@ void EditorSession::requestCanvasFocus()
++m_canvasFocusRequest;
notify();
}
void EditorSession::setIsMaskSelected(bool value)
{
m_isMaskSelected = value;
if (!value)
m_viewsMaskAlone = false;
}
void EditorSession::setViewsMaskAlone(bool value)
{
if (m_viewsMaskAlone == value)
return;
m_viewsMaskAlone = value;
notify();
}
+4 -3
View File
@@ -93,10 +93,11 @@ public:
void selectTool(NavigationTool value);
// Tab: the current tool's next mode.
void cycleToolMode();
// Whether the mask thumbnail, not the layer's, is the target.
bool isMaskSelected() const { return m_isMaskSelected; }
// Alt-click: the targeted mask alone, gray, on the canvas.
bool viewsMaskAlone() const { return m_viewsMaskAlone; }
void setViewsMaskAlone(bool value);
// A blend mode tried on by hovering; nothing is saved.
struct BlendPreview {
QUuid layerID;
@@ -132,7 +133,6 @@ public:
// An unlinked mask, selected, transforms without its layer.
bool transformTargetsMask() const;
void nudgeLayer(double dx, double dy);
const std::optional<LassoDraft> &lassoDraft() const { return m_lassoDraft; }
LassoKind lassoKind() const { return m_lassoKind; }
void setLassoKind(LassoKind kind);
@@ -350,6 +350,7 @@ private:
std::optional<BlendPreview> m_blendPreview;
std::optional<QUuid> m_opacityEditLayerID;
bool m_isMaskSelected = false;
bool m_viewsMaskAlone = false;
std::optional<QUuid> m_activeLayerID;
bool m_isProjectBusy = false;
bool m_showsNewDocument = false;
+1 -1
View File
@@ -347,7 +347,7 @@ void EditorSession::finishFilterCommit()
mask.isEnabled = true;
mask.placement = std::nullopt;
m_document->layers[index].mask = mask;
m_isMaskSelected = true;
setIsMaskSelected(true);
endEdit();
finish();
return;
+6 -1
View File
@@ -35,8 +35,13 @@ void EditorSession::setGradientSettings(const GradientSettings &settings)
void EditorSession::beginGradient(QPointF point)
{
const std::optional<ImageLayer> layer = activeLayer();
if (m_tool != NavigationTool::gradient || !(canPaint() || m_gradientEdit) || !layer)
if (m_tool != NavigationTool::gradient || !layer)
return;
// Swift explains only past this guard, where nothing refuses.
if (!canPaint() && !m_gradientEdit) {
setBrushError(paintRefusal());
return;
}
// A new line on this target replaces the pending one.
if (m_gradientEdit && m_gradientEdit->raster->layer.id == layer->id && m_gradientEdit->raster->isMask == m_isMaskSelected) {
m_gradientEdit->start = point;
+1 -1
View File
@@ -97,7 +97,7 @@ void EditorSession::selectEffect(LayerEffectKind kind, QUuid id, bool editing)
finishEffectsEditing(false);
// Swift's `selectedLayerIDs = [id]`: selectLayer sets it, unrefused here.
selectLayer(id);
m_isMaskSelected = false;
setIsMaskSelected(false);
m_effectSelection = selection;
if (editing && m_effectsEditing != selection) {
m_effectsEditingOriginal = m_document->layers[size_t(indexOf(m_document->layers, id))].effects.value_or(LayerEffects());
+21
View File
@@ -0,0 +1,21 @@
#include "Document/EditorSession.h"
// Swift's mask shown alone, an Alt-click on its thumbnail.
std::optional<ImageLayer> EditorSession::maskAloneLayer() const
{
const std::optional<ImageLayer> layer = activeLayer();
if (!m_viewsMaskAlone || !m_isMaskSelected || !layer || !layer->mask)
return std::nullopt;
return layer;
}
void EditorSession::toggleMaskAlone(QUuid id)
{
const std::optional<ImageLayer> shown = maskAloneLayer();
const bool showing = shown && shown->id == id;
selectLayerTarget(id, true);
if (m_activeLayerID != id || !m_isMaskSelected)
return;
m_viewsMaskAlone = !showing;
notify();
}
+6
View File
@@ -15,6 +15,12 @@ public:
std::optional<QImage> maskDistortPreview(const ImageLayer &layer) const;
// The selection made a mask; without one, a solid mask.
void addMask(bool revealing = true);
// The layer whose mask the canvas shows alone, if any.
std::optional<ImageLayer> maskAloneLayer() const;
// Alt-click on a mask thumbnail: alone, or the composite again.
void toggleMaskAlone(QUuid id);
private:
// Swift's didSet: the pixels targeted end the mask view.
void setIsMaskSelected(bool value);
void commitMaskTransform();
+8 -7
View File
@@ -262,7 +262,7 @@ void EditorSession::selectLayerTarget(QUuid id, bool mask)
resolveGradient();
selectLayer(id);
const std::optional<ImageLayer> active = activeLayer();
m_isMaskSelected = mask && active && active->mask;
setIsMaskSelected(mask && active && active->mask);
notify();
}
@@ -286,14 +286,15 @@ void EditorSession::addMask(bool revealing)
// The selection's coverage, soft where it is feathered.
QImage image = PixelAdjust::coverage(current->clip(m_document->size()), width, height,
BrushRaster::pixelToDocument(active->transform, width, height));
if (revealing)
// Revealing, white inside; hiding, black inside.
if (!revealing)
image.invertPixels();
const LayerMask mask(LayerMask::assetFrom(image));
finishOpacityEdit();
beginEdit(QStringLiteral("Add Mask from Selection"));
beginEdit(revealing ? QStringLiteral("Reveal Selection") : QStringLiteral("Hide Selection"));
m_document->layers[size_t(index)].mask = mask;
m_document->selection = std::nullopt;
m_isMaskSelected = true;
setIsMaskSelected(true);
endEdit();
} catch (const std::runtime_error &error) {
qCWarning(lcApp).noquote() << "cannot make a mask from the selection:" << error.what();
@@ -312,7 +313,7 @@ void EditorSession::addLayerMask(bool revealing)
if (layer.id == active->id)
layer.mask = LayerMask::solid(revealing);
}
m_isMaskSelected = true;
setIsMaskSelected(true);
endEdit();
}
@@ -341,7 +342,7 @@ void EditorSession::deleteLayerMask()
if (layer.id == active->id)
layer.mask = std::nullopt;
}
m_isMaskSelected = false;
setIsMaskSelected(false);
endEdit();
}
@@ -371,7 +372,7 @@ void EditorSession::copyMask(QUuid source, QUuid target)
beginEdit(find(target)->mask ? QStringLiteral("Replace Layer Mask") : QStringLiteral("Copy Layer Mask"));
find(target)->mask = mask;
selectLayer(target);
m_isMaskSelected = true;
setIsMaskSelected(true);
endEdit();
}
+3 -1
View File
@@ -420,5 +420,7 @@ void EditorSession::invertSelection()
const std::optional<DocumentSelection> current = selection();
if (!m_document || !current)
return;
setSelection(DocumentSelection{canvasPath(*m_document).subtracted(current->path), current->antialiased, current->feather}, QStringLiteral("Inverse"));
const DocumentSelection inverse{canvasPath(*m_document).subtracted(current->path), current->antialiased, current->feather};
// The inverse of everything is no selection, as Photoshop's.
setSelection(inverse.isEmpty() ? std::nullopt : std::optional(inverse), QStringLiteral("Inverse"));
}
+2 -3
View File
@@ -196,8 +196,7 @@ void EditorSession::beginWarp(QPointF point)
{
const std::optional<ImageLayer> layer = activeLayer();
if (!canPaint() || m_isMaskSelected || !layer || !layer->asset || !m_document) {
if (m_isMaskSelected)
setBrushError(QStringLiteral("Smudge and Liquify work on a layer's pixels, not its mask."));
setBrushError(m_isMaskSelected ? std::optional(QStringLiteral("Smudge and Liquify work on a layer's pixels, not its mask.")) : paintRefusal());
return;
}
finishOpacityEdit();
@@ -235,7 +234,7 @@ void EditorSession::finishWarp()
settings.hardness = 1;
settings.opacity = 1;
const std::unique_ptr<BrushStroke> stroke = makeRasterEdit(current, settings);
stroke->clone = BrushStroke::Clone{warp->image(), QSizeF(0, 0)};
stroke->clone = BrushStroke::Clone{warp->image(), QRectF(QPointF(0, 0), warp->image().size()), false};
stroke->replacesWithClone = true;
stroke->editName = rawValue(warp->mode);
for (const QPointF step : warp->points())
+23
View File
@@ -0,0 +1,23 @@
#include "Rendering/EditorCanvas.h"
// Swift's drawMaskAlone: gray across the canvas, edge tone past it.
void CanvasView::drawMaskAlone(const ImageLayer &layer, const CanvasDocument &document, double scale, const Center &center, QPainter &context)
{
const LayerMask &mask = layer.mask.value();
const int tone = qRound(LayerMask::background(mask.asset.thumbnail) * 255);
context.fillRect(QRectF(center(QPointF(0, 0)), document.size() * scale), QColor(tone, tone, tone));
const std::optional<GradientEdit> &gradient = m_session.gradientEdit();
const BrushStroke *stroke = m_session.brushStroke() ? m_session.brushStroke() : gradient ? gradient->raster.get() : nullptr;
// A stroke or gradient laid into it shows at once.
if (stroke && stroke->isMask && stroke->layer.id == layer.id) {
const LayerTransform &placed = stroke->paintTransform;
const std::shared_ptr<const RasterSnapshot> &raster = mask.asset.raster;
LayerRenderer::drawBrushPreview(raster ? QImage() : mask.asset.image(), placed, center(placed.center()), context, {.scale = scale},
{.patches = stroke->patches(), .pixelWidth = stroke->width, .pixelHeight = stroke->height,
.paintingMask = false, .sourceRect = stroke->sourceRect, .raster = raster});
return;
}
// Where a transform drag shows it, as the composite.
const LayerTransform placed = m_session.displayedMaskPlacement(layer).value_or(m_session.displayedTransform(layer));
LayerRenderer::draw(mask.asset.image(), placed, center(placed.center()), context, {.scale = scale});
}
+9 -1
View File
@@ -85,7 +85,15 @@ bool CanvasView::moveSelectionTool(QPointF point, Qt::KeyboardModifiers modifier
{
if (m_pixelDragStart && m_session.document()) {
const QPointF pixel = m_session.viewport.documentPoint(point, m_session.document()->size());
m_session.movePixels(QSizeF(pixel.x() - m_pixelDragStart->x(), pixel.y() - m_pixelDragStart->y()));
QSizeF offset(pixel.x() - m_pixelDragStart->x(), pixel.y() - m_pixelDragStart->y());
// Shift keeps the pixels on a line, the longer way.
if (modifiers.testFlag(Qt::ShiftModifier)) {
if (std::abs(offset.width()) >= std::abs(offset.height()))
offset.setHeight(0);
else
offset.setWidth(0);
}
m_session.movePixels(offset);
updateCursor();
synchronizeDisplay();
return true;
+6 -1
View File
@@ -85,7 +85,8 @@ CanvasView::DisplayState CanvasView::displayState() const
.layers = {},
.folderMasks = {},
.textStyle = m_session.textDraft() ? std::optional(m_session.textDraft()->style) : std::nullopt,
.textTransform = m_session.textDraft() && m_inlineTextEditor ? std::optional(m_inlineTextEditor->shownTransform()) : std::nullopt};
.textTransform = m_session.textDraft() && m_inlineTextEditor ? std::optional(m_inlineTextEditor->shownTransform()) : std::nullopt,
.maskAlone = m_session.maskAloneLayer() ? std::optional(m_session.maskAloneLayer()->mask->asset.identity()) : std::nullopt};
if (!document)
return state;
// A hidden source still clips: live masks count every layer.
@@ -299,6 +300,10 @@ FolderMaskClip::Applier CanvasView::liveFolderMaskClip(const BrushStroke &edit,
void CanvasView::drawLayers(const CanvasDocument &document, double scale, const Center &center, QPainter &context)
{
if (const std::optional<ImageLayer> alone = m_session.maskAloneLayer()) {
drawMaskAlone(*alone, document, scale, center, context);
return;
}
handOnDraftEffects(document);
m_session.effectsPreviews.prepare(document.layers);
std::map<QUuid, ImageLayer> byID;
+4
View File
@@ -122,6 +122,8 @@ private:
// Typed text, which the canvas draws as pixels.
std::optional<LayerTextStyle> textStyle;
std::optional<LayerTransform> textTransform;
// The mask shown alone, though the composite may skip it.
std::optional<ImageIdentity> maskAlone;
friend bool operator==(const DisplayState &, const DisplayState &) = default;
};
// A Zoom press: drags zoom about it, clicks step.
@@ -146,6 +148,8 @@ private:
void drawStroke(const BrushStroke &stroke, const ImageLayer &layer, const LayerTransform &transform, const std::optional<QImage> &mask,
const LayerRenderer::Options &options, double scale, const Center &center, QPainter &target);
void drawLayers(const CanvasDocument &document, double scale, const Center &center, QPainter &context);
// Alt-click's view: the mask in gray, strokes included.
void drawMaskAlone(const ImageLayer &layer, const CanvasDocument &document, double scale, const Center &center, QPainter &context);
// Typed text as pixels, where the editor shows it.
struct DraftText {
QImage image;
-12
View File
@@ -2,20 +2,10 @@
#include "IO/RecentProjects.h"
#include "Logging.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/NativeLayerList.h"
#include <QApplication>
#include <QMenu>
namespace {
// The layer list holding the keys, as Swift's table.
NativeLayerList *focusedList()
{
for (QWidget *widget = QApplication::focusWidget(); widget; widget = widget->parentWidget())
if (auto *list = qobject_cast<NativeLayerList *>(widget))
return list;
return nullptr;
}
// An entry's name: Hue/Saturation is newHueSaturationAdjustment.
QString adjustmentName(AdjustmentKind kind)
{
@@ -186,8 +176,6 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
m_field->selectAll();
else if (InlineTextEditor *text = typedText())
text->selectAll();
else if (NativeLayerList *list = focusedList())
list->selectAllRows();
else
session().selectAll();
});
+6
View File
@@ -313,6 +313,12 @@ void LayerIcons::paint(QPainter &painter, LayerIcon icon, QPointF origin, double
case LayerIcon::dottedCircle: dottedCircle(painter); break;
case LayerIcon::plusMagnifyingGlass: magnifyingGlass(painter, true); break;
case LayerIcon::minusMagnifyingGlass: magnifyingGlass(painter, false); break;
// Swift's xmark at bold weight.
case LayerIcon::xmark:
painter.setPen(QPen(colour, 2.6, Qt::SolidLine, Qt::RoundCap));
painter.drawLine(QPointF(4, 4), QPointF(14, 14));
painter.drawLine(QPointF(14, 4), QPointF(4, 14));
break;
}
painter.restore();
}
+1 -1
View File
@@ -8,7 +8,7 @@
enum class LayerIcon {
eye, eyeSlash, chevronRight, chevronDown, folder, link, newLayer, newFolder, addMask, trash, layers, text,
halfFilledCircle, sliders, curvePath, plusMinusCircle, paintPalette, circleGrid, sparkles, rightHalfCircle, hatchedCircle, axes,
scope, lineDiagonal, drop, wind, dottedCircle, plusMagnifyingGlass, minusMagnifyingGlass
scope, lineDiagonal, drop, wind, dottedCircle, plusMagnifyingGlass, minusMagnifyingGlass, xmark
};
namespace LayerIcons {
+58 -2
View File
@@ -1,6 +1,9 @@
#include "UI/LayerMaskMenu.h"
#include "UI/LayerIcons.h"
#include <QEvent>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QPainter>
LayerMaskMenu::LayerMaskMenu(EditorSession &session, QWidget *parent) : QToolButton(parent), m_session(session)
{
@@ -8,7 +11,8 @@ LayerMaskMenu::LayerMaskMenu(EditorSession &session, QWidget *parent) : QToolBut
setAccessibleName(QStringLiteral("Add layer mask"));
setAutoRaise(true);
setFixedSize(32, 40);
connect(this, &QToolButton::clicked, this, [this] { m_session.addMask(); });
// Swift reads Option from the event that clicked.
connect(this, &QToolButton::clicked, this, [this] { m_session.addMask(!QGuiApplication::keyboardModifiers().testFlag(Qt::AltModifier)); });
connect(&m_session, &EditorSession::changed, this, &LayerMaskMenu::synchronize);
synchronize();
}
@@ -17,7 +21,8 @@ void LayerMaskMenu::synchronize()
{
const std::optional<ImageLayer> active = m_session.activeLayer();
setEnabled(m_session.canEditMask() && !(active && active->mask));
setToolTip(m_session.selection() ? QStringLiteral("Add layer mask (the selection becomes black)") : QStringLiteral("Add layer mask"));
setToolTip(m_session.selection() ? QStringLiteral("Add layer mask revealing the selection (Alt-click to hide it)")
: QStringLiteral("Add layer mask (Alt-click for a black mask)"));
setIcon(LayerIcons::pixmap(LayerIcon::addMask, 16, palette().color(QPalette::PlaceholderText), devicePixelRatio()));
}
@@ -27,3 +32,54 @@ void LayerMaskMenu::changeEvent(QEvent *event)
if (event->type() == QEvent::PaletteChange)
synchronize();
}
MaskAloneBadge::MaskAloneBadge(EditorSession &session, QWidget *parent) : QWidget(parent), m_name(new QLabel(this))
{
setObjectName(QStringLiteral("maskAloneBadge"));
setFixedHeight(26);
const auto font = [](QLabel *label, bool semibold, double alpha) {
QFont face = label->font();
face.setPixelSize(12);
face.setWeight(semibold ? QFont::DemiBold : QFont::Normal);
label->setFont(face);
QPalette colours = label->palette();
colours.setColor(QPalette::WindowText, QColor::fromRgbF(1, 1, 1, alpha));
label->setPalette(colours);
};
auto *icon = new QLabel(this);
icon->setPixmap(LayerIcons::pixmap(LayerIcon::addMask, 11, Qt::white, devicePixelRatioF()));
auto *title = new QLabel(QStringLiteral("Layer Mask"), this);
title->setObjectName(QStringLiteral("maskAloneTitle"));
font(title, true, 1);
m_name->setObjectName(QStringLiteral("maskAloneName"));
m_name->setTextFormat(Qt::PlainText);
m_name->setMaximumWidth(220);
font(m_name, false, 0.6);
auto *close = new QToolButton(this);
close->setObjectName(QStringLiteral("maskAloneClose"));
close->setAutoRaise(true);
close->setIcon(LayerIcons::pixmap(LayerIcon::xmark, 9, Qt::white, devicePixelRatioF()));
close->setToolTip(QStringLiteral("Show the image again (or Alt-click the mask thumbnail)"));
close->setAccessibleName(QStringLiteral("Stop viewing the mask"));
connect(close, &QToolButton::clicked, this, [&session] { session.setViewsMaskAlone(false); });
auto *row = new QHBoxLayout(this);
row->setContentsMargins(11, 0, 8, 0);
row->setSpacing(7);
for (QWidget *widget : std::initializer_list<QWidget *>{icon, title, m_name, close})
row->addWidget(widget);
}
// Swift's byTruncatingTail within 220 points.
void MaskAloneBadge::setLayerName(const QString &name)
{
m_name->setText(m_name->fontMetrics().elidedText(name, Qt::ElideRight, 220));
}
void MaskAloneBadge::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(QPen(QColor::fromRgbF(1, 1, 1, 0.14), 1));
painter.setBrush(QColor::fromRgbF(0, 0, 0, 0.75));
painter.drawRoundedRect(QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5), 13, 13);
}
+16 -1
View File
@@ -1,8 +1,9 @@
#pragma once
#include "Document/EditorSession.h"
#include <QLabel>
#include <QToolButton>
// One click adds a mask: white, or the selection black.
// One click adds a mask revealing the selection; Alt, hiding.
class LayerMaskMenu : public QToolButton {
Q_OBJECT
public:
@@ -16,3 +17,17 @@ private:
EditorSession &m_session;
};
// While a mask shows alone: whose, and a way back.
class MaskAloneBadge : public QWidget {
Q_OBJECT
public:
explicit MaskAloneBadge(EditorSession &session, QWidget *parent = nullptr);
void setLayerName(const QString &name);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QLabel *const m_name;
};
+6 -8
View File
@@ -70,9 +70,10 @@ LayerThumbnailButton::LayerThumbnailButton(NativeLayerList &list, bool maskTarge
setAutoRaise(true);
}
void LayerThumbnailButton::setTargeted(bool targeted)
void LayerThumbnailButton::setTargeted(bool targeted, bool alone)
{
m_targeted = targeted;
m_alone = alone;
QWidget::update();
}
@@ -155,12 +156,9 @@ void LayerThumbnailButton::mouseReleaseEvent(QMouseEvent *event)
if (!m_altMaskPress || event->button() != Qt::LeftButton)
return;
endAltMaskPress();
// Released where pressed: a click, not a drag; Shift toggles.
if (!rect().contains(event->position().toPoint()))
return;
m_list.session().selectLayerTarget(layerID, true);
if (event->modifiers().testFlag(Qt::ShiftModifier))
m_list.session().toggleLayerMask();
// Released where pressed: the mask alone, or the image again.
if (rect().contains(event->position().toPoint()))
m_list.session().toggleMaskAlone(layerID);
}
// With a modifier the second press is the button's again.
@@ -179,7 +177,7 @@ void LayerThumbnailButton::paintEvent(QPaintEvent *)
if (!m_targeted)
return;
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(QPen(palette().color(QPalette::Highlight), 2));
painter.setPen(QPen(m_alone ? QColor(Qt::white) : palette().color(QPalette::Highlight), 2));
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(QRectF(rect()).adjusted(1, 1, -1, -1), 3, 3);
}
+19 -3
View File
@@ -4,6 +4,7 @@
#include <QApplication>
#include <QContextMenuEvent>
#include <QKeyEvent>
#include <QLocale>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
@@ -30,7 +31,7 @@ LayerCell::LayerCell(NativeLayerList &list)
connect(m_disclosure, &QToolButton::clicked, this, [this] { m_list.session().toggleGroupExpansion(m_layerID); });
m_thumbnail->setObjectName(QStringLiteral("layerThumbnail"));
m_maskThumbnail->setObjectName(QStringLiteral("maskThumbnail"));
m_maskThumbnail->setToolTip(QStringLiteral("Select layer mask; Shift-click to enable/disable; Ctrl-click to select its black areas (Ctrl-Shift adds, Ctrl-Alt subtracts)"));
m_maskThumbnail->setToolTip(QStringLiteral("Select layer mask; Alt-click to view it alone; Shift-click to enable/disable; Ctrl-click to select its black areas (Ctrl-Shift adds, Ctrl-Alt subtracts)"));
m_link->setObjectName(QStringLiteral("maskLink"));
m_link->setAutoRaise(true);
m_link->setFixedSize(9, 20);
@@ -115,7 +116,7 @@ void LayerCell::configure(const ImageLayer &layer, bool enabled, int depth, bool
m_dimensions->setText(m_editableText ? QStringLiteral("Text · Double-click to edit")
: m_isAdjustment ? QStringLiteral("Adjustment · Double-click to edit")
: layer.isGroup ? QStringLiteral("Folder")
: QStringLiteral("%1 × %2 px").arg(std::lround(layer.size().width())).arg(std::lround(layer.size().height())));
: sizeLabel(layer));
m_dimensions->setToolTip(QString());
if (layer.maskSourceID && session.document()) {
const int source = indexOf(session.document()->layers, layer.maskSourceID);
@@ -158,7 +159,7 @@ void LayerCell::updateTarget()
const bool active = session.activeLayerID() == m_layerID && session.selectedLayerIDs().size() == 1;
const bool mask = session.isMaskSelected();
m_thumbnail->setTargeted(active && !mask);
m_maskThumbnail->setTargeted(active && mask);
m_maskThumbnail->setTargeted(active && mask, active && session.maskAloneLayer());
QWidget::update();
}
@@ -335,3 +336,18 @@ bool LayerCell::eventFilter(QObject *watched, QEvent *event)
return false;
return true;
}
QString sizeLabel(const ImageLayer &layer)
{
const QString text = QStringLiteral("%1 × %2 px").arg(std::lround(layer.size().width())).arg(std::lround(layer.size().height()));
const int pixels = layer.asset ? layer.asset->size().width() : 0;
if (pixels <= 0)
return text;
// Across the width, as the Move bar's Scale field.
const double percent = layer.size().width() / pixels * 100;
if (std::abs(percent - 100) < 0.05)
return text;
// Swift's .number to one place at most, ties to even.
const double rounded = std::nearbyint(percent * 10) / 10;
return text + QStringLiteral(" · ") + QLocale().toString(rounded, 'f', rounded == std::trunc(rounded) ? 0 : 1) + QStringLiteral("%");
}
-10
View File
@@ -261,13 +261,3 @@ void NativeLayerList::changeEvent(QEvent *event)
if (event->type() == QEvent::PaletteChange)
QMetaObject::invokeMethod(this, &NativeLayerList::update, Qt::QueuedConnection);
}
void NativeLayerList::selectAllRows()
{
if (m_rows.empty())
return;
QSet<QUuid> ids;
for (const ImageLayer &row : m_rows)
ids.insert(row.id);
m_session.selectLayers(ids, m_rows.front().id);
}
+6 -4
View File
@@ -43,8 +43,8 @@ public:
LayerThumbnailButton(NativeLayerList &list, bool maskTarget, QWidget *parent);
QUuid layerID;
const bool isMaskTarget;
// An accent ring while the target is selected.
void setTargeted(bool targeted);
// An accent ring while targeted; white while shown alone.
void setTargeted(bool targeted, bool alone = false);
bool isTargeted() const { return m_targeted; }
~LayerThumbnailButton() override;
@@ -63,6 +63,7 @@ private:
NativeLayerList &m_list;
bool m_targeted = false;
bool m_alone = false;
// Alt on a mask: chosen on release, or dragged.
std::optional<QPoint> m_altMaskPress;
};
@@ -215,8 +216,6 @@ public:
QString dragToken() const { return m_dragToken; }
EditorSession &session() const { return m_session; }
// The table's Select All: every row, the top one primary.
void selectAllRows();
void update();
std::vector<LayerCell *> cells() const { return m_cells; }
// The row under a point of this list, or -1.
@@ -283,3 +282,6 @@ private:
const QMimeData *m_dragData = nullptr;
Qt::DropActions m_dragActions;
};
// The size on the canvas and, once scaled, how much.
QString sizeLabel(const ImageLayer &layer);
+32 -2
View File
@@ -101,7 +101,8 @@ TransformInspector::TransformInspector(EditorSession &session, QWidget *parent)
m_rotation(new TransformValueField(QStringLiteral("°"), QString(), -360, 360, session, [this](double number) {
change([number](LayerTransform &value) { value.rotation = std::fmod(number, 360.0); });
})),
m_sampling(new QComboBox(this)), m_cancel(new QPushButton(QStringLiteral("Cancel"), this)), m_apply(new QPushButton(QStringLiteral("Apply"), this))
m_sampling(new QComboBox(this)), m_cancel(new QPushButton(QStringLiteral("Cancel"), this)), m_apply(new QPushButton(QStringLiteral("Apply"), this)),
m_pendingButtons(new QWidget(this)), m_fade(new QGraphicsOpacityEffect(m_pendingButtons)), m_fadeIn(new QPropertyAnimation(m_fade, "opacity", this))
{
m_autoSelect->setObjectName(QStringLiteral("transformAutoSelect"));
m_autoSelect->setToolTip(QStringLiteral("Select layers by clicking the canvas. When off, hold Ctrl to select a layer."));
@@ -162,8 +163,25 @@ TransformInspector::TransformInspector(EditorSession &session, QWidget *parent)
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_apply, m_cancel);
connect(m_apply, &QPushButton::clicked, this, [this] { m_session.commitTransform(); });
m_pendingButtons->setObjectName(QStringLiteral("pendingTransform"));
auto *pending = new QHBoxLayout(m_pendingButtons);
pending->setContentsMargins(0, 0, 0, 0);
pending->setSpacing(12);
pending->addWidget(m_cancel);
pending->addWidget(m_apply);
// Unseen, not hidden: Return and Escape still reach a drag.
m_pendingButtons->setGraphicsEffect(m_fade);
m_fade->setOpacity(0);
m_pendingButtons->setAttribute(Qt::WA_TransparentForMouseEvents);
m_cancel->setFocusPolicy(Qt::NoFocus);
m_apply->setFocusPolicy(Qt::NoFocus);
// SwiftUI's easeOut over 0.12 seconds.
QEasingCurve easeOut(QEasingCurve::BezierSpline);
easeOut.addCubicBezierSegment(QPointF(0, 0), QPointF(0.58, 1), QPointF(1, 1));
m_fadeIn->setEasingCurve(easeOut);
m_fadeIn->setDuration(120);
// Before the stretch ending the row, in Swift's order.
for (QWidget *widget : std::initializer_list<QWidget *>{m_autoSelect, m_showControls, m_fields, m_cancel, m_apply})
for (QWidget *widget : std::initializer_list<QWidget *>{m_autoSelect, m_showControls, m_fields, m_pendingButtons})
row->insertWidget(row->count() - 1, widget, widget == m_fields ? 1 : 0);
// The scroll view takes the spare room, as SwiftUI's does.
row->setStretch(row->count() - 1, 0);
@@ -278,4 +296,16 @@ void TransformInspector::synchronize()
m_fields->widget()->setEnabled((m_session.canTransform() || editing) && !distorted);
m_cancel->setEnabled(editing);
m_apply->setEnabled(editing);
// Typed values, Ctrl+T, a distortion wait; a handle drag applies.
const bool pending = editing && m_session.transformEdit()->persistent;
if (pending == m_pending)
return;
m_pending = pending;
m_pendingButtons->setAttribute(Qt::WA_TransparentForMouseEvents, !pending);
m_cancel->setFocusPolicy(pending ? Qt::StrongFocus : Qt::NoFocus);
m_apply->setFocusPolicy(pending ? Qt::StrongFocus : Qt::NoFocus);
m_fadeIn->stop();
m_fadeIn->setStartValue(m_fade->opacity());
m_fadeIn->setEndValue(pending ? 1.0 : 0.0);
m_fadeIn->start();
}
+7
View File
@@ -3,7 +3,9 @@
#include "UI/ToolHeaderStyle.h"
#include <QCheckBox>
#include <QComboBox>
#include <QGraphicsOpacityEffect>
#include <QLineEdit>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QScrollArea>
#include <QToolButton>
@@ -76,4 +78,9 @@ private:
QComboBox *const m_sampling;
QPushButton *const m_cancel;
QPushButton *const m_apply;
// Cancel and Apply, seen only while an edit waits.
QWidget *const m_pendingButtons;
QGraphicsOpacityEffect *const m_fade;
QPropertyAnimation *const m_fadeIn;
bool m_pending = false;
};