Select › Color Range: sampled colours, Fuzziness, Invert, a live preview and the panel (13.3b)
This commit is contained in:
1 parent
462fe805b1
commit
0b7eb6cb6f
29 files changed
+1535
-71
No files matched your search
@@ -1,4 +1,5 @@
|
||||
#include "ContentView.h"
|
||||
#include "UI/ColorRangeSheet.h"
|
||||
#include "UI/LassoControls.h"
|
||||
#include "UI/EffectsSheet.h"
|
||||
#include "UI/FilterSheet.h"
|
||||
@@ -41,6 +42,16 @@ void ContentView::synchronizePanels()
|
||||
m_selectionAmountPanel.close();
|
||||
}
|
||||
}
|
||||
// Swift's onChange of colorRange == nil.
|
||||
if (m_colorRangeShown != m_session.colorRange().has_value()) {
|
||||
m_colorRangeShown = m_session.colorRange().has_value();
|
||||
if (m_colorRangeShown) {
|
||||
m_colorRangePanel.onClose = [this] { m_session.cancelColorRange(); };
|
||||
m_colorRangePanel.show(QStringLiteral("Color Range"), new ColorRangeSheet(m_session));
|
||||
} else {
|
||||
m_colorRangePanel.close();
|
||||
}
|
||||
}
|
||||
if (m_filterShown != m_session.filterEdit().has_value()) {
|
||||
m_filterShown = m_session.filterEdit().has_value();
|
||||
if (m_filterShown) {
|
||||
|
||||
@@ -102,5 +102,7 @@ private:
|
||||
FloatingPanel m_effectsPanel{QStringLiteral("effectsPanel"), *this};
|
||||
FloatingPanel m_selectionAmountPanel{QStringLiteral("selectionAmountPanel"), *this};
|
||||
std::optional<SelectionAmountOperation> m_selectionAmountShown;
|
||||
FloatingPanel m_colorRangePanel{QStringLiteral("colorRangePanel"), *this};
|
||||
bool m_colorRangeShown = false;
|
||||
std::optional<LayerEffectSelection> m_effectsShown;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// Select › Color Range's panel; its selection shows until OK.
|
||||
const std::optional<ColorRangeEdit> &colorRange() const { return m_colorRange; }
|
||||
bool canSelectColorRange() const;
|
||||
void beginColorRange();
|
||||
// A canvas click: Shift adds, Alt takes away.
|
||||
void sampleColorRange(QPointF point, bool shift, bool option);
|
||||
// Matches off the UI thread; a newer change supersedes it.
|
||||
void updateColorRange();
|
||||
void commitColorRange();
|
||||
void cancelColorRange();
|
||||
// The sheet's and the canvas's writes, Swift's edit fields.
|
||||
void setColorRangeSampleMode(HueSampleMode mode);
|
||||
void setColorRangeHeld(std::optional<HueSampleMode> held);
|
||||
void setColorRangeFuzziness(double fuzziness);
|
||||
void setColorRangeInvert(bool invert);
|
||||
@@ -0,0 +1,235 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Logging.h"
|
||||
#include <QtConcurrent>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
extern "C" {
|
||||
#include "WandPixels.h"
|
||||
}
|
||||
|
||||
// Swift's ColorRangeSelection.swift: the session's half.
|
||||
namespace {
|
||||
struct ColorRangeJob {
|
||||
QImage image;
|
||||
std::vector<uchar> include;
|
||||
std::vector<uchar> exclude;
|
||||
int fuzziness;
|
||||
bool invert;
|
||||
};
|
||||
|
||||
struct ColorRangeResult {
|
||||
std::optional<QPainterPath> path;
|
||||
QImage preview;
|
||||
std::optional<QString> error;
|
||||
};
|
||||
|
||||
// The straight colour under `point`, over 3 × 3 pixels.
|
||||
std::optional<std::vector<uchar>> colorAt(const QImage &image, QPointF point)
|
||||
{
|
||||
if (!std::isfinite(point.x()) || !std::isfinite(point.y()))
|
||||
return std::nullopt;
|
||||
const double fx = std::floor(point.x()), fy = std::floor(point.y());
|
||||
if (fx < 0 || fy < 0 || fx >= image.width() || fy >= image.height())
|
||||
return std::nullopt;
|
||||
const int x = int(fx), y = int(fy);
|
||||
std::array<int, 4> sums{};
|
||||
// Past the picture's edge, Swift's context holds clear pixels.
|
||||
for (int row = y - 1; row <= y + 1; ++row)
|
||||
for (int column = x - 1; column <= x + 1; ++column) {
|
||||
if (row < 0 || column < 0 || row >= image.height() || column >= image.width())
|
||||
continue;
|
||||
const uchar *pixel = image.constScanLine(row) + column * 4;
|
||||
for (int channel = 0; channel < 4; ++channel)
|
||||
sums[size_t(channel)] += pixel[channel];
|
||||
}
|
||||
if (sums[3] <= 0)
|
||||
return std::nullopt;
|
||||
std::vector<uchar> color(3);
|
||||
for (int channel = 0; channel < 3; ++channel)
|
||||
color[size_t(channel)] = uchar(std::min(255, (sums[size_t(channel)] * 255 + sums[3] / 2) / sums[3]));
|
||||
return color;
|
||||
}
|
||||
|
||||
std::vector<uchar> colorRangeMask(const ColorRangeJob &job)
|
||||
{
|
||||
const int width = job.image.width(), height = job.image.height();
|
||||
try {
|
||||
std::vector<uchar> mask(size_t(width) * size_t(height));
|
||||
color_range_mask(job.image.constBits(), size_t(width), size_t(height), size_t(job.image.bytesPerLine()), job.include.data(),
|
||||
int(job.include.size() / 3), job.exclude.data(), int(job.exclude.size() / 3), job.fuzziness, job.invert ? 1 : 0,
|
||||
mask.data());
|
||||
return mask;
|
||||
} catch (const std::bad_alloc &) {
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
}
|
||||
}
|
||||
|
||||
// The mask shrunk to the panel's preview, twice its size.
|
||||
QImage preview(const std::vector<uchar> &mask, int width, int height)
|
||||
{
|
||||
const double scale = std::min(ColorRangeEdit::previewSize.width() / width, ColorRangeEdit::previewSize.height() / height) * 2;
|
||||
const int w = std::max(1, int(width * scale)), h = std::max(1, int(height * scale));
|
||||
QImage full(width, height, QImage::Format_Grayscale8);
|
||||
if (full.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
for (int y = 0; y < height; ++y)
|
||||
std::copy_n(mask.data() + size_t(y) * size_t(width), width, full.scanLine(y));
|
||||
const QImage shrunk = full.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
if (shrunk.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
return shrunk;
|
||||
}
|
||||
|
||||
ColorRangeResult colorRangeResult(const ColorRangeJob &job)
|
||||
{
|
||||
ColorRangeResult result;
|
||||
try {
|
||||
const std::vector<uchar> mask = colorRangeMask(job);
|
||||
result.preview = preview(mask, job.image.width(), job.image.height());
|
||||
if (std::any_of(mask.begin(), mask.end(), [](uchar value) { return value != 0; }))
|
||||
result.path = MagicWand::outline(mask, job.image.width(), job.image.height());
|
||||
} catch (const MagicWandError &error) {
|
||||
result.error = QString::fromUtf8(error.what());
|
||||
} catch (const ExportError &error) {
|
||||
result.error = QString::fromUtf8(error.what());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
bool EditorSession::canSelectColorRange() const
|
||||
{
|
||||
return canEditSelection();
|
||||
}
|
||||
|
||||
void EditorSession::beginColorRange()
|
||||
{
|
||||
if (!canSelectColorRange())
|
||||
return;
|
||||
const std::optional<QImage> image = selectionSample(*m_document, true);
|
||||
if (!image)
|
||||
return;
|
||||
m_colorRange.emplace(*image, m_document->selection);
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::sampleColorRange(QPointF point, bool shift, bool option)
|
||||
{
|
||||
if (!m_colorRange)
|
||||
return;
|
||||
ColorRangeEdit &edit = m_colorRange.value();
|
||||
const std::optional<std::vector<uchar>> color = colorAt(edit.image, point);
|
||||
if (!color)
|
||||
return;
|
||||
switch (option ? HueSampleMode::remove : shift ? HueSampleMode::add : edit.sampleMode) {
|
||||
case HueSampleMode::replace:
|
||||
edit.include = *color;
|
||||
edit.exclude.clear();
|
||||
break;
|
||||
case HueSampleMode::add:
|
||||
edit.include.insert(edit.include.end(), color->begin(), color->end());
|
||||
break;
|
||||
case HueSampleMode::remove:
|
||||
edit.exclude.insert(edit.exclude.end(), color->begin(), color->end());
|
||||
break;
|
||||
}
|
||||
updateColorRange();
|
||||
}
|
||||
|
||||
void EditorSession::updateColorRange()
|
||||
{
|
||||
if (!m_colorRange || !m_document)
|
||||
return;
|
||||
ColorRangeEdit &edit = m_colorRange.value();
|
||||
const int generation = ++edit.generation;
|
||||
if (!edit.hasColors()) {
|
||||
m_document->selection = edit.original;
|
||||
edit.preview = QImage();
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
const ColorRangeJob job{edit.image, edit.include, edit.exclude, int(std::round(edit.fuzziness)), edit.invert};
|
||||
// A watcher a change, as Swift starts a task each.
|
||||
auto *watcher = new QFutureWatcher<ColorRangeResult>(this);
|
||||
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, id = edit.id, generation, document = m_document->id] {
|
||||
watcher->deleteLater();
|
||||
const ColorRangeResult result = watcher->result();
|
||||
if (!m_colorRange || m_colorRange->id != id || m_colorRange->generation != generation || !m_document || m_document->id != document)
|
||||
return;
|
||||
m_colorRange->error = result.error;
|
||||
m_colorRange->preview = result.preview;
|
||||
if (result.error) {
|
||||
qCWarning(lcApp).noquote() << "Color Range could not select:" << *result.error;
|
||||
} else {
|
||||
m_document->selection = result.path ? std::optional(DocumentSelection{*result.path, m_selectionAntialiased}) : std::nullopt;
|
||||
}
|
||||
notify();
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run([job] { return colorRangeResult(job); }));
|
||||
// The colours changed now; their match lands later.
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::commitColorRange()
|
||||
{
|
||||
if (!m_colorRange)
|
||||
return;
|
||||
const std::optional<DocumentSelection> result = m_document->selection;
|
||||
const ColorRangeEdit edit = std::exchange(m_colorRange, std::nullopt).value();
|
||||
m_document->selection = edit.original;
|
||||
resumeFileRequests();
|
||||
if (edit.hasColors() && !edit.error) {
|
||||
if (result)
|
||||
setSelection(result, QStringLiteral("Color Range"));
|
||||
else
|
||||
deselect();
|
||||
}
|
||||
// Closing is news even when the selection stays.
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::cancelColorRange()
|
||||
{
|
||||
if (!m_colorRange)
|
||||
return;
|
||||
m_document->selection = m_colorRange->original;
|
||||
m_colorRange.reset();
|
||||
resumeFileRequests();
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setColorRangeSampleMode(HueSampleMode mode)
|
||||
{
|
||||
if (!m_colorRange || m_colorRange->sampleMode == mode)
|
||||
return;
|
||||
m_colorRange->sampleMode = mode;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setColorRangeHeld(std::optional<HueSampleMode> held)
|
||||
{
|
||||
if (!m_colorRange || m_colorRange->held == held)
|
||||
return;
|
||||
m_colorRange->held = held;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setColorRangeFuzziness(double fuzziness)
|
||||
{
|
||||
// Swift's binding: whole, within the range, changed or nothing.
|
||||
const double clamped = std::min(ColorRangeEdit::fuzzinessHigh, std::max(ColorRangeEdit::fuzzinessLow, std::round(fuzziness)));
|
||||
if (!m_colorRange || m_colorRange->fuzziness == clamped)
|
||||
return;
|
||||
m_colorRange->fuzziness = clamped;
|
||||
updateColorRange();
|
||||
}
|
||||
|
||||
void EditorSession::setColorRangeInvert(bool invert)
|
||||
{
|
||||
if (!m_colorRange)
|
||||
return;
|
||||
m_colorRange->invert = invert;
|
||||
updateColorRange();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include "Document/HueSaturation.h"
|
||||
#include "Document/Selection.h"
|
||||
#include <QImage>
|
||||
#include <QSizeF>
|
||||
#include <QUuid>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// Select › Color Range: pixels near the clicked colours, anywhere.
|
||||
class ColorRangeEdit {
|
||||
public:
|
||||
static constexpr double fuzzinessLow = 0, fuzzinessHigh = 200;
|
||||
// The panel's preview fits in this, in points.
|
||||
static constexpr QSizeF previewSize{292, 200};
|
||||
ColorRangeEdit(QImage image, std::optional<DocumentSelection> original) : image(std::move(image)), original(std::move(original)) {}
|
||||
double fuzziness = 40;
|
||||
bool invert = false;
|
||||
// The next click: start over, add or take away.
|
||||
HueSampleMode sampleMode = HueSampleMode::replace;
|
||||
// Shift (add) or Alt (take away) held, over the mode.
|
||||
std::optional<HueSampleMode> held;
|
||||
HueSampleMode effectiveMode() const { return held.value_or(sampleMode); }
|
||||
// The selection in black and white; null until a colour.
|
||||
QImage preview;
|
||||
// Straight sRGB colours, three bytes each.
|
||||
std::vector<uchar> include;
|
||||
std::vector<uchar> exclude;
|
||||
std::optional<QString> error;
|
||||
// The composite at document size: what colours are matched against.
|
||||
QImage image;
|
||||
std::optional<DocumentSelection> original;
|
||||
int generation = 0;
|
||||
// Swift's object identity: a result lands on its own edit.
|
||||
QUuid id = QUuid::createUuid();
|
||||
bool hasColors() const { return !include.empty(); }
|
||||
};
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
bool EditorSession::canStartProjectOperation() const
|
||||
{
|
||||
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;
|
||||
return !m_selectionAmountOperation && !m_colorRange && !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)
|
||||
|
||||
@@ -198,8 +198,8 @@ void EditorSession::cycleToolMode()
|
||||
|
||||
bool EditorSession::canUseHistory() const
|
||||
{
|
||||
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;
|
||||
return !m_selectionAmountOperation && !m_colorRange && !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()
|
||||
@@ -256,7 +256,7 @@ void EditorSession::endEdit()
|
||||
|
||||
bool EditorSession::canEditLayers() const
|
||||
{
|
||||
return !m_selectionAmountOperation && !m_textDraft && m_document && !m_brushStroke && !m_warpStroke && !m_isProjectBusy && !m_isImporting
|
||||
return !m_selectionAmountOperation && !m_colorRange && !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;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/CloneStamp.h"
|
||||
#include "Document/ColorPalette.h"
|
||||
#include "Document/ColorRangeSelection.h"
|
||||
#include "Document/Gradient.h"
|
||||
#include "Document/Distort.h"
|
||||
#include "Document/DocumentLimits.h"
|
||||
@@ -282,7 +283,6 @@ public:
|
||||
void setBrushError(std::optional<QString> error);
|
||||
|
||||
void notify() { emit changed(); }
|
||||
|
||||
// Swift's extensions, each declared in its twin's header.
|
||||
#include "Document/EditorSession+Projects.h"
|
||||
#include "IO/ImageResizer+Session.h"
|
||||
@@ -319,6 +319,7 @@ public:
|
||||
#include "Document/CameraRaw+Session.h"
|
||||
#include "Document/Guides+Session.h"
|
||||
#include "Document/ObjectSelection+Session.h"
|
||||
#include "Document/ColorRangeSelection+Session.h"
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
@@ -346,7 +347,6 @@ private:
|
||||
mutable std::map<QUuid, DistortPreviewCache> m_distortPreviewCache;
|
||||
mutable std::map<QUuid, DistortEffectsCache> m_distortEffectsCache;
|
||||
mutable std::optional<MaskDistortPreviewCache> m_maskDistortPreviewCache;
|
||||
|
||||
std::optional<TransformEdit> m_transformEdit;
|
||||
std::optional<TransformDuplicate> m_transformDuplicate;
|
||||
bool m_transformAutoSelect = ToolDefaults::boolean(QStringLiteral("autoSelect"), false);
|
||||
@@ -495,4 +495,5 @@ private:
|
||||
int m_selectionContractAmount = 1;
|
||||
int m_selectionFeatherAmount = 2;
|
||||
std::optional<SelectionAmountOperation> m_selectionAmountOperation;
|
||||
std::optional<ColorRangeEdit> m_colorRange;
|
||||
};
|
||||
@@ -200,7 +200,7 @@ QCursor CanvasView::wandCursor(SelectionMode mode, double ratio)
|
||||
return QCursor(pixmap, int(hotSpot.x()), int(hotSpot.y()));
|
||||
}
|
||||
|
||||
QCursor CanvasView::eyedropperCursor(double ratio)
|
||||
QCursor CanvasView::eyedropperCursor(double ratio, HueSampleMode badge)
|
||||
{
|
||||
QPixmap pixmap = cursorPixmap(QSize(24, 24), ratio);
|
||||
QPainter painter(&pixmap);
|
||||
@@ -220,6 +220,18 @@ QCursor CanvasView::eyedropperCursor(double ratio)
|
||||
for (int step = 0; step < 16; ++step)
|
||||
draw(QPointF(std::cos(step * M_PI / 8), std::sin(step * M_PI / 8)) * 1.25, Qt::white);
|
||||
draw(QPointF(), Qt::black);
|
||||
if (badge != HueSampleMode::replace) {
|
||||
// Bottom right, clear of the tip: a signed disc.
|
||||
const QRectF spot(12.5, 12.5, 11, 11);
|
||||
painter.setPen(QPen(Qt::black, 1));
|
||||
painter.setBrush(Qt::white);
|
||||
painter.drawEllipse(spot);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(Qt::black);
|
||||
painter.drawRect(QRectF(spot.center().x() - 3, spot.center().y() - 0.8, 6, 1.6));
|
||||
if (badge == HueSampleMode::add)
|
||||
painter.drawRect(QRectF(spot.center().x() - 0.8, spot.center().y() - 3, 1.6, 6));
|
||||
}
|
||||
// The dropper's tip is the hot spot, as Swift's.
|
||||
const QPointF tip = glyph.topLeft() + EyedropperIcon::tip() * scale;
|
||||
return QCursor(pixmap, int(std::round(tip.x())), int(std::round(tip.y())));
|
||||
@@ -263,7 +275,7 @@ void CanvasView::updateCursor()
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
// Picking outranks Space and the tools, as Swift's cursor rects.
|
||||
else if (picking())
|
||||
setCursor(eyedropperCursor(devicePixelRatio()));
|
||||
setCursor(eyedropperCursor(devicePixelRatio(), m_session.colorRange() ? m_session.colorRange().value().effectiveMode() : HueSampleMode::replace));
|
||||
else if (m_session.hueTargeting())
|
||||
setCursor(Qt::SizeHorCursor);
|
||||
else if (m_spaceHeld || tool == NavigationTool::hand)
|
||||
@@ -365,6 +377,9 @@ void CanvasView::modifiersChanged()
|
||||
// As Swift's monitor: Alt may start or end picking.
|
||||
synchronizeDisplay();
|
||||
m_session.updateHeldSelectionKeys(m_shiftHeld, m_optionHeld);
|
||||
// Color Range: Shift adds and Alt takes away while held.
|
||||
if (m_session.colorRange())
|
||||
m_session.setColorRangeHeld(m_optionHeld ? std::optional(HueSampleMode::remove) : m_shiftHeld ? std::optional(HueSampleMode::add) : std::nullopt);
|
||||
updateCursor();
|
||||
updateBrushCursor();
|
||||
}
|
||||
|
||||
@@ -156,6 +156,13 @@ void CanvasView::press(QMouseEvent *event, int clicks)
|
||||
// Open Levels leaves the canvas to panning and zooming.
|
||||
if (m_session.levels() && !m_spaceHeld && m_session.tool() != NavigationTool::hand && m_session.tool() != NavigationTool::zoom)
|
||||
return;
|
||||
// Color Range takes the colour; its panel keeps the keys.
|
||||
if (m_session.colorRange() && !m_spaceHeld) {
|
||||
m_session.sampleColorRange(m_session.viewport.documentPoint(point, m_session.document()->size()), event->modifiers().testFlag(Qt::ShiftModifier),
|
||||
event->modifiers().testFlag(Qt::AltModifier));
|
||||
FloatingPanel::refocus(QStringLiteral("colorRangePanel"));
|
||||
return;
|
||||
}
|
||||
if (!m_spaceHeld && pickingPress(point))
|
||||
return;
|
||||
if (m_spaceHeld || m_session.tool() == NavigationTool::hand) {
|
||||
|
||||
@@ -14,7 +14,7 @@ bool CanvasView::palettePicking() const
|
||||
bool CanvasView::picking() const
|
||||
{
|
||||
const std::optional<FilterEdit> &edit = m_session.filterEdit();
|
||||
return palettePicking() || m_session.colorPicker() || m_session.hueSampleMode() || (m_session.levels() && m_session.levels()->sampleMode)
|
||||
return palettePicking() || m_session.colorPicker() || m_session.hueSampleMode() || (m_session.levels() && m_session.levels()->sampleMode) || m_session.colorRange()
|
||||
|| (edit
|
||||
&& (edit->rawPanel.samplesWhiteBalance || edit->rawPanel.samplesPointColor || edit->rawPanel.samplesDefringe
|
||||
|| edit->rawPanel.drawingGeometryGuide));
|
||||
@@ -23,6 +23,12 @@ bool CanvasView::picking() const
|
||||
// Picking or targeting begins or ends: a sampling goes.
|
||||
void CanvasView::syncPicking()
|
||||
{
|
||||
// The sheet's eyedroppers change the badge, not the sampling.
|
||||
const std::optional<HueSampleMode> range = m_session.colorRange() ? std::optional(m_session.colorRange().value().effectiveMode()) : std::nullopt;
|
||||
if (range != m_displayedRangeMode) {
|
||||
m_displayedRangeMode = range;
|
||||
updateCursor();
|
||||
}
|
||||
if (m_displayedPicking == picking() && m_displayedTargeting == m_session.hueTargeting())
|
||||
return;
|
||||
m_displayedPicking = picking();
|
||||
|
||||
@@ -51,8 +51,8 @@ public:
|
||||
static QCursor selectionCursor(SelectionIcon icon, SelectionMode mode, double ratio);
|
||||
// The wand, its sparkle at the hot spot, badged alike.
|
||||
static QCursor wandCursor(SelectionMode mode, double ratio);
|
||||
// Swift's eyedropper: haloed white, its tip the hot spot.
|
||||
static QCursor eyedropperCursor(double ratio);
|
||||
// Swift's eyedropper, haloed, its tip the hot spot; badged.
|
||||
static QCursor eyedropperCursor(double ratio, HueSampleMode badge = HueSampleMode::replace);
|
||||
// The brush circle as last drawn, for tests.
|
||||
const BrushCursorOverlay &brushCursor() const { return m_brushCursor; }
|
||||
const SampleRingOverlay &sampleRing() const { return m_sampleRing; }
|
||||
@@ -397,6 +397,8 @@ private:
|
||||
bool m_samplingColor = false;
|
||||
bool m_displayedPicking = false;
|
||||
bool m_displayedTargeting = false;
|
||||
// Color Range's eyedropper, which its badge shows.
|
||||
std::optional<HueSampleMode> m_displayedRangeMode;
|
||||
// Where a targeted drag began, in view points.
|
||||
std::optional<QPointF> m_hueTargetStart;
|
||||
std::optional<CropDrag> m_cropDrag;
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "UI/ColorRangeSheet.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include "UI/SampleButton.h"
|
||||
#include <QCheckBox>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLocale>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSlider>
|
||||
#include <QVBoxLayout>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
QString rangeHelp(HueSampleMode mode)
|
||||
{
|
||||
switch (mode) {
|
||||
case HueSampleMode::replace: return QStringLiteral("Click the image to select that color");
|
||||
case HueSampleMode::add: return QStringLiteral("Click the image to add that color to the selection");
|
||||
case HueSampleMode::remove: return QStringLiteral("Click the image to take that color out of the selection");
|
||||
}
|
||||
throw std::logic_error("unknown sample mode");
|
||||
}
|
||||
|
||||
// The selection in black and white, shaped like the canvas.
|
||||
class RangePreview : public QWidget {
|
||||
public:
|
||||
RangePreview(EditorSession &session, QWidget *parent) : QWidget(parent), m_session(session)
|
||||
{
|
||||
const QSizeF limit = ColorRangeEdit::previewSize;
|
||||
const QSizeF size = m_session.colorRange() ? QSizeF(m_session.colorRange()->image.size()) : limit;
|
||||
const double scale = std::min(limit.width() / size.width(), limit.height() / size.height());
|
||||
setFixedSize(int(std::round(size.width() * scale)), int(std::round(size.height() * scale)));
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), Qt::black);
|
||||
if (m_session.colorRange() && !m_session.colorRange()->preview.isNull()) {
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
painter.drawImage(QRectF(rect()), m_session.colorRange()->preview);
|
||||
}
|
||||
// Swift's strokeBorder: a white line inside, at 20%.
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 51), 1));
|
||||
painter.drawRect(QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5));
|
||||
}
|
||||
|
||||
private:
|
||||
EditorSession &m_session;
|
||||
};
|
||||
}
|
||||
|
||||
ColorRangeSheet::ColorRangeSheet(EditorSession &session, QWidget *parent)
|
||||
: QWidget(parent), m_session(session), m_preview(new RangePreview(session, this)), m_caption(new QLabel(this)), m_slider(new QSlider(Qt::Horizontal, this)),
|
||||
m_field(new PickerField(
|
||||
[this] {
|
||||
bool number = false;
|
||||
const double typed = m_field->locale().toDouble(m_field->text(), &number);
|
||||
if (m_field->isModified() && number && std::isfinite(typed))
|
||||
m_session.setColorRangeFuzziness(typed);
|
||||
m_field->setModified(false);
|
||||
synchronize();
|
||||
},
|
||||
nullptr, this)),
|
||||
m_invert(new QCheckBox(QStringLiteral("Invert"), this)), m_error(new QLabel(this)), m_ok(new QPushButton(QStringLiteral("OK"), this))
|
||||
{
|
||||
setFixedWidth(340);
|
||||
auto *column = new QVBoxLayout(this);
|
||||
column->setContentsMargins(24, 24, 24, 24);
|
||||
column->setSpacing(16);
|
||||
auto *droppers = new QHBoxLayout;
|
||||
droppers->setSpacing(6);
|
||||
for (size_t index = 0; index < allHueSampleModes.size(); ++index) {
|
||||
const HueSampleMode mode = allHueSampleModes[index];
|
||||
auto *button = new SampleButton([mode](QPainter &painter, const QColor &ink) { sampleEyedropper(painter, mode, ink); }, this);
|
||||
button->setObjectName(QStringLiteral("colorRange") + rawValue(mode));
|
||||
button->setToolTip(rangeHelp(mode));
|
||||
button->setAccessibleName(rawValue(mode) + QStringLiteral(" color"));
|
||||
connect(button, &QAbstractButton::clicked, this, [this, mode] { m_session.setColorRangeSampleMode(mode); });
|
||||
m_eyedroppers[index] = button;
|
||||
droppers->addWidget(button);
|
||||
}
|
||||
droppers->addStretch(1);
|
||||
column->addLayout(droppers);
|
||||
column->addWidget(m_preview, 0, Qt::AlignHCenter);
|
||||
m_caption->setObjectName(QStringLiteral("colorRangeCaption"));
|
||||
m_caption->setWordWrap(true);
|
||||
QFont callout = m_caption->font();
|
||||
callout.setPixelSize(12);
|
||||
m_caption->setFont(callout);
|
||||
m_caption->setForegroundRole(QPalette::PlaceholderText);
|
||||
column->addWidget(m_caption);
|
||||
// Fuzziness: a scrubbable title, the slider, the field.
|
||||
auto *row = new QWidget(this);
|
||||
row->setToolTip(QStringLiteral("How far a color may be from the picked ones and still be selected"));
|
||||
auto *fuzziness = new QHBoxLayout(row);
|
||||
fuzziness->setContentsMargins(0, 0, 0, 0);
|
||||
fuzziness->setSpacing(10);
|
||||
auto *title = new QLabel(QStringLiteral("Fuzziness"), row);
|
||||
new NumericScrub(title, {.sensitivity = 1, .low = ColorRangeEdit::fuzzinessLow, .high = ColorRangeEdit::fuzzinessHigh, .step = std::nullopt,
|
||||
.value = [this] { return m_session.colorRange() ? m_session.colorRange()->fuzziness : 40; },
|
||||
.set = [this](double value) {
|
||||
// Its value replaces the field's typing.
|
||||
m_field->setModified(false);
|
||||
m_session.setColorRangeFuzziness(value);
|
||||
}});
|
||||
title->setBuddy(m_slider);
|
||||
m_slider->setObjectName(QStringLiteral("fuzzinessSlider"));
|
||||
m_slider->setRange(int(ColorRangeEdit::fuzzinessLow), int(ColorRangeEdit::fuzzinessHigh));
|
||||
connect(m_slider, &QSlider::valueChanged, this, [this](int value) {
|
||||
m_field->setModified(false);
|
||||
m_session.setColorRangeFuzziness(value);
|
||||
});
|
||||
m_field->setObjectName(QStringLiteral("fuzzinessField"));
|
||||
m_field->setAccessibleName(QStringLiteral("Fuzziness"));
|
||||
m_field->setPlaceholderText(QStringLiteral("Fuzziness"));
|
||||
m_field->setAlignment(Qt::AlignRight);
|
||||
m_field->setFixedWidth(48);
|
||||
fuzziness->addWidget(title);
|
||||
fuzziness->addWidget(m_slider, 1);
|
||||
fuzziness->addWidget(m_field);
|
||||
column->addWidget(row);
|
||||
m_invert->setObjectName(QStringLiteral("colorRangeInvert"));
|
||||
m_invert->setToolTip(QStringLiteral("Select everything except those colors, such as all but a green screen"));
|
||||
connect(m_invert, &QCheckBox::clicked, this, [this](bool checked) { m_session.setColorRangeInvert(checked); });
|
||||
column->addWidget(m_invert);
|
||||
m_error->setObjectName(QStringLiteral("colorRangeError"));
|
||||
m_error->setWordWrap(true);
|
||||
m_error->setForegroundRole(QPalette::BrightText);
|
||||
column->addWidget(m_error);
|
||||
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->setAutoDefault(false);
|
||||
connect(cancel, &QPushButton::clicked, this, [this] { m_session.cancelColorRange(); });
|
||||
m_ok->setDefault(true);
|
||||
connect(m_ok, &QPushButton::clicked, this, [this] { m_session.commitColorRange(); });
|
||||
buttons->addWidget(cancel);
|
||||
buttons->addStretch(1);
|
||||
buttons->addWidget(m_ok);
|
||||
column->addLayout(buttons);
|
||||
NativeShortcut::bind(*this, m_ok, cancel);
|
||||
connect(&m_session, &EditorSession::changed, this, &ColorRangeSheet::synchronize);
|
||||
synchronize();
|
||||
}
|
||||
|
||||
ColorRangeSheet::~ColorRangeSheet()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
|
||||
void ColorRangeSheet::synchronize()
|
||||
{
|
||||
const std::optional<ColorRangeEdit> &edit = m_session.colorRange();
|
||||
if (!edit)
|
||||
return;
|
||||
for (size_t index = 0; index < m_eyedroppers.size(); ++index)
|
||||
m_eyedroppers[index]->setChecked(edit->effectiveMode() == allHueSampleModes[index]);
|
||||
m_caption->setText(edit->hasColors() ? QStringLiteral("Shift-click adds a color, Alt-click takes one away.")
|
||||
: QStringLiteral("Click the image to pick the color to select."));
|
||||
{
|
||||
const QSignalBlocker quiet(m_slider);
|
||||
m_slider->setValue(int(edit->fuzziness));
|
||||
}
|
||||
if (!m_field->hasFocus() || !m_field->isModified())
|
||||
m_field->setText(m_field->locale().toString(edit->fuzziness, 'f', 0));
|
||||
m_invert->setChecked(edit->invert);
|
||||
m_error->setText(edit->error.value_or(QString()));
|
||||
m_error->setVisible(edit->error.has_value());
|
||||
m_preview->update();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include "Document/EditorSession.h"
|
||||
#include <QWidget>
|
||||
#include <array>
|
||||
|
||||
class QCheckBox;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QSlider;
|
||||
class PickerField;
|
||||
class SampleButton;
|
||||
|
||||
// Swift's ColorRangeSheet: eyedroppers, preview, Fuzziness, Invert.
|
||||
class ColorRangeSheet : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ColorRangeSheet(EditorSession &session, QWidget *parent = nullptr);
|
||||
~ColorRangeSheet() override;
|
||||
|
||||
private:
|
||||
void synchronize();
|
||||
|
||||
EditorSession &m_session;
|
||||
std::array<SampleButton *, 3> m_eyedroppers{};
|
||||
QWidget *m_preview;
|
||||
QLabel *m_caption;
|
||||
QSlider *m_slider;
|
||||
PickerField *m_field;
|
||||
QCheckBox *m_invert;
|
||||
QLabel *m_error;
|
||||
QPushButton *m_ok;
|
||||
};
|
||||
@@ -198,6 +198,7 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
|
||||
});
|
||||
add(select, QStringLiteral("subject"), QStringLiteral("Subject"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_A),
|
||||
[this] { session().selectSubject(SelectionMode::replace, {}); });
|
||||
add(select, QStringLiteral("colorRange"), QStringLiteral("Color Range…"), QKeySequence(), [this] { session().beginColorRange(); });
|
||||
add(select, QStringLiteral("maskBlackAreas"), QStringLiteral("Mask's Black Areas"), QKeySequence(), [this] {
|
||||
if (const std::optional<QUuid> id = session().activeLayerID())
|
||||
session().loadMaskSelection(*id);
|
||||
@@ -380,6 +381,7 @@ void CompositorMenus::synchronize()
|
||||
action(QStringLiteral("layerPixels"))->setEnabled(active && active->asset && s.canEditSelection());
|
||||
action(QStringLiteral("maskBlackAreas"))->setEnabled(active && active->mask && s.canEditSelection());
|
||||
action(QStringLiteral("subject"))->setEnabled(s.canSelectSubject());
|
||||
action(QStringLiteral("colorRange"))->setEnabled(s.canSelectColorRange());
|
||||
for (const char *name : {"expandSelection", "contractSelection", "featherSelection"})
|
||||
action(QString::fromLatin1(name))->setEnabled(s.canModifySelection());
|
||||
action(QStringLiteral("levels"))->setEnabled(s.canAdjustColors() && !s.hueSaturation());
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Rendering/EyedropperIcon.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include "UI/SampleButton.h"
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QFrame>
|
||||
@@ -62,59 +63,6 @@ HueSaturationSettings current(const EditorSession &session)
|
||||
return session.hueSaturation() ? session.hueSaturation()->settings : HueSaturationSettings();
|
||||
}
|
||||
|
||||
// Swift's plain sampling buttons: a glyph, tinted when chosen.
|
||||
class SampleButton : public QToolButton {
|
||||
public:
|
||||
SampleButton(std::function<void(QPainter &, const QColor &)> glyph, QWidget *parent) : QToolButton(parent), m_glyph(std::move(glyph))
|
||||
{
|
||||
setCheckable(true);
|
||||
setFixedSize(24, 20);
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
if (isChecked()) {
|
||||
QColor tint = palette().color(QPalette::Highlight);
|
||||
tint.setAlphaF(0.25f);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(tint);
|
||||
painter.drawRoundedRect(QRectF(rect()), 4, 4);
|
||||
}
|
||||
m_glyph(painter, palette().color(QPalette::WindowText));
|
||||
}
|
||||
|
||||
private:
|
||||
const std::function<void(QPainter &, const QColor &)> m_glyph;
|
||||
};
|
||||
|
||||
// Swift's eyedropper, badged plus or minus for Add and Remove.
|
||||
void eyedropper(QPainter &painter, HueSampleMode mode, const QColor &ink)
|
||||
{
|
||||
// A 14-point glyph, centred in the 24 by 20 frame.
|
||||
painter.save();
|
||||
painter.translate(5, 3);
|
||||
painter.save();
|
||||
painter.scale(14.0 / 18, 14.0 / 18);
|
||||
painter.setPen(QPen(ink, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
||||
EyedropperIcon::paint(painter);
|
||||
painter.restore();
|
||||
if (mode != HueSampleMode::replace) {
|
||||
// An 8-point disc, bottom right, nudged 3 and 1.
|
||||
QPainterPath badge;
|
||||
badge.addEllipse(QRectF(9, 7, 8, 8));
|
||||
QPainterPath sign;
|
||||
sign.setFillRule(Qt::WindingFill);
|
||||
sign.addRect(QRectF(10.75, 10.4, 4.5, 1.2));
|
||||
if (mode == HueSampleMode::add)
|
||||
sign.addRect(QRectF(12.4, 8.75, 1.2, 4.5));
|
||||
painter.fillPath(badge.subtracted(sign), ink);
|
||||
}
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
// Swift's hand.point.up.left: a finger up and left, outlined.
|
||||
void pointingHand(QPainter &painter, const QColor &ink)
|
||||
{
|
||||
@@ -133,7 +81,7 @@ void pointingHand(QPainter &painter, const QColor &ink)
|
||||
|
||||
QToolButton *sampleButton(HueSampleMode mode, QWidget *parent)
|
||||
{
|
||||
auto *button = new SampleButton([mode](QPainter &painter, const QColor &ink) { eyedropper(painter, mode, ink); }, parent);
|
||||
auto *button = new SampleButton([mode](QPainter &painter, const QColor &ink) { sampleEyedropper(painter, mode, ink); }, parent);
|
||||
button->setObjectName(QStringLiteral("hue") + rawValue(mode));
|
||||
button->setToolTip(help(mode));
|
||||
button->setAccessibleName(rawValue(mode) + QStringLiteral(" color"));
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "UI/SampleButton.h"
|
||||
#include "Rendering/EyedropperIcon.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
SampleButton::SampleButton(std::function<void(QPainter &, const QColor &)> glyph, QWidget *parent) : QToolButton(parent), m_glyph(std::move(glyph))
|
||||
{
|
||||
setCheckable(true);
|
||||
setFixedSize(24, 20);
|
||||
}
|
||||
|
||||
void SampleButton::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
if (isChecked()) {
|
||||
QColor tint = palette().color(QPalette::Highlight);
|
||||
tint.setAlphaF(0.25f);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(tint);
|
||||
painter.drawRoundedRect(QRectF(rect()), 4, 4);
|
||||
}
|
||||
m_glyph(painter, palette().color(QPalette::WindowText));
|
||||
}
|
||||
|
||||
void sampleEyedropper(QPainter &painter, HueSampleMode mode, const QColor &ink)
|
||||
{
|
||||
// A 14-point glyph, centred in the 24 by 20 frame.
|
||||
painter.save();
|
||||
painter.translate(5, 3);
|
||||
painter.save();
|
||||
painter.scale(14.0 / 18, 14.0 / 18);
|
||||
painter.setPen(QPen(ink, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
||||
EyedropperIcon::paint(painter);
|
||||
painter.restore();
|
||||
if (mode != HueSampleMode::replace) {
|
||||
// An 8-point disc, bottom right, nudged 3 and 1.
|
||||
QPainterPath badge;
|
||||
badge.addEllipse(QRectF(9, 7, 8, 8));
|
||||
QPainterPath sign;
|
||||
sign.setFillRule(Qt::WindingFill);
|
||||
sign.addRect(QRectF(10.75, 10.4, 4.5, 1.2));
|
||||
if (mode == HueSampleMode::add)
|
||||
sign.addRect(QRectF(12.4, 8.75, 1.2, 4.5));
|
||||
painter.fillPath(badge.subtracted(sign), ink);
|
||||
}
|
||||
painter.restore();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include "Document/HueSaturation.h"
|
||||
#include <QToolButton>
|
||||
#include <functional>
|
||||
|
||||
// Swift's plain sampling buttons: a glyph, tinted when chosen.
|
||||
class SampleButton : public QToolButton {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SampleButton(std::function<void(QPainter &, const QColor &)> glyph, QWidget *parent);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
const std::function<void(QPainter &, const QColor &)> m_glyph;
|
||||
};
|
||||
|
||||
// Swift's eyedropper, badged plus or minus for Add and Remove.
|
||||
void sampleEyedropper(QPainter &painter, HueSampleMode mode, const QColor &ink);
|
||||
Reference in new issue
Block a user