The format version in one place and the layer context menu (12.10a)

This commit is contained in:
雷电芽衣 committed 2026-09-28 15:28:21 -04:00
1 parent 9ac3b1b81c
commit fc076de71d
15 files changed
+700 -63

No files matched your search

+6 -3
View File
@@ -62,7 +62,7 @@ void validate(const ProjectManifest &manifest)
{
if (manifest.format != QLatin1String("com.compositor.project"))
throw ProjectError(ProjectError::Kind::invalid);
if (manifest.version < 1 || manifest.version > 9)
if (!ProjectManifest::supports(manifest.version))
throw ProjectError::unsupportedVersion(manifest.version);
if (manifest.colorSpace != QLatin1String("sRGB"))
throw ProjectError(ProjectError::Kind::invalid);
@@ -256,7 +256,10 @@ ProjectError::ProjectError(Kind kind) : ProjectError(kind, std::nullopt, descrip
ProjectError ProjectError::unsupportedVersion(qint64 version)
{
return ProjectError(Kind::version, version,
QStringLiteral("This project uses format version %1. This app supports versions 1–9.").arg(version));
QStringLiteral("This project uses format version %1. This app supports versions %2–%3.")
.arg(version)
.arg(ProjectManifest::supported.first)
.arg(ProjectManifest::supported.second));
}
ProjectError::ProjectError(Kind kind, std::optional<qint64> version, const QString &description)
@@ -332,7 +335,7 @@ ProjectSnapshot ProjectStore::load(const QString &path)
const auto [format, version] = ProjectManifest::header(metadata);
if (format != QLatin1String("com.compositor.project"))
throw ProjectError(ProjectError::Kind::invalid);
if (version < 1 || version > 9)
if (!ProjectManifest::supports(version))
throw ProjectError::unsupportedVersion(version);
const ProjectManifest manifest = ProjectManifest::decoded(metadata);
validate(manifest);
+5 -1
View File
@@ -50,9 +50,13 @@ struct ProjectLayerRecord {
QString uuidString(const QUuid &id);
struct ProjectManifest {
// Swift's current and supported: what saves write, what loads accept.
static constexpr qint64 current = 9;
static constexpr std::pair<qint64, qint64> supported{1, current};
static bool supports(qint64 version) { return version >= supported.first && version <= supported.second; }
QString format = QStringLiteral("com.compositor.project");
// Swift's Int: a wild number must reach validation whole.
qint64 version = 9;
qint64 version = current;
QString colorSpace = QStringLiteral("sRGB");
// Version-1 projects carry none: 72 pixels per inch.
std::optional<double> resolution = std::nullopt;
+2 -47
View File
@@ -57,52 +57,6 @@ LayerCell::LayerCell(NativeLayerList &list)
m_editor->setFont(name);
m_editor->hide();
m_editor->installEventFilter(this);
menuAction(QStringLiteral("renameLayer"), QStringLiteral("Rename…"), [this] {
if (!m_list.session().canEditLayers())
return;
m_list.session().setActiveLayerID(m_layerID);
m_list.session().setRenamingLayerID(m_layerID);
});
menuAction(QStringLiteral("toggleVisibility"), QStringLiteral("Hide/Show Layer"), [this] { m_list.session().toggleLayerVisibility(m_layerID); });
// With a selection, white hides it, black shows it alone.
menuAction(QStringLiteral("addWhiteMask"), QStringLiteral("Add White Mask"), [this] {
m_list.session().selectLayerTarget(m_layerID, false);
m_list.session().addMask();
});
menuAction(QStringLiteral("addBlackMask"), QStringLiteral("Add Black Mask"), [this] {
m_list.session().selectLayerTarget(m_layerID, false);
m_list.session().addMask(false);
});
menuAction(QStringLiteral("toggleMask"), QStringLiteral("Enable/Disable Mask"), [this] {
m_list.session().selectLayerTarget(m_layerID, false);
m_list.session().toggleLayerMask();
});
menuAction(QStringLiteral("deleteMask"), QStringLiteral("Delete Mask"), [this] {
m_list.session().selectLayerTarget(m_layerID, false);
m_list.session().deleteLayerMask();
});
menuAction(QStringLiteral("releaseClippingMask"), QStringLiteral("Release Clipping Mask"), [this] { m_list.session().removeLiveMask(m_layerID); });
menuAction(QStringLiteral("moveOut"), QStringLiteral("Move Out of Folder"), [this] {
m_list.session().selectLayer(m_layerID);
m_list.session().moveActiveLayerOutOfGroup();
});
menuAction(QStringLiteral("deleteLayer"), QStringLiteral("Delete Layer / Folder"), [this] {
// Part of a multi-selection, the whole selection goes.
EditorSession &session = m_list.session();
if (session.selectedLayerIDs().size() > 1 && session.selectedLayerIDs().contains(m_layerID))
session.deleteSelectedLayers();
else
session.deleteLayer(m_layerID);
});
}
QAction *LayerCell::menuAction(const QString &name, const QString &title, const std::function<void()> &run)
{
auto *action = new QAction(title, this);
action->setObjectName(name);
connect(action, &QAction::triggered, this, run);
addAction(action);
return action;
}
void LayerCell::configure(const ImageLayer &layer, bool enabled, int depth, bool visible)
@@ -315,9 +269,10 @@ void LayerCell::mouseDoubleClickEvent(QMouseEvent *event)
session.setRenamingLayerID(m_layerID);
}
// The list routes the press and builds the menu.
void LayerCell::contextMenuEvent(QContextMenuEvent *event)
{
QMenu::exec(actions(), event->globalPos(), nullptr, this);
m_list.menuFor(*this, event->pos())->exec(event->globalPos());
}
void LayerCell::beginRenaming()
+101
View File
@@ -0,0 +1,101 @@
#include "UI/NativeLayerList.h"
#include <QMenu>
// Swift's table-level context menu (1.2.5): entries and validation.
std::unique_ptr<QMenu> NativeLayerList::contextMenu(QUuid id)
{
EditorSession &session = m_session;
if (session.selectedLayerIDs().isEmpty())
session.selectLayer(id);
auto menu = std::make_unique<QMenu>(this);
// A refused selection leaves no layer: the gate rests then.
const std::optional<ImageLayer> active = session.activeLayer();
const bool editable = session.canEditLayers();
const bool mask = active && active->mask;
const auto entry = [&](QMenu &into, const char *name, const QString &title, bool enabled, std::function<void()> run) {
QAction *action = into.addAction(title, this, std::move(run));
action->setObjectName(QString::fromLatin1(name));
action->setEnabled(enabled);
};
// Swift's actions read the active layer when chosen.
const auto onActive = [&session](std::function<void(QUuid)> run) {
return [&session, run] {
if (const std::optional<QUuid> activeID = session.activeLayerID())
run(*activeID);
};
};
entry(*menu, "duplicateLayer", QStringLiteral("Duplicate Layer"), editable, [&session] { session.duplicateActiveLayer(); });
entry(*menu, "renameLayer", QStringLiteral("Rename…"), editable && session.selectedLayerIDs().size() == 1, onActive([&session](QUuid activeID) {
if (session.canEditLayers())
session.setRenamingLayerID(activeID);
}));
// A chosen mask exists: Swift's `mask != nil` is dead.
const QString deleteTitle = session.isMaskSelected() ? QStringLiteral("Delete Mask")
: session.selectedLayerIDs().size() > 1 ? QStringLiteral("Delete Selected Layers")
: QStringLiteral("Delete Layer");
entry(*menu, "deleteLayer", deleteTitle, editable, [&session] { session.deleteLayerOrMask(); });
menu->addSeparator();
entry(*menu, "clippingMask", active && active->maskSourceID ? QStringLiteral("Release Clipping Mask") : QStringLiteral("Create Clipping Mask"),
active && session.canToggleClippingMask(active->id), onActive([&session](QUuid activeID) { session.toggleClippingMask(activeID); }));
entry(*menu, "groupLayers", QStringLiteral("Group Selected Layers"),
session.canEditLayers() && session.document() && session.document()->layers.size() < 10'000 && !session.selectedLayerIDs().isEmpty(),
[&session] { session.groupSelectedLayers(); });
entry(*menu, "moveOut", QStringLiteral("Move Out of Folder"), session.canEditLayers() && active && active->parentID,
[&session] { session.moveActiveLayerOutOfGroup(); });
entry(*menu, "mergeLayers", session.mergeTitle(), session.canMergeLayers(), [&session] { session.mergeLayers(); });
menu->addSeparator();
// The mask entries target the layer's pixels first, as Swift's.
const bool masking = session.canEditMask();
const auto onPixels = [&session, onActive](std::function<void()> then) {
return onActive([&session, then](QUuid activeID) {
session.selectLayerTarget(activeID, false);
then();
});
};
QMenu *add = menu->addMenu(QStringLiteral("Add Mask"));
add->menuAction()->setObjectName(QStringLiteral("addMask"));
add->menuAction()->setEnabled(masking && !mask);
entry(*add, "addWhiteMask", QStringLiteral("Reveal All (White)"), masking && !mask, onPixels([&session] { session.addMask(true); }));
entry(*add, "addBlackMask", QStringLiteral("Hide All (Black)"), masking && !mask, onPixels([&session] { session.addMask(false); }));
entry(*menu, "toggleMask", mask && !active->mask->isEnabled ? QStringLiteral("Enable Mask") : QStringLiteral("Disable Mask"), masking && mask,
onPixels([&session] { session.toggleLayerMask(); }));
entry(*menu, "deleteMask", QStringLiteral("Delete Mask"), masking && mask, onPixels([&session] { session.deleteLayerMask(); }));
entry(*menu, "linkMask", mask && !active->mask->isLinked ? QStringLiteral("Link Mask") : QStringLiteral("Unlink Mask"),
session.canEditLayers() && mask && !active->isGroup && !active->adjustment,
onActive([&session](QUuid activeID) { session.toggleMaskLink(activeID); }));
menu->addSeparator();
entry(*menu, "layerVisibility", active && !active->isVisible ? QStringLiteral("Show Layer") : QStringLiteral("Hide Layer"), editable,
onActive([&session](QUuid activeID) { session.toggleLayerVisibility(activeID); }));
return menu;
}
// Swift's menu(for:): the press chooses what the menu acts on.
std::unique_ptr<QMenu> NativeLayerList::menuFor(LayerCell &cell, QPoint cellPoint)
{
// Swift's targetLayer: the session's calls below may rebuild the rows.
const QUuid id = cell.layerID();
// Swift's table selects no row while an effect is chosen.
const bool rowSelected = !m_session.selectedEffect() && m_session.selectedLayerIDs().contains(id);
QWidget *under = cell.childAt(cellPoint);
// On an effect's row: that effect is chosen, rows untouched.
for (QWidget *each = under; each && each != &cell; each = each->parentWidget()) {
if (auto *effect = qobject_cast<LayerEffectRow *>(each)) {
effect->select(false);
return contextMenu(id);
}
}
// Each call below drops a chosen effect itself.
const auto *thumbnail = qobject_cast<LayerThumbnailButton *>(under);
const bool maskThumbnail = thumbnail && thumbnail->isMaskTarget, layerThumbnail = thumbnail && !thumbnail->isMaskTarget;
if (rowSelected) {
if (maskThumbnail || layerThumbnail)
m_session.selectLayerTarget(id, maskThumbnail);
else
m_session.selectLayers(QSet<QUuid>(m_session.selectedLayerIDs()), id);
} else {
m_session.selectLayers({id}, id);
focusList();
m_session.selectLayerTarget(id, maskThumbnail);
}
return contextMenu(id);
}
+4 -1
View File
@@ -12,6 +12,7 @@
#include <memory>
class NativeLayerList;
class QMenu;
// A layer's eye: a press toggles, a drag swipes rows.
class EyeSwipeButton : public QToolButton {
@@ -145,7 +146,6 @@ protected:
private:
void relayout();
void endRenaming(bool keeping);
QAction *menuAction(const QString &name, const QString &title, const std::function<void()> &run);
NativeLayerList &m_list;
EyeSwipeButton *const m_eye;
@@ -226,6 +226,9 @@ public:
// Photoshop's strip along the bottom of a row.
static bool isClippingZone(const LayerCell &cell, QPoint cellPoint);
void focusList();
// Swift's contextMenu(for:) and menu(for:): entries, then the press's routing.
std::unique_ptr<QMenu> contextMenu(QUuid id);
std::unique_ptr<QMenu> menuFor(LayerCell &cell, QPoint cellPoint);
// What a drag of this row carries: its selection.
std::unique_ptr<QMimeData> dragData(const LayerCell &cell) const;
void startDrag(LayerCell &cell, Qt::KeyboardModifiers modifiers);