1 /*
   2  * Copyright (c) 2002, 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 build.tools.generatenimbus;
  26 
  27 import java.awt.geom.Point2D;
  28 import java.util.ArrayList;
  29 import java.util.HashMap;
  30 import java.util.LinkedHashMap;
  31 import java.util.List;
  32 import java.util.Map;
  33 
  34 
  35 /**
  36  * PainterGenerator - Class for generating Painter class java source from a Canvas
  37  *
  38  * Following in the general theory that is used to generate a Painter file.
  39  *
  40  * Each Painter file represents a Region. So there is one painter file per region. In
  41  * skin.laf we support Icon subregions, which are really just hacked versions of the
  42  * parent region.
  43  *
  44  * In order to generate the most compact and efficient bytecode possible for the
  45  * Painters, we actually perform the generation sequence in two steps. The first
  46  * step is the analysis phase, where we walk through the SynthModel for the region
  47  * and discover commonality among the different states in the region. For example,
  48  * do they have common paths? Do they have common colors? Gradients? Is the painting
  49  * code for the different states identical other than for colors?
  50  *
  51  * We gather this information up. On the second pass, we use this data to determine the
  52  * methods that need to be generated, and the class variables that need to be generated.
  53  * We try to keep the actual bytecode count as small as possible so that we may reduce
  54  * the overall size of the look and feel significantly.
  55  *
  56  * @author  Richard Bair
  57  * @author  Jasper Potts
  58  */
  59 public class PainterGenerator {
  60 
  61     private static final boolean debug = false;
  62 
  63     //a handful of counters, incremented whenever the associated object type is encounted.
  64     //These counters form the basis of the field and method suffixes.
  65     //These are all 1 based, because I felt like it :-)
  66     private int colorCounter = 1;
  67     private int gradientCounter = 1;
  68     private int radialCounter = 1;
  69     private int pathCounter = 1;
  70     private int rectCounter = 1;
  71     private int roundRectCounter = 1;
  72     private int ellipseCounter = 1;
  73 
  74     private int stateTypeCounter = 1;
  75 
  76     //during the first pass, we will construct these maps
  77     private Map<String, String> colors = new HashMap<String, String>();
  78     /**
  79      * Code=>method name.
  80      */
  81     private Map<String, String> methods = new HashMap<String, String>();
  82 
  83     //these variables hold the generated code
  84     /**
  85      * The source code in this variable will be used to define the various state types
  86      */
  87     private StringBuilder stateTypeCode = new StringBuilder();
  88     /**
  89      * The source code in this variable will be used to define the switch statement for painting
  90      */
  91     private StringBuilder switchCode = new StringBuilder();
  92     /**
  93      * The source code in this variable will be used to define the methods for painting each state
  94      */
  95     private StringBuilder paintingCode = new StringBuilder();
  96     /**
  97      * The source code in this variable will be used to add getExtendedCacheKeys
  98      * implementation if needed.
  99      */
 100     private StringBuilder getExtendedCacheKeysCode = new StringBuilder();
 101     /**
 102      * The source code in this variable will be used to define the methods for decoding gradients
 103      * and shapes.
 104      */
 105     private StringBuilder gradientsCode = new StringBuilder();
 106     private StringBuilder colorCode = new StringBuilder();
 107     private StringBuilder shapesCode = new StringBuilder();
 108     /**
 109      * Map of component colors keyed by state constant name
 110      */
 111     private Map<String, List<ComponentColor>> componentColorsMap =
 112             new LinkedHashMap<String, List<ComponentColor>>();
 113     /**
 114      * For the current state the list of all component colors used by this
 115      * painter, the index in this list is also the index in the runtime array
 116      * of defaults and keys.
 117      */
 118     private List<ComponentColor> componentColors = null;
 119 
 120     PainterGenerator(UIRegion r) {
 121         generate(r);
 122     }
 123 
 124     private void generate(UIRegion r) {
 125         for (UIState state : r.getBackgroundStates()) {
 126             Canvas canvas = state.getCanvas();
 127             String type = (r instanceof UIIconRegion ? r.getKey() : "Background");
 128             generate(state, canvas, type);
 129         }
 130         for (UIState state : r.getForegroundStates()) {
 131             Canvas canvas = state.getCanvas();
 132             generate(state, canvas, "Foreground");
 133         }
 134         for (UIState state : r.getBorderStates()) {
 135             Canvas canvas = state.getCanvas();
 136             generate(state, canvas, "Border");
 137         }
 138         //now check for any uiIconRegions, since these are collapsed together.
 139         for (UIRegion sub : r.getSubRegions()) {
 140             if (sub instanceof UIIconRegion) {
 141                 generate(sub);
 142             }
 143         }
 144         //generate all the code for component colors
 145         if (!componentColorsMap.isEmpty()) {
 146             getExtendedCacheKeysCode
 147                     .append("    protected Object[] getExtendedCacheKeys(JComponent c) {\n")
 148                     .append("        Object[] extendedCacheKeys = null;\n")
 149                     .append("        switch(state) {\n");
 150             for (Map.Entry<String, List<ComponentColor>> entry : componentColorsMap.entrySet()) {
 151                 getExtendedCacheKeysCode
 152                     .append("            case ")
 153                     .append(entry.getKey()).append(":\n")
 154                     .append("                extendedCacheKeys = new Object[] {\n");
 155                 for (int i=0; i<entry.getValue().size(); i++) {
 156                     ComponentColor cc = entry.getValue().get(i);
 157                     cc.write(getExtendedCacheKeysCode);
 158                     if (i + 1 < entry.getValue().size()) {
 159                         getExtendedCacheKeysCode.append("),\n");
 160                     } else {
 161                         getExtendedCacheKeysCode.append(")");
 162                     }
 163                 }
 164                 getExtendedCacheKeysCode.append("};\n")
 165                     .append("                break;\n");
 166             }
 167             getExtendedCacheKeysCode
 168                     .append("        }\n")
 169                     .append("        return extendedCacheKeys;\n")
 170                     .append("    }");
 171         }
 172     }
 173 
 174     //type is background, foreground, border, upArrowIcon, etc.
 175     private void generate(UIState state, Canvas canvas, String type) {
 176         String states = state.getStateKeys();
 177         String stateType = Utils.statesToConstantName(type + "_" + states);
 178         String paintMethodName = "paint" + type + Utils.statesToClassName(states);
 179         //create new array for component colors for this state
 180         componentColors = new ArrayList<ComponentColor>();
 181 
 182         stateTypeCode.append("    static final int ").append(stateType).append(" = ").append(stateTypeCounter++).append(";\n");
 183 
 184         if (canvas.isBlank()) {
 185             return;
 186         }
 187 
 188         switchCode.append("            case ").append(stateType).append(": ").append(paintMethodName).append("(g); break;\n");
 189         paintingCode.append("    private void ").append(paintMethodName).append("(Graphics2D g) {\n");
 190 
 191         //start by setting up common info needed to encode the control points
 192         Insets in = canvas.getStretchingInsets();
 193         float a = in.left;
 194         float b = canvas.getSize().width - in.right;
 195         float c = in.top;
 196         float d = canvas.getSize().height - in.bottom;
 197         float width = canvas.getSize().width;
 198         float height = canvas.getSize().height;
 199         float cw = b - a;
 200         float ch = d - c;
 201 
 202         Layer[] layers = canvas.getLayers().toArray(new Layer[0]);
 203         for (int index=layers.length-1; index >= 0; index--) {
 204             Layer layer = layers[index];
 205 
 206             //shapes must be painted in reverse order
 207             List<Shape> shapes = layer.getShapes();
 208             for (int i=shapes.size()-1; i>=0; i--) {
 209                 Shape shape = shapes.get(i);
 210                 Paint paint = shape.getPaint();
 211 
 212                 /*
 213                     We attempt to write the minimal number of bytecodes as possible when
 214                     generating code. Due to the inherit complexities in determining what
 215                     is extraneous, we use the following system:
 216 
 217                     We first generate the code for the shape. Then, we check to see if
 218                     this shape has already been generated. If so, then we defer to an
 219                     existing method. If not, then we will create a new methods, stick
 220                     the code in it, and refer to that method.
 221                 */
 222 
 223                 String shapeMethodName = null; // will contain the name of the method which creates the shape
 224                 String shapeVariable = null; // will be one of rect, roundRect, ellipse, or path.
 225                 String shapeMethodBody = null;
 226 
 227                 if (shape instanceof Rectangle) {
 228                     Rectangle rshape = (Rectangle) shape;
 229                     float x1 = encode((float)rshape.getX1(), a, b, width);
 230                     float y1 = encode((float)rshape.getY1(), c, d, height);
 231                     float x2 = encode((float)rshape.getX2(), a, b, width);
 232                     float y2 = encode((float)rshape.getY2(), c, d, height);
 233                     if (rshape.isRounded()) {
 234                         //it is a rounded rectangle
 235                         float rounding = (float)rshape.getRounding();
 236 
 237                         shapeMethodBody =
 238                                 "        roundRect.setRoundRect(" +
 239                                 writeDecodeX(x1) + ", //x\n" +
 240                                 "                               " + writeDecodeY(y1) + ", //y\n" +
 241                                 "                               " + writeDecodeX(x2) + " - " + writeDecodeX(x1) + ", //width\n" +
 242                                 "                               " + writeDecodeY(y2) + " - " + writeDecodeY(y1) + ", //height\n" +
 243                                 "                               " + rounding + "f, " + rounding + "f); //rounding";
 244                         shapeVariable = "roundRect";
 245                     } else {
 246                         shapeMethodBody =
 247                                 "            rect.setRect(" +
 248                                 writeDecodeX(x1) + ", //x\n" +
 249                                 "                         " + writeDecodeY(y1) + ", //y\n" +
 250                                 "                         " + writeDecodeX(x2) + " - " + writeDecodeX(x1) + ", //width\n" +
 251                                 "                         " + writeDecodeY(y2) + " - " + writeDecodeY(y1) + "); //height";
 252                         shapeVariable = "rect";
 253                     }
 254                 } else if (shape instanceof Ellipse) {
 255                     Ellipse eshape = (Ellipse) shape;
 256                     float x1 = encode((float)eshape.getX1(), a, b, width);
 257                     float y1 = encode((float)eshape.getY1(), c, d, height);
 258                     float x2 = encode((float)eshape.getX2(), a, b, width);
 259                     float y2 = encode((float)eshape.getY2(), c, d, height);
 260                     shapeMethodBody =
 261                             "        ellipse.setFrame(" +
 262                             writeDecodeX(x1) + ", //x\n" +
 263                             "                         " + writeDecodeY(y1) + ", //y\n" +
 264                             "                         " + writeDecodeX(x2) + " - " + writeDecodeX(x1) + ", //width\n" +
 265                             "                         " + writeDecodeY(y2) + " - " + writeDecodeY(y1) + "); //height";
 266                     shapeVariable = "ellipse";
 267                 } else if (shape instanceof Path) {
 268                     Path pshape = (Path) shape;
 269                     List<Point> controlPoints = pshape.getControlPoints();
 270                     Point first, last;
 271                     first = last = controlPoints.get(0);
 272                     StringBuilder buffer = new StringBuilder();
 273                     buffer.append("        path.reset();\n");
 274                     buffer.append("        path.moveTo(" + writeDecodeX(encode((float)first.getX(), a, b, width)) + ", " + writeDecodeY(encode((float)first.getY(), c, d, height)) + ");\n");
 275                     for (int j=1; j<controlPoints.size(); j++) {
 276                         Point cp = controlPoints.get(j);
 277                         if (last.isP2Sharp() && cp.isP1Sharp()) {
 278                             float x = encode((float)cp.getX(), a, b, width);
 279                             float y = encode((float)cp.getY(), c, d, height);
 280                             buffer.append("        path.lineTo(" + writeDecodeX(x) + ", " + writeDecodeY(y) + ");\n");
 281                         } else {
 282                             float x1 = encode((float)last.getX(), a, b, width);
 283                             float y1 = encode((float)last.getY(), c, d, height);
 284                             float x2 = encode((float)cp.getX(), a, b, width);
 285                             float y2 = encode((float)cp.getY(), c, d, height);
 286                             buffer.append(
 287                                     "        path.curveTo(" + writeDecodeBezierX(x1, last.getX(), last.getCp2X()) + ", "
 288                                                             + writeDecodeBezierY(y1, last.getY(), last.getCp2Y()) + ", "
 289                                                             + writeDecodeBezierX(x2, cp.getX(), cp.getCp1X()) + ", "
 290                                                             + writeDecodeBezierY(y2, cp.getY(), cp.getCp1Y()) + ", "
 291                                                             + writeDecodeX(x2) + ", " + writeDecodeY(y2) + ");\n");
 292                         }
 293                         last = cp;
 294                     }
 295                     if (last.isP2Sharp() && first.isP1Sharp()) {
 296                         float x = encode((float)first.getX(), a, b, width);
 297                         float y = encode((float)first.getY(), c, d, height);
 298                         buffer.append("        path.lineTo(" + writeDecodeX(x) + ", " + writeDecodeY(y) + ");\n");
 299                     } else {
 300                         float x1 = encode((float)last.getX(), a, b, width);
 301                         float y1 = encode((float)last.getY(), c, d, height);
 302                         float x2 = encode((float)first.getX(), a, b, width);
 303                         float y2 = encode((float)first.getY(), c, d, height);
 304                         buffer.append(
 305                                 "        path.curveTo(" + writeDecodeBezierX(x1, last.getX(), last.getCp2X()) + ", "
 306                                                         + writeDecodeBezierY(y1, last.getY(), last.getCp2Y()) + ", "
 307                                                         + writeDecodeBezierX(x2, first.getX(), first.getCp1X()) + ", "
 308                                                         + writeDecodeBezierY(y2, first.getY(), first.getCp1Y()) + ", "
 309                                                         + writeDecodeX(x2) + ", " + writeDecodeY(y2) + ");\n");
 310                     }
 311                     buffer.append("        path.closePath();");
 312                     shapeMethodBody = buffer.toString();
 313                     shapeVariable = "path";
 314                 } else {
 315                     throw new RuntimeException("Cannot happen unless a new Shape has been defined");
 316                 }
 317 
 318                 //now that we have the shape defined in shapeMethodBody, and a shapeVariable name,
 319                 //look to see if such a body has been previously defined.
 320                 shapeMethodName = methods.get(shapeMethodBody);
 321                 String returnType = null;
 322                 if (shapeMethodName == null) {
 323                     if ("rect".equals(shapeVariable)) {
 324                         shapeMethodName = "decodeRect" + rectCounter++;
 325                         returnType = "Rectangle2D";
 326                     } else if ("roundRect".equals(shapeVariable)) {
 327                         shapeMethodName = "decodeRoundRect" + roundRectCounter++;
 328                         returnType = "RoundRectangle2D";
 329                     } else if ("ellipse".equals(shapeVariable)) {
 330                         shapeMethodName = "decodeEllipse" + ellipseCounter++;
 331                         returnType = "Ellipse2D";
 332                     } else {
 333                         shapeMethodName = "decodePath" + pathCounter++;
 334                         returnType = "Path2D";
 335                     }
 336                     methods.put(shapeMethodBody, shapeMethodName);
 337 
 338                     //since the method wasn't previously defined, time to define it
 339                     shapesCode.append("    private ").append(returnType).append(" ").append(shapeMethodName).append("() {\n");
 340                     shapesCode.append(shapeMethodBody);
 341                     shapesCode.append("\n");
 342                     shapesCode.append("        return " + shapeVariable + ";\n");
 343                     shapesCode.append("    }\n\n");
 344                 }
 345 
 346                 //now that the method has been defined, I can go on and decode the
 347                 //paint. After the paint is decoded, I can write the g.fill() method call,
 348                 //using the result of the shapeMethodName. Yay!
 349 
 350 //            if (shapeVariable != null) {
 351             //first, calculate the bounds of the shape being painted and store in variables
 352                 paintingCode.append("        ").append(shapeVariable).append(" = ").append(shapeMethodName).append("();\n");
 353 
 354                 if (paint instanceof Matte) {
 355                     String colorVariable = encodeMatte((Matte)paint);
 356                     paintingCode.append("        g.setPaint(").append(colorVariable).append(");\n");
 357                 } else if (paint instanceof Gradient) {
 358                     String gradientMethodName = encodeGradient(shape, (Gradient)paint);
 359                     paintingCode.append("        g.setPaint(").append(gradientMethodName).append("(").append(shapeVariable).append("));\n");
 360                 } else if (paint instanceof RadialGradient) {
 361                     String radialMethodName = encodeRadial(shape, (RadialGradient)paint);
 362                     paintingCode.append("        g.setPaint(").append(radialMethodName).append("(").append(shapeVariable).append("));\n");
 363                 }
 364                 paintingCode.append("        g.fill(").append(shapeVariable).append(");\n");
 365             }
 366         }
 367 
 368         paintingCode.append("\n    }\n\n");
 369 
 370         //collect component colors
 371         if (!componentColors.isEmpty()) {
 372             componentColorsMap.put(stateType, componentColors);
 373             componentColors = null;
 374         }
 375     }
 376 
 377     private float encode(float x, float a, float b, float w) {
 378         float r = 0;
 379         if (x < a) {
 380             r = (x / a);
 381         } else if (x > b) {
 382             r = 2 + ((x - b) / (w - b));
 383         } else if (x == a && x == b) {
 384             return 1.5f;
 385         } else {
 386             r = 1 + ((x - a) / (b - a));
 387         }
 388 
 389         if (Float.isNaN(r)) {
 390             if (debug) {
 391                 System.err.println("[Error] Encountered NaN: encode(" + x + ", " + a + ", " + b + ", " + w + ")");
 392             }
 393             return 0;
 394         } else if (Float.isInfinite(r)) {
 395             if (debug) {
 396                 System.err.println("[Error] Encountered Infinity: encode(" + x + ", " + a + ", " + b + ", " + w + ")");
 397             }
 398             return 0;
 399         } else if (r < 0) {
 400             if (debug) {
 401                 System.err.println("[Error] encoded value was less than 0: encode(" + x + ", " + a + ", " + b + ", " + w + ")");
 402             }
 403             return 0;
 404         } else if (r > 3) {
 405             if (debug) {
 406                 System.err.println("[Error] encoded value was greater than 3: encode(" + x + ", " + a + ", " + b + ", " + w + ")");
 407             }
 408             return 3;
 409         } else {
 410             return r;
 411         }
 412     }
 413 
 414     private String writeDecodeX(float encodedX) {
 415         return "decodeX(" + encodedX + "f)";
 416     }
 417 
 418     private String writeDecodeY(float encodedY) {
 419         return "decodeY(" + encodedY + "f)";
 420     }
 421 
 422     /**
 423      *
 424      * @param ex encoded x value
 425      * @param x unencoded x value
 426      * @param cpx unencoded cpx value
 427      * @return
 428      */
 429     private static String writeDecodeBezierX(double ex, double x, double cpx) {
 430         return "decodeAnchorX(" + ex + "f, " + (cpx - x) + "f)";
 431     }
 432 
 433     /**
 434      *
 435      * @param ey encoded y value
 436      * @param y unencoded y value
 437      * @param cpy unencoded cpy value
 438      * @return
 439      */
 440     private static String writeDecodeBezierY(double ey, double y, double cpy) {
 441         return "decodeAnchorY(" + ey + "f, " + (cpy - y) + "f)";
 442     }
 443 
 444     private String encodeMatte(Matte m) {
 445         String declaration = m.getDeclaration();
 446         String variableName = colors.get(declaration);
 447         if (variableName == null) {
 448             variableName = "color" + colorCounter++;
 449             colors.put(declaration, variableName);
 450             colorCode.append(String.format("    private Color %s = %s;\n",
 451                                            variableName, declaration));
 452         }
 453         // handle component colors
 454         if (m.getComponentPropertyName() != null) {
 455             ComponentColor cc = m.createComponentColor(variableName);
 456             int index = componentColors.indexOf(cc);
 457             if (index == -1) {
 458                 index = componentColors.size();
 459                 componentColors.add(cc);
 460             }
 461             return "(Color)componentColors[" + index + "]";
 462         } else {
 463             return variableName;
 464         }
 465     }
 466 
 467     private String encodeGradient(Shape ps, Gradient g) {
 468         StringBuilder b = new StringBuilder();
 469         float x1 = (float)ps.getPaintX1();
 470         float y1 = (float)ps.getPaintY1();
 471         float x2 = (float)ps.getPaintX2();
 472         float y2 = (float)ps.getPaintY2();
 473         b.append("        return decodeGradient((");
 474         b.append(x1);
 475         b.append("f * w) + x, (");
 476         b.append(y1);
 477         b.append("f * h) + y, (");
 478         b.append(x2);
 479         b.append("f * w) + x, (");
 480         b.append(y2);
 481         b.append("f * h) + y,\n");
 482         encodeGradientColorsAndFractions(g,b);
 483         b.append(");");
 484 
 485         String methodBody = b.toString();
 486         String methodName = methods.get(methodBody);
 487         if (methodName == null) {
 488             methodName = "decodeGradient" + gradientCounter++;
 489             gradientsCode.append("    private Paint ").append(methodName).append("(Shape s) {\n");
 490             gradientsCode.append("        Rectangle2D bounds = s.getBounds2D();\n");
 491             gradientsCode.append("        float x = (float)bounds.getX();\n");
 492             gradientsCode.append("        float y = (float)bounds.getY();\n");
 493             gradientsCode.append("        float w = (float)bounds.getWidth();\n");
 494             gradientsCode.append("        float h = (float)bounds.getHeight();\n");
 495             gradientsCode.append(methodBody);
 496             gradientsCode.append("\n    }\n\n");
 497             methods.put(methodBody, methodName);
 498         }
 499         return methodName;
 500     }
 501 
 502     /**
 503      * Takes a abstract gradient and creates the code for the fractions float
 504      * array and the colors array that can be used in the constructors of linear
 505      * and radial gradients.
 506      *
 507      * @param g The abstract gradient to get stops from
 508      * @param b Append code string of the form "new float[]{...},
 509      *          new Color[]{...}" to this StringBuilder
 510      */
 511     private void encodeGradientColorsAndFractions(AbstractGradient g,
 512                                                     StringBuilder b) {
 513         List<GradientStop> stops = g.getStops();
 514         // there are stops.size() number of main stops. Between each is a
 515         // fractional stop. Thus, there are: stops.size() + stops.size() - 1
 516         // number of fractions and colors.
 517         float[] fractions = new float[stops.size() + stops.size() - 1];
 518         String[] colors = new String[fractions.length];
 519         //for each stop, create the stop and it's associated fraction
 520         int index = 0; // the index into fractions and colors
 521         for (int i = 0; i < stops.size(); i++) {
 522             GradientStop s = stops.get(i);
 523             //copy over the stop's data
 524             colors[index] = encodeMatte(s.getColor());
 525             fractions[index] = s.getPosition();
 526 
 527             //If this isn't the last stop, then add in the fraction
 528             if (index < fractions.length - 1) {
 529                 float f1 = s.getPosition();
 530                 float f2 = stops.get(i + 1).getPosition();
 531                 index++;
 532                 fractions[index] = f1 + (f2 - f1) * s.getMidpoint();
 533                 colors[index] = "decodeColor("+
 534                         colors[index - 1]+","+
 535                         encodeMatte(stops.get(i + 1).getColor())+",0.5f)";
 536             }
 537             index++;
 538         }
 539         // Check boundry conditions
 540         for (int i = 1; i < fractions.length; i++) {
 541             //to avoid an error with LinearGradientPaint where two fractions
 542             //are identical, bump up the fraction value by a miniscule amount
 543             //if it is identical to the previous one
 544             //NOTE: The <= is critical because the previous value may already
 545             //have been bumped up
 546             if (fractions[i] <= fractions[i - 1]) {
 547                 fractions[i] = fractions[i - 1] + .000001f;
 548             }
 549         }
 550         //another boundary condition where multiple stops are all at the end. The
 551         //previous loop bumped all but one of these past 1.0, which is bad.
 552         //so remove any fractions (and their colors!) that are beyond 1.0
 553         int outOfBoundsIndex = -1;
 554         for (int i = 0; i < fractions.length; i++) {
 555             if (fractions[i] > 1) {
 556                 outOfBoundsIndex = i;
 557                 break;
 558             }
 559         }
 560         if (outOfBoundsIndex >= 0) {
 561             float[] f = fractions;
 562             String[] c = colors;
 563             fractions = new float[outOfBoundsIndex];
 564             colors = new String[outOfBoundsIndex];
 565             System.arraycopy(f, 0, fractions, 0, outOfBoundsIndex);
 566             System.arraycopy(c, 0, colors, 0, outOfBoundsIndex);
 567         }
 568         // build string
 569         b.append("                new float[] { ");
 570         for (int i = 0; i < fractions.length; i++) {
 571             if (i>0)b.append(',');
 572             b.append(fractions[i]);
 573             b.append('f');
 574         }
 575         b.append(" },\n                new Color[] { ");
 576         for (int i = 0; i < colors.length; i++) {
 577             if (i>0) b.append(",\n                            ");
 578             b.append(colors[i]);
 579         }
 580         b.append("}");
 581     }
 582 
 583     private String encodeRadial(Shape ps, RadialGradient g) {
 584         float centerX1 = (float)ps.getPaintX1();
 585         float centerY1 = (float)ps.getPaintY1();
 586         float x2 = (float)ps.getPaintX2();
 587         float y2 = (float)ps.getPaintY2();
 588         float radius = (float)Point2D.distance(centerX1, centerY1, x2, y2);
 589         StringBuilder b = new StringBuilder();
 590 
 591         b.append("        return decodeRadialGradient((");
 592         b.append(centerX1);
 593         b.append("f * w) + x, (");
 594         b.append(centerY1);
 595         b.append("f * h) + y, ");
 596         b.append(radius);
 597         b.append("f,\n");
 598         encodeGradientColorsAndFractions(g,b);
 599         b.append(");");
 600 
 601         String methodBody = b.toString();
 602         String methodName = methods.get(methodBody);
 603         if (methodName == null) {
 604             methodName = "decodeRadial" + radialCounter++;
 605             gradientsCode.append("    private Paint ").append(methodName).append("(Shape s) {\n");
 606             gradientsCode.append("        Rectangle2D bounds = s.getBounds2D();\n");
 607             gradientsCode.append("        float x = (float)bounds.getX();\n");
 608             gradientsCode.append("        float y = (float)bounds.getY();\n");
 609             gradientsCode.append("        float w = (float)bounds.getWidth();\n");
 610             gradientsCode.append("        float h = (float)bounds.getHeight();\n");
 611             gradientsCode.append(methodBody);
 612             gradientsCode.append("\n    }\n\n");
 613             methods.put(methodBody, methodName);
 614         }
 615         return methodName;
 616     }
 617 
 618     //note that this method is not thread-safe. In fact, none of this class is.
 619     public static void writePainter(UIRegion r, String painterName) {
 620         //Need only write out the stuff for this region, don't need to worry about subregions
 621         //since this method will be called for each of those (and they go in their own file, anyway).
 622         //The only subregion that we compound into this is the one for icons.
 623         PainterGenerator gen = new PainterGenerator(r);
 624         System.out.println("Generating source file: " + painterName + ".java");
 625 
 626         Map<String, String> variables = Generator.getVariables();
 627         variables.put("PAINTER_NAME", painterName);
 628         variables.put("STATIC_DECL", gen.stateTypeCode.toString());
 629         variables.put("COLORS_DECL", gen.colorCode.toString());
 630         variables.put("DO_PAINT_SWITCH_BODY", gen.switchCode.toString());
 631         variables.put("PAINTING_DECL", gen.paintingCode.toString());
 632         variables.put("GET_EXTENDED_CACHE_KEYS", gen.getExtendedCacheKeysCode.toString());
 633         variables.put("SHAPES_DECL", gen.shapesCode.toString());
 634         variables.put("GRADIENTS_DECL", gen.gradientsCode.toString());
 635 
 636         Generator.writeSrcFile("PainterImpl", variables, painterName);
 637     }
 638 }