diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 5148a0f3df..5b24af3dee 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -752,6 +752,7 @@ hxx Hyperlink IAction IActivated +IAnimatable IApp IApplication IAppx @@ -796,6 +797,7 @@ IDrop idx IDXGI IDYES +IEasing IEnum IEnumerable IEnumerator diff --git a/src/modules/colorPicker/ColorPickerUI/Behaviors/MoveWindowBehavior.cs b/src/modules/colorPicker/ColorPickerUI/Behaviors/MoveWindowBehavior.cs deleted file mode 100644 index 6a3f096d0c..0000000000 --- a/src/modules/colorPicker/ColorPickerUI/Behaviors/MoveWindowBehavior.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Windows; -using System.Windows.Media.Animation; -using Microsoft.Xaml.Behaviors; - -namespace ColorPicker.Behaviors -{ - public class MoveWindowBehavior : Behavior - { - public static readonly DependencyProperty LeftProperty = DependencyProperty.Register("Left", typeof(double), typeof(MoveWindowBehavior), new PropertyMetadata(new PropertyChangedCallback(LeftPropertyChanged))); - - private static void LeftPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var sender = ((MoveWindowBehavior)d).AssociatedObject; - var move = new DoubleAnimation(sender.Left, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop); - move.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut }; - sender.BeginAnimation(Window.LeftProperty, move, HandoffBehavior.Compose); - } - - public static readonly DependencyProperty TopProperty = DependencyProperty.Register("Top", typeof(double), typeof(MoveWindowBehavior), new PropertyMetadata(new PropertyChangedCallback(TopPropertyChanged))); - - private static void TopPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var sender = ((MoveWindowBehavior)d).AssociatedObject; - var move = new DoubleAnimation(sender.Top, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop); - move.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut }; - sender.BeginAnimation(Window.TopProperty, move, HandoffBehavior.Compose); - } - - public double Left - { - get - { - return (double)GetValue(LeftProperty); - } - - set - { - SetValue(LeftProperty, value); - } - } - - public double Top - { - get - { - return (double)GetValue(TopProperty); - } - - set - { - SetValue(TopProperty, value); - } - } - } -} diff --git a/src/modules/colorPicker/ColorPickerUI/Behaviors/ResizeBehavior.cs b/src/modules/colorPicker/ColorPickerUI/Behaviors/ResizeBehavior.cs index 1f7c3b5857..5424f44acd 100644 --- a/src/modules/colorPicker/ColorPickerUI/Behaviors/ResizeBehavior.cs +++ b/src/modules/colorPicker/ColorPickerUI/Behaviors/ResizeBehavior.cs @@ -11,20 +11,55 @@ namespace ColorPicker.Behaviors { public class ResizeBehavior : Behavior { + // animation behavior variables + // used when size is getting bigger + private static readonly TimeSpan _animationTime = TimeSpan.FromMilliseconds(200); + private static readonly IEasingFunction _easeFunction = new SineEase() { EasingMode = EasingMode.EaseOut }; + + // used when size is getting smaller + private static readonly TimeSpan _animationTimeSmaller = _animationTime; + private static readonly IEasingFunction _easeFunctionSmaller = new QuadraticEase() { EasingMode = EasingMode.EaseIn }; + + private static void CustomAnimation(DependencyProperty prop, IAnimatable sender, double fromValue, double toValue) + { + // if the animation is to/from a value of 0, it will cancel the current animation + DoubleAnimation move = null; + if (toValue > 0 && fromValue > 0) + { + // if getting bigger + if (fromValue < toValue) + { + move = new DoubleAnimation(fromValue, toValue, new Duration(_animationTime), FillBehavior.Stop) + { + EasingFunction = _easeFunction, + }; + } + else + { + move = new DoubleAnimation(fromValue, toValue, new Duration(_animationTimeSmaller), FillBehavior.Stop) + { + EasingFunction = _easeFunctionSmaller, + }; + } + } + + // HandoffBehavior must be SnapshotAndReplace + // Compose does not allow cancellation + sender.BeginAnimation(prop, move, HandoffBehavior.SnapshotAndReplace); + } + public static readonly DependencyProperty WidthProperty = DependencyProperty.Register("Width", typeof(double), typeof(ResizeBehavior), new PropertyMetadata(new PropertyChangedCallback(WidthPropertyChanged))); private static void WidthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var sender = ((ResizeBehavior)d).AssociatedObject; - var move = new DoubleAnimation(sender.Width, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop); - move.Completed += (s, e1) => - { - sender.BeginAnimation(FrameworkElement.WidthProperty, null); - sender.Width = (double)e.NewValue; - }; - move.EasingFunction = new QuadraticEase() { EasingMode = EasingMode.EaseOut }; - sender.BeginAnimation(FrameworkElement.WidthProperty, move, HandoffBehavior.Compose); + var fromValue = sender.Width; + var toValue = (double)e.NewValue; + + // setting Width before animation prevents jumping + sender.Width = toValue; + CustomAnimation(FrameworkElement.WidthProperty, sender, fromValue, toValue); } public static readonly DependencyProperty HeightProperty = DependencyProperty.Register("Height", typeof(double), typeof(ResizeBehavior), new PropertyMetadata(new PropertyChangedCallback(HeightPropertyChanged))); @@ -32,15 +67,13 @@ namespace ColorPicker.Behaviors private static void HeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var sender = ((ResizeBehavior)d).AssociatedObject; - var move = new DoubleAnimation(sender.Height, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop); - move.Completed += (s, e1) => - { - sender.BeginAnimation(FrameworkElement.HeightProperty, null); - sender.Height = (double)e.NewValue; - }; - move.EasingFunction = new QuadraticEase() { EasingMode = EasingMode.EaseOut }; - sender.BeginAnimation(FrameworkElement.HeightProperty, move, HandoffBehavior.Compose); + var fromValue = sender.Height; + var toValue = (double)e.NewValue; + + // setting Height before animation prevents jumping + sender.Height = toValue; + CustomAnimation(FrameworkElement.HeightProperty, sender, fromValue, toValue); } public double Width diff --git a/src/modules/colorPicker/ColorPickerUI/Helpers/ZoomWindowHelper.cs b/src/modules/colorPicker/ColorPickerUI/Helpers/ZoomWindowHelper.cs index ffd61da6ff..ad9564260d 100644 --- a/src/modules/colorPicker/ColorPickerUI/Helpers/ZoomWindowHelper.cs +++ b/src/modules/colorPicker/ColorPickerUI/Helpers/ZoomWindowHelper.cs @@ -18,33 +18,27 @@ namespace ColorPicker.Helpers [Export(typeof(ZoomWindowHelper))] public class ZoomWindowHelper { - private const int ZoomWindowChangeDelayInMS = 50; private const int ZoomFactor = 2; private const int BaseZoomImageSize = 50; private const int MaxZoomLevel = 4; private const int MinZoomLevel = 0; + private static readonly Bitmap _bmp = new Bitmap(BaseZoomImageSize, BaseZoomImageSize, PixelFormat.Format32bppArgb); + private static readonly Graphics _graphics = Graphics.FromImage(_bmp); + private readonly IZoomViewModel _zoomViewModel; private readonly AppStateHandler _appStateHandler; - private readonly IThrottledActionInvoker _throttledActionInvoker; private int _currentZoomLevel; private int _previousZoomLevel; private ZoomWindow _zoomWindow; - private double _lastLeft; - private double _lastTop; - - private double _previousScaledX; - private double _previousScaledY; - [ImportingConstructor] - public ZoomWindowHelper(IZoomViewModel zoomViewModel, AppStateHandler appStateHandler, IThrottledActionInvoker throttledActionInvoker) + public ZoomWindowHelper(IZoomViewModel zoomViewModel, AppStateHandler appStateHandler) { _zoomViewModel = zoomViewModel; _appStateHandler = appStateHandler; - _throttledActionInvoker = throttledActionInvoker; _appStateHandler.AppClosed += AppStateHandler_AppClosed; _appStateHandler.AppHidden += AppStateHandler_AppClosed; } @@ -73,7 +67,7 @@ namespace ColorPicker.Helpers { _currentZoomLevel = 0; _previousZoomLevel = 0; - HideZoomWindow(); + HideZoomWindow(true); } private void SetZoomImage(System.Windows.Point point) @@ -89,28 +83,17 @@ namespace ColorPicker.Helpers { var x = (int)point.X - (BaseZoomImageSize / 2); var y = (int)point.Y - (BaseZoomImageSize / 2); - var rect = new Rectangle(x, y, BaseZoomImageSize, BaseZoomImageSize); - using (var bmp = new Bitmap(rect.Width, rect.Height, PixelFormat.Format32bppArgb)) - { - var g = Graphics.FromImage(bmp); - g.CopyFromScreen(rect.Left, rect.Top, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy); + _graphics.CopyFromScreen(x, y, 0, 0, _bmp.Size, CopyPixelOperation.SourceCopy); - var bitmapImage = BitmapToImageSource(bmp); - - _zoomViewModel.ZoomArea = bitmapImage; - _zoomViewModel.ZoomFactor = 1; - } + _zoomViewModel.ZoomArea = BitmapToImageSource(_bmp); } else { - var enlarge = (_currentZoomLevel - _previousZoomLevel) > 0 ? true : false; - var currentZoomFactor = enlarge ? ZoomFactor : 1.0 / ZoomFactor; - - _zoomViewModel.ZoomFactor *= currentZoomFactor; + _zoomViewModel.ZoomFactor = Math.Pow(ZoomFactor, _currentZoomLevel - 1); } - ShowZoomWindow((int)point.X, (int)point.Y); + ShowZoomWindow(point); } private static BitmapSource BitmapToImageSource(Bitmap bitmap) @@ -130,84 +113,67 @@ namespace ColorPicker.Helpers } } - private void HideZoomWindow() + private void HideZoomWindow(bool fully = false) { if (_zoomWindow != null) { - _zoomWindow.Hide(); + _zoomWindow.Opacity = 0; + _zoomViewModel.DesiredWidth = 0; + _zoomViewModel.DesiredHeight = 0; + + if (fully) + { + _zoomWindow.Hide(); + } } } - private void ShowZoomWindow(int x, int y) + private void ShowZoomWindow(System.Windows.Point point) { - if (_zoomWindow == null) + _zoomWindow ??= new ZoomWindow { - _zoomWindow = new ZoomWindow(); - _zoomWindow.Content = _zoomViewModel; - _zoomWindow.Loaded += ZoomWindow_Loaded; - _zoomWindow.IsVisibleChanged += ZoomWindow_IsVisibleChanged; - } + Content = _zoomViewModel, + Opacity = 0, + }; - // we just started zooming, remember where we opened zoom window - if (_currentZoomLevel == 1 && _previousZoomLevel == 0) - { - var dpi = MonitorResolutionHelper.GetCurrentMonitorDpi(); - _previousScaledX = x / dpi.DpiScaleX; - _previousScaledY = y / dpi.DpiScaleY; - } - - _lastLeft = Math.Floor(_previousScaledX - (BaseZoomImageSize * Math.Pow(ZoomFactor, _currentZoomLevel - 1) / 2)); - _lastTop = Math.Floor(_previousScaledY - (BaseZoomImageSize * Math.Pow(ZoomFactor, _currentZoomLevel - 1) / 2)); - - var justShown = false; if (!_zoomWindow.IsVisible) { - _zoomWindow.Left = _lastLeft; - _zoomWindow.Top = _lastTop; - _zoomViewModel.Height = BaseZoomImageSize; - _zoomViewModel.Width = BaseZoomImageSize; _zoomWindow.Show(); - justShown = true; + } + + if (_zoomWindow.Opacity < 0.5) + { + var halfWidth = _zoomWindow.Width / 2; + var halfHeight = _zoomWindow.Height / 2; + + // usually takes 1-3 iterations to converge + // 5 is just an arbitrary limit to prevent infinite loops + for (var i = 0; i < 5; i++) + { + // mouse position relative to top left of _zoomWindow + var scaledPoint = _zoomWindow.PointFromScreen(point); + + var diffX = scaledPoint.X - halfWidth; + var diffY = scaledPoint.Y - halfHeight; + + // minimum difference that is considered important + const double minDiff = 0.05; + if (Math.Abs(diffX) < minDiff && Math.Abs(diffY) < minDiff) + { + break; + } + + _zoomWindow.Left += diffX; + _zoomWindow.Top += diffY; + } // make sure color picker window is on top of just opened zoom window AppStateHandler.SetTopMost(); + _zoomWindow.Opacity = 1; } - // dirty hack - sometimes when we just show a window on a second monitor with different DPI, - // window position is not set correctly on a first time, we need to "ping" it again to make it appear on the proper location - if (justShown) - { - _zoomWindow.Left = _lastLeft + 1; - _zoomWindow.Top = _lastTop + 1; - SessionEventHelper.Event.ZoomUsed = true; - } - - _throttledActionInvoker.ScheduleAction( - () => - { - _zoomWindow.DesiredLeft = _lastLeft; - _zoomWindow.DesiredTop = _lastTop; - _zoomViewModel.DesiredHeight = BaseZoomImageSize * _zoomViewModel.ZoomFactor; - _zoomViewModel.DesiredWidth = BaseZoomImageSize * _zoomViewModel.ZoomFactor; - }, - ZoomWindowChangeDelayInMS); - } - - private void ZoomWindow_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) - { - // need to set at this point again, to avoid issues moving between screens with different scaling - if ((bool)e.NewValue) - { - _zoomWindow.Left = _lastLeft; - _zoomWindow.Top = _lastTop; - } - } - - private void ZoomWindow_Loaded(object sender, RoutedEventArgs e) - { - // need to call it again at load time, because it does was not dpi aware at the first time of Show() call - _zoomWindow.Left = _lastLeft; - _zoomWindow.Top = _lastTop; + _zoomViewModel.DesiredHeight = BaseZoomImageSize * _zoomViewModel.ZoomFactor; + _zoomViewModel.DesiredWidth = BaseZoomImageSize * _zoomViewModel.ZoomFactor; } private void AppStateHandler_AppClosed(object sender, EventArgs e) diff --git a/src/modules/colorPicker/ColorPickerUI/Views/ZoomView.xaml b/src/modules/colorPicker/ColorPickerUI/Views/ZoomView.xaml index 1c5c0214cb..5c6c617f6f 100644 --- a/src/modules/colorPicker/ColorPickerUI/Views/ZoomView.xaml +++ b/src/modules/colorPicker/ColorPickerUI/Views/ZoomView.xaml @@ -11,6 +11,8 @@ Focusable="False"> - diff --git a/src/modules/colorPicker/ColorPickerUI/ZoomWindow.xaml.cs b/src/modules/colorPicker/ColorPickerUI/ZoomWindow.xaml.cs index 2fac62c8b3..8434b8879d 100644 --- a/src/modules/colorPicker/ColorPickerUI/ZoomWindow.xaml.cs +++ b/src/modules/colorPicker/ColorPickerUI/ZoomWindow.xaml.cs @@ -10,50 +10,16 @@ namespace ColorPicker /// /// Interaction logic for ZoomWindow.xaml /// - public partial class ZoomWindow : Window, INotifyPropertyChanged + public partial class ZoomWindow : Window { - private double _left; - private double _top; - public ZoomWindow() { InitializeComponent(); DataContext = this; - } - public double DesiredLeft - { - get - { - return _left; - } - - set - { - _left = value; - NotifyPropertyChanged(nameof(DesiredLeft)); - } - } - - public double DesiredTop - { - get - { - return _top; - } - - set - { - _top = value; - NotifyPropertyChanged(nameof(DesiredTop)); - } - } - - public event PropertyChangedEventHandler PropertyChanged; - - private void NotifyPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + // must be large enough to fit max zoom + Width = 500; + Height = 500; } } }