Click here to Skip to main content
15,896,348 members
Articles / Programming Languages / C#

DSGraphEdit: A Reasonable Facsimile of Microsoft's GraphEdit in .NET

Rate me:
Please Sign up or sign in to vote.
4.93/5 (79 votes)
28 Jan 2018MIT7 min read 304.5K   10K   142  
A library for adding DirectShow GraphEdit-like abilities to .NET applications
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.ComponentModel;
using System.Collections.Generic;
using System.Text;

using DaggerLib.Interfaces;
using DaggerLib.Core;

namespace DaggerLib.UI.Windows
{
    public class DaggerUINode : UserControl , IDaggerUINode
    {
        #region Fields

        //the parent UIGraph that holds this UINode
        internal DaggerUIGraph _parentGraph;

        //flag indicating the Control is fully initialized
        private bool _isIntialized = false;

        //the node this UI element represents
        internal DaggerNode _node;

        //the node that was created by the designer
        private DaggerNode _designtimeNode;

        //collection of user defined buttons in the caption
        private CaptionButtonCollection _captionButtons;

        //the state of the node we can alter depending on mouse locations or other conditions
        private DaggerNodeAlterState _canAlterState = DaggerNodeAlterState.None;

        /// <summary>
        /// Where we should place the pins on the panels
        /// </summary>
        private DaggerNodePinPlacement _pinPlacement = DaggerNodePinPlacement.Outset;

        /// <summary>
        /// Size in pixels to shrink internal Control
        /// </summary>
        private int _internalControlPadding = 5;

        /// <summary>
        /// Radius for the rounded corners
        /// </summary>
        private float _panelRadius = 9f;

        /// <summary>
        /// The height in Pixels of the caption
        /// </summary>
        private int _captionSize = 14;

        /// <summary>
        /// How far in the indentation curve for the pins should be
        /// </summary>
        private float _pinIndent = 1f;

        /// <summary>
        /// Can we resize the panel?
        /// </summary>
        private bool _resizable = false;

        /// <summary>
        /// The caption
        /// </summary>
        private string _captionText;

        /// <summary>
        /// Caption Colors
        /// </summary>
        private Color _captionColor = Color.Blue;
        private Color _captionColorUnfocused = Color.LightSteelBlue;
        private bool _isFocused;

        /// <summary>
        /// The Backgound image for the node
        /// </summary>
        private Bitmap _backImage;

        /// <summary>
        /// Paths for drawing the Caption and Panel
        /// </summary>
        private GraphicsPath _clientPath;
        private GraphicsPath _captionPath;

        // path for resize widget
        private GraphicsPath _resizePath;

        //the spacing between the pins
        private int _pinSpacing = 2;

        //the pin that the cursor is currently over
        private DaggerBasePin _mouseOverPin;

        //the internal panel of the UINode
        protected Panel _internalControl;

        //the internal control centered in the node (defaults to a panel)

        //panel icon
        private Bitmap _panelIcon;

        //the pin context menu
        private BasePinContextMenuStrip _pinContextMenu;

        //the user defined context menu
        private ContextMenuStrip _userContextMenu;

        //our internal ContextMenuStrip items and the list that holds them
        private List<ToolStripItem> _internalContextMenuItems;
        private ToolStripSeparator _ictxtSeparator;
        private ToolStripMenuItem _ictxtCreateSubNode;
        private ToolStripMenuItem _ictxtDeleteRelink;
        private ToolStripMenuItem _ictxtProcessNode;

        //our ToolTip thingy
        private System.Windows.Forms.ToolTip _wndToolTip;

        //our ToolTip text
        private string _tooltiptext = "";

        private bool _isProcessing = false;


        #endregion

        private IContainer components;

        #region Target Regions

        /// <summary>
        /// The client area for the caption
        /// </summary>
        private Region _targetRegionCaption;

        /// <summary>
        /// Target region for resize widget
        /// </summary>
        private Region _targetRegionResize;

        #endregion

        #region ctor

        public DaggerUINode()
        {
            //set the usercontrol up for doublebuffered drawing
            this.SetStyle(ControlStyles.UserPaint, true);
            this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            UpdateStyles();

            _captionButtons = new CaptionButtonCollection();
            _captionButtons.ButtonAdded += new EventHandler(_captionButtons_ButtonAdded);
            _captionButtons.ButtonRemoved += new EventHandler(_captionButtons_ButtonRemoved);
            _captionButtons.ButtonVisibleChanged += new EventHandler(_captionButtons_ButtonVisibleChanged);

            //create the context menu to interact with pins
            _pinContextMenu = new BasePinContextMenuStrip();

            //create and set the Design Time node.
            //this will most likely be replaced by a decendent, but we never want it to be null
            _designtimeNode = new DaggerNode();
            Node = _designtimeNode;

            //create the ToolTip control
            _wndToolTip = new ToolTip();
            _wndToolTip.Active = true;

            InitializeComponent();

            //set up our ToolTip control            
            _wndToolTip.SetToolTip(this, "DaggerNode");
            _wndToolTip.SetToolTip(_internalControl, "DaggerNode");            
            _wndToolTip.AutomaticDelay = 500;
            _wndToolTip.ReshowDelay = 100;
            _wndToolTip.OwnerDraw = true;
            _wndToolTip.Popup += new PopupEventHandler(m_wndToolTip_Popup);
            _wndToolTip.Draw += new DrawToolTipEventHandler(m_wndToolTip_Draw);

            //create the internal contextmenustrip items
            _internalContextMenuItems = new List<ToolStripItem>();
            _ictxtSeparator = new ToolStripSeparator();
            _internalContextMenuItems.Add(_ictxtSeparator);

            _ictxtCreateSubNode = new ToolStripMenuItem("Create SubNode");
            _ictxtCreateSubNode.Click += new EventHandler(_ictxtCreateSubNode_Click);
            _internalContextMenuItems.Add(_ictxtCreateSubNode);

            _ictxtDeleteRelink = new ToolStripMenuItem("Delete and Relink");
            _ictxtDeleteRelink.Click += new EventHandler(_ictxtDeleteRelink_Click);
            _internalContextMenuItems.Add(_ictxtDeleteRelink);

            _ictxtProcessNode = new ToolStripMenuItem("Process Node");
            _ictxtProcessNode.Click += new EventHandler(_ictxtProcessNode_Click);
            _internalContextMenuItems.Add(_ictxtProcessNode);

            // we want this true so we can drag pins to connect them
            this.AllowDrop = true;

            // add the internal Control
            this.Controls.Add(_internalControl);

            // hook up to internal panel's click so we can track focus
            _internalControl.Click += new EventHandler(_internalPanel_Click);

            // generate all the paths, regions, and backimage
            CalculateLayout();

            _isIntialized = true;
        }

        private void InitializeComponent()
        {
            this._internalControl = new DoubleBufferedPanel();
            this.SuspendLayout();
            // 
            // _internalControl
            // 
            this._internalControl.Location = new System.Drawing.Point(0, 0);
            this._internalControl.Name = "_internalControl";
            this._internalControl.Size = new System.Drawing.Size(611, 418);
            this._internalControl.TabIndex = 0;
            this._internalControl.Tag = "Internal";
            // 
            // DaggerUINode
            // 
            this.Name = "DaggerUINode";
            this.Size = new System.Drawing.Size(103, 72);
            this.ContextMenuStripChanged += new System.EventHandler(this.DaggerUINode_ContextMenuStripChanged);
            this.ResumeLayout(false);

        }

        #endregion

        #region Events and Handlers

        /// <summary>
        ///  Event that is raised when the UI Node is Activated
        /// </summary>
        public event EventHandler Activated;

        /// <summary>
        /// Event thay is raised when the UI Node is Deactivated
        /// </summary>
        public event EventHandler Deactivated;

        void _captionButtons_ButtonRemoved(object sender, EventArgs e)
        {
            Controls.Remove(sender as SimpleImageButton);
            ArrangeCaptionButtons();
        }

        void _captionButtons_ButtonAdded(object sender, EventArgs e)
        {
            (sender as SimpleImageButton).Size = new Size(_captionSize, _captionSize);
            Controls.Add(sender as SimpleImageButton);
            ArrangeCaptionButtons();
        }

        void _captionButtons_ButtonVisibleChanged(object sender, EventArgs e)
        {
            ArrangeCaptionButtons();
        }

        void _ictxtProcessNode_Click(object sender, EventArgs e)
        {
            _node.Process();
        }

        void _ictxtStackSelectedNodes_Click(object sender, EventArgs e)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        /// <summary>
        /// Event that is raised when the user Clicks "Create SubNode"
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void _ictxtCreateSubNode_Click(object sender, EventArgs e)
        {
            byte[] subnode = _node.ParentGraph.SerializeSubGraph(_node.SubgraphAffiliation);

            if (subnode != null)
            {
                if (_parentGraph.DaggerNodeTreeView != null)
                {
                    ValueEditorDialog vue = new ValueEditorDialog("Subnode Name","Subnode");
                    if (vue.ShowDialog() == DialogResult.OK)
                    {
                        _parentGraph.DaggerNodeTreeView.AddSubNode("User functions", (string)vue.Data, false, subnode);
                    }
                }
                else
                {
                    ValueEditorDialog vue = new ValueEditorDialog("Subnode Name", "Subnode");
                    if (vue.ShowDialog() == DialogResult.OK)
                    {
                        _node.ParentGraph.AddNode(new DaggerSubNode((string)vue.Data,subnode));
                    }
                }
            }
        }

        /// <summary>
        /// Event that is raised when user selects "Delete and Relink"
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void _ictxtDeleteRelink_Click(object sender, EventArgs e)
        {
            _parentGraph.DeleteAndRelink(_node);
        }

        /// <summary>
        /// Event that is raised when a DaggerNode is attached to this UINode
        /// </summary>
        public event DaggerNodeAttachedHandler DaggerNodeAttached;

        /// <summary>
        /// Event that is raised before a pin shows it's context menu
        /// </summary>
        public event DaggerBasePinBeforeShowContextMenuHandler BeforePinContextShown;

        /// <summary>
        /// Set the focus of the Node if clicked
        /// </summary>
        void _internalPanel_Click(object sender, EventArgs e)
        {
            this.Focus();
        }

        //Events to Handle the Adding/Removing of pins from the collections
        void _outputPins_PinRemoved(object sender, DaggerBasePin pin)
        {
            CalculateLayout();
        }
        void _outputPins_PinAdded(object sender, DaggerBasePin pin)
        {
            CreatePinUIElements(pin);
            CalculateLayout();
        }
        void _inputPins_PinRemoved(object sender, DaggerBasePin pin)
        {
            CalculateLayout();
        }
        void _inputPins_PinAdded(object sender, DaggerBasePin pin)
        {
            CreatePinUIElements(pin);
            CalculateLayout();
        }

        /// <summary>
        /// Adjust ToolTip size to reflect desired text
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void m_wndToolTip_Popup(object sender, PopupEventArgs e)
        {
            using (Font f = new Font("Tahoma", 9))
            {
                if (_mouseOverPin != null)
                {
                    //if the user has defined PinToolTipPopup, call that instead
                    if ((_mouseOverPin.PinUIElements as PinUI).UserDefinedToolTipPopup)
                    {
                        (_mouseOverPin.PinUIElements as PinUI).InvokeToolTipPopup(_mouseOverPin, e);
                    }
                    else
                    {
                        //measure the strings for pin name and pin data type
                        Size tts = TextRenderer.MeasureText(_mouseOverPin.Name, f);
                        Size typeSize = new Size();

                        switch ((_mouseOverPin.PinUIElements as PinUI).PinToolTipStyle)
                        {
                            case PinToolTipStyle.Name:
                                break;
                            case PinToolTipStyle.NameShortType:
                                typeSize = TextRenderer.MeasureText(_mouseOverPin.DataType.Name, f);
                                tts.Width = Math.Max(tts.Width, typeSize.Width);
                                tts.Height = tts.Height * 2 + 5;
                                break;
                            case PinToolTipStyle.NameLongType:
                                typeSize = TextRenderer.MeasureText( _mouseOverPin.DataType.FullName, f);
                                tts.Width = Math.Max(tts.Width, typeSize.Width);
                                tts.Height = tts.Height * 2 + 5;
                                break;
                            default:
                                break;
                        }

                        e.ToolTipSize = tts;
                    }
                }
                else if (_tooltiptext != "")
                {
                    e.ToolTipSize = TextRenderer.MeasureText(
                        _tooltiptext + (_parentGraph.ShowOrdinal?": Ordinal " + _node.Ordinal.ToString():""), f);
                }
                else
                {
                    e.ToolTipSize = TextRenderer.MeasureText(
                        _captionText + (_parentGraph.ShowOrdinal ? ": Ordinal " + _node.Ordinal.ToString() : ""), f);
                }
            }
        }

