You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1079 lines
42 KiB
1079 lines
42 KiB
/*
|
|
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
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;
|
|
}
|
|
}
|