Restrict shape movement and resizing

Feb 18, 2013 at 9:14 AM
Edited Feb 19, 2013 at 4:43 AM
Hi,

What is the best way to restrict shape movement and resizing?

Once shape "A" has been located within bounds of shape "B", I want to forbid moving shape "A" out of these bounds, as well as forbid resizing of shape "B" which can make "A" fall out of these bounds. In other words - I'm back to my emulation of "container - child" relation between shapes.

[UPDATE] In my application, I'm using a custom selection tool , so I supposed that this tool is the right place to implement these restrictions. Seems that the only method I can override there is ProcessMouseEvent(), but... this doesn't make much sense because there's nothing like "Cancel" property in the MouseEventArgsDg type. Besides, it's not easy to understand there whether the mouse event being processed has been raised as part of a resize operation (and not just movement ). The other idea was to set cursor clipping in StartToolAction() and reset it in EndToolAction(), but I'm not sure that this is the best way (besides, it's again not clear how to understand whether current action is a resize operation - enum SelectionTool.Action doesn't contain such member as "Resize"). So I still need Your help :)
Feb 28, 2013 at 11:51 AM
Edited Mar 1, 2013 at 4:06 AM
For the moment I found out how to restrict shape movement (see code below); however I cannot distinguish movement from resizing - so this code, strictly speaking, is not valid; Your help here will be greatly appreciated. Besides, beginning with v.2.0.3, shapes can be moved by keyboard "arrows" as well - and so I have to restrict this also, but this part is not ready yet.

It would be nice to hear Your comments/suggestions. In my app, Host Shape lives "inside" a Computer Shape.
Besides, I'm not sure what should I pass to the GetBoundingRectangle() method - true or false; is it important in case of shapes derived from Box Shape?
private bool _isClippingEnabled = false;

private void OnMouseDown(object sender, MouseEventArgs e)
{
    if (_display == null)
        return;
    Point pt = Point.Empty;
    _display.ControlToDiagram(e.Location, out pt);
    Shape shape = _display.Diagram.FindShape<MyBaseShape>(pt);
    if (shape == null)
        return;
    if (shape is HostShape)
    {
        var hostRect = shape.GetBoundingRectangle(false);
        var cm = (shape.ModelObject as HostModel).Computer;
        if (cm != null)
        {
            // Restrict movement of the Host Shape:
            var compRect = (cm.Shapes.First() as ComputerShape).GetBoundingRectangle(false);
            var clippingRect = new Rectangle(
                pt.X - (hostRect.Left - compRect.Left) + 2,
                pt.Y - (hostRect.Top - compRect.Top) + 2,
                compRect.Width - hostRect.Width - 4,
                compRect.Height - hostRect.Height - 4);
            _display.DiagramToControl(clippingRect, out clippingRect);
            clippingRect = _display.RectangleToScreen(clippingRect); // (clipping rectangle must be expressed in SCREEN coordinates)
            Cursor.Clip = clippingRect;
            _isClippingEnabled = true;
        }
    }
}

private void OnMouseUp(object sender, MouseEventArgs e)
{
    if (_display == null)
        return;
    if (_isClippingEnabled)
    {
        Cursor.Clip = Rectangle.Empty;
        _isClippingEnabled = false;
    }
}

// Extensions:

public static class Extensions
{
    public static Shape FindShape<T>(this Diagram diagram, Point pt)
    {
        foreach (Shape shape in diagram.Shapes.TopDown)
        {
            if (shape is T && shape.ContainsPoint(pt.X, pt.Y))
                return shape;
        }
        return null;
    }
}
[UPDATE]: there's no need to distinguish movement from resizing - this code works well for both cases, except when you resize HostShape having clicked outside its boundaries so that FindShape<T> returns another shape - and this is possible because the connection points are thicker than the shape border. Currently I'm thinking how to take this into account...
Mar 1, 2013 at 7:30 AM
Edited Mar 1, 2013 at 9:10 AM
That's another version - taking into account the above mentioned issue:
private bool _isClippingEnabled = false;
private Point mouseDownLocation = Point.Empty;

