From de617de3d72f310126ab767571642f3a6f8fab04 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 23 May 2023 12:39:53 +0200 Subject: [PATCH 1/6] Don't create DBus Tray Icon when not connected --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 43ae48341c..9abf4f97a7 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; -using Avalonia.Threading; using Tmds.DBus.Protocol; using Tmds.DBus.SourceGenerator; @@ -57,15 +56,6 @@ namespace Avalonia.FreeDesktop WatchAsync(); } - private void InitializeSNWService() - { - if (_connection is null || _isDisposed) - return; - - _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); - _serviceConnected = true; - } - private async void WatchAsync() { try @@ -84,13 +74,13 @@ namespace Avalonia.FreeDesktop private void OnNameChange(string? newOwner) { - if (_isDisposed) + if (_isDisposed || _connection is null) return; if (!_serviceConnected & newOwner is not null) { _serviceConnected = true; - InitializeSNWService(); + _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); DestroyTrayIcon(); @@ -182,8 +172,11 @@ namespace Avalonia.FreeDesktop public void SetIsVisible(bool visible) { - if (_isDisposed) + if (_isDisposed || !_serviceConnected) + { + _isVisible = visible; return; + } switch (visible) { From e69661b353d8435ac2d3df8e2707e8b8b188f453 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 23 May 2023 14:08:59 +0200 Subject: [PATCH 2/6] Rework MaxLines to limit how many lines are visible at once and don't prevent additional input if MaxLines is reached --- .../Media/TextFormatting/TextLayout.cs | 18 +--- src/Avalonia.Controls/TextBlock.cs | 3 +- src/Avalonia.Controls/TextBox.cs | 91 +++++++++++-------- .../Controls/TextBox.xaml | 3 +- .../Controls/TextBox.xaml | 3 +- .../ScrollViewerTests.cs | 4 +- .../TextBoxTests.cs | 19 +++- 7 files changed, 79 insertions(+), 62 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 0697831987..4ccb3f6a37 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -60,14 +60,10 @@ namespace Avalonia.Media.TextFormatting _textTrimming = textTrimming ?? TextTrimming.None; - LineHeight = lineHeight; - MaxWidth = maxWidth; MaxHeight = maxHeight; - LetterSpacing = letterSpacing; - MaxLines = maxLines; _textLines = CreateTextLines(); @@ -81,8 +77,6 @@ namespace Avalonia.Media.TextFormatting /// The text trimming. /// The maximum width. /// The maximum height. - /// The height of each line of text. - /// The letter spacing that is applied to rendered glyphs. /// The maximum number of text lines. public TextLayout( ITextSource textSource, @@ -90,8 +84,6 @@ namespace Avalonia.Media.TextFormatting TextTrimming? textTrimming = null, double maxWidth = double.PositiveInfinity, double maxHeight = double.PositiveInfinity, - double lineHeight = double.NaN, - double letterSpacing = 0, int maxLines = 0) { _textSource = textSource; @@ -100,14 +92,10 @@ namespace Avalonia.Media.TextFormatting _textTrimming = textTrimming ?? TextTrimming.None; - LineHeight = lineHeight; - MaxWidth = maxWidth; MaxHeight = maxHeight; - LetterSpacing = letterSpacing; - MaxLines = maxLines; _textLines = CreateTextLines(); @@ -120,7 +108,7 @@ namespace Avalonia.Media.TextFormatting /// A value of NaN (equivalent to an attribute value of "Auto") indicates that the line height /// is determined automatically from the current font characteristics. The default is NaN. /// - public double LineHeight { get; } + public double LineHeight => _paragraphProperties.LineHeight; /// /// Gets the maximum width. @@ -140,7 +128,7 @@ namespace Avalonia.Media.TextFormatting /// /// Gets the text spacing. /// - public double LetterSpacing { get; } + public double LetterSpacing => _paragraphProperties.LetterSpacing; /// /// Gets the text lines. @@ -495,7 +483,7 @@ namespace Avalonia.Media.TextFormatting /// The height of each line of text. /// The letter spacing that is applied to rendered glyphs. /// - private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, + internal static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, double letterSpacing) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 6144136882..57d709ba94 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -639,8 +639,7 @@ namespace Avalonia.Controls TextTrimming, _constraint.Width, _constraint.Height, - maxLines: MaxLines, - lineHeight: LineHeight); + MaxLines); } /// diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 51a7fe9b7f..7bc26bf3b0 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -18,9 +18,6 @@ using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Automation.Peers; using Avalonia.Threading; -using Avalonia.Platform; -using System.Reflection; -using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls { @@ -28,6 +25,7 @@ namespace Avalonia.Controls /// Represents a control that can be used to display or edit unformatted text. /// [TemplatePart("PART_TextPresenter", typeof(TextPresenter))] + [TemplatePart("PART_ScrollViewer", typeof(ScrollViewer))] [PseudoClasses(":empty")] public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { @@ -158,7 +156,7 @@ namespace Avalonia.Controls /// Defines see property. /// public static readonly StyledProperty LineHeightProperty = - TextBlock.LineHeightProperty.AddOwner(); + TextBlock.LineHeightProperty.AddOwner(new(defaultValue: double.NaN)); /// /// Defines see property. @@ -310,6 +308,7 @@ namespace Avalonia.Controls } private TextPresenter? _presenter; + private ScrollViewer? _scrollViewer; private readonly TextBoxTextInputMethodClient _imClient = new(); private readonly UndoRedoHelper _undoRedoHelper; private bool _isUndoingRedoing; @@ -490,7 +489,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the maximum character length of the TextBox + /// Gets or sets the maximum number of visible lines. /// public int MaxLength { @@ -803,6 +802,8 @@ namespace Avalonia.Controls { _presenter = e.NameScope.Get("PART_TextPresenter"); + _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); + _imClient.SetPresenter(_presenter, this); if (IsFocused) @@ -855,6 +856,10 @@ namespace Avalonia.Controls { OnSelectionEndChanged(change); } + else if (change.Property == MaxLinesProperty) + { + InvalidateMeasure(); + } else if (change.Property == UndoLimitProperty) { OnUndoLimitChanged(change.GetNewValue()); @@ -942,40 +947,10 @@ namespace Avalonia.Controls { return; } + _selectedTextChangesMadeSinceLastUndoSnapshot++; SnapshotUndoRedo(ignoreChangeCount: false); - if (_presenter != null && MaxLines > 0) - { - var lineCount = _presenter.TextLayout.TextLines.Count; - - var length = 0; - - var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan()); - - while (graphemeEnumerator.MoveNext(out var grapheme)) - { - if (grapheme.FirstCodepoint.IsBreakChar) - { - if (lineCount + 1 > MaxLines) - { - break; - } - else - { - lineCount++; - } - } - - length += grapheme.Length; - } - - if (length < input.Length) - { - input = input.Remove(Math.Max(0, length)); - } - } - var currentText = Text ?? string.Empty; var selectionLength = Math.Abs(SelectionStart - SelectionEnd); var newLength = input.Length + currentText.Length - selectionLength; @@ -1518,7 +1493,7 @@ namespace Avalonia.Controls _presenter.MoveCaretToPoint(point); var caretIndex = _presenter.CaretIndex; - + var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -1976,5 +1951,47 @@ namespace Avalonia.Controls { CanRedo = _undoRedoHelper.CanRedo; } + + protected override Size MeasureOverride(Size availableSize) + { + if(_scrollViewer != null) + { + var maxHeight = double.PositiveInfinity; + + if (MaxLines > 0 && double.IsNaN(Height)) + { + var fontSize = FontSize; + var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); + var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default); + var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties); + + maxHeight = Math.Ceiling(textLayout.Height); + } + + _scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight); + } + + return base.MeasureOverride(availableSize); + } + + private class MaxLinesTextSource : ITextSource + { + private readonly int _maxLines; + + public MaxLinesTextSource(int maxLines) + { + _maxLines = maxLines; + } + + public TextRun? GetTextRun(int textSourceIndex) + { + if (textSourceIndex >= _maxLines) + { + return null; + } + + return new TextEndOfLine(1); + } + } } } diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml index 9449fcfb72..40abac8e86 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml @@ -133,7 +133,8 @@ IsVisible="False" Text="{TemplateBinding Watermark}" DockPanel.Dock="Top" /> - - ((control, scope) => - new TextPresenter + new ScrollViewer + { + Name = "Part_ScrollViewer", + Template = new FuncControlTemplate(ScrollViewerTests.CreateTemplate), + Content = new TextPresenter { Name = "PART_TextPresenter", [!!TextPresenter.TextProperty] = new Binding @@ -1133,7 +1143,8 @@ namespace Avalonia.Controls.UnitTests Priority = BindingPriority.Template, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), } - }.RegisterInNameScope(scope)); + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope)); } private static void RaiseKeyEvent(TextBox textBox, Key key, KeyModifiers inputModifiers) From 2f48e5b79b19cc1dc220862548870a435be45401 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 23 May 2023 12:47:54 +0000 Subject: [PATCH 3/6] Touch up ManagedFileChooser design a bit --- .../Controls/ManagedFileChooser.xaml | 32 +++++----- .../Controls/ManagedFileChooser.xaml | 59 +++++++++++++------ 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index b3453569a4..e903e7d3a8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -179,8 +179,8 @@ - - + + @@ -189,7 +189,6 @@ - @@ -197,6 +196,7 @@ + - - - - - - - - + + + + + + + + - @@ -239,16 +238,17 @@ + - - - - + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml index a5b0649655..bd5151ad1a 100644 --- a/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml @@ -49,7 +49,7 @@ TargetType="dialogs:ManagedFileChooser"> - + - - + + @@ -136,7 +136,6 @@ DockPanel.Dock="Top"> - @@ -144,19 +143,45 @@ + - - - - + + - - + + + - - + + + + + - @@ -177,19 +201,20 @@ + - - - - + From 81084454f4aef7d5ea6b9bfe8ad0491b44071cf7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 23 May 2023 16:02:54 +0200 Subject: [PATCH 4/6] Added failing tests for #11484. --- .../AvaloniaObjectTests_Coercion.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index 0d0456dbda..42720cbb4c 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -124,6 +124,19 @@ namespace Avalonia.Base.UnitTests Assert.Equal(2, target.CoreChanges.Count); } + [Fact] + public void CoerceValue_Calls_Coerce_Callback_Only_Once() + { + var target = new Class1 { Foo = 99 }; + + target.MaxFoo = 50; + + target.CoerceFooInvocations.Clear(); + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(new[] { 99 }, target.CoerceFooInvocations); + } + [Fact] public void Coerced_Value_Can_Be_Restored_If_Limit_Changed() { @@ -218,6 +231,18 @@ namespace Avalonia.Base.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Default_Value_Is_Coerced_Only_Once() + { + var target = new Class1(); + + target.MinFoo = 20; + target.CoerceFooInvocations.Clear(); + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(new[] { 11 }, target.CoerceFooInvocations); + } + [Fact] public void ClearValue_Respects_Coerced_Default_Value() { @@ -338,10 +363,12 @@ namespace Avalonia.Base.UnitTests public int MinFoo { get; set; } = 0; public int MaxFoo { get; set; } = 100; + public List CoerceFooInvocations { get; } = new(); public List CoreChanges { get; } = new(); public static int CoerceFoo(AvaloniaObject instance, int value) { + (instance as Class1)?.CoerceFooInvocations.Add(value); return instance is Class1 o ? Math.Clamp(value, o.MinFoo, o.MaxFoo) : Math.Clamp(value, 0, 100); From 531f059c76f9d3a937fa3afcf6d21eea430c3399 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 23 May 2023 16:10:03 +0200 Subject: [PATCH 5/6] Only call coerce callback a single time. --- src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index b725326855..4518289335 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -208,7 +208,7 @@ namespace Avalonia.PropertyStore IsOverridenCurrentValue = isOverriddenCurrentValue; IsCoercedDefaultValue = isCoercedDefaultValue; - if (_uncommon?._coerce is { } coerce) + if (!isCoercedDefaultValue && _uncommon?._coerce is { } coerce) v = coerce(owner.Owner, value); if (priority <= Priority) @@ -262,7 +262,8 @@ namespace Avalonia.PropertyStore if (_uncommon?._coerce is { } coerce) { v = coerce(owner.Owner, value); - bv = coerce(owner.Owner, baseValue); + if (priority != basePriority) + bv = coerce(owner.Owner, baseValue); } if (!EqualityComparer.Default.Equals(Value, v)) From 7640434affc3c5e82be0e258528ddacea5e897b6 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 24 May 2023 08:39:56 +0000 Subject: [PATCH 6/6] use MinWidth to set button widths --- src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml | 4 ++-- src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index e903e7d3a8..8519099a27 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -179,8 +179,8 @@ - - + + diff --git a/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml index bd5151ad1a..cc049938ad 100644 --- a/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml @@ -93,8 +93,8 @@ - - + +