From 90e0dcc9e3161cb9659ca7381c6ff98ee7e84f10 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 23 Jun 2022 15:18:36 +0200 Subject: [PATCH] Add Copy geasture --- samples/Sandbox/MainWindow.axaml | 1 + .../Documents/InlineCollection.cs | 4 +- src/Avalonia.Controls/Documents/Span.cs | 12 +- .../Documents/TextElement.cs | 4 +- .../Primitives/AccessText.cs | 4 +- src/Avalonia.Controls/RichTextBlock.cs | 194 +++++++++++++----- src/Avalonia.Controls/TextBlock.cs | 38 ++-- .../Media/TextFormatting/BiDiClassTests.cs | 2 +- .../Styling/SetterTests.cs | 2 +- 9 files changed, 174 insertions(+), 87 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 806f6d37da..a834e3fef3 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -13,6 +13,7 @@ . + diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 2f27ca72d0..dc688fc359 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -136,7 +136,7 @@ namespace Avalonia.Controls.Documents base.Add(new Run(_text)); } - _text = string.Empty; + _text = null; } base.Add(item); @@ -160,8 +160,6 @@ namespace Avalonia.Controls.Documents Invalidated?.Invoke(this, EventArgs.Empty); } - private void Invalidate(object? sender, EventArgs e) => Invalidate(); - private void OnParentChanged(ILogical? parent) { foreach(var child in this) diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index 98851726da..c7289dbc3f 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -67,10 +67,12 @@ namespace Avalonia.Controls.Documents inline.AppendText(stringBuilder); } } - - if (Inlines.Text is string text) + else { - stringBuilder.Append(text); + if (Inlines.Text is string text) + { + stringBuilder.Append(text); + } } } @@ -87,9 +89,9 @@ namespace Avalonia.Controls.Documents } } - internal override void OnInlinesHostChanged(IInlineHost? oldValue, IInlineHost? newValue) + internal override void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue) { - base.OnInlinesHostChanged(oldValue, newValue); + base.OnInlineHostChanged(oldValue, newValue); if(Inlines is not null) { diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs index e75fd87615..5bac3642ed 100644 --- a/src/Avalonia.Controls/Documents/TextElement.cs +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -259,11 +259,11 @@ namespace Avalonia.Controls.Documents { var oldValue = _inlineHost; _inlineHost = value; - OnInlinesHostChanged(oldValue, value); + OnInlineHostChanged(oldValue, value); } } - internal virtual void OnInlinesHostChanged(IInlineHost? oldValue, IInlineHost? newValue) + internal virtual void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue) { } diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 87cf660cad..7e5b34acd9 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -79,9 +79,9 @@ namespace Avalonia.Controls.Primitives } /// - protected override TextLayout CreateTextLayout(Size constraint, string? text) + protected override TextLayout CreateTextLayout(string? text) { - return base.CreateTextLayout(constraint, RemoveAccessKeyMarker(text)); + return base.CreateTextLayout(RemoveAccessKeyMarker(text)); } /// diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 859503e693..1411d715ec 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Linq; using Avalonia.Controls.Documents; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -56,6 +57,16 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(Inlines)); + public static readonly DirectProperty CanCopyProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanCopy), + o => o.CanCopy); + + public static readonly RoutedEvent CopyingToClipboardEvent = + RoutedEvent.Register( + nameof(CopyingToClipboard), RoutingStrategies.Bubble); + + private bool _canCopy; private int _caretIndex; private int _selectionStart; private int _selectionEnd; @@ -75,7 +86,7 @@ namespace Avalonia.Controls InlineHost = this }; } - + public IBrush? SelectionBrush { get => GetValue(SelectionBrushProperty); @@ -156,50 +167,43 @@ namespace Avalonia.Controls } /// - /// Creates the used to render the text. + /// Property for determining if the Copy command can be executed. /// - /// The constraint of the text. - /// The text to format. - /// A object. - protected override TextLayout CreateTextLayout(Size constraint, string? text) + public bool CanCopy { - var defaultProperties = new GenericTextRunProperties( - new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), - FontSize, - TextDecorations, - Foreground); + get => _canCopy; + private set => SetAndRaise(CanCopyProperty, ref _canCopy, value); + } - var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, - defaultProperties, TextWrapping, LineHeight, 0); + public event EventHandler? CopyingToClipboard + { + add => AddHandler(CopyingToClipboardEvent, value); + remove => RemoveHandler(CopyingToClipboardEvent, value); + } - ITextSource textSource; + public async void Copy() + { + if (_canCopy || !IsTextSelectionEnabled) + { + return; + } - var inlines = Inlines; + var text = GetSelection(); - if (inlines is not null && inlines.HasComplexContent) + if (string.IsNullOrEmpty(text)) { - var textRuns = new List(); + return; + } - foreach (var inline in inlines) - { - inline.BuildTextRun(textRuns); - } + var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent); - textSource = new InlinesTextSource(textRuns); - } - else + RaiseEvent(eventArgs); + + if (!eventArgs.Handled) { - textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); + await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) + .SetTextAsync(text); } - - return new TextLayout( - textSource, - paragraphProperties, - TextTrimming, - constraint.Width, - constraint.Height, - maxLines: MaxLines, - lineHeight: LineHeight); } public override void Render(DrawingContext context) @@ -236,7 +240,7 @@ namespace Avalonia.Controls return; } - var text = Inlines.Text ?? Text; + var text = Text; SelectionStart = 0; SelectionEnd = text?.Length ?? 0; @@ -255,6 +259,75 @@ namespace Avalonia.Controls SelectionEnd = SelectionStart; } + + protected override string? GetText() + { + return _text ?? Inlines.Text; + } + + protected override void SetText(string? text) + { + var oldValue = _text ?? Inlines?.Text; + + if (Inlines is not null && Inlines.HasComplexContent) + { + Inlines.Text = text; + + _text = null; + } + else + { + _text = text; + } + + RaisePropertyChanged(TextProperty, oldValue, text); + } + + /// + /// Creates the used to render the text. + /// + /// A object. + protected override TextLayout CreateTextLayout(string? text) + { + var defaultProperties = new GenericTextRunProperties( + new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + FontSize, + TextDecorations, + Foreground); + + var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, + defaultProperties, TextWrapping, LineHeight, 0); + + ITextSource textSource; + + var inlines = Inlines; + + if (inlines is not null && inlines.HasComplexContent) + { + var textRuns = new List(); + + foreach (var inline in inlines) + { + inline.BuildTextRun(textRuns); + } + + textSource = new InlinesTextSource(textRuns); + } + else + { + textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); + } + + return new TextLayout( + textSource, + paragraphProperties, + TextTrimming, + _constraint.Width, + _constraint.Height, + maxLines: MaxLines, + lineHeight: LineHeight); + } + protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); @@ -262,6 +335,24 @@ namespace Avalonia.Controls ClearSelection(); } + protected override void OnKeyDown(KeyEventArgs e) + { + var handled = false; + var modifiers = e.KeyModifiers; + var keymap = AvaloniaLocator.Current.GetRequiredService(); + + bool Match(List gestures) => gestures.Any(g => g.Matches(e)); + + if (Match(keymap.Copy)) + { + Copy(); + + handled = true; + } + + e.Handled = handled; + } + protected override void OnPointerPressed(PointerPressedEventArgs e) { if (!IsTextSelectionEnabled) @@ -269,20 +360,21 @@ namespace Avalonia.Controls return; } - var text = Inlines.Text; + var text = Text; var clickInfo = e.GetCurrentPoint(this); if (text != null && clickInfo.Properties.IsLeftButtonPressed) { var point = e.GetPosition(this); - var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - - var hit = TextLayout.HitTestPoint(point); + var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); var oldIndex = CaretIndex; + + var hit = TextLayout.HitTestPoint(point); var index = hit.TextPosition; - CaretIndex = index; + + SetAndRaise(CaretIndexProperty, ref _caretIndex, index); #pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) @@ -368,7 +460,7 @@ namespace Avalonia.Controls caretIndex >= firstSelection && caretIndex <= lastSelection; if (!didClickInSelection) { - _caretIndex = SelectionEnd = SelectionStart = caretIndex; + CaretIndex = SelectionEnd = SelectionStart = caretIndex; } } @@ -389,29 +481,19 @@ namespace Avalonia.Controls } case nameof(TextProperty): { - OnTextChanged(change.OldValue as string, change.NewValue as string); + InvalidateTextLayout(); break; } } } - private void OnTextChanged(string? oldValue, string? newValue) + private string GetSelection() { - if (oldValue == newValue) - { - return; - } - - if (Inlines is null) + if (!IsTextSelectionEnabled) { - return; + return ""; } - Inlines.Text = newValue; - } - - private string GetSelection() - { var text = Inlines.Text ?? Text; if (string.IsNullOrEmpty(text)) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1f891b092f..2f83ee1002 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -130,7 +130,7 @@ namespace Avalonia.Controls protected string? _text; protected TextLayout? _textLayout; - private Size _constraint; + protected Size _constraint; /// /// Initializes static members of the class. @@ -149,7 +149,7 @@ namespace Avalonia.Controls { get { - return _textLayout ??= CreateTextLayout(_constraint, Text); + return _textLayout ??= CreateTextLayout(_text); } } @@ -176,11 +176,8 @@ namespace Avalonia.Controls /// public string? Text { - get => _text; - set - { - SetAndRaise(TextProperty, ref _text, value); - } + get => GetText(); + set => SetText(value); } /// @@ -302,11 +299,6 @@ namespace Avalonia.Controls set { SetValue(BaselineOffsetProperty, value); } } - public void Add(string text) - { - Text = text; - } - /// /// Reads the attached property from the given element /// @@ -481,6 +473,10 @@ namespace Avalonia.Controls control.SetValue(MaxLinesProperty, maxLines); } + public void Add(string text) + { + _text = text; + } /// /// Renders the to a drawing context. @@ -516,13 +512,21 @@ namespace Avalonia.Controls TextLayout.Draw(context, new Point(padding.Left, top)); } + protected virtual string? GetText() + { + return _text; + } + + protected virtual void SetText(string? text) + { + SetAndRaise(TextProperty, ref _text, text); + } + /// /// Creates the used to render the text. /// - /// The constraint of the text. - /// The text to format. /// A object. - protected virtual TextLayout CreateTextLayout(Size constraint, string? text) + protected virtual TextLayout CreateTextLayout(string? text) { var defaultProperties = new GenericTextRunProperties( new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), @@ -537,8 +541,8 @@ namespace Avalonia.Controls new SimpleTextSource((text ?? "").AsMemory(), defaultProperties), paragraphProperties, TextTrimming, - constraint.Width, - constraint.Height, + _constraint.Width, + _constraint.Height, maxLines: MaxLines, lineHeight: LineHeight); } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index 1ed33e6132..f29420ff87 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -30,7 +30,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting private bool Run(BiDiClassData t) { - var bidi = BidiAlgorithm.Instance.Value; + var bidi = new BidiAlgorithm(); var bidiData = new BidiData(t.ParagraphLevel); var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs index ed4c78aa3e..99dfc93a68 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs @@ -49,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Styling setter.Instance(control).Start(false); - Assert.Equal("", control.Text); + Assert.Equal(null, control.Text); } [Fact]