committed by
GitHub
113 changed files with 2148 additions and 544 deletions
@ -0,0 +1,30 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reactive.Linq; |
||||
|
using System.Text; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
public class Clock : ClockBase |
||||
|
{ |
||||
|
public static IClock GlobalClock => AvaloniaLocator.Current.GetService<IGlobalClock>(); |
||||
|
|
||||
|
private IDisposable _parentSubscription; |
||||
|
|
||||
|
public Clock() |
||||
|
:this(GlobalClock) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public Clock(IClock parent) |
||||
|
{ |
||||
|
_parentSubscription = parent.Subscribe(Pulse); |
||||
|
} |
||||
|
|
||||
|
protected override void Stop() |
||||
|
{ |
||||
|
_parentSubscription?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reactive.Linq; |
||||
|
using System.Text; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
public class ClockBase : IClock |
||||
|
{ |
||||
|
private ClockObservable _observable; |
||||
|
|
||||
|
private IObservable<TimeSpan> _connectedObservable; |
||||
|
|
||||
|
private TimeSpan? _previousTime; |
||||
|
private TimeSpan _internalTime; |
||||
|
|
||||
|
protected ClockBase() |
||||
|
{ |
||||
|
_observable = new ClockObservable(); |
||||
|
_connectedObservable = _observable.Publish().RefCount(); |
||||
|
} |
||||
|
|
||||
|
protected bool HasSubscriptions => _observable.HasSubscriptions; |
||||
|
|
||||
|
public PlayState PlayState { get; set; } |
||||
|
|
||||
|
protected void Pulse(TimeSpan systemTime) |
||||
|
{ |
||||
|
if (!_previousTime.HasValue) |
||||
|
{ |
||||
|
_previousTime = systemTime; |
||||
|
_internalTime = TimeSpan.Zero; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (PlayState == PlayState.Pause) |
||||
|
{ |
||||
|
_previousTime = systemTime; |
||||
|
return; |
||||
|
} |
||||
|
var delta = systemTime - _previousTime; |
||||
|
_internalTime += delta.Value; |
||||
|
_previousTime = systemTime; |
||||
|
} |
||||
|
|
||||
|
_observable.Pulse(_internalTime); |
||||
|
|
||||
|
if (PlayState == PlayState.Stop) |
||||
|
{ |
||||
|
Stop(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual void Stop() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public IDisposable Subscribe(IObserver<TimeSpan> observer) |
||||
|
{ |
||||
|
return _connectedObservable.Subscribe(observer); |
||||
|
} |
||||
|
|
||||
|
private class ClockObservable : LightweightObservableBase<TimeSpan> |
||||
|
{ |
||||
|
public bool HasSubscriptions { get; private set; } |
||||
|
public void Pulse(TimeSpan time) => PublishNext(time); |
||||
|
protected override void Initialize() => HasSubscriptions = true; |
||||
|
protected override void Deinitialize() => HasSubscriptions = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
// 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.Linq; |
||||
|
using Avalonia.Animation.Utils; |
||||
|
using Avalonia.Collections; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Manages the lifetime of animation instances as determined by its selector state.
|
||||
|
/// </summary>
|
||||
|
internal class DisposeAnimationInstanceSubject<T> : IObserver<bool>, IDisposable |
||||
|
{ |
||||
|
private IDisposable _lastInstance; |
||||
|
private bool _lastMatch; |
||||
|
private Animator<T> _animator; |
||||
|
private Animation _animation; |
||||
|
private Animatable _control; |
||||
|
private Action _onComplete; |
||||
|
private IClock _clock; |
||||
|
|
||||
|
public DisposeAnimationInstanceSubject(Animator<T> animator, Animation animation, Animatable control, IClock clock, Action onComplete) |
||||
|
{ |
||||
|
this._animator = animator; |
||||
|
this._animation = animation; |
||||
|
this._control = control; |
||||
|
this._onComplete = onComplete; |
||||
|
this._clock = clock; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_lastInstance?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
public void OnCompleted() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void OnError(Exception error) |
||||
|
{ |
||||
|
_lastInstance?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
void IObserver<bool>.OnNext(bool matchVal) |
||||
|
{ |
||||
|
if (matchVal != _lastMatch) |
||||
|
{ |
||||
|
_lastInstance?.Dispose(); |
||||
|
if (matchVal) |
||||
|
{ |
||||
|
_lastInstance = _animator.Run(_animation, _control, _clock, _onComplete); |
||||
|
} |
||||
|
_lastMatch = matchVal; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
public interface IClock : IObservable<TimeSpan> |
||||
|
{ |
||||
|
PlayState PlayState { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
public interface IGlobalClock : IClock |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -1,54 +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.Linq; |
|
||||
using System.Reactive.Linq; |
|
||||
using Avalonia.Threading; |
|
||||
|
|
||||
namespace Avalonia.Animation |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Provides global timing functions for animations.
|
|
||||
/// </summary>
|
|
||||
public static class Timing |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// The number of frames per second.
|
|
||||
/// </summary>
|
|
||||
public const int FramesPerSecond = 60; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// The time span of each frame.
|
|
||||
/// </summary>
|
|
||||
internal static readonly TimeSpan FrameTick = TimeSpan.FromSeconds(1.0 / FramesPerSecond); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes static members of the <see cref="Timing"/> class.
|
|
||||
/// </summary>
|
|
||||
static Timing() |
|
||||
{ |
|
||||
var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance); |
|
||||
|
|
||||
AnimationsTimer = globalTimer |
|
||||
.Select(_ => GetTickCount()) |
|
||||
.Publish() |
|
||||
.RefCount(); |
|
||||
} |
|
||||
|
|
||||
internal static TimeSpan GetTickCount() => TimeSpan.FromMilliseconds(Environment.TickCount); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the animation timer.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// The animation timer triggers usually at 60 times per second or as
|
|
||||
/// defined in <see cref="FramesPerSecond"/>.
|
|
||||
/// The parameter passed to a subsciber is the current playstate of the animation.
|
|
||||
/// </remarks>
|
|
||||
internal static IObservable<TimeSpan> AnimationsTimer |
|
||||
{ |
|
||||
get; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,38 @@ |
|||||
|
// 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.Interactivity; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.Models |
||||
|
{ |
||||
|
internal class EventChainLink |
||||
|
{ |
||||
|
public EventChainLink(object handler, bool handled, RoutingStrategies route) |
||||
|
{ |
||||
|
Contract.Requires<ArgumentNullException>(handler != null); |
||||
|
|
||||
|
this.Handler = handler; |
||||
|
this.Handled = handled; |
||||
|
this.Route = route; |
||||
|
} |
||||
|
|
||||
|
public object Handler { get; } |
||||
|
|
||||
|
public string HandlerName |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (Handler is INamed named && !string.IsNullOrEmpty(named.Name)) |
||||
|
{ |
||||
|
return named.Name + " (" + Handler.GetType().Name + ")"; |
||||
|
} |
||||
|
return Handler.GetType().Name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool Handled { get; } |
||||
|
|
||||
|
public RoutingStrategies Route { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
// 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 Avalonia.Collections; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Interactivity; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.ViewModels |
||||
|
{ |
||||
|
internal class EventOwnerTreeNode : EventTreeNodeBase |
||||
|
{ |
||||
|
private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[] |
||||
|
{ |
||||
|
Button.ClickEvent, |
||||
|
InputElement.KeyDownEvent, |
||||
|
InputElement.KeyUpEvent, |
||||
|
InputElement.TextInputEvent, |
||||
|
InputElement.PointerReleasedEvent, |
||||
|
InputElement.PointerPressedEvent, |
||||
|
}; |
||||
|
|
||||
|
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsViewModel vm) |
||||
|
: base(null, type.Name) |
||||
|
{ |
||||
|
this.Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name) |
||||
|
.Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) })); |
||||
|
this.IsExpanded = true; |
||||
|
} |
||||
|
|
||||
|
public override bool? IsEnabled |
||||
|
{ |
||||
|
get => base.IsEnabled; |
||||
|
set |
||||
|
{ |
||||
|
if (base.IsEnabled != value) |
||||
|
{ |
||||
|
base.IsEnabled = value; |
||||
|
if (_updateChildren && value != null) |
||||
|
{ |
||||
|
foreach (var child in Children) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
child._updateParent = false; |
||||
|
child.IsEnabled = value; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
child._updateParent = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
// 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.Diagnostics.Models; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Threading; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.ViewModels |
||||
|
{ |
||||
|
internal class EventTreeNode : EventTreeNodeBase |
||||
|
{ |
||||
|
private RoutedEvent _event; |
||||
|
private EventsViewModel _parentViewModel; |
||||
|
private bool _isRegistered; |
||||
|
private FiredEvent _currentEvent; |
||||
|
|
||||
|
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsViewModel vm) |
||||
|
: base(parent, @event.Name) |
||||
|
{ |
||||
|
Contract.Requires<ArgumentNullException>(@event != null); |
||||
|
Contract.Requires<ArgumentNullException>(vm != null); |
||||
|
|
||||
|
this._event = @event; |
||||
|
this._parentViewModel = vm; |
||||
|
} |
||||
|
|
||||
|
public override bool? IsEnabled |
||||
|
{ |
||||
|
get => base.IsEnabled; |
||||
|
set |
||||
|
{ |
||||
|
if (base.IsEnabled != value) |
||||
|
{ |
||||
|
base.IsEnabled = value; |
||||
|
UpdateTracker(); |
||||
|
if (Parent != null && _updateParent) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
Parent._updateChildren = false; |
||||
|
Parent.UpdateChecked(); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
Parent._updateChildren = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void UpdateTracker() |
||||
|
{ |
||||
|
if (IsEnabled.GetValueOrDefault() && !_isRegistered) |
||||
|
{ |
||||
|
_event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true); |
||||
|
_isRegistered = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void HandleEvent(object sender, RoutedEventArgs e) |
||||
|
{ |
||||
|
if (!_isRegistered || IsEnabled == false) |
||||
|
return; |
||||
|
if (sender is IVisual v && DevTools.BelongsToDevTool(v)) |
||||
|
return; |
||||
|
|
||||
|
var s = sender; |
||||
|
var handled = e.Handled; |
||||
|
var route = e.Route; |
||||
|
|
||||
|
Action handler = delegate |
||||
|
{ |
||||
|
if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e)) |
||||
|
{ |
||||
|
_currentEvent = new FiredEvent(e, new EventChainLink(s, handled, route)); |
||||
|
|
||||
|
_parentViewModel.RecordedEvents.Add(_currentEvent); |
||||
|
|
||||
|
while (_parentViewModel.RecordedEvents.Count > 100) |
||||
|
_parentViewModel.RecordedEvents.RemoveAt(0); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_currentEvent.AddToChain(new EventChainLink(s, handled, route)); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
if (!Dispatcher.UIThread.CheckAccess()) |
||||
|
Dispatcher.UIThread.Post(handler); |
||||
|
else |
||||
|
handler(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
// 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 Avalonia.Collections; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.ViewModels |
||||
|
{ |
||||
|
internal abstract class EventTreeNodeBase : ViewModelBase |
||||
|
{ |
||||
|
internal bool _updateChildren = true; |
||||
|
internal bool _updateParent = true; |
||||
|
private bool _isExpanded; |
||||
|
private bool? _isEnabled = false; |
||||
|
|
||||
|
public EventTreeNodeBase(EventTreeNodeBase parent, string text) |
||||
|
{ |
||||
|
this.Parent = parent; |
||||
|
this.Text = text; |
||||
|
} |
||||
|
|
||||
|
public IAvaloniaReadOnlyList<EventTreeNodeBase> Children |
||||
|
{ |
||||
|
get; |
||||
|
protected set; |
||||
|
} |
||||
|
|
||||
|
public bool IsExpanded |
||||
|
{ |
||||
|
get { return _isExpanded; } |
||||
|
set { RaiseAndSetIfChanged(ref _isExpanded, value); } |
||||
|
} |
||||
|
|
||||
|
public virtual bool? IsEnabled |
||||
|
{ |
||||
|
get { return _isEnabled; } |
||||
|
set { RaiseAndSetIfChanged(ref _isEnabled, value); } |
||||
|
} |
||||
|
|
||||
|
public EventTreeNodeBase Parent |
||||
|
{ |
||||
|
get; |
||||
|
} |
||||
|
|
||||
|
public string Text |
||||
|
{ |
||||
|
get; |
||||
|
private set; |
||||
|
} |
||||
|
|
||||
|
internal void UpdateChecked() |
||||
|
{ |
||||
|
IsEnabled = GetValue(); |
||||
|
|
||||
|
bool? GetValue() |
||||
|
{ |
||||
|
if (Children == null) |
||||
|
return false; |
||||
|
bool? value = false; |
||||
|
for (int i = 0; i < Children.Count; i++) |
||||
|
{ |
||||
|
if (i == 0) |
||||
|
{ |
||||
|
value = Children[i].IsEnabled; |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (value != Children[i].IsEnabled) |
||||
|
{ |
||||
|
value = null; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
// 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.ObjectModel; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
using System.Windows.Input; |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Data.Converters; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.ViewModels |
||||
|
{ |
||||
|
internal class EventsViewModel : ViewModelBase |
||||
|
{ |
||||
|
private readonly IControl _root; |
||||
|
private FiredEvent _selectedEvent; |
||||
|
|
||||
|
public EventsViewModel(IControl root) |
||||
|
{ |
||||
|
this._root = root; |
||||
|
this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered() |
||||
|
.GroupBy(e => e.OwnerType) |
||||
|
.OrderBy(e => e.Key.Name) |
||||
|
.Select(g => new EventOwnerTreeNode(g.Key, g, this)) |
||||
|
.ToArray(); |
||||
|
} |
||||
|
|
||||
|
public EventTreeNodeBase[] Nodes { get; } |
||||
|
|
||||
|
public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>(); |
||||
|
|
||||
|
public FiredEvent SelectedEvent |
||||
|
{ |
||||
|
get => _selectedEvent; |
||||
|
set => RaiseAndSetIfChanged(ref _selectedEvent, value); |
||||
|
} |
||||
|
|
||||
|
private void Clear() |
||||
|
{ |
||||
|
RecordedEvents.Clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal class BoolToBrushConverter : IValueConverter |
||||
|
{ |
||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) |
||||
|
{ |
||||
|
return (bool)value ? Brushes.LightGreen : Brushes.Transparent; |
||||
|
} |
||||
|
|
||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
// 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.ObjectModel; |
||||
|
|
||||
|
using Avalonia.Diagnostics.Models; |
||||
|
using Avalonia.Interactivity; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.ViewModels |
||||
|
{ |
||||
|
internal class FiredEvent : ViewModelBase |
||||
|
{ |
||||
|
private RoutedEventArgs _eventArgs; |
||||
|
private EventChainLink _handledBy; |
||||
|
|
||||
|
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator) |
||||
|
{ |
||||
|
Contract.Requires<ArgumentNullException>(eventArgs != null); |
||||
|
Contract.Requires<ArgumentNullException>(originator != null); |
||||
|
|
||||
|
this._eventArgs = eventArgs; |
||||
|
this.Originator = originator; |
||||
|
AddToChain(originator); |
||||
|
} |
||||
|
|
||||
|
public bool IsPartOfSameEventChain(RoutedEventArgs e) |
||||
|
{ |
||||
|
return e == _eventArgs; |
||||
|
} |
||||
|
|
||||
|
public RoutedEvent Event => _eventArgs.RoutedEvent; |
||||
|
|
||||
|
public bool IsHandled => HandledBy?.Handled == true; |
||||
|
|
||||
|
public ObservableCollection<EventChainLink> EventChain { get; } = new ObservableCollection<EventChainLink>(); |
||||
|
|
||||
|
public string DisplayText |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (IsHandled) |
||||
|
{ |
||||
|
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + |
||||
|
$"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; |
||||
|
} |
||||
|
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public EventChainLink Originator { get; } |
||||
|
|
||||
|
public EventChainLink HandledBy |
||||
|
{ |
||||
|
get { return _handledBy; } |
||||
|
set |
||||
|
{ |
||||
|
if (_handledBy != value) |
||||
|
{ |
||||
|
_handledBy = value; |
||||
|
RaisePropertyChanged(); |
||||
|
RaisePropertyChanged(nameof(IsHandled)); |
||||
|
RaisePropertyChanged(nameof(DisplayText)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void AddToChain(object handler, bool handled, RoutingStrategies route) |
||||
|
{ |
||||
|
AddToChain(new EventChainLink(handler, handled, route)); |
||||
|
} |
||||
|
|
||||
|
public void AddToChain(EventChainLink link) |
||||
|
{ |
||||
|
EventChain.Add(link); |
||||
|
if (HandledBy == null && link.Handled) |
||||
|
HandledBy = link; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"> |
||||
|
<UserControl.Resources> |
||||
|
<vm:BoolToBrushConverter x:Key="boolToBrush" /> |
||||
|
</UserControl.Resources> |
||||
|
<Grid ColumnDefinitions="*,4,3*"> |
||||
|
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" Grid.RowSpan="2"> |
||||
|
<TreeView.DataTemplates> |
||||
|
<TreeDataTemplate DataType="vm:EventTreeNodeBase" |
||||
|
ItemsSource="{Binding Children}"> |
||||
|
<CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" /> |
||||
|
</TreeDataTemplate> |
||||
|
</TreeView.DataTemplates> |
||||
|
<TreeView.Styles> |
||||
|
<Style Selector="TreeViewItem"> |
||||
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/> |
||||
|
</Style> |
||||
|
</TreeView.Styles> |
||||
|
</TreeView> |
||||
|
|
||||
|
<GridSplitter Width="4" Grid.Column="1" /> |
||||
|
<Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2"> |
||||
|
<ListBox Name="eventsList" Items="{Binding RecordedEvents}" SelectedItem="{Binding SelectedEvent, Mode=TwoWay}"> |
||||
|
<ListBox.ItemTemplate> |
||||
|
<DataTemplate> |
||||
|
<TextBlock Background="{Binding IsHandled, Converter={StaticResource boolToBrush}}" Text="{Binding DisplayText}" /> |
||||
|
</DataTemplate> |
||||
|
</ListBox.ItemTemplate> |
||||
|
</ListBox> |
||||
|
<GridSplitter Height="4" Grid.Row="1" /> |
||||
|
<DockPanel Grid.Row="2" LastChildFill="True"> |
||||
|
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" /> |
||||
|
<ListBox Items="{Binding SelectedEvent.EventChain}"> |
||||
|
<ListBox.ItemTemplate> |
||||
|
<DataTemplate> |
||||
|
<StackPanel Orientation="Horizontal" Background="{Binding Handled, Converter={StaticResource boolToBrush}}"> |
||||
|
<TextBlock Text="{Binding Route}" /> |
||||
|
<TextBlock Text=": " /> |
||||
|
<TextBlock Text="{Binding HandlerName}" /> |
||||
|
<TextBlock Text=" handled: " /> |
||||
|
<TextBlock Text="{Binding Handled}" /> |
||||
|
</StackPanel> |
||||
|
</DataTemplate> |
||||
|
</ListBox.ItemTemplate> |
||||
|
</ListBox> |
||||
|
</DockPanel> |
||||
|
<StackPanel Orientation="Horizontal" Grid.Row="3"> |
||||
|
<Button Content="Clear" Margin="3" Command="{Binding Clear}" /> |
||||
|
</StackPanel> |
||||
|
</Grid> |
||||
|
</Grid> |
||||
|
</UserControl> |
||||
@ -0,0 +1,32 @@ |
|||||
|
// 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.Linq; |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Diagnostics.ViewModels; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Avalonia.Diagnostics.Views |
||||
|
{ |
||||
|
public class EventsView : UserControl |
||||
|
{ |
||||
|
private ListBox _events; |
||||
|
|
||||
|
public EventsView() |
||||
|
{ |
||||
|
this.InitializeComponent(); |
||||
|
_events = this.FindControl<ListBox>("events"); |
||||
|
} |
||||
|
|
||||
|
private void RecordedEvents_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) |
||||
|
{ |
||||
|
_events.ScrollIntoView(_events.Items.OfType<FiredEvent>().LastOrDefault()); |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
using Avalonia.Rendering; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
public class RenderLoopClock : ClockBase, IRenderLoopTask, IGlobalClock |
||||
|
{ |
||||
|
protected override void Stop() |
||||
|
{ |
||||
|
AvaloniaLocator.Current.GetService<IRenderLoop>().Remove(this); |
||||
|
} |
||||
|
|
||||
|
bool IRenderLoopTask.NeedsUpdate => HasSubscriptions; |
||||
|
|
||||
|
void IRenderLoopTask.Render() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
void IRenderLoopTask.Update(TimeSpan time) |
||||
|
{ |
||||
|
Pulse(time); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Collections; |
||||
|
using Avalonia.Media.Immutable; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A collection of <see cref="GradientStop"/>s.
|
||||
|
/// </summary>
|
||||
|
public class GradientStops : AvaloniaList<GradientStop> |
||||
|
{ |
||||
|
public GradientStops() |
||||
|
{ |
||||
|
ResetBehavior = ResetBehavior.Remove; |
||||
|
} |
||||
|
|
||||
|
public IReadOnlyList<ImmutableGradientStop> ToImmutable() |
||||
|
{ |
||||
|
return this.Select(x => new ImmutableGradientStop(x.Offset, x.Color)).ToList(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Signals to a self-rendering control that changes to the resource should invoke
|
||||
|
/// <see cref="Visual.InvalidateVisual"/>.
|
||||
|
/// </summary>
|
||||
|
public interface IAffectsRender |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Raised when the resource changes visually.
|
||||
|
/// </summary>
|
||||
|
event EventHandler Invalidated; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Describes the location and color of a transition point in a gradient.
|
||||
|
/// </summary>
|
||||
|
public interface IGradientStop |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the gradient stop color.
|
||||
|
/// </summary>
|
||||
|
Color Color { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the gradient stop offset.
|
||||
|
/// </summary>
|
||||
|
double Offset { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
namespace Avalonia.Media.Immutable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Describes the location and color of a transition point in a gradient.
|
||||
|
/// </summary>
|
||||
|
public class ImmutableGradientStop : IGradientStop |
||||
|
{ |
||||
|
public ImmutableGradientStop(double offset, Color color) |
||||
|
{ |
||||
|
Offset = offset; |
||||
|
Color = color; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public double Offset { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public Color Color { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
namespace Avalonia.Rendering |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An interface to allow non-templated controls to customize their hit-testing
|
||||
|
/// when using a renderer with a simple hit-testing algorithm without a scene graph,
|
||||
|
/// such as <see cref="ImmediateRenderer" />
|
||||
|
/// </summary>
|
||||
|
public interface ICustomSimpleHitTest |
||||
|
{ |
||||
|
bool HitTest(Point point); |
||||
|
} |
||||
|
} |
||||
@ -1,19 +1,28 @@ |
|||||
using System; |
namespace Avalonia.Rendering |
||||
|
|
||||
namespace Avalonia.Rendering |
|
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Defines the interface implemented by an application render loop.
|
/// The application render loop.
|
||||
/// </summary>
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The render loop is responsible for advancing the animation timer and updating the scene
|
||||
|
/// graph for visible windows.
|
||||
|
/// </remarks>
|
||||
public interface IRenderLoop |
public interface IRenderLoop |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Raised when the render loop ticks to signal a new frame should be drawn.
|
/// Adds an update task.
|
||||
/// </summary>
|
/// </summary>
|
||||
|
/// <param name="i">The update task.</param>
|
||||
/// <remarks>
|
/// <remarks>
|
||||
/// This event can be raised on any thread; it is the responsibility of the subscriber to
|
/// Registered update tasks will be polled on each tick of the render loop after the
|
||||
/// switch execution to the right thread.
|
/// animation timer has been pulsed.
|
||||
/// </remarks>
|
/// </remarks>
|
||||
event EventHandler<EventArgs> Tick; |
void Add(IRenderLoopTask i); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes an update task.
|
||||
|
/// </summary>
|
||||
|
/// <param name="i">The update task.</param>
|
||||
|
void Remove(IRenderLoopTask i); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,12 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.Rendering |
||||
|
{ |
||||
|
public interface IRenderLoopTask |
||||
|
{ |
||||
|
bool NeedsUpdate { get; } |
||||
|
void Update(TimeSpan time); |
||||
|
void Render(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.Rendering |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines the interface implemented by an application render timer.
|
||||
|
/// </summary>
|
||||
|
public interface IRenderTimer |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Raised when the render timer ticks to signal a new frame should be drawn.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// This event can be raised on any thread; it is the responsibility of the subscriber to
|
||||
|
/// switch execution to the right thread.
|
||||
|
/// </remarks>
|
||||
|
event Action<TimeSpan> Tick; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,120 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Logging; |
||||
|
using Avalonia.Threading; |
||||
|
|
||||
|
namespace Avalonia.Rendering |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The application render loop.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The render loop is responsible for advancing the animation timer and updating the scene
|
||||
|
/// graph for visible windows.
|
||||
|
/// </remarks>
|
||||
|
public class RenderLoop : IRenderLoop |
||||
|
{ |
||||
|
private readonly IDispatcher _dispatcher; |
||||
|
private List<IRenderLoopTask> _items = new List<IRenderLoopTask>(); |
||||
|
private IRenderTimer _timer; |
||||
|
private int inTick; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="RenderLoop"/> class.
|
||||
|
/// </summary>
|
||||
|
public RenderLoop() |
||||
|
{ |
||||
|
_dispatcher = Dispatcher.UIThread; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="RenderLoop"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="timer">The render timer.</param>
|
||||
|
/// <param name="dispatcher">The UI thread dispatcher.</param>
|
||||
|
public RenderLoop(IRenderTimer timer, IDispatcher dispatcher) |
||||
|
{ |
||||
|
_timer = timer; |
||||
|
_dispatcher = dispatcher; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the render timer.
|
||||
|
/// </summary>
|
||||
|
protected IRenderTimer Timer |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (_timer == null) |
||||
|
{ |
||||
|
_timer = AvaloniaLocator.Current.GetService<IRenderTimer>(); |
||||
|
} |
||||
|
|
||||
|
return _timer; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Add(IRenderLoopTask i) |
||||
|
{ |
||||
|
Contract.Requires<ArgumentNullException>(i != null); |
||||
|
Dispatcher.UIThread.VerifyAccess(); |
||||
|
|
||||
|
_items.Add(i); |
||||
|
|
||||
|
if (_items.Count == 1) |
||||
|
{ |
||||
|
Timer.Tick += TimerTick; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Remove(IRenderLoopTask i) |
||||
|
{ |
||||
|
Contract.Requires<ArgumentNullException>(i != null); |
||||
|
Dispatcher.UIThread.VerifyAccess(); |
||||
|
|
||||
|
_items.Remove(i); |
||||
|
|
||||
|
if (_items.Count == 0) |
||||
|
{ |
||||
|
Timer.Tick -= TimerTick; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async void TimerTick(TimeSpan time) |
||||
|
{ |
||||
|
if (Interlocked.CompareExchange(ref inTick, 1, 0) == 0) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (_items.Any(item => item.NeedsUpdate)) |
||||
|
{ |
||||
|
await _dispatcher.InvokeAsync(() => |
||||
|
{ |
||||
|
foreach (var i in _items) |
||||
|
{ |
||||
|
i.Update(time); |
||||
|
} |
||||
|
}, DispatcherPriority.Render).ConfigureAwait(false); |
||||
|
} |
||||
|
|
||||
|
foreach (var i in _items) |
||||
|
{ |
||||
|
i.Render(); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Logger.Error(LogArea.Visual, this, "Exception in render loop: {Error}", ex); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
Interlocked.Exchange(ref inTick, 0); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,31 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Rendering; |
|
||||
using MonoMac.Foundation; |
|
||||
|
|
||||
namespace Avalonia.MonoMac |
|
||||
{ |
|
||||
//TODO: Switch to using CVDisplayLink
|
|
||||
public class RenderLoop : IRenderLoop |
|
||||
{ |
|
||||
private readonly object _lock = new object(); |
|
||||
private readonly IDisposable _timer; |
|
||||
|
|
||||
public RenderLoop() |
|
||||
{ |
|
||||
_timer = AvaloniaLocator.Current.GetService<IRuntimePlatform>().StartSystemTimer(new TimeSpan(0, 0, 0, 0, 1000 / 60), |
|
||||
() => |
|
||||
{ |
|
||||
lock (_lock) |
|
||||
{ |
|
||||
using (new NSAutoreleasePool()) |
|
||||
{ |
|
||||
Tick?.Invoke(this, EventArgs.Empty); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public event EventHandler<EventArgs> Tick; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,28 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering; |
||||
|
using MonoMac.Foundation; |
||||
|
|
||||
|
namespace Avalonia.MonoMac |
||||
|
{ |
||||
|
//TODO: Switch to using CVDisplayLink
|
||||
|
public class RenderTimer : DefaultRenderTimer |
||||
|
{ |
||||
|
public RenderTimer(int framesPerSecond) : base(framesPerSecond) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override IDisposable StartCore(Action<TimeSpan> tick) |
||||
|
{ |
||||
|
return AvaloniaLocator.Current.GetService<IRuntimePlatform>().StartSystemTimer( |
||||
|
TimeSpan.FromSeconds(1.0 / FramesPerSecond), |
||||
|
() => |
||||
|
{ |
||||
|
using (new NSAutoreleasePool()) |
||||
|
{ |
||||
|
tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,35 +0,0 @@ |
|||||
using System; |
|
||||
using System.Reactive.Disposables; |
|
||||
using Avalonia.Rendering; |
|
||||
using Avalonia.Win32.Interop; |
|
||||
|
|
||||
namespace Avalonia.Win32 |
|
||||
{ |
|
||||
internal class RenderLoop : DefaultRenderLoop |
|
||||
{ |
|
||||
private UnmanagedMethods.TimeCallback timerDelegate; |
|
||||
|
|
||||
public RenderLoop(int framesPerSecond) |
|
||||
: base(framesPerSecond) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected override IDisposable StartCore(Action tick) |
|
||||
{ |
|
||||
timerDelegate = (id, uMsg, user, dw1, dw2) => tick(); |
|
||||
|
|
||||
var handle = UnmanagedMethods.timeSetEvent( |
|
||||
(uint)(1000 / FramesPerSecond), |
|
||||
0, |
|
||||
timerDelegate, |
|
||||
UIntPtr.Zero, |
|
||||
1); |
|
||||
|
|
||||
return Disposable.Create(() => |
|
||||
{ |
|
||||
timerDelegate = null; |
|
||||
UnmanagedMethods.timeKillEvent(handle); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,56 @@ |
|||||
|
using System; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Win32.Interop; |
||||
|
|
||||
|
namespace Avalonia.Win32 |
||||
|
{ |
||||
|
internal class RenderTimer : DefaultRenderTimer |
||||
|
{ |
||||
|
private UnmanagedMethods.WaitOrTimerCallback timerDelegate; |
||||
|
|
||||
|
private static IntPtr _timerQueue; |
||||
|
|
||||
|
private static void EnsureTimerQueueCreated() |
||||
|
{ |
||||
|
if (Volatile.Read(ref _timerQueue) == null) |
||||
|
{ |
||||
|
var queue = UnmanagedMethods.CreateTimerQueue(); |
||||
|
if (Interlocked.CompareExchange(ref _timerQueue, queue, IntPtr.Zero) != IntPtr.Zero) |
||||
|
{ |
||||
|
UnmanagedMethods.DeleteTimerQueueEx(queue, IntPtr.Zero); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public RenderTimer(int framesPerSecond) |
||||
|
: base(framesPerSecond) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override IDisposable StartCore(Action<TimeSpan> tick) |
||||
|
{ |
||||
|
EnsureTimerQueueCreated(); |
||||
|
var msPerFrame = 1000 / FramesPerSecond; |
||||
|
|
||||
|
timerDelegate = (_, __) => tick(TimeSpan.FromMilliseconds(Environment.TickCount)); |
||||
|
|
||||
|
UnmanagedMethods.CreateTimerQueueTimer( |
||||
|
out var timer, |
||||
|
_timerQueue, |
||||
|
timerDelegate, |
||||
|
IntPtr.Zero, |
||||
|
(uint)msPerFrame, |
||||
|
(uint)msPerFrame, |
||||
|
0 |
||||
|
); |
||||
|
|
||||
|
return Disposable.Create(() => |
||||
|
{ |
||||
|
timerDelegate = null; |
||||
|
UnmanagedMethods.DeleteTimerQueueTimer(_timerQueue, timer, IntPtr.Zero); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Controls.UnitTests |
||||
|
{ |
||||
|
public class MenuItemTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Header_Of_Minus_Should_Apply_Separator_Pseudoclass() |
||||
|
{ |
||||
|
var target = new MenuItem { Header = "-" }; |
||||
|
|
||||
|
Assert.True(target.Classes.Contains(":separator")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Separator_Item_Should_Set_Focusable_False() |
||||
|
{ |
||||
|
var target = new MenuItem { Header = "-" }; |
||||
|
|
||||
|
Assert.False(target.Focusable); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue