1// Aseprite
2// Copyright (C) 2018-2021 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/app.h"
13#include "app/cmd/set_mask.h"
14#include "app/color.h"
15#include "app/color_utils.h"
16#include "app/commands/command.h"
17#include "app/context.h"
18#include "app/context_access.h"
19#include "app/doc.h"
20#include "app/ini_file.h"
21#include "app/i18n/strings.h"
22#include "app/modules/editors.h"
23#include "app/modules/gui.h"
24#include "app/tx.h"
25#include "app/ui/color_bar.h"
26#include "app/ui/color_button.h"
27#include "app/ui/selection_mode_field.h"
28#include "base/chrono.h"
29#include "base/convert_to.h"
30#include "base/scoped_value.h"
31#include "doc/image.h"
32#include "doc/mask.h"
33#include "doc/sprite.h"
34#include "ui/box.h"
35#include "ui/button.h"
36#include "ui/label.h"
37#include "ui/slider.h"
38#include "ui/tooltips.h"
39#include "ui/widget.h"
40#include "ui/window.h"
41
42// Uncomment to see the performance of doc::MaskBoundaries ctor
43//#define SHOW_BOUNDARIES_GEN_PERFORMANCE
44
45namespace app {
46
47using namespace ui;
48
49static const char* ConfigSection = "MaskColor";
50
51class MaskByColorCommand : public Command {
52public:
53 MaskByColorCommand();
54
55protected:
56 bool onEnabled(Context* context) override;
57 void onExecute(Context* context) override;
58
59private:
60 Mask* generateMask(const Mask& origMask,
61 const Sprite* sprite,
62 const Image* image,
63 int xpos, int ypos,
64 gen::SelectionMode mode);
65 void maskPreview(const ContextReader& reader);
66
67 class SelModeField : public SelectionModeField {
68 public:
69 obs::signal<void()> ModeChange;
70 protected:
71 void onSelectionModeChange(gen::SelectionMode mode) override {
72 ModeChange();
73 }
74 };
75
76 Window* m_window = nullptr;
77 ColorButton* m_buttonColor = nullptr;
78 CheckBox* m_checkPreview = nullptr;
79 Slider* m_sliderTolerance = nullptr;
80 SelModeField* m_selMode = nullptr;
81};
82
83MaskByColorCommand::MaskByColorCommand()
84 : Command(CommandId::MaskByColor(), CmdUIOnlyFlag)
85{
86}
87
88bool MaskByColorCommand::onEnabled(Context* context)
89{
90 return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
91 ContextFlags::HasActiveSprite |
92 ContextFlags::HasActiveImage);
93}
94
95void MaskByColorCommand::onExecute(Context* context)
96{
97 ASSERT(!m_window);
98
99 const ContextReader reader(context);
100 const Sprite* sprite = reader.sprite();
101
102 if (!App::instance()->isGui() || !sprite)
103 return;
104
105 int xpos, ypos;
106 const Image* image = reader.image(&xpos, &ypos);
107 if (!image)
108 return;
109
110 std::unique_ptr<Window> win(
111 new Window(Window::WithTitleBar, Strings::mask_by_color_title()));
112 base::ScopedValue<Window*> setWindow(m_window, win.get(), nullptr);
113 TooltipManager* tooltipManager = new TooltipManager();
114 m_window->addChild(tooltipManager);
115 auto box1 = new Box(VERTICAL);
116 auto box2 = new Box(HORIZONTAL);
117 auto box3 = new Box(HORIZONTAL);
118 auto box4 = new Box(HORIZONTAL | HOMOGENEOUS);
119 auto label_color = new Label(Strings::mask_by_color_label_color());
120 m_buttonColor = new ColorButton(
121 ColorBar::instance()->getFgColor(),
122 sprite->pixelFormat(),
123 ColorButtonOptions());
124 auto label_tolerance = new Label(Strings::mask_by_color_tolerance());
125 m_sliderTolerance = new Slider(0, 255, get_config_int(ConfigSection, "Tolerance", 0));
126
127 m_selMode = new SelModeField;
128 m_selMode->setupTooltips(tooltipManager);
129
130 m_checkPreview = new CheckBox(Strings::mask_by_color_preview());
131 auto button_ok = new Button(Strings::mask_by_color_ok());
132 auto button_cancel = new Button(Strings::mask_by_color_cancel());
133
134 m_checkPreview->processMnemonicFromText();
135 button_ok->processMnemonicFromText();
136 button_cancel->processMnemonicFromText();
137
138 if (get_config_bool(ConfigSection, "Preview", true))
139 m_checkPreview->setSelected(true);
140
141 button_ok->Click.connect([this, button_ok]{ m_window->closeWindow(button_ok); });
142 button_cancel->Click.connect([this, button_cancel]{ m_window->closeWindow(button_cancel); });
143
144 m_buttonColor->Change.connect([&]{ maskPreview(reader); });
145 m_sliderTolerance->Change.connect([&]{ maskPreview(reader); });
146 m_checkPreview->Click.connect([&]{ maskPreview(reader); });
147 m_selMode->ModeChange.connect([&]{ maskPreview(reader); });
148
149 button_ok->setFocusMagnet(true);
150 m_buttonColor->setExpansive(true);
151 m_sliderTolerance->setExpansive(true);
152 box2->setExpansive(true);
153
154 m_window->addChild(box1);
155 box1->addChild(m_selMode);
156 box1->addChild(box2);
157 box1->addChild(box3);
158 box1->addChild(m_checkPreview);
159 box1->addChild(box4);
160 box2->addChild(label_color);
161 box2->addChild(m_buttonColor);
162 box3->addChild(label_tolerance);
163 box3->addChild(m_sliderTolerance);
164 box4->addChild(button_ok);
165 box4->addChild(button_cancel);
166
167 // Default position
168 m_window->remapWindow();
169 m_window->centerWindow();
170
171 // Mask first preview
172 maskPreview(reader);
173
174 // Load window configuration
175 load_window_pos(m_window, ConfigSection);
176
177 // Open the window
178 m_window->openWindowInForeground();
179
180 bool apply = (m_window->closer() == button_ok);
181
182 ContextWriter writer(reader);
183 Doc* document(writer.document());
184
185 if (apply) {
186 Tx tx(writer.context(), "Mask by Color", DoesntModifyDocument);
187 std::unique_ptr<Mask> mask(generateMask(*document->mask(),
188 sprite, image, xpos, ypos,
189 m_selMode->selectionMode()));
190 tx(new cmd::SetMask(document, mask.get()));
191 tx.commit();
192
193 set_config_int(ConfigSection, "Tolerance", m_sliderTolerance->getValue());
194 set_config_bool(ConfigSection, "Preview", m_checkPreview->isSelected());
195 }
196 else {
197 document->generateMaskBoundaries();
198 }
199
200 // Update boundaries and editors.
201 update_screen_for_document(document);
202
203 // Save window configuration.
204 save_window_pos(m_window, ConfigSection);
205}
206
207Mask* MaskByColorCommand::generateMask(const Mask& origMask,
208 const Sprite* sprite,
209 const Image* image,
210 int xpos, int ypos,
211 gen::SelectionMode mode)
212{
213 int color = color_utils::color_for_image(m_buttonColor->getColor(),
214 sprite->pixelFormat());
215 int tolerance = m_sliderTolerance->getValue();
216
217 std::unique_ptr<Mask> mask(new Mask());
218 mask->byColor(image, color, tolerance);
219 mask->offsetOrigin(xpos, ypos);
220
221 if (!origMask.isEmpty()) {
222 switch (mode) {
223 case gen::SelectionMode::DEFAULT:
224 break;
225 case gen::SelectionMode::ADD:
226 mask->add(origMask);
227 break;
228 case gen::SelectionMode::SUBTRACT: {
229 if (!mask->isEmpty()) {
230 Mask mask2(origMask);
231 mask2.subtract(*mask);
232 mask->replace(mask2);
233 }
234 else {
235 mask->replace(origMask);
236 }
237 break;
238 }
239 case gen::SelectionMode::INTERSECT: {
240 mask->intersect(origMask);
241 break;
242 }
243 }
244 }
245
246 return mask.release();
247}
248
249void MaskByColorCommand::maskPreview(const ContextReader& reader)
250{
251 ASSERT(m_window);
252 if (m_window && m_checkPreview->isSelected()) {
253 int xpos, ypos;
254 const Image* image = reader.image(&xpos, &ypos);
255 std::unique_ptr<Mask> mask(generateMask(*reader.document()->mask(),
256 reader.sprite(), image,
257 xpos, ypos,
258 m_selMode->selectionMode()));
259 {
260 ContextWriter writer(reader);
261
262#ifdef SHOW_BOUNDARIES_GEN_PERFORMANCE
263 base::Chrono chrono;
264#endif
265
266 writer.document()->generateMaskBoundaries(mask.get());
267
268#ifdef SHOW_BOUNDARIES_GEN_PERFORMANCE
269 double time = chrono.elapsed();
270 m_window->setText("Mask by Color (" + base::convert_to<std::string>(time) + ")");
271#endif
272
273 update_screen_for_document(writer.document());
274 }
275 }
276}
277
278Command* CommandFactory::createMaskByColorCommand()
279{
280 return new MaskByColorCommand;
281}
282
283} // namespace app
284