Browse Source

Add Copy geasture

Benedikt Stebner 3 years ago
parent
commit
90e0dcc9e3

+ 1 - 0
samples/Sandbox/MainWindow.axaml

@@ -13,6 +13,7 @@
         </Span>.
       </RichTextBlock>
       <TextBox Text="{Binding #txtBlock.SelectedText}"/>
+      <TextBlock Text="{Binding #txtBlock.Text}"/>
     </StackPanel>
   </Border>
 </Window>

+ 1 - 3
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)

+ 7 - 5
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)
             {

+ 2 - 2
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)
         {
 
         }

+ 2 - 2
src/Avalonia.Controls/Primitives/AccessText.cs

@@ -79,9 +79,9 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <inheritdoc/>
-        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));
         }
 
         /// <inheritdoc/>

+ 138 - 56
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<RichTextBlock, InlineCollection>(
                 nameof(Inlines));
 
+        public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
+            AvaloniaProperty.RegisterDirect<TextBox, bool>(
+                nameof(CanCopy),
+                o => o.CanCopy);
+
+        public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
+            RoutedEvent.Register<RichTextBlock, RoutedEventArgs>(
+                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
         }
 
         /// <summary>
-        /// Creates the <see cref="TextLayout"/> used to render the text.
+        /// Property for determining if the Copy command can be executed.
         /// </summary>
-        /// <param name="constraint">The constraint of the text.</param>
-        /// <param name="text">The text to format.</param>
-        /// <returns>A <see cref="TextLayout"/> object.</returns>
-        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<RoutedEventArgs>? 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<TextRun>();
+                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);
+        }
+
+        /// <summary>
+        /// Creates the <see cref="TextLayout"/> used to render the text.
+        /// </summary>
+        /// <returns>A <see cref="TextLayout"/> object.</returns>
+        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<TextRun>();
+
+                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<PlatformHotkeyConfiguration>();
+
+            bool Match(List<KeyGesture> 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))

+ 21 - 17
src/Avalonia.Controls/TextBlock.cs

@@ -130,7 +130,7 @@ namespace Avalonia.Controls
 
         protected string? _text;
         protected TextLayout? _textLayout;
-        private Size _constraint;
+        protected Size _constraint;
 
         /// <summary>
         /// Initializes static members of the <see cref="TextBlock"/> 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
         /// </summary>
         public string? Text
         {
-            get => _text;
-            set
-            {
-                SetAndRaise(TextProperty, ref _text, value);
-            }
+            get => GetText();
+            set => SetText(value);
         }
 
         /// <summary>
@@ -302,11 +299,6 @@ namespace Avalonia.Controls
             set { SetValue(BaselineOffsetProperty, value); }
         }
 
-        public void Add(string text)
-        {
-            Text = text;
-        }
-
         /// <summary>
         /// Reads the attached property from the given element
         /// </summary>
@@ -481,6 +473,10 @@ namespace Avalonia.Controls
             control.SetValue(MaxLinesProperty, maxLines);
         }
 
+        public void Add(string text)
+        {
+            _text = text;
+        }
 
         /// <summary>
         /// Renders the <see cref="TextBlock"/> 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);
+        }
+
         /// <summary>
         /// Creates the <see cref="TextLayout"/> used to render the text.
         /// </summary>
-        /// <param name="constraint">The constraint of the text.</param>
-        /// <param name="text">The text to format.</param>
         /// <returns>A <see cref="TextLayout"/> object.</returns>
-        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);
         }

+ 1 - 1
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<int, byte>(t.CodePoints).ToArray());

+ 1 - 1
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]