Merge branch 'main' into beta

This commit is contained in:
David Capello 2024-05-28 11:44:56 -03:00
commit 5ddce57ab0
56 changed files with 737 additions and 268 deletions

View File

@ -22,7 +22,7 @@ jobs:
if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }}
with:
key: ${{ matrix.os }}-${{ matrix.enable_ui }}-${{ matrix.build_type }}
- uses: turtlesec-no/get-ninja@main
- uses: aseprite/get-ninja@main
- uses: ilammy/msvc-dev-cmd@v1
if: runner.os == 'Windows'
- name: Workaround for windows-2022 and cmake 3.25.0

View File

@ -284,7 +284,7 @@ if(USE_SHARED_CMARK)
find_path(CMARK_INCLUDE_DIRS NAMES cmark.h)
else()
add_definitions(-DCMARK_STATIC_DEFINE)
set(CMARK_LIBRARIES cmark_static)
set(CMARK_LIBRARIES cmark)
endif()
if(REQUIRE_CURL)

2
laf

@ -1 +1 @@
Subproject commit d590eec74a0358678f25b6d72c4fc97eb542069d
Subproject commit 0d8396ea41cf4432f07b31d9002c80ef52e0565a

View File

@ -117,6 +117,10 @@ if(REQUIRE_CURL)
add_subdirectory(net)
endif()
# We need the updater library to check for updates (when
# ENABLE_UPDATER) or for the app.os object (ENABLE_SCRIPTING).
add_subdirectory(updater)
if(GEN_EXE)
add_executable(gen IMPORTED)
set_target_properties(gen PROPERTIES IMPORTED_LOCATION ${GEN_EXE})
@ -127,10 +131,6 @@ else()
set(GEN_DEP gen)
endif()
if(ENABLE_UPDATER)
add_subdirectory(updater)
endif()
if(ENABLE_STEAM)
add_subdirectory(steam)
endif()

View File

@ -161,6 +161,7 @@ if(ENABLE_SCRIPTING)
commands/cmd_run_script.cpp
script/app_command_object.cpp
script/app_fs_object.cpp
script/app_os_object.cpp
script/app_object.cpp
script/app_theme_object.cpp
script/brush_class.cpp
@ -717,6 +718,7 @@ target_link_libraries(app-lib
laf-text
ui-lib
ver-lib
updater-lib
undo
${CMARK_LIBRARIES}
${TINYXML_LIBRARY}
@ -756,10 +758,6 @@ if(ENABLE_SCRIPTING)
endif()
endif()
if(ENABLE_UPDATER)
target_link_libraries(app-lib updater-lib)
endif()
if(ENABLE_STEAM)
# We need the ENABLE_STEAM flag in main module too so AppOptions are
# equal in both modules, app-lib and main (that's why this flag is

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2023 Igara Studio S.A.
// Copyright (c) 2023-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -156,9 +156,7 @@ void ChangeBrushCommand::onExecute(Context* context)
// Create a copy of the brush (to avoid modifying the original
// brush from the AppBrushes stock)
BrushRef newBrush = std::make_shared<Brush>(*brush);
newBrush->setImage(newImg.get(),
newMsk.get());
BrushRef newBrush = brush->cloneWithExistingImages(newImg, newMsk);
contextBar->setActiveBrush(newBrush);
}
else {
@ -210,9 +208,7 @@ void ChangeBrushCommand::onExecute(Context* context)
break;
}
BrushRef newBrush = std::make_shared<Brush>(*brush);
newBrush->setImage(newImg.get(),
newMsk.get());
BrushRef newBrush = brush->cloneWithExistingImages(newImg, newMsk);
contextBar->setActiveBrush(newBrush);
}
else {
@ -297,10 +293,7 @@ void ChangeBrushCommand::onExecute(Context* context)
ImageRef newImg2(crop_image(newImg.get(), cropBounds, bg));
ImageRef newMsk2(crop_image(newMsk.get(), cropBounds, bg));
BrushRef newBrush = std::make_shared<Brush>(*brush);
newBrush->setImage(newImg.get(),
newMsk.get());
BrushRef newBrush = brush->cloneWithExistingImages(newImg2, newMsk2);
contextBar->setActiveBrush(newBrush);
}
break;

View File

