AccessText.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using System;
  4. using Avalonia.Input;
  5. using Avalonia.Media;
  6. using Avalonia.Media.TextFormatting;
  7. namespace Avalonia.Controls.Primitives
  8. {
  9. /// <summary>
  10. /// A text block that displays a character prefixed with an underscore as an access key.
  11. /// </summary>
  12. public class AccessText : TextBlock
  13. {
  14. /// <summary>
  15. /// Defines the <see cref="ShowAccessKey"/> attached property.
  16. /// </summary>
  17. public static readonly AttachedProperty<bool> ShowAccessKeyProperty =
  18. AvaloniaProperty.RegisterAttached<AccessText, Control, bool>("ShowAccessKey", inherits: true);
  19. /// <summary>
  20. /// The access key handler for the current window.
  21. /// </summary>
  22. private IAccessKeyHandler _accessKeys;
  23. /// <summary>
  24. /// Initializes static members of the <see cref="AccessText"/> class.
  25. /// </summary>
  26. static AccessText()
  27. {
  28. AffectsRender<AccessText>(ShowAccessKeyProperty);
  29. }
  30. /// <summary>
  31. /// Initializes a new instance of the <see cref="AccessText"/> class.
  32. /// </summary>
  33. public AccessText()
  34. {
  35. this.GetObservable(TextProperty).Subscribe(TextChanged);
  36. }
  37. /// <summary>
  38. /// Gets the access key.
  39. /// </summary>
  40. public char AccessKey
  41. {
  42. get;
  43. private set;
  44. }
  45. /// <summary>
  46. /// Gets or sets a value indicating whether the access key should be underlined.
  47. /// </summary>
  48. public bool ShowAccessKey
  49. {
  50. get { return GetValue(ShowAccessKeyProperty); }
  51. set { SetValue(ShowAccessKeyProperty, value); }
  52. }
  53. /// <summary>
  54. /// Renders the <see cref="AccessText"/> to a drawing context.
  55. /// </summary>
  56. /// <param name="context">The drawing context.</param>
  57. public override void Render(DrawingContext context)
  58. {
  59. base.Render(context);
  60. int underscore = Text?.IndexOf('_') ?? -1;
  61. if (underscore != -1 && ShowAccessKey)
  62. {
  63. var rect = HitTestTextPosition(underscore);
  64. var offset = new Vector(0, -0.5);
  65. context.DrawLine(
  66. new Pen(Foreground, 1),
  67. rect.BottomLeft + offset,
  68. rect.BottomRight + offset);
  69. }
  70. }
  71. /// <summary>
  72. /// Get the pixel location relative to the top-left of the layout box given the text position.
  73. /// </summary>
  74. /// <param name="textPosition">The text position.</param>
  75. /// <returns></returns>
  76. private Rect HitTestTextPosition(int textPosition)
  77. {
  78. if (TextLayout == null)
  79. {
  80. return new Rect();
  81. }
  82. if (TextLayout.TextLines.Count == 0)
  83. {
  84. return new Rect();
  85. }
  86. if (textPosition < 0 || textPosition >= Text.Length)
  87. {
  88. var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1];
  89. var offsetX = lastLine.LineMetrics.BaselineOrigin.X;
  90. var lineX = offsetX + lastLine.LineMetrics.Size.Width;
  91. var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height;
  92. return new Rect(lineX, lineY, 0, lastLine.LineMetrics.Size.Height);
  93. }
  94. var currentY = 0.0;
  95. foreach (var textLine in TextLayout.TextLines)
  96. {
  97. if (textLine.Text.End < textPosition)
  98. {
  99. currentY += textLine.LineMetrics.Size.Height;
  100. continue;
  101. }
  102. var currentX = textLine.LineMetrics.BaselineOrigin.X;
  103. foreach (var textRun in textLine.TextRuns)
  104. {
  105. if (!(textRun is ShapedTextRun shapedRun))
  106. {
  107. continue;
  108. }
  109. if (shapedRun.GlyphRun.Characters.End < textPosition)
  110. {
  111. currentX += shapedRun.GlyphRun.Bounds.Width;
  112. continue;
  113. }
  114. var characterHit = shapedRun.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
  115. var distance = shapedRun.GlyphRun.GetDistanceFromCharacterHit(characterHit);
  116. currentX += distance - width;
  117. if (characterHit.TrailingLength == 0)
  118. {
  119. width = 0.0;
  120. }
  121. return new Rect(currentX, currentY, width, shapedRun.GlyphRun.Bounds.Height);
  122. }
  123. }
  124. return new Rect();
  125. }
  126. /// <inheritdoc/>
  127. protected override TextLayout CreateTextLayout(Size constraint, string text)
  128. {
  129. return base.CreateTextLayout(constraint, StripAccessKey(text));
  130. }
  131. /// <inheritdoc/>
  132. protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
  133. {
  134. base.OnAttachedToVisualTree(e);
  135. _accessKeys = (e.Root as IInputRoot)?.AccessKeyHandler;
  136. if (_accessKeys != null && AccessKey != 0)
  137. {
  138. _accessKeys.Register(AccessKey, this);
  139. }
  140. }
  141. /// <inheritdoc/>
  142. protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
  143. {
  144. base.OnDetachedFromVisualTree(e);
  145. if (_accessKeys != null && AccessKey != 0)
  146. {
  147. _accessKeys.Unregister(this);
  148. _accessKeys = null;
  149. }
  150. }
  151. /// <summary>
  152. /// Returns a string with the first underscore stripped.
  153. /// </summary>
  154. /// <param name="text">The text.</param>
  155. /// <returns>The text with the first underscore stripped.</returns>
  156. private string StripAccessKey(string text)
  157. {
  158. var position = text.IndexOf('_');
  159. if (position == -1)
  160. {
  161. return text;
  162. }
  163. else
  164. {
  165. return text.Substring(0, position) + text.Substring(position + 1);
  166. }
  167. }
  168. /// <summary>
  169. /// Called when the <see cref="TextBlock.Text"/> property changes.
  170. /// </summary>
  171. /// <param name="text">The new text.</param>
  172. private void TextChanged(string text)
  173. {
  174. var key = (char)0;
  175. if (text != null)
  176. {
  177. int underscore = text.IndexOf('_');
  178. if (underscore != -1 && underscore < text.Length - 1)
  179. {
  180. key = text[underscore + 1];
  181. }
  182. }
  183. AccessKey = key;
  184. if (_accessKeys != null && AccessKey != 0)
  185. {
  186. _accessKeys.Register(AccessKey, this);
  187. }
  188. }
  189. }
  190. }