/*
* Copyright (c) 2008, 2009, 2010 Denis Tulskiy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* version 3 along with this work. If not, see <http://www.gnu.org/licenses/>.
*/
package com.tulskiy.musique.gui.components;
import com.tulskiy.musique.util.Util;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.Rectangle2D;
/**
* @Author: Denis Tulskiy
* @Date: Sep 30, 2009
*/
public class GroupTable extends JTable {
private static final double FACTOR = 0.90;
private Color bgColor1;
private Color bgColor2;
private Color selectBgColor1;
private Color selectBgColor2;
private Font separatorFont;
private Color separatorColor;
private final Font defaultFont = getFont();
private boolean trackSelection = true;
public GroupTable() {
setDefaultRenderer(Object.class, new DefaultCellRenderer());
setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
buildListeners();
}
protected boolean isSeparator(int row) {
return getRowCount() > 0 && getColumnCount() > 0 && getModel().getValueAt(row, 0) instanceof Separator;
}
public void setTrackSelection(boolean trackSelection) {
this.trackSelection = trackSelection;
}
private void buildListeners() {
getSelectionModel().addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
// workaround for search dialog
if (trackSelection && getSelectedRowCount() == 1) {
if (isSeparator(getSelectedRow())) {
Separator value = (Separator) getModel().getValueAt(getSelectedRow(), 0);
int size = value.getGroupSize();
int row = getSelectedRow() + 1;
while (size > 0 && row < getModel().getRowCount() && !isSeparator(row)) {
row++;
size--;
}
getSelectionModel().setSelectionInterval(getSelectedRow(), row - 1);
}
}
}
});
InputMap imap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
ActionMap amap = getActionMap();
imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "goUp");
amap.put("goUp", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
int selectedRow = getSelectionModel().getAnchorSelectionIndex();
selectedRow = Math.max(0, selectedRow - 1);
changeSelection(selectedRow, 0, false, false);
}
});
imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "goDown");
amap.put("goDown", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
int selectedRow = getSelectionModel().getAnchorSelectionIndex();
int count = getModel().getRowCount();
if (getRowSorter() != null) {
count = getRowSorter().getViewRowCount();
}
selectedRow = Math.min(selectedRow + 1, count - 1);
changeSelection(selectedRow, 0, false, false);
}
});
imap.put(KeyStroke.getKeyStroke("HOME"), "selectFirstRow");
imap.put(KeyStroke.getKeyStroke("END"), "selectLastRow");
}
public void runAction(Object actionKey) {
Action action = getActionMap().get(actionKey);
if (action != null)
action.actionPerformed(new ActionEvent(this, 0, actionKey.toString()));
}
public void addKeyboardAction(KeyStroke keyStroke, String name, Action action) {
InputMap imap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
ActionMap amap = getActionMap();
imap.put(keyStroke, name);
amap.put(name, action);
}
private void initUI() {
setUI(new GroupTableUI());
setBackground(Color.white);
setIntercellSpacing(new Dimension(0, 0));
setFocusable(true);
setDragEnabled(true);
setFocusTraversalKeysEnabled(false);
setFillsViewportHeight(true);
setRowSelectionAllowed(true);
setShowGrid(false);
setOpaque(true);
setFont(getFont());
setForeground(getForeground());
setSelectionBackground(getSelectionBackground());
setSeparatorColor(getForeground());
//fix dropLine colors, I hate Nimbus
Color droplineColor = UIManager.getColor("Table.dropLineShortColor");
UIManager.getDefaults().put("Table.dropLineColor", droplineColor);
UIDefaults defaults = new UIDefaults();
defaults.put("Table.dropLineColor", droplineColor);
putClientProperty("Nimbus.Overrides", defaults);
putClientProperty("Nimbus.Overrides.InheritDefaults", false);
}
@Override
public void updateUI() {
setForeground(null);
setBackground(null);
setFont(null);
try {
setSelectionBackground(null);
setSelectionForeground(null);
} catch (Exception e) {
//NPE will happen here
//but it's ok
}
super.updateUI();
initUI();
}
@Override
public void setFont(Font font) {
if (font == null) {
super.setFont(font);
} else {
Font newFont;
if (defaultFont != null)
newFont = defaultFont.deriveFont(font.getAttributes());
else
newFont = font;
super.setFont(newFont);
separatorFont = newFont.deriveFont(Font.BOLD, newFont.getSize() + 2f);
setRowHeight(newFont.getSize() + 10);
}
}
@Override
public void setSelectionBackground(Color selectionBackground) {
super.setSelectionBackground(selectionBackground);
if (selectionBackground != null) {
setSelectionForeground(Util.getContrastColor(selectionBackground));
selectBgColor1 = new Color(selectionBackground.getRGB());
selectBgColor2 = darker(selectionBackground);
}
}
public void setSeparatorColor(Color color) {
this.separatorColor = color;
}
public void setBackground(Color color) {
super.setBackground(color);
if (color != null) {
bgColor1 = color;
bgColor2 = darker(color);
}
}
private Color darker(Color c) {
return new Color(Math.max((int) (c.getRed() * FACTOR), 0),
Math.max((int) (c.getGreen() * FACTOR), 0),
Math.max((int) (c.getBlue() * FACTOR), 0));
}
public void scrollToRow(int currentItem) {
Rectangle r = getCellRect(currentItem, 0, true);
if (!isVerticallyVisible(r)) center(r);
}
private boolean isVerticallyVisible(Rectangle r) {
Rectangle visible = getVisibleRect();
return visible.y <= r.y
&& visible.y + visible.height >= r.y + r.height;
}
private void center(Rectangle r) {
Rectangle visible = getVisibleRect();
if (visible.isEmpty()) {
scrollRectToVisible(r);
return;
}
visible.x = r.x - (visible.width - r.width) / 2;
visible.y = r.y - (visible.height - r.height) / 2;
Rectangle bounds = getBounds();
Insets i = getInsets();
bounds.x = i.left;
bounds.y = i.top;
bounds.width -= i.left + i.right;
bounds.height -= i.top + i.bottom;
if (visible.x < bounds.x)
visible.x = bounds.x;
if (visible.x + visible.width > bounds.x + bounds.width)
visible.x = bounds.x + bounds.width - visible.width;
if (visible.y < bounds.y)
visible.y = bounds.y;
if (visible.y + visible.height > bounds.y + bounds.height)
visible.y = bounds.y + bounds.height - visible.height;
scrollRectToVisible(visible);
}
public Rectangle getCellRect(int row, int column, boolean includeSpacing) {
final TableModel model = getModel();
// sometimes JTable asks for a cellrect that doesn't exist anymore, due
// to an editor being installed before a bunch of rows were removed.
// In this case, just return an empty rectangle, since it's going to
// be discarded anyway
if (row >= model.getRowCount() || model.getColumnCount() <= 0) {
return new Rectangle();
}
// if it's the separator row, return the entire row as one big rectangle
Object rowValue = model.getValueAt(row, 0);
if (rowValue instanceof Separator) {
Rectangle firstColumn = super.getCellRect(row, 0, includeSpacing);
Rectangle lastColumn = super.getCellRect(row, getColumnCount() - 1, includeSpacing);
return firstColumn.union(lastColumn);
// otherwise it's business as usual
} else {
return super.getCellRect(row, column, includeSpacing);
}
}
class DefaultCellRenderer extends DefaultTableCellRenderer {
private SeparatorCellRenderer separatorCellRenderer = new SeparatorCellRenderer();
@Override
public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus, int row,
int column) {
Component c;
if (value instanceof Separator) {
c = separatorCellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus,
row, column);
} else {
c = super.getTableCellRendererComponent(table, value, isSelected, false,
row, column);
}
if (!isSelected) {
c.setBackground(row % 2 == 0 ? bgColor1 : bgColor2);
} else if (!(value instanceof Separator)) {
if (table.getSelectedRowCount() > 1)
c.setBackground(row % 2 == 0 ? selectBgColor1 : selectBgColor2);
else
c.setBackground(selectBgColor1);
}
return c;
}
@Override
protected void setValue(Object value) {
if (value instanceof ImageIcon) {
setIcon((Icon) value);
setText(null);
} else {
super.setValue(value);
setIcon(null);
}
if (value instanceof String)
setHorizontalAlignment(LEFT);
else
setHorizontalAlignment(CENTER);
}
}
class SeparatorCellRenderer extends JPanel implements TableCellRenderer {
private String value;
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
if (isSelected) {
setBorder(BorderFactory.createLineBorder(selectBgColor1, 1));
} else {
setBorder(BorderFactory.createEmptyBorder());
}
setFont(separatorFont);
setForeground(separatorColor);
this.value = ((Separator) value).getGroupName();
return this;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
FontMetrics fm = g2d.getFontMetrics(separatorFont);
Rectangle2D bounds = fm.getStringBounds(value, g);
g2d.drawLine((int) (bounds.getWidth() + 20), getHeight() / 2, getWidth(), getHeight() / 2);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, // Anti-alias!
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawString(value, 5, getHeight() - (getHeight() - fm.getAscent()) / 2 - fm.getDescent() + 2);
}
}
}