Added support for showing the macos menu on right click without resorting to click emulation

This commit is contained in:
Robinson 2023-01-02 23:56:53 +01:00
parent 4ae18e1962
commit 99f4135152
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
4 changed files with 142 additions and 248 deletions

40
LICENSE
View File

@ -41,18 +41,18 @@
Copyright 2023
Timothy Wall
- SLF4J - Simple facade or abstraction for various logging frameworks
[MIT License]
http://www.slf4j.org
Copyright 2023
QOS.ch
- Logback - Logback is a logging framework for Java applications
[The Apache Software License, Version 2.0]
http://logback.qos.ch
Copyright 2023
QOS.ch
- SLF4J - Simple facade or abstraction for various logging frameworks
[MIT License]
http://www.slf4j.org
Copyright 2023
QOS.ch
- Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/Executor
@ -150,7 +150,7 @@
- Utilities - Utilities for use within Java projects
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/Utilities
Copyright 2022
Copyright 2023
Dorkbox LLC
Extra license information
@ -237,32 +237,32 @@
- kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support
[The Apache Software License, Version 2.0]
https://github.com/Kotlin/kotlinx.coroutines
Copyright 2022
Copyright 2023
JetBrains s.r.o.
- Java Uuid Generator - A set of Java classes for working with UUIDs
[The Apache Software License, Version 2.0]
https://github.com/cowtowncoder/java-uuid-generator
Copyright 2022
Copyright 2023
Tatu Saloranta (tatu.saloranta@iki.fi)
Contributors. See source release-notes/CREDITS
- kotlin-logging - Lightweight logging framework for Kotlin
[The Apache Software License, Version 2.0]
https://github.com/MicroUtils/kotlin-logging
Copyright 2022
Copyright 2023
Ohad Shai
- SLF4J - Simple facade or abstraction for various logging frameworks
[MIT License]
http://www.slf4j.org
Copyright 2022
Copyright 2023
QOS.ch
- XZ for Java - Complete implementation of XZ data compression in pure Java
[Public Domain, per Creative Commons CC0]
https://tukaani.org/xz/java.html
Copyright 2022
Copyright 2023
Lasse Collin
Igor Pavlov
@ -277,38 +277,38 @@
- JNA - Simplified native library access for Java.
[The Apache Software License, Version 2.0]
https://github.com/twall/jna
Copyright 2022
Copyright 2023
Timothy Wall
- JNA-Platform - Mappings for a number of commonly used platform functions
[The Apache Software License, Version 2.0]
https://github.com/twall/jna
Copyright 2022
Copyright 2023
Timothy Wall
- Netty - An event-driven asynchronous network application framework
[The Apache Software License, Version 2.0]
https://netty.io
Copyright 2022
Copyright 2023
The Netty Project
Contributors. See source NOTICE
- Bouncy Castle Crypto - Lightweight cryptography API and JCE Extension
[The Apache Software License, Version 2.0]
http://www.bouncycastle.org
Copyright 2022
Copyright 2023
The Legion of the Bouncy Castle Inc
- Lightweight Java Game Library - Java library that enables cross-platform access to popular native APIs
[BSD 3-Clause License]
https://github.com/LWJGL/lwjgl3
Copyright 2022
Copyright 2023
Lightweight Java Game Library
- TypeTools - A simple, zero-dependency library for working with types. Supports Java 1.6+ and Android.
[The Apache Software License, Version 2.0]
https://github.com/jhalterman/typetools
Copyright 2022
Copyright 2023
Jonathan Halterman and friends
- Collections - Niche collections to augment what is already available.
@ -663,7 +663,7 @@
- OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment.
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/OS
Copyright 2022
Copyright 2023
Dorkbox LLC
Extra license information
@ -723,7 +723,7 @@
- OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment.
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/OS
Copyright 2022
Copyright 2023
Dorkbox LLC
Extra license information

View File

