pipeline.GUI_utils.JXTablePerColumnFiltering.java Source code

Java tutorial

Introduction

Here is the source code for pipeline.GUI_utils.JXTablePerColumnFiltering.java

Source

/*******************************************************************************
 * Parismi v0.1
 * Copyright (c) 2009-2015 Cinquin Lab.
 * All rights reserved. This code is made available under a dual license:
 * the two-clause BSD license or the GNU Public License v2.
 ******************************************************************************/
package pipeline.GUI_utils;

import java.awt.Component;
import java.awt.FileDialog;
import java.awt.Frame;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.EventObject;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.RowFilter;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;

import org.apache.commons.collections.primitives.ArrayFloatList;
import org.apache.commons.collections.primitives.ArrayIntList;
import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.table.TableColumnExt;
import org.jfree.data.statistics.HistogramDataset;
import org.jfree.data.statistics.HistogramType;

import pipeline.GUI_utils.bean_table.BeanTableModel;
import pipeline.GUI_utils.bean_table.RowTableModel;
import pipeline.misc_util.Utils;
import pipeline.misc_util.Utils.LogLevel;
import pipeline.parameter_cell_views.FloatRangeSlider;
import pipeline.parameter_cell_views.IntRangeSlider;
import pipeline.parameter_cell_views.TextBox;
import pipeline.parameters.AbstractParameter;
import pipeline.parameters.FloatRangeParameter;
import pipeline.parameters.IntRangeParameter;
import pipeline.parameters.ParameterListener;
import pipeline.parameters.SpreadsheetCell;
import pipeline.parameters.TextParameter;

/**
 * JTable that displays controls to filter each column based on a range of integers or floats, or based on String
 * contents. The type
 * of each column is automatically detected by examining the first row of data. This class assumes that the underlying
 * model extends {@link RowTableModel}.
 *
 */
public class JXTablePerColumnFiltering extends JXTable {

    private RowFilter<Object, Object> filter = new RowFilter<Object, Object>() {
        @Override
        public boolean include(Entry<?, ?> entry) {
            // Utils.log("Row filter called",LogLevel.DEBUG);
            if (filteringModel.getColumnCount() == 0)
                return true;
            try {
                for (int i = entry.getValueCount() - 1; i >= 0; i--) {
                    if (filteringModel.getValueAt(0, i) == null)
                        continue;
                    Object v = entry.getValue(i);
                    if (v instanceof String) {
                        if (!(filteringModel.getValueAt(0, i) instanceof TextParameter))
                            continue;
                        if ((!"".equals(((TextParameter) filteringModel.getValueAt(0, i)).getStringValue()))
                                && !v.equals(filteringModel.getValueAt(0, i)))
                            return false;
                    } else if (v instanceof Float || v instanceof Double) {
                        float f = ((Number) v).floatValue();
                        float[] range = (float[]) ((FloatRangeParameter) filteringModel.getValueAt(0, i))
                                .getValue();
                        if (f < range[0]) {
                            // Utils.log("Excluding based on column "+i,LogLevel.VERBOSE_DEBUG);
                            return false;
                        }
                        if (f > range[1]) {
                            // Utils.log("Excluding based on column  "+i,LogLevel.VERBOSE_DEBUG);
                            return false;
                        }
                    } else if (v instanceof SpreadsheetCell) {
                        float f = ((SpreadsheetCell) v).getFloatValue();
                        float[] range = (float[]) ((FloatRangeParameter) filteringModel.getValueAt(0, i))
                                .getValue();
                        if (f < range[0]) {
                            // Utils.log("Excluding based on column  "+i,LogLevel.VERBOSE_DEBUG);
                            return false;
                        }
                        if (f > range[1]) {
                            // Utils.log("Excluding based on column  "+i,LogLevel.VERBOSE_DEBUG);
                            return false;
                        }
                    } else if (v instanceof Integer) {
                        int f = (Integer) v;
                        int[] range = (int[]) ((IntRangeParameter) filteringModel.getValueAt(0, i)).getValue();
                        if (f < range[0]) {
                            // Utils.log("Excluding based on column  "+i,LogLevel.VERBOSE_DEBUG);
                            return false;
                        }
                        if (f > range[1]) {
                            // Utils.log("Excluding based on column  "+i,LogLevel.VERBOSE_DEBUG);
                            return false;
                        }
                    }
                }
            } catch (Exception e) {
                Utils.printStack(e);
                return true;// If something went wrong during evaluation, do not hide the row
            }
            return true;
        }
    };

