Skip to content

Instantly share code, notes, and snippets.

@ctipper
Last active October 17, 2018 14:33
Show Gist options
  • Save ctipper/678268819df75b366b34e78ff413bb3a to your computer and use it in GitHub Desktop.
Save ctipper/678268819df75b366b34e78ff413bb3a to your computer and use it in GitHub Desktop.
OSX app menu compatibility shim for JDK 9+ releases
Abstract:
Hooks existing preferences/about/quit functionality from an existing Java app into handlers for the Mac OS X application menu.
It is tailored to provide compatibility with the Java 10 releases so that an app may be built on other platforms.
This work was inspired by an OSX compatibility layer provided by Apple that used a different API. This is an otherwise original contribution and is a reimplementation using newer interface methods.
Usage:
java --add-opens=java.desktop/com.apple.eawt=ALL-UNNAMED --add-opens=java.desktop/sun.awt=ALL-UNNAMED ...
// Check that we are on Mac OS X. This is crucial to loading and using the OSXHandler class.
public static boolean MAC_OS_X = (System.getProperty("os.name").toLowerCase().startsWith("mac os x"));
/**
* Generic registration with the Mac OS X application menu Checks the platform, then attempts to
* register with the Apple EAWT
*/
public void registerForMacOSXEvents() {
if (MAC_OS_X) {
try {
// JDK 9 compatible handlers
OSXHandler.setAboutHandler(this, getClass().getDeclaredMethod("about", new Class[]{EventObject.class}));
OSXHandler.setQuitHandler(this, getClass().getDeclaredMethod("quit", new Class[]{EventObject.class, Object.class}));
OSXHandler.setPreferencesHandler(this, getClass().getDeclaredMethod("preferences", new Class[]{EventObject.class}));
OSXHandler.setFileHandler(this, getClass().getDeclaredMethod("openDocument", new Class[]{java.util.ArrayList.class}));
} catch (NoSuchMethodException | SecurityException ex) {
logger.log(Level.SEVERE, "Error while loading the OSXAdapter: {0}", ex.toString());
}
}
}
/**
* General info dialog; fed to the OSXHandler as the method to call when "About OSXHandler" is
* selected from the application menu
*/
public void about(EventObject event) {
about();
}
/**
* General quit handler; fed to the OSXHandler as the method to call when a system quit event
* occurs. A quit event is triggered by Cmd-Q, selecting Quit from the application or Dock menu,
* or logging out
*/
public boolean quit(EventObject event, Object response) {
try {
Method method = response.getClass().getDeclaredMethod("performQuit", (Class[]) null);
Object r = method.invoke(response);
} catch (IllegalAccessException | IllegalArgumentException |
InvocationTargetException | NoSuchMethodException | SecurityException ex) {
logger.severe(ex.getMessage());
}
return true;
}
/**
* Preferences dialog; fed to the OSXHandler as the method to call when "Preferences" is
* selected from the application menu
*/
public void preferences(EventObject event) {
preferences();
}
/**
* Method to handle document events from the Finder registered by OSXHandler
*/
public void openDocument(java.util.ArrayList<?> files) {
if (files.get(0) instanceof File) {
final File file = (File) files.get(0);
readDocument(file);
}
}
/**
* OSXHandler.java
*
* Created on 06-Jul-2016 10:42:57
*
* Abstract: Hooks existing preferences/about/quit functionality from an existing Java app into
* handlers for the Mac OS X application menu.
*
* Uses a Proxy object to dynamically implement the java.awt.desktop.xxxHandler interfaces and register
* it with the java.awt.Desktop object. This allows the complete project to be both built
* and run on any platform without any stubs or placeholders. Useful for developers looking to
* implement Mac OS X features while supporting multiple platforms with minimal impact.
*
* Version: 1.0
*
* Updated 22-Jul-2018 13:16
*/
package net.perspective.draw;
/**
* The MIT License (MIT)
*
* Copyright (c) 2016 Christopher Tipper
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import java.lang.reflect.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author ctipper
*/
public class OSXHandler implements InvocationHandler {
protected Object targetObject;
protected Method targetMethod;
protected String proxySignature;
static Object macOSXApplication;
private static final Logger logger = Logger.getLogger(OSXHandler.class.getName());
/**
* Each OSXHandler has the name of the EAWT method it intends to listen for (handleAbout, for
* example), the Object that will ultimately perform the task, and the Method to be called on
* that Object
*/
protected OSXHandler(String proxySignature, Object target, Method handler) {
this.proxySignature = proxySignature;
this.targetObject = target;
this.targetMethod = handler;
}
/**
* Pass this method an Object and Method equipped to perform application shutdown logic The
* QuitResponse may be used to respond to a request to quit the application.
*/
@SuppressWarnings("unchecked")
public static void setQuitHandler(Object target, Method quitHandler) {
OSXHandler adapter = new OSXHandler("handleQuitRequestWith", target, quitHandler) {
public boolean callTarget(Object appleEvent, Object response) {
if (appleEvent != null) {
try {
this.targetMethod.invoke(this.targetObject, new Object[] { appleEvent, response });
} catch (IllegalAccessException | IllegalArgumentException | SecurityException |
InvocationTargetException ex) {
logger.severe("Mac OS X Adapter could not talk to EAWT:");
}
}
return true;
}
};
try {
Class<?> applicationClass = Class.forName("java.awt.Desktop");
if (macOSXApplication == null) {
Method method = applicationClass.getMethod("getDesktop", (Class<?>[]) null);
macOSXApplication = method.invoke(null);
}
Class<?> quitHandlerClass = Class.forName("java.awt.desktop.QuitHandler");
Method addHandlerMethod = applicationClass.getDeclaredMethod("setQuitHandler", new Class<?>[] { quitHandlerClass });
// Create a proxy object around this handler that can be reflectively added as an Apple AppEvent handler
Object osxAdapterProxy = Proxy.newProxyInstance(OSXHandler.class.getClassLoader(), new Class<?>[] { quitHandlerClass }, adapter);
addHandlerMethod.invoke(macOSXApplication, new Object[] { osxAdapterProxy });
} catch (ClassNotFoundException cnfe) {
logger.log(Level.SEVERE, "This version of Mac OS X does not support the Apple EAWT. ApplicationEvent handling has been disabled ({0})", cnfe.getMessage());
cnfe.printStackTrace();
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
// Likely a NoSuchMethodException or an IllegalAccessException loading/invoking eawt.Application methods
logger.severe("Mac OS X Adapter could not talk to EAWT:");
logger.severe(ex.toString());
}
}
/**
* Pass this method an Object and Method equipped to display application info. They will be
* called when the About menu item is selected from the application menu
*/
@SuppressWarnings("unchecked")
public static void setAboutHandler(Object target, Method aboutHandler) {
OSXHandler adapter = new OSXHandler("handleAbout", target, aboutHandler) {
public boolean callTarget(Object appleEvent) {
if (appleEvent != null) {
try {
this.targetMethod.invoke(this.targetObject, new Object[] { appleEvent });
} catch (IllegalAccessException | IllegalArgumentException | SecurityException |
InvocationTargetException ex) {
logger.severe("Mac OS X Adapter could not talk to EAWT:");
}
}
return true;
}
};
try {
Class<?> applicationClass = Class.forName("java.awt.Desktop");
if (macOSXApplication == null) {
Method method = applicationClass.getMethod("getDesktop", (Class<?>[])null);
macOSXApplication = method.invoke(null);
}
Class<?> aboutHandlerClass = Class.forName("java.awt.desktop.AboutHandler");
Method addHandlerMethod = applicationClass.getDeclaredMethod("setAboutHandler", new Class<?>[] { aboutHandlerClass });
// Create a proxy object around this handler that can be reflectively added as an Apple AppEvent handler
Object osxAdapterProxy = Proxy.newProxyInstance(OSXHandler.class.getClassLoader(), new Class<?>[] { aboutHandlerClass }, adapter);
addHandlerMethod.invoke(macOSXApplication, new Object[] { osxAdapterProxy });
} catch (ClassNotFoundException cnfe) {
logger.log(Level.SEVERE, "This version of Mac OS X does not support the Apple EAWT. ApplicationEvent handling has been disabled ({0})", cnfe.getMessage());
cnfe.printStackTrace();
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
// Likely a NoSuchMethodException or an IllegalAccessException loading/invoking eawt.Application methods
logger.severe("Mac OS X Adapter could not talk to EAWT:");
logger.severe(ex.toString());
}
}
/**
* Pass this method an Object and a Method equipped to display application options. They will be
* called when the Preferences menu item is selected from the application menu
*/
@SuppressWarnings("unchecked")
public static void setPreferencesHandler(Object target, Method prefsHandler) {
OSXHandler adapter = new OSXHandler("handlePreferences", target, prefsHandler) {
public boolean callTarget(Object appleEvent) {
if (appleEvent != null) {
try {
this.targetMethod.invoke(this.targetObject, new Object[] { appleEvent });
} catch (IllegalAccessException | IllegalArgumentException | SecurityException |
InvocationTargetException ex) {
logger.severe("Mac OS X Adapter could not talk to EAWT:");
}
}
return true;
}
};
try {
Class<?> applicationClass = Class.forName("java.awt.Desktop");
if (macOSXApplication == null) {
Method method = applicationClass.getMethod("getDesktop", (Class<?>[])null);
macOSXApplication = method.invoke(null);
}
Class<?> prefsHandlerClass = Class.forName("java.awt.desktop.PreferencesHandler");
Method addHandlerMethod = applicationClass.getDeclaredMethod("setPreferencesHandler", new Class<?>[] { prefsHandlerClass });
// Create a proxy object around this handler that can be reflectively added as an Apple AppEvent handler
Object osxAdapterProxy = Proxy.newProxyInstance(OSXHandler.class.getClassLoader(), new Class<?>[] { prefsHandlerClass }, adapter);
addHandlerMethod.invoke(macOSXApplication, new Object[] { osxAdapterProxy });
} catch (ClassNotFoundException cnfe) {
logger.log(Level.SEVERE, "This version of Mac OS X does not support the Apple EAWT. ApplicationEvent handling has been disabled ({0})", cnfe.getMessage());
cnfe.printStackTrace();
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
// Likely a NoSuchMethodException or an IllegalAccessException loading/invoking eawt.Application methods
logger.severe("Mac OS X Adapter could not talk to EAWT:");
logger.severe(ex.toString());
}
}
/**
* Pass this method an Object and a Method equipped to handle document events from the Finder.
* Documents are registered with the Finder via the CFBundleDocumentTypes dictionary in the
* application bundle's Info.plist
*/
@SuppressWarnings("unchecked")
public static void setFileHandler(Object target, Method fileHandler) {
OSXHandler adapter = new OSXHandler("openFiles", target, fileHandler) {
// Override OSXHandler.callTarget to send information on the file to be opened
public boolean callTarget(Object appleEvent) {
if (appleEvent != null) {
try {
Method getFilenameMethod = appleEvent.getClass().getMethod("getFiles", (Class[]) null);
java.util.List files = (java.util.List) getFilenameMethod.invoke(appleEvent, (Object[]) null);
this.targetMethod.invoke(this.targetObject, new Object[] { files });
} catch (IllegalAccessException | IllegalArgumentException |
NoSuchMethodException | SecurityException | InvocationTargetException ex) {
logger.severe("Mac OS X Adapter could not talk to EAWT:");
}
}
return true;
}
};
try {
Class<?> applicationClass = Class.forName("java.awt.Desktop");
if (macOSXApplication == null) {
Method method = applicationClass.getMethod("getDesktop", (Class<?>[])null);
macOSXApplication = method.invoke(null);
}
Class<?> filesHandlerClass = Class.forName("java.awt.desktop.OpenFilesHandler");
Method addHandlerMethod = applicationClass.getDeclaredMethod("setOpenFileHandler", new Class<?>[] { filesHandlerClass });
// Create a proxy object around this handler that can be reflectively added as an Apple AppEvent handler
Object osxAdapterProxy = Proxy.newProxyInstance(OSXHandler.class.getClassLoader(), new Class<?>[] { filesHandlerClass }, adapter);
addHandlerMethod.invoke(macOSXApplication, new Object[] { osxAdapterProxy });
} catch (ClassNotFoundException cnfe) {
logger.log(Level.SEVERE, "This version of Mac OS X does not support the Apple EAWT. ApplicationEvent handling has been disabled ({0})", cnfe.getMessage());
cnfe.printStackTrace();
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
// Likely a NoSuchMethodException or an IllegalAccessException loading/invoking eawt.Application methods
logger.severe("Mac OS X Adapter could not talk to EAWT:");
logger.severe(ex.toString());
}
}
/**
* InvocationHandler implementation This is the entry point for our proxy object; it is called
* every time an AppEvent method is invoked
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// All of the AppEvent methods are void; return null regardless of what happens
if (isCorrectMethod(method, args)) {
if (args.length == 1) {
callTarget(args[0]);
} else {
callTarget(args[0], args[1]);
}
}
return null;
}
/**
* Override this method to perform any operations on the event that comes with the various
* callbacks See setFileHandler above for an example
*/
public boolean callTarget(Object appleEvent) throws InvocationTargetException, IllegalAccessException {
Object result = targetMethod.invoke(targetObject, (Object[]) null);
if (result == null) {
return true;
}
return Boolean.valueOf(result.toString());
}
/**
* Override this method to perform any operations on the event that comes with the various
* callbacks See setQuitHandler above for an example
*/
public boolean callTarget(Object appleEvent, Object response) throws InvocationTargetException, IllegalAccessException {
Object result = targetMethod.invoke(targetObject, (Object[]) null);
if (result == null) {
return true;
}
return Boolean.valueOf(result.toString());
}
/**
* Compare the method that was called to the intended method when the OSXHandler instance was
* created (e.g. handleAbout, handleQuitRequestWith, openFiles etc.)
*/
protected boolean isCorrectMethod(Method method, Object[] args) {
return (targetMethod != null && proxySignature.equals(method.getName()) && args.length > 0);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment