From 197bc54ac6964e1be2b8963390718d1726a78449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Sto=C5=A1i=C4=87?= Date: Tue, 10 Mar 2020 15:50:44 +0100 Subject: [PATCH] FancyZones editor magnetic snapping effect (#1503) * FancyZones editor magnetic snapping effect Implemented a solution to Issue #585: FancyZones: Alignment/Snapping/Ruler * Fixed VS complaining about names and access modifiers * Removed reference to unused implementation of snapping in FZE * Converted integer constants to enums in FZE/Canvas * Convert a portion of code to a switch statement * Improved code maintainability * Fixed a screen resolution bug in FZE/Canvas Fixed a bug where the editor doesn't respect the new screen resolution. * Further maintainability improvements * Fixed a compiler warning * Changed some variables to camelCase --- .../FancyZonesEditor/CanvasEditor.xaml.cs | 4 +- .../editor/FancyZonesEditor/CanvasZone.xaml | 18 +- .../FancyZonesEditor/CanvasZone.xaml.cs | 420 ++++++++++++------ 3 files changed, 292 insertions(+), 150 deletions(-) diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/CanvasEditor.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/CanvasEditor.xaml.cs index 344bdeed9a..00ce0a98ca 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/CanvasEditor.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/CanvasEditor.xaml.cs @@ -63,8 +63,8 @@ namespace FancyZonesEditor zone.ZoneIndex = i; Canvas.SetLeft(zone, rect.X); Canvas.SetTop(zone, rect.Y); - zone.MinHeight = rect.Height; - zone.MinWidth = rect.Width; + zone.Height = rect.Height; + zone.Width = rect.Width; } } } diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/CanvasZone.xaml b/src/modules/fancyzones/editor/FancyZonesEditor/CanvasZone.xaml index 03b272cb30..fb3fb316cc 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/CanvasZone.xaml +++ b/src/modules/fancyzones/editor/FancyZonesEditor/CanvasZone.xaml @@ -24,19 +24,19 @@ - - - - - - - - + + + + + + + + - + diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/CanvasZone.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/CanvasZone.xaml.cs index 1d0bc9d9a6..085bb79b46 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/CanvasZone.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/CanvasZone.xaml.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -16,186 +17,267 @@ namespace FancyZonesEditor /// public partial class CanvasZone : UserControl { - public CanvasLayoutModel Model { get; set; } - - public int ZoneIndex { get; set; } - - private readonly Settings _settings = ((App)Application.Current).ZoneSettings; - - private static readonly int _minZoneWidth = 64; - private static readonly int _minZoneHeight = 72; - private static int _zIndex = 0; - public CanvasZone() { InitializeComponent(); - Panel.SetZIndex(this, _zIndex++); + Canvas.SetZIndex(this, zIndex++); } - private void Move(double xDelta, double yDelta) + private readonly Settings _settings = ((App)Application.Current).ZoneSettings; + + private CanvasLayoutModel model; + + private int zoneIndex; + + public enum ResizeMode { - Int32Rect rect = Model.Zones[ZoneIndex]; - if (xDelta < 0) - { - xDelta = Math.Max(xDelta, -rect.X); - } - else if (xDelta > 0) - { - xDelta = Math.Min(xDelta, _settings.WorkArea.Width - rect.Width - rect.X); - } - - if (yDelta < 0) - { - yDelta = Math.Max(yDelta, -rect.Y); - } - else if (yDelta > 0) - { - yDelta = Math.Min(yDelta, _settings.WorkArea.Height - rect.Height - rect.Y); - } - - rect.X += (int)xDelta; - rect.Y += (int)yDelta; - - Canvas.SetLeft(this, rect.X); - Canvas.SetTop(this, rect.Y); - Model.Zones[ZoneIndex] = rect; + BottomEdge, + TopEdge, + BothEdges, } - private void SizeMove(double xDelta, double yDelta) + private abstract class SnappyHelperBase { - Int32Rect rect = Model.Zones[ZoneIndex]; - if (xDelta < 0) + public int ScreenW { get; private set; } + + protected List Snaps { get; private set; } + + protected int MinValue { get; private set; } + + protected int MaxValue { get; private set; } + + public int Position { get; protected set; } + + public ResizeMode Mode { get; private set; } + + /// + /// Initializes a new instance of the class. + /// Just pass it the canvas arguments. Use mode + /// to tell it which edges of the existing masks to use when building its list + /// of snap points, and generally which edges to track. There will be two + /// SnappyHelpers, one for X-coordinates and one for + /// Y-coordinates, they work independently but share the same logic. + /// + /// The list of rectangles describing all zones + /// The index of the zone to track + /// Whether this is the X or Y SnappyHelper + /// One of the three modes of operation (for example: tracking left/right/both edges) + /// The size of the screen in this (X or Y) dimension + public SnappyHelperBase(IList zones, int zoneIndex, bool isX, ResizeMode mode, int screenAxisSize) { - if ((rect.X + xDelta) < 0) + int zonePosition = isX ? zones[zoneIndex].X : zones[zoneIndex].Y; + int zoneAxisSize = isX ? zones[zoneIndex].Width : zones[zoneIndex].Height; + int minAxisSize = isX ? MinZoneWidth : MinZoneHeight; + List keyPositions = new List(); + for (int i = 0; i < zones.Count; ++i) { - xDelta = -rect.X; + if (i != zoneIndex) + { + int ithZonePosition = isX ? zones[i].X : zones[i].Y; + int ithZoneAxisSize = isX ? zones[i].Width : zones[i].Height; + keyPositions.Add(ithZonePosition); + keyPositions.Add(ithZonePosition + ithZoneAxisSize); + if (mode == ResizeMode.BothEdges) + { + keyPositions.Add(ithZonePosition - zoneAxisSize); + keyPositions.Add(ithZonePosition + ithZoneAxisSize - zoneAxisSize); + } + } } - } - else if (xDelta > 0) - { - if ((rect.Width - (int)xDelta) < _minZoneWidth) + + // Remove duplicates and sort + keyPositions.Sort(); + Snaps = new List(); + if (keyPositions.Count > 0) { - xDelta = rect.Width - _minZoneWidth; + Snaps.Add(keyPositions[0]); + for (int i = 1; i < keyPositions.Count; ++i) + { + if (keyPositions[i] != keyPositions[i - 1]) + { + Snaps.Add(keyPositions[i]); + } + } } + + switch (mode) + { + case ResizeMode.BottomEdge: + // We're dragging the low edge, don't go below zero + MinValue = 0; + + // It can't make the zone smaller than minAxisSize + MaxValue = zonePosition + zoneAxisSize - minAxisSize; + Position = zonePosition; + break; + case ResizeMode.TopEdge: + // We're dragging the high edge, don't make the zone smaller than minAxisSize + MinValue = zonePosition + minAxisSize; + + // Don't go off the screen + MaxValue = screenAxisSize; + Position = zonePosition + zoneAxisSize; + break; + case ResizeMode.BothEdges: + // We're moving the window, don't move it below zero + MinValue = 0; + + // Don't go off the screen (this time the lower edge is tracked) + MaxValue = screenAxisSize - zoneAxisSize; + Position = zonePosition; + break; + } + + Mode = mode; + this.ScreenW = screenAxisSize; } - if (yDelta < 0) - { - if ((rect.Y + yDelta) < 0) - { - yDelta = -rect.Y; - } - } - else if (yDelta > 0) - { - if ((rect.Height - (int)yDelta) < _minZoneHeight) - { - yDelta = rect.Height - _minZoneHeight; - } - } - - rect.X += (int)xDelta; - rect.Width -= (int)xDelta; - MinWidth = rect.Width; - - rect.Y += (int)yDelta; - rect.Height -= (int)yDelta; - MinHeight = rect.Height; - - Canvas.SetLeft(this, rect.X); - Canvas.SetTop(this, rect.Y); - Model.Zones[ZoneIndex] = rect; + public abstract void Move(int delta); } - private void Size(double xDelta, double yDelta) + private class SnappyHelperMagnetic : SnappyHelperBase { - Int32Rect rect = Model.Zones[ZoneIndex]; - if (xDelta != 0) + private List magnetZoneSizes; + private int freePosition; + + private int MagnetZoneMaxSize { - int newWidth = rect.Width + (int)xDelta; - - if (newWidth < _minZoneWidth) - { - newWidth = _minZoneWidth; - } - else if (newWidth > (_settings.WorkArea.Width - rect.X)) - { - newWidth = (int)_settings.WorkArea.Width - rect.X; - } - - MinWidth = rect.Width = newWidth; + get => (int)(0.08 * ScreenW); } - if (yDelta != 0) + public SnappyHelperMagnetic(IList zones, int zoneIndex, bool isX, ResizeMode mode, int screenAxisSize) + : base(zones, zoneIndex, isX, mode, screenAxisSize) { - int newHeight = rect.Height + (int)yDelta; - - if (newHeight < _minZoneHeight) + freePosition = Position; + magnetZoneSizes = new List(); + for (int i = 0; i < Snaps.Count; ++i) { - newHeight = _minZoneHeight; + int previous = i == 0 ? 0 : Snaps[i - 1]; + int next = i == Snaps.Count - 1 ? ScreenW : Snaps[i + 1]; + magnetZoneSizes.Add(Math.Min(Snaps[i] - previous, Math.Min(next - Snaps[i], MagnetZoneMaxSize)) / 2); } - else if (newHeight > (_settings.WorkArea.Height - rect.Y)) + } + + public override void Move(int delta) + { + freePosition = Position + delta; + int snapId = -1; + for (int i = 0; i < Snaps.Count; ++i) { - newHeight = (int)_settings.WorkArea.Height - rect.Y; + if (Math.Abs(freePosition - Snaps[i]) <= magnetZoneSizes[i]) + { + snapId = i; + break; + } } - MinHeight = rect.Height = newHeight; + if (snapId == -1) + { + Position = freePosition; + } + else + { + int deadZoneWidth = (magnetZoneSizes[snapId] + 1) / 2; + if (Math.Abs(freePosition - Snaps[snapId]) <= deadZoneWidth) + { + Position = Snaps[snapId]; + } + else if (freePosition < Snaps[snapId]) + { + Position = freePosition + (freePosition - (Snaps[snapId] - magnetZoneSizes[snapId])); + } + else + { + Position = freePosition - ((Snaps[snapId] + magnetZoneSizes[snapId]) - freePosition); + } + } + + Position = Math.Max(Math.Min(MaxValue, Position), MinValue); + } + } + + private SnappyHelperBase snappyX; + private SnappyHelperBase snappyY; + + private SnappyHelperBase NewDefaultSnappyHelper(bool isX, ResizeMode mode, int screenAxisSize) + { + return new SnappyHelperMagnetic(Model.Zones, ZoneIndex, isX, mode, screenAxisSize); + } + + private void UpdateFromSnappyHelpers() + { + Int32Rect rect = Model.Zones[ZoneIndex]; + + if (snappyX != null) + { + if (snappyX.Mode == ResizeMode.BottomEdge) + { + int changeX = snappyX.Position - rect.X; + rect.X += changeX; + rect.Width -= changeX; + } + else if (snappyX.Mode == ResizeMode.TopEdge) + { + rect.Width = snappyX.Position - rect.X; + } + else + { + int changeX = snappyX.Position - rect.X; + rect.X += changeX; + } + + Canvas.SetLeft(this, rect.X); + Width = rect.Width; + } + + if (snappyY != null) + { + if (snappyY.Mode == ResizeMode.BottomEdge) + { + int changeY = snappyY.Position - rect.Y; + rect.Y += changeY; + rect.Height -= changeY; + } + else if (snappyY.Mode == ResizeMode.TopEdge) + { + rect.Height = snappyY.Position - rect.Y; + } + else + { + int changeY = snappyY.Position - rect.Y; + rect.Y += changeY; + } + + Canvas.SetTop(this, rect.Y); + Height = rect.Height; } Model.Zones[ZoneIndex] = rect; } + private static int zIndex = 0; + private const int MinZoneWidth = 64; + private const int MinZoneHeight = 72; + protected override void OnPreviewMouseDown(MouseButtonEventArgs e) { - Panel.SetZIndex(this, _zIndex++); + Canvas.SetZIndex(this, zIndex++); base.OnPreviewMouseDown(e); } - private void NWResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) + private void UniversalDragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { - SizeMove(e.HorizontalChange, e.VerticalChange); - } + if (snappyX != null) + { + snappyX.Move((int)e.HorizontalChange); + } - private void NEResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) - { - SizeMove(0, e.VerticalChange); - Size(e.HorizontalChange, 0); - } + if (snappyY != null) + { + snappyY.Move((int)e.VerticalChange); + } - private void SWResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) - { - SizeMove(e.HorizontalChange, 0); - Size(0, e.VerticalChange); - } - - private void SEResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) - { - Size(e.HorizontalChange, e.VerticalChange); - } - - private void NResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) - { - SizeMove(0, e.VerticalChange); - } - - private void SResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) - { - Size(0, e.VerticalChange); - } - - private void WResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) - { - SizeMove(e.HorizontalChange, 0); - } - - private void EResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) - { - Size(e.HorizontalChange, 0); - } - - private void Caption_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) - { - Move(e.HorizontalChange, e.VerticalChange); + UpdateFromSnappyHelpers(); } private void OnClose(object sender, RoutedEventArgs e) @@ -203,5 +285,65 @@ namespace FancyZonesEditor ((Panel)Parent).Children.Remove(this); Model.RemoveZoneAt(ZoneIndex); } + + // Corner dragging + private void Caption_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) + { + snappyX = NewDefaultSnappyHelper(true, ResizeMode.BothEdges, (int)_settings.WorkArea.Width); + snappyY = NewDefaultSnappyHelper(false, ResizeMode.BothEdges, (int)_settings.WorkArea.Height); + } + + public CanvasLayoutModel Model { get => model; set => model = value; } + + public int ZoneIndex { get => zoneIndex; set => zoneIndex = value; } + + private void NWResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) + { + snappyX = NewDefaultSnappyHelper(true, ResizeMode.BottomEdge, (int)_settings.WorkArea.Width); + snappyY = NewDefaultSnappyHelper(false, ResizeMode.BottomEdge, (int)_settings.WorkArea.Height); + } + + private void NEResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) + { + snappyX = NewDefaultSnappyHelper(true, ResizeMode.TopEdge, (int)_settings.WorkArea.Width); + snappyY = NewDefaultSnappyHelper(false, ResizeMode.BottomEdge, (int)_settings.WorkArea.Height); + } + + private void SWResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) + { + snappyX = NewDefaultSnappyHelper(true, ResizeMode.BottomEdge, (int)_settings.WorkArea.Width); + snappyY = NewDefaultSnappyHelper(false, ResizeMode.TopEdge, (int)_settings.WorkArea.Height); + } + + private void SEResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) + { + snappyX = NewDefaultSnappyHelper(true, ResizeMode.TopEdge, (int)_settings.WorkArea.Width); + snappyY = NewDefaultSnappyHelper(false, ResizeMode.TopEdge, (int)_settings.WorkArea.Height); + } + + // Edge dragging + private void NResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) + { + snappyX = null; + snappyY = NewDefaultSnappyHelper(false, ResizeMode.BottomEdge, (int)_settings.WorkArea.Height); + } + + private void SResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) + { + snappyX = null; + snappyY = NewDefaultSnappyHelper(false, ResizeMode.TopEdge, (int)_settings.WorkArea.Height); + } + + private void WResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) + { + snappyX = NewDefaultSnappyHelper(true, ResizeMode.BottomEdge, (int)_settings.WorkArea.Width); + snappyY = null; + } + + private void EResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) + { + snappyX = NewDefaultSnappyHelper(true, ResizeMode.TopEdge, (int)_settings.WorkArea.Width); + snappyY = null; + } } }