Masks as Photoshop's: the button reveals the selection, Alt-click shows a mask alone (13.12)
This commit is contained in:
1 parent
cc9bf95722
commit
0b0d36a7df
28 files changed
+554
-72
No files matched your search
+13
-1
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ¢er, 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});
|
||||
}
|
||||
@@ -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 ¢er, 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;
|
||||
|
||||
@@ -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 ¢er, QPainter &target);
|
||||
void drawLayers(const CanvasDocument &document, double scale, const Center ¢er, QPainter &context);
|
||||
// Alt-click's view: the mask in gray, strokes included.
|
||||
void drawMaskAlone(const ImageLayer &layer, const CanvasDocument &document, double scale, const Center ¢er, QPainter &context);
|
||||
// Typed text as pixels, where the editor shows it.
|
||||
struct DraftText {
|
||||
QImage image;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -31,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);
|
||||
@@ -159,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user