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