1 /*
   2  * Copyright (c) 2005, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.plaf.nimbus;
  26 
  27 import java.awt.BorderLayout;
  28 import static java.awt.BorderLayout.*;
  29 import javax.swing.JComponent;
  30 import javax.swing.UIDefaults;
  31 import javax.swing.UIManager;
  32 import javax.swing.plaf.synth.Region;
  33 import javax.swing.plaf.synth.SynthLookAndFeel;
  34 import javax.swing.plaf.synth.SynthStyle;
  35 import javax.swing.plaf.synth.SynthStyleFactory;
  36 import javax.swing.plaf.UIResource;
  37 import java.security.AccessController;
  38 import java.awt.Color;
  39 import java.awt.Container;
  40 import java.awt.Graphics2D;
  41 import java.awt.LayoutManager;
  42 import java.awt.image.BufferedImage;
  43 import java.beans.PropertyChangeEvent;
  44 import java.beans.PropertyChangeListener;
  45 import java.util.*;
  46 import javax.swing.GrayFilter;
  47 import javax.swing.Icon;
  48 import javax.swing.JToolBar;
  49 import javax.swing.border.TitledBorder;
  50 import javax.swing.plaf.BorderUIResource;
  51 import javax.swing.plaf.ColorUIResource;
  52 import sun.swing.ImageIconUIResource;
  53 import sun.swing.plaf.synth.SynthIcon;
  54 import sun.swing.plaf.GTKKeybindings;
  55 import sun.swing.plaf.WindowsKeybindings;
  56 import sun.security.action.GetPropertyAction;
  57 
  58 /**
  59  * <p>The NimbusLookAndFeel class.</p>
  60  *
  61  * @author Jasper Potts
  62  * @author Richard Bair
  63  */
  64 @SuppressWarnings("serial") // Superclass is not serializable across versions
  65 public class NimbusLookAndFeel extends SynthLookAndFeel {
  66 
  67     /** Set of standard region names for UIDefaults Keys */
  68     private static final String[] COMPONENT_KEYS = new String[]{"ArrowButton", "Button",
  69                     "CheckBox", "CheckBoxMenuItem", "ColorChooser", "ComboBox",
  70                     "DesktopPane", "DesktopIcon", "EditorPane", "FileChooser",
  71                     "FormattedTextField", "InternalFrame",
  72                     "InternalFrameTitlePane", "Label", "List", "Menu",
  73                     "MenuBar", "MenuItem", "OptionPane", "Panel",
  74                     "PasswordField", "PopupMenu", "PopupMenuSeparator",
  75                     "ProgressBar", "RadioButton", "RadioButtonMenuItem",
  76                     "RootPane", "ScrollBar", "ScrollBarTrack", "ScrollBarThumb",
  77                     "ScrollPane", "Separator", "Slider", "SliderTrack",
  78                     "SliderThumb", "Spinner", "SplitPane", "TabbedPane",
  79                     "Table", "TableHeader", "TextArea", "TextField", "TextPane",
  80                     "ToggleButton", "ToolBar", "ToolTip", "Tree", "Viewport"};
  81 
  82     /**
  83      * A reference to the auto-generated file NimbusDefaults. This file contains
  84      * the default mappings and values for the look and feel as specified in the
  85      * visual designer.
  86      */
  87     private NimbusDefaults defaults;
  88 
  89     /**
  90      * Reference to populated LAD uidefaults
  91      */
  92     private UIDefaults uiDefaults;
  93 
  94     private DefaultsListener defaultsListener = new DefaultsListener();
  95 
  96     /**
  97      * Create a new NimbusLookAndFeel.
  98      */
  99     public NimbusLookAndFeel() {
 100         super();
 101         defaults = new NimbusDefaults();
 102     }
 103 
 104     /** Called by UIManager when this look and feel is installed. */
 105     @Override public void initialize() {
 106         super.initialize();
 107         defaults.initialize();
 108         // create synth style factory
 109         setStyleFactory(new SynthStyleFactory() {
 110             @Override
 111             public SynthStyle getStyle(JComponent c, Region r) {
 112                 return defaults.getStyle(c, r);
 113             }
 114         });
 115     }
 116 
 117 
 118     /** Called by UIManager when this look and feel is uninstalled. */
 119     @Override public void uninitialize() {
 120         super.uninitialize();
 121         defaults.uninitialize();
 122         // clear all cached images to free memory
 123         ImageCache.getInstance().flush();
 124         UIManager.getDefaults().removePropertyChangeListener(defaultsListener);
 125     }
 126 
 127     /**
 128      * {@inheritDoc}
 129      */
 130     @Override public UIDefaults getDefaults() {
 131         if (uiDefaults == null){
 132             // Detect platform
 133             String osName = getSystemProperty("os.name");
 134             boolean isWindows = osName != null && osName.contains("Windows");
 135 
 136             // We need to call super for basic's properties file.
 137             uiDefaults = super.getDefaults();
 138             defaults.initializeDefaults(uiDefaults);
 139 
 140             // Install Keybindings
 141             if (isWindows) {
 142                 WindowsKeybindings.installKeybindings(uiDefaults);
 143             } else {
 144                 GTKKeybindings.installKeybindings(uiDefaults);
 145             }
 146 
 147             // Add Titled Border
 148             uiDefaults.put("TitledBorder.titlePosition",
 149                     TitledBorder.ABOVE_TOP);
 150             uiDefaults.put("TitledBorder.border", new BorderUIResource(
 151                     new LoweredBorder()));
 152             uiDefaults.put("TitledBorder.titleColor",
 153                     getDerivedColor("text",0.0f,0.0f,0.23f,0,true));
 154             uiDefaults.put("TitledBorder.font",
 155                     new NimbusDefaults.DerivedFont("defaultFont",
 156                             1f, true, null));
 157 
 158             // Choose Dialog button positions
 159             uiDefaults.put("OptionPane.isYesLast", !isWindows);
 160 
 161             // Store Table ScrollPane Corner Component
 162             uiDefaults.put("Table.scrollPaneCornerComponent",
 163                     new UIDefaults.ActiveValue() {
 164                         @Override
 165                         public Object createValue(UIDefaults table) {
 166                             return new TableScrollPaneCorner();
 167                         }
 168                     });
 169 
 170             // Setup the settings for ToolBarSeparator which is custom
 171             // installed for Nimbus
 172             uiDefaults.put("ToolBarSeparator[Enabled].backgroundPainter",
 173                     new ToolBarSeparatorPainter());
 174 
 175             // Populate UIDefaults with a standard set of properties
 176             for (String componentKey : COMPONENT_KEYS) {
 177                 String key = componentKey+".foreground";
 178                 if (!uiDefaults.containsKey(key)){
 179                     uiDefaults.put(key,
 180                             new NimbusProperty(componentKey,"textForeground"));
 181                 }
 182                 key = componentKey+".background";
 183                 if (!uiDefaults.containsKey(key)){
 184                     uiDefaults.put(key,
 185                             new NimbusProperty(componentKey,"background"));
 186                 }
 187                 key = componentKey+".font";
 188                 if (!uiDefaults.containsKey(key)){
 189                     uiDefaults.put(key,
 190                             new NimbusProperty(componentKey,"font"));
 191                 }
 192                 key = componentKey+".disabledText";
 193                 if (!uiDefaults.containsKey(key)){
 194                     uiDefaults.put(key,
 195                             new NimbusProperty(componentKey,"Disabled",
 196                                    "textForeground"));
 197                 }
 198                 key = componentKey+".disabled";
 199                 if (!uiDefaults.containsKey(key)){
 200                     uiDefaults.put(key,
 201                             new NimbusProperty(componentKey,"Disabled",
 202                                     "background"));
 203                 }
 204             }
 205 
 206             // FileView icon keys are used by some applications, we don't have
 207             // a computer icon at the moment so using home icon for now
 208             uiDefaults.put("FileView.computerIcon",
 209                     new LinkProperty("FileChooser.homeFolderIcon"));
 210             uiDefaults.put("FileView.directoryIcon",
 211                     new LinkProperty("FileChooser.directoryIcon"));
 212             uiDefaults.put("FileView.fileIcon",
 213                     new LinkProperty("FileChooser.fileIcon"));
 214             uiDefaults.put("FileView.floppyDriveIcon",
 215                     new LinkProperty("FileChooser.floppyDriveIcon"));
 216             uiDefaults.put("FileView.hardDriveIcon",
 217                     new LinkProperty("FileChooser.hardDriveIcon"));
 218         }
 219         return uiDefaults;
 220     }
 221 
 222     /**
 223      * Gets the style associated with the given component and region. This
 224      * will never return null. If an appropriate component and region cannot
 225      * be determined, then a default style is returned.
 226      *
 227      * @param c a non-null reference to a JComponent
 228      * @param r a non-null reference to the region of the component c
 229      * @return a non-null reference to a NimbusStyle.
 230      */
 231     public static NimbusStyle getStyle(JComponent c, Region r) {
 232         return (NimbusStyle)SynthLookAndFeel.getStyle(c, r);
 233     }
 234 
 235     /**
 236      * Return a short string that identifies this look and feel. This
 237      * String will be the unquoted String "Nimbus".
 238      *
 239      * @return a short string identifying this look and feel.
 240      */
 241     @Override public String getName() {
 242         return "Nimbus";
 243     }
 244 
 245     /**
 246      * Return a string that identifies this look and feel. This String will
 247      * be the unquoted String "Nimbus".
 248      *
 249      * @return a short string identifying this look and feel.
 250      */
 251     @Override public String getID() {
 252         return "Nimbus";
 253     }
 254 
 255     /**
 256      * Returns a textual description of this look and feel.
 257      *
 258      * @return textual description of this look and feel.
 259      */
 260     @Override public String getDescription() {
 261         return "Nimbus Look and Feel";
 262     }
 263 
 264     /**
 265      * {@inheritDoc}
 266      * @return {@code true}
 267      */
 268     @Override public boolean shouldUpdateStyleOnAncestorChanged() {
 269         return true;
 270     }
 271 
 272     /**
 273      * {@inheritDoc}
 274      *
 275      * <p>Overridden to return {@code true} when one of the following
 276      * properties change:
 277      * <ul>
 278      *   <li>{@code "Nimbus.Overrides"}
 279      *   <li>{@code "Nimbus.Overrides.InheritDefaults"}
 280      *   <li>{@code "JComponent.sizeVariant"}
 281      * </ul>
 282      *
 283      * @since 1.7
 284      */
 285     @Override
 286     protected boolean shouldUpdateStyleOnEvent(PropertyChangeEvent ev) {
 287         String eName = ev.getPropertyName();
 288 
 289         // These properties affect style cached inside NimbusDefaults (6860433)
 290         if ("name" == eName ||
 291             "ancestor" == eName ||
 292             "Nimbus.Overrides" == eName ||
 293             "Nimbus.Overrides.InheritDefaults" == eName ||
 294             "JComponent.sizeVariant" == eName) {
 295 
 296             JComponent c = (JComponent) ev.getSource();
 297             defaults.clearOverridesCache(c);
 298             return true;
 299         }
 300 
 301         return super.shouldUpdateStyleOnEvent(ev);
 302     }
 303 
 304     /**
 305      * <p>Registers a third party component with the NimbusLookAndFeel.</p>
 306      *
 307      * <p>Regions represent Components and areas within Components that act as
 308      * independent painting areas. Once registered with the NimbusLookAndFeel,
 309      * NimbusStyles for these Regions can be retrieved via the
 310      * <code>getStyle</code> method.</p>
 311      *
 312      * <p>The NimbusLookAndFeel uses a standard naming scheme for entries in the
 313      * UIDefaults table. The key for each property, state, painter, and other
 314      * default registered in UIDefaults for a specific Region will begin with
 315      * the specified <code>prefix</code></p>
 316      *
 317      * <p>For example, suppose I had a component named JFoo. Suppose I then registered
 318      * this component with the NimbusLookAndFeel in this manner:</p>
 319      *
 320      * <pre><code>
 321      *     laf.register(NimbusFooUI.FOO_REGION, "Foo");
 322      * </code></pre>
 323      *
 324      * <p>In this case, I could then register properties for this component with
 325      * UIDefaults in the following manner:</p>
 326      *
 327      * <pre><code>
 328      *     UIManager.put("Foo.background", new ColorUIResource(Color.BLACK));
 329      *     UIManager.put("Foo.Enabled.backgroundPainter", new FooBackgroundPainter());
 330      * </code></pre>
 331      *
 332      * <p>It is also possible to register a named component with Nimbus.
 333      * For example, suppose you wanted to style the background of a JPanel
 334      * named "MyPanel" differently from other JPanels. You could accomplish this
 335      * by doing the following:</p>
 336      *
 337      * <pre><code>
 338      *     laf.register(Region.PANEL, "\"MyPanel\"");
 339      *     UIManager.put("\"MyPanel\".background", new ColorUIResource(Color.RED));
 340      * </code></pre>
 341      *
 342      * @param region The Synth Region that is being registered. Such as Button, or
 343      *        ScrollBarThumb, or NimbusFooUI.FOO_REGION.
 344      * @param prefix The UIDefault prefix. For example, could be ComboBox, or if
 345      *        a named components, "MyComboBox", or even something like
 346      *        ToolBar."MyComboBox"."ComboBox.arrowButton"
 347      */
 348     public void register(Region region, String prefix) {
 349         defaults.register(region, prefix);
 350     }
 351 
 352     /**
 353      * Simple utility method that reads system keys.
 354      */
 355     private String getSystemProperty(String key) {
 356         return AccessController.doPrivileged(new GetPropertyAction(key));
 357     }
 358 
 359     @Override
 360     public Icon getDisabledIcon(JComponent component, Icon icon) {
 361         if (icon instanceof SynthIcon) {
 362             SynthIcon si = (SynthIcon)icon;
 363             BufferedImage img = EffectUtils.createCompatibleTranslucentImage(
 364                     si.getIconWidth(), si.getIconHeight());
 365             Graphics2D gfx = img.createGraphics();
 366             si.paintIcon(component, gfx, 0, 0);
 367             gfx.dispose();
 368             return new ImageIconUIResource(GrayFilter.createDisabledImage(img));
 369         } else {
 370             return super.getDisabledIcon(component, icon);
 371         }
 372     }
 373 
 374     /**
 375      * Get a derived color, derived colors are shared instances and is color
 376      * value will change when its parent UIDefault color changes.
 377      *
 378      * @param uiDefaultParentName The parent UIDefault key
 379      * @param hOffset             The hue offset
 380      * @param sOffset             The saturation offset
 381      * @param bOffset             The brightness offset
 382      * @param aOffset             The alpha offset
 383      * @param uiResource          True if the derived color should be a
 384      *                            UIResource, false if it should not be
 385      * @return The stored derived color
 386      */
 387     public Color getDerivedColor(String uiDefaultParentName,
 388                                  float hOffset, float sOffset,
 389                                  float bOffset, int aOffset,
 390                                  boolean uiResource) {
 391         return defaults.getDerivedColor(uiDefaultParentName, hOffset, sOffset,
 392                 bOffset, aOffset, uiResource);
 393     }
 394 
 395     /**
 396      * Decodes and returns a color, which is derived from an offset between two
 397      * other colors.
 398      *
 399      * @param color1   The first color
 400      * @param color2   The second color
 401      * @param midPoint The offset between color 1 and color 2, a value of 0.0 is
 402      *                 color 1 and 1.0 is color 2;
 403      * @param uiResource True if the derived color should be a UIResource
 404      * @return The derived color
 405      */
 406     protected final Color getDerivedColor(Color color1, Color color2,
 407                                       float midPoint, boolean uiResource) {
 408         int argb = deriveARGB(color1, color2, midPoint);
 409         if (uiResource) {
 410             return new ColorUIResource(argb);
 411         } else {
 412             return new Color(argb);
 413         }
 414     }
 415 
 416     /**
 417      * Decodes and returns a color, which is derived from a offset between two
 418      * other colors.
 419      *
 420      * @param color1   The first color
 421      * @param color2   The second color
 422      * @param midPoint The offset between color 1 and color 2, a value of 0.0 is
 423      *                 color 1 and 1.0 is color 2;
 424      * @return The derived color, which will be a UIResource
 425      */
 426     protected final Color getDerivedColor(Color color1, Color color2,
 427                                       float midPoint) {
 428         return getDerivedColor(color1, color2, midPoint, true);
 429     }
 430 
 431     /**
 432      * Package private method which returns either BorderLayout.NORTH,
 433      * BorderLayout.SOUTH, BorderLayout.EAST, or BorderLayout.WEST depending
 434      * on the location of the toolbar in its parent. The toolbar might be
 435      * in PAGE_START, PAGE_END, CENTER, or some other position, but will be
 436      * resolved to either NORTH,SOUTH,EAST, or WEST based on where the toolbar
 437      * actually IS, with CENTER being NORTH.
 438      *
 439      * This code is used to determine where the border line should be drawn
 440      * by the custom toolbar states, and also used by NimbusIcon to determine
 441      * whether the handle icon needs to be shifted to look correct.
 442      *
 443      * Toollbars are unfortunately odd in the way these things are handled,
 444      * and so this code exists to unify the logic related to toolbars so it can
 445      * be shared among the static files such as NimbusIcon and generated files
 446      * such as the ToolBar state classes.
 447      */
 448     static Object resolveToolbarConstraint(JToolBar toolbar) {
 449         //NOTE: we don't worry about component orientation or PAGE_END etc
 450         //because the BasicToolBarUI always uses an absolute position of
 451         //NORTH/SOUTH/EAST/WEST.
 452         if (toolbar != null) {
 453             Container parent = toolbar.getParent();
 454             if (parent != null) {
 455                 LayoutManager m = parent.getLayout();
 456                 if (m instanceof BorderLayout) {
 457                     BorderLayout b = (BorderLayout)m;
 458                     Object con = b.getConstraints(toolbar);
 459                     if (con == SOUTH || con == EAST || con == WEST) {
 460                         return con;
 461                     }
 462                     return NORTH;
 463                 }
 464             }
 465         }
 466         return NORTH;
 467     }
 468 
 469     /**
 470      * Derives the ARGB value for a color based on an offset between two
 471      * other colors.
 472      *
 473      * @param color1   The first color
 474      * @param color2   The second color
 475      * @param midPoint The offset between color 1 and color 2, a value of 0.0 is
 476      *                 color 1 and 1.0 is color 2;
 477      * @return the ARGB value for a new color based on this derivation
 478      */
 479     static int deriveARGB(Color color1, Color color2, float midPoint) {
 480         int r = color1.getRed() +
 481                 Math.round((color2.getRed() - color1.getRed()) * midPoint);
 482         int g = color1.getGreen() +
 483                 Math.round((color2.getGreen() - color1.getGreen()) * midPoint);
 484         int b = color1.getBlue() +
 485                 Math.round((color2.getBlue() - color1.getBlue()) * midPoint);
 486         int a = color1.getAlpha() +
 487                 Math.round((color2.getAlpha() - color1.getAlpha()) * midPoint);
 488         return ((a & 0xFF) << 24) |
 489                 ((r & 0xFF) << 16) |
 490                 ((g & 0xFF) << 8) |
 491                 (b & 0xFF);
 492     }
 493 
 494     /**
 495      * Simple Symbolic Link style UIDefalts Property
 496      */
 497     private class LinkProperty implements UIDefaults.ActiveValue, UIResource{
 498         private String dstPropName;
 499 
 500         private LinkProperty(String dstPropName) {
 501             this.dstPropName = dstPropName;
 502         }
 503 
 504         @Override
 505         public Object createValue(UIDefaults table) {
 506             return UIManager.get(dstPropName);
 507         }
 508     }
 509 
 510     /**
 511      * Nimbus Property that looks up Nimbus keys for standard key names. For
 512      * example "Button.background" --> "Button[Enabled].backgound"
 513      */
 514     private class NimbusProperty implements UIDefaults.ActiveValue, UIResource {
 515         private String prefix;
 516         private String state = null;
 517         private String suffix;
 518         private boolean isFont;
 519 
 520         private NimbusProperty(String prefix, String suffix) {
 521             this.prefix = prefix;
 522             this.suffix = suffix;
 523             isFont = "font".equals(suffix);
 524         }
 525 
 526         private NimbusProperty(String prefix, String state, String suffix) {
 527             this(prefix,suffix);
 528             this.state = state;
 529         }
 530 
 531         /**
 532          * Creates the value retrieved from the <code>UIDefaults</code> table.
 533          * The object is created each time it is accessed.
 534          *
 535          * @param table a <code>UIDefaults</code> table
 536          * @return the created <code>Object</code>
 537          */
 538         @Override
 539         public Object createValue(UIDefaults table) {
 540             Object obj = null;
 541             // check specified state
 542             if (state!=null){
 543                 obj = uiDefaults.get(prefix+"["+state+"]."+suffix);
 544             }
 545             // check enabled state
 546             if (obj==null){
 547                 obj = uiDefaults.get(prefix+"[Enabled]."+suffix);
 548             }
 549             // check for defaults
 550             if (obj==null){
 551                 if (isFont) {
 552                     obj = uiDefaults.get("defaultFont");
 553                 } else {
 554                     obj = uiDefaults.get(suffix);
 555                 }
 556             }
 557             return obj;
 558         }
 559     }
 560 
 561     private Map<String, Map<String, Object>> compiledDefaults = null;
 562     private boolean defaultListenerAdded = false;
 563 
 564     static String parsePrefix(String key) {
 565         if (key == null) {
 566             return null;
 567         }
 568         boolean inquotes = false;
 569         for (int i = 0; i < key.length(); i++) {
 570             char c = key.charAt(i);
 571             if (c == '"') {
 572                 inquotes = !inquotes;
 573             } else if ((c == '[' || c == '.') && !inquotes) {
 574                 return key.substring(0, i);
 575             }
 576         }
 577         return null;
 578     }
 579 
 580     Map<String, Object> getDefaultsForPrefix(String prefix) {
 581         if (compiledDefaults == null) {
 582             compiledDefaults = new HashMap<String, Map<String, Object>>();
 583             for (Map.Entry<Object, Object> entry: UIManager.getDefaults().entrySet()) {
 584                 if (entry.getKey() instanceof String) {
 585                     addDefault((String) entry.getKey(), entry.getValue());
 586                 }
 587             }
 588             if (! defaultListenerAdded) {
 589                 UIManager.getDefaults().addPropertyChangeListener(defaultsListener);
 590                 defaultListenerAdded = true;
 591             }
 592         }
 593         return compiledDefaults.get(prefix);
 594     }
 595 
 596     private void addDefault(String key, Object value) {
 597         if (compiledDefaults == null) {
 598             return;
 599         }
 600 
 601         String prefix = parsePrefix(key);
 602         if (prefix != null) {
 603             Map<String, Object> keys = compiledDefaults.get(prefix);
 604             if (keys == null) {
 605                 keys = new HashMap<String, Object>();
 606                 compiledDefaults.put(prefix, keys);
 607             }
 608             keys.put(key, value);
 609         }
 610     }
 611 
 612     private class DefaultsListener implements PropertyChangeListener {
 613         @Override public void propertyChange(PropertyChangeEvent ev) {
 614             String key = ev.getPropertyName();
 615             if ("UIDefaults".equals(key)) {
 616                 compiledDefaults = null;
 617             } else {
 618                 addDefault(key, ev.getNewValue());
 619             }
 620         }
 621     }
 622 }