{**************************************************************************
 ***                     INPUT TYPES FOR DATE & TIME                    ***
 **************************************************************************}
{$I compflgs}
unit InpDnt;

INTERFACE

uses
{$IFDEF WINDOWS}
	wui,	{windows}
{$ELSE}
	tuimsgs, tuiedit, dialogs, tui, views, {text}
{$ENDIF}
		help,
		dattime, objects, drivers;

const
	CCalendarView = #6#7#8#9; {header, normal text, today, selected day}

{========== MINI-Calendar VIEW ===========}
{nicked from calendar.pas in the tv examples and adapted}
type

 PCalendarView = ^TCalendarView;
 TCalendarView = object(TView)
	 Selected : TDate;
	 Acceptor : PView;
	 constructor Init(Bounds: TRect);
	 procedure HandleEvent(var Event: TEvent); virtual;
	 procedure Draw; virtual;
		function GetPalette: PPalette; virtual;
 end;

 PCalendarWindow = ^TCalendarWindow;
 TCalendarWindow = object(TWindow)
		View : PCalendarView;
		constructor Init(const X,Y : byte);
		function GetPalette : PPalette; virtual;{}
 end;


{=========== INPUT DATE ==============}
type
	PAgeIndicator = ^TAgeIndicator;
	TAgeIndicator = object(TStaticText)
		procedure SetAge(Date : TDate);
	end;

	PInputDate = ^TInputDate;
	TInputDate = object(TInputNum)
		AllowVague : boolean; {allows unspecified day/month}
		Calendar : PCalendarWindow;
		AgeIndicator : PAgeIndicator;
		constructor Init(var R : TRect);
		procedure HandleEvent(var Event : TEvent); virtual;
		function Valid (Command : Word) : boolean; virtual;
		procedure ForceLink; virtual;
		procedure SetData(var Rec); virtual;
		procedure GetData(var Rec); virtual;
		function DataSize : word; virtual;
	end;

