Scrollable GWT TabLayoutPanel

When writing a GWT app without using one of the more advanced widget toolkits, one limiting shortcoming is that the TabLayoutPanel does not allow the user to scroll invisible tabs into view when there are too many tabs to all fit into the width of the widget.

Here’ s a TabLayoutPanel that adds two scroll buttons in the upper left corner whenever the widget becomes too small to show all tabs. It works as a drop-in replacement for TabLayoutPanel. You won’t be able to use it directly in UiBinder due to an issue with parsing custom widget elements. The need for the scroll buttons is checked whenever the browser window is resized, or a tab is added or removed.

Update: See here for an updated version.

package com.gwtscrolledtablayoutpanel.ui;

import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.LayoutPanel;
import com.google.gwt.user.client.ui.TabLayoutPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * A {@link TabLayoutPanel} that shows scroll buttons if necessary
 */
public class ScrolledTabLayoutPanel extends TabLayoutPanel {

        private static final int IMAGE_PADDING_PIXELS = 4;

        private LayoutPanel panel;
        private FlowPanel tabBar;
        private Image scrollLeftButton;
        private Image scrollRightButton;
        private HandlerRegistration windowResizeHandler;

        private ImageResource leftArrowImage;
        private ImageResource rightArrowImage;

        public ScrolledTabLayoutPanel(double barHeight, Unit barUnit,
            ImageResource leftArrowImage, ImageResource rightArrowImage) {
                super(barHeight, barUnit);

                this.leftArrowImage = leftArrowImage;
                this.rightArrowImage = rightArrowImage;

                // The main widget wrapped by this composite, which is a LayoutPanel with the tab bar & the tab content
                panel = (LayoutPanel) getWidget();

                // Find the tab bar, which is the first flow panel in the LayoutPanel
                for (int i = 0; i < panel.getWidgetCount(); ++i) {
                        Widget widget = panel.getWidget(i);
                        if (widget instanceof FlowPanel) {
                                tabBar = (FlowPanel) widget;
                                break; // tab bar found
                        }
                }

                initScrollButtons();
        }

        @Override
        public void add(Widget child, Widget tab) {
                super.add(child, tab);
                checkIfScrollButtonsNecessary();
        }

        @Override
        public boolean remove(Widget w) {
                boolean b = super.remove(w);
                checkIfScrollButtonsNecessary();
                return b;
        }

        @Override
        protected void onLoad() {
                super.onLoad();

                if (windowResizeHandler == null) {
                        windowResizeHandler = Window.addResizeHandler(new ResizeHandler() {
                                @Override
                                public void onResize(ResizeEvent event) {
                                        checkIfScrollButtonsNecessary();
                                }
                        });
                }
        }

        @Override
        protected void onUnload() {
                super.onUnload();

                if (windowResizeHandler != null) {
                        windowResizeHandler.removeHandler();
                        windowResizeHandler = null;
                }
        }

        private ClickHandler createScrollClickHandler(final int diff) {
                return new ClickHandler() {
                        @Override
                        public void onClick(ClickEvent event) {
                                Widget lastTab = getLastTab();
                                if (lastTab == null)
                                        return;

                                int newLeft = parsePosition(tabBar.getElement().getStyle().getLeft()) + diff;
                                int rightOfLastTab = getRightOfWidget(lastTab);

                                // Prevent scrolling the last tab too far away form the right border,
                                // or the first tab further than the left border position
                                if (newLeft <= 0 && (getTabBarWidth() - newLeft < (rightOfLastTab + 20))) {
                                        scrollTo(newLeft);
                                }
                        }
                };
        }

        /** Create and attach the scroll button images with a click handler */
        private void initScrollButtons() {
                scrollLeftButton = new Image(leftArrowImage);
                int leftImageWidth = scrollLeftButton.getWidth();
                panel.insert(scrollLeftButton, 0);
                panel.setWidgetLeftWidth(scrollLeftButton, 0, Unit.PX, leftImageWidth, Unit.PX);
                panel.setWidgetTopHeight(scrollLeftButton, 0, Unit.PX, scrollLeftButton.getWidth(), Unit.PX);
                scrollLeftButton.addClickHandler(createScrollClickHandler(+20));
                scrollLeftButton.setVisible(false);

                scrollRightButton = new Image(rightArrowImage);
                panel.insert(scrollRightButton, 0);
                panel.setWidgetLeftWidth(scrollRightButton, leftImageWidth + IMAGE_PADDING_PIXELS, Unit.PX, scrollRightButton.getWidth(), Unit.PX);
                panel.setWidgetTopHeight(scrollRightButton, 0, Unit.PX, scrollRightButton.getHeight(), Unit.PX);

                scrollRightButton.addClickHandler(createScrollClickHandler(-20));
                scrollRightButton.setVisible(false);
        }

        private void checkIfScrollButtonsNecessary() {
                // Defer size calculations until sizes are available, when calculating immediately after
                // add(), all size methods return zero
                DeferredCommand.addCommand(new Command() {
                        @Override
                        public void execute() {
                                boolean isScrolling = isScrollingNecessary();
                                // When the scroll buttons are being hidden, reset the scroll position to zero to
                                // make sure no tabs are still out of sight
                                if (scrollRightButton.isVisible() && !isScrolling) {
                                        resetScrollPosition();
                                }
                                scrollRightButton.setVisible(isScrolling);
                                scrollLeftButton.setVisible(isScrolling);
                        }
                });
        }

        private void resetScrollPosition() {
                scrollTo(0);
        }

        private void scrollTo(int pos) {
                tabBar.getElement().getStyle().setLeft(pos, Unit.PX);
        }

        private boolean isScrollingNecessary() {
                Widget lastTab = getLastTab();
                if (lastTab == null)
                        return false;

                return getRightOfWidget(lastTab) > getTabBarWidth();
        }

        private int getRightOfWidget(Widget widget) {
                return widget.getElement().getOffsetLeft() + widget.getElement().getOffsetWidth();
        }

        private int getTabBarWidth() {
                return tabBar.getElement().getParentElement().getClientWidth();
        }

        private Widget getLastTab() {
                if (tabBar.getWidgetCount() == 0)
                        return null;

                return tabBar.getWidget(tabBar.getWidgetCount() - 1);
        }

        private static int parsePosition(String positionString) {
                int position;
                try {
                        for (int i = 0; i < positionString.length(); i++) {
                                char c = positionString.charAt(i);
                                if (c != '-' && !(c >= '0' && c <= '9')) {
                                        positionString = positionString.substring(0, i);
                                }
                        }

                        position = Integer.parseInt(positionString);
                } catch (NumberFormatException ex) {
                        position = 0;
                }
                return position;
        }
}
This entry was posted in Google Web Toolkit, Java and tagged , , . Bookmark the permalink.

5 Responses to Scrollable GWT TabLayoutPanel

  1. OlivierH says:

    Excellent tip!
    Exactly what I needed.
    I will use it in a Composite that holds a TabLayoutPanel, which allows me to use it in UiBinder declarative layout.
    Thanks

  2. OliverH, could you post your modified version of this somewhere? That would be fantastic.

  3. Thanks for this tip.
    A small update without using the deprecated DeferredCommand:
    http://pastebin.com/8T4J2M0r

  4. geodma says:

    Nice one, I needed it.

Leave a reply to Matt Cancel reply