Adventures in TextInputCanvas : 6

Ok so so far we can enter text, press enter & return etc.
But there are several things that are missing.
There is no “insertion point”, no blinking cursor to indicate where insertions are about to happen.
Folks might like that.

But to show one we will need to set up several things ;

  1. a timer to blink the thing at a reasonably reliable rate
  2. we’ll need to track the current position within our text of where we are adding characters
  3. since at this time our text buffer is just one big long string and the insertion position might be just some single number we’ll need a way to turn that “buffer insertion position” in to and x, y position on the canvas to know where to draw our blinking cursor.

And, it turns out - this IS a lot of stuff.
So lets tackle #1 first.
The timer itself is easy enough and one of the good places to use addhandler. We can, entirely in code and entirely within our canvas, create the timer and set up everything we need to be able to let it do its job of just running periodically.
We’ll set most of this up in the Constructor and then enable and disable it as needed elsewhere.
Note THAT we MUST have a NO parameter constructor for this control. Otherwise things wont work correctly when we put it on a Window.

We’ll need a method to handle the action event of the timer

Private Sub blinkTimerAction(instance as Timer)
End Sub

And we’ll add the constructor

Public Sub Constructor()
  // Calling the overridden superclass constructor.
  mBlinkTimer = New Timer
  mBlinkTimer.Mode = Timer.ModeOff
  mBlinkTimer.Period = 1000 
  addHandler mBlinkTimer.Action, addressOf blinkTimerAction
End Sub

I’ve set this up using a hard coded period of one second but you could get the blink time dynamically on each platform and even make it a preference for users as to how fast or slow the cursor should blink.

Make sure you add a private property for the timer as well

mBlinkTimer as Timer

So now we have a timer that we can run to cause the cursor to blink. But we’ll need to track the state of the cursor as well since if toggles through visible and invisible.
So lets add a private property to track that as well

  mCursorVisible as boolean

Now that we have this we can alter the timer handler to change the state every time the timer fires so the cursor would get toggled to be visible, not visible, visible etc. So lets make the blink timer handler read as follows

Private Sub blinkTimerAction(instance as Timer)
  mCursorVisible = Not mCursorVisible
  End Sub

As well we’re going to ad event handlers for “GotFocus” and “LostFocus” since the cursor should probably start blinking any time we get focus, and should stop when we lose focus.
Got focus should read as

Sub GotFocus() Handles GotFocus
  mBlinkTimer.Mode = Timer.ModeMultiple
End Sub

and LostFocus as

Sub LostFocus() Handles LostFocus
  mBlinkTimer.Mode = Timer.ModeOff
End Sub

At this point you wont see anything different. So to see that your timer Is behaving we could just have the timer handler log the state of the cursor as follows :

mCursorVisible = Not mCursorVisible


System.debuglog CurrentMethodName + " cursor visible = " + Str(mCursorVisible)

For now leave the self.invalidate in there as that wont hurt. All it does it tell the OS that the NEXT TIME you draw please redraw the whole TextInputCanvas. We’ll refine that as we go so we don’t redraw the whole area all the time.

OK- all this JUST to get to the point we can, in theory, blink a cursor !

As i said at the outset we’ll need to track the current position within our text of where we are adding characters. For now, because we have not implemented any cursor movement via the key board or by clicking etc we’re ALWAYS inserting at the end. But we dont want that to always be true.

So we need to create the notion of the “insertion position”

When the editor starts up and is empty, the position is 0 - or “before the first character”
Once we type one character it should be 1 - or “before the second”
One more character and the position is “before the third character”
So if our insertion position is always treated as “the position before whatever character” we should be able to manage things pretty well. (note that I’m literally writing this with you as I go and I have a hunch this could be a problem somewhere down the road but part of the adventure in this series is trying to sort out these issues as we go)

For now, to track the “insertion position” I’m going tpo add a private integer property as follows :slight_smile:

Private Property mInsertionPosition as integer

Eventually we could expose this as a computed property to make it possible for code to move the insertion point but for now its a private.

