Java tutorial
/* * Copyright (c) 1997, 2018, 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.util.Arrays; import java.awt.*; import java.awt.font.TextAttribute; import java.awt.geom.Rectangle2D; import javax.swing.event.*; import javax.swing.SizeRequirements; /** * View of a simple line-wrapping paragraph that supports * multiple fonts, colors, components, icons, etc. It is * basically a vertical box with a margin around it. The * contents of the box are a bunch of rows which are special * horizontal boxes. This view creates a collection of * views that represent the child elements of the paragraph * element. Each of these views are placed into a row * directly if they will fit, otherwise the <code>breakView</code> * method is called to try and carve the view into pieces * that fit. * * @author Timothy Prinzing * @author Scott Violet * @author Igor Kushnirskiy * @see View */ public class ParagraphView extends FlowView implements TabExpander { /** * Constructs a <code>ParagraphView</code> for the given element. * * @param elem the element that this view is responsible for */ public ParagraphView(Element elem) { super(elem, View.Y_AXIS); setPropertiesFromAttributes(); Document doc = elem.getDocument(); Object i18nFlag = doc.getProperty(AbstractDocument.I18NProperty); if ((i18nFlag != null) && i18nFlag.equals(Boolean.TRUE)) { try { // the classname should probably come from a property file. strategy = new TextLayoutStrategy(); } catch (Throwable e) { throw new StateInvariantError("ParagraphView: Can't create i18n strategy: " + e.getMessage()); } } } /** * Sets the type of justification. * * @param j one of the following values: * <ul> * <li><code>StyleConstants.ALIGN_LEFT</code> * <li><code>StyleConstants.ALIGN_CENTER</code> * <li><code>StyleConstants.ALIGN_RIGHT</code> * </ul> */ protected void setJustification(int j) { justification = j; } /** * Sets the line spacing. * * @param ls the value is a factor of the line hight */ protected void setLineSpacing(float ls) { lineSpacing = ls; } /** * Sets the indent on the first line. * * @param fi the value in points */ protected void setFirstLineIndent(float fi) { firstLineIndent = (int) fi; } /** * Set the cached properties from the attributes. */ protected void setPropertiesFromAttributes() { AttributeSet attr = getAttributes(); if (attr != null) { setParagraphInsets(attr); Integer a = (Integer) attr.getAttribute(StyleConstants.Alignment); int alignment; if (a == null) { Document doc = getElement().getDocument(); Object o = doc.getProperty(TextAttribute.RUN_DIRECTION); if ((o != null) && o.equals(TextAttribute.RUN_DIRECTION_RTL)) { alignment = StyleConstants.ALIGN_RIGHT; } else { alignment = StyleConstants.ALIGN_LEFT; } } else { alignment = a.intValue(); } setJustification(alignment); setLineSpacing(StyleConstants.getLineSpacing(attr)); setFirstLineIndent(StyleConstants.getFirstLineIndent(attr)); } } /** * Returns the number of views that this view is * responsible for. * The child views of the paragraph are rows which * have been used to arrange pieces of the <code>View</code>s * that represent the child elements. This is the number * of views that have been tiled in two dimensions, * and should be equivalent to the number of child elements * to the element this view is responsible for. * * @return the number of views that this <code>ParagraphView</code> * is responsible for */ protected int getLayoutViewCount() { return layoutPool.getViewCount(); } /** * Returns the view at a given <code>index</code>. * The child views of the paragraph are rows which * have been used to arrange pieces of the <code>Views</code> * that represent the child elements. This methods returns * the view responsible for the child element index * (prior to breaking). These are the Views that were * produced from a factory (to represent the child * elements) and used for layout. * * @param index the <code>index</code> of the desired view * @return the view at <code>index</code> */ protected View getLayoutView(int index) { return layoutPool.getView(index); } /** * Returns the next visual position for the cursor, in * either the east or west direction. * Overridden from <code>CompositeView</code>. * @param pos position into the model * @param b either <code>Position.Bias.Forward</code> or * <code>Position.Bias.Backward</code> * @param a the allocated region to render into * @param direction either <code>SwingConstants.NORTH</code> * or <code>SwingConstants.SOUTH</code> * @param biasRet an array containing the bias that were checked * in this method * @return the location in the model that represents the * next location visual position */ @SuppressWarnings("deprecation") protected int getNextNorthSouthVisualPositionFrom(int pos, Position.Bias b, Shape a, int direction, Position.Bias[] biasRet) throws BadLocationException { int vIndex; if (pos == -1) { vIndex = (direction == NORTH) ? getViewCount() - 1 : 0; } else { if (b == Position.Bias.Backward && pos > 0) { vIndex = getViewIndexAtPosition(pos - 1); } else { vIndex = getViewIndexAtPosition(pos); } if (direction == NORTH) { if (vIndex == 0) { return -1; } vIndex--; } else if (++vIndex >= getViewCount()) { return -1; } } // vIndex gives index of row to look in. JTextComponent text = (JTextComponent) getContainer(); Caret c = text.getCaret(); Point magicPoint; magicPoint = (c != null) ? c.getMagicCaretPosition() : null; int x; if (magicPoint == null) { Shape posBounds; try { posBounds = text.getUI().modelToView(text, pos, b); } catch (BadLocationException exc) { posBounds = null; } if (posBounds == null) { x = 0; } else { x = posBounds.getBounds().x; } } else { x = magicPoint.x; } return getClosestPositionTo(pos, b, a, direction, biasRet, vIndex, x); } /** * Returns the closest model position to <code>x</code>. * <code>rowIndex</code> gives the index of the view that corresponds * that should be looked in. * @param pos position into the model * @param b the bias * @param a the allocated region to render into * @param direction one of the following values: * <ul> * <li><code>SwingConstants.NORTH</code> * <li><code>SwingConstants.SOUTH</code> * </ul> * @param biasRet an array containing the bias that were checked * in this method * @param rowIndex the index of the view * @param x the x coordinate of interest * @throws BadLocationException if a bad location is encountered * @return the closest model position to <code>x</code> */ // NOTE: This will not properly work if ParagraphView contains // other ParagraphViews. It won't raise, but this does not message // the children views with getNextVisualPositionFrom. @SuppressWarnings("deprecation") protected int getClosestPositionTo(int pos, Position.Bias b, Shape a, int direction, Position.Bias[] biasRet, int rowIndex, int x) throws BadLocationException { JTextComponent text = (JTextComponent) getContainer(); Document doc = getDocument(); View row = getView(rowIndex); int lastPos = -1; // This could be made better to check backward positions too. biasRet[0] = Position.Bias.Forward; for (int vc = 0, numViews = row.getViewCount(); vc < numViews; vc++) { View v = row.getView(vc); int start = v.getStartOffset(); boolean ltr = AbstractDocument.isLeftToRight(doc, start, start + 1); if (ltr) { lastPos = start; for (int end = v.getEndOffset(); lastPos < end; lastPos++) { float xx = text.modelToView(lastPos).getBounds().x; if (xx >= x) { while (++lastPos < end && text.modelToView(lastPos).getBounds().x == xx) { } return --lastPos; } } lastPos--; } else { for (lastPos = v.getEndOffset() - 1; lastPos >= start; lastPos--) { float xx = text.modelToView(lastPos).getBounds().x; if (xx >= x) { while (--lastPos >= start && text.modelToView(lastPos).getBounds().x == xx) { } return ++lastPos; } } lastPos++; } } if (lastPos == -1) { return getStartOffset(); } return lastPos; } /** * Determines in which direction the next view lays. * Consider the <code>View</code> at index n. * Typically the <code>View</code>s are layed out * from left to right, so that the <code>View</code> * to the EAST will be at index n + 1, and the * <code>View</code> to the WEST will be at index n - 1. * In certain situations, such as with bidirectional text, * it is possible that the <code>View</code> to EAST is not * at index n + 1, but rather at index n - 1, * or that the <code>View</code> to the WEST is not at * index n - 1, but index n + 1. In this case this method * would return true, indicating the <code>View</code>s are * layed out in descending order. * <p> * This will return true if the text is layed out right * to left at position, otherwise false. * * @param position position into the model * @param bias either <code>Position.Bias.Forward</code> or * <code>Position.Bias.Backward</code> * @return true if the text is layed out right to left at * position, otherwise false. */ protected boolean flipEastAndWestAtEnds(int position, Position.Bias bias) { Document doc = getDocument(); position = getStartOffset(); return !AbstractDocument.isLeftToRight(doc, position, position + 1); } // --- FlowView methods --------------------------------------------- /** * Fetches the constraining span to flow against for * the given child index. * @param index the index of the view being queried * @return the constraining span for the given view at * <code>index</code> * @since 1.3 */ public int getFlowSpan(int index) { View child = getView(index); int adjust = 0; if (child instanceof Row) { Row row = (Row) child; adjust = row.getLeftInset() + row.getRightInset(); } return (layoutSpan == Integer.MAX_VALUE) ? layoutSpan : (layoutSpan - adjust); } /** * Fetches the location along the flow axis that the * flow span will start at. * @param index the index of the view being queried * @return the location for the given view at * <code>index</code> * @since 1.3 */ public int getFlowStart(int index) { View child = getView(index); int adjust = 0; if (child instanceof Row) { Row row = (Row) child; adjust = row.getLeftInset(); } return tabBase + adjust; } /** * Create a <code>View</code> that should be used to hold a * a row's worth of children in a flow. * @return the new <code>View</code> * @since 1.3 */ protected View createRow() { return new Row(getElement()); } // --- TabExpander methods ------------------------------------------ /** * Returns the next tab stop position given a reference position. * This view implements the tab coordinate system, and calls * <code>getTabbedSpan</code> on the logical children in the process * of layout to determine the desired span of the children. The * logical children can delegate their tab expansion upward to * the paragraph which knows how to expand tabs. * <code>LabelView</code> is an example of a view that delegates * its tab expansion needs upward to the paragraph. * <p> * This is implemented to try and locate a <code>TabSet</code> * in the paragraph element's attribute set. If one can be * found, its settings will be used, otherwise a default expansion * will be provided. The base location for tab expansion * is the left inset from the paragraphs most recent allocation * (which is what the layout of the children is based upon). * * @param x the X reference position * @param tabOffset the position within the text stream * that the tab occurred at >= 0 * @return the trailing end of the tab expansion >= 0 * @see TabSet * @see TabStop * @see LabelView */ public float nextTabStop(float x, int tabOffset) { // If the text isn't left justified, offset by 10 pixels! if (justification != StyleConstants.ALIGN_LEFT) return x + 10.0f; x -= tabBase; TabSet tabs = getTabSet(); if (tabs == null) { // a tab every 72 pixels. return (float) (tabBase + (((int) x / 72 + 1) * 72)); } TabStop tab = tabs.getTabAfter(x + .01f); if (tab == null) { // no tab, do a default of 5 pixels. // Should this cause a wrapping of the line? return tabBase + x + 5.0f; } int alignment = tab.getAlignment(); int offset; switch (alignment) { default: case TabStop.ALIGN_LEFT: // Simple case, left tab. return tabBase + tab.getPosition(); case TabStop.ALIGN_BAR: // PENDING: what does this mean? return tabBase + tab.getPosition(); case TabStop.ALIGN_RIGHT: case TabStop.ALIGN_CENTER: offset = findOffsetToCharactersInString(tabChars, tabOffset + 1); break; case TabStop.ALIGN_DECIMAL: offset = findOffsetToCharactersInString(tabDecimalChars, tabOffset + 1); break; } if (offset == -1) { offset = getEndOffset(); } float charsSize = getPartialSize(tabOffset + 1, offset); switch (alignment) { case TabStop.ALIGN_RIGHT: case TabStop.ALIGN_DECIMAL: // right and decimal are treated the same way, the new // position will be the location of the tab less the // partialSize. return tabBase + Math.max(x, tab.getPosition() - charsSize); case TabStop.ALIGN_CENTER: // Similar to right, but half the partialSize. return tabBase + Math.max(x, tab.getPosition() - charsSize / 2.0f); } // will never get here! return x; } /** * Gets the <code>Tabset</code> to be used in calculating tabs. * * @return the <code>TabSet</code> */ protected TabSet getTabSet() { return StyleConstants.getTabSet(getElement().getAttributes()); } /** * Returns the size used by the views between * <code>startOffset</code> and <code>endOffset</code>. * This uses <code>getPartialView</code> to calculate the * size if the child view implements the * <code>TabableView</code> interface. If a * size is needed and a <code>View</code> does not implement * the <code>TabableView</code> interface, * the <code>preferredSpan</code> will be used. * * @param startOffset the starting document offset >= 0 * @param endOffset the ending document offset >= startOffset * @return the size >= 0 */ protected float getPartialSize(int startOffset, int endOffset) { float size = 0.0f; int viewIndex; int numViews = getViewCount(); View view; int viewEnd; int tempEnd; // Have to search layoutPool! // PENDING: when ParagraphView supports breaking location // into layoutPool will have to change! viewIndex = getElement().getElementIndex(startOffset); numViews = layoutPool.getViewCount(); while (startOffset < endOffset && viewIndex < numViews) { view = layoutPool.getView(viewIndex++); viewEnd = view.getEndOffset(); tempEnd = Math.min(endOffset, viewEnd); if (view instanceof TabableView) size += ((TabableView) view).getPartialSpan(startOffset, tempEnd); else if (startOffset == view.getStartOffset() && tempEnd == view.getEndOffset()) size += view.getPreferredSpan(View.X_AXIS); else // PENDING: should we handle this better? return 0.0f; startOffset = viewEnd; } return size; } /** * Finds the next character in the document with a character in * <code>string</code>, starting at offset <code>start</code>. If * there are no characters found, -1 will be returned. * * @param string the string of characters * @param start where to start in the model >= 0 * @return the document offset, or -1 if no characters found */ protected int findOffsetToCharactersInString(char[] string, int start) { int stringLength = string.length; int end = getEndOffset(); Segment seg = new Segment(); try { getDocument().getText(start, end - start, seg); } catch (BadLocationException ble) { return -1; } for (int counter = seg.offset, maxCounter = seg.offset + seg.count; counter < maxCounter; counter++) { char currentChar = seg.array[counter]; for (int subCounter = 0; subCounter < stringLength; subCounter++) { if (currentChar == string[subCounter]) return counter - seg.offset + start; } } // No match. return -1; } /** * Returns where the tabs are calculated from. * @return where tabs are calculated from */ protected float getTabBase() { return (float) tabBase; } // ---- View methods ---------------------------------------------------- /** * Renders using the given rendering surface and area on that * surface. This is implemented to delegate to the superclass * after stashing the base coordinate for tab calculations. * * @param g the rendering surface to use * @param a the allocated region to render into * @see View#paint */ public void paint(Graphics g, Shape a) { Rectangle alloc = (a instanceof Rectangle) ? (Rectangle) a : a.getBounds(); tabBase = alloc.x + getLeftInset(); super.paint(g, a); // line with the negative firstLineIndent value needs // special handling if (firstLineIndent < 0) { Shape sh = getChildAllocation(0, a); if ((sh != null) && sh.intersects(alloc)) { int x = alloc.x + getLeftInset() + firstLineIndent; int y = alloc.y + getTopInset(); Rectangle clip = g.getClipBounds(); tempRect.x = x + getOffset(X_AXIS, 0); tempRect.y = y + getOffset(Y_AXIS, 0); tempRect.width = getSpan(X_AXIS, 0) - firstLineIndent; tempRect.height = getSpan(Y_AXIS, 0); if (tempRect.intersects(clip)) { tempRect.x = tempRect.x - firstLineIndent; paintChild(g, tempRect, 0); } } } } /** * Determines the desired alignment for this view along an * axis. This is implemented to give the alignment to the * center of the first row along the y axis, and the default * along the x axis. * * @param axis may be either <code>View.X_AXIS</code> or * <code>View.Y_AXIS</code> * @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) { switch (axis) { case Y_AXIS: float a = 0.5f; if (getViewCount() != 0) { int paragraphSpan = (int) getPreferredSpan(View.Y_AXIS); View v = getView(0); int rowSpan = (int) v.getPreferredSpan(View.Y_AXIS); a = (paragraphSpan != 0) ? ((float) (rowSpan / 2)) / paragraphSpan : 0; } return a; case X_AXIS: return 0.5f; default: throw new IllegalArgumentException("Invalid axis: " + axis); } } /** * Breaks this view on the given axis at the given length. * <p> * <code>ParagraphView</code> instances are breakable * along the <code>Y_AXIS</code> only, and only if * <code>len</code> is after the first line. * * @param axis may be either <code>View.X_AXIS</code> * or <code>View.Y_AXIS</code> * @param len specifies where a potential break is desired * along the given axis >= 0 * @param a the current allocation of the view * @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, float len, Shape a) { if (axis == View.Y_AXIS) { if (a != null) { Rectangle alloc = a.getBounds(); setSize(alloc.width, alloc.height); } // Determine what row to break on. // PENDING(prinz) add break support return this; } return this; } /** * Gets the break weight for a given location. * <p> * <code>ParagraphView</code> instances are breakable * along the <code>Y_AXIS</code> only, and only if * <code>len</code> is after the first row. If the length * is less than one row, a value of <code>BadBreakWeight</code> * is returned. * * @param axis may be either <code>View.X_AXIS</code> * or <code>View.Y_AXIS</code> * @param len specifies where a potential break is desired >= 0 * @return a value indicating the attractiveness of breaking here; * either <code>GoodBreakWeight</code> or <code>BadBreakWeight</code> * @see View#getBreakWeight */ public int getBreakWeight(int axis, float len) { if (axis == View.Y_AXIS) { // PENDING(prinz) make this return a reasonable value // when paragraph breaking support is re-implemented. // If less than one row, bad weight value should be // returned. //return GoodBreakWeight; return BadBreakWeight; } return BadBreakWeight; } /** * Calculate the needs for the paragraph along the minor axis. * * <p>This uses size requirements of the superclass, modified to take into * account the non-breakable areas at the adjacent views edges. The minimal * size requirements for such views should be no less than the sum of all * adjacent fragments.</p> * * <p>If the {@code axis} parameter is neither {@code View.X_AXIS} nor * {@code View.Y_AXIS}, {@link IllegalArgumentException} is thrown. If the * {@code r} parameter is {@code null,} a new {@code SizeRequirements} * object is created, otherwise the supplied {@code SizeRequirements} * object is returned.</p> * * @param axis the minor axis * @param r the input {@code SizeRequirements} object * @return the new or adjusted {@code SizeRequirements} object * @throws IllegalArgumentException if the {@code axis} parameter is invalid */ @Override protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) { r = super.calculateMinorAxisRequirements(axis, r); float min = 0; float glue = 0; int n = getLayoutViewCount(); for (int i = 0; i < n; i++) { View v = getLayoutView(i); float span = v.getMinimumSpan(axis); if (v.getBreakWeight(axis, 0, v.getMaximumSpan(axis)) > View.BadBreakWeight) { // find the longest non-breakable fragments at the view edges int p0 = v.getStartOffset(); int p1 = v.getEndOffset(); float start = findEdgeSpan(v, axis, p0, p0, p1); float end = findEdgeSpan(v, axis, p1, p0, p1); glue += start; min = Math.max(min, Math.max(span, glue)); glue = end; } else { // non-breakable view glue += span; min = Math.max(min, glue); } } r.minimum = Math.max(r.minimum, (int) min); r.preferred = Math.max(r.minimum, r.preferred); r.maximum = Math.max(r.preferred, r.maximum); return r; } /** * Binary search for the longest non-breakable fragment at the view edge. */ private float findEdgeSpan(View v, int axis, int fp, int p0, int p1) { int len = p1 - p0; if (len <= 1) { // further fragmentation is not possible return v.getMinimumSpan(axis); } else { int mid = p0 + len / 2; boolean startEdge = mid > fp; // initial view is breakable hence must support fragmentation View f = startEdge ? v.createFragment(fp, mid) : v.createFragment(mid, fp); boolean breakable = f.getBreakWeight(axis, 0, f.getMaximumSpan(axis)) > View.BadBreakWeight; if (breakable == startEdge) { p1 = mid; } else { p0 = mid; } return findEdgeSpan(f, axis, fp, p0, p1); } } /** * Gives notification from the document that attributes were changed * in a location that this view is responsible for. * * @param changes 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 changes, Shape a, ViewFactory f) { // update any property settings stored, and layout should be // recomputed setPropertiesFromAttributes(); layoutChanged(X_AXIS); layoutChanged(Y_AXIS); super.changedUpdate(changes, a, f); } // --- variables ----------------------------------------------- private int justification; private float lineSpacing; /** Indentation for the first line, from the left inset. */ protected int firstLineIndent = 0; /** * Used by the TabExpander functionality to determine * where to base the tab calculations. This is basically * the location of the left side of the paragraph. */ private int tabBase; /** * Used to create an i18n-based layout strategy */ static Class<?> i18nStrategy; /** Used for searching for a tab. */ static char[] tabChars; /** Used for searching for a tab or decimal character. */ static char[] tabDecimalChars; static { tabChars = new char[1]; tabChars[0] = '\t'; tabDecimalChars = new char[2]; tabDecimalChars[0] = '\t'; tabDecimalChars[1] = '.'; } /** * Internally created view that has the purpose of holding * the views that represent the children of the paragraph * that have been arranged in rows. */ class Row extends BoxView { Row(Element elem) { super(elem, View.X_AXIS); } /** * This is reimplemented to do nothing since the * paragraph fills in the row with its needed * children. */ protected void loadChildren(ViewFactory f) { } /** * Fetches the attributes to use when rendering. This view * isn't directly responsible for an element so it returns * the outer classes attributes. */ public AttributeSet getAttributes() { View p = getParent(); return (p != null) ? p.getAttributes() : null; } public float getAlignment(int axis) { if (axis == View.X_AXIS) { switch (justification) { case StyleConstants.ALIGN_LEFT: return 0; case StyleConstants.ALIGN_RIGHT: return 1; case StyleConstants.ALIGN_CENTER: return 0.5f; case StyleConstants.ALIGN_JUSTIFIED: float rv = 0.5f; //if we can justifiy the content always align to //the left. if (isJustifiableDocument()) { rv = 0f; } return rv; } } return super.getAlignment(axis); } /** * Provides a mapping from the document model coordinate space * to the coordinate space of the view mapped to it. This is * implemented to let the superclass find the position along * the major axis and the allocation of the row is used * along the minor axis, so that even though the children * are different heights they all get the same caret height. * * @param pos the position to convert * @param a the allocated region to render into * @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 { Rectangle r = a.getBounds(); View v = getViewAtPosition(pos, r); if ((v != null) && (!v.getElement().isLeaf())) { // Don't adjust the height if the view represents a branch. return super.modelToView(pos, a, b); } r = a.getBounds(); int height = r.height; int y = r.y; Shape loc = super.modelToView(pos, a, b); Rectangle2D bounds = loc.getBounds2D(); bounds.setRect(bounds.getX(), y, bounds.getWidth(), height); return bounds; } /** * Range represented by a row in the paragraph is only * a subset of the total range of the paragraph element. */ public int getStartOffset() { int offs = Integer.MAX_VALUE; int n = getViewCount(); for (int i = 0; i < n; i++) { View v = getView(i); offs = Math.min(offs, v.getStartOffset()); } return offs; } public int getEndOffset() { int offs = 0; int n = getViewCount(); for (int i = 0; i < n; i++) { View v = getView(i); offs = Math.max(offs, v.getEndOffset()); } return offs; } /** * Perform layout for the minor axis of the box (i.e. the * axis orthogonal to the axis that it represents). The results * of the layout should be placed in the given arrays which represent * the allocations to the children along the minor axis. * <p> * This is implemented to do a baseline layout of the children * by calling BoxView.baselineLayout. * * @param targetSpan the total span given to the view, which * would be used to layout the children. * @param axis the axis being layed out. * @param offsets the offsets from the origin of the view for * each of the child views. This is a return value and is * filled in by the implementation of this method. * @param spans the span of each child view. This is a return * value and is filled in by the implementation of this method. * @return the offset and span for each child view in the * offsets and spans parameters */ protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { baselineLayout(targetSpan, axis, offsets, spans); } protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) { return baselineRequirements(axis, r); } private boolean isLastRow() { View parent; return ((parent = getParent()) == null || this == parent.getView(parent.getViewCount() - 1)); } private boolean isBrokenRow() { boolean rv = false; int viewsCount = getViewCount(); if (viewsCount > 0) { View lastView = getView(viewsCount - 1); if (lastView.getBreakWeight(X_AXIS, 0, 0) >= ForcedBreakWeight) { rv = true; } } return rv; } private boolean isJustifiableDocument() { return (!Boolean.TRUE.equals(getDocument().getProperty(AbstractDocument.I18NProperty))); } /** * Whether we need to justify this {@code Row}. * At this time (jdk1.6) we support justification on for non * 18n text. * * @return {@code true} if this {@code Row} should be justified. */ private boolean isJustifyEnabled() { boolean ret = (justification == StyleConstants.ALIGN_JUSTIFIED); //no justification for i18n documents ret = ret && isJustifiableDocument(); //no justification for the last row ret = ret && !isLastRow(); //no justification for the broken rows ret = ret && !isBrokenRow(); return ret; } //Calls super method after setting spaceAddon to 0. //Justification should not affect MajorAxisRequirements @Override protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) { int[] oldJustficationData = justificationData; justificationData = null; SizeRequirements ret = super.calculateMajorAxisRequirements(axis, r); if (isJustifyEnabled()) { justificationData = oldJustficationData; } return ret; } @Override protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { int[] oldJustficationData = justificationData; justificationData = null; super.layoutMajorAxis(targetSpan, axis, offsets, spans); if (!isJustifyEnabled()) { return; } int currentSpan = 0; for (int span : spans) { currentSpan += span; } if (currentSpan == targetSpan) { //no need to justify return; } // we justify text by enlarging spaces by the {@code spaceAddon}. // justification is started to the right of the rightmost TAB. // leading and trailing spaces are not extendable. // // GlyphPainter1 uses // justificationData // for all painting and measurement. int extendableSpaces = 0; int startJustifiableContent = -1; int endJustifiableContent = -1; int lastLeadingSpaces = 0; int rowStartOffset = getStartOffset(); int rowEndOffset = getEndOffset(); int[] spaceMap = new int[rowEndOffset - rowStartOffset]; Arrays.fill(spaceMap, 0); for (int i = getViewCount() - 1; i >= 0; i--) { View view = getView(i); if (view instanceof GlyphView) { GlyphView.JustificationInfo justificationInfo = ((GlyphView) view) .getJustificationInfo(rowStartOffset); final int viewStartOffset = view.getStartOffset(); final int offset = viewStartOffset - rowStartOffset; for (int j = 0; j < justificationInfo.spaceMap.length(); j++) { if (justificationInfo.spaceMap.get(j)) { spaceMap[j + offset] = 1; } } if (startJustifiableContent > 0) { if (justificationInfo.end >= 0) { extendableSpaces += justificationInfo.trailingSpaces; } else { lastLeadingSpaces += justificationInfo.trailingSpaces; } } if (justificationInfo.start >= 0) { startJustifiableContent = justificationInfo.start + viewStartOffset; extendableSpaces += lastLeadingSpaces; } if (justificationInfo.end >= 0 && endJustifiableContent < 0) { endJustifiableContent = justificationInfo.end + viewStartOffset; } extendableSpaces += justificationInfo.contentSpaces; lastLeadingSpaces = justificationInfo.leadingSpaces; if (justificationInfo.hasTab) { break; } } } if (extendableSpaces <= 0) { //there is nothing we can do to justify return; } int adjustment = (targetSpan - currentSpan); int spaceAddon = (extendableSpaces > 0) ? adjustment / extendableSpaces : 0; int spaceAddonLeftoverEnd = -1; for (int i = startJustifiableContent - rowStartOffset, leftover = adjustment - spaceAddon * extendableSpaces; leftover > 0; leftover -= spaceMap[i], i++) { spaceAddonLeftoverEnd = i; } if (spaceAddon > 0 || spaceAddonLeftoverEnd >= 0) { justificationData = (oldJustficationData != null) ? oldJustficationData : new int[END_JUSTIFIABLE + 1]; justificationData[SPACE_ADDON] = spaceAddon; justificationData[SPACE_ADDON_LEFTOVER_END] = spaceAddonLeftoverEnd; justificationData[START_JUSTIFIABLE] = startJustifiableContent - rowStartOffset; justificationData[END_JUSTIFIABLE] = endJustifiableContent - rowStartOffset; super.layoutMajorAxis(targetSpan, axis, offsets, spans); } } //for justified row we assume the maximum horizontal span //is MAX_VALUE. @Override public float getMaximumSpan(int axis) { float ret; if (View.X_AXIS == axis && isJustifyEnabled()) { ret = Float.MAX_VALUE; } else { ret = super.getMaximumSpan(axis); } return ret; } /** * Fetches the child view index representing the given position in * the model. * * @param pos the position >= 0 * @return index of the view representing the given position, or * -1 if no view represents that position */ protected int getViewIndexAtPosition(int pos) { // This is expensive, but are views are not necessarily layed // out in model order. if (pos < getStartOffset() || pos >= getEndOffset()) return -1; for (int counter = getViewCount() - 1; counter >= 0; counter--) { View v = getView(counter); if (pos >= v.getStartOffset() && pos < v.getEndOffset()) { return counter; } } return -1; } /** * Gets the left inset. * * @return the inset */ protected short getLeftInset() { View parentView; int adjustment = 0; if ((parentView = getParent()) != null) { //use firstLineIdent for the first row if (this == parentView.getView(0)) { adjustment = firstLineIndent; } } return (short) (super.getLeftInset() + adjustment); } protected short getBottomInset() { return (short) (super.getBottomInset() + ((minorRequest != null) ? minorRequest.preferred : 0) * lineSpacing); } static final int SPACE_ADDON = 0; static final int SPACE_ADDON_LEFTOVER_END = 1; static final int START_JUSTIFIABLE = 2; //this should be the last index in justificationData static final int END_JUSTIFIABLE = 3; int[] justificationData = null; } }