|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
namespace System.Windows.Forms.Design.Behavior;
/// <summary>
/// The DragAssistanceManager, for lack of a better name, is responsible for integrating SnapLines
/// into the DragBehavior. At the beginning of a DragBehavior this class is instantiated and
/// at every mouse move this class is called and given the opportunity to adjust the position of the drag.
/// The DragAssistanceManager needs to work as fast as possible - so not to interrupt a drag operation.
/// Because of this, this class has many global variables that are re-used,
/// in hopes to limit the # of allocations per mouse move / drag operation. Also,
/// for loops are used extensively (instead of foreach calls) to eliminate the creation of an enumerator.
/// </summary>
internal sealed partial class DragAssistanceManager
{
private readonly BehaviorService _behaviorService;
private readonly IServiceProvider _serviceProvider;
private readonly Graphics _graphics; // graphics to the adornerwindow
private Point _dragOffset; // the offset from the new drag pos compared to the last
private Rectangle _cachedDragRect; // used to store drag rect between erasing & waiting to render
private readonly Pen _edgePen = SystemPens.Highlight;
private readonly bool _disposeEdgePen;
private readonly Pen _baselinePen = new(Color.Fuchsia);
// These are global lists of all the existing vertical and horizontal snaplineson the designer's surface
// excluding the targetControl. All SnapLine coords in these lists have been properly
// adjusted for the AdornerWindow coords.
private readonly List<SnapLine> _verticalSnapLines = [];
private readonly List<SnapLine> _horizontalSnapLines = [];
// These are SnapLines that represent our target control.
private readonly List<SnapLine> _targetVerticalSnapLines = [];
private readonly List<SnapLine> _targetHorizontalSnapLines = [];
// This is a list of all the different type of SnapLines our target control has.
// When compiling our global SnapLine lists, if we see a SnapLineType that doesn't exist on our target
// - we can safely ignore it
private readonly List<SnapLineType> _targetSnapLineTypes = [];
// These are created in our init() method (so we don't have to recreate them for every mousemove).
// These arrays represent the closest distance to any snap point on our target control. Once these are calculated
// - we can:
// 1) remove anything > than snapDistance and
// 2) determine the smallest distanceoverall
private int[] _verticalDistances;
private int[] _horizontalDistances;
// These are cleared and populated on every mouse move.
// These lists contain all the new vertical and horizontal lines we need to draw.
// At the end of each mouse move - these lines are stored off in the vertLines and horzLines arrays.
// This way - we can keep track of old snap lines and can avoid erasing and redrawing the same line. HA.
private readonly List<Line> _tempVertLines = [];
private readonly List<Line> _tempHorzLines = [];
private Line[] _vertLines = [];
private Line[] _horzLines = [];
// When we draw snap lines - we only draw lines from the targetControl to the control we're snapping to.
// To do this, we'll keep a dictionary... format: snapLineToBounds[SnapLine]=ControlBounds.
private readonly Dictionary<SnapLine, Rectangle> _snapLineToBounds = [];
// We remember the last set of (vert & horz) lines we draw so that we can push them to the beh. svc.
// From there, if we receive a test hook message requesting these - we got 'em
private Line[]? _recentLines;
private readonly Image? _backgroundImage; // instead of calling .invalidate on the windows below us,
// we'll just draw over w/the background image
private const int SnapDistance = 8; // default snapping distance (pixels)
private int _snapPointX, _snapPointY; // defines the snap adjustment that needs to be made
// during the mousemove/drag operation
private const int INVALID_VALUE = 0x1111; // used to represent 'un-set' distances
private readonly bool _resizing; // Are we resizing?
private readonly bool _ctrlDrag; // Are we in a ctrl-drag?
/// <summary>
/// Internal constructor called that only takes a service provider.
/// Here it is assumed that all painting will be done to the AdornerWindow and
/// that there are no target controls to exclude from snapping.
/// </summary>
internal DragAssistanceManager(IServiceProvider serviceProvider)
: this(
serviceProvider,
graphics: null,
dragComponents: null,
backgroundImage: null,
resizing: false,
ctrlDrag: false)
{
}
/// <summary>
/// Internal constructor that takes the service provider and the list of dragComponents.
/// </summary>
internal DragAssistanceManager(IServiceProvider serviceProvider, List<IComponent> dragComponents)
: this(
serviceProvider,
graphics: null,
dragComponents,
backgroundImage: null,
resizing: false,
ctrlDrag: false)
{
}
/// <summary>
/// Internal constructor that takes the service provider, the list of dragComponents, and a boolean
/// indicating that we are resizing.
/// </summary>
internal DragAssistanceManager(IServiceProvider serviceProvider, List<IComponent> dragComponents, bool resizing)
: this(
serviceProvider,
graphics: null,
dragComponents,
backgroundImage: null,
resizing,
ctrlDrag: false)
{
}
/// <summary>
/// Internal constructor called by DragBehavior.
/// </summary>
internal DragAssistanceManager(
IServiceProvider serviceProvider,
Graphics? graphics,
List<IComponent>? dragComponents,
Image? backgroundImage,
bool ctrlDrag)
: this(
serviceProvider,
graphics,
dragComponents,
backgroundImage,
resizing: false,
ctrlDrag)
{
}
/// <summary>
/// Internal constructor called by DragBehavior.
/// </summary>
internal DragAssistanceManager(
IServiceProvider serviceProvider,
Graphics? graphics,
List<IComponent>? dragComponents,
Image? backgroundImage,
bool resizing,
bool ctrlDrag)
{
_serviceProvider = serviceProvider;
_behaviorService = serviceProvider.GetRequiredService<BehaviorService>();
if (!(serviceProvider.GetService(typeof(IDesignerHost)) is IDesignerHost host) || _behaviorService is null)
{
Debug.Fail("Cannot get DesignerHost or BehaviorService");
return;
}
if (graphics is null)
{
_graphics = _behaviorService.AdornerWindowGraphics;
}
else
{
_graphics = graphics;
}
if (serviceProvider.GetService(typeof(IUIService)) is IUIService uiService)
{
// Can't use 'as' here since Color is a value type
if (uiService.Styles["VsColorSnaplines"] is Color snaplinesColor)
{
_edgePen = new Pen(snaplinesColor);
_disposeEdgePen = true;
}
if (uiService.Styles["VsColorSnaplinesTextBaseline"] is Color snaplinesTextBaselineColor)
{
_baselinePen.Dispose();
_baselinePen = new Pen(snaplinesTextBaselineColor);
}
}
_backgroundImage = backgroundImage;
_resizing = resizing;
_ctrlDrag = ctrlDrag;
Initialize(dragComponents, host);
}
/// <summary>
/// Adjusts then adds each snap line the designer has to offer to either our global horizontal and
/// vertical lists or our target lists. Note that we also keep track of our target snapline types
/// - 'cause we can safely ignore all other types.
/// If valid target is <see langword="false"/>
/// - then we don't yet know what we're snapping against
/// - so we'll exclude the check below to skip unwanted snap line types.
/// </summary>
private void AddSnapLines(ControlDesigner controlDesigner, List<SnapLine> horizontalList, List<SnapLine> verticalList, bool isTarget, bool validTarget)
{
IList<SnapLine> snapLines = controlDesigner.SnapLinesInternal;
// Used for padding snaplines
Rectangle controlRect = controlDesigner.Control.ClientRectangle;
// Used for all others
Rectangle controlBounds = controlDesigner.Control.Bounds;
// Now map the location
controlBounds.Location = controlRect.Location = _behaviorService.ControlToAdornerWindow(controlDesigner.Control);
// Remember the offset -- we need those later
int xOffset = controlBounds.Left;
int yOffset = controlBounds.Top;
// THIS IS ONLY NEEDED FOR PADDING SNAPLINES
// We need to adjust the bounds to the client area.
// This is so that we don't include borders + titlebar in the snaplines.
// In order to add padding, we need to get the offset from the
// usable client area of our control and the actual origin of our control. In other words:
// how big is the non-client area here? Ex: we want to add padding on a form to the insides of the
// borders and below the titlebar.
Point offset = controlDesigner.GetOffsetToClientArea();
controlRect.X += offset.X; // offset for non-client area
controlRect.Y += offset.Y; // offset for non-client area
// Adjust each snapline to local coords and add it to our global list
foreach (SnapLine snapLine in snapLines)
{
if (isTarget)
{
// we will remove padding snaplines from targets - it doesn't make sense to snap to the target's padding lines
if (snapLine.Filter is not null && snapLine.Filter.StartsWith(SnapLine.Padding, StringComparison.Ordinal))
{
continue;
}
if (validTarget && !_targetSnapLineTypes.Contains(snapLine.SnapLineType))
{
_targetSnapLineTypes.Add(snapLine.SnapLineType);
}
}
else
{
if (validTarget && !_targetSnapLineTypes.Contains(snapLine.SnapLineType))
{
continue;
}
// store off the bounds in our dictionary, so if we draw snaplines we know the length
// of the line we need to remember different bounds based on what type of snapline this is.
if ((snapLine.Filter is not null) && snapLine.Filter.StartsWith(SnapLine.Padding, StringComparison.Ordinal))
{
_snapLineToBounds.Add(snapLine, controlRect);
}
else
{
_snapLineToBounds.Add(snapLine, controlBounds);
}
}
if (snapLine.IsHorizontal)
{
snapLine.AdjustOffset(yOffset);
horizontalList.Add(snapLine);
}
else
{
snapLine.AdjustOffset(xOffset);
verticalList.Add(snapLine);
}
}
}
/// <summary>
/// Build up a distance array of all same-type-alignment pts to the closest point on our targetControl.
/// Also, keep track of the smallest distance overall.
/// </summary>
private int BuildDistanceArray(List<SnapLine> snapLines, List<SnapLine> targetSnapLines, int[] distances, Rectangle dragBounds)
{
int smallestDistance = INVALID_VALUE;
int highestPriority = 0;
for (int i = 0; i < snapLines.Count; i++)
{
SnapLine snapLine = snapLines[i];
if (IsMarginOrPaddingSnapLine(snapLine))
{
// validate margin and padding snaplines (to make sure it intersects with the dragbounds) if not, skip this guy
if (!ValidateMarginOrPaddingLine(snapLine, dragBounds))
{
distances[i] = INVALID_VALUE;
continue;
}
}
int smallestDelta = INVALID_VALUE; // some large #
for (int j = 0; j < targetSnapLines.Count; j++)
{
SnapLine targetSnapLine = targetSnapLines[j];
if (SnapLine.ShouldSnap(snapLine, targetSnapLine))
{
int delta = targetSnapLine.Offset - snapLine.Offset;
if (Math.Abs(delta) < Math.Abs(smallestDelta))
{
smallestDelta = delta;
}
}
}
distances[i] = smallestDelta;
int pri = (int)snapLines[i].Priority;
// save off this delta for the overall smallest delta! Need to check the priority
// here as well if the distance is the same. E.g. smallestDistance so far is 1,
// for a Low snapline. We now find another distance of -1, for a Medium snapline.
// The old check if (Math.Abs(smallestDelta) < Math.Abs(smallestDistance))
// would not set smallestDistance to -1, since the ABSOLUTE values are the same.
// Since the return value is used to physically move the control,
// we would move the control in the direction of the Low snapline,
// but draw the Medium snapline in the opposite direction.
if ((Math.Abs(smallestDelta) < Math.Abs(smallestDistance)) ||
((Math.Abs(smallestDelta) == Math.Abs(smallestDistance)) && (pri > highestPriority)))
{
smallestDistance = smallestDelta;
if (pri != (int)SnapLinePriority.Always)
{
highestPriority = pri;
}
}
}
return smallestDistance;
}
/// <summary>
/// Here, we erase all of our old horizontal and vertical snaplines UNLESS they are also contained
/// in our tempHorzLines or tempVertLines arrays
/// - if they are - then erasing them would be redundant (since we know we want to draw them on this mousemove)
/// </summary>
private Line[] EraseOldSnapLines(Line[] lines, List<Line>? tempLines)
{
if (lines is not null)
{
for (int i = 0; i < lines.Length; i++)
{
bool foundMatch = false;
Line line = lines[i];
Rectangle invalidRect;
if (tempLines is not null)
{
for (int j = 0; j < tempLines.Count; j++)
{
if (line.LineType != tempLines[j].LineType)
{
// If the lines are not the same type, then we should forcefully try to remove it.
// Say you have a Panel with a Button in it.
// By default Panel.Padding = 0, and Button.Margin = 3.
// As you move the button to the left,
// you will first get the combined LEFT margin+padding snap line.
// If you keep moving the button, you will now snap to the Left edge,
// and you will get the Blue snapline.
// You now move the button back to the right,
// and you will immediately snap to the LEFT Padding snapline.
// But what's gonna happen. Both the old (Left) snapline,
// and the LEFT Padding snapline (remember these are the panels)
// have the same coordinates, since Panel.Padding is 0.
// Thus Line.GetDiffs will return a non-null diffs.
// BUT e.g the first line will result in an invalidRect of (x1,y1,0,0),
// this we end up invalidating only a small portion of the existing Blue (left) Snapline.
// That's actually not okay since VERTICAL (e.g. LEFT) padding snaplines actually
// end up getting drawn HORIZONTALLY - thus we didn't really invalidate correctly.
continue;
}
Line[]? diffs = Line.GetDiffs(line, tempLines[j]);
if (diffs is not null)
{
for (int k = 0; k < diffs.Length; k++)
{
invalidRect = new Rectangle(diffs[k].X1, diffs[k].Y1, diffs[k].X2 - diffs[k].X1, diffs[k].Y2 - diffs[k].Y1);
invalidRect.Inflate(1, 1);
if (_backgroundImage is not null)
{
_graphics.DrawImage(_backgroundImage, invalidRect, invalidRect, GraphicsUnit.Pixel);
}
else
{
_behaviorService.Invalidate(invalidRect);
}
}
foundMatch = true;
break;
}
}
}
if (!foundMatch)
{
invalidRect = new Rectangle(line.X1, line.Y1, line.X2 - line.X1, line.Y2 - line.Y1);
invalidRect.Inflate(1, 1);
if (_backgroundImage is not null)
{
_graphics.DrawImage(_backgroundImage, invalidRect, invalidRect, GraphicsUnit.Pixel);
}
else
{
_behaviorService.Invalidate(invalidRect);
}
}
}
}
if (tempLines is not null)
{
// Now, store off all the new lines (from the temp structures),
// so next time around (next mousemove message) we know which lines to erase and which ones to keep
lines = new Line[tempLines.Count];
tempLines.CopyTo(lines);
}
else
{
lines = [];
}
return lines;
}
internal void EraseSnapLines()
{
EraseOldSnapLines(_vertLines, tempLines: null);
EraseOldSnapLines(_horzLines, tempLines: null);
}
/// <summary>
/// This internal method returns a snap line[] representing the last SnapLines that were rendered
/// before this algorithm was stopped (usually by an OnMouseUp). This is used for storing additional
/// toolbox drag/drop info and testing hooks.
/// </summary>
internal Line[] GetRecentLines()
{
if (_recentLines is not null)
{
return _recentLines;
}
return [];
}
private void IdentifyAndStoreValidLines(List<SnapLine> snapLines, int[] distances, Rectangle dragBounds, int smallestDistance)
{
int highestPriority = 1; // low
// identify top pri
for (int i = 0; i < distances.Length; i++)
{
if (distances[i] == smallestDistance)
{
int pri = (int)snapLines[i].Priority;
if ((pri > highestPriority) && (pri != (int)SnapLinePriority.Always))
{ // Always is a special category
highestPriority = pri;
}
}
}
// store all snapLines equal to the smallest distance (of the highest priority)
for (int i = 0; i < distances.Length; i++)
{
if ((distances[i] == smallestDistance) &&
(((int)snapLines[i].Priority == highestPriority) ||
((int)snapLines[i].Priority == (int)SnapLinePriority.Always)))
{ // always render SnapLines with Priority.Always which has the same distance.
StoreSnapLine(snapLines[i], dragBounds);
}
}
}
// Returns true of this child component (off the root control) should add its snaplines to the collection
private bool AddChildCompSnaplines(IComponent comp, List<IComponent>? dragComponents, Rectangle clipBounds, Control? targetControl)
{
if (!(comp is Control control) || // has to be a control to get snaplines
(dragComponents is not null && dragComponents.Contains(comp) && !_ctrlDrag) || // cannot be something that we are dragging, unless we are in a ctrlDrag
IsChildOfParent(control, targetControl) || // cannot be a child of the control we will drag
!clipBounds.IntersectsWith(control.Bounds) || // has to be partially visible on the rootcomp's surface
control.Parent is null || // control must have a parent.
!control.Visible)
{ // control itself has to be visible -- we do mean visible, not ShadowedVisible
return false;
}
Control c = control;
if (!c.Equals(targetControl))
{
if (_serviceProvider.GetService(typeof(IDesignerHost)) is IDesignerHost host)
{
if (host.GetDesigner(c) is ControlDesigner controlDesigner)
{
return controlDesigner.ControlSupportsSnaplines;
}
}
}
return true;
}
// Returns true if we should add snaplines for this control
private bool AddControlSnaplinesWhenResizing(ControlDesigner designer, Control control, Control? targetControl)
{
// do not add snaplines if we are resizing the control is a container control with
// AutoSize set to true and the control is the parent of the targetControl
if (_resizing &&
(designer is ParentControlDesigner) &&
(control.AutoSize) &&
(targetControl is not null) &&
(targetControl.Parent is not null) &&
(targetControl.Parent.Equals(control)))
{
return false;
}
return true;
}
/// <summary>
/// Initializes our class - we cache all snap lines for every control we can find. This is done for perf. reasons.
/// </summary>
[MemberNotNull(nameof(_verticalDistances))]
[MemberNotNull(nameof(_horizontalDistances))]
private void Initialize(List<IComponent>? dragComponents, IDesignerHost host)
{
// our targetControl will always be the 0th component in our dragComponents array list (a.k.a. the primary selected component).
Control? targetControl = null;
if (dragComponents is not null && dragComponents.Count > 0)
{
targetControl = dragComponents[0] as Control;
}
Control rootControl = (Control)host.RootComponent;
// the clipping bounds will be used to ignore all controls that are
// completely outside of our rootcomponent's bounds
// -this way we won't end up snapping to controls that are not visible on the form's surface
Rectangle clipBounds = new(0, 0, rootControl.ClientRectangle.Width, rootControl.ClientRectangle.Height);
clipBounds.Inflate(-1, -1);
// determine the screen offset from our rootComponent to the AdornerWindow
// (since all drag notification coords will be in adorner window coords)
if (targetControl is not null)
{
_dragOffset = _behaviorService.ControlToAdornerWindow(targetControl);
}
else
{
_dragOffset = _behaviorService.MapAdornerWindowPoint(rootControl.Handle, Point.Empty);
if (rootControl.Parent is not null && rootControl.Parent.IsMirrored)
{
_dragOffset.Offset(-rootControl.Width, 0);
}
}
if (targetControl is not null)
{
bool disposeDesigner = false;
// Get all the target snapline information we need to create one then.
ControlDesigner? designer;
if (host.GetDesigner(targetControl) is not ControlDesigner controlDesigner)
{
designer = TypeDescriptor.CreateDesigner(targetControl, typeof(IDesigner)) as ControlDesigner;
if (designer is not null)
{
// Make sure the control is not forced visible
designer.ForceVisible = false;
designer.Initialize(targetControl);
disposeDesigner = true;
}
}
else
{
designer = controlDesigner;
}
if (designer is not null)
{
AddSnapLines(designer, _targetHorizontalSnapLines, _targetVerticalSnapLines, true, targetControl is not null);
if (disposeDesigner)
{
designer.Dispose();
}
}
}
// get SnapLines for all our children (nested too) off our root control
foreach (IComponent comp in host.Container.Components)
{
if (!AddChildCompSnaplines(comp, dragComponents, clipBounds, targetControl))
{
continue;
}
if (host.GetDesigner(comp) is ControlDesigner designer)
{
if (AddControlSnaplinesWhenResizing(designer, (Control)comp, targetControl))
{
AddSnapLines(designer, _horizontalSnapLines, _verticalSnapLines, false, targetControl is not null);
}
// Does the designer have internal control designers for which we need to add snaplines
// (like SplitPanelContainer, ToolStripContainer)
int numInternalDesigners = designer.NumberOfInternalControlDesigners();
for (int i = 0; i < numInternalDesigners; i++)
{
ControlDesigner? internalDesigner = designer.InternalControlDesigner(i);
if (internalDesigner is not null &&
AddChildCompSnaplines(internalDesigner.Component, dragComponents, clipBounds, targetControl) &&
AddControlSnaplinesWhenResizing(internalDesigner, (Control)internalDesigner.Component, targetControl))
{
AddSnapLines(internalDesigner, _horizontalSnapLines, _verticalSnapLines, false, targetControl is not null);
}
}
}
}
// Now that we know how many snaplines everyone has, we can create temp arrays now.
// Intentionally avoiding this on every mousemove.
_verticalDistances = new int[_verticalSnapLines.Count];
_horizontalDistances = new int[_horizontalSnapLines.Count];
}
/// <summary>
/// Helper function that determines if the child control is related to the parent.
/// </summary>
private static bool IsChildOfParent(Control? child, Control? parent)
{
if (child is null || parent is null)
{
return false;
}
Control? currentParent = child.Parent;
while (currentParent is not null)
{
if (currentParent.Equals(parent))
{
return true;
}
currentParent = currentParent.Parent;
}
return false;
}
/// <summary>
/// Helper function that identifies margin or padding snaplines
/// </summary>
private static bool IsMarginOrPaddingSnapLine(SnapLine snapLine)
{
return snapLine.Filter is not null
&& (snapLine.Filter.StartsWith(SnapLine.Margin, StringComparison.Ordinal)
|| snapLine.Filter.StartsWith(SnapLine.Padding, StringComparison.Ordinal));
}
/// <summary>
/// Returns the offset in which the targetControl's rect needs to be re-positioned
/// (given the direction by 'directionOffset') in order to align with the nearest possible snapline.
/// This is called by commandSet during keyboard movements to auto-snap the control around the designer.
/// </summary>
internal Point OffsetToNearestSnapLocation(Control targetControl, IList targetSnaplines, Point directionOffset)
{
_targetHorizontalSnapLines.Clear();
_targetVerticalSnapLines.Clear();
// manually add our snaplines as targets
foreach (SnapLine snapline in targetSnaplines)
{
if (snapline.IsHorizontal)
{
_targetHorizontalSnapLines.Add(snapline);
}
else
{
_targetVerticalSnapLines.Add(snapline);
}
}
return OffsetToNearestSnapLocation(targetControl, directionOffset);
}
/// <summary>
/// Returns the offset in which the targetControl's rect needs to be re-positioned
/// (given the direction by 'directionOffset') in order to align with the nearest possible snapline.
/// This is called by commandSet during keyboard movements to auto-snap the control around the designer.
/// </summary>
internal Point OffsetToNearestSnapLocation(Control targetControl, Point directionOffset)
{
Point offset = Point.Empty;
Rectangle currentBounds = new(_behaviorService.ControlToAdornerWindow(targetControl), targetControl.Size);
if (directionOffset.X != 0)
{// movement somewhere in the x dir
// first, build up our distance array
BuildDistanceArray(_verticalSnapLines, _targetVerticalSnapLines, _verticalDistances, currentBounds);
// now start with the smallest distance and find the first snapline we would intercept given our horizontal direction
int minRange = directionOffset.X < 0 ? 0 : currentBounds.X;
int maxRange = directionOffset.X < 0 ? currentBounds.Right : int.MaxValue;
offset.X = FindSmallestValidDistance(_verticalSnapLines, _verticalDistances, minRange, maxRange, directionOffset.X);
if (offset.X != 0)
{
// store off the line structs for actual rendering
IdentifyAndStoreValidLines(_verticalSnapLines, _verticalDistances, currentBounds, offset.X);
if (directionOffset.X < 0)
{
offset.X *= -1;
}
}
}
if (directionOffset.Y != 0)
{// movement somewhere in the y dir
// first, build up our distance array
BuildDistanceArray(_horizontalSnapLines, _targetHorizontalSnapLines, _horizontalDistances, currentBounds);
// now start with the smallest distance and find the first snapline we would intercept given our horizontal direction
int minRange = directionOffset.Y < 0 ? 0 : currentBounds.Y;
int maxRange = directionOffset.Y < 0 ? currentBounds.Bottom : int.MaxValue;
offset.Y = FindSmallestValidDistance(_horizontalSnapLines, _horizontalDistances, minRange, maxRange, directionOffset.Y);
if (offset.Y != 0)
{
// store off the line structs for actual rendering
IdentifyAndStoreValidLines(_horizontalSnapLines, _horizontalDistances, currentBounds, offset.Y);
if (directionOffset.Y < 0)
{
offset.Y *= -1;
}
}
}
if (!offset.IsEmpty)
{
// setup the cached info for drawing
_cachedDragRect = currentBounds;
_cachedDragRect.Offset(offset.X, offset.Y);
if (offset.X != 0)
{
_vertLines = new Line[_tempVertLines.Count];
_tempVertLines.CopyTo(_vertLines);
}
if (offset.Y != 0)
{
_horzLines = new Line[_tempHorzLines.Count];
_tempHorzLines.CopyTo(_horzLines);
}
}
return offset;
}
private static int FindSmallestValidDistance(List<SnapLine> snapLines, int[] distances, int min, int max, int direction)
{
// loop while we still have valid distance to check and try to find the smallest valid distance
while (true)
{
// get the next smallest snapline index
int snapLineIndex = SmallestDistanceIndex(distances, direction, out int distanceValue);
if (snapLineIndex == INVALID_VALUE)
{
// ran out of valid distances
break;
}
if (IsWithinValidRange(snapLines[snapLineIndex].Offset, min, max))
{
// found it - make sure we restore the original value for rendering the snap line in the future
distances[snapLineIndex] = distanceValue;
return distanceValue;
}
}
return 0;
}
private static bool IsWithinValidRange(int offset, int min, int max) => offset > min && offset < max;
private static int SmallestDistanceIndex(int[] distances, int direction, out int distanceValue)
{
distanceValue = INVALID_VALUE;
int smallestIndex = INVALID_VALUE;
// check for valid array
if (distances.Length == 0)
{
return smallestIndex;
}
// find the next smallest
for (int i = 0; i < distances.Length; i++)
{
// If a distance is 0 or if it is to our left and we're heading right or
// if it is to our right and we're heading left then we can null this value out
if (distances[i] == 0 ||
(distances[i] > 0 && direction > 0) ||
(distances[i] < 0 && direction < 0))
{
distances[i] = INVALID_VALUE;
}
if (Math.Abs(distances[i]) < distanceValue)
{
distanceValue = Math.Abs(distances[i]);
smallestIndex = i;
}
}
if (smallestIndex < distances.Length)
{
// return and clear the smallest one we found
distances[smallestIndex] = INVALID_VALUE;
}
return smallestIndex;
}
/// <summary>
/// Actually draws the snaplines based on type, location, and specified pen
/// </summary>
private void RenderSnapLines(Line[] lines, Rectangle dragRect)
{
Pen currentPen;
for (int i = 0; i < lines.Length; i++)
{
if (lines[i].LineType is LineType.Margin or LineType.Padding)
{
currentPen = _edgePen;
if (lines[i].X1 == lines[i].X2)
{// vertical margin
int coord = Math.Max(dragRect.Top, lines[i].OriginalBounds.Top);
coord += (Math.Min(dragRect.Bottom, lines[i].OriginalBounds.Bottom) - coord) / 2;
lines[i].Y1 = lines[i].Y2 = coord;
if (lines[i].LineType == LineType.Margin)
{
lines[i].X1 = Math.Min(dragRect.Right, lines[i].OriginalBounds.Right);
lines[i].X2 = Math.Max(dragRect.Left, lines[i].OriginalBounds.Left);
}
else if (lines[i].PaddingLineType == PaddingLineType.PaddingLeft)
{
lines[i].X1 = lines[i].OriginalBounds.Left;
lines[i].X2 = dragRect.Left;
}
else
{
Debug.Assert(lines[i].PaddingLineType == PaddingLineType.PaddingRight);
lines[i].X1 = dragRect.Right;
lines[i].X2 = lines[i].OriginalBounds.Right;
}
lines[i].X2--; // off by 1 adjust
}
else
{// horizontal margin
int coord = Math.Max(dragRect.Left, lines[i].OriginalBounds.Left);
coord += (Math.Min(dragRect.Right, lines[i].OriginalBounds.Right) - coord) / 2;
lines[i].X1 = lines[i].X2 = coord;
if (lines[i].LineType == LineType.Margin)
{
lines[i].Y1 = Math.Min(dragRect.Bottom, lines[i].OriginalBounds.Bottom);
lines[i].Y2 = Math.Max(dragRect.Top, lines[i].OriginalBounds.Top);
}
else if (lines[i].PaddingLineType == PaddingLineType.PaddingTop)
{
lines[i].Y1 = lines[i].OriginalBounds.Top;
lines[i].Y2 = dragRect.Top;
}
else
{
Debug.Assert(lines[i].PaddingLineType == PaddingLineType.PaddingBottom);
lines[i].Y1 = dragRect.Bottom;
lines[i].Y2 = lines[i].OriginalBounds.Bottom;
}
lines[i].Y2--; // off by 1 adjust
}
}
else if (lines[i].LineType == LineType.Baseline)
{
currentPen = _baselinePen;
lines[i].X2 -= 1; // off by 1 adjust
}
else
{
// default to edgePen
currentPen = _edgePen;
if (lines[i].X1 == lines[i].X2)
{
lines[i].Y2--; // off by 1 adjustment
}
else
{
lines[i].X2--; // off by 1 adjustment
}
}
_graphics.DrawLine(currentPen, lines[i].X1, lines[i].Y1, lines[i].X2, lines[i].Y2);
}
}
/// <summary>
/// Performance improvement: Given an snapline we will render, check if it overlaps with an existing snapline.
/// If so, combine the two.
/// </summary>
private static void CombineSnaplines(Line snapLine, List<Line> currentLines)
{
bool merged = false;
for (int i = 0; i < currentLines.Count; i++)
{
Line curLine = currentLines[i];
Line? mergedLine = Line.Overlap(snapLine, curLine);
if (mergedLine is not null)
{
currentLines[i] = mergedLine;
merged = true;
}
}
if (!merged)
{
currentLines.Add(snapLine);
}
}
/// <summary>
/// Here, we store all the SnapLines we will render. This way we can erase them when they are no longer needed.
/// </summary>
private void StoreSnapLine(SnapLine snapLine, Rectangle dragBounds)
{
Rectangle bounds = _snapLineToBounds[snapLine];
// In order for CombineSnaplines to work correctly, we have to determine the type first
LineType type = LineType.Standard;
if (IsMarginOrPaddingSnapLine(snapLine))
{
// We already check if snapLine.Filter is not null inside IsMarginOrPaddingSnapLine.
type = snapLine.Filter!.StartsWith(SnapLine.Margin, StringComparison.Ordinal) ? LineType.Margin : LineType.Padding;
}
// propagate the baseline through to the linetype
else if (snapLine.SnapLineType == SnapLineType.Baseline)
{
type = LineType.Baseline;
}
Line line;
if (snapLine.IsVertical)
{
line = new Line(snapLine.Offset, Math.Min(dragBounds.Top + (_snapPointY != INVALID_VALUE ? _snapPointY : 0), bounds.Top),
snapLine.Offset, Math.Max(dragBounds.Bottom + (_snapPointY != INVALID_VALUE ? _snapPointY : 0), bounds.Bottom))
{
LineType = type
};
// Performance improvement: Check if the newly added line overlaps existing lines and if so, combine them.
CombineSnaplines(line, _tempVertLines);
}
else
{
line = new Line(Math.Min(dragBounds.Left + (_snapPointX != INVALID_VALUE ? _snapPointX : 0), bounds.Left), snapLine.Offset,
Math.Max(dragBounds.Right + (_snapPointX != INVALID_VALUE ? _snapPointX : 0), bounds.Right), snapLine.Offset)
{
LineType = type
};
// Performance improvement: Check if the newly added line overlaps existing lines and if so, combine them.
CombineSnaplines(line, _tempHorzLines);
}
if (IsMarginOrPaddingSnapLine(snapLine))
{
line.OriginalBounds = bounds;
// need to know which padding line (left, right) we are storing.
// The original check in RenderSnapLines was wrong.
// It assume that the dragRect was completely within the OriginalBounds which is not necessarily true
if (line.LineType == LineType.Padding)
{
switch (snapLine.Filter)
{
case SnapLine.PaddingRight:
line.PaddingLineType = PaddingLineType.PaddingRight;
break;
case SnapLine.PaddingLeft:
line.PaddingLineType = PaddingLineType.PaddingLeft;
break;
case SnapLine.PaddingTop:
line.PaddingLineType = PaddingLineType.PaddingTop;
break;
case SnapLine.PaddingBottom:
line.PaddingLineType = PaddingLineType.PaddingBottom;
break;
default:
Debug.Fail("Unknown snapline filter type");
break;
}
}
}
}
/// <summary>
/// This function validates a Margin or Padding SnapLine. A valid Margin SnapLine is one that will
/// be drawn only if the target control being dragged somehow intersects (vertically or horizontally)
/// the coords of the given snapLine. This is done so we don't start drawing margin lines when controls
/// are large distances apart (too much mess);
/// </summary>
private bool ValidateMarginOrPaddingLine(SnapLine snapLine, Rectangle dragBounds)
{
Rectangle bounds = _snapLineToBounds[snapLine];
if (snapLine.IsVertical)
{
if (bounds.Top < dragBounds.Top)
{
if (bounds.Top + bounds.Height < dragBounds.Top)
{
return false;
}
}
else if (dragBounds.Top + dragBounds.Height < bounds.Top)
{
return false;
}
}
else
{
if (bounds.Left < dragBounds.Left)
{
if (bounds.Left + bounds.Width < dragBounds.Left)
{
return false;
}
}
else if (dragBounds.Left + dragBounds.Width < bounds.Left)
{
return false;
}
}
// valid overlapping margin line
return true;
}
internal Point OnMouseMove(Rectangle dragBounds, SnapLine[] snapLines)
{
bool didSnap = false;
return OnMouseMove(dragBounds, snapLines, ref didSnap, true);
}
/// <summary>
/// Called by the DragBehavior on every mouse move. We first offset all
/// of our drag-control's snap lines by the amount of the mouse move
/// then follow our 2-pass heuristic to determine which SnapLines to render.
/// </summary>
internal Point OnMouseMove(Rectangle dragBounds, SnapLine[] snapLines, ref bool didSnap, bool shouldSnapHorizontally)
{
if (snapLines is null || snapLines.Length == 0)
{
return Point.Empty;
}
_targetHorizontalSnapLines.Clear();
_targetVerticalSnapLines.Clear();
// manually add our snaplines as targets
foreach (SnapLine snapline in snapLines)
{
if (snapline.IsHorizontal)
{
_targetHorizontalSnapLines.Add(snapline);
}
else
{
_targetVerticalSnapLines.Add(snapline);
}
}
return OnMouseMove(dragBounds, false, ref didSnap, shouldSnapHorizontally);
}
/// <summary>
/// Called by the DragBehavior on every mouse move. We first offset all of our drag-control's snap lines
/// by the amount of the mouse move then follow our 2-pass heuristic to determine which SnapLines to render.
/// </summary>
internal Point OnMouseMove(Rectangle dragBounds)
{
bool didSnap = false;
return OnMouseMove(dragBounds, true, ref didSnap, true);
}
/// <summary>
/// Called by the resizebehavior.
/// It needs to know whether we really snapped or not. The snapPoint could be (0,0) even though we snapped.
/// </summary>
internal Point OnMouseMove(Control targetControl, SnapLine[] snapLines, ref bool didSnap, bool shouldSnapHorizontally)
{
Rectangle dragBounds = new(_behaviorService.ControlToAdornerWindow(targetControl), targetControl.Size);
didSnap = false;
return OnMouseMove(dragBounds, snapLines, ref didSnap, shouldSnapHorizontally);
}
/// <summary>
/// Called by the DragBehavior on every mouse move. We first offset all of our drag-control's snap lines
/// by the amount of the mouse move then follow our 2-pass heuristic to determine which SnapLines to render.
/// </summary>
private Point OnMouseMove(Rectangle dragBounds, bool offsetSnapLines, ref bool didSnap, bool shouldSnapHorizontally)
{
_tempVertLines.Clear();
_tempHorzLines.Clear();
_dragOffset = new Point(dragBounds.X - _dragOffset.X, dragBounds.Y - _dragOffset.Y);
if (offsetSnapLines)
{
// offset our targetSnapLines by the amount we have dragged it
for (int i = 0; i < _targetHorizontalSnapLines.Count; i++)
{
_targetHorizontalSnapLines[i].AdjustOffset(_dragOffset.Y);
}
for (int i = 0; i < _targetVerticalSnapLines.Count; i++)
{
_targetVerticalSnapLines[i].AdjustOffset(_dragOffset.X);
}
}
// First pass - build up a distance array of all same-type-alignment pts to the closest point
// on our targetControl. Also, keep track of the smallestdistance overall
int smallestDistanceVert = BuildDistanceArray(_verticalSnapLines, _targetVerticalSnapLines, _verticalDistances, dragBounds);
int smallestDistanceHorz = INVALID_VALUE;
if (shouldSnapHorizontally)
{
smallestDistanceHorz = BuildDistanceArray(_horizontalSnapLines, _targetHorizontalSnapLines, _horizontalDistances, dragBounds);
}
// Second Pass! We only need to do a second pass if the smallest delta is <= SnapDistance.
// If this is the case - then we draw snap lines for every line equal to the smallest distance available in the distance array
_snapPointX = (Math.Abs(smallestDistanceVert) <= SnapDistance) ? -smallestDistanceVert : INVALID_VALUE;
_snapPointY = (Math.Abs(smallestDistanceHorz) <= SnapDistance) ? -smallestDistanceHorz : INVALID_VALUE;
// certain behaviors (like resize) might want to know whether we really snapped or not.
// They can't check the returned snapPoint for (0,0) since that is a valid snapPoint.
didSnap = false;
if (_snapPointX != INVALID_VALUE)
{
IdentifyAndStoreValidLines(_verticalSnapLines, _verticalDistances, dragBounds, smallestDistanceVert);
didSnap = true;
}
if (_snapPointY != INVALID_VALUE)
{
IdentifyAndStoreValidLines(_horizontalSnapLines, _horizontalDistances, dragBounds, smallestDistanceHorz);
didSnap = true;
}
Point snapPoint = new(_snapPointX != INVALID_VALUE ? _snapPointX : 0, _snapPointY != INVALID_VALUE ? _snapPointY : 0);
Rectangle tempDragRect = new(dragBounds.Left + snapPoint.X, dragBounds.Top + snapPoint.Y, dragBounds.Width, dragBounds.Height);
// out with the old...
_vertLines = EraseOldSnapLines(_vertLines, _tempVertLines);
_horzLines = EraseOldSnapLines(_horzLines, _tempHorzLines);
// store this drag rect - we'll use it when we are (eventually) called back on to actually render our lines
// NOTE NOTE NOTE: If OnMouseMove is called during a resize operation,
// then cachedDragRect is not guaranteed to work. That is why I introduced RenderSnapLinesInternal(dragRect)
_cachedDragRect = tempDragRect;
// reset the dragoffset to this last location
_dragOffset = dragBounds.Location;
// this 'snapPoint' will be the amount we want the dragBehavior to shift the dragging control by
// ('cause we snapped somewhere)
return snapPoint;
}
// NOTE NOTE NOTE: If OnMouseMove is called during a resize operation,
// then cachedDragRect is not guaranteed to work. That is why I introduced RenderSnapLinesInternal(dragRect)
/// <summary>
/// Called by the ResizeBehavior after it has finished drawing
/// </summary>
internal void RenderSnapLinesInternal(Rectangle dragRect)
{
_cachedDragRect = dragRect;
RenderSnapLinesInternal();
}
/// <summary>
/// Called by the DropSourceBehavior after it finished drawing its' dragging images
/// so that we can draw our lines on top of everything.
/// </summary>
internal void RenderSnapLinesInternal()
{
RenderSnapLines(_vertLines, _cachedDragRect);
RenderSnapLines(_horzLines, _cachedDragRect);
_recentLines = new Line[_vertLines.Length + _horzLines.Length];
_vertLines.CopyTo(_recentLines, 0);
_horzLines.CopyTo(_recentLines, _vertLines.Length);
}
/// <summary>
/// Clean up all of our references.
/// </summary>
internal void OnMouseUp()
{
// Here, we store off our recent snapline info to the behavior service - this is used for testing purposes
if (_behaviorService is not null)
{
Line[] recent = GetRecentLines();
string[] lines = new string[recent.Length];
for (int i = 0; i < recent.Length; i++)
{
lines[i] = recent[i].ToString();
}
_behaviorService.RecentSnapLines = lines;
}
EraseSnapLines();
_graphics.Dispose();
if (_disposeEdgePen && _edgePen is not null)
{
_edgePen.Dispose();
}
_baselinePen?.Dispose();
_backgroundImage?.Dispose();
}
/// <summary>
/// Describes different types of lines (used for margins, etc..)
/// </summary>
internal enum LineType
{
Standard, Margin, Padding, Baseline
}
/// <summary>
/// Describes what kind of padding line we have
/// </summary>
internal enum PaddingLineType
{
None, PaddingRight, PaddingLeft, PaddingTop, PaddingBottom
}
}
|