package iiuf.swing; import java.awt.Rectangle; import java.awt.Point; import java.awt.Graphics; import java.awt.Component; import java.awt.Dimension; import java.awt.Insets; import java.awt.Color; import java.awt.event.FocusListener; import java.awt.event.FocusEvent; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.awt.event.KeyEvent; import java.awt.event.InputEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import javax.swing.JList; import javax.swing.JViewport; import javax.swing.JComponent; import javax.swing.CellRendererPane; import javax.swing.SwingUtilities; import javax.swing.ListModel; import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.KeyStroke; import javax.swing.LookAndFeel; import javax.swing.UIManager; import javax.swing.event.MouseInputListener; import javax.swing.event.ListSelectionListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListDataListener; import javax.swing.event.ListDataEvent; import javax.swing.plaf.ListUI; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicListUI; import iiuf.log.Log; // Parts of this is based on BasicListUI copyrighted by Sun, Microsystems, Inc. /** Multicolumn implementation of ListUI. (c) 2000, 2001, IIUF, DIUF<p> @author $Author: ohitz $ @version $Name: $ $Revision: 1.1 $ */ public class MultiColumnListUI extends ListUI { protected JList list = null; protected CellRendererPane rendererPane; // Listeners that this UI attaches to the JList protected FocusListener focusListener; protected MouseInputListener mouseInputListener; protected ListSelectionListener listSelectionListener; protected ListDataListener listDataListener; protected PropertyChangeListener propertyChangeListener; // PENDING(hmuller) need a doc pointer to #getRowHeight, #maybeUpdateLayout protected int[] cellHeights = null; protected int cellHeight = -1; protected int updateLayoutStateNeeded = modelChanged; /* The bits below define JList property changes that affect layout. * When one of these properties changes we set a bit in * updateLayoutStateNeeded. The change is dealt with lazily, see * maybeUpdateLayout. Changes to the JLists model, e.g. the * models length changed, are handled similarly, see DataListener. */ protected final static int modelChanged = 1 << 0; protected final static int selectionModelChanged = 1 << 1; protected final static int fontChanged = 1 << 2; protected final static int fixedCellWidthChanged = 1 << 3; protected final static int fixedCellHeightChanged = 1 << 4; protected final static int prototypeCellValueChanged = 1 << 5; protected final static int cellRendererChanged = 1 << 6; protected final static int heightChanged = 1 << 7; /** * Paint one List cell: compute the relevant state, get the "rubber stamp" * cell renderer component, and then use the CellRendererPane to paint it. * Subclasses may want to override this method rather than paint(). * * @see #paint */ protected void paintCell( Graphics g, int row, Rectangle rowBounds, ListCellRenderer cellRenderer, ListModel dataModel, ListSelectionModel selModel, int leadIndex) { Object value = dataModel.getElementAt(row); boolean cellHasFocus = list.hasFocus() && (row == leadIndex); boolean isSelected = selModel.isSelectedIndex(row); Component rendererComponent = cellRenderer.getListCellRendererComponent(list, value, row, isSelected, cellHasFocus); int cx = rowBounds.x; int cy = rowBounds.y; int cw = rowBounds.width; int ch = rowBounds.height; rendererPane.paintComponent(g, rendererComponent, list, cx, cy, cw, ch, true); } /** * @returns The preferred size. * @see #getPreferredSize */ public Dimension getMinimumSize(JComponent c) { return getPreferredSize(c); } /** * @returns The preferred size. * @see #getPreferredSize */ public Dimension getMaximumSize(JComponent c) { return getPreferredSize(c); } /** * Selected the previous row and force it to be visible. * Called by the KeyEvent.VK_UP keyboard action. * * @see #installKeyboardActions * @see JList#ensureIndexIsVisible */ protected void selectPreviousIndex() { int s = list.getSelectedIndex(); if(s > 0) { s -= 1; list.setSelectedIndex(s); list.ensureIndexIsVisible(s); } } /** * Selected the previous row and force it to be visible. * Called by the KeyEvent.VK_DOWN keyboard action. * * @see #installKeyboardActions * @see JList#ensureIndexIsVisible */ protected void selectNextIndex() { int s = list.getSelectedIndex(); if((s + 1) < list.getModel().getSize()) { s += 1; list.setSelectedIndex(s); list.ensureIndexIsVisible(s); } } /** * Register keyboard actions for the up and down arrow keys. The * actions just call out to protected methods, subclasses that * want to override or extend keyboard behavior should consider * just overriding those methods. This method is called at * installUI() time. * * @see #selectPreviousIndex * @see #selectNextIndex * @see #installUI */ protected void installKeyboardActions() { // up list.registerKeyboardAction(new IncrementLeadSelectionAction ("SelectPreviousRow", CHANGE_SELECTION, -1), KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new IncrementLeadSelectionAction ("SelectPreviousRow", CHANGE_SELECTION, -1), KeyStroke.getKeyStroke("KP_UP"), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new IncrementLeadSelectionAction ("ExtendSelectPreviousRow", EXTEND_SELECTION, -1), KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent. SHIFT_MASK), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new IncrementLeadSelectionAction ("ExtendSelectPreviousRow", EXTEND_SELECTION, -1), KeyStroke.getKeyStroke("shift KP_UP"), JComponent.WHEN_FOCUSED); // down list.registerKeyboardAction(new IncrementLeadSelectionAction ("SelectNextRow", CHANGE_SELECTION, 1), KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new IncrementLeadSelectionAction ("SelectNextRow", CHANGE_SELECTION, 1), KeyStroke.getKeyStroke("KP_DOWN"), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new IncrementLeadSelectionAction ("ExtendSelectPreviousRow", EXTEND_SELECTION, 1), KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent. SHIFT_MASK), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new IncrementLeadSelectionAction ("ExtendSelectPreviousRow", EXTEND_SELECTION, 1), KeyStroke.getKeyStroke("shift KP_DOWN"), JComponent.WHEN_FOCUSED); // home list.registerKeyboardAction(new HomeAction ("SelectHome", CHANGE_SELECTION), KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new HomeAction ("ExtendSelectHome", EXTEND_SELECTION), KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent. SHIFT_MASK), JComponent.WHEN_FOCUSED); // end list.registerKeyboardAction(new EndAction ("SelectEnd", CHANGE_SELECTION), KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new EndAction ("ExtendSelectEnd", EXTEND_SELECTION), KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent. SHIFT_MASK), JComponent.WHEN_FOCUSED); // page up list.registerKeyboardAction(new PageUpAction ("SelectPageUp", CHANGE_SELECTION), KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new PageUpAction ("ExtendSelectPageUp", EXTEND_SELECTION), KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, InputEvent. SHIFT_MASK), JComponent.WHEN_FOCUSED); // page down list.registerKeyboardAction(new PageDownAction ("SelectPageDown", CHANGE_SELECTION), KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(new PageDownAction ("ExtendSelectPageDown", EXTEND_SELECTION), KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, InputEvent.SHIFT_MASK), JComponent.WHEN_FOCUSED); // select all ActionListener selectAll = new SelectAllAction("SelectAll"); list.registerKeyboardAction(selectAll, KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_MASK), JComponent.WHEN_FOCUSED); list.registerKeyboardAction(selectAll, KeyStroke.getKeyStroke (KeyEvent.VK_SLASH, InputEvent.CTRL_MASK), JComponent.WHEN_FOCUSED); // clear selection list.registerKeyboardAction(new ClearSelectionAction("ClearSelection"), KeyStroke.getKeyStroke(KeyEvent. VK_BACK_SLASH, InputEvent.CTRL_MASK), JComponent.WHEN_FOCUSED); } /** * Unregister keyboard actions for the up and down arrow keys. * This method is called at uninstallUI() time - subclassess should * ensure that all of the keyboard actions registered at installUI * time are removed here. * * @see #selectPreviousIndex * @see #selectNextIndex * @see #installUI */ protected void uninstallKeyboardActions() { // up list.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0)); list.unregisterKeyboardAction(KeyStroke.getKeyStroke("KP_UP")); list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_UP, InputEvent.SHIFT_MASK)); list.unregisterKeyboardAction(KeyStroke.getKeyStroke ("shift KP_UP")); // down list.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent. VK_DOWN, 0)); list.unregisterKeyboardAction(KeyStroke.getKeyStroke("KP_DOWN")); list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK)); list.unregisterKeyboardAction(KeyStroke.getKeyStroke ("shift KP_DOWN")); // home list.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0)); list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_HOME, InputEvent.SHIFT_MASK)); // end list.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent. VK_END, 0)); list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_END, InputEvent.SHIFT_MASK)); // page up list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_PAGE_UP, 0)); list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_PAGE_UP, InputEvent.SHIFT_MASK)); // page down list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_PAGE_DOWN, 0)); list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_PAGE_DOWN, InputEvent.SHIFT_MASK)); // select all list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_A, InputEvent.CTRL_MASK)); list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_SLASH, InputEvent.CTRL_MASK)); // clear selection list.unregisterKeyboardAction(KeyStroke.getKeyStroke (KeyEvent.VK_BACK_SLASH, InputEvent.CTRL_MASK)); } /** * Create and install the listeners for the JList, its model, and its * selectionModel. This method is called at installUI() time. * * @see #installUI * @see #uninstallListeners */ protected void installListeners() { focusListener = createFocusListener(); mouseInputListener = createMouseInputListener(); propertyChangeListener = createPropertyChangeListener(); listSelectionListener = createListSelectionListener(); listDataListener = createListDataListener(); list.addFocusListener(focusListener); list.addMouseListener(mouseInputListener); list.addMouseMotionListener(mouseInputListener); list.addPropertyChangeListener(propertyChangeListener); ListModel model = list.getModel(); if (model != null) { model.addListDataListener(listDataListener); } ListSelectionModel selectionModel = list.getSelectionModel(); if (selectionModel != null) { selectionModel.addListSelectionListener(listSelectionListener); } } /** * Remove the listeners for the JList, its model, and its * selectionModel. All of the listener fields, are reset to * null here. This method is called at uninstallUI() time, * it should be kept in sync with installListeners. * * @see #uninstallUI * @see #installListeners */ protected void uninstallListeners() { list.removeFocusListener(focusListener); list.removeMouseListener(mouseInputListener); list.removeMouseMotionListener(mouseInputListener); list.removePropertyChangeListener(propertyChangeListener); ListModel model = list.getModel(); if (model != null) { model.removeListDataListener(listDataListener); } ListSelectionModel selectionModel = list.getSelectionModel(); if (selectionModel != null) { selectionModel.removeListSelectionListener(listSelectionListener); } focusListener = null; mouseInputListener = null; listSelectionListener = null; listDataListener = null; propertyChangeListener = null; } /** * Initialize JList properties, e.g. font, foreground, and background, * and add the CellRendererPane. The font, foreground, and background * properties are only set if their current value is either null * or a UIResource, other properties are set if the current * value is null. * * @see #uninstallDefaults * @see #installUI * @see CellRendererPane */ protected void installDefaults() { list.setLayout(null); LookAndFeel.installBorder(list, "List.border"); LookAndFeel.installColorsAndFont(list, "List.background", "List.foreground", "List.font"); if (list.getCellRenderer() == null) { list.setCellRenderer((ListCellRenderer)(UIManager.get("List.cellRenderer"))); } Color sbg = list.getSelectionBackground(); if (sbg == null || sbg instanceof UIResource) { list.setSelectionBackground(UIManager.getColor("List.selectionBackground")); } Color sfg = list.getSelectionForeground(); if (sfg == null || sfg instanceof UIResource) { list.setSelectionForeground(UIManager.getColor("List.selectionForeground")); } } /** * Set the JList properties that haven't been explicitly overriden to * null. A property is considered overridden if its current value * is not a UIResource. * * @see #installDefaults * @see #uninstallUI * @see CellRendererPane */ protected void uninstallDefaults() { if (list.getCellRenderer() instanceof UIResource) { list.setCellRenderer(null); } } /** * Initializes <code>this.list</code> by calling <code>installDefaults()</code>, * <code>installListeners()</code>, and <code>installKeyboardActions()</code> * in order. * * @see #installDefaults * @see #installListeners * @see #installKeyboardActions */ public void installUI(JComponent c) { list = (JList)c; rendererPane = new CellRendererPane(); list.add(rendererPane); installDefaults(); installListeners(); installKeyboardActions(); } /** * Uninitializes <code>this.list</code> by calling <code>uninstallListeners()</code>, * <code>uninstallKeyboardActions()</code>, and <code>uninstallDefaults()</code> * in order. Sets this.list to null. * * @see #uninstallListeners * @see #uninstallKeyboardActions * @see #uninstallDefaults */ public void uninstallUI(JComponent c) { uninstallDefaults(); uninstallListeners(); uninstallKeyboardActions(); cellHeight = -1; cellHeights = null; list.remove(rendererPane); rendererPane = null; list = null; } /** * Returns a new instance of BasicListUI. BasicListUI delegates are * allocated one per JList. * * @return A new ListUI implementation for the Windows look and feel. */ public static ComponentUI createUI(JComponent list) { return new BasicListUI(); } // PENDING(hmuller) explain the cell geometry abstraction in // the getRowHeight javadoc /** * Returns the height of the specified row based on the current layout. * * @return The specified row height or -1 if row isn't valid. * @see #convertYToRow * @see #convertRowToY * @see #updateLayoutState */ protected int getRowHeight(int row) { if ((row < 0) || (row >= list.getModel().getSize())) { return -1; } return (cellHeights == null) ? cellHeight : ((row < cellHeights.length) ? cellHeights[row] : -1); } /** * If updateLayoutStateNeeded is non zero, call updateLayoutState() and reset * updateLayoutStateNeeded. This method should be called by methods * before doing any computation based on the geometry of the list. * For example it's the first call in paint() and getPreferredSize(). * * @see #updateLayoutState */ protected void maybeUpdateLayoutState() { if (updateLayoutStateNeeded != 0) { updateLayoutState(); updateLayoutStateNeeded = 0; } } /** * Mouse input, and focus handling for JList. An instance of this * class is added to the appropriate java.awt.Component lists * at installUI() time. Note keyboard input is handled with JComponent * KeyboardActions, see installKeyboardActions(). * <p> * <strong>Warning:</strong> * Serialized objects of this class will not be compatible with * future Swing releases. The current serialization support is appropriate * for short term storage or RMI between applications running the same * version of Swing. A future release of Swing will provide support for * long term persistence. * * @see #createMouseInputListener * @see #installKeyboardActions * @see #installUI */ public class MouseInputHandler implements MouseInputListener { public void mouseClicked(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} public void mousePressed(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } if (!list.isEnabled()) { return; } /* Request focus before updating the list selection. This implies * that the current focus owner will see a focusLost() event * before the lists selection is updated IF requestFocus() is * synchronous (it is on Windows). See bug 4122345 */ if (!list.hasFocus()) { list.requestFocus(); } int row = locationToIndex(e.getX(), e.getY()); if (row != -1) { list.setValueIsAdjusting(true); int anchorIndex = list.getAnchorSelectionIndex(); if (e.isControlDown()) { if (list.isSelectedIndex(row)) { list.removeSelectionInterval(row, row); } else { list.addSelectionInterval(row, row); } } else if (e.isShiftDown() && (anchorIndex != -1)) { list.setSelectionInterval(anchorIndex, row); } else { list.setSelectionInterval(row, row); } } } public void mouseDragged(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } if (!list.isEnabled()) { return; } if (e.isShiftDown() || e.isControlDown()) { return; } int row = MultiColumnListUI.this.locationToIndex(e.getX(), e.getY()); if (row != -1) { Rectangle cellBounds = getCellBounds(list, row, row); if (cellBounds != null) { list.scrollRectToVisible(cellBounds); list.setSelectionInterval(row, row); } } } public void mouseMoved(MouseEvent e) { } public void mouseReleased(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } list.setValueIsAdjusting(false); } } /** * Creates a delegate that implements MouseInputListener. * The delegate is added to the corresponding java.awt.Component listener * lists at installUI() time. Subclasses can override this method to return * a custom MouseInputListener, e.g. * <pre> * class MyListUI extends BasicListUI { * protected MouseInputListener <b>createMouseInputListener</b>() { * return new MyMouseInputHandler(); * } * public class MyMouseInputHandler extends MouseInputHandler { * public void mouseMoved(MouseEvent e) { * // do some extra work when the mouse moves * super.mouseMoved(e); * } * } * } * </pre> * * @see MouseInputHandler * @see #installUI */ protected MouseInputListener createMouseInputListener() { return new MouseInputHandler(); } /** * This inner class is marked "public" due to a compiler bug. * This class should be treated as a "protected" inner class. * Instantiate it only within subclasses of BasicTableUI. */ public class FocusHandler implements FocusListener { protected void repaintCellFocus() { int leadIndex = list.getLeadSelectionIndex(); if (leadIndex != -1) { Rectangle r = getCellBounds(list, leadIndex, leadIndex); if (r != null) { list.repaint(r.x, r.y, r.width, r.height); } } } /* The focusGained() focusLost() methods run when the JList * focus changes. */ public void focusGained(FocusEvent e) { // hasFocus = true; repaintCellFocus(); } public void focusLost(FocusEvent e) { // hasFocus = false; repaintCellFocus(); } } protected FocusListener createFocusListener() { return new FocusHandler(); } /** * The ListSelectionListener that's added to the JLists selection * model at installUI time, and whenever the JList.selectionModel property * changes. When the selection changes we repaint the affected rows. * <p> * <strong>Warning:</strong> * Serialized objects of this class will not be compatible with * future Swing releases. The current serialization support is appropriate * for short term storage or RMI between applications running the same * version of Swing. A future release of Swing will provide support for * long term persistence. * * @see #createListSelectionListener * @see #getCellBounds * @see #installUI */ public class ListSelectionHandler implements ListSelectionListener { public void valueChanged(ListSelectionEvent e) { maybeUpdateLayoutState(); Rectangle bounds = getCellBounds(list, e.getFirstIndex(), e.getLastIndex()); if(bounds != null) list.repaint(bounds); } } /** * Creates an instance of ListSelectionHandler that's added to * the JLists by selectionModel as needed. Subclasses can override * this method to return a custom ListSelectionListener, e.g. * <pre> * class MyListUI extends BasicListUI { * protected ListSelectionListener <b>createListSelectionListener</b>() { * return new MySelectionListener(); * } * public class MySelectionListener extends ListSelectionHandler { * public void valueChanged(ListSelectionEvent e) { * // do some extra work when the selection changes * super.valueChange(e); * } * } * } * </pre> * * @see ListSelectionHandler * @see #installUI */ protected ListSelectionListener createListSelectionListener() { return new ListSelectionHandler(); } private void redrawList() { list.revalidate(); list.repaint(); } /** * The ListDataListener that's added to the JLists model at * installUI time, and whenever the JList.model property changes. * <p> * <strong>Warning:</strong> * Serialized objects of this class will not be compatible with * future Swing releases. The current serialization support is appropriate * for short term storage or RMI between applications running the same * version of Swing. A future release of Swing will provide support for * long term persistence. * * @see JList#getModel * @see #maybeUpdateLayout * @see #createListDataListener * @see #installUI */ public class ListDataHandler implements ListDataListener { public void intervalAdded(ListDataEvent e) { updateLayoutStateNeeded = modelChanged; int minIndex = Math.min(e.getIndex0(), e.getIndex1()); int maxIndex = Math.max(e.getIndex0(), e.getIndex1()); /* Sync the SelectionModel with the DataModel. */ ListSelectionModel sm = list.getSelectionModel(); if (sm != null) { sm.insertIndexInterval(minIndex, maxIndex - minIndex, true); } /* Repaint the entire list, from the origin of * the first added cell, to the bottom of the * component. */ Rectangle r = getCellBounds(list, minIndex, list.getModel().getSize()); list.revalidate(); list.repaint(r); } public void intervalRemoved(ListDataEvent e) { updateLayoutStateNeeded = modelChanged; /* Sync the SelectionModel with the DataModel. */ ListSelectionModel sm = list.getSelectionModel(); if (sm != null) { sm.removeIndexInterval(e.getIndex0(), e.getIndex1()); } /* Repaint the entire list, from the origin of * the first removed cell, to the bottom of the * component. */ int minIndex = Math.min(e.getIndex0(), e.getIndex1()); list.revalidate(); list.repaint(getCellBounds(list, minIndex, list.getModel().getSize())); } public void contentsChanged(ListDataEvent e) { updateLayoutStateNeeded = modelChanged; redrawList(); } } /** * Creates an instance of ListDataListener that's added to * the JLists by model as needed. Subclasses can override * this method to return a custom ListDataListener, e.g. * <pre> * class MyListUI extends BasicListUI { * protected ListDataListener <b>createListDataListener</b>() { * return new MyListDataListener(); * } * public class MyListDataListener extends ListDataHandler { * public void contentsChanged(ListDataEvent e) { * // do some extra work when the models contents change * super.contentsChange(e); * } * } * } * </pre> * * @see ListDataListener * @see JList#getModel * @see #installUI */ protected ListDataListener createListDataListener() { return new ListDataHandler(); } /** * The PropertyChangeListener that's added to the JList at * installUI time. When the value of a JList property that * affects layout changes, we set a bit in updateLayoutStateNeeded. * If the JLists model changes we additionally remove our listeners * from the old model. Likewise for the JList selectionModel. * <p> * <strong>Warning:</strong> * Serialized objects of this class will not be compatible with * future Swing releases. The current serialization support is appropriate * for short term storage or RMI between applications running the same * version of Swing. A future release of Swing will provide support for * long term persistence. * * @see #maybeUpdateLayout * @see #createPropertyListener * @see #installUI */ public class PropertyChangeHandler implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent e) { String propertyName = e.getPropertyName(); /* If the JList.model property changes, remove our listener, * listDataListener from the old model and add it to the new one. */ if (propertyName.equals("model")) { ListModel oldModel = (ListModel)e.getOldValue(); ListModel newModel = (ListModel)e.getNewValue(); if (oldModel != null) { oldModel.removeListDataListener(listDataListener); } if (newModel != null) { newModel.addListDataListener(listDataListener); } updateLayoutStateNeeded |= modelChanged; redrawList(); } /* If the JList.selectionModel property changes, remove our listener, * listSelectionListener from the old selectionModel and add it to the new one. */ else if (propertyName.equals("selectionModel")) { ListSelectionModel oldModel = (ListSelectionModel)e.getOldValue(); ListSelectionModel newModel = (ListSelectionModel)e.getNewValue(); if (oldModel != null) { oldModel.removeListSelectionListener(listSelectionListener); } if (newModel != null) { newModel.addListSelectionListener(listSelectionListener); } updateLayoutStateNeeded |= modelChanged; redrawList(); } else if (propertyName.equals("cellRenderer")) { updateLayoutStateNeeded |= cellRendererChanged; redrawList(); } else if (propertyName.equals("font")) { updateLayoutStateNeeded |= fontChanged; redrawList(); } else if (propertyName.equals("prototypeCellValue")) { updateLayoutStateNeeded |= prototypeCellValueChanged; redrawList(); } else if (propertyName.equals("fixedCellHeight")) { updateLayoutStateNeeded |= fixedCellHeightChanged; redrawList(); } else if (propertyName.equals("fixedCellWidth")) { updateLayoutStateNeeded |= fixedCellWidthChanged; redrawList(); } else if (propertyName.equals("cellRenderer")) { updateLayoutStateNeeded |= cellRendererChanged; redrawList(); } else if (propertyName.equals("selectionForeground")) { list.repaint(); } else if (propertyName.equals("selectionBackground")) { list.repaint(); } } } /** * Creates an instance of PropertyChangeHandler that's added to * the JList by installUI(). Subclasses can override this method * to return a custom PropertyChangeListener, e.g. * <pre> * class MyListUI extends BasicListUI { * protected PropertyChangeListener <b>createPropertyChangeListener</b>() { * return new MyPropertyChangeListener(); * } * public class MyPropertyChangeListener extends PropertyChangeHandler { * public void propertyChange(PropertyChangeEvent e) { * if (e.getPropertyName().equals("model")) { * // do some extra work when the model changes * } * super.propertyChange(e); * } * } * } * </pre> * * @see PropertyChangeListener * @see #installUI */ protected PropertyChangeListener createPropertyChangeListener() { return new PropertyChangeHandler(); } // Keyboard navigation actions. // NOTE: DefaultListSelectionModel.setAnchorSelectionIndex and // DefaultListSelectionModel.setLeadSelectionIndex both force the // new index to be selected. Because of this not all the bindings // could be appropriately implemented. Specifically those that // change the lead/anchor without selecting are not enabled. // Once this has been fixed the following actions will appropriately // work with selectionType == CHANGE_LEAD. /** Used by IncrementLeadSelectionAction. Indicates the action should * change the lead, and not select it. */ private static final int CHANGE_LEAD = 0; /** Used by IncrementLeadSelectionAction. Indicates the action should * change the selection and lead. */ private static final int CHANGE_SELECTION = 1; /** Used by IncrementLeadSelectionAction. Indicates the action should * extend the selection from the anchor to the next index. */ private static final int EXTEND_SELECTION = 2; /** * A generaic action this is only enabled when the JList is enabled. */ private abstract class ListAction implements ActionListener { protected ListAction(String name) { } public boolean isEnabled() { return (list != null && list.isEnabled()); } } /** * Action to increment the selection in the list up/down a row at * a type. This also has the option to extend the selection, or * only move the lead. */ private class IncrementLeadSelectionAction extends ListAction { /** Amount to offset, subclasses will define what this means. */ protected int amount; /** One of CHANGE_LEAD, CHANGE_SELECTION or EXTEND_SELECTION. */ protected int selectionType; protected IncrementLeadSelectionAction(String name, int type) { this(name, type, -1); } protected IncrementLeadSelectionAction(String name, int type, int amount) { super(name); this.amount = amount; this.selectionType = type; } /** * Returns the next index to select. This is based on the lead * selected index and the <code>amount</code> ivar. */ protected int getNextIndex() { int index = list.getLeadSelectionIndex(); int size = list.getModel().getSize(); if (index == -1) { if (size > 0) { if (amount > 0) { index = 0; } else { index = size - 1; } } } else { index += amount; } return index; } /** * Ensures the particular index is visible. This simply forwards * the method to list. */ protected void ensureIndexIsVisible(int index) { list.ensureIndexIsVisible(index); } /** * Invokes <code>getNextIndex</code> to determine the next index * to select. If the index is valid (not -1 and < size of the model), * this will either: move the selection to the new index if * the selectionType == CHANGE_SELECTION, move the lead to the * new index if selectionType == CHANGE_LEAD, otherwise the * selection is extend from the anchor to the new index and the * lead is set to the new index. */ public void actionPerformed(ActionEvent e) { int index = getNextIndex(); if (index >= 0 && index < list.getModel().getSize()) { ListSelectionModel lsm = list.getSelectionModel(); if (selectionType == EXTEND_SELECTION) { int anchor = lsm.getAnchorSelectionIndex(); if (anchor == -1) { anchor = index; } list.setSelectionInterval(anchor, index); lsm.setAnchorSelectionIndex(anchor); lsm.setLeadSelectionIndex(index); } else if (selectionType == CHANGE_SELECTION) { list.setSelectedIndex(index); } else { lsm.setLeadSelectionIndex(index); } ensureIndexIsVisible(index); } } } /** * Action to move the selection to the first item in the list. */ private class HomeAction extends IncrementLeadSelectionAction { protected HomeAction(String name, int type) { super(name, type); } protected int getNextIndex() { return 0; } } /** * Action to move the selection to the last item in the list. */ private class EndAction extends IncrementLeadSelectionAction { protected EndAction(String name, int type) { super(name, type); } protected int getNextIndex() { return list.getModel().getSize() - 1; } } /** * Action to move up one page. */ private class PageUpAction extends IncrementLeadSelectionAction { protected PageUpAction(String name, int type) { super(name, type); } protected int getNextIndex() { int index = list.getFirstVisibleIndex(); ListSelectionModel lsm = list.getSelectionModel(); if (lsm.getLeadSelectionIndex() == index) { Rectangle visRect = list.getVisibleRect(); visRect.y = Math.max(0, visRect.y - visRect.height); index = list.locationToIndex(visRect.getLocation()); } return index; } protected void ensureIndexIsVisible(int index) { Rectangle visRect = list.getVisibleRect(); Rectangle cellBounds = list.getCellBounds(index, index); cellBounds.height = visRect.height; list.scrollRectToVisible(cellBounds); } } /** * Action to move down one page. */ private class PageDownAction extends IncrementLeadSelectionAction { protected PageDownAction(String name, int type) { super(name, type); } protected int getNextIndex() { int index = list.getLastVisibleIndex(); ListSelectionModel lsm = list.getSelectionModel(); if (index == -1) { // Will happen if size < viewport size. index = list.getModel().getSize() - 1; } if (lsm.getLeadSelectionIndex() == index) { Rectangle visRect = list.getVisibleRect(); visRect.y += visRect.height + visRect.height - 1; index = list.locationToIndex(visRect.getLocation()); if (index == -1) { index = list.getModel().getSize() - 1; } } return index; } protected void ensureIndexIsVisible(int index) { Rectangle visRect = list.getVisibleRect(); Rectangle cellBounds = list.getCellBounds(index, index); cellBounds.y = Math.max(0, cellBounds.y + cellBounds.height - visRect.height); cellBounds.height = visRect.height; list.scrollRectToVisible(cellBounds); } } /** * Action to select all the items in the list. */ private class SelectAllAction extends ListAction { private SelectAllAction(String name) { super(name); } public void actionPerformed(ActionEvent e) { // Select all should not alter the lead and anchor. // ListSelectionModel encforces the selection to the anchor/lead, // so it is commented out. // ListSelectionModel lsm = list.getSelectionModel(); // int anchor = lsm.getAnchorSelectionIndex(); // int lead = lsm.getLeadSelectionIndex(); list.setSelectionInterval(0, list.getModel().getSize() - 1); // lsm.setAnchorSelectionIndex(anchor); // lsm.setLeadSelectionIndex(lead); } } /** * Action to clear the selection in the list. */ private class ClearSelectionAction extends ListAction { private ClearSelectionAction(String name) { super(name); } public void actionPerformed(ActionEvent e) { // Unselect all should not alter the lead and anchor. // ListSelectionModel encforces the selection to the anchor/lead, // so it is commented out. // ListSelectionModel lsm = list.getSelectionModel(); // int anchor = lsm.getAnchorSelectionIndex(); // int lead = lsm.getLeadSelectionIndex(); list.clearSelection(); // lsm.setAnchorSelectionIndex(anchor); // lsm.setLeadSelectionIndex(lead); } } /** * @return The index of the cell at location, or -1. * @see ListUI#locationToIndex */ public int locationToIndex(JList list, Point location) { return locationToIndex(location.x, location.y); } /** * @return The index of the cell at location, or -1. * @see ListUI#locationToIndex */ private int locationToIndex(int x, int y) { maybeUpdateLayoutState(); if(cellHeights.length == 0) return -1; // figure out column int off = 0; int idx = 0; for(int i = 0; i < columnWidths.length; i++) { if(x >= off && x <= off + columnWidths[i]) { // figure out row int h = cellHeights[idx]; while(y > h) { if(idx >= cellHeights.length) return -1; h += cellHeights[idx++]; } return idx >= cellHeights.length ? -1 : idx; } off += columnWidths[i]; idx += rowsPerColumn[i]; } return -1; } /** * Paint the rows that intersect the Graphics objects clipRect. This * method calls paintCell as necessary. Subclasses * may want to override these methods. * * @see #paintCell */ public void paint(Graphics g, JComponent c) { maybeUpdateLayoutState(); ListCellRenderer renderer = list.getCellRenderer(); ListModel dataModel = list.getModel(); ListSelectionModel selModel = list.getSelectionModel(); if ((renderer == null) || (dataModel.getSize() == 0)) { return; } /* Compute the area we're going to paint in terms of the affected * rows (firstPaintRow, lastPaintRow), and the clip bounds. */ Rectangle paintBounds = g.getClipBounds(); int firstPaintRow = locationToIndex(paintBounds.x, paintBounds.y); int lastPaintRow = locationToIndex((paintBounds.x + paintBounds.width) - 1, (paintBounds.y + paintBounds.height) - 1); if (firstPaintRow == -1) { firstPaintRow = 0; } if (lastPaintRow == -1) { lastPaintRow = dataModel.getSize() - 1; } Rectangle rowBounds = getCellBounds(list, firstPaintRow, lastPaintRow); if (rowBounds == null) { return; } int leadIndex = list.getLeadSelectionIndex(); for(int row = firstPaintRow; row <= lastPaintRow; row++) { /* Set the clip rect to be the intersection of rowBounds * and paintBounds and then paint the cell. */ g.setClip(rowBounds.x, rowBounds.y, rowBounds.width, rowBounds.height); g.clipRect(paintBounds.x, paintBounds.y, paintBounds.width, paintBounds.height); Rectangle bounds = getCellBounds(row); if(bounds != null) paintCell(g, row, bounds, renderer, dataModel, selModel, leadIndex); } } private int[] columnWidths = new int[1]; private int[] columnOff; private int[] rowsPerColumn = new int[1]; private int[] cellColumn; private int[] cellYOff; /** * Recompute the value of cellHeight or cellHeights based * and cellWidth, based on the current font and the current * values of fixedCellWidth, fixedCellHeight, and prototypeCellValue. * * @see #maybeUpdateLayoutState */ boolean updating; protected void updateLayoutState() { synchronized(this) { if(updating) return; updating = true; } /* If both JList fixedCellWidth and fixedCellHeight have been * set, then initialize cellWidth and cellHeight, and set * cellHeights to null. */ int listHeight = getPreferredHeight(); int fixedCellHeight = list.getFixedCellHeight(); int fixedCellWidth = list.getFixedCellWidth(); columnWidths = new int[1]; rowsPerColumn = new int[1]; if (fixedCellHeight != -1) { cellHeight = fixedCellHeight; cellHeights = null; cellColumn = null; } else { cellHeight = -1; cellHeights = new int[list.getModel().getSize()]; cellColumn = new int[list.getModel().getSize()]; cellYOff = new int[list.getModel().getSize()]; } /* If either of JList fixedCellWidth and fixedCellHeight haven't * been set, then initialize cellWidth and cellHeights by * scanning through the entire model. Note: if the renderer is * null, we just set cellWidth and cellHeights[*] to zero, * if they're not set already. */ if ((fixedCellWidth == -1) || (fixedCellHeight == -1)) { ListModel dataModel = list.getModel(); int dataModelSize = dataModel.getSize(); ListCellRenderer renderer = list.getCellRenderer(); if (renderer != null) { int tmph = 0; int column = 0; for(int index = 0; index < dataModelSize; index++) { Object value = dataModel.getElementAt(index); Component c = renderer.getListCellRendererComponent(list, value, index, false, false); rendererPane.add(c); Dimension cellSize = c.getPreferredSize(); cellYOff[index] = tmph; tmph += cellSize.height; if(tmph > listHeight) { column++; tmph = cellSize.height; cellYOff[index] = 0; int[] tmp = columnWidths; columnWidths = new int[column + 1]; System.arraycopy(tmp, 0, columnWidths, 0, Math.min(tmp.length, columnWidths.length)); tmp = rowsPerColumn; rowsPerColumn = new int[column + 1]; System.arraycopy(tmp, 0, rowsPerColumn, 0, Math.min(tmp.length, rowsPerColumn.length)); } if (fixedCellWidth == -1) { columnWidths[column] = Math.max(cellSize.width, columnWidths[column]); } if (fixedCellHeight == -1) { cellHeights[index] = cellSize.height; } cellColumn[index] = column; rowsPerColumn[column]++; } } else { columnWidths = new int[1]; for(int index = 0; index < dataModelSize; index++) { cellHeights[index] = 0; } } } columnOff = new int[columnWidths.length]; for(int i = 1; i < columnOff.length; i++) columnOff[i] = columnOff[i - 1] + columnWidths[i - 1]; list.invalidate(); /* System.out.println("listHeight:" + listHeight); for(int i = 0; i < rowsPerColumn.length; i++) System.out.println("rowsPerColumn/Widths/off[" + i + "] " + rowsPerColumn[i] + "," + columnWidths[i] + "," + columnOff[i]); for(int i = 0; i < cellHeights.length; i++) System.out.println("cellHeights/column[" + i + "] " + cellHeights[i] + "/" + cellColumn[i] + "/" + cellYOff[i]); */ updating = false; } public Dimension getPreferredSize(JComponent c) { maybeUpdateLayoutState(); int lastRow = list.getModel().getSize() - 1; if (lastRow < 0) { return new Dimension(0, 0); } int w = 0; for(int i = 0; i < columnWidths.length; i++) w += columnWidths[i]; return new Dimension(w, getPreferredHeight()); } int oldHeight; private int getPreferredHeight() { int result = 0; if(list.getParent() instanceof JViewport) { result = Math.max(list.getParent().getHeight(), 200); } else for(int i = 0; i < cellHeights.length; i++) result += cellHeights[i]; if(result != oldHeight) { oldHeight = result; updateLayoutStateNeeded = heightChanged; } return result; } private Rectangle getCellBounds(int index) { maybeUpdateLayoutState(); if(index < 0 || index >= cellHeights.length) return null; return new Rectangle(columnOff[cellColumn[index]], cellYOff[index], columnWidths[cellColumn[index]], cellHeights[index]); } /** * @return The bounds of the index'th cell. * @see ListUI#getCellBounds */ public Rectangle getCellBounds(JList list, int index1, int index2) { Rectangle r1 = getCellBounds(index1); Rectangle r2 = getCellBounds(index2); if(r1 == null && r2 == null) return null; if(r1 == null) return r2; if(r2 == null) return r1; int tmp = index1; index1 = Math.min(index1, index2); index2 = Math.max(tmp, index2); if(cellColumn[index1] != cellColumn[index2]) return new Rectangle(columnOff[cellColumn[index1]], 0, columnOff[cellColumn[index2]] + columnWidths[cellColumn[index2]], getPreferredHeight()); else return r1.union(r2); } /** * @return The origin of the index'th cell. * @see ListUI#indexToLocation */ public Point indexToLocation(JList list, int index) { maybeUpdateLayoutState(); return new Point(columnOff[cellColumn[index]], cellYOff[index]); } } /* $Log: MultiColumnListUI.java,v $ Revision 1.1 2002/07/11 12:09:52 ohitz Initial checkin Revision 1.7 2001/01/17 09:55:46 schubige Logger update Revision 1.6 2001/01/15 15:08:58 schubige some sourcewatch bug fixes Revision 1.5 2001/01/14 13:21:13 schubige Win NT update Revision 1.4 2001/01/04 16:28:39 schubige Header update for 2001 and DIUF Revision 1.3 2000/11/09 07:48:44 schubige early checkin for DCJava Revision 1.2 2000/10/10 16:32:12 schubige Added subtree display to TreeView, fixed some bugs Revision 1.1 2000/10/03 08:39:39 schubige Added tree view and contect menu stuff */