File: System\Windows\Forms\Controls\ToolStrips\ToolStripSplitButton.cs
Web Access
Project: src\src\System.Windows.Forms\src\System.Windows.Forms.csproj (System.Windows.Forms)
// 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;
using System.Windows.Forms.Design;
using System.Windows.Forms.Layout;
 
namespace System.Windows.Forms;
 
[ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.ToolStrip | ToolStripItemDesignerAvailability.StatusStrip)]
[DefaultEvent(nameof(ButtonClick))]
public partial class ToolStripSplitButton : ToolStripDropDownItem
{
    private ToolStripItem? _defaultItem;
    private ToolStripSplitButtonButton? _splitButtonButton;
    private Rectangle _dropDownButtonBounds = Rectangle.Empty;
    private ToolStripSplitButtonButtonLayout? _splitButtonButtonLayout;
    private int _dropDownButtonWidth;
    private int _splitterWidth = 1;
    private Rectangle _splitterBounds = Rectangle.Empty;
    private byte _openMouseId;
    private long _lastClickTime;
 
    private const int DefaultDropDownWidth = 11;
 
    private static readonly object s_eventDefaultItemChanged = new();
    private static readonly object s_eventButtonClick = new();
    private static readonly object s_eventButtonDoubleClick = new();
 
    private static bool s_isScalingInitialized;
    private static int s_scaledDropDownButtonWidth = DefaultDropDownWidth;
 
    public ToolStripSplitButton()
    {
        Initialize(); // all additional work should be done in Initialize
    }
 
    public ToolStripSplitButton(string? text)
        : base(text, image: null, onClick: null)
    {
        Initialize();
    }
 
    public ToolStripSplitButton(Image? image)
        : base(text: null, image, onClick: null)
    {
        Initialize();
    }
 
    public ToolStripSplitButton(string? text, Image? image)
        : base(text, image, onClick: null)
    {
        Initialize();
    }
 
    public ToolStripSplitButton(string? text, Image? image, EventHandler? onClick)
        : base(text, image, onClick)
    {
        Initialize();
    }
 
    public ToolStripSplitButton(string? text, Image? image, EventHandler? onClick, string? name)
        : base(text, image, onClick, name)
    {
        Initialize();
    }
 
    public ToolStripSplitButton(string? text, Image? image, params ToolStripItem[]? dropDownItems)
        : base(text, image, dropDownItems)
    {
        Initialize();
    }
 
    [DefaultValue(true)]
    public new bool AutoToolTip
    {
        get => base.AutoToolTip;
        set => base.AutoToolTip = value;
    }
 
    [Browsable(false)]
    public Rectangle ButtonBounds
    {
        get
        {
            // Rectangle bounds = SplitButtonButton.Bounds;
            // bounds.Offset(this.Bounds.Location);
            return SplitButtonButton.Bounds;
        }
    }
 
    [Browsable(false)]
    public bool ButtonPressed
    {
        get
        {
            return SplitButtonButton.Pressed;
        }
    }
 
    [Browsable(false)]
    public bool ButtonSelected
    {
        get
        {
            return SplitButtonButton.Selected || DropDownButtonPressed;
        }
    }
 
    /// <summary>
    ///  Occurs when the button portion of a split button is clicked.
    /// </summary>
    [SRCategory(nameof(SR.CatAction))]
    [SRDescription(nameof(SR.ToolStripSplitButtonOnButtonClickDescr))]
    public event EventHandler? ButtonClick
    {
        add => Events.AddHandler(s_eventButtonClick, value);
        remove => Events.RemoveHandler(s_eventButtonClick, value);
    }
 
    /// <summary>
    ///  Occurs when the button portion of a split button  is double clicked.
    /// </summary>
    [SRCategory(nameof(SR.CatAction))]
    [SRDescription(nameof(SR.ToolStripSplitButtonOnButtonDoubleClickDescr))]
    public event EventHandler? ButtonDoubleClick
    {
        add => Events.AddHandler(s_eventButtonDoubleClick, value);
        remove => Events.RemoveHandler(s_eventButtonDoubleClick, value);
    }
 
    protected override bool DefaultAutoToolTip
    {
        get
        {
            return true;
        }
    }
 
    [DefaultValue(null)]
    [Browsable(false)]
    public ToolStripItem? DefaultItem
    {
        get
        {
            return _defaultItem;
        }
        set
        {
            if (_defaultItem != value)
            {
                OnDefaultItemChanged(EventArgs.Empty);
                _defaultItem = value;
            }
        }
    }
 
