Modify Behavior to make it more robust
MODIFY
WalletWasabi.Fluent/Behaviors/ShowAttachedFlyoutWhenFocusedBehavior.cs
WalletWasabi.Fluent/Behaviors/ShowAttachedFlyoutWhenFocusedBehavior.cs
+using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia;
@@ -15,15 +16,18 @@ namespace WalletWasabi.Fluent.Behaviors;
public class ShowAttachedFlyoutWhenFocusedBehavior : AttachedToVisualTreeBehavior<Control>
{
- public static readonly StyledProperty<bool> IsFlyoutOpenProperty =
- AvaloniaProperty.Register<ShowAttachedFlyoutWhenFocusedBehavior, bool>(nameof(IsFlyoutOpen));
+ public static readonly DirectProperty<ShowAttachedFlyoutWhenFocusedBehavior, bool> IsFlyoutOpenProperty =
+ AvaloniaProperty.RegisterDirect<ShowAttachedFlyoutWhenFocusedBehavior, bool>(
+ "IsFlyoutOpen",
+ o => o.IsFlyoutOpen,
+ (o, v) => o.IsFlyoutOpen = v);
- private FlyoutController? _flyoutController;
+ private bool _isFlyoutOpen;
public bool IsFlyoutOpen
{
- get => GetValue(IsFlyoutOpenProperty);
- set => SetValue(IsFlyoutOpenProperty, value);
+ get => _isFlyoutOpen;
+ set => SetAndRaise(IsFlyoutOpenProperty, ref _isFlyoutOpen, value);
}
protected override void OnAttachedToVisualTree(CompositeDisposable disposable)
@@ -33,104 +37,97 @@ public class ShowAttachedFlyoutWhenFocusedBehavior : AttachedToVisualTreeBehavio
return;
}
- var flyoutBase = FlyoutBase.GetAttachedFlyout(AssociatedObject);
- if (flyoutBase is null)
+ if (AssociatedObject.GetVisualRoot() is not Control visualRoot)
{
return;
}
- if (AssociatedObject.GetVisualRoot() is not Control visualRoot)
+ var flyoutBase = FlyoutBase.GetAttachedFlyout(AssociatedObject);
+ if (flyoutBase is null)
{
return;
}
- _flyoutController = new FlyoutController(flyoutBase, AssociatedObject)
- .DisposeWith(disposable);
-
- DescendantPressed(visualRoot)
- .Select(descendant => AssociatedObject.IsVisualAncestorOf(descendant))
- .Do(
- isAncestor =>
- {
- _flyoutController.SetIsOpen(isAncestor);
- IsFlyoutOpen = isAncestor;
- })
- .Subscribe()
- .DisposeWith(disposable);
-
- Observable.FromEventPattern(AssociatedObject, nameof(InputElement.GotFocus))
- .Do(
- _ =>
- {
- _flyoutController.SetIsOpen(true);
- IsFlyoutOpen = true;
- })
- .Subscribe()
- .DisposeWith(disposable);
-
- Observable.FromEventPattern(visualRoot, nameof(Window.Activated))
- .Do(
- _ =>
- {
- if (AssociatedObject.IsFocused)
- {
- _flyoutController.SetIsOpen(true);
- }
- })
- .Subscribe()
- .DisposeWith(disposable);
+ var controller = new FlyoutShowController(AssociatedObject, flyoutBase).DisposeWith(disposable);
- Observable.FromEventPattern(AssociatedObject, nameof(InputElement.LostFocus))
- .Where(_ => !IsFocusInside(flyoutBase))
- .Do(
- _ =>
- {
- _flyoutController.SetIsOpen(false);
- IsFlyoutOpen = false;
- })
- .Subscribe()
- .DisposeWith(disposable);
+ FocusBasedFlyoutOpener(AssociatedObject, flyoutBase, controller).DisposeWith(disposable);
+ IsOpenPropertySynchronizer(controller).DisposeWith(disposable);
- this.GetObservable(IsFlyoutOpenProperty)
- .Do(b => _flyoutController.SetIsOpen(b))
- .Subscribe()
- .DisposeWith(disposable);
+ // EDGE CASES
+ // Edge case when the Visual Root becomes active and the Associated object is focused.
+ ActivateOpener(AssociatedObject, visualRoot, controller).DisposeWith(disposable);
+ DeactivateCloser(visualRoot, controller).DisposeWith(disposable);
// This is a workaround for the case when the user switches theme. The same behavior is detached and re-attached on theme changes.
// If you don't close it, the Flyout will show in an incorrect position. Maybe bug in Avalonia?
if (IsFlyoutOpen)
{
- _flyoutController.SetIsOpen(false);
+ controller.SetIsOpen(false);
}
}
- private static IObservable<Visual> DescendantPressed(IInteractive interactive)
+ private static IDisposable DeactivateCloser(Control visualRoot, FlyoutShowController controller)
{
- return interactive
- .OnEvent(InputElement.PointerPressedEvent, RoutingStrategies.Tunnel)
- .Select(x => x.EventArgs.Source)
- .WhereNotNull()
- .OfType<Visual>()
- .Where(x => x is not LightDismissOverlayLayer);
+ return Observable.FromEventPattern(visualRoot, nameof(Window.Deactivated))
+ .Do(_ => controller.SetIsOpen(false))
+ .Subscribe();
}
- private static bool IsFocusInside(IPopupHostProvider popupHostProvider)
+ private static IDisposable ActivateOpener(
+ IInputElement associatedObject,
+ Control visualRoot,
+ FlyoutShowController controller)
{
- var focusManager = FocusManager.Instance;
-
- if (focusManager?.Current is null)
+ return Observable.FromEventPattern(visualRoot, nameof(Window.Activated))
+ .Do(
+ _ =>
+ {
+ if (associatedObject.IsFocused)
{
- return false;
+ controller.SetIsOpen(true);
+ }
+ })
+ .Subscribe();
}
- var popupPresenter = popupHostProvider.PopupHost?.Presenter;
-
- if (popupPresenter is null)
+ private IDisposable IsOpenPropertySynchronizer(FlyoutShowController controller)
{
- return false;
+ return this.WhenAnyValue(x => x.IsFlyoutOpen)
+ .Do(controller.SetIsOpen)
+ .Subscribe();
}
- var currentlyFocused = focusManager.Current;
- return popupPresenter.IsVisualAncestorOf(currentlyFocused);
+ private IDisposable FocusBasedFlyoutOpener(
+ IInteractive associatedObject,
+ FlyoutBase flyoutBase,
+ FlyoutShowController controller)
+ {
+ var currentPopupHost = Observable
+ .FromEventPattern(flyoutBase, nameof(flyoutBase.Opened))
+ .Select(_ => ((IPopupHostProvider) flyoutBase).PopupHost?.Presenter)
+ .WhereNotNull();
+
+ var associatedGotFocus = associatedObject.OnEvent(InputElement.GotFocusEvent).ToSignal();
+ var associatedLostFocus = associatedObject.OnEvent(InputElement.LostFocusEvent).ToSignal();
+ var popupGotFocus = currentPopupHost.Select(x => x.OnEvent(InputElement.GotFocusEvent)).Switch().ToSignal();
+ var popupLostFocus = currentPopupHost.Select(x => x.OnEvent(InputElement.LostFocusEvent)).Switch().ToSignal();
+
+ var isFocused = associatedGotFocus
+ .Merge(popupGotFocus).Select(_ => true)
+ .Merge(associatedLostFocus.Merge(popupLostFocus).Select(_ => false));
+
+ return isFocused
+ .Buffer(TimeSpan.FromSeconds(0.1))
+ .Where(focusedList => focusedList.Any())
+ .Select(focusedList => focusedList.Any(focused => focused))
+ .DistinctUntilChanged()
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Do(
+ isOpen =>
+ {
+ controller.SetIsOpen(isOpen);
+ IsFlyoutOpen = isOpen;
+ })
+ .Subscribe();
}
}

