Skip to content

Migrate to JDK 21 and OpenJFX 21#13

Open
hkmoon wants to merge 2 commits intomasterfrom
jdk21
Open

Migrate to JDK 21 and OpenJFX 21#13
hkmoon wants to merge 2 commits intomasterfrom
jdk21

Conversation

@hkmoon
Copy link
Copy Markdown
Owner

@hkmoon hkmoon commented Mar 22, 2026

Summary

  • Replace all com.sun.* internal JavaFX API usage with public APIs, enabling compatibility with JDK 21's strong encapsulation
  • Rewrite ContentTabPaneSkin from a 2500-line copy of internal TabPaneSkin to a 57-line class extending the public javafx.scene.control.skin.TabPaneSkin
  • Update build tooling (Gradle 8.5 + OpenJFX plugin, Maven with OpenJFX 21.0.2 deps) and add JPMS module-info.java

Files changed

File Change
build.gradle JDK 21 target, OpenJFX 21.0.2 plugin, Gradle 8.5
pom.xml JDK 21 target, OpenJFX 21.0.2 dependencies, updated plugins
settings.gradle Created (was missing)
gradle-wrapper.properties Gradle 4.10.2 → 8.5
gradle.properties Removed unsupported -XX:MaxPermSize flag
module-info.java JPMS module descriptor for org.dockfx
ContentTabPaneSkin.java Rewritten to extend public TabPaneSkin
DockTitleBar.java StageHelper.getStages()Window.getWindows()
DockPane.java StyleManagergetStylesheets().add()
DockEvent.java InputEventUtils → manual localToScene()

Test plan

  • Verified ./gradlew build passes with JDK 21.0.10
  • Confirmed zero com.sun.* imports remain
  • Confirmed zero deprecation warnings
  • Manual test: launch demo app (org.dockfx.demo.DockFX) and verify docking, undocking, tab reordering, and window dragging

🤖 Generated with Claude Code

HongKee Moon and others added 2 commits September 14, 2022 11:16
Replace all com.sun.* internal JavaFX API usage with public APIs,
update build tooling, and add JPMS module descriptor.

- Rewrite ContentTabPaneSkin to extend public TabPaneSkin (2500→57 lines)
- Replace StageHelper.getStages() with Window.getWindows() in DockTitleBar
- Replace StyleManager with getStylesheets().add() in DockPane
- Replace InputEventUtils.recomputeCoordinates() with Node.localToScene() in DockEvent
- Update build.gradle: JDK 21, OpenJFX 21.0.2 plugin, Gradle 8.5
- Update pom.xml: JDK 21, OpenJFX 21.0.2 dependencies, updated plugins
- Create module-info.java (org.dockfx module)
- Create settings.gradle
- Remove unsupported -XX:MaxPermSize JVM flag

Constraint: JavaFX was unbundled from JDK after Java 10, requiring explicit OpenJFX deps
Constraint: com.sun.* APIs are inaccessible under JDK 21 strong encapsulation
Rejected: --add-opens JVM flags | fragile, breaks on future JDK updates
Rejected: Drop custom TabPaneSkin entirely | loses DockFX-specific tab behavior
Confidence: high
Scope-risk: broad

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 22, 2026 20:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates DockFX to JDK 21 / OpenJFX 21 by removing internal com.sun.* JavaFX usages, modernizing build tooling, and introducing a JPMS module descriptor to stay compatible with strong encapsulation.

Changes:

  • Replaced internal JavaFX API usage with public equivalents (Window.getWindows(), per-node stylesheets, manual coordinate conversion).
  • Rewrote ContentTabPaneSkin to extend public javafx.scene.control.skin.TabPaneSkin instead of vendoring an internal skin.
  • Updated Gradle/Maven build configuration for Java 21 + OpenJFX 21.0.2 and added module-info.java.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java Replaces a large internal-skin copy with a small subclass and adds DockFX-specific tab text handling.
