AbstractRegionPainter.java 32.8 KB
Newer Older
P
peterz 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
/*
 * Copyright 2005-2006 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package javax.swing.plaf.nimbus;

import java.awt.*;
import java.awt.image.*;
import java.lang.reflect.Method;
import javax.swing.*;
import javax.swing.plaf.UIResource;
import javax.swing.Painter;
import java.awt.print.PrinterGraphics;

/**
 * Convenient base class for defining Painter instances for rendering a
 * region or component in Nimbus.
 *
 * @author Jasper Potts
 * @author Richard Bair
 */
public abstract class AbstractRegionPainter implements Painter<JComponent> {
    /**
     * PaintContext, which holds a lot of the state needed for cache hinting and x/y value decoding
     * The data contained within the context is typically only computed once and reused over
     * multiple paint calls, whereas the other values (w, h, f, leftWidth, etc) are recomputed
     * for each call to paint.
     *
     * This field is retrieved from subclasses on each paint operation. It is up
     * to the subclass to compute and cache the PaintContext over multiple calls.
     */
    private PaintContext ctx;
    /**
     * The scaling factor. Recomputed on each call to paint.
     */
    private float f;
    /*
      Various metrics used for decoding x/y values based on the canvas size
      and stretching insets.

      On each call to paint, we first ask the subclass for the PaintContext.
      From the context we get the canvas size and stretching insets, and whether
      the algorithm should be "inverted", meaning the center section remains
      a fixed size and the other sections scale.

      We then use these values to compute a series of metrics (listed below)
      which are used to decode points in a specific axis (x or y).

      The leftWidth represents the distance from the left edge of the region
      to the first stretching inset, after accounting for any scaling factor
      (such as DPI scaling). The centerWidth is the distance between the leftWidth
      and the rightWidth. The rightWidth is the distance from the right edge,
      to the right inset (after scaling has been applied).

      The same logic goes for topHeight, centerHeight, and bottomHeight.

      The leftScale represents the proportion of the width taken by the left section.
      The same logic is applied to the other scales.

      The various widths/heights are used to decode control points. The
      various scales are used to decode bezier handles (or anchors).
    */
    /**
     * The width of the left section. Recomputed on each call to paint.
     */
    private float leftWidth;
    /**
     * The height of the top section. Recomputed on each call to paint.
     */
    private float topHeight;
    /**
     * The width of the center section. Recomputed on each call to paint.
     */
    private float centerWidth;
    /**
     * The height of the center section. Recomputed on each call to paint.
     */
    private float centerHeight;
    /**
     * The width of the right section. Recomputed on each call to paint.
     */
    private float rightWidth;
    /**
     * The height of the bottom section. Recomputed on each call to paint.
     */
    private float bottomHeight;
    /**
     * The scaling factor to use for the left section. Recomputed on each call to paint.
     */
    private float leftScale;
    /**
     * The scaling factor to use for the top section. Recomputed on each call to paint.
     */
    private float topScale;
    /**
     * The scaling factor to use for the center section, in the horizontal
     * direction. Recomputed on each call to paint.
     */
    private float centerHScale;
    /**
     * The scaling factor to use for the center section, in the vertical
     * direction. Recomputed on each call to paint.
     */
    private float centerVScale;
    /**
     * The scaling factor to use for the right section. Recomputed on each call to paint.
     */
    private float rightScale;
    /**
     * The scaling factor to use for the bottom section. Recomputed on each call to paint.
     */
    private float bottomScale;

    /**
     * Create a new AbstractRegionPainter
     */
    protected AbstractRegionPainter() { }

