diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index df8a27705..74c238f55 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -182,6 +182,7 @@ if(ENABLE_SCRIPTING) script/image_iterator_class.cpp script/image_spec_class.cpp script/images_class.cpp + script/keys.cpp script/layer_class.cpp script/layers_class.cpp script/luacpp.cpp diff --git a/src/app/script/canvas_widget.cpp b/src/app/script/canvas_widget.cpp index 36932416a..2028ba347 100644 --- a/src/app/script/canvas_widget.cpp +++ b/src/app/script/canvas_widget.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (c) 2022 Igara Studio S.A. +// Copyright (c) 2022-2023 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -29,6 +29,15 @@ ui::WidgetType Canvas::Type() return type; } +// static +bool Canvas::s_stopKeyEventPropagation = false; + +// static +void Canvas::stopKeyEventPropagation() +{ + s_stopKeyEventPropagation = true; +} + Canvas::Canvas() : ui::Widget(Type()) { } @@ -64,6 +73,26 @@ bool Canvas::onProcessMessage(ui::Message* msg) { switch (msg->type()) { + case ui::kKeyDownMessage: + if (hasFocus()) { + s_stopKeyEventPropagation = false; + auto keyMsg = static_cast(msg); + KeyDown(keyMsg); + if (s_stopKeyEventPropagation) + return true; + } + break; + + case ui::kKeyUpMessage: + if (hasFocus()) { + s_stopKeyEventPropagation = false; + auto keyMsg = static_cast(msg); + KeyUp(keyMsg); + if (s_stopKeyEventPropagation) + return true; + } + break; + case ui::kSetCursorMessage: ui::set_mouse_cursor(m_cursorType); return true; @@ -78,6 +107,9 @@ bool Canvas::onProcessMessage(ui::Message* msg) if (!hasCapture()) captureMouse(); + if (isFocusStop() && !hasFocus()) + requestFocus(); + auto mouseMsg = static_cast(msg); MouseDown(mouseMsg); break; diff --git a/src/app/script/canvas_widget.h b/src/app/script/canvas_widget.h index b33d7f327..a4af27da2 100644 --- a/src/app/script/canvas_widget.h +++ b/src/app/script/canvas_widget.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (c) 2022 Igara Studio S.A. +// Copyright (c) 2022-2023 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -36,13 +36,19 @@ public: } obs::signal Paint; + obs::signal KeyDown; + obs::signal KeyUp; obs::signal MouseMove; obs::signal MouseDown; obs::signal MouseUp; obs::signal Wheel; obs::signal TouchMagnify; + static void stopKeyEventPropagation(); + private: + static bool s_stopKeyEventPropagation; + void onInitTheme(ui::InitThemeEvent& ev) override; bool onProcessMessage(ui::Message* msg) override; void onResize(ui::ResizeEvent& ev) override; diff --git a/src/app/script/dialog_class.cpp b/src/app/script/dialog_class.cpp index d975578e3..766756eca 100644 --- a/src/app/script/dialog_class.cpp +++ b/src/app/script/dialog_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -16,6 +16,7 @@ #include "app/script/canvas_widget.h" #include "app/script/engine.h" #include "app/script/graphics_context.h" +#include "app/script/keys.h" #include "app/script/luacpp.h" #include "app/ui/color_button.h" #include "app/ui/color_shades.h" @@ -914,6 +915,7 @@ int Dialog_canvas(lua_State* L) widget->setSizeHint(sz); + bool handleKeyEvents = false; if (lua_istable(L, 2)) { int type = lua_getfield(L, 2, "onpaint"); if (type == LUA_TFUNCTION) { @@ -927,6 +929,47 @@ int Dialog_canvas(lua_State* L) lua_pop(L, 1); // Auxiliary callbacks used in Canvas events + auto keyCallback = + [](lua_State* L, ui::KeyMessage* msg) { + ASSERT(msg->recipient()); + if (!msg->recipient()) + return; + + // Key modifiers + lua_pushboolean(L, msg->modifiers() & ui::kKeyAltModifier); + lua_setfield(L, -2, "altKey"); + + lua_pushboolean(L, msg->modifiers() & (ui::kKeyCmdModifier | ui::kKeyWinModifier)); + lua_setfield(L, -2, "metaKey"); + + lua_pushboolean(L, msg->modifiers() & ui::kKeyCtrlModifier); + lua_setfield(L, -2, "ctrlKey"); + + lua_pushboolean(L, msg->modifiers() & ui::kKeyShiftModifier); + lua_setfield(L, -2, "shiftKey"); + + lua_pushboolean(L, msg->modifiers() & ui::kKeySpaceModifier); + lua_setfield(L, -2, "spaceKey"); + + // KeyMessage specifics + lua_pushinteger(L, msg->repeat()); + lua_setfield(L, -2, "repeat"); + + // TODO improve this (create an Event metatable) + lua_pushcfunction(L, [](lua_State*) -> int { + Canvas::stopKeyEventPropagation(); + return 0; + }); + lua_setfield(L, -2, "stopPropagation"); + + std::wstring keyString(1, (wchar_t)msg->unicodeChar()); + lua_pushstring(L, base::to_utf8(keyString).c_str()); + lua_setfield(L, -2, "key"); + + lua_pushstring(L, vkcode_to_code(msg->scancode())); + lua_setfield(L, -2, "code"); + }; + auto mouseCallback = [](lua_State* L, ui::MouseMessage* msg) { ASSERT(msg->recipient()); @@ -970,6 +1013,20 @@ int Dialog_canvas(lua_State* L) lua_setfield(L, -2, "magnification"); }; + type = lua_getfield(L, 2, "onkeydown"); + if (type == LUA_TFUNCTION) { + Dialog_connect_signal(L, 1, widget->KeyDown, keyCallback); + handleKeyEvents = true; + } + lua_pop(L, 1); + + type = lua_getfield(L, 2, "onkeyup"); + if (type == LUA_TFUNCTION) { + Dialog_connect_signal(L, 1, widget->KeyUp, keyCallback); + handleKeyEvents = true; + } + lua_pop(L, 1); + type = lua_getfield(L, 2, "onmousemove"); if (type == LUA_TFUNCTION) { Dialog_connect_signal(L, 1, widget->MouseMove, mouseCallback); @@ -1000,6 +1057,11 @@ int Dialog_canvas(lua_State* L) } lua_pop(L, 1); } + + // If this canvas handle keydown/up events, we set it as a focus + // stop. + if (handleKeyEvents) + widget->setFocusStop(true); } return Dialog_add_widget(L, widget); diff --git a/src/app/script/keys.cpp b/src/app/script/keys.cpp new file mode 100644 index 000000000..7214fa1eb --- /dev/null +++ b/src/app/script/keys.cpp @@ -0,0 +1,165 @@ +// Aseprite +// Copyright (C) 2023 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 "ui/keys.h" + +namespace app { +namespace script { + +// Same order that os::KeyScancode +// Based on code values of the KeyboardEvent on web code: +// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values +static const char* vkcode_to_code_table[] = { + "Unidentified", + "KeyA", + "KeyB", + "KeyC", + "KeyD", + "KeyE", + "KeyF", + "KeyG", + "KeyH", + "KeyI", + "KeyJ", + "KeyK", + "KeyL", + "KeyM", + "KeyN", + "KeyO", + "KeyP", + "KeyQ", + "KeyR", + "KeyS", + "KeyT", + "KeyU", + "KeyV", + "KeyW", + "KeyX", + "KeyY", + "KeyZ", + "Digit0", + "Digit1", + "Digit2", + "Digit3", + "Digit4", + "Digit5", + "Digit6", + "Digit7", + "Digit8", + "Digit9", + "Numpad0", + "Numpad1", + "Numpad2", + "Numpad3", + "Numpad4", + "Numpad5", + "Numpad6", + "Numpad7", + "Numpad8", + "Numpad9", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + "Escape", + "Backquote", + "Minus", + "Equal", + "Backspace", + "Tab", + "BracketLeft", + "BracketRight", + "Enter", + "Semicolon", + "Quote", + "Backslash", + nullptr, // kKeyBackslash2, + "Comma", + "Period", + "Slash", + "Space", + "Insert", + "Delete", + "Home", + "End", + "PageUp", + "PageDown", + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "ArrowDown", + "NumpadDivide", + "NumpadMultiply", + "NumpadSubtract", + "NumpadAdd", + "NumpadComma", + "NumpadEnter", + "PrintScreen", + "Pause", + nullptr, // kKeyAbntC1 + "IntlYen", + "KanaMode", + "Convert", + "NonConvert", + nullptr, // kKeyAt + nullptr, // kKeyCircumflex + nullptr, // kKeyColon2 + nullptr, // kKeyKanji + "NumpadEqual", // kKeyEqualsPad + "Backquote", + nullptr, // kKeySemicolon + nullptr, // kKeyUnknown1 + nullptr, // kKeyUnknown2 + nullptr, // kKeyUnknown3 + nullptr, // kKeyUnknown4 + nullptr, // kKeyUnknown5 + nullptr, // kKeyUnknown6 + nullptr, // kKeyUnknown7 + nullptr, // kKeyUnknown8 + "ShiftLeft", + "ShiftRight", + "ControlLeft", + "ControlRight", + "AltLeft", + "AltRight" + "MetaLeft", + "MetaRight", + "ContextMenu", + "MetaLeft", // kKeyCommand + "ScrollLock", + "NumLock", + "CapsLock", +}; + +static int vkcode_to_code_table_size = + sizeof(vkcode_to_code_table) / sizeof(vkcode_to_code_table[0]); + +const char* vkcode_to_code(const ui::KeyScancode vkcode) +{ + if (vkcode >= 0 && + vkcode < vkcode_to_code_table_size && + vkcode_to_code_table[vkcode]) { + return vkcode_to_code_table[vkcode]; + } + else { + return vkcode_to_code_table[0]; + } +} + +} // namespace script +} // namespace app diff --git a/src/app/script/keys.h b/src/app/script/keys.h new file mode 100644 index 000000000..ad7315c18 --- /dev/null +++ b/src/app/script/keys.h @@ -0,0 +1,21 @@ +// Aseprite +// Copyright (C) 2023 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_SCRIPT_KEYS_H_INCLUDED +#define APP_SCRIPT_KEYS_H_INCLUDED +#pragma once + +#include "ui/keys.h" + +namespace app { +namespace script { + + const char* vkcode_to_code(const ui::KeyScancode vkcode); + +} // namespace script +} // namespace app + +#endif