|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
namespace System.Windows.Forms.Design.Behavior;
/// <summary>
/// The ResizeBehavior is pushed onto the BehaviorStack in response to a positively hit tested SelectionGlyph.
/// The ResizeBehavior simply tracks the MouseMove messages and updates the bounds of the related
/// control based on the new mouse location and the resize Rules.
/// </summary>
internal class ResizeBehavior : Behavior
{
private struct ResizeComponent
{
public Control resizeControl;
public Rectangle resizeBounds;
public SelectionRules resizeRules;
}
private ResizeComponent[] _resizeComponents;
private readonly IServiceProvider _serviceProvider;
private BehaviorService _behaviorService;
private SelectionRules _targetResizeRules; // rules dictating which sizes we can change
private Point _initialPoint; // the initial point of the mouse down
private bool _dragging; // indicates that the behavior is currently 'dragging'
private bool _pushedBehavior;
private bool _initialResize; // true for the first resize of the control, false after that.
private DesignerTransaction _resizeTransaction; // the transaction we create for the resize
private const int MINSIZE = 10;
private const int BorderSize = 2;
private DragAssistanceManager _dragManager; // this object will integrate SnapLines into the resize
private Point _lastMouseLoc; // helps us avoid re-entering code if the mouse hasn't moved
private Point _parentLocation; // used to snap resize ops to the grid
private Size _parentGridSize; // used to snap resize ops to the grid
private Point _lastMouseAbs; // last absolute mouse position
private Point _lastSnapOffset; // the last snapoffset we used.
private bool _didSnap; // did we actually snap.
private Control _primaryControl; // the primary control the status bar will queue off of
private Cursor _cursor = Cursors.Default; // used to set the correct cursor during resizing
private readonly StatusCommandUI _statusCommandUI; // used to update the StatusBar Information.
private Region _lastResizeRegion;
private bool _captureLost;
/// <summary>
/// Constructor that caches all values for perf. reasons.
/// </summary>
internal ResizeBehavior(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_dragging = false;
_pushedBehavior = false;
_lastSnapOffset = Point.Empty;
_didSnap = false;
_statusCommandUI = new StatusCommandUI(serviceProvider);
}
/// <summary>
/// Demand creates the BehaviorService.
/// </summary>
private BehaviorService BehaviorService
{
get
{
_behaviorService ??= (BehaviorService)_serviceProvider.GetService(typeof(BehaviorService));
return _behaviorService;
}
}
public override Cursor Cursor
{
get
{
return _cursor;
}
}
/// <summary>
/// Called during the resize operation, we'll try to determine an offset so that
/// the controls snap to the grid settings of the parent.
/// </summary>
private Rectangle AdjustToGrid(Rectangle controlBounds, SelectionRules rules)
{
Rectangle rect = controlBounds;
if ((rules & SelectionRules.RightSizeable) != 0)
{
int xDelta = controlBounds.Right % _parentGridSize.Width;
if (xDelta > _parentGridSize.Width / 2)
{
rect.Width += _parentGridSize.Width - xDelta;
}
else
{
rect.Width -= xDelta;
}
}
else if ((rules & SelectionRules.LeftSizeable) != 0)
{
int xDelta = controlBounds.Left % _parentGridSize.Width;
if (xDelta > _parentGridSize.Width / 2)
{
rect.X += _parentGridSize.Width - xDelta;
rect.Width -= _parentGridSize.Width - xDelta;
}
else
{
rect.X -= xDelta;
rect.Width += xDelta;
}
}
if ((rules & SelectionRules.BottomSizeable) != 0)
{
int yDelta = controlBounds.Bottom % _parentGridSize.Height;
if (yDelta > _parentGridSize.Height / 2)
{
rect.Height += _parentGridSize.Height - yDelta;
}
else
{
rect.Height -= yDelta;
}
}
else if ((rules & SelectionRules.TopSizeable) != 0)
{
int yDelta = controlBounds.Top % _parentGridSize.Height;
if (yDelta > _parentGridSize.Height / 2)
{
rect.Y += _parentGridSize.Height - yDelta;
rect.Height -= _parentGridSize.Height - yDelta;
}
else
{
rect.Y -= yDelta;
rect.Height += yDelta;
}
}
// validate our dimensions
rect.Width = Math.Max(rect.Width, _parentGridSize.Width);
rect.Height = Math.Max(rect.Height, _parentGridSize.Height);
return rect;
}
/// <summary>
/// Builds up an array of snaplines used during resize to adjust/snap the controls bounds.
/// </summary>
private SnapLine[] GenerateSnapLines(SelectionRules rules, Point loc)
{
List<SnapLine> lines = new(2);
// the four margins and edges of our control
if ((rules & SelectionRules.BottomSizeable) != 0)
{
lines.Add(new SnapLine(SnapLineType.Bottom, loc.Y - 1));
if (_primaryControl is not null)
{
lines.Add(new SnapLine(SnapLineType.Horizontal, loc.Y + _primaryControl.Margin.Bottom, SnapLine.MarginBottom, SnapLinePriority.Always));
}
}
else if ((rules & SelectionRules.TopSizeable) != 0)
{
lines.Add(new SnapLine(SnapLineType.Top, loc.Y));
if (_primaryControl is not null)
{
lines.Add(new SnapLine(SnapLineType.Horizontal, loc.Y - _primaryControl.Margin.Top, SnapLine.MarginTop, SnapLinePriority.Always));
}
}
if ((rules & SelectionRules.RightSizeable) != 0)
{
lines.Add(new SnapLine(SnapLineType.Right, loc.X - 1));
if (_primaryControl is not null)
{
lines.Add(new SnapLine(SnapLineType.Vertical, loc.X + _primaryControl.Margin.Right, SnapLine.MarginRight, SnapLinePriority.Always));
}
}
else if ((rules & SelectionRules.LeftSizeable) != 0)
{
lines.Add(new SnapLine(SnapLineType.Left, loc.X));
if (_primaryControl is not null)
{
lines.Add(new SnapLine(SnapLineType.Vertical, loc.X - _primaryControl.Margin.Left, SnapLine.MarginLeft, SnapLinePriority.Always));
}
}
return [.. lines];
}
/// <summary>
/// This is called in response to the mouse moving far enough away from its initial point.
/// Basically, we calculate the bounds for each control we're resizing and disable any adorners.
/// </summary>
private void InitiateResize()
{
bool useSnapLines = BehaviorService.UseSnapLines;
List<IComponent> components = [];
// check to see if the current designer participate with SnapLines cache the control bounds
for (int i = 0; i < _resizeComponents.Length; i++)
{
_resizeComponents[i].resizeBounds = _resizeComponents[i].resizeControl.Bounds;
if (useSnapLines)
{
components.Add(_resizeComponents[i].resizeControl);
}
if (_serviceProvider.GetService(typeof(IDesignerHost)) is IDesignerHost designerHost)
{
if (designerHost.GetDesigner(_resizeComponents[i].resizeControl) is ControlDesigner designer)
{
_resizeComponents[i].resizeRules = designer.SelectionRules;
}
else
{
Debug.Fail($"Initiating resize. Could not get the designer for {_resizeComponents[i].resizeControl}");
_resizeComponents[i].resizeRules = SelectionRules.None;
}
}
}
// disable all glyphs in all adorners
BehaviorService.EnableAllAdorners(false);
// build up our resize transaction
IDesignerHost host = (IDesignerHost)_serviceProvider.GetService(typeof(IDesignerHost));
if (host is not null)
{
string locString;
if (_resizeComponents.Length == 1)
{
string name = TypeDescriptor.GetComponentName(_resizeComponents[0].resizeControl);
if (name is null || name.Length == 0)
{
name = _resizeComponents[0].resizeControl.GetType().Name;
}
locString = string.Format(SR.BehaviorServiceResizeControl, name);
}
else
{
locString = string.Format(SR.BehaviorServiceResizeControls, _resizeComponents.Length);
}
_resizeTransaction = host.CreateTransaction(locString);
}
_initialResize = true;
if (useSnapLines)
{
// instantiate our class to manage snap/margin lines...
_dragManager = new DragAssistanceManager(_serviceProvider, components, true);
}
else if (_resizeComponents.Length > 0)
{
// try to get the parents grid and snap settings
if (_resizeComponents[0].resizeControl is Control control && control.Parent is not null)
{
PropertyDescriptor snapProp = TypeDescriptor.GetProperties(control.Parent)["SnapToGrid"];
if (snapProp is not null && (bool)snapProp.GetValue(control.Parent))
{
PropertyDescriptor gridProp = TypeDescriptor.GetProperties(control.Parent)["GridSize"];
if (gridProp is not null)
{
// cache of the gridsize and the location of the parent on the adornerwindow
_parentGridSize = (Size)gridProp.GetValue(control.Parent);
_parentLocation = _behaviorService.ControlToAdornerWindow(control);
_parentLocation.X -= control.Location.X;
_parentLocation.Y -= control.Location.Y;
}
}
}
}
_captureLost = false;
}
/// <summary>
/// In response to a MouseDown, the SelectionBehavior will push (initiate) a dragBehavior by
/// alerting the SelectionMananger that a new control has been selected and the mouse is down.
/// Note that this is only if we find the related control's Dock property == none.
/// </summary>
public override bool OnMouseDown(Glyph g, MouseButtons button, Point mouseLoc)
{
// we only care about the right mouse button for resizing
if (button != MouseButtons.Left)
{
// pass any other mouse click along - unless we've already started our resize in which case we'll ignore it
return _pushedBehavior;
}
// start with no selection rules and try to obtain this info from the glyph
_targetResizeRules = SelectionRules.None;
if (g is SelectionGlyphBase sgb)
{
_targetResizeRules = sgb.SelectionRules;
_cursor = sgb.HitTestCursor;
}
if (_targetResizeRules == SelectionRules.None)
{
return false;
}
ISelectionService selSvc = (ISelectionService)_serviceProvider.GetService(typeof(ISelectionService));
if (selSvc is null)
{
return false;
}
_initialPoint = mouseLoc;
_lastMouseLoc = mouseLoc;
// build up a list of our selected controls
_primaryControl = selSvc.PrimarySelection as Control;
// Since we don't know exactly how many valid objects we are going to have we use this temp
List<Control> components = [];
foreach (object component in selSvc.GetSelectedComponents())
{
if (component is Control control)
{
// don't drag locked controls
PropertyDescriptor prop = TypeDescriptor.GetProperties(control)["Locked"];
if (prop is not null)
{
if ((bool)prop.GetValue(control))
{
continue;
}
}
components.Add(control);
}
}
if (components.Count == 0)
{
return false;
}
_resizeComponents = new ResizeComponent[components.Count];
for (int i = 0; i < components.Count; i++)
{
_resizeComponents[i].resizeControl = components[i];
}
// push this resizebehavior
_pushedBehavior = true;
BehaviorService.PushCaptureBehavior(this);
return false;
}
/// <summary>
/// This method is called when we lose capture, which can occur when another window requests capture or
/// the user presses ESC during a drag. We check to see if we are currently dragging,
/// and if we are we abort the transaction. We pop our behavior off the stack at this time.
/// </summary>
public override void OnLoseCapture(Glyph g, EventArgs e)
{
_captureLost = true;
if (_pushedBehavior)
{
_pushedBehavior = false;
Debug.Assert(BehaviorService is not null, "We should have a behavior service.");
if (BehaviorService is not null)
{
if (_dragging)
{
_dragging = false;
// make sure we get rid of the selection rectangle
for (int i = 0; !_captureLost && i < _resizeComponents.Length; i++)
{
Control control = _resizeComponents[i].resizeControl;
Rectangle borderRect = BehaviorService.ControlRectInAdornerWindow(control);
if (!borderRect.IsEmpty)
{
using Graphics graphics = BehaviorService.AdornerWindowGraphics;
graphics.SetClip(borderRect);
using (Region newRegion = new(borderRect))
{
newRegion.Exclude(Rectangle.Inflate(borderRect, -BorderSize, -BorderSize));
BehaviorService.Invalidate(newRegion);
}
graphics.ResetClip();
}
}
// re-enable all glyphs in all adorners
BehaviorService.EnableAllAdorners(true);
}
BehaviorService.PopBehavior(this);
if (_lastResizeRegion is not null)
{
BehaviorService.Invalidate(_lastResizeRegion); // might be the same, might not.
_lastResizeRegion.Dispose();
_lastResizeRegion = null;
}
}
}
Debug.Assert(!_dragging, "How can we be dragging without pushing a behavior?");
// If we still have a transaction, roll it back.
if (_resizeTransaction is not null)
{
DesignerTransaction t = _resizeTransaction;
_resizeTransaction = null;
using (t)
{
t.Cancel();
}
}
}
internal static int AdjustPixelsForIntegralHeight(Control control, int pixelsMoved)
{
PropertyDescriptor propIntegralHeight = TypeDescriptor.GetProperties(control)["IntegralHeight"];
if (propIntegralHeight is not null)
{
object value = propIntegralHeight.GetValue(control);
if (value is bool boolValue && boolValue)
{
PropertyDescriptor propItemHeight = TypeDescriptor.GetProperties(control)["ItemHeight"];
if (propItemHeight is not null)
{
if (pixelsMoved >= 0)
{
return pixelsMoved - (pixelsMoved % (int)propItemHeight.GetValue(control));
}
else
{
int integralHeight = (int)propItemHeight.GetValue(control);
return pixelsMoved - (integralHeight - (Math.Abs(pixelsMoved) % integralHeight));
}
}
}
}
// if the control does not have the IntegralHeight property, then the pixels moved are fine
return pixelsMoved;
}
/// <summary>
/// This method will either initiate a new resize operation or continue with an existing one.
/// If we're currently dragging (i.e. resizing) then we look at the resize rules and set the
/// bounds of each control to the new location of the mouse pointer.
/// </summary>
public override bool OnMouseMove(Glyph g, MouseButtons button, Point mouseLoc)
{
if (!_pushedBehavior)
{
return false;
}
bool altKeyPressed = Control.ModifierKeys == Keys.Alt;
if (altKeyPressed && _dragManager is not null)
{
// erase any snaplines (if we had any)
_dragManager.EraseSnapLines();
}
if (!altKeyPressed && mouseLoc.Equals(_lastMouseLoc))
{
return true;
}
// When DesignerWindowPane has scrollbars and we resize, shrinking the DesignerWindowPane
// makes it look like the mouse has moved to the BS.
// To compensate for that we keep track of the mouse's previous position in screen coordinates,
// and use that to compare if the mouse has really moved.
if (_lastMouseAbs != Point.Empty)
{
Point mouseLocAbs = new(mouseLoc.X, mouseLoc.Y);
PInvoke.ClientToScreen(_behaviorService.AdornerWindowControl, ref mouseLocAbs);
if (mouseLocAbs.X == _lastMouseAbs.X && mouseLocAbs.Y == _lastMouseAbs.Y)
{
return true;
}
}
if (!_dragging)
{
if (Math.Abs(_initialPoint.X - mouseLoc.X) > DesignerUtils.MinDragSize.Width / 2 || Math.Abs(_initialPoint.Y - mouseLoc.Y) > DesignerUtils.MinDragSize.Height / 2)
{
InitiateResize();
_dragging = true;
}
else
{
return false;
}
}
if (_resizeComponents is null || _resizeComponents.Length == 0)
{
return false;
}
// we do these separately so as not to disturb the cached sizes for values we're not actually changing.
// For example, if a control is docked top and we modify the height, the width shouldn't be modified.
PropertyDescriptor propWidth = null;
PropertyDescriptor propHeight = null;
PropertyDescriptor propTop = null;
PropertyDescriptor propLeft = null;
// We do this to make sure that Undo works correctly.
if (_initialResize)
{
propWidth = TypeDescriptor.GetProperties(_resizeComponents[0].resizeControl)["Width"];
propHeight = TypeDescriptor.GetProperties(_resizeComponents[0].resizeControl)["Height"];
propTop = TypeDescriptor.GetProperties(_resizeComponents[0].resizeControl)["Top"];
propLeft = TypeDescriptor.GetProperties(_resizeComponents[0].resizeControl)["Left"];
// validate each of the property descriptors.
if (propWidth is not null && !typeof(int).IsAssignableFrom(propWidth.PropertyType))
{
propWidth = null;
}
if (propHeight is not null && !typeof(int).IsAssignableFrom(propHeight.PropertyType))
{
propHeight = null;
}
if (propTop is not null && !typeof(int).IsAssignableFrom(propTop.PropertyType))
{
propTop = null;
}
if (propLeft is not null && !typeof(int).IsAssignableFrom(propLeft.PropertyType))
{
propLeft = null;
}
}
Control targetControl = _resizeComponents[0].resizeControl;
_lastMouseLoc = mouseLoc;
_lastMouseAbs = new Point(mouseLoc.X, mouseLoc.Y);
PInvoke.ClientToScreen(_behaviorService.AdornerWindowControl, ref _lastMouseAbs);
int minHeight = Math.Max(targetControl.MinimumSize.Height, MINSIZE);
int minWidth = Math.Max(targetControl.MinimumSize.Width, MINSIZE);
if (_dragManager is not null)
{
bool shouldSnap = true;
bool shouldSnapHorizontally = true;
// if the targetcontrol is at min-size then we do not want to offer up snaplines
if ((((_targetResizeRules & SelectionRules.BottomSizeable) != 0) || ((_targetResizeRules & SelectionRules.TopSizeable) != 0)) &&
(targetControl.Height == minHeight))
{
shouldSnap = false;
}
else if ((((_targetResizeRules & SelectionRules.RightSizeable) != 0) || ((_targetResizeRules & SelectionRules.LeftSizeable) != 0)) &&
(targetControl.Width == minWidth))
{
shouldSnap = false;
}
// if the targetControl has IntegralHeight turned on, then don't snap if the control can be resized vertically
PropertyDescriptor propIntegralHeight = TypeDescriptor.GetProperties(targetControl)["IntegralHeight"];
if (propIntegralHeight is not null)
{
object value = propIntegralHeight.GetValue(targetControl);
if (value is bool boolValue && boolValue)
{
shouldSnapHorizontally = false;
}
}
if (!altKeyPressed && shouldSnap)
{
// here, ask the snapline engine to suggest an offset during our resize
// Remembering the last snapoffset allows us to correctly erase snaplines,
// if the user subsequently holds down the Alt-Key. Remember that we don't physically move the mouse,
// we move the control. So if we didn't remember the last snapoffset and the user then hit the Alt-Key,
// we would actually redraw the control at the actual mouse location,
// which would make the control "jump" which is not what the user would expect.
// Why does the control "jump"? Because when a control is snapped,
// we have offset the control relative to where the mouse is,
// but we have not update the physical mouse position.
// When the user hits the Alt-Key they expect the control to be where it was (whether snapped or not).
// we can't rely on lastSnapOffset to check whether we snapped. We used to check if it was empty,
// but it can be empty and we still snapped (say the control was snapped,
// as you continue to move the mouse, it will stay snapped for a while.
// During that while the snapoffset will got from x to -x (or vice versa) and a one point hit 0.
// Since we have to calculate the new size/location differently based on whether we snapped or not,
// we have to know for sure if we snapped. We do different math because of bug 264996:
// - if you snap, we want to move the control edge.
// - otherwise, we just want to change the size by the number of pixels moved.
_lastSnapOffset = _dragManager.OnMouseMove(targetControl, GenerateSnapLines(_targetResizeRules, mouseLoc), ref _didSnap, shouldSnapHorizontally);
}
else
{
/*just an invalid rect - so we won't snap*/// );
_dragManager.OnMouseMove(new Rectangle(-100, -100, 0, 0));
}
// If there's a line to snap to, the offset will come back non-zero.
// In that case we should adjust the mouse position with the offset such that
// the size calculation below takes that offset into account. If there's no line,
// then the offset is 0, and there's no harm in adding the offset.
mouseLoc.X += _lastSnapOffset.X;
mouseLoc.Y += _lastSnapOffset.Y;
}
// IF WE ARE SNAPPING TO A CONTROL, then we also need to adjust for the offset between the
// initialPoint (where the MouseDown happened) and the edge of the control otherwise we
// would be those pixels off when resizing the control. Remember that snaplines are based on the targetControl,
// so we need to use the targetControl to figure out the offset.
Rectangle controlBounds = new(_resizeComponents[0].resizeBounds.X, _resizeComponents[0].resizeBounds.Y,
_resizeComponents[0].resizeBounds.Width, _resizeComponents[0].resizeBounds.Height);
if ((_didSnap) && (targetControl.Parent is not null))
{
controlBounds.Location = _behaviorService.MapAdornerWindowPoint(targetControl.Parent.Handle, controlBounds.Location);
if (targetControl.Parent.IsMirrored)
{
controlBounds.Offset(-controlBounds.Width, 0);
}
}
Rectangle newBorderRect = Rectangle.Empty;
Rectangle targetBorderRect = Rectangle.Empty;
bool drawSnapline = true;
Color backColor = targetControl.Parent is not null ? targetControl.Parent.BackColor : Color.Empty;
for (int i = 0; i < _resizeComponents.Length; i++)
{
Control control = _resizeComponents[i].resizeControl;
Rectangle bounds = control.Bounds;
Rectangle oldBounds = bounds;
// We need to compute the offset based on the original cached Bounds ...
// ListBox doesn't allow drag on the top boundary if this is not done when it is "IntegralHeight"
Rectangle baseBounds = _resizeComponents[i].resizeBounds;
Rectangle oldBorderRect = BehaviorService.ControlRectInAdornerWindow(control);
bool needToUpdate = true;
// The ResizeBehavior can easily get into a situation where we are fighting with a layout engine.
// E.g., We resize control to 50px, LayoutEngine lays out and finds 50px was too small
// and resized back to 100px. This is what should happen, but it looks bad in the designer.
// To avoid the flicker we temporarily turn off painting while we do the resize.
PInvokeCore.SendMessage(control, PInvokeCore.WM_SETREDRAW, (WPARAM)(BOOL)false);
try
{
bool fRTL = false;
// If the container is mirrored the control origin is in upper-right,
// so we need to adjust our math for that.
// Remember that mouse coords have origin in upper left.
if (control.Parent is not null && control.Parent.IsMirrored)
{
fRTL = true;
}
// figure out which ones we're actually changing so we don't blow away the controls cached sizing state.
// This is important if things are docked we don't want to destroy their "pre-dock" size.
BoundsSpecified specified = BoundsSpecified.None;
// When we check if we should change height, width, location, we first have to check
// if the targetControl allows resizing, and then if the control we are currently resizin
// g allows it as well.
SelectionRules resizeRules = _resizeComponents[i].resizeRules;
if (((_targetResizeRules & SelectionRules.BottomSizeable) != 0) &&
((resizeRules & SelectionRules.BottomSizeable) != 0))
{
int pixelHeight;
if (_didSnap)
{
pixelHeight = mouseLoc.Y - controlBounds.Bottom;
}
else
{
pixelHeight = AdjustPixelsForIntegralHeight(control, mouseLoc.Y - _initialPoint.Y);
}
bounds.Height = Math.Max(minHeight, baseBounds.Height + pixelHeight);
specified |= BoundsSpecified.Height;
}
if (((_targetResizeRules & SelectionRules.TopSizeable) != 0) &&
((resizeRules & SelectionRules.TopSizeable) != 0))
{
int yOffset;
if (_didSnap)
{
yOffset = controlBounds.Y - mouseLoc.Y;
}
else
{
yOffset = AdjustPixelsForIntegralHeight(control, _initialPoint.Y - mouseLoc.Y);
}
specified |= BoundsSpecified.Height;
bounds.Height = Math.Max(minHeight, baseBounds.Height + yOffset);
if ((bounds.Height != minHeight) ||
((bounds.Height == minHeight) && (oldBounds.Height != minHeight)))
{
specified |= BoundsSpecified.Y;
// if you do it fast enough, we actually could end up placing the control off the parent
// (say off the form), so enforce a "minimum" location
bounds.Y = Math.Min(baseBounds.Bottom - minHeight, baseBounds.Y - yOffset);
}
}
if (((((_targetResizeRules & SelectionRules.RightSizeable) != 0) && ((resizeRules & SelectionRules.RightSizeable) != 0)) && (!fRTL)) ||
((((_targetResizeRules & SelectionRules.LeftSizeable) != 0) && ((resizeRules & SelectionRules.LeftSizeable) != 0)) && (fRTL)))
{
specified |= BoundsSpecified.Width;
int xOffset = _initialPoint.X;
if (_didSnap)
{
xOffset = !fRTL ? controlBounds.Right : controlBounds.Left;
}
bounds.Width = Math.Max(minWidth, baseBounds.Width + (!fRTL ? (mouseLoc.X - xOffset) : (xOffset - mouseLoc.X)));
}
if (((((_targetResizeRules & SelectionRules.RightSizeable) != 0) && ((resizeRules & SelectionRules.RightSizeable) != 0)) && (fRTL)) ||
((((_targetResizeRules & SelectionRules.LeftSizeable) != 0) && ((resizeRules & SelectionRules.LeftSizeable) != 0)) && (!fRTL)))
{
specified |= BoundsSpecified.Width;
int xPos = _initialPoint.X;
if (_didSnap)
{
xPos = !fRTL ? controlBounds.Left : controlBounds.Right;
}
int xOffset = !fRTL ? (xPos - mouseLoc.X) : (mouseLoc.X - xPos);
bounds.Width = Math.Max(minWidth, baseBounds.Width + xOffset);
if ((bounds.Width != minWidth) ||
((bounds.Width == minWidth) && (oldBounds.Width != minWidth)))
{
specified |= BoundsSpecified.X;
// if you do it fast enough, we actually could end up placing the control off the parent
// (say off the form), so enforce a "minimum" location
bounds.X = Math.Min(baseBounds.Right - minWidth, baseBounds.X - xOffset);
}
}
if (!_parentGridSize.IsEmpty)
{
bounds = AdjustToGrid(bounds, _targetResizeRules);
}
// Checking specified (check the diff) rather than bounds.<foo> != resizeBounds[i].<foo>
// also handles the following corner cases:
// 1. Create a form and add 2 buttons. Make sure that they are snapped to the left edge.
// Now grab the left edge of button 1, and start resizing to the left,
// past the snapline you will initially get, and then back to the right.
// What you would expect is to get the left edge snapline again.
// But without the specified check you wouldn't.
// This is because the bounds.<foo> != resizeBounds[i].<foo> checks would fail,
// since the new size would now be the original size.
// We could probably live with that, except that we draw the snapline below,
// since we correctly identified one. We could hack it so that we didn't draw the snapline,
// but that would confuse the user even more.
// 2. Create a form and add a single button. Place it at 100,100.
// Now start resizing it to the left and then back to the right.
// Note that with the original check (see diff), you would never be able to
// resize it back to position 100,100. You would get to 99,100 and then to 101,100.
if (((specified & BoundsSpecified.Width) == BoundsSpecified.Width) &&
_dragging && _initialResize && propWidth is not null)
{
propWidth.SetValue(_resizeComponents[i].resizeControl, bounds.Width);
}
if (((specified & BoundsSpecified.Height) == BoundsSpecified.Height) &&
_dragging && _initialResize && propHeight is not null)
{
propHeight.SetValue(_resizeComponents[i].resizeControl, bounds.Height);
}
if (((specified & BoundsSpecified.X) == BoundsSpecified.X) &&
_dragging && _initialResize && propLeft is not null)
{
propLeft.SetValue(_resizeComponents[i].resizeControl, bounds.X);
}
if (((specified & BoundsSpecified.Y) == BoundsSpecified.Y) &&
_dragging && _initialResize && propTop is not null)
{
propTop.SetValue(_resizeComponents[i].resizeControl, bounds.Y);
}
// We check the dragging bit here at every turn,
// because if there was a popup we may have lost capture and we are terminated.
// At that point we shouldn't make any changes.
if (_dragging)
{
control.SetBounds(bounds.X, bounds.Y, bounds.Width, bounds.Height, specified);
// Get the new resize border
newBorderRect = BehaviorService.ControlRectInAdornerWindow(control);
if (control.Equals(targetControl))
{
Debug.Assert(i == 0, "The first control in the Selection should be the target control");
targetBorderRect = newBorderRect;
}
// Check that the control really did resize itself.
// Some controls (like ListBox, MonthCalendar)
// might adjust to a slightly different size than the one we pass in SetBounds.
// If if didn't size, then there's no need to invalidate anything
if (control.Bounds == oldBounds)
{
needToUpdate = false;
}
// We would expect the bounds now to be what we set it to above,
// but this might not be the case. If the control is hosted with e.g. a FLP,
// then setting the bounds above actually might force a re-layout,
// and the control will get moved to another spot. In this case,
// we don't really want to draw a snapline.
// Even if we snapped to a snapline, if the control got moved,
// the snapline would be in the wrong place.
if (control.Bounds != bounds)
{
drawSnapline = false;
}
}
if (control == _primaryControl && _statusCommandUI is not null)
{
_statusCommandUI.SetStatusInformation(control);
}
}
finally
{
// While we were resizing we discarded painting messages to reduce flicker.
// We now turn painting back on and manually refresh the controls.
PInvokeCore.SendMessage(control, PInvokeCore.WM_SETREDRAW, (WPARAM)(BOOL)true);
// update the control
if (needToUpdate)
{
Control parent = control.Parent;
if (parent is not null)
{
control.Invalidate(/* invalidateChildren = */ true);
parent.Invalidate(oldBounds, /* invalidateChildren = */ true);
parent.Update();
}
else
{
control.Refresh();
}
}
// render the resize border
if (!newBorderRect.IsEmpty)
{
using Region newRegion = new(newBorderRect);
newRegion.Exclude(Rectangle.Inflate(newBorderRect, -BorderSize, -BorderSize));
// No reason to get smart about only invalidating part of the border.
// Thought we could be but no.The reason is the order: ...
// the new border is drawn (last resize) On next mousemove,
// the control is resized which redraws the control AND ERASES THE BORDER
// Then we draw the new border - flash baby. Thus this will always flicker.
if (needToUpdate)
{
using Region oldRegion = new(oldBorderRect);
oldRegion.Exclude(Rectangle.Inflate(oldBorderRect, -BorderSize, -BorderSize));
BehaviorService.Invalidate(oldRegion);
}
// draw the new border captureLost could be true if a popup came up and caused a lose focus
if (!_captureLost)
{
using (Graphics graphics = BehaviorService.AdornerWindowGraphics)
{
if (_lastResizeRegion is not null)
{
if (!_lastResizeRegion.Equals(newRegion, graphics))
{
_lastResizeRegion.Exclude(newRegion); // we don't want to invalidate this region.
BehaviorService.Invalidate(_lastResizeRegion); // might be the same, might not.
_lastResizeRegion.Dispose();
_lastResizeRegion = null;
}
}
DesignerUtils.DrawResizeBorder(graphics, newRegion, backColor);
}
_lastResizeRegion ??= newRegion.Clone(); // we will need to dispose it later.
}
}
}
}
if ((drawSnapline) && (!altKeyPressed) && (_dragManager is not null))
{
_dragManager.RenderSnapLinesInternal(targetBorderRect);
}
_initialResize = false;
return true;
}
/// <summary>
/// This ends the Behavior by popping itself from the BehaviorStack.
/// Also, all Adorners are re-enabled at the end of a successful drag.
/// </summary>
public override bool OnMouseUp(Glyph g, MouseButtons button)
{
try
{
if (_dragging)
{
if (_dragManager is not null)
{
_dragManager.OnMouseUp();
_dragManager = null;
_lastSnapOffset = Point.Empty;
_didSnap = false;
}
if (_resizeComponents is not null && _resizeComponents.Length > 0)
{
// we do these separately so as not to disturb the cached sizes for values
// we're not actually changing. For example, if a control is docked top and
// we modify the height, the width shouldn't be modified.
PropertyDescriptor propWidth = TypeDescriptor.GetProperties(_resizeComponents[0].resizeControl)["Width"];
PropertyDescriptor propHeight = TypeDescriptor.GetProperties(_resizeComponents[0].resizeControl)["Height"];
PropertyDescriptor propTop = TypeDescriptor.GetProperties(_resizeComponents[0].resizeControl)["Top"];
PropertyDescriptor propLeft = TypeDescriptor.GetProperties(_resizeComponents[0].resizeControl)["Left"];
for (int i = 0; i < _resizeComponents.Length; i++)
{
if (propWidth is not null && _resizeComponents[i].resizeControl.Width != _resizeComponents[i].resizeBounds.Width)
{
propWidth.SetValue(_resizeComponents[i].resizeControl, _resizeComponents[i].resizeControl.Width);
}
if (propHeight is not null && _resizeComponents[i].resizeControl.Height != _resizeComponents[i].resizeBounds.Height)
{
propHeight.SetValue(_resizeComponents[i].resizeControl, _resizeComponents[i].resizeControl.Height);
}
if (propTop is not null && _resizeComponents[i].resizeControl.Top != _resizeComponents[i].resizeBounds.Y)
{
propTop.SetValue(_resizeComponents[i].resizeControl, _resizeComponents[i].resizeControl.Top);
}
if (propLeft is not null && _resizeComponents[i].resizeControl.Left != _resizeComponents[i].resizeBounds.X)
{
propLeft.SetValue(_resizeComponents[i].resizeControl, _resizeComponents[i].resizeControl.Left);
}
if (_resizeComponents[i].resizeControl == _primaryControl && _statusCommandUI is not null)
{
_statusCommandUI.SetStatusInformation(_primaryControl);
}
}
}
}
if (_resizeTransaction is not null)
{
DesignerTransaction t = _resizeTransaction;
_resizeTransaction = null;
using (t)
{
t.Commit();
}
}
}
finally
{
// This pops us off the stack, re-enables adorners and clears the "dragging" flag.
OnLoseCapture(g, EventArgs.Empty);
}
return false;
}
}
|