| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using Avalonia.Automation.Peers;
- using Avalonia.Controls.Primitives;
- using Avalonia.Controls.Templates;
- using Avalonia.Input;
- using Avalonia.Input.Platform;
- using Avalonia.Interactivity;
- using Avalonia.LogicalTree;
- using Avalonia.Rendering;
- using Avalonia.Styling;
- using Avalonia.Threading;
- using Avalonia.VisualTree;
- namespace Avalonia.Controls
- {
- /// <summary>
- /// Base class for Avalonia controls.
- /// </summary>
- /// <remarks>
- /// The control class extends <see cref="InputElement"/> and adds the following features:
- ///
- /// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
- /// - <see cref="ContextRequestedEvent"/> and other context menu related members.
- /// </remarks>
- public class Control : InputElement, IDataTemplateHost, IVisualBrushInitialize, ISetterValue
- {
- /// <summary>
- /// Defines the <see cref="FocusAdorner"/> property.
- /// </summary>
- public static readonly StyledProperty<ITemplate<Control>?> FocusAdornerProperty =
- AvaloniaProperty.Register<Control, ITemplate<Control>?>(nameof(FocusAdorner));
- /// <summary>
- /// Defines the <see cref="Tag"/> property.
- /// </summary>
- public static readonly StyledProperty<object?> TagProperty =
- AvaloniaProperty.Register<Control, object?>(nameof(Tag));
-
- /// <summary>
- /// Defines the <see cref="ContextMenu"/> property.
- /// </summary>
- public static readonly StyledProperty<ContextMenu?> ContextMenuProperty =
- AvaloniaProperty.Register<Control, ContextMenu?>(nameof(ContextMenu));
- /// <summary>
- /// Defines the <see cref="ContextFlyout"/> property
- /// </summary>
- public static readonly StyledProperty<FlyoutBase?> ContextFlyoutProperty =
- AvaloniaProperty.Register<Control, FlyoutBase?>(nameof(ContextFlyout));
- /// <summary>
- /// Event raised when an element wishes to be scrolled into view.
- /// </summary>
- public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
- RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>(
- "RequestBringIntoView",
- RoutingStrategies.Bubble);
- /// <summary>
- /// Provides event data for the <see cref="ContextRequested"/> event.
- /// </summary>
- public static readonly RoutedEvent<ContextRequestedEventArgs> ContextRequestedEvent =
- RoutedEvent.Register<Control, ContextRequestedEventArgs>(
- nameof(ContextRequested),
- RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
-
- /// <summary>
- /// Defines the <see cref="Loaded"/> event.
- /// </summary>
- public static readonly RoutedEvent<RoutedEventArgs> LoadedEvent =
- RoutedEvent.Register<Control, RoutedEventArgs>(
- nameof(Loaded),
- RoutingStrategies.Direct);
- /// <summary>
- /// Defines the <see cref="Unloaded"/> event.
- /// </summary>
- public static readonly RoutedEvent<RoutedEventArgs> UnloadedEvent =
- RoutedEvent.Register<Control, RoutedEventArgs>(
- nameof(Unloaded),
- RoutingStrategies.Direct);
- /// <summary>
- /// Defines the <see cref="SizeChanged"/> event.
- /// </summary>
- public static readonly RoutedEvent<SizeChangedEventArgs> SizeChangedEvent =
- RoutedEvent.Register<Control, SizeChangedEventArgs>(
- nameof(SizeChanged), RoutingStrategies.Direct);
- // Note the following:
- // _loadedQueue :
- // Is the queue where any control will be added to indicate that its loaded
- // event should be scheduled and called later.
- // _loadedProcessingQueue :
- // Contains a copied snapshot of the _loadedQueue at the time when processing
- // starts and individual events are being fired. This was needed to avoid
- // exceptions if new controls were added in the Loaded event itself.
- private static bool _isLoadedProcessing = false;
- private static readonly HashSet<Control> _loadedQueue = new HashSet<Control>();
- private static readonly HashSet<Control> _loadedProcessingQueue = new HashSet<Control>();
- private LoadState _loadState = LoadState.Unloaded;
- private DataTemplates? _dataTemplates;
- private Control? _focusAdorner;
- private AutomationPeer? _automationPeer;
- /// <summary>
- /// Gets or sets the control's focus adorner.
- /// </summary>
- public ITemplate<Control>? FocusAdorner
- {
- get => GetValue(FocusAdornerProperty);
- set => SetValue(FocusAdornerProperty, value);
- }
- /// <summary>
- /// Gets or sets the data templates for the control.
- /// </summary>
- /// <remarks>
- /// Each control may define data templates which are applied to the control itself and its
- /// children.
- /// </remarks>
- public DataTemplates DataTemplates => _dataTemplates ??= new DataTemplates();
- /// <summary>
- /// Gets or sets a context menu to the control.
- /// </summary>
- public ContextMenu? ContextMenu
- {
- get => GetValue(ContextMenuProperty);
- set => SetValue(ContextMenuProperty, value);
- }
- /// <summary>
- /// Gets or sets a context flyout to the control
- /// </summary>
- public FlyoutBase? ContextFlyout
- {
- get => GetValue(ContextFlyoutProperty);
- set => SetValue(ContextFlyoutProperty, value);
- }
- /// <summary>
- /// Gets a value indicating whether the control is fully constructed in the visual tree
- /// and both layout and render are complete.
- /// </summary>
- /// <remarks>
- /// This is set to true while raising the <see cref="Loaded"/> event.
- /// </remarks>
- public bool IsLoaded => _loadState == LoadState.Loaded;
- /// <summary>
- /// Gets or sets a user-defined object attached to the control.
- /// </summary>
- public object? Tag
- {
- get => GetValue(TagProperty);
- set => SetValue(TagProperty, value);
- }
-
- /// <summary>
- /// Occurs when the user has completed a context input gesture, such as a right-click.
- /// </summary>
- public event EventHandler<ContextRequestedEventArgs>? ContextRequested
- {
- add => AddHandler(ContextRequestedEvent, value);
- remove => RemoveHandler(ContextRequestedEvent, value);
- }
- /// <summary>
- /// Occurs when the control has been fully constructed in the visual tree and both
- /// layout and render are complete.
- /// </summary>
- /// <remarks>
- /// This event is guaranteed to occur after the control template is applied and references
- /// to objects created after the template is applied are available. This makes it different
- /// from OnAttachedToVisualTree which doesn't have these references. This event occurs at the
- /// latest possible time in the control creation life-cycle.
- /// </remarks>
- public event EventHandler<RoutedEventArgs>? Loaded
- {
- add => AddHandler(LoadedEvent, value);
- remove => RemoveHandler(LoadedEvent, value);
- }
- /// <summary>
- /// Occurs when the control is removed from the visual tree.
- /// </summary>
- /// <remarks>
- /// This is API symmetrical with <see cref="Loaded"/> and exists for compatibility with other
- /// XAML frameworks; however, it behaves the same as OnDetachedFromVisualTree.
- /// </remarks>
- public event EventHandler<RoutedEventArgs>? Unloaded
- {
- add => AddHandler(UnloadedEvent, value);
- remove => RemoveHandler(UnloadedEvent, value);
- }
- /// <summary>
- /// Occurs when the bounds (actual size) of the control have changed.
- /// </summary>
- public event EventHandler<SizeChangedEventArgs>? SizeChanged
- {
- add => AddHandler(SizeChangedEvent, value);
- remove => RemoveHandler(SizeChangedEvent, value);
- }
- /// <inheritdoc/>
- bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
- /// <inheritdoc/>
- void ISetterValue.Initialize(SetterBase setter)
- {
- if (setter is Setter s && s.Property == ContextFlyoutProperty)
- {
- return; // Allow ContextFlyout to not need wrapping in <Template>
- }
- throw new InvalidOperationException(
- "Cannot use a control as a Setter value. Wrap the control in a <Template>.");
- }
- /// <inheritdoc/>
- void IVisualBrushInitialize.EnsureInitialized()
- {
- if (VisualRoot == null)
- {
- if (!IsInitialized)
- {
- foreach (var i in this.GetSelfAndVisualDescendants())
- {
- var c = i as Control;
- if (c?.IsInitialized == false && c is ISupportInitialize init)
- {
- init.BeginInit();
- init.EndInit();
- }
- }
- }
- if (!IsArrangeValid)
- {
- Measure(Size.Infinity);
- Arrange(new Rect(DesiredSize));
- }
- }
- }
- /// <summary>
- /// Gets the element that receives the focus adorner.
- /// </summary>
- /// <returns>The control that receives the focus adorner.</returns>
- protected virtual Control? GetTemplateFocusTarget() => this;
- private static Action loadedProcessingAction = () =>
- {
- // Copy the loaded queue for processing
- // There was a possibility of the "Collection was modified; enumeration operation may not execute."
- // exception when only a single hash set was used. This could happen when new controls are added
- // within the Loaded callback/event itself. To fix this, two hash sets are used and while one is
- // being processed the other accepts adding new controls to process next.
- _loadedProcessingQueue.Clear();
- foreach (Control control in _loadedQueue)
- {
- _loadedProcessingQueue.Add(control);
- }
- _loadedQueue.Clear();
- foreach (Control control in _loadedProcessingQueue)
- {
- control.OnLoadedCore();
- }
- _loadedProcessingQueue.Clear();
- _isLoadedProcessing = false;
- // Restart if any controls were added to the queue while processing
- if (_loadedQueue.Count > 0)
- {
- _isLoadedProcessing = true;
- Dispatcher.UIThread.Post(loadedProcessingAction!, DispatcherPriority.Loaded);
- }
- };
- /// <summary>
- /// Schedules <see cref="OnLoadedCore"/> to be called for this control.
- /// For performance, it will be queued with other controls.
- /// </summary>
- internal void ScheduleOnLoadedCore()
- {
- if (_loadState == LoadState.Unloaded)
- {
- bool isAdded = _loadedQueue.Add(this);
- _loadState = LoadState.LoadPending;
- if (isAdded &&
- _isLoadedProcessing == false)
- {
- _isLoadedProcessing = true;
- Dispatcher.UIThread.Post(loadedProcessingAction!, DispatcherPriority.Loaded);
- }
- }
- }
- /// <summary>
- /// Invoked as the first step of marking the control as loaded and raising the
- /// <see cref="Loaded"/> event.
- /// </summary>
- internal void OnLoadedCore()
- {
- if (_loadState == LoadState.LoadPending &&
- ((ILogical)this).IsAttachedToLogicalTree)
- {
- _loadState = LoadState.Loaded;
- OnLoaded(new RoutedEventArgs(LoadedEvent, this));
- }
- else
- {
- // We somehow got here while being detached?
- _loadState = LoadState.Unloaded;
- }
- }
- /// <summary>
- /// Invoked as the first step of marking the control as unloaded and raising the
- /// <see cref="Unloaded"/> event.
- /// </summary>
- internal void OnUnloadedCore()
- {
- switch (_loadState)
- {
- case LoadState.Loaded:
- _loadState = LoadState.Unloaded;
- OnUnloaded(new RoutedEventArgs(UnloadedEvent, this));
- break;
- case LoadState.LoadPending:
- // Remove from the loaded event queue here as a failsafe in case the control
- // is detached before the dispatcher runs the Loaded jobs.
- _loadedQueue.Remove(this);
- _loadState = LoadState.Unloaded;
- break;
- }
- }
- /// <summary>
- /// Invoked just before the <see cref="Loaded"/> event.
- /// </summary>
- /// <param name="e">The event args.</param>
- protected virtual void OnLoaded(RoutedEventArgs e)
- {
- RaiseEvent(e);
- }
- /// <summary>
- /// Invoked just before the <see cref="Unloaded"/> event.
- /// </summary>
- /// <param name="e">The event args.</param>
- protected virtual void OnUnloaded(RoutedEventArgs e)
- {
- RaiseEvent(e);
- }
- /// <summary>
- /// Invoked just before the <see cref="SizeChanged"/> event.
- /// </summary>
- /// <param name="e">The event args.</param>
- protected virtual void OnSizeChanged(SizeChangedEventArgs e)
- {
- RaiseEvent(e);
- }
- /// <inheritdoc/>
- protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
- {
- base.OnAttachedToVisualTreeCore(e);
- InitializeIfNeeded();
- ScheduleOnLoadedCore();
- Holding += OnHoldEvent;
- }
- private void OnHoldEvent(object? sender, HoldingRoutedEventArgs e)
- {
- if (e.Source == this && !e.Handled && e.HoldingState == HoldingState.Started)
- {
- // Trigger ContentRequest when hold has started
- var contextEvent = e.PointerEventArgs is { } ev ? new ContextRequestedEventArgs(ev) : new ContextRequestedEventArgs();
- RaiseEvent(contextEvent);
- e.Handled = contextEvent.Handled;
- }
- }
- /// <inheritdoc/>
- protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
- {
- base.OnDetachedFromVisualTreeCore(e);
- OnUnloadedCore();
- Holding -= OnHoldEvent;
- }
- /// <inheritdoc/>
- protected override void OnGotFocus(GotFocusEventArgs e)
- {
- base.OnGotFocus(e);
- if (IsFocused &&
- (e.NavigationMethod == NavigationMethod.Tab ||
- e.NavigationMethod == NavigationMethod.Directional))
- {
- var adornerLayer = AdornerLayer.GetAdornerLayer(this);
- if (adornerLayer != null)
- {
- if (_focusAdorner == null)
- {
- var template = IsSet(FocusAdornerProperty)
- ? GetValue(FocusAdornerProperty)
- : adornerLayer.DefaultFocusAdorner;
- if (template != null)
- {
- _focusAdorner = template.Build();
- }
- }
- if (_focusAdorner != null && GetTemplateFocusTarget() is Visual target)
- {
- AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target);
- adornerLayer.Children.Add(_focusAdorner);
- }
- }
- }
- }
- /// <inheritdoc/>
- protected override void OnLostFocus(RoutedEventArgs e)
- {
- base.OnLostFocus(e);
- if (_focusAdorner?.Parent != null)
- {
- var adornerLayer = (Panel)_focusAdorner.Parent;
- adornerLayer.Children.Remove(_focusAdorner);
- _focusAdorner = null;
- }
- }
- /// <summary>
- /// Returns a new, type-specific <see cref="AutomationPeer"/> implementation for the control.
- /// </summary>
- /// <returns>The type-specific <see cref="AutomationPeer"/> implementation.</returns>
- protected virtual AutomationPeer OnCreateAutomationPeer()
- {
- return new NoneAutomationPeer(this);
- }
- internal AutomationPeer? GetAutomationPeer()
- {
- VerifyAccess();
- return _automationPeer;
- }
- internal AutomationPeer GetOrCreateAutomationPeer()
- {
- VerifyAccess();
- if (_automationPeer is object)
- {
- return _automationPeer;
- }
- _automationPeer = OnCreateAutomationPeer();
- return _automationPeer;
- }
- /// <inheritdoc/>
- protected override void OnPointerReleased(PointerReleasedEventArgs e)
- {
- base.OnPointerReleased(e);
- if (e.Source == this
- && !e.Handled
- && e.InitialPressMouseButton == MouseButton.Right)
- {
- var args = new ContextRequestedEventArgs(e);
- RaiseEvent(args);
- e.Handled = args.Handled;
- }
- }
- /// <inheritdoc/>
- protected override void OnKeyUp(KeyEventArgs e)
- {
- base.OnKeyUp(e);
- if (e.Source == this
- && !e.Handled)
- {
- var keymap = TopLevel.GetTopLevel(this)?.PlatformSettings?.HotkeyConfiguration.OpenContextMenu;
- if (keymap is null)
- {
- return;
- }
- var matches = false;
- for (var index = 0; index < keymap.Count; index++)
- {
- var key = keymap[index];
- matches |= key.Matches(e);
- if (matches)
- {
- break;
- }
- }
- if (matches)
- {
- var args = new ContextRequestedEventArgs();
- RaiseEvent(args);
- e.Handled = args.Handled;
- }
- }
- }
- /// <inheritdoc/>
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
- {
- base.OnPropertyChanged(change);
- if (change.Property == BoundsProperty)
- {
- var oldValue = change.GetOldValue<Rect>();
- var newValue = change.GetNewValue<Rect>();
- // Bounds is a Rect with an X/Y Position as well as Height/Width.
- // This means it is possible for the Rect to change position but not size.
- // Therefore, we want to explicity check only the size and raise an event
- // only when that size has changed.
- if (newValue.Size != oldValue.Size)
- {
- var sizeChangedEventArgs = new SizeChangedEventArgs(
- SizeChangedEvent,
- source: this,
- previousSize: new Size(oldValue.Width, oldValue.Height),
- newSize: new Size(newValue.Width, newValue.Height));
- OnSizeChanged(sizeChangedEventArgs);
- }
- }
- }
- // Since we are resetting the dispatcher instance, the callback might never arrive
- internal static void ResetLoadedQueueForUnitTests()
- {
- _loadedQueue.Clear();
- _loadedProcessingQueue.Clear();
- _isLoadedProcessing = false;
- }
- private enum LoadState : byte
- {
- Unloaded,
- Loaded,
- LoadPending
- }
- }
- }
|