| /* |
| * Copyright (c) 1999, 2010, 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.*; |
| import java.text.BreakIterator; |
| import javax.swing.event.*; |
| import java.util.BitSet; |
| import java.util.Locale; |
| |
| import javax.swing.UIManager; |
| import sun.swing.SwingUtilities2; |
| import static sun.swing.SwingUtilities2.IMPLIED_CR; |
| |
| /** |
| * A GlyphView is a styled chunk of text that represents a view |
| * mapped over an element in the text model. This view is generally |
| * responsible for displaying text glyphs using character level |
| * attributes in some way. |
| * An implementation of the GlyphPainter class is used to do the |
| * actual rendering and model/view translations. This separates |
| * rendering from layout and management of the association with |
| * the model. |
| * <p> |
| * The view supports breaking for the purpose of formatting. |
| * The fragments produced by breaking share the view that has |
| * primary responsibility for the element (i.e. they are nested |
| * classes and carry only a small amount of state of their own) |
| * so they can share its resources. |
| * <p> |
| * Since this view |
| * represents text that may have tabs embedded in it, it implements the |
| * <code>TabableView</code> interface. Tabs will only be |
| * expanded if this view is embedded in a container that does |
| * tab expansion. ParagraphView is an example of a container |
| * that does tab expansion. |
| * <p> |
| * |
| * @since 1.3 |
| * |
| * @author Timothy Prinzing |
| */ |
| public class GlyphView extends View implements TabableView, Cloneable { |
| |
| /** |
| * Constructs a new view wrapped on an element. |
| * |
| * @param elem the element |
| */ |
| public GlyphView(Element elem) { |
| super(elem); |
| offset = 0; |
| length = 0; |
| Element parent = elem.getParentElement(); |
| AttributeSet attr = elem.getAttributes(); |
| |
| // if there was an implied CR |
| impliedCR = (attr != null && attr.getAttribute(IMPLIED_CR) != null && |
| // if this is non-empty paragraph |
| parent != null && parent.getElementCount() > 1); |
| skipWidth = elem.getName().equals("br"); |
| } |
| |
| /** |
| * Creates a shallow copy. This is used by the |
| * createFragment and breakView methods. |
| * |
| * @return the copy |
| */ |
| protected final Object clone() { |
| Object o; |
| try { |
| o = super.clone(); |
| } catch (CloneNotSupportedException cnse) { |
| o = null; |
| } |
| return o; |
| } |
| |
| /** |
| * Fetch the currently installed glyph painter. |
| * If a painter has not yet been installed, and |
| * a default was not yet needed, null is returned. |
| */ |
| public GlyphPainter getGlyphPainter() { |
| return painter; |
| } |
| |
| /** |
| * Sets the painter to use for rendering glyphs. |
| */ |
| public void setGlyphPainter(GlyphPainter p) { |
| painter = p; |
| } |
| |
| /** |
| * Fetch a reference to the text that occupies |
| * the given range. This is normally used by |
| * the GlyphPainter to determine what characters |
| * it should render glyphs for. |
| * |
| * @param p0 the starting document offset >= 0 |
| * @param p1 the ending document offset >= p0 |
| * @return the <code>Segment</code> containing the text |
| */ |
| public Segment getText(int p0, int p1) { |
| // When done with the returned Segment it should be released by |
| // invoking: |
| // SegmentCache.releaseSharedSegment(segment); |
| Segment text = SegmentCache.getSharedSegment(); |
| try { |
| Document doc = getDocument(); |
| doc.getText(p0, p1 - p0, text); |
| } catch (BadLocationException bl) { |
| throw new StateInvariantError("GlyphView: Stale view: " + bl); |
| } |
| return text; |
| } |
| |
| /** |
| * Fetch the background color to use to render the |
| * glyphs. If there is no background color, null should |
| * be returned. This is implemented to call |
| * <code>StyledDocument.getBackground</code> if the associated |
| * document is a styled document, otherwise it returns null. |
| */ |
| public Color getBackground() { |
| Document doc = getDocument(); |
| if (doc instanceof StyledDocument) { |
| AttributeSet attr = getAttributes(); |
| if (attr.isDefined(StyleConstants.Background)) { |
| return ((StyledDocument)doc).getBackground(attr); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Fetch the foreground color to use to render the |
| * glyphs. If there is no foreground color, null should |
| * be returned. This is implemented to call |
| * <code>StyledDocument.getBackground</code> if the associated |
| * document is a StyledDocument. If the associated document |
| * is not a StyledDocument, the associated components foreground |
| * color is used. If there is no associated component, null |
| * is returned. |
| */ |
| public Color getForeground() { |
| Document doc = getDocument(); |
| if (doc instanceof StyledDocument) { |
| AttributeSet attr = getAttributes(); |
| return ((StyledDocument)doc).getForeground(attr); |
| } |
| Component c = getContainer(); |
| if (c != null) { |
| return c.getForeground(); |
| } |
| return null; |
| } |
| |
| /** |
| * Fetch the font that the glyphs should be based |
| * upon. This is implemented to call |
| * <code>StyledDocument.getFont</code> if the associated |
| * document is a StyledDocument. If the associated document |
| * is not a StyledDocument, the associated components font |
| * is used. If there is no associated component, null |
| * is returned. |
| */ |
| public Font getFont() { |
| Document doc = getDocument(); |
| if (doc instanceof StyledDocument) { |
| AttributeSet attr = getAttributes(); |
| return ((StyledDocument)doc).getFont(attr); |
| } |
| Component c = getContainer(); |
| if (c != null) { |
| return c.getFont(); |
| } |
| return null; |
| } |
| |
| /** |
| * Determine if the glyphs should be underlined. If true, |
| * an underline should be drawn through the baseline. |
| */ |
| public boolean isUnderline() { |
| AttributeSet attr = getAttributes(); |
| return StyleConstants.isUnderline(attr); |
| } |
| |
| /** |
| * Determine if the glyphs should have a strikethrough |
| * line. If true, a line should be drawn through the center |
| * of the glyphs. |
| */ |
| public boolean isStrikeThrough() { |
| AttributeSet attr = getAttributes(); |
| return StyleConstants.isStrikeThrough(attr); |
| } |
| |
| /** |
| * Determine if the glyphs should be rendered as superscript. |
| */ |
| public boolean isSubscript() { |
| AttributeSet attr = getAttributes(); |
| return StyleConstants.isSubscript(attr); |
| } |
| |
| /** |
| * Determine if the glyphs should be rendered as subscript. |
| */ |
| public boolean isSuperscript() { |
| AttributeSet attr = getAttributes(); |
| return StyleConstants.isSuperscript(attr); |
| } |
| |
| /** |
| * Fetch the TabExpander to use if tabs are present in this view. |
| */ |
| public TabExpander getTabExpander() { |
| return expander; |
| } |
| |
| /** |
| * Check to see that a glyph painter exists. If a painter |
| * doesn't exist, a default glyph painter will be installed. |
| */ |
| protected void checkPainter() { |
| if (painter == null) { |
| if (defaultPainter == null) { |
| // the classname should probably come from a property file. |
| String classname = "javax.swing.text.GlyphPainter1"; |
| try { |
| Class c; |
| ClassLoader loader = getClass().getClassLoader(); |
| if (loader != null) { |
| c = loader.loadClass(classname); |
| } else { |
| c = Class.forName(classname); |
| } |
| Object o = c.newInstance(); |
| if (o instanceof GlyphPainter) { |
| defaultPainter = (GlyphPainter) o; |
| } |
| } catch (Throwable e) { |
| throw new StateInvariantError("GlyphView: Can't load glyph painter: " |
| + classname); |
| } |
| } |
| setGlyphPainter(defaultPainter.getPainter(this, getStartOffset(), |
| getEndOffset())); |
| } |
| } |
| |
| // --- TabableView methods -------------------------------------- |
| |
| /** |
| * Determines the desired span when using the given |
| * tab expansion implementation. |
| * |
| * @param x the position the view would be located |
| * at for the purpose of tab expansion >= 0. |
| * @param e how to expand the tabs when encountered. |
| * @return the desired span >= 0 |
| * @see TabableView#getTabbedSpan |
| */ |
| public float getTabbedSpan(float x, TabExpander e) { |
| checkPainter(); |
| |
| TabExpander old = expander; |
| expander = e; |
| |
| if (expander != old) { |
| // setting expander can change horizontal span of the view, |
| // so we have to call preferenceChanged() |
| preferenceChanged(null, true, false); |
| } |
| |
| this.x = (int) x; |
| int p0 = getStartOffset(); |
| int p1 = getEndOffset(); |
| float width = painter.getSpan(this, p0, p1, expander, x); |
| return width; |
| } |
| |
| /** |
| * Determines the span along the same axis as tab |
| * expansion for a portion of the view. This is |
| * intended for use by the TabExpander for cases |
| * where the tab expansion involves aligning the |
| * portion of text that doesn't have whitespace |
| * relative to the tab stop. There is therefore |
| * an assumption that the range given does not |
| * contain tabs. |
| * <p> |
| * This method can be called while servicing the |
| * getTabbedSpan or getPreferredSize. It has to |
| * arrange for its own text buffer to make the |
| * measurements. |
| * |
| * @param p0 the starting document offset >= 0 |
| * @param p1 the ending document offset >= p0 |
| * @return the span >= 0 |
| */ |
| public float getPartialSpan(int p0, int p1) { |
| checkPainter(); |
| float width = painter.getSpan(this, p0, p1, expander, x); |
| return width; |
| } |
| |
| // --- View methods --------------------------------------------- |
| |
| /** |
| * Fetches the portion of the model that this view is responsible for. |
| * |
| * @return the starting offset into the model |
| * @see View#getStartOffset |
| */ |
| public int getStartOffset() { |
| Element e = getElement(); |
| return (length > 0) ? e.getStartOffset() + offset : e.getStartOffset(); |
| } |
| |
| /** |
| * Fetches the portion of the model that this view is responsible for. |
| * |
| * @return the ending offset into the model |
| * @see View#getEndOffset |
| */ |
| public int getEndOffset() { |
| Element e = getElement(); |
| return (length > 0) ? e.getStartOffset() + offset + length : e.getEndOffset(); |
| } |
| |
| /** |
| * Lazily initializes the selections field |
| */ |
| private void initSelections(int p0, int p1) { |
| int viewPosCount = p1 - p0 + 1; |
| if (selections == null || viewPosCount > selections.length) { |
| selections = new byte[viewPosCount]; |
| return; |
| } |
| for (int i = 0; i < viewPosCount; selections[i++] = 0); |
| } |
| |
| /** |
| * Renders a portion of a text style run. |
| * |
| * @param g the rendering surface to use |
| * @param a the allocated region to render into |
| */ |
| public void paint(Graphics g, Shape a) { |
| checkPainter(); |
| |
| boolean paintedText = false; |
| Component c = getContainer(); |
| int p0 = getStartOffset(); |
| int p1 = getEndOffset(); |
| Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds(); |
| Color bg = getBackground(); |
| Color fg = getForeground(); |
| |
| if (c != null && ! c.isEnabled()) { |
| fg = (c instanceof JTextComponent ? |
| ((JTextComponent)c).getDisabledTextColor() : |
| UIManager.getColor("textInactiveText")); |
| } |
| if (bg != null) { |
| g.setColor(bg); |
| g.fillRect(alloc.x, alloc.y, alloc.width, alloc.height); |
| } |
| if (c instanceof JTextComponent) { |
| JTextComponent tc = (JTextComponent) c; |
| Highlighter h = tc.getHighlighter(); |
| if (h instanceof LayeredHighlighter) { |
| ((LayeredHighlighter)h).paintLayeredHighlights |
| (g, p0, p1, a, tc, this); |
| } |
| } |
| |
| if (Utilities.isComposedTextElement(getElement())) { |
| Utilities.paintComposedText(g, a.getBounds(), this); |
| paintedText = true; |
| } else if(c instanceof JTextComponent) { |
| JTextComponent tc = (JTextComponent) c; |
| Color selFG = tc.getSelectedTextColor(); |
| |
| if (// there's a highlighter (bug 4532590), and |
| (tc.getHighlighter() != null) && |
| // selected text color is different from regular foreground |
| (selFG != null) && !selFG.equals(fg)) { |
| |
| Highlighter.Highlight[] h = tc.getHighlighter().getHighlights(); |
| if(h.length != 0) { |
| boolean initialized = false; |
| int viewSelectionCount = 0; |
| for (int i = 0; i < h.length; i++) { |
| Highlighter.Highlight highlight = h[i]; |
| int hStart = highlight.getStartOffset(); |
| int hEnd = highlight.getEndOffset(); |
| if (hStart > p1 || hEnd < p0) { |
| // the selection is out of this view |
| continue; |
| } |
| if (!SwingUtilities2.useSelectedTextColor(highlight, tc)) { |
| continue; |
| } |
| if (hStart <= p0 && hEnd >= p1){ |
| // the whole view is selected |
| paintTextUsingColor(g, a, selFG, p0, p1); |
| paintedText = true; |
| break; |
| } |
| // the array is lazily created only when the view |
| // is partially selected |
| if (!initialized) { |
| initSelections(p0, p1); |
| initialized = true; |
| } |
| hStart = Math.max(p0, hStart); |
| hEnd = Math.min(p1, hEnd); |
| paintTextUsingColor(g, a, selFG, hStart, hEnd); |
| // the array represents view positions [0, p1-p0+1] |
| // later will iterate this array and sum its |
| // elements. Positions with sum == 0 are not selected. |
| selections[hStart-p0]++; |
| selections[hEnd-p0]--; |
| |
| viewSelectionCount++; |
| } |
| |
| if (!paintedText && viewSelectionCount > 0) { |
| // the view is partially selected |
| int curPos = -1; |
| int startPos = 0; |
| int viewLen = p1 - p0; |
| while (curPos++ < viewLen) { |
| // searching for the next selection start |
| while(curPos < viewLen && |
| selections[curPos] == 0) curPos++; |
| if (startPos != curPos) { |
| // paint unselected text |
| paintTextUsingColor(g, a, fg, |
| p0 + startPos, p0 + curPos); |
| } |
| int checkSum = 0; |
| // searching for next start position of unselected text |
| while (curPos < viewLen && |
| (checkSum += selections[curPos]) != 0) curPos++; |
| startPos = curPos; |
| } |
| paintedText = true; |
| } |
| } |
| } |
| } |
| if(!paintedText) |
| paintTextUsingColor(g, a, fg, p0, p1); |
| } |
| |
| /** |
| * Paints the specified region of text in the specified color. |
| */ |
| final void paintTextUsingColor(Graphics g, Shape a, Color c, int p0, int p1) { |
| // render the glyphs |
| g.setColor(c); |
| painter.paint(this, g, a, p0, p1); |
| |
| // render underline or strikethrough if set. |
| boolean underline = isUnderline(); |
| boolean strike = isStrikeThrough(); |
| if (underline || strike) { |
| // calculate x coordinates |
| Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds(); |
| View parent = getParent(); |
| if ((parent != null) && (parent.getEndOffset() == p1)) { |
| // strip whitespace on end |
| Segment s = getText(p0, p1); |
| while (Character.isWhitespace(s.last())) { |
| p1 -= 1; |
| s.count -= 1; |
| } |
| SegmentCache.releaseSharedSegment(s); |
| } |
| int x0 = alloc.x; |
| int p = getStartOffset(); |
| if (p != p0) { |
| x0 += (int) painter.getSpan(this, p, p0, getTabExpander(), x0); |
| } |
| int x1 = x0 + (int) painter.getSpan(this, p0, p1, getTabExpander(), x0); |
| |
| // calculate y coordinate |
| int y = alloc.y + alloc.height - (int) painter.getDescent(this); |
| if (underline) { |
| int yTmp = y + 1; |
| g.drawLine(x0, yTmp, x1, yTmp); |
| } |
| if (strike) { |
| // move y coordinate above baseline |
| int yTmp = y - (int) (painter.getAscent(this) * 0.3f); |
| g.drawLine(x0, yTmp, x1, yTmp); |
| } |
| |
| } |
| } |
| |
| /** |
| * Determines the minimum span for this view along an axis. |
| * |
| * <p>This implementation returns the longest non-breakable area within |
| * the view as a minimum span for {@code View.X_AXIS}.</p> |
| * |
| * @param axis may be either {@code View.X_AXIS} or {@code View.Y_AXIS} |
| * @return the minimum span the view can be rendered into |
| * @throws IllegalArgumentException if the {@code axis} parameter is invalid |
| * @see javax.swing.text.View#getMinimumSpan |
| */ |
| @Override |
| public float getMinimumSpan(int axis) { |
| switch (axis) { |
| case View.X_AXIS: |
| if (minimumSpan < 0) { |
| minimumSpan = 0; |
| int p0 = getStartOffset(); |
| int p1 = getEndOffset(); |
| while (p1 > p0) { |
| int breakSpot = getBreakSpot(p0, p1); |
| if (breakSpot == BreakIterator.DONE) { |
| // the rest of the view is non-breakable |
| breakSpot = p0; |
| } |
| minimumSpan = Math.max(minimumSpan, |
| getPartialSpan(breakSpot, p1)); |
| // Note: getBreakSpot returns the *last* breakspot |
| p1 = breakSpot - 1; |
| } |
| } |
| return minimumSpan; |
| case View.Y_AXIS: |
| return super.getMinimumSpan(axis); |
| default: |
| throw new IllegalArgumentException("Invalid axis: " + axis); |
| } |
| } |
| |
| /** |
| * Determines the preferred span for this view along an |
| * axis. |
| * |
| * @param axis may be either View.X_AXIS or View.Y_AXIS |
| * @return the span the view would like to be rendered into >= 0. |
| * Typically the view is told to render into the span |
| * that is returned, although there is no guarantee. |
| * The parent may choose to resize or break the view. |
| */ |
| public float getPreferredSpan(int axis) { |
| if (impliedCR) { |
| return 0; |
| } |
| checkPainter(); |
| int p0 = getStartOffset(); |
| int p1 = getEndOffset(); |
| switch (axis) { |
| case View.X_AXIS: |
| if (skipWidth) { |
| return 0; |
| } |
| return painter.getSpan(this, p0, p1, expander, this.x); |
| case View.Y_AXIS: |
| float h = painter.getHeight(this); |
| if (isSuperscript()) { |
| h += h/3; |
| } |
| return h; |
| default: |
| throw new IllegalArgumentException("Invalid axis: " + axis); |
| } |
| } |
| |
| /** |
| * Determines the desired alignment for this view along an |
| * axis. For the label, the alignment is along the font |
| * baseline for the y axis, and the superclasses alignment |
| * along the x axis. |
| * |
| * @param axis may be either View.X_AXIS or View.Y_AXIS |
| * @return the desired alignment. This should be a value |
| * between 0.0 and 1.0 inclusive, where 0 indicates alignment at the |
| * origin and 1.0 indicates alignment to the full span |
| * away from the origin. An alignment of 0.5 would be the |
| * center of the view. |
| */ |
| public float getAlignment(int axis) { |
| checkPainter(); |
| if (axis == View.Y_AXIS) { |
| boolean sup = isSuperscript(); |
| boolean sub = isSubscript(); |
| float h = painter.getHeight(this); |
| float d = painter.getDescent(this); |
| float a = painter.getAscent(this); |
| float align; |
| if (sup) { |
| align = 1.0f; |
| } else if (sub) { |
| align = (h > 0) ? (h - (d + (a / 2))) / h : 0; |
| } else { |
| align = (h > 0) ? (h - d) / h : 0; |
| } |
| return align; |
| } |
| return super.getAlignment(axis); |
| } |
| |
| /** |
| * Provides a mapping from the document model coordinate space |
| * to the coordinate space of the view mapped to it. |
| * |
| * @param pos the position to convert >= 0 |
| * @param a the allocated region to render into |
| * @param b either <code>Position.Bias.Forward</code> |
| * or <code>Position.Bias.Backward</code> |
| * @return the bounding box of the given position |
| * @exception BadLocationException if the given position does not represent a |
| * valid location in the associated document |
| * @see View#modelToView |
| */ |
| public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { |
| checkPainter(); |
| return painter.modelToView(this, pos, b, a); |
| } |
| |
| /** |
| * Provides a mapping from the view coordinate space to the logical |
| * coordinate space of the model. |
| * |
| * @param x the X coordinate >= 0 |
| * @param y the Y coordinate >= 0 |
| * @param a the allocated region to render into |
| * @param biasReturn either <code>Position.Bias.Forward</code> |
| * or <code>Position.Bias.Backward</code> is returned as the |
| * zero-th element of this array |
| * @return the location within the model that best represents the |
| * given point of view >= 0 |
| * @see View#viewToModel |
| */ |
| public int viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn) { |
| checkPainter(); |
| return painter.viewToModel(this, x, y, a, biasReturn); |
| } |
| |
| /** |
| * Determines how attractive a break opportunity in |
| * this view is. This can be used for determining which |
| * view is the most attractive to call <code>breakView</code> |
| * on in the process of formatting. The |
| * higher the weight, the more attractive the break. A |
| * value equal to or lower than <code>View.BadBreakWeight</code> |
| * should not be considered for a break. A value greater |
| * than or equal to <code>View.ForcedBreakWeight</code> should |
| * be broken. |
| * <p> |
| * This is implemented to forward to the superclass for |
| * the Y_AXIS. Along the X_AXIS the following values |
| * may be returned. |
| * <dl> |
| * <dt><b>View.ExcellentBreakWeight</b> |
| * <dd>if there is whitespace proceeding the desired break |
| * location. |
| * <dt><b>View.BadBreakWeight</b> |
| * <dd>if the desired break location results in a break |
| * location of the starting offset. |
| * <dt><b>View.GoodBreakWeight</b> |
| * <dd>if the other conditions don't occur. |
| * </dl> |
| * This will normally result in the behavior of breaking |
| * on a whitespace location if one can be found, otherwise |
| * breaking between characters. |
| * |
| * @param axis may be either View.X_AXIS or View.Y_AXIS |
| * @param pos the potential location of the start of the |
| * broken view >= 0. This may be useful for calculating tab |
| * positions. |
| * @param len specifies the relative length from <em>pos</em> |
| * where a potential break is desired >= 0. |
| * @return the weight, which should be a value between |
| * View.ForcedBreakWeight and View.BadBreakWeight. |
| * @see LabelView |
| * @see ParagraphView |
| * @see View#BadBreakWeight |
| * @see View#GoodBreakWeight |
| * @see View#ExcellentBreakWeight |
| * @see View#ForcedBreakWeight |
| */ |
| public int getBreakWeight(int axis, float pos, float len) { |
| if (axis == View.X_AXIS) { |
| checkPainter(); |
| int p0 = getStartOffset(); |
| int p1 = painter.getBoundedPosition(this, p0, pos, len); |
| return p1 == p0 ? View.BadBreakWeight : |
| getBreakSpot(p0, p1) != BreakIterator.DONE ? |
| View.ExcellentBreakWeight : View.GoodBreakWeight; |
| } |
| return super.getBreakWeight(axis, pos, len); |
| } |
| |
| /** |
| * Breaks this view on the given axis at the given length. |
| * This is implemented to attempt to break on a whitespace |
| * location, and returns a fragment with the whitespace at |
| * the end. If a whitespace location can't be found, the |
| * nearest character is used. |
| * |
| * @param axis may be either View.X_AXIS or View.Y_AXIS |
| * @param p0 the location in the model where the |
| * fragment should start it's representation >= 0. |
| * @param pos the position along the axis that the |
| * broken view would occupy >= 0. This may be useful for |
| * things like tab calculations. |
| * @param len specifies the distance along the axis |
| * where a potential break is desired >= 0. |
| * @return the fragment of the view that represents the |
| * given span, if the view can be broken. If the view |
| * doesn't support breaking behavior, the view itself is |
| * returned. |
| * @see View#breakView |
| */ |
| public View breakView(int axis, int p0, float pos, float len) { |
| if (axis == View.X_AXIS) { |
| checkPainter(); |
| int p1 = painter.getBoundedPosition(this, p0, pos, len); |
| int breakSpot = getBreakSpot(p0, p1); |
| |
| if (breakSpot != -1) { |
| p1 = breakSpot; |
| } |
| // else, no break in the region, return a fragment of the |
| // bounded region. |
| if (p0 == getStartOffset() && p1 == getEndOffset()) { |
| return this; |
| } |
| GlyphView v = (GlyphView) createFragment(p0, p1); |
| v.x = (int) pos; |
| return v; |
| } |
| return this; |
| } |
| |
| /** |
| * Returns a location to break at in the passed in region, or |
| * BreakIterator.DONE if there isn't a good location to break at |
| * in the specified region. |
| */ |
| private int getBreakSpot(int p0, int p1) { |
| if (breakSpots == null) { |
| // Re-calculate breakpoints for the whole view |
| int start = getStartOffset(); |
| int end = getEndOffset(); |
| int[] bs = new int[end + 1 - start]; |
| int ix = 0; |
| |
| // Breaker should work on the parent element because there may be |
| // a valid breakpoint at the end edge of the view (space, etc.) |
| Element parent = getElement().getParentElement(); |
| int pstart = (parent == null ? start : parent.getStartOffset()); |
| int pend = (parent == null ? end : parent.getEndOffset()); |
| |
| Segment s = getText(pstart, pend); |
| s.first(); |
| BreakIterator breaker = getBreaker(); |
| breaker.setText(s); |
| |
| // Backward search should start from end+1 unless there's NO end+1 |
| int startFrom = end + (pend > end ? 1 : 0); |
| for (;;) { |
| startFrom = breaker.preceding(s.offset + (startFrom - pstart)) |
| + (pstart - s.offset); |
| if (startFrom > start) { |
| // The break spot is within the view |
| bs[ix++] = startFrom; |
| } else { |
| break; |
| } |
| } |
| |
| SegmentCache.releaseSharedSegment(s); |
| breakSpots = new int[ix]; |
| System.arraycopy(bs, 0, breakSpots, 0, ix); |
| } |
| |
| int breakSpot = BreakIterator.DONE; |
| for (int i = 0; i < breakSpots.length; i++) { |
| int bsp = breakSpots[i]; |
| if (bsp <= p1) { |
| if (bsp > p0) { |
| breakSpot = bsp; |
| } |
| break; |
| } |
| } |
| return breakSpot; |
| } |
| |
| /** |
| * Return break iterator appropriate for the current document. |
| * |
| * For non-i18n documents a fast whitespace-based break iterator is used. |
| */ |
| private BreakIterator getBreaker() { |
| Document doc = getDocument(); |
| if ((doc != null) && Boolean.TRUE.equals( |
| doc.getProperty(AbstractDocument.MultiByteProperty))) { |
| Container c = getContainer(); |
| Locale locale = (c == null ? Locale.getDefault() : c.getLocale()); |
| return BreakIterator.getLineInstance(locale); |
| } else { |
| return new WhitespaceBasedBreakIterator(); |
| } |
| } |
| |
| /** |
| * Creates a view that represents a portion of the element. |
| * This is potentially useful during formatting operations |
| * for taking measurements of fragments of the view. If |
| * the view doesn't support fragmenting (the default), it |
| * should return itself. |
| * <p> |
| * This view does support fragmenting. It is implemented |
| * to return a nested class that shares state in this view |
| * representing only a portion of the view. |
| * |
| * @param p0 the starting offset >= 0. This should be a value |
| * greater or equal to the element starting offset and |
| * less than the element ending offset. |
| * @param p1 the ending offset > p0. This should be a value |
| * less than or equal to the elements end offset and |
| * greater than the elements starting offset. |
| * @return the view fragment, or itself if the view doesn't |
| * support breaking into fragments |
| * @see LabelView |
| */ |
| public View createFragment(int p0, int p1) { |
| checkPainter(); |
| Element elem = getElement(); |
| GlyphView v = (GlyphView) clone(); |
| v.offset = p0 - elem.getStartOffset(); |
| v.length = p1 - p0; |
| v.painter = painter.getPainter(v, p0, p1); |
| v.justificationInfo = null; |
| return v; |
| } |
| |
| /** |
| * 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. |
| * |
| * @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 SwingConstants.WEST, SwingConstants.EAST, |
| * SwingConstants.NORTH, or SwingConstants.SOUTH. |
| * @return the location within the model that best represents the next |
| * location visual position. |
| * @exception BadLocationException |
| * @exception IllegalArgumentException for an invalid direction |
| */ |
| public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a, |
| int direction, |
| Position.Bias[] biasRet) |
| throws BadLocationException { |
| |
| return painter.getNextVisualPositionFrom(this, pos, b, a, direction, biasRet); |
| } |
| |
| /** |
| * Gives notification that something was inserted into |
| * the document in a location that this view is responsible for. |
| * This is implemented to call preferenceChanged along the |
| * axis the glyphs are rendered. |
| * |
| * @param e the change information from the associated document |
| * @param a the current allocation of the view |
| * @param f the factory to use to rebuild if the view has children |
| * @see View#insertUpdate |
| */ |
| public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { |
| justificationInfo = null; |
| breakSpots = null; |
| minimumSpan = -1; |
| syncCR(); |
| preferenceChanged(null, true, false); |
| } |
| |
| /** |
| * Gives notification that something was removed from the document |
| * in a location that this view is responsible for. |
| * This is implemented to call preferenceChanged along the |
| * axis the glyphs are rendered. |
| * |
| * @param e the change information from the associated document |
| * @param a the current allocation of the view |
| * @param f the factory to use to rebuild if the view has children |
| * @see View#removeUpdate |
| */ |
| public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { |
| justificationInfo = null; |
| breakSpots = null; |
| minimumSpan = -1; |
| syncCR(); |
| preferenceChanged(null, true, false); |
| } |
| |
| /** |
| * Gives notification from the document that attributes were changed |
| * in a location that this view is responsible for. |
| * This is implemented to call preferenceChanged along both the |
| * horizontal and vertical axis. |
| * |
| * @param e the change information from the associated document |
| * @param a the current allocation of the view |
| * @param f the factory to use to rebuild if the view has children |
| * @see View#changedUpdate |
| */ |
| public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { |
| minimumSpan = -1; |
| syncCR(); |
| preferenceChanged(null, true, true); |
| } |
| |
| // checks if the paragraph is empty and updates impliedCR flag |
| // accordingly |
| private void syncCR() { |
| if (impliedCR) { |
| Element parent = getElement().getParentElement(); |
| impliedCR = (parent != null && parent.getElementCount() > 1); |
| } |
| } |
| |
| /** |
| * Class to hold data needed to justify this GlyphView in a PargraphView.Row |
| */ |
| static class JustificationInfo { |
| //justifiable content start |
| final int start; |
| //justifiable content end |
| final int end; |
| final int leadingSpaces; |
| final int contentSpaces; |
| final int trailingSpaces; |
| final boolean hasTab; |
| final BitSet spaceMap; |
| JustificationInfo(int start, int end, |
| int leadingSpaces, |
| int contentSpaces, |
| int trailingSpaces, |
| boolean hasTab, |
| BitSet spaceMap) { |
| this.start = start; |
| this.end = end; |
| this.leadingSpaces = leadingSpaces; |
| this.contentSpaces = contentSpaces; |
| this.trailingSpaces = trailingSpaces; |
| this.hasTab = hasTab; |
| this.spaceMap = spaceMap; |
| } |
| } |
| |
| |
| |
| JustificationInfo getJustificationInfo(int rowStartOffset) { |
| if (justificationInfo != null) { |
| return justificationInfo; |
| } |
| //states for the parsing |
| final int TRAILING = 0; |
| final int CONTENT = 1; |
| final int SPACES = 2; |
| int startOffset = getStartOffset(); |
| int endOffset = getEndOffset(); |
| Segment segment = getText(startOffset, endOffset); |
| int txtOffset = segment.offset; |
| int txtEnd = segment.offset + segment.count - 1; |
| int startContentPosition = txtEnd + 1; |
| int endContentPosition = txtOffset - 1; |
| int lastTabPosition = txtOffset - 1; |
| int trailingSpaces = 0; |
| int contentSpaces = 0; |
| int leadingSpaces = 0; |
| boolean hasTab = false; |
| BitSet spaceMap = new BitSet(endOffset - startOffset + 1); |
| |
| //we parse conent to the right of the rightmost TAB only. |
| //we are looking for the trailing and leading spaces. |
| //position after the leading spaces (startContentPosition) |
| //position before the trailing spaces (endContentPosition) |
| for (int i = txtEnd, state = TRAILING; i >= txtOffset; i--) { |
| if (' ' == segment.array[i]) { |
| spaceMap.set(i - txtOffset); |
| if (state == TRAILING) { |
| trailingSpaces++; |
| } else if (state == CONTENT) { |
| state = SPACES; |
| leadingSpaces = 1; |
| } else if (state == SPACES) { |
| leadingSpaces++; |
| } |
| } else if ('\t' == segment.array[i]) { |
| hasTab = true; |
| break; |
| } else { |
| if (state == TRAILING) { |
| if ('\n' != segment.array[i] |
| && '\r' != segment.array[i]) { |
| state = CONTENT; |
| endContentPosition = i; |
| } |
| } else if (state == CONTENT) { |
| //do nothing |
| } else if (state == SPACES) { |
| contentSpaces += leadingSpaces; |
| leadingSpaces = 0; |
| } |
| startContentPosition = i; |
| } |
| } |
| |
| SegmentCache.releaseSharedSegment(segment); |
| |
| int startJustifiableContent = -1; |
| if (startContentPosition < txtEnd) { |
| startJustifiableContent = |
| startContentPosition - txtOffset; |
| } |
| int endJustifiableContent = -1; |
| if (endContentPosition > txtOffset) { |
| endJustifiableContent = |
| endContentPosition - txtOffset; |
| } |
| justificationInfo = |
| new JustificationInfo(startJustifiableContent, |
| endJustifiableContent, |
| leadingSpaces, |
| contentSpaces, |
| trailingSpaces, |
| hasTab, |
| spaceMap); |
| return justificationInfo; |
| } |
| |
| // --- variables ------------------------------------------------ |
| |
| /** |
| * Used by paint() to store highlighted view positions |
| */ |
| private byte[] selections = null; |
| |
| int offset; |
| int length; |
| // if it is an implied newline character |
| boolean impliedCR; |
| boolean skipWidth; |
| |
| /** |
| * how to expand tabs |
| */ |
| TabExpander expander; |
| |
| /** Cached minimum x-span value */ |
| private float minimumSpan = -1; |
| |
| /** Cached breakpoints within the view */ |
| private int[] breakSpots = null; |
| |
| /** |
| * location for determining tab expansion against. |
| */ |
| int x; |
| |
| /** |
| * Glyph rendering functionality. |
| */ |
| GlyphPainter painter; |
| |
| /** |
| * The prototype painter used by default. |
| */ |
| static GlyphPainter defaultPainter; |
| |
| private JustificationInfo justificationInfo = null; |
| |
| /** |
| * A class to perform rendering of the glyphs. |
| * This can be implemented to be stateless, or |
| * to hold some information as a cache to |
| * facilitate faster rendering and model/view |
| * translation. At a minimum, the GlyphPainter |
| * allows a View implementation to perform its |
| * duties independant of a particular version |
| * of JVM and selection of capabilities (i.e. |
| * shaping for i18n, etc). |
| * |
| * @since 1.3 |
| */ |
| public static abstract class GlyphPainter { |
| |
| /** |
| * Determine the span the glyphs given a start location |
| * (for tab expansion). |
| */ |
| public abstract float getSpan(GlyphView v, int p0, int p1, TabExpander e, float x); |
| |
| public abstract float getHeight(GlyphView v); |
| |
| public abstract float getAscent(GlyphView v); |
| |
| public abstract float getDescent(GlyphView v); |
| |
| /** |
| * Paint the glyphs representing the given range. |
| */ |
| public abstract void paint(GlyphView v, Graphics g, Shape a, int p0, int p1); |
| |
| /** |
| * Provides a mapping from the document model coordinate space |
| * to the coordinate space of the view mapped to it. |
| * This is shared by the broken views. |
| * |
| * @param v the <code>GlyphView</code> containing the |
| * destination coordinate space |
| * @param pos the position to convert |
| * @param bias either <code>Position.Bias.Forward</code> |
| * or <code>Position.Bias.Backward</code> |
| * @param a Bounds of the View |
| * @return the bounding box of the given position |
| * @exception BadLocationException if the given position does not represent a |
| * valid location in the associated document |
| * @see View#modelToView |
| */ |
| public abstract Shape modelToView(GlyphView v, |
| int pos, Position.Bias bias, |
| Shape a) throws BadLocationException; |
| |
| /** |
| * Provides a mapping from the view coordinate space to the logical |
| * coordinate space of the model. |
| * |
| * @param v the <code>GlyphView</code> to provide a mapping for |
| * @param x the X coordinate |
| * @param y the Y coordinate |
| * @param a the allocated region to render into |
| * @param biasReturn either <code>Position.Bias.Forward</code> |
| * or <code>Position.Bias.Backward</code> |
| * is returned as the zero-th element of this array |
| * @return the location within the model that best represents the |
| * given point of view |
| * @see View#viewToModel |
| */ |
| public abstract int viewToModel(GlyphView v, |
| float x, float y, Shape a, |
| Position.Bias[] biasReturn); |
| |
| /** |
| * Determines the model location that represents the |
| * maximum advance that fits within the given span. |
| * This could be used to break the given view. The result |
| * should be a location just shy of the given advance. This |
| * differs from viewToModel which returns the closest |
| * position which might be proud of the maximum advance. |
| * |
| * @param v the view to find the model location to break at. |
| * @param p0 the location in the model where the |
| * fragment should start it's representation >= 0. |
| * @param x the graphic location along the axis that the |
| * broken view would occupy >= 0. This may be useful for |
| * things like tab calculations. |
| * @param len specifies the distance into the view |
| * where a potential break is desired >= 0. |
| * @return the maximum model location possible for a break. |
| * @see View#breakView |
| */ |
| public abstract int getBoundedPosition(GlyphView v, int p0, float x, float len); |
| |
| /** |
| * Create a painter to use for the given GlyphView. If |
| * the painter carries state it can create another painter |
| * to represent a new GlyphView that is being created. If |
| * the painter doesn't hold any significant state, it can |
| * return itself. The default behavior is to return itself. |
| * @param v the <code>GlyphView</code> to provide a painter for |
| * @param p0 the starting document offset >= 0 |
| * @param p1 the ending document offset >= p0 |
| */ |
| public GlyphPainter getPainter(GlyphView v, int p0, int p1) { |
| return this; |
| } |
| |
| /** |
| * 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. |
| * |
| * @param v the view to use |
| * @param pos the position to convert >= 0 |
| * @param b either <code>Position.Bias.Forward</code> |
| * or <code>Position.Bias.Backward</code> |
| * @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 SwingConstants.WEST, SwingConstants.EAST, |
| * SwingConstants.NORTH, or SwingConstants.SOUTH. |
| * @param biasRet either <code>Position.Bias.Forward</code> |
| * or <code>Position.Bias.Backward</code> |
| * is returned as the zero-th element of this array |
| * @return the location within the model that best represents the next |
| * location visual position. |
| * @exception BadLocationException |
| * @exception IllegalArgumentException for an invalid direction |
| */ |
| public int getNextVisualPositionFrom(GlyphView v, int pos, Position.Bias b, Shape a, |
| int direction, |
| Position.Bias[] biasRet) |
| throws BadLocationException { |
| |
| int startOffset = v.getStartOffset(); |
| int endOffset = v.getEndOffset(); |
| Segment text; |
| |
| switch (direction) { |
| case View.NORTH: |
| case View.SOUTH: |
| if (pos != -1) { |
| // Presumably pos is between startOffset and endOffset, |
| // since GlyphView is only one line, we won't contain |
| // the position to the nort/south, therefore return -1. |
| return -1; |
| } |
| Container container = v.getContainer(); |
| |
| if (container instanceof JTextComponent) { |
| Caret c = ((JTextComponent)container).getCaret(); |
| Point magicPoint; |
| magicPoint = (c != null) ? c.getMagicCaretPosition() :null; |
| |
| if (magicPoint == null) { |
| biasRet[0] = Position.Bias.Forward; |
| return startOffset; |
| } |
| int value = v.viewToModel(magicPoint.x, 0f, a, biasRet); |
| return value; |
| } |
| break; |
| case View.EAST: |
| if(startOffset == v.getDocument().getLength()) { |
| if(pos == -1) { |
| biasRet[0] = Position.Bias.Forward; |
| return startOffset; |
| } |
| // End case for bidi text where newline is at beginning |
| // of line. |
| return -1; |
| } |
| if(pos == -1) { |
| biasRet[0] = Position.Bias.Forward; |
| return startOffset; |
| } |
| if(pos == endOffset) { |
| return -1; |
| } |
| if(++pos == endOffset) { |
| // Assumed not used in bidi text, GlyphPainter2 will |
| // override as necessary, therefore return -1. |
| return -1; |
| } |
| else { |
| biasRet[0] = Position.Bias.Forward; |
| } |
| return pos; |
| case View.WEST: |
| if(startOffset == v.getDocument().getLength()) { |
| if(pos == -1) { |
| biasRet[0] = Position.Bias.Forward; |
| return startOffset; |
| } |
| // End case for bidi text where newline is at beginning |
| // of line. |
| return -1; |
| } |
| if(pos == -1) { |
| // Assumed not used in bidi text, GlyphPainter2 will |
| // override as necessary, therefore return -1. |
| biasRet[0] = Position.Bias.Forward; |
| return endOffset - 1; |
| } |
| if(pos == startOffset) { |
| return -1; |
| } |
| biasRet[0] = Position.Bias.Forward; |
| return (pos - 1); |
| default: |
| throw new IllegalArgumentException("Bad direction: " + direction); |
| } |
| return pos; |
| |
| } |
| } |
| } |