Add slices copy&paste and duplication (fix #4466)

This commit is contained in:
Martín Capello 2025-03-07 18:03:49 -03:00
parent 53c415c933
commit 28a22c06cb
18 changed files with 313 additions and 29 deletions

View File

@ -1203,6 +1203,7 @@
<menu id="slice_popup_menu">
<item command="SliceProperties" text="@.properties" group="slice_popup_properties" />
<item command="DuplicateSlice" text="@.duplicate" group="slice_popup_duplicate" />
<item command="RemoveSlice" text="@.delete" group="slice_popup_delete" />
</menu>

View File

@ -265,6 +265,7 @@ Despeckle = Despeckle
DeveloperConsole = Developer Console
DiscardBrush = Discard Brush
DuplicateLayer = Duplicate Layer
DuplicateSlice = Duplicate Slice
DuplicateSprite = Duplicate Sprite
DuplicateView = Duplicate View
Exit = Exit
@ -1658,6 +1659,10 @@ from = From:
to = To:
tolerance = Tolerance:
[duplicate_slice]
x_duplicated = Slice "{}" duplicated
n_slices_duplicated = {} slice(s) duplicated
[remove_slice]
x_removed = Slice "{}" removed
n_slices_removed = {} slice(s) removed
@ -1736,6 +1741,7 @@ delete_file = Delete file, I've already sent it
[slice_popup_menu]
properties = Slice &Properties...
duplicate = D&uplicate Slice
delete = &Delete Slice
[slice_properties]

View File

@ -394,6 +394,7 @@ target_sources(app-lib PRIVATE
commands/cmd_deselect_mask.cpp
commands/cmd_discard_brush.cpp
commands/cmd_duplicate_layer.cpp
commands/cmd_duplicate_slice.cpp
commands/cmd_duplicate_sprite.cpp
commands/cmd_duplicate_view.cpp
commands/cmd_enter_license.cpp
@ -713,6 +714,7 @@ target_sources(app-lib PRIVATE
util/render_text.cpp
util/resize_image.cpp
util/shader_helpers.cpp
util/slice_utils.cpp
util/tile_flags_utils.cpp
util/tileset_utils.cpp
util/wrap_point.cpp

View File

@ -0,0 +1,108 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// 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/cmd/add_slice.h"
#include "app/commands/command.h"
#include "app/context.h"
#include "app/context_access.h"
#include "app/context_flags.h"
#include "app/i18n/strings.h"
#include "app/site.h"
#include "app/tx.h"
#include "app/ui/status_bar.h"
#include "app/util/slice_utils.h"
#include "base/convert_to.h"
#include "doc/object_id.h"
#include "doc/slice.h"
namespace app {
class DuplicateSliceCommand : public Command {
public:
DuplicateSliceCommand();
protected:
void onLoadParams(const Params& params) override;
bool onEnabled(Context* context) override;
void onExecute(Context* context) override;
private:
ObjectId m_sliceId;
};
DuplicateSliceCommand::DuplicateSliceCommand()
: Command(CommandId::DuplicateSlice(), CmdRecordableFlag)
{
}
void DuplicateSliceCommand::onLoadParams(const Params& params)
{
std::string id = params.get("id");
if (!id.empty())
m_sliceId = ObjectId(base::convert_to<doc::ObjectId>(id));
else
m_sliceId = NullId;
}
bool DuplicateSliceCommand::onEnabled(Context* context)
{
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
ContextFlags::HasActiveSprite | ContextFlags::HasActiveLayer);
}
void DuplicateSliceCommand::onExecute(Context* context)
{
std::vector<Slice*> selectedSlices;
{
const ContextReader reader(context);
if (m_sliceId == NullId) {
selectedSlices = get_selected_slices(reader.site());
if (selectedSlices.empty())
return;
}
else
selectedSlices.push_back(reader.sprite()->slices().getById(m_sliceId));
}
ContextWriter writer(context);
Tx tx(writer, "Duplicate Slice");
Sprite* sprite = writer.site().sprite();
Doc* doc = static_cast<Doc*>(sprite->document());
doc->notifyBeforeSlicesDuplication();
for (auto* s : selectedSlices) {
Slice* slice = new Slice(*s);
slice->setName(get_unique_slice_name(sprite, s->name()));
tx(new cmd::AddSlice(sprite, slice));
doc->notifySliceDuplicated(slice);
}
tx.commit();
std::string sliceName;
if (selectedSlices.size() == 1)
sliceName = selectedSlices[0]->name();
StatusBar::instance()->invalidate();
if (!sliceName.empty()) {
StatusBar::instance()->showTip(1000, Strings::duplicate_slice_x_duplicated(sliceName));
}
else {
StatusBar::instance()->showTip(
1000,
Strings::duplicate_slice_n_slices_duplicated(selectedSlices.size()));
}
}
Command* CommandFactory::createDuplicateSliceCommand()
{
return new DuplicateSliceCommand;
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -40,6 +40,7 @@ FOR_EACH_COMMAND(DeselectMask)
FOR_EACH_COMMAND(Despeckle)
FOR_EACH_COMMAND(DiscardBrush)
FOR_EACH_COMMAND(DuplicateLayer)
FOR_EACH_COMMAND(DuplicateSlice)
FOR_EACH_COMMAND(DuplicateSprite)
FOR_EACH_COMMAND(DuplicateView)
FOR_EACH_COMMAND(Exit)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -338,6 +338,19 @@ void Doc::notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti)
notify_observers<DocEvent&>(&DocObserver::onAfterAddTile, ev);
}
void Doc::notifyBeforeSlicesDuplication()
{
DocEvent ev(this);
notify_observers<DocEvent&>(&DocObserver::onBeforeSlicesDuplication, ev);
}
void Doc::notifySliceDuplicated(Slice* slice)
{
DocEvent ev(this);
ev.slice(slice);
notify_observers<DocEvent&>(&DocObserver::onSliceDuplicated, ev);
}
bool Doc::isModified() const
{
return !m_undo->isInSavedStateOrSimilar();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -141,6 +141,8 @@ public:
void notifyTilesetChanged(Tileset* tileset);
void notifyLayerGroupCollapseChange(Layer* layer);
void notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti);
void notifyBeforeSlicesDuplication();
void notifySliceDuplicated(Slice* slice);
//////////////////////////////////////////////////////////////////////
// File related properties

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -90,6 +90,8 @@ public:
// Slices
virtual void onSliceNameChange(DocEvent& ev) {}
virtual void onBeforeSlicesDuplication(DocEvent& ev) {}
virtual void onSliceDuplicated(DocEvent& ev) {}
// The tileset has changed.
virtual void onTilesetChanged(DocEvent& ev) {}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -36,9 +36,11 @@
#include "app/ui/workspace.h"
#include "app/ui_context.h"
#include "app/util/clipboard.h"
#include "app/util/slice_utils.h"
#include "base/fs.h"
#include "doc/color.h"
#include "doc/layer.h"
#include "doc/slice.h"
#include "doc/sprite.h"
#include "fmt/format.h"
#include "ui/accelerator.h"
@ -510,6 +512,8 @@ bool DocView::onCanCopy(Context* ctx)
return true;
else if (m_editor->isMovingPixels())
return true;
else if (m_editor->hasSelectedSlices())
return true;
else
return false;
}
@ -528,6 +532,11 @@ bool DocView::onCanPaste(Context* ctx)
return true;
}
}
if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable) &&
ctx->clipboard()->format() == ClipboardFormat::Slices) {
return true;
}
return false;
}
@ -560,15 +569,22 @@ bool DocView::onCopy(Context* ctx)
ctx->clipboard()->copy(reader);
return true;
}
else
return false;
std::vector<Slice*> selectedSlices = get_selected_slices(reader.site());
if (!selectedSlices.empty()) {
ctx->clipboard()->copySlices(selectedSlices);
return true;
}
return false;
}
bool DocView::onPaste(Context* ctx, const gfx::Point* position)
{
auto clipboard = ctx->clipboard();
if (clipboard->format() == ClipboardFormat::Image ||
clipboard->format() == ClipboardFormat::Tilemap) {
clipboard->format() == ClipboardFormat::Tilemap ||
clipboard->format() == ClipboardFormat::Slices) {
clipboard->paste(ctx, true, position);
return true;
}

View File

@ -2534,6 +2534,16 @@ void Editor::onBeforeLayerEditableChange(DocEvent& ev, bool newState)
m_state->onBeforeLayerEditableChange(this, ev.layer(), newState);
}
void Editor::onBeforeSlicesDuplication(DocEvent& ev)
{
clearSlicesSelection();
}
void Editor::onSliceDuplicated(DocEvent& ev)
{
selectSlice(ev.slice());
}
void Editor::setCursor(const gfx::Point& mouseDisplayPos)
{
Rect vp = View::getView(this)->viewportBounds();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -343,6 +343,8 @@ protected:
void onRemoveSlice(DocEvent& ev) override;
void onBeforeLayerVisibilityChange(DocEvent& ev, bool newState) override;
void onBeforeLayerEditableChange(DocEvent& ev, bool newState) override;
void onBeforeSlicesDuplication(DocEvent& ev) override;
void onSliceDuplicated(DocEvent& ev) override;
// ActiveToolObserver impl
void onActiveToolChange(tools::Tool* tool) override;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -45,6 +45,7 @@
#include "app/ui_context.h"
#include "app/util/expand_cel_canvas.h"
#include "app/util/layer_utils.h"
#include "app/util/slice_utils.h"
#include "doc/brush.h"
#include "doc/cel.h"
#include "doc/image.h"
@ -693,7 +694,7 @@ public:
// popup menu to create a new one.
if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) {
Slice* slice = new Slice;
slice->setName(getUniqueSliceName());
slice->setName(get_unique_slice_name(m_sprite));
SliceKey key(bounds);
slice->insert(getFrame(), key);
@ -716,18 +717,6 @@ private:
// EditorObserver impl
void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
std::string getUniqueSliceName() const
{
std::string prefix = "Slice";
int max = 0;
for (Slice* slice : m_sprite->slices())
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
return fmt::format("{} {}", prefix, max + 1);
}
};
//////////////////////////////////////////////////////////////////////

