File: System\ComponentModel\Design\SelectionService.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.Collections;
using System.Collections.Specialized;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.Design;
 
namespace System.ComponentModel.Design;
 
/// <summary>
///  The selection service handles selection within a form.
///  There is one selection service for each <see cref="DesignerHost"/>.
///  A selection consists of an list of <see cref="IComponent"/>.
///  The first component in the list is designated the <see cref="PrimarySelection"/>.
/// </summary>
internal sealed class SelectionService : ISelectionService, IDisposable
{
    // These are the selection types we use for context help.
    private static readonly string[] s_selectionKeywords = ["None", "Single", "Multiple"];
 
    // State flags for the selection service
    private static readonly int s_stateTransaction = BitVector32.CreateMask(); // Designer is in a transaction
    private static readonly int s_stateTransactionChange = BitVector32.CreateMask(s_stateTransaction); // Component change occurred while in a transaction
 
    // ISelectionService events
    private static readonly object s_eventSelectionChanging = new();
    private static readonly object s_eventSelectionChanged = new();
 
    // Member variables
    private IServiceProvider _provider; // The service provider
    private BitVector32 _state; // state of the selection service
    private readonly EventHandlerList _events; // the events we raise
    private List<IComponent>? _selection; // list of selected objects
    private List<string>? _contextAttributes; // help context information we have pushed to the help service.
    private short _contextKeyword; // the offset into the selection keywords for the current selection.
    private StatusCommandUI _statusCommandUI; // UI for setting the StatusBar Information..
 
    /// <summary>
    ///  Creates a new selection manager object.
    ///  The selection manager manages all selection of all designers under the current form file.
    /// </summary>
    internal SelectionService(IServiceProvider provider) : base()
    {
        _provider = provider;
        _state = default;
        _events = new EventHandlerList();
        _statusCommandUI = new StatusCommandUI(provider);
    }
 
    /// <summary>
    ///  Adds the given selection to our selection list.
    /// </summary>
    internal void AddSelection(IComponent selection)
    {
        ArgumentNullException.ThrowIfNull(selection, nameof(selection));
 
        if (_selection is null)
        {
            _selection = [];
 
            // Now is the opportune time to hook up all of our events
            if (GetService(typeof(IComponentChangeService)) is IComponentChangeService cs)
            {
                cs.ComponentRemoved += OnComponentRemove;
            }
 
            if (GetService(typeof(IDesignerHost)) is IDesignerHost host)
            {
                host.TransactionOpened += OnTransactionOpened;
                host.TransactionClosed += OnTransactionClosed;
                if (host.InTransaction)
                {
                    OnTransactionOpened(host, EventArgs.Empty);
                }
            }
        }
 
        if (!_selection.Contains(selection))
        {
            _selection.Add(selection);
        }
    }
 
    /// <summary>
    ///  Called when our visibility or batch mode changes.
    ///  Flushes any pending notifications or updates if possible.
    /// </summary>
    private void FlushSelectionChanges()
    {
        if (!_state[s_stateTransaction] && _state[s_stateTransactionChange])
        {
            _state[s_stateTransactionChange] = false;
            OnSelectionChanged();
        }
    }
 
    /// <summary>
    ///  Helper function to retrieve services.
    /// </summary>
    private object? GetService(Type serviceType) => _provider?.GetService(serviceType);
 
    /// <summary>
    ///  called by the formcore when someone has removed a component.
    ///  This will remove any selection on the component without disturbing the rest of the selection
    /// </summary>
    private void OnComponentRemove(object? sender, ComponentEventArgs ce)
    {
        if (_selection is not null && ce.Component is not null && _selection.Contains(ce.Component))
        {
            RemoveSelection(ce.Component);
            OnSelectionChanged();
        }
    }
 
