Hit Testing
In code, a point returned by a mouse click is passed to the
TextLayout.hitTestChar method, which returns a
TextHitInfo object that represents the character and side of the
character where the end user clicked. If the end user clicks on the o,
the hit is to position 5.
However, in the source text, position 5 is before the H, so
the TextLayout object checks the TextHitInfo
object to find out what side of the character the hit is on and displays
the dual carets if the hit is on the side of the o towards
the Hebrew. If the hit is on the side of the o towards the
l, a single caret is drawn between the l and the
o to indicate that the end user will insert English text
at that location.
The source text is initialized the way the words are spoken, and
not the way they are printed. The source text looks like this:
"\u05D0\u05E0\u05D9 Hello
\u05DC\u05D0 \u05DE\u05D1\u05D9\ u05DF " +
"\u05E2\u05D1\u05E8\u05D9\u05EA Arabic
\u0644\u0645\u062C\ u0645\u0648\u0639\u0629", map);
The first three unicode characters and the space \u05D0\u05E0\u05D9
define what you see on the display starting on the right side up to the o.
You can see that the H is next in the source code, but the o is
next on the display. This is because in the right-to-left run, the English
word Hello is turned around to display correctly in an embedded
left-to-right run.
Determining the location of the insertion point is taken care of
by the TextLayout object. All you need to do in your
code is specify the caret colors, get the mouse click location, return
a TextHitInfo object, and draw the layout and carets.
Here is the code to initialize the colors for the strong
and weak carets:
private static final Color STRONG_CARET_COLOR = Color.red;
private static final Color WEAK_CARET_COLOR = Color.black;
Here is the code that draws the TextLayout and
the carets. The insertionPoint variable is
retrieved from a TextHitInfo object in the
HitTestMouseListener method.
If the insertion point is not between text running in different
directions, only the strong caret draws.
// Draw textLayout.
textLayout.draw(graphics2D, 0, 0);
// Retrieve caret Shapes for insertionIndex.
Shape[] carets = textLayout.getCaretShapes(insertionIndex);
// Draw the carets. carets[0] is the strong caret, and
// is never null. carets[1], if it is not null, is the
// weak caret.
graphics2D.setColor(STRONG_CARET_COLOR);
graphics2D.draw(carets[0]);
if (carets[1] != null) {
graphics2D.setColor(WEAK_CARET_COLOR);
graphics2D.draw(carets[1]);
}
Here is the HitTestMouseListener method:
private class HitTestMouseListener extends MouseAdapter {
/**
* Compute the character position of the mouse click.
*/
public void mouseClicked(MouseEvent e) {
Point2D origin = computeLayoutOrigin();
// Compute the mouse click location relative to
// textLayout's origin.
float clickX = (float) (e.getX() - origin.getX());
float clickY = (float) (e.getY() - origin.getY());
// Get the character position of the mouse click.
TextHitInfo currentHit = textLayout.hitTestChar(clickX,
clickY);
insertionIndex = currentHit.getInsertionIndex();
// Repaint the Component so the new caret(s)
// will be displayed.
repaint();
}
}
And here is the complete
Selection Highlighting
The next figure shows how a
contiguous range of characters in the source text (on the top)
might not map to a contiguous highlight region (on the bottom)
on screen if the selection range includes left-to-right and right-to-left
characters. When the Arabic text is turned around to run right-to-left on
the display, the selected portion of the Arabic text is not contiguous
with the is and space before it, althout these characters
are contiguous in the source text.
The next figure shows how a contiguous highlight region on the
display (on the bottom) might not map to a single contiguous range
of characters in the source text (on the top).
This point is illustrated in the next figure.
A TextLayout object provides two strategies for selection
highlighting to handle these two situations: logical highlighting and visual
highlighting.
With logical highlighting, the selected characters are always contiguous
in the source text, and the highlight region is allowed to be discontiguous
on the display. With visual highlighting, there might be more than one
range of selected characters, but the highlight region is always contiguous.
Logical highlighting is simpler for programmers to use because the selected
characters are always contiguous in the source. The
TextLayout.getLogicalHighlightShape method takes two insertion
offsets and returns a Shape that represents the highlight
region marked by the two offsets. A recommended way to show highlighting
is to fill the Shape with the highlight color, and then draw
the TextLayout over the Shape.
Here is the code to get and draw the selection range:
// Retrieve highlight region for selection range.
Shape highlight = textLayout.getLogicalHighlightShape(anchorEnd,
activeEnd);
// Fill the highlight region with the highlight color.
graphics2D.setColor(HIGHLIGHT_COLOR);
graphics2D.fill(highlight);
Here is the complete
Moving the Caret
In bidirectional text, the cursor should move smoothly through
the text on the display in the direction that corresponds to
the direction of the Arrow key being pressed. The problem, is
that right-to-left text
is positioned in the source text in the direction it is spoken
and not in the direction it is displayed. For a caret to
have a smooth journey across the display, the character offset
does not move smoothly through the source text. This point is
illustrated by the figure.
Progressing through the three screen positions shown on the bottom
of the figure from left-to-right corresponds to progressing through
the character offsets in the source text in the order of 7, 19, and 18.
The TextLayout object handles the details for you. All
your code needs to do is update the insertion index in response to
an arrow key press:
private class ArrowKeyListener extends KeyAdapter {
/**
* Update the insertion index in response to an arrow key.
*/
private void handleArrowKey(boolean rightArrow) {
TextHitInfo newPosition;
if (rightArrow) {
newPosition = textLayout.getNextRightHit(insertionIndex);
}
else {
newPosition = textLayout.getNextLeftHit(insertionIndex);
}
// getNextRightHit() / getNextLeftHit() will return null if
// there is not a caret position to the right (left) of the
// current position.
if (newPosition != null) {
// Update insertionIndex.
insertionIndex = newPosition.getInsertionIndex();
// Repaint the Component so the new caret(s)
// will be displayed.
repaint();
}
}
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_LEFT ||
keyCode == KeyEvent.VK_RIGHT) {
handleArrowKey(keyCode == KeyEvent.VK_RIGHT);
}
}
}
Here is the complete