@ -16,10 +16,15 @@
package dorkbox.systemTray.ui.osx;
import java.awt.AWTException;
import java.awt.Component;
import java.awt.Image;
import java.awt.Point;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.io.File;
import java.util.concurrent.CountDownLatch;
@ -28,6 +33,7 @@ import javax.swing.ImageIcon;
import dorkbox.collections.ArrayMap;
import dorkbox.systemTray.MenuItem;
import dorkbox.systemTray.Tray;
import dorkbox.systemTray.util.AwtAccessor;
import dorkbox.systemTray.util.ImageResizeUtil;
import dorkbox.util.SwingUtil;
@ -157,6 +163,27 @@ class _OsxAwtTray extends Tray {
trayIcon.setPopupMenu((PopupMenu) _native);
final AwtOsxMenu awtOsxMenu = this;
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public
void mouseClicked(final MouseEvent e) {
final Point2D location = AwtAccessor.getLocation(trayIcon);
final Component component = new Component() {
@Override
public
Point getLocationOnScreen() {
return new Point((int) location.getX()-5, (int) location.getY()+5);
}
};
AwtAccessor.showPopup(component, _native);
}
});
try {
tray.add(trayIcon);
visible = true;

View File

@ -1,6 +1,8 @@
package dorkbox.systemTray.util;
import java.awt.Component;
import java.awt.Image;
import java.awt.Menu;
public
class AwtAccessor {
@ -15,4 +17,12 @@ class AwtAccessor {
public static void setToolTipText(final Object peerObj, final String text) {
}
public static void showPopup(final Component component, final Menu nativeComponent) {
}
public static java.awt.geom.Point2D getLocation(java.awt.TrayIcon trayIcon) {
return null;
}
}

View File

@ -17,15 +17,8 @@ package dorkbox.systemTray.util;
import static dorkbox.systemTray.SystemTray.logger;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Robot;
import java.util.concurrent.atomic.AtomicReference;
import dorkbox.jna.ClassUtils;
import dorkbox.os.OS;
import dorkbox.systemTray.SystemTray;
import dorkbox.util.SwingUtil;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
@ -92,6 +85,58 @@ class SystemTrayFixesMacOS {
ClassPool pool = ClassPool.getDefault();
try {
{
{
// have to make the peer field public
CtClass trayIconClass = pool.get("java.awt.TrayIcon");
CtField peer = trayIconClass.getField("peer");
peer.setModifiers(peer.getModifiers() & Modifier.PUBLIC);
ClassUtils.defineClass(null, trayIconClass.toBytecode());
}
CtClass trayClass = pool.get("sun.lwawt.macosx.CTrayIcon");
{
CtMethod method2 = CtNewMethod.make("public java.awt.geom.Point2D getIconLocation(long ptr) {" +
"return nativeGetIconLocation(ptr);" +
"}",
trayClass);
trayClass.addMethod(method2);
// javassist cannot create ANONYMOUS inner classes, but can create normal classes. Such a pain to do it this way
CtClass dynamicClass = pool.makeClass("sun.lwawt.macosx.CTrayIconLocationAccessory");
dynamicClass.addInterface(pool.get("sun.lwawt.macosx.CFRetainedResource$CFNativeAction"));
CtField ctField = new CtField(pool.get("java.util.concurrent.atomic.AtomicReference"), "ref", dynamicClass);
dynamicClass.addField(ctField, "new java.util.concurrent.atomic.AtomicReference();");
ctField = new CtField(pool.get("sun.lwawt.macosx.CTrayIcon"), "icon", dynamicClass);
dynamicClass.addField(ctField);
CtMethod method3 = CtNewMethod.make("public void run(long ptr){" +
"ref.set(icon.getIconLocation(ptr));" +
"}", dynamicClass);
dynamicClass.addMethod(method3);
ClassUtils.defineClass(null, dynamicClass.toBytecode());
}
CtMethod method = CtNewMethod.make(
"public java.awt.geom.Point2D getLocation() { " +
"sun.lwawt.macosx.CTrayIconLocationAccessory refAccess = new sun.lwawt.macosx.CTrayIconLocationAccessory();" +
"refAccess.icon = this;" +
"execute(refAccess);" +
"return refAccess.ref.get();" +
"}", trayClass);
trayClass.addMethod(method);
ClassUtils.defineClass(null, trayClass.toBytecode());
if (SystemTray.DEBUG) {
logger.debug("Successfully added getLocation() to macOS AWT tray menus");
}
}
// allow non-reflection access to sun.awt.AWTAccessor...getPeer()
{
CtClass dynamicClass = pool.makeClass("java.awt.MenuComponentAccessory");
@ -116,6 +161,21 @@ class SystemTrayFixesMacOS {
"}", dynamicClass);
dynamicClass.addMethod(method);
method = CtNewMethod.make(
"public static void showPopup(java.awt.Component component, java.awt.Menu nativeComponent) { " +
"java.awt.peer.PopupMenuPeer peer = (java.awt.peer.PopupMenuPeer) getPeer(nativeComponent);" +
//"java.lang.System.err.println(\"showing popup peer!\");" +
"peer.show(new java.awt.Event(component, 0L, java.awt.Event.MOUSE_DOWN, 0, 0, 0, 0));" +
"}", dynamicClass);
dynamicClass.addMethod(method);
method = CtNewMethod.make(
"public static java.awt.geom.Point2D getLocation(java.awt.TrayIcon trayIcon) { " +
"return ((sun.lwawt.macosx.CTrayIcon) trayIcon.peer).getLocation();" +
"}", dynamicClass);
dynamicClass.addMethod(method);
dynamicClass.setModifiers(dynamicClass.getModifiers() & ~Modifier.STATIC);
@ -130,26 +190,41 @@ class SystemTrayFixesMacOS {
ctMethod.setBody("{" +
"return java.awt.MenuComponentAccessory.getPeer($1);" +
"}");
// perform pre-verification for the modified method
ctMethod.getMethodInfo().rebuildStackMapForME(pool);
ctMethod = classFixer.getDeclaredMethod("setImage");
ctMethod.setBody("{" +
"java.awt.MenuComponentAccessory.setImage($1, $2);" +
"}");
// perform pre-verification for the modified method
ctMethod.getMethodInfo().rebuildStackMapForME(pool);
ctMethod = classFixer.getDeclaredMethod("setToolTipText");
ctMethod.setBody("{" +
"java.awt.MenuComponentAccessory.setToolTipText($1, $2);" +
"}");
// perform pre-verification for the modified method
ctMethod.getMethodInfo().rebuildStackMapForME(pool);
ctMethod = classFixer.getDeclaredMethod("showPopup");
ctMethod.setBody("{" +
"return java.awt.MenuComponentAccessory.showPopup($1, $2);" +
"}");
// perform pre-verification for the modified method
ctMethod.getMethodInfo().rebuildStackMapForME(pool);
ctMethod = classFixer.getDeclaredMethod("getLocation");
ctMethod.setBody("{" +
"return java.awt.MenuComponentAccessory.getLocation($1);" +
"}");
// perform pre-verification for the modified method
ctMethod.getMethodInfo().rebuildStackMapForME(pool);
final byte[] classFixerBytes = classFixer.toBytecode();
ClassUtils.defineClass(ClassLoader.getSystemClassLoader(), classFixerBytes);
}
@ -160,223 +235,5 @@ class SystemTrayFixesMacOS {
} catch (Exception e) {
logger.error("Error adding SystemTray images/tooltips for macOS AWT tray menus.", e);
}
try {
// must call this otherwise the robot call later on will crash.
final Robot robot = new Robot();
robot.waitForIdle();
Point location = MouseInfo.getPointerInfo()
.getLocation();
final int x = location.x;
final int y = location.y;
robot.setAutoWaitForIdle(true);
robot.mouseMove(x+1, y+1);
location = MouseInfo.getPointerInfo().getLocation();
final int x2 = location.x;
final int y2 = location.y;
if (x == x2 && y == y2) {
// we cannot control the mouse, so we CANNOT rely on click emulation.
logger.warn("Unable to control the mouse, please enable Accessibility permissions.");
return;
}
// NOTE: This ONLY works if accessibility is granted via macOS permissions!
byte[] mouseEventBytes;
CtClass trayClass = pool.get("sun.lwawt.macosx.CTrayIcon");
// now have to make a new "system tray" (that is null) in order to init/load this class completely
// have to modify the SystemTray.getIconSize as well.
trayClass.setModifiers(trayClass.getModifiers() & Modifier.PUBLIC);
trayClass.getConstructors()[0].setModifiers(trayClass.getConstructors()[0].getModifiers() & Modifier.PUBLIC);
CtField ctField = new CtField(CtClass.intType, "lastButton", trayClass);
trayClass.addField(ctField);
ctField = new CtField(pool.get("java.awt.Robot"), "robot", trayClass);
trayClass.addField(ctField);
CtMethod ctMethodGet = trayClass.getDeclaredMethod("handleMouseEvent");
Class<?> nsEventClass = null;
try {
nsEventClass = Class.forName("sun.lwawt.macosx.event.NSEvent");
} catch (Exception ignored) {
}
try {
nsEventClass = Class.forName("sun.lwawt.macosx.NSEvent");
} catch (Exception ignored) {
}
if (nsEventClass == null) {
logger.error("Unable to properly check mouse trigger classes for macOS AWT tray menus");
return;
}
String nsEventFQND = nsEventClass.getName();
String mouseModInfo = "";
String mousePressEventInfo = "";
String mouseReleaseEventInfo = "";
try {
if (nsEventClass.getDeclaredMethod("nsToJavaMouseModifiers", int.class, int.class) != null) {
mouseModInfo = "int mouseMods = " + nsEventFQND + ".nsToJavaMouseModifiers(button, event.getModifierFlags());";
mousePressEventInfo = "java.awt.event.MouseEvent mEvent = new java.awt.event.MouseEvent(this.dummyFrame, eventType, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);";
mouseReleaseEventInfo = "java.awt.event.MouseEvent event7 = new java.awt.event.MouseEvent(this.dummyFrame, 500, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);";
}
} catch (Exception ignored) {
}
try {
if (nsEventClass.getDeclaredMethod("nsToJavaModifiers", int.class) != null) {
mouseModInfo = "int mouseMods = " + nsEventFQND + ".nsToJavaModifiers(event.getModifierFlags());";
mousePressEventInfo = "java.awt.event.MouseEvent mEvent = new java.awt.event.MouseEvent(this.dummyFrame, eventType, event0, mouseMods, mouseX, mouseY, jClickCount, popupTrigger, jButton);";
mouseReleaseEventInfo = "java.awt.event.MouseEvent event7 = new java.awt.event.MouseEvent(this.dummyFrame, 500, event0, mouseMods, mouseX, mouseY, jClickCount, popupTrigger, jButton);";
}
} catch (Exception ignored) {
}
String mouseClickAction = "";
if (OS.INSTANCE.getJavaVersion() > 8) {
// java8 on macOS has problems where it doesn't properly assign mouse movement/clicks when emulating left-click behavior.
// We aren't going to further support something that is also no longer supported.
mouseClickAction =
"int maskButton1 = java.awt.event.InputEvent.getMaskForButton(java.awt.event.MouseEvent.BUTTON1);" +
"robot.mouseMove(mouseX, mouseY);" +
"robot.mousePress(maskButton1);";
}
ctMethodGet.setBody("{" +
nsEventFQND + " event = $1;" +
"sun.awt.SunToolkit toolKit = (sun.awt.SunToolkit)java.awt.Toolkit.getDefaultToolkit();" +
"int button = event.getButtonNumber();" +
"int mouseX = event.getAbsX();" +
"int mouseY = event.getAbsY();" +
// have to intercept to see if it was a button click redirect to preserve what button was used in the event
"if (button > 0 && lastButton == 1) {" +
"int eventType = " + nsEventFQND + ".nsToJavaEventType(event.getType());" +
"if (eventType == 501) {" +
// "java.lang.System.err.println(\"Redefining button press to 1: \" + eventType);" +
"button = 1;" +
"lastButton = -1;" +
"}" +
"}" +
"if (button > 0 && (button <= 2 || toolKit.areExtraMouseButtonsEnabled()) && button <= toolKit.getNumberOfButtons() - 1) {" +
"int eventType = " + nsEventFQND + ".nsToJavaEventType(event.getType());" +
"int jButton = 0;" +
"int jClickCount = 0;" +
"if (eventType != 503) {" +
"jButton = " + nsEventFQND + ".nsToJavaButton(button);" +
"jClickCount = event.getClickCount();" +
"}" +
// "java.lang.System.err.println(\"Click \" + jButton + \" event: \" + eventType + \" x: \" + mouseX + \" y: \" + mouseY);" +
//"int mouseMods = " + nsEventFQND + ".nsToJavaMouseModifiers(button, event.getModifierFlags());" +
mouseModInfo +
// surprisingly, this is false when the popup is showing
"boolean popupTrigger = " + nsEventFQND + ".isPopupTrigger(mouseMods);" +
"int mouseMask = jButton > 0 ? java.awt.event.MouseEvent.getMaskForButton(jButton) : 0;" +
"long event0 = System.currentTimeMillis();" +
"if (eventType == 501) {" +
"mouseClickButtons |= mouseMask;" +
"} else if(eventType == 506) {" +
"mouseClickButtons = 0;" +
"}" +
// have to swallow + re-dispatch events in specific cases. (right click)
"if (eventType == 501 && popupTrigger && button != 0) {" +
// "java.lang.System.err.println(\"Redispatching mouse press. Has popupTrigger \" + " + "popupTrigger + \" event: \" + " + "eventType);" +
// we use Robot to left click where we right clicked, in order to "fool" the native part to show the popup
// For what it's worth, this is the only way to get the native bits to behave (since we cannot access the native parts).
"if (robot == null) {" +
"try {" +
"robot = new java.awt.Robot();" +
// the delay is necessary for this to work correctly.
"robot.setAutoDelay(10);" +
"robot.setAutoWaitForIdle(false);" +
"} " +
"catch (java.awt.AWTException e) {" +
"e.printStackTrace();" +
"}" +
"}" +
"lastButton = 1;" +
// "java.lang.System.err.println(\"Click \" + button + \" x: \" + mouseX + \" y: \" + mouseY);" +
// NOTE: This ONLY works if accessibility is granted via macOS permissions!
// Mouse release is not necessary.
// this simulates *just enough* of the default behavior so that right click behaves the same as left click.
// "int maskButton1 = java.awt.event.InputEvent.getMaskForButton(java.awt.event.MouseEvent.BUTTON1);" +
// "robot.mouseMove(mouseX, mouseY);" +
// "robot.mousePress(maskButton1);" +
mouseClickAction +
"return;" +
"}" +
//"java.awt.event.MouseEvent mEvent = new java.awt.event.MouseEvent(this.dummyFrame, eventType, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);" +
mousePressEventInfo +
"mEvent.setSource(this.target);" +
"this.postEvent(mEvent);" +
// mouse press
"if (eventType == 501) {" +
"if (popupTrigger) {" +
"String event5 = this.target.getActionCommand();" +
"java.awt.event.ActionEvent event6 = new java.awt.event.ActionEvent(this.target, 1001, event5);" +
"this.postEvent(event6);" +
"}" +
"}" +
// mouse release
"if (eventType == 502) {" +
"if ((mouseClickButtons & mouseMask) != 0) {" +
// "java.awt.event.MouseEvent event7 = new java.awt.event.MouseEvent(this.dummyFrame, 500, event0, mouseMods, mouseX, mouseY, mouseX, mouseY, jClickCount, popupTrigger, jButton);" +
mouseReleaseEventInfo +
"event7.setSource(this.target);" +
"this.postEvent(event7);" +
"}" +
"mouseClickButtons &= ~mouseMask;" +
"}" +
"}" +
"}");
// perform pre-verification for the modified method
ctMethodGet.getMethodInfo().rebuildStackMapForME(pool);
mouseEventBytes = trayClass.toBytecode();
// whoosh, past the classloader and directly into memory.
// ClassUtils.defineClass(null, mouseEventBytes);
if (SystemTray.DEBUG) {
logger.debug("Successfully changed mouse trigger for macOS AWT tray menus");
}
} catch (Exception e) {
logger.error("Error changing SystemTray mouse trigger for macOS AWT tray menus.", e);
}
}
}