File: System\Windows\Forms\Design\Behavior\DesignerActionGlyph.cs
Web Access
Project: src\src\System.Windows.Forms.Design\src\System.Windows.Forms.Design.csproj (System.Windows.Forms.Design)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Drawing;
 
namespace System.Windows.Forms.Design.Behavior;
 
/// <summary>
///  This Glyph represents the UI appended to a control when DesignerActions are available. Each image that represents
///  these states are demand created. This is done because it is entirely possible that a DesignerActionGlyph will
///  only ever be in one of these states during its lifetime... kind of sad really.
/// </summary>
internal sealed class DesignerActionGlyph : Glyph
{
    internal const int CONTROLOVERLAP_X = 5;                    // number of pixels the anchor should be offset to the left of the control's upper-right
    internal const int CONTROLOVERLAP_Y = 2;                    // number of pixels the anchor overlaps the control in the y-direction
 
    private const byte IconSize = 10;
    private Rectangle _alternativeBounds = Rectangle.Empty;     // if !empty, this represents the bounds of the tray control this glyph is related to
    private Rectangle _bounds;                                  // the bounds of our glyph
    private readonly Adorner? _adorner;                         // A ptr back to our adorner - so when we decide to change state, we can invalidate
    private readonly Control? _alternativeParent;               // if this is valid - then the glyph will invalidate itself here instead of on the adorner
    private bool _mouseOver;                                    // on mouse over, we shade our image differently, this is used to track that state
    private bool _insidePaint;
    private DockStyle _dockStyle;
    private Bitmap? _glyphImageClosed;
    private Bitmap? _glyphImageOpened;
 
    /// <summary>
    ///  Constructor that passes empty alternative bounds and parents.
    ///  Typically this is done for control on the designer's surface since component tray glyphs will have these
    ///  alternative values.
    /// </summary>
    public DesignerActionGlyph(DesignerActionBehavior? behavior, Adorner? adorner)
        : this(behavior, adorner, Rectangle.Empty, null)
    {
    }
 
    public DesignerActionGlyph(DesignerActionBehavior? behavior, Rectangle alternativeBounds, Control? alternativeParent)
        : this(behavior, null, alternativeBounds, alternativeParent)
    {
    }
 
    /// <summary>
    ///  Constructor that sets the dropdownbox size, creates a our hottrack brush and invalidates the glyph (to configure location).
    /// </summary>
    private DesignerActionGlyph(DesignerActionBehavior? behavior, Adorner? adorner, Rectangle alternativeBounds, Control? alternativeParent)
        : base(behavior)
    {
        _adorner = adorner;
        _alternativeBounds = alternativeBounds;
        _alternativeParent = alternativeParent;
        Invalidate();
    }
 
    /// <summary>
    ///  Returns the bounds of our glyph. This is used by the related Behavior to determine where to show the
    ///  contextmenu (list of actions).
    /// </summary>
    public override Rectangle Bounds
    {
        get => _bounds;
    }
 
    public DockStyle DockEdge
    {
        get => _dockStyle;
        set
        {
            if (_dockStyle != value)
            {
                _dockStyle = value;
            }
        }
    }
 
    public bool IsInComponentTray
    {
        get => (_adorner is null); // adorner and alternative bounds are exclusive
    }
 
    /// <summary>
    ///  Standard hit test logic that returns true if the point is contained within our bounds.
    ///  This is also used to manage out mouse over state.
    /// </summary>
    public override Cursor? GetHitTest(Point p)
    {
        if (_bounds.Contains(p))
        {
            MouseOver = true;
            return Cursors.Default;
        }
 
        MouseOver = false;
        return null;
    }
 
    private Image GlyphImageClosed => _glyphImageClosed ??= ScaleHelper.GetIconResourceAsBitmap(
        typeof(DesignerActionGlyph),
        "Close_left",
        ScaleHelper.ScaleToDpi(new Size(IconSize, IconSize), ScaleHelper.InitialSystemDpi));
 
