using System; using System.Drawing; using System.Windows.Forms; namespace Netron.Diagramming.Core { /// /// This tool implement the action of moving shapes on the canvas. /// Note that this tool is slightly different than other tools since it activates itself unless it has been suspended by another tool. /// public class MoveTool : AbstractTool, IMouseListener { #region Fields /// /// the location of the mouse when the motion starts /// private Point initialPoint; /// /// the intermediate location of the mouse during the motion /// private Point lastPoint; private IConnector hoveredConnector; /// /// the AsConnectorMover field /// private bool connectorMove; /// /// Gets or sets the AsConnectorMover /// public bool AsConnectorMover { get { return connectorMove; } set { connectorMove = value; } } public static bool Blocked = false; #endregion #region Constructor /// /// Initializes a new instance of the class. /// /// The name of the tool. public MoveTool(string name) : base(name) { } #endregion #region Methods /// /// Called when the tool is activated. /// protected override void OnActivateTool() { Controller.View.CurrentCursor = CursorPalette.Move; } /// /// Handles the mouse down event /// /// The instance containing the event data. public bool MouseDown(MouseEventArgs e) { if (e == null) throw new ArgumentNullException("The argument object is 'null'"); if (e.Button == MouseButtons.Left && Enabled && !IsSuspended && !Blocked) { if (this.Controller.Model.Selection.SelectedItems.Count > 0) { initialPoint = e.Location; lastPoint = initialPoint; connectorMove = false; //while moving the shapes we'll clear the tracker this.Controller.View.ResetTracker(); //now, go for it this.ActivateTool(); // Even if an entity is hit, I don't think the mouse down // event should be handled here. That is, return false so // other mouse listeners can respond. This tool really // does its thing when the mouse is moved. //return true; } else if (this.Controller.Model.Selection.Connector != null && this.Controller.Model.Selection.Connector.Parent is IConnection) { if (!typeof(IShape).IsInstanceOfType(this.Controller.Model.Selection.Connector.Parent)) //note that there is a separate tool to move shape-connectors! { if (this.Controller.Model.Selection.Connector.AllowMove) { initialPoint = e.Location; lastPoint = initialPoint; connectorMove = true; //keep this for the final packaging into the undo manager this.ActivateTool(); //return true; } } } } return false; } /// /// Handles the mouse move event /// /// The instance containing the event data. public void MouseMove(MouseEventArgs e) { if (e == null) throw new ArgumentNullException("The argument object is 'null'"); Point point = e.Location; if (IsActive) { //can be a connector if (this.Controller.Model.Selection.Connector != null) { this.Controller.Model.Selection.Connector.MoveBy(new Point(point.X - lastPoint.X, point.Y - lastPoint.Y)); #region Do we hit something meaningful? if (hoveredConnector != null) hoveredConnector.Hovered = false; hoveredConnector = this.Controller.Model.Selection.FindConnectorAt(e.Location); if (hoveredConnector != null) hoveredConnector.Hovered = true; if (hoveredConnector != null && hoveredConnector != this.Controller.Model.Selection.Connector) { Controller.View.CurrentCursor = CursorPalette.Grip; } else Controller.View.CurrentCursor = CursorPalette.Move; #endregion } else //can be a selection { foreach (IDiagramEntity entity in this.Controller.Model.Selection.SelectedItems) { if (entity.AllowMove) { entity.MoveBy(new Point(point.X - lastPoint.X, point.Y - lastPoint.Y)); } } } lastPoint = point; } } /// /// When an entity is moved there are various possibilities: /// /// /// a connector is moved /// in this case the moved connector can only be part of a onnection because moving shape-connectors is not allowed unless by means of the . /// If a connector attached to a connection is moved we have the following fork: /// /// the connector was attached /// the connector has a parent and needs to be detached before being moved and eventually attached to another connector /// the connector is moved /// this is a standard motion and is similar for any /// /// the connector ends up somewhere near another connector and will become attached to it /// the connector in the proximity of the moved connector will become the parent of it. Note that we previously detached any binding and that a connector can have only one parent. /// /// /// /// an entity is moved /// the normal can be used /// a set of entities is moved /// we need to create a bundle to package the entities and then use the /// /// Several important remarks are in order here: /// /// a connector can have only one parent /// we need to package a move action in a command but this command needs NOT to be performed (i.e. call the Redo() method) because the motion already occured through the MouseMove handler. Because of this situation we cannot perform a Redo() on the full package since it would move the entities twice. Hence, commands are execute just after their creation (except for the move). /// when the stack of actions are undone the stack has to be reverted /// /// whatever the situation is, the most economical way to code the different cases is by means of a object /// /// /// The instance containing the event data. public void MouseUp(MouseEventArgs e) { if (IsActive) { DeactivateTool(); //creation of the total undoredo package CompoundCommand package = new CompoundCommand(this.Controller); string message = string.Empty; //notice that the connector can only be a connection connector because of the MouseDown check above if (connectorMove) { #region We are moving a connection-connector #region Is the connector attached? //detach only if there is a parent different than a connection; the join of a chained connection is allowed to move if (this.Controller.Model.Selection.Connector.AttachedTo != null && !typeof(IConnection).IsInstanceOfType(this.Controller.Model.Selection.Connector.AttachedTo)) { DetachConnectorCommand detach = new DetachConnectorCommand(this.Controller, this.Controller.Model.Selection.Connector.AttachedTo, this.Controller.Model.Selection.Connector); detach.Redo(); package.Commands.Add(detach); } #endregion #region The moving part //a bundle might look like overkill here but it makes the coding model uniform Bundle bundle = new Bundle(Controller.Model); bundle.Entities.Add(this.Controller.Model.Selection.Connector); MoveCommand move = new MoveCommand(this.Controller, bundle, new Point(lastPoint.X - initialPoint.X, lastPoint.Y - initialPoint.Y)); //no Redo() necessary here! package.Commands.Add(move); #endregion #region The re-attachment near another connector //let's see if the connection endpoints hit other connectors (different than the selected one!) //define a predicate delegate to filter things out otherwise the hit will return the moved connector which would results //in a stack overflow later on Predicate predicate = delegate(IConnector conn) { //whatever, except itself and any children of the moved connector //since this would entail a child becoming a parent! if (conn.Hit(e.Location) && conn != this.Controller.Model.Selection.Connector && !this.Controller.Model.Selection.Connector.AttachedConnectors.Contains(conn)) return true; return false; }; //find it! IConnector parentConnector = this.Controller.Model.Selection.FindConnector(predicate); if (parentConnector != null) //aha, there's an attachment { BindConnectorsCommand binder = new BindConnectorsCommand(this.Controller, parentConnector, this.Controller.Model.Selection.Connector); package.Commands.Add(binder); binder.Redo(); //this one is necessary since the redo cannot be performed on the whole compound command } #endregion message = "Connector move"; #endregion } else { #region We are moving entities other than a connector Bundle bundle = new Bundle(Controller.Model); bundle.Entities.AddRange(this.Controller.Model.Selection.SelectedItems); MoveCommand cmd = new MoveCommand(this.Controller, bundle, new Point(lastPoint.X - initialPoint.X, lastPoint.Y - initialPoint.Y)); package.Commands.Add(cmd); //not necessary to perform the Redo action of the command since the mouse-move already moved the bundle! #endregion message = "Entities move"; } //reset the hovered connector, if any if (hoveredConnector != null) hoveredConnector.Hovered = false; package.Text = message; //whatever the content of the package we add it to the undo history this.Controller.UndoManager.AddUndoCommand(package); //show the tracker again this.Controller.View.ShowTracker(); } } #endregion } }