diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 4b814a9cfb..a35f4f3eeb 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -144,6 +144,7 @@ enum AvnStandardCursorType CursorDragMove, CursorDragCopy, CursorDragLink, + CursorNone }; enum AvnWindowEdge diff --git a/native/Avalonia.Native/src/OSX/cursor.h b/native/Avalonia.Native/src/OSX/cursor.h index a8eb49c0b9..cfe91955d8 100644 --- a/native/Avalonia.Native/src/OSX/cursor.h +++ b/native/Avalonia.Native/src/OSX/cursor.h @@ -11,18 +11,24 @@ class Cursor : public ComSingleObject { private: NSCursor * _native; - + bool _isHidden; public: FORWARD_IUNKNOWN() - Cursor(NSCursor * cursor) + Cursor(NSCursor * cursor, bool isHidden = false) { _native = cursor; + _isHidden = isHidden; } NSCursor* GetNative() { return _native; } + + bool IsHidden () + { + return _isHidden; + } }; extern std::map s_cursorMap; diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index bd2c94a4d8..799fa9e8e6 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -21,6 +21,7 @@ class CursorFactory : public ComSingleObject s_cursorMap = { @@ -46,11 +47,13 @@ class CursorFactory : public ComSingleObject(cursor); this->cursor = avnCursor->GetNative(); UpdateCursor(); + + if(avnCursor->IsHidden()) + { + [NSCursor hide]; + } + else + { + [NSCursor unhide]; + } + return S_OK; } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 152b551f9a..7b91d6235d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -54,7 +54,8 @@ namespace Avalonia.Controls.Primitives nameof(SelectedIndex), o => o.SelectedIndex, (o, v) => o.SelectedIndex = v, - unsetValue: -1); + unsetValue: -1, + defaultBindingMode: BindingMode.TwoWay); /// /// Defines the property. diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index d3618f30f3..8139af1659 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -38,6 +38,7 @@ namespace Avalonia.Input DragMove, DragCopy, DragLink, + None, // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index f67cb7f40a..ced26a3004 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI return builder.AfterSetup(_ => { RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; - Locator.CurrentMutable.Register( - () => new AvaloniaActivationForViewFetcher(), - typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); }); } } diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs new file mode 100644 index 0000000000..3f41f54363 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using ReactiveUI; + +namespace Avalonia.ReactiveUI +{ + /// + /// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls + /// that don't have DataTemplates, and assigns a default DataTemplate that + /// loads the View associated with each ViewModel. + /// + public class AutoDataTemplateBindingHook : IPropertyBindingHook + { + private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate(x => + { + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); + + /// + public bool ExecuteHook( + object source, object target, + Func[]> getCurrentViewModelProperties, + Func[]> getCurrentViewProperties, + BindingDirection direction) + { + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + var itemsControl = lastViewProperty?.Sender as ItemsControl; + if (itemsControl == null) + return true; + + var propertyName = viewProperties.Last().GetPropertyName(); + if (propertyName != "Items" && + propertyName != "ItemsSource") + return true; + + if (itemsControl.ItemTemplate != null) + return true; + + itemsControl.ItemTemplate = DefaultItemTemplate; + return true; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 4bd86a67c0..05edeea683 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + public class RoutedViewHost : TransitioningContentControl, IActivatable, IEnableLogger { /// - /// The router dependency property. + /// for the property. /// public static readonly AvaloniaProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); - - /// - /// The default content property. - /// - public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); - - /// - /// Fade in animation property. - /// - public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); - - /// - /// Fade out animation property. - /// - public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI set => SetValue(RouterProperty, value); } - /// - /// Gets or sets the content displayed whenever there is no page currently routed. - /// - public object DefaultContent - { - get => GetValue(DefaultContentProperty); - set => SetValue(DefaultContentProperty, value); - } - - /// - /// Gets or sets the animation played when page appears. - /// - public IAnimation FadeInAnimation - { - get => GetValue(FadeInAnimationProperty); - set => SetValue(FadeInAnimationProperty, value); - } - - /// - /// Gets or sets the animation played when page disappears. - /// - public IAnimation FadeOutAnimation - { - get => GetValue(FadeOutAnimationProperty); - set => SetValue(FadeOutAnimationProperty, value); - } - - /// - /// Duplicates the Content property with a private setter. - /// - public new object Content - { - get => base.Content; - private set => base.Content = value; - } - /// /// Gets or sets the ReactiveUI view locator used by this router. /// @@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - /// - /// Thrown when ViewLocator is unable to find the appropriate view. - /// - private void NavigateToViewModel(IRoutableViewModel viewModel) + private void NavigateToViewModel(object viewModel) { if (viewModel == null) { - this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; return; } var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(viewModel); - if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } - this.Log().Info($"Ready to show {view} with autowired {viewModel}."); - view.ViewModel = viewModel; - if (view is IStyledElement styled) + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) styled.DataContext = viewModel; - UpdateContent(view); - } - - /// - /// Updates the content with transitions. - /// - /// New content to set. - private async void UpdateContent(object newContent) - { - if (FadeOutAnimation != null) - await FadeOutAnimation.RunAsync(this, Clock); - Content = newContent; - if (FadeInAnimation != null) - await FadeInAnimation.RunAsync(this, Clock); - } - - /// - /// Creates opacity animation for this routed view host. - /// - /// Opacity to start from. - /// Opacity to finish with. - /// Duration of the animation. - /// Animation object instance. - 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) - } - } - }; + Content = viewInstance; } } -} +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs new file mode 100644 index 0000000000..1bec5fc365 --- /dev/null +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -0,0 +1,75 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.ReactiveUI +{ + /// + /// A ContentControl that animates the transition when its content is changed. + /// + public class TransitioningContentControl : ContentControl, IStyleable + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty PageTransitionProperty = + AvaloniaProperty.Register(nameof(PageTransition), + new CrossFade(TimeSpan.FromSeconds(0.5))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Gets or sets the animation played when content appears and disappears. + /// + public IPageTransition PageTransition + { + get => GetValue(PageTransitionProperty); + set => SetValue(PageTransitionProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the content with animation. + /// + public new object Content + { + get => base.Content; + set => UpdateContentWithTransition(value); + } + + /// + /// TransitioningContentControl uses the default ContentControl + /// template from Avalonia default theme. + /// + Type IStyleable.StyleKey => typeof(ContentControl); + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContentWithTransition(object content) + { + if (PageTransition != null) + await PageTransition.Start(this, null, true); + base.Content = content; + if (PageTransition != null) + await PageTransition.Start(null, this, true); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs new file mode 100644 index 0000000000..5cfa464c37 --- /dev/null +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -0,0 +1,80 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; +using ReactiveUI; +using Splat; + +namespace Avalonia.ReactiveUI +{ + /// + /// This content control will automatically load the View associated with + /// the ViewModel property and display it. This control is very useful + /// inside a DataTemplate to display the View associated with a ViewModel. + /// + public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty ViewModelProperty = + AvaloniaProperty.Register(nameof(ViewModel)); + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyValue(x => x.ViewModel) + .Subscribe(NavigateToViewModel) + .DisposeWith(disposables); + }); + } + + /// + /// Gets or sets the ViewModel to display. + /// + public object ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the view locator. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + private void NavigateToViewModel(object viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } + + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) + styled.DataContext = viewModel; + Content = viewInstance; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index f84e09510b..8f7d56dbc6 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -46,11 +46,11 @@ - - - - - + + + + + 1,1,1,1 0.5 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 18c32b02bc..666596d710 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -46,11 +46,11 @@ - - - - - + + + + + 1 0.5 diff --git a/src/Avalonia.Themes.Default/NotificationCard.xaml b/src/Avalonia.Themes.Default/NotificationCard.xaml index e94cb33d1e..47d5988e8c 100644 --- a/src/Avalonia.Themes.Default/NotificationCard.xaml +++ b/src/Avalonia.Themes.Default/NotificationCard.xaml @@ -13,7 +13,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Margin="8,8,0,0"> - + @@ -40,6 +40,10 @@ + +