    String getRowAsString(int index) {
        StringBuffer b = new StringBuffer();
        for (int i = 0; i < this.getColumnCount(); i++) {
            Object value = getValueAt(index, i);
            if (Utils.skipListsInTableTextExport
                    && (value instanceof List || value instanceof ArrayFloatList || value instanceof ArrayIntList))
                b.append("skipped_list");
            else
                b.append(value + "");
            if (i < getColumnCount() - 1)
                b.append("\t");
        }
        return b.toString();
    }

    public int getNumberFilteredRows() {
        int nFiltered = 0;
        for (int i = 0; i < model.getRowCount(); i++) {
            int rowIndex = JXTablePerColumnFiltering.this.convertRowIndexToView(i);
            if (rowIndex == -1) {
                nFiltered++;
            }
        }
        return nFiltered;
    }

    /**
     *
     * @param columnsFormulasToPrint
     *            If null, print all columns
     * @param stripNewLinesInCells
     * @param filePath
     *            Full path to save file to; if null, user will be prompted.
     */
    public void saveFilteredRowsToFile(List<Integer> columnsFormulasToPrint, boolean stripNewLinesInCells,
            String filePath) {
        String saveTo;
        if (filePath == null) {
            FileDialog dialog = new FileDialog(new Frame(), "Save filtered rows to", FileDialog.SAVE);
            dialog.setVisible(true);
            saveTo = dialog.getDirectory();
            if (saveTo == null)
                return;

            saveTo += "/" + dialog.getFile();
        } else {
            saveTo = filePath;
        }

        PrintWriter out = null;
        try {
            out = new PrintWriter(saveTo);

            StringBuffer b = new StringBuffer();
            if (columnsFormulasToPrint == null) {
                for (int i = 0; i < this.getColumnCount(); i++) {
                    b.append(getColumnName(i));
                    if (i < getColumnCount() - 1)
                        b.append("\t");
                }
            } else {
                int index = 0;
                for (int i : columnsFormulasToPrint) {
                    b.append(getColumnName(i));
                    if (index + 1 < columnsFormulasToPrint.size())
                        b.append("\t");
                    index++;
                }
            }
            out.println(b);

            for (int i = 0; i < model.getRowCount(); i++) {
                int rowIndex = convertRowIndexToView(i);
                if (rowIndex > -1) {
                    if (columnsFormulasToPrint == null)
                        if (stripNewLinesInCells)
                            out.println(getRowAsString(rowIndex).replaceAll("\n", " "));
                        else
                            out.println(getRowAsString(rowIndex));
                    else {
                        boolean first = true;
                        for (int column : columnsFormulasToPrint) {
                            if (!first) {
                                out.print("\t");
                            }
                            first = false;
                            SpreadsheetCell cell = (SpreadsheetCell) getValueAt(rowIndex, column);
                            if (cell != null) {
                                String formula = cell.getFormula();
                                if (formula != null) {
                                    if (stripNewLinesInCells)
                                        out.print(formula.replaceAll("\n", " "));
                                    else
                                        out.print(formula);
                                }
                            }
                        }
                        out.println("");
                    }
                }
            }

            out.close();
        } catch (FileNotFoundException e) {
            Utils.printStack(e);
            Utils.displayMessage("Could not save table", true, LogLevel.ERROR);
        }
    }

    private static final long serialVersionUID = 1L;