private void OnMouseDown(object sender, MouseEventArgs e)
{
    if (_display == null)
        return;
    mouseDownLocation = e.Location;
    this.EnableClippingIfNeeded(e.Location);
}

private void DisableClipping()
{
    if (_isClippingEnabled)
    {
        Cursor.Clip = Rectangle.Empty;
        _isClippingEnabled = false;
    }
}

private void EnableClippingIfNeeded(Point displayCursorLocation)
{
    if (_isClippingEnabled)
        return; // already enabled - nothing to do
    if (!_display.SelectedShapes.Any())
        return;
    Point pt = Point.Empty;
    _display.ControlToDiagram(displayCursorLocation, out pt);
    var clippingRectFinal = new Rectangle(0, 0, int.MaxValue, int.MaxValue);
    foreach (var shape in _display.SelectedShapes)
    {
        var clippingRect = this.CalculateClippingRect(pt, shape);
        if (clippingRect != Rectangle.Empty)
            clippingRectFinal.Intersect(clippingRect);
    }
    if (clippingRectFinal.Width != int.MaxValue)
    {
        Cursor.Clip = clippingRectFinal;
        _isClippingEnabled = true;
    }
}

private Rectangle CalculateClippingRect(Point pt, Shape shape)
{
    IModelObject containerModel = null;
    if (shape is HostShape)
    {
        containerModel = (shape.ModelObject as HostModel).Computer;
        if (containerModel == null)
            return Rectangle.Empty;
    }
    else
        return Rectangle.Empty;

    // Restrict movement of the shape:
    if (!(_display.CurrentTool is MySelectionTool))
    {
        Debug.Fail("To correctly restrict shape resizing, current selection tool must derive from type MySelectionTool!");
        return Rectangle.Empty;
    }
    Rectangle clippingRect = Rectangle.Empty;
    var containerRect = containerModel.Shapes.First().GetBoundingRectangle(false);
    if ((_display.CurrentTool as MySelectionTool).IsResizing)
    {
        clippingRect = containerRect;
    }
    else if ((_display.CurrentTool as MySelectionTool).IsMoving)
    {
        Rectangle shapeRect = shape.GetBoundingRectangle(false);
        clippingRect = new Rectangle(
            pt.X - (shapeRect.Left - containerRect.Left) + 2,
            pt.Y - (shapeRect.Top - containerRect.Top) + 2,
            containerRect.Width - shapeRect.Width - 4,
            containerRect.Height - shapeRect.Height - 4);
    }
    else
        return Rectangle.Empty;

    _display.DiagramToControl(clippingRect, out clippingRect);
    clippingRect = _display.RectangleToScreen(clippingRect); // (clipping rectangle must be expressed in SCREEN coordinates)
    return clippingRect;
}

private void OnMouseUp(object sender, MouseEventArgs e)
{
    if (_display == null)
        return;
    this.DisableClipping();
}

private void OnMouseMove(object sender, MouseEventArgs e)
{
    if (_display == null)
        return;
    bool leftMouseButtonIsPressed = Control.MouseButtons.HasFlag(MouseButtons.Left);
    if (leftMouseButtonIsPressed)
        this.EnableClippingIfNeeded(mouseDownLocation);
}
This version also supports several selected shapes and the case when we start moving a shape when it's not selected yet (that's why we call EnableClippingIfNeeded() method from OnMouseMove() handler as well).

[UPDATE]: A-a-a... shame on me... I do need to distinguish movement from resizing! clipping rectangles must be different between these two cases! working on it... [UPDATE]: I corrected the CalculateClippingRect() method so that clipping rectangle is calculated differently for movement and resizing. This also requires a custom selection tool (in my code - MySelectionTool type) exposing 2 properties: IsResizing and IsMoving. These properties are based on the value of CurrentToolAction.Action (5 for resizing and 4 for moving).