/* * Copyright (c) 2010-2013 Evolveum * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.evolveum.midpoint.web.component; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.extensions.markup.html.tabs.ITab; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.html.list.Loop; import org.apache.wicket.markup.html.list.LoopItem; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.lang.Args; import org.jetbrains.annotations.Nullable; import com.evolveum.midpoint.gui.api.GuiStyleConstants; import com.evolveum.midpoint.gui.api.model.CountModelProvider; import java.io.Serializable; import java.util.List; /** * @author lazyman * @author Igor Vaynberg (ivaynberg) */ public class TabbedPanel<T extends ITab> extends Panel { /** * id used for child panels */ public static final String TAB_PANEL_ID = "panel"; public static final String RIGHT_SIDE_TAB_ITEM_ID = "rightSideTabItem"; public static final String RIGHT_SIDE_TAB_ID = "rightSideTab"; protected static final String ID_TITLE = "title"; protected static final String ID_COUNT = "count"; protected static final String ID_LINK = "link"; private final IModel<List<T>> tabs; /** * the current tab */ private int currentTab = -1; private transient VisibilityCache visibilityCache; public TabbedPanel(final String id, final List<T> tabs) { this(id, tabs, null); } public TabbedPanel(final String id, final List<T> tabs, @Nullable RightSideItemProvider rightSideItemProvider) { this(id, tabs, null, rightSideItemProvider); } public TabbedPanel(final String id, final List<T> tabs, IModel<Integer> model, @Nullable RightSideItemProvider rightSideItemProvider) { this(id, new Model((Serializable) tabs), model, rightSideItemProvider); } /** * Constructor * * @param id component id * @param tabs list of ITab objects used to represent tabs */ public TabbedPanel(final String id, final IModel<List<T>> tabs) { this(id, tabs, null, null); } /** * Constructor * * @param id component id * @param tabs list of ITab objects used to represent tabs * @param model model holding the index of the selected tab */ public TabbedPanel(final String id, final IModel<List<T>> tabs, IModel<Integer> model, RightSideItemProvider rightSideItemProvider) { super(id, model); this.tabs = Args.notNull(tabs, "tabs"); final IModel<Integer> tabCount = new AbstractReadOnlyModel<Integer>() { private static final long serialVersionUID = 1L; @Override public Integer getObject() { return tabs.getObject().size(); } }; WebMarkupContainer tabsContainer = newTabsContainer("tabs-container"); add(tabsContainer); // add the loop used to generate tab names tabsContainer.add(new Loop("tabs", tabCount) { private static final long serialVersionUID = 1L; @Override protected void populateItem(final LoopItem item) { final int index = item.getIndex(); final T tab = TabbedPanel.this.tabs.getObject().get(index); final WebMarkupContainer titleLink = newLink(ID_LINK, index); titleLink.add(newTitle(ID_TITLE, tab.getTitle(), index)); item.add(titleLink); final IModel<String> countModel; if (tab instanceof CountModelProvider) { countModel = ((CountModelProvider)tab).getCountModel(); } else { countModel = null; } Label countLabel = new Label(ID_COUNT, countModel); countLabel.setVisible(countModel != null); countLabel.add(AttributeModifier.append("class", new AbstractReadOnlyModel<String>() { private static final long serialVersionUID = 1L; @Override public String getObject() { if (countModel == null) { return GuiStyleConstants.CLASS_BADGE_PASSIVE; } String count = countModel.getObject(); if ("0".equals(count)) { return GuiStyleConstants.CLASS_BADGE_PASSIVE; } else { return GuiStyleConstants.CLASS_BADGE_ACTIVE; } } })); titleLink.add(countLabel); } @Override protected LoopItem newItem(final int iteration) { return newTabContainer(iteration); } }); WebMarkupContainer rightSideTabItem = new WebMarkupContainer(RIGHT_SIDE_TAB_ITEM_ID); Component rightSideTabPanel = rightSideItemProvider != null ? rightSideItemProvider.createRightSideItem(RIGHT_SIDE_TAB_ID) : null; if (rightSideTabPanel != null) { rightSideTabItem.add(rightSideTabPanel); } else { rightSideTabItem.setVisible(false); } tabsContainer.add(rightSideTabItem); add(newPanel()); } /** * Override of the default initModel behaviour. This component <strong>will not</strong> use any * compound model of a parent. * * @see org.apache.wicket.Component#initModel() */ @Override protected IModel<?> initModel() { return new Model<Integer>(-1); } /** * Generates the container for all tabs. The default container automatically adds the css * <code>class</code> attribute based on the return value of {@link #getTabContainerCssClass()} * * @param id container id * @return container */ protected WebMarkupContainer newTabsContainer(final String id) { WebMarkupContainer tabs = new WebMarkupContainer(id); tabs.setOutputMarkupId(true); return tabs; } /** * Generates a loop item used to represent a specific tab's <code>li</code> element. * * @param tabIndex * @return new loop item */ protected LoopItem newTabContainer(final int tabIndex) { return new LoopItem(tabIndex) { private static final long serialVersionUID = 1L; @Override protected void onConfigure() { super.onConfigure(); setVisible(getVisiblityCache().isVisible(tabIndex)); } @Override protected void onComponentTag(final ComponentTag tag) { super.onComponentTag(tag); String cssClass = tag.getAttribute("class"); if (cssClass == null) { cssClass = " "; } cssClass += " tab" + getIndex(); if (getIndex() == getSelectedTab()) { cssClass += ' ' + getSelectedTabCssClass(); } if (getVisiblityCache().getLastVisible() == getIndex()) { cssClass += ' ' + getLastTabCssClass(); } tag.put("class", cssClass.trim()); } }; } @Override protected void onBeforeRender() { int index = getSelectedTab(); if (index == -1 || getVisiblityCache().isVisible(index) == false) { // find first visible tab index = -1; for (int i = 0; i < tabs.getObject().size(); i++) { if (getVisiblityCache().isVisible(i)) { index = i; break; } } if (index != -1) { // found a visible tab, so select it setSelectedTab(index); } } setCurrentTab(index); super.onBeforeRender(); } /** * @return the value of css class attribute that will be added to last tab. The default value is * <code>last</code> */ protected String getLastTabCssClass() { return ""; } /** * @return the value of css class attribute that will be added to a div containing the tabs. The * default value is <code>tab-row</code> */ protected String getTabContainerCssClass() { return "tab-row"; } /** * @return the value of css class attribute that will be added to selected tab. The default * value is <code>selected</code> */ protected String getSelectedTabCssClass() { return "active"; } /** * @return list of tabs that can be used by the user to add/remove/reorder tabs in the panel */ public final IModel<List<T>> getTabs() { return tabs; } /** * Factory method for tab titles. Returned component can be anything that can attach to span * tags such as a fragment, panel, or a label * * @param titleId id of tiatle component * @param titleModel model containing tab title * @param index index of tab * @return title component */ protected Component newTitle(final String titleId, final IModel<?> titleModel, final int index) { Label label = new Label(titleId, titleModel); label.setRenderBodyOnly(true); return label; } /** * Factory method for links used to switch between tabs. * <p/> * The created component is attached to the following markup. Label component with id: title * will be added for you by the tabbed panel. * <p/> * <pre> * <a href="#" wicket:id="link"><span wicket:id="title">[[tab title]]</span></a> * </pre> * <p/> * Example implementation: * <p/> * <pre> * protected WebMarkupContainer newLink(String linkId, final int index) * { * return new Link(linkId) * { * private static final long serialVersionUID = 1L; * * public void onClick() * { * setSelectedTab(index); * } * }; * } * </pre> * * @param linkId component id with which the link should be created * @param index index of the tab that should be activated when this link is clicked. See * {@link #setSelectedTab(int)}. * @return created link component */ protected WebMarkupContainer newLink(final String linkId, final int index) { return new Link<Void>(linkId) { private static final long serialVersionUID = 1L; @Override public void onClick() { setSelectedTab(index); onTabChange(index); } }; } /** * sets the selected tab * * @param index index of the tab to select * @return this for chaining * @throws IndexOutOfBoundsException if index is not in the range of available tabs */ public TabbedPanel<T> setSelectedTab(final int index) { if ((index < 0) || (index >= tabs.getObject().size())) { throw new IndexOutOfBoundsException(); } setDefaultModelObject(index); // force the tab's component to be aquired again if already the current tab currentTab = -1; setCurrentTab(index); return this; } private void setCurrentTab(int index) { if (this.currentTab == index) { // already current return; } this.currentTab = index; final Component component; if (currentTab == -1 || (tabs.getObject().size() == 0) || !getVisiblityCache().isVisible(currentTab)) { // no tabs or the current tab is not visible component = newPanel(); } else { // show panel from selected tab T tab = tabs.getObject().get(currentTab); component = tab.getPanel(TAB_PANEL_ID); if (component == null) { throw new WicketRuntimeException("ITab.getPanel() returned null. TabbedPanel [" + getPath() + "] ITab index [" + currentTab + "]"); } } if (!component.getId().equals(TAB_PANEL_ID)) { throw new WicketRuntimeException( "ITab.getPanel() returned a panel with invalid id [" + component.getId() + "]. You must always return a panel with id equal to the provided panelId parameter. TabbedPanel [" + getPath() + "] ITab index [" + currentTab + "]"); } addOrReplace(component); } private WebMarkupContainer newPanel() { return new WebMarkupContainer(TAB_PANEL_ID); } /** * @return index of the selected tab */ public final int getSelectedTab() { return (Integer) getDefaultModelObject(); } @Override protected void onDetach() { visibilityCache = null; super.onDetach(); } private VisibilityCache getVisiblityCache() { if (visibilityCache == null) { visibilityCache = new VisibilityCache(); } return visibilityCache; } /** * A cache for visibilities of {@link ITab}s. */ private class VisibilityCache { /** * Visibility for each tab. */ private Boolean[] visibilities; /** * Last visible tab. */ private int lastVisible = -1; public VisibilityCache() { visibilities = new Boolean[tabs.getObject().size()]; } public int getLastVisible() { if (lastVisible == -1) { for (int t = 0; t < tabs.getObject().size(); t++) { if (isVisible(t)) { lastVisible = t; } } } return lastVisible; } public boolean isVisible(int index) { if (visibilities.length < index + 1) { Boolean[] resized = new Boolean[index + 1]; System.arraycopy(visibilities, 0, resized, 0, visibilities.length); visibilities = resized; } if (visibilities.length > 0) { Boolean visible = visibilities[index]; if (visible == null) { List<T> tabsList = tabs.getObject(); T tab = tabsList == null || tabsList.size() == 0 ? null : tabs.getObject().get(index); visible = tab != null && tab.isVisible(); if (tab != null) { visibilities[index] = visible; } } return visible; } else { return false; } } } /** * Method called after tab was changed - user clicked on link in tab header. * * @param index Index of new tab. */ protected void onTabChange(int index) {} public interface RightSideItemProvider extends Serializable { Component createRightSideItem(String id); } }