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