        /// <summary>
        /// Draw the ToolTip with the desired text
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void m_wndToolTip_Draw(object sender, DrawToolTipEventArgs e)
        {
            //if the user has defined PinToolTipPopup, call that instead
            if (_mouseOverPin != null && (_mouseOverPin.PinUIElements as PinUI).UserDefinedToolTipDraw)
            {
                (_mouseOverPin.PinUIElements as PinUI).InvokeToolTipDraw(_mouseOverPin, e);
            }
            else
            {
                // what text shall we draw?
                string theText = (_mouseOverPin != null) ? _mouseOverPin.Name :
                    ((_tooltiptext == "") ? _captionText : _tooltiptext) + (_parentGraph.ShowOrdinal ? ": Ordinal " + _node.Ordinal.ToString() : "");

                Color textColor = SystemColors.ActiveCaptionText;

                if (_mouseOverPin != null)
                {
                    textColor = ControlPaint.Light((_mouseOverPin.PinUIElements as PinUI).NoodleColor);

                    using (Pen ddp = new Pen((_mouseOverPin.PinUIElements as PinUI).NoodleColor))
                    {
                        using (Brush ddb = new SolidBrush(ControlPaint.Dark((_mouseOverPin.PinUIElements as PinUI).NoodleColor)))
                        {

                            e.Graphics.FillRectangle(ddb, e.Bounds);

                            switch ((_mouseOverPin.PinUIElements as PinUI).PinToolTipStyle)
                            {
                                case PinToolTipStyle.Name:
                                    break;
                                case PinToolTipStyle.NameShortType:
                                    theText += "\n" + _mouseOverPin.DataType.Name;
                                    //draw serperator line
                                    e.Graphics.DrawLine(ddp, 0, e.Bounds.Height / 2, e.Bounds.Width, e.Bounds.Height / 2);
                                    break;
                                case PinToolTipStyle.NameLongType:
                                    theText += "\n" + _mouseOverPin.DataType.FullName;
                                    //draw serperator line
                                    e.Graphics.DrawLine(ddp, 0, e.Bounds.Height / 2, e.Bounds.Width, e.Bounds.Height / 2);
                                    break;
                                default:
                                    break;
                            }
                        }
                    }
                }
                else
                {
                    // Draw the custom background.
                    e.Graphics.FillRectangle(SystemBrushes.ActiveCaption, e.Bounds);
                }

                // Draw the standard border.
                e.DrawBorder();

                // Draw the custom text.
                using (StringFormat sf = new StringFormat())
                {
                    sf.Alignment = StringAlignment.Center;
                    sf.LineAlignment = StringAlignment.Center;
                    sf.HotkeyPrefix = System.Drawing.Text.HotkeyPrefix.None;
                    using (Font f = new Font("Tahoma", 9))
                    {
                        using (Brush textBrush = new SolidBrush(textColor))
                        {
                            e.Graphics.DrawString(theText, f,
                                textBrush, e.Bounds, sf);
                        }
                    }
                }
            }
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets if this node has focus (use instead of this.Focused)
        /// </summary>
        [Browsable(false)]
        public bool IsFocused
        {
            get
            {
                return _isFocused;
            }
        }

        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public CaptionButtonCollection CaptionButtons
        {
            get
            {
                return _captionButtons;
            }
        }

        /// <summary>
        /// Gets the parent ui graph this ui node belongs to
        /// </summary>
        [Browsable(false)]
        public IDaggerUIGraph ParentUIGraph
        {
            get
            {
                return (IDaggerUIGraph)_parentGraph;
            }
        }

        /// <summary>
        /// Gets or sets the underlying DaggerNode for this UI element
        /// </summary>
        [Browsable(false)]
        public DaggerNode Node
        {
            get
            {
                return _node;
            }
            set
            {
                if (value != null)
                {
                    _node = value;

                    // merge the designtime node's pins with this node
                    foreach (DaggerOutputPin pin in _designtimeNode.OutputPins)
                    {
                        if (!_node.OutputPins.Contains(pin))
                        {
                            _node.OutputPins.Add(pin);
                        }
                    }
                    foreach (DaggerInputPin pin in _designtimeNode.InputPins)
                    {
                        if (!_node.InputPins.Contains(pin))
                        {
                            _node.InputPins.Add(pin);
                        }
                    }

                    // create the UI Elements for the pins
                    foreach (DaggerOutputPin pin in _node.OutputPins)
                    {
                        CreatePinUIElements(pin);
                    }
                    foreach (DaggerInputPin pin in _node.InputPins)
                    {
                        CreatePinUIElements(pin);
                    }

                    //set the default caption
                    CaptionText = _node.ToString();

                    //let the node know this UI is controlling it
                    _node.UINode = this as IDaggerUINode;

                    //hook the events to the Pin Collections
                    _node.InputPins.PinAdded += new DaggerPinAdded(_inputPins_PinAdded);
                    _node.InputPins.PinRemoved += new DaggerPinRemoved(_inputPins_PinRemoved);

                    _node.OutputPins.PinAdded += new DaggerPinAdded(_outputPins_PinAdded);
                    _node.OutputPins.PinRemoved += new DaggerPinRemoved(_outputPins_PinRemoved);

                    //only do this if we are fully initialized
                    if (_isIntialized)
                    {
                        CalculateLayout();

                        //raise the Attached Events
                        if (DaggerNodeAttached != null)
                        {
                            DaggerNodeAttached(_node);
                        }
                        _node.InvokeUINodeAttached(this as IDaggerUINode);
                    }
                }
            }
        }

        [Browsable(false)]
        public int PinSize
        {
            get
            {
                if (_parentGraph != null)
                {
                    return _parentGraph.PinLegend.PinSize;
                }
                else
                {
                    return 11;
                }
            }
        }

        /// <summary>
        /// The Internal Control or Container
        /// </summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public Panel InternalControl
        {
            get
            {
                return this._internalControl;
            }
        }

        public bool IsProcessing
        {
            get
            {
                return _isProcessing;
            }
            set
            {
                _isProcessing = value;
                if (this.InvokeRequired)
                {
                    this.BeginInvoke(new MethodInvoker(delegate()
                    {
                        DrawCaption(null);
                        //only update the caption area
                        Invalidate(_targetRegionCaption);
                    }));
                }
                else
                {
                    DrawCaption(null);
                    //only update the caption area
                    Invalidate(_targetRegionCaption);
                }
            }
        }

        /// <summary>
        /// Nodes that are directed into this node
        /// </summary>
        [Browsable(true), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public DaggerPinCollection<DaggerInputPin> InputPins
        {
            get
            {
                return _node.InputPins;
            }
        }

        /// <summary>
        /// Adjacency list for this node
        /// </summary>
        [Browsable(true), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public DaggerPinCollection<DaggerOutputPin> OutputPins
        {
            get
            {
                return _node.OutputPins;
            }
        }

        public Bitmap CaptionIcon
        {
            get
            {
                return _panelIcon;
            }
            set
            {
                _panelIcon = value;

                DrawCaption(null);
                //only update the caption area
                this.Invalidate(_targetRegionCaption);
            }
        }

        public Color CaptionColor
        {
            get
            {
                return _captionColor;
            }
            set
            {
                _captionColor = value;
                if (this.Focused)
                {
                    DrawCaption(null);
                    //only update the caption area
                    this.Invalidate(_targetRegionCaption);
                }
            }
        }

        public Color CaptionColorUnfocused
        {
            get
            {
                return _captionColorUnfocused;
            }
            set
            {
                _captionColorUnfocused = value;
                if (!this.Focused)
                {
                    DrawCaption(null);
                    //only update the caption area
                    this.Invalidate(_targetRegionCaption);
                }
            }
        }

        public int CaptionSize
        {
            get
            {
                return _captionSize;
            }
            set
            {
                _captionSize = value;
                CalculateLayout();
            }
        }

        /// <summary>
        /// Get or Set the radius of the rounded edges
        /// </summary>
        public float PanelRadius
        {
            get
            {
                return _panelRadius;
            }
            set
            {
                _panelRadius = value;
                CalculateLayout();
            }
        }

        public DaggerNodePinPlacement PinPlacement
        {
            get
            {
                return _pinPlacement;
            }
            set
            {
                _pinPlacement = value;
                CalculateLayout();
            }
        }

        /// <summary>
        /// Text that is displayed in the Caption
        /// </summary>
        public string CaptionText
        {
            get
            {
                return _captionText;
            }
            set
            {
                _captionText = value;

                try
                {
                    //recreate caption
                    DrawCaption(null);

                    //only update the caption area
                    this.Invalidate(_targetRegionCaption);
                }
                catch (Exception ex)
                {
                    // the uinode may not be fully formed yet
                }
            }
        }

        /// <summary>
        /// Gets the minimum size of control to allow pins, caption, and caption buttons to be visible
        /// </summary>
        public Size NodeMinimumSize
        {
            get
            {
                int neededPinSize = InputPins.Count * (_parentGraph.PinLegend.PinSize + _pinSpacing) + (_captionSize) + _internalControlPadding;
                neededPinSize = Math.Max(neededPinSize, OutputPins.Count * (_parentGraph.PinLegend.PinSize + _pinSpacing) + (_captionSize)) + _internalControlPadding;

                Graphics g = Graphics.FromHwnd(Handle);
                int capsize = (int)g.MeasureString(_captionText, Font).Width + 2 + _captionButtons.Count * _captionSize;
                switch (_pinPlacement)
                {
                    case DaggerNodePinPlacement.Indent:
                    case DaggerNodePinPlacement.Inset:
                        {
                            capsize += _pinSpacing * 2;
                        }
                        break;
                    case DaggerNodePinPlacement.Outset:
                        {
                            capsize += _pinSpacing * 2 + (_parentGraph.PinLegend.PinSize * 2);
                        }
                        break;
                    default:
                        break;
                }
                if (CaptionIcon != null)
                {
                    capsize += _captionSize + 2;
                }

                return new Size((int)Math.Max(capsize , InternalControlMinimumSize.Width + (PinSize * 2)), neededPinSize + InternalControlMinimumSize.Height);
            }
        }

        /// <summary>
        /// Get the minimum size for the internal control
        /// </summary>
        public virtual Size InternalControlMinimumSize
        {
            get
            {
                return new Size(0, 0);
            }
        }

        /// <summary>
        /// Gets the area the node occupies to calculate auto-arranging
        /// </summary>
        public virtual Size AutoArrangeSize
        {
            get
            {
                return this.Size;
            }
        }

        /// <summary>
        /// Gets the offset the node should placed during auto-arranging
        /// </summary>
        public virtual Point AutoArrangeOffset
        {
            get
            {
                return new Point(0, 0);
            }
        }

        public bool ToolTipActive
        {
            get
            {
                return _wndToolTip.Active;
            }
            set
            {
                _wndToolTip.Active = value;
            }
        }

        public string ToolTipText
        {
            get
            {
                return _tooltiptext;
            }
            set
            {
                _tooltiptext = value;
            }
        }

        /// <summary>
        /// Distance between the internal panel and eddges of node
        /// </summary>
        public int InternalControlPadding
        {
            get
            {
                return _internalControlPadding;
            }
            set
            {
                _internalControlPadding = value;
                CalculateLayout();
            }
        }

        /// <summary>
        /// The vertical padding between pins
        /// </summary>
        public int PinSpacing
        {
            get
            {
                return _pinSpacing;
            }
            set
            {
                _pinSpacing = value;
                CalculateLayout();
            }
        }

        /// <summary>
        /// How far to curve the node's sides when pins are present
        /// </summary>
        public float PinBevelIndent
        {
            get
            {
                return _pinIndent;
            }
            set
            {
                _pinIndent = Math.Max(0.6f, value);
                CalculateLayout();
            }
        }

        /// <summary>
        /// Gets or sets if the UINode can be resized
        /// </summary>
        public bool Resizable
        {
            get
            {
                return _resizable;
            }
            set
            {
                _resizable = value;
                CalculateLayout();
            }
        }

        /// <summary>
        /// Menu strip to show for this Node
        /// </summary>
        public new ContextMenuStrip ContextMenuStrip
        {
            get
            {
                return _userContextMenu;
            }
            set
            {
                _userContextMenu = value;
                _userContextMenu.Opening += new CancelEventHandler(_userContextMenu_Opening);
                _userContextMenu.Closed += new ToolStripDropDownClosedEventHandler(_userContextMenu_Closed);
            }
        }

        #region ContextMenuStrip Merging

        // we want to merge our menu items with the User defined ctxt menu
        // but we don't want to keep them there when not displaying it.
        // This is to prevent the end-user from getting confused if they use indexers
        // on the ToolStripItemCollection and are unaware that we shoe-horned in some extra menu items.
        void _userContextMenu_Opening(object sender, CancelEventArgs e)
        {
            //add our menu items
            foreach (ToolStripItem ti in _internalContextMenuItems)
            {
                _userContextMenu.Items.Add(ti);
            }

            ValidateContextMenuStrip(_userContextMenu);
        }

        void _userContextMenu_Closed(object sender, ToolStripDropDownClosedEventArgs e)
        {
            //remove our menu items
            foreach (ToolStripItem ti in _internalContextMenuItems)
            {
                _userContextMenu.Items.Remove(ti);
            }
        }

        #endregion

        #endregion

        #region Overrides

        protected override void OnParentChanged(EventArgs e)
        {
            base.OnParentChanged(e);

            _parentGraph = Parent as DaggerUIGraph;

            //refresh to use the PinLegend of this the new UIGraph
            CalculateLayout();
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);

            //see if we are over a pin
            _canAlterState = CanConnectToPin(e.Location);

            //move zone
            if (_targetRegionCaption.IsVisible(e.Location))
            {
                _canAlterState = DaggerNodeAlterState.Move;
            }

            if (_resizable && _targetRegionResize.IsVisible(e.Location))
            {
                _canAlterState = DaggerNodeAlterState.SouthEast;
            }

            switch (_canAlterState)
            {
                case DaggerNodeAlterState.CanConnectToInput:
                    {
                        Bitmap cursorimage = (Bitmap)_parentGraph.CursorImages.Images[1];
                        Cursor = new Cursor(cursorimage.GetHicon());
                    }
                    break;
                case DaggerNodeAlterState.CanConnectToOutput:
                    {
                        Bitmap cursorimage = (Bitmap)_parentGraph.CursorImages.Images[0];
                        Cursor = new Cursor(cursorimage.GetHicon());
                    }
                    break;
                case DaggerNodeAlterState.ConnectFromOutput:
                    {
                        Bitmap cursorimage = (Bitmap)_parentGraph.CursorImages.Images[1];
                        Cursor = new Cursor(cursorimage.GetHicon());
                    }
                    break;
                case DaggerNodeAlterState.ConnectFromInput:
                    {
                        Bitmap cursorimage = (Bitmap)_parentGraph.CursorImages.Images[0];
                        Cursor = new Cursor(cursorimage.GetHicon());
                    }
                    break;
                case DaggerNodeAlterState.None:
                    Cursor = Cursors.Default;
                    break;
                case DaggerNodeAlterState.Context:
                    Cursor = Cursors.Default;
                    break;
                case DaggerNodeAlterState.Move:
                    Cursor = Cursors.Default;
                    break;
                case DaggerNodeAlterState.SouthEast:
                    Cursor = Cursors.SizeNWSE;
                    break;
                default:
                    Cursor = Cursors.Default;
                    break;
            }
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                //execute the _canAlterState
                switch (_canAlterState)
                {
                    case DaggerNodeAlterState.Move:
                        {
                            DaggerOverlay eol = null;

                            // if more than one control is selected do a multi-control DaggerOverlay
                            if ((ParentUIGraph as DaggerUIGraph)._trackingSelector != null && (ParentUIGraph as DaggerUIGraph)._trackingSelector.SelectedNodes.Contains(this as IDaggerUINode))
                            {
                                // convert the list of DaggerUINodes to Controls
                                List<Control> tl = new List<Control>();
                                foreach (DaggerUINode nn in (ParentUIGraph as DaggerUIGraph)._trackingSelector.SelectedNodes)
                                {
                                    tl.Add((Control)nn);
                                }
                                eol = new DaggerOverlay(tl, PointToScreen(e.Location));
                            }
                            else
                            {
                                eol = new DaggerOverlay(this, _canAlterState, PointToScreen(e.Location), NodeMinimumSize.Width, NodeMinimumSize.Height);
                            }

                            Cursor = Cursors.SizeAll;
                            eol.BeginOperations();
                        }
                        break;
                    case DaggerNodeAlterState.SouthEast:
                        {
                            DaggerOverlay eol = new DaggerOverlay(this, _canAlterState, PointToScreen(e.Location), NodeMinimumSize.Width, NodeMinimumSize.Height);
                            eol.BeginOperations();
                        }
                        break;
                    case DaggerNodeAlterState.ConnectFromOutput:
                        if (_parentGraph != null)
                        {
                            _parentGraph.BeginOutputPinConnect((DaggerOutputPin)_mouseOverPin);
                            DoDragDrop(this, DragDropEffects.All);
                        }
                        break;
                    case DaggerNodeAlterState.ConnectFromInput:
                        if (_parentGraph != null)
                        {
                            _parentGraph.BeginInputPinConnect((DaggerInputPin)_mouseOverPin);
                            DoDragDrop(this, DragDropEffects.All);
                        }
                        break;
                    case DaggerNodeAlterState.CanConnectToOutput:
                    case DaggerNodeAlterState.CanConnectToInput:
                        {
                            if (_mouseOverPin is DaggerInputPin)
                            {
                                _parentGraph.EndPinConnect((DaggerInputPin)_mouseOverPin);
                            }
                            else
                            {
                                _parentGraph.EndPinConnect((DaggerOutputPin)_mouseOverPin);
                            }
                        }
                        break;
                    default:
                        break;
                }
            }
            else
            {
                //musta been a right click
                if (_mouseOverPin != null)
                {
                    _pinContextMenu.Tag = _mouseOverPin;
                    if (BeforePinContextShown != null)
                    {
                        BeforePinContextShown(_mouseOverPin);
                    }

                    // stop pin connections
                    _parentGraph.StopPinConnect();
                    _mouseOverPin = null;
                    Cursor = Cursors.Default;
                    _canAlterState = DaggerNodeAlterState.None;

                    _pinContextMenu.Show(PointToScreen(e.Location));
                }
                else
                {
                    if (_userContextMenu != null)
                    {
                        _userContextMenu.Show(PointToScreen(e.Location));
                    }
                    else
                    {
                        //no user defined ContextMenu so create and show a temp one with _internalContextMenuItems 
                        ContextMenuStrip tcon = new ContextMenuStrip();
                        foreach (ToolStripItem ti in _internalContextMenuItems)
                        {
                            tcon.Items.Add(ti);
                        }
                                                
                        ValidateContextMenuStrip(tcon);

                        tcon.Show(PointToScreen(e.Location));
                    }
                }
            }

            base.OnMouseDown(e);
        }

        //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // We use a combination of MouseDown, MouseMove AND DragDrop to allow the user 
        // the choice of clicking pins or dragging them to connect.
        //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>
        /// See if the user is dragging a pin into this node
        /// </summary>
        /// <param name="drgevent"></param>
        protected override void OnDragOver(DragEventArgs drgevent)
        {
            base.OnDragOver(drgevent);

            // since we cancel Noodle dragging whenever the topmost form loses focus,
            // we know that if TrackingPin != null, we are dragging a pin and don't
            // have to bother with serializing and type conversion of Pins.

            if (_parentGraph != null)
            {
                if (_parentGraph.TrackingPin != null)
                {
                    // Disallow a node from connecting to itself
                    if ((_parentGraph.TrackingPin.ParentNode != null && _parentGraph.TrackingPin.ParentNode.UINode != this) || _parentGraph.TrackingPin.ParentNode == null)
                    {
                        //convert the screen space to client space
                        Point clientPoint = PointToClient(new Point(drgevent.X, drgevent.Y));

                        //get the alterState
                        DaggerNodeAlterState alterState = CanConnectToPin(clientPoint);

                        //can we allow the dragged pin to be dropped here?
                        if (_parentGraph.TrackingPin is DaggerOutputPin)
                        {
                            if (alterState == DaggerNodeAlterState.CanConnectToInput)
                            {
                                _canAlterState = alterState;
                                drgevent.Effect = DragDropEffects.All;
                            }
                            else
                            {
                                drgevent.Effect = DragDropEffects.None;
                            }
                        }
                        else
                        {
                            if (alterState == DaggerNodeAlterState.CanConnectToOutput)
                            {
                                _canAlterState = alterState;
                                drgevent.Effect = DragDropEffects.All;
                            }
                            else
                            {
                                drgevent.Effect = DragDropEffects.None;
                            }
                        }
                    }
                    else
                    {
                        drgevent.Effect = DragDropEffects.None;
                    }
                }
            }
        }

        protected override void OnDragDrop(DragEventArgs drgevent)
        {
            switch (_canAlterState)
            {
                case DaggerNodeAlterState.CanConnectToOutput:
                case DaggerNodeAlterState.CanConnectToInput:
                    {
                        if (_parentGraph != null)
                        {
                            if (_mouseOverPin is DaggerInputPin)
                            {
                                _parentGraph.EndPinConnect((DaggerInputPin)_mouseOverPin);
                            }
                            else
                            {
                                _parentGraph.EndPinConnect((DaggerOutputPin)_mouseOverPin);
                            }
                        }
                    }
                    break;
                default:
                    break;
            }
        }

        /// <summary>
        /// Called when resizing, or when we need to Calcluate Layout
        /// </summary>
        /// <param name="e"></param>
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            CalculateLayout();

            if (_parentGraph != null)
            {
                _parentGraph.NodeResized(_node);
            }
        }

