// 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.Collections.Generic; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using FancyZonesEditor.Models; namespace FancyZonesEditor { /// /// Once you've "Commit"ted the starter grid, then the Zones within the grid come to life for you to be able to further subdivide them /// using splitters /// public partial class CanvasZone : UserControl { private readonly int defaultMoveAmount = 10; private readonly int smallMoveAmount = 1; public CanvasZone() { InitializeComponent(); Canvas.SetZIndex(this, zIndex++); SizeChanged += CanvasZone_SizeChanged; } private void CanvasZone_SizeChanged(object sender, SizeChangedEventArgs e) { // using current culture as this is end user facing WidthLabel.Text = Width.ToString(CultureInfo.CurrentCulture); HeightLabel.Text = Height.ToString(CultureInfo.CurrentCulture); } private CanvasLayoutModel model; private int zoneIndex; public enum ResizeMode { BottomEdge, TopEdge, BothEdges, } private abstract class SnappyHelperBase { 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 origin (left/top) of the screen in this (X or Y) dimension /// The size of the screen in this (X or Y) dimension public SnappyHelperBase(IList zones, int zoneIndex, bool isX, ResizeMode mode, int screenAxisOrigin, int screenAxisSize) { 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) { 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); } } } foreach (Rect singleMonitor in App.Overlay.WorkAreas) { int monitorPositionLow = (int)(isX ? singleMonitor.Left : singleMonitor.Top); int monitorPositionHigh = (int)(isX ? singleMonitor.Right : singleMonitor.Bottom); keyPositions.Add(monitorPositionLow - screenAxisOrigin); keyPositions.Add(monitorPositionHigh - screenAxisOrigin); if (mode == ResizeMode.BothEdges) { keyPositions.Add(monitorPositionLow - screenAxisOrigin - zoneAxisSize); keyPositions.Add(monitorPositionHigh - screenAxisOrigin - zoneAxisSize); } } // Remove duplicates and sort keyPositions.Sort(); Snaps = new List(); if (keyPositions.Count > 0) { 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; } public abstract void Move(int delta); } private sealed class SnappyHelperMagnetic : SnappyHelperBase { private List magnetZoneSizes; private int freePosition; private int MagnetZoneMaxSize { get => (int)(0.08 * ScreenW); } public SnappyHelperMagnetic(IList zones, int zoneIndex, bool isX, ResizeMode mode, int screenAxisOrigin, int screenAxisSize) : base(zones, zoneIndex, isX, mode, screenAxisOrigin, screenAxisSize) { freePosition = Position; magnetZoneSizes = new List(); for (int i = 0; i < Snaps.Count; ++i) { 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); } } public override void Move(int delta) { freePosition = Position + delta; int snapId = -1; for (int i = 0; i < Snaps.Count; ++i) { if (Math.Abs(freePosition - Snaps[i]) <= magnetZoneSizes[i]) { snapId = i; break; } } 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 sealed class SnappyHelperNonMagnetic : SnappyHelperBase { public SnappyHelperNonMagnetic(IList zones, int zoneIndex, bool isX, ResizeMode mode, int screenAxisOrigin, int screenAxisSize) : base(zones, zoneIndex, isX, mode, screenAxisOrigin, screenAxisSize) { } public override void Move(int delta) { var pos = Position + delta; Position = Math.Max(Math.Min(MaxValue, pos), MinValue); } } private SnappyHelperBase snappyX; private SnappyHelperBase snappyY; private SnappyHelperMagnetic NewMagneticSnapper(bool isX, ResizeMode mode) { Rect workingArea = App.Overlay.WorkArea; int screenAxisOrigin = (int)(isX ? workingArea.Left : workingArea.Top); int screenAxisSize = (int)(isX ? workingArea.Width : workingArea.Height); return new SnappyHelperMagnetic(Model.Zones, ZoneIndex, isX, mode, screenAxisOrigin, screenAxisSize); } private SnappyHelperNonMagnetic NewNonMagneticSnapper(bool isX, ResizeMode mode) { Rect workingArea = App.Overlay.WorkArea; int screenAxisOrigin = (int)(isX ? workingArea.Left : workingArea.Top); int screenAxisSize = (int)(isX ? workingArea.Width : workingArea.Height); return new SnappyHelperNonMagnetic(Model.Zones, ZoneIndex, isX, mode, screenAxisOrigin, screenAxisSize); } private void UpdateFromSnappyHelpers() { if (ZoneIndex >= Model.Zones.Count) { return; } 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; private const int MinZoneWidth = 64; private const int MinZoneHeight = 72; protected override void OnPreviewMouseDown(MouseButtonEventArgs e) { Canvas.SetZIndex(this, zIndex++); base.OnPreviewMouseDown(e); } private void UniversalDragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { if (snappyX != null) { snappyX.Move((int)e.HorizontalChange); } if (snappyY != null) { snappyY.Move((int)e.VerticalChange); } UpdateFromSnappyHelpers(); } // Corner dragging private void Caption_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { snappyX = NewMagneticSnapper(true, ResizeMode.BothEdges); snappyY = NewMagneticSnapper(false, ResizeMode.BothEdges); } public CanvasLayoutModel Model { get => model; set => model = value; } public int ZoneIndex { get => zoneIndex; set => zoneIndex = value; } public void FocusZone() { Keyboard.Focus(RootBorder); } private void NWResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { snappyX = NewMagneticSnapper(true, ResizeMode.BottomEdge); snappyY = NewMagneticSnapper(false, ResizeMode.BottomEdge); } private void NEResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { snappyX = NewMagneticSnapper(true, ResizeMode.TopEdge); snappyY = NewMagneticSnapper(false, ResizeMode.BottomEdge); } private void SWResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { snappyX = NewMagneticSnapper(true, ResizeMode.BottomEdge); snappyY = NewMagneticSnapper(false, ResizeMode.TopEdge); } private void SEResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { snappyX = NewMagneticSnapper(true, ResizeMode.TopEdge); snappyY = NewMagneticSnapper(false, ResizeMode.TopEdge); } // Edge dragging private void NResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { snappyX = null; snappyY = NewMagneticSnapper(false, ResizeMode.BottomEdge); } private void SResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { snappyX = null; snappyY = NewMagneticSnapper(false, ResizeMode.TopEdge); } private void WResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { snappyX = NewMagneticSnapper(true, ResizeMode.BottomEdge); snappyY = null; } private void EResize_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { snappyX = NewMagneticSnapper(true, ResizeMode.TopEdge); snappyY = null; } private void OnClose(object sender, RoutedEventArgs e) { RemoveZone(); } private void RemoveZone() { ((Panel)Parent).Children.Remove(this); Model.RemoveZoneAt(ZoneIndex); } private void Border_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Tab) { return; } e.Handled = true; if (e.Key == Key.Delete) { RemoveZone(); return; } var moveValue = IsCtrlKeyDown() ? smallMoveAmount : defaultMoveAmount; if (IsShiftKeyDown()) { moveValue = Math.Max(1, moveValue / 2); } if (e.Key == Key.Right) { if (IsShiftKeyDown()) { // Make the zone larger (width) MoveZoneX(moveValue, ResizeMode.TopEdge, ResizeMode.BottomEdge); MoveZoneX(-moveValue, ResizeMode.BottomEdge, ResizeMode.BottomEdge); } else { // Move zone right MoveZoneX(moveValue, ResizeMode.BothEdges, ResizeMode.BothEdges); } } else if (e.Key == Key.Left) { if (IsShiftKeyDown()) { // Make the zone smaller (width) MoveZoneX(-moveValue, ResizeMode.TopEdge, ResizeMode.BottomEdge); MoveZoneX(moveValue, ResizeMode.BottomEdge, ResizeMode.BottomEdge); } else { // Move zone left MoveZoneX(-moveValue, ResizeMode.BothEdges, ResizeMode.BothEdges); } } else if (e.Key == Key.Up) { if (IsShiftKeyDown()) { // Make the zone larger (height) MoveZoneY(moveValue, ResizeMode.TopEdge, ResizeMode.BottomEdge); MoveZoneY(-moveValue, ResizeMode.BottomEdge, ResizeMode.BottomEdge); } else { // Move zone up MoveZoneY(-moveValue, ResizeMode.BothEdges, ResizeMode.BothEdges); } } else if (e.Key == Key.Down) { if (IsShiftKeyDown()) { // Make the zone smaller (height) MoveZoneY(-moveValue, ResizeMode.TopEdge, ResizeMode.BottomEdge); MoveZoneY(moveValue, ResizeMode.BottomEdge, ResizeMode.BottomEdge); } else { // Move zone down MoveZoneY(moveValue, ResizeMode.BothEdges, ResizeMode.BothEdges); } } } private void MoveZoneX(int value, ResizeMode top, ResizeMode bottom) { snappyX = NewNonMagneticSnapper(true, top); snappyY = NewNonMagneticSnapper(false, bottom); snappyX.Move(value); UpdateFromSnappyHelpers(); } private void MoveZoneY(int value, ResizeMode top, ResizeMode bottom) { snappyX = NewNonMagneticSnapper(true, bottom); snappyY = NewNonMagneticSnapper(false, top); snappyY.Move(value); UpdateFromSnappyHelpers(); } private bool IsShiftKeyDown() { if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) { return true; } else { return false; } } private bool IsCtrlKeyDown() { if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) { return true; } else { return false; } } private void Border_PreviewMouseDown(object sender, MouseButtonEventArgs e) { // Set (keyboard)focus on this zone when click Border selectedBorder = sender as Border; selectedBorder.Focus(); Keyboard.Focus(selectedBorder); } } }