/* * DataJTable.java Copyright (C) 2020. Daniel H. Huson * * (Some files contain contributions from other authors, who are then mentioned separately.) * * 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 megan.timeseriesviewer; import jloda.swing.director.IDirector; import jloda.swing.util.PopupMenu; import megan.chart.ChartColorManager; import megan.core.Director; import megan.core.Document; import megan.core.SampleAttributeTable; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.*; /** * the table allowing arrangement of series and time points */ public class DataJTable { private final TimeSeriesViewer viewer; private final Director dir; private final Document doc; private final ChartColorManager chartColorManager; private final JTable jTable; private final MyCellRenderer renderer; private final MyTableModel tableModel; private final Set<String> disabledSamples = new HashSet<>(); private final Set<String> selectedSamples = new HashSet<>(); private final static Color selectionBackgroundColor = new Color(255, 240, 240); private final static Color backgroundColor = new Color(230, 230, 230); private int columnPressed = -1; /** * Constructor * * @param viewer */ public DataJTable(TimeSeriesViewer viewer) { this.viewer = viewer; this.dir = viewer.getDirector(); this.doc = dir.getDocument(); this.chartColorManager = doc.getChartColorManager(); jTable = new JTable(); jTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); renderer = new MyCellRenderer(); jTable.setColumnModel(new DefaultTableColumnModel() { public void addColumn(TableColumn tc) { tc.setMinWidth(150); super.addColumn(tc); } }); jTable.addMouseListener(new MyMouseListener()); jTable.getTableHeader().addMouseListener(new MyMouseListener()); tableModel = new MyTableModel(); setDefault(); jTable.setModel(tableModel); final MySelectionListener mySelectionListener = new MySelectionListener(); jTable.getSelectionModel().addListSelectionListener(mySelectionListener); jTable.getColumnModel().getSelectionModel().addListSelectionListener(mySelectionListener); } public void setDefault() { int numberOfSamples = doc.getNumberOfSamples(); int rows = (int) Math.sqrt(numberOfSamples); int cols = 0; while (rows * cols < numberOfSamples) cols++; setRowsAndCols(rows, cols); String[] sampleNames = doc.getSampleNamesAsArray(); int count = 0; for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { if (row == 0) setColumnName(col, "Time " + col); if (count < numberOfSamples) putCell(row, col, sampleNames[count++]); else putCell(row, col, null); } } updateView(); } public void setRowsAndCols(int rows, int cols) { tableModel.resize(rows, cols); } public void putCell(int row, int col, String sample) { tableModel.put(row, col, sample); } public void setColumnName(int col, String name) { tableModel.setColumnName(col, name); } public void updateView() { jTable.createDefaultColumnsFromModel(); for (int i = 0; i < jTable.getColumnCount(); i++) { TableColumn col = jTable.getColumnModel().getColumn(i); col.setCellRenderer(renderer); } jTable.revalidate(); } /** * get the table * * @return table */ public JTable getJTable() { return jTable; } private Font getFont() { return jTable.getFont(); } public void selectRow(int row, boolean select) { if (row >= 0) { for (int col = 0; col < tableModel.getColumnCount(); col++) { final DataNode dataNode = (DataNode) tableModel.getValueAt(row, col); if (dataNode != null) { dataNode.setSelected(select); } } } } private void selectColumn(int col, boolean select) { if (col >= 0) { for (int row = 0; row < tableModel.getRowCount(); row++) { final DataNode dataNode = (DataNode) tableModel.getValueAt(row, col); if (dataNode != null) { dataNode.setSelected(select); } } } } public void select(String name, boolean select) { for (int row = 0; row < getJTable().getRowCount(); row++) { for (int col = 0; col < getJTable().getColumnCount(); col++) { final DataNode dataNode = (DataNode) tableModel.getValueAt(row, col); if (dataNode != null && dataNode.getName() != null && dataNode.getName().endsWith(name)) { dataNode.setSelected(select); } } } } public void clearSelection() { jTable.clearSelection(); for (int row = 0; row < getJTable().getRowCount(); row++) { for (int col = 0; col < getJTable().getColumnCount(); col++) { ((DataNode) tableModel.getValueAt(row, col)).setSelected(false); } } } public void selectAll() { jTable.selectAll(); for (int row = 0; row < getJTable().getRowCount(); row++) { for (int col = 0; col < getJTable().getColumnCount(); col++) { ((DataNode) tableModel.getValueAt(row, col)).setSelected(true); } } } public int getColumnPressed() { return columnPressed; } public void setColumnPressed(int columnPressed) { this.columnPressed = columnPressed; } private boolean isEnabled(int modelRow) { Object sampleName = jTable.getModel().getValueAt(modelRow, 0); return sampleName != null && !disabledSamples.contains(sampleName.toString()); } public String getSampleName(int modelRow) { return jTable.getModel().getValueAt(modelRow, 0).toString(); } public void setDisabledSamples(Collection<String> disabledSamples) { this.disabledSamples.clear(); this.disabledSamples.addAll(disabledSamples); } public Set<String> getDisabledSamples() { return disabledSamples; } public void selectSamples(Collection<String> names, boolean select) { for (int row = 0; row < tableModel.getRowCount(); row++) { for (int col = 0; col < tableModel.getColumnCount(); col++) { final DataNode dataNode = (DataNode) tableModel.getValueAt(row, col); if (dataNode != null && dataNode.getName() != null && names.contains(dataNode.getName())) { dataNode.setSelected(select); } } } } public Collection<String> getSelectedSamples() { ArrayList<String> selectedSamples = new ArrayList<>(tableModel.getRowCount() * tableModel.getColumnCount()); int[] columnsInView = getColumnsInView(); for (int row = 0; row < tableModel.getRowCount(); row++) { for (int col : columnsInView) { final DataNode dataNode = (DataNode) tableModel.getValueAt(row, col); if (dataNode != null && dataNode.isSelected()) { selectedSamples.add(dataNode.getName()); } } } return selectedSamples; } private int[] getColumnsInView() { int[] result = new int[jTable.getColumnCount()]; // Use an enumeration Enumeration<TableColumn> e = jTable.getColumnModel().getColumns(); for (int i = 0; e.hasMoreElements(); i++) { result[i] = e.nextElement().getModelIndex(); } return result; } /** * table model */ class MyTableModel extends AbstractTableModel { private DataNode[][] data; private String[] columnNames; MyTableModel() { } void resize(int rows, int cols) { data = new DataNode[rows][cols]; columnNames = new String[cols]; } void put(int row, int col, String name) { data[row][col] = new DataNode(name); } public int getColumnCount() { return columnNames.length; } public int getRowCount() { return data.length; } public String getColumnName(int col) { return columnNames[col]; } void setColumnName(int col, String name) { columnNames[col] = name; } public Object getValueAt(int row, int col) { if (row < data.length && data[row] != null && col < data[row].length) return data[row][col]; else return null; } public Class getColumnClass(int c) { return getValueAt(0, c).getClass(); } /* * Don't need to implement this method unless your table's * editable. */ public boolean isCellEditable(int row, int col) { //Note that the data/cell address is constant, //no matter where the cell appears onscreen. return col >= 2; } /* * Don't need to implement this method unless your table's * data can change. */ public void setValueAt(Object value, int row, int col) { data[row][col] = (DataNode) value; fireTableCellUpdated(row, col); } } static class DataNode { private final String name; private boolean selected; DataNode(String name) { this.name = name; } boolean isSelected() { return selected; } void setSelected(boolean selected) { this.selected = selected; } public String toString() { return name; } String getName() { return name; } } class MyCellRenderer implements TableCellRenderer { private JLabel label; private Swatch swatch; private JPanel both; public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { if (both == null) { both = new JPanel(); both.setLayout(new BoxLayout(both, BoxLayout.X_AXIS)); swatch = new Swatch(); swatch.setMaximumSize(new Dimension(getFont().getSize(), getFont().getSize())); swatch.setBorder(BorderFactory.createLineBorder(Color.WHITE)); label = new JLabel(); both.add(swatch); both.add(Box.createHorizontalStrut(4)); both.add(label); label.setPreferredSize(new Dimension(label.getPreferredSize().width + 100, label.getPreferredSize().height)); label.setMinimumSize(label.getPreferredSize()); } both.setEnabled(isEnabled(row)); label.setForeground(both.isEnabled() ? Color.BLACK : Color.GRAY); if (value != null) { final DataNode dataNode = (DataNode) value; label.setText(doc.getSampleLabelGetter().getLabel(dataNode.getName())); if (dataNode.isSelected()) both.setBorder(BorderFactory.createLineBorder(Color.RED)); else both.setBorder(null); Color color = chartColorManager.getSampleColor(dataNode.getName()); if (dataNode.getName() != null && dataNode.isSelected()) { both.setBackground(selectionBackgroundColor); } else both.setBackground(backgroundColor); swatch.setBackground(isEnabled(row) ? color : Color.LIGHT_GRAY); if (dataNode.getName() != null) swatch.setShape((String) dir.getDocument().getSampleAttributeTable().get(dataNode.getName(), SampleAttributeTable.HiddenAttribute.Shape.toString())); else swatch.setShape("None"); final String description = (String) dir.getDocument().getSampleAttributeTable().get(dataNode.getName(), SampleAttributeTable.DescriptionAttribute); if (description != null && description.length() > 0) both.setToolTipText(description); else both.setToolTipText(dataNode.getName()); } return both; } } private static class Swatch extends JPanel { private String shape; void setShape(String shape) { this.shape = shape; } public void paint(Graphics gc0) { Graphics2D gc = (Graphics2D) gc0; if (shape != null && shape.equalsIgnoreCase("Square")) { gc.setColor(getBackground()); gc.fillRect(1, 1, getWidth() - 2, getHeight() - 2); gc.setColor(Color.BLACK); gc.drawRect(1, 1, getWidth() - 2, getHeight() - 2); } else if (shape != null && shape.equalsIgnoreCase("Triangle")) { Shape polygon = new Polygon(new int[]{1, getWidth() - 1, getWidth() / 2}, new int[]{getHeight() - 1, getHeight() - 1, 1}, 3); gc.setColor(getBackground()); gc.fill(polygon); gc.setColor(Color.BLACK); gc.draw(polygon); } else if (shape != null && shape.equalsIgnoreCase("Diamond")) { Shape polygon = new Polygon(new int[]{1, (int) Math.round(getWidth() / 2.0), getWidth() - 1, (int) Math.round(getWidth() / 2.0)}, new int[]{(int) Math.round(getHeight() / 2.0), getHeight() - 1, (int) Math.round(getHeight() / 2.0), 1}, 4); gc.setColor(getBackground()); gc.fill(polygon); gc.setColor(Color.BLACK); gc.draw(polygon); } else if (shape == null || !shape.equalsIgnoreCase("None")) { // circle gc.setColor(getBackground()); gc.fillOval(1, 1, getWidth() - 2, getHeight() - 2); gc.setColor(Color.BLACK); gc.drawOval(1, 1, getWidth() - 2, getHeight() - 2); } } } class MyMouseListener extends MouseAdapter { public void mouseClicked(MouseEvent e) { jTable.repaint(); } public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { showPopupMenu(e); } else { if (e.getSource() instanceof JTableHeader) { columnPressed = jTable.getTableHeader().columnAtPoint(e.getPoint()); if (columnPressed >= 0) { if (!e.isShiftDown() && !e.isMetaDown()) clearSelection(); selectColumn(columnPressed, true); } } } } public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { showPopupMenu(e); } jTable.repaint(); } } private void showPopupMenu(final MouseEvent e) { // show the header popup menu: if (e.getSource() instanceof JTableHeader) { columnPressed = jTable.getTableHeader().columnAtPoint(e.getPoint()); (new PopupMenu(this, GUIConfiguration.getDataColumnHeaderPopupConfiguration(), dir.getCommandManager())).show(e.getComponent(), e.getX(), e.getY()); } else if (e.getSource() instanceof JTable && e.getSource() == jTable) { JPopupMenu popupMenu = (new PopupMenu(this, GUIConfiguration.getDataPopupConfiguration(), dir.getCommandManager())); popupMenu.addSeparator(); popupMenu.add(new AbstractAction("Select Row") { public void actionPerformed(ActionEvent ae) { clearSelection(); selectRow(jTable.rowAtPoint(e.getPoint()), true); } }); popupMenu.add(new AbstractAction("Select Column") { public void actionPerformed(ActionEvent ae) { clearSelection(); selectColumn(jTable.columnAtPoint(e.getPoint()), true); } }); popupMenu.show(e.getComponent(), e.getX(), e.getY()); } } class MySelectionListener implements ListSelectionListener { public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { // The mouse button has not yet been released } else { Set<String> toSelect = new HashSet<>(); for (int row = 0; row < jTable.getRowCount(); row++) { for (int col = 0; col < jTable.getColumnCount(); col++) { Object sample = jTable.getValueAt(row, col); if (sample != null) { boolean selectedInDocument = doc.getSampleSelection().isSelected(sample.toString()); boolean selectedOnGrid = jTable.isRowSelected(row) && jTable.isColumnSelected(col); if (selectedOnGrid || (selectedInDocument && (jTable.isRowSelected(row) || jTable.isColumnSelected(col)))) toSelect.add(sample.toString()); } } } doc.getSampleSelection().clear(); doc.getSampleSelection().setSelected(toSelect, true); viewer.updateView(IDirector.ENABLE_STATE); } } } }