aboutsummaryrefslogtreecommitdiff
path: root/src/share/classes/javax/swing/plaf/nimbus/Defaults.template
blob: 492b53583ed4a5f18f911301996eefedc691a40b (plain)
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
349
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
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
/*
 * 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 ${PACKAGE};

import javax.swing.Painter;
import java.awt.Graphics;
import sun.font.FontManager;
import sun.swing.plaf.synth.DefaultSynthStyle;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JInternalFrame;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.plaf.BorderUIResource;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.DimensionUIResource;
import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.InsetsUIResource;
import javax.swing.plaf.synth.Region;
import javax.swing.plaf.synth.SynthStyle;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.image.BufferedImage;
import static java.awt.image.BufferedImage.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import javax.swing.border.Border;
import javax.swing.plaf.UIResource;

/**
 * This class contains all the implementation details related to
 * ${LAF_NAME}. It contains all the code for initializing the UIDefaults table,
 * as well as for selecting
 * a SynthStyle based on a JComponent/Region pair.
 *
 * @author Richard Bair
 */
final class ${LAF_NAME}Defaults {
    /**
     * The map of SynthStyles. This map is keyed by Region. Each Region maps
     * to a List of LazyStyles. Each LazyStyle has a reference to the prefix
     * that was registered with it. This reference can then be inspected to see
     * if it is the proper lazy style.
     * <p/>
     * There can be more than one LazyStyle for a single Region if there is more
     * than one prefix defined for a given region. For example, both Button and
     * "MyButton" might be prefixes assigned to the Region.Button region.
     */
    private Map<Region, List<LazyStyle>> m;
    /**
     * A map of regions which have been registered.
     * This mapping is maintained so that the Region can be found based on
     * prefix in a very fast manner. This is used in the "matches" method of
     * LazyStyle.
     */
    private Map<String, Region> registeredRegions =
            new HashMap<String, Region>();
    /**
     * Our fallback style to avoid NPEs if the proper style cannot be found in
     * this class. Not sure if relying on DefaultSynthStyle is the best choice.
     */
    private DefaultSynthStyle defaultStyle;
    /**
     * The default font that will be used. I store this value so that it can be
     * set in the UIDefaults when requested.
     */
    private FontUIResource defaultFont;

    /**
     * Map of lists of derived colors keyed by the DerivedColorKeys
     */
    private Map<DerivedColorKey, DerivedColor> derivedColorsMap =
            new HashMap<DerivedColorKey, DerivedColor>();

    /** Tempory key used for fetching from the derivedColorsMap */
    private final DerivedColorKey tmpDCKey = new DerivedColorKey();

    /** Listener for changes to user defaults table */
    private DefaultsListener defaultsListener = new DefaultsListener();

    /** Called by UIManager when this look and feel is installed. */
    void initialize() {
        // add listener for derived colors
        UIManager.addPropertyChangeListener(defaultsListener);
        UIManager.getDefaults().addPropertyChangeListener(defaultsListener);
    }

    /** Called by UIManager when this look and feel is uninstalled. */
    void uninitialize() {
        // remove listener for derived colors
        UIManager.getDefaults().removePropertyChangeListener(defaultsListener);
        UIManager.removePropertyChangeListener(defaultsListener);
    }

    /**
     * Create a new ${LAF_NAME}Defaults. This constructor is only called from
     * within ${LAF_NAME}LookAndFeel.
     */
    ${LAF_NAME}Defaults() {
        m = new HashMap<Region, List<LazyStyle>>();

        //Create the default font and default style. Also register all of the
        //regions and their states that this class will use for later lookup.
        //Additional regions can be registered later by 3rd party components.
        //These are simply the default registrations.
        defaultFont = FontManager.getFontConfigFUIR("sans", Font.PLAIN, 12);
        defaultStyle = new DefaultSynthStyle();
        defaultStyle.setFont(defaultFont);

        //initialize the map of styles
${STYLE_INIT}
    }

    //--------------- Methods called by ${LAF_NAME}LookAndFeel

    /**
     * Called from ${LAF_NAME}LookAndFeel to initialize the UIDefaults.
     *
     * @param d UIDefaults table to initialize. This will never be null.
     *          If listeners are attached to <code>d</code>, then you will
     *          only receive notification of LookAndFeel level defaults, not
     *          all defaults on the UIManager.
     */
    void initializeDefaults(UIDefaults d) {
${UI_DEFAULT_INIT}
    }

    /**
     * <p>Registers the given region and prefix. The prefix, if it contains
     * quoted sections, refers to certain named components. If there are not
     * quoted sections, then the prefix refers to a generic component type.</p>
     *
     * <p>If the given region/prefix combo has already been registered, then
     * it will not be registered twice. The second registration attempt will
     * fail silently.</p>
     *
     * @param region The Synth Region that is being registered. Such as Button,
     *        or ScrollBarThumb.
     * @param prefix The UIDefault prefix. For example, could be ComboBox, or if
     *        a named components, "MyComboBox", or even something like
     *        ToolBar:"MyComboBox":"ComboBox.arrowButton"
     */
    void register(Region region, String prefix) {
        //validate the method arguments
        if (region == null || prefix == null) {
            throw new IllegalArgumentException(
                    "Neither Region nor Prefix may be null");
        }

        //Add a LazyStyle for this region/prefix to m.
        List<LazyStyle> styles = m.get(region);
        if (styles == null) {
            styles = new LinkedList<LazyStyle>();
            styles.add(new LazyStyle(prefix));
            m.put(region, styles);
        } else {
            //iterate over all the current styles and see if this prefix has
            //already been registered. If not, then register it.
            for (LazyStyle s : styles) {
                if (prefix.equals(s.prefix)) {
                    return;
                }
            }
            styles.add(new LazyStyle(prefix));
        }

        //add this region to the map of registered regions
        registeredRegions.put(region.getName(), region);
    }

    /**
     * <p>Locate the style associated with the given region, and component.
     * This is called from ${LAF_NAME}LookAndFeel in the SynthStyleFactory
     * implementation.</p>
     *
     * <p>Lookup occurs as follows:<br/>
     * Check the map of styles <code>m</code>. If the map contains no styles at
     * all, then simply return the defaultStyle. If the map contains styles,
     * then iterate over all of the styles for the Region <code>r</code> looking
     * for the best match, based on prefix. If a match was made, then return
     * that SynthStyle. Otherwise, return the defaultStyle.</p>
     *
     * @param comp The component associated with this region. For example, if
     *        the Region is Region.Button then the component will be a JButton.
     *        If the Region is a subregion, such as ScrollBarThumb, then the
     *        associated component will be the component that subregion belongs
     *        to, such as JScrollBar. The JComponent may be named. It may not be
     *        null.
     * @param r The region we are looking for a style for. May not be null.
     */
    SynthStyle getStyle(JComponent comp, Region r) {
        //validate method arguments
        if (comp == null || r == null) {
            throw new IllegalArgumentException(
                    "Neither comp nor r may be null");
        }

        //if there are no lazy styles registered for the region r, then return
        //the default style
        List<LazyStyle> styles = m.get(r);
        if (styles == null || styles.size() == 0) {
            return defaultStyle;
        }

        //Look for the best SynthStyle for this component/region pair.
        LazyStyle foundStyle = null;
        for (LazyStyle s : styles) {
            if (s.matches(comp)) {
                //replace the foundStyle if foundStyle is null, or
                //if the new style "s" is more specific (ie, its path was
                //longer), or if the foundStyle was "simple" and the new style
                //was not (ie: the foundStyle was for something like Button and
                //the new style was for something like "MyButton", hence, being
                //more specific.) In all cases, favor the most specific style
                //found.
                if (foundStyle == null ||
                   (foundStyle.parts.length < s.parts.length) ||
                   (foundStyle.parts.length == s.parts.length 
                    && foundStyle.simple && !s.simple)) {
                    foundStyle = s;
                }
            }
        }

        //return the style, if found, or the default style if not found
        return foundStyle == null ? defaultStyle : foundStyle.getStyle(comp);
    }

    /*
        Various public helper classes.
        These may be used to register 3rd party values into UIDefaults
    */

    /**
     * <p>Derives its font value based on a parent font and a set of offsets and
     * attributes. This class is an ActiveValue, meaning that it will recompute
     * its value each time it is requested from UIDefaults. It is therefore
     * recommended to read this value once and cache it in the UI delegate class
     * until asked to reinitialize.</p>
     *
     * <p>To use this class, create an instance with the key of the font in the
     * UI defaults table from which to derive this font, along with a size
     * offset (if any), and whether it is to be bold, italic, or left in its
     * default form.</p>
     */
    static final class DerivedFont implements UIDefaults.ActiveValue {
        private float sizeOffset;
        private Boolean bold;
        private Boolean italic;
        private String parentKey;

        /**
         * Create a new DerivedFont.
         *
         * @param key The UIDefault key associated with this derived font's
         *            parent or source. If this key leads to a null value, or a
         *            value that is not a font, then null will be returned as
         *            the derived font. The key must not be null.
         * @param sizeOffset The size offset, as a percentage, to use. For
         *                   example, if the source font was a 12pt font and the
         *                   sizeOffset were specified as .9, then the new font
         *                   will be 90% of what the source font was, or, 10.8
         *                   pts which is rounded to 11pts. This fractional
         *                   based offset allows for proper font scaling in high
         *                   DPI or large system font scenarios.
         * @param bold Whether the new font should be bold. If null, then this
         *             new font will inherit the bold setting of the source
         *             font.
         * @param italic Whether the new font should be italicized. If null,
         *               then this new font will inherit the italic setting of
         *               the source font.
         */
        public DerivedFont(String key, float sizeOffset, Boolean bold,
                           Boolean italic) {
            //validate the constructor arguments
            if (key == null) {
                throw new IllegalArgumentException("You must specify a key");
            }

            //set the values
            this.parentKey = key;
            this.sizeOffset = sizeOffset;
            this.bold = bold;
            this.italic = italic;
        }

        /**
         * @inheritDoc
         */
        @Override
        public Object createValue(UIDefaults defaults) {
            Font f = defaults.getFont(parentKey);
            if (f != null) {
                // always round size for now so we have exact int font size
                // (or we may have lame looking fonts)
                float size = Math.round(f.getSize2D() * sizeOffset);
                int style = f.getStyle();
                if (bold != null) {
                    if (bold.booleanValue()) {
                        style = style | Font.BOLD;
                    } else {
                        style = style & ~Font.BOLD;
                    }
                }
                if (italic != null) {
                    if (italic.booleanValue()) {
                        style = style | Font.ITALIC;
                    } else {
                        style = style & ~Font.ITALIC;
                    }
                }
                return f.deriveFont(style, size);
            } else {
                return null;
            }
        }
    }


    /**
     * This class is private because it relies on the constructor of the
     * auto-generated AbstractRegionPainter subclasses. Hence, it is not
     * generally useful, and is private.
     * <p/>
     * LazyPainter is a LazyValue class. It will create the
     * AbstractRegionPainter lazily, when asked. It uses reflection to load the
     * proper class and invoke its constructor.
     */
    private static final class LazyPainter implements UIDefaults.LazyValue {
        private int which;
        private AbstractRegionPainter.PaintContext ctx;
        private String className;

        LazyPainter(String className, int which, Insets insets,
                    Dimension canvasSize, boolean inverted) {
            if (className == null) {
                throw new IllegalArgumentException(
                        "The className must be specified");
            }

            this.className = className;
            this.which = which;
            this.ctx = new AbstractRegionPainter.PaintContext(
                insets, canvasSize, inverted);
        }