View File

@ -10,6 +10,7 @@
#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"
@ -32,6 +33,7 @@
#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"
@ -114,6 +116,9 @@ struct Clipboard::Data {
// Selected set of layers/layers/cels
ClipboardRange range;
// Selected slices
std::vector<Slice> slices;
Data() { range.observeUIContext(); }
~Data()
@ -132,6 +137,7 @@ struct Clipboard::Data {
picks.clear();
mask.reset();
range.invalidate();
slices.clear();
}
ClipboardFormat format() const
@ -146,6 +152,8 @@ struct Clipboard::Data {
return ClipboardFormat::PaletteEntries;
else if (tileset && picks.picks())
return ClipboardFormat::Tileset;
else if (!slices.empty())
return ClipboardFormat::Slices;
else
return ClipboardFormat::None;
}
@ -212,6 +220,7 @@ 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)
{
@ -226,6 +235,11 @@ void Clipboard::setData(Image* 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) {
@ -262,6 +276,7 @@ bool Clipboard::copyFromDocument(const Site& site, bool merged)
(mask ? new Mask(*mask) : nullptr),
(pal ? new Palette(*pal) : nullptr),
Tileset::MakeCopyCopyingImages(ts),
nullptr,
true, // set native clipboard
site.layer() && !site.layer()->isBackground());
@ -277,6 +292,7 @@ bool Clipboard::copyFromDocument(const Site& site, bool merged)
(mask ? new Mask(*mask) : nullptr),
(pal ? new Palette(*pal) : nullptr),
nullptr,
nullptr,
true, // set native clipboard
site.layer() && !site.layer()->isBackground());
@ -401,6 +417,7 @@ void Clipboard::copyImage(const Image* image, const Mask* mask, const Palette* p
(mask ? new Mask(*mask) : nullptr),
(pal ? new Palette(*pal) : nullptr),
nullptr,
nullptr,
App::instance()->isGui(),
false);
}
@ -415,6 +432,7 @@ void Clipboard::copyTilemap(const Image* image,
(mask ? new Mask(*mask) : nullptr),
(pal ? new Palette(*pal) : nullptr),
Tileset::MakeCopyCopyingImages(tileset),
nullptr,
true,
false);
}
@ -428,6 +446,7 @@ void Clipboard::copyPalette(const Palette* palette, const PalettePicks& picks)
nullptr,
new Palette(*palette),
nullptr,
nullptr,
false, // Don't touch the native clipboard now
false);
@ -438,6 +457,20 @@ void Clipboard::copyPalette(const Palette* palette, const PalettePicks& 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();
@ -782,6 +815,26 @@ void Clipboard::paste(Context* ctx, const bool interactive, const gfx::Point* po
}
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(get_unique_slice_name(dstSpr, s.name()));
tx(new cmd::AddSlice(dstSpr, slice));
editor->selectSlice(slice);
}
tx.commit();
updateDstDoc = true;
break;
}
}
// Update all editors/views showing this document
@ -799,7 +852,7 @@ ImageRef Clipboard::getImage(Palette* palette)
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, false, false);
setData(native_image, native_mask, native_palette, native_tileset, nullptr, false, false);
}
}
if (m_data->palette && palette)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -23,6 +23,7 @@ class Image;
class Mask;
class Palette;
class PalettePicks;
class Slice;
class Tileset;
} // namespace doc
@ -45,6 +46,7 @@ enum class ClipboardFormat {
PaletteEntries,
Tilemap,
Tileset,
Slices,
};
class Clipboard : public ui::ClipboardDelegate {
@ -74,6 +76,7 @@ public:
const doc::Palette* pal,
const doc::Tileset* tileset);
void copyPalette(const doc::Palette* palette, const doc::PalettePicks& picks);
void copySlices(const std::vector<doc::Slice*> slices);
void paste(Context* ctx, const bool interactive, const gfx::Point* position = nullptr);
doc::ImageRef getImage(doc::Palette* palette);
@ -106,6 +109,7 @@ private:
doc::Mask* mask,
doc::Palette* palette,
doc::Tileset* tileset,
const std::vector<doc::Slice*>* slices,
bool set_native_clipboard,
bool image_source_is_transparent);
bool copyFromDocument(const Site& site, bool merged = false);

