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