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