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.MultiResolutionCachedImage;
  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(
  86                                 "getCreator", new Class<?>[] {});
  87                     getCreatorMethod.setAccessible(true);
  88                     return (Creator)getCreatorMethod.invoke(null, new Object[] {});
  89                 } catch (final Exception ignored) {
  90                     return null;
  91                 }
  92             }
  93         });
  94     }
  95 
  96     private static final RecyclableSingleton<Creator> cImageCreator = new RecyclableSingleton<Creator>() {
  97         @Override
  98         protected Creator getInstance() {
  99             return getCImageCreatorInternal();
 100         }
 101     };
 102     static Creator getCImageCreator() {
 103         return cImageCreator.get();
 104     }
 105 
 106     static Image generateSelectedDarkImage(final Image image) {
 107         final ImageProducer prod = new FilteredImageSource(image.getSource(), new IconImageFilter() {
 108             @Override
 109             int getGreyFor(final int gray) {
 110                 return gray * 75 / 100;
 111             }
 112         });
 113         return Toolkit.getDefaultToolkit().createImage(prod);
 114     }
 115 
 116     static Image generateDisabledImage(final Image image) {
 117         final ImageProducer prod = new FilteredImageSource(image.getSource(), new IconImageFilter() {
 118             @Override
 119             int getGreyFor(final int gray) {
 120                 return 255 - ((255 - gray) * 65 / 100);
 121             }
 122         });
 123         return Toolkit.getDefaultToolkit().createImage(prod);
 124     }
 125 
 126     static Image generateLightenedImage(final Image image, final int percent) {
 127         final GrayFilter filter = new GrayFilter(true, percent);
 128         return (image instanceof MultiResolutionCachedImage)
 129                 ? ((MultiResolutionCachedImage) image).map(
 130                         rv -> generateLightenedImage(rv, filter))
 131                 : generateLightenedImage(image, filter);
 132     }
 133 
 134     static Image generateLightenedImage(Image image, ImageFilter filter) {
 135         final ImageProducer prod = new FilteredImageSource(image.getSource(), filter);
 136         return Toolkit.getDefaultToolkit().createImage(prod);
 137     }
 138 
 139     private abstract static class IconImageFilter extends RGBImageFilter {
 140         IconImageFilter() {
 141             super();
 142             canFilterIndexColorModel = true;
 143         }
 144 
 145         @Override
 146         public final int filterRGB(final int x, final int y, final int rgb) {
 147             final int red = (rgb >> 16) & 0xff;
 148             final int green = (rgb >> 8) & 0xff;
 149             final int blue = rgb & 0xff;
 150             final int gray = getGreyFor((int)((0.30 * red + 0.59 * green + 0.11 * blue) / 3));
 151 
 152             return (rgb & 0xff000000) | (grayTransform(red, gray) << 16) | (grayTransform(green, gray) << 8) | (grayTransform(blue, gray) << 0);
 153         }
 154 
 155         private static int grayTransform(final int color, final int gray) {
 156             int result = color - gray;
 157             if (result < 0) result = 0;
 158             if (result > 255) result = 255;
 159             return result;
 160         }
 161 
 162         abstract int getGreyFor(int gray);
 163     }
 164 
 165     abstract static class RecyclableObject<T> {
 166         private SoftReference<T> objectRef;
 167 
 168         T get() {
 169             T referent;
 170             if (objectRef != null && (referent = objectRef.get()) != null) return referent;
 171             referent = create();
 172             objectRef = new SoftReference<T>(referent);
 173             return referent;
 174         }
 175 
 176         protected abstract T create();
 177     }
 178 
 179     abstract static class RecyclableSingleton<T> {
 180         final T get() {
 181             return AppContext.getSoftReferenceValue(this, () -> getInstance());
 182         }
 183 
 184         void reset() {
 185             AppContext.getAppContext().remove(this);
 186         }
 187 
 188         abstract T getInstance();
 189     }
 190 
 191     static class RecyclableSingletonFromDefaultConstructor<T> extends RecyclableSingleton<T> {
 192         private final Class<T> clazz;
 193 
 194         RecyclableSingletonFromDefaultConstructor(final Class<T> clazz) {
 195             this.clazz = clazz;
 196         }
 197 
 198         @Override
 199         T getInstance() {
 200             try {
 201                 ReflectUtil.checkPackageAccess(clazz);
 202                 return clazz.newInstance();
 203             } catch (InstantiationException | IllegalAccessException ignored) {
 204             }
 205             return null;
 206         }
 207     }
 208 
 209     abstract static class LazyKeyedSingleton<K, V> {
 210         private Map<K, V> refs;
 211 
 212         V get(final K key) {
 213             if (refs == null) refs = new HashMap<>();
 214 
 215             final V cachedValue = refs.get(key);
 216             if (cachedValue != null) return cachedValue;
 217 
 218             final V value = getInstance(key);
 219             refs.put(key, value);
 220             return value;
 221         }
 222 
 223         protected abstract V getInstance(K key);
 224     }
 225 
 226     private static final RecyclableSingleton<Boolean> enableAnimations = new RecyclableSingleton<Boolean>() {
 227         @Override
 228         protected Boolean getInstance() {
 229             final String sizeProperty = (String) AccessController.doPrivileged((PrivilegedAction<?>)new GetPropertyAction(
 230                     ANIMATIONS_PROPERTY));
 231             return !"false".equals(sizeProperty); // should be true by default
 232         }
 233     };
 234     private static boolean animationsEnabled() {
 235         return enableAnimations.get();
 236     }
 237 
 238     private static final int MENU_BLINK_DELAY = 50; // 50ms == 3/60 sec, according to the spec
 239     static void blinkMenu(final Selectable selectable) {
 240         if (!animationsEnabled()) return;
 241         try {
 242             selectable.paintSelected(false);
 243             Thread.sleep(MENU_BLINK_DELAY);
 244             selectable.paintSelected(true);
 245             Thread.sleep(MENU_BLINK_DELAY);
 246         } catch (final InterruptedException ignored) { }
 247     }
 248 
 249     interface Selectable {
 250         void paintSelected(boolean selected);
 251     }
 252 
 253     interface JComponentPainter {
 254         void paint(JComponent c, Graphics g, int x, int y, int w, int h);
 255     }
 256 
 257     interface Painter {
 258         void paint(Graphics g, int x, int y, int w, int h);
 259     }
 260 
 261     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) {
 262         g.setFont(font);
 263         g.setColor(shadowColor);
 264         SwingUtilities2.drawString(c, g, text, x + offsetX, y + offsetY + metrics.getAscent());
 265         g.setColor(textColor);
 266         SwingUtilities2.drawString(c, g, text, x, y + metrics.getAscent());
 267     }
 268 
 269     static class ShadowBorder implements Border {
 270         private final Painter prePainter;
 271         private final Painter postPainter;
 272 
 273         private final int offsetX;
 274         private final int offsetY;
 275         private final float distance;
 276         private final int blur;
 277         private final Insets insets;
 278         private final ConvolveOp blurOp;
 279 
 280         ShadowBorder(final Painter prePainter, final Painter postPainter, final int offsetX, final int offsetY, final float distance, final float intensity, final int blur) {
 281             this.prePainter = prePainter; this.postPainter = postPainter;
 282             this.offsetX = offsetX; this.offsetY = offsetY; this.distance = distance; this.blur = blur;
 283             final int halfBlur = blur / 2;
 284             insets = new Insets(halfBlur - offsetY, halfBlur - offsetX, halfBlur + offsetY, halfBlur + offsetX);
 285 
 286             final float blurry = intensity / (blur * blur);
 287             final float[] blurKernel = new float[blur * blur];
 288             for (int i = 0; i < blurKernel.length; i++) blurKernel[i] = blurry;
 289             blurOp = new ConvolveOp(new Kernel(blur, blur, blurKernel));
 290         }
 291 
 292         @Override
 293         public final boolean isBorderOpaque() {
 294             return false;
 295         }
 296 
 297         @Override
 298         public final Insets getBorderInsets(final Component c) {
 299             return insets;
 300         }
 301 
 302         @Override
 303         public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
 304             final BufferedImage img = new BufferedImage(width + blur * 2, height + blur * 2, BufferedImage.TYPE_INT_ARGB_PRE);
 305             paintToImage(img, x, y, width, height);
 306 //            debugFrame("border", img);
 307             g.drawImage(img, -blur, -blur, null);
 308         }
 309 
 310         private void paintToImage(final BufferedImage img, final int x, final int y, final int width, final int height) {
 311             // clear the prior image
 312             Graphics2D imgG = (Graphics2D)img.getGraphics();
 313             imgG.setComposite(AlphaComposite.Clear);
 314             imgG.setColor(Color.black);
 315             imgG.fillRect(0, 0, width + blur * 2, height + blur * 2);
 316 
 317             final int adjX = (int)(x + blur + offsetX + (insets.left * distance));
 318             final int adjY = (int)(y + blur + offsetY + (insets.top * distance));
 319             final int adjW = (int)(width - (insets.left + insets.right) * distance);
 320             final int adjH = (int)(height - (insets.top + insets.bottom) * distance);
 321 
 322             // let the delegate paint whatever they want to be blurred
 323             imgG.setComposite(AlphaComposite.DstAtop);
 324             if (prePainter != null) prePainter.paint(imgG, adjX, adjY, adjW, adjH);
 325             imgG.dispose();
 326 
 327             // blur the prior image back into the same pixels
 328             imgG = (Graphics2D)img.getGraphics();
 329             imgG.setComposite(AlphaComposite.DstAtop);
 330             imgG.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
 331             imgG.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
 332             imgG.drawImage(img, blurOp, 0, 0);
 333 
 334             if (postPainter != null) postPainter.paint(imgG, adjX, adjY, adjW, adjH);
 335             imgG.dispose();
 336         }
 337     }
 338 
 339     static class SlicedShadowBorder extends ShadowBorder {
 340         private final SlicedImageControl slices;
 341 
 342         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) {
 343             super(prePainter, postPainter, offsetX, offsetY, distance, intensity, blur);
 344 
 345             final BufferedImage i = new BufferedImage(templateWidth, templateHeight, BufferedImage.TYPE_INT_ARGB_PRE);
 346             super.paintBorder(null, i.getGraphics(), 0, 0, templateWidth, templateHeight);
 347 //            debugFrame("slices", i);
 348             slices = new SlicedImageControl(i, leftCut, topCut, rightCut, bottomCut, false);
 349         }
 350 
 351         @Override
 352         public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
 353             slices.paint(g, x, y, width, height);
 354         }
 355     }
 356 
 357 //    static void debugFrame(String name, Image image) {
 358 //        JFrame f = new JFrame(name);
 359 //        f.setContentPane(new JLabel(new ImageIcon(image)));
 360 //        f.pack();
 361 //        f.setVisible(true);
 362 //    }
 363 
 364     // special casing naughty applications, like InstallAnywhere
 365     // <rdar://problem/4851533> REGR: JButton: Myst IV: the buttons of 1.0.3 updater have redraw issue
 366     static boolean shouldUseOpaqueButtons() {
 367         final ClassLoader launcherClassLoader = Launcher.getLauncher().getClassLoader();
 368         if (classExists(launcherClassLoader, "com.installshield.wizard.platform.macosx.MacOSXUtils")) return true;
 369         return false;
 370     }
 371 
 372     private static boolean classExists(final ClassLoader classLoader, final String clazzName) {
 373         try {
 374             return Class.forName(clazzName, false, classLoader) != null;
 375         } catch (final Throwable ignored) { }
 376         return false;
 377     }
 378 
 379     private static final RecyclableSingleton<Method> getJComponentGetFlagMethod = new RecyclableSingleton<Method>() {
 380         @Override
 381         protected Method getInstance() {
 382             return AccessController.doPrivileged(
 383                 new PrivilegedAction<Method>() {
 384                     @Override
 385                     public Method run() {
 386                         try {
 387                             final Method method = JComponent.class.getDeclaredMethod(
 388                                     "getFlag", new Class<?>[] { int.class });
 389                             method.setAccessible(true);
 390                             return method;
 391                         } catch (final Throwable ignored) {
 392                             return null;
 393                         }
 394                     }
 395                 }
 396             );
 397         }
 398     };
 399 
 400     private static final Integer OPAQUE_SET_FLAG = 24; // private int JComponent.OPAQUE_SET
 401     static boolean hasOpaqueBeenExplicitlySet(final JComponent c) {
 402         final Method method = getJComponentGetFlagMethod.get();
 403         if (method == null) return false;
 404         try {
 405             return Boolean.TRUE.equals(method.invoke(c, OPAQUE_SET_FLAG));
 406         } catch (final Throwable ignored) {
 407             return false;
 408         }
 409     }
 410 
 411     private static boolean isWindowTextured(final Component c) {
 412         if (!(c instanceof JComponent)) {
 413             return false;
 414         }
 415         final JRootPane pane = ((JComponent) c).getRootPane();
 416         if (pane == null) {
 417             return false;
 418         }
 419         Object prop = pane.getClientProperty(
 420                 CPlatformWindow.WINDOW_BRUSH_METAL_LOOK);
 421         if (prop != null) {
 422             return Boolean.parseBoolean(prop.toString());
 423         }
 424         prop = pane.getClientProperty(CPlatformWindow.WINDOW_STYLE);
 425         return prop != null && "textured".equals(prop);
 426     }
 427 
 428     private static Color resetAlpha(final Color color) {
 429         return new Color(color.getRed(), color.getGreen(), color.getBlue(), 0);
 430     }
 431 
 432     static void fillRect(final Graphics g, final Component c) {
 433         fillRect(g, c, c.getBackground(), 0, 0, c.getWidth(), c.getHeight());
 434     }
 435 
 436     static void fillRect(final Graphics g, final Component c, final Color color,
 437                          final int x, final int y, final int w, final int h) {
 438         if (!(g instanceof Graphics2D)) {
 439             return;
 440         }
 441         final Graphics2D cg = (Graphics2D) g.create();
 442         try {
 443             if (color instanceof UIResource && isWindowTextured(c)
 444                     && color.equals(SystemColor.window)) {
 445                 cg.setComposite(AlphaComposite.Src);
 446                 cg.setColor(resetAlpha(color));
 447             } else {
 448                 cg.setColor(color);
 449             }
 450             cg.fillRect(x, y, w, h);
 451         } finally {
 452             cg.dispose();
 453         }
 454     }
 455 }
 456