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

Summary

Maintainability
F
5 days
Test Coverage
/* Tabbox.java

    Purpose:

    Description:

    History:
        Tue Jul 12 10:42:31     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.Iterator;

import org.zkoss.io.Serializables;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApps;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Deferrable;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.event.ListDataListener;
import org.zkoss.zul.event.ZulEvents;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.impl.TabboxEngine;
import org.zkoss.zul.impl.Utils;
import org.zkoss.zul.impl.XulElement;

/**
 * A tabbox.
 *
 * <p>
 * Event:
 * <ol>
 * <li>org.zkoss.zk.ui.event.SelectEvent is sent when user changes the tab.</li>
 * </ol>
 *
 * <p>
 * Mold:
 * <dl>
 * <dt>default</dt>
 * <dd>The default tabbox.</dd>
 * <dt>accordion</dt>
 * <dd>The accordion tabbox.</dd>
 * </dl>
 *
 * <p>{@link Toolbar} only works in the horizontal default mold and
 * the {@link #isTabscroll()} to be true. (since 3.6.3)
 *  
 * <p>Default {@link #getZclass}: z-tabbox. (since 3.5.0)
 *
 * <p>
 * Besides creating {@link Tab}  and {@link Tabpanel} programmatically, you could
 * assign a data model (a {@link ListModel} to a Tabbox via {@link #setModel(ListModel)}
 * and then the tabbox will retrieve data via {@link ListModel#getElementAt} when
 * necessary. (since 7.0.0) [ZK EE]
 *
 * <p>
 * Besides assign a list model, you could assign a renderer (a
 * {@link TabboxRenderer} instance) to a Tabbox, such that the Tabbox will
 * use this renderer to render the data returned by
 * {@link ListModel#getElementAt}. If not assigned, the default renderer, which
 * assumes a label per Tab and Tabpanel, is used. In other words, the default renderer
 * adds a label to a Tab and Tabpanel by calling toString against the object returned by
 * {@link ListModel#getElementAt}  (since 7.0.0) [ZK EE]
 *
 * <p>To retrieve what are selected in Tabbox with a {@link Selectable}
 * {@link ListModel}, you shall use {@link Selectable#getSelection} to get what
 * is currently selected object in {@link ListModel} rather than using
 * {@link Tabbox#getSelectedTab()}. That is, you shall operate on the data of
 * the {@link ListModel} rather than on the {@link Tab} of the {@link Tabbox}
 * if you use the {@link Selectable} {@link ListModel}.  (since 7.0.0) [ZK EE]
 *
 * <pre><code>
 * Set selection = ((Selectable)getModel()).getSelection();
 * </code></pre>
 * 
 * @author tomyeh
 */
public class Tabbox extends XulElement {
    private transient Tabs _tabs;
    private transient Toolbar _toolbar;
    private transient Tabpanels _tabpanels;
    private transient Tab _seltab;
    private String _panelSpacing;
    private String _orient = "top";
    private boolean _tabscroll = true;
    private boolean _maximalHeight = false;
    /** The event listener used to listen onSelect for each tab. */
    /* package */transient EventListener<Event> _listener;

    private transient ListModel<?> _model;
    private transient ListDataListener _dataListener;
    private transient TabboxRenderer<?> _renderer;
    private transient TabboxEngine _engine;

    public Tabbox() {
        init();
    }

    private void init() {
        _listener = new Listener();
    }

    /** Returns the implementation tabbox engine.
     * @exception UiException if failed to load the engine.
     * @since 7.0.0
     */
    public TabboxEngine getEngine() throws UiException {
        if (_engine == null)
            _engine = newTabboxEngine();
        return _engine;
    }

    /** Sets the tabbox engine for {@link ListModel}
     * @since 7.0.0
     */
    public void setEngine(TabboxEngine engine) {
        if (_engine != engine) {
            _engine = engine;
        }

        //Always redraw, if any
        postOnInitRender();
    }

    /** Instantiates the default tabbox engine.
     * It is called, if {@link #setEngine} is not called with non-null
     * engine.
     *
     * <p>By default, it looks up the library property called
     * org.zkoss.zul.tabbox.engine.class.
     * If found, the value is assumed to be
     * the class name of the tabbox engine (it must implement
     * {@link TabboxEngine}).
     * If not found, {@link UiException} is thrown.
     *
     * <p>Derived class might override this method to provide your
     * own default class.
     *
     * @exception UiException if failed to instantiate the engine
     * @since 7.0.0
     */
    protected TabboxEngine newTabboxEngine() throws UiException {
        final String PROP = "org.zkoss.zul.tabbox.engine.class";
        final String klass = Library.getProperty(PROP);
        if (klass == null)
            throw new UiException("Library property,  " + PROP + ", required");

        final Object v;
        try {
            v = Classes.newInstanceByThread(klass);
        } catch (Exception ex) {
            throw UiException.Aide.wrap(ex);
        }
        if (!(v instanceof TabboxEngine))
            throw new UiException(TabboxEngine.class + " must be implemented by " + v);
        return (TabboxEngine) v;
    }

    @SuppressWarnings("unchecked")
    /**
     * Returns the selectable model, if any.
     * @since 7.0.0
     */
    public Selectable<Object> getSelectableModel() {
        return (Selectable<Object>) _model;
    }

    private static boolean disableFeature() {
        return !WebApps.getFeature("ee");
    }

    /**
     * Sets the list model associated with this t. If a non-null model
     * is assigned, no matter whether it is the same as the previous, it will
     * always cause re-render. [ZK EE]
     * 
     * @param model
     *            the list model to associate, or null to dissociate any
     *            previous model.
     * @exception UiException
     *                if failed to initialize with the model
     * @since 7.0.0
     */
    public void setModel(ListModel<?> model) {
        if (disableFeature())
            throw new IllegalAccessError("ZK EE version only!");
        if (model != null) {
            if (!(model instanceof Selectable))
                throw new UiException(model.getClass() + " must implement " + Selectable.class);

            if (_model != model) {
                if (_model != null) {
                    _model.removeListDataListener(_dataListener);
                }
                _model = model;
                _seltab = null;
                initDataListener();
                postOnInitRender();
            }
        } else if (_model != null) {
            _model.removeListDataListener(_dataListener);
            _model = null;
            invalidate();
        }
    }

    private void initDataListener() {
        if (_dataListener == null)
            _dataListener = new ListDataListener() {
                public void onChange(ListDataEvent event) {
                    getEngine().doDataChange(Tabbox.this, event);
                }
            };
        _model.addListDataListener(_dataListener);
    }

    /**
     * Returns the renderer to render each tab and tabpanel, or null if the default renderer
     * is used.
     * @since 7.0.0
     */
    @SuppressWarnings("unchecked")
    public <T> TabboxRenderer<T> getTabboxRenderer() {
        return (TabboxRenderer<T>) _renderer;
    }

    /**
     * Sets the renderer which is used to render each tab and tabpanel if {@link #getModel}
     * is not null. [ZK EE]
     * 
     * <p>
     * Note: changing a render will not cause the tabbox to re-render. If you
     * want it to re-render, you could assign the same model again (i.e.,
     * setModel(null) and than setModel(oldModel)), or fire an {@link ListDataEvent} event.
     * 
     * @param renderer
     *            the renderer, or null to use the default.
     * @exception UiException
     *                if failed to initialize with the model
     * @since 7.0.0
     */
    public void setTabboxRenderer(TabboxRenderer<?> renderer) {
        if (disableFeature())
            throw new IllegalAccessError("ZK EE version only!");
        if (_renderer != renderer) {
            _renderer = renderer;
            postOnInitRender();
        }
    }

