1// Aseprite
2// Copyright (C) 2018-2022 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/ui/brush_popup.h"
13
14#include "app/app.h"
15#include "app/brush_slot.h"
16#include "app/commands/command.h"
17#include "app/commands/commands.h"
18#include "app/i18n/strings.h"
19#include "app/modules/gui.h"
20#include "app/modules/palettes.h"
21#include "app/pref/preferences.h"
22#include "app/tools/tool.h"
23#include "app/ui/app_menuitem.h"
24#include "app/ui/button_set.h"
25#include "app/ui/context_bar.h"
26#include "app/ui/keyboard_shortcuts.h"
27#include "app/ui/main_window.h"
28#include "app/ui/skin/skin_theme.h"
29#include "app/ui_context.h"
30#include "app/util/conversion_to_surface.h"
31#include "base/convert_to.h"
32#include "doc/brush.h"
33#include "doc/image.h"
34#include "doc/palette.h"
35#include "gfx/border.h"
36#include "gfx/region.h"
37#include "os/surface.h"
38#include "os/system.h"
39#include "ui/button.h"
40#include "ui/fit_bounds.h"
41#include "ui/link_label.h"
42#include "ui/listitem.h"
43#include "ui/menu.h"
44#include "ui/message.h"
45#include "ui/separator.h"
46
47#include "brush_slot_params.xml.h"
48
49namespace app {
50
51using namespace app::skin;
52using namespace doc;
53using namespace ui;
54
55namespace {
56
57void show_popup_menu(PopupWindow* popupWindow,
58 Menu* popupMenu,
59 const gfx::Point& pt,
60 Display* display)
61{
62 // Add the menu window region when it popups to the the BrushPopup
63 // hot region, so when we click inside the popup menu it doesn't
64 // close the BrushPopup.
65 obs::scoped_connection c = popupMenu->OpenPopup.connect([popupWindow, popupMenu]{
66 gfx::Region rgn(popupWindow->boundsOnScreen());
67 rgn |= gfx::Region(popupMenu->boundsOnScreen());
68 popupWindow->setHotRegion(rgn);
69 });
70
71 popupMenu->showPopup(pt, display);
72
73 // Restore hot region of the BrushPopup window
74 popupWindow->setHotRegion(gfx::Region(popupWindow->boundsOnScreen()));
75}
76
77class SelectBrushItem : public ButtonSet::Item {
78public:
79 SelectBrushItem(const BrushSlot& brush, int slot = -1)
80 : m_brushes(App::instance()->brushes())
81 , m_brush(brush)
82 , m_slot(slot) {
83 if (m_brush.hasBrush()) {
84 SkinPartPtr icon(new SkinPart);
85 icon->setBitmap(0, BrushPopup::createSurfaceForBrush(
86 m_brush.brush(),
87 m_brush.hasFlag(BrushSlot::Flags::ImageColor)));
88 setIcon(icon);
89 }
90 }
91
92 const BrushSlot& brush() const {
93 return m_brush;
94 }
95
96private:
97 void onClick() override {
98 ContextBar* contextBar = App::instance()->contextBar();
99 tools::Tool* tool = App::instance()->activeTool();
100
101 if (m_slot >= 0)
102 contextBar->setActiveBrushBySlot(tool, m_slot);
103 else if (m_brush.hasBrush()) {
104 auto& brushPref = Preferences::instance().tool(tool).brush;
105 BrushRef brush;
106
107 brush.reset(
108 new Brush(
109 static_cast<doc::BrushType>(m_brush.brush()->type()),
110 brushPref.size(),
111 brushPref.angle()));
112
113 contextBar->setActiveBrush(brush);
114 }
115 }
116
117 AppBrushes& m_brushes;
118 BrushSlot m_brush;
119 int m_slot;
120};
121
122class BrushShortcutItem : public ButtonSet::Item {
123public:
124 BrushShortcutItem(const std::string& text, int slot)
125 : m_slot(slot) {
126 setText(text);
127 }
128
129private:
130 void onClick() override {
131 Params params;
132 params.set("change", "custom");
133 params.set("slot", base::convert_to<std::string>(m_slot).c_str());
134 Command* cmd = Commands::instance()->byId(CommandId::ChangeBrush());
135 cmd->loadParams(params);
136 std::string search = cmd->friendlyName();
137 if (!search.empty()) {
138 params.clear();
139 params.set("search", search.c_str());
140 cmd = Commands::instance()->byId(CommandId::KeyboardShortcuts());
141 ASSERT(cmd);
142 if (cmd)
143 UIContext::instance()->executeCommand(cmd, params);
144 }
145 }
146
147 int m_slot;
148};
149
150class BrushOptionsItem : public ButtonSet::Item {
151public:
152 BrushOptionsItem(BrushPopup* popup, int slot)
153 : m_popup(popup)
154 , m_brushes(App::instance()->brushes())
155 , m_slot(slot) {
156 auto theme = skin::SkinTheme::get(this);
157 setIcon(theme->parts.iconArrowDown(), true);
158 }
159
160private:
161
162 void onClick() override {
163 Menu menu;
164 AppMenuItem save(Strings::brush_slot_params_save_brush());
165 AppMenuItem lockItem(Strings::brush_slot_params_locked());
166 AppMenuItem deleteItem(Strings::brush_slot_params_delete());
167 AppMenuItem deleteAllItem(Strings::brush_slot_params_delete_all());
168
169 lockItem.setSelected(m_brushes.isBrushSlotLocked(m_slot));
170
171 save.Click.connect(&BrushOptionsItem::onSaveBrush, this);
172 lockItem.Click.connect(&BrushOptionsItem::onLockBrush, this);
173 deleteItem.Click.connect(&BrushOptionsItem::onDeleteBrush, this);
174 deleteAllItem.Click.connect(&BrushOptionsItem::onDeleteAllBrushes, this);
175
176 menu.addChild(&save);
177 menu.addChild(new MenuSeparator);
178 menu.addChild(&lockItem);
179 menu.addChild(&deleteItem);
180 menu.addChild(new MenuSeparator);
181 menu.addChild(&deleteAllItem);
182 menu.addChild(new Label(""));
183 menu.addChild(
184 new Separator(Strings::brush_slot_params_saved_parameters(), HORIZONTAL));
185
186 app::gen::BrushSlotParams params;
187 menu.addChild(&params);
188
189 // Load preferences
190 BrushSlot brush = m_brushes.getBrushSlot(m_slot);
191 params.brushType()->setSelected(brush.hasFlag(BrushSlot::Flags::BrushType));
192 params.brushSize()->setSelected(brush.hasFlag(BrushSlot::Flags::BrushSize));
193 params.brushAngle()->setSelected(brush.hasFlag(BrushSlot::Flags::BrushAngle));
194 params.fgColor()->setSelected(brush.hasFlag(BrushSlot::Flags::FgColor));
195 params.bgColor()->setSelected(brush.hasFlag(BrushSlot::Flags::BgColor));
196 params.imageColor()->setSelected(brush.hasFlag(BrushSlot::Flags::ImageColor));
197 params.inkType()->setSelected(brush.hasFlag(BrushSlot::Flags::InkType));
198 params.inkOpacity()->setSelected(brush.hasFlag(BrushSlot::Flags::InkOpacity));
199 params.shade()->setSelected(brush.hasFlag(BrushSlot::Flags::Shade));
200 params.pixelPerfect()->setSelected(brush.hasFlag(BrushSlot::Flags::PixelPerfect));
201
202 m_changeFlags = true;
203 show_popup_menu(m_popup, &menu,
204 gfx::Point(origin().x, origin().y+bounds().h),
205 display());
206
207 if (m_changeFlags) {
208 brush = m_brushes.getBrushSlot(m_slot);
209
210 int flags = (int(brush.flags()) & int(BrushSlot::Flags::Locked));
211 if (params.brushType()->isSelected()) flags |= int(BrushSlot::Flags::BrushType);
212 if (params.brushSize()->isSelected()) flags |= int(BrushSlot::Flags::BrushSize);
213 if (params.brushAngle()->isSelected()) flags |= int(BrushSlot::Flags::BrushAngle);
214 if (params.fgColor()->isSelected()) flags |= int(BrushSlot::Flags::FgColor);
215 if (params.bgColor()->isSelected()) flags |= int(BrushSlot::Flags::BgColor);
216 if (params.imageColor()->isSelected()) flags |= int(BrushSlot::Flags::ImageColor);
217 if (params.inkType()->isSelected()) flags |= int(BrushSlot::Flags::InkType);
218 if (params.inkOpacity()->isSelected()) flags |= int(BrushSlot::Flags::InkOpacity);
219 if (params.shade()->isSelected()) flags |= int(BrushSlot::Flags::Shade);
220 if (params.pixelPerfect()->isSelected()) flags |= int(BrushSlot::Flags::PixelPerfect);
221
222 if (brush.flags() != BrushSlot::Flags(flags)) {
223 brush.setFlags(BrushSlot::Flags(flags));
224 m_brushes.setBrushSlot(m_slot, brush);
225 }
226 }
227 }
228
229private:
230
231 void onSaveBrush() {
232 ContextBar* contextBar = App::instance()->contextBar();
233
234 m_brushes.setBrushSlot(
235 m_slot, contextBar->createBrushSlotFromPreferences());
236 m_brushes.lockBrushSlot(m_slot);
237
238 m_changeFlags = false;
239 }
240
241 void onLockBrush() {
242 if (m_brushes.isBrushSlotLocked(m_slot))
243 m_brushes.unlockBrushSlot(m_slot);
244 else
245 m_brushes.lockBrushSlot(m_slot);
246 }
247
248 void onDeleteBrush() {
249 m_brushes.removeBrushSlot(m_slot);
250 m_changeFlags = false;
251 }
252
253 void onDeleteAllBrushes() {
254 m_brushes.removeAllBrushSlots();
255 m_changeFlags = false;
256 }
257
258 BrushPopup* m_popup;
259 AppBrushes& m_brushes;
260 BrushRef m_brush;
261 int m_slot;
262 bool m_changeFlags;
263};
264
265class NewCustomBrushItem : public ButtonSet::Item {
266public:
267 NewCustomBrushItem() {
268 setText(Strings::brush_slot_params_save_brush());
269 }
270
271private:
272 void onClick() override {
273 ContextBar* contextBar = App::instance()->contextBar();
274
275 auto& brushes = App::instance()->brushes();
276 int slot = brushes.addBrushSlot(
277 contextBar->createBrushSlotFromPreferences());
278 brushes.lockBrushSlot(slot);
279 }
280};
281
282class NewBrushOptionsItem : public ButtonSet::Item {
283public:
284 NewBrushOptionsItem() {
285 auto theme = skin::SkinTheme::get(this);
286 setIcon(theme->parts.iconArrowDown(), true);
287 }
288
289private:
290 void onClick() override {
291 Menu menu;
292
293 menu.addChild(new Separator("Parameters to Save", HORIZONTAL));
294
295 app::gen::BrushSlotParams params;
296 menu.addChild(&params);
297
298 // Load preferences
299 auto& saveBrush = Preferences::instance().saveBrush;
300 params.brushType()->setSelected(saveBrush.brushType());
301 params.brushSize()->setSelected(saveBrush.brushSize());
302 params.brushAngle()->setSelected(saveBrush.brushAngle());
303 params.fgColor()->setSelected(saveBrush.fgColor());
304 params.bgColor()->setSelected(saveBrush.bgColor());
305 params.imageColor()->setSelected(saveBrush.imageColor());
306 params.inkType()->setSelected(saveBrush.inkType());
307 params.inkOpacity()->setSelected(saveBrush.inkOpacity());
308 params.shade()->setSelected(saveBrush.shade());
309 params.pixelPerfect()->setSelected(saveBrush.pixelPerfect());
310
311 show_popup_menu(static_cast<PopupWindow*>(window()),
312 &menu,
313 gfx::Point(origin().x, origin().y+bounds().h),
314 display());
315
316 // Save preferences
317 if (saveBrush.brushType() != params.brushType()->isSelected())
318 saveBrush.brushType(params.brushType()->isSelected());
319 if (saveBrush.brushSize() != params.brushSize()->isSelected())
320 saveBrush.brushSize(params.brushSize()->isSelected());
321 if (saveBrush.brushAngle() != params.brushAngle()->isSelected())
322 saveBrush.brushAngle(params.brushAngle()->isSelected());
323 if (saveBrush.fgColor() != params.fgColor()->isSelected())
324 saveBrush.fgColor(params.fgColor()->isSelected());
325 if (saveBrush.bgColor() != params.bgColor()->isSelected())
326 saveBrush.bgColor(params.bgColor()->isSelected());
327 if (saveBrush.imageColor() != params.imageColor()->isSelected())
328 saveBrush.imageColor(params.imageColor()->isSelected());
329 if (saveBrush.inkType() != params.inkType()->isSelected())
330 saveBrush.inkType(params.inkType()->isSelected());
331 if (saveBrush.inkOpacity() != params.inkOpacity()->isSelected())
332 saveBrush.inkOpacity(params.inkOpacity()->isSelected());
333 if (saveBrush.shade() != params.shade()->isSelected())
334 saveBrush.shade(params.shade()->isSelected());
335 if (saveBrush.pixelPerfect() != params.pixelPerfect()->isSelected())
336 saveBrush.pixelPerfect(params.pixelPerfect()->isSelected());
337 }
338};
339
340} // anonymous namespace
341
342BrushPopup::BrushPopup()
343 : PopupWindow("", ClickBehavior::CloseOnClickOutsideHotRegion)
344 , m_standardBrushes(3)
345 , m_customBrushes(nullptr)
346{
347 auto& brushes = App::instance()->brushes();
348
349 setAutoRemap(false);
350
351 m_standardBrushes.setTriggerOnMouseUp(true);
352
353 addChild(&m_box);
354
355 HBox* top = new HBox;
356 top->addChild(&m_standardBrushes);
357 top->addChild(new BoxFiller);
358
359 m_box.addChild(top);
360 m_box.addChild(new Separator("", HORIZONTAL));
361
362 for (const auto& brush : brushes.getStandardBrushes()) {
363 m_standardBrushes.addItem(
364 new SelectBrushItem(
365 BrushSlot(BrushSlot::Flags::BrushType, brush)))
366 ->setMono(true);
367 }
368 m_standardBrushes.setTransparent(true);
369
370 brushes.ItemsChange.connect(&BrushPopup::onBrushChanges, this);
371
372 InitTheme.connect(
373 [this]{
374 setBorder(gfx::Border(2)*guiscale());
375 setChildSpacing(0);
376 m_box.noBorderNoChildSpacing();
377 m_standardBrushes.setBgColor(gfx::ColorNone);
378 });
379 initTheme();
380}
381
382void BrushPopup::setBrush(Brush* brush)
383{
384 for (auto child : m_standardBrushes.children()) {
385 SelectBrushItem* item = static_cast<SelectBrushItem*>(child);
386
387 // Same type and same image
388 if (item->brush().hasBrush() &&
389 item->brush().brush()->type() == brush->type() &&
390 (brush->type() != kImageBrushType ||
391 item->brush().brush()->image() == brush->image())) {
392 m_standardBrushes.setSelectedItem(item);
393 return;
394 }
395 }
396}
397
398void BrushPopup::regenerate(ui::Display* display,
399 const gfx::Point& pos)
400{
401 auto& brushSlots = App::instance()->brushes().getBrushSlots();
402
403 if (m_customBrushes) {
404 // As BrushPopup::regenerate() can be called when a
405 // "m_customBrushes" button is clicked we cannot delete
406 // "m_customBrushes" right now.
407 m_customBrushes->parent()->removeChild(m_customBrushes);
408 m_customBrushes->deferDelete();
409 }
410
411 m_customBrushes = new ButtonSet(3);
412 m_customBrushes->setTriggerOnMouseUp(true);
413
414 int slot = 0;
415 for (const auto& brush : brushSlots) {
416 ++slot;
417
418 // Get shortcut
419 std::string shortcut;
420 {
421 Params params;
422 params.set("change", "custom");
423 params.set("slot", base::convert_to<std::string>(slot).c_str());
424 KeyPtr key = KeyboardShortcuts::instance()->command(
425 CommandId::ChangeBrush(), params);
426 if (key && !key->accels().empty())
427 shortcut = key->accels().front().toString();
428 }
429 m_customBrushes->addItem(new SelectBrushItem(brush, slot));
430 m_customBrushes->addItem(new BrushShortcutItem(shortcut, slot));
431 m_customBrushes->addItem(new BrushOptionsItem(this, slot));
432 }
433
434 m_customBrushes->addItem(new NewCustomBrushItem, 2, 1);
435 m_customBrushes->addItem(new NewBrushOptionsItem);
436 m_customBrushes->setExpansive(true);
437 m_box.addChild(m_customBrushes);
438
439 // Resize the window and change the hot region.
440 fit_bounds(display, this, gfx::Rect(pos, sizeHint()));
441 setHotRegion(gfx::Region(boundsOnScreen()));
442}
443
444void BrushPopup::onBrushChanges()
445{
446 if (isVisible()) {
447 gfx::Region rgn;
448 getDrawableRegion(rgn, kCutTopWindowsAndUseChildArea);
449
450 Display* mainDisplay = manager()->display();
451 regenerate(mainDisplay,
452 mainDisplay->nativeWindow()->pointFromScreen(boundsOnScreen().origin()));
453 invalidate();
454
455 parent()->invalidateRegion(rgn);
456 }
457}
458
459// static
460os::SurfaceRef BrushPopup::createSurfaceForBrush(const BrushRef& origBrush,
461 const bool useOriginalImage)
462{
463 Image* image = nullptr;
464 BrushRef brush = origBrush;
465 if (brush) {
466 if (brush->type() != kImageBrushType && brush->size() > 10) {
467 brush.reset(new Brush(*brush));
468 brush->setSize(10);
469 }
470 // Show the original image in the popup (without the image colors
471 // modified if there were some modification).
472 if (useOriginalImage)
473 image = brush->originalImage();
474 else
475 image = brush->image();
476 }
477
478 os::SurfaceRef surface = os::instance()->makeRgbaSurface(
479 std::min(10, image ? image->width(): 4),
480 std::min(10, image ? image->height(): 4));
481
482 if (image) {
483 Palette* palette = get_current_palette();
484 if (image->pixelFormat() == IMAGE_BITMAP) {
485 palette = new Palette(frame_t(0), 2);
486 palette->setEntry(0, rgba(0, 0, 0, 0));
487 palette->setEntry(1, rgba(0, 0, 0, 255));
488 }
489
490 convert_image_to_surface(
491 image, palette, surface.get(),
492 0, 0, 0, 0, image->width(), image->height());
493
494 if (image->pixelFormat() == IMAGE_BITMAP)
495 delete palette;
496 }
497 else {
498 surface->clear();
499 }
500
501 return surface;
502}
503
504} // namespace app
505