1 /*
   2  * Copyright (c) 1999, 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  */
  26 
  27 /*
  28  * (C) Copyright IBM Corp. 1999,  All rights reserved.
  29  */
  30 package java.awt.font;
  31 
  32 import java.awt.Font;
  33 import java.awt.Toolkit;
  34 import java.awt.im.InputMethodHighlight;
  35 import java.text.Annotation;
  36 import java.text.AttributedCharacterIterator;
  37 import java.text.AttributedCharacterIterator.Attribute;
  38 import java.util.Vector;
  39 import java.util.HashMap;
  40 import java.util.Map;
  41 import sun.font.CodePointIterator;
  42 import sun.font.Decoration;
  43 import sun.font.FontResolver;
  44 
  45 /**
  46  * This class stores Font, GraphicAttribute, and Decoration intervals
  47  * on a paragraph of styled text.
  48  * <p>
  49  * Currently, this class is optimized for a small number of intervals
  50  * (preferrably 1).
  51  */
  52 final class StyledParagraph {
  53 
  54     // the length of the paragraph
  55     private int length;
  56 
  57     // If there is a single Decoration for the whole paragraph, it
  58     // is stored here.  Otherwise this field is ignored.
  59 
  60     private Decoration decoration;
  61 
  62     // If there is a single Font or GraphicAttribute for the whole
  63     // paragraph, it is stored here.  Otherwise this field is ignored.
  64     private Object font;
  65 
  66     // If there are multiple Decorations in the paragraph, they are
  67     // stored in this Vector, in order.  Otherwise this vector and
  68     // the decorationStarts array are null.
  69     private Vector<Decoration> decorations;
  70     // If there are multiple Decorations in the paragraph,
  71     // decorationStarts[i] contains the index where decoration i
  72     // starts.  For convenience, there is an extra entry at the
  73     // end of this array with the length of the paragraph.
  74     int[] decorationStarts;
  75 
  76     // If there are multiple Fonts/GraphicAttributes in the paragraph,
  77     // they are
  78     // stored in this Vector, in order.  Otherwise this vector and
  79     // the fontStarts array are null.
  80     private Vector<Object> fonts;
  81     // If there are multiple Fonts/GraphicAttributes in the paragraph,
  82     // fontStarts[i] contains the index where decoration i
  83     // starts.  For convenience, there is an extra entry at the
  84     // end of this array with the length of the paragraph.
  85     int[] fontStarts;
  86 
  87     private static int INITIAL_SIZE = 8;
  88 
  89     /**
  90      * Create a new StyledParagraph over the given styled text.
  91      * @param aci an iterator over the text
  92      * @param chars the characters extracted from aci
  93      */
  94     public StyledParagraph(AttributedCharacterIterator aci,
  95                            char[] chars) {
  96 
  97         int start = aci.getBeginIndex();
  98         int end = aci.getEndIndex();
  99         length = end - start;
 100 
 101         int index = start;
 102         aci.first();
 103 
 104         do {
 105             final int nextRunStart = aci.getRunLimit();
 106             final int localIndex = index-start;
 107 
 108             Map<? extends Attribute, ?> attributes = aci.getAttributes();
 109             attributes = addInputMethodAttrs(attributes);
 110             Decoration d = Decoration.getDecoration(attributes);
 111             addDecoration(d, localIndex);
 112 
 113             Object f = getGraphicOrFont(attributes);
 114             if (f == null) {
 115                 addFonts(chars, attributes, localIndex, nextRunStart-start);
 116             }
 117             else {
 118                 addFont(f, localIndex);
 119             }
 120 
 121             aci.setIndex(nextRunStart);
 122             index = nextRunStart;
 123 
 124         } while (index < end);
 125 
 126         // Add extra entries to starts arrays with the length
 127         // of the paragraph.  'this' is used as a dummy value
 128         // in the Vector.
 129         if (decorations != null) {
 130             decorationStarts = addToVector(this, length, decorations, decorationStarts);
 131         }
 132         if (fonts != null) {
 133             fontStarts = addToVector(this, length, fonts, fontStarts);
 134         }
 135     }
 136 
 137     /**
 138      * Adjust indices in starts to reflect an insertion after pos.
 139      * Any index in starts greater than pos will be increased by 1.
 140      */
 141     private static void insertInto(int pos, int[] starts, int numStarts) {
 142 
 143         while (starts[--numStarts] > pos) {
 144             starts[numStarts] += 1;
 145         }
 146     }
 147 
 148     /**
 149      * Return a StyledParagraph reflecting the insertion of a single character
 150      * into the text.  This method will attempt to reuse the given paragraph,
 151      * but may create a new paragraph.
 152      * @param aci an iterator over the text.  The text should be the same as the
 153      *     text used to create (or most recently update) oldParagraph, with
 154      *     the exception of inserting a single character at insertPos.
 155      * @param chars the characters in aci
 156      * @param insertPos the index of the new character in aci
 157      * @param oldParagraph a StyledParagraph for the text in aci before the
 158      *     insertion
 159      */
 160     public static StyledParagraph insertChar(AttributedCharacterIterator aci,
 161                                              char[] chars,
 162                                              int insertPos,
 163                                              StyledParagraph oldParagraph) {
 164 
 165         // If the styles at insertPos match those at insertPos-1,
 166         // oldParagraph will be reused.  Otherwise we create a new
 167         // paragraph.
 168 
 169         char ch = aci.setIndex(insertPos);
 170         int relativePos = Math.max(insertPos - aci.getBeginIndex() - 1, 0);
 171 
 172         Map<? extends Attribute, ?> attributes =
 173             addInputMethodAttrs(aci.getAttributes());
 174         Decoration d = Decoration.getDecoration(attributes);
 175         if (!oldParagraph.getDecorationAt(relativePos).equals(d)) {
 176             return new StyledParagraph(aci, chars);
 177         }
 178         Object f = getGraphicOrFont(attributes);
 179         if (f == null) {
 180             FontResolver resolver = FontResolver.getInstance();
 181             int fontIndex = resolver.getFontIndex(ch);
 182             f = resolver.getFont(fontIndex, attributes);
 183         }
 184         if (!oldParagraph.getFontOrGraphicAt(relativePos).equals(f)) {
 185             return new StyledParagraph(aci, chars);
 186         }
 187 
 188         // insert into existing paragraph
 189         oldParagraph.length += 1;
 190         if (oldParagraph.decorations != null) {
 191             insertInto(relativePos,
 192                        oldParagraph.decorationStarts,
 193                        oldParagraph.decorations.size());
 194         }
 195         if (oldParagraph.fonts != null) {
 196             insertInto(relativePos,
 197                        oldParagraph.fontStarts,
 198                        oldParagraph.fonts.size());
 199         }
 200         return oldParagraph;
 201     }
 202 
 203     /**
 204      * Adjust indices in starts to reflect a deletion after deleteAt.
 205      * Any index in starts greater than deleteAt will be increased by 1.
 206      * It is the caller's responsibility to make sure that no 0-length
 207      * runs result.
 208      */
 209     private static void deleteFrom(int deleteAt, int[] starts, int numStarts) {
 210 
 211         while (starts[--numStarts] > deleteAt) {
 212             starts[numStarts] -= 1;
 213         }
 214     }
 215 
 216     /**
 217      * Return a StyledParagraph reflecting the insertion of a single character
 218      * into the text.  This method will attempt to reuse the given paragraph,
 219      * but may create a new paragraph.
 220      * @param aci an iterator over the text.  The text should be the same as the
 221      *     text used to create (or most recently update) oldParagraph, with
 222      *     the exception of deleting a single character at deletePos.
 223      * @param chars the characters in aci
 224      * @param deletePos the index where a character was removed
 225      * @param oldParagraph a StyledParagraph for the text in aci before the
 226      *     insertion
 227      */
 228     public static StyledParagraph deleteChar(AttributedCharacterIterator aci,
 229                                              char[] chars,
 230                                              int deletePos,
 231                                              StyledParagraph oldParagraph) {
 232 
 233         // We will reuse oldParagraph unless there was a length-1 run
 234         // at deletePos.  We could do more work and check the individual
 235         // Font and Decoration runs, but we don't right now...
 236         deletePos -= aci.getBeginIndex();
 237 
 238         if (oldParagraph.decorations == null && oldParagraph.fonts == null) {
 239             oldParagraph.length -= 1;
 240             return oldParagraph;
 241         }
 242 
 243         if (oldParagraph.getRunLimit(deletePos) == deletePos+1) {
 244             if (deletePos == 0 || oldParagraph.getRunLimit(deletePos-1) == deletePos) {
 245                 return new StyledParagraph(aci, chars);
 246             }
 247         }
 248 
 249         oldParagraph.length -= 1;
 250         if (oldParagraph.decorations != null) {
 251             deleteFrom(deletePos,
 252                        oldParagraph.decorationStarts,
 253                        oldParagraph.decorations.size());
 254         }
 255         if (oldParagraph.fonts != null) {
 256             deleteFrom(deletePos,
 257                        oldParagraph.fontStarts,
 258                        oldParagraph.fonts.size());
 259         }
 260         return oldParagraph;
 261     }
 262 
 263     /**
 264      * Return the index at which there is a different Font, GraphicAttribute, or
 265      * Dcoration than at the given index.
 266      * @param index a valid index in the paragraph
 267      * @return the first index where there is a change in attributes from
 268      *      those at index
 269      */
 270     public int getRunLimit(int index) {
 271 
 272         if (index < 0 || index >= length) {
 273             throw new IllegalArgumentException("index out of range");
 274         }
 275         int limit1 = length;
 276         if (decorations != null) {
 277             int run = findRunContaining(index, decorationStarts);
 278             limit1 = decorationStarts[run+1];
 279         }
 280         int limit2 = length;
 281         if (fonts != null) {
 282             int run = findRunContaining(index, fontStarts);
 283             limit2 = fontStarts[run+1];
 284         }
 285         return Math.min(limit1, limit2);
 286     }
 287 
 288     /**
 289      * Return the Decoration in effect at the given index.
 290      * @param index a valid index in the paragraph
 291      * @return the Decoration at index.
 292      */
 293     public Decoration getDecorationAt(int index) {
 294 
 295         if (index < 0 || index >= length) {
 296             throw new IllegalArgumentException("index out of range");
 297         }
 298         if (decorations == null) {
 299             return decoration;
 300         }
 301         int run = findRunContaining(index, decorationStarts);
 302         return decorations.elementAt(run);
 303     }
 304 
 305     /**
 306      * Return the Font or GraphicAttribute in effect at the given index.
 307      * The client must test the type of the return value to determine what
 308      * it is.
 309      * @param index a valid index in the paragraph
 310      * @return the Font or GraphicAttribute at index.
 311      */
 312     public Object getFontOrGraphicAt(int index) {
 313 
 314         if (index < 0 || index >= length) {
 315             throw new IllegalArgumentException("index out of range");
 316         }
 317         if (fonts == null) {
 318             return font;
 319         }
 320         int run = findRunContaining(index, fontStarts);
 321         return fonts.elementAt(run);
 322     }
 323 
 324     /**
 325      * Return i such that starts[i] &lt;= index &lt; starts[i+1].  starts
 326      * must be in increasing order, with at least one element greater
 327      * than index.
 328      */
 329     private static int findRunContaining(int index, int[] starts) {
 330 
 331         for (int i=1; true; i++) {
 332             if (starts[i] > index) {
 333                 return i-1;
 334             }
 335         }
 336     }
 337 
 338     /**
 339      * Append the given Object to the given Vector.  Add
 340      * the given index to the given starts array.  If the
 341      * starts array does not have room for the index, a
 342      * new array is created and returned.
 343      */
 344     @SuppressWarnings({"rawtypes", "unchecked"})
 345     private static int[] addToVector(Object obj,
 346                                      int index,
 347                                      Vector v,
 348                                      int[] starts) {
 349 
 350         if (!v.lastElement().equals(obj)) {
 351             v.addElement(obj);
 352             int count = v.size();
 353             if (starts.length == count) {
 354                 int[] temp = new int[starts.length*2];
 355                 System.arraycopy(starts, 0, temp, 0, starts.length);
 356                 starts = temp;
 357             }
 358             starts[count-1] = index;
 359         }
 360         return starts;
 361     }
 362 
 363     /**
 364      * Add a new Decoration run with the given Decoration at the
 365      * given index.
 366      */
 367     private void addDecoration(Decoration d, int index) {
 368 
 369         if (decorations != null) {
 370             decorationStarts = addToVector(d,
 371                                            index,
 372                                            decorations,
 373                                            decorationStarts);
 374         }
 375         else if (decoration == null) {
 376             decoration = d;
 377         }
 378         else {
 379             if (!decoration.equals(d)) {
 380                 decorations = new Vector<Decoration>(INITIAL_SIZE);
 381                 decorations.addElement(decoration);
 382                 decorations.addElement(d);
 383                 decorationStarts = new int[INITIAL_SIZE];
 384                 decorationStarts[0] = 0;
 385                 decorationStarts[1] = index;
 386             }
 387         }
 388     }
 389 
 390     /**
 391      * Add a new Font/GraphicAttribute run with the given object at the
 392      * given index.
 393      */
 394     private void addFont(Object f, int index) {
 395 
 396         if (fonts != null) {
 397             fontStarts = addToVector(f, index, fonts, fontStarts);
 398         }
 399         else if (font == null) {
 400             font = f;
 401         }
 402         else {
 403             if (!font.equals(f)) {
 404                 fonts = new Vector<Object>(INITIAL_SIZE);
 405                 fonts.addElement(font);
 406                 fonts.addElement(f);
 407                 fontStarts = new int[INITIAL_SIZE];
 408                 fontStarts[0] = 0;
 409                 fontStarts[1] = index;
 410             }
 411         }
 412     }
 413 
 414     /**
 415      * Resolve the given chars into Fonts using FontResolver, then add
 416      * font runs for each.
 417      */
 418     private void addFonts(char[] chars, Map<? extends Attribute, ?> attributes,
 419                           int start, int limit) {
 420 
 421         FontResolver resolver = FontResolver.getInstance();
 422         CodePointIterator iter = CodePointIterator.create(chars, start, limit);
 423         for (int runStart = iter.charIndex(); runStart < limit; runStart = iter.charIndex()) {
 424             int fontIndex = resolver.nextFontRunIndex(iter);
 425             addFont(resolver.getFont(fontIndex, attributes), runStart);
 426         }
 427     }
 428 
 429     /**
 430      * Return a Map with entries from oldStyles, as well as input
 431      * method entries, if any.
 432      */
 433     static Map<? extends Attribute, ?>
 434            addInputMethodAttrs(Map<? extends Attribute, ?> oldStyles) {
 435 
 436         Object value = oldStyles.get(TextAttribute.INPUT_METHOD_HIGHLIGHT);
 437 
 438         try {
 439             if (value != null) {
 440                 if (value instanceof Annotation) {
 441                     value = ((Annotation)value).getValue();
 442                 }
 443 
 444                 InputMethodHighlight hl;
 445                 hl = (InputMethodHighlight) value;
 446 
 447                 Map<? extends Attribute, ?> imStyles = null;
 448                 try {
 449                     imStyles = hl.getStyle();
 450                 } catch (NoSuchMethodError e) {
 451                 }
 452 
 453                 if (imStyles == null) {
 454                     Toolkit tk = Toolkit.getDefaultToolkit();
 455                     imStyles = tk.mapInputMethodHighlight(hl);
 456                 }
 457 
 458                 if (imStyles != null) {
 459                     HashMap<Attribute, Object>
 460                         newStyles = new HashMap<>(5, (float)0.9);
 461                     newStyles.putAll(oldStyles);
 462 
 463                     newStyles.putAll(imStyles);
 464 
 465                     return newStyles;
 466                 }
 467             }
 468         }
 469         catch(ClassCastException e) {
 470         }
 471 
 472         return oldStyles;
 473     }
 474 
 475     /**
 476      * Extract a GraphicAttribute or Font from the given attributes.
 477      * If attributes does not contain a GraphicAttribute, Font, or
 478      * Font family entry this method returns null.
 479      */
 480     private static Object getGraphicOrFont(
 481             Map<? extends Attribute, ?> attributes) {
 482 
 483         Object value = attributes.get(TextAttribute.CHAR_REPLACEMENT);
 484         if (value != null) {
 485             return value;
 486         }
 487         value = attributes.get(TextAttribute.FONT);
 488         if (value != null) {
 489             return value;
 490         }
 491 
 492         if (attributes.get(TextAttribute.FAMILY) != null) {
 493             return Font.getFont(attributes);
 494         }
 495         else {
 496             return null;
 497         }
 498     }
 499 }