io.guise.framework.component.BooleanValueControlSelectControl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of guise-framework Show documentation
Show all versions of guise-framework Show documentation
Guise™ Internet application framework.
/*
* Copyright © 2005-2008 GlobalMentor, Inc.
*
* 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 io.guise.framework.component;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.Objects.*;
import static com.globalmentor.java.Integers.*;
import com.globalmentor.beans.*;
import com.globalmentor.java.Arrays;
import io.guise.framework.GuiseApplication;
import io.guise.framework.component.layout.*;
import io.guise.framework.converter.AbstractStringLiteralConverter;
import io.guise.framework.event.*;
import io.guise.framework.model.*;
import io.guise.framework.theme.Theme;
/**
* A list select control that uses child components to show the list items. Only {@link ValueControl}s of type {@link Boolean} will be recognized as able to
* receive user input to select items in the list. Only a {@link Layout} that supports default constraints can be used.
* @param The type of values to select.
* @author Garret Wilson
*/
public class BooleanValueControlSelectControl extends AbstractListSelectControl implements LayoutComponent, ListSelectControl {
//TODO make sure newly created components have the correct value set automatically
/** The layout definition for the component. */
private Layout extends Constraints> layout;
@Override
public Layout extends Constraints> getLayout() {
return layout;
}
/**
* Sets the layout definition for the component. This is a bound property. The layout is marked as not yet having a theme applied, as the specific theme rules
* applied to the layout may depend on the layout's owner.
* @param The type of the constraint.
* @param newLayout The new layout definition for the container.
* @throws NullPointerException if the given layout is null
.
* @see #LAYOUT_PROPERTY
*/
protected void setLayout(final Layout newLayout) {
if(layout != requireNonNull(newLayout, "Layout cannot be null.")) { //if the value is really changing
final Layout extends Constraints> oldLayout = layout; //get the old value
oldLayout.setOwner(null); //tell the old layout it is no longer installed
layout = newLayout; //actually change the value
layout.setOwner(this); //tell the new layout which container owns it
for(final Component childComponent : getChildComponents()) { //for each child component
newLayout.getConstraints(childComponent); //make sure the constraints of all components are compatible with the layout TODO do we even need to do this? why not wait until later? but this may be OK---perhaps we can assume that if components are installed before the layout, they will be used with this layout and not another
}
setLayoutThemeApplied(false); //indicate that a theme haven't yet been set for this layout, as the specific rules applied may depend on the layout's owner
firePropertyChange(LAYOUT_PROPERTY, oldLayout, newLayout); //indicate that the value changed
}
}
/** Whether a theme has been applied to this component's layout. */
private boolean layoutThemeApplied = false;
@Override
public boolean isLayoutThemeApplied() {
return layoutThemeApplied;
}
@Override
public void setLayoutThemeApplied(final boolean newLayoutThemeApplied) {
if(layoutThemeApplied != newLayoutThemeApplied) { //if the value is really changing
final boolean oldLayoutThemeApplied = layoutThemeApplied; //get the current value
layoutThemeApplied = newLayoutThemeApplied; //update the value
firePropertyChange(LAYOUT_THEME_APPLIED_PROPERTY, Boolean.valueOf(oldLayoutThemeApplied), Boolean.valueOf(newLayoutThemeApplied));
}
}
/**
* {@inheritDoc}
*
* This version returns the children in the same order as the list values.
*
*/
@Override
public Iterable getChildComponents() {
final List children = new ArrayList(size()); //create a list big enough to hold components for all values; the size could change before we get the iterator, so don't create a fixed array just in case
for(final V value : this) { //for each value
children.add(determineComponentState(value).getComponent()); //determine the component state for this value and add it to the list of children
}
return children; //return the children we found
}
/** The value policy model group for boolean value models, or null
if there is no value policy model group in use. */
private final ValuePolicyModelGroup valuePolicyModelGroup;
/** @return The value policy model group for boolean value models, or null
if there is no value policy model group in use. */
protected ValuePolicyModelGroup getValuePolicyModelGroup() {
return valuePolicyModelGroup;
}
/** The listener that detects changes to a child component's {@link Boolean} value and updates the control's selected values accordingly. */
protected final GenericPropertyChangeListener componentValueChangeListener = new AbstractGenericPropertyChangeListener() {
@Override
public void propertyChange(final GenericPropertyChangeEvent genericPropertyChangeEvent) { //if the property of this control changes, update all the values rather than trying to keep track with them; this brute-force method is simplest and ensures everything stays in-synch
updateListSelectedValues(); //update the list selected values based upon the new child control values
}
};
/**
* Value class constructor with a default data model to represent a given type with multiple selection and a {@link FlowLayout} with {@link Flow#PAGE} flow.
* @param valueClass The class indicating the type of value held in the model.
* @throws NullPointerException if the given value class is null
.
*/
public BooleanValueControlSelectControl(final Class valueClass) {
this(new DefaultListSelectModel(valueClass)); //construct the class with a default model
}
/**
* Value class and value representation strategy constructor with a default data model to represent a given type with multiple selection and a
* {@link FlowLayout} with {@link Flow#PAGE} flow.
* @param valueClass The class indicating the type of value held in the model.
* @param valueRepresentationStrategy The strategy to create label models to represent this model's values.
* @throws NullPointerException if the given value class and/or value representation strategy is null
.
*/
public BooleanValueControlSelectControl(final Class valueClass, final ValueRepresentationStrategy valueRepresentationStrategy) {
this(new DefaultListSelectModel(valueClass), valueRepresentationStrategy); //construct the class with a default model
}
/**
* Value class and selection strategy constructor with a default data model to represent a given type and a {@link FlowLayout} with {@link Flow#PAGE} flow.
* @param valueClass The class indicating the type of value held in the model.
* @param selectionStrategy The strategy for selecting values in the model.
* @throws NullPointerException if the given value class and/or selection strategy is null
.
*/
public BooleanValueControlSelectControl(final Class valueClass, final ListSelectionPolicy selectionStrategy) {
this(new DefaultListSelectModel(valueClass, selectionStrategy)); //construct the class with a default model
}
/**
* Value class, selection strategy, and value representation strategy constructor with a default data model to represent a given type and a {@link FlowLayout}
* with {@link Flow#PAGE} flow.
* @param valueClass The class indicating the type of value held in the model.
* @param selectionStrategy The strategy for selecting values in the model.
* @param valueRepresentationStrategy The strategy to create label models to represent this model's values.
* @throws NullPointerException if the given value class, selection strategy, and/or value representation strategy is null
.
*/
public BooleanValueControlSelectControl(final Class valueClass, final ListSelectionPolicy selectionStrategy,
final ValueRepresentationStrategy valueRepresentationStrategy) {
this(new DefaultListSelectModel(valueClass, selectionStrategy), valueRepresentationStrategy); //construct the class with a default model
}
/**
* List select model constructor with a {@link FlowLayout} with {@link Flow#PAGE} flow.
* @param listSelectModel The component list select model.
* @throws NullPointerException if the given list select model is null
.
*/
public BooleanValueControlSelectControl(final ListSelectModel listSelectModel) {
this(listSelectModel, new DefaultValueRepresentationStrategy(AbstractStringLiteralConverter.getInstance(listSelectModel.getValueClass()))); //construct the class with a default representation strategy
}
/**
* List select model and value representation strategy constructor with a {@link FlowLayout} with {@link Flow#PAGE} flow.
* @param listSelectModel The component list select model.
* @param valueRepresentationStrategy The strategy to create label models to represent this model's values.
* @throws NullPointerException if the given list select model and/or value representation strategy is null
.
*/
public BooleanValueControlSelectControl(final ListSelectModel listSelectModel, final ValueRepresentationStrategy valueRepresentationStrategy) {
this(listSelectModel, new FlowLayout(Flow.PAGE), valueRepresentationStrategy); //construct the control with page flow layout
}
/**
* Value class and layout constructor with a default data model to represent a given type with multiple selection.
* @param valueClass The class indicating the type of value held in the model.
* @param layout The layout definition for the component.
* @throws NullPointerException if the given value class and/or layout is null
.
*/
public BooleanValueControlSelectControl(final Class valueClass, final Layout> layout) {
this(new DefaultListSelectModel(valueClass)); //construct the class with a default model
}
/**
* Value class, layout, and value representation strategy constructor with a default data model to represent a given type with multiple selection.
* @param valueClass The class indicating the type of value held in the model.
* @param valueRepresentationStrategy The strategy to create label models to represent this model's values.
* @param layout The layout definition for the component.
* @throws NullPointerException if the given value class, layout, and/or value representation strategy is null
.
*/
public BooleanValueControlSelectControl(final Class valueClass, final Layout> layout, final ValueRepresentationStrategy valueRepresentationStrategy) {
this(new DefaultListSelectModel(valueClass), valueRepresentationStrategy); //construct the class with a default model
}
/**
* Value class, selection strategy, and layout constructor with a default data model to represent a given type.
* @param valueClass The class indicating the type of value held in the model.
* @param selectionStrategy The strategy for selecting values in the model.
* @param layout The layout definition for the component.
* @throws NullPointerException if the given value class, selection strategy, and/or layout is null
.
*/
public BooleanValueControlSelectControl(final Class valueClass, final ListSelectionPolicy selectionStrategy, final Layout> layout) {
this(new DefaultListSelectModel(valueClass, selectionStrategy)); //construct the class with a default model
}
/**
* Value class, selection strategy, layout, and value representation strategy constructor with a default data model to represent a given type.
* @param valueClass The class indicating the type of value held in the model.
* @param selectionStrategy The strategy for selecting values in the model.
* @param layout The layout definition for the component.
* @param valueRepresentationStrategy The strategy to create label models to represent this model's values.
* @throws NullPointerException if the given value class, selection strategy, layout, and/or value representation strategy is null
.
*/
public BooleanValueControlSelectControl(final Class valueClass, final ListSelectionPolicy selectionStrategy, final Layout> layout,
final ValueRepresentationStrategy valueRepresentationStrategy) {
this(new DefaultListSelectModel(valueClass, selectionStrategy), valueRepresentationStrategy); //construct the class with a default model
}
/**
* List select model and layout constructor.
* @param listSelectModel The component list select model.
* @param layout The layout definition for the component.
* @throws NullPointerException if the given list select model and/or layout is null
.
*/
public BooleanValueControlSelectControl(final ListSelectModel listSelectModel, final Layout> layout) {
this(listSelectModel, new DefaultValueRepresentationStrategy(AbstractStringLiteralConverter.getInstance(listSelectModel.getValueClass()))); //construct the class with a default representation strategy
}
/**
* List select model, layout, and value representation strategy constructor.
* @param listSelectModel The component list select model.
* @param layout The layout definition for the component.
* @param valueRepresentationStrategy The strategy to create label models to represent this model's values.
* @throws NullPointerException if the given list select model, layout, and/or value representation strategy is null
.
*/
@SuppressWarnings("unchecked")
//an empty varargs doesn't need a generics cast, but Java requires one anyway
public BooleanValueControlSelectControl(final ListSelectModel listSelectModel, final Layout> layout,
final ValueRepresentationStrategy valueRepresentationStrategy) {
super(listSelectModel, valueRepresentationStrategy); //construct the parent class
this.layout = requireNonNull(layout, "Layout cannot be null."); //save the layout
layout.setOwner(this); //tell the layout which composite component owns it
valuePolicyModelGroup = getSelectionPolicy() instanceof SingleListSelectionPolicy ? new MutualExclusionPolicyModelGroup() : null; //if we're selecting only one item at a time, use a mutual exclusion policy group model
addListSelectionListener(new ListSelectionListener() { //if the list values change, update the components
@Override
public void listSelectionChanged(final ListSelectionEvent selectionEvent) { //if the list selection changes
updateChildControlValues(); //update the values of the child controls
}
});
}
@Override
protected void addComponent(final Component childComponent) {
super.addComponent(childComponent); //add the component normally
getLayout().addComponent(childComponent); //add the component to the layout
if(childComponent instanceof ValueControl && ((ValueControl>)childComponent).getValueClass().equals(Boolean.class)) { //if the component is a Boolean value control
final ValueControl booleanValueControl = (ValueControl)childComponent; //get the component as a boolean value control
final ValuePolicyModelGroup valuePolicyModelGroup = getValuePolicyModelGroup(); //get the value policy model group, if any
if(valuePolicyModelGroup != null) { //if there is a policy group
valuePolicyModelGroup.add(booleanValueControl); //add the component to the group
}
booleanValueControl.addPropertyChangeListener(ValueControl.VALUE_PROPERTY, componentValueChangeListener); //listen for the child component's value changing and update the selected values accordingly
}
}
@Override
protected void removeComponent(final Component childComponent) {
//TODO check the order of uninitialization; if an error occurs during removal, this could result in an inconsistent state
if(childComponent instanceof ValueControl && ((ValueControl>)childComponent).getValueClass().equals(Boolean.class)) { //if the component is a Boolean value control
final ValueControl booleanValueControl = (ValueControl)childComponent; //get the component as a boolean value control
final ValuePolicyModelGroup valuePolicyModelGroup = getValuePolicyModelGroup(); //get the value policy model group, if any
if(valuePolicyModelGroup != null) { //if there is a policy group
valuePolicyModelGroup.remove(booleanValueControl); //remove this component from the group
}
booleanValueControl.removePropertyChangeListener(ValueControl.VALUE_PROPERTY, componentValueChangeListener); //stop listening for changes in the child component's value
}
getLayout().removeComponent(childComponent); //remove the component from the layout
super.removeComponent(childComponent); //do the default removal
}
/** The atomic flag that allows us to know whether we're synchronizing values, either from the list to the child controls or vice versa. */
protected final AtomicBoolean synchronizingValuesFlag = new AtomicBoolean(false);
/** Updates the list selected values based upon the current values of the child controls. */
protected void updateListSelectedValues() {
if(synchronizingValuesFlag.compareAndSet(false, true)) { //if we can synchronize values
try {
final Set selectedIndexes = new HashSet(); //create a set in which to store selected indexes
int i = 0; //keep track of the index
for(final Component component : getChildComponents()) { //look at all the child components
if(component instanceof ValueControl && ((ValueControl>)component).getValueClass().equals(Boolean.class)
&& Boolean.TRUE.equals(((ValueControl)component).getValue())) { //if the component is a Boolean value control set to TRUE
selectedIndexes.add(new Integer(i)); //add this index to the set of selected indexes
}
++i; //go to the next index
}
try {
setSelectedIndexes(toIntArray(selectedIndexes.toArray(new Integer[selectedIndexes.size()]))); //convert the selected indexes to an int array and set the selected indexes
} catch(final PropertyVetoException propertyVetoException) { //ignore any problems setting the new value
}
} finally //always clear the synchronizing flag
{
synchronizingValuesFlag.set(false); //indicate that we're finished synchronizing values
}
}
}
/** Updates the values of child controls based upon the current state of the list selected values. */
protected void updateChildControlValues() {
if(synchronizingValuesFlag.compareAndSet(false, true)) { //if we can synchronize values
try {
final int[] selectedIndexes = getSelectedIndexes(); //get the selected indexes
int i = 0; //keep track of the index
for(final Component component : getChildComponents()) { //look at all the child components
if(component instanceof ValueControl && ((ValueControl>)component).getValueClass().equals(Boolean.class)) { //if the component is a Boolean value control set to TRUE
try {
((ValueControl)component).setValue(Boolean.valueOf(Arrays.contains(selectedIndexes, i))); //select or unselect this control, based upon whether this index is selected
} catch(final PropertyVetoException propertyVetoException) { //we must ignore any problems setting the new value, because transitioning between boolean controls may result in a temporary state with no controls selected or with two controls selected, temporarily violating a validator, and there's no way to know if the change is transitory
}
}
++i; //go to the next index
}
} finally //always clear the synchronizing flag
{
synchronizingValuesFlag.set(false); //indicate that we're finished synchronizing values
}
}
}
/**
* {@inheritDoc}
*
* This version resets the theme of the given layout.
*
*/
@Override
public void resetTheme() {
super.resetTheme(); //reset the theme
setLayoutThemeApplied(false); //indicate that no theme has been applied to the layout
}
/**
* {@inheritDoc} This version checks to see if the theme needs to be applied to the given layout.
*/
@Override
public void updateTheme() throws IOException {
super.updateTheme(); //update the theme
if(!isLayoutThemeApplied()) { //if the theme haven't yet been applied to the layout (which also means that our version of applyTheme() hasn't been called, or it would have updated the layout theme applied status)
applyTheme(); //apply the theme to this component
}
}
/**
* {@inheritDoc}
*
* This version applies the theme to the current layout and updates the layout theme applied status.
*
*/
@Override
public void applyTheme() throws IOException {
super.applyTheme(); //apply the theme to this component normally
setLayoutThemeApplied(true); //indicate that we've applied the theme to the layout as well
}
@Override
public void applyTheme(final Theme theme) {
super.applyTheme(theme); //apply the theme to this component normally
theme.apply(getLayout()); //apply the theme to the currently installed layout
}
}