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/toolbar.h"
13
14#include "app/app.h"
15#include "app/commands/command.h"
16#include "app/commands/commands.h"
17#include "app/i18n/strings.h"
18#include "app/modules/editors.h"
19#include "app/modules/gfx.h"
20#include "app/tools/active_tool.h"
21#include "app/tools/tool_box.h"
22#include "app/ui/keyboard_shortcuts.h"
23#include "app/ui/main_window.h"
24#include "app/ui/preview_editor.h"
25#include "app/ui/skin/skin_theme.h"
26#include "app/ui/status_bar.h"
27#include "app/ui_context.h"
28#include "fmt/format.h"
29#include "gfx/size.h"
30#include "obs/signal.h"
31#include "os/surface.h"
32#include "ui/ui.h"
33
34#include <string>
35
36namespace app {
37
38using namespace app::skin;
39using namespace gfx;
40using namespace ui;
41using namespace tools;
42
43// Class to show a group of tools (horizontally)
44// This widget is inside the ToolBar::m_popupWindow
45class ToolBar::ToolStrip : public Widget {
46public:
47 ToolStrip(ToolGroup* group, ToolBar* toolbar);
48 ~ToolStrip();
49
50 ToolGroup* toolGroup() { return m_group; }
51
52 obs::signal<void(Tool*)> ToolSelected;
53
54protected:
55 bool onProcessMessage(Message* msg) override;
56 void onSizeHint(SizeHintEvent& ev) override;
57 void onPaint(PaintEvent& ev) override;
58
59private:
60 Rect getToolBounds(int index);
61
62 ToolGroup* m_group;
63 Tool* m_hotTool;
64 ToolBar* m_toolbar;
65};
66
67static Size getToolIconSize(Widget* widget)
68{
69 auto theme = SkinTheme::get(widget);
70 os::Surface* icon = theme->getToolIcon("configuration");
71 if (icon)
72 return Size(icon->width(), icon->height());
73 else
74 return Size(16, 16) * guiscale();
75}
76
77//////////////////////////////////////////////////////////////////////
78// ToolBar
79
80ToolBar* ToolBar::m_instance = NULL;
81
82ToolBar::ToolBar()
83 : Widget(kGenericWidget)
84 , m_openedRecently(false)
85 , m_tipTimer(300, this)
86{
87 m_instance = this;
88
89 setBorder(gfx::Border(1*guiscale(), 0, 1*guiscale(), 0));
90
91 m_hotTool = NULL;
92 m_hotIndex = NoneIndex;
93 m_openOnHot = false;
94 m_popupWindow = NULL;
95 m_currentStrip = NULL;
96 m_tipWindow = NULL;
97 m_tipOpened = false;
98
99 ToolBox* toolbox = App::instance()->toolBox();
100 for (Tool* tool : *toolbox) {
101 if (m_selectedInGroup.find(tool->getGroup()) == m_selectedInGroup.end())
102 m_selectedInGroup[tool->getGroup()] = tool;
103 }
104
105 App::instance()->activeToolManager()->add_observer(this);
106}
107
108ToolBar::~ToolBar()
109{
110 App::instance()->activeToolManager()->remove_observer(this);
111
112 delete m_popupWindow;
113 delete m_tipWindow;
114}
115
116bool ToolBar::isToolVisible(Tool* tool)
117{
118 return (m_selectedInGroup[tool->getGroup()] == tool);
119}
120
121bool ToolBar::onProcessMessage(Message* msg)
122{
123 switch (msg->type()) {
124
125 case kMouseDownMessage: {
126 auto mouseMsg = static_cast<const MouseMessage*>(msg);
127 const Point mousePos = mouseMsg->positionForDisplay(display());
128 ToolBox* toolbox = App::instance()->toolBox();
129 int groups = toolbox->getGroupsCount();
130 Rect toolrc;
131
132 ToolGroupList::iterator it = toolbox->begin_group();
133 for (int c=0; c<groups; ++c, ++it) {
134 ToolGroup* tool_group = *it;
135 Tool* tool = m_selectedInGroup[tool_group];
136
137 toolrc = getToolGroupBounds(c);
138 if (mousePos.y >= toolrc.y &&
139 mousePos.y < toolrc.y+toolrc.h) {
140 selectTool(tool);
141
142 openPopupWindow(c, tool_group);
143
144 // We capture the mouse so the user can continue navigating
145 // the ToolBar to open other groups while he is pressing the
146 // mouse button.
147 captureMouse();
148 }
149 }
150
151 toolrc = getToolGroupBounds(PreviewVisibilityIndex);
152 if (mousePos.y >= toolrc.y &&
153 mousePos.y < toolrc.y+toolrc.h) {
154 // Toggle preview visibility
155 PreviewEditorWindow* preview =
156 App::instance()->mainWindow()->getPreviewEditor();
157 bool state = preview->isPreviewEnabled();
158 preview->setPreviewEnabled(!state);
159 }
160 break;
161 }
162
163 case kMouseMoveMessage: {
164 auto mouseMsg = static_cast<const MouseMessage*>(msg);
165 const Point mousePos = mouseMsg->positionForDisplay(display());
166 ToolBox* toolbox = App::instance()->toolBox();
167 int groups = toolbox->getGroupsCount();
168 Tool* new_hot_tool = NULL;
169 int new_hot_index = NoneIndex;
170 Rect toolrc;
171
172 ToolGroupList::iterator it = toolbox->begin_group();
173
174 for (int c=0; c<groups; ++c, ++it) {
175 ToolGroup* tool_group = *it;
176 Tool* tool = m_selectedInGroup[tool_group];
177
178 toolrc = getToolGroupBounds(c);
179 if (mousePos.y >= toolrc.y &&
180 mousePos.y < toolrc.y+toolrc.h) {
181 new_hot_tool = tool;
182 new_hot_index = c;
183
184 if ((m_openOnHot) && (m_hotTool != new_hot_tool) && hasCapture()) {
185 openPopupWindow(c, tool_group);
186 }
187 break;
188 }
189 }
190
191 toolrc = getToolGroupBounds(PreviewVisibilityIndex);
192 if (mousePos.y >= toolrc.y &&
193 mousePos.y < toolrc.y+toolrc.h) {
194 new_hot_index = PreviewVisibilityIndex;
195 }
196
197 // hot button changed
198 if (new_hot_tool != m_hotTool ||
199 new_hot_index != m_hotIndex) {
200
201 m_hotTool = new_hot_tool;
202 m_hotIndex = new_hot_index;
203 invalidate();
204
205 if (!m_currentStrip) {
206 if (m_hotIndex != NoneIndex && !hasCapture())
207 openTipWindow(m_hotIndex, m_hotTool);
208 else
209 closeTipWindow();
210 }
211
212 if (m_hotTool) {
213 if (hasCapture())
214 selectTool(m_hotTool);
215 else
216 StatusBar::instance()->showTool(0, m_hotTool);
217 }
218 }
219
220 // We can change the current tool if the user is dragging the
221 // mouse over the ToolBar.
222 if (hasCapture()) {
223 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
224 Widget* pick = manager()->pickFromScreenPos(mouseMsg->screenPosition());
225 if (ToolStrip* strip = dynamic_cast<ToolStrip*>(pick)) {
226 releaseMouse();
227
228 MouseMessage* mouseMsg2 = new MouseMessage(
229 kMouseDownMessage,
230 *mouseMsg,
231 mouseMsg->positionForDisplay(strip->display()));
232 mouseMsg2->setRecipient(strip);
233 mouseMsg2->setDisplay(strip->display());
234 manager()->enqueueMessage(mouseMsg2);
235 }
236 }
237 break;
238 }
239
240 case kMouseUpMessage:
241 if (!hasCapture())
242 break;
243
244 if (!m_openedRecently) {
245 if (m_popupWindow && m_popupWindow->isVisible())
246 m_popupWindow->closeWindow(this);
247 }
248 m_openedRecently = false;
249
250 releaseMouse();
251 [[fallthrough]];
252
253 case kMouseLeaveMessage:
254 if (hasCapture())
255 break;
256
257 closeTipWindow();
258
259 if (!m_popupWindow || !m_popupWindow->isVisible()) {
260 m_tipOpened = false;
261
262 m_hotTool = NULL;
263 m_hotIndex = NoneIndex;
264 invalidate();
265 }
266
267 StatusBar::instance()->showDefaultText();
268 break;
269
270 case kTimerMessage:
271 if (static_cast<TimerMessage*>(msg)->timer() == &m_tipTimer) {
272 if (m_tipWindow)
273 m_tipWindow->openWindow();
274
275 m_tipTimer.stop();
276 m_tipOpened = true;
277 }
278 break;
279
280 }
281
282 return Widget::onProcessMessage(msg);
283}
284
285void ToolBar::onSizeHint(SizeHintEvent& ev)
286{
287 Size iconsize = getToolIconSize(this);
288 iconsize.w += border().width();
289 iconsize.h += border().height();
290 ev.setSizeHint(iconsize);
291}
292
293void ToolBar::onPaint(ui::PaintEvent& ev)
294{
295 gfx::Rect bounds = clientBounds();
296 Graphics* g = ev.graphics();
297 auto theme = SkinTheme::get(this);
298 ToolBox* toolbox = App::instance()->toolBox();
299 Tool* activeTool = App::instance()->activeTool();
300 ToolGroupList::iterator it = toolbox->begin_group();
301 int groups = toolbox->getGroupsCount();
302 Rect toolrc;
303
304 g->fillRect(theme->colors.tabActiveFace(), bounds);
305
306 for (int c=0; c<groups; ++c, ++it) {
307 ToolGroup* tool_group = *it;
308 Tool* tool = m_selectedInGroup[tool_group];
309 SkinPartPtr nw;
310
311 if (activeTool == tool || m_hotIndex == c) {
312 nw = theme->parts.toolbuttonHot();
313 }
314 else {
315 nw = c >= 0 && c < groups-1 ? theme->parts.toolbuttonNormal():
316 theme->parts.toolbuttonLast();
317 }
318
319 toolrc = getToolGroupBounds(c);
320 toolrc.offset(-origin());
321 theme->drawRect(g, toolrc, nw.get());
322
323 // Draw the tool icon
324 os::Surface* icon = theme->getToolIcon(tool->getId().c_str());
325 if (icon) {
326 g->drawRgbaSurface(icon,
327 toolrc.x+toolrc.w/2-icon->width()/2,
328 toolrc.y+toolrc.h/2-icon->height()/2);
329 }
330 }
331
332 // Draw button to show/hide preview
333 toolrc = getToolGroupBounds(PreviewVisibilityIndex);
334 toolrc.offset(-origin());
335 bool isHot = (m_hotIndex == PreviewVisibilityIndex ||
336 App::instance()->mainWindow()->getPreviewEditor()->isPreviewEnabled());
337 theme->drawRect(
338 g,
339 toolrc,
340 (isHot ? theme->parts.toolbuttonHot().get():
341 theme->parts.toolbuttonLast().get()));
342
343 os::Surface* icon = theme->getToolIcon("minieditor");
344 if (icon) {
345 g->drawRgbaSurface(icon,
346 toolrc.x+toolrc.w/2-icon->width()/2,
347 toolrc.y+toolrc.h/2-icon->height()/2);
348 }
349}
350
351void ToolBar::onVisible(bool visible)
352{
353 Widget::onVisible(visible);
354 if (!visible) {
355 if (m_popupWindow) {
356 closePopupWindow();
357 closeTipWindow();
358 }
359 }
360}
361
362int ToolBar::getToolGroupIndex(ToolGroup* group)
363{
364 ToolBox* toolbox = App::instance()->toolBox();
365 ToolGroupList::iterator it = toolbox->begin_group();
366 int groups = toolbox->getGroupsCount();
367
368 for (int c=0; c<groups; ++c, ++it) {
369 if (group == *it)
370 return c;
371 }
372
373 return -1;
374}
375
376void ToolBar::openPopupWindow(int group_index, ToolGroup* tool_group)
377{
378 if (m_popupWindow) {
379 // If we've already open the given group, do nothing.
380 if (m_currentStrip && m_currentStrip->toolGroup() == tool_group)
381 return;
382
383 if (m_closeConn)
384 m_closeConn.disconnect();
385
386 onClosePopup();
387 closePopupWindow();
388 }
389
390 // Close tip window
391 closeTipWindow();
392
393 // If this group contains only one tool, do not show the popup
394 ToolBox* toolbox = App::instance()->toolBox();
395 int count = 0;
396 for (ToolIterator it = toolbox->begin(); it != toolbox->end(); ++it) {
397 Tool* tool = *it;
398 if (tool->getGroup() == tool_group)
399 ++count;
400 }
401 m_openOnHot = true;
402 if (count <= 1)
403 return;
404
405 // In case this tool contains more than just one tool, show the popup window
406 m_popupWindow = new TransparentPopupWindow(PopupWindow::ClickBehavior::CloseOnClickOutsideHotRegion);
407 m_closeConn = m_popupWindow->Close.connect([this]{ onClosePopup(); });
408 m_openedRecently = true;
409
410 ToolStrip* toolstrip = new ToolStrip(tool_group, this);
411 m_currentStrip = toolstrip;
412 m_popupWindow->addChild(toolstrip);
413
414 Rect rc = getToolGroupBounds(group_index);
415 int w = 0;
416
417 for (Tool* tool : *toolbox) {
418 if (tool->getGroup() == tool_group)
419 w += bounds().w-border().width()-1;
420 }
421
422 rc.x -= w;
423 rc.w = w;
424
425 // Set hotregion of popup window
426 m_popupWindow->setAutoRemap(false);
427 ui::fit_bounds(display(), m_popupWindow, rc);
428 m_popupWindow->setBounds(rc);
429
430 Region rgn(m_popupWindow->boundsOnScreen().enlarge(16*guiscale()));
431 rgn.createUnion(rgn, Region(boundsOnScreen()));
432 m_popupWindow->setHotRegion(rgn);
433
434 m_popupWindow->openWindow();
435}
436
437void ToolBar::closePopupWindow()
438{
439 if (m_popupWindow) {
440 m_popupWindow->closeWindow(nullptr);
441 delete m_popupWindow;
442 m_popupWindow = nullptr;
443 }
444}
445
446Rect ToolBar::getToolGroupBounds(int group_index)
447{
448 ToolBox* toolbox = App::instance()->toolBox();
449 int groups = toolbox->getGroupsCount();
450 Size iconsize = getToolIconSize(this);
451 Rect rc(bounds());
452 rc.shrink(border());
453
454 switch (group_index) {
455
456 case PreviewVisibilityIndex:
457 rc.y += rc.h - iconsize.h - 2*guiscale();
458 rc.h = iconsize.h+2*guiscale();
459 break;
460
461 default:
462 rc.y += group_index*(iconsize.h-1*guiscale());
463 rc.h = group_index < groups-1 ? iconsize.h+1*guiscale():
464 iconsize.h+2*guiscale();
465 break;
466 }
467
468 return rc;
469}
470
471Point ToolBar::getToolPositionInGroup(int group_index, Tool* tool)
472{
473 ToolBox* toolbox = App::instance()->toolBox();
474 Size iconsize = getToolIconSize(this);
475 int nth = 0;
476
477 for (ToolIterator it = toolbox->begin(); it != toolbox->end(); ++it) {
478 if (tool == *it)
479 break;
480
481 if ((*it)->getGroup() == tool->getGroup()) {
482 ++nth;
483 }
484 }
485
486 return Point(iconsize.w/2+iconsize.w*nth, iconsize.h);
487}
488
489void ToolBar::openTipWindow(ToolGroup* tool_group, Tool* tool)
490{
491 openTipWindow(getToolGroupIndex(tool_group), tool);
492}
493
494void ToolBar::openTipWindow(int group_index, Tool* tool)
495{
496 if (m_tipWindow)
497 closeTipWindow();
498
499 std::string tooltip;
500 if (tool && group_index >= 0) {
501 tooltip = tool->getText();
502 if (tool->getTips().size() > 0) {
503 tooltip += ":\n";
504 tooltip += tool->getTips();
505 }
506
507 // Tool shortcut
508 KeyPtr key = KeyboardShortcuts::instance()->tool(tool);
509 if (key && !key->accels().empty()) {
510 tooltip += "\n\n";
511 tooltip += fmt::format(Strings::tools_shortcut(),
512 key->accels().front().toString());
513 }
514 }
515 else if (group_index == PreviewVisibilityIndex) {
516 if (App::instance()->mainWindow()->getPreviewEditor()->isPreviewEnabled())
517 tooltip = Strings::tools_preview_hide();
518 else
519 tooltip = Strings::tools_preview_show();
520 }
521 else
522 return;
523
524 m_tipWindow = new TipWindow(tooltip);
525 m_tipWindow->remapWindow();
526
527 Rect toolrc = getToolGroupBounds(group_index);
528 Point arrow = (tool ? getToolPositionInGroup(group_index, tool): Point(0, 0));
529 if (tool && m_popupWindow && m_popupWindow->isVisible())
530 toolrc.x += arrow.x - m_popupWindow->bounds().w;
531
532 m_tipWindow->pointAt(TOP | RIGHT, toolrc,
533 ui::Manager::getDefault()->display());
534
535 if (m_tipOpened)
536 m_tipWindow->openWindow();
537 else
538 m_tipTimer.start();
539}
540
541void ToolBar::closeTipWindow()
542{
543 m_tipTimer.stop();
544
545 if (m_tipWindow) {
546 m_tipWindow->closeWindow(NULL);
547 delete m_tipWindow;
548 m_tipWindow = NULL;
549 }
550}
551
552void ToolBar::selectTool(Tool* tool)
553{
554 ASSERT(tool);
555
556 m_selectedInGroup[tool->getGroup()] = tool;
557
558 // Inform to the active tool manager about this tool change.
559 App::instance()->activeToolManager()->setSelectedTool(tool);
560
561 if (m_currentStrip)
562 m_currentStrip->invalidate();
563
564 invalidate();
565}
566
567void ToolBar::selectToolGroup(tools::ToolGroup* toolGroup)
568{
569 ASSERT(toolGroup);
570 ASSERT(m_selectedInGroup[toolGroup]);
571 if (m_selectedInGroup[toolGroup])
572 selectTool(m_selectedInGroup[toolGroup]);
573}
574
575void ToolBar::onClosePopup()
576{
577 closeTipWindow();
578
579 if (!hasMouse())
580 m_tipOpened = false;
581
582 m_openOnHot = false;
583 m_hotTool = NULL;
584 m_hotIndex = NoneIndex;
585 m_currentStrip = NULL;
586
587 invalidate();
588}
589
590//////////////////////////////////////////////////////////////////////
591// ToolStrip
592//////////////////////////////////////////////////////////////////////
593
594ToolBar::ToolStrip::ToolStrip(ToolGroup* group, ToolBar* toolbar)
595 : Widget(kGenericWidget)
596{
597 m_group = group;
598 m_hotTool = NULL;
599 m_toolbar = toolbar;
600
601 setDoubleBuffered(true);
602 setTransparent(true);
603}
604
605ToolBar::ToolStrip::~ToolStrip()
606{
607}
608
609bool ToolBar::ToolStrip::onProcessMessage(Message* msg)
610{
611 switch (msg->type()) {
612
613 case kMouseDownMessage:
614 captureMouse();
615 [[fallthrough]];
616
617 case kMouseMoveMessage: {
618 auto mouseMsg = static_cast<const MouseMessage*>(msg);
619 const Point mousePos = mouseMsg->positionForDisplay(display());
620 ToolBox* toolbox = App::instance()->toolBox();
621 Tool* hot_tool = NULL;
622 Rect toolrc;
623 int index = 0;
624
625 for (Tool* tool : *toolbox) {
626 if (tool->getGroup() == m_group) {
627 toolrc = getToolBounds(index++);
628 if (toolrc.contains(Point(mousePos.x, mousePos.y))) {
629 hot_tool = tool;
630 break;
631 }
632 }
633 }
634
635 // Hot button changed
636 if (m_hotTool != hot_tool) {
637 m_hotTool = hot_tool;
638 invalidate();
639
640 // Show the tooltip for the hot tool
641 if (m_hotTool && !hasCapture())
642 m_toolbar->openTipWindow(m_group, m_hotTool);
643 else
644 m_toolbar->closeTipWindow();
645
646 if (m_hotTool)
647 StatusBar::instance()->showTool(0, m_hotTool);
648 }
649
650 if (hasCapture()) {
651 if (m_hotTool)
652 m_toolbar->selectTool(m_hotTool);
653
654 Widget* pick = manager()->pickFromScreenPos(mouseMsg->screenPosition());
655 if (ToolBar* bar = dynamic_cast<ToolBar*>(pick)) {
656 releaseMouse();
657
658 MouseMessage* mouseMsg2 = new MouseMessage(
659 kMouseDownMessage,
660 *mouseMsg,
661 mouseMsg->positionForDisplay(pick->display()));
662 mouseMsg2->setRecipient(bar);
663 mouseMsg2->setDisplay(pick->display());
664 manager()->enqueueMessage(mouseMsg2);
665 }
666 }
667 break;
668 }
669
670 case kMouseUpMessage:
671 if (hasCapture()) {
672 releaseMouse();
673 closeWindow();
674 }
675 break;
676
677 }
678 return Widget::onProcessMessage(msg);
679}
680
681void ToolBar::ToolStrip::onSizeHint(SizeHintEvent& ev)
682{
683 ToolBox* toolbox = App::instance()->toolBox();
684 int c = 0;
685
686 for (ToolIterator it = toolbox->begin(); it != toolbox->end(); ++it) {
687 Tool* tool = *it;
688 if (tool->getGroup() == m_group) {
689 ++c;
690 }
691 }
692
693 Size iconsize = getToolIconSize(this);
694 ev.setSizeHint(Size(iconsize.w * c, iconsize.h));
695}
696
697void ToolBar::ToolStrip::onPaint(PaintEvent& ev)
698{
699 Graphics* g = ev.graphics();
700 auto theme = SkinTheme::get(this);
701 ToolBox* toolbox = App::instance()->toolBox();
702 Tool* activeTool = App::instance()->activeTool();
703 Rect toolrc;
704 int index = 0;
705
706 for (Tool* tool : *toolbox) {
707 if (tool->getGroup() == m_group) {
708 SkinPartPtr nw;
709
710 if (activeTool == tool || m_hotTool == tool) {
711 nw = theme->parts.toolbuttonHot();
712 }
713 else {
714 nw = theme->parts.toolbuttonLast();
715 }
716
717 toolrc = getToolBounds(index++);
718 toolrc.offset(-bounds().x, -bounds().y);
719 theme->drawRect(g, toolrc, nw.get());
720
721 // Draw the tool icon
722 os::Surface* icon = theme->getToolIcon(tool->getId().c_str());
723 if (icon) {
724 g->drawRgbaSurface(
725 icon,
726 toolrc.x+toolrc.w/2-icon->width()/2,
727 toolrc.y+toolrc.h/2-icon->height()/2);
728 }
729 }
730 }
731}
732
733Rect ToolBar::ToolStrip::getToolBounds(int index)
734{
735 const Rect& bounds(this->bounds());
736 Size iconsize = getToolIconSize(this);
737
738 return Rect(bounds.x+index*(iconsize.w-1), bounds.y,
739 iconsize.w, bounds.h);
740}
741
742void ToolBar::onActiveToolChange(tools::Tool* tool)
743{
744 invalidate();
745}
746
747void ToolBar::onSelectedToolChange(tools::Tool* tool)
748{
749 if (tool && m_selectedInGroup[tool->getGroup()] != tool)
750 m_selectedInGroup[tool->getGroup()] = tool;
751
752 invalidate();
753}
754
755} // namespace app
756