Skip to content

Instantly share code, notes, and snippets.

@alanwhite
Created June 2, 2022 20:34
Show Gist options
  • Save alanwhite/42502f20390baf879d093691ebb72066 to your computer and use it in GitHub Desktop.
Save alanwhite/42502f20390baf879d093691ebb72066 to your computer and use it in GitHub Desktop.
Example Mac OSX Magnify and Rotate gestures in a Swing app that will compile on all platform but only work on Mac (help with other platforms welcome)
package xyz.arwhite.swing;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
/*
* To make this work ensure on jdk17.02+2 onwards
* Compile with -XDignore.symbol.file --add-exports java.desktop/com.apple.eawt.event=ALL-UNNAMED
* Run with --add-opens java.desktop/com.apple.eawt.event=ALL-UNNAMED
*
* This will compile and run on platforms other than osx as it uses reflection to reach the
* osx specific classes.
*/
@SuppressWarnings("serial")
public class Indigesturing extends JFrame {
public class MagnifyHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) {
// method.getName() should always return "magnify" as that's all we subscribed to
// production code may wish to double check this.
try {
for(Object o: args) {
Object mag = o.getClass()
.getMethod("getMagnification")
.invoke(o);
// mag.getClass() should always return java.lang.Double
// production code may wish to check double this (no pun intended)
System.out.println("Magnify: "+mag);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public class RotateHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) {
// method.getName() should always return "rotate" as that's all we subscribed to
// production code may wish to double check this.
try {
for(Object o: args) {
Object rot = o.getClass()
.getMethod("getRotation")
.invoke(o);
// mag.getClass() should always return java.lang.Double
// production code may wish to check double this (no pun intended)
System.out.println("Rotate: "+rot);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public Indigesturing() {
super();
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setPreferredSize(new Dimension(600,400));
JPanel p = new JPanel();
getContentPane().add(p);
/*
GestureUtilities.addGestureListenerTo(p, (GestureListener) new MagnificationListener() {
@Override
public void magnify(MagnificationEvent magnificationEvent) {
System.out.println("Magnify "+magnificationEvent.getMagnification());
}
@Override
public void rotate(RotationEvent rotationEvent) {
System.out.println("Rotate: "+rotationEvent.getrotation());
}
});
*/
// 40 odd lines of code replace 10 lines to make it cross-platform friendly
if (System.getProperty("os.name").contains("Mac")) {
try {
// Run with --add-opens java.desktop/com.apple.eawt.event=ALL-UNNAMED
Constructor[] constructors = Class.forName("com.apple.eawt.event.GestureUtilities")
.getDeclaredConstructors();
Object gu=null;
for (Constructor constructor : constructors)
{
constructor.setAccessible(true);
gu = constructor.newInstance();
break;
}
Object mh = Proxy.newProxyInstance(
Class.forName("com.apple.eawt.event.MagnificationListener").getClassLoader(),
new Class[]{Class.forName("com.apple.eawt.event.MagnificationListener")},
new MagnifyHandler()
);
gu.getClass()
.getMethod("addGestureListenerTo",
Class.forName("javax.swing.JComponent"),
Class.forName("com.apple.eawt.event.GestureListener"))
.invoke(gu, p, mh);
Object rh = Proxy.newProxyInstance(
Class.forName("com.apple.eawt.event.RotationListener").getClassLoader(),
new Class[]{Class.forName("com.apple.eawt.event.RotationListener")},
new RotateHandler()
);
gu.getClass()
.getMethod("addGestureListenerTo",
Class.forName("javax.swing.JComponent"),
Class.forName("com.apple.eawt.event.GestureListener"))
.invoke(gu, p, rh);
}
catch (Exception e) {
e.printStackTrace();
}
}
var ma = new MouseAdapter() {
/*
* 2-finger swipe is a mouse wheel scroll
* modifier/ext-modifier of shift is supplied if horizontal, otherwise vertical
* wheelRotation is positive for up/left swipes, negative for down/right, preciseWheelRotation follows, almost always updates, even if wheelRotation 0
* using the touchpad appears to use some momentum algo under the covers as events can keep coming after fingers have left contact with the pad
* not evident how to tell when contact has been broken, rotation reduces as moment reduces
*
* This bug is the only place I've seen that documents the shift behaviour https://bugs.openjdk.java.net/browse/JDK-8203048
*/
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
System.out.println(e);
}
/*
* 3-finger swipe is a drag event, reported as Button1 being pressed, even if no click down has been made
* There is no difference between a 3-finger drag whilst clicked/pressed into the pad, and not
*
* 2-finger click and drag, is reported as a drag event with cmd+Button3 modifiers
* 2-finger swipe, ie no click, is a mouse scroll event, see above
*
* 1-finger click and drag, is a drag and reports as Button1 pressed, exactly the same as a 3-finger swipe, just no button down event before (I imagine)
*
* Direction is always determined by the co-ordinate move. There is no concept of momentum as this event relies on the co-ordinates of the pointer.
*/
@Override
public void mouseDragged(MouseEvent e) { System.out.println(e); }
};
p.addMouseListener(ma);
p.addMouseMotionListener(ma);
p.addMouseWheelListener(ma);
pack();
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Indigesturing();
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment