1/*
2 * Copyright (c) 2002, 2017, 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
26package com.sun.java.swing.plaf.gtk;
27
28import java.awt.*;
29import java.lang.reflect.*;
30import java.security.*;
31import java.util.*;
32import javax.swing.*;
33import javax.swing.plaf.*;
34import javax.swing.plaf.synth.*;
35
36import sun.awt.AppContext;
37import sun.awt.UNIXToolkit;
38import sun.swing.SwingUtilities2;
39import javax.swing.plaf.synth.SynthIcon;
40
41import com.sun.java.swing.plaf.gtk.GTKEngine.WidgetType;
42import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING;
43import static java.awt.RenderingHints.KEY_TEXT_LCD_CONTRAST;
44
45/**
46 *
47 * @author Scott Violet
48 */
49class GTKStyle extends SynthStyle implements GTKConstants {
50
51    private static native int nativeGetXThickness(int widgetType);
52    private static native int nativeGetYThickness(int widgetType);
53    private static native int nativeGetColorForState(int widgetType,
54                                                     int state, int typeID);
55    private static native Object nativeGetClassValue(int widgetType,
56                                                     String key);
57    private static native String nativeGetPangoFontName(int widgetType);
58
59    private static final String ICON_PROPERTY_PREFIX = "gtk.icon.";
60
61    static final Color BLACK_COLOR = new ColorUIResource(Color.BLACK);
62    static final Color WHITE_COLOR = new ColorUIResource(Color.WHITE);
63
64    static final Font DEFAULT_FONT = new FontUIResource("sansserif",
65                                                        Font.PLAIN, 10  );
66    static final Insets BUTTON_DEFAULT_BORDER_INSETS = new Insets(1, 1, 1, 1);
67
68    private static final GTKGraphicsUtils GTK_GRAPHICS = new GTKGraphicsUtils();
69
70    /**
71     * Maps from a key that is passed to Style.get to the equivalent class
72     * specific key.
73     */
74    private static final Map<String,String> CLASS_SPECIFIC_MAP;
75
76    /**
77     * Backing style properties that are used if the style does not
78     * defined the property.
79     */
80    private static final Map<String,GTKStockIcon> ICONS_MAP;
81
82    /**
83     * The font used for this particular style, as determined at
84     * construction time.
85     */
86    private final Font font;
87
88    /** Widget type used when looking up class specific values. */
89    private final int widgetType;
90
91    /** The x/y thickness values for this particular style. */
92    private final int xThickness, yThickness;
93
94    GTKStyle(Font userFont, WidgetType widgetType) {
95        this.widgetType = widgetType.ordinal();
96
97        String pangoFontName;
98        synchronized (sun.awt.UNIXToolkit.GTK_LOCK) {
99            xThickness = nativeGetXThickness(this.widgetType);
100            yThickness = nativeGetYThickness(this.widgetType);
101            pangoFontName = nativeGetPangoFontName(this.widgetType);
102        }
103
104        Font pangoFont = null;
105        if (pangoFontName != null) {
106            pangoFont = PangoFonts.lookupFont(pangoFontName);
107        }
108        if (pangoFont != null) {
109            this.font = pangoFont;
110        } else if (userFont != null) {
111            this.font = userFont;
112        } else {
113            this.font = DEFAULT_FONT;
114        }
115    }
116
117    @Override
118    public void installDefaults(SynthContext context) {
119        super.installDefaults(context);
120        Map<Object, Object> aaTextInfo = GTKLookAndFeel.aaTextInfo;
121        if (aaTextInfo != null && !context.getRegion().isSubregion()) {
122            context.getComponent().putClientProperty(KEY_TEXT_ANTIALIASING,
123                    aaTextInfo.get(KEY_TEXT_ANTIALIASING));
124            context.getComponent().putClientProperty(KEY_TEXT_LCD_CONTRAST,
125                    aaTextInfo.get(KEY_TEXT_LCD_CONTRAST));
126        }
127    }
128
129    @Override
130    public SynthGraphicsUtils getGraphicsUtils(SynthContext context) {
131        return GTK_GRAPHICS;
132    }
133
134    /**
135     * Returns a <code>SynthPainter</code> that will route the appropriate
136     * calls to a <code>GTKEngine</code>.
137     *
138     * @param state SynthContext identifying requestor
139     * @return SynthPainter
140     */
141    @Override
142    public SynthPainter getPainter(SynthContext state) {
143        return GTKPainter.INSTANCE;
144    }
145
146    protected Color getColorForState(SynthContext context, ColorType type) {
147        if (type == ColorType.FOCUS || type == GTKColorType.BLACK) {
148            return BLACK_COLOR;
149        }
150        else if (type == GTKColorType.WHITE) {
151            return WHITE_COLOR;
152        }
153
154        Region id = context.getRegion();
155        int state = context.getComponentState();
156        state = GTKLookAndFeel.synthStateToGTKState(id, state);
157
158        if (type == ColorType.TEXT_FOREGROUND &&
159               (id == Region.BUTTON ||
160                id == Region.CHECK_BOX ||
161                id == Region.CHECK_BOX_MENU_ITEM ||
162                id == Region.MENU ||
163                id == Region.MENU_ITEM ||
164                id == Region.RADIO_BUTTON ||
165                id == Region.RADIO_BUTTON_MENU_ITEM ||
166                id == Region.TABBED_PANE_TAB ||
167                id == Region.TOGGLE_BUTTON ||
168                id == Region.TOOL_TIP ||
169                id == Region.MENU_ITEM_ACCELERATOR ||
170                id == Region.TABBED_PANE_TAB)) {
171            type = ColorType.FOREGROUND;
172        } else if (id == Region.TABLE ||
173                   id == Region.LIST ||
174                   id == Region.TREE ||
175                   id == Region.TREE_CELL) {
176            if (type == ColorType.FOREGROUND) {
177                type = ColorType.TEXT_FOREGROUND;
178                if (state == SynthConstants.PRESSED) {
179                    state = SynthConstants.SELECTED;
180                }
181            } else if (type == ColorType.BACKGROUND) {
182                type = ColorType.TEXT_BACKGROUND;
183            }
184        }
185
186        return getStyleSpecificColor(context, state, type);
187    }
188
189    /**
190     * Returns color specific to the current style. This method is
191     * invoked when other variants don't fit.
192     */
193    private Color getStyleSpecificColor(SynthContext context, int state,
194                                        ColorType type)
195    {
196        state = GTKLookAndFeel.synthStateToGTKStateType(state).ordinal();
197        synchronized (sun.awt.UNIXToolkit.GTK_LOCK) {
198            int rgb = nativeGetColorForState(widgetType, state,
199                                             type.getID());
200            return new ColorUIResource(rgb);
201        }
202    }
203
204    Color getGTKColor(int state, ColorType type) {
205        return getGTKColor(null, state, type);
206    }
207
208    /**
209     * Returns the color for the specified state.
210     *
211     * @param context SynthContext identifying requestor
212     * @param state to get the color for
213     * @param type of the color
214     * @return Color to render with
215     */
216    Color getGTKColor(SynthContext context, int state, ColorType type) {
217        if (context != null) {
218            JComponent c = context.getComponent();
219            Region id = context.getRegion();
220
221            state = GTKLookAndFeel.synthStateToGTKState(id, state);
222            if (!id.isSubregion() &&
223                (state & SynthConstants.ENABLED) != 0) {
224                if (type == ColorType.BACKGROUND ||
225                    type == ColorType.TEXT_BACKGROUND) {
226                    Color bg = c.getBackground();
227                    if (!(bg instanceof UIResource)) {
228                        return bg;
229                    }
230                }
231                else if (type == ColorType.FOREGROUND ||
232                         type == ColorType.TEXT_FOREGROUND) {
233                    Color fg = c.getForeground();
234                    if (!(fg instanceof UIResource)) {
235                        return fg;
236                    }
237                }
238            }
239        }
240
241        return getStyleSpecificColor(context, state, type);
242    }
243
244    @Override
245    public Color getColor(SynthContext context, ColorType type) {
246        JComponent c = context.getComponent();
247        Region id = context.getRegion();
248        int state = context.getComponentState();
249
250        if (c.getName() == "Table.cellRenderer") {
251             if (type == ColorType.BACKGROUND) {
252                 return c.getBackground();
253             }
254             if (type == ColorType.FOREGROUND) {
255                 return c.getForeground();
256             }
257        }
258
259        if (id == Region.LABEL && type == ColorType.TEXT_FOREGROUND) {
260            type = ColorType.FOREGROUND;
261        }
262
263        // For the enabled state, prefer the widget's colors
264        if (!id.isSubregion() && (state & SynthConstants.ENABLED) != 0) {
265            if (type == ColorType.BACKGROUND) {
266                return c.getBackground();
267            }
268            else if (type == ColorType.FOREGROUND) {
269                return c.getForeground();
270            }
271            else if (type == ColorType.TEXT_FOREGROUND) {
272                // If getForeground returns a non-UIResource it means the
273                // developer has explicitly set the foreground, use it over
274                // that of TEXT_FOREGROUND as that is typically the expected
275                // behavior.
276                Color color = c.getForeground();
277                if (color != null && !(color instanceof UIResource)) {
278                    return color;
279                }
280            }
281        }
282        return getColorForState(context, type);
283    }
284
285    Font getDefaultFont() {
286        return font;
287    }
288
289    protected Font getFontForState(SynthContext context) {
290        Font propFont = UIManager
291                              .getFont(context.getRegion().getName() + ".font");
292        if (propFont != null) {
293            // if font property got a value then return it
294            return propFont;
295        }
296        return font;
297    }
298
299    /**
300     * Returns the X thickness to use for this GTKStyle.
301     *
302     * @return x thickness.
303     */
304    int getXThickness() {
305        return xThickness;
306    }
307
308    /**
309     * Returns the Y thickness to use for this GTKStyle.
310     *
311     * @return y thickness.
312     */
313    int getYThickness() {
314        return yThickness;
315    }
316
317    /**
318     * Returns the Insets. If <code>insets</code> is non-null the resulting
319     * insets will be placed in it, otherwise a new Insets object will be
320     * created and returned.
321     *
322     * @param state SynthContext identifying requestor
323     * @param insets Where to place Insets
324     * @return Insets.
325     */
326    @Override
327    public Insets getInsets(SynthContext state, Insets insets) {
328        Region id = state.getRegion();
329        JComponent component = state.getComponent();
330        String name = (id.isSubregion()) ? null : component.getName();
331
332        if (insets == null) {
333            insets = new Insets(0, 0, 0, 0);
334        } else {
335            insets.top = insets.bottom = insets.left = insets.right = 0;
336        }
337
338        if (id == Region.ARROW_BUTTON || id == Region.BUTTON ||
339                id == Region.TOGGLE_BUTTON) {
340            if ("Spinner.previousButton" == name ||
341                    "Spinner.nextButton" == name) {
342                return getSimpleInsets(state, insets, 1);
343            } else {
344                return getButtonInsets(state, insets);
345            }
346        }
347        else if (id == Region.CHECK_BOX || id == Region.RADIO_BUTTON) {
348            return getRadioInsets(state, insets);
349        }
350        else if (id == Region.MENU_BAR) {
351            return getMenuBarInsets(state, insets);
352        }
353        else if (id == Region.MENU ||
354                 id == Region.MENU_ITEM ||
355                 id == Region.CHECK_BOX_MENU_ITEM ||
356                 id == Region.RADIO_BUTTON_MENU_ITEM) {
357            return getMenuItemInsets(state, insets);
358        }
359        else if (id == Region.FORMATTED_TEXT_FIELD) {
360            return getTextFieldInsets(state, insets);
361        }
362        else if (id == Region.INTERNAL_FRAME) {
363            insets = Metacity.INSTANCE.getBorderInsets(state, insets);
364        }
365        else if (id == Region.LABEL) {
366            if ("TableHeader.renderer" == name) {
367                return getButtonInsets(state, insets);
368            }
369            else if (component instanceof ListCellRenderer) {
370                return getTextFieldInsets(state, insets);
371            }
372            else if ("Tree.cellRenderer" == name) {
373                return getSimpleInsets(state, insets, 1);
374            }
375        }
376        else if (id == Region.OPTION_PANE) {
377            return getSimpleInsets(state, insets, 6);
378        }
379        else if (id == Region.POPUP_MENU) {
380            return getSimpleInsets(state, insets, 2);
381        }
382        else if (id == Region.PROGRESS_BAR || id == Region.SLIDER ||
383                 id == Region.TABBED_PANE  || id == Region.TABBED_PANE_CONTENT ||
384                 id == Region.TOOL_BAR     ||
385                 id == Region.TOOL_BAR_DRAG_WINDOW ||
386                 id == Region.TOOL_TIP) {
387            return getThicknessInsets(state, insets);
388        }
389        else if (id == Region.SCROLL_BAR) {
390            return getScrollBarInsets(state, insets);
391        }
392        else if (id == Region.SLIDER_TRACK) {
393            return getSliderTrackInsets(state, insets);
394        }
395        else if (id == Region.TABBED_PANE_TAB) {
396            return getTabbedPaneTabInsets(state, insets);
397        }
398        else if (id == Region.TEXT_FIELD || id == Region.PASSWORD_FIELD) {
399            if (name == "Tree.cellEditor") {
400                return getSimpleInsets(state, insets, 1);
401            }
402            return getTextFieldInsets(state, insets);
403        } else if (id == Region.SEPARATOR ||
404                   id == Region.POPUP_MENU_SEPARATOR ||
405                   id == Region.TOOL_BAR_SEPARATOR) {
406            return getSeparatorInsets(state, insets);
407        } else if (id == GTKEngine.CustomRegion.TITLED_BORDER) {
408            return getThicknessInsets(state, insets);
409        }
410        return insets;
411    }
412
413    private Insets getButtonInsets(SynthContext context, Insets insets) {
414        // The following calculations are derived from gtkbutton.c
415        // (GTK+ version 2.8.20), gtk_button_size_allocate() method.
416        int CHILD_SPACING = 1;
417        int focusSize = getClassSpecificIntValue(context, "focus-line-width",1);
418        int focusPad = getClassSpecificIntValue(context, "focus-padding", 1);
419        int xThickness = getXThickness();
420        int yThickness = getYThickness();
421        int w = focusSize + focusPad + xThickness + CHILD_SPACING;
422        int h = focusSize + focusPad + yThickness + CHILD_SPACING;
423        insets.left = insets.right = w;
424        insets.top = insets.bottom = h;
425
426        Component component = context.getComponent();
427        if ((component instanceof JButton) &&
428            !(component.getParent() instanceof JToolBar) &&
429            ((JButton)component).isDefaultCapable())
430        {
431            // Include the default border insets, but only for JButtons
432            // that are default capable.  Note that
433            // JButton.getDefaultCapable() returns true by default, but
434            // GtkToolButtons are never default capable, so we skip this
435            // step if the button is contained in a toolbar.
436            Insets defaultInsets = getClassSpecificInsetsValue(context,
437                          "default-border", BUTTON_DEFAULT_BORDER_INSETS);
438            insets.left += defaultInsets.left;
439            insets.right += defaultInsets.right;
440            insets.top += defaultInsets.top;
441            insets.bottom += defaultInsets.bottom;
442        }
443
444        return insets;
445    }
446
447    /*
448     * This is used for both RADIO_BUTTON and CHECK_BOX.
449     */
450    private Insets getRadioInsets(SynthContext context, Insets insets) {
451        // The following calculations are derived from gtkcheckbutton.c
452        // (GTK+ version 2.8.20), gtk_check_button_size_allocate() method.
453        int focusSize =
454            getClassSpecificIntValue(context, "focus-line-width", 1);
455        int focusPad =
456            getClassSpecificIntValue(context, "focus-padding", 1);
457        int totalFocus = focusSize + focusPad;
458
459        // Note: GTKIconFactory.DelegateIcon will have already included the
460        // "indicator-spacing" value in the size of the indicator icon,
461        // which explains why we use zero as the left inset (or right inset
462        // in the RTL case); see 6489585 for more details.
463        insets.top    = totalFocus;
464        insets.bottom = totalFocus;
465        if (context.getComponent().getComponentOrientation().isLeftToRight()) {
466            insets.left  = 0;
467            insets.right = totalFocus;
468        } else {
469            insets.left  = totalFocus;
470            insets.right = 0;
471        }
472
473        return insets;
474    }
475
476    private Insets getMenuBarInsets(SynthContext context, Insets insets) {
477        // The following calculations are derived from gtkmenubar.c
478        // (GTK+ version 2.8.20), gtk_menu_bar_size_allocate() method.
479        int internalPadding = getClassSpecificIntValue(context,
480                                                       "internal-padding", 1);
481        int xThickness = getXThickness();
482        int yThickness = getYThickness();
483        insets.left = insets.right = xThickness + internalPadding;
484        insets.top = insets.bottom = yThickness + internalPadding;
485        return insets;
486    }
487
488    private Insets getMenuItemInsets(SynthContext context, Insets insets) {
489        // The following calculations are derived from gtkmenuitem.c
490        // (GTK+ version 2.8.20), gtk_menu_item_size_allocate() method.
491        int horizPadding = getClassSpecificIntValue(context,
492                                                    "horizontal-padding", 3);
493        int xThickness = getXThickness();
494        int yThickness = getYThickness();
495        insets.left = insets.right = xThickness + horizPadding;
496        insets.top = insets.bottom = yThickness;
497        return insets;
498    }
499
500    private Insets getThicknessInsets(SynthContext context, Insets insets) {
501        insets.left = insets.right = getXThickness();
502        insets.top = insets.bottom = getYThickness();
503        return insets;
504    }
505
506    private Insets getSeparatorInsets(SynthContext context, Insets insets) {
507        int horizPadding = 0;
508        if (context.getRegion() == Region.POPUP_MENU_SEPARATOR) {
509            horizPadding =
510                getClassSpecificIntValue(context, "horizontal-padding", 3);
511        }
512        insets.right = insets.left = getXThickness() + horizPadding;
513        insets.top = insets.bottom = getYThickness();
514        return insets;
515    }
516
517    private Insets getSliderTrackInsets(SynthContext context, Insets insets) {
518        int focusSize = getClassSpecificIntValue(context, "focus-line-width", 1);
519        int focusPad = getClassSpecificIntValue(context, "focus-padding", 1);
520        insets.top = insets.bottom =
521                insets.left = insets.right = focusSize + focusPad;
522        return insets;
523    }
524
525    private Insets getSimpleInsets(SynthContext context, Insets insets, int n) {
526        insets.top = insets.bottom = insets.right = insets.left = n;
527        return insets;
528    }
529
530    private Insets getTabbedPaneTabInsets(SynthContext context, Insets insets) {
531        int xThickness = getXThickness();
532        int yThickness = getYThickness();
533        int focusSize = getClassSpecificIntValue(context, "focus-line-width",1);
534        int pad = 2;
535
536        insets.left = insets.right = focusSize + pad + xThickness;
537        insets.top = insets.bottom = focusSize + pad + yThickness;
538        return insets;
539    }
540
541    // NOTE: this is called for ComboBox, and FormattedTextField also
542    private Insets getTextFieldInsets(SynthContext context, Insets insets) {
543        insets = getClassSpecificInsetsValue(context, "inner-border",
544                                    getSimpleInsets(context, insets, 2));
545
546        int xThickness = getXThickness();
547        int yThickness = getYThickness();
548        boolean interiorFocus =
549                getClassSpecificBoolValue(context, "interior-focus", true);
550        int focusSize = 0;
551
552        if (!interiorFocus) {
553            focusSize = getClassSpecificIntValue(context, "focus-line-width",1);
554        }
555
556        insets.left   += focusSize + xThickness;
557        insets.right  += focusSize + xThickness;
558        insets.top    += focusSize + yThickness;
559        insets.bottom += focusSize + yThickness;
560        return insets;
561    }
562
563    private Insets getScrollBarInsets(SynthContext context, Insets insets) {
564        int troughBorder =
565            getClassSpecificIntValue(context, "trough-border", 1);
566        insets.left = insets.right = insets.top = insets.bottom = troughBorder;
567
568        JComponent c = context.getComponent();
569        if (c.getParent() instanceof JScrollPane) {
570            // This scrollbar is part of a scrollpane; use only the
571            // "scrollbar-spacing" style property to determine the padding
572            // between the scrollbar and its parent scrollpane.
573            int spacing =
574                getClassSpecificIntValue(WidgetType.SCROLL_PANE,
575                                         "scrollbar-spacing", 3);
576            if (((JScrollBar)c).getOrientation() == JScrollBar.HORIZONTAL) {
577                insets.top += spacing;
578            } else {
579                if (c.getComponentOrientation().isLeftToRight()) {
580                    insets.left += spacing;
581                } else {
582                    insets.right += spacing;
583                }
584            }
585        } else {
586            // This is a standalone scrollbar; leave enough room for the
587            // focus line in addition to the trough border.
588            if (c.isFocusable()) {
589                int focusSize =
590                    getClassSpecificIntValue(context, "focus-line-width", 1);
591                int focusPad =
592                    getClassSpecificIntValue(context, "focus-padding", 1);
593                int totalFocus = focusSize + focusPad;
594                insets.left   += totalFocus;
595                insets.right  += totalFocus;
596                insets.top    += totalFocus;
597                insets.bottom += totalFocus;
598            }
599        }
600        return insets;
601    }
602
603    /**
604     * Returns the value for a class specific property for a particular
605     * WidgetType.  This method is useful in those cases where we need to
606     * fetch a value for a Region that is not associated with the component
607     * currently in use (e.g. we need to figure out the insets for a
608     * SCROLL_BAR, but certain values can only be extracted from a
609     * SCROLL_PANE region).
610     *
611     * @param wt WidgetType for which to fetch the value
612     * @param key Key identifying class specific value
613     * @return Value, or null if one has not been defined
614     */
615    private static Object getClassSpecificValue(WidgetType wt, String key) {
616        synchronized (UNIXToolkit.GTK_LOCK) {
617            return nativeGetClassValue(wt.ordinal(), key);
618        }
619    }
620
621    /**
622     * Convenience method to get a class specific integer value for
623     * a particular WidgetType.
624     *
625     * @param wt WidgetType for which to fetch the value
626     * @param key Key identifying class specific value
627     * @param defaultValue Returned if there is no value for the specified
628     *        type
629     * @return Value, or defaultValue if <code>key</code> is not defined
630     */
631    private static int getClassSpecificIntValue(WidgetType wt, String key,
632                                                int defaultValue)
633    {
634        Object value = getClassSpecificValue(wt, key);
635        if (value instanceof Number) {
636            return ((Number)value).intValue();
637        }
638        return defaultValue;
639    }
640
641    /**
642     * Returns the value for a class specific property. A class specific value
643     * is a value that will be picked up based on class hierarchy.
644     *
645     * @param key Key identifying class specific value
646     * @return Value, or null if one has not been defined.
647     */
648    Object getClassSpecificValue(String key) {
649        synchronized (sun.awt.UNIXToolkit.GTK_LOCK) {
650            return nativeGetClassValue(widgetType, key);
651        }
652    }
653
654    /**
655     * Convenience method to get a class specific integer value.
656     *
657     * @param context SynthContext identifying requestor
658     * @param key Key identifying class specific value
659     * @param defaultValue Returned if there is no value for the specified
660     *        type
661     * @return Value, or defaultValue if <code>key</code> is not defined
662     */
663    int getClassSpecificIntValue(SynthContext context, String key,
664                                 int defaultValue)
665    {
666        Object value = getClassSpecificValue(key);
667
668        if (value instanceof Number) {
669            return ((Number)value).intValue();
670        }
671        return defaultValue;
672    }
673
674    /**
675     * Convenience method to get a class specific Insets value.
676     *
677     * @param context SynthContext identifying requestor
678     * @param key Key identifying class specific value
679     * @param defaultValue Returned if there is no value for the specified
680     *        type
681     * @return Value, or defaultValue if <code>key</code> is not defined
682     */
683    Insets getClassSpecificInsetsValue(SynthContext context, String key,
684                                       Insets defaultValue)
685    {
686        Object value = getClassSpecificValue(key);
687
688        if (value instanceof Insets) {
689            return (Insets)value;
690        }
691        return defaultValue;
692    }
693
694    /**
695     * Convenience method to get a class specific Boolean value.
696     *
697     * @param context SynthContext identifying requestor
698     * @param key Key identifying class specific value
699     * @param defaultValue Returned if there is no value for the specified
700     *        type
701     * @return Value, or defaultValue if <code>key</code> is not defined
702     */
703    boolean getClassSpecificBoolValue(SynthContext context, String key,
704                                      boolean defaultValue)
705    {
706        Object value = getClassSpecificValue(key);
707
708        if (value instanceof Boolean) {
709            return ((Boolean)value).booleanValue();
710        }
711        return defaultValue;
712    }
713
714    /**
715     * Returns the value to initialize the opacity property of the Component
716     * to. A Style should NOT assume the opacity will remain this value, the
717     * developer may reset it or override it.
718     *
719     * @param context SynthContext identifying requestor
720     * @return opaque Whether or not the JComponent is opaque.
721     */
722    @Override
723    public boolean isOpaque(SynthContext context) {
724        Region region = context.getRegion();
725        if (region == Region.COMBO_BOX ||
726              region == Region.DESKTOP_PANE ||
727              region == Region.DESKTOP_ICON ||
728              region == Region.INTERNAL_FRAME ||
729              region == Region.LIST ||
730              region == Region.MENU_BAR ||
731              region == Region.PANEL ||
732              region == Region.POPUP_MENU ||
733              region == Region.PROGRESS_BAR ||
734              region == Region.ROOT_PANE ||
735              region == Region.SCROLL_PANE ||
736              region == Region.SPLIT_PANE_DIVIDER ||
737              region == Region.TABLE ||
738              region == Region.TEXT_AREA ||
739              region == Region.TOOL_BAR_DRAG_WINDOW ||
740              region == Region.TOOL_TIP ||
741              region == Region.TREE ||
742              region == Region.VIEWPORT) {
743            return true;
744        }
745        if (!GTKLookAndFeel.is3()) {
746            if (region == Region.EDITOR_PANE ||
747                  region == Region.FORMATTED_TEXT_FIELD ||
748                  region == Region.PASSWORD_FIELD ||
749                  region == Region.SPINNER ||
750                  region == Region.TEXT_FIELD ||
751                  region == Region.TEXT_PANE) {
752                return true;
753            }
754        }
755        Component c = context.getComponent();
756        String name = c.getName();
757        if (name == "ComboBox.renderer" || name == "ComboBox.listRenderer") {
758            return true;
759        }
760        return false;
761    }
762
763    @Override
764    public Object get(SynthContext context, Object key) {
765        // See if this is a class specific value.
766        String classKey = CLASS_SPECIFIC_MAP.get(key);
767        if (classKey != null) {
768            Object value = getClassSpecificValue(classKey);
769            if (value != null) {
770                return value;
771            }
772        }
773
774        // Is it a specific value ?
775        if (key == "ScrollPane.viewportBorderInsets") {
776            return getThicknessInsets(context, new Insets(0, 0, 0, 0));
777        }
778        else if (key == "Slider.tickColor") {
779            return getColorForState(context, ColorType.FOREGROUND);
780        }
781        else if (key == "ScrollBar.minimumThumbSize") {
782            int len =
783                getClassSpecificIntValue(context, "min-slider-length", 21);
784            JScrollBar sb = (JScrollBar)context.getComponent();
785            if (sb.getOrientation() == JScrollBar.HORIZONTAL) {
786                return new DimensionUIResource(len, 0);
787            } else {
788                return new DimensionUIResource(0, len);
789            }
790        }
791        else if (key == "Separator.thickness") {
792            JSeparator sep = (JSeparator)context.getComponent();
793            if (getClassSpecificBoolValue(context, "wide-separators", false)) {
794                if (sep.getOrientation() == JSeparator.HORIZONTAL) {
795                    return getClassSpecificIntValue(context,
796                            "separator-height", 0);
797                } else {
798                    return getClassSpecificIntValue(context,
799                            "separator-width", 0);
800                }
801            }
802            if (sep.getOrientation() == JSeparator.HORIZONTAL) {
803                return getYThickness();
804            } else {
805                return getXThickness();
806            }
807        }
808        else if (key == "ToolBar.separatorSize") {
809            if (getClassSpecificBoolValue(context, "wide-separators", false)) {
810                return new DimensionUIResource(
811                    getClassSpecificIntValue(context, "separator-width", 2),
812                    getClassSpecificIntValue(context, "separator-height", 2)
813                );
814            }
815            int size = getClassSpecificIntValue(WidgetType.TOOL_BAR,
816                                                "space-size", 12);
817            return new DimensionUIResource(size, size);
818        }
819        else if (key == "ScrollBar.buttonSize") {
820            JScrollBar sb = (JScrollBar)context.getComponent().getParent();
821            boolean horiz = (sb.getOrientation() == JScrollBar.HORIZONTAL);
822            WidgetType wt = horiz ?
823                WidgetType.HSCROLL_BAR : WidgetType.VSCROLL_BAR;
824            int sliderWidth = getClassSpecificIntValue(wt, "slider-width", 14);
825            int stepperSize = getClassSpecificIntValue(wt, "stepper-size", 14);
826            return horiz ?
827                new DimensionUIResource(stepperSize, sliderWidth) :
828                new DimensionUIResource(sliderWidth, stepperSize);
829        }
830        else if (key == "ArrowButton.size") {
831            String name = context.getComponent().getName();
832            if (name != null && name.startsWith("Spinner")) {
833                // Believe it or not, the size of a spinner arrow button is
834                // dependent upon the size of the spinner's font.  These
835                // calculations come from gtkspinbutton.c (version 2.8.20),
836                // spin_button_get_arrow_size() method.
837                String pangoFontName;
838                synchronized (sun.awt.UNIXToolkit.GTK_LOCK) {
839                    pangoFontName =
840                        nativeGetPangoFontName(WidgetType.SPINNER.ordinal());
841                }
842                int arrowSize = (pangoFontName != null) ?
843                    PangoFonts.getFontSize(pangoFontName) : 10;
844                return (arrowSize + (getXThickness() * 2));
845            }
846            // For all other kinds of arrow buttons (e.g. combobox arrow
847            // buttons), we will simply fall back on the value of
848            // ArrowButton.size as defined in the UIDefaults for
849            // GTKLookAndFeel when we call UIManager.get() below...
850        }
851        else if ("CheckBox.iconTextGap".equals(key) ||
852                 "RadioButton.iconTextGap".equals(key))
853        {
854            // The iconTextGap value needs to include "indicator-spacing"
855            // and it also needs to leave enough space for the focus line,
856            // which falls between the indicator icon and the text.
857            // See getRadioInsets() and 6489585 for more details.
858            int indicatorSpacing =
859                getClassSpecificIntValue(context, "indicator-spacing", 2);
860            int focusSize =
861                getClassSpecificIntValue(context, "focus-line-width", 1);
862            int focusPad =
863                getClassSpecificIntValue(context, "focus-padding", 1);
864            return indicatorSpacing + focusSize + focusPad;
865        } else if (GTKLookAndFeel.is3() && "ComboBox.forceOpaque".equals(key)) {
866            return true;
867        } else if ("Tree.expanderSize".equals(key)) {
868            Object value = getClassSpecificValue("expander-size");
869            if (value instanceof Integer) {
870                return (Integer)value + 4;
871            }
872            return null;
873        }
874
875        // Is it a stock icon ?
876        GTKStockIcon stockIcon = null;
877        synchronized (ICONS_MAP) {
878            stockIcon = ICONS_MAP.get(key);
879        }
880
881        if (stockIcon != null) {
882            return stockIcon;
883        }
884
885        // Is it another kind of value ?
886        if (key != "engine") {
887            // For backward compatibility we'll fallback to the UIManager.
888            // We don't go to the UIManager for engine as the engine is GTK
889            // specific.
890            Object value = UIManager.get(key);
891            if (key == "Table.rowHeight") {
892                int focusLineWidth = getClassSpecificIntValue(context,
893                        "focus-line-width", 0);
894                if (value == null && focusLineWidth > 0) {
895                    value = Integer.valueOf(16 + 2 * focusLineWidth);
896                }
897            }
898            return value;
899        }
900
901        // Don't call super, we don't want to pick up defaults from
902        // SynthStyle.
903        return null;
904    }
905
906    private Icon getStockIcon(SynthContext context, String key, int type) {
907        TextDirection direction = TextDirection.LTR;
908
909        if (context != null) {
910            ComponentOrientation co = context.getComponent().
911                                              getComponentOrientation();
912
913            if (co != null && !co.isLeftToRight()) {
914                direction = TextDirection.RTL;
915            }
916        }
917
918        // First try loading a theme-specific icon using the native
919        // GTK libraries (native GTK handles the resizing for us).
920        Icon icon = getStyleSpecificIcon(key, direction, type);
921        if (icon != null) {
922            return icon;
923        }
924
925        // In a failure case where native GTK (unexpectedly) returns a
926        // null icon, we can try loading a default icon as a fallback.
927        String propName = ICON_PROPERTY_PREFIX + key + '.' + type + '.' +
928                          (direction == TextDirection.RTL ? "rtl" : "ltr");
929        Image img = (Image)
930            Toolkit.getDefaultToolkit().getDesktopProperty(propName);
931        if (img != null) {
932            return new ImageIcon(img);
933        }
934
935        // In an extreme failure situation, just return null (callers are
936        // already prepared to handle a null icon, so the worst that can
937        // happen is that an icon won't be included in the button/dialog).
938        return null;
939    }
940
941    private Icon getStyleSpecificIcon(String key,
942                                      TextDirection direction, int type)
943    {
944        UNIXToolkit tk = (UNIXToolkit)Toolkit.getDefaultToolkit();
945        Image img =
946            tk.getStockIcon(widgetType, key, type, direction.ordinal(), null);
947        return (img != null) ? new ImageIcon(img) : null;
948    }
949
950    static class GTKStockIconInfo {
951        private static Map<String,Integer> ICON_TYPE_MAP;
952        private static final Object ICON_SIZE_KEY = new StringBuffer("IconSize");
953
954        private static Dimension[] getIconSizesMap() {
955            AppContext appContext = AppContext.getAppContext();
956            Dimension[] iconSizes = (Dimension[])appContext.get(ICON_SIZE_KEY);
957
958            if (iconSizes == null) {
959                iconSizes = new Dimension[7];
960                iconSizes[0] = null;                  // GTK_ICON_SIZE_INVALID
961                iconSizes[1] = new Dimension(16, 16); // GTK_ICON_SIZE_MENU
962                iconSizes[2] = new Dimension(18, 18); // GTK_ICON_SIZE_SMALL_TOOLBAR
963                iconSizes[3] = new Dimension(24, 24); // GTK_ICON_SIZE_LARGE_TOOLBAR
964                iconSizes[4] = new Dimension(20, 20); // GTK_ICON_SIZE_BUTTON
965                iconSizes[5] = new Dimension(32, 32); // GTK_ICON_SIZE_DND
966                iconSizes[6] = new Dimension(48, 48); // GTK_ICON_SIZE_DIALOG
967                appContext.put(ICON_SIZE_KEY, iconSizes);
968            }
969            return iconSizes;
970        }
971
972        /**
973         * Return the size of a particular icon type (logical size)
974         *
975         * @param type icon type (GtkIconSize value)
976         * @return a Dimension object, or null if lsize is invalid
977         */
978        public static Dimension getIconSize(int type) {
979            Dimension[] iconSizes = getIconSizesMap();
980            return type >= 0 && type < iconSizes.length ?
981                iconSizes[type] : null;
982        }
983
984        /**
985         * Change icon size in a type to size mapping. This is called by code
986         * that parses the gtk-icon-sizes setting
987         *
988         * @param type icon type (GtkIconSize value)
989         * @param w the new icon width
990         * @param h the new icon height
991         */
992        public static void setIconSize(int type, int w, int h) {
993            Dimension[] iconSizes = getIconSizesMap();
994            if (type >= 0 && type < iconSizes.length) {
995                iconSizes[type] = new Dimension(w, h);
996            }
997        }
998
999        /**
1000         * Return icon type (GtkIconSize value) given a symbolic name which can
1001         * occur in a theme file.
1002         *
1003         * @param size symbolic name, e.g. gtk-button
1004         * @return icon type. Valid types are 1 to 6
1005         */
1006        public static int getIconType(String size) {
1007            if (size == null) {
1008                return UNDEFINED;
1009            }
1010            if (ICON_TYPE_MAP == null) {
1011                initIconTypeMap();
1012            }
1013            Integer n = ICON_TYPE_MAP.get(size);
1014            return n != null ? n.intValue() : UNDEFINED;
1015        }
1016
1017        private static void initIconTypeMap() {
1018            ICON_TYPE_MAP = new HashMap<String,Integer>();
1019            ICON_TYPE_MAP.put("gtk-menu", Integer.valueOf(1));
1020            ICON_TYPE_MAP.put("gtk-small-toolbar", Integer.valueOf(2));
1021            ICON_TYPE_MAP.put("gtk-large-toolbar", Integer.valueOf(3));
1022            ICON_TYPE_MAP.put("gtk-button", Integer.valueOf(4));
1023            ICON_TYPE_MAP.put("gtk-dnd", Integer.valueOf(5));
1024            ICON_TYPE_MAP.put("gtk-dialog", Integer.valueOf(6));
1025        }
1026
1027    }
1028
1029    /**
1030     * An Icon that is fetched using getStockIcon.
1031     */
1032    private static class GTKStockIcon implements SynthIcon {
1033        private String key;
1034        private int size;
1035        private boolean loadedLTR;
1036        private boolean loadedRTL;
1037        private Icon ltrIcon;
1038        private Icon rtlIcon;
1039        private SynthStyle style;
1040
1041        GTKStockIcon(String key, int size) {
1042            this.key = key;
1043            this.size = size;
1044        }
1045
1046        public void paintIcon(SynthContext context, Graphics g, int x,
1047                              int y, int w, int h) {
1048            Icon icon = getIcon(context);
1049
1050            if (icon != null) {
1051                if (context == null) {
1052                    icon.paintIcon(null, g, x, y);
1053                }
1054                else {
1055                    icon.paintIcon(context.getComponent(), g, x, y);
1056                }
1057            }
1058        }
1059
1060        public int getIconWidth(SynthContext context) {
1061            Icon icon = getIcon(context);
1062
1063            if (icon != null) {
1064                return icon.getIconWidth();
1065            }
1066            return 0;
1067        }
1068
1069        public int getIconHeight(SynthContext context) {
1070            Icon icon = getIcon(context);
1071
1072            if (icon != null) {
1073                return icon.getIconHeight();
1074            }
1075            return 0;
1076        }
1077
1078        private Icon getIcon(SynthContext context) {
1079            if (context != null) {
1080                ComponentOrientation co = context.getComponent().
1081                                                  getComponentOrientation();
1082                SynthStyle style = context.getStyle();
1083
1084                if (style != this.style) {
1085                    this.style = style;
1086                    loadedLTR = loadedRTL = false;
1087                }
1088                if (co == null || co.isLeftToRight()) {
1089                    if (!loadedLTR) {
1090                        loadedLTR = true;
1091                        ltrIcon = ((GTKStyle)context.getStyle()).
1092                                  getStockIcon(context, key, size);
1093                    }
1094                    return ltrIcon;
1095                }
1096                else if (!loadedRTL) {
1097                    loadedRTL = true;
1098                    rtlIcon = ((GTKStyle)context.getStyle()).
1099                              getStockIcon(context, key,size);
1100                }
1101                return rtlIcon;
1102            }
1103            return ltrIcon;
1104        }
1105    }
1106
1107    /**
1108     * GTKLazyValue is a slimmed down version of <code>ProxyLaxyValue</code>.
1109     * The code is duplicate so that it can get at the package private
1110     * classes in gtk.
1111     */
1112    static class GTKLazyValue implements UIDefaults.LazyValue {
1113        /**
1114         * Name of the class to create.
1115         */
1116        private String className;
1117        private String methodName;
1118
1119        GTKLazyValue(String name) {
1120            this(name, null);
1121        }
1122
1123        GTKLazyValue(String name, String methodName) {
1124            this.className = name;
1125            this.methodName = methodName;
1126        }
1127
1128        @SuppressWarnings("deprecation")
1129        public Object createValue(UIDefaults table) {
1130            try {
1131                Class<?> c = Class.forName(className, true,Thread.currentThread().
1132                                           getContextClassLoader());
1133
1134                if (methodName == null) {
1135                    return c.newInstance();
1136                }
1137                Method m = c.getMethod(methodName, (Class<?>[])null);
1138
1139                return m.invoke(c, (Object[])null);
1140            } catch (ReflectiveOperationException e) {
1141            }
1142            return null;
1143        }
1144    }
1145
1146    static {
1147        CLASS_SPECIFIC_MAP = new HashMap<String,String>();
1148        CLASS_SPECIFIC_MAP.put("Slider.thumbHeight", "slider-width");
1149        CLASS_SPECIFIC_MAP.put("Slider.thumbWidth", "slider-length");
1150        CLASS_SPECIFIC_MAP.put("Slider.trackBorder", "trough-border");
1151        CLASS_SPECIFIC_MAP.put("SplitPane.size", "handle-size");
1152        CLASS_SPECIFIC_MAP.put("ScrollBar.thumbHeight", "slider-width");
1153        CLASS_SPECIFIC_MAP.put("ScrollBar.width", "slider-width");
1154        CLASS_SPECIFIC_MAP.put("TextArea.caretForeground", "cursor-color");
1155        CLASS_SPECIFIC_MAP.put("TextArea.caretAspectRatio", "cursor-aspect-ratio");
1156        CLASS_SPECIFIC_MAP.put("TextField.caretForeground", "cursor-color");
1157        CLASS_SPECIFIC_MAP.put("TextField.caretAspectRatio", "cursor-aspect-ratio");
1158        CLASS_SPECIFIC_MAP.put("PasswordField.caretForeground", "cursor-color");
1159        CLASS_SPECIFIC_MAP.put("PasswordField.caretAspectRatio", "cursor-aspect-ratio");
1160        CLASS_SPECIFIC_MAP.put("FormattedTextField.caretForeground", "cursor-color");
1161        CLASS_SPECIFIC_MAP.put("FormattedTextField.caretAspectRatio", "cursor-aspect-");
1162        CLASS_SPECIFIC_MAP.put("TextPane.caretForeground", "cursor-color");
1163        CLASS_SPECIFIC_MAP.put("TextPane.caretAspectRatio", "cursor-aspect-ratio");
1164        CLASS_SPECIFIC_MAP.put("EditorPane.caretForeground", "cursor-color");
1165        CLASS_SPECIFIC_MAP.put("EditorPane.caretAspectRatio", "cursor-aspect-ratio");
1166
1167        ICONS_MAP = new HashMap<String, GTKStockIcon>();
1168        ICONS_MAP.put("FileChooser.cancelIcon", new GTKStockIcon("gtk-cancel", 4));
1169        ICONS_MAP.put("FileChooser.okIcon",     new GTKStockIcon("gtk-ok",     4));
1170        ICONS_MAP.put("OptionPane.errorIcon", new GTKStockIcon("gtk-dialog-error", 6));
1171        ICONS_MAP.put("OptionPane.informationIcon", new GTKStockIcon("gtk-dialog-info", 6));
1172        ICONS_MAP.put("OptionPane.warningIcon", new GTKStockIcon("gtk-dialog-warning", 6));
1173        ICONS_MAP.put("OptionPane.questionIcon", new GTKStockIcon("gtk-dialog-question", 6));
1174        ICONS_MAP.put("OptionPane.yesIcon", new GTKStockIcon("gtk-yes", 4));
1175        ICONS_MAP.put("OptionPane.noIcon", new GTKStockIcon("gtk-no", 4));
1176        ICONS_MAP.put("OptionPane.cancelIcon", new GTKStockIcon("gtk-cancel", 4));
1177        ICONS_MAP.put("OptionPane.okIcon", new GTKStockIcon("gtk-ok", 4));
1178    }
1179}
1180