    private Image GlyphImageOpened => _glyphImageOpened ??= ScaleHelper.GetIconResourceAsBitmap(
        typeof(DesignerActionGlyph),
        "Open_left",
        ScaleHelper.ScaleToDpi(new Size(IconSize, IconSize), ScaleHelper.InitialSystemDpi));
 
    internal void InvalidateOwnerLocation()
    {
        if (_alternativeParent is not null)
        {
            // alternative parent and adorner are exclusive...
            _alternativeParent.Invalidate(_bounds);
        }
        else
        {
            _adorner?.Invalidate(_bounds);
        }
    }
 
    /// <summary>
    ///  Called when the state for this DesignerActionGlyph changes. Or when the related component's size or
    ///  location change. Here, we re-calculate the Glyph's bounds and change our image.
    /// </summary>
    internal void Invalidate()
    {
        if (Behavior is null)
            return;
 
        IComponent relatedComponent = ((DesignerActionBehavior)Behavior).RelatedComponent;
        Point topRight = Point.Empty;
        // handle the case that our comp is a control
        if (relatedComponent is Control relatedControl && !(relatedComponent is ToolStripDropDown) && _adorner?.BehaviorService is not null)
        {
            topRight = _adorner.BehaviorService.ControlToAdornerWindow(relatedControl);
            topRight.X += relatedControl.Width;
        }
 
        // ISSUE: we can't have this special cased here - we should find a more generic approach to solving this
        // problem special logic here for our comp being a toolstrip item
        else
        {
            // update alternative bounds if possible...
            if (_alternativeParent is ComponentTray)
            {
                ComponentTray.TrayControl trayControl = ComponentTray.GetTrayControlFromComponent(relatedComponent);
                if (trayControl is not null)
                {
                    _alternativeBounds = trayControl.Bounds;
                }
            }
 
            Rectangle newRect = DesignerUtils.GetBoundsForNoResizeSelectionType(_alternativeBounds, SelectionBorderGlyphType.Top);
            topRight.X = newRect.Right;
            topRight.Y = newRect.Top;
        }
 
        topRight.X -= (GlyphImageOpened.Width + CONTROLOVERLAP_X);
        topRight.Y -= (GlyphImageOpened.Height - CONTROLOVERLAP_Y);
        _bounds = (new Rectangle(topRight.X, topRight.Y, GlyphImageOpened.Width, GlyphImageOpened.Height));
    }
 
    /// <summary>
    ///  Used to manage the mouse-pointer-is-over-glyph state. If this is true, then we will shade our BoxImage
    ///  in the Paint logic.
    /// </summary>
    private bool MouseOver
    {
        get => _mouseOver;
        set
        {
            if (_mouseOver != value)
            {
                _mouseOver = value;
 
                InvalidateOwnerLocation();
            }
        }
    }
 
    /// <summary>
    ///  Responds to a paint event. This Glyph will paint its current image and, if MouseHover is true,
    ///  we'll paint over the image with the 'hoverBrush'.
    /// </summary>
    public override void Paint(PaintEventArgs pe)
    {
        Image image;
        if (Behavior is DesignerActionBehavior behavior)
        {
            if (_insidePaint)
            {
                return;
            }
 
            IComponent? panelComponent = behavior.ParentUI.LastPanelComponent;
            IComponent relatedComponent = behavior.RelatedComponent;
            if (panelComponent is not null && panelComponent == relatedComponent)
            {
                image = GlyphImageOpened;
            }
            else
            {
                image = GlyphImageClosed;
            }
 
            try
            {
                _insidePaint = true;
                pe.Graphics.DrawImage(image, _bounds.Left, _bounds.Top);
                if (MouseOver || (panelComponent is not null && panelComponent == relatedComponent))
                {
                    pe.Graphics.FillRectangle(DesignerUtils.HoverBrush, Rectangle.Inflate(_bounds, -1, -1));
                }
            }
            finally
            {
                _insidePaint = false;
            }
        }
    }
 
    /// <summary>
    ///  Called by the ComponentTray when a tray control changes location.
    /// </summary>
    internal void UpdateAlternativeBounds(Rectangle newBounds)
    {
        _alternativeBounds = newBounds;
        Invalidate();
    }
}