Adventures in TextInputCanvas : 10

Sorry for the delay

At this point we have basic implementations for most useful things
There are still a number of items that we havent touched
BaseLineAtIndex, CharacterAtPoint, DiscardIncompleteText, KeyFallsThrough, SetIncompleteText, TextForRange, and even TextLength
Two others have minimal implementations - SelectedRange and IncompleteTextRange

And yet most things seem to work ?
So what are all these extras for ?
If you open the IDE and double click a word in the text then right click you should see the “Services” option at the bottom of the contextual menu on a Mac.
Some of these are involved in making it so those services can work.

But right now since we dont have a way to “select” anything we cant really make those work.

So thats what we’ll work on now.
On macOS its normal to implement click, double click and triple click.
A single click might simply move the cursor. A double click would select the word under the mouse, and a triple click might select the line under the cursor.

In order to track what kind of click it is we need to keep track of the X and Y position of a click and the duration since the last click.
So we’re going to add

  1. an enum for “clicktypes” - with 4 members “none”, “single”, “double”, “triple”
  2. a property to track the "click type (mClickType as ClickTypes)
  3. properties for lastClickX and Y (mLastClickX mLastClickY as double)
  4. a property for the last click time (mLastClickTime as double)

With that we can track clicks with some code like in the MouseDown event

// double and triple clicks are both TIME & SPACE
// if you click move a long way click this should not be a double or triple click

// triple click ?
If (mClickType = ClickTypes.Double) _
  And (Ticks - mLastClickTime < DoubleClickInterval) _
  And ( Abs(x - mLastClickX) < 5) _
  And ( Abs(y - mLastClickY) < 5)  Then
  
  mClickType  = ClickTypes.triple
  dbglog currentmethodname + " triple click"
  // double click ?
Elseif (mClickType = ClickTypes.Single) _
  And (Ticks - mLastClickTime < DoubleClickInterval) _
  And ( Abs(x - mLastClickX) < 5) _
  And ( Abs(y - mLastClickY) < 5)  Then
  
  mClickType = ClickTypes.Double
  dbglog currentmethodname + " double click"
Else
  mClickType = ClickTypes.Single
  dbglog currentmethodname + " single click"
End If

mLastClickX = x
mLastClickY = y

mLastClickTime = Ticks

Note that if you quadruple click that the click type will run through single, double, triple then back to single click. And it will continue to cycle through like that as you click rapidly.

And now that we are able to figure out what kind of click it is we can start making the code do selections when a person clicks

So lets start with the single click
First we’ll need to take the X, Y and turn that into a Line & column number, and move the cursor to that position.
So we’ll need a couple more helper methods - one that can take an X Y and return a line & column
And then one that can take a line and column and return the linear index position in our buffer

The method to convert from X Y to line & column will need a graphics surface to measure things using the SAME mechanism used during a paint event (otherwise things get out of what really quickly)

We already have this in one method so we should probably lift that into its own method we can just reuse. Lines 9 - 16 of RectForRange can be converted into a method named GetMeasuringPicture that returns a suitable picture set up the way we need.
So in RectForRange lines 8 - 11 now look like

// get the position as a line & column #
pos = PositionToLineAndColumn( range.Location )

Dim p As picture = GetMeasuringPicture

// get the x (left) edge and (y) baseline for the position
Dim position As REAlbasic.point = LineColumnToXY(p.Graphics, pos.X, pos.Y)

The method we just created, GetMeasuringPicture, will need a
return p
as its last line

And now in MouseDown lines 28 and on can be


mLastClickTime = Ticks

Dim p As Picture = GetMeasuringPicture

Dim line, col As Integer

XYToLineColumn(p.Graphics, x, y, line, col)

Select Case mClickType
  
Case ClickTypes.Single
  mInsertionPosition = LineColumnToPosition( line, col )
End Select

XY to LineColumn can simply break things into lines and then determine which Y position encompasses the line of text. And then we can measure that line of text in increasing pieces to see where the X position fell and we’ll know the line & column number that is where the click point was.

Protected Sub XYToLineColumn(g as Graphics, x as double, y as double, byref lineNumber as integer, byref column as Integer)
  Dim xPos As Double
  Dim yPos As Double
  
  // NOTE this is NOT EFFICIENT !!!!!!
  // since we split things into lines in PAINT and again here
  // if we really need lines we should figure out how to do this as few times as possible
  
  Dim lines() As String = Split( ReplaceLineEndings(mTextBuffer, EndOfLine), EndOfLine )
  
  Dim lineTopY As Double
  
  lineTopY = 0
  lineNumber = 0
  
  // so a person may not click rih on the baseline ans we need to see if the 
  // click is anywhere from the top of the line to the bottom
  While lineTopY < Y And lineTopY + g.TextHeight <= Y
    lineNumber = lineNumber + 1
    
    lineTopY = lineTopY + g.TextHeight
  Wend
  
  If lineNumber >= 0 And lineNumber <= lines.ubound Then
    // ok wo what column does this X represent ?
    
    column = 0
    Dim lineX As Double = 0
    Dim lineSeg As String = lines(lineNumber).Left(column)
    While column <= lines(lineNumber).Len and g.StringWidth( lineseg ) < x 
      column = column + 1
      lineSeg = lines(lineNumber).Left(column)
    Wend
    // column has been incremented once too many times
    column = column - 1
    
  End If
    
End Sub

And to know where to position the insertion point on a single click we need to convert that line and column in to a linear position in our internal buffer. And that too is reasonably straightforward and looks like

Protected Function LineColumnToPosition(line as integer, column as integer) as integer
  // NOTE this is NOT EFFICIENT !!!!!!
  // since we split things into lines in PAINT and again here
  // if we really need lines we should figure out how to do this as few times as possible
  
  Dim lines() As String = Split( ReplaceLineEndings(mTextBuffer, EndOfLine), EndOfLine )
  
  Dim tmpPosition As Integer
  
  // count up the lengths of whole lines
  For i As Integer = 0 To line - 1
    tmpPosition = tmpPosition + lines(i).Len
  Next
  
  // plus hte last line we add in just the columns since it may not be the wbole line
  tmpPosition = tmpPosition + column
  
  Return tmpPosition
  
End Function

And now when you single click the insertion position should move to where you clicked

5 Likes