Since we’re now trying to track the insertion position we’ll need to modify a few places where we added text and ends of line.
Our inserttext event handler should read like

Sub InsertText(text as string, range as TextRange) Handles InsertText
  If range Is Nil Then
    System.debuglog currentmethodname + "[" + Text + "] nil range"
    System.debuglog currentmethodname + "[" + Text + "] range (location, length, end) = [" + Str(range.Location) + " ," + Str(range.Length) + ", " + Str(range.EndLocation) + "]"
  End If
  mTextBuffer = mTextBuffer + Text
  mInsertionPosition = mInsertionPosition + Len(Text)
End Sub

And the select case inside DoCommand should read as

Case CmdInsertNewline
  mTextBuffer = mTextBuffer + EndOfLine
  mInsertionPosition = mInsertionPosition + Len(EndOfLine)  
Case CmdInsertNewlineIgnoringFieldEditor
  mTextBuffer = mTextBuffer + EndOfLine
  mInsertionPosition = mInsertionPosition + Len(EndOfLine)
Case CmdInsertLineBreak
  mTextBuffer = mTextBuffer + EndOfLine
  mInsertionPosition = mInsertionPosition + Len(EndOfLine)

If you want you could put in some debuglogs to log the current insertion position as well
One thing I have done in some projects with controls like this is add a private DbgLog method in the control itself. And this method I write as follows so I dont forget to disable this spammy logging for a non-debug build
And the DebugMe constant on this control I can also set to true or false to disable debug logging with this control when needed.

Private Sub DbgLog(msg as string)
  If DebugBuild = False Then
  End If
  If Self.debugMe Then
    System.debuglog msg
  End If
End Sub

And so my debug log messages the can read like

dbglog CurrentMethodName + " insertion position = " + Str(mInsertionPosition)

and they’ll all be disabled in a non-debug run.

Now if you type and, in the IDE , show the messages pane where all the debug log messages go you should see the timer flipping the CursorVisible boolean from true to false and back AND as you type you should also see the insertion point getting incremented.

And now we can actually draw the blinking cursor but - were going to cheat a little
Since we are aways adding and editing at the END the insertion point which we’re now tracking the position of, is always at the end.
We’re going to be a bit lazy here and just draw it that way - but it will blink.

The Paint event can just read as follows

Sub Paint(g as Graphics, areas() as object) Handles Paint
  g.forecolor = &cFF000000
  g.FillRect 0, 0, g.Height, g.width
  // our text buffer MAY have ultiple lines in it so lets 
  // do the blindingly lazy and ReplaceLineEndings 
  // and split on line endsins and just draw
  Dim lines() As String = Split( ReplaceLineEndings(mTextBuffer, EndOfLine), EndOfLine )
  g.ClearRect 0, 0, g.Height, g.width
  // ok now ... draw away !
  g.ForeColor = Self.TextColor
  g.TextFont = Self.TextFont
  g.TextUnit = CType(Self.TextUnit, REALbasic.FontUnits)
  g.TextSize = Self.TextSize
  g.Underline = Self.Underline
  g.Bold = Self.Bold
  g.Italic = Self.Italic
  Dim drawAtX As Double
  Dim drawAtY As Double
  drawAtY = g.TextAscent
  For i As Integer = 0 To lines.ubound
    drawAtX = 0
    g.DrawString lines(i), drawAtX, drawAtY
    drawAtX = g.StringWidth(lines(i))
    // if we're drawing the last line then we probably do not want to advance the Y position 
    If i < lines.Ubound Then
      drawAtY = drawAtY + g.TextHeight
    End If
  // IF the cursor should be visible then draw it - otherwise dont and the clear above will have done the right thing
  If mCursorVisible Then
    g.DrawLine drawAtX, drawAtY - g.TextAscent, drawAtX, drawAtY - g.TextAscent + g.TextHeight
  End If
End Sub

And now if you type you WILL see the cursor blink
As well if you remove focus from the window the cursor should STOP blinking
And when focus returns to the window it will start blinking again IF it has focus OR is the only focusable control on a window.