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 | |
45 | namespace app { |
46 | |
47 | using namespace ui; |
48 | |
49 | static const char* ConfigSection = "MaskColor" ; |
50 | |
51 | class MaskByColorCommand : public Command { |
52 | public: |
53 | MaskByColorCommand(); |
54 | |
55 | protected: |
56 | bool onEnabled(Context* context) override; |
57 | void onExecute(Context* context) override; |
58 | |
59 | private: |
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 | |
83 | MaskByColorCommand::MaskByColorCommand() |
84 | : Command(CommandId::MaskByColor(), CmdUIOnlyFlag) |
85 | { |
86 | } |
87 | |
88 | bool MaskByColorCommand::onEnabled(Context* context) |
89 | { |
90 | return context->checkFlags(ContextFlags::ActiveDocumentIsWritable | |
91 | ContextFlags::HasActiveSprite | |
92 | ContextFlags::HasActiveImage); |
93 | } |
94 | |
95 | void 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 | |
207 | Mask* 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 | |
249 | void 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 | |
278 | Command* CommandFactory::createMaskByColorCommand() |
279 | { |
280 | return new MaskByColorCommand; |
281 | } |
282 | |
283 | } // namespace app |
284 | |