        protected override void OnMove(EventArgs e)
        {
            base.OnMove(e);

            if (_parentGraph != null)
            {
                _parentGraph.NodeMoved(this._node);
            }
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            if (_backImage == null)
            {
                CreateBackImage(e.ClipRectangle);
            }
            e.Graphics.DrawImage(_backImage, 0, 0);
        }

        private void DaggerUINode_ContextMenuStripChanged(object sender, EventArgs e)
        {
            //if it's not PinContextMenuStrip, store it
            if (sender != _pinContextMenu && sender != _userContextMenu)
            {
                _userContextMenu = sender as ContextMenuStrip;
            }
        }

        protected override void OnEnter(EventArgs e)
        {
            // the AutoScroll properies are too AUTO.  Prevent the DaggerUIGraph from scrolling a UINode
            // into view when it's entered.  This is a clever hack by Jim Nakashima:
            // http://seewinapp.blogspot.com/2005/09/is-your-autoscroll-too-auto.html
            if (Parent is DaggerUIGraph)
            {
                (Parent as DaggerUIGraph)._isScrolling = true;
                Point p = (Parent as DaggerUIGraph).AutoScrollPosition;
                AutoScrollPositionDelegate del = new AutoScrollPositionDelegate(SetAutoScrollPosition);
                Object[] args = { (DaggerUIGraph)Parent, p };
                BeginInvoke(del, args);
            }

            base.OnEnter(e);
                        
            _isFocused = true;
            DrawCaption(null);
            //only update the caption area
            this.Invalidate(_targetRegionCaption);
            BringToFront();

            if (Activated != null)
            {
                Activated(this, new EventArgs());
            }
        }


