Skip to main content

EnhancedMouseScrollableUI

1 reply [Last post]
davide71
Offline
Joined: 2007-09-28

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...

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 instead of AbstractLayerUI

2. Added viewport instance variable

2. In processMouseEvent method the actual JViewport to be scrolled is now determined by the following code

<br />
    		Component comp = e.getComponent();<br />
    		comp = SwingUtilities.getDeepestComponentAt(comp, e.getX(), e<br />
    				.getY());<br />
    		viewport = (JViewport) SwingUtilities<br />
    				.getAncestorOfClass(JViewport.class, comp);<br />

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:

<br />
package org.jdesktop.jxlayer.plaf.ext;</p>
<p>import java.awt.AWTEvent;<br />
import java.awt.Component;<br />
import java.awt.Cursor;<br />
import java.awt.Dimension;<br />
import java.awt.Point;<br />
import java.awt.Rectangle;<br />
import java.awt.event.ActionEvent;<br />
import java.awt.event.ActionListener;<br />
import java.awt.event.ComponentAdapter;<br />
import java.awt.event.ComponentEvent;<br />
import java.awt.event.ComponentListener;<br />
import java.awt.event.InputEvent;<br />
import java.awt.event.MouseAdapter;<br />
import java.awt.event.MouseEvent;<br />
import java.awt.event.MouseListener;</p>
<p>import javax.swing.Icon;<br />
import javax.swing.ImageIcon;<br />
import javax.swing.JComponent;<br />
import javax.swing.JLabel;<br />
import javax.swing.JViewport;<br />
import javax.swing.Scrollable;<br />
import javax.swing.SwingUtilities;<br />
import javax.swing.Timer;</p>
<p>import org.jdesktop.jxlayer.JXLayer;<br />
import org.jdesktop.jxlayer.plaf.AbstractLayerUI;</p>
<p>/**<br />
 * The {@code MouseScrollableUI} provides the mouse auto-scrolling feature<br />
 * for your applications. Wrap your {@code JScrollPane} with a {@link JXLayer},<br />
 * set an instance of MouseScrollableUI as the {@code JXLayer}'s ui<br />
 * and after that, pressing the middle mouse button inside the {@code JScrollPane}<br />
 * will activate the auto-scrolling mode.<br />
 *<br />
 * Here is an example of using {@code MouseScrollableUI}:<br />
 * 
 * // a JScrollPane to be enhanced with mouse auto-scrolling
* JScrollPane myScrollPane = getMyScrollPane();
* <p/>
* JXLayer&lt;JScrollPane&gt; layer =
*          new JXLayer&lt;JScrollPane&gt;(myScrollPane, new MouseScrollableUI());
* <p/>
* // add the layer to a frame or a panel, like any other component
* frame.add(layer);
*
<br /> * The MouseScrollableDemo is<br /> * available<br /> */<br /> public class EnhancedMouseScrollableUI extends AbstractLayerUI<br /> implements ActionListener {</p> <p> private JXLayer currentLayer;<br /> private Point scrollOrigin;<br /> private Point mousePoint;<br /> private Timer timer;<br /> private JViewport viewport;</p> <p> private JLabel indicator;<br /> private Icon crissCrossIcon;<br /> private Icon horizontalIcon;<br /> private Icon verticalIcon;</p> <p> private boolean isAWTEventListenerEnabled;</p> <p> // It is used to make a component consume mouse events<br /> private final MouseListener emptyMouseListener = new MouseAdapter() {};</p> <p> private ComponentListener componentListener = new ComponentAdapter() {<br /> @SuppressWarnings("unchecked")<br /> public void componentResized(ComponentEvent e) {<br /> JXLayer layer = (JXLayer) e.getComponent();<br /> if (indicator.isShowing() && layer == currentLayer) {<br /> deactivateMouseScrolling(layer);<br /> }<br /> }<br /> };</p> <p> /**<br /> * Creates a new instance of {@code MouseScrollableUI}.<br /> */<br /> public EnhancedMouseScrollableUI() {<br /> this(false);<br /> }</p> <p> /**<br /> * Creates a new instance of {@code MouseScrollableUI}.<br /> *<br /> * @param isAWTEventListenerEnabled if {@code true} then the AWTEventListener<br /> * will be used to catch the AWT events<br /> */<br /> public EnhancedMouseScrollableUI(boolean isAWTEventListenerEnabled) {<br /> this.isAWTEventListenerEnabled = isAWTEventListenerEnabled;<br /> timer = new Timer(20, this);<br /> crissCrossIcon = new ImageIcon(getClass().getResource("images/criss-cross.png"));<br /> horizontalIcon = new ImageIcon(getClass().getResource("images/horizontal.png"));<br /> verticalIcon = new ImageIcon(getClass().getResource("images/vertical.png"));<br /> indicator = new JLabel(crissCrossIcon);<br /> // Make it consume mouse events<br /> indicator.addMouseListener(emptyMouseListener);<br /> }</p> <p> @Override<br /> public void installUI(JComponent c) {<br /> super.installUI(c);<br /> c.addComponentListener(componentListener);<br /> JXLayer l = (JXLayer) c;<br /> l.getGlassPane().setLayout(null);<br /> }</p> <p> @Override<br /> public void uninstallUI(JComponent c) {<br /> super.uninstallUI(c);<br /> c.removeComponentListener(componentListener);<br /> }</p> <p> /**<br /> * {@inheritDoc}<br /> */<br /> protected boolean isAWTEventListenerEnabled() {<br /> return isAWTEventListenerEnabled;<br /> }</p> <p> /**<br /> * {@inheritDoc}<br /> *<br /> * Deactivates the mouse scrolling for disabled {@code MouseScrollableUI}<br /> */<br /> @Override<br /> public void setEnabled(boolean enabled) {<br /> super.setEnabled(enabled);<br /> if (!enabled && indicator.isShowing()) {<br /> deactivateMouseScrolling(currentLayer);<br /> }<br /> }</p> <p> /**<br /> * {@inheritDoc}<br /> *<br /> * Activates the mouse auto-scrolling if {@code e} is a scrolling trigger.<br /> *<br /> * @see #isMouseScrollingTrigger(MouseEvent)<br /> */<br /> @Override<br /> protected void processMouseEvent(MouseEvent e, JXLayer l) {<br /> super.processMouseEvent(e, l);</p> <p> if (isMouseScrollingTrigger(e) ) {<br /> Component comp = SwingUtilities.getDeepestComponentAt(e.getComponent(), e.getX(), e.getY());<br /> viewport = (JViewport) SwingUtilities<br /> .getAncestorOfClass(JViewport.class, comp);<br /> if (viewport != null && canScroll(viewport)) {<br /> currentLayer = l;<br /> scrollOrigin = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(),<br /> currentLayer);<br /> activateMouseScrolling(scrollOrigin, currentLayer);<br /> mousePoint = scrollOrigin;<br /> e.consume();<br /> }<br /> }<br /> }</p> <p> /**<br /> * Returns {@code true} if {@code mouseEvent} is a mouse auto-scrolling<br /> * trigger.<br /> *<br /> * @param mouseEvent<br /> * the mouseEvent to be tested<br /> * @return {@code true} if {@code mouseEvent} is a mouse auto-scrolling<br /> * trigger, otherwise returns {@code false}<br /> */<br /> protected boolean isMouseScrollingTrigger(MouseEvent mouseEvent) {<br /> return mouseEvent.getID() == MouseEvent.MOUSE_PRESSED<br /> && mouseEvent.getButton() == MouseEvent.BUTTON2<br /> && !mouseEvent.isPopupTrigger();<br /> }</p> <p> /**<br /> * {@inheritDoc}<br /> *<br /> * Scrolls the {@code JXLayer}'s view {@code JScrollPane}<br /> * if mouse scrolling is activated<br /> */<br /> @Override<br /> protected void processMouseMotionEvent(MouseEvent e, JXLayer l) {<br /> super.processMouseMotionEvent(e, l);<br /> if (indicator.isShowing()) {<br /> mousePoint = SwingUtilities.convertPoint(e.getComponent(),<br /> e.getPoint(), currentLayer);<br /> updateViewPosition(scrollOrigin, mousePoint, currentLayer);<br /> }<br /> }</p> <p> private void updateViewPosition(Point scrollOrigin, Point mousePoint,<br /> JXLayer layer) {<br /> Point indicatorPoint =<br /> SwingUtilities.convertPoint(layer, mousePoint, indicator);<br /> if (!indicator.contains(indicatorPoint)) {<br /> Point viewPosition = viewport.getViewPosition();<br /> int x = mousePoint.x - scrollOrigin.x;<br /> int y = mousePoint.y - scrollOrigin.y;<br /> // make the scrolling more smooth<br /> viewPosition.x += x / 5;<br /> viewPosition.y += y / 5;<br /> if (viewPosition.x > viewport.getView().getWidth() - viewport.getWidth()) {<br /> viewPosition.x = viewport.getView().getWidth() - viewport.getWidth();<br /> }<br /> if (viewPosition.x < 0) {<br /> viewPosition.x = 0;<br /> }<br /> if (viewPosition.y > viewport.getView().getHeight() - viewport.getHeight()) {<br /> viewPosition.y = viewport.getView().getHeight() - viewport.getHeight();<br /> }<br /> if (viewPosition.y < 0) {<br /> viewPosition.y = 0;<br /> }<br /> viewport.setViewPosition(viewPosition);<br /> }<br /> }</p> <p> /**<br /> * {@inheritDoc}<br /> *<br /> * Preprocesses the {@code AWTEvent} to deactivate mouse scrolling<br /> * when any key was pressed or focus left the {@code JXLayer}.<br /> */<br /> @Override<br /> public void eventDispatched(AWTEvent e, JXLayer l) {<br /> if (indicator.isShowing()) {<br /> if (e instanceof MouseEvent) {<br /> MouseEvent mouseEvent = (MouseEvent) e;<br /> if (mouseEvent.getID() != MouseEvent.MOUSE_WHEEL<br /> && mouseEvent.getID() != MouseEvent.MOUSE_PRESSED<br /> && (mouseEvent.getID() != MouseEvent.MOUSE_RELEASED<br /> && mouseEvent.getID() != MouseEvent.MOUSE_CLICKED<br /> || scrollOrigin.equals(mousePoint))) {<br /> super.eventDispatched(e, l);<br /> return;<br /> }<br /> }<br /> deactivateMouseScrolling(currentLayer);<br /> if (e instanceof InputEvent) {<br /> InputEvent inputEvent = (InputEvent) e;<br /> inputEvent.consume();<br /> }<br /> } else {<br /> super.eventDispatched(e, l);<br /> }<br /> }</p> <p> public void actionPerformed(ActionEvent e) {<br /> updateViewPosition(scrollOrigin, mousePoint, currentLayer);<br /> }</p> <p> private void activateMouseScrolling(Point point, JXLayer layer) {<br /> layer.getGlassPane().addMouseListener(emptyMouseListener);<br /> layer.getGlassPane().add(indicator);<br /> Dimension prefSize = indicator.getPreferredSize();<br /> indicator.setBounds(point.x - prefSize.width / 2,<br /> point.y - prefSize.height / 2,<br /> prefSize.width, prefSize.height);<br /> updateIndicator(layer);<br /> layer.getGlassPane().repaint();<br /> timer.start();<br /> }</p> <p> private void updateIndicator(JXLayer layer) {<br /> Dimension extentSize = viewport.getExtentSize();</p> <p> Cursor cursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);<br /> Icon icon = crissCrossIcon;<br /> if (extentSize.width >= viewport.getView().getWidth()<br /> && extentSize.height >= viewport.getView().getHeight()<br /> || extentSize.width < viewport.getView().getWidth()<br /> && extentSize.height < viewport.getView().getHeight()) {<br /> cursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);<br /> icon = crissCrossIcon;<br /> } else if (extentSize.width < viewport.getView().getWidth()) {<br /> cursor = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);<br /> icon = horizontalIcon;<br /> } else if (extentSize.height < viewport.getView().getHeight()) {<br /> cursor = Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);<br /> icon = verticalIcon;<br /> }<br /> indicator.setCursor(cursor);<br /> indicator.setIcon(icon);<br /> }</p> <p> private void deactivateMouseScrolling(JXLayer layer) {<br /> layer.getGlassPane().removeMouseListener(emptyMouseListener);<br /> indicator.setCursor(null);<br /> layer.getGlassPane().remove(indicator);<br /> layer.getGlassPane().repaint();<br /> timer.stop();<br /> }</p> <p> public static boolean canScrollHorizontally(JViewport viewport) {<br /> Rectangle availR = viewport.getBounds();<br /> Component view = viewport.getView();<br /> Dimension viewPrefSize = view != null ? view.getPreferredSize()<br /> : new Dimension(0, 0);<br /> Dimension extentSize = viewport.toViewCoordinates(availR.getSize());</p> <p> boolean canHScroll = true;<br /> if (view instanceof Scrollable)<br /> canHScroll = !((Scrollable) view)<br /> .getScrollableTracksViewportWidth();<br /> if (canHScroll && (viewPrefSize.width <= extentSize.width))<br /> canHScroll = false;</p> <p> return canHScroll;<br /> }</p> <p> public static boolean canScrollVertically(JViewport viewport) {<br /> Rectangle availR = viewport.getBounds();<br /> Component view = viewport.getView();<br /> Dimension viewPrefSize = view != null ? view.getPreferredSize()<br /> : new Dimension(0, 0);<br /> Dimension extentSize = viewport.toViewCoordinates(availR.getSize());<br /> boolean canVScroll = true;<br /> if (view instanceof Scrollable)<br /> canVScroll = !((Scrollable) view)<br /> .getScrollableTracksViewportHeight();<br /> if (canVScroll && (viewPrefSize.height <= extentSize.height))<br /> canVScroll = false;</p> <p> return canVScroll;<br /> }</p> <p> public static boolean canScroll(JViewport viewport) {<br /> return canScrollHorizontally(viewport) || canScrollVertically(viewport);<br /> }<br /> }<br />

And here is the demo (note the left scroll pane never shows scrollbars but still remains autoscrollable):

<br />
package org.jdesktop.jxlayer.demo;</p>
<p>import java.awt.GridLayout;<br />
import java.awt.event.InputEvent;<br />
import java.awt.event.ItemEvent;<br />
import java.awt.event.ItemListener;<br />
import java.awt.event.KeyEvent;</p>
<p>import javax.swing.JCheckBox;<br />
import javax.swing.JCheckBoxMenuItem;<br />
import javax.swing.JComponent;<br />
import javax.swing.JFrame;<br />
import javax.swing.JMenu;<br />
import javax.swing.JMenuBar;<br />
import javax.swing.JPanel;<br />
import javax.swing.JRadioButton;<br />
import javax.swing.JScrollPane;<br />
import javax.swing.JSplitPane;<br />
import javax.swing.JTable;<br />
import javax.swing.JTextField;<br />
import javax.swing.KeyStroke;<br />
import javax.swing.SwingUtilities;<br />
import javax.swing.table.AbstractTableModel;</p>
<p>import org.jdesktop.jxlayer.JXLayer;<br />
import org.jdesktop.jxlayer.demo.util.LafMenu;<br />
import org.jdesktop.jxlayer.plaf.ext.EnhancedMouseScrollableUI;<br />
import org.jdesktop.jxlayer.plaf.ext.MouseScrollableUI;</p>
<p>/**<br />
 * A demo to show the abilities of the {@link MouseScrollableUI}.<br />
 * Click the mouse wheel button inside any of JScrollPanes to check it out.<br />
 */<br />
public class EnhancedMouseScrollableDemo extends JFrame {</p>
<p>    private EnhancedMouseScrollableUI mouseScrollableUI = new EnhancedMouseScrollableUI();</p>
<p>    public EnhancedMouseScrollableDemo() {<br />
        super("JXLayer MouseScrollableDemo");<br />
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);</p>
<p>        JMenuBar bar = new JMenuBar();</p>
<p>        JMenu optionsMenu = new JMenu("Options");<br />
        final JCheckBoxMenuItem disableMouseScrollableItem = new JCheckBoxMenuItem("Disable mouse scrolling");<br />
        disableMouseScrollableItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.ALT_MASK));<br />
        disableMouseScrollableItem.addItemListener(new ItemListener() {<br />
            public void itemStateChanged(ItemEvent e) {<br />
            	mouseScrollableUI.setEnabled(!disableMouseScrollableItem.isSelected());<br />
            }<br />
        });<br />
        optionsMenu.add(disableMouseScrollableItem);<br />
        bar.add(optionsMenu);<br />
        bar.add(new LafMenu());<br />
        setJMenuBar(bar);</p>