    /**
     * @inheritDoc
     */
    @Override
    public final void paint(Graphics2D g, JComponent c, int w, int h) {
        //don't render if the width/height are too small
        if (w <= 0 || h <=0) return;

        Object[] extendedCacheKeys = getExtendedCacheKeys(c);
        ctx = getPaintContext();
        PaintContext.CacheMode cacheMode = ctx == null ? PaintContext.CacheMode.NO_CACHING : ctx.cacheMode;
        if (cacheMode == PaintContext.CacheMode.NO_CACHING ||
                !ImageCache.getInstance().isImageCachable(w, h) ||
                g instanceof PrinterGraphics) {
            // no caching so paint directly
            paint0(g, c, w, h, extendedCacheKeys);
        } else if (cacheMode == PaintContext.CacheMode.FIXED_SIZES) {
            paintWithFixedSizeCaching(g, c, w, h, extendedCacheKeys);
        } else {
            // 9 Square caching
            paintWith9SquareCaching(g, ctx, c, w, h, extendedCacheKeys);
        }
    }

    /**
     * Get any extra attributes which the painter implementation would like
     * to include in the image cache lookups. This is checked for every call
     * of the paint(g, c, w, h) method.
     *
     * @param c The component on the current paint call
     * @return Array of extra objects to be included in the cache key
     */
    protected Object[] getExtendedCacheKeys(JComponent c) {
        return null;
    }

    /**
     * <p>Gets the PaintContext for this painting operation. This method is called on every
     * paint, and so should be fast and produce no garbage. The PaintContext contains
     * information such as cache hints. It also contains data necessary for decoding
     * points at runtime, such as the stretching insets, the canvas size at which the
     * encoded points were defined, and whether the stretching insets are inverted.</p>
     *
     * <p> This method allows for subclasses to package the painting of different states
     * with possibly different canvas sizes, etc, into one AbstractRegionPainter implementation.</p>
     *
     * @return a PaintContext associated with this paint operation.
     */
    protected abstract PaintContext getPaintContext();

    /**
     * <p>Configures the given Graphics2D. Often, rendering hints or compositiing rules are
     * applied to a Graphics2D object prior to painting, which should affect all of the
     * subsequent painting operations. This method provides a convenient hook for configuring
     * the Graphics object prior to rendering, regardless of whether the render operation is
     * performed to an intermediate buffer or directly to the display.</p>
     *
     * @param g The Graphics2D object to configure. Will not be null.
     */
    protected void configureGraphics(Graphics2D g) {
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    }

    /**
     * Actually performs the painting operation. Subclasses must implement this method.
     * The graphics object passed may represent the actual surface being rendererd to,
     * or it may be an intermediate buffer. It has also been pre-translated. Simply render
     * the component as if it were located at 0, 0 and had a width of <code>width</code>
     * and a height of <code>height</code>. For performance reasons, you may want to read
     * the clip from the Graphics2D object and only render within that space.
     *
     * @param g The Graphics2D surface to paint to
     * @param c The JComponent related to the drawing event. For example, if the
     *          region being rendered is Button, then <code>c</code> will be a
     *          JButton. If the region being drawn is ScrollBarSlider, then the
     *          component will be JScrollBar. This value may be null.
     * @param width The width of the region to paint. Note that in the case of
     *              painting the foreground, this value may differ from c.getWidth().
     * @param height The height of the region to paint. Note that in the case of
     *               painting the foreground, this value may differ from c.getHeight().
     * @param extendedCacheKeys The result of the call to getExtendedCacheKeys()
     */
    protected abstract void doPaint(Graphics2D g, JComponent c, int width,
                                    int height, Object[] extendedCacheKeys);

    /**
     * Decodes and returns a float value representing the actual pixel location for
     * the given encoded X value.
     *
     * @param x an encoded x value (0...1, or 1...2, or 2...3)
     * @return the decoded x value
     */
    protected final float decodeX(float x) {
        if (ctx.canvasSize == null) return x;

        if (x >= 0 && x <= 1) {
            return x * leftWidth;
        } else if (x > 1 && x < 2) {
            return ((x-1) * centerWidth) + leftWidth;
        } else if (x >= 2 && x <= 3) {
            return ((x-2) * rightWidth) + leftWidth + centerWidth;
        } else {
            throw new AssertionError("Invalid x");
        }
    }