        delegate void AutoScrollPositionDelegate(DaggerUIGraph sender, Point p);
        private void SetAutoScrollPosition(DaggerUIGraph sender, Point p)
        {
            p.X = Math.Abs(p.X);
            p.Y = Math.Abs(p.Y);
            sender.AutoScrollPosition = p;
            sender._isScrolling = false;
        }

        protected override void OnLeave(EventArgs e)
        {
            base.OnLostFocus(e);

            _isFocused = false;

            DrawCaption(null);

            //only update the caption area
            this.Invalidate(_targetRegionCaption);

            if (Deactivated != null)
            {
                Deactivated(this, new EventArgs());
            }
        }
        #endregion

        #region Private Methods

        /// <summary>
        /// Allign the location of the caption buttons
        /// </summary>
        private void ArrangeCaptionButtons()
        {
            SuspendLayout();

            //set the initial offset
            int offset = Width - _captionSize - 5;
            if (_pinPlacement == DaggerNodePinPlacement.Outset)
            {
                if (_parentGraph != null)
                {
                    offset -= _parentGraph.PinLegend.PinSize;
                }
                else
                {
                    offset -= 11;
                }
            }

            //go through reverse order and position the buttons in the caption
            for (int i = _captionButtons.Count - 1; i > -1; i--)
            {
                SimpleImageButton button = (SimpleImageButton)_captionButtons[i];
                if (button.Width != _captionSize)
                {
                    button.Size = new Size(_captionSize, _captionSize);
                }

                if (button.Visible)
                {
                    button.Location = new Point(offset, 0);
                    offset -= _captionSize;
                }
            }

            ResumeLayout(true);
        }

        /// <summary>
        /// Modifies the Menu Strip Items based on the context and state of DaggerNode
        /// </summary>
        /// <param name="strip"></param>
        private void ValidateContextMenuStrip(ContextMenuStrip strip)
        {
            // can we trigger node processing
            _ictxtProcessNode.Visible = _parentGraph.AllowNodeProcessing;

            // can we create subnodes?
            _ictxtCreateSubNode.Visible = _parentGraph.AllowSubNodes;

            //can we perform delete and relink?
            if (_parentGraph.AllowDeleteRelink)
            {
                _ictxtDeleteRelink.Visible = false;
                List<DaggerInputPin> inpins = InputPins.ConnectedPins;
                List<DaggerOutputPin> outpins = OutputPins.ConnectedPins;
                bool canRelink = true;
                if (inpins.Count == outpins.Count && inpins.Count > 0)
                {
                    for (int i = 0; i < inpins.Count; i++)
                    {
                        if (inpins[i].ConnectedToOutput.ParentNode == null)
                        {
                            //can't relink to imported pins
                            canRelink = false;
                            break;
                        }

                        foreach (DaggerInputPin pin in outpins[i].ConnectedTo)
                        {
                            if (pin.ParentNode == null)
                            {
                                //can't relink to exported pins
                                canRelink = false;
                                break;
                            }
                        }

                        if (!inpins[i].DataType.IsAssignableFrom(outpins[i].DataType))
                        {
                            //not compatible
                            canRelink = false;
                            break;
                        }
                    }
                }
                else
                {
                    canRelink = false;
                }
                _ictxtDeleteRelink.Visible = canRelink;
            }
            else
            {
                _ictxtDeleteRelink.Visible = false;
            }
        }

