1/*
2 * Copyright 2004-2011, Haiku, Inc. All Rights Reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Mike Berg <mike@berg-net.us>
7 *		Julun <host.haiku@gmx.de>
8 *		Stephan A��mus <superstippi@gmx.de>
9 *		Clemens <mail@Clemens-Zeidler.de>
10 *		Hamish Morrison <hamish@lavabit.com>
11 */
12
13
14#include "AnalogClock.h"
15
16#include <math.h>
17#include <stdio.h>
18
19#include <LayoutUtils.h>
20#include <Message.h>
21#include <Window.h>
22
23#include "TimeMessages.h"
24
25
26#define DRAG_DELTA_PHI 0.2
27
28
29TAnalogClock::TAnalogClock(const char* name, bool drawSecondHand,
30	bool interactive)
31	:
32	BView(name, B_WILL_DRAW | B_DRAW_ON_CHILDREN | B_FRAME_EVENTS),
33	fHours(0),
34	fMinutes(0),
35	fSeconds(0),
36	fDirty(true),
37	fCenterX(0.0),
38	fCenterY(0.0),
39	fRadius(0.0),
40	fHourDragging(false),
41	fMinuteDragging(false),
42	fDrawSecondHand(drawSecondHand),
43	fInteractive(interactive),
44	fTimeChangeIsOngoing(false)
45{
46	SetFlags(Flags() | B_SUBPIXEL_PRECISE);
47}
48
49
50TAnalogClock::~TAnalogClock()
51{
52}
53
54
55void
56TAnalogClock::Draw(BRect /*updateRect*/)
57{
58	if (fDirty)
59		DrawClock();
60}
61
62
63void
64TAnalogClock::MessageReceived(BMessage* message)
65{
66	int32 change;
67	switch (message->what) {
68		case B_OBSERVER_NOTICE_CHANGE:
69			message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change);
70			switch (change) {
71				case H_TM_CHANGED:
72				{
73					int32 hour = 0;
74					int32 minute = 0;
75					int32 second = 0;
76					if (message->FindInt32("hour", &hour) == B_OK
77					 && message->FindInt32("minute", &minute) == B_OK
78					 && message->FindInt32("second", &second) == B_OK)
79						SetTime(hour, minute, second);
80					break;
81				}
82				default:
83					BView::MessageReceived(message);
84					break;
85			}
86		break;
87		default:
88			BView::MessageReceived(message);
89			break;
90	}
91}
92
93
94void
95TAnalogClock::MouseDown(BPoint point)
96{
97	if (!fInteractive) {
98		BView::MouseDown(point);
99		return;
100	}
101
102	if (InMinuteHand(point)) {
103		fMinuteDragging = true;
104		fDirty = true;
105		SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
106		Invalidate();
107		return;
108	}
109
110	if (InHourHand(point)) {
111		fHourDragging = true;
112		fDirty = true;
113		SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
114		Invalidate();
115		return;
116	}
117}
118
119
120void
121TAnalogClock::MouseUp(BPoint point)
122{
123	if (!fInteractive) {
124		BView::MouseUp(point);
125		return;
126	}
127
128	if (fHourDragging || fMinuteDragging) {
129		int32 hour, minute, second;
130		GetTime(&hour, &minute, &second);
131		BMessage message(H_USER_CHANGE);
132		message.AddBool("time", true);
133		message.AddInt32("hour", hour);
134		message.AddInt32("minute", minute);
135		Window()->PostMessage(&message);
136		fTimeChangeIsOngoing = true;
137	}
138	fHourDragging = false;
139	fDirty = true;
140	fMinuteDragging = false;
141	fDirty = true;
142}
143
144
145void
146TAnalogClock::MouseMoved(BPoint point, uint32 transit, const BMessage* message)
147{
148	if (!fInteractive) {
149		BView::MouseMoved(point, transit, message);
150		return;
151	}
152
153	if (fMinuteDragging)
154		SetMinuteHand(point);
155	if (fHourDragging)
156		SetHourHand(point);
157
158	Invalidate();
159}
160
161
162void
163TAnalogClock::DoLayout()
164{
165	BRect bounds = Bounds();
166
167	// + 0.5 is for the offset to pixel centers
168	// (important when drawing with B_SUBPIXEL_PRECISE)
169	fCenterX = floorf((bounds.left + bounds.right) / 2 + 0.5) + 0.5;
170	fCenterY = floorf((bounds.top + bounds.bottom) / 2 + 0.5) + 0.5;
171	fRadius = floorf((MIN(bounds.Width(), bounds.Height()) / 2.0)) - 5.5;
172}
173
174
175BSize
176TAnalogClock::MaxSize()
177{
178	return BLayoutUtils::ComposeSize(ExplicitMaxSize(),
179		BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
180}
181
182
183BSize
184TAnalogClock::MinSize()
185{
186	return BSize(64.f, 64.f);
187}
188
189
190BSize
191TAnalogClock::PreferredSize()
192{
193	return BLayoutUtils::ComposeSize(ExplicitPreferredSize(),
194		BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
195}
196
197
198void
199TAnalogClock::SetTime(int32 hour, int32 minute, int32 second)
200{
201	// don't set the time if the hands are in a drag action
202	if (fHourDragging || fMinuteDragging || fTimeChangeIsOngoing)
203		return;
204
205	if (fHours == hour && fMinutes == minute && fSeconds == second)
206		return;
207
208	fHours = hour;
209	fMinutes = minute;
210	fSeconds = second;
211
212	fDirty = true;
213
214	BWindow* window = Window();
215	if (window && window->Lock()) {
216		Invalidate();
217		Window()->Unlock();
218	}
219}
220
221
222bool
223TAnalogClock::IsChangingTime()
224{
225	return fTimeChangeIsOngoing;
226}
227
228
229void
230TAnalogClock::ChangeTimeFinished()
231{
232	fTimeChangeIsOngoing = false;
233}
234
235
236void
237TAnalogClock::GetTime(int32* hour, int32* minute, int32* second)
238{
239	*hour = fHours;
240	*minute = fMinutes;
241	*second = fSeconds;
242}
243
244
245void
246TAnalogClock::DrawClock()
247{
248	if (!LockLooper())
249		return;
250
251	BRect bounds = Bounds();
252	// clear background
253	rgb_color background = ViewColor();
254	SetHighColor(background);
255	FillRect(bounds);
256
257	bounds.Set(fCenterX - fRadius, fCenterY - fRadius,
258		fCenterX + fRadius, fCenterY + fRadius);
259
260	SetPenSize(2.0);
261
262	SetHighColor(tint_color(background, B_DARKEN_1_TINT));
263	StrokeEllipse(bounds.OffsetByCopy(-1, -1));
264
265	SetHighColor(tint_color(background, B_LIGHTEN_2_TINT));
266	StrokeEllipse(bounds.OffsetByCopy(1, 1));
267
268	SetHighColor(tint_color(background, B_DARKEN_3_TINT));
269	StrokeEllipse(bounds);
270
271	SetLowColor(255, 255, 255);
272	FillEllipse(bounds, B_SOLID_LOW);
273
274	SetHighColor(tint_color(HighColor(), B_DARKEN_2_TINT));
275
276	// minutes
277	SetPenSize(1.0);
278	SetLineMode(B_BUTT_CAP, B_MITER_JOIN);
279	for (int32 minute = 1; minute < 60; minute++) {
280		if (minute % 5 == 0)
281			continue;
282		float x1 = fCenterX + sinf(minute * M_PI / 30.0) * fRadius;
283		float y1 = fCenterY + cosf(minute * M_PI / 30.0) * fRadius;
284		float x2 = fCenterX + sinf(minute * M_PI / 30.0) * (fRadius * 0.95);
285		float y2 = fCenterY + cosf(minute * M_PI / 30.0) * (fRadius * 0.95);
286		StrokeLine(BPoint(x1, y1), BPoint(x2, y2));
287	}
288
289	SetHighColor(tint_color(HighColor(), B_DARKEN_1_TINT));
290
291	// hours
292	SetPenSize(2.0);
293	SetLineMode(B_ROUND_CAP, B_MITER_JOIN);
294	for (int32 hour = 0; hour < 12; hour++) {
295		float x1 = fCenterX + sinf(hour * M_PI / 6.0) * fRadius;
296		float y1 = fCenterY + cosf(hour * M_PI / 6.0) * fRadius;
297		float x2 = fCenterX + sinf(hour * M_PI / 6.0) * (fRadius * 0.9);
298		float y2 = fCenterY + cosf(hour * M_PI / 6.0) * (fRadius * 0.9);
299		StrokeLine(BPoint(x1, y1), BPoint(x2, y2));
300	}
301
302	rgb_color knobColor = tint_color(HighColor(), B_DARKEN_2_TINT);;
303	rgb_color hourColor;
304	if (fHourDragging)
305		hourColor = (rgb_color){ 0, 0, 255, 255 };
306	else
307	 	hourColor = tint_color(HighColor(), B_DARKEN_2_TINT);
308
309	rgb_color minuteColor;
310	if (fMinuteDragging)
311		minuteColor = (rgb_color){ 0, 0, 255, 255 };
312	else
313	 	minuteColor = tint_color(HighColor(), B_DARKEN_2_TINT);
314
315	rgb_color secondsColor = (rgb_color){ 255, 0, 0, 255 };
316	rgb_color shadowColor = tint_color(LowColor(),
317		(B_DARKEN_1_TINT + B_DARKEN_2_TINT) / 2);
318
319	_DrawHands(fCenterX + 1.5, fCenterY + 1.5, fRadius,
320		shadowColor, shadowColor, shadowColor, shadowColor);
321	_DrawHands(fCenterX, fCenterY, fRadius,
322		hourColor, minuteColor, secondsColor, knobColor);
323
324	Sync();
325
326	UnlockLooper();
327}
328
329
330bool
331TAnalogClock::InHourHand(BPoint point)
332{
333	int32 ticks = fHours;
334	if (ticks > 12)
335		ticks -= 12;
336	ticks *= 5;
337	ticks += int32(5. * fMinutes / 60.0);
338	if (ticks > 60)
339		ticks -= 60;
340	return _InHand(point, ticks, fRadius * 0.7);
341}
342
343
344bool
345TAnalogClock::InMinuteHand(BPoint point)
346{
347	return _InHand(point, fMinutes, fRadius * 0.9);
348}
349
350
351void
352TAnalogClock::SetHourHand(BPoint point)
353{
354	point.x -= fCenterX;
355	point.y -= fCenterY;
356
357	float pointPhi = _GetPhi(point);
358	float hoursExact = 6.0 * pointPhi / M_PI;
359	if (fHours >= 12)
360		fHours = 12;
361	else
362		fHours = 0;
363	fHours += int32(hoursExact);
364
365	SetTime(fHours, fMinutes, fSeconds);
366}
367
368
369void
370TAnalogClock::SetMinuteHand(BPoint point)
371{
372	point.x -= fCenterX;
373	point.y -= fCenterY;
374
375	float pointPhi = _GetPhi(point);
376	float minutesExact = 30.0 * pointPhi / M_PI;
377	fMinutes = int32(ceilf(minutesExact));
378
379	SetTime(fHours, fMinutes, fSeconds);
380}
381
382
383float
384TAnalogClock::_GetPhi(BPoint point)
385{
386	if (point.x == 0 && point.y < 0)
387		return 2 * M_PI;
388	if (point.x == 0 && point.y > 0)
389		return M_PI;
390	if (point.y == 0 && point.x < 0)
391		return M_PI * 3 / 2;
392	if (point.y == 0 && point.x > 0)
393		return M_PI / 2;
394
395	float pointPhi = atanf(-1. * point.y / point.x);
396	if (point.y < 0. && point.x > 0.)	// right upper corner
397		pointPhi = M_PI / 2. - pointPhi;
398	if (point.y > 0. && point.x > 0.)	// right lower corner
399		pointPhi = M_PI / 2 - pointPhi;
400	if (point.y > 0. && point.x < 0.)	// left lower corner
401		pointPhi = (M_PI * 3. / 2. - pointPhi);
402	if (point.y < 0. && point.x < 0.)	// left upper corner
403		pointPhi = 3. / 2. * M_PI - pointPhi;
404	return pointPhi;
405}
406
407
408bool
409TAnalogClock::_InHand(BPoint point, int32 ticks, float radius)
410{
411	point.x -= fCenterX;
412	point.y -= fCenterY;
413
414	float pRadius = sqrt(pow(point.x, 2) + pow(point.y, 2));
415
416	if (radius < pRadius)
417		return false;
418
419	float pointPhi = _GetPhi(point);
420	float handPhi = M_PI / 30.0 * ticks;
421	float delta = pointPhi - handPhi;
422	if (fabs(delta) > DRAG_DELTA_PHI)
423		return false;
424
425	return true;
426}
427
428
429void
430TAnalogClock::_DrawHands(float x, float y, float radius,
431	rgb_color hourColor, rgb_color minuteColor,
432	rgb_color secondsColor, rgb_color knobColor)
433{
434	float offsetX;
435	float offsetY;
436
437	// calc, draw hour hand
438	SetHighColor(hourColor);
439	SetPenSize(4.0);
440	float hours = fHours + float(fMinutes) / 60.0;
441	offsetX = (radius * 0.7) * sinf((hours * M_PI) / 6.0);
442	offsetY = (radius * 0.7) * cosf((hours * M_PI) / 6.0);
443	StrokeLine(BPoint(x, y), BPoint(x + offsetX, y - offsetY));
444
445	// calc, draw minute hand
446	SetHighColor(minuteColor);
447	SetPenSize(3.0);
448	float minutes = fMinutes + float(fSeconds) / 60.0;
449	offsetX = (radius * 0.9) * sinf((minutes * M_PI) / 30.0);
450	offsetY = (radius * 0.9) * cosf((minutes * M_PI) / 30.0);
451	StrokeLine(BPoint(x, y), BPoint(x + offsetX, y - offsetY));
452
453	if (fDrawSecondHand) {
454		// calc, draw second hand
455		SetHighColor(secondsColor);
456		SetPenSize(1.0);
457		offsetX = (radius * 0.95) * sinf((fSeconds * M_PI) / 30.0);
458		offsetY = (radius * 0.95) * cosf((fSeconds * M_PI) / 30.0);
459		StrokeLine(BPoint(x, y), BPoint(x + offsetX, y - offsetY));
460	}
461
462	// draw the center knob
463	SetHighColor(knobColor);
464	FillEllipse(BPoint(x, y), radius * 0.06, radius * 0.06);
465}
466