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

io.github.jonestimd.swing.table.model.BeanListMultimapTableModel Maven / Gradle / Ivy

There is a newer version: 1.4.5
Show newest version
// Copyright (c) 2016 Timothy D. Jones
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package io.github.jonestimd.swing.table.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;

import javax.swing.table.AbstractTableModel;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import io.github.jonestimd.swing.table.sort.SectionTableRowSorter;
import io.github.jonestimd.util.JavaPredicates;

/**
 * A {@link SectionTableModel} that stores the bean groups in a {@link ListMultimap}.  Groups are displayed in ascending
 * order based on the group name.
 * @param  the type of the group keys
 * @param  the class representing a row in the table
 * @see SectionTableRowSorter
 */
public class BeanListMultimapTableModel extends AbstractTableModel implements ColumnIdentifier, SectionTableModel, BeanTableModel {
    private static final int GROWTH_FACTOR = 10;
    private final BeanTableAdapter beanTableAdapter;
    private final Function groupingFunction;
    private final Function groupNameFunction;
    private final ListMultimap groups = ArrayListMultimap.create();
    private final Comparator groupOrdering;
    private final List sortedGroups = new ArrayList<>();
    private int[] groupOffsets = new int[GROWTH_FACTOR];

    /**
     * Create a new model.
     * @param columnAdapters provides access to column values on the row beans
     * @param tableDataProviders supplemental data providers
     * @param groupingFunction provides the group key for a row
     * @param groupNameFunction provides the display name for a group
     */
    public BeanListMultimapTableModel(List> columnAdapters,
                                      Iterable> tableDataProviders,
                                      Function groupingFunction, Function groupNameFunction) {
        this.beanTableAdapter = new BeanTableAdapter<>(this, columnAdapters, tableDataProviders);
        this.groupingFunction = groupingFunction;
        this.groupNameFunction = groupNameFunction;
        groupOrdering = Comparator.comparing(groupNameFunction);
    }

    @Override
    public boolean isSectionRow(int rowIndex) {
        return Arrays.binarySearch(groupOffsets, 0, sortedGroups.size(), rowIndex) >= 0;
    }

    @Override
    public int getSectionRow(int rowIndex) {
        return groupOffsets[getGroupNumber(rowIndex)];
    }

    @Override
    public String getSectionName(int rowIndex) {
        int groupIndex = getGroupNumber(rowIndex);
        return groupNameFunction.apply(sortedGroups.get(groupIndex));
    }

    @Override
    public int getGroupNumber(int rowIndex) {
        int i = Arrays.binarySearch(groupOffsets, 0, sortedGroups.size(), rowIndex);
        return i >= 0 ? i : -i - 2;
    }

    @Override
    public List getGroup(int groupNumber) {
        return groups.get(sortedGroups.get(groupNumber));
    }

    public void setBeans(Collection beans) {
        setBeans(Multimaps.index(beans, groupingFunction::apply));
    }

    public void setBeans(Multimap beans) {
        groups.clear();
        groups.putAll(beans);
        sortedGroups.clear();
        sortedGroups.addAll(groups.keySet());
        sortedGroups.sort(groupOrdering);

        groupOffsets = new int[sortedGroups.size() + GROWTH_FACTOR];
        int groupIndex = 0;
        for (G group : sortedGroups) {
            groupOffsets[groupIndex+1] = groupOffsets[groupIndex] + groups.get(group).size() + 1;
            groupIndex++;
        }
        fireTableDataChanged();
        beanTableAdapter.setBeans(beans.values());
    }

    @Override
    public void updateBeans(Collection beans, BiPredicate isEqual) {
        for (T bean : beans) {
            int index = indexOf(item -> isEqual.test(bean, item));
            if (index < 0) put(groupingFunction.apply(bean), bean);
            else setBean(index, bean);
        }
    }

    public List getBeans() {
        return Lists.newArrayList(groups.values());
    }

    public List getSections() {
        return Collections.unmodifiableList(sortedGroups);
    }

    public List getBeans(G group) {
        return Collections.unmodifiableList(groups.get(group));
    }

    @Override
    public int getBeanCount() {
        return groups.size();
    }

    @Override
    public T getBean(int rowIndex) {
        if (isSectionRow(rowIndex)) {
            return null;
        }
        int groupNumber = getGroupNumber(rowIndex);
        return groups.get(sortedGroups.get(groupNumber)).get(rowIndex - groupOffsets[groupNumber] - 1);
    }

    protected void setBean(int rowIndex, T bean) {
        int groupNumber = getGroupNumber(rowIndex);
        groups.get(sortedGroups.get(groupNumber)).set(rowIndex - groupOffsets[groupNumber] - 1, bean);
        fireTableRowsUpdated(rowIndex, rowIndex);
    }

    @Override
    public Object getValue(T bean, int columnIndex) {
        return beanTableAdapter.getValue(bean, columnIndex);
    }

    public void addBean(T bean) {
        put(groupingFunction.apply(bean), bean);
    }