src/main/java/org/dockfx/DockTitleBar.java Replaces StageHelper.getStages() with public Window.getWindows() traversal.
src/main/java/org/dockfx/DockPane.java Moves DockFX stylesheet application away from internal StyleManager and adjusts the legacy initialization API.
src/main/java/org/dockfx/DockEvent.java Replaces internal input coordinate recomputation with localToScene() logic.
src/main/java/module-info.java Adds JPMS module descriptor with required JavaFX modules + exports/opens.
settings.gradle Adds missing Gradle settings with rootProject.name.
pom.xml Updates Java target to 21 and adds OpenJFX 21 dependencies/plugins.
gradle/wrapper/gradle-wrapper.properties Upgrades Gradle wrapper to 8.5.
gradle/master.gradle Updates version map/formatting for ClearControl tooling.
gradle.properties Removes obsolete -XX:MaxPermSize JVM flag.
build.gradle Modernizes Gradle build (Java 21, OpenJFX plugin, application/publishing configuration).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +50 to 55
private void bindTabText(Tab tab)
{
private Rectangle headerClip;
private StackPane headersRegion;
private StackPane headerBackground;
private TabControlButtons controlButtons;

private boolean measureClosingTabs = false;

private double scrollOffset;

public TabHeaderArea()
if (tab instanceof DockNodeTab)
{
getStyleClass().setAll("tab-header-area");
setManaged(false);
final TabPane tabPane = getSkinnable();

headerClip = new Rectangle();

headersRegion = new StackPane()
{
@Override
protected double computePrefWidth(double height)
{
double width = 0.0F;
for (Node child : getChildren())
{
TabHeaderSkin tabHeaderSkin = (TabHeaderSkin) child;
if (tabHeaderSkin.isVisible()
&& (measureClosingTabs || !tabHeaderSkin.isClosing))
{
width += tabHeaderSkin.prefWidth(height);
}
}
return snapSize(width) + snappedLeftInset()
+ snappedRightInset();
}

@Override
protected double computePrefHeight(double width)
{
double height = 0.0F;
for (Node child : getChildren())
{
TabHeaderSkin tabHeaderSkin = (TabHeaderSkin) child;
height =
Math.max(height, tabHeaderSkin.prefHeight(width));
}
return snapSize(height) + snappedTopInset()
+ snappedBottomInset();
}

@Override
protected void layoutChildren()
{
if (tabsFit())
{
setScrollOffset(0.0);
}
else
{
if (!removeTab.isEmpty())
{
double offset = 0;
double w = tabHeaderArea.getWidth()
- snapSize(controlButtons.prefWidth(-1))
- firstTabIndent()
- SPACER;
Iterator<Node> i = getChildren().iterator();
while (i.hasNext())
{
TabHeaderSkin tabHeader = (TabHeaderSkin) i.next();
double tabHeaderPrefWidth =
snapSize(tabHeader.prefWidth(-1));
if (removeTab.contains(tabHeader))
{
if (offset < w)
{
isSelectingTab = true;
}
i.remove();
removeTab.remove(tabHeader);
if (removeTab.isEmpty())
{
break;
}
}
offset += tabHeaderPrefWidth;
}
// } else {
// isSelectingTab = true;
}
}

if (isSelectingTab)
{
ensureSelectedTabIsVisible();
isSelectingTab = false;
}
else
{
validateScrollOffset();
}

Side tabPosition = getSkinnable().getSide();
double tabBackgroundHeight = snapSize(prefHeight(-1));
double tabX =
(tabPosition.equals(Side.LEFT)
|| tabPosition.equals(Side.BOTTOM)) ? snapSize(getWidth())
- getScrollOffset()
: getScrollOffset();

updateHeaderClip();
for (Node node : getChildren())
{
TabHeaderSkin tabHeader = (TabHeaderSkin) node;

// size and position the header relative to the other headers
double tabHeaderPrefWidth =
snapSize(tabHeader.prefWidth(-1)
* tabHeader.animationTransition.get());
double tabHeaderPrefHeight =
snapSize(tabHeader.prefHeight(-1));
tabHeader.resize(tabHeaderPrefWidth, tabHeaderPrefHeight);

// This ensures that the tabs are located in the correct position
// when there are tabs of differing heights.
double startY =
tabPosition.equals(Side.BOTTOM) ? 0
: tabBackgroundHeight
- tabHeaderPrefHeight
- snappedBottomInset();
if (tabPosition.equals(Side.LEFT)
|| tabPosition.equals(Side.BOTTOM))
{
// build from the right
tabX -= tabHeaderPrefWidth;
tabHeader.relocate(tabX, startY);
}
else
{
// build from the left
tabHeader.relocate(tabX, startY);
tabX += tabHeaderPrefWidth;
}
}
}

};
headersRegion.getStyleClass().setAll("headers-region");
headersRegion.setClip(headerClip);

headerBackground = new StackPane();
headerBackground.getStyleClass()
.setAll("tab-header-background");

int i = 0;
for (Tab tab : tabPane.getTabs())
{
addTab(tab, i++);
}

controlButtons = new TabControlButtons();
controlButtons.setVisible(false);
if (controlButtons.isVisible())
{
controlButtons.setVisible(true);
}
getChildren().addAll(headerBackground,
headersRegion,
controlButtons);

// support for mouse scroll of header area (for when the tabs exceed
// the available space)
addEventHandler(ScrollEvent.SCROLL, (ScrollEvent e) -> {
Side side = getSkinnable().getSide();
side = side == null ? Side.TOP : side;
switch (side)
{
default:
case TOP:
case BOTTOM:
setScrollOffset(scrollOffset - e.getDeltaY());
break;
case LEFT:
case RIGHT:
setScrollOffset(scrollOffset + e.getDeltaY());
break;
}

});
}

private void updateHeaderClip()
{
Side tabPosition = getSkinnable().getSide();

double x = 0;
double y = 0;
double clipWidth = 0;
double clipHeight = 0;
double maxWidth = 0;
double shadowRadius = 0;
double clipOffset = firstTabIndent();
double controlButtonPrefWidth =
snapSize(controlButtons.prefWidth(-1));

measureClosingTabs = true;
double headersPrefWidth = snapSize(headersRegion.prefWidth(-1));
measureClosingTabs = false;

double headersPrefHeight =
snapSize(headersRegion.prefHeight(-1));

// Add the spacer if isShowTabsMenu is true.
if (controlButtonPrefWidth > 0)
{
controlButtonPrefWidth = controlButtonPrefWidth + SPACER;
}

if (headersRegion.getEffect() instanceof DropShadow)
{
DropShadow shadow = (DropShadow) headersRegion.getEffect();
shadowRadius = shadow.getRadius();
}

maxWidth = snapSize(getWidth()) - controlButtonPrefWidth
- clipOffset;
if (tabPosition.equals(Side.LEFT)
|| tabPosition.equals(Side.BOTTOM))
{
if (headersPrefWidth < maxWidth)
{
clipWidth = headersPrefWidth + shadowRadius;
}
else
{
x = headersPrefWidth - maxWidth;
clipWidth = maxWidth + shadowRadius;
}
clipHeight = headersPrefHeight;
}
else
{
// If x = 0 the header region's drop shadow is clipped.
x = -shadowRadius;
clipWidth = (headersPrefWidth < maxWidth ? headersPrefWidth
: maxWidth)
+ shadowRadius;
clipHeight = headersPrefHeight;
}

headerClip.setX(x);
headerClip.setY(y);
headerClip.setWidth(clipWidth);
headerClip.setHeight(clipHeight);
}

private void addTab(Tab tab, int addToIndex)
{
TabHeaderSkin tabHeaderSkin = new TabHeaderSkin(tab);
headersRegion.getChildren().add(addToIndex, tabHeaderSkin);
}

private List<TabHeaderSkin> removeTab = new ArrayList<>();

private void removeTab(Tab tab)
{
TabHeaderSkin tabHeaderSkin = getTabHeaderSkin(tab);
if (tabHeaderSkin != null)
{
if (tabsFit())
{
headersRegion.getChildren().remove(tabHeaderSkin);
}
else
{
// The tab will be removed during layout because
// we need its width to compute the scroll offset.
removeTab.add(tabHeaderSkin);
tabHeaderSkin.removeListeners(tab);
}
}
}

private TabHeaderSkin getTabHeaderSkin(Tab tab)
{
for (Node child : headersRegion.getChildren())
{
TabHeaderSkin tabHeaderSkin = (TabHeaderSkin) child;
if (tabHeaderSkin.getTab().equals(tab))
{
return tabHeaderSkin;
}
}
return null;
}

private boolean tabsFit()
{
double headerPrefWidth = snapSize(headersRegion.prefWidth(-1));
double controlTabWidth = snapSize(controlButtons.prefWidth(-1));
double visibleWidth = headerPrefWidth + controlTabWidth
+ firstTabIndent()
+ SPACER;
return visibleWidth < getWidth();
}

private void ensureSelectedTabIsVisible()
{
// work out the visible width of the tab header
double tabPaneWidth =
snapSize(isHorizontal() ? getSkinnable().getWidth()
: getSkinnable().getHeight());
double controlTabWidth = snapSize(controlButtons.getWidth());
double visibleWidth = tabPaneWidth - controlTabWidth
- firstTabIndent()
- SPACER;

// and get where the selected tab is in the header area
double offset = 0.0;
double selectedTabOffset = 0.0;
double selectedTabWidth = 0.0;
for (Node node : headersRegion.getChildren())
{
TabHeaderSkin tabHeader = (TabHeaderSkin) node;

double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));

if (selectedTab != null
&& selectedTab.equals(tabHeader.getTab()))
{
selectedTabOffset = offset;
selectedTabWidth = tabHeaderPrefWidth;
}
offset += tabHeaderPrefWidth;
}

final double scrollOffset = getScrollOffset();
final double selectedTabStartX = selectedTabOffset;
final double selectedTabEndX = selectedTabOffset
+ selectedTabWidth;

final double visibleAreaEndX = visibleWidth;

if (selectedTabStartX < -scrollOffset)
{
setScrollOffset(-selectedTabStartX);
}
else if (selectedTabEndX > (visibleAreaEndX - scrollOffset))
{
setScrollOffset(visibleAreaEndX - selectedTabEndX);
}
}

public double getScrollOffset()
{
return scrollOffset;
}

private void validateScrollOffset()
{
setScrollOffset(getScrollOffset());
}

private void setScrollOffset(double newScrollOffset)
{
// work out the visible width of the tab header
double tabPaneWidth =
snapSize(isHorizontal() ? getSkinnable().getWidth()
: getSkinnable().getHeight());
double controlTabWidth = snapSize(controlButtons.getWidth());
double visibleWidth = tabPaneWidth - controlTabWidth
- firstTabIndent()
- SPACER;

// measure the width of all tabs
double offset = 0.0;
for (Node node : headersRegion.getChildren())
{
TabHeaderSkin tabHeader = (TabHeaderSkin) node;
double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));
offset += tabHeaderPrefWidth;
}

double actualNewScrollOffset;

if ((visibleWidth - newScrollOffset) > offset
&& newScrollOffset < 0)
{
// need to make sure the right-most tab is attached to the
// right-hand side of the tab header (e.g. if the tab header area width
// is expanded), and if it isn't modify the scroll offset to bring
// it into line. See RT-35194 for a test case.
actualNewScrollOffset = visibleWidth - offset;
}
else if (newScrollOffset > 0)
{
// need to prevent the left-most tab from becoming detached
// from the left-hand side of the tab header.
actualNewScrollOffset = 0;
}
else
{
actualNewScrollOffset = newScrollOffset;
}

if (actualNewScrollOffset != scrollOffset)
{
scrollOffset = actualNewScrollOffset;
headersRegion.requestLayout();
}
}

private double firstTabIndent()
{
switch (getSkinnable().getSide())
{
case TOP:
case BOTTOM:
return snappedLeftInset();
case RIGHT:
case LEFT:
return snappedTopInset();
default:
return 0;
}
}

@Override
protected double computePrefWidth(double height)
{
double padding = isHorizontal()
? snappedLeftInset()
+ snappedRightInset()
: snappedTopInset()
+ snappedBottomInset();
return snapSize(headersRegion.prefWidth(height))
+ controlButtons.prefWidth(height)
+ firstTabIndent()
+ SPACER
+ padding;
}

@Override
protected double computePrefHeight(double width)
{
double padding = isHorizontal()
? snappedTopInset()
+ snappedBottomInset()
: snappedLeftInset()
+ snappedRightInset();
return snapSize(headersRegion.prefHeight(-1)) + padding;
}

@Override
public double getBaselineOffset()
{
if (getSkinnable().getSide() == Side.TOP)
{
return headersRegion.getBaselineOffset() + snappedTopInset();
}
return 0;
}

@Override
protected void layoutChildren()
{
final double leftInset = snappedLeftInset();
final double rightInset = snappedRightInset();
final double topInset = snappedTopInset();
final double bottomInset = snappedBottomInset();
double w = snapSize(getWidth())
- (isHorizontal() ? leftInset + rightInset
: topInset + bottomInset);
double h = snapSize(getHeight())
- (isHorizontal() ? topInset + bottomInset
: leftInset + rightInset);
double tabBackgroundHeight = snapSize(prefHeight(-1));
double headersPrefWidth = snapSize(headersRegion.prefWidth(-1));
double headersPrefHeight =
snapSize(headersRegion.prefHeight(-1));

controlButtons.showTabsMenu(!tabsFit());

updateHeaderClip();
headersRegion.requestLayout();

// RESIZE CONTROL BUTTONS
double btnWidth = snapSize(controlButtons.prefWidth(-1));
final double btnHeight = controlButtons.prefHeight(btnWidth);
controlButtons.resize(btnWidth, btnHeight);

// POSITION TABS
headersRegion.resize(headersPrefWidth, headersPrefHeight);

if (isFloatingStyleClass())
{
headerBackground.setVisible(false);
}
else
{
headerBackground.resize(snapSize(getWidth()),
snapSize(getHeight()));
headerBackground.setVisible(true);
}

double startX = 0;
double startY = 0;
double controlStartX = 0;
double controlStartY = 0;
Side tabPosition = getSkinnable().getSide();

if (tabPosition.equals(Side.TOP))
{
startX = leftInset;
startY =
tabBackgroundHeight - headersPrefHeight - bottomInset;
controlStartX = w - btnWidth + leftInset;
controlStartY =
snapSize(getHeight()) - btnHeight - bottomInset;
}
else if (tabPosition.equals(Side.RIGHT))
{
startX = topInset;
startY = tabBackgroundHeight - headersPrefHeight - leftInset;
controlStartX = w - btnWidth + topInset;
controlStartY = snapSize(getHeight()) - btnHeight - leftInset;
}
else if (tabPosition.equals(Side.BOTTOM))
{
startX = snapSize(getWidth()) - headersPrefWidth - leftInset;
startY = tabBackgroundHeight - headersPrefHeight - topInset;
controlStartX = rightInset;
controlStartY = snapSize(getHeight()) - btnHeight - topInset;
}
else if (tabPosition.equals(Side.LEFT))
{
startX = snapSize(getWidth()) - headersPrefWidth - topInset;
startY = tabBackgroundHeight - headersPrefHeight - rightInset;
controlStartX = leftInset;
controlStartY =
snapSize(getHeight()) - btnHeight - rightInset;
}
if (headerBackground.isVisible())
{
positionInArea(headerBackground,
0,
0,
snapSize(getWidth()),
snapSize(getHeight()),
/*baseline ignored*/0,
HPos.CENTER,
VPos.CENTER);
}
positionInArea(headersRegion,
startX,
startY,
w,
h,
/*baseline ignored*/0,
HPos.LEFT,
VPos.CENTER);
positionInArea(controlButtons,
controlStartX,
controlStartY,
btnWidth,
btnHeight,
/*baseline ignored*/0,
HPos.CENTER,
VPos.CENTER);
}
} /* End TabHeaderArea */

static int CLOSE_BTN_SIZE = 16;

/**************************************************************************
*
* TabHeaderSkin: skin for each tab
*
**************************************************************************/

class TabHeaderSkin extends StackPane
{
private final Tab tab;

public Tab getTab()
{
return tab;
}

private Label label;
private StackPane closeBtn;
private StackPane inner;
private Tooltip oldTooltip;
private Tooltip tooltip;
private Rectangle clip;

private boolean isClosing = false;

private MultiplePropertyChangeListenerHandler listener =
new MultiplePropertyChangeListenerHandler(param -> {
handlePropertyChanged(param);
return null;
});

private final ListChangeListener<String> styleClassListener =
new ListChangeListener<String>()
{
@Override
public void onChanged(Change<? extends String> c)
{
getStyleClass().setAll(tab.getStyleClass());
}
};

private final WeakListChangeListener<String> weakStyleClassListener =
new WeakListChangeListener<>(styleClassListener);

public TabHeaderSkin(final Tab tab)
{
getStyleClass().setAll(tab.getStyleClass());
setId(tab.getId());
setStyle(tab.getStyle());
setAccessibleRole(AccessibleRole.TAB_ITEM);

this.tab = tab;
clip = new Rectangle();
setClip(clip);

label = new Label(tab.getText(), tab.getGraphic());
label.getStyleClass().setAll("tab-label");

closeBtn = new StackPane()
{
@Override
protected double computePrefWidth(double h)
{
return CLOSE_BTN_SIZE;
}

@Override
protected double computePrefHeight(double w)
{
return CLOSE_BTN_SIZE;
}

@Override
public void executeAccessibleAction(AccessibleAction action,
Object... parameters)
{
switch (action)
{
case FIRE:
{
Tab tab = getTab();
TabPaneBehavior behavior = getBehavior();
if (behavior.canCloseTab(tab))
{
behavior.closeTab(tab);
setOnMousePressed(null);
}
}
default:
super.executeAccessibleAction(action, parameters);
}
}
};
closeBtn.setAccessibleRole(AccessibleRole.BUTTON);
closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton"));
closeBtn.getStyleClass().setAll("tab-close-button");
closeBtn.setOnMousePressed(new EventHandler<MouseEvent>()
{
@Override
public void handle(MouseEvent me)
{
Tab tab = getTab();
TabPaneBehavior behavior = getBehavior();
if (behavior.canCloseTab(tab))
{
behavior.closeTab(tab);
setOnMousePressed(null);
}
}
});

updateGraphicRotation();

final Region focusIndicator = new Region();
focusIndicator.setMouseTransparent(true);
focusIndicator.getStyleClass().add("focus-indicator");

inner = new StackPane()
{
@Override
protected void layoutChildren()
{
final TabPane skinnable = getSkinnable();

final double paddingTop = snappedTopInset();
final double paddingRight = snappedRightInset();
final double paddingBottom = snappedBottomInset();
final double paddingLeft = snappedLeftInset();
final double w = getWidth() - (paddingLeft + paddingRight);
final double h = getHeight() - (paddingTop + paddingBottom);

final double prefLabelWidth = snapSize(label.prefWidth(-1));
final double prefLabelHeight =
snapSize(label.prefHeight(-1));

final double closeBtnWidth =
showCloseButton() ? snapSize(closeBtn.prefWidth(-1))
: 0;
final double closeBtnHeight =
showCloseButton() ? snapSize(closeBtn.prefHeight(-1))
: 0;
final double minWidth =
snapSize(skinnable.getTabMinWidth());
final double maxWidth =
snapSize(skinnable.getTabMaxWidth());
final double maxHeight =
snapSize(skinnable.getTabMaxHeight());

double labelAreaWidth = prefLabelWidth;
double labelWidth = prefLabelWidth;
double labelHeight = prefLabelHeight;

final double childrenWidth = labelAreaWidth + closeBtnWidth;
final double childrenHeight = Math.max(labelHeight,
closeBtnHeight);

if (childrenWidth > maxWidth
&& maxWidth != Double.MAX_VALUE)
{
labelAreaWidth = maxWidth - closeBtnWidth;
labelWidth = maxWidth - closeBtnWidth;
}
else if (childrenWidth < minWidth)
{
labelAreaWidth = minWidth - closeBtnWidth;
}

if (childrenHeight > maxHeight
&& maxHeight != Double.MAX_VALUE)
{
labelHeight = maxHeight;
}

if (animationState != TabAnimationState.NONE)
{
// if (prefWidth.getValue() < labelAreaWidth) {
// labelAreaWidth = prefWidth.getValue();
// }
labelAreaWidth *= animationTransition.get();
closeBtn.setVisible(false);
}
else
{
closeBtn.setVisible(showCloseButton());
}

label.resize(labelWidth, labelHeight);

double labelStartX = paddingLeft;

// If maxWidth is less than Double.MAX_VALUE, the user has
// clamped the max width, but we should
// position the close button at the end of the tab,
// which may not necessarily be the entire width of the
// provided max width.
double closeBtnStartX = (maxWidth < Double.MAX_VALUE
? Math.min(w,
maxWidth)
: w)
- paddingRight - closeBtnWidth;

positionInArea(label,
labelStartX,
paddingTop,
labelAreaWidth,
h,
/*baseline ignored*/0,
HPos.CENTER,
VPos.CENTER);

if (closeBtn.isVisible())
{
closeBtn.resize(closeBtnWidth, closeBtnHeight);
positionInArea(closeBtn,
closeBtnStartX,
paddingTop,
closeBtnWidth,
h,
/*baseline ignored*/0,
HPos.CENTER,
VPos.CENTER);
}

// Magic numbers regretfully introduced for RT-28944 (so that
// the focus rect appears as expected on Windows and Mac).
// In short we use the vPadding to shift the focus rect down
// into the content area (whereas previously it was being clipped
// on Windows, whilst it still looked fine on Mac). In the
// future we may want to improve this code to remove the
// magic number. Similarly, the hPadding differs on Mac.
final int vPadding = Utils.isMac() ? 2 : 3;
final int hPadding = Utils.isMac() ? 2 : 1;
focusIndicator.resizeRelocate(paddingLeft - hPadding,
paddingTop + vPadding,
w + 2 * hPadding,
h - 2 * vPadding);
}
};
inner.getStyleClass().add("tab-container");
inner.setRotate(getSkinnable().getSide()
.equals(Side.BOTTOM) ? 180.0F
: 0.0F);
inner.getChildren().addAll(label, closeBtn, focusIndicator);

getChildren().addAll(inner);

tooltip = tab.getTooltip();
if (tooltip != null)
{
Tooltip.install(this, tooltip);
oldTooltip = tooltip;
}

listener.registerChangeListener(tab.closableProperty(),
"CLOSABLE");
listener.registerChangeListener(tab.selectedProperty(),
"SELECTED");
listener.registerChangeListener(tab.textProperty(), "TEXT");
listener.registerChangeListener(tab.graphicProperty(),
"GRAPHIC");
listener.registerChangeListener(tab.contextMenuProperty(),
"CONTEXT_MENU");
listener.registerChangeListener(tab.tooltipProperty(),
"TOOLTIP");
listener.registerChangeListener(tab.disableProperty(),
"DISABLE");
listener.registerChangeListener(tab.styleProperty(), "STYLE");

tab.getStyleClass().addListener(weakStyleClassListener);

listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(),
"TAB_CLOSING_POLICY");
listener.registerChangeListener(getSkinnable().sideProperty(),
"SIDE");
listener.registerChangeListener(getSkinnable().rotateGraphicProperty(),
"ROTATE_GRAPHIC");
listener.registerChangeListener(getSkinnable().tabMinWidthProperty(),
"TAB_MIN_WIDTH");
listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(),
"TAB_MAX_WIDTH");
listener.registerChangeListener(getSkinnable().tabMinHeightProperty(),
"TAB_MIN_HEIGHT");
listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(),
"TAB_MAX_HEIGHT");

