/*
Wotonomy: OpenStep design patterns for pure Java applications.
Copyright (C) 2000 Intersect Software Corporation
This library 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 2.1 of the License, or (at your option) any later version.
This library 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 along with this library; if not, see http://www.gnu.org
*/
package net.wotonomy.ui.swing.components;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Enumeration;
import java.util.EventObject;
import java.util.Vector;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
/**
* A cell renderer that displays icons in addition to text, and additionally is
* an editor in case you want to click the icon to trigger some kind of action.
* You probably should override both getStringForContext and getIconForContext
* to achieve your desired results. To receive mouse clicks, set the same
* instance of the renderer as the editor for the same component.
*
*
* One notable addition is that this class is an action event broadcaster.
* ActionEvents are broadcast when the mouse is clicked on the button with an
* action event containing a user-configurable string that defaults to CLICKED.
*
*
*
* The renderer itself can be used as a JComponent if you need something like a
* JLabel that allows you to click on the icon. You will want to call setIcon
* and setText to configure the component since the renderer method would not be
* called. (If you add an instance of the renderer to a container, you cannnot
* use the same instance as an editor in a table, tree, or list.)
*
* @author michael@mpowers.net
* @version $Revision: 904 $ $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006)
* $
*/
public class IconCellRenderer extends JPanel implements TableCellRenderer, TableCellEditor, TreeCellRenderer,
TreeCellEditor, ListCellRenderer, Runnable, ActionListener, MouseListener {
public static final String CLICKED = "CLICKED";
/**
* The panel that is re-used to render everything. This is returned by
* getRendererForContext.
*/
protected JPanel rendererPanel;
protected JLabel rendererLabel;
protected JButton rendererButton;
/**
* The panel that is used to receive mouse clicks. It must be a different
* component from rendererPanel. This is returned by getEditorForContext.
*/
protected JPanel editorPanel;
protected JLabel editorLabel;
protected JButton editorButton;
private Object lastKnownValue;
private JComponent lastKnownComponent;
// do as DefaultTableCellRenderer does
private Border noFocusBorder;
private Border treeFocusBorder;
private Color unselectedForeground;
private Color unselectedBackground;
private Vector actionListeners;
private String actionCommand;
private Vector cellEditorListeners;
private boolean editable;
private boolean clickable;
/**
* Default constructor.
*/
public IconCellRenderer() {
editable = true;
clickable = true;
noFocusBorder = new EmptyBorder(1, 1, 1, 1);
treeFocusBorder = new LineBorder(UIManager.getColor("Tree.selectionBorderColor"));
setActionCommand(CLICKED);
rendererPanel = new JPanel();
rendererPanel.setLayout(new GridBagLayout());
editorPanel = this;
editorPanel.setLayout(new GridBagLayout());
// set up constraints
GridBagConstraints imageConstraints = new GridBagConstraints();
imageConstraints.gridx = 0;
GridBagConstraints labelConstraints = new GridBagConstraints();
labelConstraints.fill = GridBagConstraints.HORIZONTAL;
labelConstraints.gridx = 1;
labelConstraints.weightx = 1.0;
labelConstraints.ipadx = 1;
labelConstraints.insets = new Insets(0, 1, 0, 0); // sweat the pixel
// make the editor panel go away when not in use
// and pass through all mouse events to container
// this is not very useful since editorLabel and editorButton
// get all of the events
editorPanel.addMouseListener(this);
rendererLabel = new JLabel();
rendererLabel.setOpaque(false);
rendererPanel.add(rendererLabel, labelConstraints);
editorLabel = new JLabel();
editorLabel.setText(""); // default state
editorLabel.setOpaque(false);
editorPanel.add(editorLabel, labelConstraints);
unselectedForeground = rendererLabel.getForeground();
unselectedBackground = rendererLabel.getBackground();
rendererButton = new JButton();
rendererButton.setBorder(null);
rendererButton.setBorderPainted(false);
rendererButton.setContentAreaFilled(false);
rendererButton.setFocusPainted(false);
rendererButton.setMargin(new Insets(0, 0, 0, 0));
rendererPanel.add(rendererButton, imageConstraints);
editorButton = new JButton();
editorButton.setEnabled(clickable); // default state
editorButton.setIcon(null); // default state
editorButton.setBorder(null);
editorButton.setBorderPainted(false);
editorButton.setContentAreaFilled(false);
editorButton.setFocusPainted(false);
editorButton.setMargin(new Insets(0, 0, 0, 0));
editorPanel.add(editorButton, imageConstraints);
editorButton.addActionListener(this);
// add these in order to dispatch the MouseEvents
// to the lastKnownComponent, and proper management of
// DnD operations
editorLabel.addMouseListener(this);
editorButton.addMouseListener(this);
}
/**
* Returns the text string currently displayed in the editor component.
*/
public String getText() {
return editorLabel.getText();
}
/**
* Sets the text string displayed in the editor component. Default is an empty
* string.
*/
public void setText(String aString) {
editorLabel.setText(aString);
}
/**
* Returns the icon currently displayed in the editor component.
*/
public Icon getIcon() {
return editorButton.getIcon();
}
/**
* Sets the icon currently displayed in the editor component. Default is null.
*/
public void setIcon(Icon anIcon) {
editorButton.setIcon(anIcon);
if (!isClickable()) {
editorButton.setDisabledIcon(anIcon);
}
}
/**
* Returns whether the editor component's label text is editable.
*/
public boolean isEditable() {
return editable;
}
/**
* Sets whether the editor component's label text is editable. Default is true.
* Editable text is not yet implemented.
*/
public void setEditable(boolean isEditable) {
editable = isEditable;
}
/**
* Returns whether the editor component's icon is clickable.
*/
public boolean isClickable() {
return clickable;
}
/**
* Sets whether the editor component's icon is clickable. Default is true.
*/
public void setClickable(boolean isClickable) {
clickable = isClickable;
editorButton.setEnabled(clickable);
}
/**
* Returns the component from getRendererForContext.
*/
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
boolean cellHasFocus) {
lastKnownComponent = list;
return getRendererForContext(list, value, index, 0, isSelected, cellHasFocus, false, true);
}
/**
* Returns the component from getRendererForContext.
*/
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
lastKnownComponent = table;
return getRendererForContext(table, value, row, column, isSelected, hasFocus, false, true);
}
/**
* Returns the component from getRendererForContext.
*/
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded,
boolean leaf, int row, boolean hasFocus) {
lastKnownComponent = tree;
return getRendererForContext(tree, value, row, 0, selected, hasFocus, expanded, leaf);
}
/**
* Returns getEditorForContext with the same parameters with hasFocus true.
*/
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
lastKnownValue = value;
lastKnownComponent = table;
return getEditorForContext(table, value, row, column, isSelected, true, false, true);
}
/**
* Returns the component from getEditorForContext with hasFocus true.
*/
public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded,
boolean leaf, int row) {
lastKnownValue = value;
lastKnownComponent = tree;
return getEditorForContext(tree, value, row, 0, isSelected, true, expanded, leaf);
}
/**
* This default implementation returns a JPanel that is configured by calling
* configureComponentForContext.
*
* @return An component that is used to render content.
*/
public Component getRendererForContext(JComponent container, Object value, int row, int column, boolean isSelected,
boolean hasFocus, boolean isExpanded, boolean isLeaf) {
configureComponentForContext(rendererPanel, rendererButton, rendererLabel, container, value, row, column,
isSelected, hasFocus, isExpanded, isLeaf);
return rendererPanel;
}
/**
* This method returns a separate component that should be visually identical to
* the renderer component. We can't simply reuse the renderer component because
* the renderer is still used to paint the table while the editor component is
* displayed. Clicks are received on this component. This default implementation
* returns a JPanel that is configured by calling configureComponentForContext.
*
* @return A component used to receive clicks on the cell.
*/
public Component getEditorForContext(JComponent container, Object value, int row, int column, boolean isSelected,
boolean hasFocus, boolean isExpanded, boolean isLeaf) {
configureComponentForContext(editorPanel, editorButton, editorLabel, container, value, row, column, true,
hasFocus, isExpanded, isLeaf); // editor should always be selected
return editorPanel;
}
/**
* Called to configure components
*/
protected void configureComponentForContext(JPanel component, JButton iconButton, JLabel label,
JComponent container, Object value, int row, int column, boolean isSelected, boolean hasFocus,
boolean isExpanded, boolean isLeaf) {
if (hasFocus) {
if (container instanceof JTable) {
component.setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
} else {
component.setBorder(noFocusBorder);
}
if (container instanceof JTree) // was: (false)
{
label.setBorder(treeFocusBorder);
} else {
label.setBorder(noFocusBorder);
}
} else {
label.setBorder(noFocusBorder);
component.setBorder(noFocusBorder);
}
if (isSelected) {
if (container instanceof JTree) {
label.setOpaque(true);
label.setForeground(UIManager.getColor("Tree.selectionForeground"));
label.setBackground(UIManager.getColor("Tree.selectionBackground"));
component.setBackground(container.getBackground());
} else if (container instanceof JTable) {
label.setOpaque(false);
label.setForeground(((JTable) container).getSelectionForeground());
component.setBackground(((JTable) container).getSelectionBackground());
} else {
label.setOpaque(false);
label.setForeground(UIManager.getColor("Table.selectionForeground"));
component.setBackground(UIManager.getColor("Table.selectionBackground"));
}
} else {
label.setOpaque(false);
label.setForeground(container.getForeground());
component.setBackground(container.getBackground());
}
label.setFont(container.getFont());
Icon icon = getIconForContext(container, value, row, column, isSelected, hasFocus, isExpanded, isLeaf);
iconButton.setIcon(icon);
if (!isClickable()) {
iconButton.setDisabledIcon(icon);
}
String text = getStringForContext(container, value, row, column, isSelected, hasFocus, isExpanded, isLeaf);
if ((text == null) || ("".equals(text))) {
if (!label.getText().equals(""))
label.setText("");
} else {
if (!label.getText().equals(text))
label.setText(text);
}
}
/**
* Override this method to provide an icon for the renderer. This default
* implementation returns null.
*
* @return An icon to be displayed in the cell, or null to omit the icon from
* the cell.
*/
public Icon getIconForContext(JComponent container, Object value, int row, int column, boolean isSelected,
boolean hasFocus, boolean isExpanded, boolean isLeaf) {
return null;
}
/**
* Override this method to provide a string for the renderer. This default
* implementation returns toString on the value parameter, or null if the value
* is null.
*
* @return A string to be displayed in the cell.
*/
public String getStringForContext(JComponent container, Object value, int row, int column, boolean isSelected,
boolean hasFocus, boolean isExpanded, boolean isLeaf) {
if (value == null)
return null;
return value.toString();
}
/**
* Adds the specified listener to the list of listeners to be notified when the
* button receives a click.
*/
public void addActionListener(ActionListener aListener) {
if (actionListeners == null) {
actionListeners = new Vector(2);
}
actionListeners.add(aListener);
}
/**
* Removes the specified listener from the list of listeners to be notified when
* the button receives a click.
*/
public void removeActionListener(ActionListener aListener) {
actionListeners.remove(aListener);
}
/**
* Broadcasts the specified action event to all listeners.
*/
protected void fireActionEvent(ActionEvent anActionEvent) {
if (actionListeners == null)
return;
// vector's enumeration is not fail-fast
Enumeration e = actionListeners.elements();
while (e.hasMoreElements()) {
((ActionListener) e.nextElement()).actionPerformed(anActionEvent);
}
}
/**
* Returns the action command broadcast when this icon receives a click.
* Defaults to CLICKED.
*/
public String getActionCommand() {
return actionCommand;
}
/**
* Sets the action command broadcast when this table receives a double click.
*/
public void setActionCommand(String anActionCommand) {
actionCommand = anActionCommand;
}
// interface CellEditor
/**
* Returns lastKnownValue, although this should not be called.
*/
public Object getCellEditorValue() {
return lastKnownValue;
}
/**
* Returns true.
*/
public boolean isCellEditable(EventObject anEvent) {
return true;
}
/**
* Returns true.
*/
public boolean shouldSelectCell(EventObject anEvent) {
return true;
}
/**
* Fires an editing stopped event and returns true.
*/
public boolean stopCellEditing() {
ChangeEvent event = new ChangeEvent(this);
if (cellEditorListeners != null) {
// vector's enumeration is not fail-fast
Enumeration e = cellEditorListeners.elements();
while (e.hasMoreElements()) {
// broadcast editing cancelled since no value is edited
((CellEditorListener) e.nextElement()).editingCanceled(event);
}
}
lastKnownComponent = null;
return true;
}
/**
* Fires an editing cancelled event and returns true.
*/
public void cancelCellEditing() {
// HACK: cancelCellEditing() causes for the dragGesture
// to be NOT recognized AT ALL since on the next MOUSE_PRESSED
// the cell editor first needs to startEditing() [if in the tree
// the CellEditorListener is a BasicTreeUI class]
// (before the drag gesture event can be recognized).
// Also the lastKnownComponent should not be set to null,
// none of the mouse events won't dispathced to the lastKnownComponent
// in that case.
// Not calling it at all does seem to fix it, but what are the
// consequences???
// Trying to workaround this might solve it, but it introduces having
// an extra listener (a MouseMotionListnener), which might be wasteful
// (i.e. only if a Mouse_dragged event has been initiated, but DragGesture
// hasn't been recognized, postpone calling this till finish the DnD event)
// But what if do DnD and not exited ??? The mouseExited() is not called
// anyway until the DnD event is done.
ChangeEvent event = new ChangeEvent(this);
if (cellEditorListeners == null)
return;
// vector's enumeration is not fail-fast
Enumeration e = cellEditorListeners.elements();
while (e.hasMoreElements()) {
((CellEditorListener) e.nextElement()).editingCanceled(event);
}
// DO not nullify this
lastKnownComponent = null;
}
/**
* Adds the specified listener to the list of listeners to be notified when the
* table receives a double click.
*/
public void addCellEditorListener(CellEditorListener aListener) {
if (cellEditorListeners == null) {
cellEditorListeners = new Vector(2);
}
cellEditorListeners.add(aListener);
}
/**
* Removes the specified listener from the list of listeners to be notified when
* the table receives a double click.
*/
public void removeCellEditorListener(CellEditorListener aListener) {
cellEditorListeners.remove(aListener);
}
// interface ActionListener
/**
* Puts ourself on the end of the event queue for firing our action event to all
* listeners.
*/
public void actionPerformed(ActionEvent evt) {
// commented out in order NOT to set lastKnownComponent to null, since
// if this object is inside a table or tree, relying on getCellEditorValue()
// to return the currently edited object
// cancelCellEditing();
SwingUtilities.invokeLater(this);
}
// interface Runnable
/**
* Fires the action event to all listeners. This is triggered by a click on the
* icon.
*/
public void run() {
fireActionEvent(new ActionEvent(this, 0, getActionCommand()));
}
// interface MouseListener
/**
* Passes through editor mouse clicks to last known component. (left click only)
*/
public void mouseClicked(MouseEvent e) {
if (lastKnownComponent != null) {
Object source = e.getSource();
if (source != null) {
if (source == editorPanel) {
lastKnownComponent
.dispatchEvent(SwingUtilities.convertMouseEvent(editorPanel, e, lastKnownComponent));
} else if (source == editorLabel) {
lastKnownComponent
.dispatchEvent(SwingUtilities.convertMouseEvent(editorLabel, e, lastKnownComponent));
}
else if (source == editorButton) {
lastKnownComponent
.dispatchEvent(SwingUtilities.convertMouseEvent(editorButton, e, lastKnownComponent));
}
}
}
}
/**
* Passes through editor right-mouse (popup trigger) mouse events to last known
* component. Needed for possible displaying of popup menus on right click
*/
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
if (lastKnownComponent != null) {
Object source = e.getSource();
if (source != null) {
if (source == editorPanel) {
lastKnownComponent
.dispatchEvent(SwingUtilities.convertMouseEvent(editorPanel, e, lastKnownComponent));
} else if (source == editorLabel) {
lastKnownComponent
.dispatchEvent(SwingUtilities.convertMouseEvent(editorLabel, e, lastKnownComponent));
}
else if (source == editorButton) {
lastKnownComponent
.dispatchEvent(SwingUtilities.convertMouseEvent(editorButton, e, lastKnownComponent));
}
}
}
}
}
/**
* Does nothing.
*/
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
if (lastKnownComponent != null) {
Object source = e.getSource();
if (source != null) {
if (source == editorPanel) {
lastKnownComponent
.dispatchEvent(SwingUtilities.convertMouseEvent(editorPanel, e, lastKnownComponent));
}
else if (source == editorLabel) {
lastKnownComponent
.dispatchEvent(SwingUtilities.convertMouseEvent(editorLabel, e, lastKnownComponent));
}
else if (source == editorButton) {
lastKnownComponent
.dispatchEvent(SwingUtilities.convertMouseEvent(editorButton, e, lastKnownComponent));
}
}
}
}
}
/**
* Does nothing.
*/
public void mouseEntered(MouseEvent e) {
}
/**
* Cancels cell editing.
*/
public void mouseExited(MouseEvent e) {
Object source = e.getSource();
if (source != null && source instanceof JComponent) {
// need to convert the Point from the source's coordinate system to
// editorPanel's coordinate system.
// (note that simple editorPanel.contains(e.getPoint()) fails if source is
// editorButton)
Point convertedPoint = SwingUtilities.convertPoint((JComponent) source, e.getPoint(), editorPanel);
// check if exited from editorButton, but still inside the editorPanel (works
// for editorLabel as well)
if (!editorPanel.contains(convertedPoint)) {
// This was getting called before, but it interfers with the DnD operation
cancelCellEditing();
}
}
}
/*
* This might be redundant public void cleanUp(){
*
* //since cancelCellEditing() was never called call it now cancelCellEditing();
* stopCellEditing();
*
* editorButton.removeActionListener( this ); editorPanel.removeMouseListener(
* this ); editorLabel.removeMouseListener( this );
* editorButton.removeMouseListener( this ); lastKnownComponent = null;
* lastKnownValue = null; }
*/
}