Hi Alex, I'm really impressed by your MouseScrollableUI. It's just what we were talking about 1 year ago. See comments in your blog http://weblogs.java.net/blog/alexfromsun/archive/2007/11/debug_swing_rep.html#comments
Now I've enhanced your MouseScrollableUI so that it's easier to apply this feature to an existing application without wrapping each JScrollPane with JXLayer. Now you can simply wrap the content pane of the application's frame and each JViewport will be autoscrollable.
Major changes to the code:
1. MouseScrollableUI extends AbstractLayerUI<JComponent> instead of AbstractLayerUI<JScollPane>
2. Added viewport instance variable
2. In processMouseEvent method the actual JViewport to be scrolled is now determined by the following code
Component comp = e.getComponent();
comp = SwingUtilities.getDeepestComponentAt(comp, e.getX(), e
.getY());
viewport = (JViewport) SwingUtilities
.getAncestorOfClass(JViewport.class, comp);
3. Using scrollbars visibility to enable/disable auto scrolling doesn't work if the user set scrollbar policy to show scrollbars never. It can be avoided by comparing view preferred size and view extent size
Here is the code for EnhancedMouseScrollableUI:
package org.jdesktop.jxlayer.plaf.ext;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.AbstractLayerUI;
/**
* The {@code MouseScrollableUI} provides the mouse auto-scrolling feature
* for your applications. Wrap your {@code JScrollPane} with a {@link JXLayer},
* set an instance of MouseScrollableUI as the {@code JXLayer}'s ui
* and after that, pressing the middle mouse button inside the {@code JScrollPane}
* will activate the auto-scrolling mode.
* <p/>
* Here is an example of using {@code MouseScrollableUI}:
* <pre>
* // a JScrollPane to be enhanced with mouse auto-scrolling
* JScrollPane myScrollPane = getMyScrollPane();
* <p/>
* JXLayer<JScrollPane> layer =
* new JXLayer<JScrollPane>(myScrollPane, new MouseScrollableUI());
* <p/>
* // add the layer to a frame or a panel, like any other component
* frame.add(layer);
* </pre>
* The MouseScrollableDemo is
* <a href="https://jxlayer.dev.java.net/source/browse/jxlayer/trunk/src/demo/org/jdesktop/jxlayer/demo/MouseScrollableDemo.java?view=markup">available</a>
*/
public class EnhancedMouseScrollableUI extends AbstractLayerUI<JComponent>
implements ActionListener {
private JXLayer<JComponent> currentLayer;
private Point scrollOrigin;
private Point mousePoint;
private Timer timer;
private JViewport viewport;
private JLabel indicator;
private Icon crissCrossIcon;
private Icon horizontalIcon;
private Icon verticalIcon;
private boolean isAWTEventListenerEnabled;
// It is used to make a component consume mouse events
private final MouseListener emptyMouseListener = new MouseAdapter() {};
private ComponentListener componentListener = new ComponentAdapter() {
@SuppressWarnings("unchecked")
public void componentResized(ComponentEvent e) {
JXLayer<JComponent> layer = (JXLayer<JComponent>) e.getComponent();
if (indicator.isShowing() && layer == currentLayer) {
deactivateMouseScrolling(layer);
}
}
};
/**
* Creates a new instance of {@code MouseScrollableUI}.
*/
public EnhancedMouseScrollableUI() {
this(false);
}
/**
* Creates a new instance of {@code MouseScrollableUI}.
*
* @param isAWTEventListenerEnabled if {@code true} then the AWTEventListener
* will be used to catch the AWT events
*/
public EnhancedMouseScrollableUI(boolean isAWTEventListenerEnabled) {
this.isAWTEventListenerEnabled = isAWTEventListenerEnabled;
timer = new Timer(20, this);
crissCrossIcon = new ImageIcon(getClass().getResource("images/criss-cross.png"));
horizontalIcon = new ImageIcon(getClass().getResource("images/horizontal.png"));
verticalIcon = new ImageIcon(getClass().getResource("images/vertical.png"));
indicator = new JLabel(crissCrossIcon);
// Make it consume mouse events
indicator.addMouseListener(emptyMouseListener);
}
@Override
public void installUI(JComponent c) {
super.installUI(c);
c.addComponentListener(componentListener);
JXLayer l = (JXLayer) c;
l.getGlassPane().setLayout(null);
}
@Override
public void uninstallUI(JComponent c) {
super.uninstallUI(c);
c.removeComponentListener(componentListener);
}
/**
* {@inheritDoc}
*/
protected boolean isAWTEventListenerEnabled() {
return isAWTEventListenerEnabled;
}
/**
* {@inheritDoc}
* <p/>
* Deactivates the mouse scrolling for disabled {@code MouseScrollableUI}
*/
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
if (!enabled && indicator.isShowing()) {
deactivateMouseScrolling(currentLayer);
}
}
/**
* {@inheritDoc}
* <p/>
* Activates the mouse auto-scrolling if {@code e} is a scrolling trigger.
*
* @see #isMouseScrollingTrigger(MouseEvent)
*/
@Override
protected void processMouseEvent(MouseEvent e, JXLayer<JComponent> l) {
super.processMouseEvent(e, l);
if (isMouseScrollingTrigger(e) ) {
Component comp = SwingUtilities.getDeepestComponentAt(e.getComponent(), e.getX(), e.getY());
viewport = (JViewport) SwingUtilities
.getAncestorOfClass(JViewport.class, comp);
if (viewport != null && canScroll(viewport)) {
currentLayer = l;
scrollOrigin = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(),
currentLayer);
activateMouseScrolling(scrollOrigin, currentLayer);
mousePoint = scrollOrigin;
e.consume();
}
}
}
/**
* Returns {@code true} if {@code mouseEvent} is a mouse auto-scrolling
* trigger.
*
* @param mouseEvent
* the mouseEvent to be tested
* @return {@code true} if {@code mouseEvent} is a mouse auto-scrolling
* trigger, otherwise returns {@code false}
*/
protected boolean isMouseScrollingTrigger(MouseEvent mouseEvent) {
return mouseEvent.getID() == MouseEvent.MOUSE_PRESSED
&& mouseEvent.getButton() == MouseEvent.BUTTON2
&& !mouseEvent.isPopupTrigger();
}
/**
* {@inheritDoc}
* <p/>
* Scrolls the {@code JXLayer}'s view {@code JScrollPane}
* if mouse scrolling is activated
*/
@Override
protected void processMouseMotionEvent(MouseEvent e, JXLayer<JComponent> l) {
super.processMouseMotionEvent(e, l);
if (indicator.isShowing()) {
mousePoint = SwingUtilities.convertPoint(e.getComponent(),
e.getPoint(), currentLayer);
updateViewPosition(scrollOrigin, mousePoint, currentLayer);
}
}
private void updateViewPosition(Point scrollOrigin, Point mousePoint,
JXLayer<JComponent> layer) {
Point indicatorPoint =
SwingUtilities.convertPoint(layer, mousePoint, indicator);
if (!indicator.contains(indicatorPoint)) {
Point viewPosition = viewport.getViewPosition();
int x = mousePoint.x - scrollOrigin.x;
int y = mousePoint.y - scrollOrigin.y;
// make the scrolling more smooth
viewPosition.x += x / 5;
viewPosition.y += y / 5;
if (viewPosition.x > viewport.getView().getWidth() - viewport.getWidth()) {
viewPosition.x = viewport.getView().getWidth() - viewport.getWidth();
}
if (viewPosition.x < 0) {
viewPosition.x = 0;
}
if (viewPosition.y > viewport.getView().getHeight() - viewport.getHeight()) {
viewPosition.y = viewport.getView().getHeight() - viewport.getHeight();
}
if (viewPosition.y < 0) {
viewPosition.y = 0;
}
viewport.setViewPosition(viewPosition);
}
}
/**
* {@inheritDoc}
* <p/>
* Preprocesses the {@code AWTEvent} to deactivate mouse scrolling
* when any key was pressed or focus left the {@code JXLayer}.
*/
@Override
public void eventDispatched(AWTEvent e, JXLayer<JComponent> l) {
if (indicator.isShowing()) {
if (e instanceof MouseEvent) {
MouseEvent mouseEvent = (MouseEvent) e;
if (mouseEvent.getID() != MouseEvent.MOUSE_WHEEL
&& mouseEvent.getID() != MouseEvent.MOUSE_PRESSED
&& (mouseEvent.getID() != MouseEvent.MOUSE_RELEASED
&& mouseEvent.getID() != MouseEvent.MOUSE_CLICKED
|| scrollOrigin.equals(mousePoint))) {
super.eventDispatched(e, l);
return;
}
}
deactivateMouseScrolling(currentLayer);
if (e instanceof InputEvent) {
InputEvent inputEvent = (InputEvent) e;
inputEvent.consume();
}
} else {
super.eventDispatched(e, l);
}
}
public void actionPerformed(ActionEvent e) {
updateViewPosition(scrollOrigin, mousePoint, currentLayer);
}
private void activateMouseScrolling(Point point, JXLayer<JComponent> layer) {
layer.getGlassPane().addMouseListener(emptyMouseListener);
layer.getGlassPane().add(indicator);
Dimension prefSize = indicator.getPreferredSize();
indicator.setBounds(point.x - prefSize.width / 2,
point.y - prefSize.height / 2,
prefSize.width, prefSize.height);
updateIndicator(layer);
layer.getGlassPane().repaint();
timer.start();
}
private void updateIndicator(JXLayer<JComponent> layer) {
Dimension extentSize = viewport.getExtentSize();
Cursor cursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
Icon icon = crissCrossIcon;
if (extentSize.width >= viewport.getView().getWidth()
&& extentSize.height >= viewport.getView().getHeight()
|| extentSize.width < viewport.getView().getWidth()
&& extentSize.height < viewport.getView().getHeight()) {
cursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
icon = crissCrossIcon;
} else if (extentSize.width < viewport.getView().getWidth()) {
cursor = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
icon = horizontalIcon;
} else if (extentSize.height < viewport.getView().getHeight()) {
cursor = Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
icon = verticalIcon;
}
indicator.setCursor(cursor);
indicator.setIcon(icon);
}
private void deactivateMouseScrolling(JXLayer<JComponent> layer) {
layer.getGlassPane().removeMouseListener(emptyMouseListener);
indicator.setCursor(null);
layer.getGlassPane().remove(indicator);
layer.getGlassPane().repaint();
timer.stop();
}
public static boolean canScrollHorizontally(JViewport viewport) {
Rectangle availR = viewport.getBounds();
Component view = viewport.getView();
Dimension viewPrefSize = view != null ? view.getPreferredSize()
: new Dimension(0, 0);
Dimension extentSize = viewport.toViewCoordinates(availR.getSize());
boolean canHScroll = true;
if (view instanceof Scrollable)
canHScroll = !((Scrollable) view)
.getScrollableTracksViewportWidth();
if (canHScroll && (viewPrefSize.width <= extentSize.width))
canHScroll = false;
return canHScroll;
}
public static boolean canScrollVertically(JViewport viewport) {
Rectangle availR = viewport.getBounds();
Component view = viewport.getView();
Dimension viewPrefSize = view != null ? view.getPreferredSize()
: new Dimension(0, 0);
Dimension extentSize = viewport.toViewCoordinates(availR.getSize());
boolean canVScroll = true;
if (view instanceof Scrollable)
canVScroll = !((Scrollable) view)
.getScrollableTracksViewportHeight();
if (canVScroll && (viewPrefSize.height <= extentSize.height))
canVScroll = false;
return canVScroll;
}
public static boolean canScroll(JViewport viewport) {
return canScrollHorizontally(viewport) || canScrollVertically(viewport);
}
}
And here is the demo (note the left scroll pane never shows scrollbars but still remains autoscrollable):
package org.jdesktop.jxlayer.demo;
import java.awt.GridLayout;
import java.awt.event.InputEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.demo.util.LafMenu;
import org.jdesktop.jxlayer.plaf.ext.EnhancedMouseScrollableUI;
import org.jdesktop.jxlayer.plaf.ext.MouseScrollableUI;
/**
* A demo to show the abilities of the {@link MouseScrollableUI}.
* Click the mouse wheel button inside any of JScrollPanes to check it out.
*/
public class EnhancedMouseScrollableDemo extends JFrame {
private EnhancedMouseScrollableUI mouseScrollableUI = new EnhancedMouseScrollableUI();
public EnhancedMouseScrollableDemo() {
super("JXLayer MouseScrollableDemo");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar bar = new JMenuBar();
JMenu optionsMenu = new JMenu("Options");
final JCheckBoxMenuItem disableMouseScrollableItem = new JCheckBoxMenuItem("Disable mouse scrolling");
disableMouseScrollableItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.ALT_MASK));
disableMouseScrollableItem.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
mouseScrollableUI.setEnabled(!disableMouseScrollableItem.isSelected());
}
});
optionsMenu.add(disableMouseScrollableItem);
bar.add(optionsMenu);
bar.add(new LafMenu());
setJMenuBar(bar);
JSplitPane splitPane = new JSplitPane();
splitPane.setLeftComponent(createLeftScrollPane());
splitPane.setRightComponent(createRightScrollPane());
splitPane.setDividerLocation(330);
add(new JXLayer<JComponent>(splitPane,mouseScrollableUI));
setSize(800, 600);
setLocationRelativeTo(null);
}
private JScrollPane createLeftScrollPane() {
JPanel panel = new JPanel(new GridLayout(0, 3));
for (int i = 0; i < 25; i++) {
panel.add(new JTextField(8));
panel.add(new JPanel());
panel.add(new JCheckBox("JCheckBox"));
panel.add(new JRadioButton("JRadioButton"));
}
return new JScrollPane(panel,JScrollPane.VERTICAL_SCROLLBAR_NEVER,JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
}
private JScrollPane createRightScrollPane() {
JTable table = new JTable(new AbstractTableModel() {
public int getRowCount() {
return 50;
}
public int getColumnCount() {
return 20;
}
public Object getValueAt(int rowIndex, int columnIndex) {
return "" + rowIndex + " " + columnIndex;
}
public boolean isCellEditable(int rowIndex, int columnIndex) {
return true;
}
});
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
return new JScrollPane(table);
}
public static void main(String[] args) throws Exception {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new EnhancedMouseScrollableDemo().setVisible(true);
}
});
}
}
|