com.sencha.gxt.widget.core.client.TabPanel.java Source code

Java tutorial

Introduction

Here is the source code for com.sencha.gxt.widget.core.client.TabPanel.java

Source

/**
 * Sencha GXT 4.0.0 - Sencha for GWT
 * Copyright (c) 2006-2015, Sencha Inc.
 *
 * licensing@sencha.com
 * http://www.sencha.com/products/gxt/license/
 *
 * ================================================================================
 * Open Source License
 * ================================================================================
 * This version of Sencha GXT is licensed under the terms of the Open Source GPL v3
 * license. You may use this license only if you are prepared to distribute and
 * share the source code of your application under the GPL v3 license:
 * http://www.gnu.org/licenses/gpl.html
 *
 * If you are NOT prepared to distribute and share the source code of your
 * application under the GPL v3 license, other commercial and oem licenses
 * are available for an alternate download of Sencha GXT.
 *
 * Please see the Sencha GXT Licensing page at:
 * http://www.sencha.com/products/gxt/license/
 *
 * For clarification or additional options, please contact:
 * licensing@sencha.com
 * ================================================================================
 *
 *
 * ================================================================================
 * Disclaimer
 * ================================================================================
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 * ================================================================================
 */
package com.sencha.gxt.widget.core.client;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.logical.shared.BeforeSelectionEvent;
import com.google.gwt.event.logical.shared.BeforeSelectionHandler;
import com.google.gwt.event.logical.shared.HasBeforeSelectionHandlers;
import com.google.gwt.event.logical.shared.HasSelectionHandlers;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.uibinder.client.UiChild;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.IndexedPanel;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Widget;
import com.sencha.gxt.core.client.Style.ScrollDirection;
import com.sencha.gxt.core.client.Style.Side;
import com.sencha.gxt.core.client.dom.XDOM;
import com.sencha.gxt.core.client.dom.XElement;
import com.sencha.gxt.core.client.gestures.GestureRecognizer;
import com.sencha.gxt.core.client.gestures.LongPressOrTapGestureRecognizer;
import com.sencha.gxt.core.client.gestures.TapGestureRecognizer;
import com.sencha.gxt.core.client.gestures.TouchData;
import com.sencha.gxt.core.client.util.AccessStack;
import com.sencha.gxt.core.client.util.Point;
import com.sencha.gxt.core.client.util.Size;
import com.sencha.gxt.fx.client.FxElement;
import com.sencha.gxt.fx.client.animation.AfterAnimateEvent;
import com.sencha.gxt.fx.client.animation.AfterAnimateEvent.AfterAnimateHandler;
import com.sencha.gxt.fx.client.animation.Fx;
import com.sencha.gxt.messages.client.DefaultMessages;
import com.sencha.gxt.widget.core.client.container.CardLayoutContainer;
import com.sencha.gxt.widget.core.client.container.HasActiveWidget;
import com.sencha.gxt.widget.core.client.container.HasLayout;
import com.sencha.gxt.widget.core.client.event.BeforeCloseEvent;
import com.sencha.gxt.widget.core.client.event.BeforeCloseEvent.BeforeCloseHandler;
import com.sencha.gxt.widget.core.client.event.BeforeCloseEvent.HasBeforeCloseHandlers;
import com.sencha.gxt.widget.core.client.event.CloseEvent;
import com.sencha.gxt.widget.core.client.event.CloseEvent.CloseHandler;
import com.sencha.gxt.widget.core.client.event.CloseEvent.HasCloseHandlers;
import com.sencha.gxt.widget.core.client.event.HideEvent;
import com.sencha.gxt.widget.core.client.event.HideEvent.HideHandler;
import com.sencha.gxt.widget.core.client.event.XEvent;
import com.sencha.gxt.widget.core.client.menu.Menu;
import com.sencha.gxt.widget.core.client.menu.MenuItem;

/**
 * A basic tab container.
 * <p/>
 * Code snippet:
 * <p/>
 * 
 * <pre>
 *   TabPanel panel = new TabPanel();
 *   panel.setTabScroll(true);
 *   panel.setAnimScroll(true);
 *   panel.add(new Label("Tab 1 Content"), new TabItemConfig("Tab 1", true));
 *   panel.add(new Label("Tab 2 Content"), new TabItemConfig("Tab 2", true));
 * </pre>
 */
public class TabPanel extends Component implements IndexedPanel.ForIsWidget, HasActiveWidget,
        HasBeforeSelectionHandlers<Widget>, HasSelectionHandlers<Widget>, HasBeforeCloseHandlers<Widget>,
        HasCloseHandlers<Widget>, HasLayout, HasWidgets {

    @SuppressWarnings("javadoc")
    public static interface TabPanelAppearance {

        void createScrollers(XElement parent);

        XElement getBar(XElement parent);

        XElement getBody(XElement parent);

        String getItemSelector();

        XElement getScrollLeft(XElement parent);

        XElement getScrollRight(XElement parent);

        XElement getStripEdge(XElement parent);

        XElement getStripWrap(XElement parent);

        void insert(XElement parent, TabItemConfig config, int index);

        boolean isClose(XElement target);

        void onDeselect(Element item);

        void onMouseOut(XElement parent, XElement target);

        void onMouseOver(XElement parent, XElement target);

        void onScrolling(XElement bar, boolean scrolling);

        void onSelect(Element item);

        void render(SafeHtmlBuilder builder);

        void setItemWidth(XElement element, int width);

        void updateItem(XElement item, TabItemConfig config);

        void updateScrollButtons(XElement parent);

    }

    public static interface TabPanelBottomAppearance extends TabPanelAppearance {

    }

    @SuppressWarnings("javadoc")
    public interface TabPanelMessages {

        String closeOtherTabs();

        String closeTab();

    }

    protected class DefaultTabPanelMessages implements TabPanelMessages {

        public String closeOtherTabs() {
            return DefaultMessages.getMessages().tabPanelItem_closeOtherText();
        }

        public String closeTab() {
            return DefaultMessages.getMessages().tabPanelItem_closeText();
        }

    }

    private final TabPanelAppearance appearance;

    protected Menu closeContextMenu;
    private GestureRecognizer closeContextMenuGestureRecognizer;
    private boolean animScroll = true;
    private boolean autoSelect = true;
    private boolean bodyBorder = true;
    private boolean closeMenu = false;
    private boolean scheduledDelegateUpdates;
    private TabPanelMessages messages;
    private boolean resizeTabs = false;
    private int scrollDuration = 150;
    private int scrollIncrement = 100;
    private boolean scrolling;
    private AccessStack<Widget> stack;
    private boolean tabScroll = false;
    private Widget contextMenuItem;

    private int tabMargin = 2;
    private int tabWidth = 120;
    private int minTabWidth = 30;

    private HashMap<Widget, TabItemConfig> configMap = new HashMap<Widget, TabItemConfig>();
    private CardLayoutContainer container = new CardLayoutContainer() {
        @Override
        protected Widget getParentLayoutWidget() {
            return container.getParent();
        }

        protected void onRemove(Widget child) {
            super.onRemove(child);
            configMap.remove(child);
        }
    };

    /**
     * Creates a new tab panel with the default appearance.
     */
    public TabPanel() {
        this(GWT.<TabPanelAppearance>create(TabPanelAppearance.class));
    }

    /**
     * Creates a new tab panel with the specified appearance.
     * 
     * @param appearance the appearance of the tab panel
     */
    public TabPanel(TabPanelAppearance appearance) {
        this.appearance = appearance;

        SafeHtmlBuilder sb = new SafeHtmlBuilder();
        appearance.render(sb);

        setElement((Element) XDOM.create(sb.toSafeHtml()));

        ComponentHelper.setParent(this, container);
        appearance.getBody(getElement()).appendChild(container.getElement());

        setDeferHeight(true);

        addGestureRecognizer(new TapGestureRecognizer() {

            @Override
            protected void onTap(TouchData touchData) {
                TabPanel.this.onTap(touchData.getLastNativeEvent().<Event>cast());
                super.onTap(touchData);
            }

            @Override
            protected void handlePreventDefault(NativeEvent event) {
                XElement target = event.getEventTarget().cast();
                if (getAppearance().getBar(getElement()).isOrHasChild(target)) {
                    event.preventDefault();
                }
            }
        });
    }

    /**
     * Adds an item to the tab panel with the specified tab configuration.
     * 
     * @param widget the widget to add to the tab panel
     * @param config the configuration of the tab
     */
    @UiChild(tagname = "child")
    public void add(IsWidget widget, TabItemConfig config) {
        add(asWidgetOrNull(widget), config);
    }

    /**
     * Adds an item to the tab panel with the specified text.
     * 
     * @param widget the widget to add to the tab panel
     * @param text the text for the tab
     */
    public void add(IsWidget widget, String text) {
        add(asWidgetOrNull(widget), text);
    }

    @Override
    public void add(Widget w) {
        throw new UnsupportedOperationException("this add method not supported");
    }

    /**
     * Adds an item to the tab panel with the specified text. Shorthand for {@link #add(Widget, TabItemConfig)}.
     * 
     * @param widget the widget to add to the tab panel
     * @param text the text for the tab
     */
    public void add(Widget widget, String text) {
        insert(widget, getWidgetCount(), new TabItemConfig(text));
    }

    /**
     * Adds an item to the tab panel with the specified tab configuration.
     * 
     * @param widget the item to add to the tab panel
     * @param config the configuration of the tab
     */
    public void add(Widget widget, TabItemConfig config) {
        insert(widget, getWidgetCount(), config);
    }

    @Override
    public HandlerRegistration addBeforeCloseHandler(BeforeCloseHandler<Widget> handler) {
        return addHandler(handler, BeforeCloseEvent.getType());
    }

    @Override
    public HandlerRegistration addBeforeSelectionHandler(BeforeSelectionHandler<Widget> handler) {
        return addHandler(handler, BeforeSelectionEvent.getType());
    }

    @Override
    public HandlerRegistration addCloseHandler(CloseHandler<Widget> handler) {
        return addHandler(handler, CloseEvent.getType());
    }

    @Override
    public HandlerRegistration addSelectionHandler(SelectionHandler<Widget> handler) {
        return addHandler(handler, SelectionEvent.getType());
    }

    @Override
    public void clear() {
        container.clear();
    }

    /**
     * Searches for a child widget based on its id and optionally the text of the {@link TabItemConfig}.
     * <p/>
     * Iterates through each item and checks if its id matches the parameter {@code id}. Then, if the
     * {@code alsoCheckText} parameter is true, this also looks at the last passed in string of html or text for each
     * {@code TabItemConfig}. If that content matches, then it returns that tab.
     * <p/>
     * With {@code alsoCheckText} set to true, the first matching item will be returned.
     * 
     * @param id the item id
     * @param alsoCheckText {@code false} to only compare with id, {@code true} to compare both the items id and text
     * @return the item
     */
    public Widget findItem(String id, boolean alsoCheckText) {
        int count = container.getWidgetCount();
        for (int i = 0; i < count; i++) {
            Widget item = container.getWidget(i);
            String widgetId = ComponentHelper.getWidgetId(item);

            if (widgetId.equals(id))
                return item;
            if (item instanceof Component && ((Component) item).getItemId().equals(id))
                return item;
            if (alsoCheckText && getConfig(item).getHTML().equals(id))
                return item;
        }
        return null;
    }

    @Override
    public void forceLayout() {
        container.forceLayout();

    }

    @Override
    public Widget getActiveWidget() {
        return container.getActiveWidget();
    }

    public TabPanelAppearance getAppearance() {
        return appearance;
    }

    /**
     * Returns true if scrolling is animated.
     * 
     * @return the animation scroll state
     */
    public boolean getAnimScroll() {
        return animScroll;
    }

    /**
     * Returns true if the body border is enabled.
     * 
     * @return the body border state
     */
    public boolean getBodyBorder() {
        return bodyBorder;
    }

    /**
     * Returns the tab item config for the given widget.
     * 
     * @param widget the widget
     * @return the config or null
     */
    public TabItemConfig getConfig(Widget widget) {
        return configMap.get(widget);
    }

    /**
     * Returns the internal card layout container.
     * 
     * @return the card layout container
     */
    public CardLayoutContainer getContainer() {
        return container;
    }

    /**
     * Returns the tab panel messages.
     * 
     * @return the messages
     */
    public TabPanelMessages getMessages() {
        if (messages == null) {
            messages = new DefaultTabPanelMessages();
        }
        return messages;
    }

    /**
     * Returns the minimum tab width.
     * 
     * @return the minimum tab width
     */
    public int getMinTabWidth() {
        return minTabWidth;
    }

    /**
     * Returns true if tab resizing is enabled.
     * 
     * @return the tab resizing state
     */
    public boolean getResizeTabs() {
        return resizeTabs;
    }

    /**
     * Returns the scroll duration in milliseconds.
     * 
     * @return the duration
     */
    public int getScrollDuration() {
        return scrollDuration;
    }

    /**
     * Returns the panel's tab margin.
     * 
     * @return the margin
     */
    public int getTabMargin() {
        return tabMargin;
    }

    /**
     * Returns true if tab scrolling is enabled.
     * 
     * @return the tab scroll state
     */
    public boolean getTabScroll() {
        return tabScroll;
    }

    /**
     * Returns the default tab width.
     * 
     * @return the width
     */
    public int getTabWidth() {
        return tabWidth;
    }

    @Override
    public Widget getWidget(int index) {
        return container.getWidget(index);
    }

    @Override
    public int getWidgetCount() {
        return container.getWidgetCount();
    }

    @Override
    public int getWidgetIndex(IsWidget child) {
        return container.getWidgetIndex(child);
    }

    public int getWidgetIndex(Widget widget) {
        return container.getWidgetIndex(widget);
    }

    /**
     * Inserts the specified item into the tab panel.
     * 
     * @param widget the item to insert
     * @param index the insert index
     * @param config the configuration of the tab item
     */
    public void insert(Widget widget, int index, TabItemConfig config) {
        configMap.put(widget, config);
        container.insert(widget, index);
        appearance.insert(getElement(), config, index);

        if (getActiveWidget() == null && autoSelect) {
            setActiveWidget(widget);
        }

        if (getWidgetCount() == 1) {
            syncSize();
        }
    }

    /**
     * Returns true if auto select is enabled.
     * 
     * @return the auto select state
     */
    public boolean isAutoSelect() {
        return autoSelect;
    }

    /**
     * Returns true if close context menu is enabled.
     * 
     * @return the close menu state
     */
    public boolean isCloseContextMenu() {
        return closeMenu;
    }

    @Override
    public boolean isLayoutRunning() {
        return container.isLayoutRunning();
    }

    @Override
    public boolean isOrWasLayoutRunning() {
        return container.isOrWasLayoutRunning();
    }

    @Override
    public Iterator<Widget> iterator() {
        return container.iterator();
    }

    @Override
    public void onBrowserEvent(Event event) {
        XElement target = event.getEventTarget().cast();
        if (target == null) {
            return;
        }
        boolean isbar = appearance.getBar(getElement()).isOrHasChild(target);
        boolean orig = disableContextMenu;

        // allow right clicks in tab panel body
        if (!isbar && disableContextMenu) {
            disableContextMenu = false;
        }

        super.onBrowserEvent(event);

        if (!isbar) {
            disableContextMenu = orig;
            return;
        }

        Element item = findItem(target);
        if (item != null) {
            int index = itemIndex(item);
            if (index < 0) {
                // tab may have already closed
                return;
            }
            TabItemConfig config = getConfig(getWidget(index));
            if (config != null && !config.isEnabled()) {
                return;
            }
        }

        switch (event.getTypeInt()) {
        case Event.ONCLICK:
            onClick(event);
            break;
        case Event.ONMOUSEOVER:
            appearance.onMouseOver(getElement(), event.getEventTarget().<XElement>cast());
            break;
        case Event.ONMOUSEOUT:

            appearance.onMouseOut(getElement(), event.getEventTarget().<XElement>cast());
            break;
        }
    }

    @Override
    public boolean remove(int index) {
        return remove(getWidget(index));
    }

    public boolean remove(Widget child) {
        int idx = getWidgetIndex(child);
        Widget activeWidget = getActiveWidget();
        boolean removed = container.remove(child);

        if (removed) {
            if (stack != null) {
                stack.remove(child);
            }

            Element item = findItem(idx);
            item.removeFromParent();

            if (child == activeWidget) {
                Widget next = stack != null ? stack.next() : null;
                if (next != null) {
                    setActiveWidget(next);
                } else if (getWidgetCount() > 0) {
                    setActiveWidget(getWidget(0));
                } else {
                    setActiveWidget(null);
                }
            }
            delegateUpdates();
        }

        return removed;
    }

    /**
     * Scrolls to a particular tab if tab scrolling is enabled.
     * 
     * @param item the item to scroll to
     * @param animate true to animate the scroll
     */
    public void scrollToTab(Widget item, boolean animate) {
        if (item == null)
            return;
        int pos = getScrollPos();
        int area = getScrollArea();
        XElement itemEl = findItem(container.getWidgetIndex(item)).cast();
        int left = itemEl.getOffsetsTo(getStripWrap()).getX() + pos;
        int right = left + itemEl.getOffsetWidth();
        if (left < pos) {
            scrollTo(left, animate);
        } else if (right > (pos + area)) {
            scrollTo(right - area, animate);
        }
    }

    /**
     * Sets the active widget.
     * 
     * @param widget the widget
     */
    public void setActiveWidget(IsWidget widget) {
        setActiveWidget(asWidgetOrNull(widget));
    }

    /**
     * Sets the active widget.
     * 
     * @param item the widget
     * @param fireEvents {@code true} to fire events
     */
    public void setActiveWidget(Widget item, boolean fireEvents) {
        if (item == null || item.getParent() != container) {
            return;
        }

        if (getActiveWidget() != item) {
            if (fireEvents) {
                BeforeSelectionEvent<Widget> event = BeforeSelectionEvent.fire(this, item);
                // event can be null if no handlers
                if (event != null && event.isCanceled()) {
                    return;
                }
            }

            if (getActiveWidget() != null) {
                appearance.onDeselect(findItem(getWidgetIndex(getActiveWidget())));
            }
            appearance.onSelect(findItem(getWidgetIndex(item)));
            container.setActiveWidget(item);

            if (stack == null) {
                stack = new AccessStack<Widget>();
            }
            stack.add(item);

            focusTab(item, false);
            if (fireEvents) {
                SelectionEvent.fire(this, item);
            }
            delegateUpdates();
        }
    }

    @Override
    public void setActiveWidget(Widget item) {
        setActiveWidget(item, true);
    }

    /**
     * True to animate tab scrolling so that hidden tabs slide smoothly into view (defaults to true). Only applies when
     * {@link #tabScroll} = true.
     * 
     * @param animScroll the animation scroll state
     */
    public void setAnimScroll(boolean animScroll) {
        this.animScroll = animScroll;
    }

    /**
     * True to have the first item selected when the panel is displayed for the first time if there is not selection
     * (defaults to true).
     * 
     * @param autoSelect the auto select state
     */
    public void setAutoSelect(boolean autoSelect) {
        this.autoSelect = autoSelect;
    }

    /**
     * True to display an interior border on the body element of the panel, false to hide it (defaults to true,
     * pre-render).
     * 
     * @param bodyBorder the body border style
     */
    public void setBodyBorder(boolean bodyBorder) {
        this.bodyBorder = bodyBorder;
    }

    /**
     * True to show the close context menu (defaults to false).
     * 
     * @param closeMenu true to show it
     */
    public void setCloseContextMenu(boolean closeMenu) {
        this.closeMenu = closeMenu;
        disableContextMenu = true;
        if (closeMenu) {
            sinkEvents(Event.ONCONTEXTMENU);
        }

        if (closeContextMenuGestureRecognizer == null) {
            closeContextMenuGestureRecognizer = new LongPressOrTapGestureRecognizer() {
                @Override
                protected void onLongPress(TouchData touchData) {
                    super.onLongPress(touchData);
                    onRightClick((Event) touchData.getLastNativeEvent());
                }

                @Override
                public boolean handleEnd(NativeEvent endEvent) {
                    // onRightClick does preventDefault and stopPropagation
                    cancel();
                    return super.handleEnd(endEvent);
                }
            };
            addGestureRecognizer(closeContextMenuGestureRecognizer);
        }
    }

    /**
     * Sets the tab panel messages.
     * 
     * @param messages the messages
     */
    public void setMessages(TabPanelMessages messages) {
        this.messages = messages;
    }

    /**
     * The minimum width in pixels for each tab when {@link #resizeTabs} = true (defaults to 30).
     * 
     * @param minTabWidth the minimum tab width
     */
    public void setMinTabWidth(int minTabWidth) {
        this.minTabWidth = minTabWidth;
    }

    /**
     * True to automatically resize tabs. The resize operation takes into consideration the current width of the tab panel
     * as well as the current values of {@link #setTabWidth(int)} and {@link #setMinTabWidth(int)}. The resulting tab
     * width will not be less than the value specified by <code>setMinTabWidth</code> nor greater than the value specified
     * by <code>setTabWidth</code>. To automatically resize the tabs to completely fill the tab strip, use
     * <code>setTabWidth(Integer.MAX_VALUE)</code> and <code>setResizeTabs(true)</code>.
     * 
     * @param resizeTabs true to enable tab resizing
     */
    public void setResizeTabs(boolean resizeTabs) {
        this.resizeTabs = resizeTabs;
    }

    /**
     * Sets the number of milliseconds that each scroll animation should last (defaults to 150).
     * 
     * @param scrollDuration the scroll duration
     */
    public void setScrollDuration(int scrollDuration) {
        this.scrollDuration = scrollDuration;
    }

    /**
     * Sets the number of pixels to scroll each time a tab scroll button is pressed (defaults to 100, or if
     * {@link #setResizeTabs(boolean)} = true, the calculated tab width). Only applies when {@link #setTabScroll(boolean)}
     * = true.
     * 
     * @param scrollIncrement the scroll increment
     */
    public void setScrollIncrement(int scrollIncrement) {
        this.scrollIncrement = scrollIncrement;
    }

    /**
     * The number of pixels of space to calculate into the sizing and scrolling of tabs (defaults to 2).
     * 
     * @param tabMargin the tab margin
     */
    public void setTabMargin(int tabMargin) {
        this.tabMargin = tabMargin;
    }

    /**
     * True to enable scrolling to tabs that may be invisible due to overflowing the overall TabPanel width. Only
     * available with tabs on top. (defaults to false).
     * 
     * @param tabScroll true to enable tab scrolling
     */
    public void setTabScroll(boolean tabScroll) {
        this.tabScroll = tabScroll;
    }

    /**
     * Sets the initial width in pixels of each new tab (defaults to 120).
     * 
     * @param tabWidth the tab width
     */
    public void setTabWidth(int tabWidth) {
        this.tabWidth = tabWidth;
    }

    /**
     * Updates the appearance of the specified tab item. Must be invoked after changing the tab item configuration.
     * 
     * @param widget the widget for the tab to update
     * @param config the new or updated tab item configuration
     */
    public void update(Widget widget, TabItemConfig config) {
        XElement item = findItem(getWidgetIndex(widget));
        if (item != null) {
            configMap.put(widget, config);
            appearance.updateItem(item, config);
        }
    }

    protected void close(Widget item) {
        if (fireCancellableEvent(new BeforeCloseEvent<Widget>(item)) && remove(item)) {
            fireEvent(new CloseEvent<Widget>(item));
        }
    }

    @Override
    protected void doAttachChildren() {
        super.doAttachChildren();
        ComponentHelper.doAttach(container);
    }

    @Override
    protected void doDetachChildren() {
        super.doDetachChildren();
        ComponentHelper.doDetach(container);
    }

    protected Element findItem(Element target) {
        return target.<XElement>cast().findParentElement(appearance.getItemSelector(), -1);
    }

    protected XElement findItem(int index) {
        NodeList<Element> items = appearance.getStripWrap(getElement()).select(appearance.getItemSelector());
        return items.getItem(index).cast();
    }

    protected XElement getStripWrap() {
        return appearance.getStripWrap(getElement());
    }

    protected int itemIndex(Element item) {
        NodeList<Element> items = appearance.getStripWrap(getElement()).select(appearance.getItemSelector());
        for (int i = 0; i < items.getLength(); i++) {
            if (items.getItem(i) == item) {
                return i;
            }
        }
        return -1;
    }

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

        if (!bodyBorder) {
            appearance.getBody(getElement()).getStyle().setProperty("border", "none");
        }

        getElement().setTabIndex(0);
        getElement().setAttribute("hideFocus", "true");

        sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.ONKEYUP | Event.FOCUSEVENTS);
    }

    @Override
    protected void onAttach() {
        super.onAttach();
        appearance.getBar(getElement()).disableTextSelection(true);

        if (getActiveWidget() == null && autoSelect && getWidgetCount() > 0) {
            setActiveWidget(getWidget(0));
        }
    }

    @Override
    protected void onBlur(Event event) {
        super.onBlur(event);
    }

    protected void onClick(Event event) {
        XElement target = event.getEventTarget().cast();
        Element item = findItem(target);
        if (item != null) {
            event.stopPropagation();
            Widget w = getWidget(itemIndex(item));
            boolean close = appearance.isClose(target);
            if (close) {
                close(w);
            } else if (w != getActiveWidget()) {
                setActiveWidget(w);
                focusTab(w, true);
            } else if (w == getActiveWidget()) {
                focusTab(w, true);
            }
        }

        if (appearance.getScrollLeft(getElement()) != null
                && target.isOrHasChild(appearance.getScrollLeft(getElement()))) {
            event.stopPropagation();
            onScrollLeft();
        }
        if (appearance.getScrollRight(getElement()) != null
                && target.isOrHasChild(appearance.getScrollRight(getElement()))) {
            event.stopPropagation();
            onScrollRight();
        }
    }

    @Override
    protected void onDetach() {
        appearance.getBar(getElement()).disableTextSelection(false);
        super.onDetach();
    }

    @Override
    protected void onFocus(Event event) {
        if (getWidgetCount() > 0 && getActiveWidget() == null) {
            setActiveWidget(getWidget(0));
        } else if (getActiveWidget() != null) {
            focusTab(getActiveWidget(), true);
        }
    }

    protected void onItemContextMenu(final Widget item, int x, int y) {
        if (closeMenu) {
            if (closeContextMenu == null) {
                closeContextMenu = new Menu();
                closeContextMenu.addHideHandler(new HideHandler() {

                    @Override
                    public void onHide(HideEvent event) {
                        contextMenuItem = null;
                    }
                });

                closeContextMenu.add(new MenuItem(getMessages().closeTab(), new SelectionHandler<MenuItem>() {
                    @Override
                    public void onSelection(SelectionEvent<MenuItem> event) {
                        close(contextMenuItem);
                    }
                }));

                closeContextMenu.add(new MenuItem(getMessages().closeOtherTabs(), new SelectionHandler<MenuItem>() {
                    @Override
                    public void onSelection(SelectionEvent<MenuItem> event) {
                        List<Widget> widgets = new ArrayList<Widget>();
                        for (int i = 0, len = getWidgetCount(); i < len; i++) {
                            widgets.add(getWidget(i));
                        }

                        for (Widget w : widgets) {
                            TabItemConfig config = getConfig(w);
                            if (w != contextMenuItem && config.isClosable()) {
                                close(w);
                            }
                        }
                    }

                }));
            }
            TabItemConfig c = configMap.get(item);
            MenuItem mi = (MenuItem) closeContextMenu.getWidget(0);
            mi.setEnabled(c.isClosable());
            contextMenuItem = item;
            boolean hasClosable = false;

            for (int i = 0, len = getWidgetCount(); i < len; i++) {
                Widget item2 = container.getWidget(i);
                TabItemConfig config = configMap.get(item2);
                if (config.isClosable() && item2 != item) {
                    hasClosable = true;
                    break;
                }
            }
            MenuItem m = (MenuItem) closeContextMenu.getWidget(1);
            m.setEnabled(hasClosable);
            closeContextMenu.showAt(x, y);
        }
    }

    protected void onItemTextChange(Widget tabItem, String oldText, String newText) {
        delegateUpdates();
    }

    @Override
    protected void onResize(int width, int height) {
        super.onResize(width, height);
        Size frameWidth = getElement().getFrameSize();

        if (!isAutoHeight()) {
            height -= frameWidth.getHeight() + appearance.getBar(getElement()).getOffsetHeight();
        }
        if (!isAutoWidth()) {
            width -= frameWidth.getWidth();
        }

        appearance.getBody(getElement()).setSize(width, height, true);
        appearance.getBar(getElement()).setWidth(width, true);

        if (!isAutoHeight()) {
            height -= appearance.getBody(getElement()).getFrameWidth(Side.TOP, Side.BOTTOM);
        }
        if (!isAutoWidth()) {
            width -= appearance.getBody(getElement()).getFrameWidth(Side.LEFT, Side.RIGHT);
        }
        container.setPixelSize(width, height);

        delegateUpdates();
    }

    @Override
    protected void onRightClick(Event event) {
        Element target = event.getEventTarget().cast();
        if (appearance.getBar(getElement()).isOrHasChild(target)) {
            Element item = findItem(event.getEventTarget().<Element>cast());
            if (item != null) {
                int idx = itemIndex(item);
                if (idx != -1) {
                    event.preventDefault();
                    event.stopPropagation();

                    Point point = event.<XEvent>cast().getXY();

                    final Widget w = getWidget(idx);
                    final int x = point.getX();
                    final int y = point.getY();
                    Scheduler.get().scheduleDeferred(new ScheduledCommand() {

                        @Override
                        public void execute() {
                            onItemContextMenu(w, x, y);
                        }
                    });
                }
            }
        } else {
            super.onRightClick(event);
        }
    }

    protected void onTap(Event event) {
        XElement target = event.getEventTarget().cast();
        Element item = findItem(target);
        if (item != null) {
            int index = itemIndex(item);
            if (index < 0) {
                return;
            }
            TabItemConfig config = getConfig(getWidget(index));
            if (config != null && !config.isEnabled()) {
                return;
            }
        }
        onClick(event);
    }

    @Override
    protected void onUnload() {
        super.onUnload();
        if (stack != null) {
            stack.clear();
        }
    }

    private void autoScrollTabs() {
        int count = getWidgetCount();
        int tw = appearance.getBar(getElement()).getClientWidth();
        if (tw == 0) {
            tw = getElement().getStyleSize().getWidth();
        }

        XElement stripWrap = appearance.getStripWrap(getElement());
        XElement edge = appearance.getStripEdge(getElement());
        XElement scrollLeft = appearance.getScrollLeft(getElement());
        XElement scrollRight = appearance.getScrollRight(getElement());

        int cw = stripWrap.getOffsetWidth();

        int pos = getScrollPos();
        int l = edge.<XElement>cast().getOffsetsTo(stripWrap).getX() + pos;

        if (!getTabScroll() || count < 1 || cw < 20) {
            return;
        }

        if (l <= tw) {
            stripWrap.<XElement>cast().setWidth(tw);
            if (scrolling) {
                stripWrap.setScrollLeft(0);
                scrolling = false;

                scrollLeft.setVisible(false);
                scrollRight.setVisible(false);

                // add a class that CSS can hook into to add/remove padding as necessary when scrollers change visibility
                scrollLeft.addClassName("x-tabScrollerLeftHidden");
                appearance.onScrolling(getElement(), false);
            }
        } else {
            if (!scrolling) {
                appearance.onScrolling(getElement(), true);
            }

            if (!scrolling) {
                if (scrollLeft == null) {
                    appearance.createScrollers(getElement());

                    // need to re-initialize scrollers as they will still be null
                    scrollLeft = appearance.getScrollLeft(getElement());
                    scrollRight = appearance.getScrollRight(getElement());
                } else {
                    scrollLeft.setVisible(true);
                    scrollRight.setVisible(true);
                    scrollLeft.removeClassName("x-tabScrollerLeftHidden");
                }
            }

            // account for scrollers before setting width
            tw -= scrollLeft.getComputedWidth() + scrollRight.getComputedWidth();
            stripWrap.<XElement>cast().setWidth(tw > 20 ? tw : 20);
            scrolling = true;
            if (pos > (l - tw)) {
                stripWrap.setScrollLeft(l - tw);
            } else {
                scrollToTab(getActiveWidget(), false);
            }
            appearance.updateScrollButtons(getElement());
        }
    }

    private void autoSizeTabs() {
        int count = getWidgetCount();
        if (count == 0)
            return;
        int aw = appearance.getBar(getElement()).getClientWidth();
        if (aw == 0) {
            aw = getElement().getStyleSize().getWidth();
        }
        int each = (int) Math.max(Math.min(Math.floor((aw - 4) / count) - tabMargin, tabWidth), minTabWidth);

        for (int i = 0; i < count; i++) {
            XElement el = findItem(i).cast();
            appearance.setItemWidth(el, each);
        }
    }

    private void delegateUpdates() {
        if (!scheduledDelegateUpdates) {
            scheduledDelegateUpdates = true;
            Scheduler.get().scheduleFinally(new ScheduledCommand() {
                @Override
                public void execute() {
                    scheduledDelegateUpdates = false;
                    if (resizeTabs) {
                        autoSizeTabs();
                    }
                    if (tabScroll) {
                        autoScrollTabs();
                    }
                }
            });
        }
    }

    private void focusTab(Widget item, boolean setFocus) {
        if (setFocus) {
            // item.getHeader().getElement().focus();
        }
    }

    private int getScrollArea() {
        return Math.max(0, appearance.getStripWrap(getElement()).getClientWidth());
    }

    private int getScrollIncrement() {
        return scrollIncrement;
    }

    private int getScrollPos() {
        return appearance.getStripWrap(getElement()).getScrollLeft();
    }

    private int getScrollWidth() {
        return appearance.getStripEdge(getElement()).getOffsetsTo(getStripWrap()).getX() + getScrollPos();
    }

    private void onScrollLeft() {
        int pos = getScrollPos();
        int s = Math.max(0, pos - getScrollIncrement());
        if (s != pos) {
            scrollTo(s, getAnimScroll());
        }
    }

    private void onScrollRight() {
        int sw = getScrollWidth() - getScrollArea();
        int pos = getScrollPos();
        int s = Math.min(sw, pos + getScrollIncrement());
        if (s != pos) {
            scrollTo(s, getAnimScroll());
        }
    }

    private void scrollTo(int pos, boolean animate) {
        XElement stripWrap = getStripWrap();
        if (animate) {
            Fx fx = new Fx();
            fx.addAfterAnimateHandler(new AfterAnimateHandler() {
                @Override
                public void onAfterAnimate(AfterAnimateEvent event) {
                    appearance.updateScrollButtons(getElement());
                }
            });
            stripWrap.<FxElement>cast().scrollTo(ScrollDirection.LEFT, pos, true, fx);
        } else {
            stripWrap.setScrollLeft(pos);
            appearance.updateScrollButtons(getElement());
        }
    }
}