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:
1 parent
3e52fd90ee
commit
ee34c338d4
42 files changed
+1087
-94
No files matched your search
@@ -1,4 +1,5 @@
|
||||
#include "ContentView.h"
|
||||
#include "UI/LassoControls.h"
|
||||
#include "UI/EffectsSheet.h"
|
||||
#include "UI/FilterSheet.h"
|
||||
#include "UI/HueSaturationSheet.h"
|
||||
@@ -27,6 +28,17 @@ void ContentView::synchronizePanels()
|
||||
m_adjustmentPanel.close();
|
||||
}
|
||||
}
|
||||
// Swift's onChange of the amount's question: a sheet per question.
|
||||
if (m_selectionAmountShown != m_session.selectionAmountOperation()) {
|
||||
m_selectionAmountShown = m_session.selectionAmountOperation();
|
||||
if (m_selectionAmountShown) {
|
||||
m_selectionAmountPanel.onClose = [this] { m_session.setSelectionAmountOperation(std::nullopt); };
|
||||
m_selectionAmountPanel.show(rawValue(*m_selectionAmountShown) + QStringLiteral(" Selection"),
|
||||
new SelectionAmountSheet(m_session, *m_selectionAmountShown));
|
||||
} else {
|
||||
m_selectionAmountPanel.close();
|
||||
}
|
||||
}
|
||||
if (m_filterShown != m_session.filterEdit().has_value()) {
|
||||
m_filterShown = m_session.filterEdit().has_value();
|
||||
if (m_filterShown) {
|
||||
|
||||
@@ -89,5 +89,7 @@ private:
|
||||
FloatingPanel m_filterPanel{QStringLiteral("filterPanel"), *this};
|
||||
bool m_filterShown = false;
|
||||
FloatingPanel m_effectsPanel{QStringLiteral("effectsPanel"), *this};
|
||||
FloatingPanel m_selectionAmountPanel{QStringLiteral("selectionAmountPanel"), *this};
|
||||
std::optional<SelectionAmountOperation> m_selectionAmountShown;
|
||||
std::optional<LayerEffectSelection> m_effectsShown;
|
||||
};
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
bool EditorSession::canStartProjectOperation() const
|
||||
{
|
||||
return !m_textDraft && !m_isProjectBusy && !m_isImporting && !m_brushStroke && !m_warpStroke && !m_showsNewDocument && !m_showsImporter
|
||||
&& !m_renamingLayerID && !m_importError && !m_levels && !m_adjustmentEditingID;
|
||||
return !m_selectionAmountOperation && !m_textDraft && !m_isProjectBusy && !m_isImporting && !m_brushStroke && !m_warpStroke && !m_showsNewDocument
|
||||
&& !m_showsImporter && !m_renamingLayerID && !m_importError && !m_levels && !m_adjustmentEditingID;
|
||||
}
|
||||
|
||||
void EditorSession::waitForFileRequest(std::function<void()> ready)
|
||||
|
||||
@@ -56,6 +56,15 @@ struct SessionJobs {
|
||||
std::optional<QPainterPath> path;
|
||||
std::optional<QString> failure;
|
||||
};
|
||||
struct Subjecting {
|
||||
SelectionMode mode;
|
||||
QUuid documentID;
|
||||
std::function<void()> done;
|
||||
};
|
||||
struct Subjected {
|
||||
std::optional<QImage> mask;
|
||||
std::optional<QString> failure;
|
||||
};
|
||||
// The last stroke's end, for a Shift-click's line.
|
||||
struct LastBrushPoint {
|
||||
QPointF point;
|
||||
|
||||
@@ -16,6 +16,7 @@ EditorSession::EditorSession(QObject *parent) : QObject(parent), m_busyTimer(thi
|
||||
connect(&m_baker, &QFutureWatcher<Baked>::finished, this, &EditorSession::finishBake);
|
||||
connect(&m_inverter, &QFutureWatcher<Inverted>::finished, this, &EditorSession::finishInvert);
|
||||
connect(&m_wand, &QFutureWatcher<Wanded>::finished, this, &EditorSession::finishWand);
|
||||
connect(&m_subject, &QFutureWatcher<Subjected>::finished, this, &EditorSession::finishSubject);
|
||||
connect(&m_filterPreview, &QFutureWatcher<Filtered>::finished, this, &EditorSession::finishFilterPreview);
|
||||
connect(&m_filterCommit, &QFutureWatcher<FilterMade>::finished, this, &EditorSession::finishFilterCommit);
|
||||
connect(&m_cropCommit, &QFutureWatcher<Cropped>::finished, this, &EditorSession::finishCropCommit);
|
||||
@@ -183,8 +184,8 @@ void EditorSession::cycleToolMode()
|
||||
|
||||
bool EditorSession::canUseHistory() const
|
||||
{
|
||||
return !m_textDraft && !m_isProjectBusy && !m_isImporting && !m_brushStroke && !m_warpStroke && !m_showsNewDocument && !m_showsImporter
|
||||
&& !m_renamingLayerID && !m_importError && !m_transformEdit && !m_levels;
|
||||
return !m_selectionAmountOperation && !m_textDraft && !m_isProjectBusy && !m_isImporting && !m_brushStroke && !m_warpStroke && !m_showsNewDocument
|
||||
&& !m_showsImporter && !m_renamingLayerID && !m_importError && !m_transformEdit && !m_levels;
|
||||
}
|
||||
|
||||
void EditorSession::undo()
|
||||
@@ -241,7 +242,8 @@ void EditorSession::endEdit()
|
||||
|
||||
bool EditorSession::canEditLayers() const
|
||||
{
|
||||
return !m_textDraft && m_document && !m_brushStroke && !m_warpStroke && !m_isProjectBusy && !m_isImporting && !m_showsNewDocument && !m_showsImporter
|
||||
return !m_selectionAmountOperation && !m_textDraft && m_document && !m_brushStroke && !m_warpStroke && !m_isProjectBusy && !m_isImporting
|
||||
&& !m_showsNewDocument && !m_showsImporter
|
||||
&& !m_renamingLayerID && !m_transformEdit && !m_cropRect && !m_gradientEdit && !m_filterEdit && !m_pixelMove && !m_levels
|
||||
&& !m_hueSaturation && !m_adjustmentEditingID;
|
||||
}
|
||||
|
||||
@@ -139,6 +139,11 @@ public:
|
||||
void setSelectionExpandAmount(int amount);
|
||||
int selectionContractAmount() const { return m_selectionContractAmount; }
|
||||
void setSelectionContractAmount(int amount);
|
||||
int selectionFeatherAmount() const { return m_selectionFeatherAmount; }
|
||||
void setSelectionFeatherAmount(int amount);
|
||||
// The amount sheet's question, open while set.
|
||||
std::optional<SelectionAmountOperation> selectionAmountOperation() const { return m_selectionAmountOperation; }
|
||||
void setSelectionAmountOperation(std::optional<SelectionAmountOperation> operation);
|
||||
const std::optional<DocumentSelection> &selectionMoveOrigin() const { return m_selectionMoveOrigin; }
|
||||
const WandSettings &wandSettings() const { return m_wandSettings; }
|
||||
void setWandSettings(const WandSettings &settings);
|
||||
@@ -267,6 +272,7 @@ public:
|
||||
#include "Document/LayerFlip+Session.h"
|
||||
#include "Document/Selection+Session.h"
|
||||
#include "Document/MagicWand+Session.h"
|
||||
#include "Document/SubjectRemoval+Session.h"
|
||||
#include "Document/EditorSession+Brush.h"
|
||||
#include "Document/CloneStamp+Session.h"
|
||||
#include "Document/ColorPalette+Session.h"
|
||||
@@ -356,6 +362,8 @@ private:
|
||||
WandSettings m_wandSettings;
|
||||
std::optional<Wanding> m_wanding;
|
||||
QFutureWatcher<Wanded> m_wand;
|
||||
std::optional<Subjecting> m_subjecting;
|
||||
QFutureWatcher<Subjected> m_subject;
|
||||
std::unique_ptr<BrushStroke> m_brushStroke;
|
||||
std::unique_ptr<PixelMove> m_pixelMove;
|
||||
BrushSettings m_brushSettings;
|
||||
@@ -434,4 +442,6 @@ private:
|
||||
bool m_selectionAntialiased = true;
|
||||
int m_selectionExpandAmount = 1;
|
||||
int m_selectionContractAmount = 1;
|
||||
int m_selectionFeatherAmount = 2;
|
||||
std::optional<SelectionAmountOperation> m_selectionAmountOperation;
|
||||
};
|
||||
@@ -122,9 +122,9 @@ void EditorSession::mergeFloatingTransform(const TransformEdit &edit, const Floa
|
||||
if (current && edit.corners) {
|
||||
const QTransform placement = BrushRaster::pixelToDocument(floating.original, int(floating.pixelSize.width()), int(floating.pixelSize.height()));
|
||||
if (const std::optional<QPainterPath> path = DistortWarp::mapPath(current->path, placement, floating.pixelSize, edit.draft, *edit.corners))
|
||||
moved = DocumentSelection{*path, current->antialiased};
|
||||
moved = DocumentSelection{*path, current->antialiased, current->feather};
|
||||
} else if (const std::optional<QTransform> shift = floatingSelectionTransform(edit); current && shift) {
|
||||
moved = DocumentSelection{shift->map(current->path), current->antialiased};
|
||||
moved = DocumentSelection{shift->map(current->path), current->antialiased, current->feather};
|
||||
}
|
||||
std::erase_if(m_document->layers, [&](const ImageLayer &layer) { return layer.id == edit.layerID; });
|
||||
ImageLayer &layer = m_document->layers[indexOf(m_document->layers, source.id)];
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
const std::array<std::pair<LayerBlendMode, const char *>, 13> names{{
|
||||
const std::array<std::pair<LayerBlendMode, const char *>, 14> names{{
|
||||
{LayerBlendMode::normal, "Normal"}, {LayerBlendMode::multiply, "Multiply"}, {LayerBlendMode::screen, "Screen"},
|
||||
{LayerBlendMode::overlay, "Overlay"}, {LayerBlendMode::darken, "Darken"}, {LayerBlendMode::lighten, "Lighten"},
|
||||
{LayerBlendMode::overlay, "Overlay"}, {LayerBlendMode::softLight, "Soft Light"}, {LayerBlendMode::darken, "Darken"}, {LayerBlendMode::lighten, "Lighten"},
|
||||
{LayerBlendMode::difference, "Difference"}, {LayerBlendMode::colorDodge, "Color Dodge"},
|
||||
{LayerBlendMode::colorBurn, "Color Burn"}, {LayerBlendMode::hue, "Hue"}, {LayerBlendMode::saturation, "Saturation"},
|
||||
{LayerBlendMode::color, "Color"}, {LayerBlendMode::luminosity, "Luminosity"},
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
#include <optional>
|
||||
|
||||
enum class LayerBlendMode {
|
||||
normal, multiply, screen, overlay, darken, lighten, difference,
|
||||
normal, multiply, screen, overlay, softLight, darken, lighten, difference,
|
||||
colorDodge, colorBurn, hue, saturation, color, luminosity
|
||||
};
|
||||
|
||||
// Swift's `allCases`: the order of the blend menu.
|
||||
inline constexpr std::array allLayerBlendModes{
|
||||
LayerBlendMode::normal, LayerBlendMode::multiply, LayerBlendMode::screen, LayerBlendMode::overlay,
|
||||
LayerBlendMode::darken, LayerBlendMode::lighten, LayerBlendMode::difference, LayerBlendMode::colorDodge,
|
||||
LayerBlendMode::softLight, LayerBlendMode::darken, LayerBlendMode::lighten, LayerBlendMode::difference, LayerBlendMode::colorDodge,
|
||||
LayerBlendMode::colorBurn, LayerBlendMode::hue, LayerBlendMode::saturation, LayerBlendMode::color,
|
||||
LayerBlendMode::luminosity};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Document/Distort.h"
|
||||
#include "Document/LayerMask.h"
|
||||
#include "Document/PixelAdjust.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
@@ -281,12 +282,11 @@ void EditorSession::addMask(bool revealing)
|
||||
try {
|
||||
if (width <= 0 || height <= 0 || qint64(width) * height > 100'000'000)
|
||||
throw ProjectError(ProjectError::Kind::tooLarge);
|
||||
QImage image = BrushRaster::context(width, height, true);
|
||||
image.fill(revealing ? Qt::white : Qt::black);
|
||||
const QTransform toPixels = BrushRaster::pixelToDocument(active->transform, width, height).inverted();
|
||||
QPainter painter(&image);
|
||||
painter.setRenderHint(QPainter::Antialiasing, current->antialiased);
|
||||
painter.fillPath(toPixels.map(current->path), revealing ? Qt::black : Qt::white);
|
||||
// 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)
|
||||
image.invertPixels();
|
||||
const LayerMask mask(LayerMask::assetFrom(image));
|
||||
finishOpacityEdit();
|
||||
beginEdit(QStringLiteral("Add Mask from Selection"));
|
||||
|
||||
@@ -89,6 +89,11 @@ std::optional<QPainterPath> MaskTracing::darkPixels(const QImage &image)
|
||||
return trace(image, false, [](uchar value) { return value < 128; });
|
||||
}
|
||||
|
||||
std::optional<QPainterPath> MaskTracing::whitePixels(const QImage &image)
|
||||
{
|
||||
return trace(image, false, [](uchar value) { return value >= 128; });
|
||||
}
|
||||
|
||||
std::optional<QPainterPath> MaskTracing::opaquePixels(const QImage &image)
|
||||
{
|
||||
return trace(image, true, [](uchar value) { return value >= 128; });
|
||||
|
||||
@@ -6,5 +6,7 @@
|
||||
// Raster coverage traced into an outline along exact pixel edges.
|
||||
namespace MaskTracing {
|
||||
std::optional<QPainterPath> darkPixels(const QImage &image);
|
||||
// A mask's pixels at least half white: what it shows.
|
||||
std::optional<QPainterPath> whitePixels(const QImage &image);
|
||||
std::optional<QPainterPath> opaquePixels(const QImage &image);
|
||||
}
|
||||
@@ -31,6 +31,11 @@ public:
|
||||
bool canModifySelection() const;
|
||||
void expandSelection(int amount);
|
||||
void contractSelection(int amount);
|
||||
// Menu commands ask an amount; the bar applies directly.
|
||||
void promptSelectionAmount(SelectionAmountOperation operation);
|
||||
void confirmSelectionAmount(int amount);
|
||||
// Softens the edge further each time, as blurs stack.
|
||||
void featherSelection(int amount);
|
||||
void selectAll();
|
||||
void deselect();
|
||||
void invertSelection();
|
||||
|
||||
+87
-12
@@ -1,6 +1,7 @@
|
||||
#include "Document/Selection.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/PixelAdjust.h"
|
||||
#include <QPainterPathStroker>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
@@ -40,6 +41,19 @@ QString rawValue(LassoKind kind)
|
||||
throw std::logic_error("unknown lasso kind");
|
||||
}
|
||||
|
||||
QString rawValue(SelectionAmountOperation operation)
|
||||
{
|
||||
switch (operation) {
|
||||
case SelectionAmountOperation::expand:
|
||||
return QStringLiteral("Expand");
|
||||
case SelectionAmountOperation::contract:
|
||||
return QStringLiteral("Contract");
|
||||
case SelectionAmountOperation::feather:
|
||||
return QStringLiteral("Feather");
|
||||
}
|
||||
throw std::logic_error("unknown selection amount operation");
|
||||
}
|
||||
|
||||
QString rawValue(SelectionMode mode)
|
||||
{
|
||||
switch (mode) {
|
||||
@@ -61,22 +75,31 @@ bool DocumentSelection::isEmpty() const
|
||||
QImage DocumentSelection::coverage(int width, int height) const
|
||||
{
|
||||
QImage image = BrushRaster::context(width, height, true);
|
||||
QPainter painter(&image);
|
||||
fill(painter, path, antialiased);
|
||||
return image;
|
||||
{
|
||||
QPainter painter(&image);
|
||||
fill(painter, path, antialiased || feather > 0);
|
||||
}
|
||||
if (!(feather > 0))
|
||||
return image;
|
||||
// A feathered edge fades either side of the outline.
|
||||
return PixelAdjust::gaussianBlur(image, feather / 2, true);
|
||||
}
|
||||
|
||||
QRectF DocumentSelection::coverageBounds() const
|
||||
{
|
||||
// Four standard deviations keep the falloff past the outline.
|
||||
const double reach = std::ceil(feather * 2);
|
||||
return path.boundingRect().adjusted(-reach, -reach, reach, reach);
|
||||
}
|
||||
|
||||
SelectionClip DocumentSelection::clip(QSizeF canvas) const
|
||||
{
|
||||
const QRect region = path.boundingRect().adjusted(-1, -1, 1, 1).toAlignedRect()
|
||||
const QRect region = coverageBounds().adjusted(-1, -1, 1, 1).toAlignedRect()
|
||||
.intersected(QRect(0, 0, int(canvas.width()), int(canvas.height())));
|
||||
if (isEmpty() || region.isEmpty())
|
||||
return SelectionClip{QRectF(), std::nullopt};
|
||||
QImage image = BrushRaster::context(region.width(), region.height(), true);
|
||||
QPainter painter(&image);
|
||||
painter.translate(-region.x(), -region.y());
|
||||
fill(painter, path, antialiased);
|
||||
return SelectionClip{QRectF(region), image};
|
||||
const DocumentSelection local{path.translated(-region.x(), -region.y()), antialiased, feather};
|
||||
return SelectionClip{QRectF(region), local.coverage(region.width(), region.height())};
|
||||
}
|
||||
|
||||
QRectF DragBox::rect(QPointF anchor, QPointF point, bool square, bool fromCenter)
|
||||
@@ -162,6 +185,19 @@ void EditorSession::setSelectionContractAmount(int amount)
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionFeatherAmount(int amount)
|
||||
{
|
||||
m_selectionFeatherAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionAmountOperation(std::optional<SelectionAmountOperation> operation)
|
||||
{
|
||||
m_selectionAmountOperation = operation;
|
||||
resumeFileRequests();
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::beginLasso(QPointF point, SelectionMode mode)
|
||||
{
|
||||
// The Magic Wand selects with a click, never an outline.
|
||||
@@ -344,7 +380,7 @@ void EditorSession::moveSelection(QSizeF offset)
|
||||
return;
|
||||
// Whole pixels, so edges stay crisp; not re-clipped.
|
||||
m_document->selection = DocumentSelection{m_selectionMoveOrigin->path.translated(std::round(offset.width()), std::round(offset.height())),
|
||||
m_selectionMoveOrigin->antialiased};
|
||||
m_selectionMoveOrigin->antialiased, m_selectionMoveOrigin->feather};
|
||||
notify();
|
||||
}
|
||||
|
||||
@@ -380,6 +416,45 @@ void EditorSession::contractSelection(int amount)
|
||||
resizeSelection(-amount, QStringLiteral("Contract Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::promptSelectionAmount(SelectionAmountOperation operation)
|
||||
{
|
||||
if (!canModifySelection())
|
||||
return;
|
||||
setSelectionAmountOperation(operation);
|
||||
}
|
||||
|
||||
void EditorSession::confirmSelectionAmount(int amount)
|
||||
{
|
||||
const std::optional<SelectionAmountOperation> operation = m_selectionAmountOperation;
|
||||
if (!operation || amount < 1 || amount > (*operation == SelectionAmountOperation::feather ? 250 : 500))
|
||||
return;
|
||||
setSelectionAmountOperation(std::nullopt);
|
||||
switch (*operation) {
|
||||
case SelectionAmountOperation::expand:
|
||||
setSelectionExpandAmount(amount);
|
||||
expandSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::contract:
|
||||
setSelectionContractAmount(amount);
|
||||
contractSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::feather:
|
||||
setSelectionFeatherAmount(amount);
|
||||
featherSelection(amount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorSession::featherSelection(int amount)
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (!canModifySelection() || !current || amount <= 0)
|
||||
return;
|
||||
// Two soft edges spread less than their sum.
|
||||
const double softened = std::sqrt(current->feather * current->feather + double(amount) * amount);
|
||||
setSelection(DocumentSelection{current->path, current->antialiased, std::min(250.0, softened)}, QStringLiteral("Feather Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::resizeSelection(double delta, const QString &name)
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
@@ -392,7 +467,7 @@ void EditorSession::resizeSelection(double delta, const QString &name)
|
||||
stroker.setJoinStyle(Qt::RoundJoin);
|
||||
const QPainterPath band = stroker.createStroke(current->path);
|
||||
const QPainterPath result = delta > 0 ? current->path.united(band).intersected(canvasPath(*m_document)) : current->path.subtracted(band);
|
||||
setSelection(DocumentSelection{result, current->antialiased}, name);
|
||||
setSelection(DocumentSelection{result, current->antialiased, current->feather}, name);
|
||||
}
|
||||
|
||||
void EditorSession::selectAll()
|
||||
@@ -414,5 +489,5 @@ void EditorSession::invertSelection()
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (!m_document || !current)
|
||||
return;
|
||||
setSelection(DocumentSelection{canvasPath(*m_document).subtracted(current->path), current->antialiased}, QStringLiteral("Inverse"));
|
||||
setSelection(DocumentSelection{canvasPath(*m_document).subtracted(current->path), current->antialiased, current->feather}, QStringLiteral("Inverse"));
|
||||
}
|
||||
@@ -18,11 +18,15 @@ struct SelectionClip {
|
||||
struct DocumentSelection {
|
||||
QPainterPath path;
|
||||
bool antialiased = true;
|
||||
// How far the edge fades, in pixels; 0 is hard.
|
||||
double feather = 0;
|
||||
|
||||
// An explicit empty selection: later edits touch nothing.
|
||||
bool isEmpty() const;
|
||||
// Gray coverage at document resolution, white where selected.
|
||||
QImage coverage(int width, int height) const;
|
||||
// The outline's box, grown by the feather's visible falloff.
|
||||
QRectF coverageBounds() const;
|
||||
SelectionClip clip(QSizeF canvas) const;
|
||||
friend bool operator==(const DocumentSelection &, const DocumentSelection &) = default;
|
||||
};
|
||||
@@ -35,6 +39,10 @@ QString rawValue(LassoKind kind);
|
||||
enum class SelectionMode { replace, add, subtract };
|
||||
QString rawValue(SelectionMode mode);
|
||||
|
||||
// What Select's Expand, Contract and Feather ask an amount for.
|
||||
enum class SelectionAmountOperation { expand, contract, feather };
|
||||
QString rawValue(SelectionAmountOperation operation);
|
||||
|
||||
// A drag's box in whole pixels; shared with Shape.
|
||||
namespace DragBox {
|
||||
QRectF rect(QPointF anchor, QPointF point, bool square, bool fromCenter);
|
||||
|
||||
@@ -49,7 +49,7 @@ std::optional<QRectF> EditorSession::selectionCopyRegion() const
|
||||
return std::nullopt;
|
||||
const QRectF canvas(QPointF(0, 0), m_document->size());
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
const QRectF bounds = current ? current->path.boundingRect() : canvas;
|
||||
const QRectF bounds = current ? current->coverageBounds() : canvas;
|
||||
// Path operations leave float noise: round with a tolerance.
|
||||
constexpr double tolerance = 0.001;
|
||||
const double minX = std::floor(bounds.left() + tolerance), minY = std::floor(bounds.top() + tolerance);
|
||||
|
||||
@@ -234,7 +234,7 @@ PixelMove::PixelMove(std::shared_ptr<BrushStroke> raster, DocumentSelection orig
|
||||
|
||||
DocumentSelection PixelMove::movedSelection() const
|
||||
{
|
||||
return DocumentSelection{QTransform::fromTranslate(offset.width(), offset.height()).map(origin.path), origin.antialiased};
|
||||
return DocumentSelection{QTransform::fromTranslate(offset.width(), offset.height()).map(origin.path), origin.antialiased, origin.feather};
|
||||
}
|
||||
|
||||
// Ctrl-drag: starts moving the selected pixels; false with none.
|
||||
@@ -292,7 +292,7 @@ std::optional<DocumentSelection> EditorSession::displayedSelection() const
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (m_transformEdit && current) {
|
||||
if (const std::optional<QTransform> matrix = floatingSelectionTransform(*m_transformEdit))
|
||||
return DocumentSelection{matrix->map(current->path), current->antialiased};
|
||||
return DocumentSelection{matrix->map(current->path), current->antialiased, current->feather};
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Swift's SubjectRemoval extension: Select → Subject.
|
||||
public:
|
||||
bool canSelectSubject() const;
|
||||
// The canvas's foreground, as Remove Background finds it, selected.
|
||||
void selectSubject(SelectionMode mode, std::function<void()> done);
|
||||
|
||||
private:
|
||||
void finishSubject();
|
||||
@@ -1,10 +1,14 @@
|
||||
#include "Document/SubjectRemoval.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/MaskTracing.h"
|
||||
#include "Document/GuidedMatte.h"
|
||||
#include "Document/PixelAdjust.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Logging.h"
|
||||
#include <QMutex>
|
||||
#include <QStandardPaths>
|
||||
#include <QtConcurrent>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
@@ -236,3 +240,65 @@ QImage SubjectRemoval::run(const QImage &image, const FilterSettings &settings)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool EditorSession::canSelectSubject() const
|
||||
{
|
||||
// canEditLayers reads the document and the busy project.
|
||||
return canEditSelection();
|
||||
}
|
||||
|
||||
void EditorSession::selectSubject(SelectionMode mode, std::function<void()> done)
|
||||
{
|
||||
// The caller resumes from the event loop, as after await.
|
||||
const auto finish = [this, &done] {
|
||||
if (done)
|
||||
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
|
||||
};
|
||||
if (!canSelectSubject()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
QImage shown;
|
||||
try {
|
||||
shown = BrushRaster::context(m_document->width, m_document->height, false);
|
||||
QPainter painter(&shown);
|
||||
drawLiveComposite(*m_document, painter);
|
||||
} catch (const ExportError &error) {
|
||||
qCWarning(lcApp) << "Select Subject cannot draw the canvas:" << error.what();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setIsProjectBusy(true);
|
||||
m_subjecting = Subjecting{mode, m_document->id, std::move(done)};
|
||||
m_subject.setFuture(QtConcurrent::run([shown]() -> Subjected {
|
||||
try {
|
||||
return Subjected{SubjectRemoval::subjectMask(shown, std::nullopt, FilterSettings()), std::nullopt};
|
||||
} catch (const SubjectRemovalError &error) {
|
||||
return Subjected{std::nullopt, QString::fromUtf8(error.what())};
|
||||
} catch (const ExportError &error) {
|
||||
return Subjected{std::nullopt, QString::fromUtf8(error.what())};
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void EditorSession::finishSubject()
|
||||
{
|
||||
const Subjected result = m_subject.result();
|
||||
const Subjecting pending = std::exchange(m_subjecting, std::nullopt).value();
|
||||
setIsProjectBusy(false);
|
||||
if (m_document && m_document->id == pending.documentID) {
|
||||
if (result.failure) {
|
||||
qCWarning(lcApp).noquote() << "Select Subject failed:" << *result.failure;
|
||||
setBrushError(result.failure);
|
||||
} else if (const std::optional<QPainterPath> traced = MaskTracing::whitePixels(*result.mask)) {
|
||||
// White where the subject is: its outline is the selection.
|
||||
const QTransform toDocument = BrushRaster::pixelToDocument(LayerTransform{.origin = QPointF(0, 0), .size = m_document->size()},
|
||||
result.mask->width(), result.mask->height());
|
||||
applySelection(toDocument.map(*traced), pending.mode, QStringLiteral("Select Subject"));
|
||||
} else {
|
||||
qCWarning(lcApp) << "Select Subject traced no outline";
|
||||
}
|
||||
}
|
||||
if (pending.done)
|
||||
QMetaObject::invokeMethod(this, pending.done, Qt::QueuedConnection);
|
||||
}
|
||||
@@ -62,8 +62,11 @@ std::optional<LayerText> ImageLayer::liveText() const
|
||||
|
||||
void EditorSession::setTextDraft(std::optional<TextDraft> draft)
|
||||
{
|
||||
// Only a draft that ends frees waiting file requests.
|
||||
const bool ended = m_textDraft && !draft;
|
||||
m_textDraft = std::move(draft);
|
||||
resumeFileRequests();
|
||||
if (ended)
|
||||
resumeFileRequests();
|
||||
notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,15 @@ void InlineTextEditor::synchronize(const TextDraft &draft)
|
||||
}
|
||||
const LayerTextStyle &style = draft.style;
|
||||
// Point text has no box: it is what is typed.
|
||||
m_logicalSize = EditorSession::textBoxSize(style);
|
||||
if (style.boxSize) {
|
||||
m_logicalSize = *style.boxSize;
|
||||
} else {
|
||||
if (m_measuredStyle != style) {
|
||||
m_measuredSize = EditorSession::textBoxSize(style);
|
||||
m_measuredStyle = style;
|
||||
}
|
||||
m_logicalSize = m_measuredSize;
|
||||
}
|
||||
LayerTransform transform = draft.transform.value_or(LayerTransform{.origin = draft.origin, .size = m_logicalSize});
|
||||
// Point text grows at its layer's scale, corner kept.
|
||||
const int index = draft.layerID ? indexOf(document.layers, *draft.layerID) : -1;
|
||||
|
||||
@@ -136,6 +136,9 @@ private:
|
||||
QUuid m_draftID;
|
||||
std::optional<LayerTextStyle> m_shownStyle;
|
||||
QSizeF m_logicalSize{360, 160};
|
||||
// Point text's measure, kept while its style holds.
|
||||
std::optional<LayerTextStyle> m_measuredStyle;
|
||||
QSizeF m_measuredSize;
|
||||
LayerTransform m_shownTransform;
|
||||
double m_handleSize = 6;
|
||||
std::unique_ptr<TextLines> m_lines;
|
||||
|
||||
@@ -20,6 +20,8 @@ QPainter::CompositionMode compositionMode(LayerBlendMode mode)
|
||||
return QPainter::CompositionMode_Screen;
|
||||
case LayerBlendMode::overlay:
|
||||
return QPainter::CompositionMode_Overlay;
|
||||
case LayerBlendMode::softLight:
|
||||
return QPainter::CompositionMode_SoftLight;
|
||||
case LayerBlendMode::darken:
|
||||
return QPainter::CompositionMode_Darken;
|
||||
case LayerBlendMode::lighten:
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user