    /**
     * Add a bean to a group.
     */
    public void put(G group, T bean) {
        groups.put(group, bean);
        if (! sortedGroups.contains(group)) {
            sortedGroups.add(group);
            sortedGroups.sort(groupOrdering);
            int groupIndex = sortedGroups.indexOf(group);
            groupAdded(groupIndex, 1);
        }
        else {
            int groupIndex = sortedGroups.indexOf(group);
            groupChanged(groupIndex, 1);
            fireTableRowsInserted(groupOffsets[groupIndex+1]-1, groupOffsets[groupIndex+1]-1);
        }
        beanTableAdapter.addBean(bean);
    }

    /**
     * Remove a bean from a group.  If the group is empty then the group is also removed.
     */
    public void remove(int rowIndex) {
        T bean = getBean(rowIndex);
        int groupIndex = getGroupNumber(rowIndex);
        G group = sortedGroups.get(groupIndex);
        if (groups.remove(group, bean)) {
            if (groups.get(group).isEmpty()) {
                groupRemoved(groupIndex, rowIndex - 1, 1);
            }
            else {
                groupChanged(groupIndex, -1);
                fireTableRowsDeleted(rowIndex, rowIndex);
            }
            beanTableAdapter.removeBean(bean);
        }
    }

    /**
     * Remove an entire group.
     * @return the removed beans.
     */
    public List removeAll(G group) {
        int groupIndex = sortedGroups.indexOf(group);
        int groupOffset = groupOffsets[groupIndex];
        List beans = groups.removeAll(group);
        groupRemoved(groupIndex, groupOffset, beans.size());
        return beans;
    }

    /**
     * Add beans to a group.
     */
    public void putAll(G group, Collection beans) {
        groups.putAll(group, beans);
        if (! sortedGroups.contains(group)) {
            sortedGroups.add(group);
            sortedGroups.sort(groupOrdering);
            int groupIndex = sortedGroups.indexOf(group);
            groupAdded(groupIndex, beans.size());
        }
        else {
            int groupIndex = sortedGroups.indexOf(group);
            groupChanged(groupIndex, beans.size());
            fireTableRowsInserted(groupOffsets[groupIndex+1]-beans.size(), groupOffsets[groupIndex+1]-1);
        }
    }

    private void groupRemoved(int groupIndex, int groupOffset, int size) {
        groupChanged(groupIndex, -size - 1);
        sortedGroups.remove(groupIndex);
        System.arraycopy(groupOffsets, groupIndex + 1, groupOffsets, groupIndex, groupOffsets.length - groupIndex - 1);
        fireTableRowsDeleted(groupOffset, groupOffset + size);
    }

    private void groupAdded(int groupIndex, int groupSize) {
        if (sortedGroups.size()+1 > groupOffsets.length) {
            groupOffsets = Arrays.copyOf(groupOffsets, groupOffsets.length + GROWTH_FACTOR);
        }
        for (int i = sortedGroups.size(); i > groupIndex; i--) {
            groupOffsets[i] = groupOffsets[i-1] + 1 + groupSize;
        }
        fireTableRowsInserted(groupOffsets[groupIndex], groupOffsets[groupIndex]+groupSize);
    }

    private void groupChanged(int groupIndex, int delta) {
        for (int i = groupIndex+1; i <= sortedGroups.size(); i++) {
            groupOffsets[i] += delta;
        }
    }

    @Override
    public ColumnAdapter getColumnIdentifier(int modelIndex) {
        return beanTableAdapter.getColumnAdapter(modelIndex);
    }

    @Override
    public String getColumnName(int modelIndex) {
        return beanTableAdapter.getColumnName(modelIndex);
    }

    @Override
    public Class getColumnClass(int columnIndex) {
        return beanTableAdapter.getColumnClass(columnIndex);
    }

    @Override
    public int getRowCount() {
        return sortedGroups.size() + groups.values().size();
    }

    @Override
    public int getColumnCount() {
        return beanTableAdapter.getColumnCount();
    }

    // TODO call from fireEvent methods instead of from sub-class
    protected void notifyDataProviders(T row, String columnId, Object oldValue) {
        beanTableAdapter.notifyDataProviders(row, indexOf(row::equals), columnId, oldValue);
    }

    /**
     * Find the index of a bean matching a predicate.
     * @return the index of the first matching bean or -1 if none match.
     */
    public int indexOf(Predicate predicate) {
        return groups.entries().stream().filter(JavaPredicates.onResult(Entry::getValue, predicate))
                .findFirst().map(this::indexOf)
                .orElse(-1);
    }

    private int indexOf(Entry entry) {
        int groupIndex = sortedGroups.indexOf(entry.getKey());
        return groupOffsets[groupIndex] + groups.get(entry.getKey()).indexOf(entry.getValue()) + 1;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        if (isSectionRow(rowIndex)) {
            return null;
        }
        return beanTableAdapter.getValue(getBean(rowIndex), columnIndex);
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return beanTableAdapter.isCellEditable(getBean(rowIndex), columnIndex);
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (isSectionRow(rowIndex)) {
            throw new IllegalArgumentException("Can't edit section row");
        }
        beanTableAdapter.setValue(aValue, getBean(rowIndex), rowIndex, columnIndex);
        fireTableCellUpdated(rowIndex, columnIndex);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy