1/*
2 * Copyright 2004-2011, Haiku, Inc. All Rights Reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		McCall <mccall@@digitalparadise.co.uk>
7 *		Mike Berg <mike@berg-net.us>
8 *		Julun <host.haiku@gmx.de>
9 *		Clemens <mail@Clemens-Zeidler.de>
10 *		Adrien Destugues <pulkomandy@pulkomandy.cx>
11 *		Hamish Morrison <hamish@lavabit.com>
12 */
13
14
15#include "DateTimeEdit.h"
16
17#include <stdlib.h>
18
19#include <DateFormat.h>
20#include <List.h>
21#include <Locale.h>
22#include <String.h>
23#include <Window.h>
24
25
26using BPrivate::B_LOCAL_TIME;
27
28
29TTimeEdit::TTimeEdit(const char* name, uint32 sections)
30	:
31	TSectionEdit(name, sections),
32	fLastKeyDownTime(0),
33	fFields(NULL),
34	fFieldCount(0),
35	fFieldPositions(NULL),
36	fFieldPosCount(0)
37{
38	InitView();
39}
40
41
42TTimeEdit::~TTimeEdit()
43{
44	free(fFieldPositions);
45	free(fFields);
46}
47
48
49void
50TTimeEdit::KeyDown(const char* bytes, int32 numBytes)
51{
52	TSectionEdit::KeyDown(bytes, numBytes);
53
54	// only accept valid input
55	int32 number = atoi(bytes);
56	if (number - 1 < 0)
57		return;
58
59	int32 section = FocusIndex();
60	if (section < 0 || section > 2)
61		return;
62	bigtime_t currentTime = system_time();
63	if (currentTime - fLastKeyDownTime < 1000000) {
64		int32 doubleDigit = number + fLastKeyDownInt * 10;
65		if (_IsValidDoubleDigit(doubleDigit))
66			number = doubleDigit;
67		fLastKeyDownTime = 0;
68	} else {
69		fLastKeyDownTime = currentTime;
70		fLastKeyDownInt = number;
71	}
72
73	// update display value
74	fHoldValue = number;
75
76	_CheckRange();
77
78	// send message to change time
79	DispatchMessage();
80}
81
82
83void
84TTimeEdit::InitView()
85{
86	// make sure we call the base class method, as it
87	// will create the arrow bitmaps and the section list
88	fTime = BDateTime::CurrentDateTime(B_LOCAL_TIME);
89	_UpdateFields();
90}
91
92
93void
94TTimeEdit::DrawSection(uint32 index, BRect bounds, bool hasFocus)
95{
96	if (fFieldPositions == NULL || index * 2 + 1 >= (uint32)fFieldPosCount)
97		return;
98
99	if (hasFocus)
100		SetLowColor(mix_color(ui_color(B_CONTROL_HIGHLIGHT_COLOR),
101			ViewColor(), 192));
102	else
103		SetLowColor(ViewColor());
104
105	BString field;
106	fText.CopyCharsInto(field, fFieldPositions[index * 2],
107		fFieldPositions[index * 2 + 1] - fFieldPositions[index * 2]);
108
109	BPoint point(bounds.LeftBottom());
110	point.y -= bounds.Height() / 2.0 - 6.0;
111	point.x += (bounds.Width() - StringWidth(field)) / 2;
112	SetHighUIColor(B_PANEL_TEXT_COLOR);
113	FillRect(bounds, B_SOLID_LOW);
114	DrawString(field, point);
115}
116
117
118void
119TTimeEdit::DrawSeparator(uint32 index, BRect bounds)
120{
121	if (fFieldPositions == NULL || index * 2 + 2 >= (uint32)fFieldPosCount)
122		return;
123
124	BString field;
125	fText.CopyCharsInto(field, fFieldPositions[index * 2 + 1],
126		fFieldPositions[index * 2 + 2] - fFieldPositions[index * 2 + 1]);
127
128	BPoint point(bounds.LeftBottom());
129	point.y -= bounds.Height() / 2.0 - 6.0;
130	point.x += (bounds.Width() - StringWidth(field)) / 2;
131	SetHighUIColor(B_PANEL_TEXT_COLOR);
132	DrawString(field, point);
133}
134
135
136float
137TTimeEdit::SeparatorWidth()
138{
139	return 10.0f;
140}
141
142
143float
144TTimeEdit::MinSectionWidth()
145{
146	return be_plain_font->StringWidth("00");
147}
148
149
150void
151TTimeEdit::SectionFocus(uint32 index)
152{
153	fLastKeyDownTime = 0;
154	fFocus = index;
155	fHoldValue = _SectionValue(index);
156	Draw(Bounds());
157}
158
159
160void
161TTimeEdit::SetTime(int32 hour, int32 minute, int32 second)
162{
163	// make sure to update date upon overflow
164	if (hour == 0 && minute == 0 && second == 0)
165		fTime = BDateTime::CurrentDateTime(B_LOCAL_TIME);
166
167	fTime.SetTime(BTime(hour, minute, second));
168
169	if (LockLooper()) {
170		_UpdateFields();
171		UnlockLooper();
172	}
173
174	Invalidate(Bounds());
175}
176
177
178void
179TTimeEdit::DoUpPress()
180{
181	if (fFocus == -1)
182		SectionFocus(0);
183
184	// update displayed value
185	fHoldValue += 1;
186
187	_CheckRange();
188
189	// send message to change time
190	DispatchMessage();
191}
192
193
194void
195TTimeEdit::DoDownPress()
196{
197	if (fFocus == -1)
198		SectionFocus(0);
199
200	// update display value
201	fHoldValue -= 1;
202
203	_CheckRange();
204
205	// send message to change time
206	DispatchMessage();
207
208}
209
210
211void
212TTimeEdit::BuildDispatch(BMessage* message)
213{
214	if (fFocus < 0 || fFocus >= fFieldCount)
215		return;
216
217	message->AddBool("time", true);
218
219	for (int32 index = 0; index < (int)fSectionCount; ++index) {
220		uint32 data = _SectionValue(index);
221
222		if (fFocus == index)
223			data = fHoldValue;
224
225		switch (fFields[index]) {
226			case B_DATE_ELEMENT_HOUR:
227				message->AddInt32("hour", data);
228				break;
229
230			case B_DATE_ELEMENT_MINUTE:
231				message->AddInt32("minute", data);
232				break;
233
234			case B_DATE_ELEMENT_SECOND:
235				message->AddInt32("second", data);
236				break;
237
238			default:
239				break;
240		}
241	}
242}
243
244
245void
246TTimeEdit::_UpdateFields()
247{
248	time_t time = fTime.Time_t();
249
250	if (fFieldPositions != NULL) {
251		free(fFieldPositions);
252		fFieldPositions = NULL;
253	}
254	fTimeFormat.Format(fText, fFieldPositions, fFieldPosCount, time,
255		B_MEDIUM_TIME_FORMAT);
256
257	if (fFields != NULL) {
258		free(fFields);
259		fFields = NULL;
260	}
261	fTimeFormat.GetTimeFields(fFields, fFieldCount, B_MEDIUM_TIME_FORMAT);
262}
263
264
265void
266TTimeEdit::_CheckRange()
267{
268	if (fFocus < 0 || fFocus >= fFieldCount)
269		return;
270
271	int32 value = fHoldValue;
272	switch (fFields[fFocus]) {
273		case B_DATE_ELEMENT_HOUR:
274			if (value > 23)
275				value = 0;
276			else if (value < 0)
277				value = 23;
278
279			fTime.SetTime(BTime(value, fTime.Time().Minute(),
280				fTime.Time().Second()));
281			break;
282
283		case B_DATE_ELEMENT_MINUTE:
284			if (value> 59)
285				value = 0;
286			else if (value < 0)
287				value = 59;
288
289			fTime.SetTime(BTime(fTime.Time().Hour(), value,
290				fTime.Time().Second()));
291			break;
292
293		case B_DATE_ELEMENT_SECOND:
294			if (value > 59)
295				value = 0;
296			else if (value < 0)
297				value = 59;
298
299			fTime.SetTime(BTime(fTime.Time().Hour(), fTime.Time().Minute(),
300				value));
301			break;
302
303		case B_DATE_ELEMENT_AM_PM:
304			value = fTime.Time().Hour();
305			if (value < 13)
306				value += 12;
307			else
308				value -= 12;
309			if (value == 24)
310				value = 0;
311
312			// modify hour value to reflect change in am/ pm
313			fTime.SetTime(BTime(value, fTime.Time().Minute(),
314				fTime.Time().Second()));
315			break;
316
317		default:
318			return;
319	}
320
321	fHoldValue = value;
322	Invalidate(Bounds());
323}
324
325
326bool
327TTimeEdit::_IsValidDoubleDigit(int32 value)
328{
329	if (fFocus < 0 || fFocus >= fFieldCount)
330		return false;
331
332	bool isInRange = false;
333	switch (fFields[fFocus]) {
334		case B_DATE_ELEMENT_HOUR:
335			if (value <= 23)
336				isInRange = true;
337			break;
338
339		case B_DATE_ELEMENT_MINUTE:
340			if (value <= 59)
341				isInRange = true;
342			break;
343
344		case B_DATE_ELEMENT_SECOND:
345			if (value <= 59)
346				isInRange = true;
347			break;
348
349		default:
350			break;
351	}
352
353	return isInRange;
354}
355
356
357int32
358TTimeEdit::_SectionValue(int32 index) const
359{
360	if (index < 0 || index >= fFieldCount)
361		return 0;
362
363	int32 value;
364	switch (fFields[index]) {
365		case B_DATE_ELEMENT_HOUR:
366			value = fTime.Time().Hour();
367			break;
368
369		case B_DATE_ELEMENT_MINUTE:
370			value = fTime.Time().Minute();
371			break;
372
373		case B_DATE_ELEMENT_SECOND:
374			value = fTime.Time().Second();
375			break;
376
377		default:
378			value = 0;
379			break;
380	}
381
382	return value;
383}
384
385
386float
387TTimeEdit::PreferredHeight()
388{
389	font_height fontHeight;
390	GetFontHeight(&fontHeight);
391	return ceilf((fontHeight.ascent + fontHeight.descent) * 1.4);
392}
393
394
395//	#pragma mark -
396
397
398TDateEdit::TDateEdit(const char* name, uint32 sections)
399	:
400	TSectionEdit(name, sections),
401	fFields(NULL),
402	fFieldCount(0),
403	fFieldPositions(NULL),
404	fFieldPosCount(0)
405{
406	InitView();
407}
408
409
410TDateEdit::~TDateEdit()
411{
412	free(fFieldPositions);
413	free(fFields);
414}
415
416
417void
418TDateEdit::KeyDown(const char* bytes, int32 numBytes)
419{
420	TSectionEdit::KeyDown(bytes, numBytes);
421
422	// only accept valid input
423	int32 number = atoi(bytes);
424	if (number - 1 < 0)
425		return;
426
427	int32 section = FocusIndex();
428	if (section < 0 || section > 2)
429		return;
430
431	bigtime_t currentTime = system_time();
432	if (currentTime - fLastKeyDownTime < 1000000) {
433		int32 doubleDigit = number + fLastKeyDownInt * 10;
434		if (_IsValidDoubleDigit(doubleDigit))
435			number = doubleDigit;
436		fLastKeyDownTime = 0;
437	} else {
438		fLastKeyDownTime = currentTime;
439		fLastKeyDownInt = number;
440	}
441
442	// if year add 2000
443
444	if (fFields[section] == B_DATE_ELEMENT_YEAR) {
445		int32 oldCentury = int32(fHoldValue / 100) * 100;
446		if (number < 10 && oldCentury == 1900)
447			number += 70;
448		number += oldCentury;
449	}
450
451	// update display value
452	fHoldValue = number;
453
454	_CheckRange();
455
456	// send message to change time
457	DispatchMessage();
458}
459
460
461void
462TDateEdit::InitView()
463{
464	// make sure we call the base class method, as it
465	// will create the arrow bitmaps and the section list
466	fDate = BDate::CurrentDate(B_LOCAL_TIME);
467	_UpdateFields();
468}
469
470
471void
472TDateEdit::DrawSection(uint32 index, BRect bounds, bool hasFocus)
473{
474	if (fFieldPositions == NULL || index * 2 + 1 >= (uint32)fFieldPosCount)
475		return;
476
477	if (hasFocus)
478		SetLowColor(mix_color(ui_color(B_CONTROL_HIGHLIGHT_COLOR),
479			ViewColor(), 192));
480	else
481		SetLowColor(ViewColor());
482
483	BString field;
484	fText.CopyCharsInto(field, fFieldPositions[index * 2],
485		fFieldPositions[index * 2 + 1] - fFieldPositions[index * 2]);
486
487	BPoint point(bounds.LeftBottom());
488	point.y -= bounds.Height() / 2.0 - 6.0;
489	point.x += (bounds.Width() - StringWidth(field)) / 2;
490	SetHighUIColor(B_PANEL_TEXT_COLOR);
491	FillRect(bounds, B_SOLID_LOW);
492	DrawString(field, point);
493}
494
495
496void
497TDateEdit::DrawSeparator(uint32 index, BRect bounds)
498{
499	if (index >= 2)
500		return;
501
502	if (fFieldPositions == NULL || index * 2 + 2 >= (uint32)fFieldPosCount)
503		return;
504
505	BString field;
506	fText.CopyCharsInto(field, fFieldPositions[index * 2 + 1],
507		fFieldPositions[index * 2 + 2] - fFieldPositions[index * 2 + 1]);
508
509	BPoint point(bounds.LeftBottom());
510	point.y -= bounds.Height() / 2.0 - 6.0;
511	point.x += (bounds.Width() - StringWidth(field)) / 2;
512	SetHighUIColor(B_PANEL_TEXT_COLOR);
513	DrawString(field, point);
514}
515
516
517void
518TDateEdit::SectionFocus(uint32 index)
519{
520	fLastKeyDownTime = 0;
521	fFocus = index;
522	fHoldValue = _SectionValue(index);
523	Draw(Bounds());
524}
525
526
527float
528TDateEdit::MinSectionWidth()
529{
530	return be_plain_font->StringWidth("00");
531}
532
533
534float
535TDateEdit::SeparatorWidth()
536{
537	return 10.0f;
538}
539
540
541void
542TDateEdit::SetDate(int32 year, int32 month, int32 day)
543{
544	fDate.SetDate(year, month, day);
545
546	if (LockLooper()) {
547		_UpdateFields();
548		UnlockLooper();
549	}
550
551	Invalidate(Bounds());
552}
553
554
555void
556TDateEdit::DoUpPress()
557{
558	if (fFocus == -1)
559		SectionFocus(0);
560
561	// update displayed value
562	fHoldValue += 1;
563
564	_CheckRange();
565
566	// send message to change Date
567	DispatchMessage();
568}
569
570
571void
572TDateEdit::DoDownPress()
573{
574	if (fFocus == -1)
575		SectionFocus(0);
576
577	// update display value
578	fHoldValue -= 1;
579
580	_CheckRange();
581
582	// send message to change Date
583	DispatchMessage();
584}
585
586
587void
588TDateEdit::BuildDispatch(BMessage* message)
589{
590	if (fFocus < 0 || fFocus >= fFieldCount)
591		return;
592
593	message->AddBool("time", false);
594
595	for (int32 index = 0; index < (int)fSectionCount; ++index) {
596		uint32 data = _SectionValue(index);
597
598		if (fFocus == index)
599			data = fHoldValue;
600
601		switch (fFields[index]) {
602			case B_DATE_ELEMENT_MONTH:
603				message->AddInt32("month", data);
604				break;
605
606			case B_DATE_ELEMENT_DAY:
607				message->AddInt32("day", data);
608				break;
609
610			case B_DATE_ELEMENT_YEAR:
611				message->AddInt32("year", data);
612				break;
613
614			default:
615				break;
616		}
617	}
618}
619
620
621void
622TDateEdit::_UpdateFields()
623{
624	time_t time = BDateTime(fDate, BTime()).Time_t();
625
626	if (fFieldPositions != NULL) {
627		free(fFieldPositions);
628		fFieldPositions = NULL;
629	}
630
631	fDateFormat.Format(fText, fFieldPositions, fFieldPosCount, time,
632		B_SHORT_DATE_FORMAT);
633
634	if (fFields != NULL) {
635		free(fFields);
636		fFields = NULL;
637	}
638	fDateFormat.GetFields(fFields, fFieldCount, B_SHORT_DATE_FORMAT);
639}
640
641
642void
643TDateEdit::_CheckRange()
644{
645	if (fFocus < 0 || fFocus >= fFieldCount)
646		return;
647
648	int32 value = fHoldValue;
649	switch (fFields[fFocus]) {
650		case B_DATE_ELEMENT_DAY:
651		{
652			int32 days = fDate.DaysInMonth();
653			if (value > days)
654				value = 1;
655			else if (value < 1)
656				value = days;
657
658			fDate.SetDate(fDate.Year(), fDate.Month(), value);
659			break;
660		}
661
662		case B_DATE_ELEMENT_MONTH:
663			if (value > 12)
664				value = 1;
665			else if (value < 1)
666				value = 12;
667
668			fDate.SetDate(fDate.Year(), value, fDate.Day());
669			break;
670
671		case B_DATE_ELEMENT_YEAR:
672			// 2037 is the end of 32-bit UNIX time
673			if (value > 2037)
674				value = 2037;
675			else if (value < 1970)
676				value = 1970;
677
678			fDate.SetDate(value, fDate.Month(), fDate.Day());
679			break;
680
681		default:
682			return;
683	}
684
685	fHoldValue = value;
686	Draw(Bounds());
687}
688
689
690bool
691TDateEdit::_IsValidDoubleDigit(int32 value)
692{
693	if (fFocus < 0 || fFocus >= fFieldCount)
694		return false;
695
696	bool isInRange = false;
697	switch (fFields[fFocus]) {
698		case B_DATE_ELEMENT_DAY:
699		{
700			int32 days = fDate.DaysInMonth();
701			if (value >= 1 && value <= days)
702				isInRange = true;
703			break;
704		}
705
706		case B_DATE_ELEMENT_MONTH:
707		{
708			if (value >= 1 && value <= 12)
709				isInRange = true;
710			break;
711		}
712
713		case B_DATE_ELEMENT_YEAR:
714		{
715			int32 year = int32(fHoldValue / 100) * 100 + value;
716			if (year <= 2037 && year >= 1970)
717				isInRange = true;
718			break;
719		}
720
721		default:
722			break;
723	}
724
725	return isInRange;
726}
727
728
729int32
730TDateEdit::_SectionValue(int32 index) const
731{
732	if (index < 0 || index >= fFieldCount)
733		return 0;
734
735	int32 value = 0;
736	switch (fFields[index]) {
737		case B_DATE_ELEMENT_YEAR:
738			value = fDate.Year();
739			break;
740
741		case B_DATE_ELEMENT_MONTH:
742			value = fDate.Month();
743			break;
744
745		case B_DATE_ELEMENT_DAY:
746			value = fDate.Day();
747			break;
748
749		default:
750			break;
751	}
752
753	return value;
754}
755
756
757float
758TDateEdit::PreferredHeight()
759{
760	font_height fontHeight;
761	GetFontHeight(&fontHeight);
762	return ceilf((fontHeight.ascent + fontHeight.descent) * 1.4);
763}
764
765