Browse Source

Merge branch 'master' into DotNetCoreRuntimeD2D1

pull/1805/head
Wiesław Šoltés 8 years ago
committed by GitHub
parent
commit
9a1cae4d0d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 73
      src/Avalonia.Animation/Animation.cs
  2. 2
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  3. 7
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  4. 9
      src/Avalonia.Animation/Animator`1.cs
  5. 10
      src/Avalonia.Animation/IAnimation.cs
  6. 2
      src/Avalonia.Animation/IAnimator.cs
  7. 71
      src/Avalonia.Controls/ContextMenu.cs
  8. 6
      src/Avalonia.Controls/Expander.cs
  9. 2
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  10. 5
      src/Avalonia.Themes.Default/MenuItem.xaml
  11. 60
      src/Avalonia.Visuals/Animation/CrossFade.cs
  12. 2
      src/Avalonia.Visuals/Animation/IPageTransition.cs
  13. 64
      src/Avalonia.Visuals/Animation/PageSlide.cs
  14. 6
      src/Avalonia.Visuals/Animation/TransformAnimator.cs
  15. 25
      src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs
  16. 58
      src/OSX/Avalonia.MonoMac/WindowImpl.cs
  17. 179
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  18. 26
      tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs

73
src/Avalonia.Animation/Animation.cs

@ -10,13 +10,16 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reflection;
using System.Linq;
using System.Threading.Tasks;
using System.Reactive.Linq;
using System.Reactive.Disposables;
namespace Avalonia.Animation
{
/// <summary>
/// Tracks the progress of an animation.
/// </summary>
public class Animation : AvaloniaList<KeyFrame>, IDisposable, IAnimation
public class Animation : AvaloniaList<KeyFrame>, IAnimation
{
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
{
@ -24,7 +27,7 @@ namespace Avalonia.Animation
};
public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
where TAnimator: IAnimator
where TAnimator : IAnimator
{
Animators.Insert(0, (condition, typeof(TAnimator)));
}
@ -41,8 +44,6 @@ namespace Avalonia.Animation
return null;
}
private bool _isChildrenChanged = false;
private List<IDisposable> _subscription = new List<IDisposable>();
public AvaloniaList<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>();
/// <summary>
@ -72,18 +73,14 @@ namespace Avalonia.Animation
/// <summary>
/// Easing function to be used.
/// </summary>
/// </summary>
public Easing Easing { get; set; } = new LinearEasing();
public Animation()
{
this.CollectionChanged += delegate { _isChildrenChanged = true; };
}
private IList<IAnimator> InterpretKeyframes(Animatable control)
private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
{
var handlerList = new List<(Type type, AvaloniaProperty property)>();
var animatorKeyFrames = new List<AnimatorKeyFrame>();
var subscriptions = new List<IDisposable>();
foreach (var keyframe in this)
{
@ -108,7 +105,7 @@ namespace Avalonia.Animation
var newKF = new AnimatorKeyFrame(handler, cue);
_subscription.Add(newKF.BindSetter(setter, control));
subscriptions.Add(newKF.BindSetter(setter, control));
animatorKeyFrames.Add(newKF);
}
@ -130,28 +127,56 @@ namespace Avalonia.Animation
animator.Add(keyframe);
}
return newAnimatorInstances;
return (newAnimatorInstances, subscriptions);
}
/// <summary>
/// Cancels the animation.
/// </summary>
public void Dispose()
/// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete)
{
foreach (var sub in _subscription)
var (animators, subscriptions) = InterpretKeyframes(control);
if (animators.Count == 1)
{
sub.Dispose();
subscriptions.Add(animators[0].Apply(this, control, match, onComplete));
}
else
{
var completionTasks = onComplete != null ? new List<Task>() : null;
foreach (IAnimator animator in animators)
{
Action animatorOnComplete = null;
if (onComplete != null)
{
var tcs = new TaskCompletionSource<object>();
animatorOnComplete = () => tcs.SetResult(null);
completionTasks.Add(tcs.Task);
}
subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete));
}
if (onComplete != null)
{
Task.WhenAll(completionTasks).ContinueWith(_ => onComplete());
}
}
return new CompositeDisposable(subscriptions);
}
/// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> matchObs)
public Task RunAsync(Animatable control)
{
foreach (IAnimator animator in InterpretKeyframes(control))
var run = new TaskCompletionSource<object>();
if (this.RepeatCount == RepeatCount.Loop)
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null;
subscriptions = this.Apply(control, Observable.Return(true), () =>
{
_subscription.Add(animator.Apply(this, control, matchObs));
}
return this;
run.SetResult(null);
subscriptions?.Dispose();
});
return run.Task;
}
}
}

2
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -16,7 +16,7 @@ namespace Avalonia.Animation
public class AnimatorKeyFrame : AvaloniaObject
{
public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k._value, (k, v) => k._value = v);
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public AnimatorKeyFrame()
{

7
src/Avalonia.Animation/AnimatorStateMachine`1.cs

