diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index ead5b0d9f3..0c8fc31df1 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
-
+using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
@@ -21,7 +21,7 @@ namespace Avalonia.Controls
///
/// A control context menu.
///
- public class ContextMenu : MenuBase, ISetterValue
+ public class ContextMenu : MenuBase, ISetterValue, IPopupHostProvider
{
///
/// Defines the property.
@@ -82,6 +82,7 @@ namespace Avalonia.Controls
private Popup? _popup;
private List? _attachedControls;
private IInputElement? _previousFocus;
+ private Action? _popupHostChangedHandler;
///
/// Initializes a new instance of the class.
@@ -304,6 +305,14 @@ namespace Avalonia.Controls
}
}
+ IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host;
+
+ event Action? IPopupHostProvider.PopupHostChanged
+ {
+ add => _popupHostChangedHandler += value;
+ remove => _popupHostChangedHandler -= value;
+ }
+
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
@@ -364,6 +373,8 @@ namespace Avalonia.Controls
{
_previousFocus = FocusManager.Instance?.Current;
Focus();
+
+ _popupHostChangedHandler?.Invoke(_popup!.Host);
}
private void PopupClosing(object sender, CancelEventArgs e)
@@ -397,6 +408,8 @@ namespace Avalonia.Controls
RoutedEvent = MenuClosedEvent,
Source = this,
});
+
+ _popupHostChangedHandler?.Invoke(null);
}
private void PopupKeyUp(object sender, KeyEventArgs e)
diff --git a/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs
new file mode 100644
index 0000000000..11d3b1792a
--- /dev/null
+++ b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs
@@ -0,0 +1,23 @@
+using System;
+using Avalonia.Controls.Primitives;
+
+#nullable enable
+
+namespace Avalonia.Controls.Diagnostics
+{
+ ///
+ /// Diagnostics interface to retrieve an associated .
+ ///
+ public interface IPopupHostProvider
+ {
+ ///
+ /// The popup host.
+ ///
+ IPopupHost? PopupHost { get; }
+
+ ///
+ /// Raised when the popup host changes.
+ ///
+ event Action? PopupHostChanged;
+ }
+}
diff --git a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs
new file mode 100644
index 0000000000..4acf2a217f
--- /dev/null
+++ b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs
@@ -0,0 +1,15 @@
+#nullable enable
+
+namespace Avalonia.Controls.Diagnostics
+{
+ ///
+ /// Helper class to provide diagnostics information for .
+ ///
+ public static class ToolTipDiagnostics
+ {
+ ///
+ /// Provides access to the internal for use in DevTools.
+ ///
+ public static AvaloniaProperty ToolTipProperty = ToolTip.ToolTipProperty;
+ }
+}
diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs
index 8448dde21e..230b4954fe 100644
--- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs
+++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs
@@ -1,19 +1,18 @@
using System;
using System.ComponentModel;
+using Avalonia.Controls.Diagnostics;
using System.Linq;
-
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
-using Avalonia.Rendering;
#nullable enable
namespace Avalonia.Controls.Primitives
{
- public abstract class FlyoutBase : AvaloniaObject
+ public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider
{
static FlyoutBase()
{
@@ -59,6 +58,7 @@ namespace Avalonia.Controls.Primitives
private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
+ private Action? _popupHostChangedHandler;
public FlyoutBase()
{
@@ -103,6 +103,14 @@ namespace Avalonia.Controls.Primitives
private set => SetAndRaise(TargetProperty, ref _target, value);
}
+ IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
+
+ event Action? IPopupHostProvider.PopupHostChanged
+ {
+ add => _popupHostChangedHandler += value;
+ remove => _popupHostChangedHandler -= value;
+ }
+
public event EventHandler? Closed;
public event EventHandler? Closing;
public event EventHandler? Opened;
@@ -363,6 +371,8 @@ namespace Avalonia.Controls.Primitives
private void OnPopupOpened(object sender, EventArgs e)
{
IsOpen = true;
+
+ _popupHostChangedHandler?.Invoke(Popup!.Host);
}
private void OnPopupClosing(object sender, CancelEventArgs e)
@@ -376,6 +386,8 @@ namespace Avalonia.Controls.Primitives
private void OnPopupClosed(object sender, EventArgs e)
{
HideCore(false);
+
+ _popupHostChangedHandler?.Invoke(null);
}
// This method is handling both popup logical tree and target logical tree.
diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
index cb1291410a..a5495fdfc9 100644
--- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
+++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
@@ -21,10 +21,12 @@ namespace Avalonia.Controls.Platform
public void RunLoop(CancellationToken cancellationToken)
{
- while (true)
+ var handles = new[] { _signaled, cancellationToken.WaitHandle };
+
+ while (!cancellationToken.IsCancellationRequested)
{
Signaled?.Invoke(null);
- _signaled.WaitOne();
+ WaitHandle.WaitAny(handles);
}
}
diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
index 762d8d37a6..403902f676 100644
--- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
@@ -140,10 +140,5 @@ namespace Avalonia.Controls.Primitives
return new OverlayPopupHost(overlayLayer);
}
-
- public override void Render(DrawingContext context)
- {
- context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size));
- }
}
}
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index f23a27e67a..d5fb69a672 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
+using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
@@ -18,7 +19,7 @@ namespace Avalonia.Controls.Primitives
///
/// Displays a popup window.
///
- public class Popup : Control, IVisualTreeHost
+ public class Popup : Control, IVisualTreeHost, IPopupHostProvider
{
public static readonly StyledProperty WindowManagerAddShadowHintProperty =
AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true);
@@ -134,6 +135,7 @@ namespace Avalonia.Controls.Primitives
private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState;
private IInputElement _overlayInputPassThroughElement;
+ private Action? _popupHostChangedHandler;
///
/// Initializes static members of the class.
@@ -351,6 +353,14 @@ namespace Avalonia.Controls.Primitives
///
IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot;
+ IPopupHost? IPopupHostProvider.PopupHost => Host;
+
+ event Action? IPopupHostProvider.PopupHostChanged
+ {
+ add => _popupHostChangedHandler += value;
+ remove => _popupHostChangedHandler -= value;
+ }
+
///
/// Opens the popup.
///
@@ -482,6 +492,8 @@ namespace Avalonia.Controls.Primitives
}
Opened?.Invoke(this, EventArgs.Empty);
+
+ _popupHostChangedHandler?.Invoke(Host);
}
///
@@ -591,6 +603,8 @@ namespace Avalonia.Controls.Primitives
_openState.Dispose();
_openState = null;
+ _popupHostChangedHandler?.Invoke(null);
+
using (BeginIgnoringIsOpen())
{
IsOpen = false;
diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs
index d264ed76cc..d5d6af8bfa 100644
--- a/src/Avalonia.Controls/Primitives/ScrollBar.cs
+++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs
@@ -57,6 +57,18 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty AllowAutoHideProperty =
AvaloniaProperty.Register(nameof(AllowAutoHide), true);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HideDelayProperty =
+ AvaloniaProperty.Register(nameof(HideDelay), TimeSpan.FromSeconds(2));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ShowDelayProperty =
+ AvaloniaProperty.Register(nameof(ShowDelay), TimeSpan.FromSeconds(0.5));
+
private Button _lineUpButton;
private Button _lineDownButton;
private Button _pageUpButton;
@@ -126,6 +138,24 @@ namespace Avalonia.Controls.Primitives
get => GetValue(AllowAutoHideProperty);
set => SetValue(AllowAutoHideProperty, value);
}
+
+ ///
+ /// Gets a value that determines how long will be the hide delay after user stops interacting with the scrollbar.
+ ///
+ public TimeSpan HideDelay
+ {
+ get => GetValue(HideDelayProperty);
+ set => SetValue(HideDelayProperty, value);
+ }
+
+ ///
+ /// Gets a value that determines how long will be the show delay when user starts interacting with the scrollbar.
+ ///
+ public TimeSpan ShowDelay
+ {
+ get => GetValue(ShowDelayProperty);
+ set => SetValue(ShowDelayProperty, value);
+ }
public event EventHandler Scroll;
@@ -296,12 +326,12 @@ namespace Avalonia.Controls.Primitives
private void CollapseAfterDelay()
{
- InvokeAfterDelay(Collapse, TimeSpan.FromSeconds(2));
+ InvokeAfterDelay(Collapse, HideDelay);
}
private void ExpandAfterDelay()
{
- InvokeAfterDelay(Expand, TimeSpan.FromMilliseconds(400));
+ InvokeAfterDelay(Expand, ShowDelay);
}
private void Collapse()
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 91411cbd3b..0eade8d6df 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -31,7 +31,7 @@ namespace Avalonia.Controls
public static KeyGesture PasteGesture { get; } = AvaloniaLocator.Current
.GetService()?.Paste.FirstOrDefault();
-
+
public static readonly StyledProperty AcceptsReturnProperty =
AvaloniaProperty.Register(nameof(AcceptsReturn));
@@ -117,7 +117,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty RevealPasswordProperty =
AvaloniaProperty.Register(nameof(RevealPassword));
-
+
public static readonly DirectProperty CanCutProperty =
AvaloniaProperty.RegisterDirect(
nameof(CanCut),
@@ -135,7 +135,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty IsUndoEnabledProperty =
AvaloniaProperty.Register(
- nameof(IsUndoEnabled),
+ nameof(IsUndoEnabled),
defaultValue: true);
public static readonly DirectProperty UndoLimitProperty =
@@ -157,6 +157,10 @@ namespace Avalonia.Controls
}
public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text);
+
+ public override bool Equals(object obj) => obj is UndoRedoState other && Equals(other);
+
+ public override int GetHashCode() => Text.GetHashCode();
}
private string _text;
@@ -174,6 +178,10 @@ namespace Avalonia.Controls
private string _newLine = Environment.NewLine;
private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
+ private int _selectedTextChangesMadeSinceLastUndoSnapshot;
+ private bool _hasDoneSnapshotOnce;
+ private const int _maxCharsBeforeUndoSnapshot = 7;
+
static TextBox()
{
FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
@@ -202,7 +210,8 @@ namespace Avalonia.Controls
horizontalScrollBarVisibility,
BindingPriority.Style);
_undoRedoHelper = new UndoRedoHelper(this);
-
+ _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
+ _hasDoneSnapshotOnce = false;
UpdatePseudoclasses();
}
@@ -331,6 +340,7 @@ namespace Avalonia.Controls
if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
{
_undoRedoHelper.Clear();
+ SnapshotUndoRedo(); // so we always have an initial state
}
}
}
@@ -341,16 +351,16 @@ namespace Avalonia.Controls
get { return GetSelection(); }
set
{
- SnapshotUndoRedo();
if (string.IsNullOrEmpty(value))
{
+ _selectedTextChangesMadeSinceLastUndoSnapshot++;
+ SnapshotUndoRedo(ignoreChangeCount: false);
DeleteSelection();
}
else
{
HandleTextInput(value);
}
- SnapshotUndoRedo();
}
}
@@ -422,7 +432,7 @@ namespace Avalonia.Controls
get { return _newLine; }
set { SetAndRaise(NewLineProperty, ref _newLine, value); }
}
-
+
///
/// Clears the current selection, maintaining the
///
@@ -480,11 +490,13 @@ namespace Avalonia.Controls
var oldValue = _undoRedoHelper.Limit;
_undoRedoHelper.Limit = value;
RaisePropertyChanged(UndoLimitProperty, oldValue, value);
- }
+ }
// from docs at
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
// "Setting UndoLimit clears the undo queue."
_undoRedoHelper.Clear();
+ _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
+ _hasDoneSnapshotOnce = false;
}
}
@@ -515,6 +527,8 @@ namespace Avalonia.Controls
// Therefore, if you disable undo and then re-enable it, undo commands still do not work
// because the undo stack was emptied when you disabled undo."
_undoRedoHelper.Clear();
+ _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
+ _hasDoneSnapshotOnce = false;
}
}
@@ -577,23 +591,25 @@ namespace Avalonia.Controls
{
return;
}
-
+
input = RemoveInvalidCharacters(input);
-
+
if (string.IsNullOrEmpty(input))
{
return;
}
-
+ _selectedTextChangesMadeSinceLastUndoSnapshot++;
+ SnapshotUndoRedo(ignoreChangeCount: false);
+
string text = Text ?? string.Empty;
int caretIndex = CaretIndex;
int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
-
+
if (MaxLength > 0 && newLength > MaxLength)
{
input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
}
-
+
if (!string.IsNullOrEmpty(input))
{
DeleteSelection();
@@ -627,7 +643,6 @@ namespace Avalonia.Controls
SnapshotUndoRedo();
Copy();
DeleteSelection();
- SnapshotUndoRedo();
}
public async void Copy()
@@ -647,7 +662,6 @@ namespace Avalonia.Controls
SnapshotUndoRedo();
HandleTextInput(text);
- SnapshotUndoRedo();
}
protected override void OnKeyDown(KeyEventArgs e)
@@ -696,6 +710,7 @@ namespace Avalonia.Controls
{
try
{
+ SnapshotUndoRedo();
_isUndoingRedoing = true;
_undoRedoHelper.Undo();
}
@@ -830,7 +845,6 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters;
ClearSelection();
}
- SnapshotUndoRedo();
handled = true;
break;
@@ -858,7 +872,6 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
}
- SnapshotUndoRedo();
handled = true;
break;
@@ -868,7 +881,6 @@ namespace Avalonia.Controls
{
SnapshotUndoRedo();
HandleTextInput(NewLine);
- SnapshotUndoRedo();
handled = true;
}
@@ -879,7 +891,6 @@ namespace Avalonia.Controls
{
SnapshotUndoRedo();
HandleTextInput("\t");
- SnapshotUndoRedo();
handled = true;
}
else
@@ -889,6 +900,10 @@ namespace Avalonia.Controls
break;
+ case Key.Space:
+ SnapshotUndoRedo(); // always snapshot in between words
+ break;
+
default:
handled = false;
break;
@@ -1319,11 +1334,19 @@ namespace Avalonia.Controls
}
}
- private void SnapshotUndoRedo()
+ private void SnapshotUndoRedo(bool ignoreChangeCount = true)
{
if (IsUndoEnabled)
{
- _undoRedoHelper.Snapshot();
+ if (ignoreChangeCount ||
+ !_hasDoneSnapshotOnce ||
+ (!ignoreChangeCount &&
+ _selectedTextChangesMadeSinceLastUndoSnapshot >= _maxCharsBeforeUndoSnapshot))
+ {
+ _undoRedoHelper.Snapshot();
+ _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
+ _hasDoneSnapshotOnce = true;
+ }
}
}
}
diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs
index ab507d07a2..ab310d60ef 100644
--- a/src/Avalonia.Controls/ToolTip.cs
+++ b/src/Avalonia.Controls/ToolTip.cs
@@ -1,9 +1,8 @@
#nullable enable
using System;
-using System.Reactive.Linq;
+using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
-using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@@ -17,7 +16,7 @@ namespace Avalonia.Controls
/// assigning the content that you want displayed.
///
[PseudoClasses(":open")]
- public class ToolTip : ContentControl
+ public class ToolTip : ContentControl, IPopupHostProvider
{
///
/// Defines the ToolTip.Tip attached property.
@@ -61,7 +60,8 @@ namespace Avalonia.Controls
internal static readonly AttachedProperty ToolTipProperty =
AvaloniaProperty.RegisterAttached("ToolTip");
- private IPopupHost? _popup;
+ private IPopupHost? _popupHost;
+ private Action? _popupHostChangedHandler;
///
/// Initializes static members of the class.
@@ -251,35 +251,45 @@ namespace Avalonia.Controls
tooltip.RecalculatePosition(control);
}
+
+ IPopupHost? IPopupHostProvider.PopupHost => _popupHost;
+
+ event Action? IPopupHostProvider.PopupHostChanged
+ {
+ add => _popupHostChangedHandler += value;
+ remove => _popupHostChangedHandler -= value;
+ }
internal void RecalculatePosition(Control control)
{
- _popup?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
+ _popupHost?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
}
private void Open(Control control)
{
Close();
- _popup = OverlayPopupHost.CreatePopupHost(control, null);
- _popup.SetChild(this);
- ((ISetLogicalParent)_popup).SetParent(control);
+ _popupHost = OverlayPopupHost.CreatePopupHost(control, null);
+ _popupHost.SetChild(this);
+ ((ISetLogicalParent)_popupHost).SetParent(control);
- _popup.ConfigurePosition(control, GetPlacement(control),
+ _popupHost.ConfigurePosition(control, GetPlacement(control),
new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
- WindowManagerAddShadowHintChanged(_popup, false);
+ WindowManagerAddShadowHintChanged(_popupHost, false);
- _popup.Show();
+ _popupHost.Show();
+ _popupHostChangedHandler?.Invoke(_popupHost);
}
private void Close()
{
- if (_popup != null)
+ if (_popupHost != null)
{
- _popup.SetChild(null);
- _popup.Dispose();
- _popup = null;
+ _popupHost.SetChild(null);
+ _popupHost.Dispose();
+ _popupHost = null;
+ _popupHostChangedHandler?.Invoke(null);
}
}
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 96729c2c29..5d9a0c8eed 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -90,6 +90,7 @@ namespace Avalonia.Controls
///
static TopLevel()
{
+ KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle);
AffectsMeasure(ClientSizeProperty);
TransparencyLevelHintProperty.Changed.AddClassHandler(
diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs
index 7374f20a0c..fd1ca54b57 100644
--- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs
+++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs
@@ -7,7 +7,7 @@ using Avalonia.Utilities;
namespace Avalonia.Controls.Utils
{
- class UndoRedoHelper : WeakTimer.IWeakTimerSubscriber where TState : struct, IEquatable
+ class UndoRedoHelper
{
private readonly IUndoRedoHost _host;
@@ -31,7 +31,6 @@ namespace Avalonia.Controls.Utils
public UndoRedoHelper(IUndoRedoHost host)
{
_host = host;
- WeakTimer.StartWeakTimer(this, TimeSpan.FromSeconds(1));
}
public void Undo()
@@ -61,7 +60,7 @@ namespace Avalonia.Controls.Utils
if (_states.Last != null)
{
_states.Last.Value = state;
- }
+ }
}
public void UpdateLastState()
@@ -103,11 +102,5 @@ namespace Avalonia.Controls.Utils
_states.Clear();
_currentNode = null;
}
-
- bool WeakTimer.IWeakTimerSubscriber.Tick()
- {
- Snapshot();
- return true;
- }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
index 3f367165ac..d0a4ad38c5 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel;
-
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
@@ -22,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
+ private bool _freezePopups;
#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
@@ -41,6 +41,12 @@ namespace Avalonia.Diagnostics.ViewModels
Console = new ConsoleViewModel(UpdateConsoleContext);
}
+ public bool FreezePopups
+ {
+ get => _freezePopups;
+ set => RaiseAndSetIfChanged(ref _freezePopups, value);
+ }
+
public bool ShouldVisualizeMarginPadding
{
get => _shouldVisualizeMarginPadding;
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
index 4cb470eeac..94707ac189 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
@@ -1,26 +1,28 @@
using System;
-using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree;
+using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class TreeNode : ViewModelBase, IDisposable
{
- private IDisposable? _classesSubscription;
+ private readonly IDisposable? _classesSubscription;
private string _classes;
private bool _isExpanded;
- public TreeNode(IVisual visual, TreeNode? parent)
+ protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null)
{
+ _classes = string.Empty;
Parent = parent;
- Type = visual.GetType().Name;
+ Type = customName ?? visual.GetType().Name;
Visual = visual;
- _classes = string.Empty;
+ FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal;
if (visual is IControl control)
{
@@ -52,6 +54,12 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
+ private bool IsRoot => Visual is TopLevel ||
+ Visual is ContextMenu ||
+ Visual is IPopupHost;
+
+ public FontWeight FontWeight { get; }
+
public abstract TreeNodeCollection Children
{
get;
@@ -95,20 +103,5 @@ namespace Avalonia.Diagnostics.ViewModels
_classesSubscription?.Dispose();
Children.Dispose();
}
-
- private static int IndexOf(IReadOnlyList collection, TreeNode item)
- {
- var count = collection.Count;
-
- for (var i = 0; i < count; ++i)
- {
- if (collection[i] == item)
- {
- return i;
- }
- }
-
- throw new AvaloniaInternalException("TreeNode was not present in parent Children collection.");
- }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
index 48fa636664..6a430897ba 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
@@ -1,5 +1,10 @@
using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Diagnostics;
+using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using Avalonia.VisualTree;
@@ -7,31 +12,30 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class VisualTreeNode : TreeNode
{
- public VisualTreeNode(IVisual visual, TreeNode? parent)
- : base(visual, parent)
+ public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null)
+ : base(visual, parent, customName)
{
Children = new VisualTreeNodeCollection(this, visual);
- if ((Visual is IStyleable styleable))
- {
+ if (Visual is IStyleable styleable)
IsInTemplate = styleable.TemplatedParent != null;
- }
}
- public bool IsInTemplate { get; private set; }
+ public bool IsInTemplate { get; }
public override TreeNodeCollection Children { get; }
public static VisualTreeNode[] Create(object control)
{
- var visual = control as IVisual;
- return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty();
+ return control is IVisual visual ?
+ new[] { new VisualTreeNode(visual, null) } :
+ Array.Empty();
}
internal class VisualTreeNodeCollection : TreeNodeCollection
{
private readonly IVisual _control;
- private IDisposable? _subscription;
+ private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2);
public VisualTreeNodeCollection(TreeNode owner, IVisual control)
: base(owner)
@@ -41,15 +45,106 @@ namespace Avalonia.Diagnostics.ViewModels
public override void Dispose()
{
- _subscription?.Dispose();
+ _subscriptions.Dispose();
+ }
+
+ private static IObservable? GetHostedPopupRootObservable(IVisual visual)
+ {
+ static IObservable GetPopupHostObservable(
+ IPopupHostProvider popupHostProvider,
+ string? providerName = null)
+ {
+ return Observable.FromEvent(
+ x => popupHostProvider.PopupHostChanged += x,
+ x => popupHostProvider.PopupHostChanged -= x)
+ .StartWith(popupHostProvider.PopupHost)
+ .Select(popupHost =>
+ {
+ if (popupHost is IControl control)
+ return new PopupRoot(
+ control,
+ providerName != null ? $"{providerName} ({control.GetType().Name})" : null);
+
+ return (PopupRoot?)null;
+ });
+ }
+
+ return visual switch
+ {
+ Popup p => GetPopupHostObservable(p),
+ Control c => Observable.CombineLatest(
+ c.GetObservable(Control.ContextFlyoutProperty),
+ c.GetObservable(Control.ContextMenuProperty),
+ c.GetObservable(FlyoutBase.AttachedFlyoutProperty),
+ c.GetObservable(ToolTipDiagnostics.ToolTipProperty),
+ (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) =>
+ {
+ if (ContextMenu != null)
+ //Note: ContextMenus are special since all the items are added as visual children.
+ //So we don't need to go via Popup
+ return Observable.Return(new PopupRoot(ContextMenu));
+
+ if (ContextFlyout != null)
+ return GetPopupHostObservable(ContextFlyout, "ContextFlyout");
+
+ if (AttachedFlyout != null)
+ return GetPopupHostObservable(AttachedFlyout, "AttachedFlyout");
+
+ if (ToolTip != null)
+ return GetPopupHostObservable(ToolTip, "ToolTip");
+
+ return Observable.Return(null);
+ })
+ .Switch(),
+ _ => null
+ };
}
protected override void Initialize(AvaloniaList nodes)
{
- _subscription = _control.VisualChildren.ForEachItem(
- (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
- (i, item) => nodes.RemoveAt(i),
- () => nodes.Clear());
+ _subscriptions.Clear();
+
+ if (GetHostedPopupRootObservable(_control) is { } popupRootObservable)
+ {
+ VisualTreeNode? childNode = null;
+
+ _subscriptions.Add(
+ popupRootObservable
+ .Subscribe(popupRoot =>
+ {
+ if (popupRoot != null)
+ {
+ childNode = new VisualTreeNode(
+ popupRoot.Value.Root,
+ Owner,
+ popupRoot.Value.CustomName);
+
+ nodes.Add(childNode);
+ }
+ else if (childNode != null)
+ {
+ nodes.Remove(childNode);
+ }
+ }));
+ }
+
+ _subscriptions.Add(
+ _control.VisualChildren.ForEachItem(
+ (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
+ (i, item) => nodes.RemoveAt(i),
+ () => nodes.Clear()));
+ }
+
+ private struct PopupRoot
+ {
+ public PopupRoot(IControl root, string? customName = null)
+ {
+ Root = root;
+ CustomName = customName;
+ }
+
+ public IControl Root { get; }
+ public string? CustomName { get; }
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
index 8c4db33f91..6f2ac96a66 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
@@ -5,14 +5,14 @@
-
+
-
-
-
+
+
+
+ Content="{Binding Content}" />
-
+ IsVisible="False" />
+
+ IsVisible="{Binding IsVisible}" />
+
-
- Hold Ctrl+Shift over a control to inspect.
-
- Focused:
-
-
- Pointer Over:
-
-
+
+
+ Hold Ctrl+Shift over a control to inspect.
+
+ Focused:
+
+
+ Pointer Over:
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
index d1232b749a..ea06c33e4d 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
@@ -1,8 +1,11 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
+using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
+using Avalonia.Data;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
@@ -15,6 +18,7 @@ namespace Avalonia.Diagnostics.Views
internal class MainWindow : Window, IStyleHost
{
private readonly IDisposable _keySubscription;
+ private readonly Dictionary _frozenPopupStates;
private TopLevel? _root;
public MainWindow()
@@ -23,23 +27,26 @@ namespace Avalonia.Diagnostics.Views
_keySubscription = InputManager.Instance.Process
.OfType()
+ .Where(x => x.Type == RawKeyEventType.KeyDown)
.Subscribe(RawKeyDown);
+ _frozenPopupStates = new Dictionary();
+
EventHandler? lh = default;
lh = (s, e) =>
- {
- this.Opened -= lh;
- if ((DataContext as MainViewModel)?.StartupScreenIndex is int index)
- {
- var screens = this.Screens;
- if (index > -1 && index < screens.ScreenCount)
- {
- var screen = screens.All[index];
- this.Position = screen.Bounds.TopLeft;
- this.WindowState = WindowState.Maximized;
- }
- }
- };
+ {
+ this.Opened -= lh;
+ if ((DataContext as MainViewModel)?.StartupScreenIndex is { } index)
+ {
+ var screens = this.Screens;
+ if (index > -1 && index < screens.ScreenCount)
+ {
+ var screen = screens.All[index];
+ this.Position = screen.Bounds.TopLeft;
+ this.WindowState = WindowState.Maximized;
+ }
+ }
+ };
this.Opened += lh;
}
@@ -77,6 +84,13 @@ namespace Avalonia.Diagnostics.Views
base.OnClosed(e);
_keySubscription.Dispose();
+ foreach (var state in _frozenPopupStates)
+ {
+ state.Value.Dispose();
+ }
+
+ _frozenPopupStates.Clear();
+
if (_root != null)
{
_root.Closed -= RootClosed;
@@ -91,6 +105,53 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this);
}
+ private IControl? GetHoveredControl(TopLevel topLevel)
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ var point = (topLevel as IInputRoot)?.MouseDevice?.GetPosition(topLevel) ?? default;
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ return (IControl?)topLevel.GetVisualsAt(point, x =>
+ {
+ if (x is AdornerLayer || !x.IsVisible)
+ {
+ return false;
+ }
+
+ return !(x is IInputElement ie) || ie.IsHitTestVisible;
+ })
+ .FirstOrDefault();
+ }
+
+ private static List GetPopupRoots(IVisual root)
+ {
+ var popupRoots = new List();
+
+ void ProcessProperty(IControl control, AvaloniaProperty property)
+ {
+ if (control.GetValue(property) is IPopupHostProvider popupProvider
+ && popupProvider.PopupHost is PopupRoot popupRoot)
+ {
+ popupRoots.Add(popupRoot);
+ }
+ }
+
+ foreach (var control in root.GetVisualDescendants().OfType())
+ {
+ if (control is Popup p && p.Host is PopupRoot popupRoot)
+ {
+ popupRoots.Add(popupRoot);
+ }
+
+ ProcessProperty(control, ContextFlyoutProperty);
+ ProcessProperty(control, ContextMenuProperty);
+ ProcessProperty(control, FlyoutBase.AttachedFlyoutProperty);
+ ProcessProperty(control, ToolTipDiagnostics.ToolTipProperty);
+ }
+
+ return popupRoots;
+ }
+
private void RawKeyDown(RawKeyEventArgs e)
{
var vm = (MainViewModel?)DataContext;
@@ -99,34 +160,72 @@ namespace Avalonia.Diagnostics.Views
return;
}
- const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
-
- if (e.Modifiers == modifiers)
+ switch (e.Modifiers)
{
-#pragma warning disable CS0618 // Type or member is obsolete
- var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default;
-#pragma warning restore CS0618 // Type or member is obsolete
+ case RawInputModifiers.Control | RawInputModifiers.Shift:
+ {
+ IControl? control = null;
+
+ foreach (var popupRoot in GetPopupRoots(Root))
+ {
+ control = GetHoveredControl(popupRoot);
+
+ if (control != null)
+ {
+ break;
+ }
+ }
+
+ control ??= GetHoveredControl(Root);
- var control = Root.GetVisualsAt(point, x =>
+ if (control != null)
{
- if (x is AdornerLayer || !x.IsVisible) return false;
- if (!(x is IInputElement ie)) return true;
- return ie.IsHitTestVisible;
- })
- .FirstOrDefault();
+ vm.SelectControl(control);
+ }
+
+ break;
+ }
- if (control != null)
+ case RawInputModifiers.Control | RawInputModifiers.Alt when e.Key == Key.F:
{
- vm.SelectControl((IControl)control);
+ vm.FreezePopups = !vm.FreezePopups;
+
+ foreach (var popupRoot in GetPopupRoots(Root))
+ {
+ if (popupRoot.Parent is Popup popup)
+ {
+ if (vm.FreezePopups)
+ {
+ var lightDismissEnabledState = popup.SetValue(
+ Popup.IsLightDismissEnabledProperty,
+ !vm.FreezePopups,
+ BindingPriority.Animation);
+
+ if (lightDismissEnabledState != null)
+ {
+ _frozenPopupStates[popup] = lightDismissEnabledState;
+ }
+ }
+ else
+ {
+ //TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1
+ if (_frozenPopupStates.ContainsKey(popup))
+ {
+ _frozenPopupStates[popup].Dispose();
+ _frozenPopupStates.Remove(popup);
+ }
+ }
+ }
+ }
+
+ break;
}
- }
- else if (e.Modifiers == RawInputModifiers.Alt)
- {
- if (e.Key == Key.S || e.Key == Key.D)
+
+ case RawInputModifiers.Alt when e.Key == Key.S || e.Key == Key.D:
{
- var enable = e.Key == Key.S;
+ vm.EnableSnapshotStyles(e.Key == Key.S);
- vm.EnableSnapshotStyles(enable);
+ break;
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
index a5328716fc..bb661f7f4c 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
@@ -11,7 +11,7 @@
-
+
diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj
index c39c81a965..69a80290d1 100644
--- a/src/Avalonia.Input/Avalonia.Input.csproj
+++ b/src/Avalonia.Input/Avalonia.Input.csproj
@@ -4,6 +4,9 @@
Enable
CS8600;CS8602;CS8603
+
+
+
diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs
index 474b212a21..1432092ba1 100644
--- a/src/Avalonia.Input/FocusManager.cs
+++ b/src/Avalonia.Input/FocusManager.cs
@@ -92,6 +92,17 @@ namespace Avalonia.Input
}
}
+ public IInputElement? GetFocusedElement(IInputElement e)
+ {
+ if (e is IFocusScope scope)
+ {
+ _focusScopes.TryGetValue(scope, out var result);
+ return result;
+ }
+
+ return null;
+ }
+
///
/// Sets the currently focused element in the specified scope.
///
@@ -151,6 +162,8 @@ namespace Avalonia.Input
Focus(e);
}
+ public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope;
+
///
/// Checks if the specified element can be focused.
///
diff --git a/src/Avalonia.Input/ICustomKeyboardNavigation.cs b/src/Avalonia.Input/ICustomKeyboardNavigation.cs
index 3d2927c632..357395c42f 100644
--- a/src/Avalonia.Input/ICustomKeyboardNavigation.cs
+++ b/src/Avalonia.Input/ICustomKeyboardNavigation.cs
@@ -1,4 +1,5 @@
-
+#nullable enable
+
namespace Avalonia.Input
{
///
@@ -6,6 +7,18 @@ namespace Avalonia.Input
///
public interface ICustomKeyboardNavigation
{
- (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction);
+ ///
+ /// Gets the next element in the specified navigation direction.
+ ///
+ /// The element being navigated from.
+ /// The navigation direction.
+ ///
+ /// A tuple consisting of:
+ /// - A boolean indicating whether the request was handled. If false is returned then
+ /// custom navigation will be ignored and default navigation will take place.
+ /// - If handled is true: the next element in the navigation direction, or null if default
+ /// navigation should continue outside the element.
+ ///
+ (bool handled, IInputElement? next) GetNext(IInputElement element, NavigationDirection direction);
}
}
diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs
index 65b9acae76..63080e74e4 100644
--- a/src/Avalonia.Input/InputElement.cs
+++ b/src/Avalonia.Input/InputElement.cs
@@ -71,6 +71,12 @@ namespace Avalonia.Input
public static readonly DirectProperty IsPointerOverProperty =
AvaloniaProperty.RegisterDirect(nameof(IsPointerOver), o => o.IsPointerOver);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsTabStopProperty =
+ KeyboardNavigation.IsTabStopProperty.AddOwner();
+
///
/// Defines the event.
///
@@ -99,6 +105,12 @@ namespace Avalonia.Input
"KeyUp",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty TabIndexProperty =
+ KeyboardNavigation.TabIndexProperty.AddOwner();
+
///
/// Defines the event.
///
@@ -426,6 +438,15 @@ namespace Avalonia.Input
internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); }
}
+ ///
+ /// Gets or sets a value that indicates whether the control is included in tab navigation.
+ ///
+ public bool IsTabStop
+ {
+ get => GetValue(IsTabStopProperty);
+ set => SetValue(IsTabStopProperty, value);
+ }
+
///
public bool IsEffectivelyEnabled
{
@@ -437,6 +458,16 @@ namespace Avalonia.Input
}
}
+ ///
+ /// Gets or sets a value that determines the order in which elements receive focus when the
+ /// user navigates through controls by pressing the Tab key.
+ ///
+ public int TabIndex
+ {
+ get => GetValue(TabIndexProperty);
+ set => SetValue(TabIndexProperty, value);
+ }
+
public List KeyBindings { get; } = new List();
///
diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs
index 6ef3c4fd60..a25aed6811 100644
--- a/src/Avalonia.Input/KeyboardNavigation.cs
+++ b/src/Avalonia.Input/KeyboardNavigation.cs
@@ -5,6 +5,15 @@ namespace Avalonia.Input
///
public static class KeyboardNavigation
{
+ ///
+ /// Defines the TabIndex attached property.
+ ///
+ public static readonly AttachedProperty TabIndexProperty =
+ AvaloniaProperty.RegisterAttached(
+ "TabIndex",
+ typeof(KeyboardNavigation),
+ int.MaxValue);
+
///
/// Defines the TabNavigation attached property.
///
@@ -42,6 +51,26 @@ namespace Avalonia.Input
typeof(KeyboardNavigation),
true);
+ ///
+ /// Gets the for an element.
+ ///
+ /// The container.
+ /// The for the container.
+ public static int GetTabIndex(IInputElement element)
+ {
+ return ((IAvaloniaObject)element).GetValue(TabIndexProperty);
+ }
+
+ ///
+ /// Sets the for an element.
+ ///
+ /// The element.
+ /// The tab index.
+ public static void SetTabIndex(IInputElement element, int value)
+ {
+ ((IAvaloniaObject)element).SetValue(TabIndexProperty, value);
+ }
+
///
/// Gets the for a container.
///
@@ -83,7 +112,7 @@ namespace Avalonia.Input
}
///
- /// Sets the for a container.
+ /// Sets the for an element.
///
/// The container.
/// Value indicating whether the container is a tab stop.
@@ -93,7 +122,7 @@ namespace Avalonia.Input
}
///
- /// Gets the for a container.
+ /// Gets the for an element.
///
/// The container.
/// Whether the container is a tab stop.
diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs
index dbefe63789..6493777105 100644
--- a/src/Avalonia.Input/KeyboardNavigationHandler.cs
+++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Input.Navigation;
using Avalonia.VisualTree;
@@ -48,39 +49,24 @@ namespace Avalonia.Input
{
element = element ?? throw new ArgumentNullException(nameof(element));
- var customHandler = element.GetSelfAndVisualAncestors()
- .OfType()
- .FirstOrDefault();
+ // If there's a custom keyboard navigation handler as an ancestor, use that.
+ var custom = element.FindAncestorOfType(true);
+ if (custom is object && HandlePreCustomNavigation(custom, element, direction, out var ce))
+ return ce;
- if (customHandler != null)
+ var result = direction switch
{
- var (handled, next) = customHandler.GetNext(element, direction);
+ NavigationDirection.Next => TabNavigation.GetNextTab(element, false),
+ NavigationDirection.Previous => TabNavigation.GetPrevTab(element, null, false),
+ _ => throw new NotSupportedException(),
+ };
- if (handled)
- {
- if (next != null)
- {
- return next;
- }
- else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
- {
- return TabNavigation.GetNextInTabOrder((IInputElement)customHandler, direction, true);
- }
- else
- {
- return null;
- }
- }
- }
+ // If there wasn't a custom navigation handler as an ancestor of the current element,
+ // but there is one as an ancestor of the new element, use that.
+ if (custom is null && HandlePostCustomNavigation(element, result, direction, out ce))
+ return ce;
- if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
- {
- return TabNavigation.GetNextInTabOrder(element, direction);
- }
- else
- {
- throw new NotSupportedException();
- }
+ return result;
}
///
@@ -90,7 +76,7 @@ namespace Avalonia.Input
/// The direction to move.
/// Any key modifiers active at the time of focus.
public void Move(
- IInputElement element,
+ IInputElement element,
NavigationDirection direction,
KeyModifiers keyModifiers = KeyModifiers.None)
{
@@ -124,5 +110,70 @@ namespace Avalonia.Input
e.Handled = true;
}
}
+
+ private static bool HandlePreCustomNavigation(
+ ICustomKeyboardNavigation customHandler,
+ IInputElement element,
+ NavigationDirection direction,
+ [NotNullWhen(true)] out IInputElement? result)
+ {
+ if (customHandler != null)
+ {
+ var (handled, next) = customHandler.GetNext(element, direction);
+
+ if (handled)
+ {
+ if (next != null)
+ {
+ result = next;
+ return true;
+ }
+ else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
+ {
+ var r = direction switch
+ {
+ NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler),
+ NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler),
+ _ => throw new NotSupportedException(),
+ };
+
+ if (r is object)
+ {
+ result = r;
+ return true;
+ }
+ }
+ }
+ }
+
+ result = null;
+ return false;
+ }
+
+ private static bool HandlePostCustomNavigation(
+ IInputElement element,
+ IInputElement? newElement,
+ NavigationDirection direction,
+ [NotNullWhen(true)] out IInputElement? result)
+ {
+ if (newElement is object)
+ {
+ var customHandler = newElement.FindAncestorOfType(true);
+
+ if (customHandler is object)
+ {
+ var (handled, next) = customHandler.GetNext(element, direction);
+
+ if (handled && next is object)
+ {
+ result = next;
+ return true;
+ }
+ }
+ }
+
+ result = null;
+ return false;
+ }
}
}
diff --git a/src/Avalonia.Input/KeyboardNavigationMode.cs b/src/Avalonia.Input/KeyboardNavigationMode.cs
index 41e778bf49..e01ebf0330 100644
--- a/src/Avalonia.Input/KeyboardNavigationMode.cs
+++ b/src/Avalonia.Input/KeyboardNavigationMode.cs
@@ -36,5 +36,10 @@ namespace Avalonia.Input
/// The container's children will not be focused when using the tab key.
///
None,
+
+ ///
+ /// TabIndexes are considered on local subtree only inside this container
+ ///
+ Local,
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs
index 6f6d68940b..ed7df67bf2 100644
--- a/src/Avalonia.Input/Navigation/TabNavigation.cs
+++ b/src/Avalonia.Input/Navigation/TabNavigation.cs
@@ -10,277 +10,663 @@ namespace Avalonia.Input.Navigation
///
internal static class TabNavigation
{
- ///
- /// Gets the next control in the specified tab direction.
- ///
- /// The element.
- /// The tab direction. Must be Next or Previous.
- ///
- /// If true will not descend into to find next control.
- ///
- ///
- /// The next element in the specified direction, or null if
- /// was the last in the requested direction.
- ///
- public static IInputElement? GetNextInTabOrder(
- IInputElement element,
- NavigationDirection direction,
- bool outsideElement = false)
+ public static IInputElement? GetNextTab(IInputElement e, bool goDownOnly)
{
- element = element ?? throw new ArgumentNullException(nameof(element));
-
- if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous)
- {
- throw new ArgumentException("Invalid direction: must be Next or Previous.");
- }
+ return GetNextTab(e, GetGroupParent(e), goDownOnly);
+ }
- var container = element.GetVisualParent();
+ public static IInputElement? GetNextTab(IInputElement? e, IInputElement container, bool goDownOnly)
+ {
+ var tabbingType = GetKeyNavigationMode(container);
- if (container != null)
+ if (e == null)
{
- var mode = KeyboardNavigation.GetTabNavigation((InputElement)container);
+ if (IsTabStop(container))
+ return container;
- switch (mode)
+ // Using ActiveElement if set
+ var activeElement = GetActiveElement(container);
+ if (activeElement != null)
+ return GetNextTab(null, activeElement, true);
+ }
+ else
+ {
+ if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None)
{
- case KeyboardNavigationMode.Continue:
- return GetNextInContainer(element, container, direction, outsideElement) ??
- GetFirstInNextContainer(element, element, direction);
- case KeyboardNavigationMode.Cycle:
- return GetNextInContainer(element, container, direction, outsideElement) ??
- GetFocusableDescendant(container, direction);
- case KeyboardNavigationMode.Contained:
- return GetNextInContainer(element, container, direction, outsideElement);
- default:
- return GetFirstInNextContainer(element, container, direction);
+ if (container != e)
+ {
+ if (goDownOnly)
+ return null;
+ var parentContainer = GetGroupParent(container);
+ return GetNextTab(container, parentContainer, goDownOnly);
+ }
}
}
- else
+
+ // All groups
+ IInputElement? loopStartElement = null;
+ var nextTabElement = e;
+ var currentTabbingType = tabbingType;
+
+ // Search down inside the container
+ while ((nextTabElement = GetNextTabInGroup(nextTabElement, container, currentTabbingType)) != null)
{
- return GetFocusableDescendants(element, direction).FirstOrDefault();
+ // Avoid the endless loop here for Cycle groups
+ if (loopStartElement == nextTabElement)
+ break;
+ if (loopStartElement == null)
+ loopStartElement = nextTabElement;
+
+ var firstTabElementInside = GetNextTab(null, nextTabElement, true);
+ if (firstTabElementInside != null)
+ return firstTabElementInside;
+
+ // If we want to continue searching inside the Once groups, we should change the navigation mode
+ if (currentTabbingType == KeyboardNavigationMode.Once)
+ currentTabbingType = KeyboardNavigationMode.Contained;
}
+
+ // If there is no next element in the group (nextTabElement == null)
+
+ // Search up in the tree if allowed
+ // consider: Use original tabbingType instead of currentTabbingType
+ if (!goDownOnly && currentTabbingType != KeyboardNavigationMode.Contained && GetParent(container) != null)
+ {
+ return GetNextTab(container, GetGroupParent(container), false);
+ }
+
+ return null;
}
- ///
- /// Gets the first or last focusable descendant of the specified element.
- ///
- /// The element.
- /// The direction to search.
- /// The element or null if not found.##
- private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction)
+ public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e)
{
- return direction == NavigationDirection.Next ?
- GetFocusableDescendants(container, direction).FirstOrDefault() :
- GetFocusableDescendants(container, direction).LastOrDefault();
+ if (e is IInputElement container)
+ {
+ var last = GetLastInTree(container);
+
+ if (last is object)
+ return GetNextTab(last, false);
+ }
+
+ return null;
}
- ///
- /// Gets the focusable descendants of the specified element.
- ///
- /// The element.
- /// The tab direction. Must be Next or Previous.
- /// The element's focusable descendants.
- private static IEnumerable GetFocusableDescendants(IInputElement element,
- NavigationDirection direction)
+ public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly)
{
- var mode = KeyboardNavigation.GetTabNavigation((InputElement)element);
+ if (e is null && container is null)
+ throw new InvalidOperationException("Either 'e' or 'container' must be non-null.");
- if (mode == KeyboardNavigationMode.None)
- {
- yield break;
- }
+ if (container is null)
+ container = GetGroupParent(e!);
- var children = element.GetVisualChildren().OfType();
+ KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container);
- if (mode == KeyboardNavigationMode.Once)
+ if (e == null)
{
- var active = KeyboardNavigation.GetTabOnceActiveElement((InputElement)element);
-
- if (active != null)
+ // Using ActiveElement if set
+ var activeElement = GetActiveElement(container);
+ if (activeElement != null)
+ return GetPrevTab(null, activeElement, true);
+ else
{
- yield return active;
- yield break;
+ // If we Shift+Tab on a container with KeyboardNavigationMode=Once, and ActiveElement is null
+ // then we want to go to the first item (not last) within the container
+ if (tabbingType == KeyboardNavigationMode.Once)
+ {
+ var firstTabElement = GetNextTabInGroup(null, container, tabbingType);
+ if (firstTabElement == null)
+ {
+ if (IsTabStop(container))
+ return container;
+ if (goDownOnly)
+ return null;
+
+ return GetPrevTab(container, null, false);
+ }
+ else
+ {
+ return GetPrevTab(null, firstTabElement, true);
+ }
+ }
}
- else
+ }
+ else
+ {
+ if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None)
{
- children = children.Take(1);
+ if (goDownOnly || container == e)
+ return null;
+
+ // FocusedElement should not be e otherwise we will delegate focus to the same element
+ if (IsTabStop(container))
+ return container;
+
+ return GetPrevTab(container, null, false);
}
}
- foreach (var child in children)
+ // All groups (except Once) - continue
+ IInputElement? loopStartElement = null;
+ IInputElement? nextTabElement = e;
+
+ // Look for element with the same TabIndex before the current element
+ while ((nextTabElement = GetPrevTabInGroup(nextTabElement, container, tabbingType)) != null)
{
- var customNext = GetCustomNext(child, direction);
+ if (nextTabElement == container && tabbingType == KeyboardNavigationMode.Local)
+ break;
+
+ // At this point nextTabElement is TabStop or TabGroup
+ // In case it is a TabStop only return the element
+ if (IsTabStop(nextTabElement) && !IsGroup(nextTabElement))
+ return nextTabElement;
+
+ // Avoid the endless loop here
+ if (loopStartElement == nextTabElement)
+ break;
+ if (loopStartElement == null)
+ loopStartElement = nextTabElement;
+
+ // At this point nextTabElement is TabGroup
+ var lastTabElementInside = GetPrevTab(null, nextTabElement, true);
+ if (lastTabElementInside != null)
+ return lastTabElementInside;
+ }
- if (customNext.handled)
- {
- yield return customNext.next!;
- }
- else
+ if (tabbingType == KeyboardNavigationMode.Contained)
+ return null;
+
+ if (e != container && IsTabStop(container))
+ return container;
+
+ // If end of the subtree is reached or there no other elements above
+ if (!goDownOnly && GetParent(container) != null)
+ {
+ return GetPrevTab(container, null, false);
+ }
+
+ return null;
+ }
+
+ public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e)
+ {
+ if (e is IInputElement container)
+ {
+ var first = GetFirstChild(container);
+
+ if (first is object)
+ return GetPrevTab(first, null, false);
+ }
+
+ return null;
+ }
+
+ private static IInputElement? FocusedElement(IInputElement e)
+ {
+ var iie = e;
+ // Focus delegation is enabled only if keyboard focus is outside the container
+ if (iie != null && !iie.IsKeyboardFocusWithin)
+ {
+ var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e);
+ if (focusedElement != null)
{
- if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child))
+ if (!IsFocusScope(e))
{
- yield return child;
+ // Verify if focusedElement is a visual descendant of e
+ if (focusedElement is IVisual visualFocusedElement &&
+ visualFocusedElement != e &&
+ e.IsVisualAncestorOf(visualFocusedElement))
+ {
+ return focusedElement;
+ }
}
+ }
+ }
+
+ return null;
+ }
+
+ private static IInputElement? GetFirstChild(IInputElement e)
+ {
+ // If the element has a FocusedElement it should be its first child
+ if (FocusedElement(e) is IInputElement focusedElement)
+ return focusedElement;
- if (child.CanFocusDescendants())
+ // Return the first visible element.
+ var uiElement = e as InputElement;
+
+ if (uiElement is null || uiElement.IsVisible)
+ {
+ if (e is IVisual elementAsVisual)
+ {
+ var children = elementAsVisual.VisualChildren;
+ var count = children.Count;
+
+ for (int i = 0; i < count; i++)
{
- foreach (var descendant in GetFocusableDescendants(child, direction))
+ if (children[i] is InputElement ie)
{
- if (KeyboardNavigation.GetIsTabStop((InputElement)descendant))
+ if (ie.IsVisible)
+ return ie;
+ else
{
- yield return descendant;
+ var firstChild = GetFirstChild(ie);
+ if (firstChild != null)
+ return firstChild;
}
}
}
}
}
+
+ return null;
}
- ///
- /// Gets the next item that should be focused in the specified container.
- ///
- /// The starting element/
- /// The container.
- /// The direction.
- ///
- /// If true will not descend into to find next control.
- ///
- /// The next element, or null if the element is the last.
- private static IInputElement? GetNextInContainer(
- IInputElement element,
- IInputElement container,
- NavigationDirection direction,
- bool outsideElement)
+ private static IInputElement? GetLastChild(IInputElement e)
{
- IInputElement? e = element;
+ // If the element has a FocusedElement it should be its last child
+ if (FocusedElement(e) is IInputElement focusedElement)
+ return focusedElement;
- if (direction == NavigationDirection.Next && !outsideElement)
- {
- var descendant = GetFocusableDescendants(element, direction).FirstOrDefault();
+ // Return the last visible element.
+ var uiElement = e as InputElement;
- if (descendant != null)
- {
- return descendant;
- }
- }
-
- if (container != null)
+ if (uiElement == null || uiElement.IsVisible)
{
- var navigable = container as INavigableContainer;
+ var elementAsVisual = e as IVisual;
- // TODO: Do a spatial search here if the container doesn't implement
- // INavigableContainer.
- if (navigable != null)
+ if (elementAsVisual != null)
{
- while (e != null)
- {
- e = navigable.GetControl(direction, e, false);
+ var children = elementAsVisual.VisualChildren;
+ var count = children.Count;
- if (e != null &&
- e.CanFocus() &&
- KeyboardNavigation.GetIsTabStop((InputElement)e))
+ for (int i = count - 1; i >= 0; i--)
+ {
+ if (children[i] is InputElement ie)
{
- break;
+ if (ie.IsVisible)
+ return ie;
+ else
+ {
+ var lastChild = GetLastChild(ie);
+ if (lastChild != null)
+ return lastChild;
+ }
}
}
}
- else
+ }
+
+ return null;
+ }
+
+ private static IInputElement? GetFirstTabInGroup(IInputElement container)
+ {
+ IInputElement? firstTabElement = null;
+ int minIndexFirstTab = int.MinValue;
+
+ var currElement = container;
+ while ((currElement = GetNextInTree(currElement, container)) != null)
+ {
+ if (IsTabStopOrGroup(currElement))
{
- // TODO: Do a spatial search here if the container doesn't implement
- // INavigableContainer.
- e = null;
+ int currPriority = KeyboardNavigation.GetTabIndex(currElement);
+
+ if (currPriority < minIndexFirstTab || firstTabElement == null)
+ {
+ minIndexFirstTab = currPriority;
+ firstTabElement = currElement;
+ }
}
+ }
+ return firstTabElement;
+ }
+
+ private static IInputElement? GetLastInTree(IInputElement container)
+ {
+ IInputElement? result;
+ IInputElement? c = container;
+
+ do
+ {
+ result = c;
+ c = GetLastChild(c);
+ } while (c != null && !IsGroup(c));
+
+ if (c != null)
+ return c;
+
+ return result;
+ }
- if (e != null && direction == NavigationDirection.Previous)
+ private static IInputElement? GetLastTabInGroup(IInputElement container)
+ {
+ IInputElement? lastTabElement = null;
+ int maxIndexFirstTab = int.MaxValue;
+ var currElement = GetLastInTree(container);
+ while (currElement != null && currElement != container)
+ {
+ if (IsTabStopOrGroup(currElement))
{
- var descendant = GetFocusableDescendants(e, direction).LastOrDefault();
+ int currPriority = KeyboardNavigation.GetTabIndex(currElement);
- if (descendant != null)
+ if (currPriority > maxIndexFirstTab || lastTabElement == null)
{
- return descendant;
+ maxIndexFirstTab = currPriority;
+ lastTabElement = currElement;
}
}
+ currElement = GetPreviousInTree(currElement, container);
+ }
+ return lastTabElement;
+ }
+
+ private static IInputElement? GetNextInTree(IInputElement e, IInputElement container)
+ {
+ IInputElement? result = null;
+
+ if (e == container || !IsGroup(e))
+ result = GetFirstChild(e);
+
+ if (result != null || e == container)
+ return result;
+
+ IInputElement? parent = e;
+ do
+ {
+ var sibling = GetNextSibling(parent);
+ if (sibling != null)
+ return sibling;
- return e;
+ parent = GetParent(parent);
+ } while (parent != null && parent != container);
+
+ return null;
+ }
+
+ private static IInputElement? GetNextSibling(IInputElement e)
+ {
+ if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual)
+ {
+ var children = parentAsVisual.VisualChildren;
+ var count = children.Count;
+ var i = 0;
+
+ //go till itself
+ for (; i < count; i++)
+ {
+ var vchild = children[i];
+ if (vchild == elementAsVisual)
+ break;
+ }
+ i++;
+ //search ahead
+ for (; i < count; i++)
+ {
+ var visual = children[i];
+ if (visual is IInputElement ie)
+ return ie;
+ }
}
return null;
}
- ///
- /// Gets the first item that should be focused in the next container.
- ///
- /// The element being navigated away from.
- /// The container.
- /// The direction of the search.
- /// The first element, or null if there are no more elements.
- private static IInputElement? GetFirstInNextContainer(
- IInputElement element,
- IInputElement container,
- NavigationDirection direction)
+ private static IInputElement? GetNextTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType)
{
- var parent = container.GetVisualParent();
- IInputElement? next = null;
+ // None groups: Tab navigation is not supported
+ if (tabbingType == KeyboardNavigationMode.None)
+ return null;
- if (parent != null)
+ // e == null or e == container -> return the first TabStopOrGroup
+ if (e == null || e == container)
{
- if (direction == NavigationDirection.Previous &&
- parent.CanFocus() &&
- KeyboardNavigation.GetIsTabStop((InputElement) parent))
+ return GetFirstTabInGroup(container);
+ }
+
+ if (tabbingType == KeyboardNavigationMode.Once)
+ return null;
+
+ var nextTabElement = GetNextTabWithSameIndex(e, container);
+ if (nextTabElement != null)
+ return nextTabElement;
+
+ return GetNextTabWithNextIndex(e, container, tabbingType);
+ }
+
+ private static IInputElement? GetNextTabWithSameIndex(IInputElement e, IInputElement container)
+ {
+ var elementTabPriority = KeyboardNavigation.GetTabIndex(e);
+ var currElement = e;
+ while ((currElement = GetNextInTree(currElement, container)) != null)
+ {
+ if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority)
{
- return parent;
+ return currElement;
}
+ }
- var allSiblings = parent.GetVisualChildren()
- .OfType()
- .Where(FocusExtensions.CanFocusDescendants);
- var siblings = direction == NavigationDirection.Next ?
- allSiblings.SkipWhile(x => x != container).Skip(1) :
- allSiblings.TakeWhile(x => x != container).Reverse();
+ return null;
+ }
- foreach (var sibling in siblings)
+ private static IInputElement? GetNextTabWithNextIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType)
+ {
+ // Find the next min index in the tree
+ // min (index>currentTabIndex)
+ IInputElement? nextTabElement = null;
+ IInputElement? firstTabElement = null;
+ int minIndexFirstTab = int.MinValue;
+ int minIndex = int.MinValue;
+ int elementTabPriority = KeyboardNavigation.GetTabIndex(e);
+
+ IInputElement? currElement = container;
+ while ((currElement = GetNextInTree(currElement, container)) != null)
+ {
+ if (IsTabStopOrGroup(currElement))
{
- var customNext = GetCustomNext(sibling, direction);
- if (customNext.handled)
+ int currPriority = KeyboardNavigation.GetTabIndex(currElement);
+ if (currPriority > elementTabPriority)
{
- return customNext.next;
+ if (currPriority < minIndex || nextTabElement == null)
+ {
+ minIndex = currPriority;
+ nextTabElement = currElement;
+ }
}
- if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling))
+ if (currPriority < minIndexFirstTab || firstTabElement == null)
{
- return sibling;
+ minIndexFirstTab = currPriority;
+ firstTabElement = currElement;
}
+ }
+ }
- next = direction == NavigationDirection.Next ?
- GetFocusableDescendants(sibling, direction).FirstOrDefault() :
- GetFocusableDescendants(sibling, direction).LastOrDefault();
+ // Cycle groups: if not found - return first element
+ if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null)
+ nextTabElement = firstTabElement;
+
+ return nextTabElement;
+ }
- if (next != null)
+ private static IInputElement? GetPrevTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType)
+ {
+ // None groups: Tab navigation is not supported
+ if (tabbingType == KeyboardNavigationMode.None)
+ return null;
+
+ // Search the last index inside the group
+ if (e == null)
+ {
+ return GetLastTabInGroup(container);
+ }
+
+ if (tabbingType == KeyboardNavigationMode.Once)
+ return null;
+
+ if (e == container)
+ return null;
+
+ var nextTabElement = GetPrevTabWithSameIndex(e, container);
+ if (nextTabElement != null)
+ return nextTabElement;
+
+ return GetPrevTabWithPrevIndex(e, container, tabbingType);
+ }
+
+ private static IInputElement? GetPrevTabWithSameIndex(IInputElement e, IInputElement container)
+ {
+ int elementTabPriority = KeyboardNavigation.GetTabIndex(e);
+ var currElement = GetPreviousInTree(e, container);
+ while (currElement != null)
+ {
+ if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority && currElement != container)
+ {
+ return currElement;
+ }
+ currElement = GetPreviousInTree(currElement, container);
+ }
+ return null;
+ }
+
+ private static IInputElement? GetPrevTabWithPrevIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType)
+ {
+ // Find the next max index in the tree
+ // max (index maxIndex || nextTabElement == null)
+ {
+ maxIndex = currPriority;
+ nextTabElement = currElement;
+ }
+ }
+
+ if (currPriority > maxIndexFirstTab || lastTabElement == null)
{
- return next;
+ maxIndexFirstTab = currPriority;
+ lastTabElement = currElement;
}
}
- next = GetFirstInNextContainer(element, parent, direction);
+ currElement = GetPreviousInTree(currElement, container);
+ }
+
+ // Cycle groups: if not found - return first element
+ if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null)
+ nextTabElement = lastTabElement;
+
+ return nextTabElement;
+ }
+
+ private static IInputElement? GetPreviousInTree(IInputElement e, IInputElement container)
+ {
+ if (e == container)
+ return null;
+
+ var result = GetPreviousSibling(e);
+
+ if (result != null)
+ {
+ if (IsGroup(result))
+ return result;
+ else
+ return GetLastInTree(result);
}
else
+ return GetParent(e);
+ }
+
+ private static IInputElement? GetPreviousSibling(IInputElement e)
+ {
+ if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual)
{
- next = direction == NavigationDirection.Next ?
- GetFocusableDescendants(container, direction).FirstOrDefault() :
- GetFocusableDescendants(container, direction).LastOrDefault();
+ var children = parentAsVisual.VisualChildren;
+ var count = children.Count;
+ IInputElement? prev = null;
+
+ for (int i = 0; i < count; i++)
+ {
+ var vchild = children[i];
+ if (vchild == elementAsVisual)
+ break;
+ if (vchild.IsVisible == true && vchild is IInputElement ie)
+ prev = ie;
+ }
+ return prev;
}
+ return null;
+ }
- return next;
+ private static IInputElement? GetActiveElement(IInputElement e)
+ {
+ return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty);
}
- private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element,
- NavigationDirection direction)
+ private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false);
+
+ private static IInputElement GetGroupParent(IInputElement element, bool includeCurrent)
{
- if (element is ICustomKeyboardNavigation custom)
+ var result = element; // Keep the last non null element
+ var e = element;
+
+ // If we don't want to include the current element,
+ // start at the parent of the element. If the element
+ // is the root, then just return it as the group parent.
+ if (!includeCurrent)
+ {
+ result = e;
+ e = GetParent(e);
+ if (e == null)
+ return result;
+ }
+
+ while (e != null)
{
- return custom.GetNext(element, direction);
+ if (IsGroup(e))
+ return e;
+
+ result = e;
+ e = GetParent(e);
}
- return (false, null);
+ return result;
}
+
+ private static IInputElement? GetParent(IInputElement e)
+ {
+ // For Visual - go up the visual parent chain until we find Visual.
+ if (e is IVisual v)
+ return v.FindAncestorOfType();
+
+ // This will need to be implemented when we have non-visual input elements.
+ throw new NotSupportedException();
+ }
+
+ private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e)
+ {
+ return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty);
+ }
+ private static bool IsFocusScope(IInputElement e) => FocusManager.GetIsFocusScope(e) || GetParent(e) == null;
+ private static bool IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue;
+
+ private static bool IsTabStop(IInputElement e)
+ {
+ if (e is InputElement ie)
+ return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEnabled;
+ return false;
+ }
+
+ private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e);
}
}
diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs
index 60c0b36891..a7d05e416f 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatform.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs
@@ -109,11 +109,17 @@ namespace Avalonia.Native
.Bind().ToConstant(new RenderLoop())
.Bind().ToConstant(new DefaultRenderTimer(60))
.Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
- .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta))
+ .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt))
.Bind().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind().ToConstant(new AvaloniaNativeDragSource(_factory))
.Bind().ToConstant(applicationPlatform);
+ var hotkeys = AvaloniaLocator.Current.GetService();
+ hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers));
+ hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers));
+ hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers));
+ hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers));
+
if (_options.UseGpu)
{
try
diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
index 76cb7a8057..eef765e7ec 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
@@ -26,19 +26,52 @@ namespace Avalonia
}
}
+ ///
+ /// OSX backend options.
+ ///
public class AvaloniaNativePlatformOptions
{
+ ///
+ /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.
+ ///
+ ///
+ /// Avalonia has two rendering modes: Immediate and Deferred rendering.
+ /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements.
+ ///
public bool UseDeferredRendering { get; set; } = true;
+
+ ///
+ /// Determines whether to use GPU for rendering in your project. The default value is true.
+ ///
public bool UseGpu { get; set; } = true;
+
+ ///
+ /// Embeds popups to the window when set to true. The default value is false.
+ ///
public bool OverlayPopups { get; set; }
+
+ ///
+ /// This property should be used in case you want to build Avalonia OSX native part by yourself
+ /// and make your Avalonia app run with it. The default value is null.
+ ///
public string AvaloniaNativeLibraryPath { get; set; }
}
// ReSharper disable once InconsistentNaming
+ ///
+ /// OSX front-end options.
+ ///
public class MacOSPlatformOptions
{
+ ///
+ /// Determines whether to show your application in the dock when it runs. The default value is true.
+ ///
public bool ShowInDock { get; set; } = true;
-
+
+ ///
+ /// By default, Avalonia adds items like Quit, Hide to the OSX Application Menu.
+ /// You can prevent Avalonia from adding those items to the OSX Application Menu with this property. The default value is false.
+ ///
public bool DisableDefaultApplicationMenuItems { get; set; }
}
}
diff --git a/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs b/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs
index b63c49bd5a..01212b238d 100644
--- a/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs
+++ b/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs
@@ -1,8 +1,11 @@
+using System;
+
namespace Avalonia.VisualTree
{
///
/// Interface for controls that host their own separate visual tree, such as popups.
///
+ [Obsolete]
public interface IVisualTreeHost
{
///
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index a57bdbdf87..3a919c8814 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -162,14 +162,49 @@ namespace Avalonia.X11
namespace Avalonia
{
-
+ ///
+ /// Platform-specific options which apply to Linux.
+ ///
public class X11PlatformOptions
{
+ ///
+ /// Enables native Linux EGL when set to true. The default value is false.
+ ///
public bool UseEGL { get; set; }
+
+ ///
+ /// Determines whether to use GPU for rendering in your project. The default value is true.
+ ///
public bool UseGpu { get; set; } = true;
+
+ ///
+ /// Embeds popups to the window when set to true. The default value is false.
+ ///
public bool OverlayPopups { get; set; }
+
+ ///
+ /// Enables global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
+ /// The default value is false.
+ ///
public bool UseDBusMenu { get; set; }
+
+ ///
+ /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.
+ ///
+ ///
+ /// Avalonia has two rendering modes: Immediate and Deferred rendering.
+ /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements.
+ ///
public bool UseDeferredRendering { get; set; } = true;
+
+ ///
+ /// Determines whether to use IME.
+ /// IME would be enabled by default if the current user input language is one of the following: Mandarin, Japanese, Vietnamese or Korean.
+ ///
+ ///
+ /// Input method editor is a component that enables users to generate characters not natively available
+ /// on their input devices by using sequences of characters or mouse operations that are natively available on their input devices.
+ ///
public bool? EnableIme { get; set; }
public IList GlProfiles { get; set; } = new List
@@ -190,7 +225,14 @@ namespace Avalonia
"llvmpipe"
};
public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication";
- public bool? EnableMultiTouch { get; set; }
+
+ ///
+ /// Enables multitouch support. The default value is true.
+ ///
+ ///
+ /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time.
+ ///
+ public bool? EnableMultiTouch { get; set; } = true;
}
public static class AvaloniaX11PlatformExtensions
{
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index 10a6db0b57..a881c45cd0 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -35,15 +35,46 @@ namespace Avalonia
}
}
+ ///
+ /// Platform-specific options which apply to Windows.
+ ///
public class Win32PlatformOptions
{
+ ///
+ /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.
+ ///
+ ///
+ /// Avalonia has two rendering modes: Immediate and Deferred rendering.
+ /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements.
+ ///
public bool UseDeferredRendering { get; set; } = true;
-
+
+ ///
+ /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false.
+ ///
+ ///
+ /// GPU rendering will not be enabled if this is set to false.
+ ///
public bool? AllowEglInitialization { get; set; }
-
- public bool? EnableMultitouch { get; set; }
+
+ ///
+ /// Enables multitouch support. The default value is true.
+ ///
+ ///
+ /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time.
+ ///
+ public bool? EnableMultitouch { get; set; } = true;
+
+ ///
+ /// Embeds popups to the window when set to true. The default value is false.
+ ///
public bool OverlayPopups { get; set; }
+
+ ///
+ /// Avalonia would try to use native Widows OpenGL when set to true. The default value is false.
+ ///
public bool UseWgl { get; set; }
+
public IList WglProfiles { get; set; } = new List
{
new GlVersion(GlProfileType.OpenGL, 4, 0),
diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs
index f72d6ba9c9..f9c85ee4ca 100644
--- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs
+++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs
@@ -95,6 +95,7 @@ namespace Avalonia.Input.UnitTests
var root = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
target,
@@ -125,6 +126,7 @@ namespace Avalonia.Input.UnitTests
var root = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(current = new Button { Content = "Outside" }),
@@ -137,6 +139,36 @@ namespace Avalonia.Input.UnitTests
Assert.Same(next, result);
}
+ [Fact]
+ public void ShiftTab_Should_Navigate_Outside_When_Null_Returned_As_Next()
+ {
+ Button current;
+ Button next;
+ var target = new CustomNavigatingStackPanel
+ {
+ Children =
+ {
+ new Button { Content = "Button 1" },
+ (current = new Button { Content = "Button 2" }),
+ new Button { Content = "Button 3" },
+ },
+ };
+
+ var root = new StackPanel
+ {
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
+ Children =
+ {
+ target,
+ (next = new Button { Content = "Outside" }),
+ }
+ };
+
+ var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
+
+ Assert.Same(next, result);
+ }
+
[Fact]
public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next()
{
@@ -154,6 +186,7 @@ namespace Avalonia.Input.UnitTests
var root = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
target,
diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
index 1efbbed2e8..edcbf75a1d 100644
--- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
+++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using Avalonia.Controls;
using Xunit;
@@ -13,6 +14,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -49,6 +51,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -85,6 +88,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -122,6 +126,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -165,6 +170,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -193,6 +199,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -222,6 +229,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -263,6 +271,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(next = new Button { Name = "Button1" }),
@@ -282,6 +291,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -324,6 +334,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -361,6 +372,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -398,6 +410,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -434,6 +447,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -471,6 +485,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -509,6 +524,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(container = new StackPanel
@@ -548,6 +564,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -586,6 +603,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(container = new StackPanel
@@ -625,6 +643,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -661,6 +680,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -697,6 +717,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -725,6 +746,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -767,6 +789,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -828,6 +851,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -865,6 +889,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -902,6 +927,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -938,6 +964,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -975,6 +1002,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -1013,6 +1041,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(container = new StackPanel
@@ -1052,6 +1081,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -1103,5 +1133,97 @@ namespace Avalonia.Input.UnitTests
Assert.Null(result);
}
+
+ [Fact]
+ public void Respects_TabIndex_Moving_Forwards()
+ {
+ Button start;
+
+ var top = new StackPanel
+ {
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
+ Children =
+ {
+ new StackPanel
+ {
+ Children =
+ {
+ new Button { Name = "Button1", TabIndex = 5 },
+ (start = new Button { Name = "Button2", TabIndex = 2 }),
+ new Button { Name = "Button3", TabIndex = 1 },
+ }
+ },
+ new StackPanel
+ {
+ Children =
+ {
+ new Button { Name = "Button4", TabIndex = 3 },
+ new Button { Name = "Button5", TabIndex = 6 },
+ new Button { Name = "Button6", TabIndex = 4 },
+ }
+ },
+ }
+ };
+
+ var result = new List();
+ var current = (IInputElement)start;
+
+ do
+ {
+ result.Add(((IControl)current).Name);
+ current = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
+ } while (current is object && current != start);
+
+ Assert.Equal(new[]
+ {
+ "Button2", "Button4", "Button6", "Button1", "Button5", "Button3"
+ }, result);
+ }
+
+ [Fact]
+ public void Respects_TabIndex_Moving_Backwards()
+ {
+ Button start;
+
+ var top = new StackPanel
+ {
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
+ Children =
+ {
+ new StackPanel
+ {
+ Children =
+ {
+ new Button { Name = "Button1", TabIndex = 5 },
+ (start = new Button { Name = "Button2", TabIndex = 2 }),
+ new Button { Name = "Button3", TabIndex = 1 },
+ }
+ },
+ new StackPanel
+ {
+ Children =
+ {
+ new Button { Name = "Button4", TabIndex = 3 },
+ new Button { Name = "Button5", TabIndex = 6 },
+ new Button { Name = "Button6", TabIndex = 4 },
+ }
+ },
+ }
+ };
+
+ var result = new List();
+ var current = (IInputElement)start;
+
+ do
+ {
+ result.Add(((IControl)current).Name);
+ current = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
+ } while (current is object && current != start);
+
+ Assert.Equal(new[]
+ {
+ "Button2", "Button3", "Button5", "Button1", "Button6", "Button4"
+ }, result);
+ }
}
}
diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs
index b69bf990d9..4601dd7e5b 100644
--- a/tests/Avalonia.UnitTests/TestRoot.cs
+++ b/tests/Avalonia.UnitTests/TestRoot.cs
@@ -21,6 +21,7 @@ namespace Avalonia.UnitTests
Renderer = Mock.Of();
LayoutManager = new LayoutManager(this);
IsVisible = true;
+ KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
}
public TestRoot(IControl child)