You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Apple.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. /*
  2. * Copyright (c) 2006-2017 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
  5. * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
  6. * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
  7. * permit persons to whom the Software is furnished to do so, subject to the following conditions:
  8. *
  9. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
  10. * Software.
  11. *
  12. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  13. * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
  14. * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  15. * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  16. */
  17. package com.dmdirc.addons.ui_swing;
  18. import com.dmdirc.addons.ui_swing.components.menubar.MenuBar;
  19. import com.dmdirc.config.GlobalConfig;
  20. import com.dmdirc.events.ClientOpenedEvent;
  21. import com.dmdirc.interfaces.ConnectionManager;
  22. import com.dmdirc.events.eventbus.EventBus;
  23. import com.dmdirc.interfaces.config.AggregateConfigProvider;
  24. import com.dmdirc.util.InvalidURIException;
  25. import com.dmdirc.util.URIParser;
  26. import java.awt.Image;
  27. import java.awt.PopupMenu;
  28. import java.awt.event.ActionEvent;
  29. import java.awt.event.ActionListener;
  30. import java.lang.reflect.InvocationHandler;
  31. import java.lang.reflect.InvocationTargetException;
  32. import java.lang.reflect.Method;
  33. import java.lang.reflect.Proxy;
  34. import java.net.URI;
  35. import java.util.ArrayList;
  36. import java.util.Collection;
  37. import java.util.EventObject;
  38. import javax.inject.Inject;
  39. import javax.inject.Singleton;
  40. import javax.swing.JMenu;
  41. import javax.swing.JMenuBar;
  42. import javax.swing.JMenuItem;
  43. import javax.swing.UIManager;
  44. import net.engio.mbassy.listener.Handler;
  45. import org.slf4j.Logger;
  46. import org.slf4j.LoggerFactory;
  47. import static com.dmdirc.util.LogUtils.USER_ERROR;
  48. /**
  49. * Integrate DMDirc with OS X better.
  50. */
  51. @Singleton
  52. public class Apple implements InvocationHandler {
  53. private static final Logger LOG = LoggerFactory.getLogger(Apple.class);
  54. /** Store any addresses that are opened before CLIENT_OPENED. */
  55. private final Collection<URI> addresses = new ArrayList<>();
  56. /** Config manager used to read settings. */
  57. private final AggregateConfigProvider configManager;
  58. /** The "Application" object used to do stuff on OS X. */
  59. private Object application;
  60. /** Whether we're listening or not. */
  61. private boolean isListener;
  62. /** The MenuBar for the application. */
  63. private MenuBar menuBar;
  64. /** Whether the CLIENT_OPENED action has been called or not. */
  65. private boolean clientOpened;
  66. /** The server manager to use to connect to URLs. */
  67. private final ConnectionManager connectionManager;
  68. /** Event bus. */
  69. private final EventBus eventBus;
  70. /**
  71. * Creates a new instance of {@link Apple}.
  72. *
  73. * <p>
  74. * This will attempt to load the native library and register the URL open callback.
  75. *
  76. * @param configManager Config manager
  77. * @param connectionManager The server manager to use to connect to URLs.
  78. * @param eventBus The bus to listen for events on.
  79. */
  80. @Inject
  81. public Apple(
  82. @GlobalConfig final AggregateConfigProvider configManager,
  83. final ConnectionManager connectionManager,
  84. final EventBus eventBus) {
  85. this.configManager = configManager;
  86. this.connectionManager = connectionManager;
  87. this.eventBus = eventBus;
  88. }
  89. public void load() {
  90. if (isApple()) {
  91. try {
  92. System.loadLibrary("DMDirc-Apple"); // NOPMD
  93. registerOpenURLCallback();
  94. eventBus.subscribe(this);
  95. } catch (UnsatisfiedLinkError ule) {
  96. LOG.warn(USER_ERROR, "Unable to load JNI library", ule);
  97. }
  98. }
  99. }
  100. /**
  101. * Register the getURL Callback.
  102. *
  103. * @return 0 on success, 1 on failure.
  104. */
  105. private synchronized native int registerOpenURLCallback();
  106. /**
  107. * Call a method on the given object.
  108. *
  109. * @param obj Object to call method on.
  110. * @param className Name of class that object really is.
  111. * @param methodName Method to call
  112. * @param classes Array of classes to pass when calling getMethod
  113. * @param objects Array of objects to pass when invoking.
  114. *
  115. * @return Output from method.invoke()
  116. */
  117. private Object reflectMethod(final Object obj, final String className, final String methodName,
  118. final Class<?>[] classes, final Object[] objects) {
  119. try {
  120. final Class<?> clazz = className == null ? obj.getClass() : Class.forName(className);
  121. final Method method = clazz.getMethod(methodName, classes == null ? new Class<?>[0]
  122. : classes);
  123. return method.invoke(obj, objects == null ? new Object[0] : objects);
  124. } catch (ReflectiveOperationException ex) {
  125. LOG.info(USER_ERROR, "Unable to find OS X classes.", ex);
  126. }
  127. return null;
  128. }
  129. /**
  130. * Handle a method call to the apple Application class.
  131. *
  132. * @param methodName Method to call
  133. * @param classes Array of classes to pass when calling getMethod
  134. * @param objects Array of objects to pass when invoking.
  135. *
  136. * @return Output from method.invoke()
  137. */
  138. private Object doAppleMethod(final String methodName, final Class<?>[] classes,
  139. final Object[] objects) {
  140. if (!isApple()) {
  141. return null;
  142. }
  143. return reflectMethod(getApplication(), null, methodName, classes, objects);
  144. }
  145. /**
  146. * Get the "Application" object.
  147. *
  148. * @return Object that on OSX will be an "Application"
  149. */
  150. public Object getApplication() {
  151. synchronized (Apple.class) {
  152. if (isApple() && application == null) {
  153. application = reflectMethod(null, "com.apple.eawt.Application", "getApplication",
  154. null, null);
  155. }
  156. return application;
  157. }
  158. }
  159. /**
  160. * Are we on OS X?
  161. *
  162. * @return true if we are running on OS X
  163. */
  164. public static boolean isApple() {
  165. return System.getProperty("os.name").contains("OS X");
  166. }
  167. /**
  168. * Are we using the OS X look and feel?
  169. *
  170. * @return true if we are using the OS X look and feel
  171. */
  172. public static boolean isAppleUI() {
  173. final String name = UIManager.getLookAndFeel().getClass().getName();
  174. return isApple() && ("apple.laf.AquaLookAndFeel".equals(name)
  175. || "com.apple.laf.AquaLookAndFeel".equals(name));
  176. }
  177. /**
  178. * Set some OS X only UI settings.
  179. */
  180. public void setUISettings() {
  181. if (!isApple()) {
  182. return;
  183. }
  184. // Set some Apple OS X related stuff from http://tinyurl.com/6xwuld
  185. final String aaText = configManager.getOptionBool("ui", "antialias") ? "on" : "off";
  186. System.setProperty("apple.awt.antialiasing", aaText);
  187. System.setProperty("apple.awt.textantialiasing", aaText);
  188. System.setProperty("apple.awt.showGrowBox", "true");
  189. System.setProperty("com.apple.mrj.application.apple.menu.about.name", "DMDirc");
  190. System.setProperty("apple.laf.useScreenMenuBar", "true");
  191. System.setProperty("com.apple.mrj.application.growbox.intrudes", "false");
  192. System.setProperty("com.apple.mrj.application.live-resize", "true");
  193. }
  194. /**
  195. * Requests this application to move to the foreground.
  196. *
  197. * @param allWindows if all windows of this application should be moved to the foreground, or
  198. * only the foremost one
  199. */
  200. public void requestForeground(final boolean allWindows) {
  201. doAppleMethod("requestForeground", new Class<?>[]{Boolean.TYPE}, new Object[]{allWindows});
  202. }
  203. /**
  204. * Requests user attention to this application (usually through bouncing the Dock icon).
  205. * Critical requests will continue to bounce the Dock icon until the app is activated. An
  206. * already active application requesting attention does nothing.
  207. *
  208. * @param isCritical If this is false, the dock icon only bounces once, otherwise it will bounce
  209. * until clicked on.
  210. */
  211. public void requestUserAttention(final boolean isCritical) {
  212. doAppleMethod("requestUserAttention", new Class<?>[]{Boolean.TYPE}, new Object[]{isCritical});
  213. }
  214. /**
  215. * Attaches the contents of the provided PopupMenu to the application's Dock icon.
  216. *
  217. * @param menu the PopupMenu to attach to this application's Dock icon
  218. */
  219. public void setDockMenu(final PopupMenu menu) {
  220. doAppleMethod("setDockMenu", new Class<?>[]{PopupMenu.class}, new Object[]{menu});
  221. }
  222. /**
  223. * Get the PopupMenu attached to the application's Dock icon.
  224. *
  225. * @return the PopupMenu attached to this application's Dock icon
  226. */
  227. public PopupMenu getDockMenu() {
  228. final Object result = doAppleMethod("getDockMenu", null, null);
  229. return result instanceof PopupMenu ? (PopupMenu) result : null;
  230. }
  231. /**
  232. * Changes this application's Dock icon to the provided image.
  233. *
  234. * @param image The image to use
  235. */
  236. public void setDockIconImage(final Image image) {
  237. doAppleMethod("setDockIconImage", new Class<?>[]{Image.class}, new Object[]{image});
  238. }
  239. /**
  240. * Obtains an image of this application's Dock icon.
  241. *
  242. * @return The application's dock icon.
  243. */
  244. public Image getDockIconImage() {
  245. final Object result = doAppleMethod("getDockIconImage", null, null);
  246. return result instanceof Image ? (Image) result : null;
  247. }
  248. /**
  249. * Affixes a small system provided badge to this application's Dock icon. Usually a number.
  250. *
  251. * @param badge textual label to affix to the Dock icon
  252. */
  253. public void setDockIconBadge(final String badge) {
  254. doAppleMethod("setDockIconBadge", new Class<?>[]{String.class}, new Object[]{badge});
  255. }
  256. /**
  257. * Sets the default menu bar to use when there are no active frames. Only used when the system
  258. * property "apple.laf.useScreenMenuBar" is "true", and the Aqua Look and Feel is active.
  259. *
  260. * @param menuBar to use when no other frames are active
  261. */
  262. public void setDefaultMenuBar(final JMenuBar menuBar) {
  263. doAppleMethod("setDefaultMenuBar", new Class<?>[]{JMenuBar.class}, new Object[]{menuBar});
  264. }
  265. /**
  266. * Add this application as a handler for the given event.
  267. *
  268. * @param handlerClass Class used as the handler.
  269. * @param handlerMethod Method used to set the handler.
  270. *
  271. * @return True if we succeeded.
  272. */
  273. private boolean addHandler(final String handlerClass, final String handlerMethod) {
  274. try {
  275. final Class<?> listenerClass = Class.forName(handlerClass);
  276. final Object listener = Proxy.newProxyInstance(getClass().getClassLoader(),
  277. new Class<?>[]{listenerClass}, this);
  278. final Method method = getApplication().getClass().getMethod(handlerMethod,
  279. listenerClass);
  280. method.invoke(getApplication(), listener);
  281. return true;
  282. } catch (ClassNotFoundException | NoSuchMethodException |
  283. IllegalAccessException | InvocationTargetException ex) {
  284. return false;
  285. }
  286. }
  287. /**
  288. * Set this up as a listener for the Apple Events.
  289. *
  290. * @return True if the listener was added, else false.
  291. */
  292. public boolean setListener() {
  293. if (!isApple() || isListener) {
  294. return false;
  295. }
  296. addHandler("com.apple.eawt.OpenURIHandler", "setOpenURIHandler");
  297. addHandler("com.apple.eawt.AboutHandler", "setAboutHandler");
  298. addHandler("com.apple.eawt.QuitHandler", "setQuitHandler");
  299. addHandler("com.apple.eawt.PreferencesHandler", "setPreferencesHandler");
  300. isListener = true;
  301. return true;
  302. }
  303. @Override
  304. public Object invoke(final Object proxy, final Method method, final Object[] args)
  305. throws ReflectiveOperationException {
  306. if (!isApple()) {
  307. return null;
  308. }
  309. try {
  310. final Class<?>[] classes = new Class<?>[args.length];
  311. for (int i = 0; i < args.length; i++) {
  312. if (EventObject.class.isInstance(args[i])) {
  313. classes[i] = EventObject.class;
  314. } else {
  315. final Class<?> c = args[i].getClass();
  316. if ("com.apple.eawt.QuitResponse".equals(c.getCanonicalName())) {
  317. classes[i] = Object.class;
  318. } else {
  319. classes[i] = c;
  320. }
  321. }
  322. }
  323. final Method thisMethod = getClass().getMethod(method.getName(), classes);
  324. return thisMethod.invoke(this, args);
  325. } catch (final NoSuchMethodException e) {
  326. if ("equals".equals(method.getName()) && args.length == 1) {
  327. return proxy == args[0];
  328. }
  329. }
  330. return null;
  331. }
  332. /**
  333. * Set the MenuBar. This will unset all menu mnemonics aswell if on the OSX ui.
  334. *
  335. * @param newMenuBar MenuBar to use to send events to,
  336. */
  337. public void setMenuBar(final MenuBar newMenuBar) {
  338. menuBar = newMenuBar;
  339. if (!isAppleUI()) {
  340. return;
  341. }
  342. for (int i = 0; i < menuBar.getMenuCount(); i++) {
  343. final JMenu menu = menuBar.getMenu(i);
  344. if (menu == null) {
  345. continue;
  346. }
  347. menu.setMnemonic(0);
  348. for (int j = 0; j < menu.getItemCount(); j++) {
  349. final JMenuItem menuItem = menu.getItem(j);
  350. if (menuItem != null) {
  351. menuItem.setMnemonic(0);
  352. }
  353. }
  354. }
  355. }
  356. /**
  357. * Handle an event using the menuBar.
  358. *
  359. * @param name The name of the event according to the menubar
  360. */
  361. public void handleMenuBarEvent(final String name) {
  362. if (!isApple() || menuBar == null) {
  363. return;
  364. }
  365. final ActionEvent actionEvent = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, name);
  366. for (int i = 0; i < menuBar.getMenuCount(); i++) {
  367. final JMenu menu = menuBar.getMenu(i);
  368. if (menu instanceof ActionListener) {
  369. ((ActionListener) menu).actionPerformed(actionEvent);
  370. }
  371. }
  372. }
  373. /**
  374. * This is called when About is selected from the Application menu.
  375. *
  376. * @param event an ApplicationEvent object
  377. */
  378. public void handleAbout(final EventObject event) {
  379. handleMenuBarEvent("About");
  380. }
  381. /**
  382. * This is called when Preferences is selected from the Application menu.
  383. *
  384. * @param event an ApplicationEvent object
  385. */
  386. public void handlePreferences(final EventObject event) {
  387. handleMenuBarEvent("Preferences");
  388. }
  389. /**
  390. * Called when the client is opened for the first time.
  391. *
  392. * @param event The event describing the client opening.
  393. */
  394. @Handler
  395. public void handleClientOpened(final ClientOpenedEvent event) {
  396. synchronized (addresses) {
  397. clientOpened = true;
  398. addresses.forEach(connectionManager::connectToAddress);
  399. addresses.clear();
  400. }
  401. }
  402. /**
  403. * This is called when Quit is selected from the Application menu.
  404. *
  405. * @param event an ApplicationEvent object
  406. * @param quitResponse QuitResponse object.
  407. */
  408. public void handleQuitRequestWith(final EventObject event, final Object quitResponse) {
  409. // Technically we should tell OS X if the quit succeeds or not, but we
  410. // have no way of knowing the result just yet.
  411. //
  412. // So instead we will just tell it that the quit was cancelled every
  413. // time, and then just quit anyway if we need to.
  414. reflectMethod(quitResponse, null, "cancelQuit", null, null);
  415. handleMenuBarEvent("Exit");
  416. }
  417. /**
  418. * Callback from our JNI library. This should work when not launcher via JavaApplicationStub
  419. *
  420. * @param url The irc url string to connect to.
  421. */
  422. public void handleOpenURL(final String url) {
  423. try {
  424. handleURI(new URIParser().parseFromText(url));
  425. } catch (final InvalidURIException ex) {
  426. // Do nothing?...
  427. }
  428. }
  429. /**
  430. * Callback from OSX Directly. This will work if we were launched using the JavaApplicationStub
  431. *
  432. * @param event Event related to this callback. This event will have a reflectable getURI method
  433. * to get a URI.
  434. */
  435. public void openURI(final EventObject event) {
  436. if (!isApple()) {
  437. return;
  438. }
  439. final Object obj = reflectMethod(event, null, "getURI", null, null);
  440. if (obj instanceof URI) {
  441. handleURI((URI) obj);
  442. }
  443. }
  444. /**
  445. * Handle connecting to a URI.
  446. *
  447. * If called before the client has finished opening, the URI will be added to a list that will
  448. * be connected to once the CLIENT_OPENED action is called. Otherwise we connect right away.
  449. *
  450. * @param uri URI to connect to.
  451. */
  452. private void handleURI(final URI uri) {
  453. synchronized (addresses) {
  454. if (clientOpened) {
  455. // When the JNI callback is called there is no
  456. // ContextClassLoader set, which causes an NPE in
  457. // IconManager if no servers have been connected to yet.
  458. if (Thread.currentThread().getContextClassLoader() == null) {
  459. Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
  460. }
  461. connectionManager.connectToAddress(uri);
  462. } else {
  463. addresses.add(uri);
  464. }
  465. }
  466. }
  467. }