1 /*
2 * Copyright 2009-2012, Axel Dörfler, axeld@pinc-software.de.
3 * Copyright 2009, Stephan Aßmus <superstippi@gmx.de>.
4 * All rights reserved. Distributed under the terms of the MIT License.
5 */
6
7
8 #include <ToolTipManager.h>
9 #include <ToolTipWindow.h>
10
11 #include <pthread.h>
12
13 #include <Autolock.h>
14 #include <LayoutBuilder.h>
15 #include <MessageRunner.h>
16 #include <Screen.h>
17
18 #include <WindowPrivate.h>
19 #include <ToolTip.h>
20
21
22 static pthread_once_t sManagerInitOnce = PTHREAD_ONCE_INIT;
23 BToolTipManager* BToolTipManager::sDefaultInstance;
24
25 static const uint32 kMsgHideToolTip = 'hide';
26 static const uint32 kMsgShowToolTip = 'show';
27 static const uint32 kMsgCurrentToolTip = 'curr';
28 static const uint32 kMsgCloseToolTip = 'clos';
29
30
31 namespace BPrivate {
32
33
34 class ToolTipView : public BView {
35 public:
36 ToolTipView(BToolTip* tip);
37 virtual ~ToolTipView();
38
39 virtual void AttachedToWindow();
40 virtual void DetachedFromWindow();
41
42 virtual void FrameResized(float width, float height);
43 virtual void MouseMoved(BPoint where, uint32 transit,
44 const BMessage* dragMessage);
45 virtual void KeyDown(const char* bytes, int32 numBytes);
46
47 void HideTip();
48 void ShowTip();
49
50 void ResetWindowFrame();
51 void ResetWindowFrame(BPoint where);
52
Tip() const53 BToolTip* Tip() const { return fToolTip; }
IsTipHidden() const54 bool IsTipHidden() const { return fHidden; }
55
56 private:
57 BToolTip* fToolTip;
58 bool fHidden;
59 };
60
61
ToolTipView(BToolTip * tip)62 ToolTipView::ToolTipView(BToolTip* tip)
63 :
64 BView("tool tip", B_WILL_DRAW | B_FRAME_EVENTS),
65 fToolTip(tip),
66 fHidden(false)
67 {
68 fToolTip->AcquireReference();
69 SetViewUIColor(B_TOOL_TIP_BACKGROUND_COLOR);
70 SetHighUIColor(B_TOOL_TIP_TEXT_COLOR);
71
72 BGroupLayout* layout = new BGroupLayout(B_VERTICAL);
73 layout->SetInsets(5, 5, 5, 5);
74 SetLayout(layout);
75
76 AddChild(fToolTip->View());
77 }
78
79
~ToolTipView()80 ToolTipView::~ToolTipView()
81 {
82 fToolTip->ReleaseReference();
83 }
84
85
86 void
AttachedToWindow()87 ToolTipView::AttachedToWindow()
88 {
89 SetEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, 0);
90 fToolTip->AttachedToWindow();
91 }
92
93
94 void
DetachedFromWindow()95 ToolTipView::DetachedFromWindow()
96 {
97 BToolTipManager* manager = BToolTipManager::Manager();
98 manager->Lock();
99
100 RemoveChild(fToolTip->View());
101 // don't delete this one!
102 fToolTip->DetachedFromWindow();
103
104 manager->Unlock();
105 }
106
107
108 void
FrameResized(float width,float height)109 ToolTipView::FrameResized(float width, float height)
110 {
111 ResetWindowFrame();
112 }
113
114
115 void
MouseMoved(BPoint where,uint32 transit,const BMessage * dragMessage)116 ToolTipView::MouseMoved(BPoint where, uint32 transit,
117 const BMessage* dragMessage)
118 {
119 if (fToolTip->IsSticky()) {
120 ResetWindowFrame(ConvertToScreen(where));
121 } else if (transit == B_ENTERED_VIEW) {
122 // close instantly if the user managed to enter
123 Window()->Quit();
124 } else {
125 // close with the preferred delay in case the mouse just moved
126 HideTip();
127 }
128 }
129
130
131 void
KeyDown(const char * bytes,int32 numBytes)132 ToolTipView::KeyDown(const char* bytes, int32 numBytes)
133 {
134 if (!fToolTip->IsSticky())
135 HideTip();
136 }
137
138
139 void
HideTip()140 ToolTipView::HideTip()
141 {
142 if (fHidden)
143 return;
144
145 BMessage quit(kMsgCloseToolTip);
146 BMessageRunner::StartSending(Window(), &quit,
147 BToolTipManager::Manager()->HideDelay(), 1);
148 fHidden = true;
149 }
150
151
152 void
ShowTip()153 ToolTipView::ShowTip()
154 {
155 fHidden = false;
156 }
157
158
159 void
ResetWindowFrame()160 ToolTipView::ResetWindowFrame()
161 {
162 BPoint where;
163 GetMouse(&where, NULL, false);
164
165 ResetWindowFrame(ConvertToScreen(where));
166 }
167
168
169 /*! Tries to find the right frame to show the tool tip in, trying to use the
170 alignment that the tool tip specifies.
171 Makes sure the tool tip can be shown on screen in its entirety, ie. it will
172 resize the window if necessary.
173 */
174 void
ResetWindowFrame(BPoint where)175 ToolTipView::ResetWindowFrame(BPoint where)
176 {
177 if (Window() == NULL)
178 return;
179
180 BSize size = PreferredSize();
181
182 BScreen screen(Window());
183 BRect screenFrame = screen.Frame().InsetBySelf(2, 2);
184 BPoint offset = fToolTip->MouseRelativeLocation();
185
186 // Ensure that the tip can be placed on screen completely
187
188 if (size.width > screenFrame.Width())
189 size.width = screenFrame.Width();
190
191 if (size.width > where.x - screenFrame.left
192 && size.width > screenFrame.right - where.x) {
193 // There is no space to put the tip to the left or the right of the
194 // cursor, it can either be below or above it
195 if (size.height > where.y - screenFrame.top
196 && where.y - screenFrame.top > screenFrame.Height() / 2) {
197 size.height = where.y - offset.y - screenFrame.top;
198 } else if (size.height > screenFrame.bottom - where.y
199 && screenFrame.bottom - where.y > screenFrame.Height() / 2) {
200 size.height = screenFrame.bottom - where.y - offset.y;
201 }
202 }
203
204 // Find best alignment, starting with the requested one
205
206 BAlignment alignment = fToolTip->Alignment();
207 BPoint location = where;
208 bool doesNotFit = false;
209
210 switch (alignment.horizontal) {
211 case B_ALIGN_LEFT:
212 location.x -= size.width + offset.x;
213 if (location.x < screenFrame.left) {
214 location.x = screenFrame.left;
215 doesNotFit = true;
216 }
217 break;
218 case B_ALIGN_CENTER:
219 location.x -= size.width / 2 - offset.x;
220 if (location.x < screenFrame.left) {
221 location.x = screenFrame.left;
222 doesNotFit = true;
223 } else if (location.x + size.width > screenFrame.right) {
224 location.x = screenFrame.right - size.width;
225 doesNotFit = true;
226 }
227 break;
228
229 default:
230 location.x += offset.x;
231 if (location.x + size.width > screenFrame.right) {
232 location.x = screenFrame.right - size.width;
233 doesNotFit = true;
234 }
235 break;
236 }
237
238 if ((doesNotFit && alignment.vertical == B_ALIGN_MIDDLE)
239 || (alignment.vertical == B_ALIGN_MIDDLE
240 && alignment.horizontal == B_ALIGN_CENTER))
241 alignment.vertical = B_ALIGN_BOTTOM;
242
243 // Adjust the tooltip position in cases where it would be partly out of the
244 // screen frame. Try to fit the tooltip on the requested side of the
245 // cursor, if that fails, try the opposite side, and if that fails again,
246 // give up and leave the tooltip under the mouse cursor.
247 bool firstTry = true;
248 while (true) {
249 switch (alignment.vertical) {
250 case B_ALIGN_TOP:
251 location.y = where.y - size.height - offset.y;
252 if (location.y < screenFrame.top) {
253 alignment.vertical = firstTry ? B_ALIGN_BOTTOM
254 : B_ALIGN_MIDDLE;
255 firstTry = false;
256 continue;
257 }
258 break;
259
260 case B_ALIGN_MIDDLE:
261 location.y -= size.height / 2 - offset.y;
262 if (location.y < screenFrame.top)
263 location.y = screenFrame.top;
264 else if (location.y + size.height > screenFrame.bottom)
265 location.y = screenFrame.bottom - size.height;
266 break;
267
268 default:
269 location.y = where.y + offset.y;
270 if (location.y + size.height > screenFrame.bottom) {
271 alignment.vertical = firstTry ? B_ALIGN_TOP
272 : B_ALIGN_MIDDLE;
273 firstTry = false;
274 continue;
275 }
276 break;
277 }
278 break;
279 }
280
281 where = location;
282
283 // Cut off any out-of-screen areas
284
285 if (screenFrame.left > where.x) {
286 size.width -= where.x - screenFrame.left;
287 where.x = screenFrame.left;
288 } else if (screenFrame.right < where.x + size.width)
289 size.width = screenFrame.right - where.x;
290
291 if (screenFrame.top > where.y) {
292 size.height -= where.y - screenFrame.top;
293 where.y = screenFrame.top;
294 } else if (screenFrame.bottom < where.y + size.height)
295 size.height -= screenFrame.bottom - where.y;
296
297 // Change window frame
298
299 Window()->ResizeTo(size.width, size.height);
300 Window()->MoveTo(where);
301 }
302
303
304 // #pragma mark -
305
306
ToolTipWindow(BToolTip * tip,BPoint where,void * owner)307 ToolTipWindow::ToolTipWindow(BToolTip* tip, BPoint where, void* owner)
308 :
309 BWindow(BRect(0, 0, 250, 10).OffsetBySelf(where), "tool tip",
310 B_BORDERED_WINDOW_LOOK, kMenuWindowFeel,
311 B_NOT_ZOOMABLE | B_NOT_MINIMIZABLE | B_AUTO_UPDATE_SIZE_LIMITS
312 | B_AVOID_FRONT | B_AVOID_FOCUS),
313 fOwner(owner)
314 {
315 SetLayout(new BGroupLayout(B_VERTICAL));
316
317 BToolTipManager* manager = BToolTipManager::Manager();
318 ToolTipView* view = new ToolTipView(tip);
319
320 manager->Lock();
321 AddChild(view);
322 manager->Unlock();
323
324 // figure out size and location
325
326 view->ResetWindowFrame(where);
327 }
328
329
330 void
MessageReceived(BMessage * message)331 ToolTipWindow::MessageReceived(BMessage* message)
332 {
333 ToolTipView* view = static_cast<ToolTipView*>(ChildAt(0));
334
335 switch (message->what) {
336 case kMsgHideToolTip:
337 view->HideTip();
338 break;
339
340 case kMsgCurrentToolTip:
341 {
342 BToolTip* tip = view->Tip();
343
344 BMessage reply(B_REPLY);
345 reply.AddPointer("current", tip);
346 reply.AddPointer("owner", fOwner);
347
348 if (message->SendReply(&reply) == B_OK)
349 tip->AcquireReference();
350 break;
351 }
352
353 case kMsgShowToolTip:
354 view->ShowTip();
355 break;
356
357 case kMsgCloseToolTip:
358 if (view->IsTipHidden())
359 Quit();
360 break;
361
362 default:
363 BWindow::MessageReceived(message);
364 }
365 }
366
367
368 } // namespace BPrivate
369
370
371 // #pragma mark -
372
373
374 /*static*/ BToolTipManager*
Manager()375 BToolTipManager::Manager()
376 {
377 // Note: The check is not necessary; it's just faster than always calling
378 // pthread_once(). It requires reading/writing of pointers to be atomic
379 // on the architecture.
380 if (sDefaultInstance == NULL)
381 pthread_once(&sManagerInitOnce, &_InitSingleton);
382
383 return sDefaultInstance;
384 }
385
386
387 void
ShowTip(BToolTip * tip,BPoint where,void * owner)388 BToolTipManager::ShowTip(BToolTip* tip, BPoint where, void* owner)
389 {
390 BToolTip* current = NULL;
391 void* currentOwner = NULL;
392 BMessage reply;
393 if (fWindow.SendMessage(kMsgCurrentToolTip, &reply) == B_OK) {
394 reply.FindPointer("current", (void**)¤t);
395 reply.FindPointer("owner", ¤tOwner);
396 }
397
398 // Release reference from the message
399 if (current != NULL)
400 current->ReleaseReference();
401
402 if (current == tip || currentOwner == owner) {
403 fWindow.SendMessage(kMsgShowToolTip);
404 return;
405 }
406
407 fWindow.SendMessage(kMsgHideToolTip);
408
409 if (tip != NULL) {
410 BWindow* window = new BPrivate::ToolTipWindow(tip, where, owner);
411 window->Show();
412
413 fWindow = BMessenger(window);
414 }
415 }
416
417
418 void
HideTip()419 BToolTipManager::HideTip()
420 {
421 fWindow.SendMessage(kMsgHideToolTip);
422 }
423
424
425 void
SetShowDelay(bigtime_t time)426 BToolTipManager::SetShowDelay(bigtime_t time)
427 {
428 // between 10ms and 3s
429 if (time < 10000)
430 time = 10000;
431 else if (time > 3000000)
432 time = 3000000;
433
434 fShowDelay = time;
435 }
436
437
438 bigtime_t
ShowDelay() const439 BToolTipManager::ShowDelay() const
440 {
441 return fShowDelay;
442 }
443
444
445 void
SetHideDelay(bigtime_t time)446 BToolTipManager::SetHideDelay(bigtime_t time)
447 {
448 // between 0 and 0.5s
449 if (time < 0)
450 time = 0;
451 else if (time > 500000)
452 time = 500000;
453
454 fHideDelay = time;
455 }
456
457
458 bigtime_t
HideDelay() const459 BToolTipManager::HideDelay() const
460 {
461 return fHideDelay;
462 }
463
464
BToolTipManager()465 BToolTipManager::BToolTipManager()
466 :
467 fLock("tool tip manager"),
468 fShowDelay(750000),
469 fHideDelay(50000)
470 {
471 }
472
473
~BToolTipManager()474 BToolTipManager::~BToolTipManager()
475 {
476 }
477
478
479 /*static*/ void
_InitSingleton()480 BToolTipManager::_InitSingleton()
481 {
482 sDefaultInstance = new BToolTipManager();
483 }
484