7045593: Possible Regression : JTextfield cursor placement behavior algorithm has changed.
Reviewed-by: peterz
/*
* Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javax.swing.text;
import java.lang.reflect.Method;
import java.awt.Component;
import java.awt.Rectangle;
import java.awt.Graphics;
import java.awt.FontMetrics;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.font.TextAttribute;
import java.text.*;
import javax.swing.JComponent;
import javax.swing.SwingConstants;
import javax.swing.text.ParagraphView.Row;
import sun.swing.SwingUtilities2;
/**
* A collection of methods to deal with various text
* related activities.
*
* @author Timothy Prinzing
*/
public class Utilities {
/**
* If <code>view</code>'s container is a <code>JComponent</code> it
* is returned, after casting.
*/
static JComponent getJComponent(View view) {
if (view != null) {
Component component = view.getContainer();
if (component instanceof JComponent) {
return (JComponent)component;
}
}
return null;
}
/**
* Draws the given text, expanding any tabs that are contained
* using the given tab expansion technique. This particular
* implementation renders in a 1.1 style coordinate system
* where ints are used and 72dpi is assumed.
*
* @param s the source of the text
* @param x the X origin >= 0
* @param y the Y origin >= 0
* @param g the graphics context
* @param e how to expand the tabs. If this value is null,
* tabs will be expanded as a space character.
* @param startOffset starting offset of the text in the document >= 0
* @return the X location at the end of the rendered text
*/
public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
TabExpander e, int startOffset) {
return drawTabbedText(null, s, x, y, g, e, startOffset);
}
/**
* Draws the given text, expanding any tabs that are contained
* using the given tab expansion technique. This particular
* implementation renders in a 1.1 style coordinate system
* where ints are used and 72dpi is assumed.
*
* @param view View requesting rendering, may be null.
* @param s the source of the text
* @param x the X origin >= 0
* @param y the Y origin >= 0
* @param g the graphics context
* @param e how to expand the tabs. If this value is null,
* tabs will be expanded as a space character.
* @param startOffset starting offset of the text in the document >= 0
* @return the X location at the end of the rendered text
*/
static final int drawTabbedText(View view,
Segment s, int x, int y, Graphics g,
TabExpander e, int startOffset) {
return drawTabbedText(view, s, x, y, g, e, startOffset, null);
}
// In addition to the previous method it can extend spaces for
// justification.
//
// all params are the same as in the preious method except the last
// one:
// @param justificationData justificationData for the row.
// if null not justification is needed
static final int drawTabbedText(View view,
Segment s, int x, int y, Graphics g,
TabExpander e, int startOffset,
int [] justificationData) {
JComponent component = getJComponent(view);
FontMetrics metrics = SwingUtilities2.getFontMetrics(component, g);
int nextX = x;
char[] txt = s.array;
int txtOffset = s.offset;
int flushLen = 0;
int flushIndex = s.offset;
int spaceAddon = 0;
int spaceAddonLeftoverEnd = -1;
int startJustifiableContent = 0;
int endJustifiableContent = 0;
if (justificationData != null) {
int offset = - startOffset + txtOffset;
View parent = null;
if (view != null
&& (parent = view.getParent()) != null) {
offset += parent.getStartOffset();
}
spaceAddon =
justificationData[Row.SPACE_ADDON];
spaceAddonLeftoverEnd =
justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
startJustifiableContent =
justificationData[Row.START_JUSTIFIABLE] + offset;
endJustifiableContent =
justificationData[Row.END_JUSTIFIABLE] + offset;
}
int n = s.offset + s.count;
for (int i = txtOffset; i < n; i++) {
if (txt[i] == '\t'
|| ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
&& (txt[i] == ' ')
&& startJustifiableContent <= i
&& i <= endJustifiableContent
)) {
if (flushLen > 0) {
nextX = SwingUtilities2.drawChars(component, g, txt,
flushIndex, flushLen, x, y);
flushLen = 0;
}
flushIndex = i + 1;
if (txt[i] == '\t') {
if (e != null) {
nextX = (int) e.nextTabStop((float) nextX, startOffset + i - txtOffset);
} else {
nextX += metrics.charWidth(' ');
}
} else if (txt[i] == ' ') {
nextX += metrics.charWidth(' ') + spaceAddon;
if (i <= spaceAddonLeftoverEnd) {
nextX++;
}
}
x = nextX;
} else if ((txt[i] == '\n') || (txt[i] == '\r')) {
if (flushLen > 0) {
nextX = SwingUtilities2.drawChars(component, g, txt,
flushIndex, flushLen, x, y);
flushLen = 0;
}
flushIndex = i + 1;
x = nextX;
} else {
flushLen += 1;
}
}
if (flushLen > 0) {
nextX = SwingUtilities2.drawChars(component, g,txt, flushIndex,
flushLen, x, y);
}
return nextX;
}
/**
* Determines the width of the given segment of text taking tabs
* into consideration. This is implemented in a 1.1 style coordinate
* system where ints are used and 72dpi is assumed.
*
* @param s the source of the text
* @param metrics the font metrics to use for the calculation
* @param x the X origin >= 0
* @param e how to expand the tabs. If this value is null,
* tabs will be expanded as a space character.
* @param startOffset starting offset of the text in the document >= 0
* @return the width of the text
*/
public static final int getTabbedTextWidth(Segment s, FontMetrics metrics, int x,
TabExpander e, int startOffset) {
return getTabbedTextWidth(null, s, metrics, x, e, startOffset, null);
}
// In addition to the previous method it can extend spaces for
// justification.
//
// all params are the same as in the preious method except the last
// one:
// @param justificationData justificationData for the row.
// if null not justification is needed
static final int getTabbedTextWidth(View view, Segment s, FontMetrics metrics, int x,
TabExpander e, int startOffset,
int[] justificationData) {
int nextX = x;
char[] txt = s.array;
int txtOffset = s.offset;
int n = s.offset + s.count;
int charCount = 0;
int spaceAddon = 0;
int spaceAddonLeftoverEnd = -1;
int startJustifiableContent = 0;
int endJustifiableContent = 0;
if (justificationData != null) {
int offset = - startOffset + txtOffset;
View parent = null;
if (view != null
&& (parent = view.getParent()) != null) {
offset += parent.getStartOffset();
}
spaceAddon =
justificationData[Row.SPACE_ADDON];
spaceAddonLeftoverEnd =
justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
startJustifiableContent =
justificationData[Row.START_JUSTIFIABLE] + offset;
endJustifiableContent =
justificationData[Row.END_JUSTIFIABLE] + offset;
}
for (int i = txtOffset; i < n; i++) {
if (txt[i] == '\t'
|| ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
&& (txt[i] == ' ')
&& startJustifiableContent <= i
&& i <= endJustifiableContent
)) {
nextX += metrics.charsWidth(txt, i-charCount, charCount);
charCount = 0;
if (txt[i] == '\t') {
if (e != null) {
nextX = (int) e.nextTabStop((float) nextX,
startOffset + i - txtOffset);
} else {
nextX += metrics.charWidth(' ');
}
} else if (txt[i] == ' ') {
nextX += metrics.charWidth(' ') + spaceAddon;
if (i <= spaceAddonLeftoverEnd) {
nextX++;
}
}
} else if(txt[i] == '\n') {
// Ignore newlines, they take up space and we shouldn't be
// counting them.
nextX += metrics.charsWidth(txt, i - charCount, charCount);
charCount = 0;
} else {
charCount++;
}
}
nextX += metrics.charsWidth(txt, n - charCount, charCount);
return nextX - x;
}
/**
* Determines the relative offset into the given text that
* best represents the given span in the view coordinate
* system. This is implemented in a 1.1 style coordinate
* system where ints are used and 72dpi is assumed.
*
* @param s the source of the text
* @param metrics the font metrics to use for the calculation
* @param x0 the starting view location representing the start
* of the given text >= 0.
* @param x the target view location to translate to an
* offset into the text >= 0.
* @param e how to expand the tabs. If this value is null,
* tabs will be expanded as a space character.
* @param startOffset starting offset of the text in the document >= 0
* @return the offset into the text >= 0
*/
public static final int getTabbedTextOffset(Segment s, FontMetrics metrics,
int x0, int x, TabExpander e,
int startOffset) {
return getTabbedTextOffset(s, metrics, x0, x, e, startOffset, true);
}
static final int getTabbedTextOffset(View view, Segment s, FontMetrics metrics,
int x0, int x, TabExpander e,
int startOffset,
int[] justificationData) {
return getTabbedTextOffset(view, s, metrics, x0, x, e, startOffset, true,
justificationData);
}
public static final int getTabbedTextOffset(Segment s,
FontMetrics metrics,
int x0, int x, TabExpander e,
int startOffset,
boolean round) {
return getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, round, null);
}
// In addition to the previous method it can extend spaces for
// justification.
//
// all params are the same as in the preious method except the last
// one:
// @param justificationData justificationData for the row.
// if null not justification is needed
static final int getTabbedTextOffset(View view,
Segment s,
FontMetrics metrics,
int x0, int x, TabExpander e,
int startOffset,
boolean round,
int[] justificationData) {
if (x0 >= x) {
// x before x0, return.
return 0;
}
int nextX = x0;
// s may be a shared segment, so it is copied prior to calling
// the tab expander
char[] txt = s.array;
int txtOffset = s.offset;
int txtCount = s.count;
int spaceAddon = 0 ;
int spaceAddonLeftoverEnd = -1;
int startJustifiableContent = 0 ;
int endJustifiableContent = 0;
if (justificationData != null) {
int offset = - startOffset + txtOffset;
View parent = null;
if (view != null
&& (parent = view.getParent()) != null) {
offset += parent.getStartOffset();
}
spaceAddon =
justificationData[Row.SPACE_ADDON];
spaceAddonLeftoverEnd =
justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
startJustifiableContent =
justificationData[Row.START_JUSTIFIABLE] + offset;
endJustifiableContent =
justificationData[Row.END_JUSTIFIABLE] + offset;
}
int n = s.offset + s.count;
for (int i = s.offset; i < n; i++) {
if (txt[i] == '\t'
|| ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
&& (txt[i] == ' ')
&& startJustifiableContent <= i
&& i <= endJustifiableContent
)){
if (txt[i] == '\t') {
if (e != null) {
nextX = (int) e.nextTabStop((float) nextX,
startOffset + i - txtOffset);
} else {
nextX += metrics.charWidth(' ');
}
} else if (txt[i] == ' ') {
nextX += metrics.charWidth(' ') + spaceAddon;
if (i <= spaceAddonLeftoverEnd) {
nextX++;
}
}
} else {
nextX += metrics.charWidth(txt[i]);
}
if (x < nextX) {
// found the hit position... return the appropriate side
int offset;
// the length of the string measured as a whole may differ from
// the sum of individual character lengths, for example if
// fractional metrics are enabled; and we must guard from this.
if (round) {
offset = i + 1 - txtOffset;
int width = metrics.charsWidth(txt, txtOffset, offset);
int span = x - x0;
if (span < width) {
while (offset > 0) {
int nextWidth = offset > 1 ? metrics.charsWidth(txt, txtOffset, offset - 1) : 0;
if (span >= nextWidth) {
if (span - nextWidth < width - span) {
offset--;
}
break;
}
width = nextWidth;
offset--;
}
}
} else {
offset = i - txtOffset;
while (offset > 0 && metrics.charsWidth(txt, txtOffset, offset) > (x - x0)) {
offset--;
}
}
return offset;
}
}
// didn't find, return end offset
return txtCount;
}
/**
* Determine where to break the given text to fit
* within the given span. This tries to find a word boundary.
* @param s the source of the text
* @param metrics the font metrics to use for the calculation
* @param x0 the starting view location representing the start
* of the given text.
* @param x the target view location to translate to an
* offset into the text.
* @param e how to expand the tabs. If this value is null,
* tabs will be expanded as a space character.
* @param startOffset starting offset in the document of the text
* @return the offset into the given text
*/
public static final int getBreakLocation(Segment s, FontMetrics metrics,
int x0, int x, TabExpander e,
int startOffset) {
char[] txt = s.array;
int txtOffset = s.offset;
int txtCount = s.count;
int index = Utilities.getTabbedTextOffset(s, metrics, x0, x,
e, startOffset, false);
if (index >= txtCount - 1) {
return txtCount;
}
for (int i = txtOffset + index; i >= txtOffset; i--) {
char ch = txt[i];
if (ch < 256) {
// break on whitespace
if (Character.isWhitespace(ch)) {
index = i - txtOffset + 1;
break;
}
} else {
// a multibyte char found; use BreakIterator to find line break
BreakIterator bit = BreakIterator.getLineInstance();
bit.setText(s);
int breakPos = bit.preceding(i + 1);
if (breakPos > txtOffset) {
index = breakPos - txtOffset;
}
break;
}
}
return index;
}
/**
* Determines the starting row model position of the row that contains
* the specified model position. The component given must have a
* size to compute the result. If the component doesn't have a size
* a value of -1 will be returned.
*
* @param c the editor
* @param offs the offset in the document >= 0
* @return the position >= 0 if the request can be computed, otherwise
* a value of -1 will be returned.
* @exception BadLocationException if the offset is out of range
*/
public static final int getRowStart(JTextComponent c, int offs) throws BadLocationException {
Rectangle r = c.modelToView(offs);
if (r == null) {
return -1;
}
int lastOffs = offs;
int y = r.y;
while ((r != null) && (y == r.y)) {
// Skip invisible elements
if(r.height !=0) {
offs = lastOffs;
}
lastOffs -= 1;
r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
}
return offs;
}
/**
* Determines the ending row model position of the row that contains
* the specified model position. The component given must have a
* size to compute the result. If the component doesn't have a size
* a value of -1 will be returned.
*
* @param c the editor
* @param offs the offset in the document >= 0
* @return the position >= 0 if the request can be computed, otherwise
* a value of -1 will be returned.
* @exception BadLocationException if the offset is out of range
*/
public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException {
Rectangle r = c.modelToView(offs);
if (r == null) {
return -1;
}
int n = c.getDocument().getLength();
int lastOffs = offs;
int y = r.y;
while ((r != null) && (y == r.y)) {
// Skip invisible elements
if (r.height !=0) {
offs = lastOffs;
}
lastOffs += 1;
r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
}
return offs;
}
/**
* Determines the position in the model that is closest to the given
* view location in the row above. The component given must have a
* size to compute the result. If the component doesn't have a size
* a value of -1 will be returned.
*
* @param c the editor
* @param offs the offset in the document >= 0
* @param x the X coordinate >= 0
* @return the position >= 0 if the request can be computed, otherwise
* a value of -1 will be returned.
* @exception BadLocationException if the offset is out of range
*/
public static final int getPositionAbove(JTextComponent c, int offs, int x) throws BadLocationException {
int lastOffs = getRowStart(c, offs) - 1;
if (lastOffs < 0) {
return -1;
}
int bestSpan = Integer.MAX_VALUE;
int y = 0;
Rectangle r = null;
if (lastOffs >= 0) {
r = c.modelToView(lastOffs);
y = r.y;
}
while ((r != null) && (y == r.y)) {
int span = Math.abs(r.x - x);
if (span < bestSpan) {
offs = lastOffs;
bestSpan = span;
}
lastOffs -= 1;
r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
}
return offs;
}
/**
* Determines the position in the model that is closest to the given
* view location in the row below. The component given must have a
* size to compute the result. If the component doesn't have a size
* a value of -1 will be returned.
*
* @param c the editor
* @param offs the offset in the document >= 0
* @param x the X coordinate >= 0
* @return the position >= 0 if the request can be computed, otherwise
* a value of -1 will be returned.
* @exception BadLocationException if the offset is out of range
*/
public static final int getPositionBelow(JTextComponent c, int offs, int x) throws BadLocationException {
int lastOffs = getRowEnd(c, offs) + 1;
if (lastOffs <= 0) {
return -1;
}
int bestSpan = Integer.MAX_VALUE;
int n = c.getDocument().getLength();
int y = 0;
Rectangle r = null;
if (lastOffs <= n) {
r = c.modelToView(lastOffs);
y = r.y;
}
while ((r != null) && (y == r.y)) {
int span = Math.abs(x - r.x);
if (span < bestSpan) {
offs = lastOffs;
bestSpan = span;
}
lastOffs += 1;
r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
}
return offs;
}
/**
* Determines the start of a word for the given model location.
* Uses BreakIterator.getWordInstance() to actually get the words.
*
* @param c the editor
* @param offs the offset in the document >= 0
* @return the location in the model of the word start >= 0
* @exception BadLocationException if the offset is out of range
*/
public static final int getWordStart(JTextComponent c, int offs) throws BadLocationException {
Document doc = c.getDocument();
Element line = getParagraphElement(c, offs);
if (line == null) {
throw new BadLocationException("No word at " + offs, offs);
}
int lineStart = line.getStartOffset();
int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
Segment seg = SegmentCache.getSharedSegment();
doc.getText(lineStart, lineEnd - lineStart, seg);
if(seg.count > 0) {
BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
words.setText(seg);
int wordPosition = seg.offset + offs - lineStart;
if(wordPosition >= words.last()) {
wordPosition = words.last() - 1;
}
words.following(wordPosition);
offs = lineStart + words.previous() - seg.offset;
}
SegmentCache.releaseSharedSegment(seg);
return offs;
}
/**
* Determines the end of a word for the given location.
* Uses BreakIterator.getWordInstance() to actually get the words.
*
* @param c the editor
* @param offs the offset in the document >= 0
* @return the location in the model of the word end >= 0
* @exception BadLocationException if the offset is out of range
*/
public static final int getWordEnd(JTextComponent c, int offs) throws BadLocationException {
Document doc = c.getDocument();
Element line = getParagraphElement(c, offs);
if (line == null) {
throw new BadLocationException("No word at " + offs, offs);
}
int lineStart = line.getStartOffset();
int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
Segment seg = SegmentCache.getSharedSegment();
doc.getText(lineStart, lineEnd - lineStart, seg);
if(seg.count > 0) {
BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
words.setText(seg);
int wordPosition = offs - lineStart + seg.offset;
if(wordPosition >= words.last()) {
wordPosition = words.last() - 1;
}
offs = lineStart + words.following(wordPosition) - seg.offset;
}
SegmentCache.releaseSharedSegment(seg);
return offs;
}
/**
* Determines the start of the next word for the given location.
* Uses BreakIterator.getWordInstance() to actually get the words.
*
* @param c the editor
* @param offs the offset in the document >= 0
* @return the location in the model of the word start >= 0
* @exception BadLocationException if the offset is out of range
*/
public static final int getNextWord(JTextComponent c, int offs) throws BadLocationException {
int nextWord;
Element line = getParagraphElement(c, offs);
for (nextWord = getNextWordInParagraph(c, line, offs, false);
nextWord == BreakIterator.DONE;
nextWord = getNextWordInParagraph(c, line, offs, true)) {
// didn't find in this line, try the next line
offs = line.getEndOffset();
line = getParagraphElement(c, offs);
}
return nextWord;
}
/**
* Finds the next word in the given elements text. The first
* parameter allows searching multiple paragraphs where even
* the first offset is desired.
* Returns the offset of the next word, or BreakIterator.DONE
* if there are no more words in the element.
*/
static int getNextWordInParagraph(JTextComponent c, Element line, int offs, boolean first) throws BadLocationException {
if (line == null) {
throw new BadLocationException("No more words", offs);
}
Document doc = line.getDocument();
int lineStart = line.getStartOffset();
int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
if ((offs >= lineEnd) || (offs < lineStart)) {
throw new BadLocationException("No more words", offs);
}
Segment seg = SegmentCache.getSharedSegment();
doc.getText(lineStart, lineEnd - lineStart, seg);
BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
words.setText(seg);
if ((first && (words.first() == (seg.offset + offs - lineStart))) &&
(! Character.isWhitespace(seg.array[words.first()]))) {
return offs;
}
int wordPosition = words.following(seg.offset + offs - lineStart);
if ((wordPosition == BreakIterator.DONE) ||
(wordPosition >= seg.offset + seg.count)) {
// there are no more words on this line.
return BreakIterator.DONE;
}
// if we haven't shot past the end... check to
// see if the current boundary represents whitespace.
// if so, we need to try again
char ch = seg.array[wordPosition];
if (! Character.isWhitespace(ch)) {
return lineStart + wordPosition - seg.offset;
}
// it was whitespace, try again. The assumption
// is that it must be a word start if the last
// one had whitespace following it.
wordPosition = words.next();
if (wordPosition != BreakIterator.DONE) {
offs = lineStart + wordPosition - seg.offset;
if (offs != lineEnd) {
return offs;
}
}
SegmentCache.releaseSharedSegment(seg);
return BreakIterator.DONE;
}
/**
* Determine the start of the prev word for the given location.
* Uses BreakIterator.getWordInstance() to actually get the words.
*
* @param c the editor
* @param offs the offset in the document >= 0
* @return the location in the model of the word start >= 0
* @exception BadLocationException if the offset is out of range
*/
public static final int getPreviousWord(JTextComponent c, int offs) throws BadLocationException {
int prevWord;
Element line = getParagraphElement(c, offs);
for (prevWord = getPrevWordInParagraph(c, line, offs);
prevWord == BreakIterator.DONE;
prevWord = getPrevWordInParagraph(c, line, offs)) {
// didn't find in this line, try the prev line
offs = line.getStartOffset() - 1;
line = getParagraphElement(c, offs);
}
return prevWord;
}
/**
* Finds the previous word in the given elements text. The first
* parameter allows searching multiple paragraphs where even
* the first offset is desired.
* Returns the offset of the next word, or BreakIterator.DONE
* if there are no more words in the element.
*/
static int getPrevWordInParagraph(JTextComponent c, Element line, int offs) throws BadLocationException {
if (line == null) {
throw new BadLocationException("No more words", offs);
}
Document doc = line.getDocument();
int lineStart = line.getStartOffset();
int lineEnd = line.getEndOffset();
if ((offs > lineEnd) || (offs < lineStart)) {
throw new BadLocationException("No more words", offs);
}
Segment seg = SegmentCache.getSharedSegment();
doc.getText(lineStart, lineEnd - lineStart, seg);
BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
words.setText(seg);
if (words.following(seg.offset + offs - lineStart) == BreakIterator.DONE) {
words.last();
}
int wordPosition = words.previous();
if (wordPosition == (seg.offset + offs - lineStart)) {
wordPosition = words.previous();
}
if (wordPosition == BreakIterator.DONE) {
// there are no more words on this line.
return BreakIterator.DONE;
}
// if we haven't shot past the end... check to
// see if the current boundary represents whitespace.
// if so, we need to try again
char ch = seg.array[wordPosition];
if (! Character.isWhitespace(ch)) {
return lineStart + wordPosition - seg.offset;
}
// it was whitespace, try again. The assumption
// is that it must be a word start if the last
// one had whitespace following it.
wordPosition = words.previous();
if (wordPosition != BreakIterator.DONE) {
return lineStart + wordPosition - seg.offset;
}
SegmentCache.releaseSharedSegment(seg);
return BreakIterator.DONE;
}
/**
* Determines the element to use for a paragraph/line.
*
* @param c the editor
* @param offs the starting offset in the document >= 0
* @return the element
*/
public static final Element getParagraphElement(JTextComponent c, int offs) {
Document doc = c.getDocument();
if (doc instanceof StyledDocument) {
return ((StyledDocument)doc).getParagraphElement(offs);
}
Element map = doc.getDefaultRootElement();
int index = map.getElementIndex(offs);
Element paragraph = map.getElement(index);
if ((offs >= paragraph.getStartOffset()) && (offs < paragraph.getEndOffset())) {
return paragraph;
}
return null;
}
static boolean isComposedTextElement(Document doc, int offset) {
Element elem = doc.getDefaultRootElement();
while (!elem.isLeaf()) {
elem = elem.getElement(elem.getElementIndex(offset));
}
return isComposedTextElement(elem);
}
static boolean isComposedTextElement(Element elem) {
AttributeSet as = elem.getAttributes();
return isComposedTextAttributeDefined(as);
}
static boolean isComposedTextAttributeDefined(AttributeSet as) {
return ((as != null) &&
(as.isDefined(StyleConstants.ComposedTextAttribute)));
}
/**
* Draws the given composed text passed from an input method.
*
* @param view View hosting text
* @param attr the attributes containing the composed text
* @param g the graphics context
* @param x the X origin
* @param y the Y origin
* @param p0 starting offset in the composed text to be rendered
* @param p1 ending offset in the composed text to be rendered
* @return the new insertion position
*/
static int drawComposedText(View view, AttributeSet attr, Graphics g,
int x, int y, int p0, int p1)
throws BadLocationException {
Graphics2D g2d = (Graphics2D)g;
AttributedString as = (AttributedString)attr.getAttribute(
StyleConstants.ComposedTextAttribute);
as.addAttribute(TextAttribute.FONT, g.getFont());
if (p0 >= p1)
return x;
AttributedCharacterIterator aci = as.getIterator(null, p0, p1);
return x + (int)SwingUtilities2.drawString(
getJComponent(view), g2d,aci,x,y);
}
/**
* Paints the composed text in a GlyphView
*/
static void paintComposedText(Graphics g, Rectangle alloc, GlyphView v) {
if (g instanceof Graphics2D) {
Graphics2D g2d = (Graphics2D) g;
int p0 = v.getStartOffset();
int p1 = v.getEndOffset();
AttributeSet attrSet = v.getElement().getAttributes();
AttributedString as =
(AttributedString)attrSet.getAttribute(StyleConstants.ComposedTextAttribute);
int start = v.getElement().getStartOffset();
int y = alloc.y + alloc.height - (int)v.getGlyphPainter().getDescent(v);
int x = alloc.x;
//Add text attributes
as.addAttribute(TextAttribute.FONT, v.getFont());
as.addAttribute(TextAttribute.FOREGROUND, v.getForeground());
if (StyleConstants.isBold(v.getAttributes())) {
as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
}
if (StyleConstants.isItalic(v.getAttributes())) {
as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
}
if (v.isUnderline()) {
as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
}
if (v.isStrikeThrough()) {
as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
}
if (v.isSuperscript()) {
as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER);
}
if (v.isSubscript()) {
as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB);
}
// draw
AttributedCharacterIterator aci = as.getIterator(null, p0 - start, p1 - start);
SwingUtilities2.drawString(getJComponent(v),
g2d,aci,x,y);
}
}
/*
* Convenience function for determining ComponentOrientation. Helps us
* avoid having Munge directives throughout the code.
*/
static boolean isLeftToRight( java.awt.Component c ) {
return c.getComponentOrientation().isLeftToRight();
}
/**
* Provides a way to determine the next visually represented model
* location that one might place a caret. Some views may not be visible,
* they might not be in the same order found in the model, or they just
* might not allow access to some of the locations in the model.
* <p>
* This implementation assumes the views are layed out in a logical
* manner. That is, that the view at index x + 1 is visually after
* the View at index x, and that the View at index x - 1 is visually
* before the View at x. There is support for reversing this behavior
* only if the passed in <code>View</code> is an instance of
* <code>CompositeView</code>. The <code>CompositeView</code>
* must then override the <code>flipEastAndWestAtEnds</code> method.
*
* @param v View to query
* @param pos the position to convert >= 0
* @param a the allocated region to render into
* @param direction the direction from the current position that can
* be thought of as the arrow keys typically found on a keyboard;
* this may be one of the following:
* <ul>
* <li><code>SwingConstants.WEST</code>
* <li><code>SwingConstants.EAST</code>
* <li><code>SwingConstants.NORTH</code>
* <li><code>SwingConstants.SOUTH</code>
* </ul>
* @param biasRet an array contain the bias that was checked
* @return the location within the model that best represents the next
* location visual position
* @exception BadLocationException
* @exception IllegalArgumentException if <code>direction</code> is invalid
*/
static int getNextVisualPositionFrom(View v, int pos, Position.Bias b,
Shape alloc, int direction,
Position.Bias[] biasRet)
throws BadLocationException {
if (v.getViewCount() == 0) {
// Nothing to do.
return pos;
}
boolean top = (direction == SwingConstants.NORTH ||
direction == SwingConstants.WEST);
int retValue;
if (pos == -1) {
// Start from the first View.
int childIndex = (top) ? v.getViewCount() - 1 : 0;
View child = v.getView(childIndex);
Shape childBounds = v.getChildAllocation(childIndex, alloc);
retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
direction, biasRet);
if (retValue == -1 && !top && v.getViewCount() > 1) {
// Special case that should ONLY happen if first view
// isn't valid (can happen when end position is put at
// beginning of line.
child = v.getView(1);
childBounds = v.getChildAllocation(1, alloc);
retValue = child.getNextVisualPositionFrom(-1, biasRet[0],
childBounds,
direction, biasRet);
}
}
else {
int increment = (top) ? -1 : 1;
int childIndex;
if (b == Position.Bias.Backward && pos > 0) {
childIndex = v.getViewIndex(pos - 1, Position.Bias.Forward);
}
else {
childIndex = v.getViewIndex(pos, Position.Bias.Forward);
}
View child = v.getView(childIndex);
Shape childBounds = v.getChildAllocation(childIndex, alloc);
retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
direction, biasRet);
if ((direction == SwingConstants.EAST ||
direction == SwingConstants.WEST) &&
(v instanceof CompositeView) &&
((CompositeView)v).flipEastAndWestAtEnds(pos, b)) {
increment *= -1;
}
childIndex += increment;
if (retValue == -1 && childIndex >= 0 &&
childIndex < v.getViewCount()) {
child = v.getView(childIndex);
childBounds = v.getChildAllocation(childIndex, alloc);
retValue = child.getNextVisualPositionFrom(
-1, b, childBounds, direction, biasRet);
// If there is a bias change, it is a fake position
// and we should skip it. This is usually the result
// of two elements side be side flowing the same way.
if (retValue == pos && biasRet[0] != b) {
return getNextVisualPositionFrom(v, pos, biasRet[0],
alloc, direction,
biasRet);
}
}
else if (retValue != -1 && biasRet[0] != b &&
((increment == 1 && child.getEndOffset() == retValue) ||
(increment == -1 &&
child.getStartOffset() == retValue)) &&
childIndex >= 0 && childIndex < v.getViewCount()) {
// Reached the end of a view, make sure the next view
// is a different direction.
child = v.getView(childIndex);
childBounds = v.getChildAllocation(childIndex, alloc);
Position.Bias originalBias = biasRet[0];
int nextPos = child.getNextVisualPositionFrom(
-1, b, childBounds, direction, biasRet);
if (biasRet[0] == b) {
retValue = nextPos;
}
else {
biasRet[0] = originalBias;
}
}
}
return retValue;
}
}