    /**
     * Decodes and returns a float value representing the actual pixel location for
     * the given encoded y value.
     *
     * @param y an encoded y value (0...1, or 1...2, or 2...3)
     * @return the decoded y value
     */
    protected final float decodeY(float y) {
        if (ctx.canvasSize == null) return y;

        if (y >= 0 && y <= 1) {
            return y * topHeight;
        } else if (y > 1 && y < 2) {
            return ((y-1) * centerHeight) + topHeight;
        } else if (y >= 2 && y <= 3) {
            return ((y-2) * bottomHeight) + topHeight + centerHeight;
        } else {
            throw new AssertionError("Invalid y");
        }
    }

    /**
     * Decodes and returns a float value representing the actual pixel location for
     * the anchor point given the encoded X value of the control point, and the offset
     * distance to the anchor from that control point.
     *
     * @param x an encoded x value of the bezier control point (0...1, or 1...2, or 2...3)
     * @param dx the offset distance to the anchor from the control point x
     * @return the decoded x location of the control point
     */
    protected final float decodeAnchorX(float x, float dx) {
        if (ctx.canvasSize == null) return x + dx;

        if (x >= 0 && x <= 1) {
            return decodeX(x) + (dx * leftScale);
        } else if (x > 1 && x < 2) {
            return decodeX(x) + (dx * centerHScale);
        } else if (x >= 2 && x <= 3) {
            return decodeX(x) + (dx * rightScale);
        } else {
            throw new AssertionError("Invalid x");
        }
    }

    /**
     * Decodes and returns a float value representing the actual pixel location for
     * the anchor point given the encoded Y value of the control point, and the offset
     * distance to the anchor from that control point.
     *
     * @param y an encoded y value of the bezier control point (0...1, or 1...2, or 2...3)
     * @param dy the offset distance to the anchor from the control point y
     * @return the decoded y position of the control point
     */
    protected final float decodeAnchorY(float y, float dy) {
        if (ctx.canvasSize == null) return y + dy;

        if (y >= 0 && y <= 1) {
            return decodeY(y) + (dy * topScale);
        } else if (y > 1 && y < 2) {
            return decodeY(y) + (dy * centerVScale);
        } else if (y >= 2 && y <= 3) {
            return decodeY(y) + (dy * bottomScale);
        } else {
            throw new AssertionError("Invalid y");
        }
    }

    /**
     * Decodes and returns a color, which is derived from a base color in UI
     * defaults.
     *
     * @param key     A key corrosponding to the value in the UI Defaults table
     *                of UIManager where the base color is defined
     * @param hOffset The hue offset used for derivation.
     * @param sOffset The saturation offset used for derivation.
     * @param bOffset The brightness offset used for derivation.
     * @param aOffset The alpha offset used for derivation. Between 0...255
     * @return The derived color, whos color value will change if the parent
     *         uiDefault color changes.
     */
    protected final Color decodeColor(String key, float hOffset, float sOffset,
                                      float bOffset, int aOffset) {
        if (UIManager.getLookAndFeel() instanceof NimbusLookAndFeel){
            NimbusLookAndFeel laf = (NimbusLookAndFeel) UIManager.getLookAndFeel();
            return laf.getDerivedColor(key, hOffset, sOffset, bOffset, aOffset, true);
        } else {
            // can not give a right answer as painter sould not be used outside
            // of nimbus laf but do the best we can
            return Color.getHSBColor(hOffset,sOffset,bOffset);
        }
    }

    /**
     * Decodes and returns a color, which is derived from a offset between two
     * other colors.
     *
     * @param color1   The first color
     * @param color2   The second color
     * @param midPoint The offset between color 1 and color 2, a value of 0.0 is
     *                 color 1 and 1.0 is color 2;
     * @return The derived color
     */
    protected final Color decodeColor(Color color1, Color color2,
                                      float midPoint) {
349
        return new Color(NimbusLookAndFeel.deriveARGB(color1, color2, midPoint));
P
peterz 已提交
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
    }

    /**
     * Given parameters for creating a LinearGradientPaint, this method will
     * create and return a linear gradient paint. One primary purpose for this
     * method is to avoid creating a LinearGradientPaint where the start and
     * end points are equal. In such a case, the end y point is slightly
     * increased to avoid the overlap.
     *
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @param midpoints
     * @param colors
     * @return a valid LinearGradientPaint. This method never returns null.
     */
    protected final LinearGradientPaint decodeGradient(float x1, float y1, float x2, float y2, float[] midpoints, Color[] colors) {
        if (x1 == x2 && y1 == y2) {
            y2 += .00001f;
        }
        return new LinearGradientPaint(x1, y1, x2, y2, midpoints, colors);
    }

    /**
     * Given parameters for creating a RadialGradientPaint, this method will
     * create and return a radial gradient paint. One primary purpose for this
     * method is to avoid creating a RadialGradientPaint where the radius
     * is non-positive. In such a case, the radius is just slightly
     * increased to avoid 0.
     *
     * @param x
     * @param y
     * @param r
     * @param midpoints
     * @param colors
     * @return a valid RadialGradientPaint. This method never returns null.
     */
    protected final RadialGradientPaint decodeRadialGradient(float x, float y, float r, float[] midpoints, Color[] colors) {
        if (r == 0f) {
            r = .00001f;
        }
        return new RadialGradientPaint(x, y, r, midpoints, colors);
    }

    /**
     * Get a color property from the given JComponent. First checks for a
     * <code>getXXX()</code> method and if that fails checks for a client
     * property with key <code>property</code>. If that still fails to return
     * a Color then <code>defaultColor</code> is returned.
     *
     * @param c The component to get the color property from
     * @param property The name of a bean style property or client property
     * @param defaultColor The color to return if no color was obtained from
     *        the component.
     * @return The color that was obtained from the component or defaultColor
     */
    protected final Color getComponentColor(JComponent c, String property,
                                            Color defaultColor,
                                            float saturationOffset,
                                            float brightnessOffset,
                                            int alphaOffset) {
        Color color = null;
        if (c != null) {
            // handle some special cases for performance
            if ("background".equals(property)) {
                color = c.getBackground();
            } else if ("foreground".equals(property)) {
                color = c.getForeground();
            } else if (c instanceof JList && "selectionForeground".equals(property)) {
                color = ((JList) c).getSelectionForeground();
            } else if (c instanceof JList && "selectionBackground".equals(property)) {
                color = ((JList) c).getSelectionBackground();
            } else if (c instanceof JTable && "selectionForeground".equals(property)) {
                color = ((JTable) c).getSelectionForeground();
            } else if (c instanceof JTable && "selectionBackground".equals(property)) {
                color = ((JTable) c).getSelectionBackground();
            } else {
                String s = "get" + Character.toUpperCase(property.charAt(0)) + property.substring(1);
                try {
                    Method method = c.getClass().getMethod(s);
                    color = (Color) method.invoke(c);
                } catch (Exception e) {
                    //don't do anything, it just didn't work, that's all.
                    //This could be a normal occurance if you use a property
                    //name referring to a key in clientProperties instead of
                    //a real property
                }
                if (color == null) {
                    Object value = c.getClientProperty(property);
                    if (value instanceof Color) {
                        color = (Color) value;
                    }
                }
            }
        }
        // we return the defaultColor if the color found is null, or if
        // it is a UIResource. This is done because the color for the
        // ENABLED state is set on the component, but you don't want to use
        // that color for the over state. So we only respect the color
        // specified for the property if it was set by the user, as opposed
        // to set by us.
        if (color == null || color instanceof UIResource) {
            return defaultColor;
        } else if (saturationOffset != 0 || brightnessOffset != 0 || alphaOffset != 0) {
            float[] tmp = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
            tmp[1] = clamp(tmp[1] + saturationOffset);
            tmp[2] = clamp(tmp[2] + brightnessOffset);
            int alpha = clamp(color.getAlpha() + alphaOffset);
            return new Color((Color.HSBtoRGB(tmp[0], tmp[1], tmp[2]) & 0xFFFFFF) | (alpha <<24));
        } else {
            return color;
        }
    }

    /**
     * A class encapsulating state useful when painting. Generally, instances of this
     * class are created once, and reused for each paint request without modification.
     * This class contains values useful when hinting the cache engine, and when decoding
     * control points and bezier curve anchors.
     */
    protected static class PaintContext {
        protected static enum CacheMode {
            NO_CACHING, FIXED_SIZES, NINE_SQUARE_SCALE
        }

        private static Insets EMPTY_INSETS = new Insets(0, 0, 0, 0);

        private Insets stretchingInsets;
        private Dimension canvasSize;
        private boolean inverted;
        private CacheMode cacheMode;
        private double maxHorizontalScaleFactor;
        private double maxVerticalScaleFactor;

        private float a; // insets.left
        private float b; // canvasSize.width - insets.right
        private float c; // insets.top
        private float d; // canvasSize.height - insets.bottom;
        private float aPercent; // only used if inverted == true
        private float bPercent; // only used if inverted == true
        private float cPercent; // only used if inverted == true
        private float dPercent; // only used if inverted == true

        /**
         * Creates a new PaintContext which does not attempt to cache or scale any cached
         * images.
         *
         * @param insets The stretching insets. May be null. If null, then assumed to be 0, 0, 0, 0.
         * @param canvasSize The size of the canvas used when encoding the various x/y values. May be null.
         *                   If null, then it is assumed that there are no encoded values, and any calls
         *                   to one of the "decode" methods will return the passed in value.
         * @param inverted Whether to "invert" the meaning of the 9-square grid and stretching insets
         */
        public PaintContext(Insets insets, Dimension canvasSize, boolean inverted) {
            this(insets, canvasSize, inverted, null, 1, 1);
        }

        /**
         * Creates a new PaintContext.
         *
         * @param insets The stretching insets. May be null. If null, then assumed to be 0, 0, 0, 0.
         * @param canvasSize The size of the canvas used when encoding the various x/y values. May be null.
         *                   If null, then it is assumed that there are no encoded values, and any calls
         *                   to one of the "decode" methods will return the passed in value.
         * @param inverted Whether to "invert" the meaning of the 9-square grid and stretching insets
         * @param cacheMode A hint as to which caching mode to use. If null, then set to no caching.
         * @param maxH The maximium scale in the horizontal direction to use before punting and redrawing from scratch.
         *             For example, if maxH is 2, then we will attempt to scale any cached images up to 2x the canvas
         *             width before redrawing from scratch. Reasonable maxH values may improve painting performance.
         *             If set too high, then you may get poor looking graphics at higher zoom levels. Must be >= 1.
         * @param maxV The maximium scale in the vertical direction to use before punting and redrawing from scratch.
         *             For example, if maxV is 2, then we will attempt to scale any cached images up to 2x the canvas
         *             height before redrawing from scratch. Reasonable maxV values may improve painting performance.
         *             If set too high, then you may get poor looking graphics at higher zoom levels. Must be >= 1.
         */
        public PaintContext(Insets insets, Dimension canvasSize, boolean inverted,
                            CacheMode cacheMode, double maxH, double maxV) {
            if (maxH < 1 || maxH < 1) {
                throw new IllegalArgumentException("Both maxH and maxV must be >= 1");
            }

            this.stretchingInsets = insets == null ? EMPTY_INSETS : insets;
            this.canvasSize = canvasSize;
            this.inverted = inverted;
            this.cacheMode = cacheMode == null ? CacheMode.NO_CACHING : cacheMode;
            this.maxHorizontalScaleFactor = maxH;
            this.maxVerticalScaleFactor = maxV;

            if (canvasSize != null) {
                a = insets.left;
                b = canvasSize.width - insets.right;
                c = insets.top;
                d = canvasSize.height - insets.bottom;
                this.canvasSize = canvasSize;
                this.inverted = inverted;
                if (inverted) {
                    float available = canvasSize.width - (b - a);
                    aPercent = available > 0f ? a / available : 0f;
                    bPercent = available > 0f ? b / available : 0f;
                    available = canvasSize.height - (d - c);
                    cPercent = available > 0f ? c / available : 0f;
                    dPercent = available > 0f ? d / available : 0f;
                }
            }
        }
    }

