xref: /haiku/src/kits/interface/ToolTipManager.cpp (revision 1e60bdeab63fa7a57bc9a55b032052e95a18bd2c)
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 	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 
80 ToolTipView::~ToolTipView()
81 {
82 	fToolTip->ReleaseReference();
83 }
84 
85 
86 void
87 ToolTipView::AttachedToWindow()
88 {
89 	SetEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, 0);
90 	fToolTip->AttachedToWindow();
91 }
92 
93 
94 void
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
109 ToolTipView::FrameResized(float width, float height)
110 {
111 	ResetWindowFrame();
112 }
113 
114 
115 void
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
132 ToolTipView::KeyDown(const char* bytes, int32 numBytes)
133 {
134 	if (!fToolTip->IsSticky())
135 		HideTip();
136 }
137 
138 
139 void
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
153 ToolTipView::ShowTip()
154 {
155 	fHidden = false;
156 }
157 
158 
159 void
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
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 
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
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*
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
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**)&current);
395 		reply.FindPointer("owner", &currentOwner);
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
419 BToolTipManager::HideTip()
420 {
421 	fWindow.SendMessage(kMsgHideToolTip);
422 }
423 
424 
425 void
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
439 BToolTipManager::ShowDelay() const
440 {
441 	return fShowDelay;
442 }
443 
444 
445 void
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
459 BToolTipManager::HideDelay() const
460 {
461 	return fHideDelay;
462 }
463 
464 
465 BToolTipManager::BToolTipManager()
466 	:
467 	fLock("tool tip manager"),
468 	fShowDelay(750000),
469 	fHideDelay(50000)
470 {
471 }
472 
473 
474 BToolTipManager::~BToolTipManager()
475 {
476 }
477 
478 
479 /*static*/ void
480 BToolTipManager::_InitSingleton()
481 {
482 	sDefaultInstance = new BToolTipManager();
483 }
484