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
This commit is contained in:
Ivan Stošić 2020-03-10 15:50:44 +01:00 committed by GitHub
parent 013a58e634
commit 197bc54ac6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 292 additions and 150 deletions

View file

@ -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;
}
}
}

View file

@ -24,19 +24,19 @@
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="8"/>
</Grid.ColumnDefinitions>
<Thumb x:Name="NWResize" Cursor="SizeNWSE" Background="Black" Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="NWResize_DragDelta"/>
<Thumb x:Name="NEResize" Cursor="SizeNESW" Background="Black" Grid.Row="0" Grid.Column="3" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="NEResize_DragDelta"/>
<Thumb x:Name="SWResize" Cursor="SizeNESW" Background="Black" Grid.Row="4" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="SWResize_DragDelta"/>
<Thumb x:Name="SEResize" Cursor="SizeNWSE" Background="Black" Grid.Row="4" Grid.Column="3" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="SEResize_DragDelta"/>
<Thumb x:Name="NResize" Cursor="SizeNS" Background="Black" Margin="1,0,1,0" Grid.Row="0" Grid.Column="2" DragDelta="NResize_DragDelta"/>
<Thumb x:Name="SResize" Cursor="SizeNS" Background="Black" Margin="1,0,1,0" Grid.Row="5" Grid.Column="2" DragDelta="SResize_DragDelta"/>
<Thumb x:Name="WResize" Cursor="SizeWE" Background="Black" Margin="0,1,0,1" Grid.Row="2" Grid.Column="0" Grid.RowSpan="2" DragDelta="WResize_DragDelta"/>
<Thumb x:Name="EResize" Cursor="SizeWE" Background="Black" Margin="0,1,0,1" Grid.Row="2" Grid.Column="4" Grid.RowSpan="2" DragDelta="EResize_DragDelta"/>
<Thumb x:Name="NWResize" Cursor="SizeNWSE" Background="Black" Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="UniversalDragDelta" DragStarted="NWResize_DragStarted"/>
<Thumb x:Name="NEResize" Cursor="SizeNESW" Background="Black" Grid.Row="0" Grid.Column="3" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="UniversalDragDelta" DragStarted="NEResize_DragStarted"/>
<Thumb x:Name="SWResize" Cursor="SizeNESW" Background="Black" Grid.Row="4" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="UniversalDragDelta" DragStarted="SWResize_DragStarted"/>
<Thumb x:Name="SEResize" Cursor="SizeNWSE" Background="Black" Grid.Row="4" Grid.Column="3" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="UniversalDragDelta" DragStarted="SEResize_DragStarted"/>
<Thumb x:Name="NResize" Cursor="SizeNS" Background="Black" Margin="1,0,1,0" Grid.Row="0" Grid.Column="2" DragDelta="UniversalDragDelta" DragStarted="NResize_DragStarted"/>
<Thumb x:Name="SResize" Cursor="SizeNS" Background="Black" Margin="1,0,1,0" Grid.Row="5" Grid.Column="2" DragDelta="UniversalDragDelta" DragStarted="SResize_DragStarted"/>
<Thumb x:Name="WResize" Cursor="SizeWE" Background="Black" Margin="0,1,0,1" Grid.Row="2" Grid.Column="0" Grid.RowSpan="2" DragDelta="UniversalDragDelta" DragStarted="WResize_DragStarted"/>
<Thumb x:Name="EResize" Cursor="SizeWE" Background="Black" Margin="0,1,0,1" Grid.Row="2" Grid.Column="4" Grid.RowSpan="2" DragDelta="UniversalDragDelta" DragStarted="EResize_DragStarted"/>
<DockPanel Grid.Row="1" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="3">
<Button DockPanel.Dock="Right" Padding="8,0" Click="OnClose">
<Image Source="images/ChromeClose.png" Height="24" Width="24" />
</Button>
<Thumb x:Name="Caption" Cursor="SizeAll" Background="DarkGray" DragDelta="Caption_DragDelta"/>
<Thumb x:Name="Caption" Cursor="SizeAll" Background="DarkGray" DragDelta="UniversalDragDelta" DragStarted="Caption_DragStarted"/>
</DockPanel>
<Rectangle Fill="LightGray" Grid.Row="3" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="3"/>
<Canvas x:Name="Body" />

View file

@ -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
/// </summary>
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<int> 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; }
/// <summary>
/// Initializes a new instance of the <see cref="SnappyHelperBase"/> 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.
/// </summary>
/// <param name="zones">The list of rectangles describing all zones</param>
/// <param name="zoneIndex">The index of the zone to track</param>
/// <param name="isX"> Whether this is the X or Y SnappyHelper</param>
/// <param name="mode"> One of the three modes of operation (for example: tracking left/right/both edges)</param>
/// <param name="screenAxisSize"> The size of the screen in this (X or Y) dimension</param>
public SnappyHelperBase(IList<Int32Rect> 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<int> keyPositions = new List<int>();
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<int>();
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<int> 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<Int32Rect> 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<int>();
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;
}
}
}