From 32495086d1f5d53b61b67d5466dab05aea2a91d0 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Sat, 18 Jun 2016 19:06:17 +0700 Subject: [PATCH] Inspector: Add new AdvancedSlider control Replace the old RangeEditor and TextBox numeric editors with the new advanced slider control. The user can use a combination of ALT, CTRL and SHIFT keys to adjust the rate of change. The user can also double click the value to edit the value numerically. Signed-off-by: Axel Gembe --- .../AdvancedSliderPropertyEditorBuilder.cs | 44 +++ ...dvancedSliderRangePropertyEditorBuilder.cs | 53 +++ .../Conventions/DefaultPropertyInspectors.cs | 6 +- .../Gemini.Modules.Inspector.csproj | 10 + .../Inspectors/AdvancedSliderEditorView.xaml | 40 ++ .../AdvancedSliderEditorView.xaml.cs | 15 + .../AdvancedSliderEditorViewModel.cs | 151 ++++++++ .../Framework/Controls/AdvancedSlider.cs | 173 +++++++++ .../Controls/AdvancedSliderBase.xaml | 57 +++ .../Controls/AdvancedSliderBase.xaml.cs | 357 ++++++++++++++++++ .../Framework/Controls/RepeatingButton.cs | 121 ++++++ src/Gemini/Framework/Util/SpeedMultiplier.cs | 27 ++ src/Gemini/Framework/Win32/NativeMethods.cs | 10 +- src/Gemini/Gemini.csproj | 14 + src/Gemini/Properties/Resources.de.resx | 3 + src/Gemini/Properties/Resources.resx | 3 + src/Gemini/Properties/Resources.zh-Hans.resx | 6 - src/Gemini/Themes/Generic.xaml | 30 +- src/Gemini/Themes/VS2013/BlueTheme.xaml | 7 + .../VS2013/Controls/AdvancedSlider.xaml | 29 ++ src/Gemini/Themes/VS2013/Controls/Button.xaml | 1 + src/Gemini/Themes/VS2013/Controls/Merged.xaml | 1 + src/Gemini/Themes/VS2013/DarkTheme.xaml | 7 + src/Gemini/Themes/VS2013/LightTheme.xaml | 7 + 24 files changed, 1160 insertions(+), 12 deletions(-) create mode 100644 src/Gemini.Modules.Inspector/Conventions/AdvancedSliderPropertyEditorBuilder.cs create mode 100644 src/Gemini.Modules.Inspector/Conventions/AdvancedSliderRangePropertyEditorBuilder.cs create mode 100644 src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorView.xaml create mode 100644 src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorView.xaml.cs create mode 100644 src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorViewModel.cs create mode 100644 src/Gemini/Framework/Controls/AdvancedSlider.cs create mode 100644 src/Gemini/Framework/Controls/AdvancedSliderBase.xaml create mode 100644 src/Gemini/Framework/Controls/AdvancedSliderBase.xaml.cs create mode 100644 src/Gemini/Framework/Controls/RepeatingButton.cs create mode 100644 src/Gemini/Framework/Util/SpeedMultiplier.cs create mode 100644 src/Gemini/Themes/VS2013/Controls/AdvancedSlider.xaml diff --git a/src/Gemini.Modules.Inspector/Conventions/AdvancedSliderPropertyEditorBuilder.cs b/src/Gemini.Modules.Inspector/Conventions/AdvancedSliderPropertyEditorBuilder.cs new file mode 100644 index 00000000..352bdcb6 --- /dev/null +++ b/src/Gemini.Modules.Inspector/Conventions/AdvancedSliderPropertyEditorBuilder.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel; +using Gemini.Modules.Inspector.Inspectors; + +namespace Gemini.Modules.Inspector.Conventions +{ + public class AdvancedSliderPropertyEditorBuilder : PropertyEditorBuilder + { + public override bool IsApplicable(PropertyDescriptor propertyDescriptor) + { + var isNumberType = propertyDescriptor.PropertyType == typeof(int) + || propertyDescriptor.PropertyType == typeof(double) + || propertyDescriptor.PropertyType == typeof(float); + + return isNumberType; + } + + public override IEditor BuildEditor(PropertyDescriptor propertyDescriptor) + { + if (propertyDescriptor.PropertyType == typeof(int)) + { + return new AdvancedSliderEditorViewModel() { + Speed = 1 + }; + } + + if (propertyDescriptor.PropertyType == typeof(double)) + { + return new AdvancedSliderEditorViewModel() { + Speed = 0.1 + }; + } + + if (propertyDescriptor.PropertyType == typeof(float)) + { + return new AdvancedSliderEditorViewModel() { + Speed = 0.1f + }; + } + + throw new InvalidOperationException(); + } + } +} diff --git a/src/Gemini.Modules.Inspector/Conventions/AdvancedSliderRangePropertyEditorBuilder.cs b/src/Gemini.Modules.Inspector/Conventions/AdvancedSliderRangePropertyEditorBuilder.cs new file mode 100644 index 00000000..65640027 --- /dev/null +++ b/src/Gemini.Modules.Inspector/Conventions/AdvancedSliderRangePropertyEditorBuilder.cs @@ -0,0 +1,53 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Gemini.Modules.Inspector.Inspectors; + +namespace Gemini.Modules.Inspector.Conventions +{ + public class AdvancedSliderRangePropertyEditorBuilder : PropertyEditorBuilder + { + public override bool IsApplicable(PropertyDescriptor propertyDescriptor) + { + var isNumberType = propertyDescriptor.PropertyType == typeof(int) + || propertyDescriptor.PropertyType == typeof(double) + || propertyDescriptor.PropertyType == typeof(float); + + if (!isNumberType) + return false; + + return propertyDescriptor.Attributes.Cast().Any(x => x is RangeAttribute); + } + + public override IEditor BuildEditor(PropertyDescriptor propertyDescriptor) + { + var rangeAttribute = propertyDescriptor.Attributes + .Cast().OfType() + .First(); + + if (propertyDescriptor.PropertyType == typeof(int)) + { + return new AdvancedSliderEditorViewModel((int) rangeAttribute.Minimum, (int) rangeAttribute.Maximum) { + Speed = 1 + }; + } + + if (propertyDescriptor.PropertyType == typeof(double)) + { + return new AdvancedSliderEditorViewModel((double) rangeAttribute.Minimum, (double) rangeAttribute.Maximum) { + Speed = 0.1 + }; + } + + if (propertyDescriptor.PropertyType == typeof(float)) + { + return new AdvancedSliderEditorViewModel((float) rangeAttribute.Minimum, (float) rangeAttribute.Maximum) { + Speed = 0.1f + }; + } + + throw new InvalidOperationException(); + } + } +} diff --git a/src/Gemini.Modules.Inspector/Conventions/DefaultPropertyInspectors.cs b/src/Gemini.Modules.Inspector/Conventions/DefaultPropertyInspectors.cs index 1a850c7a..f7a8781c 100644 --- a/src/Gemini.Modules.Inspector/Conventions/DefaultPropertyInspectors.cs +++ b/src/Gemini.Modules.Inspector/Conventions/DefaultPropertyInspectors.cs @@ -20,14 +20,12 @@ static DefaultPropertyInspectors() { _inspectorBuilders = new List { - new RangePropertyEditorBuilder(), + new AdvancedSliderPropertyEditorBuilder(), + new AdvancedSliderRangePropertyEditorBuilder(), new EnumPropertyEditorBuilder(), new StandardPropertyEditorBuilder(), new StandardPropertyEditorBuilder(), - new StandardPropertyEditorBuilder>(), - new StandardPropertyEditorBuilder>(), - new StandardPropertyEditorBuilder>(), new StandardPropertyEditorBuilder>(), new StandardPropertyEditorBuilder>(), new StandardPropertyEditorBuilder>(), diff --git a/src/Gemini.Modules.Inspector/Gemini.Modules.Inspector.csproj b/src/Gemini.Modules.Inspector/Gemini.Modules.Inspector.csproj index 9343b96f..7e828e0b 100644 --- a/src/Gemini.Modules.Inspector/Gemini.Modules.Inspector.csproj +++ b/src/Gemini.Modules.Inspector/Gemini.Modules.Inspector.csproj @@ -98,14 +98,20 @@ + + + + AdvancedSliderEditorView.xaml + + BitmapSourceEditorView.xaml @@ -183,6 +189,10 @@ + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorView.xaml b/src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorView.xaml new file mode 100644 index 00000000..3caa7e94 --- /dev/null +++ b/src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorView.xaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorView.xaml.cs b/src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorView.xaml.cs new file mode 100644 index 00000000..8c9db361 --- /dev/null +++ b/src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorView.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace Gemini.Modules.Inspector.Inspectors +{ + /// + /// Interaction logic for AdvancedSliderEditorView.xaml + /// + public partial class AdvancedSliderEditorView : UserControl + { + public AdvancedSliderEditorView() + { + InitializeComponent(); + } + } +} diff --git a/src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorViewModel.cs b/src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorViewModel.cs new file mode 100644 index 00000000..3e1d1084 --- /dev/null +++ b/src/Gemini.Modules.Inspector/Inspectors/AdvancedSliderEditorViewModel.cs @@ -0,0 +1,151 @@ +using System; + +using Caliburn.Micro; + +using Gemini.Framework.Controls; +using Gemini.Framework.Util; + +namespace Gemini.Modules.Inspector.Inspectors +{ + public class AdvancedSliderEditorViewModel : SelectiveUndoEditorBase, ILabelledInspector, IViewAware where TValue : IComparable + { + private static readonly string _defaultValueFormat = "{0:0.#####}"; + + public AdvancedSliderEditorViewModel() + { + _valueFormat = _defaultValueFormat; + _valueType = typeof(TValue); + _type = AdvancedSliderBase.DisplayType.Number; + } + + public AdvancedSliderEditorViewModel(TValue min, TValue max) + { + _minimum = min; + _maximum = max; + _valueFormat = _defaultValueFormat; + _valueType = typeof(TValue); + _type = AdvancedSliderBase.DisplayType.Bar; + } + + private TValue _minimum; + + public TValue Minimum + { + get { return _minimum; } + set + { + _minimum = value; + NotifyOfPropertyChange(() => Minimum); + } + } + + private TValue _maximum; + + public TValue Maximum + { + get { return _maximum; } + set + { + _maximum = value; + NotifyOfPropertyChange(() => Maximum); + } + } + + private TValue _speed; + + public TValue Speed + { + get { return _speed; } + set + { + _speed = value; + NotifyOfPropertyChange(() => Speed); + } + } + + private bool _mouseCaptured; + + public bool MouseCaptured + { + get { return _mouseCaptured; } + set + { + if (_mouseCaptured == value) + return; + + _mouseCaptured = value; + + if (value) + OnBeginEdit(); + else + OnEndEdit(); + } + } + + private string _valueFormat; + + public string ValueFormat + { + get { return _valueFormat; } + set + { + _valueFormat = value; + NotifyOfPropertyChange(() => ValueFormat); + } + } + + private Type _valueType; + + public Type ValueType + { + get { return _valueType; } + set + { + _valueType = value; + NotifyOfPropertyChange(() => ValueType); + } + } + + private AdvancedSliderBase.DisplayType _type; + + public AdvancedSliderBase.DisplayType Type + { + get { return _type; } + + set + { + if (Equals(_type, value)) + return; + + _type = value; + + NotifyOfPropertyChange(() => Type); + } + } + + public void Up() + { + _view.slider.ApplyValueChange(SpeedMultiplier.Get()); + } + + public void Down() + { + _view.slider.ApplyValueChange(-SpeedMultiplier.Get()); + } + + private AdvancedSliderEditorView _view; + + public event EventHandler ViewAttached; + + public void AttachView(object view, object context = null) + { + _view = (AdvancedSliderEditorView) view; + ViewAttached?.Invoke(this, new ViewAttachedEventArgs() { View = view, Context = context }); + } + + public object GetView(object context = null) + { + return _view; + } + } +} diff --git a/src/Gemini/Framework/Controls/AdvancedSlider.cs b/src/Gemini/Framework/Controls/AdvancedSlider.cs new file mode 100644 index 00000000..d783f58b --- /dev/null +++ b/src/Gemini/Framework/Controls/AdvancedSlider.cs @@ -0,0 +1,173 @@ +using System; +using System.ComponentModel; +using System.Windows; + +namespace Gemini.Framework.Controls +{ + public class AdvancedSlider : AdvancedSliderBase + { + public override void ApplyValueChange(double ammount) + { + var s = (dynamic) Speed; + var change = s * ammount; + + var newValue = (dynamic) Value + Convert.ChangeType(change, ValueType); + newValue = CoerceValue(this, newValue); + + Value = newValue; + } + + protected override void CommitEditText() + { + try + { + + Value = TypeDescriptor.GetConverter(ValueType).ConvertFrom(EditText); + } + catch (Exception ex) + { + var msg = string.Format(Properties.Resources.AdvancedSliderCommitErrorFormat, ex.Message); + MessageBox.Show(Application.Current.MainWindow, msg, Application.Current.MainWindow.Title, MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + public Type ValueType + { + get { return (Type) GetValue(ValueTypeProperty); } + set { SetValue(ValueTypeProperty, value); } + } + + public static readonly DependencyProperty ValueTypeProperty = + DependencyProperty.Register("ValueType", typeof(Type), typeof(AdvancedSlider)); + + public object Value + { + get { return GetValue(ValueProperty); } + set + { + SetValue(ValueProperty, value); + if (ValueType == null) + ValueType = Value.GetType(); + } + } + + public static readonly DependencyProperty ValueProperty = + DependencyProperty.Register("Value", typeof(object), typeof(AdvancedSlider), + new FrameworkPropertyMetadata(DependencyPropertyChanged, CoerceValue) + ); + + public object ValueMin + { + get { return GetValue(ValueMinProperty); } + set { SetValue(ValueMinProperty, value); } + } + + public static readonly DependencyProperty ValueMinProperty = + DependencyProperty.Register("ValueMin", typeof(object), typeof(AdvancedSlider), + new FrameworkPropertyMetadata(DependencyPropertyChanged) + ); + + public object ValueMax + { + get { return GetValue(ValueMaxProperty); } + set { SetValue(ValueMaxProperty, value); } + } + + public static readonly DependencyProperty ValueMaxProperty = + DependencyProperty.Register("ValueMax", typeof(object), typeof(AdvancedSlider), + new FrameworkPropertyMetadata(DependencyPropertyChanged) + ); + + public object Speed + { + get { return GetValue(SpeedProperty); } + set { SetValue(SpeedProperty, value); } + } + + public static readonly DependencyProperty SpeedProperty = + DependencyProperty.Register("Speed", typeof(object), typeof(AdvancedSlider), + new FrameworkPropertyMetadata(DependencyPropertyChanged) + ); + + public string ValueFormat + { + get { return (string) GetValue(ValueFormatProperty); } + set { SetValue(ValueFormatProperty, value); } + } + + public static readonly DependencyProperty ValueFormatProperty = + DependencyProperty.Register("ValueFormat", typeof(string), typeof(AdvancedSlider), + new FrameworkPropertyMetadata("{0:0.#####}", DependencyPropertyChanged) + ); + + public string ValueEditFormat + { + get { return (string) GetValue(ValueEditFormatProperty); } + set { SetValue(ValueEditFormatProperty, value); } + } + + public static readonly DependencyProperty ValueEditFormatProperty = + DependencyProperty.Register("ValueEditFormat", typeof(string), typeof(AdvancedSlider), + new FrameworkPropertyMetadata("{0:0.#####}", DependencyPropertyChanged) + ); + + private static void DependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var slider = d as AdvancedSlider; + slider.UpdateValues(); + } + + private static object CoerceValue(DependencyObject d, object baseValue) + { + if (baseValue == null) + return null; + + var slider = d as AdvancedSlider; + + if (slider.Type == DisplayType.Number) + return baseValue; + + var value = (dynamic) baseValue; + + if (slider.ValueMin != null) + { + if (value.CompareTo(slider.ValueMin) < 0) + value = slider.ValueMin; + } + + if (slider.ValueMax != null) + { + if (value.CompareTo(slider.ValueMax) > 0) + value = slider.ValueMax; + } + + return value; + } + + private void UpdateValues() + { + if (Value == null) + return; + + DisplayText = string.Format(ValueFormat, Value); + EditText = string.Format(ValueEditFormat, Value); + + if (Type == DisplayType.Number) + return; + + if (ValueMin != null && ValueMax != null) + { + try + { + var v = (dynamic) Value; + var vmin = (dynamic) ValueMin; + var vmax = (dynamic) ValueMax; + Ratio = (v - vmin) / (vmax - vmin); + } + catch (DivideByZeroException) + { + } + } + } + } +} diff --git a/src/Gemini/Framework/Controls/AdvancedSliderBase.xaml b/src/Gemini/Framework/Controls/AdvancedSliderBase.xaml new file mode 100644 index 00000000..0e0bce00 --- /dev/null +++ b/src/Gemini/Framework/Controls/AdvancedSliderBase.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + diff --git a/src/Gemini/Framework/Controls/AdvancedSliderBase.xaml.cs b/src/Gemini/Framework/Controls/AdvancedSliderBase.xaml.cs new file mode 100644 index 00000000..30815bc1 --- /dev/null +++ b/src/Gemini/Framework/Controls/AdvancedSliderBase.xaml.cs @@ -0,0 +1,357 @@ +using Gemini.Framework.Util; +using Gemini.Framework.Win32; +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace Gemini.Framework.Controls +{ + public abstract partial class AdvancedSliderBase : UserControl, IDisposable + { + static AdvancedSliderBase() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AdvancedSliderBase), new FrameworkPropertyMetadata(typeof(AdvancedSliderBase))); + } + + public AdvancedSliderBase() + { + _clickTimer = new System.Timers.Timer(NativeMethods.GetDoubleClickTime()); + _clickTimer.Elapsed += ClickTimer_Elapsed; + InitializeComponent(); + } + + private void Update() + { + numberText.Text = DisplayText; + + if (Type == DisplayType.Number) + { + bar.Visibility = Visibility.Hidden; + return; + } + + var width = grid.ActualWidth; + + switch (Type) + { + case DisplayType.Bar: + bar.Visibility = Visibility.Visible; + bar.HorizontalAlignment = HorizontalAlignment.Left; + bar.Margin = new Thickness(0.0); + bar.Width = width * Ratio; + break; + case DisplayType.Line: + if (Ratio == 0.5) + { + bar.Visibility = Visibility.Hidden; + } + else + { + bar.Visibility = Visibility.Visible; + + var hwidth = (width / 2.0); + if (Ratio > 0.5) + { + bar.HorizontalAlignment = HorizontalAlignment.Left; + bar.Width = Math.Ceiling(hwidth * ((Ratio - 0.5) * 2.0)); + bar.Margin = new Thickness(Math.Floor(hwidth), 0.0, 0.0, 0.0); + } + else + { + bar.HorizontalAlignment = HorizontalAlignment.Right; + bar.Width = Math.Ceiling(hwidth * (1.0 - (Ratio * 2.0))); + bar.Margin = new Thickness(0.0, 0.0, Math.Floor(hwidth), 0.0); + } + } + break; + } + } + + public string DisplayText + { + get { return (string) GetValue(DisplayTextProperty); } + set { SetValue(DisplayTextProperty, value); } + } + + public static readonly DependencyProperty DisplayTextProperty = + DependencyProperty.Register("DisplayText", typeof(string), typeof(AdvancedSliderBase), + new FrameworkPropertyMetadata(string.Empty, DependencyPropertyChanged) + ); + + public string EditText + { + get { return (string) GetValue(EditTextProperty); } + set { SetValue(EditTextProperty, value); } + } + + public static readonly DependencyProperty EditTextProperty = + DependencyProperty.Register("EditText", typeof(string), typeof(AdvancedSliderBase), + new FrameworkPropertyMetadata(string.Empty) + ); + + public enum DisplayType + { + Number, + Bar, + Line + } + + public DisplayType Type + { + get { return (DisplayType) GetValue(TypeProperty); } + set { SetValue(TypeProperty, value); } + } + + public static readonly DependencyProperty TypeProperty = + DependencyProperty.Register("Type", typeof(DisplayType), typeof(AdvancedSliderBase), + new FrameworkPropertyMetadata(DisplayType.Number, DependencyPropertyChanged) + ); + + public double Ratio + { + get { return (double) GetValue(RatioProperty); } + set { SetValue(RatioProperty, value); } + } + + public static readonly DependencyProperty RatioProperty = + DependencyProperty.Register("Ratio", typeof(double), typeof(AdvancedSliderBase), + new FrameworkPropertyMetadata(0.5, DependencyPropertyChanged, CoerceValue) + ); + + public Brush BarBrush + { + get { return (Brush) GetValue(BarBrushProperty); } + set { SetValue(BarBrushProperty, value); } + } + + public static readonly DependencyProperty BarBrushProperty = + DependencyProperty.Register("BarBrush", typeof(Brush), typeof(AdvancedSliderBase), + new FrameworkPropertyMetadata(Brushes.DarkGray, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, DependencyPropertyChanged) + ); + + private static void DependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var slider = d as AdvancedSliderBase; + slider.Update(); + } + + private static object CoerceValue(DependencyObject d, object baseValue) + { + var value = (double) baseValue; + if (value < 0.0) + value = 0.0; + if (value > 1.0) + value = 1.0; + return value; + } + + public bool MouseCaptured + { + get { return (bool) GetValue(MouseCapturedProperty); } + set { SetValue(MouseCapturedProperty, value); } + } + + public static readonly DependencyProperty MouseCapturedProperty = + DependencyProperty.Register("MouseCaptured", typeof(bool), typeof(AdvancedSliderBase)); + + private double _startValue; + private Point _originalPos; + private Point _resetPos; + private Point _lastPos; + private bool _lostMouseGuard; + private bool _relativeMouse; + + private System.Timers.Timer _clickTimer; + private DateTime _firstClickTime; + private int _clickCounter; + + private void ClickTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + _clickTimer.Stop(); + _clickCounter = 0; + } + + private void OnMouseDown(object sender, MouseButtonEventArgs e) + { + e.Handled = true; + + if (e.ChangedButton == MouseButton.Right) + { + /* User aborted */ + EndMouseCapture(true); + return; + } + + if (_clickCounter > 0) + { + /* Double click, enable editor */ + BeginTextBoxUpdate(); + return; + } + + MouseCaptured = CaptureMouse(); + if (!MouseCaptured) + return; + + _startValue = Ratio; + + _lastPos = _originalPos = PointToScreen(Mouse.GetPosition(this)); + Mouse.OverrideCursor = Cursors.None; + _resetPos = new Point(SystemParameters.PrimaryScreenWidth / 2, SystemParameters.PrimaryScreenHeight / 2); + _relativeMouse = NativeMethods.SetCursorPos(_resetPos); + + _clickTimer.Stop(); + _clickCounter++; + _clickTimer.Start(); + _firstClickTime = DateTime.Now; + } + + private void EndMouseCapture(bool reset) + { + try + { + _lostMouseGuard = true; + if (IsMouseCaptured) + ReleaseMouseCapture(); + } + finally + { + _lostMouseGuard = false; + } + + if (!MouseCaptured) + return; + + /* Reset if user only pressed very short */ + if ((DateTime.Now - _firstClickTime).TotalMilliseconds < 100) + reset = true; + + if (reset) + Ratio = _startValue; + + MouseCaptured = false; + Mouse.OverrideCursor = null; + NativeMethods.SetCursorPos(_originalPos); + } + + private void OnLostMouseCapture(object sender, MouseEventArgs e) + { + if (!_lostMouseGuard) + EndMouseCapture(false); + } + + private void OnMouseUp(object sender, MouseButtonEventArgs e) + { + EndMouseCapture(false); + } + + private bool _failing = false; + + private void OnMouseMove(object sender, MouseEventArgs e) + { + if (!MouseCaptured) + return; + + e.Handled = true; + + var pos = PointToScreen(e.GetPosition(this)); + var diff = (pos - (_relativeMouse ? _resetPos : _lastPos)).X; + var multi = SpeedMultiplier.Get(); + + ApplyValueChange(multi * diff); + + /* + * TODO: There seems to be a bug with Synergy where setting the cursor position can fail. + * https://github.com/symless/synergy/issues/5372 + */ + _relativeMouse = NativeMethods.SetCursorPos(_resetPos); + + _lastPos = pos; + } + + public abstract void ApplyValueChange(double ammount); + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + Update(); + } + + protected abstract void CommitEditText(); + + private void BeginTextBoxUpdate() + { + textBox.Text = EditText; + textBox.Visibility = Visibility.Visible; + numberText.Visibility = Visibility.Hidden; + textBox.SelectAll(); + textBox.Focus(); + } + + private void EndTextBoxUpdate(bool commit) + { + if (commit) + { + EditText = textBox.Text; + CommitEditText(); + } + + textBox.Visibility = Visibility.Hidden; + numberText.Visibility = Visibility.Visible; + } + + private void numberText_GotFocus(object sender, RoutedEventArgs e) + { + BeginTextBoxUpdate(); + } + + private void textBox_LostFocus(object sender, RoutedEventArgs e) + { + e.Handled = true; + EndTextBoxUpdate(true); + } + + private void textBox_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + e.Handled = true; + EndTextBoxUpdate(false); + } + else if (e.Key == Key.Escape) + { + e.Handled = true; + EndTextBoxUpdate(true); + } + } + + #region IDisposable Support + + private bool _disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (_clickTimer != null) + { + _clickTimer.Dispose(); + _clickTimer = null; + } + } + + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } + + #endregion + } +} diff --git a/src/Gemini/Framework/Controls/RepeatingButton.cs b/src/Gemini/Framework/Controls/RepeatingButton.cs new file mode 100644 index 00000000..b52ff905 --- /dev/null +++ b/src/Gemini/Framework/Controls/RepeatingButton.cs @@ -0,0 +1,121 @@ +using Caliburn.Micro; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Timers; +using System.Windows.Controls; +using System.Windows.Input; + +namespace Gemini.Framework.Controls +{ + /// + /// Button that automatically repeats when the user keeps pressing it. + /// + public class RepeatingButton : Button, IDisposable + { + /// + /// How many times the button has repeated so far. + /// + public uint RepeatCount + { + get; + private set; + } + + /// + /// A mapping of repetitions to intervals in milliseconds. + /// When the number of repetitions in the dictionaries key is reached, + /// the repetition timers speed is set to the value. This is used to + /// make the repetitions go faster after a certain ammount of + /// repetitions. + /// + public Dictionary RepeatSpeed + { + get; + set; + } + + private Timer _loopTimer; + + public RepeatingButton() + { + RepeatSpeed = new Dictionary { + { 0, 300 }, + { 1, 250 }, + { 5, 100 }, + { 15, 10 }, + { 50, 5 } + }; + + PreviewMouseDown += RepeatingButton_MouseDown; + PreviewMouseUp += RepeatingButton_MouseUp; + + _loopTimer = new Timer(); + _loopTimer.AutoReset = true; + _loopTimer.Elapsed += _loopTimer_Elapsed; + } + + private void RepeatingButton_MouseDown(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton != MouseButton.Left) + return; + + e.Handled = true; + + OnClick(); + + RepeatCount = 1; + _loopTimer.Interval = RepeatSpeed.First().Value; + _loopTimer.Enabled = true; + } + + private void RepeatingButton_MouseUp(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton != MouseButton.Left) + return; + + e.Handled = true; + + RepeatCount = 0; + _loopTimer.Enabled = false; + } + + private void _loopTimer_Elapsed(object sender, ElapsedEventArgs e) + { + RepeatCount++; + + if (RepeatSpeed.ContainsKey(RepeatCount)) + _loopTimer.Interval = RepeatSpeed[RepeatCount]; + + Execute.OnUIThread(() => OnClick()); + } + + #region IDisposable Support + + private bool _disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (_loopTimer != null) + { + _loopTimer.Dispose(); + _loopTimer = null; + } + } + + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } + + #endregion + } +} diff --git a/src/Gemini/Framework/Util/SpeedMultiplier.cs b/src/Gemini/Framework/Util/SpeedMultiplier.cs new file mode 100644 index 00000000..ce16a581 --- /dev/null +++ b/src/Gemini/Framework/Util/SpeedMultiplier.cs @@ -0,0 +1,27 @@ +using System.Windows.Input; + +namespace Gemini.Framework.Util +{ + public static class SpeedMultiplier + { + public static double Get() + { + var multi = 1.0; + + var ctrl = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl); + var shift = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + var alt = Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt); + + if (ctrl) + { + multi = shift ? 0.01 : 0.1; + } + else if (alt) + { + multi = shift ? 100.0 : 10.0; + } + + return multi; + } + } +} diff --git a/src/Gemini/Framework/Win32/NativeMethods.cs b/src/Gemini/Framework/Win32/NativeMethods.cs index b45f9d74..d8a5cf27 100644 --- a/src/Gemini/Framework/Win32/NativeMethods.cs +++ b/src/Gemini/Framework/Win32/NativeMethods.cs @@ -163,12 +163,15 @@ public static extern IntPtr CreateWindowEx( [DllImport("user32.dll")] public static extern bool GetCursorPos(ref NativePoint point); - [DllImport("user32.dll")] + [DllImport("user32.dll", SetLastError = true)] public static extern bool SetCursorPos(int x, int y); [DllImport("user32.dll")] public static extern int ShowCursor(bool bShow); + [DllImport("user32.dll")] + public static extern uint GetDoubleClickTime(); + #endregion #region Helpers @@ -198,6 +201,11 @@ public static int HighWord(int input) return (short) (input >> 16); } + public static bool SetCursorPos(System.Windows.Point pos) + { + return SetCursorPos((int) pos.X, (int) pos.Y); + } + #endregion } } \ No newline at end of file diff --git a/src/Gemini/Gemini.csproj b/src/Gemini/Gemini.csproj index 2a4fc08f..962aa980 100644 --- a/src/Gemini/Gemini.csproj +++ b/src/Gemini/Gemini.csproj @@ -123,12 +123,17 @@ + + + AdvancedSliderBase.xaml + + @@ -164,6 +169,7 @@ + @@ -339,6 +345,10 @@ + + MSBuild:Compile + Designer + MSBuild:Compile Designer @@ -415,6 +425,10 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + MSBuild:Compile Designer diff --git a/src/Gemini/Properties/Resources.de.resx b/src/Gemini/Properties/Resources.de.resx index 64e7ff7c..30dd79e6 100644 --- a/src/Gemini/Properties/Resources.de.resx +++ b/src/Gemini/Properties/Resources.de.resx @@ -282,4 +282,7 @@ Vollbild + + Setzen des Werts fehlgeschlagen: {0} + \ No newline at end of file diff --git a/src/Gemini/Properties/Resources.resx b/src/Gemini/Properties/Resources.resx index 99371a76..2946cb90 100644 --- a/src/Gemini/Properties/Resources.resx +++ b/src/Gemini/Properties/Resources.resx @@ -285,4 +285,7 @@ Full Screen + + Failed to set value: {0} + \ No newline at end of file diff --git a/src/Gemini/Properties/Resources.zh-Hans.resx b/src/Gemini/Properties/Resources.zh-Hans.resx index 0f874dc7..8bba3c87 100644 --- a/src/Gemini/Properties/Resources.zh-Hans.resx +++ b/src/Gemini/Properties/Resources.zh-Hans.resx @@ -267,10 +267,4 @@ 设置的数值失败:{0} - - 全屏 - - - 全屏 - \ No newline at end of file diff --git a/src/Gemini/Themes/Generic.xaml b/src/Gemini/Themes/Generic.xaml index 25badef8..2b9bad23 100644 --- a/src/Gemini/Themes/Generic.xaml +++ b/src/Gemini/Themes/Generic.xaml @@ -72,4 +72,32 @@ - \ No newline at end of file + + + + + diff --git a/src/Gemini/Themes/VS2013/BlueTheme.xaml b/src/Gemini/Themes/VS2013/BlueTheme.xaml index 4f5f1143..35b872ed 100644 --- a/src/Gemini/Themes/VS2013/BlueTheme.xaml +++ b/src/Gemini/Themes/VS2013/BlueTheme.xaml @@ -119,6 +119,13 @@ + + + + + + + diff --git a/src/Gemini/Themes/VS2013/Controls/AdvancedSlider.xaml b/src/Gemini/Themes/VS2013/Controls/AdvancedSlider.xaml new file mode 100644 index 00000000..91f1c065 --- /dev/null +++ b/src/Gemini/Themes/VS2013/Controls/AdvancedSlider.xaml @@ -0,0 +1,29 @@ + + + diff --git a/src/Gemini/Themes/VS2013/Controls/Button.xaml b/src/Gemini/Themes/VS2013/Controls/Button.xaml index 219f20a8..1234bbdf 100644 --- a/src/Gemini/Themes/VS2013/Controls/Button.xaml +++ b/src/Gemini/Themes/VS2013/Controls/Button.xaml @@ -39,4 +39,5 @@