12.2a: folders duplicate with their contents, folder opacity, format version 8, View > Snap (Compositor 1.1.5–1.1.6)
This commit is contained in:
1 parent
ee34c338d4
commit
f1b2c65c1e
52 files changed
+909
-239
No files matched your search
@@ -168,6 +168,10 @@ SnapGuides EditorSession::transformSnapTargets(const QSet<QUuid> &moving) const
|
||||
// `draft` nudged onto a nearby edge or middle, within `tolerance`.
|
||||
LayerTransform EditorSession::snappedMove(const LayerTransform &draft, const QSet<QUuid> &moving, double tolerance)
|
||||
{
|
||||
if (!m_snappingEnabled) {
|
||||
snapGuides = {};
|
||||
return draft;
|
||||
}
|
||||
const SnapGuides targets = transformSnapTargets(moving);
|
||||
const TransformSnap::Offset snap = TransformSnap::offset(boxAround(draft), targets.xs, targets.ys, tolerance);
|
||||
snapGuides = {snap.x ? std::vector<double>{*snap.x} : std::vector<double>{}, snap.y ? std::vector<double>{*snap.y} : std::vector<double>{}};
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#include "Document/TypeTool.h"
|
||||
#include "IO/ImageImporter.h"
|
||||
#include <QString>
|
||||
#include <QHash>
|
||||
#include <QUuid>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
@@ -40,6 +42,8 @@ struct ImageLayer {
|
||||
QPointF origin() const { return transform.origin; }
|
||||
QSizeF size() const { return transform.size; }
|
||||
ProjectLayerRecord hierarchyRecord() const;
|
||||
// Drawn opacity, folders included (LayerOpacity).
|
||||
double effectiveOpacity(const std::map<QUuid, ImageLayer> &byID) const;
|
||||
LayerTransform maskTransform() const;
|
||||
// The shape this layer still is: none once repainted otherwise.
|
||||
std::optional<LayerShape> liveShape() const;
|
||||
@@ -70,6 +74,7 @@ struct CanvasDocument {
|
||||
static std::optional<int> validDimension(const QString &value);
|
||||
std::vector<LayerHierarchy::Entry> hierarchyEntries() const;
|
||||
QSet<QUuid> effectiveVisibleIDs() const;
|
||||
QHash<QUuid, double> effectiveOpacities() const;
|
||||
std::vector<ImageLayer> renderLayers() const;
|
||||
friend bool operator==(const CanvasDocument &lhs, const CanvasDocument &rhs) = default;
|
||||
};
|
||||
|
||||
@@ -100,7 +100,7 @@ void EditorSession::setLocksTransformRatio(bool locks)
|
||||
notify();
|
||||
}
|
||||
|
||||
// A folder has no pixels to copy: it just moves.
|
||||
// Alt-drag copies selected roots with their contents.
|
||||
void EditorSession::beginDuplicateTransform()
|
||||
{
|
||||
if (m_transformDuplicate || !m_activeLayerID)
|
||||
@@ -110,10 +110,14 @@ void EditorSession::beginDuplicateTransform()
|
||||
if (!canTransform())
|
||||
return;
|
||||
const QSet<QUuid> selection = m_selectedLayerIDs;
|
||||
// A selected folder carries its selected contents along.
|
||||
QSet<QUuid> carried;
|
||||
for (const QUuid &id : selection)
|
||||
carried.unite(descendantIDs(id));
|
||||
// Bottom to top: the copies keep their order.
|
||||
std::vector<QUuid> targets;
|
||||
for (const ImageLayer &layer : m_document->layers) {
|
||||
if (selection.contains(layer.id) && !layer.isGroup)
|
||||
if (selection.contains(layer.id) && !carried.contains(layer.id))
|
||||
targets.push_back(layer.id);
|
||||
}
|
||||
if (targets.empty())
|
||||
@@ -123,7 +127,14 @@ void EditorSession::beginDuplicateTransform()
|
||||
for (const QUuid &id : targets) {
|
||||
selectLayer(id);
|
||||
duplicateActiveLayer();
|
||||
copies.push_back(m_activeLayerID.value());
|
||||
if (m_activeLayerID != id)
|
||||
copies.push_back(m_activeLayerID.value());
|
||||
}
|
||||
// Past the layer limit nothing was copied.
|
||||
if (copies.empty()) {
|
||||
endEdit();
|
||||
selectLayers(selection, primary);
|
||||
return;
|
||||
}
|
||||
m_transformDuplicate = TransformDuplicate{copies, selection, primary};
|
||||
selectLayers(QSet<QUuid>(copies.begin(), copies.end()), copies.back());
|
||||
@@ -206,9 +217,11 @@ void EditorSession::cancelTransform()
|
||||
if (m_transformDuplicate) {
|
||||
// The copies go and the old selection returns.
|
||||
const TransformDuplicate duplicate = *m_transformDuplicate;
|
||||
std::erase_if(m_document->layers, [&](const ImageLayer &layer) {
|
||||
return std::find(duplicate.copies.begin(), duplicate.copies.end(), layer.id) != duplicate.copies.end();
|
||||
});
|
||||
QSet<QUuid> removed(duplicate.copies.begin(), duplicate.copies.end());
|
||||
for (const QUuid © : duplicate.copies)
|
||||
removed.unite(descendantIDs(copy));
|
||||
std::erase_if(m_document->layers, [&](const ImageLayer &layer) { return removed.contains(layer.id); });
|
||||
m_collapsedGroupIDs.subtract(removed);
|
||||
selectLayers(duplicate.source, duplicate.primary);
|
||||
endDuplicateTransform();
|
||||
}
|
||||
|
||||
@@ -318,6 +318,14 @@ void EditorSession::setShowsPixelGrid(bool shows)
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSnappingEnabled(bool enabled)
|
||||
{
|
||||
m_snappingEnabled = enabled;
|
||||
if (!enabled)
|
||||
snapGuides = {};
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setCropRect(std::optional<QRectF> rect)
|
||||
{
|
||||
if (std::exchange(m_cropRect, rect) != rect)
|
||||
|
||||
@@ -238,6 +238,9 @@ public:
|
||||
// The View menu's grid, shown from 800%.
|
||||
bool showsPixelGrid() const { return m_showsPixelGrid; }
|
||||
void setShowsPixelGrid(bool shows);
|
||||
// View > Snap: moves and crops line up with edges.
|
||||
bool snappingEnabled() const { return m_snappingEnabled; }
|
||||
void setSnappingEnabled(bool enabled);
|
||||
// The Eyedropper's ring: the sampled colour over the original.
|
||||
bool showsSampleRing() const { return m_showsSampleRing; }
|
||||
void setShowsSampleRing(bool shows);
|
||||
@@ -336,6 +339,7 @@ private:
|
||||
std::optional<QUuid> m_renamingLayerID;
|
||||
std::optional<QString> m_projectPath;
|
||||
bool m_showsPixelGrid = true;
|
||||
bool m_snappingEnabled = true;
|
||||
bool m_showsSampleRing = true;
|
||||
std::optional<QRectF> m_cropRect;
|
||||
QString m_cropRatioChoice = QStringLiteral("Free");
|
||||
|
||||
@@ -3,6 +3,8 @@ public:
|
||||
LayerBlendMode displayedBlendMode(const ImageLayer &layer) const;
|
||||
void previewBlendMode(std::optional<LayerBlendMode> mode, std::optional<QUuid> id);
|
||||
bool canEditAppearance() const;
|
||||
// A folder dims what it holds; blending stays per layer.
|
||||
bool canEditOpacity() const;
|
||||
// A slider drag: many values, one undo step.
|
||||
void beginOpacityEdit();
|
||||
void finishOpacityEdit();
|
||||
|
||||
@@ -54,9 +54,14 @@ bool EditorSession::canEditAppearance() const
|
||||
return canEditLayers() && m_selectedLayerIDs.size() == 1 && active && !active->isGroup;
|
||||
}
|
||||
|
||||
bool EditorSession::canEditOpacity() const
|
||||
{
|
||||
return canEditLayers() && m_selectedLayerIDs.size() == 1 && activeLayer();
|
||||
}
|
||||
|
||||
void EditorSession::beginOpacityEdit()
|
||||
{
|
||||
if (!canEditAppearance() || m_opacityEditLayerID)
|
||||
if (!canEditOpacity() || m_opacityEditLayerID)
|
||||
return;
|
||||
beginEdit(QStringLiteral("Layer Opacity"));
|
||||
m_opacityEditLayerID = m_activeLayerID;
|
||||
@@ -72,7 +77,7 @@ void EditorSession::finishOpacityEdit()
|
||||
|
||||
void EditorSession::setLayerOpacity(double opacity)
|
||||
{
|
||||
if (!std::isfinite(opacity) || !canEditAppearance())
|
||||
if (!std::isfinite(opacity) || !canEditOpacity())
|
||||
return;
|
||||
const QUuid id = m_opacityEditLayerID.value_or(*m_activeLayerID);
|
||||
const auto layer = std::find_if(m_document->layers.begin(), m_document->layers.end(), [&](const ImageLayer &each) { return each.id == id; });
|
||||
@@ -88,9 +93,9 @@ void EditorSession::setSelectedLayersOpacity(double opacity)
|
||||
{
|
||||
if (!std::isfinite(opacity) || !canEditLayers())
|
||||
return;
|
||||
// Folders have no opacity of their own.
|
||||
// A selected folder takes it too, dimming what it holds.
|
||||
const double value = std::clamp(opacity, 0.0, 1.0);
|
||||
const auto changes = [&](const ImageLayer &layer) { return m_selectedLayerIDs.contains(layer.id) && !layer.isGroup && layer.opacity != value; };
|
||||
const auto changes = [&](const ImageLayer &layer) { return m_selectedLayerIDs.contains(layer.id) && layer.opacity != value; };
|
||||
if (std::none_of(m_document->layers.begin(), m_document->layers.end(), changes))
|
||||
return;
|
||||
finishOpacityEdit();
|
||||
|
||||
@@ -86,6 +86,54 @@ void LayerHierarchy::validate(const std::vector<ProjectLayerRecord> &layers)
|
||||
}
|
||||
}
|
||||
|
||||
double LayerOpacity::effective(double own, std::optional<QUuid> parent, const Node &folder)
|
||||
{
|
||||
double opacity = own;
|
||||
std::optional<QUuid> id = parent;
|
||||
// Past 64 folders the tree is wrong: Swift stops there.
|
||||
for (int depth = 0; id && depth < 64; ++depth) {
|
||||
const std::optional<std::pair<double, std::optional<QUuid>>> node = folder(*id);
|
||||
if (!node)
|
||||
break;
|
||||
opacity *= node->first;
|
||||
id = node->second;
|
||||
}
|
||||
return opacity;
|
||||
}
|
||||
|
||||
double ImageLayer::effectiveOpacity(const std::map<QUuid, ImageLayer> &byID) const
|
||||
{
|
||||
return LayerOpacity::effective(opacity, parentID, [&byID](QUuid id) -> std::optional<std::pair<double, std::optional<QUuid>>> {
|
||||
if (!byID.contains(id))
|
||||
return std::nullopt;
|
||||
const ImageLayer &node = byID.at(id);
|
||||
return std::pair(node.opacity, node.parentID);
|
||||
});
|
||||
}
|
||||
|
||||
double ProjectLayerRecord::effectiveOpacity(const std::map<QUuid, ProjectLayerRecord> &byID) const
|
||||
{
|
||||
return LayerOpacity::effective(opacity.value_or(1), parentID, [&byID](QUuid id) -> std::optional<std::pair<double, std::optional<QUuid>>> {
|
||||
if (!byID.contains(id))
|
||||
return std::nullopt;
|
||||
const ProjectLayerRecord &node = byID.at(id);
|
||||
return std::pair(node.opacity.value_or(1), node.parentID);
|
||||
});
|
||||
}
|
||||
|
||||
QHash<QUuid, double> CanvasDocument::effectiveOpacities() const
|
||||
{
|
||||
std::map<QUuid, ImageLayer> byID;
|
||||
for (const ImageLayer &layer : layers) {
|
||||
if (!byID.emplace(layer.id, layer).second)
|
||||
throw std::logic_error("two layers share an id");
|
||||
}
|
||||
QHash<QUuid, double> result;
|
||||
for (const ImageLayer &layer : layers)
|
||||
result.insert(layer.id, layer.effectiveOpacity(byID));
|
||||
return result;
|
||||
}
|
||||
|
||||
ProjectLayerRecord ImageLayer::hierarchyRecord() const
|
||||
{
|
||||
const QString file = id.toString(QUuid::WithoutBraces).toUpper();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "IO/ProjectStore.h"
|
||||
#include <QSet>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace LayerHierarchy {
|
||||
@@ -15,3 +16,10 @@ std::vector<Entry> entries(const std::vector<ProjectLayerRecord> &layers, bool t
|
||||
std::vector<ProjectLayerRecord> visibleLayers(const std::vector<ProjectLayerRecord> &layers);
|
||||
void validate(const std::vector<ProjectLayerRecord> &layers);
|
||||
}
|
||||
|
||||
// A folder's opacity multiplies into each layer inside it.
|
||||
namespace LayerOpacity {
|
||||
// A layer's opacity and parent, or none when unknown.
|
||||
using Node = std::function<std::optional<std::pair<double, std::optional<QUuid>>>(QUuid)>;
|
||||
double effective(double own, std::optional<QUuid> parent, const Node &folder);
|
||||
}
|
||||
@@ -42,14 +42,14 @@ void EditorSession::drawLiveComposite(const CanvasDocument &document, QPainter &
|
||||
if (const auto effects = LayerEffectsRenderer::cached(image, mask, layer.effects)) {
|
||||
const LayerTransform grown = LayerEffectsRenderer::placed(transform, effects->image, effects->inset);
|
||||
LayerRenderer::draw(effects->image, grown, grown.center(), target,
|
||||
{.opacity = layer.opacity, .blendMode = displayedBlendMode(layer), .clip = clip});
|
||||
{.opacity = layer.effectiveOpacity(records), .blendMode = displayedBlendMode(layer), .clip = clip});
|
||||
return;
|
||||
}
|
||||
LayerRenderer::draw(image, transform, transform.center(), target,
|
||||
{.opacity = layer.opacity, .blendMode = displayedBlendMode(layer), .mask = mask.value_or(QImage()), .clip = clip});
|
||||
{.opacity = layer.effectiveOpacity(records), .blendMode = displayedBlendMode(layer), .mask = mask.value_or(QImage()), .clip = clip});
|
||||
});
|
||||
live.adjustment = [&](QUuid id) { return records.contains(id) ? records.at(id).adjustment : std::nullopt; };
|
||||
live.adjustmentOpacity = [&](QUuid id) { return records.at(id).opacity; };
|
||||
live.adjustmentOpacity = [&](QUuid id) { return records.at(id).effectiveOpacity(records); };
|
||||
live.adjustmentClip = [&](QUuid id, const QPainter &painter, QImage &coverage) {
|
||||
const ImageLayer &layer = records.at(id);
|
||||
if (const std::optional<QImage> image = layer.mask ? layer.mask->enabledImage() : std::nullopt)
|
||||
@@ -116,6 +116,14 @@ std::optional<ImportedImage> LiveMaskBaker::bake(const ProjectSnapshot &snapshot
|
||||
throw std::logic_error("a snapshot to bake repeats a layer id");
|
||||
records.insert(layer.id, &layer);
|
||||
}
|
||||
// A source dims with its folders, as exported.
|
||||
const auto folded = [&records](const ProjectLayerRecord &layer) {
|
||||
return LayerOpacity::effective(layer.opacity.value_or(1), layer.parentID, [&records](QUuid id) -> std::optional<std::pair<double, std::optional<QUuid>>> {
|
||||
if (!records.contains(id))
|
||||
return std::nullopt;
|
||||
return std::pair(records.value(id)->opacity.value_or(1), records.value(id)->parentID);
|
||||
});
|
||||
};
|
||||
LiveMaskRenderer live([&](QUuid id) { return records.contains(id) ? records.value(id)->maskSourceID : std::nullopt; },
|
||||
[&](QUuid id, QPainter &context, const QImage &clip) {
|
||||
// `at` is checked: end() would read wild memory.
|
||||
@@ -135,7 +143,7 @@ std::optional<ImportedImage> LiveMaskBaker::bake(const ProjectSnapshot &snapshot
|
||||
context.save();
|
||||
context.setTransform(inverse, true);
|
||||
LayerRenderer::draw(pixels, layer.transform, layer.transform.center(), context,
|
||||
{.opacity = layer.opacity.value_or(1), .mask = shown.value_or(QImage()), .clip = clip});
|
||||
{.opacity = folded(layer), .mask = shown.value_or(QImage()), .clip = clip});
|
||||
context.restore();
|
||||
});
|
||||
{
|
||||
|
||||
@@ -317,28 +317,54 @@ QString EditorSession::nextLayerName() const
|
||||
void EditorSession::duplicateActiveLayer()
|
||||
{
|
||||
const int index = canEditLayers() ? indexOf(m_document->layers, m_activeLayerID) : -1;
|
||||
if (index < 0 || m_document->layers[index].isGroup)
|
||||
if (index < 0)
|
||||
return;
|
||||
ImageLayer copy = m_document->layers[index];
|
||||
copy.id = QUuid::createUuid();
|
||||
copy.name = copy.name + QStringLiteral(" copy");
|
||||
// A folder carries its whole tree, links kept inside it.
|
||||
const QUuid id = *m_activeLayerID;
|
||||
QSet<QUuid> included = descendantIDs(id);
|
||||
included.insert(id);
|
||||
std::vector<ImageLayer> copies;
|
||||
for (const ImageLayer &layer : m_document->layers) {
|
||||
if (included.contains(layer.id))
|
||||
copies.push_back(layer);
|
||||
}
|
||||
if (m_document->layers.size() + copies.size() > 10'000)
|
||||
return;
|
||||
QHash<QUuid, QUuid> mapping;
|
||||
for (const ImageLayer © : copies)
|
||||
mapping.insert(copy.id, QUuid::createUuid());
|
||||
const auto renamed = [&mapping](std::optional<QUuid> &link) {
|
||||
if (link && mapping.contains(*link))
|
||||
link = mapping.value(*link);
|
||||
};
|
||||
for (ImageLayer © : copies) {
|
||||
if (copy.id == id)
|
||||
copy.name += QStringLiteral(" copy");
|
||||
copy.id = mapping.value(copy.id);
|
||||
renamed(copy.parentID);
|
||||
renamed(copy.maskSourceID);
|
||||
}
|
||||
beginEdit(QStringLiteral("Duplicate Layer"));
|
||||
m_document->layers.insert(m_document->layers.begin() + index + 1, copy);
|
||||
setActiveLayerID(copy.id);
|
||||
m_document->layers.insert(m_document->layers.begin() + index + 1, copies.begin(), copies.end());
|
||||
for (auto original = mapping.cbegin(); original != mapping.cend(); ++original) {
|
||||
if (m_collapsedGroupIDs.contains(original.key()))
|
||||
m_collapsedGroupIDs.insert(original.value());
|
||||
}
|
||||
setActiveLayerID(mapping.value(id));
|
||||
endEdit();
|
||||
}
|
||||
|
||||
// Alt-drag in the Layers panel: a copy placed where dropped.
|
||||
bool EditorSession::duplicateLayer(QUuid id, std::optional<QUuid> parent, std::optional<QUuid> above, bool atBottom)
|
||||
{
|
||||
const int index = canEditLayers() ? indexOf(m_document->layers, id) : -1;
|
||||
if (index < 0 || m_document->layers[index].isGroup || !canPlaceLayer(id, parent))
|
||||
// canPlaceLayer reads canEditLayers, Swift's first term.
|
||||
if (!canPlaceLayer(id, parent))
|
||||
return false;
|
||||
beginEdit(QStringLiteral("Duplicate Layer"));
|
||||
selectLayer(id);
|
||||
duplicateActiveLayer();
|
||||
// The copy is active: nothing above refuses it.
|
||||
const bool placed = placeLayer(*m_activeLayerID, parent, above, atBottom);
|
||||
// Past the layer limit no copy was made.
|
||||
const bool placed = m_activeLayerID != id && placeLayer(*m_activeLayerID, parent, above, atBottom);
|
||||
endEdit();
|
||||
return placed;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ try {
|
||||
const std::optional<LayerMask> mask = snapshot.mask(layer);
|
||||
const std::optional<QImage> shown =
|
||||
mask ? mask->clipImage(mask->placement, layer.transform, pixels.width(), pixels.height()) : std::nullopt;
|
||||
const double opacity = layer.opacity.value_or(1);
|
||||
const double opacity = layer.effectiveOpacity(records);
|
||||
const LayerBlendMode mode = layer.blendMode.value_or(LayerBlendMode::normal);
|
||||
// With effects the mask is in their image already.
|
||||
if (const auto effects = LayerEffectsRenderer::cached(pixels, shown, layer.effects)) {
|
||||
@@ -87,7 +87,7 @@ try {
|
||||
{.opacity = opacity, .blendMode = mode, .mask = shown.value_or(QImage()), .clip = clip});
|
||||
});
|
||||
live.adjustment = [&](QUuid id) { return records.contains(id) ? records.at(id).adjustment : std::nullopt; };
|
||||
live.adjustmentOpacity = [&](QUuid id) { return records.at(id).opacity.value_or(1); };
|
||||
live.adjustmentOpacity = [&](QUuid id) { return records.at(id).effectiveOpacity(records); };
|
||||
live.adjustmentClip = [&](QUuid id, const QPainter &context, QImage &coverage) {
|
||||
const ProjectLayerRecord &layer = records.at(id);
|
||||
const std::optional<LayerMask> mask = snapshot.mask(layer);
|
||||
|
||||
@@ -42,7 +42,7 @@ void validate(const ProjectManifest &manifest)
|
||||
{
|
||||
if (manifest.format != QLatin1String("com.compositor.project"))
|
||||
throw ProjectError(ProjectError::Kind::invalid);
|
||||
if (manifest.version < 1 || manifest.version > 7)
|
||||
if (manifest.version < 1 || manifest.version > 8)
|
||||
throw ProjectError::unsupportedVersion(manifest.version);
|
||||
if (manifest.colorSpace != QLatin1String("sRGB"))
|
||||
throw ProjectError(ProjectError::Kind::invalid);
|
||||
@@ -64,9 +64,11 @@ void validate(const ProjectManifest &manifest)
|
||||
|| (manifest.version >= (group ? 6 : 4) && *layer.maskFile == uuidString(layer.id) + QLatin1String(".mask.png"));
|
||||
const bool placementAllowed = !layer.maskPlacement || (layer.maskPlacement->isValid() && layer.maskFile);
|
||||
const double opacity = layer.opacity.value_or(1);
|
||||
const bool plain = opacity == 1 && layer.blendMode.value_or(LayerBlendMode::normal) == LayerBlendMode::normal;
|
||||
const bool normal = layer.blendMode.value_or(LayerBlendMode::normal) == LayerBlendMode::normal;
|
||||
const bool plain = opacity == 1 && normal;
|
||||
// Folders dim from version 8; they always pass through.
|
||||
if (!maskAllowed || (layer.maskEnabled && !layer.maskFile) || !placementAllowed || !std::isfinite(opacity) || opacity < 0
|
||||
|| opacity > 1 || (manifest.version < 3 && !plain) || (group && !plain))
|
||||
|| opacity > 1 || (manifest.version < 3 && !plain) || (group && !(normal && (manifest.version >= 8 || opacity == 1))))
|
||||
throw ProjectError(ProjectError::Kind::invalid);
|
||||
}
|
||||
LayerHierarchy::validate(manifest.layers);
|
||||
@@ -228,7 +230,7 @@ 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–7.").arg(version));
|
||||
QStringLiteral("This project uses format version %1. This app supports versions 1–8.").arg(version));
|
||||
}
|
||||
|
||||
ProjectError::ProjectError(Kind kind, std::optional<qint64> version, const QString &description)
|
||||
@@ -304,7 +306,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 > 7)
|
||||
if (version < 1 || version > 8)
|
||||
throw ProjectError::unsupportedVersion(version);
|
||||
const ProjectManifest manifest = ProjectManifest::decoded(metadata);
|
||||
validate(manifest);
|
||||
|
||||
@@ -40,6 +40,9 @@ struct ProjectLayerRecord {
|
||||
// What the layer draws round itself; its pixels stay.
|
||||
std::optional<LayerEffects> effects = std::nullopt;
|
||||
std::optional<LayerTextStyle> text = std::nullopt;
|
||||
|
||||
// Drawn opacity, folders included; defined in LayerGroups.cpp.
|
||||
double effectiveOpacity(const std::map<QUuid, ProjectLayerRecord> &byID) const;
|
||||
};
|
||||
|
||||
// A UUID as Swift writes it: upper case, no braces.
|
||||
@@ -48,7 +51,7 @@ QString uuidString(const QUuid &id);
|
||||
struct ProjectManifest {
|
||||
QString format = QStringLiteral("com.compositor.project");
|
||||
// Swift's Int: a wild number must reach validation whole.
|
||||
qint64 version = 7;
|
||||
qint64 version = 8;
|
||||
QString colorSpace = QStringLiteral("sRGB");
|
||||
// Version-1 projects carry none: 72 pixels per inch.
|
||||
std::optional<double> resolution = std::nullopt;
|
||||
|
||||
@@ -81,7 +81,7 @@ void CanvasView::dragCrop(QPointF point, Qt::KeyboardModifiers modifiers)
|
||||
const std::optional<double> ratio = m_session.cropRatio();
|
||||
const CropDrag &drag = m_cropDrag.value();
|
||||
QRectF next = drag.updated(pixel, ratio, symmetric);
|
||||
if (!modifiers.testFlag(Qt::ControlModifier))
|
||||
if (m_session.snappingEnabled() && !modifiers.testFlag(Qt::ControlModifier))
|
||||
next = m_cropSnap.value().apply(next, drag, pixel, ratio, symmetric);
|
||||
if (CropGeometry::valid(next))
|
||||
m_session.setCropRect(next);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "Rendering/EditorCanvas.h"
|
||||
#include <QPainterPath>
|
||||
#include <cmath>
|
||||
|
||||
// One-screen-pixel lines on document pixel boundaries, over the image only.
|
||||
void CanvasView::drawPixelGrid(const QRectF &view, const CanvasDocument &document, QPainter &context) const
|
||||
{
|
||||
const CanvasViewport &viewport = m_session.viewport;
|
||||
const double points = viewport.pointsPerPixel();
|
||||
const QRectF canvas(viewport.viewPoint(QPointF(0, 0), document.size()), QSizeF(document.width * points, document.height * points));
|
||||
const QRectF area = view.intersected(canvas);
|
||||
if (area.isEmpty())
|
||||
return;
|
||||
const QPointF first = viewport.documentPoint(area.topLeft(), document.size());
|
||||
const QPointF last = viewport.documentPoint(area.bottomRight(), document.size());
|
||||
const double hairline = 1 / viewport.backingScale;
|
||||
// Crossings fill once, as CoreGraphics fills the union.
|
||||
QPainterPath path;
|
||||
path.setFillRule(Qt::WindingFill);
|
||||
for (int column = int(std::ceil(first.x())); column <= int(std::floor(last.x())); ++column) {
|
||||
const double x = viewport.viewPoint(QPointF(column, 0), document.size()).x();
|
||||
path.addRect(QRectF(x - hairline / 2, area.top(), hairline, area.height()));
|
||||
}
|
||||
for (int row = int(std::ceil(first.y())); row <= int(std::floor(last.y())); ++row) {
|
||||
const double y = viewport.viewPoint(QPointF(0, row), document.size()).y();
|
||||
path.addRect(QRectF(area.left(), y - hairline / 2, area.width(), hairline));
|
||||
}
|
||||
context.save();
|
||||
context.setRenderHint(QPainter::Antialiasing, true);
|
||||
context.fillPath(path, QColor::fromRgbF(0.55, 0.55, 0.55, 0.45));
|
||||
context.restore();
|
||||
}
|
||||
@@ -81,6 +81,8 @@ CanvasView::DisplayState CanvasView::displayState() const
|
||||
[](const ImageLayer &layer) { return layer.maskSourceID.has_value(); });
|
||||
const std::vector<ImageLayer> rendered = liveMasks ? std::vector<ImageLayer>() : document->renderLayers();
|
||||
const QSet<QUuid> visible = document->effectiveVisibleIDs();
|
||||
// A folder's opacity reaches the canvas through its layers.
|
||||
const QHash<QUuid, double> opacities = document->effectiveOpacities();
|
||||
for (const ImageLayer &layer : liveMasks ? document->layers : rendered) {
|
||||
// A blank layer counts while a draft sits on it.
|
||||
if (!layer.asset && !layer.adjustment && !(m_session.shapeDraft() && layer.id == m_session.activeLayerID()))
|
||||
@@ -88,7 +90,7 @@ CanvasView::DisplayState CanvasView::displayState() const
|
||||
state.layers.push_back({.id = layer.id, .transform = m_session.displayedTransform(layer),
|
||||
.imageID = layer.asset ? std::optional(layer.asset->identity()) : std::nullopt,
|
||||
.maskID = enabledMaskID(layer.mask), .maskSourceID = layer.maskSourceID, .parentID = layer.parentID,
|
||||
.visible = visible.contains(layer.id), .opacity = layer.opacity,
|
||||
.visible = visible.contains(layer.id), .opacity = opacities.value(layer.id),
|
||||
.blendMode = m_session.displayedBlendMode(layer), .maskPlacement = m_session.displayedMaskPlacement(layer),
|
||||
.adjustment = layer.adjustment, .effects = layer.effects});
|
||||
}
|
||||
@@ -308,6 +310,8 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
|
||||
if (!byID.contains(id) || (m_session.textDraft() && m_session.textDraft().value().layerID == id))
|
||||
return;
|
||||
const ImageLayer &layer = byID.at(id);
|
||||
// Its folders dim it with everything inside them.
|
||||
const double opacity = layer.effectiveOpacity(byID);
|
||||
// A stroke or pixel move stands in for the pixels.
|
||||
const PixelMove *move = m_session.pixelMove();
|
||||
const std::optional<GradientEdit> &gradient = m_session.gradientEdit();
|
||||
@@ -323,7 +327,7 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
|
||||
const std::optional<QImage> mask = layer.mask ? layer.mask->clipImage(layer.maskTransform(), canvas, warp->width, warp->height, 2048)
|
||||
: std::nullopt;
|
||||
LayerRenderer::draw(warp->image(), canvas, center(canvas.center()), target,
|
||||
{.scale = scale, .opacity = layer.opacity, .blendMode = m_session.displayedBlendMode(layer),
|
||||
{.scale = scale, .opacity = opacity, .blendMode = m_session.displayedBlendMode(layer),
|
||||
.mask = mask.value_or(QImage()), .clip = clip});
|
||||
return;
|
||||
}
|
||||
@@ -344,12 +348,12 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
|
||||
const QSize size = owner.asset ? owner.asset->size() : QSize(int(std::round(base.size.width())), int(std::round(base.size.height())));
|
||||
mask = layer.mask->clipImage(placement, base, size.width(), size.height(), m_session.transformEdit() ? std::min(2048.0, steady) : steady);
|
||||
}
|
||||
const LayerRenderer::Options options{.scale = scale, .opacity = layer.opacity, .blendMode = m_session.displayedBlendMode(layer),
|
||||
const LayerRenderer::Options options{.scale = scale, .opacity = opacity, .blendMode = m_session.displayedBlendMode(layer),
|
||||
.mask = mask.value_or(QImage()), .clip = clip};
|
||||
// Swift asks twice; once, with what shows, is one request.
|
||||
const std::optional<EffectsPreviewCache::Result> effects =
|
||||
stroke ? std::nullopt : m_session.effectsPreviews.preview(layer, mask, transform, placement, repaint);
|
||||
const LayerRenderer::Options bare{.scale = scale, .opacity = layer.opacity, .blendMode = options.blendMode, .clip = clip};
|
||||
const LayerRenderer::Options bare{.scale = scale, .opacity = opacity, .blendMode = options.blendMode, .clip = clip};
|
||||
// A pending distortion warps the effects along with the layer.
|
||||
if (const auto warped = effects ? m_session.distortedEffects(layer, effects->image, effects->inset) : std::nullopt) {
|
||||
LayerRenderer::draw(warped->image, warped->transform, center(warped->transform.center()), target, bare);
|
||||
@@ -409,7 +413,7 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
|
||||
};
|
||||
LiveMaskRenderer live([&](QUuid id) { return byID.contains(id) ? byID.at(id).maskSourceID : std::nullopt; }, drawOwnWithDraft);
|
||||
live.adjustment = [&](QUuid id) { return byID.contains(id) ? byID.at(id).adjustment : std::nullopt; };
|
||||
live.adjustmentOpacity = [&](QUuid id) { return byID.at(id).opacity; };
|
||||
live.adjustmentOpacity = [&](QUuid id) { return byID.at(id).effectiveOpacity(byID); };
|
||||
live.adjustmentClip = [&](QUuid id, const QPainter &painter, QImage &coverage) {
|
||||
const ImageLayer &layer = byID.at(id);
|
||||
if (!layer.mask || !layer.mask->isEnabled)
|
||||
@@ -468,32 +472,3 @@ void CanvasView::drawDocumentPixels(const QRectF &view, const QRectF &pixels, co
|
||||
context.drawImage(target, raster);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
// One-screen-pixel lines on document pixel boundaries, over the image only.
|
||||
void CanvasView::drawPixelGrid(const QRectF &view, const CanvasDocument &document, QPainter &context) const
|
||||
{
|
||||
const CanvasViewport &viewport = m_session.viewport;
|
||||
const double points = viewport.pointsPerPixel();
|
||||
const QRectF canvas(viewport.viewPoint(QPointF(0, 0), document.size()), QSizeF(document.width * points, document.height * points));
|
||||
const QRectF area = view.intersected(canvas);
|
||||
if (area.isEmpty())
|
||||
return;
|
||||
const QPointF first = viewport.documentPoint(area.topLeft(), document.size());
|
||||
const QPointF last = viewport.documentPoint(area.bottomRight(), document.size());
|
||||
const double hairline = 1 / viewport.backingScale;
|
||||
// Crossings fill once, as CoreGraphics fills the union.
|
||||
QPainterPath path;
|
||||
path.setFillRule(Qt::WindingFill);
|
||||
for (int column = int(std::ceil(first.x())); column <= int(std::floor(last.x())); ++column) {
|
||||
const double x = viewport.viewPoint(QPointF(column, 0), document.size()).x();
|
||||
path.addRect(QRectF(x - hairline / 2, area.top(), hairline, area.height()));
|
||||
}
|
||||
for (int row = int(std::ceil(first.y())); row <= int(std::floor(last.y())); ++row) {
|
||||
const double y = viewport.viewPoint(QPointF(0, row), document.size()).y();
|
||||
path.addRect(QRectF(area.left(), y - hairline / 2, area.width(), hairline));
|
||||
}
|
||||
context.save();
|
||||
context.setRenderHint(QPainter::Antialiasing, true);
|
||||
context.fillPath(path, QColor::fromRgbF(0.55, 0.55, 0.55, 0.45));
|
||||
context.restore();
|
||||
}
|
||||
@@ -348,8 +348,7 @@ void TransformOverlay::drawTransformHandles(QPainter &context, const QPalette &p
|
||||
context.save();
|
||||
context.setRenderHint(QPainter::Antialiasing, true);
|
||||
context.setBrush(Qt::NoBrush);
|
||||
context.setPen(QPen(QColor(0, 0, 0, 179), 3));
|
||||
context.drawPath(path);
|
||||
// The accent alone: a dark line read as a halo.
|
||||
context.setPen(QPen(accent, 1));
|
||||
context.drawPath(path);
|
||||
context.setBrush(Qt::white);
|
||||
|
||||
@@ -118,6 +118,8 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
|
||||
add(view, QStringLiteral("zoomOut"), QStringLiteral("Zoom Out"), QKeySequence(Qt::CTRL | Qt::Key_Minus), [this] { session().zoom(session().viewport.zoom() / 1.25); });
|
||||
add(view, QStringLiteral("pixelGrid"), QStringLiteral("Pixel Grid (800% and above)"), QKeySequence(), [this] { session().setShowsPixelGrid(!session().showsPixelGrid()); })
|
||||
->setCheckable(true);
|
||||
add(view, QStringLiteral("snap"), QStringLiteral("Snap"), QKeySequence(), [this] { session().setSnappingEnabled(!session().snappingEnabled()); })
|
||||
->setCheckable(true);
|
||||
// Ctrl+H: the Move tool's box; the desktop hides apps.
|
||||
add(view, QStringLiteral("transformControls"), QStringLiteral("Show Transform Controls"), QKeySequence(Qt::CTRL | Qt::Key_H),
|
||||
[this] { session().setShowsTransformControls(!session().showsTransformControls()); })
|
||||
@@ -275,6 +277,7 @@ void CompositorMenus::synchronize()
|
||||
for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"})
|
||||
action(QString::fromLatin1(name))->setEnabled(drawn);
|
||||
action(QStringLiteral("pixelGrid"))->setChecked(s.showsPixelGrid());
|
||||
action(QStringLiteral("snap"))->setChecked(s.snappingEnabled());
|
||||
action(QStringLiteral("transformControls"))->setChecked(s.showsTransformControls());
|
||||
action(QStringLiteral("transformControls"))->setEnabled(s.tool() == NavigationTool::move && s.document().has_value());
|
||||
const bool selected = s.selection().has_value();
|
||||
|
||||
+11
-10
@@ -53,7 +53,7 @@ EffectsSheet::EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget
|
||||
header(QStringLiteral("Stroke"), true);
|
||||
colour(true);
|
||||
slider(QStringLiteral("Size"), [](const LayerEffects &e) { return e.stroke ? std::optional(e.stroke->size) : std::nullopt; },
|
||||
[](LayerEffects &e, double value) { e.stroke->size = value; }, 1, StrokeEffect::maxSize, QStringLiteral("px"));
|
||||
[](LayerEffects &e, double value) { e.stroke->size = value; }, 0, 20, QStringLiteral("px"), StrokeEffect::maxSize);
|
||||
slider(QStringLiteral("Opacity"), [](const LayerEffects &e) { return percent(e.stroke); },
|
||||
[](LayerEffects &e, double value) { e.stroke->opacity = value / 100; }, 0, 100, QStringLiteral("%"));
|
||||
break;
|
||||
@@ -64,9 +64,9 @@ EffectsSheet::EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget
|
||||
slider(QStringLiteral("Angle"), [](const LayerEffects &e) { return e.shadow ? std::optional(e.shadow->angle) : std::nullopt; },
|
||||
[](LayerEffects &e, double value) { e.shadow->angle = value; }, -180, 180, QStringLiteral("°"));
|
||||
slider(QStringLiteral("Distance"), [](const LayerEffects &e) { return e.shadow ? std::optional(e.shadow->distance) : std::nullopt; },
|
||||
[](LayerEffects &e, double value) { e.shadow->distance = value; }, 0, 300, QStringLiteral("px"));
|
||||
[](LayerEffects &e, double value) { e.shadow->distance = value; }, 0, 100, QStringLiteral("px"), 5000);
|
||||
slider(QStringLiteral("Blur"), [](const LayerEffects &e) { return e.shadow ? std::optional(e.shadow->blur) : std::nullopt; },
|
||||
[](LayerEffects &e, double value) { e.shadow->blur = value; }, 0, 300, QStringLiteral("px"));
|
||||
[](LayerEffects &e, double value) { e.shadow->blur = value; }, 0, 100, QStringLiteral("px"), 500);
|
||||
break;
|
||||
case LayerEffectKind::colorOverlay:
|
||||
header(QStringLiteral("Color Overlay"), false);
|
||||
@@ -80,9 +80,9 @@ EffectsSheet::EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget
|
||||
slider(QStringLiteral("Angle"), [](const LayerEffects &e) { return e.innerShadow ? std::optional(e.innerShadow->angle) : std::nullopt; },
|
||||
[](LayerEffects &e, double value) { e.innerShadow->angle = value; }, -180, 180, QStringLiteral("°"));
|
||||
slider(QStringLiteral("Distance"), [](const LayerEffects &e) { return e.innerShadow ? std::optional(e.innerShadow->distance) : std::nullopt; },
|
||||
[](LayerEffects &e, double value) { e.innerShadow->distance = value; }, 0, 300, QStringLiteral("px"));
|
||||
[](LayerEffects &e, double value) { e.innerShadow->distance = value; }, 0, 50, QStringLiteral("px"), 5000);
|
||||
slider(QStringLiteral("Blur"), [](const LayerEffects &e) { return e.innerShadow ? std::optional(e.innerShadow->blur) : std::nullopt; },
|
||||
[](LayerEffects &e, double value) { e.innerShadow->blur = value; }, 0, 300, QStringLiteral("px"));
|
||||
[](LayerEffects &e, double value) { e.innerShadow->blur = value; }, 0, 100, QStringLiteral("px"), 500);
|
||||
break;
|
||||
}
|
||||
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
|
||||
@@ -139,7 +139,7 @@ void EffectsSheet::header(const QString &title, bool position)
|
||||
// The effect's colour, opened in the app's own picker.
|
||||
void EffectsSheet::colour(bool labelled)
|
||||
{
|
||||
auto *swatch = new SwatchButton([this] { return m_session.editingEffects().color(m_kind).value_or(PaletteColor::black()); }, 3, 0, 0.5, this);
|
||||
auto *swatch = new SwatchButton([this] { return m_session.editingEffects().color(m_kind).value_or(PaletteColor::black()); }, 3, 1, 1, this);
|
||||
swatch->setObjectName(QStringLiteral("effectColor"));
|
||||
swatch->setFixedSize(36, 18);
|
||||
swatch->setToolTip(rawValue(m_kind) + QStringLiteral(" color"));
|
||||
@@ -162,7 +162,8 @@ void EffectsSheet::colour(bool labelled)
|
||||
}
|
||||
|
||||
void EffectsSheet::slider(const QString &title, std::function<std::optional<double>(const LayerEffects &)> value,
|
||||
std::function<void(LayerEffects &, double)> change, double low, double high, const QString &unit)
|
||||
std::function<void(LayerEffects &, double)> change, double low, double high, const QString &unit,
|
||||
std::optional<double> typedHigh)
|
||||
{
|
||||
const size_t index = m_sliders.size();
|
||||
const QString name = title.toLower();
|
||||
@@ -215,12 +216,12 @@ void EffectsSheet::slider(const QString &title, std::function<std::optional<doub
|
||||
layout->addLayout(suffixed);
|
||||
m_column->addWidget(row);
|
||||
m_controls.push_back(row);
|
||||
m_sliders.push_back(Slider{std::move(value), std::move(change), low, high, slider, field});
|
||||
m_sliders.push_back(Slider{std::move(value), std::move(change), low, high, typedHigh.value_or(high), slider, field});
|
||||
}
|
||||
|
||||
void EffectsSheet::apply(const Slider &control, double value)
|
||||
{
|
||||
const double clamped = std::clamp(value, control.low, control.high);
|
||||
const double clamped = std::clamp(value, control.low, control.typedHigh);
|
||||
m_session.changeEffects([&control, clamped](LayerEffects &effects) { control.change(effects, clamped); });
|
||||
}
|
||||
|
||||
@@ -244,7 +245,7 @@ void EffectsSheet::synchronize()
|
||||
if (!value)
|
||||
continue;
|
||||
{
|
||||
// The slider shows the session's number without writing back.
|
||||
// QSlider pins a larger typed value at its end.
|
||||
const QSignalBlocker quiet(control.slider);
|
||||
control.slider->setValue(int(std::lround((*value - control.low) / (control.high - control.low) * travel)));
|
||||
}
|
||||
|
||||
@@ -22,14 +22,17 @@ private:
|
||||
std::function<void(LayerEffects &, double)> change;
|
||||
double low;
|
||||
double high;
|
||||
// What a field or step may reach, past the slider.
|
||||
double typedHigh;
|
||||
QSlider *slider;
|
||||
PickerField *field;
|
||||
};
|
||||
void header(const QString &title, bool position);
|
||||
void colour(bool labelled);
|
||||
void slider(const QString &title, std::function<std::optional<double>(const LayerEffects &)> value,
|
||||
std::function<void(LayerEffects &, double)> change, double low, double high, const QString &unit);
|
||||
// A value clamped to the slider's range, as Swift's binding.
|
||||
std::function<void(LayerEffects &, double)> change, double low, double high, const QString &unit,
|
||||
std::optional<double> typedHigh = std::nullopt);
|
||||
// A value clamped to the typed range, as Swift's setAmount.
|
||||
void apply(const Slider &control, double value);
|
||||
void synchronize();
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ LayerAppearanceControls::LayerAppearanceControls(EditorSession &session, QWidget
|
||||
column->setContentsMargins(12, 12, 12, 12);
|
||||
column->setSpacing(8);
|
||||
auto *blend = new QHBoxLayout;
|
||||
blend->addWidget(caption(QStringLiteral("Blend"), this));
|
||||
m_blendCaption = caption(QStringLiteral("Blend"), this);
|
||||
blend->addWidget(m_blendCaption);
|
||||
blend->addWidget(m_picker, 1);
|
||||
column->addLayout(blend);
|
||||
auto *opacity = new QHBoxLayout;
|
||||
@@ -74,7 +75,9 @@ LayerAppearanceControls::~LayerAppearanceControls()
|
||||
|
||||
void LayerAppearanceControls::synchronize()
|
||||
{
|
||||
setEnabled(m_session.canEditAppearance());
|
||||
// A folder takes an opacity; blending rests with the picker.
|
||||
setEnabled(m_session.canEditOpacity());
|
||||
m_blendCaption->setEnabled(m_session.canEditAppearance());
|
||||
const std::optional<ImageLayer> active = m_session.activeLayer();
|
||||
const double opacity = active ? active->opacity : 1;
|
||||
m_syncing = true;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QWidget>
|
||||
|
||||
class BlendModePicker;
|
||||
class QLabel;
|
||||
|
||||
// The active layer's blend mode and opacity.
|
||||
class LayerAppearanceControls : public QWidget {
|
||||
@@ -25,6 +26,7 @@ private:
|
||||
|
||||
EditorSession &m_session;
|
||||
BlendModePicker *const m_picker;
|
||||
QLabel *m_blendCaption = nullptr;
|
||||
QSlider *const m_slider;
|
||||
QLineEdit *const m_percentage;
|
||||
// The layer the controls were last made for.
|
||||
|
||||
@@ -157,7 +157,7 @@ void LayerCell::configure(const ImageLayer &layer, bool enabled, int depth, bool
|
||||
// A pending rename closes the edits: rows never move.
|
||||
if (!m_renaming)
|
||||
m_name->setText((layer.maskSourceID ? QStringLiteral("↳ ") : QString()) + layer.name);
|
||||
m_dimensions->setText(m_editableText ? QStringLiteral("Text · Double-click to edit")
|
||||
m_dimensions->setText(m_editableText ? QStringLiteral("Text")
|
||||
: m_isAdjustment ? QStringLiteral("Adjustment · Double-click to edit")
|
||||
: layer.isGroup ? QStringLiteral("Folder")
|
||||
: QStringLiteral("%1 × %2 px").arg(std::lround(layer.size().width())).arg(std::lround(layer.size().height())));
|
||||
|
||||
@@ -127,11 +127,6 @@ std::optional<LayerDropTarget> NativeLayerList::dropTarget(const QMimeData &data
|
||||
return std::nullopt;
|
||||
// Swift duplicates when the source offers a copy alone (Alt).
|
||||
const bool copying = offered == Qt::CopyAction;
|
||||
// Folders are not duplicated: a copy with one is refused.
|
||||
for (const QUuid &id : ids) {
|
||||
if (copying && m_rows[size_t(indexOf(m_rows, id))].isGroup)
|
||||
return std::nullopt;
|
||||
}
|
||||
const QPoint inColumn = m_column->mapFrom(viewport(), viewport()->mapFrom(this, listPoint));
|
||||
int row = int(m_rows.size());
|
||||
bool onRow = false;
|
||||
@@ -326,7 +321,7 @@ QCursor NativeLayerList::cursorFor(QPoint listPoint, Qt::KeyboardModifiers modif
|
||||
if (cell.maskThumbnail().isVisible() && cell.maskThumbnail().geometry().contains(inCell))
|
||||
return editable ? CanvasView::duplicateCursor(ratio) : QCursor(Qt::ArrowCursor);
|
||||
if (!isClippingZone(cell, inCell))
|
||||
return editable && !layer.isGroup ? CanvasView::duplicateCursor(ratio) : QCursor(Qt::ArrowCursor);
|
||||
return editable ? CanvasView::duplicateCursor(ratio) : QCursor(Qt::ArrowCursor);
|
||||
if (!m_session.canToggleClippingMask(layer.id))
|
||||
return QCursor(Qt::ArrowCursor);
|
||||
return clippingCursor(layer.maskSourceID.has_value(), ratio);
|
||||
|
||||
@@ -91,7 +91,8 @@ int NativeLayerList::rowAt(QPoint listPoint) const
|
||||
|
||||
bool NativeLayerList::isClippingZone(const LayerCell &cell, QPoint cellPoint)
|
||||
{
|
||||
return cellPoint.y() >= cell.height() - 10;
|
||||
// A folder clips nothing, so it has no strip.
|
||||
return !cell.isGroup() && cellPoint.y() >= cell.height() - 10;
|
||||
}
|
||||
|
||||
bool NativeLayerList::clickRow(LayerCell &cell, Qt::KeyboardModifiers modifiers, QPoint cellPoint)
|
||||
@@ -177,7 +178,7 @@ void NativeLayerList::keyPressEvent(QKeyEvent *event)
|
||||
{Qt::Key_Z, NavigationTool::zoom}, {Qt::Key_G, NavigationTool::gradient}, {Qt::Key_L, NavigationTool::lasso}, {Qt::Key_M, NavigationTool::marquee},
|
||||
{Qt::Key_W, NavigationTool::wand}, {Qt::Key_J, NavigationTool::spotHealing}, {Qt::Key_S, NavigationTool::cloneStamp},
|
||||
{Qt::Key_U, NavigationTool::shape}, {Qt::Key_R, NavigationTool::blur}, {Qt::Key_I, NavigationTool::eyedropper},
|
||||
{Qt::Key_C, NavigationTool::crop}};
|
||||
{Qt::Key_C, NavigationTool::crop}, {Qt::Key_T, NavigationTool::type}};
|
||||
if (plain && (key == Qt::Key_Backspace || key == Qt::Key_Delete)) {
|
||||
m_session.deleteKeyPressed();
|
||||
return;
|
||||
|
||||
@@ -124,6 +124,7 @@ public:
|
||||
void beginRenaming();
|
||||
bool isOnControl(QPoint point) const;
|
||||
bool isRenaming() const { return m_renaming; }
|
||||
bool isGroup() const { return m_isGroup; }
|
||||
QUuid layerID() const { return m_layerID; }
|
||||
LayerThumbnailButton &thumbnail() { return *m_thumbnail; }
|
||||
LayerThumbnailButton &maskThumbnail() { return *m_maskThumbnail; }
|
||||
|
||||
Reference in new issue
Block a user