using System.Diagnostics.CodeAnalysis ;
using System.Globalization ;
using System.Text.RegularExpressions ;
using Connected.Annotations ;
using Connected.Extensions ;
using Connected.Utilities ;
using Microsoft.AspNetCore.Components ;
using Microsoft.AspNetCore.Components.Web ;
namespace Connected.Components ;
public partial class TimePicker : Picker < TimeSpan ? >
{
private const string format24Hours = "HH:mm" ;
private const string format12Hours = "hh:mm tt" ;
public TimePicker ( ) : base ( )
{
_timeFormat = format24Hours ;
SetConverter ( new LambdaConverter < TimeSpan ? , string > ( ( e ) = > OnSet ( e ) , ( e ) = > OnGet ( e ) ) ) ;
AdornmentIcon = Icons . Material . Filled . AccessTime ;
AdornmentAriaLabel = "Open Time Picker" ;
}
private string OnSet ( TimeSpan ? timespan )
{
if ( timespan = = null )
return string . Empty ;
var time = DateTime . Today . Add ( timespan . Value ) ;
return time . ToString ( _timeFormat , Culture ) ;
}
private TimeSpan ? OnGet ( string value )
{
if ( string . IsNullOrEmpty ( value ) )
return null ;
if ( DateTime . TryParseExact ( value , _timeFormat , Culture , DateTimeStyles . None , out var time ) )
{
return time . TimeOfDay ;
}
else
{
var m = Regex . Match ( value , "AM|PM" , RegexOptions . IgnoreCase ) ;
if ( m . Success )
{
return DateTime . ParseExact ( value , format12Hours , CultureInfo . InvariantCulture ) . TimeOfDay ;
}
else
{
return DateTime . ParseExact ( value , format24Hours , CultureInfo . InvariantCulture ) . TimeOfDay ;
}
}
}
private bool _amPm = false ;
private OpenTo _currentView ;
private string _timeFormat = string . Empty ;
internal TimeSpan ? TimeIntermediate { get ; private set ; }
/// <summary>
/// First view to show in the MudDatePicker.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public OpenTo OpenTo { get ; set ; } = OpenTo . Hours ;
/// <summary>
/// Choose the edition mode. By default, you can edit hours and minutes.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public TimeEditMode TimeEditMode { get ; set ; } = TimeEditMode . Normal ;
/// <summary>
/// Sets the amount of time in milliseconds to wait before closing the picker. This helps the user see that the time was selected before the popover disappears.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public int ClosingDelay { get ; set ; } = 200 ;
/// <summary>
/// If AutoClose is set to true and PickerActions are defined, the hour and the minutes can be defined without any action.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public bool AutoClose { get ; set ; }
/// <summary>
/// If true, sets 12 hour selection clock.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Behavior)]
public bool AmPm
{
get = > _amPm ;
set
{
if ( value = = _amPm )
return ;
_amPm = value ;
_timeFormat = AmPm ? format12Hours : format24Hours ;
Modified = true ;
SetTextAsync ( Converter . Convert ( _value ) , false ) . AndForget ( ) ;
}
}
/// <summary>
/// String Format for selected time view
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Behavior)]
public string TimeFormat
{
get = > _timeFormat ;
set
{
if ( _timeFormat = = value )
return ;
_timeFormat = value ;
Modified = true ;
SetTextAsync ( Converter . Convert ( _value ) , false ) . AndForget ( ) ;
}
}
/// <summary>
/// The currently selected time (two-way bindable). If null, then nothing was selected.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Data)]
public TimeSpan ? Time
{
get = > _value ;
set = > SetTimeAsync ( value , true ) . AndForget ( ) ;
}
protected async Task SetTimeAsync ( TimeSpan ? time , bool updateValue )
{
if ( _value ! = time )
{
TimeIntermediate = time ;
_value = time ;
if ( updateValue )
await SetTextAsync ( Converter . Convert ( _value ) , false ) ;
UpdateTimeSetFromTime ( ) ;
await TimeChanged . InvokeAsync ( _value ) ;
BeginValidate ( ) ;
FieldChanged ( _value ) ;
}
}
/// <summary>
/// Fired when the date changes.
/// </summary>
[Parameter] public EventCallback < TimeSpan ? > TimeChanged { get ; set ; }
protected override Task StringValueChanged ( string value )
{
Modified = true ;
// Update the time property (without updating back the Value property)
return SetTimeAsync ( Converter . ConvertBack ( value ) , false ) ;
}
//The last line cannot be tested
[ExcludeFromCodeCoverage]
protected override void OnPickerOpened ( )
{
base . OnPickerOpened ( ) ;
_currentView = TimeEditMode switch
{
TimeEditMode . Normal = > OpenTo ,
TimeEditMode . OnlyHours = > OpenTo . Hours ,
TimeEditMode . OnlyMinutes = > OpenTo . Minutes ,
_ = > _currentView
} ;
}
protected internal override void Submit ( )
{
if ( ReadOnly )
return ;
Time = TimeIntermediate ;
}
public override async void Clear ( bool close = true )
{
TimeIntermediate = null ;
await SetTimeAsync ( null , true ) ;
if ( AutoClose = = true )
{
Close ( false ) ;
}
}
private string GetHourString ( )
{
if ( TimeIntermediate = = null )
return "--" ;
var h = AmPm ? TimeIntermediate . Value . ToAmPmHour ( ) : TimeIntermediate . Value . Hours ;
return Math . Min ( 23 , Math . Max ( 0 , h ) ) . ToString ( CultureInfo . InvariantCulture ) ;
}
private string GetMinuteString ( )
{
if ( TimeIntermediate = = null )
return "--" ;
return $"{Math.Min(59, Math.Max(0, TimeIntermediate.Value.Minutes)):D2}" ;
}
private void UpdateTime ( )
{
TimeIntermediate = new TimeSpan ( _timeSet . Hour , _timeSet . Minute , 0 ) ;
if ( ( PickerVariant = = PickerVariant . Static & & PickerActions = = null ) | | ( PickerActions ! = null & & AutoClose ) )
{
Submit ( ) ;
}
}
private void OnHourClick ( )
{
_currentView = OpenTo . Hours ;
FocusAsync ( ) . AndForget ( ) ;
}
private void OnMinutesClick ( )
{
_currentView = OpenTo . Minutes ;
FocusAsync ( ) . AndForget ( ) ;
}
private void OnAmClicked ( )
{
_timeSet . Hour % = 12 ; // "12:-- am" is "00:--" in 24h
UpdateTime ( ) ;
FocusAsync ( ) . AndForget ( ) ;
}
private void OnPmClicked ( )
{
if ( _timeSet . Hour < = 12 ) // "12:-- pm" is "12:--" in 24h
_timeSet . Hour + = 12 ;
_timeSet . Hour % = 24 ;
UpdateTime ( ) ;
FocusAsync ( ) . AndForget ( ) ;
}
protected string ToolbarClass = >
new CssBuilder ( "mud-picker-timepicker-toolbar" )
. AddClass ( $"mud-picker-timepicker-toolbar-landscape" , Orientation = = Orientation . Landscape & & PickerVariant = = PickerVariant . Static )
. AddClass ( Class )
. Build ( ) ;
protected string HoursButtonClass = >
new CssBuilder ( "mud-timepicker-button" )
. AddClass ( $"mud-timepicker-toolbar-text" , _currentView = = OpenTo . Minutes )
. Build ( ) ;
protected string MinuteButtonClass = >
new CssBuilder ( "mud-timepicker-button" )
. AddClass ( $"mud-timepicker-toolbar-text" , _currentView = = OpenTo . Hours )
. Build ( ) ;
protected string AmButtonClass = >
new CssBuilder ( "mud-timepicker-button" )
. AddClass ( $"mud-timepicker-toolbar-text" , ! IsAm ) // gray it out
. Build ( ) ;
protected string PmButtonClass = >
new CssBuilder ( "mud-timepicker-button" )
. AddClass ( $"mud-timepicker-toolbar-text" , ! IsPm ) // gray it out
. Build ( ) ;
private string HourDialClass = >
new CssBuilder ( "mud-time-picker-hour" )
. AddClass ( $"mud-time-picker-dial" )
. AddClass ( $"mud-time-picker-dial-out" , _currentView ! = OpenTo . Hours )
. AddClass ( $"mud-time-picker-dial-hidden" , _currentView ! = OpenTo . Hours )
. Build ( ) ;
private string MinuteDialClass = >
new CssBuilder ( "mud-time-picker-minute" )
. AddClass ( $"mud-time-picker-dial" )
. AddClass ( $"mud-time-picker-dial-out" , _currentView ! = OpenTo . Minutes )
. AddClass ( $"mud-time-picker-dial-hidden" , _currentView ! = OpenTo . Minutes )
. Build ( ) ;
private bool IsAm = > _timeSet . Hour > = 00 & & _timeSet . Hour < 12 ; // am is 00:00 to 11:59
private bool IsPm = > _timeSet . Hour > = 12 & & _timeSet . Hour < 24 ; // pm is 12:00 to 23:59
private string GetClockPinColor ( )
{
return $"mud-picker-time-clock-pin mud-{Color.ToDescriptionString()}" ;
}
private string GetClockPointerColor ( )
{
if ( MouseDown )
return $"mud-picker-time-clock-pointer mud-{Color.ToDescriptionString()}" ;
else
return $"mud-picker-time-clock-pointer mud-picker-time-clock-pointer-animation mud-{Color.ToDescriptionString()}" ;
}
private string GetClockPointerThumbColor ( )
{
var deg = GetDeg ( ) ;
if ( deg % 30 = = 0 )
return $"mud-picker-time-clock-pointer-thumb mud-onclock-text mud-onclock-primary mud-{Color.ToDescriptionString()}" ;
else
return $"mud-picker-time-clock-pointer-thumb mud-onclock-minute mud-{Color.ToDescriptionString()}-text" ;
}
private string GetNumberColor ( int value )
{
if ( _currentView = = OpenTo . Hours )
{
var h = _timeSet . Hour ;
if ( AmPm )
{
h = _timeSet . Hour % 12 ;
if ( _timeSet . Hour % 12 = = 0 )
h = 12 ;
}
if ( h = = value )
return $"mud-clock-number mud-theme-{Color.ToDescriptionString()}" ;
}
else if ( _currentView = = OpenTo . Minutes & & _timeSet . Minute = = value )
{
return $"mud-clock-number mud-theme-{Color.ToDescriptionString()}" ;
}
return $"mud-clock-number" ;
}
private double GetDeg ( )
{
double deg = 0 ;
if ( _currentView = = OpenTo . Hours )
deg = ( _timeSet . Hour * 30 ) % 360 ;
if ( _currentView = = OpenTo . Minutes )
deg = ( _timeSet . Minute * 6 ) % 360 ;
return deg ;
}
private string GetTransform ( double angle , double radius , double offsetX , double offsetY )
{
angle = angle / 180 * Math . PI ;
var x = ( Math . Sin ( angle ) * radius + offsetX ) . ToString ( "F3" , CultureInfo . InvariantCulture ) ;
var y = ( ( Math . Cos ( angle ) + 1 ) * radius + offsetY ) . ToString ( "F3" , CultureInfo . InvariantCulture ) ;
return $"transform: translate({x}px, {y}px);" ;
}
private string GetPointerRotation ( )
{
double deg = 0 ;
if ( _currentView = = OpenTo . Hours )
deg = ( _timeSet . Hour * 30 ) % 360 ;
if ( _currentView = = OpenTo . Minutes )
deg = ( _timeSet . Minute * 6 ) % 360 ;
return $"rotateZ({deg}deg);" ;
}
private string GetPointerHeight ( )
{
var height = 40 ;
if ( _currentView = = OpenTo . Minutes )
height = 40 ;
if ( _currentView = = OpenTo . Hours )
{
if ( ! AmPm & & _timeSet . Hour > 0 & & _timeSet . Hour < 13 )
height = 26 ;
else
height = 40 ;
}
return $"{height}%;" ;
}
private readonly SetTime _timeSet = new ( ) ;
private int _initialHour ;
private int _initialMinute ;
protected override void OnInitialized ( )
{
base . OnInitialized ( ) ;
UpdateTimeSetFromTime ( ) ;
_currentView = OpenTo ;
_initialHour = _timeSet . Hour ;
_initialMinute = _timeSet . Minute ;
}
private void UpdateTimeSetFromTime ( )
{
if ( TimeIntermediate = = null )
{
_timeSet . Hour = 0 ;
_timeSet . Minute = 0 ;
return ;
}
_timeSet . Hour = TimeIntermediate . Value . Hours ;
_timeSet . Minute = TimeIntermediate . Value . Minutes ;
}
public bool MouseDown { get ; set ; }
/// <summary>
/// Sets Mouse Down bool to true if mouse is inside the clock mask.
/// </summary>
private void OnMouseDown ( MouseEventArgs e )
{
MouseDown = true ;
}
/// <summary>
/// Sets Mouse Down bool to false if mouse is inside the clock mask.
/// </summary>
private void OnMouseUp ( MouseEventArgs e )
{
if ( MouseDown & & _currentView = = OpenTo . Minutes & & _timeSet . Minute ! = _initialMinute | | _currentView = = OpenTo . Hours & & _timeSet . Hour ! = _initialHour & & TimeEditMode = = TimeEditMode . OnlyHours )
{
MouseDown = false ;
SubmitAndClose ( ) ;
}
MouseDown = false ;
if ( _currentView = = OpenTo . Hours & & _timeSet . Hour ! = _initialHour & & TimeEditMode = = TimeEditMode . Normal )
{
_currentView = OpenTo . Minutes ;
}
}
/// <summary>
/// If MouseDown is true enables "dragging" effect on the clock pin/stick.
/// </summary>
private void OnMouseOverHour ( int value )
{
if ( MouseDown )
{
_timeSet . Hour = value ;
UpdateTime ( ) ;
}
}
/// <summary>
/// On click for the hour "sticks", sets the hour.
/// </summary>
private void OnMouseClickHour ( int value )
{
var h = value ;
if ( AmPm )
{
if ( IsAm & & value = = 12 )
h = 0 ;
else if ( IsPm & & value < 12 )
h = value + 12 ;
}
_timeSet . Hour = h ;
UpdateTime ( ) ;
if ( TimeEditMode = = TimeEditMode . Normal )
{
_currentView = OpenTo . Minutes ;
}
else if ( TimeEditMode = = TimeEditMode . OnlyHours )
{
SubmitAndClose ( ) ;
}
}
/// <summary>
/// On mouse over for the minutes "sticks", sets the minute.
/// </summary>
private void OnMouseOverMinute ( int value )
{
if ( MouseDown )
{
_timeSet . Minute = value ;
UpdateTime ( ) ;
}
}
/// <summary>
/// On click for the minute "sticks", sets the minute.
/// </summary>
private void OnMouseClickMinute ( int value )
{
_timeSet . Minute = value ;
UpdateTime ( ) ;
SubmitAndClose ( ) ;
}
protected async void SubmitAndClose ( )
{
if ( PickerActions = = null | | AutoClose )
{
Submit ( ) ;
if ( PickerVariant ! = PickerVariant . Static )
{
await Task . Delay ( ClosingDelay ) ;
Close ( false ) ;
}
}
}
protected internal override void HandleKeyDown ( KeyboardEventArgs obj )
{
if ( Disabled | | ReadOnly )
return ;
base . HandleKeyDown ( obj ) ;
switch ( obj . Key )
{
case "ArrowRight" :
if ( IsOpen )
{
if ( obj . CtrlKey = = true )
{
ChangeHour ( 1 ) ;
}
else if ( obj . ShiftKey = = true )
{
if ( _timeSet . Minute > 55 )
{
ChangeHour ( 1 ) ;
}
ChangeMinute ( 5 ) ;
}
else
{
if ( _timeSet . Minute = = 59 )
{
ChangeHour ( 1 ) ;
}
ChangeMinute ( 1 ) ;
}
}
break ;
case "ArrowLeft" :
if ( IsOpen )
{
if ( obj . CtrlKey = = true )
{
ChangeHour ( - 1 ) ;
}
else if ( obj . ShiftKey = = true )
{
if ( _timeSet . Minute < 5 )
{
ChangeHour ( - 1 ) ;
}
ChangeMinute ( - 5 ) ;
}
else
{
if ( _timeSet . Minute = = 0 )
{
ChangeHour ( - 1 ) ;
}
ChangeMinute ( - 1 ) ;
}
}
break ;
case "ArrowUp" :
if ( IsOpen = = false & & Editable = = false )
{
IsOpen = true ;
}
else if ( obj . AltKey = = true )
{
IsOpen = false ;
}
else if ( obj . ShiftKey = = true )
{
ChangeHour ( 5 ) ;
}
else
{
ChangeHour ( 1 ) ;
}
break ;
case "ArrowDown" :
if ( IsOpen = = false & & Editable = = false )
{
IsOpen = true ;
}
else if ( obj . ShiftKey = = true )
{
ChangeHour ( - 5 ) ;
}
else
{
ChangeHour ( - 1 ) ;
}
break ;
case "Escape" :
ReturnTimeBackUp ( ) ;
break ;
case "Enter" :
case "NumpadEnter" :
if ( ! IsOpen )
{
Open ( ) ;
}
else
{
Submit ( ) ;
Close ( ) ;
_inputReference ? . SetText ( Text ) ;
}
break ;
case " " :
if ( ! Editable )
{
if ( ! IsOpen )
{
Open ( ) ;
}
else
{
Submit ( ) ;
Close ( ) ;
_inputReference ? . SetText ( Text ) ;
}
}
break ;
}
}
protected void ChangeMinute ( int val )
{
_currentView = OpenTo . Minutes ;
_timeSet . Minute = ( _timeSet . Minute + val + 60 ) % 60 ;
UpdateTime ( ) ;
}
protected void ChangeHour ( int val )
{
_currentView = OpenTo . Hours ;
_timeSet . Hour = ( _timeSet . Hour + val + 24 ) % 24 ;
UpdateTime ( ) ;
}
protected void ReturnTimeBackUp ( )
{
if ( Time = = null )
{
TimeIntermediate = null ;
}
else
{
_timeSet . Hour = Time . Value . Hours ;
_timeSet . Minute = Time . Value . Minutes ;
UpdateTime ( ) ;
}
}
private class SetTime
{
public int Hour { get ; set ; }
public int Minute { get ; set ; }
}
}