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 extends Tab> 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());
+ }
}