mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-29 19:20:09 +00:00
Merge branch 'main' into beta
This commit is contained in:
commit
5ddce57ab0
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -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
|
||||
|
@ -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
2
laf
@ -1 +1 @@
|
||||
Subproject commit d590eec74a0358678f25b6d72c4fc97eb542069d
|
||||
Subproject commit 0d8396ea41cf4432f07b31d9002c80ef52e0565a
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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" };
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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" } };
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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() &&
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
121
src/app/script/app_os_object.cpp
Normal file
121
src/app/script/app_os_object.cpp
Normal 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
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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; }
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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()),
|
||||
|
@ -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()) {
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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'))
|
||||
|
@ -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
|
||||
|
1
third_party/CMakeLists.txt
vendored
1
third_party/CMakeLists.txt
vendored
@ -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
2
third_party/cmark
vendored
@ -1 +1 @@
|
||||
Subproject commit 728c68465062223295076d8cb365ca911a55a218
|
||||
Subproject commit 186592f7ff021cd20c5e758239934a3b7848d51f
|
Loading…
x
Reference in New Issue
Block a user