1 /*
   2  * Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.text;
  26 
  27 import java.lang.reflect.Method;
  28 
  29 import java.awt.Component;
  30 import java.awt.Rectangle;
  31 import java.awt.Graphics;
  32 import java.awt.FontMetrics;
  33 import java.awt.Shape;
  34 import java.awt.Toolkit;
  35 import java.awt.Graphics2D;
  36 import java.awt.font.FontRenderContext;
  37 import java.awt.font.TextLayout;
  38 import java.awt.font.TextAttribute;
  39 
  40 import java.text.*;
  41 import javax.swing.JComponent;
  42 import javax.swing.SwingConstants;
  43 import javax.swing.text.ParagraphView.Row;
  44 import sun.swing.SwingUtilities2;
  45 
  46 /**
  47  * A collection of methods to deal with various text
  48  * related activities.
  49  *
  50  * @author  Timothy Prinzing
  51  */
  52 public class Utilities {
  53     /**
  54      * If <code>view</code>'s container is a <code>JComponent</code> it
  55      * is returned, after casting.
  56      */
  57     static JComponent getJComponent(View view) {
  58         if (view != null) {
  59             Component component = view.getContainer();
  60             if (component instanceof JComponent) {
  61                 return (JComponent)component;
  62             }
  63         }
  64         return null;
  65     }
  66 
  67     /**
  68      * Draws the given text, expanding any tabs that are contained
  69      * using the given tab expansion technique.  This particular
  70      * implementation renders in a 1.1 style coordinate system
  71      * where ints are used and 72dpi is assumed.
  72      *
  73      * @param s  the source of the text
  74      * @param x  the X origin >= 0
  75      * @param y  the Y origin >= 0
  76      * @param g  the graphics context
  77      * @param e  how to expand the tabs.  If this value is null,
  78      *   tabs will be expanded as a space character.
  79      * @param startOffset starting offset of the text in the document >= 0
  80      * @return  the X location at the end of the rendered text
  81      */
  82     public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
  83                                            TabExpander e, int startOffset) {
  84         return drawTabbedText(null, s, x, y, g, e, startOffset);
  85     }
  86 
  87     /**
  88      * Draws the given text, expanding any tabs that are contained
  89      * using the given tab expansion technique.  This particular
  90      * implementation renders in a 1.1 style coordinate system
  91      * where ints are used and 72dpi is assumed.
  92      *
  93      * @param view View requesting rendering, may be null.
  94      * @param s  the source of the text
  95      * @param x  the X origin >= 0
  96      * @param y  the Y origin >= 0
  97      * @param g  the graphics context
  98      * @param e  how to expand the tabs.  If this value is null,
  99      *   tabs will be expanded as a space character.
 100      * @param startOffset starting offset of the text in the document >= 0
 101      * @return  the X location at the end of the rendered text
 102      */
 103     static final int drawTabbedText(View view,
 104                                 Segment s, int x, int y, Graphics g,
 105                                 TabExpander e, int startOffset) {
 106         return drawTabbedText(view, s, x, y, g, e, startOffset, null);
 107     }
 108 
 109     // In addition to the previous method it can extend spaces for
 110     // justification.
 111     //
 112     // all params are the same as in the preious method except the last
 113     // one:
 114     // @param justificationData justificationData for the row.
 115     // if null not justification is needed
 116     static final int drawTabbedText(View view,
 117                                 Segment s, int x, int y, Graphics g,
 118                                 TabExpander e, int startOffset,
 119                                 int [] justificationData) {
 120         JComponent component = getJComponent(view);
 121         FontMetrics metrics = SwingUtilities2.getFontMetrics(component, g);
 122         int nextX = x;
 123         char[] txt = s.array;
 124         int txtOffset = s.offset;
 125         int flushLen = 0;
 126         int flushIndex = s.offset;
 127         int spaceAddon = 0;
 128         int spaceAddonLeftoverEnd = -1;
 129         int startJustifiableContent = 0;
 130         int endJustifiableContent = 0;
 131         if (justificationData != null) {
 132             int offset = - startOffset + txtOffset;
 133             View parent = null;
 134             if (view != null
 135                   && (parent = view.getParent()) != null) {
 136                 offset += parent.getStartOffset();
 137             }
 138             spaceAddon =
 139                 justificationData[Row.SPACE_ADDON];
 140             spaceAddonLeftoverEnd =
 141                 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
 142             startJustifiableContent =
 143                 justificationData[Row.START_JUSTIFIABLE] + offset;
 144             endJustifiableContent =
 145                 justificationData[Row.END_JUSTIFIABLE] + offset;
 146         }
 147         int n = s.offset + s.count;
 148         for (int i = txtOffset; i < n; i++) {
 149             if (txt[i] == '\t'
 150                 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
 151                     && (txt[i] == ' ')
 152                     && startJustifiableContent <= i
 153                     && i <= endJustifiableContent
 154                     )) {
 155                 if (flushLen > 0) {
 156                     nextX = SwingUtilities2.drawChars(component, g, txt,
 157                                                 flushIndex, flushLen, x, y);
 158                     flushLen = 0;
 159                 }
 160                 flushIndex = i + 1;
 161                 if (txt[i] == '\t') {
 162                     if (e != null) {
 163                         nextX = (int) e.nextTabStop((float) nextX, startOffset + i - txtOffset);
 164                     } else {
 165                         nextX += metrics.charWidth(' ');
 166                     }
 167                 } else if (txt[i] == ' ') {
 168                     nextX += metrics.charWidth(' ') + spaceAddon;
 169                     if (i <= spaceAddonLeftoverEnd) {
 170                         nextX++;
 171                     }
 172                 }
 173                 x = nextX;
 174             } else if ((txt[i] == '\n') || (txt[i] == '\r')) {
 175                 if (flushLen > 0) {
 176                     nextX = SwingUtilities2.drawChars(component, g, txt,
 177                                                 flushIndex, flushLen, x, y);
 178                     flushLen = 0;
 179                 }
 180                 flushIndex = i + 1;
 181                 x = nextX;
 182             } else {
 183                 flushLen += 1;
 184             }
 185         }
 186         if (flushLen > 0) {
 187             nextX = SwingUtilities2.drawChars(component, g,txt, flushIndex,
 188                                               flushLen, x, y);
 189         }
 190         return nextX;
 191     }
 192 
 193     /**
 194      * Determines the width of the given segment of text taking tabs
 195      * into consideration.  This is implemented in a 1.1 style coordinate
 196      * system where ints are used and 72dpi is assumed.
 197      *
 198      * @param s  the source of the text
 199      * @param metrics the font metrics to use for the calculation
 200      * @param x  the X origin >= 0
 201      * @param e  how to expand the tabs.  If this value is null,
 202      *   tabs will be expanded as a space character.
 203      * @param startOffset starting offset of the text in the document >= 0
 204      * @return  the width of the text
 205      */
 206     public static final int getTabbedTextWidth(Segment s, FontMetrics metrics, int x,
 207                                                TabExpander e, int startOffset) {
 208         return getTabbedTextWidth(null, s, metrics, x, e, startOffset, null);
 209     }
 210 
 211 
 212     // In addition to the previous method it can extend spaces for
 213     // justification.
 214     //
 215     // all params are the same as in the preious method except the last
 216     // one:
 217     // @param justificationData justificationData for the row.
 218     // if null not justification is needed
 219     static final int getTabbedTextWidth(View view, Segment s, FontMetrics metrics, int x,
 220                                         TabExpander e, int startOffset,
 221                                         int[] justificationData) {
 222         int nextX = x;
 223         char[] txt = s.array;
 224         int txtOffset = s.offset;
 225         int n = s.offset + s.count;
 226         int charCount = 0;
 227         int spaceAddon = 0;
 228         int spaceAddonLeftoverEnd = -1;
 229         int startJustifiableContent = 0;
 230         int endJustifiableContent = 0;
 231         if (justificationData != null) {
 232             int offset = - startOffset + txtOffset;
 233             View parent = null;
 234             if (view != null
 235                   && (parent = view.getParent()) != null) {
 236                 offset += parent.getStartOffset();
 237             }
 238             spaceAddon =
 239                 justificationData[Row.SPACE_ADDON];
 240             spaceAddonLeftoverEnd =
 241                 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
 242             startJustifiableContent =
 243                 justificationData[Row.START_JUSTIFIABLE] + offset;
 244             endJustifiableContent =
 245                 justificationData[Row.END_JUSTIFIABLE] + offset;
 246         }
 247 
 248         for (int i = txtOffset; i < n; i++) {
 249             if (txt[i] == '\t'
 250                 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
 251                     && (txt[i] == ' ')
 252                     && startJustifiableContent <= i
 253                     && i <= endJustifiableContent
 254                     )) {
 255                 nextX += metrics.charsWidth(txt, i-charCount, charCount);
 256                 charCount = 0;
 257                 if (txt[i] == '\t') {
 258                     if (e != null) {
 259                         nextX = (int) e.nextTabStop((float) nextX,
 260                                                     startOffset + i - txtOffset);
 261                     } else {
 262                         nextX += metrics.charWidth(' ');
 263                     }
 264                 } else if (txt[i] == ' ') {
 265                     nextX += metrics.charWidth(' ') + spaceAddon;
 266                     if (i <= spaceAddonLeftoverEnd) {
 267                         nextX++;
 268                     }
 269                 }
 270             } else if(txt[i] == '\n') {
 271             // Ignore newlines, they take up space and we shouldn't be
 272             // counting them.
 273                 nextX += metrics.charsWidth(txt, i - charCount, charCount);
 274                 charCount = 0;
 275             } else {
 276                 charCount++;
 277         }
 278         }
 279         nextX += metrics.charsWidth(txt, n - charCount, charCount);
 280         return nextX - x;
 281     }
 282 
 283     /**
 284      * Determines the relative offset into the given text that
 285      * best represents the given span in the view coordinate
 286      * system.  This is implemented in a 1.1 style coordinate
 287      * system where ints are used and 72dpi is assumed.
 288      *
 289      * @param s  the source of the text
 290      * @param metrics the font metrics to use for the calculation
 291      * @param x0 the starting view location representing the start
 292      *   of the given text >= 0.
 293      * @param x  the target view location to translate to an
 294      *   offset into the text >= 0.
 295      * @param e  how to expand the tabs.  If this value is null,
 296      *   tabs will be expanded as a space character.
 297      * @param startOffset starting offset of the text in the document >= 0
 298      * @return  the offset into the text >= 0
 299      */
 300     public static final int getTabbedTextOffset(Segment s, FontMetrics metrics,
 301                                              int x0, int x, TabExpander e,
 302                                              int startOffset) {
 303         return getTabbedTextOffset(s, metrics, x0, x, e, startOffset, true);
 304     }
 305 
 306     static final int getTabbedTextOffset(View view, Segment s, FontMetrics metrics,
 307                                          int x0, int x, TabExpander e,
 308                                          int startOffset,
 309                                          int[] justificationData) {
 310         return getTabbedTextOffset(view, s, metrics, x0, x, e, startOffset, true,
 311                                    justificationData);
 312     }
 313 
 314     public static final int getTabbedTextOffset(Segment s,
 315                                                 FontMetrics metrics,
 316                                                 int x0, int x, TabExpander e,
 317                                                 int startOffset,
 318                                                 boolean round) {
 319         return getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, round, null);
 320     }
 321 
 322     // In addition to the previous method it can extend spaces for
 323     // justification.
 324     //
 325     // all params are the same as in the preious method except the last
 326     // one:
 327     // @param justificationData justificationData for the row.
 328     // if null not justification is needed
 329     static final int getTabbedTextOffset(View view,
 330                                          Segment s,
 331                                          FontMetrics metrics,
 332                                          int x0, int x, TabExpander e,
 333                                          int startOffset,
 334                                          boolean round,
 335                                          int[] justificationData) {
 336         if (x0 >= x) {
 337             // x before x0, return.
 338             return 0;
 339         }
 340         int currX = x0;
 341         int nextX = currX;
 342         // s may be a shared segment, so it is copied prior to calling
 343         // the tab expander
 344         char[] txt = s.array;
 345         int txtOffset = s.offset;
 346         int txtCount = s.count;
 347         int spaceAddon = 0 ;
 348         int spaceAddonLeftoverEnd = -1;
 349         int startJustifiableContent = 0 ;
 350         int endJustifiableContent = 0;
 351         if (justificationData != null) {
 352             int offset = - startOffset + txtOffset;
 353             View parent = null;
 354             if (view != null
 355                   && (parent = view.getParent()) != null) {
 356                 offset += parent.getStartOffset();
 357             }
 358             spaceAddon =
 359                 justificationData[Row.SPACE_ADDON];
 360             spaceAddonLeftoverEnd =
 361                 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
 362             startJustifiableContent =
 363                 justificationData[Row.START_JUSTIFIABLE] + offset;
 364             endJustifiableContent =
 365                 justificationData[Row.END_JUSTIFIABLE] + offset;
 366         }
 367         int n = s.offset + s.count;
 368         for (int i = s.offset; i < n; i++) {
 369             if (txt[i] == '\t'
 370                 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
 371                     && (txt[i] == ' ')
 372                     && startJustifiableContent <= i
 373                     && i <= endJustifiableContent
 374                     )){
 375                 if (txt[i] == '\t') {
 376                     if (e != null) {
 377                         nextX = (int) e.nextTabStop((float) nextX,
 378                                                     startOffset + i - txtOffset);
 379                     } else {
 380                         nextX += metrics.charWidth(' ');
 381                     }
 382                 } else if (txt[i] == ' ') {
 383                     nextX += metrics.charWidth(' ') + spaceAddon;
 384                     if (i <= spaceAddonLeftoverEnd) {
 385                         nextX++;
 386                     }
 387                 }
 388             } else {
 389                 nextX += metrics.charWidth(txt[i]);
 390             }
 391             if ((x >= currX) && (x < nextX)) {
 392                 // found the hit position... return the appropriate side
 393                 int offset = ((round == false) || ((x - currX) < (nextX - x))) ?
 394                         (i - txtOffset) : (i + 1 - txtOffset);
 395                 // the length of the string measured as a whole may differ from
 396                 // the sum of individual character lengths, for example if
 397                 // fractional metrics are enabled; and we must guard from this.
 398                 while (metrics.charsWidth(txt, txtOffset, offset + 1) > (x - x0)) {
 399                     offset--;
 400                 }
 401                 return (offset < 0 ? 0 : offset);
 402             }
 403             currX = nextX;
 404         }
 405 
 406         // didn't find, return end offset
 407         return txtCount;
 408     }
 409 
 410     /**
 411      * Determine where to break the given text to fit
 412      * within the given span. This tries to find a word boundary.
 413      * @param s  the source of the text
 414      * @param metrics the font metrics to use for the calculation
 415      * @param x0 the starting view location representing the start
 416      *   of the given text.
 417      * @param x  the target view location to translate to an
 418      *   offset into the text.
 419      * @param e  how to expand the tabs.  If this value is null,
 420      *   tabs will be expanded as a space character.
 421      * @param startOffset starting offset in the document of the text
 422      * @return  the offset into the given text
 423      */
 424     public static final int getBreakLocation(Segment s, FontMetrics metrics,
 425                                              int x0, int x, TabExpander e,
 426                                              int startOffset) {
 427         char[] txt = s.array;
 428         int txtOffset = s.offset;
 429         int txtCount = s.count;
 430         int index = Utilities.getTabbedTextOffset(s, metrics, x0, x,
 431                                                   e, startOffset, false);
 432 
 433         if (index >= txtCount - 1) {
 434             return txtCount;
 435         }
 436 
 437         for (int i = txtOffset + index; i >= txtOffset; i--) {
 438             char ch = txt[i];
 439             if (ch < 256) {
 440                 // break on whitespace
 441                 if (Character.isWhitespace(ch)) {
 442                     index = i - txtOffset + 1;
 443                     break;
 444                 }
 445             } else {
 446                 // a multibyte char found; use BreakIterator to find line break
 447                 BreakIterator bit = BreakIterator.getLineInstance();
 448                 bit.setText(s);
 449                 int breakPos = bit.preceding(i + 1);
 450                 if (breakPos > txtOffset) {
 451                     index = breakPos - txtOffset;
 452                 }
 453                 break;
 454             }
 455         }
 456         return index;
 457     }
 458 
 459     /**
 460      * Determines the starting row model position of the row that contains
 461      * the specified model position.  The component given must have a
 462      * size to compute the result.  If the component doesn't have a size
 463      * a value of -1 will be returned.
 464      *
 465      * @param c the editor
 466      * @param offs the offset in the document >= 0
 467      * @return the position >= 0 if the request can be computed, otherwise
 468      *  a value of -1 will be returned.
 469      * @exception BadLocationException if the offset is out of range
 470      */
 471     public static final int getRowStart(JTextComponent c, int offs) throws BadLocationException {
 472         Rectangle r = c.modelToView(offs);
 473         if (r == null) {
 474             return -1;
 475         }
 476         int lastOffs = offs;
 477         int y = r.y;
 478         while ((r != null) && (y == r.y)) {
 479             // Skip invisible elements
 480             if(r.height !=0) {
 481                 offs = lastOffs;
 482             }
 483             lastOffs -= 1;
 484             r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
 485         }
 486         return offs;
 487     }
 488 
 489     /**
 490      * Determines the ending row model position of the row that contains
 491      * the specified model position.  The component given must have a
 492      * size to compute the result.  If the component doesn't have a size
 493      * a value of -1 will be returned.
 494      *
 495      * @param c the editor
 496      * @param offs the offset in the document >= 0
 497      * @return the position >= 0 if the request can be computed, otherwise
 498      *  a value of -1 will be returned.
 499      * @exception BadLocationException if the offset is out of range
 500      */
 501     public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException {
 502         Rectangle r = c.modelToView(offs);
 503         if (r == null) {
 504             return -1;
 505         }
 506         int n = c.getDocument().getLength();
 507         int lastOffs = offs;
 508         int y = r.y;
 509         while ((r != null) && (y == r.y)) {
 510             // Skip invisible elements
 511             if (r.height !=0) {
 512                 offs = lastOffs;
 513             }
 514             lastOffs += 1;
 515             r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
 516         }
 517         return offs;
 518     }
 519 
 520     /**
 521      * Determines the position in the model that is closest to the given
 522      * view location in the row above.  The component given must have a
 523      * size to compute the result.  If the component doesn't have a size
 524      * a value of -1 will be returned.
 525      *
 526      * @param c the editor
 527      * @param offs the offset in the document >= 0
 528      * @param x the X coordinate >= 0
 529      * @return the position >= 0 if the request can be computed, otherwise
 530      *  a value of -1 will be returned.
 531      * @exception BadLocationException if the offset is out of range
 532      */
 533     public static final int getPositionAbove(JTextComponent c, int offs, int x) throws BadLocationException {
 534         int lastOffs = getRowStart(c, offs) - 1;
 535         if (lastOffs < 0) {
 536             return -1;
 537         }
 538         int bestSpan = Integer.MAX_VALUE;
 539         int y = 0;
 540         Rectangle r = null;
 541         if (lastOffs >= 0) {
 542             r = c.modelToView(lastOffs);
 543             y = r.y;
 544         }
 545         while ((r != null) && (y == r.y)) {
 546             int span = Math.abs(r.x - x);
 547             if (span < bestSpan) {
 548                 offs = lastOffs;
 549                 bestSpan = span;
 550             }
 551             lastOffs -= 1;
 552             r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
 553         }
 554         return offs;
 555     }
 556 
 557     /**
 558      * Determines the position in the model that is closest to the given
 559      * view location in the row below.  The component given must have a
 560      * size to compute the result.  If the component doesn't have a size
 561      * a value of -1 will be returned.
 562      *
 563      * @param c the editor
 564      * @param offs the offset in the document >= 0
 565      * @param x the X coordinate >= 0
 566      * @return the position >= 0 if the request can be computed, otherwise
 567      *  a value of -1 will be returned.
 568      * @exception BadLocationException if the offset is out of range
 569      */
 570     public static final int getPositionBelow(JTextComponent c, int offs, int x) throws BadLocationException {
 571         int lastOffs = getRowEnd(c, offs) + 1;
 572         if (lastOffs <= 0) {
 573             return -1;
 574         }
 575         int bestSpan = Integer.MAX_VALUE;
 576         int n = c.getDocument().getLength();
 577         int y = 0;
 578         Rectangle r = null;
 579         if (lastOffs <= n) {
 580             r = c.modelToView(lastOffs);
 581             y = r.y;
 582         }
 583         while ((r != null) && (y == r.y)) {
 584             int span = Math.abs(x - r.x);
 585             if (span < bestSpan) {
 586                 offs = lastOffs;
 587                 bestSpan = span;
 588             }
 589             lastOffs += 1;
 590             r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
 591         }
 592         return offs;
 593     }
 594 
 595     /**
 596      * Determines the start of a word for the given model location.
 597      * Uses BreakIterator.getWordInstance() to actually get the words.
 598      *
 599      * @param c the editor
 600      * @param offs the offset in the document >= 0
 601      * @return the location in the model of the word start >= 0
 602      * @exception BadLocationException if the offset is out of range
 603      */
 604     public static final int getWordStart(JTextComponent c, int offs) throws BadLocationException {
 605         Document doc = c.getDocument();
 606         Element line = getParagraphElement(c, offs);
 607         if (line == null) {
 608             throw new BadLocationException("No word at " + offs, offs);
 609         }
 610         int lineStart = line.getStartOffset();
 611         int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
 612 
 613         Segment seg = SegmentCache.getSharedSegment();
 614         doc.getText(lineStart, lineEnd - lineStart, seg);
 615         if(seg.count > 0) {
 616             BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
 617             words.setText(seg);
 618             int wordPosition = seg.offset + offs - lineStart;
 619             if(wordPosition >= words.last()) {
 620                 wordPosition = words.last() - 1;
 621             }
 622             words.following(wordPosition);
 623             offs = lineStart + words.previous() - seg.offset;
 624         }
 625         SegmentCache.releaseSharedSegment(seg);
 626         return offs;
 627     }
 628 
 629     /**
 630      * Determines the end of a word for the given location.
 631      * Uses BreakIterator.getWordInstance() to actually get the words.
 632      *
 633      * @param c the editor
 634      * @param offs the offset in the document >= 0
 635      * @return the location in the model of the word end >= 0
 636      * @exception BadLocationException if the offset is out of range
 637      */
 638     public static final int getWordEnd(JTextComponent c, int offs) throws BadLocationException {
 639         Document doc = c.getDocument();
 640         Element line = getParagraphElement(c, offs);
 641         if (line == null) {
 642             throw new BadLocationException("No word at " + offs, offs);
 643         }
 644         int lineStart = line.getStartOffset();
 645         int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
 646 
 647         Segment seg = SegmentCache.getSharedSegment();
 648         doc.getText(lineStart, lineEnd - lineStart, seg);
 649         if(seg.count > 0) {
 650             BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
 651             words.setText(seg);
 652             int wordPosition = offs - lineStart + seg.offset;
 653             if(wordPosition >= words.last()) {
 654                 wordPosition = words.last() - 1;
 655             }
 656             offs = lineStart + words.following(wordPosition) - seg.offset;
 657         }
 658         SegmentCache.releaseSharedSegment(seg);
 659         return offs;
 660     }
 661 
 662     /**
 663      * Determines the start of the next word for the given location.
 664      * Uses BreakIterator.getWordInstance() to actually get the words.
 665      *
 666      * @param c the editor
 667      * @param offs the offset in the document >= 0
 668      * @return the location in the model of the word start >= 0
 669      * @exception BadLocationException if the offset is out of range
 670      */
 671     public static final int getNextWord(JTextComponent c, int offs) throws BadLocationException {
 672         int nextWord;
 673         Element line = getParagraphElement(c, offs);
 674         for (nextWord = getNextWordInParagraph(c, line, offs, false);
 675              nextWord == BreakIterator.DONE;
 676              nextWord = getNextWordInParagraph(c, line, offs, true)) {
 677 
 678             // didn't find in this line, try the next line
 679             offs = line.getEndOffset();
 680             line = getParagraphElement(c, offs);
 681         }
 682         return nextWord;
 683     }
 684 
 685     /**
 686      * Finds the next word in the given elements text.  The first
 687      * parameter allows searching multiple paragraphs where even
 688      * the first offset is desired.
 689      * Returns the offset of the next word, or BreakIterator.DONE
 690      * if there are no more words in the element.
 691      */
 692     static int getNextWordInParagraph(JTextComponent c, Element line, int offs, boolean first) throws BadLocationException {
 693         if (line == null) {
 694             throw new BadLocationException("No more words", offs);
 695         }
 696         Document doc = line.getDocument();
 697         int lineStart = line.getStartOffset();
 698         int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
 699         if ((offs >= lineEnd) || (offs < lineStart)) {
 700             throw new BadLocationException("No more words", offs);
 701         }
 702         Segment seg = SegmentCache.getSharedSegment();
 703         doc.getText(lineStart, lineEnd - lineStart, seg);
 704         BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
 705         words.setText(seg);
 706         if ((first && (words.first() == (seg.offset + offs - lineStart))) &&
 707             (! Character.isWhitespace(seg.array[words.first()]))) {
 708 
 709             return offs;
 710         }
 711         int wordPosition = words.following(seg.offset + offs - lineStart);
 712         if ((wordPosition == BreakIterator.DONE) ||
 713             (wordPosition >= seg.offset + seg.count)) {
 714                 // there are no more words on this line.
 715                 return BreakIterator.DONE;
 716         }
 717         // if we haven't shot past the end... check to
 718         // see if the current boundary represents whitespace.
 719         // if so, we need to try again
 720         char ch = seg.array[wordPosition];
 721         if (! Character.isWhitespace(ch)) {
 722             return lineStart + wordPosition - seg.offset;
 723         }
 724 
 725         // it was whitespace, try again.  The assumption
 726         // is that it must be a word start if the last
 727         // one had whitespace following it.
 728         wordPosition = words.next();
 729         if (wordPosition != BreakIterator.DONE) {
 730             offs = lineStart + wordPosition - seg.offset;
 731             if (offs != lineEnd) {
 732                 return offs;
 733             }
 734         }
 735         SegmentCache.releaseSharedSegment(seg);
 736         return BreakIterator.DONE;
 737     }
 738 
 739 
 740     /**
 741      * Determine the start of the prev word for the given location.
 742      * Uses BreakIterator.getWordInstance() to actually get the words.
 743      *
 744      * @param c the editor
 745      * @param offs the offset in the document >= 0
 746      * @return the location in the model of the word start >= 0
 747      * @exception BadLocationException if the offset is out of range
 748      */
 749     public static final int getPreviousWord(JTextComponent c, int offs) throws BadLocationException {
 750         int prevWord;
 751         Element line = getParagraphElement(c, offs);
 752         for (prevWord = getPrevWordInParagraph(c, line, offs);
 753              prevWord == BreakIterator.DONE;
 754              prevWord = getPrevWordInParagraph(c, line, offs)) {
 755 
 756             // didn't find in this line, try the prev line
 757             offs = line.getStartOffset() - 1;
 758             line = getParagraphElement(c, offs);
 759         }
 760         return prevWord;
 761     }
 762 
 763     /**
 764      * Finds the previous word in the given elements text.  The first
 765      * parameter allows searching multiple paragraphs where even
 766      * the first offset is desired.
 767      * Returns the offset of the next word, or BreakIterator.DONE
 768      * if there are no more words in the element.
 769      */
 770     static int getPrevWordInParagraph(JTextComponent c, Element line, int offs) throws BadLocationException {
 771         if (line == null) {
 772             throw new BadLocationException("No more words", offs);
 773         }
 774         Document doc = line.getDocument();
 775         int lineStart = line.getStartOffset();
 776         int lineEnd = line.getEndOffset();
 777         if ((offs > lineEnd) || (offs < lineStart)) {
 778             throw new BadLocationException("No more words", offs);
 779         }
 780         Segment seg = SegmentCache.getSharedSegment();
 781         doc.getText(lineStart, lineEnd - lineStart, seg);
 782         BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
 783         words.setText(seg);
 784         if (words.following(seg.offset + offs - lineStart) == BreakIterator.DONE) {
 785             words.last();
 786         }
 787         int wordPosition = words.previous();
 788         if (wordPosition == (seg.offset + offs - lineStart)) {
 789             wordPosition = words.previous();
 790         }
 791 
 792         if (wordPosition == BreakIterator.DONE) {
 793             // there are no more words on this line.
 794             return BreakIterator.DONE;
 795         }
 796         // if we haven't shot past the end... check to
 797         // see if the current boundary represents whitespace.
 798         // if so, we need to try again
 799         char ch = seg.array[wordPosition];
 800         if (! Character.isWhitespace(ch)) {
 801             return lineStart + wordPosition - seg.offset;
 802         }
 803 
 804         // it was whitespace, try again.  The assumption
 805         // is that it must be a word start if the last
 806         // one had whitespace following it.
 807         wordPosition = words.previous();
 808         if (wordPosition != BreakIterator.DONE) {
 809             return lineStart + wordPosition - seg.offset;
 810         }
 811         SegmentCache.releaseSharedSegment(seg);
 812         return BreakIterator.DONE;
 813     }
 814 
 815     /**
 816      * Determines the element to use for a paragraph/line.
 817      *
 818      * @param c the editor
 819      * @param offs the starting offset in the document >= 0
 820      * @return the element
 821      */
 822     public static final Element getParagraphElement(JTextComponent c, int offs) {
 823         Document doc = c.getDocument();
 824         if (doc instanceof StyledDocument) {
 825             return ((StyledDocument)doc).getParagraphElement(offs);
 826         }
 827         Element map = doc.getDefaultRootElement();
 828         int index = map.getElementIndex(offs);
 829         Element paragraph = map.getElement(index);
 830         if ((offs >= paragraph.getStartOffset()) && (offs < paragraph.getEndOffset())) {
 831             return paragraph;
 832         }
 833         return null;
 834     }
 835 
 836     static boolean isComposedTextElement(Document doc, int offset) {
 837         Element elem = doc.getDefaultRootElement();
 838         while (!elem.isLeaf()) {
 839             elem = elem.getElement(elem.getElementIndex(offset));
 840         }
 841         return isComposedTextElement(elem);
 842     }
 843 
 844     static boolean isComposedTextElement(Element elem) {
 845         AttributeSet as = elem.getAttributes();
 846         return isComposedTextAttributeDefined(as);
 847     }
 848 
 849     static boolean isComposedTextAttributeDefined(AttributeSet as) {
 850         return ((as != null) &&
 851                 (as.isDefined(StyleConstants.ComposedTextAttribute)));
 852     }
 853 
 854     /**
 855      * Draws the given composed text passed from an input method.
 856      *
 857      * @param view View hosting text
 858      * @param attr the attributes containing the composed text
 859      * @param g  the graphics context
 860      * @param x  the X origin
 861      * @param y  the Y origin
 862      * @param p0 starting offset in the composed text to be rendered
 863      * @param p1 ending offset in the composed text to be rendered
 864      * @return  the new insertion position
 865      */
 866     static int drawComposedText(View view, AttributeSet attr, Graphics g,
 867                                 int x, int y, int p0, int p1)
 868                                      throws BadLocationException {
 869         Graphics2D g2d = (Graphics2D)g;
 870         AttributedString as = (AttributedString)attr.getAttribute(
 871             StyleConstants.ComposedTextAttribute);
 872         as.addAttribute(TextAttribute.FONT, g.getFont());
 873 
 874         if (p0 >= p1)
 875             return x;
 876 
 877         AttributedCharacterIterator aci = as.getIterator(null, p0, p1);
 878         return x + (int)SwingUtilities2.drawString(
 879                              getJComponent(view), g2d,aci,x,y);
 880     }
 881 
 882     /**
 883      * Paints the composed text in a GlyphView
 884      */
 885     static void paintComposedText(Graphics g, Rectangle alloc, GlyphView v) {
 886         if (g instanceof Graphics2D) {
 887             Graphics2D g2d = (Graphics2D) g;
 888             int p0 = v.getStartOffset();
 889             int p1 = v.getEndOffset();
 890             AttributeSet attrSet = v.getElement().getAttributes();
 891             AttributedString as =
 892                 (AttributedString)attrSet.getAttribute(StyleConstants.ComposedTextAttribute);
 893             int start = v.getElement().getStartOffset();
 894             int y = alloc.y + alloc.height - (int)v.getGlyphPainter().getDescent(v);
 895             int x = alloc.x;
 896 
 897             //Add text attributes
 898             as.addAttribute(TextAttribute.FONT, v.getFont());
 899             as.addAttribute(TextAttribute.FOREGROUND, v.getForeground());
 900             if (StyleConstants.isBold(v.getAttributes())) {
 901                 as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
 902             }
 903             if (StyleConstants.isItalic(v.getAttributes())) {
 904                 as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
 905             }
 906             if (v.isUnderline()) {
 907                 as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
 908             }
 909             if (v.isStrikeThrough()) {
 910                 as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
 911             }
 912             if (v.isSuperscript()) {
 913                 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER);
 914             }
 915             if (v.isSubscript()) {
 916                 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB);
 917             }
 918 
 919             // draw
 920             AttributedCharacterIterator aci = as.getIterator(null, p0 - start, p1 - start);
 921             SwingUtilities2.drawString(getJComponent(v),
 922                                        g2d,aci,x,y);
 923         }
 924     }
 925 
 926     /*
 927      * Convenience function for determining ComponentOrientation.  Helps us
 928      * avoid having Munge directives throughout the code.
 929      */
 930     static boolean isLeftToRight( java.awt.Component c ) {
 931         return c.getComponentOrientation().isLeftToRight();
 932     }
 933 
 934 
 935     /**
 936      * Provides a way to determine the next visually represented model
 937      * location that one might place a caret.  Some views may not be visible,
 938      * they might not be in the same order found in the model, or they just
 939      * might not allow access to some of the locations in the model.
 940      * <p>
 941      * This implementation assumes the views are layed out in a logical
 942      * manner. That is, that the view at index x + 1 is visually after
 943      * the View at index x, and that the View at index x - 1 is visually
 944      * before the View at x. There is support for reversing this behavior
 945      * only if the passed in <code>View</code> is an instance of
 946      * <code>CompositeView</code>. The <code>CompositeView</code>
 947      * must then override the <code>flipEastAndWestAtEnds</code> method.
 948      *
 949      * @param v View to query
 950      * @param pos the position to convert >= 0
 951      * @param a the allocated region to render into
 952      * @param direction the direction from the current position that can
 953      *  be thought of as the arrow keys typically found on a keyboard;
 954      *  this may be one of the following:
 955      *  <ul>
 956      *  <li><code>SwingConstants.WEST</code>
 957      *  <li><code>SwingConstants.EAST</code>
 958      *  <li><code>SwingConstants.NORTH</code>
 959      *  <li><code>SwingConstants.SOUTH</code>
 960      *  </ul>
 961      * @param biasRet an array contain the bias that was checked
 962      * @return the location within the model that best represents the next
 963      *  location visual position
 964      * @exception BadLocationException
 965      * @exception IllegalArgumentException if <code>direction</code> is invalid
 966      */
 967     static int getNextVisualPositionFrom(View v, int pos, Position.Bias b,
 968                                           Shape alloc, int direction,
 969                                           Position.Bias[] biasRet)
 970                              throws BadLocationException {
 971         if (v.getViewCount() == 0) {
 972             // Nothing to do.
 973             return pos;
 974         }
 975         boolean top = (direction == SwingConstants.NORTH ||
 976                        direction == SwingConstants.WEST);
 977         int retValue;
 978         if (pos == -1) {
 979             // Start from the first View.
 980             int childIndex = (top) ? v.getViewCount() - 1 : 0;
 981             View child = v.getView(childIndex);
 982             Shape childBounds = v.getChildAllocation(childIndex, alloc);
 983             retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
 984                                                        direction, biasRet);
 985             if (retValue == -1 && !top && v.getViewCount() > 1) {
 986                 // Special case that should ONLY happen if first view
 987                 // isn't valid (can happen when end position is put at
 988                 // beginning of line.
 989                 child = v.getView(1);
 990                 childBounds = v.getChildAllocation(1, alloc);
 991                 retValue = child.getNextVisualPositionFrom(-1, biasRet[0],
 992                                                            childBounds,
 993                                                            direction, biasRet);
 994             }
 995         }
 996         else {
 997             int increment = (top) ? -1 : 1;
 998             int childIndex;
 999             if (b == Position.Bias.Backward && pos > 0) {
1000                 childIndex = v.getViewIndex(pos - 1, Position.Bias.Forward);
1001             }
1002             else {
1003                 childIndex = v.getViewIndex(pos, Position.Bias.Forward);
1004             }
1005             View child = v.getView(childIndex);
1006             Shape childBounds = v.getChildAllocation(childIndex, alloc);
1007             retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
1008                                                        direction, biasRet);
1009             if ((direction == SwingConstants.EAST ||
1010                  direction == SwingConstants.WEST) &&
1011                 (v instanceof CompositeView) &&
1012                 ((CompositeView)v).flipEastAndWestAtEnds(pos, b)) {
1013                 increment *= -1;
1014             }
1015             childIndex += increment;
1016             if (retValue == -1 && childIndex >= 0 &&
1017                                   childIndex < v.getViewCount()) {
1018                 child = v.getView(childIndex);
1019                 childBounds = v.getChildAllocation(childIndex, alloc);
1020                 retValue = child.getNextVisualPositionFrom(
1021                                      -1, b, childBounds, direction, biasRet);
1022                 // If there is a bias change, it is a fake position
1023                 // and we should skip it. This is usually the result
1024                 // of two elements side be side flowing the same way.
1025                 if (retValue == pos && biasRet[0] != b) {
1026                     return getNextVisualPositionFrom(v, pos, biasRet[0],
1027                                                      alloc, direction,
1028                                                      biasRet);
1029                 }
1030             }
1031             else if (retValue != -1 && biasRet[0] != b &&
1032                      ((increment == 1 && child.getEndOffset() == retValue) ||
1033                       (increment == -1 &&
1034                        child.getStartOffset() == retValue)) &&
1035                      childIndex >= 0 && childIndex < v.getViewCount()) {
1036                 // Reached the end of a view, make sure the next view
1037                 // is a different direction.
1038                 child = v.getView(childIndex);
1039                 childBounds = v.getChildAllocation(childIndex, alloc);
1040                 Position.Bias originalBias = biasRet[0];
1041                 int nextPos = child.getNextVisualPositionFrom(
1042                                     -1, b, childBounds, direction, biasRet);
1043                 if (biasRet[0] == b) {
1044                     retValue = nextPos;
1045                 }
1046                 else {
1047                     biasRet[0] = originalBias;
1048                 }
1049             }
1050         }
1051         return retValue;
1052     }
1053 }