OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor (github.com/robbietilton/Compositor), whose Swift source is its specification. It reads and writes Compositor's .comp projects. Packages for Ubuntu, Fedora and Arch come from the release workflow; scripts/install/ holds one install script per distribution family, and flake.nix serves NixOS. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
c864304695
564 files changed
+100804
No files matched your search
@@ -0,0 +1,276 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Logging.h"
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace {
|
||||
// Swift's adjustedPixels: the render, or why it failed.
|
||||
SessionJobs::Adjusted adjusted(const HueSaturationJob &job)
|
||||
{
|
||||
try {
|
||||
return {HueSaturationFilter::run(job), std::nullopt};
|
||||
} catch (const ExportError &error) {
|
||||
return {std::nullopt, QString::fromUtf8(error.what())};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Swift's session extension in HueSaturation.swift.
|
||||
bool EditorSession::canAdjustColors() const
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
return !m_levels && !m_filterEdit && m_document && layer && !m_isProjectBusy && !m_isImporting && !m_brushStroke && !m_pixelMove && !m_renamingLayerID && !m_showsNewDocument
|
||||
&& !m_showsImporter && m_selectedLayerIDs.size() == 1 && !layer->isGroup && !m_isMaskSelected && layer->asset
|
||||
&& m_document->effectiveVisibleIDs().contains(layer->id) && !(current && current->isEmpty());
|
||||
}
|
||||
|
||||
void EditorSession::beginHueSaturation()
|
||||
{
|
||||
// Swift beeps.
|
||||
if (m_hueSaturation || !canAdjustColors()) {
|
||||
qCWarning(lcApp) << "Hue/Saturation cannot open here";
|
||||
return;
|
||||
}
|
||||
// A pending gradient lands first, as beginLevels waits.
|
||||
if (m_gradientEdit) {
|
||||
commitGradient([this] { beginHueSaturation(); });
|
||||
return;
|
||||
}
|
||||
commitTransform();
|
||||
const ImageLayer layer = activeLayer().value();
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
try {
|
||||
m_hueSaturation.emplace(layer.id, layer.asset.value(), current ? std::optional(current->clip(m_document->size())) : std::nullopt,
|
||||
layer.transform);
|
||||
} catch (const ExportError &error) {
|
||||
setBrushError(QString::fromUtf8(error.what()));
|
||||
return;
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::updateHueSaturation(const HueSaturationSettings &settings, bool preview)
|
||||
{
|
||||
if (!m_hueSaturation)
|
||||
return;
|
||||
HueSaturationEdit &edit = *m_hueSaturation;
|
||||
edit.settings = settings;
|
||||
edit.preview = preview;
|
||||
// An adjustment layer takes the settings; its pixels stay.
|
||||
if (previewAdjustmentEditing(preview)) {
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
if (!preview || settings.isIdentity()) {
|
||||
// Swift cancels the running preview: its result drops.
|
||||
m_hueSaturationPreviewFor = QUuid();
|
||||
m_hueSaturationPending.reset();
|
||||
edit.setPreview(std::nullopt);
|
||||
++m_brushRevision;
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
m_hueSaturationPending = HueSaturationJob{edit.previewSource, settings, edit.selection, edit.previewPixelToDocument, false};
|
||||
renderPendingHuePreview();
|
||||
notify();
|
||||
}
|
||||
|
||||
// Renders the newest job; one that arrives mid-render waits.
|
||||
void EditorSession::renderPendingHuePreview()
|
||||
{
|
||||
if (!m_hueSaturation || !m_hueSaturationPending || m_hueSaturationRendering)
|
||||
return;
|
||||
const HueSaturationJob job = *std::exchange(m_hueSaturationPending, std::nullopt);
|
||||
m_hueSaturationPreviewFor = m_hueSaturation->id;
|
||||
m_hueSaturationRendering = true;
|
||||
m_hueSaturationTask.setFuture(QtConcurrent::run([job] { return adjusted(job); }));
|
||||
}
|
||||
|
||||
void EditorSession::finishHueSaturationPreview()
|
||||
{
|
||||
m_hueSaturationRendering = false;
|
||||
const Adjusted made = m_hueSaturationTask.result();
|
||||
// Swift's adjustedPixels shows a failure whichever edit asked.
|
||||
if (made.failure)
|
||||
setBrushError(made.failure);
|
||||
if (m_hueSaturation && m_hueSaturation->id == std::exchange(m_hueSaturationPreviewFor, QUuid()) && made.pixels) {
|
||||
m_hueSaturation->setPreview(made.pixels->image);
|
||||
++m_brushRevision;
|
||||
notify();
|
||||
}
|
||||
renderPendingHuePreview();
|
||||
}
|
||||
|
||||
void EditorSession::commitHueSaturation(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 (finishAdjustmentEditing(true) || !m_hueSaturation || m_committingHueSaturation) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
m_hueSampleMode.reset();
|
||||
m_hueTargeting = false;
|
||||
m_hueTargetDrag.reset();
|
||||
m_hueSaturationPending.reset();
|
||||
m_hueSaturationPreviewFor = QUuid();
|
||||
const HueSaturationEdit &edit = *m_hueSaturation;
|
||||
if (edit.settings.isIdentity()) {
|
||||
m_hueSaturation.reset();
|
||||
++m_brushRevision;
|
||||
notify();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// The preview stays on screen until the pixels land.
|
||||
m_committingHueSaturation.emplace(CommittingHue{edit, std::move(done)});
|
||||
setIsProjectBusy(true);
|
||||
// A painted layer flattens there, where a failure is caught.
|
||||
m_hueSaturationCommit.setFuture(QtConcurrent::run([original = edit.original, settings = edit.settings, selection = edit.selection,
|
||||
mapping = edit.pixelToDocument]() -> Adjusted {
|
||||
try {
|
||||
return adjusted(HueSaturationJob{original.image(), settings, selection, mapping, true});
|
||||
} catch (const ExportError &error) {
|
||||
return {std::nullopt, QString::fromUtf8(error.what())};
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void EditorSession::finishHueSaturationCommit()
|
||||
{
|
||||
const Adjusted made = m_hueSaturationCommit.result();
|
||||
const CommittingHue committing = std::exchange(m_committingHueSaturation, std::nullopt).value();
|
||||
const HueSaturationEdit &edit = committing.edit;
|
||||
const int index = m_document ? indexOf(m_document->layers, edit.layerID) : -1;
|
||||
if (made.failure)
|
||||
setBrushError(made.failure);
|
||||
else if (index >= 0 && m_document->layers[index].asset && m_document->layers[index].asset->identity() == edit.original.identity()) {
|
||||
beginEdit(QStringLiteral("Hue/Saturation"));
|
||||
ImageLayer &layer = m_document->layers[index];
|
||||
layer.asset = ImportedImage(made.pixels->image, made.pixels->thumbnail.value_or(made.pixels->image), layer.name);
|
||||
// Swift rebuilds the layer without shape, effects and text.
|
||||
layer.shape = std::nullopt;
|
||||
layer.effects = std::nullopt;
|
||||
layer.text = std::nullopt;
|
||||
endEdit();
|
||||
}
|
||||
// Swift's defers: the project frees, then the edit goes.
|
||||
setIsProjectBusy(false);
|
||||
m_hueSaturation.reset();
|
||||
++m_brushRevision;
|
||||
notify();
|
||||
if (committing.done)
|
||||
QMetaObject::invokeMethod(this, committing.done, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
std::optional<double> EditorSession::sampledHue(QPointF point) const
|
||||
{
|
||||
const std::optional<PaletteColor> color = sampleCompositeColor(point);
|
||||
if (!color)
|
||||
return std::nullopt;
|
||||
const PickerHSB hsb(*color);
|
||||
return hsb.saturation > 0.02 ? std::optional(hsb.hue) : std::nullopt;
|
||||
}
|
||||
|
||||
void EditorSession::sampleHueRange(QPointF point)
|
||||
{
|
||||
if (!m_hueSaturation || !m_hueSampleMode)
|
||||
return;
|
||||
HueSaturationSettings settings = m_hueSaturation->settings;
|
||||
const std::optional<double> hue = settings.range != ColorRange::master && !settings.colorize ? sampledHue(point) : std::nullopt;
|
||||
// Swift beeps: Master, Colorize, or a gray pixel.
|
||||
if (!hue) {
|
||||
qCWarning(lcApp) << "no hue to sample here";
|
||||
return;
|
||||
}
|
||||
HueBand band = settings.band();
|
||||
if (*m_hueSampleMode == HueSampleMode::replace)
|
||||
band = band.centered(*hue);
|
||||
else if (*m_hueSampleMode == HueSampleMode::add)
|
||||
band.include(*hue);
|
||||
else
|
||||
band.exclude(*hue);
|
||||
settings.setBand(band);
|
||||
updateHueSaturation(settings, m_hueSaturation->preview);
|
||||
}
|
||||
|
||||
bool EditorSession::beginHueTargeting(QPointF point)
|
||||
{
|
||||
const std::optional<double> hue = m_hueSaturation && m_hueTargeting && !m_hueSaturation->settings.colorize ? sampledHue(point) : std::nullopt;
|
||||
// Swift beeps.
|
||||
if (!hue) {
|
||||
qCWarning(lcApp) << "no colour to target here";
|
||||
return false;
|
||||
}
|
||||
HueSaturationSettings settings = m_hueSaturation->settings;
|
||||
// Swift's max(by:): the first of the strongest claims.
|
||||
ColorRange range = colorRanges.front();
|
||||
for (const ColorRange each : colorRanges) {
|
||||
if (settings.weight(each, *hue) > settings.weight(range, *hue))
|
||||
range = each;
|
||||
}
|
||||
settings.range = range;
|
||||
const RangeAdjustment adjustment = settings.adjustments.contains(range) ? settings.adjustments.at(range) : RangeAdjustment();
|
||||
m_hueTargetDrag = HueTargetDrag{range, adjustment.hue, adjustment.saturation};
|
||||
updateHueSaturation(settings, m_hueSaturation->preview);
|
||||
return true;
|
||||
}
|
||||
|
||||
void EditorSession::dragHueTargeting(double viewDelta, bool adjustsHue)
|
||||
{
|
||||
if (!m_hueSaturation || !m_hueTargetDrag)
|
||||
return;
|
||||
HueSaturationSettings settings = m_hueSaturation->settings;
|
||||
const HueTargetDrag drag = *m_hueTargetDrag;
|
||||
RangeAdjustment &adjustment = settings.adjustments[drag.range];
|
||||
if (adjustsHue)
|
||||
adjustment.hue = std::min(180.0, std::max(-180.0, drag.hue + viewDelta / 2));
|
||||
else
|
||||
adjustment.saturation = std::min(100.0, std::max(-100.0, drag.saturation + viewDelta / 2));
|
||||
updateHueSaturation(settings, m_hueSaturation->preview);
|
||||
}
|
||||
|
||||
void EditorSession::endHueTargeting()
|
||||
{
|
||||
m_hueTargetDrag.reset();
|
||||
}
|
||||
|
||||
void EditorSession::cancelHueSaturation()
|
||||
{
|
||||
if (finishAdjustmentEditing(false))
|
||||
return;
|
||||
const bool armed = m_hueSampleMode || m_hueTargeting;
|
||||
m_hueSampleMode.reset();
|
||||
m_hueTargeting = false;
|
||||
m_hueTargetDrag.reset();
|
||||
if (!m_hueSaturation) {
|
||||
if (armed)
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
// A running preview drops: its edit is gone.
|
||||
m_hueSaturationPending.reset();
|
||||
m_hueSaturation.reset();
|
||||
++m_brushRevision;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setHueSampleMode(std::optional<HueSampleMode> mode)
|
||||
{
|
||||
if (m_hueSampleMode == mode)
|
||||
return;
|
||||
m_hueSampleMode = mode;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setHueTargeting(bool targeting)
|
||||
{
|
||||
if (m_hueTargeting == targeting)
|
||||
return;
|
||||
m_hueTargeting = targeting;
|
||||
notify();
|
||||
}
|
||||
Reference in new issue
Block a user