zul/src/main/java/org/zkoss/zul/Tab.java

Summary

Maintainability
D
2 days
Test Coverage
/* Tab.java

    Purpose:

    Description:

    History:
        Tue Jul 12 10:43:18     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
 */
package org.zkoss.zul;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.SelectEvent;
import org.zkoss.zul.impl.LabelImageElement;

/**
 * A tab.
 * <p>
 * Default {@link #getZclass}: z-tab. (since 3.5.0)
 *
 * <h3>Support child component</h3>
 * {@link Caption} child component is allowed.
 * [ZK EE]
 * [Since 6.5.0]
 *
 * @author tomyeh
 */
@SuppressWarnings("serial")
public class Tab extends LabelImageElement {

    private static final Logger log = LoggerFactory.getLogger(Tab.class);

    private boolean _selected;
    /** Whether to show a close button. */
    private boolean _closable;

    private boolean _disabled;
    private transient Caption _caption;
    private transient Object _value;

    static {
        addClientEvent(Tab.class, Events.ON_CLOSE, 0);
        addClientEvent(Tab.class, Events.ON_SELECT, CE_IMPORTANT);
    }

    public Tab() {
    }

    public Tab(String label) {
        super(label);
    }

    public Tab(String label, String image) {
        super(label, image);
    }

    /** Returns the value.
     * <p>Default: null.
     * <p>Note: the value is application dependent, you can place
     * whatever value you want.
     * @since 7.0.0
     */
    @SuppressWarnings("unchecked")
    public <T> T getValue() {
        return (T) _value;
    }

    /** Sets the value.
     * @param value the value.
     * <p>Note: the value is application dependent, you can place
     * whatever value you want.
     * @since 7.0.0
     */
    public <T> void setValue(T value) {
        if (!Objects.equals(_value, value)) {
            _value = value;
        }
    }

    /** Returns the caption of this tab.
     * @since 6.5.0
     */
    public Caption getCaption() {
        return _caption;
    }

    //-- super --//
    public void setWidth(String width) {
        Tabbox tb = getTabbox();
        if (tb != null && tb.isVertical())
            throw new UnsupportedOperationException("Set Tabs' width instead");
        super.setWidth(width);
    }

    /**
     * Returns whether this tab is closable. If closable, a button is displayed
     * and the onClose event is sent if a user clicks the button.
     * <p>
     * Default: false.
     */
    public boolean isClosable() {
        return _closable;
    }

    /**
     * Sets whether this tab is closable. If closable, a button is displayed and
     * the onClose event is sent if a user clicks the button.
     * <p>
     * Default: false.
     * <p>
     * You can intercept the default behavior by either overriding
     * {@link #onClose}, or listening the onClose event.
     *
     * <p>If {@link Tabbox#getModel()} is assigned, there is no an action to do with {@link #onClose},
     * i.e. developer has to listen onClose event to delete that item in model not
     * component itself. (since 7.0.0)
     */
    public void setClosable(boolean closable) {
        if (_closable != closable) {
            _closable = closable;
            smartUpdate("closable", _closable);
        }
    }

    /**
     * Process the onClose event sent when the close button is pressed.
     * <p>
     * Default: invoke {@link #close} to detach itself and the corresponding {@link Tabpanel}.
     */
    public void onClose() {
        close();
    }

    /** Closes this tab and the linked tabpanel.
     * This method detaches this component and the linked {@link Tabpanel}), only if
     * {@link Tabbox#getModel()} is null. (since 7.0.0)
     * @since 5.0.0
     */
    public void close() {
        if (_selected) {
            final Tab tab = selectNextTab();
            if (tab != null) {
                final Set<Tab> selItems = new HashSet<Tab>(2);
                selItems.add(tab);
                Events.postEvent(new SelectEvent<Tab, Object>(Events.ON_SELECT, tab, selItems));
            }
        }
        Tabbox tabbox = getTabbox();

        // Nothing to do according to ZK-2027 issue, let application developer to do so.
        if (tabbox != null && tabbox.getModel() != null)
            return;

        //Cache panel before detach , or we couldn't get it after tab is detached.
        final Tabpanel panel = getLinkedPanel();

        detach();

        if (panel != null) {
            // B60-ZK-1160: Exception when closing tab with included content
            // Must clean up included content before detaching tab panel
            Component include = panel.getFirstChild();
            if (include instanceof Include) {
                include.detach();
            }
            panel.detach();
        }
    }