MODIFY
WalletWasabi.Fluent/Behaviors/TextBoxAutoSelectTextBehavior.cs
WalletWasabi.Fluent/Behaviors/TextBoxAutoSelectTextBehavior.cs
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls;
+using Avalonia.Input;
+using ReactiveUI;
+using WalletWasabi.Fluent.Extensions;
namespace WalletWasabi.Fluent.Behaviors;
@@ -13,12 +16,17 @@ internal class TextBoxAutoSelectTextBehavior : AttachedToVisualTreeBehavior<Text
return;
}
- AssociatedObject.SelectAll();
- Observable.FromEventPattern(AssociatedObject, nameof(AssociatedObject.GotFocus))
- .Subscribe(_ => AssociatedObject.SelectAll())
+ var gotFocus = AssociatedObject.OnEvent(InputElement.GotFocusEvent);
+ var lostFocus = AssociatedObject.OnEvent(InputElement.LostFocusEvent);
+ var isFocused = gotFocus.Select(_ => true).Merge(lostFocus.Select(_ => false));
+
+ isFocused
+ .Throttle(TimeSpan.FromSeconds(0.1))
+ .DistinctUntilChanged()
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Where(focused => focused)
+ .Do(_ => AssociatedObject.SelectAll())
+ .Subscribe()
.DisposeWith(disposable);
- Observable.FromEventPattern(AssociatedObject, nameof(AssociatedObject.PointerReleased))
- .Subscribe(_ => AssociatedObject.SelectAll())
- .DisposeWith(disposable);;
}
}

