xref: /haiku/src/kits/interface/ToolTipManager.cpp (revision 204dee708a999d5a71d0cb9497650ee7cef85d0a)
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 	while (true) {
243 		switch (alignment.vertical) {
244 			case B_ALIGN_TOP:
245 				location.y = where.y - size.height - offset.y;
246 				if (location.y < screenFrame.top) {
247 					alignment.vertical = B_ALIGN_BOTTOM;
248 					continue;
249 				}
250 				break;
251 
252 			case B_ALIGN_MIDDLE:
253 				location.y -= size.height / 2 - offset.y;
254 				if (location.y < screenFrame.top)
255 					location.y = screenFrame.top;
256 				else if (location.y + size.height > screenFrame.bottom)
257 					location.y = screenFrame.bottom - size.height;
258 				break;
259 
260 			default:
261 				location.y = where.y + offset.y;
262 				if (location.y + size.height > screenFrame.bottom) {
263 					alignment.vertical = B_ALIGN_TOP;
264 					continue;
265 				}
266 				break;
267 		}
268 		break;
269 	}
270 
271 	where = location;
272 
273 	// Cut off any out-of-screen areas
274 
275 	if (screenFrame.left > where.x) {
276 		size.width -= where.x - screenFrame.left;
277 		where.x = screenFrame.left;
278 	} else if (screenFrame.right < where.x + size.width)
279 		size.width = screenFrame.right - where.x;
280 
281 	if (screenFrame.top > where.y) {
282 		size.height -= where.y - screenFrame.top;
283 		where.y = screenFrame.top;
284 	} else if (screenFrame.bottom < where.y + size.height)
285 		size.height -= screenFrame.bottom - where.y;
286 
287 	// Change window frame
288 
289 	Window()->ResizeTo(size.width, size.height);
290 	Window()->MoveTo(where);
291 }
292 
293 
294 // #pragma mark -
295 
296 
297 ToolTipWindow::ToolTipWindow(BToolTip* tip, BPoint where, void* owner)
298 	:
299 	BWindow(BRect(0, 0, 250, 10).OffsetBySelf(where), "tool tip",
300 		B_BORDERED_WINDOW_LOOK, kMenuWindowFeel,
301 		B_NOT_ZOOMABLE | B_NOT_MINIMIZABLE | B_AUTO_UPDATE_SIZE_LIMITS
302 			| B_AVOID_FRONT | B_AVOID_FOCUS),
303 	fOwner(owner)
304 {
305 	SetLayout(new BGroupLayout(B_VERTICAL));
306 
307 	BToolTipManager* manager = BToolTipManager::Manager();
308 	ToolTipView* view = new ToolTipView(tip);
309 
310 	manager->Lock();
311 	AddChild(view);
312 	manager->Unlock();
313 
314 	// figure out size and location
315 
316 	view->ResetWindowFrame(where);
317 }
318 
319 
320 void
321 ToolTipWindow::MessageReceived(BMessage* message)
322 {
323 	ToolTipView* view = static_cast<ToolTipView*>(ChildAt(0));
324 
325 	switch (message->what) {
326 		case kMsgHideToolTip:
327 			view->HideTip();
328 			break;
329 
330 		case kMsgCurrentToolTip:
331 		{
332 			BToolTip* tip = view->Tip();
333 
334 			BMessage reply(B_REPLY);
335 			reply.AddPointer("current", tip);
336 			reply.AddPointer("owner", fOwner);
337 
338 			if (message->SendReply(&reply) == B_OK)
339 				tip->AcquireReference();
340 			break;
341 		}
342 
343 		case kMsgShowToolTip:
344 			view->ShowTip();
345 			break;
346 
347 		case kMsgCloseToolTip:
348 			if (view->IsTipHidden())
349 				Quit();
350 			break;
351 
352 		default:
353 			BWindow::MessageReceived(message);
354 	}
355 }
356 
357 
358 }	// namespace BPrivate
359 
360 
361 // #pragma mark -
362 
363 
364 /*static*/ BToolTipManager*
365 BToolTipManager::Manager()
366 {
367 	// Note: The check is not necessary; it's just faster than always calling
368 	// pthread_once(). It requires reading/writing of pointers to be atomic
369 	// on the architecture.
370 	if (sDefaultInstance == NULL)
371 		pthread_once(&sManagerInitOnce, &_InitSingleton);
372 
373 	return sDefaultInstance;
374 }
375 
376 
377 void
378 BToolTipManager::ShowTip(BToolTip* tip, BPoint where, void* owner)
379 {
380 	BToolTip* current = NULL;
381 	void* currentOwner = NULL;
382 	BMessage reply;
383 	if (fWindow.SendMessage(kMsgCurrentToolTip, &reply) == B_OK) {
384 		reply.FindPointer("current", (void**)&current);
385 		reply.FindPointer("owner", &currentOwner);
386 	}
387 
388 	// Release reference from the message
389 	if (current != NULL)
390 		current->ReleaseReference();
391 
392 	if (current == tip || currentOwner == owner) {
393 		fWindow.SendMessage(kMsgShowToolTip);
394 		return;
395 	}
396 
397 	fWindow.SendMessage(kMsgHideToolTip);
398 
399 	if (tip != NULL) {
400 		BWindow* window = new BPrivate::ToolTipWindow(tip, where, owner);
401 		window->Show();
402 
403 		fWindow = BMessenger(window);
404 	}
405 }
406 
407 
408 void
409 BToolTipManager::HideTip()
410 {
411 	fWindow.SendMessage(kMsgHideToolTip);
412 }
413 
414 
415 void
416 BToolTipManager::SetShowDelay(bigtime_t time)
417 {
418 	// between 10ms and 3s
419 	if (time < 10000)
420 		time = 10000;
421 	else if (time > 3000000)
422 		time = 3000000;
423 
424 	fShowDelay = time;
425 }
426 
427 
428 bigtime_t
429 BToolTipManager::ShowDelay() const
430 {
431 	return fShowDelay;
432 }
433 
434 
435 void
436 BToolTipManager::SetHideDelay(bigtime_t time)
437 {
438 	// between 0 and 0.5s
439 	if (time < 0)
440 		time = 0;
441 	else if (time > 500000)
442 		time = 500000;
443 
444 	fHideDelay = time;
445 }
446 
447 
448 bigtime_t
449 BToolTipManager::HideDelay() const
450 {
451 	return fHideDelay;
452 }
453 
454 
455 BToolTipManager::BToolTipManager()
456 	:
457 	fLock("tool tip manager"),
458 	fShowDelay(750000),
459 	fHideDelay(50000)
460 {
461 }
462 
463 
464 BToolTipManager::~BToolTipManager()
465 {
466 }
467 
468 
469 /*static*/ void
470 BToolTipManager::_InitSingleton()
471 {
472 	sDefaultInstance = new BToolTipManager();
473 }
474