Add 'x' and 'y' as input parameters to app.command.Paste()

This commit is contained in:
Gaspar Capello 2024-09-04 17:37:24 -03:00 committed by David Capello
parent de1fc581f2
commit e70bbbd369
21 changed files with 212 additions and 93 deletions

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -10,6 +11,7 @@
#include "app/app.h"
#include "app/commands/command.h"
#include "app/commands/params.h"
#include "app/ui/input_chain.h"
namespace app {
@ -19,8 +21,11 @@ public:
PasteCommand();
protected:
void onLoadParams(const Params& params) override;
bool onEnabled(Context* ctx) override;
void onExecute(Context* ctx) override;
private:
std::shared_ptr<gfx::Point> m_position;
};
PasteCommand::PasteCommand()
@ -28,6 +33,16 @@ PasteCommand::PasteCommand()
{
}
void PasteCommand::onLoadParams(const Params& params)
{
m_position.reset();
if (params.has_param("x") || params.has_param("y")) {
m_position.reset(new gfx::Point);
m_position->x = params.get_as<int>("x");
m_position->y = params.get_as<int>("y");
}
}
bool PasteCommand::onEnabled(Context* ctx)
{
return App::instance()->inputChain().canPaste(ctx);
@ -35,7 +50,7 @@ bool PasteCommand::onEnabled(Context* ctx)
void PasteCommand::onExecute(Context* ctx)
{
App::instance()->inputChain().paste(ctx);
App::instance()->inputChain().paste(ctx, m_position.get());
}
Command* CommandFactory::createPasteCommand()

View File

@ -84,13 +84,14 @@ bool ScriptInputChain::onCopy(Context* ctx)
return false;
}
bool ScriptInputChain::onPaste(Context* ctx)
bool ScriptInputChain::onPaste(Context* ctx,
const gfx::Point* position)
{
Clipboard* clipboard = ctx->clipboard();
if (!clipboard)
return false;
if (clipboard->format() == ClipboardFormat::Image) {
clipboard->paste(ctx, false);
clipboard->paste(ctx, false, position);
return true;
}
return false;

View File

@ -27,7 +27,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;
};

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -1692,7 +1692,8 @@ bool ColorBar::onCopy(Context* ctx)
return true;
}
bool ColorBar::onPaste(Context* ctx)
bool ColorBar::onPaste(Context* ctx,
const gfx::Point* position)
{
if (m_tilemapMode == TilemapMode::Tiles) {
showRemapTiles();

View File

@ -119,7 +119,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;

View File

@ -589,12 +589,13 @@ bool DocView::onCopy(Context* ctx)
return false;
}
bool DocView::onPaste(Context* ctx)
bool DocView::onPaste(Context* ctx,
const gfx::Point* position)
{
auto clipboard = ctx->clipboard();
if (clipboard->format() == ClipboardFormat::Image ||
clipboard->format() == ClipboardFormat::Tilemap) {
clipboard->paste(ctx, true);
clipboard->paste(ctx, true, position);
return true;
}
else

View File

@ -102,7 +102,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;

View File

@ -2654,7 +2654,9 @@ void Editor::setZoomAndCenterInMouse(const Zoom& zoom,
}
}
void Editor::pasteImage(const Image* image, const Mask* mask)
void Editor::pasteImage(const Image* image,
const Mask* mask,
const gfx::Point* position)
{
ASSERT(image);
@ -2686,11 +2688,14 @@ void Editor::pasteImage(const Image* image, const Mask* mask)
Sprite* sprite = this->sprite();
// Check bounds where the image will be pasted.
int x = mask->bounds().x;
int y = mask->bounds().y;
int x = (position ? position->x : mask->bounds().x);
int y = (position ? position->y : mask->bounds().y);
{
const Rect visibleBounds = getViewportBounds();
const Point maskCenter = mask->bounds().center();
const Point maskCenter = mask->bounds().center() +
(position ? gfx::Point(position->x - mask->bounds().x,
position->y - mask->bounds().y)
: gfx::Point());
// If the pasted image original location center point isn't
// visible, we center the image in the editor's visible bounds.
@ -2743,7 +2748,8 @@ void Editor::pasteImage(const Image* image, const Mask* mask)
m_brushPreview.hide();
Mask mask2(*mask);
mask2.setOrigin(x, y);
position ? mask2.setOrigin(position->x, position->y)
: mask2.setOrigin(x, y);
PixelsMovementPtr pixelsMovement(
new PixelsMovement(UIContext::instance(), site,

View File

@ -252,7 +252,9 @@ namespace app {
void setZoomAndCenterInMouse(const render::Zoom& zoom,
const gfx::Point& mousePos, ZoomBehavior zoomBehavior);
void pasteImage(const Image* image, const Mask* mask = nullptr);
void pasteImage(const Image* image,
const Mask* mask = nullptr,
const gfx::Point* position = nullptr);
void startSelectionTransformation(const gfx::Point& move, double angle);
void startFlipTransformation(doc::algorithm::FlipType flipType);

View File

@ -230,7 +230,8 @@ bool HomeView::onCopy(Context* ctx)
return false;
}
bool HomeView::onPaste(Context* ctx)
bool HomeView::onPaste(Context* ctx,
const gfx::Point* position)
{
auto clipboard = ctx->clipboard();
if (clipboard->format() == ClipboardFormat::Image) {

View File

@ -74,7 +74,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -87,10 +88,11 @@ void InputChain::copy(Context* ctx)
}
}
void InputChain::paste(Context* ctx)
void InputChain::paste(Context* ctx,
const gfx::Point* position)
{
for (auto e : m_elements) {
if (e->onCanPaste(ctx) && e->onPaste(ctx))
if (e->onCanPaste(ctx) && e->onPaste(ctx, position))
break;
}
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -8,6 +9,8 @@
#define APP_INPUT_CHAIN_H_INCLUDED
#pragma once
#include "gfx/point.h"
#include <vector>
namespace ui {
@ -35,7 +38,8 @@ namespace app {
void cut(Context* ctx);
void copy(Context* ctx);
void paste(Context* ctx);
void paste(Context* ctx,
const gfx::Point* position);
void clear(Context* ctx);
void cancel(Context* ctx);

View File

@ -9,6 +9,8 @@
#define APP_INPUT_CHAIN_ELEMENT_H_INCLUDED
#pragma once
#include "gfx/point.h"
namespace ui {
class Message;
}
@ -34,7 +36,8 @@ namespace app {
// which catch any exception that is thrown.
virtual bool onCut(Context* ctx) = 0;
virtual bool onCopy(Context* ctx) = 0;
virtual bool onPaste(Context* ctx) = 0;
virtual bool onPaste(Context* ctx,
const gfx::Point* position) = 0;
virtual bool onClear(Context* ctx) = 0;
virtual void onCancel(Context* ctx) = 0;
};

View File

@ -4398,7 +4398,8 @@ bool Timeline::onCopy(Context* ctx)
return false;
}
bool Timeline::onPaste(Context* ctx)
bool Timeline::onPaste(Context* ctx,
const gfx::Point* position)
{
auto clipboard = ctx->clipboard();
if (clipboard->format() == ClipboardFormat::DocRange) {

View File

@ -190,7 +190,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -381,12 +381,13 @@ bool Workspace::onCopy(Context* ctx)
return false;
}
bool Workspace::onPaste(Context* ctx)
bool Workspace::onPaste(Context* ctx,
const gfx::Point* position)
{
WorkspaceView* view = activeView();
InputChainElement* activeElement = (view ? view->onGetInputChainElement(): nullptr);
if (activeElement)
return activeElement->onPaste(ctx);
return activeElement->onPaste(ctx, position);
else
return false;
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2022-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -71,7 +71,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;

View File

@ -34,6 +34,8 @@
#include "app/util/new_image_from_mask.h"
#include "app/util/range_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"
@ -425,7 +427,7 @@ void Clipboard::copyImage(const Image* image,
(mask ? new Mask(*mask): nullptr),
(pal ? new Palette(*pal): nullptr),
nullptr,
true, false);
App::instance()->isGui(), false);
}
void Clipboard::copyTilemap(const Image* image,
@ -463,7 +465,8 @@ void Clipboard::copyPalette(const Palette* palette,
}
void Clipboard::paste(Context* ctx,
const bool interactive)
const bool interactive,
const gfx::Point* position)
{
const Site site = ctx->activeSite();
Doc* dstDoc = site.document();
@ -518,38 +521,94 @@ void Clipboard::paste(Context* ctx,
// Change to MovingPixelsState
editor->pasteImage(src_image.get(),
m_data->mask.get());
m_data->mask.get(),
position);
}
else {
// Non-interactive version (just copy the image to the cel)
// 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())
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 = api.addCel(
static_cast<LayerImage*>(dstLayer), site.frame(),
ImageRef(Image::createCopy(src_image.get())));
Cel* dstCel;
if (isAnImageOnDstCel)
api.clearCel(ctx->activeSite().cel());
else
result.reset(Image::createCopy(src_image.get()));
// Adjust bounds
// 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) {
if (m_data->mask) {
if (dstLayer->isReference()) {
dstCel->setBounds(dstSpr->bounds());
Mask emptyMask;
tx(new cmd::SetMask(dstDoc, &emptyMask));
}
else {
dstCel->setBounds(m_data->mask->bounds());
tx(new cmd::SetMask(dstDoc, m_data->mask.get()));
}
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;
@ -562,7 +621,8 @@ void Clipboard::paste(Context* ctx,
// Change to MovingTilemapState
editor->pasteImage(m_data->tilemap.get(),
m_data->mask.get());
m_data->mask.get(),
position);
}
else {
// TODO non-interactive version (for scripts)

View File

@ -75,7 +75,8 @@ namespace app {
void copyPalette(const doc::Palette* palette,
const doc::PalettePicks& picks);
void paste(Context* ctx,
const bool interactive);
const bool interactive,
const gfx::Point* position = nullptr);
doc::ImageRef getImage(doc::Palette* palette);

View File

@ -28,19 +28,10 @@ do
{ 2, 2,
2, 2 })
-- TO DO: Fix this difference between running this script with
-- 'UI Available' versus 'UI Not Available'
app.layer = sprite.layers[3]
if (app.isUIAvailable) then
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1 })
else
assert(app.cel.position == Point(0, 1))
expect_img(app.activeImage,
{ 0, 1, 1, 0, 0 })
end
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1 })
app.command.FlattenLayers()
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
@ -53,6 +44,18 @@ do
app.undo() -- New Layer
app.undo() -- Cut
app.layer = sprite.layers[1]
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1,
1, 1 })
app.layer = sprite.layers[2]
assert(app.cel.position == Point(2, 2))
expect_img(app.activeImage,
{ 2, 2,
2, 2 })
assert(#sprite.layers == 2)
-- Another test
app.layer = sprite.layers[1]
app.useTool {
@ -62,30 +65,21 @@ do
}
app.command.Cut()
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1,
1, 0 })
sprite:newLayer()
app.command.Paste()
-- TO DO: Fix this difference between running this script with
-- 'UI Available' versus 'UI Not Available'
app.layer = sprite.layers[3]
if (app.isUIAvailable) then
assert(app.cel.position == Point(2, 2))
expect_img(app.activeImage,
{ 1 })
else
assert(app.cel.position == Point(2, 2))
expect_img(app.activeImage,
{ 1, 0, 0 })
end
assert(app.cel.position == Point(2, 2))
expect_img(app.activeImage,
{ 1 })
app.undo() -- Paste
app.undo() -- New Layer
app.undo() -- Cut
app.undo() -- MoveMask
-- TO DO: at the moment useTool requires double undo to undo
-- the selection action (Just one undo should be enough).
app.undo() -- useTool
app.undo() -- useTool
-- Test app.command.Copy
app.layer = sprite.layers[1]
@ -108,19 +102,10 @@ do
expect_img(app.activeImage,
{ 2, 2,
2, 2 })
-- TO DO: Fix this difference between running this script with
-- 'UI Available' versus 'UI Not Available'
app.layer = sprite.layers[3]
if (app.isUIAvailable) then
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1 })
else
assert(app.cel.position == Point(0, 1))
expect_img(app.activeImage,
{ 0, 1, 1, 0, 0 })
end
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1 })
app.command.FlattenLayers()
assert(app.cel.position == Point(1, 1))
@ -130,6 +115,17 @@ do
0, 2, 2 })
-- Test app.command.Clear()
app.useTool {
tool = "rectangular_marquee",
points = {Point(2,2), Point(4,2)},
selection = SelectionMode.REPLACE
}
app.command.Clear()
expect_img(app.activeImage,
{ 1, 1, 0,
1, 0, 0,
0, 2, 2 })
app.useTool {
tool = "rectangular_marquee",
points = {Point(0,1), Point(4,2)},
@ -141,6 +137,8 @@ do
{ 2, 2 })
app.undo()
app.undo()
app.undo()
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
@ -151,7 +149,7 @@ do
-- Test app.command.Cancel()
app.useTool {
tool = "rectangular_marquee",
points = {Point(0,1), Point(4,2)},
points = {Point(2,2), Point(4,2)},
selection = SelectionMode.REPLACE
}
app.command.Cancel()
@ -161,4 +159,20 @@ do
{ 1, 1, 0,
1, 2, 2,
0, 2, 2 })
app.useTool {
tool = "rectangular_marquee",
points = {Point(2,0), Point(4,1)},
selection = SelectionMode.REPLACE
}
app.command.Copy()
sprite:newLayer()
app.command.Paste { x=3, y=3 }
app.command.FlattenLayers()
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1, 0,
1, 2, 2,
0, 2, 2,
0, 0, 1, })
end