com.lyonesgamer.propertygrid.JPropertyGrid Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of JPropertyGrid Show documentation
Show all versions of JPropertyGrid Show documentation
A property editor component for Swing.
The newest version!
package com.lyonesgamer.propertygrid;
import com.lyonesgamer.propertygrid.properties.BooleanProperty;
import com.lyonesgamer.propertygrid.properties.PropertyCategory;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
/**
* A class representing a single property grid. This class fits into Swing like any other component.
*
* @author Tristan Patch
* @since 1.0
*/
public class JPropertyGrid extends JScrollPane {
protected BoxLayout layout;
protected JPanel innerPanel;
//Ordering is provided on the categories list.
protected List categories = new LinkedList<>();
protected Map> properties = new HashMap<>();
protected Map labels = new HashMap<>();
protected Map components = new HashMap<>();
protected Set listeners = new HashSet<>();
protected static final ImageIcon plusImage = new ImageIcon();
protected static final ImageIcon minusImage = new ImageIcon();
/**
* Creates a new property grid.
*/
public JPropertyGrid() {
super(VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED);
this.setAlignmentX(LEFT_ALIGNMENT);
this.setLayout(new ScrollPaneLayout());
innerPanel = new JPanel();
layout = new BoxLayout(innerPanel, BoxLayout.Y_AXIS);
innerPanel.setLayout(layout);
this.add(innerPanel);
super.setViewportView(innerPanel);
}
/**
* Adds a new listener for property changes. The listener is called when any property has its values changed either
* programmatically or by the user. The source will be the property and name will be the property name.
*
* @param listener The listener to add.
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
if (listeners != null)
listeners.add(listener);
}
/**
* Add a property as a new row. If the property already exists, returns the existing property. If the property is a
* category, returns the property without appending. If the category doesn't exist, appends the category.
*
* @param property The property to add.
* @param category The category this property falls under.
* @return The property just added; used for chaining.
*/
public PGProperty append(PGProperty property, PropertyCategory category){
if (property instanceof PropertyCategory) //If property is category
return property;
if (!properties.containsKey(category)) //If category doesn't exist
append(category);
PropertyCategory category1 = findProperty(property); //If property already exists
if (category1 != null) {
int index = findProperty(property, category1);
if (index != -1) return properties.get(category1).get(index);
}
List props = properties.get(category);
props.add(property);
PGTable propTable = components.get(category);
propTable.addRow(property);
property.afterAdded(this, category);
return property;
}
/**
* Add this category as a new row below the other categories. If the category already exists, returns the category.
*
* @param category The category you're adding.
* @return The category just added; used for chaining.
*/
public PropertyCategory append(PropertyCategory category) {
if (properties.containsKey(category))
return category;
categories.add(category);
properties.put(category, new ArrayList<>());
JLabel categoryLabel = new JLabel(category.name, minusImage, SwingConstants.LEFT);
categoryLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (isExpanded(category))
collapse(category);
else
expand(category);
}
});
PGTable table = new PGTable();
table.setRowHeight((int)(table.getRowHeight()*1.3));
labels.put(category, categoryLabel);
components.put(category, table);
if (properties.size() > 1)
innerPanel.add(Box.createHorizontalStrut(this.getWidth()));
innerPanel.add(categoryLabel);
innerPanel.add(table);
category.afterAdded(this, null);
return category;
}
/**
* Deletes all of the properties and categories from this grid.
*/
public void clear() {
properties.clear();
labels.clear();
components.clear();
}
/**
* Collapses a category, no longer showing the children.
*
* @param category The category to collapse.
* @return If the operation succeeded.
*/
public boolean collapse(PropertyCategory category) {
if (!labels.containsKey(category) || !components.containsKey(category))
return false;
components.get(category).setVisible(false);
labels.get(category).setIcon(plusImage);
return true;
}
/**
* Collapses all categories that have been added to this grid.
*
* @return If the operation succeeded.
*/
public boolean collapseAll() {
boolean success = true;
for (PropertyCategory category : properties.keySet())
if (!collapse(category))
success = false;
return success;
}
/**
* Utility function to change the value of a single property.
*
* @param property The property to change the value of.
* @param value The new value of the property.
* @param The type of value that belongs to the property.
* @return Whether the property could be changed to this value.
*/
public boolean changeProperty(PGProperty property, T value) {
if (findProperty(property, findProperty(property)) == -1) return false;
firePropertyChangeEvent(property, value);
property.setValue(value);
return true;
}
/**
* Removes a given property from the grid.
*
* @param property The property to remove.
*/
public void deleteProperty(PGProperty property) {
int index;
for (Map.Entry> entry : properties.entrySet())
if ((index = entry.getValue().indexOf(property)) != -1) {
entry.getValue().remove(index);
PGTable table = components.get(entry.getKey());
table.remove(index);
}
}
/**
* Removes the given category from the grid, removing all children as well.
*
* @param category The category to delete.
*/
public void deleteCategory(PropertyCategory category) {
categories.remove(category);
properties.remove(category);
labels.remove(category);
components.remove(category);
}
/**
* Expand a category, displaying all of its children below it.
*
* @param category The category to expand.
* @return If the operation succeeded.
*/
public boolean expand(PropertyCategory category) {
if (!labels.containsKey(category) || !components.containsKey(category))
return false;
components.get(category).setVisible(true);
labels.get(category).setIcon(minusImage);
return true;
}
/**
* Expand all the categories in this grid.
*
* @return If the operation succeeded.
*/
public boolean expandAll() {
boolean success = true;
for (PropertyCategory category : properties.keySet())
if (!expand(category))
success = false;
return success;
}
/**
* Finds the property in the grid and returns the category
* the property belongs to. If the property exists in multiple
* categories, behavior is undefined.
*
* @param property The property to find.
* @return The category holding the property.
*/
public PropertyCategory findProperty(PGProperty property) {
if (property == null)
return null;
for (Map.Entry> entry : properties.entrySet())
if (entry.getValue().contains(property))
return entry.getKey();
return null;
}
/**
* Finds the location of the property in the category. May be different than
* when added, if sort() was called.
*
* @param property The property to find.
* @param category The category to search in.
* @return The index of the property in the category.
*/
public int findProperty(PGProperty property, PropertyCategory category) {
if (category == null || !properties.containsKey(category))
return -1;
return properties.get(category).indexOf(property);
}
/**
* Alerts all listeners of a change to the given property.
*
* @param property The property being changed. This value is the change event's source, and gives its name and old value.
* @param value The new value being change to.
* @param The type of value being handled.
*/
public void firePropertyChangeEvent(PGProperty property, T value) {
T oldValue = property.getValue();
if (oldValue != null && value.equals(oldValue))
return;
for (PropertyChangeListener listener : listeners)
listener.propertyChange(new PropertyChangeEvent(property, property.getName(), oldValue, value));
}
/**
* Inserts a property into the middle of the grid.
*
* @param before The property which will be before this one. The category will be the same as this.
* @param newProperty The property added to the grid.
* @return The added property, used for chaining; null if `before` has not been added or newProperty has.
*/
public PGProperty insert(PGProperty before, PGProperty newProperty) {
if (before == null || newProperty == null)
return null;
PropertyCategory category = findProperty(before);
if (category == null)
return null;
if (findProperty(newProperty, category) >= 0)
return null;
int index = findProperty(before, category);
return insert(index+1, newProperty, category);
}
/**
* Inserts a property into the middle of the grid.
*
* @param index The location the property will be added at, starting at 0.
* @param newProperty The property added to the grid.
* @param category The category this property will be added to.
* @return The added property; used for chaining.
*/
public PGProperty insert(int index, PGProperty newProperty, PropertyCategory category) {
List props = properties.get(category);
props.add(index+1, newProperty);
PGTable propTable = components.get(category);
propTable.addRow(newProperty);
newProperty.afterAdded(this, category);
return newProperty;
}
/**
* Inserts a category into the middle of the grid.
*
* @param index The location the category will be added at, starting at 0.
* @param category The category to be added.
* @return The added category; used for chaining.
*/
public PropertyCategory insert(int index, PropertyCategory category) {
if (index < 0 || index > categories.size() || category == null)
return null;
categories.add(index, category);
properties.put(category, new ArrayList<>());
JLabel categoryLabel = new JLabel(category.name, minusImage, SwingConstants.LEFT);
categoryLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (isExpanded(category))
collapse(category);
else
expand(category);
}
});
PGTable table = new PGTable();
labels.put(category, categoryLabel);
components.put(category, table);
if (properties.size() > 1)
innerPanel.add(Box.createHorizontalStrut(this.getWidth()));
innerPanel.add(categoryLabel);
innerPanel.add(table);
category.afterAdded(this, null);
return category;
}
/**
* Gets the current expansion state of the category. Properties of a collapsed category maintain their state and can
* be used as if expanded.
*
* @param category The category.
* @return If the category is expanded.
*/
public boolean isExpanded(PropertyCategory category) {
return labels.containsKey(category) && components.containsKey(category) && components.get(category).isVisible();
}
/**
* Removes this listener from the grid. The listener will no longer be notified.
*
* @param listener The listener to remove.
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
listeners.remove(listener);
}
/**
* Replaces one property with another.
*
* @param old The property to be removed.
* @param newProperty The property to be added.
* @return The property added; used for chaining.
*/
public PGProperty replace(PGProperty old, PGProperty newProperty) {
if (old == null || newProperty == null)
return null;
PropertyCategory category = findProperty(old);
if (category == null)
return null;
if (findProperty(newProperty, category) >= 0)
return null;
int index = findProperty(old, category);
List props = properties.get(category);
props.remove(index);
props.add(index, newProperty);
PGTable propTable = components.get(category);
propTable.remove(index);
propTable.addRow(newProperty, index);
newProperty.afterAdded(this, category);
return newProperty;
}
/**
* Sorts properties according to given comparators.
*
* @param categoryComparator The function to use to compare categories.
* @param propertyComparator The function to use to compare properties within categories.
*/
public void sort(Comparator categoryComparator, Comparator propertyComparator) {
categories.sort(categoryComparator);
for (PropertyCategory category : properties.keySet())
properties.get(category).sort(propertyComparator);
}
/**
* Sorts categories and properties alphabetically.
*/
public void sort() {
categories.sort((o1, o2) -> o1.compareTo(o2));
for (PropertyCategory category : properties.keySet())
properties.get(category).sort((o1, o2) -> o1.compareTo(o2));
}
/**
* Sets the words used for `true` and `false` values in boolean properties that use choice boxes.
*
* @param trueString The word/string used to represent `true`.
* @param falseString The word/string used to represent `false`.
*/
public static void setBooleanNames(String trueString, String falseString) {
BooleanProperty.names.put(true, trueString);
BooleanProperty.names.put(false, falseString);
}
protected class PGTable extends JTable {
private static final long serialVersionUID = 131548102L;
private List data = new ArrayList<>();
private TableModel dataModel = new AbstractTableModel() {
@Override
public int getRowCount() {
return data.size();
}
@Override
public int getColumnCount() {
return 2;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
PGProperty property = data.get(rowIndex);
if (columnIndex == 0)
return property.name;
else
return property.getValue();
}
@Override
public boolean isCellEditable(int row, int col) {
return col == 1 && !data.get(row).isDisabled();
}
};
public PGTable() {
super();
this.setModel(dataModel);
}
@Override
public TableCellRenderer getCellRenderer(int row, int column) {
if (column == 1) {
PGProperty property = data.get(row);
return property.getRenderer();
} else {
return super.getCellRenderer(row, column);
}
}
@Override
public TableCellEditor getCellEditor(int row, int column) {
if (column == 1) {
PGProperty property = data.get(row);
return property.getEditor();
} else {
return super.getCellEditor(row, column);
}
}
public void addRow(PGProperty property) {
data.add(property);
}
public void addRow(PGProperty property, int index) {
data.add(index, property);
}
public void removeRow(int index) {
data.remove(index);
}
public void removeRow(PGProperty property) {
data.remove(property);
}
public void clear() {
data.clear();
}
}
}