Whole layers on the clipboard and several duplicates (12.10b)
This commit is contained in:
1 parent
fc076de71d
commit
7a873eb9c7
18 files changed
+769
-212
No files matched your search
@@ -116,7 +116,6 @@ void EditorSession::beginDuplicateTransform()
|
||||
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) && !carried.contains(layer.id))
|
||||
@@ -125,21 +124,17 @@ void EditorSession::beginDuplicateTransform()
|
||||
if (targets.empty())
|
||||
return;
|
||||
beginEdit(targets.size() > 1 ? QStringLiteral("Duplicate Layers") : QStringLiteral("Duplicate Layer"));
|
||||
std::vector<QUuid> copies;
|
||||
for (const QUuid &id : targets) {
|
||||
selectLayer(id);
|
||||
duplicateActiveLayer();
|
||||
if (m_activeLayerID != id)
|
||||
copies.push_back(m_activeLayerID.value());
|
||||
}
|
||||
// Stacked as Duplicate Layer stacks them.
|
||||
duplicateLayers(targets);
|
||||
const QSet<QUuid> copies = m_selectedLayerIDs - selection;
|
||||
// Past the layer limit nothing was copied.
|
||||
if (copies.empty()) {
|
||||
if (copies.isEmpty()) {
|
||||
endEdit();
|
||||
selectLayers(selection, primary);
|
||||
return;
|
||||
}
|
||||
m_transformDuplicate = TransformDuplicate{copies, selection, primary};
|
||||
selectLayers(QSet<QUuid>(copies.begin(), copies.end()), copies.back());
|
||||
m_transformDuplicate = TransformDuplicate{std::vector<QUuid>(copies.begin(), copies.end()), selection, primary};
|
||||
selectLayers(copies, m_activeLayerID);
|
||||
beginTransform(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -276,8 +276,6 @@ public:
|
||||
int canvasFocusRequest() const { return m_canvasFocusRequest; }
|
||||
void requestCanvasFocus();
|
||||
|
||||
// What Paste puts back in place, until another copy.
|
||||
const std::optional<PixelClipboard> &pixelClipboard() const { return m_pixelClipboard; }
|
||||
// What went wrong in a tool; the window shows it.
|
||||
const std::optional<QString> &brushError() const { return m_brushError; }
|
||||
void setBrushError(std::optional<QString> error);
|
||||
@@ -481,9 +479,7 @@ private:
|
||||
std::optional<LayerEffects> m_effectsEditingOriginal;
|
||||
std::optional<LayerEffectSelection> m_effectSelection;
|
||||
std::optional<PixelClipboard> m_pixelClipboard;
|
||||
// Swift's pasteboard change count, counted from the first copy.
|
||||
int m_clipboardChanges = 0;
|
||||
bool m_watchingClipboard = false;
|
||||
std::optional<CopiedLayer> m_copiedLayer;
|
||||
std::optional<LassoDraft> m_lassoDraft;
|
||||
LassoKind m_lassoKind = LassoKind::freehand;
|
||||
LassoKind m_marqueeKind = LassoKind::rectangle;
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "Document/ProjectWorkspace.h"
|
||||
#include "Document/LiveLayerMask.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Logging.h"
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrent>
|
||||
|
||||
// Several rows are copied one after another, as Swift awaits.
|
||||
void ProjectWorkspace::copyNext(std::vector<QUuid> ids, std::optional<QUuid> destination, std::optional<QPointF> point, std::function<void()> done)
|
||||
{
|
||||
if (ids.empty()) {
|
||||
finish(done);
|
||||
return;
|
||||
}
|
||||
const QUuid first = ids.front();
|
||||
ids.erase(ids.begin());
|
||||
copyLayer(first, destination, point, [self = QPointer(this), ids = std::move(ids), destination, point, done = std::move(done)]() mutable {
|
||||
if (self)
|
||||
self->copyNext(std::move(ids), destination, point, std::move(done));
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<QUuid> ProjectWorkspace::draggedLayerSource(const QMimeData &data) const
|
||||
{
|
||||
const std::vector<QUuid> ids = layerIDs(data);
|
||||
if (ids.empty())
|
||||
return std::nullopt;
|
||||
for (const std::shared_ptr<ProjectTab> &each : m_tabs) {
|
||||
if (each->session.document() && indexOf(each->session.document()->layers, ids.front()) >= 0)
|
||||
return each->id;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool ProjectWorkspace::canReceiveDrag(const QMimeData &data, std::optional<QUuid> destination) const
|
||||
{
|
||||
const std::optional<QUuid> source = draggedLayerSource(data);
|
||||
return !destination || !source || *source != *destination;
|
||||
}
|
||||
|
||||
void ProjectWorkspace::copyLayer(QUuid id, std::optional<QUuid> destination, std::optional<QPointF> point, std::function<void()> done)
|
||||
{
|
||||
copyLayers({id}, destination, point, std::move(done));
|
||||
}
|
||||
|
||||
// Several keep their places relative to each other.
|
||||
void ProjectWorkspace::copyLayers(const std::vector<QUuid> &ids, std::optional<QUuid> destination, std::optional<QPointF> point,
|
||||
std::function<void()> done)
|
||||
{
|
||||
// A null pointer aborts when read; an iterator would not.
|
||||
std::shared_ptr<ProjectTab> from;
|
||||
for (const std::shared_ptr<ProjectTab> &each : m_tabs) {
|
||||
if (!from && !ids.empty() && each->session.document() && indexOf(each->session.document()->layers, ids.front()) >= 0)
|
||||
from = each;
|
||||
}
|
||||
if (!canSwitch() || !from || !from->session.canEditLayers() || destination == std::optional(from->id)) {
|
||||
finish(done);
|
||||
return;
|
||||
}
|
||||
Copy copy{.ids = ids, .from = from, .target = nullptr, .original = from->session.document().value(), .layers = {}, .included = {}, .point = point};
|
||||
const ProjectSnapshot snapshot = copy.from->session.projectSnapshot().value();
|
||||
copy.target = destination ? tab(*destination) : tab(addTab(false).id);
|
||||
if (!copy.target || (destination && !copy.target->session.canStartProjectOperation())
|
||||
|| (copy.target->session.document() && !copy.target->session.canEditLayers())) {
|
||||
finish(done);
|
||||
return;
|
||||
}
|
||||
for (const QUuid &id : ids) {
|
||||
copy.included.insert(id);
|
||||
copy.included.unite(copy.from->session.descendantIDs(id));
|
||||
}
|
||||
const auto pixels = [](const std::vector<ImageLayer> &layers) {
|
||||
qint64 total = 0;
|
||||
for (const ImageLayer &layer : layers)
|
||||
total += layer.asset ? qint64(layer.asset->size().width()) * layer.asset->size().height() : 0;
|
||||
return total;
|
||||
};
|
||||
for (const ImageLayer &layer : copy.original.layers) {
|
||||
if (copy.included.contains(layer.id))
|
||||
copy.layers.push_back(layer);
|
||||
}
|
||||
const qint64 used = copy.target->session.document() ? pixels(copy.target->session.document()->layers) : 0;
|
||||
if (used + pixels(copy.layers) > 100'000'000) {
|
||||
copy.target->session.setImportError(QStringLiteral("The copied layers exceed this project’s 100-megapixel limit."));
|
||||
finish(done);
|
||||
return;
|
||||
}
|
||||
setManaging(true);
|
||||
copy.from->session.setIsProjectBusy(true);
|
||||
copy.target->session.setIsProjectBusy(true);
|
||||
// A mask from outside the copy goes into the pixels.
|
||||
std::vector<QUuid> outside;
|
||||
for (const ImageLayer &layer : copy.layers) {
|
||||
if (layer.maskSourceID && !copy.included.contains(*layer.maskSourceID))
|
||||
outside.push_back(layer.id);
|
||||
}
|
||||
auto *watcher = new QFutureWatcher<Baked>(this);
|
||||
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, copy, done] {
|
||||
watcher->deleteLater();
|
||||
const Baked baked = watcher->result();
|
||||
if (baked.failure) {
|
||||
qCWarning(lcApp).noquote() << "cannot copy the layers:" << *baked.failure;
|
||||
copy.target->session.setImportError(baked.failure);
|
||||
} else {
|
||||
place(copy, baked.images);
|
||||
}
|
||||
// Managing ends last: its signal sees both projects free.
|
||||
copy.from->session.setIsProjectBusy(false);
|
||||
copy.target->session.setIsProjectBusy(false);
|
||||
setManaging(false);
|
||||
finish(done);
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run([snapshot, outside]() -> Baked {
|
||||
Baked result;
|
||||
try {
|
||||
for (const QUuid layer : outside) {
|
||||
if (const std::optional<ImportedImage> image = LiveMaskBaker::bake(snapshot, layer))
|
||||
result.images.insert({layer, *image});
|
||||
}
|
||||
} catch (const ExportError &error) {
|
||||
result.failure = QString::fromUtf8(error.what());
|
||||
}
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
|
||||
void ProjectWorkspace::place(const Copy ©, const EditorSession::BakedImages &baked)
|
||||
{
|
||||
QHash<QUuid, QUuid> mapping;
|
||||
for (const ImageLayer &layer : copy.layers)
|
||||
mapping.insert(layer.id, QUuid::createUuid());
|
||||
EditorSession &into = copy.target->session;
|
||||
const QSizeF size = into.document() ? into.document()->size() : copy.original.size();
|
||||
// One layer anchors on its centre; several, on their box.
|
||||
QRectF pictured;
|
||||
for (const ImageLayer &layer : copy.layers) {
|
||||
if (!layer.isGroup)
|
||||
pictured = pictured.isNull() ? QRectF(layer.transform.origin, layer.transform.size) : pictured.united(QRectF(layer.transform.origin, layer.transform.size));
|
||||
}
|
||||
const QPointF anchor = copy.ids.size() == 1 || pictured.isNull() ? copy.layers[indexOf(copy.layers, copy.ids.front())].transform.center() : pictured.center();
|
||||
const QPointF shift = copy.point.value_or(QPointF(size.width() / 2, size.height() / 2)) - anchor;
|
||||
const auto mapped = [&](const std::optional<QUuid> &old) {
|
||||
return old && mapping.contains(*old) ? std::optional(mapping.value(*old)) : std::nullopt;
|
||||
};
|
||||
std::vector<ImageLayer> layers;
|
||||
for (ImageLayer layer : copy.layers) {
|
||||
if (baked.contains(layer.id))
|
||||
layer.asset = baked.at(layer.id);
|
||||
layer.id = mapping.value(layer.id);
|
||||
layer.transform.origin += shift;
|
||||
if (layer.mask && layer.mask->placement)
|
||||
layer.mask->placement->origin += shift;
|
||||
// A parent or mask source outside the copy lets go.
|
||||
layer.parentID = mapped(layer.parentID);
|
||||
layer.maskSourceID = mapped(layer.maskSourceID);
|
||||
layers.push_back(layer);
|
||||
}
|
||||
// Busy sessions refuse a new canvas: free it first.
|
||||
into.setIsProjectBusy(false);
|
||||
into.beginEdit(QStringLiteral("Copy Layers from Project"));
|
||||
if (!into.document())
|
||||
into.createDocument(int(size.width()), int(size.height()));
|
||||
into.m_document->layers.insert(into.m_document->layers.end(), layers.begin(), layers.end());
|
||||
into.setActiveLayerID(mapping.value(copy.ids.front()));
|
||||
into.m_selectedLayerIDs.clear();
|
||||
// A layer the source tab lacks was not copied.
|
||||
for (const QUuid &id : copy.ids) {
|
||||
if (mapping.contains(id))
|
||||
into.m_selectedLayerIDs.insert(mapping.value(id));
|
||||
}
|
||||
into.endEdit();
|
||||
// Announced by the caller, which stops managing next.
|
||||
m_selectedID = copy.target->id;
|
||||
}
|
||||
|
||||
// Ctrl+V with layers copied whole; false leaves Paste to pixels.
|
||||
bool ProjectWorkspace::pasteCopiedLayer()
|
||||
{
|
||||
std::shared_ptr<ProjectTab> source;
|
||||
for (const std::shared_ptr<ProjectTab> &each : m_tabs) {
|
||||
const std::optional<CopiedLayer> &copied = each->session.copiedLayer();
|
||||
if (!source && copied && EditorSession::clipboardHolds(copied->data))
|
||||
source = each;
|
||||
}
|
||||
if (!source || !source->session.document())
|
||||
return false;
|
||||
std::vector<QUuid> ids;
|
||||
for (const QUuid &id : source->session.copiedLayer()->ids) {
|
||||
if (indexOf(source->session.document()->layers, id) >= 0)
|
||||
ids.push_back(id);
|
||||
}
|
||||
if (ids.empty())
|
||||
return false;
|
||||
if (source->id == m_selectedID) {
|
||||
if (!source->session.canEditLayers())
|
||||
return false;
|
||||
source->session.duplicateLayers(ids, QStringLiteral("Paste"));
|
||||
return true;
|
||||
}
|
||||
copyLayers(ids, m_selectedID);
|
||||
return true;
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
#include "Document/ProjectWorkspace.h"
|
||||
#include "Document/LiveLayerMask.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "IO/ImageFileDrop.h"
|
||||
#include "Logging.h"
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QFutureWatcher>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
const QString ProjectWorkspace::layerType = QStringLiteral("com.compositor.layer-row");
|
||||
|
||||
@@ -348,152 +344,3 @@ std::vector<QUuid> ProjectWorkspace::layerIDs(const QMimeData &data)
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Several rows are copied one after another, as Swift awaits.
|
||||
void ProjectWorkspace::copyNext(std::vector<QUuid> ids, std::optional<QUuid> destination, std::optional<QPointF> point, std::function<void()> done)
|
||||
{
|
||||
if (ids.empty()) {
|
||||
finish(done);
|
||||
return;
|
||||
}
|
||||
const QUuid first = ids.front();
|
||||
ids.erase(ids.begin());
|
||||
copyLayer(first, destination, point, [self = QPointer(this), ids = std::move(ids), destination, point, done = std::move(done)]() mutable {
|
||||
if (self)
|
||||
self->copyNext(std::move(ids), destination, point, std::move(done));
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<QUuid> ProjectWorkspace::draggedLayerSource(const QMimeData &data) const
|
||||
{
|
||||
const std::vector<QUuid> ids = layerIDs(data);
|
||||
if (ids.empty())
|
||||
return std::nullopt;
|
||||
for (const std::shared_ptr<ProjectTab> &each : m_tabs) {
|
||||
if (each->session.document() && indexOf(each->session.document()->layers, ids.front()) >= 0)
|
||||
return each->id;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool ProjectWorkspace::canReceiveDrag(const QMimeData &data, std::optional<QUuid> destination) const
|
||||
{
|
||||
const std::optional<QUuid> source = draggedLayerSource(data);
|
||||
return !destination || !source || *source != *destination;
|
||||
}
|
||||
|
||||
void ProjectWorkspace::copyLayer(QUuid id, std::optional<QUuid> destination, std::optional<QPointF> point, std::function<void()> done)
|
||||
{
|
||||
// A null pointer aborts when read; an iterator would not.
|
||||
std::shared_ptr<ProjectTab> from;
|
||||
for (const std::shared_ptr<ProjectTab> &each : m_tabs) {
|
||||
if (!from && each->session.document() && indexOf(each->session.document()->layers, id) >= 0)
|
||||
from = each;
|
||||
}
|
||||
if (!canSwitch() || !from || !from->session.canEditLayers() || destination == std::optional(from->id)) {
|
||||
finish(done);
|
||||
return;
|
||||
}
|
||||
Copy copy{.id = id, .from = from, .target = nullptr, .original = from->session.document().value(), .layers = {}, .included = {}, .point = point};
|
||||
const ProjectSnapshot snapshot = copy.from->session.projectSnapshot().value();
|
||||
copy.target = destination ? tab(*destination) : tab(addTab(false).id);
|
||||
if (!copy.target || (destination && !copy.target->session.canStartProjectOperation())
|
||||
|| (copy.target->session.document() && !copy.target->session.canEditLayers())) {
|
||||
finish(done);
|
||||
return;
|
||||
}
|
||||
copy.included = copy.from->session.descendantIDs(id);
|
||||
copy.included.insert(id);
|
||||
const auto pixels = [](const std::vector<ImageLayer> &layers) {
|
||||
qint64 total = 0;
|
||||
for (const ImageLayer &layer : layers)
|
||||
total += layer.asset ? qint64(layer.asset->size().width()) * layer.asset->size().height() : 0;
|
||||
return total;
|
||||
};
|
||||
for (const ImageLayer &layer : copy.original.layers) {
|
||||
if (copy.included.contains(layer.id))
|
||||
copy.layers.push_back(layer);
|
||||
}
|
||||
const qint64 used = copy.target->session.document() ? pixels(copy.target->session.document()->layers) : 0;
|
||||
if (used + pixels(copy.layers) > 100'000'000) {
|
||||
copy.target->session.setImportError(QStringLiteral("The copied layers exceed this project’s 100-megapixel limit."));
|
||||
finish(done);
|
||||
return;
|
||||
}
|
||||
setManaging(true);
|
||||
copy.from->session.setIsProjectBusy(true);
|
||||
copy.target->session.setIsProjectBusy(true);
|
||||
// A mask from outside the copy goes into the pixels.
|
||||
std::vector<QUuid> outside;
|
||||
for (const ImageLayer &layer : copy.layers) {
|
||||
if (layer.maskSourceID && !copy.included.contains(*layer.maskSourceID))
|
||||
outside.push_back(layer.id);
|
||||
}
|
||||
auto *watcher = new QFutureWatcher<Baked>(this);
|
||||
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, copy, done] {
|
||||
watcher->deleteLater();
|
||||
const Baked baked = watcher->result();
|
||||
if (baked.failure) {
|
||||
qCWarning(lcApp).noquote() << "cannot copy the layers:" << *baked.failure;
|
||||
copy.target->session.setImportError(baked.failure);
|
||||
} else {
|
||||
place(copy, baked.images);
|
||||
}
|
||||
// Managing ends last: its signal sees both projects free.
|
||||
copy.from->session.setIsProjectBusy(false);
|
||||
copy.target->session.setIsProjectBusy(false);
|
||||
setManaging(false);
|
||||
finish(done);
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run([snapshot, outside]() -> Baked {
|
||||
Baked result;
|
||||
try {
|
||||
for (const QUuid layer : outside) {
|
||||
if (const std::optional<ImportedImage> image = LiveMaskBaker::bake(snapshot, layer))
|
||||
result.images.insert({layer, *image});
|
||||
}
|
||||
} catch (const ExportError &error) {
|
||||
result.failure = QString::fromUtf8(error.what());
|
||||
}
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
|
||||
void ProjectWorkspace::place(const Copy ©, const EditorSession::BakedImages &baked)
|
||||
{
|
||||
QHash<QUuid, QUuid> mapping;
|
||||
for (const ImageLayer &layer : copy.layers)
|
||||
mapping.insert(layer.id, QUuid::createUuid());
|
||||
EditorSession &into = copy.target->session;
|
||||
const QSizeF size = into.document() ? into.document()->size() : copy.original.size();
|
||||
const QPointF anchor = copy.layers[indexOf(copy.layers, copy.id)].transform.center();
|
||||
const QPointF shift = copy.point.value_or(QPointF(size.width() / 2, size.height() / 2)) - anchor;
|
||||
const auto mapped = [&](const std::optional<QUuid> &old) {
|
||||
return old && mapping.contains(*old) ? std::optional(mapping.value(*old)) : std::nullopt;
|
||||
};
|
||||
std::vector<ImageLayer> layers;
|
||||
for (ImageLayer layer : copy.layers) {
|
||||
if (baked.contains(layer.id))
|
||||
layer.asset = baked.at(layer.id);
|
||||
layer.id = mapping.value(layer.id);
|
||||
layer.transform.origin += shift;
|
||||
if (layer.mask && layer.mask->placement)
|
||||
layer.mask->placement->origin += shift;
|
||||
// A parent or mask source outside the copy lets go.
|
||||
layer.parentID = mapped(layer.parentID);
|
||||
layer.maskSourceID = mapped(layer.maskSourceID);
|
||||
// Swift's copy lists its fields; effects are not among them.
|
||||
layer.effects = std::nullopt;
|
||||
layers.push_back(layer);
|
||||
}
|
||||
// Busy sessions refuse a new canvas: free it first.
|
||||
into.setIsProjectBusy(false);
|
||||
into.beginEdit(QStringLiteral("Copy Layers from Project"));
|
||||
if (!into.document())
|
||||
into.createDocument(int(size.width()), int(size.height()));
|
||||
into.m_document->layers.insert(into.m_document->layers.end(), layers.begin(), layers.end());
|
||||
into.setActiveLayerID(mapping.value(copy.id));
|
||||
into.endEdit();
|
||||
// Announced by the caller, which stops managing next.
|
||||
m_selectedID = copy.target->id;
|
||||
}
|
||||
@@ -60,6 +60,11 @@ public:
|
||||
static std::optional<QUuid> layerID(const QString &text);
|
||||
void copyLayer(QUuid id, std::optional<QUuid> destination, std::optional<QPointF> point = std::nullopt,
|
||||
std::function<void()> done = {});
|
||||
// Folders with all they hold, as one step there.
|
||||
void copyLayers(const std::vector<QUuid> &ids, std::optional<QUuid> destination, std::optional<QPointF> point = std::nullopt,
|
||||
std::function<void()> done = {});
|
||||
// Ctrl+V with layers copied whole; false when none.
|
||||
bool pasteCopiedLayer();
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
@@ -70,7 +75,7 @@ private:
|
||||
std::optional<QString> failure;
|
||||
};
|
||||
struct Copy {
|
||||
QUuid id;
|
||||
std::vector<QUuid> ids;
|
||||
std::shared_ptr<ProjectTab> from, target;
|
||||
CanvasDocument original;
|
||||
std::vector<ImageLayer> layers;
|
||||
|
||||
@@ -2,11 +2,21 @@
|
||||
public:
|
||||
// Ctrl+X: copy, then clear the selected pixels.
|
||||
void cutSelection(std::function<void()> done = {});
|
||||
// A copy above the layer, then placed; never a folder.
|
||||
// What Paste puts back in place, until another copy.
|
||||
const std::optional<PixelClipboard> &pixelClipboard() const;
|
||||
// The layers Copy took whole, until another copy.
|
||||
const std::optional<CopiedLayer> &copiedLayer() const;
|
||||
// Whether the clipboard still holds this very copy.
|
||||
static bool clipboardHolds(const QPointer<QMimeData> &data);
|
||||
// Every selected layer, a folder with all it holds.
|
||||
void duplicateActiveLayer();
|
||||
// Each layer copied as one step; the copies end selected.
|
||||
void duplicateLayers(const std::vector<QUuid> &ids, const QString &editName = QStringLiteral("Duplicate Layer"));
|
||||
bool duplicateLayer(QUuid id, std::optional<QUuid> parent, std::optional<QUuid> above = std::nullopt, bool atBottom = false);
|
||||
std::optional<QRectF> selectionCopyRegion() const;
|
||||
bool canCopyPixels() const;
|
||||
// Copy with no selection takes the layers themselves.
|
||||
bool canCopyLayer() const;
|
||||
std::optional<CopiedPixels> renderSelectedPixels(const ImageLayer &layer, bool mask) const;
|
||||
bool canCopyMerged() const;
|
||||
std::optional<CopiedPixels> renderMergedPixels() const;
|
||||
@@ -22,3 +32,6 @@ public:
|
||||
|
||||
private:
|
||||
void store(const CopiedPixels &copied);
|
||||
|
||||
std::vector<QUuid> copiedLayerIDs() const;
|
||||
std::optional<QUuid> insertCopy(QUuid id);
|
||||
@@ -163,32 +163,60 @@ void EditorSession::copyMergedSelection()
|
||||
}
|
||||
}
|
||||
|
||||
bool EditorSession::canCopyLayer() const
|
||||
{
|
||||
return canEditLayers() && activeLayer() && !selection() && !m_isMaskSelected;
|
||||
}
|
||||
|
||||
// Swift's pasteboard type for a layer copied whole.
|
||||
static const QString copiedLayerType = QStringLiteral("com.compositor.copied-layer");
|
||||
|
||||
void EditorSession::copySelection()
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
if (!canCopyPixels() || !layer)
|
||||
if (!canCopyPixels() && !canCopyLayer())
|
||||
return;
|
||||
const ImageLayer layer = activeLayer().value();
|
||||
if (!canCopyPixels()) {
|
||||
auto *data = new QMimeData;
|
||||
data->setData(copiedLayerType, uuidString(layer.id).toUtf8());
|
||||
QGuiApplication::clipboard()->setMimeData(data);
|
||||
m_pixelClipboard = std::nullopt;
|
||||
m_copiedLayer = CopiedLayer{copiedLayerIDs(), data};
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const std::optional<CopiedPixels> copied = renderSelectedPixels(*layer, m_isMaskSelected);
|
||||
const std::optional<CopiedPixels> copied = renderSelectedPixels(layer, m_isMaskSelected);
|
||||
if (!copied) {
|
||||
qCWarning(lcApp) << "nothing to copy";
|
||||
return;
|
||||
}
|
||||
store(*copied);
|
||||
if (canCopyLayer())
|
||||
m_copiedLayer = CopiedLayer{copiedLayerIDs(), m_pixelClipboard.value().data};
|
||||
} catch (const ExportError &error) {
|
||||
setBrushError(QString::fromUtf8(error.what()));
|
||||
}
|
||||
}
|
||||
|
||||
const std::optional<PixelClipboard> &EditorSession::pixelClipboard() const
|
||||
{
|
||||
return m_pixelClipboard;
|
||||
}
|
||||
|
||||
const std::optional<CopiedLayer> &EditorSession::copiedLayer() const
|
||||
{
|
||||
return m_copiedLayer;
|
||||
}
|
||||
|
||||
// Swift's change count: our own object while we own it.
|
||||
bool EditorSession::clipboardHolds(const QPointer<QMimeData> &data)
|
||||
{
|
||||
return data && QGuiApplication::clipboard()->mimeData() == data.data();
|
||||
}
|
||||
|
||||
// Keeps pixels for Paste; the system clipboard gets PNG.
|
||||
void EditorSession::store(const CopiedPixels &copied)
|
||||
{
|
||||
QClipboard *clipboard = QGuiApplication::clipboard();
|
||||
// Counted from the first copy: another since makes Paste external.
|
||||
if (!m_watchingClipboard) {
|
||||
m_watchingClipboard = true;
|
||||
connect(clipboard, &QClipboard::dataChanged, this, [this] { ++m_clipboardChanges; });
|
||||
}
|
||||
auto *data = new QMimeData;
|
||||
data->setImageData(copied.image);
|
||||
QByteArray png;
|
||||
@@ -198,8 +226,9 @@ void EditorSession::store(const CopiedPixels &copied)
|
||||
data->setData(QStringLiteral("image/png"), png);
|
||||
else
|
||||
qCWarning(lcApp) << "the copied pixels could not be encoded as PNG:" << writer.errorString();
|
||||
clipboard->setMimeData(data);
|
||||
m_pixelClipboard = PixelClipboard{copied.image, copied.region.topLeft(), m_clipboardChanges};
|
||||
QGuiApplication::clipboard()->setMimeData(data);
|
||||
m_pixelClipboard = PixelClipboard{copied.image, copied.region.topLeft(), data};
|
||||
m_copiedLayer = std::nullopt;
|
||||
}
|
||||
|
||||
void EditorSession::cutSelection(std::function<void()> done)
|
||||
@@ -217,7 +246,7 @@ bool EditorSession::canPaste() const
|
||||
{
|
||||
if (!m_document || !canEditLayers())
|
||||
return false;
|
||||
if (m_pixelClipboard && m_clipboardChanges == m_pixelClipboard->changeCount)
|
||||
if (m_pixelClipboard && clipboardHolds(m_pixelClipboard->data))
|
||||
return true;
|
||||
// An empty clipboard has no data object at all.
|
||||
const QMimeData *data = clipboardData();
|
||||
@@ -229,7 +258,7 @@ void EditorSession::paste()
|
||||
{
|
||||
if (!canPaste())
|
||||
return;
|
||||
if (m_pixelClipboard && m_clipboardChanges == m_pixelClipboard->changeCount) {
|
||||
if (m_pixelClipboard && clipboardHolds(m_pixelClipboard->data)) {
|
||||
addPixelLayer(m_pixelClipboard->image, m_pixelClipboard->origin, nextLayerName(), QStringLiteral("Paste"));
|
||||
return;
|
||||
}
|
||||
@@ -253,12 +282,15 @@ void EditorSession::layerViaCopy()
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (!canEditLayers() || !layer || layer->isGroup || (current && current->isEmpty()))
|
||||
if (!canEditLayers() || !layer || (current && current->isEmpty()))
|
||||
return;
|
||||
// Without a selection it duplicates, folders included.
|
||||
if (!current) {
|
||||
duplicateActiveLayer();
|
||||
return;
|
||||
}
|
||||
if (layer->isGroup)
|
||||
return;
|
||||
try {
|
||||
const std::optional<CopiedPixels> copied = renderSelectedPixels(*layer, m_isMaskSelected);
|
||||
if (!copied) {
|
||||
@@ -314,13 +346,80 @@ QString EditorSession::nextLayerName() const
|
||||
return QStringLiteral("Layer %1").arg(number);
|
||||
}
|
||||
|
||||
// Selected layers in document order, none inside a selected folder.
|
||||
std::vector<QUuid> EditorSession::copiedLayerIDs() const
|
||||
{
|
||||
if (!m_document)
|
||||
return {};
|
||||
// The active layer is always selected: Swift's union is dead.
|
||||
const QSet<QUuid> &selected = m_selectedLayerIDs;
|
||||
QSet<QUuid> nested;
|
||||
for (const QUuid &id : selected)
|
||||
nested.unite(descendantIDs(id));
|
||||
std::vector<QUuid> ids;
|
||||
for (const ImageLayer &layer : m_document->layers) {
|
||||
if (selected.contains(layer.id) && !nested.contains(layer.id))
|
||||
ids.push_back(layer.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
void EditorSession::duplicateActiveLayer()
|
||||
{
|
||||
const int index = canEditLayers() ? indexOf(m_document->layers, m_activeLayerID) : -1;
|
||||
if (index < 0)
|
||||
duplicateLayers(copiedLayerIDs());
|
||||
}
|
||||
|
||||
// One copy sits above its original; several, above the topmost.
|
||||
void EditorSession::duplicateLayers(const std::vector<QUuid> &ids, const QString &editName)
|
||||
{
|
||||
if (!canEditLayers() || ids.empty())
|
||||
return;
|
||||
const std::optional<QUuid> active = m_activeLayerID;
|
||||
beginEdit(editName);
|
||||
QHash<QUuid, QUuid> copiesOf;
|
||||
for (const QUuid &id : ids) {
|
||||
if (const std::optional<QUuid> copy = insertCopy(id))
|
||||
copiesOf.insert(id, *copy);
|
||||
}
|
||||
if (copiesOf.isEmpty()) {
|
||||
endEdit();
|
||||
return;
|
||||
}
|
||||
if (copiesOf.size() > 1) {
|
||||
// Panel order, top first, so folders compare as seen.
|
||||
std::vector<ProjectLayerRecord> records;
|
||||
for (const ImageLayer &layer : m_document->layers)
|
||||
records.push_back(layer.hierarchyRecord());
|
||||
std::vector<QUuid> originals;
|
||||
for (const LayerHierarchy::Entry &entry : LayerHierarchy::entries(records, true)) {
|
||||
if (copiesOf.contains(entry.layer.id))
|
||||
originals.push_back(entry.layer.id);
|
||||
}
|
||||
const std::optional<QUuid> parent = m_document->layers[indexOf(m_document->layers, originals.front())].parentID;
|
||||
QUuid below = originals.front();
|
||||
for (auto original = originals.rbegin(); original != originals.rend(); ++original) {
|
||||
if (placeLayer(copiesOf.value(*original), parent, below))
|
||||
below = copiesOf.value(*original);
|
||||
}
|
||||
}
|
||||
// Else the first copy in order, where Swift takes any.
|
||||
std::optional<QUuid> lead = active && copiesOf.contains(*active) ? std::optional(copiesOf.value(*active)) : std::nullopt;
|
||||
for (const QUuid &id : ids) {
|
||||
if (!lead && copiesOf.contains(id))
|
||||
lead = copiesOf.value(id);
|
||||
}
|
||||
setActiveLayerID(lead.value());
|
||||
m_selectedLayerIDs = QSet<QUuid>(copiesOf.cbegin(), copiesOf.cend());
|
||||
endEdit();
|
||||
}
|
||||
|
||||
// Copies the layer and all it holds, just above it.
|
||||
std::optional<QUuid> EditorSession::insertCopy(QUuid id)
|
||||
{
|
||||
const int index = indexOf(m_document->layers, id);
|
||||
if (index < 0)
|
||||
return std::nullopt;
|
||||
// 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;
|
||||
@@ -329,7 +428,7 @@ void EditorSession::duplicateActiveLayer()
|
||||
copies.push_back(layer);
|
||||
}
|
||||
if (m_document->layers.size() + copies.size() > 10'000)
|
||||
return;
|
||||
return std::nullopt;
|
||||
QHash<QUuid, QUuid> mapping;
|
||||
for (const ImageLayer © : copies)
|
||||
mapping.insert(copy.id, QUuid::createUuid());
|
||||
@@ -344,14 +443,12 @@ void EditorSession::duplicateActiveLayer()
|
||||
renamed(copy.parentID);
|
||||
renamed(copy.maskSourceID);
|
||||
}
|
||||
beginEdit(QStringLiteral("Duplicate Layer"));
|
||||
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();
|
||||
return mapping.value(id);
|
||||
}
|
||||
|
||||
// Alt-drag in the Layers panel: a copy placed where dropped.
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
#pragma once
|
||||
#include <QImage>
|
||||
#include <QMimeData>
|
||||
#include <QPointer>
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
#include <QUuid>
|
||||
#include <vector>
|
||||
|
||||
// Pixels copied from the canvas, with where they came from.
|
||||
struct PixelClipboard {
|
||||
QImage image;
|
||||
QPointF origin;
|
||||
// The clipboard's changes when written; more means another copy since.
|
||||
int changeCount;
|
||||
// What we put there; another object means another copy.
|
||||
QPointer<QMimeData> data;
|
||||
};
|
||||
|
||||
// Swift's (image, region) tuple: pixels and their canvas bounds.
|
||||
@@ -16,3 +20,11 @@ struct CopiedPixels {
|
||||
QImage image;
|
||||
QRectF region;
|
||||
};
|
||||
|
||||
// Layers Copy took whole: Paste brings them back complete.
|
||||
struct CopiedLayer {
|
||||
// Document order; a copied folder's contents come with it.
|
||||
std::vector<QUuid> ids;
|
||||
// As PixelClipboard's: another copy since replaces it.
|
||||
QPointer<QMimeData> data;
|
||||
};
|
||||
@@ -82,7 +82,7 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
|
||||
m_field->copy();
|
||||
else if (InlineTextEditor *text = typedText())
|
||||
text->copy();
|
||||
else if (session().canCopyPixels())
|
||||
else if (session().canCopyPixels() || session().canCopyLayer())
|
||||
session().copySelection();
|
||||
else
|
||||
qCWarning(lcApp) << "nothing to copy";
|
||||
@@ -93,6 +93,8 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
|
||||
m_field->paste();
|
||||
else if (InlineTextEditor *text = typedText())
|
||||
text->paste();
|
||||
else if (m_workspace.pasteCopiedLayer())
|
||||
return;
|
||||
else if (session().canPaste())
|
||||
session().paste();
|
||||
else
|
||||
@@ -395,7 +397,7 @@ void CompositorMenus::synchronize()
|
||||
action(QStringLiteral("fillBackground"))->setEnabled(s.canEditPixels());
|
||||
action(QStringLiteral("clearSelectionPixels"))->setEnabled(s.selection().has_value() && s.canEditPixels());
|
||||
action(QStringLiteral("layerViaCopy"))->setText(s.selection() ? QStringLiteral("Layer via Copy") : QStringLiteral("Duplicate Layer"));
|
||||
action(QStringLiteral("layerViaCopy"))->setEnabled(s.canCopyPixels() || (!s.selection() && s.canEditLayers() && active && !active->isGroup));
|
||||
action(QStringLiteral("layerViaCopy"))->setEnabled(s.canCopyPixels() || (!s.selection() && s.canEditLayers() && active));
|
||||
action(QStringLiteral("mergeLayers"))->setText(s.mergeTitle());
|
||||
action(QStringLiteral("mergeLayers"))->setEnabled(s.canMergeLayers());
|
||||
action(QStringLiteral("flipHorizontal"))->setEnabled(s.canTransform());
|
||||
|
||||
Reference in new issue
Block a user