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
22static pthread_once_t sManagerInitOnce = PTHREAD_ONCE_INIT;
23BToolTipManager* BToolTipManager::sDefaultInstance;
24
25static const uint32 kMsgHideToolTip = 'hide';
26static const uint32 kMsgShowToolTip = 'show';
27static const uint32 kMsgCurrentToolTip = 'curr';
28static const uint32 kMsgCloseToolTip = 'clos';
29
30
31namespace BPrivate {
32
33
34class ToolTipView : public BView {
35public:
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
56private:
57			BToolTip*			fToolTip;
58			bool				fHidden;
59};
60
61
62ToolTipView::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
80ToolTipView::~ToolTipView()
81{
82	fToolTip->ReleaseReference();
83}
84
85
86void
87ToolTipView::AttachedToWindow()
88{
89	SetEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, 0);
90	fToolTip->AttachedToWindow();
91}
92
93
94void
95ToolTipView::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
108void
109ToolTipView::FrameResized(float width, float height)
110{
111	ResetWindowFrame();
112}
113
114
115void
116ToolTipView::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
131void
132ToolTipView::KeyDown(const char* bytes, int32 numBytes)
133{
134	if (!fToolTip->IsSticky())
135		HideTip();
136}
137
138
139void
140ToolTipView::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
152void
153ToolTipView::ShowTip()
154{
155	fHidden = false;
156}
157
158
159void
160ToolTipView::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*/
174void
175ToolTipView::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
307ToolTipWindow::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
330void
331ToolTipWindow::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*
375BToolTipManager::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
387void
388BToolTipManager::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
418void
419BToolTipManager::HideTip()
420{
421	fWindow.SendMessage(kMsgHideToolTip);
422}
423
424
425void
426BToolTipManager::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
438bigtime_t
439BToolTipManager::ShowDelay() const
440{
441	return fShowDelay;
442}
443
444
445void
446BToolTipManager::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
458bigtime_t
459BToolTipManager::HideDelay() const
460{
461	return fHideDelay;
462}
463
464
465BToolTipManager::BToolTipManager()
466	:
467	fLock("tool tip manager"),
468	fShowDelay(750000),
469	fHideDelay(50000)
470{
471}
472
473
474BToolTipManager::~BToolTipManager()
475{
476}
477
478
479/*static*/ void
480BToolTipManager::_InitSingleton()
481{
482	sDefaultInstance = new BToolTipManager();
483}
484