@ -484,7 +484,8 @@ protected:
std::string onGetFriendlyName() const override;
private:
bool m_useUI;
bool m_showDlg;
bool m_showProgress;
doc::PixelFormat m_format;
render::Dithering m_dithering;
doc::RgbMapAlgorithm m_rgbmap;
@ -494,7 +495,8 @@ private:
ChangePixelFormatCommand::ChangePixelFormatCommand()
: Command(CommandId::ChangePixelFormat(), CmdUIOnlyFlag)
{
m_useUI = true;
m_showDlg = true;
m_showProgress = true;
m_format = IMAGE_RGB;
m_dithering = render::Dithering();
m_rgbmap = doc::RgbMapAlgorithm::DEFAULT;
@ -503,15 +505,20 @@ ChangePixelFormatCommand::ChangePixelFormatCommand()
void ChangePixelFormatCommand::onLoadParams(const Params& params)
{
m_useUI = false;
m_showDlg = false;
m_showProgress = true;
std::string format = params.get("format");
if (format == "rgb") m_format = IMAGE_RGB;
else if (format == "grayscale" ||
format == "gray") m_format = IMAGE_GRAYSCALE;
else if (format == "indexed") m_format = IMAGE_INDEXED;
else
m_useUI = true;
else {
m_showDlg = true;
}
if (params.has_param("ui"))
m_showDlg = m_showProgress = params.get_as<bool>("ui");
std::string dithering = params.get("dithering");
if (dithering == "ordered")
@ -587,7 +594,7 @@ bool ChangePixelFormatCommand::onEnabled(Context* context)
if (!sprite)
return false;
if (m_useUI)
if (m_showDlg)
return true;
if (sprite->pixelFormat() == IMAGE_INDEXED &&
@ -600,7 +607,7 @@ bool ChangePixelFormatCommand::onEnabled(Context* context)
bool ChangePixelFormatCommand::onChecked(Context* context)
{
if (m_useUI)
if (m_showDlg)
return false;
const ContextReader reader(context);
@ -622,7 +629,7 @@ void ChangePixelFormatCommand::onExecute(Context* context)
bool flatten = false;
#ifdef ENABLE_UI
if (m_useUI) {
if (context->isUIAvailable() && m_showDlg) {
ColorModeWindow window(Editor::activeEditor());
window.remapWindow();
@ -651,7 +658,7 @@ void ChangePixelFormatCommand::onExecute(Context* context)
return;
{
SpriteJob job(context, doc, Strings::color_mode_title());
SpriteJob job(context, doc, Strings::color_mode_title(), m_showProgress);
Sprite* sprite(job.sprite());
// TODO this was moved in the main UI thread because
@ -691,7 +698,7 @@ std::string ChangePixelFormatCommand::onGetFriendlyName() const
{
std::string conversion;
if (!m_useUI) {
if (!m_showDlg) {
switch (m_format) {
case IMAGE_RGB:
conversion = Strings::commands_ChangePixelFormat_RGB();

View File

@ -93,10 +93,7 @@ bool ColorQuantizationCommand::onEnabled(Context* ctx)
void ColorQuantizationCommand::onExecute(Context* ctx)
{
#ifdef ENABLE_UI
const bool ui = (params().ui() && ctx->isUIAvailable());
#endif
auto& pref = Preferences::instance();
bool withAlpha = params().withAlpha();
int maxColors = params().maxColors();
@ -183,7 +180,7 @@ void ColorQuantizationCommand::onExecute(Context* ctx)
const Palette* curPalette = site.sprite()->palette(frame);
Palette tmpPalette(frame, entries.picks());
SpriteJob job(ctx, doc, "Color Quantization");
SpriteJob job(ctx, doc, "Color Quantization", ui);
const bool newBlend = pref.experimental.newBlend();
job.startJobWithCallback(
[sprite, withAlpha, curPalette, &tmpPalette, &job, &entries,

View File

@ -1193,8 +1193,9 @@ public:
ExportSpriteSheetJob(
DocExporter& exporter,
const Site& site,
const ExportSpriteSheetParams& params)
: Job(Strings::export_sprite_sheet_generating().c_str())
const ExportSpriteSheetParams& params,
const bool showProgress)
: Job(Strings::export_sprite_sheet_generating(), showProgress)
, m_exporter(exporter)
, m_site(site)
, m_params(params) { }
@ -1373,7 +1374,9 @@ void ExportSpriteSheetCommand::onExecute(Context* context)
std::unique_ptr<Doc> newDocument;
#ifdef ENABLE_UI
if (context->isUIAvailable()) {
ExportSpriteSheetJob job(exporter, site, params);
ExportSpriteSheetJob job(exporter, site, params,
// Progress bar can be disabled with ui=false
params.ui());
job.startJob();
job.waitJob();
@ -1386,8 +1389,10 @@ void ExportSpriteSheetCommand::onExecute(Context* context)
statusbar->showTip(1000, Strings::export_sprite_sheet_generated());
// Save the exported sprite sheet as a recent file
if (newDocument->isAssociatedToFile())
if (newDocument->isAssociatedToFile() &&
should_add_file_to_recents(context, params)) {
App::instance()->recentFiles()->addRecentFile(newDocument->filename());
}
// Copy background and grid preferences
DocumentPreferences& newDocPref(

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -19,6 +19,7 @@ namespace app {
struct ExportSpriteSheetParams : public NewParams {
Param<bool> ui { this, true, "ui" };
Param<bool> recent { this, true, "recent" };
Param<bool> askOverwrite { this, true, { "askOverwrite", "ask-overwrite" } };
Param<app::SpriteSheetType> type { this, app::SpriteSheetType::None, "type" };
Param<int> columns { this, 0, "columns" };

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -36,8 +36,8 @@ namespace app {
class OpenFileJob : public Job, public IFileOpProgress {
public:
OpenFileJob(FileOp* fop)
: Job(Strings::open_file_loading().c_str())
OpenFileJob(FileOp* fop, const bool showProgress)
: Job(Strings::open_file_loading(), showProgress)
, m_fop(fop)
{
}
@ -76,6 +76,7 @@ private:
OpenFileCommand::OpenFileCommand()
: Command(CommandId::OpenFile(), CmdRecordableFlag)
, m_ui(true)
, m_repeatCheckbox(false)
, m_oneFrame(false)
, m_seqDecision(gen::SequenceDecision::ASK)
@ -86,6 +87,12 @@ void OpenFileCommand::onLoadParams(const Params& params)
{
m_filename = params.get("filename");
m_folder = params.get("folder"); // Initial folder
if (params.has_param("ui"))
m_ui = params.get_as<bool>("ui");
else
m_ui = true;
m_repeatCheckbox = params.get_as<bool>("repeat_checkbox");
m_oneFrame = params.get_as<bool>("oneframe");
@ -220,7 +227,7 @@ void OpenFileCommand::onExecute(Context* context)
m_usedFiles.push_back(fn);
}
OpenFileJob task(fop.get());
OpenFileJob task(fop.get(), m_ui);
task.showProgressWindow();
// Post-load processing, it is called from the GUI because may require user intervention.

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2020-2024 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -36,6 +36,7 @@ namespace app {
private:
std::string m_filename;
std::string m_folder;
bool m_ui;
bool m_repeatCheckbox;
bool m_oneFrame;
base::paths m_usedFiles;

View File

@ -46,8 +46,10 @@ public:
RotateJob(Context* ctx, Doc* doc,
const std::string& jobName,
int angle, const CelList& cels, bool rotateSprite)
: SpriteJob(ctx, doc, jobName)
int angle, const CelList& cels,
const bool rotateSprite,
const bool showProgress)
: SpriteJob(ctx, doc, jobName, showProgress)
, m_cels(cels)
, m_rotateSprite(rotateSprite) {
m_angle = angle;
@ -167,12 +169,18 @@ protected:
RotateCommand::RotateCommand()
: Command(CommandId::Rotate(), CmdRecordableFlag)
{
m_ui = true;
m_flipMask = false;
m_angle = 0;
}
void RotateCommand::onLoadParams(const Params& params)
{
if (params.has_param("ui"))
m_ui = params.get_as<bool>("ui");
else
m_ui = true;
std::string target = params.get("target");
m_flipMask = (target == "mask");
@ -238,7 +246,7 @@ void RotateCommand::onExecute(Context* context)
}
{
RotateJob job(context, doc, friendlyName(), m_angle, cels, rotateSprite);
RotateJob job(context, doc, friendlyName(), m_angle, cels, rotateSprite, m_ui);
job.startJob();
job.waitJob();
}

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
@ -27,6 +28,7 @@ namespace app {
std::string onGetFriendlyName() const override;
private:
bool m_ui;
bool m_flipMask;
int m_angle;
};

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -49,8 +49,8 @@ namespace app {
class SaveFileJob : public Job, public IFileOpProgress {
public:
SaveFileJob(FileOp* fop)
: Job(Strings::save_file_saving().c_str())
SaveFileJob(FileOp* fop, const bool showProgressBar)
: Job(Strings::save_file_saving(), showProgressBar)
, m_fop(fop)
{
}
@ -239,7 +239,7 @@ void SaveFileBaseCommand::saveDocumentInBackground(
if (resizeOnTheFly == ResizeOnTheFly::On)
fop->setOnTheFlyScale(scale);
SaveFileJob job(fop.get());
SaveFileJob job(fop.get(), params().ui());
job.showProgressWindow();
if (fop->hasError()) {
@ -257,7 +257,7 @@ void SaveFileBaseCommand::saveDocumentInBackground(
document->impossibleToBackToSavedState();
}
else {
if (context->isUIAvailable() && params().ui())
if (should_add_file_to_recents(context, params()))
App::instance()->recentFiles()->addRecentFile(filename);
if (markAsSaved == MarkAsSaved::On) {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2021-2022 Igara Studio S.A.
// Copyright (C) 2021-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -23,6 +23,7 @@ namespace app {
struct SaveFileParams : public NewParams {
Param<bool> ui { this, true, { "ui", "useUI" } };
Param<bool> recent { this, true, "recent" };
Param<std::string> filename { this, std::string(), "filename" };
Param<std::string> filenameFormat { this, std::string(), { "filenameFormat", "filename-format" } };
Param<std::string> tag { this, std::string(), { "tag", "frame-tag" } };

View File

@ -83,8 +83,9 @@ public:
SpriteSizeJob(Context* ctx, Doc* doc,
const int new_width,
const int new_height,
const ResizeMethod resize_method)
: SpriteJob(ctx, doc, Strings::sprite_size_title()) {
const ResizeMethod resize_method,
const bool showProgress)
: SpriteJob(ctx, doc, Strings::sprite_size_title(), showProgress) {
m_new_width = new_width;
m_new_height = new_height;
m_resize_method = resize_method;
@ -373,9 +374,7 @@ bool SpriteSizeCommand::onEnabled(Context* context)
void SpriteSizeCommand::onExecute(Context* context)
{
#ifdef ENABLE_UI
const bool ui = (params().ui() && context->isUIAvailable());
#endif
const Site site = context->activeSite();
Doc* doc = site.document();
Sprite* sprite = site.sprite();
@ -465,7 +464,7 @@ void SpriteSizeCommand::onExecute(Context* context)
new_height = std::clamp(new_height, 1, DOC_SPRITE_MAX_HEIGHT);
{
SpriteSizeJob job(context, doc, new_width, new_height, resize_method);
SpriteSizeJob job(context, doc, new_width, new_height, resize_method, ui);
job.startJob();
job.waitJob();
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -9,6 +9,7 @@
#include "app/commands/command.h"
#include "app/commands/params.h"
#include "app/context.h"
#include <map>
#include <string>
@ -152,6 +153,20 @@ namespace app {
T m_params;
};
// Common logic to know if we should add a file to recent files. We
// offer two params: "ui" and "recent", if "recent" is specified, we
// do what it says. In other case "ui" is like the default value of
// "recent", i.e. if there is ui=true, we add to recent, if there is
// ui=false, we don't add it.
template<typename T>
inline bool should_add_file_to_recents(const Context* ctx,
const T& params) {
ASSERT(ctx);
return (ctx->isUIAvailable()
&& ((params.recent.isSet() && params.recent()) ||
(!params.recent.isSet() && params.ui())));
}
} // namespace app
#endif

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -43,10 +44,10 @@ namespace crash {
// Adds a version (we don't know if the version if the latest one)
void add(doc::ObjectVersion ver) {
auto minver = std::min_element(m_vers, m_vers+2);
auto* minver = std::min_element(m_vers, m_vers+size());
if (*minver < ver) {
*minver = ver;
std::sort(m_vers, m_vers+2, std::greater<doc::ObjectVersion>());
std::sort(m_vers, m_vers+size(), std::greater<doc::ObjectVersion>());
}
}

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
@ -58,6 +58,14 @@ using namespace doc;
namespace {
// Returns true if the file was saved correctly (has the "FINE" magic
// number), so we can ignore broken versions of objects directly.
bool check_magic_number(const std::string& fn)
{
std::ifstream s(FSTREAM_PATH(fn), std::ifstream::binary);
return (read32(s) == MAGIC_NUMBER);
}
class Reader : public SubObjectsIO {
public:
Reader(const std::string& dir,
@ -83,6 +91,11 @@ public:
if (!id || !ver)
continue; // Error converting strings to ID/ver
if (!check_magic_number(base::join_path(m_dir, fn))) {
RECO_TRACE("RECO: Ignoring invalid file %s (no magic number)\n", fn.c_str());
continue;
}
ObjVersions& versions = m_objVersions[id];
versions.add(ver);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -191,8 +191,8 @@ DocDiff compare_docs(const Doc* a,
if (aLay->type() != bLay->type() ||
aLay->name() != bLay->name() ||
aLay->userData() != bLay->userData() ||
((int(aLay->flags()) & int(LayerFlags::PersistentFlagsMask)) !=
(int(bLay->flags()) & int(LayerFlags::PersistentFlagsMask))) ||
((int(aLay->flags()) & int(LayerFlags::StructuralFlagsMask)) !=
(int(bLay->flags()) & int(LayerFlags::StructuralFlagsMask))) ||
(aLay->isImage() && bLay->isImage() &&
(((const LayerImage*)aLay)->opacity() != ((const LayerImage*)bLay)->opacity())) ||
(aLay->isTilemap() && bLay->isTilemap() &&

View File

@ -33,18 +33,19 @@ int Job::runningJobs()
return g_runningJobs;
}
Job::Job(const std::string& jobName)
Job::Job(const std::string& jobName,
const bool showProgress)
{
m_last_progress = 0.0;
m_done_flag = false;
m_canceled_flag = false;
if (App::instance()->isGui()) {
if (showProgress && App::instance()->isGui()) {
m_alert_window = ui::Alert::create(
fmt::format(Strings::alerts_job_working(), jobName));
m_alert_window->addProgress();
m_timer.reset(new ui::Timer(kMonitoringPeriod, m_alert_window.get()));
m_timer = std::make_unique<ui::Timer>(kMonitoringPeriod, m_alert_window.get());
m_timer->Tick.connect(&Job::onMonitoringTick, this);
m_timer->start();
}
@ -53,7 +54,7 @@ Job::Job(const std::string& jobName)
Job::~Job()
{
if (App::instance()->isGui()) {
ASSERT(!m_timer->isRunning());
ASSERT(!m_timer || !m_timer->isRunning());
if (m_alert_window)
m_alert_window->closeWindow(NULL);

View File

@ -24,7 +24,10 @@ namespace app {
public:
static int runningJobs();
Job(const std::string& jobName);
Job(const std::string& jobName, bool showProgress);
Job() = delete;
Job(const Job&) = delete;
Job& operator==(const Job&) = delete;
virtual ~Job();
// Starts the job calling onJob() event in another thread and
@ -68,12 +71,6 @@ namespace app {
bool m_done_flag;
bool m_canceled_flag;
std::exception_ptr m_error;
// these methods are privated and not defined
Job();
Job(const Job&);
Job& operator==(const Job&);
};
} // namespace app

View File

@ -10,6 +10,6 @@
// Increment this value if the scripting API is modified between two
// released Aseprite versions.
#define API_VERSION 27
#define API_VERSION 28
#endif

View File

@ -0,0 +1,121 @@
// 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/script/engine.h"
#include "app/script/luacpp.h"
#include "base/config.h"
#include "base/platform.h"
#include "updater/user_agent.h"
namespace app {
namespace script {
namespace {
struct AppOS { };
int AppOS_get_name(lua_State* L)
{
#if LAF_WINDOWS
lua_pushstring(L, "Windows");
#elif LAF_MACOS
lua_pushstring(L, "macOS");
#elif LAF_LINUX
lua_pushstring(L, "Linux");
#else
lua_pushnil(L);
#endif
return 1;
}
int AppOS_get_version(lua_State* L)
{
base::Platform p = base::get_platform();
push_version(L, p.osVer);
return 1;
}
int AppOS_get_fullName(lua_State* L)
{
lua_pushstring(L, updater::getFullOSString().c_str());
return 1;
}
int AppOS_get_windows(lua_State* L)
{
lua_pushboolean(L, base::Platform::os == base::Platform::OS::Windows);
return 1;
}
int AppOS_get_macos(lua_State* L)
{
lua_pushboolean(L, base::Platform::os == base::Platform::OS::macOS);
return 1;
}
int AppOS_get_linux(lua_State* L)
{
lua_pushboolean(L, base::Platform::os == base::Platform::OS::Linux);
return 1;
}
int AppOS_get_x64(lua_State* L)
{
lua_pushboolean(L, base::Platform::arch == base::Platform::Arch::x64);
return 1;
}
int AppOS_get_x86(lua_State* L)
{
lua_pushboolean(L, base::Platform::arch == base::Platform::Arch::x86);
return 1;
}
int AppOS_get_arm64(lua_State* L)
{
lua_pushboolean(L, base::Platform::arch == base::Platform::Arch::arm64);
return 1;
}
const Property AppOS_properties[] = {
{ "name", AppOS_get_name, nullptr },
{ "version", AppOS_get_version, nullptr },
{ "fullName", AppOS_get_fullName, nullptr },
{ "windows", AppOS_get_windows, nullptr },
{ "macos", AppOS_get_macos, nullptr },
{ "linux", AppOS_get_linux, nullptr },
{ "x64", AppOS_get_x64, nullptr },
{ "x86", AppOS_get_x86, nullptr },
{ "arm64", AppOS_get_arm64, nullptr },
{ nullptr, nullptr, nullptr }
};
const luaL_Reg AppOS_methods[] = {
{ nullptr, nullptr }
};
} // anonymous namespace
DEF_MTNAME(AppOS);
void register_app_os_object(lua_State* L)
{
REG_CLASS(L, AppOS);
REG_CLASS_PROPERTIES(L, AppOS);
lua_getglobal(L, "app");
lua_pushstring(L, "os");
push_new<AppOS>(L);
lua_rawset(L, -3);
lua_pop(L, 1);
}
} // namespace script
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -36,7 +36,7 @@ BrushRef Brush_new(lua_State* L, int index)
if (auto brush2 = may_get_obj<BrushObj>(L, index)) {
ASSERT(brush2->brush);
if (brush2->brush)
brush.reset(new Brush(*brush2->brush));
brush = brush2->brush->cloneWithNewImages();
}
else if (auto image = may_get_image_from_arg(L, index)) {
if (image) {

View File

@ -165,6 +165,7 @@ int os_clock(lua_State* L)
void register_app_object(lua_State* L);
void register_app_pixel_color_object(lua_State* L);
void register_app_fs_object(lua_State* L);
void register_app_os_object(lua_State* L);
void register_app_command_object(lua_State* L);
void register_app_preferences_object(lua_State* L);
void register_json_object(lua_State* L);
@ -259,6 +260,7 @@ Engine::Engine()
register_app_object(L);
register_app_pixel_color_object(L);
register_app_fs_object(L);
register_app_os_object(L);
register_app_command_object(L);
register_app_preferences_object(L);
register_json_object(L);

View File

@ -16,8 +16,9 @@
namespace app {
SpriteJob::SpriteJob(Context* ctx, Doc* doc,
const std::string& jobName)
: Job(jobName)
const std::string& jobName,
const bool showProgress)
: Job(jobName, showProgress)
, m_doc(doc)
, m_sprite(doc->sprite())
, m_tx(Tx::DontLockDoc, ctx, doc, jobName, ModifyDocument)

View File

@ -37,7 +37,8 @@ class SpriteJob : public Job,
public render::TaskDelegate {
public:
SpriteJob(Context* ctx, Doc* doc,
const std::string& jobName);
const std::string& jobName,
const bool showProgress);
~SpriteJob();
Doc* document() const { return m_doc; }

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
@ -564,7 +564,7 @@ public:
// point so we can restore it when erasing a point because of
// pixel-perfect. So we set the following flag to indicate this, and
// use it in doTransformPoint.
m_saveStrokeArea = (c == m_pts.size() - 1 && !m_retainedTracePolicyLast);
m_saveStrokeArea = (c == m_pts.size() - 1);
if (m_saveStrokeArea) {
clearPointshapeStrokePtAreas();
setLastPtIndex(c);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -44,7 +44,8 @@ using namespace filters;
ToolLoopManager::ToolLoopManager(ToolLoop* toolLoop)
: m_toolLoop(toolLoop)
, m_canceled(false)
, m_brush0(*toolLoop->getBrush())
, m_brushSize0(toolLoop->getBrush()->size())
, m_brushAngle0(toolLoop->getBrush()->angle())
, m_dynamics(toolLoop->getDynamics())
{
}
@ -202,6 +203,12 @@ void ToolLoopManager::movement(Pointer pointer)
doLoopStep(false);
}
void ToolLoopManager::disableMouseStabilizer()
{
// Disable mouse stabilizer for the current ToolLoopManager
m_dynamics.stabilizer = false;
}
void ToolLoopManager::doLoopStep(bool lastStep)
{
// Original set of points to interwine (original user stroke,
@ -358,8 +365,8 @@ Stroke::Pt ToolLoopManager::getSpriteStrokePt(const Pointer& pointer)
{
// Convert the screen point to a sprite point
Stroke::Pt spritePoint = pointer.point();
spritePoint.size = m_brush0.size();
spritePoint.angle = m_brush0.angle();
spritePoint.size = m_brushSize0;
spritePoint.angle = m_brushAngle0;
// Center the input to some grid point if needed
snapToGrid(spritePoint);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -78,6 +78,10 @@ public:
// Should be called each time the user moves the mouse inside the editor.
void movement(Pointer pointer);
// Should be called when Shift+brush tool is used to disable stabilizer
// on the line preview
void disableMouseStabilizer();
const Pointer& lastPointer() const { return m_lastPointer; }
private:
@ -95,7 +99,8 @@ private:
Pointer m_lastPointer;
gfx::Region m_dirtyArea;
gfx::Region m_nextDirtyArea;
doc::Brush m_brush0;
const int m_brushSize0;
const int m_brushAngle0;
DynamicsOptions m_dynamics;
gfx::PointF m_stabilizerCenter;
};

View File

@ -361,9 +361,10 @@ BrushPopup::BrushPopup()
m_box.addChild(new Separator("", HORIZONTAL));
for (const auto& brush : brushes.getStandardBrushes()) {
auto* theme = SkinTheme::get(this);
m_standardBrushes.addItem(
new SelectBrushItem(
BrushSlot(BrushSlot::Flags::BrushType, brush)), "standard_brush");
BrushSlot(BrushSlot::Flags::BrushType, brush)), theme->styles.standardBrush());
}
m_standardBrushes.setTransparent(true);
@ -398,6 +399,7 @@ void BrushPopup::setBrush(Brush* brush)
void BrushPopup::regenerate(ui::Display* display,
const gfx::Point& pos)
{
auto* theme = SkinTheme::get(this);
auto& brushSlots = App::instance()->brushes().getBrushSlots();
if (m_customBrushes) {
@ -428,11 +430,11 @@ void BrushPopup::regenerate(ui::Display* display,
}
m_customBrushes->addItem(new SelectBrushItem(brush, slot));
m_customBrushes->addItem(new BrushShortcutItem(shortcut, slot));
m_customBrushes->addItem(new BrushOptionsItem(this, slot), "buttonset_item_icon_mono");
m_customBrushes->addItem(new BrushOptionsItem(this, slot), theme->styles.buttonsetItemIconMono());
}
m_customBrushes->addItem(new NewCustomBrushItem, 2, 1);
m_customBrushes->addItem(new NewBrushOptionsItem, "buttonset_item_icon_mono");
m_customBrushes->addItem(new NewBrushOptionsItem, theme->styles.buttonsetItemIconMono());
m_customBrushes->setExpansive(true);
m_customBrushes->initTheme();
m_box.addChild(m_customBrushes);
@ -467,7 +469,9 @@ os::SurfaceRef BrushPopup::createSurfaceForBrush(const BrushRef& origBrush,
BrushRef brush = origBrush;
if (brush) {
if (brush->type() != kImageBrushType && brush->size() > kMaxSize) {
brush.reset(new Brush(*brush));
// Clone with shared images, as setSize() will re-create the
// images and the brush is no kImageBrushType anyway.
brush = brush->cloneWithSharedImages();
brush->setSize(kMaxSize);
}
// Show the original image in the popup (without the image colors

View File

@ -208,60 +208,50 @@ ButtonSet::ButtonSet(int columns)
initTheme();
}
ButtonSet::Item* ButtonSet::addItem(const std::string& text, const char* styleId)
ButtonSet::Item* ButtonSet::addItem(const std::string& text, ui::Style* style)
{
return addItem(text, 1, 1, styleId);
return addItem(text, 1, 1, style);
}
ButtonSet::Item* ButtonSet::addItem(const std::string& text, int hspan, int vspan, const char* styleId)
ButtonSet::Item* ButtonSet::addItem(const std::string& text, int hspan, int vspan, ui::Style* style)
{
Item* item = new Item();
item->setText(text);
addItem(item, hspan, vspan, styleId);
addItem(item, hspan, vspan, style);
return item;
}
ButtonSet::Item* ButtonSet::addItem(const skin::SkinPartPtr& icon, const char* styleId)
ButtonSet::Item* ButtonSet::addItem(const skin::SkinPartPtr& icon, ui::Style* style)
{
return addItem(icon, 1, 1, styleId);
return addItem(icon, 1, 1, style);
}
ButtonSet::Item* ButtonSet::addItem(const skin::SkinPartPtr& icon, int hspan, int vspan, const char* styleId)
ButtonSet::Item* ButtonSet::addItem(const skin::SkinPartPtr& icon, int hspan, int vspan, ui::Style* style)
{
Item* item = new Item();
item->setIcon(icon);
addItem(item, hspan, vspan, styleId);
addItem(item, hspan, vspan, style);
return item;
}
ButtonSet::Item* ButtonSet::addItem(Item* item, const char* styleId)
ButtonSet::Item* ButtonSet::addItem(Item* item, ui::Style* style)
{
return addItem(item, 1, 1, styleId);
return addItem(item, 1, 1, style);
}
ButtonSet::Item* ButtonSet::addItem(Item* item, int hspan, int vspan, const char* styleIdStr)
ButtonSet::Item* ButtonSet::addItem(Item* item, int hspan, int vspan, ui::Style* style)
{
std::string styleId;
if (styleIdStr)
styleId = styleIdStr;
item->InitTheme.connect(
[item, styleId] {
auto theme = SkinTheme::get(item);
ui::Style* style;
if (!styleId.empty()) {
style = theme->getStyleById(styleId);
if (!style)
throw base::Exception(fmt::format("Style {} not found", styleId));
}
else {
style = theme->styles.buttonsetItemIcon();
[item, style] {
ui::Style* s = style;
if (!s) {
auto* theme = SkinTheme::get(item);
s = theme->styles.buttonsetItemIcon();
if (!item->text().empty()) {
style = (item->icon() ? theme->styles.buttonsetItemTextTopIconBottom() :
theme->styles.buttonsetItemText());
s = (item->icon() ? theme->styles.buttonsetItemTextTopIconBottom() :
theme->styles.buttonsetItemText());
}
}
item->setStyle(style);
item->setStyle(s);
}
);
addChildInCell(item, hspan, vspan, HORIZONTAL | VERTICAL);

View File

@ -44,12 +44,12 @@ namespace app {
ButtonSet(int columns);
Item* addItem(const std::string& text, const char* styleId);
Item* addItem(const std::string& text, int hspan = 1, int vspan = 1, const char* styleId = nullptr);
Item* addItem(const skin::SkinPartPtr& icon, const char* styleId);
Item* addItem(const skin::SkinPartPtr& icon, int hspan = 1, int vspan = 1, const char* styleId = nullptr);
Item* addItem(Item* item, const char* styleId);
Item* addItem(Item* item, int hspan = 1, int vspan = 1, const char* styleId = nullptr);
Item* addItem(const std::string& text, ui::Style* style);
Item* addItem(const std::string& text, int hspan = 1, int vspan = 1, ui::Style* style = nullptr);
Item* addItem(const skin::SkinPartPtr& icon, ui::Style* style);
Item* addItem(const skin::SkinPartPtr& icon, int hspan = 1, int vspan = 1, ui::Style* style = nullptr);
Item* addItem(Item* item, ui::Style* style);
Item* addItem(Item* item, int hspan = 1, int vspan = 1, ui::Style* style = nullptr);
Item* getItem(int index);
int getItemIndex(const Item* item) const;

View File

@ -185,7 +185,7 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
m_instance = this;
auto& pref = Preferences::instance();
auto theme = SkinTheme::get(this);
auto* theme = SkinTheme::get(this);
auto item = m_editPal.addItem("");
item->InitTheme.connect(
@ -195,9 +195,9 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
SkinTheme::instance()->styles.palEditLock());
item->setStyle(style);
});
m_buttons.addItem(theme->parts.palSort(), "pal_button");
m_buttons.addItem(theme->parts.palPresets(), "pal_button");
m_buttons.addItem(theme->parts.palOptions(), "pal_button");
m_buttons.addItem(theme->parts.palSort(), theme->styles.palButton());
m_buttons.addItem(theme->parts.palPresets(), theme->styles.palButton());
m_buttons.addItem(theme->parts.palOptions(), theme->styles.palButton());
item = m_tilesButton.addItem(theme->parts.tiles());
item->InitTheme.connect(
[this, item]() {
@ -213,9 +213,9 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
1 == int(TilesetMode::Auto) &&
2 == int(TilesetMode::Stack), "Tileset mode buttons doesn't match TilesetMode enum values");
m_tilesetModeButtons.addItem(theme->parts.tilesManual(), "pal_button");
m_tilesetModeButtons.addItem(theme->parts.tilesAuto(), "pal_button");
m_tilesetModeButtons.addItem(theme->parts.tilesStack(), "pal_button");
m_tilesetModeButtons.addItem(theme->parts.tilesManual(), theme->styles.palButton());
m_tilesetModeButtons.addItem(theme->parts.tilesAuto(), theme->styles.palButton());
m_tilesetModeButtons.addItem(theme->parts.tilesStack(), theme->styles.palButton());
m_tilesetMode = pref.colorBar.defaultTilesetMode();
setTilesetMode(m_tilesetMode);

View File

@ -164,7 +164,8 @@ public:
, m_brushes(App::instance()->brushes()) {
SkinPartPtr part(new SkinPart);
part->setBitmap(0, BrushPopup::createSurfaceForBrush(BrushRef(nullptr)));
addItem(part, "brush_type");
auto* theme = SkinTheme::get(this);
addItem(part, theme->styles.brushType());
m_popupWindow.Open.connect(
[this]{
@ -385,8 +386,8 @@ protected:
class ContextBar::PaintBucketSettingsField : public ButtonSet {
public:
PaintBucketSettingsField() : ButtonSet(1) {
auto theme = SkinTheme::get(this);
addItem(theme->parts.timelineGear(), "context_bar_button");
auto* theme = SkinTheme::get(this);
addItem(theme->parts.timelineGear(), theme->styles.contextBarButton());
}
protected:
@ -472,8 +473,8 @@ class ContextBar::InkTypeField : public ButtonSet {
public:
InkTypeField(ContextBar* owner) : ButtonSet(1)
, m_owner(owner) {
auto theme = SkinTheme::get(this);
addItem(theme->parts.inkSimple(), "ink_type");
auto* theme = SkinTheme::get(this);
addItem(theme->parts.inkSimple(), theme->styles.inkType());
}
void setInkType(InkType inkType) {
@ -872,7 +873,8 @@ class ContextBar::PivotField : public ButtonSet {
public:
PivotField()
: ButtonSet(1) {
addItem(SkinTheme::get(this)->parts.pivotCenter(), "pivot_field");
auto* theme = SkinTheme::get(this);
addItem(SkinTheme::get(this)->parts.pivotCenter(), theme->styles.pivotField());
m_pivotConn = Preferences::instance().selection.pivotPosition.AfterChange.connect(
[this]{ onPivotChange(); });
@ -885,22 +887,22 @@ private:
void onItemChange(Item* item) override {
ButtonSet::onItemChange(item);
auto theme = SkinTheme::get(this);
auto* theme = SkinTheme::get(this);
gfx::Rect bounds = this->bounds();
Menu menu;
CheckBox visible(Strings::context_bar_default_display_pivot());
HBox box;
ButtonSet buttonset(3);
buttonset.addItem(theme->parts.pivotNorthwest(), "pivot_dir");
buttonset.addItem(theme->parts.pivotNorth(), "pivot_dir");
buttonset.addItem(theme->parts.pivotNortheast(), "pivot_dir");
buttonset.addItem(theme->parts.pivotWest(), "pivot_dir");
buttonset.addItem(theme->parts.pivotCenter(), "pivot_dir");
buttonset.addItem(theme->parts.pivotEast(), "pivot_dir");
buttonset.addItem(theme->parts.pivotSouthwest(), "pivot_dir");
buttonset.addItem(theme->parts.pivotSouth(), "pivot_dir");
buttonset.addItem(theme->parts.pivotSoutheast(), "pivot_dir");
buttonset.addItem(theme->parts.pivotNorthwest(), theme->styles.pivotDir());
buttonset.addItem(theme->parts.pivotNorth(), theme->styles.pivotDir());
buttonset.addItem(theme->parts.pivotNortheast(), theme->styles.pivotDir());
buttonset.addItem(theme->parts.pivotWest(), theme->styles.pivotDir());
buttonset.addItem(theme->parts.pivotCenter(), theme->styles.pivotDir());
buttonset.addItem(theme->parts.pivotEast(), theme->styles.pivotDir());
buttonset.addItem(theme->parts.pivotSouthwest(), theme->styles.pivotDir());
buttonset.addItem(theme->parts.pivotSouth(), theme->styles.pivotDir());
buttonset.addItem(theme->parts.pivotSoutheast(), theme->styles.pivotDir());
box.addChild(&buttonset);
menu.addChild(&visible);
@ -1154,7 +1156,8 @@ public:
DynamicsField(ContextBar* ctxBar)
: ButtonSet(1)
, m_ctxBar(ctxBar) {
addItem(SkinTheme::get(this)->parts.dynamics(), "dynamics_field");
auto* theme = SkinTheme::get(this);
addItem(theme->parts.dynamics(), theme->styles.dynamicsField());
loadDynamicsPref();
initTheme();
@ -1381,10 +1384,10 @@ protected:
class ContextBar::GradientTypeField : public ButtonSet {
public:
GradientTypeField() : ButtonSet(2) {
auto theme = SkinTheme::get(this);
auto* theme = SkinTheme::get(this);
addItem(theme->parts.linearGradient(), "context_bar_button");
addItem(theme->parts.radialGradient(), "context_bar_button");
addItem(theme->parts.linearGradient(), theme->styles.contextBarButton());
addItem(theme->parts.radialGradient(), theme->styles.contextBarButton());
setSelectedItem(0);
}
@ -1404,10 +1407,10 @@ public:
class ContextBar::DropPixelsField : public ButtonSet {
public:
DropPixelsField() : ButtonSet(2) {
auto theme = SkinTheme::get(this);
auto* theme = SkinTheme::get(this);
addItem(theme->parts.dropPixelsOk(), "context_bar_button");
addItem(theme->parts.dropPixelsCancel(), "context_bar_button");
addItem(theme->parts.dropPixelsOk(), theme->styles.contextBarButton());
addItem(theme->parts.dropPixelsCancel(), theme->styles.contextBarButton());
setOfferCapture(false);
}
@ -1529,10 +1532,10 @@ class ContextBar::SymmetryField : public ButtonSet {
public:
SymmetryField() : ButtonSet(3) {
setMultiMode(MultiMode::Set);
auto theme = SkinTheme::get(this);
addItem(theme->parts.horizontalSymmetry(), "symmetry_field");
addItem(theme->parts.verticalSymmetry(), "symmetry_field");
addItem("...", "symmetry_options");
auto* theme = SkinTheme::get(this);
addItem(theme->parts.horizontalSymmetry(), theme->styles.symmetryField());
addItem(theme->parts.verticalSymmetry(), theme->styles.symmetryField());
addItem("...", theme->styles.symmetryOptions());
}
void setupTooltips(TooltipManager* tooltipManager) {
@ -1654,7 +1657,7 @@ public:
, m_combobox(this)
, m_action(2)
{
auto theme = SkinTheme::get(this);
auto* theme = SkinTheme::get(this);
m_sel.addItem(Strings::context_bar_all());
m_sel.addItem(Strings::context_bar_none());
@ -1667,8 +1670,8 @@ public:
m_combobox.setExpansive(true);
m_combobox.setMinSize(gfx::Size(256*guiscale(), 0));
m_action.addItem(theme->parts.iconUserData(), "buttonset_item_icon_mono");
m_action.addItem(theme->parts.iconClose(), "buttonset_item_icon_mono");
m_action.addItem(theme->parts.iconUserData(), theme->styles.buttonsetItemIconMono());
m_action.addItem(theme->parts.iconClose(), theme->styles.buttonsetItemIconMono());
m_action.ItemChange.connect(
[this](ButtonSet::Item* item){
onAction(m_action.selectedItem());

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2023 Igara Studio S.A.
// Copyright (C) 2020-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -165,7 +165,9 @@ private:
break;
auto mouseMsg = static_cast<MouseMessage*>(msg);
const gfx::Rect rc = bounds();
gfx::Rect rc = bounds();
rc.shrink(border());
rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale());
float u = (mouseMsg->position().x - rc.x) / float(rc.w);
u = std::clamp(u, 0.0f, 1.0f);
switch (capture) {

View File

@ -129,6 +129,12 @@ void DrawingState::initToolLoop(Editor* editor,
editor->captureMouse();
}
void DrawingState::disableMouseStabilizer()
{
ASSERT(m_toolLoopManager);
m_toolLoopManager->disableMouseStabilizer();
}
void DrawingState::sendMovementToToolLoop(const tools::Pointer& pointer)
{
ASSERT(m_toolLoopManager);

View File

@ -60,6 +60,10 @@ namespace app {
const ui::MouseMessage* msg,
const tools::Pointer& pointer);
// Used to disable the current ToolLoopManager's stabilizer
// when Shift+brush tool is used to paint a line
void disableMouseStabilizer();
// Used to send a movement() to the ToolLoopManager when
// Shift+brush tool is used to paint a line.
void sendMovementToToolLoop(const tools::Pointer& pointer);

View File

@ -696,6 +696,9 @@ bool StandbyState::checkStartDrawingStraightLine(Editor* editor,
pointer ? pointer->type(): PointerType::Unknown,
pointer ? pointer->pressure(): 0.0f));
if (drawingState) {
// Disable stabilizer so that it does not affect the line preview
drawingState->disableMouseStabilizer();
drawingState->sendMovementToToolLoop(
tools::Pointer(
pointer ? pointer->point(): editor->screenToEditor(editor->mousePosInDisplay()),

View File

@ -45,6 +45,7 @@
#include "app/ui_context.h"
#include "app/util/expand_cel_canvas.h"
#include "app/util/layer_utils.h"
#include "doc/brush.h"
#include "doc/cel.h"
#include "doc/image.h"
#include "doc/layer.h"
@ -189,6 +190,33 @@ public:
ASSERT(m_ink);
ASSERT(m_controller);
// If the user right-clicks with a custom/image brush we change
// the image's colors of the brush to the background color.
//
// This is different from SwitchColors that makes a new brush
// switching fg <-> bg colors, so here we have some extra
// functionality with custom brushes (quickly convert the custom
// brush with a plain color, or in other words, replace the custom
// brush area with the background color).
if (m_brush->type() == kImageBrushType && m_button == Right) {
// We've to recalculate the background color to use for the
// brush using the specific brush image pixel format/color mode,
// as we cannot use m_primaryColor or m_bgColor here because
// those are in the sprite pixel format/color mode.
const color_t brushColor =
color_utils::color_for_target_mask(
Preferences::instance().colorBar.bgColor(),
ColorTarget(ColorTarget::TransparentLayer,
m_brush->image()->pixelFormat(),
-1));
// Clone the brush with new images to avoid modifying the
// current brush used in left-click / brush preview.
BrushRef newBrush = m_brush->cloneWithNewImages();
newBrush->setImageColor(Brush::ImageColor::BothColors, brushColor);
m_brush = newBrush;
}
if (m_tilesMode) {
// Use FloodFillPointShape or TilePointShape in tiles mode
if (!m_pointShape->isFloodFill()) {

View File

@ -23,12 +23,12 @@ using namespace ui;
SelectionModeField::SelectionModeField()
: ButtonSet(4)
{
auto theme = SkinTheme::get(this);
auto* theme = SkinTheme::get(this);
addItem(theme->parts.selectionReplace(), "selection_mode");
addItem(theme->parts.selectionAdd(), "selection_mode");
addItem(theme->parts.selectionSubtract(), "selection_mode");
addItem(theme->parts.selectionIntersect(), "selection_mode");
addItem(theme->parts.selectionReplace(), theme->styles.selectionMode());
addItem(theme->parts.selectionAdd(), theme->styles.selectionMode());
addItem(theme->parts.selectionSubtract(), theme->styles.selectionMode());
addItem(theme->parts.selectionIntersect(), theme->styles.selectionMode());
setSelectedItem((int)Preferences::instance().selection.mode());
initTheme();

View File

@ -42,13 +42,13 @@ enum AniAction {
AniControls::AniControls(TooltipManager* tooltipManager)
: ButtonSet(5)
{
auto theme = SkinTheme::get(this);
auto* theme = SkinTheme::get(this);
addItem(theme->parts.aniFirst(), "ani_button");
addItem(theme->parts.aniPrevious(), "ani_button");
addItem(theme->parts.aniPlay(), "ani_button");
addItem(theme->parts.aniNext(), "ani_button");
addItem(theme->parts.aniLast(), "ani_button");
addItem(theme->parts.aniFirst(), theme->styles.aniButton());
addItem(theme->parts.aniPrevious(), theme->styles.aniButton());
addItem(theme->parts.aniPlay(), theme->styles.aniButton());
addItem(theme->parts.aniNext(), theme->styles.aniButton());
addItem(theme->parts.aniLast(), theme->styles.aniButton());
ItemChange.connect([this]{ onClickButton(); });
setTriggerOnMouseUp(true);

View File

@ -24,21 +24,19 @@
#include <climits>
#include <cmath>
#include <limits>
#include <vector>
namespace doc {
namespace algorithm {
struct FLOODED_LINE { // store segments which have been flooded
short flags; // status of the segment
short lpos, rpos; // left and right ends of segment
short y; // y coordinate of the segment
char flags; // status of the segment
int lpos, rpos; // left and right ends of segment
int y; // y coordinate of the segment
int next; // linked list if several per line
};
/* Note: a 'short' is not sufficient for 'next' above in some corner cases. */
static std::vector<FLOODED_LINE> flood_buf;
static int flood_count; /* number of flooded segments */
@ -415,8 +413,8 @@ void floodfill(const Image* image,
FLOODED_LINE* p = (FLOODED_LINE*)&flood_buf[0];
for (int c=0; c<flood_count; c++) {
p[c].flags = 0;
p[c].lpos = SHRT_MAX;
p[c].rpos = SHRT_MIN;
p[c].lpos = std::numeric_limits<int>::max();
p[c].rpos = std::numeric_limits<int>::min();
p[c].y = y;
p[c].next = 0;
}

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -48,32 +48,43 @@ Brush::Brush(BrushType type, int size, int angle)
regenerate();
}
Brush::Brush(const Brush& brush)
{
m_type = brush.m_type;
m_size = brush.m_size;
m_angle = brush.m_angle;
m_image = brush.m_image;
m_maskBitmap = brush.m_maskBitmap;
m_pattern = brush.m_pattern;
m_patternOrigin = brush.m_patternOrigin;
m_gen = 0;
regenerate();
}
Brush::~Brush()
{
clean();
}
void Brush::setType(BrushType type)
BrushRef Brush::cloneWithSharedImages() const
{
m_type = type;
if (m_type != kImageBrushType)
regenerate();
BrushRef newBrush = std::make_shared<Brush>();
newBrush->copyFieldsFromBrush(*this);
return newBrush;
}
BrushRef Brush::cloneWithNewImages() const
{
BrushRef newBrush = std::make_shared<Brush>();
newBrush->copyFieldsFromBrush(*this);
if (newBrush->m_image)
newBrush->m_image.reset(Image::createCopy(newBrush->m_image.get()));
if (newBrush->m_maskBitmap)
newBrush->m_maskBitmap.reset(Image::createCopy(newBrush->m_maskBitmap.get()));
return newBrush;
}
BrushRef Brush::cloneWithExistingImages(const ImageRef& image,
const ImageRef& maskBitmap) const
{
BrushRef newBrush = std::make_shared<Brush>();
newBrush->copyFieldsFromBrush(*this);
newBrush->m_image = image;
if (maskBitmap)
newBrush->m_maskBitmap = maskBitmap;
else
clean();
newBrush->regenerateMaskBitmap();
newBrush->resetBounds();
return newBrush;
}
void Brush::setSize(int size)
@ -95,16 +106,8 @@ void Brush::setImage(const Image* image,
m_image.reset(Image::createCopy(image));
if (maskBitmap)
m_maskBitmap.reset(Image::createCopy(maskBitmap));
else {
int w = image->width();
int h = image->height();
m_maskBitmap.reset(Image::create(IMAGE_BITMAP, w, h));
LockImageBits<BitmapTraits> bits(m_maskBitmap.get());
auto pos = bits.begin();
for (int v=0; v<h; ++v)
for (int u=0; u<w; ++u, ++pos)
*pos = (get_pixel(image, u, v) != image->maskColor());
}
else
regenerateMaskBitmap();
m_backupImage.reset();
m_mainColor.reset();
@ -234,7 +237,8 @@ static void replace_image_colors_indexed(
}
}
void Brush::setImageColor(ImageColor imageColor, color_t color)
void Brush::setImageColor(const ImageColor imageColor,
const color_t color)
{
ASSERT(m_image);
if (!m_image)
@ -249,10 +253,13 @@ void Brush::setImageColor(ImageColor imageColor, color_t color)
switch (imageColor) {
case ImageColor::MainColor:
m_mainColor = color_t(color);
m_mainColor = color;
break;
case ImageColor::BackgroundColor:
m_bgColor = color_t(color);
m_bgColor = color;
break;
case ImageColor::BothColors:
m_mainColor = m_bgColor = color;
break;
}
@ -283,8 +290,11 @@ void Brush::setImageColor(ImageColor imageColor, color_t color)
void Brush::resetImageColors()
{
if (m_backupImage)
if (m_backupImage) {
m_image.reset(Image::createCopy(m_backupImage.get()));
m_mainColor.reset();
m_bgColor.reset();
}
}
void Brush::setCenter(const gfx::Point& center)
@ -376,6 +386,22 @@ void Brush::regenerate()
}
}
void Brush::regenerateMaskBitmap()
{
ASSERT(m_image);
if (!m_image)
return;
int w = m_image->width();
int h = m_image->height();
m_maskBitmap.reset(Image::create(IMAGE_BITMAP, w, h));
LockImageBits<BitmapTraits> bits(m_maskBitmap.get());
auto pos = bits.begin();
for (int v=0; v<h; ++v)
for (int u=0; u<w; ++u, ++pos)
*pos = (get_pixel(m_image.get(), u, v) != m_image->maskColor());
}
void Brush::resetBounds()
{
m_center = gfx::Point(std::max(0, m_image->width()/2),
@ -385,4 +411,19 @@ void Brush::resetBounds()
m_image->height()));
}
void Brush::copyFieldsFromBrush(const Brush& brush)
{
m_type = brush.m_type;
m_size = brush.m_size;
m_angle = brush.m_angle;
m_image = brush.m_image;
m_maskBitmap = brush.m_maskBitmap;
m_bounds = brush.m_bounds;
m_center = brush.m_center;
m_pattern = brush.m_pattern;
m_patternOrigin = brush.m_patternOrigin;
m_patternImage = brush.m_patternImage;
m_gen = 0;
}
} // namespace doc

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -22,18 +22,34 @@
namespace doc {
class Brush;
using BrushRef = std::shared_ptr<Brush>;
class Brush {
public:
static const int kMinBrushSize = 1;
static const int kMaxBrushSize = 64;
enum class ImageColor { MainColor, BackgroundColor };
enum class ImageColor { MainColor, BackgroundColor, BothColors };
Brush();
Brush(BrushType type, int size, int angle);
Brush(const Brush& brush);
~Brush();
// Don't offer copy constructor/operator, use clone*() functions
// instead.
Brush(const Brush&) = delete;
Brush& operator=(const Brush&) = delete;
// Cloned brushes can share the same image until
// setSize()/Angle()/etc. (regenerate()) is called for the new
// brush. In that case the original brush and the cloned one will
// have a different image after all.
BrushRef cloneWithSharedImages() const;
BrushRef cloneWithNewImages() const;
BrushRef cloneWithExistingImages(const ImageRef& image,
const ImageRef& maskBitmap) const;
BrushType type() const { return m_type; }
int size() const { return m_size; }
int angle() const { return m_angle; }
@ -48,7 +64,6 @@ namespace doc {
const gfx::Rect& bounds() const { return m_bounds; }
const gfx::Point& center() const { return m_center; }
void setType(BrushType type);
void setSize(int size);
void setAngle(int angle);
void setImage(const Image* image,
@ -81,7 +96,9 @@ namespace doc {
private:
void clean();
void regenerate();
void regenerateMaskBitmap();
void resetBounds();
void copyFieldsFromBrush(const Brush& brush);
BrushType m_type; // Type of brush
int m_size; // Size (diameter)
@ -101,8 +118,6 @@ namespace doc {
std::optional<color_t> m_bgColor; // Background color
};
typedef std::shared_ptr<Brush> BrushRef;
} // namespace doc
#endif

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -42,10 +42,13 @@ namespace doc {
Reference = 64, // Is a reference layer
PersistentFlagsMask = 0xffff,
Internal_WasVisible = 0x10000, // Was visible in the alternative state (Alt+click)
BackgroundLayerFlags = LockMove | Background,
// Flags that change the modified flag of the document
// (e.g. created by undoable actions).
StructuralFlagsMask = Background | Reference,
};
class Layer : public WithUserData {

View File

@ -311,7 +311,7 @@ LayerImage* Sprite::backgroundLayer() const
Layer* Sprite::firstLayer() const
{
Layer* layer = root()->firstLayer();
while (layer->isGroup())
while (layer && layer->isGroup())
layer = static_cast<LayerGroup*>(layer)->firstLayer();
return layer;
}

View File

@ -1,15 +1,15 @@
# ASEPRITE
# Copyright (C) 2020-2021 Igara Studio S.A.
# Copyright (C) 2020-2024 Igara Studio S.A.
# Copyright (C) 2001-2017 David Capello
set(UPDATER_LIB_SOURCES
check_update.cpp
user_agent.cpp)
# By default the updater-lib will contain only the functions related
# the user agent string.
add_library(updater-lib user_agent.cpp)
target_link_libraries(updater-lib laf-base ver-lib)
add_library(updater-lib ${UPDATER_LIB_SOURCES})
target_link_libraries(updater-lib
laf-base
net-lib
ver-lib
${TINYXML_LIBRARY})
# Only when ENABLE_UPDATER is ON we'll enable the "check for update"
# portion of the library.
if(ENABLE_UPDATER)
target_sources(updater-lib PRIVATE check_update.cpp)
target_link_libraries(updater-lib net-lib ${TINYXML_LIBRARY})
endif()

View File

@ -19,45 +19,42 @@
namespace updater {
std::string getUserAgent()
std::string getFullOSString()
{
base::Platform p = base::get_platform();
std::stringstream userAgent;
// App name and version
userAgent << get_app_name() << "/" << get_app_version() << " (";
std::stringstream os;
#if LAF_WINDOWS
// ----------------------------------------------------------------------
// Windows
userAgent << "Windows";
os << "Windows";
switch (p.windowsType) {
case base::Platform::WindowsType::Server:
userAgent << " Server";
os << " Server";
break;
case base::Platform::WindowsType::NT:
userAgent << " NT";
os << " NT";
break;
}
userAgent << " " << p.osVer.str();
os << " " << p.osVer.str();
if (p.servicePack.major() > 0)
userAgent << " SP" << p.servicePack.major();
os << " SP" << p.servicePack.major();
if (p.isWow64)
userAgent << "; WOW64";
os << "; WOW64";
if (p.wineVer)
userAgent << "; Wine " << p.wineVer;
os << "; Wine " << p.wineVer;
#elif LAF_MACOS
userAgent << "macOS "
<< p.osVer.major() << "."
<< p.osVer.minor() << "."
<< p.osVer.patch();
os << "macOS "
<< p.osVer.major() << "."
<< p.osVer.minor() << "."
<< p.osVer.patch();
#else
@ -65,14 +62,23 @@ std::string getUserAgent()
// Unix like
if (!p.distroName.empty()) {
userAgent << p.distroName;
os << p.distroName;
if (!p.distroVer.empty())
userAgent << " " << p.distroVer;
os << " " << p.distroVer;
}
#endif
userAgent << ")";
return os.str();
}
std::string getUserAgent()
{
std::stringstream userAgent;
// App name and version
userAgent << get_app_name() << "/" << get_app_version()
<< " (" << getFullOSString() << ")";
return userAgent.str();
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -12,6 +13,7 @@
namespace updater {
std::string getFullOSString();
std::string getUserAgent();
} // namespace updater

View File

@ -8,21 +8,29 @@ local sep = fs.pathSeparator
assert('' == fs.filePath('first.png'))
assert('path' == fs.filePath('path/second.png'))
assert('C:\\path' == fs.filePath('C:\\path\\third.png'))
if app.os.windows then
assert('C:\\path' == fs.filePath('C:\\path\\third.png'))
end
assert('first.png' == fs.fileName('first.png'))
assert('second.png' == fs.fileName('path/second.png'))
assert('third.png' == fs.fileName('C:\\path\\third.png'))
if app.os.windows then
assert('third.png' == fs.fileName('C:\\path\\third.png'))
end
assert('png' == fs.fileExtension('path/file.png'))
assert('first' == fs.fileTitle('first.png'))
assert('second' == fs.fileTitle('path/second.png'))
assert('third' == fs.fileTitle('C:\\path\\third.png'))
if app.os.windows then
assert('third' == fs.fileTitle('C:\\path\\third.png'))
end
assert('first' == fs.filePathAndTitle('first.png'))
assert('path/second' == fs.filePathAndTitle('path/second.png'))
assert('C:\\path\\third' == fs.filePathAndTitle('C:\\path\\third.png'))
if app.os.windows then
assert('C:\\path\\third' == fs.filePathAndTitle('C:\\path\\third.png'))
end
assert('hi/bye' == fs.joinPath('hi/', 'bye'))
assert('hi/bye' .. sep .. 'smth.png' == fs.joinPath('hi/', 'bye', 'smth.png'))

View File

@ -1,4 +1,4 @@
-- Copyright (C) 2019 Igara Studio S.A.
-- Copyright (C) 2019-2024 Igara Studio S.A.
--
-- This file is released under the terms of the MIT license.
-- Read LICENSE.txt for more information.
@ -51,6 +51,7 @@ do
assert(b.patternOrigin.y == 0)
end
-- Image brush
do
local rgba = app.pixelColor.rgba
local r = rgba(255, 0, 0)
@ -71,4 +72,175 @@ do
brush:setBgColor(b)
expect_img(brush.image, { b, g, g, b })
-- Test copy image brushes
local brush2 = Brush(brush)
expect_img(brush2.image, { b, g, g, b })
brush2:setFgColor(r)
expect_img(brush2.image, { b, r, r, b })
brush2:setBgColor(r)
expect_img(brush2.image, { r, r, r, r })
expect_img(brush.image, { b, g, g, b }) -- First brush wasn't modified
end
-- Tests with Image Brushes
-- Brush in a certain pixel format used on different sprites of
-- all available pixel formats.
do
-- RGB sprite
local sprRGB = Sprite(2, 2, ColorMode.RGB)
local cel = sprRGB.cels[1]
expect_img(cel.image, { 0, 0,
0, 0})
local pal = Palette(4)
pal:setColor(1, Color{ r=255, g=0, b=0, a=128 })
pal:setColor(2, Color{ r=0, g=255, b=0, a=128 })
pal:setColor(3, Color{ r=0, g=0, b=255, a=128 })
sprRGB:setPalette(pal)
-- Test Sprite RGB with RGB brush
local brushImg = Image(2, 2, ColorMode.RGB)
array_to_pixels({ pal:getColor(1), pal:getColor(2),
pal:getColor(3), pal:getColor(0) }, brushImg)
local bruRGB = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruRGB, points={ Point(1, 1) } }
expect_img(cel.image,
{ pal:getColor(1).rgbaPixel, pal:getColor(2).rgbaPixel,
pal:getColor(3).rgbaPixel, pal:getColor(0).rgbaPixel })
app.undo()
-- Test Sprite RGB with INDEXED brush
local brushImg = Image(2, 2, ColorMode.INDEXED)
array_to_pixels({ 1, 2,
3, 0 }, brushImg)
local bruINDEXED = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruINDEXED, points={ Point(1, 1) } }
expect_img(cel.image,
{ pal:getColor(1).rgbaPixel, pal:getColor(2).rgbaPixel,
pal:getColor(3).rgbaPixel, 0 })
app.undo()
-- Test Sprite RGB with GRAYSCALE brush
local brushImg = Image(2, 2, ColorMode.GRAYSCALE)
array_to_pixels({ Color{ gray=255, alpha=128 }, Color{ gray=128, alpha=128 },
Color{ gray=64, alpha=255 }, Color{ gray=0, alpha=255 } }, brushImg)
local bruGRAYSCALE = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruGRAYSCALE, points={ Point(1, 1) } }
expect_img(cel.image,
{ Color{ gray=255, alpha=128 }.rgbaPixel, Color{ gray=128, alpha=128 }.rgbaPixel,
Color{ gray=64, alpha=255 }.rgbaPixel, Color{ gray=0, alpha=255 }.rgbaPixel })
-- -- -- -- -- -- --
-- INDEXED sprite
local sprINDEXED = Sprite(2, 2, ColorMode.INDEXED)
local cel = sprINDEXED.cels[1]
expect_img(cel.image, { 0, 0,
0, 0 })
local pal = Palette(4)
pal:setColor(1, Color{ r=255, g=0, b=0, a=128 })
pal:setColor(2, Color{ r=0, g=255, b=0, a=128 })
pal:setColor(3, Color{ r=0, g=0, b=255, a=128 })
sprINDEXED:setPalette(pal)
-- Test Sprite INDEXED with RGB brush
local brushImg = Image(2, 2, ColorMode.RGB)
array_to_pixels({ pal:getColor(1), pal:getColor(2),
pal:getColor(3), app.pixelColor.rgba(0, 0, 0, 0) }, brushImg)
local bruRGB = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruRGB, points={ Point(1, 1) } }
expect_img(cel.image,
{ 1, 2,
3, 3 })
app.undo()
-- Test Sprite INDEXED with INDEXED brush
local brushImg = Image(2, 2, ColorMode.INDEXED)
array_to_pixels({ 1, 2,
3, 0 }, brushImg)
local bruINDEXED = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruINDEXED, points={ Point(1, 1) } }
expect_img(cel.image,
{ 1, 2,
3, 0 })
app.undo()
-- Test Sprite INDEXED with INDEXED brush
-- (INDEXED brush with one out of bounds index)
local brushImg = Image(2, 2, ColorMode.INDEXED)
array_to_pixels({ 1, 5,
3, 0 }, brushImg)
local bruINDEXED = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruINDEXED, points={ Point(1, 1) } }
expect_img(cel.image,
{ 1, 3,
3, 0 })
app.undo()
-- Test Sprite INDEXED with GRAYSCALE brush
local brushImg = Image(2, 2, ColorMode.GRAYSCALE)
array_to_pixels({ Color{ gray=255, alpha=128 }, Color{ gray=128, alpha=128 },
Color{ gray=64, alpha=255 }, Color{ gray=0, alpha=255 } }, brushImg)
local bruGRAYSCALE = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruGRAYSCALE, points={ Point(1, 1) } }
expect_img(cel.image,
{ 2, 3,
3, 3 })
-- -- -- -- -- -- --
-- GRAYSCALE sprite
local sprGRAYSCALE = Sprite(2, 2, ColorMode.GRAYSCALE)
local cel = sprGRAYSCALE.cels[1]
expect_img(cel.image, { 0, 0,
0, 0 })
local pal = Palette(4)
pal:setColor(1, Color{ gray=128, alpha=128 }.grayPixel)
pal:setColor(2, Color{ gray=64, alpha=128 }.grayPixel)
pal:setColor(3, Color{ gray=32, alpha=255 }.grayPixel)
print(pal:getColor(1).grayPixel)
print(pal:getColor(2).grayPixel)
print(pal:getColor(3).grayPixel)
sprGRAYSCALE:setPalette(pal)
-- Test Sprite GRAYSCALE with RGB brush
local brushImg = Image(2, 2, ColorMode.RGB)
array_to_pixels({ Color{ r=255, g=0, b=0, a=128 }, Color{ r=0, g=255, b=0, a=128 },
Color{ r=0, g=0, b=255, a=128 }, app.pixelColor.rgba(0, 0, 0, 0) }, brushImg)
local bruRGB = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruRGB, points={ Point(1, 1) } }
expect_img(cel.image,
{ Color{ gray=54, alpha=128 }.grayPixel, Color{ gray=182, alpha=128 }.grayPixel,
Color{ gray=18, alpha=128 }.grayPixel, 0 })
app.undo()
-- Test Sprite GRAYSCALE with INDEXED brush
-- (INDEXED brush with out of bound index)
local brushImg = Image(2, 2, ColorMode.INDEXED)
array_to_pixels({ 1, 5,
3, 0 }, brushImg)
local bruINDEXED = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruINDEXED, points={ Point(1, 1) } }
expect_img(cel.image,
{ Color{ gray=128, alpha=128 }.grayPixel,
Color{ gray=32, alpha=255 }.grayPixel })
app.undo()
-- Test Sprite GRAYSCALE with GRAYSCALE brush
local brushImg = Image(2, 2, ColorMode.GRAYSCALE)
array_to_pixels({ Color{ gray=128, alpha=128 }, Color{ gray=222, alpha=222 },
Color{ gray=32, alpha=255 }, Color{ gray=0, alpha=255 } }, brushImg)
local bruGRAYSCALE = Brush { image=brushImg }
app.useTool{ tool=pencil, brush=bruGRAYSCALE, points={ Point(1, 1) } }
expect_img(cel.image,
{ pal:getColor(1).grayPixel, Color{ gray=222, alpha=222 }.grayPixel,
pal:getColor(3).grayPixel, Color{ gray=0, alpha=255 }.grayPixel })
end

View File

@ -50,6 +50,7 @@ if(ENABLE_WEBP AND NOT LAF_BACKEND STREQUAL "skia")
endif()
if(NOT USE_SHARED_TINYXML)
set(tinyxml2_BUILD_TESTING OFF CACHE BOOL "Build tests for tinyxml2")
add_subdirectory(tinyxml2)
endif()

2
third_party/cmark vendored

@ -1 +1 @@
Subproject commit 728c68465062223295076d8cb365ca911a55a218
Subproject commit 186592f7ff021cd20c5e758239934a3b7848d51f