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

io.github.jonestimd.swing.table.DecoratedTable Maven / Gradle / Ivy

There is a newer version: 1.4.5
Show newest version
// The MIT License (MIT)
//
// Copyright (c) 2019 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;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EventObject;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.RowSorter.SortKey;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.TableUI;
import javax.swing.plaf.UIResource;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import javax.swing.text.JTextComponent;

import io.github.jonestimd.swing.ComponentDefaults;
import io.github.jonestimd.swing.FocusContainer;
import io.github.jonestimd.swing.table.model.BeanTableModel;
import io.github.jonestimd.swing.table.model.ColumnIdentifier;

/**
 * A table that uses {@link TableDecorator}s to prepare cell renderers and provide consistent styling of cell values.
 * Extends {@link JTable} to provide the following:
 * 
    *
  • apply a list of {@link TableDecorator}s to cell renderers
  • *
  • handle multiline column headers
  • *
  • ensure uniform row background color when an alternate row color is defined (fixes boolean cells)
  • *
  • improve keyboard handling for cell editing
  • *
      *
    • start editing on text input
    • *
    • clear cell and start editing on {@code ctrl-BACKSPACE}
    • *
    • use initial text key for item selection in combo box cell editor
    • *
    • stop editing on {@code ENTER} and remain on current cell
    • *
    *
*/ public class DecoratedTable> extends JTable { private final List decorators = new ArrayList<>(); private Color evenBackground; private Color oddBackground; private int headerRows; public DecoratedTable(Model dm) { super(dm); setSurrendersFocusOnKeystroke(true); // putClientProperty("JTable.autoStartsEdit", Boolean.FALSE); // putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); addMouseMotionListener(new MouseMotionListener() { @Override public void mouseDragged(MouseEvent event) { setCursor(event); } @Override public void mouseMoved(MouseEvent event) { setCursor(event); } }); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent event) { handleClick(event); } }); } private void setCursor(MouseEvent event) { int hitColumnIndex = columnAtPoint(event.getPoint()); int hitRowIndex = rowAtPoint(event.getPoint()); if ((hitColumnIndex != -1) && (hitRowIndex != -1)) { setCursor(getModel().getCursor(event, this, convertRowIndexToModel(hitRowIndex), convertColumnIndexToModel(hitColumnIndex))); } } private void handleClick(MouseEvent event) { int hitColumnIndex = columnAtPoint(event.getPoint()); int hitRowIndex = rowAtPoint(event.getPoint()); if ((hitColumnIndex != -1) && (hitRowIndex != -1)) { getModel().handleClick(event, this, convertRowIndexToModel(hitRowIndex), convertColumnIndexToModel(hitColumnIndex)); } } @Override public void setModel(TableModel dataModel) { boolean setSortKeys = getRowSorter() != null && getAutoCreateRowSorter(); List sortKeys = setSortKeys ? getRowSorter().getSortKeys() : Collections.emptyList(); super.setModel(dataModel); if (setSortKeys) getRowSorter().setSortKeys(sortKeys); } /** * Overridden to set background colors. */ @Override public void setUI(TableUI ui) { evenBackground = ComponentDefaults.getColor("Table.alternateRowColor"); oddBackground = ComponentDefaults.getColor("Table.background"); super.setUI(ui); } /** * Set the background color for odd rows. */ @Override public void setBackground(Color color) { super.setBackground(color); oddBackground = color; } /** * Get the background color for even rows. */ public Color getAlternateBackground() { return evenBackground; } /** * Set the background color for even rows. */ public void setAlternateBackground(Color background) { this.evenBackground = background; } /** * Overridden to handle multiline column headers. */ @Override public void addColumn(TableColumn aColumn) { super.addColumn(aColumn); if (getModel() instanceof ColumnIdentifier) { aColumn.setIdentifier(((ColumnIdentifier) getModel()).getColumnIdentifier(aColumn.getModelIndex())); } int columnHeaderRows = aColumn.getHeaderValue().toString().split("\n").length; if (columnHeaderRows > 1) { aColumn.setHeaderValue("
" + aColumn.getHeaderValue().toString().replaceAll("\n", "
") + "
"); } headerRows = Math.max(headerRows, columnHeaderRows); } /** * Overridden to handle multiline column headers. */ @Override public void setTableHeader(JTableHeader tableHeader) { super.setTableHeader(tableHeader); if (headerRows > 1 && tableHeader != null) { JComponent renderer = (JComponent) tableHeader.getDefaultRenderer().getTableCellRendererComponent(this, "x", false, false, 0, 0); Dimension size = renderer.getPreferredSize(); size.height -= renderer.getInsets().top + renderer.getInsets().bottom; size.height *= headerRows; size.height += renderer.getInsets().top + renderer.getInsets().bottom; tableHeader.setPreferredSize(size); } } /** * Get the selected beans. */ public List getSelectedItems() { return getBeanIndexes(getSelectedRows()).mapToObj(getModel()::getBean).collect(Collectors.toList()); } protected IntStream getBeanIndexes(int[] viewIndexes) { return Arrays.stream(viewIndexes).map(this::convertRowIndexToModel); } private Component getEditorField() { Component component = super.getEditorComponent(); return component instanceof FocusContainer ? ((FocusContainer) component).getFocusField() : component; } /** * Overridden to improve keyboard handling for starting/stopping edit. */ @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) { putClientProperty("JTable.autoStartsEdit", isAutoStartEdit(e)); int row = -1; if (getCellEditor() != null) { row = convertRowIndexToModel(getSelectedRow()); } boolean result = super.processKeyBinding(ks, e, condition, pressed); // pass key that started the edit to editor if (condition == WHEN_FOCUSED && getEditorField() instanceof JComboBox) { JComboBox editorComponent = (JComboBox) getEditorField(); if (editorComponent.isEditable()) { JTextComponent textEditor = (JTextComponent) editorComponent.getEditor().getEditorComponent(); setEditorText(textEditor, e.getKeyChar()); } else { editorComponent.selectWithKeyChar(e.getKeyChar()); } } if (getCellEditor() != null) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { int column = getSelectedColumn(); getCellEditor().stopCellEditing(); selectViewRow(row); setColumnSelectionInterval(column, column); } } else if (row >= 0) selectViewRow(row); return result; } private void selectViewRow(int modelRow) { int viewRow = convertRowIndexToView(modelRow); setRowSelectionInterval(viewRow, viewRow); scrollRectToVisible(getCellRect(viewRow, getSelectedColumn(), true)); } /** * Overridden to handle initial keystroke on a {@link JTextComponent} editor. */ @Override public boolean editCellAt(int row, int column, EventObject e) { boolean startEdit = super.editCellAt(row, column, e); if (startEdit && e instanceof KeyEvent && getEditorField() instanceof JTextComponent) { if (((KeyEvent) e).getKeyCode() == KeyEvent.VK_DELETE) { ((JTextComponent) getEditorField()).setCaretPosition(0); } else if (((KeyEvent) e).getKeyCode() != KeyEvent.VK_BACK_SPACE && !(getEditorField() instanceof JFormattedTextField)) { ((JTextComponent) getEditorField()).selectAll(); } } return startEdit; } private boolean isAutoStartEdit(KeyEvent e) { return ! e.isControlDown() && ! e.isAltDown() || isCtrlBackspace(e); } private boolean isCtrlBackspace(KeyEvent e) { return e.getKeyCode() == KeyEvent.VK_BACK_SPACE && e.getModifiers() == KeyEvent.CTRL_MASK; } /** * Handle first keystroke when starting edit. Backspace deletes the last character in the cell. * Otherwise, any printable character replaces the entire text of the cell. */ protected void setEditorText(JTextComponent editor, char ch) { if (ch == KeyEvent.VK_BACK_SPACE) { String text = editor.getText(); if (text.length() > 0) { editor.setText(text.substring(0, text.length()-1)); } } else if (! Character.isISOControl(ch)) { editor.setText(Character.toString(ch)); } } /** * Overridden to set background, foreground and apply {@link TableDecorator}s. */ @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { if (renderer instanceof JComponent) { ((JComponent) renderer).setToolTipText(null); // force DefaultTableCellRenderer to use table background and foreground ((JComponent) renderer).setBackground(getRowBackground(row)); ((JComponent) renderer).setForeground(getForeground()); } JComponent component = (JComponent) super.prepareRenderer(renderer, row, column); int modelRow = convertRowIndexToModel(row); int modelColumn = convertColumnIndexToModel(column); for (TableDecorator decorator : decorators) { decorator.prepareRenderer(this, component, modelRow, modelColumn); } return component; } /** * Used by {@link #prepareRenderer(TableCellRenderer, int, int)} to determine the background color of a row. * @return the background color of the row. */ protected Color getRowBackground(int row) { if (row % 2 == 0) { return evenBackground; } return oddBackground; } @Override @SuppressWarnings("unchecked") public Model getModel() { return (Model) super.getModel(); } public List getDecorators() { return decorators; } public void setDecorators(List decorators) { this.decorators.clear(); this.decorators.addAll(decorators); } /** * Overridden to fix background color of boolean cells. */ @Override protected void createDefaultRenderers() { super.createDefaultRenderers(); setDefaultRenderer(Boolean.class, new BooleanRenderer()); } /** * Copy of JTable.BooleanRenderer that only sets the background when selected. This is the only way * to make the background of boolean cells match the rest of the row when using alternating backgrounds. */ private static class BooleanRenderer extends JCheckBox implements TableCellRenderer, UIResource { private static final Border noFocusBorder = new EmptyBorder(1, 1, 1, 1); public BooleanRenderer() { super(); setHorizontalAlignment(JLabel.CENTER); setBorderPainted(true); } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (isSelected) { setForeground(table.getSelectionForeground()); setBackground(table.getSelectionBackground()); } setSelected(Boolean.TRUE.equals(value)); setBorder(hasFocus ? UIManager.getBorder("Table.focusCellHighlightBorder") : noFocusBorder); return this; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy