|
// 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();
}
}
|