Masks as Photoshop's: the button reveals the selection, Alt-click shows a mask alone (13.12)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 18:04:14 -04:00
1 parent cc9bf95722
commit 0b0d36a7df
28 files changed
+554 -72

No files matched your search

+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
@@ -351,7 +351,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;
+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();
}