/*
* Copyright 2001-2013 Stephen Colebourne
*
* 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 org.joda.beans.ui.swing.component;
import java.awt.AWTError;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager2;
import java.awt.Rectangle;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.ButtonModel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
/**
* A panel that contains radio buttons.
* <p>
* This {@code JPanel} is an easy way to produce a linked set of radio buttons.
* The buttons will be correctly initialized into a {@code ButtonGroup}.
* <p>
* Each radio button is linked to a domain object and it is the domain object
* that can be set and queried on the panel.
* Pass the map of domain object to display text into the constructor.
* It is also possible to add radio buttons one at a time setting the constraint
* to be the domain object.
* <p>
* By default, the layout will place two or three buttons on the same
* line. If there are four buttons, there are two rows of two. Five or
* more will be arranged vertically. This behaviour can be overridden
* by the columns property.
*
* @param <T> the type of the data being displayed
*/
public class JRadioButtonPanel<T> extends JPanel {
/** Serialization version. */
private static final long serialVersionUID = 1L;
/** Masked null. */
private static final String NULL = "";
/**
* The button group.
*/
private final ButtonGroup buttonGroup = new ButtonGroup();
/**
* The map from key to button.
*/
private final Map<T, JRadioButton> keyToButtonMap = new HashMap<>();
/**
* The map from model to key.
*/
private final Map<ButtonModel, T> modelToKeyMap = new HashMap<>();
/**
* Creates an instance.
*/
public JRadioButtonPanel() {
super();
setLayout(new RadioButtonLayout(this, -1));
}
/**
* Creates an instance.
* <p>
* The map should typically be ordered.
* The key may be any immutable value type object, such as {@code Boolean} or an {@code Enum}.
*
* @param keyToText the map from data key to display text for each radio button, not null
*/
public JRadioButtonPanel(Map<T, String> keyToText) {
super();
init(keyToText);
}
/**
* Creates an instance.
* <p>
* The map should typically be ordered.
* The key may be any immutable value type object, such as {@code Boolean} or an {@code Enum}.
* Null may be used as a key.
*
* @param keyToText the map from data key to display text for each radio button, not null
* @param selectedKey the selected key, null if no selection
*/
public JRadioButtonPanel(Map<T, String> keyToText, T selectedKey) {
super();
init(keyToText);
setSelection(selectedKey);
}
private void init(Map<T, String> keyToText) {
Objects.requireNonNull(keyToText, "keyToText");
setBorder(BorderFactory.createEmptyBorder());
setLayout(new RadioButtonLayout(this, -1));
int i = 0;
for (Entry<T, String> entry : keyToText.entrySet()) {
Object key = maskNull(entry.getKey());
Objects.requireNonNull(entry.getValue(), "keyToText[" + i++ + "]");
JRadioButton btn = new JRadioButton(entry.getValue());
add(btn, key);
}
}
//-------------------------------------------------------------------------
/**
* Gets the button group that this panel is managing.
*
* @return the button group, not null
*/
public ButtonGroup getButtonGroup() {
return buttonGroup;
}
//-------------------------------------------------------------------------
/**
* Gets the number of columns to group the radio buttons in.
*
* @return the number of columns, one or greater, or minus one for default
*/
public int getLayoutColumns() {
if (getLayout() instanceof RadioButtonLayout) {
return ((RadioButtonLayout) getLayout()).getColumns();
}
return -1;
}
/**
* Sets the number of columns to group the radio buttons in.
*
* @param columns the number of columns, one or greater
*/
public void setLayoutColumns(int columns) {
setLayout(new RadioButtonLayout(this, columns));
}
//-------------------------------------------------------------------------
/**
* Gets the current selection key of the radio button set.
* <p>
* This returns the key associated with the text, not the text itself.
*
* @return the object key associated with the selection, null if no selection
*/
public T getSelection() {
ButtonModel selected = buttonGroup.getSelection();
if (selected == null) {
return null;
}
T key = modelToKeyMap.get(selected);
if (key == null) {
throw new IllegalArgumentException("Key not found for selected radio button: " + selected);
}
return (key != NULL ? key : null);
}
/**
* Sets the current selection key of the radio button set.
*
* @param key the key to select, null may be used as a key
* @throws IllegalArgumentException if the key is not valid
*/
public void setSelection(T key) {
key = maskNull(key);
JRadioButton btn = keyToButtonMap.get(key);
if (btn == null) {
throw new IllegalArgumentException("Key is not a valid radio button: " + key);
}
buttonGroup.setSelected(btn.getModel(), true);
}
/**
* Clears the current selection of the radio button set.
*/
public void clearSelection() {
buttonGroup.clearSelection();
}
//-------------------------------------------------------------------------
/**
* Override add method to attach radio buttons to the button group.
* <p>
* The constraints are used as the key.
*
* @param comp the component to add, not null
* @param constraints the constraints to use, not null
* @param index the index to add at
*/
@Override
protected void addImpl(Component comp, Object constraints, int index) {
Objects.requireNonNull(comp, "comp");
Objects.requireNonNull(constraints, "constraints");
if (comp instanceof JRadioButton == false) {
throw new IllegalArgumentException("Only a JRadioButton can be added to JRadioButtonPanel");
}
super.addImpl(comp, constraints, index);
JRadioButton radio = (JRadioButton) comp;
buttonGroup.add(radio);
// use constraints as the key
@SuppressWarnings("unchecked")
T key = (T) constraints;
keyToButtonMap.put(key, radio);
modelToKeyMap.put(radio.getModel(), key);
}
@Override
public void remove(Component comp) {
Objects.requireNonNull(comp, "comp");
if (comp instanceof JRadioButton == false) {
throw new IllegalArgumentException("Only a JRadioButton can be removed from JRadioButtonPanel");
}
JRadioButton radio = (JRadioButton) comp;
Object key = modelToKeyMap.remove(radio.getModel());
if (key == null) {
throw new IllegalArgumentException("Only a JRadioButton that was previously added can be removed");
}
keyToButtonMap.remove(key);
buttonGroup.remove(radio);
super.remove(comp);
}
@Override
public void remove(int index) {
remove(getComponent(index));
}
@Override
public void removeAll() {
while (buttonGroup.getButtonCount() > 0) {
buttonGroup.remove(buttonGroup.getElements().nextElement());
}
modelToKeyMap.clear();
keyToButtonMap.clear();
}
@SuppressWarnings("unchecked")
private static <T> T maskNull(T key) {
return (key != null ? key : (T) NULL);
}
//-------------------------------------------------------------------------
// delegate baseline through to first radio button
@Override
public int getBaseline(int width, int height) {
if (getComponentCount() > 0) {
return getComponent(0).getBaseline(width, height);
}
return super.getBaseline(width, height);
}
@Override
public BaselineResizeBehavior getBaselineResizeBehavior() {
if (getComponentCount() > 0) {
return getComponent(0).getBaselineResizeBehavior();
}
return super.getBaselineResizeBehavior();
}
//-------------------------------------------------------------------------
/**
* Layout manager to manage the radio buttons
*/
public static class RadioButtonLayout implements LayoutManager2 {
private Container target;
private int columns = -1;
private Rectangle[] layout;
private int xTotal;
private int yTotal;
/**
* Creates an instance.
*
* @param target the target container, not null
* @param columns the number of columns, one or greater, or minus one for default
*/
public RadioButtonLayout(Container target, int columns) {
super();
this.target = target;
this.columns = columns;
}
/**
* Gets the number of columns to group the radio buttons in.
*
* @return the number of columns, one or greater
*/
public int getColumns() {
return columns;
}
@Override
public float getLayoutAlignmentX(Container target) {
checkContainer(target);
return 0;
}
@Override
public float getLayoutAlignmentY(Container target) {
checkContainer(target);
return 0.5f;
}
@Override
public void invalidateLayout(Container target) {
checkContainer(target);
layout = null;
xTotal = 0;
yTotal = 0;
}
@Override
public void addLayoutComponent(Component comp, Object constraints) {
layout = null;
xTotal = 0;
yTotal = 0;
}
@Override
public void addLayoutComponent(String name, Component comp) {
layout = null;
xTotal = 0;
yTotal = 0;
}
@Override
public void removeLayoutComponent(Component comp) {
layout = null;
xTotal = 0;
yTotal = 0;
}
/**
* Imposes the layout onto the children.
*
* @param target the target container, not null
*/
@Override
public void layoutContainer(Container target) {
checkContainer(target);
calculate(target);
int columns = calculateColumns(target);
int rows = calculateRows(target);
Dimension alloc = target.getSize();
Insets in = target.getInsets();
int allocHeight = alloc.height - in.top - in.bottom;
int centre = 0;
if (allocHeight > yTotal) {
centre = (allocHeight - yTotal) / 2;
}
Component[] comps = target.getComponents();
int comp = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (comp < comps.length) {
comps[comp].setBounds(
in.left + layout[comp].x,
in.top + centre + layout[comp].y,
layout[comp].width,
layout[comp].height);
}
comp++;
}
}
}
/**
* Calculate the number of columns.
*/
private int calculateColumns(Container target) {
if (columns > 0) {
return columns;
}
int compCount = target.getComponentCount();
if (compCount < 4) {
return compCount;
}
if (compCount == 4) {
return 2;
}
return 1;
}
/**
* Calculate the number of rows.
*/
private int calculateRows(Container target) {
int compCount = target.getComponentCount();
if (columns > 0) {
return (compCount + columns - 1) / columns;
}
if (compCount < 4) {
return 1;
}
if (compCount == 4) {
return 2;
}
return compCount;
}
/**
* Calculate sizes.
*/
private void calculate(Container target) {
if (layout != null) {
return;
}
int columns = calculateColumns(target);
int rows = calculateRows(target);
Component[] comps = target.getComponents();
layout = new Rectangle[comps.length];
for (int i = 0; i < comps.length; i++) {
layout[i] = new Rectangle();
}
int comp = 0;
// setup basic sizes in out array
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (comp < comps.length) {
layout[comp].setSize(comps[comp].getPreferredSize());
}
comp++;
}
}
// set column to same width
for (int j = 0; j < columns; j++) {
int biggest = 0;
for (int i = 0; i < rows; i++) {
comp = i * columns + j;
if (comp < comps.length) {
biggest = Math.max(biggest, layout[comp].width);
}
}
for (int i = 0; i < rows; i++) {
comp = i * columns + j;
if (comp < comps.length) {
layout[comp].width = biggest;
}
}
}
// setup x y positions
int xTotal = 0;
int yTotal = 0;
for (int i = 0; i < rows; i++) {
xTotal = 0;
for (int j = 0; j < columns; j++) {
comp = i * columns + j;
if (comp < comps.length) {
layout[comp].setLocation(xTotal, yTotal);
xTotal = xTotal + layout[comp].width;
}
}
yTotal = yTotal + layout[i * columns].height;
}
this.xTotal = xTotal;
this.yTotal = yTotal;
}
@Override
public Dimension minimumLayoutSize(Container target) {
return preferredLayoutSize(target);
}
@Override
public Dimension preferredLayoutSize(Container target) {
checkContainer(target);
calculate(target);
Insets in = target.getInsets();
return new Dimension(xTotal + in.left + in.right, yTotal + in.top + in.bottom);
}
@Override
public Dimension maximumLayoutSize(Container target) {
return preferredLayoutSize(target);
}
/**
* Ensure the correct container is being used.
*/
void checkContainer(Container target) {
if (this.target != target) {
throw new AWTError("Layout can't be shared");
}
}
}
}