Browse Source

Add Copy geasture

pull/8370/head
Benedikt Stebner 4 years ago
parent
commit
90e0dcc9e3
  1. 1
      samples/Sandbox/MainWindow.axaml
  2. 4
      src/Avalonia.Controls/Documents/InlineCollection.cs
  3. 12
      src/Avalonia.Controls/Documents/Span.cs
  4. 4
      src/Avalonia.Controls/Documents/TextElement.cs
  5. 4
      src/Avalonia.Controls/Primitives/AccessText.cs
  6. 194
      src/Avalonia.Controls/RichTextBlock.cs
  7. 38
      src/Avalonia.Controls/TextBlock.cs
  8. 2
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs
  9. 2
      tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

1
samples/Sandbox/MainWindow.axaml

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

4
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)

12
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)
{

4
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)
{
}

4
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/>

194
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))

38
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);
}

2
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());

2
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]

Loading…
Cancel
Save