        /// <summary>
        /// Tests clientMousePosition to see if we can connect to/from a pin
        /// </summary>
        /// <param name="clientMousePosition">Client position to hit test</param>
        /// <returns>AlterState of allowed connection</returns>
        private DaggerNodeAlterState CanConnectToPin(Point clientMousePosition)
        {
            DaggerNodeAlterState alterState = DaggerNodeAlterState.None;

            //reset the _mouseOverPin field
            DaggerBasePin oldOverPin = _mouseOverPin;
            _mouseOverPin = null;

            //Check if over Input pin
            foreach (DaggerInputPin pin in _node.InputPins)
            {
                if (pin.MutexAvailable && (pin.PinUIElements as PinUI).TargetRegion.IsVisible(clientMousePosition))
                {
                    alterState = DaggerNodeAlterState.ConnectFromInput;
                    _mouseOverPin = pin;
                    if (oldOverPin != _mouseOverPin)
                    {
                        //the pin has changed, reset the tooltip
                        _wndToolTip.SetToolTip(this, "DaggerNode");
                    }
                    break;
                }
            }

            //Check if over Output pin
            foreach (DaggerOutputPin pin in _node.OutputPins)
            {
                if (pin.MutexAvailable && (pin.PinUIElements as PinUI).TargetRegion.IsVisible(clientMousePosition))
                {
                    alterState = DaggerNodeAlterState.ConnectFromOutput;
                    _mouseOverPin = pin;
                    if (oldOverPin != _mouseOverPin)
                    {
                        //the pin has changed, reset the tooltip
                        _wndToolTip.SetToolTip(this, "DaggerNode");
                    }
                    break;
                }
            }

            // if the parent is currently dragging a pin and over the proper type of pin then allow connect
            // check precedence to make sure we don't loopback!!!
            if (_parentGraph.TrackingPin != null)
            {
                if (_mouseOverPin != null)
                {
                    //get the pin that is being dragged
                    DaggerBasePin trackingPin = _parentGraph.TrackingPin;
                    int trackingPrecedence = 0;

                    if (trackingPin.ParentNode != null)
                    {
                        //get the precedence of the pin that is being tracked
                        trackingPrecedence = trackingPin.ParentNode.Ordinal;
                    }

                    //are all the conditions met to allow the Tracking Pin to connect here?
                    if (trackingPin is DaggerOutputPin)
                    {
                        alterState = trackingPin.CanConnectToPin(_mouseOverPin) ? DaggerNodeAlterState.CanConnectToInput : DaggerNodeAlterState.None;
                    }
                    else
                    {
                        alterState = trackingPin.CanConnectToPin(_mouseOverPin) ? DaggerNodeAlterState.CanConnectToOutput : DaggerNodeAlterState.None;
                    }
                }
            }
            return alterState;
        }

        /// <summary>
        /// Initializes the UI elements for a DaggerBasePin
        /// </summary>
        /// <param name="pin"></param>
        private void CreatePinUIElements(DaggerBasePin pin)
        {
            // only create the UI elements if they haven't been instantiated yet
            if (pin.PinUIElements == null)
            {
                pin.PinUIElements = (PinUI)new PinUI(pin);
            }
        }