<p>        JSplitPane splitPane = new JSplitPane();<br />
        splitPane.setLeftComponent(createLeftScrollPane());<br />
        splitPane.setRightComponent(createRightScrollPane());<br />
        splitPane.setDividerLocation(330);</p>
<p>        add(new JXLayer(splitPane,mouseScrollableUI));<br />
        setSize(800, 600);<br />
        setLocationRelativeTo(null);<br />
    }</p>
<p>    private JScrollPane createLeftScrollPane() {<br />
        JPanel panel = new JPanel(new GridLayout(0, 3));<br />
        for (int i = 0; i < 25; i++) {<br />
            panel.add(new JTextField(8));<br />
            panel.add(new JPanel());<br />
            panel.add(new JCheckBox("JCheckBox"));<br />
            panel.add(new JRadioButton("JRadioButton"));<br />
        }<br />
        return new JScrollPane(panel,JScrollPane.VERTICAL_SCROLLBAR_NEVER,JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);<br />
    }</p>
<p>    private JScrollPane createRightScrollPane() {<br />
        JTable table = new JTable(new AbstractTableModel() {<br />
            public int getRowCount() {<br />
                return 50;<br />
            }</p>
<p>            public int getColumnCount() {<br />
                return 20;<br />
            }</p>
<p>            public Object getValueAt(int rowIndex, int columnIndex) {<br />
                return "" + rowIndex + " " + columnIndex;<br />
            }</p>
<p>            public boolean isCellEditable(int rowIndex, int columnIndex) {<br />
                return true;<br />
            }<br />
        });<br />
        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);<br />
        return new JScrollPane(table);<br />
    }</p>
<p>    public static void main(String[] args) throws Exception {<br />
        SwingUtilities.invokeLater(new Runnable() {<br />
            public void run() {<br />
                new EnhancedMouseScrollableDemo().setVisible(true);<br />
            }<br />
        });<br />
    }<br />
}<br />

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
alexfromsun
Offline
Joined: 2005-09-05

Hello David

I remember your comments, they helped me to create MouseScrollableUI.
Your suggestion #3 (about scrollbars) is really valuable, I'll fix it soon.

The EnhancedMouseScrollableUI overall is an interesting approach,
but to tell the truth I have always been reluctant to such general solutions
when you change a glassPane of the frame or substitute its contentPane.

I admit that it may be more convenient to wrap it only once
rather than wrap each JScrollPane, if you have many of them in the frame.

I can see that you use SwingUtilities.getDeepestComponentAt() method
for finding a component which was clicked, this method is known to be unreliable
for this kind of job. Could you check if EnhancedMouseScrollableUI works when frame's glassPane is visible?

Thanks
alexp