    /// <summary>
    ///  called anytime the selection has changed.
    ///  We update our UI for the selection, and then we fire a juicy change event.
    /// </summary>
    private void OnSelectionChanged()
    {
        if (_state[s_stateTransaction])
        {
            _state[s_stateTransactionChange] = true;
        }
        else
        {
            EventHandler? eh = _events[s_eventSelectionChanging] as EventHandler;
            eh?.Invoke(this, EventArgs.Empty);
 
            UpdateHelpKeyword(true);
 
            eh = _events[s_eventSelectionChanged] as EventHandler;
            if (eh is not null)
            {
                try
                {
                    eh(this, EventArgs.Empty);
                }
                catch
                {
                    // eat exceptions - required for compatibility with Everett.
                }
            }
        }
    }
 
    /// <summary>
    ///  Called by the designer host when it is entering or leaving a batch operation.
    ///  Here we queue up selection notification and we turn off our UI.
    /// </summary>
    private void OnTransactionClosed(object? sender, DesignerTransactionCloseEventArgs e)
    {
        if (e.LastTransaction)
        {
            _state[s_stateTransaction] = false;
            FlushSelectionChanges();
        }
    }
 
    /// <summary>
    ///  Called by the designer host when it is entering or leaving a batch operation.
    ///  Here we queue up selection notification and we turn off our UI.
    /// </summary>
    private void OnTransactionOpened(object? sender, EventArgs e) => _state[s_stateTransaction] = true;
 
    internal IComponent? PrimarySelection => _selection?.Count > 0 ? _selection[0] : null;
 
    /// <summary>
    ///  Removes the given selection from the selection list.
    /// </summary>
    internal void RemoveSelection(IComponent selection) => _selection?.Remove(selection);
 
    private void ApplicationIdle(object? source, EventArgs args)
    {
        UpdateHelpKeyword(false);
        Application.Idle -= ApplicationIdle;
    }
 
    /// <summary>
    ///  Pushes the help context into the help service for the current set of selected objects.
    /// </summary>
    private void UpdateHelpKeyword(bool tryLater)
    {
        if (GetService(typeof(IHelpService)) is not IHelpService helpService)
        {
            if (tryLater)
            {
                // We don't have a HelpService yet, hook up to the ApplicationIdle event.
                // VS is always returning a UserContext, so instantiating the HelpService
                // beforehand and doing class PushContext on it to try to
                // stack up help context in the HelpService to be flushed when we get the
                // documentation event may not work, so we need to wait for a HelpService instead.
                Application.Idle += ApplicationIdle;
            }
 
            return;
        }
 
        // If there is an old set of context attributes, remove them.
        if (_contextAttributes is not null)
        {
            foreach (string helpContext in _contextAttributes)
            {
                helpService.RemoveContextAttribute("Keyword", helpContext);
            }
 
            _contextAttributes = null;
        }
 
        // Clear the selection keyword
        helpService.RemoveContextAttribute("Selection", s_selectionKeywords[_contextKeyword]);
        // Get a list of unique class names.
        Debug.Assert(_selection is not null, "Should be impossible to update the help context before configuring the selection hash");
        bool baseComponentSelected = false;
        if (_selection.Count == 0)
        {
            baseComponentSelected = true;
        }
        else if (_selection.Count == 1)
        {
            if (GetService(typeof(IDesignerHost)) is IDesignerHost host && _selection.Contains(host.RootComponent))
            {
                baseComponentSelected = true;
            }
        }
 
        _contextAttributes = [];
 
        for (int i = 0; i < _selection.Count; i++)
        {
            object component = _selection[i];
            string? helpContext = TypeDescriptor.GetClassName(component);
            HelpKeywordAttribute? contextAttribute = (HelpKeywordAttribute?)TypeDescriptor.GetAttributes(component)[typeof(HelpKeywordAttribute)];
            if (contextAttribute is not null && !contextAttribute.IsDefaultAttribute())
            {
                helpContext = contextAttribute.HelpKeyword;
            }
 
            if (helpContext is not null)
            {
                _contextAttributes.Add(helpContext);
            }
        }
 
        // And push them into the help context as keywords.
        HelpKeywordType selectionType = baseComponentSelected ? HelpKeywordType.GeneralKeyword : HelpKeywordType.F1Keyword;
        foreach (string helpContext in _contextAttributes)
        {
            helpService.AddContextAttribute("Keyword", helpContext, selectionType);
        }
 
        // Now add the appropriate selection keyword.
        // Note that we do not count the base component as being selected if it is the only thing selected.
        int count = _selection.Count;
        if (count == 1 && baseComponentSelected)
        {
            count--;
        }
 
        _contextKeyword = (short)Math.Min(count, s_selectionKeywords.Length - 1);
        helpService.AddContextAttribute("Selection", s_selectionKeywords[_contextKeyword], HelpKeywordType.FilterKeyword);
    }
 
    /// <summary>
    ///  Disposes the entire selection service.
    /// </summary>
    void IDisposable.Dispose()
    {
        if (_selection is not null)
        {
            if (GetService(typeof(IDesignerHost)) is IDesignerHost host)
            {
                host.TransactionOpened -= OnTransactionOpened;
                host.TransactionClosed -= OnTransactionClosed;
                if (host.InTransaction)
                {
                    OnTransactionClosed(host, new(commit: true, lastTransaction: true));
                }
            }
 
            if (GetService(typeof(IComponentChangeService)) is IComponentChangeService cs)
            {
                cs.ComponentRemoved -= OnComponentRemove;
            }
 
            _selection.Clear();
        }
 
        _statusCommandUI = null!;
        _provider = null!;
    }
 
    /// <summary>
    ///  Retrieves the object that is currently the primary selection.
    ///  The primary selection has a slightly different UI look and is used as
    ///  a "key" when an operation is to be done on multiple components.
    /// </summary>
    object? ISelectionService.PrimarySelection => PrimarySelection;
 
    /// <summary>
    ///  Retrieves the count of selected objects.
    /// </summary>
    int ISelectionService.SelectionCount => _selection?.Count ?? 0;
 
    /// <summary>
    ///  Adds a <see cref="ISelectionService.SelectionChanged"/> event handler to the selection service.
    /// </summary>
    event EventHandler ISelectionService.SelectionChanged
    {
        add => _events.AddHandler(s_eventSelectionChanged, value);
        remove => _events.RemoveHandler(s_eventSelectionChanged, value);
    }
 
    /// <summary>
    ///  Occurs whenever the user changes the current list of selected components in the designer.
    ///  This event is raised before the actual selection changes.
    /// </summary>
    event EventHandler ISelectionService.SelectionChanging
    {
        add => _events.AddHandler(s_eventSelectionChanging, value);
        remove => _events.RemoveHandler(s_eventSelectionChanging, value);
    }
 
    /// <summary>
    ///  Determines if the component is currently selected.
    ///  This is faster than getting the entire list of selected components.
    /// </summary>
    bool ISelectionService.GetComponentSelected(object component)
    {
        ArgumentNullException.ThrowIfNull(component, nameof(component));
        return _selection is not null && _selection.Contains(component);
    }
 
    /// <summary>
    ///  Retrieves an array of components that are currently part of the user's selection.
    /// </summary>
    ICollection ISelectionService.GetSelectedComponents()
    {
        // Must clone here. Otherwise the values collection is a live collection and will change when the
        // selection changes. GetSelectedComponents should be a snapshot.
        return _selection?.ToArray() ?? [];
    }
 
    /// <summary>
    ///  Changes the user's current set of selected components to the components in the given array.
    ///  If the array is null or doesn't contain any components, this will select the top level component in the designer.
    /// </summary>
    void ISelectionService.SetSelectedComponents(ICollection? components)
        => ((ISelectionService)this).SetSelectedComponents(components, SelectionTypes.Auto);
 