    JXTable filteringTable, tableForColumnDescriptions;
    private RowTableModel<?> model;
    private TableModel filteringModel;
    private int nColumns;

    private class filterListener implements ParameterListener {
        int columnIndex = -1;

        public filterListener(int index) {
            columnIndex = index;
        }

        @Override
        public void parameterValueChanged(boolean stillChanging, AbstractParameter parameterWhoseValueChanged,
                boolean keepQuiet) {
            // .log("Update filter for column "+parameterWhoseValueChanged.creatorReference,LogLevel.DEBUG);
            // sorterChanged(new RowSorterEvent(getRowSorter()));
            // invalidate();
            if (silenceUpdates.get() == 0) {
                model.fireTableChanged(
                        new PipelineTableModelEvent(model, PipelineTableModelEvent.FILTER_ADJUSTING));
            }
            // if (needToInitializeFilterModel) initializeFilterModel();
            // Utils.log(""+model.getRowCount(),LogLevel.DEBUG);
        }

        @Override
        public void buttonPressed(String commandName, AbstractParameter parameter, ActionEvent event) {
            switch (commandName) {
            case "Reset Range":
                updateRangeOfColumn(columnIndex, true, BOTH_BOUNDS, true);
                break;
            case "Reset Min":
                updateRangeOfColumn(columnIndex, true, LOWER_BOUND, true);
                break;
            case "Reset Max":
                updateRangeOfColumn(columnIndex, true, UPPER_BOUND, true);
                break;
            default:
                throw new IllegalStateException("Unknown command " + commandName);
            }

        }

        @Override
        public void parameterPropertiesChanged(AbstractParameter parameterWhosePropertiesChanged) {

        }

        @Override
        public boolean alwaysNotify() {
            return false;
        }

        @Override
        public String getParameterName() {
            throw new RuntimeException("Unimplemented");
        }

        @Override
        public void setParameterName(String name) {
            throw new RuntimeException("Unimplemented");
        }
    }

    public void resetFilterRanges(boolean notifyTableListeners) {
        silenceUpdates.incrementAndGet();
        for (int i = 0; i < getColumnCount(); i++) {
            updateRangeOfColumn(i, true, 0, true);
        }
        silenceUpdates.decrementAndGet();
        if (notifyTableListeners)
            model.fireTableDataChanged();
    }

    public static final int BOTH_BOUNDS = 0, LOWER_BOUND = -1, UPPER_BOUND = 1;

    public void updateRangeOfColumn(int columnIndex, boolean reinitializeSelection, int boundsToUpdate,
            boolean suppressModelInit) {
        if (!suppressModelInit) {
            needToInitializeFilterModel = true;
            initializeFilterModel();
        }
        boolean isFloat = model.getValueAt(0, columnIndex) instanceof Float;
        boolean isDouble = model.getValueAt(0, columnIndex) instanceof Double;
        boolean isInteger = model.getValueAt(0, columnIndex) instanceof Integer;
        boolean isSpreadsheetCell = model.getValueAt(0, columnIndex) instanceof SpreadsheetCell;
        if (!(isFloat || isInteger || isSpreadsheetCell || isDouble))
            return;
        double min = Double.MAX_VALUE;
        double max = Double.MIN_VALUE;

        double[] valuesForHistogram = new double[model.getRowCount()];

        for (int i = 0; i < model.getRowCount(); i++) {
            double value;
            if (isFloat)
                value = (Float) model.getValueAt(i, columnIndex);
            else if (isDouble)
                value = (Double) model.getValueAt(i, columnIndex);
            else if (isInteger)
                value = (Integer) model.getValueAt(i, columnIndex);
            else {
                value = ((SpreadsheetCell) model.getValueAt(i, columnIndex)).getFloatValue();
            }
            if (Double.isNaN(value))
                value = 0.0d;
            if (value < min)
                min = value;
            if (value > max)
                max = value;
            valuesForHistogram[i] = value;
        }

        // Now compute a histogram; this could be optimized

        HistogramDataset dataset = new HistogramDataset();
        dataset.setType(HistogramType.RELATIVE_FREQUENCY);
        dataset.addSeries("Histogram", valuesForHistogram, 15);

        if (isFloat || isDouble || isSpreadsheetCell) {
            FloatRangeParameter param = (FloatRangeParameter) filteringModel.getValueAt(0, columnIndex);
            param.histogram = dataset;
            float[] currentValue = (float[]) param.getValue();
            if ((boundsToUpdate == BOTH_BOUNDS) || boundsToUpdate == LOWER_BOUND)
                currentValue[2] = (float) min;
            if ((boundsToUpdate == BOTH_BOUNDS) || boundsToUpdate == UPPER_BOUND)
                currentValue[3] = (float) max;
            if (reinitializeSelection) {
                currentValue[0] = currentValue[2];
                currentValue[1] = currentValue[3];
            }
            param.setValueFireIfAppropriate(currentValue, false, true, true);
        } else {
            IntRangeParameter param = (IntRangeParameter) filteringModel.getValueAt(0, columnIndex);
            int[] currentValue = (int[]) param.getValue();
            if ((boundsToUpdate == BOTH_BOUNDS) || boundsToUpdate == LOWER_BOUND)
                currentValue[2] = (int) min;
            if ((boundsToUpdate == BOTH_BOUNDS) || boundsToUpdate == UPPER_BOUND)
                currentValue[3] = (int) max;
            if (reinitializeSelection) {
                currentValue[0] = currentValue[2];
                currentValue[1] = currentValue[3];
            }
            param.setValueFireIfAppropriate(currentValue, false, true, true);
        }
    }

    transient public boolean needToInitializeFilterModel = true;

    public void initializeFilterModel() {
        if (!needToInitializeFilterModel)
            return;
        if (model.getRowCount() > 0) {
            needToInitializeFilterModel = false;
            updateFilteringTable();
            for (int i = 0; i < model.getColumnCount(); i++) {
                if ((model.getValueAt(0, i) instanceof Float) || (model.getValueAt(0, i) instanceof SpreadsheetCell)
                        || (model.getValueAt(0, i) instanceof Double)) {
                    filteringModel.setValueAt(new FloatRangeParameter("", "", 0.0f, 0.0f, 0.0f, 0.0f, true, true,
                            new filterListener(i), i), 0, i);
                } else if (model.getValueAt(0, i) instanceof Integer)
                    filteringModel.setValueAt(
                            new IntRangeParameter("", "", 0, 0, 0, 0, true, true, new filterListener(i), i), 0, i);
                else if (model.getValueAt(0, i) instanceof String)
                    filteringModel.setValueAt(new TextParameter("", "", "", true, new filterListener(i), i), 0, i);
                else if (model.getValueAt(0, i) == null) {
                    // Ignore the column since we can't determine the object type
                } else
                    Utils.log("Unsupported object type at column " + i, LogLevel.DEBUG);// +": "+model.getValueAt(0, i)
                updateRangeOfColumn(i, true, 0, true);
            }
        } else
            needToInitializeFilterModel = true;
    }

    transient AtomicInteger silenceUpdates = new AtomicInteger(0);

    private ColumnHeaderToolTips tips;

    @Override
    public void setModel(TableModel newModel) {
        this.model = (RowTableModel<?>) newModel;
        super.setModel(newModel);
        updateFilteringTable();
        this.setRowFilter(filter);
    }

    private void updateFilteringTable() {
        nColumns = model.getColumnCount();
        filteringModel = new DefaultTableModel(1, nColumns);
        if (filteringTable != null) {
            filteringTable.setModel(filteringModel);
            for (int i = 0; i < nColumns; i++) {
                MultiRenderer multiRenderer = getMultiRenderer();
                filteringTable.getColumn(i).setCellEditor(multiRenderer);
                filteringTable.getColumn(i).setCellRenderer(multiRenderer);
            }
        }
    }