getProperties().put(Tab.class, tab);
getProperties().put(ContextMenu.class, tab.getContextMenu());

setOnContextMenuRequested((ContextMenuEvent me) -> {
if (getTab().getContextMenu() != null)
{
getTab().getContextMenu()
.show(inner, me.getScreenX(), me.getScreenY());
me.consume();
}
});
setOnMousePressed(new EventHandler<MouseEvent>()
{
@Override
public void handle(MouseEvent me)
{
if (getTab().isDisable())
{
return;
}
if (me.getButton().equals(MouseButton.MIDDLE))
{
if (showCloseButton())
{
Tab tab = getTab();
TabPaneBehavior behavior = getBehavior();
if (behavior.canCloseTab(tab))
{
removeListeners(tab);
behavior.closeTab(tab);
}
}
}
else if (me.getButton().equals(MouseButton.PRIMARY))
{
getBehavior().selectTab(getTab());
}
}
});

// initialize pseudo-class state
pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE,
tab.isSelected());
pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE,
tab.isDisable());
final Side side = getSkinnable().getSide();
pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE,
(side == Side.TOP));
pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE,
(side == Side.RIGHT));
pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE,
(side == Side.BOTTOM));
pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE,
(side == Side.LEFT));
}

private void handlePropertyChanged(final String p)
{
// --- Tab properties
if ("CLOSABLE".equals(p))
{
inner.requestLayout();
requestLayout();
}
else if ("SELECTED".equals(p))
{
pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE,
tab.isSelected());
// Need to request a layout pass for inner because if the width
// and height didn't not change the label or close button may have
// changed.
inner.requestLayout();
requestLayout();
}
else if ("TEXT".equals(p))
{
label.setText(getTab().getText());
}
else if ("GRAPHIC".equals(p))
{
label.setGraphic(getTab().getGraphic());
}
else if ("CONTEXT_MENU".equals(p))
{
// todo
}
else if ("TOOLTIP".equals(p))
{
// uninstall the old tooltip
if (oldTooltip != null)
{
Tooltip.uninstall(this, oldTooltip);
}
tooltip = tab.getTooltip();
if (tooltip != null)
{
// install new tooltip and save as old tooltip.
Tooltip.install(this, tooltip);
oldTooltip = tooltip;
}
}
else if ("DISABLE".equals(p))
{
pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE,
tab.isDisable());
inner.requestLayout();
requestLayout();
}
else if ("STYLE".equals(p))
{
setStyle(tab.getStyle());
}

// --- Skinnable properties
else if ("TAB_CLOSING_POLICY".equals(p))
{
inner.requestLayout();
requestLayout();
}
else if ("SIDE".equals(p))
{
final Side side = getSkinnable().getSide();
pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE,
(side == Side.TOP));
pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE,
(side == Side.RIGHT));
pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE,
(side == Side.BOTTOM));
pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE,
(side == Side.LEFT));
inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F);
if (getSkinnable().isRotateGraphic())
{
updateGraphicRotation();
}
}
else if ("ROTATE_GRAPHIC".equals(p))
{
updateGraphicRotation();
}
else if ("TAB_MIN_WIDTH".equals(p))
{
requestLayout();
getSkinnable().requestLayout();
}
else if ("TAB_MAX_WIDTH".equals(p))
{
requestLayout();
getSkinnable().requestLayout();
}
else if ("TAB_MIN_HEIGHT".equals(p))
{
requestLayout();
getSkinnable().requestLayout();
}
else if ("TAB_MAX_HEIGHT".equals(p))
{
requestLayout();
getSkinnable().requestLayout();
}
}

private void updateGraphicRotation()
{
if (label.getGraphic() != null)
{
label.getGraphic()
.setRotate(getSkinnable().isRotateGraphic() ? 0.0F
: (getSkinnable().getSide()
.equals(Side.RIGHT) ? -90.0F
: (getSkinnable().getSide()
.equals(Side.LEFT) ? 90.0F
: 0.0F)));
}
}

private boolean showCloseButton()
{
return tab.isClosable() && (getSkinnable().getTabClosingPolicy()
.equals(TabClosingPolicy.ALL_TABS)
|| getSkinnable().getTabClosingPolicy()
.equals(TabClosingPolicy.SELECTED_TAB)
&& tab.isSelected());
}

