65 changed files with 1980 additions and 189 deletions
@ -1,6 +1,5 @@ |
|||||
copy ..\samples\ControlCatalog.Desktop\bin\Debug\net461\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\net461\ |
|
||||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
||||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
||||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Gtk3.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ |
|
||||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
||||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
||||
|
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ |
||||
|
|||||
@ -1,22 +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.
|
|
||||
|
|
||||
namespace Avalonia |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Specifies that this object supports a simple, transacted notification for batch
|
|
||||
/// initialization.
|
|
||||
/// </summary>
|
|
||||
public interface ISupportInitialize |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Signals the object that initialization is starting.
|
|
||||
/// </summary>
|
|
||||
void BeginInit(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Signals the object that initialization is complete.
|
|
||||
/// </summary>
|
|
||||
void EndInit(); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,224 @@ |
|||||
|
using System; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.Reactive.Linq; |
||||
|
using Avalonia.Animation; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Styling; |
||||
|
using ReactiveUI; |
||||
|
using Splat; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// This control hosts the View associated with ReactiveUI RoutingState,
|
||||
|
/// and will display the View and wire up the ViewModel whenever a new
|
||||
|
/// ViewModel is navigated to. Nested routing is also supported.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// <para>
|
||||
|
/// ReactiveUI routing consists of an IScreen that contains current
|
||||
|
/// RoutingState, several IRoutableViewModels, and a platform-specific
|
||||
|
/// XAML control called RoutedViewHost.
|
||||
|
/// </para>
|
||||
|
/// <para>
|
||||
|
/// RoutingState manages the ViewModel navigation stack and allows
|
||||
|
/// ViewModels to navigate to other ViewModels. IScreen is the root of
|
||||
|
/// a navigation stack; despite the name, its views don't have to occupy
|
||||
|
/// the whole screen. RoutedViewHost monitors an instance of RoutingState,
|
||||
|
/// responding to any changes in the navigation stack by creating and
|
||||
|
/// embedding the appropriate view.
|
||||
|
/// </para>
|
||||
|
/// <para>
|
||||
|
/// Place this control to a view containing your ViewModel that implements
|
||||
|
/// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property.
|
||||
|
/// <code>
|
||||
|
/// <![CDATA[
|
||||
|
/// <rxui:RoutedViewHost
|
||||
|
/// HorizontalAlignment="Stretch"
|
||||
|
/// VerticalAlignment="Stretch"
|
||||
|
/// Router="{Binding Router}">
|
||||
|
/// <rxui:RoutedViewHost.DefaultContent>
|
||||
|
/// <TextBlock Text="Default Content"/>
|
||||
|
/// </rxui:RoutedViewHost.DefaultContent>
|
||||
|
/// </rxui:RoutedViewHost>
|
||||
|
/// ]]>
|
||||
|
/// </code>
|
||||
|
/// </para>
|
||||
|
/// <para>
|
||||
|
/// See <see href="https://reactiveui.net/docs/handbook/routing/">
|
||||
|
/// ReactiveUI routing documentation website</see> for more info.
|
||||
|
/// </para>
|
||||
|
/// </remarks>
|
||||
|
public class RoutedViewHost : UserControl, IActivatable, IEnableLogger |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The router dependency property.
|
||||
|
/// </summary>
|
||||
|
public static readonly AvaloniaProperty<RoutingState> RouterProperty = |
||||
|
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The default content property.
|
||||
|
/// </summary>
|
||||
|
public static readonly AvaloniaProperty<object> DefaultContentProperty = |
||||
|
AvaloniaProperty.Register<RoutedViewHost, object>(nameof(DefaultContent)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Fade in animation property.
|
||||
|
/// </summary>
|
||||
|
public static readonly AvaloniaProperty<IAnimation> FadeInAnimationProperty = |
||||
|
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent), |
||||
|
CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Fade out animation property.
|
||||
|
/// </summary>
|
||||
|
public static readonly AvaloniaProperty<IAnimation> FadeOutAnimationProperty = |
||||
|
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent), |
||||
|
CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
|
||||
|
/// </summary>
|
||||
|
public RoutedViewHost() |
||||
|
{ |
||||
|
this.WhenActivated(disposables => |
||||
|
{ |
||||
|
this.WhenAnyObservable(x => x.Router.CurrentViewModel) |
||||
|
.DistinctUntilChanged() |
||||
|
.Subscribe(NavigateToViewModel) |
||||
|
.DisposeWith(disposables); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the <see cref="RoutingState"/> of the view model stack.
|
||||
|
/// </summary>
|
||||
|
public RoutingState Router |
||||
|
{ |
||||
|
get => GetValue(RouterProperty); |
||||
|
set => SetValue(RouterProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the content displayed whenever there is no page currently routed.
|
||||
|
/// </summary>
|
||||
|
public object DefaultContent |
||||
|
{ |
||||
|
get => GetValue(DefaultContentProperty); |
||||
|
set => SetValue(DefaultContentProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the animation played when page appears.
|
||||
|
/// </summary>
|
||||
|
public IAnimation FadeInAnimation |
||||
|
{ |
||||
|
get => GetValue(FadeInAnimationProperty); |
||||
|
set => SetValue(FadeInAnimationProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the animation played when page disappears.
|
||||
|
/// </summary>
|
||||
|
public IAnimation FadeOutAnimation |
||||
|
{ |
||||
|
get => GetValue(FadeOutAnimationProperty); |
||||
|
set => SetValue(FadeOutAnimationProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Duplicates the Content property with a private setter.
|
||||
|
/// </summary>
|
||||
|
public new object Content |
||||
|
{ |
||||
|
get => base.Content; |
||||
|
private set => base.Content = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ReactiveUI view locator used by this router.
|
||||
|
/// </summary>
|
||||
|
public IViewLocator ViewLocator { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Invoked when ReactiveUI router navigates to a view model.
|
||||
|
/// </summary>
|
||||
|
/// <param name="viewModel">ViewModel to which the user navigates.</param>
|
||||
|
/// <exception cref="Exception">
|
||||
|
/// Thrown when ViewLocator is unable to find the appropriate view.
|
||||
|
/// </exception>
|
||||
|
private void NavigateToViewModel(IRoutableViewModel viewModel) |
||||
|
{ |
||||
|
if (viewModel == null) |
||||
|
{ |
||||
|
this.Log().Info("ViewModel is null, falling back to default content."); |
||||
|
UpdateContent(DefaultContent); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; |
||||
|
var view = viewLocator.ResolveView(viewModel); |
||||
|
if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); |
||||
|
|
||||
|
this.Log().Info($"Ready to show {view} with autowired {viewModel}."); |
||||
|
view.ViewModel = viewModel; |
||||
|
UpdateContent(view); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates the content with transitions.
|
||||
|
/// </summary>
|
||||
|
/// <param name="newContent">New content to set.</param>
|
||||
|
private async void UpdateContent(object newContent) |
||||
|
{ |
||||
|
if (FadeOutAnimation != null) |
||||
|
await FadeOutAnimation.RunAsync(this, Clock); |
||||
|
Content = newContent; |
||||
|
if (FadeInAnimation != null) |
||||
|
await FadeInAnimation.RunAsync(this, Clock); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates opacity animation for this routed view host.
|
||||
|
/// </summary>
|
||||
|
/// <param name="from">Opacity to start from.</param>
|
||||
|
/// <param name="to">Opacity to finish with.</param>
|
||||
|
/// <param name="duration">Duration of the animation.</param>
|
||||
|
/// <returns>Animation object instance.</returns>
|
||||
|
private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) |
||||
|
{ |
||||
|
return new Avalonia.Animation.Animation |
||||
|
{ |
||||
|
Duration = duration, |
||||
|
Children = |
||||
|
{ |
||||
|
new KeyFrame |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter |
||||
|
{ |
||||
|
Property = OpacityProperty, |
||||
|
Value = from |
||||
|
} |
||||
|
}, |
||||
|
Cue = new Cue(0d) |
||||
|
}, |
||||
|
new KeyFrame |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter |
||||
|
{ |
||||
|
Property = OpacityProperty, |
||||
|
Value = to |
||||
|
} |
||||
|
}, |
||||
|
Cue = new Cue(1d) |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
// 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.Linq; |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The `:not()` style selector.
|
||||
|
/// </summary>
|
||||
|
internal class NotSelector : Selector |
||||
|
{ |
||||
|
private readonly Selector _previous; |
||||
|
private readonly Selector _argument; |
||||
|
private string _selectorString; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="NotSelector"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="previous">The previous selector.</param>
|
||||
|
/// <param name="argument">The selector to be not-ed.</param>
|
||||
|
public NotSelector(Selector previous, Selector argument) |
||||
|
{ |
||||
|
_previous = previous; |
||||
|
_argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool InTemplate => _argument.InTemplate; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool IsCombinator => false; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override Type TargetType => _previous?.TargetType; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
if (_selectorString == null) |
||||
|
{ |
||||
|
_selectorString = ":not(" + _argument.ToString() + ")"; |
||||
|
} |
||||
|
|
||||
|
return _selectorString; |
||||
|
} |
||||
|
|
||||
|
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) |
||||
|
{ |
||||
|
var innerResult = _argument.Match(control, subscribe); |
||||
|
|
||||
|
switch (innerResult.Result) |
||||
|
{ |
||||
|
case SelectorMatchResult.AlwaysThisInstance: |
||||
|
return SelectorMatch.NeverThisInstance; |
||||
|
case SelectorMatchResult.AlwaysThisType: |
||||
|
return SelectorMatch.NeverThisType; |
||||
|
case SelectorMatchResult.NeverThisInstance: |
||||
|
return SelectorMatch.AlwaysThisInstance; |
||||
|
case SelectorMatchResult.NeverThisType: |
||||
|
return SelectorMatch.AlwaysThisType; |
||||
|
case SelectorMatchResult.Sometimes: |
||||
|
return new SelectorMatch(innerResult.Activator.Select(x => !x)); |
||||
|
default: |
||||
|
throw new InvalidOperationException("Invalid SelectorMatchResult."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override Selector MovePrevious() => _previous; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,263 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Platform.Interop; |
||||
|
// ReSharper disable IdentifierTypo
|
||||
|
namespace Avalonia.X11.NativeDialogs |
||||
|
{ |
||||
|
|
||||
|
static unsafe class Glib |
||||
|
{ |
||||
|
private const string GlibName = "libglib-2.0.so.0"; |
||||
|
private const string GObjectName = "libgobject-2.0.so.0"; |
||||
|
|
||||
|
[DllImport(GlibName)] |
||||
|
public static extern void g_slist_free(GSList* data); |
||||
|
|
||||
|
[DllImport(GObjectName)] |
||||
|
private static extern void g_object_ref(IntPtr instance); |
||||
|
|
||||
|
[DllImport(GObjectName)] |
||||
|
private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal, |
||||
|
IntPtr handler, IntPtr userData, int flags); |
||||
|
|
||||
|
[DllImport(GObjectName)] |
||||
|
private static extern void g_object_unref(IntPtr instance); |
||||
|
|
||||
|
[DllImport(GObjectName)] |
||||
|
private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId); |
||||
|
|
||||
|
private delegate bool timeout_callback(IntPtr data); |
||||
|
|
||||
|
[DllImport(GlibName)] |
||||
|
private static extern ulong g_timeout_add_full(int prio, uint interval, timeout_callback callback, IntPtr data, |
||||
|
IntPtr destroy); |
||||
|
|
||||
|
|
||||
|
class ConnectedSignal : IDisposable |
||||
|
{ |
||||
|
private readonly IntPtr _instance; |
||||
|
private GCHandle _handle; |
||||
|
private readonly ulong _id; |
||||
|
|
||||
|
public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id) |
||||
|
{ |
||||
|
_instance = instance; |
||||
|
g_object_ref(instance); |
||||
|
_handle = handle; |
||||
|
_id = id; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (_handle.IsAllocated) |
||||
|
{ |
||||
|
g_signal_handler_disconnect(_instance, _id); |
||||
|
g_object_unref(_instance); |
||||
|
_handle.Free(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static IDisposable ConnectSignal<T>(IntPtr obj, string name, T handler) |
||||
|
{ |
||||
|
var handle = GCHandle.Alloc(handler); |
||||
|
var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler); |
||||
|
using (var utf = new Utf8Buffer(name)) |
||||
|
{ |
||||
|
var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); |
||||
|
if (id == 0) |
||||
|
throw new ArgumentException("Unable to connect to signal " + name); |
||||
|
return new ConnectedSignal(obj, handle, id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
static bool TimeoutHandler(IntPtr data) |
||||
|
{ |
||||
|
var handle = GCHandle.FromIntPtr(data); |
||||
|
var cb = (Func<bool>)handle.Target; |
||||
|
if (!cb()) |
||||
|
{ |
||||
|
handle.Free(); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static readonly timeout_callback s_pinnedHandler; |
||||
|
|
||||
|
static Glib() |
||||
|
{ |
||||
|
s_pinnedHandler = TimeoutHandler; |
||||
|
} |
||||
|
|
||||
|
static void AddTimeout(int priority, uint interval, Func<bool> callback) |
||||
|
{ |
||||
|
var handle = GCHandle.Alloc(callback); |
||||
|
g_timeout_add_full(priority, interval, s_pinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero); |
||||
|
} |
||||
|
|
||||
|
public static Task<T> RunOnGlibThread<T>(Func<T> action) |
||||
|
{ |
||||
|
var tcs = new TaskCompletionSource<T>(); |
||||
|
AddTimeout(0, 0, () => |
||||
|
{ |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
tcs.SetResult(action()); |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
tcs.TrySetException(e); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
}); |
||||
|
return tcs.Task; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[StructLayout(LayoutKind.Sequential)] |
||||
|
unsafe struct GSList |
||||
|
{ |
||||
|
public readonly IntPtr Data; |
||||
|
public readonly GSList* Next; |
||||
|
} |
||||
|
|
||||
|
enum GtkFileChooserAction |
||||
|
{ |
||||
|
Open, |
||||
|
Save, |
||||
|
SelectFolder, |
||||
|
} |
||||
|
|
||||
|
// ReSharper disable UnusedMember.Global
|
||||
|
enum GtkResponseType |
||||
|
{ |
||||
|
Help = -11, |
||||
|
Apply = -10, |
||||
|
No = -9, |
||||
|
Yes = -8, |
||||
|
Close = -7, |
||||
|
Cancel = -6, |
||||
|
Ok = -5, |
||||
|
DeleteEvent = -4, |
||||
|
Accept = -3, |
||||
|
Reject = -2, |
||||
|
None = -1, |
||||
|
} |
||||
|
// ReSharper restore UnusedMember.Global
|
||||
|
|
||||
|
static unsafe class Gtk |
||||
|
{ |
||||
|
private static IntPtr s_display; |
||||
|
private const string GdkName = "libgdk-3.so.0"; |
||||
|
private const string GtkName = "libgtk-3.so.0"; |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
static extern void gtk_main_iteration(); |
||||
|
|
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern void gtk_window_set_modal(IntPtr window, bool modal); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern void gtk_window_present(IntPtr gtkWindow); |
||||
|
|
||||
|
|
||||
|
public delegate bool signal_generic(IntPtr gtkWidget, IntPtr userData); |
||||
|
|
||||
|
public delegate bool signal_dialog_response(IntPtr gtkWidget, GtkResponseType response, IntPtr userData); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern IntPtr gtk_file_chooser_dialog_new(Utf8Buffer title, IntPtr parent, |
||||
|
GtkFileChooserAction action, IntPtr ignore); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern void |
||||
|
gtk_dialog_add_button(IntPtr raw, Utf8Buffer button_text, GtkResponseType response_id); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern GSList* gtk_file_chooser_get_filenames(IntPtr chooser); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern void gtk_file_chooser_set_filename(IntPtr chooser, Utf8Buffer file); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern void gtk_widget_realize(IntPtr gtkWidget); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern IntPtr gtk_widget_get_window(IntPtr gtkWidget); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
public static extern void gtk_widget_hide(IntPtr gtkWidget); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
static extern bool gtk_init_check(int argc, IntPtr argv); |
||||
|
|
||||
|
[DllImport(GdkName)] |
||||
|
static extern IntPtr gdk_x11_window_foreign_new_for_display(IntPtr display, IntPtr xid); |
||||
|
|
||||
|
[DllImport(GdkName)] |
||||
|
static extern IntPtr gdk_set_allowed_backends(Utf8Buffer backends); |
||||
|
|
||||
|
[DllImport(GdkName)] |
||||
|
static extern IntPtr gdk_display_get_default(); |
||||
|
|
||||
|
[DllImport(GtkName)] |
||||
|
static extern IntPtr gtk_application_new(Utf8Buffer appId, int flags); |
||||
|
|
||||
|
[DllImport(GdkName)] |
||||
|
public static extern void gdk_window_set_transient_for(IntPtr window, IntPtr parent); |
||||
|
|
||||
|
public static IntPtr GetForeignWindow(IntPtr xid) => gdk_x11_window_foreign_new_for_display(s_display, xid); |
||||
|
|
||||
|
public static Task<bool> StartGtk() |
||||
|
{ |
||||
|
var tcs = new TaskCompletionSource<bool>(); |
||||
|
new Thread(() => |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
using (var backends = new Utf8Buffer("x11")) |
||||
|
gdk_set_allowed_backends(backends); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
//Ignore
|
||||
|
} |
||||
|
|
||||
|
Environment.SetEnvironmentVariable("WAYLAND_DISPLAY", |
||||
|
"/proc/fake-display-to-prevent-wayland-initialization-by-gtk3"); |
||||
|
|
||||
|
if (!gtk_init_check(0, IntPtr.Zero)) |
||||
|
{ |
||||
|
tcs.SetResult(false); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
IntPtr app; |
||||
|
using (var utf = new Utf8Buffer($"avalonia.app.a{Guid.NewGuid():N}")) |
||||
|
app = gtk_application_new(utf, 0); |
||||
|
if (app == IntPtr.Zero) |
||||
|
{ |
||||
|
tcs.SetResult(false); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
s_display = gdk_display_get_default(); |
||||
|
tcs.SetResult(true); |
||||
|
while (true) |
||||
|
gtk_main_iteration(); |
||||
|
}) {Name = "GTK3THREAD", IsBackground = true}.Start(); |
||||
|
return tcs.Task; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,122 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Platform; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Platform.Interop; |
||||
|
using static Avalonia.X11.NativeDialogs.Glib; |
||||
|
using static Avalonia.X11.NativeDialogs.Gtk; |
||||
|
// ReSharper disable AccessToModifiedClosure
|
||||
|
namespace Avalonia.X11.NativeDialogs |
||||
|
{ |
||||
|
class GtkSystemDialog : ISystemDialogImpl |
||||
|
{ |
||||
|
private Task<bool> _initialized; |
||||
|
private unsafe Task<string[]> ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action, |
||||
|
bool multiSelect, string initialFileName) |
||||
|
{ |
||||
|
IntPtr dlg; |
||||
|
using (var name = new Utf8Buffer(title)) |
||||
|
dlg = gtk_file_chooser_dialog_new(name, IntPtr.Zero, action, IntPtr.Zero); |
||||
|
UpdateParent(dlg, parent); |
||||
|
if (multiSelect) |
||||
|
gtk_file_chooser_set_select_multiple(dlg, true); |
||||
|
|
||||
|
gtk_window_set_modal(dlg, true); |
||||
|
var tcs = new TaskCompletionSource<string[]>(); |
||||
|
List<IDisposable> disposables = null; |
||||
|
|
||||
|
void Dispose() |
||||
|
{ |
||||
|
// ReSharper disable once PossibleNullReferenceException
|
||||
|
foreach (var d in disposables) d.Dispose(); |
||||
|
disposables.Clear(); |
||||
|
} |
||||
|
|
||||
|
disposables = new List<IDisposable> |
||||
|
{ |
||||
|
ConnectSignal<signal_generic>(dlg, "close", delegate |
||||
|
{ |
||||
|
tcs.TrySetResult(null); |
||||
|
Dispose(); |
||||
|
return false; |
||||
|
}), |
||||
|
ConnectSignal<signal_dialog_response>(dlg, "response", (_, resp, __) => |
||||
|
{ |
||||
|
string[] result = null; |
||||
|
if (resp == GtkResponseType.Accept) |
||||
|
{ |
||||
|
var resultList = new List<string>(); |
||||
|
var gs = gtk_file_chooser_get_filenames(dlg); |
||||
|
var cgs = gs; |
||||
|
while (cgs != null) |
||||
|
{ |
||||
|
if (cgs->Data != IntPtr.Zero) |
||||
|
resultList.Add(Utf8Buffer.StringFromPtr(cgs->Data)); |
||||
|
cgs = cgs->Next; |
||||
|
} |
||||
|
g_slist_free(gs); |
||||
|
result = resultList.ToArray(); |
||||
|
} |
||||
|
|
||||
|
gtk_widget_hide(dlg); |
||||
|
Dispose(); |
||||
|
tcs.TrySetResult(result); |
||||
|
return false; |
||||
|
}) |
||||
|
}; |
||||
|
using (var open = new Utf8Buffer("Open")) |
||||
|
gtk_dialog_add_button(dlg, open, GtkResponseType.Accept); |
||||
|
using (var open = new Utf8Buffer("Cancel")) |
||||
|
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel); |
||||
|
if (initialFileName != null) |
||||
|
using (var fn = new Utf8Buffer(initialFileName)) |
||||
|
gtk_file_chooser_set_filename(dlg, fn); |
||||
|
gtk_window_present(dlg); |
||||
|
return tcs.Task; |
||||
|
} |
||||
|
|
||||
|
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) |
||||
|
{ |
||||
|
await EnsureInitialized(); |
||||
|
return await await RunOnGlibThread( |
||||
|
() => ShowDialog(dialog.Title, parent, |
||||
|
dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save, |
||||
|
(dialog as OpenFileDialog)?.AllowMultiple ?? false, |
||||
|
Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory, |
||||
|
string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName))); |
||||
|
} |
||||
|
|
||||
|
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) |
||||
|
{ |
||||
|
await EnsureInitialized(); |
||||
|
return await await RunOnGlibThread(async () => |
||||
|
{ |
||||
|
var res = await ShowDialog(dialog.Title, parent, |
||||
|
GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory); |
||||
|
return res?.FirstOrDefault(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async Task EnsureInitialized() |
||||
|
{ |
||||
|
if (_initialized == null) _initialized = StartGtk(); |
||||
|
|
||||
|
if (!(await _initialized)) |
||||
|
throw new Exception("Unable to initialize GTK on separate thread"); |
||||
|
} |
||||
|
|
||||
|
void UpdateParent(IntPtr chooser, IWindowImpl parentWindow) |
||||
|
{ |
||||
|
var xid = parentWindow.Handle.Handle; |
||||
|
gtk_widget_realize(chooser); |
||||
|
var window = gtk_widget_get_window(chooser); |
||||
|
var parent = GetForeignWindow(xid); |
||||
|
if (window != IntPtr.Zero && parent != IntPtr.Zero) |
||||
|
gdk_window_set_transient_for(window, parent); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1 +1 @@ |
|||||
Subproject commit 8abbe09592668efb573ac4d5548ba2d7e464ba78 |
Subproject commit ab5526173722b8988bc5ca3c03c8752ce89c0975 |
||||
@ -0,0 +1,62 @@ |
|||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Avalonia.DesignerSupport.Tests |
||||
|
{ |
||||
|
static class Helpers |
||||
|
{ |
||||
|
public static void StructDiff(object parsed, object expected) => StructDiff(parsed, expected, "{root}"); |
||||
|
|
||||
|
static void StructDiff(object parsed, object expected, string path) |
||||
|
{ |
||||
|
if (parsed == null && expected == null) |
||||
|
return; |
||||
|
if ((parsed == null && expected != null) || (parsed != null && expected == null)) |
||||
|
throw new Exception( |
||||
|
$"{path}: Null mismatch: {(parsed == null ? "null" : "not-null")} {(expected == null ? "null" : "not-null")}"); |
||||
|
|
||||
|
if (parsed.GetType() != expected.GetType()) |
||||
|
throw new Exception($"{path}: Type mismatch: {parsed.GetType()} {expected.GetType()}"); |
||||
|
|
||||
|
if (parsed is string || parsed.GetType().IsPrimitive) |
||||
|
{ |
||||
|
if (!parsed.Equals(expected)) |
||||
|
throw new Exception($"{path}: Not equal {parsed} {expected}"); |
||||
|
} |
||||
|
else if (parsed is IDictionary dic) |
||||
|
{ |
||||
|
var dic2 = (IDictionary) expected; |
||||
|
if (dic.Count != dic2.Count) |
||||
|
throw new Exception($"{path}: Dictionary count mismatch: {dic.Count} {dic2.Count}"); |
||||
|
|
||||
|
foreach (var k in dic.Keys.Cast<object>().OrderBy(o => o.ToString())) |
||||
|
{ |
||||
|
var v1 = dic[k]; |
||||
|
var v2 = dic2[k]; |
||||
|
StructDiff(v1, v2, path + "['" + k + "']"); |
||||
|
} |
||||
|
} |
||||
|
else if (parsed is IList col) |
||||
|
{ |
||||
|
var col2 = (IList) expected; |
||||
|
if (col.Count != col2.Count) |
||||
|
throw new Exception($"{path}: Collection count mismatch: {col.Count} {col2.Count}"); |
||||
|
for (var c = 0; c < col.Count; c++) |
||||
|
StructDiff(col[c], col2[c], path + "[" + c + "]"); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
foreach (var prop in parsed.GetType().GetProperties() |
||||
|
.Where(p => p.GetMethod != null && p.GetMethod.IsPublic)) |
||||
|
{ |
||||
|
StructDiff(prop.GetValue(parsed), prop.GetValue(expected), path + "." + prop.Name); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,171 @@ |
|||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Net; |
||||
|
using System.Net.Sockets; |
||||
|
using System.Reflection; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Remote.Protocol; |
||||
|
using Avalonia.Remote.Protocol.Viewport; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.DesignerSupport.Tests |
||||
|
{ |
||||
|
public class RemoteProtocolTests : IDisposable |
||||
|
{ |
||||
|
private readonly List<IDisposable> _disposables = new List<IDisposable>(); |
||||
|
private IAvaloniaRemoteTransportConnection _server; |
||||
|
private IAvaloniaRemoteTransportConnection _client; |
||||
|
private BlockingCollection<object> _serverMessages = new BlockingCollection<object>(); |
||||
|
private BlockingCollection<object> _clientMessages = new BlockingCollection<object>(); |
||||
|
private SynchronizationContext _originalContext; |
||||
|
|
||||
|
|
||||
|
class DisabledSyncContext : SynchronizationContext |
||||
|
{ |
||||
|
public override void Post(SendOrPostCallback d, object state) |
||||
|
{ |
||||
|
throw new InvalidCastException("Not allowed"); |
||||
|
} |
||||
|
|
||||
|
public override void Send(SendOrPostCallback d, object state) |
||||
|
{ |
||||
|
throw new InvalidCastException("Not allowed"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void Init(IMessageTypeResolver clientResolver = null, IMessageTypeResolver serverResolver = null) |
||||
|
{ |
||||
|
_originalContext = SynchronizationContext.Current; |
||||
|
SynchronizationContext.SetSynchronizationContext(new DisabledSyncContext()); |
||||
|
var clientTransport = new BsonTcpTransport(clientResolver ?? new DefaultMessageTypeResolver()); |
||||
|
var serverTransport = new BsonTcpTransport(serverResolver ?? new DefaultMessageTypeResolver()); |
||||
|
|
||||
|
var tcpListener = new TcpListener(IPAddress.Loopback, 0); |
||||
|
tcpListener.Start(); |
||||
|
var port = ((IPEndPoint)tcpListener.LocalEndpoint).Port; |
||||
|
tcpListener.Stop(); |
||||
|
|
||||
|
var tcs = new TaskCompletionSource<int>(); |
||||
|
serverTransport.Listen(IPAddress.Loopback, port, connected => |
||||
|
{ |
||||
|
_server = connected; |
||||
|
tcs.SetResult(0); |
||||
|
}); |
||||
|
_client = clientTransport.Connect(IPAddress.Loopback, port).Result; |
||||
|
_disposables.Add(_client); |
||||
|
_client.OnMessage += (_, m) => _clientMessages.Add(m); |
||||
|
tcs.Task.Wait(); |
||||
|
_disposables.Add(_server); |
||||
|
_server.OnMessage += (_, m) => _serverMessages.Add(m); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
object TakeServer() |
||||
|
{ |
||||
|
var src = new CancellationTokenSource(200); |
||||
|
try |
||||
|
{ |
||||
|
return _serverMessages.Take(src.Token); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
src.Dispose(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
void EntitiesAreProperlySerializedAndDeserialized() |
||||
|
{ |
||||
|
Init(); |
||||
|
var rnd = new Random(); |
||||
|
_server.OnMessage += (_, message) => { }; |
||||
|
|
||||
|
|
||||
|
object GetRandomValue(Type t, string pathInfo) |
||||
|
{ |
||||
|
if (t.IsArray) |
||||
|
{ |
||||
|
var arr = Array.CreateInstance(t.GetElementType(), 1); |
||||
|
((IList)arr)[0] = GetRandomValue(t.GetElementType(), pathInfo); |
||||
|
return arr; |
||||
|
} |
||||
|
|
||||
|
if (t == typeof(bool)) |
||||
|
return true; |
||||
|
if (t == typeof(int) || t == typeof(long)) |
||||
|
return rnd.Next(); |
||||
|
if (t == typeof(byte)) |
||||
|
return (byte)rnd.Next(255); |
||||
|
if (t == typeof(double)) |
||||
|
return rnd.NextDouble(); |
||||
|
if (t.IsEnum) |
||||
|
return ((IList)Enum.GetValues(t)).Cast<object>().Last(); |
||||
|
if (t == typeof(string)) |
||||
|
return Guid.NewGuid().ToString(); |
||||
|
if (t == typeof(Guid)) |
||||
|
return Guid.NewGuid(); |
||||
|
throw new Exception($"Doesn't know how to fabricate a random value for {t}, path {pathInfo}"); |
||||
|
} |
||||
|
|
||||
|
foreach (var t in typeof(MeasureViewportMessage).Assembly.GetTypes().Where(t => |
||||
|
t.GetCustomAttribute(typeof(AvaloniaRemoteMessageGuidAttribute)) != null)) |
||||
|
{ |
||||
|
var o = Activator.CreateInstance(t); |
||||
|
foreach (var p in t.GetProperties()) |
||||
|
p.SetValue(o, GetRandomValue(p.PropertyType, $"{t.FullName}.{p.Name}")); |
||||
|
|
||||
|
_client.Send(o).Wait(200); |
||||
|
var received = TakeServer(); |
||||
|
Helpers.StructDiff(received, o); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
void RemoteProtocolShouldBeBackwardsCompatible() |
||||
|
{ |
||||
|
Init(new DefaultMessageTypeResolver(typeof(ExtendedMeasureViewportMessage).Assembly)); |
||||
|
_client.Send(new ExtendedMeasureViewportMessage() |
||||
|
{ |
||||
|
Width = 100, Height = 200, SomeNewProperty = 300, |
||||
|
SomeArrayProperty = new[]{1,2,3}, |
||||
|
SubObjectProperty = new ExtendedMeasureViewportMessage.SubObject() |
||||
|
{ |
||||
|
Foo = 543 |
||||
|
} |
||||
|
}); |
||||
|
var received = (MeasureViewportMessage)TakeServer(); |
||||
|
Assert.Equal(100, received.Width); |
||||
|
Assert.Equal(200, received.Height); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_disposables.ForEach(d => d.Dispose()); |
||||
|
SynchronizationContext.SetSynchronizationContext(_originalContext); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[AvaloniaRemoteMessageGuid("6E3C5310-E2B1-4C3D-8688-01183AA48C5B")] |
||||
|
public class ExtendedMeasureViewportMessage |
||||
|
{ |
||||
|
public double Width { get; set; } |
||||
|
|
||||
|
public int SomeNewProperty { get; set; } |
||||
|
public int[] SomeArrayProperty { get; set; } |
||||
|
public class SubObject |
||||
|
{ |
||||
|
public int Foo { get; set; } |
||||
|
} |
||||
|
public SubObject SubObjectProperty { get; set; } |
||||
|
public double Height { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,104 @@ |
|||||
|
using System; |
||||
|
using System.Reactive.Concurrency; |
||||
|
using System.Reactive.Disposables; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Avalonia; |
||||
|
using ReactiveUI; |
||||
|
using DynamicData; |
||||
|
using Xunit; |
||||
|
using Splat; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using System.ComponentModel; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Reactive; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
public class RoutedViewHostTest |
||||
|
{ |
||||
|
public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel |
||||
|
{ |
||||
|
public string UrlPathSegment => "first"; |
||||
|
|
||||
|
public IScreen HostScreen { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class FirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { } |
||||
|
|
||||
|
public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel |
||||
|
{ |
||||
|
public string UrlPathSegment => "second"; |
||||
|
|
||||
|
public IScreen HostScreen { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { } |
||||
|
|
||||
|
public class ScreenViewModel : ReactiveObject, IScreen |
||||
|
{ |
||||
|
public RoutingState Router { get; } = new RoutingState(); |
||||
|
} |
||||
|
|
||||
|
public RoutedViewHostTest() |
||||
|
{ |
||||
|
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); |
||||
|
Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>)); |
||||
|
Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void RoutedViewHostShouldStayInSyncWithRoutingState() |
||||
|
{ |
||||
|
var screen = new ScreenViewModel(); |
||||
|
var defaultContent = new TextBlock(); |
||||
|
var host = new RoutedViewHost |
||||
|
{ |
||||
|
Router = screen.Router, |
||||
|
DefaultContent = defaultContent, |
||||
|
FadeOutAnimation = null, |
||||
|
FadeInAnimation = null |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot |
||||
|
{ |
||||
|
Child = host |
||||
|
}; |
||||
|
|
||||
|
Assert.NotNull(host.Content); |
||||
|
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
||||
|
Assert.Equal(defaultContent, host.Content); |
||||
|
|
||||
|
screen.Router.Navigate |
||||
|
.Execute(new FirstRoutableViewModel()) |
||||
|
.Subscribe(); |
||||
|
|
||||
|
Assert.NotNull(host.Content); |
||||
|
Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); |
||||
|
|
||||
|
screen.Router.Navigate |
||||
|
.Execute(new SecondRoutableViewModel()) |
||||
|
.Subscribe(); |
||||
|
|
||||
|
Assert.NotNull(host.Content); |
||||
|
Assert.Equal(typeof(SecondRoutableView), host.Content.GetType()); |
||||
|
|
||||
|
screen.Router.NavigateBack |
||||
|
.Execute(Unit.Default) |
||||
|
.Subscribe(); |
||||
|
|
||||
|
Assert.NotNull(host.Content); |
||||
|
Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); |
||||
|
|
||||
|
screen.Router.NavigateBack |
||||
|
.Execute(Unit.Default) |
||||
|
.Subscribe(); |
||||
|
|
||||
|
Assert.NotNull(host.Content); |
||||
|
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
||||
|
Assert.Equal(defaultContent, host.Content); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
// 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.Reactive.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Styling.UnitTests |
||||
|
{ |
||||
|
public class SelectorTests_Not |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Not_Selector_Should_Have_Correct_String_Representation() |
||||
|
{ |
||||
|
var target = default(Selector).Not(x => x.Class("foo")); |
||||
|
|
||||
|
Assert.Equal(":not(.foo)", target.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Not_OfType_Matches_Control_Of_Incorrect_Type() |
||||
|
{ |
||||
|
var control = new Control1(); |
||||
|
var target = default(Selector).Not(x => x.OfType<Control1>()); |
||||
|
|
||||
|
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Not_OfType_Doesnt_Match_Control_Of_Correct_Type() |
||||
|
{ |
||||
|
var control = new Control2(); |
||||
|
var target = default(Selector).Not(x => x.OfType<Control1>()); |
||||
|
|
||||
|
Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Not_Class_Doesnt_Match_Control_With_Class() |
||||
|
{ |
||||
|
var control = new Control1 |
||||
|
{ |
||||
|
Classes = new Classes { "foo" }, |
||||
|
}; |
||||
|
|
||||
|
var target = default(Selector).Not(x => x.Class("foo")); |
||||
|
var match = target.Match(control); |
||||
|
|
||||
|
Assert.Equal(SelectorMatchResult.Sometimes, match.Result); |
||||
|
Assert.False(await match.Activator.Take(1)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Not_Class_Matches_Control_Without_Class() |
||||
|
{ |
||||
|
var control = new Control1 |
||||
|
{ |
||||
|
Classes = new Classes { "bar" }, |
||||
|
}; |
||||
|
|
||||
|
var target = default(Selector).Not(x => x.Class("foo")); |
||||
|
var match = target.Match(control); |
||||
|
|
||||
|
Assert.Equal(SelectorMatchResult.Sometimes, match.Result); |
||||
|
Assert.True(await match.Activator.Take(1)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task OfType_Not_Class_Matches_Control_Without_Class() |
||||
|
{ |
||||
|
var control = new Control1 |
||||
|
{ |
||||
|
Classes = new Classes { "bar" }, |
||||
|
}; |
||||
|
|
||||
|
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo")); |
||||
|
var match = target.Match(control); |
||||
|
|
||||
|
Assert.Equal(SelectorMatchResult.Sometimes, match.Result); |
||||
|
Assert.True(await match.Activator.Take(1)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void OfType_Not_Class_Doesnt_Match_Control_Of_Wrong_Type() |
||||
|
{ |
||||
|
var control = new Control2 |
||||
|
{ |
||||
|
Classes = new Classes { "foo" }, |
||||
|
}; |
||||
|
|
||||
|
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo")); |
||||
|
var match = target.Match(control); |
||||
|
|
||||
|
Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Returns_Correct_TargetType() |
||||
|
{ |
||||
|
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo")); |
||||
|
|
||||
|
Assert.Equal(typeof(Control1), target.TargetType); |
||||
|
} |
||||
|
|
||||
|
public class Control1 : TestControlBase |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public class Control2 : TestControlBase |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue