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    public MenuBarMode getMenuBarMode() {
104        return MenuBarMode.MAC_OS;
105    }
106
107    /**
108     * Setup the About and Preferences native menut items with the
109     * given application name and links them to the callback.
110     *
111     * @param appName The application name.
112     * @param display The SWT display. Must not be null.
113     * @param callbacks The callbacks invoked by the menus.
114     */
115    public void setupMenu(
116            String appName,
117            Display display,
118            IMenuBarCallback callbacks) {
119
120        mAppName = appName;
121
122        // This is our callback object whose 'actionProc' method will be called
123        // when the About or Preferences menuItem is invoked.
124        ActionProctarget target = new ActionProctarget(callbacks);
125
126        try {
127            // Initialize the menuItems.
128            initialize(target);
129        } catch (Exception e) {
130            throw new IllegalStateException(e);
131        }
132
133        // Schedule disposal of callback object
134        display.disposeExec(new Runnable() {
135            public void run() {
136                invoke(mProc3Args, "dispose");
137            }
138        });
139    }
140
141    private void initialize(Object callbackObject)
142            throws Exception {
143
144        Class<?> osCls = classForName("org.eclipse.swt.internal.cocoa.OS");
145
146        // Register names in objective-c.
147        if (mSelAboutMenuItemSelected == 0) {
148            mSelPreferencesMenuItemSelected = registerName(osCls, "preferencesMenuItemSelected:"); //$NON-NLS-1$
149            mSelAboutMenuItemSelected = registerName(osCls, "aboutMenuItemSelected:");             //$NON-NLS-1$
150        }
151
152        // Create an SWT Callback object that will invoke the actionProc method
153        // of our internal callback Object.
154        mProc3Args = new Callback(callbackObject, "actionProc", 3); //$NON-NLS-1$
155        Method getAddress = Callback.class.getMethod("getAddress", new Class[0]);
156        Object object = getAddress.invoke(mProc3Args, (Object[]) null);
157        long proc3 = convertToLong(object);
158        if (proc3 == 0) {
159            SWT.error(SWT.ERROR_NO_MORE_CALLBACKS);
160        }
161
162        Class<?> nsMenuCls        = classForName("org.eclipse.swt.internal.cocoa.NSMenu");
163        Class<?> nsMenuitemCls    = classForName("org.eclipse.swt.internal.cocoa.NSMenuItem");
164        Class<?> nsStringCls      = classForName("org.eclipse.swt.internal.cocoa.NSString");
165        Class<?> nsApplicationCls = classForName("org.eclipse.swt.internal.cocoa.NSApplication");
166
167        // Instead of creating a new delegate class in objective-c,
168        // just use the current SWTApplicationDelegate. An instance of this
169        // is a field of the Cocoa Display object and is already the target
170        // for the menuItems. So just get this class and add the new methods
171        // to it.
172        object = invoke(osCls, "objc_lookUpClass", new Object[] {
173            "SWTApplicationDelegate"
174        });
175        long cls = convertToLong(object);
176
177        // Add the action callbacks for Preferences and About menu items.
178        invoke(osCls, "class_addMethod",
179                new Object[] {
180                    wrapPointer(cls),
181                    wrapPointer(mSelPreferencesMenuItemSelected),
182                    wrapPointer(proc3), "@:@"}); //$NON-NLS-1$
183        invoke(osCls,  "class_addMethod",
184                new Object[] {
185                    wrapPointer(cls),
186                    wrapPointer(mSelAboutMenuItemSelected),
187                    wrapPointer(proc3), "@:@"}); //$NON-NLS-1$
188
189        // Get the Mac OS X Application menu.
190        Object sharedApplication = invoke(nsApplicationCls, "sharedApplication");
191        Object mainMenu = invoke(sharedApplication, "mainMenu");
192        Object mainMenuItem = invoke(nsMenuCls, mainMenu, "itemAtIndex", new Object[] {
193            wrapPointer(0)
194        });
195        Object appMenu = invoke(mainMenuItem, "submenu");
196
197        // Create the About <application-name> menu command
198        Object aboutMenuItem =
199                invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
200                    wrapPointer(kAboutMenuItem)
201                });
202        if (mAppName != null) {
203            Object nsStr = invoke(nsStringCls, "stringWith", new Object[] {
204                "About " + mAppName
205            });
206            invoke(nsMenuitemCls, aboutMenuItem, "setTitle", new Object[] {
207                nsStr
208            });
209        }
210        // Rename the quit action.
211        if (mAppName != null) {
212            Object quitMenuItem =
213                    invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
214                        wrapPointer(kQuitMenuItem)
215                    });
216            Object nsStr = invoke(nsStringCls, "stringWith", new Object[] {
217                "Quit " + mAppName
218            });
219            invoke(nsMenuitemCls, quitMenuItem, "setTitle", new Object[] {
220                nsStr
221            });
222        }
223
224        // Enable the Preferences menuItem.
225        Object prefMenuItem =
226                invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
227                    wrapPointer(kPreferencesMenuItem)
228                });
229        invoke(nsMenuitemCls, prefMenuItem, "setEnabled", new Object[] {
230            true
231        });
232
233        // Set the action to execute when the About or Preferences menuItem is
234        // invoked.
235        //
236        // We don't need to set the target here as the current target is the
237        // SWTApplicationDelegate and we have registerd the new selectors on
238        // it. So just set the new action to invoke the selector.
239        invoke(nsMenuitemCls, prefMenuItem, "setAction",
240                new Object[] {
241                    wrapPointer(mSelPreferencesMenuItemSelected)
242                });
243        invoke(nsMenuitemCls, aboutMenuItem, "setAction",
244                new Object[] {
245                    wrapPointer(mSelAboutMenuItemSelected)
246                });
247    }
248
249    private long registerName(Class<?> osCls, String name)
250            throws IllegalArgumentException, SecurityException, IllegalAccessException,
251            InvocationTargetException, NoSuchMethodException {
252        Object object = invoke(osCls, "sel_registerName", new Object[] {
253            name
254        });
255        return convertToLong(object);
256    }
257
258    private long convertToLong(Object object) {
259        if (object instanceof Integer) {
260            Integer i = (Integer) object;
261            return i.longValue();
262        }
263        if (object instanceof Long) {
264            Long l = (Long) object;
265            return l.longValue();
266        }
267        return 0;
268    }
269
270    private static Object wrapPointer(long value) {
271        Class<?> PTR_CLASS = C.PTR_SIZEOF == 8 ? long.class : int.class;
272        if (PTR_CLASS == long.class) {
273            return new Long(value);
274        } else {
275            return new Integer((int) value);
276        }
277    }
278
279    private static Object invoke(Class<?> clazz, String methodName, Object[] args) {
280        return invoke(clazz, null, methodName, args);
281    }
282
283    private static Object invoke(Class<?> clazz, Object target, String methodName, Object[] args) {
284        try {
285            Class<?>[] signature = new Class<?>[args.length];
286            for (int i = 0; i < args.length; i++) {
287                Class<?> thisClass = args[i].getClass();
288                if (thisClass == Integer.class)
289                    signature[i] = int.class;
290                else if (thisClass == Long.class)
291                    signature[i] = long.class;
292                else if (thisClass == Byte.class)
293                    signature[i] = byte.class;
294                else if (thisClass == Boolean.class)
295                    signature[i] = boolean.class;
296                else
297                    signature[i] = thisClass;
298            }
299            Method method = clazz.getMethod(methodName, signature);
300            return method.invoke(target, args);
301        } catch (Exception e) {
302            throw new IllegalStateException(e);
303        }
304    }
305
306    private Class<?> classForName(String classname) {
307        try {
308            Class<?> cls = Class.forName(classname);
309            return cls;
310        } catch (ClassNotFoundException e) {
311            throw new IllegalStateException(e);
312        }
313    }
314
315    private Object invoke(Class<?> cls, String methodName) {
316        return invoke(cls, methodName, (Class<?>[]) null, (Object[]) null);
317    }
318
319    private Object invoke(Class<?> cls, String methodName, Class<?>[] paramTypes,
320            Object... arguments) {
321        try {
322            Method m = cls.getDeclaredMethod(methodName, paramTypes);
323            return m.invoke(null, arguments);
324        } catch (Exception e) {
325            throw new IllegalStateException(e);
326        }
327    }
328
329    private Object invoke(Object obj, String methodName) {
330        return invoke(obj, methodName, (Class<?>[]) null, (Object[]) null);
331    }
332
333    private Object invoke(Object obj, String methodName, Class<?>[] paramTypes, Object... arguments) {
334        try {
335            Method m = obj.getClass().getDeclaredMethod(methodName, paramTypes);
336            return m.invoke(obj, arguments);
337        } catch (Exception e) {
338            throw new IllegalStateException(e);
339        }
340    }
341}
342