123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- /*
- * Copyright (c) 2006-2017 DMDirc Developers
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
- * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
- * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
- * permit persons to whom the Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
- * Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
- * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
- * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
- * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
- package com.dmdirc.addons.ui_swing;
-
- import com.dmdirc.addons.ui_swing.components.menubar.MenuBar;
- import com.dmdirc.config.GlobalConfig;
- import com.dmdirc.events.ClientOpenedEvent;
- import com.dmdirc.interfaces.ConnectionManager;
- import com.dmdirc.events.eventbus.EventBus;
- import com.dmdirc.interfaces.config.AggregateConfigProvider;
- import com.dmdirc.util.InvalidURIException;
- import com.dmdirc.util.URIParser;
- import java.awt.Image;
- import java.awt.PopupMenu;
- import java.awt.event.ActionEvent;
- import java.awt.event.ActionListener;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
- import java.lang.reflect.Proxy;
- import java.net.URI;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.EventObject;
- import javax.inject.Inject;
- import javax.inject.Singleton;
- import javax.swing.JMenu;
- import javax.swing.JMenuBar;
- import javax.swing.JMenuItem;
- import javax.swing.UIManager;
- import net.engio.mbassy.listener.Handler;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-
- import static com.dmdirc.util.LogUtils.USER_ERROR;
-
- /**
- * Integrate DMDirc with OS X better.
- */
- @Singleton
- public class Apple implements InvocationHandler {
-
- private static final Logger LOG = LoggerFactory.getLogger(Apple.class);
- /** Store any addresses that are opened before CLIENT_OPENED. */
- private final Collection<URI> addresses = new ArrayList<>();
- /** Config manager used to read settings. */
- private final AggregateConfigProvider configManager;
- /** The "Application" object used to do stuff on OS X. */
- private Object application;
- /** Whether we're listening or not. */
- private boolean isListener;
- /** The MenuBar for the application. */
- private MenuBar menuBar;
- /** Whether the CLIENT_OPENED action has been called or not. */
- private boolean clientOpened;
- /** The server manager to use to connect to URLs. */
- private final ConnectionManager connectionManager;
- /** Event bus. */
- private final EventBus eventBus;
-
- /**
- * Creates a new instance of {@link Apple}.
- *
- * <p>
- * This will attempt to load the native library and register the URL open callback.
- *
- * @param configManager Config manager
- * @param connectionManager The server manager to use to connect to URLs.
- * @param eventBus The bus to listen for events on.
- */
- @Inject
- public Apple(
- @GlobalConfig final AggregateConfigProvider configManager,
- final ConnectionManager connectionManager,
- final EventBus eventBus) {
- this.configManager = configManager;
- this.connectionManager = connectionManager;
- this.eventBus = eventBus;
- }
-
- public void load() {
- if (isApple()) {
- try {
- System.loadLibrary("DMDirc-Apple"); // NOPMD
- registerOpenURLCallback();
- eventBus.subscribe(this);
- } catch (UnsatisfiedLinkError ule) {
- LOG.warn(USER_ERROR, "Unable to load JNI library", ule);
- }
- }
- }
-
- /**
- * Register the getURL Callback.
- *
- * @return 0 on success, 1 on failure.
- */
- private synchronized native int registerOpenURLCallback();
-
- /**
- * Call a method on the given object.
- *
- * @param obj Object to call method on.
- * @param className Name of class that object really is.
- * @param methodName Method to call
- * @param classes Array of classes to pass when calling getMethod
- * @param objects Array of objects to pass when invoking.
- *
- * @return Output from method.invoke()
- */
- private Object reflectMethod(final Object obj, final String className, final String methodName,
- final Class<?>[] classes, final Object[] objects) {
- try {
- final Class<?> clazz = className == null ? obj.getClass() : Class.forName(className);
- final Method method = clazz.getMethod(methodName, classes == null ? new Class<?>[0]
- : classes);
- return method.invoke(obj, objects == null ? new Object[0] : objects);
- } catch (ReflectiveOperationException ex) {
- LOG.info(USER_ERROR, "Unable to find OS X classes.", ex);
- }
-
- return null;
- }
-
- /**
- * Handle a method call to the apple Application class.
- *
- * @param methodName Method to call
- * @param classes Array of classes to pass when calling getMethod
- * @param objects Array of objects to pass when invoking.
- *
- * @return Output from method.invoke()
- */
- private Object doAppleMethod(final String methodName, final Class<?>[] classes,
- final Object[] objects) {
- if (!isApple()) {
- return null;
- }
-
- return reflectMethod(getApplication(), null, methodName, classes, objects);
- }
-
- /**
- * Get the "Application" object.
- *
- * @return Object that on OSX will be an "Application"
- */
- public Object getApplication() {
- synchronized (Apple.class) {
- if (isApple() && application == null) {
- application = reflectMethod(null, "com.apple.eawt.Application", "getApplication",
- null, null);
- }
- return application;
- }
- }
-
- /**
- * Are we on OS X?
- *
- * @return true if we are running on OS X
- */
- public static boolean isApple() {
- return System.getProperty("os.name").contains("OS X");
- }
-
- /**
- * Are we using the OS X look and feel?
- *
- * @return true if we are using the OS X look and feel
- */
- public static boolean isAppleUI() {
- final String name = UIManager.getLookAndFeel().getClass().getName();
- return isApple() && ("apple.laf.AquaLookAndFeel".equals(name)
- || "com.apple.laf.AquaLookAndFeel".equals(name));
- }
-
- /**
- * Set some OS X only UI settings.
- */
- public void setUISettings() {
- if (!isApple()) {
- return;
- }
-
- // Set some Apple OS X related stuff from http://tinyurl.com/6xwuld
- final String aaText = configManager.getOptionBool("ui", "antialias") ? "on" : "off";
-
- System.setProperty("apple.awt.antialiasing", aaText);
- System.setProperty("apple.awt.textantialiasing", aaText);
- System.setProperty("apple.awt.showGrowBox", "true");
- System.setProperty("com.apple.mrj.application.apple.menu.about.name", "DMDirc");
- System.setProperty("apple.laf.useScreenMenuBar", "true");
- System.setProperty("com.apple.mrj.application.growbox.intrudes", "false");
- System.setProperty("com.apple.mrj.application.live-resize", "true");
- }
-
- /**
- * Requests this application to move to the foreground.
- *
- * @param allWindows if all windows of this application should be moved to the foreground, or
- * only the foremost one
- */
- public void requestForeground(final boolean allWindows) {
- doAppleMethod("requestForeground", new Class<?>[]{Boolean.TYPE}, new Object[]{allWindows});
- }
-
- /**
- * Requests user attention to this application (usually through bouncing the Dock icon).
- * Critical requests will continue to bounce the Dock icon until the app is activated. An
- * already active application requesting attention does nothing.
- *
- * @param isCritical If this is false, the dock icon only bounces once, otherwise it will bounce
- * until clicked on.
- */
- public void requestUserAttention(final boolean isCritical) {
- doAppleMethod("requestUserAttention", new Class<?>[]{Boolean.TYPE}, new Object[]{isCritical});
- }
-
- /**
- * Attaches the contents of the provided PopupMenu to the application's Dock icon.
- *
- * @param menu the PopupMenu to attach to this application's Dock icon
- */
- public void setDockMenu(final PopupMenu menu) {
- doAppleMethod("setDockMenu", new Class<?>[]{PopupMenu.class}, new Object[]{menu});
- }
-
- /**
- * Get the PopupMenu attached to the application's Dock icon.
- *
- * @return the PopupMenu attached to this application's Dock icon
- */
- public PopupMenu getDockMenu() {
- final Object result = doAppleMethod("getDockMenu", null, null);
- return result instanceof PopupMenu ? (PopupMenu) result : null;
- }
-
- /**
- * Changes this application's Dock icon to the provided image.
- *
- * @param image The image to use
- */
- public void setDockIconImage(final Image image) {
- doAppleMethod("setDockIconImage", new Class<?>[]{Image.class}, new Object[]{image});
- }
-
- /**
- * Obtains an image of this application's Dock icon.
- *
- * @return The application's dock icon.
- */
- public Image getDockIconImage() {
- final Object result = doAppleMethod("getDockIconImage", null, null);
- return result instanceof Image ? (Image) result : null;
- }
-
- /**
- * Affixes a small system provided badge to this application's Dock icon. Usually a number.
- *
- * @param badge textual label to affix to the Dock icon
- */
- public void setDockIconBadge(final String badge) {
- doAppleMethod("setDockIconBadge", new Class<?>[]{String.class}, new Object[]{badge});
- }
-
- /**
- * Sets the default menu bar to use when there are no active frames. Only used when the system
- * property "apple.laf.useScreenMenuBar" is "true", and the Aqua Look and Feel is active.
- *
- * @param menuBar to use when no other frames are active
- */
- public void setDefaultMenuBar(final JMenuBar menuBar) {
- doAppleMethod("setDefaultMenuBar", new Class<?>[]{JMenuBar.class}, new Object[]{menuBar});
- }
-
- /**
- * Add this application as a handler for the given event.
- *
- * @param handlerClass Class used as the handler.
- * @param handlerMethod Method used to set the handler.
- *
- * @return True if we succeeded.
- */
- private boolean addHandler(final String handlerClass, final String handlerMethod) {
- try {
- final Class<?> listenerClass = Class.forName(handlerClass);
- final Object listener = Proxy.newProxyInstance(getClass().getClassLoader(),
- new Class<?>[]{listenerClass}, this);
-
- final Method method = getApplication().getClass().getMethod(handlerMethod,
- listenerClass);
- method.invoke(getApplication(), listener);
-
- return true;
- } catch (ClassNotFoundException | NoSuchMethodException |
- IllegalAccessException | InvocationTargetException ex) {
- return false;
- }
- }
-
- /**
- * Set this up as a listener for the Apple Events.
- *
- * @return True if the listener was added, else false.
- */
- public boolean setListener() {
- if (!isApple() || isListener) {
- return false;
- }
-
- addHandler("com.apple.eawt.OpenURIHandler", "setOpenURIHandler");
- addHandler("com.apple.eawt.AboutHandler", "setAboutHandler");
- addHandler("com.apple.eawt.QuitHandler", "setQuitHandler");
- addHandler("com.apple.eawt.PreferencesHandler", "setPreferencesHandler");
- isListener = true;
-
- return true;
- }
-
- @Override
- public Object invoke(final Object proxy, final Method method, final Object[] args)
- throws ReflectiveOperationException {
- if (!isApple()) {
- return null;
- }
-
- try {
- final Class<?>[] classes = new Class<?>[args.length];
-
- for (int i = 0; i < args.length; i++) {
- if (EventObject.class.isInstance(args[i])) {
- classes[i] = EventObject.class;
- } else {
- final Class<?> c = args[i].getClass();
- if ("com.apple.eawt.QuitResponse".equals(c.getCanonicalName())) {
- classes[i] = Object.class;
- } else {
- classes[i] = c;
- }
- }
- }
-
- final Method thisMethod = getClass().getMethod(method.getName(), classes);
- return thisMethod.invoke(this, args);
- } catch (final NoSuchMethodException e) {
- if ("equals".equals(method.getName()) && args.length == 1) {
- return proxy == args[0];
- }
- }
-
- return null;
- }
-
- /**
- * Set the MenuBar. This will unset all menu mnemonics aswell if on the OSX ui.
- *
- * @param newMenuBar MenuBar to use to send events to,
- */
- public void setMenuBar(final MenuBar newMenuBar) {
- menuBar = newMenuBar;
- if (!isAppleUI()) {
- return;
- }
- for (int i = 0; i < menuBar.getMenuCount(); i++) {
- final JMenu menu = menuBar.getMenu(i);
- if (menu == null) {
- continue;
- }
- menu.setMnemonic(0);
- for (int j = 0; j < menu.getItemCount(); j++) {
- final JMenuItem menuItem = menu.getItem(j);
- if (menuItem != null) {
- menuItem.setMnemonic(0);
- }
- }
- }
- }
-
- /**
- * Handle an event using the menuBar.
- *
- * @param name The name of the event according to the menubar
- */
- public void handleMenuBarEvent(final String name) {
- if (!isApple() || menuBar == null) {
- return;
- }
- final ActionEvent actionEvent = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, name);
-
- for (int i = 0; i < menuBar.getMenuCount(); i++) {
- final JMenu menu = menuBar.getMenu(i);
- if (menu instanceof ActionListener) {
- ((ActionListener) menu).actionPerformed(actionEvent);
- }
- }
- }
-
- /**
- * This is called when About is selected from the Application menu.
- *
- * @param event an ApplicationEvent object
- */
- public void handleAbout(final EventObject event) {
- handleMenuBarEvent("About");
- }
-
- /**
- * This is called when Preferences is selected from the Application menu.
- *
- * @param event an ApplicationEvent object
- */
- public void handlePreferences(final EventObject event) {
- handleMenuBarEvent("Preferences");
- }
-
- /**
- * Called when the client is opened for the first time.
- *
- * @param event The event describing the client opening.
- */
- @Handler
- public void handleClientOpened(final ClientOpenedEvent event) {
- synchronized (addresses) {
- clientOpened = true;
- addresses.forEach(connectionManager::connectToAddress);
- addresses.clear();
- }
- }
-
- /**
- * This is called when Quit is selected from the Application menu.
- *
- * @param event an ApplicationEvent object
- * @param quitResponse QuitResponse object.
- */
- public void handleQuitRequestWith(final EventObject event, final Object quitResponse) {
- // Technically we should tell OS X if the quit succeeds or not, but we
- // have no way of knowing the result just yet.
- //
- // So instead we will just tell it that the quit was cancelled every
- // time, and then just quit anyway if we need to.
- reflectMethod(quitResponse, null, "cancelQuit", null, null);
-
- handleMenuBarEvent("Exit");
- }
-
- /**
- * Callback from our JNI library. This should work when not launcher via JavaApplicationStub
- *
- * @param url The irc url string to connect to.
- */
- public void handleOpenURL(final String url) {
- try {
- handleURI(new URIParser().parseFromText(url));
- } catch (final InvalidURIException ex) {
- // Do nothing?...
- }
- }
-
- /**
- * Callback from OSX Directly. This will work if we were launched using the JavaApplicationStub
- *
- * @param event Event related to this callback. This event will have a reflectable getURI method
- * to get a URI.
- */
- public void openURI(final EventObject event) {
- if (!isApple()) {
- return;
- }
-
- final Object obj = reflectMethod(event, null, "getURI", null, null);
- if (obj instanceof URI) {
- handleURI((URI) obj);
- }
- }
-
- /**
- * Handle connecting to a URI.
- *
- * If called before the client has finished opening, the URI will be added to a list that will
- * be connected to once the CLIENT_OPENED action is called. Otherwise we connect right away.
- *
- * @param uri URI to connect to.
- */
- private void handleURI(final URI uri) {
- synchronized (addresses) {
- if (clientOpened) {
- // When the JNI callback is called there is no
- // ContextClassLoader set, which causes an NPE in
- // IconManager if no servers have been connected to yet.
- if (Thread.currentThread().getContextClassLoader() == null) {
- Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
- }
-
- connectionManager.connectToAddress(uri);
- } else {
- addresses.add(uri);
- }
- }
- }
-
- }
|