1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2017 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/button_set.h"
13
14#include "app/modules/gui.h"
15#include "app/ui/skin/skin_theme.h"
16#include "gfx/color.h"
17#include "os/surface.h"
18#include "ui/box.h"
19#include "ui/button.h"
20#include "ui/graphics.h"
21#include "ui/message.h"
22#include "ui/paint_event.h"
23#include "ui/size_hint_event.h"
24#include "ui/system.h"
25#include "ui/theme.h"
26#include "ui/widget.h"
27
28#include <algorithm>
29#include <cstdarg>
30
31namespace app {
32
33using namespace ui;
34using namespace app::skin;
35
36// Last selected item for ButtonSet activated on mouse up when the
37// mouse capture is get.
38static int g_itemBeforeCapture = -1;
39
40WidgetType buttonset_item_type()
41{
42 static WidgetType type = kGenericWidget;
43 if (type == kGenericWidget)
44 type = register_widget_type();
45 return type;
46}
47
48ButtonSet::Item::Item()
49 : Widget(buttonset_item_type())
50 , m_icon(NULL)
51 , m_hotColor(gfx::ColorNone)
52{
53 setup_mini_font(this);
54 setAlign(CENTER | MIDDLE);
55 setFocusStop(true);
56}
57
58void ButtonSet::Item::setHotColor(gfx::Color color)
59{
60 m_hotColor = color;
61}
62
63void ButtonSet::Item::setIcon(const SkinPartPtr& icon, bool mono)
64{
65 m_icon = icon;
66 m_mono = mono;
67 invalidate();
68}
69
70ButtonSet* ButtonSet::Item::buttonSet()
71{
72 return static_cast<ButtonSet*>(parent());
73}
74
75void ButtonSet::Item::onPaint(ui::PaintEvent& ev)
76{
77 auto theme = SkinTheme::get(this);
78 Graphics* g = ev.graphics();
79 gfx::Rect rc = clientBounds();
80 gfx::Color fg;
81 SkinPartPtr nw;
82 gfx::Rect boxRc, textRc, iconRc;
83 gfx::Size iconSize;
84 if (m_icon)
85 iconSize = m_icon->size();
86
87 getTextIconInfo(
88 &boxRc, &textRc, &iconRc,
89 CENTER | (hasText() ? BOTTOM: MIDDLE),
90 iconSize.w, iconSize.h);
91
92 Grid::Info info = buttonSet()->getChildInfo(this);
93 bool isLastCol = (info.col+info.hspan >= info.grid_cols);
94 bool isLastRow = (info.row+info.vspan >= info.grid_rows);
95
96 if (m_icon || isLastRow) {
97 textRc.y -= 2*guiscale();
98 iconRc.y -= 1*guiscale();
99 if (isLastRow && info.row > 0) iconRc.y -= 2*guiscale();
100 }
101
102 if (!gfx::is_transparent(bgColor()))
103 g->fillRect(bgColor(), g->getClipBounds());
104
105 if (isSelected() || hasMouseOver()) {
106 if (hasCapture()) {
107 nw = theme->parts.buttonsetItemPushed();
108 fg = theme->colors.buttonSelectedText();
109 }
110 else {
111 nw = (hasFocus() ? theme->parts.buttonsetItemHotFocused():
112 theme->parts.buttonsetItemHot());
113 fg = theme->colors.buttonHotText();
114 }
115 }
116 else {
117 nw = (hasFocus() ? theme->parts.buttonsetItemFocused():
118 theme->parts.buttonsetItemNormal());
119 fg = theme->colors.buttonNormalText();
120 }
121
122 if (!isLastCol)
123 rc.w += 1*guiscale();
124
125 if (!isLastRow) {
126 if (nw == theme->parts.buttonsetItemHotFocused())
127 rc.h += 2*guiscale();
128 else
129 rc.h += 3*guiscale();
130 }
131
132 theme->drawRect(g, rc, nw.get(),
133 gfx::is_transparent(m_hotColor));
134
135 if (!gfx::is_transparent(m_hotColor)) {
136 gfx::Rect rc2(rc);
137 gfx::Rect sprite(nw->spriteBounds());
138 gfx::Rect slices(nw->slicesBounds());
139 rc2.shrink(
140 gfx::Border(
141 slices.x-1, // TODO this "-1" is an ugly hack for the pal edit
142 // button, replace all this with styles
143 slices.y-1,
144 sprite.w-slices.w-slices.x-1,
145 sprite.h-slices.h-slices.y));
146 g->fillRect(m_hotColor, rc2);
147 }
148
149 if (m_icon) {
150 os::Surface* bmp = m_icon->bitmap(0);
151
152 if (!isEnabled())
153 g->drawColoredRgbaSurface(bmp, theme->colors.disabled(),
154 iconRc.x, iconRc.y);
155 else if (isSelected() && hasCapture())
156 g->drawColoredRgbaSurface(bmp, theme->colors.buttonSelectedText(),
157 iconRc.x, iconRc.y);
158 else if (m_mono)
159 g->drawColoredRgbaSurface(bmp, theme->colors.buttonNormalText(),
160 iconRc.x, iconRc.y);
161 else
162 g->drawRgbaSurface(bmp, iconRc.x, iconRc.y);
163 }
164
165 if (hasText()) {
166 g->setFont(AddRef(font()));
167 g->drawUIText(text(), fg, gfx::ColorNone, textRc.origin(), 0);
168 }
169}
170
171bool ButtonSet::Item::onProcessMessage(ui::Message* msg)
172{
173 switch (msg->type()) {
174
175 case kFocusEnterMessage:
176 case kFocusLeaveMessage:
177 if (isEnabled()) {
178 // TODO theme specific stuff
179 invalidate();
180 }
181 break;
182
183 case ui::kKeyDownMessage:
184 if (isEnabled() && hasText()) {
185 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
186 bool mnemonicPressed = (msg->altPressed() &&
187 isMnemonicPressed(keymsg));
188
189 if (mnemonicPressed ||
190 (hasFocus() && keymsg->scancode() == kKeySpace)) {
191 buttonSet()->onSelectItem(this, true, msg);
192 onClick();
193 }
194 }
195 break;
196
197 case ui::kMouseDownMessage:
198 // Only for single-item and trigerred on mouse up ButtonSets: We
199 // save the current selected item to restore it just in case the
200 // user leaves the ButtonSet without releasing the mouse button
201 // and the mouse capture if offered to other ButtonSet.
202 if (buttonSet()->m_triggerOnMouseUp) {
203 // g_itemBeforeCapture can be >= 0 if we clicked other button
204 // without releasing the first button.
205 //ASSERT(g_itemBeforeCapture < 0);
206 g_itemBeforeCapture = buttonSet()->selectedItem();
207 }
208
209 captureMouse();
210 buttonSet()->onSelectItem(this, true, msg);
211 invalidate();
212
213 if (static_cast<MouseMessage*>(msg)->left() &&
214 !buttonSet()->m_triggerOnMouseUp) {
215 onClick();
216 }
217 break;
218
219 case ui::kMouseUpMessage:
220 if (hasCapture()) {
221 if (g_itemBeforeCapture >= 0)
222 g_itemBeforeCapture = -1;
223
224 releaseMouse();
225 invalidate();
226
227 if (static_cast<MouseMessage*>(msg)->left()) {
228 if (buttonSet()->m_triggerOnMouseUp)
229 onClick();
230 }
231 else if (static_cast<MouseMessage*>(msg)->right()) {
232 onRightClick();
233 }
234 }
235 break;
236
237 case ui::kMouseMoveMessage:
238 if (hasCapture()) {
239 if (buttonSet()->m_offerCapture) {
240 if (offerCapture(static_cast<ui::MouseMessage*>(msg), buttonset_item_type())) {
241 // Only for ButtonSets trigerred on mouse up.
242 if (buttonSet()->m_triggerOnMouseUp &&
243 g_itemBeforeCapture >= 0) {
244 if (g_itemBeforeCapture < (int)children().size()) {
245 Item* item = dynamic_cast<Item*>(at(g_itemBeforeCapture));
246 ASSERT(item);
247
248 // As we never received a kMouseUpMessage (so we never
249 // called onClick()), we have to restore the selected
250 // item at the point when we received the mouse capture.
251 buttonSet()->onSelectItem(item, true, msg);
252 }
253 g_itemBeforeCapture = -1;
254 }
255 }
256 }
257 }
258 break;
259
260 case ui::kMouseLeaveMessage:
261 case ui::kMouseEnterMessage:
262 invalidate();
263 break;
264 }
265 return Widget::onProcessMessage(msg);
266}
267
268void ButtonSet::Item::onSizeHint(ui::SizeHintEvent& ev)
269{
270 gfx::Size iconSize;
271 if (m_icon) {
272 iconSize = m_icon->size();
273 iconSize.w = std::max(iconSize.w, 16*guiscale());
274 iconSize.h = std::max(iconSize.h, 16*guiscale());
275 }
276
277 gfx::Rect boxRc;
278 getTextIconInfo(
279 &boxRc, NULL, NULL,
280 CENTER | (hasText() ? BOTTOM: MIDDLE),
281 iconSize.w, iconSize.h);
282
283 gfx::Size sz = boxRc.size();
284 if (hasText())
285 sz += 8*guiscale();
286
287 Grid::Info info = buttonSet()->getChildInfo(this);
288 if (info.row == info.grid_rows-1)
289 sz.h += 3*guiscale();
290
291 ev.setSizeHint(sz);
292}
293
294void ButtonSet::Item::onClick()
295{
296 buttonSet()->onItemChange(this);
297}
298
299void ButtonSet::Item::onRightClick()
300{
301 buttonSet()->onRightClick(this);
302}
303
304ButtonSet::ButtonSet(int columns)
305 : Grid(columns, false)
306 , m_offerCapture(true)
307 , m_triggerOnMouseUp(false)
308 , m_multiMode(MultiMode::One)
309{
310 InitTheme.connect(
311 [this]{
312 noBorderNoChildSpacing();
313 });
314 initTheme();
315}
316
317ButtonSet::Item* ButtonSet::addItem(const std::string& text, int hspan, int vspan)
318{
319 Item* item = new Item();
320 item->setText(text);
321 addItem(item, hspan, vspan);
322 return item;
323}
324
325ButtonSet::Item* ButtonSet::addItem(const skin::SkinPartPtr& icon, int hspan, int vspan)
326{
327 Item* item = new Item();
328 item->setIcon(icon);
329 addItem(item, hspan, vspan);
330 return item;
331}
332
333ButtonSet::Item* ButtonSet::addItem(Item* item, int hspan, int vspan)
334{
335 addChildInCell(item, hspan, vspan, HORIZONTAL | VERTICAL);
336 return item;
337}
338
339ButtonSet::Item* ButtonSet::getItem(int index)
340{
341 return dynamic_cast<Item*>(at(index));
342}
343
344int ButtonSet::getItemIndex(const Item* item) const
345{
346 int index = 0;
347 for (Widget* child : children()) {
348 if (child == item)
349 return index;
350 ++index;
351 }
352 return -1;
353}
354
355int ButtonSet::selectedItem() const
356{
357 int index = 0;
358 for (Widget* child : children()) {
359 if (child->isSelected())
360 return index;
361 ++index;
362 }
363 return -1;
364}
365
366int ButtonSet::countSelectedItems() const
367{
368 int count = 0;
369 for (auto child : children())
370 if (child->isSelected())
371 ++count;
372 return count;
373}
374
375void ButtonSet::setSelectedItem(int index, bool focusItem)
376{
377 if (index >= 0 && index < (int)children().size())
378 setSelectedItem(static_cast<Item*>(at(index)), focusItem);
379 else
380 setSelectedItem(static_cast<Item*>(nullptr), focusItem);
381}
382
383void ButtonSet::setSelectedItem(Item* item, bool focusItem)
384{
385 onSelectItem(item, focusItem, nullptr);
386}
387
388void ButtonSet::onSelectItem(Item* item, bool focusItem, ui::Message* msg)
389{
390 const int count = countSelectedItems();
391
392 if ((m_multiMode == MultiMode::One) ||
393 (m_multiMode == MultiMode::OneOrMore &&
394 msg &&
395 !msg->shiftPressed() &&
396 !msg->altPressed() &&
397 !msg->ctrlPressed() &&
398 !msg->cmdPressed())) {
399 if (item && item->isSelected() &&
400 ((m_multiMode == MultiMode::One) ||
401 (m_multiMode == MultiMode::OneOrMore && count == 1)))
402 return;
403
404 if (m_multiMode == MultiMode::One) {
405 if (auto sel = findSelectedItem())
406 sel->setSelected(false);
407 }
408 else if (m_multiMode == MultiMode::OneOrMore) {
409 for (auto child : children())
410 child->setSelected(false);
411 }
412 }
413
414 if (item) {
415 if (m_multiMode == MultiMode::OneOrMore) {
416 // Item already selected
417 if (count == 1 && item == findSelectedItem())
418 return;
419 }
420
421 // Toggle item
422 item->setSelected(!item->isSelected());
423 if (focusItem)
424 item->requestFocus();
425 }
426}
427
428void ButtonSet::deselectItems()
429{
430 Item* sel = findSelectedItem();
431 if (sel)
432 sel->setSelected(false);
433}
434
435void ButtonSet::setOfferCapture(bool state)
436{
437 m_offerCapture = state;
438}
439
440void ButtonSet::setTriggerOnMouseUp(bool state)
441{
442 m_triggerOnMouseUp = state;
443}
444
445void ButtonSet::setMultiMode(MultiMode mode)
446{
447 m_multiMode = mode;
448}
449
450void ButtonSet::onItemChange(Item* item)
451{
452 ItemChange(item);
453}
454
455void ButtonSet::onRightClick(Item* item)
456{
457 RightClick(item);
458}
459
460ButtonSet::Item* ButtonSet::findSelectedItem() const
461{
462 for (auto child : children()) {
463 if (child->isSelected())
464 return static_cast<Item*>(child);
465 }
466 return nullptr;
467}
468
469} // namespace app
470