    private static void updateColumn(TableColumnExt template, TableColumnExt toUpdate) {
        if (template == null || toUpdate == null) {
            return;
        }
        if (toUpdate.getModelIndex() != template.getModelIndex()) {
            toUpdate.setModelIndex(template.getModelIndex());
        }
        if (toUpdate.getWidth() != template.getWidth()) {
            toUpdate.setWidth(template.getWidth());
            toUpdate.setPreferredWidth(template.getPreferredWidth());
            toUpdate.setMaxWidth(template.getWidth());
            toUpdate.setMinWidth(template.getWidth());
        }
        if (toUpdate.isVisible() != template.isVisible()) {
            toUpdate.setVisible(template.isVisible());
        }

    }

    void updateFilteringTableSetup() {
        if (filteringTable == null) {
            return;
        }
        int nColumns = getColumns(true).size();
        if (nColumns != filteringTable.getColumns(true).size()) {
            updateFilteringTable();
        }
        final boolean updateDesc = tableForColumnDescriptions != null
                && tableForColumnDescriptions.getColumns().size() == nColumns;
        for (int i = 0; i < nColumns; i++) {
            TableColumnExt filteringColumn = (TableColumnExt) filteringTable.getColumns(true).get(i);
            TableColumnExt dataColumn = (TableColumnExt) getColumns(true).get(i);
            updateColumn(dataColumn, filteringColumn);

            if (updateDesc) {
                TableColumnExt descColumn = (TableColumnExt) tableForColumnDescriptions.getColumns(true).get(i);
                updateColumn(dataColumn, descColumn);
            }
        }
    }