private final DoubleProperty animationTransition =
new SimpleDoubleProperty(this,
"animationTransition",
1.0)
{
@Override
protected void invalidated()
{
requestLayout();
}
};

private void removeListeners(Tab tab)
{
listener.dispose();
inner.getChildren().clear();
getChildren().clear();
}

private TabAnimationState animationState = TabAnimationState.NONE;
private Timeline currentAnimation;

@Override
protected double computePrefWidth(double height)
{
// if (animating) {
// return prefWidth.getValue();
// }
double minWidth = snapSize(getSkinnable().getTabMinWidth());
double maxWidth = snapSize(getSkinnable().getTabMaxWidth());
double paddingRight = snappedRightInset();
double paddingLeft = snappedLeftInset();
double tmpPrefWidth = snapSize(label.prefWidth(-1));

// only include the close button width if it is relevant
if (showCloseButton())
{
tmpPrefWidth += snapSize(closeBtn.prefWidth(-1));
}

if (tmpPrefWidth > maxWidth)
{
tmpPrefWidth = maxWidth;
}
else if (tmpPrefWidth < minWidth)
{
tmpPrefWidth = minWidth;
}
tmpPrefWidth += paddingRight + paddingLeft;
// prefWidth.setValue(tmpPrefWidth);
return tmpPrefWidth;
}

@Override
protected double computePrefHeight(double width)
{
double minHeight = snapSize(getSkinnable().getTabMinHeight());
double maxHeight = snapSize(getSkinnable().getTabMaxHeight());
double paddingTop = snappedTopInset();
double paddingBottom = snappedBottomInset();
double tmpPrefHeight = snapSize(label.prefHeight(width));

if (tmpPrefHeight > maxHeight)
{
tmpPrefHeight = maxHeight;
}
else if (tmpPrefHeight < minHeight)
{
tmpPrefHeight = minHeight;
}
tmpPrefHeight += paddingTop + paddingBottom;
return tmpPrefHeight;
}

@Override
protected void layoutChildren()
{
double w = (snapSize(getWidth()) - snappedRightInset()
- snappedLeftInset())
* animationTransition.getValue();
inner.resize(w,
snapSize(getHeight()) - snappedTopInset()
- snappedBottomInset());
inner.relocate(snappedLeftInset(), snappedTopInset());
}

@Override
protected void setWidth(double value)
{
super.setWidth(value);
clip.setWidth(value);
}

@Override
protected void setHeight(double value)
{
super.setHeight(value);
clip.setHeight(value);
}

@Override
public Object queryAccessibleAttribute(AccessibleAttribute attribute,
Object... parameters)
{
switch (attribute)
{
case TEXT:
return getTab().getText();
case SELECTED:
return selectedTab == getTab();
default:
return super.queryAccessibleAttribute(attribute, parameters);
}
}

@Override
public void executeAccessibleAction(AccessibleAction action,
Object... parameters)
{
switch (action)
{
case REQUEST_FOCUS:
getSkinnable().getSelectionModel().select(getTab());
break;
default:
super.executeAccessibleAction(action, parameters);
}
}

} /* End TabHeaderSkin */

private static final PseudoClass SELECTED_PSEUDOCLASS_STATE =
PseudoClass.getPseudoClass("selected");
private static final PseudoClass TOP_PSEUDOCLASS_STATE =
PseudoClass.getPseudoClass("top");
private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE =
PseudoClass.getPseudoClass("bottom");
private static final PseudoClass LEFT_PSEUDOCLASS_STATE =
PseudoClass.getPseudoClass("left");
private static final PseudoClass RIGHT_PSEUDOCLASS_STATE =
PseudoClass.getPseudoClass("right");
private static final PseudoClass DISABLED_PSEUDOCLASS_STATE =
PseudoClass.getPseudoClass("disabled");

/**************************************************************************
*
* TabContentRegion: each tab has one to contain the tab's content node
*
**************************************************************************/
class TabContentRegion extends StackPane
{

private TraversalEngine engine;
private Direction direction = Direction.NEXT;
private Tab tab;

private InvalidationListener tabContentListener = valueModel -> {
updateContent();
};
private InvalidationListener tabSelectedListener =
new InvalidationListener()
{
@Override
public void invalidated(Observable valueModel)
{
setVisible(tab.isSelected());
}
};

private WeakInvalidationListener weakTabContentListener =
new WeakInvalidationListener(tabContentListener);
private WeakInvalidationListener weakTabSelectedListener =
new WeakInvalidationListener(tabSelectedListener);

public Tab getTab()
{
return tab;
}

public TabContentRegion(Tab tab)
{
getStyleClass().setAll("tab-content-area");
setManaged(false);
this.tab = tab;
updateContent();
setVisible(tab.isSelected());

tab.selectedProperty().addListener(weakTabSelectedListener);
tab.contentProperty().addListener(weakTabContentListener);
}

private void updateContent()
{
Node newContent = getTab().getContent();
if (newContent == null)
{
getChildren().clear();
}
else
{
getChildren().setAll(newContent);
}
}

private void removeListeners(Tab tab)
{
tab.selectedProperty().removeListener(weakTabSelectedListener);
tab.contentProperty().removeListener(weakTabContentListener);
}

} /* End TabContentRegion */

