1 /*
   2  * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.apple.laf;
  27 
  28 import java.awt.*;
  29 import java.awt.image.*;
  30 import java.lang.ref.SoftReference;
  31 import java.lang.reflect.Method;
  32 import java.security.AccessController;
  33 import java.security.PrivilegedAction;
  34 import java.util.*;
  35 
  36 import javax.swing.*;
  37 import javax.swing.border.Border;
  38 import javax.swing.plaf.UIResource;
  39 
  40 import sun.awt.AppContext;
  41 
  42 import sun.lwawt.macosx.CImage;
  43 import sun.lwawt.macosx.CImage.Creator;
  44 import sun.lwawt.macosx.CPlatformWindow;
  45 import sun.misc.Launcher;
  46 import sun.reflect.misc.ReflectUtil;
  47 import sun.security.action.GetPropertyAction;
  48 import sun.swing.SwingUtilities2;
  49 
  50 import com.apple.laf.AquaImageFactory.SlicedImageControl;
  51 import sun.awt.image.MultiResolutionBufferedImage;
  52 
  53 final class AquaUtils {
  54 
  55     private static final String ANIMATIONS_PROPERTY = "swing.enableAnimations";
  56 
  57     /**
  58      * Suppresses default constructor, ensuring non-instantiability.
  59      */
  60     private AquaUtils() {
  61     }
  62 
  63     /**
  64      * Convenience function for determining ComponentOrientation.  Helps us
  65      * avoid having Munge directives throughout the code.
  66      */
  67     static boolean isLeftToRight(final Component c) {
  68         return c.getComponentOrientation().isLeftToRight();
  69     }
  70 
  71     static void enforceComponentOrientation(final Component c, final ComponentOrientation orientation) {
  72         c.setComponentOrientation(orientation);
  73         if (c instanceof Container) {
  74             for (final Component child : ((Container)c).getComponents()) {
  75                 enforceComponentOrientation(child, orientation);
  76             }
  77         }
  78     }
  79 
  80     private static Creator getCImageCreatorInternal() {
  81         return AccessController.doPrivileged(new PrivilegedAction<Creator>() {
  82             @Override
  83             public Creator run() {
  84                 try {
  85                     final Method getCreatorMethod = CImage.class.getDeclaredMethod("getCreator", new Class[] {});
  86                     getCreatorMethod.setAccessible(true);
  87                     return (Creator)getCreatorMethod.invoke(null, new Object[] {});
  88                 } catch (final Exception ignored) {
  89                     return null;
  90                 }
  91             }
  92         });
  93     }
  94 
  95     private static final RecyclableSingleton<Creator> cImageCreator = new RecyclableSingleton<Creator>() {
  96         @Override
  97         protected Creator getInstance() {
  98             return getCImageCreatorInternal();
  99         }
 100     };
 101     static Creator getCImageCreator() {
 102         return cImageCreator.get();
 103     }
 104 
 105     static Image generateSelectedDarkImage(final Image image) {
 106         final ImageProducer prod = new FilteredImageSource(image.getSource(), new IconImageFilter() {
 107             @Override
 108             int getGreyFor(final int gray) {
 109                 return gray * 75 / 100;
 110             }
 111         });
 112         return Toolkit.getDefaultToolkit().createImage(prod);
 113     }
 114 
 115     static Image generateDisabledImage(final Image image) {
 116         final ImageProducer prod = new FilteredImageSource(image.getSource(), new IconImageFilter() {
 117             @Override
 118             int getGreyFor(final int gray) {
 119                 return 255 - ((255 - gray) * 65 / 100);
 120             }
 121         });
 122         return Toolkit.getDefaultToolkit().createImage(prod);
 123     }
 124 
 125     static Image generateLightenedImage(final Image image, final int percent) {
 126         final GrayFilter filter = new GrayFilter(true, percent);
 127         return (image instanceof MultiResolutionBufferedImage)
 128                 ? ((MultiResolutionBufferedImage) image).map(
 129                         rv -> generateLightenedImage(rv, filter))
 130                 : generateLightenedImage(image, filter);
 131     }
 132 
 133     static Image generateLightenedImage(Image image, ImageFilter filter) {
 134         final ImageProducer prod = new FilteredImageSource(image.getSource(), filter);
 135         return Toolkit.getDefaultToolkit().createImage(prod);
 136     }
 137 
 138     private abstract static class IconImageFilter extends RGBImageFilter {
 139         IconImageFilter() {
 140             super();
 141             canFilterIndexColorModel = true;
 142         }
 143 
 144         @Override
 145         public final int filterRGB(final int x, final int y, final int rgb) {
 146             final int red = (rgb >> 16) & 0xff;
 147             final int green = (rgb >> 8) & 0xff;
 148             final int blue = rgb & 0xff;
 149             final int gray = getGreyFor((int)((0.30 * red + 0.59 * green + 0.11 * blue) / 3));
 150 
 151             return (rgb & 0xff000000) | (grayTransform(red, gray) << 16) | (grayTransform(green, gray) << 8) | (grayTransform(blue, gray) << 0);
 152         }
 153 
 154         private static int grayTransform(final int color, final int gray) {
 155             int result = color - gray;
 156             if (result < 0) result = 0;
 157             if (result > 255) result = 255;
 158             return result;
 159         }
 160 
 161         abstract int getGreyFor(int gray);
 162     }
 163 
 164     abstract static class RecyclableObject<T> {
 165         private SoftReference<T> objectRef;
 166 
 167         T get() {
 168             T referent;
 169             if (objectRef != null && (referent = objectRef.get()) != null) return referent;
 170             referent = create();
 171             objectRef = new SoftReference<T>(referent);
 172             return referent;
 173         }
 174 
 175         protected abstract T create();
 176     }
 177 
 178     abstract static class RecyclableSingleton<T> {
 179         final T get() {
 180             final AppContext appContext = AppContext.getAppContext();
 181             SoftReference<T> ref = (SoftReference<T>) appContext.get(this);
 182             if (ref != null) {
 183                 final T object = ref.get();
 184                 if (object != null) return object;
 185             }
 186             final T object = getInstance();
 187             ref = new SoftReference<T>(object);
 188             appContext.put(this, ref);
 189             return object;
 190         }
 191 
 192         void reset() {
 193             AppContext.getAppContext().remove(this);
 194         }
 195 
 196         abstract T getInstance();
 197     }
 198 
 199     static class RecyclableSingletonFromDefaultConstructor<T> extends RecyclableSingleton<T> {
 200         private final Class<T> clazz;
 201 
 202         RecyclableSingletonFromDefaultConstructor(final Class<T> clazz) {
 203             this.clazz = clazz;
 204         }
 205 
 206         @Override
 207         T getInstance() {
 208             try {
 209                 ReflectUtil.checkPackageAccess(clazz);
 210                 return clazz.newInstance();
 211             } catch (InstantiationException | IllegalAccessException ignored) {
 212             }
 213             return null;
 214         }
 215     }
 216 
 217     abstract static class LazyKeyedSingleton<K, V> {
 218         private Map<K, V> refs;
 219 
 220         V get(final K key) {
 221             if (refs == null) refs = new HashMap<>();
 222 
 223             final V cachedValue = refs.get(key);
 224             if (cachedValue != null) return cachedValue;
 225 
 226             final V value = getInstance(key);
 227             refs.put(key, value);
 228             return value;
 229         }
 230 
 231         protected abstract V getInstance(K key);
 232     }
 233 
 234     private static final RecyclableSingleton<Boolean> enableAnimations = new RecyclableSingleton<Boolean>() {
 235         @Override
 236         protected Boolean getInstance() {
 237             final String sizeProperty = (String) AccessController.doPrivileged((PrivilegedAction<?>)new GetPropertyAction(
 238                     ANIMATIONS_PROPERTY));
 239             return !"false".equals(sizeProperty); // should be true by default
 240         }
 241     };
 242     private static boolean animationsEnabled() {
 243         return enableAnimations.get();
 244     }
 245 
 246     private static final int MENU_BLINK_DELAY = 50; // 50ms == 3/60 sec, according to the spec
 247     static void blinkMenu(final Selectable selectable) {
 248         if (!animationsEnabled()) return;
 249         try {
 250             selectable.paintSelected(false);
 251             Thread.sleep(MENU_BLINK_DELAY);
 252             selectable.paintSelected(true);
 253             Thread.sleep(MENU_BLINK_DELAY);
 254         } catch (final InterruptedException ignored) { }
 255     }
 256 
 257     interface Selectable {
 258         void paintSelected(boolean selected);
 259     }
 260 
 261     interface JComponentPainter {
 262         void paint(JComponent c, Graphics g, int x, int y, int w, int h);
 263     }
 264 
 265     interface Painter {
 266         void paint(Graphics g, int x, int y, int w, int h);
 267     }
 268 
 269     static void paintDropShadowText(final Graphics g, final JComponent c, final Font font, final FontMetrics metrics, final int x, final int y, final int offsetX, final int offsetY, final Color textColor, final Color shadowColor, final String text) {
 270         g.setFont(font);
 271         g.setColor(shadowColor);
 272         SwingUtilities2.drawString(c, g, text, x + offsetX, y + offsetY + metrics.getAscent());
 273         g.setColor(textColor);
 274         SwingUtilities2.drawString(c, g, text, x, y + metrics.getAscent());
 275     }
 276 
 277     static class ShadowBorder implements Border {
 278         private final Painter prePainter;
 279         private final Painter postPainter;
 280 
 281         private final int offsetX;
 282         private final int offsetY;
 283         private final float distance;
 284         private final int blur;
 285         private final Insets insets;
 286         private final ConvolveOp blurOp;
 287 
 288         ShadowBorder(final Painter prePainter, final Painter postPainter, final int offsetX, final int offsetY, final float distance, final float intensity, final int blur) {
 289             this.prePainter = prePainter; this.postPainter = postPainter;
 290             this.offsetX = offsetX; this.offsetY = offsetY; this.distance = distance; this.blur = blur;
 291             final int halfBlur = blur / 2;
 292             insets = new Insets(halfBlur - offsetY, halfBlur - offsetX, halfBlur + offsetY, halfBlur + offsetX);
 293 
 294             final float blurry = intensity / (blur * blur);
 295             final float[] blurKernel = new float[blur * blur];
 296             for (int i = 0; i < blurKernel.length; i++) blurKernel[i] = blurry;
 297             blurOp = new ConvolveOp(new Kernel(blur, blur, blurKernel));
 298         }
 299 
 300         @Override
 301         public final boolean isBorderOpaque() {
 302             return false;
 303         }
 304 
 305         @Override
 306         public final Insets getBorderInsets(final Component c) {
 307             return insets;
 308         }
 309 
 310         @Override
 311         public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
 312             final BufferedImage img = new BufferedImage(width + blur * 2, height + blur * 2, BufferedImage.TYPE_INT_ARGB_PRE);
 313             paintToImage(img, x, y, width, height);
 314 //            debugFrame("border", img);
 315             g.drawImage(img, -blur, -blur, null);
 316         }
 317 
 318         private void paintToImage(final BufferedImage img, final int x, final int y, final int width, final int height) {
 319             // clear the prior image
 320             Graphics2D imgG = (Graphics2D)img.getGraphics();
 321             imgG.setComposite(AlphaComposite.Clear);
 322             imgG.setColor(Color.black);
 323             imgG.fillRect(0, 0, width + blur * 2, height + blur * 2);
 324 
 325             final int adjX = (int)(x + blur + offsetX + (insets.left * distance));
 326             final int adjY = (int)(y + blur + offsetY + (insets.top * distance));
 327             final int adjW = (int)(width - (insets.left + insets.right) * distance);
 328             final int adjH = (int)(height - (insets.top + insets.bottom) * distance);
 329 
 330             // let the delegate paint whatever they want to be blurred
 331             imgG.setComposite(AlphaComposite.DstAtop);
 332             if (prePainter != null) prePainter.paint(imgG, adjX, adjY, adjW, adjH);
 333             imgG.dispose();
 334 
 335             // blur the prior image back into the same pixels
 336             imgG = (Graphics2D)img.getGraphics();
 337             imgG.setComposite(AlphaComposite.DstAtop);
 338             imgG.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
 339             imgG.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
 340             imgG.drawImage(img, blurOp, 0, 0);
 341 
 342             if (postPainter != null) postPainter.paint(imgG, adjX, adjY, adjW, adjH);
 343             imgG.dispose();
 344         }
 345     }
 346 
 347     static class SlicedShadowBorder extends ShadowBorder {
 348         private final SlicedImageControl slices;
 349 
 350         SlicedShadowBorder(final Painter prePainter, final Painter postPainter, final int offsetX, final int offsetY, final float distance, final float intensity, final int blur, final int templateWidth, final int templateHeight, final int leftCut, final int topCut, final int rightCut, final int bottomCut) {
 351             super(prePainter, postPainter, offsetX, offsetY, distance, intensity, blur);
 352 
 353             final BufferedImage i = new BufferedImage(templateWidth, templateHeight, BufferedImage.TYPE_INT_ARGB_PRE);
 354             super.paintBorder(null, i.getGraphics(), 0, 0, templateWidth, templateHeight);
 355 //            debugFrame("slices", i);
 356             slices = new SlicedImageControl(i, leftCut, topCut, rightCut, bottomCut, false);
 357         }
 358 
 359         @Override
 360         public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
 361             slices.paint(g, x, y, width, height);
 362         }
 363     }
 364 
 365 //    static void debugFrame(String name, Image image) {
 366 //        JFrame f = new JFrame(name);
 367 //        f.setContentPane(new JLabel(new ImageIcon(image)));
 368 //        f.pack();
 369 //        f.setVisible(true);
 370 //    }
 371 
 372     // special casing naughty applications, like InstallAnywhere
 373     // <rdar://problem/4851533> REGR: JButton: Myst IV: the buttons of 1.0.3 updater have redraw issue
 374     static boolean shouldUseOpaqueButtons() {
 375         final ClassLoader launcherClassLoader = Launcher.getLauncher().getClassLoader();
 376         if (classExists(launcherClassLoader, "com.installshield.wizard.platform.macosx.MacOSXUtils")) return true;
 377         return false;
 378     }
 379 
 380     private static boolean classExists(final ClassLoader classLoader, final String clazzName) {
 381         try {
 382             return Class.forName(clazzName, false, classLoader) != null;
 383         } catch (final Throwable ignored) { }
 384         return false;
 385     }
 386 
 387     private static final RecyclableSingleton<Method> getJComponentGetFlagMethod = new RecyclableSingleton<Method>() {
 388         @Override
 389         protected Method getInstance() {
 390             return AccessController.doPrivileged(
 391                 new PrivilegedAction<Method>() {
 392                     @Override
 393                     public Method run() {
 394                         try {
 395                             final Method method = JComponent.class.getDeclaredMethod("getFlag", new Class[] { int.class });
 396                             method.setAccessible(true);
 397                             return method;
 398                         } catch (final Throwable ignored) {
 399                             return null;
 400                         }
 401                     }
 402                 }
 403             );
 404         }
 405     };
 406 
 407     private static final Integer OPAQUE_SET_FLAG = 24; // private int JComponent.OPAQUE_SET
 408     static boolean hasOpaqueBeenExplicitlySet(final JComponent c) {
 409         final Method method = getJComponentGetFlagMethod.get();
 410         if (method == null) return false;
 411         try {
 412             return Boolean.TRUE.equals(method.invoke(c, OPAQUE_SET_FLAG));
 413         } catch (final Throwable ignored) {
 414             return false;
 415         }
 416     }
 417 
 418     private static boolean isWindowTextured(final Component c) {
 419         if (!(c instanceof JComponent)) {
 420             return false;
 421         }
 422         final JRootPane pane = ((JComponent) c).getRootPane();
 423         if (pane == null) {
 424             return false;
 425         }
 426         Object prop = pane.getClientProperty(
 427                 CPlatformWindow.WINDOW_BRUSH_METAL_LOOK);
 428         if (prop != null) {
 429             return Boolean.parseBoolean(prop.toString());
 430         }
 431         prop = pane.getClientProperty(CPlatformWindow.WINDOW_STYLE);
 432         return prop != null && "textured".equals(prop);
 433     }
 434 
 435     private static Color resetAlpha(final Color color) {
 436         return new Color(color.getRed(), color.getGreen(), color.getBlue(), 0);
 437     }
 438 
 439     static void fillRect(final Graphics g, final Component c) {
 440         fillRect(g, c, c.getBackground(), 0, 0, c.getWidth(), c.getHeight());
 441     }
 442 
 443     static void fillRect(final Graphics g, final Component c, final Color color,
 444                          final int x, final int y, final int w, final int h) {
 445         if (!(g instanceof Graphics2D)) {
 446             return;
 447         }
 448         final Graphics2D cg = (Graphics2D) g.create();
 449         try {
 450             if (color instanceof UIResource && isWindowTextured(c)
 451                     && color.equals(SystemColor.window)) {
 452                 cg.setComposite(AlphaComposite.Src);
 453                 cg.setColor(resetAlpha(color));
 454             } else {
 455                 cg.setColor(color);
 456             }
 457             cg.fillRect(x, y, w, h);
 458         } finally {
 459             cg.dispose();
 460         }
 461     }
 462 }
 463