File: System\Windows\Forms\Design\ToolStripDesignerUtils.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.
 
#nullable disable
 
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Drawing.Design;
using System.Reflection;
using System.Windows.Forms.Design.Behavior;
using Microsoft.Win32;
 
namespace System.Windows.Forms.Design;
 
internal sealed class ToolStripDesignerUtils
{
    private static readonly Type s_toolStripItemType = typeof(ToolStripItem);
    [ThreadStatic]
    private static Dictionary<Type, ToolboxItem> t_cachedToolboxItems;
    [ThreadStatic]
    private static int t_customToolStripItemCount;
    private const int TOOLSTRIPCHARCOUNT = 9;
 
    // Used to cache in the selection. This is used when the selection is changing and we need to invalidate
    // the original selection Especially, when the selection is changed through NON UI designer action like
    // through propertyGrid or through Doc Outline.
    public static ArrayList s_originalSelComps;
 
    [ThreadStatic]
    private static Dictionary<Type, Bitmap> t_cachedWinformsImages;
    private static readonly string s_systemWindowsFormsNamespace = typeof(ToolStripItem).Namespace;
 
    private ToolStripDesignerUtils()
    {
    }
 
    #region NewItemTypeLists
    // This section controls the ordering of standard item types in all the various pieces of toolstrip designer UI.
    // ToolStrip - Default item is determined by being first in the list
    private static readonly Type[] s_newItemTypesForToolStrip =
        [typeof(ToolStripButton), typeof(ToolStripLabel), typeof(ToolStripSplitButton), typeof(ToolStripDropDownButton), typeof(ToolStripSeparator), typeof(ToolStripComboBox), typeof(ToolStripTextBox), typeof(ToolStripProgressBar)];
    // StatusStrip - Default item is determined by being first in the list
    private static readonly Type[] s_newItemTypesForStatusStrip =
        [typeof(ToolStripStatusLabel), typeof(ToolStripProgressBar), typeof(ToolStripDropDownButton), typeof(ToolStripSplitButton)];
    // MenuStrip - Default item is determined by being first in the list
    private static readonly Type[] s_newItemTypesForMenuStrip =
        [typeof(ToolStripMenuItem), typeof(ToolStripComboBox), typeof(ToolStripTextBox)];
    // ToolStripDropDown - roughly same as menu strip.
    private static readonly Type[] s_newItemTypesForToolStripDropDownMenu =
        [typeof(ToolStripMenuItem), typeof(ToolStripComboBox), typeof(ToolStripSeparator), typeof(ToolStripTextBox)];
    #endregion
 
    // Get the Correct bounds for painting...
    public static void GetAdjustedBounds(ToolStripItem item, ref Rectangle r)
    {
        // adjust bounds as per item
        if (!(item is ToolStripControlHost && item.IsOnDropDown))
        {
            // Deflate the SelectionGlyph for the MenuItems...
            if (item is ToolStripMenuItem && item.IsOnDropDown)
            {
                r.Inflate(-3, -2);
                r.Width++;
            }
            else if (item is ToolStripControlHost && !item.IsOnDropDown)
            {
                r.Inflate(0, -2);
            }
            else if (item is ToolStripMenuItem && !item.IsOnDropDown)
            {
                r.Inflate(-3, -3);
            }
            else
            {
                r.Inflate(-1, -1);
            }
        }
    }
 
    /// <summary>
    ///  If IComponent is ToolStrip return ToolStrip
    ///  If IComponent is ToolStripItem return the Owner ToolStrip
    ///  If IComponent is ToolStripDropDownItem return the child DropDown ToolStrip
    /// </summary>
    private static ToolStrip GetToolStripFromComponent(IComponent component)
    {
        ToolStrip parent;
        if (component is ToolStripItem stripItem)
        {
            if (stripItem is not ToolStripDropDownItem)
            {
                parent = stripItem.Owner;
            }
            else
            {
                parent = ((ToolStripDropDownItem)stripItem).DropDown;
            }
        }
        else
        {
            parent = component as ToolStrip;
        }
 
        return parent;
    }
 
