paulb@235 | 1 | #!/usr/bin/env python |
paulb@235 | 2 | |
paulb@235 | 3 | """ |
paulb@256 | 4 | DOM Level 3 Events support, with SVG Tiny 1.2 implementation additions. |
paulb@256 | 5 | See: http://www.w3.org/TR/DOM-Level-3-Events/events.html |
paulb@265 | 6 | See: http://www.w3.org/TR/xml-events/ |
paulb@235 | 7 | |
paulb@235 | 8 | Copyright (C) 2007 Paul Boddie <paul@boddie.org.uk> |
paulb@235 | 9 | |
paulb@235 | 10 | This library is free software; you can redistribute it and/or |
paulb@235 | 11 | modify it under the terms of the GNU Lesser General Public |
paulb@235 | 12 | License as published by the Free Software Foundation; either |
paulb@235 | 13 | version 2.1 of the License, or (at your option) any later version. |
paulb@235 | 14 | |
paulb@235 | 15 | This library is distributed in the hope that it will be useful, |
paulb@235 | 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
paulb@235 | 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
paulb@235 | 18 | Lesser General Public License for more details. |
paulb@235 | 19 | |
paulb@235 | 20 | You should have received a copy of the GNU Lesser General Public |
paulb@235 | 21 | License along with this library; if not, write to the Free Software |
paulb@235 | 22 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
paulb@235 | 23 | """ |
paulb@235 | 24 | |
paulb@262 | 25 | import xml.dom |
paulb@256 | 26 | import time |
paulb@256 | 27 | |
paulb@256 | 28 | XML_EVENTS_NAMESPACE = "http://www.w3.org/2001/xml-events" |
paulb@256 | 29 | |
paulb@235 | 30 | class EventException(Exception): |
paulb@235 | 31 | |
paulb@235 | 32 | UNSPECIFIED_EVENT_TYPE_ERR = 0 |
paulb@235 | 33 | DISPATCH_REQUEST_ERR = 1 |
paulb@235 | 34 | |
paulb@256 | 35 | class DocumentEvent: |
paulb@256 | 36 | |
paulb@262 | 37 | """ |
paulb@262 | 38 | An event interface supportable by documents. |
paulb@262 | 39 | See: http://www.w3.org/TR/DOM-Level-3-Events/events.html#Events-DocumentEvent |
paulb@262 | 40 | """ |
paulb@256 | 41 | |
paulb@256 | 42 | def canDispatch(self, namespaceURI, type): |
paulb@262 | 43 | return namespaceURI is None and event_types.has_key(type) |
paulb@256 | 44 | |
paulb@256 | 45 | def createEvent(self, eventType): |
paulb@262 | 46 | try: |
paulb@262 | 47 | return event_types[eventType]() |
paulb@262 | 48 | except KeyError: |
paulb@262 | 49 | raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR) |
paulb@256 | 50 | |
paulb@235 | 51 | class Event: |
paulb@235 | 52 | |
paulb@265 | 53 | """ |
paulb@265 | 54 | An event class. |
paulb@265 | 55 | See: http://www.w3.org/TR/SVGMobile12/svgudom.html#events__Event |
paulb@265 | 56 | See: http://www.w3.org/TR/DOM-Level-3-Events/events.html#Events-Event |
paulb@265 | 57 | """ |
paulb@256 | 58 | |
paulb@235 | 59 | CAPTURING_PHASE = 1 |
paulb@235 | 60 | AT_TARGET = 2 |
paulb@235 | 61 | BUBBLING_PHASE = 3 |
paulb@235 | 62 | |
paulb@262 | 63 | def __init__(self): |
paulb@256 | 64 | |
paulb@256 | 65 | "Initialise the event." |
paulb@256 | 66 | |
paulb@256 | 67 | # Initialised later: |
paulb@256 | 68 | |
paulb@262 | 69 | self.target = None |
paulb@262 | 70 | self.currentTarget = None |
paulb@262 | 71 | self.defaultPrevented = 0 |
paulb@256 | 72 | self.type = None |
paulb@256 | 73 | self.namespaceURI = None |
paulb@256 | 74 | |
paulb@256 | 75 | # DOM Level 3 Events: |
paulb@256 | 76 | |
paulb@265 | 77 | self.bubbles = 1 |
paulb@265 | 78 | self.eventPhase = self.AT_TARGET # permits direct invocation of dispatchEvent |
paulb@256 | 79 | self.timeStamp = time.time() |
paulb@235 | 80 | |
paulb@265 | 81 | # Propagation flags: |
paulb@265 | 82 | |
paulb@265 | 83 | self.stop = 0 |
paulb@265 | 84 | self.stop_now = 0 |
paulb@265 | 85 | |
paulb@235 | 86 | def initEvent(self, eventTypeArg, canBubbleArg, cancelableArg): |
paulb@256 | 87 | self.initEventNS(None, eventTypeArg, canBubbleArg, cancelableArg) |
paulb@235 | 88 | |
paulb@235 | 89 | def initEventNS(self, namespaceURIArg, eventTypeArg, canBubbleArg, cancelableArg): |
paulb@256 | 90 | self.namespaceURI = namespaceURIArg |
paulb@256 | 91 | self.type = eventTypeArg |
paulb@256 | 92 | self.bubbles = canBubbleArg |
paulb@256 | 93 | self.cancelable = cancelableArg |
paulb@235 | 94 | |
paulb@235 | 95 | def preventDefault(self): |
paulb@256 | 96 | self.defaultPrevented = 1 |
paulb@235 | 97 | |
paulb@235 | 98 | def stopPropagation(self): |
paulb@265 | 99 | self.stop = 1 |
paulb@235 | 100 | |
paulb@235 | 101 | def stopImmediatePropagation(self): |
paulb@265 | 102 | self.stop = 1 |
paulb@265 | 103 | self.stop_now = 1 |
paulb@235 | 104 | |
paulb@262 | 105 | class UIEvent(Event): |
paulb@262 | 106 | |
paulb@262 | 107 | "A user interface event." |
paulb@262 | 108 | |
paulb@262 | 109 | def __init__(self): |
paulb@262 | 110 | Event.__init__(self) |
paulb@262 | 111 | self.detail = None |
paulb@262 | 112 | |
paulb@262 | 113 | class MouseEvent(UIEvent): |
paulb@262 | 114 | |
paulb@262 | 115 | "A mouse-related event." |
paulb@262 | 116 | |
paulb@262 | 117 | def __init__(self): |
paulb@262 | 118 | Event.__init__(self) |
paulb@262 | 119 | self.screenX, self.screenY, self.clientX, self.clientY, self.button = None, None, None, None, None |
paulb@262 | 120 | |
paulb@262 | 121 | # Event types registry. |
paulb@262 | 122 | |
paulb@262 | 123 | event_types = { |
paulb@262 | 124 | "Event" : Event, |
paulb@262 | 125 | "UIEvent" : UIEvent, |
paulb@262 | 126 | "MouseEvent" : MouseEvent |
paulb@262 | 127 | } |
paulb@262 | 128 | |
paulb@235 | 129 | class EventTarget: |
paulb@235 | 130 | |
paulb@265 | 131 | """ |
paulb@265 | 132 | An event target class. |
paulb@265 | 133 | See: http://www.w3.org/TR/SVGMobile12/svgudom.html#events__EventTarget |
paulb@265 | 134 | See: http://www.w3.org/TR/DOM-Level-3-Events/events.html#Events-EventTarget |
paulb@265 | 135 | |
paulb@265 | 136 | The listeners for a node are accessed through the global object. This common |
paulb@265 | 137 | collection is consequently accessed by all nodes in a document, meaning that |
paulb@265 | 138 | distinct objects representing the same node can still obtain the set of |
paulb@265 | 139 | listeners registered for that node. In contrast, any attempt to directly |
paulb@265 | 140 | store listeners on particular objects would result in the specific object |
paulb@265 | 141 | which registered the listeners holding the record of such objects, whereas |
paulb@265 | 142 | other objects obtained independently for the same node would hold no such |
paulb@265 | 143 | record. |
paulb@265 | 144 | """ |
paulb@235 | 145 | |
paulb@235 | 146 | def addEventListener(self, type, listener, useCapture): |
paulb@265 | 147 | |
paulb@265 | 148 | """ |
paulb@265 | 149 | For the given event 'type', register the given 'listener' for events in |
paulb@265 | 150 | the capture phase if 'useCapture' is a true value, or for events in the |
paulb@265 | 151 | target and bubble phases otherwise. |
paulb@265 | 152 | """ |
paulb@265 | 153 | |
paulb@235 | 154 | self.addEventListenerNS(None, type, listener, useCapture) |
paulb@235 | 155 | |
paulb@262 | 156 | def addEventListenerNS(self, namespaceURI, type, listener, useCapture, group=None): # NOTE: group ignored |
paulb@265 | 157 | |
paulb@265 | 158 | """ |
paulb@265 | 159 | For the given 'namespaceURI' and event 'type', register the given |
paulb@265 | 160 | 'listener' for events in the capture phase if 'useCapture' is a true |
paulb@265 | 161 | value, or for events in the target and bubble phases otherwise. |
paulb@265 | 162 | """ |
paulb@265 | 163 | |
paulb@262 | 164 | listeners = self.ownerDocument.global_.listeners |
paulb@262 | 165 | if not listeners.has_key(self): |
paulb@262 | 166 | listeners[self] = {} |
paulb@262 | 167 | if not listeners[self].has_key((namespaceURI, type)): |
paulb@262 | 168 | listeners[self][(namespaceURI, type)] = [] |
paulb@262 | 169 | listeners[self][(namespaceURI, type)].append((listener, useCapture)) |
paulb@235 | 170 | |
paulb@235 | 171 | def dispatchEvent(self, evt): |
paulb@265 | 172 | |
paulb@265 | 173 | "For this node, dispatch event 'evt' to the registered listeners." |
paulb@265 | 174 | |
paulb@262 | 175 | listeners = self.ownerDocument.global_.listeners |
paulb@235 | 176 | if not evt.type: |
paulb@235 | 177 | raise EventException(EventException.UNSPECIFIED_EVENT_TYPE_ERR) |
paulb@265 | 178 | |
paulb@265 | 179 | # Determine the phase and define the current target (this node) for the |
paulb@265 | 180 | # use of listeners. |
paulb@265 | 181 | |
paulb@265 | 182 | capturing = evt.eventPhase == evt.CAPTURING_PHASE |
paulb@265 | 183 | evt.currentTarget = self |
paulb@265 | 184 | |
paulb@256 | 185 | # Dispatch on namespaceURI, type. |
paulb@265 | 186 | |
paulb@262 | 187 | for listener, useCapture in listeners.get(self, {}).get((evt.namespaceURI, evt.type), []): |
paulb@265 | 188 | |
paulb@265 | 189 | # Detect requests to stop propagation immediately. |
paulb@265 | 190 | |
paulb@265 | 191 | if evt.stop_now: |
paulb@265 | 192 | break |
paulb@265 | 193 | |
paulb@265 | 194 | # Dispatch the event to the appropriate listeners according to the |
paulb@265 | 195 | # phase. |
paulb@265 | 196 | |
paulb@265 | 197 | if capturing and useCapture or not capturing and not useCapture: |
paulb@265 | 198 | listener.handleEvent(evt) |
paulb@265 | 199 | |
paulb@256 | 200 | return evt.defaultPrevented |
paulb@235 | 201 | |
paulb@235 | 202 | def removeEventListener(self, type, listener, useCapture): |
paulb@265 | 203 | |
paulb@265 | 204 | """ |
paulb@265 | 205 | For the given event 'type', deregister the given 'listener' for events |
paulb@265 | 206 | in the capture phase if 'useCapture' is a true value, or for events in |
paulb@265 | 207 | the target and bubble phases otherwise. |
paulb@265 | 208 | """ |
paulb@265 | 209 | |
paulb@235 | 210 | self.removeEventListenerNS(None, type, listener, useCapture) |
paulb@235 | 211 | |
paulb@235 | 212 | def removeEventListenerNS(self, namespaceURI, type, listener, useCapture): |
paulb@265 | 213 | |
paulb@265 | 214 | """ |
paulb@265 | 215 | For the given 'namespaceURI' and event 'type', deregister the given |
paulb@265 | 216 | 'listener' for events in the capture phase if 'useCapture' is a true |
paulb@265 | 217 | value, or for events in the target and bubble phases otherwise. |
paulb@265 | 218 | """ |
paulb@265 | 219 | |
paulb@262 | 220 | listeners = self.ownerDocument.global_.listeners |
paulb@262 | 221 | if listeners.has_key(self) and listeners[self].has_key((namespaceURI, type)): |
paulb@235 | 222 | try: |
paulb@262 | 223 | listeners[self][(namespaceURI, type)].remove((listener, useCapture)) |
paulb@235 | 224 | except ValueError: |
paulb@235 | 225 | pass |
paulb@235 | 226 | |
paulb@265 | 227 | # NOTE: The specification doesn't say much about the event system, but we will |
paulb@265 | 228 | # NOTE: provide a class to manage the different phases. This is mixed into the |
paulb@265 | 229 | # NOTE: SVGDocument class (and potentially other classes in future). |
paulb@265 | 230 | |
paulb@265 | 231 | class EventSystem: |
paulb@265 | 232 | |
paulb@265 | 233 | "An event system which manages the different DOM event flow phases." |
paulb@265 | 234 | |
paulb@265 | 235 | def sendEventToTarget(self, evt, target): |
paulb@265 | 236 | |
paulb@265 | 237 | "Send event 'evt' to the specified 'target' element." |
paulb@265 | 238 | |
paulb@265 | 239 | # Determine the path of the event. |
paulb@265 | 240 | |
paulb@265 | 241 | bubble_route = target.xpath("ancestor::*") |
paulb@265 | 242 | capture_route = bubble_route[:] |
paulb@265 | 243 | capture_route.reverse() |
paulb@265 | 244 | |
paulb@265 | 245 | # Initialise the target and execute the capture phase. |
paulb@265 | 246 | |
paulb@265 | 247 | evt.target = target |
paulb@265 | 248 | evt.eventPhase = evt.CAPTURING_PHASE |
paulb@265 | 249 | for element in capture_route: |
paulb@265 | 250 | if evt.stop: |
paulb@265 | 251 | break |
paulb@265 | 252 | element.dispatchEvent(evt) |
paulb@265 | 253 | |
paulb@265 | 254 | # Execute the target phase. |
paulb@265 | 255 | |
paulb@265 | 256 | evt.eventPhase = evt.AT_TARGET |
paulb@265 | 257 | if not evt.stop: |
paulb@265 | 258 | target.dispatchEvent(evt) |
paulb@265 | 259 | |
paulb@265 | 260 | # Execute the bubble phase, if appropriate. |
paulb@265 | 261 | |
paulb@265 | 262 | evt.eventPhase = evt.BUBBLING_PHASE |
paulb@265 | 263 | if evt.bubbles: |
paulb@265 | 264 | for element in bubble_route: |
paulb@265 | 265 | if evt.stop: |
paulb@265 | 266 | break |
paulb@265 | 267 | element.dispatchEvent(evt) |
paulb@265 | 268 | |
paulb@235 | 269 | # vim: tabstop=4 expandtab shiftwidth=4 |