All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.lyonesgamer.propertygrid.JPropertyGrid Maven / Gradle / Ivy

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();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy