diff --git a/readme.md b/readme.md index 2b26cbdd1a..4fe76a2faf 100644 --- a/readme.md +++ b/readme.md @@ -18,7 +18,7 @@ Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a fl ## Getting Started -Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! +Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! Avalonia is delivered via NuGet package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) @@ -52,7 +52,7 @@ Please read the [contribution guidelines](http://avaloniaui.net/contributing/con ### Contributors This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. - + ### Backers diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index d28281e476..749f83c1ab 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -20,7 +20,7 @@ namespace ControlCatalog ctl.Cursor = new Cursor(cursor); ctl.PointerPressed += delegate { - PlatformImpl.BeginResizeDrag(edge); + PlatformImpl?.BeginResizeDrag(edge); }; } @@ -29,7 +29,7 @@ namespace ControlCatalog AvaloniaXamlLoader.Load(this); this.FindControl("TitleBar").PointerPressed += delegate { - PlatformImpl.BeginMoveDrag(); + PlatformImpl?.BeginMoveDrag(); }; SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West); SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East); diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs index 78f744cea0..041d91043a 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs @@ -110,7 +110,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform { //Not supported } - + public void SetTopmost(bool value) + { + //Not supported + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index ae70cacdba..ac7e97a4b1 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -11,6 +11,7 @@ namespace Avalonia.Data.Core { internal abstract class ExpressionNode : ISubject { + private static readonly object CacheInvalid = new object(); protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); @@ -18,6 +19,8 @@ namespace Avalonia.Data.Core private IDisposable _valueSubscription; private IObserver _observer; + protected WeakReference LastValue { get; private set; } + public abstract string Description { get; } public ExpressionNode Next { get; set; } @@ -61,6 +64,7 @@ namespace Avalonia.Data.Core { _valueSubscription?.Dispose(); _valueSubscription = null; + LastValue = null; nextSubscription?.Dispose(); _observer = null; }); @@ -120,6 +124,7 @@ namespace Avalonia.Data.Core if (notification == null) { + LastValue = new WeakReference(value); if (Next != null) { Next.Target = new WeakReference(value); @@ -131,6 +136,7 @@ namespace Avalonia.Data.Core } else { + LastValue = new WeakReference(notification.Value); if (Next != null) { Next.Target = new WeakReference(notification.Value); diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 7719f93a02..14bc09f5b7 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -154,7 +154,7 @@ namespace Avalonia.Data.Core /// public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue) { - if (Leaf is ISettableNode settable) + if (Leaf is SettableNode settable) { var node = _node; while (node != null) @@ -188,7 +188,7 @@ namespace Avalonia.Data.Core /// Gets the type of the expression result or null if the expression could not be /// evaluated. /// - public Type ResultType => (Leaf as ISettableNode)?.PropertyType; + public Type ResultType => (Leaf as SettableNode)?.PropertyType; /// /// Gets the leaf node. diff --git a/src/Avalonia.Base/Data/Core/ISettableNode.cs b/src/Avalonia.Base/Data/Core/ISettableNode.cs deleted file mode 100644 index 7788407833..0000000000 --- a/src/Avalonia.Base/Data/Core/ISettableNode.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Data; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Data.Core -{ - interface ISettableNode - { - bool SetTargetValue(object value, BindingPriority priority); - Type PropertyType { get; } - } -} diff --git a/src/Avalonia.Base/Data/Core/IndexerNode.cs b/src/Avalonia.Base/Data/Core/IndexerNode.cs index 47e82fa2d3..633d3558ee 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNode.cs @@ -15,7 +15,7 @@ using Avalonia.Data; namespace Avalonia.Data.Core { - internal class IndexerNode : ExpressionNode, ISettableNode + internal class IndexerNode : SettableNode { public IndexerNode(IList arguments) { @@ -52,7 +52,7 @@ namespace Avalonia.Data.Core return Observable.Merge(inputs).StartWith(GetValue(target)); } - public bool SetTargetValue(object value, BindingPriority priority) + protected override bool SetTargetValueCore(object value, BindingPriority priority) { var typeInfo = Target.Target.GetType().GetTypeInfo(); var list = Target.Target as IList; @@ -154,7 +154,7 @@ namespace Avalonia.Data.Core public IList Arguments { get; } - public Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; + public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; private object GetValue(object target) { diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 4dbff4602f..9d657b3144 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -10,7 +10,7 @@ using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { - internal class PropertyAccessorNode : ExpressionNode, ISettableNode + internal class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; private IPropertyAccessor _accessor; @@ -23,13 +23,17 @@ namespace Avalonia.Data.Core public override string Description => PropertyName; public string PropertyName { get; } - public Type PropertyType => _accessor?.PropertyType; + public override Type PropertyType => _accessor?.PropertyType; - public bool SetTargetValue(object value, BindingPriority priority) + protected override bool SetTargetValueCore(object value, BindingPriority priority) { if (_accessor != null) { - try { return _accessor.SetValue(value, priority); } catch { } + try + { + return _accessor.SetValue(value, priority); + } + catch { } } return false; @@ -56,7 +60,10 @@ namespace Avalonia.Data.Core () => { _accessor = accessor; - return Disposable.Create(() => _accessor = null); + return Disposable.Create(() => + { + _accessor = null; + }); }, _ => accessor); } diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs new file mode 100644 index 0000000000..092cdbe48f --- /dev/null +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -0,0 +1,38 @@ +using Avalonia.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Data.Core +{ + internal abstract class SettableNode : ExpressionNode + { + public bool SetTargetValue(object value, BindingPriority priority) + { + if (ShouldNotSet(value)) + { + return true; + } + return SetTargetValueCore(value, priority); + } + + private bool ShouldNotSet(object value) + { + if (PropertyType == null) + { + return false; + } + if (PropertyType.IsValueType) + { + return LastValue?.Target != null && LastValue.Target.Equals(value); + } + return LastValue != null && Object.ReferenceEquals(LastValue?.Target, value); + } + + protected abstract bool SetTargetValueCore(object value, BindingPriority priority); + + public abstract Type PropertyType { get; } + } +} diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 351cbf5520..96fb9be8ac 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -2150,7 +2150,7 @@ namespace Avalonia.Controls } // Update the view - if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace) + if ((e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace) && e.OldItems != null) { for (int index = 0; index < e.OldItems.Count; index++) { diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 59281c5ad0..029e4dadc8 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -769,7 +769,7 @@ namespace Avalonia.Controls } private static void UpdateDisplayDate(Calendar c, DateTime addedDate, DateTime removedDate) { - Debug.Assert(c != null, "c should not be null!"); + Contract.Requires(c != null); // If DisplayDate < DisplayDateStart, DisplayDate = DisplayDateStart if (DateTime.Compare(addedDate, c.DisplayDateRangeStart) < 0) @@ -1016,8 +1016,6 @@ namespace Avalonia.Controls internal CalendarDayButton FindDayButtonFromDay(DateTime day) { - CalendarDayButton b; - DateTime? d; CalendarItem monthControl = MonthControl; // REMOVE_RTM: should be updated if we support MultiCalendar @@ -1028,14 +1026,16 @@ namespace Avalonia.Controls { for (int childIndex = ColumnsPerMonth; childIndex < count; childIndex++) { - b = monthControl.MonthView.Children[childIndex] as CalendarDayButton; - d = b.DataContext as DateTime?; - - if (d.HasValue) + if (monthControl.MonthView.Children[childIndex] is CalendarDayButton b) { - if (DateTimeHelper.CompareDays(d.Value, day) == 0) + var d = b.DataContext as DateTime?; + + if (d.HasValue) { - return b; + if (DateTimeHelper.CompareDays(d.Value, day) == 0) + { + return b; + } } } } @@ -1044,20 +1044,6 @@ namespace Avalonia.Controls return null; } - private void Calendar_SizeChanged(object sender, EventArgs e) - { - Debug.Assert(sender is Calendar, "The sender should be a Calendar!"); - - var size = Bounds.Size; - RectangleGeometry rg = new RectangleGeometry(); - rg.Rect = new Rect(0, 0, size.Width, size.Height); - - if (Root != null) - { - Root.Clip = rg; - } - } - private void OnSelectedMonthChanged(DateTime? selectedMonth) { if (selectedMonth.HasValue) @@ -1090,7 +1076,6 @@ namespace Avalonia.Controls internal void ResetStates() { - CalendarDayButton d; CalendarItem monthControl = MonthControl; int count = RowsPerMonth * ColumnsPerMonth; if (monthControl != null) @@ -1099,7 +1084,7 @@ namespace Avalonia.Controls { for (int childIndex = ColumnsPerMonth; childIndex < count; childIndex++) { - d = monthControl.MonthView.Children[childIndex] as CalendarDayButton; + var d = (CalendarDayButton)monthControl.MonthView.Children[childIndex]; d.IgnoreMouseOverState(); } } @@ -1190,8 +1175,6 @@ namespace Avalonia.Controls if (HoverEnd != null && HoverStart != null) { int startIndex, endIndex, i; - CalendarDayButton b; - DateTime? d; CalendarItem monthControl = MonthControl; // This assumes a contiguous set of dates: @@ -1201,18 +1184,20 @@ namespace Avalonia.Controls for (i = startIndex; i <= endIndex; i++) { - b = monthControl.MonthView.Children[i] as CalendarDayButton; - b.IsSelected = true; - d = b.DataContext as DateTime?; - - if (d.HasValue && DateTimeHelper.CompareDays(HoverEnd.Value, d.Value) == 0) + if (monthControl.MonthView.Children[i] is CalendarDayButton b) { - if (FocusButton != null) + b.IsSelected = true; + var d = b.DataContext as DateTime?; + + if (d.HasValue && DateTimeHelper.CompareDays(HoverEnd.Value, d.Value) == 0) { - FocusButton.IsCurrent = false; + if (FocusButton != null) + { + FocusButton.IsCurrent = false; + } + b.IsCurrent = HasFocusInternal; + FocusButton = b; } - b.IsCurrent = HasFocusInternal; - FocusButton = b; } } } @@ -1228,8 +1213,6 @@ namespace Avalonia.Controls if (HoverEnd != null && HoverStart != null) { CalendarItem monthControl = MonthControl; - CalendarDayButton b; - DateTime? d; if (HoverEndIndex != null && HoverStartIndex != null) { @@ -1240,15 +1223,17 @@ namespace Avalonia.Controls { for (i = startIndex; i <= endIndex; i++) { - b = monthControl.MonthView.Children[i] as CalendarDayButton; - d = b.DataContext as DateTime?; - - if (d.HasValue) + if (monthControl.MonthView.Children[i] is CalendarDayButton b) { - if (!SelectedDates.Contains(d.Value)) + var d = b.DataContext as DateTime?; + + if (d.HasValue) { - b.IsSelected = false; - } + if (!SelectedDates.Contains(d.Value)) + { + b.IsSelected = false; + } + } } } } @@ -1257,7 +1242,7 @@ namespace Avalonia.Controls // It is SingleRange for (i = startIndex; i <= endIndex; i++) { - (monthControl.MonthView.Children[i] as CalendarDayButton).IsSelected = false; + ((CalendarDayButton)monthControl.MonthView.Children[i]).IsSelected = false; } } } @@ -1628,16 +1613,15 @@ namespace Avalonia.Controls e.Handled = true; } } - internal void Calendar_KeyDown(object sender, KeyEventArgs e) - { - Calendar c = sender as Calendar; - Debug.Assert(c != null, "c should not be null!"); - if (!e.Handled && c.IsEnabled) + internal void Calendar_KeyDown(KeyEventArgs e) + { + if (!e.Handled && IsEnabled) { e.Handled = ProcessCalendarKey(e); } } + internal bool ProcessCalendarKey(KeyEventArgs e) { if (DisplayMode == CalendarMode.Month) @@ -1976,7 +1960,7 @@ namespace Avalonia.Controls } } } - private void Calendar_KeyUp(object sender, KeyEventArgs e) + private void Calendar_KeyUp(KeyEventArgs e) { if (!e.Handled && (e.Key == Key.LeftShift || e.Key == Key.RightShift)) { @@ -2083,6 +2067,9 @@ namespace Avalonia.Controls DisplayDateProperty.Changed.AddClassHandler(x => x.OnDisplayDateChanged); DisplayDateStartProperty.Changed.AddClassHandler(x => x.OnDisplayDateStartChanged); DisplayDateEndProperty.Changed.AddClassHandler(x => x.OnDisplayDateEndChanged); + KeyDownEvent.AddClassHandler(x => x.Calendar_KeyDown); + KeyUpEvent.AddClassHandler(x => x.Calendar_KeyUp); + } /// @@ -2122,10 +2109,6 @@ namespace Avalonia.Controls month.Owner = this; } } - - LayoutUpdated += Calendar_SizeChanged; - KeyDown += Calendar_KeyDown; - KeyUp += Calendar_KeyUp; } } diff --git a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs index 273cda8c5b..718cc7142b 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs @@ -66,7 +66,7 @@ namespace Avalonia.Controls /// Inherited code: Requires comment 2. internal bool ContainsAny(CalendarDateRange range) { - Debug.Assert(range != null, "range should not be null!"); + Contract.Requires(range != null); int start = DateTime.Compare(Start, range.Start); diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 3432fa549d..b0cbd0be53 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -517,7 +517,7 @@ namespace Avalonia.Controls.Primitives for (int childIndex = Calendar.ColumnsPerMonth; childIndex < count; childIndex++) { CalendarDayButton childButton = MonthView.Children[childIndex] as CalendarDayButton; - Debug.Assert(childButton != null, "childButton should not be null!"); + Contract.Requires(childButton != null); childButton.Index = childIndex; SetButtonState(childButton, dateToAdd); @@ -554,7 +554,7 @@ namespace Avalonia.Controls.Primitives for (int i = childIndex; i < count; i++) { childButton = MonthView.Children[i] as CalendarDayButton; - Debug.Assert(childButton != null, "childButton should not be null!"); + Contract.Requires(childButton != null); // button needs a content to occupy the necessary space // for the content presenter childButton.Content = i.ToString(DateTimeHelper.GetCurrentDateFormat()); @@ -650,7 +650,7 @@ namespace Avalonia.Controls.Primitives foreach (object child in YearView.Children) { CalendarButton childButton = child as CalendarButton; - Debug.Assert(childButton != null, "childButton should not be null!"); + Contract.Requires(childButton != null); // There should be no time component. Time is 12:00 AM DateTime day = new DateTime(_currentMonth.Year, count + 1, 1); childButton.DataContext = day; @@ -746,7 +746,7 @@ namespace Avalonia.Controls.Primitives foreach (object child in YearView.Children) { CalendarButton childButton = child as CalendarButton; - Debug.Assert(childButton != null, "childButton should not be null!"); + Contract.Requires(childButton != null); year = decade + count; if (year <= DateTime.MaxValue.Year && year >= DateTime.MinValue.Year) @@ -826,7 +826,7 @@ namespace Avalonia.Controls.Primitives { Owner.Focus(); } - Button b = sender as Button; + Button b = (Button)sender; DateTime d; if (b.IsEnabled) @@ -863,7 +863,7 @@ namespace Avalonia.Controls.Primitives Owner.Focus(); } - Button b = sender as Button; + Button b = (Button)sender; if (b.IsEnabled) { Owner.OnPreviousClick(); @@ -878,7 +878,7 @@ namespace Avalonia.Controls.Primitives { Owner.Focus(); } - Button b = sender as Button; + Button b = (Button)sender; if (b.IsEnabled) { @@ -891,8 +891,7 @@ namespace Avalonia.Controls.Primitives { if (Owner != null) { - CalendarDayButton b = sender as CalendarDayButton; - if (_isMouseLeftButtonDown && b != null && b.IsEnabled && !b.IsBlackout) + if (_isMouseLeftButtonDown && sender is CalendarDayButton b && b.IsEnabled && !b.IsBlackout) { // Update the states of all buttons to be selected starting // from HoverStart to b @@ -918,7 +917,7 @@ namespace Avalonia.Controls.Primitives Debug.Assert(b.DataContext != null, "The DataContext should not be null!"); Owner.UnHighlightDays(); Owner.HoverEndIndex = b.Index; - Owner.HoverEnd = (DateTime)b.DataContext; + Owner.HoverEnd = (DateTime?)b.DataContext; // Update the States of the buttons Owner.HighlightDays(); return; @@ -931,7 +930,7 @@ namespace Avalonia.Controls.Primitives { if (_isMouseLeftButtonDown) { - CalendarDayButton b = sender as CalendarDayButton; + CalendarDayButton b = (CalendarDayButton)sender; // The button is in Pressed state. Change the state to normal. if (e.Device.Captured == b) e.Device.Capture(null); @@ -973,7 +972,7 @@ namespace Avalonia.Controls.Primitives if (b.IsEnabled && !b.IsBlackout) { DateTime selectedDate = (DateTime)b.DataContext; - Debug.Assert(selectedDate != null, "selectedDate should not be null!"); + Contract.Requires(selectedDate != null); _isMouseLeftButtonDown = true; // null check is added for unit tests if (e != null) @@ -1149,7 +1148,7 @@ namespace Avalonia.Controls.Primitives if (_isControlPressed && Owner.SelectionMode == CalendarSelectionMode.MultipleRange) { CalendarDayButton b = sender as CalendarDayButton; - Debug.Assert(b != null, "The sender should be a non-null CalendarDayButton!"); + Contract.Requires(b != null); if (b.IsSelected) { @@ -1169,7 +1168,7 @@ namespace Avalonia.Controls.Primitives private void Month_CalendarButtonMouseDown(object sender, PointerPressedEventArgs e) { CalendarButton b = sender as CalendarButton; - Debug.Assert(b != null, "The sender should be a non-null CalendarDayButton!"); + Contract.Requires(b != null); _isMouseLeftButtonDownYearView = true; @@ -1208,7 +1207,7 @@ namespace Avalonia.Controls.Primitives if (_isMouseLeftButtonDownYearView) { CalendarButton b = sender as CalendarButton; - Debug.Assert(b != null, "The sender should be a non-null CalendarDayButton!"); + Contract.Requires(b != null); UpdateYearViewSelection(b); } } @@ -1217,7 +1216,7 @@ namespace Avalonia.Controls.Primitives { if (_isMouseLeftButtonDownYearView) { - CalendarButton b = sender as CalendarButton; + CalendarButton b = (CalendarButton)sender; // The button is in Pressed state. Change the state to normal. if (e.Device.Captured == b) e.Device.Capture(null); diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 418ef50b2c..08608ad359 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -842,7 +842,7 @@ namespace Avalonia.Controls private void Calendar_KeyDown(object sender, KeyEventArgs e) { Calendar c = sender as Calendar; - Debug.Assert(c != null, "The Calendar should not be null!"); + Contract.Requires(c != null); if (!e.Handled && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape) && c.DisplayMode == CalendarMode.Month) { diff --git a/src/Avalonia.Controls/DropDownItem.cs b/src/Avalonia.Controls/DropDownItem.cs index 3fd80c4562..fb465e93ec 100644 --- a/src/Avalonia.Controls/DropDownItem.cs +++ b/src/Avalonia.Controls/DropDownItem.cs @@ -22,14 +22,13 @@ namespace Avalonia.Controls static DropDownItem() { FocusableProperty.OverrideDefaultValue(true); - IsFocusedProperty.Changed.Subscribe(x => - { - var sender = x.Sender as IControl; + } - if (sender != null) - { - ((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue); - } + public DropDownItem() + { + this.GetObservable(DropDownItem.IsFocusedProperty).Subscribe(focused => + { + PseudoClasses.Set(":selected", focused); }); } diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index cd2da692ba..4db16c71a5 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -57,7 +57,7 @@ namespace Avalonia.Controls.Embedding.Offscreen Type IStyleable.StyleKey => typeof(EmbeddableControlRoot); public void Dispose() { - PlatformImpl.Dispose(); + PlatformImpl?.Dispose(); } } } diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 4f7ac82df7..9ba68f584e 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -72,6 +72,11 @@ namespace Avalonia.Platform /// void SetMinMaxSize(Size minSize, Size maxSize); + /// + /// Sets whether this window appears on top of all other windows + /// + void SetTopmost(bool value); + /// /// Gets platform specific display information /// diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index e136efe2a9..52e09aba45 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -62,7 +62,7 @@ namespace Avalonia.Platform RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects); var tl = root.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - tl.PlatformImpl.Input(rawEvent); + tl.PlatformImpl?.Input(rawEvent); var effect = GetPreferredEffect(rawEvent.Effects & _allowedEffects, modifiers); UpdateCursor(root, effect); diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 144174e371..e438843078 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -126,7 +126,21 @@ namespace Avalonia.Controls.Presenters generator.Clear(); Panel.Children.RemoveAll(containers.Select(x => x.ContainerControl)); - MoveToPage(-1, SelectedIndex >= 0 ? SelectedIndex : 0); + var newIndex = SelectedIndex; + + if(SelectedIndex < 0) + { + if(Items != null && Items.Count() > 0) + { + newIndex = 0; + } + else + { + newIndex = -1; + } + } + + MoveToPage(-1, newIndex); } break; } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 656f3890cd..005717d681 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -70,6 +70,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty StaysOpenProperty = AvaloniaProperty.Register(nameof(StaysOpen), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty TopmostProperty = + AvaloniaProperty.Register(nameof(Topmost)); + private bool _isOpen; private PopupRoot _popupRoot; private TopLevel _topLevel; @@ -84,6 +90,7 @@ namespace Avalonia.Controls.Primitives IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged); + TopmostProperty.Changed.AddClassHandler((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue); } /// @@ -194,6 +201,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(StaysOpenProperty, value); } } + /// + /// Gets or sets whether this popup appears on top of all other windows + /// + public bool Topmost + { + get { return GetValue(TopmostProperty); } + set { SetValue(TopmostProperty, value); } + } + /// /// Gets the root of the popup window. /// diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 0057b15150..3ddcb06303 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -6,9 +6,21 @@ using System.Reactive; using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Interactivity; +using Avalonia.Input; namespace Avalonia.Controls.Primitives { + public class ScrollEventArgs : EventArgs + { + public ScrollEventArgs(ScrollEventType eventType, double newValue) + { + ScrollEventType = eventType; + NewValue = newValue; + } + public double NewValue { get; private set; } + public ScrollEventType ScrollEventType { get; private set; } + } + /// /// A scrollbar control. /// @@ -44,6 +56,9 @@ namespace Avalonia.Controls.Primitives { PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); + + Thumb.DragDeltaEvent.AddClassHandler(o => o.OnThumbDragDelta, RoutingStrategies.Bubble); + Thumb.DragCompletedEvent.AddClassHandler(o => o.OnThumbDragComplete, RoutingStrategies.Bubble); } /// @@ -88,6 +103,8 @@ namespace Avalonia.Controls.Primitives set { SetValue(OrientationProperty, value); } } + public event EventHandler Scroll; + /// /// Calculates whether the scrollbar should be visible. /// @@ -140,6 +157,8 @@ namespace Avalonia.Controls.Primitives _pageUpButton = e.NameScope.Find - /// - public static implicit operator FontFamily(string fontFamily) + /// + public static implicit operator FontFamily(string s) { - return new FontFamily(fontFamily); + return Parse(s); } /// diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index 1adaf9f4e1..9cc6ba6901 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -261,10 +261,13 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_unmaximize(GtkWindow window); - + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_close(GtkWindow window); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_set_keep_above(GtkWindow gtkWindow, bool setting); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_set_geometry_hints(GtkWindow window, IntPtr geometry_widget, ref GdkGeometry geometry, GdkWindowHints geom_mask); @@ -472,6 +475,7 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_window_maximize GtkWindowMaximize; public static D.gtk_window_unmaximize GtkWindowUnmaximize; public static D.gtk_window_close GtkWindowClose; + public static D.gtk_window_set_keep_above GtkWindowSetKeepAbove; public static D.gdk_window_begin_move_drag GdkWindowBeginMoveDrag; public static D.gdk_window_begin_resize_drag GdkWindowBeginResizeDrag; public static D.gdk_event_request_motions GdkEventRequestMotions; diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 94537d3475..8a880fd306 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -416,6 +416,8 @@ namespace Avalonia.Gtk3 public void Hide() => Native.GtkWidgetHide(GtkWidget); + public void SetTopmost(bool value) => Native.GtkWindowSetKeepAbove(GtkWidget, value); + void GetGlobalPointer(out int x, out int y) { int mask; diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs index bae34db6f3..a0b754c229 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -81,7 +81,7 @@ namespace Avalonia.Gtk3 public void ShowTaskbarIcon(bool value) => Native.GtkWindowSetSkipTaskbarHint(GtkWidget, !value); public void CanResize(bool value) => Native.GtkWindowSetResizable(GtkWidget, value); - + class EmptyDisposable : IDisposable { diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index f6f62dcd1d..a3fa6880ac 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -10,6 +10,7 @@ using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Metadata; +using JetBrains.Annotations; namespace Avalonia.Data { @@ -65,7 +66,7 @@ namespace Avalonia.Data var children = Bindings.Select(x => x.Initiate(target, null)); var input = children.Select(x => x.Subject).CombineLatest().Select(x => ConvertValue(x, targetType)); var mode = Mode == BindingMode.Default ? - targetProperty.GetMetadata(target.GetType()).DefaultBindingMode : Mode; + targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; switch (mode) { diff --git a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs index 8cbc6cbdd8..89cef59b53 100644 --- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs @@ -123,6 +123,7 @@ namespace Avalonia.MonoMac public void Hide() => Window?.OrderOut(Window); + public void SetTopmost(bool value) => Window.Level = value ? NSWindowLevel.Floating : NSWindowLevel.Normal; public void BeginMoveDrag() { diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 00f0a48a7b..13dcd9669d 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -560,13 +560,11 @@ namespace Avalonia.Skia } measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber); - AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); line.TextLength = measured; - + line.Start = curOff; subString = Text.Substring(line.Start, line.TextLength); lineWidth = _paint.MeasureText(subString); - line.Start = curOff; line.Length = measured - trailingnumber; line.Width = lineWidth; line.Height = _lineHeight; @@ -575,10 +573,33 @@ namespace Avalonia.Skia _skiaLines.Add(line); curY += _lineHeight; - curY += mLeading; - curOff += measured; + + //if this is the last line and there are trailing newline characters then + //insert a additional line + if (curOff >= length) + { + var subStringMinusNewlines = subString.TrimEnd('\n', '\r'); + var lengthDiff = subString.Length - subStringMinusNewlines.Length; + if (lengthDiff > 0) + { + AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine(); + lastLine.TextLength = lengthDiff; + lastLine.Start = curOff - lengthDiff; + var lastLineSubString = Text.Substring(line.Start, line.TextLength); + var lastLineWidth = _paint.MeasureText(lastLineSubString); + lastLine.Length = 0; + lastLine.Width = lastLineWidth; + lastLine.Height = _lineHeight; + lastLine.Top = curY; + + _skiaLines.Add(lastLine); + + curY += _lineHeight; + curY += mLeading; + } + } } // Now convert to Avalonia data formats diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 86dcec410b..daaee9636e 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -78,6 +78,14 @@ namespace Avalonia.Win32.Interop SWP_RESIZE = SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER } + public static class WindowPosZOrder + { + public static readonly IntPtr HWND_BOTTOM = new IntPtr(1); + public static readonly IntPtr HWND_TOP = new IntPtr(0); + public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); + } + public enum SizeCommand { Restored, diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index e08fecbbd9..9b6bf57e92 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -54,7 +54,7 @@ namespace Avalonia.Win32 var fileBuffer = new char[256]; dialog.InitialFileName?.CopyTo(0, fileBuffer, 0, dialog.InitialFileName.Length); - string userSelectedExt = null; + string userSelectedExt = string.Empty; var title = ToChars(dialog.Title); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 159c8386b6..9f67f97252 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -32,6 +32,7 @@ namespace Avalonia.Win32 private bool _trackingMouse; private bool _decorated = true; private bool _resizable = true; + private bool _topmost = false; private double _scaling = 1; private WindowState _showWindowState; private WindowState _lastWindowState; @@ -841,7 +842,7 @@ namespace Avalonia.Win32 var cx = Math.Abs(monitorInfo.rcWork.right - x); var cy = Math.Abs(monitorInfo.rcWork.bottom - y); - SetWindowPos(_hwnd, new IntPtr(-2), x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); + SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } } @@ -904,5 +905,21 @@ namespace Avalonia.Win32 _resizable = value; } + + public void SetTopmost(bool value) + { + if (value == _topmost) + { + return; + } + + IntPtr hWndInsertAfter = value ? WindowPosZOrder.HWND_TOPMOST : WindowPosZOrder.HWND_NOTOPMOST; + UnmanagedMethods.SetWindowPos(_hwnd, + hWndInsertAfter, + 0, 0, 0, 0, + SetWindowPosFlags.SWP_NOMOVE | SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE); + + _topmost = value; + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 02fb1f11ad..4638aa84a5 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -457,6 +457,28 @@ namespace Avalonia.Base.UnitTests Assert.True(target.IsAnimating(Class1.FooProperty)); } + [Fact] + public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + + target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); + + Assert.False(source.SetterCalled); + } + + [Fact] + public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + + target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source }); + + Assert.False(source.SetterCalled); + } + /// /// Returns an observable that returns a single value but does not complete. /// @@ -545,5 +567,32 @@ namespace Avalonia.Base.UnitTests } } } + + private class TestTwoWayBindingViewModel + { + private double _value; + + public double Value + { + get => _value; + set + { + _value = value; + SetterCalled = true; + } + } + + public double this[int index] + { + get => _value; + set + { + _value = value; + SetterCalled = true; + } + } + + public bool SetterCalled { get; private set; } + } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index df61698209..f36e5864f6 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -170,6 +170,41 @@ namespace Avalonia.Controls.UnitTests Assert.Equal("Bar", ((TextBlock)child).Text); } + [Fact] + public void Selected_Index_Changes_To_When_Items_Assigned_Null() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; + + var target = new Carousel + { + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + Assert.Single(target.GetLogicalChildren()); + + var child = target.GetLogicalChildren().Single(); + + Assert.IsType(child); + Assert.Equal("Foo", ((TextBlock)child).Text); + + target.Items = null; + + var numChildren = target.GetLogicalChildren().Count(); + + Assert.Equal(0, numChildren); + Assert.Equal(-1, target.SelectedIndex); + } + [Fact] public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs index 0af4d791c7..672b5c608b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Input; using Avalonia.Media; using Xunit; @@ -59,6 +60,64 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(50, target.Value); } + [Fact] + public void Thumb_DragDelta_Event_Should_Raise_Scroll_Event() + { + var target = new ScrollBar + { + Template = new FuncControlTemplate(Template), + }; + + target.ApplyTemplate(); + + var track = (Track)target.GetTemplateChildren().First(x => x.Name == "track"); + + var raisedEvent = Assert.Raises( + handler => target.Scroll += handler, + handler => target.Scroll -= handler, + () => + { + var ev = new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, 0) + }; + + track.Thumb.RaiseEvent(ev); + }); + + Assert.Equal(ScrollEventType.ThumbTrack, raisedEvent.Arguments.ScrollEventType); + } + + [Fact] + public void Thumb_DragComplete_Event_Should_Raise_Scroll_Event() + { + var target = new ScrollBar + { + Template = new FuncControlTemplate(Template), + }; + + target.ApplyTemplate(); + + var track = (Track)target.GetTemplateChildren().First(x => x.Name == "track"); + + var raisedEvent = Assert.Raises( + handler => target.Scroll += handler, + handler => target.Scroll -= handler, + () => + { + var ev = new VectorEventArgs + { + RoutedEvent = Thumb.DragCompletedEvent, + Vector = new Vector(0, 0) + }; + + track.Thumb.RaiseEvent(ev); + }); + + Assert.Equal(ScrollEventType.EndScroll, raisedEvent.Arguments.ScrollEventType); + } + [Fact] public void ScrollBar_Can_AutoHide() {