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