    private static ToolboxItem GetCachedToolboxItem(Type itemType)
    {
        t_cachedToolboxItems ??= [];
        if (t_cachedToolboxItems.TryGetValue(itemType, out ToolboxItem tbxItem))
        {
            return tbxItem;
        }
 
        // no cache hit - load the item.
        // create a toolbox item to match
        tbxItem = new ToolboxItem(itemType);
 
        t_cachedToolboxItems[itemType] = tbxItem;
        if (t_customToolStripItemCount > 0 && (t_customToolStripItemCount * 2 < t_cachedToolboxItems.Count))
        {
            // time to clear the toolbox item cache - we've got twice the number of toolbox items than actual custom item types.
            t_cachedToolboxItems.Clear();
        }
 
        return tbxItem;
    }
 
    // only call this for well known items.
    private static Bitmap GetKnownToolboxBitmap(Type itemType)
    {
        t_cachedWinformsImages ??= [];
 
        if (!t_cachedWinformsImages.TryGetValue(itemType, out Bitmap value))
        {
            Bitmap knownImage = ToolboxBitmapAttribute.GetImageFromResource(itemType, null, false) as Bitmap;
            value = knownImage;
            t_cachedWinformsImages[itemType] = value;
            return knownImage;
        }
 
        return value;
    }
 
    public static Bitmap GetToolboxBitmap(Type itemType)
    {
        // if and only if the namespace of the type is System.Windows.Forms. try to pull the image out of the manifest.
        if (itemType.Namespace == s_systemWindowsFormsNamespace)
        {
            return GetKnownToolboxBitmap(itemType);
        }
 
        // check to see if we've got a toolbox item, and use it.
        ToolboxItem tbxItem = GetCachedToolboxItem(itemType);
        if (tbxItem is not null)
        {
            return tbxItem.Bitmap;
        }
 
        // if all else fails, throw up a default image.
        return GetKnownToolboxBitmap(typeof(Component));
    }
 
    /// <summary>
    ///  Fishes out the display name attribute from the Toolbox item if not present, uses Type.Name
    /// </summary>
    public static string GetToolboxDescription(Type itemType)
    {
        string currentName = null;
        ToolboxItem tbxItem = GetCachedToolboxItem(itemType);
        if (tbxItem is not null)
        {
            currentName = tbxItem.DisplayName;
        }
 
        currentName ??= itemType.Name;
 
        if (currentName.StartsWith("ToolStrip", StringComparison.Ordinal))
        {
            return currentName[TOOLSTRIPCHARCOUNT..];
        }
 
        return currentName;
    }
 
    /// <summary>
    ///  The first item returned should be the DefaultItem to create on the ToolStrip
    /// </summary>
    public static Type[] GetStandardItemTypes(IComponent component)
    {
        ToolStrip toolStrip = GetToolStripFromComponent(component);
        if (toolStrip is MenuStrip)
        {
            return s_newItemTypesForMenuStrip;
        }
        else if (toolStrip is ToolStripDropDownMenu)
        {
            // ToolStripDropDown gets default items.
            return s_newItemTypesForToolStripDropDownMenu;
        }
        else if (toolStrip is StatusStrip)
        {
            return s_newItemTypesForStatusStrip;
        }
 
        Debug.Assert(toolStrip is not null, "why werent we handed a toolstrip here? returning default list");
        return s_newItemTypesForToolStrip;
    }
 
    private static ToolStripItemDesignerAvailability GetDesignerVisibility(ToolStrip toolStrip)
    {
        ToolStripItemDesignerAvailability visibility;
        if (toolStrip is StatusStrip)
        {
            visibility = ToolStripItemDesignerAvailability.StatusStrip;
        }
        else if (toolStrip is MenuStrip)
        {
            visibility = ToolStripItemDesignerAvailability.MenuStrip;
        }
        else if (toolStrip is ToolStripDropDownMenu)
        {
            visibility = ToolStripItemDesignerAvailability.ContextMenuStrip;
        }
        else
        {
            visibility = ToolStripItemDesignerAvailability.ToolStrip;
        }
 
        return visibility;
    }
 
    public static Type[] GetCustomItemTypes(IComponent component, IServiceProvider serviceProvider)
    {
        ITypeDiscoveryService discoveryService = null;
        if (serviceProvider is not null)
        {
            discoveryService = serviceProvider.GetService(typeof(ITypeDiscoveryService)) as ITypeDiscoveryService;
        }
 
        return GetCustomItemTypes(component, discoveryService);
    }
 
    public static Type[] GetCustomItemTypes(IComponent component, ITypeDiscoveryService discoveryService)
    {
        if (discoveryService is not null)
        {
            // fish out all types which derive from toolstrip item
            ICollection itemTypes = discoveryService.GetTypes(s_toolStripItemType, false /*excludeGlobalTypes*/);
            ToolStrip toolStrip = GetToolStripFromComponent(component);
            // determine the value of the visibility attribute which matches the current toolstrip type.
            ToolStripItemDesignerAvailability currentToolStripVisibility = GetDesignerVisibility(toolStrip);
            Debug.Assert(currentToolStripVisibility != ToolStripItemDesignerAvailability.None, "Why is GetDesignerVisibility returning None?");
            // fish out the ones we've already listed.
            Type[] stockItemTypeList = GetStandardItemTypes(component);
            if (currentToolStripVisibility != ToolStripItemDesignerAvailability.None)
            {
                List<Type> creatableTypes = new(itemTypes.Count);
                foreach (Type t in itemTypes)
                {
                    if (t.IsAbstract)
                    {
                        continue;
                    }
 
                    if (!t.IsPublic && !t.IsNestedPublic)
                    {
                        continue;
                    }
 
                    if (t.ContainsGenericParameters)
                    {
                        continue;
                    }
 
                    // Check if we have public constructor...
                    ConstructorInfo ctor = t.GetConstructor([]);
                    if (ctor is null)
                    {
                        continue;
                    }
 
                    // if the visibility matches the current toolstrip type, add it to the list of possible types to create.
                    ToolStripItemDesignerAvailabilityAttribute visibilityAttribute = (ToolStripItemDesignerAvailabilityAttribute)TypeDescriptor.GetAttributes(t)[typeof(ToolStripItemDesignerAvailabilityAttribute)];
                    if (visibilityAttribute is not null && ((visibilityAttribute.ItemAdditionVisibility & currentToolStripVisibility) == currentToolStripVisibility))
                    {
                        bool isStockType = false;
                        // PERF: consider a dictionary - but this list will usually be 3-7 items.
                        foreach (Type stockType in stockItemTypeList)
                        {
                            if (stockType == t)
                            {
                                isStockType = true;
                                break;
                            }
                        }
 
                        if (!isStockType)
                        {
                            creatableTypes.Add(t);
                        }
                    }
                }
 
                t_customToolStripItemCount = creatableTypes.Count;
                return [.. creatableTypes];
            }
        }
 
        t_customToolStripItemCount = 0;
        return [];
    }
 
    /// <summary>
    ///  wraps the result of GetStandardItemTypes in ItemTypeToolStripMenuItems.
    /// </summary>
    public static ToolStripItem[] GetStandardItemMenuItems(IComponent component, EventHandler onClick, bool convertTo)
    {
        Type[] standardTypes = GetStandardItemTypes(component);
        ToolStripItem[] items = new ToolStripItem[standardTypes.Length];
        for (int i = 0; i < standardTypes.Length; i++)
        {
            ItemTypeToolStripMenuItem item = new(standardTypes[i])
            {
                ConvertTo = convertTo
            };
            if (onClick is not null)
            {
                item.Click += onClick;
            }
 
            items[i] = item;
        }
 
        return items;
    }
 
    /// <summary>
    ///  wraps the result of GetCustomItemTypes in ItemTypeToolStripMenuItems.
    /// </summary>
    public static ToolStripItem[] GetCustomItemMenuItems(IComponent component, EventHandler onClick, bool convertTo, IServiceProvider serviceProvider)
    {
        Type[] customTypes = GetCustomItemTypes(component, serviceProvider);
        ToolStripItem[] items = new ToolStripItem[customTypes.Length];
        for (int i = 0; i < customTypes.Length; i++)
        {
            ItemTypeToolStripMenuItem item = new(customTypes[i])
            {
                ConvertTo = convertTo
            };
            if (onClick is not null)
            {
                item.Click += onClick;
            }
 
            items[i] = item;
        }
 
        return items;
    }
 
    /// <summary>
    ///  build up a list of standard items separated by the custom items
    /// </summary>
    public static NewItemsContextMenuStrip GetNewItemDropDown(IComponent component, ToolStripItem currentItem, EventHandler onClick, bool convertTo, IServiceProvider serviceProvider, bool populateCustom)
    {
        NewItemsContextMenuStrip contextMenu = new(component, currentItem, onClick, convertTo, serviceProvider);
        contextMenu.GroupOrdering.Add("StandardList");
        contextMenu.GroupOrdering.Add("CustomList");
        // plumb through the standard and custom items.
        foreach (ToolStripItem item in GetStandardItemMenuItems(component, onClick, convertTo))
        {
            contextMenu.Groups["StandardList"].Items.Add(item);
            if (convertTo)
            {
                if (item is ItemTypeToolStripMenuItem toolItem && currentItem is not null && toolItem.ItemType == currentItem.GetType())
                {
                    toolItem.Enabled = false;
                }
            }
        }
 
        if (populateCustom)
        {
            GetCustomNewItemDropDown(contextMenu, component, currentItem, onClick, convertTo, serviceProvider);
        }
 
        if (serviceProvider.GetService(typeof(IUIService)) is IUIService uis)
        {
            contextMenu.Renderer = (ToolStripProfessionalRenderer)uis.Styles["VsRenderer"];
            contextMenu.Font = (Font)uis.Styles["DialogFont"];
            if (uis.Styles["VsColorPanelText"] is Color color)
            {
                contextMenu.ForeColor = color;
            }
        }
 
        contextMenu.Populate();
        return contextMenu;
    }
 
    public static void GetCustomNewItemDropDown(NewItemsContextMenuStrip contextMenu, IComponent component, ToolStripItem currentItem, EventHandler onClick, bool convertTo, IServiceProvider serviceProvider)
    {
        foreach (ToolStripItem item in GetCustomItemMenuItems(component, onClick, convertTo, serviceProvider))
        {
            contextMenu.Groups["CustomList"].Items.Add(item);
            if (convertTo)
            {
                if (item is ItemTypeToolStripMenuItem toolItem && currentItem is not null && toolItem.ItemType == currentItem.GetType())
                {
                    toolItem.Enabled = false;
                }
            }
        }
 
        contextMenu.Populate();
    }
 
    public static void InvalidateSelection(ArrayList originalSelComps, ToolStripItem nextSelection, IServiceProvider provider, bool shiftPressed)
    {
        // if we are not selecting a ToolStripItem then return (don't invalidate).
        if (nextSelection is null || provider is null)
        {
            return;
        }
 
        // InvalidateOriginal SelectedComponents.
        Region invalidateRegion = null;
        Region itemRegion = null;
        int GLYPHBORDER = 1;
        int GLYPHINSET = 2;
        ToolStripItemDesigner designer = null;
        bool templateNodeSelected = false;
 
        try
        {
            Rectangle invalidateBounds = Rectangle.Empty;
            IDesignerHost designerHost = (IDesignerHost)provider.GetService(typeof(IDesignerHost));
 
            if (designerHost is not null)
            {
                foreach (Component comp in originalSelComps)
                {
                    if (comp is ToolStripItem selItem)
                    {
                        if ((originalSelComps.Count > 1) ||
                           (originalSelComps.Count == 1 && selItem.GetCurrentParent() != nextSelection.GetCurrentParent()) ||
                           selItem is ToolStripSeparator || selItem is ToolStripControlHost || !selItem.IsOnDropDown || selItem.IsOnOverflow)
                        {
                            // finally Invalidate the selection rect ...
                            designer = designerHost.GetDesigner(selItem) as ToolStripItemDesigner;
                            if (designer is not null)
                            {
                                invalidateBounds = designer.GetGlyphBounds();
                                GetAdjustedBounds(selItem, ref invalidateBounds);
                                invalidateBounds.Inflate(GLYPHBORDER, GLYPHBORDER);
 
                                if (invalidateRegion is null)
                                {
                                    invalidateRegion = new Region(invalidateBounds);
                                    invalidateBounds.Inflate(-GLYPHINSET, -GLYPHINSET);
                                    invalidateRegion.Exclude(invalidateBounds);
                                }
                                else
                                {
                                    itemRegion = new Region(invalidateBounds);
                                    invalidateBounds.Inflate(-GLYPHINSET, -GLYPHINSET);
                                    itemRegion.Exclude(invalidateBounds);
                                    invalidateRegion.Union(itemRegion);
                                }
                            }
                        }
                    }
                }
            }
 
            if (invalidateRegion is not null || templateNodeSelected || shiftPressed)
            {
                BehaviorService behaviorService = (BehaviorService)provider.GetService(typeof(BehaviorService));
                if (behaviorService is not null)
                {
                    if (invalidateRegion is not null)
                    {
                        behaviorService.Invalidate(invalidateRegion);
                    }
 
                    // When a ToolStripItem is PrimarySelection, the glyph bounds are not invalidated
                    // through the SelectionManager so we have to do this.
                    designer = designerHost.GetDesigner(nextSelection) as ToolStripItemDesigner;
                    if (designer is not null)
                    {
                        invalidateBounds = designer.GetGlyphBounds();
                        GetAdjustedBounds(nextSelection, ref invalidateBounds);
                        invalidateBounds.Inflate(GLYPHBORDER, GLYPHBORDER);
                        invalidateRegion = new Region(invalidateBounds);
 
                        invalidateBounds.Inflate(-GLYPHINSET, -GLYPHINSET);
                        invalidateRegion.Exclude(invalidateBounds);
                        behaviorService.Invalidate(invalidateRegion);
                    }
                }
            }
        }
        finally
        {
            invalidateRegion?.Dispose();
 
            itemRegion?.Dispose();
        }
    }
 
    /// <summary> represents cached information about the display</summary>
    internal static class DisplayInformation
    {
        private static bool s_highContrast;  // whether we are under hight contrast mode
        private static bool s_lowRes;  // whether we are under low resolution mode
        private static bool s_isTerminalServerSession;   // whether this application is run on a terminal server (remote desktop)
        private static bool s_highContrastSettingValid;   // indicates whether the high contrast setting is correct
        private static bool s_lowResSettingValid;  // indicates whether the low resolution setting is correct
        private static bool s_terminalSettingValid; // indicates whether the terminal server setting is correct
        private static short s_bitsPerPixel;
        private static bool s_dropShadowSettingValid;
        private static bool s_dropShadowEnabled;
 
        static DisplayInformation()
        {
            SystemEvents.UserPreferenceChanged += UserPreferenceChanged;
            SystemEvents.DisplaySettingsChanged += DisplaySettingChanged;
        }
 
        public static short BitsPerPixel
        {
            get
            {
                if (s_bitsPerPixel == 0)
                {
                    foreach (Screen s in Screen.AllScreens)
                    {
                        if (s_bitsPerPixel == 0)
                        {
                            s_bitsPerPixel = (short)s.BitsPerPixel;
                        }
                        else
                        {
                            s_bitsPerPixel = (short)Math.Min(s.BitsPerPixel, s_bitsPerPixel);
                        }
                    }
                }
 
                return s_bitsPerPixel;
            }
        }
 
        /// <summary>
        ///  Tests to see if the monitor is in low resolution mode (8-bit color depth or less).
        /// </summary>
        public static bool LowResolution
        {
            get
            {
                if (s_lowResSettingValid)
                {
                    return s_lowRes;
                }
 
                s_lowRes = BitsPerPixel <= 8;
                s_lowResSettingValid = true;
                return s_lowRes;
            }
        }
 
        /// <summary>
        ///  Tests to see if we are under high contrast mode
        /// </summary>
        public static bool HighContrast
        {
            get
            {
                if (s_highContrastSettingValid)
                {
                    return s_highContrast;
                }
 
                s_highContrast = SystemInformation.HighContrast;
                s_highContrastSettingValid = true;
                return s_highContrast;
            }
        }
 
        public static bool IsDropShadowEnabled
        {
            get
            {
                if (s_dropShadowSettingValid)
                {
                    return s_dropShadowEnabled;
                }
 
                s_dropShadowEnabled = SystemInformation.IsDropShadowEnabled;
                s_dropShadowSettingValid = true;
                return s_dropShadowEnabled;
            }
        }
 
        /// <summary>
        ///test to see if we are under terminal server mode
        /// </summary>
        public static bool TerminalServer
        {
            get
            {
                if (s_terminalSettingValid)
                {
                    return s_isTerminalServerSession;
                }
 
                s_isTerminalServerSession = SystemInformation.TerminalServerSession;
                s_terminalSettingValid = true;
                return s_isTerminalServerSession;
            }
        }
 
        /// <summary>
        ///event handler for change in display setting
        /// </summary>
        private static void DisplaySettingChanged(object obj, EventArgs ea)
        {
            s_highContrastSettingValid = false;
            s_lowResSettingValid = false;
            s_terminalSettingValid = false;
            s_dropShadowSettingValid = false;
        }
 
        /// <summary>
        ///event handler for change in user preference
        /// </summary>
        private static void UserPreferenceChanged(object obj, UserPreferenceChangedEventArgs ea)
        {
            s_highContrastSettingValid = false;
            s_lowResSettingValid = false;
            s_terminalSettingValid = false;
            s_dropShadowSettingValid = false;
            s_bitsPerPixel = 0;
        }
    }
}