1//--------------------------------------------------------------------
2//
3//	TToolTip.cpp
4//
5//	Written by: Robert Polic
6//
7//--------------------------------------------------------------------
8
9#include <Screen.h>
10#include <stdio.h>
11#include <stdlib.h>
12#include <string.h>
13
14#include <Application.h>
15#include <Roster.h>
16
17#include "TToolTip.h"
18
19#define kHOR_MARGIN					  4		// hor. gap between frame and tip
20#define kVER_MARGIN					  3		// ver. gap between frame and tip
21#define kTIP_HOR_OFFSET				 10		// tip position right of cursor
22#define kTIP_VER_OFFSET				 16		// tip position below cursor
23#define kSLOP						  4		// mouse slop before tip hides
24
25#define kTOOL_TIP_DELAY_TIME	 500000		// default delay time before tip shows (.5 secs.)
26#define kTOOL_TIP_HOLD_TIME		3000000		// default hold time of time (3 secs.)
27
28#define kDRAW_WINDOW_FRAME
29
30const rgb_color kVIEW_COLOR	= {255, 203, 0, 255};	// view background color (light yellow)
31const rgb_color kLIGHT_VIEW_COLOR = {255, 255, 80, 255}; // top left frame highlight
32const rgb_color kDARK_VIEW_COLOR = {175, 123, 0, 255}; // bottom right frame highlight
33const rgb_color kTEXT_COLOR = {0, 0, 0, 255};		// text color (black)
34
35
36//====================================================================
37
38TToolTip::TToolTip(tool_tip_settings *settings)
39	   :BWindow(BRect(0, 0, 10, 10), "tool_tip", B_NO_BORDER_WINDOW_LOOK,
40	   			B_FLOATING_ALL_WINDOW_FEEL, B_AVOID_FRONT)
41{
42	// setup the tooltip view
43	AddChild(fView = new TToolTipView(settings));
44	// start the message loop thread
45	Run();
46}
47
48//--------------------------------------------------------------------
49
50void TToolTip::MessageReceived(BMessage *msg)
51{
52	switch (msg->what) {
53		// forward interesting messages to the view
54		case B_SOME_APP_ACTIVATED:
55		case eToolTipStart:
56		case eToolTipStop:
57			PostMessage(msg, fView);
58			break;
59		default:
60			BWindow::MessageReceived(msg);
61	}
62}
63
64//--------------------------------------------------------------------
65
66void TToolTip::GetSettings(tool_tip_settings *settings)
67{
68	fView->GetSettings(settings);
69}
70
71//--------------------------------------------------------------------
72
73void TToolTip::SetSettings(tool_tip_settings *settings)
74{
75	fView->SetSettings(settings);
76}
77
78
79//====================================================================
80
81TToolTipView::TToolTipView(tool_tip_settings *settings)
82			 :BView(BRect(0, 0, 10, 10), "tool_tip", B_FOLLOW_ALL, B_WILL_DRAW)
83{
84	// initialize tooltip settings
85	if (settings)
86		// we should probably sanity-check user defined settings (but we won't)
87		fTip.settings = *settings;
88	else {
89		// use defaults if no settings are passed
90		fTip.settings.enabled = true;
91		fTip.settings.one_time_only = false;
92		fTip.settings.delay = kTOOL_TIP_DELAY_TIME;
93		fTip.settings.hold = kTOOL_TIP_HOLD_TIME;
94		fTip.settings.font = be_plain_font;
95	}
96
97	// initialize the tip
98	fString = (char *)malloc(1);
99	fString[0] = 0;
100
101	// initialize the view
102	SetFont(&fTip.settings.font);
103	SetViewColor(kVIEW_COLOR);
104}
105
106//--------------------------------------------------------------------
107
108TToolTipView::~TToolTipView()
109{
110	status_t	status;
111
112	// kill tool_tip thread
113	fTip.quit = true;
114	wait_for_thread(fThread, &status);
115
116	// free tip
117	free(fString);
118}
119
120//--------------------------------------------------------------------
121
122void TToolTipView::AllAttached()
123{
124	// initialize internal settings
125	fTip.app_active = true;
126	fTip.quit = false;
127	fTip.stopped = true;
128
129	fTip.tool_tip_view = this;
130	fTip.tool_tip_window = Window();
131
132	// start tool_tip thread
133	resume_thread(fThread = spawn_thread((status_t (*)(void *)) ToolTipThread,
134				"tip_thread", B_DISPLAY_PRIORITY, &fTip));
135}
136
137//--------------------------------------------------------------------
138
139void TToolTipView::Draw(BRect /* where */)
140{
141	char		*src_strings[1];
142	char		*tmp_string;
143	char		*truncated_strings[1];
144	BFont		font;
145	BRect		r = Bounds();
146	font_height	finfo;
147
148	// draw border around window
149#ifdef kDRAW_WINDOW_FRAME
150	SetHighColor(0, 0, 0, 255);
151	StrokeRect(r);
152	r.InsetBy(1, 1);
153#endif
154	SetHighColor(kLIGHT_VIEW_COLOR);
155	StrokeLine(BPoint(r.left, r.bottom), BPoint(r.left, r.top));
156	StrokeLine(BPoint(r.left + 1, r.top), BPoint(r.right - 1, r.top));
157	SetHighColor(kDARK_VIEW_COLOR);
158	StrokeLine(BPoint(r.right, r.top), BPoint(r.right, r.bottom));
159	StrokeLine(BPoint(r.right - 1, r.bottom), BPoint(r.left + 1, r.bottom));
160
161	// set pen position
162	GetFont(&font);
163	font.GetHeight(&finfo);
164	MovePenTo(kHOR_MARGIN + 1, kVER_MARGIN + finfo.ascent);
165
166	// truncate string if needed
167	src_strings[0] = fString;
168	tmp_string = (char *)malloc(strlen(fString) + 16);
169	truncated_strings[0] = tmp_string;
170	font.GetTruncatedStrings((const char **)src_strings, 1, B_TRUNCATE_END,
171		Bounds().Width() - (2 * kHOR_MARGIN) + 1, truncated_strings);
172
173	// draw string
174	SetLowColor(kVIEW_COLOR);
175	SetHighColor(kTEXT_COLOR);
176	DrawString(tmp_string);
177	free(tmp_string);
178}
179
180//--------------------------------------------------------------------
181
182void TToolTipView::MessageReceived(BMessage *msg)
183{
184	switch (msg->what) {
185		case B_SOME_APP_ACTIVATED:
186			msg->FindBool("active", &fTip.app_active);
187			break;
188
189		case eToolTipStart:
190			{
191				const char	*str;
192
193				// extract parameters
194				msg->FindPoint("start", &fTip.start);
195				msg->FindRect("bounds", &fTip.bounds);
196				msg->FindString("string", &str);
197				free(fString);
198				fString = (char *)malloc(strlen(str) + 1);
199				strcpy(fString, str);
200
201				// force window to fit new parameters
202				AdjustWindow();
203
204				// flag thread to reset
205				fTip.reset = true;
206			}
207			break;
208
209		case eToolTipStop:
210			// flag thread to stop
211			fTip.stop = true;
212			break;
213	}
214}
215
216//--------------------------------------------------------------------
217
218void TToolTipView::GetSettings(tool_tip_settings *settings)
219{
220	// return current settings
221	*settings = fTip.settings;
222}
223
224//--------------------------------------------------------------------
225
226void TToolTipView::SetSettings(tool_tip_settings *settings)
227{
228	bool	invalidate = fTip.settings.font != settings->font;
229
230	// we should probably sanity-check user defined settings (but we won't)
231	fTip.settings = *settings;
232
233	// if the font changed, adjust window to fit
234	if (invalidate) {
235		Window()->Lock();
236		SetFont(&fTip.settings.font);
237		AdjustWindow();
238		Window()->Unlock();
239	}
240}
241
242//--------------------------------------------------------------------
243
244void TToolTipView::AdjustWindow()
245{
246	float		width;
247	float		height;
248	float		x;
249	float		y;
250	BScreen		s(B_MAIN_SCREEN_ID);
251	BRect		screen = s.Frame();
252	BWindow		*wind = Window();
253	font_height	finfo;
254
255	screen.InsetBy(2, 2);	// we want a 2-pixel clearance
256	fTip.settings.font.GetHeight(&finfo);
257	width = fTip.settings.font.StringWidth(fString) + (kHOR_MARGIN * 2);  // string width
258	height = (finfo.ascent + finfo.descent + finfo.leading) + (kVER_MARGIN * 2);  // string height
259
260	// calculate new position and size of window
261	x = fTip.start.x + kTIP_HOR_OFFSET;
262	if ((x + width) > screen.right)
263		x = screen.right - width;
264	y = fTip.start.y + kTIP_VER_OFFSET;
265	if ((y + height) > screen.bottom) {
266		y = screen.bottom - height;
267		if ((fTip.start.y >= (y - kSLOP)) && (fTip.start.y <= (y + height)))
268			y = fTip.start.y - kTIP_VER_OFFSET - height;
269	}
270	if (x < screen.left) {
271		width -= screen.left - x;
272		x = screen.left;
273	}
274	if (y < screen.top) {
275		height -= screen.top - y;
276		y = screen.top;
277	}
278
279	wind->MoveTo((int)x, (int)y);
280	wind->ResizeTo((int)width, (int)height);
281
282	// force an update
283	Invalidate(Bounds());
284}
285
286//--------------------------------------------------------------------
287
288status_t TToolTipView::ToolTipThread(tool_tip *tip)
289{
290	uint32	buttons;
291	BPoint	where;
292	BScreen	s(B_MAIN_SCREEN_ID);
293	BRect	screen = s.Frame();
294
295	screen.InsetBy(2, 2);
296	while (!tip->quit) {
297		if (tip->tool_tip_window->LockWithTimeout(0) == B_NO_ERROR) {
298			tip->tool_tip_view->GetMouse(&where, &buttons);
299			tip->tool_tip_view->ConvertToScreen(&where);
300
301			tip->stopped = tip->stop;
302			if (tip->reset) {
303				if (tip->showing)
304					tip->tool_tip_window->Hide();
305				tip->stop = false;
306				tip->stopped = false;
307				tip->reset = false;
308				tip->shown = false;
309				tip->showing = false;
310				tip->start_time = system_time() + tip->settings.delay;
311			}
312			else if (tip->showing) {
313				if ((tip->stop) ||
314					(!tip->settings.enabled) ||
315					(!tip->app_active) ||
316					(!tip->bounds.Contains(where)) ||
317					(tip->expire_time < system_time()) ||
318					(abs((int)tip->start.x - (int)where.x) > kSLOP) ||
319					(abs((int)tip->start.y - (int)where.y) > kSLOP) ||
320					(buttons)) {
321					tip->tool_tip_window->Hide();
322					tip->shown = tip->settings.one_time_only;
323					tip->showing = false;
324					tip->tip_timed_out = (tip->expire_time < system_time());
325					tip->start_time = system_time() + tip->settings.delay;
326				}
327			}
328			else if ((tip->settings.enabled) &&
329					 (!tip->stopped) &&
330					 (tip->app_active) &&
331					 (!tip->shown) &&
332					 (!tip->tip_timed_out) &&
333					 (!buttons) &&
334					 (tip->bounds.Contains(where)) &&
335					 (tip->start_time < system_time())) {
336				tip->start = where;
337				tip->tool_tip_view->AdjustWindow();
338				tip->tool_tip_window->Show();
339				tip->tool_tip_window->Activate(false);
340				tip->showing = true;
341				tip->expire_time = system_time() + tip->settings.hold;
342				tip->start = where;
343			}
344			else if ((abs((int)tip->start.x - (int)where.x) > kSLOP) ||
345					 (abs((int)tip->start.y - (int)where.y) > kSLOP)) {
346				tip->start = where;
347				tip->start_time = system_time() + tip->settings.delay;
348				tip->tip_timed_out = false;
349			}
350			if (buttons)
351				tip->start_time = system_time() + tip->settings.delay;
352			tip->tool_tip_window->Unlock();
353		}
354		snooze(50000);
355	}
356	return B_NO_ERROR;
357}