mirror of https://github.com/aseprite/aseprite.git
894 lines
25 KiB
C++
894 lines
25 KiB
C++
// Aseprite
|
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
|
// Copyright (C) 2001-2018 David Capello
|
|
//
|
|
// This program is distributed under the terms of
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "app/app.h"
|
|
#include "app/cmd/add_slice.h"
|
|
#include "app/cmd/clear_mask.h"
|
|
#include "app/cmd/deselect_mask.h"
|
|
#include "app/cmd/set_mask.h"
|
|
#include "app/cmd/trim_cel.h"
|
|
#include "app/console.h"
|
|
#include "app/context_access.h"
|
|
#include "app/doc.h"
|
|
#include "app/doc_api.h"
|
|
#include "app/doc_range.h"
|
|
#include "app/doc_range_ops.h"
|
|
#include "app/i18n/strings.h"
|
|
#include "app/modules/gfx.h"
|
|
#include "app/modules/gui.h"
|
|
#include "app/pref/preferences.h"
|
|
#include "app/tx.h"
|
|
#include "app/ui/color_bar.h"
|
|
#include "app/ui/editor/editor.h"
|
|
#include "app/ui/skin/skin_theme.h"
|
|
#include "app/ui/timeline/timeline.h"
|
|
#include "app/ui_context.h"
|
|
#include "app/util/cel_ops.h"
|
|
#include "app/util/clipboard.h"
|
|
#include "app/util/new_image_from_mask.h"
|
|
#include "app/util/slice_utils.h"
|
|
#include "clip/clip.h"
|
|
#include "doc/algorithm/shrink_bounds.h"
|
|
#include "doc/blend_image.h"
|
|
#include "doc/doc.h"
|
|
#include "render/dithering.h"
|
|
#include "render/ordered_dither.h"
|
|
#include "render/quantization.h"
|
|
#include "view/cels.h"
|
|
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
|
|
namespace app {
|
|
|
|
using namespace doc;
|
|
|
|
namespace {
|
|
|
|
class ClipboardRange : public DocsObserver {
|
|
public:
|
|
ClipboardRange() : m_doc(nullptr) {}
|
|
|
|
~ClipboardRange() { ASSERT(!m_doc); }
|
|
|
|
void observeUIContext() { UIContext::instance()->documents().add_observer(this); }
|
|
|
|
void unobserveUIContext() { UIContext::instance()->documents().remove_observer(this); }
|
|
|
|
bool valid() const { return (m_doc != nullptr); }
|
|
|
|
void invalidate() { m_doc = nullptr; }
|
|
|
|
void setRange(Doc* doc, const DocRange& range)
|
|
{
|
|
m_doc = doc;
|
|
m_range = range;
|
|
}
|
|
|
|
Doc* document() const { return m_doc; }
|
|
DocRange range() const { return m_range; }
|
|
|
|
// DocsObserver impl
|
|
void onRemoveDocument(Doc* doc) override
|
|
{
|
|
if (doc == m_doc)
|
|
invalidate();
|
|
}
|
|
|
|
private:
|
|
Doc* m_doc;
|
|
DocRange m_range;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// Data in the clipboard
|
|
struct Clipboard::Data {
|
|
// Text used when the native clipboard is disabled
|
|
std::string text;
|
|
|
|
// RGB/Grayscale/Indexed image
|
|
ImageRef image;
|
|
|
|
// The palette of the image (or tileset) if it's indexed
|
|
std::shared_ptr<Palette> palette;
|
|
|
|
// In case we copy a tilemap information
|
|
ImageRef tilemap;
|
|
|
|
// Tileset for the tilemap or a set of tiles if we are copying tiles
|
|
// in the color bar
|
|
std::shared_ptr<Tileset> tileset;
|
|
|
|
// Selected entries copied from the palette or the tileset
|
|
PalettePicks picks;
|
|
|
|
// Original selection used to copy the image
|
|
std::shared_ptr<Mask> mask;
|
|
|
|
// Selected set of layers/layers/cels
|
|
ClipboardRange range;
|
|
|
|
// Selected slices
|
|
std::vector<Slice> slices;
|
|
|
|
Data() { range.observeUIContext(); }
|
|
|
|
~Data()
|
|
{
|
|
clear();
|
|
range.unobserveUIContext();
|
|
}
|
|
|
|
void clear()
|
|
{
|
|
text.clear();
|
|
image.reset();
|
|
palette.reset();
|
|
tilemap.reset();
|
|
tileset.reset();
|
|
picks.clear();
|
|
mask.reset();
|
|
range.invalidate();
|
|
slices.clear();
|
|
}
|
|
|
|
ClipboardFormat format() const
|
|
{
|
|
if (image)
|
|
return ClipboardFormat::Image;
|
|
else if (tilemap)
|
|
return ClipboardFormat::Tilemap;
|
|
else if (range.valid())
|
|
return ClipboardFormat::DocRange;
|
|
else if (palette && picks.picks())
|
|
return ClipboardFormat::PaletteEntries;
|
|
else if (tileset && picks.picks())
|
|
return ClipboardFormat::Tileset;
|
|
else if (!slices.empty())
|
|
return ClipboardFormat::Slices;
|
|
else
|
|
return ClipboardFormat::None;
|
|
}
|
|
};
|
|
|
|
static bool use_native_clipboard()
|
|
{
|
|
return Preferences::instance().experimental.useNativeClipboard();
|
|
}
|
|
|
|
static Clipboard* g_instance = nullptr;
|
|
|
|
Clipboard* Clipboard::instance()
|
|
{
|
|
return g_instance;
|
|
}
|
|
|
|
Clipboard::Clipboard() : m_data(new Data)
|
|
{
|
|
ASSERT(!g_instance);
|
|
g_instance = this;
|
|
|
|
registerNativeFormats();
|
|
}
|
|
|
|
Clipboard::~Clipboard()
|
|
{
|
|
ASSERT(g_instance == this);
|
|
g_instance = nullptr;
|
|
}
|
|
|
|
void Clipboard::setClipboardText(const std::string& text)
|
|
{
|
|
if (use_native_clipboard()) {
|
|
clip::set_text(text);
|
|
}
|
|
else {
|
|
m_data->text = text;
|
|
}
|
|
}
|
|
|
|
bool Clipboard::getClipboardText(std::string& text)
|
|
{
|
|
if (use_native_clipboard()) {
|
|
return clip::get_text(text);
|
|
}
|
|
else {
|
|
text = m_data->text;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool Clipboard::hasClipboardText()
|
|
{
|
|
if (use_native_clipboard()) {
|
|
return clip::has(clip::text_format());
|
|
}
|
|
else {
|
|
return !m_data->text.empty();
|
|
}
|
|
}
|
|
|
|
void Clipboard::setData(Image* image,
|
|
Mask* mask,
|
|
Palette* palette,
|
|
Tileset* tileset,
|
|
const std::vector<Slice*>* slices,
|
|
bool set_native_clipboard,
|
|
bool image_source_is_transparent)
|
|
{
|
|
const bool isTilemap = (image && image->isTilemap());
|
|
|
|
m_data->clear();
|
|
m_data->palette.reset(palette);
|
|
m_data->tileset.reset(tileset);
|
|
m_data->mask.reset(mask);
|
|
if (isTilemap)
|
|
m_data->tilemap.reset(image);
|
|
else
|
|
m_data->image.reset(image);
|
|
|
|
if (slices) {
|
|
for (auto* slice : *slices)
|
|
m_data->slices.push_back(*slice);
|
|
}
|
|
|
|
if (set_native_clipboard && use_native_clipboard()) {
|
|
// Copy tilemap to the native clipboard
|
|
if (isTilemap) {
|
|
ASSERT(tileset);
|
|
setNativeBitmap(image, mask, palette, tileset, -1);
|
|
}
|
|
// Copy non-tilemap images to the native clipboard
|
|
else {
|
|
setNativeBitmap(image,
|
|
mask,
|
|
palette,
|
|
nullptr,
|
|
image_source_is_transparent ? image->maskColor() : -1);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Clipboard::copyFromDocument(const Site& site, bool merged)
|
|
{
|
|
ASSERT(site.document());
|
|
const Doc* doc = static_cast<const Doc*>(site.document());
|
|
const Mask* mask = doc->mask();
|
|
const Palette* pal = doc->sprite()->palette(site.frame());
|
|
|
|
if (!merged && site.layer() && site.layer()->isTilemap() &&
|
|
site.tilemapMode() == TilemapMode::Tiles) {
|
|
const Tileset* ts = static_cast<LayerTilemap*>(site.layer())->tileset();
|
|
|
|
Image* image = new_tilemap_from_mask(site, mask);
|
|
if (!image)
|
|
return false;
|
|
|
|
setData(image,
|
|
(mask ? new Mask(*mask) : nullptr),
|
|
(pal ? new Palette(*pal) : nullptr),
|
|
Tileset::MakeCopyCopyingImages(ts),
|
|
nullptr,
|
|
true, // set native clipboard
|
|
site.layer() && !site.layer()->isBackground());
|
|
|
|
return true;
|
|
}
|
|
|
|
Image* image =
|
|
new_image_from_mask(site, mask, Preferences::instance().experimental.newBlend(), merged);
|
|
if (!image)
|
|
return false;
|
|
|
|
setData(image,
|
|
(mask ? new Mask(*mask) : nullptr),
|
|
(pal ? new Palette(*pal) : nullptr),
|
|
nullptr,
|
|
nullptr,
|
|
true, // set native clipboard
|
|
site.layer() && !site.layer()->isBackground());
|
|
|
|
return true;
|
|
}
|
|
|
|
ClipboardFormat Clipboard::format() const
|
|
{
|
|
// Check if the native clipboard has an image
|
|
if (use_native_clipboard() && hasNativeBitmap()) {
|
|
return ClipboardFormat::Image;
|
|
}
|
|
else {
|
|
return m_data->format();
|
|
}
|
|
}
|
|
|
|
void Clipboard::getDocumentRangeInfo(Doc** document, DocRange* range)
|
|
{
|
|
if (m_data->range.valid()) {
|
|
*document = m_data->range.document();
|
|
*range = m_data->range.range();
|
|
}
|
|
else {
|
|
*document = NULL;
|
|
}
|
|
}
|
|
|
|
void Clipboard::clearMaskFromCels(Tx& tx,
|
|
Doc* doc,
|
|
const Site& site,
|
|
const CelList& cels,
|
|
const bool deselectMask)
|
|
{
|
|
for (Cel* cel : cels) {
|
|
ObjectId celId = cel->id();
|
|
|
|
clear_mask_from_cel(tx, cel, site.tilemapMode(), site.tilesetMode());
|
|
|
|
// Get cel again just in case the cmd::ClearMask() called cmd::ClearCel()
|
|
cel = doc::get<Cel>(celId);
|
|
if (site.shouldTrimCel(cel))
|
|
tx(new cmd::TrimCel(cel));
|
|
}
|
|
|
|
if (deselectMask)
|
|
tx(new cmd::DeselectMask(doc));
|
|
}
|
|
|
|
void Clipboard::clearContent()
|
|
{
|
|
if (use_native_clipboard())
|
|
clearNativeContent();
|
|
m_data->clear();
|
|
}
|
|
|
|
void Clipboard::cut(ContextWriter& writer)
|
|
{
|
|
ASSERT(writer.document() != NULL);
|
|
ASSERT(writer.sprite() != NULL);
|
|
ASSERT(writer.layer() != NULL);
|
|
|
|
if (!copyFromDocument(writer.site())) {
|
|
Console console;
|
|
console.printf("Can't copying an image portion from the current layer\n");
|
|
}
|
|
else {
|
|
// TODO This code is similar to DocView::onClear()
|
|
{
|
|
Tx tx(writer, "Cut");
|
|
Site site = writer.context()->activeSite();
|
|
CelList cels = site.selectedUniqueCelsToEditPixels();
|
|
clearMaskFromCels(tx, writer.document(), site, cels,
|
|
true); // Deselect mask
|
|
tx.commit();
|
|
}
|
|
writer.document()->generateMaskBoundaries();
|
|
update_screen_for_document(writer.document());
|
|
}
|
|
}
|
|
|
|
void Clipboard::copy(const ContextReader& reader)
|
|
{
|
|
ASSERT(reader.document() != NULL);
|
|
|
|
if (!copyFromDocument(reader.site())) {
|
|
Console console;
|
|
console.printf("Can't copying an image portion from the current layer\n");
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Clipboard::copyMerged(const ContextReader& reader)
|
|
{
|
|
ASSERT(reader.document() != NULL);
|
|
|
|
copyFromDocument(reader.site(), true);
|
|
}
|
|
|
|
void Clipboard::copyRange(const ContextReader& reader, const DocRange& range)
|
|
{
|
|
ASSERT(reader.document() != NULL);
|
|
|
|
ContextWriter writer(reader);
|
|
|
|
clearContent();
|
|
m_data->range.setRange(writer.document(), range);
|
|
|
|
// TODO Replace this with a signal, because here the timeline
|
|
// depends on the clipboard and the clipboard on the timeline.
|
|
if (App* app = App::instance()) {
|
|
if (Timeline* timeline = app->timeline()) {
|
|
timeline->activateClipboardRange();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Clipboard::copyImage(const Image* image, const Mask* mask, const Palette* pal)
|
|
{
|
|
ASSERT(image->pixelFormat() != IMAGE_TILEMAP);
|
|
setData(Image::createCopy(image),
|
|
(mask ? new Mask(*mask) : nullptr),
|
|
(pal ? new Palette(*pal) : nullptr),
|
|
nullptr,
|
|
nullptr,
|
|
App::instance()->isGui(),
|
|
false);
|
|
}
|
|
|
|
void Clipboard::copyTilemap(const Image* image,
|
|
const Mask* mask,
|
|
const Palette* pal,
|
|
const Tileset* tileset)
|
|
{
|
|
ASSERT(image->pixelFormat() == IMAGE_TILEMAP);
|
|
setData(Image::createCopy(image),
|
|
(mask ? new Mask(*mask) : nullptr),
|
|
(pal ? new Palette(*pal) : nullptr),
|
|
Tileset::MakeCopyCopyingImages(tileset),
|
|
nullptr,
|
|
true,
|
|
false);
|
|
}
|
|
|
|
void Clipboard::copyPalette(const Palette* palette, const PalettePicks& picks)
|
|
{
|
|
if (!picks.picks())
|
|
return; // Do nothing case
|
|
|
|
setData(nullptr,
|
|
nullptr,
|
|
new Palette(*palette),
|
|
nullptr,
|
|
nullptr,
|
|
false, // Don't touch the native clipboard now
|
|
false);
|
|
|
|
// Here is where we copy the palette as text (hex format)
|
|
if (use_native_clipboard())
|
|
setNativePalette(palette, picks);
|
|
|
|
m_data->picks = picks;
|
|
}
|
|
|
|
void Clipboard::copySlices(const std::vector<Slice*> slices)
|
|
{
|
|
if (slices.empty())
|
|
return;
|
|
|
|
setData(nullptr,
|
|
nullptr,
|
|
nullptr,
|
|
nullptr,
|
|
&slices,
|
|
false, // Don't touch the native clipboard now
|
|
false);
|
|
}
|
|
|
|
void Clipboard::paste(Context* ctx, const bool interactive, const gfx::Point* position)
|
|
{
|
|
const Site site = ctx->activeSite();
|
|
Doc* dstDoc = site.document();
|
|
if (!dstDoc)
|
|
return;
|
|
|
|
Sprite* dstSpr = site.sprite();
|
|
if (!dstSpr)
|
|
return;
|
|
|
|
Editor* editor = Editor::activeEditor();
|
|
bool updateDstDoc = false;
|
|
|
|
switch (format()) {
|
|
case ClipboardFormat::Image: {
|
|
// Get the image from the native clipboard.
|
|
if (!getImage(nullptr))
|
|
return;
|
|
|
|
ASSERT(m_data->image);
|
|
|
|
Palette* dst_palette = dstSpr->palette(site.frame());
|
|
|
|
// Source image (clipboard or a converted copy to the destination 'imgtype')
|
|
ImageRef src_image;
|
|
if ( // Copy image of the same pixel format
|
|
(m_data->image->pixelFormat() == dstSpr->pixelFormat() &&
|
|
// Indexed images can be copied directly only if both images
|
|
// have the same palette.
|
|
(m_data->image->pixelFormat() != IMAGE_INDEXED ||
|
|
m_data->palette->countDiff(dst_palette, NULL, NULL) == 0))) {
|
|
src_image = m_data->image;
|
|
}
|
|
else {
|
|
RgbMap* dst_rgbmap = dstSpr->rgbMap(site.frame());
|
|
|
|
src_image.reset(render::convert_pixel_format(m_data->image.get(),
|
|
NULL,
|
|
dstSpr->pixelFormat(),
|
|
render::Dithering(),
|
|
dst_rgbmap,
|
|
m_data->palette.get(),
|
|
false,
|
|
0));
|
|
}
|
|
|
|
if (editor && interactive) {
|
|
// TODO we don't support pasting in multiple cels at the
|
|
// moment, so we clear the range here (same as in
|
|
// PasteTextCommand::onExecute())
|
|
App::instance()->timeline()->clearAndInvalidateRange();
|
|
|
|
// Change to MovingPixelsState
|
|
editor->pasteImage(src_image.get(), m_data->mask.get(), position);
|
|
}
|
|
else {
|
|
// CLI version:
|
|
// Paste the image according the position param.
|
|
// If there are no parameters, we assume the origin
|
|
// of the pasted image mask is the position.
|
|
// If there is no mask, x=0, y=0 is taken as position.
|
|
// TODO Support 'paste' command between images
|
|
// that do not match their pixel format.
|
|
Layer* dstLayer = site.layer();
|
|
ASSERT(dstLayer);
|
|
if (!dstLayer || !dstLayer->isImage() ||
|
|
(src_image->pixelFormat() != dstSpr->pixelFormat()))
|
|
return;
|
|
|
|
ImageRef result;
|
|
// resultBounds starts with the same bounds as source image,
|
|
// but it'll be merged with the active cel bounds (if any).
|
|
gfx::Rect resultBounds = gfx::Rect(
|
|
position ? *position : (m_data->mask ? m_data->mask->origin() : gfx::Point()),
|
|
src_image->size());
|
|
const bool isAnImageOnDstCel = ctx->activeSite().cel() && ctx->activeSite().cel()->image();
|
|
ASSERT(!ctx->activeSite().cel() || ctx->activeSite().cel()->image());
|
|
if (isAnImageOnDstCel) {
|
|
Cel* cel = ctx->activeSite().cel();
|
|
resultBounds = cel->bounds().createUnion(resultBounds);
|
|
// Create a new image (result) as a blend of the active cel image +
|
|
// the source image (clipboard image).
|
|
result.reset(Image::create(dstSpr->pixelFormat(), resultBounds.w, resultBounds.h));
|
|
doc::blend_image(
|
|
result.get(),
|
|
cel->image(),
|
|
gfx::Clip(cel->bounds().origin() - resultBounds.origin(), cel->image()->bounds()),
|
|
site.palette(),
|
|
255,
|
|
BlendMode::NORMAL);
|
|
doc::blend_image(result.get(),
|
|
src_image.get(),
|
|
gfx::Clip(*position - resultBounds.origin(), src_image->bounds()),
|
|
site.palette(),
|
|
255,
|
|
BlendMode::NORMAL);
|
|
}
|
|
|
|
ContextWriter writer(ctx);
|
|
Tx tx(writer, "Paste Image");
|
|
DocApi api = dstDoc->getApi(tx);
|
|
Cel* dstCel;
|
|
if (isAnImageOnDstCel)
|
|
api.clearCel(ctx->activeSite().cel());
|
|
else
|
|
result.reset(Image::createCopy(src_image.get()));
|
|
|
|
// Calculate the active image + pasted image bounds
|
|
const gfx::Rect startBounds(gfx::Point(), result->size());
|
|
const gfx::Point startOrigin(resultBounds.origin());
|
|
doc::algorithm::shrink_bounds(result.get(),
|
|
result->maskColor(),
|
|
dstLayer,
|
|
startBounds,
|
|
resultBounds);
|
|
// Cropped image according the shrink bounds
|
|
result.reset(crop_image(result.get(), resultBounds, result->maskColor()));
|
|
resultBounds.x = startOrigin.x + resultBounds.x;
|
|
resultBounds.y = startOrigin.y + resultBounds.y;
|
|
|
|
// Set image on the new Cel
|
|
dstCel = api.addCel(static_cast<LayerImage*>(dstLayer), site.frame(), result);
|
|
// Set cel bounds
|
|
if (dstCel) {
|
|
const Mask emptyMask;
|
|
if (dstLayer->isReference()) {
|
|
dstCel->setBounds(dstSpr->bounds());
|
|
tx(new cmd::SetMask(dstDoc, &emptyMask));
|
|
}
|
|
else {
|
|
dstCel->setBounds(resultBounds);
|
|
tx(new cmd::SetMask(dstDoc, m_data->mask ? m_data->mask.get() : &emptyMask));
|
|
}
|
|
}
|
|
tx.commit();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ClipboardFormat::Tilemap: {
|
|
if (editor && interactive) {
|
|
// TODO match both tilesets?
|
|
// TODO add post-command parameters (issue #2324)
|
|
|
|
// Change to MovingTilemapState
|
|
editor->pasteImage(m_data->tilemap.get(), m_data->mask.get(), position);
|
|
}
|
|
else {
|
|
// TODO non-interactive version (for scripts)
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ClipboardFormat::DocRange: {
|
|
DocRange srcRange = m_data->range.range();
|
|
Doc* srcDoc = m_data->range.document();
|
|
Sprite* srcSpr = srcDoc->sprite();
|
|
|
|
switch (srcRange.type()) {
|
|
case DocRange::kCels: {
|
|
Layer* dstLayer = site.layer();
|
|
ASSERT(dstLayer);
|
|
if (!dstLayer)
|
|
return;
|
|
|
|
frame_t dstFrameFirst = site.frame();
|
|
|
|
DocRange dstRange;
|
|
dstRange.startRange(dstLayer, dstFrameFirst, DocRange::kCels);
|
|
for (layer_t i = 1; i < srcRange.layers(); ++i) {
|
|
dstLayer = dstLayer->getPreviousBrowsable();
|
|
if (dstLayer == nullptr)
|
|
break;
|
|
}
|
|
dstRange.endRange(dstLayer, dstFrameFirst + srcRange.frames() - 1);
|
|
|
|
// We can use a document range op (copy_range) to copy/paste
|
|
// cels in the same document.
|
|
if (srcDoc == dstDoc) {
|
|
// This is the app::copy_range (not clipboard::copy_range()).
|
|
if (srcRange.layers() == dstRange.layers()) {
|
|
app::copy_range(srcDoc, srcRange, dstRange, kDocRangeBefore);
|
|
updateDstDoc = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
ContextWriter writer(ctx);
|
|
Tx tx(writer, "Paste Cels");
|
|
DocApi api = dstDoc->getApi(tx);
|
|
|
|
// Add extra frames if needed
|
|
while (dstFrameFirst + srcRange.frames() > dstSpr->totalFrames())
|
|
api.addFrame(dstSpr, dstSpr->totalFrames());
|
|
|
|
auto srcLayers = srcRange.selectedLayers().toBrowsableLayerList();
|
|
auto dstLayers = dstRange.selectedLayers().toBrowsableLayerList();
|
|
|
|
auto srcIt = srcLayers.begin();
|
|
auto dstIt = dstLayers.begin();
|
|
auto srcEnd = srcLayers.end();
|
|
auto dstEnd = dstLayers.end();
|
|
|
|
for (; srcIt != srcEnd && dstIt != dstEnd; ++srcIt, ++dstIt) {
|
|
auto srcLayer = *srcIt;
|
|
auto dstLayer = *dstIt;
|
|
|
|
if (!srcLayer->isImage() || !dstLayer->isImage())
|
|
continue;
|
|
|
|
frame_t dstFrame = dstFrameFirst;
|
|
for (frame_t srcFrame : srcRange.selectedFrames()) {
|
|
Cel* srcCel = srcLayer->cel(srcFrame);
|
|
|
|
if (srcCel && srcCel->image()) {
|
|
api.copyCel(static_cast<LayerImage*>(srcLayer),
|
|
srcFrame,
|
|
static_cast<LayerImage*>(dstLayer),
|
|
dstFrame);
|
|
}
|
|
else {
|
|
if (Cel* dstCel = dstLayer->cel(dstFrame))
|
|
api.clearCel(dstCel);
|
|
}
|
|
|
|
++dstFrame;
|
|
}
|
|
}
|
|
|
|
tx.commit();
|
|
updateDstDoc = true;
|
|
break;
|
|
}
|
|
|
|
case DocRange::kFrames: {
|
|
frame_t dstFrame = site.frame();
|
|
|
|
// We use a DocRange operation to copy frames inside
|
|
// the same sprite.
|
|
if (srcSpr == dstSpr) {
|
|
DocRange dstRange;
|
|
dstRange.startRange(nullptr, dstFrame, DocRange::kFrames);
|
|
dstRange.endRange(nullptr, dstFrame);
|
|
app::copy_range(srcDoc, srcRange, dstRange, kDocRangeBefore);
|
|
updateDstDoc = true;
|
|
break;
|
|
}
|
|
|
|
ContextWriter writer(ctx);
|
|
Tx tx(writer, "Paste Frames");
|
|
DocApi api = dstDoc->getApi(tx);
|
|
|
|
auto srcLayers = srcSpr->allBrowsableLayers();
|
|
auto dstLayers = dstSpr->allBrowsableLayers();
|
|
|
|
for (frame_t srcFrame : srcRange.selectedFrames()) {
|
|
api.addEmptyFrame(dstSpr, dstFrame);
|
|
api.setFrameDuration(dstSpr, dstFrame, srcSpr->frameDuration(srcFrame));
|
|
|
|
auto srcIt = srcLayers.begin();
|
|
auto dstIt = dstLayers.begin();
|
|
auto srcEnd = srcLayers.end();
|
|
auto dstEnd = dstLayers.end();
|
|
|
|
for (; srcIt != srcEnd && dstIt != dstEnd; ++srcIt, ++dstIt) {
|
|
auto srcLayer = *srcIt;
|
|
auto dstLayer = *dstIt;
|
|
|
|
if (!srcLayer->isImage() || !dstLayer->isImage())
|
|
continue;
|
|
|
|
Cel* cel = static_cast<LayerImage*>(srcLayer)->cel(srcFrame);
|
|
if (cel && cel->image()) {
|
|
api.copyCel(static_cast<LayerImage*>(srcLayer),
|
|
srcFrame,
|
|
static_cast<LayerImage*>(dstLayer),
|
|
dstFrame);
|
|
}
|
|
}
|
|
|
|
++dstFrame;
|
|
}
|
|
|
|
tx.commit();
|
|
updateDstDoc = true;
|
|
break;
|
|
}
|
|
|
|
case DocRange::kLayers: {
|
|
if (srcDoc->colorMode() != dstDoc->colorMode())
|
|
throw std::runtime_error(
|
|
"You cannot copy layers of document with different color modes");
|
|
|
|
ContextWriter writer(ctx);
|
|
Tx tx(writer, "Paste Layers");
|
|
DocApi api = dstDoc->getApi(tx);
|
|
|
|
// Remove children if their parent is selected so we only
|
|
// copy the parent.
|
|
SelectedLayers srcLayersSet = srcRange.selectedLayers();
|
|
srcLayersSet.removeChildrenIfParentIsSelected();
|
|
LayerList srcLayers = srcLayersSet.toBrowsableLayerList();
|
|
|
|
// Expand frames of dstDoc if it's needed.
|
|
frame_t maxFrame = 0;
|
|
for (Layer* srcLayer : srcLayers) {
|
|
if (!srcLayer->isImage())
|
|
continue;
|
|
|
|
Cel* lastCel = static_cast<LayerImage*>(srcLayer)->getLastCel();
|
|
if (lastCel && maxFrame < lastCel->frame())
|
|
maxFrame = lastCel->frame();
|
|
}
|
|
while (dstSpr->totalFrames() < maxFrame + 1)
|
|
api.addEmptyFrame(dstSpr, dstSpr->totalFrames());
|
|
|
|
for (Layer* srcLayer : srcLayers) {
|
|
Layer* afterThis;
|
|
if (srcLayer->isBackground() && !dstDoc->sprite()->backgroundLayer())
|
|
afterThis = nullptr;
|
|
else
|
|
afterThis = dstSpr->root()->lastLayer();
|
|
|
|
Layer* newLayer = nullptr;
|
|
if (srcLayer->isImage())
|
|
newLayer = new LayerImage(dstSpr);
|
|
else if (srcLayer->isGroup())
|
|
newLayer = new LayerGroup(dstSpr);
|
|
else
|
|
continue;
|
|
|
|
api.addLayer(dstSpr->root(), newLayer, afterThis);
|
|
|
|
srcDoc->copyLayerContent(srcLayer, dstDoc, newLayer);
|
|
}
|
|
|
|
tx.commit();
|
|
updateDstDoc = true;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ClipboardFormat::Slices: {
|
|
auto& slices = m_data->slices;
|
|
|
|
if (slices.empty())
|
|
return;
|
|
|
|
ContextWriter writer(ctx);
|
|
Tx tx(writer, "Paste Slices");
|
|
editor->clearSlicesSelection();
|
|
for (auto& s : slices) {
|
|
Slice* slice = new Slice(s);
|
|
slice->setName(Strings::general_copy_of(slice->name()));
|
|
tx(new cmd::AddSlice(dstSpr, slice));
|
|
editor->selectSlice(slice);
|
|
}
|
|
tx.commit();
|
|
updateDstDoc = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update all editors/views showing this document
|
|
if (updateDstDoc)
|
|
dstDoc->notifyGeneralUpdate();
|
|
}
|
|
|
|
ImageRef Clipboard::getImage(Palette* palette)
|
|
{
|
|
// Get the image from the native clipboard.
|
|
if (use_native_clipboard()) {
|
|
Image* native_image = nullptr;
|
|
Mask* native_mask = nullptr;
|
|
Palette* native_palette = nullptr;
|
|
Tileset* native_tileset = nullptr;
|
|
getNativeBitmap(&native_image, &native_mask, &native_palette, &native_tileset);
|
|
if (native_image) {
|
|
setData(native_image, native_mask, native_palette, native_tileset, nullptr, false, false);
|
|
}
|
|
}
|
|
if (m_data->palette && palette)
|
|
m_data->palette->copyColorsTo(palette);
|
|
return m_data->image;
|
|
}
|
|
|
|
bool Clipboard::getImageSize(gfx::Size& size)
|
|
{
|
|
if (use_native_clipboard() && getNativeBitmapSize(&size))
|
|
return true;
|
|
|
|
if (m_data->image) {
|
|
size.w = m_data->image->width();
|
|
size.h = m_data->image->height();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Palette* Clipboard::getPalette()
|
|
{
|
|
if (format() == ClipboardFormat::PaletteEntries) {
|
|
ASSERT(m_data->palette);
|
|
return m_data->palette.get();
|
|
}
|
|
else
|
|
return nullptr;
|
|
}
|
|
|
|
const PalettePicks& Clipboard::getPalettePicks()
|
|
{
|
|
return m_data->picks;
|
|
}
|
|
|
|
} // namespace app
|