/* * Copyright (C) 2014 by Array Systems Computing Inc. http://www.array.ca * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package org.esa.snap.productlibrary.rcp.toolviews.model; import org.apache.commons.math3.util.FastMath; import org.esa.snap.core.util.Guardian; import org.esa.snap.engine_utilities.db.ProductEntry; import org.esa.snap.productlibrary.rcp.toolviews.model.dataprovider.DataProvider; import javax.swing.Icon; import javax.swing.JLabel; import javax.swing.JTable; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import java.awt.Color; import java.awt.Component; import java.awt.Graphics; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; public class SortingDecorator extends AbstractTableModel { private static final int DESCENDING = -1; private static final int NOT_SORTED = 0; private static final int ASCENDING = 1; private static final Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED); private final ProductEntryTableModel tableModel; private final JTableHeader _tableHeader; private final List<Directive> sortingColumns = new ArrayList<>(); private Row[] viewToModel = null; private boolean doSortBy = false; public enum SORT_BY {NAME, DATE, MISSON, TYPE, FILESIZE} private SORT_BY sortBy; private int sortedByDirection = NOT_SORTED; public SortingDecorator(final ProductEntryTableModel tableModel, final JTableHeader tableHeader) { Guardian.assertNotNull("tableModel", tableModel); Guardian.assertNotNull("tableHeader", tableHeader); this.tableModel = tableModel; this.tableModel.addTableModelListener(new TableModelListener() { public void tableChanged(final TableModelEvent e) { initViewToModel(); fireTableChanged(e); } }); _tableHeader = tableHeader; _tableHeader.addMouseListener(new MouseHandler()); _tableHeader.setDefaultRenderer(new SortableHeaderRenderer(_tableHeader.getDefaultRenderer())); } public int getRowCount() { return tableModel.getRowCount(); } public int getColumnCount() { return tableModel.getColumnCount(); } public Object getValueAt(final int rowIndex, final int columnIndex) { return tableModel.getValueAt(getSortedIndex(rowIndex), columnIndex); } @Override public String getColumnName(final int column) { return tableModel.getColumnName(column); } @Override public Class getColumnClass(final int column) { return tableModel.getColumnClass(column); } @Override public boolean isCellEditable(final int row, final int column) { return tableModel.isCellEditable(getSortedIndex(row), column); } @Override public void setValueAt(final Object aValue, final int row, final int column) { tableModel.setValueAt(aValue, getSortedIndex(row), column); } public int getSortedIndex(final int rowIndex) { final Row[] view = getViewToModel(); if (rowIndex < view.length) { return view[rowIndex].modelIndex; } else if (view.length > 0) { return view[0].modelIndex; } return 0; } private Row[] getViewToModel() { if (viewToModel == null) { initViewToModel(); } return viewToModel; } private void initViewToModel() { final int tableModelRowCount = tableModel.getRowCount(); viewToModel = new Row[tableModelRowCount]; for (int row = 0; row < tableModelRowCount; row++) { viewToModel[row] = new Row(row); } if (isSorting()) { Arrays.sort(getViewToModel()); } } boolean isSorting() { return !sortingColumns.isEmpty() || doSortBy; } public void sortBy(final SORT_BY val) { doSortBy = true; sortedByDirection = sortBy == val ? DESCENDING : ASCENDING; sortBy = val; initViewToModel(); fireTableDataChanged(); } private class MouseHandler extends MouseAdapter { @Override public void mouseClicked(final MouseEvent e) { final JTableHeader h = (JTableHeader) e.getSource(); final TableColumnModel columnModel = h.getColumnModel(); final int viewColumnIndex = columnModel.getColumnIndexAtX(e.getX()); final int columnIndex = columnModel.getColumn(viewColumnIndex).getModelIndex(); if (columnIndex != -1) { int direction = getSortingDirection(columnIndex); if (!e.isControlDown()) { clearSortingDirections(); _tableHeader.repaint(); } // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed. direction += (e.isShiftDown() ? -1 : 1); direction = (direction + 4) % 3 - 1; // signed mod, returning {-1, 0, 1} setDirectionForColumn(columnIndex, direction); doSortBy = false; initViewToModel(); fireTableDataChanged(); } } } private void setDirectionForColumn(final int column, final int direction) { final Directive directive = getDirective(column); if (directive != EMPTY_DIRECTIVE) { sortingColumns.remove(directive); } if (direction != NOT_SORTED) { sortingColumns.add(new Directive(column, direction)); } } private class SortableHeaderRenderer implements TableCellRenderer { private final TableCellRenderer tableCellRenderer; public SortableHeaderRenderer(final TableCellRenderer tableCellRenderer) { this.tableCellRenderer = tableCellRenderer; } public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) { final Component c = tableCellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (c instanceof JLabel) { final JLabel l = (JLabel) c; l.setHorizontalTextPosition(JLabel.LEFT); final int modelColumn = table.convertColumnIndexToModel(column); l.setIcon(getHeaderRendererIcon(modelColumn, (int) (l.getFont().getSize() * 1.6))); } return c; } } private Icon getHeaderRendererIcon(final int column, final int size) { final Directive directive = getDirective(column); if (directive == EMPTY_DIRECTIVE) { return null; } return new Arrow(directive.direction == DESCENDING, size, sortingColumns.indexOf(directive)); } private int getSortingDirection(final int column) { return getDirective(column).direction; } private void clearSortingDirections() { sortingColumns.clear(); } private Directive getDirective(final int column) { for (Object sortingColumn : sortingColumns) { final Directive directive = (Directive) sortingColumn; if (directive.column == column) { return directive; } } return EMPTY_DIRECTIVE; } private static class Directive { private final int column; private final int direction; public Directive(final int column, final int direction) { this.column = column; this.direction = direction; } } private static class Arrow implements Icon { private final boolean descending; private final int size; private final int priority; public Arrow(final boolean descending, final int size, final int priority) { this.descending = descending; this.size = size; this.priority = priority; } public void paintIcon(final Component c, final Graphics g, final int x, int y) { final Color color = c == null ? Color.GRAY : c.getBackground(); // In a compound sort, make each successive triangle 20% // smaller than the previous one. final int dx = (int) (size / 2 * FastMath.pow(0.8, priority)); final int dy = descending ? dx : -dx; // Align icon (roughly) with font baseline. y = y + 5 * size / 6 + (descending ? -dy : 0); final int shift = descending ? 1 : -1; g.translate(x, y); // Right diagonal. g.setColor(color.darker()); g.drawLine(dx / 2, dy, 0, 0); g.drawLine(dx / 2, dy + shift, 0, shift); // Left diagonal. g.setColor(color.brighter()); g.drawLine(dx / 2, dy, dx, 0); g.drawLine(dx / 2, dy + shift, dx, shift); // Horizontal line. if (descending) { g.setColor(color.darker().darker()); } else { g.setColor(color.brighter().brighter()); } g.drawLine(dx, 0, 0, 0); g.setColor(color); g.translate(-x, -y); } public int getIconWidth() { return size; } public int getIconHeight() { return size; } } private class Row implements Comparable { private final int modelIndex; public Row(final int index) { modelIndex = index; } public int compareTo(final Object o) { final int idxRow1 = modelIndex; final int idxRow2 = ((Row) o).modelIndex; if (doSortBy) { final Object o1 = tableModel.getValueAt(idxRow1, 0); final Object o2 = tableModel.getValueAt(idxRow2, 0); final int comparison = SORTEDBY_COMPARATOR.compare(o1, o2); if (comparison != 0) { return sortedByDirection == DESCENDING ? -comparison : comparison; } } for (Object sortingColumn : sortingColumns) { final Directive directive = (Directive) sortingColumn; final int column = directive.column; final Object o1 = tableModel.getValueAt(idxRow1, column); final Object o2 = tableModel.getValueAt(idxRow2, column); final int comparison = getComparator(column).compare(o1, o2); if (comparison != 0) { return directive.direction == DESCENDING ? -comparison : comparison; } } return 0; } } private static final Comparator COMPARABLE_COMAPRATOR = new Comparator() { public int compare(final Object o1, final Object o2) { if (o1 == null && o2 == null) { return 0; } else if (o1 == null) { return -1; } else if (o2 == null) { return 1; } else { return ((Comparable) o1).compareTo(o2); } } }; private static final Comparator LEXICAL_COMPARATOR = new Comparator() { public int compare(final Object o1, final Object o2) { if (o1 == null && o2 == null) { return 0; } else if (o1 == null) { return -1; } else if (o2 == null) { return 1; } else { return o1.toString().compareTo(o2.toString()); } } }; private final Comparator SORTEDBY_COMPARATOR = new Comparator() { public int compare(final Object o1, final Object o2) { if (o1 == null && o2 == null) { return 0; } else if (o1 == null) { return -1; } else if (o2 == null) { return 1; } else if (o1 instanceof ProductEntry && o2 instanceof ProductEntry) { final ProductEntry e1 = (ProductEntry) o1; final ProductEntry e2 = (ProductEntry) o2; if (sortBy.equals(SORT_BY.NAME)) { return e1.getName().compareTo(e2.getName()); } else if (sortBy.equals(SORT_BY.TYPE)) { return e1.getProductType().compareTo(e2.getProductType()); } else if (sortBy.equals(SORT_BY.MISSON)) { return e1.getMission().compareTo(e2.getMission()); } else if (sortBy.equals(SORT_BY.DATE)) { return e1.getFirstLineTime().getAsDate().compareTo(e2.getFirstLineTime().getAsDate()); } else if (sortBy.equals(SORT_BY.FILESIZE)) { final Long size1 = e1.getFileSize(); final Long size2 = e2.getFileSize(); return size1.compareTo(size2); } } return o1.toString().compareTo(o2.toString()); } }; private Comparator getComparator(final int column) { final DataProvider dataProvider = tableModel.getDataProvider(column); final Class columnType = tableModel.getColumnClass(column); Comparator comparator = dataProvider.getComparator(); if (comparator == null) { if (Comparable.class.isAssignableFrom(columnType)) { comparator = COMPARABLE_COMAPRATOR; } else { comparator = LEXICAL_COMPARATOR; } } return comparator; } }