    //---------------------- private methods

    //initializes the class to prepare it for being able to decode points
    private void prepare(float w, float h) {
        //if no PaintContext has been specified, reset the values and bail
        //also bail if the canvasSize was not set (since decoding will not work)
        if (ctx == null || ctx.canvasSize == null) {
            f = 1f;
            leftWidth = centerWidth = rightWidth = 0f;
            topHeight = centerHeight = bottomHeight = 0f;
            leftScale = centerHScale = rightScale = 0f;
            topScale = centerVScale = bottomScale = 0f;
            return;
        }

        //calculate the scaling factor, and the sizes for the various 9-square sections
        Number scale = (Number)UIManager.get("scale");
        f = scale == null ? 1f : scale.floatValue();

        if (ctx.inverted) {
            centerWidth = (ctx.b - ctx.a) * f;
            float availableSpace = w - centerWidth;
            leftWidth = availableSpace * ctx.aPercent;
            rightWidth = availableSpace * ctx.bPercent;
            centerHeight = (ctx.d - ctx.c) * f;
            availableSpace = h - centerHeight;
            topHeight = availableSpace * ctx.cPercent;
            bottomHeight = availableSpace * ctx.dPercent;
        } else {
            leftWidth = ctx.a * f;
            rightWidth = (float)(ctx.canvasSize.getWidth() - ctx.b) * f;
            centerWidth = w - leftWidth - rightWidth;
            topHeight = ctx.c * f;
            bottomHeight = (float)(ctx.canvasSize.getHeight() - ctx.d) * f;
            centerHeight = h - topHeight - bottomHeight;
        }

        leftScale = ctx.a == 0f ? 0f : leftWidth / ctx.a;
        centerHScale = (ctx.b - ctx.a) == 0f ? 0f : centerWidth / (ctx.b - ctx.a);
        rightScale = (ctx.canvasSize.width - ctx.b) == 0f ? 0f : rightWidth / (ctx.canvasSize.width - ctx.b);
        topScale = ctx.c == 0f ? 0f : topHeight / ctx.c;
        centerVScale = (ctx.d - ctx.c) == 0f ? 0f : centerHeight / (ctx.d - ctx.c);
        bottomScale = (ctx.canvasSize.height - ctx.d) == 0f ? 0f : bottomHeight / (ctx.canvasSize.height - ctx.d);
    }

    private void paintWith9SquareCaching(Graphics2D g, PaintContext ctx,
                                         JComponent c, int w, int h,
                                         Object[] extendedCacheKeys) {
        // check if we can scale to the requested size
        Dimension canvas = ctx.canvasSize;
        Insets insets = ctx.stretchingInsets;

        if (w <= (canvas.width * ctx.maxHorizontalScaleFactor) && h <= (canvas.height * ctx.maxVerticalScaleFactor)) {
            // get image at canvas size
            VolatileImage img = getImage(g.getDeviceConfiguration(), c, canvas.width, canvas.height, extendedCacheKeys);
            if (img != null) {
                // calculate dst inserts
                // todo: destination inserts need to take into acount scale factor for high dpi. Note: You can use f for this, I think
                Insets dstInsets;
                if (ctx.inverted){
                    int leftRight = (w-(canvas.width-(insets.left+insets.right)))/2;
                    int topBottom = (h-(canvas.height-(insets.top+insets.bottom)))/2;
                    dstInsets = new Insets(topBottom,leftRight,topBottom,leftRight);
                } else {
                    dstInsets = insets;
                }
                // paint 9 square scaled
                Object oldScaleingHints = g.getRenderingHint(RenderingHints.KEY_INTERPOLATION);
                g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                ImageScalingHelper.paint(g, 0, 0, w, h, img, insets, dstInsets,
                        ImageScalingHelper.PaintType.PAINT9_STRETCH, ImageScalingHelper.PAINT_ALL);
                g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                    oldScaleingHints!=null?oldScaleingHints:RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
            } else {
                // render directly
                paint0(g, c, w, h, extendedCacheKeys);
            }
        } else {
            // paint directly
            paint0(g, c, w, h, extendedCacheKeys);
        }
    }

