diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index dea9b35e24..61f2443eb7 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 87cb5e9c5c..ec3bf799b4 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -20,8 +20,9 @@ + - + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml new file mode 100644 index 0000000000..3dd8be91c2 --- /dev/null +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -0,0 +1,13 @@ + + + ListBox + Hosts a collection of ListBoxItem. + + + + + + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs new file mode 100644 index 0000000000..dbe6c74800 --- /dev/null +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ListBoxPage : UserControl + { + public ListBoxPage() + { + this.InitializeComponent(); + DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" ) + .ToArray(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + } +} diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index 305bcd177c..2bb6214b58 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -14,7 +14,7 @@ AllowSpin: - + ClipValueToMinMax: @@ -77,4 +77,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index 34aa85b8aa..fd66185832 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -42,7 +42,11 @@ namespace ControlCatalog.Pages context.DrawRectangle(p, boundsRect); context.DrawRectangle(p, workingAreaRect); - FormattedText text = new FormattedText(); + FormattedText text = new FormattedText() + { + Typeface = Typeface.Default + }; + text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}"; context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height), text); @@ -59,4 +63,4 @@ namespace ControlCatalog.Pages context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10)); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 6da6da54a5..f9241956bd 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -45,6 +45,8 @@ namespace Avalonia.Controls static ContentControl() { ContentControlMixin.Attach(ContentProperty, x => x.LogicalChildren); + PseudoClass(ContentProperty, x => x != null, ":valid"); + PseudoClass(ContentProperty, x => x == null, ":invalid"); } /// diff --git a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs new file mode 100644 index 0000000000..54bd6bcf39 --- /dev/null +++ b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + public class MarginMultiplierConverter : IValueConverter + { + public double Indent { get; set; } + + public bool Left { get; set; } = false; + + public bool Top { get; set; } = false; + + public bool Right { get; set; } = false; + + public bool Bottom { get; set; } = false; + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (!(value is int depth)) + return new Thickness(0); + + return new Thickness(Left ? Indent * depth : 0, Top ? Indent * depth : 0, Right ? Indent * depth : 0, Bottom ? Indent * depth : 0); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 3003dab85e..c574799724 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -1,6 +1,8 @@ // 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.Collections.Generic; using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; @@ -31,6 +33,14 @@ namespace Avalonia.Controls o => o.SelectedItem, (o, v) => o.SelectedItem = v); + /// + /// Defines the event. + /// + public static readonly RoutedEvent SelectedItemChangedEvent = + RoutedEvent.Register( + "SelectedItemChanged", + RoutingStrategies.Bubble); + private object _selectedItem; /// @@ -41,6 +51,15 @@ namespace Avalonia.Controls // HACK: Needed or SelectedItem property will not be found in Release build. } + /// + /// Occurs when the control's selection changes. + /// + public event EventHandler SelectedItemChanged + { + add { AddHandler(SelectedItemChangedEvent, value); } + remove { RemoveHandler(SelectedItemChangedEvent, value); } + } + /// /// Gets the for the tree view. /// @@ -74,6 +93,7 @@ namespace Avalonia.Controls MarkContainerSelected(container, false); } + var oldItem = _selectedItem; SetAndRaise(SelectedItemProperty, ref _selectedItem, value); if (_selectedItem != null) @@ -86,6 +106,28 @@ namespace Avalonia.Controls container.BringIntoView(); } } + + if (oldItem != _selectedItem) + { + // Fire the SelectionChanged event + List removed = new List(); + if (oldItem != null) + { + removed.Add(oldItem); + } + + List added = new List(); + if (_selectedItem != null) + { + added.Add(_selectedItem); + } + + var changed = new SelectionChangedEventArgs( + SelectedItemChangedEvent, + added, + removed); + RaiseEvent(changed); + } } } diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 0886c05038..4d229390cb 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls /// public static readonly DirectProperty IsExpandedProperty = AvaloniaProperty.RegisterDirect( - "IsExpanded", + nameof(IsExpanded), o => o.IsExpanded, (o, v) => o.IsExpanded = v); @@ -31,17 +31,25 @@ namespace Avalonia.Controls public static readonly StyledProperty IsSelectedProperty = ListBoxItem.IsSelectedProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly DirectProperty LevelProperty = + AvaloniaProperty.RegisterDirect( + nameof(Level), o => o.Level); + private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); private TreeView _treeView; private bool _isExpanded; + private int _level; /// /// Initializes static members of the class. /// static TreeViewItem() - { + { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); @@ -65,6 +73,15 @@ namespace Avalonia.Controls set { SetValue(IsSelectedProperty, value); } } + /// + /// Gets the level/indentation of the item. + /// + public int Level + { + get { return _level; } + private set { SetAndRaise(LevelProperty, ref _level, value); } + } + /// /// Gets the for the tree view. /// @@ -89,6 +106,8 @@ namespace Avalonia.Controls base.OnAttachedToLogicalTree(e); _treeView = this.GetLogicalAncestors().OfType().FirstOrDefault(); + Level = CalculateDistanceFromLogicalParent(this) - 1; + if (ItemTemplate == null && _treeView?.ItemTemplate != null) { ItemTemplate = _treeView.ItemTemplate; @@ -126,5 +145,18 @@ namespace Avalonia.Controls // Don't call base.OnKeyDown - let events bubble up to containing TreeView. } + + private static int CalculateDistanceFromLogicalParent(ILogical logical, int @default = -1) where T : class + { + var result = 0; + + while (logical != null && logical.GetType() != typeof(T)) + { + ++result; + logical = logical.LogicalParent; + } + + return logical != null ? result : @default; + } } } diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs index 57d5ad9271..25f12ffa57 100644 --- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs @@ -60,7 +60,7 @@ namespace Avalonia.Styling builder.Append(_property.Name); builder.Append('='); - builder.Append(_value); + builder.Append(_value ?? string.Empty); builder.Append(']'); _selectorString = builder.ToString(); @@ -78,11 +78,11 @@ namespace Avalonia.Styling } else if (subscribe) { - return new SelectorMatch(control.GetObservable(_property).Select(v => Equals(v, _value))); + return new SelectorMatch(control.GetObservable(_property).Select(v => Equals(v ?? string.Empty, _value))); } else { - return new SelectorMatch(control.GetValue(_property).Equals(_value)); + return new SelectorMatch((control.GetValue(_property) ?? string.Empty).Equals(_value)); } } diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 4c85e172ff..46767feca0 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -1,30 +1,56 @@ \ No newline at end of file + #FF086F9E + #FFFF0000 + #10FF0000 + + + + + + + + + + + + + + + + + + + + + + + 2 + 0.5 + + 10 + 12 + 16 + + diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index 6a9af487cb..11d8a344d9 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -36,8 +36,4 @@ - - - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/CalendarButton.xaml b/src/Avalonia.Themes.Default/CalendarButton.xaml index 84969c135f..b70740e0c8 100644 --- a/src/Avalonia.Themes.Default/CalendarButton.xaml +++ b/src/Avalonia.Themes.Default/CalendarButton.xaml @@ -7,6 +7,7 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/CalendarDayButton.xaml b/src/Avalonia.Themes.Default/CalendarDayButton.xaml index ee7afdc73b..4a971ef0cb 100644 --- a/src/Avalonia.Themes.Default/CalendarDayButton.xaml +++ b/src/Avalonia.Themes.Default/CalendarDayButton.xaml @@ -42,7 +42,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RenderTransformOrigin="0.5,0.5" - Fill="#FF000000" + Fill="{DynamicResource ThemeForegroundBrush}" Stretch="Fill" Data="M8.1772461,11.029181 L10.433105,11.029181 L11.700684,12.801641 L12.973633,11.029181 L15.191895,11.029181 L12.844727,13.999395 L15.21875,17.060919 L12.962891,17.060919 L11.673828,15.256231 L10.352539,17.060919 L8.1396484,17.060919 L10.519043,14.042364 z" /> @@ -103,7 +103,7 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/CalendarItem.xaml b/src/Avalonia.Themes.Default/CalendarItem.xaml index 3d3d75a39a..b52b7acf53 100644 --- a/src/Avalonia.Themes.Default/CalendarItem.xaml +++ b/src/Avalonia.Themes.Default/CalendarItem.xaml @@ -153,7 +153,7 @@ @@ -180,4 +180,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/CheckBox.xaml b/src/Avalonia.Themes.Default/CheckBox.xaml index 62a05a3525..3d14dd8e51 100644 --- a/src/Avalonia.Themes.Default/CheckBox.xaml +++ b/src/Avalonia.Themes.Default/CheckBox.xaml @@ -40,6 +40,9 @@ + @@ -58,4 +61,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/DataValidationErrors.xaml index f7f28d90d0..16c2d6adef 100644 --- a/src/Avalonia.Themes.Default/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Default/DataValidationErrors.xaml @@ -21,10 +21,10 @@ + Background="Transparent"> @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/DatePicker.xaml index b706b5b4e5..93bafdb56f 100644 --- a/src/Avalonia.Themes.Default/DatePicker.xaml +++ b/src/Avalonia.Themes.Default/DatePicker.xaml @@ -6,7 +6,8 @@ --> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:sys="clr-namespace:System;assembly=mscorlib"> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/DropDownItem.xaml b/src/Avalonia.Themes.Default/DropDownItem.xaml index 257030d8af..f52608c0a8 100644 --- a/src/Avalonia.Themes.Default/DropDownItem.xaml +++ b/src/Avalonia.Themes.Default/DropDownItem.xaml @@ -18,10 +18,24 @@ + + + - + + + + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/Expander.xaml b/src/Avalonia.Themes.Default/Expander.xaml index 0bea0c9763..d12f704cc4 100644 --- a/src/Avalonia.Themes.Default/Expander.xaml +++ b/src/Avalonia.Themes.Default/Expander.xaml @@ -108,7 +108,7 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ListBoxItem.xaml b/src/Avalonia.Themes.Default/ListBoxItem.xaml index fc2600c1a9..19a6e3d4ec 100644 --- a/src/Avalonia.Themes.Default/ListBoxItem.xaml +++ b/src/Avalonia.Themes.Default/ListBoxItem.xaml @@ -1,6 +1,9 @@ + + + + + + + + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index b24c863be9..ef57474eaf 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/src/Avalonia.Themes.Default/ScrollBar.xaml @@ -10,7 +10,7 @@ Grid.Column="0"> + Fill="{DynamicResource ThemeForegroundLightBrush}" /> + Fill="{DynamicResource ThemeForegroundLightBrush}" /> @@ -61,7 +61,7 @@ Grid.Column="0"> + Fill="{DynamicResource ThemeForegroundLightBrush}" /> + Fill="{DynamicResource ThemeForegroundLightBrush}" /> @@ -124,4 +124,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index 58fd67b2f6..95924b31a1 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -80,10 +80,11 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml index 95347044bc..b5e0e7a005 100644 --- a/src/Avalonia.Themes.Default/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml @@ -1,69 +1,92 @@ - - + + - + - - - - - - - + - - \ No newline at end of file + + + + + + + + + + + + + + diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 40e98d1565..6dde2bb591 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -7,6 +7,8 @@ namespace Avalonia.Media /// public class Typeface { + public static Typeface Default = new Typeface(FontFamily.Default); + /// /// Initializes a new instance of the class. /// diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index c49c343a45..1a913865cb 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -166,6 +166,39 @@ namespace Avalonia.Controls.UnitTests Assert.True(container.IsSelected); } + + [Fact] + public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var item = tree[0].Children[1].Children[0]; + + var called = false; + target.SelectedItemChanged += (s, e) => + { + Assert.Empty(e.RemovedItems); + Assert.Equal(1, e.AddedItems.Count); + Assert.Same(item, e.AddedItems[0]); + called = true; + }; + + target.SelectedItem = item; + Assert.True(called); + } + + [Fact] public void LogicalChildren_Should_Be_Set() {