// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Controls.Primitives
{
///
/// A text block that displays a character prefixed with an underscore as an access key.
///
public class AccessText : TextBlock
{
///
/// Defines the attached property.
///
public static readonly AttachedProperty ShowAccessKeyProperty =
AvaloniaProperty.RegisterAttached("ShowAccessKey", inherits: true);
///
/// The access key handler for the current window.
///
private IAccessKeyHandler _accessKeys;
///
/// Initializes static members of the class.
///
static AccessText()
{
AffectsRender(ShowAccessKeyProperty);
}
///
/// Initializes a new instance of the class.
///
public AccessText()
{
this.GetObservable(TextProperty).Subscribe(TextChanged);
}
///
/// Gets the access key.
///
public char AccessKey
{
get;
private set;
}
///
/// Gets or sets a value indicating whether the access key should be underlined.
///
public bool ShowAccessKey
{
get { return GetValue(ShowAccessKeyProperty); }
set { SetValue(ShowAccessKeyProperty, value); }
}
///
/// Renders the to a drawing context.
///
/// The drawing context.
public override void Render(DrawingContext context)
{
base.Render(context);
int underscore = Text?.IndexOf('_') ?? -1;
if (underscore != -1 && ShowAccessKey)
{
var rect = HitTestTextPosition(underscore);
var offset = new Vector(0, -0.5);
context.DrawLine(
new Pen(Foreground, 1),
rect.BottomLeft + offset,
rect.BottomRight + offset);
}
}
///
/// Get the pixel location relative to the top-left of the layout box given the text position.
///
/// The text position.
///
private Rect HitTestTextPosition(int textPosition)
{
if (TextLayout == null)
{
return new Rect();
}
if (TextLayout.TextLines.Count == 0)
{
return new Rect();
}
if (textPosition < 0 || textPosition >= Text.Length)
{
var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1];
var offsetX = lastLine.LineMetrics.BaselineOrigin.X;
var lineX = offsetX + lastLine.LineMetrics.Size.Width;
var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height;
return new Rect(lineX, lineY, 0, lastLine.LineMetrics.Size.Height);
}
var currentY = 0.0;
foreach (var textLine in TextLayout.TextLines)
{
if (textLine.Text.End < textPosition)
{
currentY += textLine.LineMetrics.Size.Height;
continue;
}
var currentX = textLine.LineMetrics.BaselineOrigin.X;
foreach (var textRun in textLine.TextRuns)
{
if (!(textRun is ShapedTextRun shapedRun))
{
continue;
}
if (shapedRun.GlyphRun.Characters.End < textPosition)
{
currentX += shapedRun.GlyphRun.Bounds.Width;
continue;
}
var characterHit = shapedRun.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
var distance = shapedRun.GlyphRun.GetDistanceFromCharacterHit(characterHit);
currentX += distance - width;
if (characterHit.TrailingLength == 0)
{
width = 0.0;
}
return new Rect(currentX, currentY, width, shapedRun.GlyphRun.Bounds.Height);
}
}
return new Rect();
}
///
protected override TextLayout CreateTextLayout(Size constraint, string text)
{
return base.CreateTextLayout(constraint, StripAccessKey(text));
}
///
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_accessKeys = (e.Root as IInputRoot)?.AccessKeyHandler;
if (_accessKeys != null && AccessKey != 0)
{
_accessKeys.Register(AccessKey, this);
}
}
///
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (_accessKeys != null && AccessKey != 0)
{
_accessKeys.Unregister(this);
_accessKeys = null;
}
}
///
/// Returns a string with the first underscore stripped.
///
/// The text.
/// The text with the first underscore stripped.
private string StripAccessKey(string text)
{
var position = text.IndexOf('_');
if (position == -1)
{
return text;
}
else
{
return text.Substring(0, position) + text.Substring(position + 1);
}
}
///
/// Called when the property changes.
///
/// The new text.
private void TextChanged(string text)
{
var key = (char)0;
if (text != null)
{
int underscore = text.IndexOf('_');
if (underscore != -1 && underscore < text.Length - 1)
{
key = text[underscore + 1];
}
}
AccessKey = key;
if (_accessKeys != null && AccessKey != 0)
{
_accessKeys.Register(AccessKey, this);
}
}
}
}