⛏️ index : haiku.git

/*
 * Copyright 2004-2011, Haiku, Inc. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		McCall <mccall@@digitalparadise.co.uk>
 *		Mike Berg <mike@berg-net.us>
 *		Julun <host.haiku@gmx.de>
 *		Clemens <mail@Clemens-Zeidler.de>
 *		Adrien Destugues <pulkomandy@pulkomandy.cx>
 *		Hamish Morrison <hamish@lavabit.com>
 */


#include "DateTimeEdit.h"

#include <stdlib.h>

#include <ControlLook.h>
#include <DateFormat.h>
#include <LayoutUtils.h>
#include <List.h>
#include <Locale.h>
#include <String.h>
#include <Window.h>


namespace BPrivate {


const uint32 kArrowAreaWidth = 16;


TimeEdit::TimeEdit(const char* name, uint32 sections, BMessage* message)
	:
	SectionEdit(name, sections, message),
	fLastKeyDownTime(0),
	fFields(NULL),
	fFieldCount(0),
	fFieldPositions(NULL),
	fFieldPosCount(0)
{
	InitView();
}


TimeEdit::~TimeEdit()
{
	free(fFieldPositions);
	free(fFields);
}


void
TimeEdit::KeyDown(const char* bytes, int32 numBytes)
{
	if (IsEnabled() == false)
		return;
	SectionEdit::KeyDown(bytes, numBytes);

	// only accept valid input
	int32 number = atoi(bytes);
	if (number < 0 || bytes[0] < '0')
		return;

	int32 section = FocusIndex();
	if (section < 0 || section > 2)
		return;

	bigtime_t currentTime = system_time();
	if (currentTime - fLastKeyDownTime < 1000000) {
		int32 doubleDigit = number + fLastKeyDownInt * 10;
		if (_IsValidDoubleDigit(doubleDigit))
			number = doubleDigit;
		fLastKeyDownTime = 0;
	} else {
		fLastKeyDownTime = currentTime;
		fLastKeyDownInt = number;
	}

	// update display value
	fHoldValue = number;
	_CheckRange();
	_UpdateFields();

	// send message to change time
	Invoke();
}


void
TimeEdit::InitView()
{
	// make sure we call the base class method, as it
	// will create the arrow bitmaps and the section list
	fTime = BDateTime::CurrentDateTime(B_LOCAL_TIME);
	_UpdateFields();
}


void
TimeEdit::DrawSection(uint32 index, BRect bounds, bool hasFocus)
{
	if (fFieldPositions == NULL || index * 2 + 1 >= (uint32)fFieldPosCount)
		return;

	if (hasFocus)
		SetLowColor(mix_color(ui_color(B_CONTROL_HIGHLIGHT_COLOR),
			ui_color(B_DOCUMENT_BACKGROUND_COLOR), 192));
	else
		SetLowUIColor(B_DOCUMENT_BACKGROUND_COLOR);

	BString field;
	fText.CopyCharsInto(field, fFieldPositions[index * 2],
		fFieldPositions[index * 2 + 1] - fFieldPositions[index * 2]);

	BPoint point(bounds.LeftBottom());
	point.y -= bounds.Height() / 2.0 - 6.0;
	point.x += (bounds.Width() - StringWidth(field)) / 2;
	FillRect(bounds, B_SOLID_LOW);
	DrawString(field, point);
}


void
TimeEdit::DrawSeparator(uint32 index, BRect bounds)
{
	if (fFieldPositions == NULL || index * 2 + 2 >= (uint32)fFieldPosCount)
		return;

	BString field;
	fText.CopyCharsInto(field, fFieldPositions[index * 2 + 1],
		fFieldPositions[index * 2 + 2] - fFieldPositions[index * 2 + 1]);

	BPoint point(bounds.LeftBottom());
	point.y -= bounds.Height() / 2.0 - 6.0;
	point.x += (bounds.Width() - StringWidth(field)) / 2;
	DrawString(field, point);
}


float
TimeEdit::SeparatorWidth()
{
	return 10.0f;
}


float
TimeEdit::MinSectionWidth()
{
	return be_plain_font->StringWidth("00");
}


void
TimeEdit::SectionFocus(uint32 index)
{
	fLastKeyDownTime = 0;
	fFocus = index;
	fHoldValue = _SectionValue(index);
	Draw(Bounds());
}


void
TimeEdit::SetTime(int32 hour, int32 minute, int32 second)
{
	// make sure to update date upon overflow
	if (hour == 0 && minute == 0 && second == 0)
		fTime = BDateTime::CurrentDateTime(B_LOCAL_TIME);

	fTime.SetTime(BTime(hour, minute, second));

	if (LockLooper()) {
		_UpdateFields();
		UnlockLooper();
	}

	Invalidate(Bounds());
}


BTime
TimeEdit::GetTime()
{
	return fTime.Time();
}


void
TimeEdit::DoUpPress()
{
	if (fFocus == -1)
		SectionFocus(0);

	// update displayed value
	fHoldValue += 1;

	_CheckRange();
	_UpdateFields();

	// send message to change time
	Invoke();
}


void
TimeEdit::DoDownPress()
{
	if (fFocus == -1)
		SectionFocus(0);

	// update display value
	fHoldValue -= 1;

	_CheckRange();
	_UpdateFields();

	Invoke();
}


void
TimeEdit::PopulateMessage(BMessage* message)
{
	if (fFocus < 0 || fFocus >= fFieldCount)
		return;

	message->AddBool("time", true);
	message->AddInt32("hour", fTime.Time().Hour());
	message->AddInt32("minute", fTime.Time().Minute());
	message->AddInt32("second", fTime.Time().Second());
}


void
TimeEdit::_UpdateFields()
{
	time_t time = fTime.Time_t();

	if (fFieldPositions != NULL) {
		free(fFieldPositions);
		fFieldPositions = NULL;
	}
	fTimeFormat.Format(fText, fFieldPositions, fFieldPosCount, time,
		B_MEDIUM_TIME_FORMAT);

	if (fFields != NULL) {
		free(fFields);
		fFields = NULL;
	}
	fTimeFormat.GetTimeFields(fFields, fFieldCount, B_MEDIUM_TIME_FORMAT);
}


void
TimeEdit::_CheckRange()
{
	if (fFocus < 0 || fFocus >= fFieldCount)
		return;

	int32 value = fHoldValue;
	switch (fFields[fFocus]) {
		case B_DATE_ELEMENT_HOUR:
			if (value > 23)
				value = 0;
			else if (value < 0)
				value = 23;

			fTime.SetTime(BTime(value, fTime.Time().Minute(),
				fTime.Time().Second()));
			break;

		case B_DATE_ELEMENT_MINUTE:
			if (value> 59)
				value = 0;
			else if (value < 0)
				value = 59;

			fTime.SetTime(BTime(fTime.Time().Hour(), value,
				fTime.Time().Second()));
			break;

		case B_DATE_ELEMENT_SECOND:
			if (value > 59)
				value = 0;
			else if (value < 0)
				value = 59;

			fTime.SetTime(BTime(fTime.Time().Hour(), fTime.Time().Minute(),
				value));
			break;

		case B_DATE_ELEMENT_AM_PM:
			value = fTime.Time().Hour();
			if (value < 13)
				value += 12;
			else
				value -= 12;
			if (value == 24)
				value = 0;

			// modify hour value to reflect change in am/ pm
			fTime.SetTime(BTime(value, fTime.Time().Minute(),
				fTime.Time().Second()));
			break;

		default:
			return;
	}


	fHoldValue = value;
	Invalidate(Bounds());
}


bool
TimeEdit::_IsValidDoubleDigit(int32 value)
{
	if (fFocus < 0 || fFocus >= fFieldCount)
		return false;

	bool isInRange = false;
	switch (fFields[fFocus]) {
		case B_DATE_ELEMENT_HOUR:
			if (value <= 23)
				isInRange = true;
			break;

		case B_DATE_ELEMENT_MINUTE:
			if (value <= 59)
				isInRange = true;
			break;

		case B_DATE_ELEMENT_SECOND:
			if (value <= 59)
				isInRange = true;
			break;

		default:
			break;
	}

	return isInRange;
}


int32
TimeEdit::_SectionValue(int32 index) const
{
	if (index < 0 || index >= fFieldCount)
		return 0;

	int32 value;
	switch (fFields[index]) {
		case B_DATE_ELEMENT_HOUR:
			value = fTime.Time().Hour();
			break;

		case B_DATE_ELEMENT_MINUTE:
			value = fTime.Time().Minute();
			break;

		case B_DATE_ELEMENT_SECOND:
			value = fTime.Time().Second();
			break;

		default:
			value = 0;
			break;
	}

	return value;
}


float
TimeEdit::PreferredHeight()
{
	font_height fontHeight;
	GetFontHeight(&fontHeight);
	return ceilf((fontHeight.ascent + fontHeight.descent) * 1.4);
}


// #pragma mark -


DateEdit::DateEdit(const char* name, uint32 sections, BMessage* message)
	:
	SectionEdit(name, sections, message),
	fFields(NULL),
	fFieldCount(0),
	fFieldPositions(NULL),
	fFieldPosCount(0)
{
	InitView();
}


DateEdit::~DateEdit()
{
	free(fFieldPositions);
	free(fFields);
}


void
DateEdit::KeyDown(const char* bytes, int32 numBytes)
{
	if (IsEnabled() == false)
		return;
	SectionEdit::KeyDown(bytes, numBytes);

	// only accept valid input
	int32 number = atoi(bytes);
	if (number < 0 || bytes[0] < '0')
		return;

	int32 section = FocusIndex();
	if (section < 0 || section > 2)
		return;

	bigtime_t currentTime = system_time();
	if (currentTime - fLastKeyDownTime < 1000000) {
		int32 doubleDigit = number + fLastKeyDownInt * 10;
		if (_IsValidDoubleDigit(doubleDigit))
			number = doubleDigit;
		fLastKeyDownTime = 0;
	} else {
		fLastKeyDownTime = currentTime;
		fLastKeyDownInt = number;
	}

	// if year add 2000

	if (fFields[section] == B_DATE_ELEMENT_YEAR) {
		int32 oldCentury = int32(fHoldValue / 100) * 100;
		if (number < 10 && oldCentury == 1900)
			number += 70;
		number += oldCentury;
	}
	fHoldValue = number;

	// update display value
	_CheckRange();
	_UpdateFields();

	// send message to change time
	Invoke();
}


void
DateEdit::InitView()
{
	// make sure we call the base class method, as it
	// will create the arrow bitmaps and the section list
	fDate = BDate::CurrentDate(B_LOCAL_TIME);
	_UpdateFields();
}


void
DateEdit::DrawSection(uint32 index, BRect bounds, bool hasFocus)
{
	if (fFieldPositions == NULL || index * 2 + 1 >= (uint32)fFieldPosCount)
		return;

	if (hasFocus)
		SetLowColor(mix_color(ui_color(B_CONTROL_HIGHLIGHT_COLOR),
			ui_color(B_DOCUMENT_BACKGROUND_COLOR), 192));
	else
		SetLowUIColor(B_DOCUMENT_BACKGROUND_COLOR);

	BString field;
	fText.CopyCharsInto(field, fFieldPositions[index * 2],
		fFieldPositions[index * 2 + 1] - fFieldPositions[index * 2]);

	BPoint point(bounds.LeftBottom());
	point.y -= bounds.Height() / 2.0 - 6.0;
	point.x += (bounds.Width() - StringWidth(field)) / 2;
	FillRect(bounds, B_SOLID_LOW);
	DrawString(field, point);
}


void
DateEdit::DrawSeparator(uint32 index, BRect bounds)
{
	if (index >= 2)
		return;

	if (fFieldPositions == NULL || index * 2 + 2 >= (uint32)fFieldPosCount)
		return;

	BString field;
	fText.CopyCharsInto(field, fFieldPositions[index * 2 + 1],
		fFieldPositions[index * 2 + 2] - fFieldPositions[index * 2 + 1]);

	BPoint point(bounds.LeftBottom());
	point.y -= bounds.Height() / 2.0 - 6.0;
	point.x += (bounds.Width() - StringWidth(field)) / 2;
	DrawString(field, point);
}


void
DateEdit::SectionFocus(uint32 index)
{
	fLastKeyDownTime = 0;
	fFocus = index;
	fHoldValue = _SectionValue(index);
	Draw(Bounds());
}


float
DateEdit::MinSectionWidth()
{
	return be_plain_font->StringWidth("00");
}


float
DateEdit::SeparatorWidth()
{
	return 10.0f;
}


void
DateEdit::SetDate(int32 year, int32 month, int32 day)
{
	fDate.SetDate(year, month, day);

	if (LockLooper()) {
		_UpdateFields();
		UnlockLooper();
	}

	Invalidate(Bounds());
}


BDate
DateEdit::GetDate()
{
	return fDate;
}


void
DateEdit::DoUpPress()
{
	if (fFocus == -1)
		SectionFocus(0);

	// update displayed value
	fHoldValue += 1;

	_CheckRange();
	_UpdateFields();

	// send message to change Date
	Invoke();
}


void
DateEdit::DoDownPress()
{
	if (fFocus == -1)
		SectionFocus(0);

	// update display value
	fHoldValue -= 1;

	_CheckRange();
	_UpdateFields();

	// send message to change Date
	Invoke();
}


void
DateEdit::PopulateMessage(BMessage* message)
{
	if (fFocus < 0 || fFocus >= fFieldCount)
		return;

	message->AddBool("time", false);
	message->AddInt32("year", fDate.Year());
	message->AddInt32("month", fDate.Month());
	message->AddInt32("day", fDate.Day());
}


void
DateEdit::_UpdateFields()
{
	time_t time = BDateTime(fDate, BTime()).Time_t();

	if (fFieldPositions != NULL) {
		free(fFieldPositions);
		fFieldPositions = NULL;
	}

	fDateFormat.Format(fText, fFieldPositions, fFieldPosCount, time,
		B_SHORT_DATE_FORMAT);

	if (fFields != NULL) {
		free(fFields);
		fFields = NULL;
	}
	fDateFormat.GetFields(fFields, fFieldCount, B_SHORT_DATE_FORMAT);
}


void
DateEdit::_CheckRange()
{
	if (fFocus < 0 || fFocus >= fFieldCount)
		return;

	int32 value = fHoldValue;
	switch (fFields[fFocus]) {
		case B_DATE_ELEMENT_DAY:
		{
			int32 days = fDate.DaysInMonth();
			if (value > days)
				value = 1;
			else if (value < 1)
				value = days;

			fDate.SetDate(fDate.Year(), fDate.Month(), value);
			break;
		}

		case B_DATE_ELEMENT_MONTH:
		{
			if (value > 12)
				value = 1;
			else if (value < 1)
				value = 12;

			int32 day = fDate.Day();
			fDate.SetDate(fDate.Year(), value, 1);

			// changing between months with differing amounts of days
			while (day > fDate.DaysInMonth())
				day--;
			fDate.SetDate(fDate.Year(), value, day);
			break;
		}

		case B_DATE_ELEMENT_YEAR:
			fDate.SetDate(value, fDate.Month(), fDate.Day());
			break;

		default:
			return;
	}

	fHoldValue = value;
	Invalidate(Bounds());
}


bool
DateEdit::_IsValidDoubleDigit(int32 value)
{
	if (fFocus < 0 || fFocus >= fFieldCount)
		return false;

	bool isInRange = false;
	switch (fFields[fFocus]) {
		case B_DATE_ELEMENT_DAY:
		{
			int32 days = fDate.DaysInMonth();
			if (value >= 1 && value <= days)
				isInRange = true;
			break;
		}

		case B_DATE_ELEMENT_MONTH:
		{
			if (value >= 1 && value <= 12)
				isInRange = true;
			break;
		}

		case B_DATE_ELEMENT_YEAR:
		{
			int32 year = int32(fHoldValue / 100) * 100 + value;
			if (year >= 2000)
				isInRange = true;
			break;
		}

		default:
			break;
	}

	return isInRange;
}


int32
DateEdit::_SectionValue(int32 index) const
{
	if (index < 0 || index >= fFieldCount)
		return 0;

	int32 value = 0;
	switch (fFields[index]) {
		case B_DATE_ELEMENT_YEAR:
			value = fDate.Year();
			break;

		case B_DATE_ELEMENT_MONTH:
			value = fDate.Month();
			break;

		case B_DATE_ELEMENT_DAY:
			value = fDate.Day();
			break;

		default:
			break;
	}

	return value;
}


float
DateEdit::PreferredHeight()
{
	font_height fontHeight;
	GetFontHeight(&fontHeight);
	return ceilf((fontHeight.ascent + fontHeight.descent) * 1.4);
}


// #pragma mark -


SectionEdit::SectionEdit(const char* name, uint32 sections, BMessage* message)
	:
	BControl(name, NULL, message, B_WILL_DRAW | B_NAVIGABLE),
	fFocus(-1),
	fSectionCount(sections),
	fHoldValue(0)
{
}


SectionEdit::~SectionEdit()
{
}


void
SectionEdit::AttachedToWindow()
{
	BControl::AttachedToWindow();

	// Low colors are set in Draw() methods.
	SetHighUIColor(B_DOCUMENT_TEXT_COLOR);
}


void
SectionEdit::Draw(BRect updateRect)
{
	DrawBorder(updateRect);

	for (uint32 idx = 0; idx < fSectionCount; idx++) {
		DrawSection(idx, FrameForSection(idx),
			((uint32)fFocus == idx) && IsFocus());
		if (idx < fSectionCount - 1)
			DrawSeparator(idx, FrameForSeparator(idx));
	}
}


void
SectionEdit::MouseDown(BPoint where)
{
	if (IsEnabled() == false)
		return;

	MakeFocus(true);

	if (fUpRect.Contains(where))
		DoUpPress();
	else if (fDownRect.Contains(where))
		DoDownPress();
	else if (fSectionCount > 0) {
		for (uint32 idx = 0; idx < fSectionCount; idx++) {
			if (FrameForSection(idx).Contains(where)) {
				SectionFocus(idx);
				return;
			}
		}
	}
}


BSize
SectionEdit::MaxSize()
{
	return BLayoutUtils::ComposeSize(ExplicitMaxSize(),
		BSize(B_SIZE_UNLIMITED, PreferredHeight()));
}


BSize
SectionEdit::MinSize()
{
	BSize minSize;
	minSize.height = PreferredHeight();
	minSize.width = (SeparatorWidth() + MinSectionWidth())
		* fSectionCount;
	return BLayoutUtils::ComposeSize(ExplicitMinSize(),
		minSize);
}


BSize
SectionEdit::PreferredSize()
{
	return BLayoutUtils::ComposeSize(ExplicitPreferredSize(),
		MinSize());
}


BRect
SectionEdit::FrameForSection(uint32 index)
{
	BRect area = SectionArea();
	float sepWidth = SeparatorWidth();

	float width = (area.Width() -
		sepWidth * (fSectionCount - 1))
		/ fSectionCount;
	area.left += index * (width + sepWidth);
	area.right = area.left + width;

	return area;
}


BRect
SectionEdit::FrameForSeparator(uint32 index)
{
	BRect area = SectionArea();
	float sepWidth = SeparatorWidth();

	float width = (area.Width() -
		sepWidth * (fSectionCount - 1))
		/ fSectionCount;
	area.left += (index + 1) * width + index * sepWidth;
	area.right = area.left + sepWidth;

	return area;
}


void
SectionEdit::MakeFocus(bool focused)
{
	if (focused == IsFocus())
		return;

	BControl::MakeFocus(focused);

	if (fFocus == -1)
		SectionFocus(0);
	else
		SectionFocus(fFocus);
}


void
SectionEdit::KeyDown(const char* bytes, int32 numbytes)
{
	if (IsEnabled() == false)
		return;
	if (fFocus == -1)
		SectionFocus(0);

	switch (bytes[0]) {
		case B_LEFT_ARROW:
			fFocus -= 1;
			if (fFocus < 0)
				fFocus = fSectionCount - 1;
			SectionFocus(fFocus);
			break;

		case B_RIGHT_ARROW:
			fFocus += 1;
			if ((uint32)fFocus >= fSectionCount)
				fFocus = 0;
			SectionFocus(fFocus);
			break;

		case B_UP_ARROW:
			DoUpPress();
			break;

		case B_DOWN_ARROW:
			DoDownPress();
			break;

		default:
			BControl::KeyDown(bytes, numbytes);
			break;
	}
	Draw(Bounds());
}


status_t
SectionEdit::Invoke(BMessage* message)
{
	if (message == NULL)
		message = Message();
	if (message == NULL)
		return BControl::Invoke(NULL);

	BMessage clone(*message);
	PopulateMessage(&clone);
	return BControl::Invoke(&clone);
}


uint32
SectionEdit::CountSections() const
{
	return fSectionCount;
}


int32
SectionEdit::FocusIndex() const
{
	return fFocus;
}


BRect
SectionEdit::SectionArea() const
{
	BRect sectionArea = Bounds().InsetByCopy(2, 2);
	sectionArea.right -= kArrowAreaWidth;
	return sectionArea;
}


void
SectionEdit::DrawBorder(const BRect& updateRect)
{
	BRect bounds(Bounds());
	bool showFocus = (IsFocus() && Window() && Window()->IsActive());

	be_control_look->DrawTextControlBorder(this, bounds, updateRect, ViewColor(),
		showFocus ? BControlLook::B_FOCUSED : 0);

	SetLowUIColor(B_DOCUMENT_BACKGROUND_COLOR);
	FillRect(bounds, B_SOLID_LOW);

	// draw up/down control

	bounds.left = bounds.right - kArrowAreaWidth;
	bounds.right = Bounds().right - 2;
	fUpRect.Set(bounds.left + 3, bounds.top + 2, bounds.right,
		bounds.bottom / 2.0);
	fDownRect = fUpRect.OffsetByCopy(0, fUpRect.Height() + 2);

	BPoint middle(floorf(fUpRect.left + fUpRect.Width() / 2),
		fUpRect.top + 1);
	BPoint left(fUpRect.left + 3, fUpRect.bottom - 1);
	BPoint right(left.x + 2 * (middle.x - left.x), fUpRect.bottom - 1);

	SetPenSize(2);

	if (updateRect.Intersects(fUpRect)) {
		FillRect(fUpRect, B_SOLID_LOW);
		BeginLineArray(2);
			AddLine(left, middle, HighColor());
			AddLine(middle, right, HighColor());
		EndLineArray();
	}
	if (updateRect.Intersects(fDownRect)) {
		middle.y = fDownRect.bottom - 1;
		left.y = right.y = fDownRect.top + 1;

		FillRect(fDownRect, B_SOLID_LOW);
		BeginLineArray(2);
			AddLine(left, middle, HighColor());
			AddLine(middle, right, HighColor());
		EndLineArray();
	}

	SetPenSize(1);
}


}	// namespace BPrivate