/**************************************************************************
*
* TabControlButtons: controls to manipulate tab interaction
*
**************************************************************************/
class TabControlButtons extends StackPane
{
private StackPane inner;
private StackPane downArrow;
private Pane downArrowBtn;
private boolean showControlButtons;
private ContextMenu popup;

public TabControlButtons()
{
getStyleClass().setAll("control-buttons-tab");

TabPane tabPane = getSkinnable();

downArrowBtn = new Pane();
downArrowBtn.getStyleClass().setAll("tab-down-button");
downArrowBtn.setVisible(isShowTabsMenu());
downArrow = new StackPane();
downArrow.setManaged(false);
downArrow.getStyleClass().setAll("arrow");
downArrow.setRotate(tabPane.getSide()
.equals(Side.BOTTOM) ? 180.0F
: 0.0F);
downArrowBtn.getChildren().add(downArrow);
downArrowBtn.setOnMouseClicked(me -> {
showPopupMenu();
});

setupPopupMenu();

inner = new StackPane()
{
@Override
protected double computePrefWidth(double height)
{
double pw;
double maxArrowWidth =
!isShowTabsMenu() ? 0
: snapSize(downArrow.prefWidth(getHeight()))
+ snapSize(downArrowBtn.prefWidth(getHeight()));
pw = 0.0F;
if (isShowTabsMenu())
{
pw += maxArrowWidth;
}
if (pw > 0)
{
pw += snappedLeftInset() + snappedRightInset();
}
return pw;
}

@Override
protected double computePrefHeight(double width)
{
double height = 0.0F;
if (isShowTabsMenu())
{
height =
Math.max(height,
snapSize(downArrowBtn.prefHeight(width)));
}
if (height > 0)
{
height += snappedTopInset() + snappedBottomInset();
}
return height;
}

@Override
protected void layoutChildren()
{
if (isShowTabsMenu())
{
double x = 0;
double y = snappedTopInset();
double w = snapSize(getWidth()) - x + snappedLeftInset();
double h =
snapSize(getHeight()) - y + snappedBottomInset();
positionArrow(downArrowBtn, downArrow, x, y, w, h);
}
}

private void positionArrow(Pane btn,
StackPane arrow,
double x,
double y,
double width,
double height)
{
btn.resize(width, height);
positionInArea(btn,
x,
y,
width,
height,
/*baseline ignored*/0,
HPos.CENTER,
VPos.CENTER);
// center arrow region within arrow button
double arrowWidth = snapSize(arrow.prefWidth(-1));
double arrowHeight = snapSize(arrow.prefHeight(-1));
arrow.resize(arrowWidth, arrowHeight);
positionInArea(arrow,
btn.snappedLeftInset(),
btn.snappedTopInset(),
width - btn.snappedLeftInset()
- btn.snappedRightInset(),
height - btn.snappedTopInset()
- btn.snappedBottomInset(),
/*baseline ignored*/0,
HPos.CENTER,
VPos.CENTER);
}
};
inner.getStyleClass().add("container");
inner.getChildren().add(downArrowBtn);

getChildren().add(inner);

tabPane.sideProperty().addListener(valueModel -> {
Side tabPosition = getSkinnable().getSide();
downArrow.setRotate(tabPosition.equals(Side.BOTTOM) ? 180.0F
: 0.0F);
});
tabPane.getTabs()
.addListener((ListChangeListener<Tab>) c -> setupPopupMenu());
showControlButtons = false;
if (isShowTabsMenu())
{
showControlButtons = true;
requestLayout();
}
getProperties().put(ContextMenu.class, popup);
}

private boolean showTabsMenu = false;

private void showTabsMenu(boolean value)
{
final boolean wasTabsMenuShowing = isShowTabsMenu();
this.showTabsMenu = value;

if (showTabsMenu && !wasTabsMenuShowing)
{
downArrowBtn.setVisible(true);
showControlButtons = true;
inner.requestLayout();
tabHeaderArea.requestLayout();
}
else if (!showTabsMenu && wasTabsMenuShowing)
{
hideControlButtons();
}
}

private boolean isShowTabsMenu()
{
return showTabsMenu;
}

@Override
protected double computePrefWidth(double height)
{
double pw = snapSize(inner.prefWidth(height));
if (pw > 0)
{
pw += snappedLeftInset() + snappedRightInset();
}
return pw;
}

@Override
protected double computePrefHeight(double width)
{
return Math.max(getSkinnable().getTabMinHeight(),
snapSize(inner.prefHeight(width)))
+ snappedTopInset() + snappedBottomInset();
}

@Override
protected void layoutChildren()
{
double x = snappedLeftInset();
double y = snappedTopInset();
double w = snapSize(getWidth()) - x + snappedRightInset();
double h = snapSize(getHeight()) - y + snappedBottomInset();

if (showControlButtons)
{
showControlButtons();
showControlButtons = false;
}

inner.resize(w, h);
positionInArea(inner,
x,
y,
w,
h,
/*baseline ignored*/0,
HPos.CENTER,
VPos.BOTTOM);
}

private void showControlButtons()
{
setVisible(true);
if (popup == null)
{
setupPopupMenu();
}
}

private void hideControlButtons()
{
// If the scroll arrows or tab menu is still visible we don't want
// to hide it animate it back it.
if (isShowTabsMenu())
{
showControlButtons = true;
}
else
{
setVisible(false);
popup.getItems().clear();
popup = null;
}

// This needs to be called when we are in the left tabPosition
// to allow for the clip offset to move properly (otherwise
// it jumps too early - before the animation is done).
requestLayout();
}

private void setupPopupMenu()
{
if (popup == null)
{
popup = new ContextMenu();
}
popup.getItems().clear();
ToggleGroup group = new ToggleGroup();
ObservableList<RadioMenuItem> menuitems =
FXCollections.<RadioMenuItem> observableArrayList();
for (final Tab tab : getSkinnable().getTabs())
{
TabMenuItem item = new TabMenuItem(tab);
item.setToggleGroup(group);
item.setOnAction(t -> getSkinnable().getSelectionModel()
.select(tab));
menuitems.add(item);
}
popup.getItems().addAll(menuitems);
}

private void showPopupMenu()
{
for (MenuItem mi : popup.getItems())
{
TabMenuItem tmi = (TabMenuItem) mi;
if (selectedTab.equals(tmi.getTab()))
{
tmi.setSelected(true);
break;
}
}
popup.show(downArrowBtn, Side.BOTTOM, 0, 0);
}
} /* End TabControlButtons*/

class TabMenuItem extends RadioMenuItem
{
DockNodeTab tab;

private InvalidationListener disableListener =
new InvalidationListener()
{
@Override
public void invalidated(Observable o)
{
setDisable(tab.isDisable());
}
};

private WeakInvalidationListener weakDisableListener =
new WeakInvalidationListener(disableListener);

public TabMenuItem(final Tab tab)
{
this((DockNodeTab) tab);
}

public TabMenuItem(final DockNodeTab tab)
{
super(tab.getTitle(),
ContentTabPaneSkin.clone(tab.getGraphic()));
this.tab = tab;
setDisable(tab.isDisable());
tab.disableProperty().addListener(weakDisableListener);
textProperty().bind(tab.titleProperty());
}

public Tab getTab()
{
return tab;
}

public void dispose()
{
tab.disableProperty().removeListener(weakDisableListener);
}
}

