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