    /// <summary>
    ///  Occurs when the default item has changed
    /// </summary>
    [SRCategory(nameof(SR.CatAction))]
    [SRDescription(nameof(SR.ToolStripSplitButtonOnDefaultItemChangedDescr))]
    public event EventHandler? DefaultItemChanged
    {
        add => Events.AddHandler(s_eventDefaultItemChanged, value);
        remove => Events.RemoveHandler(s_eventDefaultItemChanged, value);
    }
 
    protected internal override bool DismissWhenClicked => !DropDown.Visible;
 
    internal override Rectangle DropDownButtonArea => DropDownButtonBounds;
 
    /// <summary>
    ///  The bounds of the DropDown in ToolStrip coordinates.
    /// </summary>
    [Browsable(false)]
    public Rectangle DropDownButtonBounds => _dropDownButtonBounds;
 
    [Browsable(false)]
    public bool DropDownButtonPressed => DropDown.Visible;
 
    [Browsable(false)]
    public bool DropDownButtonSelected => Selected;
 
    [SRCategory(nameof(SR.CatLayout))]
    [SRDescription(nameof(SR.ToolStripSplitButtonDropDownButtonWidthDescr))]
    public int DropDownButtonWidth
    {
        get => _dropDownButtonWidth;
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegative(value);
 
            if (_dropDownButtonWidth != value)
            {
                _dropDownButtonWidth = value;
                InvalidateSplitButtonLayout();
                InvalidateItemLayout(PropertyNames.DropDownButtonWidth);
            }
        }
    }
 
    private static int DefaultDropDownButtonWidth
    {
        get
        {
            // Start off with a size roughly equivalent to a ComboBox dropdown.
            if (!s_isScalingInitialized)
            {
                s_scaledDropDownButtonWidth = ScaleHelper.ScaleToInitialSystemDpi(DefaultDropDownWidth);
                s_isScalingInitialized = true;
            }
 
            return s_scaledDropDownButtonWidth;
        }
    }
 
    /// <summary>
    ///  Just used as a convenience to help manage layout
    /// </summary>
    private ToolStripSplitButtonButton SplitButtonButton
    {
        get
        {
            _splitButtonButton ??= new ToolStripSplitButtonButton(this);
 
            _splitButtonButton.Image = Image;
            _splitButtonButton.Text = Text;
            _splitButtonButton.BackColor = BackColor;
            _splitButtonButton.ForeColor = ForeColor;
            _splitButtonButton.Font = Font;
            _splitButtonButton.ImageAlign = ImageAlign;
            _splitButtonButton.TextAlign = TextAlign;
            _splitButtonButton.TextImageRelation = TextImageRelation;
            return _splitButtonButton;
        }
    }
 
    internal ToolStripItemInternalLayout SplitButtonButtonLayout
    {
        get
        {
            // For preferred size caching reasons, we need to keep our two
            // internal layouts (button, dropdown button) in sync.
 
            if (InternalLayout is not null /*if layout is invalid - calls CreateInternalLayout - which resets splitButtonButtonLayout to null*/
                && _splitButtonButtonLayout is null)
            {
                _splitButtonButtonLayout = new ToolStripSplitButtonButtonLayout(this);
            }
 
            return _splitButtonButtonLayout!;
        }
    }
 
    /// <summary>
    ///  The width of the separator between the default and drop down button
    /// </summary>
    [SRDescription(nameof(SR.ToolStripSplitButtonSplitterWidthDescr))]
    [SRCategory(nameof(SR.CatLayout))]
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    internal int SplitterWidth
    {
        get
        {
            return _splitterWidth;
        }
        set
        {
            if (value < 0)
            {
                _splitterWidth = 0;
            }
            else
            {
                _splitterWidth = value;
            }
 
            InvalidateSplitButtonLayout();
        }
    }
 
    /// <summary>
    ///  the boundaries of the separator between the default and drop down button, exposed for custom
    ///  painting purposes.
    /// </summary>
    [Browsable(false)]
    public Rectangle SplitterBounds
    {
        get
        {
            return _splitterBounds;
        }
    }
 
    private void CalculateLayout()
    {
        // Figure out where the DropDown image goes.
        Rectangle dropDownButtonBounds = new(Point.Empty, new Size(Math.Min(Width, DropDownButtonWidth), Height));
 
        // Figure out the height and width of the selected item.
        int splitButtonButtonWidth = Math.Max(0, Width - dropDownButtonBounds.Width);
        int splitButtonButtonHeight = Math.Max(0, Height);
 
        Rectangle splitButtonButtonBounds = new(Point.Empty, new Size(splitButtonButtonWidth, splitButtonButtonHeight));
 
        // grow the selected item by one since we're overlapping the borders.
        splitButtonButtonBounds.Width -= _splitterWidth;
 
        if (RightToLeft == RightToLeft.No)
        {
            // the dropdown button goes on the right
            dropDownButtonBounds.Offset(splitButtonButtonBounds.Right + _splitterWidth, 0);
            _splitterBounds = new Rectangle(splitButtonButtonBounds.Right, splitButtonButtonBounds.Top, _splitterWidth, splitButtonButtonBounds.Height);
        }
        else
        {
            // the split button goes on the right.
            splitButtonButtonBounds.Offset(DropDownButtonWidth + _splitterWidth, 0);
            _splitterBounds = new Rectangle(dropDownButtonBounds.Right, dropDownButtonBounds.Top, _splitterWidth, dropDownButtonBounds.Height);
        }
 
        SplitButtonButton.SetBounds(splitButtonButtonBounds);
        SetDropDownButtonBounds(dropDownButtonBounds);
    }
 
    protected override AccessibleObject CreateAccessibilityInstance()
    {
        return new ToolStripSplitButtonUiaProvider(this);
    }
 
    protected override ToolStripDropDown CreateDefaultDropDown()
    {
        // AutoGenerate a ToolStrip DropDown - set the property so we hook events
        return new ToolStripDropDownMenu(this, isAutoGenerated: true);
    }
 
    private protected override ToolStripItemInternalLayout CreateInternalLayout()
    {
        // whenever the master layout is invalidated - invalidate the splitbuttonbutton layout.
        _splitButtonButtonLayout = null;
        return new ToolStripItemInternalLayout(this);
    }
 
    public override Size GetPreferredSize(Size constrainingSize)
    {
        Size preferredSize = SplitButtonButtonLayout.GetPreferredSize(constrainingSize);
        preferredSize.Width += DropDownButtonWidth + SplitterWidth + Padding.Horizontal;
        return preferredSize;
    }
 
    private void InvalidateSplitButtonLayout()
    {
        _splitButtonButtonLayout = null;
        CalculateLayout();
    }
 
    private void Initialize()
    {
        _dropDownButtonWidth = DefaultDropDownButtonWidth;
        SupportsSpaceKey = true;
    }
 
    protected internal override bool ProcessDialogKey(Keys keyData)
    {
        if (Enabled && (keyData == Keys.Enter || (SupportsSpaceKey && keyData == Keys.Space)))
        {
            PerformButtonClick();
            return true;
        }
 
        return base.ProcessDialogKey(keyData);
    }
 
    protected internal override bool ProcessMnemonic(char charCode)
    {
        // checking IsMnemonic is not necessary - toolstrip does this for us
        PerformButtonClick();
        return true;
    }
 
    /// <summary>
    ///  called when the button portion of a split button is clicked
    ///  if there is a default item, this will route the click to the default item
    /// </summary>
    protected virtual void OnButtonClick(EventArgs e)
    {
        DefaultItem?.FireEvent(ToolStripItemEventType.Click);
        ((EventHandler?)Events[s_eventButtonClick])?.Invoke(this, e);
    }
 
    /// <summary>
    ///  called when the button portion of a split button is double clicked
    ///  if there is a default item, this will route the double click to the default item
    /// </summary>
    public virtual void OnButtonDoubleClick(EventArgs e)
    {
        DefaultItem?.FireEvent(ToolStripItemEventType.DoubleClick);
        ((EventHandler?)Events[s_eventButtonDoubleClick])?.Invoke(this, e);
    }
 
    /// <summary>
    ///  Inheriting classes should override this method to handle this event.
    /// </summary>
    protected virtual void OnDefaultItemChanged(EventArgs e)
    {
        InvalidateSplitButtonLayout();
        if (CanRaiseEvents)
        {
            if (Events[s_eventDefaultItemChanged] is EventHandler eh)
            {
                eh(this, e);
            }
        }
    }
 
    protected override void OnMouseDown(MouseEventArgs e)
    {
        if (DropDownButtonBounds.Contains(e.Location))
        {
            if (e.Button == MouseButtons.Left)
            {
                if (!DropDown.Visible)
                {
                    Debug.Assert(ParentInternal is not null, "Parent is null here, not going to get accurate ID");
                    _openMouseId = (ParentInternal is null) ? (byte)0 : ParentInternal.GetMouseId();
                    ShowDropDown(mousePush: true);
                }
            }
        }
        else
        {
            SplitButtonButton.Push(true);
        }
    }
 
    protected override void OnMouseUp(MouseEventArgs e)
    {
        if (!Enabled)
        {
            return;
        }
 
        SplitButtonButton.Push(false);
 
        if (DropDownButtonBounds.Contains(e.Location))
        {
            if (e.Button == MouseButtons.Left)
            {
                if (DropDown.Visible)
                {
                    Debug.Assert(ParentInternal is not null, "Parent is null here, not going to get accurate ID");
                    byte closeMouseId = (ParentInternal is null) ? (byte)0 : ParentInternal.GetMouseId();
                    if (closeMouseId != _openMouseId)
                    {
                        _openMouseId = 0;  // reset the mouse id, we should never get this value from toolstrip.
                        ToolStripManager.ModalMenuFilter.CloseActiveDropDown(DropDown, ToolStripDropDownCloseReason.AppClicked);
                        Select();
                    }
                }
            }
        }
 
        Point clickPoint = new(e.X, e.Y);
        if ((e.Button == MouseButtons.Left) && SplitButtonButton.Bounds.Contains(clickPoint))
        {
            bool shouldFireDoubleClick = false;
            if (DoubleClickEnabled)
            {
                long newTime = DateTime.Now.Ticks;
                long deltaTicks = newTime - _lastClickTime;
                _lastClickTime = newTime;
                // use >= for cases where the succession of click events is so fast it's not picked up by
                // DateTime resolution.
                Debug.Assert(deltaTicks >= 0, "why are deltaticks less than zero? thats some mighty fast clicking");
                // if we've seen a mouse up less than the double click time ago, we should fire.
                if (deltaTicks >= 0 && deltaTicks < DoubleClickTicks)
                {
                    shouldFireDoubleClick = true;
                }
            }
 
            if (shouldFireDoubleClick)
            {
                OnButtonDoubleClick(EventArgs.Empty);
                // If we actually fired DoubleClick - reset the lastClickTime.
                _lastClickTime = 0;
            }
            else
            {
                OnButtonClick(EventArgs.Empty);
            }
        }
    }
 
    protected override void OnMouseLeave(EventArgs e)
    {
        _openMouseId = 0;  // reset the mouse id, we should never get this value from toolstrip.
        SplitButtonButton.Push(false);
        base.OnMouseLeave(e);
    }
 
    protected override void OnRightToLeftChanged(EventArgs e)
    {
        base.OnRightToLeftChanged(e);
        InvalidateSplitButtonLayout();
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
        ToolStripRenderer? renderer = Renderer;
        if (renderer is not null)
        {
            InvalidateSplitButtonLayout();
            Graphics g = e.Graphics;
 
            renderer.DrawSplitButton(new ToolStripItemRenderEventArgs(g, this));
 
            if ((DisplayStyle & ToolStripItemDisplayStyle.Image) != ToolStripItemDisplayStyle.None)
            {
                renderer.DrawItemImage(new ToolStripItemImageRenderEventArgs(g, this, SplitButtonButtonLayout.ImageRectangle));
            }
 
            if ((DisplayStyle & ToolStripItemDisplayStyle.Text) != ToolStripItemDisplayStyle.None)
            {
                renderer.DrawItemText(new ToolStripItemTextRenderEventArgs(g, this, SplitButtonButton.Text, SplitButtonButtonLayout.TextRectangle, ForeColor, Font, SplitButtonButtonLayout.TextFormat));
            }
        }
    }
 
    public void PerformButtonClick()
    {
        if (Enabled && Available)
        {
            PerformClick();
            OnButtonClick(EventArgs.Empty);
        }
    }
 
    [EditorBrowsable(EditorBrowsableState.Never)]
    public virtual void ResetDropDownButtonWidth() => DropDownButtonWidth = DefaultDropDownButtonWidth;
 
    private void SetDropDownButtonBounds(Rectangle rect) => _dropDownButtonBounds = rect;
 
    /// <summary>
    ///  Determines if the <see cref="ToolStripItem.Size"/> property needs to be persisted.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    internal virtual bool ShouldSerializeDropDownButtonWidth() => DropDownButtonWidth != DefaultDropDownButtonWidth;
}