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