    private void paintWithFixedSizeCaching(Graphics2D g, JComponent c, int w,
                                           int h, Object[] extendedCacheKeys) {
        VolatileImage img = getImage(g.getDeviceConfiguration(), c, w, h, extendedCacheKeys);
        if (img != null) {
            //render cached image
            g.drawImage(img, 0, 0, null);
        } else {
            // render directly
            paint0(g, c, w, h, extendedCacheKeys);
        }
    }

    /** Gets the rendered image for this painter at the requested size, either from cache or create a new one */
    private VolatileImage getImage(GraphicsConfiguration config, JComponent c,
                                   int w, int h, Object[] extendedCacheKeys) {
        ImageCache imageCache = ImageCache.getInstance();
        //get the buffer for this component
        VolatileImage buffer = (VolatileImage) imageCache.getImage(config, w, h, this, extendedCacheKeys);

        int renderCounter = 0; //to avoid any potential, though unlikely, infinite loop
        do {
            //validate the buffer so we can check for surface loss
            int bufferStatus = VolatileImage.IMAGE_INCOMPATIBLE;
            if (buffer != null) {
                bufferStatus = buffer.validate(config);
            }

            //If the buffer status is incompatible or restored, then we need to re-render to the volatile image
            if (bufferStatus == VolatileImage.IMAGE_INCOMPATIBLE || bufferStatus == VolatileImage.IMAGE_RESTORED) {
                //if the buffer is null (hasn't been created), or isn't the right size, or has lost its contents,
                //then recreate the buffer
                if (buffer == null || buffer.getWidth() != w || buffer.getHeight() != h ||
                        bufferStatus == VolatileImage.IMAGE_INCOMPATIBLE) {
                    //clear any resources related to the old back buffer
                    if (buffer != null) {
                        buffer.flush();
                        buffer = null;
                    }
                    //recreate the buffer
                    buffer = config.createCompatibleVolatileImage(w, h,
                            Transparency.TRANSLUCENT);
                    // put in cache for future
                    imageCache.setImage(buffer, config, w, h, this, extendedCacheKeys);
                }
                //create the graphics context with which to paint to the buffer
                Graphics2D bg = buffer.createGraphics();
                //clear the background before configuring the graphics
                bg.setComposite(AlphaComposite.Clear);
                bg.fillRect(0, 0, w, h);
                bg.setComposite(AlphaComposite.SrcOver);
                configureGraphics(bg);
                // paint the painter into buffer
                paint0(bg, c, w, h, extendedCacheKeys);
                //close buffer graphics
                bg.dispose();
            }
        } while (buffer.contentsLost() && renderCounter++ < 3);
        // check if we failed
        if (renderCounter == 3) return null;
        // return image
        return buffer;
    }

    //convenience method which creates a temporary graphics object by creating a
    //clone of the passed in one, configuring it, drawing with it, disposing it.
    //These steps have to be taken to ensure that any hints set on the graphics
    //are removed subsequent to painting.
    private void paint0(Graphics2D g, JComponent c, int width, int height,
                        Object[] extendedCacheKeys) {
        prepare(width, height);
        g = (Graphics2D)g.create();
        configureGraphics(g);
        doPaint(g, c, width, height, extendedCacheKeys);
        g.dispose();
    }

    private float clamp(float value) {
        if (value < 0) {
            value = 0;
        } else if (value > 1) {
            value = 1;
        }
        return value;
    }

    private int clamp(int value) {
        if (value < 0) {
            value = 0;
        } else if (value > 255) {
            value = 255;
        }
        return value;
    }
}