diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs index 6721adcc8..dc2f184bf 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs @@ -244,7 +244,29 @@ internal bool OnPopupAttachmentClicked(PopupAttachment attachment) if (handler is not null) { var args = new PopupAttachmentClickedEventArgs(attachment); - PopupAttachmentClicked?.Invoke(this, args); + handler.Invoke(this, args); + return args.Handled; + } + return false; + } + + /// + /// Raised when a link is clicked + /// + /// + /// By default, when an link is clicked, the default application (Browser) for the file type (if any) is launched. To override this, + /// listen to this event, set the property to true and perform + /// your own logic. + /// + public event EventHandler? HyperLinkClicked; + + internal bool OnHyperLinkClicked(Uri uri) + { + var handler = HyperLinkClicked; + if (handler is not null) + { + var args = new HyperLinkClickedEventArgs(uri); + handler.Invoke(this, args); return args.Handled; } return false; @@ -271,5 +293,26 @@ internal PopupAttachmentClickedEventArgs(PopupAttachment attachment) /// public PopupAttachment Attachment { get; } } + + /// + /// Event argument for the event. + /// + public class HyperLinkClickedEventArgs : EventArgs + { + internal HyperLinkClickedEventArgs(Uri uri) + { + Uri = uri; + } + + /// + /// Gets or sets a value indicating whether the event handler has handled the event and the default action should be prevented. + /// + public bool Handled { get; set; } + + /// + /// Gets the URI that was clicked. + /// + public Uri Uri { get; } + } } #endif \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Maui.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Maui.cs index 6f6db3bde..6643bdb6d 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Maui.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Maui.cs @@ -83,7 +83,7 @@ private void OnElementPropertyChanged() } } - private static IEnumerable VisitChildren(MarkupNode parent) + private IEnumerable VisitChildren(MarkupNode parent) { // Create views for all the children of a given node. // Nodes with blocks are converted individually, but consecutive inline-only nodes are grouped into labels. @@ -116,7 +116,7 @@ private static IEnumerable VisitChildren(MarkupNode parent) } } - private static View CreateBlock(MarkupNode node) + private View CreateBlock(MarkupNode node) { // Create a view for a single block node. switch (node.Type) @@ -218,7 +218,7 @@ static View VerticallyAlignTableCell(MarkupNode node, View cellContent) } } - private static Label CreateFormattedText(IEnumerable nodes) + private Label CreateFormattedText(IEnumerable nodes) { // Flattens given tree of inline nodes into a single label. var str = new FormattedString(); @@ -232,7 +232,7 @@ private static Label CreateFormattedText(IEnumerable nodes) return new Label { FormattedText = str, LineBreakMode = LineBreakMode.WordWrap }; } - private static IEnumerable VisitInline(MarkupNode node) + private IEnumerable VisitInline(MarkupNode node) { // Converts a single inline node into a sequence of spans. // The whole tree is expected to only contain inline nodes. Other nodes are handled by VisitBlock. @@ -245,11 +245,7 @@ private static IEnumerable VisitInline(MarkupNode node) var tapRecognizer = new TapGestureRecognizer(); tapRecognizer.Tapped += (s, e) => { - try - { - Browser.OpenAsync(node.Content, BrowserLaunchMode.SystemPreferred); - } - catch { } + OnHyperlinkClicked(linkUri); }; foreach (var subNode in node.Children) { @@ -298,7 +294,7 @@ private static IEnumerable VisitInline(MarkupNode node) } } - private static Grid ConvertTableToGrid(MarkupNode table) + private Grid ConvertTableToGrid(MarkupNode table) { // Determines the dimensions of a grid necessary to hold a given table. // Utilizes a dynamically-sized 2D bitmap (`gridMap`) to mark occupied cells while iterating over the table. @@ -455,6 +451,16 @@ private static bool MapsToBlock(MarkupNode node) { return node.Type is MarkupType.List or MarkupType.Table or MarkupType.Block or MarkupType.Divider or MarkupType.Image; } + + private PopupViewer? GetPopupViewerParent() + { + var parent = this.Parent; + while (parent is not null && parent is not PopupViewer popup) + { + parent = parent.Parent; + } + return parent as PopupViewer; + } } } #endif \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs index b9a0956ac..9f12cb4f5 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs @@ -17,6 +17,7 @@ #if WPF using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.Toolkit.Internal; +using Esri.ArcGISRuntime.Toolkit.UI.Controls; using Esri.ArcGISRuntime.UI; using System.Windows.Documents; using System.Windows.Input; @@ -57,7 +58,7 @@ private void OnElementPropertyChanged() } } - private static IEnumerable VisitAndAddBlocks(IEnumerable nodes) + private IEnumerable VisitAndAddBlocks(IEnumerable nodes) { Paragraph? inlineHolder = null; foreach (var node in nodes) @@ -81,7 +82,7 @@ private static IEnumerable VisitAndAddBlocks(IEnumerable node yield return inlineHolder; } - private static IEnumerable VisitAndAddInlines(IEnumerable nodes) + private IEnumerable VisitAndAddInlines(IEnumerable nodes) { foreach (var node in nodes) { @@ -96,7 +97,7 @@ private static IEnumerable VisitAndAddInlines(IEnumerable no } } - private static Block VisitBlock(MarkupNode node) + private Block VisitBlock(MarkupNode node) { switch (node.Type) { @@ -176,7 +177,7 @@ private static Block VisitBlock(MarkupNode node) } } - private static Inline VisitInline(MarkupNode node) + private Inline VisitInline(MarkupNode node) { switch (node.Type) { @@ -287,9 +288,19 @@ private static System.Windows.Media.Color ConvertColor(System.Drawing.Color colo _ => TextAlignment.Left, }; - private static async void NavigateToUri(object sender, RequestNavigateEventArgs ea) + private void NavigateToUri(object sender, RequestNavigateEventArgs ea) { - await Launcher.LaunchUriAsync(ea.Uri); + OnHyperlinkClicked(ea.Uri); + } + + private PopupViewer? GetPopupViewerParent() + { + var parent = VisualTreeHelper.GetParent(this); + while (parent is not null and not PopupViewer) + { + parent = VisualTreeHelper.GetParent(parent); + } + return parent as PopupViewer; } } } diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs index 6cbb0409e..8e086efd6 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs @@ -19,9 +19,13 @@ using Esri.ArcGISRuntime.Toolkit.Internal; using Esri.ArcGISRuntime.UI; #if WPF +using Esri.ArcGISRuntime.Toolkit.UI.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Navigation; +#elif MAUI +using Esri.ArcGISRuntime.Toolkit.Maui; +using Microsoft.Maui.ApplicationModel; #endif @@ -70,6 +74,41 @@ public TextPopupElement? Element /// public static readonly DependencyProperty ElementProperty = PropertyHelper.CreateProperty(nameof(Element), null, (s, oldValue, newValue) => s.OnElementPropertyChanged()); + + /// + /// Occurs when an URL is clicked. + /// + /// + /// Override this to prevent the default open action. + /// + /// Clicked URL + public virtual async void OnHyperlinkClicked(Uri uri) + { + if (uri is not null) + { + var viewer = GetPopupViewerParent(); + if (viewer is not null) + { + var handled = viewer.OnHyperLinkClicked(uri); + if (handled) + return; + } + + try + { +#if WPF + await Launcher.LaunchUriAsync(uri); +#elif MAUI + await Browser.OpenAsync(uri, BrowserLaunchMode.SystemPreferred); +#endif + } + catch (Exception ex) + { + System.Diagnostics.Trace.WriteLine($"Failed to open URL: " + ex.Message); + } + } + + } } } #endif \ No newline at end of file