1 /*
   2  * Copyright (c) 1997, 2015, 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 &gt;= 0
  75      * @param y  the Y origin &gt;= 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 &gt;= 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 &gt;= 0
  96      * @param y  the Y origin &gt;= 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 &gt;= 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 &gt;= 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 &gt;= 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 &gt;= 0.
 293      * @param x  the target view location to translate to an
 294      *   offset into the text &gt;= 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 &gt;= 0
 298      * @return  the offset into the text &gt;= 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     /**
 315      * Determines the relative offset into the given text that
 316      * best represents the given span in the view coordinate
 317      * system.
 318      *
 319      * @param s  the source of the text
 320      * @param metrics the font metrics to use for the calculation
 321      * @param x0 the starting view location representing the start
 322      *   of the given text &gt;= 0.
 323      * @param x  the target view location to translate to an
 324      *   offset into the text &gt;= 0.
 325      * @param e  how to expand the tabs.  If this value is null,
 326      *   tabs will be expanded as a space character.
 327      * @param startOffset starting offset of the text in the document &gt;= 0
 328      * @param round whether or not to round
 329      * @return  the offset into the text &gt;= 0
 330      */
 331     public static final int getTabbedTextOffset(Segment s,
 332                                                 FontMetrics metrics,
 333                                                 int x0, int x, TabExpander e,
 334                                                 int startOffset,
 335                                                 boolean round) {
 336         return getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, round, null);
 337     }
 338 
 339     // In addition to the previous method it can extend spaces for
 340     // justification.
 341     //
 342     // all params are the same as in the preious method except the last
 343     // one:
 344     // @param justificationData justificationData for the row.
 345     // if null not justification is needed
 346     static final int getTabbedTextOffset(View view,
 347                                          Segment s,
 348                                          FontMetrics metrics,
 349                                          int x0, int x, TabExpander e,
 350                                          int startOffset,
 351                                          boolean round,
 352                                          int[] justificationData) {
 353         if (x0 >= x) {
 354             // x before x0, return.
 355             return 0;
 356         }
 357         int nextX = x0;
 358         // s may be a shared segment, so it is copied prior to calling
 359         // the tab expander
 360         char[] txt = s.array;
 361         int txtOffset = s.offset;
 362         int txtCount = s.count;
 363         int spaceAddon = 0 ;
 364         int spaceAddonLeftoverEnd = -1;
 365         int startJustifiableContent = 0 ;
 366         int endJustifiableContent = 0;
 367         if (justificationData != null) {
 368             int offset = - startOffset + txtOffset;
 369             View parent = null;
 370             if (view != null
 371                   && (parent = view.getParent()) != null) {
 372                 offset += parent.getStartOffset();
 373             }
 374             spaceAddon =
 375                 justificationData[Row.SPACE_ADDON];
 376             spaceAddonLeftoverEnd =
 377                 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
 378             startJustifiableContent =
 379                 justificationData[Row.START_JUSTIFIABLE] + offset;
 380             endJustifiableContent =
 381                 justificationData[Row.END_JUSTIFIABLE] + offset;
 382         }
 383         int n = s.offset + s.count;
 384         for (int i = s.offset; i < n; i++) {
 385             if (txt[i] == '\t'
 386                 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
 387                     && (txt[i] == ' ')
 388                     && startJustifiableContent <= i
 389                     && i <= endJustifiableContent
 390                     )){
 391                 if (txt[i] == '\t') {
 392                     if (e != null) {
 393                         nextX = (int) e.nextTabStop((float) nextX,
 394                                                     startOffset + i - txtOffset);
 395                     } else {
 396                         nextX += metrics.charWidth(' ');
 397                     }
 398                 } else if (txt[i] == ' ') {
 399                     nextX += metrics.charWidth(' ') + spaceAddon;
 400                     if (i <= spaceAddonLeftoverEnd) {
 401                         nextX++;
 402                     }
 403                 }
 404             } else {
 405                 nextX += metrics.charWidth(txt[i]);
 406             }
 407             if (x < nextX) {
 408                 // found the hit position... return the appropriate side
 409                 int offset;
 410 
 411                 // the length of the string measured as a whole may differ from
 412                 // the sum of individual character lengths, for example if
 413                 // fractional metrics are enabled; and we must guard from this.
 414                 if (round) {
 415                     offset = i + 1 - txtOffset;
 416 
 417                     int width = metrics.charsWidth(txt, txtOffset, offset);
 418                     int span = x - x0;
 419 
 420                     if (span < width) {
 421                         while (offset > 0) {
 422                             int nextWidth = offset > 1 ? metrics.charsWidth(txt, txtOffset, offset - 1) : 0;
 423 
 424                             if (span >= nextWidth) {
 425                                 if (span - nextWidth < width - span) {
 426                                     offset--;
 427                                 }
 428 
 429                                 break;
 430                             }
 431 
 432                             width = nextWidth;
 433                             offset--;
 434                         }
 435                     }
 436                 } else {
 437                     offset = i - txtOffset;
 438 
 439                     while (offset > 0 && metrics.charsWidth(txt, txtOffset, offset) > (x - x0)) {
 440                         offset--;
 441                     }
 442                 }
 443 
 444                 return offset;
 445             }
 446         }
 447 
 448         // didn't find, return end offset
 449         return txtCount;
 450     }
 451 
 452     /**
 453      * Determine where to break the given text to fit
 454      * within the given span. This tries to find a word boundary.
 455      * @param s  the source of the text
 456      * @param metrics the font metrics to use for the calculation
 457      * @param x0 the starting view location representing the start
 458      *   of the given text.
 459      * @param x  the target view location to translate to an
 460      *   offset into the text.
 461      * @param e  how to expand the tabs.  If this value is null,
 462      *   tabs will be expanded as a space character.
 463      * @param startOffset starting offset in the document of the text
 464      * @return  the offset into the given text
 465      */
 466     public static final int getBreakLocation(Segment s, FontMetrics metrics,
 467                                              int x0, int x, TabExpander e,
 468                                              int startOffset) {
 469         char[] txt = s.array;
 470         int txtOffset = s.offset;
 471         int txtCount = s.count;
 472         int index = Utilities.getTabbedTextOffset(s, metrics, x0, x,
 473                                                   e, startOffset, false);
 474 
 475         if (index >= txtCount - 1) {
 476             return txtCount;
 477         }
 478 
 479         for (int i = txtOffset + index; i >= txtOffset; i--) {
 480             char ch = txt[i];
 481             if (ch < 256) {
 482                 // break on whitespace
 483                 if (Character.isWhitespace(ch)) {
 484                     index = i - txtOffset + 1;
 485                     break;
 486                 }
 487             } else {
 488                 // a multibyte char found; use BreakIterator to find line break
 489                 BreakIterator bit = BreakIterator.getLineInstance();
 490                 bit.setText(s);
 491                 int breakPos = bit.preceding(i + 1);
 492                 if (breakPos > txtOffset) {
 493                     index = breakPos - txtOffset;
 494                 }
 495                 break;
 496             }
 497         }
 498         return index;
 499     }
 500 
 501     /**
 502      * Determines the starting row model position of the row that contains
 503      * the specified model position.  The component given must have a
 504      * size to compute the result.  If the component doesn't have a size
 505      * a value of -1 will be returned.
 506      *
 507      * @param c the editor
 508      * @param offs the offset in the document &gt;= 0
 509      * @return the position &gt;= 0 if the request can be computed, otherwise
 510      *  a value of -1 will be returned.
 511      * @exception BadLocationException if the offset is out of range
 512      */
 513     public static final int getRowStart(JTextComponent c, int offs) throws BadLocationException {
 514         Rectangle r = c.modelToView(offs);
 515         if (r == null) {
 516             return -1;
 517         }
 518         int lastOffs = offs;
 519         int y = r.y;
 520         while ((r != null) && (y == r.y)) {
 521             // Skip invisible elements
 522             if(r.height !=0) {
 523                 offs = lastOffs;
 524             }
 525             lastOffs -= 1;
 526             r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
 527         }
 528         return offs;
 529     }
 530 
 531     /**
 532      * Determines the ending row model position of the row that contains
 533      * the specified model position.  The component given must have a
 534      * size to compute the result.  If the component doesn't have a size
 535      * a value of -1 will be returned.
 536      *
 537      * @param c the editor
 538      * @param offs the offset in the document &gt;= 0
 539      * @return the position &gt;= 0 if the request can be computed, otherwise
 540      *  a value of -1 will be returned.
 541      * @exception BadLocationException if the offset is out of range
 542      */
 543     public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException {
 544         Rectangle r = c.modelToView(offs);
 545         if (r == null) {
 546             return -1;
 547         }
 548         int n = c.getDocument().getLength();
 549         int lastOffs = offs;
 550         int y = r.y;
 551         while ((r != null) && (y == r.y)) {
 552             // Skip invisible elements
 553             if (r.height !=0) {
 554                 offs = lastOffs;
 555             }
 556             lastOffs += 1;
 557             r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
 558         }
 559         return offs;
 560     }
 561 
 562     /**
 563      * Determines the position in the model that is closest to the given
 564      * view location in the row above.  The component given must have a
 565      * size to compute the result.  If the component doesn't have a size
 566      * a value of -1 will be returned.
 567      *
 568      * @param c the editor
 569      * @param offs the offset in the document &gt;= 0
 570      * @param x the X coordinate &gt;= 0
 571      * @return the position &gt;= 0 if the request can be computed, otherwise
 572      *  a value of -1 will be returned.
 573      * @exception BadLocationException if the offset is out of range
 574      */
 575     public static final int getPositionAbove(JTextComponent c, int offs, int x) throws BadLocationException {
 576         int lastOffs = getRowStart(c, offs) - 1;
 577         if (lastOffs < 0) {
 578             return -1;
 579         }
 580         int bestSpan = Integer.MAX_VALUE;
 581         int y = 0;
 582         Rectangle r = null;
 583         if (lastOffs >= 0) {
 584             r = c.modelToView(lastOffs);
 585             y = r.y;
 586         }
 587         while ((r != null) && (y == r.y)) {
 588             int span = Math.abs(r.x - x);
 589             if (span < bestSpan) {
 590                 offs = lastOffs;
 591                 bestSpan = span;
 592             }
 593             lastOffs -= 1;
 594             r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
 595         }
 596         return offs;
 597     }
 598 
 599     /**
 600      * Determines the position in the model that is closest to the given
 601      * view location in the row below.  The component given must have a
 602      * size to compute the result.  If the component doesn't have a size
 603      * a value of -1 will be returned.
 604      *
 605      * @param c the editor
 606      * @param offs the offset in the document &gt;= 0
 607      * @param x the X coordinate &gt;= 0
 608      * @return the position &gt;= 0 if the request can be computed, otherwise
 609      *  a value of -1 will be returned.
 610      * @exception BadLocationException if the offset is out of range
 611      */
 612     public static final int getPositionBelow(JTextComponent c, int offs, int x) throws BadLocationException {
 613         int lastOffs = getRowEnd(c, offs) + 1;
 614         if (lastOffs <= 0) {
 615             return -1;
 616         }
 617         int bestSpan = Integer.MAX_VALUE;
 618         int n = c.getDocument().getLength();
 619         int y = 0;
 620         Rectangle r = null;
 621         if (lastOffs <= n) {
 622             r = c.modelToView(lastOffs);
 623             y = r.y;
 624         }
 625         while ((r != null) && (y == r.y)) {
 626             int span = Math.abs(x - r.x);
 627             if (span < bestSpan) {
 628                 offs = lastOffs;
 629                 bestSpan = span;
 630             }
 631             lastOffs += 1;
 632             r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
 633         }
 634         return offs;
 635     }
 636 
 637     /**
 638      * Determines the start of a word for the given model location.
 639      * Uses BreakIterator.getWordInstance() to actually get the words.
 640      *
 641      * @param c the editor
 642      * @param offs the offset in the document &gt;= 0
 643      * @return the location in the model of the word start &gt;= 0
 644      * @exception BadLocationException if the offset is out of range
 645      */
 646     public static final int getWordStart(JTextComponent c, int offs) throws BadLocationException {
 647         Document doc = c.getDocument();
 648         Element line = getParagraphElement(c, offs);
 649         if (line == null) {
 650             throw new BadLocationException("No word at " + offs, offs);
 651         }
 652         int lineStart = line.getStartOffset();
 653         int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
 654 
 655         Segment seg = SegmentCache.getSharedSegment();
 656         doc.getText(lineStart, lineEnd - lineStart, seg);
 657         if(seg.count > 0) {
 658             BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
 659             words.setText(seg);
 660             int wordPosition = seg.offset + offs - lineStart;
 661             if(wordPosition >= words.last()) {
 662                 wordPosition = words.last() - 1;
 663             }
 664             words.following(wordPosition);
 665             offs = lineStart + words.previous() - seg.offset;
 666         }
 667         SegmentCache.releaseSharedSegment(seg);
 668         return offs;
 669     }
 670 
 671     /**
 672      * Determines the end of a word for the given location.
 673      * Uses BreakIterator.getWordInstance() to actually get the words.
 674      *
 675      * @param c the editor
 676      * @param offs the offset in the document &gt;= 0
 677      * @return the location in the model of the word end &gt;= 0
 678      * @exception BadLocationException if the offset is out of range
 679      */
 680     public static final int getWordEnd(JTextComponent c, int offs) throws BadLocationException {
 681         Document doc = c.getDocument();
 682         Element line = getParagraphElement(c, offs);
 683         if (line == null) {
 684             throw new BadLocationException("No word at " + offs, offs);
 685         }
 686         int lineStart = line.getStartOffset();
 687         int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
 688 
 689         Segment seg = SegmentCache.getSharedSegment();
 690         doc.getText(lineStart, lineEnd - lineStart, seg);
 691         if(seg.count > 0) {
 692             BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
 693             words.setText(seg);
 694             int wordPosition = offs - lineStart + seg.offset;
 695             if(wordPosition >= words.last()) {
 696                 wordPosition = words.last() - 1;
 697             }
 698             offs = lineStart + words.following(wordPosition) - seg.offset;
 699         }
 700         SegmentCache.releaseSharedSegment(seg);
 701         return offs;
 702     }
 703 
 704     /**
 705      * Determines the start of the next word for the given location.
 706      * Uses BreakIterator.getWordInstance() to actually get the words.
 707      *
 708      * @param c the editor
 709      * @param offs the offset in the document &gt;= 0
 710      * @return the location in the model of the word start &gt;= 0
 711      * @exception BadLocationException if the offset is out of range
 712      */
 713     public static final int getNextWord(JTextComponent c, int offs) throws BadLocationException {
 714         int nextWord;
 715         Element line = getParagraphElement(c, offs);
 716         for (nextWord = getNextWordInParagraph(c, line, offs, false);
 717              nextWord == BreakIterator.DONE;
 718              nextWord = getNextWordInParagraph(c, line, offs, true)) {
 719 
 720             // didn't find in this line, try the next line
 721             offs = line.getEndOffset();
 722             line = getParagraphElement(c, offs);
 723         }
 724         return nextWord;
 725     }
 726 
 727     /**
 728      * Finds the next word in the given elements text.  The first
 729      * parameter allows searching multiple paragraphs where even
 730      * the first offset is desired.
 731      * Returns the offset of the next word, or BreakIterator.DONE
 732      * if there are no more words in the element.
 733      */
 734     static int getNextWordInParagraph(JTextComponent c, Element line, int offs, boolean first) throws BadLocationException {
 735         if (line == null) {
 736             throw new BadLocationException("No more words", offs);
 737         }
 738         Document doc = line.getDocument();
 739         int lineStart = line.getStartOffset();
 740         int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
 741         if ((offs >= lineEnd) || (offs < lineStart)) {
 742             throw new BadLocationException("No more words", offs);
 743         }
 744         Segment seg = SegmentCache.getSharedSegment();
 745         doc.getText(lineStart, lineEnd - lineStart, seg);
 746         BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
 747         words.setText(seg);
 748         if ((first && (words.first() == (seg.offset + offs - lineStart))) &&
 749             (! Character.isWhitespace(seg.array[words.first()]))) {
 750 
 751             return offs;
 752         }
 753         int wordPosition = words.following(seg.offset + offs - lineStart);
 754         if ((wordPosition == BreakIterator.DONE) ||
 755             (wordPosition >= seg.offset + seg.count)) {
 756                 // there are no more words on this line.
 757                 return BreakIterator.DONE;
 758         }
 759         // if we haven't shot past the end... check to
 760         // see if the current boundary represents whitespace.
 761         // if so, we need to try again
 762         char ch = seg.array[wordPosition];
 763         if (! Character.isWhitespace(ch)) {
 764             return lineStart + wordPosition - seg.offset;
 765         }
 766 
 767         // it was whitespace, try again.  The assumption
 768         // is that it must be a word start if the last
 769         // one had whitespace following it.
 770         wordPosition = words.next();
 771         if (wordPosition != BreakIterator.DONE) {
 772             offs = lineStart + wordPosition - seg.offset;
 773             if (offs != lineEnd) {
 774                 return offs;
 775             }
 776         }
 777         SegmentCache.releaseSharedSegment(seg);
 778         return BreakIterator.DONE;
 779     }
 780 
 781 
 782     /**
 783      * Determine the start of the prev word for the given location.
 784      * Uses BreakIterator.getWordInstance() to actually get the words.
 785      *
 786      * @param c the editor
 787      * @param offs the offset in the document &gt;= 0
 788      * @return the location in the model of the word start &gt;= 0
 789      * @exception BadLocationException if the offset is out of range
 790      */
 791     public static final int getPreviousWord(JTextComponent c, int offs) throws BadLocationException {
 792         int prevWord;
 793         Element line = getParagraphElement(c, offs);
 794         for (prevWord = getPrevWordInParagraph(c, line, offs);
 795              prevWord == BreakIterator.DONE;
 796              prevWord = getPrevWordInParagraph(c, line, offs)) {
 797 
 798             // didn't find in this line, try the prev line
 799             offs = line.getStartOffset() - 1;
 800             line = getParagraphElement(c, offs);
 801         }
 802         return prevWord;
 803     }
 804 
 805     /**
 806      * Finds the previous word in the given elements text.  The first
 807      * parameter allows searching multiple paragraphs where even
 808      * the first offset is desired.
 809      * Returns the offset of the next word, or BreakIterator.DONE
 810      * if there are no more words in the element.
 811      */
 812     static int getPrevWordInParagraph(JTextComponent c, Element line, int offs) throws BadLocationException {
 813         if (line == null) {
 814             throw new BadLocationException("No more words", offs);
 815         }
 816         Document doc = line.getDocument();
 817         int lineStart = line.getStartOffset();
 818         int lineEnd = line.getEndOffset();
 819         if ((offs > lineEnd) || (offs < lineStart)) {
 820             throw new BadLocationException("No more words", offs);
 821         }
 822         Segment seg = SegmentCache.getSharedSegment();
 823         doc.getText(lineStart, lineEnd - lineStart, seg);
 824         BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
 825         words.setText(seg);
 826         if (words.following(seg.offset + offs - lineStart) == BreakIterator.DONE) {
 827             words.last();
 828         }
 829         int wordPosition = words.previous();
 830         if (wordPosition == (seg.offset + offs - lineStart)) {
 831             wordPosition = words.previous();
 832         }
 833 
 834         if (wordPosition == BreakIterator.DONE) {
 835             // there are no more words on this line.
 836             return BreakIterator.DONE;
 837         }
 838         // if we haven't shot past the end... check to
 839         // see if the current boundary represents whitespace.
 840         // if so, we need to try again
 841         char ch = seg.array[wordPosition];
 842         if (! Character.isWhitespace(ch)) {
 843             return lineStart + wordPosition - seg.offset;
 844         }
 845 
 846         // it was whitespace, try again.  The assumption
 847         // is that it must be a word start if the last
 848         // one had whitespace following it.
 849         wordPosition = words.previous();
 850         if (wordPosition != BreakIterator.DONE) {
 851             return lineStart + wordPosition - seg.offset;
 852         }
 853         SegmentCache.releaseSharedSegment(seg);
 854         return BreakIterator.DONE;
 855     }
 856 
 857     /**
 858      * Determines the element to use for a paragraph/line.
 859      *
 860      * @param c the editor
 861      * @param offs the starting offset in the document &gt;= 0
 862      * @return the element
 863      */
 864     public static final Element getParagraphElement(JTextComponent c, int offs) {
 865         Document doc = c.getDocument();
 866         if (doc instanceof StyledDocument) {
 867             return ((StyledDocument)doc).getParagraphElement(offs);
 868         }
 869         Element map = doc.getDefaultRootElement();
 870         int index = map.getElementIndex(offs);
 871         Element paragraph = map.getElement(index);
 872         if ((offs >= paragraph.getStartOffset()) && (offs < paragraph.getEndOffset())) {
 873             return paragraph;
 874         }
 875         return null;
 876     }
 877 
 878     static boolean isComposedTextElement(Document doc, int offset) {
 879         Element elem = doc.getDefaultRootElement();
 880         while (!elem.isLeaf()) {
 881             elem = elem.getElement(elem.getElementIndex(offset));
 882         }
 883         return isComposedTextElement(elem);
 884     }
 885 
 886     static boolean isComposedTextElement(Element elem) {
 887         AttributeSet as = elem.getAttributes();
 888         return isComposedTextAttributeDefined(as);
 889     }
 890 
 891     static boolean isComposedTextAttributeDefined(AttributeSet as) {
 892         return ((as != null) &&
 893                 (as.isDefined(StyleConstants.ComposedTextAttribute)));
 894     }
 895 
 896     /**
 897      * Draws the given composed text passed from an input method.
 898      *
 899      * @param view View hosting text
 900      * @param attr the attributes containing the composed text
 901      * @param g  the graphics context
 902      * @param x  the X origin
 903      * @param y  the Y origin
 904      * @param p0 starting offset in the composed text to be rendered
 905      * @param p1 ending offset in the composed text to be rendered
 906      * @return  the new insertion position
 907      */
 908     static int drawComposedText(View view, AttributeSet attr, Graphics g,
 909                                 int x, int y, int p0, int p1)
 910                                      throws BadLocationException {
 911         Graphics2D g2d = (Graphics2D)g;
 912         AttributedString as = (AttributedString)attr.getAttribute(
 913             StyleConstants.ComposedTextAttribute);
 914         as.addAttribute(TextAttribute.FONT, g.getFont());
 915 
 916         if (p0 >= p1)
 917             return x;
 918 
 919         AttributedCharacterIterator aci = as.getIterator(null, p0, p1);
 920         return x + (int)SwingUtilities2.drawString(
 921                              getJComponent(view), g2d,aci,x,y);
 922     }
 923 
 924     /**
 925      * Paints the composed text in a GlyphView
 926      */
 927     static void paintComposedText(Graphics g, Rectangle alloc, GlyphView v) {
 928         if (g instanceof Graphics2D) {
 929             Graphics2D g2d = (Graphics2D) g;
 930             int p0 = v.getStartOffset();
 931             int p1 = v.getEndOffset();
 932             AttributeSet attrSet = v.getElement().getAttributes();
 933             AttributedString as =
 934                 (AttributedString)attrSet.getAttribute(StyleConstants.ComposedTextAttribute);
 935             int start = v.getElement().getStartOffset();
 936             int y = alloc.y + alloc.height - (int)v.getGlyphPainter().getDescent(v);
 937             int x = alloc.x;
 938 
 939             //Add text attributes
 940             as.addAttribute(TextAttribute.FONT, v.getFont());
 941             as.addAttribute(TextAttribute.FOREGROUND, v.getForeground());
 942             if (StyleConstants.isBold(v.getAttributes())) {
 943                 as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
 944             }
 945             if (StyleConstants.isItalic(v.getAttributes())) {
 946                 as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
 947             }
 948             if (v.isUnderline()) {
 949                 as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
 950             }
 951             if (v.isStrikeThrough()) {
 952                 as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
 953             }
 954             if (v.isSuperscript()) {
 955                 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER);
 956             }
 957             if (v.isSubscript()) {
 958                 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB);
 959             }
 960 
 961             // draw
 962             AttributedCharacterIterator aci = as.getIterator(null, p0 - start, p1 - start);
 963             SwingUtilities2.drawString(getJComponent(v),
 964                                        g2d,aci,x,y);
 965         }
 966     }
 967 
 968     /*
 969      * Convenience function for determining ComponentOrientation.  Helps us
 970      * avoid having Munge directives throughout the code.
 971      */
 972     static boolean isLeftToRight( java.awt.Component c ) {
 973         return c.getComponentOrientation().isLeftToRight();
 974     }
 975 
 976 
 977     /**
 978      * Provides a way to determine the next visually represented model
 979      * location that one might place a caret.  Some views may not be visible,
 980      * they might not be in the same order found in the model, or they just
 981      * might not allow access to some of the locations in the model.
 982      * <p>
 983      * This implementation assumes the views are layed out in a logical
 984      * manner. That is, that the view at index x + 1 is visually after
 985      * the View at index x, and that the View at index x - 1 is visually
 986      * before the View at x. There is support for reversing this behavior
 987      * only if the passed in <code>View</code> is an instance of
 988      * <code>CompositeView</code>. The <code>CompositeView</code>
 989      * must then override the <code>flipEastAndWestAtEnds</code> method.
 990      *
 991      * @param v View to query
 992      * @param pos the position to convert &gt;= 0
 993      * @param a the allocated region to render into
 994      * @param direction the direction from the current position that can
 995      *  be thought of as the arrow keys typically found on a keyboard;
 996      *  this may be one of the following:
 997      *  <ul>
 998      *  <li><code>SwingConstants.WEST</code>
 999      *  <li><code>SwingConstants.EAST</code>
1000      *  <li><code>SwingConstants.NORTH</code>
1001      *  <li><code>SwingConstants.SOUTH</code>
1002      *  </ul>
1003      * @param biasRet an array contain the bias that was checked
1004      * @return the location within the model that best represents the next
1005      *  location visual position
1006      * @exception BadLocationException
1007      * @exception IllegalArgumentException if <code>direction</code> is invalid
1008      */
1009     static int getNextVisualPositionFrom(View v, int pos, Position.Bias b,
1010                                           Shape alloc, int direction,
1011                                           Position.Bias[] biasRet)
1012                              throws BadLocationException {
1013         if (v.getViewCount() == 0) {
1014             // Nothing to do.
1015             return pos;
1016         }
1017         boolean top = (direction == SwingConstants.NORTH ||
1018                        direction == SwingConstants.WEST);
1019         int retValue;
1020         if (pos == -1) {
1021             // Start from the first View.
1022             int childIndex = (top) ? v.getViewCount() - 1 : 0;
1023             View child = v.getView(childIndex);
1024             Shape childBounds = v.getChildAllocation(childIndex, alloc);
1025             retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
1026                                                        direction, biasRet);
1027             if (retValue == -1 && !top && v.getViewCount() > 1) {
1028                 // Special case that should ONLY happen if first view
1029                 // isn't valid (can happen when end position is put at
1030                 // beginning of line.
1031                 child = v.getView(1);
1032                 childBounds = v.getChildAllocation(1, alloc);
1033                 retValue = child.getNextVisualPositionFrom(-1, biasRet[0],
1034                                                            childBounds,
1035                                                            direction, biasRet);
1036             }
1037         }
1038         else {
1039             int increment = (top) ? -1 : 1;
1040             int childIndex;
1041             if (b == Position.Bias.Backward && pos > 0) {
1042                 childIndex = v.getViewIndex(pos - 1, Position.Bias.Forward);
1043             }
1044             else {
1045                 childIndex = v.getViewIndex(pos, Position.Bias.Forward);
1046             }
1047             View child = v.getView(childIndex);
1048             Shape childBounds = v.getChildAllocation(childIndex, alloc);
1049             retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
1050                                                        direction, biasRet);
1051             if ((direction == SwingConstants.EAST ||
1052                  direction == SwingConstants.WEST) &&
1053                 (v instanceof CompositeView) &&
1054                 ((CompositeView)v).flipEastAndWestAtEnds(pos, b)) {
1055                 increment *= -1;
1056             }
1057             childIndex += increment;
1058             if (retValue == -1 && childIndex >= 0 &&
1059                                   childIndex < v.getViewCount()) {
1060                 child = v.getView(childIndex);
1061                 childBounds = v.getChildAllocation(childIndex, alloc);
1062                 retValue = child.getNextVisualPositionFrom(
1063                                      -1, b, childBounds, direction, biasRet);
1064                 // If there is a bias change, it is a fake position
1065                 // and we should skip it. This is usually the result
1066                 // of two elements side be side flowing the same way.
1067                 if (retValue == pos && biasRet[0] != b) {
1068                     return getNextVisualPositionFrom(v, pos, biasRet[0],
1069                                                      alloc, direction,
1070                                                      biasRet);
1071                 }
1072             }
1073             else if (retValue != -1 && biasRet[0] != b &&
1074                      ((increment == 1 && child.getEndOffset() == retValue) ||
1075                       (increment == -1 &&
1076                        child.getStartOffset() == retValue)) &&
1077                      childIndex >= 0 && childIndex < v.getViewCount()) {
1078                 // Reached the end of a view, make sure the next view
1079                 // is a different direction.
1080                 child = v.getView(childIndex);
1081                 childBounds = v.getChildAllocation(childIndex, alloc);
1082                 Position.Bias originalBias = biasRet[0];
1083                 int nextPos = child.getNextVisualPositionFrom(
1084                                     -1, b, childBounds, direction, biasRet);
1085                 if (biasRet[0] == b) {
1086                     retValue = nextPos;
1087                 }
1088                 else {
1089                     biasRet[0] = originalBias;
1090                 }
1091             }
1092         }
1093         return retValue;
1094     }
1095 }