zul/src/main/java/org/zkoss/zul/Tabbox.java
/* 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);
}
}