diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs
index 127cfa98..618fa166 100644
--- a/src/Models/IssueTrackerRule.cs
+++ b/src/Models/IssueTrackerRule.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
-
+using Avalonia.Controls.Documents;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Models
@@ -10,6 +10,7 @@ namespace SourceGit.Models
public int Start { get; set; } = 0;
public int Length { get; set; } = 0;
public string URL { get; set; } = "";
+ public Run Link { get; set; } = null;
public bool Intersect(int start, int length)
{
diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml
index 9ff41264..d05e2ed9 100644
--- a/src/Resources/Styles.axaml
+++ b/src/Resources/Styles.axaml
@@ -280,11 +280,11 @@
-
-
diff --git a/src/Views/CommitMessagePresenter.cs b/src/Views/CommitMessagePresenter.cs
index 116442d9..1ab93ecd 100644
--- a/src/Views/CommitMessagePresenter.cs
+++ b/src/Views/CommitMessagePresenter.cs
@@ -6,6 +6,7 @@ using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Input;
+using Avalonia.Utilities;
namespace SourceGit.Views
{
@@ -38,6 +39,8 @@ namespace SourceGit.Views
if (change.Property == MessageProperty || change.Property == IssueTrackerRulesProperty)
{
Inlines.Clear();
+ _matches = null;
+ ClearHoveredIssueLink();
var message = Message;
if (string.IsNullOrEmpty(message))
@@ -61,6 +64,7 @@ namespace SourceGit.Views
}
matches.Sort((l, r) => l.Start - r.Start);
+ _matches = matches;
int pos = 0;
foreach (var match in matches)
@@ -68,12 +72,9 @@ namespace SourceGit.Views
if (match.Start > pos)
Inlines.Add(new Run(message.Substring(pos, match.Start - pos)));
- var link = new TextBlock();
- link.SetValue(TextProperty, message.Substring(match.Start, match.Length));
- link.SetValue(ToolTip.TipProperty, match.URL);
- link.Classes.Add("issue_link");
- link.PointerPressed += OnLinkPointerPressed;
- Inlines.Add(link);
+ match.Link = new Run(message.Substring(match.Start, match.Length));
+ match.Link.Classes.Add("issue_link");
+ Inlines.Add(match.Link);
pos = match.Start + match.Length;
}
@@ -83,16 +84,70 @@ namespace SourceGit.Views
}
}
- private void OnLinkPointerPressed(object sender, PointerPressedEventArgs e)
+ protected override void OnPointerMoved(PointerEventArgs e)
{
- if (sender is TextBlock text)
- {
- var tooltip = text.GetValue(ToolTip.TipProperty) as string;
- if (!string.IsNullOrEmpty(tooltip))
- Native.OS.OpenBrowser(tooltip);
+ base.OnPointerMoved(e);
- e.Handled = true;
+ if (e.Pointer.Captured == null && _matches != null)
+ {
+ var padding = Padding;
+ var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
+ point = new Point(
+ MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)),
+ MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Height, 0)));
+
+ var pos = TextLayout.HitTestPoint(point).TextPosition;
+ foreach (var match in _matches)
+ {
+ if (!match.Intersect(pos, 1))
+ continue;
+
+ if (match == _lastHover)
+ return;
+
+ _lastHover = match;
+ SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
+ ToolTip.SetTip(this, match.URL);
+ ToolTip.SetIsOpen(this, true);
+ return;
+ }
+
+ ClearHoveredIssueLink();
}
}
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ if (_lastHover != null)
+ {
+ SetCurrentValue(SelectionEndProperty, SelectionStart);
+ Native.OS.OpenBrowser(_lastHover.URL);
+ ClearHoveredIssueLink();
+ e.Handled = true;
+ return;
+ }
+
+ base.OnPointerPressed(e);
+ }
+
+ protected override void OnPointerExited(PointerEventArgs e)
+ {
+ base.OnPointerExited(e);
+ ClearHoveredIssueLink();
+ }
+
+ private void ClearHoveredIssueLink()
+ {
+ if (_lastHover != null)
+ {
+ ToolTip.SetTip(this, null);
+ SetCurrentValue(CursorProperty, Cursor.Parse("IBeam"));
+ _lastHover.Link.Classes.Remove("issue_link_hovered");
+ _lastHover = null;
+ }
+ }
+
+ private List _matches = null;
+ private Models.IssueTrackerMatch _lastHover = null;
}
}
diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs
index f104a3ea..5ac31695 100644
--- a/src/Views/Histories.axaml.cs
+++ b/src/Views/Histories.axaml.cs
@@ -11,6 +11,7 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
+using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace SourceGit.Views
@@ -185,6 +186,8 @@ namespace SourceGit.Views
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
{
Inlines.Clear();
+ _matches = null;
+ ClearHoveredIssueLink();
var subject = Subject;
if (string.IsNullOrEmpty(subject))
@@ -208,6 +211,7 @@ namespace SourceGit.Views
}
matches.Sort((l, r) => l.Start - r.Start);
+ _matches = matches;
int pos = 0;
foreach (var match in matches)
@@ -215,12 +219,9 @@ namespace SourceGit.Views
if (match.Start > pos)
Inlines.Add(new Run(subject.Substring(pos, match.Start - pos)));
- var link = new TextBlock();
- link.SetValue(TextProperty, subject.Substring(match.Start, match.Length));
- link.SetValue(ToolTip.TipProperty, match.URL);
- link.Classes.Add("issue_link");
- link.PointerPressed += OnLinkPointerPressed;
- Inlines.Add(link);
+ match.Link = new Run(subject.Substring(match.Start, match.Length));
+ match.Link.Classes.Add("issue_link");
+ Inlines.Add(match.Link);
pos = match.Start + match.Length;
}
@@ -230,17 +231,71 @@ namespace SourceGit.Views
}
}
- private void OnLinkPointerPressed(object sender, PointerPressedEventArgs e)
+ protected override void OnPointerMoved(PointerEventArgs e)
{
- if (sender is TextBlock text)
- {
- var tooltip = text.GetValue(ToolTip.TipProperty) as string;
- if (!string.IsNullOrEmpty(tooltip))
- Native.OS.OpenBrowser(tooltip);
+ base.OnPointerMoved(e);
- e.Handled = true;
+ if (_matches != null)
+ {
+ var padding = Padding;
+ var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
+ point = new Point(
+ MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)),
+ MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Height, 0)));
+
+ var textPosition = TextLayout.HitTestPoint(point).TextPosition;
+ foreach (var match in _matches)
+ {
+ if (!match.Intersect(textPosition, 1))
+ continue;
+
+ if (match == _lastHover)
+ return;
+
+ _lastHover = match;
+ SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
+ ToolTip.SetTip(this, match.URL);
+ ToolTip.SetIsOpen(this, true);
+ e.Handled = true;
+ return;
+ }
+
+ ClearHoveredIssueLink();
}
}
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ if (_lastHover != null)
+ {
+ Native.OS.OpenBrowser(_lastHover.URL);
+ ClearHoveredIssueLink();
+ e.Handled = true;
+ return;
+ }
+
+ base.OnPointerPressed(e);
+ }
+
+ protected override void OnPointerExited(PointerEventArgs e)
+ {
+ base.OnPointerExited(e);
+ ClearHoveredIssueLink();
+ }
+
+ private void ClearHoveredIssueLink()
+ {
+ if (_lastHover != null)
+ {
+ ToolTip.SetTip(this, null);
+ SetCurrentValue(CursorProperty, Cursor.Parse("Arrow"));
+ _lastHover.Link.Classes.Remove("issue_link_hovered");
+ _lastHover = null;
+ }
+ }
+
+ private List _matches = null;
+ private Models.IssueTrackerMatch _lastHover = null;
}
public class CommitTimeTextBlock : TextBlock