001/* Support for Mac OS X Specific key bindings.
002
003 Copyright (c) 2011-2018 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028
029package ptolemy.gui;
030
031import java.awt.Desktop;
032import java.lang.reflect.InvocationHandler;
033import java.lang.reflect.Method;
034import java.lang.reflect.Proxy;
035
036/**
037 * Support for Mac OS X specific features such as key bindings.
038 *
039 * There is no public constructor.  Instead, call stati set*Method() methods.
040 *
041 *  @author Christopher Brooks, Based on OSXAdapter.java, downloaded from: <a href="http://developer.apple.com/library/mac/#samplecode/OSXAdapter/Listings/src_OSXAdapter_java.html">http://developer.apple.com/library/mac/#samplecode/OSXAdapter/Listings/src_OSXAdapter_java.html</a> on July 26, 2011.
042 * @version $Id$
043 * @since Ptolemy II 10.0
044 * @Pt.ProposedRating Red (cxh)
045 * @Pt.AcceptedRating Red (cxh)
046 */
047public class MacOSXAdapter implements InvocationHandler {
048
049    ///////////////////////////////////////////////////////////////////
050    ////                         public methods                    ////
051
052    /** Invoke a method.  This method is part of the java.lang.reflect.InvocationHandler
053     *  interface.
054     *  @param proxy The object upon which the method is invoked.
055     *  @param method The method to be invoked.
056     *  @param args The arguments to the method, which must be non-null.
057     *  @exception Throwable If the method does not exist or is not accessible or if
058     *  thrown while invoking the method.
059     *  @return the result, which in this case is always null because
060     *  the com.apple.eawt.ApplicationListener methods are all void.
061     */
062    @Override
063    public Object invoke(Object proxy, Method method, Object[] args)
064            throws Throwable {
065        if (_topMethod == null || !_proxySignature.equals(method.getName())
066                || args.length != 1) {
067            return null;
068        }
069        boolean handled = true;
070        Object result = _topMethod.invoke(_top, (Object[]) null);
071        if (result != null) {
072            handled = Boolean.valueOf(result.toString()).booleanValue();
073        }
074
075        try {
076            Method setHandledMethod = args[0].getClass().getDeclaredMethod(
077                    "setHandled", new Class[] { boolean.class });
078            setHandledMethod.invoke(args[0],
079                    new Object[] { Boolean.valueOf(handled) });
080        } catch (Exception ex) {
081            _top.report("The Application event \"" + args[0]
082                    + "\" was not handled.", ex);
083        }
084        return null;
085    }
086
087    /** Set the about menu handler for a Top window.
088     *  Under Mac OS X, the About menu may be selected from the application
089     *  menu, which is in the upper left, next to the apple.
090     *  @param top the Top level window to perform the operation.
091     *  @param aboutMethod The method to invoke in Top, typically
092     *  {@link ptolemy.gui.Top#about()}.
093     */
094    public static void setAboutMethod(Top top, Method aboutMethod) {
095        _setHandler(top, new MacOSXAdapter("handleAbout", top, aboutMethod));
096        if (_desktop != null) {
097            // Running under Java 9 or later. Use the Desktop class.
098            // Use reflection here so that this compiles under Java 8.
099            try {
100                Class[] args = new Class[1];
101                args[0] = Class.forName("java.awt.desktop.AboutHandler");
102                Method setAboutHandler = _desktop.getClass().getMethod("setAboutHandler", args);
103                Object handler = Proxy.newProxyInstance(args[0].getClassLoader(), 
104                        args,
105                        new InvocationHandler() {
106                    @Override
107                    public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) throws Throwable {
108                        // AboutHandler has only one method, so we don't need to check anything here.
109                        return aboutMethod.invoke(top, (Object[]) null);
110                    }
111                });
112                setAboutHandler.invoke(_desktop, new Object[] { handler });
113            } catch (Exception e) {
114                System.err.println("Warning: Desktop class not working as expected: " + e);
115                System.err.println("About menu will not work properly. You can continue using the system.");
116            }
117            return;
118        }
119        if (_macOSXApplication == null) {
120            // _setHandler should set _macOSXApplication, so perhaps
121            // we are running as Applet or under -sandbox.
122            return;
123        }
124        try {
125            Method enableAboutMethod = _macOSXApplication.getClass()
126                    .getDeclaredMethod("setEnabledAboutMenu",
127                            new Class[] { boolean.class });
128            enableAboutMethod.invoke(_macOSXApplication,
129                    new Object[] { Boolean.TRUE });
130        } catch (SecurityException ex) {
131            if (!_printedSecurityExceptionMessage) {
132                _printedSecurityExceptionMessage = true;
133                System.out.println(
134                        "Warning: Failed to enable " + "the about menu. "
135                                + "(applets and -sandbox always causes this)");
136            }
137        } catch (NoSuchMethodException ex2) {
138            if (!_printedNoSuchMethodExceptionMessageAboutMenu) {
139                _printedNoSuchMethodExceptionMessageAboutMenu = true;
140                System.out.println(
141                        "Warning: Failed to get the setEnabledAboutMenu method.  "
142                                + "This is a known limitation of Java 9 and later.");
143            }
144        } catch (Exception ex3) {
145            top.report("The about menu could not be set.", ex3);
146        }
147    }
148
149    /** Set the quit handler (Command-q) for a Top window.
150     *  @param top the Top level window to perform the operation.
151     *  @param quitMethod The method to invoke in Top, typically
152     *  {@link ptolemy.gui.Top#exit()}.
153     */
154    public static void setQuitMethod(Top top, Method quitMethod) {
155        _setHandler(top, new MacOSXAdapter("handleQuit", top, quitMethod));
156    }
157
158    ///////////////////////////////////////////////////////////////////
159    ////                         private methods                   ////
160
161    /** Create an adapter that has the name of a method to be listened for,
162     *  the Top window that performs the task and the method in Top.
163     *  @param proxySignature  A method name from com.apple.eawt.ApplicationListener,
164     *  for example "handleQuit".
165     *  @param top the Top level window to perform the operation.
166     *  @param topMethod The Method in Top to be called.
167     */
168    private MacOSXAdapter(String proxySignature, Top top, Method topMethod) {
169        _proxySignature = proxySignature;
170        _top = top;
171        _topMethod = topMethod;
172    }
173
174    /** Create a proxy object from the adapter and add it as an ApplicationListener.
175     *  @param top the Top level window to perform the operation.
176     *  @param adapter The adapter.
177     */
178    private static void _setHandler(Top top, MacOSXAdapter adapter) {
179        // Sadly, Oracle broke backward compatibility with Java 9, so we have
180        // to use a different technique.
181        String version = System.getProperty("java.version");
182        int dot = version.indexOf(".");
183 
184        int majorVersion = (dot == -1) ? Integer.parseInt(version)
185                                       : Integer.parseInt(version.substring(0, dot));
186        
187        if (majorVersion >= 9) {
188            // Desktop class exists in Java 8, so this will compile.
189            // But the methods we need to not exist, so we use reflection for those.
190            if (Desktop.isDesktopSupported()) {
191                _desktop = Desktop.getDesktop();
192            } else {
193                if (!_printedMacInitializerWarning) {
194                    _printedMacInitializerWarning = true;
195                    System.err.println(
196                            "FIXME: Top.java: java.version is 9 or later, and Desktop is not"
197                            + " supported on this platform, so no Mac menus and key bindings yet.");
198                }
199            }
200            return;
201        }
202
203        try {
204            Class applicationClass = null;
205            String applicationClassName = "com.apple.eawt.Application";
206            try {
207                applicationClass = Class.forName(applicationClassName);
208            } catch (NoClassDefFoundError ex) {
209                if (!_printedNoClassDefFoundMessageApplication) {
210                    System.out.println("Warning: Failed to find the \""
211                            + applicationClassName + "\" class: " + ex
212                            + " (applets and -sandbox always causes this)");
213                    _printedNoClassDefFoundMessageApplication = true;
214                }
215                return;
216            } catch (ExceptionInInitializerError ex) {
217                if (ex.getCause() instanceof SecurityException) {
218                    if (!_printedSecurityExceptionMessage) {
219                        System.out.println("Warning: Failed to create new "
220                                + "instance of \"" + applicationClassName
221                                + "\": " + ex
222                                + "(applets and -sandbox always causes this)");
223                    }
224                    return;
225                }
226            }
227
228            if (applicationClass == null) {
229                throw new NullPointerException("Internal Error!  class "
230                        + applicationClassName + " was not found "
231                        + "and the exception was missed?");
232            } else {
233                if (_macOSXApplication == null) {
234                    try {
235                        _macOSXApplication = applicationClass
236                                .getConstructor((Class[]) null)
237                                .newInstance((Object[]) null);
238                    } catch (java.lang.reflect.InvocationTargetException ex) {
239                        if (ex.getCause() instanceof SecurityException) {
240                            if (!_printedSecurityExceptionMessage) {
241                                System.out.println("Warning: Failed to get the"
242                                        + "constructor of \""
243                                        + applicationClassName + "\" ("
244                                        + applicationClass + "): " + ex
245                                        + "(applets and -sandbox always causes this)");
246                                _printedSecurityExceptionMessage = true;
247                            }
248                        }
249                        return;
250                    } catch (java.lang.IllegalAccessException ex2) {
251                        if (!_printedIllegalAccessExceptionMessage) {
252                            System.out.println(
253                                    "Warning: Failed to access the Application class "
254                                            + applicationClassName + "\" ("
255                                            + applicationClass + "): " + ex2);
256                            _printedIllegalAccessExceptionMessage = true;
257                        }
258                        return;
259                    }
260                }
261                Class applicationListenerClass = Class
262                        .forName("com.apple.eawt.ApplicationListener");
263                Method addListenerMethod = applicationClass.getDeclaredMethod(
264                        "addApplicationListener",
265                        new Class[] { applicationListenerClass });
266
267                // Create a proxy object around this handler that can be
268                // reflectively added as an Apple ApplicationListener
269                Object osxAdapterProxy = Proxy.newProxyInstance(
270                        MacOSXAdapter.class.getClassLoader(),
271                        new Class[] { applicationListenerClass }, adapter);
272                addListenerMethod.invoke(_macOSXApplication,
273                        new Object[] { osxAdapterProxy });
274            }
275        } catch (ClassNotFoundException ex) {
276            if (!_printedNoClassDefFoundMessageApplicationListener) {
277                System.err.println(
278                        "Warning The com.apple.eawt.ApplicationListener class was not found.  "
279                                + "This is a known limitation of Java 9 and later.");
280                _printedNoClassDefFoundMessageApplicationListener = true;
281            }
282        } catch (Exception ex2) {
283            top.report(
284                    "There was a problem invoking the addApplicationListener method",
285                    ex2);
286        }
287    }
288    
289    /** An instance of java.awt.Desktop, upon which methods are invoked.
290     *  This variable is used only if the Java version is 9 or more.
291     *  Our usage is designed to compile with Java 8, using reflection
292     *  to avoid directly referencing methods that are not present in Java 8.
293     */
294    private static Desktop _desktop;
295
296    /** An instance of com.apple.eawt.Application, upon which methods are invoked.
297     *  We use Object here instead of com.apple.eawt.Application so as to avoid
298     *  compile-time dependencies on Apple-specific code.
299     *  The _setHandler() method sets macOSXApplication.
300     *  If we are running as an unsigned applet or using -sandbox, then
301     *  this variable will be null.
302     *  This variable is used only if the Java version is 8 or less.
303     */
304    private static Object _macOSXApplication;
305
306    /** True if we have printed the IllegalAccess message. */
307    private static boolean _printedIllegalAccessExceptionMessage = false;
308
309    /** True if we have printed the Mac initializer warning. */
310    private static boolean _printedMacInitializerWarning = false;
311
312    /** True if we have printed the NoClassDefFound message for com.apple.eawt.Application. */
313    private static boolean _printedNoClassDefFoundMessageApplication = false;
314
315    /** True if we have printed the NoClassDefFound message for com.apple.eawt.ApplicationListener. */
316    private static boolean _printedNoClassDefFoundMessageApplicationListener = false;
317    
318    /** True if we can't find the setEnabledAboutMenu method and have printed the message. */
319    private static boolean _printedNoSuchMethodExceptionMessageAboutMenu = false;
320
321    /** True if we have printed the securityException message. */
322    private static boolean _printedSecurityExceptionMessage = false;
323
324    /**  The name of a method in com.apple.eawt.ApplicationListener,
325     *  for example "handleQuit".
326     */
327    private String _proxySignature;
328
329    /** The Top level window to perform the operation.  This window is also
330     *  used to report errors.
331     */
332    private Top _top;
333
334    /** The Method in Top to be called. */
335    private Method _topMethod;
336}