Skip to content

Updated Tutorial: Active Words

This post was originally published in 2018 and has been updated to use Xojo API 2.0.

Follow this tutorial to create active, aka clickable, words in the text of a TextArea control in your Xojo projects. Learn to use the Object-oriented Delegate design pattern to dynamically change how your app reacts when the user clicks on those active words. Best of all, this project is cross-platform, so you can use it for macOS, Windows and Linux!

The active words will be based on the use of Pairs, so one piece of information will be the active word and the other piece of information will be the associated data, which is always a String. The associated data could be a link or text you want to show in another control when the user clicks on the active word.

For the sake of this tutorial, the class we are creating has some limitations. For example, you won’t be able to repeat the same word and assign it to take different actions. If you want your code to react the same to the same active word or words throughout your project, well then, that’s what it will do. Anyway, this tutorial serves as a good starting point and you’ll be able to modify and improve it to meet your own needs.

Download the class with the example project from here.

TextArea Subclass

This feature will be based in the TextArea class, the first step is add a new TextArea control from the Library panel to the Navigator, change its name to LinkDetectorTextArea. Next, add the following Event Handlers to the TextArea subclass. These will be responsible for following the pointer movement and reacting to the registered active words for the instance.

  • Open. To assign the pointer cursor, more appropriate for the kind of functionality this subclass will provide
  • MouseMove. To detect if there is an active word under the cursor pointer to click on
  • MouseDown. To call all the registered Observers when the user clicks on an active word, passing along the detected word and the associated information

Since we also want to make these events available for any instance created from the subclass, we have to create them again as Event Definitions. With the subclass selected in the Navigator, select Insert > Event Definition using the following signatures:

  • Event Name. Open.
  • Event Name. MouseMove. Parameters: X as Integer, Y as Integer.
  • Event Name. MouseDown. Parameters: X as Integer, Y as Integer. Return Type: Boolean.

Add some Private properties used by the added Event Handlers and other methods we have yet to define to the class. Set these properties as private because they just need to be accessed from the class itself and not from other pieces of external code or additional classes inherited from the subclass:

  • Name. boundedWord. Type: Pair. This is the property that will point to the current matched active word from the text.
  • Name. previousBoundedWord. Type: Pair. This property points to the previous matched word from the text.
  • Name. linkedWords. Type: Dictionary. This is the Dictionary that will contain all the active words / additional data pairs.
  • Name. observers(). Type: ActionDelegate. This is an Array containing all the registered observers of the type ActionDelegate yet to be defined.

As we have seen, we need to define a Delegate type for the class. Select Insert > Delegate with the following data:

  • Delegate Name: ActionDelegate.
  • Parameters: boundedWord as Pair.
  • Scope: Public.

Assigning the Cursor Type

Select the Open Event and add the following code in the corresponding Code Editor:

Me.MouseCursor = System.cursors.standardpointer
RaiseEvent open

We simply use this Event for assigning the Arrow cursor type that is most appropriate.

Matching the Active Words

Next, select the MouseMove Event Handler and type the following code in the corresponding Code Editor:

Var tlen As Integer = Me.Text.Length
If previousBoundedWord <> Nil Then
  setStyleForWord(previousBoundedWord, False)
  previousBoundedWord = Nil
End If

Var boundLeft, boundRight As Integer = -1
Var startPosition As Integer = Me.CharacterPosition(x,y)

If CharacterPosition(x,y) >= tLen Then
  If boundedWord <> Nil Then setStyleForWord( boundedWord, False )
  mboundedWord = Nil
ElseIf Me.Text.Middle(startPosition,1) <> Chr(32) And Me.Text.Middle(startPosition,1) <> EndOfLine Then
  For n As Integer = startPosition DownTo 0
    If Me.Text.Middle(n,1) = Chr(32) Or Me.Text.Middle(n,1) = EndOfLine or n = 0 Then
      boundLeft = n
      Exit
    End
    
  Next
  For n As Integer = startPosition To tlen
    If Me.Text.Middle(n+1,1) = Chr(32) Or Me.Text.Middle(n+1,1) = EndOfLine Or n = tlen Then
      boundRight = n+1
      Exit
    End
  Next
End

If boundLeft <> -1 And boundRight <> -1 Then
  Var isolatedWord As String = Me.Text.Middle(boundLeft, boundRight - boundLeft)
  Var check As pair = wordInDictionary( isolatedWord, boundleft, boundRight )
  If check  <> Nil Then 
    mboundedWord  = check
    If previousBoundedWord = Nil Then previousBoundedWord = mboundedWord
    setStyleForWord(previousBoundedWord, True)
  Else
    mboundedWord = Nil 
  End If
End If

RaiseEvent mouseMove(X, Y)

This code is in charge of detecting the limits of the word under the cursor and finding if it is one of the active words registered for the class.

Reacting to Active Words

Finally, select the MouseDown Event Handler and write the following code into the associated Code Editor:

If linkedWords <> Nil And boundedWord <> Nil Then
  Var p As New pair(mboundedWord.Left,linkedWords.Value(mboundedWord.Left))
  For Each item As LinkDetectorTextArea.ActionDelegate In observers
    item.Invoke p
  Next
  Return True
End If

Return RaiseEvent mousedown(x,y)

This code will invoke all the registered Observers for the active word detected under the cursor.

Registering the Active Words

We need a way to inform the instances about the active words to be aware of. We could do that from a Constructor or using a Computed Property. In this case, we will use a method for assigning the received Dictionary of Pairs to the Property in charge of storing that information. Select Insert > Method and use this data in the associated Inspector Panel:

  • Name: setDictionary.
  • Parameters: d As Dictionary.
  • Scope: Public.

