From bc0bd11f8ab8a6a6a411a2767cd067a0c8e0aae8 Mon Sep 17 00:00:00 2001 From: Pavel Kovalenko Date: Sat, 18 Oct 2014 21:26:46 +0400 Subject: [PATCH] Add IntegerUpDown control. --- .../Controls/IntegerUpDown/IntegerUpDown.cs | 733 ++++++++++++++++++ .../IntegerUpDownAcceleration.cs | 71 ++ .../IntegerUpDownAccelerationCollection.cs | 180 +++++ .../Interfaces}/IIncrementable.cs | 0 .../Interfaces}/IMouseListener.cs | 0 .../{ => Controls/Interfaces}/IProperty.cs | 0 .../Interfaces}/IPropertyContainer.cs | 0 .../{ => Controls}/PropertyGrid.cs | 0 .../xrSdkControls/xrSdkControls.csproj | 16 +- 9 files changed, 995 insertions(+), 5 deletions(-) create mode 100644 src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDown.cs create mode 100644 src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDownAcceleration.cs create mode 100644 src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDownAccelerationCollection.cs rename src/editors/xrSdkControls/{ => Controls/Interfaces}/IIncrementable.cs (100%) rename src/editors/xrSdkControls/{ => Controls/Interfaces}/IMouseListener.cs (100%) rename src/editors/xrSdkControls/{ => Controls/Interfaces}/IProperty.cs (100%) rename src/editors/xrSdkControls/{ => Controls/Interfaces}/IPropertyContainer.cs (100%) rename src/editors/xrSdkControls/{ => Controls}/PropertyGrid.cs (100%) diff --git a/src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDown.cs b/src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDown.cs new file mode 100644 index 00000000000..72203d4fe85 --- /dev/null +++ b/src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDown.cs @@ -0,0 +1,733 @@ +// Based on original System.Windows.Forms.NumericUpDown +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace XRay.SdkControls +{ + /// + /// Represents a Windows up-down control that displays integer values. + /// + [ComVisible(true), + ClassInterface(ClassInterfaceType.AutoDispatch), + DefaultProperty("Value"), + DefaultEvent("ValueChanged"), + DefaultBindingProperty("Value")] + public class IntegerUpDown : UpDownBase, ISupportInitialize + { + private const bool DefaultHexadecimal = false; + private const int InvalidValue = -1; + private static readonly Int32 DefaultValue = 0; + private static readonly Int32 DefaultMinimum = 0; + private static readonly Int32 DefaultMaximum = 100; + private static readonly Int32 DefaultIncrement = 1; + + // Provides for finer acceleration behavior. + private IntegerUpDownAccelerationCollection accelerations; + + // the current NumericUpDownAcceleration object. + private int accelerationsCurrentIndex; + + // Used to calculate the time elapsed since the up/down button was pressed, + // to know when to get the next entry in the accelaration table. + private long buttonPressedStartTime; + private Int32 currentValue = DefaultValue; + private bool currentValueChanged; + private bool hexadecimal = DefaultHexadecimal; + + /// + /// The amount to increment by. + /// + private Int32 increment = DefaultIncrement; + + private bool initializing; + private Int32 maximum = DefaultMaximum; + private Int32 minimum = DefaultMinimum; + private EventHandler onValueChanged; + + /// + /// [To be supplied.] + /// + [ + SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters") // "0" is the default value for numeric up down. + // So we don't have to localize it. + ] + public IntegerUpDown() + { + // this class overrides GetPreferredSizeCore, let Control automatically cache the result + //SetState2(STATE2_USEPREFERREDSIZECACHE, true); + Text = "0"; + StopAcceleration(); + } + + ////////////////////////////////////////////////////////////// + // Properties + // + ////////////////////////////////////////////////////////////// + + + /// + /// Specifies the acceleration information. + /// + [ + Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) + ] + public IntegerUpDownAccelerationCollection Accelerations + { + get + { + if (accelerations == null) + { + accelerations = new IntegerUpDownAccelerationCollection(); + } + return accelerations; + } + } + + /// + /// Gets or sets a value indicating whether the up-down control should + /// display the value it contains in hexadecimal format. + /// + public bool Hexadecimal + { + get + { + return hexadecimal; + } + set + { + hexadecimal = value; + UpdateEditText(); + } + } + + /// + /// Gets or sets the value to increment or decrement + /// the up-down control when the up or down buttons are clicked. + /// + public Int32 Increment + { + get + { + if (accelerationsCurrentIndex != InvalidValue) + { + return Accelerations[accelerationsCurrentIndex].Increment; + } + return increment; + } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("Increment"); + } + else + { + increment = value; + } + } + } + + /// + /// Gets or sets the maximum value for the up-down control. + /// + public Int32 Maximum + { + get + { + return maximum; + } + set + { + maximum = value; + if (minimum > maximum) + { + minimum = maximum; + } + + Value = Constrain(currentValue); + + Debug.Assert(maximum == value, "Maximum != what we just set it to!"); + } + } + + /// + /// Gets or sets the minimum allowed value for the up-down control. + /// + public Int32 Minimum + { + get + { + return minimum; + } + set + { + minimum = value; + if (minimum > maximum) + { + maximum = value; + } + + Value = Constrain(currentValue); + + Debug.Assert(minimum.Equals(value), "Minimum != what we just set it to!"); + } + } + + /// + /// [To be supplied.] + /// + [Browsable(false), + EditorBrowsable(EditorBrowsableState.Never), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public new Padding Padding + { + get + { + return base.Padding; + } + set + { + base.Padding = value; + } + } + + /// + /// Determines whether the UpDownButtons have been pressed for enough time to activate acceleration. + /// + private bool Spinning + { + get + { + return accelerations != null && buttonPressedStartTime != InvalidValue; + } + } + + /// + /// The text displayed in the control. + /// + [ + Browsable(false), EditorBrowsable(EditorBrowsableState.Never), + Bindable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) + ] + // We're just overriding this to make it non-browsable. + public override string Text + { + get + { + return base.Text; + } + set + { + base.Text = value; + } + } + + /* + * The current value of the control + */ + + /// + /// Gets or sets the value assigned to the up-down control. + /// + public Int32 Value + { + get + { + if (UserEdit) + { + ValidateEditText(); + } + return currentValue; + } + + set + { + if (value != currentValue) + { + if (!initializing && ((value < minimum) || (value > maximum))) + { + throw new ArgumentOutOfRangeException("Value"); + } + else + { + currentValue = value; + + OnValueChanged(EventArgs.Empty); + currentValueChanged = true; + UpdateEditText(); + } + } + } + } + + + ////////////////////////////////////////////////////////////// + // Methods + // + ////////////////////////////////////////////////////////////// + + /// + /// Handles tasks required when the control is being initialized. + /// + public void BeginInit() + { + initializing = true; + } + + /// + /// Called when initialization of the control is complete. + /// + public void EndInit() + { + initializing = false; + Value = Constrain(currentValue); + UpdateEditText(); + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public new event EventHandler PaddingChanged + { + add + { + base.PaddingChanged += value; + } + remove + { + base.PaddingChanged -= value; + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public new event EventHandler TextChanged + { + add + { + base.TextChanged += value; + } + remove + { + base.TextChanged -= value; + } + } + + /// + /// Occurs when the IntegerUpDown.Value property has been changed in some way. + /// + public event EventHandler ValueChanged + { + add + { + onValueChanged += value; + } + remove + { + onValueChanged -= value; + } + } + + // + // Returns the provided value constrained to be within the min and max. + // + private Int32 Constrain(Int32 value) + { + Debug.Assert(minimum <= maximum, "minimum > maximum"); + + if (value < minimum) + { + value = minimum; + } + + if (value > maximum) + { + value = maximum; + } + + return value; + } + + /// + /// Decrements the value of the up-down control. + /// + public override void DownButton() + { + SetNextAcceleration(); + + if (UserEdit) + { + ParseEditText(); + } + + var newValue = currentValue; + + // Operations on Decimals can throw OverflowException. + // + try + { + newValue -= Increment; + + if (newValue < minimum) + { + newValue = minimum; + if (Spinning) + { + StopAcceleration(); + } + } + } + catch (OverflowException) + { + newValue = minimum; + } + + Value = newValue; + } + + /// + /// Overridden to set/reset acceleration variables. + /// + protected override void OnKeyDown(KeyEventArgs e) + { + if (base.InterceptArrowKeys && (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down) && !Spinning) + { + StartAcceleration(); + } + + base.OnKeyDown(e); + } + + /// + /// Overridden to set/reset acceleration variables. + /// + protected override void OnKeyUp(KeyEventArgs e) + { + if (base.InterceptArrowKeys && (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)) + { + StopAcceleration(); + } + + base.OnKeyUp(e); + } + + /// + /// Restricts the entry of characters to digits (including hex), the negative sign, + /// The decimal point, and editing keystrokes (backspace). + /// + protected override void OnTextBoxKeyPress(object source, KeyPressEventArgs e) + { + base.OnTextBoxKeyPress(source, e); + + var numberFormatInfo = CultureInfo.CurrentCulture.NumberFormat; + var decimalSeparator = numberFormatInfo.NumberDecimalSeparator; + var groupSeparator = numberFormatInfo.NumberGroupSeparator; + var negativeSign = numberFormatInfo.NegativeSign; + + var keyInput = e.KeyChar.ToString(); + + if (Char.IsDigit(e.KeyChar)) + { + // Digits are OK + } + else if (keyInput.Equals(decimalSeparator) || keyInput.Equals(groupSeparator) || + keyInput.Equals(negativeSign)) + { + // Int32 separator is OK + } + else if (e.KeyChar == '\b') + { + // Backspace key is OK + } + else if (Hexadecimal && ((e.KeyChar >= 'a' && e.KeyChar <= 'f') || e.KeyChar >= 'A' && e.KeyChar <= 'F')) + { + // Hexadecimal digits are OK + } + else if ((ModifierKeys & (Keys.Control | Keys.Alt)) != 0) + { + // Let the edit control handle control and alt key combinations + } + else + { + // Eat this invalid key and beep + e.Handled = true; + //SafeNativeMethods.MessageBeep(0); + } + } + + /// + /// Raises the IntegerUpDown.OnValueChanged event. + /// + protected virtual void OnValueChanged(EventArgs e) + { + // Call the event handler + if (onValueChanged != null) + { + onValueChanged(this, e); + } + } + + protected override void OnLostFocus(EventArgs e) + { + base.OnLostFocus(e); + if (UserEdit) + { + UpdateEditText(); + } + } + + /// + /// Overridden to start/end acceleration. + /// + internal void OnStartTimer() + { + StartAcceleration(); + } + + /// + /// Overridden to start/end acceleration. + /// + internal void OnStopTimer() + { + StopAcceleration(); + } + + /// + /// Converts the text displayed in the up-down control to a + /// numeric value and evaluates it. + /// + protected void ParseEditText() + { + Debug.Assert(UserEdit, "ParseEditText() - UserEdit == false"); + + try + { + // VSWhidbey 173332: Verify that the user is not starting the string with a "-" + // before attempting to set the Value property since a "-" is a valid character with + // which to start a string representing a negative number. + if (!string.IsNullOrEmpty(Text) && + !(Text.Length == 1 && Text == "-")) + { + if (Hexadecimal) + { + Value = Constrain(Convert.ToInt32(Text, 16)); + } + else + { + Value = Constrain(Int32.Parse(Text, CultureInfo.CurrentCulture)); + } + } + } + catch + { + // Leave value as it is + } + finally + { + UserEdit = false; + } + } + + /// + /// Updates the index of the UpDownNumericAcceleration entry to use (if needed). + /// + private void SetNextAcceleration() + { + // Spinning will check if accelerations is null. + if (Spinning && accelerationsCurrentIndex < (accelerations.Count - 1)) + { // if index not the last entry ... + // Ticks are in 100-nanoseconds (1E-7 seconds). + var nowTicks = DateTime.Now.Ticks; + var buttonPressedElapsedTime = nowTicks - buttonPressedStartTime; + var accelerationInterval = 10000000L * accelerations[accelerationsCurrentIndex + 1].Seconds; // next entry. + + // If Up/Down button pressed for more than the current acceleration entry interval, get next entry in the accel table. + if (buttonPressedElapsedTime > accelerationInterval) + { + buttonPressedStartTime = nowTicks; + accelerationsCurrentIndex++; + } + } + } + + private void ResetIncrement() + { + Increment = DefaultIncrement; + } + + private void ResetMaximum() + { + Maximum = DefaultMaximum; + } + + private void ResetMinimum() + { + Minimum = DefaultMinimum; + } + + private void ResetValue() + { + Value = DefaultValue; + } + + /// + /// Records when UpDownButtons are pressed to enable acceleration. + /// + private void StartAcceleration() + { + buttonPressedStartTime = DateTime.Now.Ticks; + } + + /// + /// Reset when UpDownButtons are pressed. + /// + private void StopAcceleration() + { + accelerationsCurrentIndex = InvalidValue; + buttonPressedStartTime = InvalidValue; + } + + /// + /// Provides some interesting info about this control in String form. + /// + public override string ToString() + { + var s = base.ToString(); + s += ", Minimum = " + Minimum.ToString(CultureInfo.CurrentCulture) + ", Maximum = " + Maximum.ToString(CultureInfo.CurrentCulture); + return s; + } + + /// + /// Increments the value of the up-down control. + /// + public override void UpButton() + { + SetNextAcceleration(); + + if (UserEdit) + { + ParseEditText(); + } + + var newValue = currentValue; + + // Operations on Decimals can throw OverflowException. + // + try + { + newValue += Increment; + + if (newValue > maximum) + { + newValue = maximum; + if (Spinning) + { + StopAcceleration(); + } + } + } + catch (OverflowException) + { + newValue = maximum; + } + + Value = newValue; + } + + private string GetNumberText(Int32 num) + { + string text; + + if (Hexadecimal) + { + text = ((Int64)num).ToString("X", CultureInfo.InvariantCulture); + Debug.Assert(text == text.ToUpper(CultureInfo.InvariantCulture), "GetPreferredSize assumes hex digits to be uppercase."); + } + else + { + text = num.ToString("D", CultureInfo.CurrentCulture); + } + return text; + } + + /// + /// Displays the current value of the up-down control in the appropriate format. + /// + protected override void UpdateEditText() + { + // If we're initializing, we don't want to update the edit text yet, + // just in case the value is invalid. + if (initializing) + { + return; + } + + // If the current value is user-edited, then parse this value before reformatting + if (UserEdit) + { + ParseEditText(); + } + + // VSWhidbey 173332: Verify that the user is not starting the string with a "-" + // before attempting to set the Value property since a "-" is a valid character with + // which to start a string representing a negative number. + if (currentValueChanged || (!string.IsNullOrEmpty(Text) && + !(Text.Length == 1 && Text == "-"))) + { + currentValueChanged = false; + ChangingText = true; + + // Make sure the current value is within the min/max + Debug.Assert(minimum <= currentValue && currentValue <= maximum, + "DecimalValue lies outside of [minimum, maximum]"); + + Text = GetNumberText(currentValue); + Debug.Assert(ChangingText == false, "ChangingText should have been set to false"); + } + } + + /// + /// Validates and updates + /// the text displayed in the up-down control. + /// + protected override void ValidateEditText() + { + // See if the edit text parses to a valid decimal + ParseEditText(); + UpdateEditText(); + } + + private int GetLargestDigit(int start, int end) + { + var largestDigit = -1; + var digitWidth = -1; + + for (var i = start; i < end; ++i) + { + char ch; + if (i < 10) + { + ch = i.ToString(CultureInfo.InvariantCulture)[0]; + } + else + { + ch = (char)('A' + (i - 10)); + } + + var digitSize = TextRenderer.MeasureText(ch.ToString(), Font); + + if (digitSize.Width >= digitWidth) + { + digitWidth = digitSize.Width; + largestDigit = i; + } + } + Debug.Assert(largestDigit != -1 && digitWidth != -1, "Failed to find largest digit."); + return largestDigit; + } + } +} diff --git a/src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDownAcceleration.cs b/src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDownAcceleration.cs new file mode 100644 index 00000000000..37a97805326 --- /dev/null +++ b/src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDownAcceleration.cs @@ -0,0 +1,71 @@ +using System; +using System.Windows.Forms; + +namespace XRay.SdkControls +{ + /// + /// Comprises the information specifying how acceleration should be performed + /// on a Windows up-down control when the up/down button is pressed for certain + /// amount of time. + /// + public class IntegerUpDownAcceleration + { + private Int32 seconds; // Ideally we would use UInt32 but it is not CLS-compliant. + private Int32 increment; // Ideally we would use UInt32 but it is not CLS-compliant. + + public IntegerUpDownAcceleration(Int32 seconds, Int32 increment) + { + if (seconds < 0) + { + throw new ArgumentOutOfRangeException("seconds < 0"); + } + + if (increment < 0) + { + throw new ArgumentOutOfRangeException("increment < 0"); + } + + this.seconds = seconds; + this.increment = increment; + } + + /// + /// Determines the amount of time for the UpDown control to wait to set the increment + /// step when holding the up/down button. + /// + public Int32 Seconds + { + get + { + return seconds; + } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("seconds < 0"); + } + seconds = value; + } + } + + /// + /// Determines the amount to increment by. + /// + public Int32 Increment + { + get + { + return increment; + } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("increment < 0"); + } + increment = value; + } + } + } +} \ No newline at end of file diff --git a/src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDownAccelerationCollection.cs b/src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDownAccelerationCollection.cs new file mode 100644 index 00000000000..dfe85d6d981 --- /dev/null +++ b/src/editors/xrSdkControls/Controls/IntegerUpDown/IntegerUpDownAccelerationCollection.cs @@ -0,0 +1,180 @@ +using System; +using System.Diagnostics; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows.Forms; + +namespace XRay.SdkControls +{ + /// + /// Represents a SORTED collection of IntegerUpDownAcceleration objects in the NumericUpDown Control. + /// The elements in the collection are sorted by the IntegerUpDownAcceleration.Seconds property. + /// + [ListBindable(false)] + public class IntegerUpDownAccelerationCollection : + MarshalByRefObject, + ICollection, + IEnumerable + { + private List items; + + /// + /// Class constructor. + /// + public IntegerUpDownAccelerationCollection() + { + items = new List(); + } + + /// + /// Gets (ReadOnly) the element at the specified index. In C#, this property is the indexer for + /// the IList class. + /// + /// + /// + public IntegerUpDownAcceleration this[int index] + { + get + { + return items[index]; + } + } + + // ICollection implementation. + + /// + /// Adds an item (IntegerUpDownAcceleration object) to the ICollection. + /// The item is added preserving the collection sorted. + /// + /// + public void Add(IntegerUpDownAcceleration acceleration) + { + if (acceleration == null) + { + throw new ArgumentNullException("acceleration"); + } + + // Keep the array sorted, insert in the right spot. + var index = 0; + + while (index < items.Count) + { + if (acceleration.Seconds < items[index].Seconds) + { + break; + } + ++index; + } + items.Insert(index, acceleration); + } + + /// + /// Removes all items from the ICollection. + /// + public void Clear() + { + items.Clear(); + } + + /// + /// Determines whether the IList contains a specific value. + /// + /// + /// + public bool Contains(IntegerUpDownAcceleration acceleration) + { + return items.Contains(acceleration); + } + + /// + /// Copies the elements of the ICollection to an Array, starting at a particular Array index. + /// + /// + /// + public void CopyTo(IntegerUpDownAcceleration[] array, int index) + { + items.CopyTo(array, index); + } + + /// + /// Gets the number of elements contained in the ICollection. + /// + public int Count + { + get + { + return items.Count; + } + } + + /// + /// Gets a value indicating whether the ICollection is read-only. + /// This collection property returns false always. + /// + public bool IsReadOnly + { + get + { + return false; + } + } + + /// + /// Removes the specified item from the ICollection. + /// + /// + /// + public bool Remove(IntegerUpDownAcceleration acceleration) + { + return items.Remove(acceleration); + } + + // IEnumerable implementation. + + + /// + /// Returns an enumerator that can iterate through the collection. + /// + /// + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)items).GetEnumerator(); + } + + // IntegerUpDownAccelerationCollection methods. + + /// + /// Adds the elements of specified array to the collection, keeping the collection sorted. + /// + /// + public void AddRange(params IntegerUpDownAcceleration[] accelerations) + { + if (accelerations == null) + { + throw new ArgumentNullException("accelerations"); + } + + // Accept the range only if ALL elements in the array are not null. + foreach (var acceleration in accelerations) + { + if (acceleration == null) + { + throw new ArgumentNullException("At least oe entry is null"); + } + } + + // The expected array size is typically small (5 items?), so we don't need to try to be smarter about the + // way we add the elements to the collection, just call Add. + foreach (var acceleration in accelerations) + { + Add(acceleration); + } + } + } +} diff --git a/src/editors/xrSdkControls/IIncrementable.cs b/src/editors/xrSdkControls/Controls/Interfaces/IIncrementable.cs similarity index 100% rename from src/editors/xrSdkControls/IIncrementable.cs rename to src/editors/xrSdkControls/Controls/Interfaces/IIncrementable.cs diff --git a/src/editors/xrSdkControls/IMouseListener.cs b/src/editors/xrSdkControls/Controls/Interfaces/IMouseListener.cs similarity index 100% rename from src/editors/xrSdkControls/IMouseListener.cs rename to src/editors/xrSdkControls/Controls/Interfaces/IMouseListener.cs diff --git a/src/editors/xrSdkControls/IProperty.cs b/src/editors/xrSdkControls/Controls/Interfaces/IProperty.cs similarity index 100% rename from src/editors/xrSdkControls/IProperty.cs rename to src/editors/xrSdkControls/Controls/Interfaces/IProperty.cs diff --git a/src/editors/xrSdkControls/IPropertyContainer.cs b/src/editors/xrSdkControls/Controls/Interfaces/IPropertyContainer.cs similarity index 100% rename from src/editors/xrSdkControls/IPropertyContainer.cs rename to src/editors/xrSdkControls/Controls/Interfaces/IPropertyContainer.cs diff --git a/src/editors/xrSdkControls/PropertyGrid.cs b/src/editors/xrSdkControls/Controls/PropertyGrid.cs similarity index 100% rename from src/editors/xrSdkControls/PropertyGrid.cs rename to src/editors/xrSdkControls/Controls/PropertyGrid.cs diff --git a/src/editors/xrSdkControls/xrSdkControls.csproj b/src/editors/xrSdkControls/xrSdkControls.csproj index 0110cdc7072..38b2d485e37 100644 --- a/src/editors/xrSdkControls/xrSdkControls.csproj +++ b/src/editors/xrSdkControls/xrSdkControls.csproj @@ -49,15 +49,21 @@ - + + Component + + + + Component - - - - + + + + +