@ -35,6 +35,7 @@ namespace Avalonia.Animation
private T _neutralValue;
internal bool _unsubscribe = false;
private IObserver<object> _targetObserver;
private readonly Action _onComplete;
[Flags]
private enum KeyFramesStates
@ -51,7 +52,7 @@ namespace Avalonia.Animation
Disposed
}
public void Initialize(Animation animation, Animatable control, Animator<T> animator)
public AnimatorStateMachine(Animation animation, Animatable control, Animator<T> animator, Action onComplete)
{
_parent = animator;
_targetAnimation = animation;
@ -82,6 +83,7 @@ namespace Avalonia.Animation
_currentState = KeyFramesStates.DoDelay;
else
_currentState = KeyFramesStates.DoRun;
_onComplete = onComplete;
}
public void Step(PlayState _playState, Func<double, T, T> Interpolator)
@ -243,7 +245,10 @@ namespace Avalonia.Animation
{
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
}
_targetObserver.OnCompleted();
_onComplete?.Invoke();
Dispose();
handled = true;
break;
default:

9
src/Avalonia.Animation/Animator`1.cs

@ -35,7 +35,7 @@ namespace Avalonia.Animation
}
/// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete)
{
if (!_isVerfifiedAndConverted)
VerifyConvertKeyFrames();
@ -45,7 +45,7 @@ namespace Avalonia.Animation
.Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ =>
{
var timerObs = RunKeyFrames(animation, control);
var timerObs = RunKeyFrames(animation, control, onComplete);
});
}
@ -97,10 +97,9 @@ namespace Avalonia.Animation
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control)
private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete)
{
var stateMachine = new AnimatorStateMachine<T>();
stateMachine.Initialize(animation, control, this);
var stateMachine = new AnimatorStateMachine<T>(animation, control, this, onComplete);
Timing.AnimationStateTimer
.TakeWhile(_ => !stateMachine._unsubscribe)

10
src/Avalonia.Animation/IAnimation.cs

@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Animation
{
@ -12,6 +13,11 @@ namespace Avalonia.Animation
/// <summary>
/// Apply the animation to the specified control
/// </summary>
IDisposable Apply(Animatable control, IObservable<bool> match);
IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete = null);
/// <summary>
/// Run the animation to the specified control
/// </summary>
Task RunAsync(Animatable control);
}
}

2
src/Avalonia.Animation/IAnimator.cs

@ -17,6 +17,6 @@ namespace Avalonia.Animation
/// <summary>
/// Applies the current KeyFrame group to the specified control.
/// </summary>
IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch);
IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete);
}
}

71
src/Avalonia.Controls/ContextMenu.cs

@ -7,11 +7,20 @@ namespace Avalonia.Controls
using System;
using System.Reactive.Linq;
using System.Linq;
using System.ComponentModel;
public class ContextMenu : SelectingItemsControl
{
private bool _isOpen;
private Popup _popup;
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly DirectProperty<ContextMenu, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<ContextMenu, bool>(nameof(IsOpen), o => o.IsOpen);
/// <summary>
/// Initializes static members of the <see cref="ContextMenu"/> class.
/// </summary>
@ -22,6 +31,26 @@ namespace Avalonia.Controls
MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true);
}
/// <summary>
/// Gets a value indicating whether the popup is open
/// </summary>
public bool IsOpen => _isOpen;
/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from false to true.
/// </summary>
public event CancelEventHandler ContextMenuOpening;
/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from true to false.
/// </summary>
public event CancelEventHandler ContextMenuClosing;
/// <summary>
/// Called when the <see cref="Control.ContextMenu"/> property changes on a control.
/// </summary>
@ -59,12 +88,12 @@ namespace Avalonia.Controls
{
if (_popup != null && _popup.IsVisible)
{
_popup.Close();
_popup.IsOpen = false;
}
SelectedIndex = -1;
_isOpen = false;
SetAndRaise(IsOpenProperty, ref _isOpen, false);
}
/// <summary>
@ -89,11 +118,11 @@ namespace Avalonia.Controls
}
((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = control.ContextMenu;
_popup.Child = this;
_popup.Open();
_popup.IsOpen = true;
control.ContextMenu._isOpen = true;
SetAndRaise(IsOpenProperty, ref _isOpen, true);
}
}
@ -118,21 +147,37 @@ namespace Avalonia.Controls
var control = (Control)sender;
var contextMenu = control.ContextMenu;
if (e.MouseButton == MouseButton.Right)
if (control.ContextMenu._isOpen)
{
if (control.ContextMenu._isOpen)
{
control.ContextMenu.Hide();
}
if (contextMenu.CancelClosing())
return;
contextMenu.Show(control);
control.ContextMenu.Hide();
e.Handled = true;
}
else if (contextMenu._isOpen)
if (e.MouseButton == MouseButton.Right)
{
control.ContextMenu.Hide();
if (contextMenu.CancelOpening())
return;
contextMenu.Show(control);
e.Handled = true;
}
}
private bool CancelClosing()
{
var eventArgs = new CancelEventArgs();
ContextMenuClosing?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}
private bool CancelOpening()
{
var eventArgs = new CancelEventArgs();
ContextMenuOpening?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}
}
}

6
src/Avalonia.Controls/Expander.cs

@ -66,9 +66,7 @@ namespace Avalonia.Controls
protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
{
IVisual visualContent = Presenter;
if (Content != null && ContentTransition != null && visualContent != null)
if (Content != null && ContentTransition != null && Presenter is Visual visualContent)
{
bool forward = ExpandDirection == ExpandDirection.Left ||
ExpandDirection == ExpandDirection.Up;
@ -87,4 +85,4 @@ namespace Avalonia.Controls
private ExpandDirection _expandDirection;
private bool _isExpanded;
}
}
}

2
src/Avalonia.Controls/Primitives/ToggleButton.cs

@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives
nameof(IsChecked),
o => o.IsChecked,
(o, v) => o.IsChecked = v,
unsetValue: false,
unsetValue: null,
defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<bool> IsThreeStateProperty =

5
src/Avalonia.Themes.Default/MenuItem.xaml

@ -122,6 +122,11 @@
</Setter>
</Style>
<Style Selector="MenuItem:selected /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>
</Style>
<Style Selector="MenuItem:pointerover /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

60
src/Avalonia.Visuals/Animation/CrossFade.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Reactive.Threading.Tasks;
using System.Threading.Tasks;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Animation
@ -14,10 +15,14 @@ namespace Avalonia.Animation
/// </summary>
public class CrossFade : IPageTransition
{
private Animation _fadeOutAnimation;
private Animation _fadeInAnimation;
/// <summary>
/// Initializes a new instance of the <see cref="CrossFade"/> class.
/// </summary>
public CrossFade()
:this(TimeSpan.Zero)
{
}
@ -27,13 +32,51 @@ namespace Avalonia.Animation
/// <param name="duration">The duration of the animation.</param>
public CrossFade(TimeSpan duration)
{
Duration = duration;
_fadeOutAnimation = new Animation
{
new KeyFrame
(
new Setter
{
Property = Visual.OpacityProperty,
Value = 0.0
}
)
{
Cue = new Cue(1.0)
}
};
_fadeInAnimation = new Animation
{
new KeyFrame
(
new Setter
{
Property = Visual.OpacityProperty,
Value = 0.0
}
)
{
Cue = new Cue(0.0)
}
};
_fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration;
}
/// <summary>
/// Gets the duration of the animation.
/// </summary>
public TimeSpan Duration { get; set; }
public TimeSpan Duration
{
get
{
return _fadeOutAnimation.Duration;
}
set
{
_fadeOutAnimation.Duration = _fadeInAnimation.Duration = value;
}
}
/// <summary>
/// Starts the animation.
@ -47,12 +90,10 @@ namespace Avalonia.Animation
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
public async Task Start(IVisual from, IVisual to)
public async Task Start(Visual from, Visual to)
{
var tasks = new List<Task>();
// TODO: Implement relevant transition logic here (or discard this class)
// in favor of XAML based transition for pages
if (to != null)
{
to.Opacity = 0;
@ -60,22 +101,21 @@ namespace Avalonia.Animation
if (from != null)
{
tasks.Add(_fadeOutAnimation.RunAsync(from));
}
if (to != null)
{
to.Opacity = 0;
to.IsVisible = true;
tasks.Add(_fadeInAnimation.RunAsync(to));
}
// FIXME: This is temporary until animations are fixed.
await Task.Delay(1);
await Task.WhenAll(tasks);
if (from != null)
{
from.IsVisible = false;
from.Opacity = 1;
}
if (to != null)
@ -99,7 +139,7 @@ namespace Avalonia.Animation
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
Task IPageTransition.Start(IVisual from, IVisual to, bool forward)
Task IPageTransition.Start(Visual from, Visual to, bool forward)
{
return Start(from, to);
}

2
src/Avalonia.Visuals/Animation/IPageTransition.cs

@ -26,6 +26,6 @@ namespace Avalonia.Animation
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
Task Start(IVisual from, IVisual to, bool forward);
Task Start(Visual from, Visual to, bool forward);
}
}

64
src/Avalonia.Visuals/Animation/PageSlide.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Reactive.Threading.Tasks;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Animation
@ -67,7 +68,7 @@ namespace Avalonia.Animation
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
public async Task Start(IVisual from, IVisual to, bool forward)
public async Task Start(Visual from, Visual to, bool forward)
{
var tasks = new List<Task>();
var parent = GetVisualParent(from, to);
@ -79,16 +80,69 @@ namespace Avalonia.Animation
// in favor of XAML based transition for pages
if (from != null)
{
var animation = new Animation
{
new KeyFrame
(
new Setter
{
Property = translateProperty,
Value = 0
}
)
{
Cue = new Cue(0.0)
},
new KeyFrame
(
new Setter
{
Property = translateProperty,
Value = forward ? -distance : distance
}
)
{
Cue = new Cue(1.0)
}
};
animation.Duration = Duration;
tasks.Add(animation.RunAsync(from));
}
if (to != null)
{
to.IsVisible = true;
var animation = new Animation
{
new KeyFrame
(
new Setter
{
Property = translateProperty,
Value = forward ? -distance : distance
}
)
{
Cue = new Cue(0.0)
},
new KeyFrame
(
new Setter
{
Property = translateProperty,
Value = 0
}
)
{
Cue = new Cue(1.0)
},
};
animation.Duration = Duration;
tasks.Add(animation.RunAsync(to));
}
// FIXME: This is temporary until animations are fixed.
await Task.Delay(1);
await Task.WhenAll(tasks);
if (from != null)
{

6
src/Avalonia.Visuals/Animation/TransformAnimator.cs

@ -19,7 +19,7 @@ namespace Avalonia.Animation
DoubleAnimator childKeyFrames;
/// <inheritdoc/>
public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete)
{
var ctrl = (Visual)control;
@ -51,7 +51,7 @@ namespace Avalonia.Animation
// It's a transform object so let's target that.
if (renderTransformType == Property.OwnerType)
{
return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch);
return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch, onComplete);
}
// It's a TransformGroup and try finding the target there.
else if (renderTransformType == typeof(TransformGroup))
@ -60,7 +60,7 @@ namespace Avalonia.Animation
{
if (transform.GetType() == Property.OwnerType)
{
return childKeyFrames.Apply(animation, transform, obsMatch);
return childKeyFrames.Apply(animation, transform, obsMatch, onComplete);
}
}
}

25
src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs

@ -14,6 +14,8 @@ namespace Avalonia.MonoMac
private readonly ManagedWindowResizeDragHelper _managedDrag;
public CustomWindow Window { get; private set; }
private bool _closed;
public WindowBaseImpl()
{
_managedDrag = new ManagedWindowResizeDragHelper(this, _ => { }, ResizeForManagedDrag);
@ -78,9 +80,8 @@ namespace Avalonia.MonoMac
public override void WillClose(global::MonoMac.Foundation.NSNotification notification)
{
_impl.Window.Dispose();
_impl.Window = null;
_impl.Dispose();
_impl._closed = true;
_impl.Closed?.Invoke();
}
public override CGRect WillUseStandardFrame(NSWindow window, CGRect newFrame)
@ -94,8 +95,19 @@ namespace Avalonia.MonoMac
{
return true;
}
public override void DidResize(NSNotification notification)
{
_impl.OnResized();
}
}
/// <summary>
/// As you can't combine NSWindowDelegate overrides and events this is a workaround
/// </summary>
protected virtual void OnResized()
{
}
public Point Position
{
@ -185,12 +197,11 @@ namespace Avalonia.MonoMac
return cocoaScreenPoint.ConvertPointY().ToAvaloniaPoint();
}
public override void Dispose()
{
Window?.Close();
Window?.Dispose();
if (!_closed)
Window.Close();
Window.Dispose();
base.Dispose();
}
}

58
src/OSX/Avalonia.MonoMac/WindowImpl.cs

@ -26,18 +26,19 @@ namespace Avalonia.MonoMac
});
Window.SetCanBecomeKeyAndMain();
Window.DidResize += delegate
{
var windowState = Window.IsMiniaturized ? WindowState.Minimized
: (IsZoomed ? WindowState.Maximized : WindowState.Normal);
}
if (windowState != _lastWindowState)
{
_lastWindowState = windowState;
WindowStateChanged?.Invoke(windowState);
}
};
protected override void OnResized()
{
var windowState = Window.IsMiniaturized ? WindowState.Minimized
: (IsZoomed ? WindowState.Maximized : WindowState.Normal);
if (windowState != _lastWindowState)
{
_lastWindowState = windowState;
WindowStateChanged?.Invoke(windowState);
}
}
public WindowState WindowState
@ -129,24 +130,47 @@ namespace Avalonia.MonoMac
class ModalDisposable : IDisposable
{
readonly WindowImpl _impl;
readonly IntPtr _modalSession;
bool disposed;
public ModalDisposable(WindowImpl impl)
public ModalDisposable(WindowImpl impl, IntPtr modalSession)
{
_impl = impl;
_modalSession = modalSession;
}
public void Continue()
{
if (disposed)
return;
var response = (NSRunResponse)NSApplication.SharedApplication.RunModalSession(_modalSession);
if (response == NSRunResponse.Continues)
{
Dispatcher.UIThread.Post(Continue, DispatcherPriority.ContextIdle);
}
else
{
Logging.Logger.Log(Logging.LogEventLevel.Debug, "MonoMac", this, "Modal session ended");
}
}
public void Dispose()
{
Logging.Logger.Log(Logging.LogEventLevel.Debug, "MonoMac", this, "ModalDisposable disposed");
_impl.Window.OrderOut(_impl.Window);
NSApplication.SharedApplication.EndModalSession(_modalSession);
disposed = true;
}
}
public IDisposable ShowDialog()
{
//TODO: Investigate how to return immediately.
// May be add some magic to our run loop or something
NSApplication.SharedApplication.RunModalForWindow(Window);
return new ModalDisposable(this);
var session = NSApplication.SharedApplication.BeginModalSession(Window);
var disposable = new ModalDisposable(this, session);
Dispatcher.UIThread.Post(disposable.Continue, DispatcherPriority.ContextIdle);
return disposable;
}
}
}
}

179
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -0,0 +1,179 @@
using System;
using System.Windows.Input;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Data;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class ContextMenuTests
{
private Mock<IPopupImpl> popupImpl;
[Fact]
public void Clicking_On_Control_Toggles_ContextMenu()
{
using (Application())
{
popupImpl.Setup(x => x.Show()).Verifiable();
popupImpl.Setup(x => x.Hide()).Verifiable();
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
new Window { Content = target };
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
Assert.True(sut.IsOpen);
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.None
});
Assert.False(sut.IsOpen);
popupImpl.Verify(x => x.Show(), Times.Once);
popupImpl.Verify(x => x.Hide(), Times.Once);
}
}
[Fact]
public void Right_Clicking_On_Control_Twice_Re_Opens_ContextMenu()
{
using (Application())
{
popupImpl.Setup(x => x.Show()).Verifiable();
popupImpl.Setup(x => x.Hide()).Verifiable();
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
new Window { Content = target };
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
Assert.True(sut.IsOpen);
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
Assert.True(sut.IsOpen);
popupImpl.Verify(x => x.Hide(), Times.Once);
popupImpl.Verify(x => x.Show(), Times.Exactly(2));
}
}
[Fact]
public void Cancelling_Opening_Does_Not_Show_ContextMenu()
{
using (Application())
{
popupImpl.Setup(x => x.Show()).Verifiable();
bool eventCalled = false;
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
new Window { Content = target };
sut.ContextMenuOpening += (c, e) => { eventCalled = true; e.Cancel = true; };
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
Assert.True(eventCalled);
Assert.False(sut.IsOpen);
popupImpl.Verify(x => x.Show(), Times.Never);
}
}
[Fact]
public void Cancelling_Closing_Leaves_ContextMenuOpen()
{
using (Application())
{
popupImpl.Setup(x => x.Show()).Verifiable();
popupImpl.Setup(x => x.Hide()).Verifiable();
bool eventCalled = false;
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
new Window { Content = target };
sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; };
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
Assert.True(sut.IsOpen);
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.None
});
Assert.True(eventCalled);
Assert.True(sut.IsOpen);
popupImpl.Verify(x => x.Show(), Times.Once());
popupImpl.Verify(x => x.Hide(), Times.Never);
}
}
private IDisposable Application()
{
var screen = new Rect(new Point(), new Size(100, 100));
var screenImpl = new Mock<IScreenImpl>();
screenImpl.Setup(x => x.ScreenCount).Returns(1);
screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(screen, screen, true) });
var windowImpl = new Mock<IWindowImpl>();
windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
popupImpl = new Mock<IPopupImpl>();
popupImpl.SetupGet(x => x.Scaling).Returns(1);
var services = TestServices.StyledWindow.With(
inputManager: new InputManager(),
windowImpl: windowImpl.Object,
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, () => popupImpl.Object));
return UnitTestApplication.Start(services);
}
}
}

26
tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs

@ -44,15 +44,41 @@ namespace Avalonia.Controls.Primitives.UnitTests
Assert.False(toggleButton.IsChecked);
}
[Fact]
public void ToggleButton_ThreeState_Checked_Binds_To_Nullable_Bool()
{
var threeStateButton = new ToggleButton();
var source = new Class1();
threeStateButton.DataContext = source;
threeStateButton.Bind(ToggleButton.IsCheckedProperty, new Binding(nameof(Class1.NullableFoo)));
source.NullableFoo = true;
Assert.True(threeStateButton.IsChecked);
source.NullableFoo = false;
Assert.False(threeStateButton.IsChecked);
source.NullableFoo = null;
Assert.Null(threeStateButton.IsChecked);
}
private class Class1 : NotifyingBase
{
private bool _foo;
private bool? nullableFoo;
public bool Foo
{
get { return _foo; }
set { _foo = value; RaisePropertyChanged(); }
}
public bool? NullableFoo
{
get { return nullableFoo; }
set { nullableFoo = value; RaisePropertyChanged(); }
}
}
}
}

Loading…
Cancel
Save