Merge branch 'main' into beta

This commit is contained in:
David Capello 2024-12-10 16:16:16 -03:00
commit f62b5eafb1
18 changed files with 615 additions and 193 deletions

32
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,32 @@
exclude: 'third_party/.*|laf/.*'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.5
hooks:
- id: remove-tabs
args: [--whitespaces-count, '8']
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.5
hooks:
- id: clang-format
files: \.(c|h|cpp|hpp|cc|hh|cxx|hxx)$
types_or: [text]
- repo: https://github.com/pocc/pre-commit-hooks
rev: v1.3.5
hooks:
- id: clang-tidy
files: \.(c|h|cpp|hpp|cc|hh|cxx|hxx)$
args: [--fix, --quiet, --use-color]
types_or: [text]
additional_dependencies: [clang-tidy==19.1.0]
require_serial: true
stages: [manual]

View File

@ -16,7 +16,7 @@ Before you submit an issue:
click the `Subscribe` or `Watching` button to get notifications
via email.
# Compilation problem
# Compilation problems
Before you submit an issue or a post about a **compilation problem**,
check the following items:
@ -77,6 +77,20 @@ new [features](https://community.aseprite.org/c/features),
[bug reports](https://community.aseprite.org/c/bugs), etc. You are
encouraged to create mockups for any issue you see and attach them.
## Pre-commit hooks
We use [pre-commit](https://pre-commit.com/) as way to set up hooks, you can install it with:
```
pip install pre-commit
pre-commit install
```
If you need to run it manually, use `pre-commit run`.
To run `clang-tidy`, you can use `pre-commit run --hook-stage manual clang-tidy`.
Make sure to check the suggestions and to not apply them arbitrarily, since some might not be 100% applicable to what you're doing.
## Code submission policy
We have some rules for the changes and commits that are contributed:

View File

@ -59,7 +59,7 @@ To compile Aseprite you will need:
## Windows dependencies
* Windows 10 (we don't support cross-compiling)
* Windows 10/11 (we don't support cross-compiling)
* [Visual Studio Community 2022](https://visualstudio.microsoft.com/downloads/) (we don't support [MinGW](#mingw))
* The [Desktop development with C++ item + Windows 10.0.18362.0 SDK](https://imgur.com/a/7zs51IT)
from the Visual Studio installer

View File

@ -859,6 +859,8 @@ x = X:
y = Y:
width = Width:
height = Height:
columns = Columns:
rows = Rows:
padding = Padding
horizontal_padding = Horizontal:
vertical_padding = Vertical:

View File

@ -24,6 +24,14 @@
<label text="@.height" />
<expr id="height" text="16" />
<separator horizontal="true" cell_hspan="4" />
<label text="@.columns" />
<expr id="columns" />
<label text="@.rows" />
<expr id="rows" />
<check id="padding_enabled" text="@.padding" cell_hspan="4" />
<label text="@.horizontal_padding" id="horizontal_padding_label" />

2
laf

@ -1 +1 @@
Subproject commit 4766fd95801af52b904146e79e5d89a916de49de
Subproject commit 3eee03a1f3a3ce7b06e93b1106fc356bc0307c42

View File

@ -44,18 +44,41 @@ namespace app {
using namespace ui;
gfx::Size calcFrameSize(gfx::Size availSize,
int cols, int rows,
const gfx::Point& frameOrigin,
const gfx::Size& padding)
{
if (cols <= 0)
cols = 1;
if (rows <= 0)
rows = 1;
availSize.w -= (frameOrigin.x + padding.w*(cols-1));
availSize.h -= (frameOrigin.y + padding.h*(rows-1));
return gfx::Size(availSize.w / cols, availSize.h / rows);
}
struct ImportSpriteSheetParams : public NewParams {
Param<bool> ui { this, true, "ui" };
Param<app::SpriteSheetType> type { this, app::SpriteSheetType::None, "type" };
Param<gfx::Rect> frameBounds { this, gfx::Rect(0, 0, 0, 0), "frameBounds" };
Param<gfx::Size> padding { this, gfx::Size(0, 0), "padding" };
Param<bool> partialTiles { this, false, "partialTiles" };
// Columns and rows are optional, and they just help in calculating
// frameBounds automatically when the number of columns and rows are known
// beforehand. So, if these are specified along frameBounds, then frameBounds
// width/height might be recalculated.
Param<int> columns { this, 0, "columns" };
Param<int> rows { this, 0, "rows" };
};
class ImportSpriteSheetWindow : public app::gen::ImportSpriteSheet
, public SelectBoxDelegate {
public:
ImportSpriteSheetWindow(Context* context)
ImportSpriteSheetWindow(const ImportSpriteSheetParams& params,
Context* context)
: m_context(context)
, m_document(NULL)
, m_editor(NULL)
@ -81,6 +104,8 @@ public:
y()->Change.connect([this]{ onEntriesChange(); });
width()->Change.connect([this]{ onEntriesChange(); });
height()->Change.connect([this]{ onEntriesChange(); });
columns()->Change.connect([this]{ onColumnsChange(); });
rows()->Change.connect([this]{ onRowsChange(); });
paddingEnabled()->Click.connect([this]{ onPaddingEnabledChange(); });
horizontalPadding()->Change.connect([this]{ onEntriesChange(); });
verticalPadding()->Change.connect([this]{ onEntriesChange(); });
@ -96,7 +121,40 @@ public:
m_fileOpened = false;
}
if (params.type.isSet())
sheetType()->setSelectedItemIndex((int)params.type()-1);
if (params.frameBounds.isSet()) {
x()->setTextf("%d", params.frameBounds().x);
y()->setTextf("%d", params.frameBounds().y);
width()->setTextf("%d", params.frameBounds().w);
height()->setTextf("%d", params.frameBounds().h);
}
paddingEnabled()->setSelected(params.padding.isSet());
if (params.padding.isSet()) {
if (m_docPref)
m_docPref->importSpriteSheet.paddingBounds(params.padding());
else {
horizontalPadding()->setTextf("%d", params.padding().w);
verticalPadding()->setTextf("%d", params.padding().h);
}
}
if (params.partialTiles.isSet())
partialTiles()->setSelected(params.partialTiles());
onPaddingEnabledChange();
if (params.columns.isSet()) {
columns()->setTextf("%d", params.columns());
onColumnsChange();
}
if (params.rows.isSet()) {
rows()->setTextf("%d", params.rows());
onRowsChange();
}
}
~ImportSpriteSheetWindow() {
@ -150,6 +208,7 @@ protected:
void onSheetTypeChange() {
updateGridState();
onEntriesChange();
}
void onSelectFile() {
@ -187,7 +246,7 @@ protected:
return gfx::Size(padW, padH);
}
void onEntriesChange() {
void updateRulers() {
m_rect = getRectFromEntries();
m_padding = getPaddingFromEntries();
@ -205,6 +264,32 @@ protected:
}
}
void onEntriesChange() {
updateRulers();
columns()->setTextf("%d", calcColumns());
rows()->setTextf("%d", calcRows());
}
#define ON_COLROWS_CHANGE(widtheigth, wh) \
if (!m_editor) \
return; \
auto frameSize = calcFrameSize(m_editor->sprite()->size(), \
columns()->textInt(), rows()->textInt(), \
getRectFromEntries().origin(), \
getPaddingFromEntries()); \
widtheigth()->setTextf("%d", frameSize.wh); \
updateRulers();
void onColumnsChange() {
ON_COLROWS_CHANGE(width, w);
}
void onRowsChange() {
ON_COLROWS_CHANGE(height, h);
}
#undef ON_COLROWS_CHANGE
bool onProcessMessage(ui::Message* msg) override {
switch (msg->type()) {
case kCloseMessage:
@ -223,6 +308,35 @@ protected:
targets.push_back(View::getView(m_editor));
}
#define CALC_SPANS(xy, wh) \
if (!m_editor) \
return 0; \
int spans = 0; \
int rectwh = (m_rect.wh <= 0 ? 1 : m_rect.wh); \
int wh = m_editor->sprite()->size().wh - m_rect.xy; \
while(wh > 0) { \
wh -= rectwh; \
if (wh >= 0) { \
spans++; \
} \
wh -= m_padding.wh; \
} \
return spans;
int calcColumns() {
if (sheetTypeValue() == SpriteSheetType::Vertical)
return 1;
CALC_SPANS(x, w);
}
int calcRows() {
if (sheetTypeValue() == SpriteSheetType::Horizontal)
return 1;
CALC_SPANS(y, h);
}
#undef CALC_SPANS
// SelectBoxDelegate impleentation
void onChangeRectangle(const gfx::Rect& rect) override {
m_rect = rect;
@ -231,6 +345,8 @@ protected:
y()->setTextf("%d", m_rect.y);
width()->setTextf("%d", m_rect.w);
height()->setTextf("%d", m_rect.h);
columns()->setTextf("%d", calcColumns());
rows()->setTextf("%d", calcRows());
}
void onChangePadding(const gfx::Size& padding) override {
@ -248,6 +364,8 @@ protected:
horizontalPadding()->setTextf("%d", 0);
verticalPadding()->setTextf("%d", 0);
}
columns()->setTextf("%d", calcColumns());
rows()->setTextf("%d", calcRows());
}
std::string onGetContextBarHelp() override {
@ -340,13 +458,19 @@ private:
switch (sheetTypeValue()) {
case SpriteSheetType::Horizontal:
flags |= int(SelectBoxState::Flags::HGrid);
columns()->setEnabled(true);
rows()->setEnabled(false);
break;
case SpriteSheetType::Vertical:
flags |= int(SelectBoxState::Flags::VGrid);
columns()->setEnabled(false);
rows()->setEnabled(true);
break;
case SpriteSheetType::Rows:
case SpriteSheetType::Columns:
flags |= int(SelectBoxState::Flags::Grid);
columns()->setEnabled(true);
rows()->setEnabled(true);
break;
}
@ -417,9 +541,7 @@ void ImportSpriteSheetCommand::onExecute(Context* context)
auto& params = this->params();
if (context->isUIAvailable() && params.ui()) {
// TODO use params as input values for the ImportSpriteSheetWindow
ImportSpriteSheetWindow window(context);
ImportSpriteSheetWindow window(params, context);
window.openWindowInForeground();
if (!window.ok())
return;
@ -443,6 +565,24 @@ void ImportSpriteSheetCommand::onExecute(Context* context)
document = context->activeDocument();
if (!document)
return;
Sprite* sprite = document->sprite();
auto newFrameBounds = calcFrameSize(sprite->size(),
params.columns(), params.rows(),
params.frameBounds().origin(),
params.padding());
if (params.columns.isSet()) {
auto fb = params.frameBounds();
fb.w = newFrameBounds.w;
params.frameBounds(fb);
}
if (params.rows.isSet()) {
auto fb = params.frameBounds();
fb.h = newFrameBounds.h;
params.frameBounds(fb);
}
}
// The list of frames imported from the sheet

View File

@ -14,6 +14,7 @@
#include "app/color.h"
#include "app/color_utils.h"
#include "app/commands/command.h"
#include "app/commands/new_params.h"
#include "app/console.h"
#include "app/context.h"
#include "app/context_access.h"
@ -48,22 +49,96 @@ using namespace ui;
static const char* ConfigSection = "MaskColor";
class MaskByColorCommand : public Command {
public:
MaskByColorCommand();
struct MaskByColorParams : public NewParams {
Param<bool> ui { this, true, "ui" };
Param<app::Color> color { this, app::Color(), "color" };
Param<int> tolerance { this, 0, "tolerance" };
Param<gen::SelectionMode> mode { this, gen::SelectionMode::DEFAULT, "mode" };
};
protected:
bool onEnabled(Context* context) override;
void onExecute(Context* context) override;
class MaskByColorWindow : public ui::Window {
public:
MaskByColorWindow(MaskByColorParams& params, const ContextReader& reader)
: Window(Window::WithTitleBar, Strings::mask_by_color_title())
, m_reader(&reader)
// Save original mask visibility to process it correctly in
// ADD/SUBTRACT/INTERSECT Selection Mode
, m_isOrigMaskVisible(reader.document()->isMaskVisible()) {
TooltipManager* tooltipManager = new TooltipManager();
addChild(tooltipManager);
auto box1 = new Box(VERTICAL);
auto box2 = new Box(HORIZONTAL);
auto box3 = new Box(HORIZONTAL);
auto box4 = new Box(HORIZONTAL | HOMOGENEOUS);
auto label_color = new Label(Strings::mask_by_color_label_color());
m_buttonColor = new ColorButton(
params.color(),
reader.sprite()->pixelFormat(),
ColorButtonOptions());
auto label_tolerance = new Label(Strings::mask_by_color_tolerance());
m_sliderTolerance = new Slider(0, 255, params.tolerance());
m_selMode = new SelModeField;
m_selMode->setSelectionMode(params.mode());
m_selMode->setupTooltips(tooltipManager);
m_checkPreview = new CheckBox(Strings::mask_by_color_preview());
m_buttonOk = new Button(Strings::mask_by_color_ok());
auto button_cancel = new Button(Strings::mask_by_color_cancel());
m_checkPreview->processMnemonicFromText();
m_buttonOk->processMnemonicFromText();
button_cancel->processMnemonicFromText();
if (get_config_bool(ConfigSection, "Preview", true))
m_checkPreview->setSelected(true);
m_buttonOk->Click.connect([this]{ closeWindow(m_buttonOk); });
button_cancel->Click.connect([this, button_cancel]{ closeWindow(button_cancel); });
m_buttonColor->Change.connect([&]{ maskPreview(); });
m_sliderTolerance->Change.connect([&]{ maskPreview(); });
m_checkPreview->Click.connect([&]{ maskPreview(); });
m_selMode->ModeChange.connect([&]{ maskPreview(); });
m_buttonOk->setFocusMagnet(true);
m_buttonColor->setExpansive(true);
m_sliderTolerance->setExpansive(true);
box2->setExpansive(true);
addChild(box1);
box1->addChild(m_selMode);
box1->addChild(box2);
box1->addChild(box3);
box1->addChild(m_checkPreview);
box1->addChild(box4);
box2->addChild(label_color);
box2->addChild(m_buttonColor);
box3->addChild(label_tolerance);
box3->addChild(m_sliderTolerance);
box4->addChild(m_buttonOk);
box4->addChild(button_cancel);
// Default position
remapWindow();
centerWindow();
// Mask first preview
maskPreview();
}
bool accepted() const { return closer() == m_buttonOk; }
app::Color getColor() const { return m_buttonColor->getColor(); }
int getTolerance() const { return m_sliderTolerance->getValue(); }
gen::SelectionMode getSelectionMode() const { return m_selMode->selectionMode(); }
bool isPreviewChecked() const { return m_checkPreview->isSelected(); }
private:
Mask* generateMask(const Mask& origMask,
const Sprite* sprite,
const Image* image,
int xpos, int ypos,
gen::SelectionMode mode);
void maskPreview(const ContextReader& reader);
class SelModeField : public SelectionModeField {
public:
obs::signal<void()> ModeChange;
@ -73,157 +148,31 @@ private:
}
};
Window* m_window = nullptr;
void maskPreview();
const ContextReader* m_reader = nullptr;
bool m_isOrigMaskVisible;
Button* m_buttonOk = nullptr;
ColorButton* m_buttonColor = nullptr;
CheckBox* m_checkPreview = nullptr;
Slider* m_sliderTolerance = nullptr;
SelModeField* m_selMode = nullptr;
bool m_isOrigMaskVisible;
};
MaskByColorCommand::MaskByColorCommand()
: Command(CommandId::MaskByColor(), CmdUIOnlyFlag)
static Mask* generateMask(const Mask& origMask,
bool isOrigMaskVisible,
const Image* image,
int xpos, int ypos,
gen::SelectionMode mode,
int color,
int tolerance)
{
}
bool MaskByColorCommand::onEnabled(Context* context)
{
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
ContextFlags::HasActiveSprite |
ContextFlags::HasActiveImage);
}
void MaskByColorCommand::onExecute(Context* context)
{
ASSERT(!m_window);
const ContextReader reader(context);
const Sprite* sprite = reader.sprite();
if (!App::instance()->isGui() || !sprite)
return;
int xpos, ypos;
const Image* image = reader.image(&xpos, &ypos);
if (!image)
return;
std::unique_ptr<Window> win(
new Window(Window::WithTitleBar, Strings::mask_by_color_title()));
base::ScopedValue<Window*> setWindow(m_window, win.get(), nullptr);
TooltipManager* tooltipManager = new TooltipManager();
m_window->addChild(tooltipManager);
auto box1 = new Box(VERTICAL);
auto box2 = new Box(HORIZONTAL);
auto box3 = new Box(HORIZONTAL);
auto box4 = new Box(HORIZONTAL | HOMOGENEOUS);
auto label_color = new Label(Strings::mask_by_color_label_color());
m_buttonColor = new ColorButton(
ColorBar::instance()->getFgColor(),
sprite->pixelFormat(),
ColorButtonOptions());
auto label_tolerance = new Label(Strings::mask_by_color_tolerance());
m_sliderTolerance = new Slider(0, 255, get_config_int(ConfigSection, "Tolerance", 0));
m_selMode = new SelModeField;
m_selMode->setupTooltips(tooltipManager);
m_checkPreview = new CheckBox(Strings::mask_by_color_preview());
auto button_ok = new Button(Strings::mask_by_color_ok());
auto button_cancel = new Button(Strings::mask_by_color_cancel());
m_checkPreview->processMnemonicFromText();
button_ok->processMnemonicFromText();
button_cancel->processMnemonicFromText();
if (get_config_bool(ConfigSection, "Preview", true))
m_checkPreview->setSelected(true);
button_ok->Click.connect([this, button_ok]{ m_window->closeWindow(button_ok); });
button_cancel->Click.connect([this, button_cancel]{ m_window->closeWindow(button_cancel); });
m_buttonColor->Change.connect([&]{ maskPreview(reader); });
m_sliderTolerance->Change.connect([&]{ maskPreview(reader); });
m_checkPreview->Click.connect([&]{ maskPreview(reader); });
m_selMode->ModeChange.connect([&]{ maskPreview(reader); });
button_ok->setFocusMagnet(true);
m_buttonColor->setExpansive(true);
m_sliderTolerance->setExpansive(true);
box2->setExpansive(true);
m_window->addChild(box1);
box1->addChild(m_selMode);
box1->addChild(box2);
box1->addChild(box3);
box1->addChild(m_checkPreview);
box1->addChild(box4);
box2->addChild(label_color);
box2->addChild(m_buttonColor);
box3->addChild(label_tolerance);
box3->addChild(m_sliderTolerance);
box4->addChild(button_ok);
box4->addChild(button_cancel);
// Default position
m_window->remapWindow();
m_window->centerWindow();
// Save original mask visibility to process it correctly in
// ADD/SUBTRACT/INTERSECT Selection Mode
m_isOrigMaskVisible = reader.document()->isMaskVisible();
// Mask first preview
maskPreview(reader);
// Load window configuration
load_window_pos(m_window, ConfigSection);
// Open the window
m_window->openWindowInForeground();
bool apply = (m_window->closer() == button_ok);
ContextWriter writer(reader);
Doc* document(writer.document());
if (apply) {
Tx tx(writer, "Mask by Color", DoesntModifyDocument);
std::unique_ptr<Mask> mask(generateMask(*document->mask(),
sprite, image, xpos, ypos,
m_selMode->selectionMode()));
tx(new cmd::SetMask(document, mask.get()));
tx.commit();
set_config_int(ConfigSection, "Tolerance", m_sliderTolerance->getValue());
set_config_bool(ConfigSection, "Preview", m_checkPreview->isSelected());
}
else {
document->generateMaskBoundaries();
}
// Update boundaries and editors.
update_screen_for_document(document);
// Save window configuration.
save_window_pos(m_window, ConfigSection);
}
Mask* MaskByColorCommand::generateMask(const Mask& origMask,
const Sprite* sprite,
const Image* image,
int xpos, int ypos,
gen::SelectionMode mode)
{
int color = color_utils::color_for_image(m_buttonColor->getColor(),
sprite->pixelFormat());
int tolerance = m_sliderTolerance->getValue();
std::unique_ptr<Mask> mask(new Mask());
mask->byColor(image, color, tolerance);
mask->offsetOrigin(xpos, ypos);
if (!origMask.isEmpty() && m_isOrigMaskVisible) {
if (!origMask.isEmpty() && isOrigMaskVisible) {
switch (mode) {
case gen::SelectionMode::DEFAULT:
break;
@ -251,18 +200,123 @@ Mask* MaskByColorCommand::generateMask(const Mask& origMask,
return mask.release();
}
void MaskByColorCommand::maskPreview(const ContextReader& reader)
{
ASSERT(m_window);
if (m_window && m_checkPreview->isSelected()) {
int xpos, ypos;
const Image* image = reader.image(&xpos, &ypos);
std::unique_ptr<Mask> mask(generateMask(*reader.document()->mask(),
reader.sprite(), image,
xpos, ypos,
m_selMode->selectionMode()));
class MaskByColorCommand : public CommandWithNewParams<MaskByColorParams> {
public:
MaskByColorCommand();
ContextWriter writer(reader);
protected:
bool onEnabled(Context* context) override;
void onExecute(Context* context) override;
};
MaskByColorCommand::MaskByColorCommand()
: CommandWithNewParams(CommandId::MaskByColor(), CmdUIOnlyFlag)
{
}
bool MaskByColorCommand::onEnabled(Context* context)
{
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
ContextFlags::HasActiveSprite |
ContextFlags::HasActiveImage);
}
void MaskByColorCommand::onExecute(Context* context)
{
const bool ui = (params().ui() && context->isUIAvailable());
const ContextReader reader(context);
const Sprite* sprite = reader.sprite();
if (!sprite)
return;
int xpos, ypos;
const Image* image = reader.image(&xpos, &ypos);
if (!image)
return;
// Save original mask visibility to process it correctly in
// ADD/SUBTRACT/INTERSECT Selection Mode
bool isOrigMaskVisible = reader.document()->isMaskVisible();
bool apply = true;
auto& params = this->params();
// If UI is available, set parameters default values from the UI/configuration
if (context->isUIAvailable()) {
if (!params.color.isSet())
params.color(ColorBar::instance()->getFgColor());
if (!params.tolerance.isSet())
params.tolerance(get_config_int(ConfigSection, "Tolerance", 0));
if (!params.mode.isSet())
params.mode(Preferences::instance().selection.mode());
}
if (ui) {
MaskByColorWindow window(params, reader);
// Load window configuration
load_window_pos(&window, ConfigSection);
// Open the window
window.openWindowInForeground();
// Save window configuration.
save_window_pos(&window, ConfigSection);
apply = window.accepted();
if (apply) {
params.color(window.getColor());
params.mode(window.getSelectionMode());
params.tolerance(window.getTolerance());
set_config_int(ConfigSection, "Tolerance", params.tolerance());
set_config_bool(ConfigSection, "Preview", window.isPreviewChecked());
}
}
ContextWriter writer(reader);
Doc* document(writer.document());
if (apply) {
int color = color_utils::color_for_image(params.color(),
sprite->pixelFormat());
Tx tx(writer, "Mask by Color", DoesntModifyDocument);
std::unique_ptr<Mask> mask(generateMask(*document->mask(),
isOrigMaskVisible,
image, xpos, ypos,
params.mode(),
color, params.tolerance()));
tx(new cmd::SetMask(document, mask.get()));
tx.commit();
}
else {
document->generateMaskBoundaries();
}
// Update boundaries and editors.
update_screen_for_document(document);
}
void MaskByColorWindow::maskPreview()
{
if (isPreviewChecked()) {
int xpos, ypos;
const Image* image = m_reader->image(&xpos, &ypos);
int color = color_utils::color_for_image(m_buttonColor->getColor(),
m_reader->sprite()->pixelFormat());
int tolerance = m_sliderTolerance->getValue();
std::unique_ptr<Mask> mask(generateMask(*m_reader->document()->mask(),
m_isOrigMaskVisible,
image, xpos, ypos,
m_selMode->selectionMode(),
color, tolerance));
ContextWriter writer(*m_reader);
#ifdef SHOW_BOUNDARIES_GEN_PERFORMANCE
base::Chrono chrono;

View File

@ -12,6 +12,7 @@
#include "app/color.h"
#include "app/doc_exporter.h"
#include "app/pref/preferences.h"
#include "app/sprite_sheet_type.h"
#include "app/tools/ink_type.h"
#include "base/convert_to.h"
@ -241,6 +242,21 @@ void Param<doc::RgbMapAlgorithm>::fromString(const std::string& value)
setValue(doc::RgbMapAlgorithm::DEFAULT);
}
template<>
void Param<gen::SelectionMode>::fromString(const std::string& value)
{
if (base::utf8_icmp(value, "replace") == 0)
setValue(gen::SelectionMode::REPLACE);
else if (base::utf8_icmp(value, "add") == 0)
setValue(gen::SelectionMode::ADD);
else if (base::utf8_icmp(value, "subtract") == 0)
setValue(gen::SelectionMode::SUBTRACT);
else if (base::utf8_icmp(value, "intersect") == 0)
setValue(gen::SelectionMode::INTERSECT);
else
setValue(gen::SelectionMode::DEFAULT);
}
//////////////////////////////////////////////////////////////////////
// Convert values from Lua
//////////////////////////////////////////////////////////////////////
@ -405,6 +421,15 @@ void Param<doc::RgbMapAlgorithm>::fromLua(lua_State* L, int index)
setValue((doc::RgbMapAlgorithm)lua_tointeger(L, index));
}
template<>
void Param<gen::SelectionMode>::fromLua(lua_State* L, int index)
{
if (lua_type(L, index) == LUA_TSTRING)
fromString(lua_tostring(L, index));
else
setValue((gen::SelectionMode)lua_tointeger(L, index));
}
void CommandWithNewParamsBase::loadParamsFromLuaTable(lua_State* L, int index)
{
onResetValues();

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) 2018 David Capello
//
// This program is distributed under the terms of
@ -9,6 +9,7 @@
#include "config.h"
#endif
#include "app/cmd/remove_cel.h"
#include "app/cmd/replace_image.h"
#include "app/cmd/set_cel_opacity.h"
#include "app/cmd/set_cel_position.h"
@ -122,13 +123,16 @@ int Cel_set_frame(lua_State* L)
int Cel_set_image(lua_State* L)
{
auto cel = get_docobj<Cel>(L, 1);
auto srcImage = get_image_from_arg(L, 2);
ImageRef newImage(Image::createCopy(srcImage));
Tx tx(cel->sprite());
tx(new cmd::ReplaceImage(cel->sprite(),
cel->imageRef(),
newImage));
if (may_get_obj<Image>(L, 2)) {
const auto* srcImage = get_image_from_arg(L, 2);
const ImageRef newImage(Image::createCopy(srcImage));
tx(new cmd::ReplaceImage(cel->sprite(),
cel->imageRef(),
newImage));
}
else if (lua_isnil(L, 2))
tx(new cmd::RemoveCel(cel));
tx.commit();
return 0;
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 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
@ -207,6 +207,7 @@ namespace app {
virtual const doc::Grid& getGrid() const = 0;
virtual gfx::Rect getGridBounds() = 0;
virtual bool isPixelConnectivityEightConnected() = 0;
virtual bool isPointInsideCanvas(const gfx::Point& point) = 0;
// Returns true if the figure must be filled when we release the
// mouse (e.g. a filled rectangle, etc.)

View File

@ -153,9 +153,8 @@ bool ToolLoopManager::releaseButton(const Pointer& pointer)
if (m_toolLoop->getController()->isOnePoint() &&
m_toolLoop->getInk()->isSelection() &&
!m_toolLoop->getSrcImage()->bounds().contains(pointer.point())) {
!m_toolLoop->isPointInsideCanvas(pointer.point()))
return false;
}
Stroke::Pt spritePoint = getSpriteStrokePt(pointer);
bool res = m_toolLoop->getController()->releaseButton(m_stroke, spritePoint);

View File

@ -609,6 +609,12 @@ bool StandbyState::onUpdateStatusBar(Editor* editor)
gfx::Point pt = grid.canvasToTile(gfx::Point(spritePos));
buf += fmt::format(" :grid: {} {}", pt.x, pt.y);
// Number of columns of the current grid/tilemap to show a
// "tile index", i.e. a linear index that might be used in a
// 1D array to represent the current grid tile/cell. If it's 0
// we don't show the index.
int tileIndexColumns = 0;
// Show the tile index of this specific tile
if (site.layer() &&
site.layer()->isTilemap() &&
@ -620,8 +626,20 @@ bool StandbyState::onUpdateStatusBar(Editor* editor)
std::string str;
build_tile_flags_string(tf, str);
buf += fmt::format(" [{}{}]", ti, str);
// Show tile index for a tilemaps (using the tilemap size)
tileIndexColumns = site.image()->width();
}
}
// Show the tile index for a regular layer/grid
else if (sprite->bounds().contains(gfx::Point(spritePos))) {
tileIndexColumns =
int(std::ceil(double(sprite->bounds().w - grid.origin().x)
/ grid.tileSize().w));
}
if (tileIndexColumns > 0 && pt.x >= 0 && pt.y >= 0)
buf += fmt::format(" :search: {}", pt.x+pt.y*tileIndexColumns);
}
}

View File

@ -360,6 +360,15 @@ public:
== app::gen::PixelConnectivity::EIGHT_CONNECTED);
}
bool isPointInsideCanvas(const gfx::Point& point) override {
const int a = ((getTiledMode() == TiledMode::X_AXIS ||
getTiledMode() == TiledMode::BOTH) ? 3 : 1);
const int b = ((getTiledMode() == TiledMode::Y_AXIS ||
getTiledMode() == TiledMode::BOTH) ? 3 : 1);
return 0 <= point.x && point.x < a * sprite()->size().w &&
0 <= point.y && point.y < b * sprite()->size().h;
}
const doc::Grid& getGrid() const override { return m_grid; }
gfx::Rect getGridBounds() override { return m_gridBounds; }
gfx::Point getCelOrigin() override { return m_celOrigin; }

View File

@ -86,11 +86,7 @@ bool ButtonBase::onProcessMessage(Message* msg)
if (isEnabled() && isVisible()) {
const bool mnemonicPressed =
(mnemonic() &&
(!mnemonicRequiresModifiers() ||
msg->altPressed() ||
msg->cmdPressed()) &&
isMnemonicPressed(keymsg));
isMnemonicPressedWithModifiers(keymsg);
// For kButtonWidget
if (m_behaviorType == kButtonWidget) {
@ -154,11 +150,19 @@ bool ButtonBase::onProcessMessage(Message* msg)
break;
case kCheckWidget: {
// Fire onClick() event
onClick();
return true;
}
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
KeyScancode scancode = keymsg->scancode();
const bool mnemonicPressed =
isMnemonicPressedWithModifiers(keymsg);
// Fire the onClick() event only if the user pressed space or
// Alt+the underscored letter of the checkbox label.
if (scancode == kKeySpace || mnemonicPressed) {
onClick();
return true;
}
break;
}
}
}
break;

View File

@ -1586,6 +1586,15 @@ bool Widget::isMnemonicPressed(const KeyMessage* keyMsg) const
(chr >= '0' && chr <= '9' && keyMsg->scancode() == (kKey0 + chr - '0'))));
}
bool Widget::isMnemonicPressedWithModifiers(const KeyMessage* msg) const
{
return (mnemonic() &&
(!mnemonicRequiresModifiers() ||
msg->altPressed() ||
msg->cmdPressed()) &&
isMnemonicPressed(msg));
}
bool Widget::onProcessMessage(Message* msg)
{
ASSERT(msg != nullptr);

View File

@ -388,7 +388,7 @@ namespace ui {
// Offer the capture to widgets of the given type. Returns true if
// the capture was passed to other widget.
bool offerCapture(ui::MouseMessage* mouseMsg, int widget_type);
bool offerCapture(MouseMessage* mouseMsg, int widget_type);
// Returns lower-case letter that represet the mnemonic of the widget
// (the underscored character, i.e. the letter after & symbol).
@ -409,7 +409,11 @@ namespace ui {
// Returns true if the mnemonic character is pressed (without modifiers).
// TODO maybe we can add check for modifiers now that this
// information is included in the Widget
bool isMnemonicPressed(const ui::KeyMessage* keyMsg) const;
bool isMnemonicPressed(const KeyMessage* keyMsg) const;
// Returns true if the mnemonic character is pressed with
// modifiers (Alt or Command key).
bool isMnemonicPressedWithModifiers(const KeyMessage* msg) const;
// Signals
obs::signal<void()> InitTheme;

View File

@ -664,3 +664,102 @@ do
expect_img(i, { 1, 1, 1, 1,
1, 0, 0, 1 })
end
-- MaskByColor
do
local s = Sprite(5, 5, ColorMode.INDEXED)
local c = s.cels[1]
local i = c.image
array_to_pixels({
1, 1, 0, 0, 1,
1, 1, 0, 0, 1,
1, 0, 0, 0, 1,
1, 0, 0, 1, 1,
1, 0, 0, 1, 1,
}, i)
app.command.MaskByColor {
color = Color{ index=1 },
tolerance = 0,
}
app.fgColor = Color{ index=2 }
app.command.Fill()
expect_img(i, {
2, 2, 0, 0, 2,
2, 2, 0, 0, 2,
2, 0, 0, 0, 2,
2, 0, 0, 2, 2,
2, 0, 0, 2, 2,
})
-- Subtract from current selection by color
app.command.MaskAll {}
app.command.MaskByColor {
color = Color{ index=2 },
tolerance = 0,
mode = SelectionMode.SUBTRACT,
}
app.fgColor = Color{ index=3 }
app.command.Fill()
expect_img(i, {
2, 2, 3, 3, 2,
2, 2, 3, 3, 2,
2, 3, 3, 3, 2,
2, 3, 3, 2, 2,
2, 3, 3, 2, 2,
})
-- Add to current selection by color
app.command.MaskByColor {
color = Color{ index=2 },
tolerance = 0,
mode = SelectionMode.ADD,
}
app.fgColor = Color{ index=4 }
app.command.Fill()
expect_img(i, {
4, 4, 4, 4, 4,
4, 4, 4, 4, 4,
4, 4, 4, 4, 4,
4, 4, 4, 4, 4,
4, 4, 4, 4, 4,
})
-- Reset image for new test
array_to_pixels({
1, 1, 0, 0, 1,
1, 1, 0, 0, 1,
1, 0, 0, 0, 1,
1, 0, 0, 1, 1,
1, 0, 0, 1, 1,
}, i)
-- Select a centered 3x3 square
app.command.MaskAll {}
app.command.ModifySelection {
modifier = 'contract',
quantity = 1,
brush = 'square'
}
-- Intersect with current selection by color
app.command.MaskByColor {
color = Color{ index=1 },
tolerance = 0,
mode = SelectionMode.INTERSECT,
}
app.fgColor = Color{ index=2 }
app.command.Fill()
expect_img(i, {
1, 1, 0, 0, 1,
1, 2, 0, 0, 1,
1, 0, 0, 0, 1,
1, 0, 0, 2, 1,
1, 0, 0, 1, 1,
})
end