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

+282
View File
@@ -0,0 +1,282 @@
#include "UI/ProjectTabs.h"
#include "IO/ImageFileDrop.h"
#include <QApplication>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QEvent>
#include <QHBoxLayout>
#include <QPainter>
#include <QScrollBar>
namespace {
QFont tabFont(bool active)
{
QFont font;
font.setPixelSize(12);
font.setWeight(active ? QFont::DemiBold : QFont::Medium);
return font;
}
}
ProjectTabButton::ProjectTabButton(ProjectWorkspace &workspace, std::shared_ptr<ProjectTab> tab, QWidget *parent)
: QWidget(parent), tab(std::move(tab)), m_workspace(workspace), m_select(new QToolButton(this)), m_close(new QToolButton(this))
{
// 28 high: the strip's 34 less its margins.
m_select->setObjectName(QStringLiteral("selectTab"));
m_select->setAutoRaise(true);
m_select->setFocusPolicy(Qt::NoFocus);
m_select->setMinimumWidth(35);
m_select->setMaximumWidth(155);
m_close->setObjectName(QStringLiteral("closeTab"));
m_close->setAutoRaise(true);
m_close->setFocusPolicy(Qt::NoFocus);
m_close->setText(QStringLiteral("×"));
m_close->setFixedSize(16, 28);
auto *row = new QHBoxLayout(this);
row->setContentsMargins(11, 0, 5, 0);
row->setSpacing(0);
row->addWidget(m_select);
row->addWidget(m_close);
setAcceptDrops(true);
connect(m_select, &QToolButton::clicked, this, [this] { m_workspace.select(this->tab->id); });
connect(m_close, &QToolButton::clicked, this, [this] { m_workspace.close(this->tab->id); });
// The strip syncs every button on the workspace's news.
connect(&this->tab->session, &EditorSession::changed, this, &ProjectTabButton::synchronize);
synchronize();
}
void ProjectTabButton::synchronize()
{
const bool active = m_workspace.selectedID() == tab->id;
// Swift's dot for unsaved changes stands before the title.
m_select->setText((tab->session.isModified() ? QStringLiteral("● ") : QString()) + tab->title());
m_select->setFont(tabFont(active));
m_select->setToolTip(tab->title());
m_select->setEnabled(m_workspace.canSwitch() || active);
m_close->setToolTip(QStringLiteral("Close %1").arg(tab->title()));
m_close->setAccessibleName(QStringLiteral("Close %1").arg(tab->title()));
m_close->setEnabled(m_workspace.canSwitch());
// The capsule repaints with the buttons it holds.
setProperty("active", active);
}
// Another tab's layer, unless Alt duplicates it, or files.
bool ProjectTabButton::acceptsDrop(const QMimeData &data) const
{
const bool layer = data.hasFormat(ProjectWorkspace::layerType);
if ((layer && QApplication::keyboardModifiers().testFlag(Qt::AltModifier)) || !(layer || ImageFileDrop::holdsImages(data)))
return false;
return m_workspace.canSwitch() && m_workspace.canReceiveDrag(data, tab->id);
}
void ProjectTabButton::setTargeted(bool targeted)
{
m_targeted = targeted;
m_select->setToolTip(targeted ? QStringLiteral("Add to %1").arg(tab->title()) : tab->title());
QWidget::update();
}
void ProjectTabButton::dragEnterEvent(QDragEnterEvent *event)
{
if (!acceptsDrop(*event->mimeData()))
return;
event->setDropAction(Qt::CopyAction);
event->accept();
setTargeted(true);
}
void ProjectTabButton::dragMoveEvent(QDragMoveEvent *event)
{
if (!acceptsDrop(*event->mimeData())) {
event->ignore();
return;
}
event->setDropAction(Qt::CopyAction);
event->accept();
}
void ProjectTabButton::dragLeaveEvent(QDragLeaveEvent *)
{
setTargeted(false);
}
void ProjectTabButton::dropEvent(QDropEvent *event)
{
setTargeted(false);
if (!acceptsDrop(*event->mimeData()))
return;
event->setDropAction(Qt::CopyAction);
event->accept();
m_workspace.receiveProviders(*event->mimeData(), tab->id);
}
bool ProjectTabButton::isActive() const
{
return property("active").toBool();
}
void ProjectTabButton::paintEvent(QPaintEvent *)
{
// A capsule: brighter under the front tab.
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const bool active = property("active").toBool();
QColor fill = palette().color(QPalette::WindowText), edge = fill;
fill.setAlphaF(active ? 0.12 : 0.035);
edge.setAlphaF(active ? 0.22 : 0.08);
// A drop target takes the accent, filled and rimmed.
if (m_targeted) {
fill = palette().color(QPalette::Highlight);
fill.setAlphaF(0.3);
edge = palette().color(QPalette::Highlight);
}
painter.setPen(QPen(edge, m_targeted ? 2 : 1));
painter.setBrush(fill);
const double inset = m_targeted ? 1 : 0.5;
painter.drawRoundedRect(QRectF(inset, inset, width() - 2 * inset, height() - 2 * inset), 14, 14);
}
NewCanvasButton::NewCanvasButton(ProjectWorkspace &workspace, QWidget *parent) : QToolButton(parent), m_workspace(workspace)
{
setObjectName(QStringLiteral("newCanvasButton"));
setAcceptDrops(true);
}
// Any tab's layer or files, for a fresh tab.
bool NewCanvasButton::acceptsDrop(const QMimeData &data) const
{
const bool layer = data.hasFormat(ProjectWorkspace::layerType);
if ((layer && QApplication::keyboardModifiers().testFlag(Qt::AltModifier)) || !(layer || ImageFileDrop::holdsImages(data)))
return false;
return m_workspace.canSwitch() && m_workspace.canReceiveDrag(data, std::nullopt);
}
void NewCanvasButton::setTargeted(bool targeted)
{
m_targeted = targeted;
setToolTip(targeted ? QStringLiteral("Open in a new project tab") : defaultAction()->toolTip());
update();
}
void NewCanvasButton::paintEvent(QPaintEvent *event)
{
QToolButton::paintEvent(event);
if (!m_targeted)
return;
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(QPen(palette().color(QPalette::Highlight), 2));
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(QRectF(rect()).adjusted(1, 1, -1, -1), 6, 6);
}
void NewCanvasButton::dragEnterEvent(QDragEnterEvent *event)
{
if (!acceptsDrop(*event->mimeData()))
return;
event->setDropAction(Qt::CopyAction);
event->accept();
setTargeted(true);
}
void NewCanvasButton::dragMoveEvent(QDragMoveEvent *event)
{
if (!acceptsDrop(*event->mimeData())) {
event->ignore();
return;
}
event->setDropAction(Qt::CopyAction);
event->accept();
}
void NewCanvasButton::dragLeaveEvent(QDragLeaveEvent *)
{
setTargeted(false);
}
void NewCanvasButton::dropEvent(QDropEvent *event)
{
setTargeted(false);
if (!acceptsDrop(*event->mimeData()))
return;
event->setDropAction(Qt::CopyAction);
event->accept();
m_workspace.receiveProviders(*event->mimeData(), std::nullopt);
}
ProjectTabStrip::ProjectTabStrip(ProjectWorkspace &workspace, QWidget *parent)
: QScrollArea(parent), m_workspace(workspace), m_row(new QWidget(this))
{
setObjectName(QStringLiteral("projectTabs"));
setAccessibleName(QStringLiteral("Project tabs"));
setFixedHeight(34);
setFrameShape(QFrame::NoFrame);
// The toolbar shows through; a stylesheet would freeze the palette.
setAutoFillBackground(false);
viewport()->setAutoFillBackground(false);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
auto *row = new QHBoxLayout(m_row);
row->setContentsMargins(0, 3, 0, 3);
row->setSpacing(6);
row->addStretch(1);
setWidget(m_row);
// setWidget turns the row's fill on; the toolbar shows through.
m_row->setAutoFillBackground(false);
m_row->installEventFilter(this);
connect(&m_workspace, &ProjectWorkspace::changed, this, &ProjectTabStrip::synchronize);
synchronize();
}
QList<ProjectTabButton *> ProjectTabStrip::buttons() const
{
QList<ProjectTabButton *> result;
for (int index = 0; index < m_row->layout()->count(); ++index) {
if (auto *button = qobject_cast<ProjectTabButton *>(m_row->layout()->itemAt(index)->widget()))
result << button;
}
return result;
}
void ProjectTabStrip::synchronize()
{
// Buttons follow the tabs: kept where the tab stays.
auto *row = static_cast<QHBoxLayout *>(m_row->layout());
QList<ProjectTabButton *> shown = buttons();
int position = 0;
for (const std::shared_ptr<ProjectTab> &tab : m_workspace.tabs()) {
const auto kept = std::find_if(shown.begin(), shown.end(), [&](ProjectTabButton *button) { return button->tab == tab; });
ProjectTabButton *button = kept == shown.end() ? new ProjectTabButton(m_workspace, tab, m_row) : *kept;
if (kept != shown.end())
shown.erase(kept);
row->removeWidget(button);
row->insertWidget(position, button);
button->show();
button->synchronize();
position += 1;
}
for (ProjectTabButton *gone : shown)
delete gone;
// The row is as wide as its tabs, never squeezed.
m_row->resize(m_row->sizeHint());
showFront();
}
bool ProjectTabStrip::eventFilter(QObject *watched, QEvent *event)
{
// A title changed size: the row takes its new width.
if (event->type() == QEvent::LayoutRequest) {
m_row->resize(m_row->sizeHint());
showFront();
}
return QScrollArea::eventFilter(watched, event);
}
// The front tab scrolls into view.
void ProjectTabStrip::showFront()
{
for (ProjectTabButton *button : buttons()) {
if (button->tab->id == m_workspace.selectedID())
ensureWidgetVisible(button, 0, 0);
}
}