12.1: feathered selections, Select Subject, Soft Light, the fixed-width font menu (Compositor 1.1.1–1.1.4)

This commit is contained in:
雷电芽衣 committed 2026-09-26 17:15:26 -04:00
1 parent 3e52fd90ee
commit ee34c338d4
42 files changed
+1087 -94

No files matched your search

+11 -6
View File
@@ -139,13 +139,19 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
if (const std::optional<QUuid> id = session().activeLayerID())
session().loadLayerSelection(*id);
});
add(select, QStringLiteral("subject"), QStringLiteral("Subject"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_A),
[this] { session().selectSubject(SelectionMode::replace, {}); });
add(select, QStringLiteral("maskBlackAreas"), QStringLiteral("Mask's Black Areas"), QKeySequence(), [this] {
if (const std::optional<QUuid> id = session().activeLayerID())
session().loadMaskSelection(*id);
});
select->addSeparator();
add(select, QStringLiteral("expandSelection"), QStringLiteral("Expand by 1 px"), QKeySequence(), [this] { session().expandSelection(session().selectionExpandAmount()); });
add(select, QStringLiteral("contractSelection"), QStringLiteral("Contract by 1 px"), QKeySequence(), [this] { session().contractSelection(session().selectionContractAmount()); });
add(select, QStringLiteral("expandSelection"), QStringLiteral("Expand…"), QKeySequence(),
[this] { session().promptSelectionAmount(SelectionAmountOperation::expand); });
add(select, QStringLiteral("contractSelection"), QStringLiteral("Contract…"), QKeySequence(),
[this] { session().promptSelectionAmount(SelectionAmountOperation::contract); });
add(select, QStringLiteral("featherSelection"), QStringLiteral("Feather…"), QKeySequence(),
[this] { session().promptSelectionAmount(SelectionAmountOperation::feather); });
QMenu *image = bar.addMenu(QStringLiteral("&Image"));
add(image, QStringLiteral("curves"), QStringLiteral("Curves…"), QKeySequence(Qt::CTRL | Qt::Key_M), [this] { session().beginFilter(FilterKind::curves); });
@@ -277,10 +283,9 @@ void CompositorMenus::synchronize()
action(QStringLiteral("inverse"))->setEnabled(selected && s.canEditSelection());
action(QStringLiteral("layerPixels"))->setEnabled(active && active->asset && s.canEditSelection());
action(QStringLiteral("maskBlackAreas"))->setEnabled(active && active->mask && s.canEditSelection());
action(QStringLiteral("expandSelection"))->setText(QStringLiteral("Expand by %1 px").arg(s.selectionExpandAmount()));
action(QStringLiteral("expandSelection"))->setEnabled(s.canModifySelection());
action(QStringLiteral("contractSelection"))->setText(QStringLiteral("Contract by %1 px").arg(s.selectionContractAmount()));
action(QStringLiteral("contractSelection"))->setEnabled(s.canModifySelection());
action(QStringLiteral("subject"))->setEnabled(s.canSelectSubject());
for (const char *name : {"expandSelection", "contractSelection", "featherSelection"})
action(QString::fromLatin1(name))->setEnabled(s.canModifySelection());
action(QStringLiteral("levels"))->setEnabled(s.canAdjustColors() && !s.hueSaturation());
for (const FilterKind kind : allFilterKinds) {
if (kind != FilterKind::contentAwareFill)
+105 -1
View File
@@ -1,5 +1,8 @@
#include "UI/LassoControls.h"
#include <QBoxLayout>
#include <QFrame>
#include <QLabel>
#include <QSignalBlocker>
#include <QRegularExpressionValidator>
#include <QFocusEvent>
#include <QKeyEvent>
@@ -98,6 +101,9 @@ LassoControls::LassoControls(EditorSession &session, QWidget *parent)
m_contract(new QPushButton(QStringLiteral("Contract"), this)),
m_contractAmount(new SelectionAmountField(session, 1, 500, [this] { return double(m_session.selectionContractAmount()); },
[this](double amount) { m_session.setSelectionContractAmount(int(std::lround(amount))); }, this)),
m_feather(new QPushButton(QStringLiteral("Feather"), this)),
m_featherAmount(new SelectionAmountField(session, 1, 250, [this] { return double(m_session.selectionFeatherAmount()); },
[this](double amount) { m_session.setSelectionFeatherAmount(int(std::lround(amount))); }, this)),
m_empty(new QLabel(QStringLiteral("Empty selection"), this)), m_deselect(new QPushButton(QStringLiteral("Deselect"), this))
{
m_toleranceLabel->setFont(ToolHeaderStyle::controlFont());
@@ -132,6 +138,11 @@ LassoControls::LassoControls(EditorSession &session, QWidget *parent)
connect(m_contract, &QPushButton::clicked, this, [this] { m_session.contractSelection(m_session.selectionContractAmount()); });
m_contractAmount->setObjectName(QStringLiteral("selectionContractAmount"));
m_contractAmount->setToolTip(m_contract->toolTip());
m_feather->setObjectName(QStringLiteral("featherSelection"));
m_feather->setToolTip(QStringLiteral("Fade the edge of the selection by this many pixels"));
connect(m_feather, &QPushButton::clicked, this, [this] { m_session.featherSelection(m_session.selectionFeatherAmount()); });
m_featherAmount->setObjectName(QStringLiteral("selectionFeatherAmount"));
m_featherAmount->setToolTip(m_feather->toolTip());
m_empty->setObjectName(QStringLiteral("emptySelection"));
m_empty->setFont(ToolHeaderStyle::controlFont());
m_empty->setForegroundRole(QPalette::PlaceholderText);
@@ -141,6 +152,7 @@ LassoControls::LassoControls(EditorSession &session, QWidget *parent)
for (QWidget *widget : std::initializer_list<QWidget *>{m_rectangle, m_ellipse, m_freehand, m_polygonal, m_replace, m_add, m_subtract, m_toleranceLabel,
m_tolerance, m_sampleSize, m_thisLayer, m_allLayers, m_contiguous, m_antialias,
divider, m_expand, m_expandAmount, new QLabel(QStringLiteral("px"), this), m_contract, m_contractAmount,
new QLabel(QStringLiteral("px"), this), m_feather, m_featherAmount,
new QLabel(QStringLiteral("px"), this)})
row->insertWidget(row->count() - 1, widget);
row->addWidget(m_empty);
@@ -202,13 +214,105 @@ void LassoControls::synchronize()
if (m_antialias->isChecked() != m_session.selectionAntialiased())
m_antialias->setChecked(m_session.selectionAntialiased());
const bool modifies = m_session.canModifySelection();
for (QWidget *widget : std::initializer_list<QWidget *>{m_expand, m_expandAmount, m_contract, m_contractAmount})
for (QWidget *widget : std::initializer_list<QWidget *>{m_expand, m_expandAmount, m_contract, m_contractAmount, m_feather, m_featherAmount})
widget->setEnabled(modifies);
m_expandAmount->sync(m_session.selectionExpandAmount());
m_contractAmount->sync(m_session.selectionContractAmount());
m_featherAmount->sync(m_session.selectionFeatherAmount());
const std::optional<DocumentSelection> selection = m_session.selection();
m_empty->setVisible(selection && selection->isEmpty());
m_deselect->setVisible(selection.has_value());
m_deselect->setEnabled(m_session.canEditSelection());
setEnabled(!m_session.showsBusy() && m_session.document().has_value());
}
SelectionAmountSheet::SelectionAmountSheet(EditorSession &session, SelectionAmountOperation operation, QWidget *parent)
: QWidget(parent), m_session(session), m_maximum(operation == SelectionAmountOperation::feather ? 250 : 500),
m_slider(new QSlider(Qt::Horizontal, this)), m_input(new QLineEdit(this)),
m_note(new QLabel(QStringLiteral("Enter a whole number from 1 to %1 px.").arg(m_maximum), this)), m_ok(new QPushButton(QStringLiteral("OK"), this))
{
const int start = operation == SelectionAmountOperation::expand ? session.selectionExpandAmount()
: operation == SelectionAmountOperation::contract ? session.selectionContractAmount()
: session.selectionFeatherAmount();
setFixedWidth(380);
auto *column = new QVBoxLayout(this);
column->setContentsMargins(24, 24, 24, 24);
column->setSpacing(16);
auto *row = new QHBoxLayout;
row->setSpacing(10);
auto *title = new QLabel(QStringLiteral("Amount"), this);
title->setMinimumWidth(60);
title->setBuddy(m_slider);
m_slider->setObjectName(QStringLiteral("amountSlider"));
m_slider->setRange(1, m_maximum);
m_input->setObjectName(QStringLiteral("amountField"));
m_input->setAccessibleName(QStringLiteral("Amount"));
m_input->setPlaceholderText(QStringLiteral("Amount"));
m_input->setFixedWidth(56);
m_input->setAlignment(Qt::AlignRight);
m_input->setText(QString::number(start));
row->addWidget(title);
row->addWidget(m_slider, 1);
row->addWidget(m_input);
row->addSpacing(2);
row->addWidget(new QLabel(QStringLiteral("px"), this));
column->addLayout(row);
m_note->setObjectName(QStringLiteral("amountNote"));
m_note->setForegroundRole(QPalette::PlaceholderText);
QFont callout = m_note->font();
callout.setPixelSize(12);
m_note->setFont(callout);
// Hidden, the note keeps its room, as SwiftUI's opacity does.
QSizePolicy kept = m_note->sizePolicy();
kept.setRetainSizeWhenHidden(true);
m_note->setSizePolicy(kept);
column->addWidget(m_note);
auto *divider = new QFrame(this);
divider->setFrameShape(QFrame::HLine);
divider->setForegroundRole(QPalette::Mid);
column->addWidget(divider);
auto *buttons = new QHBoxLayout;
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("amountCancel"));
cancel->setAutoDefault(false);
m_ok->setObjectName(QStringLiteral("amountOK"));
m_ok->setDefault(true);
buttons->addWidget(cancel);
buttons->addStretch(1);
buttons->addWidget(m_ok);
column->addLayout(buttons);
connect(m_slider, &QSlider::valueChanged, this, [this](int value) {
m_input->setText(QString::number(value));
});
connect(m_input, &QLineEdit::textChanged, this, [this] { refresh(); });
connect(cancel, &QPushButton::clicked, this, [this] { m_session.setSelectionAmountOperation(std::nullopt); });
connect(m_ok, &QPushButton::clicked, this, [this] {
if (const std::optional<int> chosen = amount())
m_session.confirmSelectionAmount(*chosen);
});
refresh();
}
void SelectionAmountSheet::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
m_input->setFocus();
}
std::optional<int> SelectionAmountSheet::amount() const
{
bool ok = false;
const int value = m_input->text().toInt(&ok);
if (!ok || value < 1 || value > m_maximum)
return std::nullopt;
return value;
}
void SelectionAmountSheet::refresh()
{
const std::optional<int> chosen = amount();
const QSignalBlocker quiet(m_slider);
m_slider->setValue(chosen.value_or(1));
m_note->setVisible(!chosen);
m_ok->setEnabled(chosen.has_value());
}
+25
View File
@@ -6,6 +6,7 @@
#include <QComboBox>
#include <QLineEdit>
#include <QPushButton>
#include <QSlider>
#include <QToolButton>
#include <functional>
@@ -70,6 +71,30 @@ private:
SelectionAmountField *const m_expandAmount;
QPushButton *const m_contract;
SelectionAmountField *const m_contractAmount;
QPushButton *const m_feather;
SelectionAmountField *const m_featherAmount;
QLabel *const m_empty;
QPushButton *const m_deselect;
};
// Select's Expand, Contract or Feather amount, in a floating panel.
class SelectionAmountSheet : public QWidget {
Q_OBJECT
public:
SelectionAmountSheet(EditorSession &session, SelectionAmountOperation operation, QWidget *parent = nullptr);
protected:
void showEvent(QShowEvent *event) override;
private:
// A whole number in range, else none; toInt trims spaces.
std::optional<int> amount() const;
void refresh();
EditorSession &m_session;
const int m_maximum;
QSlider *const m_slider;
QLineEdit *const m_input;
QLabel *const m_note;
QPushButton *const m_ok;
};
+53 -12
View File
@@ -1,4 +1,7 @@
#include "UI/TypeControls.h"
#include <QAbstractItemView>
#include <QStylePainter>
#include <QSignalBlocker>
#include "UI/ColorPaletteControls.h"
#include "Document/EditorSession.h"
#include "Rendering/TextLayout.h"
@@ -82,8 +85,52 @@ void TextStyleField::focusOutEvent(QFocusEvent *event)
QLineEdit::focusOutEvent(event);
}
TypeFontPicker::TypeFontPicker(QWidget *parent) : QComboBox(parent)
{
setAccessibleName(QStringLiteral("Font"));
}
void TypeFontPicker::sync(const QString &name)
{
if (view()->isVisible())
return;
if (findText(name) < 0)
addItem(name);
setCurrentIndex(findText(name));
}
void TypeFontPicker::showPopup()
{
if (!m_loaded) {
// Every installed face, and the style's own though missing.
const QString selected = currentText();
QStringList names = TextLayout::availableFonts();
if (!names.contains(selected))
names << selected;
names.sort();
const QSignalBlocker quiet(this);
clear();
addItems(names);
setCurrentIndex(findText(selected));
m_loaded = true;
}
QComboBox::showPopup();
}
void TypeFontPicker::paintEvent(QPaintEvent *)
{
QStylePainter painter(this);
QStyleOptionComboBox option;
initStyleOption(&option);
painter.drawComplexControl(QStyle::CC_ComboBox, option);
const QRect field = style()->subControlRect(QStyle::CC_ComboBox, &option, QStyle::SC_ComboBoxEditField, this);
// The label draws inset a pixel each side.
option.currentText = fontMetrics().elidedText(option.currentText, Qt::ElideRight, field.width() - 2);
painter.drawControl(QStyle::CE_ComboBoxLabel, option);
}
TypeControls::TypeControls(EditorSession &session, QWidget *parent)
: ToolHeaderBar(QStringLiteral("Type"), parent), m_session(session), m_font(new QComboBox),
: ToolHeaderBar(QStringLiteral("Type"), parent), m_session(session), m_font(new TypeFontPicker),
m_size(new TextStyleField(
[this] { return number(m_session.currentTextStyle().fontSize, locale()); },
[this](const QString &text) {
@@ -136,10 +183,13 @@ TypeControls::TypeControls(EditorSession &session, QWidget *parent)
fields->setContentsMargins(0, 0, 0, 0);
fields->setSpacing(10);
m_font->setObjectName(QStringLiteral("typeFont"));
m_font->setFixedWidth(185);
m_font->setFixedWidth(210);
m_font->setToolTip(QStringLiteral("Font face, including bold and italic variants"));
connect(m_font, &QComboBox::activated, this, [this](int index) {
const QString name = m_font->itemText(index);
// The face already shown changes nothing, as Swift's choose.
if (name == m_session.currentTextStyle().fontName)
return;
m_session.changeTextStyle([&name](LayerTextStyle &style) { style.fontName = name; });
});
m_size->setObjectName(QStringLiteral("typeSize"));
@@ -223,16 +273,7 @@ QToolButton *TypeControls::alignment(TextAlignment value)
void TypeControls::synchronize()
{
const LayerTextStyle style = m_session.currentTextStyle();
// Every installed face, and the style's own though missing.
QStringList names = TextLayout::availableFonts();
if (!names.contains(style.fontName))
names << style.fontName;
names.sort();
if (m_font->count() != names.size() || m_font->findText(style.fontName) < 0) {
m_font->clear();
m_font->addItems(names);
}
m_font->setCurrentIndex(m_font->findText(style.fontName));
m_font->sync(style.fontName);
for (size_t index = 0; index < m_alignments.size(); ++index)
m_alignments[index]->setChecked(style.alignment == allTextAlignments[index]);
for (TextStyleField *field : {m_size, m_tracking, m_leading})
+19 -2
View File
@@ -1,12 +1,12 @@
#pragma once
#include "Document/TypeTool.h"
#include "UI/ToolHeaderStyle.h"
#include <QComboBox>
#include <QLineEdit>
#include <array>
#include <functional>
class EditorSession;
class QComboBox;
class QPushButton;
class QToolButton;
class SwatchButton;
@@ -32,6 +32,23 @@ private:
bool m_borrowed = false;
};
// Swift's TypeFontPicker: the catalog loads when the menu opens.
class TypeFontPicker : public QComboBox {
Q_OBJECT
public:
explicit TypeFontPicker(QWidget *parent = nullptr);
// Shows `name`; an open menu is left alone.
void sync(const QString &name);
void showPopup() override;
protected:
// A long name is cut short, never widening the control.
void paintEvent(QPaintEvent *event) override;
private:
bool m_loaded = false;
};
// Swift's TypeControls: font, size, alignment, spacing, Done.
class TypeControls : public ToolHeaderBar {
Q_OBJECT
@@ -47,7 +64,7 @@ private:
void synchronize();
EditorSession &m_session;
QComboBox *const m_font;
TypeFontPicker *const m_font;
TextStyleField *const m_size;
// Open text's colour, else the next text's: the foreground.
SwatchButton *const m_colour;