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\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.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