    /// <summary>
    ///  Changes the user's current set of selected components to the components in the given array.
    ///  If the array is null or doesn't contain any components, this will select the top level component in the designer.
    /// </summary>
    void ISelectionService.SetSelectedComponents(ICollection? components, SelectionTypes selectionType)
    {
        bool fToggle = (selectionType & SelectionTypes.Toggle) == SelectionTypes.Toggle;
        bool fPrimary = (selectionType & SelectionTypes.Primary) == SelectionTypes.Primary;
        bool fAdd = (selectionType & SelectionTypes.Add) == SelectionTypes.Add;
        bool fRemove = (selectionType & SelectionTypes.Remove) == SelectionTypes.Remove;
        bool fReplace = (selectionType & SelectionTypes.Replace) == SelectionTypes.Replace;
        bool fAuto = !(fToggle | fAdd | fRemove | fReplace);
 
        // We always want to allow NULL arrays coming in.
        components ??= Array.Empty<IComponent>();
 
        // If toggle, replace, remove or add are not specifically specified, infer them from the state of the modifier keys.
        // This creates the "Auto" selection type for us by default.
        if (fAuto)
        {
            fToggle = (Control.ModifierKeys & (Keys.Control | Keys.Shift)) > 0;
            fAdd |= Control.ModifierKeys == Keys.Shift;
            // If we are in auto mode, and if we are toggling or adding new controls, then cancel out the primary flag.
            if (fToggle || fAdd)
            {
                fPrimary = false;
            }
        }
 
        // This flag is true if we changed selection and should therefore raise a selection change event.
        bool fChanged = false;
        // Handle the click case
        IComponent? requestedPrimary = null;
        int primaryIndex;
 
        if (fPrimary && components.Count == 1)
        {
            foreach (IComponent component in components)
            {
                requestedPrimary = component;
                ArgumentNullException.ThrowIfNull(component, nameof(components));
 
                break;
            }
        }
 
        if (requestedPrimary is not null && _selection is not null && (primaryIndex = _selection.IndexOf(requestedPrimary)) != -1)
        {
            if (primaryIndex != 0)
            {
                (_selection[primaryIndex], _selection[0]) = (_selection[0], _selection[primaryIndex]);
                fChanged = true;
            }
        }
        else
        {
            // If we are replacing the selection, only remove the ones that are not in our new list.
            // We also handle the special case here of having a singular component selected that's already selected.
            // In this case we just move it to the primary selection.
            if (!fToggle && !fAdd && !fRemove)
            {
                if (_selection is not null)
                {
                    IComponent[] selections = new IComponent[_selection.Count];
                    _selection.CopyTo(selections, 0);
                    // Yucky and N^2, but even with several hundred components this should be fairly fast
                    foreach (IComponent item in selections)
                    {
                        bool remove = true;
                        foreach (IComponent comp in components)
                        {
                            ArgumentNullException.ThrowIfNull(comp, nameof(components));
 
                            if (ReferenceEquals(comp, item))
                            {
                                remove = false;
                                break;
                            }
                        }
 
                        if (remove)
                        {
                            RemoveSelection(item);
                            fChanged = true;
                        }
                    }
                }
            }
 
            // Now select / toggle the components.
            foreach (IComponent comp in components)
            {
                ArgumentNullException.ThrowIfNull(comp, nameof(components));
 
                if (_selection is not null && _selection.Contains(comp))
                {
                    if (fToggle || fRemove)
                    {
                        RemoveSelection(comp);
                        fChanged = true;
                    }
                }
                else if (!fRemove)
                {
                    AddSelection(comp);
                    fChanged = true;
                }
            }
        }
 
        // Notify that our selection has changed
        if (fChanged)
        {
            // Set the SelectionInformation
            if (_selection?.Count > 0)
            {
                _statusCommandUI.SetStatusInformation(_selection[0] as Component);
            }
            else
            {
                _statusCommandUI.SetStatusInformation(Rectangle.Empty);
            }
 
            OnSelectionChanged();
        }
    }
}