1// Aseprite UI Library
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This file is released under the terms of the MIT license.
6// Read LICENSE.txt for more information.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "ui/tooltips.h"
13
14#include "gfx/size.h"
15#include "ui/fit_bounds.h"
16#include "ui/graphics.h"
17#include "ui/intern.h"
18#include "ui/manager.h"
19#include "ui/message.h"
20#include "ui/paint_event.h"
21#include "ui/scale.h"
22#include "ui/size_hint_event.h"
23#include "ui/system.h"
24#include "ui/textbox.h"
25#include "ui/theme.h"
26
27#include <string>
28
29static const int kTooltipDelayMsecs = 300;
30
31namespace ui {
32
33using namespace gfx;
34
35TooltipManager::TooltipManager()
36 : Widget(kGenericWidget)
37{
38 Manager* manager = Manager::getDefault();
39 manager->addMessageFilter(kMouseEnterMessage, this);
40 manager->addMessageFilter(kKeyDownMessage, this);
41 manager->addMessageFilter(kMouseDownMessage, this);
42 manager->addMessageFilter(kMouseLeaveMessage, this);
43
44 setVisible(false);
45}
46
47TooltipManager::~TooltipManager()
48{
49 Manager* manager = Manager::getDefault();
50 manager->removeMessageFilterFor(this);
51}
52
53void TooltipManager::addTooltipFor(Widget* widget, const std::string& text, int arrowAlign)
54{
55 ASSERT(!widget->hasFlags(IGNORE_MOUSE));
56
57 m_tips[widget] = TipInfo(text, arrowAlign);
58}
59
60void TooltipManager::removeTooltipFor(Widget* widget)
61{
62 auto it = m_tips.find(widget);
63 if (it != m_tips.end())
64 m_tips.erase(it);
65}
66
67bool TooltipManager::onProcessMessage(Message* msg)
68{
69 switch (msg->type()) {
70
71 case kMouseEnterMessage: {
72 // Tooltips are only for widgets that can directly get the mouse
73 // (get the kMouseEnterMessage directly).
74 if (Widget* widget = msg->recipient()) {
75 Tips::iterator it = m_tips.find(widget);
76 if (it != m_tips.end()) {
77 m_target.widget = it->first;
78 m_target.tipInfo = it->second;
79
80 if (m_timer == nullptr) {
81 m_timer.reset(new Timer(kTooltipDelayMsecs, this));
82 m_timer->Tick.connect(&TooltipManager::onTick, this);
83 }
84
85 m_timer->start();
86 }
87 }
88 break;
89 }
90
91 case kKeyDownMessage:
92 case kMouseDownMessage:
93 case kMouseLeaveMessage:
94 if (m_tipWindow) {
95 m_tipWindow->closeWindow(nullptr);
96 m_tipWindow.reset();
97 }
98
99 if (m_timer)
100 m_timer->stop();
101
102 return false;
103 }
104
105 return Widget::onProcessMessage(msg);
106}
107
108void TooltipManager::onInitTheme(InitThemeEvent& ev)
109{
110 Widget::onInitTheme(ev);
111 if (m_tipWindow)
112 m_tipWindow->initTheme();
113}
114
115void TooltipManager::onTick()
116{
117 if (!m_tipWindow) {
118 m_tipWindow.reset(new TipWindow(m_target.tipInfo.text));
119
120 int arrowAlign = m_target.tipInfo.arrowAlign;
121 gfx::Rect target = m_target.widget->bounds();
122 if (!arrowAlign)
123 target.setOrigin(m_target.widget->mousePosInDisplay()+12*guiscale());
124
125 ui::Display* targetDisplay = m_target.widget->display();
126
127 if (m_tipWindow->pointAt(arrowAlign,
128 target,
129 targetDisplay)) {
130 m_tipWindow->openWindow();
131 m_tipWindow->adjustTargetFrom(targetDisplay);
132 }
133 else {
134 // No enough room for the tooltip
135 m_tipWindow.reset();
136 m_timer->stop();
137 }
138 }
139 m_timer->stop();
140}
141
142// TipWindow
143
144TipWindow::TipWindow(const std::string& text)
145 // Put an empty string in the ctor so the window label isn't build
146 : PopupWindow("", ClickBehavior::CloseOnClickInOtherWindow)
147 , m_arrowStyle(nullptr)
148 , m_arrowAlign(0)
149 , m_closeOnKeyDown(true)
150 , m_textBox(new TextBox("", LEFT | TOP))
151{
152 setTransparent(true);
153
154 // Here we build our own custimized label for the window
155 // (a text box).
156 m_textBox->setVisible(false);
157 addChild(m_textBox);
158 setText(text);
159
160 makeFixed();
161 initTheme();
162}
163
164void TipWindow::setCloseOnKeyDown(bool state)
165{
166 m_closeOnKeyDown = state;
167}
168
169bool TipWindow::pointAt(int arrowAlign,
170 const gfx::Rect& target,
171 const ui::Display* display)
172{
173 // TODO merge this code with the new ui::fit_bounds() algorithm
174
175 m_target = target;
176 m_arrowAlign = arrowAlign;
177
178 remapWindow();
179
180 int x = target.x;
181 int y = target.y;
182 int w = bounds().w;
183 int h = bounds().h;
184
185 os::Window* nativeParentWindow = display->nativeWindow();
186
187 int trycount = 0;
188 for (; trycount < 4; ++trycount) {
189 switch (arrowAlign) {
190 case TOP | LEFT:
191 x = m_target.x + m_target.w;
192 y = m_target.y + m_target.h;
193 break;
194 case TOP | RIGHT:
195 x = m_target.x - w;
196 y = m_target.y + m_target.h;
197 break;
198 case BOTTOM | LEFT:
199 x = m_target.x + m_target.w;
200 y = m_target.y - h;
201 break;
202 case BOTTOM | RIGHT:
203 x = m_target.x - w;
204 y = m_target.y - h;
205 break;
206 case TOP:
207 x = m_target.x + m_target.w/2 - w/2;
208 y = m_target.y + m_target.h;
209 break;
210 case BOTTOM:
211 x = m_target.x + m_target.w/2 - w/2;
212 y = m_target.y - h;
213 break;
214 case LEFT:
215 x = m_target.x + m_target.w;
216 y = m_target.y + m_target.h/2 - h/2;
217 break;
218 case RIGHT:
219 x = m_target.x - w;
220 y = m_target.y + m_target.h/2 - h/2;
221 break;
222 }
223
224 if (get_multiple_displays()) {
225 const gfx::Rect waBounds = nativeParentWindow->screen()->workarea();
226 gfx::Point pt = nativeParentWindow->pointToScreen(gfx::Point(x, y));
227 pt.x = std::clamp(pt.x, waBounds.x, waBounds.x2()-w);
228 pt.y = std::clamp(pt.y, waBounds.y, waBounds.y2()-h);
229 pt = nativeParentWindow->pointFromScreen(pt);
230 x = pt.x;
231 y = pt.y;
232 }
233 else {
234 const gfx::Rect displayBounds = display->bounds();
235 x = std::clamp(x, displayBounds.x, displayBounds.x2()-w);
236 y = std::clamp(y, displayBounds.y, displayBounds.y2()-h);
237 }
238
239 if (m_target.intersects(gfx::Rect(x, y, w, h))) {
240 switch (trycount) {
241 case 0:
242 case 2:
243 // Switch position
244 if (arrowAlign & (TOP | BOTTOM)) arrowAlign ^= TOP | BOTTOM;
245 if (arrowAlign & (LEFT | RIGHT)) arrowAlign ^= LEFT | RIGHT;
246 break;
247 case 1:
248 // Rotate positions
249 if (arrowAlign & (TOP | LEFT)) arrowAlign ^= TOP | LEFT;
250 if (arrowAlign & (BOTTOM | RIGHT)) arrowAlign ^= BOTTOM | RIGHT;
251 break;
252 }
253 }
254 else {
255 m_arrowAlign = arrowAlign;
256 ui::fit_bounds(display, this, gfx::Rect(x, y, w, h));
257 break;
258 }
259 }
260
261 return (trycount < 4);
262}
263
264void TipWindow::adjustTargetFrom(const ui::Display* targetDisplay)
265{
266 // Convert the target relative to this window coordinates
267 if (get_multiple_displays()) {
268 gfx::Point pt = m_target.origin();
269 pt = targetDisplay->nativeWindow()->pointToScreen(pt);
270 pt = display()->nativeWindow()->pointFromScreen(pt);
271 m_target.setOrigin(pt);
272 }
273 else {
274 m_target.offset(-bounds().origin());
275 }
276}
277
278bool TipWindow::onProcessMessage(Message* msg)
279{
280 switch (msg->type()) {
281
282 case kKeyDownMessage:
283 if (m_closeOnKeyDown &&
284 static_cast<KeyMessage*>(msg)->scancode() < kKeyFirstModifierScancode)
285 closeWindow(nullptr);
286 break;
287
288 }
289
290 return PopupWindow::onProcessMessage(msg);
291}
292
293void TipWindow::onPaint(PaintEvent& ev)
294{
295 theme()->paintTooltip(
296 ev.graphics(), this, style(), arrowStyle(),
297 clientBounds(), arrowAlign(),
298 target());
299}
300
301void TipWindow::onBuildTitleLabel()
302{
303 if (!text().empty()) {
304 m_textBox->setVisible(true);
305 m_textBox->setText(text());
306 }
307 else
308 m_textBox->setVisible(false);
309}
310
311} // namespace ui
312