MODIFY
WalletWasabi.Fluent/Extensions/AvaloniaMixin.cs
WalletWasabi.Fluent/Extensions/AvaloniaMixin.cs
@@ -6,7 +6,10 @@ namespace WalletWasabi.Fluent.Extensions;
public static class AvaloniaMixin
{
- public static IObservable<EventPattern<TEventArgs>> OnEvent<TEventArgs>(this IInteractive target, RoutedEvent<TEventArgs> routedEvent, RoutingStrategies routingStrategies) where TEventArgs : RoutedEventArgs
+ public static IObservable<EventPattern<TEventArgs>> OnEvent<TEventArgs>(
+ this IInteractive target,
+ RoutedEvent<TEventArgs> routedEvent,
+ RoutingStrategies routingStrategies = RoutingStrategies.Bubble) where TEventArgs : RoutedEventArgs
{
return Observable.FromEventPattern<TEventArgs>(
add => target.AddHandler(routedEvent, add, routingStrategies),

RENAME
WalletWasabi.Fluent/Helpers/FlyoutController.cs
WalletWasabi.Fluent/Helpers/FlyoutShowController.cs
@@ -4,14 +4,14 @@ using Avalonia.Controls.Primitives;
namespace WalletWasabi.Fluent.Helpers;
-public class FlyoutController : IDisposable
+public class FlyoutShowController : IDisposable
{
private readonly FlyoutBase _flyout;
private readonly Control _parent;
private bool _isForcedOpen;
private bool _isOpen;
- public FlyoutController(FlyoutBase flyout, Control parent)
+ public FlyoutShowController(Control parent, FlyoutBase flyout)
{
_flyout = flyout;
_parent = parent;

MODIFY
WalletWasabi.Fluent/Views/SearchBar/SearchBar.axaml
WalletWasabi.Fluent/Views/SearchBar/SearchBar.axaml
<DoubleTransition Property="Opacity"
Duration="0:0:0.127"
Easing="{StaticResource FluentEasing}" />
- <!--NOTE: This was previously reported to cause a glitchy transition. This is likely because of a bug in Avalonia. -->
- <ThicknessTransition Property="Margin"
- Duration="0:0:0.127"
- Easing="{StaticResource FluentEasing}" />
</Transitions>
</Setter>
</Style>
</Style>
<!-- Focused State -->
+
<Style Selector="TextBox:focus-within">
<Setter Property="Foreground" Value="{DynamicResource AcrylicTrimForeground}" />
<Setter Property="Cursor" Value="Arrow" />
<i:Interaction.Behaviors>
<behaviors:TextBoxAutoSelectTextBehavior />
<behaviors:SwallowEnterKeyBehavior />
+ <behaviors:ShowAttachedFlyoutWhenFocusedBehavior IsFlyoutOpen="{Binding IsSearchListVisible, Mode=TwoWay}" />
</i:Interaction.Behaviors>
<TextBox.InnerLeftContent>
</Flyout>
</FlyoutBase.AttachedFlyout>
- <i:Interaction.Behaviors>
- <behaviors:ShowAttachedFlyoutWhenFocusedBehavior IsFlyoutOpen="{Binding IsSearchListVisible, Mode=TwoWay}" />
- </i:Interaction.Behaviors>
-
</TextBox>
</UserControl>

Modify Behavior to make it more robust