diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/MenuBar.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/MenuBar.java index b3fe8deca3d..01de075bbe1 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/MenuBar.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/MenuBar.java @@ -89,4 +89,9 @@ public List getMenus() { Application.checkEventThread(); return Collections.unmodifiableList(menus); } + + public boolean handleKeyEvent(int code, int modifiers) { + Application.checkEventThread(); + return delegate.handleKeyEvent(code, modifiers); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/delegate/MenuBarDelegate.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/delegate/MenuBarDelegate.java index 353fe26e07f..27720c40847 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/delegate/MenuBarDelegate.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/delegate/MenuBarDelegate.java @@ -31,4 +31,8 @@ public interface MenuBarDelegate { // removes a submenu at {@code pos} which delegate is {@code menu} parameter public boolean remove(MenuDelegate menu, int pos); public long getNativeMenu(); + // Returns true if the key event was processed + public default boolean handleKeyEvent(int code, int modifiers) { + return false; + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacMenuBarDelegate.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacMenuBarDelegate.java index 3d54e366f1e..916a4d98258 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacMenuBarDelegate.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacMenuBarDelegate.java @@ -53,6 +53,12 @@ class MacMenuBarDelegate implements MenuBarDelegate { return true; } + private native boolean _handleKeyEvent(long menubarPtr, int code, int modifiers); + @Override + public boolean handleKeyEvent(int code, int modifiers) { + return _handleKeyEvent(ptr, code, modifiers); + } + @Override public long getNativeMenu() { return ptr; } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowEventDispatcher.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowEventDispatcher.java index 1ccbe95af8e..13536947483 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowEventDispatcher.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowEventDispatcher.java @@ -25,11 +25,15 @@ package com.sun.javafx.stage; +import com.sun.javafx.tk.Toolkit; + import com.sun.javafx.event.BasicEventDispatcher; import com.sun.javafx.event.CompositeEventDispatcher; import com.sun.javafx.event.EventHandlerManager; import com.sun.javafx.event.EventRedirector; +import javafx.event.Event; +import javafx.scene.input.KeyEvent; import javafx.stage.Window; /** @@ -38,29 +42,73 @@ * and then through {@code EventHandlerManager}. */ public class WindowEventDispatcher extends CompositeEventDispatcher { + + static class SystemMenuHandler extends BasicEventDispatcher { + private enum SupportedState { + TRUE, + FALSE, + UNKNOWN + }; + + private SupportedState supported = SupportedState.UNKNOWN; + + @Override + public Event dispatchBubblingEvent(Event event) { + if (supported == SupportedState.UNKNOWN) { + var systemMenu = Toolkit.getToolkit().getSystemMenu(); + if (systemMenu != null && systemMenu.isSupported()) { + supported = SupportedState.TRUE; + } else { + supported = SupportedState.FALSE; + } + } + if (supported == SupportedState.TRUE && event.getEventType() == KeyEvent.KEY_PRESSED && event instanceof KeyEvent ke) { + Toolkit.getToolkit().getSystemMenu().handleKeyEvent(ke); + } + return event; + } + } + private final EventRedirector eventRedirector; private final WindowCloseRequestHandler windowCloseRequestHandler; private final EventHandlerManager eventHandlerManager; + private final SystemMenuHandler systemMenuHandler; + public WindowEventDispatcher(final Window window) { this(new EventRedirector(window), new WindowCloseRequestHandler(window), - new EventHandlerManager(window)); - + new EventHandlerManager(window), + new SystemMenuHandler()); } public WindowEventDispatcher( final EventRedirector eventRedirector, final WindowCloseRequestHandler windowCloseRequestHandler, final EventHandlerManager eventHandlerManager) { + this(eventRedirector, + windowCloseRequestHandler, + eventHandlerManager, + null); + } + + private WindowEventDispatcher( + final EventRedirector eventRedirector, + final WindowCloseRequestHandler windowCloseRequestHandler, + final EventHandlerManager eventHandlerManager, + final SystemMenuHandler systemMenuHandler) { this.eventRedirector = eventRedirector; this.windowCloseRequestHandler = windowCloseRequestHandler; this.eventHandlerManager = eventHandlerManager; + this.systemMenuHandler = systemMenuHandler; eventRedirector.insertNextDispatcher(windowCloseRequestHandler); windowCloseRequestHandler.insertNextDispatcher(eventHandlerManager); + if (systemMenuHandler != null) { + eventHandlerManager.insertNextDispatcher(systemMenuHandler); + } } public final EventRedirector getEventRedirector() { @@ -82,6 +130,9 @@ public BasicEventDispatcher getFirstDispatcher() { @Override public BasicEventDispatcher getLastDispatcher() { + if (systemMenuHandler != null) { + return systemMenuHandler; + } return eventHandlerManager; } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSystemMenu.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSystemMenu.java index 0414b418479..5424bce2d85 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSystemMenu.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSystemMenu.java @@ -28,7 +28,7 @@ import java.util.List; import com.sun.javafx.menu.MenuBase; - +import javafx.scene.input.KeyEvent; /** * We use this interface to access the Glass native system menu @@ -45,4 +45,5 @@ public interface TKSystemMenu { public void setMenus(List menus); + public void handleKeyEvent(KeyEvent event); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassSystemMenu.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassSystemMenu.java index 7572f33f4b9..7afe71b90f2 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassSystemMenu.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassSystemMenu.java @@ -120,6 +120,13 @@ protected MenuBar getMenuBar() { } } + @Override + public void handleKeyEvent(javafx.scene.input.KeyEvent event) { + if (glassSystemMenuBar.handleKeyEvent(event.getCode().getCode(), glassModifiers(event))) { + event.consume(); + } + } + // Clear the menu to prevent a memory leak, as outlined in JDK-8094232 private void clearMenu(Menu menu) { ListChangeListener lcl = menuListeners.get(menu); @@ -407,4 +414,31 @@ private int glassModifiers(KeyCombination kcc) { return (ret); } + private int glassModifiers(javafx.scene.input.KeyEvent event) { + int ret = 0; + if (event.isShiftDown()) { + ret |= KeyEvent.MODIFIER_SHIFT; + } + if (event.isControlDown()) { + ret |= KeyEvent.MODIFIER_CONTROL; + } + if (event.isAltDown()) { + ret |= KeyEvent.MODIFIER_ALT; + } + if (event.isShortcutDown()) { + if (PlatformUtil.isMac()) { + ret |= KeyEvent.MODIFIER_COMMAND; + } else { + ret |= KeyEvent.MODIFIER_CONTROL; + } + } + if (event.isMetaDown()) { + if (PlatformUtil.isLinux()) { + ret |= KeyEvent.MODIFIER_WINDOWS; + } else if (PlatformUtil.isMac()) { + ret |= KeyEvent.MODIFIER_COMMAND; + } + } + return ret; + } } diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.h b/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.h index 8fc86d6b4c5..e02d55e2bd7 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.h +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.h @@ -71,6 +71,8 @@ + (void)registerKeyEvent:(NSEvent*)event; + (jint)getKeyCodeForChar:(jchar)c; ++ (void)setMenuKeyEvent:(NSEvent*)event; ++ (BOOL)handleMenuKeyEventForCode:(jint)code modifiers:(jint)modifiers; + (BOOL)syncRenderingDisabled; diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.m index fc9268e82e8..17cb135a680 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.m @@ -82,6 +82,8 @@ // embedded mode. static NSString* awtEmbeddedEvent = @"AWTEmbeddedEvent"; +static NSEvent* menuKeyEvent = nil; + #ifdef STATIC_BUILD jint JNICALL JNI_OnLoad_glass(JavaVM *vm, void *reserved) #else @@ -908,6 +910,30 @@ + (jint)getKeyCodeForChar:(jchar)c; } } ++ (void)setMenuKeyEvent:(NSEvent*)event +{ + if (menuKeyEvent != nil) { + [menuKeyEvent release]; + menuKeyEvent = nil; + } + if (event != nil) { + menuKeyEvent = [event retain]; + } +} + ++ (BOOL)handleMenuKeyEventForCode:(jint)code modifiers:(jint)modifiers; +{ + BOOL result = NO; + if (menuKeyEvent != nil) { + if (code == GetJavaKeyCode(menuKeyEvent) && modifiers == GetJavaKeyModifiers(menuKeyEvent)) { + result = [NSApp.mainMenu performKeyEquivalent: menuKeyEvent]; + [menuKeyEvent release]; + menuKeyEvent = nil; + } + } + return result; +} + + (BOOL)syncRenderingDisabled { return disableSyncRendering; } diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassMenu.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassMenu.m index 97fe78f88f9..0a5dba66362 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassMenu.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassMenu.m @@ -32,6 +32,7 @@ #import "GlassMenu.h" #import "GlassHelper.h" #import "GlassKey.h" +#import "GlassApplication.h" #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -380,6 +381,31 @@ - (void)_setPixels:(jobject)pixels GLASS_CHECK_EXCEPTION(env); } +/* + * Class: com_sun_glass_ui_mac_MacMenuBarDelegate + * Method: _handleKeyEvent + * Signature: (JII)Z + */ +JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_mac_MacMenuBarDelegate__1handleKeyEvent + (JNIEnv *env, jobject jMenuDelegate, jlong jMenubarPtr, jint code, jint modifiers) +{ + LOG("Java_com_sun_glass_ui_mac_MacMenuBarDelegate__1handleKeyEvent"); + + jboolean result = false; + + GLASS_ASSERT_MAIN_JAVA_THREAD(env); + GLASS_POOL_ENTER; + { + if ([GlassApplication handleMenuKeyEventForCode: code modifiers: modifiers]) { + result = true; + } + } + GLASS_POOL_EXIT; + GLASS_CHECK_EXCEPTION(env); + + return result; +} + /* * Class: com_sun_glass_ui_mac_MacMenuDelegate * Method: _initIDs diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassView3D.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassView3D.m index b173e8957c9..85237588cbd 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassView3D.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassView3D.m @@ -361,23 +361,13 @@ - (BOOL)performKeyEquivalent:(NSEvent *)theEvent { KEYLOG("performKeyEquivalent"); - // JDK-8093711, JDK-8094601 Command-EQUALS and Command-DOT needs special casing on Mac - // as it is passed through as two calls to performKeyEquivalent, which in turn - // create extra KeyEvents. - // - // If the user presses Command-"=" on a US keyboard the OS will send that - // to performKeyEquivalent. If it isn't handled it will then send - // Command-"+". This allows a user to invoke Command-"+" without using - // the Shift key. The OS does this for any key where + is the shifted - // character above =. It does something similar with the period key; - // Command-"." leads to Escape for dismissing dialogs. Here we detect and - // ignore the second key event. - if (theEvent != NSApp.currentEvent && NSApp.currentEvent == lastKeyEvent) { - return YES; - } - - BOOL result = [self handleKeyDown: theEvent]; - return result; + // We can return YES here unconditionally. If the scene graph wants to + // invoke the system menu it will call handleKeyEvent in the + // MenuBarDelegate while the KeyEvent is being dispatched. + [GlassApplication setMenuKeyEvent: theEvent]; + [self handleKeyDown: theEvent]; + [GlassApplication setMenuKeyEvent: nil]; + return YES; } - (BOOL)handleKeyDown:(NSEvent *)theEvent diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubToolkit.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubToolkit.java index b7842c82374..03195afa574 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubToolkit.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubToolkit.java @@ -960,6 +960,10 @@ public void setMenus(List menus) { this.menus = menus; } + @Override + public void handleKeyEvent(KeyEvent keyEvent) { + } + // make menus accessible to unit tests public List getMenus() { return menus; diff --git a/tests/system/src/test/java/test/robot/javafx/scene/MenuDoubleShortcutTest.java b/tests/system/src/test/java/test/robot/javafx/scene/MenuDoubleShortcutTest.java index 48f0e6a5107..789a6da15c6 100644 --- a/tests/system/src/test/java/test/robot/javafx/scene/MenuDoubleShortcutTest.java +++ b/tests/system/src/test/java/test/robot/javafx/scene/MenuDoubleShortcutTest.java @@ -63,9 +63,11 @@ private enum TestResult { // We provide an explanation of what happened. Since we only see this // explanation on failure it is worded accordingly. IGNORED("Key press event triggered no actions"), - FIREDTWICE("Key press event fired scene action and also a menu bar item"), FIREDMENUITEM("Key press event fired menu bar item instead of scene action"), - FIREDSCENE("Key press event fired scene action instead of menu bar item"); + FIREDSCENE("Key press event fired scene action instead of menu bar item"), + FIREDBOTH("Key press event fired scene action and also a menu bar item"), + FIREDMENUITEMTWICE("Key press event fired menu bar item twice"), + FIREDSCENETWICE("Key press event fired scene action twice"); private String explanation; TestResult(String e) { @@ -152,14 +154,14 @@ static void exit() { public static class TestApp extends Application { - private boolean sceneAcceleratorFired = false; - private boolean menuBarItemFired = false; + private int sceneAcceleratorFiredCount = 0; + private int menuBarItemFiredCount = 0; private MenuItem createMenuItem(KeyCombination accelerator) { MenuItem menuItem = new MenuItem(accelerator.getName() + " menu item"); menuItem.setAccelerator(accelerator); menuItem.setOnAction(e -> { - menuBarItemFired = true; + menuBarItemFiredCount += 1; e.consume(); }); return menuItem; @@ -182,10 +184,10 @@ public void start(Stage stage) { Scene scene = new Scene(new VBox(menuBar, label), 200, 200); scene.getAccelerators().put(menuBarAndSceneAccelerator, () -> { - sceneAcceleratorFired = true; + sceneAcceleratorFiredCount += 1; }); scene.getAccelerators().put(sceneOnlyAccelerator, () -> { - sceneAcceleratorFired = true; + sceneAcceleratorFiredCount += 1; }); stage.setScene(scene); @@ -198,8 +200,8 @@ public void start(Stage stage) { } public void testKey(KeyCode code) { - sceneAcceleratorFired = false; - menuBarItemFired = false; + sceneAcceleratorFiredCount = 0; + menuBarItemFiredCount = 0; Platform.runLater(() -> { KeyCode shortcutCode = (PlatformUtil.isMac() ? KeyCode.COMMAND : KeyCode.CONTROL); Robot robot = new Robot(); @@ -211,11 +213,17 @@ public void testKey(KeyCode code) { } public TestResult testResult() { - if (sceneAcceleratorFired && menuBarItemFired) { - return TestResult.FIREDTWICE; - } else if (sceneAcceleratorFired) { + if (menuBarItemFiredCount > 1) { + return TestResult.FIREDMENUITEMTWICE; + } + else if (sceneAcceleratorFiredCount > 1) { + return TestResult.FIREDSCENETWICE; + } + else if (sceneAcceleratorFiredCount == 1 && menuBarItemFiredCount == 1) { + return TestResult.FIREDBOTH; + } else if (sceneAcceleratorFiredCount == 1) { return TestResult.FIREDSCENE; - } else if (menuBarItemFired) { + } else if (menuBarItemFiredCount == 1) { return TestResult.FIREDMENUITEM; } return TestResult.IGNORED;