OmaPhoto 1.0.0: the Linux port of Compositor
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s

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:
雷电芽衣andClaude committed 2026-09-26 00:44:52 -04:00
commit c864304695
564 files changed
+100804

No files matched your search

+181
View File
@@ -0,0 +1,181 @@
#include "UI/CurvesControls.h"
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QVBoxLayout>
#include <algorithm>
#include <cmath>
// The curve over its grid; presses and drags edit points.
class CurveGraph : public QWidget {
public:
explicit CurveGraph(CurvesControls &controls) : QWidget(&controls), m_controls(controls) { setFixedHeight(260); }
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.fillRect(rect(), QColor(0, 0, 0, 89));
const CurvesSettings settings = m_controls.m_value();
const auto position = [&](double x, double y) { return QPointF(x / 255 * width(), (1 - y / 255) * height()); };
QPainterPath grid;
for (int index = 0; index <= 4; ++index) {
const double f = index / 4.0;
grid.moveTo(f * width(), 0);
grid.lineTo(f * width(), height());
grid.moveTo(0, f * height());
grid.lineTo(width(), f * height());
}
painter.setRenderHint(QPainter::Antialiasing);
painter.strokePath(grid, QPen(QColor(255, 255, 255, 31), 1));
QPainterPath line;
for (int x = 0; x <= 255; ++x) {
const QPointF point = position(x, settings.value(x, size_t(settings.channel)));
if (x == 0)
line.moveTo(point);
else
line.lineTo(point);
}
painter.strokePath(line, QPen(Qt::white, 2));
const std::vector<CurvePoint> &points = settings.channels.at(size_t(settings.channel));
for (size_t index = 0; index < points.size(); ++index) {
const QPointF at = position(points[index].x, points[index].y);
QPainterPath dot;
dot.addEllipse(QRectF(at.x() - 4, at.y() - 4, 8, 8));
painter.fillPath(dot, m_controls.m_selected == index ? palette().color(QPalette::Highlight) : QColor(Qt::white));
}
}
void mousePressEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton)
m_controls.drag(event->position(), size());
}
void mouseMoveEvent(QMouseEvent *event) override
{
if (event->buttons().testFlag(Qt::LeftButton))
m_controls.drag(event->position(), size());
}
void mouseReleaseEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton)
m_controls.m_dragging.reset();
}
private:
CurvesControls &m_controls;
};
CurvesControls::CurvesControls(std::function<CurvesSettings()> value, std::function<void(const CurvesSettings &)> change, QWidget *parent)
: QWidget(parent), m_value(std::move(value)), m_change(std::move(change)), m_channel(new QComboBox(this)), m_graph(new CurveGraph(*this)),
m_readout(new QLabel(this)), m_remove(new QPushButton(QStringLiteral("Remove point"), this)),
m_reset(new QPushButton(QStringLiteral("Reset curve"), this))
{
for (const LevelsChannel channel : allLevelsChannels)
m_channel->addItem(rawValue(channel));
connect(m_channel, &QComboBox::activated, this, [this](int index) {
CurvesSettings settings = m_value();
settings.channel = allLevelsChannels[size_t(index)];
m_change(settings);
synchronize();
});
m_graph->setObjectName(QStringLiteral("curvesGraph"));
m_readout->setObjectName(QStringLiteral("curvesReadout"));
auto *title = new QLabel(QStringLiteral("Channel"), this);
title->setBuddy(m_channel);
auto *picker = new QHBoxLayout;
picker->addWidget(title);
picker->addWidget(m_channel, 1);
auto *hint = new QLabel(QStringLiteral("Click to add a point. Drag to adjust."), this);
QFont small = hint->font();
small.setPixelSize(10);
hint->setFont(small);
hint->setForegroundRole(QPalette::PlaceholderText);
m_remove->setAutoDefault(false);
m_reset->setAutoDefault(false);
connect(m_remove, &QPushButton::clicked, this, [this] {
std::vector<CurvePoint> points = this->points();
// Swift's guard: an inner point that is still there.
if (!m_selected || *m_selected == 0 || *m_selected >= points.size() - 1)
return;
points.erase(points.begin() + qsizetype(*m_selected));
m_selected.reset();
setPoints(points);
});
connect(m_reset, &QPushButton::clicked, this, [this] {
m_selected.reset();
setPoints({{0, 0}, {255, 255}});
});
auto *column = new QVBoxLayout(this);
column->setContentsMargins(0, 0, 0, 0);
column->setSpacing(12);
column->addLayout(picker);
column->addWidget(m_graph);
column->addWidget(hint);
auto *selection = new QHBoxLayout;
selection->addWidget(m_readout);
selection->addStretch(1);
selection->addWidget(m_remove);
column->addLayout(selection);
column->addWidget(m_reset, 0, Qt::AlignLeft);
synchronize();
}
std::vector<CurvePoint> CurvesControls::points() const
{
const CurvesSettings settings = m_value();
return settings.channels.at(size_t(settings.channel));
}
void CurvesControls::setPoints(const std::vector<CurvePoint> &points)
{
CurvesSettings settings = m_value();
settings.channels.at(size_t(settings.channel)) = points;
m_change(settings);
synchronize();
}
void CurvesControls::drag(QPointF at, QSizeF size)
{
const double x = std::min(255.0, std::max(0.0, at.x() / size.width() * 255));
const double y = std::min(255.0, std::max(0.0, 255 - at.y() / size.height() * 255));
std::vector<CurvePoint> p = points();
if (!m_dragging) {
const auto distance = [&](const CurvePoint &point) { return std::hypot(point.x - x, point.y - y); };
const auto nearest = std::min_element(p.begin(), p.end(), [&](const CurvePoint &a, const CurvePoint &b) { return distance(a) < distance(b); });
if (distance(*nearest) < 14) {
m_dragging = size_t(nearest - p.begin());
} else if (p.size() < 32 && x > 1 && x < 254 && std::all_of(p.begin(), p.end(), [x](const CurvePoint &point) { return std::abs(point.x - x) > 1; })) {
p.push_back({x, y});
std::sort(p.begin(), p.end(), [](const CurvePoint &a, const CurvePoint &b) { return a.x < b.x; });
m_dragging = size_t(std::find_if(p.begin(), p.end(), [x](const CurvePoint &point) { return point.x == x; }) - p.begin());
}
}
if (!m_dragging || *m_dragging >= p.size())
return;
const size_t i = *m_dragging;
m_selected = i;
p[i].y = y;
if (i > 0 && i < p.size() - 1)
p[i].x = std::min(p[i + 1].x - 1, std::max(p[i - 1].x + 1, x));
setPoints(p);
}
void CurvesControls::synchronize()
{
const CurvesSettings settings = m_value();
// Another channel drops the selection, as Swift's onChange.
if (m_shownChannel != settings.channel) {
m_shownChannel = settings.channel;
m_selected.reset();
m_dragging.reset();
}
m_channel->setCurrentIndex(int(settings.channel));
const std::vector<CurvePoint> &points = settings.channels.at(size_t(settings.channel));
const bool shown = m_selected && *m_selected < points.size();
m_readout->setVisible(shown);
if (shown)
m_readout->setText(QStringLiteral("Input %1 · Output %2").arg(int(points[*m_selected].x)).arg(int(points[*m_selected].y)));
m_remove->setEnabled(m_selected && *m_selected != 0 && *m_selected != points.size() - 1);
m_graph->update();
}