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