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 ;
- a timer to blink the thing at a reasonably reliable rate
- we’ll need to track the current position within our text of where we are adding characters
- 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. Super.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 Self.invalidate 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
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" Else 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) me.Invalidate End Sub
And the select case inside DoCommand should read as
Case CmdInsertNewline mTextBuffer = mTextBuffer + EndOfLine mInsertionPosition = mInsertionPosition + Len(EndOfLine) Self.Invalidate Case CmdInsertNewlineIgnoringFieldEditor mTextBuffer = mTextBuffer + EndOfLine mInsertionPosition = mInsertionPosition + Len(EndOfLine) Self.Invalidate Case CmdInsertLineBreak mTextBuffer = mTextBuffer + EndOfLine mInsertionPosition = mInsertionPosition + Len(EndOfLine) Self.Invalidate
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 Return 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 Next // 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.