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
3 changed files with 292 additions and 150 deletions

View File

@@ -63,8 +63,8 @@ namespace FancyZonesEditor
zone.ZoneIndex = i; zone.ZoneIndex = i;
Canvas.SetLeft(zone, rect.X); Canvas.SetLeft(zone, rect.X);
Canvas.SetTop(zone, rect.Y); Canvas.SetTop(zone, rect.Y);
zone.MinHeight = rect.Height; zone.Height = rect.Height;
zone.MinWidth = rect.Width; zone.Width = rect.Width;
} }
} }
} }

View File

@@ -24,19 +24,19 @@
<ColumnDefinition Width="16"/> <ColumnDefinition Width="16"/>
<ColumnDefinition Width="8"/> <ColumnDefinition Width="8"/>
</Grid.ColumnDefinitions> </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="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="NEResize_DragDelta"/> <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="SWResize_DragDelta"/> <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="SEResize_DragDelta"/> <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="NResize_DragDelta"/> <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="SResize_DragDelta"/> <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="WResize_DragDelta"/> <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="EResize_DragDelta"/> <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"> <DockPanel Grid.Row="1" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="3">
<Button DockPanel.Dock="Right" Padding="8,0" Click="OnClose"> <Button DockPanel.Dock="Right" Padding="8,0" Click="OnClose">
<Image Source="images/ChromeClose.png" Height="24" Width="24" /> <Image Source="images/ChromeClose.png" Height="24" Width="24" />
</Button> </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> </DockPanel>
<Rectangle Fill="LightGray" Grid.Row="3" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="3"/> <Rectangle Fill="LightGray" Grid.Row="3" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="3"/>
<Canvas x:Name="Body" /> <Canvas x:Name="Body" />

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
@@ -16,186 +17,267 @@ namespace FancyZonesEditor
/// </summary> /// </summary>
public partial class CanvasZone : UserControl 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() public CanvasZone()
{ {
InitializeComponent(); 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]; BottomEdge,
if (xDelta < 0) TopEdge,
{ BothEdges,
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;
} }
private void SizeMove(double xDelta, double yDelta) private abstract class SnappyHelperBase
{ {
Int32Rect rect = Model.Zones[ZoneIndex]; public int ScreenW { get; private set; }
if (xDelta < 0)
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) // Remove duplicates and sort
{ keyPositions.Sort();
if ((rect.Width - (int)xDelta) < _minZoneWidth) 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) public abstract void Move(int delta);
{
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;
} }
private void Size(double xDelta, double yDelta) private class SnappyHelperMagnetic : SnappyHelperBase
{ {
Int32Rect rect = Model.Zones[ZoneIndex]; private List<int> magnetZoneSizes;
if (xDelta != 0) private int freePosition;
private int MagnetZoneMaxSize
{ {
int newWidth = rect.Width + (int)xDelta; get => (int)(0.08 * ScreenW);
if (newWidth < _minZoneWidth)
{
newWidth = _minZoneWidth;
}
else if (newWidth > (_settings.WorkArea.Width - rect.X))
{
newWidth = (int)_settings.WorkArea.Width - rect.X;
}
MinWidth = rect.Width = newWidth;
} }
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; freePosition = Position;
magnetZoneSizes = new List<int>();
if (newHeight < _minZoneHeight) 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; 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) protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{ {
Panel.SetZIndex(this, _zIndex++); Canvas.SetZIndex(this, zIndex++);
base.OnPreviewMouseDown(e); 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) if (snappyY != null)
{ {
SizeMove(0, e.VerticalChange); snappyY.Move((int)e.VerticalChange);
Size(e.HorizontalChange, 0); }
}
private void SWResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) UpdateFromSnappyHelpers();
{
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);
} }
private void OnClose(object sender, RoutedEventArgs e) private void OnClose(object sender, RoutedEventArgs e)
@@ -203,5 +285,65 @@ namespace FancyZonesEditor
((Panel)Parent).Children.Remove(this); ((Panel)Parent).Children.Remove(this);
Model.RemoveZoneAt(ZoneIndex); 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;
}
} }
} }