| File: System\ComponentModel\Design\SelectionService.cs | Web Access |
| Project: src\winforms\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(); } } }