Conversation
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>
There was a problem hiding this comment.
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
ContentTabPaneSkinto extend publicjavafx.scene.control.skin.TabPaneSkininstead 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.
| 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()); | ||
| } |
There was a problem hiding this comment.
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.
| 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(); | ||
| }); |
There was a problem hiding this comment.
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.
| /** | ||
| * 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. | ||
| } |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| 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. | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
Summary
com.sun.*internal JavaFX API usage with public APIs, enabling compatibility with JDK 21's strong encapsulationContentTabPaneSkinfrom a 2500-line copy of internalTabPaneSkinto a 57-line class extending the publicjavafx.scene.control.skin.TabPaneSkinmodule-info.javaFiles changed
build.gradlepom.xmlsettings.gradlegradle-wrapper.propertiesgradle.properties-XX:MaxPermSizeflagmodule-info.javaorg.dockfxContentTabPaneSkin.javaTabPaneSkinDockTitleBar.javaStageHelper.getStages()→Window.getWindows()DockPane.javaStyleManager→getStylesheets().add()DockEvent.javaInputEventUtils→ manuallocalToScene()Test plan
./gradlew buildpasses with JDK 21.0.10com.sun.*imports remainorg.dockfx.demo.DockFX) and verify docking, undocking, tab reordering, and window dragging🤖 Generated with Claude Code