@Override
public Object queryAccessibleAttribute(AccessibleAttribute attribute,
Object... parameters)
{
switch (attribute)
{
case FOCUS_ITEM:
return tabHeaderArea.getTabHeaderSkin(selectedTab);
case ITEM_COUNT:
return tabHeaderArea.headersRegion.getChildren().size();
case ITEM_AT_INDEX:
{
Integer index = (Integer) parameters[0];
if (index == null)
return null;
return tabHeaderArea.headersRegion.getChildren().get(index);
}
default:
return super.queryAccessibleAttribute(attribute, parameters);
tab.textProperty().bind(((DockNodeTab) tab).titleProperty());
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Binding DockNodeTab.textProperty() to titleProperty() will likely make the tab header show duplicate titles: DockNodeTab already sets its graphic to DockNode.getDockTitleBar(), and DockTitleBar’s internal Label is bound to dockNode.titleProperty(). If the intention is only to fix the overflow popup text, consider keeping the tab header graphic-only (e.g., add a dedicated style class for DockNodeTab and use CSS like -fx-content-display: graphic-only on .tab-label) while still setting text for the menu, or use another non-visual source for the menu label.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +47
tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> {
while (c.next())
{
tabRegion.isClosing = true;

tabRegion.removeListeners(tab);
removeTabContent(tab);

// remove the menu item from the popup menu
ContextMenu popupMenu = tabHeaderArea.controlButtons.popup;
TabMenuItem tabItem = null;
if (popupMenu != null)
{
for (MenuItem item : popupMenu.getItems())
{
tabItem = (TabMenuItem) item;
if (tab == tabItem.getTab())
{
break;
}
tabItem = null;
}
}
if (tabItem != null)
if (c.wasAdded())
{
tabItem.dispose();
popupMenu.getItems().remove(tabItem);
}
// end of removing menu item

EventHandler<ActionEvent> cleanup = ae -> {
tabRegion.animationState = TabAnimationState.NONE;

tabHeaderArea.removeTab(tab);
tabHeaderArea.requestLayout();
if (getSkinnable().getTabs().isEmpty())
for (Tab tab : c.getAddedSubList())
{
tabHeaderArea.setVisible(false);
bindTabText(tab);
}
};

if (closeTabAnimation.get() == TabAnimation.GROW)
{
tabRegion.animationState = TabAnimationState.HIDING;
Timeline closedTabTimeline =
tabRegion.currentAnimation =
createTimeline(tabRegion,
Duration.millis(ANIMATION_SPEED),
0.0F,
cleanup);
closedTabTimeline.play();
}
else
{
cleanup.handle(null);
}
}
}
}

private void stopCurrentAnimation(Tab tab)
{
final TabHeaderSkin tabRegion =
tabHeaderArea.getTabHeaderSkin(tab);
if (tabRegion != null)
{
// Execute the code immediately, don't wait for the animation to finish.
Timeline timeline = tabRegion.currentAnimation;
if (timeline != null
&& timeline.getStatus() == Animation.Status.RUNNING)
{
timeline.getOnFinished().handle(null);
timeline.stop();
tabRegion.currentAnimation = null;
}
}
}

private void addTabs(List<? extends Tab> addedList, int from)
{
int i = 0;

// RT-39984: check if any other tabs are animating - they must be completed
// first.
List<Node> headers =
new ArrayList<>(tabHeaderArea.headersRegion.getChildren());
for (Node n : headers)
{
TabHeaderSkin header = (TabHeaderSkin) n;
if (header.animationState == TabAnimationState.HIDING)
{
stopCurrentAnimation(header.tab);
}
}
// end of fix for RT-39984

for (final Tab tab : addedList)
{
stopCurrentAnimation(tab); // Note that this must happen before addTab()
// call below
// A new tab was added - animate it out
if (!tabHeaderArea.isVisible())
{
tabHeaderArea.setVisible(true);
}
int index = from + i++;
tabHeaderArea.addTab(tab, index);
addTabContent(tab);
final TabHeaderSkin tabRegion =
tabHeaderArea.getTabHeaderSkin(tab);
if (tabRegion != null)
{
if (openTabAnimation.get() == TabAnimation.GROW)
{
tabRegion.animationState = TabAnimationState.SHOWING;
tabRegion.animationTransition.setValue(0.0);
tabRegion.setVisible(true);
tabRegion.currentAnimation = createTimeline(tabRegion,
Duration.millis(ANIMATION_SPEED),
1.0,
event -> {
tabRegion.animationState =
TabAnimationState.NONE;
tabRegion.setVisible(true);
tabRegion.inner.requestLayout();
});
tabRegion.currentAnimation.play();
}
else
{
tabRegion.setVisible(true);
tabRegion.inner.requestLayout();
}
}
}
}

private void initializeTabListener()
{
getSkinnable().getTabs()
.addListener((ListChangeListener<Tab>) c -> {
List<Tab> tabsToRemove = new ArrayList<>();
List<Tab> tabsToAdd = new ArrayList<>();
int insertPos = -1;

while (c.next())
{
if (c.wasPermutated())
{
TabPane tabPane = getSkinnable();
List<Tab> tabs = tabPane.getTabs();

// tabs sorted : create list of permutated tabs.
// clear selection, set tab animation to NONE
// remove permutated tabs, add them back in correct
// order.
// restore old selection, and old tab animation states.
int size = c.getTo() - c.getFrom();
Tab selTab = tabPane.getSelectionModel()
.getSelectedItem();
List<Tab> permutatedTabs =
new ArrayList<Tab>(size);
getSkinnable().getSelectionModel()
.clearSelection();

// save and set tab animation to none - as it is not a
// good idea
// to animate on the same data for open and close.
TabAnimation prevOpenAnimation =
openTabAnimation.get();
TabAnimation prevCloseAnimation =
closeTabAnimation.get();
openTabAnimation.set(TabAnimation.NONE);
closeTabAnimation.set(TabAnimation.NONE);
for (int i = c.getFrom(); i < c.getTo(); i++)
{
permutatedTabs.add(tabs.get(i));
}

removeTabs(permutatedTabs);
addTabs(permutatedTabs, c.getFrom());
openTabAnimation.set(prevOpenAnimation);
closeTabAnimation.set(prevCloseAnimation);
getSkinnable().getSelectionModel()
.select(selTab);
}

if (c.wasRemoved())
{
tabsToRemove.addAll(c.getRemoved());
}

if (c.wasAdded())
{
tabsToAdd.addAll(c.getAddedSubList());
insertPos = c.getFrom();
}
}

// now only remove the tabs that are not in the tabsToAdd
// list
tabsToRemove.removeAll(tabsToAdd);
removeTabs(tabsToRemove);

// and add in any new tabs (that we don't already have
// showing)
if (!tabsToAdd.isEmpty())
{
for (TabContentRegion tabContentRegion : tabContentRegions)
{
Tab tab = tabContentRegion.getTab();
TabHeaderSkin tabHeader =
tabHeaderArea.getTabHeaderSkin(tab);
if (!tabHeader.isClosing
&& tabsToAdd.contains(tabContentRegion.getTab()))
{
tabsToAdd.remove(tabContentRegion.getTab());
}
}

addTabs(tabsToAdd,
insertPos == -1 ? tabContentRegions.size()
: insertPos);
}

// Fix for RT-34692
getSkinnable().requestLayout();
});
}

private void addTabContent(Tab tab)
{
TabContentRegion tabContentRegion = new TabContentRegion(tab);
tabContentRegion.setClip(new Rectangle());
tabContentRegions.add(tabContentRegion);
// We want the tab content to always sit below the tab headers
getChildren().add(0, tabContentRegion);
}

private void removeTabContent(Tab tab)
{
for (TabContentRegion contentRegion : tabContentRegions)
{
if (contentRegion.getTab().equals(tab))
{
contentRegion.removeListeners(tab);
getChildren().remove(contentRegion);
tabContentRegions.remove(contentRegion);
break;
}
}
}

private void updateTabPosition()
{
tabHeaderArea.setScrollOffset(0.0F);
getSkinnable().applyCss();
getSkinnable().requestLayout();
}

private Timeline createTimeline(final TabHeaderSkin tabRegion,
final Duration duration,
final double endValue,
final EventHandler<ActionEvent> func)
{
Timeline timeline = new Timeline();
timeline.setCycleCount(1);

KeyValue keyValue = new KeyValue(tabRegion.animationTransition,
endValue,
Interpolator.LINEAR);
timeline.getKeyFrames().clear();
timeline.getKeyFrames().add(new KeyFrame(duration, keyValue));

timeline.setOnFinished(func);
return timeline;
}

private boolean isHorizontal()
{
Side tabPosition = getSkinnable().getSide();
return Side.TOP.equals(tabPosition)
|| Side.BOTTOM.equals(tabPosition);
}

private void initializeSwipeHandlers()
{
if (IS_TOUCH_SUPPORTED)
{
getSkinnable().addEventHandler(SwipeEvent.SWIPE_LEFT, t -> {
getBehavior().selectNextTab();
});

getSkinnable().addEventHandler(SwipeEvent.SWIPE_RIGHT, t -> {
getBehavior().selectPreviousTab();
});
}
}

// TODO need to cache this.
private boolean isFloatingStyleClass()
{
return getSkinnable().getStyleClass()
.contains(TabPane.STYLE_CLASS_FLOATING);
}

private double maxw = 0.0d;

@Override
protected double computePrefWidth(double height,
double topInset,
double rightInset,
double bottomInset,
double leftInset)
{
// The TabPane can only be as wide as it widest content width.
for (TabContentRegion contentRegion : tabContentRegions)
{
maxw = Math.max(maxw, snapSize(contentRegion.prefWidth(-1)));
}

final boolean isHorizontal = isHorizontal();
final double tabHeaderAreaSize =
snapSize(isHorizontal ? tabHeaderArea.prefWidth(-1)
: tabHeaderArea.prefHeight(-1));

double prefWidth =
isHorizontal ? Math.max(maxw, tabHeaderAreaSize)
: maxw + tabHeaderAreaSize;
return snapSize(prefWidth) + rightInset + leftInset;
}

private double maxh = 0.0d;

@Override
protected double computePrefHeight(double width,
double topInset,
double rightInset,
double bottomInset,
double leftInset)
{
// The TabPane can only be as high as it highest content height.
for (TabContentRegion contentRegion : tabContentRegions)
{
maxh = Math.max(maxh, snapSize(contentRegion.prefHeight(-1)));
}

final boolean isHorizontal = isHorizontal();
final double tabHeaderAreaSize =
snapSize(isHorizontal ? tabHeaderArea.prefHeight(-1)
: tabHeaderArea.prefWidth(-1));

double prefHeight = isHorizontal
? maxh
+ snapSize(tabHeaderAreaSize)
: Math.max(maxh,
tabHeaderAreaSize);
return snapSize(prefHeight) + topInset + bottomInset;
}

@Override
public double computeBaselineOffset(double topInset,
double rightInset,
double bottomInset,
double leftInset)
{
Side tabPosition = getSkinnable().getSide();
if (tabPosition == Side.TOP)
{
return tabHeaderArea.getBaselineOffset() + topInset;
}
return 0;
}

@Override
protected void layoutChildren(final double x,
final double y,
final double w,
final double h)
{
TabPane tabPane = getSkinnable();
Side tabPosition = tabPane.getSide();

double headerHeight = snapSize(tabHeaderArea.prefHeight(-1));
double tabsStartX = tabPosition.equals(Side.RIGHT)
? x + w
- headerHeight
: x;
double tabsStartY = tabPosition.equals(Side.BOTTOM)
? y + h
- headerHeight
: y;

if (tabPosition == Side.TOP)
{
tabHeaderArea.resize(w, headerHeight);
tabHeaderArea.relocate(tabsStartX, tabsStartY);
tabHeaderArea.getTransforms().clear();
tabHeaderArea.getTransforms()
.add(new Rotate(getRotation(Side.TOP)));
}
else if (tabPosition == Side.BOTTOM)
{
tabHeaderArea.resize(w, headerHeight);
tabHeaderArea.relocate(w, tabsStartY - headerHeight);
tabHeaderArea.getTransforms().clear();
tabHeaderArea.getTransforms()
.add(new Rotate(getRotation(Side.BOTTOM),
0,
headerHeight));
}
else if (tabPosition == Side.LEFT)
{
tabHeaderArea.resize(h, headerHeight);
tabHeaderArea.relocate(tabsStartX + headerHeight,
h - headerHeight);
tabHeaderArea.getTransforms().clear();
tabHeaderArea.getTransforms()
.add(new Rotate(getRotation(Side.LEFT),
0,
headerHeight));
}
else if (tabPosition == Side.RIGHT)
{
tabHeaderArea.resize(h, headerHeight);
tabHeaderArea.relocate(tabsStartX, y - headerHeight);
tabHeaderArea.getTransforms().clear();
tabHeaderArea.getTransforms()
.add(new Rotate(getRotation(Side.RIGHT),
0,
headerHeight));
}

tabHeaderAreaClipRect.setX(0);
tabHeaderAreaClipRect.setY(0);
if (isHorizontal())
{
tabHeaderAreaClipRect.setWidth(w);
}
else
{
tabHeaderAreaClipRect.setWidth(h);
}
tabHeaderAreaClipRect.setHeight(headerHeight);

// ==================================
// position the tab content for the selected tab only
// ==================================
// if the tabs are on the left, the content needs to be indented
double contentStartX = 0;
double contentStartY = 0;

if (tabPosition == Side.TOP)
{
contentStartX = x;
contentStartY = y + headerHeight;
if (isFloatingStyleClass())
{
// This is to hide the top border content
contentStartY -= 1;
}
}
else if (tabPosition == Side.BOTTOM)
{
contentStartX = x;
contentStartY = y;
if (isFloatingStyleClass())
{
// This is to hide the bottom border content
contentStartY = 1;
}
}
else if (tabPosition == Side.LEFT)
{
contentStartX = x + headerHeight;
contentStartY = y;
if (isFloatingStyleClass())
{
// This is to hide the left border content
contentStartX -= 1;
}
}
else if (tabPosition == Side.RIGHT)
{
contentStartX = x;
contentStartY = y;
if (isFloatingStyleClass())
{
// This is to hide the right border content
contentStartX = 1;
}
}

double contentWidth = w - (isHorizontal() ? 0 : headerHeight);
double contentHeight = h - (isHorizontal() ? headerHeight : 0);

for (int i = 0, max = tabContentRegions.size(); i < max; i++)
{
TabContentRegion tabContent = tabContentRegions.get(i);

tabContent.setAlignment(Pos.TOP_LEFT);
if (tabContent.getClip() != null)
{
((Rectangle) tabContent.getClip()).setWidth(contentWidth);
((Rectangle) tabContent.getClip()).setHeight(contentHeight);
}

// we need to size all tabs, even if they aren't visible. For example,
// see RT-29167
tabContent.resize(contentWidth, contentHeight);
tabContent.relocate(contentStartX, contentStartY);
}
}

/**
* Super-lazy instantiation pattern from Bill Pugh.
*
* @treatAsPrivate implementation detail
*/
private static class StyleableProperties
{
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

private final static CssMetaData<TabPane, TabAnimation> OPEN_TAB_ANIMATION =
new CssMetaData<TabPane, ContentTabPaneSkin.TabAnimation>("-fx-open-tab-animation",
new EnumConverter<TabAnimation>(TabAnimation.class),
TabAnimation.GROW)
{

@Override
public boolean isSettable(TabPane node)
{
return true;
}

@Override
public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node)
{
ContentTabPaneSkin skin =
(ContentTabPaneSkin) node.getSkin();
return (StyleableProperty<TabAnimation>) (WritableValue<TabAnimation>) skin.openTabAnimation;
}
};

private final static CssMetaData<TabPane, TabAnimation> CLOSE_TAB_ANIMATION =
new CssMetaData<TabPane, ContentTabPaneSkin.TabAnimation>("-fx-close-tab-animation",
new EnumConverter<TabAnimation>(TabAnimation.class),
TabAnimation.GROW)
{

@Override
public boolean isSettable(TabPane node)
{
return true;
}

@Override
public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node)
{
ContentTabPaneSkin skin =
(ContentTabPaneSkin) node.getSkin();
return (StyleableProperty<TabAnimation>) (WritableValue<TabAnimation>) skin.closeTabAnimation;
}
};

