/* 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; } */ }