diff --git a/data/gui.xml b/data/gui.xml
index a213b7e6e..a990a11bb 100644
--- a/data/gui.xml
+++ b/data/gui.xml
@@ -224,6 +224,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/strings/en.ini b/data/strings/en.ini
index 01ba2a067..94bcd2ade 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -359,6 +359,10 @@ ChangeBrush_DecrementAngle = Decrement Angle
ChangeBrush_DecrementSize = Decrement Size
ChangeBrush_IncrementAngle = Increment Angle
ChangeBrush_IncrementSize = Increment Size
+ChangeBrush_FlipX = Flip Horizontally
+ChangeBrush_FlipY = Flip Vertically
+ChangeBrush_FlipD = Flip Diagonally
+ChangeBrush_Rotate90CW = Rotate 90 CW
ChangeColor = Change Color: {0}
ChangeColor_IncrementFgIndex = Increment Foreground Index
ChangeColor_DecrementFgIndex = Decrement Foreground Index
diff --git a/src/app/commands/cmd_change_brush.cpp b/src/app/commands/cmd_change_brush.cpp
index 622897a45..57680f069 100644
--- a/src/app/commands/cmd_change_brush.cpp
+++ b/src/app/commands/cmd_change_brush.cpp
@@ -1,4 +1,5 @@
// Aseprite
+// Copyright (c) 2023 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@@ -8,8 +9,6 @@
#include "config.h"
#endif
-#include
-
#include "app/app.h"
#include "app/commands/command.h"
#include "app/commands/params.h"
@@ -17,14 +16,26 @@
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "app/tools/active_tool.h"
+#include "app/tools/ink.h"
#include "app/tools/tool.h"
#include "app/ui/context_bar.h"
+#include "app/ui/main_window.h"
#include "base/convert_to.h"
+#include "doc/algorithm/flip_image.h"
#include "doc/brush.h"
+#include "doc/image_ref.h"
+#include "doc/primitives.h"
+#include "doc/tile.h"
#include "fmt/format.h"
+#include
+#include
+
namespace app {
+using namespace doc;
+using namespace doc::algorithm;
+
class ChangeBrushCommand : public Command {
enum Change {
None,
@@ -32,6 +43,10 @@ class ChangeBrushCommand : public Command {
DecrementSize,
IncrementAngle,
DecrementAngle,
+ FlipX,
+ FlipY,
+ FlipD,
+ Rotate90CW,
CustomBrush,
};
@@ -63,6 +78,10 @@ void ChangeBrushCommand::onLoadParams(const Params& params)
else if (change == "decrement-size") m_change = DecrementSize;
else if (change == "increment-angle") m_change = IncrementAngle;
else if (change == "decrement-angle") m_change = DecrementAngle;
+ else if (change == "flip-x") m_change = FlipX;
+ else if (change == "flip-y") m_change = FlipY;
+ else if (change == "flip-d") m_change = FlipD;
+ else if (change == "rotate-90cw") m_change = Rotate90CW;
else if (change == "custom") m_change = CustomBrush;
if (m_change == CustomBrush)
@@ -73,37 +92,206 @@ void ChangeBrushCommand::onLoadParams(const Params& params)
void ChangeBrushCommand::onExecute(Context* context)
{
- // Change the brush of the selected tool in the toolbar (not the
- // active tool which might be different, e.g. the quick tool)
- tools::Tool* tool = App::instance()->activeToolManager()->activeTool();
- ToolPreferences::Brush& brush =
- Preferences::instance().tool(tool).brush;
+ auto app = App::instance();
+ auto atm = app->activeToolManager();
+ auto& pref = Preferences::instance();
+ auto contextBar = (app->mainWindow() ? app->mainWindow()->getContextBar():
+ nullptr);
+ const BrushRef brush = (contextBar ? contextBar->activeBrush():
+ nullptr);
+ const bool isImageBrush = (brush && brush->type() == kImageBrushType);
+
+ // Change the brush of the active tool (i.e. quick tool) only if the
+ // active tool is of paint/eraser (e.g. proximity tool for eraser),
+ // in other case (e.g. if the active tool is the Hand tool because
+ // the user pressed the Space bar modifier), we'll change the brush
+ // of the selected tool in the toolbar (ignoring the quick tool).
+ tools::Tool* tool = atm->activeTool();
+ if (!tool->getInk(0)->isPaint() &&
+ !tool->getInk(0)->isEffect() &&
+ !tool->getInk(0)->isEraser() &&
+ !tool->getInk(0)->isShading()) {
+ tool = atm->selectedTool();
+ }
+
+ ToolPreferences::Brush& brushPref = pref.tool(tool).brush;
switch (m_change) {
+
case None:
// Do nothing
break;
+
case IncrementSize:
- if (brush.size() < doc::Brush::kMaxBrushSize)
- brush.size(brush.size()+1);
- break;
case DecrementSize:
- if (brush.size() > doc::Brush::kMinBrushSize)
- brush.size(brush.size()-1);
+ // Resize x2 or x0.5 when resizing image brushes
+ if (isImageBrush) {
+ double scale = 1.0;
+ switch (m_change) {
+ case IncrementSize: scale = 2.0; break;
+ case DecrementSize: scale = 0.5; break;
+ }
+
+ gfx::Size size = brush->bounds().size();
+ size = gfx::Size(std::max(1, size.w * scale),
+ std::max(1, size.h * scale));
+
+ ImageRef newImg(Image::create(brush->image()->pixelFormat(), size.w, size.h));
+ ImageRef newMsk(Image::create(IMAGE_BITMAP, size.w, size.h));
+ const color_t bg = brush->image()->maskColor();
+ newImg->setMaskColor(bg);
+
+ newImg->clear(bg);
+ newMsk->clear(0);
+
+ resize_image(brush->image(), newImg.get(),
+ RESIZE_METHOD_NEAREST_NEIGHBOR,
+ nullptr, nullptr, bg);
+
+ resize_image(brush->maskBitmap(), newMsk.get(),
+ RESIZE_METHOD_NEAREST_NEIGHBOR,
+ nullptr, nullptr, 0);
+
+ // Create a copy of the brush (to avoid modifying the original
+ // brush from the AppBrushes stock)
+ BrushRef newBrush = std::make_shared(*brush);
+ newBrush->setImage(newImg.get(),
+ newMsk.get());
+ contextBar->setActiveBrush(newBrush);
+ }
+ else {
+ switch (m_change) {
+ case IncrementSize:
+ if (brushPref.size() < Brush::kMaxBrushSize)
+ brushPref.size(brushPref.size()+1);
+ break;
+ case DecrementSize:
+ if (brushPref.size() > Brush::kMinBrushSize)
+ brushPref.size(brushPref.size()-1);
+ break;
+ }
+ }
break;
+
case IncrementAngle:
- if (brush.angle() < 180)
- brush.angle(brush.angle()+1);
+ if (brushPref.angle() < 180)
+ brushPref.angle(brushPref.angle()+1);
break;
+
case DecrementAngle:
- if (brush.angle() > 0)
- brush.angle(brush.angle()-1);
+ if (brushPref.angle() > 0)
+ brushPref.angle(brushPref.angle()-1);
break;
+
+ case FlipX:
+ case FlipY:
+ if (isImageBrush) {
+ ImageRef newImg(Image::createCopy(brush->image()));
+ ImageRef newMsk(Image::createCopy(brush->maskBitmap()));
+ const gfx::Rect bounds = newImg->bounds();
+
+ switch (m_change) {
+ case FlipX:
+ flip_image(newImg.get(), bounds, FlipType::FlipHorizontal);
+ flip_image(newMsk.get(), bounds, FlipType::FlipHorizontal);
+ break;
+ case FlipY:
+ flip_image(newImg.get(), bounds, FlipType::FlipVertical);
+ flip_image(newMsk.get(), bounds, FlipType::FlipVertical);
+ break;
+ }
+
+ BrushRef newBrush = std::make_shared(*brush);
+ newBrush->setImage(newImg.get(),
+ newMsk.get());
+ contextBar->setActiveBrush(newBrush);
+ }
+ else {
+ switch (m_change) {
+ case FlipX:
+ brushPref.angle(brushPref.angle() < 0 ? 180 + brushPref.angle():
+ 180 - brushPref.angle());
+ break;
+ case FlipY:
+ brushPref.angle(-brushPref.angle());
+ break;
+ }
+ }
+ break;
+
+ case FlipD:
+ case Rotate90CW:
+ if (isImageBrush) {
+ const gfx::Rect origBounds = brush->bounds();
+ const int m = std::max(origBounds.w, origBounds.h);
+ const gfx::Rect maxBounds(0, 0, m, m);
+ ImageRef newImg(Image::create(brush->image()->pixelFormat(), m, m));
+ ImageRef newMsk(Image::create(IMAGE_BITMAP, m, m));
+ const color_t bg = brush->image()->maskColor();
+ newImg->setMaskColor(bg);
+
+ newImg->clear(bg);
+ newMsk->clear(0);
+
+ const gfx::Clip clip(0, 0, 0, 0, origBounds.w, origBounds.h);
+ newImg->copy(brush->image(), clip);
+ newMsk->copy(brush->maskBitmap(), clip);
+
+ gfx::Rect cropBounds;
+
+ switch (m_change) {
+
+ case FlipD:
+ flip_image(newImg.get(), maxBounds, FlipType::FlipDiagonal);
+ flip_image(newMsk.get(), maxBounds, FlipType::FlipDiagonal);
+ cropBounds = gfx::Rect(0, 0, origBounds.h, origBounds.w);
+ break;
+
+ case Rotate90CW:
+ // To rotate 90cw:
+ //
+ // A B -> C A
+ // C D D B
+ //
+ // We can flip Y and then flip D:
+ //
+ // A B -> C D -> C A
+ // C D A B D B
+ //
+ flip_image(newImg.get(), maxBounds, FlipType::FlipVertical);
+ flip_image(newImg.get(), maxBounds, FlipType::FlipDiagonal);
+ flip_image(newMsk.get(), maxBounds, FlipType::FlipVertical);
+ flip_image(newMsk.get(), maxBounds, FlipType::FlipDiagonal);
+
+ cropBounds = gfx::Rect(m - origBounds.h, 0,
+ origBounds.h, origBounds.w);
+ break;
+ }
+
+ ImageRef newImg2(crop_image(newImg.get(), cropBounds, bg));
+ ImageRef newMsk2(crop_image(newMsk.get(), cropBounds, bg));
+
+ BrushRef newBrush = std::make_shared(*brush);
+ newBrush->setImage(newImg.get(),
+ newMsk.get());
+ contextBar->setActiveBrush(newBrush);
+ }
+ break;
+
case CustomBrush:
- App::instance()->contextBar()
- ->setActiveBrushBySlot(tool, m_slot);
+ if (contextBar)
+ contextBar->setActiveBrushBySlot(tool, m_slot);
break;
}
+
+ // Notify the ActiveToolManager that the active brush changed, so we
+ // can show the new brush in the real (non-quick) active tool.
+ //
+ // E.g. Space key activates the Hand quick tool, Space+H flips the
+ // brush, but we are still in the Hand tool, in this case we'd like
+ // to go back to the original tool (e.g. Pencil) and show the new
+ // brush.
+ atm->brushChanged();
}
std::string ChangeBrushCommand::onGetFriendlyName() const
@@ -115,6 +303,10 @@ std::string ChangeBrushCommand::onGetFriendlyName() const
case DecrementSize: change = Strings::commands_ChangeBrush_DecrementSize(); break;
case IncrementAngle: change = Strings::commands_ChangeBrush_IncrementAngle(); break;
case DecrementAngle: change = Strings::commands_ChangeBrush_DecrementAngle(); break;
+ case FlipX: change = Strings::commands_ChangeBrush_FlipX(); break;
+ case FlipY: change = Strings::commands_ChangeBrush_FlipY(); break;
+ case FlipD: change = Strings::commands_ChangeBrush_FlipD(); break;
+ case Rotate90CW: change = Strings::commands_ChangeBrush_Rotate90CW(); break;
case CustomBrush:
change = fmt::format(Strings::commands_ChangeBrush_CustomBrush(), m_slot);
break;
diff --git a/src/app/tools/active_tool.cpp b/src/app/tools/active_tool.cpp
index 594b46cb3..ea09997b3 100644
--- a/src/app/tools/active_tool.cpp
+++ b/src/app/tools/active_tool.cpp
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2018-2020 Igara Studio S.A.
+// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2016 David Capello
//
// This program is distributed under the terms of
@@ -144,6 +144,12 @@ void ActiveToolManager::newQuickToolSelectedFromEditor(Tool* tool)
m_quickTool = tool;
}
+void ActiveToolManager::brushChanged()
+{
+ ActiveToolChangeTrigger trigger(this);
+ m_quickTool = nullptr;
+}
+
void ActiveToolManager::regularTipProximity()
{
if (m_proximityTool != nullptr) {
diff --git a/src/app/tools/active_tool.h b/src/app/tools/active_tool.h
index 04731bde2..ed71958ff 100644
--- a/src/app/tools/active_tool.h
+++ b/src/app/tools/active_tool.h
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2020 Igara Studio S.A.
+// Copyright (C) 2020-2023 Igara Studio S.A.
// Copyright (C) 2016 David Capello
//
// This program is distributed under the terms of
@@ -42,6 +42,7 @@ public:
// modify the active tool.
void newToolSelectedInToolBar(Tool* tool);
void newQuickToolSelectedFromEditor(Tool* tool);
+ void brushChanged();
void regularTipProximity();
void eraserTipProximity();
void pressButton(const Pointer& pointer);