MenuBarEnhancerCocoa.java revision 9d333439029e282e9b8fec93e57ebedeb5c4016a
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.eclipse.org/org/documents/epl-v10.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 * History:
17 * Original code by the <a href="http://www.simidude.com/blog/2008/macify-a-swt-application-in-a-cross-platform-way/">CarbonUIEnhancer from Agynami</a>
18 * with the implementation being modified from the <a href="http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.ui.cocoa/src/org/eclipse/ui/internal/cocoa/CocoaUIEnhancer.java">org.eclipse.ui.internal.cocoa.CocoaUIEnhancer</a>,
19 * then modified by http://www.transparentech.com/opensource/cocoauienhancer to use reflection
20 * rather than 'link' to SWT cocoa, and finally modified to be usable by the SwtMenuBar project.
21 */
22
23package com.android.menubar.internal;
24
25import com.android.menubar.IMenuBarCallback;
26import com.android.menubar.IMenuBarEnhancer;
27
28import org.eclipse.swt.SWT;
29import org.eclipse.swt.internal.C;
30import org.eclipse.swt.internal.Callback;
31import org.eclipse.swt.widgets.Display;
32import org.eclipse.swt.widgets.Event;
33import org.eclipse.swt.widgets.Listener;
34import org.eclipse.swt.widgets.Menu;
35
36import java.lang.reflect.InvocationTargetException;
37import java.lang.reflect.Method;
38
39public class MenuBarEnhancerCocoa implements IMenuBarEnhancer {
40
41    private static final long kAboutMenuItem = 0;
42    private static final long kPreferencesMenuItem = 2;
43    // private static final long kServicesMenuItem = 4;
44    // private static final long kHideApplicationMenuItem = 6;
45    private static final long kQuitMenuItem = 10;
46
47    static long mSelPreferencesMenuItemSelected;
48    static long mSelAboutMenuItemSelected;
49    static Callback mProc3Args;
50
51    private String mAppName;
52
53    /**
54     * Class invoked via the Callback object to run the about and preferences
55     * actions.
56     * <p>
57     * If you don't use JFace in your application (SWT only), change the
58     * {@link org.eclipse.jface.action.IAction}s to
59     * {@link org.eclipse.swt.widgets.Listener}s.
60     * </p>
61     */
62    private static class ActionProctarget {
63        private final IMenuBarCallback mCallbacks;
64
65        public ActionProctarget(IMenuBarCallback callbacks) {
66            mCallbacks = callbacks;
67        }
68
69        /**
70         * Will be called on 32bit SWT.
71         */
72        @SuppressWarnings("unused")
73        public int actionProc(int id, int sel, int arg0) {
74            return (int) actionProc((long) id, (long) sel, (long) arg0);
75        }
76
77        /**
78         * Will be called on 64bit SWT.
79         */
80        public long actionProc(long id, long sel, long arg0) {
81            if (sel == mSelAboutMenuItemSelected) {
82                mCallbacks.onAboutMenuSelected();
83            } else if (sel == mSelPreferencesMenuItemSelected) {
84                mCallbacks.onPreferencesMenuSelected();
85            } else {
86                // Unknown selection!
87            }
88            // Return value is not used.
89            return 0;
90        }
91    }
92
93    /**
94     * Construct a new CocoaUIEnhancer.
95     *
96     * @param mAppName The name of the application. It will be used to customize
97     *            the About and Quit menu items. If you do not wish to customize
98     *            the About and Quit menu items, just pass <tt>null</tt> here.
99     */
100    public MenuBarEnhancerCocoa() {
101    }
102
103    /**
104     * Setup the About and Preferences native menut items with the
105     * given application name and links them to the callback.
106     *
107     * @param appName The application name.
108     * @param swtMenu The tools menu. Not used here.
109     * @param callbacks The callbacks invoked by the menus.
110     */
111    public void setupMenu(
112            String appName,
113            Menu swtMenu,
114            IMenuBarCallback callbacks) {
115
116        mAppName = appName;
117        final Display display = swtMenu.getDisplay();
118
119        // This is our callback object whose 'actionProc' method will be called
120        // when the About or Preferences menuItem is invoked.
121        ActionProctarget target = new ActionProctarget(callbacks);
122
123        try {
124            // Initialize the menuItems.
125            initialize(target);
126        } catch (Exception e) {
127            throw new IllegalStateException(e);
128        }
129
130        // Schedule disposal of callback object
131        display.disposeExec(new Runnable() {
132            public void run() {
133                invoke(mProc3Args, "dispose");
134            }
135        });
136    }
137
138    private void initialize(Object callbackObject)
139            throws Exception {
140
141        Class<?> osCls = classForName("org.eclipse.swt.internal.cocoa.OS");
142
143        // Register names in objective-c.
144        if (mSelAboutMenuItemSelected == 0) {
145            mSelPreferencesMenuItemSelected = registerName(osCls, "preferencesMenuItemSelected:"); //$NON-NLS-1$
146            mSelAboutMenuItemSelected = registerName(osCls, "aboutMenuItemSelected:");             //$NON-NLS-1$
147        }
148
149        // Create an SWT Callback object that will invoke the actionProc method
150        // of our internal callback Object.
151        mProc3Args = new Callback(callbackObject, "actionProc", 3); //$NON-NLS-1$
152        Method getAddress = Callback.class.getMethod("getAddress", new Class[0]);
153        Object object = getAddress.invoke(mProc3Args, (Object[]) null);
154        long proc3 = convertToLong(object);
155        if (proc3 == 0) {
156            SWT.error(SWT.ERROR_NO_MORE_CALLBACKS);
157        }
158
159        Class<?> nsMenuCls        = classForName("org.eclipse.swt.internal.cocoa.NSMenu");
160        Class<?> nsMenuitemCls    = classForName("org.eclipse.swt.internal.cocoa.NSMenuItem");
161        Class<?> nsStringCls      = classForName("org.eclipse.swt.internal.cocoa.NSString");
162        Class<?> nsApplicationCls = classForName("org.eclipse.swt.internal.cocoa.NSApplication");
163
164        // Instead of creating a new delegate class in objective-c,
165        // just use the current SWTApplicationDelegate. An instance of this
166        // is a field of the Cocoa Display object and is already the target
167        // for the menuItems. So just get this class and add the new methods
168        // to it.
169        object = invoke(osCls, "objc_lookUpClass", new Object[] {
170            "SWTApplicationDelegate"
171        });
172        long cls = convertToLong(object);
173
174        // Add the action callbacks for Preferences and About menu items.
175        invoke(osCls, "class_addMethod",
176                new Object[] {
177                    wrapPointer(cls),
178                    wrapPointer(mSelPreferencesMenuItemSelected),
179                    wrapPointer(proc3), "@:@"}); //$NON-NLS-1$
180        invoke(osCls,  "class_addMethod",
181                new Object[] {
182                    wrapPointer(cls),
183                    wrapPointer(mSelAboutMenuItemSelected),
184                    wrapPointer(proc3), "@:@"}); //$NON-NLS-1$
185
186        // Get the Mac OS X Application menu.
187        Object sharedApplication = invoke(nsApplicationCls, "sharedApplication");
188        Object mainMenu = invoke(sharedApplication, "mainMenu");
189        Object mainMenuItem = invoke(nsMenuCls, mainMenu, "itemAtIndex", new Object[] {
190            wrapPointer(0)
191        });
192        Object appMenu = invoke(mainMenuItem, "submenu");
193
194        // Create the About <application-name> menu command
195        Object aboutMenuItem =
196                invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
197                    wrapPointer(kAboutMenuItem)
198                });
199        if (mAppName != null) {
200            Object nsStr = invoke(nsStringCls, "stringWith", new Object[] {
201                "About " + mAppName
202            });
203            invoke(nsMenuitemCls, aboutMenuItem, "setTitle", new Object[] {
204                nsStr
205            });
206        }
207        // Rename the quit action.
208        if (mAppName != null) {
209            Object quitMenuItem =
210                    invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
211                        wrapPointer(kQuitMenuItem)
212                    });
213            Object nsStr = invoke(nsStringCls, "stringWith", new Object[] {
214                "Quit " + mAppName
215            });
216            invoke(nsMenuitemCls, quitMenuItem, "setTitle", new Object[] {
217                nsStr
218            });
219        }
220
221        // Enable the Preferences menuItem.
222        Object prefMenuItem =
223                invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
224                    wrapPointer(kPreferencesMenuItem)
225                });
226        invoke(nsMenuitemCls, prefMenuItem, "setEnabled", new Object[] {
227            true
228        });
229
230        // Set the action to execute when the About or Preferences menuItem is
231        // invoked.
232        //
233        // We don't need to set the target here as the current target is the
234        // SWTApplicationDelegate and we have registerd the new selectors on
235        // it. So just set the new action to invoke the selector.
236        invoke(nsMenuitemCls, prefMenuItem, "setAction",
237                new Object[] {
238                    wrapPointer(mSelPreferencesMenuItemSelected)
239                });
240        invoke(nsMenuitemCls, aboutMenuItem, "setAction",
241                new Object[] {
242                    wrapPointer(mSelAboutMenuItemSelected)
243                });
244    }
245
246    private long registerName(Class<?> osCls, String name)
247            throws IllegalArgumentException, SecurityException, IllegalAccessException,
248            InvocationTargetException, NoSuchMethodException {
249        Object object = invoke(osCls, "sel_registerName", new Object[] {
250            name
251        });
252        return convertToLong(object);
253    }
254
255    private long convertToLong(Object object) {
256        if (object instanceof Integer) {
257            Integer i = (Integer) object;
258            return i.longValue();
259        }
260        if (object instanceof Long) {
261            Long l = (Long) object;
262            return l.longValue();
263        }
264        return 0;
265    }
266
267    private static Object wrapPointer(long value) {
268        Class<?> PTR_CLASS = C.PTR_SIZEOF == 8 ? long.class : int.class;
269        if (PTR_CLASS == long.class) {
270            return new Long(value);
271        } else {
272            return new Integer((int) value);
273        }
274    }
275
276    private static Object invoke(Class<?> clazz, String methodName, Object[] args) {
277        return invoke(clazz, null, methodName, args);
278    }
279
280    private static Object invoke(Class<?> clazz, Object target, String methodName, Object[] args) {
281        try {
282            Class<?>[] signature = new Class<?>[args.length];
283            for (int i = 0; i < args.length; i++) {
284                Class<?> thisClass = args[i].getClass();
285                if (thisClass == Integer.class)
286                    signature[i] = int.class;
287                else if (thisClass == Long.class)
288                    signature[i] = long.class;
289                else if (thisClass == Byte.class)
290                    signature[i] = byte.class;
291                else if (thisClass == Boolean.class)
292                    signature[i] = boolean.class;
293                else
294                    signature[i] = thisClass;
295            }
296            Method method = clazz.getMethod(methodName, signature);
297            return method.invoke(target, args);
298        } catch (Exception e) {
299            throw new IllegalStateException(e);
300        }
301    }
302
303    private Class<?> classForName(String classname) {
304        try {
305            Class<?> cls = Class.forName(classname);
306            return cls;
307        } catch (ClassNotFoundException e) {
308            throw new IllegalStateException(e);
309        }
310    }
311
312    private Object invoke(Class<?> cls, String methodName) {
313        return invoke(cls, methodName, (Class<?>[]) null, (Object[]) null);
314    }
315
316    private Object invoke(Class<?> cls, String methodName, Class<?>[] paramTypes,
317            Object... arguments) {
318        try {
319            Method m = cls.getDeclaredMethod(methodName, paramTypes);
320            return m.invoke(null, arguments);
321        } catch (Exception e) {
322            throw new IllegalStateException(e);
323        }
324    }
325
326    private Object invoke(Object obj, String methodName) {
327        return invoke(obj, methodName, (Class<?>[]) null, (Object[]) null);
328    }
329
330    private Object invoke(Object obj, String methodName, Class<?>[] paramTypes, Object... arguments) {
331        try {
332            Method m = obj.getClass().getDeclaredMethod(methodName, paramTypes);
333            return m.invoke(obj, arguments);
334        } catch (Exception e) {
335            throw new IllegalStateException(e);
336        }
337    }
338}
339