home *** CD-ROM | disk | FTP | other *** search
/ Java 1.2 How-To / JavaHowTo.iso / 3rdParty / jbuilder / unsupported / JDK1.2beta3 / SOURCE / SRC.ZIP / java / awt / font / LineBreakMeasurer.java < prev    next >
Encoding:
Java Source  |  1998-03-20  |  16.4 KB  |  442 lines

  1. /*
  2.  * @(#)LineBreakMeasurer.java    1.4 98/03/18
  3.  *
  4.  * Copyright 1998 by Sun Microsystems, Inc.,
  5.  * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
  6.  * All rights reserved.
  7.  *
  8.  * This software is the confidential and proprietary information
  9.  * of Sun Microsystems, Inc. ("Confidential Information").  You
  10.  * shall not disclose such Confidential Information and shall use
  11.  * it only in accordance with the terms of the license agreement
  12.  * you entered into with Sun.
  13.  */
  14.  
  15. /*
  16.  * (C) Copyright Taligent, Inc. 1996 - 1997, All Rights Reserved
  17.  * (C) Copyright IBM Corp. 1996 - 1998, All Rights Reserved
  18.  *
  19.  * The original version of this source code and documentation is
  20.  * copyrighted and owned by Taligent, Inc., a wholly-owned subsidiary
  21.  * of IBM. These materials are provided under terms of a License
  22.  * Agreement between Taligent and Sun. This technology is protected
  23.  * by multiple US and International patents.
  24.  *
  25.  * This notice and attribution to Taligent may not be removed.
  26.  * Taligent is a registered trademark of Taligent, Inc.
  27.  *
  28.  */
  29.  
  30. package java.awt.font;
  31.  
  32. import java.text.BreakIterator;
  33. import java.text.CharacterIterator;
  34. import java.text.AttributedCharacterIterator;
  35.  
  36. /**
  37.  * LineBreakMeasurer allows styled text to be broken into lines (or
  38.  * segments) which fit within a given visual advance.  This is useful
  39.  * for clients who wish to display a paragraph of text which fits
  40.  * within a specific width, called the <b>wrapping width</b>.
  41.  * <p>
  42.  * LineBreakMeasurer is constructed with an iterator over styled text.
  43.  * The iterator's range should be a single paragraph in the text.
  44.  * LineBreakMeasurer maintains a position in the text for the start of
  45.  * of the next text segment;  initially, this position is the start of
  46.  * text.  Paragraphs are assigned an overall direction (either
  47.  * left-to-right or right-to-left) according to the bidirectional
  48.  * formatting rules.  All segments obtained from a paragraph have the
  49.  * same direction as the paragraph.
  50.  * <p>
  51.  * Segments of text are obtained by calling the method nextLayout(),
  52.  * which will return a TextLayout representing the text that fits
  53.  * within the wrapping width.  nextLayout() moves the current position
  54.  * to the end of the layout returned from nextLayout().
  55.  * <p>
  56.  * LineBreakMeasurer implements the most commonly used line-breaking
  57.  * policy: Every word which will fit within the wrapping width is
  58.  * placed on the line. If the first word will not fit, then all of the
  59.  * characters which fit within the wrapping width are placed on the
  60.  * line.  At least one character is placed on each line.
  61.  * <p>
  62.  * The TextLayout instances returned by LineBreakMeasurer treat tabs
  63.  * ('\t') like 0-width spaces.  Clients who wish to obtain
  64.  * tab-delimited segments for positioning should use the overload of
  65.  * nextLayout() which takes a limiting offset in the text.
  66.  * The limiting offset should be the first character after the tab.
  67.  * The TextLayouts returned from this method will end at the limit
  68.  * provided (or before, if the text between the current position and
  69.  * the limit won't fit entirely within the given wrapping width).
  70.  * <p>
  71.  * Clients who are laying out tab-delimited text need a slightly
  72.  * different line-breaking policy after the first segment has been
  73.  * placed on a line.  Instead of fitting partial words in the
  74.  * remaining space, they should place words which don't fit in the
  75.  * remaining space entirely on the next line.  This change of policy
  76.  * can be requested in the overload of nextLayout() which takes a
  77.  * boolean parameter.  If this parameter is <code>true</code>,
  78.  * nextLayout() will return null if the first word won't fit in
  79.  * the given space.  See the tab sample below.
  80.  * <p>
  81.  * In general, if the text used to construct the LineBreakMeasurer
  82.  * changes, a new LineBreakMeasurer must be constructed to reflect
  83.  * the change.  (The old LineBreakMeasurer will continue to function
  84.  * properly, but it won't be aware of the text change.)  However, if
  85.  * the text change is the insertion or deletion of a single
  86.  * character, an existing LineBreakMeasurer may be 'updated' by
  87.  * calling insertChar() or deleteChar().  Updating an existing
  88.  * LineBreakMeasurer is much faster than creating a new one.  Clients
  89.  * that modify text based on user typing will want to take advantage
  90.  * of these methods.
  91.  * <p>
  92.  * <strong>Examples</strong>:<p>
  93.  * Drawing a paragraph in a component
  94.  * <blockquote>
  95.  * <pre>
  96.  * public void paint(Graphics graphics) {
  97.  *
  98.  *     Point2D pen = new Point2D(10, 20);
  99.  *
  100.  *     // let styledText be a StyledString containing at least one
  101.  *     // character
  102.  *
  103.  *     LineBreakMeasurer measurer =
  104.  *         new LineBreakMeasurer(new StyledStringIterator(styledText));
  105.  *     float wrappingWidth = getSize().width - 15;
  106.  *
  107.  *     while (measurer.getPosition() < fStyledText.length()) {
  108.  *
  109.  *         TextLayout layout = measurer.nextLayout(wrappingWidth);
  110.  *
  111.  *         pen.y += (layout.getAscent());
  112.  *         float dx = layout.isLeftToRight() ?
  113.  *             0 : (wrappingWidth - layout.getAdvance());
  114.  *
  115.  *         layout.draw(graphics, pen.x + dx, pen.y);
  116.  *         pen.y += layout.getDescent() + layout.getLeading();
  117.  *     }
  118.  * }
  119.  * </pre>
  120.  * </blockquote>
  121.  * <p>
  122.  * Drawing text with tabs.  For simplicity, the overall text
  123.  * direction is assumed to be left-to-right
  124.  * <blockquote>
  125.  * <pre>
  126.  * public void paint(Graphics graphics) {
  127.  *
  128.  *     float leftMargin = 10, rightMargin = 310;
  129.  *     float[] tabStops = { 100, 250 };
  130.  *
  131.  *     // assume fStyledText is a StyledString, and the number
  132.  *     // of tabs in fStyledText is fTabCount
  133.  *
  134.  *     int[] tabLocations = new int[fTabCount+1];
  135.  *
  136.  *     AttributedCharacterIterator iter =
  137.  *         new StyledStringIterator(fStyledText);
  138.  *     int i=0;
  139.  *     for (char c = iter.first(); c != iter.DONE; c = iter.next()) {
  140.  *         if (c == '\t') {
  141.  *             tabLocations[i++] = iter.getIndex();
  142.  *         }
  143.  *     }
  144.  *     tabLocations[fTabCount] = iter.getEndIndex() - 1;
  145.  *
  146.  *     // Now tabLocations has an entry for every tab's offset in
  147.  *     // the text.  For convenience, the last entry is tabLocations
  148.  *     // is the offset of the last character in the text.
  149.  *
  150.  *     LineBreakMeasurer measurer = new LineBreakMeasurer(iter);
  151.  *     int currentTab = 0;
  152.  *     float verticalPos = 20;
  153.  *
  154.  *     while (measurer.getPosition() < iter.getEndIndex()) {
  155.  *
  156.  *         // Lay out and draw each line.  All segments on a line
  157.  *         // must be computed before any drawing can occur, since
  158.  *         // we must know the largest ascent on the line.
  159.  *         // TextLayouts are computed and stored in a Vector;
  160.  *         // their horizontal positions are stored in a parallel
  161.  *         // Vector.
  162.  *
  163.  *         // lineContainsText is true after first segment is drawn
  164.  *         boolean lineContainsText = false;
  165.  *         boolean lineComplete = false;
  166.  *         float maxAscent = 0, maxDescent = 0;
  167.  *         float horizontalPos = leftMargin;
  168.  *         Vector layouts = new Vector(1);
  169.  *         Vector penPositions = new Vector(1);
  170.  *
  171.  *         while (!lineComplete) {
  172.  *             float wrappingWidth = rightMargin - horizontalPos;
  173.  *             TextLayout layout =
  174.  *                     measurer.nextLayout(wrappingWidth,
  175.  *                                         tabLocations[currentTab]+1,
  176.  *                                         lineContainsText);
  177.  *
  178.  *             // layout can be null if lineContainsText is true
  179.  *             if (layout != null) {
  180.  *                 layouts.addElement(layout);
  181.  *                 penPositions.addElement(new Float(horizontalPos));
  182.  *                 horizontalPos += layout.getAdvance();
  183.  *                 maxAscent = Math.max(maxAscent, layout.getAscent());
  184.  *                 maxDescent = Math.max(maxDescent,
  185.  *                     layout.getDescent() + layout.getLeading());
  186.  *             } else {
  187.  *                 lineComplete = true;
  188.  *             }
  189.  *
  190.  *             lineContainsText = true;
  191.  *
  192.  *             if (measurer.getPosition() == tabLocations[currentTab]+1) {
  193.  *                 currentTab++;
  194.  *             }
  195.  *
  196.  *             if (measurer.getPosition() == iter.getEndIndex())
  197.  *                 lineComplete = true;
  198.  *             else if (horizontalPos >= tabStops[tabStops.length-1])
  199.  *                 lineComplete = true;
  200.  *
  201.  *             if (!lineComplete) {
  202.  *                 // move to next tab stop
  203.  *                 int j;
  204.  *                 for (j=0; horizontalPos >= tabStops[j]; j++) {}
  205.  *                 horizontalPos = tabStops[j];
  206.  *             }
  207.  *         }
  208.  *
  209.  *         verticalPos += maxAscent;
  210.  *
  211.  *         Enumeration layoutEnum = layouts.elements();
  212.  *         Enumeration positionEnum = penPositions.elements();
  213.  *
  214.  *         // now iterate through layouts and draw them
  215.  *         while (layoutEnum.hasMoreElements()) {
  216.  *             TextLayout nextLayout = (TextLayout) layoutEnum.nextElement();
  217.  *             Float nextPosition = (Float) positionEnum.nextElement();
  218.  *             nextLayout.draw(graphics, nextPosition.floatValue(), verticalPos);
  219.  *         }
  220.  *
  221.  *         verticalPos += maxDescent;
  222.  *     }
  223.  * }
  224.  * </pre>
  225.  * </blockquote>
  226.  * @see TextLayout
  227.  */
  228.  
  229. public final class LineBreakMeasurer {
  230.     private AttributedCharacterIterator text;
  231.     private BreakIterator breakIter;
  232.     private int pos;
  233.     private int limit;
  234.     private TextMeasurer measurer;
  235.  
  236.     /**
  237.      * Construct a LineBreakMeasurer for the given text.
  238.      * @param text the text for which the LineBreakMeasurer will
  239.      * produce TextLayouts.  Must contain at least one character.
  240.      * @see LineBreakMeasurer#insertChar
  241.      * @see LineBreakMeasurer#deleteChar
  242.      */
  243.     public LineBreakMeasurer(AttributedCharacterIterator text) {
  244.         this(text, BreakIterator.getLineInstance());
  245.     }
  246.  
  247.     public LineBreakMeasurer(AttributedCharacterIterator text,
  248.                              BreakIterator breakIter) {
  249.         this.text = text;
  250.         this.breakIter = breakIter;
  251.         this.breakIter.setText((CharacterIterator)text.clone());
  252.         this.measurer = new TextMeasurer(text);
  253.         this.limit = text.getEndIndex();
  254.         this.pos = text.getBeginIndex();
  255.         text.setIndex(pos);
  256.     }
  257.  
  258.     /**
  259.      * Return the position at the end of the next layout.  Does NOT
  260.      * update the current position of the LineBreakMeasurer.
  261.      * @param maxAdvance the maximum visible advance permitted for
  262.      * the text in the next layout.
  263.      * @return an offset in the text representing the limit of the
  264.      * next TextLayout
  265.      */
  266.     public int nextOffset(float maxAdvance) {
  267.         return nextOffset(maxAdvance, limit, false);
  268.     }
  269.  
  270.     /**
  271.      * Return the position at the end of the next layout.  Does NOT
  272.      * update the current position of the LineBreakMeasurer.
  273.      * @param wrappingWidth the maximum visible advance permitted for
  274.      * the text in the next layout.
  275.      * @param offsetLimit the first character which may not be included
  276.      * in the next layout, even if the text after the limit would fit
  277.      * within the wrapping width.  Must be greater than the current
  278.      * position.
  279.      * @param requireNextWord if true, the current position will be
  280.      * returned if the entire next word will not fit within
  281.      * wrappingWidth. If false, the offset returned will be at least
  282.      * one greater than the current position.
  283.      * @return an offset in the text representing the limit of the
  284.      * next TextLayout
  285.      */
  286.     public int nextOffset(float wrappingWidth, int offsetLimit,
  287.                           boolean requireNextWord) {
  288.  
  289.         int nextOffset = pos;
  290.  
  291.         if (pos < limit) {
  292.             if (offsetLimit <= pos) {
  293.                     throw new IllegalArgumentException("offsetLimit must be after current position");
  294.             }
  295.  
  296.             int charAtMaxAdvance =
  297.                             measurer.getLineBreakIndex(pos, wrappingWidth);
  298.             text.setIndex(charAtMaxAdvance);
  299.  
  300.             if (charAtMaxAdvance == limit) {
  301.                 nextOffset = limit;
  302.             }
  303.             else if (Character.isWhitespace(text.current())) {
  304.                 nextOffset = breakIter.following(charAtMaxAdvance);
  305.             }
  306.             else {
  307.             // Break is in a word;  back up to previous break.
  308.  
  309.                 breakIter.following(charAtMaxAdvance);
  310.                 nextOffset = breakIter.previous();
  311.  
  312.                 if (nextOffset <= pos) {
  313.                     // first word doesn't fit on line
  314.                     if (requireNextWord) {
  315.                         nextOffset = pos;
  316.                     }
  317.                     else {
  318.                         nextOffset = Math.max(pos+1, charAtMaxAdvance);
  319.                     }
  320.                 }
  321.             }
  322.         }
  323.  
  324.         if (nextOffset > offsetLimit) {
  325.             nextOffset = offsetLimit;
  326.         }
  327.  
  328.         return nextOffset;
  329.     }
  330.  
  331.     /**
  332.      * Return the next layout, and update the current position.
  333.      * @param maxAdvance the maximum visible advance permitted for
  334.      * the text in the next layout.
  335.      * @return a TextLayout, beginning at the current position,
  336.      * which represents the next line fitting within maxAdvance.
  337.      */
  338.     public TextLayout nextLayout(float maxAdvance) {
  339.         return nextLayout(maxAdvance, limit, false);
  340.     }
  341.  
  342.     /**
  343.      * Return the next layout, and update the current position.
  344.      * @param wrappingWidth the maximum visible advance permitted
  345.      * for the text in the next layout.
  346.      * @param offsetLimit the first character which may not be
  347.      * included in the next layout, even if the text after the limit
  348.      * would fit within the wrapping width.  Must be greater than the
  349.      * current position.
  350.      * @param requireNextWord if true, and if the entire word at the
  351.      * current position won't fit within wrapping width, null will be
  352.      * returned. If false, a valid layout will be returned
  353.      * that includes at least the character at the current position.
  354.      * @return a TextLayout, beginning at the current position, that
  355.      * represents the next line fitting within maxAdvance.  If the
  356.      * current position is at the end of the text used by the
  357.      * LineBreakMeasurer, null is returned.
  358.      */
  359.     public TextLayout nextLayout(float wrappingWidth, int offsetLimit,
  360.                                  boolean requireNextWord) {
  361.  
  362.         if (pos < text.getEndIndex()) {
  363.             int layoutLimit = nextOffset(wrappingWidth, offsetLimit, requireNextWord);
  364.             if (layoutLimit == pos) {
  365.                 return null;
  366.             }
  367.  
  368.             TextLayout result = measurer.getLayout(pos, layoutLimit);
  369.             pos = layoutLimit;
  370.  
  371.             return result;
  372.         } else {
  373.             return null;
  374.         }
  375.     }
  376.  
  377.     /**
  378.      * Return the current position of the LineBreakMeasurer.
  379.      * @return the current position of the LineBreakMeasurer
  380.      * @see #setPosition
  381.      */
  382.     public int getPosition() {
  383.         return pos;
  384.     }
  385.  
  386.     /**
  387.      * Set the current position of the LineBreakMeasurer.
  388.      * @param newPosition the current position of the LineBreakMeasurer.
  389.      * The position should be within the text used to construct the
  390.      * LineBreakMeasurer (or in the text most recently passed to
  391.      * insertChar or deleteChar).
  392.      * @see #getPosition
  393.      */
  394.     public void setPosition(int newPosition) {
  395.         if (newPosition < text.getBeginIndex() || newPosition > limit) {
  396.             throw new IllegalArgumentException("position is out of range");
  397.         }
  398.         pos = newPosition;
  399.     }
  400.  
  401.     /**
  402.      * Update the LineBreakMeasurer after a single character was inserted
  403.      * into the text.
  404.      * @param newParagraph the text after the insertion
  405.      * @param insertPos the position in the text at which the character
  406.      * was inserted
  407.      * @see #deleteChar
  408.      */
  409.     public void insertChar(AttributedCharacterIterator newParagraph,
  410.                            int insertPos) {
  411.  
  412.         text = newParagraph;
  413.         breakIter.setText((CharacterIterator)text.clone());
  414.  
  415.         limit = text.getEndIndex();
  416.         pos = text.getBeginIndex();
  417.  
  418.         measurer.insertChar(newParagraph, insertPos);
  419.     }
  420.  
  421.     /**
  422.      * Update the LineBreakMeasurer after a single character was deleted
  423.      * from the text.
  424.      * @param newParagraph the text after the deletion
  425.      * @param deletePos the position in the text at which the character
  426.      * was deleted
  427.      * @see #insertChar
  428.      */
  429.     public void deleteChar(AttributedCharacterIterator newParagraph,
  430.                            int deletePos) {
  431.  
  432.         text = newParagraph;
  433.         breakIter.setText((CharacterIterator)text.clone());
  434.  
  435.         limit = text.getEndIndex();
  436.         pos = text.getBeginIndex();
  437.  
  438.         measurer.deleteChar(newParagraph, deletePos);
  439.     }
  440. }
  441.  
  442.