Skip to content

Tutorial: Active Words

Follow this tutorial to learn how to create active (clickable) words in a text of a TextArea control using the OOP Delegate design pattern, which allows you to dynamically change how your app will react when the user clicks on any of these active words. Best of all, this is cross-platform, so you can use it for macOS, Windows and Linux deployments!

Our active words will be based on the use of Pairs, so one of the pieces of information will be the active word (or words) we want to detect on the text, and the other piece of information for the Pair will be any associated data you want…this is always a String. For example, the associated data could be a link or text you want to show in another control when the user clicks on the word.

For the sake of this tutorial, the class created has some limitations. For example, you can’t repeat the same word (or same combination of words) assigning them to different actions. If you do that, the class will alway react to the first occurrence found; if you want your code to always react the same to the same active word or words…then that’s ok. Anyway, this tutorial serves as a good starting point so you can modify and improve it to meet your own needs.

You can download the class with the example project from here.

TextArea subclass

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

  • Open. Just to assign the Pointer cursor, more appropriate for the kind of functionality this subclass will provide.
  • MouseMove. Here is where we will detect if there is an active word (or combination of words) found under the cursor pointer.
  • MouseDown. This is the Event Handler that will call all the registered Observers when the user clicks on an active word, passing along the detected word and the associated information.

As we want to make these events also available for any instance created from our subclass, then 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.

Next we will need to add to the class some Private properties used by the added Event Handlers and other methods yet to define. These properties will be set 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 our own 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. Array containing all the registered observers of the type ActionDelegate yet to define.

As we have seen, we need to define a Delegate type for our 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. We simply use this Event for assigning the Arrow cursor type that is more appropriate for the kind of functionality offered:

Me.MouseCursor = System.cursors.standardpointer
RaiseEvent open

Matching the Active Words

Next, select the MouseMove Event Handler and type the following code in the corresponding Code Editor. 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:

Dim length As Integer = Me.Text.Len
if previousBoundedWord <> nil then
  setStyleForWord(previousBoundedWord, false)
  previousBoundedWord = nil
end if
dim boundLeft, boundRight as integer

Dim startPosition As Integer = Me.CharPosAtXY(x,y)

If CharPosAtxy(x,y) >= length Then
  if boundedWord <> nil then setStyleForWord( boundedWord, false )
  mboundedWord = nil
elseif me.text.mid(startPosition,1) <> chr(32) and me.Text.mid(startPosition,1) <> EndOfLine then
  for n as integer = startPosition DownTo 1
    if me.Text.mid(n-1,1) = chr(32) or me.Text.mid(n-1,1) = EndOfLine then
      boundLeft = n
      exit
    end
  next
  for n as integer = startPosition to length
    if me.Text.mid(n+1,1) = chr(32) or me.Text.mid(n+1,1) = EndOfLine or n = length then
      boundRight = n+1
      exit
    end
  next
end
dim isolatedWord as string = me.Text.mid(boundLeft, boundRight - boundLeft)
dim 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
RaiseEvent mouseMove(X, Y)

Reacting to Active Words

Finally, select the MouseDown Event Handler and write the following code into the associated Code Editor. This code will invoke all the registered observers for the active word detected under the cursor pointer (if any):

If linkedWords <> Nil And boundedWord <> Nil Then
  Dim 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)

Registering the Active Words

We need a way to inform the instances about the active words it has to be aware of. We could do that from a Constructor or using a Computed Property instead of a regular one. In this case, we will use a method for assigning the received Dictionary of Pairs to the Property in charge of storing that information. So, 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 we just need to put this simple line of code:

linkedWords = d

Registering (and deregistering) Observers

As we have said, our 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.Append 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
  observers.Remove( observers.IndexOf( observer ) )
end

Regular Expressions to the rescue… and Styling!

Finally we just need to add a couple of methods more for finishing our TextArea subclass. These will be Private for the class, auxiliary methods called mainly from the Event Handlers. The first one will be the method in charge of doing the matching from the received candidate, and if it finds it, then it will return the corresponding pair of data to the caller. For this one, 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:

Dim theRightPosition As Integer
dim re as new RegEx
re.SearchPattern = "[a-zA-Z]+"
dim rm as RegExMatch
rm = re.Search(word)
if rm <> nil then
  dim foundword as string = rm.SubExpressionString(0)
  dim characterPosition as integer = word.LeftB(rm.SubExpressionStartB(0)).Len
  word = foundword
  leftposition = leftposition + characterPosition
  therightposition = leftposition + word.Len
end if
if linkedWords.HasKey(word) then Return new pair(word,leftposition.ToText+"-"+ theRightPosition.ToText)
dim blu as integer
dim dictionaryKeys() as variant = linkedWords.Keys
for each thekey as string in dictionaryKeys
  if thekey.InStr(word) > 0 then
    blu = me.text.instr(thekey)
    if leftposition >= blu and leftposition <= (blu + thekey.Len) then
      dim finalposition as integer =blu+thekey.len
      Return new pair(thekey,blu.ToText+"-"+finalposition.totext)
    end if
  end
next
Return nil

Adding some style

Finally, we need a way to inform the user that the word down the pointer cursor is active so she can click on it. For this example I’ve used the Underline text style, but you can adapt it once you see how this is done. So, let’s add the last method for the class using the following data:

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

We use the mode parameter so we can use the method to apply or delete the style to the received word; removing for example the style from the previous detected active word and styiling the new one. Write the following code in the associated Code Editor:

Dim cStart, cEnd, sStart As Double
cStart = NthField(word.Right,"-",1).Val
cEnd = NthField(word.Right,"-",2).Val - cStart
Dim t As StyledText
t = Me.StyledText
cstart = If( cstart - 1 < 0, 0, cstart-1)
t.Underline(cstart, cEnd) = mode

Putting it all together!

With our 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 in order to create a new instance. Use the Inspector to change its name to myURLField. Then add three Label controls from the Library and an HTMLViewer. 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 be something like this:

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

Let’s add now to the Window1 object the method that will act as an observer, so 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 now the Open event to the Window1 window, adding the following code to the associated Code Editor. Here is where we will initialize our LinkDetectorTextArea instance providing the clickable words and their associated URL:

myURLField.setDictionary New Dictionary("Xojo":"http://www.xojo.com","Web":"https://www.w3.org","iOS":"http://www.apple.com/ios/",_"OS X":"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":"http://www.aprendexojo.com")
myURLField.registerObserver WeakAddressOf updateContents

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

Of course, this can be vastly improved…and that is something that maybe you can do in any number of ways!

Javier Rodri­guez has been the Xojo Spanish Evangelist since 2008, he’s also a Developer, Consultant and Trainer who has be using Xojo since 1998. He manages AprendeXojo.com and is the developer behind the GuancheMOS plug-in for Xojo Developers, Markdown Parser for Xojo, HTMLColorizer for Xojo and the Snippery app, among others

*Read this post in Spanish