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             return AppContext.getSoftReferenceValue(this, () -> getInstance());
 181         }
 182 
 183         void reset() {
 184             AppContext.getAppContext().remove(this);
 185         }
 186 
 187         abstract T getInstance();
 188     }
 189 
 190     static class RecyclableSingletonFromDefaultConstructor<T> extends RecyclableSingleton<T> {
 191         private final Class<T> clazz;
 192 
 193         RecyclableSingletonFromDefaultConstructor(final Class<T> clazz) {
 194             this.clazz = clazz;
 195         }
 196 
 197         @Override
 198         T getInstance() {
 199             try {
 200                 ReflectUtil.checkPackageAccess(clazz);
 201                 return clazz.newInstance();
 202             } catch (InstantiationException | IllegalAccessException ignored) {
 203             }
 204             return null;
 205         }
 206     }
 207 
 208     abstract static class LazyKeyedSingleton<K, V> {
 209         private Map<K, V> refs;
 210 
 211         V get(final K key) {
 212             if (refs == null) refs = new HashMap<>();
 213 
 214             final V cachedValue = refs.get(key);
 215             if (cachedValue != null) return cachedValue;
 216 
 217             final V value = getInstance(key);
 218             refs.put(key, value);
 219             return value;
 220         }
 221 
 222         protected abstract V getInstance(K key);
 223     }
 224 
 225     private static final RecyclableSingleton<Boolean> enableAnimations = new RecyclableSingleton<Boolean>() {
 226         @Override
 227         protected Boolean getInstance() {
 228             final String sizeProperty = (String) AccessController.doPrivileged((PrivilegedAction<?>)new GetPropertyAction(
 229                     ANIMATIONS_PROPERTY));
 230             return !"false".equals(sizeProperty); // should be true by default
 231         }
 232     };
 233     private static boolean animationsEnabled() {
 234         return enableAnimations.get();
 235     }
 236 
 237     private static final int MENU_BLINK_DELAY = 50; // 50ms == 3/60 sec, according to the spec
 238     static void blinkMenu(final Selectable selectable) {
 239         if (!animationsEnabled()) return;
 240         try {
 241             selectable.paintSelected(false);
 242             Thread.sleep(MENU_BLINK_DELAY);
 243             selectable.paintSelected(true);
 244             Thread.sleep(MENU_BLINK_DELAY);
 245         } catch (final InterruptedException ignored) { }
 246     }
 247 
 248     interface Selectable {
 249         void paintSelected(boolean selected);
 250     }
 251 
 252     interface JComponentPainter {
 253         void paint(JComponent c, Graphics g, int x, int y, int w, int h);
 254     }
 255 
 256     interface Painter {
 257         void paint(Graphics g, int x, int y, int w, int h);
 258     }
 259 
 260     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) {
 261         g.setFont(font);
 262         g.setColor(shadowColor);
 263         SwingUtilities2.drawString(c, g, text, x + offsetX, y + offsetY + metrics.getAscent());
 264         g.setColor(textColor);
 265         SwingUtilities2.drawString(c, g, text, x, y + metrics.getAscent());
 266     }
 267 
 268     static class ShadowBorder implements Border {
 269         private final Painter prePainter;
 270         private final Painter postPainter;
 271 
 272         private final int offsetX;
 273         private final int offsetY;
 274         private final float distance;
 275         private final int blur;
 276         private final Insets insets;
 277         private final ConvolveOp blurOp;
 278 
 279         ShadowBorder(final Painter prePainter, final Painter postPainter, final int offsetX, final int offsetY, final float distance, final float intensity, final int blur) {
 280             this.prePainter = prePainter; this.postPainter = postPainter;
 281             this.offsetX = offsetX; this.offsetY = offsetY; this.distance = distance; this.blur = blur;
 282             final int halfBlur = blur / 2;
 283             insets = new Insets(halfBlur - offsetY, halfBlur - offsetX, halfBlur + offsetY, halfBlur + offsetX);
 284 
 285             final float blurry = intensity / (blur * blur);
 286             final float[] blurKernel = new float[blur * blur];
 287             for (int i = 0; i < blurKernel.length; i++) blurKernel[i] = blurry;
 288             blurOp = new ConvolveOp(new Kernel(blur, blur, blurKernel));
 289         }
 290 
 291         @Override
 292         public final boolean isBorderOpaque() {
 293             return false;
 294         }
 295 
 296         @Override
 297         public final Insets getBorderInsets(final Component c) {
 298             return insets;
 299         }
 300 
 301         @Override
 302         public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
 303             final BufferedImage img = new BufferedImage(width + blur * 2, height + blur * 2, BufferedImage.TYPE_INT_ARGB_PRE);
 304             paintToImage(img, x, y, width, height);
 305 //            debugFrame("border", img);
 306             g.drawImage(img, -blur, -blur, null);
 307         }
 308 
 309         private void paintToImage(final BufferedImage img, final int x, final int y, final int width, final int height) {
 310             // clear the prior image
 311             Graphics2D imgG = (Graphics2D)img.getGraphics();
 312             imgG.setComposite(AlphaComposite.Clear);
 313             imgG.setColor(Color.black);
 314             imgG.fillRect(0, 0, width + blur * 2, height + blur * 2);
 315 
 316             final int adjX = (int)(x + blur + offsetX + (insets.left * distance));
 317             final int adjY = (int)(y + blur + offsetY + (insets.top * distance));
 318             final int adjW = (int)(width - (insets.left + insets.right) * distance);
 319             final int adjH = (int)(height - (insets.top + insets.bottom) * distance);
 320 
 321             // let the delegate paint whatever they want to be blurred
 322             imgG.setComposite(AlphaComposite.DstAtop);
 323             if (prePainter != null) prePainter.paint(imgG, adjX, adjY, adjW, adjH);
 324             imgG.dispose();
 325 
 326             // blur the prior image back into the same pixels
 327             imgG = (Graphics2D)img.getGraphics();
 328             imgG.setComposite(AlphaComposite.DstAtop);
 329             imgG.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
 330             imgG.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
 331             imgG.drawImage(img, blurOp, 0, 0);
 332 
 333             if (postPainter != null) postPainter.paint(imgG, adjX, adjY, adjW, adjH);
 334             imgG.dispose();
 335         }
 336     }
 337 
 338     static class SlicedShadowBorder extends ShadowBorder {
 339         private final SlicedImageControl slices;
 340 
 341         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) {
 342             super(prePainter, postPainter, offsetX, offsetY, distance, intensity, blur);
 343 
 344             final BufferedImage i = new BufferedImage(templateWidth, templateHeight, BufferedImage.TYPE_INT_ARGB_PRE);
 345             super.paintBorder(null, i.getGraphics(), 0, 0, templateWidth, templateHeight);
 346 //            debugFrame("slices", i);
 347             slices = new SlicedImageControl(i, leftCut, topCut, rightCut, bottomCut, false);
 348         }
 349 
 350         @Override
 351         public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
 352             slices.paint(g, x, y, width, height);
 353         }
 354     }
 355 
 356 //    static void debugFrame(String name, Image image) {
 357 //        JFrame f = new JFrame(name);
 358 //        f.setContentPane(new JLabel(new ImageIcon(image)));
 359 //        f.pack();
 360 //        f.setVisible(true);
 361 //    }
 362 
 363     // special casing naughty applications, like InstallAnywhere
 364     // <rdar://problem/4851533> REGR: JButton: Myst IV: the buttons of 1.0.3 updater have redraw issue
 365     static boolean shouldUseOpaqueButtons() {
 366         final ClassLoader launcherClassLoader = Launcher.getLauncher().getClassLoader();
 367         if (classExists(launcherClassLoader, "com.installshield.wizard.platform.macosx.MacOSXUtils")) return true;
 368         return false;
 369     }
 370 
 371     private static boolean classExists(final ClassLoader classLoader, final String clazzName) {
 372         try {
 373             return Class.forName(clazzName, false, classLoader) != null;
 374         } catch (final Throwable ignored) { }
 375         return false;
 376     }
 377 
 378     private static final RecyclableSingleton<Method> getJComponentGetFlagMethod = new RecyclableSingleton<Method>() {
 379         @Override
 380         protected Method getInstance() {
 381             return AccessController.doPrivileged(
 382                 new PrivilegedAction<Method>() {
 383                     @Override
 384                     public Method run() {
 385                         try {
 386                             final Method method = JComponent.class.getDeclaredMethod("getFlag", new Class[] { int.class });
 387                             method.setAccessible(true);
 388                             return method;
 389                         } catch (final Throwable ignored) {
 390                             return null;
 391                         }
 392                     }
 393                 }
 394             );
 395         }
 396     };
 397 
 398     private static final Integer OPAQUE_SET_FLAG = 24; // private int JComponent.OPAQUE_SET
 399     static boolean hasOpaqueBeenExplicitlySet(final JComponent c) {
 400         final Method method = getJComponentGetFlagMethod.get();
 401         if (method == null) return false;
 402         try {
 403             return Boolean.TRUE.equals(method.invoke(c, OPAQUE_SET_FLAG));
 404         } catch (final Throwable ignored) {
 405             return false;
 406         }
 407     }
 408 
 409     private static boolean isWindowTextured(final Component c) {
 410         if (!(c instanceof JComponent)) {
 411             return false;
 412         }
 413         final JRootPane pane = ((JComponent) c).getRootPane();
 414         if (pane == null) {
 415             return false;
 416         }
 417         Object prop = pane.getClientProperty(
 418                 CPlatformWindow.WINDOW_BRUSH_METAL_LOOK);
 419         if (prop != null) {
 420             return Boolean.parseBoolean(prop.toString());
 421         }
 422         prop = pane.getClientProperty(CPlatformWindow.WINDOW_STYLE);
 423         return prop != null && "textured".equals(prop);
 424     }
 425 
 426     private static Color resetAlpha(final Color color) {
 427         return new Color(color.getRed(), color.getGreen(), color.getBlue(), 0);
 428     }
 429 
 430     static void fillRect(final Graphics g, final Component c) {
 431         fillRect(g, c, c.getBackground(), 0, 0, c.getWidth(), c.getHeight());
 432     }
 433 
 434     static void fillRect(final Graphics g, final Component c, final Color color,
 435                          final int x, final int y, final int w, final int h) {
 436         if (!(g instanceof Graphics2D)) {
 437             return;
 438         }
 439         final Graphics2D cg = (Graphics2D) g.create();
 440         try {
 441             if (color instanceof UIResource && isWindowTextured(c)
 442                     && color.equals(SystemColor.window)) {
 443                 cg.setComposite(AlphaComposite.Src);
 444                 cg.setColor(resetAlpha(color));
 445             } else {
 446                 cg.setColor(color);
 447             }
 448             cg.fillRect(x, y, w, h);
 449         } finally {
 450             cg.dispose();
 451         }
 452     }
 453 }
 454