Java tutorial
/* * Copyright (c) 1997, 2017, 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.awt.Component; import java.awt.Rectangle; import java.awt.Graphics; import java.awt.FontMetrics; import java.awt.Shape; import java.awt.Graphics2D; import java.awt.font.TextAttribute; import java.awt.geom.Rectangle2D; import java.text.*; import javax.swing.JComponent; import javax.swing.SwingConstants; import javax.swing.text.ParagraphView.Row; import sun.swing.SwingUtilities2; import static sun.swing.SwingUtilities2.drawChars; import static sun.swing.SwingUtilities2.getFontCharWidth; import static sun.swing.SwingUtilities2.getFontCharsWidth; /** * 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 * * @deprecated replaced by * {@link #drawTabbedText(Segment, float, float, Graphics2D, TabExpander, int)} */ @Deprecated(since = "9") 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. * * @param s the source of the text * @param x the X origin {@code >= 0} * @param y the Y origin {@code >= 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 {@code >= 0} * @return the X location at the end of the rendered text * * @since 9 */ public static final float drawTabbedText(Segment s, float x, float y, Graphics2D g, TabExpander e, int startOffset) { return drawTabbedText(null, s, x, y, g, e, startOffset, null, true); } /** * 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) { return (int) drawTabbedText(view, s, x, y, g, e, startOffset, justificationData, false); } static final float drawTabbedText(View view, Segment s, float x, float y, Graphics g, TabExpander e, int startOffset, int[] justificationData, boolean useFPAPI) { JComponent component = getJComponent(view); FontMetrics metrics = SwingUtilities2.getFontMetrics(component, g); float 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 = drawChars(component, g, txt, flushIndex, flushLen, x, y); flushLen = 0; } flushIndex = i + 1; if (txt[i] == '\t') { if (e != null) { nextX = e.nextTabStop(nextX, startOffset + i - txtOffset); } else { nextX += getFontCharWidth(' ', metrics, useFPAPI); } } else if (txt[i] == ' ') { float spaceWidth = getFontCharWidth(' ', metrics, useFPAPI); nextX += spaceWidth + spaceAddon; if (i <= spaceAddonLeftoverEnd) { nextX++; } } x = nextX; } else if ((txt[i] == '\n') || (txt[i] == '\r')) { if (flushLen > 0) { nextX = drawChars(component, g, txt, flushIndex, flushLen, x, y, useFPAPI); flushLen = 0; } flushIndex = i + 1; x = nextX; } else { flushLen += 1; } } if (flushLen > 0) { nextX = drawChars(component, g, txt, flushIndex, flushLen, x, y, useFPAPI); } 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 * * @deprecated replaced by * {@link #getTabbedTextWidth(Segment, FontMetrics, float, TabExpander, int)} */ @Deprecated(since = "9") public static final int getTabbedTextWidth(Segment s, FontMetrics metrics, int x, TabExpander e, int startOffset) { return getTabbedTextWidth(null, s, metrics, x, e, startOffset, null); } /** * Determines the width of the given segment of text taking tabs * into consideration. * * @param s the source of the text * @param metrics the font metrics to use for the calculation * @param x the X origin {@code >= 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 {@code >= 0} * @return the width of the text * * @since 9 */ public static final float getTabbedTextWidth(Segment s, FontMetrics metrics, float 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) { return (int) getTabbedTextWidth(view, s, metrics, x, e, startOffset, justificationData, false); } static final float getTabbedTextWidth(View view, Segment s, FontMetrics metrics, float x, TabExpander e, int startOffset, int[] justificationData) { return getTabbedTextWidth(view, s, metrics, x, e, startOffset, justificationData, true); } static final float getTabbedTextWidth(View view, Segment s, FontMetrics metrics, float x, TabExpander e, int startOffset, int[] justificationData, boolean useFPAPI) { float 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 = e.nextTabStop(nextX, startOffset + i - txtOffset); } else { nextX += getFontCharWidth(' ', metrics, useFPAPI); } } else if (txt[i] == ' ') { float spaceWidth = getFontCharWidth(' ', metrics, useFPAPI); nextX += spaceWidth + spaceAddon; if (i <= spaceAddonLeftoverEnd) { nextX++; } } } else if (txt[i] == '\n') { // Ignore newlines, they take up space and we shouldn't be // counting them. nextX += getFontCharsWidth(txt, i - charCount, charCount, metrics, useFPAPI); charCount = 0; } else { charCount++; } } nextX += getFontCharsWidth(txt, n - charCount, charCount, metrics, useFPAPI); 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 * * @deprecated replaced by * {@link #getTabbedTextOffset(Segment, FontMetrics, float, float, * TabExpander, int, boolean)} */ @Deprecated(since = "9") 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, false); } /** * Determines the relative offset into the given text that * best represents the given span in the view coordinate * system. * * @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 * @param round whether or not to round * @return the offset into the text >= 0 * * @deprecated replaced by * {@link #getTabbedTextOffset(Segment, FontMetrics, float, float, * TabExpander, int, boolean)} */ @Deprecated(since = "9") 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, false); } /** * Determines the relative offset into the given text that * best represents the given span in the view coordinate * system. * * @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 {@code >= 0}. * @param x the target view location to translate to an * offset into the text {@code >= 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 {@code >= 0} * @param round whether or not to round * @return the offset into the text {@code >= 0} * * @since 9 */ public static final int getTabbedTextOffset(Segment s, FontMetrics metrics, float x0, float x, TabExpander e, int startOffset, boolean round) { return getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, round, null, true); } // 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, float x0, float x, TabExpander e, int startOffset, boolean round, int[] justificationData, boolean useFPAPI) { if (x0 >= x) { // x before x0, return. return 0; } float 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 = e.nextTabStop(nextX, startOffset + i - txtOffset); } else { nextX += getFontCharWidth(' ', metrics, useFPAPI); } } else if (txt[i] == ' ') { nextX += getFontCharWidth(' ', metrics, useFPAPI); nextX += spaceAddon; if (i <= spaceAddonLeftoverEnd) { nextX++; } } } else { nextX += getFontCharWidth(txt[i], metrics, useFPAPI); } 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; float width = getFontCharsWidth(txt, txtOffset, offset, metrics, useFPAPI); float span = x - x0; if (span < width) { while (offset > 0) { float charsWidth = getFontCharsWidth(txt, txtOffset, offset - 1, metrics, useFPAPI); float nextWidth = offset > 1 ? charsWidth : 0; if (span >= nextWidth) { if (span - nextWidth < width - span) { offset--; } break; } width = nextWidth; offset--; } } } else { offset = i - txtOffset; while (offset > 0 && getFontCharsWidth(txt, txtOffset, offset, metrics, useFPAPI) > (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 * * @deprecated replaced by * {@link #getBreakLocation(Segment, FontMetrics, float, float, * TabExpander, int)} */ @Deprecated(since = "9") public static final int getBreakLocation(Segment s, FontMetrics metrics, int x0, int x, TabExpander e, int startOffset) { return getBreakLocation(s, metrics, x0, x, e, startOffset, false); } static final int getBreakLocation(Segment s, FontMetrics metrics, float x0, float x, TabExpander e, int startOffset, boolean useFPIAPI) { char[] txt = s.array; int txtOffset = s.offset; int txtCount = s.count; int index = getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, false, null, useFPIAPI); 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; } /** * 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 * * @since 9 */ public static final int getBreakLocation(Segment s, FontMetrics metrics, float x0, float x, TabExpander e, int startOffset) { return getBreakLocation(s, metrics, x0, x, e, startOffset, true); } /** * 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 */ @SuppressWarnings("deprecation") 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 */ @SuppressWarnings("deprecation") public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException { Rectangle2D r = c.modelToView2D(offs); if (r == null) { return -1; } int n = c.getDocument().getLength(); int lastOffs = offs; double y = r.getY(); while ((r != null) && (y == r.getY())) { // Skip invisible elements if (r.getHeight() != 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 * * @deprecated replaced by * {@link #getPositionAbove(JTextComponent, int, float)} */ @Deprecated(since = "9") public static final int getPositionAbove(JTextComponent c, int offs, int x) throws BadLocationException { return getPositionAbove(c, offs, x, false); } @SuppressWarnings("deprecation") static final int getPositionAbove(JTextComponent c, int offs, float x, boolean useFPAPI) throws BadLocationException { int lastOffs = getRowStart(c, offs) - 1; if (lastOffs < 0) { return -1; } double bestSpan = Integer.MAX_VALUE; double y = 0; Rectangle2D r = null; if (lastOffs >= 0) { r = useFPAPI ? c.modelToView2D(lastOffs) : c.modelToView(lastOffs); y = r.getY(); } while ((r != null) && (y == r.getY())) { double span = Math.abs(r.getX() - x); if (span < bestSpan) { offs = lastOffs; bestSpan = span; } lastOffs -= 1; if ((lastOffs >= 0)) { r = useFPAPI ? c.modelToView2D(lastOffs) : c.modelToView(lastOffs); } else { r = 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 {@code >= 0} * @param x the X coordinate {@code >= 0} * @return the position {@code >= 0} if the request can be computed, otherwise * a value of -1 will be returned. * @exception BadLocationException if the offset is out of range * * @since 9 */ public static final int getPositionAbove(JTextComponent c, int offs, float x) throws BadLocationException { return getPositionAbove(c, offs, x, true); } /** * 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 * * @deprecated replaced by * {@link #getPositionBelow(JTextComponent, int, float)} */ @Deprecated(since = "9") public static final int getPositionBelow(JTextComponent c, int offs, int x) throws BadLocationException { return getPositionBelow(c, offs, x, false); } @SuppressWarnings("deprecation") static final int getPositionBelow(JTextComponent c, int offs, float x, boolean useFPAPI) throws BadLocationException { int lastOffs = getRowEnd(c, offs) + 1; if (lastOffs <= 0) { return -1; } double bestSpan = Integer.MAX_VALUE; int n = c.getDocument().getLength(); double y = 0; Rectangle2D r = null; if (lastOffs <= n) { r = useFPAPI ? c.modelToView2D(lastOffs) : c.modelToView(lastOffs); y = r.getY(); } while ((r != null) && (y == r.getY())) { double span = Math.abs(x - r.getX()); if (span < bestSpan) { offs = lastOffs; bestSpan = span; } lastOffs += 1; if (lastOffs <= n) { r = useFPAPI ? c.modelToView2D(lastOffs) : c.modelToView(lastOffs); } else { r = 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 {@code >= 0} * @param x the X coordinate {@code >= 0} * @return the position {@code >= 0} if the request can be computed, otherwise * a value of -1 will be returned. * @exception BadLocationException if the offset is out of range * * @since 9 */ public static final int getPositionBelow(JTextComponent c, int offs, float x) throws BadLocationException { return getPositionBelow(c, offs, x, true); } /** * 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 { return (int) drawComposedText(view, attr, g, x, y, p0, p1, false); } static float drawComposedText(View view, AttributeSet attr, Graphics g, float x, float y, int p0, int p1) throws BadLocationException { return drawComposedText(view, attr, g, x, y, p0, p1, true); } static float drawComposedText(View view, AttributeSet attr, Graphics g, float x, float y, int p0, int p1, boolean useFPAPI) 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 + 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 alloc 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; } }