#include "Document/BrushStroke.h" #include "Document/EditorSession.h" #include "Document/PixelAdjust.h" #include "IO/ImageExporter.h" #include "Logging.h" #include "Rendering/LayerRenderer.h" #include #include #include #include #include #include #include namespace { // The working format, whatever another app copied. QImage sRGBCopy(const QImage &image) { // CoreGraphics converts colour spaces as it draws; Qt does not. const bool tagged = image.colorSpace().isValid() && image.colorSpace() != QColorSpace::SRgb; const QImage source = tagged ? image.convertedToColorSpace(QColorSpace::SRgb) : image; QImage copy = BrushRaster::context(source.width(), source.height(), false); QPainter painter(©); BrushRaster::draw(source, QRectF(0, 0, source.width(), source.height()), painter); copy.setColorSpace(QColorSpace::SRgb); return copy; } // Swift clips the context; QPainter multiplies the coverage in. void clipTo(const std::optional &clip, QPainter &painter) { if (!clip) return; painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); painter.drawImage(clip->rect, BrushRaster::alphaView(*clip->coverage)); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); } const QMimeData *clipboardData() { return QGuiApplication::clipboard()->mimeData(); } } // Swift's SelectionClipboard extension; Cut arrives with 8.2. std::optional EditorSession::selectionCopyRegion() const { if (!m_document) return std::nullopt; const QRectF canvas(QPointF(0, 0), m_document->size()); const std::optional current = selection(); const QRectF bounds = current ? current->coverageBounds() : canvas; // Path operations leave float noise: round with a tolerance. constexpr double tolerance = 0.001; const double minX = std::floor(bounds.left() + tolerance), minY = std::floor(bounds.top() + tolerance); const QRectF region = QRectF(minX, minY, std::ceil(bounds.right() - tolerance) - minX, std::ceil(bounds.bottom() - tolerance) - minY).intersected(canvas); if (region.width() < 1 || region.height() < 1) return std::nullopt; return region; } bool EditorSession::canCopyPixels() const { const std::optional layer = activeLayer(); const std::optional current = selection(); if (!canEditLayers() || !layer || (layer->isGroup && !m_isMaskSelected) || (current && current->isEmpty())) return false; return m_isMaskSelected ? layer->mask.has_value() : layer->asset.has_value(); } // The layer's pixels as shown, through the selection. std::optional EditorSession::renderSelectedPixels(const ImageLayer &layer, bool mask) const { if (!m_document) return std::nullopt; const std::optional current = selection(); const std::optional clip = current ? std::optional(current->clip(m_document->size())) : std::nullopt; if (clip && !clip->coverage) return std::nullopt; const std::optional region = selectionCopyRegion(); if (!region) return std::nullopt; QImage image = BrushRaster::context(int(region->width()), int(region->height()), false); QPainter painter(&image); painter.translate(-region->left(), -region->top()); const LayerTransform transform = displayedTransform(layer); if (mask && layer.mask) { // A mask as opaque gray: ground, then white through it. const std::optional placement = displayedMaskPlacement(layer); const double ground = placement ? LayerMask::background(layer.mask->asset.thumbnail) : 0; painter.fillRect(*region, QColor::fromRgbF(ground, ground, ground)); clipTo(clip, painter); // Each pass is clipped alone, so soft edges stack. QImage white = BrushRaster::context(image.width(), image.height(), false); QPainter second(&white); second.translate(-region->left(), -region->top()); LayerRenderer::drawCoverage(layer.mask->asset.image(), placement.value_or(transform), second); clipTo(clip, second); second.end(); painter.drawImage(*region, white); } else if (!mask && layer.asset) { LayerRenderer::draw(layer.asset->image(), transform, transform.center(), painter, {}); clipTo(clip, painter); } else { return std::nullopt; } painter.end(); return CopiedPixels{image, *region}; } bool EditorSession::canCopyMerged() const { const std::optional current = selection(); if (!canEditLayers() || (current && current->isEmpty())) return false; for (const ImageLayer &layer : m_document->renderLayers()) { if (layer.asset) return true; } return false; } // Every visible layer as shown, through the selection. std::optional EditorSession::renderMergedPixels() const { if (!m_document) return std::nullopt; const std::optional current = selection(); const std::optional clip = current ? std::optional(current->clip(m_document->size())) : std::nullopt; if (clip && !clip->coverage) return std::nullopt; const std::optional region = selectionCopyRegion(); if (!region) return std::nullopt; // Composited first: Color Burn and Dodge read their backdrop. QImage merged = BrushRaster::context(int(region->width()), int(region->height()), false); QPainter composite(&merged); composite.translate(-region->left(), -region->top()); drawLiveComposite(*m_document, composite); composite.end(); if (clip) { QPainter painter(&merged); painter.translate(-region->left(), -region->top()); painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); painter.drawImage(clip->rect, BrushRaster::alphaView(*clip->coverage)); } return CopiedPixels{merged, *region}; } void EditorSession::copyMergedSelection() { if (!canCopyMerged()) return; try { const std::optional copied = renderMergedPixels(); if (!copied) { qCWarning(lcApp) << "nothing to copy merged"; return; } store(*copied); } catch (const ExportError &error) { setBrushError(QString::fromUtf8(error.what())); } } 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() { 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 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 &EditorSession::pixelClipboard() const { return m_pixelClipboard; } const std::optional &EditorSession::copiedLayer() const { return m_copiedLayer; } // Swift's change count: our own object while we own it. bool EditorSession::clipboardHolds(const QPointer &data) { return data && QGuiApplication::clipboard()->mimeData() == data.data(); } // Keeps pixels for Paste; the system clipboard gets PNG. void EditorSession::store(const CopiedPixels &copied) { auto *data = new QMimeData; data->setImageData(copied.image); QByteArray png; QBuffer buffer(&png); QImageWriter writer(&buffer, "png"); if (writer.write(copied.image)) data->setData(QStringLiteral("image/png"), png); else qCWarning(lcApp) << "the copied pixels could not be encoded as PNG:" << writer.errorString(); QGuiApplication::clipboard()->setMimeData(data); m_pixelClipboard = PixelClipboard{copied.image, copied.region.topLeft(), data}; m_copiedLayer = std::nullopt; } void EditorSession::cutSelection(std::function done) { if (!selection() || !canCopyPixels()) { if (done) QMetaObject::invokeMethod(this, done, Qt::QueuedConnection); return; } copySelection(); clearSelectedPixels(std::move(done)); } bool EditorSession::canPaste() const { if (!m_document || !canEditLayers()) return false; if (m_pixelClipboard && clipboardHolds(m_pixelClipboard->data)) return true; // An empty clipboard has no data object at all. const QMimeData *data = clipboardData(); return data && data->hasImage(); } // Pixels copied here go back in place; others centre. void EditorSession::paste() { if (!canPaste()) return; if (m_pixelClipboard && clipboardHolds(m_pixelClipboard->data)) { addPixelLayer(m_pixelClipboard->image, m_pixelClipboard->origin, nextLayerName(), QStringLiteral("Paste")); return; } const QImage external = qvariant_cast(clipboardData()->imageData()); if (external.isNull()) { qCWarning(lcApp) << "the clipboard's image could not be read"; return; } try { const QImage image = sRGBCopy(external); const QPointF origin(std::floor((m_document->size().width() - image.width()) / 2), std::floor((m_document->size().height() - image.height()) / 2)); addPixelLayer(image, origin, nextLayerName(), QStringLiteral("Paste")); } catch (const ExportError &error) { // Swift's `try?` beeps here, where a layer's copy alerts. qCWarning(lcApp) << "the clipboard's image could not be copied:" << error.what(); } } // Ctrl+J: the selection as a layer; without one, a duplicate. void EditorSession::layerViaCopy() { const std::optional layer = activeLayer(); const std::optional current = selection(); 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 copied = renderSelectedPixels(*layer, m_isMaskSelected); if (!copied) { qCWarning(lcApp) << "nothing to copy into a layer"; return; } addPixelLayer(copied->image, copied->region.topLeft(), nextLayerName(), QStringLiteral("Layer via Copy")); } catch (const ExportError &error) { setBrushError(QString::fromUtf8(error.what())); } } // A new layer above the active one, in its folder. void EditorSession::addPixelLayer(const QImage &image, QPointF origin, const QString &name, const QString &editName, bool dropsSelection, const std::optional &shape, const std::optional &text) { if (!m_document) return; QImage thumbnail; try { thumbnail = PixelAdjust::thumbnail(image); } catch (const ExportError &error) { qCWarning(lcApp) << "no thumbnail for the new layer:" << error.what(); return; } ImageLayer layer(ImportedImage(image, thumbnail, name), origin); if (shape) layer.shape = LayerShape{*shape, layer.asset->identity()}; if (text) layer.text = LayerText{*text, layer.asset->identity()}; const std::optional active = activeLayer(); layer.parentID = active && active->isGroup ? m_activeLayerID : active ? active->parentID : std::nullopt; const int activeIndex = indexOf(m_document->layers, m_activeLayerID); const int index = activeIndex >= 0 ? activeIndex + 1 : int(m_document->layers.size()); finishOpacityEdit(); beginEdit(editName); m_document->layers.insert(m_document->layers.begin() + index, layer); // Pasting drops the selection, as Photoshop; a shape keeps it. if (dropsSelection) m_document->selection = std::nullopt; setActiveLayerID(layer.id); endEdit(); } QString EditorSession::nextLayerName() const { QSet names; for (const ImageLayer &layer : m_document->layers) names.insert(layer.name); int number = 1; while (names.contains(QStringLiteral("Layer %1").arg(number))) ++number; return QStringLiteral("Layer %1").arg(number); } // Selected layers in document order, none inside a selected folder. std::vector EditorSession::copiedLayerIDs() const { if (!m_document) return {}; // The active layer is always selected: Swift's union is dead. const QSet &selected = m_selectedLayerIDs; QSet nested; for (const QUuid &id : selected) nested.unite(descendantIDs(id)); std::vector 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() { duplicateLayers(copiedLayerIDs()); } // One copy sits above its original; several, above the topmost. void EditorSession::duplicateLayers(const std::vector &ids, const QString &editName) { if (!canEditLayers() || ids.empty()) return; const std::optional active = m_activeLayerID; beginEdit(editName); QHash copiesOf; for (const QUuid &id : ids) { if (const std::optional 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 records; for (const ImageLayer &layer : m_document->layers) records.push_back(layer.hierarchyRecord()); std::vector 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 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 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(copiesOf.cbegin(), copiesOf.cend()); endEdit(); } // Copies the layer and all it holds, just above it. std::optional 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. QSet included = descendantIDs(id); included.insert(id); std::vector 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 std::nullopt; QHash mapping; for (const ImageLayer © : copies) mapping.insert(copy.id, QUuid::createUuid()); const auto renamed = [&mapping](std::optional &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); } 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()); } return mapping.value(id); } // Alt-drag in the Layers panel: a copy placed where dropped. bool EditorSession::duplicateLayer(QUuid id, std::optional parent, std::optional above, bool atBottom) { // canPlaceLayer reads canEditLayers, Swift's first term. if (!canPlaceLayer(id, parent)) return false; beginEdit(QStringLiteral("Duplicate Layer")); selectLayer(id); duplicateActiveLayer(); // Past the layer limit no copy was made. const bool placed = m_activeLayerID != id && placeLayer(*m_activeLayerID, parent, above, atBottom); endEdit(); return placed; }