Source for java.awt.font.TextLayout

   1: /* TextLayout.java --
   2:    Copyright (C) 2006  Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10: 
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: 
  39: package java.awt.font;
  40: 
  41: import gnu.classpath.NotImplementedException;
  42: 
  43: import java.awt.Font;
  44: import java.awt.Graphics2D;
  45: import java.awt.Shape;
  46: import java.awt.Toolkit;
  47: import java.awt.geom.AffineTransform;
  48: import java.awt.geom.Rectangle2D;
  49: import java.awt.geom.GeneralPath;
  50: import java.awt.geom.Point2D;
  51: import java.text.AttributedCharacterIterator;
  52: import java.text.AttributedString;
  53: import java.text.Bidi;
  54: import java.util.Map;
  55: 
  56: /**
  57:  * @author Sven de Marothy
  58:  */
  59: public final class TextLayout implements Cloneable
  60: {
  61:   private GlyphVector[] runs;
  62:   private Font font;
  63:   private FontRenderContext frc;
  64:   private String string;
  65:   private Rectangle2D boundsCache;
  66:   private LineMetrics lm;
  67: 
  68:   /**
  69:    * Start and end character indices of the runs.
  70:    * First index is the run number, second is 0 or 1 for the starting 
  71:    * and ending character index of the run, respectively.
  72:    */
  73:   private int[][] runIndices;
  74: 
  75:   /**
  76:    * Base directionality, determined from the first char.
  77:    */
  78:   private boolean leftToRight;
  79: 
  80:   /**
  81:    * Whether this layout contains whitespace or not.
  82:    */
  83:   private boolean hasWhitespace = false;
  84: 
  85:   /**
  86:    * The default caret policy.
  87:    */
  88:   static TextLayout.CaretPolicy DEFAULT_CARET_POLICY = new CaretPolicy();
  89: 
  90:   /**
  91:    * Constructs a TextLayout.
  92:    */
  93:   public TextLayout (String string, Font font, FontRenderContext frc) 
  94:   {
  95:     this.font = font;
  96:     this.frc = frc;
  97:     this.string = string;
  98:     lm = font.getLineMetrics(string, frc);
  99: 
 100:     // Get base direction and whitespace info
 101:     getStringProperties();
 102: 
 103:     if( Bidi.requiresBidi( string.toCharArray(), 0, string.length() ) )
 104:       {
 105:     Bidi bidi = new Bidi( string, leftToRight ? 
 106:                   Bidi.DIRECTION_LEFT_TO_RIGHT : 
 107:                   Bidi.DIRECTION_RIGHT_TO_LEFT );
 108:     int rc = bidi.getRunCount();
 109:     byte[] table = new byte[ rc ];
 110:     for(int i = 0; i < table.length; i++)
 111:       table[i] = (byte)bidi.getRunLevel(i);
 112: 
 113:     runs = new GlyphVector[ rc ];
 114:     runIndices = new int[rc][2];
 115:     for(int i = 0; i < runs.length; i++)
 116:       {
 117:         runIndices[i][0] = bidi.getRunStart( i );
 118:         runIndices[i][1] = bidi.getRunLimit( i );
 119:         if( runIndices[i][0] != runIndices[i][1] ) // no empty runs.
 120:           {
 121:         runs[i] = font.layoutGlyphVector
 122:           ( frc, string.toCharArray(),
 123:             runIndices[i][0], runIndices[i][1],
 124:             ((table[i] & 1) == 0) ? Font.LAYOUT_LEFT_TO_RIGHT :
 125:             Font.LAYOUT_RIGHT_TO_LEFT );
 126:           }
 127:       }
 128:     Bidi.reorderVisually( table, 0, runs, 0, runs.length );
 129:       }
 130:     else
 131:       {
 132:     runs = new GlyphVector[ 1 ];
 133:     runIndices = new int[1][2];
 134:     runIndices[0][0] = 0;
 135:     runIndices[0][1] = string.length();
 136:     runs[ 0 ] = font.layoutGlyphVector( frc, string.toCharArray(), 
 137:                         0, string.length(),
 138:                         leftToRight ?
 139:                         Font.LAYOUT_LEFT_TO_RIGHT :
 140:                         Font.LAYOUT_RIGHT_TO_LEFT );
 141:       }
 142:   }
 143: 
 144:   public TextLayout (String string, Map attributes, FontRenderContext frc)  
 145:   {
 146:     this( string, new Font( attributes ), frc );
 147:   }
 148: 
 149:   public TextLayout (AttributedCharacterIterator text, FontRenderContext frc)
 150:     throws NotImplementedException
 151:   {
 152:     throw new Error ("not implemented");
 153:   }
 154: 
 155:   /**
 156:    * Scan the character run for the first strongly directional character,
 157:    * which in turn defines the base directionality of the whole layout.
 158:    */
 159:   private void getStringProperties()
 160:   {
 161:     boolean gotDirection = false;
 162:     int i = 0;
 163: 
 164:     leftToRight = true;
 165:     while( i < string.length() && !gotDirection )
 166:       switch( Character.getDirectionality( string.charAt( i++ ) ) )
 167:     {
 168:     case Character.DIRECTIONALITY_LEFT_TO_RIGHT:
 169:     case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING:
 170:     case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE:
 171:       gotDirection = true;
 172:       break;
 173:       
 174:     case Character.DIRECTIONALITY_RIGHT_TO_LEFT:
 175:     case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
 176:     case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING:
 177:     case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE:
 178:       leftToRight = false;
 179:       gotDirection = true;
 180:       break;
 181:     }
 182: 
 183:     // Determine if there's whitespace in the thing.
 184:     // Ignore trailing chars.
 185:     i = string.length() - 1; 
 186:     hasWhitespace = false;
 187:     while( i >= 0 && Character.isWhitespace( string.charAt(i) ) )
 188:       i--;
 189:     // Check the remaining chars
 190:     while( i >= 0 )
 191:       if( Character.isWhitespace( string.charAt(i--) ) )
 192:     hasWhitespace = true;
 193:   }
 194: 
 195:   protected Object clone ()
 196:   {
 197:     return new TextLayout( string, font, frc );
 198:   }
 199: 
 200:   public void draw (Graphics2D g2, float x, float y) 
 201:   {    
 202:     for(int i = 0; i < runs.length; i++)
 203:       {
 204:     g2.drawGlyphVector(runs[i], x, y);
 205:     Rectangle2D r = runs[i].getLogicalBounds();
 206:     x += r.getWidth();
 207:       }
 208:   }
 209: 
 210:   public boolean equals (Object obj)
 211:   {
 212:     if( !( obj instanceof TextLayout) )
 213:       return false;
 214: 
 215:     return equals( (TextLayout) obj );
 216:   }
 217: 
 218:   public boolean equals (TextLayout tl)
 219:   {
 220:     if( runs.length != tl.runs.length )
 221:       return false;
 222:     // Compare all glyph vectors.
 223:     for( int i = 0; i < runs.length; i++ )
 224:       if( !runs[i].equals( tl.runs[i] ) )
 225:     return false;
 226:     return true;
 227:   }
 228: 
 229:   public float getAdvance ()
 230:   {
 231:     float totalAdvance = 0f;
 232:     for(int i = 0; i < runs.length; i++)
 233:       totalAdvance += runs[i].getLogicalBounds().getWidth();
 234:     return totalAdvance;
 235:   }
 236: 
 237:   public float getAscent ()
 238:   {
 239:     return lm.getAscent();
 240:   }
 241: 
 242:   public byte getBaseline ()
 243:   {
 244:     return (byte)lm.getBaselineIndex();
 245:   }
 246: 
 247:   public float[] getBaselineOffsets ()
 248:   {
 249:     return lm.getBaselineOffsets();
 250:   }
 251: 
 252:   public Shape getBlackBoxBounds (int firstEndpoint, int secondEndpoint)
 253:   {
 254:     if( firstEndpoint < 0 || secondEndpoint > getCharacterCount() )
 255:       return new Rectangle2D.Float();
 256: 
 257:     GeneralPath gp = new GeneralPath();
 258:     int i = 0; // run index
 259:     double advance = 0;
 260: 
 261:     // go to first run
 262:     while( runIndices[i + 1][1] < firstEndpoint ) 
 263:       {
 264:     advance += runs[i].getLogicalBounds().getWidth();
 265:     i++;
 266:       }
 267: 
 268:     int j = 0; // index into the run.
 269:     if( runIndices[i][1] - runIndices[i][0] > 1 )
 270:       {
 271:     while( runs[i].getGlyphCharIndex( j + 1 ) <
 272:            (firstEndpoint - runIndices[i][0] ) )j++;
 273:       }
 274: 
 275:     gp.append(runs[i].getGlyphVisualBounds( j ), false);
 276:     boolean keepGoing = true;;
 277: 
 278:     do
 279:       {
 280:     while( j < runs[i].getNumGlyphs() && 
 281:            runs[i].getGlyphCharIndex( j ) + runIndices[i][0] < 
 282:            secondEndpoint )
 283:       {
 284:         Rectangle2D r2 = (runs[i].getGlyphVisualBounds( j )).
 285:           getBounds2D();
 286:         Point2D p = runs[i].getGlyphPosition( j );
 287:         r2.setRect( advance + p.getX(), r2.getY(), 
 288:             r2.getWidth(), r2.getHeight() );
 289:         gp.append(r2, false);
 290:         j++;
 291:       }
 292: 
 293:     if( j >= runs[i].getNumGlyphs() )
 294:       {
 295:         advance += runs[i].getLogicalBounds().getWidth();
 296:         i++; 
 297:         j = 0;
 298:       }
 299:     else
 300:       keepGoing = false;
 301:       }
 302:     while( keepGoing );
 303: 
 304:     return gp;
 305:   }
 306: 
 307:   public Rectangle2D getBounds()
 308:   {
 309:     if( boundsCache == null )
 310:       boundsCache = getOutline(new AffineTransform()).getBounds();
 311:     return boundsCache;
 312:   }
 313: 
 314:   public float[] getCaretInfo (TextHitInfo hit)
 315:   {
 316:     return getCaretInfo(hit, getBounds());
 317:   }
 318: 
 319:   public float[] getCaretInfo (TextHitInfo hit, Rectangle2D bounds)
 320:     throws NotImplementedException
 321:   {
 322:     throw new Error ("not implemented");
 323:   }
 324: 
 325:   public Shape getCaretShape (TextHitInfo hit)
 326:   {
 327:     return getCaretShape( hit, getBounds() );
 328:   }
 329: 
 330:   public Shape getCaretShape (TextHitInfo hit, Rectangle2D bounds)
 331:     throws NotImplementedException
 332:   {
 333:     throw new Error ("not implemented");
 334:   }
 335: 
 336:   public Shape[] getCaretShapes (int offset)
 337:   {
 338:     return getCaretShapes( offset, getBounds() );
 339:   }
 340: 
 341:   public Shape[] getCaretShapes (int offset, Rectangle2D bounds)
 342:     throws NotImplementedException
 343:   {
 344:     throw new Error ("not implemented");
 345:   }
 346: 
 347:   public int getCharacterCount ()
 348:   {
 349:     return string.length();
 350:   }
 351: 
 352:   public byte getCharacterLevel (int index)
 353:     throws NotImplementedException
 354:   {
 355:     throw new Error ("not implemented");
 356:   }
 357: 
 358:   public float getDescent ()
 359:   {
 360:     return lm.getDescent();
 361:   }
 362: 
 363:   public TextLayout getJustifiedLayout (float justificationWidth)
 364:   {
 365:     TextLayout newLayout = (TextLayout)clone();
 366: 
 367:     if( hasWhitespace )
 368:       newLayout.handleJustify( justificationWidth );
 369: 
 370:     return newLayout;
 371:   }
 372: 
 373:   public float getLeading ()
 374:   {
 375:     return lm.getLeading();
 376:   }
 377: 
 378:   public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint)
 379:   {
 380:     return getLogicalHighlightShape( firstEndpoint, secondEndpoint, 
 381:                      getBounds() );
 382:   }
 383: 
 384:   public Shape getLogicalHighlightShape (int firstEndpoint, int secondEndpoint,
 385:                                          Rectangle2D bounds)
 386:   {
 387:     if( firstEndpoint < 0 || secondEndpoint > getCharacterCount() )
 388:       return new Rectangle2D.Float();
 389: 
 390:     int i = 0; // run index
 391:     double advance = 0;
 392: 
 393:     // go to first run
 394:     if( i > 0 )
 395:       while( runIndices[i + 1][1] < firstEndpoint ) 
 396:     {
 397:       advance += runs[i].getLogicalBounds().getWidth();
 398:       i++;
 399:     }
 400: 
 401:     int j = 0; // index into the run.
 402:     if( runIndices[i][1] - runIndices[i][0] > 1 )
 403:       {
 404:     while( runs[i].getGlyphCharIndex( j + 1 ) <
 405:            (firstEndpoint - runIndices[i][0] ) )j++;
 406:       }
 407: 
 408:     Rectangle2D r = (runs[i].getGlyphLogicalBounds( j )).getBounds2D();
 409:     boolean keepGoing = true;;
 410: 
 411:     do
 412:       {
 413:     while( j < runs[i].getNumGlyphs() && 
 414:            runs[i].getGlyphCharIndex( j ) + runIndices[i][0] < 
 415:            secondEndpoint )
 416:       {
 417:         Rectangle2D r2 = (runs[i].getGlyphLogicalBounds( j )).
 418:           getBounds2D();
 419:         Point2D p = runs[i].getGlyphPosition( j );
 420:         r2.setRect( advance + p.getX(), r2.getY(), 
 421:             r2.getWidth(), r2.getHeight() );
 422:         r = r.createUnion( r2 );
 423:         j++;
 424:       }
 425: 
 426:     if( j >= runs[i].getNumGlyphs() )
 427:       {
 428:         advance += runs[i].getLogicalBounds().getWidth();
 429:         i++; 
 430:         j = 0;
 431:       }
 432:     else
 433:       keepGoing = false;
 434:       }
 435:     while( keepGoing );
 436: 
 437:     return r;
 438:   }
 439: 
 440:   public int[] getLogicalRangesForVisualSelection (TextHitInfo firstEndpoint,
 441:                                                    TextHitInfo secondEndpoint)
 442:     throws NotImplementedException
 443:   {
 444:     throw new Error ("not implemented");
 445:   }
 446: 
 447:   public TextHitInfo getNextLeftHit (int offset)
 448:     throws NotImplementedException
 449:   {
 450:     throw new Error ("not implemented");
 451:   }
 452: 
 453:   public TextHitInfo getNextLeftHit (TextHitInfo hit)
 454:     throws NotImplementedException
 455:   {
 456:     throw new Error ("not implemented");
 457:   }
 458: 
 459:   public TextHitInfo getNextRightHit (int offset)
 460:     throws NotImplementedException
 461:   {
 462:     throw new Error ("not implemented");
 463:   }
 464: 
 465:   public TextHitInfo getNextRightHit (TextHitInfo hit)
 466:     throws NotImplementedException
 467:   {
 468:     throw new Error ("not implemented");
 469:   }
 470: 
 471:   public Shape getOutline (AffineTransform tx)
 472:   {
 473:     float x = 0f;
 474:     GeneralPath gp = new GeneralPath();
 475:     for(int i = 0; i < runs.length; i++)
 476:       {
 477:     gp.append( runs[i].getOutline( x, 0f ), false );
 478:     Rectangle2D r = runs[i].getLogicalBounds();
 479:     x += r.getWidth();
 480:       }
 481:     if( tx != null )
 482:       gp.transform( tx );
 483:     return gp;
 484:   }
 485: 
 486:   public float getVisibleAdvance ()
 487:   {
 488:     float totalAdvance = 0f;
 489: 
 490:     if( runs.length <= 0 )
 491:       return 0f;
 492: 
 493:     // No trailing whitespace
 494:     if( !Character.isWhitespace( string.charAt( string.length() -1 ) ) )
 495:       return getAdvance();
 496: 
 497:     // Get length of all runs up to the last
 498:     for(int i = 0; i < runs.length - 1; i++)
 499:       totalAdvance += runs[i].getLogicalBounds().getWidth();
 500: 
 501:     int lastRun = runIndices[ runs.length - 1 ][0];
 502:     int j = string.length() - 1;
 503:     while( j >= lastRun && Character.isWhitespace( string.charAt( j ) ) ) j--;
 504: 
 505:     if( j < lastRun )
 506:       return totalAdvance; // entire last run is whitespace
 507: 
 508:     int lastNonWSChar = j - lastRun;
 509:     j = 0;
 510:     while( runs[ runs.length - 1 ].getGlyphCharIndex( j )
 511:        <= lastNonWSChar )
 512:       {
 513:     totalAdvance += runs[ runs.length - 1 ].getGlyphLogicalBounds( j ).
 514:       getBounds2D().getWidth();
 515:     j ++;
 516:       }
 517:     
 518:     return totalAdvance;
 519:   }
 520: 
 521:   public Shape getVisualHighlightShape (TextHitInfo firstEndpoint,
 522:                                         TextHitInfo secondEndpoint)
 523:   {
 524:     return getVisualHighlightShape( firstEndpoint, secondEndpoint, 
 525:                     getBounds() );
 526:   }
 527: 
 528:   public Shape getVisualHighlightShape (TextHitInfo firstEndpoint,
 529:                                         TextHitInfo secondEndpoint,
 530:                                         Rectangle2D bounds)
 531:     throws NotImplementedException
 532:   {
 533:     throw new Error ("not implemented");
 534:   }
 535: 
 536:   public TextHitInfo getVisualOtherHit (TextHitInfo hit)
 537:     throws NotImplementedException
 538:   {
 539:     throw new Error ("not implemented");
 540:   }
 541: 
 542:   /**
 543:    * This is a protected method of a <code>final</code> class, meaning
 544:    * it exists only to taunt you.
 545:    */
 546:   protected void handleJustify (float justificationWidth)
 547:   {
 548:     // We assume that the text has non-trailing whitespace.
 549:     // First get the change in width to insert into the whitespaces.
 550:     double deltaW = justificationWidth - getVisibleAdvance();
 551:     int nglyphs = 0; // # of whitespace chars
 552: 
 553:     // determine last non-whitespace char.
 554:     int lastNWS = string.length() - 1;
 555:     while( Character.isWhitespace( string.charAt( lastNWS ) ) ) lastNWS--;
 556: 
 557:     // locations of the glyphs.
 558:     int[] wsglyphs = new int[string.length() * 10];
 559:     for(int run = 0; run < runs.length; run++ )
 560:       for(int i = 0; i < runs[run].getNumGlyphs(); i++ )
 561:     {
 562:       int cindex = runIndices[run][0] + runs[run].getGlyphCharIndex( i );
 563:       if( Character.isWhitespace( string.charAt( cindex ) ) )
 564:         //          && cindex < lastNWS )
 565:         {
 566:           wsglyphs[ nglyphs * 2 ] = run;
 567:           wsglyphs[ nglyphs * 2 + 1] = i;
 568:           nglyphs++;
 569:         }
 570:     }
 571: 
 572:     deltaW = deltaW / nglyphs; // Change in width per whitespace glyph
 573:     double w = 0;
 574:     int cws = 0;
 575:     // Shift all characters
 576:     for(int run = 0; run < runs.length; run++ )
 577:       for(int i = 0; i < runs[ run ].getNumGlyphs(); i++ )
 578:     {
 579:       if( wsglyphs[ cws * 2 ] == run && wsglyphs[ cws * 2 + 1 ] == i )
 580:         {
 581:           cws++; // update 'current whitespace'
 582:           w += deltaW; // increment the shift
 583:         }
 584:       Point2D p = runs[ run ].getGlyphPosition( i );
 585:       p.setLocation( p.getX() + w, p.getY() );
 586:       runs[ run ].setGlyphPosition( i, p );
 587:     }
 588:   }
 589: 
 590:   public TextHitInfo hitTestChar (float x, float y)
 591:   {
 592:     return hitTestChar(x, y, getBounds());
 593:   }
 594: 
 595:   public TextHitInfo hitTestChar (float x, float y, Rectangle2D bounds)
 596:   {
 597:     return hitTestChar( x, y, getBounds() );
 598:   }
 599: 
 600:   public boolean isLeftToRight ()
 601:   {
 602:     return leftToRight;
 603:   }
 604: 
 605:   public boolean isVertical ()
 606:   {
 607:     return false; // FIXME: How do you create a vertical layout?
 608:   }
 609: 
 610:   public int hashCode ()
 611:     throws NotImplementedException
 612:   {
 613:     throw new Error ("not implemented");
 614:   }
 615: 
 616:   public String toString ()
 617:   {
 618:     return "TextLayout [string:"+string+", Font:"+font+" Rendercontext:"+
 619:       frc+"]";
 620:   }
 621: 
 622:   /**
 623:    * Inner class describing a caret policy
 624:    */
 625:   public static class CaretPolicy
 626:   {
 627:     public CaretPolicy()
 628:     {
 629:     }
 630: 
 631:     public TextHitInfo getStrongCaret(TextHitInfo hit1,
 632:                       TextHitInfo hit2,
 633:                       TextLayout layout)
 634:       throws NotImplementedException
 635:     {
 636:       throw new Error ("not implemented");
 637:     }
 638:   }
 639: }
 640: