001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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 https://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
089import org.slf4j.Logger;
090import org.slf4j.LoggerFactory;
091
092public class OSXAdapter implements InvocationHandler {
093    
094    private static final Logger logger =
095        LoggerFactory.getLogger(OSXAdapter.class);
096    
097    protected Object targetObject;
098    protected Method targetMethod;
099    protected String proxySignature;
100    
101    static Object macOSXApplication;
102
103    // Pass this method an Object and Method equipped to perform application shutdown logic
104    // The method passed should return a boolean stating whether or not the quit should occur
105    public static void setQuitHandler(Object target, Method quitHandler) {
106        setHandler(new OSXAdapter("handleQuit", target, quitHandler));
107    }
108    
109    // Pass this method an Object and Method equipped to display application info
110    // They will be called when the About menu item is selected from the application menu
111    public static void setAboutHandler(Object target, Method aboutHandler) {
112        boolean enableAboutMenu = (target != null && aboutHandler != null);
113        if (enableAboutMenu) {
114            setHandler(new OSXAdapter("handleAbout", target, aboutHandler));
115        }
116        // If we're setting a handler, enable the About menu item by calling
117        // com.apple.eawt.Application reflectively
118        try {
119            Method enableAboutMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledAboutMenu", new Class[] { boolean.class });
120            enableAboutMethod.invoke(macOSXApplication, new Object[] { Boolean.valueOf(enableAboutMenu) });
121        } catch (Exception ex) {
122            logger.error("OSXAdapter could not access the About Menu", ex);
123        }
124    }
125    
126    // Pass this method an Object and a Method equipped to display application options
127    // They will be called when the Preferences menu item is selected from the application menu
128    public static void setPreferencesHandler(Object target, Method prefsHandler) {
129        boolean enablePrefsMenu = (target != null && prefsHandler != null);
130        if (enablePrefsMenu) {
131            setHandler(new OSXAdapter("handlePreferences", target, prefsHandler));
132        }
133        // If we're setting a handler, enable the Preferences menu item by calling
134        // com.apple.eawt.Application reflectively
135        try {
136            Method enablePrefsMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledPreferencesMenu", new Class[] { boolean.class });
137            enablePrefsMethod.invoke(macOSXApplication, new Object[] { Boolean.valueOf(enablePrefsMenu) });
138        } catch (Exception ex) {
139            logger.error("OSXAdapter could not access the About Menu", ex);
140        }
141    }
142    
143    // Pass this method an Object and a Method equipped to handle document events from the Finder
144    // Documents are registered with the Finder via the CFBundleDocumentTypes dictionary in the 
145    // application bundle's Info.plist
146    public static void setFileHandler(Object target, Method fileHandler) {
147        setHandler(new OSXAdapter("handleOpenFile", target, fileHandler) {
148            // Override OSXAdapter.callTarget to send information on the
149            // file to be opened
150            public boolean callTarget(Object appleEvent) {
151                if (appleEvent != null) {
152                    try {
153                        Method getFilenameMethod = appleEvent.getClass().getDeclaredMethod("getFilename", (Class[])null);
154                        String filename = (String) getFilenameMethod.invoke(appleEvent, (Object[])null);
155                        this.targetMethod.invoke(this.targetObject, new Object[] { filename });
156                    } catch (Exception ex) {
157                        logger.error("Error setting file handler", ex);
158                    }
159                }
160                return true;
161            }
162        });
163    }
164    
165    // setHandler creates a Proxy object from the passed OSXAdapter and adds it as an ApplicationListener
166    public static void setHandler(OSXAdapter adapter) {
167        try {
168            Class applicationClass = Class.forName("com.apple.eawt.Application");
169            if (macOSXApplication == null) {
170                macOSXApplication = applicationClass.getConstructor((Class[])null).newInstance((Object[])null);
171            }
172            Class applicationListenerClass = Class.forName("com.apple.eawt.ApplicationListener");
173            Method addListenerMethod = applicationClass.getDeclaredMethod("addApplicationListener", new Class[] { applicationListenerClass });
174            // Create a proxy object around this handler that can be reflectively added as an Apple ApplicationListener
175            Object osxAdapterProxy = Proxy.newProxyInstance(OSXAdapter.class.getClassLoader(), new Class[] { applicationListenerClass }, adapter);
176            addListenerMethod.invoke(macOSXApplication, new Object[] { osxAdapterProxy });
177        } catch (ClassNotFoundException cnfe) {
178            logger.error("This version of Mac OS X does not support the " +
179                "Apple EAWT.  ApplicationEvent handling has been disabled", cnfe);
180        } catch (Exception ex) {  // Likely a NoSuchMethodException or an IllegalAccessException loading/invoking eawt.Application methods
181            logger.error("Mac OS X Adapter could not talk to EAWT", ex);
182        }
183    }
184
185    // Each OSXAdapter has the name of the EAWT method it intends to listen for (handleAbout, for example),
186    // the Object that will ultimately perform the task, and the Method to be called on that Object
187    protected OSXAdapter(String proxySignature, Object target, Method handler) {
188        this.proxySignature = proxySignature;
189        this.targetObject = target;
190        this.targetMethod = handler;
191    }
192    
193    // Override this method to perform any operations on the event 
194    // that comes with the various callbacks
195    // See setFileHandler above for an example
196    public boolean callTarget(Object appleEvent) throws InvocationTargetException, IllegalAccessException {
197        Object result = targetMethod.invoke(targetObject, (Object[])null);
198        if (result == null) {
199            return true;
200        }
201        return Boolean.valueOf(result.toString()).booleanValue();
202    }
203    
204    // InvocationHandler implementation
205    // This is the entry point for our proxy object; it is called every time an ApplicationListener method is invoked
206    public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
207        if (isCorrectMethod(method, args)) {
208            boolean handled = callTarget(args[0]);
209            setApplicationEventHandled(args[0], handled);
210        }
211        // All of the ApplicationListener methods are void; return null regardless of what happens
212        return null;
213    }
214    
215    // Compare the method that was called to the intended method when the OSXAdapter instance was created
216    // (e.g. handleAbout, handleQuit, handleOpenFile, etc.)
217    protected boolean isCorrectMethod(Method method, Object[] args) {
218        return (targetMethod != null && proxySignature.equals(method.getName()) && args.length == 1);
219    }
220    
221    // It is important to mark the ApplicationEvent as handled and cancel the default behavior
222    // This method checks for a boolean result from the proxy method and sets the event accordingly
223    protected void setApplicationEventHandled(Object event, boolean handled) {
224        if (event != null) {
225            try {
226                Method setHandledMethod = event.getClass().getDeclaredMethod("setHandled", new Class[] { boolean.class });
227                // If the target method returns a boolean, use that as a hint
228                setHandledMethod.invoke(event, new Object[] { Boolean.valueOf(handled) });
229            } catch (Exception ex) {
230                logger.error("OSXAdapter was unable to handle an " +
231                    "ApplicationEvent: " + event, ex);
232            }
233        }
234    }
235}