        /// <summary>
        /// Generates the background image of the UI Node
        /// </summary>
        /// <param name="clipRectangle"></param>
        private void CreateBackImage(Rectangle clipRectangle)
        {
            if (_backImage != null)
            {
                _backImage.Dispose();
            }

            _backImage = new Bitmap(Width, Height);

            Graphics g = Graphics.FromImage(_backImage);

            //if called from OnPaint, set the clip rectangle
            if (clipRectangle != null)
            {
                g.SetClip(clipRectangle);
            }

            g.SmoothingMode = SmoothingMode.HighQuality;

            //draw the resize widget
            if (_resizable)
            {
                using (Brush db = new SolidBrush(ControlPaint.Light(BackColor)))
                {
                    g.FillPath(db, _resizePath);
                    using (Pen dp = new Pen(ControlPaint.Dark(BackColor)))
                    {
                        g.DrawPath(dp, _resizePath);
                    }
                }
            }

            //draw the client border            
            g.DrawPath(Pens.Black, _clientPath);

            //draw the pins
            Rectangle sourceRect = new Rectangle(0, 0, PinSize, PinSize);
            if ((_node.InputPins != null) && (_node.OutputPins != null))
            {
                int inputpintop = (Height - ((_node.InputPins.MutexAvailablePins.Count * PinSize) + ((_node.InputPins.MutexAvailablePins.Count - 1) * _pinSpacing))) / 2;
                int outputpintop = (Height - ((_node.OutputPins.MutexAvailablePins.Count * PinSize) + ((_node.OutputPins.MutexAvailablePins.Count - 1) * _pinSpacing))) / 2;
                for (int i = 0; i < _node.InputPins.MutexAvailablePins.Count; i++)
                {
                    Image pinimage = _node.InputPins.MutexAvailablePins[i].IsConnected ? (_node.InputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinImageConnected : (_node.InputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinImageDisconnected;
                    ImageAttributes att = new ImageAttributes();
                    Color alphakey = _node.InputPins.MutexAvailablePins[i].IsConnected ? (_node.InputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinImageConnectedTransparent : (_node.InputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinImageDisconnectedTransparent;
                    att.SetColorKey(alphakey, alphakey);
                    g.DrawImage(pinimage, new Rectangle(0, inputpintop, PinSize, PinSize), 0, 0, PinSize, PinSize, GraphicsUnit.Pixel, att);
                    inputpintop += PinSize + _pinSpacing;
                }
                for (int i = 0; i < _node.OutputPins.MutexAvailablePins.Count; i++)
                {
                    Image pinimage = _node.OutputPins.MutexAvailablePins[i].IsConnected ? (_node.OutputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinImageConnected : (_node.OutputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinImageDisconnected;
                    ImageAttributes att = new ImageAttributes();
                    Color alphakey = _node.OutputPins.MutexAvailablePins[i].IsConnected ? (_node.OutputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinImageConnectedTransparent : (_node.OutputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinImageDisconnectedTransparent;
                    att.SetColorKey(alphakey, alphakey);
                    g.DrawImage(pinimage, new Rectangle(Width - PinSize, outputpintop, PinSize, PinSize), 0, 0, PinSize, PinSize, GraphicsUnit.Pixel, att);
                    outputpintop += PinSize + _pinSpacing;
                }
            }

            //draw the caption (it will dispose of g)
            DrawCaption(g);
        }

        /// <summary>
        /// Draws the caption onto the BackImage
        /// </summary>
        private void DrawCaption(Graphics g)
        {
            if (g == null)
            {
                //DrawCaption was called from somewhere besides CreateBackImage so make sure the Backimage
                //was created.
                if (_backImage == null)
                {
                    CreateBackImage(ClientRectangle);
                    return;
                }
                else
                {
                    g = Graphics.FromImage(_backImage);
                }
            }

            //clip to the caption region
            g.SetClip(_captionPath,CombineMode.Replace);

            //Fill the Caption
            Color capColor = _isProcessing ? Color.Red : (_isFocused ? _captionColor : _captionColorUnfocused);
            using(LinearGradientBrush b = new LinearGradientBrush(new Point(0,0),new Point(0,_captionSize),ControlPaint.Dark(capColor),ControlPaint.Light(capColor)))
            {
                b.SetBlendTriangularShape(0.5f,1.0f);
                g.FillPath(b, _captionPath);
            }

            //measure text length
            StringFormat stringFormat = null;
            float xAxis = 0;

            //offset text if pins are outset
            if (_pinPlacement == DaggerNodePinPlacement.Outset)
            {
                xAxis = PinSize;
            }

            //offset if there is an Icon
            if (_panelIcon != null)
            {
                xAxis += _captionSize;
            }

            float yAxis = 0;
            stringFormat = new StringFormat();
            SizeF ssize = g.MeasureString(_captionText, this.Font, new PointF(0, 0), stringFormat);
            yAxis = (_captionSize - ssize.Height) * 0.5f;

            //draw the text
            g.DrawString(_captionText, this.Font, new SolidBrush(ForeColor), new PointF(xAxis + 3, yAxis), stringFormat);

            //draw the caption icon if one is set
            if (_panelIcon != null)
            {
                Rectangle iconDest = new Rectangle((int)(xAxis - _captionSize + 3), 0, _captionSize, _captionSize);
                Rectangle iconSrc = new Rectangle(0,0,_panelIcon.Width,_panelIcon.Height);
                g.DrawImage(_panelIcon, iconDest, iconSrc, GraphicsUnit.Pixel);
            }

            //unclip so the caption border can be drawn
            g.SetClip(ClientRectangle, CombineMode.Replace);

            //draw the caption border
            using (Pen capPen = new Pen(ControlPaint.Dark(capColor)))
            {
                g.DrawPath(capPen, _captionPath);
            }

            g.Dispose();
        }

        /// <summary>
        /// Updates the graphics path of the caption and the panel
        /// </summary>
        private void UpdatePaths()
        {
            RectangleF r;
            if (_pinPlacement == DaggerNodePinPlacement.Outset)
            {
                //we want the pins completely outside the panel, so make the client rectangle a little smaller
                r = new RectangleF(PinSize, 0, (Width - 1) - PinSize * 2, Height - 1);
            }
            else
            {
                r = new RectangleF(0, 0, Width - 1, Height - 1);
            }

            _clientPath = GetTopClippedRoundedRect(r, _panelRadius, _captionSize);
            _captionPath = GetRoundedRect(new Rectangle((int)r.X, 0, (int)r.Width, _captionSize), _panelRadius);

            if (_resizable)
            {
                Point lr = new Point();

                //get the lower right point based on pin placement
                switch (_pinPlacement)
                {
                    case DaggerNodePinPlacement.Indent:
                    case DaggerNodePinPlacement.Inset:
                        lr = new Point(Width - 1, Height - 1);
                        break;
                    case DaggerNodePinPlacement.Outset:
                        if (_parentGraph != null)
                        {
                            lr = new Point(Width - 1 - _parentGraph.PinLegend.PinSize, Height - 1);
                        }
                        else
                        {
                            lr = new Point(Width - 11, Height - 1);
                        }
                        break;
                    default:
                        break;
                }

                //create a small triangle in lower right corner
                _resizePath = new GraphicsPath();
                _resizePath.StartFigure();
                _resizePath.AddLine(lr.X, lr.Y, lr.X - 12, lr.Y);
                _resizePath.AddLine(lr.X - 12, lr.Y, lr.X, lr.Y - 12);
                _resizePath.CloseFigure();
            }

            if (_pinPlacement == DaggerNodePinPlacement.Outset)
            {
                //offset the client path by the pin size
                Matrix translateMatrix = new Matrix();
                translateMatrix.Translate(PinSize, 0);
                _clientPath.Transform(translateMatrix);
            }
        }

        /// <summary>
        /// Adjust positions and target click regions of the pins, caption, resizer
        /// </summary>
        private void UpdateTargetRegions()
        {
            _targetRegionCaption = new Region(new Rectangle(0, 0, Width - 1, _captionSize));

            // if resizable update the resize widget
            if (_resizable)
            {
                if (_targetRegionResize != null)
                {
                    _targetRegionResize.Dispose();                    
                }
                _targetRegionResize = new Region(_resizePath);
            }

            //calculate targets for the Pins
            if ((_node.InputPins != null) && (_node.OutputPins != null))
            {
                int inputpintop = (Height - ((_node.InputPins.MutexAvailablePins.Count * PinSize) + ((_node.InputPins.MutexAvailablePins.Count - 1) * _pinSpacing))) / 2;
                int outputpintop = (Height - ((_node.OutputPins.MutexAvailablePins.Count * PinSize) + ((_node.OutputPins.MutexAvailablePins.Count - 1) * _pinSpacing))) / 2;
                for (int i = 0; i < _node.InputPins.MutexAvailablePins.Count; i++)
                {
                    if ((_node.InputPins.MutexAvailablePins[i].PinUIElements as PinUI).TargetRegion != null)
                    {
                        (_node.InputPins.MutexAvailablePins[i].PinUIElements as PinUI).TargetRegion.Dispose();
                        (_node.InputPins.MutexAvailablePins[i].PinUIElements as PinUI).TargetRegion = null;
                    }
                    (_node.InputPins.MutexAvailablePins[i].PinUIElements as PinUI).TargetRegion = new Region(new Rectangle(0, inputpintop, PinSize, PinSize));
                    (_node.InputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinLocation = new Point(0, inputpintop);
                    inputpintop += PinSize + _pinSpacing;
                }
                for (int i = 0; i < _node.OutputPins.MutexAvailablePins.Count; i++)
                {
                    if ((_node.OutputPins.MutexAvailablePins[i].PinUIElements as PinUI).TargetRegion != null)
                    {
                        (_node.OutputPins.MutexAvailablePins[i].PinUIElements as PinUI).TargetRegion.Dispose();
                        (_node.OutputPins.MutexAvailablePins[i].PinUIElements as PinUI).TargetRegion = null;
                    }
                    (_node.OutputPins.MutexAvailablePins[i].PinUIElements as PinUI).TargetRegion = new Region(new Rectangle(Width - PinSize, outputpintop, PinSize, PinSize));
                    (OutputPins.MutexAvailablePins[i].PinUIElements as PinUI).PinLocation = new Point(Width - PinSize, outputpintop);
                    outputpintop += PinSize + _pinSpacing;
                }
            }
        }

        /// <summary>
        /// Adjust size of UINode to fit pins
        /// </summary>
        /// <returns>true if UINode was resized</returns>
        private bool AdjustSize()
        {
            if (_parentGraph == null)
            {
                //not added to a graph yet
                return false;
            }

            bool wasResized = false;
            Size newSize = NodeMinimumSize;

            if (newSize.Height > Height)
            {
                Height = newSize.Height;
                wasResized = true;
            }
            if (newSize.Width > Width)
            {
                Width = newSize.Width;
                wasResized = true;
            }

            return wasResized;
        }

        /// <summary>
        /// Updates the region of the DaggerNode
        /// </summary>
        private void UpdateRegion()
        {
            try
            {
                //widen the paths to prevent pixel clipping
                Pen widepen = new Pen(Color.Black, 0.0f);
                widepen.MiterLimit = 0;

                GraphicsPath wideClientPath = (GraphicsPath)_clientPath.Clone();
                wideClientPath.Widen(widepen);
                GraphicsPath wideCaptionPath = (GraphicsPath)_captionPath.Clone();
                wideCaptionPath.Widen(widepen);

                //create and merge all the paths
                Region capreg = new Region(wideCaptionPath);
                capreg.Union(_captionPath);
                Region clientregion = new Region(wideClientPath);
                clientregion.Union(_clientPath);
                clientregion.Union(capreg);

                if ((_node.InputPins != null) && (_node.OutputPins != null))
                {
                    foreach (DaggerBasePin pin in _node.OutputPins.MutexAvailablePins)
                    {
                        if ((pin.PinUIElements as PinUI).PinConnectedRegion != null)
                        {
                            Region tempr = new Region(pin.IsConnected ? (pin.PinUIElements as PinUI).PinConnectedRegion.GetRegionData() : (pin.PinUIElements as PinUI).PinDisconnectedRegion.GetRegionData());
                            tempr.Translate((pin.PinUIElements as PinUI).PinLocation.X, (pin.PinUIElements as PinUI).PinLocation.Y);
                            clientregion.Union(tempr);
                            tempr.Dispose();
                            tempr = null;
                        }
                    }

                    foreach (DaggerBasePin pin in _node.InputPins.MutexAvailablePins)
                    {
                        if ((pin.PinUIElements as PinUI).PinConnectedRegion != null)
                        {
                            Region tempr = new Region(pin.IsConnected ? (pin.PinUIElements as PinUI).PinConnectedRegion.GetRegionData() : (pin.PinUIElements as PinUI).PinDisconnectedRegion.GetRegionData());
                            tempr.Translate((pin.PinUIElements as PinUI).PinLocation.X, (pin.PinUIElements as PinUI).PinLocation.Y);
                            clientregion.Union(tempr);
                            tempr.Dispose();
                            tempr = null;
                        }
                    }
                }

                if (this.Region != null)
                {
                    this.Region.Dispose();
                }

                this.Region = clientregion;

                widepen.Dispose();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error Updating Region");
            }
        }

        private GraphicsPath GetTopClippedRoundedRect(RectangleF baseRect, float radius, float captionSize)
        {
            // if corner radius is less than or equal to zero,
            // return the original rectangle
            if (radius <= 0.0F)
            {
                GraphicsPath mPath = new GraphicsPath();
                mPath.AddRectangle(baseRect);
                mPath.CloseFigure();
                return mPath;
            }

            GraphicsPath gp = new GraphicsPath();
            gp.StartFigure();
            gp.AddLine(0, captionSize / 2, baseRect.Width, captionSize / 2);

            if (_pinPlacement == DaggerNodePinPlacement.Indent)
            {
                //indent right if any output pins
                if ((_node.OutputPins != null) && (_node.OutputPins.Count > 0))
                {
                    gp.AddLine(baseRect.Width, captionSize / 2, baseRect.Width, captionSize);

                    PointF point1 = new PointF(baseRect.Width, captionSize);
                    PointF point2 = new PointF(baseRect.Width - PinSize * _pinIndent, captionSize + PinSize);
                    PointF point3 = new PointF(baseRect.Width - PinSize * _pinIndent, baseRect.Height - radius - PinSize);
                    PointF point4 = new PointF(baseRect.Width, baseRect.Height - radius);
                    gp.AddBezier(point1, point2, point3, point4);
                }
            }

            gp.AddArc(baseRect.Width - radius, baseRect.Height - radius, radius, radius, 0, 90);
            gp.AddArc(0, baseRect.Height - radius, radius, radius, 90, 90);

            if (_pinPlacement == DaggerNodePinPlacement.Indent)
            {
                //indent left if any input pins
                if ((_node.InputPins != null) && (_node.InputPins.Count > 0))
                {
                    PointF point4 = new PointF(0, captionSize);
                    PointF point3 = new PointF(PinSize * _pinIndent, captionSize + PinSize);
                    PointF point2 = new PointF(PinSize * _pinIndent, baseRect.Height - radius - PinSize);
                    PointF point1 = new PointF(0, baseRect.Height - radius);
                    gp.AddBezier(point1, point2, point3, point4);

                    gp.AddLine(0, captionSize, 0, captionSize / 2);
                }
            }

            gp.CloseFigure();
            return gp;
        }

        private GraphicsPath GetRoundedRect(RectangleF baseRect, float radius)
        {
            // if corner radius is less than or equal to zero,
            // return the original rectangle
            if (radius <= 0.0F)
            {
                GraphicsPath mPath = new GraphicsPath();
                mPath.AddRectangle(baseRect);
                mPath.CloseFigure();
                return mPath;
            }

            // if the corner radius is greater than or equal to
            // half the baseRect.Width, or baseRect.Height (whichever is shorter)
            // then return a capsule instead of a lozenge
            if (radius >= (Math.Min(baseRect.Width, baseRect.Height)) / 2.5)
                return GetCapsule(baseRect);

            // create the arc for the rectangle sides and declare
            // a graphics path object for the drawing

            GraphicsPath gp = new GraphicsPath();
            gp.StartFigure();
            gp.AddArc(baseRect.X, baseRect.Y, radius, radius, 180, 90);
            gp.AddArc(baseRect.X + baseRect.Width - radius, baseRect.Y, radius, radius, 270, 90);
            gp.AddArc(baseRect.X + baseRect.Width - radius, baseRect.Y + baseRect.Height - radius, radius, radius, 0, 90);
            gp.AddArc(baseRect.X, baseRect.Y + baseRect.Height - radius, radius, radius, 90, 90);
            gp.CloseFigure();
            return gp;
        }

        private GraphicsPath GetCapsule(RectangleF baseRect)
        {
            float diameter;
            RectangleF arc;
            GraphicsPath path = new GraphicsPath();
            try
            {
                if (baseRect.Width > baseRect.Height)
                {
                    // return horizontal capsule 
                    diameter = baseRect.Height;
                    SizeF sizeF = new SizeF(diameter, diameter);
                    arc = new RectangleF(baseRect.Location, sizeF);
                    path.AddArc(arc, 90, 180);
                    arc.X = baseRect.Right - diameter;
                    path.AddArc(arc, 270, 180);
                }
                else if (baseRect.Width < baseRect.Height)
                {
                    // return vertical capsule 
                    diameter = baseRect.Width;
                    SizeF sizeF = new SizeF(diameter, diameter);
                    arc = new RectangleF(baseRect.Location, sizeF);
                    path.AddArc(arc, 180, 180);
                    arc.Y = baseRect.Bottom - diameter;
                    path.AddArc(arc, 0, 180);
                }
                else
                {
                    // return circle 
                    path.AddEllipse(baseRect);
                }
            }
            catch (Exception)
            {
                path.AddEllipse(baseRect);
            }
            finally
            {
                path.CloseFigure();
            }
            return path;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Updates the Layout of the node and refreshes all child controls
        /// </summary>
        public void CalculateLayout()
        {
            if (AdjustSize())
            {
                //if we resized return, because OnResize will call CalculateLayout again
                return;
            }

            // make sure the pins have UI elements
            foreach (DaggerBasePin pin in _node.InputPins)
            {
                CreatePinUIElements(pin);
            }
            foreach (DaggerBasePin pin in _node.OutputPins)
            {
                CreatePinUIElements(pin);
            }

            //recreate the back image and update the control region
            UpdatePaths();
            UpdateTargetRegions();            
            UpdateRegion();
            CreateBackImage(ClientRectangle);

            _internalControl.Location = new Point(_internalControlPadding + PinSize, _internalControlPadding + _captionSize);
            _internalControl.Size = new Size(this.Width - (_internalControlPadding * 2) - (PinSize * 2), this.Height - (_internalControlPadding * 2) - _captionSize);

            ArrangeCaptionButtons();

            Refresh();
        }

        /// <summary>
        /// Override to provide processing of UI elements.  Called AFTER the node's DoProcessing event has been raised
        /// </summary>
        public virtual void DoUIProcessing()
        {

        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior)
United States United States
AKA Rich Insley.

I have over 25 years experience in programming, and I'm completely self taught. (Except for one year at California State University Fresno where I had to learn the God awful language Miranda (http://miranda.org.uk/). I've spent 10 years as a Paratrooper in the US Army during the Clinton Administration.

Comments and Discussions