View File

@ -0,0 +1,42 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#include "app/util/slice_utils.h"
#include "app/context_access.h"
#include "app/site.h"
#include "doc/slice.h"
#include "doc/sprite.h"
#include "fmt/format.h"
namespace app {
std::string get_unique_slice_name(const doc::Sprite* sprite, const std::string& namePrefix)
{
std::string prefix = namePrefix.empty() ? "Slice" : namePrefix;
int max = 0;
for (doc::Slice* slice : sprite->slices())
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
return fmt::format("{} {}", prefix, max + 1);
}
std::vector<doc::Slice*> get_selected_slices(const Site& site)
{
std::vector<Slice*> selectedSlices;
if (site.sprite() && !site.selectedSlices().empty()) {
for (auto* slice : site.sprite()->slices()) {
if (site.selectedSlices().contains(slice->id())) {
selectedSlices.push_back(slice);
}
}
}
return selectedSlices;
}
} // namespace app

View File

@ -0,0 +1,30 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_SLICE_UTILS_H_INCLUDED
#define APP_SLICE_UTILS_H_INCLUDED
#pragma once
#include <string>
#include <vector>
namespace doc {
class Slice;
class Sprite;
} // namespace doc
namespace app {
class Site;
std::string get_unique_slice_name(const doc::Sprite* sprite,
const std::string& namePrefix = std::string());
std::vector<doc::Slice*> get_selected_slices(const Site& site);
} // namespace app
#endif

View File

@ -31,7 +31,10 @@ Slices::~Slices()
void Slices::add(Slice* slice)
{
m_slices.push_back(slice);
// Insert the slice at the begining to display it at the front of the others.
// This is useful when duplicating (or copy & pasting) slices, because the
// user can drag the new slices instead of the originally selected ones.
m_slices.insert(m_slices.begin(), slice);
slice->setOwner(this);
}

View File

@ -14,7 +14,7 @@ do
assert(b.bounds == Rectangle(0, 2, 8, 10))
assert(c.bounds == Rectangle(0, 0, 32, 32))
local bounds = { nil, Rectangle(0, 2, 8, 10), Rectangle(0, 0, 32, 32) }
local bounds = { Rectangle(0, 0, 32, 32), Rectangle(0, 2, 8, 10), nil }
local i = 1
for k,v in ipairs(s.slices) do
@ -25,8 +25,8 @@ do
end
s:deleteSlice(b)
assert(a == s.slices[1])
assert(c == s.slices[2])
assert(c == s.slices[1])
assert(a == s.slices[2])
assert(2 == #s.slices)
app.undo()