static
{

final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
styleables.add(OPEN_TAB_ANIMATION);
styleables.add(CLOSE_TAB_ANIMATION);
STYLEABLES = Collections.unmodifiableList(styleables);

}
}

/**
* @return The CssMetaData associated with this class, which may include the
* CssMetaData of its super classes.
*/
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData()
{
return StyleableProperties.STYLEABLES;
}

/**
* {@inheritDoc}
*/
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData()
{
return getClassCssMetaData();
});
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tabs ListChangeListener added in the skin constructor is never removed. If the skin is disposed/replaced, this listener will keep the old skin reachable and can leak. Store the listener in a field and remove it in an overridden dispose(), or wrap it in a WeakListChangeListener.

Copilot uses AI. Check for mistakes.
Comment on lines 399 to 414
/**
* Helper function to add the default style sheet of DockFX to the user agent
* style sheets.
* The default stylesheet URL for DockFX.
*/
private static final String defaultStylesheet =
DockPane.class.getResource("default.css").toExternalForm();

/**
* Helper function to ensure the default style sheet of DockFX is applied.
* The stylesheet is now automatically added to each DockPane instance via
* the constructor. This method is retained for backward compatibility.
*/
public final static void initializeDefaultUserAgentStylesheet()
{
StyleManager.getInstance()
.addUserAgentStylesheet(DockPane.class.getResource("default.css")
.toExternalForm());
// Stylesheet is automatically applied in the DockPane constructor.
// This method is kept for API compatibility.
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initializeDefaultUserAgentStylesheet() is now a no-op, but it is called by the demo and (per the existing docs) historically applied DockFX styling globally. Keeping the method for backward compatibility should preserve behavior as closely as possible (e.g., add the stylesheet to all existing Window scenes’ stylesheets and optionally listen for new windows), or clearly deprecate/rename it to avoid silently breaking callers that relied on global styling.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +47
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>${javafx.version}</version>
</dependency>
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpenJFX dependencies are declared without an OS classifier. For JavaFX 11+ this commonly resolves the non-platform artifacts and can lead to missing native libraries at runtime (UnsatisfiedLinkError). Add the appropriate classifier (often ${javafx.platform}) for each JavaFX dependency and configure the platform property (or integrate javafx-maven-plugin platform detection) so Maven consumers get the correct natives.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46bf68f845

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 410 to 414
public final static void initializeDefaultUserAgentStylesheet()
{
StyleManager.getInstance()
.addUserAgentStylesheet(DockPane.class.getResource("default.css")
.toExternalForm());
// Stylesheet is automatically applied in the DockPane constructor.
// This method is kept for API compatibility.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore global DockFX stylesheet registration

Leaving initializeDefaultUserAgentStylesheet() empty removes the only global stylesheet hook, but several DockFX UIs are rendered in separate popup/window scenes (for example the docking-indicator Popups in DockPane and floating stages created in DockNode.setFloating(...)) that are not descendants of a DockPane and therefore do not inherit DockPane.getStylesheets(). In normal drag/undock flows those nodes lose default.css styling and icons, so docking indicators and floating-window visuals regress.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants