From 5bf12ac0f0be4d1624ccf22776f5ad7a7e8cf82d Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Wed, 16 Sep 2020 12:58:05 +0300 Subject: [PATCH 01/13] Added TextSelector and AutoCompleteMode properties to the AutoCompleteBox Now it's possible to change the logic for modifying the text of the control after choosing the autocomplete option --- src/Avalonia.Controls/AutoCompleteBox.cs | 222 ++++++++++++++++++++++- 1 file changed, 221 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c164f282e8..16b7d09e8a 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -225,6 +225,52 @@ namespace Avalonia.Controls Custom = 13, } + /// + /// Represents the selector used by the + /// control to + /// determine how the specified text should be modified with an item. + /// + /// + /// Modified text that will be used by the + /// . + /// + /// The string used as the basis for filtering. + /// + /// The selected item that should be combined with the + /// parameter. + /// + /// + /// The type used for filtering the + /// . + /// At the moment this type known only as a string. + /// + public delegate string AutoCompleteSelector(string search, T item); + + /// + /// Specifies how the selected autocomplete result should be treated. + /// + public enum AutoCompleteMode + { + /// + /// Specifies that the text will be replaced + /// with the selected autocomplete result. + /// + Replace = 0, + + /// + /// Specifies that the selected autocomplete result + /// will be appended to the text. + /// + Append = 1, + + /// + /// Specifies that a custom selector is used. This mode is used when + /// the + /// property is set. + /// + Custom = 2 + } + /// /// Represents a control that provides a text box for user input and a /// drop-down that contains possible matches based on the input in the text @@ -362,6 +408,8 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + private AutoCompleteSelector _textSelector = AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace); + public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); @@ -499,6 +547,17 @@ namespace Avalonia.Controls defaultValue: AutoCompleteFilterMode.StartsWith, validate: IsValidFilterMode); + /// + /// Gets the identifier for the + /// + /// dependency property. + /// + public static readonly StyledProperty AutoCompleteModeProperty = + AvaloniaProperty.Register( + nameof(AutoCompleteMode), + defaultValue: AutoCompleteMode.Replace, + validate: IsValidAutoCompleteMode); + /// /// Identifies the /// @@ -528,6 +587,21 @@ namespace Avalonia.Controls (o, v) => o.TextFilter = v, unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> TextSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(TextSelector), + o => o.TextSelector, + (o, v) => o.TextSelector = v, + unsetValue: AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace)); + /// /// Identifies the /// @@ -578,6 +652,19 @@ namespace Avalonia.Controls } } + private static bool IsValidAutoCompleteMode(AutoCompleteMode mode) + { + switch (mode) + { + case AutoCompleteMode.Replace: + case AutoCompleteMode.Append: + case AutoCompleteMode.Custom: + return true; + default: + return false; + } + } + /// /// Handle the change of the IsEnabled property. /// @@ -728,6 +815,19 @@ namespace Avalonia.Controls TextFilter = AutoCompleteSearch.GetFilter(mode); } + /// + /// AutoCompleteModeProperty property changed handler. + /// + /// Event arguments. + private void OnAutoCompleteModePropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteMode mode = (AutoCompleteMode)e.NewValue; + + // Sets the text selector for the new value + if (mode != AutoCompleteMode.Custom) + TextSelector = AutoCompleteSelection.GetSelector(mode); + } + /// /// ItemFilterProperty property changed handler. /// @@ -748,6 +848,25 @@ namespace Avalonia.Controls } } + /// + /// TextSelectorProperty property changed handler. + /// + /// Event arguments. + private void OnTextSelectorPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteSelector value = e.NewValue as AutoCompleteSelector; + + // If null, revert to the "Replace" predicate + if (value == null) + { + AutoCompleteMode = AutoCompleteMode.Replace; + } + else if (value.Method.DeclaringType != typeof(AutoCompleteSelection)) + { + AutoCompleteMode = AutoCompleteMode.Custom; + } + } + /// /// ItemsSourceProperty property changed handler. /// @@ -793,6 +912,8 @@ namespace Avalonia.Controls SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); + AutoCompleteModeProperty.Changed.AddClassHandler((x,e) => x.OnAutoCompleteModePropertyChanged(e)); + TextSelectorProperty.Changed.AddClassHandler((x,e) => x.OnTextSelectorPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } @@ -1015,6 +1136,31 @@ namespace Avalonia.Controls set { SetValue(FilterModeProperty, value); } } + /// + /// Gets or sets how the text in the text box will be modified + /// with the selected autocomplete item. + /// + /// + /// One of the + /// values. The default is + /// . + /// + /// The specified value is not a valid + /// . + /// + /// + /// Use the AutoCompleteMode property to specify the way the text will + /// be modified with the selected autocomplete item. For example, text + /// can be modified in a predefined or custom way. The autocomplete + /// mode is automatically set to Custom if you set the TextSelector + /// property. + /// + public AutoCompleteMode AutoCompleteMode + { + get { return GetValue(AutoCompleteModeProperty); } + set { SetValue(AutoCompleteModeProperty, value); } + } + public string Watermark { get { return GetValue(WatermarkProperty); } @@ -1061,6 +1207,26 @@ namespace Avalonia.Controls set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } } + /// + /// Gets or sets the custom method that combines the user-entered + /// text to and one of the items specified by the + /// . + /// + /// + /// The custom method that combines the user-entered + /// text to and one of the items specified by the + /// . + /// + /// + /// The AutoCompleteMode is automatically set to Custom if you set + /// the TextSelector property. + /// + public AutoCompleteSelector TextSelector + { + get { return _textSelector; } + set { SetAndRaise(TextSelectorProperty, ref _textSelector, value); } + } + public Func>> AsyncPopulator { get { return _asyncPopulator; } @@ -2331,7 +2497,7 @@ namespace Avalonia.Controls } else { - text = FormatValue(newItem, true); + text = TextSelector(SearchText, FormatValue(newItem, true)); } // Update the Text property and the TextBox values @@ -2590,6 +2756,60 @@ namespace Avalonia.Controls } } + /// + /// A predefined set of selector functions for the known, built-in + /// AutoCompleteMode enumeration values. + /// + private static class AutoCompleteSelection + { + /// + /// Index function that retrieves the selector for the provided + /// AutoCompleteMode. + /// + /// The built-in autocomplete mode. + /// Returns the string-based selector function. + public static AutoCompleteSelector GetSelector(AutoCompleteMode completeMode) + { + switch (completeMode) + { + case AutoCompleteMode.Replace: + return Replace; + case AutoCompleteMode.Append: + return Append; + case AutoCompleteMode.Custom: + default: + return null; + } + } + + /// + /// Implements AutoCompleteMode.Replace. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// + /// Return the and ignores the + /// . + /// + private static string Replace(string text, string value) + { + return value ?? String.Empty; + } + + /// + /// Implements AutoCompleteMode.Append. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// + /// Returns the concatenated string. + /// + private static string Append(string text, string value) + { + return text + value; + } + } + /// /// A framework element that permits a binding to be evaluated in a new data /// context leaf node. From 9a1dd58273ff33d3c53cfb9d9f288d21e8e6bf38 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Wed, 16 Sep 2020 12:59:58 +0300 Subject: [PATCH 02/13] Well, default AutoCompleteMode.Append isn't doing well, tbh --- src/Avalonia.Controls/AutoCompleteBox.cs | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 16b7d09e8a..22b09ef110 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -257,18 +257,12 @@ namespace Avalonia.Controls /// Replace = 0, - /// - /// Specifies that the selected autocomplete result - /// will be appended to the text. - /// - Append = 1, - /// /// Specifies that a custom selector is used. This mode is used when /// the /// property is set. /// - Custom = 2 + Custom = 1 } /// @@ -657,7 +651,6 @@ namespace Avalonia.Controls switch (mode) { case AutoCompleteMode.Replace: - case AutoCompleteMode.Append: case AutoCompleteMode.Custom: return true; default: @@ -2774,8 +2767,6 @@ namespace Avalonia.Controls { case AutoCompleteMode.Replace: return Replace; - case AutoCompleteMode.Append: - return Append; case AutoCompleteMode.Custom: default: return null; @@ -2795,19 +2786,6 @@ namespace Avalonia.Controls { return value ?? String.Empty; } - - /// - /// Implements AutoCompleteMode.Append. - /// - /// The AutoCompleteBox prefix text. - /// The item's string value. - /// - /// Returns the concatenated string. - /// - private static string Append(string text, string value) - { - return text + value; - } } /// From 0fa3350a9d704d8d94b6635b0db80d59c9518d87 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Wed, 16 Sep 2020 13:25:38 +0300 Subject: [PATCH 03/13] Made tests for the AutoCompleteMode and TextSelector properties --- .../AutoCompleteBoxTests.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 57cea91834..2205384542 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -363,6 +363,47 @@ namespace Avalonia.Controls.UnitTests }); } + [Fact] + public void Test_Selectors() + { + Assert.Equal(GetSelector(AutoCompleteMode.Replace)("Never", "gonna"), "gonna"); + Assert.Equal(GetSelector(AutoCompleteMode.Replace)("give", "you"), "you"); + Assert.NotEqual(GetSelector(AutoCompleteMode.Replace)("up", "!"), "42"); + } + + [Fact] + public void AutoCompleteMode_Changes_To_Custom_And_Back() + { + RunTest((control, textbox) => + { + Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); + + control.TextSelector = (text, item) => text + item; + Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Custom); + + control.AutoCompleteMode = AutoCompleteMode.Replace; + Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); + Assert.Equal(control.TextSelector, GetSelector(AutoCompleteMode.Replace)); + }); + } + + [Fact] + public void Custom_TextSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.TextSelector = (text, item) => text + item; + Assert.Equal(control.TextSelector("4", "2"), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); + }); + } + /// /// Retrieves a defined predicate filter through a new AutoCompleteBox /// control instance. @@ -375,6 +416,17 @@ namespace Avalonia.Controls.UnitTests .TextFilter; } + /// + /// Retrieves a defined selector through a new AutoCompleteBox + /// control instance. + /// + /// The AutoCompleteMode of interest. + /// Returns the selector instance. + private static AutoCompleteSelector GetSelector(AutoCompleteMode mode) + { + return new AutoCompleteBox { AutoCompleteMode = mode }.TextSelector; + } + /// /// Creates a large list of strings for AutoCompleteBox testing. /// From 58c333b6f16eb13581bed38f9f337204727ada9b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 16:43:01 +0200 Subject: [PATCH 04/13] Added PseudoClassesAttribute. And apply it to our controls. Can be used by designers to add auto-completion support for pseudoclasses. --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 ++ src/Avalonia.Controls.DataGrid/DataGridCell.cs | 2 ++ .../DataGridColumnHeader.cs | 2 ++ src/Avalonia.Controls.DataGrid/DataGridRow.cs | 2 ++ .../DataGridRowGroupHeader.cs | 2 ++ .../DataGridRowHeader.cs | 2 ++ src/Avalonia.Controls/AutoCompleteBox.cs | 2 ++ src/Avalonia.Controls/Button.cs | 2 ++ src/Avalonia.Controls/ButtonSpinner.cs | 2 ++ .../Calendar/CalendarButton.cs | 2 ++ .../Calendar/CalendarDayButton.cs | 2 ++ src/Avalonia.Controls/Calendar/CalendarItem.cs | 2 ++ src/Avalonia.Controls/Chrome/CaptionButtons.cs | 2 ++ src/Avalonia.Controls/Chrome/TitleBar.cs | 2 ++ src/Avalonia.Controls/DataValidationErrors.cs | 2 ++ .../DateTimePickers/DatePicker.cs | 4 +++- .../DateTimePickers/TimePicker.cs | 4 +++- src/Avalonia.Controls/Expander.cs | 3 ++- src/Avalonia.Controls/ItemsControl.cs | 2 ++ src/Avalonia.Controls/ListBoxItem.cs | 2 ++ src/Avalonia.Controls/MenuItem.cs | 2 ++ .../Notifications/NotificationCard.cs | 2 ++ .../Notifications/WindowNotificationManager.cs | 2 ++ src/Avalonia.Controls/Primitives/ScrollBar.cs | 2 ++ src/Avalonia.Controls/Primitives/Thumb.cs | 2 ++ .../Primitives/ToggleButton.cs | 2 ++ src/Avalonia.Controls/Primitives/Track.cs | 2 ++ src/Avalonia.Controls/ProgressBar.cs | 2 ++ src/Avalonia.Controls/Slider.cs | 2 ++ src/Avalonia.Controls/SplitView.cs | 7 ++++++- src/Avalonia.Controls/TabItem.cs | 2 ++ src/Avalonia.Controls/TextBox.cs | 2 ++ src/Avalonia.Controls/ToggleSwitch.cs | 4 +++- src/Avalonia.Controls/ToolTip.cs | 2 ++ src/Avalonia.Controls/TreeViewItem.cs | 2 ++ src/Avalonia.Input/InputElement.cs | 2 ++ .../Metadata/PseudoClassesAttribute.cs | 18 ++++++++++++++++++ 37 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 9ca0b91523..7c57ea3db9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -24,12 +24,14 @@ using Avalonia.Input.Platform; using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; using Avalonia.Layout; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Displays data in a customizable grid. /// + [PseudoClasses(":invalid")] public partial class DataGrid : TemplatedControl { private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e5fbfa1a81..445dc541a7 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Input; @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// /// Represents an individual cell. /// + [PseudoClasses(":selected", ":current", ":edited", ":invalid")] public class DataGridCell : ContentControl { private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 25aae99942..856d1f6566 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -14,12 +14,14 @@ using Avalonia.Utilities; using System; using Avalonia.Controls.Utils; using Avalonia.Controls.Mixins; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Represents an individual column header. /// + [PseudoClasses(":dragIndicator", ":pressed", ":sortascending", ":sortdescending")] public class DataGridColumnHeader : ContentControl { private enum DragMode diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index d5ce8dba75..c3562c53a4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// Represents a row. /// + [PseudoClasses(":selected", ":editing", ":invalid")] public class DataGridRow : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 0833247439..1e03b134b1 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -13,6 +14,7 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + [PseudoClasses(":pressed", ":current", ":expanded")] public class DataGridRowGroupHeader : TemplatedControl { private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 8f8b1742ba..0cd3589a57 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Media; using System.Diagnostics; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an individual row header. /// + [PseudoClasses(":invalid", ":selected", ":editing", ":current")] public class DataGridRowHeader : ContentControl { private const string DATAGRIDROWHEADER_elementRootName = "PART_Root"; diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c164f282e8..6bbb4f0b75 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -14,6 +14,7 @@ using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; @@ -30,6 +31,7 @@ namespace Avalonia.Controls /// /// event. /// + [PseudoClasses(":dropdownopen")] public class PopulatedEventArgs : EventArgs { /// diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index b54eb2ac57..e94d00b2ff 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -28,6 +29,7 @@ namespace Avalonia.Controls /// /// A button control. /// + [PseudoClasses(":pressed")] public class Button : ContentControl { /// diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 44f66d397a..5fe2cf3704 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; @@ -15,6 +16,7 @@ namespace Avalonia.Controls /// /// Represents a spinner control that includes two Buttons. /// + [PseudoClasses(":left", ":right")] public class ButtonSpinner : Spinner { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 80370df145..76af933b55 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using System; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// Represents a button on a /// . /// + [PseudoClasses(":selected", ":inactive", ":btnfocused")] public sealed class CalendarButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 3a39bd10fa..d5748bb9e4 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -5,10 +5,12 @@ using System; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Input; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed", ":disabled", ":selected", ":inactive", ":today", ":blackout", ":dayfocused")] public sealed class CalendarDayButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 0be7c4f67e..e9ea942142 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -18,6 +19,7 @@ namespace Avalonia.Controls.Primitives /// Represents the currently displayed month or year on a /// . /// + [PseudoClasses(":calendardisabled")] public sealed class CalendarItem : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index a86cbc271b..cd60130c5b 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class CaptionButtons : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index c0c8076dd8..fbddb06952 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws a titlebar when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class TitleBar : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index dfe9a16532..3c64691816 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// /// You will probably only want to create instances inside of control templates. /// + [PseudoClasses(":error")] public class DataValidationErrors : ContentControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index a41c159980..8d893154eb 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Interactivity; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a date /// + [PseudoClasses(":hasnodate")] public class DatePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index e54da1fb3a..e4ff5e9e5b 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using System; @@ -9,6 +10,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a time /// + [PseudoClasses(":hasnotime")] public class TimePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 43882b70c8..9ff2e41fa9 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,6 +1,6 @@ using Avalonia.Animation; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Data; namespace Avalonia.Controls { @@ -12,6 +12,7 @@ namespace Avalonia.Controls Right } + [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")] public class Expander : HeaderedContentControl { public static readonly StyledProperty ContentTransitionProperty = diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a3dfe33641..3aec06e4eb 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -18,6 +19,7 @@ namespace Avalonia.Controls /// /// Displays a collection of items. /// + [PseudoClasses(":empty", ":singleitem")] public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener { /// diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index e04c79987f..4fe5f4de40 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Input; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// A selectable item in a . /// + [PseudoClasses(":pressed", ":selected")] public class ListBoxItem : ContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index b4d3272471..3d8ab3ae48 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reactive.Linq; using System.Windows.Input; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// A menu item control. /// + [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { /// diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index f90746bf06..cdbace3ced 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Notifications /// /// Control that represents and displays a notification. /// + [PseudoClasses(":error", ":information", ":success", ":warning")] public class NotificationCard : ContentControl { private bool _isClosed; diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 6d9f6b8b77..8f5c6faf40 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -7,12 +7,14 @@ using Avalonia.Controls.Primitives; using Avalonia.Rendering; using Avalonia.Data; using Avalonia.VisualTree; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Notifications { /// /// An that displays notifications in a . /// + [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")] public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest { private IList _items; diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 477d24dc99..a7fb7ae08c 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -4,6 +4,7 @@ using Avalonia.Interactivity; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Primitives { @@ -21,6 +22,7 @@ namespace Avalonia.Controls.Primitives /// /// A scrollbar control. /// + [PseudoClasses(":vertical", ":horizontal")] public class ScrollBar : RangeBase { /// diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 96810ed01b..348922b71d 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -1,9 +1,11 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed")] public class Thumb : TemplatedControl { public static readonly RoutedEvent DragStartedEvent = diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 13031ddad8..f96ca9310d 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Interactivity; @@ -7,6 +8,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. /// + [PseudoClasses(":checked", ":unchecked", ":indeterminate")] public class ToggleButton : Button { /// diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 29e7f28b44..9399f5fb31 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; @@ -12,6 +13,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":vertical", ":horizontal")] public class Track : Control { public static readonly DirectProperty MinimumProperty = diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a92f24a050..161f09d9b6 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Media; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A control used to indicate the progress of an operation. /// + [PseudoClasses(":vertical", ":horizontal", ":indeterminate")] public class ProgressBar : RangeBase { public class ProgressBarTemplateProperties : AvaloniaObject diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 293cbac82f..6e08e78813 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -39,6 +40,7 @@ namespace Avalonia.Controls /// /// A control that lets the user select from a range of values by moving a Thumb control along a Track. /// + [PseudoClasses(":vertical", ":horizontal", ":pressed")] public class Slider : RangeBase { /// diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index b71858f796..8267efc466 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -73,6 +74,10 @@ namespace Avalonia.Controls /// /// A control with two views: A collapsible pane and an area for content /// + [PseudoClasses(":open", ":closed")] + [PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")] + [PseudoClasses(":left", ":right")] + [PseudoClasses(":lightdismiss")] public class SplitView : TemplatedControl { /* diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 6320443a13..593643a1eb 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// An item in a or . /// + [PseudoClasses(":pressed", ":selected")] public class TabItem : HeaderedContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 73a1ae3335..0fe3ac62e4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -13,9 +13,11 @@ using Avalonia.Metadata; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Utilities; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { + [PseudoClasses(":empty")] public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index c32f2d8102..662a355dac 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Presenters; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A Toggle Switch control. /// + [PseudoClasses(":dragging")] public class ToggleSwitch : ToggleButton { private Panel _knobsPanel; diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index d56ff5752f..71bd0726d4 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.VisualTree; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// To add a tooltip to a control, use the attached property, /// assigning the content that you want displayed. /// + [PseudoClasses(":open")] public class ToolTip : ContentControl { /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 4942d4d313..8ce258b546 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -1,5 +1,6 @@ using System.Linq; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// An item in a . /// + [PseudoClasses(":pressed", ":selected")] public class TreeViewItem : HeaderedItemsControl, ISelectable { /// diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 25f2d553d7..9ace7fd92d 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; @@ -12,6 +13,7 @@ namespace Avalonia.Input /// /// Implements input-related functionality for a control. /// + [PseudoClasses(":disabled", ":focus", ":focus-visible", ":pointerover")] public class InputElement : Interactive, IInputElement { /// diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs new file mode 100644 index 0000000000..66d0282b92 --- /dev/null +++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Avalonia.Controls.Metadata +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class PseudoClassesAttribute : Attribute + { + public PseudoClassesAttribute(params string[] pseudoClasses) + { + PseudoClasses = pseudoClasses; + } + + public IReadOnlyList PseudoClasses { get; } + } +} From 0042e82db2766931acde34de6e994abed235698f Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Sat, 19 Sep 2020 13:42:25 -0400 Subject: [PATCH 05/13] Add resizing ui test for RenderDemo --- samples/RenderDemo/MainWindow.xaml | 8 +++- .../ViewModels/MainWindowViewModel.cs | 48 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 770960d7c4..93fbe5e412 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -3,8 +3,8 @@ x:Class="RenderDemo.MainWindow" Title="AvaloniaUI Rendering Test" xmlns:pages="clr-namespace:RenderDemo.Pages" - Width="800" - Height="600"> + Width="{Binding Width, Mode=TwoWay}" + Height="{Binding Height, Mode=TwoWay}"> @@ -24,6 +24,10 @@ + + + diff --git a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs index d2d789a687..eda5e80530 100644 --- a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,6 @@ -using System; -using System.Reactive; +using System.Reactive; +using System.Threading.Tasks; + using ReactiveUI; namespace RenderDemo.ViewModels @@ -8,26 +9,61 @@ namespace RenderDemo.ViewModels { private bool drawDirtyRects = false; private bool drawFps = true; + private double width = 800; + private double height = 600; public MainWindowViewModel() { ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects); ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps); + ResizeWindow = ReactiveCommand.CreateFromTask(ResizeWindowAsync); } public bool DrawDirtyRects { - get { return drawDirtyRects; } - set { this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } + get => drawDirtyRects; + set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } public bool DrawFps { - get { return drawFps; } - set { this.RaiseAndSetIfChanged(ref drawFps, value); } + get => drawFps; + set => this.RaiseAndSetIfChanged(ref drawFps, value); + } + + public double Width + { + get => width; + set => this.RaiseAndSetIfChanged(ref width, value); + } + + public double Height + { + get => height; + set => this.RaiseAndSetIfChanged(ref height, value); } public ReactiveCommand ToggleDrawDirtyRects { get; } public ReactiveCommand ToggleDrawFps { get; } + public ReactiveCommand ResizeWindow { get; } + + private async Task ResizeWindowAsync() + { + for (int i = 0; i < 30; i++) + { + Width += 10; + Height += 5; + await Task.Delay(10); + } + + await Task.Delay(10); + + for (int i = 0; i < 30; i++) + { + Width -= 10; + Height -= 5; + await Task.Delay(10); + } + } } } From 7481aea60620b05a915fdb03b20ee56dc731908c Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Mon, 21 Sep 2020 00:57:08 +0300 Subject: [PATCH 06/13] Removed AutoCompleteMode enum --- src/Avalonia.Controls/AutoCompleteBox.cs | 148 +----------------- .../AutoCompleteBoxTests.cs | 35 ----- 2 files changed, 4 insertions(+), 179 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 22b09ef110..c9b50a46fd 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -246,25 +246,6 @@ namespace Avalonia.Controls /// public delegate string AutoCompleteSelector(string search, T item); - /// - /// Specifies how the selected autocomplete result should be treated. - /// - public enum AutoCompleteMode - { - /// - /// Specifies that the text will be replaced - /// with the selected autocomplete result. - /// - Replace = 0, - - /// - /// Specifies that a custom selector is used. This mode is used when - /// the - /// property is set. - /// - Custom = 1 - } - /// /// Represents a control that provides a text box for user input and a /// drop-down that contains possible matches based on the input in the text @@ -402,7 +383,7 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); - private AutoCompleteSelector _textSelector = AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace); + private AutoCompleteSelector _textSelector; public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); @@ -541,17 +522,6 @@ namespace Avalonia.Controls defaultValue: AutoCompleteFilterMode.StartsWith, validate: IsValidFilterMode); - /// - /// Gets the identifier for the - /// - /// dependency property. - /// - public static readonly StyledProperty AutoCompleteModeProperty = - AvaloniaProperty.Register( - nameof(AutoCompleteMode), - defaultValue: AutoCompleteMode.Replace, - validate: IsValidAutoCompleteMode); - /// /// Identifies the /// @@ -593,8 +563,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect>( nameof(TextSelector), o => o.TextSelector, - (o, v) => o.TextSelector = v, - unsetValue: AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace)); + (o, v) => o.TextSelector = v); /// /// Identifies the @@ -646,18 +615,6 @@ namespace Avalonia.Controls } } - private static bool IsValidAutoCompleteMode(AutoCompleteMode mode) - { - switch (mode) - { - case AutoCompleteMode.Replace: - case AutoCompleteMode.Custom: - return true; - default: - return false; - } - } - /// /// Handle the change of the IsEnabled property. /// @@ -808,19 +765,6 @@ namespace Avalonia.Controls TextFilter = AutoCompleteSearch.GetFilter(mode); } - /// - /// AutoCompleteModeProperty property changed handler. - /// - /// Event arguments. - private void OnAutoCompleteModePropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - AutoCompleteMode mode = (AutoCompleteMode)e.NewValue; - - // Sets the text selector for the new value - if (mode != AutoCompleteMode.Custom) - TextSelector = AutoCompleteSelection.GetSelector(mode); - } - /// /// ItemFilterProperty property changed handler. /// @@ -841,25 +785,6 @@ namespace Avalonia.Controls } } - /// - /// TextSelectorProperty property changed handler. - /// - /// Event arguments. - private void OnTextSelectorPropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - AutoCompleteSelector value = e.NewValue as AutoCompleteSelector; - - // If null, revert to the "Replace" predicate - if (value == null) - { - AutoCompleteMode = AutoCompleteMode.Replace; - } - else if (value.Method.DeclaringType != typeof(AutoCompleteSelection)) - { - AutoCompleteMode = AutoCompleteMode.Custom; - } - } - /// /// ItemsSourceProperty property changed handler. /// @@ -905,8 +830,6 @@ namespace Avalonia.Controls SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); - AutoCompleteModeProperty.Changed.AddClassHandler((x,e) => x.OnAutoCompleteModePropertyChanged(e)); - TextSelectorProperty.Changed.AddClassHandler((x,e) => x.OnTextSelectorPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } @@ -1129,31 +1052,6 @@ namespace Avalonia.Controls set { SetValue(FilterModeProperty, value); } } - /// - /// Gets or sets how the text in the text box will be modified - /// with the selected autocomplete item. - /// - /// - /// One of the - /// values. The default is - /// . - /// - /// The specified value is not a valid - /// . - /// - /// - /// Use the AutoCompleteMode property to specify the way the text will - /// be modified with the selected autocomplete item. For example, text - /// can be modified in a predefined or custom way. The autocomplete - /// mode is automatically set to Custom if you set the TextSelector - /// property. - /// - public AutoCompleteMode AutoCompleteMode - { - get { return GetValue(AutoCompleteModeProperty); } - set { SetValue(AutoCompleteModeProperty, value); } - } - public string Watermark { get { return GetValue(WatermarkProperty); } @@ -2490,7 +2388,8 @@ namespace Avalonia.Controls } else { - text = TextSelector(SearchText, FormatValue(newItem, true)); + string formattedValue = FormatValue(newItem, true); + text = TextSelector == null ? formattedValue : TextSelector(SearchText, formattedValue); } // Update the Text property and the TextBox values @@ -2749,45 +2648,6 @@ namespace Avalonia.Controls } } - /// - /// A predefined set of selector functions for the known, built-in - /// AutoCompleteMode enumeration values. - /// - private static class AutoCompleteSelection - { - /// - /// Index function that retrieves the selector for the provided - /// AutoCompleteMode. - /// - /// The built-in autocomplete mode. - /// Returns the string-based selector function. - public static AutoCompleteSelector GetSelector(AutoCompleteMode completeMode) - { - switch (completeMode) - { - case AutoCompleteMode.Replace: - return Replace; - case AutoCompleteMode.Custom: - default: - return null; - } - } - - /// - /// Implements AutoCompleteMode.Replace. - /// - /// The AutoCompleteBox prefix text. - /// The item's string value. - /// - /// Return the and ignores the - /// . - /// - private static string Replace(string text, string value) - { - return value ?? String.Empty; - } - } - /// /// A framework element that permits a binding to be evaluated in a new data /// context leaf node. diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 2205384542..2e609132d6 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -363,30 +363,6 @@ namespace Avalonia.Controls.UnitTests }); } - [Fact] - public void Test_Selectors() - { - Assert.Equal(GetSelector(AutoCompleteMode.Replace)("Never", "gonna"), "gonna"); - Assert.Equal(GetSelector(AutoCompleteMode.Replace)("give", "you"), "you"); - Assert.NotEqual(GetSelector(AutoCompleteMode.Replace)("up", "!"), "42"); - } - - [Fact] - public void AutoCompleteMode_Changes_To_Custom_And_Back() - { - RunTest((control, textbox) => - { - Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); - - control.TextSelector = (text, item) => text + item; - Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Custom); - - control.AutoCompleteMode = AutoCompleteMode.Replace; - Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); - Assert.Equal(control.TextSelector, GetSelector(AutoCompleteMode.Replace)); - }); - } - [Fact] public void Custom_TextSelector() { @@ -416,17 +392,6 @@ namespace Avalonia.Controls.UnitTests .TextFilter; } - /// - /// Retrieves a defined selector through a new AutoCompleteBox - /// control instance. - /// - /// The AutoCompleteMode of interest. - /// Returns the selector instance. - private static AutoCompleteSelector GetSelector(AutoCompleteMode mode) - { - return new AutoCompleteBox { AutoCompleteMode = mode }.TextSelector; - } - /// /// Creates a large list of strings for AutoCompleteBox testing. /// From bc87efc65fbae7ab338716f1e89a43b48fea6428 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Mon, 21 Sep 2020 01:56:05 +0300 Subject: [PATCH 07/13] Added ItemSelector property to the AutoCompleteBox --- src/Avalonia.Controls/AutoCompleteBox.cs | 54 ++++++++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c9b50a46fd..c119fd1964 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -242,7 +242,7 @@ namespace Avalonia.Controls /// /// The type used for filtering the /// . - /// At the moment this type known only as a string. + /// This type can be either a string or an object. /// public delegate string AutoCompleteSelector(string search, T item); @@ -383,6 +383,7 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + private AutoCompleteSelector _itemSelector; private AutoCompleteSelector _textSelector; public static readonly RoutedEvent SelectionChangedEvent = @@ -551,6 +552,20 @@ namespace Avalonia.Controls (o, v) => o.TextFilter = v, unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> ItemSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(ItemSelector), + o => o.ItemSelector, + (o, v) => o.ItemSelector = v); + /// /// Identifies the /// @@ -1100,18 +1115,32 @@ namespace Avalonia.Controls /// /// Gets or sets the custom method that combines the user-entered - /// text to and one of the items specified by the + /// text and one of the items specified by the /// . /// /// /// The custom method that combines the user-entered - /// text to and one of the items specified by the + /// text and one of the items specified by the /// . /// - /// - /// The AutoCompleteMode is automatically set to Custom if you set - /// the TextSelector property. - /// + public AutoCompleteSelector ItemSelector + { + get { return _itemSelector; } + set { SetAndRaise(ItemSelectorProperty, ref _itemSelector, value); } + } + + /// + /// Gets or sets the custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// + /// + /// The custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// public AutoCompleteSelector TextSelector { get { return _textSelector; } @@ -2386,10 +2415,17 @@ namespace Avalonia.Controls { text = SearchText; } + else if (TextSelector != null) + { + text = TextSelector(SearchText, FormatValue(newItem, true)); + } + else if (ItemSelector != null) + { + text = ItemSelector(SearchText, newItem); + } else { - string formattedValue = FormatValue(newItem, true); - text = TextSelector == null ? formattedValue : TextSelector(SearchText, formattedValue); + text = FormatValue(newItem, true); } // Update the Text property and the TextBox values From 997385eab7307201943dc94b88d417b5e3207fbd Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Mon, 21 Sep 2020 01:56:34 +0300 Subject: [PATCH 08/13] Made test for AutoCompleteBox.ItemSelector --- .../AutoCompleteBoxTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 2e609132d6..3e78e951e2 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -379,6 +379,23 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); }); } + + [Fact] + public void Custom_ItemSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.ItemSelector = (text, item) => text + item; + Assert.Equal(control.ItemSelector("4", 2), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.ItemSelector(input, selectedItem)); + }); + } /// /// Retrieves a defined predicate filter through a new AutoCompleteBox From 14aff78be5e40a92f2c58431e2472d882f0488c5 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Mon, 21 Sep 2020 03:16:14 -0700 Subject: [PATCH 09/13] use sealed for attribute class. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dariusz Komosiński --- .../Controls/Metadata/PseudoClassesAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs index 66d0282b92..0060767565 100644 --- a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs +++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace Avalonia.Controls.Metadata { [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class PseudoClassesAttribute : Attribute + public sealed class PseudoClassesAttribute : Attribute { public PseudoClassesAttribute(params string[] pseudoClasses) { From 3f3ec4b8351a1f034f71584751ca71ba03fe089b Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Tue, 22 Sep 2020 03:40:10 +0300 Subject: [PATCH 10/13] Added custom TextSelector example --- .../Pages/AutoCompleteBoxPage.xaml | 5 ++ .../Pages/AutoCompleteBoxPage.xaml.cs | 58 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index f90a0c4658..a49616e543 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -51,6 +51,11 @@ Width="200" Margin="0,0,0,8" FilterMode="None"/> + + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs index f9d6a72a3a..574cc79a7d 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -92,13 +92,28 @@ namespace ControlCatalog.Pages } public StateData[] States { get; private set; } + private LinkedList[] BuildAllSentences() + { + return new string[] + { + "Hello world", + "No this is Patrick", + "Never gonna give you up", + "How does one patch KDE2 under FreeBSD" + } + .Select(x => new LinkedList(x.Split(' '))) + .ToArray(); + } + public LinkedList[] Sentences { get; private set; } + public AutoCompleteBoxPage() { this.InitializeComponent(); States = BuildAllStates(); + Sentences = BuildAllSentences(); - foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) + foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox")) { box.Items = States; } @@ -116,6 +131,11 @@ namespace ControlCatalog.Pages var asyncBox = this.FindControl("AsyncBox"); asyncBox.AsyncPopulator = PopulateAsync; + + var customAutocompleteBox = this.FindControl("CustomAutocompleteBox"); + customAutocompleteBox.Items = Sentences.SelectMany(x => x); + customAutocompleteBox.TextFilter = LastWordContains; + customAutocompleteBox.TextSelector = AppendWord; } private IEnumerable GetAllAutoCompleteBox() { @@ -137,6 +157,42 @@ namespace ControlCatalog.Pages .ToList(); } + private bool LastWordContains(string searchText, string item) + { + var words = searchText.Split(' '); + var options = Sentences.Select(x => x.First).ToArray(); + for (var i = 0; i < words.Length; ++i) + { + var word = words[i]; + for (var j = 0; j < options.Length; ++j) + { + var option = options[j]; + if (option == null) + continue; + + if (i == words.Length - 1) + { + options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null; + } + else + { + options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null; + } + } + } + + return options.Any(x => x != null && x.Value == item); + } + private string AppendWord(string text, string item) + { + string[] parts = text.Split(' '); + if (parts.Length == 0) + return item; + + parts[parts.Length - 1] = item; + return string.Join(" ", parts); + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); From ab354827244116f18b331d8cf022c151ccf44493 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 22 Sep 2020 00:27:51 -0400 Subject: [PATCH 11/13] Make Fluent buttons animation consistent --- src/Avalonia.Themes.Fluent/Button.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index e58e8758d2..021c6eae1d 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -77,7 +77,7 @@ - - From 95f07a9f9687adc93c0783defd1aebba7fbf47d8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 23 Sep 2020 01:04:03 -0400 Subject: [PATCH 12/13] Only specific buttons should have pressing animation --- src/Avalonia.Themes.Fluent/Button.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index 021c6eae1d..8522c933ae 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -77,7 +77,7 @@ - - From 1430bf86e32b820aac26a159c3ee98a33b58f5dc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Sep 2020 12:13:38 +0200 Subject: [PATCH 13/13] Use screen bounds Bottom instead of Height. Secondard screens on Windows can have a Y offset (i.e. `bounds.Y != 0`) if they're offset vertically from the primary screen, and when this was the case the popup wasn't getting properly constrained as the bottom is not equal to the height. Fixes #4726 --- .../Primitives/PopupPositioning/ManagedPopupPositioner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index 8c464c7aad..7f1dbdf592 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -221,7 +221,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom)) { - unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y); + unconstrainedRect = unconstrainedRect.WithHeight(bounds.Bottom - unconstrainedRect.Y); } if (IsValid(unconstrainedRect))