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