From 317621c567c860e4a48ebd4123be07970ec293ca Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Fri, 11 Sep 2009 15:29:51 +0000 Subject: [PATCH] Build hyperlink support into AvalonEdit and enable it by default. git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@4908 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61 --- .../Editing/EditingCommandHandler.cs | 2 +- .../Editing/TextArea.cs | 17 +++ .../ICSharpCode.AvalonEdit.csproj | 2 + .../Rendering/LinkElementGenerator.cs | 138 ++++++++++++++++++ .../Rendering/NewLineElementGenerator.cs | 12 +- .../SingleCharacterElementGenerator.cs | 33 +---- .../Rendering/TextView.cs | 43 ++++-- .../Rendering/VisualLine.cs | 3 - .../Rendering/VisualLineElementGenerator.cs | 5 + .../Rendering/VisualLineLinkText.cs | 111 ++++++++++++++ .../TextEditorOptions.cs | 56 +++++++ .../Project/Src/Gui/Workbench/WpfWorkbench.cs | 1 + 12 files changed, 375 insertions(+), 48 deletions(-) create mode 100644 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/LinkElementGenerator.cs create mode 100644 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.cs diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs index f81a1e6c45..335e94ed80 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs @@ -377,7 +377,7 @@ namespace ICSharpCode.AvalonEdit.Editing TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); - textArea.Selection = new SimpleSelection(currentLine.Offset, currentLine.EndOffset); + textArea.Selection = new SimpleSelection(currentLine.Offset, currentLine.Offset + currentLine.TotalLength); textArea.RemoveSelectedText(); args.Handled = true; } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextArea.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextArea.cs index df945327ab..3a8a8afa49 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextArea.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextArea.cs @@ -756,6 +756,23 @@ namespace ICSharpCode.AvalonEdit.Editing } #endregion + #region OnKeyDown/OnKeyUp + // Make life easier for text editor extensions that use a different cursor based on the pressed modifier keys. + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + TextView.InvalidateCursor(); + } + + /// + protected override void OnKeyUp(KeyEventArgs e) + { + base.OnKeyUp(e); + TextView.InvalidateCursor(); + } + #endregion + /// /// Gets the requested service. /// diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index 8d2c0da60b..276e6c66ba 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -239,6 +239,7 @@ TextView.cs + @@ -254,6 +255,7 @@ VisualLine.cs + diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/LinkElementGenerator.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/LinkElementGenerator.cs new file mode 100644 index 0000000000..b24ca749e8 --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/LinkElementGenerator.cs @@ -0,0 +1,138 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; + +using ICSharpCode.AvalonEdit.Document; +using System.Windows.Navigation; + +namespace ICSharpCode.AvalonEdit.Rendering +{ + // This class is public because it can be used as a base class for custom links. + + /// + /// Detects hyperlinks and makes them clickable. + /// + /// + /// This element generator can be easily enabled and configured using the + /// . + /// + public class LinkElementGenerator : VisualLineElementGenerator, IBuiltinElementGenerator + { + // a link starts with a protocol (or just with www), followed by 0 or more 'link characters', followed by a link end character + // (this allows accepting punctuation inside links but not at the end) + internal readonly static Regex defaultLinkRegex = new Regex(@"\b(https?://|ftp://|www\.)[\w\d\._/\-~%()+:?&=]*[\w\d/]"); + + // try to detect email addresses + internal readonly static Regex defaultMailRegex = new Regex(@"\b[\w\d\.\-]+\@[\w\d\.\-]+\.[a-z]{2,6}\b"); + + readonly Regex linkRegex; + + /// + /// Gets/Sets whether the user needs to press Control to click the link. + /// The default value is true. + /// + public bool RequireControlModifierForClick { get; set; } + + /// + /// Creates a new LinkElementGenerator. + /// + public LinkElementGenerator() + { + this.linkRegex = defaultLinkRegex; + this.RequireControlModifierForClick = true; + } + + /// + /// Creates a new LinkElementGenerator using the specified regex. + /// + protected LinkElementGenerator(Regex regex) : this() + { + if (regex == null) + throw new ArgumentNullException("regex"); + this.linkRegex = regex; + } + + void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options) + { + this.RequireControlModifierForClick = options.RequireControlModifierForHyperlinkClick; + } + + Match GetMatch(int startOffset) + { + DocumentLine endLine = CurrentContext.VisualLine.LastDocumentLine; + int endOffset = endLine.Offset + endLine.Length; + string relevantText = CurrentContext.Document.GetText(startOffset, endOffset - startOffset); + return linkRegex.Match(relevantText); + } + + /// + public override int GetFirstInterestedOffset(int startOffset) + { + Match m = GetMatch(startOffset); + return m.Success ? startOffset + m.Index : -1; + } + + /// + public override VisualLineElement ConstructElement(int offset) + { + Match m = GetMatch(offset); + if (m.Success && m.Index == 0) { + VisualLineLinkText linkText = new VisualLineLinkText(CurrentContext.VisualLine, m.Length); + linkText.NavigateUri = GetUriFromMatch(m); + linkText.RequireControlModifierForClick = this.RequireControlModifierForClick; + return linkText; + } else { + return null; + } + } + + /// + /// Fetches the URI from the regex match. + /// + protected virtual Uri GetUriFromMatch(Match match) + { + string targetUrl = match.Value; + if (targetUrl.StartsWith("www.", StringComparison.Ordinal)) + targetUrl = "http://" + targetUrl; + return new Uri(targetUrl); + } + } + + // This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions. + + /// + /// Detects e-mail addresses and makes them clickable. + /// + /// + /// This element generator can be easily enabled and configured using the + /// . + /// + sealed class MailLinkElementGenerator : LinkElementGenerator + { + /// + /// Creates a new MailLinkElementGenerator. + /// + public MailLinkElementGenerator() + : base(defaultMailRegex) + { + } + + /// + protected override Uri GetUriFromMatch(Match match) + { + return new Uri("mailto:" + match.Value); + } + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/NewLineElementGenerator.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/NewLineElementGenerator.cs index 3f412db493..24e561cbfa 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/NewLineElementGenerator.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/NewLineElementGenerator.cs @@ -14,11 +14,21 @@ using ICSharpCode.AvalonEdit.Document; namespace ICSharpCode.AvalonEdit.Rendering { + // This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions. + /// /// Elements generator that displays "¶" at the end of lines. /// - public class NewLineElementGenerator : VisualLineElementGenerator + /// + /// This element generator can be easily enabled and configured using the + /// . + /// + sealed class NewLineElementGenerator : VisualLineElementGenerator, IBuiltinElementGenerator { + void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options) + { + } + /// public override int GetFirstInterestedOffset(int startOffset) { diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/SingleCharacterElementGenerator.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/SingleCharacterElementGenerator.cs index 95d27872d6..ad9aef6ab7 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/SingleCharacterElementGenerator.cs @@ -16,6 +16,8 @@ using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Rendering { + // This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions. + /// /// Element generator that displays · for spaces and » for tabs and a box for control characters. /// @@ -24,7 +26,7 @@ namespace ICSharpCode.AvalonEdit.Rendering /// . /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace")] - public class SingleCharacterElementGenerator : VisualLineElementGenerator, IWeakEventListener + sealed class SingleCharacterElementGenerator : VisualLineElementGenerator, IBuiltinElementGenerator { /// /// Gets/Sets whether to show · for spaces. @@ -51,34 +53,7 @@ namespace ICSharpCode.AvalonEdit.Rendering this.ShowBoxForControlCharacters = true; } - /// - /// Fetch options from the text editor and synchronize with future option changes. - /// - public void SynchronizeOptions(ITextEditorComponent textEditor) - { - if (textEditor == null) - throw new ArgumentNullException("textEditor"); - FetchOptions(textEditor.Options); - TextEditorWeakEventManager.OptionChanged.AddListener(textEditor, this); - } - - /// - protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) - { - if (managerType == typeof(TextEditorWeakEventManager.OptionChanged)) { - ITextEditorComponent component = (ITextEditorComponent)sender; - FetchOptions(component.Options); - return true; - } - return false; - } - - bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) - { - return ReceiveWeakEvent(managerType, sender, e); - } - - void FetchOptions(TextEditorOptions options) + void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options) { this.ShowSpaces = options.ShowSpaces; this.ShowTabs = options.ShowTabs; diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs index 34b5936cf5..d4f93e6087 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs @@ -50,15 +50,12 @@ namespace ICSharpCode.AvalonEdit.Rendering public TextView() { services.AddService(typeof(TextView), this); - this.Options = new TextEditorOptions(); textLayer = new TextLayer(this); elementGenerators = new ObserveAddRemoveCollection(ElementGenerator_Added, ElementGenerator_Removed); lineTransformers = new ObserveAddRemoveCollection(LineTransformer_Added, LineTransformer_Removed); backgroundRenderers = new ObserveAddRemoveCollection(BackgroundRenderer_Added, BackgroundRenderer_Removed); - - SingleCharacterElementGenerator sceg = new SingleCharacterElementGenerator(); - sceg.SynchronizeOptions(this); - elementGenerators.Add(sceg); + this.Options = new TextEditorOptions(); + Debug.Assert(singleCharacterElementGenerator != null); // assert that the option change created the builtin element generators layers = new UIElementCollection(this, this); InsertLayer(textLayer, KnownLayer.Text, LayerInsertionPosition.Replace); @@ -172,7 +169,7 @@ namespace ICSharpCode.AvalonEdit.Rendering if (OptionChanged != null) { OptionChanged(this, e); } - UpdateNewlineVisibilityFromOptions(); + UpdateBuiltinElementGeneratorsFromOptions(); Redraw(); } @@ -235,21 +232,39 @@ namespace ICSharpCode.AvalonEdit.Rendering DisconnectFromTextView(lineTransformer); Redraw(); } + #endregion + #region Builtin ElementGenerators NewLineElementGenerator newLineElementGenerator; + SingleCharacterElementGenerator singleCharacterElementGenerator; + LinkElementGenerator linkElementGenerator; + MailLinkElementGenerator mailLinkElementGenerator; + + void UpdateBuiltinElementGeneratorsFromOptions() + { + TextEditorOptions options = this.Options; + + AddRemoveDefaultElementGeneratorOnDemand(ref newLineElementGenerator, options.ShowEndOfLine); + AddRemoveDefaultElementGeneratorOnDemand(ref singleCharacterElementGenerator, options.ShowBoxForControlCharacters || options.ShowSpaces || options.ShowTabs); + AddRemoveDefaultElementGeneratorOnDemand(ref linkElementGenerator, options.EnableHyperlinks); + AddRemoveDefaultElementGeneratorOnDemand(ref mailLinkElementGenerator, options.EnableEmailHyperlinks); + } - void UpdateNewlineVisibilityFromOptions() + void AddRemoveDefaultElementGeneratorOnDemand(ref T generator, bool demand) + where T : VisualLineElementGenerator, IBuiltinElementGenerator, new() { - bool hasNewlineGenerator = newLineElementGenerator != null; - if (hasNewlineGenerator != Options.ShowEndOfLine) { - if (Options.ShowEndOfLine) { - newLineElementGenerator = new NewLineElementGenerator(); - this.ElementGenerators.Add(newLineElementGenerator); + bool hasGenerator = generator != null; + if (hasGenerator != demand) { + if (demand) { + generator = new T(); + this.ElementGenerators.Add(generator); } else { - this.ElementGenerators.Remove(newLineElementGenerator); - newLineElementGenerator = null; + this.ElementGenerators.Remove(generator); + generator = null; } } + if (generator != null) + generator.FetchOptions(this.Options); } #endregion diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs index 915752c838..a07a2cfa38 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs @@ -81,9 +81,6 @@ namespace ICSharpCode.AvalonEdit.Rendering g.FinishGeneration(); } -// if (FirstDocumentLine.Length != 0) -// elements.Add(new VisualLineText(FirstDocumentLine.Text, FirstDocumentLine.Length)); -// //elements.Add(new VisualNewLine(VisualNewLine.NewLineType.Lf)); this.Elements = elements.AsReadOnly(); CalculateOffsets(context.GlobalTextRunProperties); } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineElementGenerator.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineElementGenerator.cs index fea9d62d96..1aca4c00f3 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineElementGenerator.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineElementGenerator.cs @@ -59,4 +59,9 @@ namespace ICSharpCode.AvalonEdit.Rendering /// public abstract VisualLineElement ConstructElement(int offset); } + + internal interface IBuiltinElementGenerator + { + void FetchOptions(TextEditorOptions options); + } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.cs new file mode 100644 index 0000000000..8efd56eb02 --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.cs @@ -0,0 +1,111 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; +using System.Windows.Navigation; + +namespace ICSharpCode.AvalonEdit.Rendering +{ + /// + /// VisualLineElement that represents a piece of text and is a clickable link. + /// + public class VisualLineLinkText : VisualLineText + { + /// + /// Gets/Sets the URL that is navigated to when the link is clicked. + /// + public Uri NavigateUri { get; set; } + + /// + /// Gets/Sets the window name where the URL will be opened. + /// + public string TargetName { get; set; } + + /// + /// Gets/Sets whether the user needs to press Control to click the link. + /// The default value is true. + /// + public bool RequireControlModifierForClick { get; set; } + + /// + /// Creates a visual line text element with the specified length. + /// It uses the and its + /// to find the actual text string. + /// + public VisualLineLinkText(VisualLine parentVisualLine, int length) : base(parentVisualLine, length) + { + this.RequireControlModifierForClick = true; + } + + /// + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + this.TextRunProperties.SetForegroundBrush(Brushes.Blue); + this.TextRunProperties.SetTextDecorations(TextDecorations.Underline); + return base.CreateTextRun(startVisualColumn, context); + } + + bool LinkIsClickable() + { + if (NavigateUri == null) + return false; + if (RequireControlModifierForClick) + return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control; + else + return true; + } + + /// + protected internal override void OnQueryCursor(QueryCursorEventArgs e) + { + if (LinkIsClickable()) { + e.Handled = true; + e.Cursor = Cursors.Hand; + } + } + + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", + Justification = "I've seen Process.Start throw undocumented exceptions when the mail client / web browser is installed incorrectly")] + protected internal override void OnMouseDown(MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Left && !e.Handled && LinkIsClickable()) { + RequestNavigateEventArgs args = new RequestNavigateEventArgs(this.NavigateUri, this.TargetName); + args.RoutedEvent = Hyperlink.RequestNavigateEvent; + FrameworkElement element = e.Source as FrameworkElement; + if (element != null) { + // allow user code to handle the navigation request + element.RaiseEvent(args); + } + if (!args.Handled) { + try { + Process.Start(this.NavigateUri.ToString()); + } catch { + // ignore all kinds of errors during web browser start + } + } + e.Handled = true; + } + } + + /// + protected override VisualLineText CreateInstance(int length) + { + return new VisualLineLinkText(ParentVisualLine, length) { + NavigateUri = this.NavigateUri, + TargetName = this.TargetName, + RequireControlModifierForClick = this.RequireControlModifierForClick + }; + } + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs index 7ab0f6d476..2e5e14a310 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs @@ -111,6 +111,60 @@ namespace ICSharpCode.AvalonEdit } #endregion + #region EnableHyperlinks + bool enableHyperlinks = true; + + /// + /// Gets/Sets whether to enable clickable hyperlinks in the editor. + /// + /// The default value is true. + [DefaultValue(true)] + public virtual bool EnableHyperlinks { + get { return enableHyperlinks; } + set { + if (enableHyperlinks != value) { + enableHyperlinks = value; + OnPropertyChanged("EnableHyperlinks"); + } + } + } + + bool enableEmailHyperlinks = true; + + /// + /// Gets/Sets whether to enable clickable hyperlinks for e-mail addresses in the editor. + /// + /// The default value is true. + [DefaultValue(true)] + public virtual bool EnableEmailHyperlinks { + get { return enableEmailHyperlinks; } + set { + if (enableEmailHyperlinks != value) { + enableEmailHyperlinks = value; + OnPropertyChanged("EnableEMailHyperlinks"); + } + } + } + + bool requireControlModifierForHyperlinkClick = true; + + /// + /// Gets/Sets whether the user needs to press Control to click hyperlinks. + /// The default value is true. + /// + /// The default value is true. + [DefaultValue(true)] + public virtual bool RequireControlModifierForHyperlinkClick { + get { return requireControlModifierForHyperlinkClick; } + set { + if (requireControlModifierForHyperlinkClick != value) { + requireControlModifierForHyperlinkClick = value; + OnPropertyChanged("RequireControlModifierForHyperlinkClick"); + } + } + } + #endregion + #region TabSize / IndentationSize / ConvertTabsToSpaces / GetIndentationString // I'm using '_' prefixes for the fields here to avoid confusion with the local variables // in the methods below. @@ -157,6 +211,7 @@ namespace ICSharpCode.AvalonEdit /// Gets the text used for indentation. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")] + [Browsable(false)] public string IndentationString { get { return GetIndentationString(1); } } @@ -182,6 +237,7 @@ namespace ICSharpCode.AvalonEdit /// /// Gets/Sets whether copying without a selection copies the whole current line. /// + [DefaultValue(true)] public virtual bool CutCopyWholeLine { get { return cutCopyWholeLine; } set { diff --git a/src/Main/Base/Project/Src/Gui/Workbench/WpfWorkbench.cs b/src/Main/Base/Project/Src/Gui/Workbench/WpfWorkbench.cs index 9f0236a63c..f1db245b1e 100644 --- a/src/Main/Base/Project/Src/Gui/Workbench/WpfWorkbench.cs +++ b/src/Main/Base/Project/Src/Gui/Workbench/WpfWorkbench.cs @@ -123,6 +123,7 @@ namespace ICSharpCode.SharpDevelop.Gui void OnRequestNavigate(object sender, RequestNavigateEventArgs e) { + e.Handled = true; if (e.Uri.Scheme == "mailto") { try { Process.Start(e.Uri.ToString());