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