In the associated Code Editor, put this simple line of code:

linkedWords = d

Registering (and Deregistering) Observers

As we have said, the class will be able to react to the active words, so we need to provide a couple of methods to register and deregister Observers at any time. Use the following information for the first method:

  • Method Name: registerObserver.
  • Parameters: observer as ActionDelegate.
  • Scope: Public.

And write this line of code in the associated Code Editor:

if observer <> nil then observers.add observer

Add a second method that will be in charge of deregistering an Observer. Use the following data for that:

  • Method Name: deleteObserver.
  • Parameters: observer As LinkDetectorTextArea.ActionDelegate.
  • Scope: Public.

This is the code that will search and remove the received ActionDelegate from the instance Array of Observers:

If observer <> Nil Then
  Var n As Integer = observers.IndexOf(observer)
  observers.RemoveAt(n)
end

Regular Expressions to the Rescue … and Styling!

Finally, add a couple of methods to finish the TextArea subclass. These will be Private for the class, auxiliary methods called mainly from the Event Handlers. The first one is the method that does the matching with the received candidate and upon finding a match, will return the corresponding data pair to the caller. To do this, add a new Method using the following data:

  • Method Name: wordInDictionary.
  • Parameters: word As string, leftposition As integer, RightPosition As integer.
  • Return Type: Pair.
  • Scope: Private.

And write the following code in the associated Code Editor:

#Pragma Unused RightPosition

Var theRightPosition As Integer
Var re As New RegEx
re.SearchPattern = "[a-zA-Z]+"
Var rm As RegExMatch
rm = re.Search(word)

If rm <> Nil Then 
  Var foundword As String = rm.SubExpressionString(0)
  Var characterPosition As Integer = word.LeftBytes(rm.SubExpressionStartB(0)).Length
  word = foundword
  leftposition = leftposition + characterPosition
  therightposition = leftposition + word.Length
End If

If linkedWords.HasKey(word) Then Return New pair(word,leftposition.ToString+"-"+ theRightPosition.ToString)

Var blu As Integer
Var dictionaryKeys() as variant = linkedWords.Keys

for each thekey as string in dictionaryKeys  
  If thekey.IndexOf(word) <> -1 Then
    blu = Me.Text.IndexOf(thekey)
    If leftposition >= blu And leftposition <= (blu + thekey.Length) Then
      Var finalposition As Integer =blu+thekey.Length
      Return New pair(thekey,blu.ToText+"-"+finalposition.ToString)
    end if
  end
next

Return nil

Adding Style

Finally, we need to choose how to inform the user that the word below the cursor is an active word so they can click on it. For this example, I’ve used the Underline text style, but you can change that. Add the last method for the class using the following data:

  • Method Name: setStyleForWord
  • Parameters: word As pair, mode As Boolean.
  • Scope: Private.

Use the mode parameter in order to use the method to apply or delete the style. For example, removing the style from the previous detected active word and styling the new one. Write the following code in the associated Code Editor:

Var cStart, cEnd As Double
cStart = word.Right.StringValue.NthField("-",1).Val
cEnd = word.Right.StringValue.NthField("-",2).Val - cStart

Var t As StyledText

t = Me.StyledText

cstart = If( cstart - 1 < 0, 0, cstart)

t.Underline(cstart, cEnd) = mode

Putting it all together!

With the subclass finished it is time to do some Window Layout so we can test the subclass functionality. Select the Window1 item in the Navigator to access the Window Layout Editor and drag the LinkDetectorTextArea subclass from the Navigator onto the Window Layout. Use the Inspector to change its name to myURLField. Then add three Label controls and an HTMLViewer, both from the Library. The HTMLViewer will show the URL associated with the active word, while one of the three Label controls will show the detected active words. Both the URL loading as the displaying of the detected word will be done by a previously registered Observer (a window method). The finished window layout should look something like this:

Where the text under the “Sample Text” label is the one assigned to the Text property of the LinkDetectorTextArea instance.

Now, let’s add to the Window1 object the method that will act as an Observer, select the Add > Method option in combination with the following data:

  • Name: updateContents
  • Parameters: p as pair
  • Scope: Public

And put the following code in the associated Code Editor:

linkedControl.Text = p.Left
HTMLViewer1.LoadURL p.Right

Lastly, add the Open event to the Window1 window, adding the following code to the associated Code Editor. This will initialize the LinkDetectorTextArea instance providing the clickable words and their associated URLs:

myURLField.setDictionary New Dictionary("Xojo":"https://www.xojo.com","iOS":"http://www.apple.com/ios/",_
"macOS":"http://www.apple.com/osx/", "Windows":"https://www.microsoft.com/en-US/windows","Linux":"https://es.wikipedia.org/wiki/GNU/Linux",_
"Raspberry Pi":"https://www.raspberrypi.org/","AprendeXojo":"https://www.aprendexojo.com","Web":"https://www.xojo.com/web")

myURLField.registerObserver WeakAddressOf updateContents

Run the project, move the cursor over the text and you will see the how the active words are underlined on the fly; and if you click on any of them, the Window will display the detected word, while loading the associated URL.

Of course, this can be vastly improved and expanded upon … enjoy!

Paul learned to program in BASIC at age 13 and has programmed in more languages than he remembers, with Xojo being an obvious favorite. When not working on Xojo, you can find him talking about retrocomputing at Goto 10 and on Mastodon @lefebvre@hachyderm.io.