How to make Tab work inside a RichTextBox with Hyperlinks on Windows 8

I came across this problem recently:

You can´t navigate using only the keyboard to a Hyperlink inside a RichTextBox in a desktop program on Windows 8.

First I thought that it is always this way until I tried my program on a Windows 7 computer where it worked. I search the internet to find a solution/reason to this problem but couldn’t find one so I decided to make a behavior so that Tab to Hyperlink in a RichTextBox works the same way on Windows 8 as it works on Windows 7.

Capture tab

First we need a behavior that captures the Tab key by using the KeyDown event. This is fairly straight forward:

class TabToHyperlinksInRichTextBox : Behavior<RichTextBox>
    {
        protected override void OnAttached()
        {
            // Capture Keydown to capture the Tab key
            AssociatedObject.KeyDown += AssociatedObjectOnKeyDown;
        }

And of course after we capture the KeyDown event we need to filter out Tab. After we have found a Tab we need to test if Shift is down and if so the move focus the reverse way.

private void AssociatedObjectOnKeyDown(object sender, KeyEventArgs e)
{
    var rtf = sender as RichTextBox;
    // if not Tab key is pressed or there are no Hyperlinks in the RichTextBox let Windows handle it
    if (e.Key != Key.Tab || rtf == null || !ListOfAllHyperlinks(rtf.Document).Any())
        return;

    // Check if Shift is down
    var reverse = (Keyboard.Modifiers & ModifierKeys.Shift) > 0;
    // if not first Hyperlink has focus or if not _previousHyperlinkFocus is first Hyperlink when reverse is true.
    if (!(HasFirstLastHyperlinkFocus(rtf.Document, !reverse)  ||
       (reverse && _previousHyperlinkFocus != null &&
        _previousHyperlinkFocus.Equals(ListOfAllHyperlinks(rtf.Document).FirstOrDefault()))))
    {
        SetFocusToNextLink(rtf.Document, reverse);
        e.Handled = true;
    }

    _previousHyperlinkFocus = GetFocusedHyperlink(rtf.Document);
}

To set focus to a Hyperlink we iterate over the Hyperlinks inside the RichTextBox and set the focus to the next one. If no one has focus the first one gets it. And of course this is done reverse if parameter reverse is true. Pretty straight forward.

private static void SetFocusToNextLink(FlowDocument document, bool reverse)
{
    if (document == null)
        return;
    var setFocusToNext = !HasAnyHyperLinkFocus(document);
    var list = ListOfAllHyperlinks(document);
    if (reverse)
        list.Reverse();

    foreach (var link in list)
    {
        if (setFocusToNext)
        {
            link.Focus();
            return;
        }
        if (link.IsKeyboardFocusWithin)
            setFocusToNext = true;
    }
}

HasFirstLastHyperlinkFocus, GetFocusedHyperlink, HasAnyHyperLinkFocus and ListOfAllHyperlinks is helper functions to keep code a little cleaner, they can be seen in the github repo.

The problem

If it would be as described above it would be a quite nice piece of code, but (there always seems to be a but) as you can see above there is a variable called _previousHyperlinkFocus which makes the code a little complicated. This is needed because when we Tab or Shift+Tab to a RichTextBox it gets focused first before any Hyperlink inside. This focus state can not be separated from when we are on our way out of the RichTextBox using keyboard navigation. Therefore I have the _previousHyperlinkFocus variable to remember where we came from and thus know if we should let Windows handle the Tab (and leave the RichTextBox) or stay inside and set focus to a Hyperlink. Perhaps one can argue the the RichTextBox itself should not be focused only the Hyperlinks inside but this is how it works on Windows 7 so I did the same here.

Summary

It is strange that keyboard navigation on the RichTextBox works this differently between Windows 7 and Windows 8 and perhaps in the future this is going to change, but in the meantime feel free to use this behavior if you have the same problem as I did. It is located on github here and contains a fully working example.

This Post Has One Comment

Leave a Reply

Close Menu