The format version in one place and the layer context menu (12.10a)
This commit is contained in:
1 parent
9ac3b1b81c
commit
fc076de71d
15 files changed
+700
-63
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user