/*******************************************************************************
* Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Oracle - initial API and implementation from Oracle TopLink
******************************************************************************/
package org.eclipse.persistence.tools.workbench.framework.ui.view;
import java.awt.BorderLayout;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JTabbedPane;
import javax.swing.SwingConstants;
import org.eclipse.persistence.tools.workbench.framework.app.ApplicationNode;
import org.eclipse.persistence.tools.workbench.framework.context.WorkbenchContext;
import org.eclipse.persistence.tools.workbench.framework.context.WorkbenchContextHolder;
import org.eclipse.persistence.tools.workbench.uitools.app.PropertyValueModel;
import org.eclipse.persistence.tools.workbench.uitools.app.PropertyValueModelWrapper;
import org.eclipse.persistence.tools.workbench.uitools.app.ValueModel;
import org.eclipse.persistence.tools.workbench.utility.CollectionTools;
/**
* If an ApplicationNode can have more than one PropertiesPage, use this class.
* It provides support for adding and removing PropertiesPages (or other components)
* to/from a tabbed pane. Typically the tab pages will be subclasses of
* ScrollablePropertiesPage.
*
* This tabbed properties page supports two types of PropertiesPages:
*
* static - instantiated and added at the time of subclass initialization.
* dynamic - created and added by a ComponentBuilder based on changes indicated
* by a provided ValueModel.
*
* Concurrent use of static and dynamic tabs is supported.
*
* In order to determine the ordering of tabs in the PropertiesPage, a weight-based
* system has been employed. Valid weight values are 0..Integer.MAX_VALUE. 0 is the
* great weight (priority). Generally, the given weight of a page should correspond to
* the desired index of the tab (from left to right) when all static and dynamic tabs are present in the PropertiesPage.
* In the case that mutiple tabs of the same weight exist, they will appear in the order
* that they were added. Because of this, the weights can be tweeked to account for
* PropertyPage inheritance allowing the correct tab ordering in the case that tabs are
* added in varying orders and appear dynamically. The default for a tab added without
* weight is 0 (highest weight).
*
* If your ApplicationNode will only have one PropertiesPage use TitledPropertiesPage.
*
* @see ScrollablePropertiesPage
* @see TitledPropertiesPage
*/
public abstract class TabbedPropertiesPage
extends AbstractPropertiesPage
{
JTabbedPane tabbedPane;
private List componentTabWeightHolders;
private List dynamicTabHandlers;
private Component previouslySelectedComponent;
protected static final int DEFAULT_WEIGHT = 0;
protected TabbedPropertiesPage(WorkbenchContext context) {
super(context);
}
protected TabbedPropertiesPage(PropertyValueModel nodeHolder, WorkbenchContextHolder contextHolder) {
super(nodeHolder, contextHolder);
}
protected void initializeLayout() {
this.add(this.buildTitlePanel(), BorderLayout.PAGE_START);
this.tabbedPane = this.buildTabbedPane();
this.add(this.tabbedPane, BorderLayout.CENTER);
this.initializeTabs();
}
protected JTabbedPane buildTabbedPane() {
JTabbedPane result = new JTabbedPane(SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
result.setBorder(BorderFactory.createEmptyBorder(0, 2, 2, 2));
return result;
}
/**
* Subclasses should implement this method and call the various
* #addTab() methods to add the properties pages that should
* appear when the node is selected. These tabs can be specified
* as static (created and added at initialization) or as dynamic
* (created and added dynamically based on model changes).
*
*/
protected abstract void initializeTabs();
protected int tabCount() {
return this.tabbedPane.getTabCount();
}
protected void initialize(PropertyValueModel nodeHolder) {
super.initialize(nodeHolder);
this.componentTabWeightHolders = new ArrayList();
this.dynamicTabHandlers = new ArrayList();
}
/**
* Adds a new tab for the given <code>Component</code> with specified <code>tabWeight</code>.
*/
protected void addTab(Component component, int tabWeight, String tabTitleKey) {
this.addTab(this.buildDefaultComponentBuilder(component), tabWeight, tabTitleKey);
}
/**
* Adds a new tab for the given <code>Component</code> with <code>DEFAULT_WEIGHT</code> set as
* the tab weight.
*/
protected void addTab(Component component, String tabTitleKey) {
this.addTab(component, DEFAULT_WEIGHT, tabTitleKey);
}
/**
* Adds a new tab for the <code>Component</code> with <code>tabWeight</code> set as
* the tab weight. The tab created for the component is then set as selected.
*/
protected void addTabAndSelect(Component component, int tabWeight, String tabTitleKey) {
this.addTab(component, tabWeight, tabTitleKey);
this.tabbedPane.setSelectedComponent(component);
}
/**
* Adds a new tab for the <code>Component</code> with <code>DEFAULT_WEIGHT</code> set as
* the tab weight. The tab created for the component is then set as selected.
*/
protected void addTabAndSelect(Component component, String tabTitleKey) {
this.addTabAndSelect(component, DEFAULT_WEIGHT, tabTitleKey);
}
/**
* Adds a new tab, using the <code>ComponentBuilder</code> to create the new <code>Component</code> using <code>tabWeight</code> as
* the tab weight. The given <code>ValueModel</code> should return a <code>Boolean</code> value to determine if the tab should be created
* based upon the state of the underlying model.
*/
protected void addTab(ValueModel enabledValueModel, int tabWeight, ComponentBuilder pageBuilder, String tabTitleKey) {
DynamicTabHandler handler = new DynamicTabHandler(pageBuilder, tabWeight, tabTitleKey, enabledValueModel);
handler.engage();
handler.nodeSet(getNode());
this.dynamicTabHandlers.add(handler);
}
/**
* Adds a new tab for the given <code>Component</code> with specified <code>tabWeight</code>.
* The given <code>ValueModel</code> should return a <code>Boolean</code> value to determine if the tab should be displayed
* based upon the state of the underlying model.
*/
protected void addTab(ValueModel enabledValueModel, int tabWeight, Component component, String tabTitleKey) {
this.addTab(enabledValueModel, tabWeight, this.buildDefaultComponentBuilder(component), tabTitleKey);
}
/**
* Adds a new tab, using the <code>ComponentBuilder</code> to create the new <code>Component</code> using <code>tabWeight</code> as
* the tab weight. The tab created for the component is then set as selected.
*/
protected void addTab(ComponentBuilder pageBuilder, int tabWeight, String tabTitleKey) {
Component component = pageBuilder.buildComponent(getNode());
this.insertTab(component, tabWeight, tabTitleKey);
}
/**
* Sets the tab represented by the given <code>Component</code> as selected.
*/
protected void setSelectedTab(Component component) {
this.tabbedPane.setSelectedComponent(component);
}
/**
* WARNING: be very carful using this method. In the case where the tab
* editor specificied by this component is static, only the Component will
* be removed from the tab. However, if the specified Component belongs to
* a dynamic tab editor, the enabling mode, all listeners, etc will be completely
* removed for the Component. This is necessary as the TabbedPropertiesPage
* would be left in an inconsistent state otherwise.
*/
protected void destroyTab(Component component) {
this.removeTab(component);
// quick sanity check. since it is possible that
// dynamic tabs have not been created yet, we don't
// want to remove the first one we encounter.
if (component == null) return;
for (Iterator handlers = this.dynamicTabHandlers.iterator(); handlers.hasNext(); ) {
DynamicTabHandler handler = (DynamicTabHandler)handlers.next();
if (handler.getComponent() == component) {
handler.disengage();
this.dynamicTabHandlers.remove(handler);
return;
}
}
}
/**
* Necessary override. When the node is changed, the enabledStateModel given with a dynamic tab editor,
* should handle adding and removing the tab when the is engaged by the contained selectionHolder. However,
* when the properties page has it's applicationNode set to null, properties in the model can change without the
* tabbed properties page's knowledge since it has been disengaged. Upon engaging with the same model object,
* or a different one, the dynamic tabs need to be added or removed based on the new model node's state.
*/
public void setNode(ApplicationNode node, WorkbenchContext context) {
if (node == null) {
this.previouslySelectedComponent = this.tabbedPane.getSelectedComponent();
}
super.setNode(node, context);
for (Iterator handlers = this.dynamicTabHandlers.iterator(); handlers.hasNext(); ) {
DynamicTabHandler handler = (DynamicTabHandler) handlers.next();
// let the handler know that a new node has been set, and adjust accordingly...
handler.nodeSet(node);
}
if (node != null) {
if (CollectionTools.contains(this.tabbedPane.getComponents(), this.previouslySelectedComponent)) {
this.setSelectedTab(this.previouslySelectedComponent);
} else {
this.setSelectedTab(this.tabbedPane.getComponentAt(0));
}
}
}
void insertTab(Component component, int pageWeight, String tabTitleKey) {
ComponentTabWeightHolder tabWeightHolder = new ComponentTabWeightHolder(component, pageWeight);
int pageIndex = this.insertTabWeightHolder(tabWeightHolder);
this.tabbedPane.insertTab(this.resourceRepository().getString(tabTitleKey), null, component, null, pageIndex);
}
private int insertTabWeightHolder(ComponentTabWeightHolder tabWeightHolder) {
// assume insertion index is the end of the list unless otherwise specified.
int insertionIndex = this.componentTabWeightHolders.size();
for (int i = 0; i < this.componentTabWeightHolders.size(); i++) {
ComponentTabWeightHolder currentHolder = (ComponentTabWeightHolder) this.componentTabWeightHolders.get(i);
if (currentHolder.getPageWeight() > tabWeightHolder.getPageWeight()) {
insertionIndex = i;
break;
}
}
this.componentTabWeightHolders.add(insertionIndex, tabWeightHolder);
return insertionIndex;
}
/**
* @see <code>destroyTab</code> for external removal
*/
void removeTab(Component component) {
this.tabbedPane.remove(component);
this.removeTabWeightHolderFor(component);
}
private void removeTabWeightHolderFor(Component component) {
ComponentTabWeightHolder holderToRemove = null;
for (Iterator stream = this.componentTabWeightHolders.iterator(); stream.hasNext(); ) {
ComponentTabWeightHolder holder = (ComponentTabWeightHolder) stream.next();
if (holder.getComponent() == component) {
holderToRemove = holder;
// Since we are sorting here and this code is called quite
// frequently by the UI, it is reasonable to use a break here
// for performance.
break;
}
}
if (holderToRemove != null) {
this.componentTabWeightHolders.remove(holderToRemove);
} else {
throw new RuntimeException("DEBUG: Could not find the correct holder for tab.");
}
}
private ComponentBuilder buildDefaultComponentBuilder(Component component) {
return new DefaultComponentBuilder(component);
}
/**
* Registered with the Dynamic tab's ValueModel, this class handles adding and
* removing the associated tab editor based upon the existence of the property
* in the descriptor model.
*/
private class DynamicTabHandler implements PropertyChangeListener {
// need to hold on to the Component since it is not
// gauranteed that the ComponentBuilder will return
// the same instance every time....
private Component component;
private String tabTitleKey;
private int tabWeight;
private ComponentBuilder componentBuilder;
private boolean hasTabBeenAdded;
private ValueModel enabledStateModel;
private DynamicTabHandler(ComponentBuilder componentBuilder, int tabWeight, String tabTitleKey, ValueModel enabledStateModel) {
this.componentBuilder = componentBuilder;
this.tabTitleKey = tabTitleKey;
this.tabWeight = tabWeight;
this.enabledStateModel = enabledStateModel;
}
protected void engage() {
this.enabledStateModel.addPropertyChangeListener(ValueModel.VALUE, this);
}
protected void disengage() {
this.enabledStateModel.removePropertyChangeListener(ValueModel.VALUE, this);
}
public void propertyChange(PropertyChangeEvent event) {
Boolean properyEnabled = (Boolean) event.getNewValue();
// if the property is enabled
if (properyEnabled.booleanValue()) {
this.addPropertiesPage();
} else {
this.removePropertiesPage();
}
}
protected void nodeSet(ApplicationNode applicationNode) {
if (applicationNode != null) {
boolean isPropertyEnabled = ((Boolean) this.enabledStateModel.getValue()).booleanValue();
if (isPropertyEnabled && ! this.hasTabBeenAdded) {
this.addPropertiesPage();
} else if (!isPropertyEnabled && this.hasTabBeenAdded) {
this.removePropertiesPage();
}
}
}
private void addPropertiesPage() {
this.component = buildComponent();
TabbedPropertiesPage.this.insertTab(this.component, this.tabWeight, this.tabTitleKey);
TabbedPropertiesPage.this.setSelectedTab(this.component);
this.hasTabBeenAdded = true;
}
private void removePropertiesPage() {
int componentIndex = TabbedPropertiesPage.this.tabbedPane.indexOfComponent(this.component);
int selectedIndex = TabbedPropertiesPage.this.tabbedPane.getSelectedIndex();
TabbedPropertiesPage.this.removeTab(this.component);
if (componentIndex < selectedIndex) {
TabbedPropertiesPage.this.tabbedPane.setSelectedIndex(selectedIndex - 1);
}
this.component = null;
this.hasTabBeenAdded = false;
}
private Component buildComponent() {
DynamicTabNodeHolder holder = new DynamicTabNodeHolder(getNodeHolder(), this.enabledStateModel);
return this.componentBuilder.buildComponent(holder);
}
protected Component getComponent() {
return this.component;
}
}
/**
* This class represents a value pairing of the specified tab's Component with
* the associated weight. This class is used in tracking the location of an
* existing tab as well as providing a way to measure its positioning weight
* in the system.
*/
private class ComponentTabWeightHolder {
private Component component;
private int pageWeight;
private ComponentTabWeightHolder(Component component, int pageWeight) {
this.pageWeight = pageWeight;
this.component = component;
}
int getPageWeight() {
return this.pageWeight;
}
Component getComponent() {
return this.component;
}
}
/**
* This class is used by implementor of this class may want to pass a concrete instance
* of a Component in the case where the Component's tab is statically placed.
* In this case, the desired behavior is a ComponentBuilder that caches and returns
* the specified instance.
*/
private class DefaultComponentBuilder implements ComponentBuilder {
private Component component;
private DefaultComponentBuilder(Component component) {
this.component = component;
}
public Component buildComponent(PropertyValueModel nodeHolder) {
return this.component;
}
}
/**
* Defines a proxy ApplicationNode PVM holder for all a dynamic properties page. This
* class requires the original node holder from the parent TabbedPropertiesPage as well
* as a ValueModel that returns a Boolean describing the whether the properties page should
* be shown. When the Boolean ValueModel returns false, this node holder acts as if the
* node is null. Otherwise, all events and value are propogated from the underlying node holder.
*/
private class DynamicTabNodeHolder extends PropertyValueModelWrapper {
private ValueModel enabledStateModel;
/** This listens to the enabled state model holder. */
protected PropertyChangeListener enabledStateChangeListener;
private DynamicTabNodeHolder(PropertyValueModel nodeHolder, ValueModel enabledStateModel) {
super(nodeHolder);
this.enabledStateModel = enabledStateModel;
}
protected void initialize() {
super.initialize();
this.enabledStateChangeListener = this.buildEnabledStateChangeListener();
}
protected PropertyChangeListener buildEnabledStateChangeListener() {
return new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
enabledStateModelChanged(e);
}
};
}
public Object getValue() {
return (this.enabledStateModelValue()) ? this.valueHolder.getValue() : null;
}
public void setValue(Object value) {
// do nothing for now..
}
protected void engageValueHolder() {
super.engageValueHolder();
this.enabledStateModel.addPropertyChangeListener(VALUE, this.enabledStateChangeListener);
}
protected void disengageValueHolder() {
this.enabledStateModel.removePropertyChangeListener(VALUE, this.enabledStateChangeListener);
super.disengageValueHolder();
}
protected void valueChanged(PropertyChangeEvent e) {
if (this.enabledStateModelValue()){
this.firePropertyChanged(VALUE, e.getOldValue(), e.getNewValue());
}
}
protected void enabledStateModelChanged(PropertyChangeEvent e) {
if (this.enabledStateModelValue()) {
this.firePropertyChanged(VALUE, null, this.valueHolder.getValue());
} else {
this.firePropertyChanged(VALUE, this.valueHolder.getValue(), null);
}
}
private boolean enabledStateModelValue() {
return ((Boolean) this.enabledStateModel.getValue()).booleanValue();
}
}
}