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