diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index f35d6510e..90b5ac3aa 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -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 diff --git a/src/app/cli/default_cli_delegate.cpp b/src/app/cli/default_cli_delegate.cpp index 89d32db46..2e0e66438 100644 --- a/src/app/cli/default_cli_delegate.cpp +++ b/src/app/cli/default_cli_delegate.cpp @@ -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 @@ -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()); diff --git a/src/app/commands/cmd_cancel.cpp b/src/app/commands/cmd_cancel.cpp index 794781243..41c42bb56 100644 --- a/src/app/commands/cmd_cancel.cpp +++ b/src/app/commands/cmd_cancel.cpp @@ -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); diff --git a/src/app/script/script_input_chain.cpp b/src/app/script/script_input_chain.cpp new file mode 100644 index 000000000..1bae43a4f --- /dev/null +++ b/src/app/script/script_input_chain.cpp @@ -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 +#include +#include + +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 diff --git a/src/app/script/script_input_chain.h b/src/app/script/script_input_chain.h new file mode 100644 index 000000000..8333adf92 --- /dev/null +++ b/src/app/script/script_input_chain.h @@ -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 diff --git a/src/app/util/clipboard_native.cpp b/src/app/util/clipboard_native.cpp index 05d266607..801c72954 100644 --- a/src/app/util/clipboard_native.cpp +++ b/src/app/util/clipboard_native.cpp @@ -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) { diff --git a/tests/scripts/app_cut_paste.lua b/tests/scripts/app_cut_paste.lua new file mode 100644 index 000000000..5de9b6284 --- /dev/null +++ b/tests/scripts/app_cut_paste.lua @@ -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 \ No newline at end of file diff --git a/tests/sprites/cut_paste.aseprite b/tests/sprites/cut_paste.aseprite new file mode 100644 index 000000000..9007d941e Binary files /dev/null and b/tests/sprites/cut_paste.aseprite differ