Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Show some tabs ahead from selected tab in a JavaFx 8 TabPane header

I'm trying to modify the TabPage control in JavaFx 8, to make it reveal to the viewport some tabs ahead (to the right) of the current selected one, or if the selected tab is at the extreme left of the header, it shows the nearby tabs before the current one.

How it is now:

Default behaviour

How I'm trying to make it behave like:

When the user selects the tab of index X, the tab pane header reveals another 2 or 3 nearby tabs.

When the user selects the tab of index X, the tab pane header reveals another 2 or 3 nearby tabs.

This is what I tried so far, with no success, apparently the code bellow is too fast to make the interface thread sync the tab selections on time (the idea was to select a tab ahead and then fallback to the one selected by the user, making the header reveal the tabs after the selected tab):

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;

public class TabSelectionListener implements ChangeListener<Tab> {

    protected TabPane owner;
    protected boolean lock;
    protected int nextItems;

    TabSelectionListener(TabPane listenTo){
        owner = listenTo;
        lock = false;
        nextItems = 2;
    }

    TabSelectionListener(TabPane listenTo, int minimalInFront){
        owner = listenTo;
        lock = false;
        nextItems = minimalInFront;
    }

    @Override
    public void changed(ObservableValue<? extends Tab> list, Tab old, Tab newT) {
        int maxTab;
        int curTab;
        int i;

        // Locks this listener, because the selections owner.getSelectionModel().select(X)
        // will call this listener again, and we are calling those from here.
        if(!lock){
            lock = true;
            maxTab = owner.getTabs().size() - 1;
            curTab = owner.getSelectionModel().getSelectedIndex();

            for(i = 0; i < nextItems && curTab + i < maxTab; i++);
            owner.getSelectionModel().select(i); // int overload
            owner.getSelectionModel().select(newT);

            lock = false;
        }
    }
}

The tabPane calls it for every tab selection:

tabPane.getSelectionModel().selectedItemProperty().addListener(new TabSelectionListener(tabPane,3));

I have been reading some topics here, and it appears to me that the header is actually a StackPane, and can be obtained by executing:

StackPane region = (StackPane) tabPane.lookup(".headers-region");

It works, but after that I have no idea how to access the properties that implements the default behaviour.

Any suggestions?

Thanks for reading.

like image 774
Eugênio Fonseca Avatar asked Jul 30 '15 21:07

Eugênio Fonseca


1 Answers

I finally did it.

Found out the class I was looking for was com.sun.javafx.scene.control.skin.TabPaneSkin @ jfxrt.jar, it has a method to make the selected tab visible, it runs everytime a selected tab at a TabPane is not fully visible, I overwrote it.

TabPaneSkin is the default Skin of TabPane, it applies some behaviours to the TabPane control.

/*
* Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*

Hopefully Oracle will not mind...

Pick your TabPane, and make...

tabPane.setSkin(new TabPaneNewSkin(tabPane));

... to overwrite Oracle's default TabPaneSkin with this one I wrote that shows nearby tabs.

Original Oracle's code for repositioning tabs when one is selected to make it visible:

    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);
            }
        }

Code I wrote into my custom TabPane skin:

    // This function was overwritten
    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;

        // OVERWRITE
        // Makes the nearby 3 tabs for each side of the selected tab visible.
        ObservableList<Node> headersRegionChildren = headersRegion.getChildren();
        boolean nextTabs = false;
        int nextTabsCount = 0;
        int current = 0;
        int numOfTabsToShowNext = 3;
        int numOfTabsToShowBefore = 3;
        double tabHeaderPrefWidth;       
        TabHeaderSkin tabHeader;

        for (Node node : headersRegionChildren) {
            tabHeader = (TabHeaderSkin)node;

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

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

                // OVERWRITE: Finds the offset of the first tab in the limit numOfTabsToShowBefore before the selected one to be shown
                for(int i = current - 1; i >= 0 && numOfTabsToShowBefore > 1; i--, numOfTabsToShowBefore--){
                    tabHeader = (TabHeaderSkin)headersRegionChildren.get(i);
                    tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));
                    offset -= tabHeaderPrefWidth;
                    selectedTabWidth += tabHeaderPrefWidth;
                }

                selectedTabOffset = offset;
                // OVERWRITE: Sets the flag to start counting in the next 3 nearby tabs.  
                nextTabs = true;
            }
            // OVERWRITE: Sums the width of the next nearby tabs with the
            // width of the selected tab, so it will scroll enough to show
            // them too.
            if(nextTabs && nextTabsCount < numOfTabsToShowNext){
                selectedTabWidth += tabHeaderPrefWidth;
                nextTabsCount++;
            }else if(nextTabsCount == numOfTabsToShowNext){
                break;
            }

            offset += tabHeaderPrefWidth;
            current++;
        }
        // END OVERWRITE

        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);
        }
    }

Code above reveals the 3 nearest tabs at each side from the selected tab (if one of those is out of the screen and exists), for every tab selection.

So that was it. com.sun.javafx.scene.control.skin.TabPaneSkin was not supposed to be extended, almost every method is private, so I made a copy of it and changed only the function mentioned above, and renamed it to TabPaneNewSkin, and it is at my package.

like image 99
Eugênio Fonseca Avatar answered Nov 15 '22 01:11

Eugênio Fonseca