diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..ca09cc0b5
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -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]
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7e099c19a..dd260d280 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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:
diff --git a/INSTALL.md b/INSTALL.md
index 7b6a69396..45f8501e7 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -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
diff --git a/data/strings/en.ini b/data/strings/en.ini
index 98311e711..5678095f1 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -859,6 +859,8 @@ x = X:
y = Y:
width = Width:
height = Height:
+columns = Columns:
+rows = Rows:
padding = Padding
horizontal_padding = Horizontal:
vertical_padding = Vertical:
diff --git a/data/widgets/import_sprite_sheet.xml b/data/widgets/import_sprite_sheet.xml
index 9472d8e9e..02655edc7 100644
--- a/data/widgets/import_sprite_sheet.xml
+++ b/data/widgets/import_sprite_sheet.xml
@@ -24,6 +24,14 @@
+
+
+
+
+
+
+
+
diff --git a/laf b/laf
index 4766fd958..3eee03a1f 160000
--- a/laf
+++ b/laf
@@ -1 +1 @@
-Subproject commit 4766fd95801af52b904146e79e5d89a916de49de
+Subproject commit 3eee03a1f3a3ce7b06e93b1106fc356bc0307c42
diff --git a/src/app/commands/cmd_import_sprite_sheet.cpp b/src/app/commands/cmd_import_sprite_sheet.cpp
index d40a313bc..70413415d 100644
--- a/src/app/commands/cmd_import_sprite_sheet.cpp
+++ b/src/app/commands/cmd_import_sprite_sheet.cpp
@@ -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 ui { this, true, "ui" };
Param type { this, app::SpriteSheetType::None, "type" };
Param frameBounds { this, gfx::Rect(0, 0, 0, 0), "frameBounds" };
Param padding { this, gfx::Size(0, 0), "padding" };
Param 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 columns { this, 0, "columns" };
+ Param 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
diff --git a/src/app/commands/cmd_mask_by_color.cpp b/src/app/commands/cmd_mask_by_color.cpp
index 71d6fad04..7e6ef2d94 100644
--- a/src/app/commands/cmd_mask_by_color.cpp
+++ b/src/app/commands/cmd_mask_by_color.cpp
@@ -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 ui { this, true, "ui" };
+ Param color { this, app::Color(), "color" };
+ Param tolerance { this, 0, "tolerance" };
+ Param 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 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 win(
- new Window(Window::WithTitleBar, Strings::mask_by_color_title()));
- base::ScopedValue 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(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(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(generateMask(*reader.document()->mask(),
- reader.sprite(), image,
- xpos, ypos,
- m_selMode->selectionMode()));
+class MaskByColorCommand : public CommandWithNewParams {
+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(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(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;
diff --git a/src/app/commands/new_params.cpp b/src/app/commands/new_params.cpp
index 184b30ee6..8ffb1e654 100644
--- a/src/app/commands/new_params.cpp
+++ b/src/app/commands/new_params.cpp
@@ -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::fromString(const std::string& value)
setValue(doc::RgbMapAlgorithm::DEFAULT);
}
+template<>
+void Param::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::fromLua(lua_State* L, int index)
setValue((doc::RgbMapAlgorithm)lua_tointeger(L, index));
}
+template<>
+void Param::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();
diff --git a/src/app/script/cel_class.cpp b/src/app/script/cel_class.cpp
index 30e49e3aa..a433653c2 100644
--- a/src/app/script/cel_class.cpp
+++ b/src/app/script/cel_class.cpp
@@ -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(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(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;
}
diff --git a/src/app/tools/tool_loop.h b/src/app/tools/tool_loop.h
index 377515400..36f07e081 100644
--- a/src/app/tools/tool_loop.h
+++ b/src/app/tools/tool_loop.h
@@ -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.)
diff --git a/src/app/tools/tool_loop_manager.cpp b/src/app/tools/tool_loop_manager.cpp
index 7bb4ab59d..874263a5a 100644
--- a/src/app/tools/tool_loop_manager.cpp
+++ b/src/app/tools/tool_loop_manager.cpp
@@ -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);
diff --git a/src/app/ui/editor/standby_state.cpp b/src/app/ui/editor/standby_state.cpp
index f21c35346..cb5d745f2 100644
--- a/src/app/ui/editor/standby_state.cpp
+++ b/src/app/ui/editor/standby_state.cpp
@@ -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);
}
}
diff --git a/src/app/ui/editor/tool_loop_impl.cpp b/src/app/ui/editor/tool_loop_impl.cpp
index 866f554b6..e44974d24 100644
--- a/src/app/ui/editor/tool_loop_impl.cpp
+++ b/src/app/ui/editor/tool_loop_impl.cpp
@@ -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; }
diff --git a/src/ui/button.cpp b/src/ui/button.cpp
index f7222a4b7..c950b2b16 100644
--- a/src/ui/button.cpp
+++ b/src/ui/button.cpp
@@ -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(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;
diff --git a/src/ui/widget.cpp b/src/ui/widget.cpp
index f0b3e29f9..0273c1563 100644
--- a/src/ui/widget.cpp
+++ b/src/ui/widget.cpp
@@ -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);
diff --git a/src/ui/widget.h b/src/ui/widget.h
index a8010d0a7..70e949f9f 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -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 InitTheme;
diff --git a/tests/scripts/app_command.lua b/tests/scripts/app_command.lua
index 4f70e5d1e..646b838ee 100644
--- a/tests/scripts/app_command.lua
+++ b/tests/scripts/app_command.lua
@@ -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