blob: 9dc2c15f4236a2c8e0d4adb4266567dacc912b2e [file] [log] [blame]
/*
Copyright 2000-2003 The Apache Software Foundation
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
http://www.apache.org/licenses/LICENSE-2.0
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 org.apache.batik.dom.events;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import org.apache.batik.dom.AbstractDocument;
import org.apache.batik.dom.AbstractNode;
import org.apache.batik.dom.util.HashTable;
import org.w3c.dom.DOMException;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventException;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
/**
* The class allows registration and removal of EventListeners on
* an NodeEventTarget and dispatch of events to that NodeEventTarget.
*
* @see NodeEventTarget
* @author <a href="mailto:Thierry.Kormann@sophia.inria.fr">Thierry Kormann</a>
* @author <a href="mailto:stephane@hillion.org">Stephane Hillion</a>
* @version $Id$
*/
public class EventSupport {
/**
* The capturing listeners table.
*/
protected HashTable capturingListeners;
/**
* The bubbling listeners table.
*/
protected HashTable bubblingListeners;
/**
* The node for which events are being handled.
*/
protected AbstractNode node;
/**
* Creates a new EventSupport object.
* @param n the node for which events are being handled
*/
public EventSupport(AbstractNode n) {
node = n;
}
/**
* This method allows the registration of event listeners on the
* event target. If an <code>EventListener</code> is added to an
* <code>EventTarget</code> which is currently processing an event
* the new listener will not be triggered by the current event.
* <br> If multiple identical <code>EventListener</code>s are
* registered on the same <code>EventTarget</code> with the same
* parameters the duplicate instances are discarded. They do not
* cause the <code>EventListener</code> to be called twice and
* since they are discarded they do not need to be removed with
* the <code>removeEventListener</code> method.
*
* @param type The event type for which the user is registering
*
* @param listener The <code>listener</code> parameter takes an
* interface implemented by the user which contains the methods to
* be called when the event occurs.
*
* @param useCapture If true, <code>useCapture</code> indicates
* that the user wishes to initiate capture. After initiating
* capture, all events of the specified type will be dispatched to
* the registered <code>EventListener</code> before being
* dispatched to any <code>EventTargets</code> beneath them in the
* tree. Events which are bubbling upward through the tree will
* not trigger an <code>EventListener</code> designated to use
* capture.
*/
public void addEventListener(String type, EventListener listener,
boolean useCapture) {
addEventListenerNS(null, type, null, listener, useCapture);
}
/**
* Registers an event listener for the given namespaced event type
* in the specified group.
*/
public void addEventListenerNS(String namespaceURI,
String type,
Object group,
EventListener listener,
boolean useCapture) {
HashTable listeners;
if (useCapture) {
if (capturingListeners == null) {
capturingListeners = new HashTable();
}
listeners = capturingListeners;
} else {
if (bubblingListeners == null) {
bubblingListeners = new HashTable();
}
listeners = bubblingListeners;
}
EventListenerList list = (EventListenerList) listeners.get(type);
if (list == null) {
list = new EventListenerList();
listeners.put(type, list);
}
list.addListener(namespaceURI, group, listener);
}
/**
* This method allows the removal of event listeners from the
* event target. If an <code>EventListener</code> is removed from
* an <code>EventTarget</code> while it is processing an event, it
* will complete its current actions but will not be triggered
* again during any later stages of event flow. <br>If an
* <code>EventListener</code> is removed from an
* <code>EventTarget</code> which is currently processing an event
* the removed listener will still be triggered by the current
* event. <br>Calling <code>removeEventListener</code> with
* arguments which do not identify any currently registered
* <code>EventListener</code> on the <code>EventTarget</code> has
* no effect.
*
* @param type Specifies the event type of the
* <code>EventListener</code> being removed.
*
* @param listener The <code>EventListener</code> parameter
* indicates the <code>EventListener </code> to be removed.
*
* @param useCapture Specifies whether the
* <code>EventListener</code> being removed was registered as a
* capturing listener or not. If a listener was registered twice,
* one with capture and one without, each must be removed
* separately. Removal of a capturing listener does not affect a
* non-capturing version of the same listener, and vice versa.
*/
public void removeEventListener(String type, EventListener listener,
boolean useCapture) {
removeEventListener(null, type, listener, useCapture);
}
/**
* Deregisters an event listener.
*/
public void removeEventListener(String namespaceURI,
String type,
EventListener listener,
boolean useCapture) {
HashTable listeners;
if (useCapture) {
listeners = capturingListeners;
} else {
listeners = bubblingListeners;
}
if (listeners == null) {
return;
}
EventListenerList list = (EventListenerList) listeners.get(type);
if (list != null) {
list.removeListener(namespaceURI, listener);
if (list.size() == 0) {
listeners.remove(type);
}
}
}
/**
* Moves all of the event listeners from this EventSupport object
* to the given EventSupport object.
* Used by {@link
* org.apache.batik.dom.AbstractDocument#renameNode(String,String,Node)}.
*/
public void moveEventListeners(EventSupport other) {
other.capturingListeners = capturingListeners;
other.bubblingListeners = bubblingListeners;
capturingListeners = null;
bubblingListeners = null;
}
/**
* This method allows the dispatch of events into the
* implementations event model. Events dispatched in this manner
* will have the same capturing and bubbling behavior as events
* dispatched directly by the implementation. The target of the
* event is the <code> EventTarget</code> on which
* <code>dispatchEvent</code> is called.
*
* @param target the target node
* @param e Specifies the event type, behavior, and contextual
* information to be used in processing the event.
*
* @return The return value of <code>dispatchEvent</code>
* indicates whether any of the listeners which handled the event
* called <code>preventDefault</code>. If
* <code>preventDefault</code> was called the value is false, else
* the value is true.
*
* @exception EventException
* UNSPECIFIED_EVENT_TYPE_ERR: Raised if the
* <code>Event</code>'s type was not specified by initializing
* the event before <code>dispatchEvent</code> was
* called. Specification of the <code>Event</code>'s type as
* <code>null</code> or an empty string will also trigger this
* exception.
*/
public boolean dispatchEvent(NodeEventTarget target, Event e)
throws EventException {
if (e == null) {
return false;
}
AbstractEvent aevt = null;
CustomEvent ce = null;
boolean isCustom;
if (e instanceof CustomEvent) {
isCustom = true;
ce = (CustomEvent) ce;
} else if (e instanceof org.w3c.dom.events.CustomEvent) {
isCustom = true;
ce = new WrappedEvent((org.w3c.dom.events.CustomEvent) e);
} else if (e instanceof AbstractEvent) {
isCustom = false;
aevt = (AbstractEvent) e;
} else {
throw createEventException
(DOMException.NOT_SUPPORTED_ERR,
"unsupported.event",
new Object[] {});
}
String type = e.getType();
if (type == null || type.length() == 0) {
throw createEventException
(EventException.UNSPECIFIED_EVENT_TYPE_ERR,
"unspecified.event",
new Object[] {});
}
// fix event status
if (!isCustom) {
aevt.setTarget(target);
aevt.stopPropagation(false);
aevt.stopImmediatePropagation(false);
aevt.preventDefault(false);
}
// dump the tree hierarchy from top to the target
NodeEventTarget [] ancestors = getAncestors(target);
// CAPTURING_PHASE : fire event listeners from top to EventTarget
if (!isCustom) {
aevt.setEventPhase(Event.CAPTURING_PHASE);
}
HashSet stoppedGroups = new HashSet();
HashSet toBeStoppedGroups = new HashSet();
for (int i = 0; i < ancestors.length; i++) {
NodeEventTarget node = ancestors[i];
if (isCustom) {
ce.setDispatchState(node, Event.CAPTURING_PHASE);
} else {
aevt.setCurrentTarget(node);
}
fireEventListeners(node, e, true,
stoppedGroups, toBeStoppedGroups, isCustom);
stoppedGroups.addAll(toBeStoppedGroups);
toBeStoppedGroups.clear();
}
// AT_TARGET : fire local event listeners
if (isCustom) {
ce.setDispatchState(target, Event.AT_TARGET);
} else {
aevt.setEventPhase(Event.AT_TARGET);
aevt.setCurrentTarget(target);
}
fireEventListeners(target, e, false,
stoppedGroups, toBeStoppedGroups, isCustom);
stoppedGroups.addAll(toBeStoppedGroups);
toBeStoppedGroups.clear();
// BUBBLING_PHASE : fire event listeners from target to top
if (e.getBubbles()) {
if (!isCustom) {
aevt.setEventPhase(Event.BUBBLING_PHASE);
}
for (int i = ancestors.length - 1; i >= 0; i--) {
NodeEventTarget node = ancestors[i];
if (isCustom) {
ce.setDispatchState(node, Event.BUBBLING_PHASE);
} else {
aevt.setCurrentTarget(node);
}
fireEventListeners(node, e, false,
stoppedGroups, toBeStoppedGroups, isCustom);
stoppedGroups.addAll(toBeStoppedGroups);
toBeStoppedGroups.clear();
}
}
return isCustom ? ce.isDefaultPrevented() : aevt.isDefaultPrevented();
}
private static void fireEventListeners(NodeEventTarget node,
Event e,
boolean useCapture,
HashSet stoppedGroups,
HashSet toBeStoppedGroups,
boolean isCustom) {
String type = e.getType();
EventSupport support = node.getEventSupport();
// check if the event support has been instantiated
if (support == null) {
return;
}
EventListenerList list = support.getEventListeners(type, useCapture);
// check if the event listeners list is not empty
if (list == null) {
return;
}
// dump event listeners, we get the registered listeners NOW
EventListenerList.Entry[] listeners = list.getEventListeners();
// check if event listeners with the correct event type exist
if (listeners == null) {
return;
}
// fire event listeners
CustomEvent ce = null;
AbstractEvent aevt = null;
String eventNS;
if (isCustom) {
ce = (CustomEvent) e;
eventNS = ce.getNamespaceURI();
} else {
aevt = (AbstractEvent) e;
eventNS = aevt.getNamespaceURI();
}
for (int i = 0; i < listeners.length; i++) {
try {
String listenerNS = listeners[i].getNamespaceURI();
if (listenerNS != null && eventNS != null
&& !listenerNS.equals(eventNS)) {
continue;
}
Object group = listeners[i].getGroup();
if (!stoppedGroups.contains(group)) {
listeners[i].getListener().handleEvent(e);
if (isCustom) {
if (ce.isImmediatePropagationStopped()) {
stoppedGroups.add(group);
// XXX How to not stop other groups?
// Need something like
// aevt.stopImmediatePropagation(false);
} else if (ce.isPropagationStopped()) {
toBeStoppedGroups.add(group);
// XXX How to not stop other groups?
// Need something like aevt.stopPropagation(false);
}
} else {
if (aevt.getStopImmediatePropagation()) {
stoppedGroups.add(group);
aevt.stopImmediatePropagation(false);
} else if (aevt.getStopPropagation()) {
toBeStoppedGroups.add(group);
aevt.stopPropagation(false);
}
}
}
} catch (ThreadDeath td) {
throw td;
} catch (Throwable th) {
th.printStackTrace();
}
}
}
// Returns all ancestors of the specified node
private static NodeEventTarget [] getAncestors(NodeEventTarget node) {
node = node.getParentNodeEventTarget(); // skip current node
int nancestors = 0;
for (NodeEventTarget n = node;
n != null;
n = n.getParentNodeEventTarget(), nancestors++) {}
NodeEventTarget [] ancestors = new NodeEventTarget[nancestors];
for (int i=nancestors-1;
i >= 0;
--i, node = node.getParentNodeEventTarget()) {
ancestors[i] = node;
}
return ancestors;
}
/**
* Returns whether this node target has an event listener for the
* given event namespace URI and type.
*/
public boolean hasEventListenerNS(String namespaceURI, String type) {
if (capturingListeners != null) {
EventListenerList ell
= (EventListenerList) capturingListeners.get(type);
if (ell != null) {
if (ell.hasEventListener(namespaceURI)) {
return true;
}
}
}
if (bubblingListeners != null) {
EventListenerList ell
= (EventListenerList) capturingListeners.get(type);
if (ell != null) {
return ell.hasEventListener(namespaceURI);
}
}
return false;
}
/**
* Returns a list event listeners depending on the specified event
* type and phase.
* @param type the event type
* @param useCapture
*/
public EventListenerList getEventListeners(String type,
boolean useCapture) {
HashTable listeners
= useCapture ? capturingListeners : bubblingListeners;
if (listeners == null) {
return null;
}
return (EventListenerList) listeners.get(type);
}
/**
* Creates an EventException. Overrides this method if you need to
* create your own RangeException subclass.
* @param code the exception code
* @param key the resource key
* @param args arguments to use when formatting the message
*/
protected EventException createEventException(short code,
String key,
Object[] args) {
try {
AbstractDocument doc = (AbstractDocument) node.getOwnerDocument();
return new EventException(code, doc.formatMessage(key, args));
} catch (Exception e) {
return new EventException(code, key);
}
}
/**
* Wrapper class for {@org.w3c.dom.events.CustomEvent} objects.
*/
protected class WrappedEvent implements CustomEvent {
/**
* The wrapped event object.
*/
protected org.w3c.dom.events.CustomEvent e;
/**
* The getNamespaceURI method of the wrapped event object.
*/
protected Method getNamespaceURIMethod;
/**
* The stopImmediatePropagation method of the wrapped event object.
*/
protected Method stopImmediatePropagationMethod;
/**
* The isDefaultPrevented method of the wrapped event object.
*/
protected Method isDefaultPreventedMethod;
/**
* Creates a new WrappedEvent object.
*/
public WrappedEvent(org.w3c.dom.events.CustomEvent e) {
this.e = e;
Class cls = e.getClass();
try {
getNamespaceURIMethod = cls.getMethod("getNamespaceURI", null);
stopImmediatePropagationMethod
= cls.getMethod("stopImmediatePropagation", null);
isDefaultPreventedMethod
= cls.getMethod("isDefaultPrevented", null);
} catch (NoSuchMethodException nsme) {
throw createEventException
(DOMException.NOT_SUPPORTED_ERR,
"unsupported.event",
new Object[] {});
} catch (SecurityException se) {
throw createEventException
(DOMException.NOT_SUPPORTED_ERR,
"unsupported.event",
new Object[] {});
}
}
// Event (since DOM 2) ///////////////////////////////////////////////
/**
* Returns the type of this event.
*/
public String getType() {
return e.getType();
}
/**
* Returns the current target of this event.
*/
public EventTarget getCurrentTarget() {
return e.getCurrentTarget();
}
/**
* Returns the target of this event.
*/
public EventTarget getTarget() {
return e.getTarget();
}
/**
* Returns the current event phase of this event.
*/
public short getEventPhase() {
return e.getEventPhase();
}
/**
* Returns whether this event bubbles.
*/
public boolean getBubbles() {
return e.getBubbles();
}
/**
* Returns whether this event can be cancelled.
*/
public boolean getCancelable() {
return e.getCancelable();
}
/**
* Returns the timestamp of this event object.
*/
public long getTimeStamp() {
return e.getTimeStamp();
}
/**
* Stops propagation of this event.
*/
public void stopPropagation() {
e.stopPropagation();
}
/**
* Prevents default processing of the event.
*/
public void preventDefault() {
e.preventDefault();
}
/**
* Initializes this event object.
*/
public void initEvent(String eventTypeArg,
boolean canBubbleArg,
boolean cancelableArg) {
e.initEvent(eventTypeArg, canBubbleArg, cancelableArg);
}
// org.w3c.dom.events.CustomEvent ////////////////////////////////////
/**
* Sets the value of currentTarget and eventPhase.
*/
public void setDispatchState(EventTarget target, short phase) {
e.setDispatchState(target, phase);
}
/**
* Returns whether {@link #stopPropagation} has been called.
*/
public boolean isPropagationStopped() {
return e.isPropagationStopped();
}
/**
* Returns whether {@link #stopImmediatePropagation} has been called.
*/
public boolean isImmediatePropagationStopped() {
return e.isImmediatePropagationStopped();
}
// CustomEvent ///////////////////////////////////////////////////////
/**
* Returns the namespace URI of this custom event.
* @see org.w3c.dom.events.Event#getNamespaceURI
*/
public String getNamespaceURI() {
try {
return (String) getNamespaceURIMethod.invoke(e, null);
} catch (InvocationTargetException ite) {
ite.printStackTrace();
} catch (IllegalAccessException iae) {
iae.printStackTrace();
}
return null;
}
/**
* Indicates whether this object implements the
* {@link org.w3c.dom.events.CustomEvent} interface.
* This must return true for classes implementing this interface.
* @see org.w3c.dom.events.Event#isCustom
*/
public boolean isCustom() {
return true;
}
/**
* Stops event listeners of the same group being triggered.
* @see org.w3c.dom.events.Event#stopImmediatePropagation
*/
public void stopImmediatePropagation() {
try {
stopImmediatePropagationMethod.invoke(e, null);
} catch (InvocationTargetException ite) {
ite.printStackTrace();
} catch (IllegalAccessException iae) {
iae.printStackTrace();
}
}
/**
* Returns whether {@link #stopImmediatePropagation} has been called.
* @see org.w3c.dom.events.Event#isDefaultPrevented
*/
public boolean isDefaultPrevented() {
try {
return ((Boolean) isDefaultPreventedMethod.invoke(e, null))
.booleanValue();
} catch (InvocationTargetException ite) {
ite.printStackTrace();
} catch (IllegalAccessException iae) {
iae.printStackTrace();
}
return false;
}
/**
* Initializes this event object.
* @see org.w3c.dom.events.Event#initEventNS
*/
public void initEventNS(String namespaceURIArg,
String eventTypeArg,
boolean canBubbleArg,
boolean cancelableArg) {
// This method is not needed for event processing.
}
}
}