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 | |
49 | namespace app { |
50 | |
51 | using namespace app::skin; |
52 | using namespace doc; |
53 | using namespace ui; |
54 | |
55 | namespace { |
56 | |
57 | void (PopupWindow* , |
58 | Menu* , |
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 | |
77 | class SelectBrushItem : public ButtonSet::Item { |
78 | public: |
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 | |
96 | private: |
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 | |
122 | class BrushShortcutItem : public ButtonSet::Item { |
123 | public: |
124 | BrushShortcutItem(const std::string& text, int slot) |
125 | : m_slot(slot) { |
126 | setText(text); |
127 | } |
128 | |
129 | private: |
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 | |
150 | class BrushOptionsItem : public ButtonSet::Item { |
151 | public: |
152 | (BrushPopup* , 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 | |
160 | private: |
161 | |
162 | void onClick() override { |
163 | 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(¶ms); |
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 | |
229 | private: |
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* ; |
259 | AppBrushes& m_brushes; |
260 | BrushRef m_brush; |
261 | int m_slot; |
262 | bool m_changeFlags; |
263 | }; |
264 | |
265 | class NewCustomBrushItem : public ButtonSet::Item { |
266 | public: |
267 | NewCustomBrushItem() { |
268 | setText(Strings::brush_slot_params_save_brush()); |
269 | } |
270 | |
271 | private: |
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 | |
282 | class NewBrushOptionsItem : public ButtonSet::Item { |
283 | public: |
284 | NewBrushOptionsItem() { |
285 | auto theme = skin::SkinTheme::get(this); |
286 | setIcon(theme->parts.iconArrowDown(), true); |
287 | } |
288 | |
289 | private: |
290 | void onClick() override { |
291 | Menu ; |
292 | |
293 | menu.addChild(new Separator("Parameters to Save" , HORIZONTAL)); |
294 | |
295 | app::gen::BrushSlotParams params; |
296 | menu.addChild(¶ms); |
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 | |
342 | 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 | |
382 | void BrushPopup::(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 | |
398 | void BrushPopup::(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 | |
444 | void BrushPopup::() |
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 |
460 | os::SurfaceRef BrushPopup::(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 | |