diff --git a/modules/javafx.controls/src/main/java/javafx/scene/control/MenuBar.java b/modules/javafx.controls/src/main/java/javafx/scene/control/MenuBar.java index b60822fac6d..346d3a7c26a 100644 --- a/modules/javafx.controls/src/main/java/javafx/scene/control/MenuBar.java +++ b/modules/javafx.controls/src/main/java/javafx/scene/control/MenuBar.java @@ -172,6 +172,49 @@ public final boolean isUseSystemMenuBar() { return useSystemMenuBar == null ? false : useSystemMenuBar.getValue(); } + /** + * Remove any default menus that are normally added to the system + * menu bar. Only effective when the system menu bar is in use. + * + * @return the use default menus property + * @since JavaFX 24 + */ + public final BooleanProperty useDefaultMenusProperty() { + if (useDefaultMenus == null) { + useDefaultMenus = new StyleableBooleanProperty(true) { + + @Override + public CssMetaData getCssMetaData() { + return StyleableProperties.USE_DEFAULT_MENUS; + } + + @Override + public Object getBean() { + return MenuBar.this; + } + + @Override + public String getName() { + return "useDefaultMenus"; + } + + @Override + public void bind(final ObservableValue rawObservable) { + throw new RuntimeException("cannot uni-directionally bind to use default menus - use bindBidirectional instead"); + } + + }; + } + return useDefaultMenus; + } + + private BooleanProperty useDefaultMenus; + public final void setUseDefaultMenus(boolean value) { + useDefaultMenusProperty().setValue(value); + } + public final boolean isUseDefaultMenus() { + return useDefaultMenus == null ? true : useDefaultMenus.getValue(); + } /* ************************************************************************* * * @@ -216,6 +259,19 @@ private static class StyleableProperties { } }; + private static final CssMetaData USE_DEFAULT_MENUS = + new CssMetaData<>("-fx-use-default-menus", + BooleanConverter.getInstance(), + true) { + @Override public boolean isSettable(MenuBar n) { + return n.useDefaultMenus == null || !n.useDefaultMenus.isBound(); + } + + @Override public StyleableProperty getStyleableProperty(MenuBar n) { + return (StyleableProperty)n.useDefaultMenusProperty(); + } + }; + private static final List> STYLEABLES; static { final List> styleables = diff --git a/modules/javafx.controls/src/main/java/javafx/scene/control/skin/MenuBarSkin.java b/modules/javafx.controls/src/main/java/javafx/scene/control/skin/MenuBarSkin.java index cb38c98857f..59b1db73d60 100644 --- a/modules/javafx.controls/src/main/java/javafx/scene/control/skin/MenuBarSkin.java +++ b/modules/javafx.controls/src/main/java/javafx/scene/control/skin/MenuBarSkin.java @@ -259,6 +259,15 @@ public MenuBarSkin(final MenuBar control) { rebuildUI(); }); + if (Toolkit.getToolkit().getSystemMenu().isSupported()) { + lh.addInvalidationListener(control.useSystemMenuBarProperty(), (v) -> { + rebuildUI(); + }); + lh.addInvalidationListener(control.useDefaultMenusProperty(), (v) -> { + rebuildUI(); + }); + } + // When the mouse leaves the menu, the last hovered item should lose // it's focus so that it is no longer selected. This code returns focus // to the MenuBar itself, such that keyboard navigation can continue. @@ -541,6 +550,9 @@ private static void initSystemMenuBar() { stages.addListener((ListChangeListener) c -> { while (c.next()) { for (Window stage : c.getRemoved()) { + if (stage == currentMenuBarStage) { + setSystemMenu(null); + } stage.focusedProperty().removeListener(focusedStageListener); } for (Window stage : c.getAddedSubList()) { @@ -848,6 +860,9 @@ private void rebuildUI() { cleanUpListeners(); if (Toolkit.getToolkit().getSystemMenu().isSupported()) { + boolean useDefaultMenus = !getSkinnable().isUseSystemMenuBar() || getSkinnable().isUseDefaultMenus(); + Toolkit.getToolkit().getSystemMenu().setUseDefaultMenus(useDefaultMenus); + final Scene scene = getSkinnable().getScene(); if (scene != null) { // JDK-8094110 - make sure system menu is updated when this MenuBar's scene changes. diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java index b3e9f42e0e9..720b3d4d005 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java @@ -334,6 +334,11 @@ public void installDefaultMenus(MenuBar menubar) { // To override in subclasses } + public void removeDefaultMenus(MenuBar menubar) { + checkEventThread(); + // To override in subclasses + } + public EventHandler getEventHandler() { //checkEventThread(); // Glass (Mac) // When an app is closing, Mac calls notify- Will/DidHide, Will/DidResignActive @@ -813,6 +818,21 @@ public Map> getPlatformKeys() { */ public void checkPlatformPreferencesSupport() {} + /** + * Hides the current application. Only implemented on Mac. + */ + public void hideApplication() {} + + /** + * Hides all other applications. Only implemented on Mac. + */ + public void hideOtherApplications() {} + + /** + * Undoes the effects of hideOtherApplications. Only implemented on Mac. + */ + public void showAllApplications() {} + private static native void _overrideNativeWindowHandle(Class lwFrameWrapperClass, Object frame, long handle, Runnable closeWindow); diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java index a9d93a75bf5..7f8ce1b0c59 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java @@ -219,6 +219,18 @@ private void setEventThread() { native private void _hideOtherApplications(); native private void _unhideAllApplications(); + @Override public void hideApplication() { + _hide(); + } + + @Override public void hideOtherApplications() { + _hideOtherApplications(); + } + + @Override public void showAllApplications() { + _unhideAllApplications(); + } + public void installAppleMenu(MenuBar menubar) { this.appleMenu = createMenu("Apple"); @@ -263,7 +275,7 @@ public void installAppleMenu(MenuBar menubar) { }, 'q', KeyEvent.MODIFIER_COMMAND); this.appleMenu.add(quitMenu); - menubar.add(this.appleMenu); + menubar.insert(this.appleMenu, 0); } public Menu getAppleMenu() { @@ -274,6 +286,12 @@ public Menu getAppleMenu() { installAppleMenu(menubar); } + @Override public void removeDefaultMenus(MenuBar menubar) { + if (appleMenu != null) { + menubar.remove(appleMenu); + appleMenu = null; + } + } // FACTORY METHODS diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/PlatformImpl.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/PlatformImpl.java index 0a03c37cceb..d0f1ed47f8f 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/PlatformImpl.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/PlatformImpl.java @@ -982,6 +982,27 @@ private static void checkHighContrastThemeChanged(Map preference } } + public static void hideApplication() { + var application = com.sun.glass.ui.Application.GetApplication(); + if (application != null) { + application.hideApplication(); + } + } + + public static void hideOtherApplications() { + var application = com.sun.glass.ui.Application.GetApplication(); + if (application != null) { + application.hideOtherApplications(); + } + } + + public static void showAllApplications() { + var application = com.sun.glass.ui.Application.GetApplication(); + if (application != null) { + application.showAllApplications(); + } + } + /** * The maximum number of nested event loops. */ 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..a4756e27c8a 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 @@ -43,6 +43,7 @@ public interface TKSystemMenu { */ public boolean isSupported(); + public void setUseDefaultMenus(boolean use); public void setMenus(List menus); } 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..c62f1515116 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 @@ -61,6 +61,7 @@ class GlassSystemMenu implements TKSystemMenu { private List systemMenus = null; private MenuBar glassSystemMenuBar = null; + private boolean useDefaultMenus = true; private final Map> menuListeners = new HashMap<>(); private final Map, ObservableList> listenerItems = new HashMap<>(); private BooleanProperty active; @@ -75,8 +76,9 @@ protected void createMenuBar() { if (glassSystemMenuBar == null) { Application app = Application.GetApplication(); glassSystemMenuBar = app.createMenuBar(); - app.installDefaultMenus(glassSystemMenuBar); - + if (useDefaultMenus) { + app.installDefaultMenus(glassSystemMenuBar); + } if (systemMenus != null) { setMenus(systemMenus); } @@ -91,6 +93,19 @@ protected MenuBar getMenuBar() { return Application.GetApplication().supportsSystemMenu(); } + @Override public void setUseDefaultMenus(boolean use) { + if (use != useDefaultMenus) { + if (glassSystemMenuBar != null) { + if (useDefaultMenus) { + Application.GetApplication().removeDefaultMenus(glassSystemMenuBar); + } else { + Application.GetApplication().installDefaultMenus(glassSystemMenuBar); + } + } + useDefaultMenus = use; + } + } + @Override public void setMenus(List menus) { if (active != null) { active.set(false); @@ -99,6 +114,14 @@ protected MenuBar getMenuBar() { systemMenus = menus; if (glassSystemMenuBar != null) { + /* + * If we added default menus to ensure the menu bar did + * not become empty remove them now. + */ + if (!useDefaultMenus) { + Application.GetApplication().removeDefaultMenus(glassSystemMenuBar); + } + /* * Remove existing menus */ @@ -106,9 +129,10 @@ protected MenuBar getMenuBar() { int existingSize = existingMenus.size(); /* - * Leave the Apple menu in place + * Leave the Apple menu in place, if using */ - for (int index = existingSize - 1; index >= 1; index--) { + int limit = (useDefaultMenus ? 1 : 0); + for (int index = existingSize - 1; index >= limit; index--) { Menu menu = existingMenus.get(index); clearMenu(menu); glassSystemMenuBar.remove(index); @@ -117,6 +141,13 @@ protected MenuBar getMenuBar() { for (MenuBase menu : menus) { addMenu(null, menu); } + + /* + * Do not let the menu bar become empty. + */ + if (existingMenus.size() == 0) { + Application.GetApplication().installDefaultMenus(glassSystemMenuBar); + } } } diff --git a/modules/javafx.graphics/src/main/java/javafx/application/ApplicationServices.java b/modules/javafx.graphics/src/main/java/javafx/application/ApplicationServices.java new file mode 100644 index 00000000000..182b8d40eb9 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/application/ApplicationServices.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.application; + +import com.sun.javafx.application.PlatformImpl; + +/** + * This class provides services for an Application. This includes + * methods to show and hide other applications. + * + * @since JavaFX 24 + */ +public final class ApplicationServices { + + // To prevent instantiation + private ApplicationServices() { + } + + /** + * Hide the application. + */ + public static void hideApplication() { + PlatformImpl.hideApplication(); + } + + /** + * Hide applications other than the current one. + */ + public static void hideOtherApplications() { + PlatformImpl.hideOtherApplications(); + } + + /** + * Show all applications. + */ + public static void showAllApplications() { + PlatformImpl.showAllApplications(); + } + +} 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..cc3fbdc1baf 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassMenu.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassMenu.m @@ -52,13 +52,6 @@ static jfieldID jPixelsScaleXField = 0; static jfieldID jPixelsScaleYField = 0; -@interface NSMenuItem (SPI) - -// Apple's SPI -- setAppleMenu:(NSMenuItem*)item; - -@end - @implementation GlassMenubar - (id)init @@ -342,12 +335,6 @@ - (void)_setPixels:(jobject)pixels [glassmenu->menu setAutoenablesItems:YES]; - if ([[glassmenu->item title] compare:@"Apple"] == NSOrderedSame) - { - LOG("calling setAppleMenu"); - [NSApp performSelector:@selector(setAppleMenu:) withObject:glassmenu->item]; - } - [[NSApp mainMenu] update]; } GLASS_POOL_EXIT; 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 4c84b92f661..dbc48b48ca9 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 @@ -929,6 +929,10 @@ public boolean isSupported() { // return (os != null && os.startsWith("Mac")); } + @Override + public void setUseDefaultMenus(boolean use) { + } + @Override public void setMenus(List menus) { this.menus = menus; diff --git a/tests/manual/controls/DefaultAppMenu.java b/tests/manual/controls/DefaultAppMenu.java new file mode 100644 index 00000000000..ca5d3e841bc --- /dev/null +++ b/tests/manual/controls/DefaultAppMenu.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.util.ArrayList; +import java.util.List; +import javafx.application.Application; +import javafx.application.ApplicationServices; +import javafx.application.Platform; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.TextArea; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.layout.VBox; +import javafx.scene.layout.Priority; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class DefaultAppMenu extends Application { + private final TextArea messageArea = new TextArea(""); + private final MenuBar menuBar = new MenuBar(); + private final Menu applicationMenu = new Menu("Ignored"); + private final Menu testMenu = new Menu("Test"); + private final MenuItem toggleSystemMenus = new MenuItem(); + private final MenuItem toggleDefaultMenus = new MenuItem(); + + private Scene scene = null; + + public static void main(String[] args) { + Application.launch(DefaultAppMenu.class, args); + } + + private void updateUI() { + if (menuBar.isUseSystemMenuBar()) { + toggleSystemMenus.setText("Turn off system menu bar"); + } else { + toggleSystemMenus.setText("Turn on system menu bar"); + } + + if (menuBar.isUseDefaultMenus()) { + toggleDefaultMenus.setText("Hide default menus"); + } else { + toggleDefaultMenus.setText("Show default menus"); + } + + if (menuBar.isUseSystemMenuBar()) { + messageArea.appendText("System menu bar is ON and "); + } else { + messageArea.appendText("System menu bar is OFF and "); + } + + if (menuBar.isUseDefaultMenus()) { + messageArea.appendText("default menus are ON\n"); + } else { + messageArea.appendText("default menus are OFF\n"); + } + + if (!menuBar.isUseSystemMenuBar() || menuBar.isUseDefaultMenus()) { + messageArea.appendText("Using default application menu\n"); + menuBar.getMenus().remove(applicationMenu); + } else { + messageArea.appendText("Using custom application menu\n"); + if (!menuBar.getMenus().contains(applicationMenu)) { + var toPrepend = new ArrayList(); + toPrepend.add(applicationMenu); + menuBar.getMenus().addAll(0, toPrepend); + } + } + } + + private MenuItem addItem(Menu menu, String title) { + var item = new MenuItem(title); + item.setOnAction(e -> { + messageArea.appendText(title + "\n"); + }); + menu.getItems().add(item); + return item; + } + + private void buildApplicationMenu() { + var item = addItem(applicationMenu, "Custom menu item"); + item.setAccelerator(new KeyCodeCombination(KeyCode.E, KeyCombination.SHORTCUT_DOWN)); + item.setOnAction(e -> { + messageArea.appendText("Custom menu item\n"); + }); + + applicationMenu.getItems().add(new SeparatorMenuItem()); + + item = addItem(applicationMenu, "Hide DefaultAppMenu"); + item.setAccelerator(new KeyCodeCombination(KeyCode.H, KeyCombination.SHORTCUT_DOWN)); + item.setOnAction(e -> { + ApplicationServices.hideApplication(); + }); + + item = addItem(applicationMenu, "Hide Others"); + item.setAccelerator(new KeyCodeCombination(KeyCode.H, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)); + item.setOnAction(e -> { + ApplicationServices.hideOtherApplications(); + }); + + item = addItem(applicationMenu, "Show All"); + item.setOnAction(e -> { + ApplicationServices.showAllApplications(); + }); + + applicationMenu.getItems().add(new SeparatorMenuItem()); + + item = addItem(applicationMenu, "Quit DefaultAppMenu"); + item.setAccelerator(new KeyCodeCombination(KeyCode.Q, KeyCombination.SHORTCUT_DOWN)); + item.setOnAction(e -> { + Platform.exit(); + }); + } + + private Menu buildTestMenu() { + toggleSystemMenus.setAccelerator(new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN)); + toggleSystemMenus.setOnAction(e -> { + if (menuBar.isUseSystemMenuBar()) { + menuBar.setUseSystemMenuBar(false); + updateUI(); + } else { + menuBar.setUseSystemMenuBar(true); + updateUI(); + } + }); + testMenu.getItems().add(toggleSystemMenus); + + toggleDefaultMenus.setAccelerator(new KeyCodeCombination(KeyCode.D, KeyCombination.SHORTCUT_DOWN)); + toggleDefaultMenus.setOnAction(e -> { + if (menuBar.isUseDefaultMenus()) { + menuBar.setUseDefaultMenus(false); + updateUI(); + } else { + menuBar.setUseDefaultMenus(true); + updateUI(); + } + }); + testMenu.getItems().add(toggleDefaultMenus); + + testMenu.getItems().add(new SeparatorMenuItem()); + + addItem(testMenu, "Test item one"); + addItem(testMenu, "Test item two"); + addItem(testMenu, "Test item three"); + addItem(testMenu, "Test item four"); + + return testMenu; + } + + @Override + public void start(Stage stage) { + messageArea.setEditable(false); + messageArea.appendText("Use items in the Test menu to test the system menu bar\n"); + + buildApplicationMenu(); + buildTestMenu(); + + menuBar.getMenus().add(testMenu); + menuBar.setUseSystemMenuBar(true); + updateUI(); + + var box = new VBox(menuBar, messageArea); + box.setVgrow(messageArea, Priority.ALWAYS); + scene = new Scene(box, 640, 640); + + stage.setScene(scene); + stage.setTitle("Menu Key Test"); + stage.show(); + } +}