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}