type
	PInputTime = ^TInputTime;
	TInputTime = object(TInputNum)
		itType : byte;
		constructor Init(var R : TRect; const NitType : byte);
		procedure HandleEvent(var Event : TEVent); virtual;
		procedure GetData(var Rec); virtual;
		procedure SetData(var Rec); virtual;
		function Valid (Command : Word) : boolean; virtual;
		function DataSize : word; virtual;
	end;

	{every time it's drawn it picks up latest time from time}
	PTimerView = ^TTImerView;
	TTimerView = object(TStaticText)
		Time : PTime;
		constructor Init(Bounds : TRect; NTime : PTime);
		procedure Draw; virtual;
	end;

IMPLEMENTATION

uses dos, app, tasks, global, minilib;

{********************************
 ***    Calendar VIEW         ***
 ********************************}

{ TCalendarWindow }
constructor TCalendarWindow.Init;
var
	R:TRect;
begin
	R.Assign(X, Y, 22+X, 10+Y);
	inherited Init(R, 'Calendar', 0);
	Flags := Flags and not (wfZoom + wfGrow);    { Not resizeable }
	GrowMode :=0;
	Palette := dpBlueDialog;   {utility window - see palette.pas}

	GetExtent(R);
	R.Grow(-1, -1);
	View := New(PCalendarView, Init(R));
	Insert(View);

	HelpCtx := hcCalendar;
end;

{Dialog-style palette}
function TCalendarWindow.GetPalette: PPalette;
const
	P: array[dpBlueDialog..dpGrayDialog] of string[Length(CBlueDialog)] =
		(CBlueDialog, CCyanDialog, CGrayDialog);
begin
	GetPalette := @P[Palette];
end;



{ TCalendarView }
constructor TCalendarView.Init(Bounds: TRect);
begin
	inherited Init(Bounds);
	Options := Options or ofSelectable or ofFirstClick;
	EventMask := EventMask or evMouseAuto or evBroadCast and not evMouseMove;

	Acceptor := nil;

	Today.SetToToday;
	Selected.SetToToday;

	DrawView;
end;

procedure TCalendarView.Draw;
const
	Width = 20;
var
	Line, Col, CurDays: Integer;
	B: array[0..Width] of Word;
	Color, TodayColor, CurrentCOlor, NormalColor: Byte;

begin
	{uses labels colours}
	if GetState(sfSelected) then begin
		NormalColor := GetColor(2);
		CurrentColor := GetColor(3);
	end else begin
		NormalColor := GetColor(3);
		CurrentColor := GetColor(2);
	end;
	TodayColor := GetCOlor(4);

	{set up top line.  Unlike original, up goes back in time, down goes
	forwards (to match diary, and in fact the calendar itself which has
	later at the bottom....)}
	MoveChar(B, ' ', GetColor(1), Width);
	MoveStr(B, N2Str(Selected.Year)+' '+Setlength(MonthName[Selected.Month],9) + ' '#31'  '#30, GetColor(4));
	WriteLine(0, 0, Width, 1, B);

	{set up days of week line}
	MoveChar(B, ' ', GetColor(1), Width);
	MoveStr(B, 'Su Mo Tu We Th Fr Sa', GetColor(1));
	WriteLine(0, 1, Width, 1, B);

	CurDays := 1 - DayOfWeek(1, Selected.Month, Selected.Year); {1st day of week
																				starts -ve as the first pos (ie col 1 line 1) may be before the 1st...}
	for Line := 1 to 6 do begin
		MoveChar(B, ' ', NormalColor, Width);
		for Col := 0 to 6 do begin {days of week}
			if (CurDays < 1) or (CurDays > DaysInMonth(Selected.Month, Selected.Year)) then
				MoveStr(B[Col * 3], '   ', NormalColor)
			else begin
				{mark today, and the currently selected day}
				Color := NormalColor;
				if (Selected.Year = Today.Year) and (Selected.Month = Today.Month) and (CurDays = Today.Day) then Color := TodayColor;
				if (CurDays = Selected.Day) then Color := CurrentColor;
				MoveStr(B[Col * 3], Right(' '+N2Str(CurDays),2), Color);
			end;
			Inc(CurDays);
		end;
		WriteLine(0, Line + 1, Width, 1, B);
	end;
end;

procedure TCalendarView.HandleEvent(var Event: TEvent);
var
	Point:TPoint;
	P : pointer;
	OldSelected : TDate;
	MD,D : byte;

begin
	inherited HandleEvent(Event);

	{redraw for focussed colour}
	if (Event.Command = cmReleasedFocus) or (Event.Command = cmReceivedFocus) then DrawView;{}

	if (State and sfSelected)<>0 then begin

		if (Event.What = evKeyDown) and (Event.KeyCode = kbAccept) then begin
			P :=Message(Acceptor, evCommand, cmUpdateFromLink, @Self);
			{Close list if message handled}
			if P <> nil then StartEvent(evCommand, cmClose); {ie event has been dealt with}
			ClearEvent(Event);
		end;

		if (Event.What = evKeyDown) and (Event.KeyCode = kbESC) then
			StartEvent(evCommand, cmClose);

	end;

	{---- moving up & down ------}
	OldSelected.SetToDate(Selected);

	{keyboard movements}
	if (Event.What=evKeyDown) then begin
		if Event.KeyCode = kbRight then Selected.AddDay(True);
		if Event.KeyCode = kbLeft then Selected.AddDay(False);
		if Event.KeyCode = kbUp then Selected.AddWeek(False);
		if Event.KeyCode = kbDown then Selected.AddWeek(True);
		if Event.KeyCode = kbPgDn then Selected.AddMonth(True);
		if Event.KeyCode = kbPgUp then Selected.AddMonth(False);{}

		{Kameleon stds}
		if Event.CharCode = '+' then Selected.AddDay(True);
		if Event.CharCode = '-' then Selected.AddDay(False);
		if Event.CharCode = 'w' then Selected.AddWeek(True);
		if Event.CharCode = 'W' then Selected.AddWeek(False);
		if Event.CharCode = 'm' then Selected.AddMonth(True);
		if Event.CharCode = 'M' then Selected.AddMonth(False);
		if Event.CharCode = 'y' then Selected.AddYear(True);
		if Event.CharCode = 'Y' then Selected.AddYear(False);

		if Event.CharCode = 't' then Selected.SetToDate(Today);
		if Event.CharCode = 'T' then Selected.SetToDate(Today);

		DrawView;
	end;

	{mouse movements}
	if Event.What and (evMouseDown + evMouseAuto) <> 0 then begin
		MakeLocal(Event.Where, Point);
		if Point.Y = 0 then begin
			{fist line - page up/down}
			if (Point.X = 15) then begin
				{clicked on page down}
				Selected.AddMOnth(True); {add month}
				DrawView;
			end;
			if (Point.X = 18) then begin
				{clicked on page up}
				Selected.AddMonth(False);
				DrawView;
			end;
		end;
		if Point.Y>1 then begin
			{o/w try & figure out which date has been clicked on, and move to that}
			MD := (Point.X div 3) + (Point.Y-2)*7;
			D := DayOfWeek(1, Selected.Month, Selected.Year);
			if MD>=D then begin
				Selected.SetToNum(MD-D+1, Selected.Month, Selected.Year);
				DrawView;
			end;
		end;

	end;

	if (OldSelected.Day<>Selected.Day) or (OldSelected.Month<>Selected.Month) or
			(OldSelected.Year<>Selected.Year) then begin
		Drawview;
		Message(Owner, evBroadCast, cmDateChanged, @Selected); {notify other views}
	end;

	{for diary - picking up changed message}
	if (Event.What = evBroadCast) and (Event.COmmand = cmDateChanged) and
		(PDate(Event.InfoPtr)^.Days<>Selected.Days) then begin
			Selected.SetToDate(PDate(Event.InfoPtr)^);
			DrawView;
	end;

end;


function TCalendarView.GetPalette: PPalette;
const
	P: String[Length(CCalendarView)] = CCalendarView;
begin
	GetPalette := @P;
end;

{**************************************************************************
 ***                      DATE (4 dig year)                             ***
 **************************************************************************}


	constructor TInputDate.Init(var R:Trect);
	begin
		inherited Init(R, 10);
		Blockcursor;        {Force non-input}
		Data^ := '  -  -    ';
		Calendar := nil;
		AllowVague := False;
		AgeIndicator := nil;
		DecPlaces := 0; {don't allow full stops, etc}
	end;

	{=== HANDLE EVENT ==========================================}
	procedure TInputDate.HandleEvent(var Event : TEvent);
	var	Date : TDate;

	begin
		BlockCursor;
{		if (CurPos<6) and (GetState(sfCursorIns)) then begin BlockCursor; DrawView; end;    {JIC - force typeover before year}

		if (Event.what = evCommand) and (Event.Command = cmUpdateFromLink)
			and (Event.InfoPtr=pointer(Calendar^.View)) and (Calendar<>nil) then begin
			{updated from Calendar}
			Date.SetToDate(PCalendarView(Event.InfoPtr)^.Selected);
			if DatErr=0 then begin
				Data^ := Date.Digit10;
				ForceLink;
				ClearEvent(Event);
			end;
		end;

		if (Event.What = evKeyBoard) then begin

			if Event.KeyCOde = kbUp then begin
				{produce Calendar!}
				New(Calendar, init(10,5));
				Calendar^.View^.Acceptor := @Self;
				Calendar^.View^.Selected.SetToStr(Data^);
				if Calendar^.View^.Selected.Blank then Calendar^.View^.Selected.SetToToday;
				Desktop^.ExecView(Calendar);
				dispose(Calendar, done);
				ClearEvent(Event);
			end;

			if Event.CharCode = #39 then begin
				{shorthand typing in year}
				Date.SetToToday;
				Data^ :=Copy(Data^,1,6) + Copy(N2Str(Date.Year),1,2)+'  '; {day & month, today's year, space}
				CurPos := 8;
				drawView;
				CLearEvent(Event);
			end;

			{Special time-saving keys}
			{TOMORROW/YESTERDAY/LAST MONTH/ETC}
			if Pos(Event.CharCode, 'tT+-wWmMyY') > 0 then begin
				{before adding year, etc, test to see if date valid}
				Date.SetToStr(Data^);
				if DatErr = 0 then
					case Event.CharCode of
						'+' : Date.AddDay(True);
						'-' : Date.AddDay(False);
						'w' : Date.AddWeek(True);
						'W' : Date.AddWeek(False);
						'm' : Date.AddMonth(True);
						'M' : Date.AddMonth(False);
						'y' : Date.AddYear(True);
						'Y' : Date.AddYear(False);
					end;

				case Event.CharCode of
					't' : Date.SetToToday;
					'T' : Date.SetToToday;
				end;

				Data^ := Date.Digit10;
				DrawView;
				ClearEvent(Event);
				SetChanged;
				ForceLink;
			end;

			if (Event.KeyCode = kbDel) and (CurPos=0) then begin
				Date.Clear;  {Blank out whole date}
				Data^ := Date.Digit10;
				DrawView;
				SetChanged;
				ForceLink;
				ClearEvent(Event);
			end;

{			if (Event.CharCode = '/') then
				if (CurPos<2) then begin CurPos := 3; Draw; end
{				else if CurPos<5 then begin CurPos := 6; Draw; end;{}


			{Trap for insert request - not allowed}
			if Event.KeyCode = kbIns then ClearEvent(Event);

			{Trap for deletions}
			if (Event.KeyCode=kbBack) then begin
				if (CurPos = 3) or (CurPos = 6) then CurPos := CurPos -1;  {Skip hyphen}
				if CurPos>0 then CurPos := CurPos -1;                {Move left one}
				Data^[CurPos+1] :=' ';                                 {Create space}
				ClearEvent(EVent);                                   {Handled}
				DrawView;                                                {Update display}
			end;

			if (Event.KeyCode=kbDel) then begin
				Data^[CurPos+1] :=' ';                                 {Create space at position}
				ClearEvent(Event);
				DrawView;                                                {Update display}
			end;

			{--- Left Skip over hyphen ----}
			if (Event.KeyCode = kbLeft) and ((CurPos = 3) or (CurPos = 6)) then CurPos := CurPos -1;


			{--- Right skip over first two digits of year if '9' or '0' pressed at CurPos 6}
			{or 1 is pressed and today is >year 2007.... hopeful or what?!}
			if (CurPos=6) and
				((Event.CharCode='9') or (Event.CharCode='8') or (Event.CharCode='0') or
				((Event.CharCode='1') and (Today.Year>2007))) then begin
				inc(CurPos, 2); {move to last two digits}
				DrawView;
			end;

			if (Event.CharCode='.') then begin
				{needs to translate presses 1.1.96 as well as 20.11.1997}
				{so ignore pos 0,3,6}
				case CurPos of
					1	: begin
								Data^[2] := Data^[1];
								Data^[1] := '0';
								CurPos := 3;
							end;
					4 : begin
								Data^[5] := Data^[4];
								Data^[4] := '0';
								if Maxlen>5 then CurPos := 6;
							end;
				end;
				DrawView;
				ClearEvent(Event);
			end;


		end;


		inherited HandleEvent(Event);  {Numerical input only}


		{--- Right skip over hyphen ----}
		if (CurPos = 2) or (CurPos = 5) then begin
			inc(CurPos);
			DrawView;
		end;
	end;

	{=== VALIDATION ==============================================}
	function TInputDate.Valid(Command: Word) : boolean;
	var
		TestDate : TDate;
		V : boolean;

	begin
		V := TINputELine.Valid(Command);{skip inherited numeric one due to - - chars}

		if V and DoValidFor(Command) then begin{(Command<>cmValid) and (Command <> cmClose) and (Command <>cmCancel) then begin{}
			GetData(TestDate);
			if (DatErr>0) then begin {Allow blank date}
				V := False;
				if Command<>cmForceLink then begin
					WrongFldBleep;
					Case (Daterr and $0F) of
						$01 : CurPos := 0; {Day}
						$02 : CurPos := 3; {Month}
						$03 : CurPos := 6; {Year}
					else
						CurPos := 0;  {General}
					end;
					DrawView; {Put cursor in right place}

					InputWarning(DateErrorMsg(DatErr), hcIWDateMsg);
				end;
			end;
			if TestDate.Blank then
				{test for mustinput}
				if MustInput or (MustInputToClose and (Command=cmOK)) then begin
					V := False;
					{need to do this as editbox.valid does not do it until after all valids done,
					BUT must not do if just mustinput as stack overflow occurs if doing
					a focus after box creation}
					if MustInputToClose then Focus;
					InputWarning('Date Required', hcIWEntryReqMsg);
				end;

		end;

		Valid := V;
	end;

	procedure TInputDate.ForceLink;
	var Date : TDate;
	begin
		inherited ForceLink;
		if AgeIndicator<>nil then begin
			GetData(Date);
			AgeIndicator^.SetAge(Date);
		end;
	end;

	{=== SET DATA ==================================================}
	procedure TInputDate.SetData(var Rec);
	begin
		Data^ := TDate(Rec).Digit10;
		if AgeIndicator<>nil then AgeIndicator^.SetAge(TDate(Rec));
	end;

	{=== GET DATA ==================================================}
	procedure TInputDate.GetData(var Rec);
	begin
		TDate(Rec).SetToStr(Data^);  {Assume OK - been validated}
	end;

	{--- DATA SIZE ---------------------}
	function TInputDate.DataSize : word;
	begin DataSize := sizeof(TDate); end;



{***********************************************
 ***         AGE INDICATOR                   ***
 ***********************************************}
{Meant to sit next to a date, indicating num years between that
date and today}

procedure TAgeIndicator.SetAge(Date : TDate);
var M : word;
begin
	DisposeStr(Text);
	if Date.Blank then
		Text := NewStr(space(Size.X)) {clear}
	else begin
		M := Date.AgeMonths;
		if M>0 then
			if M>=12 then
				Text := NewStr(N2Str(M div 12)+'y')
			else
				Text := NewStr(N2Str(M)+'m')
		else
			Text := NewStr(Space(Size.X)); {clear}
	end;
	DrawView;
end;

{********************************************
 ***             TIME (24hr & mins)       ***
 ********************************************}
{TODO - allow blanking of time}


constructor TInputTime.Init;
begin
	case (NitType and $0F) of
		itHMS : inherited Init(R,8);
		itHMS1 : inherited Init(R,11);
	else
		inherited Init(R, 5);
	end;
	itType := NitType;
	Blockcursor;
end;

procedure TInputTime.HandleEvent(var Event : TEvent);
var Time : TTime;
begin
	if (Event.What = evKeyBoard) then begin

		{Special time-saving keys}
		if Pos(Event.CharCode, 'nN+-hH') > 0 then begin
			case Event.CharCode of
				'n' : begin time.SetToNow; TimErr := 0; end;
				'N' : begin Time.SetToNow; TimErr := 0; end;
			else
				{before adding mins, etc, test to see if time valid}
				Time.SetToStr(Data^);
				if TimErr = 0 then
					case Event.CharCode of
						'+' : Time.SetToSecs(Time.Secs+60);
						'-' : if Time.Secs>60 then time.SetToSecs(Time.Secs-60);
						'h' : if Time.Secs>3600 then Time.SetToSecs(Time.Secs-3600);
						'H' : Time.SetToSecs(Time.Secs+3600);
					end
				else
					Valid(cmOK); {error}
			end;

			if TimErr=0 then SetData(Time);
			DrawView;
			ClearEvent(Event);
		end;

		{Trap for insert request - not allowed}
		if Event.KeyCode = kbIns then ClearEvent(Event);

		BlockCursor;     {JIC - force typeover}

		{Trap for deletions}
		case Event.KeyCode of
			kbBack : begin
				if (CurPos = 3) or (CurPos=6) then CurPos := CurPos -1;  {Skip hyphen}
				if CurPos>0 then CurPos := CurPos -1;                {Move left one}
				Data^[CurPos+1] :=' ';                                 {Create space}
				ClearEvent(EVent);                                   {Handled}
				DrawView;                                                {Update display}
			end;

			kbDel : begin
				Data^[CurPos+1] :=' ';                                 {Create space at position}
				ClearEvent(Event);
				DrawView;                                                {Update display}
			end;

			{--- Left Skip over colon ----}
			kbLeft : if ((CurPos = 3) or (CurPos=6)) then dec(CurPos);

			kbUp	 : ClearEvent(Event); {don't do normal up - gets calculator}
		end;


		{--- Check for ":" ----}
		if (Event.CharCode = ':') or (Event.CharCode='.') then begin
			case CurPos of
				0,1 : CurPos := 3;
				3,4 : if Maxlen>5 then CurPos := 6;
				6,7 : if Maxlen>8 then CurPos := 9;
			end;
			DrawView;
			ClearEvent(Event);
		end; {skip to right of :}

	end;

	{--- Check characters ----}
	inherited HandleEvent(Event);

	{--- Right skip over hyphen ----}
	if ((CurPos = 2) or (CurPOs=5) or (CurPos=8)) and (CurPos<Maxlen) then begin
			CurPos := CurPos +1; DrawView;
	end;

end;

{--- Check at end of input to see if OK ----}
function TInputTime.Valid(Command: Word) : boolean;
var
	S : string; {input string}
	TestTime : TTime;
	V : boolean;

begin
	V := TInputELine.Valid(Command); {don't want to do inputnum as it falls over colon}

	if V and DoValidFor(Command) then begin

		GetData(TestTime);
		if TimErr>0 then begin
			V := False;
			if Command<>cmForceLink then begin
				Case (Timerr and $0F) of
					$01 : begin
						{Hour}
						CurPos := 0;  {Hour}
						if (TimErr and $F0)>0 then InputWarning('Hour too large',hcIWTimeMsg)
																	else InputWarning('Hour invalid',hcIWTimeMsg);
					end;
					$02 : begin
						{Mins}
						CurPos := 3;
						if (TimErr and $F0)>0 then InputWarning('Minutes too large',hcIWTimeMsg)
																	else InputWarning('Minutes invalid',hcIWTimeMsg);
					end;
					$03 : begin
						{Secs}
						CurPos := 6;
						if (TimErr and $F0)>0 then InputWarning('Seconds too large',hcIWTimeMsg)
																	else InputWarning('Seconds invalid',hcIWTimeMsg);
					end;
				else
					InputWarning('Invalid Time',hcIWTimeMsg);
				end;
				DrawView;
			end;

			{-- Test for valid *time* rather than time *period* --}
			if (itType and itTime)>0 then
				if TestTime.Hour>23 then begin
					V := False;
					if Command<>cmForceLink then InputWarning('Incorrect Time',hcIWTimeMsg);
				end;

		end;
		if (Command<>cmForceLink) and TestTime.Blank then
			{test for mustinput}
			if MustInput or (MustInputToClose and (Command=cmOK)) then begin
				V := False;
				Focus; {need to do this as editbox.valid does not do it until after all valids done}
				InputWarning('Time Required',hcIWEntryReqMsg);
			end;

	end;

	Valid := V;
end;

{=== SET DATA ==================================================}
procedure TInputTime.SetData(var Rec);
begin
	if (itType and $F0 = itZeroBlank) and (TTime(rec).Blank) then
		Data^ :=Copy('  :  :  :  ',1,Maxlen)
	else
		case (itType and $0f) of
			itHMS : Data^ := TTime(rec).Digit8;
			itHMS1 : Data^ := TTime(rec).Digit11;
		else
			Data^ := TTime(rec).Digit5;
		end;

end;

{=== GET DATA ==================================================}
procedure TInputTime.GetData(var Rec);
begin
	TTime(Rec).SetToStr(Data^);
end;

{--- DATA SIZE ---------------------}
function TInputTime.DataSize : word;
begin DataSize := sizeof(TTime); end;


{========== TIMER VIEW =====================}
constructor TTimerView.Init;
begin
	inherited Init(Bounds, NTime^.Digit8);
	Time := NTime;
end;

procedure TTimerView.Draw;
begin
	Text^ := Time^.Digit8;
	inherited Draw;
end;

procedure RunCalendar; far;
begin
	Desktop^.Insert(New(PCalendarWindow, init(1,1)));
end;

begin
	RegisterTask(DesktopTasks, cmCalendar, @RunCalendar);
end.
