diff --git a/data/strings/en.ini b/data/strings/en.ini
index 556bbae11..68911271c 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -1667,6 +1667,7 @@ script_label = The following script:
file_label = wants to access to this file:
command_label = wants to execute the following command:
websocket_label = wants to open a WebSocket connection to this URL:
+clipboard_label = wants to access the system clipboard
dont_show_for_this_access = Don't show this specific alert again for this script
dont_show_for_this_script = Give full trust to this script
allow_execute_access = &Allow Execute Access
diff --git a/data/widgets/script_access.xml b/data/widgets/script_access.xml
index 36cf7dacf..9202be3f7 100644
--- a/data/widgets/script_access.xml
+++ b/data/widgets/script_access.xml
@@ -6,7 +6,7 @@
-
+
diff --git a/src/app/script/app_clipboard_object.cpp b/src/app/script/app_clipboard_object.cpp
index d3aff54f4..f92e63e4c 100644
--- a/src/app/script/app_clipboard_object.cpp
+++ b/src/app/script/app_clipboard_object.cpp
@@ -12,44 +12,56 @@
#include "app/script/luacpp.h"
#include "app/util/clipboard.h"
#include "clip/clip.h"
+#include "doc/mask.h"
+#include "doc/palette.h"
+#include "doc/tileset.h"
#include "engine.h"
+#include "docobj.h"
+#include "security.h"
+
namespace app { namespace script {
namespace {
struct Clipboard {};
+#define CLIPBOARD_GATE(mode) \
+ if (!ask_access(L, nullptr, mode, ResourceType::Clipboard)) \
+ return luaL_error(L, \
+ "the script doesn't have %s access to the clipboard", \
+ mode == FileAccessMode::Read ? "read" : "write");
+
int Clipboard_clear(lua_State* L)
{
+ CLIPBOARD_GATE(FileAccessMode::Read);
+
if (!clip::clear())
return luaL_error(L, "failed to clear the clipboard");
- return 1;
+ return 0;
}
-int Clipboard_is_text(lua_State* L)
+int Clipboard_hasText(lua_State* L)
{
+ CLIPBOARD_GATE(FileAccessMode::Read);
+
lua_pushboolean(L, clip::has(clip::text_format()));
return 1;
}
-int Clipboard_is_image(lua_State* L)
+int Clipboard_hasImage(lua_State* L)
{
- lua_pushboolean(L, clip::has(clip::image_format()));
- return 1;
-}
+ CLIPBOARD_GATE(FileAccessMode::Read);
-int Clipboard_is_empty(lua_State* L)
-{
- // Using clip::has(clip::empty_format()) had inconsistent results, might as well avoid false
- // positives by just checking the two formats we support
- lua_pushboolean(L, !clip::has(clip::image_format()) && !clip::has(clip::text_format()));
+ lua_pushboolean(L, clip::has(clip::image_format()));
return 1;
}
int Clipboard_get_image(lua_State* L)
{
+ CLIPBOARD_GATE(FileAccessMode::Read);
+
doc::Image* image = nullptr;
doc::Mask* mask = nullptr;
doc::Palette* palette = nullptr;
@@ -57,8 +69,12 @@ int Clipboard_get_image(lua_State* L)
const bool result =
app::Clipboard::instance()->getNativeBitmap(&image, &mask, &palette, &tileset);
- // TODO: If we get a tileset, should we convert it to an image?
- if (image == nullptr || !result)
+ if (image == nullptr) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ if (!result) // TODO: Can we have a false "nil" value without an error?
return luaL_error(L, "failed to get image from clipboard");
push_image(L, image);
@@ -67,16 +83,21 @@ int Clipboard_get_image(lua_State* L)
int Clipboard_get_text(lua_State* L)
{
- std::string str;
- if (!clip::get_text(str))
- return luaL_error(L, "failed to get text from clipboard");
+ CLIPBOARD_GATE(FileAccessMode::Read);
+
+ std::string str;
+ if (clip::get_text(str))
+ lua_pushstring(L, str.c_str());
+ else
+ lua_pushnil(L);
- lua_pushstring(L, str.c_str());
return 1;
}
int Clipboard_set_image(lua_State* L)
{
+ CLIPBOARD_GATE(FileAccessMode::Read);
+
auto* image = may_get_image_from_arg(L, 2);
if (!image)
return luaL_error(L, "invalid image");
@@ -84,7 +105,7 @@ int Clipboard_set_image(lua_State* L)
const bool result = app::Clipboard::instance()->setNativeBitmap(
image,
nullptr,
- get_current_palette(), // TODO: Not sure if there's any way to get the palette from the image
+ get_current_palette(),
nullptr,
image->maskColor() // TODO: Unsure if this is sufficient.
);
@@ -97,25 +118,131 @@ int Clipboard_set_image(lua_State* L)
int Clipboard_set_text(lua_State* L)
{
+ CLIPBOARD_GATE(FileAccessMode::Read);
+
const char* text = lua_tostring(L, 2);
- if (text != NULL && strlen(text) > 0 && !clip::set_text(text))
+ if (!clip::set_text(text ? text : ""))
return luaL_error(L, "failed to set the clipboard text to '%s'", text);
return 0;
}
+int Clipboard_get_content(lua_State* L)
+{
+ CLIPBOARD_GATE(FileAccessMode::Read);
+
+ doc::Image* image = nullptr;
+ doc::Mask* mask = nullptr;
+ doc::Palette* palette = nullptr;
+ doc::Tileset* tileset = nullptr;
+ const bool bitmapResult =
+ app::Clipboard::instance()->getNativeBitmap(&image, &mask, &palette, &tileset);
+
+ std::string text;
+ const bool clipResult = clip::get_text(text);
+
+ lua_createtable(L, 0, 5);
+
+ if (bitmapResult && image)
+ push_image(L, image);
+ else
+ lua_pushnil(L);
+ lua_setfield(L, -2, "image");
+
+ if (bitmapResult && mask)
+ push_docobj(L, mask);
+ else
+ lua_pushnil(L);
+ lua_setfield(L, -2, "mask");
+
+ if (bitmapResult && palette)
+ push_docobj(L, palette);
+ else
+ lua_pushnil(L);
+ lua_setfield(L, -2, "palette");
+
+ if (bitmapResult && tileset)
+ push_docobj(L, tileset);
+ else
+ lua_pushnil(L);
+ lua_setfield(L, -2, "tileset");
+
+ if (clipResult)
+ lua_pushstring(L, text.c_str());
+ else
+ lua_pushnil(L);
+ lua_setfield(L, -2, "text");
+
+ return 1;
+}
+
+int Clipboard_set_content(lua_State* L)
+{
+ CLIPBOARD_GATE(FileAccessMode::Read);
+
+ doc::Image* image = nullptr;
+ doc::Mask* mask = nullptr;
+ doc::Palette* palette = nullptr;
+ doc::Tileset* tileset = nullptr;
+ std::optional text = std::nullopt;
+
+ if (!lua_istable(L, 2))
+ return luaL_error(L, "app.clipboard.content must be a table");
+
+ int type = lua_getfield(L, 2, "image");
+ if (type != LUA_TNIL)
+ image = may_get_image_from_arg(L, -1);
+ lua_pop(L, 1);
+
+ type = lua_getfield(L, 2, "mask");
+ if (type != LUA_TNIL)
+ mask = may_get_docobj(L, -1);
+ lua_pop(L, 1);
+
+ type = lua_getfield(L, 2, "palette");
+ if (type != LUA_TNIL)
+ palette = may_get_docobj(L, -1);
+ lua_pop(L, 1);
+
+ type = lua_getfield(L, 2, "tileset");
+ if (type != LUA_TNIL)
+ tileset = may_get_docobj(L, -1);
+ lua_pop(L, 1);
+
+ type = lua_getfield(L, 2, "text");
+ if (type != LUA_TNIL) {
+ const char* tableText = lua_tostring(L, -1);
+ if (tableText != nullptr && strlen(tableText) > 0)
+ text = std::string(tableText);
+ }
+ lua_pop(L, 1);
+
+ if (image &&
+ !app::Clipboard::instance()->setNativeBitmap(image,
+ mask,
+ palette ? palette : get_current_palette(),
+ tileset,
+ image ? image->maskColor() : -1))
+ return luaL_error(L, "failed to set data to clipboard");
+
+ if (text != std::nullopt && !clip::set_text(*text))
+ return luaL_error(L, "failed to set the clipboard text to '%s'", (*text).c_str());
+
+ return 0;
+}
+
const luaL_Reg Clipboard_methods[] = {
{ "clear", Clipboard_clear },
{ nullptr, nullptr }
};
const Property Clipboard_properties[] = {
- { "isText", Clipboard_is_text, nullptr },
- { "isImage", Clipboard_is_image, nullptr },
- { "isEmpty", Clipboard_is_empty, nullptr },
- { "text", Clipboard_get_text, Clipboard_set_text },
- { "image", Clipboard_get_image, Clipboard_set_image },
- { nullptr, nullptr, nullptr }
+ { "hasText", Clipboard_hasText, nullptr },
+ { "hasImage", Clipboard_hasImage, nullptr },
+ { "text", Clipboard_get_text, Clipboard_set_text },
+ { "image", Clipboard_get_image, Clipboard_set_image },
+ { "content", Clipboard_get_content, Clipboard_set_content },
+ { nullptr, nullptr, nullptr }
};
} // anonymous namespace
diff --git a/src/app/script/security.cpp b/src/app/script/security.cpp
index 75b976b36..6c0522bce 100644
--- a/src/app/script/security.cpp
+++ b/src/app/script/security.cpp
@@ -22,6 +22,7 @@
#include "base/fs.h"
#include "base/sha1.h"
#include "fmt/format.h"
+#include "ui/widget_type.h"
#include "script_access.xml.h"
@@ -91,7 +92,7 @@ std::string get_script_filename(lua_State* L)
const char* source = lua_tostring(L, -1);
std::string script;
if (source && *source)
- script = source + 1;
+ script = source;
lua_pop(L, 2);
return script;
}
@@ -242,12 +243,25 @@ bool ask_access(lua_State* L,
if ((access & int(mode)) == int(mode))
return true;
- std::string allowButtonText =
- (mode == FileAccessMode::LoadLib ? Strings::script_access_allow_load_lib_access() :
- mode == FileAccessMode::OpenSocket ? Strings::script_access_allow_open_conn_access() :
- mode == FileAccessMode::Execute ? Strings::script_access_allow_execute_access() :
- mode == FileAccessMode::Write ? Strings::script_access_allow_write_access() :
- Strings::script_access_allow_read_access());
+ std::string allowButtonText;
+ switch (mode) {
+ case FileAccessMode::LoadLib:
+ allowButtonText = Strings::script_access_allow_load_lib_access();
+ break;
+ case FileAccessMode::OpenSocket:
+ allowButtonText = Strings::script_access_allow_open_conn_access();
+ break;
+ case FileAccessMode::Execute:
+ allowButtonText = Strings::script_access_allow_execute_access();
+ break;
+ case FileAccessMode::Write:
+ allowButtonText = Strings::script_access_allow_write_access();
+ break;
+ case FileAccessMode::Read:
+ allowButtonText = Strings::script_access_allow_read_access();
+ break;
+ default: return luaL_error(L, "invalid access request");
+ }
app::gen::ScriptAccess dlg;
dlg.script()->setText(script);
@@ -258,15 +272,26 @@ bool ask_access(lua_State* L,
case ResourceType::File: label = Strings::script_access_file_label(); break;
case ResourceType::Command: label = Strings::script_access_command_label(); break;
case ResourceType::WebSocket: label = Strings::script_access_websocket_label(); break;
+ case ResourceType::Clipboard: label = Strings::script_access_clipboard_label(); break;
}
dlg.fileLabel()->setText(label);
}
- dlg.file()->setText(filename);
+ if (filename && strlen(filename) > 0)
+ dlg.file()->setText(filename);
+ else
+ dlg.fileContainer()->setVisible(false);
+
dlg.allow()->setText(allowButtonText);
dlg.allow()->processMnemonicFromText();
- dlg.script()->Click.connect([&dlg] { app::launcher::open_folder(dlg.script()->text()); });
+ if (script == "internal") {
+ // Make it look like a normal label
+ dlg.script()->setType(ui::WidgetType::kLabelWidget);
+ dlg.script()->initTheme();
+ }
+ else
+ dlg.script()->Click.connect([&dlg] { app::launcher::open_folder(dlg.script()->text()); });
dlg.full()->Click.connect([&dlg, &allowButtonText]() {
if (dlg.full()->isSelected()) {
diff --git a/src/app/script/security.h b/src/app/script/security.h
index 64a904728..b2beb8121 100644
--- a/src/app/script/security.h
+++ b/src/app/script/security.h
@@ -30,6 +30,7 @@ enum class ResourceType {
File,
Command,
WebSocket,
+ Clipboard,
};
void overwrite_unsecure_functions(lua_State* L);
diff --git a/tests/scripts/app_clipboard.lua b/tests/scripts/app_clipboard.lua
index 7583873fd..ef8cc2ab0 100644
--- a/tests/scripts/app_clipboard.lua
+++ b/tests/scripts/app_clipboard.lua
@@ -5,46 +5,90 @@
dofile('./test_utils.lua')
-do -- Clipboard clearing
- app.clipboard.text = "hello world"
- expect_eq(false, app.clipboard.isEmpty)
+do -- Text clearing
+ app.clipboard.text = "clear me"
+ expect_eq(true, app.clipboard.hasText)
app.clipboard.clear()
- expect_eq(true, app.clipboard.isEmpty)
+ expect_eq(false, app.clipboard.hasText)
+end
+
+do -- Text clearing (with content)
+ app.clipboard.content = { text = "clear me 2" }
+ expect_eq(true, app.clipboard.hasText)
+ app.clipboard.clear()
+ expect_eq(false, app.clipboard.hasText)
end
do -- Text copying and access
app.clipboard.clear()
- expect_eq(false, app.clipboard.isText)
- expect_eq(false, app.clipboard.isImage)
- expect_eq(true, app.clipboard.isEmpty)
+ expect_eq(false, app.clipboard.hasText)
+ expect_eq(false, app.clipboard.hasImage)
app.clipboard.text = "hello world"
- expect_eq(true, app.clipboard.isText)
- expect_eq(false, app.clipboard.isImage)
- expect_eq(false, app.clipboard.isEmpty)
+ expect_eq(true, app.clipboard.hasText)
+ expect_eq(false, app.clipboard.hasImage)
expect_eq("hello world", app.clipboard.text)
end
+do -- Text copying and access (with .content)
+ app.clipboard.clear()
+
+ expect_eq(false, app.clipboard.hasText)
+ expect_eq(false, app.clipboard.hasImage)
+
+ app.clipboard.content = { text = "hello world 2"}
+
+ expect_eq(true, app.clipboard.hasText)
+ expect_eq(false, app.clipboard.hasImage)
+
+ expect_eq("hello world 2", app.clipboard.content.text)
+end
+
do -- Image copying and access
local sprite = Sprite{ fromFile="sprites/abcd.aseprite" }
app.clipboard.clear()
- expect_eq(false, app.clipboard.isText)
- expect_eq(false, app.clipboard.isImage)
- expect_eq(true, app.clipboard.isEmpty)
+ expect_eq(false, app.clipboard.hasText)
+ expect_eq(false, app.clipboard.hasImage)
assert(app.image ~= nil)
app.clipboard.image = app.image
- expect_eq(false, app.clipboard.isText)
- expect_eq(true, app.clipboard.isImage)
- expect_eq(false, app.clipboard.isEmpty)
+ expect_eq(false, app.clipboard.hasText)
+ expect_eq(true, app.clipboard.hasImage)
expect_eq(app.image.width, app.clipboard.image.width)
expect_eq(app.image.height, app.clipboard.image.height)
expect_eq(app.image.bytes, app.clipboard.image.bytes)
end
+
+do -- Image copying and access (with .content)
+ local sprite = Sprite{ fromFile="sprites/abcd.aseprite" }
+
+ app.clipboard.clear()
+
+ expect_eq(false, app.clipboard.hasText)
+ expect_eq(false, app.clipboard.hasImage)
+ assert(app.image ~= nil)
+
+ app.clipboard.content = {
+ image = app.image,
+ palettte = sprite.palettes[1],
+ mask = sprite.spec.transparentColor,
+ tileset = nil,
+ text = nil, -- TODO: Error when this happens
+ }
+
+ expect_eq(false, app.clipboard.hasText)
+ expect_eq(true, app.clipboard.hasImage)
+
+ local result = app.clipboard.content
+ assert(result ~= nil)
+
+ expect_eq(sprite.image.bytes, c.image.bytes)
+ -- TODO: the rest
+end