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