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; } }
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
OliverH, could you post your modified version of this somewhere? That would be fantastic.
Thanks for this tip.
A small update without using the deprecated DeferredCommand:
http://pastebin.com/8T4J2M0r
Thanks for the update
Nice one, I needed it.