001/*
002 * $Id: OSXAdapter.java,v 1.6 2011/03/24 16:06:31 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030/*
031File: OSXAdapter.java
032
033Abstract: Hooks existing preferences/about/quit functionality from an
034    existing Java app into handlers for the Mac OS X application menu.
035    Uses a Proxy object to dynamically implement the 
036    com.apple.eawt.ApplicationListener interface and register it with the
037    com.apple.eawt.Application object.  This allows the complete project
038    to be both built and run on any platform without any stubs or 
039    placeholders. Useful for developers looking to implement Mac OS X 
040    features while supporting multiple platforms with minimal impact.
041                        
042Version: 2.0
043
044Disclaimer: IMPORTANT:  This Apple software is supplied to you by 
045Apple Inc. ("Apple") in consideration of your agreement to the
046following terms, and your use, installation, modification or
047redistribution of this Apple software constitutes acceptance of these
048terms.  If you do not agree with these terms, please do not use,
049install, modify or redistribute this Apple software.
050
051In consideration of your agreement to abide by the following terms, and
052subject to these terms, Apple grants you a personal, non-exclusive
053license, under Apple's copyrights in this original Apple software (the
054"Apple Software"), to use, reproduce, modify and redistribute the Apple
055Software, with or without modifications, in source and/or binary forms;
056provided that if you redistribute the Apple Software in its entirety and
057without modifications, you must retain this notice and the following
058text and disclaimers in all such redistributions of the Apple Software. 
059Neither the name, trademarks, service marks or logos of Apple Inc. 
060may be used to endorse or promote products derived from the Apple
061Software without specific prior written permission from Apple.  Except
062as expressly stated in this notice, no other rights or licenses, express
063or implied, are granted by Apple herein, including but not limited to
064any patent rights that may be infringed by your derivative works or by
065other works in which the Apple Software may be incorporated.
066
067The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
068MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
069THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
070FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
071OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
072
073IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
074OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
075SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
076INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
077MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
078AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
079STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
080POSSIBILITY OF SUCH DAMAGE.
081
082Copyright 2003-2007 Apple, Inc., All Rights Reserved
083
084*/
085
086package edu.wisc.ssec.mcidasv;
087
088import java.lang.reflect.*;
089import java.util.HashMap;
090
091
092public class OSXAdapter implements InvocationHandler {
093
094    protected Object targetObject;
095    protected Method targetMethod;
096    protected String proxySignature;
097    
098    static Object macOSXApplication;
099
100    // Pass this method an Object and Method equipped to perform application shutdown logic
101    // The method passed should return a boolean stating whether or not the quit should occur
102    public static void setQuitHandler(Object target, Method quitHandler) {
103        setHandler(new OSXAdapter("handleQuit", target, quitHandler));
104    }
105    
106    // Pass this method an Object and Method equipped to display application info
107    // They will be called when the About menu item is selected from the application menu
108    public static void setAboutHandler(Object target, Method aboutHandler) {
109        boolean enableAboutMenu = (target != null && aboutHandler != null);
110        if (enableAboutMenu) {
111            setHandler(new OSXAdapter("handleAbout", target, aboutHandler));
112        }
113        // If we're setting a handler, enable the About menu item by calling
114        // com.apple.eawt.Application reflectively
115        try {
116            Method enableAboutMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledAboutMenu", new Class[] { boolean.class });
117            enableAboutMethod.invoke(macOSXApplication, new Object[] { Boolean.valueOf(enableAboutMenu) });
118        } catch (Exception ex) {
119            System.err.println("OSXAdapter could not access the About Menu");
120            ex.printStackTrace();
121        }
122    }
123    
124    // Pass this method an Object and a Method equipped to display application options
125    // They will be called when the Preferences menu item is selected from the application menu
126    public static void setPreferencesHandler(Object target, Method prefsHandler) {
127        boolean enablePrefsMenu = (target != null && prefsHandler != null);
128        if (enablePrefsMenu) {
129            setHandler(new OSXAdapter("handlePreferences", target, prefsHandler));
130        }
131        // If we're setting a handler, enable the Preferences menu item by calling
132        // com.apple.eawt.Application reflectively
133        try {
134            Method enablePrefsMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledPreferencesMenu", new Class[] { boolean.class });
135            enablePrefsMethod.invoke(macOSXApplication, new Object[] { Boolean.valueOf(enablePrefsMenu) });
136        } catch (Exception ex) {
137            System.err.println("OSXAdapter could not access the About Menu");
138            ex.printStackTrace();
139        }
140    }
141    
142    // Pass this method an Object and a Method equipped to handle document events from the Finder
143    // Documents are registered with the Finder via the CFBundleDocumentTypes dictionary in the 
144    // application bundle's Info.plist
145    public static void setFileHandler(Object target, Method fileHandler) {
146        setHandler(new OSXAdapter("handleOpenFile", target, fileHandler) {
147            // Override OSXAdapter.callTarget to send information on the
148            // file to be opened
149            public boolean callTarget(Object appleEvent) {
150                if (appleEvent != null) {
151                    try {
152                        Method getFilenameMethod = appleEvent.getClass().getDeclaredMethod("getFilename", (Class[])null);
153                        String filename = (String) getFilenameMethod.invoke(appleEvent, (Object[])null);
154                        this.targetMethod.invoke(this.targetObject, new Object[] { filename });
155                    } catch (Exception ex) {
156                        
157                    }
158                }
159                return true;
160            }
161        });
162    }
163    
164    // setHandler creates a Proxy object from the passed OSXAdapter and adds it as an ApplicationListener
165    public static void setHandler(OSXAdapter adapter) {
166        try {
167            Class applicationClass = Class.forName("com.apple.eawt.Application");
168            if (macOSXApplication == null) {
169                macOSXApplication = applicationClass.getConstructor((Class[])null).newInstance((Object[])null);
170            }
171            Class applicationListenerClass = Class.forName("com.apple.eawt.ApplicationListener");
172            Method addListenerMethod = applicationClass.getDeclaredMethod("addApplicationListener", new Class[] { applicationListenerClass });
173            // Create a proxy object around this handler that can be reflectively added as an Apple ApplicationListener
174            Object osxAdapterProxy = Proxy.newProxyInstance(OSXAdapter.class.getClassLoader(), new Class[] { applicationListenerClass }, adapter);
175            addListenerMethod.invoke(macOSXApplication, new Object[] { osxAdapterProxy });
176        } catch (ClassNotFoundException cnfe) {
177            System.err.println("This version of Mac OS X does not support the Apple EAWT.  ApplicationEvent handling has been disabled (" + cnfe + ")");
178        } catch (Exception ex) {  // Likely a NoSuchMethodException or an IllegalAccessException loading/invoking eawt.Application methods
179            System.err.println("Mac OS X Adapter could not talk to EAWT:");
180            ex.printStackTrace();
181        }
182    }
183
184    // Each OSXAdapter has the name of the EAWT method it intends to listen for (handleAbout, for example),
185    // the Object that will ultimately perform the task, and the Method to be called on that Object
186    protected OSXAdapter(String proxySignature, Object target, Method handler) {
187        this.proxySignature = proxySignature;
188        this.targetObject = target;
189        this.targetMethod = handler;
190    }
191    
192    // Override this method to perform any operations on the event 
193    // that comes with the various callbacks
194    // See setFileHandler above for an example
195    public boolean callTarget(Object appleEvent) throws InvocationTargetException, IllegalAccessException {
196        Object result = targetMethod.invoke(targetObject, (Object[])null);
197        if (result == null) {
198            return true;
199        }
200        return Boolean.valueOf(result.toString()).booleanValue();
201    }
202    
203    // InvocationHandler implementation
204    // This is the entry point for our proxy object; it is called every time an ApplicationListener method is invoked
205    public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
206        if (isCorrectMethod(method, args)) {
207            boolean handled = callTarget(args[0]);
208            setApplicationEventHandled(args[0], handled);
209        }
210        // All of the ApplicationListener methods are void; return null regardless of what happens
211        return null;
212    }
213    
214    // Compare the method that was called to the intended method when the OSXAdapter instance was created
215    // (e.g. handleAbout, handleQuit, handleOpenFile, etc.)
216    protected boolean isCorrectMethod(Method method, Object[] args) {
217        return (targetMethod != null && proxySignature.equals(method.getName()) && args.length == 1);
218    }
219    
220    // It is important to mark the ApplicationEvent as handled and cancel the default behavior
221    // This method checks for a boolean result from the proxy method and sets the event accordingly
222    protected void setApplicationEventHandled(Object event, boolean handled) {
223        if (event != null) {
224            try {
225                Method setHandledMethod = event.getClass().getDeclaredMethod("setHandled", new Class[] { boolean.class });
226                // If the target method returns a boolean, use that as a hint
227                setHandledMethod.invoke(event, new Object[] { Boolean.valueOf(handled) });
228            } catch (Exception ex) {
229                System.err.println("OSXAdapter was unable to handle an ApplicationEvent: " + event);
230                ex.printStackTrace();
231            }
232        }
233    }
234}