    private Tab selectNextTab() {
        for (Tab tab = (Tab) getNextSibling(); tab != null; tab = (Tab) tab.getNextSibling())
            if (!tab.isDisabled()) {
                tab.setSelected(true);
                return tab;
            }
        for (Tab tab = (Tab) getPreviousSibling(); tab != null; tab = (Tab) tab.getPreviousSibling())
            if (!tab.isDisabled()) {
                tab.setSelected(true);
                return tab;
            }
        return null;
    }

    /**
     * Returns the tabbox owns this component.
     */
    public Tabbox getTabbox() {
        final Tabs tabs = (Tabs) getParent();
        return tabs != null ? tabs.getTabbox() : null;
    }

    /**
     * Returns the panel associated with this tab.
     */
    public Tabpanel getLinkedPanel() {
        final int j = getIndex();
        if (j >= 0) {
            final Tabbox tabbox = getTabbox();
            if (tabbox != null) {
                final Tabpanels tabpanels = tabbox.getTabpanels();
                if (tabpanels != null && tabpanels.getChildren().size() > j)
                    return (Tabpanel) tabpanels.getChildren().get(j);
            }
        }
        return null;
    }

    /**
     * Returns whether this tab is selected.
     */
    public boolean isSelected() {
        return _selected;
    }

    /**
     * Sets whether this tab is selected.
     */
    public void setSelected(boolean selected) {
        final Tabbox tabbox = (Tabbox) getTabbox();
        if (tabbox != null) {
            // Note: we don't update it here but let its parent does the job
            if (selected) { //Note that if already selected , tabbox will ignore it.
                tabbox.setSelectedTab(this);
            } else if (tabbox.getSelectedTab() == this) { //selected false and selected

                //clean selected tab , not set any others selected , if user call setSelected(false) manually ,
                //they should set another tab to be selected or no any tab will be selected.
                tabbox.clearSelectedTab();
                _selected = false;
            }
        } else if (_selected != selected) {
            _selected = selected;
            smartUpdate("selected", _selected);
        }
    }

    /**
     * Returns whether this tab is disabled.
     * <p>
     * Default: false.
     *
     * @since 3.0.0
     */
    public boolean isDisabled() {
        return _disabled;
    }

    /**
     * Sets whether this tab is disabled. If a tab is disabled, then it cann't
     * be selected or closed by user, but it still can be controlled by server
     * side program.
     *
     * @since 3.0.0
     */
    public void setDisabled(boolean disabled) {
        if (_disabled != disabled) {
            _disabled = disabled;
            smartUpdate("disabled", _disabled);
        }
    }

    /**
     * Updates _selected directly without updating the client.
     */
    /* package */void setSelectedDirectly(boolean selected) {
        _selected = selected;
    }

    /**
     * Returns the index of this panel, or -1 if it doesn't belong to any tabs.
     */
    public int getIndex() {
        final Tabs tabs = (Tabs) getParent();
        if (tabs == null)
            return -1;
        int j = 0;
        for (Iterator<Component> it = tabs.getChildren().iterator();; ++j)
            if (it.next() == this)
                return j;
    }

    // -- super --//
    public String getZclass() {
        return _zclass == null ? "z-tab" : _zclass;
    }

    // -- Component --//
    /**
     * Child is allowed, {@link Caption} only.
     * @since 6.5.0
     */
    protected boolean isChildable() {
        return true;
    }

    public void beforeChildAdded(Component child, Component refChild) {
        if (child instanceof Caption) {
            if (_caption != null && _caption != child)
                throw new UiException("Only one caption is allowed: " + this);
            super.beforeChildAdded(child, refChild);
        } else if (child instanceof Label) { // backward compatible
            super.beforeChildAdded(child, refChild);
        } else
            throw new UiException("Only caption is allowed: " + this);
    }

    // backward compatible
    private transient Label _tmpLabel;

