/************************************************************************************* Extended WPF Toolkit Copyright (C) 2007-2013 Xceed Software Inc. This program is provided to you under the terms of the Microsoft Public License (Ms-PL) as published at http://wpftoolkit.codeplex.com/license For more features, controls, and fast professional support, pick up the Plus Edition at http://xceed.com/wpf_toolkit Stay informed: follow @datagrid on Twitter or Like http://facebook.com/datagrids ***********************************************************************************/ using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Threading; using Xceed.Wpf.Toolkit.Core.Utilities; namespace Xceed.Wpf.Toolkit { [TemplatePart( Name = PART_CalculatorButtonPanel, Type = typeof( ContentControl ) )] public class Calculator : Control { private const string PART_CalculatorButtonPanel = "PART_CalculatorButtonPanel"; #region Members private ContentControl _buttonPanel; private bool _showNewNumber = true; private decimal _previousValue; private Operation _lastOperation = Operation.None; private readonly Dictionary _timers = new Dictionary(); #endregion //Members #region Enumerations public enum CalculatorButtonType { Add, Back, Cancel, Clear, Decimal, Divide, Eight, Equal, Five, Four, Fraction, MAdd, MC, MR, MS, MSub, Multiply, Negate, Nine, None, One, Percent, Seven, Six, Sqrt, Subtract, Three, Two, Zero } public enum Operation { Add, Subtract, Divide, Multiply, Percent, Sqrt, Fraction, None, Clear, Negate } #endregion //Enumerations #region Properties #region CalculatorButtonPanelTemplate public static readonly DependencyProperty CalculatorButtonPanelTemplateProperty = DependencyProperty.Register( "CalculatorButtonPanelTemplate" , typeof( ControlTemplate ), typeof( Calculator ), new UIPropertyMetadata( null ) ); public ControlTemplate CalculatorButtonPanelTemplate { get { return (ControlTemplate)GetValue( CalculatorButtonPanelTemplateProperty ); } set { SetValue( CalculatorButtonPanelTemplateProperty, value ); } } #endregion //CalculatorButtonPanelTemplate #region CalculatorButtonType public static readonly DependencyProperty CalculatorButtonTypeProperty = DependencyProperty.RegisterAttached( "CalculatorButtonType", typeof( CalculatorButtonType ), typeof( Calculator ), new UIPropertyMetadata( CalculatorButtonType.None, OnCalculatorButtonTypeChanged ) ); public static CalculatorButtonType GetCalculatorButtonType( DependencyObject target ) { return ( CalculatorButtonType )target.GetValue( CalculatorButtonTypeProperty ); } public static void SetCalculatorButtonType( DependencyObject target, CalculatorButtonType value ) { target.SetValue( CalculatorButtonTypeProperty, value ); } private static void OnCalculatorButtonTypeChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { OnCalculatorButtonTypeChanged( o, ( CalculatorButtonType )e.OldValue, ( CalculatorButtonType )e.NewValue ); } private static void OnCalculatorButtonTypeChanged( DependencyObject o, CalculatorButtonType oldValue, CalculatorButtonType newValue ) { Button button = o as Button; button.CommandParameter = newValue; if( button.Content == null ) { button.Content = CalculatorUtilities.GetCalculatorButtonContent( newValue ); } } #endregion //CalculatorButtonType #region DisplayText public static readonly DependencyProperty DisplayTextProperty = DependencyProperty.Register( "DisplayText", typeof( string ), typeof( Calculator ), new UIPropertyMetadata( "0", OnDisplayTextChanged ) ); public string DisplayText { get { return ( string )GetValue( DisplayTextProperty ); } set { SetValue( DisplayTextProperty, value ); } } private static void OnDisplayTextChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { Calculator calculator = o as Calculator; if( calculator != null ) calculator.OnDisplayTextChanged( ( string )e.OldValue, ( string )e.NewValue ); } protected virtual void OnDisplayTextChanged( string oldValue, string newValue ) { // TODO: Add your property changed side-effects. Descendants can override as well. } #endregion //DisplayText #region Memory public static readonly DependencyProperty MemoryProperty = DependencyProperty.Register( "Memory", typeof( decimal ), typeof( Calculator ), new UIPropertyMetadata( default( decimal ) ) ); public decimal Memory { get { return ( decimal )GetValue( MemoryProperty ); } set { SetValue( MemoryProperty, value ); } } #endregion //Memory #region Precision public static readonly DependencyProperty PrecisionProperty = DependencyProperty.Register( "Precision", typeof( int ), typeof( Calculator ), new UIPropertyMetadata( 6 ) ); public int Precision { get { return ( int )GetValue( PrecisionProperty ); } set { SetValue( PrecisionProperty, value ); } } #endregion //Precision #region Value public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof( decimal? ), typeof( Calculator ), new FrameworkPropertyMetadata( default( decimal ), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged ) ); public decimal? Value { get { return ( decimal? )GetValue( ValueProperty ); } set { SetValue( ValueProperty, value ); } } private static void OnValueChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { Calculator calculator = o as Calculator; if( calculator != null ) calculator.OnValueChanged( ( decimal? )e.OldValue, ( decimal? )e.NewValue ); } protected virtual void OnValueChanged( decimal? oldValue, decimal? newValue ) { SetDisplayText( newValue ); RoutedPropertyChangedEventArgs args = new RoutedPropertyChangedEventArgs( oldValue, newValue ); args.RoutedEvent = ValueChangedEvent; RaiseEvent( args ); } #endregion //Value #endregion //Properties #region Constructors static Calculator() { DefaultStyleKeyProperty.OverrideMetadata( typeof( Calculator ), new FrameworkPropertyMetadata( typeof( Calculator ) ) ); } public Calculator() { CommandBindings.Add( new CommandBinding( CalculatorCommands.CalculatorButtonClick, ExecuteCalculatorButtonClick ) ); AddHandler( MouseDownEvent, new MouseButtonEventHandler( Calculator_OnMouseDown ), true ); } #endregion //Constructors #region Base Class Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); _buttonPanel = GetTemplateChild( PART_CalculatorButtonPanel ) as ContentControl; } protected override void OnTextInput( TextCompositionEventArgs e ) { var buttonType = CalculatorUtilities.GetCalculatorButtonTypeFromText( e.Text ); if( buttonType != CalculatorButtonType.None ) { SimulateCalculatorButtonClick( buttonType ); ProcessCalculatorButton( buttonType ); } } #endregion //Base Class Overrides #region Event Handlers private void Calculator_OnMouseDown( object sender, MouseButtonEventArgs e ) { if( !IsFocused ) { Focus(); e.Handled = true; } } void Timer_Tick( object sender, EventArgs e ) { DispatcherTimer timer = ( DispatcherTimer )sender; timer.Stop(); timer.Tick -= Timer_Tick; if( _timers.ContainsValue( timer ) ) { var button = _timers.Where( x => x.Value == timer ).Select( x => x.Key ).FirstOrDefault(); if( button != null ) { VisualStateManager.GoToState( button, button.IsMouseOver ? "MouseOver" : "Normal", true ); _timers.Remove( button ); } } } #endregion //Event Handlers #region Methods internal void InitializeToValue(decimal? value) { _previousValue = 0; _lastOperation = Operation.None; _showNewNumber = true; Value = value; // Since the display text may be out of sync // with "Value", this call will force the // text update if Value was already equal to // the value parameter. this.SetDisplayText( value ); } private void Calculate() { if( _lastOperation == Operation.None ) return; try { Value = Decimal.Round( CalculateValue( _lastOperation ), Precision ); SetDisplayText( Value ); //Set DisplayText even when Value doesn't change } catch { Value = null; DisplayText = "ERROR"; } } private void SetDisplayText( decimal? newValue ) { if( newValue.HasValue && ( newValue.Value != 0 ) ) DisplayText = newValue.ToString(); else DisplayText = "0"; } private void Calculate( Operation newOperation ) { if( !_showNewNumber ) Calculate(); _lastOperation = newOperation; } private void Calculate( Operation currentOperation, Operation newOperation ) { _lastOperation = currentOperation; Calculate(); _lastOperation = newOperation; } private decimal CalculateValue( Operation operation ) { decimal newValue = decimal.Zero; decimal currentValue = CalculatorUtilities.ParseDecimal( DisplayText ); switch( operation ) { case Operation.Add: newValue = CalculatorUtilities.Add( _previousValue, currentValue ); break; case Operation.Subtract: newValue = CalculatorUtilities.Subtract( _previousValue, currentValue ); break; case Operation.Multiply: newValue = CalculatorUtilities.Multiply( _previousValue, currentValue ); break; case Operation.Divide: newValue = CalculatorUtilities.Divide( _previousValue, currentValue ); break; //case Operation.Percent: // newValue = CalculatorUtilities.Percent(_previousValue, currentValue); // break; case Operation.Sqrt: newValue = CalculatorUtilities.SquareRoot( currentValue ); break; case Operation.Fraction: newValue = CalculatorUtilities.Fraction( currentValue ); break; case Operation.Negate: newValue = CalculatorUtilities.Negate( currentValue ); break; default: newValue = decimal.Zero; break; } return newValue; } void ProcessBackKey() { string displayText; if( DisplayText.Length > 1 && !( DisplayText.Length == 2 && DisplayText[ 0 ] == '-' ) ) { displayText = DisplayText.Remove( DisplayText.Length - 1, 1 ); } else { displayText = "0"; _showNewNumber = true; } DisplayText = displayText; } private void ProcessCalculatorButton( CalculatorButtonType buttonType ) { if( CalculatorUtilities.IsDigit( buttonType ) ) ProcessDigitKey( buttonType ); else if( ( CalculatorUtilities.IsMemory( buttonType ) ) ) ProcessMemoryKey( buttonType ); else ProcessOperationKey( buttonType ); } private void ProcessDigitKey( CalculatorButtonType buttonType ) { if( _showNewNumber ) DisplayText = CalculatorUtilities.GetCalculatorButtonContent( buttonType ); else DisplayText += CalculatorUtilities.GetCalculatorButtonContent( buttonType ); _showNewNumber = false; } private void ProcessMemoryKey( Calculator.CalculatorButtonType buttonType ) { decimal currentValue = CalculatorUtilities.ParseDecimal( DisplayText ); _showNewNumber = true; switch( buttonType ) { case Calculator.CalculatorButtonType.MAdd: Memory += currentValue; break; case Calculator.CalculatorButtonType.MC: Memory = decimal.Zero; break; case Calculator.CalculatorButtonType.MR: DisplayText = Memory.ToString(); _showNewNumber = false; break; case Calculator.CalculatorButtonType.MS: Memory = currentValue; break; case Calculator.CalculatorButtonType.MSub: Memory -= currentValue; break; default: break; } } private void ProcessOperationKey( CalculatorButtonType buttonType ) { switch( buttonType ) { case CalculatorButtonType.Add: Calculate( Operation.Add ); break; case CalculatorButtonType.Subtract: Calculate( Operation.Subtract ); break; case CalculatorButtonType.Multiply: Calculate( Operation.Multiply ); break; case CalculatorButtonType.Divide: Calculate( Operation.Divide ); break; case CalculatorButtonType.Percent: if( _lastOperation != Operation.None ) { decimal currentValue = CalculatorUtilities.ParseDecimal( DisplayText ); decimal newValue = CalculatorUtilities.Percent( _previousValue, currentValue ); DisplayText = newValue.ToString(); } else { DisplayText = "0"; _showNewNumber = true; } return; case CalculatorButtonType.Sqrt: Calculate( Operation.Sqrt, Operation.None ); break; case CalculatorButtonType.Fraction: Calculate( Operation.Fraction, Operation.None ); break; case CalculatorButtonType.Negate: Calculate( Operation.Negate, Operation.None ); break; case CalculatorButtonType.Equal: Calculate( Operation.None ); break; case CalculatorButtonType.Clear: Calculate( Operation.Clear, Operation.None ); break; case CalculatorButtonType.Cancel: DisplayText = _previousValue.ToString(); _lastOperation = Operation.None; _showNewNumber = true; return; case CalculatorButtonType.Back: ProcessBackKey(); return; default: break; } Decimal.TryParse( DisplayText, out _previousValue ); _showNewNumber = true; } private void SimulateCalculatorButtonClick( CalculatorButtonType buttonType ) { var button = CalculatorUtilities.FindButtonByCalculatorButtonType( _buttonPanel, buttonType ); if( button != null ) { VisualStateManager.GoToState( button, "Pressed", true ); DispatcherTimer timer; if( _timers.ContainsKey( button ) ) { timer = _timers[ button ]; timer.Stop(); } else { timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromMilliseconds( 100 ); timer.Tick += Timer_Tick; _timers.Add( button, timer ); } timer.Start(); } } #endregion //Methods #region Events //Due to a bug in Visual Studio, you cannot create event handlers for nullable args in XAML, so I have to use object instead. public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent( "ValueChanged", RoutingStrategy.Bubble, typeof( RoutedPropertyChangedEventHandler ), typeof( Calculator ) ); public event RoutedPropertyChangedEventHandler ValueChanged { add { AddHandler( ValueChangedEvent, value ); } remove { RemoveHandler( ValueChangedEvent, value ); } } #endregion //Events #region Commands private void ExecuteCalculatorButtonClick( object sender, ExecutedRoutedEventArgs e ) { var buttonType = ( CalculatorButtonType )e.Parameter; ProcessCalculatorButton( buttonType ); } #endregion //Commands } }