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