        LazyPainter(String className, int which, Insets insets,
                    Dimension canvasSize, boolean inverted,
                    AbstractRegionPainter.PaintContext.CacheMode cacheMode,
                    double maxH, double maxV) {
            if (className == null) {
                throw new IllegalArgumentException(
                        "The className must be specified");
            }

            this.className = className;
            this.which = which;
            this.ctx = new AbstractRegionPainter.PaintContext(
                    insets, canvasSize, inverted, cacheMode, maxH, maxV);
        }

        @Override
        public Object createValue(UIDefaults table) {
            try {
                Class c;
                Object cl;
                // See if we should use a separate ClassLoader
                if (table == null || !((cl = table.get("ClassLoader"))
                                       instanceof ClassLoader)) {
                    cl = Thread.currentThread().
                                getContextClassLoader();
                    if (cl == null) {
                        // Fallback to the system class loader.
                        cl = ClassLoader.getSystemClassLoader();
                    }
                }

                c = Class.forName(className, true, (ClassLoader)cl);
                Constructor constructor = c.getConstructor(
                        AbstractRegionPainter.PaintContext.class, int.class);
                if (constructor == null) {
                    throw new NullPointerException(
                            "Failed to find the constructor for the class: " +
                            className);
                }
                return constructor.newInstance(ctx, which);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    }

    /**
     * A class which creates the NimbusStyle associated with it lazily, but also
     * manages a lot more information about the style. It is less of a LazyValue
     * type of class, and more of an Entry or Item type of class, as it
     * represents an entry in the list of LazyStyles in the map m.
     *
     * The primary responsibilities of this class include:
     * <ul>
     *   <li>Determining whether a given component/region pair matches this
     *       style</li>
     *   <li>Splitting the prefix specified in the constructor into its
     *       constituent parts to facilitate quicker matching</li>
     *   <li>Creating and vending a NimbusStyle lazily.</li>
     * </ul>
     */
    private final class LazyStyle {
        /**
         * The prefix this LazyStyle was registered with. Something like
         * Button or ComboBox:"ComboBox.arrowButton"
         */
        private String prefix;
        /**
         * Whether or not this LazyStyle represents an unnamed component
         */
        private boolean simple = true;
        /**
         * The various parts, or sections, of the prefix. For example,
         * the prefix:
         *     ComboBox:"ComboBox.arrowButton"
         *
         * will be broken into two parts,
         *     ComboBox and "ComboBox.arrowButton"
         */
        private Part[] parts;
        /**
         * Cached shared style.
         */
        private NimbusStyle style;
        /**
         * A weakly referenced hash map such that if the reference JComponent
         * key is garbage collected then the entry is removed from the map.
         * This cache exists so that when a JComponent has nimbus overrides
         * in its client map, a unique style will be created and returned
         * for that JComponent instance, always. In such a situation each
         * JComponent instance must have its own instance of NimbusStyle.
         */
        private WeakHashMap<JComponent, WeakReference<NimbusStyle>> overridesCache;

        /**
         * Create a new LazyStyle.
         *
         * @param prefix The prefix associated with this style. Cannot be null.
         */
        private LazyStyle(String prefix) {
            if (prefix == null) {
                throw new IllegalArgumentException(
                        "The prefix must not be null");
            }

            this.prefix = prefix;

            //there is one odd case that needs to be supported here: cell
            //renderers. A cell renderer is defined as a named internal
            //component, so for example:
            // List."List.cellRenderer"
            //The problem is that the component named List.cellRenderer is not a
            //child of a JList. Rather, it is treated more as a direct component
            //Thus, if the prefix ends with "cellRenderer", then remove all the
            //previous dotted parts of the prefix name so that it becomes, for
            //example: "List.cellRenderer"
            //Likewise, we have a hacked work around for cellRenderer, renderer,
            //and listRenderer.
            String temp = prefix;
            if (temp.endsWith("cellRenderer\"")
                    || temp.endsWith("renderer\"")
                    || temp.endsWith("listRenderer\"")) {
                temp = temp.substring(temp.lastIndexOf(":\"") + 1);
            }

            //otherwise, normal code path
            List<String> sparts = split(temp);
            parts = new Part[sparts.size()];
            for (int i = 0; i < parts.length; i++) {
                parts[i] = new Part(sparts.get(i));
                if (parts[i].named) {
                    simple = false;
                }
            }
        }

        /**
         * Gets the style. Creates it if necessary.
         * @return the style
         */
        SynthStyle getStyle(JComponent c) {
            // if the component has overrides, it gets its own unique style
            // instead of the shared style.
            if (c.getClientProperty("Nimbus.Overrides") != null) {
                if (overridesCache == null)
                    overridesCache = new WeakHashMap<JComponent, WeakReference<NimbusStyle>>();
                WeakReference<NimbusStyle> ref = overridesCache.get(c);
                NimbusStyle s = ref == null ? null : ref.get();
                if (s == null) {
                    s = new NimbusStyle(prefix, c);
                    overridesCache.put(c, new WeakReference<NimbusStyle>(s));
                }
                return s;
            }
            
            // lazily create the style if necessary
            if (style == null)
                style = new NimbusStyle(prefix, null);
            
            // return the style
            return style;
        }

        /**
         * This LazyStyle is a match for the given component if, and only if,
         * for each part of the prefix the component hierarchy matches exactly.
         * That is, if given "a":something:"b", then:
         * c.getName() must equals "b"
         * c.getParent() can be anything
         * c.getParent().getParent().getName() must equal "a".
         */
        boolean matches(JComponent c) {
            return matches(c, parts.length - 1);
        }

        private boolean matches(Component c, int partIndex) {
            if (partIndex < 0) return true;
            if (c == null) return false;
            //only get here if partIndex > 0 and c == null

            String name = c.getName();
            if (parts[partIndex].named && parts[partIndex].s.equals(name)) {
                //so far so good, recurse
                return matches(c.getParent(), partIndex - 1);
            } else if (!parts[partIndex].named) {
                //if c is not named, and parts[partIndex] has an expected class
                //type registered, then check to make sure c is of the
                //right type;
                Class clazz = parts[partIndex].c;
                if (clazz != null && clazz.isAssignableFrom(c.getClass())) {
                    //so far so good, recurse
                    return matches(c.getParent(), partIndex - 1);
                } else if (clazz == null &&
                           registeredRegions.containsKey(parts[partIndex].s)) {
                    Region r = registeredRegions.get(parts[partIndex].s);
                    Component parent = r.isSubregion() ? c : c.getParent();
                    //special case the JInternalFrameTitlePane, because it
                    //doesn't fit the mold. very, very funky.
                    if (r == Region.INTERNAL_FRAME_TITLE_PANE && parent != null
                        && parent instanceof JInternalFrame.JDesktopIcon) {
                        JInternalFrame.JDesktopIcon icon =
                                (JInternalFrame.JDesktopIcon) parent;
                        parent = icon.getInternalFrame();
                    }
                    //it was the name of a region. So far, so good. Recurse.
                    return matches(parent, partIndex - 1);
                }
            }

            return false;
        }

        /**
         * Given some dot separated prefix, split on the colons that are
         * not within quotes, and not within brackets.
         *
         * @param prefix
         * @return
         */
        private List<String> split(String prefix) {
            List<String> parts = new ArrayList<String>();
            int bracketCount = 0;
            boolean inquotes = false;
            int lastIndex = 0;
            for (int i = 0; i < prefix.length(); i++) {
                char c = prefix.charAt(i);

                if (c == '[') {
                    bracketCount++;
                    continue;
                } else if (c == '"') {
                    inquotes = !inquotes;
                    continue;
                } else if (c == ']') {
                    bracketCount--;
                    if (bracketCount < 0) {
                        throw new RuntimeException(
                                "Malformed prefix: " + prefix);
                    }
                    continue;
                }

                if (c == ':' && !inquotes && bracketCount == 0) {
                    //found a character to split on.
                    parts.add(prefix.substring(lastIndex, i));
                    lastIndex = i + 1;
                }
            }
            if (lastIndex < prefix.length() - 1 && !inquotes
                    && bracketCount == 0) {
                parts.add(prefix.substring(lastIndex));
            }
            return parts;

        }

        private final class Part {
            private String s;
            //true if this part represents a component name
            private boolean named;
            private Class c;

            Part(String s) {
                named = s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"';
                if (named) {
                    this.s = s.substring(1, s.length() - 1);
                } else {
                    this.s = s;
                    //TODO use a map of known regions for Synth and Swing, and
                    //then use [classname] instead of org_class_name style
                    try {
                        c = Class.forName("javax.swing.J" + s);
                    } catch (Exception e) {
                    }
                    try {
                        c = Class.forName(s.replace("_", "."));
                    } catch (Exception e) {
                    }
                }
            }
        }
    }

    /**
     * Get a derived color, derived colors are shared instances and will be
     * updated when its parent UIDefault color changes.
     *
     * @param uiDefaultParentName The parent UIDefault key
     * @param hOffset The hue offset
     * @param sOffset The saturation offset
     * @param bOffset The brightness offset
     * @param aOffset The alpha offset
     * @return The stored derived color
     */
    public DerivedColor getDerivedColor(String uiDefaultParentName,
                                        float hOffset, float sOffset,
                                        float bOffset, int aOffset){
        return getDerivedColor(uiDefaultParentName, hOffset, sOffset,
                               bOffset, aOffset, true);
    }

    /**
     * Get a derived color, derived colors are shared instances and will be
     * updated when its parent UIDefault color changes.
     *
     * @param uiDefaultParentName The parent UIDefault key
     * @param hOffset The hue offset
     * @param sOffset The saturation offset
     * @param bOffset The brightness offset
     * @param aOffset The alpha offset
     * @param uiResource True if the derived color should be a UIResource,
     *        false if it should not be a UIResource
     * @return The stored derived color
     */
    public DerivedColor getDerivedColor(String uiDefaultParentName,
                                        float hOffset, float sOffset,
                                        float bOffset, int aOffset,
                                        boolean uiResource){
        tmpDCKey.set(uiDefaultParentName, hOffset, sOffset, bOffset, aOffset,
            uiResource);
        DerivedColor color = derivedColorsMap.get(tmpDCKey);
        if (color == null){
            if (uiResource) {
                color = new DerivedColor.UIResource(uiDefaultParentName,
                        hOffset, sOffset, bOffset, aOffset);
            } else {
                color = new DerivedColor(uiDefaultParentName, hOffset, sOffset,
                    bOffset, aOffset);
            }
            // calculate the initial value
            color.rederiveColor();
            // add the listener so that if the color changes we'll propogate it
            color.addPropertyChangeListener(defaultsListener);
            // add to the derived colors table
            derivedColorsMap.put(new DerivedColorKey(uiDefaultParentName,
                    hOffset, sOffset, bOffset, aOffset, uiResource),color);
        }
        return color;
    }

    /**
     * Key class for derived colors
     */
    private class DerivedColorKey {
        private String uiDefaultParentName;
        private float hOffset, sOffset, bOffset;
        private int aOffset;
        private boolean uiResource;

        DerivedColorKey(){}

        DerivedColorKey(String uiDefaultParentName, float hOffset,
                        float sOffset, float bOffset, int aOffset,
                        boolean uiResource) {
            set(uiDefaultParentName, hOffset, sOffset, bOffset, aOffset, uiResource);
        }

        void set (String uiDefaultParentName, float hOffset,
                        float sOffset, float bOffset, int aOffset,
                        boolean uiResource) {
            this.uiDefaultParentName = uiDefaultParentName;
            this.hOffset = hOffset;
            this.sOffset = sOffset;
            this.bOffset = bOffset;
            this.aOffset = aOffset;
            this.uiResource = uiResource;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof DerivedColorKey)) return false;
            DerivedColorKey that = (DerivedColorKey) o;
            if (aOffset != that.aOffset) return false;
            if (Float.compare(that.bOffset, bOffset) != 0) return false;
            if (Float.compare(that.hOffset, hOffset) != 0) return false;
            if (Float.compare(that.sOffset, sOffset) != 0) return false;
            if (uiDefaultParentName != null ?
                !uiDefaultParentName.equals(that.uiDefaultParentName) :
                that.uiDefaultParentName != null) return false;
            if (this.uiResource != that.uiResource) return false;
            return true;
        }

        @Override
        public int hashCode() {
            int result = super.hashCode();
            result = 31 * result + uiDefaultParentName.hashCode();
            result = 31 * result + hOffset != +0.0f ?
                    Float.floatToIntBits(hOffset) : 0;
            result = 31 * result + sOffset != +0.0f ?
                    Float.floatToIntBits(sOffset) : 0;
            result = 31 * result + bOffset != +0.0f ?
                    Float.floatToIntBits(bOffset) : 0;
            result = 31 * result + aOffset;
            result = 31 * result + (uiResource ? 1 : 0);
            return result;
        }
    }

    /**
     * Listener to update derived colors on UIManager Defaults changes
     */
    private class DefaultsListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            Object src = evt.getSource();
            String key = evt.getPropertyName();
            if (key.equals("lookAndFeel")){
                // LAF has been installed, this is the first point at which we
                // can access our defaults table via UIManager so before now
                // all derived colors will be incorrect.
                // First we need to update
                for (DerivedColor color : derivedColorsMap.values()) {
                    color.rederiveColor();
                }
            } else if (src instanceof DerivedColor && key.equals("rgb")) {
                // derived color that is in UIManager defaults has changed
                // update all its dependent colors. Don't worry about doing
                // this recursively since calling rederiveColor will cause
                // another PCE to be fired, ending up here and essentially
                // recursing
                DerivedColor parentColor = (DerivedColor)src;
                String parentKey = null;
                Set<Map.Entry<Object,Object>> entries =
                        UIManager.getDefaults().entrySet();
                
                for (Map.Entry entry : entries) {
                    Object value = entry.getValue();
                    if (value == parentColor) {
                        parentKey = entry.getKey().toString();
                    }
                }
                
                if (parentKey == null) {
                    //couldn't find the DerivedColor in the UIDefaults map,
                    //so we just bail.
                    return;
                }
                
                for (Map.Entry entry : entries) {
                    Object value = entry.getValue();
                    if (value instanceof DerivedColor) {
                        DerivedColor color = (DerivedColor)entry.getValue();
                        if (parentKey.equals(color.getUiDefaultParentName())) {
                            color.rederiveColor();
                        }
                    }
                }
            }
        }
    }

    private static final class PainterBorder implements Border, UIResource {
        private Insets insets;
        private Painter painter;
        private String painterKey;
        
        PainterBorder(String painterKey, Insets insets) {
            this.insets = insets;
            this.painterKey = painterKey;
        }
        
        @Override
        public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) {
            if (painter == null) {
                painter = (Painter)UIManager.get(painterKey);
                if (painter == null) return;
            }
            
            g.translate(x, y);
            if (g instanceof Graphics2D)
                painter.paint((Graphics2D)g, c, w, h);
            else {
                BufferedImage img = new BufferedImage(w, h, TYPE_INT_ARGB);
                Graphics2D gfx = img.createGraphics();
                painter.paint(gfx, c, w, h);
                gfx.dispose();
                g.drawImage(img, x, y, null);
                img = null;
            }
            g.translate(-x, -y);
        }

        @Override
        public Insets getBorderInsets(Component c) {
            return (Insets)insets.clone();
        }

        @Override
        public boolean isBorderOpaque() {
            return false;
        }
    }
}