/* $Id: JXScrollPane.java,v 1.3 2009/02/10 14:55:00 ebenius Exp $ */
/* Copyright 2009 Sebastian Haufe
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
[url]http://www.apache.org/licenses/LICENSE-2.0[/url]
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */
package com.ebenius.widget;
import static java.awt.AWTEvent.MOUSE_EVENT_MASK;
import static java.awt.AWTEvent.MOUSE_MOTION_EVENT_MASK;
import static java.awt.event.InputEvent.BUTTON3_DOWN_MASK;
import static java.awt.event.MouseEvent.MOUSE_DRAGGED;
import static java.awt.event.MouseEvent.MOUSE_PRESSED;
import static java.awt.event.MouseEvent.MOUSE_RELEASED;
import java.awt.*;
import java.awt.event.AWTEventListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
/**
* A {@link JScrollPane} derivate, enhanced by mouse dragging support.
*
* @see #isViewDraggingEnabled()
* @see #isHorizontalViewDraggingEnabled()
* @see #isVerticalViewDraggingEnabled()
* @version $Revision: 1.3 $ as of $Date: 2009/02/10 14:55:00 $
* @author Sebastian Haufe
*/
public class JXScrollPane extends JScrollPane {
/** Serial version UID */
private static final long serialVersionUID = 4240385570313149664L;
private static ToolkitMouseListener multiListener = null;
// -------------------------------------------------------------------------
// Instance fields
// -------------------------------------------------------------------------
private Point draggingFrom;
private int viewDragModifierMask = BUTTON3_DOWN_MASK;
private boolean horizontalViewDraggingEnabled = true;
private boolean verticalViewDraggingEnabled = true;
// -------------------------------------------------------------------------
// Constructors
// -------------------------------------------------------------------------
/**
* Creates an empty (no viewport view) <code>JXScrollPane</code> where both
* horizontal and vertical scrollbars appear when needed.
*/
public JXScrollPane() {
super();
enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
}
/**
* Creates a <code>JXScrollPane</code> that displays the view component in a
* viewport whose view position can be controlled with a pair of scrollbars.
* The scrollbar policies specify when the scrollbars are displayed, For
* example, if <code>vsbPolicy</code> is
* <code>VERTICAL_SCROLLBAR_AS_NEEDED</code> then the vertical scrollbar
* only appears if the view doesn't fit vertically. The available policy
* settings are listed at {@link #setVerticalScrollBarPolicy} and
* {@link #setHorizontalScrollBarPolicy}.
*
* @see #setViewportView
* @param view the component to display in the scrollpane's viewport
* @param vsbPolicy an integer that specifies the vertical scrollbar policy
* @param hsbPolicy an integer that specifies the horizontal scrollbar
* policy
*/
public JXScrollPane(Component view, int vsbPolicy, int hsbPolicy) {
super(view, vsbPolicy, hsbPolicy);
enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
}
/**
* Creates a <code>JXScrollPane</code> that displays the contents of the
* specified component, where both horizontal and vertical scrollbars appear
* whenever the component's contents are larger than the view.
*
* @see #setViewportView
* @param view the component to display in the scrollpane's viewport
*/
public JXScrollPane(Component view) {
super(view);
enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
}
/**
* Creates an empty (no viewport view) <code>JXScrollPane</code> with
* specified scrollbar policies. The available policy settings are listed at
* {@link #setVerticalScrollBarPolicy} and
* {@link #setHorizontalScrollBarPolicy}.
*
* @see #setViewportView
* @param vsbPolicy an integer that specifies the vertical scrollbar policy
* @param hsbPolicy an integer that specifies the horizontal scrollbar
* policy
*/
public JXScrollPane(int vsbPolicy, int hsbPolicy) {
super(vsbPolicy, hsbPolicy);
enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
}
// -------------------------------------------------------------------------
// Viewport dragging support
// -------------------------------------------------------------------------
/**
* Determines whether the view is draggable based on the given mouse event.
* This implementation checks whether {@link #isViewDraggingEnabled()
* dragging is enabled} and the {@link #getViewDragModifierMask()} matches
* the {@link MouseEvent#getModifiersEx() modifiers} of the given mouse
* event.
* <p>
* The method is being called on {@link MouseEvent MOUSE_PRESSED} and
* {@link MouseEvent#MOUSE_DRAGGED} events to check whether the event is
* used or dismissed for view dragging.
*
* @param e the mouse event to inspect
* @return {@code true} if the mouse event is a view drag trigger
*/
protected boolean isViewDraggable(MouseEvent e) {
final int mask = viewDragModifierMask;
return isViewDraggingEnabled() && (mask & e.getModifiersEx()) == mask;
}
/**
* Determines whether the given point is in the viewport of {@code this}
* scroll pane.
*
* @param point the point to check
* @return {@code true} if the point is on the viewport
*/
protected boolean viewportContainsPoint(Point point) {
return getViewport().getBounds().contains(point);
}
/**
* Start dragging the view with the given mouse event.
*
* @param e the mouse event
*/
protected void startDraggingView(MouseEvent e) {
draggingFrom = e.getPoint();
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
multiListener.setCurrentDraggingSlave(this);
}
/**
* Stop dragging the view with the given mouse event.
*
* @param e the mouse event
*/
protected void stopDraggingView(MouseEvent e) {
multiListener.setCurrentDraggingSlave(null);
draggingFrom = null;
setCursor(null);
}
/**
* Drag the view according to the given mouse event.
*
* @param e the mouse event
*/
protected void dragView(MouseEvent e) {
final JViewport viewport = getViewport();
final Component view = viewport.getView();
final Point from;
/* currently not dragging or no viewport view; get out-a-here */
if ((from = draggingFrom) == null || view == null) {
return;
}
/* the amount of dragging */
final Point to = e.getPoint();
int dx = isHorizontalViewDraggingEnabled() ? from.x - to.x : 0;
int dy = isVerticalViewDraggingEnabled() ? from.y - to.y : 0;
// /* accelerate by the unit increment */
// if (view instanceof Scrollable) {
// final Scrollable scrollable = (Scrollable) view;
// final Rectangle viewRect = viewport.getViewRect();
// dx *= scrollable.getScrollableUnitIncrement(viewRect, dx, HORIZONTAL);
// dy *= scrollable.getScrollableUnitIncrement(viewRect, dy, VERTICAL);
// }
/* calculate the new view position */
final Point viewPos = viewport.getViewPosition();
final Dimension viewSize = viewport.getViewSize();
final Dimension extentSize = viewport.getExtentSize();
final int maxViewPosX = viewSize.width - extentSize.width;
final int maxViewPosY = viewSize.height - extentSize.height;
viewPos.x = Math.max(0, Math.min(viewPos.x + dx, maxViewPosX));
viewPos.y = Math.max(0, Math.min(viewPos.y + dy, maxViewPosY));
/* move the view and repaint the viewport */
viewport.setViewPosition(viewPos);
viewport.repaint();
draggingFrom = to;
}
// -------------------------------------------------------------------------
// Event dispatching
// -------------------------------------------------------------------------
@Override
protected void processMouseEvent(MouseEvent e) {
super.processMouseEvent(e);
processDragEvent(e);
}
@Override
protected void processMouseMotionEvent(MouseEvent e) {
super.processMouseMotionEvent(e);
processDragEvent(e);
}
protected void processDragEvent(MouseEvent e) {
switch (e.getID()) {
case MOUSE_DRAGGED:
if (draggingFrom != null && !e.isConsumed() && isViewDraggable(e)) {
dragView(e);
}
break;
case MOUSE_RELEASED:
stopDraggingView(e);
break;
case MOUSE_PRESSED:
if (!e.isConsumed()
&& isViewDraggable(e)
&& viewportContainsPoint(e.getPoint())) {
startDraggingView(e);
}
break;
}
}
// -------------------------------------------------------------------------
// Child listener support
// -------------------------------------------------------------------------
@Override
public void addNotify() {
synchronized (ToolkitMouseListener.class) {
if (multiListener == null) {
multiListener = new ToolkitMouseListener();
}
multiListener.addSlave(this);
}
super.addNotify();
}
@Override
public void removeNotify() {
super.removeNotify();
synchronized (ToolkitMouseListener.class) {
if (multiListener != null && !multiListener.removeSlave(this)) {
multiListener = null;
}
}
}
// -------------------------------------------------------------------------
// Bean getters and setters
// -------------------------------------------------------------------------
/**
* Returns the modifier mask to be checked for triggering view drag support.
* By default, this is the {@link InputEvent#BUTTON3_DOWN_MASK}.
*
* @return the modifier mask
*/
public int getViewDragModifierMask() {
return viewDragModifierMask;
}
/**
* Sets the modifier mask to be checked for triggering view drag support. By
* default, this is the {@link InputEvent#BUTTON3_DOWN_MASK}.
*
* @param modifierMask the modifier mask to set
*/
public void setViewDragModifierMask(int modifierMask) {
final int old = this.viewDragModifierMask;
this.viewDragModifierMask = modifierMask;
firePropertyChange("viewDragModifierMask", //$NON-NLS-1$
old, viewDragModifierMask);
}
/**
* Determines whether {@code this} scroll pane's view dragging is enabled in
* at least one direction. Defaults to {@code true}.
*
* @return {@code true} if view dragging is enabled
* @see #isHorizontalViewDraggingEnabled()
* @see #isVerticalViewDraggingEnabled()
*/
public boolean isViewDraggingEnabled() {
return horizontalViewDraggingEnabled || verticalViewDraggingEnabled;
}
/**
* Determines whether {@code this} scroll pane's view dragging is enabled in
* horizontal direction. Defaults to {@code true}.
*
* @return {@code true} if horizontal view dragging is enabled
*/
public boolean isHorizontalViewDraggingEnabled() {
return horizontalViewDraggingEnabled;
}
/**
* Sets whether {@code this} scroll pane's horizontal view dragging is
* enabled. Defaults to {@code true}.
*
* @param enabled {@code true} to enable view dragging horizontally
*/
public void setHorizontalViewDraggingEnabled(boolean enabled) {
final boolean old = this.horizontalViewDraggingEnabled;
this.horizontalViewDraggingEnabled = enabled;
firePropertyChange("horizontalViewDraggingEnabled", //$NON-NLS-1$
old, horizontalViewDraggingEnabled);
}
/**
* Determines whether {@code this} scroll pane's view dragging is enabled in
* vertical direction. Defaults to {@code true}.
*
* @return {@code true} if vertical view dragging is enabled
*/
public boolean isVerticalViewDraggingEnabled() {
return verticalViewDraggingEnabled;
}
/**
* Sets whether {@code this} scroll pane's vertical view dragging is
* enabled. Defaults to {@code true}.
*
* @param enabled {@code true} to enable view dragging vertically
*/
public void setVerticalViewDraggingEnabled(boolean enabled) {
final boolean old = this.verticalViewDraggingEnabled;
this.verticalViewDraggingEnabled = enabled;
firePropertyChange("horizontalViewDraggingEnabled", //$NON-NLS-1$
old, verticalViewDraggingEnabled);
}
// -------------------------------------------------------------------------
// Inner classes
// -------------------------------------------------------------------------
private static class ToolkitMouseListener implements AWTEventListener {
private final Set<JXScrollPane> slaves = new HashSet<JXScrollPane>();
private JXScrollPane currentDraggingSlave = null;
/** Creates a new {@code ToolkitMouseListener}. */
public ToolkitMouseListener() {}
public void eventDispatched(AWTEvent e) {
final JXScrollPane slave = currentDraggingSlave;
switch (e.getID()) {
case MOUSE_DRAGGED:
if (slave != null) {
slave.processDragEvent(convertMouseEvent((MouseEvent) e, slave));
}
break;
case MOUSE_RELEASED:
if (slave != null) {
slave.stopDraggingView(convertMouseEvent((MouseEvent) e, slave));
}
break;
case MOUSE_PRESSED:
redispatchDragEvent((MouseEvent) e);
break;
default:
return;
}
}
private void redispatchDragEvent(MouseEvent e) {
final Component src = (Component) e.getSource();
for (Component c = src.getParent(); c != null; c = c.getParent()) {
if (c instanceof JXScrollPane) {
((JXScrollPane) c).processDragEvent(convertMouseEvent(e, c));
break;
}
}
}
private MouseEvent convertMouseEvent(
MouseEvent e,
Component targetComponent) {
return SwingUtilities.convertMouseEvent((Component) e.getSource(), e,
targetComponent);
}
void addSlave(JXScrollPane slave) {
synchronized (ToolkitMouseListener.class) {
slaves.add(slave);
if (slaves.size() == 1) {
slave.getToolkit().addAWTEventListener(this, MOUSE_EVENT_MASK);
}
}
}
boolean removeSlave(JXScrollPane slave) {
synchronized (ToolkitMouseListener.class) {
slaves.remove(slave);
final boolean hasMoreSlaves = !slaves.isEmpty();
if (!hasMoreSlaves) {
slave.getToolkit().removeAWTEventListener(this);
}
return hasMoreSlaves;
}
}
void setCurrentDraggingSlave(JXScrollPane slave) {
synchronized (ToolkitMouseListener.class) {
final JXScrollPane oldSlave = currentDraggingSlave;
final Toolkit tk;
long mask;
if (slave != null) {
tk = slave.getToolkit();
mask = MOUSE_EVENT_MASK | MOUSE_MOTION_EVENT_MASK;
} else if (oldSlave != null) {
tk = oldSlave.getToolkit();
mask = MOUSE_EVENT_MASK;
} else {
return;
}
tk.removeAWTEventListener(this);
tk.addAWTEventListener(this, mask);
this.currentDraggingSlave = slave;
}
}
}
}