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 | |
29 | static const int kTooltipDelayMsecs = 300; |
30 | |
31 | namespace ui { |
32 | |
33 | using namespace gfx; |
34 | |
35 | TooltipManager::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 | |
47 | TooltipManager::~TooltipManager() |
48 | { |
49 | Manager* manager = Manager::getDefault(); |
50 | manager->removeMessageFilterFor(this); |
51 | } |
52 | |
53 | void 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 | |
60 | void 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 | |
67 | bool 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 | |
108 | void TooltipManager::onInitTheme(InitThemeEvent& ev) |
109 | { |
110 | Widget::onInitTheme(ev); |
111 | if (m_tipWindow) |
112 | m_tipWindow->initTheme(); |
113 | } |
114 | |
115 | void 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 | |
144 | TipWindow::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 | |
164 | void TipWindow::setCloseOnKeyDown(bool state) |
165 | { |
166 | m_closeOnKeyDown = state; |
167 | } |
168 | |
169 | bool 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 | |
264 | void 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 | |
278 | bool 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 | |
293 | void TipWindow::onPaint(PaintEvent& ev) |
294 | { |
295 | theme()->paintTooltip( |
296 | ev.graphics(), this, style(), arrowStyle(), |
297 | clientBounds(), arrowAlign(), |
298 | target()); |
299 | } |
300 | |
301 | void 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 | |