    /**
     * Sets the renderer by use of a class name. It creates an instance
     * automatically. [ZK EE]
     * 
     * @since 7.0.0
     * @see #setTabboxRenderer(TabboxRenderer)
     */
    public void setTabboxRenderer(String clsnm) throws ClassNotFoundException, NoSuchMethodException,
            IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
        if (clsnm != null)
            setTabboxRenderer((TabboxRenderer<?>) Classes.newInstanceByThread(clsnm));
    }

    public void onInitRender() {
        if (disableFeature())
            throw new IllegalAccessError("ZK EE version only!");
        removeAttribute(TabboxEngine.ATTR_ON_INIT_RENDER_POSTED);
        doInitRenderer();
        invalidate();
    }

    private void doInitRenderer() {
        if (disableFeature())
            throw new IllegalAccessError("ZK EE version only!");
        getEngine().doInitRenderer(this);
        Events.postEvent(ZulEvents.ON_AFTER_RENDER, this, null); // notify the tabbox when items have been rendered.

    }

    /**
     * Component internal use only.
     * @since 7.0.0
     */
    public void postOnInitRender() {
        if (getAttribute(TabboxEngine.ATTR_ON_INIT_RENDER_POSTED) == null) {
            setAttribute(TabboxEngine.ATTR_ON_INIT_RENDER_POSTED, Boolean.TRUE);
            Events.postEvent("onInitRender", this, null);
        }
    }

    /**
     * Returns the model associated with this selectbox, or null if this
     * selectbox is not associated with any list data model.
     */
    @SuppressWarnings("unchecked")
    public <T> ListModel<T> getModel() {
        return (ListModel<T>) _model;
    }

    /**
     * Returns whether it is in the accordion mold.
     */
    /* package */boolean inAccordionMold() {
        return getMold().startsWith("accordion");
    }

    /**
     * Returns the tabs that this tabbox owns.
     */
    public Tabs getTabs() {
        return _tabs;
    }

    /**
     * Returns the auxiliary toolbar that this tabbox owns.
     * 
     * @since 3.6.3
     */
    public Toolbar getToolbar() {
        return _toolbar;
    }

    /**
     * Returns the tabpanels that this tabbox owns.
     */
    public Tabpanels getTabpanels() {
        return _tabpanels;
    }

    /**
     * Returns whether the tab scrolling is enabled.
     * Default: true.
     * @since 3.5.0
     */
    public boolean isTabscroll() {
        return _tabscroll;
    }

    /**
     * Sets whether to enable the tab scrolling.
     * When enabled, if tab list is wider than tab bar, left, right arrow will appear.
     * @since 3.5.0
     */
    public void setTabscroll(boolean tabscroll) {
        if (_tabscroll != tabscroll) {
            _tabscroll = tabscroll;
            smartUpdate("tabscroll", _tabscroll);
        }
    }

    /**
     * Returns whether to use maximum height of all tabpanel in initial phase.
     * <p> 
     * Default: false.
     * @since 7.0.0
     */
    public boolean isMaximalHeight() {
        return _maximalHeight;
    }

    /**
     * Sets whether to use maximum height of all tabpanel in initial phase.
     * <p>
     * The Client ROD feature will be disabled if it is set to true.
     * @since 7.0.0
     */
    public void setMaximalHeight(boolean maximalHeight) {
        if (_maximalHeight != maximalHeight) {
            _maximalHeight = maximalHeight;
            smartUpdate("maximalHeight", _maximalHeight);
        }
    }

    /**
     * Returns the spacing between {@link Tabpanel}. This is used by certain
     * molds, such as accordion.
     * <p>
     * Default: null (no spacing).
     */
    public String getPanelSpacing() {
        return _panelSpacing;
    }

    /**
     * Sets the spacing between {@link Tabpanel}. This is used by certain molds,
     * such as accordion.
     */
    public void setPanelSpacing(String panelSpacing) {
        if (panelSpacing != null && panelSpacing.length() == 0)
            panelSpacing = null;

        if (!Objects.equals(_panelSpacing, panelSpacing)) {
            _panelSpacing = panelSpacing;
            smartUpdate("panelSpacing", _panelSpacing);
        }
    }

    /**
     * Returns the selected index.
     */
    public int getSelectedIndex() {
        return _seltab != null ? _seltab.getIndex() : -1;
    }

    /**
     * Sets the selected index.
     */
    public void setSelectedIndex(int j) {
        final Tabs tabs = getTabs();
        if (tabs == null)
            throw new IllegalStateException("No tab at all");
        if (j >= 0)
            setSelectedTab((Tab) tabs.getChildren().get(j));
        else
            setSelectedTab((Tab) tabs.getFirstChild()); // keep the first one selected.
    }

    /**
     * Returns the selected tab panel.
     */
    public Tabpanel getSelectedPanel() {
        return _seltab != null ? _seltab.getLinkedPanel() : null;
    }

    /**
     * Sets the selected tab panel.
     */
    public void setSelectedPanel(Tabpanel panel) {
        if (panel == null)
            throw new IllegalArgumentException("null tabpanel");
        if (panel.getTabbox() != this)
            throw new UiException("Not a child: " + panel);
        final Tab tab = panel.getLinkedTab();
        if (tab != null)
            setSelectedTab(tab);
    }

    /**
     * Returns the selected tab.
     */
    public Tab getSelectedTab() {
        return _seltab;
    }

    /**
     * Sets the selected tab.
     */
    public void setSelectedTab(Tab tab) {
        selectTabDirectly(tab, false);
    }

    /** Sets the selected tab. */
    /* packge */void selectTabDirectly(Tab tab, boolean byClient) {
        if (tab == null)
            throw new IllegalArgumentException("null tab");
        if (tab.getTabbox() != this)
            throw new UiException("Not my child: " + tab);
        if (tab != _seltab) {
            if (_seltab != null)
                _seltab.setSelectedDirectly(false);

            try {
                // avoid recursive invoking
                if (getAttribute(TabboxEngine.ATTR_CHANGING_SELECTION) == null) {
                    setAttribute(TabboxEngine.ATTR_CHANGING_SELECTION, Boolean.TRUE);
                    _seltab = tab;
                    _seltab.setSelectedDirectly(true);
                    if (byClient && _model != null) {
                        Selectable<Object> sm = getSelectableModel();
                        if (!sm.isSelected(_model.getElementAt(_seltab.getIndex()))) {
                            sm.clearSelection();
                            sm.addToSelection(_model.getElementAt(_seltab.getIndex()));
                        }
                    }
                }
            } finally {
                removeAttribute(TabboxEngine.ATTR_CHANGING_SELECTION);
            }
            if (!byClient)
                smartUpdate("selectedTab", _seltab);
        }
    }

    /**
     * Returns the orient.
     *
     * <p>
     * Default: "top".
     *
     * <p>
     * Note: only the default mold supports it (not supported if accordion).
     */
    public String getOrient() {
        return this._orient;
    }

    /**
     * Sets the mold.
     *
     * @param mold default , accordion and accordion-lite
     *
     */
    public void setMold(String mold) {
        if (isVertical()) {
            if (mold.startsWith("accordion")) {
                throw new WrongValueException("Unsupported vertical orient in mold : " + mold);
            } else {
                super.setMold(mold);
            }
        } else {
            super.setMold(mold);
        }
    }

    /**
     * Sets the orient.
     *
     * @param orient either "top", "left", "bottom or "right".
     * @since 7.0.0 "horizontal" is renamed to "top" and "vertical" is renamed to "left".
     */
    public void setOrient(String orient) throws WrongValueException {
        if (!"horizontal".equals(orient) && !"top".equals(orient) && !"bottom".equals(orient)
                && !"vertical".equals(orient) && !"right".equals(orient) && !"left".equals(orient))
            throw new WrongValueException("Unknow orient : " + orient);
        if (inAccordionMold())
            throw new WrongValueException("Unsupported vertical orient in mold : " + getMold());
        if (_toolbar != null && ("vertical".equals(orient) || "right".equals(orient) || "left".equals(orient)))
            throw new WrongValueException("Unsupported vertical orient when there is a toolbar");
        if (!Objects.equals(_orient, orient)) {
            if ("horizontal".equals(orient))
                this._orient = "top";
            else if ("vertical".equals(orient))
                this._orient = "left";
            else
                this._orient = orient;
            smartUpdate("orient", _orient);
        }
    }

    /**
     * Returns whether it is a horizontal tabbox.
     *
     * @since 3.0.3
     */
    public boolean isHorizontal() {
        String orient = getOrient();
        return "horizontal".equals(orient) || "top".equals(orient) || "bottom".equals(orient);
    }

    /**
     * Returns whether it is the top orientation.
     * @since 7.0.0
     */
    public boolean isTop() {
        String orient = getOrient();
        return "horizontal".equals(orient) || "top".equals(orient);
    }

    /**
     * Returns whether it is the bottom orientation.
     * @since 7.0.0
     */
    public boolean isBottom() {
        return "bottom".equals(getOrient());
    }

    /**
     * Returns whether it is a vertical tabbox.
     *
     * @since 3.0.3
     */
    public boolean isVertical() {
        String orient = getOrient();
        return "vertical".equals(orient) || "right".equals(orient) || "left".equals(orient);
    }

    /**
     * Returns whether it is the left orientation.
     *
     * @since 7.0.0
     */
    public boolean isLeft() {
        String orient = getOrient();
        return "vertical".equals(orient) || "left".equals(orient);
    }

    /**
     * Returns whether it is the right orientation.
     *
     * @since 7.0.0
     */
    public boolean isRight() {
        return "right".equals(getOrient());
    }

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

    //ZK-3678: Provide a switch to enable/disable iscroll
    /*package*/ boolean isNativeScrollbar() {
        return Utils.testAttribute(this, "org.zkoss.zul.nativebar", true, true);
    }
    // -- Component --//
    public void beforeChildAdded(Component child, Component refChild) {
        if (child instanceof Toolbar) {
            if (_toolbar != null && _toolbar != child)
                throw new UiException("Only one Toolbar is allowed: " + this);
            if (this.isVertical()) //ZK-4270: meaningful message to the developer indicating incorrect usage
                throw new UiException("Toolbar is allowed only when the tabbox is horizontal." + this);
        } else if (child instanceof Tabs) {
            if (_tabs != null && _tabs != child)
                throw new UiException("Only one tabs is allowed: " + this);
        } else if (child instanceof Tabpanels) {
            if (_tabpanels != null && _tabpanels != child)
                throw new UiException("Only one tabpanels is allowed: " + this);
        } else {
            throw new UiException("Unsupported child for tabbox: " + child);
        }
        super.beforeChildAdded(child, refChild);
    }

    public boolean insertBefore(Component child, Component refChild) {
        if (child instanceof Tabs) {
            if (super.insertBefore(child, refChild)) {
                _tabs = (Tabs) child;
                for (Iterator<Component> it = _tabs.getChildren().iterator(); it.hasNext();) {
                    final Tab tab = (Tab) it.next();
                    if (tab.isSelected()) {
                        _seltab = tab;
                        break;
                    }
                }

                addTabsListeners();
                return true;
            }
        } else if (child instanceof Tabpanels) {
            if (super.insertBefore(child, refChild)) {
                _tabpanels = (Tabpanels) child;
                return true;
            }
        } else if (child instanceof Toolbar) {
            if (super.insertBefore(child, refChild)) {
                _toolbar = (Toolbar) child;
                return true;
            }
        } else {
            return super.insertBefore(child, refChild);
            //impossible but make it more extensible
        }
        return false;
    }

    public void onChildRemoved(Component child) {
        if (_tabs == child) {
            removeTabsListeners();
            _tabs = null;
            _seltab = null;
        } else if (_tabpanels == child) {
            _tabpanels = null;
        } else if (_toolbar == child) {
            _toolbar = null;
        }
        super.onChildRemoved(child);
    }

    /** Removes _listener from all {@link Tab} instances. */
    private void removeTabsListeners() {
        if (_tabs != null) {
            for (Iterator<Component> it = _tabs.getChildren().iterator(); it.hasNext();) {
                final Tab tab = (Tab) it.next();
                tab.removeEventListener(Events.ON_SELECT, _listener);
            }
        }
    }

    /** Adds _listener to all {@link Tab} instances. */
    private void addTabsListeners() {
        if (_tabs != null) {
            for (Iterator<Component> it = _tabs.getChildren().iterator(); it.hasNext();) {
                final Tab tab = (Tab) it.next();
                tab.addEventListener(Events.ON_SELECT, _listener);
            }
        }
    }

    protected void clearSelectedTab() {
        _seltab = null;
    }

    // Cloneable//
    public Object clone() {
        final Tabbox clone = (Tabbox) super.clone();

        clone.removeTabsListeners();
        clone.init();

        int cnt = 0;
        if (clone._tabs != null)
            ++cnt;
        if (clone._toolbar != null)
            ++cnt;
        if (clone._tabpanels != null)
            ++cnt;
        if (cnt > 0)
            clone.afterUnmarshal(cnt);

        return clone;
    }

    private void afterUnmarshal(int cnt) {
        for (Iterator<Component> it = getChildren().iterator(); it.hasNext();) {
            final Object child = it.next();
            if (child instanceof Tabs) {
                _tabs = (Tabs) child;
                for (Iterator<Component> e = _tabs.getChildren().iterator(); e.hasNext();) {
                    final Tab tab = (Tab) e.next();
                    if (tab.isSelected()) {
                        _seltab = tab;
                        break;
                    }
                }
                if (--cnt == 0)
                    break;
            } else if (child instanceof Toolbar) {
                _toolbar = (Toolbar) child;
                if (--cnt == 0)
                    break;
            } else if (child instanceof Tabpanels) {
                _tabpanels = (Tabpanels) child;
                if (--cnt == 0)
                    break;
            }
        }

        addTabsListeners();
    }

    // -- Serializable --//
    private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        s.defaultWriteObject();

        willSerialize(_model);
        Serializables.smartWrite(s, _model);
        willSerialize(_renderer);
        Serializables.smartWrite(s, _renderer);
    }

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

        _model = (ListModel) s.readObject();
        didDeserialize(_model);
        _renderer = (TabboxRenderer) s.readObject();
        didDeserialize(_renderer);

        init();
        afterUnmarshal(-1);

        if (_model != null) {
            initDataListener();
        }
    }

    private class Listener implements EventListener<Event>, Deferrable {
        public void onEvent(Event event) {
            Events.sendEvent(Tabbox.this, event);
        }

        public boolean isDeferrable() {
            return !Events.isListened(Tabbox.this, Events.ON_SELECT, true);
        }
    }

    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
        super.renderProperties(renderer);
        if (_panelSpacing != null)
            render(renderer, "panelSpacing", _panelSpacing);
        if (!isTop())
            render(renderer, "orient", _orient);
        if (!_tabscroll)
            renderer.render("tabscroll", _tabscroll);
        if (_maximalHeight) {
            renderer.render("z$rod", false);
            renderer.render("maximalHeight", _maximalHeight);
        }
        //ZK-3678: Provide a switch to enable/disable iscroll
        if (!isNativeScrollbar())
            renderer.render("_nativebar", false);
    }

    public void onPageAttached(Page newpage, Page oldpage) {
        super.onPageAttached(newpage, oldpage);
        if (_model != null) {
            postOnInitRender();
            if (_dataListener != null) {
                _model.removeListDataListener(_dataListener);
                _model.addListDataListener(_dataListener);
            }
        }
    }

    public void onPageDetached(Page page) {
        super.onPageDetached(page);
        if (_model != null && _dataListener != null)
            _model.removeListDataListener(_dataListener);
    }
}