12.2a: folders duplicate with their contents, folder opacity, format version 8, View > Snap (Compositor 1.1.5–1.1.6)

This commit is contained in:
雷电芽衣 committed 2026-09-26 18:17:14 -04:00
1 parent ee34c338d4
commit f1b2c65c1e
52 files changed
+909 -239

No files matched your search

+3
View File
@@ -118,6 +118,8 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
add(view, QStringLiteral("zoomOut"), QStringLiteral("Zoom Out"), QKeySequence(Qt::CTRL | Qt::Key_Minus), [this] { session().zoom(session().viewport.zoom() / 1.25); });
add(view, QStringLiteral("pixelGrid"), QStringLiteral("Pixel Grid (800% and above)"), QKeySequence(), [this] { session().setShowsPixelGrid(!session().showsPixelGrid()); })
->setCheckable(true);
add(view, QStringLiteral("snap"), QStringLiteral("Snap"), QKeySequence(), [this] { session().setSnappingEnabled(!session().snappingEnabled()); })
->setCheckable(true);
// Ctrl+H: the Move tool's box; the desktop hides apps.
add(view, QStringLiteral("transformControls"), QStringLiteral("Show Transform Controls"), QKeySequence(Qt::CTRL | Qt::Key_H),
[this] { session().setShowsTransformControls(!session().showsTransformControls()); })
@@ -275,6 +277,7 @@ void CompositorMenus::synchronize()
for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"})
action(QString::fromLatin1(name))->setEnabled(drawn);
action(QStringLiteral("pixelGrid"))->setChecked(s.showsPixelGrid());
action(QStringLiteral("snap"))->setChecked(s.snappingEnabled());
action(QStringLiteral("transformControls"))->setChecked(s.showsTransformControls());
action(QStringLiteral("transformControls"))->setEnabled(s.tool() == NavigationTool::move && s.document().has_value());
const bool selected = s.selection().has_value();
+11 -10
View File
@@ -53,7 +53,7 @@ EffectsSheet::EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget
header(QStringLiteral("Stroke"), true);
colour(true);
slider(QStringLiteral("Size"), [](const LayerEffects &e) { return e.stroke ? std::optional(e.stroke->size) : std::nullopt; },
[](LayerEffects &e, double value) { e.stroke->size = value; }, 1, StrokeEffect::maxSize, QStringLiteral("px"));
[](LayerEffects &e, double value) { e.stroke->size = value; }, 0, 20, QStringLiteral("px"), StrokeEffect::maxSize);
slider(QStringLiteral("Opacity"), [](const LayerEffects &e) { return percent(e.stroke); },
[](LayerEffects &e, double value) { e.stroke->opacity = value / 100; }, 0, 100, QStringLiteral("%"));
break;
@@ -64,9 +64,9 @@ EffectsSheet::EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget
slider(QStringLiteral("Angle"), [](const LayerEffects &e) { return e.shadow ? std::optional(e.shadow->angle) : std::nullopt; },
[](LayerEffects &e, double value) { e.shadow->angle = value; }, -180, 180, QStringLiteral("°"));
slider(QStringLiteral("Distance"), [](const LayerEffects &e) { return e.shadow ? std::optional(e.shadow->distance) : std::nullopt; },
[](LayerEffects &e, double value) { e.shadow->distance = value; }, 0, 300, QStringLiteral("px"));
[](LayerEffects &e, double value) { e.shadow->distance = value; }, 0, 100, QStringLiteral("px"), 5000);
slider(QStringLiteral("Blur"), [](const LayerEffects &e) { return e.shadow ? std::optional(e.shadow->blur) : std::nullopt; },
[](LayerEffects &e, double value) { e.shadow->blur = value; }, 0, 300, QStringLiteral("px"));
[](LayerEffects &e, double value) { e.shadow->blur = value; }, 0, 100, QStringLiteral("px"), 500);
break;
case LayerEffectKind::colorOverlay:
header(QStringLiteral("Color Overlay"), false);
@@ -80,9 +80,9 @@ EffectsSheet::EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget
slider(QStringLiteral("Angle"), [](const LayerEffects &e) { return e.innerShadow ? std::optional(e.innerShadow->angle) : std::nullopt; },
[](LayerEffects &e, double value) { e.innerShadow->angle = value; }, -180, 180, QStringLiteral("°"));
slider(QStringLiteral("Distance"), [](const LayerEffects &e) { return e.innerShadow ? std::optional(e.innerShadow->distance) : std::nullopt; },
[](LayerEffects &e, double value) { e.innerShadow->distance = value; }, 0, 300, QStringLiteral("px"));
[](LayerEffects &e, double value) { e.innerShadow->distance = value; }, 0, 50, QStringLiteral("px"), 5000);
slider(QStringLiteral("Blur"), [](const LayerEffects &e) { return e.innerShadow ? std::optional(e.innerShadow->blur) : std::nullopt; },
[](LayerEffects &e, double value) { e.innerShadow->blur = value; }, 0, 300, QStringLiteral("px"));
[](LayerEffects &e, double value) { e.innerShadow->blur = value; }, 0, 100, QStringLiteral("px"), 500);
break;
}
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
@@ -139,7 +139,7 @@ void EffectsSheet::header(const QString &title, bool position)
// The effect's colour, opened in the app's own picker.
void EffectsSheet::colour(bool labelled)
{
auto *swatch = new SwatchButton([this] { return m_session.editingEffects().color(m_kind).value_or(PaletteColor::black()); }, 3, 0, 0.5, this);
auto *swatch = new SwatchButton([this] { return m_session.editingEffects().color(m_kind).value_or(PaletteColor::black()); }, 3, 1, 1, this);
swatch->setObjectName(QStringLiteral("effectColor"));
swatch->setFixedSize(36, 18);
swatch->setToolTip(rawValue(m_kind) + QStringLiteral(" color"));
@@ -162,7 +162,8 @@ void EffectsSheet::colour(bool labelled)
}
void EffectsSheet::slider(const QString &title, std::function<std::optional<double>(const LayerEffects &)> value,
std::function<void(LayerEffects &, double)> change, double low, double high, const QString &unit)
std::function<void(LayerEffects &, double)> change, double low, double high, const QString &unit,
std::optional<double> typedHigh)
{
const size_t index = m_sliders.size();
const QString name = title.toLower();
@@ -215,12 +216,12 @@ void EffectsSheet::slider(const QString &title, std::function<std::optional<doub
layout->addLayout(suffixed);
m_column->addWidget(row);
m_controls.push_back(row);
m_sliders.push_back(Slider{std::move(value), std::move(change), low, high, slider, field});
m_sliders.push_back(Slider{std::move(value), std::move(change), low, high, typedHigh.value_or(high), slider, field});
}
void EffectsSheet::apply(const Slider &control, double value)
{
const double clamped = std::clamp(value, control.low, control.high);
const double clamped = std::clamp(value, control.low, control.typedHigh);
m_session.changeEffects([&control, clamped](LayerEffects &effects) { control.change(effects, clamped); });
}
@@ -244,7 +245,7 @@ void EffectsSheet::synchronize()
if (!value)
continue;
{
// The slider shows the session's number without writing back.
// QSlider pins a larger typed value at its end.
const QSignalBlocker quiet(control.slider);
control.slider->setValue(int(std::lround((*value - control.low) / (control.high - control.low) * travel)));
}
+5 -2
View File
@@ -22,14 +22,17 @@ private:
std::function<void(LayerEffects &, double)> change;
double low;
double high;
// What a field or step may reach, past the slider.
double typedHigh;
QSlider *slider;
PickerField *field;
};
void header(const QString &title, bool position);
void colour(bool labelled);
void slider(const QString &title, std::function<std::optional<double>(const LayerEffects &)> value,
std::function<void(LayerEffects &, double)> change, double low, double high, const QString &unit);
// A value clamped to the slider's range, as Swift's binding.
std::function<void(LayerEffects &, double)> change, double low, double high, const QString &unit,
std::optional<double> typedHigh = std::nullopt);
// A value clamped to the typed range, as Swift's setAmount.
void apply(const Slider &control, double value);
void synchronize();
+5 -2
View File
@@ -28,7 +28,8 @@ LayerAppearanceControls::LayerAppearanceControls(EditorSession &session, QWidget
column->setContentsMargins(12, 12, 12, 12);
column->setSpacing(8);
auto *blend = new QHBoxLayout;
blend->addWidget(caption(QStringLiteral("Blend"), this));
m_blendCaption = caption(QStringLiteral("Blend"), this);
blend->addWidget(m_blendCaption);
blend->addWidget(m_picker, 1);
column->addLayout(blend);
auto *opacity = new QHBoxLayout;
@@ -74,7 +75,9 @@ LayerAppearanceControls::~LayerAppearanceControls()
void LayerAppearanceControls::synchronize()
{
setEnabled(m_session.canEditAppearance());
// A folder takes an opacity; blending rests with the picker.
setEnabled(m_session.canEditOpacity());
m_blendCaption->setEnabled(m_session.canEditAppearance());
const std::optional<ImageLayer> active = m_session.activeLayer();
const double opacity = active ? active->opacity : 1;
m_syncing = true;
+2
View File
@@ -5,6 +5,7 @@
#include <QWidget>
class BlendModePicker;
class QLabel;
// The active layer's blend mode and opacity.
class LayerAppearanceControls : public QWidget {
@@ -25,6 +26,7 @@ private:
EditorSession &m_session;
BlendModePicker *const m_picker;
QLabel *m_blendCaption = nullptr;
QSlider *const m_slider;
QLineEdit *const m_percentage;
// The layer the controls were last made for.
+1 -1
View File
@@ -157,7 +157,7 @@ void LayerCell::configure(const ImageLayer &layer, bool enabled, int depth, bool
// A pending rename closes the edits: rows never move.
if (!m_renaming)
m_name->setText((layer.maskSourceID ? QStringLiteral("↳ ") : QString()) + layer.name);
m_dimensions->setText(m_editableText ? QStringLiteral("Text · Double-click to edit")
m_dimensions->setText(m_editableText ? QStringLiteral("Text")
: 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())));
+1 -6
View File
@@ -127,11 +127,6 @@ std::optional<LayerDropTarget> NativeLayerList::dropTarget(const QMimeData &data
return std::nullopt;
// Swift duplicates when the source offers a copy alone (Alt).
const bool copying = offered == Qt::CopyAction;
// Folders are not duplicated: a copy with one is refused.
for (const QUuid &id : ids) {
if (copying && m_rows[size_t(indexOf(m_rows, id))].isGroup)
return std::nullopt;
}
const QPoint inColumn = m_column->mapFrom(viewport(), viewport()->mapFrom(this, listPoint));
int row = int(m_rows.size());
bool onRow = false;
@@ -326,7 +321,7 @@ QCursor NativeLayerList::cursorFor(QPoint listPoint, Qt::KeyboardModifiers modif
if (cell.maskThumbnail().isVisible() && cell.maskThumbnail().geometry().contains(inCell))
return editable ? CanvasView::duplicateCursor(ratio) : QCursor(Qt::ArrowCursor);
if (!isClippingZone(cell, inCell))
return editable && !layer.isGroup ? CanvasView::duplicateCursor(ratio) : QCursor(Qt::ArrowCursor);
return editable ? CanvasView::duplicateCursor(ratio) : QCursor(Qt::ArrowCursor);
if (!m_session.canToggleClippingMask(layer.id))
return QCursor(Qt::ArrowCursor);
return clippingCursor(layer.maskSourceID.has_value(), ratio);
+3 -2
View File
@@ -91,7 +91,8 @@ int NativeLayerList::rowAt(QPoint listPoint) const
bool NativeLayerList::isClippingZone(const LayerCell &cell, QPoint cellPoint)
{
return cellPoint.y() >= cell.height() - 10;
// A folder clips nothing, so it has no strip.
return !cell.isGroup() && cellPoint.y() >= cell.height() - 10;
}
bool NativeLayerList::clickRow(LayerCell &cell, Qt::KeyboardModifiers modifiers, QPoint cellPoint)
@@ -177,7 +178,7 @@ void NativeLayerList::keyPressEvent(QKeyEvent *event)
{Qt::Key_Z, NavigationTool::zoom}, {Qt::Key_G, NavigationTool::gradient}, {Qt::Key_L, NavigationTool::lasso}, {Qt::Key_M, NavigationTool::marquee},
{Qt::Key_W, NavigationTool::wand}, {Qt::Key_J, NavigationTool::spotHealing}, {Qt::Key_S, NavigationTool::cloneStamp},
{Qt::Key_U, NavigationTool::shape}, {Qt::Key_R, NavigationTool::blur}, {Qt::Key_I, NavigationTool::eyedropper},
{Qt::Key_C, NavigationTool::crop}};
{Qt::Key_C, NavigationTool::crop}, {Qt::Key_T, NavigationTool::type}};
if (plain && (key == Qt::Key_Backspace || key == Qt::Key_Delete)) {
m_session.deleteKeyPressed();
return;
+1
View File
@@ -124,6 +124,7 @@ public:
void beginRenaming();
bool isOnControl(QPoint point) const;
bool isRenaming() const { return m_renaming; }
bool isGroup() const { return m_isGroup; }
QUuid layerID() const { return m_layerID; }
LayerThumbnailButton &thumbnail() { return *m_thumbnail; }
LayerThumbnailButton &maskThumbnail() { return *m_maskThumbnail; }