--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/javax/swing/text/GlyphView.java Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,1334 @@
+/*
+ * Copyright 1999-2006 Sun Microsystems, Inc. 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. Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.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 sun.swing.SwingUtilities2;
+
+/**
+ * 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 instanceof JTextComponent) {
+ JTextComponent tc = (JTextComponent) c;
+ if (!tc.isEnabled()) {
+ fg = tc.getDisabledTextColor();
+ }
+ }
+ 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) && (getBreakSpot(p0, p1) != BreakIterator.DONE)) ?
+ View.ExcellentBreakWeight : View.BadBreakWeight;
+ }
+ 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;
+ private static final String IMPLIED_CR = "CR";
+ 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;
+
+ }
+ }
+}