components/visual-panels/core/src/java/util/com/oracle/solaris/vp/util/swing/ListCellOverlay.java
changeset 827 0944d8c0158b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/visual-panels/core/src/java/util/com/oracle/solaris/vp/util/swing/ListCellOverlay.java	Thu May 24 04:16:47 2012 -0400
@@ -0,0 +1,651 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
+ */
+
+package com.oracle.solaris.vp.util.swing;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.beans.*;
+import java.util.*;
+import java.util.List;
+import javax.swing.*;
+import javax.swing.Timer;
+import javax.swing.event.*;
+import com.oracle.solaris.vp.util.misc.CollectionUtil;
+
+/**
+ * The {@code ListCellOverlay} class provides an overlay over each cell in a
+ * {@code JList}.  This overlay extends the cell to its preferred size so that
+ * the user can see the entire contents of a cell that is partially hidden
+ * within a scroll pane or is sized smaller than its preferred size.
+ * <br/>
+ * This class assumes that the {@code JList} is contained within a {@code
+ * JScrollPane}; the overlay is styled to complement that scroll pane.
+ * <br/>
+ * The {@link #getOverlayComponent overlay component} or the {@link
+ * #getOverlayScrollPane scroll pane} that contains it may be changed visually
+ * (custom borders, background color, etc.) to affect the look of the overlay.
+ */
+public class ListCellOverlay {
+    //
+    // Enums
+    //
+
+    public enum Style {
+	SINGLE_POPUP, MULTI_POPUP
+    }
+
+    //
+    // Inner classes
+    //
+
+    private static abstract class CellPanel extends JPanel {
+	//
+	// Instance data
+	//
+
+	private CellRendererPane rPane = new CellRendererPane();
+
+	//
+	// Constructors
+	//
+
+	public CellPanel() {
+	    setLayout(null);
+	    add(rPane);
+	}
+
+	//
+	// Component methods
+	//
+
+	@Override
+	public Dimension getPreferredSize() {
+	    Dimension d = getRendererComponentPreferredSize();
+	    Insets insets = getInsets();
+	    d.width += insets.left + insets.right;
+	    d.height += insets.top + insets.bottom;
+
+	    return d;
+	}
+
+	//
+	// JComponent methods
+	//
+
+	@Override
+	public void paintComponent(Graphics g) {
+	    super.paintComponent(g);
+
+	    Component comp = getRendererComponent();
+
+	    Insets insets = getInsets();
+	    int x = insets.left;
+	    int y = insets.top;
+	    int width = getWidth() - insets.left - insets.right;
+	    int height = getHeight() - insets.top - insets.bottom;
+
+	    rPane.paintComponent(g, comp, this, x, y, width, height, true);
+	}
+
+	//
+	// CellPanel methods
+	//
+
+	public abstract Component getRendererComponent();
+
+	public Dimension getRendererComponentPreferredSize() {
+	    return getRendererComponent().getPreferredSize();
+	}
+    }
+
+    private class SimpleCellPanel extends CellPanel {
+	//
+	// Instance data
+	//
+
+	private Component comp;
+
+	//
+	// Constructors
+	//
+
+	public SimpleCellPanel(Component comp) {
+	    this.comp = comp;
+
+	    MouseAdapter listener =
+		new MouseAdapter() {
+		    //
+		    // MouseListener methods
+		    //
+
+		    @Override
+		    public void mouseClicked(MouseEvent e) {
+			GUIUtil.propagate(e, list);
+		    }
+
+		    @Override
+		    public void mouseExited(MouseEvent e) {
+			configurePopups(e);
+		    }
+
+		    @Override
+		    public void mousePressed(MouseEvent e) {
+			GUIUtil.propagate(e, list);
+		    }
+
+		    @Override
+		    public void mouseReleased(MouseEvent e) {
+			GUIUtil.propagate(e, list);
+		    }
+
+		    //
+		    // MouseMotionListener methods
+		    //
+
+		    @Override
+		    public void mouseDragged(MouseEvent e) {
+			GUIUtil.propagate(e, list);
+		    }
+
+		    @Override
+		    public void mouseMoved(MouseEvent e) {
+			GUIUtil.propagate(e, list);
+		    }
+
+		    //
+		    // MouseWheelListener methods
+		    //
+
+		    @Override
+		    public void mouseWheelMoved(MouseWheelEvent e) {
+			if (listScroll != null) {
+			    GUIUtil.propagate(e, listScroll);
+			}
+		    }
+		};
+
+	    addMouseListener(listener);
+	    addMouseMotionListener(listener);
+	    addMouseWheelListener(listener);
+	}
+
+	//
+	// CellPanel methods
+	//
+
+	@Override
+	public Component getRendererComponent() {
+	    return comp;
+	}
+    }
+
+    //
+    // Static data
+    //
+
+    /**
+     * The initial delay after hovering over the list before an overlay appears.
+     */
+    public static final int DELAY = 0;
+
+    //
+    // Instance data
+    //
+
+    // Set to true to paint each overlay rectangle with a solid color
+    private boolean debug = false;
+
+    private JList list;
+    private JScrollPane listScroll;
+
+    private CellPanel panel;
+    private JScrollPane panelScroll;
+    private Map<Popup, CellPanel> popups = new HashMap<Popup, CellPanel>();
+    private Timer timer;
+
+    private int index = -1;
+    private boolean enabled = true;
+    private Style style = Style.MULTI_POPUP;
+
+    private ListDataListener listModelListener =
+	new ListDataListener() {
+	    @Override
+	    public void contentsChanged(ListDataEvent e) {
+		repaintPopups();
+	    }
+
+	    @Override
+	    public void intervalAdded(ListDataEvent e) {
+		repaintPopups();
+	    }
+
+	    @Override
+	    public void intervalRemoved(ListDataEvent e) {
+		hidePopups();
+	    }
+	};
+
+    //
+    // Constructors
+    //
+
+    public ListCellOverlay(final JList list) {
+	this.list = list;
+
+	panel = new CellPanel() {
+	    @Override
+	    public Component getRendererComponent() {
+		Object value = list.getModel().getElementAt(index);
+		boolean isSelected = list.isSelectedIndex(index);
+		boolean hasFocus = list.hasFocus() &&
+		    (index == list.getLeadSelectionIndex());
+
+		return list.getCellRenderer().getListCellRendererComponent(
+		    list, value, index, isSelected, hasFocus);
+	    }
+
+	    @Override
+	    public Dimension getRendererComponentPreferredSize() {
+		Dimension d = super.getRendererComponentPreferredSize();
+
+		Rectangle r = list.getCellBounds(index, index);
+		d.width = Math.max(d.width, r.width);
+		d.height = Math.max(d.height, r.height);
+
+		return d;
+	    }
+	};
+
+	panel.setOpaque(true);
+	panel.setBackground(list.getBackground());
+
+	panelScroll = new JScrollPane();
+
+	timer = new Timer(DELAY,
+	    new ActionListener() {
+		@Override
+		public void actionPerformed(ActionEvent e) {
+		    for (Popup popup : popups.keySet()) {
+			popup.show();
+		    }
+		}
+	    });
+	timer.setRepeats(false);
+
+	HierarchyListener listener =
+	    new HierarchyListener() {
+		@Override
+		public void hierarchyChanged(HierarchyEvent e) {
+		    listScroll = (JScrollPane)SwingUtilities.getAncestorOfClass(
+			JScrollPane.class, list);
+		}
+	    };
+	list.addHierarchyListener(listener);
+
+	// Initialize
+	listener.hierarchyChanged(null);
+
+	list.addHierarchyBoundsListener(
+	    new HierarchyBoundsListener() {
+		@Override
+		public void ancestorMoved(HierarchyEvent e) {
+		    hidePopups();
+		}
+
+		@Override
+		public void ancestorResized(HierarchyEvent e) {
+		    hidePopups();
+		}
+	    });
+
+	list.addComponentListener(
+	    new ComponentAdapter() {
+		@Override
+		public void componentMoved(ComponentEvent e) {
+		    hidePopups();
+		}
+
+		@Override
+		public void componentResized(ComponentEvent e) {
+		    hidePopups();
+		}
+	    });
+
+	list.addMouseMotionListener(
+	    new MouseMotionAdapter() {
+		@Override
+		public void mouseMoved(MouseEvent e) {
+		    configurePopups(e);
+		}
+	    });
+
+	list.addMouseListener(
+	    new MouseAdapter() {
+		@Override
+		public void mouseExited(MouseEvent e) {
+		    configurePopups(e);
+		}
+	    });
+
+	list.addListSelectionListener(
+	    new ListSelectionListener() {
+		@Override
+		public void valueChanged(ListSelectionEvent e) {
+		    repaintPopups();
+		}
+	    });
+
+	list.addPropertyChangeListener("model",
+	    new PropertyChangeListener() {
+		@Override
+		public void propertyChange(PropertyChangeEvent e) {
+		    modelChanged((ListModel)e.getOldValue(),
+			(ListModel)e.getNewValue());
+		}
+	    });
+
+	// Initialize
+	modelChanged(null, list.getModel());
+    }
+
+    //
+    // ListCellOverlay methods
+    //
+
+    /**
+     * Gets the initial delay after hovering over the list before an overlay of
+     * a cell appears.
+     */
+    public int getDelay() {
+	return timer.getInitialDelay();
+    }
+
+    /**
+     * Gets whether this {@code ListCellOverlay} is enabled.
+     */
+    public boolean getEnabled() {
+	return enabled;
+    }
+
+    public JList getList() {
+	return list;
+    }
+
+    /**
+     * Gets the {@code JComponent} that is displays the cell renderer in the
+     * overlay.  This {@code JComponent} can be customized with a border, a
+     * change in background color, etc.
+     */
+    public JComponent getOverlayComponent() {
+	return panel;
+    }
+
+    /**
+     * Gets the {@code JScrollPane} containing the {@link #getOverlayComponent
+     * overlay component}.  This provides a border that (presumably) matches the
+     * one surrounding the list.
+     */
+    public JScrollPane getOverlayScrollPane() {
+	return panelScroll;
+    }
+
+    /**
+     * Gets the style to use when displaying the overlay.
+     */
+    public Style getStyle() {
+	return style;
+    }
+
+    /**
+     * Sets the style to use when displaying the overlay. {@link
+     * Style#SINGLE_POPUP} uses a single, tooltip-like popup, complete with
+     * borders, as the overlay.  {@link Style#MULTI_POPUP} uses multiple popups
+     * to make it appear as though the scroll pane that encloses the list
+     * changes shape to accommodate the overlay.  The latter style may appear
+     * more polished, but the former has a lesser performance hit.  The default
+     * value is {@link Style#MULTI_POPUP}.
+     */
+    public void setStyle(Style style) {
+	this.style = style;
+    }
+
+    /**
+     * Sets the initial delay after hovering over the list before an overlay of
+     * a cell appears.
+     */
+    public void setDelay(int delay) {
+	timer.setInitialDelay(delay);
+    }
+
+    /**
+     * Gets whether this {@code ListCellOverlay} is enabled.
+     */
+    public void setEnabled(boolean enabled) {
+	if (this.enabled != enabled) {
+	    if (!enabled) {
+		hidePopups();
+	    }
+	    this.enabled = enabled;
+	}
+    }
+
+    //
+    // Private methods
+    //
+
+    private void configurePopups(MouseEvent e) {
+	if (!enabled || listScroll == null) {
+	    return;
+	}
+
+	Rectangle cellRect = null;
+	int index;
+
+	Point listScrollPoint = SwingUtilities.convertPoint(
+	    e.getComponent(), e.getPoint(), listScroll);
+
+	// Is the point visible?
+	if (!listScroll.contains(listScrollPoint)) {
+	    index = -1;
+	} else {
+	    Point listPoint = SwingUtilities.convertPoint(
+		listScroll, listScrollPoint, list);
+
+	    index = list.locationToIndex(listPoint);
+	    if (index != -1) {
+		cellRect = list.getCellBounds(index, index);
+		if (!cellRect.contains(listPoint)) {
+		    // The point is not actually in a list cell
+		    index = -1;
+		}
+	    }
+	}
+
+	// Has the index of the overlaid cell changed?
+	if (this.index != index) {
+	    hidePopups();
+
+	    if (index != -1) {
+		this.index = index;
+
+		if (isCellObscured()) {
+		    // We now need to figure out where to place popup windows
+		    // (containing scroll, which contains panel, which paints
+		    // the cell renderer) so that the cell renderer within the
+		    // popups aligns with the cell renderer in the list.
+
+		    // The size of the panel scroll pane border
+		    // (panelScroll.getInsets() doesn't work under some L&Fs
+		    // (like synth), so we are forced to do a layout and compare
+		    // points)
+		    panelScroll.setViewportView(panel);
+		    panelScroll.doLayout();
+		    Point panelScrollInsets = SwingUtilities.convertPoint(
+			panel, 0, 0, panelScroll);
+
+		    // The size of the panel border
+		    Insets panelInsets = panel.getInsets();
+
+		    if (cellRect == null) {
+			cellRect = list.getCellBounds(index, index);
+		    }
+
+		    Dimension panelScrollPrefSize =
+			panelScroll.getPreferredSize();
+
+		    // The bounds of the panel scroll pane in list coordinates
+		    Rectangle panelScrollBounds = new Rectangle(
+			cellRect.x - panelInsets.left - panelScrollInsets.x,
+			cellRect.y - panelInsets.top - panelScrollInsets.y,
+			panelScrollPrefSize.width, panelScrollPrefSize.height);
+
+		    // Convert to the list scroll pane's coordinates
+		    panelScrollBounds = SwingUtilities.convertRectangle(
+			list, panelScrollBounds, listScroll);
+
+		    List<Rectangle> rectList = new ArrayList<Rectangle>();
+
+		    switch (style) {
+			case SINGLE_POPUP:
+			    rectList.add(panelScrollBounds);
+			    break;
+
+			default:
+			case MULTI_POPUP:
+			    // The bounds of the list viewport in the list
+			    // scroll pane's coordinates
+			    Rectangle listViewBounds =
+				listScroll.getViewport().getBounds();
+
+			    Rectangle[] rects =
+				SwingUtilities.computeDifference(
+				panelScrollBounds, listViewBounds);
+
+			    CollectionUtil.addAll(rectList, rects);
+
+			    // Convert to the list scroll pane's coordinates
+			    cellRect = SwingUtilities.convertRectangle(
+				list, cellRect, listScroll);
+
+			    // Now add one more rect to cover the visible area
+			    // of the cell entirely.  This is necessary because
+			    // the renderer may be drawing its content truncated
+			    // (ie. "Foo bar..."), and we need it to be drawn at
+			    // full size.
+			    rectList.add(cellRect);
+		    }
+
+		    Point listScrollScreenPoint =
+			listScroll.getLocationOnScreen();
+
+		    for (Rectangle rect : rectList) {
+			Point point = rect.getLocation();
+			point.translate(-panelScrollBounds.x,
+			    -panelScrollBounds.y);
+
+			JViewport viewport = new JViewport();
+			viewport.setPreferredSize(new Dimension(
+			    rect.width, rect.height));
+
+			CellPanel cell = new SimpleCellPanel(panelScroll);
+
+			if (debug) {
+			    JLabel label = new JLabel();
+			    label.setOpaque(true);
+			    label.setBackground(ColorUtil.getRandomColor());
+			    cell = new SimpleCellPanel(label);
+			}
+
+			viewport.setView(cell);
+			viewport.setViewPosition(point);
+
+			int x = rect.x + listScrollScreenPoint.x;
+			int y = rect.y + listScrollScreenPoint.y;
+
+			Popup popup = PopupFactory.getSharedInstance().getPopup(
+			    list, viewport, x, y);
+
+			popups.put(popup, cell);
+		    }
+
+		    timer.restart();
+		}
+	    }
+	}
+    }
+
+    private boolean getPopupsShowing() {
+	return !popups.isEmpty();
+    }
+
+    private void hidePopups() {
+	timer.stop();
+	index = -1;
+	for (Iterator<Popup> i = popups.keySet().iterator(); i.hasNext();) {
+	    Popup popup = i.next();
+	    popup.hide();
+	    i.remove();
+	}
+	list.repaint();
+    }
+
+    private boolean isCellObscured() {
+	Rectangle cellRect = list.getCellBounds(index, index);
+	Dimension panelSize = panel.getPreferredSize();
+
+	// Is the cell rect smaller than the preferred size?
+	if (cellRect.width < panelSize.width ||
+	    cellRect.height < panelSize.height) {
+	    return true;
+	}
+
+	// The bounds of panel at its preferred size, relative to listScroll
+	Rectangle panelBounds = SwingUtilities.convertRectangle(list,
+	    new Rectangle(cellRect), listScroll);
+
+	Rectangle scrollBounds = new Rectangle(listScroll.getSize());
+
+	return !scrollBounds.contains(panelBounds);
+    }
+
+    private void modelChanged(ListModel oldModel, ListModel newModel) {
+	if (oldModel != null) {
+	    oldModel.removeListDataListener(listModelListener);
+	}
+
+	if (newModel != null) {
+	    newModel.addListDataListener(listModelListener);
+	}
+    }
+
+    private void repaintPopups() {
+	if (getPopupsShowing()) {
+	    for (CellPanel cell : popups.values()) {
+		cell.repaint();
+	    }
+	}
+    }
+}