Fix app.command.Cut/Paste does not work in --batch mode (fix #4354)

This fix adds support for cut/copy/paste selected areas during
script execution when there isn't UI.
This commit is contained in:
Gaspar Capello 2024-09-04 16:34:32 -03:00 committed by David Capello
parent 09bb5cc3d3
commit de1fc581f2
8 changed files with 344 additions and 3 deletions

View File

@ -220,6 +220,7 @@ if(ENABLE_SCRIPTING)
script/range_class.cpp
script/rectangle_class.cpp
script/require.cpp
script/script_input_chain.cpp
script/security.cpp
script/selection_class.cpp
script/site_class.cpp

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) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -31,6 +31,8 @@
#ifdef ENABLE_SCRIPTING
#include "app/app.h"
#include "app/script/engine.h"
#include "app/script/script_input_chain.h"
#include "app/ui/input_chain.h"
#endif
#include <iostream>
@ -143,6 +145,10 @@ void DefaultCliDelegate::exportFiles(Context* ctx, DocExporter& exporter)
int DefaultCliDelegate::execScript(const std::string& filename,
const Params& params)
{
ScriptInputChain scriptInputChain;
if (!App::instance()->isGui()) {
App::instance()->inputChain().prioritize(&scriptInputChain, nullptr);
}
auto engine = App::instance()->scriptEngine();
if (!engine->evalUserFile(filename, params))
throw base::Exception("Error executing script %s", filename.c_str());

View File

@ -63,7 +63,7 @@ void CancelCommand::onExecute(Context* context)
case All:
// TODO should the ContextBar be a InputChainElement to intercept onCancel()?
// Discard brush
{
if (context->isUIAvailable()) {
Command* discardBrush = Commands::instance()->byId(
CommandId::DiscardBrush());
context->executeCommand(discardBrush);

View File

@ -0,0 +1,131 @@
// Aseprite
// Copyright (C) 2024 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/app.h"
#include "app/cmd/deselect_mask.h"
#include "app/cmd/remap_colors.h"
#include "app/commands/commands.h"
#include "app/context_access.h"
#include "app/script/script_input_chain.h"
#include "app/site.h"
#include "app/tx.h"
#include "app/util/clipboard.h"
#include "doc/mask.h"
#include "doc/layer.h"
#include "doc/primitives.h"
#include <cstring>
#include <limits>
#include <memory>
namespace app {
ScriptInputChain::~ScriptInputChain() { }
void ScriptInputChain::onNewInputPriority(InputChainElement* element,
const ui::Message* msg) { }
bool ScriptInputChain::onCanCut(Context* ctx)
{
return ctx->activeDocument() &&
ctx->activeDocument()->isMaskVisible();
}
bool ScriptInputChain::onCanCopy(Context* ctx)
{
return onCanCut(ctx);
}
bool ScriptInputChain::onCanPaste(Context* ctx)
{
const Clipboard* clipboard(ctx->clipboard());
if (!clipboard)
return false;
return clipboard->format() == ClipboardFormat::Image &&
ctx->activeSite().layer() &&
ctx->activeSite().layer()->type() == ObjectType::LayerImage;
}
bool ScriptInputChain::onCanClear(Context* ctx)
{
return onCanCut(ctx);
}
bool ScriptInputChain::onCut(Context* ctx)
{
ContextWriter writer(ctx);
Clipboard* clipboard = ctx->clipboard();
if (!clipboard)
return false;
if (writer.document()) {
clipboard->cut(writer);
return true;
}
return false;
}
bool ScriptInputChain::onCopy(Context* ctx)
{
ContextReader reader(ctx);
Clipboard* clipboard = ctx->clipboard();
if (!clipboard)
return false;
if (reader.document()) {
clipboard->copy(reader);
return true;
}
return false;
}
bool ScriptInputChain::onPaste(Context* ctx)
{
Clipboard* clipboard = ctx->clipboard();
if (!clipboard)
return false;
if (clipboard->format() == ClipboardFormat::Image) {
clipboard->paste(ctx, false);
return true;
}
return false;
}
bool ScriptInputChain::onClear(Context* ctx)
{
// TODO This code is similar to DocView::onClear() and Clipboard::cut()
ContextWriter writer(ctx);
Doc* document = ctx->activeDocument();
if (writer.document()) {
ctx->clipboard()->clearContent();
CelList cels;
const Site site = ctx->activeSite();
cels.push_back(site.cel());
if (cels.empty()) // No cels to modify
return false;
Tx tx(writer, "Clear");
ctx->clipboard()->clearMaskFromCels(
tx, document, site, cels, true);
tx.commit();
return true;
}
return false;
}
void ScriptInputChain::onCancel(Context* ctx)
{
// Deselect mask
if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
ContextFlags::HasVisibleMask)) {
Command* deselectMask = Commands::instance()->byId(CommandId::DeselectMask());
ctx->executeCommand(deselectMask);
ctx->activeDocument()->setMaskVisible(false);
}
}
} // namespace app

View File

@ -0,0 +1,37 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef ENABLE_SCRIPTING
#ifndef APP_SCRIPT_SCRIPT_INPUT_CHAIN_H_INCLUDED
#define APP_SCRIPT_SCRIPT_INPUT_CHAIN_H_INCLUDED
#pragma once
#include "app/ui/input_chain_element.h"
namespace app {
class ScriptInputChain : public InputChainElement {
public:
// InputChainElement impl
~ScriptInputChain() override;
void onNewInputPriority(InputChainElement* element,
const ui::Message* msg) override;
bool onCanCut(Context* ctx) override;
bool onCanCopy(Context* ctx) override;
bool onCanPaste(Context* ctx) override;
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;
};
} // namespace app
#endif
#endif

View File

@ -53,7 +53,9 @@ namespace {
};
void* native_window_handle() {
return os::instance()->defaultWindow()->nativeHandle();
if (os::instance()->defaultWindow())
return os::instance()->defaultWindow()->nativeHandle();
return nullptr;
}
void custom_error_handler(clip::ErrorCode code) {

View File

@ -0,0 +1,164 @@
-- Copyright (C) 2024 Igara Studio S.A.
--
-- This file is released under the terms of the MIT license.
-- Read LICENSE.txt for more information.
dofile('./test_utils.lua')
do
local sprite = Sprite{ fromFile="sprites/cut_paste.aseprite" }
app.layer = sprite.layers[1]
app.useTool {
tool = "rectangular_marquee",
points = {Point(0,1), Point(4,1)},
selection = SelectionMode.REPLACE
}
app.command.Cut()
sprite:newLayer()
app.command.Paste()
app.layer = sprite.layers[1]
assert(app.cel.position == Point(1, 2))
expect_img(app.activeImage,
{ 1, 1 })
app.layer = sprite.layers[2]
assert(app.cel.position == Point(2, 2))
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
app.command.FlattenLayers()
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1, 0,
1, 2, 2,
0, 2, 2 })
app.undo() -- Flatten
app.undo() -- Paste
app.undo() -- New Layer
app.undo() -- Cut
-- Another test
app.layer = sprite.layers[1]
app.useTool {
tool = "rectangular_marquee",
points = {Point(2,2), Point(4,2)},
selection = SelectionMode.REPLACE
}
app.command.Cut()
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
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]
app.useTool {
tool = "rectangular_marquee",
points = {Point(0,1), Point(4,1)},
selection = SelectionMode.REPLACE
}
app.command.Copy()
sprite:newLayer()
app.command.Paste()
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 })
-- 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
app.command.FlattenLayers()
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1, 0,
1, 2, 2,
0, 2, 2 })
-- Test app.command.Clear()
app.useTool {
tool = "rectangular_marquee",
points = {Point(0,1), Point(4,2)},
selection = SelectionMode.REPLACE
}
app.command.Clear()
assert(app.cel.position == Point(2, 3))
expect_img(app.activeImage,
{ 2, 2 })
app.undo()
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1, 0,
1, 2, 2,
0, 2, 2 })
-- Test app.command.Cancel()
app.useTool {
tool = "rectangular_marquee",
points = {Point(0,1), Point(4,2)},
selection = SelectionMode.REPLACE
}
app.command.Cancel()
app.command.Cut()
assert(app.cel.position == Point(1, 1))
expect_img(app.activeImage,
{ 1, 1, 0,
1, 2, 2,
0, 2, 2 })
end

Binary file not shown.