    @Override
    public void columnMoved(TableColumnModelEvent e) {
        super.columnMoved(e);
        if (e.getFromIndex() == e.getToIndex()) {
            return;
        }
        try {
            filteringTable.editingCanceled(null);
            if (e.getFromIndex() != -1 && e.getFromIndex() < getColumns(true).size()) {
                filteringTable.moveColumn(e.getFromIndex(), e.getToIndex());
                tableForColumnDescriptions.moveColumn(e.getFromIndex(), e.getToIndex());
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            //Changes in column counts apparently occur when hiding/unhiding columns,
            //which generate column move events
            Utils.printStack(ex, LogLevel.DEBUG);
        }
        updateFilteringTableSetup();
    }

    @Override
    public void columnPropertyChange(PropertyChangeEvent event) {
        super.columnPropertyChange(event);
        updateFilteringTableSetup();
    }

    @Override
    public void columnMarginChanged(ChangeEvent e) {
        super.columnMarginChanged(e);
        updateFilteringTableSetup();
    }

    private static MultiRenderer getMultiRenderer() {
        MultiRenderer multiRenderer = new MultiRenderer();

        FloatRangeSlider myFloatSliderRange = new FloatRangeSlider();
        FloatRangeSlider myFloatSliderRange2 = new FloatRangeSlider();
        IntRangeSlider myIntSliderRange = new IntRangeSlider();
        IntRangeSlider myIntSliderRange2 = new IntRangeSlider();
        TextBox textBox = new TextBox();
        TextBox textBox2 = new TextBox();

        multiRenderer.registerRenderer(IntRangeParameter.class, myIntSliderRange);
        multiRenderer.registerRenderer(FloatRangeParameter.class, myFloatSliderRange);
        multiRenderer.registerRenderer(TextParameter.class, textBox);

        multiRenderer.registerEditor(FloatRangeParameter.class, myFloatSliderRange2);
        multiRenderer.registerEditor(IntRangeParameter.class, myIntSliderRange2);
        multiRenderer.registerEditor(TextParameter.class, textBox2);

        return multiRenderer;
    }

    public JXTablePerColumnFiltering(TableModel model) {
        super(model);
        this.model = (BeanTableModel<?>) model;

        // Create the 1-row filtering Table

        nColumns = model.getColumnCount();

        // DependencyEngine e = new DependencyEngine(new BasicEngineProvider());

        for (int row = 0; row < model.getRowCount(); row++) {
            for (int i = 0; i < nColumns; i++) {
                if (getColumnName(i).contains("userCell")) {
                    // this is a column with cells that can contain formulas in addition to computed values
                } else {

                }
            }
        }

        filteringModel = new DefaultTableModel(1, nColumns);

        initializeFilterModel();

        filteringTable = new JXTableBetterFocus(filteringModel);
        filteringTable.setTableHeader(null);

        for (int i = 0; i < nColumns; i++) {
            TableColumn fColumn = filteringTable.getColumn(i);
            MultiRenderer multiRenderer = getMultiRenderer();

            fColumn.setCellRenderer(multiRenderer);
            fColumn.setCellEditor(multiRenderer);
            fColumn.setWidth(getColumn(i).getWidth());
        }

        this.setRowFilter(filter);

        JTableHeader header = this.getTableHeader();
        if (tips == null) {
            tips = new ColumnHeaderToolTips();
        }
        header.addMouseMotionListener(tips);
    }

    @Override
    //Overridden to work around problem with editor returning null value and
    //to allow parsing to right object type
    public void editingStopped(ChangeEvent e) {
        // Take in the new value
        int editingRow = this.editingRow;
        int editingColumn = this.editingColumn;
        TableCellEditor editor = getCellEditor();
        if (editor != null) {
            if (editingRow > -1 && editingColumn > -1) {
                Object value = editor.getCellEditorValue();
                if (getValueAt(editingRow, editingColumn) instanceof Float && value != null) {
                    try {
                        value = Float.parseFloat((String) value);
                        setValueAt(value, editingRow, editingColumn);
                    } catch (NumberFormatException ex) {
                        Utils.log("Could not parse " + value + " to float", LogLevel.WARNING);
                    }
                } else if (getValueAt(editingRow, editingColumn) instanceof Double && value != null) {
                    try {
                        value = Double.parseDouble((String) value);
                        setValueAt(value, editingRow, editingColumn);
                    } catch (NumberFormatException ex) {
                        Utils.log("Could not parse " + value + " to float", LogLevel.WARNING);
                    }
                } else if (getValueAt(editingRow, editingColumn) instanceof Integer && value != null) {
                    try {
                        value = Integer.parseInt((String) value);
                        setValueAt(value, editingRow, editingColumn);
                    } catch (NumberFormatException ex) {
                        Utils.log("Could not parse " + value + " to int", LogLevel.WARNING);
                    }
                } else if (value != null) {
                    setValueAt(value, editingRow, editingColumn);
                }
                //It might make more sense for update event to be fired by setValueAt,
                //but BeanTableModel does not currently do that
                model.fireTableCellUpdated(editingRow, editingColumn);
            }
            removeEditor();
        }
    }

    @Override
    //Overridden to prevent cell editing from starting just because the
    //user pressed the command key (which is necessary for discontiguous
    //cell selection)
    public boolean editCellAt(int row, int column, EventObject e) {
        if (e instanceof KeyEvent) {
            KeyEvent ke = (KeyEvent) e;
            if ((ke.getKeyCode() == 0 || ke.getKeyCode() == 157)
                    && (ke.getModifiers() == 260 || ke.getModifiers() == 4)) {
                return false;
            }
        }
        return super.editCellAt(row, column, e);
    }

    @Override
    //Adapted from http://stackoverflow.com/questions/27102546/show-tooltips-in-jtable-only-when-column-is-cut-off
    public String getToolTipText(MouseEvent e) {
        Point p = e.getPoint();
        int col = columnAtPoint(p);
        int row = rowAtPoint(p);
        if (row == -1 || col == -1) {
            return null;
        }
        Rectangle bounds = getCellRect(row, col, false);
        Object value = getValueAt(row, col);
        Component comp = prepareRenderer(getCellRenderer(row, col), row, col);
        if (comp.getPreferredSize().width > bounds.width) {
            return (value.toString());
        } else {
            return null;
        }
    }

}