committed by
GitHub
149 changed files with 2863 additions and 2005 deletions
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,34 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Utilities |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A visitor to resolve an untyped <see cref="AvaloniaProperty"/> to a typed property.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TData">The type of user data passed.</typeparam>
|
||||
|
/// <remarks>
|
||||
|
/// Pass an instance that implements this interface to
|
||||
|
/// <see cref="AvaloniaProperty.Accept{TData}(IAvaloniaPropertyVisitor{TData}, ref TData)"/>
|
||||
|
/// in order to resolve un untyped <see cref="AvaloniaProperty"/> to a typed
|
||||
|
/// <see cref="StyledPropertyBase{TValue}"/> or <see cref="DirectPropertyBase{TValue}"/>.
|
||||
|
/// </remarks>
|
||||
|
public interface IAvaloniaPropertyVisitor<TData> |
||||
|
where TData : struct |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Called when the property is a styled property.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The property value type.</typeparam>
|
||||
|
/// <param name="property">The property.</param>
|
||||
|
/// <param name="data">The user data.</param>
|
||||
|
void Visit<T>(StyledPropertyBase<T> property, ref TData data); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the property is a direct property.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The property value type.</typeparam>
|
||||
|
/// <param name="property">The property.</param>
|
||||
|
/// <param name="data">The user data.</param>
|
||||
|
void Visit<T>(DirectPropertyBase<T> property, ref TData data); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,195 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Text; |
||||
|
using Avalonia.Data.Converters; |
||||
|
using Avalonia.Input; |
||||
|
|
||||
|
namespace Avalonia.Controls.Converters |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
|
||||
|
/// platform's style guidelines.
|
||||
|
/// </summary>
|
||||
|
public class PlatformKeyGestureConverter : IValueConverter |
||||
|
{ |
||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) |
||||
|
{ |
||||
|
if (value is null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
else if (value is KeyGesture gesture && targetType == typeof(string)) |
||||
|
{ |
||||
|
return ToPlatformString(gesture); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
|
||||
|
/// platform's style guidelines.
|
||||
|
/// </summary>
|
||||
|
/// <param name="gesture">The gesture.</param>
|
||||
|
/// <returns>The gesture formatted according to the current platform.</returns>
|
||||
|
public static string ToPlatformString(KeyGesture gesture) |
||||
|
{ |
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
return ToString(gesture, "Win"); |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
||||
|
{ |
||||
|
return ToString(gesture, "Super"); |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
||||
|
{ |
||||
|
return ToOSXString(gesture); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return gesture.ToString(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string ToString(KeyGesture gesture, string meta) |
||||
|
{ |
||||
|
var s = new StringBuilder(); |
||||
|
|
||||
|
static void Plus(StringBuilder s) |
||||
|
{ |
||||
|
if (s.Length > 0) |
||||
|
{ |
||||
|
s.Append("+"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control)) |
||||
|
{ |
||||
|
s.Append("Ctrl"); |
||||
|
} |
||||
|
|
||||
|
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift)) |
||||
|
{ |
||||
|
Plus(s); |
||||
|
s.Append("Shift"); |
||||
|
} |
||||
|
|
||||
|
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt)) |
||||
|
{ |
||||
|
Plus(s); |
||||
|
s.Append("Alt"); |
||||
|
} |
||||
|
|
||||
|
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta)) |
||||
|
{ |
||||
|
Plus(s); |
||||
|
s.Append(meta); |
||||
|
} |
||||
|
|
||||
|
Plus(s); |
||||
|
s.Append(ToString(gesture.Key)); |
||||
|
|
||||
|
return s.ToString(); |
||||
|
} |
||||
|
|
||||
|
private static string ToOSXString(KeyGesture gesture) |
||||
|
{ |
||||
|
var s = new StringBuilder(); |
||||
|
|
||||
|
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control)) |
||||
|
{ |
||||
|
s.Append('⌃'); |
||||
|
} |
||||
|
|
||||
|
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt)) |
||||
|
{ |
||||
|
s.Append('⌥'); |
||||
|
} |
||||
|
|
||||
|
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift)) |
||||
|
{ |
||||
|
s.Append('⇧'); |
||||
|
} |
||||
|
|
||||
|
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta)) |
||||
|
{ |
||||
|
s.Append('⌘'); |
||||
|
} |
||||
|
|
||||
|
s.Append(ToOSXString(gesture.Key)); |
||||
|
|
||||
|
return s.ToString(); |
||||
|
} |
||||
|
|
||||
|
private static string ToString(Key key) |
||||
|
{ |
||||
|
return key switch |
||||
|
{ |
||||
|
Key.Add => "+", |
||||
|
Key.Back => "Backspace", |
||||
|
Key.D0 => "0", |
||||
|
Key.D1 => "1", |
||||
|
Key.D2 => "2", |
||||
|
Key.D3 => "3", |
||||
|
Key.D4 => "4", |
||||
|
Key.D5 => "5", |
||||
|
Key.D6 => "6", |
||||
|
Key.D7 => "7", |
||||
|
Key.D8 => "8", |
||||
|
Key.D9 => "9", |
||||
|
Key.Decimal => ".", |
||||
|
Key.Divide => "/", |
||||
|
Key.Down => "Down Arrow", |
||||
|
Key.Left => "Left Arrow", |
||||
|
Key.Multiply => "*", |
||||
|
Key.OemBackslash => "\\", |
||||
|
Key.OemCloseBrackets => "]", |
||||
|
Key.OemComma => ",", |
||||
|
Key.OemMinus => "-", |
||||
|
Key.OemOpenBrackets => "[", |
||||
|
Key.OemPeriod=> ".", |
||||
|
Key.OemPipe => "|", |
||||
|
Key.OemPlus => "+", |
||||
|
Key.OemQuestion => "/", |
||||
|
Key.OemQuotes => "\"", |
||||
|
Key.OemSemicolon => ";", |
||||
|
Key.OemTilde => "`", |
||||
|
Key.Right => "Right Arrow", |
||||
|
Key.Separator => "/", |
||||
|
Key.Subtract => "-", |
||||
|
Key.Up => "Up Arrow", |
||||
|
_ => key.ToString(), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static string ToOSXString(Key key) |
||||
|
{ |
||||
|
return key switch |
||||
|
{ |
||||
|
Key.Back => "⌫", |
||||
|
Key.Down => "↓", |
||||
|
Key.End => "↘", |
||||
|
Key.Escape => "⎋", |
||||
|
Key.Home => "↖", |
||||
|
Key.Left => "←", |
||||
|
Key.Return => "↩", |
||||
|
Key.PageDown => "⇞", |
||||
|
Key.PageUp => "⇟", |
||||
|
Key.Right => "→", |
||||
|
Key.Space => "␣", |
||||
|
Key.Tab => "⇥", |
||||
|
Key.Up => "↑", |
||||
|
_ => ToString(key), |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,200 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Collections.Pooled; |
||||
|
|
||||
|
namespace Avalonia.Interactivity |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Holds the route for a routed event and supports raising an event on that route.
|
||||
|
/// </summary>
|
||||
|
public class EventRoute : IDisposable |
||||
|
{ |
||||
|
private readonly RoutedEvent _event; |
||||
|
private PooledList<RouteItem>? _route; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="RoutedEvent"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The routed event to be raised.</param>
|
||||
|
public EventRoute(RoutedEvent e) |
||||
|
{ |
||||
|
e = e ?? throw new ArgumentNullException(nameof(e)); |
||||
|
|
||||
|
_event = e; |
||||
|
_route = null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether the route has any handlers.
|
||||
|
/// </summary>
|
||||
|
public bool HasHandlers => _route?.Count > 0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds a handler to the route.
|
||||
|
/// </summary>
|
||||
|
/// <param name="target">The target on which the event should be raised.</param>
|
||||
|
/// <param name="handler">The handler for the event.</param>
|
||||
|
/// <param name="routes">The routing strategies to listen to.</param>
|
||||
|
/// <param name="handledEventsToo">
|
||||
|
/// If true the handler will be raised even when the routed event is marked as handled.
|
||||
|
/// </param>
|
||||
|
/// <param name="adapter">
|
||||
|
/// An optional adapter which if supplied, will be called with <paramref name="handler"/>
|
||||
|
/// and the parameters for the event. This adapter can be used to avoid calling
|
||||
|
/// `DynamicInvoke` on the handler.
|
||||
|
/// </param>
|
||||
|
public void Add( |
||||
|
IInteractive target, |
||||
|
Delegate handler, |
||||
|
RoutingStrategies routes, |
||||
|
bool handledEventsToo = false, |
||||
|
Action<Delegate, object, RoutedEventArgs>? adapter = null) |
||||
|
{ |
||||
|
target = target ?? throw new ArgumentNullException(nameof(target)); |
||||
|
handler = handler ?? throw new ArgumentNullException(nameof(handler)); |
||||
|
|
||||
|
_route ??= new PooledList<RouteItem>(16); |
||||
|
_route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds a class handler to the route.
|
||||
|
/// </summary>
|
||||
|
/// <param name="target">The target on which the event should be raised.</param>
|
||||
|
public void AddClassHandler(IInteractive target) |
||||
|
{ |
||||
|
target = target ?? throw new ArgumentNullException(nameof(target)); |
||||
|
|
||||
|
_route ??= new PooledList<RouteItem>(16); |
||||
|
_route.Add(new RouteItem(target, null, null, 0, false)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Raises an event along the route.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The event source.</param>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
public void RaiseEvent(IInteractive source, RoutedEventArgs e) |
||||
|
{ |
||||
|
source = source ?? throw new ArgumentNullException(nameof(source)); |
||||
|
e = e ?? throw new ArgumentNullException(nameof(e)); |
||||
|
|
||||
|
e.Source = source; |
||||
|
|
||||
|
if (_event.RoutingStrategies == RoutingStrategies.Direct) |
||||
|
{ |
||||
|
e.Route = RoutingStrategies.Direct; |
||||
|
RaiseEventImpl(e); |
||||
|
_event.InvokeRouteFinished(e); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) |
||||
|
{ |
||||
|
e.Route = RoutingStrategies.Tunnel; |
||||
|
RaiseEventImpl(e); |
||||
|
_event.InvokeRouteFinished(e); |
||||
|
} |
||||
|
|
||||
|
if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble)) |
||||
|
{ |
||||
|
e.Route = RoutingStrategies.Bubble; |
||||
|
RaiseEventImpl(e); |
||||
|
_event.InvokeRouteFinished(e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disposes of the event route.
|
||||
|
/// </summary>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_route?.Dispose(); |
||||
|
_route = null; |
||||
|
} |
||||
|
|
||||
|
private void RaiseEventImpl(RoutedEventArgs e) |
||||
|
{ |
||||
|
if (_route is null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (e.Source is null) |
||||
|
{ |
||||
|
throw new ArgumentException("Event source may not be null", nameof(e)); |
||||
|
} |
||||
|
|
||||
|
IInteractive? lastTarget = null; |
||||
|
var start = 0; |
||||
|
var end = _route.Count; |
||||
|
var step = 1; |
||||
|
|
||||
|
if (e.Route == RoutingStrategies.Tunnel) |
||||
|
{ |
||||
|
start = end - 1; |
||||
|
step = end = -1; |
||||
|
} |
||||
|
|
||||
|
for (var i = start; i != end; i += step) |
||||
|
{ |
||||
|
var entry = _route[i]; |
||||
|
|
||||
|
// If we've got to a new control then call any RoutedEvent.Raised listeners.
|
||||
|
if (entry.Target != lastTarget) |
||||
|
{ |
||||
|
if (!e.Handled) |
||||
|
{ |
||||
|
_event.InvokeRaised(entry.Target, e); |
||||
|
} |
||||
|
|
||||
|
// If this is a direct event and we've already raised events then we're finished.
|
||||
|
if (e.Route == RoutingStrategies.Direct && lastTarget is object) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
lastTarget = entry.Target; |
||||
|
} |
||||
|
|
||||
|
// Raise the event handler.
|
||||
|
if (entry.Handler is object && |
||||
|
entry.Routes.HasFlagCustom(e.Route) && |
||||
|
(!e.Handled || entry.HandledEventsToo)) |
||||
|
{ |
||||
|
if (entry.Adapter is object) |
||||
|
{ |
||||
|
entry.Adapter(entry.Handler, entry.Target, e); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
entry.Handler.DynamicInvoke(entry.Target, e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly struct RouteItem |
||||
|
{ |
||||
|
public RouteItem( |
||||
|
IInteractive target, |
||||
|
Delegate? handler, |
||||
|
Action<Delegate, object, RoutedEventArgs>? adapter, |
||||
|
RoutingStrategies routes, |
||||
|
bool handledEventsToo) |
||||
|
{ |
||||
|
Target = target; |
||||
|
Handler = handler; |
||||
|
Adapter = adapter; |
||||
|
Routes = routes; |
||||
|
HandledEventsToo = handledEventsToo; |
||||
|
} |
||||
|
|
||||
|
public IInteractive Target { get; } |
||||
|
public Delegate? Handler { get; } |
||||
|
public Action<Delegate, object, RoutedEventArgs>? Adapter { get; } |
||||
|
public RoutingStrategies Routes { get; } |
||||
|
public bool HandledEventsToo { get; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,20 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
|
|
||||
namespace Avalonia.Interactivity |
|
||||
{ |
|
||||
internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args); |
|
||||
|
|
||||
internal class EventSubscription |
|
||||
{ |
|
||||
public HandlerInvokeSignature InvokeAdapter { get; set; } |
|
||||
|
|
||||
public Delegate Handler { get; set; } |
|
||||
|
|
||||
public RoutingStrategies Routes { get; set; } |
|
||||
|
|
||||
public bool AlsoIfHandled { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,77 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
|
|
||||
namespace Avalonia.Styling |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// An observable which is switched on or off according to an activator observable.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// An <see cref="ActivatedObservable"/> has two inputs: an activator observable and a
|
|
||||
/// <see cref="Source"/> observable which produces the activated value. When the activator
|
|
||||
/// produces true, the <see cref="ActivatedObservable"/> will produce the current activated
|
|
||||
/// value. When the activator produces false it will produce
|
|
||||
/// <see cref="AvaloniaProperty.UnsetValue"/>.
|
|
||||
/// </remarks>
|
|
||||
internal class ActivatedObservable : ActivatedValue, IDescription |
|
||||
{ |
|
||||
private IDisposable _sourceSubscription; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="activator">The activator.</param>
|
|
||||
/// <param name="source">An observable that produces the activated value.</param>
|
|
||||
/// <param name="description">The binding description.</param>
|
|
||||
public ActivatedObservable( |
|
||||
IObservable<bool> activator, |
|
||||
IObservable<object> source, |
|
||||
string description) |
|
||||
: base(activator, AvaloniaProperty.UnsetValue, description) |
|
||||
{ |
|
||||
Contract.Requires<ArgumentNullException>(source != null); |
|
||||
|
|
||||
Source = source; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets an observable which produces the <see cref="ActivatedValue"/>.
|
|
||||
/// </summary>
|
|
||||
public IObservable<object> Source { get; } |
|
||||
|
|
||||
protected override ActivatorListener CreateListener() => new ValueListener(this); |
|
||||
|
|
||||
protected override void Deinitialize() |
|
||||
{ |
|
||||
base.Deinitialize(); |
|
||||
_sourceSubscription.Dispose(); |
|
||||
_sourceSubscription = null; |
|
||||
} |
|
||||
|
|
||||
protected override void Initialize() |
|
||||
{ |
|
||||
base.Initialize(); |
|
||||
_sourceSubscription = Source.Subscribe((ValueListener)Listener); |
|
||||
} |
|
||||
|
|
||||
protected virtual void NotifyValue(object value) |
|
||||
{ |
|
||||
Value = value; |
|
||||
} |
|
||||
|
|
||||
private class ValueListener : ActivatorListener, IObserver<object> |
|
||||
{ |
|
||||
public ValueListener(ActivatedObservable parent) |
|
||||
: base(parent) |
|
||||
{ |
|
||||
} |
|
||||
protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent; |
|
||||
|
|
||||
void IObserver<object>.OnCompleted() => Parent.CompletedReceived(); |
|
||||
void IObserver<object>.OnError(Exception error) => Parent.ErrorReceived(error); |
|
||||
void IObserver<object>.OnNext(object value) => Parent.NotifyValue(value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,110 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Reactive.Subjects; |
|
||||
|
|
||||
namespace Avalonia.Styling |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// A subject which is switched on or off according to an activator observable.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// An <see cref="ActivatedSubject"/> extends <see cref="ActivatedObservable"/> to
|
|
||||
/// be an <see cref="ISubject{Object}"/>. When the object is active then values
|
|
||||
/// received via <see cref="OnNext(object)"/> will be passed to the source subject.
|
|
||||
/// </remarks>
|
|
||||
internal class ActivatedSubject : ActivatedObservable, ISubject<object>, IDescription |
|
||||
{ |
|
||||
private bool _completed; |
|
||||
private object _pushValue; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="ActivatedSubject"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="activator">The activator.</param>
|
|
||||
/// <param name="source">An observable that produces the activated value.</param>
|
|
||||
/// <param name="description">The binding description.</param>
|
|
||||
public ActivatedSubject( |
|
||||
IObservable<bool> activator, |
|
||||
ISubject<object> source, |
|
||||
string description) |
|
||||
: base(activator, source, description) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the underlying subject.
|
|
||||
/// </summary>
|
|
||||
public new ISubject<object> Source |
|
||||
{ |
|
||||
get { return (ISubject<object>)base.Source; } |
|
||||
} |
|
||||
|
|
||||
public void OnCompleted() |
|
||||
{ |
|
||||
Source.OnCompleted(); |
|
||||
} |
|
||||
|
|
||||
public void OnError(Exception error) |
|
||||
{ |
|
||||
Source.OnError(error); |
|
||||
} |
|
||||
|
|
||||
public void OnNext(object value) |
|
||||
{ |
|
||||
_pushValue = value; |
|
||||
|
|
||||
if (IsActive == true && !_completed) |
|
||||
{ |
|
||||
Source.OnNext(_pushValue); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected override void ActiveChanged(bool active) |
|
||||
{ |
|
||||
bool first = !IsActive.HasValue; |
|
||||
|
|
||||
base.ActiveChanged(active); |
|
||||
|
|
||||
if (!first) |
|
||||
{ |
|
||||
Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected override void CompletedReceived() |
|
||||
{ |
|
||||
base.CompletedReceived(); |
|
||||
|
|
||||
if (!_completed) |
|
||||
{ |
|
||||
Source.OnCompleted(); |
|
||||
_completed = true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected override void ErrorReceived(Exception error) |
|
||||
{ |
|
||||
base.ErrorReceived(error); |
|
||||
|
|
||||
if (!_completed) |
|
||||
{ |
|
||||
Source.OnError(error); |
|
||||
_completed = true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void ActivatorCompleted() |
|
||||
{ |
|
||||
_completed = true; |
|
||||
Source.OnCompleted(); |
|
||||
} |
|
||||
|
|
||||
private void ActivatorError(Exception e) |
|
||||
{ |
|
||||
_completed = true; |
|
||||
Source.OnError(e); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,133 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using Avalonia.Reactive; |
|
||||
|
|
||||
namespace Avalonia.Styling |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// An value which is switched on or off according to an activator observable.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// An <see cref="ActivatedValue"/> has two inputs: an activator observable and an
|
|
||||
/// <see cref="Value"/>. When the activator produces true, the
|
|
||||
/// <see cref="ActivatedValue"/> will produce the current value. When the activator
|
|
||||
/// produces false it will produce <see cref="AvaloniaProperty.UnsetValue"/>.
|
|
||||
/// </remarks>
|
|
||||
internal class ActivatedValue : LightweightObservableBase<object>, IDescription |
|
||||
{ |
|
||||
private static readonly object NotSent = new object(); |
|
||||
private IDisposable _activatorSubscription; |
|
||||
private object _value; |
|
||||
private object _last = NotSent; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="activator">The activator.</param>
|
|
||||
/// <param name="value">The activated value.</param>
|
|
||||
/// <param name="description">The binding description.</param>
|
|
||||
public ActivatedValue( |
|
||||
IObservable<bool> activator, |
|
||||
object value, |
|
||||
string description) |
|
||||
{ |
|
||||
Contract.Requires<ArgumentNullException>(activator != null); |
|
||||
|
|
||||
Activator = activator; |
|
||||
Value = value; |
|
||||
Description = description; |
|
||||
Listener = CreateListener(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the activator observable.
|
|
||||
/// </summary>
|
|
||||
public IObservable<bool> Activator { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a description of the binding.
|
|
||||
/// </summary>
|
|
||||
public string Description { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a value indicating whether the activator is active.
|
|
||||
/// </summary>
|
|
||||
public bool? IsActive { get; private set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the value that will be produced when <see cref="IsActive"/> is true.
|
|
||||
/// </summary>
|
|
||||
public object Value |
|
||||
{ |
|
||||
get => _value; |
|
||||
protected set |
|
||||
{ |
|
||||
_value = value; |
|
||||
PublishValue(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected ActivatorListener Listener { get; } |
|
||||
|
|
||||
protected virtual void ActiveChanged(bool active) |
|
||||
{ |
|
||||
IsActive = active; |
|
||||
PublishValue(); |
|
||||
} |
|
||||
|
|
||||
protected virtual void CompletedReceived() => PublishCompleted(); |
|
||||
|
|
||||
protected virtual ActivatorListener CreateListener() => new ActivatorListener(this); |
|
||||
|
|
||||
protected override void Deinitialize() |
|
||||
{ |
|
||||
_activatorSubscription.Dispose(); |
|
||||
_activatorSubscription = null; |
|
||||
} |
|
||||
|
|
||||
protected virtual void ErrorReceived(Exception error) => PublishError(error); |
|
||||
|
|
||||
protected override void Initialize() |
|
||||
{ |
|
||||
_activatorSubscription = Activator.Subscribe(Listener); |
|
||||
} |
|
||||
|
|
||||
protected override void Subscribed(IObserver<object> observer, bool first) |
|
||||
{ |
|
||||
if (IsActive == true && !first) |
|
||||
{ |
|
||||
observer.OnNext(Value); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void PublishValue() |
|
||||
{ |
|
||||
if (IsActive.HasValue) |
|
||||
{ |
|
||||
var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue; |
|
||||
|
|
||||
if (!Equals(v, _last)) |
|
||||
{ |
|
||||
PublishNext(v); |
|
||||
_last = v; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected class ActivatorListener : IObserver<bool> |
|
||||
{ |
|
||||
public ActivatorListener(ActivatedValue parent) |
|
||||
{ |
|
||||
Parent = parent; |
|
||||
} |
|
||||
|
|
||||
protected ActivatedValue Parent { get; } |
|
||||
|
|
||||
void IObserver<bool>.OnCompleted() => Parent.CompletedReceived(); |
|
||||
void IObserver<bool>.OnError(Exception error) => Parent.ErrorReceived(error); |
|
||||
void IObserver<bool>.OnNext(bool value) => Parent.ActiveChanged(value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,71 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An aggregate <see cref="IStyleActivator"/> which is active when all of its inputs are
|
||||
|
/// active.
|
||||
|
/// </summary>
|
||||
|
internal class AndActivator : StyleActivatorBase, IStyleActivatorSink |
||||
|
{ |
||||
|
private List<IStyleActivator>? _sources; |
||||
|
private ulong _flags; |
||||
|
private ulong _mask; |
||||
|
|
||||
|
public int Count => _sources?.Count ?? 0; |
||||
|
|
||||
|
public void Add(IStyleActivator activator) |
||||
|
{ |
||||
|
_sources ??= new List<IStyleActivator>(); |
||||
|
_sources.Add(activator); |
||||
|
} |
||||
|
|
||||
|
void IStyleActivatorSink.OnNext(bool value, int tag) |
||||
|
{ |
||||
|
if (value) |
||||
|
{ |
||||
|
_flags |= 1ul << tag; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_flags &= ~(1ul << tag); |
||||
|
} |
||||
|
|
||||
|
if (_mask != 0) |
||||
|
{ |
||||
|
PublishNext(_flags == _mask); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void Initialize() |
||||
|
{ |
||||
|
if (_sources is object) |
||||
|
{ |
||||
|
var i = 0; |
||||
|
|
||||
|
foreach (var source in _sources) |
||||
|
{ |
||||
|
source.Subscribe(this, i++); |
||||
|
} |
||||
|
|
||||
|
_mask = (1ul << Count) - 1; |
||||
|
PublishNext(_flags == _mask); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void Deinitialize() |
||||
|
{ |
||||
|
if (_sources is object) |
||||
|
{ |
||||
|
foreach (var source in _sources) |
||||
|
{ |
||||
|
source.Unsubscribe(this); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_mask = 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Builds an <see cref="AndActivator"/>.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// When ANDing style activators, if there is more than one input then creates an instance of
|
||||
|
/// <see cref="AndActivator"/>. If there is only one input, returns the input directly.
|
||||
|
/// </remarks>
|
||||
|
internal struct AndActivatorBuilder |
||||
|
{ |
||||
|
private IStyleActivator? _single; |
||||
|
private AndActivator? _multiple; |
||||
|
|
||||
|
public void Add(IStyleActivator? activator) |
||||
|
{ |
||||
|
if (activator == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (_single is null && _multiple is null) |
||||
|
{ |
||||
|
_single = activator; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (_multiple is null) |
||||
|
{ |
||||
|
_multiple = new AndActivator(); |
||||
|
_multiple.Add(_single!); |
||||
|
_single = null; |
||||
|
} |
||||
|
|
||||
|
_multiple.Add(activator); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IStyleActivator Get() => _single ?? _multiple!; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines a style activator.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// A style activator is very similar to an `IObservable{bool}` but is optimized for the
|
||||
|
/// particular use-case of activating a style according to a selector. It differs from
|
||||
|
/// an observable in two major ways:
|
||||
|
///
|
||||
|
/// - Can only have a single subscription
|
||||
|
/// - The subscription can have a tag associated with it, allowing a subscriber to index
|
||||
|
/// into a list of subscriptions without having to allocate additional objects.
|
||||
|
/// </remarks>
|
||||
|
public interface IStyleActivator : IDisposable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Subscribes to the activator.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sink">The listener.</param>
|
||||
|
/// <param name="tag">An optional tag.</param>
|
||||
|
void Subscribe(IStyleActivatorSink sink, int tag = 0); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Unsubscribes from the activator.
|
||||
|
/// </summary>
|
||||
|
void Unsubscribe(IStyleActivatorSink sink); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Receives notifications from an <see cref="IStyleActivator"/>.
|
||||
|
/// </summary>
|
||||
|
public interface IStyleActivatorSink |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Called when the subscribed activator value changes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="value">The new value.</param>
|
||||
|
/// <param name="tag">The subscription tag.</param>
|
||||
|
void OnNext(bool value, int tag); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An <see cref="IStyleActivator"/> which inverts the state of an input activator.
|
||||
|
/// </summary>
|
||||
|
internal class NotActivator : StyleActivatorBase, IStyleActivatorSink |
||||
|
{ |
||||
|
private readonly IStyleActivator _source; |
||||
|
public NotActivator(IStyleActivator source) => _source = source; |
||||
|
void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value); |
||||
|
protected override void Initialize() => _source.Subscribe(this, 0); |
||||
|
protected override void Deinitialize() => _source.Unsubscribe(this); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An aggregate <see cref="IStyleActivator"/> which is active when any of its inputs are
|
||||
|
/// active.
|
||||
|
/// </summary>
|
||||
|
internal class OrActivator : StyleActivatorBase, IStyleActivatorSink |
||||
|
{ |
||||
|
private List<IStyleActivator>? _sources; |
||||
|
private ulong _flags; |
||||
|
private bool _initializing; |
||||
|
|
||||
|
public int Count => _sources?.Count ?? 0; |
||||
|
|
||||
|
public void Add(IStyleActivator activator) |
||||
|
{ |
||||
|
_sources ??= new List<IStyleActivator>(); |
||||
|
_sources.Add(activator); |
||||
|
} |
||||
|
|
||||
|
void IStyleActivatorSink.OnNext(bool value, int tag) |
||||
|
{ |
||||
|
if (value) |
||||
|
{ |
||||
|
_flags |= 1ul << tag; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_flags &= ~(1ul << tag); |
||||
|
} |
||||
|
|
||||
|
if (!_initializing) |
||||
|
{ |
||||
|
PublishNext(_flags != 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void Initialize() |
||||
|
{ |
||||
|
if (_sources is object) |
||||
|
{ |
||||
|
var i = 0; |
||||
|
|
||||
|
_initializing = true; |
||||
|
|
||||
|
foreach (var source in _sources) |
||||
|
{ |
||||
|
source.Subscribe(this, i++); |
||||
|
} |
||||
|
|
||||
|
_initializing = false; |
||||
|
PublishNext(_flags != 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void Deinitialize() |
||||
|
{ |
||||
|
if (_sources is object) |
||||
|
{ |
||||
|
foreach (var source in _sources) |
||||
|
{ |
||||
|
source.Unsubscribe(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Builds an <see cref="OrActivator"/>.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// When ORing style activators, if there is more than one input then creates an instance of
|
||||
|
/// <see cref="OrActivator"/>. If there is only one input, returns the input directly.
|
||||
|
/// </remarks>
|
||||
|
internal struct OrActivatorBuilder |
||||
|
{ |
||||
|
private IStyleActivator? _single; |
||||
|
private OrActivator? _multiple; |
||||
|
|
||||
|
public int Count => _multiple?.Count ?? (_single is object ? 1 : 0); |
||||
|
|
||||
|
public void Add(IStyleActivator? activator) |
||||
|
{ |
||||
|
if (activator == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (_single is null && _multiple is null) |
||||
|
{ |
||||
|
_single = activator; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (_multiple is null) |
||||
|
{ |
||||
|
_multiple = new OrActivator(); |
||||
|
_multiple.Add(_single!); |
||||
|
_single = null; |
||||
|
} |
||||
|
|
||||
|
_multiple.Add(activator); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IStyleActivator Get() => _single ?? _multiple!; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
using System; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An <see cref="IStyleActivator"/> which listens to a property value on a control.
|
||||
|
/// </summary>
|
||||
|
internal class PropertyEqualsActivator : StyleActivatorBase, IObserver<object> |
||||
|
{ |
||||
|
private readonly IStyleable _control; |
||||
|
private readonly AvaloniaProperty _property; |
||||
|
private readonly object? _value; |
||||
|
private IDisposable? _subscription; |
||||
|
|
||||
|
public PropertyEqualsActivator( |
||||
|
IStyleable control, |
||||
|
AvaloniaProperty property, |
||||
|
object? value) |
||||
|
{ |
||||
|
_control = control ?? throw new ArgumentNullException(nameof(control)); |
||||
|
_property = property ?? throw new ArgumentNullException(nameof(property)); |
||||
|
_value = value; |
||||
|
} |
||||
|
|
||||
|
protected override void Initialize() |
||||
|
{ |
||||
|
_subscription = _control.GetObservable(_property).Subscribe(this); |
||||
|
} |
||||
|
|
||||
|
protected override void Deinitialize() => _subscription?.Dispose(); |
||||
|
|
||||
|
void IObserver<object>.OnCompleted() { } |
||||
|
void IObserver<object>.OnError(Exception error) { } |
||||
|
void IObserver<object>.OnNext(object value) => PublishNext(Equals(value, _value)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Base class implementation of <see cref="IStyleActivator"/>.
|
||||
|
/// </summary>
|
||||
|
internal abstract class StyleActivatorBase : IStyleActivator |
||||
|
{ |
||||
|
private IStyleActivatorSink? _sink; |
||||
|
private int _tag; |
||||
|
private bool? _value; |
||||
|
|
||||
|
public void Subscribe(IStyleActivatorSink sink, int tag = 0) |
||||
|
{ |
||||
|
if (_sink is null) |
||||
|
{ |
||||
|
_sink = sink; |
||||
|
_tag = tag; |
||||
|
_value = null; |
||||
|
Initialize(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Unsubscribe(IStyleActivatorSink sink) |
||||
|
{ |
||||
|
if (_sink != sink) |
||||
|
{ |
||||
|
throw new AvaloniaInternalException("StyleActivatorSink is not subscribed."); |
||||
|
} |
||||
|
|
||||
|
_sink = null; |
||||
|
Deinitialize(); |
||||
|
} |
||||
|
|
||||
|
public void PublishNext(bool value) |
||||
|
{ |
||||
|
if (_value != value) |
||||
|
{ |
||||
|
_value = value; |
||||
|
_sink?.OnNext(value, _tag); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_sink = null; |
||||
|
Deinitialize(); |
||||
|
} |
||||
|
|
||||
|
protected abstract void Initialize(); |
||||
|
protected abstract void Deinitialize(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Specialized; |
||||
|
using Avalonia.Collections; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a
|
||||
|
/// control.
|
||||
|
/// </summary>
|
||||
|
internal sealed class StyleClassActivator : StyleActivatorBase |
||||
|
{ |
||||
|
private readonly IList<string> _match; |
||||
|
private readonly IAvaloniaReadOnlyList<string> _classes; |
||||
|
|
||||
|
public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match) |
||||
|
{ |
||||
|
_classes = classes; |
||||
|
_match = match; |
||||
|
} |
||||
|
|
||||
|
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch) |
||||
|
{ |
||||
|
int remainingMatches = toMatch.Count; |
||||
|
int classesCount = classes.Count; |
||||
|
|
||||
|
// Early bail out - we can't match if control does not have enough classes.
|
||||
|
if (classesCount < remainingMatches) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
for (var i = 0; i < classesCount; i++) |
||||
|
{ |
||||
|
var c = classes[i]; |
||||
|
|
||||
|
if (toMatch.Contains(c)) |
||||
|
{ |
||||
|
--remainingMatches; |
||||
|
|
||||
|
// Already matched so we can skip checking other classes.
|
||||
|
if (remainingMatches == 0) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return remainingMatches == 0; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
protected override void Initialize() |
||||
|
{ |
||||
|
PublishNext(IsMatching()); |
||||
|
_classes.CollectionChanged += ClassesChanged; |
||||
|
} |
||||
|
|
||||
|
protected override void Deinitialize() |
||||
|
{ |
||||
|
_classes.CollectionChanged -= ClassesChanged; |
||||
|
} |
||||
|
|
||||
|
private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Action != NotifyCollectionChangedAction.Move) |
||||
|
{ |
||||
|
PublishNext(IsMatching()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool IsMatching() => AreClassesMatching(_classes, _match); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a setter that has been instanced on a control.
|
||||
|
/// </summary>
|
||||
|
public interface ISetterInstance : IDisposable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Starts the setter instance.
|
||||
|
/// </summary>
|
||||
|
/// <param name="hasActivator">Whether the parent style has an activator.</param>
|
||||
|
/// <remarks>
|
||||
|
/// If <paramref name="hasActivator"/> is false then the setter should be immediately
|
||||
|
/// applied and <see cref="Activate"/> and <see cref="Deactivate"/> should not be called.
|
||||
|
/// If true, then bindings etc should be initiated but not produce a value until
|
||||
|
/// <see cref="Activate"/> called.
|
||||
|
/// </remarks>
|
||||
|
public void Start(bool hasActivator); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Activates the setter.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
|
||||
|
/// </remarks>
|
||||
|
public void Activate(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Deactivates the setter.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
|
||||
|
/// </remarks>
|
||||
|
public void Deactivate(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using System; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a style that has been instanced on a control.
|
||||
|
/// </summary>
|
||||
|
public interface IStyleInstance : IDisposable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the source style.
|
||||
|
/// </summary>
|
||||
|
IStyle Source { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Instructs the style to start acting upon the control.
|
||||
|
/// </summary>
|
||||
|
void Start(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,180 @@ |
|||||
|
using System; |
||||
|
using System.Reactive.Subjects; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A <see cref="Setter"/> which has been instanced on a control and has an
|
||||
|
/// <see cref="IBinding"/> as its value.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The target property type.</typeparam>
|
||||
|
internal class PropertySetterBindingInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
||||
|
ISubject<BindingValue<T>>, |
||||
|
ISetterInstance |
||||
|
{ |
||||
|
private readonly IStyleable _target; |
||||
|
private readonly StyledPropertyBase<T>? _styledProperty; |
||||
|
private readonly DirectPropertyBase<T>? _directProperty; |
||||
|
private readonly InstancedBinding _binding; |
||||
|
private readonly Inner _inner; |
||||
|
private BindingValue<T> _value; |
||||
|
private IDisposable? _subscription; |
||||
|
private IDisposable? _subscriptionTwoWay; |
||||
|
private bool _isActive; |
||||
|
|
||||
|
public PropertySetterBindingInstance( |
||||
|
IStyleable target, |
||||
|
StyledPropertyBase<T> property, |
||||
|
IBinding binding) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_styledProperty = property; |
||||
|
_binding = binding.Initiate(_target, property); |
||||
|
|
||||
|
if (_binding.Mode == BindingMode.OneTime) |
||||
|
{ |
||||
|
// For the moment, we don't support OneTime bindings in setters, because I'm not
|
||||
|
// sure what the semantics should be in the case of activation/deactivation.
|
||||
|
throw new NotSupportedException("OneTime bindings are not supported in setters."); |
||||
|
} |
||||
|
|
||||
|
_inner = new Inner(this); |
||||
|
} |
||||
|
|
||||
|
public PropertySetterBindingInstance( |
||||
|
IStyleable target, |
||||
|
DirectPropertyBase<T> property, |
||||
|
IBinding binding) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_directProperty = property; |
||||
|
_binding = binding.Initiate(_target, property); |
||||
|
_inner = new Inner(this); |
||||
|
} |
||||
|
|
||||
|
public void Start(bool hasActivator) |
||||
|
{ |
||||
|
_isActive = !hasActivator; |
||||
|
|
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
if (_binding.Mode != BindingMode.OneWayToSource) |
||||
|
{ |
||||
|
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; |
||||
|
_subscription = _target.Bind(_styledProperty, this, priority); |
||||
|
} |
||||
|
|
||||
|
if (_binding.Mode == BindingMode.TwoWay) |
||||
|
{ |
||||
|
_subscriptionTwoWay = _target.GetBindingObservable(_styledProperty).Subscribe(this); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (_binding.Mode != BindingMode.OneWayToSource) |
||||
|
{ |
||||
|
_subscription = _target.Bind(_directProperty!, this); |
||||
|
} |
||||
|
|
||||
|
if (_binding.Mode == BindingMode.TwoWay) |
||||
|
{ |
||||
|
_subscriptionTwoWay = _target.GetBindingObservable(_directProperty!).Subscribe(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Activate() |
||||
|
{ |
||||
|
if (!_isActive) |
||||
|
{ |
||||
|
_isActive = true; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Deactivate() |
||||
|
{ |
||||
|
if (_isActive) |
||||
|
{ |
||||
|
_isActive = false; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
if (_subscription is object) |
||||
|
{ |
||||
|
var sub = _subscription; |
||||
|
_subscription = null; |
||||
|
sub.Dispose(); |
||||
|
} |
||||
|
|
||||
|
if (_subscriptionTwoWay is object) |
||||
|
{ |
||||
|
var sub = _subscriptionTwoWay; |
||||
|
_subscriptionTwoWay = null; |
||||
|
sub.Dispose(); |
||||
|
} |
||||
|
|
||||
|
base.Dispose(); |
||||
|
} |
||||
|
|
||||
|
void IObserver<BindingValue<T>>.OnCompleted() |
||||
|
{ |
||||
|
// This is the observable coming from the target control. It should not complete.
|
||||
|
} |
||||
|
|
||||
|
void IObserver<BindingValue<T>>.OnError(Exception error) |
||||
|
{ |
||||
|
// This is the observable coming from the target control. It should not error.
|
||||
|
} |
||||
|
|
||||
|
void IObserver<BindingValue<T>>.OnNext(BindingValue<T> value) |
||||
|
{ |
||||
|
if (value.HasValue && _isActive) |
||||
|
{ |
||||
|
_binding.Subject.OnNext(value.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void Subscribed() |
||||
|
{ |
||||
|
_subscription = _binding.Observable.Subscribe(_inner); |
||||
|
} |
||||
|
|
||||
|
protected override void Unsubscribed() |
||||
|
{ |
||||
|
_subscription?.Dispose(); |
||||
|
_subscription = null; |
||||
|
} |
||||
|
|
||||
|
private void PublishNext() |
||||
|
{ |
||||
|
PublishNext(_isActive ? _value : default); |
||||
|
} |
||||
|
|
||||
|
private void ConvertAndPublishNext(object? value) |
||||
|
{ |
||||
|
_value = value is T v ? v : BindingValue<object>.FromUntyped(value).Convert<T>(); |
||||
|
|
||||
|
if (_isActive) |
||||
|
{ |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class Inner : IObserver<object?> |
||||
|
{ |
||||
|
private readonly PropertySetterBindingInstance<T> _owner; |
||||
|
public Inner(PropertySetterBindingInstance<T> owner) => _owner = owner; |
||||
|
public void OnCompleted() => _owner.PublishCompleted(); |
||||
|
public void OnError(Exception error) => _owner.PublishError(error); |
||||
|
public void OnNext(object? value) => _owner.ConvertAndPublishNext(value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A <see cref="Setter"/> which has been instance on a control.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The target property type.</typeparam>
|
||||
|
internal class PropertySetterInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
||||
|
ISetterInstance |
||||
|
{ |
||||
|
private readonly IStyleable _target; |
||||
|
private readonly StyledPropertyBase<T>? _styledProperty; |
||||
|
private readonly DirectPropertyBase<T>? _directProperty; |
||||
|
private readonly T _value; |
||||
|
private IDisposable? _subscription; |
||||
|
private bool _isActive; |
||||
|
|
||||
|
public PropertySetterInstance( |
||||
|
IStyleable target, |
||||
|
StyledPropertyBase<T> property, |
||||
|
T value) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_styledProperty = property; |
||||
|
_value = value; |
||||
|
} |
||||
|
|
||||
|
public PropertySetterInstance( |
||||
|
IStyleable target, |
||||
|
DirectPropertyBase<T> property, |
||||
|
T value) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_directProperty = property; |
||||
|
_value = value; |
||||
|
} |
||||
|
|
||||
|
public void Start(bool hasActivator) |
||||
|
{ |
||||
|
if (hasActivator) |
||||
|
{ |
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
_subscription = _target.Bind(_styledProperty, this, BindingPriority.StyleTrigger); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_subscription = _target.Bind(_directProperty, this); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
_subscription = _target.SetValue(_styledProperty, _value, BindingPriority.Style); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_target.SetValue(_directProperty!, _value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Activate() |
||||
|
{ |
||||
|
if (!_isActive) |
||||
|
{ |
||||
|
_isActive = true; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Deactivate() |
||||
|
{ |
||||
|
if (_isActive) |
||||
|
{ |
||||
|
_isActive = false; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
if (_subscription is object) |
||||
|
{ |
||||
|
var sub = _subscription; |
||||
|
_subscription = null; |
||||
|
sub.Dispose(); |
||||
|
} |
||||
|
else if (_isActive) |
||||
|
{ |
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
_target.ClearValue(_styledProperty); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_target.ClearValue(_directProperty); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
base.Dispose(); |
||||
|
} |
||||
|
|
||||
|
protected override void Subscribed() => PublishNext(); |
||||
|
protected override void Unsubscribed() { } |
||||
|
|
||||
|
private void PublishNext() |
||||
|
{ |
||||
|
PublishNext(_isActive ? new BindingValue<T>(_value) : default); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,56 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Reactive; |
|
||||
using System.Reactive.Linq; |
|
||||
|
|
||||
namespace Avalonia.Styling |
|
||||
{ |
|
||||
public enum ActivatorMode |
|
||||
{ |
|
||||
And, |
|
||||
Or, |
|
||||
} |
|
||||
|
|
||||
public static class StyleActivator |
|
||||
{ |
|
||||
public static IObservable<bool> And(IList<IObservable<bool>> inputs) |
|
||||
{ |
|
||||
if (inputs.Count == 0) |
|
||||
{ |
|
||||
throw new ArgumentException("StyleActivator.And inputs may not be empty."); |
|
||||
} |
|
||||
else if (inputs.Count == 1) |
|
||||
{ |
|
||||
return inputs[0]; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
return inputs.CombineLatest() |
|
||||
.Select(values => values.All(x => x)) |
|
||||
.DistinctUntilChanged(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static IObservable<bool> Or(IList<IObservable<bool>> inputs) |
|
||||
{ |
|
||||
if (inputs.Count == 0) |
|
||||
{ |
|
||||
throw new ArgumentException("StyleActivator.Or inputs may not be empty."); |
|
||||
} |
|
||||
else if (inputs.Count == 1) |
|
||||
{ |
|
||||
return inputs[0]; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
return inputs.CombineLatest() |
|
||||
.Select(values => values.Any(x => x)) |
|
||||
.DistinctUntilChanged(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,135 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reactive.Subjects; |
||||
|
using Avalonia.Animation; |
||||
|
using Avalonia.Styling.Activators; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A <see cref="Style"/> which has been instanced on a control.
|
||||
|
/// </summary>
|
||||
|
internal class StyleInstance : IStyleInstance, IStyleActivatorSink |
||||
|
{ |
||||
|
private readonly List<ISetterInstance>? _setters; |
||||
|
private readonly List<IDisposable>? _animations; |
||||
|
private readonly IStyleActivator? _activator; |
||||
|
private readonly Subject<bool>? _animationTrigger; |
||||
|
private bool _active; |
||||
|
|
||||
|
public StyleInstance( |
||||
|
IStyle source, |
||||
|
IStyleable target, |
||||
|
IReadOnlyList<ISetter>? setters, |
||||
|
IReadOnlyList<IAnimation>? animations, |
||||
|
IStyleActivator? activator = null) |
||||
|
{ |
||||
|
Source = source ?? throw new ArgumentNullException(nameof(source)); |
||||
|
Target = target ?? throw new ArgumentNullException(nameof(target)); |
||||
|
_activator = activator; |
||||
|
|
||||
|
if (setters is object) |
||||
|
{ |
||||
|
var setterCount = setters.Count; |
||||
|
|
||||
|
_setters = new List<ISetterInstance>(setterCount); |
||||
|
|
||||
|
for (var i = 0; i < setterCount; ++i) |
||||
|
{ |
||||
|
_setters.Add(setters[i].Instance(Target)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (animations is object && target is Animatable animatable) |
||||
|
{ |
||||
|
var animationsCount = animations.Count; |
||||
|
|
||||
|
_animations = new List<IDisposable>(animationsCount); |
||||
|
_animationTrigger = new Subject<bool>(); |
||||
|
|
||||
|
for (var i = 0; i < animationsCount; ++i) |
||||
|
{ |
||||
|
_animations.Add(animations[i].Apply(animatable, null, _animationTrigger)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IStyle Source { get; } |
||||
|
public IStyleable Target { get; } |
||||
|
|
||||
|
public void Start() |
||||
|
{ |
||||
|
var hasActivator = _activator is object; |
||||
|
|
||||
|
if (_setters is object) |
||||
|
{ |
||||
|
foreach (var setter in _setters) |
||||
|
{ |
||||
|
setter.Start(hasActivator); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (hasActivator) |
||||
|
{ |
||||
|
_activator!.Subscribe(this, 0); |
||||
|
} |
||||
|
else if (_animationTrigger != null) |
||||
|
{ |
||||
|
_animationTrigger.OnNext(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (_setters is object) |
||||
|
{ |
||||
|
foreach (var setter in _setters) |
||||
|
{ |
||||
|
setter.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (_animations is object) |
||||
|
{ |
||||
|
foreach (var subscripion in _animations) |
||||
|
{ |
||||
|
subscripion.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_activator?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
private void ActivatorChanged(bool value) |
||||
|
{ |
||||
|
if (_active != value) |
||||
|
{ |
||||
|
_active = value; |
||||
|
|
||||
|
_animationTrigger?.OnNext(value); |
||||
|
|
||||
|
if (_setters is object) |
||||
|
{ |
||||
|
if (_active) |
||||
|
{ |
||||
|
foreach (var setter in _setters) |
||||
|
{ |
||||
|
setter.Activate(); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
foreach (var setter in _setters) |
||||
|
{ |
||||
|
setter.Deactivate(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using Avalonia.Media; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Extensions for <see cref="AvaloniaProperty"/>.
|
||||
|
/// </summary>
|
||||
|
public static class AvaloniaPropertyExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Checks if values of given property can affect rendering (via <see cref="IAffectsRender"/>).
|
||||
|
/// </summary>
|
||||
|
/// <param name="property">Property to check.</param>
|
||||
|
public static bool CanValueAffectRender(this AvaloniaProperty property) |
||||
|
{ |
||||
|
var propertyType = property.PropertyType; |
||||
|
|
||||
|
// Only case that we are sure that property value CAN'T affect render are sealed types that don't implement
|
||||
|
// the interface.
|
||||
|
var cannotAffectRender = propertyType.IsSealed && !typeof(IAffectsRender).IsAssignableFrom(propertyType); |
||||
|
|
||||
|
return !cannotAffectRender; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue