Say why a brush cannot paint; paint at the layer's own resolution; Cancel and Apply only when an edit waits, scaled layers' scale (13.11)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 17:48:38 -04:00
1 parent 51e373af4c
commit cc9bf95722
24 files changed
+820 -129

No files matched your search

+17 -1
View File
@@ -4,6 +4,7 @@
#include <QApplication>
#include <QContextMenuEvent>
#include <QKeyEvent>
#include <QLocale>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
@@ -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);
@@ -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("%");
}
+3
View File
@@ -279,3 +279,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;
};