    /**
     * Internal use only
     * @since 6.5.0
     */
    public void onCreate(Event evt) {
        if (_tmpLabel != null) {
            setLabel(_tmpLabel.getValue());
            removeChild(_tmpLabel);
        }
        _tmpLabel = null;
    }

    public boolean insertBefore(Component child, Component refChild) {
        if (child instanceof Caption) {
            refChild = getFirstChild();
            //always makes caption as the first child
            if (super.insertBefore(child, refChild)) {
                _caption = (Caption) child;
                invalidate();
                return true;
            }
            return false;
        } else if (child instanceof Label) { // backward compatible
            _tmpLabel = (Label) child;
            log.warn("Please use Tab#setLabel(msg) instead! [" + this + "]");
        }
        return super.insertBefore(child, refChild);

    }

    public void onChildRemoved(Component child) {
        if (child instanceof Caption) {
            _caption = null;
            invalidate();
        }
        super.onChildRemoved(child);
    }

    public void beforeParentChanged(Component parent) {
        if (parent != null && !(parent instanceof Tabs))
            throw new UiException("Wrong parent: " + parent);
        super.beforeParentChanged(parent);
    }

    //Cloneable//
    public Object clone() {
        final Tab clone = (Tab) super.clone();
        if (clone._caption != null)
            clone.afterUnmarshal();
        return clone;
    }

    private void afterUnmarshal() {
        for (Iterator<Component> it = getChildren().iterator(); it.hasNext();) {
            final Object child = it.next();
            if (child instanceof Caption) {
                _caption = (Caption) child;
                break;
            }
        }
    }

    //Serializable//
    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        afterUnmarshal();
    }

    // -- ComponentCtrl --//
    /** Processes an AU request.
     *
     * <p>Default: in addition to what are handled by {@link LabelImageElement#service},
     * it also handles onSelect.
     * @since 5.0.0
     */
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if (cmd.equals(Events.ON_SELECT)) {
            final Tabbox tabbox = getTabbox();
            final Set<Tab> prevSeldItems = new LinkedHashSet<Tab>();
            if (tabbox.getSelectedTab() != null)
                prevSeldItems.add(tabbox.getSelectedTab());
            final SelectEvent<Tab, Object> evt = SelectEvent.getSelectEvent(request,
                    new SelectEvent.SelectedObjectHandler<Tab>() {
                        public Set<Object> getObjects(Set<Tab> items) {
                            if (items == null || items.isEmpty() || tabbox.getModel() == null)
                                return null;
                            Set<Object> objs = new LinkedHashSet<Object>();
                            ListModel<Object> model = tabbox.getModel();
                            for (Tab i : items)
                                objs.add(model.getElementAt(i.getIndex()));
                            return objs;
                        }

                        public Set<Tab> getPreviousSelectedItems() {
                            return prevSeldItems;
                        }

                        // in single selection, getUnselectedItems() is same as getPreviousSelectedItems()
                        public Set<Tab> getUnselectedItems() {
                            return getPreviousSelectedItems();
                        }

                        public Set<Object> getPreviousSelectedObjects() {
                            ListModel<Object> model = tabbox.getModel();
                            Set<Tab> items = getPreviousSelectedItems();
                            if (model == null || items.size() < 1)
                                return null;
                            else {
                                Set<Object> s = new LinkedHashSet<Object>();
                                s.add(model.getElementAt(((Tab) items.iterator().next()).getIndex()));
                                return s;
                            }
                        }

                        // in single selection, getUnselectedObjects() is same as getPreviousSelectedObjects()
                        public Set<Object> getUnselectedObjects() {
                            return getPreviousSelectedObjects();
                        }
                    });

            final Set<Tab> selItems = evt.getSelectedItems();
            if (selItems == null || selItems.size() != 1)
                throw new UiException("Exactly one selected tab is required: " + selItems); // debug purpose

            if (tabbox != null)
                tabbox.selectTabDirectly((Tab) selItems.iterator().next(), true);

            Events.postEvent(evt);
        } else
            super.service(request, everError);
    }

    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
        super.renderProperties(renderer);
        if (_disabled)
            render(renderer, "disabled", _disabled);
        if (_selected)
            render(renderer, "selected", _selected);
        if (_closable)
            render(renderer, "closable", _closable);
    }
}