diff --git a/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java b/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java index 515ec9440e9..ddcc31bd1cb 100644 --- a/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java +++ b/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Function; import javafx.animation.Animation; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; @@ -42,6 +43,7 @@ import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.WritableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; @@ -67,7 +69,6 @@ import javafx.scene.AccessibleRole; import javafx.scene.Node; import javafx.scene.control.ContextMenu; -import javafx.scene.control.Control; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.RadioMenuItem; @@ -162,10 +163,10 @@ private enum TabAnimationState { /** * Creates a new TabPaneSkin instance, installing the necessary child - * nodes into the Control {@link Control#getChildren() children} list, as + * nodes into the Control {@link TabPane#getChildren() children} list, as * well as the necessary input mappings for handling key, mouse, etc events. * - * @param control The control that this skin should be installed onto. + * @param control The TabPane that this skin should be installed onto. */ public TabPaneSkin(TabPane control) { super(control); @@ -256,6 +257,45 @@ public TabPaneSkin(TabPane control) { } }; + /** + * This property allows to control the graphic for the overflow menu items, + * by generating graphic {@code Node}s when the menu is shown. + *

+ * When this property is {@code null}, the menu provides only the basic graphic copied from the corresponding + * {@link Tab} - either an {@link ImageView} or a {@link Label} with an {@link ImageView} as its graphic. + *

+ * Changing this property while the menu is shown has no effect. + * + * @since 25 + * @defaultValue null + */ + private ObjectProperty> menuGraphicFactory; + + public final ObjectProperty> menuGraphicFactoryProperty() { + if (menuGraphicFactory == null) { + menuGraphicFactory = new SimpleObjectProperty<>() { + @Override + public Object getBean() { + return TabPaneSkin.this; + } + + @Override + public String getName() { + return "menuGraphicFactory"; + } + }; + } + return menuGraphicFactory; + } + + public final Function getMenuGraphicFactory() { + return menuGraphicFactory == null ? null : menuGraphicFactory.get(); + } + + public final void setMenuGraphicFactory(Function f) { + menuGraphicFactoryProperty().set(f); + } + /* ************************************************************************* * * * Public API * @@ -485,27 +525,28 @@ private static int getRotation(Side pos) { } } - /** - * VERY HACKY - this lets us 'duplicate' Label and ImageView nodes to be used in a - * Tab and the tabs menu at the same time. - */ - private static Node clone(Node n) { - if (n == null) { - return null; + private Node prepareGraphic(Tab t) { + Function f = getMenuGraphicFactory(); + if (f != null) { + return f.apply(t); } - if (n instanceof ImageView) { - ImageView iv = (ImageView) n; + + Node n = t.getGraphic(); + return extractGraphic(n); + } + + private Node extractGraphic(Node n) { + if (n instanceof ImageView v) { ImageView imageview = new ImageView(); - imageview.imageProperty().bind(iv.imageProperty()); + imageview.imageProperty().bind(v.imageProperty()); return imageview; - } - if (n instanceof Label) { - Label l = (Label)n; - Label label = new Label(l.getText(), clone(l.getGraphic())); + } else if (n instanceof Label l) { + Label label = new Label(l.getText(), extractGraphic(l.getGraphic())); label.textProperty().bind(l.textProperty()); return label; + } else { + return null; } - return null; } private void removeTabs(List removedList) { @@ -1509,7 +1550,6 @@ public void handle(MouseEvent me) { }); getProperties().put(Tab.class, tab); - getProperties().put(ContextMenu.class, tab.getContextMenu()); setOnContextMenuRequested((ContextMenuEvent me) -> { if (getTab().getContextMenu() != null) { @@ -1767,8 +1807,6 @@ public TabControlButtons() { showPopupMenu(); }); - setupPopupMenu(); - inner = new StackPane() { @Override protected double computePrefWidth(double height) { double pw; @@ -1830,7 +1868,6 @@ private void positionArrow(Pane btn, StackPane arrow, double x, double y, double showControlButtons = true; requestLayout(); } - getProperties().put(ContextMenu.class, popup); } InvalidationListener sidePropListener = e -> { @@ -1928,7 +1965,8 @@ private void setupPopupMenu() { ToggleGroup group = new ToggleGroup(); ObservableList menuitems = FXCollections.observableArrayList(); for (final Tab tab : getSkinnable().getTabs()) { - TabMenuItem item = new TabMenuItem(tab); + Node graphic = prepareGraphic(tab); + TabMenuItem item = new TabMenuItem(tab, graphic); item.setToggleGroup(group); item.setOnAction(t -> getSkinnable().getSelectionModel().select(tab)); menuitems.add(item); @@ -1944,6 +1982,9 @@ private void clearPopupMenu() { } private void showPopupMenu() { + if (popup == null) { + setupPopupMenu(); + } for (MenuItem mi: popup.getItems()) { TabMenuItem tmi = (TabMenuItem)mi; if (selectedTab.equals(tmi.getTab())) { @@ -1953,25 +1994,23 @@ private void showPopupMenu() { } popup.show(downArrowBtn, Side.BOTTOM, 0, 0); } - } /* End TabControlButtons*/ - - static class TabMenuItem extends RadioMenuItem { - Tab tab; - private InvalidationListener disableListener = new InvalidationListener() { - @Override public void invalidated(Observable o) { - setDisable(tab.isDisable()); + private ContextMenu test_getTabsMenu() { + if (popup == null) { + setupPopupMenu(); } - }; + return popup; + } + } /* End TabControlButtons*/ - private WeakInvalidationListener weakDisableListener = - new WeakInvalidationListener(disableListener); + /** The MenuItem for use in the overflow menu */ + static class TabMenuItem extends RadioMenuItem { + private Tab tab; - public TabMenuItem(final Tab tab) { - super(tab.getText(), TabPaneSkin.clone(tab.getGraphic())); + public TabMenuItem(Tab tab, Node graphic) { + super(tab.getText(), graphic); this.tab = tab; - setDisable(tab.isDisable()); - tab.disableProperty().addListener(weakDisableListener); + disableProperty().bind(tab.disableProperty()); textProperty().bind(tab.textProperty()); } @@ -1979,9 +2018,10 @@ public Tab getTab() { return tab; } + // is this really necessary? public void dispose() { textProperty().unbind(); - tab.disableProperty().removeListener(weakDisableListener); + disableProperty().unbind(); tab = null; } } @@ -2347,7 +2387,7 @@ private void stopAnim(Animation anim) { // For testing purpose. ContextMenu test_getTabsMenu() { - return tabHeaderArea.controlButtons.popup; + return tabHeaderArea.controlButtons.test_getTabsMenu(); } void test_disableAnimations() { diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/TabPaneTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/TabPaneTest.java index 2333ed7698f..27c437c4881 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/TabPaneTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/TabPaneTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 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 @@ -41,6 +41,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.function.Function; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; @@ -55,9 +56,11 @@ import javafx.geometry.Bounds; import javafx.geometry.Side; import javafx.scene.Group; +import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.SelectionModel; import javafx.scene.control.SingleSelectionModel; @@ -73,6 +76,7 @@ import javafx.scene.input.ScrollEvent; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.scene.shape.Path; import javafx.stage.Stage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -1338,4 +1342,49 @@ private void scrollTabPane(Side side, double deltaX, double deltaY) { assertEquals(firstTabBounds.getMinY() - deltaY, newFirstTabBounds.getMinY(), 0); } } + + private ContextMenu setupMenuGraphicFactory() { + TabPaneSkin skin = new TabPaneSkin(tabPane); + skin.setMenuGraphicFactory(new Function() { + @Override + public Node apply(Tab t) { + return new Path(); + } + }); + tabPane.setSkin(skin); + + tabPane.setMaxSize(20, 20); + root.getChildren().add(tabPane); + tabPane.getTabs().addAll(tab1, tab2, tab3); + show(); + tk.firePulse(); + + ContextMenu menu = TabPaneSkinShim.getTabsMenu(skin); + assertNotNull(menu); + assertEquals(3, menu.getItems().size()); + return menu; + } + + @Test + public void menuGraphicFactory() { + ContextMenu menu = setupMenuGraphicFactory(); + for (MenuItem mi : menu.getItems()) { + assertTrue(mi.getGraphic() instanceof Path); + } + } + + @Test + public void menuBindings() { + ContextMenu menu = setupMenuGraphicFactory(); + MenuItem mi = menu.getItems().get(0); + + assertFalse(mi.isDisable()); + assertEquals("one", mi.getText()); + + tab1.setText("yo"); + tab1.setDisable(true); + + assertTrue(mi.isDisable()); + assertEquals("yo", mi.getText()); + } }